Browse Source

Add live AV1 intra mode selection

pull/2633/head
James Jackson-South 1 month ago
parent
commit
2c86883984
  1. 4
      HEIF_IMPLEMENTATION_PLAN.md
  2. 24
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs
  3. 397
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  4. 120
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
  5. 95
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs
  6. 81
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.Operator.cs
  7. 18
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs
  8. 64
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1ResidualBuilder.cs
  9. 220
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
  10. 48
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs
  11. 168
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  12. 13
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  13. 273
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
  14. 24
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ResidualBuilderTests.cs
  15. 83
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

4
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. - [ ] 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. - [~] 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. - [ ] 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. - [~] 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. - [~] Encoder mode information now uses a frame-owned integer alias grid over a packed 8-byte value allocation, matching current libaom's `mi_grid_base` and `mi_alloc` relationship without a managed object or reference per 4x4 entry. The visible dimensions are aligned to eight luma samples, the grid stride and allocated row count are aligned to 32 mode-information units, and optional 8x8 allocation granularity reduces the value store in both dimensions exactly as current libaom does. One clean ImageSharp byte owner contains both independently typed regions, reducing libaom's two allocation lifetimes to one without a copy. At 4K, the 4x4 layout occupies about 6.0 MiB in total; the 8x8 layout occupies about 3.0 MiB. Exact geometry, clean allocation, typed lengths, aligned mapping, untouched row padding, and exactly-once return pass 4 of 4 direct net11 VSTest cases in Release. Every coded 4x4 cell covered by square, rectangular, or clipped edge blocks maps to its owning allocation entry before context-dependent symbols are written. Packed syntax, relative neighbor lookup, full block mapping, writer traversal, entropy, and OBU coverage pass 1,947 of 1,947 direct net11 VSTest cases in Release; complete mode decision still remains.
- [~] The final-block decision workspace uses one reusable 10.3 KiB ImageSharp allocator owner. It contains 1,024 explicitly packed 10-byte final-block entries and the 341 preorder partition bytes required by a complete 128x128-through-8x8 quadtree, replacing separate managed arrays. Construction and the explicit per-superblock reset initialize every syntax field, including the nonzero sentinel that disables filter-intra prediction; pooled palette, quantizer, prediction, and partition bytes cannot leak into the next decision pass. Exact allocation, size, initialization, reset, return, repeated-run, writer, entropy, and OBU coverage pass 1,957 of 1,957 direct net11 VSTest cases in Release; complete mode decision still remains. - [~] The final-block decision workspace uses one reusable 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. - [~] 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. - [ ] 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 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. - [~] 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. - [~] 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. - [~] 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.

24
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary> /// <summary>
@ -8,6 +10,28 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// </summary> /// </summary>
internal static class Av1RateDistortion internal static class Av1RateDistortion
{ {
/// <summary>
/// Gets the key-frame rate multiplier for an AV1 quantizer and sample precision.
/// </summary>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The rate multiplier.</returns>
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);
}
/// <summary> /// <summary>
/// Gets a rate-distortion cost using the encoder probability-cost precision. /// Gets a rate-distortion cost using the encoder probability-cost precision.
/// </summary> /// </summary>

397
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;
/// <content>
/// Provides live-probability final-block mode decisions for intra encoding.
/// </content>
internal static partial class Av1IntraSuperblockEncoder
{
/// <summary>
/// Builds the fixed 8x8 partition skeleton consumed by interleaved mode decision and tile writing.
/// </summary>
/// <param name="picture">The frame coding and mode-information state.</param>
/// <param name="superblock">The reusable partition and final-block decisions.</param>
/// <param name="superblockOrigin">The absolute luma-sample origin of the superblock.</param>
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);
}
/// <summary>
/// Produces one final block at a time against the tile state immediately preceding its syntax.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
/// <typeparam name="TOperator">The type-specific block encoding operations.</typeparam>
internal struct ModeDecision<TSample, TOperator> : Av1TileWriter.IBlockEncodingHandler
where TSample : unmanaged
where TOperator : struct, IBlockEncodingOperator<TSample>
{
private readonly Av1EncoderFrame<TSample>.PlanarView source;
private readonly Av1EncoderFrame<TSample>.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;
/// <summary>
/// Initializes a new instance of the <see cref="ModeDecision{TSample, TOperator}"/> struct.
/// </summary>
/// <param name="source">The coded source frame.</param>
/// <param name="reconstruction">The reconstructed frame updated by winning candidates.</param>
/// <param name="picture">The frame coding and mode-information state.</param>
/// <param name="superblock">The current superblock.</param>
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
public ModeDecision(
Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> 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;
}
/// <inheritdoc/>
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<int> lumaCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.Y);
Span<Av1EncoderTransformBlockState> 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<int> blueCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.U);
Span<int> redCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.V);
Span<Av1EncoderTransformBlockState> blueTransformBlocks =
this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.U);
Span<Av1EncoderTransformBlockState> 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<TSample, TOperator>(
this.source,
this.reconstruction,
this.blockWorkspace,
this.quantization,
this.bitDepth,
Av1Plane.U,
chromaOrigin,
chromaTransformSize,
blueCoefficients[this.codedAreaChroma..],
ref blueState);
EncodePlaneBlock<TSample, TOperator>(
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<int> retainedCoefficients,
ref Av1EncoderTransformBlockState retainedState)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
const int SampleCount = 8 * 8;
Buffer2DRegion<TSample> sourcePlane = this.source.GetPlane(Av1Plane.Y);
Buffer2DRegion<TSample> reconstructionPlane = this.reconstruction.GetPlane(Av1Plane.Y);
bool hasLeft = macroBlock.IsLeftAvailable;
bool hasAbove = macroBlock.IsUpAvailable;
ReadOnlySpan<TSample> above = hasAbove
? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, 8)
: [];
Span<TSample> 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<TSample> candidateReconstruction = stackalloc TSample[SampleCount];
Span<int> 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<TSample> sourcePlane,
Point blockOrigin,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
Av1TransformBlockContext blockContext,
Span<TSample> candidateReconstruction,
Span<int> 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<TSample> candidateReconstruction,
ReadOnlySpan<int> candidateCoefficients,
Buffer2DRegion<TSample> reconstructionPlane,
Point blockOrigin,
Span<int> 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;
}
}
}

120
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
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;
@ -17,7 +18,7 @@ internal static partial class Av1IntraSuperblockEncoder
/// Defines type-specific block encoding without coupling traversal to sample storage width. /// Defines type-specific block encoding without coupling traversal to sample storage width.
/// </summary> /// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam> /// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
private interface IBlockEncodingOperator<TSample> internal interface IBlockEncodingOperator<TSample>
where TSample : unmanaged where TSample : unmanaged
{ {
/// <summary> /// <summary>
@ -64,12 +65,50 @@ internal static partial class Av1IntraSuperblockEncoder
Av1Plane plane, Av1Plane plane,
Av1BitDepth bitDepth, Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state); ref Av1EncoderTransformBlockState state);
/// <summary>
/// Encodes one luma candidate into contiguous decision scratch.
/// </summary>
/// <param name="workspace">The reusable block workspace.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="above">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The transform dimensions.</param>
/// <param name="qIndex">The effective segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The candidate transform state.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static abstract long EncodeCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<TSample> source,
Point blockOrigin,
Span<TSample> reconstruction,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
} }
/// <summary> /// <summary>
/// Encodes blocks stored as eight-bit samples. /// Encodes blocks stored as eight-bit samples.
/// </summary> /// </summary>
private readonly struct ByteOperator : IBlockEncodingOperator<byte> internal readonly struct ByteOperator : IBlockEncodingOperator<byte>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public static Span<byte> GetLeftReference(Span<short> residual, int length) public static Span<byte> GetLeftReference(Span<short> residual, int length)
@ -110,12 +149,49 @@ internal static partial class Av1IntraSuperblockEncoder
acDeltaQ, acDeltaQ,
plane, plane,
ref state); ref state);
/// <inheritdoc/>
public static long EncodeCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
Span<byte> reconstruction,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
Span<int> 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);
} }
/// <summary> /// <summary>
/// Encodes blocks stored as high-bit-depth samples. /// Encodes blocks stored as high-bit-depth samples.
/// </summary> /// </summary>
private readonly struct UInt16Operator : IBlockEncodingOperator<ushort> internal readonly struct UInt16Operator : IBlockEncodingOperator<ushort>
{ {
/// <inheritdoc/> /// <inheritdoc/>
public static Span<ushort> GetLeftReference(Span<short> residual, int length) public static Span<ushort> GetLeftReference(Span<short> residual, int length)
@ -157,5 +233,43 @@ internal static partial class Av1IntraSuperblockEncoder
plane, plane,
bitDepth, bitDepth,
ref state); ref state);
/// <inheritdoc/>
public static long EncodeCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
Span<ushort> reconstruction,
ReadOnlySpan<ushort> above,
ReadOnlySpan<ushort> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
Span<int> 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);
} }
} }

95
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs

@ -77,6 +77,59 @@ internal static partial class Av1IntraSuperblockEncoder
traversal.EncodePartitionTree(superblockOrigin, picture.Sequence.SequenceHeader.SuperblockSize); traversal.EncodePartitionTree(superblockOrigin, picture.Sequence.SequenceHeader.SuperblockSize);
} }
private static void EncodePlaneBlock<TSample, TOperator>(
Av1EncoderFrame<TSample>.PlanarView source,
Av1EncoderFrame<TSample>.PlanarView reconstruction,
Av1EncoderBlockWorkspace blockWorkspace,
ObuQuantizationParameters quantization,
Av1BitDepth bitDepth,
Av1Plane plane,
Point blockOrigin,
Av1TransformSize transformSize,
Span<int> coefficients,
ref Av1EncoderTransformBlockState state)
where TSample : unmanaged
where TOperator : struct, IBlockEncodingOperator<TSample>
{
Buffer2DRegion<TSample> sourcePlane = source.GetPlane(plane);
Buffer2DRegion<TSample> reconstructionPlane = reconstruction.GetPlane(plane);
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
bool hasLeft = blockOrigin.X > 0;
bool hasAbove = blockOrigin.Y > 0;
ReadOnlySpan<TSample> above = hasAbove
? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, width)
: [];
Span<TSample> 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);
}
/// <summary> /// <summary>
/// Retains the stack-only state shared by recursive partition and final-block traversal. /// Retains the stack-only state shared by recursive partition and final-block traversal.
/// </summary> /// </summary>
@ -237,44 +290,16 @@ internal static partial class Av1IntraSuperblockEncoder
Av1TransformSize transformSize, Av1TransformSize transformSize,
Span<int> coefficients, Span<int> coefficients,
ref Av1EncoderTransformBlockState state) ref Av1EncoderTransformBlockState state)
{ => Av1IntraSuperblockEncoder.EncodePlaneBlock<TSample, TOperator>(
Buffer2DRegion<TSample> sourcePlane = this.source.GetPlane(plane); this.source,
Buffer2DRegion<TSample> reconstructionPlane = this.reconstruction.GetPlane(plane); this.reconstruction,
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
bool hasLeft = blockOrigin.X > 0;
bool hasAbove = blockOrigin.Y > 0;
ReadOnlySpan<TSample> above = hasAbove
? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, width)
: [];
Span<TSample> 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(
this.blockWorkspace, this.blockWorkspace,
sourcePlane, this.quantization,
reconstructionPlane, this.bitDepth,
plane,
blockOrigin, blockOrigin,
above,
left,
hasLeft,
hasAbove,
coefficients,
transformSize, transformSize,
this.quantization.QIndex[0], coefficients,
this.quantization.DeltaQDc[(int)plane],
this.quantization.DeltaQAc[(int)plane],
plane,
this.bitDepth,
ref state); ref state);
}
} }
} }

81
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.Operator.cs

@ -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;
/// <content>
/// Defines the sample-storage operations used by single-tile intra encoding.
/// </content>
internal sealed partial class Av1IntraTileWriter
{
/// <summary>
/// Defines type-specific superblock encoding without coupling tile traversal to sample storage width.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
private interface ITileEncodingOperator<TSample>
where TSample : unmanaged
{
/// <summary>
/// Encodes and reconstructs one superblock.
/// </summary>
/// <param name="source">The coded source frame.</param>
/// <param name="reconstruction">The reconstructed frame updated by the block transforms.</param>
/// <param name="picture">The frame coding and mode-information state.</param>
/// <param name="superblock">The reusable partition and final-block decisions.</param>
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
public static abstract void EncodeSuperblock(
Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> reconstruction,
Av1PictureControlSet picture,
Av1Superblock superblock,
Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderBlockWorkspace blockWorkspace);
}
/// <summary>
/// Encodes tiles stored as eight-bit samples.
/// </summary>
private readonly struct ByteOperator : ITileEncodingOperator<byte>
{
/// <inheritdoc/>
public static void EncodeSuperblock(
Av1EncoderFrame<byte> source,
Av1EncoderFrame<byte> reconstruction,
Av1PictureControlSet picture,
Av1Superblock superblock,
Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderBlockWorkspace blockWorkspace)
=> Av1IntraSuperblockEncoder.Encode(
source,
reconstruction,
picture,
superblock,
coefficientBuffer,
blockWorkspace);
}
/// <summary>
/// Encodes tiles stored as high-bit-depth samples.
/// </summary>
private readonly struct UInt16Operator : ITileEncodingOperator<ushort>
{
/// <inheritdoc/>
public static void EncodeSuperblock(
Av1EncoderFrame<ushort> source,
Av1EncoderFrame<ushort> reconstruction,
Av1PictureControlSet picture,
Av1Superblock superblock,
Av1EncoderCoefficientBuffer coefficientBuffer,
Av1EncoderBlockWorkspace blockWorkspace)
=> Av1IntraSuperblockEncoder.Encode(
source,
reconstruction,
picture,
superblock,
coefficientBuffer,
blockWorkspace);
}
}

18
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs

@ -37,7 +37,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderBlockWorkspace blockWorkspace, Av1EncoderBlockWorkspace blockWorkspace,
int initialSize) int initialSize)
{ {
this.tileData = Encode<byte, ByteOperator>( this.tileData = Encode<byte, Av1IntraSuperblockEncoder.ByteOperator>(
configuration, configuration,
source, source,
reconstruction, reconstruction,
@ -70,7 +70,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderBlockWorkspace blockWorkspace, Av1EncoderBlockWorkspace blockWorkspace,
int initialSize) int initialSize)
{ {
this.tileData = Encode<ushort, UInt16Operator>( this.tileData = Encode<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(
configuration, configuration,
source, source,
reconstruction, reconstruction,
@ -109,7 +109,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
int initialSize, int initialSize,
out int tileDataLength) out int tileDataLength)
where TSample : unmanaged where TSample : unmanaged
where TOperator : struct, ITileEncodingOperator<TSample> where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample>
{ {
ObuFrameHeader frameHeader = picture.Parent.FrameHeader; ObuFrameHeader frameHeader = picture.Parent.FrameHeader;
ObuSequenceHeader sequenceHeader = picture.Sequence.SequenceHeader; ObuSequenceHeader sequenceHeader = picture.Sequence.SequenceHeader;
@ -151,9 +151,12 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
modeInfoColumn << Av1Constants.ModeInfoSizeLog2, modeInfoColumn << Av1Constants.ModeInfoSizeLog2,
modeInfoRow << Av1Constants.ModeInfoSizeLog2); modeInfoRow << Av1Constants.ModeInfoSizeLog2);
// Analyze immediately before entropy coding so the reusable decision workspace and reconstructed Av1IntraSuperblockEncoder.Prepare(
// neighbors remain synchronized without a second superblock-sized decision allocation. picture,
TOperator.EncodeSuperblock( superblock,
entropyContext.SuperblockOrigin);
Av1IntraSuperblockEncoder.ModeDecision<TSample, TOperator> blockEncoder = new(
source, source,
reconstruction, reconstruction,
picture, picture,
@ -167,7 +170,8 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
writer, writer,
superblock, superblock,
coefficientBuffer, coefficientBuffer,
TileIndex); TileIndex,
ref blockEncoder);
} }
} }

64
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1ResidualBuilder.cs

@ -56,6 +56,70 @@ internal static partial class Av1ResidualBuilder
int height) int height)
=> Subtract<ushort, UInt16Operator>(source, sourceStride, prediction, predictionStride, residual, residualStride, width, height); => Subtract<ushort, UInt16Operator>(source, sourceStride, prediction, predictionStride, residual, residualStride, width, height);
/// <summary>
/// Sums the squares of a contiguous signed residual block.
/// </summary>
/// <param name="residual">The residual samples.</param>
/// <returns>The exact sum of squared sample differences.</returns>
public static long SumSquares(ReadOnlySpan<short> 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<short>();
for (; vectorCount > 0; vectorCount--, offset += Vector512<short>.Count)
{
Vector512<short> values = Unsafe.As<short, Vector512<short>>(ref Unsafe.Add(ref residualBase, offset));
Vector512<int> lower = Vector512.WidenLower(values);
Vector512<int> upper = Vector512.WidenUpper(values);
sum += Vector512.Sum(lower * lower);
sum += Vector512.Sum(upper * upper);
}
}
if (Vector256.IsHardwareAccelerated)
{
nuint vectorCount = residual[offset..].Vector256Count<short>();
for (; vectorCount > 0; vectorCount--, offset += Vector256<short>.Count)
{
Vector256<short> values = Unsafe.As<short, Vector256<short>>(ref Unsafe.Add(ref residualBase, offset));
Vector256<int> lower = Vector256.WidenLower(values);
Vector256<int> upper = Vector256.WidenUpper(values);
sum += Vector256.Sum(lower * lower);
sum += Vector256.Sum(upper * upper);
}
}
if (Vector128.IsHardwareAccelerated)
{
nuint vectorCount = residual[offset..].Vector128Count<short>();
for (; vectorCount > 0; vectorCount--, offset += Vector128<short>.Count)
{
Vector128<short> values = Unsafe.As<short, Vector128<short>>(ref Unsafe.Add(ref residualBase, offset));
Vector128<int> lower = Vector128.WidenLower(values);
Vector128<int> 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<TSample, TOperator>( private static void Subtract<TSample, TOperator>(
ReadOnlySpan<TSample> source, ReadOnlySpan<TSample> source,
int sourceStride, int sourceStride,

220
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs

@ -55,7 +55,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<byte> sourceSamples = GetPlaneSpan(source, blockOrigin); ReadOnlySpan<byte> sourceSamples = GetPlaneSpan(source, blockOrigin);
Span<byte> reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin); Span<byte> reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin);
EncodeIntraDcLossyContiguous( EncodeIntraLossyContiguous(
workspace, workspace,
sourceSamples, sourceSamples,
source.Stride, source.Stride,
@ -65,6 +65,7 @@ internal static class Av1TransformBlockEncoder
left, left,
hasLeft, hasLeft,
hasAbove, hasAbove,
Av1PredictionMode.DC,
quantizedCoefficients, quantizedCoefficients,
transformSize, transformSize,
transformType, transformType,
@ -75,6 +76,83 @@ internal static class Av1TransformBlockEncoder
ref state); ref state);
} }
/// <summary>
/// Encodes one eight-bit intra candidate into contiguous decision scratch.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="above">The contiguous top reference samples.</param>
/// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="state">The candidate transform type and end-of-block syntax.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static long EncodeIntraLossyCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
Span<byte> reconstruction,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
Span<int> 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<byte> 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;
}
/// <summary> /// <summary>
/// Encodes and reconstructs one high-bit-depth lossy DC intra block in contiguous encoder planes. /// Encodes and reconstructs one high-bit-depth lossy DC intra block in contiguous encoder planes.
/// </summary> /// </summary>
@ -117,7 +195,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<ushort> sourceSamples = GetPlaneSpan(source, blockOrigin); ReadOnlySpan<ushort> sourceSamples = GetPlaneSpan(source, blockOrigin);
Span<ushort> reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin); Span<ushort> reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin);
EncodeIntraDcLossyContiguous( EncodeIntraLossyContiguous(
workspace, workspace,
sourceSamples, sourceSamples,
source.Stride, source.Stride,
@ -127,6 +205,7 @@ internal static class Av1TransformBlockEncoder
left, left,
hasLeft, hasLeft,
hasAbove, hasAbove,
Av1PredictionMode.DC,
quantizedCoefficients, quantizedCoefficients,
transformSize, transformSize,
transformType, transformType,
@ -139,7 +218,93 @@ internal static class Av1TransformBlockEncoder
} }
/// <summary> /// <summary>
/// Encodes and reconstructs one eight-bit lossy DC intra block. /// Encodes one high-bit-depth intra candidate into contiguous decision scratch.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="above">The contiguous top reference samples.</param>
/// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The candidate transform type and end-of-block syntax.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static long EncodeIntraLossyCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
Span<ushort> reconstruction,
ReadOnlySpan<ushort> above,
ReadOnlySpan<ushort> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
Span<int> 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<ushort> 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;
}
/// <summary>
/// Encodes and reconstructs one eight-bit lossy intra block.
/// </summary> /// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param> /// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The source samples.</param> /// <param name="source">The source samples.</param>
@ -150,6 +315,7 @@ internal static class Av1TransformBlockEncoder
/// <param name="left">The contiguous left reference samples.</param> /// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param> /// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param> /// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param> /// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param> /// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param> /// <param name="transformType">The selected compound transform type.</param>
@ -158,7 +324,7 @@ internal static class Av1TransformBlockEncoder
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param> /// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param> /// <param name="plane">The component plane containing the block.</param>
/// <param name="state">The retained transform type and end-of-block syntax.</param> /// <param name="state">The retained transform type and end-of-block syntax.</param>
private static void EncodeIntraDcLossyContiguous( private static void EncodeIntraLossyContiguous(
Av1EncoderBlockWorkspace workspace, Av1EncoderBlockWorkspace workspace,
ReadOnlySpan<byte> source, ReadOnlySpan<byte> source,
int sourceStride, int sourceStride,
@ -168,6 +334,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<byte> left, ReadOnlySpan<byte> left,
bool hasLeft, bool hasLeft,
bool hasAbove, bool hasAbove,
Av1PredictionMode mode,
Span<int> quantizedCoefficients, Span<int> quantizedCoefficients,
Av1TransformSize transformSize, Av1TransformSize transformSize,
Av1TransformType transformType, Av1TransformType transformType,
@ -181,7 +348,16 @@ internal static class Av1TransformBlockEncoder
int height = transformSize.GetHeight(); int height = transformSize.GetHeight();
// Prediction and subtraction stay in their SIMD-first operators while this method owns the required block-stage ordering. // 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); Av1ResidualBuilder.Subtract(source, sourceStride, reconstruction, reconstructionStride, workspace.Residual, width, width, height);
EncodeLossy( EncodeLossy(
@ -212,7 +388,7 @@ internal static class Av1TransformBlockEncoder
} }
/// <summary> /// <summary>
/// Encodes and reconstructs one high-bit-depth lossy DC intra block. /// Encodes and reconstructs one high-bit-depth lossy intra block.
/// </summary> /// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param> /// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The source samples.</param> /// <param name="source">The source samples.</param>
@ -223,6 +399,7 @@ internal static class Av1TransformBlockEncoder
/// <param name="left">The contiguous left reference samples.</param> /// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param> /// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param> /// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param> /// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param> /// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param> /// <param name="transformType">The selected compound transform type.</param>
@ -232,7 +409,7 @@ internal static class Av1TransformBlockEncoder
/// <param name="plane">The component plane containing the block.</param> /// <param name="plane">The component plane containing the block.</param>
/// <param name="bitDepth">The coded sample bit depth.</param> /// <param name="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The retained transform type and end-of-block syntax.</param> /// <param name="state">The retained transform type and end-of-block syntax.</param>
private static void EncodeIntraDcLossyContiguous( private static void EncodeIntraLossyContiguous(
Av1EncoderBlockWorkspace workspace, Av1EncoderBlockWorkspace workspace,
ReadOnlySpan<ushort> source, ReadOnlySpan<ushort> source,
int sourceStride, int sourceStride,
@ -242,6 +419,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<ushort> left, ReadOnlySpan<ushort> left,
bool hasLeft, bool hasLeft,
bool hasAbove, bool hasAbove,
Av1PredictionMode mode,
Span<int> quantizedCoefficients, Span<int> quantizedCoefficients,
Av1TransformSize transformSize, Av1TransformSize transformSize,
Av1TransformType transformType, Av1TransformType transformType,
@ -259,16 +437,24 @@ internal static class Av1TransformBlockEncoder
Span<short> signedReconstruction = MemoryMarshal.Cast<ushort, short>(reconstruction); Span<short> signedReconstruction = MemoryMarshal.Cast<ushort, short>(reconstruction);
ReadOnlySpan<short> signedAbove = MemoryMarshal.Cast<ushort, short>(above); ReadOnlySpan<short> signedAbove = MemoryMarshal.Cast<ushort, short>(above);
ReadOnlySpan<short> signedLeft = MemoryMarshal.Cast<ushort, short>(left); ReadOnlySpan<short> signedLeft = MemoryMarshal.Cast<ushort, short>(left);
Av1DcIntraPredictor.Predict( if (mode == Av1PredictionMode.DC)
hasLeft, {
hasAbove, Av1DcIntraPredictor.Predict(
signedReconstruction, hasLeft,
reconstructionStride, hasAbove,
signedAbove, signedReconstruction,
signedLeft, reconstructionStride,
width, signedAbove,
height, signedLeft,
bitDepth.GetBitCount()); width,
height,
bitDepth.GetBitCount());
}
else
{
Av1NonDirectionalIntraPredictorBase.GetPredictor(mode)
.Predict(signedReconstruction, reconstructionStride, signedAbove, signedLeft, width, height);
}
Av1ResidualBuilder.Subtract( Av1ResidualBuilder.Subtract(
source, source,

48
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;
/// <content>
/// Defines the final-block decision contract used by interleaved tile encoding.
/// </content>
internal partial class Av1TileWriter
{
/// <summary>
/// Supplies a final block decision immediately before its symbols are written.
/// </summary>
internal interface IBlockEncodingHandler
{
/// <summary>
/// Encodes one final block against the current reconstructed neighbors and live tile probabilities.
/// </summary>
/// <param name="writer">The live tile symbol encoder.</param>
/// <param name="macroBlock">The current block's mapped neighbor state.</param>
/// <param name="blockOrigin">The absolute luma-sample origin.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="modeInfo">The mode information to publish.</param>
/// <param name="block">The encoder block state to publish.</param>
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)
{
}
}
}

168
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs

@ -67,6 +67,38 @@ internal partial class Av1TileWriter
Av1Superblock superblock, Av1Superblock superblock,
Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderCoefficientBuffer coefficientBuffer,
ushort tileIndex) ushort tileIndex)
{
PrecomputedBlockEncodingHandler blockEncoder = default;
WriteSuperblock(
pcs,
ec_ctx,
writer,
superblock,
coefficientBuffer,
tileIndex,
ref blockEncoder);
}
/// <summary>
/// Writes a partition tree while producing each final block against the immediately preceding tile state.
/// </summary>
/// <typeparam name="TBlockEncoder">The value type that produces final-block decisions.</typeparam>
/// <param name="pcs">The picture coding state.</param>
/// <param name="ec_ctx">The entropy-coding position state for the superblock.</param>
/// <param name="writer">The tile symbol encoder.</param>
/// <param name="superblock">The encoder decisions for the superblock.</param>
/// <param name="coefficientBuffer">The transformed coefficients retained by raster-ordered superblock.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="blockEncoder">The handler invoked for each final block.</param>
public static void WriteSuperblock<TBlockEncoder>(
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.CodedAreaSuperblock = 0;
ec_ctx.CodedAreaSuperblockUv = 0; ec_ctx.CodedAreaSuperblockUv = 0;
@ -86,13 +118,14 @@ internal partial class Av1TileWriter
pcs.Sequence.SequenceHeader.SuperblockSize, pcs.Sequence.SequenceHeader.SuperblockSize,
ec_ctx.SuperblockOrigin, ec_ctx.SuperblockOrigin,
ref partitionIndex, ref partitionIndex,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
} }
/// <summary> /// <summary>
/// Writes one selected partition node and recursively visits its split children. /// Writes one selected partition node and recursively visits its split children.
/// </summary> /// </summary>
private static void WritePartitionTree( private static void WritePartitionTree<TBlockEncoder>(
Av1PictureControlSet pcs, Av1PictureControlSet pcs,
Av1EntropyCodingContext entropyCodingContext, Av1EntropyCodingContext entropyCodingContext,
Av1SymbolEncoder writer, Av1SymbolEncoder writer,
@ -102,7 +135,9 @@ internal partial class Av1TileWriter
Av1BlockSize blockSize, Av1BlockSize blockSize,
Point blockOrigin, Point blockOrigin,
ref int partitionIndex, ref int partitionIndex,
ref int finalBlockIndex) ref int finalBlockIndex,
ref TBlockEncoder blockEncoder)
where TBlockEncoder : struct, IBlockEncodingHandler
{ {
Av1EncoderCommon common = pcs.Parent.Common; Av1EncoderCommon common = pcs.Parent.Common;
int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2; int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2;
@ -136,7 +171,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
break; break;
case Av1PartitionType.Horizontal: case Av1PartitionType.Horizontal:
@ -148,7 +184,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
if (modeInfoRow + (blockSize.Get4x4HighCount() >> 1) < common.ModeInfoRowCount) if (modeInfoRow + (blockSize.Get4x4HighCount() >> 1) < common.ModeInfoRowCount)
{ {
@ -160,7 +197,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(0, halfBlockSize), blockOrigin + new Size(0, halfBlockSize),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
} }
break; break;
@ -173,7 +211,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
if (modeInfoColumn + (blockSize.Get4x4WideCount() >> 1) < common.ModeInfoColumnCount) if (modeInfoColumn + (blockSize.Get4x4WideCount() >> 1) < common.ModeInfoColumnCount)
{ {
@ -185,7 +224,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(halfBlockSize, 0), blockOrigin + new Size(halfBlockSize, 0),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
} }
break; break;
@ -200,7 +240,8 @@ internal partial class Av1TileWriter
subSize, subSize,
blockOrigin, blockOrigin,
ref partitionIndex, ref partitionIndex,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WritePartitionTree( WritePartitionTree(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -211,7 +252,8 @@ internal partial class Av1TileWriter
subSize, subSize,
blockOrigin + new Size(halfBlockSize, 0), blockOrigin + new Size(halfBlockSize, 0),
ref partitionIndex, ref partitionIndex,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WritePartitionTree( WritePartitionTree(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -222,7 +264,8 @@ internal partial class Av1TileWriter
subSize, subSize,
blockOrigin + new Size(0, halfBlockSize), blockOrigin + new Size(0, halfBlockSize),
ref partitionIndex, ref partitionIndex,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WritePartitionTree( WritePartitionTree(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -233,7 +276,8 @@ internal partial class Av1TileWriter
subSize, subSize,
blockOrigin + new Size(halfBlockSize, halfBlockSize), blockOrigin + new Size(halfBlockSize, halfBlockSize),
ref partitionIndex, ref partitionIndex,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
break; break;
case Av1PartitionType.HorizontalA: case Av1PartitionType.HorizontalA:
@ -245,7 +289,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -254,7 +299,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(halfBlockSize, 0), blockOrigin + new Size(halfBlockSize, 0),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -263,7 +309,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(0, halfBlockSize), blockOrigin + new Size(0, halfBlockSize),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
break; break;
case Av1PartitionType.HorizontalB: case Av1PartitionType.HorizontalB:
@ -275,7 +322,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -284,7 +332,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(0, halfBlockSize), blockOrigin + new Size(0, halfBlockSize),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -293,7 +342,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(halfBlockSize, halfBlockSize), blockOrigin + new Size(halfBlockSize, halfBlockSize),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
break; break;
case Av1PartitionType.VerticalA: case Av1PartitionType.VerticalA:
@ -305,7 +355,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -314,7 +365,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(0, halfBlockSize), blockOrigin + new Size(0, halfBlockSize),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -323,7 +375,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(halfBlockSize, 0), blockOrigin + new Size(halfBlockSize, 0),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
break; break;
case Av1PartitionType.VerticalB: case Av1PartitionType.VerticalB:
@ -335,7 +388,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -344,7 +398,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(halfBlockSize, 0), blockOrigin + new Size(halfBlockSize, 0),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
WriteFinalBlock( WriteFinalBlock(
pcs, pcs,
entropyCodingContext, entropyCodingContext,
@ -353,7 +408,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
blockOrigin + new Size(halfBlockSize, halfBlockSize), blockOrigin + new Size(halfBlockSize, halfBlockSize),
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
break; break;
case Av1PartitionType.Horizontal4: case Av1PartitionType.Horizontal4:
@ -374,7 +430,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
childOrigin, childOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
} }
break; break;
@ -396,7 +453,8 @@ internal partial class Av1TileWriter
coefficientBuffer, coefficientBuffer,
tileIndex, tileIndex,
childOrigin, childOrigin,
ref finalBlockIndex); ref finalBlockIndex,
ref blockEncoder);
} }
break; break;
@ -413,7 +471,7 @@ internal partial class Av1TileWriter
/// <summary> /// <summary>
/// Writes the next final block selected by partition traversal. /// Writes the next final block selected by partition traversal.
/// </summary> /// </summary>
private static void WriteFinalBlock( private static void WriteFinalBlock<TBlockEncoder>(
Av1PictureControlSet pcs, Av1PictureControlSet pcs,
Av1EntropyCodingContext entropyCodingContext, Av1EntropyCodingContext entropyCodingContext,
Av1SymbolEncoder writer, Av1SymbolEncoder writer,
@ -421,7 +479,9 @@ internal partial class Av1TileWriter
Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderCoefficientBuffer coefficientBuffer,
ushort tileIndex, ushort tileIndex,
Point blockOrigin, Point blockOrigin,
ref int finalBlockIndex) ref int finalBlockIndex,
ref TBlockEncoder blockEncoder)
where TBlockEncoder : struct, IBlockEncodingHandler
{ {
ref Av1EncoderBlockStruct block = ref superblock.FinalBlocks[finalBlockIndex++]; ref Av1EncoderBlockStruct block = ref superblock.FinalBlocks[finalBlockIndex++];
WriteModesBlock( WriteModesBlock(
@ -432,7 +492,8 @@ internal partial class Av1TileWriter
ref block, ref block,
tileIndex, tileIndex,
blockOrigin, blockOrigin,
coefficientBuffer); coefficientBuffer,
ref blockEncoder);
} }
/// <summary> /// <summary>
@ -614,7 +675,8 @@ internal partial class Av1TileWriter
/// <param name="tile_idx">The zero-based tile index.</param> /// <param name="tile_idx">The zero-based tile index.</param>
/// <param name="blockOrigin">The absolute luma-sample origin of the block.</param> /// <param name="blockOrigin">The absolute luma-sample origin of the block.</param>
/// <param name="coefficientBuffer">The transformed coefficients retained by raster-ordered superblock.</param> /// <param name="coefficientBuffer">The transformed coefficients retained by raster-ordered superblock.</param>
private static void WriteModesBlock( /// <param name="blockEncoder">The final-block decision producer.</param>
private static void WriteModesBlock<TBlockEncoder>(
Av1PictureControlSet pcs, Av1PictureControlSet pcs,
Av1EntropyCodingContext entropyCodingContext, Av1EntropyCodingContext entropyCodingContext,
Av1SymbolEncoder writer, Av1SymbolEncoder writer,
@ -622,7 +684,9 @@ internal partial class Av1TileWriter
ref Av1EncoderBlockStruct blk_ptr, ref Av1EncoderBlockStruct blk_ptr,
ushort tile_idx, ushort tile_idx,
Point blockOrigin, Point blockOrigin,
Av1EncoderCoefficientBuffer coefficientBuffer) Av1EncoderCoefficientBuffer coefficientBuffer,
ref TBlockEncoder blockEncoder)
where TBlockEncoder : struct, IBlockEncodingHandler
{ {
Av1SequenceControlSet scs = pcs.Sequence; Av1SequenceControlSet scs = pcs.Sequence;
ObuFrameHeader frm_hdr = pcs.Parent.FrameHeader; ObuFrameHeader frm_hdr = pcs.Parent.FrameHeader;
@ -635,7 +699,6 @@ internal partial class Av1TileWriter
Point modeInfoPosition = new(mi_col, mi_row); Point modeInfoPosition = new(mi_col, mi_row);
ref Av1MacroBlockModeInfo macroBlockModeInfo = ref pcs.GetMacroBlockModeInfo(modeInfoPosition); ref Av1MacroBlockModeInfo macroBlockModeInfo = ref pcs.GetMacroBlockModeInfo(modeInfoPosition);
Av1BlockSize blockSize = macroBlockModeInfo.Block.BlockSize; Av1BlockSize blockSize = macroBlockModeInfo.Block.BlockSize;
bool skipWritingCoefficients = macroBlockModeInfo.Block.Skip;
pcs.MapModeInfoBlock(modeInfoPosition, blockSize); pcs.MapModeInfoBlock(modeInfoPosition, blockSize);
Av1MacroBlockD macroBlock = entropyCodingContext.MacroBlock; Av1MacroBlockD macroBlock = entropyCodingContext.MacroBlock;
@ -651,6 +714,17 @@ internal partial class Av1TileWriter
pcs.Parent.Common.ModeInfoRowCount, pcs.Parent.Common.ModeInfoRowCount,
pcs.Parent.Common.ModeInfoColumnCount); 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. // 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) if (pcs.Parent.FrameHeader.SegmentationParameters.Enabled && pcs.Parent.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip)
@ -909,6 +983,30 @@ internal partial class Av1TileWriter
left_ctx = IntraModeContextLookup[(int)intraLumaLeftMode]; left_ctx = IntraModeContextLookup[(int)intraLumaLeftMode];
} }
/// <summary>
/// Gets the key-frame luma mode rate against the current neighboring modes and tile probabilities.
/// </summary>
/// <param name="writer">The live tile symbol encoder.</param>
/// <param name="macroBlock">The current block's mapped neighbor state.</param>
/// <param name="blockSize">The selected block size.</param>
/// <param name="mode">The candidate luma mode.</param>
/// <returns>The luma mode and zero-angle rate in 1/512-bit units.</returns>
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;
}
/// <summary> /// <summary>
/// Writes the key-frame luma prediction mode and any directional angle adjustment. /// Writes the key-frame luma prediction mode and any directional angle adjustment.
/// </summary> /// </summary>
@ -1731,8 +1829,8 @@ internal partial class Av1TileWriter
/// <param name="skip">The skip value to write.</param> /// <param name="skip">The skip value to write.</param>
public static void EncodeSkipCoefficients(Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, bool skip) public static void EncodeSkipCoefficients(Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, bool skip)
{ {
int above_skip = macroBlock.IsUpAvailable && macroBlock.GetRelativeModeInfo(-macroBlock.ModeInfoStride).Block.Skip ? 1 : 0; int aboveSkip = macroBlock.IsUpAvailable && macroBlock.GetRelativeModeInfo(-macroBlock.ModeInfoStride).Block.Skip ? 1 : 0;
int left_skip = macroBlock.IsLeftAvailable && macroBlock.GetRelativeModeInfo(-1).Block.Skip ? 1 : 0; int leftSkip = macroBlock.IsLeftAvailable && macroBlock.GetRelativeModeInfo(-1).Block.Skip ? 1 : 0;
writer.WriteSkip(skip, above_skip + left_skip); writer.WriteSkip(skip, aboveSkip + leftSkip);
} }
} }

13
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -532,6 +532,19 @@ public class Av1EntropyTests
long expected) long expected)
=> Assert.Equal(expected, Av1RateDistortion.GetCost(rateMultiplier, rate, distortion)); => 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] [Fact]
public void SymbolWriterMatchesCurrentLibaomCarryRegression() public void SymbolWriterMatchesCurrentLibaomCarryRegression()
{ {

273
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); 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( using Av1EncoderCoefficientBuffer coefficients = new(
Configuration.Default, Configuration.Default,
picture.Sequence.SequenceHeader, pictureTemplate.Sequence.SequenceHeader,
Width, Width,
Height); 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.U)[0].EndOfBlock);
Assert.Equal((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.V)[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<byte> precomputedTile = writer.Exit();
using Av1EncoderFrameBuffer<byte> liveReconstruction = new(
Configuration.Default,
Width,
Height,
8,
colorFormat,
1,
1);
ClearPlane(liveReconstruction.Luma);
if (!isMonochrome)
{
ClearPlane(Assert.IsType<Buffer2D<byte>>(liveReconstruction.ChromaBlue));
ClearPlane(Assert.IsType<Buffer2D<byte>>(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] [Fact]
@ -465,6 +533,150 @@ public class Av1IntraSuperblockEncoderTests
Assert.InRange(reconstructedSample, (ushort)(byte.MaxValue + 1), (ushort)4095); 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<byte> 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<byte> source = new(
Configuration.Default,
Width,
Height,
8,
Av1ColorFormat.Yuv400,
0,
0);
using Av1EncoderFrameBuffer<byte> reconstruction = new(
Configuration.Default,
Width,
Height,
8,
Av1ColorFormat.Yuv400,
0,
0);
Buffer2DRegion<byte> sourcePlane = source.Frame.CodedView.GetPlane(Av1Plane.Y);
for (int y = 0; y < Height; y++)
{
Span<byte> 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] [Fact]
public void TileWriterMapsClippedRasterTraversalToEverySuperblockCoefficientSegment() public void TileWriterMapsClippedRasterTraversalToEverySuperblockCoefficientSegment()
{ {
@ -668,4 +880,61 @@ public class Av1IntraSuperblockEncoderTests
Assert.True(containsNonzero); Assert.True(containsNonzero);
} }
/// <summary>
/// Records the live luma-mode cost while supplying an all-skipped final block.
/// </summary>
private struct BlockCostRecorder : Av1TileWriter.IBlockEncodingHandler
{
private readonly int[] costs;
private readonly int qIndex;
/// <summary>
/// Initializes a new instance of the <see cref="BlockCostRecorder"/> struct.
/// </summary>
/// <param name="costs">The destination for costs observed in writer order.</param>
/// <param name="qIndex">The block quantizer index.</param>
public BlockCostRecorder(int[] costs, int qIndex)
{
this.costs = costs;
this.qIndex = qIndex;
this.Count = 0;
}
/// <summary>
/// Gets the number of final blocks visited by the writer.
/// </summary>
public int Count { get; private set; }
/// <inheritdoc/>
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;
}
}
} }

24
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ResidualBuilderTests.cs

@ -98,6 +98,13 @@ public class Av1ResidualBuilderTests
AssertEqual(uint16Expected, uint16Actual, Vector512<ushort>.Count); AssertEqual(uint16Expected, uint16Actual, Vector512<ushort>.Count);
} }
/// <summary>
/// Verifies exact 12-bit residual energy through every hardware-selected vector width and the scalar tail.
/// </summary>
[Fact]
public void SumSquaresMatchesScalarAcrossHardwareWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSumSquares, ResidualConfigurations);
/// <summary> /// <summary>
/// Verifies that repeated maximum-transform residual construction uses only caller-owned buffers. /// Verifies that repeated maximum-transform residual construction uses only caller-owned buffers.
/// </summary> /// </summary>
@ -128,6 +135,23 @@ public class Av1ResidualBuilderTests
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); 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() private static void ValidateResiduals()
{ {
ValidateByteResiduals(); ValidateByteResiduals();

83
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

@ -293,6 +293,65 @@ public class Av1TransformBlockEncoderTests
Assert.Equal(expectedState.TransformType, actualState.TransformType); Assert.Equal(expectedState.TransformType, actualState.TransformType);
} }
/// <summary>
/// Verifies that high-bit-depth candidate distortion follows the codec's pixel-domain normalization order.
/// </summary>
[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<ushort> sourceBuffer = Buffer2D<ushort>.WrapMemory(source, Width, Height, Width);
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default);
Av1EncoderTransformBlockState state = default;
long distortion = Av1TransformBlockEncoder.EncodeIntraLossyCandidate(
workspace,
new Buffer2DRegion<ushort>(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);
}
/// <summary> /// <summary>
/// Verifies that complete eight-bit and high-bit-depth DC block encoding uses only caller-owned storage. /// Verifies that complete eight-bit and high-bit-depth DC block encoding uses only caller-owned storage.
/// </summary> /// </summary>
@ -424,16 +483,20 @@ public class Av1TransformBlockEncoderTests
FillResidual(workspace.Residual, width, height, 4095); FillResidual(workspace.Residual, width, height, 4095);
Av1EncoderTransformBlockState state = default; Av1EncoderTransformBlockState state = default;
Av1TransformBlockEncoder.EncodeLossy( // Cross tiered-compilation call thresholds before measuring the steady-state transform kernel.
workspace, for (int iteration = 0; iteration < 64; iteration++)
quantized, {
transformSize, Av1TransformBlockEncoder.EncodeLossy(
Av1TransformType.DctDct, workspace,
73, quantized,
-1, transformSize,
3, Av1TransformType.DctDct,
Av1BitDepth.TwelveBit, 73,
ref state); -1,
3,
Av1BitDepth.TwelveBit,
ref state);
}
long before = GC.GetAllocatedBytesForCurrentThread(); long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 16; iteration++) for (int iteration = 0; iteration < 16; iteration++)

Loading…
Cancel
Save