Browse Source

Select AV1 operating points

pull/2633/head
James Jackson-South 6 days ago
parent
commit
ccd7553e2a
  1. 5
      HEIF_IMPLEMENTATION_PLAN.md
  2. 12
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  3. 30
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs
  4. 75
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs
  5. 8
      src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
  6. 94
      tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

5
HEIF_IMPLEMENTATION_PLAN.md

@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat
## Delivery dashboard ## 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: 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. - [ ] **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] 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. - [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. - [ ] 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. - [ ] 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. - [ ] Return the explicitly selected spatial layer or the final displayed layer, keeping reference reconstruction separate from display-only film grain.

12
src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs

@ -37,9 +37,19 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// </summary> /// </summary>
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param> /// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
public Av1Decoder(Configuration configuration) public Av1Decoder(Configuration configuration)
: this(configuration, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Decoder"/> class for one selected AV1 operating point.
/// </summary>
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
/// <param name="operatingPointIndex">The zero-based sequence-header operating-point index to decode.</param>
public Av1Decoder(Configuration configuration, byte operatingPointIndex)
{ {
this.configuration = configuration; this.configuration = configuration;
this.obuReader = new(); this.obuReader = new(operatingPointIndex);
} }
/// <summary> /// <summary>

30
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs

@ -10,6 +10,21 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// </summary> /// </summary>
internal class ObuFrameHeader internal class ObuFrameHeader
{ {
/// <summary>
/// Stores the validity state of the eight reference-frame slots without a per-header array allocation.
/// </summary>
private InlineArray8<bool> referenceValid;
/// <summary>
/// Stores the multi-bit order hint associated with each of the eight reference-frame slots.
/// </summary>
private InlineArray8<uint> referenceOrderHint;
/// <summary>
/// Stores the reference-map index selected for each of the eight inter references.
/// </summary>
private InlineArray8<uint> referenceFrameIndex;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether motion vectors use integer-sample precision. /// Gets or sets a value indicating whether motion vectors use integer-sample precision.
/// </summary> /// </summary>
@ -156,14 +171,16 @@ internal class ObuFrameHeader
internal ObuFrameType FrameType { get; set; } internal ObuFrameType FrameType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the validity state of each reference-frame slot. /// Gets the validity state of each reference-frame slot.
/// </summary> /// </summary>
internal bool[] ReferenceValid { get; set; } = new bool[Av1Constants.ReferenceFrameCount]; /// <returns>The mutable eight-entry reference-validity table.</returns>
public Span<bool> GetReferenceValidity() => this.referenceValid;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
internal bool[] ReferenceOrderHint { get; set; } = new bool[Av1Constants.ReferenceFrameCount]; /// <returns>The mutable eight-entry reference-order-hint table.</returns>
public Span<uint> GetReferenceOrderHints() => this.referenceOrderHint;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the decoded frame is immediately displayed. /// 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; } internal uint CurrentFrameId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the reference-map index selected for each inter reference. /// Gets the reference-map index selected for each inter reference.
/// </summary> /// </summary>
internal uint[] ReferenceFrameIndex { get; set; } = new uint[Av1Constants.ReferenceFrameCount]; /// <returns>The mutable eight-entry reference-frame-index table.</returns>
public Span<uint> GetReferenceFrameIndices() => this.referenceFrameIndex;
/// <summary> /// <summary>
/// Gets or sets the frame order hint. /// Gets or sets the frame order hint.

75
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs

@ -11,11 +11,36 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// </summary> /// </summary>
internal class ObuReader internal class ObuReader
{ {
/// <summary>
/// The zero-based sequence-header operating-point index selected by the container.
/// </summary>
private readonly byte operatingPointIndex;
/// <summary> /// <summary>
/// The tile reader created for the current coded frame. /// The tile reader created for the current coded frame.
/// </summary> /// </summary>
private IAv1TileReader? decoder; private IAv1TileReader? decoder;
/// <summary>
/// The temporal- and spatial-layer mask for the selected operating point.
/// </summary>
private uint currentOperatingPointIdc;
/// <summary>
/// Initializes a new instance of the <see cref="ObuReader"/> class using operating-point index zero.
/// </summary>
public ObuReader()
: this(0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ObuReader"/> class for one selected AV1 operating point.
/// </summary>
/// <param name="operatingPointIndex">The zero-based sequence-header operating-point index to decode.</param>
public ObuReader(byte operatingPointIndex)
=> this.operatingPointIndex = operatingPointIndex;
/// <summary> /// <summary>
/// Gets or sets the most recently parsed sequence header. /// Gets or sets the most recently parsed sequence header.
/// </summary> /// </summary>
@ -97,6 +122,22 @@ internal class ObuReader
// A dedicated payload reader prevents malformed syntax from consuming the following OBU. The parent // 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. // advances once here, so ignored metadata, padding, and reserved OBUs are skipped without copying.
Span<byte> obuPayload = reader.ReadBytes(payloadSize); Span<byte> 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); Av1BitStreamReader payloadReader = new(obuPayload);
int decodedPayloadSize; int decodedPayloadSize;
@ -105,6 +146,14 @@ internal class ObuReader
case ObuType.SequenceHeader: case ObuType.SequenceHeader:
this.SequenceHeader = new(); this.SequenceHeader = new();
ReadSequenceHeader(ref payloadReader, this.SequenceHeader); 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); decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition);
break; break;
case ObuType.FrameHeader: case ObuType.FrameHeader:
@ -1064,10 +1113,8 @@ internal class ObuReader
if (frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame) if (frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame)
{ {
frameHeader.ReferenceValid = new bool[Av1Constants.ReferenceFrameCount]; frameHeader.GetReferenceValidity().Clear();
frameHeader.ReferenceOrderHint = new bool[Av1Constants.ReferenceFrameCount]; frameHeader.GetReferenceOrderHints().Clear();
Array.Fill(frameHeader.ReferenceValid, false);
Array.Fill(frameHeader.ReferenceOrderHint, false);
} }
frameHeader.DisableCdfUpdate = reader.ReadBoolean(); frameHeader.DisableCdfUpdate = reader.ReadBoolean();
@ -1122,20 +1169,22 @@ internal class ObuReader
} }
int diffLength = sequenceHeader.DeltaFrameIdLength; int diffLength = sequenceHeader.DeltaFrameIdLength;
Span<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
Span<bool> referenceValidity = frameHeader.GetReferenceValidity();
for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++) for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++)
{ {
if (frameHeader.CurrentFrameId > (1U << diffLength)) if (frameHeader.CurrentFrameId > (1U << diffLength))
{ {
if ((frameHeader.ReferenceFrameIndex[i] > frameHeader.CurrentFrameId) || if ((referenceFrameIndices[i] > frameHeader.CurrentFrameId) ||
frameHeader.ReferenceFrameIndex[i] > (frameHeader.CurrentFrameId - (1 - diffLength))) referenceFrameIndices[i] > (frameHeader.CurrentFrameId - (1 - diffLength)))
{ {
frameHeader.ReferenceValid[i] = false; referenceValidity[i] = false;
} }
} }
else if (frameHeader.ReferenceFrameIndex[i] > frameHeader.CurrentFrameId && else if (referenceFrameIndices[i] > frameHeader.CurrentFrameId &&
frameHeader.ReferenceFrameIndex[i] < ((1 << idLength) + (frameHeader.CurrentFrameId - (1 << diffLength)))) 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) if (frameHeader.ErrorResilientMode && sequenceHeader.OrderHintInfo != null)
{ {
Span<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints();
Span<bool> referenceValidity = frameHeader.GetReferenceValidity();
for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++) for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++)
{ {
int referenceOrderHint = (int)reader.ReadLiteral(sequenceHeader.OrderHintInfo.OrderHintBits); 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;
} }
} }
} }

8
src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs

@ -52,7 +52,9 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlpha
out HeifContentLightLevel? obuContentLightLevel, out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
using Av1Decoder decoder = new(options.Configuration); byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0;
using Av1Decoder decoder = new(options.Configuration, operatingPointIndex);
Image<TPixel> image = decoder.Decode<TPixel>(itemData, colorProfile, codecConfiguration); Image<TPixel> image = decoder.Decode<TPixel>(itemData, colorProfile, codecConfiguration);
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = this.CompressionMethod; metadata.CompressionMethod = this.CompressionMethod;
@ -82,7 +84,9 @@ internal class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlpha
throw new InvalidImageContentException($"AV1 alpha image item {item.Id} is not monochrome."); 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( decoder.DecodeAlpha(
itemData, itemData,
item.CicpProfile, item.CicpProfile,

94
tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

@ -12,6 +12,14 @@ public class ObuFrameHeaderTests
private static readonly byte[] DefaultSequenceHeaderBitStream = 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]; [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<byte> ProgressiveSequenceHeaderBitStream =>
[
0x12, 0x00,
0x0A, 0x0F, 0x20, 0x13, 0x01, 0x00, 0x80, 0x81, 0x4E, 0x0A, 0x36, 0xBE, 0x48, 0x08, 0x20, 0x34, 0x80
];
// Bits Syntax element Value // Bits Syntax element Value
// 1 obu_forbidden_bit 0 // 1 obu_forbidden_bit 0
// 4 obu_type 2 (OBU_TEMPORAL_DELIMITER) // 4 obu_type 2 (OBU_TEMPORAL_DELIMITER)
@ -196,6 +204,88 @@ public class ObuFrameHeaderTests
Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader));
} }
/// <summary>
/// Verifies that an item cannot select an operating-point index absent from its sequence header.
/// </summary>
[Fact]
public void ReadOperatingPointRejectsIndexOutsideSequenceHeader()
{
byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream];
Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream, 2));
}
/// <summary>
/// Verifies that an extended OBU belongs to an operating point only when both of its layer identifiers are selected.
/// </summary>
/// <param name="temporalId">The temporal-layer identifier carried by the test OBU.</param>
/// <param name="spatialId">The spatial-layer identifier carried by the test OBU.</param>
/// <param name="isIncluded">Whether operating point one selects both identifiers.</param>
[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<InvalidImageContentException>(() => ReadObuStream(bitStream, 1));
}
else
{
ReadObuStream(bitStream, 1);
}
}
/// <summary>
/// Verifies that an all-zero operating-point mask includes every extended OBU.
/// </summary>
[Fact]
public void ReadZeroOperatingPointMaskIncludesExtendedObu()
{
byte[] bitStream = [.. DefaultSequenceHeaderBitStream, 0x2E, 0x08, 0x01, 0x00];
Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream));
}
/// <summary>
/// Verifies that an OBU without an extension header applies to every operating point.
/// </summary>
[Fact]
public void ReadOperatingPointIncludesUnextendedObu()
{
byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2A, 0x01, 0x00];
Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream, 1));
}
/// <summary>
/// Verifies that temporal delimiters remain part of stream framing even when their extension is outside the selected mask.
/// </summary>
[Fact]
public void ReadOperatingPointDoesNotFilterTemporalDelimiter()
{
byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x16, 0x08, 0x01, 0x01];
Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream, 1));
}
/// <summary>
/// Verifies that an excluded OBU cannot escape validation of its declared payload boundary.
/// </summary>
[Fact]
public void ReadFilteredOperatingPointObuStillValidatesBoundary()
{
byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2E, 0x08, 0x02, 0x80];
Assert.Throws<InvalidImageContentException>(() => ReadObuStream(bitStream, 1));
}
/// <summary> /// <summary>
/// Verifies that the reduced sequence syntax cannot be used without declaring a still picture. /// Verifies that the reduced sequence syntax cannot be used without declaring a still picture.
/// </summary> /// </summary>
@ -438,10 +528,10 @@ public class ObuFrameHeaderTests
/// Reads one complete OBU stream for malformed-input assertions that cannot capture a ref-struct reader. /// Reads one complete OBU stream for malformed-input assertions that cannot capture a ref-struct reader.
/// </summary> /// </summary>
/// <param name="bitStream">The complete encoded OBU stream.</param> /// <param name="bitStream">The complete encoded OBU stream.</param>
private static void ReadObuStream(byte[] bitStream) private static void ReadObuStream(byte[] bitStream, byte operatingPointIndex = 0)
{ {
Av1BitStreamReader reader = new(bitStream); Av1BitStreamReader reader = new(bitStream);
ObuReader obuReader = new(); ObuReader obuReader = new(operatingPointIndex);
IAv1TileReader tileDecoder = new Av1TileDecoderStub(); IAv1TileReader tileDecoder = new Av1TileDecoderStub();
obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder); obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder);

Loading…
Cancel
Save