Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/ImageSharp.Textures/Compression/Astc/AstcDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ namespace SixLabors.ImageSharp.Textures.Compression.Astc;
/// <remarks>
/// Image data is streamed from a source of ASTC blocks to a destination <see cref="Stream"/> of pixels, one
/// block-row band at a time, so peak memory is independent of the image height.
/// The decoder returns raw decoded values and does not apply any gamma or color-space
/// transform. Callers loading ASTC data from an sRGB-tagged container (e.g. a KTX file
/// with an *_SRGB_BLOCK format) are responsible for applying sRGB-to-linear conversion
/// downstream if they need linear values.
/// The decoder returns raw decoded values and does not apply an sRGB-to-linear transform.
/// </remarks>
public static class AstcDecoder
{
Expand Down Expand Up @@ -56,16 +53,24 @@ private interface IBandSerializer<TElement>
/// <param name="width">Image width in pixels.</param>
/// <param name="height">Image height in pixels.</param>
/// <param name="footprint">The ASTC block footprint.</param>
/// <param name="mode">LDR decode mode — linear (default) or sRGB endpoint expansion.</param>
/// <exception cref="EndOfStreamException">
/// Thrown if <paramref name="source"/> contains fewer bytes than the footprint requires.
/// </exception>
public static void DecompressImage(Stream source, Stream destination, int width, int height, Footprint footprint)
public static void DecompressImage(Stream source, Stream destination, int width, int height, Footprint footprint, LdrDecodeMode mode = LdrDecodeMode.Linear)
{
Guard.NotNull(source);
Guard.NotNull(destination);
ValidateStreamDecodeArgs(width, height);

DecodeToStream<LdrPipeline, byte, ByteBandSerializer>(source, destination, width, height, footprint);
if (mode == LdrDecodeMode.Srgb)
{
DecodeToStream<LdrPipeline<SrgbMode>, byte, ByteBandSerializer>(source, destination, width, height, footprint);
}
else
{
DecodeToStream<LdrPipeline<LinearMode>, byte, ByteBandSerializer>(source, destination, width, height, footprint);
}
}

/// <summary>
Expand All @@ -77,19 +82,22 @@ public static void DecompressImage(Stream source, Stream destination, int width,
/// <param name="width">Image width in pixels.</param>
/// <param name="height">Image height in pixels.</param>
/// <param name="footprint">The ASTC block footprint.</param>
/// <param name="mode">LDR decode mode — linear (default) or sRGB endpoint expansion.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A task that completes when the decode has finished.</returns>
/// <exception cref="EndOfStreamException">
/// Thrown if <paramref name="source"/> contains fewer bytes than the footprint requires.
/// </exception>
public static Task DecompressImageAsync(
Stream source, Stream destination, int width, int height, Footprint footprint, CancellationToken cancellationToken = default)
Stream source, Stream destination, int width, int height, Footprint footprint, LdrDecodeMode mode = LdrDecodeMode.Linear, CancellationToken cancellationToken = default)
{
Guard.NotNull(source);
Guard.NotNull(destination);
ValidateStreamDecodeArgs(width, height);

return DecodeToStreamAsync<LdrPipeline, byte, ByteBandSerializer>(source, destination, width, height, footprint, cancellationToken);
return mode == LdrDecodeMode.Srgb
? DecodeToStreamAsync<LdrPipeline<SrgbMode>, byte, ByteBandSerializer>(source, destination, width, height, footprint, cancellationToken)
: DecodeToStreamAsync<LdrPipeline<LinearMode>, byte, ByteBandSerializer>(source, destination, width, height, footprint, cancellationToken);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ internal static class FusedLdrBlockDecoder
/// <summary>
/// Fused LDR decode to a contiguous buffer.
/// Only handles single-partition, non-dual-plane, LDR blocks.
/// <typeparamref name="TMode"/> selects linear vs sRGB decode (ASTC spec §C.2.19).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
internal static void DecompressBlockFusedLdr(UInt128 bits, in BlockInfo info, Footprint footprint, Span<byte> buffer)
=> DecompressBlock(
internal static void DecompressBlockFusedLdr<TMode>(UInt128 bits, in BlockInfo info, Footprint footprint, Span<byte> buffer)
where TMode : struct, ILdrColorMode
=> DecompressBlock<TMode>(
bits,
in info,
footprint,
Expand All @@ -34,17 +36,19 @@ internal static void DecompressBlockFusedLdr(UInt128 bits, in BlockInfo info, Fo
/// <summary>
/// Fused LDR decode writing directly to image buffer at strided positions.
/// Only handles single-partition, non-dual-plane, LDR blocks.
/// <typeparamref name="TMode"/> selects linear vs sRGB decode (ASTC spec §C.2.19).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
internal static void DecompressBlockFusedLdrToImage(
internal static void DecompressBlockFusedLdrToImage<TMode>(
UInt128 bits,
in BlockInfo info,
Footprint footprint,
int dstBaseX,
int dstBaseY,
int imageWidth,
Span<byte> imageBuffer)
=> DecompressBlock(
where TMode : struct, ILdrColorMode
=> DecompressBlock<TMode>(
bits,
in info,
footprint,
Expand All @@ -54,35 +58,38 @@ internal static void DecompressBlockFusedLdrToImage(
dstRowStride: imageWidth * BlockInfo.ChannelsPerPixel);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecompressBlock(
private static void DecompressBlock<TMode>(
UInt128 bits,
in BlockInfo info,
Footprint footprint,
Span<byte> buffer,
int dstBaseX,
int dstBaseY,
int dstRowStride)
where TMode : struct, ILdrColorMode
{
// Up to 12×12 = 144 ints (576 bytes) for the largest 2D footprint per spec §C.2.4.
Span<int> texelWeights = stackalloc int[footprint.PixelCount];
ColorEndpointPair endpointPair = FusedBlockDecoder.DecodeFusedCore(bits, in info, footprint, texelWeights);
WriteLdrPixels(buffer, footprint, dstBaseX, dstBaseY, dstRowStride, in endpointPair, texelWeights);
WriteLdrPixels<TMode>(buffer, footprint, dstBaseX, dstBaseY, dstRowStride, in endpointPair, texelWeights);
}

/// <summary>
/// Writes a footprint-sized block of LDR pixels into <paramref name="buffer"/> at position
/// (<paramref name="dstBaseX"/>, <paramref name="dstBaseY"/>) with the given row stride.
/// Uses SIMD where hardware-accelerated; scalar otherwise.
/// Uses SIMD where hardware-accelerated; scalar otherwise. <typeparamref name="TMode"/>
/// selects linear vs sRGB decode (ASTC spec §C.2.19).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteLdrPixels(
private static void WriteLdrPixels<TMode>(
Span<byte> buffer,
Footprint footprint,
int dstBaseX,
int dstBaseY,
int dstRowStride,
in ColorEndpointPair endpointPair,
Span<int> texelWeights)
where TMode : struct, ILdrColorMode
{
int lowR = endpointPair.LdrLow.R, lowG = endpointPair.LdrLow.G, lowB = endpointPair.LdrLow.B, lowA = endpointPair.LdrLow.A;
int highR = endpointPair.LdrHigh.R, highG = endpointPair.LdrHigh.G, highB = endpointPair.LdrHigh.B, highA = endpointPair.LdrHigh.A;
Expand All @@ -107,7 +114,7 @@ private static void WriteLdrPixels(
texelWeights[texelIndex + 1],
texelWeights[texelIndex + 2],
texelWeights[texelIndex + 3]);
SimdHelpers.Write4PixelLdr(
SimdHelpers.Write4PixelLdr<TMode>(
buffer,
dstRowOffset + (pixelX * BlockInfo.ChannelsPerPixel),
lowR,
Expand All @@ -124,7 +131,7 @@ private static void WriteLdrPixels(

for (; pixelX < footprintWidth; pixelX++)
{
SimdHelpers.WriteSinglePixelLdr(
SimdHelpers.WriteSinglePixelLdr<TMode>(
buffer,
dstRowOffset + (pixelX * BlockInfo.ChannelsPerPixel),
lowR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ namespace SixLabors.ImageSharp.Textures.Compression.Astc.BlockDecoding;
/// <summary>
/// <see cref="IBlockPipeline{T}"/> implementation for the LDR (byte RGBA) decode profile
/// (ASTC spec §C.2.5 "LDR Mode"). HDR-mode blocks are reserved in the LDR profile per §C.2.25
/// and produce the error colour (magenta) per §C.2.19, §C.2.24.
/// and produce the error colour (magenta) per §C.2.19, §C.2.24. <typeparamref name="TMode"/>
/// selects linear vs sRGB decode (ASTC spec §C.2.19).
/// </summary>
internal readonly struct LdrPipeline : IBlockPipeline<byte>
internal readonly struct LdrPipeline<TMode> : IBlockPipeline<byte>
where TMode : struct, ILdrColorMode
{
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -38,17 +40,17 @@ public void WriteErrorColorClipped(
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FusedToImage(UInt128 blockBits, in BlockInfo info, Footprint footprint, int dstBaseX, int dstBaseY, int imageWidth, Span<byte> imageBuffer)
=> FusedLdrBlockDecoder.DecompressBlockFusedLdrToImage(blockBits, in info, footprint, dstBaseX, dstBaseY, imageWidth, imageBuffer);
=> FusedLdrBlockDecoder.DecompressBlockFusedLdrToImage<TMode>(blockBits, in info, footprint, dstBaseX, dstBaseY, imageWidth, imageBuffer);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FusedToScratch(UInt128 blockBits, in BlockInfo info, Footprint footprint, Span<byte> decodedPixels)
=> FusedLdrBlockDecoder.DecompressBlockFusedLdr(blockBits, in info, footprint, decodedPixels);
=> FusedLdrBlockDecoder.DecompressBlockFusedLdr<TMode>(blockBits, in info, footprint, decodedPixels);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogicalWrite(UInt128 blockBits, in BlockInfo info, Footprint footprint, Span<byte> decodedPixels)
=> LogicalBlock.DecodeToBytes(blockBits, in info, footprint, decodedPixels);
=> LogicalBlock.DecodeToBytes<TMode>(blockBits, in info, footprint, decodedPixels);

/// <summary>
/// Spec §C.2.19 error colour: opaque magenta <c>(0xFF, 0x00, 0xFF, 0xFF)</c> as UNORM8 RGBA.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ namespace SixLabors.ImageSharp.Textures.Compression.Astc.BlockDecoding;

/// <summary>
/// LDR <see cref="IPixelWriter{T}"/> — writes UNORM8 RGBA bytes via the scalar SIMD helpers.
/// <typeparamref name="TMode"/> selects linear vs sRGB decode (ASTC spec §C.2.19).
/// </summary>
internal readonly struct LdrPixelWriter : IPixelWriter<byte>
internal readonly struct LdrPixelWriter<TMode> : IPixelWriter<byte>
where TMode : struct, ILdrColorMode
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePixel(Span<byte> buffer, int offset, in ColorEndpointPair endpoint, int weight)
=> SimdHelpers.WriteSinglePixelLdr(
=> SimdHelpers.WriteSinglePixelLdr<TMode>(
buffer,
offset,
endpoint.LdrLow.R,
Expand All @@ -35,7 +37,7 @@ public void WritePixelDualPlane(
int primaryWeight,
int dualPlaneChannel,
int dualPlaneWeight)
=> SimdHelpers.WriteSinglePixelLdrDualPlane(
=> SimdHelpers.WriteSinglePixelLdrDualPlane<TMode>(
buffer,
offset,
endpoint.LdrLow.R,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ internal static class LogicalBlock
/// <summary>
/// Decodes a block to its UNORM8 RGBA pixels. HDR-endpoint blocks must not reach this
/// method: the LDR entry points in <see cref="AstcDecoder"/> reject HDR content per
/// ASTC spec §C.2.19, so every partition's endpoint here is LDR.
/// ASTC spec §C.2.19, so every partition's endpoint here is LDR. <typeparamref name="TMode"/>
/// selects linear vs sRGB decode (ASTC spec §C.2.19).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void DecodeToBytes(UInt128 bits, in BlockInfo info, Footprint footprint, Span<byte> pixels)
public static void DecodeToBytes<TMode>(UInt128 bits, in BlockInfo info, Footprint footprint, Span<byte> pixels)
where TMode : struct, ILdrColorMode
{
if (!info.IsValid)
{
Expand All @@ -30,15 +32,15 @@ public static void DecodeToBytes(UInt128 bits, in BlockInfo info, Footprint foot

if (info.DualPlane.Enabled && !info.IsVoidExtent)
{
DecodeDualPlane<LdrPixelWriter, byte>(bits, in info, footprint, pixels);
DecodeDualPlane<LdrPixelWriter<TMode>, byte>(bits, in info, footprint, pixels);
return;
}

// Up to 12×12 = 144 ints (576 bytes) for the largest 2D footprint per spec §C.2.4.
Span<int> weights = stackalloc int[footprint.PixelCount];
DecodedBlockState state = DecodeSinglePlane(bits, in info, footprint, weights);

WriteAllPixels<LdrPixelWriter, byte>(footprint, pixels, in state);
WriteAllPixels<LdrPixelWriter<TMode>, byte>(footprint, pixels, in state);
}

/// <summary>
Expand Down Expand Up @@ -295,7 +297,7 @@ private static void WriteAllPixelsDualPlane<TWriter, T>(
/// <summary>
/// Inline storage for up to 4 per-partition <see cref="ColorEndpointPair"/> values
/// (spec §C.2.10 caps partition count at 4). Used as a stack-local buffer to hold the
/// decoded endpoints during a single <see cref="DecodeToBytes"/>/<see cref="DecodeToFloats"/> call.
/// decoded endpoints during a single <see cref="DecodeToBytes{TMode}"/>/<see cref="DecodeToFloats"/> call.
/// </summary>
[InlineArray(BlockInfo.MaxPartitionCount)]
private struct EndpointBuffer
Expand Down
10 changes: 6 additions & 4 deletions src/ImageSharp.Textures/Compression/Astc/Core/Interpolation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ public static int BlendWeighted(int p0, int p1, int weight)
=> ((p0 * (64 - weight)) + (p1 * weight) + 32) / 64;

/// <summary>
/// LDR-to-UNORM16 blend: each 8-bit endpoint is bit-replicated to 16 bits
/// (<c>(p &lt;&lt; 8) | p</c>) per §C.2.19 before the weighted blend. Every LDR decode
/// path that produces 16-bit intermediate values goes through this primitive.
/// LDR-to-UNORM16 blend with linear bit-replication (§C.2.19).
/// </summary>
/// <remarks>
/// Used by the HDR output path where LDR channels always expand linearly. The LDR-output path
/// expands per channel via <see cref="ILdrColorMode"/> before calling <see cref="BlendWeighted"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int BlendLdrReplicated(int p0, int p1, int weight)
=> BlendWeighted((p0 << 8) | p0, (p1 << 8) | p1, weight);
=> BlendWeighted(LinearExpand.Expand(p0), LinearExpand.Expand(p1), weight);

/// <summary>
/// Normalises a UNORM16 value (clamped to [0, 0xFFFF]) to the [0.0, 1.0] float range.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Textures.Compression.Astc.Core;

#pragma warning disable SA1201 // Readability: keep each expansion strategy adjacent to the interface it implements.
#pragma warning disable SA1649 // Multiple small, tightly-related expansion strategies share one file.

/// <summary>
/// Linear LDR expansion (ASTC spec §C.2.19). Used for every channel in linear decode mode, for the
/// alpha channel in every mode, and for LDR channels borrowed by the HDR output path.
/// </summary>
internal readonly struct LinearExpand
{
/// <summary>
/// Expands an 8-bit component <paramref name="c"/> to its 16-bit form.
/// </summary>
/// <param name="c">The 8-bit component value.</param>
/// <returns>The 16-bit expanded value.</returns>
public static int Expand(int c) => (c << 8) | c;
}

/// <summary>
/// sRGB LDR expansion (ASTC spec §C.2.19). Used for the R, G, and B channels in sRGB decode mode.
/// </summary>
internal readonly struct SrgbExpand
{
/// <summary>
/// Expands an 8-bit component <paramref name="c"/> to its 16-bit form.
/// </summary>
/// <param name="c">The 8-bit component value.</param>
/// <returns>The 16-bit expanded value.</returns>
public static int Expand(int c) => (c << 8) | 0x80;
}

/// <summary>
/// An LDR decode mode's per-channel endpoint expansion (ASTC spec §C.2.19).
/// </summary>
internal interface ILdrColorMode
{
/// <summary>
/// Expands an 8-bit R, G, or B endpoint component to 16 bits.
/// </summary>
/// <param name="c">The 8-bit colour component value.</param>
/// <returns>The 16-bit expanded value.</returns>
public static abstract int ExpandColor(int c);

/// <summary>
/// Expands an 8-bit alpha endpoint component to 16 bits.
/// </summary>
/// <param name="c">The 8-bit alpha component value.</param>
/// <returns>The 16-bit expanded value.</returns>
public static abstract int ExpandAlpha(int c);
}

/// <summary>
/// Linear LDR decode mode
/// </summary>
internal readonly struct LinearMode : ILdrColorMode
{
/// <inheritdoc />
public static int ExpandColor(int c) => LinearExpand.Expand(c);

/// <inheritdoc />
public static int ExpandAlpha(int c) => LinearExpand.Expand(c);
}

/// <summary>
/// sRGB LDR decode mode: R, G, B use <see cref="SrgbExpand"/>; alpha stays <see cref="LinearExpand"/>.
/// ASTC spec §C.2.19, only the colour channels take the sRGB low byte.
/// </summary>
internal readonly struct SrgbMode : ILdrColorMode
{
/// <inheritdoc />
public static int ExpandColor(int c) => SrgbExpand.Expand(c);

/// <inheritdoc />
public static int ExpandAlpha(int c) => LinearExpand.Expand(c);
}

#pragma warning restore SA1201
#pragma warning restore SA1649
Loading
Loading