diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 9f4ed3d1f2..fa5224d13c 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -45,6 +45,9 @@ Reconciled with the worktree on 2026-09-05. Current interpolation-search checkpoint, implemented on 2026-09-05 with focused verification in progress: - [x] The requested in-progress tree was committed as `433afd1a9` before further encoder work. That commit is a checkpoint, not a claim of completed interpolation or codec delivery. +- [x] The subsequent partition/interpolation/lossless correction was committed as `7cf7fc4` after the 225-case affected encoder run and exact current-main native comparison. +- [x] Odd-sized color sequence verification exposed and corrected two further production defects. Empty inter luma transforms now retain the inferred DCT type before chroma inherits it; normalizing only during writing was too late. Region-major coefficient writing now rounds chroma end coordinates in 4x4 units, preserving the shared minimum chroma transform on sub-8x8 partitions instead of truncating it away. Both rules match current libaom's transform-type inference and `av1_write_intra_coeffs_mb` region bounds. The corrections add no allocation or sample copy. +- [x] All 12 moving-color sequence cases pass with SIMD enabled and disabled: 8/10/12-bit 4:2:0 at effort eight, and all three bit depths across 4:2:0/4:2:2/4:4:4 at effort nine. The 23x19 sources require actual inter motion and subsampled chroma phases. Current-main libaom decodes all 24 emitted frames with exact native Y/U/V equality. Evidence: `artifacts/TestResults/av1-interpolation-color-sequence-20260905/color-sequence-r3.trx`, `color-sequence-scalar-r3.trx`, and the adjacent raw plane outputs under `tests/Images/ActualOutput/Heif/Av1/SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion`. The affected public/frame/superblock set passes 237 cases; the expanded encoder/entropy set passes 2,524, with no failures or skips. Evidence: `artifacts/TestResults/av1-interpolation-partition-broad-20260905/color-broad-r3.trx` and `color-encoder-entropy-r3.trx`. The final Release build and Roslyn compiler/analyzer passes have no errors. This closes the identified subsampled inter syntax/reconstruction gaps, not the remaining end-to-end performance and complete codec release matrix. - [x] Production interpolation verification now forces Smooth and Sharp at effort eight, both dual-filter axis orders at effort nine, and native 10/12-bit two-axis half-sample motion. The tests assert actual retained filter symbols, vectors, exact reconstruction, and no allocator rent during tile coding. The sequence fixture now uses the retained-reference decode contract; the still-image buffer-transfer API intentionally releases the reference map and cannot decode dependent samples in succession. - [x] This verification exposed a live partition traversal defect: after search changed an earlier node's child count, a later node could consume an unrelated entry from the initial flat 8x8 skeleton. Unsearched intra partitions now derive their default from the current block size, preserving the geometry-driven traversal used by current libaom. Wide and tall lossless regressions cover clipped parents and superblock boundaries at efforts nine and ten. - [x] The affected frame encoder, intra-superblock encoder, and public HEIF encoder set passes all 225 cases on the corrected tree with zero failures or skips. Evidence: `artifacts/TestResults/av1-interpolation-partition-broad-20260905/partition-broad-r3.trx`. This is the affected encoder surface, not the full codec release matrix. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs index 2c810a0741..dc29dfdbc5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ReferenceModeDecision.cs @@ -1100,6 +1100,13 @@ internal static partial class Av1IntraSuperblockEncoder out Av1EncoderTransformBlockState emptyLumaState, out long emptyLumaDistortion); + // Empty luma transforms signal no transform type. Chroma inherits the decoder's inferred DCT + // type, not the last searched luma type, so normalize before evaluating either chroma plane. + if (lumaState.EndOfBlock == 0) + { + lumaState.TransformType = Av1TransformType.DctDct; + } + ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs index 480f6d80d6..a757f23c94 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs @@ -2223,8 +2223,11 @@ internal partial class Av1TileWriter { int chromaRegionRow = regionRow >> subsamplingY; int chromaRegionColumn = regionColumn >> subsamplingX; - int chromaUnitBottom = unitBottom >> subsamplingY; - int chromaUnitRight = unitRight >> subsamplingX; + + // Region limits count 4x4 units. Round the subsampled end upward so a chroma-owning + // 4x4, 4x8, or 8x4 luma block still emits its shared 4x4 chroma transform. + int chromaUnitBottom = Av1Math.RoundPowerOf2(unitBottom, subsamplingY); + int chromaUnitRight = Av1Math.RoundPowerOf2(unitRight, subsamplingX); EncodeTransformCoefficientRegion( pcs, entropyCodingContext, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs index 2ba63c7a05..3fce5999c3 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs @@ -237,6 +237,134 @@ public class Av1EncoderFrameTests Assert.Equal(new Size(Width, Height), decoded.Size); } + /// + /// Verifies dependent color samples with odd visible dimensions and motion across subsampled chroma phases. + /// + [Theory] + [InlineData(EightBit, Yuv420, 8)] + [InlineData(TenBit, Yuv420, 8)] + [InlineData(TwelveBit, Yuv420, 8)] + [InlineData(EightBit, Yuv420, 9)] + [InlineData(TenBit, Yuv420, 9)] + [InlineData(TwelveBit, Yuv420, 9)] + [InlineData(EightBit, Yuv422, 9)] + [InlineData(TenBit, Yuv422, 9)] + [InlineData(TwelveBit, Yuv422, 9)] + [InlineData(EightBit, Yuv444, 9)] + [InlineData(TenBit, Yuv444, 9)] + [InlineData(TwelveBit, Yuv444, 9)] + public void SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion(int bitDepthValue, int colorFormatValue, int effort) + { + const int Width = 23; + const int Height = 19; + const int QIndex = 17; + const int ByteToUInt16Scale = ushort.MaxValue / byte.MaxValue; + Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue; + ObuColorConfig colorConfig = CreateColorConfig(bitDepth, colorFormat); + ReadOnlySpan period = [0, 28, 40, 28, 0, -28, -40, -12]; + using Image source = new(Width, Height); + using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder( + Configuration.Default, Width, Height, colorConfig, QIndex, effort); + + string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion)); + string outputName = $"{bitDepth.GetBitCount()}-{colorFormat}-effort{effort}"; + using FileStream output = File.Create(Path.Combine(outputDirectory, outputName + ".obu")); + using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".managed.yuv"))); + using Av1Decoder decoder = new(Configuration.Default); + using MemoryStream sample = new(); + for (int frameIndex = 0; frameIndex < 2; frameIndex++) + { + // The second source translates all three channels by one luma sample on each axis. Chroma is + // converted independently by the production converter, so 4:2:0 and 4:2:2 cannot hide behind + // constant neutral planes. Odd dimensions also exercise each plane's visible-edge clipping. + for (int y = 0; y < Height; y++) + { + Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + int referenceY = Math.Min(y + frameIndex, Height - 1); + for (int x = 0; x < Width; x++) + { + int referenceX = Math.Min(x + frameIndex, Width - 1); + row[x] = new Rgb48( + (ushort)((128 + period[referenceX % period.Length]) * ByteToUInt16Scale), + (ushort)((128 + period[referenceY % period.Length]) * ByteToUInt16Scale), + (ushort)((128 + period[(referenceX + referenceY) % period.Length]) * ByteToUInt16Scale)); + } + } + + sample.SetLength(0); + if (frameIndex == 0) + { + encoder.EncodeKeyFrame(source.Frames.RootFrame, sample); + } + else + { + encoder.EncodeInterFrame(source.Frames.RootFrame, sample); + } + + sample.Position = 0; + sample.CopyTo(output); + decoder.DecodeSequenceReference(sample.ToArray(), null, null); + Av1FrameBuffer decoded = Assert.IsType>(decoder.FrameBuffer); + Assert.Equal(Width, decoded.Width); + Assert.Equal(Height, decoded.Height); + Assert.Equal(bitDepth, decoded.BitDepth); + for (int planeIndex = 0; planeIndex < 3; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane == Av1Plane.Y || !colorConfig.SubSamplingX ? 0 : 1; + int subsamplingY = plane == Av1Plane.Y || !colorConfig.SubSamplingY ? 0 : 1; + int planeHeight = (Height + subsamplingY) >> subsamplingY; + if (bitDepth == Av1BitDepth.EightBit) + { + Buffer2DRegion planeSamples = decoded.DeriveBlockPointer(plane, subsamplingX, subsamplingY); + for (int y = 0; y < planeHeight; y++) + { + rawOutput.Write(planeSamples.DangerousGetRowSpan(y)); + } + } + else + { + for (int y = 0; y < planeHeight; y++) + { + foreach (ushort value in decoded.GetHighBitDepthRowSpan(plane, y, subsamplingX, subsamplingY)) + { + // Raw high-bit-depth output uses explicit little-endian samples on every host. + rawOutput.Write(value); + } + } + } + } + } + + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + Assert.Equal(ObuFrameType.InterFrame, frameHeader.FrameType); + Assert.Equal(Av1InterpolationFilter.Switchable, frameHeader.InterpolationFilter); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + bool hasMotion = false; + bool hasFractionalChromaMotion = false; + foreach (Av1BlockModeInfo mode in frameInfo.GetModeInfos(Point.Empty, frameInfo.GetModeInfoCount(Point.Empty))) + { + if (mode.ReferenceFrames[0] == Av1ReferenceFrameType.Last) + { + Av1MotionVector vector = mode.MotionVectors[0]; + hasMotion |= vector.Column != 0 || vector.Row != 0; + + // A subsampled chroma phase repeats every two luma pixels, or sixteen Q3 motion units. + int chromaPhaseMask = (Av1MotionVector.SubpixelScale << 1) - 1; + hasFractionalChromaMotion |= + (colorConfig.SubSamplingX && (vector.Column & chromaPhaseMask) != 0) || + (colorConfig.SubSamplingY && (vector.Row & chromaPhaseMask) != 0); + } + } + + Assert.True(hasMotion); + if (colorConfig.SubSamplingX || colorConfig.SubSamplingY) + { + Assert.True(hasFractionalChromaMotion); + } + } + /// /// Verifies retained reference reconstruction and effort-dependent filter signaling through production sequence decoding. ///