Browse Source

Implement SIMD-first AV1 chroma-from-luma prediction

pull/2633/head
James Jackson-South 2 months ago
parent
commit
c94cababb1
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs
  3. 68
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  4. 429
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs
  5. 86
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs
  6. 231
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaPredictor.cs
  7. 2
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.Operator.cs
  8. 2
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuv420ToRgb8Converter.Operator.cs
  9. 136
      tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ChromaFromLumaBenchmarks.cs
  10. 377
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

@ -265,7 +265,7 @@ This snapshot pins or classifies the available references and failures; it does
| `HeifTransferFunctions` | ITU-T H.273 (V4) Table 3 transfer characteristics 1-18 | Official ITU-T H.273 (V4) (07/2024); libavif `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826` | Apply every HEVC/AV1-signallable transfer function required by constant-luminance, ICtCp, and IPT-C2 conversion. Retain the H.273 normalized PQ and HLG definitions; do not import libavif's display-oriented 203-nit scaling or HLG OOTF into codec sample interpretation. Use libavif's midpoint convention only for the non-bijective zero code of the two logarithmic curves. |
| `ObuReader.ReadSequenceHeader`, `ReadUncompressedFrameHeader`, decoder-model parsing, and operating-parameter consumption | AV1 sections 5.5.2 through 5.5.4 sequence timing and decoder-model syntax, section 5.9.2 uncompressed frame-header syntax, and section 5.9.31 temporal-point syntax | libaom `av1/decoder/decodeframe.c` functions `av1_read_decoder_model_info`, `av1_read_op_parameters_info`, `read_temporal_point_info`, and `read_uncompressed_header`, plus `common/av1_config.c`, at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Read the normative 32-bit decoding-tick field; consume operating-point buffer delays and the layer-applicable frame removal times needed to keep a non-reduced still-image sequence aligned; and read presentation time only under the normative decoder-model condition. Retain no scheduling or playback behavior from those values and introduce no ISO BMFF timing, track, sample-table, or sequence surface. |
| `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit packed-pixel conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use ImageSharp's existing `Rgb48` pixel-operation paths in both directions so packed-pixel staging does not reduce high-bit-depth samples to eight bits. |
| `Av1PredictionDecoder`, `Av1HighBitDepthPredictor`, `Av1ChromaFromLumaContext`, `Av1PartitionInfo`, and the DC, directional, Paeth, smooth, filter-intra, and chroma-from-luma predictor operators | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c`, `av1/common/reconintra.c`, `av1/common/av1_common_int.h`, `av1/common/blockd.h`, `av1/common/cfl.c`, and `av1/common/cfl.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design the shared predictor contract, sample layout, reference preparation, and scratch ownership for widest-to-narrowest SIMD traversal, then provide the equivalent scalar fallback through that contract. Preserve luma/chroma mode-neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, high-bit-depth clipping, chroma-from-luma storage/subsampling, and chroma-from-luma row strides. The WIP rectangular byte-pipeline smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. |
| `Av1PredictionDecoder`, `Av1IntraPredictor`, `Av1ChromaFromLumaContext`, `Av1ChromaFromLumaPredictor`, `Av1PartitionInfo`, and the DC, directional, Paeth, smooth, filter-intra, and chroma-from-luma predictor operators | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c`, `av1/common/reconintra.c`, `av1/common/av1_common_int.h`, `av1/common/blockd.h`, `av1/common/cfl.c`, and `av1/common/cfl.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design the shared predictor contract, sample layout, reference preparation, and scratch ownership for widest-to-narrowest SIMD traversal, then provide the equivalent scalar fallback through that contract. Preserve luma/chroma mode-neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, high-bit-depth clipping, chroma-from-luma storage/subsampling, and chroma-from-luma row strides. The WIP rectangular byte-pipeline smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. |
| `Av1TileReader` palette mode/color-map parsing, `Av1SymbolDecoder` palette distributions, `Av1BlockModeInfo` palette state, and `Av1PredictionDecoder` palette reconstruction | AV1 sections 5.11.46, 5.11.49, and 7.11.2 palette prediction | libaom `av1/decoder/decodemv.c`, `av1/decoder/detokenize.c`, `av1/decoder/decoder.h`, `av1/common/pred_common.c`, `av1/common/pred_common.h`, and `av1/common/entropymode.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Reuse the existing tile range decoder and frame-owned mode map while porting the normative palette cache merge, high-bit-depth color deltas, tile-adaptive mode/size/index distributions, diagonal color-map traversal, edge padding, and direct palette-sample reconstruction. This is AV1 still-image compression syntax and does not add retained video reference state or any ISO BMFF surface. |
| `Av1LoopFilterKernels`, `Av1LoopFilterContext`, and `Av1LoopFilterDecoder` | AV1 section 7.14 deblocking loop filter | libaom `aom_dsp/loopfilter.c` and `av1/common/av1_loopfilter.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design the low/high-bit-depth edge layout and 4-, 6-, 8-, and 14-tap kernels for SIMD-first traversal, with sharpness thresholds, still-frame intra filter-level derivation, transform-edge selection, and plane traversal shared by the exact scalar fallback. Implement and verify both through the same contract before enabling the stage. This is normative AV1 image reconstruction and adds neither generic ISO BMFF models nor retained video reference state. |
| `Av1CdefDecoder`, `Av1CdefKernels`, and CDEF-unit strength storage | AV1 sections 7.15.2 through 7.15.4 constrained directional enhancement filtering | libaom `av1/common/cdef.c`, `av1/common/cdef_block.c`, `av1/common/cdef.h`, and `av1/common/cdef_block.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design direction search and constrained primary/secondary filtering around vector-friendly block storage and SIMD-first kernels, with variance adjustment, subsampling direction conversion, skipped-8x8 selection, and frame-edge sentinel behavior shared by the scalar fallback. Use a frame-owned source snapshot so filtering never consumes already modified samples. This is normative AV1 still-image reconstruction and introduces no ISO BMFF, track, timing, or sequence-playback surface. |
@ -355,10 +355,10 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu
- Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, clipped-edge padding, and direct sample prediction through the existing reconstruction pipeline. The current scalar behavior matches the pinned libaom source, but this path is not production-complete until it is expressed through the SIMD-first predictor architecture and verified with an independently encoded palette AVIF fixture.
- Non-reduced still-image sequence parsing now consumes decoder-model operating parameters, temporal presentation fields, and OBU-layer-applicable buffer-removal fields only to preserve AV1 bit alignment. The scheduling values are not retained, and no movie, track, timing, playback, or generic ISO BMFF surface has been introduced. Existing focused sequence-header coverage exercises only reduced-still syntax, so an independent non-reduced still AVIF vector remains required.
- Loop-restoration unit parsing records tile-local switchable/Wiener/self-guided filter selections and coefficients in frame-owned plane grids, including super-resolution-adjusted unit corners and the corrected conditional 64x64-superblock unit-size bit. The active restoration stage implements the normative unit geometry, striped deblocked boundaries, Wiener filtering, self-guided projection, and 8/10/12-bit clipping, while reusing the existing SIMD-backed super-resolution and adjacent multiply/add primitives. Independently encoded fixtures covering every parameter set, plane layout, bit depth, and frame-edge geometry are still required. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state.
- The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Chroma-from-luma storage, subsampling, parameter derivation, U/V sharing, and 8/10/12-bit prediction are active; independently encoded high-bit-depth and chroma-from-luma AVIF conformance files are still required.
- The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Chroma-from-luma storage, 4:4:4/4:2:2/4:2:0 subsampling, rounded mean subtraction, U/V sharing, and 8/10/12-bit prediction now traverse AVX2 and cross-platform `Vector128` paths before an exact scalar fallback. `FeatureTestRunner` verifies every tier against independent fixed-point definitions across every supported block width; independently encoded high-bit-depth and chroma-from-luma AVIF conformance files are still required.
- `Av1YuvConverter` now consumes the signaled full or limited range, every non-reserved AV1 H.273 matrix coefficient, transfer characteristics where the matrix definition requires them, subsampling, and chroma sample position for 8, 10, and 12-bit output. Its high-bit-depth decode and encode paths use allocator-backed `Rgb48` rows and the existing `PixelOperations<TPixel>` conversions, avoiding the former eight-bit intermediate. Encoder conversion covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with libavif-compatible box averaging. Identity, full/limited-range YCgCo, the fixed non-constant-luminance matrices, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, and PQ/HLG ICtCp are active in both directions. Independent vectors for every matrix, transfer, range, bit depth, sampling layout, and chroma position remain required before the complete color matrix is externally verified.
- Forward and inverse transforms use operation-owned workspace, explicit sequential fixed storage, and stateless static-generic operator structs for every valid DCT, ADST, and identity size. Named configuration factories keep the encoder's three shifts and variable cosine precision separate from the decoder's two shifts, fixed 12-bit cosine precision, and 8/10/12-bit clamp ranges. Forward traversal shares one libaom-shaped stage network across scalar, `Vector128`, `Vector256`, and `Vector512`; inverse production traversal uses the verified scalar, `Vector128`, and `Vector256` tiers. The focused 511-case Release matrix passes across the `FeatureTestRunner` hardware configurations, and the production 32x32 benchmark reports zero allocation with the paired stage implementation.
- The production prediction and nonlinear self-guided paths remain predominantly scalar and therefore do not meet the completion architecture. They must be redesigned around vector-friendly storage, shared operator contracts, and SIMD-first traversal with benchmark-gated lane selection, with their current results retained only as fallback/parity behavior. Transform traversal is already SIMD-first, while normative super-resolution and Wiener horizontal products reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks.
- Core intra prediction and chroma-from-luma now use SIMD-first operator or packed-kernel architectures with exact scalar fallbacks. Palette reconstruction and nonlinear self-guided restoration remain scalar and therefore do not meet the completion architecture. They must be redesigned around vector-friendly storage, shared operator contracts, and SIMD-first traversal with benchmark-gated lane selection, with their current results retained only as fallback/parity behavior. Transform traversal is already SIMD-first, while normative super-resolution and Wiener horizontal products reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks.
### AV1 encoder
@ -495,6 +495,7 @@ Implement and verify in dependency order:
- [ ] One coherent decoder lifecycle that retains parsed frame and tile state and disposes all buffers deterministically.
- [ ] Tile partitioning, mode information, segmentation, delta quantization, transform-size selection, coefficient token decode, inverse quantization, and inverse transforms.
- [ ] Intra prediction, including every directional, smooth, Paeth, CFL, filter-intra, and palette case permitted by AV1.
- [x] Implement SIMD-first chroma-from-luma storage, 4:4:4/4:2:2/4:2:0 subsampling, rounded mean subtraction, and 8/10/12-bit prediction with exact scalar fallback and `FeatureTestRunner` parity.
- [ ] Lossless and high-bit-depth reconstruction with correct clipping and intermediate precision.
- [ ] Deblocking loop filter.
- [ ] CDEF.
@ -634,6 +635,7 @@ Tasks:
- [x] Add a permanent frame-wide HEVC inverse-quantization benchmark. On .NET 10, dense 32x32 twelve-bit flat and scaling-list paths measured 69.98 and 238.4 microseconds per padded 1920x1088 frame, compared with forced-scalar timings of 1.469 and 1.609 milliseconds: 21.0 and 6.7 times faster with zero managed allocations. Pre-expanding the scaling matrices once reduced the SIMD scaling-list path from 4.974 milliseconds to 238.4 microseconds.
- [x] Add a permanent frame-wide HEVC residual-reconstruction benchmark. On .NET 10, dense 32x32 twelve-bit transform skip, horizontal RDPCM, and vertical RDPCM measured 79.64, 275.61, and 110.81 microseconds per padded 1920x1088 frame, compared with forced-scalar timings of 571.5 microseconds, 1.512 milliseconds, and 1.257 milliseconds: 7.2, 5.5, and 11.3 times faster with zero managed allocations.
- [x] Add a permanent frame-wide HEVC grouped coefficient-scan benchmark. On .NET 10, generating the complete diagonal 32x32 scan for all 2,040 transform blocks in a padded 1920x1088 frame measured 1.432 milliseconds with zero managed allocations. CABAC decisions remain serial and require an independently encoded complete-slice workload after transform-unit traversal is connected.
- [x] Add a permanent frame-wide AV1 chroma-from-luma prediction benchmark. The lane-selection run rejected the explicit `Vector512` experiment because AVX2 was 2.6% faster at 8-bit and 4.8% faster at 12-bit on the test system. With that path removed, the selected hardware and forced-AVX2 paths are equivalent; the final .NET 10 run measured 88.76 and 74.89 microseconds per padded 1920x1088 frame at 8 and 12 bits, compared with forced-scalar timings of 1.594 and 1.604 milliseconds: 18.0 and 21.4 times faster with zero managed allocations.
- [ ] Implement vector paths only for confirmed hot loops, using existing `Vector128`, `Vector256`, and `Vector512` helper and dispatch patterns where supported.
- [ ] Prioritize shared color conversion and pixel packing, chroma upsampling, inverse-transform add-and-clip, intra predictors, HEVC deblock/SAO, AV1 loop filter/CDEF/restoration, and contiguous grid copies.
- [ ] Benchmark the complete decode color pipeline on representative 8/10/12-bit AVIF and HEIC images with and without embedded ICC profiles. Report absolute end-to-end timings and allocations in addition to the isolated YUV/CICP and ICC stage costs.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Simd.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs

@ -8,7 +8,7 @@ using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content>
/// Provides the SIMD projection kernels for AV1 directional intra prediction.
/// Provides packed projection kernels for AV1 directional intra prediction.
/// </content>
internal static partial class Av1DirectionalIntraPredictor
{

68
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs

@ -248,16 +248,17 @@ internal class Av1PredictionDecoder
int alphaQ3 = ChromaFromLumaIndexToAlpha(modeInfo.ChromaFromLumaAlphaIndex, modeInfo.ChromaFromLumaAlphaSign, plane);
Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth;
ChromaFromLumaPredict(
chromaFromLumaContext.Q3Buffer,
pixelBuffer,
stride,
pixelBuffer,
stride,
alphaQ3,
bitDepth,
transformSize.GetWidth(),
transformSize.GetHeight());
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
if (typeof(T) == typeof(byte))
{
Av1ChromaFromLumaPredictor.Predict(chromaFromLumaContext.Q3Buffer, MemoryMarshal.Cast<T, byte>(pixelBuffer), stride, alphaQ3, width, height);
}
else
{
Av1ChromaFromLumaPredictor.Predict(chromaFromLumaContext.Q3Buffer, MemoryMarshal.Cast<T, short>(pixelBuffer), stride, alphaQ3, bitDepth.GetBitCount(), width, height);
}
}
/// <summary>
@ -305,53 +306,6 @@ internal class Av1PredictionDecoder
return (alphaSign == Av1ChromaFromLumaMath.SignPositive) ? absAlphaQ3 + 1 : -absAlphaQ3 - 1;
}
/// <summary>
/// Multiplies a Q3 chroma-from-luma alpha by a Q3 luma residual and returns an integer sample adjustment.
/// </summary>
/// <param name="alphaQ3">The signed Q3 chroma scaling factor.</param>
/// <param name="predictedQ3">The signed Q3 luma residual.</param>
/// <returns>The signed integer sample adjustment.</returns>
private static int GetScaledLumaQ0(int alphaQ3, short predictedQ3)
{
int scaledLumaQ6 = alphaQ3 * predictedQ3;
return Av1Math.RoundPowerOf2Signed(scaledLumaQ6, 6);
}
/// <summary>
/// Applies a chroma-from-luma residual to a chroma prediction block.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="predictedBufferQ3">The Q3 luma residuals stored with the normative 32-sample CfL row stride.</param>
/// <param name="predictedBuffer">The base chroma prediction samples.</param>
/// <param name="predictedStride">The distance, in samples, between base prediction rows.</param>
/// <param name="destinationBuffer">The buffer that receives the adjusted chroma samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="alphaQ3">The signed Q3 chroma scaling factor.</param>
/// <param name="bitDepth">The bit depth used to clamp reconstructed samples.</param>
/// <param name="width">The width of the prediction block in samples.</param>
/// <param name="height">The height of the prediction block in samples.</param>
public static void ChromaFromLumaPredict<T>(Span<short> predictedBufferQ3, Span<T> predictedBuffer, int predictedStride, Span<T> destinationBuffer, int destinationStride, int alphaQ3, Av1BitDepth bitDepth, int width, int height)
where T : unmanaged, IBinaryInteger<T>
{
// TODO: Make SIMD variant of this method.
int maxPixelValue = (1 << bitDepth.GetBitCount()) - 1;
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
int alphaQ0 = GetScaledLumaQ0(alphaQ3, predictedBufferQ3[i]);
int predicted = int.CreateChecked(predictedBuffer[i]);
destinationBuffer[i] = T.CreateChecked(Av1Math.Clamp(alphaQ0 + predicted, 0, maxPixelValue));
}
destinationBuffer = destinationBuffer[destinationStride..];
predictedBuffer = predictedBuffer[predictedStride..];
// The CfL scratch buffer has a fixed 32-sample row stride independent of transform width.
predictedBufferQ3 = predictedBufferQ3[32..];
}
}
/// <summary>
/// Determines available reference samples and dispatches prediction for one transform block.
/// </summary>

429
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs

@ -0,0 +1,429 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
/// <content>
/// Provides packed luma subsampling and mean-subtraction operations for chroma-from-luma prediction.
/// </content>
internal partial class Av1ChromaFromLumaContext
{
/// <summary>
/// Stores 8-bit reconstructed luma samples in the Q3 predictor surface.
/// </summary>
/// <param name="input">The reconstructed luma samples.</param>
/// <param name="inputStride">The distance, in samples, between input rows.</param>
/// <param name="outputOffset">The first destination sample in the fixed-stride predictor buffer.</param>
/// <param name="width">The luma width in samples.</param>
/// <param name="height">The luma height in samples.</param>
private void StoreSamples(ReadOnlySpan<byte> input, int inputStride, int outputOffset, int width, int height)
{
ref byte inputBase = ref MemoryMarshal.GetReference(input);
ref short outputBase = ref MemoryMarshal.GetArrayDataReference(this.Q3Buffer);
if (!this.subX)
{
// One luma sample maps directly to one chroma sample, so multiplying by eight converts it to Q3.
for (int row = 0; row < height; row++)
{
ref byte inputRow = ref Unsafe.Add(ref inputBase, row * inputStride);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + (row * BufferLine));
int column = 0;
if (Vector256.IsHardwareAccelerated)
{
for (; column <= width - Vector256<byte>.Count; column += Vector256<byte>.Count)
{
(Vector256<ushort> lower, Vector256<ushort> upper) = Vector256.Widen(Vector256.LoadUnsafe(ref inputRow, (nuint)column));
(lower << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)column);
(upper << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)(column + Vector256<ushort>.Count));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; column <= width - Vector128<byte>.Count; column += Vector128<byte>.Count)
{
(Vector128<ushort> lower, Vector128<ushort> upper) = Vector128.Widen(Vector128.LoadUnsafe(ref inputRow, (nuint)column));
(lower << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)column);
(upper << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)(column + Vector128<ushort>.Count));
}
if (column < width)
{
ulong packed = width - column == 4
? Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref inputRow, column))
: Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref inputRow, column));
Vector128<short> q3 = (Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()) << 3).AsInt16();
if (width - column == 4)
{
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref outputRow, column)), q3.AsUInt64().ToScalar());
}
else
{
q3.StoreUnsafe(ref outputRow, (nuint)column);
}
column = width;
}
}
for (; column < width; column++)
{
Unsafe.Add(ref outputRow, column) = (short)(Unsafe.Add(ref inputRow, column) << 3);
}
}
return;
}
Vector256<sbyte> ones256 = Vector256.Create((sbyte)1);
Vector128<sbyte> ones128 = Vector128.Create((sbyte)1);
int rowStep = this.subY ? 2 : 1;
int outputShift = this.subY ? 1 : 2;
for (int row = 0; row < height; row += rowStep)
{
ref byte inputRow = ref Unsafe.Add(ref inputBase, row * inputStride);
ref byte nextInputRow = ref Unsafe.Add(ref inputRow, this.subY ? inputStride : 0);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (this.subY ? 1 : 0)) * BufferLine));
int column = 0;
if (Avx2.IsSupported)
{
for (; column <= width - Vector256<byte>.Count; column += Vector256<byte>.Count)
{
Vector256<short> sum = Avx2.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref inputRow, (nuint)column), ones256);
if (this.subY)
{
sum += Avx2.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref nextInputRow, (nuint)column), ones256);
}
(sum << outputShift).StoreUnsafe(ref outputRow, (nuint)(column >> 1));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; column <= width - Vector128<byte>.Count; column += Vector128<byte>.Count)
{
Vector128<short> sum = PairSum(Vector128.LoadUnsafe(ref inputRow, (nuint)column), ones128);
if (this.subY)
{
sum += PairSum(Vector128.LoadUnsafe(ref nextInputRow, (nuint)column), ones128);
}
(sum << outputShift).StoreUnsafe(ref outputRow, (nuint)(column >> 1));
}
if (column < width)
{
int remaining = width - column;
ulong packed = remaining == 4
? Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref inputRow, column))
: Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref inputRow, column));
Vector128<short> sum = PairSum(Vector128.CreateScalarUnsafe(packed).AsByte(), ones128);
if (this.subY)
{
packed = remaining == 4
? Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref nextInputRow, column))
: Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref nextInputRow, column));
sum += PairSum(Vector128.CreateScalarUnsafe(packed).AsByte(), ones128);
}
Vector128<short> q3 = sum << outputShift;
if (remaining == 4)
{
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref outputRow, column >> 1)), q3.AsUInt32().ToScalar());
}
else
{
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref outputRow, column >> 1)), q3.AsUInt64().ToScalar());
}
column = width;
}
}
for (; column < width; column += 2)
{
int sum = Unsafe.Add(ref inputRow, column) + Unsafe.Add(ref inputRow, column + 1);
if (this.subY)
{
sum += Unsafe.Add(ref nextInputRow, column) + Unsafe.Add(ref nextInputRow, column + 1);
}
Unsafe.Add(ref outputRow, column >> 1) = (short)(sum << outputShift);
}
}
}
/// <summary>
/// Stores high-bit-depth reconstructed luma samples in the Q3 predictor surface.
/// </summary>
/// <param name="input">The reconstructed luma samples.</param>
/// <param name="inputStride">The distance, in samples, between input rows.</param>
/// <param name="outputOffset">The first destination sample in the fixed-stride predictor buffer.</param>
/// <param name="width">The luma width in samples.</param>
/// <param name="height">The luma height in samples.</param>
private void StoreSamples(ReadOnlySpan<short> input, int inputStride, int outputOffset, int width, int height)
{
ref short inputBase = ref MemoryMarshal.GetReference(input);
ref short outputBase = ref MemoryMarshal.GetArrayDataReference(this.Q3Buffer);
if (!this.subX)
{
for (int row = 0; row < height; row++)
{
ref short inputRow = ref Unsafe.Add(ref inputBase, row * inputStride);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + (row * BufferLine));
int column = 0;
if (Vector256.IsHardwareAccelerated)
{
for (; column <= width - Vector256<short>.Count; column += Vector256<short>.Count)
{
(Vector256.LoadUnsafe(ref inputRow, (nuint)column) << 3).StoreUnsafe(ref outputRow, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count)
{
(Vector128.LoadUnsafe(ref inputRow, (nuint)column) << 3).StoreUnsafe(ref outputRow, (nuint)column);
}
if (column < width)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref inputRow, column)));
Vector128<short> q3 = Vector128.CreateScalarUnsafe(packed).AsInt16() << 3;
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref outputRow, column)), q3.AsUInt64().ToScalar());
column += 4;
}
}
for (; column < width; column++)
{
Unsafe.Add(ref outputRow, column) = (short)(Unsafe.Add(ref inputRow, column) << 3);
}
}
return;
}
Vector256<short> ones256 = Vector256.Create((short)1);
Vector128<short> ones128 = Vector128.Create((short)1);
int rowStep = this.subY ? 2 : 1;
int outputShift = this.subY ? 1 : 2;
for (int row = 0; row < height; row += rowStep)
{
ref short inputRow = ref Unsafe.Add(ref inputBase, row * inputStride);
ref short nextInputRow = ref Unsafe.Add(ref inputRow, this.subY ? inputStride : 0);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (this.subY ? 1 : 0)) * BufferLine));
int column = 0;
if (Vector256.IsHardwareAccelerated)
{
for (; column <= width - Vector256<short>.Count; column += Vector256<short>.Count)
{
Vector256<int> sum = Vector256_.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref inputRow, (nuint)column), ones256);
if (this.subY)
{
sum += Vector256_.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref nextInputRow, (nuint)column), ones256);
}
Vector256.Narrow(sum << outputShift, Vector256<int>.Zero).GetLower().StoreUnsafe(ref outputRow, (nuint)(column >> 1));
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count)
{
Vector128<int> sum = Vector128_.MultiplyAddAdjacent(Vector128.LoadUnsafe(ref inputRow, (nuint)column), ones128);
if (this.subY)
{
sum += Vector128_.MultiplyAddAdjacent(Vector128.LoadUnsafe(ref nextInputRow, (nuint)column), ones128);
}
Vector128.Narrow(sum << outputShift, Vector128<int>.Zero).GetLower().StoreUnsafe(ref outputRow, (nuint)(column >> 1));
}
if (column < width)
{
Vector128<short> samples = Vector128.CreateScalarUnsafe(
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref inputRow, column)))).AsInt16();
Vector128<int> sum = Vector128_.MultiplyAddAdjacent(samples, ones128);
if (this.subY)
{
samples = Vector128.CreateScalarUnsafe(
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref nextInputRow, column)))).AsInt16();
sum += Vector128_.MultiplyAddAdjacent(samples, ones128);
}
Vector64<short> q3 = Vector128.Narrow(sum << outputShift, Vector128<int>.Zero).GetLower();
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref outputRow, column >> 1)), q3.AsUInt32().ToScalar());
column += 4;
}
}
for (; column < width; column += 2)
{
int sum = Unsafe.Add(ref inputRow, column) + Unsafe.Add(ref inputRow, column + 1);
if (this.subY)
{
sum += Unsafe.Add(ref nextInputRow, column) + Unsafe.Add(ref nextInputRow, column + 1);
}
Unsafe.Add(ref outputRow, column >> 1) = (short)(sum << outputShift);
}
}
}
/// <summary>
/// Subtracts the rounded Q3 average from each predictor sample, leaving the AC contribution used by CfL.
/// </summary>
/// <param name="transformSize">The populated predictor dimensions.</param>
private void SubtractAverage(Av1TransformSize transformSize)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Transform dimensions are powers of two, so division by the sample count is an exact right shift. Half
// the sample count is accumulated first to implement the normative nearest-integer rounding.
int sumQ3 = (width * height) >> 1;
ref short bufferBase = ref MemoryMarshal.GetArrayDataReference(this.Q3Buffer);
if (Vector256.IsHardwareAccelerated && width >= Vector256<short>.Count)
{
for (int row = 0; row < height; row++)
{
int rowOffset = row * BufferLine;
for (int column = 0; column < width; column += Vector256<short>.Count)
{
(Vector256<int> lower, Vector256<int> upper) = Vector256.Widen(Vector256.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column)));
sumQ3 += Vector256.Sum(lower) + Vector256.Sum(upper);
}
}
}
else if (Vector128.IsHardwareAccelerated)
{
for (int row = 0; row < height; row++)
{
int rowOffset = row * BufferLine;
int column = 0;
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count)
{
(Vector128<int> lower, Vector128<int> upper) = Vector128.Widen(Vector128.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column)));
sumQ3 += Vector128.Sum(lower) + Vector128.Sum(upper);
}
if (column < width)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref bufferBase, rowOffset + column)));
sumQ3 += Vector128.Sum(Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsInt16()));
}
}
}
else
{
for (int row = 0; row < height; row++)
{
int rowOffset = row * BufferLine;
for (int column = 0; column < width; column++)
{
sumQ3 += Unsafe.Add(ref bufferBase, rowOffset + column);
}
}
}
int pelCountLog2 = transformSize.GetBlockWidthLog2() + transformSize.GetBlockHeightLog2();
short averageQ3 = (short)(sumQ3 >> pelCountLog2);
if (Vector256.IsHardwareAccelerated && width >= Vector256<short>.Count)
{
Vector256<short> average = Vector256.Create(averageQ3);
for (int row = 0; row < height; row++)
{
int rowOffset = row * BufferLine;
for (int column = 0; column < width; column += Vector256<short>.Count)
{
(Vector256.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column)) - average).StoreUnsafe(ref bufferBase, (nuint)(rowOffset + column));
}
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<short> average = Vector128.Create(averageQ3);
for (int row = 0; row < height; row++)
{
int rowOffset = row * BufferLine;
int column = 0;
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count)
{
(Vector128.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column)) - average).StoreUnsafe(ref bufferBase, (nuint)(rowOffset + column));
}
if (column < width)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref bufferBase, rowOffset + column)));
Vector128<short> result = Vector128.CreateScalarUnsafe(packed).AsInt16() - average;
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref bufferBase, rowOffset + column)), result.AsUInt64().ToScalar());
}
}
return;
}
for (int row = 0; row < height; row++)
{
int rowOffset = row * BufferLine;
for (int column = 0; column < width; column++)
{
Unsafe.Add(ref bufferBase, rowOffset + column) -= averageQ3;
}
}
}
/// <summary>
/// Adds adjacent 8-bit samples into eight 16-bit lanes.
/// </summary>
/// <param name="samples">The packed source samples.</param>
/// <param name="ones">The multiplier used by the x86 adjacent multiply-add instruction.</param>
/// <returns>The adjacent pair sums.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> PairSum(Vector128<byte> samples, Vector128<sbyte> ones)
{
if (Ssse3.IsSupported)
{
return Ssse3.MultiplyAddAdjacent(samples, ones);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.AddPairwiseWidening(samples).AsInt16();
}
// WebAssembly has byte shuffles but no pairwise-widening intrinsic. Grouping even and odd bytes before
// widening keeps all eight pair additions vectorized without adding a general-purpose utility wrapper.
Vector128<byte> even = Vector128.Shuffle(samples, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 255, 255, 255, 255, 255, 255, 255, 255));
Vector128<byte> odd = Vector128.Shuffle(samples, Vector128.Create((byte)1, 3, 5, 7, 9, 11, 13, 15, 255, 255, 255, 255, 255, 255, 255, 255));
return (Vector128.WidenLower(even) + Vector128.WidenLower(odd)).AsInt16();
}
}

86
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -10,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
/// <summary>
/// Accumulates subsampled luma samples and derives the zero-mean Q3 predictor surface used by AV1 chroma-from-luma prediction.
/// </summary>
internal class Av1ChromaFromLumaContext
internal partial class Av1ChromaFromLumaContext
{
/// <summary>
/// The fixed row stride and maximum dimension, in chroma samples, of the luma predictor buffer.
@ -119,51 +120,16 @@ internal class Av1ChromaFromLumaContext
}
int outputOffset = (storeRow * BufferLine) + storeColumn;
if (!this.subX)
{
// A direct luma sample is multiplied by eight to produce the Q3 representation used by CfL.
for (int y = 0; y < height; y++)
{
int inputRow = y * inputStride;
int outputRow = outputOffset + (y * BufferLine);
for (int x = 0; x < width; x++)
{
this.Q3Buffer[outputRow + x] = (short)(int.CreateChecked(input[inputRow + x]) << 3);
}
}
}
else if (!this.subY)
// Reconstruction reaches this method only through the byte and short decoder pipelines. Dispatching once
// here keeps sample conversion out of the row kernels and lets the JIT specialize both storage layouts.
if (typeof(T) == typeof(byte))
{
// The pair sum is multiplied by four, which is the Q3 representation of its horizontal average.
for (int y = 0; y < height; y++)
{
int inputRow = y * inputStride;
int outputRow = outputOffset + (y * BufferLine);
for (int x = 0; x < width; x += 2)
{
int sum = int.CreateChecked(input[inputRow + x]) + int.CreateChecked(input[inputRow + x + 1]);
this.Q3Buffer[outputRow + (x >> 1)] = (short)(sum << 2);
}
}
this.StoreSamples(MemoryMarshal.Cast<T, byte>(input), inputStride, outputOffset, width, height);
}
else
{
// The 2x2 sum is multiplied by two, which is the Q3 representation of its four-sample average.
for (int y = 0; y < height; y += 2)
{
int inputRow = y * inputStride;
int nextInputRow = inputRow + inputStride;
int outputRow = outputOffset + ((y >> 1) * BufferLine);
for (int x = 0; x < width; x += 2)
{
int sum = int.CreateChecked(input[inputRow + x]) +
int.CreateChecked(input[inputRow + x + 1]) +
int.CreateChecked(input[nextInputRow + x]) +
int.CreateChecked(input[nextInputRow + x + 1]);
this.Q3Buffer[outputRow + (x >> 1)] = (short)(sum << 1);
}
}
this.StoreSamples(MemoryMarshal.Cast<T, short>(input), inputStride, outputOffset, width, height);
}
}
@ -216,40 +182,4 @@ internal class Av1ChromaFromLumaContext
this.bufferHeight = height;
}
}
/// <summary>
/// Subtracts the rounded Q3 average from each predictor sample, leaving the AC contribution used by CfL.
/// </summary>
/// <param name="transformSize">The populated predictor dimensions.</param>
/// <remarks>SVT-AV1: <c>svt_subtract_average_c</c>.</remarks>
private void SubtractAverage(Av1TransformSize transformSize)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Transform dimensions are powers of two, so division by the sample count is an exact right shift.
// Half the sample count is accumulated first to round the signed Q3 mean to the nearest integer.
int roundOffset = (width * height) >> 1;
int pelCountLog2 = transformSize.GetBlockWidthLog2() + transformSize.GetBlockHeightLog2();
int sumQ3 = roundOffset;
for (int y = 0; y < height; y++)
{
int rowOffset = y * BufferLine;
for (int x = 0; x < width; x++)
{
sumQ3 += this.Q3Buffer[rowOffset + x];
}
}
int averageQ3 = sumQ3 >> pelCountLog2;
for (int y = 0; y < height; y++)
{
int rowOffset = y * BufferLine;
for (int x = 0; x < width; x++)
{
this.Q3Buffer[rowOffset + x] -= (short)averageQ3;
}
}
}
}

231
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaPredictor.cs

@ -0,0 +1,231 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
/// <summary>
/// Applies an AV1 chroma-from-luma residual to a DC-predicted chroma block.
/// </summary>
internal static class Av1ChromaFromLumaPredictor
{
/// <summary>
/// The fixed row stride of the AV1 chroma-from-luma scratch buffer.
/// </summary>
private const int BufferLine = 32;
/// <summary>
/// Applies chroma-from-luma prediction to an 8-bit chroma block.
/// </summary>
/// <param name="lumaQ3">The zero-mean Q3 luma surface.</param>
/// <param name="destination">The DC-predicted chroma block that receives the luma adjustment.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="alphaQ3">The signed Q3 chroma scaling factor.</param>
/// <param name="width">The block width in samples.</param>
/// <param name="height">The block height in samples.</param>
public static void Predict(ReadOnlySpan<short> lumaQ3, Span<byte> destination, int destinationStride, int alphaQ3, int width, int height)
{
ref short lumaBase = ref MemoryMarshal.GetReference(lumaQ3);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
short dc = destinationBase;
// CfL always follows DC prediction, so the first sample is the common base value for every lane. This
// mirrors libaom and avoids loading a block that is known to contain a single repeated prediction value.
if (Avx2.IsSupported && width >= Vector256<short>.Count)
{
Vector256<short> alphaSign = Vector256.Create((short)alphaQ3);
Vector256<short> alphaQ12 = Vector256.Create((short)(Math.Abs(alphaQ3) << 9));
Vector256<short> dcVector = Vector256.Create(dc);
Vector256<short> maximum = Vector256.Create((short)byte.MaxValue);
for (int row = 0; row < height; row++)
{
int lumaRowOffset = row * BufferLine;
int destinationRowOffset = row * destinationStride;
for (int column = 0; column < width; column += Vector256<short>.Count)
{
Vector256<short> prediction = Predict(Vector256.LoadUnsafe(ref lumaBase, (nuint)(lumaRowOffset + column)), alphaSign, alphaQ12, dcVector);
prediction = Vector256.Clamp(prediction, Vector256<short>.Zero, maximum);
Vector256.Narrow(prediction.AsUInt16(), Vector256<ushort>.Zero).GetLower().StoreUnsafe(ref destinationBase, (nuint)(destinationRowOffset + column));
}
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<short> alphaSign = Vector128.Create((short)alphaQ3);
Vector128<short> alphaQ12 = Vector128.Create((short)(Math.Abs(alphaQ3) << 9));
Vector128<short> dcVector = Vector128.Create(dc);
Vector128<short> maximum = Vector128.Create((short)byte.MaxValue);
for (int row = 0; row < height; row++)
{
int lumaRowOffset = row * BufferLine;
int destinationRowOffset = row * destinationStride;
int column = 0;
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count)
{
Vector128<short> prediction = Predict(Vector128.LoadUnsafe(ref lumaBase, (nuint)(lumaRowOffset + column)), alphaSign, alphaQ12, dcVector, alphaQ3);
prediction = Vector128.Clamp(prediction, Vector128<short>.Zero, maximum);
Vector128.Narrow(prediction.AsUInt16(), Vector128<ushort>.Zero).GetLower().StoreUnsafe(ref destinationBase, (nuint)(destinationRowOffset + column));
}
// Four-wide CfL blocks still have a complete padded scratch row, so reading eight residuals is
// valid. Only the four active predictions are stored to the image buffer.
if (column < width)
{
Vector128<short> prediction = Predict(Vector128.LoadUnsafe(ref lumaBase, (nuint)(lumaRowOffset + column)), alphaSign, alphaQ12, dcVector, alphaQ3);
prediction = Vector128.Clamp(prediction, Vector128<short>.Zero, maximum);
Vector64<byte> packed = Vector128.Narrow(prediction.AsUInt16(), Vector128<ushort>.Zero).GetLower();
Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationBase, destinationRowOffset + column), packed.AsUInt32().ToScalar());
}
}
return;
}
for (int row = 0; row < height; row++)
{
int lumaRowOffset = row * BufferLine;
int destinationRowOffset = row * destinationStride;
for (int column = 0; column < width; column++)
{
int scaledLumaQ0 = Av1Math.RoundPowerOf2Signed(alphaQ3 * Unsafe.Add(ref lumaBase, lumaRowOffset + column), 6);
Unsafe.Add(ref destinationBase, destinationRowOffset + column) = (byte)Math.Clamp(dc + scaledLumaQ0, byte.MinValue, byte.MaxValue);
}
}
}
/// <summary>
/// Applies chroma-from-luma prediction to a high-bit-depth chroma block.
/// </summary>
/// <param name="lumaQ3">The zero-mean Q3 luma surface.</param>
/// <param name="destination">The DC-predicted chroma block that receives the luma adjustment.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="alphaQ3">The signed Q3 chroma scaling factor.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
/// <param name="width">The block width in samples.</param>
/// <param name="height">The block height in samples.</param>
public static void Predict(ReadOnlySpan<short> lumaQ3, Span<short> destination, int destinationStride, int alphaQ3, int bitDepth, int width, int height)
{
ref short lumaBase = ref MemoryMarshal.GetReference(lumaQ3);
ref short destinationBase = ref MemoryMarshal.GetReference(destination);
short dc = destinationBase;
short maximum = (short)((1 << bitDepth) - 1);
if (Avx2.IsSupported && width >= Vector256<short>.Count)
{
Vector256<short> alphaSign = Vector256.Create((short)alphaQ3);
Vector256<short> alphaQ12 = Vector256.Create((short)(Math.Abs(alphaQ3) << 9));
Vector256<short> dcVector = Vector256.Create(dc);
Vector256<short> maximumVector = Vector256.Create(maximum);
for (int row = 0; row < height; row++)
{
int lumaRowOffset = row * BufferLine;
int destinationRowOffset = row * destinationStride;
for (int column = 0; column < width; column += Vector256<short>.Count)
{
Vector256<short> prediction = Predict(Vector256.LoadUnsafe(ref lumaBase, (nuint)(lumaRowOffset + column)), alphaSign, alphaQ12, dcVector);
Vector256.Clamp(prediction, Vector256<short>.Zero, maximumVector).StoreUnsafe(ref destinationBase, (nuint)(destinationRowOffset + column));
}
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<short> alphaSign = Vector128.Create((short)alphaQ3);
Vector128<short> alphaQ12 = Vector128.Create((short)(Math.Abs(alphaQ3) << 9));
Vector128<short> dcVector = Vector128.Create(dc);
Vector128<short> maximumVector = Vector128.Create(maximum);
for (int row = 0; row < height; row++)
{
int lumaRowOffset = row * BufferLine;
int destinationRowOffset = row * destinationStride;
int column = 0;
for (; column <= width - Vector128<short>.Count; column += Vector128<short>.Count)
{
Vector128<short> prediction = Predict(Vector128.LoadUnsafe(ref lumaBase, (nuint)(lumaRowOffset + column)), alphaSign, alphaQ12, dcVector, alphaQ3);
Vector128.Clamp(prediction, Vector128<short>.Zero, maximumVector).StoreUnsafe(ref destinationBase, (nuint)(destinationRowOffset + column));
}
if (column < width)
{
Vector128<short> prediction = Predict(Vector128.LoadUnsafe(ref lumaBase, (nuint)(lumaRowOffset + column)), alphaSign, alphaQ12, dcVector, alphaQ3);
prediction = Vector128.Clamp(prediction, Vector128<short>.Zero, maximumVector);
Unsafe.WriteUnaligned(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref destinationBase, destinationRowOffset + column)), prediction.AsUInt64().ToScalar());
}
}
return;
}
for (int row = 0; row < height; row++)
{
int lumaRowOffset = row * BufferLine;
int destinationRowOffset = row * destinationStride;
for (int column = 0; column < width; column++)
{
int scaledLumaQ0 = Av1Math.RoundPowerOf2Signed(alphaQ3 * Unsafe.Add(ref lumaBase, lumaRowOffset + column), 6);
Unsafe.Add(ref destinationBase, destinationRowOffset + column) = (short)Math.Clamp(dc + scaledLumaQ0, 0, maximum);
}
}
}
/// <summary>
/// Calculates sixteen chroma predictions using the packed Q3 arithmetic defined by AV1.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<short> Predict(Vector256<short> lumaQ3, Vector256<short> alphaSign, Vector256<short> alphaQ12, Vector256<short> dc)
{
Vector256<short> scaledLumaQ0 = Avx2.MultiplyHighRoundScale(Avx2.Abs(lumaQ3).AsInt16(), alphaQ12);
Vector256<short> signMask = (lumaQ3 ^ alphaSign) >> 15;
return ((scaledLumaQ0 ^ signMask) - signMask) + dc;
}
/// <summary>
/// Calculates eight chroma predictions using the packed Q3 arithmetic defined by AV1.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> Predict(Vector128<short> lumaQ3, Vector128<short> alphaSign, Vector128<short> alphaQ12, Vector128<short> dc, int alphaQ3)
{
Vector128<short> scaledLumaQ0;
if (Ssse3.IsSupported)
{
scaledLumaQ0 = Ssse3.MultiplyHighRoundScale(Ssse3.Abs(lumaQ3).AsInt16(), alphaQ12);
}
else if (AdvSimd.IsSupported)
{
scaledLumaQ0 = AdvSimd.MultiplyRoundedDoublingSaturateHigh(Vector128.Abs(lumaQ3), alphaQ12);
}
else
{
// WebAssembly and other Vector128 targets do not expose packed rounded-high multiply. Widening retains
// SIMD traversal while reproducing the same signed nearest-integer result in ordinary integer lanes.
(Vector128<int> lower, Vector128<int> upper) = Vector128.Widen(lumaQ3);
Vector128<int> alpha = Vector128.Create(alphaQ3);
lower *= alpha;
upper *= alpha;
lower = (lower + Vector128.Create(32) + (lower >> 31)) >> 6;
upper = (upper + Vector128.Create(32) + (upper >> 31)) >> 6;
return Vector128.Narrow(lower, upper) + dc;
}
Vector128<short> signMask = (lumaQ3 ^ alphaSign) >> 15;
return ((scaledLumaQ0 ^ signMask) - signMask) + dc;
}
}

2
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.Simd.cs → src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.Operator.cs

@ -8,7 +8,7 @@ using static SixLabors.ImageSharp.Formats.Heif.Components.HeifTransferVectorOper
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides the SIMD implementations of the H.273 transfer characteristics used by HEIF color conversion.
/// Provides fixed-width vector overloads and shared H.273 transfer operations for HEIF color conversion.
/// </content>
internal static partial class HeifTransferFunctions
{

2
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuv420ToRgb8Converter.Simd.cs → src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuv420ToRgb8Converter.Operator.cs

@ -9,7 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides the operator-driven SIMD row traversal for eight-bit 4:2:0 conversion.
/// Defines fixed-point operators and row traversal for eight-bit 4:2:0 conversion.
/// </content>
internal static partial class HeifYuv420ToRgb8Converter
{

136
tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ChromaFromLumaBenchmarks.cs

@ -0,0 +1,136 @@
// 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.Prediction.ChromaFromLuma;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
/// <summary>
/// Measures frame-wide AV1 chroma-from-luma prediction at each available intrinsic tier.
/// </summary>
[Config(typeof(Configuration))]
[MemoryDiagnoser(displayGenColumns: false)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class Av1ChromaFromLumaBenchmarks
{
/// <summary>
/// The coded frame width, which is an exact multiple of the maximum CfL block side.
/// </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 maximum CfL block side in chroma samples.
/// </summary>
private const int BlockSize = 32;
/// <summary>
/// The fixed-stride Q3 luma residual surface reused by each benchmark block.
/// </summary>
private readonly short[] lumaQ3 = new short[BlockSize * BlockSize];
/// <summary>
/// The frame-wide 8-bit DC prediction surface.
/// </summary>
private readonly byte[] destination8 = new byte[Width * Height];
/// <summary>
/// The frame-wide 12-bit DC prediction surface.
/// </summary>
private readonly short[] destination12 = new short[Width * Height];
/// <summary>
/// Populates deterministic Q3 residuals and DC predictions outside the measured traversal.
/// </summary>
[GlobalSetup]
public void Setup()
{
for (int index = 0; index < this.lumaQ3.Length; index++)
{
this.lumaQ3[index] = (short)(((index * 4051) % 65521) - 32760);
}
this.destination8.AsSpan().Fill(137);
this.destination12.AsSpan().Fill(2101);
}
/// <summary>
/// Measures frame-wide 8-bit chroma-from-luma prediction.
/// </summary>
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
[Benchmark]
[BenchmarkCategory("8Bit")]
public byte Predict8BitFrame()
{
for (int row = 0; row < Height; row += BlockSize)
{
for (int column = 0; column < Width; column += BlockSize)
{
Av1ChromaFromLumaPredictor.Predict(this.lumaQ3, this.destination8.AsSpan((row * Width) + column), Width, 11, BlockSize, BlockSize);
}
}
return this.destination8[^1];
}
/// <summary>
/// Measures frame-wide 12-bit chroma-from-luma prediction.
/// </summary>
/// <returns>The final reconstructed sample, keeping the frame output observable.</returns>
[Benchmark]
[BenchmarkCategory("12Bit")]
public short Predict12BitFrame()
{
for (int row = 0; row < Height; row += BlockSize)
{
for (int column = 0; column < Width; column += BlockSize)
{
Av1ChromaFromLumaPredictor.Predict(this.lumaQ3, this.destination12.AsSpan((row * Width) + column), Width, 11, 12, BlockSize, BlockSize);
}
}
return this.destination12[^1];
}
/// <summary>
/// Configures production-process measurements for the default, AVX2, Vector128, and scalar paths.
/// </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("Avx2")
.WithEnvironmentVariable("DOTNET_EnableAVX512F", "0"));
this.AddJob(
Job.ShortRun
.WithId("Vector128")
.WithEnvironmentVariable("DOTNET_EnableAVX512F", "0")
.WithEnvironmentVariable("DOTNET_EnableAVX2", "0"));
this.AddJob(
Job.ShortRun
.WithId("Scalar")
.WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0"));
}
}
}

377
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs

@ -1,18 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies AV1 chroma-from-luma storage, parameter derivation, and prediction.
/// </summary>
[Trait("Format", "Avif")]
public class Av1ChromaFromLumaTests
{
/// <summary>
/// The hardware configurations required to exercise each SIMD tier and the complete scalar fallback.
/// </summary>
private const HwIntrinsics PredictorConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// Verifies Q3 luma storage for every AV1 chroma-subsampling layout.
/// </summary>
[Theory]
[InlineData(false, false, new short[] { 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 })]
[InlineData(true, false, new short[] { 12, 28, 44, 60, 76, 92, 108, 124 })]
@ -30,6 +44,9 @@ public class Av1ChromaFromLumaTests
Assert.Equal(expected, GetBlock(context.Q3Buffer, width, height));
}
/// <summary>
/// Verifies that Q3 storage retains the complete 12-bit sample range.
/// </summary>
[Fact]
public void StoreHighBitDepthPreservesTwelveBitQ3Range()
{
@ -42,6 +59,9 @@ public class Av1ChromaFromLumaTests
Assert.All(GetBlock(context.Q3Buffer, 4, 4), value => Assert.Equal(32760, value));
}
/// <summary>
/// Verifies that sub-8-by-8 luma blocks are combined before the shared average is removed.
/// </summary>
[Fact]
public void StoreCombinesSub8x8LumaBeforeSubtractingAverage()
{
@ -66,6 +86,9 @@ public class Av1ChromaFromLumaTests
Assert.Equal(expected, GetBlock(context.Q3Buffer, 4, 4));
}
/// <summary>
/// Verifies that frame-edge extension precedes average subtraction.
/// </summary>
[Fact]
public void ComputeParametersPadsFrameEdgeBeforeSubtractingAverage()
{
@ -84,19 +107,24 @@ public class Av1ChromaFromLumaTests
Assert.Equal(0, actual.Sum(x => x));
}
/// <summary>
/// Verifies 8-bit CfL scaling and clipping with known values.
/// </summary>
[Fact]
public void Predict8BitAddsScaledLumaAndClips()
{
short[] lumaQ3 = new short[32 * 32];
new short[] { -64, -32, 64, 64 }.CopyTo(lumaQ3, 0);
byte[] dcPrediction = [0, 128, 250, 255];
byte[] destination = new byte[4];
byte[] destination = [128, 128, 128, 128];
Av1PredictionDecoder.ChromaFromLumaPredict(lumaQ3, dcPrediction, 4, destination, 4, 8, Av1BitDepth.EightBit, 4, 1);
Av1ChromaFromLumaPredictor.Predict(lumaQ3, destination, 4, 8, 4, 1);
Assert.Equal(new byte[] { 0, 124, 255, 255 }, destination);
Assert.Equal(new byte[] { 120, 124, 136, 136 }, destination);
}
/// <summary>
/// Verifies high-bit-depth CfL scaling and clipping with known values.
/// </summary>
[Theory]
[InlineData((int)Av1BitDepth.TenBit, 1023)]
[InlineData((int)Av1BitDepth.TwelveBit, 4095)]
@ -104,23 +132,31 @@ public class Av1ChromaFromLumaTests
{
short[] lumaQ3 = new short[32 * 32];
new short[] { -128, -64, 64, 128 }.CopyTo(lumaQ3, 0);
short[] dcPrediction = [5, (short)(maximum / 2), (short)(maximum - 5), maximum];
short[] destination = new short[4];
short dc = (short)(maximum / 2);
short[] destination = [dc, dc, dc, dc];
Av1PredictionDecoder.ChromaFromLumaPredict(
lumaQ3,
dcPrediction,
4,
destination,
4,
16,
(Av1BitDepth)bitDepthIndex,
4,
1);
Av1ChromaFromLumaPredictor.Predict(lumaQ3, destination, 4, 16, ((Av1BitDepth)bitDepthIndex).GetBitCount(), 4, 1);
Assert.Equal(new short[] { 0, (short)((maximum / 2) - 16), maximum, maximum }, destination);
Assert.Equal(new short[] { (short)(dc - 32), (short)(dc - 16), (short)(dc + 16), (short)(dc + 32) }, destination);
}
/// <summary>
/// Verifies exact 8-, 10-, and 12-bit CfL output and padding preservation across all intrinsic tiers.
/// </summary>
[Fact]
public void PredictMatchesIndependentDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePredictors, PredictorConfigurations);
/// <summary>
/// Verifies exact luma subsampling and average subtraction across all intrinsic tiers.
/// </summary>
[Fact]
public void ContextOperationsMatchIndependentDefinitionAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateContextOperations, PredictorConfigurations);
/// <summary>
/// Verifies the plane-specific alpha magnitude and joint-sign mapping.
/// </summary>
[Theory]
[InlineData((int)Av1Plane.U, 3)]
[InlineData((int)Av1Plane.V, 4)]
@ -138,6 +174,313 @@ public class Av1ChromaFromLumaTests
Assert.Equal(expected, actual);
}
/// <summary>
/// Exercises every valid CfL width, both alpha signs, clipping boundaries, and padded destination rows.
/// </summary>
private static void ValidatePredictors()
{
int[] widths = [4, 8, 16, 32];
int[] alphaValues = [-16, -9, 0, 7, 16];
foreach (int width in widths)
{
const int height = 8;
int stride = width + 5;
short[] lumaQ3 = CreateLumaSurface(height);
foreach (int alphaQ3 in alphaValues)
{
byte[] expected = CreateByteDestination(stride, height, 137);
byte[] actual = (byte[])expected.Clone();
ApplyReference(lumaQ3, expected, stride, alphaQ3, width, height);
Av1ChromaFromLumaPredictor.Predict(lumaQ3, actual, stride, alphaQ3, width, height);
Assert.Equal(expected, actual);
foreach (int bitDepth in new[] { 10, 12 })
{
short dc = (short)((1 << (bitDepth - 1)) + 53);
short[] expectedHigh = CreateHighBitDepthDestination(stride, height, dc);
short[] actualHigh = (short[])expectedHigh.Clone();
ApplyReference(lumaQ3, expectedHigh, stride, alphaQ3, bitDepth, width, height);
Av1ChromaFromLumaPredictor.Predict(lumaQ3, actualHigh, stride, alphaQ3, bitDepth, width, height);
Assert.Equal(expectedHigh, actualHigh);
}
}
}
}
/// <summary>
/// Exercises every CfL sampling layout, source width, sample precision, and padded minimum block extent.
/// </summary>
private static void ValidateContextOperations()
{
int[] sourceSizes = [4, 8, 16, 32];
int[] samplingLayouts = [0, 1, 3];
foreach (int sampling in samplingLayouts)
{
bool subX = (sampling & 1) != 0;
bool subY = (sampling & 2) != 0;
ObuColorConfig colorConfig = new() { SubSamplingX = subX, SubSamplingY = subY };
foreach (int sourceSize in sourceSizes)
{
int stride = sourceSize + 3;
Av1TransformSize sourceTransform = GetTransformSize(sourceSize, sourceSize);
int activeWidth = sourceSize >> (subX ? 1 : 0);
int activeHeight = sourceSize >> (subY ? 1 : 0);
int targetWidth = Math.Max(activeWidth, 4);
int targetHeight = Math.Max(activeHeight, 4);
Av1TransformSize targetTransform = GetTransformSize(targetWidth, targetHeight);
byte[] input = CreateByteInput(stride, sourceSize);
short[] expected = CreateStoredReference(input, stride, sourceSize, subX, subY);
PadAndSubtractAverage(expected, activeWidth, activeHeight, targetWidth, targetHeight);
Av1ChromaFromLumaContext context = new(colorConfig);
context.Store(input, stride, 0, 0, sourceTransform, sourceTransform.ToBlockSize(), 0, 0);
context.ComputeParameters(targetTransform);
Assert.Equal(expected, context.Q3Buffer);
short[] highInput = CreateHighBitDepthInput(stride, sourceSize);
expected = CreateStoredReference(highInput, stride, sourceSize, subX, subY);
PadAndSubtractAverage(expected, activeWidth, activeHeight, targetWidth, targetHeight);
context = new Av1ChromaFromLumaContext(colorConfig);
context.Store(highInput, stride, 0, 0, sourceTransform, sourceTransform.ToBlockSize(), 0, 0);
context.ComputeParameters(targetTransform);
Assert.Equal(expected, context.Q3Buffer);
}
}
}
/// <summary>
/// Maps dimensions used by the CfL operation matrix to their AV1 transform identifier.
/// </summary>
private static Av1TransformSize GetTransformSize(int width, int height)
=> (width, height) switch
{
(4, 4) => Av1TransformSize.Size4x4,
(8, 8) => Av1TransformSize.Size8x8,
(16, 16) => Av1TransformSize.Size16x16,
(32, 32) => Av1TransformSize.Size32x32,
(4, 8) => Av1TransformSize.Size4x8,
(8, 4) => Av1TransformSize.Size8x4,
(8, 16) => Av1TransformSize.Size8x16,
(16, 8) => Av1TransformSize.Size16x8,
(16, 32) => Av1TransformSize.Size16x32,
(32, 16) => Av1TransformSize.Size32x16,
_ => throw new InvalidOperationException(),
};
/// <summary>
/// Creates deterministic 8-bit luma rows with padding that must not contribute to CfL.
/// </summary>
private static byte[] CreateByteInput(int stride, int height)
{
byte[] result = Enumerable.Repeat((byte)251, stride * height).ToArray();
for (int row = 0; row < height; row++)
{
for (int column = 0; column < stride - 3; column++)
{
result[(row * stride) + column] = (byte)(((row * 67) + (column * 29) + 11) & byte.MaxValue);
}
}
return result;
}
/// <summary>
/// Creates deterministic 12-bit luma rows with padding that must not contribute to CfL.
/// </summary>
private static short[] CreateHighBitDepthInput(int stride, int height)
{
short[] result = Enumerable.Repeat((short)4095, stride * height).ToArray();
for (int row = 0; row < height; row++)
{
for (int column = 0; column < stride - 3; column++)
{
result[(row * stride) + column] = (short)(((row * 977) + (column * 353) + 101) & 4095);
}
}
return result;
}
/// <summary>
/// Produces the normative Q3 luma surface for 8-bit input.
/// </summary>
private static short[] CreateStoredReference(byte[] input, int stride, int size, bool subX, bool subY)
{
short[] result = new short[32 * 32];
int rowStep = subY ? 2 : 1;
int columnStep = subX ? 2 : 1;
int shift = subX ? (subY ? 1 : 2) : 3;
for (int row = 0; row < size; row += rowStep)
{
for (int column = 0; column < size; column += columnStep)
{
int sum = input[(row * stride) + column];
if (subX)
{
sum += input[(row * stride) + column + 1];
}
if (subY)
{
sum += input[((row + 1) * stride) + column] + input[((row + 1) * stride) + column + 1];
}
result[((row / rowStep) * 32) + (column / columnStep)] = (short)(sum << shift);
}
}
return result;
}
/// <summary>
/// Produces the normative Q3 luma surface for high-bit-depth input.
/// </summary>
private static short[] CreateStoredReference(short[] input, int stride, int size, bool subX, bool subY)
{
short[] result = new short[32 * 32];
int rowStep = subY ? 2 : 1;
int columnStep = subX ? 2 : 1;
int shift = subX ? (subY ? 1 : 2) : 3;
for (int row = 0; row < size; row += rowStep)
{
for (int column = 0; column < size; column += columnStep)
{
int sum = input[(row * stride) + column];
if (subX)
{
sum += input[(row * stride) + column + 1];
}
if (subY)
{
sum += input[((row + 1) * stride) + column] + input[((row + 1) * stride) + column + 1];
}
result[((row / rowStep) * 32) + (column / columnStep)] = (short)(sum << shift);
}
}
return result;
}
/// <summary>
/// Applies CfL edge extension and rounded average subtraction to an independently stored Q3 surface.
/// </summary>
private static void PadAndSubtractAverage(short[] buffer, int activeWidth, int activeHeight, int width, int height)
{
for (int row = 0; row < activeHeight; row++)
{
buffer.AsSpan((row * 32) + activeWidth, width - activeWidth).Fill(buffer[(row * 32) + activeWidth - 1]);
}
for (int row = activeHeight; row < height; row++)
{
buffer.AsSpan((row - 1) * 32, width).CopyTo(buffer.AsSpan(row * 32, width));
}
int sum = (width * height) >> 1;
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
sum += buffer[(row * 32) + column];
}
}
short average = (short)(sum >> (BitOperations.Log2((uint)width) + BitOperations.Log2((uint)height)));
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
buffer[(row * 32) + column] -= average;
}
}
}
/// <summary>
/// Creates a deterministic Q3 surface spanning the legal signed 12-bit CfL range.
/// </summary>
private static short[] CreateLumaSurface(int height)
{
short[] result = new short[32 * height];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < 32; column++)
{
result[(row * 32) + column] = (short)(((row * 7919) + (column * 4051)) % 65521 - 32760);
}
}
return result;
}
/// <summary>
/// Creates an 8-bit DC prediction with non-image padding sentinels.
/// </summary>
private static byte[] CreateByteDestination(int stride, int height, byte dc)
{
byte[] result = Enumerable.Repeat((byte)203, stride * height).ToArray();
for (int row = 0; row < height; row++)
{
result.AsSpan(row * stride, stride - 5).Fill(dc);
}
return result;
}
/// <summary>
/// Creates a high-bit-depth DC prediction with non-image padding sentinels.
/// </summary>
private static short[] CreateHighBitDepthDestination(int stride, int height, short dc)
{
short[] result = Enumerable.Repeat((short)-1, stride * height).ToArray();
for (int row = 0; row < height; row++)
{
result.AsSpan(row * stride, stride - 5).Fill(dc);
}
return result;
}
/// <summary>
/// Applies the AV1 signed Q3 rounding definition to an 8-bit destination.
/// </summary>
private static void ApplyReference(short[] lumaQ3, byte[] destination, int stride, int alphaQ3, int width, int height)
{
int dc = destination[0];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int product = alphaQ3 * lumaQ3[(row * 32) + column];
int scaled = product < 0 ? -((-product + 32) >> 6) : (product + 32) >> 6;
destination[(row * stride) + column] = (byte)Math.Clamp(dc + scaled, byte.MinValue, byte.MaxValue);
}
}
}
/// <summary>
/// Applies the AV1 signed Q3 rounding definition to a high-bit-depth destination.
/// </summary>
private static void ApplyReference(short[] lumaQ3, short[] destination, int stride, int alphaQ3, int bitDepth, int width, int height)
{
int dc = destination[0];
int maximum = (1 << bitDepth) - 1;
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int product = alphaQ3 * lumaQ3[(row * 32) + column];
int scaled = product < 0 ? -((-product + 32) >> 6) : (product + 32) >> 6;
destination[(row * stride) + column] = (short)Math.Clamp(dc + scaled, 0, maximum);
}
}
}
/// <summary>
/// Extracts the active rows from the fixed-stride CfL buffer.
/// </summary>
private static short[] GetBlock(short[] buffer, int width, int height)
{
short[] result = new short[width * height];

Loading…
Cancel
Save