Browse Source

Bound AV1 intra reference extensions by coded frame extent

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
2424ff9f91
  1. 44
      HEIF_IMPLEMENTATION_PLAN.md
  2. 12
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs
  3. 16
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  4. 62
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs
  5. 35
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

44
HEIF_IMPLEMENTATION_PLAN.md

@ -227,6 +227,50 @@ Color/output source coverage and remaining limits:
The temporary comparison adapter supplies I420 using the managed RGB conversion. Its output cannot independently The temporary comparison adapter supplies I420 using the managed RGB conversion. Its output cannot independently
validate that RGB conversion, even when both codec decoders agree on native planes. validate that RGB conversion, even when both codec decoders agree on native planes.
Intra-reference frame-extent correction after checkpoint `182f39ae5`, verified on 2026-09-05:
- Source comparison found that extension availability and extension length had been conflated.
`Av1IntraSuperblockEncoder.ChromaModeDecision.cs:1211-1240` and
`Av1IntraSuperblockEncoder.ModeDecision.cs:2393-2449` previously copied a complete adjacent extent whenever
its coding-order availability flag was true. Reference `av1/common/reconintra.c:1737-1742,1817-1820`
also clips the available count to the remaining coded frame extent, then repeats the final available
sample in the edge preparation at `reconintra.c:1149-1184`.
- The new 56x56 production partition case failed before the correction with `ArgumentOutOfRangeException`
at the top-right copy (`edge-extent-before.trx`: two existing 32x32 cases passed, then execution stopped
on the new failure). This is an implementation defect, not a search-performance hypothesis.
Bottom-edge reads also require the bound: `Buffer2DRegion{T}.cs:89-97` limits row width but resolves the
row index against the backing buffer, whose encoder border can contain samples outside the coded region.
- Both shared luma/chroma references and tiled candidate references now bound adjacent samples by the
plane's coded extent before endpoint repetition. The existing availability rules and allocation ownership
remain the governing contracts; no new guard, rejection policy, owner, or scratch buffer was introduced.
- The rectangular reference cases retain all twelve earlier checks and add six explicit clipped-extent
cases across 8, 10, and 12 bits and both orientations. They use a larger backing buffer with distinct
values beyond the coded region, fixed expected edge sequences, and destination sentinels.
The production mixed-partition test retains both 32x32 orientations and adds both 56x56 orientations.
- Final Release net11.0 build: zero errors and 1,009 existing warnings. Serialized Visual Studio VSTest:
**273/273 passed** in `edge-extent-final.trx` (20.3789 seconds), covering encoder frames, intra-superblocks,
HEIF encoder contracts, and the retained empty-transform cost-helper test. Roslynk reports zero compiler errors.
- Fresh optimized-reference decoding of four regenerated partition streams matches all 8,320 retained luma samples.
Twelve regenerated moving color streams match all 21,348 Y/U/V samples. Combined maximum error is **0** across
**29,668** samples, with **0** samples exceeding one. These are same-bitstream decoder/reconstruction comparisons;
they do not establish separate-encoder parity or performance. No benchmark was run.
The intra-edge investigation also confirmed these unresolved integration requirements:
- `Av1PredictionDecoder.cs:989-1837` owns separate directional preparation, edge smoothing, upsampling,
strength selection, and neighboring-mode selection. Native `reconintra.c:989-1082,1349-1381` uses endpoint
extension, rounded nonnegative smoothing kernels, and clipped signed four-tap half-sample interpolation.
No new numerical discrepancy in those arithmetic kernels has been established by this comparison.
- Encoder `Av1EncoderModeDecisionWorkspace.cs:50-53,133-135` retains four raw edge spans with one prefix sample.
The decoder needs writable prefix positions -1 and -2 and candidate-specific filtering; mutating those raw
encoder spans across mode trials would contaminate later candidates. `Av1EncoderBlockWorkspace.cs:143-144`
exposes transform scratch whose lifetime must be reconciled with directional prediction before sharing it.
- `Av1IntraSuperblockEncoder.ModeDecision.cs:2272-2466` draws tiled edges from both committed reconstruction
and the current candidate mosaic. Enabling filtering must preserve that distinction, coded extents,
chroma neighbor ownership, corner preparation, and smooth-neighbor-dependent thresholds across all callers.
The sequence flag remains disabled pending that complete integration. The decoder's private kernels are
not a substitute for the required shared closed-generic traversal and semantic-operator architecture.
### Required completion gates ### Required completion gates
Motion-controller investigation continued after correction checkpoint `578ec34d9`: Motion-controller investigation continued after correction checkpoint `578ec34d9`:

12
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs

@ -1208,7 +1208,12 @@ internal static partial class Av1IntraSuperblockEncoder
left[..height].Fill(hasAbove ? above[0] : TOperator.CreateSample(midpoint + 1)); left[..height].Fill(hasAbove ? above[0] : TOperator.CreateSample(midpoint + 1));
} }
int topRightCount = hasTopRight ? Math.Min(width, height) : 0; // Availability describes coding order, not the number of samples left at the frame boundary.
// A partially present adjacent block contributes only its coded samples before endpoint repetition.
int topRightCount = hasTopRight
? Math.Min(Math.Min(width, height), reconstructionPlane.Width - blockOrigin.X - width)
: 0;
if (hasTopRight) if (hasTopRight)
{ {
reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1) reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1)
@ -1219,7 +1224,10 @@ internal static partial class Av1IntraSuperblockEncoder
int topCount = width + topRightCount; int topCount = width + topRightCount;
above[topCount..].Fill(above[topCount - 1]); above[topCount..].Fill(above[topCount - 1]);
int bottomLeftCount = hasBottomLeft ? Math.Min(height, width) : 0; int bottomLeftCount = hasBottomLeft
? Math.Min(Math.Min(height, width), reconstructionPlane.Height - blockOrigin.Y - height)
: 0;
for (int row = height; row < height + bottomLeftCount; row++) for (int row = height; row < height + bottomLeftCount; row++)
{ {
left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1]; left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1];

16
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -2390,7 +2390,14 @@ internal static partial class Av1IntraSuperblockEncoder
left[..transformHeight].Fill(hasAbove ? above[0] : TOperator.CreateSample(midpoint + 1)); left[..transformHeight].Fill(hasAbove ? above[0] : TOperator.CreateSample(midpoint + 1));
} }
int topRightCount = hasTopRight ? Math.Min(transformWidth, transformHeight) : 0; // Candidate mosaics share the committed frame's coded extent. Padding beyond that extent is
// never a reference sample, even when coding order makes the adjacent block available.
int topRightCount = hasTopRight
? Math.Min(
Math.Min(transformWidth, transformHeight),
reconstructionPlane.Width - planeBlockOrigin.X - columnOffset - transformWidth)
: 0;
if (hasTopRight) if (hasTopRight)
{ {
if (transformRow > 0) if (transformRow > 0)
@ -2412,7 +2419,12 @@ internal static partial class Av1IntraSuperblockEncoder
int topCount = transformWidth + topRightCount; int topCount = transformWidth + topRightCount;
above[topCount..].Fill(above[topCount - 1]); above[topCount..].Fill(above[topCount - 1]);
int bottomLeftCount = hasBottomLeft ? Math.Min(transformHeight, transformWidth) : 0; int bottomLeftCount = hasBottomLeft
? Math.Min(
Math.Min(transformHeight, transformWidth),
reconstructionPlane.Height - planeBlockOrigin.Y - rowOffset - transformHeight)
: 0;
if (hasBottomLeft) if (hasBottomLeft)
{ {
if (transformColumn > 0) if (transformColumn > 0)

62
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -30,35 +30,42 @@ public class Av1EncoderFrameTests
private const int Yuv444 = (int)Av1ColorFormat.Yuv444; private const int Yuv444 = (int)Av1ColorFormat.Yuv444;
[Theory] [Theory]
[InlineData(EightBit, false, false)] [InlineData(EightBit, false, false, false)]
[InlineData(EightBit, false, true)] [InlineData(EightBit, false, true, false)]
[InlineData(EightBit, true, false)] [InlineData(EightBit, true, false, false)]
[InlineData(EightBit, true, true)] [InlineData(EightBit, true, true, false)]
[InlineData(TenBit, false, false)] [InlineData(TenBit, false, false, false)]
[InlineData(TenBit, false, true)] [InlineData(TenBit, false, true, false)]
[InlineData(TenBit, true, false)] [InlineData(TenBit, true, false, false)]
[InlineData(TenBit, true, true)] [InlineData(TenBit, true, true, false)]
[InlineData(TwelveBit, false, false)] [InlineData(TwelveBit, false, false, false)]
[InlineData(TwelveBit, false, true)] [InlineData(TwelveBit, false, true, false)]
[InlineData(TwelveBit, true, false)] [InlineData(TwelveBit, true, false, false)]
[InlineData(TwelveBit, true, true)] [InlineData(TwelveBit, true, true, false)]
public void RectangularIntraReferencesExtendTheLastAvailableSample(int bitDepthValue, bool transpose, bool extensionAvailable) [InlineData(EightBit, false, true, true)]
[InlineData(EightBit, true, true, true)]
[InlineData(TenBit, false, true, true)]
[InlineData(TenBit, true, true, true)]
[InlineData(TwelveBit, false, true, true)]
[InlineData(TwelveBit, true, true, true)]
public void RectangularIntraReferencesExtendTheLastAvailableSample(int bitDepthValue, bool transpose, bool extensionAvailable, bool limitedExtent)
{ {
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
if (bitDepth == Av1BitDepth.EightBit) if (bitDepth == Av1BitDepth.EightBit)
{ {
AssertRectangularIntraReferences<byte, Av1IntraSuperblockEncoder.ByteOperator>(bitDepth, transpose, extensionAvailable); AssertRectangularIntraReferences<byte, Av1IntraSuperblockEncoder.ByteOperator>(bitDepth, transpose, extensionAvailable, limitedExtent);
} }
else else
{ {
AssertRectangularIntraReferences<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(bitDepth, transpose, extensionAvailable); AssertRectangularIntraReferences<ushort, Av1IntraSuperblockEncoder.UInt16Operator>(bitDepth, transpose, extensionAvailable, limitedExtent);
} }
} }
private static void AssertRectangularIntraReferences<TSample, TOperator>( private static void AssertRectangularIntraReferences<TSample, TOperator>(
Av1BitDepth bitDepth, Av1BitDepth bitDepth,
bool transpose, bool transpose,
bool extensionAvailable) bool extensionAvailable,
bool limitedExtent)
where TSample : unmanaged where TSample : unmanaged
where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample> where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator<TSample>
{ {
@ -76,14 +83,19 @@ public class Av1EncoderFrameTests
// Native reconintra.c extends a four-sample edge through its four-sample neighbor, then // Native reconintra.c extends a four-sample edge through its four-sample neighbor, then
// repeats sample seven to cover the twenty samples required by a 4x16 directional ray. // repeats sample seven to cover the twenty samples required by a 4x16 directional ray.
// These explicit offsets also distinguish unavailable neighbors from available extension. // A clipped frame leaves only two adjacent samples; backing-buffer values beyond the region
int[] shortEdge = extensionAvailable // must not contribute. These explicit offsets distinguish all three extension cases.
? [0, 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7] int[] shortEdge = limitedExtent
: [0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]; ? [0, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
: extensionAvailable
int[] longEdge = extensionAvailable ? [0, 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] : [0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3];
: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15];
int[] longEdge = limitedExtent
? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 17, 17]
: extensionAvailable
? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15];
int[] expectedAbove = transpose ? longEdge : shortEdge; int[] expectedAbove = transpose ? longEdge : shortEdge;
int[] expectedLeft = transpose ? shortEdge : longEdge; int[] expectedLeft = transpose ? shortEdge : longEdge;
@ -96,7 +108,7 @@ public class Av1EncoderFrameTests
// The exact-sized interior includes the corner and twenty projected samples. Sentinel samples // The exact-sized interior includes the corner and twenty projected samples. Sentinel samples
// on either side detect writes outside the reference view, including the former 2*long-edge span. // on either side detect writes outside the reference view, including the former 2*long-edge span.
Av1IntraSuperblockEncoder.ModeDecision<TSample, TOperator>.PrepareReferenceSamples( Av1IntraSuperblockEncoder.ModeDecision<TSample, TOperator>.PrepareReferenceSamples(
plane.GetRegion(), plane.GetRegion(0, 0, limitedExtent ? width + 3 : 33, limitedExtent ? height + 3 : 33),
new Point(1, 1), new Point(1, 1),
width, width,
height, height,

35
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

@ -2918,11 +2918,12 @@ public class Av1IntraSuperblockEncoderTests
/// Verifies that mixed partition trials and final writing retain the decoder's reconstruction order. /// Verifies that mixed partition trials and final writing retain the decoder's reconstruction order.
/// </summary> /// </summary>
[Theory] [Theory]
[InlineData(false)] [InlineData(false, 32)]
[InlineData(true)] [InlineData(true, 32)]
public void ProductionMixedPartitionsPreserveReconstructionOrder(bool transpose) [InlineData(false, 56)]
[InlineData(true, 56)]
public void ProductionMixedPartitionsPreserveReconstructionOrder(bool transpose, int size)
{ {
const int Size = 32;
const int QIndex = 4; const int QIndex = 4;
ObuColorConfig colorConfig = new() ObuColorConfig colorConfig = new()
{ {
@ -2932,12 +2933,12 @@ public class Av1IntraSuperblockEncoderTests
BitDepth = Av1BitDepth.EightBit BitDepth = Av1BitDepth.EightBit
}; };
using Av1EncoderFrameBuffer<byte> source = new(Configuration.Default, Size, Size, 8, Av1ColorFormat.Yuv400, 0, 0); using Av1EncoderFrameBuffer<byte> source = new(Configuration.Default, size, size, 8, Av1ColorFormat.Yuv400, 0, 0);
using Av1EncoderFrameBuffer<byte> reconstruction = new(Configuration.Default, Size, Size, 8, Av1ColorFormat.Yuv400, 0, 0); using Av1EncoderFrameBuffer<byte> reconstruction = new(Configuration.Default, size, size, 8, Av1ColorFormat.Yuv400, 0, 0);
Buffer2DRegion<byte> sourcePlane = source.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion<byte> sourcePlane = source.Frame.CodedView.GetPlane(Av1Plane.Y);
for (int y = 0; y < Size; y++) for (int y = 0; y < size; y++)
{ {
for (int x = 0; x < Size; x++) for (int x = 0; x < size; x++)
{ {
// The lower-right quadrant contains two different square surfaces beside one vertical // The lower-right quadrant contains two different square surfaces beside one vertical
// surface. Transposition exercises the corresponding horizontal reconstruction order. // surface. Transposition exercises the corresponding horizontal reconstruction order.
@ -2953,19 +2954,19 @@ public class Av1IntraSuperblockEncoderTests
} }
ClearPlane(reconstruction.Luma); ClearPlane(reconstruction.Luma);
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Size, Size, disallow4x4AllFrames: false); using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, size, size, disallow4x4AllFrames: false);
Av1PictureControlSet template = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, QIndex); Av1PictureControlSet template = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, QIndex);
using Av1EncoderPictureBuffer picture = new( using Av1EncoderPictureBuffer picture = new(
Configuration.Default, template.Sequence.SequenceHeader, template.Parent.FrameHeader, Size, Size, disallow4x4AllFrames: false); Configuration.Default, template.Sequence.SequenceHeader, template.Parent.FrameHeader, size, size, disallow4x4AllFrames: false);
using Av1EncoderCoefficientBuffer coefficients = new(Configuration.Default, template.Sequence.SequenceHeader, Size, Size); using Av1EncoderCoefficientBuffer coefficients = new(Configuration.Default, template.Sequence.SequenceHeader, size, size);
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(picture.Picture, 8192); using Av1SymbolEncoder symbolEncoder = CreateTileSymbolEncoder(picture.Picture, 8192);
Av1TileEncoder tileWriter = new( Av1TileEncoder tileWriter = new(
symbolEncoder, source.Frame, reconstruction.Frame, picture.Picture, coefficients, superblockWorkspace, blockWorkspace, effort: 9); symbolEncoder, source.Frame, reconstruction.Frame, picture.Picture, coefficients, superblockWorkspace, blockWorkspace, effort: 9);
byte[] payload = WriteCompleteTileObu(picture.Picture, tileWriter, Size, Size); byte[] payload = WriteCompleteTileObu(picture.Picture, tileWriter, size, size);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
decoder.DecodeSequenceReference(payload, null, null); decoder.DecodeSequenceReference(payload, null, null);
Av1FrameInfo decodedInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo decodedInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
@ -2973,10 +2974,10 @@ public class Av1IntraSuperblockEncoderTests
Buffer2DRegion<byte> decodedPlane = decodedFrame.DeriveBlockPointer(Av1Plane.Y, 0, 0); Buffer2DRegion<byte> decodedPlane = decodedFrame.DeriveBlockPointer(Av1Plane.Y, 0, 0);
Buffer2DRegion<byte> retainedPlane = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion<byte> retainedPlane = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y);
bool hasMixedPartition = false; bool hasMixedPartition = false;
for (int y = 0; y < Size; y++) for (int y = 0; y < size; y++)
{ {
Assert.Equal(retainedPlane.DangerousGetRowSpan(y).ToArray(), decodedPlane.DangerousGetRowSpan(y).ToArray()); Assert.Equal(retainedPlane.DangerousGetRowSpan(y).ToArray(), decodedPlane.DangerousGetRowSpan(y).ToArray());
for (int x = 0; x < Size; x += 4) for (int x = 0; x < size; x += 4)
{ {
Point position = new(x >> 2, y >> 2); Point position = new(x >> 2, y >> 2);
Av1PartitionType partition = decodedInfo.GetModeInfoAt(position).PartitionType; Av1PartitionType partition = decodedInfo.GetModeInfoAt(position).PartitionType;
@ -2995,9 +2996,9 @@ public class Av1IntraSuperblockEncoderTests
TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", nameof(this.ProductionMixedPartitionsPreserveReconstructionOrder)); TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", nameof(this.ProductionMixedPartitionsPreserveReconstructionOrder));
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
File.WriteAllBytes(Path.Combine(directory, $"{transpose}.obu"), payload); File.WriteAllBytes(Path.Combine(directory, $"{size}-{transpose}.obu"), payload);
using FileStream raw = File.Create(Path.Combine(directory, $"{transpose}.retained.yuv")); using FileStream raw = File.Create(Path.Combine(directory, $"{size}-{transpose}.retained.yuv"));
for (int y = 0; y < Size; y++) for (int y = 0; y < size; y++)
{ {
raw.Write(retainedPlane.DangerousGetRowSpan(y)); raw.Write(retainedPlane.DangerousGetRowSpan(y));
} }

Loading…
Cancel
Save