diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index da762411f4..b382ee6cec 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -819,7 +819,7 @@ Encoder verification contract: ### 6. Build the complete AV1 frame encoder - [~] SIMD-first RGB-to-native-plane conversion now feeds eight-bit and high-bit-depth bordered AV1 source frames directly, preserving ImageSharp's arbitrary packed-pixel input contract without an intermediate full-frame native-plane copy. -- [~] Forward transform families, transform workspace, and an allocation-free DC intra block boundary exist locally. For eight-bit and high-bit-depth samples, the composed boundary now follows current libaom's encoder order: predict into the reconstruction plane, subtract prediction from source, transform, quantize into separate qcoeff and dqcoeff storage, retain EOB and transform type, and inverse-transform only when EOB is nonzero so later blocks consume decoder-identical references. Prediction and subtraction retain their SIMD-first operators, independent source and reconstruction strides are preserved, and no frame-sized or per-block buffer is introduced. One reusable 61 KiB allocator owner supplies tightly packed residual, aligned transform-coefficient, dequantized-coefficient, and transform scratch spans across transform blocks; quantized coefficients write directly to the retained frame coefficient owner instead of being duplicated. Stage-by-stage scalar-oracle, padding, retained-syntax, and zero-allocation coverage passes 6 of 6 through direct net11 VSTest in Release. Frame traversal still needs to select blocks, gather contiguous left references, and supply coefficient-owner slices. +- [~] Forward transform families, transform workspace, and an allocation-free DC intra block boundary exist locally. For eight-bit and high-bit-depth samples, the composed boundary now follows current libaom's encoder order: predict into the reconstruction plane, subtract prediction from source, transform, quantize into separate qcoeff and dqcoeff storage, retain EOB and transform type, and inverse-transform only when EOB is nonzero so later blocks consume decoder-identical references. Prediction and subtraction retain their SIMD-first operators, independent source and reconstruction strides are preserved, and no frame-sized or per-block buffer is introduced. The block boundary consumes the real bordered encoder-plane regions and indexes their one-segment owner directly; this preserves physical row strides without a row copy and avoids the per-call enumerator allocation exposed by the initial array-only test. One reusable 61 KiB allocator owner supplies tightly packed residual, aligned transform-coefficient, dequantized-coefficient, and transform scratch spans across transform blocks; quantized coefficients write directly to the retained frame coefficient owner instead of being duplicated. Stage-by-stage scalar-oracle, physical-border, retained-syntax, and zero-allocation coverage passes 6 of 6 through direct net11 VSTest in Release. Frame traversal still needs to select blocks, gather contiguous left references, and supply coefficient-owner slices. - [~] Symbol writer, coefficient writer, and tile writer fragments exist locally. - [~] A non-owning encoder-frame view now separates visible conversion regions from coded regions and performs complete left, top, right, bottom, and corner extension across each bordered plane. Current libaom uses 8-sample-aligned coded dimensions, a 32-sample-aligned luma stride with chroma stride derived from it, and a 64-pixel luma border for non-resized all-intra encoding. One operation-ready frame owner now rents the aligned Y, U, and V storage contiguously, exposes non-owning `Buffer2D` plane views, and returns the rent exactly once. A 4K 4:2:0 frame occupies about 13.0 MiB at 8-bit or 26.0 MiB at 10/12-bit; source and reconstruction therefore remain distinct frame owners rather than adding a full-frame copy. The corrected tests use this real ownership path and verify the exact 54 KiB 64x64 4:2:0 rent. The frame-encoder boundary converts packed pixels directly into the source owner before extension; the containing encode operation still needs to instantiate matching source and reconstruction owners with ordinary `using` lifetimes. - [~] Temporal delimiter, sequence header, frame header, and combined-frame tile-group writing exist locally. The remaining required metadata, padding, and encoder-wide syntax paths are not complete. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderFrame.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderFrame.cs index ba2cf49d3e..69bd3974b2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderFrame.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderFrame.cs @@ -332,6 +332,19 @@ internal readonly struct Av1EncoderFrame } } + /// + /// Gets a writable coded component plane. + /// + /// The requested component plane. + /// The complete coded plane region. + public Buffer2DRegion GetPlane(Av1Plane plane) + => plane switch + { + Av1Plane.Y => this.luma, + Av1Plane.U => this.chromaBlue, + _ => this.chromaRed + }; + /// public Span GetLumaRowSpan(int row) => this.luma.DangerousGetRowSpan(row); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs index d4c2e5ddcd..e0341b2bf4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; @@ -14,6 +15,129 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; /// internal static class Av1TransformBlockEncoder { + /// + /// Encodes and reconstructs one eight-bit lossy DC intra block in contiguous encoder planes. + /// + /// The reusable residual, coefficient, and transform storage. + /// The coded source plane. + /// The coded reconstruction plane. + /// The block origin in plane samples. + /// The contiguous top reference samples. + /// The contiguous left reference samples. + /// Whether the left reference is available. + /// Whether the top reference is available. + /// The retained entropy-coding coefficients. + /// The selected transform dimensions. + /// The selected compound transform type. + /// The segment quantizer index. + /// The plane DC quantizer adjustment. + /// The plane AC quantizer adjustment. + /// The component plane containing the block. + /// The retained transform type and end-of-block syntax. + public static void EncodeIntraDcLossy( + Av1EncoderBlockWorkspace workspace, + Buffer2DRegion source, + Buffer2DRegion reconstruction, + Point blockOrigin, + ReadOnlySpan above, + ReadOnlySpan left, + bool hasLeft, + bool hasAbove, + Span quantizedCoefficients, + Av1TransformSize transformSize, + Av1TransformType transformType, + int qIndex, + int dcDeltaQ, + int acDeltaQ, + Av1Plane plane, + ref Av1EncoderTransformBlockState state) + { + ReadOnlySpan sourceSamples = GetPlaneSpan(source, blockOrigin); + Span reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin); + + EncodeIntraDcLossyContiguous( + workspace, + sourceSamples, + source.Stride, + reconstructionSamples, + reconstruction.Stride, + above, + left, + hasLeft, + hasAbove, + quantizedCoefficients, + transformSize, + transformType, + qIndex, + dcDeltaQ, + acDeltaQ, + plane, + ref state); + } + + /// + /// Encodes and reconstructs one high-bit-depth lossy DC intra block in contiguous encoder planes. + /// + /// The reusable residual, coefficient, and transform storage. + /// The coded source plane. + /// The coded reconstruction plane. + /// The block origin in plane samples. + /// The contiguous top reference samples. + /// The contiguous left reference samples. + /// Whether the left reference is available. + /// Whether the top reference is available. + /// The retained entropy-coding coefficients. + /// The selected transform dimensions. + /// The selected compound transform type. + /// The segment quantizer index. + /// The plane DC quantizer adjustment. + /// The plane AC quantizer adjustment. + /// The component plane containing the block. + /// The coded sample bit depth. + /// The retained transform type and end-of-block syntax. + public static void EncodeIntraDcLossy( + Av1EncoderBlockWorkspace workspace, + Buffer2DRegion source, + Buffer2DRegion reconstruction, + Point blockOrigin, + ReadOnlySpan above, + ReadOnlySpan left, + bool hasLeft, + bool hasAbove, + Span quantizedCoefficients, + Av1TransformSize transformSize, + Av1TransformType transformType, + int qIndex, + int dcDeltaQ, + int acDeltaQ, + Av1Plane plane, + Av1BitDepth bitDepth, + ref Av1EncoderTransformBlockState state) + { + ReadOnlySpan sourceSamples = GetPlaneSpan(source, blockOrigin); + Span reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin); + + EncodeIntraDcLossyContiguous( + workspace, + sourceSamples, + source.Stride, + reconstructionSamples, + reconstruction.Stride, + above, + left, + hasLeft, + hasAbove, + quantizedCoefficients, + transformSize, + transformType, + qIndex, + dcDeltaQ, + acDeltaQ, + plane, + bitDepth, + ref state); + } + /// /// Encodes and reconstructs one eight-bit lossy DC intra block. /// @@ -34,7 +158,7 @@ internal static class Av1TransformBlockEncoder /// The plane AC quantizer adjustment. /// The component plane containing the block. /// The retained transform type and end-of-block syntax. - public static void EncodeIntraDcLossy( + private static void EncodeIntraDcLossyContiguous( Av1EncoderBlockWorkspace workspace, ReadOnlySpan source, int sourceStride, @@ -108,7 +232,7 @@ internal static class Av1TransformBlockEncoder /// The component plane containing the block. /// The coded sample bit depth. /// The retained transform type and end-of-block syntax. - public static void EncodeIntraDcLossy( + private static void EncodeIntraDcLossyContiguous( Av1EncoderBlockWorkspace workspace, ReadOnlySpan source, int sourceStride, @@ -236,4 +360,16 @@ internal static class Av1TransformBlockEncoder state.TransformType = transformType; } + + private static Span GetPlaneSpan(Buffer2DRegion plane, Point blockOrigin) + where TSample : unmanaged + { + int offset = + ((plane.Bounds.Y + blockOrigin.Y) * plane.Stride) + + plane.Bounds.X + + blockOrigin.X; + + // Encoder planes wrap one contiguous frame owner, so direct segment access retains physical strides without an enumerator or row copy. + return plane.Buffer.FastMemoryGroup[0].Span[offset..]; + } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs index dcbaa6e0b6..12297cabd2 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; @@ -49,6 +50,26 @@ public class Av1TransformBlockEncoderTests byte[] left = new byte[height]; int[] expectedQuantized = new int[coefficientCount + 7]; int[] actualQuantized = new int[coefficientCount + 7]; + using Av1EncoderFrameBuffer sourceFrame = new( + Configuration.Default, + width, + height, + 8, + Av1ColorFormat.Yuv400, + 0, + 0); + + using Av1EncoderFrameBuffer reconstructionFrame = new( + Configuration.Default, + width, + height, + 8, + Av1ColorFormat.Yuv400, + 0, + 0); + + Buffer2DRegion sourcePlane = sourceFrame.Frame.CodedView.GetPlane(Av1Plane.Y); + Buffer2DRegion reconstructionPlane = reconstructionFrame.Frame.CodedView.GetPlane(Av1Plane.Y); using Av1EncoderBlockWorkspace expectedWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace actualWorkspace = new(Configuration.Default); FillSource(source, SourceStride, width, height, byte.MaxValue); @@ -57,6 +78,12 @@ public class Av1TransformBlockEncoderTests Array.Fill(expectedQuantized, int.MinValue); Array.Fill(actualQuantized, int.MinValue); + for (int y = 0; y < height; y++) + { + source.AsSpan(y * SourceStride, width).CopyTo(sourcePlane.DangerousGetRowSpan(y)); + reconstructionPlane.DangerousGetRowSpan(y).Fill(211); + } + for (int i = 0; i < above.Length; i++) { above[i] = (byte)(37 + (i * 11)); @@ -115,10 +142,9 @@ public class Av1TransformBlockEncoderTests Av1EncoderTransformBlockState actualState = default; Av1TransformBlockEncoder.EncodeIntraDcLossy( actualWorkspace, - source, - SourceStride, - actualReconstruction, - ReconstructionStride, + sourcePlane, + reconstructionPlane, + Point.Empty, above, left, true, @@ -132,10 +158,21 @@ public class Av1TransformBlockEncoderTests Av1Plane.Y, ref actualState); + for (int y = 0; y < height; y++) + { + reconstructionPlane.DangerousGetRowSpan(y).CopyTo( + actualReconstruction.AsSpan(y * ReconstructionStride, width)); + } + + int physicalRow = reconstructionPlane.Bounds.Y; + int physicalColumn = reconstructionPlane.Bounds.X; + ReadOnlySpan completeRow = reconstructionFrame.Luma.DangerousGetRowSpan(physicalRow); Assert.Equal(expectedReconstruction, actualReconstruction); Assert.Equal(expectedQuantized, actualQuantized); Assert.Equal(expectedState.EndOfBlock, actualState.EndOfBlock); Assert.Equal(expectedState.TransformType, actualState.TransformType); + Assert.Equal(0, completeRow[physicalColumn - 1]); + Assert.Equal(0, completeRow[physicalColumn + width]); } /// @@ -158,6 +195,10 @@ public class Av1TransformBlockEncoderTests ushort[] left = new ushort[height]; int[] expectedQuantized = new int[coefficientCount + 7]; int[] actualQuantized = new int[coefficientCount + 7]; + using Buffer2D sourceBuffer = Buffer2D.WrapMemory(source, SourceStride, height, SourceStride); + using Buffer2D reconstructionBuffer = + Buffer2D.WrapMemory(actualReconstruction, ReconstructionStride, height, ReconstructionStride); + using Av1EncoderBlockWorkspace expectedWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace actualWorkspace = new(Configuration.Default); FillSource(source, SourceStride, width, height, (1 << bitDepth.GetBitCount()) - 1); @@ -227,10 +268,9 @@ public class Av1TransformBlockEncoderTests Av1EncoderTransformBlockState actualState = default; Av1TransformBlockEncoder.EncodeIntraDcLossy( actualWorkspace, - source, - SourceStride, - actualReconstruction, - ReconstructionStride, + new Buffer2DRegion(sourceBuffer), + new Buffer2DRegion(reconstructionBuffer), + Point.Empty, above, left, true, @@ -269,6 +309,14 @@ public class Av1TransformBlockEncoderTests ushort[] above10 = new ushort[Stride]; ushort[] left10 = new ushort[Stride]; int[] quantized = new int[coefficientCount]; + using Buffer2D sourceBuffer8 = Buffer2D.WrapMemory(source8, Stride, Stride); + using Buffer2D reconstructionBuffer8 = Buffer2D.WrapMemory(reconstruction8, Stride, Stride); + using Buffer2D sourceBuffer10 = Buffer2D.WrapMemory(source10, Stride, Stride); + using Buffer2D reconstructionBuffer10 = Buffer2D.WrapMemory(reconstruction10, Stride, Stride); + Buffer2DRegion sourcePlane8 = new(sourceBuffer8); + Buffer2DRegion reconstructionPlane8 = new(reconstructionBuffer8); + Buffer2DRegion sourcePlane10 = new(sourceBuffer10); + Buffer2DRegion reconstructionPlane10 = new(reconstructionBuffer10); using Av1EncoderBlockWorkspace workspace = new(Configuration.Default); FillSource(source8, Stride, Stride, Stride, byte.MaxValue); FillSource(source10, Stride, Stride, Stride, 1023); @@ -280,10 +328,9 @@ public class Av1TransformBlockEncoderTests Av1TransformBlockEncoder.EncodeIntraDcLossy( workspace, - source8, - Stride, - reconstruction8, - Stride, + sourcePlane8, + reconstructionPlane8, + Point.Empty, above8, left8, true, @@ -299,10 +346,9 @@ public class Av1TransformBlockEncoderTests Av1TransformBlockEncoder.EncodeIntraDcLossy( workspace, - source10, - Stride, - reconstruction10, - Stride, + sourcePlane10, + reconstructionPlane10, + Point.Empty, above10, left10, true, @@ -322,10 +368,9 @@ public class Av1TransformBlockEncoderTests { Av1TransformBlockEncoder.EncodeIntraDcLossy( workspace, - source8, - Stride, - reconstruction8, - Stride, + sourcePlane8, + reconstructionPlane8, + Point.Empty, above8, left8, true, @@ -341,10 +386,9 @@ public class Av1TransformBlockEncoderTests Av1TransformBlockEncoder.EncodeIntraDcLossy( workspace, - source10, - Stride, - reconstruction10, - Stride, + sourcePlane10, + reconstructionPlane10, + Point.Empty, above10, left10, true,