diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 72fd7fe1f3..4d665c3e9c 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -845,6 +845,7 @@ Encoder verification contract: - [~] The same internal frame operation now produces 4:2:0, 4:2:2, and 4:4:4 payloads at 8, 10, and 12 bits. Twenty-one color cases cover constant and spatially varying input at aligned dimensions plus odd 13x11 visible dimensions for every chroma geometry. The production decoder consumes every payload, the decoded output retains non-neutral chroma, and current-main `aomdec` accepts all 29 monochrome and color outputs. The odd-dimension decoded-frame MD5 values are `d025030a44750e703bcca9a6d3e131e8` (4:2:0), `75936a76104ec678c7a1f10b4c560172` (4:2:2), and `044026b1518aa94f3c2db82c7247c0d6` (4:4:4). This proves legal current-libaom payload syntax across native plane geometries; it does not yet prove target quality or native-plane equality with an independently encoded reference. - [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips. - [x] Combined-frame OBU output now counts the byte-aligned frame and tile-group headers, non-final tile-size fields, and owned tile payloads before emitting the OBU size. It retains only the small allocator-owned header scratch and writes each entropy-coded tile span directly from its detached owner, removing the second file-sized allocator rent and complete-payload copy. A 64 KiB regression proves exactly one sub-payload-sized byte rent with a balanced return and verifies the exact streamed tile tail; the existing two-tile round trip proves size-prefix and ordering parity. The focused writer and production-frame set passes 32 of 32 direct net11 VSTest cases, current-main `aomdec` accepts all 29 generated native-format payloads, and the complete HEIF/AV1 namespace passes 8,860 of 8,860 cases with zero failures or skips. +- [x] Finalized fixed-block decisions now set the block-level transform-skip flag only when every retained luma and coded chroma transform has zero EOB, matching current libaom's conjunction of per-plane skip state. The previous always-false flag produced legal but redundant non-skip and zero-coefficient syntax. Monochrome and 4:2:0 regressions prove both branches from actual coefficient state; the focused decision and production-frame set passes 32 of 32 direct net11 VSTest cases. Current-main `aomdec` accepts all 29 regenerated payloads, the recorded decoded-frame MD5s are unchanged, and affected 16x16 constant 8-bit and 10-bit payloads are one byte smaller. The complete HEIF/AV1 namespace passes 8,862 of 8,862 cases with zero failures or skips. ### 7. Write complete AVIF output diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs index df9453217c..ea6b469243 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs @@ -192,8 +192,10 @@ internal static partial class Av1IntraSuperblockEncoder ref lumaState); this.codedAreaLuma += LumaTransformSize.GetSize2d(); + bool skipTransform = lumaState.EndOfBlock == 0; if (this.source.IsMonochrome) { + modeInfo.Block.Skip = skipTransform; return; } @@ -224,6 +226,8 @@ internal static partial class Av1IntraSuperblockEncoder this.redCoefficients[this.codedAreaChroma..], ref redState); + // A block-level skip is valid only when every coded plane reconstructs directly from its prediction. + modeInfo.Block.Skip = skipTransform && blueState.EndOfBlock == 0 && redState.EndOfBlock == 0; this.codedAreaChroma += chromaTransformSize.GetSize2d(); } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs index a4c1c7fc2b..e0f7052748 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs @@ -258,6 +258,85 @@ public class Av1IntraSuperblockEncoderTests Assert.True(encoded.GetSpan().SequenceEqual(tileWriter.GetTileData(0))); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MarksAllZeroTransformBlockAsSkipped(bool isMonochrome) + { + const int Width = 8; + const int Height = 8; + Av1ColorFormat colorFormat = isMonochrome ? Av1ColorFormat.Yuv400 : Av1ColorFormat.Yuv420; + ObuColorConfig colorConfig = new() + { + IsMonochrome = isMonochrome, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + }; + + using Av1EncoderFrameBuffer source = new( + Configuration.Default, + Width, + Height, + 8, + colorFormat, + 1, + 1); + + using Av1EncoderFrameBuffer reconstruction = new( + Configuration.Default, + Width, + Height, + 8, + colorFormat, + 1, + 1); + + FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.Y), 128); + ClearPlane(reconstruction.Luma); + if (!isMonochrome) + { + FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.U), 128); + FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.V), 128); + ClearPlane(Assert.IsType>(reconstruction.ChromaBlue)); + ClearPlane(Assert.IsType>(reconstruction.ChromaRed)); + } + + using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true); + Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, qIndex: 37); + using Av1EncoderCoefficientBuffer coefficients = new( + Configuration.Default, + picture.Sequence.SequenceHeader, + Width, + Height); + + using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); + using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); + Av1Superblock superblock = new() + { + Workspace = superblockWorkspace, + TileInfo = new Av1TileInfo(0, 0, picture.Parent.FrameHeader), + Index = 0 + }; + + Av1IntraSuperblockEncoder.Encode( + source.Frame, + reconstruction.Frame, + picture, + superblock, + coefficients, + blockWorkspace); + + ref Av1MacroBlockModeInfo block = ref picture.GetMacroBlockModeInfo(default); + Assert.True(block.Block.Skip); + Assert.Equal((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.Y)[0].EndOfBlock); + if (!isMonochrome) + { + Assert.Equal((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.U)[0].EndOfBlock); + Assert.Equal((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.V)[0].EndOfBlock); + } + } + [Fact] public void PreservesTwelveBitMonochromeReconstructionPrecision() { @@ -558,6 +637,14 @@ public class Av1IntraSuperblockEncoderTests } } + private static void FillPlane(Buffer2DRegion plane, byte value) + { + for (int y = 0; y < plane.Height; y++) + { + plane.DangerousGetRowSpan(y).Fill(value); + } + } + private static void ClearPlane(Buffer2D plane) where TSample : unmanaged {