Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Se
int tlambda = dqm.TLambda;
Span<byte> src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc);
Span<int> scratch = it.Scratch3.AsSpan();
Vp8ModeScore rdTmp = new();
Vp8Residual res = new();

// Reuse the iterator scratch accumulator instead of allocating one per macroblock.
// Clear it so that it starts from the same zeroed state as a fresh instance.
Vp8ModeScore rdTmp = it.RdScratch;
rdTmp.Clear();
Vp8Residual res = it.ResidualScratch;
Vp8ModeScore rdCur = rdTmp;
Vp8ModeScore rdBest = rd;
int mode;
Expand Down Expand Up @@ -80,6 +84,10 @@ public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Se
}
}

// The rejected accumulator becomes the scratch instance for the next macroblock. When the
// best result lives in the scratch instance, the caller's reference is exchanged for it so
// that no levels need to be copied.
it.RdScratch = rdCur;
if (rdBest != rd)
{
rd = rdBest;
Expand Down Expand Up @@ -107,7 +115,11 @@ public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Seg
Span<byte> bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc);
Span<int> scratch = it.Scratch3.AsSpan();
int totalHeaderBits = 0;
Vp8ModeScore rdBest = new();

// Reuse the iterator scratch accumulators instead of allocating per macroblock. rdBest
// collects the winning levels of every 4x4 block, so it must start zeroed like a fresh instance.
Vp8ModeScore rdBest = it.RdScratch2;
rdBest.Clear();

if (maxI4HeaderBits == 0)
{
Expand All @@ -118,9 +130,11 @@ public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Seg
rdBest.H = 211; // '211' is the value of VP8BitCost(0, 145)
rdBest.SetRdScore(dqm.LambdaMode);
it.StartI4();
Vp8ModeScore rdi4 = new();
Vp8ModeScore rdTmp = new();
Vp8Residual res = new();

// Both accumulators are cleared inside the loops before they are read.
Vp8ModeScore rdi4 = it.RdScratch3;
Vp8ModeScore rdTmp = it.RdScratch;
Vp8Residual res = it.ResidualScratch;
Span<short> tmpLevels = stackalloc short[16];
do
{
Expand Down Expand Up @@ -220,9 +234,12 @@ public static void PickBestUv(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Segment
Span<byte> tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.UOffEnc);
Span<byte> dst0 = it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc);
Span<byte> dst = dst0;
Vp8ModeScore rdBest = new();
Vp8ModeScore rdUv = new();
Vp8Residual res = new();

// Reuse the iterator scratch accumulators instead of allocating per macroblock. Only the
// scores of rdBest are read, and rdUv is cleared before every mode, so neither needs clearing here.
Vp8ModeScore rdBest = it.RdScratch2;
Vp8ModeScore rdUv = it.RdScratch3;
Vp8Residual res = it.ResidualScratch;
int mode;

rd.ModeUv = -1;
Expand Down
36 changes: 33 additions & 3 deletions src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[]
this.Scratch = new byte[WebpConstants.Bps * 16];
this.Scratch2 = new short[17 * 16];
this.Scratch3 = new int[16];
this.RdScratch = new Vp8ModeScore();
this.RdScratch2 = new Vp8ModeScore();
this.RdScratch3 = new Vp8ModeScore();
this.ResidualScratch = new Vp8Residual();

// To match the C initial values of the reference implementation, initialize all with 204.
const byte defaultInitVal = 204;
Expand Down Expand Up @@ -234,6 +238,30 @@ public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[]
/// </summary>
public int[] Scratch3 { get; }

/// <summary>
/// Gets or sets the first reusable rate-distortion score accumulator.
/// <see cref="QuantEnc"/> uses the three accumulators as scratch state while it evaluates the
/// prediction modes of a macroblock, so mode evaluation does not allocate per macroblock.
/// <see cref="QuantEnc.PickBestIntra16"/> exchanges this instance with the caller's accumulator
/// when the best mode ends up in the scratch instance, which is why the property has a setter.
/// </summary>
public Vp8ModeScore RdScratch { get; set; }

/// <summary>
/// Gets the second reusable rate-distortion score accumulator.
/// </summary>
public Vp8ModeScore RdScratch2 { get; }

/// <summary>
/// Gets the third reusable rate-distortion score accumulator.
/// </summary>
public Vp8ModeScore RdScratch3 { get; }

/// <summary>
/// Gets the reusable residual used to cost the candidate modes of a macroblock.
/// </summary>
public Vp8Residual ResidualScratch { get; }

public Vp8MacroBlockInfo CurrentMacroBlockInfo => this.Mb[this.currentMbIdx];

private Vp8MacroBlockInfo[] Mb { get; }
Expand Down Expand Up @@ -416,18 +444,20 @@ public int MbAnalyzeBestIntra4Mode(int bestAlpha)
const int maxMode = MaxIntra4Mode;
Vp8Histogram totalHisto = new();
int curHisto = 0;

// The two entries alternate between the candidate under test and the best candidate so far.
// CollectHistogram fully initializes an entry before it is read, so no per-block reset is needed.
Span<Vp8Histogram> histos = stackalloc Vp8Histogram[2];
this.StartI4();
do
{
int mode;
int bestModeAlpha = DefaultAlpha;
Vp8Histogram[] histos = new Vp8Histogram[2];
Span<byte> src = this.YuvIn.AsSpan(YOffEnc + WebpLookupTables.Vp8Scan[this.I4]);

this.MakeIntra4Preds();
for (mode = 0; mode < maxMode; ++mode)
{
histos[curHisto] = new Vp8Histogram();
histos[curHisto].CollectHistogram(src, this.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]), 0, 1);

int alpha = histos[curHisto].GetAlpha();
Expand All @@ -442,7 +472,7 @@ public int MbAnalyzeBestIntra4Mode(int bestAlpha)
}

// Accumulate best histogram.
histos[curHisto ^ 1].Merge(totalHisto);
histos[curHisto ^ 1].Merge(ref totalHisto);
}
while (this.RotateI4(this.YuvIn.AsSpan(YOffEnc))); // Note: we reuse the original samples for predictors.

Expand Down
7 changes: 4 additions & 3 deletions src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd
it.Init();
this.SetLoopParams(stats.Q);
Vp8ModeScore info = new();
Vp8Residual residual = new();
do
{
info.Clear();
Expand All @@ -642,7 +643,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd
++this.Proba.NbSkip;
}

this.RecordResiduals(it, info);
this.RecordResiduals(it, info, residual);
size += info.R + info.H;
sizeP0 += info.H;
distortion += info.D;
Expand Down Expand Up @@ -1190,10 +1191,10 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid
/// </summary>
/// <param name="it">The iterator.</param>
/// <param name="rd">The score accumulator.</param>
private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd)
/// <param name="residual">The residual reused across the macroblocks of one pass.</param>
private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual residual)
{
int x, y, ch;
Vp8Residual residual = new();
bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16;

it.NzToBytes();
Expand Down
20 changes: 16 additions & 4 deletions src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

namespace SixLabors.ImageSharp.Formats.Webp.Lossy;

internal sealed class Vp8Histogram
/// <summary>
/// Summarizes the coefficient distribution of one prediction candidate during macroblock analysis.
/// This is a value type so that mode analysis, which evaluates several candidates per macroblock,
/// does not allocate. Create instances with <c>new()</c> rather than <see langword="default"/>:
/// the constructor sets the last-non-zero index to 1, which the alpha computation of an
/// accumulated histogram expects.
/// </summary>
internal struct Vp8Histogram
{
/// <summary>
/// Size of histogram used by CollectHistogram.
Expand All @@ -20,15 +27,15 @@ internal sealed class Vp8Histogram
private int lastNonZero;

/// <summary>
/// Initializes a new instance of the <see cref="Vp8Histogram" /> class.
/// Initializes a new instance of the <see cref="Vp8Histogram" /> struct.
/// </summary>
public Vp8Histogram()
{
this.maxValue = 0;
this.lastNonZero = 1;
}

public int GetAlpha()
public readonly int GetAlpha()
{
// 'alpha' will later be clipped to [0..MAX_ALPHA] range, clamping outer
// values which happen to be mostly noise. This leaves the maximum precision
Expand Down Expand Up @@ -87,7 +94,12 @@ public void CollectHistogram(Span<byte> reference, Span<byte> pred, int startBlo
this.SetHistogramData(distribution);
}

public void Merge(Vp8Histogram other)
/// <summary>
/// Raises the maximum value and the last-non-zero index of <paramref name="other"/> to those of this
/// histogram, so that <paramref name="other"/> accumulates the best candidate of every block.
/// </summary>
/// <param name="other">The accumulated histogram to merge into.</param>
public readonly void Merge(ref Vp8Histogram other)
{
if (this.maxValue > other.maxValue)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public void Merge_Works(byte[] reference, byte[] pred)
Vp8Histogram histogram1 = new();
histogram1.CollectHistogram(reference, pred, 0, 1);
Vp8Histogram histogram2 = new();
histogram1.Merge(histogram2);
histogram1.Merge(ref histogram2);

// act
int alpha = histogram2.GetAlpha();
Expand Down
Loading