diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index ff0659553e..febe2f8b9c 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -826,7 +826,7 @@ Encoder verification contract: - [~] Finalized transform coefficients and packed EOB/type state now use raster-ordered, per-superblock plane segments matching current libaom's coefficient-pool geometry. One ImageSharp allocator owner replaces libaom's separate coefficient, EOB, and entropy-context allocations while preserving the full 1024 luma and 256-per-chroma 4x4 state capacity of a 128x128 4:2:0 superblock; the forward transform and mode-decision stages still need to populate this owner. - [~] Tile partition writing now follows current libaom's recursive `write_modes_sb` preorder traversal and `update_ext_partition_context` edge updates directly. The obsolete SVT-derived global geometry catalog and its unimplemented lookup are removed; transform geometry is derived in libaom's bounded 64x64 residual order, fixed intra transform-size symbols use the reference depth and neighbor contexts, frame-edge and segmentation syntax use mode-information units, and 128x128 CDEF units use libaom's 0-to-3 indexing and first-block strength ownership. Partition and mode analysis still need to populate these retained decisions; variable inter-transform syntax remains part of later inter-frame support. - [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions. -- [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform. The reference-type symbol encoder is passed normally through tile traversal, and the operation boundary owns the allocator-backed item payload stream for exactly one synchronous encode. Every remaining encoder fragment must be audited before it becomes active. +- [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform. Its range coder matches current libaom's 64-bit coding window, bulk big-endian byte flush, and backward carry propagation while using one byte of allocator scratch per estimated output byte instead of the former 16-bit pre-carry storage. The reference-type symbol encoder is passed normally through tile traversal, and the operation boundary owns the allocator-backed item payload stream for exactly one synchronous encode. Every remaining encoder fragment must be audited before it becomes active. - [~] The planar conversion, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Apply the same rule to every later hot-path family. - [~] Forward-quantizer FeatureTestRunner and zero-allocation tests compare every hardware tier with an independent scan-order scalar oracle shaped from current-main libaom. Both passed direct net11 VSTest in Release. - [~] The combined-frame writer now completes the byte-counted uncompressed frame header before starting the optional multi-tile tile-group flag, matching current libaom's separate frame-header and tile-group writers. A non-uniform two-tile round trip verifies the explicit boundaries, both tile payloads, and complete stream consumption through direct net11 VSTest in Release. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs index 0249ca7c2f..6053db40d4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Buffers.Binary; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; @@ -14,7 +15,7 @@ internal class Av1SymbolWriter : IDisposable /// /// The lower endpoint of the current coding interval. /// - private uint low; + private ulong low; /// /// The width of the current normalized coding interval. @@ -35,9 +36,9 @@ internal class Av1SymbolWriter : IDisposable private readonly Configuration configuration; /// - /// The pre-carry output values accumulated during renormalization. + /// The output bytes accumulated during renormalization. /// - private readonly AutoExpandingMemory memory; + private readonly AutoExpandingMemory memory; /// /// Indicates whether encoded symbols adapt their distributions. @@ -45,7 +46,7 @@ internal class Av1SymbolWriter : IDisposable private readonly bool updateCdf; /// - /// The next pre-carry output position. + /// The next output byte position. /// private int position; @@ -58,7 +59,7 @@ internal class Av1SymbolWriter : IDisposable public Av1SymbolWriter(Configuration configuration, int initialSize, bool updateCdf = true) { this.configuration = configuration; - this.memory = new AutoExpandingMemory(configuration, (initialSize + 1) >> 1); + this.memory = new AutoExpandingMemory(configuration, initialSize); this.updateCdf = updateCdf; } @@ -129,22 +130,29 @@ internal class Av1SymbolWriter : IDisposable /// An owner containing the shortest byte sequence that preserves every encoded symbol. public IMemoryOwner Exit() { - // Round the low endpoint into the current interval so the emitted prefix selects every symbol encoded so far + // Round the low endpoint into the current interval so the emitted prefix selects every symbol encoded so far, // regardless of the bits that follow it. - uint l = this.low; + ulong l = this.low; int c = this.cnt; int pos = this.position; int s = 10; - uint m = 0x3FFFU; - uint e = ((l + m) & ~m) | (m + 1); + ulong m = 0x3FFFU; + ulong e = ((l + m) & ~m) | (m + 1); s += c; - Span buffer = this.memory.GetSpan(this.position + ((s + 7) >> 3)); + int pendingByteCount = Math.Max((s + 7) >> 3, 0); + Span buffer = this.memory.GetSpan(pos + pendingByteCount); if (s > 0) { - uint n = (1U << (c + 16)) - 1; + ulong n = (1UL << (c + 16)) - 1; do { - buffer[pos] = (ushort)(e >> (c + 16)); + ushort value = (ushort)(e >> (c + 16)); + buffer[pos] = (byte)value; + if ((value & 0x100) != 0) + { + PropagateCarryBackward(buffer, pos - 1); + } + pos++; e &= n; s -= 8; @@ -154,20 +162,8 @@ internal class Av1SymbolWriter : IDisposable while (s > 0); } - c = Math.Max((s + 7) >> 3, 0); - IMemoryOwner output = this.configuration.MemoryAllocator.Allocate(pos + c); - - // Pre-carry values use 16-bit elements so a byte plus a propagated carry can coexist. Walking backwards folds - // each carry into the preceding byte without shifting the buffered sequence. - Span outputSlice = output.GetSpan()[(output.Length() - pos)..]; - c = 0; - while (pos > 0) - { - pos--; - c = buffer[pos] + c; - outputSlice[pos] = (byte)c; - c >>= 8; - } + IMemoryOwner output = this.configuration.MemoryAllocator.Allocate(pos); + buffer[..pos].CopyTo(output.GetSpan()[..pos]); return output; } @@ -179,7 +175,7 @@ internal class Av1SymbolWriter : IDisposable /// The probability that the value is true, scaled by 32768. private void EncodeBoolQ15(bool val, uint frequency) { - uint l; + ulong l; uint r; uint v; DebugGuard.MustBeGreaterThan(frequency, 0U, nameof(frequency)); @@ -227,7 +223,7 @@ internal class Av1SymbolWriter : IDisposable private void EncodeIntegerQ15(uint lowFrequency, uint highFrequency, int symbol, int numberOfSymbols) { const int totalShift = 7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift; - uint l = this.low; + ulong l = this.low; uint r = this.rng; DebugGuard.MustBeLessThanOrEqualTo(32768U, r, nameof(r)); DebugGuard.MustBeLessThanOrEqualTo(highFrequency, lowFrequency, nameof(highFrequency)); @@ -261,42 +257,61 @@ internal class Av1SymbolWriter : IDisposable /// /// The new value of . /// The new value of . - private void Normalize(uint low, uint rng) + private void Normalize(ulong low, uint rng) { - int d; - int c; - int s; - c = this.cnt; + int c = this.cnt; DebugGuard.MustBeLessThanOrEqualTo(rng, 65535U, nameof(rng)); - d = 15 - Av1Math.MostSignificantBit(rng); - s = c + d; + int d = 15 - Av1Math.MostSignificantBit(rng); + int s = c + d; - // The 32-bit low endpoint is flushed whenever a byte becomes available. Retaining pre-carry values as - // ushort elements defers carry propagation until Exit without requiring a separate wider coding window. - if (s >= 0) + // Keeping 16 bits free for the next symbol allows the 64-bit coding window to flush up to eight completed + // bytes together while preserving one carry bit. + if (s >= 40) { - uint m; - Span buffer = this.memory.GetSpan(this.position + 2); - - c += 16; - m = (1U << c) - 1; - if (s >= 8) + Span buffer = this.memory.GetSpan(this.position + sizeof(ulong)); + int readyByteCount = (s >> 3) + 1; + c += 24 - (readyByteCount << 3); + ulong output = low >> c; + low &= (1UL << c) - 1; + ulong carryMask = 1UL << (readyByteCount << 3); + bool hasCarry = (output & carryMask) != 0; + output &= carryMask - 1; + + // Writing one big-endian word avoids a byte-at-a-time hot loop. Only readyByteCount bytes become part + // of the logical output; the following bytes are overwritten by the next flush. + BinaryPrimitives.WriteUInt64BigEndian( + buffer.Slice(this.position, sizeof(ulong)), + output << ((sizeof(ulong) - readyByteCount) << 3)); + + if (hasCarry) { - buffer[this.position] = (ushort)(low >> c); - this.position++; - low &= m; - c -= 8; - m >>= 8; + PropagateCarryBackward(buffer, this.position - 1); } - buffer[this.position] = (ushort)(low >> c); - this.position++; + this.position += readyByteCount; s = c + d - 24; - low &= m; } this.low = low << d; this.rng = rng << d; this.cnt = s; } + + /// + /// Adds a carry to the completed output prefix. + /// + /// The accumulated output bytes. + /// The final completed byte. + private static void PropagateCarryBackward(Span buffer, int offset) + { + int carry; + do + { + int sum = buffer[offset] + 1; + buffer[offset] = (byte)sum; + carry = sum >> 8; + offset--; + } + while (carry != 0); + } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index fcc05f944f..eaf34fa525 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; @@ -17,6 +18,43 @@ public class Av1EntropyTests { private const int BaseQIndex = 23; + [Fact] + public void SymbolWriterMatchesCurrentLibaomCarryRegression() + { + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: false); + writer.WriteBoolean(false, 16_384); + writer.WriteBoolean(false, 16_384); + writer.WriteBoolean(true, 512); + writer.WriteBoolean(false, 8_192); + using IMemoryOwner encoded = writer.Exit(); + + Assert.Equal(2, encoded.Memory.Length); + Assert.Equal(63, encoded.Memory.Span[0]); + } + + [Fact] + public void SymbolWriterUsesOneByteOfScratchPerEstimatedOutputByte() + { + const int initialSize = 257; + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + TestMemoryAllocator.AllocationRequest allocation; + + using (Av1SymbolWriter writer = new(configuration, initialSize, updateCdf: false)) + { + writer.WriteLiteral(false); + allocation = Assert.Single(allocator.AllocationLog); + + Assert.Equal(typeof(byte), allocation.ElementType); + Assert.Equal(initialSize, allocation.Length); + } + + TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); + Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer); + } + [Fact] public void ReadRandomLiteral() {