Skip to content

Conversation

@hybridherbst
Copy link
Contributor

Related issue: #31686

Description

Adds support for ASTC_6x6_SFLOAT_BLOCK in KTX2Loader. 4x4 blocks were already there.

This contribution is funded by Needle

@donmccurdy
Copy link
Collaborator

donmccurdy commented Aug 20, 2025

Thanks @hybridherbst!

I think what's implemented in this PR is support for plain ASTC 6x6 without the fallback to BC6H or RGBA16. We should certainly add the support for plain ASTC 6x6, but it's maybe not what you intended based on the "uastc" in the filename? The Basis Universal subset of ASTC HDR is identified by a specific color model in the header:

// Basis UASTC HDR is a subset of ASTC, which can be transcoded efficiently
// to BC6H. To detect whether a KTX2 file uses Basis UASTC HDR, or default
// ASTC, inspect the DFD color model.
//
// Source: https://github.com/BinomialLLC/basis_universal/issues/381
const isBasisHDR = container.vkFormat === VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT
&& container.dataFormatDescriptor[ 0 ].colorModel === 0xA7;
// If the device supports ASTC, Basis UASTC HDR requires no transcoder.
const needsTranscoder = container.vkFormat === VK_FORMAT_UNDEFINED
|| isBasisHDR && ! this.workerConfig.astcHDRSupported;
if ( ! needsTranscoder ) {
return createRawTexture( container );
}

I haven't tested but I assume it's the same color model for 6x6 as for 4x4, and you could just add one more vkFormat to the check above? Related:

Aside — do these files look as expected (same color as non-HDR textures) on the ktx2 example page? I'm not sure I understand the implications of the -hdr_ldr_upconversion_nit_multiplier flag offered by the basisu CLI yet, which seems to be on by default.

@hybridherbst
Copy link
Contributor Author

hybridherbst commented Aug 20, 2025

Hi @donmccurdy,

I was confused by that from the BasisU docs as well, since there's a mix of namings – sometimes UASTC HDR 6x6 is called just ASTC HDR 6x6 but still can be transcoded to BC6H, see https://github.com/BinomialLLC/basis_universal?tab=readme-ov-file#supported-hdr-gpu-texture-formats.

The HDR support notes here also make me believe the files are "uastc": https://github.com/BinomialLLC/basis_universal/wiki/UASTC-HDR-6x6-Support-Notes

Also here: https://github.com/BinomialLLC/basis_universal?tab=readme-ov-file#supported-texture-compression-modes

UASTC HDR 4x4 data is 100% standard ASTC texture data
UASTC HDR 6x6 data is 100% standard ASTC texture data.

In fact, when I run basisu -unpack <file>.ktx2 I get BC6H variants out as I would expect (I omitted the other mipmap layers here for clarity):

basis_uastc_hdr4x4.ktx2

basis_uastc_hdr4x4_hdr_unpacked_rgb_ASTC_HDR_4X4_RGBA_face_0_layer_0000-1.exr
basis_uastc_hdr4x4_hdr_unpacked_rgb_ASTC_HDR_4X4_RGBA_face_0_layer_0000.exr
basis_uastc_hdr4x4_hdr_unpacked_rgb_BC6H_face_0_layer_0000.exr
basis_uastc_hdr4x4_hdr_unpacked_rgb_RGB_9E5_level_0_face_0_layer_0000.exr
basis_uastc_hdr4x4_hdr_unpacked_rgb_RGB_HALF_level_0_face_0_layer_0000.exr
basis_uastc_hdr4x4_hdr_unpacked_rgba_RGBA_HALF_level_0_face_0_layer_0000.exr
basis_uastc_hdr4x4_transcoded_ASTC_HDR_4X4_RGBA_layer_0000.ktx
basis_uastc_hdr4x4_transcoded_BC6H_layer_0000.dds
basis_uastc_hdr4x4_transcoded_BC6H_layer_0000.ktx

basis_uastc_hdr6x6.ktx2

basis_uastc_hdr6x6_hdr_unpacked_rgb_ASTC_HDR_6X6_RGBA_face_0_layer_0000.exr
basis_uastc_hdr6x6_hdr_unpacked_rgb_BC6H_face_0_layer_0000.exr
basis_uastc_hdr6x6_hdr_unpacked_rgb_RGB_9E5_level_0_face_0_layer_0000.exr
basis_uastc_hdr6x6_hdr_unpacked_rgb_RGB_HALF_level_0_face_0_layer_0000.exr
basis_uastc_hdr6x6_hdr_unpacked_rgba_RGBA_HALF_level_0_face_0_layer_0000.exr
basis_uastc_hdr6x6_transcoded_ASTC_HDR_6X6_RGBA_layer_0000.ktx
basis_uastc_hdr6x6_transcoded_BC6H_layer_0000.dds
basis_uastc_hdr6x6_transcoded_BC6H_layer_0000.ktx

I agree that this conflicts with the code snippet you posted…

If you want to reproduce this, I added them to your KTX2-Samples repo as well:
donmccurdy/KTX2-Samples#3

@hybridherbst
Copy link
Contributor Author

I just noticed that the 6x6 compression logs "Mode: ASTC 6x6 HDR" whereas the 4x4 compression logs "Mode: UASTC 4x4 HDR". I also tried with an EXR input file with -hdr_6x6 and the -hdr_6x6i mode; neither adds the DND but both transcode to BC6H with -unpack.

% basisu -hdr_6x6 basis_uastc_hdr6x6.png

Basis Universal LDR/HDR GPU Texture Compression and Transcoding System v1.60.0
Copyright (C) 2019-2025 Binomial LLC, All rights reserved
No SSE, Multithreading: 1, Zstandard support: 1, OpenCL: 0
Processing 1 total file(s)
Processing source file "basis_uastc_hdr6x6.png"
Read source image "basis_uastc_hdr6x6.png", 40x40
Total slices: 1
Slice: 0, alpha: 0, orig width/height: 40x40, width/height: 40x40, first_block: 0, image_index: 0, mip_level: 0, iframe: 0
Mode: ASTC 6x6 HDR , Base Level: 0, Highest Level: 1, Lambda: 0.000000, REC 2020: false
Blurring image
Transforming to ITP
Wrote output .basis/.ktx2 file "basis_uastc_hdr6x6.ktx2"
Compression succeeded to file "basis_uastc_hdr6x6.ktx2" size 573 bytes in 0.006 secs
Total successes: 1 failures: 0
% basisu -hdr_4x4 basis_uastc_hdr4x4.png

Basis Universal LDR/HDR GPU Texture Compression and Transcoding System v1.60.0
Copyright (C) 2019-2025 Binomial LLC, All rights reserved
No SSE, Multithreading: 1, Zstandard support: 1, OpenCL: 0
Processing 1 total file(s)
Processing source file "basis_uastc_hdr4x4.png"
Read source image "basis_uastc_hdr4x4.png", 40x40
Total slices: 1
Slice: 0, alpha: 0, orig width/height: 40x40, width/height: 40x40, first_block: 0, image_index: 0, mip_level: 0, iframe: 0
Mode: UASTC 4x4 HDR Level 1
Wrote output .basis/.ktx2 file "basis_uastc_hdr4x4.ktx2"
Compression succeeded to file "basis_uastc_hdr4x4.ktx2" size 827 bytes in 0.009 secs
Total successes: 1 failures: 0

@richgel999 Is that expected? Is there a way to differentiate "regular ASTC 6x6 HDR" from "UASTC 6x6 HDR" like for the 4x4 version?

@donmccurdy
Copy link
Collaborator

@hybridherbst hoping to avoid being blocked here, maybe we can remove the examples from this PR and just merge the changes to KTX2Loader? Those changes look good regardless, it's just the validity of the samples I'm still a little confused about.

@hybridherbst hybridherbst force-pushed the feature/ktx2_astc_6x6_sfloat branch from 7af4854 to c3ad482 Compare August 22, 2025 08:48
@hybridherbst
Copy link
Contributor Author

hybridherbst commented Aug 22, 2025

Sure! I removed the 6x6 example and kept the 4x4 example because I believe that one is already correct (and was already working/supported before this PR).

@donmccurdy
Copy link
Collaborator

I'd be OK with keeping the 4x4 sample — but that still leaves the scaling issue, it's clipped to Notorious Six yellow. A scaling factor of 1 is probably what we'd want, if that gives floating point data in the range 0–1.

CleanShot 2025-08-22 at 14 55 44@2x

@hybridherbst
Copy link
Contributor Author

hybridherbst commented Aug 22, 2025

Fixed!

Command is now:

./basisu -hdr_4x4 basis_uastc_hdr4x4.png -mipmap -hdr_ldr_upconversion_nit_multiplier 1
image

(Note: for some reason the color on the HDR4x4 image looks slightly different to me, but I measured and it's the same #b49831 color value as the basis UASTC one. The etc1s version is expected to lose color correctness I believe.)

@donmccurdy donmccurdy merged commit c5e5c60 into mrdoob:dev Aug 22, 2025
8 checks passed
@donmccurdy
Copy link
Collaborator

Thanks for rolling with the feedback/questions, looks good to me now! :)

And yeah, not worried about color difference on the ETC1S example at this point. Possibly my encoding script could use better defaults.

@richgel999
Copy link

basisu -hdr_6x6 basis_uastc_hdr6x6.png

There are 2 different modes for 6x6 HDR (both use the same encoder however):

  • hdr_6x6: Outputs 100% standard ASTC 6x6 HDR texture data, with or without RDO conditioning for better compression. -lambda X controls the quality/rate tradeoff (higher=better). Can be transcoded to BC6H or various uncompressed HDR pixels formats. Good ranges to try for lambda value X are 300-5000 for HDR, and 600-20000 for upconverted LDR.
  • -hdr_6x6i: Outputs a custom intermediate format that transcodes to ASTC 6x6 HDR or BC6H and uncompressed HDR pixel formats. -lambda X controls the quality. Better compression than RDO, but slower decompression to ASTC/BC6H or uncompressed.

For 4x4 HDR:

  • -hdr_4x4: Encodes to 100% standard ASTC HDR 4x4 texture data, except this ASTC subset is also a common subset of BC6H which permits extremely fast transcoding to BC6H. Also supports transcoding to various uncompressed HDR pixel formats.

Note all the HDR encoders support automatically upconverting LDR/SDR to HDR images, by converting them to normalized linear light and multiplying by 80-100 nits. (This stores the exit radiances an sRGB monitor would emit into a scene referenced HDR file. There doesn't seem to be a standard multiplier, so we use 100 nits.) The exact upconversion multiplier used is stored in the KTX2 file and is configurable via command line or in the compressor's API.

Is there a way to differentiate "regular ASTC 6x6 HDR" from "UASTC 6x6 HDR" like for the 4x4 version?

Yes, if you compress the same .EXR to -hdr_4x4, -hdr_6x6, and -hdr_6x6i, and unpack each .KTX2 file using the "basisu" tool, you'll see the various fields that get placed into the KTX2 header/DFD.

-hdr_4x4:
Supercompression Format: UASTC_HDR_4x4
Supercompression Scheme: ZSTANDARD

-hdr_6x6:
Supercompression Format: ASTC_HDR_6x6
Supercompression Scheme: ZSTANDARD

-hdr_6x6i:
Supercompression Format: ASTC_HDR_6x6_INTERMEDIATE
Supercompression Scheme: BASISLZ

This output from the tool comes from examining the KTX2 header and the DFD. m_header.m_vk_format will equal basist::KTX2_FORMAT_ASTC_4x4_SFLOAT_BLOCK for 4x4 HDR, or KTX2_FORMAT_ASTC_6x6_SFLOAT_BLOCK for 6x6 HDR.

The DFD's color model is examined by the transcoder to determine the type of file. A DFD color model of KTX2_KDF_DF_MODEL_ASTC_HDR_6X6_INTERMEDIATE is the HDR 6x6 intermediate format. For this format, the KTX2 header's vk format will be KTX2_VK_FORMAT_UNDEFINED. (See the code transcoder/basisu_transcoder.cpp, method ktx2_transcoder::init().) The actual constants can be found by grepping the transcoder's source files.

Hope this helps.

@hybridherbst
Copy link
Contributor Author

hybridherbst commented Aug 22, 2025

Thank you @richgel999 for the additional info. I believe I've mostly read these bits of information on your docs/wiki pages already – great that these exist!

What I'm still confused about is the naming and what is supposed to be transcodable. I think my main confusion is "Why is nothing here named UASTC_HDR_6x6"?

  • UASTC_HDR_4x4, colorModel=KDF_DF_MODEL_UASTC_HDR (167): this is clear. The "U" tells me this is transcodable.

  • ASTC_HDR_6x6, colorModel=KHR_DF_MODEL_ASTC (162): not clear to me. It does not have the "U" prefix, and the colorModel does not specify HDR like the 4x4 one. The docs list it as transcodable, and basisu -unpack will produce various transcoded files, but trying to transcode the file from KTX2Loader in three.js fails.

  • ASTC_HDR_6x6_INTERMEDIATE, colorModel=KDF_DF_MODEL_ASTC_HDR_6X6_INTERMEDIATE (168): Always requires transocding. It also does not have the "U" prefix. basisu -unpack will produce various transcoded files. Trying to transcode the file from KTX2Loader also fails.

Some links I read through: KhronosGroup/KTX-Specification#216 (comment), BinomialLLC/basis_universal#381


@donmccurdy the transcoder errors look like this when I override needsTranscoder to true, for these files:
2d_astc_hdr6x6_and_i.zip
It looks like ASTC_HDR_6x6_INTERMEDIATE fails in ktx-parse already.

image

@donmccurdy
Copy link
Collaborator

donmccurdy commented Aug 24, 2025

Both ktx validate from KTX-Software, and my implementation in ktx-parse (JavaScript), are having trouble with the supercompression global data headers on the ASTC_HDR_6x6_INTERMEDIATE sample. The validator reports:

error-8102: Invalid sgdByteLength for BasisLZ/ETC1S. sgdByteLength must be consistent with image count and BasisLzGlobalHeader.
sgdByteLength is 48 but based on image count of 6 and the BasisLzGlobalHeader the expected value is 842 (20 + 20 * imageCount + endpointsByteLength + selectorsByteLength + tablesByteLength + extendedByteLength).

The implementation and the validation error make sense to me based on https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#basislz_gd — is there an expected difference versus the previous (LDR) BasisLZ supercompression scheme not yet in the KTX2 spec, or another document on the supercompression scheme?

@hybridherbst
Copy link
Contributor Author

hybridherbst commented Aug 24, 2025

I believe this is the most comprehensive writeup:
https://github.com/BinomialLLC/basis_universal/wiki/UASTC-HDR-6x6-Intermediate-File-Format-(Basis-GPU-Photo-6x6)

As the note on the top states, it's still a draft, I believe there are still some discussions on the spec:
KhronosGroup/KTX-Specification#216

@donmccurdy
Copy link
Collaborator

donmccurdy commented Aug 24, 2025

Ah, thanks! I think the first link describes the block-compression format and not the KTX2 SGD headers. The second link is probably where the SGD description might land, but isn't there yet. This might not yet be at a stage where I want to make changes in ktx-parse. It would be helpful to have a release candidate (or similar) for any KTX2 specification updates.

@richgel999
Copy link

richgel999 commented Sep 2, 2025

What I'm still confused about is the naming and what is supposed to be transcodable. I think my main confusion is "Why is nothing here named UASTC_HDR_6x6"?

The exact names for the HDR modes has been in flux. The "U" prefix was added to the docs but not everywhere in the code yet.

ASTC_HDR_6x6, colorModel=KHR_DF_MODEL_ASTC (162): not clear to me. It does not have the "U" prefix

This data (ASTC HDR 6x6 mode) is just 100% standard ASTC HDR data - so no "U" prefix needed. The basisu encoder can output either highest quality or RDO preconditioned data (using a non-zero Lambda setting to control the rate-distortion tradeoff). The transcoder can unpack this data and give you either BC6H, plain ASTC (just a memcpy), or a variety of uncompressed formats (RGB9E5, half float, etc.). This type of texture data is not specific to our system or encoder (because the block data follows the ASTC HDR spec exactly).

and the colorModel does not specify HDR like the 4x4 one.

The DFD's we write for the various HDR modes are in encoder/basisu_comp.cpp - see the g_ktx2_astc_hdr_6x6_nonalpha_dfd[] byte array with the comments for the various formats. The colorModel is set to "0xA2/162, standard ASTC, KTX2_KDF_DF_MODEL_ASTC". I can double and triple check that this is correct - I did contact Mark Callow at Khronos about this.

ASTC_HDR_6x6_INTERMEDIATE, colorModel=KDF_DF_MODEL_ASTC_HDR_6X6_INTERMEDIATE (168): Always requires transocding. It also does not have the "U" prefix. basisu -unpack will produce various transcoded files. Trying to transcode the file from KTX2Loader also fails.

I'll be adding the "U" prefix here to the next release (scheduled for early Jan.). This is our custom intermediate format for 6x6 HDR, which is directly based off ASTC HDR 6x6 but with a higher level structure to reduce per-block overhead/redundancies. This format is locked in stone (i.e. we won't be breaking compatibility). It gets substantially higher compression than plain ASTC HDR 6x6+LZ.

Note that all the HDR data generated by the basisu library is transcodable to BC6H, or various uncompressed HDR formats, or you can get plain ASTC 4x4 or 6x6 HDR.

@richgel999
Copy link

richgel999 commented Sep 2, 2025

The implementation and the validation error make sense to me based on https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#basislz_gd — is there an expected difference versus the previous (LDR) BasisLZ supercompression scheme not yet in the KTX2 spec, or another document on the supercompression scheme?

The HDR 6x6 intermediate mode is not in the KTX2 spec yet, so it's going to fail validation. This is not true of the plain ASTC HDR 6x6 mode, which is just 100% standard ASTC HDR texture data.

Unfortunately, HDR imaging is still not super well understood by most developers, so there hasn't been a big push to standardize the format yet. Scene-referenced half-float HDR at 1.5-2.5 bpp is far ahead of the adoption curve. We're releasing another major update to the library in early Jan., then we'll see if we can get a few major tech corps to help push for standardization. I am in contact with Mark Callow at Khronos and we're talking about this.

Note that the HDR intermediate format is already being utilized by at least one major corporation at a large scale, FWIW.

@donmccurdy
Copy link
Collaborator

Unfortunately, HDR imaging is still not super well understood by most developers, so there hasn't been a big push to standardize the format yet. Scene-referenced half-float HDR at 1.5-2.5 bpp is far ahead of the adoption curve.

Thanks @richgel999! Hoping to confirm what you mean here. I understand there's a big distinction between HDR imagery intended for output to display, and scene-referred floating-point RGB stimulus data that happens to be stored in textures, intended for use throughout a rendering pipeline. I'd consider HEIC or UltraHDR suitable quality-wise for HDR images but very lossy for floating-point data, given perception-based assumptions in compression. I'd consider OpenEXR suitable in quality for both uses, but larger than necessary in filesize for HDR images.

Does a similar distinction hold up for (U)ASTC HDR? I believe BasisU supports the "HDR imagery" case, primarily?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants