Browse Source

Dequantize AV1 coefficients during entropy parsing

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
c06fabf304
  1. 51
      HEIF_IMPLEMENTATION_PLAN.md
  2. 41
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  3. 18
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  4. 143
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs
  5. 4
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs
  6. 34
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  7. 14
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformInfo.cs
  8. 131
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  9. 40
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  10. 28
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  11. 15
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs
  12. 91
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseQuantizationTests.cs

51
HEIF_IMPLEMENTATION_PLAN.md

@ -363,6 +363,57 @@ Retained-state and cost-policy follow-up after `ef8b1a823`:
and initializes frame costs; `encodeframe_utils.c:1629-1689` suppresses block refresh when CDF updates are
disabled. No new managed effort mapping or isolated cost-refresh threshold was introduced.
Decoder coefficient-stage correction after `b305e6e89`, verified on 2026-09-05:
- The source trace established an architectural deviation: `Av1SymbolDecoder.cs:1415-1459` published a
count prefix and scan-ordered quantized levels; `Av1TileReader.cs:1148-1157` packed those variable-length
groups. `Av1BlockDecoder.cs:126-153,1349-1399` then used another superblock-sized, all-plane workspace
to dequantize and reorder every transform during reconstruction. Both buffers were cleared separately.
The native entropy traversal dequantizes each signed level directly into its coefficient region
(`av1/decoder/decodetxb.c:116-165,279-312`); EOB belongs to separate metadata
(`av1/common/blockd.h:452-461`). Native region cursors advance by nominal transform area
(`av1/decoder/decodeframe.c:274-279`), independently of EOB.
- Parsing now publishes dequantized coefficients directly and records EOB in `Av1TransformInfo`. Each plane's
parser/reconstruction cursor advances by nominal transform area, including skipped transforms. Frame state
reserves 16 coefficient slots per 4x4 unit, with no count prefix. Reconstruction consumes that storage
directly; its second coefficient workspace and inverse-quantization pass are removed. These changes span
the production parser, transform descriptors, frame storage, and reconstruction caller, rather than adding
a disconnected native primitive.
- Quantization state moves to the parser. Mode syntax establishes delta-Q before `Residual` updates the
segment/plane values, matching `decodeframe.c:1172-1221`. Transform-local parameters preserve matrix
bypass, weighted-quantizer rounding, the 24-bit product mask, transform scaling before sign, and signed
precision clipping (`Av1InverseQuantizer.cs:92-135`, `decodetxb.c:52-58,298-312`). The entropy context still
uses the masked quantized magnitude and original DC sign (`Av1SymbolDecoder.cs:1424-1476`). No additional
allocator-owned buffer or native production dependency was added.
- The coefficient capacities decrease by 25.5 KiB for a 64x64 4:2:0 superblock configuration and 102 KiB for
128x128 4:2:0: this combines removal of the second workspace with removal of count-prefix capacity.
These are source-derived coefficient-buffer sizes, excluding descriptor/object overhead, not measured
total memory or a timing improvement. No benchmark was run.
- Existing entropy tests now check the published dequantized raster values, including sparse and beyond-EOB
zeros, against fixed reference qindex-23 DC/AC values. Four added matrix/arithmetic cases use explicit
8/10/12-bit reference values, matrix bypass for identity/one-dimensional transforms, lossless bypass,
asymmetric precision limits, product-mask wraparound, and sign-after-scaling rounding. The old matrix test
checked lengths only. These component cases do not establish complete signaled-matrix bitstream coverage.
- After the production edit, serialized Release .NET 11 Visual Studio VSTest passed **9,371/9,371** AV1 and
public HEIF encoder cases in 2.6933 minutes (`coefficient-stage-final.trx`). After adding the fixed-value
tests, a focused set passed **130/130** in 3.0217 seconds (`coefficient-stage-last-edit.trx`). Following final
whitespace cleanup, the checkpoint set passed **159/159** in 5.5189 seconds (`coefficient-stage-checkpoint.trx`).
The final build had zero errors and the existing 1,009 warnings; Roslynk reported zero compiler errors.
No production behavior changed after the broad run, and no retained reference samples were altered.
- Optimized current-reference redecoding matched the retained restoration and film-grain references across
**8,500,087** samples, maximum error **0**, zero exceeding one; those references also passed the managed
conformance tests. Twelve regenerated color sequences matched another **21,348** native samples exactly.
Reports are `restoration-comparison.json`, `film-grain-comparison.json`, and `decoder-comparison.json` in
`D:\GitHub\ynse01\av1-takeover-20260905`, outside the repository. This is bounded same-bitstream evidence,
not separately encoded output parity, complete decoder conformance, or a performance acceptance result.
- Remaining architecture differences are explicit: the managed reader still parses a complete superblock
before reconstruction (`Av1TileReader.ReadTile`, `Av1FrameDecoder.DecodePartition`) and clears its complete
coefficient regions before reuse. Native single-thread decoding interleaves parsing/reconstruction through
visitors (`decodeframe.c:935-958,2746-2765,2792-2801`), clears only through the maximum populated raster
position after inverse transform (`:154-164`), and separates parsing/reconstruction for row workers with
different buffer lifetimes (`:3244-3277`). Those traversal, clearing, and worker-lifetime differences remain
open; this checkpoint does not claim that changing coefficient representation completes them.
Film-grain decoder source comparison after `ef8b1a823`:
- The complete template generation, random state, autoregression, scaling interpolation, overlap traversal,

41
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -1177,11 +1178,12 @@ internal ref struct Av1SymbolDecoder
/// <param name="isLossless">Indicates whether the active segment is lossless.</param>
/// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param>
/// <param name="lumaTransformType">The luma transform type shared by inter-predicted chroma.</param>
/// <param name="transformInfo">The transform descriptor updated with the decoded type and coded-block flag.</param>
/// <param name="transformInfo">The transform descriptor updated with the decoded type and end-of-block position.</param>
/// <param name="modeBlocksToRightEdge">The signed distance from the mode block to the right frame edge.</param>
/// <param name="modeBlocksToBottomEdge">The signed distance from the mode block to the bottom frame edge.</param>
/// <param name="levels">Reusable padded coefficient-context storage owned by the tile reader.</param>
/// <param name="coefficientBuffer">The destination receiving the coefficient count followed by scan-ordered signed levels.</param>
/// <param name="coefficientBuffer">The zero-initialized destination receiving dequantized raster coefficients.</param>
/// <param name="inverseQuantizer">The quantizer containing the active segment and superblock delta-Q values.</param>
/// <returns>The one-based end-of-block position, or zero for an empty transform block.</returns>
public int ReadCoefficients(
Av1BlockModeInfo modeInfo,
@ -1202,7 +1204,8 @@ internal ref struct Av1SymbolDecoder
int modeBlocksToRightEdge,
int modeBlocksToBottomEdge,
Av1LevelBuffer levels,
Span<int> coefficientBuffer)
Span<int> coefficientBuffer,
Av1InverseQuantizer inverseQuantizer)
{
Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted();
int width = adjustedTransformSize.GetWidth();
@ -1219,10 +1222,10 @@ internal ref struct Av1SymbolDecoder
int endOfBlock;
if (allZero)
{
transformInfo.EndOfBlock = 0;
if (plane == 0)
{
transformInfo.Type = Av1TransformType.DctDct;
transformInfo.CodeBlockFlag = false;
}
UpdateCoefficientContext(aboveContexts, leftContexts, blocksWide, blocksHigh, transformSize, blockPosition, aboveOffset, leftOffset, culLevel, modeBlocksToRightEdge, modeBlocksToBottomEdge);
@ -1278,10 +1281,15 @@ internal ref struct Av1SymbolDecoder
}
DebugGuard.MustBeGreaterThan(scan.Length, 0, nameof(scan));
culLevel = this.ReadCoefficientsSign(coefficientBuffer, endOfBlock, scan, levels, transformBlockContext.DcSignContext, planeType);
Av1InverseQuantizer.TransformParameters quantization = new(
inverseQuantizer, modeInfo, transformInfo.Type, transformSize, (Av1Plane)plane);
culLevel = this.ReadCoefficientsSign(
coefficientBuffer, endOfBlock, scan, levels, transformBlockContext.DcSignContext, planeType, quantization);
UpdateCoefficientContext(aboveContexts, leftContexts, blocksWide, blocksHigh, transformSize, blockPosition, aboveOffset, leftOffset, culLevel, modeBlocksToRightEdge, modeBlocksToBottomEdge);
transformInfo.CodeBlockFlag = true;
transformInfo.EndOfBlock = (ushort)endOfBlock;
return endOfBlock;
}
@ -1403,21 +1411,28 @@ internal ref struct Av1SymbolDecoder
}
/// <summary>
/// Reads coefficient signs and Golomb extensions, then writes scan-ordered signed levels.
/// Reads coefficient signs and Golomb extensions, then writes dequantized raster coefficients.
/// </summary>
/// <param name="coefficientBuffer">The destination receiving the coefficient count followed by signed levels.</param>
/// <param name="coefficientBuffer">The zero-initialized destination receiving dequantized coefficients.</param>
/// <param name="endOfBlock">The one-based end-of-block position and coefficient count.</param>
/// <param name="scan">The transform's scan-to-raster mapping.</param>
/// <param name="levels">The decoded absolute-coefficient level plane.</param>
/// <param name="dcSignContext">The neighboring DC sign context.</param>
/// <param name="planeType">The luma or chroma plane category.</param>
/// <param name="quantization">The segment, plane, matrix, scale, and clipping parameters for this transform.</param>
/// <returns>The packed coefficient context used by adjacent transform blocks.</returns>
public int ReadCoefficientsSign(Span<int> coefficientBuffer, int endOfBlock, ReadOnlySpan<short> scan, Av1LevelBuffer levels, int dcSignContext, Av1PlaneType planeType)
private int ReadCoefficientsSign(
Span<int> coefficientBuffer,
int endOfBlock,
ReadOnlySpan<short> scan,
Av1LevelBuffer levels,
int dcSignContext,
Av1PlaneType planeType,
Av1InverseQuantizer.TransformParameters quantization)
{
ref Av1SymbolReader r = ref this.reader;
int culLevel = 0;
int dcValue = 0;
coefficientBuffer[0] = endOfBlock;
for (int c = 0; c < endOfBlock; c++)
{
int sign = 0;
@ -1447,9 +1462,11 @@ internal ref struct Av1SymbolDecoder
level &= 0xfffff;
culLevel += level;
}
coefficientBuffer[c + 1] = sign != 0 ? -level : level;
// The entropy context uses the masked quantized magnitude, while reconstruction consumes the
// dequantized raster coefficient. Write it directly into the current superblock's zeroed region.
coefficientBuffer[pos] = quantization.Dequantize(level, pos, sign != 0);
}
}
culLevel = Math.Min(Av1Constants.CoefficientContextMask, culLevel);

18
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs

@ -5,12 +5,10 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
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;
@ -44,16 +42,6 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
/// </summary>
private readonly Av1ReferenceFrameStore referenceFrames;
/// <summary>
/// The coefficient inverse-quantization stage shared across superblocks.
/// </summary>
private readonly Av1InverseQuantizer inverseQuantizer;
/// <summary>
/// The frame's base per-segment and per-plane dequantization values.
/// </summary>
private readonly Av1DeQuantizationContext deQuants;
/// <summary>
/// The transform-size map populated during reconstruction and consumed by deblocking.
/// </summary>
@ -86,8 +74,6 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer;
this.referenceFrames = referenceFrames;
this.inverseQuantizer = new(sequenceHeader, frameHeader);
this.deQuants = new(sequenceHeader, frameHeader);
this.loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
try
{
@ -96,7 +82,6 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
this.frameHeader,
this.frameBuffer,
this.loopFilterContext,
this.inverseQuantizer,
this.referenceFrames,
paletteColorIndexMaps);
}
@ -165,7 +150,7 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
}
/// <summary>
/// Reconstructs one superblock after applying its block state and delta-Q context.
/// Reconstructs one superblock from its parsed block state and dequantized coefficients.
/// </summary>
/// <param name="modeInfoPosition">The superblock's top-left position in 4x4 mode-info units.</param>
/// <param name="superblockInfo">The decoded syntax and block modes for the superblock.</param>
@ -173,7 +158,6 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
public void DecodeSuperblock(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
{
this.blockDecoder.UpdateSuperblock(superblockInfo);
this.inverseQuantizer.UpdateDequant(this.deQuants, superblockInfo);
this.DecodePartition(modeInfoPosition, superblockInfo, tileInfo);
}

143
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs

@ -70,99 +70,68 @@ internal sealed class Av1InverseQuantizer
}
/// <summary>
/// Converts scan-ordered quantized levels into clamped, raster-ordered transform coefficients.
/// Applies the active segment, plane, matrix, and transform scale to decoded coefficient magnitudes.
/// </summary>
/// <param name="mode">The block mode information containing the active segment identifier.</param>
/// <param name="level">The packed coefficient buffer: the first element is the coefficient count and the remaining elements are scan-ordered levels.</param>
/// <param name="qCoefficients">The destination for raster-ordered dequantized coefficients.</param>
/// <param name="transformType">The transform type that selects the coefficient scan and matrix class.</param>
/// <param name="transformSize">The transform dimensions and scale.</param>
/// <param name="plane">The color plane whose quantizer and matrix are used.</param>
/// <returns>The number of coefficient levels consumed.</returns>
public int InverseQuantize(Av1BlockModeInfo mode, Span<int> level, Span<int> qCoefficients, Av1TransformType transformType, Av1TransformSize transformSize, Av1Plane plane)
public readonly ref struct TransformParameters
{
Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType);
ReadOnlySpan<short> scanIndices = scanOrder.Scan;
// AV1 bounds reconstructed coefficients to a signed range with seven headroom bits beyond pixel precision.
int maxValue = (1 << (7 + this.sequenceHeader.ColorConfig.BitDepth.GetBitCount())) - 1;
int minValue = -(1 << (7 + this.sequenceHeader.ColorConfig.BitDepth.GetBitCount()));
bool usingQuantizationMatrix = this.frameHeader.QuantizationParameters.IsUsingQMatrix;
bool lossless = this.frameHeader.LosslessArray[mode.SegmentId];
short dequantDc = this.deQuantsDeltaQ.GetDc(mode.SegmentId, plane);
short dequantAc = this.deQuantsDeltaQ.GetAc(mode.SegmentId, plane);
// The final matrix level is flat. Lossless blocks and frames without matrices select it globally. AV1 also
// requires identity and one-dimensional transform types, which occupy the enum range from Identity onward,
// to bypass frequency weighting even when the frame signals quantization matrices.
int qmLevel = lossless || !usingQuantizationMatrix
? Av1ScanOrderConstants.QuantizationMatrixLevelCount - 1
: this.frameHeader.SegmentationParameters.QMLevel[(int)plane][mode.SegmentId];
ReadOnlySpan<int> iqMatrix = transformType < Av1TransformType.Identity
? Av1InverseQuantizationLookup.GetQuantizationMatrix(qmLevel, plane, transformSize)
: Av1InverseQuantizationLookup.GetQuantizationMatrix(Av1Constants.QuantificationMatrixLevelCount - 1, Av1Plane.Y, transformSize);
int shift = transformSize.GetScale();
// Entropy decoding stores the populated coefficient count in the leading slot and the levels after it.
int coefficientCount = level[0];
level = level[1..];
int lev = level[0];
int qCoefficient;
if (lev != 0)
private readonly short dc;
private readonly short ac;
private readonly int minimum;
private readonly int maximum;
private readonly int shift;
private readonly ReadOnlySpan<int> inverseMatrix;
/// <summary>
/// Initializes a new instance of the <see cref="TransformParameters"/> struct.
/// </summary>
/// <param name="quantizer">The active frame and superblock quantization values.</param>
/// <param name="mode">The block mode selecting the segment.</param>
/// <param name="transformType">The transform type selecting frequency weighting.</param>
/// <param name="transformSize">The transform dimensions and coefficient scale.</param>
/// <param name="plane">The color plane selecting DC, AC, and matrix values.</param>
public TransformParameters(
Av1InverseQuantizer quantizer,
Av1BlockModeInfo mode,
Av1TransformType transformType,
Av1TransformSize transformSize,
Av1Plane plane)
{
int pos = scanIndices[0];
// Preserve the AV1 24-bit dequantization intermediate before removing transform-size scaling.
qCoefficient = (int)(((long)Math.Abs(lev) * GetDeQuantizedValue(dequantDc, pos, iqMatrix)) & 0xffffff);
qCoefficient >>= shift;
if (lev < 0)
{
qCoefficient = -qCoefficient;
}
qCoefficients[0] = Av1Math.Clamp(qCoefficient, minValue, maxValue);
int bitCount = quantizer.sequenceHeader.ColorConfig.BitDepth.GetBitCount();
this.minimum = -(1 << (7 + bitCount));
this.maximum = (1 << (7 + bitCount)) - 1;
this.dc = quantizer.deQuantsDeltaQ.GetDc(mode.SegmentId, plane);
this.ac = quantizer.deQuantsDeltaQ.GetAc(mode.SegmentId, plane);
this.shift = transformSize.GetScale();
// Lossless segments and one-dimensional or identity transforms use the flat matrix. Matrix lookup
// happens once per transform, before the entropy loop supplies its nonzero magnitudes and signs.
int matrixLevel = quantizer.frameHeader.LosslessArray[mode.SegmentId] ||
!quantizer.frameHeader.QuantizationParameters.IsUsingQMatrix ||
transformType >= Av1TransformType.Identity
? Av1ScanOrderConstants.QuantizationMatrixLevelCount - 1
: quantizer.frameHeader.SegmentationParameters.QMLevel[(int)plane][mode.SegmentId];
this.inverseMatrix = Av1InverseQuantizationLookup.GetQuantizationMatrix(matrixLevel, plane, transformSize);
}
for (int i = 1; i < coefficientCount; i++)
/// <summary>
/// Dequantizes one coefficient magnitude and applies its sign and precision bounds.
/// </summary>
/// <param name="magnitude">The nonnegative coefficient magnitude masked to twenty bits.</param>
/// <param name="coefficientIndex">The coefficient's raster position.</param>
/// <param name="negative">Whether the decoded coefficient sign is negative.</param>
/// <returns>The signed, scaled, and clipped transform coefficient.</returns>
public int Dequantize(int magnitude, int coefficientIndex, bool negative)
{
lev = level[i];
if (lev != 0)
{
int pos = scanIndices[i];
// AC levels arrive in entropy scan order but the inverse transform consumes raster positions.
qCoefficient = (int)(((long)Math.Abs(lev) * GetDeQuantizedValue(dequantAc, pos, iqMatrix)) & 0xffffff);
qCoefficient >>= shift;
if (lev < 0)
{
qCoefficient = -qCoefficient;
}
qCoefficients[pos] = Av1Math.Clamp(qCoefficient, minValue, maxValue);
}
int dequant = coefficientIndex == 0 ? this.dc : this.ac;
// Matrix weights have five fractional bits. Round the weighted quantizer first, then retain the
// normative 24-bit product before removing transform-size scaling. Sign and clipping follow the shift.
const int bias = 1 << (Av1Constants.QuantizationMatrixElementBitCount - 1);
dequant = ((this.inverseMatrix[coefficientIndex] * dequant) + bias) >> Av1Constants.QuantizationMatrixElementBitCount;
int coefficient = (int)(((long)magnitude * dequant) & 0xffffff) >> this.shift;
coefficient = negative ? -coefficient : coefficient;
return Av1Math.Clamp(coefficient, this.minimum, this.maximum);
}
return coefficientCount;
}
/// <summary>
/// Applies an inverse quantization-matrix weight to a plane dequantization value.
/// </summary>
/// <param name="dequant">The unweighted DC or AC dequantization value.</param>
/// <param name="coefficientIndex">The raster coefficient index into the inverse matrix.</param>
/// <param name="iqMatrix">The inverse quantization matrix for the current level, plane, and transform size.</param>
/// <returns>The matrix-weighted dequantization value.</returns>
private static int GetDeQuantizedValue(short dequant, int coefficientIndex, ReadOnlySpan<int> iqMatrix)
{
// Matrix elements use fixed-point precision; adding half a unit produces nearest-integer rounding on shift.
const int bias = 1 << (Av1Constants.QuantizationMatrixElementBitCount - 1);
int deQuantifiedValue = dequant;
deQuantifiedValue = ((iqMatrix[coefficientIndex] * deQuantifiedValue) + bias) >> Av1Constants.QuantizationMatrixElementBitCount;
return deQuantifiedValue;
}
}

4
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs

@ -83,9 +83,9 @@ internal sealed partial class Av1FrameInfo : IDisposable
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The coefficient slots reserved for one 4x4 mode-information unit: one end index followed by 16 coefficients.
/// The raster coefficient slots reserved for one 4x4 mode-information unit.
/// </summary>
public const int CoefficientCountPerModeInfo = 1 + 16;
public const int CoefficientCountPerModeInfo = 16;
/// <summary>
/// Owns the luma and chroma coefficient scratch for the superblock currently being decoded.

34
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
@ -76,6 +77,16 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary>
private int currentQuantizerIndex;
/// <summary>
/// The coefficient quantizer updated before residual syntax consumes the active superblock delta-Q state.
/// </summary>
private readonly Av1InverseQuantizer inverseQuantizer;
/// <summary>
/// The frame's base per-segment and per-plane dequantization values.
/// </summary>
private readonly Av1DeQuantizationContext deQuants;
/// <summary>
/// Stores the loop-filter delta values carried between superblocks in the current tile.
/// </summary>
@ -258,6 +269,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
this.referenceFrames = referenceFrames;
this.ownsPaletteColorIndexMaps = sharedPaletteColorIndexMaps is null;
this.entropyContexts.BeginFrame(frameHeader.QuantizationParameters.BaseQIndex, primaryReferenceContext);
this.inverseQuantizer = new(sequenceHeader, frameHeader);
this.deQuants = new(sequenceHeader, frameHeader);
// FrameInfo owns traversal records for this coded frame and one superblock of coefficient scratch.
this.FrameInfo = new(this.configuration, this.SequenceHeader, this.FrameHeader);
@ -1038,6 +1051,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
Av1TileInfo tileInfo,
Av1BlockSize blockSize)
{
// Mode syntax has established delta-Q before residual decoding. Keep dequantization at this parsing
// boundary so each signed level is published once in the form consumed by inverse reconstruction.
this.inverseQuantizer.UpdateDequant(this.deQuants, superblockInfo);
int maxBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false);
int maxBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false);
Av1BlockSize maxUnitSize = Av1BlockSize.Block64x64;
@ -1145,17 +1161,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
subY != 0);
}
if (endOfBlock != 0)
{
// Coefficients are stored as an end index followed by scan-order values, so the
// next transform begins after both the prefix and its decoded coefficient range.
this.coefficientIndex[plane] += endOfBlock + 1;
transformInfo.CodeBlockFlag = true;
}
else
{
transformInfo.CodeBlockFlag = false;
}
// Each transform reserves its nominal area even when its residual is empty. EOB belongs
// to the descriptor, so the raster coefficient region contains no packed metadata prefix.
this.coefficientIndex[plane] += transformInfo.Size.GetWidth() * transformInfo.Size.GetHeight();
transformInfo.EndOfBlock = (ushort)endOfBlock;
transformInfoIndex++;
}
@ -1353,7 +1362,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
partitionInfo.ModeBlockToRightEdge,
partitionInfo.ModeBlockToBottomEdge,
this.coefficientLevels,
coefficientBuffer);
coefficientBuffer,
this.inverseQuantizer);
}
/// <summary>

14
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformInfo.cs

@ -63,17 +63,7 @@ internal struct Av1TransformInfo
public int OffsetY { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the transform block contains a coded residual.
/// <list type="table">
/// <item>
/// <term>false</term>
/// <description>The block has no residual.</description>
/// </item>
/// <item>
/// <term>true</term>
/// <description>The block has a residual.</description>
/// </item>
/// </list>
/// Gets or sets the end position of the coded coefficients in entropy scan order; zero means no residual.
/// </summary>
public bool CodeBlockFlag { get; set; }
public ushort EndOfBlock { get; set; }
}

131
src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs

@ -2,19 +2,16 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -43,31 +40,16 @@ internal sealed class Av1BlockDecoder : IDisposable
/// </summary>
private readonly Av1LoopFilterContext loopFilterContext;
/// <summary>
/// The frame-owned inverse quantizer carrying the active superblock delta-Q state.
/// </summary>
private readonly Av1InverseQuantizer inverseQuantizer;
/// <summary>
/// The retained reconstructed frames addressable by inter prediction.
/// </summary>
private readonly Av1ReferenceFrameStore referenceFrames;
/// <summary>
/// Owns all reusable inverse-quantization, transform, and prediction storage.
/// Owns the reusable inverse-transform and prediction storage.
/// </summary>
private readonly IMemoryOwner<short> workspaceOwner;
/// <summary>
/// The inverse-quantization prefix length in signed-short storage elements.
/// </summary>
private readonly int inverseQuantizationStorageLength;
/// <summary>
/// The inverse-transform workspace offset in signed-short storage elements.
/// </summary>
private readonly int transformWorkspaceOffset;
/// <summary>
/// The prediction workspace offset in signed-short storage elements.
/// </summary>
@ -89,7 +71,7 @@ internal sealed class Av1BlockDecoder : IDisposable
private readonly bool isLoopFilterEnabled;
/// <summary>
/// The next packed coefficient position for each plane in the current superblock.
/// The next raster coefficient region for each plane in the current superblock.
/// </summary>
private InlineArray4<int> currentCoefficientIndex;
@ -105,7 +87,6 @@ internal sealed class Av1BlockDecoder : IDisposable
/// <param name="frameHeader">The decoded frame header.</param>
/// <param name="frameBuffer">The frame buffer receiving reconstructed samples.</param>
/// <param name="loopFilterContext">The transform-size map populated while reconstructing blocks.</param>
/// <param name="inverseQuantizer">The inverse quantizer carrying the active superblock delta-Q state.</param>
/// <param name="referenceFrames">The retained reconstructed frames selected by inter blocks.</param>
/// <param name="paletteColorIndexMaps">The complete decoder-session palette map state.</param>
public Av1BlockDecoder(
@ -113,7 +94,6 @@ internal sealed class Av1BlockDecoder : IDisposable
ObuFrameHeader frameHeader,
Av1FrameBuffer<byte> frameBuffer,
Av1LoopFilterContext loopFilterContext,
Av1InverseQuantizer inverseQuantizer,
Av1ReferenceFrameStore referenceFrames,
Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null)
{
@ -121,16 +101,7 @@ internal sealed class Av1BlockDecoder : IDisposable
this.frameHeader = frameHeader;
this.frameBuffer = frameBuffer;
this.loopFilterContext = loopFilterContext;
this.inverseQuantizer = inverseQuantizer;
this.referenceFrames = referenceFrames;
int ySize = (1 << this.sequenceHeader.SuperblockSizeLog2) * (1 << this.sequenceHeader.SuperblockSizeLog2);
// One scratch plane is reused for every transform unit. Its maximum size must cover a complete superblock
// across all coded planes, with chroma dimensions reduced independently by their subsampling axes.
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
int chromaSubsampling = (colorConfig.SubSamplingX ? 1 : 0) + (colorConfig.SubSamplingY ? 1 : 0);
int chromaSize = ySize >> chromaSubsampling;
int inverseQuantizationSize = colorConfig.IsMonochrome ? ySize : ySize + (2 * chromaSize);
int maximumBlockLength = 1 << sequenceHeader.SuperblockSizeLog2;
int maximumBlockArea = maximumBlockLength * maximumBlockLength;
int predictorWorkingLength = Math.Max(
@ -143,9 +114,7 @@ internal sealed class Av1BlockDecoder : IDisposable
int predictorWorkingOffset = (2 * maximumBlockArea) + compoundMaskLength;
int chromaFromLumaOffset = predictorWorkingOffset + predictorWorkingLength;
int predictionScratchLength = chromaFromLumaOffset + Av1ChromaFromLumaContext.BufferLength;
this.inverseQuantizationStorageLength = inverseQuantizationSize * 2;
this.transformWorkspaceOffset = this.inverseQuantizationStorageLength;
this.predictionScratchOffset = this.transformWorkspaceOffset + (Av1TransformWorkspace.MaximumLength * 2);
this.predictionScratchOffset = Av1TransformWorkspace.MaximumLength * 2;
// Integer workspaces occupy even signed-short slices so one allocator owner can retain the complete block
// lifetime while prediction still receives the Memory<short> contract needed by its reusable context.
@ -167,25 +136,19 @@ internal sealed class Av1BlockDecoder : IDisposable
predictionScratch.Slice(chromaFromLumaOffset, Av1ChromaFromLumaContext.BufferLength));
}
/// <summary>
/// Gets the reusable raster-order coefficient buffer populated by inverse quantization.
/// </summary>
public Span<int> CurrentInverseQuantizationCoefficients
=> MemoryMarshal.Cast<short, int>(this.workspaceOwner.Memory.Span[..this.inverseQuantizationStorageLength]);
/// <summary>
/// Releases the pooled reconstruction workspaces owned by this decoder.
/// </summary>
public void Dispose() => this.workspaceOwner.Dispose();
/// <summary>
/// Resets the per-plane packed coefficient cursors before reconstructing a superblock.
/// Resets the per-plane coefficient-region cursors before reconstructing a superblock.
/// </summary>
/// <param name="superblockInfo">The superblock whose coefficient streams will be consumed.</param>
public void UpdateSuperblock(Av1SuperblockInfo superblockInfo)
{
// Each superblock owns independent packed coefficient streams for Y, U, and V. The first value for each
// transform unit stores its coefficient count, so DecodeBlock advances a plane cursor as units are consumed.
// Each superblock owns independent coefficient regions for Y, U, and V. Every transform advances its
// plane cursor by its nominal area, including transforms with no coded residual.
this.currentCoefficientIndex[0] = 0;
this.currentCoefficientIndex[1] = 0;
this.currentCoefficientIndex[2] = 0;
@ -202,9 +165,7 @@ internal sealed class Av1BlockDecoder : IDisposable
public void DecodeBlock(Av1BlockModeInfo modeInfo, Point modeInfoPosition, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
{
Span<int> transformWorkspace = MemoryMarshal.Cast<short, int>(
this.workspaceOwner.Memory.Span.Slice(
this.transformWorkspaceOffset,
Av1TransformWorkspace.MaximumLength * 2));
this.workspaceOwner.Memory.Span[..(Av1TransformWorkspace.MaximumLength * 2)]);
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
Av1TransformType transformType;
@ -1346,58 +1307,44 @@ internal sealed class Av1BlockDecoder : IDisposable
}
}
int numberOfCoefficients = 0;
if (!modeInfo.Skip && transformInfo[0].CodeBlockFlag)
int endOfBlock = transformInfo[0].EndOfBlock;
if (endOfBlock != 0)
{
Span<int> quantizationCoefficients = this.CurrentInverseQuantizationCoefficients;
int inverseQuantizationSize = transformSize.GetWidth() * transformSize.GetHeight();
quantizationCoefficients[..inverseQuantizationSize].Clear();
transformType = transformInfo[0].Type;
// Inverse quantization writes raster coefficients into the reusable superblock scratch plane.
numberOfCoefficients = this.inverseQuantizer.InverseQuantize(
modeInfo, coefficients, quantizationCoefficients, transformType, transformSize, (Av1Plane)plane);
if (numberOfCoefficients != 0)
// Entropy decoding has already applied quantization, scan placement, and coefficient clipping.
// Prediction includes a top-reference row; inverse reconstruction begins one stride after it.
if (highBitDepth)
{
// The packed coefficient stream prefixes every transform unit with its decoded coefficient
// count. Advance past that prefix as well as the coefficient values before the next unit.
this.currentCoefficientIndex[plane] += numberOfCoefficients + 1;
if (highBitDepth)
{
// Prediction receives a reference-prefixed span beginning on the previous row. Inverse
// reconstruction operates on the transform itself, so advance to the first destination row.
Av1InverseTransformer.ReconstructHighBitDepth(
quantizationCoefficients,
highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..],
reconstructionStride,
transformSize,
transformType,
plane,
numberOfCoefficients,
isLossless,
this.frameBuffer.BitDepth,
transformWorkspace);
}
else
{
// Keep the reference-prefix convention local to prediction; residuals are added at the
// first reconstructed row rather than the top-neighbor row.
Av1InverseTransformer.Reconstruct8Bit(
quantizationCoefficients,
transformBlockReconstructionBuffer[reconstructionStride..],
reconstructionStride,
transformSize,
transformType,
plane,
numberOfCoefficients,
isLossless,
transformWorkspace);
}
Av1InverseTransformer.ReconstructHighBitDepth(
coefficients,
highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..],
reconstructionStride,
transformSize,
transformType,
plane,
endOfBlock,
isLossless,
this.frameBuffer.BitDepth,
transformWorkspace);
}
else
{
Av1InverseTransformer.Reconstruct8Bit(
coefficients,
transformBlockReconstructionBuffer[reconstructionStride..],
reconstructionStride,
transformSize,
transformType,
plane,
endOfBlock,
isLossless,
transformWorkspace);
}
}
this.currentCoefficientIndex[plane] += transformSize.GetWidth() * transformSize.GetHeight();
// Store Luma for CFL if required!
if (plane == (int)Av1Plane.Y && StoreChromaFromLumaRequired(colorConfig, ref partitionInfo))
{
@ -1429,7 +1376,7 @@ internal sealed class Av1BlockDecoder : IDisposable
}
}
// Transform descriptors are stored in the same traversal order as their packed coefficient groups.
// Transform descriptors and their coefficient regions follow the same per-plane traversal order.
transformInfo = transformInfo[1..];
}
}

40
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -1287,10 +1288,11 @@ public class Av1CoefficientsEntropyTests
0,
0,
levels,
actuals);
actuals,
CreateInverseQuantizer());
// Assert
Assert.Equal(endOfBlock, actuals[0]);
Assert.Equal(endOfBlock, transformInfo.EndOfBlock);
Assert.Equal(expected, actuals);
}
@ -1334,7 +1336,7 @@ public class Av1CoefficientsEntropyTests
coefficientsBuffer[scan[scanIndex]] = 0;
}
Span<int> actuals = new int[16 + 1];
Span<int> actuals = new int[16];
// Act
encoder.WriteCoefficients(transformSize, transformType, intraDirection, coefficientsBuffer, componentType, transformBlockContext, endOfBlock, true, filterIntraMode, usesInterTransformSet: false);
@ -1363,12 +1365,13 @@ public class Av1CoefficientsEntropyTests
0,
0,
levels,
actuals);
actuals,
CreateInverseQuantizer());
decoder.ValidateTrailingBits();
// Assert
Assert.Equal(endOfBlock, actuals[0]);
Assert.Equal(endOfBlock, transformInfo.EndOfBlock);
}
[Theory]
@ -1420,6 +1423,18 @@ public class Av1CoefficientsEntropyTests
RoundTripCoefficientsCore(endOfBlock, componentType, blockSize, transformSize, transformType, intraDirection, filterIntraMode, false, true);
}
private static Av1InverseQuantizer CreateInverseQuantizer()
{
ObuSequenceHeader sequenceHeader = new()
{
ColorConfig = new ObuColorConfig { BitDepth = Av1BitDepth.EightBit }
};
ObuFrameHeader frameHeader = new();
frameHeader.QuantizationParameters.BaseQIndex = BaseQIndex;
return new Av1InverseQuantizer(sequenceHeader, frameHeader);
}
private static void RoundTripCoefficientsCore(
ushort endOfBlock,
Av1ComponentType componentType,
@ -1453,7 +1468,7 @@ public class Av1CoefficientsEntropyTests
}
}
Span<int> actuals = new int[coefficientCount + 1];
Span<int> actuals = new int[coefficientCount];
// Act
encoder.WriteCoefficients(
@ -1492,17 +1507,20 @@ public class Av1CoefficientsEntropyTests
0,
0,
levels,
actuals);
actuals,
CreateInverseQuantizer());
decoder.ValidateTrailingBits();
// Assert
Assert.Equal(endOfBlock, actuals[0]);
Assert.Equal(endOfBlock, transformInfo.EndOfBlock);
// The parser retains quantized levels in entropy scan order; inverse quantization maps them back to raster positions.
for (int coefficientIndex = 0; coefficientIndex < endOfBlock; coefficientIndex++)
// Reference quant_common.c defines 8-bit qindex 23 as DC=26 and AC=30. Entropy output now publishes
// dequantized raster values, including zero runs and positions beyond EOB, rather than packed raw levels.
for (int coefficientIndex = 0; coefficientIndex < coefficientCount; coefficientIndex++)
{
Assert.Equal(coefficientsBuffer[scan[coefficientIndex]], actuals[coefficientIndex + 1]);
int dequant = coefficientIndex == 0 ? 26 : 30;
Assert.Equal(coefficientsBuffer[coefficientIndex] * dequant, actuals[coefficientIndex]);
}
}

28
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs

@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
@ -86,13 +85,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -205,13 +203,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -331,13 +328,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -422,13 +418,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -505,13 +500,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -601,13 +595,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -857,13 +850,12 @@ public class Av1CompoundBlockDecoderTests
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -1252,13 +1244,12 @@ public class Av1CompoundBlockDecoderTests
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
@ -1461,13 +1452,12 @@ public class Av1CompoundBlockDecoderTests
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);

15
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs

@ -127,15 +127,14 @@ public class Av1FrameBufferTests
/// Verifies that block reconstruction uses one exact-size owner across monochrome and chroma plane layouts.
/// </summary>
[Theory]
[InlineData(true, false, false, 4096)]
[InlineData(false, true, true, 6144)]
[InlineData(false, true, false, 8192)]
[InlineData(false, false, false, 12288)]
[InlineData(true, false, false)]
[InlineData(false, true, true)]
[InlineData(false, true, false)]
[InlineData(false, false, false)]
public void BlockDecoderUsesOneContiguousWorkspaceOwner(
bool isMonochrome,
bool subsamplingX,
bool subsamplingY,
int expectedInverseQuantizationSize)
bool subsamplingY)
{
TestMemoryAllocator allocator = new();
Configuration configuration = Configuration.Default.Clone();
@ -170,7 +169,6 @@ public class Av1FrameBufferTests
using Av1LoopFilterContext loopFilterContext =
new(Configuration.Default.MemoryAllocator, sequenceHeader, frameHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1ReferenceFrameStore referenceFrames = new();
// Reset the frame-plane logs so the following assertions describe only the block decoder's scratch owner.
@ -191,7 +189,6 @@ public class Av1FrameBufferTests
Av1ChromaFromLumaContext.BufferLength;
int expectedWorkspaceLength =
(expectedInverseQuantizationSize * 2) +
(Av1TransformWorkspace.MaximumLength * 2) +
predictionScratchLength;
@ -201,14 +198,12 @@ public class Av1FrameBufferTests
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames))
{
workspaceAllocation = Assert.Single(allocator.AllocationLog);
Assert.Empty(allocator.ReturnLog);
Assert.Equal(typeof(short), workspaceAllocation.ElementType);
Assert.Equal(expectedWorkspaceLength, workspaceAllocation.Length);
Assert.Equal(expectedInverseQuantizationSize, blockDecoder.CurrentInverseQuantizationCoefficients.Length);
}
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);

91
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseQuantizationTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -11,6 +12,96 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
[Trait("Format", "Avif")]
public class Av1InverseQuantizationTests
{
[Theory]
[InlineData((int)Av1BitDepth.EightBit, 26, 30, 40, 32767, -32768)]
[InlineData((int)Av1BitDepth.TenBit, 75, 83, 112, 131071, -131072)]
[InlineData((int)Av1BitDepth.TwelveBit, 266, 297, 399, 524287, -524288)]
public void DequantizationMatchesReferenceMatrixAndPrecisionValues(
int bitDepthValue,
int dc,
int ac,
int weightedAc,
int maximum,
int minimum)
{
ObuSequenceHeader sequenceHeader = new()
{
ColorConfig = new ObuColorConfig { BitDepth = (Av1BitDepth)bitDepthValue }
};
ObuFrameHeader frameHeader = new();
frameHeader.QuantizationParameters.BaseQIndex = 23;
frameHeader.QuantizationParameters.IsUsingQMatrix = true;
frameHeader.SegmentationParameters.QMLevel[0][0] = 0;
Av1InverseQuantizer quantizer = new(sequenceHeader, frameHeader);
Av1BlockModeInfo mode = new(Av1BlockSize.Block4x4, Point.Empty);
Av1InverseQuantizer.TransformParameters matrix = new(
quantizer, mode, Av1TransformType.DctDct, Av1TransformSize.Size4x4, Av1Plane.Y);
// quant_common.c's qindex-23 tables supply the three DC/AC pairs above. Its level-zero luma matrix
// begins with weights 32 and 43; the rounded AC values are independently fixed in the theory data.
Assert.Equal(7 * dc, matrix.Dequantize(7, 0, false));
Assert.Equal(11 * weightedAc, matrix.Dequantize(11, 1, false));
Assert.Equal(-11 * weightedAc, matrix.Dequantize(11, 1, true));
Assert.Equal(maximum, matrix.Dequantize(0xfffff, 1, false));
Assert.Equal(minimum, matrix.Dequantize(0xfffff, 1, true));
// Identity and one-dimensional transforms bypass matrix weighting even when the frame enables it.
Av1InverseQuantizer.TransformParameters identity = new(
quantizer, mode, Av1TransformType.Identity, Av1TransformSize.Size4x4, Av1Plane.Y);
Av1InverseQuantizer.TransformParameters horizontal = new(
quantizer, mode, Av1TransformType.HorizontalAdst, Av1TransformSize.Size4x4, Av1Plane.Y);
Assert.Equal(11 * ac, identity.Dequantize(11, 1, false));
Assert.Equal(11 * ac, horizontal.Dequantize(11, 1, false));
}
[Fact]
public void DequantizationPreservesProductMaskAndTransformRounding()
{
ObuSequenceHeader sequenceHeader = new()
{
ColorConfig = new ObuColorConfig { BitDepth = Av1BitDepth.EightBit }
};
ObuFrameHeader frameHeader = new();
frameHeader.QuantizationParameters.BaseQIndex = 23;
frameHeader.QuantizationParameters.IsUsingQMatrix = true;
frameHeader.SegmentationParameters.QMLevel[0][0] = 0;
Av1InverseQuantizer quantizer = new(sequenceHeader, frameHeader);
Av1BlockModeInfo mode = new(Av1BlockSize.Block64x64, Point.Empty);
Av1InverseQuantizer.TransformParameters matrix = new(
quantizer, mode, Av1TransformType.DctDct, Av1TransformSize.Size4x4, Av1Plane.Y);
// The final 4x4 matrix weight is 200, giving AC=188. Its product with 89241 is 2^24 + 92:
// retaining the 24-bit intermediate must produce 92, rather than saturating the unmasked product.
Assert.Equal(92, matrix.Dequantize(89241, 15, false));
Assert.Equal(-92, matrix.Dequantize(89241, 15, true));
frameHeader.QuantizationParameters.IsUsingQMatrix = false;
Av1InverseQuantizer.TransformParameters scaled32 = new(
quantizer, mode, Av1TransformType.DctDct, Av1TransformSize.Size32x32, Av1Plane.Y);
Av1InverseQuantizer.TransformParameters scaled64 = new(
quantizer, mode, Av1TransformType.DctDct, Av1TransformSize.Size64x64, Av1Plane.Y);
// A magnitude of 11 with AC=30 gives 330 before scaling. Sign follows truncation of the positive
// magnitude, so the negative 64x64 result is -82 rather than the arithmetic-right-shift result -83.
Assert.Equal(165, scaled32.Dequantize(11, 1, false));
Assert.Equal(82, scaled64.Dequantize(11, 1, false));
Assert.Equal(-82, scaled64.Dequantize(11, 1, true));
frameHeader.QuantizationParameters.BaseQIndex = 0;
frameHeader.QuantizationParameters.IsUsingQMatrix = true;
frameHeader.LosslessArray[0] = true;
Av1InverseQuantizer losslessQuantizer = new(sequenceHeader, frameHeader);
Av1InverseQuantizer.TransformParameters lossless = new(
losslessQuantizer, mode, Av1TransformType.DctDct, Av1TransformSize.Size4x4, Av1Plane.Y);
Assert.Equal(44, lossless.Dequantize(11, 1, false));
}
[Fact]
public void MatricesCoverAllLevelsPlanesAndTransformSizes()
{

Loading…
Cancel
Save