Compressed Textures (BC / ASTC / ETC2)

SimVX loads GPU block-compressed textures from .ktx2 (KTX 2.0) and .dds containers, transcoding the universal UASTC interchange format to whatever block family the current GPU prefers. Block-compressed textures use a fraction of the VRAM of RGBA8 and stay compressed on the GPU, so they are the right choice for shipped game assets.

A runnable example is at examples/features/3d/compressed_texture.py (uv run python examples/features/3d/compressed_texture.py).

Authoring

Encode offline once, ship the compressed file:

  • UASTC .ktx2 (recommended interchange). Transcodes to any GPU family at load time:

    toktx --uastc --genmipmap --t2 out.ktx2 in.png
    

    --uastc selects UASTC LDR 4x4, --genmipmap builds the mip chain, --t2 writes a KTX2 container.

  • Explicit-BC .dds (for BC-only desktop targets) via basisu, DirectX texconv, or AMD Compressonator. The VkFormat is carried in the file; no transcode happens.

Prefer UASTC .ktx2: one asset transcodes to BC7 on desktop, ASTC-4x4 on mobile/tablet, and ETC2 on older mobile, with no per-platform re-encode.

Loading

There is no special API. .ktx2 / .dds are accepted transparently anywhere a texture source is accepted; the texture manager dispatches on the file suffix (or the magic bytes for in-memory sources):

mat = Material(albedo_map="hero.ktx2")     # UASTC or explicit-BC KTX2
spr = Sprite2D(texture="tiles.dds")        # explicit-BC DDS

Fallback chain

Loading never crashes; it degrades step by step:

  1. Native transcode. A UASTC .ktx2 is transcoded to the device’s chosen block target via the native basis_universal transcoder. The target is picked by a device probe in this order, each gated by BOTH the coarse compression-family feature AND a per-format SAMPLED check: BC7 -> ASTC-4x4 -> ETC2. Explicit-BC files skip the probe (they carry their own VkFormat).

  2. CPU decode. If the GPU exposes no usable block family (or the native transcoder is not built), mip 0 is decoded to RGBA8 on the CPU via the optional texture2ddecoder package and uploaded as R8G8B8A8_UNORM.

  3. Skip. If even the CPU decoder/dependency is absent, a one-time WARNING is logged and the texture resolves to the -1 “couldn’t resolve” sentinel.

Mip sampling

When a .ktx2 / .dds carries a mip chain, the whole chain is uploaded and the bound sampler’s maxLod is set to mip_count - 1, so minified surfaces sample the smaller levels (no aliasing). Single-mip textures keep maxLod = 0 unchanged. Generate the chain at authoring time with --genmipmap.

Optional dependency + build

The native transcoder is an opt-in C++ extension built once after install:

simvx build-textures      # requires a C++ compiler (g++ / clang / MSVC)

texture2ddecoder is the pip-installable CPU-decode fallback (uv pip install texture2ddecoder). Both are optional: with neither present, compressed textures degrade gracefully (see the fallback chain). See Installation for the dependency matrix.

Re-enabling the ASTC and ETC2 transcode targets grows the built .so and adds the ASTC table-generation cost at compile time (acceptable for the full platform matrix). Changing the transcoder’s define-set requires a forced rebuild: delete the cached _simvx_basis_transcoder*.so (or re-run simvx build-textures), because uv reuses the cached/installed binary otherwise.

Per-platform target matrix

Platform

Block family

Notes

Desktop (PC)

BC7

Near-universal; the desktop probe picks it first.

Mobile / tablet

ASTC-4x4

Modern GPUs (Adreno, Mali, Apple).

Older mobile / WebGL-class

ETC2

Widely supported baseline.

Web runtime

UASTC (in-browser)

The web export ships raw UASTC and the browser/WebGPU transcodes per-device via the basis_universal WebAssembly transcoder.

The desktop probe order is BC7 -> ASTC-4x4 -> ETC2 -> CPU-decode. ASTC sizes other than 4x4 are out of scope (their larger texel footprint would break the 4-texel block-row math).

sRGB vs UNORM authoring

  • Colour maps (albedo): author as sRGB. The KTX2 DFD transfer function sets the flag; the loader reads it and picks the _SRGB_BLOCK VkFormat so the texture is gamma-decoded on sample.

  • Data maps (normal / roughness / metallic / AO): author as UNORM / linear (no sRGB flag) so they are sampled raw, not gamma-decoded.

Note: the CPU-decode fallback uploads R8G8B8A8_UNORM, so an sRGB source loses sRGB-on-sample in that degraded path. This is acceptable; the GPU-native transcode path is colour-correct.