Skip to content

Commit 52432a7

Browse files
committed
Add support for UASTC decoding
1 parent 13bb80a commit 52432a7

61 files changed

Lines changed: 3466 additions & 68 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@
124124
*.ktx filter=lfs diff=lfs merge=lfs -text
125125
*.ktx2 filter=lfs diff=lfs merge=lfs -text
126126
*.astc filter=lfs diff=lfs merge=lfs -text
127+
*.uastc filter=lfs diff=lfs merge=lfs -text
128+
*.raw filter=lfs diff=lfs merge=lfs -text
127129
*.pam filter=lfs diff=lfs merge=lfs -text
128130
*.pbm filter=lfs diff=lfs merge=lfs -text
129131
*.pgm filter=lfs diff=lfs merge=lfs -text

‎src/ImageSharp.Textures/Compression/Astc/AstcDecoder.cs‎

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ private static void DecodeBlockRow<TPipeline, T>(
174174
UInt128 blockBits = ReadBlockBits(bandBlocks, blockX);
175175

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

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

227-
CopyBlockRect(decodedPixels, imageBuffer, footprint.Width, dest.CopyWidth, dest.CopyHeight, dest.DstBaseX, dest.DstBaseY, imageWidth);
227+
BlockImageWriter.CopyBlockRect(decodedPixels, imageBuffer, footprint.Width, dest.CopyWidth, dest.CopyHeight, dest.DstBaseX, dest.DstBaseY, imageWidth);
228228
}
229229

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

344-
/// <summary>
345-
/// Computes the destination rectangle for the block at (<paramref name="blockX"/>,
346-
/// <paramref name="blockY"/>) given the image bounds, clipping the footprint extents
347-
/// to fit inside the image.
348-
/// </summary>
349-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
350-
private static BlockDestination ComputeBlockDestination(int blockX, int blockY, Footprint footprint, int width, int height)
351-
{
352-
int dstBaseX = blockX * footprint.Width;
353-
int dstBaseY = blockY * footprint.Height;
354-
int copyWidth = Math.Min(footprint.Width, width - dstBaseX);
355-
int copyHeight = Math.Min(footprint.Height, height - dstBaseY);
356-
bool isFullInterior = copyWidth == footprint.Width && copyHeight == footprint.Height;
357-
358-
return new BlockDestination(dstBaseX, dstBaseY, copyWidth, copyHeight, isFullInterior);
359-
}
360-
361-
/// <summary>
362-
/// Copies a decoded block from its scratch buffer into the image at the block's pixel
363-
/// offset, row by row, clamped to the image bounds on right/bottom edges. The
364-
/// <c>channels-per-pixel</c> factor is fixed at <see cref="BlockInfo.ChannelsPerPixel"/> (RGBA).
365-
/// </summary>
366-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
367-
private static void CopyBlockRect<T>(
368-
ReadOnlySpan<T> source,
369-
Span<T> destination,
370-
int blockWidth,
371-
int copyWidth,
372-
int copyHeight,
373-
int dstBaseX,
374-
int dstBaseY,
375-
int imageWidth)
376-
{
377-
int copyElements = copyWidth * BlockInfo.ChannelsPerPixel;
378-
for (int pixelY = 0; pixelY < copyHeight; pixelY++)
379-
{
380-
int srcOffset = pixelY * blockWidth * BlockInfo.ChannelsPerPixel;
381-
int dstOffset = (((dstBaseY + pixelY) * imageWidth) + dstBaseX) * BlockInfo.ChannelsPerPixel;
382-
source.Slice(srcOffset, copyElements).CopyTo(destination.Slice(dstOffset, copyElements));
383-
}
384-
}
385-
386344
private readonly struct ByteBandSerializer : IBandSerializer<byte>
387345
{
388346
public int BytesPerElement => sizeof(byte);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Runtime.CompilerServices;
5+
using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
6+
7+
namespace SixLabors.ImageSharp.Textures.Compression.Astc.BlockDecoding;
8+
9+
/// <summary>
10+
/// Geometry helpers for placing decoded fixed-footprint blocks into an output image
11+
/// </summary>
12+
internal static class BlockImageWriter
13+
{
14+
/// <summary>
15+
/// Computes the destination rectangle for the block at (<paramref name="blockX"/>,
16+
/// <paramref name="blockY"/>), clipping the footprint extents to fit inside the image.
17+
/// </summary>
18+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
19+
public static BlockDestination ComputeBlockDestination(int blockX, int blockY, Footprint footprint, int width, int height)
20+
{
21+
int dstBaseX = blockX * footprint.Width;
22+
int dstBaseY = blockY * footprint.Height;
23+
int copyWidth = Math.Min(footprint.Width, width - dstBaseX);
24+
int copyHeight = Math.Min(footprint.Height, height - dstBaseY);
25+
bool isFullInterior = copyWidth == footprint.Width && copyHeight == footprint.Height;
26+
27+
return new BlockDestination(dstBaseX, dstBaseY, copyWidth, copyHeight, isFullInterior);
28+
}
29+
30+
/// <summary>
31+
/// Copies a decoded block from its scratch buffer into the image at the block's pixel
32+
/// offset, row by row, clamped to the image bounds on right/bottom edges.
33+
/// </summary>
34+
/// <remarks>
35+
/// The channels-per-pixel factor is fixed at <see cref="BlockInfo.ChannelsPerPixel"/> (RGBA).
36+
/// </remarks>
37+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
38+
public static void CopyBlockRect<T>(
39+
ReadOnlySpan<T> source,
40+
Span<T> destination,
41+
int blockWidth,
42+
int copyWidth,
43+
int copyHeight,
44+
int dstBaseX,
45+
int dstBaseY,
46+
int imageWidth)
47+
{
48+
int copyElements = copyWidth * BlockInfo.ChannelsPerPixel;
49+
for (int pixelY = 0; pixelY < copyHeight; pixelY++)
50+
{
51+
int srcOffset = pixelY * blockWidth * BlockInfo.ChannelsPerPixel;
52+
int dstOffset = (((dstBaseY + pixelY) * imageWidth) + dstBaseX) * BlockInfo.ChannelsPerPixel;
53+
source.Slice(srcOffset, copyElements).CopyTo(destination.Slice(dstOffset, copyElements));
54+
}
55+
}
56+
57+
/// <summary>
58+
/// Fills <paramref name="buffer"/> with the spec-mandated LDR error colour (ASTC spec §C.2.19).
59+
/// </summary>
60+
/// <param name="buffer">The buffer to fill. Must be a multiple of <see cref="BlockInfo.ChannelsPerPixel"/></param>
61+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
62+
public static void FillErrorColor(Span<byte> buffer)
63+
{
64+
for (int i = 0; i < buffer.Length; i += BlockInfo.ChannelsPerPixel)
65+
{
66+
buffer[i] = 0xFF;
67+
buffer[i + 1] = 0x00;
68+
buffer[i + 2] = 0xFF;
69+
buffer[i + 3] = 0xFF;
70+
}
71+
}
72+
}

‎src/ImageSharp.Textures/Compression/Astc/BlockDecoding/LdrPipeline.cs‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void WriteErrorColorClipped(
3333
for (int pixelY = 0; pixelY < copyHeight; pixelY++)
3434
{
3535
int dstOffset = (((dstBaseY + pixelY) * imageWidth) + dstBaseX) * BlockInfo.ChannelsPerPixel;
36-
FillMagenta(imageBuffer.Slice(dstOffset, rowElements));
36+
BlockImageWriter.FillErrorColor(imageBuffer.Slice(dstOffset, rowElements));
3737
}
3838
}
3939

@@ -51,18 +51,4 @@ public void FusedToScratch(UInt128 blockBits, in BlockInfo info, Footprint footp
5151
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5252
public void LogicalWrite(UInt128 blockBits, in BlockInfo info, Footprint footprint, Span<byte> decodedPixels)
5353
=> LogicalBlock.DecodeToBytes<TMode>(blockBits, in info, footprint, decodedPixels);
54-
55-
/// <summary>
56-
/// Spec §C.2.19 error colour: opaque magenta <c>(0xFF, 0x00, 0xFF, 0xFF)</c> as UNORM8 RGBA.
57-
/// </summary>
58-
private static void FillMagenta(Span<byte> buffer)
59-
{
60-
for (int i = 0; i < buffer.Length; i += BlockInfo.ChannelsPerPixel)
61-
{
62-
buffer[i] = 0xFF;
63-
buffer[i + 1] = 0x00;
64-
buffer[i + 2] = 0xFF;
65-
buffer[i + 3] = 0xFF;
66-
}
67-
}
6854
}

‎src/ImageSharp.Textures/Compression/Astc/BlockDecoding/LogicalBlock.cs‎

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,73 @@ private static void WriteAllPixelsDualPlane<TWriter, T>(
295295
}
296296

297297
/// <summary>
298-
/// Inline storage for up to 4 per-partition <see cref="ColorEndpointPair"/> values
299-
/// (spec §C.2.10 caps partition count at 4). Used as a stack-local buffer to hold the
300-
/// decoded endpoints during a single <see cref="DecodeToBytes{TMode}"/>/<see cref="DecodeToFloats"/> call.
298+
/// Writes a block's LDR pixels from decoded components
299+
/// </summary>
300+
/// <param name="footprint">The block's footprint</param>
301+
/// <param name="endpoints">Per-partition colour endpoints</param>
302+
/// <param name="partitionAssignment">Per-texel partition assignment</param>
303+
/// <param name="weights">Per-texel weights</param>
304+
/// <param name="pixels">The output pixel buffer</param>
305+
public static void WriteDecodedLdr<TMode>(
306+
Footprint footprint,
307+
ReadOnlySpan<ColorEndpointPair> endpoints,
308+
ReadOnlySpan<int> partitionAssignment,
309+
Span<int> weights,
310+
Span<byte> pixels)
311+
where TMode : struct, ILdrColorMode
312+
{
313+
DecodedBlockState state = BuildState(endpoints, partitionAssignment, weights);
314+
WriteAllPixels<LdrPixelWriter<TMode>, byte>(footprint, pixels, in state);
315+
}
316+
317+
/// <summary>
318+
/// Dual-plane variant of <see cref="WriteDecodedLdr{TMode}"/> (ASTC spec §C.2.20).
319+
/// </summary>
320+
/// <param name="footprint">The block's footprint</param>
321+
/// <param name="endpoints">Per-partition colour endpoints</param>
322+
/// <param name="partitionAssignment">Per-texel partition assignment</param>
323+
/// <param name="weights">Per-texel weights</param>
324+
/// <param name="secondaryWeights">The secondary plane's per-texel weights</param>
325+
/// <param name="dualPlaneChannel">The channel whose per-texel weights are used instead of the primary plane's</param>
326+
/// <param name="pixels">The output pixel buffer</param>
327+
public static void WriteDecodedLdrDualPlane<TMode>(
328+
Footprint footprint,
329+
ReadOnlySpan<ColorEndpointPair> endpoints,
330+
ReadOnlySpan<int> partitionAssignment,
331+
Span<int> weights,
332+
Span<int> secondaryWeights,
333+
int dualPlaneChannel,
334+
Span<byte> pixels)
335+
where TMode : struct, ILdrColorMode
336+
{
337+
DecodedBlockState state = BuildState(endpoints, partitionAssignment, weights);
338+
DualPlane dualPlane = new() { Weights = secondaryWeights, Channel = dualPlaneChannel };
339+
340+
WriteAllPixelsDualPlane<LdrPixelWriter<TMode>, byte>(footprint, pixels, in state, in dualPlane);
341+
}
342+
343+
/// <summary>
344+
/// Assembles a <see cref="DecodedBlockState"/> from externally decoded per-subset endpoints,
345+
/// per-texel partition assignment, and per-texel weights.
346+
/// </summary>
347+
private static DecodedBlockState BuildState(
348+
ReadOnlySpan<ColorEndpointPair> endpoints,
349+
ReadOnlySpan<int> partitionAssignment,
350+
Span<int> weights)
351+
{
352+
DecodedBlockState state = default;
353+
for (int i = 0; i < endpoints.Length; i++)
354+
{
355+
state.Endpoints[i] = endpoints[i];
356+
}
357+
358+
state.Weights = weights;
359+
state.PartitionAssignment = partitionAssignment;
360+
return state;
361+
}
362+
363+
/// <summary>
364+
/// Inline storage for up to 4 per-partition <see cref="ColorEndpointPair"/> values (spec §C.2.10)
301365
/// </summary>
302366
[InlineArray(BlockInfo.MaxPartitionCount)]
303367
private struct EndpointBuffer
@@ -308,9 +372,7 @@ private struct EndpointBuffer
308372
}
309373

310374
/// <summary>
311-
/// State common to single-plane and dual-plane blocks: per-partition endpoints, primary
312-
/// per-texel weights, and the partition-assignment map. Stack-only — holds a stack-local
313-
/// <see cref="EndpointBuffer"/> and a <see cref="Span{T}"/>.
375+
/// State common to single-plane and dual-plane blocks
314376
/// </summary>
315377
private ref struct DecodedBlockState
316378
{
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
This product includes software derived from Basis Universal
2+
(https://github.com/BinomialLLC/basis_universal), Copyright (c) 2016-2026
3+
Binomial LLC, licensed under the Apache License, Version 2.0.
4+
5+
The files in this directory port the UASTC block-format tables and decode
6+
logic from the Basis Universal transcoder to C#.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# UASTC (LDR 4×4) decoding
2+
3+
This directory implements decoding of **UASTC** texture blocks to RGBA8.
4+
5+
## What UASTC is
6+
7+
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.
8+
9+
## How it relates to ASTC
10+
11+
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**:
12+
13+
- **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.
14+
- **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.
15+
- **Weights** are stored LSB-first, the anchor texel of each subset stores one fewer bit.
16+
- **Partitions** use UASTC's own small pattern tables (shared with BC7), not the ASTC partition hash function.
17+
18+
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.
19+
20+
## Modes
21+
22+
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).
23+
24+
## Sources
25+
26+
The mode tables, partition/anchor tables, Huffman codes, BISE range table, and the decode logic are ported from Basis Universal (Apache-2.0), primarily:
27+
28+
- `transcoder/basisu_transcoder_uastc.h` — table/struct declarations and constants.
29+
- `transcoder/basisu_transcoder.cpp` — `unpack_uastc(...)` decode logic and table values.
30+
31+
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.

0 commit comments

Comments
 (0)