From e168a04390a4de0756869acf4c635a3a737ff4f8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Sep 2026 10:08:00 +1000 Subject: [PATCH] Honor AV1 encoding effort --- HEIF_IMPLEMENTATION_PLAN.md | 3 +- .../Heif/Av1/Pipeline/Av1FrameEncoder.cs | 61 +++++++++---- ...traSuperblockEncoder.ChromaModeDecision.cs | 15 +++- .../Av1IntraSuperblockEncoder.ModeDecision.cs | 29 +++++-- .../Heif/Av1/Pipeline/Av1IntraTileWriter.cs | 14 ++- .../Formats/Heif/Av1/Av1EncoderFrameTests.cs | 86 +++++++++++++++++++ 6 files changed, 174 insertions(+), 34 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 4306d1ff4d..1408db5c6a 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -829,7 +829,8 @@ Encoder verification contract: - [~] Implement intra mode search, palette, filter intra, chroma-from-luma, and intra-block copy decisions. Live luma search now covers all 13 zero-angle base modes and all six nonzero adjustments for each of the eight directional modes. Joint spatial chroma search covers the same 61 candidates, combines both chroma planes in one rate-distortion decision, and preserves the winning shared angle adjustment. Chroma-from-luma now searches the complete signed alpha alphabet from reconstructed luma and retains its joint U/V syntax. Filter-intra now searches all five predictors after ordinary luma modes. Palette entropy, retained state, production syntax, exhaustive luma and paired chroma palette selection, adaptive screen-content activation, and joint intra-block-copy mode selection are complete. - [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools. - [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain. -- [~] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. The complete luma and joint chroma candidate sets, including chroma-from-luma, filter-intra, palette, and intra-block copy, now perform live rate-distortion selection; quality mapping, effort-dependent pruning, and the remaining searches are not implemented. +- [~] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. The complete luma and joint chroma candidate sets, including chroma-from-luma, filter-intra, palette, and intra-block copy, now perform live rate-distortion selection; quality mapping, effort tiers above the current fixed-block search ceiling, and the remaining searches are not implemented. +- [~] Frame effort now progressively expands the available current search: zero is DC-only, one adds every zero-angle spatial mode, two adds every legal directional adjustment, three adds transform refinement, four adds filter-intra and chroma-from-luma, and five adds adaptive palette and intra-block-copy analysis. Lower tiers do not signal unavailable sequence or frame tools, and tiers below five skip the whole-frame screen-content scan. Values six through ten currently share the exhaustive fixed-8x8 search ceiling and remain open until transform-size and partition searches provide additional work. Six decoder-visible production cases verify emitted flags, mode restrictions, and successful decode. The exact net11 Release test-project build reports 1,992 baseline warnings and zero errors; the clean complete HEIF/AV1 namespace run passes 9,258 of 9,258 through direct foreground VSTest. Current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 40 current generated payloads. Roslynk reports zero errors and no diagnostics in the touched files. - [~] 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 follows libaom's squared DC-quantizer formula and exact 10/12-bit normalization. Live final-block selection evaluates all 61 legal 8x8 luma candidates: the 13 zero-angle base modes in current-libaom order, followed by six nonzero adjustments for each directional mode. Joint chroma selection evaluates the equivalent 61 spatial candidates, combines U and V distortion plus coefficient rate, and charges one live chroma-mode and shared-angle symbol over the actual subsampled 4x4, 4x8, or 8x8 geometry. Chroma-from-luma subsamples the reconstructed luma block once into fixed-stride Q3 stack scratch, subtracts the rounded mean, evaluates all 33 signed alpha values independently for each plane with complete transform RD, and combines the cached plane results across all 1,088 valid joint pairs with one live sign cost and the conditional U/V magnitude costs. This is the allocation-free equivalent of current libaom's exhaustive 33-value path: it requires 66 evaluation transforms rather than transforming every joint pair, preserves DC-before-CfL-before-spatial tie order, and fixes the implicit chroma transform to DCT-DCT. Filter-intra follows ordinary luma candidates, searches all five predictors in syntax order, and evaluates every legal transform while reusing one prepared prediction and source residual per filter mode. Every candidate includes its live mode, angle, filter mode, alpha, and coefficient rate plus normalized pixel-domain distortion. Each prepared reference edge retains the common-corner prefix and twice the transform dimension required by directional prediction. A shared encoder/decoder availability calculation selects reconstructed top-right and bottom-left extensions according to tile, frame, superblock, and block reconstruction order; unavailable extensions repeat the nearest coded endpoint. Missing top or left edges retain current libaom's perpendicular-sample and bit-depth-midpoint rules. Directional prediction applies the AV1 three-degree adjustment step and reuses transform workspace for zone-three transposition before the transform overwrites it, keeping candidate evaluation allocation-free. The winning luma and chroma signed adjustments are retained in the packed final-block state consumed by the tile writer. The tile writer invokes these stack-only selectors after mapping 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. Luma candidate scratch remains one 8x8 reconstruction and one 8x8 coefficient span on the stack; chroma uses one transform-sized reconstruction and coefficient span for each of U and V. Only a newly winning candidate is copied into retained frame storage. Production fixtures force every luma base predictor, both extreme adjustments in all three directional zones, available top-right and bottom-left extensions, high-bit-depth adjustment propagation, exact signed luma and chroma angle-rate terms, joint U/V decisions, packed chroma state, and 4:2:0, 4:2:2, and 4:4:4 transform geometry. The CfL fixtures derive target chroma from a pilot production encode's actual reconstructed luma through an independent scalar Q3 oracle and prove exact positive/negative alpha syntax plus zero-residual DCT-DCT reconstruction for all three subsampling geometries at 8, 10, and 12 bits. The stable fixed-DC traversal comparison uses neutral samples for which both the baseline and live search are contractually DC and skipped, instead of relying on textured content to happen to select the baseline mode. Luma palette selection now evaluates dominant-color and one-dimensional K-means candidates for every legal size, snaps near-cache colors with the reference threshold and tie order, removes duplicate snapped colors, extends boundary maps from active samples, and performs complete transform rate-distortion search. Ordinary DC and filter-intra candidates pay the palette-disabled symbol whenever screen-content syntax is enabled. The exact net11 Release rebuild reports 1,992 test-project warnings and zero errors, all 58 intra-superblock cases pass, all 8,935 AVIF cases pass, and all 230 HEIF cases pass. Remaining mode decision work includes transform-size search, broader joint mode/transform refinement, partition search, and effort-dependent pruning. Non-empty intra blocks deliberately remain non-skipped, matching current libaom; later inter mode selection owns its distinct skip-transform RD decision. - [~] 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. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs index e3b7a9a352..48af6a5942 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs @@ -26,15 +26,17 @@ internal static class Av1FrameEncoder /// The destination receiving the complete AV1 item payload. /// The resolved native color and precision configuration. /// The frame quantizer index. + /// The mode-search effort in the inclusive range zero through ten. /// The sequence header describing the encoded payload. public static ObuSequenceHeader Encode( Configuration configuration, ImageFrame image, Stream stream, ObuColorConfig colorConfig, - int qIndex) + int qIndex, + int effort = 5) where TPixel : unmanaged, IPixel - => Encode(configuration, image, stream, colorConfig, qIndex, false); + => Encode(configuration, image, stream, colorConfig, qIndex, effort, false); /// /// Encodes one packed alpha channel as a reduced-still-picture monochrome AV1 frame. @@ -45,15 +47,17 @@ internal static class Av1FrameEncoder /// The destination receiving the complete AV1 item payload. /// The resolved monochrome precision configuration. /// The frame quantizer index. + /// The mode-search effort in the inclusive range zero through ten. /// The sequence header describing the encoded payload. public static ObuSequenceHeader EncodeAlpha( Configuration configuration, ImageFrame image, Stream stream, ObuColorConfig colorConfig, - int qIndex) + int qIndex, + int effort = 5) where TPixel : unmanaged, IPixel - => Encode(configuration, image, stream, colorConfig, qIndex, true); + => Encode(configuration, image, stream, colorConfig, qIndex, effort, true); private static ObuSequenceHeader Encode( Configuration configuration, @@ -61,6 +65,7 @@ internal static class Av1FrameEncoder Stream stream, ObuColorConfig colorConfig, int qIndex, + int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel { @@ -87,7 +92,7 @@ internal static class Av1FrameEncoder Use128x128Superblock = false, ForceScreenContentTools = 2, ForceIntegerMotionVector = 2, - EnableFilterIntra = true, + EnableFilterIntra = effort >= 4, EnableIntraEdgeFilter = false, EnableSuperResolution = false, EnableCdef = false, @@ -148,11 +153,11 @@ internal static class Av1FrameEncoder int initialTileSize = checked((int)Math.Max(8192L, (sampleCount * sampleSize * 5) / 2)); if (colorConfig.BitDepth == Av1BitDepth.EightBit) { - EncodeByte(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, encodeAlpha); + EncodeByte(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, effort, encodeAlpha); } else { - EncodeHighBitDepth(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, encodeAlpha); + EncodeHighBitDepth(configuration, image, stream, sequenceHeader, frameHeader, colorFormat, initialTileSize, effort, encodeAlpha); } return sequenceHeader; @@ -198,6 +203,7 @@ internal static class Av1FrameEncoder ObuFrameHeader frameHeader, Av1ColorFormat colorFormat, int initialTileSize, + int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel { @@ -219,7 +225,7 @@ internal static class Av1FrameEncoder chromaPositionX: 1, chromaPositionY: 1); - Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, initialTileSize, encodeAlpha); + Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, initialTileSize, effort, encodeAlpha); } private static void EncodeHighBitDepth( @@ -230,6 +236,7 @@ internal static class Av1FrameEncoder ObuFrameHeader frameHeader, Av1ColorFormat colorFormat, int initialTileSize, + int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel { @@ -252,7 +259,7 @@ internal static class Av1FrameEncoder chromaPositionX: 1, chromaPositionY: 1); - Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, initialTileSize, encodeAlpha); + Encode(configuration, image, stream, sequenceHeader, frameHeader, source, reconstruction, initialTileSize, effort, encodeAlpha); } private static void Encode( @@ -264,6 +271,7 @@ internal static class Av1FrameEncoder Av1EncoderFrameBuffer source, Av1EncoderFrameBuffer reconstruction, int initialTileSize, + int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel { @@ -274,10 +282,16 @@ internal static class Av1FrameEncoder sequenceHeader.ColorConfig, encodeAlpha); - Av1ScreenContentDetector.Detect( - source.Frame, - out bool allowScreenContentTools, - out bool allowIntraBlockCopy); + bool allowScreenContentTools = false; + bool allowIntraBlockCopy = false; + if (effort >= 5) + { + // Lower effort levels omit palette and intra-block-copy searches, so they do not need the whole-frame suitability scan. + Av1ScreenContentDetector.Detect( + source.Frame, + out allowScreenContentTools, + out allowIntraBlockCopy); + } frameHeader.AllowScreenContentTools = allowScreenContentTools; frameHeader.AllowIntraBlockCopy = allowIntraBlockCopy; @@ -304,7 +318,8 @@ internal static class Av1FrameEncoder coefficients, superblockWorkspace, blockWorkspace, - initialTileSize); + initialTileSize, + effort); ObuWriter writer = new(); writer.WriteAll(configuration, stream, sequenceHeader, frameHeader, tileWriter); @@ -319,6 +334,7 @@ internal static class Av1FrameEncoder Av1EncoderFrameBuffer source, Av1EncoderFrameBuffer reconstruction, int initialTileSize, + int effort, bool encodeAlpha) where TPixel : unmanaged, IPixel { @@ -329,10 +345,16 @@ internal static class Av1FrameEncoder sequenceHeader.ColorConfig, encodeAlpha); - Av1ScreenContentDetector.Detect( - source.Frame, - out bool allowScreenContentTools, - out bool allowIntraBlockCopy); + bool allowScreenContentTools = false; + bool allowIntraBlockCopy = false; + if (effort >= 5) + { + // Lower effort levels omit palette and intra-block-copy searches, so they do not need the whole-frame suitability scan. + Av1ScreenContentDetector.Detect( + source.Frame, + out allowScreenContentTools, + out allowIntraBlockCopy); + } frameHeader.AllowScreenContentTools = allowScreenContentTools; frameHeader.AllowIntraBlockCopy = allowIntraBlockCopy; @@ -359,7 +381,8 @@ internal static class Av1FrameEncoder coefficients, superblockWorkspace, blockWorkspace, - initialTileSize); + initialTileSize, + effort); ObuWriter writer = new(); writer.WriteAll(configuration, stream, sequenceHeader, frameHeader, tileWriter); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs index c28c89872a..d45f358d40 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs @@ -164,7 +164,15 @@ internal static partial class Av1IntraSuperblockEncoder int baseModeCount = ChromaModeSearchOrder.Length; int deltaCount = AngleDeltaSearchOrder.Length; int directionalModeCount = (int)Av1ChromaPredictionMode.Directional67Degrees - (int)Av1ChromaPredictionMode.Vertical + 1; - int candidateCount = baseModeCount + (directionalModeCount * deltaCount); + + // Effort zero evaluates DC only, effort one adds every zero-angle mode, and higher levels add all directional adjustments. + int candidateCount = this.effort switch + { + 0 => 1, + 1 => baseModeCount, + _ => baseModeCount + (directionalModeCount * deltaCount) + }; + bool hasLumaPalette = paletteInfo.PaletteSizes[0] != 0; int paletteDisabledCost = this.picture.Parent.FrameHeader.AllowScreenContentTools ? writer.GetPaletteUvModeCost(false, hasLumaPalette) @@ -249,7 +257,7 @@ internal static partial class Av1IntraSuperblockEncoder colorConfig.SubSamplingX, colorConfig.SubSamplingY); - if (chromaFromLumaAllowed) + if (this.effort >= 4 && chromaFromLumaAllowed) { Span lumaQ3 = stackalloc short[Av1ChromaFromLumaContext.BufferLine * 8]; TOperator.PrepareChromaFromLuma( @@ -441,7 +449,8 @@ internal static partial class Av1IntraSuperblockEncoder } } - if (this.picture.Parent.FrameHeader.AllowScreenContentTools && + if (this.effort >= 5 && + this.picture.Parent.FrameHeader.AllowScreenContentTools && this.SelectChromaPalette( writer, macroBlock, diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs index 6ade0c1715..e65e0007d5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs @@ -117,6 +117,7 @@ internal static partial class Av1IntraSuperblockEncoder private readonly ObuQuantizationParameters quantization; private readonly Av1BitDepth bitDepth; private readonly int rateMultiplier; + private readonly int effort; private int codedAreaLuma; private int codedAreaChroma; @@ -129,13 +130,15 @@ internal static partial class Av1IntraSuperblockEncoder /// The current superblock. /// The frame-owned quantized coefficient and transform state. /// The reusable block arithmetic workspace. + /// The mode-search effort in the inclusive range zero through ten. public ModeDecision( Av1EncoderFrame source, Av1EncoderFrame reconstruction, Av1PictureControlSet picture, Av1Superblock superblock, Av1EncoderCoefficientBuffer coefficientBuffer, - Av1EncoderBlockWorkspace blockWorkspace) + Av1EncoderBlockWorkspace blockWorkspace, + int effort = 5) { this.source = source.CodedView; this.reconstruction = reconstruction.CodedView; @@ -146,6 +149,7 @@ internal static partial class Av1IntraSuperblockEncoder this.quantization = picture.Parent.FrameHeader.QuantizationParameters; this.bitDepth = picture.Sequence.SequenceHeader.ColorConfig.BitDepth; this.rateMultiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(this.quantization.QIndex[0], this.bitDepth); + this.effort = effort; this.codedAreaLuma = 0; this.codedAreaChroma = 0; } @@ -528,7 +532,15 @@ internal static partial class Av1IntraSuperblockEncoder int baseModeCount = LumaModeSearchOrder.Length; int deltaCount = AngleDeltaSearchOrder.Length; int directionalModeCount = (int)Av1PredictionMode.Directional67Degrees - (int)Av1PredictionMode.Vertical + 1; - int candidateCount = baseModeCount + (directionalModeCount * deltaCount); + + // Effort zero evaluates DC only, effort one adds every zero-angle mode, and higher levels add all directional adjustments. + int candidateCount = this.effort switch + { + 0 => 1, + 1 => baseModeCount, + _ => baseModeCount + (directionalModeCount * deltaCount) + }; + bool useReducedTransformSet = this.picture.Parent.FrameHeader.UseReducedTransformSet; Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( TransformSize, @@ -594,11 +606,11 @@ internal static partial class Av1IntraSuperblockEncoder } } - // Mode selection uses the mode-derived default transform, then the winning mode alone pays for - // an exhaustive transform refinement. This preserves reference tie order without a 61-by-7 search. - long bestTransformCost = long.MaxValue; + // Lower effort levels retain the mode-derived default transform. Higher levels refine only the winning + // mode across the legal transform set, preserving search order without evaluating every mode-transform pair. + long bestTransformCost = bestCost; for (Av1TransformType transformType = Av1TransformType.DctDct; - transformType < Av1TransformType.AllTransformTypes; + this.effort >= 3 && transformType < Av1TransformType.AllTransformTypes; transformType++) { if (!transformType.IsExtendedSetUsed(transformSetType)) @@ -641,7 +653,7 @@ internal static partial class Av1IntraSuperblockEncoder } } - if (this.picture.Sequence.SequenceHeader.EnableFilterIntra) + if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra) { Span filterPrediction = stackalloc TSample[SampleCount]; Span filterResidual = stackalloc short[SampleCount]; @@ -710,7 +722,8 @@ internal static partial class Av1IntraSuperblockEncoder } } - if (this.picture.Parent.FrameHeader.AllowScreenContentTools && + if (this.effort >= 5 && + this.picture.Parent.FrameHeader.AllowScreenContentTools && this.SelectLumaPalette( writer, macroBlock, diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs index 249eacbdfa..bc8147e1f9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs @@ -28,6 +28,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable /// The reusable partition and final-block decision workspace. /// The reusable block arithmetic workspace. /// The estimated encoded tile size in bytes. + /// The mode-search effort in the inclusive range zero through ten. public Av1IntraTileWriter( Configuration configuration, Av1EncoderFrame source, @@ -36,7 +37,8 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderBlockWorkspace blockWorkspace, - int initialSize) + int initialSize, + int effort = 5) { this.tileData = Encode( configuration, @@ -46,6 +48,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable coefficientBuffer, superblockWorkspace, blockWorkspace, + effort, initialSize, out this.tileDataLength); } @@ -61,6 +64,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable /// The reusable partition and final-block decision workspace. /// The reusable block arithmetic workspace. /// The estimated encoded tile size in bytes. + /// The mode-search effort in the inclusive range zero through ten. public Av1IntraTileWriter( Configuration configuration, Av1EncoderFrame source, @@ -69,7 +73,8 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderBlockWorkspace blockWorkspace, - int initialSize) + int initialSize, + int effort = 5) { this.tileData = Encode( configuration, @@ -79,6 +84,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable coefficientBuffer, superblockWorkspace, blockWorkspace, + effort, initialSize, out this.tileDataLength); } @@ -107,6 +113,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable Av1EncoderCoefficientBuffer coefficientBuffer, Av1EncoderSuperblockWorkspace superblockWorkspace, Av1EncoderBlockWorkspace blockWorkspace, + int effort, int initialSize, out int tileDataLength) where TSample : unmanaged @@ -171,7 +178,8 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable picture, superblock, coefficientBuffer, - blockWorkspace); + blockWorkspace, + effort); Av1TileWriter.WriteSuperblock( picture, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs index b44808faef..0b168f635d 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Components; using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; @@ -482,6 +483,91 @@ public class Av1EncoderFrameTests File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-frame-16x16-8b-444-palette.obu"), payload); } + [Theory] + [InlineData(0, false, false)] + [InlineData(1, false, false)] + [InlineData(2, false, false)] + [InlineData(3, false, false)] + [InlineData(4, true, false)] + [InlineData(5, true, true)] + public void EncodeEffortControlsSearchFeatures(int effort, bool enableFilterIntra, bool enableScreenContentTools) + { + const int width = 16; + const int height = 16; + using Image source = new(width, height); + for (int row = 0; row < height; row++) + { + Span pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row); + for (int column = 0; column < width; column++) + { + pixels[column] = (((column >> 2) + (row >> 2)) & 1) == 0 + ? new Rgba32(224, 32, 32) + : new Rgba32(32, 32, 224); + } + } + + ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444); + using MemoryStream stream = new(); + _ = Av1FrameEncoder.Encode( + Configuration.Default, + source.Frames.RootFrame, + stream, + colorConfig, + qIndex: 37, + effort); + + byte[] payload = stream.ToArray(); + using Av1Decoder decoder = new(Configuration.Default); + using Image decoded = decoder.Decode(payload); + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + Assert.Equal(enableFilterIntra, sequenceHeader.EnableFilterIntra); + Assert.Equal(enableScreenContentTools, frameHeader.AllowScreenContentTools); + Assert.Equal(enableScreenContentTools, frameHeader.AllowIntraBlockCopy); + Assert.Equal(new Size(width, height), decoded.Size); + + int modeCount = 0; + foreach (Av1BlockModeInfo modeInfo in frameInfo.GetSuperblock(Point.Empty).GetModeInfos()) + { + modeCount++; + if (effort == 0) + { + Assert.Equal(Av1PredictionMode.DC, modeInfo.YMode); + Assert.Equal(Av1ChromaPredictionMode.DC, modeInfo.UvMode); + } + + if (effort <= 1) + { + Assert.Equal(0, modeInfo.GetAngleDelta(Av1Plane.Y)); + Assert.Equal(0, modeInfo.GetAngleDelta(Av1Plane.U)); + } + + if (effort < 4) + { + Assert.False(modeInfo.UseFilterIntra); + } + + if (effort < 5) + { + Assert.False(modeInfo.UseIntraBlockCopy); + Assert.Equal(0, modeInfo.GetPaletteSize(Av1Plane.Y)); + Assert.Equal(0, modeInfo.GetPaletteSize(Av1Plane.U)); + } + } + + Assert.NotEqual(0, modeCount); + + string outputDirectory = Path.Combine( + TestEnvironment.ActualOutputDirectoryFullPath, + "Formats", + "Heif", + "Av1"); + + Directory.CreateDirectory(outputDirectory); + File.WriteAllBytes(Path.Combine(outputDirectory, $"encoder-frame-16x16-8b-444-effort-{effort}.obu"), payload); + } + [Fact] public void EncodeSelectsIntraBlockCopyForRepeatedScreenContent() {