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.

Cooking for shipping

simvx cook pre-transcodes UASTC .ktx2 sources to an explicit block format offline, baking the full mip chain into the cooked container, so shipped builds do zero transcode work at load time and never need the native transcoder extension:

simvx cook hero.ktx2 -o cooked/                 # UASTC -> explicit BC7 + mips
simvx cook scene.gltf -o cooked/                # cook everything a scene references
simvx cook hero.ktx2 -o cooked/ --target astc4x4  # mobile cook

A .gltf input carries every externally referenced buffer and image across (relative layout preserved, so the cooked scene loads as-is), cooking each UASTC .ktx2 reference along the way. Raw PNG/JPG references and explicit-format KTX2/DDS files copy unchanged: raw colour maps get their mip chains from the runtime mip generator, and explicit-format containers are already cooked. When the native transcoder is unavailable the cook degrades to a pass-through copy and says so in its report (mirroring the loading fallback chain below).

Cook per shipping target: web exports should keep the original UASTC .ktx2 (the browser transcodes per-device, see the target matrix below), so cook into a separate output directory rather than replacing your sources.

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). The extension’s name embeds a short hash of the BASISD_SUPPORT_* define-set (_simvx_basis_transcoder_<hash>), so changing the supported-format set rebuilds automatically: the new define-set maps to a new name, a stale .so is simply not found, and simvx build-textures compiles the current set instead of reusing a cached/installed binary.

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.