From ccd7553e2aa09462308177fcb7f3027c0021ef08 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Aug 2026 15:14:27 +1000 Subject: [PATCH] Select AV1 operating points --- HEIF_IMPLEMENTATION_PLAN.md | 5 +- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 12 ++- .../Av1/OpenBitstreamUnit/ObuFrameHeader.cs | 30 ++++-- .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 75 ++++++++++++--- .../Formats/Heif/Av1HeifItemDecoder.cs | 8 +- .../Formats/Heif/Av1/ObuFrameHeaderTests.cs | 94 ++++++++++++++++++- 6 files changed, 199 insertions(+), 25 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 0e50c724d..7f898dc5a 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat ## Delivery dashboard -Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `63c4d302a`, including the completed AV1 transform, OBU-framing, intra-block-copy, and 12-profile reconstruction checkpoints. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead. +Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `32d5e3b51`, including the completed AV1 transform, OBU-framing, intra-block-copy, 12-profile reconstruction, and layered-item property checkpoints. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead. Status meanings: @@ -61,7 +61,8 @@ Immediate checkpoint: **complete layered AV1 image-item decoding through the exi - [ ] **Current:** implement layered AV1 image-item properties and stateful dependency reconstruction, then verify default final-layer output against the two pinned libavif progressive fixtures. - [x] Parse and associate `a1op`, `lsel`, and `a1lx` through the bounded image-item property model, including normative essential flags, duplicate handling, exact property lengths, and the four-layer limit. - [x] Validate `a1lx` layer boundaries against the logical item size and restrict concrete `lsel` decoding to the cumulative payload through the selected spatial layer without copying item bytes. - - [ ] Apply the selected `a1op` operating-point mask while consuming extended OBUs and validate the selected index against the parsed sequence header. + - [x] Apply the selected `a1op` operating-point mask while consuming extended OBUs and validate the selected index against the parsed sequence header. + - [x] Store the eight fixed reference-validity, order-hint, and map-index tables inline on the frame header, retaining complete multi-bit order hints without per-header array allocations. - [ ] Preserve reconstruction, reference-frame, primary-CDF, segmentation, loop-filter, and motion state across every dependent layer in one image-item decoder session. - [ ] Implement the complete inter-frame entropy, mode, motion-vector, compound-prediction, inter-prediction, and warped/global-motion paths permitted by the image profile. - [ ] Return the explicitly selected spatial layer or the final displayed layer, keeping reference reconstruction separate from display-only film grain. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index aeb74c07c..b30b5de9c 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -37,9 +37,19 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable /// /// The configuration used for image and scratch-memory allocation. public Av1Decoder(Configuration configuration) + : this(configuration, 0) + { + } + + /// + /// Initializes a new instance of the class for one selected AV1 operating point. + /// + /// The configuration used for image and scratch-memory allocation. + /// The zero-based sequence-header operating-point index to decode. + public Av1Decoder(Configuration configuration, byte operatingPointIndex) { this.configuration = configuration; - this.obuReader = new(); + this.obuReader = new(operatingPointIndex); } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs index 7fb868d42..73084c3df 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs @@ -10,6 +10,21 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// internal class ObuFrameHeader { + /// + /// Stores the validity state of the eight reference-frame slots without a per-header array allocation. + /// + private InlineArray8 referenceValid; + + /// + /// Stores the multi-bit order hint associated with each of the eight reference-frame slots. + /// + private InlineArray8 referenceOrderHint; + + /// + /// Stores the reference-map index selected for each of the eight inter references. + /// + private InlineArray8 referenceFrameIndex; + /// /// Gets or sets a value indicating whether motion vectors use integer-sample precision. /// @@ -156,14 +171,16 @@ internal class ObuFrameHeader internal ObuFrameType FrameType { get; set; } /// - /// Gets or sets the validity state of each reference-frame slot. + /// Gets the validity state of each reference-frame slot. /// - internal bool[] ReferenceValid { get; set; } = new bool[Av1Constants.ReferenceFrameCount]; + /// The mutable eight-entry reference-validity table. + public Span GetReferenceValidity() => this.referenceValid; /// - /// Gets or sets the stored order-hint state for each reference-frame slot. + /// Gets the multi-bit order hint associated with each reference-frame slot. /// - internal bool[] ReferenceOrderHint { get; set; } = new bool[Av1Constants.ReferenceFrameCount]; + /// The mutable eight-entry reference-order-hint table. + public Span GetReferenceOrderHints() => this.referenceOrderHint; /// /// Gets or sets a value indicating whether the decoded frame is immediately displayed. @@ -206,9 +223,10 @@ internal class ObuFrameHeader internal uint CurrentFrameId { get; set; } /// - /// Gets or sets the reference-map index selected for each inter reference. + /// Gets the reference-map index selected for each inter reference. /// - internal uint[] ReferenceFrameIndex { get; set; } = new uint[Av1Constants.ReferenceFrameCount]; + /// The mutable eight-entry reference-frame-index table. + public Span GetReferenceFrameIndices() => this.referenceFrameIndex; /// /// Gets or sets the frame order hint. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index 457a4df51..9036cfb1c 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -11,11 +11,36 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// internal class ObuReader { + /// + /// The zero-based sequence-header operating-point index selected by the container. + /// + private readonly byte operatingPointIndex; + /// /// The tile reader created for the current coded frame. /// private IAv1TileReader? decoder; + /// + /// The temporal- and spatial-layer mask for the selected operating point. + /// + private uint currentOperatingPointIdc; + + /// + /// Initializes a new instance of the class using operating-point index zero. + /// + public ObuReader() + : this(0) + { + } + + /// + /// Initializes a new instance of the class for one selected AV1 operating point. + /// + /// The zero-based sequence-header operating-point index to decode. + public ObuReader(byte operatingPointIndex) + => this.operatingPointIndex = operatingPointIndex; + /// /// Gets or sets the most recently parsed sequence header. /// @@ -97,6 +122,22 @@ internal class ObuReader // A dedicated payload reader prevents malformed syntax from consuming the following OBU. The parent // advances once here, so ignored metadata, padding, and reserved OBUs are skipped without copying. Span obuPayload = reader.ReadBytes(payloadSize); + + // AV1 operating_point_idc uses bits 0-7 for temporal IDs and bits 8-11 for spatial IDs. libaom + // requires both selected bits for an extended OBU, while an all-zero mask and unextended OBUs apply + // universally. Sequence headers establish the mask and temporal delimiters define framing, so neither + // can be filtered even when their extension identifies a layer outside the selected operating point. + bool isOperatingPointIndependent = header.Type is ObuType.SequenceHeader or ObuType.TemporalDelimiter; + bool isInCurrentOperatingPoint = this.currentOperatingPointIdc == 0 + || !header.HasExtension + || (((this.currentOperatingPointIdc >> header.TemporalId) & 1U) != 0 + && ((this.currentOperatingPointIdc >> (header.SpatialId + 8)) & 1U) != 0); + + if (!isOperatingPointIndependent && !isInCurrentOperatingPoint) + { + continue; + } + Av1BitStreamReader payloadReader = new(obuPayload); int decodedPayloadSize; @@ -105,6 +146,14 @@ internal class ObuReader case ObuType.SequenceHeader: this.SequenceHeader = new(); ReadSequenceHeader(ref payloadReader, this.SequenceHeader); + if (this.operatingPointIndex >= this.SequenceHeader.OperatingPoint.Length) + { + throw new InvalidImageContentException( + $"The AV1 operating-point selector requests index {this.operatingPointIndex}, " + + $"but the sequence header declares {this.SequenceHeader.OperatingPoint.Length} operating points."); + } + + this.currentOperatingPointIdc = this.SequenceHeader.OperatingPoint[this.operatingPointIndex].Idc; decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition); break; case ObuType.FrameHeader: @@ -1064,10 +1113,8 @@ internal class ObuReader if (frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame) { - frameHeader.ReferenceValid = new bool[Av1Constants.ReferenceFrameCount]; - frameHeader.ReferenceOrderHint = new bool[Av1Constants.ReferenceFrameCount]; - Array.Fill(frameHeader.ReferenceValid, false); - Array.Fill(frameHeader.ReferenceOrderHint, false); + frameHeader.GetReferenceValidity().Clear(); + frameHeader.GetReferenceOrderHints().Clear(); } frameHeader.DisableCdfUpdate = reader.ReadBoolean(); @@ -1122,20 +1169,22 @@ internal class ObuReader } int diffLength = sequenceHeader.DeltaFrameIdLength; + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + Span referenceValidity = frameHeader.GetReferenceValidity(); for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++) { if (frameHeader.CurrentFrameId > (1U << diffLength)) { - if ((frameHeader.ReferenceFrameIndex[i] > frameHeader.CurrentFrameId) || - frameHeader.ReferenceFrameIndex[i] > (frameHeader.CurrentFrameId - (1 - diffLength))) + if ((referenceFrameIndices[i] > frameHeader.CurrentFrameId) || + referenceFrameIndices[i] > (frameHeader.CurrentFrameId - (1 - diffLength))) { - frameHeader.ReferenceValid[i] = false; + referenceValidity[i] = false; } } - else if (frameHeader.ReferenceFrameIndex[i] > frameHeader.CurrentFrameId && - frameHeader.ReferenceFrameIndex[i] < ((1 << idLength) + (frameHeader.CurrentFrameId - (1 << diffLength)))) + else if (referenceFrameIndices[i] > frameHeader.CurrentFrameId && + referenceFrameIndices[i] < ((1 << idLength) + (frameHeader.CurrentFrameId - (1 << diffLength)))) { - frameHeader.ReferenceValid[i] = false; + referenceValidity[i] = false; } } } @@ -1211,12 +1260,14 @@ internal class ObuReader { if (frameHeader.ErrorResilientMode && sequenceHeader.OrderHintInfo != null) { + Span referenceOrderHints = frameHeader.GetReferenceOrderHints(); + Span referenceValidity = frameHeader.GetReferenceValidity(); for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++) { int referenceOrderHint = (int)reader.ReadLiteral(sequenceHeader.OrderHintInfo.OrderHintBits); - if (referenceOrderHint != (frameHeader.ReferenceOrderHint[i] ? 1U : 0U)) + if (referenceOrderHint != referenceOrderHints[i]) { - frameHeader.ReferenceValid[i] = false; + referenceValidity[i] = false; } } } diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index 4441c3a8c..479ad8931 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -52,7 +52,9 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlpha out HeifContentLightLevel? obuContentLightLevel, out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); - using Av1Decoder decoder = new(options.Configuration); + byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0; + + using Av1Decoder decoder = new(options.Configuration, operatingPointIndex); Image image = decoder.Decode(itemData, colorProfile, codecConfiguration); HeifMetadata metadata = image.Metadata.GetHeifMetadata(); metadata.CompressionMethod = this.CompressionMethod; @@ -82,7 +84,9 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlpha throw new InvalidImageContentException($"AV1 alpha image item {item.Id} is not monochrome."); } - using Av1Decoder decoder = new(options.Configuration); + byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0; + + using Av1Decoder decoder = new(options.Configuration, operatingPointIndex); decoder.DecodeAlpha( itemData, item.CicpProfile, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index b2bb1004e..e937a1c78 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -12,6 +12,14 @@ public class ObuFrameHeaderTests private static readonly byte[] DefaultSequenceHeaderBitStream = [0x0a, 0x06, 0b001_1_1_000, 0b00_1000_01, 0b11_110101, 0b001_11101, 0b111_1_1_1_0_1, 0b1_0_0_1_1_1_10]; + // This complete temporal-delimiter and sequence-header prefix comes from the color item in libavif's + // draw_points_idat_progressive.avif. Its operating points select spatial layers 0+1 and layer 0 respectively. + private static ReadOnlySpan ProgressiveSequenceHeaderBitStream => + [ + 0x12, 0x00, + 0x0A, 0x0F, 0x20, 0x13, 0x01, 0x00, 0x80, 0x81, 0x4E, 0x0A, 0x36, 0xBE, 0x48, 0x08, 0x20, 0x34, 0x80 + ]; + // Bits Syntax element Value // 1 obu_forbidden_bit 0 // 4 obu_type 2 (OBU_TEMPORAL_DELIMITER) @@ -196,6 +204,88 @@ public class ObuFrameHeaderTests Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); } + /// + /// Verifies that an item cannot select an operating-point index absent from its sequence header. + /// + [Fact] + public void ReadOperatingPointRejectsIndexOutsideSequenceHeader() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream]; + + Assert.Throws(() => ReadObuStream(bitStream, 2)); + } + + /// + /// Verifies that an extended OBU belongs to an operating point only when both of its layer identifiers are selected. + /// + /// The temporal-layer identifier carried by the test OBU. + /// The spatial-layer identifier carried by the test OBU. + /// Whether operating point one selects both identifiers. + [Theory] + [InlineData(0, 0, true)] + [InlineData(0, 1, false)] + [InlineData(1, 0, false)] + public void ReadOperatingPointRequiresBothLayerBits(byte temporalId, byte spatialId, bool isIncluded) + { + byte extension = (byte)((temporalId << 5) | (spatialId << 3)); + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2E, extension, 0x01, 0x00]; + + // A selected metadata OBU reaches ignored-payload validation, where an all-zero payload is invalid. A filtered + // OBU has nevertheless had its complete header, size and payload boundary consumed before syntax is skipped. + if (isIncluded) + { + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + else + { + ReadObuStream(bitStream, 1); + } + } + + /// + /// Verifies that an all-zero operating-point mask includes every extended OBU. + /// + [Fact] + public void ReadZeroOperatingPointMaskIncludesExtendedObu() + { + byte[] bitStream = [.. DefaultSequenceHeaderBitStream, 0x2E, 0x08, 0x01, 0x00]; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that an OBU without an extension header applies to every operating point. + /// + [Fact] + public void ReadOperatingPointIncludesUnextendedObu() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2A, 0x01, 0x00]; + + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + + /// + /// Verifies that temporal delimiters remain part of stream framing even when their extension is outside the selected mask. + /// + [Fact] + public void ReadOperatingPointDoesNotFilterTemporalDelimiter() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x16, 0x08, 0x01, 0x01]; + + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + + /// + /// Verifies that an excluded OBU cannot escape validation of its declared payload boundary. + /// + [Fact] + public void ReadFilteredOperatingPointObuStillValidatesBoundary() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2E, 0x08, 0x02, 0x80]; + + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + /// /// Verifies that the reduced sequence syntax cannot be used without declaring a still picture. /// @@ -438,10 +528,10 @@ public class ObuFrameHeaderTests /// Reads one complete OBU stream for malformed-input assertions that cannot capture a ref-struct reader. /// /// The complete encoded OBU stream. - private static void ReadObuStream(byte[] bitStream) + private static void ReadObuStream(byte[] bitStream, byte operatingPointIndex = 0) { Av1BitStreamReader reader = new(bitStream); - ObuReader obuReader = new(); + ObuReader obuReader = new(operatingPointIndex); IAv1TileReader tileDecoder = new Av1TileDecoderStub(); obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder);