Source code for simvx.graphics._native.basis_transcoder

"""Python wrapper around the compiled ``_simvx_basis_transcoder`` cffi extension.

Exposes a tiny Pythonic surface over the vendored basis_universal transcoder:
:func:`transcode_uastc_to_bc7` / :func:`transcode_uastc_to_astc4x4` /
:func:`transcode_uastc_to_etc2` turn one UASTC level's packed 16-byte blocks
into BC7 / ASTC-4x4 / ETC2-RGBA blocks (all 16 B/block). Used by
``simvx.graphics.assets.ktx2_loader`` to load single-source UASTC KTX2 textures
on the desktop, with ``TextureManager`` picking the target per device caps.

Build the extension once after install:

    uv run python -m simvx.graphics._native.basis_transcoder_build

If the .so is missing, this module still imports cleanly (the ImportError is
swallowed): callers check :func:`is_available` and fall through to a warn + -1
sentinel, never a crash. Mirrors ``simvx.core._native.miniaudio_engine``.
"""

from __future__ import annotations

import logging
import os
import sys
from pathlib import Path
from typing import Any

log = logging.getLogger(__name__)

__all__ = [
    "BasisTranscoderUnavailable",
    "is_available",
    "astc_available",
    "etc2_available",
    "refresh",
    "transcode_uastc_to_bc7",
    "transcode_uastc_to_astc4x4",
    "transcode_uastc_to_etc2",
    "ensure_built",
]


[docs] class BasisTranscoderUnavailable(RuntimeError): """The compiled ``_simvx_basis_transcoder`` extension was not found. Run ``uv run python -m simvx.graphics._native.basis_transcoder_build`` (or ``simvx build-textures``) to compile it once, with a C++ compiler present. """
def _try_import_extension() -> tuple[Any | None, Any | None]: """Import the compiled extension, returning (ffi, lib) or (None, None).""" here = Path(__file__).resolve().parent # cffi names the module ``_simvx_basis_transcoder`` (no package prefix), so # the extension dir must be importable. if str(here) not in sys.path: sys.path.insert(0, str(here)) try: import _simvx_basis_transcoder as _ext # type: ignore[import-not-found] except ImportError: return None, None return _ext.ffi, _ext.lib _FFI, _LIB = _try_import_extension() _INITIALISED = False
[docs] def is_available() -> bool: """True if the native extension was built, loaded, and ships the BC7 target.""" return _LIB is not None and int(_LIB.simvx_basis_is_available()) == 1
[docs] def astc_available() -> bool: """True if the extension shipped the UASTC->ASTC-4x4 target (BASISD_SUPPORT_ASTC). A BC7-only build (the stale define-set) returns False here, so callers can verify the full-matrix .so actually shipped rather than silently skipping. """ return _LIB is not None and int(_LIB.simvx_basis_astc_available()) == 1
[docs] def etc2_available() -> bool: """True if the extension shipped the UASTC->ETC2-RGBA target (BASISD_SUPPORT_ETC2_EAC_A8).""" return _LIB is not None and int(_LIB.simvx_basis_etc2_available()) == 1
[docs] def refresh() -> bool: """Re-run the extension import, e.g. after a successful auto-build.""" global _FFI, _LIB, _INITIALISED _FFI, _LIB = _try_import_extension() _INITIALISED = False return is_available()
[docs] def ensure_built() -> bool: """Attempt a one-shot build of the extension if it is not already available. Honours ``SIMVX_SKIP_TEXTURE_BUILD``: when set, the build is skipped and the current availability is returned unchanged. Any build failure (no compiler, missing cffi/setuptools, etc.) is swallowed: the extension simply stays unavailable and the caller degrades gracefully. Returns the post-attempt availability. """ if is_available(): return True if os.environ.get("SIMVX_SKIP_TEXTURE_BUILD"): return False try: from . import basis_transcoder_build # noqa: PLC0415 basis_transcoder_build.build(verbose=False) except Exception as exc: # noqa: BLE001 - first-use best effort log.debug("basis transcoder first-use build failed: %s", exc) return False return refresh()
def _require_lib(): if _LIB is None: raise BasisTranscoderUnavailable( "The basis transcoder extension is not built. Run:\n" " uv run python -m simvx.graphics._native.basis_transcoder_build\n" "or `simvx build-textures` (a C++ compiler is required)." ) return _LIB def _ensure_init() -> None: """Idempotent global table init (the transcoder needs it before any call).""" global _INITIALISED if not _INITIALISED: _require_lib().simvx_basis_init() _INITIALISED = True def _transcode_uastc(uastc_bytes: bytes, width: int, height: int, *, fn_name: str, target: str) -> bytes: """Shared UASTC -> 16-byte-target transcode (BC7 / ASTC-4x4 / ETC2-RGBA). All three targets pack 16 bytes per 4x4 block, so the size math and output buffer are identical; only the native fn differs (``fn_name``). ``target`` names the format for error messages. Raises :class:`BasisTranscoderUnavailable` if the extension is absent, or ``ValueError`` on a size/transcode failure. """ lib = _require_lib() _ensure_init() blocks_x = max(1, (width + 3) // 4) blocks_y = max(1, (height + 3) // 4) num_blocks = blocks_x * blocks_y expected_in = num_blocks * 16 if len(uastc_bytes) != expected_in: raise ValueError( f"UASTC level size mismatch: {width}x{height} needs {expected_in} bytes " f"({num_blocks} blocks x 16), got {len(uastc_bytes)}" ) out_size = int(lib.simvx_basis_uastc_transcoded_size(width, height)) out = bytearray(out_size) rc = int( getattr(lib, fn_name)( _FFI.from_buffer(uastc_bytes), num_blocks, _FFI.from_buffer(out), out_size ) ) if rc != 0: raise ValueError(f"UASTC->{target} transcode failed (code {rc}) for {width}x{height}") return bytes(out)
[docs] def transcode_uastc_to_bc7(uastc_bytes: bytes, width: int, height: int) -> bytes: """Transcode one UASTC level's packed blocks to BC7 (16 B/block). ``uastc_bytes`` is the tightly-packed 16-byte UASTC blocks for a level of ``width`` x ``height`` texels. Returns ``num_blocks * 16`` bytes of BC7. """ return _transcode_uastc(uastc_bytes, width, height, fn_name="simvx_basis_transcode_uastc_to_bc7", target="BC7")
[docs] def transcode_uastc_to_astc4x4(uastc_bytes: bytes, width: int, height: int) -> bytes: """Transcode one UASTC level's packed blocks to ASTC LDR 4x4 (16 B/block). Requires the .so to have shipped the ASTC target (:func:`astc_available`). """ return _transcode_uastc(uastc_bytes, width, height, fn_name="simvx_basis_transcode_uastc_to_astc4x4", target="ASTC-4x4")
[docs] def transcode_uastc_to_etc2(uastc_bytes: bytes, width: int, height: int) -> bytes: """Transcode one UASTC level's packed blocks to ETC2 RGBA (EAC_A8, 16 B/block). Requires the .so to have shipped the ETC2 target (:func:`etc2_available`). """ return _transcode_uastc(uastc_bytes, width, height, fn_name="simvx_basis_transcode_uastc_to_etc2", target="ETC2-RGBA")