diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index e9f50fb493..aff768543c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -48,8 +48,6 @@ internal class Vp8BitWriter : BitWriterBase private uint pos; - private readonly int maxPos; - /// /// Initializes a new instance of the class. /// @@ -63,7 +61,6 @@ public Vp8BitWriter(int expectedSize, Vp8Encoder enc) this.run = 0; this.nbBits = -8; this.pos = 0; - this.maxPos = 0; this.enc = enc; } @@ -185,12 +182,15 @@ public int PutCoeffs(int ctx, Vp8Residual residual) public override void BitWriterResize(int extraSize) { long neededSize = this.pos + extraSize; - if (neededSize <= this.maxPos) + + // Compare against the current capacity and grow from it, so the buffer only reallocates + // when it is actually full and grows geometrically (libwebp tracks this as max_pos). + if (neededSize <= this.Buffer.Length) { return; } - this.ResizeBuffer(this.maxPos, (int)neededSize); + this.ResizeBuffer(this.Buffer.Length, (int)neededSize); } /// diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8BitWriterTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8BitWriterTests.cs new file mode 100644 index 0000000000..4bd5eeae0a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8BitWriterTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Lossy; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp; + +[Trait("Format", "Webp")] +public class Vp8BitWriterTests +{ + [Fact] + public void BitWriterResize_WithinCapacity_DoesNotReallocate() + { + using Vp8Encoder encoder = CreateEncoder(); + Vp8BitWriter writer = new(4096, encoder); + byte[] buffer = writer.Buffer; + + writer.BitWriterResize(1); + writer.BitWriterResize(4096); + + Assert.Same(buffer, writer.Buffer); + Assert.Equal(4096, writer.Buffer.Length); + } + + [Fact] + public void BitWriterResize_GrowsBufferGeometrically() + { + const int targetBytes = 256 * 1024; + using Vp8Encoder encoder = CreateEncoder(); + Vp8BitWriter writer = new(1024, encoder); + Random random = new(42); + byte[] buffer = writer.Buffer; + int reallocations = 0; + + // Emit pseudo-random modes until the partition holds at least targetBytes, counting how often + // the backing buffer is replaced. + while (writer.NumBytes < targetBytes) + { + writer.PutUvMode(random.Next(4)); + if (!ReferenceEquals(buffer, writer.Buffer)) + { + reallocations++; + buffer = writer.Buffer; + } + } + + writer.Finish(); + + Assert.True(writer.Buffer.Length >= writer.NumBytes); + + // Growing by 1.5x (rounded up to the next KiB) from 1 KiB needs 12 reallocations to hold 256 KiB. + // Growing in 1 KiB steps (one reallocation per KiB written) would need 256. + Assert.InRange(reallocations, 1, 16); + } + + private static Vp8Encoder CreateEncoder() + => new( + Configuration.Default.MemoryAllocator, + Configuration.Default, + 16, + 16, + 75, + false, + WebpEncodingMethod.Default, + 1, + 60, + 50, + false); +}