From d813ae7d72a5c73b728951740fce5fc617bd93a5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 2 Sep 2026 11:15:07 +1000 Subject: [PATCH] Fix AV1 tile coefficient contexts --- HEIF_IMPLEMENTATION_PLAN.md | 2 +- .../Heif/Av1/Tiling/Av1NeighborArrayUnit.cs | 75 +--- .../Formats/Heif/Av1/Tiling/Av1TileWriter.cs | 325 +++++++----------- .../Heif/Av1/Av1CoefficientsEntropyTests.cs | 56 +++ 4 files changed, 188 insertions(+), 270 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 0de3d8891b..e76f30d52c 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -820,7 +820,7 @@ Encoder verification contract: - [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools. - [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain. - [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. -- [ ] Implement tile-local entropy coding and CDF update behavior. +- [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Complete tile traversal, initialized picture state, and verified CDF update behavior remain. - [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions. - [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform. Every remaining encoder fragment must be audited before it becomes active. - [~] The planar conversion, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Apply the same rule to every later hot-path family. diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs index 38ddf2cd14..6af171473a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -127,11 +126,6 @@ internal sealed class Av1NeighborArrayUnit : IDisposable /// public required int GranularityTopLeftLog2 { get; set; } - /// - /// Gets the number of consecutive values stored for each neighbor-array unit. - /// - public int UnitSize { get; private set; } - /// /// Gets the left-neighbor unit index for a sample position. /// @@ -166,20 +160,12 @@ internal sealed class Av1NeighborArrayUnit : IDisposable /// /// Writes one context unit across the selected block edges. /// - /// The values that make up one context unit. + /// The context value to publish. /// The block origin in samples. /// The block dimensions in samples. /// The neighbor arrays to update. - public void UnitModeWrite(ReadOnlySpan value, Point origin, Size blockSize, UnitMask mask) + public void UnitModeWrite(T value, Point origin, Size blockSize, UnitMask mask) { - int idx, j; - - int count; - int na_offset; - int na_unit_size; - - na_unit_size = this.UnitSize; - if ((mask & UnitMask.Top) == UnitMask.Top) { // Top Neighbor Array @@ -196,21 +182,12 @@ internal sealed class Av1NeighborArrayUnit : IDisposable // bottom row of the source block // // Index = org_x - na_offset = this.GetTopIndex(origin); - - ref T dst_ptr = ref this.Top[na_offset * na_unit_size]; + int offset = this.GetTopIndex(origin); + int count = blockSize.Width >> this.GranularityNormalLog2; - count = blockSize.Width >> this.GranularityNormalLog2; - - for (idx = 0; idx < count; ++idx) - { - // Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position. - for (j = 0; j < na_unit_size; ++j) - { - dst_ptr = value[j]; - dst_ptr = Unsafe.Add(ref dst_ptr, 1); - } - } + // One packed value represents each AV1 edge unit. Filling the covered range mirrors the + // contiguous above-context update without retaining a caller-owned span. + this.Top.Slice(offset, count).Fill(value); } if ((mask & UnitMask.Left) == UnitMask.Left) @@ -230,21 +207,9 @@ internal sealed class Av1NeighborArrayUnit : IDisposable // right column of the source block // // Index = org_y - na_offset = this.GetLeftIndex(origin); - - ref T dst_ptr = ref this.Left[na_offset * na_unit_size]; - - count = blockSize.Height >> this.GranularityNormalLog2; - - for (idx = 0; idx < count; ++idx) - { - // Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position. - for (j = 0; j < na_unit_size; ++j) - { - dst_ptr = value[j]; - dst_ptr = Unsafe.Add(ref dst_ptr, 1); - } - } + int offset = this.GetLeftIndex(origin); + int count = blockSize.Height >> this.GranularityNormalLog2; + this.Left.Slice(offset, count).Fill(value); } if ((mask & UnitMask.TopLeft) == UnitMask.TopLeft) @@ -267,23 +232,9 @@ internal sealed class Av1NeighborArrayUnit : IDisposable // Index = org_x - org_y Point topLeft = origin; topLeft.Offset(0, blockSize.Height - 1); - na_offset = this.GetTopLeftIndex(topLeft); - - // Copy bottom-row + right-column - // *Note - start from the bottom-left corner - ref T dst_ptr = ref this.TopLeft[na_offset * na_unit_size]; - - count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1; - - for (idx = 0; idx < count; ++idx) - { - // Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position. - for (j = 0; j < na_unit_size; ++j) - { - dst_ptr = value[j]; - dst_ptr = Unsafe.Add(ref dst_ptr, 1); - } - } + int offset = this.GetTopLeftIndex(topLeft); + int count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1; + this.TopLeft.Slice(offset, count).Fill(value); } } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs index 493f4d7a68..7ea9ca7278 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; @@ -747,9 +746,8 @@ internal partial class Av1TileWriter PartitionContextLookup[(int)blockSize].Above, PartitionContextLookup[(int)blockSize].Left); Size size = new(blk_geom.BlockWidth, blk_geom.BlockHeight); - Span partitionSpan = new(ref partition); partition_context_na.UnitModeWrite( - partitionSpan, + partition, blockOrigin, size, Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); @@ -757,26 +755,26 @@ internal partial class Av1TileWriter { // A skipped block has an all-zero residual, so publish a zero sign/level context over its edges // and advance coefficient positions without reading transform units. - byte dcSignLevelCoefficient = 0; - Span dcSignSpan = new(ref dcSignLevelCoefficient); - luma_dc_sign_level_coeff_na.UnitModeWrite( - dcSignSpan, + 0, blockOrigin, size, Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); if (blk_geom.HasUv) { + Point chromaOrigin = RoundUv(blockOrigin) >> 1; + Size chromaSize = new(blk_geom.BlockWidthUv, blk_geom.BlockHeightUv); + cb_dc_sign_level_coeff_na.UnitModeWrite( - dcSignSpan, - ((blockOrigin >> 3) << 3) >> 1, - size, + 0, + chromaOrigin, + chromaSize, Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); cr_dc_sign_level_coeff_na.UnitModeWrite( - dcSignSpan, - ((blockOrigin >> 3) << 3) >> 1, - size, + 0, + chromaOrigin, + chromaSize, Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); entropyCodingContext.CodedAreaSuperblockUv += blk_geom.BlockWidthUv * blk_geom.BlockHeightUv; } @@ -1043,16 +1041,13 @@ internal partial class Av1TileWriter int coeff1d_offset = entropyCodingContext.CodedAreaSuperblock; Span coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan()[coeff1d_offset..]; - Av1TransformBlockContext blockContext = default; Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][txb_itr]; - GetTransformBlockContexts( - pcs, + Av1TransformBlockContext blockContext = GetTransformBlockContexts( Av1ComponentType.Luminance, luma_dc_sign_level_coeff_na, blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin, plane_bsize, - tx_size, - blockContext); + tx_size); Av1TransformType tx_type = blk_ptr.TransformBlocks[txb_itr].TransformType[(int)Av1ComponentType.Luminance]; int eob = blk_ptr.TransformBlocks[txb_itr].NzCoefficientCount[0]; @@ -1063,7 +1058,7 @@ internal partial class Av1TileWriter Guard.IsTrue(tx_type == Av1TransformType.DctDct, nameof(tx_type), string.Empty); } - int cul_level_y = writer.WriteCoefficients( + int culLevelY = writer.WriteCoefficients( tx_size, tx_type, intraLumaDir, @@ -1074,15 +1069,12 @@ internal partial class Av1TileWriter frameHeader.UseReducedTransformSet, blk_ptr.FilterIntraMode); - // WriteCoefficients packs the DC sign and cumulative level into one integer; publish its bytes - // across the transform edges so the next blocks derive identical entropy contexts. - Span culLevelSpan = new(ref cul_level_y); - ReadOnlySpan dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelSpan); - + // Only the packed low byte is the AV1 entropy context. Converting the value explicitly keeps + // the update independent of machine endianness and publishes one value per covered edge unit. int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth(); int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight(); luma_dc_sign_level_coeff_na.UnitModeWrite( - dc_sign_level_coeff, + (byte)culLevelY, blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin, new Size(transformWidth, transformHeight), Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); @@ -1128,81 +1120,69 @@ internal partial class Av1TileWriter int tx_depth = mbmi.Block.TransformDepth; uint txb_count = 1; ObuFrameHeader frameHeader = pcs.Parent.FrameHeader; - int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth(); - int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight(); for (uint tx_index = 0; tx_index < txb_count; ++tx_index) { - Av1TransformSize chroma_tx_size = blockGeometry.TransformSizeUv[tx_depth]; + Av1TransformSize chromaTransformSize = blockGeometry.TransformSizeUv[tx_depth]; + int transformWidth = chromaTransformSize.GetWidth(); + int transformHeight = chromaTransformSize.GetHeight(); + Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index]; + Point chromaOrigin = RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1; + + // Both chroma planes share transform geometry but retain independent coefficient contexts. + Span coefficientBuffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); + Av1TransformBlockContext blockContext = GetTransformBlockContexts( + Av1ComponentType.Chroma, + cb_dc_sign_level_coeff_na, + chromaOrigin, + blockGeometry.BlockSizeUv, + chromaTransformSize); + Av1TransformType chromaTransformType = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma]; + int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1]; + int culLevelCb = writer.WriteCoefficients( + chromaTransformSize, + chromaTransformType, + intraLumaDir, + coefficientBuffer, + Av1ComponentType.Chroma, + blockContext, + (ushort)endOfBlockCb, + frameHeader.UseReducedTransformSet, + blk_ptr.FilterIntraMode); - if (blockGeometry.HasUv) - { - // Both chroma planes share transform geometry but retain independent coefficient contexts. - Span coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); - Av1TransformBlockContext blockContext = default; - Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index]; - GetTransformBlockContexts( - pcs, - Av1ComponentType.Chroma, - cb_dc_sign_level_coeff_na, - RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1, - blockGeometry.BlockSizeUv, - chroma_tx_size, - blockContext); - Av1TransformType chroma_tx_type = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma]; - int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1]; - int cul_level_cb = writer.WriteCoefficients( - chroma_tx_size, - chroma_tx_type, - intraLumaDir, - coeff_buffer, - Av1ComponentType.Chroma, - blockContext, - (ushort)endOfBlockCb, - frameHeader.UseReducedTransformSet, - blk_ptr.FilterIntraMode); - - coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); - blockContext = default; - int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2]; - - GetTransformBlockContexts( - pcs, - Av1ComponentType.Chroma, - cr_dc_sign_level_coeff_na, - RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1, - blockGeometry.BlockSizeUv, - chroma_tx_size, - blockContext); - - int cul_level_cr = writer.WriteCoefficients( - chroma_tx_size, - chroma_tx_type, - intraLumaDir, - coeff_buffer, - Av1ComponentType.Chroma, - blockContext, - (ushort)endOfBlockCr, - frameHeader.UseReducedTransformSet, - blk_ptr.FilterIntraMode); - - // Publish each plane's packed sign/level summary across its transform edges. - Span culLevelCbSpan = new(ref cul_level_cb); - ReadOnlySpan dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCbSpan); - cb_dc_sign_level_coeff_na.UnitModeWrite( - dc_sign_level_coeff, - RoundUv(transformOrigin) >> 1, - new Size(transformWidth, transformHeight), - Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); + coefficientBuffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); + int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2]; - Span culLevelCrSpan = new(ref cul_level_cr); - dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCrSpan); - cr_dc_sign_level_coeff_na.UnitModeWrite( - dc_sign_level_coeff, - RoundUv(transformOrigin) >> 1, - new Size(transformWidth, transformHeight), - Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); - } + blockContext = GetTransformBlockContexts( + Av1ComponentType.Chroma, + cr_dc_sign_level_coeff_na, + chromaOrigin, + blockGeometry.BlockSizeUv, + chromaTransformSize); + + int culLevelCr = writer.WriteCoefficients( + chromaTransformSize, + chromaTransformType, + intraLumaDir, + coefficientBuffer, + Av1ComponentType.Chroma, + blockContext, + (ushort)endOfBlockCr, + frameHeader.UseReducedTransformSet, + blk_ptr.FilterIntraMode); + + // Each plane publishes its packed context across the complete chroma transform edges. + cb_dc_sign_level_coeff_na.UnitModeWrite( + (byte)culLevelCb, + chromaOrigin, + new Size(transformWidth, transformHeight), + Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); + + cr_dc_sign_level_coeff_na.UnitModeWrite( + (byte)culLevelCr, + chromaOrigin, + new Size(transformWidth, transformHeight), + Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); entropyCodingContext.CodedAreaSuperblockUv += transformWidth * transformHeight; } @@ -1218,84 +1198,65 @@ internal partial class Av1TileWriter /// /// Derives coefficient skip and DC-sign contexts from the transform block's above and left neighbors. /// - /// The picture coding state. /// The luma or chroma component class. /// The packed DC-sign and coefficient-level neighbor contexts. /// The transform-block origin in samples of the target plane. /// The containing block size on the target plane. /// The transform size. - /// The context object to populate. - private static void GetTransformBlockContexts( - Av1PictureControlSet pcs, + /// The coefficient skip and DC-sign contexts selected by both transform edges. + public static Av1TransformBlockContext GetTransformBlockContexts( Av1ComponentType plane, Av1NeighborArrayUnit dcSignLevelCoefficientNeighborArray, Point blockOrigin, Av1BlockSize planeBlockSize, - Av1TransformSize transformSize, - Av1TransformBlockContext blockContext) + Av1TransformSize transformSize) { - int dcSignLevelCoefficientLeftNeighborIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin); - int dcSignLevelCoefficientTopNeighborIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin); - - sbyte[] signs = [0, -1, 1]; - int transformBlockWidth; - int transformBlockHeight; - if (plane != Av1ComponentType.Luminance) - { - transformBlockWidth = Math.Min(transformSize.GetWidth(), ((pcs.Parent.AlignedWidth / 2) - blockOrigin.X) >> 2); - transformBlockHeight = Math.Min(transformSize.GetHeight(), ((pcs.Parent.AlignedHeight / 2) - blockOrigin.Y) >> 2); - } - else + int leftIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin); + int topIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin); + int transformBlockWidth = transformSize.Get4x4WideCount(); + int transformBlockHeight = transformSize.Get4x4HighCount(); + ReadOnlySpan topContexts = dcSignLevelCoefficientNeighborArray.Top.Slice(topIndex, transformBlockWidth); + ReadOnlySpan leftContexts = dcSignLevelCoefficientNeighborArray.Left.Slice(leftIndex, transformBlockHeight); + int dcSign = 0; + int top = 0; + int left = 0; + + // Each context packs a coefficient-level class in the low bits and the DC sign class above it. + // Accumulating both values in one traversal supplies every luma and chroma context without scratch storage. + foreach (byte context in topContexts) { - transformBlockWidth = Math.Min(transformSize.GetWidth(), (pcs.Parent.AlignedWidth - blockOrigin.X) >> 2); - transformBlockHeight = Math.Min(transformSize.GetHeight(), (pcs.Parent.AlignedHeight - blockOrigin.Y) >> 2); - } - - short dc_sign = 0; - ushort k = 0; - - byte sign; - - // The high bits encode the DC sign class: zero, negative, or positive. Summing classes over - // both edges selects whether neighboring DC coefficients bias the current sign context. - if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != Av1NeighborArrayUnit.InvalidNeighborData) - { - do + byte sign = (byte)(context >> Av1Constants.CoefficientContextBitCount); + DebugGuard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); + if (sign == 1) + { + dcSign--; + } + else if (sign == 2) { - sign = (byte)(dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] >> - Av1Constants.CoefficientContextBitCount); - Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); - dc_sign += signs[sign]; + dcSign++; } - while (++k < transformBlockWidth); + + top |= context; } - if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != Av1NeighborArrayUnit.InvalidNeighborData) + foreach (byte context in leftContexts) { - k = 0; - do + byte sign = (byte)(context >> Av1Constants.CoefficientContextBitCount); + DebugGuard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); + if (sign == 1) { - sign = (byte)(dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] >> - Av1Constants.CoefficientContextBitCount); - Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); - dc_sign += signs[sign]; + dcSign--; + } + else if (sign == 2) + { + dcSign++; } - while (++k < transformBlockHeight); - } - if (dc_sign > 0) - { - blockContext.DcSignContext = 2; - } - else if (dc_sign < 0) - { - blockContext.DcSignContext = 1; - } - else - { - blockContext.DcSignContext = 0; + left |= context; } + Av1TransformBlockContext blockContext = default; + blockContext.DcSignContext = dcSign > 0 ? 2 : dcSign < 0 ? 1 : 0; if (plane == Av1ComponentType.Luminance) { if (planeBlockSize == transformSize.ToBlockSize()) @@ -1304,33 +1265,7 @@ internal partial class Av1TileWriter } else { - int top = 0; - int left = 0; - - k = 0; - if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != - Av1NeighborArrayUnit.InvalidNeighborData) - { - do - { - top |= dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex]; - } - while (++k < transformBlockWidth); - } - top &= Av1Constants.CoefficientContextMask; - - if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != - Av1NeighborArrayUnit.InvalidNeighborData) - { - k = 0; - do - { - left |= dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex]; - } - while (++k < transformBlockHeight); - } - left &= Av1Constants.CoefficientContextMask; blockContext.SkipContext = Av1SymbolContextHelper.GetTransformBlockSkipContext(top, left); } @@ -1339,36 +1274,12 @@ internal partial class Av1TileWriter { // Chroma contexts use only the presence of nonzero levels on each edge, plus an offset // that distinguishes a transform smaller than its containing plane block. - short ctx_base_left = 0; - short ctx_base_top = 0; - - if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != - Av1NeighborArrayUnit.InvalidNeighborData) - { - k = 0; - do - { - ctx_base_top += - (dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] != 0) ? (short)1 : (short)0; - } - while (++k < transformBlockWidth); - } - - if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != - Av1NeighborArrayUnit.InvalidNeighborData) - { - k = 0; - do - { - ctx_base_left += dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] != 0 ? (short)1 : (short)0; - } - while (++k < transformBlockHeight); - } - - int ctx_base = ((ctx_base_left != 0) ? 1 : 0) + ((ctx_base_top != 0) ? 1 : 0); - int ctx_offset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7; - blockContext.SkipContext = (short)(ctx_base + ctx_offset); + int contextBase = (left != 0 ? 1 : 0) + (top != 0 ? 1 : 0); + int contextOffset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7; + blockContext.SkipContext = contextBase + contextOffset; } + + return blockContext; } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs index 771ae5e0a4..d800483253 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs @@ -16,6 +16,62 @@ public class Av1CoefficientsEntropyTests { private const int BaseQIndex = 23; + [Fact] + public void NeighborArrayWritesEveryCoveredFourByFourEdgeUnit() + { + using Av1NeighborArrayUnit neighbors = new( + Configuration.Default, + leftSize: 8, + topSize: 8, + topLeftSize: 16) + { + GranularityNormalLog2 = 2, + GranularityTopLeftLog2 = 2 + }; + + neighbors.UnitModeWrite( + 37, + new Point(8, 4), + new Size(16, 8), + Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); + + Assert.Equal(new byte[] { 0, 0, 37, 37, 37, 37, 0, 0 }, neighbors.Top.ToArray()); + Assert.Equal(new byte[] { 0, 37, 37, 0, 0, 0, 0, 0 }, neighbors.Left.ToArray()); + } + + [Theory] + [InlineData((int)Av1ComponentType.Luminance, 5)] + [InlineData((int)Av1ComponentType.Chroma, 12)] + public void WriterDerivesTransformContextFromCompleteFourByFourEdges( + int componentType, + int expectedSkipContext) + { + using Av1NeighborArrayUnit neighbors = new( + Configuration.Default, + leftSize: 8, + topSize: 8, + topLeftSize: 16) + { + GranularityNormalLog2 = 2, + GranularityTopLeftLog2 = 2 + }; + + // The high bits carry positive, positive, and negative DC signs. The low bits select + // the high-above and low-left coefficient classes used by the luma skip-context table. + neighbors.Top[2] = (2 << Av1Constants.CoefficientContextBitCount) | 4; + neighbors.Top[3] = 2 << Av1Constants.CoefficientContextBitCount; + neighbors.Left[1] = (1 << Av1Constants.CoefficientContextBitCount) | 1; + Av1TransformBlockContext context = Av1TileWriter.GetTransformBlockContexts( + (Av1ComponentType)componentType, + neighbors, + new Point(8, 4), + Av1BlockSize.Block16x16, + Av1TransformSize.Size8x8); + + Assert.Equal(2, context.DcSignContext); + Assert.Equal(expectedSkipContext, context.SkipContext); + } + [Fact] public void RoundTripZeroEndOfBlock() {