mirror of https://github.com/SixLabors/ImageSharp
5 changed files with 688 additions and 1 deletions
@ -0,0 +1,301 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Runtime.Intrinsics; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies the reversible four-by-four inverse Walsh-Hadamard transform used by lossless AV1 segments.
|
||||
|
/// </summary>
|
||||
|
internal static class Av1InverseWalshHadamardTransformer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The coefficient normalization shift applied before the first transform dimension.
|
||||
|
/// </summary>
|
||||
|
private const int UnitQuantizationShift = 2; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reconstructs a lossless transform block into eight-bit sample storage.
|
||||
|
/// </summary>
|
||||
|
/// <param name="coefficients">The sixteen dequantized coefficients in raster order.</param>
|
||||
|
/// <param name="readBuffer">The predicted samples read by reconstruction.</param>
|
||||
|
/// <param name="readStride">The number of read samples between rows.</param>
|
||||
|
/// <param name="writeBuffer">The destination reconstructed samples.</param>
|
||||
|
/// <param name="writeStride">The number of destination samples between rows.</param>
|
||||
|
/// <param name="coefficientCount">The decoded coefficient end position.</param>
|
||||
|
/// <param name="workspace">The reusable transform workspace.</param>
|
||||
|
public static void TransformAdd( |
||||
|
ReadOnlySpan<int> coefficients, |
||||
|
Span<byte> readBuffer, |
||||
|
int readStride, |
||||
|
Span<byte> writeBuffer, |
||||
|
int writeStride, |
||||
|
int coefficientCount, |
||||
|
Span<int> workspace) |
||||
|
=> TransformAdd<byte, Av1ByteInverseTransformOutputOperator>( |
||||
|
coefficients, |
||||
|
readBuffer, |
||||
|
readStride, |
||||
|
writeBuffer, |
||||
|
writeStride, |
||||
|
coefficientCount, |
||||
|
workspace, |
||||
|
8); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reconstructs a lossless transform block into high-bit-depth sample storage.
|
||||
|
/// </summary>
|
||||
|
/// <param name="coefficients">The sixteen dequantized coefficients in raster order.</param>
|
||||
|
/// <param name="readBuffer">The predicted samples read by reconstruction.</param>
|
||||
|
/// <param name="readStride">The number of read samples between rows.</param>
|
||||
|
/// <param name="writeBuffer">The destination reconstructed samples.</param>
|
||||
|
/// <param name="writeStride">The number of destination samples between rows.</param>
|
||||
|
/// <param name="coefficientCount">The decoded coefficient end position.</param>
|
||||
|
/// <param name="workspace">The reusable transform workspace.</param>
|
||||
|
/// <param name="bitDepth">The coded sample bit depth.</param>
|
||||
|
public static void TransformAdd( |
||||
|
ReadOnlySpan<int> coefficients, |
||||
|
Span<short> readBuffer, |
||||
|
int readStride, |
||||
|
Span<short> writeBuffer, |
||||
|
int writeStride, |
||||
|
int coefficientCount, |
||||
|
Span<int> workspace, |
||||
|
int bitDepth) |
||||
|
=> TransformAdd<short, Av1HighBitDepthInverseTransformOutputOperator>( |
||||
|
coefficients, |
||||
|
readBuffer, |
||||
|
readStride, |
||||
|
writeBuffer, |
||||
|
writeStride, |
||||
|
coefficientCount, |
||||
|
workspace, |
||||
|
bitDepth); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Selects the packed or scalar four-by-four reconstruction path.
|
||||
|
/// </summary>
|
||||
|
private static void TransformAdd<TSample, TOutputOperator>( |
||||
|
ReadOnlySpan<int> coefficients, |
||||
|
Span<TSample> readBuffer, |
||||
|
int readStride, |
||||
|
Span<TSample> writeBuffer, |
||||
|
int writeStride, |
||||
|
int coefficientCount, |
||||
|
Span<int> workspace, |
||||
|
int bitDepth) |
||||
|
where TSample : unmanaged |
||||
|
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample> |
||||
|
{ |
||||
|
if (Vector128.IsHardwareAccelerated) |
||||
|
{ |
||||
|
TransformVector<TSample, TOutputOperator>(coefficients, readBuffer, readStride, writeBuffer, writeStride, coefficientCount, bitDepth); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
TransformScalar<TSample, TOutputOperator>(coefficients, readBuffer, readStride, writeBuffer, writeStride, coefficientCount, workspace, bitDepth); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies both reversible transform dimensions to four packed coefficient rows.
|
||||
|
/// </summary>
|
||||
|
private static void TransformVector<TSample, TOutputOperator>( |
||||
|
ReadOnlySpan<int> coefficients, |
||||
|
Span<TSample> readBuffer, |
||||
|
int readStride, |
||||
|
Span<TSample> writeBuffer, |
||||
|
int writeStride, |
||||
|
int coefficientCount, |
||||
|
int bitDepth) |
||||
|
where TSample : unmanaged |
||||
|
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample> |
||||
|
{ |
||||
|
ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); |
||||
|
Vector128<int> row0; |
||||
|
Vector128<int> row1; |
||||
|
Vector128<int> row2; |
||||
|
Vector128<int> row3; |
||||
|
|
||||
|
if (coefficientCount == 1) |
||||
|
{ |
||||
|
// The DC-only form bypasses fifteen known-zero coefficients and both full butterflies. These divisions
|
||||
|
// deliberately use arithmetic shifts because negative coefficients must round toward negative infinity.
|
||||
|
int first = Unsafe.Add(ref coefficientBase, 0) >> UnitQuantizationShift; |
||||
|
int half = first >> 1; |
||||
|
Vector128<int> intermediate = Vector128.Create(first - half, half, half, half); |
||||
|
|
||||
|
row1 = intermediate >> 1; |
||||
|
row0 = intermediate - row1; |
||||
|
row2 = row1; |
||||
|
row3 = row1; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
row0 = Vector128.LoadUnsafe(ref coefficientBase) >> UnitQuantizationShift; |
||||
|
row1 = Vector128.LoadUnsafe(ref coefficientBase, 4) >> UnitQuantizationShift; |
||||
|
row2 = Vector128.LoadUnsafe(ref coefficientBase, 8) >> UnitQuantizationShift; |
||||
|
row3 = Vector128.LoadUnsafe(ref coefficientBase, 12) >> UnitQuantizationShift; |
||||
|
|
||||
|
Transform(ref row0, ref row1, ref row2, ref row3); |
||||
|
|
||||
|
// The first pass operates down four columns in parallel. Transposition turns those intermediate columns
|
||||
|
// into packed rows so the same reversible butterfly implements the second dimension without scratch.
|
||||
|
Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); |
||||
|
Transform(ref row0, ref row1, ref row2, ref row3); |
||||
|
} |
||||
|
|
||||
|
AddRows<TSample, TOutputOperator>(readBuffer, readStride, writeBuffer, writeStride, row0, row1, row2, row3, bitDepth); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies both reversible transform dimensions without hardware intrinsics.
|
||||
|
/// </summary>
|
||||
|
private static void TransformScalar<TSample, TOutputOperator>( |
||||
|
ReadOnlySpan<int> coefficients, |
||||
|
Span<TSample> readBuffer, |
||||
|
int readStride, |
||||
|
Span<TSample> writeBuffer, |
||||
|
int writeStride, |
||||
|
int coefficientCount, |
||||
|
Span<int> workspace, |
||||
|
int bitDepth) |
||||
|
where TSample : unmanaged |
||||
|
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample> |
||||
|
{ |
||||
|
ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer); |
||||
|
ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer); |
||||
|
|
||||
|
if (coefficientCount == 1) |
||||
|
{ |
||||
|
int first = coefficients[0] >> UnitQuantizationShift; |
||||
|
int half = first >> 1; |
||||
|
int firstResidual = first - half; |
||||
|
|
||||
|
for (int column = 0; column < 4; column++) |
||||
|
{ |
||||
|
int intermediate = column == 0 ? firstResidual : half; |
||||
|
int repeatedResidual = intermediate >> 1; |
||||
|
int topResidual = intermediate - repeatedResidual; |
||||
|
|
||||
|
Unsafe.Add(ref writeBase, column) = TOutputOperator.Add(Unsafe.Add(ref readBase, column), topResidual, bitDepth); |
||||
|
|
||||
|
for (int row = 1; row < 4; row++) |
||||
|
{ |
||||
|
int readOffset = (row * readStride) + column; |
||||
|
int writeOffset = (row * writeStride) + column; |
||||
|
Unsafe.Add(ref writeBase, writeOffset) = TOutputOperator.Add(Unsafe.Add(ref readBase, readOffset), repeatedResidual, bitDepth); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); |
||||
|
ref int intermediateBase = ref MemoryMarshal.GetReference(workspace); |
||||
|
|
||||
|
// The scalar fallback retains the column-first traversal. The caller-owned transform workspace keeps the
|
||||
|
// complete first dimension without introducing per-block stack or managed allocations.
|
||||
|
for (int column = 0; column < 4; column++) |
||||
|
{ |
||||
|
int a = Unsafe.Add(ref coefficientBase, column) >> UnitQuantizationShift; |
||||
|
int c = Unsafe.Add(ref coefficientBase, 4 + column) >> UnitQuantizationShift; |
||||
|
int d = Unsafe.Add(ref coefficientBase, 8 + column) >> UnitQuantizationShift; |
||||
|
int b = Unsafe.Add(ref coefficientBase, 12 + column) >> UnitQuantizationShift; |
||||
|
|
||||
|
Transform(ref a, ref b, ref c, ref d); |
||||
|
Unsafe.Add(ref intermediateBase, column) = a; |
||||
|
Unsafe.Add(ref intermediateBase, 4 + column) = b; |
||||
|
Unsafe.Add(ref intermediateBase, 8 + column) = c; |
||||
|
Unsafe.Add(ref intermediateBase, 12 + column) = d; |
||||
|
} |
||||
|
|
||||
|
for (int column = 0; column < 4; column++) |
||||
|
{ |
||||
|
int intermediateOffset = column * 4; |
||||
|
int a = Unsafe.Add(ref intermediateBase, intermediateOffset); |
||||
|
int c = Unsafe.Add(ref intermediateBase, intermediateOffset + 1); |
||||
|
int d = Unsafe.Add(ref intermediateBase, intermediateOffset + 2); |
||||
|
int b = Unsafe.Add(ref intermediateBase, intermediateOffset + 3); |
||||
|
|
||||
|
Transform(ref a, ref b, ref c, ref d); |
||||
|
|
||||
|
Unsafe.Add(ref writeBase, column) = TOutputOperator.Add(Unsafe.Add(ref readBase, column), a, bitDepth); |
||||
|
Unsafe.Add(ref writeBase, writeStride + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, readStride + column), b, bitDepth); |
||||
|
Unsafe.Add(ref writeBase, (2 * writeStride) + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, (2 * readStride) + column), c, bitDepth); |
||||
|
Unsafe.Add(ref writeBase, (3 * writeStride) + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, (3 * readStride) + column), d, bitDepth); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies one packed four-point reversible Walsh-Hadamard dimension.
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static void Transform( |
||||
|
ref Vector128<int> row0, |
||||
|
ref Vector128<int> row1, |
||||
|
ref Vector128<int> row2, |
||||
|
ref Vector128<int> row3) |
||||
|
{ |
||||
|
Vector128<int> a = row0; |
||||
|
Vector128<int> c = row1; |
||||
|
Vector128<int> d = row2; |
||||
|
Vector128<int> b = row3; |
||||
|
|
||||
|
a += c; |
||||
|
d -= b; |
||||
|
Vector128<int> middle = (a - d) >> 1; |
||||
|
b = middle - b; |
||||
|
c = middle - c; |
||||
|
a -= b; |
||||
|
d += c; |
||||
|
|
||||
|
// The transform's arithmetic names the fourth input b and the second input c. Restore raster row order
|
||||
|
// explicitly so the transpose and packed output stages see a, b, c, d exactly as the reference does.
|
||||
|
row0 = a; |
||||
|
row1 = b; |
||||
|
row2 = c; |
||||
|
row3 = d; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies one scalar four-point reversible Walsh-Hadamard dimension.
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static void Transform(ref int a, ref int b, ref int c, ref int d) |
||||
|
{ |
||||
|
a += c; |
||||
|
d -= b; |
||||
|
int middle = (a - d) >> 1; |
||||
|
b = middle - b; |
||||
|
c = middle - c; |
||||
|
a -= b; |
||||
|
d += c; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds four packed residual rows to their prediction rows through the active sample operator.
|
||||
|
/// </summary>
|
||||
|
private static void AddRows<TSample, TOutputOperator>( |
||||
|
Span<TSample> readBuffer, |
||||
|
int readStride, |
||||
|
Span<TSample> writeBuffer, |
||||
|
int writeStride, |
||||
|
Vector128<int> row0, |
||||
|
Vector128<int> row1, |
||||
|
Vector128<int> row2, |
||||
|
Vector128<int> row3, |
||||
|
int bitDepth) |
||||
|
where TSample : unmanaged |
||||
|
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample> |
||||
|
{ |
||||
|
ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer); |
||||
|
ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer); |
||||
|
TOutputOperator.Add(ref readBase, ref writeBase, row0, bitDepth); |
||||
|
TOutputOperator.Add(ref Unsafe.Add(ref readBase, readStride), ref Unsafe.Add(ref writeBase, writeStride), row1, bitDepth); |
||||
|
TOutputOperator.Add(ref Unsafe.Add(ref readBase, 2 * readStride), ref Unsafe.Add(ref writeBase, 2 * writeStride), row2, bitDepth); |
||||
|
TOutputOperator.Add(ref Unsafe.Add(ref readBase, 3 * readStride), ref Unsafe.Add(ref writeBase, 3 * writeStride), row3, bitDepth); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,138 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using BenchmarkDotNet.Attributes; |
||||
|
using BenchmarkDotNet.Columns; |
||||
|
using BenchmarkDotNet.Configs; |
||||
|
using BenchmarkDotNet.Jobs; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures frame-wide AV1 lossless inverse-transform reconstruction.
|
||||
|
/// </summary>
|
||||
|
[Config(typeof(Configuration))] |
||||
|
[MemoryDiagnoser(displayGenColumns: false)] |
||||
|
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] |
||||
|
[CategoriesColumn] |
||||
|
public class Av1LosslessTransformBenchmarks |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The coded frame width in samples.
|
||||
|
/// </summary>
|
||||
|
private const int Width = 1920; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The coded frame height including the final padded coding-tree row for a 1080-line presentation.
|
||||
|
/// </summary>
|
||||
|
private const int Height = 1088; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The lossless AV1 transform side in samples.
|
||||
|
/// </summary>
|
||||
|
private const int TransformSize = 4; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The dense dequantized coefficient block used by every transform.
|
||||
|
/// </summary>
|
||||
|
private readonly int[] coefficients = |
||||
|
[ |
||||
|
320, -192, 64, -448, |
||||
|
128, 256, -320, 96, |
||||
|
-224, 160, 384, -128, |
||||
|
448, -64, -256, 192 |
||||
|
]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The caller-owned transform workspace reused across the frame.
|
||||
|
/// </summary>
|
||||
|
private readonly int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The frame-wide eight-bit reconstruction surface.
|
||||
|
/// </summary>
|
||||
|
private readonly byte[] destination8 = new byte[Width * Height]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The frame-wide twelve-bit reconstruction surface.
|
||||
|
/// </summary>
|
||||
|
private readonly short[] destination12 = new short[Width * Height]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures dense eight-bit lossless reconstruction across a padded 1920-by-1088 frame.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
|
||||
|
[Benchmark] |
||||
|
[BenchmarkCategory("8Bit")] |
||||
|
public byte Reconstruct8BitFrame() |
||||
|
{ |
||||
|
for (int row = 0; row < Height; row += TransformSize) |
||||
|
{ |
||||
|
for (int column = 0; column < Width; column += TransformSize) |
||||
|
{ |
||||
|
Av1InverseTransformer.Reconstruct8Bit( |
||||
|
this.coefficients, |
||||
|
this.destination8.AsSpan((row * Width) + column), |
||||
|
Width, |
||||
|
Av1TransformSize.Size4x4, |
||||
|
Av1TransformType.DctDct, |
||||
|
0, |
||||
|
this.coefficients.Length, |
||||
|
true, |
||||
|
this.workspace); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this.destination8[^1]; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures dense twelve-bit lossless reconstruction across a padded 1920-by-1088 frame.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
|
||||
|
[Benchmark] |
||||
|
[BenchmarkCategory("12Bit")] |
||||
|
public short Reconstruct12BitFrame() |
||||
|
{ |
||||
|
for (int row = 0; row < Height; row += TransformSize) |
||||
|
{ |
||||
|
for (int column = 0; column < Width; column += TransformSize) |
||||
|
{ |
||||
|
Av1InverseTransformer.ReconstructHighBitDepth( |
||||
|
this.coefficients, |
||||
|
this.destination12.AsSpan((row * Width) + column), |
||||
|
Width, |
||||
|
Av1TransformSize.Size4x4, |
||||
|
Av1TransformType.DctDct, |
||||
|
0, |
||||
|
this.coefficients.Length, |
||||
|
true, |
||||
|
Av1BitDepth.TwelveBit, |
||||
|
this.workspace); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this.destination12[^1]; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Configures production-process measurements for hardware and scalar reconstruction.
|
||||
|
/// </summary>
|
||||
|
public sealed class Configuration : ManualConfig |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Configuration"/> class.
|
||||
|
/// </summary>
|
||||
|
public Configuration() |
||||
|
{ |
||||
|
this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); |
||||
|
|
||||
|
this.AddJob( |
||||
|
Job.ShortRun |
||||
|
.WithId("Scalar") |
||||
|
.WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue