diff --git a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs index 7548d5d7ea..e726b04b82 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -36,8 +36,12 @@ public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Se int tlambda = dqm.TLambda; Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); Span 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; @@ -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; @@ -107,7 +115,11 @@ public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Seg Span bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); Span 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) { @@ -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 tmpLevels = stackalloc short[16]; do { @@ -220,9 +234,12 @@ public static void PickBestUv(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Segment Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.UOffEnc); Span dst0 = it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc); Span 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; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index de996fc1c8..641e48feff 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -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; @@ -234,6 +238,30 @@ public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] /// public int[] Scratch3 { get; } + /// + /// Gets or sets the first reusable rate-distortion score accumulator. + /// uses the three accumulators as scratch state while it evaluates the + /// prediction modes of a macroblock, so mode evaluation does not allocate per macroblock. + /// 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. + /// + public Vp8ModeScore RdScratch { get; set; } + + /// + /// Gets the second reusable rate-distortion score accumulator. + /// + public Vp8ModeScore RdScratch2 { get; } + + /// + /// Gets the third reusable rate-distortion score accumulator. + /// + public Vp8ModeScore RdScratch3 { get; } + + /// + /// Gets the reusable residual used to cost the candidate modes of a macroblock. + /// + public Vp8Residual ResidualScratch { get; } + public Vp8MacroBlockInfo CurrentMacroBlockInfo => this.Mb[this.currentMbIdx]; private Vp8MacroBlockInfo[] Mb { get; } @@ -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 histos = stackalloc Vp8Histogram[2]; this.StartI4(); do { int mode; int bestModeAlpha = DefaultAlpha; - Vp8Histogram[] histos = new Vp8Histogram[2]; Span 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(); @@ -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. diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 85739a3e20..6b2f46fcd7 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -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(); @@ -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; @@ -1190,10 +1191,10 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid /// /// The iterator. /// The score accumulator. - private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) + /// The residual reused across the macroblocks of one pass. + 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(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs index 2ace43d2d5..a8d6d8b6e8 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs @@ -8,7 +8,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy; -internal sealed class Vp8Histogram +/// +/// 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 new() rather than : +/// the constructor sets the last-non-zero index to 1, which the alpha computation of an +/// accumulated histogram expects. +/// +internal struct Vp8Histogram { /// /// Size of histogram used by CollectHistogram. @@ -20,7 +27,7 @@ internal sealed class Vp8Histogram private int lastNonZero; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// public Vp8Histogram() { @@ -28,7 +35,7 @@ public Vp8Histogram() 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 @@ -87,7 +94,12 @@ public void CollectHistogram(Span reference, Span pred, int startBlo this.SetHistogramData(distribution); } - public void Merge(Vp8Histogram other) + /// + /// Raises the maximum value and the last-non-zero index of to those of this + /// histogram, so that accumulates the best candidate of every block. + /// + /// The accumulated histogram to merge into. + public readonly void Merge(ref Vp8Histogram other) { if (this.maxValue > other.maxValue) { diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs index fa240c55b9..7838405b7c 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs @@ -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();