Skip to content

fix(Texture2DConverter): decode ASTC_HDR formats with the HDR profile - #380

Open
nichinichisou0609 wants to merge 1 commit into
K0lb3:masterfrom
nichinichisou0609:fix/astc-hdr-profile
Open

nichinichisou0609 wants to merge 1 commit into
K0lb3:masterfrom
nichinichisou0609:fix/astc-hdr-profile

Conversation

@nichinichisou0609

@nichinichisou0609 nichinichisou0609 commented Sep 26, 2026 •

Copy link
Copy Markdown

I ran into this with ASTC_HDR_6x6 textures from a Unity 6000.3 build.

_get_astc_context always creates the decoder context with ASTCProfile.LDR, and CONV_TABLE routes the ASTC_HDR_4x4 ... ASTC_HDR_12x12 formats through the same astc() call. astcenc does not decode HDR blocks in an LDR profile: a block with HDR endpoint modes comes out as its error colour, magenta (255, 0, 255, 255), and an HDR constant-colour (void-extent) block as NaN, which ends up as white (255, 255, 255, 255) in the 8-bit output. The textures I ran into contain only HDR blocks, so they decode to nothing but magenta and white; the synthetic example below comes out all magenta.

Reproduction without any game file:

import struct
import astc_encoder as a
from UnityPy.enums import TextureFormat as TF
from UnityPy.export.Texture2DConverter import CONV_TABLE

w = h = 12
px = [v for x in range(w * h) for v in (0.25 + 3.0 * (x % w) / (w - 1), 0.5, 0.1, 1.0)]
img = a.ASTCImage(a.ASTCType.F32, w, h, 1, struct.pack(f"<{len(px)}f", *px))
data = a.ASTCContext(a.ASTCConfig(a.ASTCProfile.HDR, 6, 6, block_z=1, quality=100)).compress(
    img, a.ASTCSwizzle.from_str("RGBA"))
func, args = CONV_TABLE[TF.ASTC_HDR_6x6]
print(set(func(data, w, h, *args).tobytes()[i:i + 4] for i in range(0, w * h * 4, 4)))
# before: {b'\xff\x00\xff\xff'}; after: the gradient, clamped to 255 above 1.0

Changes:

  • astc() / get_astc_context() take an optional hdr argument (default False, so existing callers are unchanged). With hdr=True the context uses ASTCProfile.HDR, without USE_DECODE_UNORM8, which astcenc only accepts for the LDR profiles. The context cache is keyed by it as well, so an HDR context is only created in a thread that decodes an HDR texture.
  • The ASTC_HDR_* entries of CONV_TABLE pass hdr=True.
  • The result is still an 8-bit RGBA image, so values above 1.0 are clamped, as for RGBAHalf and BC6H. Keeping the HDR range would need a non-Pillow output, as discussed in feat: Lock-free ASTC codec operations #342; this change only replaces the error colour with the clamped image. If you would rather keep these formats unsupported until such an output exists, I can change this PR to raise NotImplementedError for them instead, which at least avoids returning a magenta image that looks like valid data.
  • tests/test_astc.py encodes synthetic gradients with astc-encoder and decodes them through CONV_TABLE: HDR for all six block sizes (no error colour, values within 16 of the clamped source, clamping above 1.0) and LDR for three block sizes (within 4, unchanged by this PR). The HDR cases fail on master with the error colour.

The ASTC decoder context always used the LDR profile, so the HDR blocks of
an ASTC_HDR_* texture decoded to error colours: magenta for HDR endpoint
modes, white for HDR constant-colour blocks. The HDR formats now get a
context with the HDR profile, without the unorm8 decode flag, which is only
valid for the LDR profiles. Values above 1.0 are clamped in the 8-bit
image, as for the other HDR formats.

tests/test_astc.py encodes synthetic HDR and LDR gradients with
astc-encoder and decodes them through CONV_TABLE.

This branch has not been deployed

No deployments
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.

1 participant