Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
*.astc filter=lfs diff=lfs merge=lfs -text
*.uastc filter=lfs diff=lfs merge=lfs -text
*.raw filter=lfs diff=lfs merge=lfs -text
*.pam filter=lfs diff=lfs merge=lfs -text
*.pbm filter=lfs diff=lfs merge=lfs -text
*.pgm filter=lfs diff=lfs merge=lfs -text
Expand Down
46 changes: 2 additions & 44 deletions src/ImageSharp.Textures/Compression/Astc/AstcDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ private static void DecodeBlockRow<TPipeline, T>(
UInt128 blockBits = ReadBlockBits(bandBlocks, blockX);

BlockInfo info = BlockModeDecoder.Decode(blockBits);
BlockDestination dest = ComputeBlockDestination(blockX, 0, footprint, destinationWidth, destinationHeight);
BlockDestination dest = BlockImageWriter.ComputeBlockDestination(blockX, 0, footprint, destinationWidth, destinationHeight);

// Spec §C.2.19, §C.2.24, §C.2.25: illegal block encodings, and HDR endpoint modes
// in the LDR profile, must produce the error colour (magenta) for every texel.
Expand Down Expand Up @@ -224,7 +224,7 @@ private static void DecodeBlock<TPipeline, T>(
pipeline.LogicalWrite(blockBits, in info, footprint, decodedPixels);
}

CopyBlockRect(decodedPixels, imageBuffer, footprint.Width, dest.CopyWidth, dest.CopyHeight, dest.DstBaseX, dest.DstBaseY, imageWidth);
BlockImageWriter.CopyBlockRect(decodedPixels, imageBuffer, footprint.Width, dest.CopyWidth, dest.CopyHeight, dest.DstBaseX, dest.DstBaseY, imageWidth);
}

/// <summary>
Expand Down Expand Up @@ -341,48 +341,6 @@ private static UInt128 ReadBlockBits(ReadOnlySpan<byte> astcData, int blockIndex
return BinaryPrimitives.ReadUInt128LittleEndian(astcData.Slice(offset, BlockInfo.SizeInBytes));
}

/// <summary>
/// Computes the destination rectangle for the block at (<paramref name="blockX"/>,
/// <paramref name="blockY"/>) given the image bounds, clipping the footprint extents
/// to fit inside the image.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BlockDestination ComputeBlockDestination(int blockX, int blockY, Footprint footprint, int width, int height)
{
int dstBaseX = blockX * footprint.Width;
int dstBaseY = blockY * footprint.Height;
int copyWidth = Math.Min(footprint.Width, width - dstBaseX);
int copyHeight = Math.Min(footprint.Height, height - dstBaseY);
bool isFullInterior = copyWidth == footprint.Width && copyHeight == footprint.Height;

return new BlockDestination(dstBaseX, dstBaseY, copyWidth, copyHeight, isFullInterior);
}

/// <summary>
/// Copies a decoded block from its scratch buffer into the image at the block's pixel
/// offset, row by row, clamped to the image bounds on right/bottom edges. The
/// <c>channels-per-pixel</c> factor is fixed at <see cref="BlockInfo.ChannelsPerPixel"/> (RGBA).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CopyBlockRect<T>(
ReadOnlySpan<T> source,
Span<T> destination,
int blockWidth,
int copyWidth,
int copyHeight,
int dstBaseX,
int dstBaseY,
int imageWidth)
{
int copyElements = copyWidth * BlockInfo.ChannelsPerPixel;
for (int pixelY = 0; pixelY < copyHeight; pixelY++)
{
int srcOffset = pixelY * blockWidth * BlockInfo.ChannelsPerPixel;
int dstOffset = (((dstBaseY + pixelY) * imageWidth) + dstBaseX) * BlockInfo.ChannelsPerPixel;
source.Slice(srcOffset, copyElements).CopyTo(destination.Slice(dstOffset, copyElements));
}
}

private readonly struct ByteBandSerializer : IBandSerializer<byte>
{
public int BytesPerElement => sizeof(byte);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Textures.Compression.Astc.Core;

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

/// <summary>
/// Geometry helpers for placing decoded fixed-footprint blocks into an output image
/// </summary>
internal static class BlockImageWriter
{
/// <summary>
/// Computes the destination rectangle for the block at (<paramref name="blockX"/>,
/// <paramref name="blockY"/>), clipping the footprint extents to fit inside the image.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockDestination ComputeBlockDestination(int blockX, int blockY, Footprint footprint, int width, int height)
{
int dstBaseX = blockX * footprint.Width;
int dstBaseY = blockY * footprint.Height;
int copyWidth = Math.Min(footprint.Width, width - dstBaseX);
int copyHeight = Math.Min(footprint.Height, height - dstBaseY);
bool isFullInterior = copyWidth == footprint.Width && copyHeight == footprint.Height;

return new BlockDestination(dstBaseX, dstBaseY, copyWidth, copyHeight, isFullInterior);
}

/// <summary>
/// Copies a decoded block from its scratch buffer into the image at the block's pixel
/// offset, row by row, clamped to the image bounds on right/bottom edges.
/// </summary>
/// <remarks>
/// The channels-per-pixel factor is fixed at <see cref="BlockInfo.ChannelsPerPixel"/> (RGBA).
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CopyBlockRect<T>(
ReadOnlySpan<T> source,
Span<T> destination,
int blockWidth,
int copyWidth,
int copyHeight,
int dstBaseX,
int dstBaseY,
int imageWidth)
{
int copyElements = copyWidth * BlockInfo.ChannelsPerPixel;
for (int pixelY = 0; pixelY < copyHeight; pixelY++)
{
int srcOffset = pixelY * blockWidth * BlockInfo.ChannelsPerPixel;
int dstOffset = (((dstBaseY + pixelY) * imageWidth) + dstBaseX) * BlockInfo.ChannelsPerPixel;
source.Slice(srcOffset, copyElements).CopyTo(destination.Slice(dstOffset, copyElements));
}
}

/// <summary>
/// Fills <paramref name="buffer"/> with the spec-mandated LDR error colour (ASTC spec §C.2.19).
/// </summary>
/// <param name="buffer">The buffer to fill. Must be a multiple of <see cref="BlockInfo.ChannelsPerPixel"/></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FillErrorColor(Span<byte> buffer)
{
for (int i = 0; i < buffer.Length; i += BlockInfo.ChannelsPerPixel)
{
buffer[i] = 0xFF;
buffer[i + 1] = 0x00;
buffer[i + 2] = 0xFF;
buffer[i + 3] = 0xFF;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void WriteErrorColorClipped(
for (int pixelY = 0; pixelY < copyHeight; pixelY++)
{
int dstOffset = (((dstBaseY + pixelY) * imageWidth) + dstBaseX) * BlockInfo.ChannelsPerPixel;
FillMagenta(imageBuffer.Slice(dstOffset, rowElements));
BlockImageWriter.FillErrorColor(imageBuffer.Slice(dstOffset, rowElements));
}
}

Expand All @@ -51,18 +51,4 @@ public void FusedToScratch(UInt128 blockBits, in BlockInfo info, Footprint footp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogicalWrite(UInt128 blockBits, in BlockInfo info, Footprint footprint, Span<byte> 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.
/// </summary>
private static void FillMagenta(Span<byte> buffer)
{
for (int i = 0; i < buffer.Length; i += BlockInfo.ChannelsPerPixel)
{
buffer[i] = 0xFF;
buffer[i + 1] = 0x00;
buffer[i + 2] = 0xFF;
buffer[i + 3] = 0xFF;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,73 @@ 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{TMode}"/>/<see cref="DecodeToFloats"/> call.
/// Writes a block's LDR pixels from decoded components
/// </summary>
/// <param name="footprint">The block's footprint</param>
/// <param name="endpoints">Per-partition colour endpoints</param>
/// <param name="partitionAssignment">Per-texel partition assignment</param>
/// <param name="weights">Per-texel weights</param>
/// <param name="pixels">The output pixel buffer</param>
public static void WriteDecodedLdr<TMode>(
Footprint footprint,
ReadOnlySpan<ColorEndpointPair> endpoints,
ReadOnlySpan<int> partitionAssignment,
Span<int> weights,
Span<byte> pixels)
where TMode : struct, ILdrColorMode
{
DecodedBlockState state = BuildState(endpoints, partitionAssignment, weights);
WriteAllPixels<LdrPixelWriter<TMode>, byte>(footprint, pixels, in state);
}

/// <summary>
/// Dual-plane variant of <see cref="WriteDecodedLdr{TMode}"/> (ASTC spec §C.2.20).
/// </summary>
/// <param name="footprint">The block's footprint</param>
/// <param name="endpoints">Per-partition colour endpoints</param>
/// <param name="partitionAssignment">Per-texel partition assignment</param>
/// <param name="weights">Per-texel weights</param>
/// <param name="secondaryWeights">The secondary plane's per-texel weights</param>
/// <param name="dualPlaneChannel">The channel whose per-texel weights are used instead of the primary plane's</param>
/// <param name="pixels">The output pixel buffer</param>
public static void WriteDecodedLdrDualPlane<TMode>(
Footprint footprint,
ReadOnlySpan<ColorEndpointPair> endpoints,
ReadOnlySpan<int> partitionAssignment,
Span<int> weights,
Span<int> secondaryWeights,
int dualPlaneChannel,
Span<byte> pixels)
where TMode : struct, ILdrColorMode
{
DecodedBlockState state = BuildState(endpoints, partitionAssignment, weights);
DualPlane dualPlane = new() { Weights = secondaryWeights, Channel = dualPlaneChannel };

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

/// <summary>
/// Assembles a <see cref="DecodedBlockState"/> from externally decoded per-subset endpoints,
/// per-texel partition assignment, and per-texel weights.
/// </summary>
private static DecodedBlockState BuildState(
ReadOnlySpan<ColorEndpointPair> endpoints,
ReadOnlySpan<int> partitionAssignment,
Span<int> weights)
{
DecodedBlockState state = default;
for (int i = 0; i < endpoints.Length; i++)
{
state.Endpoints[i] = endpoints[i];
}

state.Weights = weights;
state.PartitionAssignment = partitionAssignment;
return state;
}

/// <summary>
/// Inline storage for up to 4 per-partition <see cref="ColorEndpointPair"/> values (spec §C.2.10)
/// </summary>
[InlineArray(BlockInfo.MaxPartitionCount)]
private struct EndpointBuffer
Expand All @@ -308,9 +372,7 @@ private struct EndpointBuffer
}

/// <summary>
/// State common to single-plane and dual-plane blocks: per-partition endpoints, primary
/// per-texel weights, and the partition-assignment map. Stack-only — holds a stack-local
/// <see cref="EndpointBuffer"/> and a <see cref="Span{T}"/>.
/// State common to single-plane and dual-plane blocks
/// </summary>
private ref struct DecodedBlockState
{
Expand Down
6 changes: 6 additions & 0 deletions src/ImageSharp.Textures/Compression/Astc/Uastc/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
This product includes software derived from Basis Universal
(https://github.com/BinomialLLC/basis_universal), Copyright (c) 2016-2026
Binomial LLC, licensed under the Apache License, Version 2.0.

The files in this directory port the UASTC block-format tables and decode
logic from the Basis Universal transcoder to C#.
31 changes: 31 additions & 0 deletions src/ImageSharp.Textures/Compression/Astc/Uastc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# UASTC (LDR 4×4) decoding

This directory implements decoding of **UASTC** texture blocks to RGBA8.

## What UASTC is

UASTC ("Universal ASTC") is a block format from [Basis Universal](https://github.com/BinomialLLC/basis_universal). It is a fixed-rate, 128-bit-per-block, 4×4-texel LDR format designed as a high-quality intermediate that can be transcoded losslessly to several GPU formats (ASTC, BC7, ETC, …) at load time. In KTX2 it is identified by `vkFormat = VK_FORMAT_UNDEFINED` together with a Data Format Descriptor whose `colorModel = KHR_DF_MODEL_UASTC (166)`, and the block payload is frequently Zstandard- or zlib-supercompressed on top.

## How it relates to ASTC

UASTC is a constrained subset of LDR ASTC 4×4. Every UASTC block maps to a legal ASTC 4×4 block, and the conversion is lossless. But the two are **not bit-compatible**:

- **Block layout differs.** A UASTC block begins with a variable-length Huffman *mode* prefix (one of 19 modes, plus a reserved mode 19), followed by transcode-hint fields, an optional partition-pattern index, an optional dual-plane component selector, endpoint data, and weights. ASTC instead starts with its own 11-bit block-mode field. The two encodings share neither field order nor bit packing.
- **Endpoints** use a non-interleaved trit/quint BISE packing (trit/quint bundle codes first, then the per-value low bits), unlike ASTC's interleaved BISE.
- **Weights** are stored LSB-first, the anchor texel of each subset stores one fewer bit.
- **Partitions** use UASTC's own small pattern tables (shared with BC7), not the ASTC partition hash function.

Because UASTC endpoints and weights are still ASTC-style values, this decoder reuses the existing ASTC back end for everything past the bit-parsing stage: endpoint/weight unquantization (`Quantization`), interpolation and pixel writing (`LogicalBlock.WriteDecodedLdr` → `LdrPixelWriter` → `Interpolation`). It decodes **directly to RGBA** — it does not transcode to an ASTC block first. Notably it does not apply ASTC blue-contraction, Basis direct-unpack path omits it.

## Modes

All 19 LDR modes are decoded: solid color (mode 8), single- and multi-subset RGB (CEM 8), RGBA (CEM 12) and luminance-alpha (CEM 4, output swizzled to `{L,L,L,A}`), including the dual-plane modes (6, 11, 13, 17). Mode 19 is reserved and yields the error colour (magenta).

## Sources

The mode tables, partition/anchor tables, Huffman codes, BISE range table, and the decode logic are ported from Basis Universal (Apache-2.0), primarily:

- `transcoder/basisu_transcoder_uastc.h` — table/struct declarations and constants.
- `transcoder/basisu_transcoder.cpp` — `unpack_uastc(...)` decode logic and table values.

The format is specified by the **UASTC LDR 4×4 Texture Specification** (Binomial), a copy is committed alongside this README at [`UASTC-LDR-4x4-Texture-Specification.md`](./UASTC-LDR-4x4-Texture-Specification.md) for reference. The canonical source is the [basis_universal wiki](https://github.com/BinomialLLC/basis_universal/wiki/UASTC-LDR-4x4-Texture-Specification). KTX2/DFD signalling of UASTC is defined by the [KTX2 specification](https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html) and the Khronos Data Format Descriptor (`KHR_DF_MODEL_UASTC`). See the `NOTICE` file in this directory for attribution.
Loading
Loading