Browse Source

Complete exhaustive luma transform search

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
cab9bc47cf
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 8
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs
  3. 10
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs
  4. 10
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs
  5. 46
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.IntraBlockCopyModeDecision.cs
  6. 311
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  7. 14
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs
  8. 3
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

8
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs

@ -64,15 +64,15 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
public Av1EncoderModeDecisionWorkspace(Span<int> storage) => this.storage = storage;
/// <summary>
/// Gets the temporary prediction span used by filter-intra mode search.
/// Gets the temporary prediction span shared by mutually exclusive mode searches.
/// </summary>
public Span<TSample> FilterPrediction
public Span<TSample> Prediction
=> MemoryMarshal.Cast<int, TSample>(this.storage[TransientStorageOffset..])[..MaximumSampleCount];
/// <summary>
/// Gets the temporary residual span used by filter-intra mode search.
/// Gets the temporary residual span shared by mutually exclusive mode searches.
/// </summary>
public Span<short> FilterResidual
public Span<short> Residual
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(
TransientStorageOffset + (MaximumSampleCount * sizeof(ushort) / sizeof(int)),

10
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs

@ -200,6 +200,9 @@ internal static partial class Av1IntraSuperblockEncoder
Av1EncoderTransformBlockState candidateBlueState = default;
Av1EncoderTransformBlockState candidateRedState = default;
// A chroma mode and angle are shared by U and V, so neither plane can replace the
// retained result independently. Their complete rate and distortion compete jointly.
long candidateCost = this.GetChromaCandidateCost(
writer,
modeInfo,
@ -395,6 +398,8 @@ internal static partial class Av1IntraSuperblockEncoder
if (chromaFromLumaSelected)
{
// The alpha tables retain only rate and distortion. Regenerate the two selected planes
// once here instead of copying reconstruction and coefficient blocks for all 66 trials.
Av1EncoderTransformBlockState candidateBlueState = default;
_ = this.GetChromaFromLumaPlaneCost(
writer,
@ -561,6 +566,9 @@ internal static partial class Av1IntraSuperblockEncoder
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
Av1PredictionMode predictionMode = chromaMode.ToLumaMode();
// Intra chroma derives one transform type from the shared UV prediction mode. The type is not
// signaled independently for either chroma plane, so U and V must use the same legal fallback.
Av1TransformType transformType = Av1SymbolContextHelper.GetDefaultIntraTransformType(
predictionMode,
transformSize,
@ -608,6 +616,8 @@ internal static partial class Av1IntraSuperblockEncoder
this.bitDepth,
ref candidateRedState);
// The mode and angle are written once for the UV pair; coefficient syntax remains independent
// because each plane has its own EOB, scan values, and neighboring coefficient context.
int rate = Av1TileWriter.GetChromaModeCost(
writer,
this.picture.Parent.FrameHeader,

10
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs

@ -150,6 +150,8 @@ internal static partial class Av1IntraSuperblockEncoder
workspace.GetAlternateCentroids(1),
workspace.AlternateIndices);
// Only U participates in the neighbor-color cache. Snap bounded U deltas before sorting,
// while preserving each U/V centroid as one paired palette entry.
for (int colorIndex = 0; colorIndex < paletteSize && !colorCache.IsEmpty; colorIndex++)
{
int minimumDifference = Math.Abs(candidateBlueCentroids[colorIndex] - colorCache[0]);
@ -221,6 +223,8 @@ internal static partial class Av1IntraSuperblockEncoder
redPaletteColors[colorIndex] = (ushort)candidateRedCentroids[colorIndex];
}
// U and V share one color-index map but reconstruct through their own palette values and
// residuals. Both preparations remain valid until the next palette-size candidate.
TOperator.PreparePalette(
blueSource,
chromaOrigin,
@ -239,6 +243,8 @@ internal static partial class Av1IntraSuperblockEncoder
redResidual[..sampleCount],
transformSize);
// Intra chroma derives one transform type from the shared UV mode. Palette uses UV DC, so
// both planes use DCT while retaining independent coefficient contexts and end positions.
Av1EncoderTransformBlockState candidateBlueState = default;
long distortion = TOperator.EncodePredictionCandidate(
this.blockWorkspace,
@ -314,6 +320,8 @@ internal static partial class Av1IntraSuperblockEncoder
Av1FilterIntraMode.AllFilterIntraModes,
usesInterTransformSet: false);
// Mode, palette, and color-map syntax is shared by the pair; coefficient syntax and
// distortion remain per plane before the joint chroma rate-distortion comparison.
rate += writer.GetCoefficientCost(
transformSize,
Av1TransformType.DctDct,
@ -329,6 +337,8 @@ internal static partial class Av1IntraSuperblockEncoder
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
if (candidateCost < bestCost)
{
// Every following palette size overwrites the shared maps and candidate spans, so a
// global improvement must retain reconstruction, coefficients, colors, and indices together.
Buffer2DRegion<TSample> blueReconstruction = this.reconstruction.GetPlane(Av1Plane.U);
Buffer2DRegion<TSample> redReconstruction = this.reconstruction.GetPlane(Av1Plane.V);
CopyCandidate(

46
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.IntraBlockCopyModeDecision.cs

@ -102,6 +102,8 @@ internal static partial class Av1IntraSuperblockEncoder
return;
}
// Mode-decision costs already contain the selected coefficient syntax but not the block's skip
// or IBC choice. When regular intra skips, replace its empty-coefficient rate with skip syntax.
int skipContext = Av1TileWriter.GetSkipContext(macroBlock);
int regularRateAdjustment = writer.GetUseIntraBlockCopyCost(false) +
writer.GetSkipCost(modeInfo.Block.Skip, skipContext);
@ -128,6 +130,8 @@ internal static partial class Av1IntraSuperblockEncoder
BlockSize,
LumaTransformSize);
// A coded IBC residual uses the unsplit transform root at this fixed block size. A skipped block
// omits both the transform-partition bit and coefficient syntax, so this rate is added only below.
int transformPartitionRate = 0;
if (this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{
@ -176,6 +180,8 @@ internal static partial class Av1IntraSuperblockEncoder
chromaTransformSize);
}
// Per-vector plane results reuse candidate scratch. Separate selected spans retain only a new
// global winner, allowing the complete search to finish before committed reconstruction changes.
for (int candidateIndex = 0; candidateIndex < uniqueCandidateCount; candidateIndex++)
{
Av1MotionVector candidate = candidates[candidateIndex];
@ -291,6 +297,9 @@ internal static partial class Av1IntraSuperblockEncoder
long candidateDistortion = lumaDistortion + blueDistortion + redDistortion;
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, candidateDistortion);
bool candidateSkip = false;
// The skip alternative is available only when every coded plane has an empty transform. Its
// distortion comes from prediction alone and its rate excludes the transform tree and coefficients.
if (hasEmptyLuma && hasEmptyBlue && hasEmptyRed)
{
int skipRate = writer.GetUseIntraBlockCopyCost(true) +
@ -363,6 +372,8 @@ internal static partial class Av1IntraSuperblockEncoder
return;
}
// Only the winning vector is now visible to later coding blocks. This single publication keeps
// rejected motion vectors from contaminating intra references or entropy contexts.
Span<int> retainedLumaCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.Y);
Span<Av1EncoderTransformBlockState> retainedLumaTransformBlocks =
this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.Y);
@ -473,6 +484,8 @@ internal static partial class Av1IntraSuperblockEncoder
residual[..sampleCount],
transformSize);
// Motion compensation and subtraction do not depend on transform type. Keep them outside the
// transform loop so exhaustive luma search traverses the source and reference blocks only once.
Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(
transformSize,
isInter: true,
@ -492,6 +505,15 @@ internal static partial class Av1IntraSuperblockEncoder
hasEmptyTransform = false;
emptyState = default;
emptyDistortion = 0;
// The candidate and best spans alternate ownership whenever a transform improves the result.
// This mirrors the reference's buffer-pointer swap and replaces a copy on every improvement
// with at most one normalization copy after the transform search.
Span<TSample> candidateReconstruction = transformReconstruction[..sampleCount];
Span<int> candidateCoefficients = transformCoefficients[..sampleCount];
Span<TSample> bestReconstruction = selectedReconstruction[..sampleCount];
Span<int> bestCoefficients = selectedCoefficients[..sampleCount];
bool bestUsesSelectedStorage = true;
for (Av1TransformType transformType = firstTransformType;
transformType < transformTypeLimit;
transformType++)
@ -508,9 +530,9 @@ internal static partial class Av1IntraSuperblockEncoder
planeOrigin,
prediction[..sampleCount],
residual[..sampleCount],
transformReconstruction[..sampleCount],
candidateReconstruction,
transformSize.GetWidth(),
transformCoefficients[..sampleCount],
candidateCoefficients,
transformSize,
transformType,
plane,
@ -524,7 +546,7 @@ internal static partial class Av1IntraSuperblockEncoder
transformSize,
transformType,
Av1PredictionMode.DC,
transformCoefficients[..sampleCount],
candidateCoefficients,
componentType,
blockContext,
candidateState.EndOfBlock,
@ -539,8 +561,14 @@ internal static partial class Av1IntraSuperblockEncoder
if (candidateCost < bestCost)
{
transformReconstruction[..sampleCount].CopyTo(selectedReconstruction);
transformCoefficients[..sampleCount].CopyTo(selectedCoefficients);
Span<TSample> previousBestReconstruction = bestReconstruction;
bestReconstruction = candidateReconstruction;
candidateReconstruction = previousBestReconstruction;
Span<int> previousBestCoefficients = bestCoefficients;
bestCoefficients = candidateCoefficients;
candidateCoefficients = previousBestCoefficients;
bestUsesSelectedStorage = !bestUsesSelectedStorage;
bestCost = candidateCost;
selectedState = candidateState;
selectedRate = candidateRate;
@ -555,6 +583,14 @@ internal static partial class Av1IntraSuperblockEncoder
emptyDistortion = candidateDistortion;
}
}
// Callers retain the designated selected spans after this scratch workspace is reused by the
// next plane or motion vector, so normalize only when the final best result occupies scratch.
if (!bestUsesSelectedStorage)
{
bestReconstruction.CopyTo(selectedReconstruction);
bestCoefficients.CopyTo(selectedCoefficients);
}
}
}
}

311
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -207,6 +207,8 @@ internal static partial class Av1IntraSuperblockEncoder
block.FilterIntraMode = filterIntraMode;
modeInfo.Block.TransformSize = lumaTransformSize;
// One 8x8 coding block retains either one 8x8 transform or four 4x4 transforms. The block-level
// skip decision is legal only when every transform selected by mode decision has an empty EOB.
int lumaTransformBlockCount = LumaTransformSize.GetSize2d() / lumaTransformSize.GetSize2d();
bool lumaTransformEmpty = true;
for (int transformIndex = 0; transformIndex < lumaTransformBlockCount; transformIndex++)
@ -295,6 +297,8 @@ internal static partial class Av1IntraSuperblockEncoder
block.PredictionUnit.ChromaFromLumaIndex = chromaFromLumaIndex;
block.PredictionUnit.ChromaFromLumaSigns = chromaFromLumaSigns;
// Skip suppresses coefficient syntax for the entire coding block, not one plane independently.
// Preserve normal coefficient coding when any selected luma or chroma transform is nonempty.
bool allTransformsEmpty = lumaTransformEmpty && blueState.EndOfBlock == 0 && redState.EndOfBlock == 0;
int regularEmptyTransformRate = 0;
if (allTransformsEmpty)
@ -590,10 +594,13 @@ internal static partial class Av1IntraSuperblockEncoder
Span<TSample> candidateReconstruction = workspace.GetCandidateReconstruction(0);
Span<int> candidateCoefficients = workspace.GetCandidateCoefficients(0);
Span<TSample> prediction = workspace.Prediction;
Span<short> residual = workspace.Residual;
long bestCost = long.MaxValue;
Av1PredictionMode bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0;
selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
selectedTransformSize = TransformSize;
int baseModeCount = LumaModeSearchOrder.Length;
int deltaCount = AngleDeltaSearchOrder.Length;
int directionalModeCount = (int)Av1PredictionMode.Directional67Degrees - (int)Av1PredictionMode.Vertical + 1;
@ -611,6 +618,11 @@ internal static partial class Av1IntraSuperblockEncoder
TransformSize,
useReducedTransformSet);
// Transform type and transform size are separate search axes. Splitting their effort thresholds
// gives callers a useful intermediate tier without changing the fast default path.
bool searchEveryTransformType = this.effort >= 7;
bool searchEveryTransformSize = this.effort >= 8;
// Zero-angle modes precede groups of six nonzero adjustments for each directional mode.
// A single index preserves that tie-breaking order without duplicating candidate evaluation.
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++)
@ -629,102 +641,196 @@ internal static partial class Av1IntraSuperblockEncoder
angleDelta = AngleDeltaSearchOrder[adjustedIndex % deltaCount];
}
Av1TransformType defaultTransformType = Av1SymbolContextHelper.GetDefaultIntraTransformType(
mode,
TransformSize,
useReducedTransformSet);
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
// Prediction and subtraction do not depend on transform type. Preparing them once keeps
// exhaustive transform search from repeating the same pixel traversal for every candidate.
TOperator.PrepareIntra(
this.blockWorkspace,
sourcePlane,
blockOrigin,
prediction,
above,
left,
hasLeft,
hasAbove,
mode,
angleDelta,
defaultTransformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
residual,
TransformSize,
this.bitDepth);
// Transform types are visited in AV1 enumeration order. A strict cost comparison below keeps
// the first legal type on ties, while lower efforts visit only the mode-derived default.
Av1TransformType firstTransformType = searchEveryTransformType
? Av1TransformType.DctDct
: Av1SymbolContextHelper.GetDefaultIntraTransformType(
mode,
TransformSize,
useReducedTransformSet);
if (candidateCost < bestCost)
Av1TransformType transformTypeLimit = searchEveryTransformType
? Av1TransformType.AllTransformTypes
: (Av1TransformType)((int)firstTransformType + 1);
for (Av1TransformType transformType = firstTransformType;
transformType < transformTypeLimit;
transformType++)
{
CopyCandidate(
if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
blockOrigin,
prediction,
residual,
mode,
angleDelta,
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
if (candidateCost < bestCost)
{
// The shared candidate spans are overwritten by the next transform. Copy only a
// global improvement into final block storage so no per-mode retained buffer is needed.
CopyCandidate(
candidateReconstruction,
candidateCoefficients,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
bestCost = candidateCost;
bestMode = mode;
selectedAngleDelta = angleDelta;
selectedTransformSize = TransformSize;
}
}
// At exhaustive effort, transform size belongs to this mode's RD result. Evaluate it
// before advancing so an 8x8-only preliminary result cannot discard a better split mode.
if (searchEveryTransformSize &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{
long splitCost = this.GetSplitLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
tileIndex,
mode,
angleDelta,
Av1FilterIntraMode.AllFilterIntraModes,
0,
ReadOnlySpan<ushort>.Empty,
0,
paletteDisabledCost,
transformSizeContext,
bestCost,
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks);
bestCost = candidateCost;
bestMode = mode;
selectedAngleDelta = angleDelta;
if (splitCost < bestCost)
{
CopySplitCandidate(
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
retainedStates);
bestCost = splitCost;
bestMode = mode;
selectedAngleDelta = angleDelta;
selectedTransformSize = Av1TransformSize.Size4x4;
}
}
}
// Lower effort levels retain the mode-derived default transform. Higher levels refine only the winning
// mode across the legal transform set, preserving search order without evaluating every mode-transform pair.
// Midrange effort refines the preliminary mode only. Higher effort already searched every
// mode-transform pair above, so repeating the winning mode would add no candidates.
long bestTransformCost = bestCost;
for (Av1TransformType transformType = Av1TransformType.DctDct;
this.effort >= 3 && transformType < Av1TransformType.AllTransformTypes;
transformType++)
if (this.effort >= 3 && !searchEveryTransformType)
{
if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
// The shared spans now contain the last mode visited above, so rebuild the preliminary
// winner once before refining its transform types.
TOperator.PrepareIntra(
this.blockWorkspace,
sourcePlane,
blockOrigin,
prediction,
above,
left,
hasLeft,
hasAbove,
bestMode,
selectedAngleDelta,
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
residual,
TransformSize,
this.bitDepth);
if (candidateCost < bestTransformCost)
for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes;
transformType++)
{
CopyCandidate(
if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
blockOrigin,
prediction,
residual,
bestMode,
selectedAngleDelta,
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
ref candidateState);
bestTransformCost = candidateCost;
if (candidateCost < bestTransformCost)
{
CopyCandidate(
candidateReconstruction,
candidateCoefficients,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
bestTransformCost = candidateCost;
}
}
}
if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra)
{
Span<TSample> filterPrediction = workspace.FilterPrediction;
Span<short> filterResidual = workspace.FilterResidual;
// Each recursive filter prediction and its source residual are independent of transform type.
// Prepare them once per filter mode so all legal transforms reuse the same samples.
for (Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC;
@ -735,10 +841,10 @@ internal static partial class Av1IntraSuperblockEncoder
this.blockWorkspace,
sourcePlane,
blockOrigin,
filterPrediction,
prediction,
above,
left,
filterResidual,
residual,
filterIntraMode,
TransformSize,
this.bitDepth);
@ -758,8 +864,8 @@ internal static partial class Av1IntraSuperblockEncoder
macroBlock,
sourcePlane,
blockOrigin,
filterPrediction,
filterResidual,
prediction,
residual,
filterIntraMode,
transformType,
blockContext,
@ -785,12 +891,56 @@ internal static partial class Av1IntraSuperblockEncoder
bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0;
selectedFilterIntraMode = filterIntraMode;
selectedTransformSize = TransformSize;
}
}
// Filter-intra mode and transform size form one candidate for RD comparison, just as
// ordinary spatial mode and transform size do in the exhaustive search above.
if (searchEveryTransformSize &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{
long splitCost = this.GetSplitLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
reconstructionPlane,
blockOrigin,
tileIndex,
Av1PredictionMode.DC,
0,
filterIntraMode,
0,
ReadOnlySpan<ushort>.Empty,
0,
paletteDisabledCost,
transformSizeContext,
bestTransformCost,
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks);
if (splitCost < bestTransformCost)
{
CopySplitCandidate(
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
retainedStates);
bestTransformCost = splitCost;
bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0;
selectedFilterIntraMode = filterIntraMode;
selectedTransformSize = Av1TransformSize.Size4x4;
}
}
}
}
selectedTransformSize = TransformSize;
if (this.effort >= 5 &&
this.picture.Parent.FrameHeader.AllowScreenContentTools &&
this.SelectLumaPalette(
@ -817,7 +967,10 @@ internal static partial class Av1IntraSuperblockEncoder
selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
}
// Efforts six and seven save work by testing transform size only for the global non-palette
// winner. Effort eight and above already tested both sizes inside every candidate.
if (this.effort >= 6 &&
!searchEveryTransformSize &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select &&
paletteInfo.PaletteSizes[0] == 0)
{
@ -896,7 +1049,7 @@ internal static partial class Av1IntraSuperblockEncoder
TransformSampleCount);
Span<int> transformCoefficients = workspace.GetCandidateCoefficients(1)[..TransformSampleCount];
Span<short> residual = workspace.FilterResidual[..TransformSampleCount];
Span<short> residual = workspace.Residual[..TransformSampleCount];
Span<byte> contexts = workspace.TransformContexts;
Span<byte> topContexts = contexts[..2];
Span<byte> leftContexts = contexts[2..4];
@ -912,6 +1065,8 @@ internal static partial class Av1IntraSuperblockEncoder
TransformSize,
useReducedTransformSet);
// Prediction-mode and transform-size symbols belong to the 8x8 coding block, while each
// 4x4 transform contributes its own coefficient rate below.
int rate = writer.GetTransformSizeCost(BlockSize, TransformSize, transformSizeContext);
if (paletteSize > 0)
{
@ -941,6 +1096,9 @@ internal static partial class Av1IntraSuperblockEncoder
}
long distortion = 0;
// Raster order is observable here: each retained 4x4 reconstruction supplies reference
// samples and coefficient context to transforms that follow it in the same coding block.
for (int transformRow = 0; transformRow < 2; transformRow++)
{
for (int transformColumn = 0; transformColumn < 2; transformColumn++)
@ -1148,6 +1306,9 @@ internal static partial class Av1IntraSuperblockEncoder
int columnOffset = transformColumn * TransformWidth;
int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2;
int modeInfoColumn = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2;
// Internal top and left edges come from the candidate mosaic built in raster order. Edges outside
// the 8x8 candidate continue to read committed reconstruction, keeping unsuccessful trials isolated.
hasAbove = transformRow > 0 || macroBlock.IsUpAvailable;
hasLeft = transformColumn > 0 || macroBlock.IsLeftAvailable;
bool rightAvailable =
@ -1291,10 +1452,8 @@ internal static partial class Av1IntraSuperblockEncoder
Av1MacroBlockD macroBlock,
Buffer2DRegion<TSample> sourcePlane,
Point blockOrigin,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
bool hasLeft,
bool hasAbove,
ReadOnlySpan<TSample> prediction,
ReadOnlySpan<short> residual,
Av1PredictionMode mode,
int angleDelta,
Av1TransformType transformType,
@ -1307,17 +1466,17 @@ internal static partial class Av1IntraSuperblockEncoder
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
long distortion = TOperator.EncodeCandidate(
// Prediction and subtraction were prepared by the owning mode loop. This stage performs only
// transform, quantization, reconstruction, and distortion for the requested transform type.
long distortion = TOperator.EncodePredictionCandidate(
this.blockWorkspace,
sourcePlane,
blockOrigin,
prediction,
residual,
candidateReconstruction,
above,
left,
hasLeft,
hasAbove,
mode,
angleDelta,
TransformSize.GetWidth(),
candidateCoefficients,
TransformSize,
transformType,
@ -1328,6 +1487,8 @@ internal static partial class Av1IntraSuperblockEncoder
this.bitDepth,
ref candidateState);
// Charge every block-level choice that distinguishes this spatial candidate before adding
// coefficient syntax derived from the live neighboring-transform context.
int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode, angleDelta);
rate += transformSizeRate;
if (mode == Av1PredictionMode.DC)

14
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs

@ -55,6 +55,9 @@ internal static partial class Av1IntraSuperblockEncoder
int uniqueColorCount = 0;
short minimum = samples[0];
short maximum = samples[0];
// An 8x8 block has at most 64 distinct samples, so a compact workspace histogram avoids a
// dictionary allocation while collecting both frequency seeds and range endpoints.
foreach (short sample in samples)
{
int colorIndex = uniqueColors[..uniqueColorCount].IndexOf(sample);
@ -127,7 +130,9 @@ internal static partial class Av1IntraSuperblockEncoder
Span<short> centroids = workspace.GetCentroids(0);
bool paletteSelected = false;
// Exhaustive ascending size search avoids the reference encoder's speed-dependent pruning.
// Evaluate both frequency-seeded and range-seeded palette families for every legal size.
// Exhaustive ascending size order avoids speed-dependent pruning and gives smaller palettes
// deterministic precedence when complete rate-distortion costs tie.
for (int paletteSize = 2; paletteSize <= maximumPaletteSize; paletteSize++)
{
for (int index = 0; index < paletteSize; index++)
@ -290,6 +295,9 @@ internal static partial class Av1IntraSuperblockEncoder
int bitDepth = this.bitDepth.GetBitCount();
int cacheThreshold = 4 << (bitDepth - 8);
// Nearby colors snap to a coded-neighbor cache entry when the quantization error is bounded.
// Snapping can merge centroids, so sorting and compaction below establish the final coded palette.
for (int colorIndex = 0; colorIndex < centroids.Length && !colorCache.IsEmpty; colorIndex++)
{
int minimumDifference = Math.Abs(centroids[colorIndex] - colorCache[0]);
@ -376,6 +384,8 @@ internal static partial class Av1IntraSuperblockEncoder
columns,
colorIndexMap);
// Palette prediction and subtraction are invariant for this color map. Reuse them across legal
// transform types, whose enumeration order also supplies deterministic tie precedence.
for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes;
transformType++)
@ -420,6 +430,8 @@ internal static partial class Av1IntraSuperblockEncoder
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, distortion);
if (candidateCost < bestCost)
{
// Later palette sizes reuse every candidate span and the shared color map. Publish the
// complete palette state only when this candidate improves the global luma decision.
CopyCandidate(
candidateReconstruction,
candidateCoefficients,

3
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -496,6 +496,9 @@ public class Av1EncoderFrameTests
[InlineData(4, true, false, false)]
[InlineData(5, true, true, false)]
[InlineData(6, true, true, true)]
[InlineData(7, true, true, true)]
[InlineData(8, true, true, true)]
[InlineData(10, true, true, true)]
public void EncodeEffortControlsSearchFeatures(
int effort,
bool enableFilterIntra,

Loading…
Cancel
Save