Browse Source

Stream AV1 tile payloads without copies

pull/2633/head
James Jackson-South 1 month ago
parent
commit
e7ef639098
  1. 3
      HEIF_IMPLEMENTATION_PLAN.md
  2. 67
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs
  3. 32
      tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

3
HEIF_IMPLEMENTATION_PLAN.md

@ -806,7 +806,7 @@ Encoder data-flow contract:
2. Convert each generic `ImageFrame<TPixel>` once through `PixelOperations<TPixel>` and the SIMD-first HEIF planar converter into native 8, 10, or 12-bit planes. Alpha is encoded as an auxiliary image when requested by the resolved output contract; it is not discarded through a source scan. 2. Convert each generic `ImageFrame<TPixel>` once through `PixelOperations<TPixel>` and the SIMD-first HEIF planar converter into native 8, 10, or 12-bit planes. Alpha is encoded as an auxiliary image when requested by the resolved output contract; it is not discarded through a source scan.
3. Reuse allocator-owned plane, row, block, transform, quantization, entropy, and reconstruction workspaces for the complete frame. No active path may allocate per row, block, transform, scanline, or SIMD tail. 3. Reuse allocator-owned plane, row, block, transform, quantization, entropy, and reconstruction workspaces for the complete frame. No active path may allocate per row, block, transform, scanline, or SIMD tail.
4. Analyze and encode tiles directly from those planes, retaining reconstructed reference frames only for the bounded sequence lifetime. 4. Analyze and encode tiles directly from those planes, retaining reconstructed reference frames only for the bounded sequence lifetime.
5. Stream OBUs and container extents through allocator-backed chunked storage. Every ownership transfer is explicit, every owner is disposed exactly once, and no `ToArray` or file-sized copy crosses a layer boundary. 5. Build OBU headers in bounded allocator-backed scratch and stream entropy-coded tile owners and container extents directly. Every ownership transfer is explicit, every owner is disposed exactly once, and no `ToArray` or file-sized copy crosses a layer boundary.
6. Iterate image frames using ImageSharp frame metadata and format-connecting metadata. Root-frame-only behavior is permitted only for an explicitly static output contract. 6. Iterate image frames using ImageSharp frame metadata and format-connecting metadata. Root-frame-only behavior is permitted only for an explicitly static output contract.
Encoder verification contract: Encoder verification contract:
@ -844,6 +844,7 @@ Encoder verification contract:
- [x] The exact net11 Release rebuild completed at the established 1,005-warning repository baseline with zero errors. The complete HEIF/AV1 namespace passes 8,838 of 8,838 direct VSTest cases with zero failures or skips. Roslynk reports zero compiler errors and no diagnostics in the five changed C# files; `git diff --check` passes and `.gitattributes` is unchanged. - [x] The exact net11 Release rebuild completed at the established 1,005-warning repository baseline with zero errors. The complete HEIF/AV1 namespace passes 8,838 of 8,838 direct VSTest cases with zero failures or skips. Roslynk reports zero compiler errors and no diagnostics in the five changed C# files; `git diff --check` passes and `.gitattributes` is unchanged.
- [~] The same internal frame operation now produces 4:2:0, 4:2:2, and 4:4:4 payloads at 8, 10, and 12 bits. Twenty-one color cases cover constant and spatially varying input at aligned dimensions plus odd 13x11 visible dimensions for every chroma geometry. The production decoder consumes every payload, the decoded output retains non-neutral chroma, and current-main `aomdec` accepts all 29 monochrome and color outputs. The odd-dimension decoded-frame MD5 values are `d025030a44750e703bcca9a6d3e131e8` (4:2:0), `75936a76104ec678c7a1f10b4c560172` (4:2:2), and `044026b1518aa94f3c2db82c7247c0d6` (4:4:4). This proves legal current-libaom payload syntax across native plane geometries; it does not yet prove target quality or native-plane equality with an independently encoded reference. - [~] The same internal frame operation now produces 4:2:0, 4:2:2, and 4:4:4 payloads at 8, 10, and 12 bits. Twenty-one color cases cover constant and spatially varying input at aligned dimensions plus odd 13x11 visible dimensions for every chroma geometry. The production decoder consumes every payload, the decoded output retains non-neutral chroma, and current-main `aomdec` accepts all 29 monochrome and color outputs. The odd-dimension decoded-frame MD5 values are `d025030a44750e703bcca9a6d3e131e8` (4:2:0), `75936a76104ec678c7a1f10b4c560172` (4:2:2), and `044026b1518aa94f3c2db82c7247c0d6` (4:4:4). This proves legal current-libaom payload syntax across native plane geometries; it does not yet prove target quality or native-plane equality with an independently encoded reference.
- [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips. - [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips.
- [x] Combined-frame OBU output now counts the byte-aligned frame and tile-group headers, non-final tile-size fields, and owned tile payloads before emitting the OBU size. It retains only the small allocator-owned header scratch and writes each entropy-coded tile span directly from its detached owner, removing the second file-sized allocator rent and complete-payload copy. A 64 KiB regression proves exactly one sub-payload-sized byte rent with a balanced return and verifies the exact streamed tile tail; the existing two-tile round trip proves size-prefix and ordering parity. The focused writer and production-frame set passes 32 of 32 direct net11 VSTest cases, current-main `aomdec` accepts all 29 generated native-format payloads, and the complete HEIF/AV1 namespace passes 8,860 of 8,860 cases with zero failures or skips.
### 7. Write complete AVIF output ### 7. Write complete AVIF output

67
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -26,8 +27,8 @@ internal class ObuWriter
Justification = "Preserves the existing writer instance contract.")] Justification = "Preserves the existing writer instance contract.")]
public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter) public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter)
{ {
// The allocation expands when necessary; this initial size avoids repeated growth for // The reusable scratch only contains headers. Entropy-coded tiles remain in their owning
// the small headers and tiles produced by the current still-image encoder. // buffers and are streamed directly so the complete compressed frame is never duplicated.
int initialBufferSize = 2000; int initialBufferSize = 2000;
using AutoExpandingMemory<byte> buffer = new(configuration, initialBufferSize); using AutoExpandingMemory<byte> buffer = new(configuration, initialBufferSize);
Av1BitStreamWriter writer = new(buffer); Av1BitStreamWriter writer = new(buffer);
@ -44,14 +45,34 @@ internal class ObuWriter
if (frameHeader != null && sequenceHeader != null) if (frameHeader != null && sequenceHeader != null)
{ {
WriteFrameHeader(ref writer, sequenceHeader, frameHeader); WriteFrameHeader(ref writer, sequenceHeader, frameHeader);
if (frameHeader.TilesInfo != null) ObuTileGroupHeader tileInfo = frameHeader.TilesInfo;
if (tileInfo != null)
{ {
WriteTileGroup(ref writer, frameHeader.TilesInfo, tileWriter); WriteTileGroupHeader(ref writer, tileInfo);
} }
int bytesWritten = (writer.BitPosition + 7) >> 3; int frameHeaderBytes = (writer.BitPosition + 7) >> 3;
writer.Flush(); writer.Flush();
WriteObuHeaderAndSize(stream, ObuType.Frame, buffer.GetSpan(bytesWritten));
uint framePayloadSize = (uint)frameHeaderBytes;
if (tileInfo != null)
{
int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount;
framePayloadSize += (uint)((tileCount - 1) * tileInfo.TileSizeBytes);
for (int tileNum = 0; tileNum < tileCount; tileNum++)
{
framePayloadSize += (uint)tileWriter.GetTileData(tileNum).Length;
}
}
WriteObuHeaderAndSize(stream, ObuType.Frame, framePayloadSize);
stream.Write(buffer.GetSpan(frameHeaderBytes));
if (tileInfo != null)
{
WriteTileData(stream, tileInfo, tileWriter);
}
} }
} }
@ -74,14 +95,25 @@ internal class ObuWriter
/// <param name="type">The OBU payload type.</param> /// <param name="type">The OBU payload type.</param>
/// <param name="payload">The complete OBU payload.</param> /// <param name="payload">The complete OBU payload.</param>
private static void WriteObuHeaderAndSize(Stream stream, ObuType type, ReadOnlySpan<byte> payload) private static void WriteObuHeaderAndSize(Stream stream, ObuType type, ReadOnlySpan<byte> payload)
{
WriteObuHeaderAndSize(stream, type, (uint)payload.Length);
stream.Write(payload);
}
/// <summary>
/// Writes a byte-aligned OBU header and its little-endian base-128 payload size.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="type">The OBU payload type.</param>
/// <param name="payloadSize">The number of payload bytes that follow the header.</param>
private static void WriteObuHeaderAndSize(Stream stream, ObuType type, uint payloadSize)
{ {
stream.WriteByte(WriteObuHeader(type)); stream.WriteByte(WriteObuHeader(type));
// A 32-bit OBU payload length requires at most five base-128 bytes. // A 32-bit OBU payload length requires at most five base-128 bytes.
Span<byte> lengthBytes = stackalloc byte[5]; Span<byte> lengthBytes = stackalloc byte[5];
int lengthLength = Av1BitStreamWriter.GetLittleEndianBytes128((uint)payload.Length, lengthBytes); int lengthLength = Av1BitStreamWriter.GetLittleEndianBytes128(payloadSize, lengthBytes);
stream.Write(lengthBytes, 0, lengthLength); stream.Write(lengthBytes, 0, lengthLength);
stream.Write(payload);
} }
/// <summary> /// <summary>
@ -520,12 +552,11 @@ internal class ObuWriter
} }
/// <summary> /// <summary>
/// Writes a tile-group header and all tile payloads for a combined frame OBU. /// Writes the byte-aligned tile-group header for a combined frame OBU.
/// </summary> /// </summary>
/// <param name="writer">The bit writer receiving the tile group.</param> /// <param name="writer">The bit writer receiving the tile group.</param>
/// <param name="tileInfo">The frame tile layout.</param> /// <param name="tileInfo">The frame tile layout.</param>
/// <param name="tileWriter">The writer that produces each entropy-coded tile payload.</param> private static void WriteTileGroupHeader(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo)
private static void WriteTileGroup(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter)
{ {
int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount;
@ -541,28 +572,30 @@ internal class ObuWriter
} }
AlignToByteBoundary(ref writer); AlignToByteBoundary(ref writer);
WriteTileData(ref writer, tileInfo, tileWriter);
} }
/// <summary> /// <summary>
/// Writes the size-prefixed tile payloads in raster order. /// Writes the size-prefixed tile payloads in raster order.
/// </summary> /// </summary>
/// <param name="writer">The byte-aligned bit writer receiving tile data.</param> /// <param name="stream">The destination stream receiving tile data.</param>
/// <param name="tileInfo">The frame tile layout and tile-size field width.</param> /// <param name="tileInfo">The frame tile layout and tile-size field width.</param>
/// <param name="tileWriter">The writer that produces each tile payload.</param> /// <param name="tileWriter">The writer that produces each tile payload.</param>
private static void WriteTileData(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter) private static void WriteTileData(Stream stream, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter)
{ {
int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount;
Span<byte> tileSizeBuffer = stackalloc byte[sizeof(uint)];
for (int tileNum = 0; tileNum < tileCount; tileNum++) for (int tileNum = 0; tileNum < tileCount; tileNum++)
{ {
ReadOnlySpan<byte> tileData = tileWriter.GetTileData(tileNum); ReadOnlySpan<byte> tileData = tileWriter.GetTileData(tileNum);
if (tileNum != tileCount - 1 && tileCount > 1) if (tileNum != tileCount - 1 && tileCount > 1)
{ {
writer.WriteLittleEndian((uint)tileData.Length - 1U, tileInfo.TileSizeBytes); // AV1 stores each non-final tile size minus one with the least-significant byte first.
BinaryPrimitives.WriteUInt32LittleEndian(tileSizeBuffer, (uint)tileData.Length - 1U);
stream.Write(tileSizeBuffer[..tileInfo.TileSizeBytes]);
} }
writer.WriteBlob(tileData); stream.Write(tileData);
} }
} }

32
tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
@ -704,6 +705,37 @@ public class ObuFrameHeaderTests
Assert.Equal(bitStream.Length * 8, reader.BitPosition); Assert.Equal(bitStream.Length * 8, reader.BitPosition);
} }
/// <summary>
/// Verifies that the OBU writer streams an encoded tile from its owning buffer without renting a second payload-sized buffer.
/// </summary>
[Fact]
public void WriteFrameStreamsTilePayloadWithoutRentingPayloadCopy()
{
const int TilePayloadLength = 64 * 1024;
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = new();
configuration.MemoryAllocator = allocator;
ObuSequenceHeader sequenceHeader = GetDefaultSequenceHeader();
ObuFrameHeader frameHeader = GetKeyFrameHeader();
byte[] tileData = new byte[TilePayloadLength];
tileData.AsSpan().Fill(0x80);
Av1TileDecoderStub tileStub = new();
tileStub.ReadTile(tileData, 0);
using MemoryStream stream = new();
ObuWriter writer = new();
writer.WriteAll(configuration, stream, sequenceHeader, frameHeader, tileStub);
TestMemoryAllocator.AllocationRequest headerScratch = Assert.Single(allocator.AllocationLog);
Assert.Equal(typeof(byte), headerScratch.ElementType);
Assert.InRange(headerScratch.Length, 1, TilePayloadLength - 1);
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(headerScratch.AllocationId, returned.AllocationId);
Assert.True(stream.GetBuffer().AsSpan((int)stream.Length - TilePayloadLength, TilePayloadLength).SequenceEqual(tileData));
}
/// <summary> /// <summary>
/// Verifies non-uniform tile boundaries use the next stored boundary and retain the clipped final mode-info edge. /// Verifies non-uniform tile boundaries use the next stored boundary and retain the clipped final mode-info edge.
/// </summary> /// </summary>

Loading…
Cancel
Save