Source code for simvx.graphics._native.basis_transcoder_build

"""CFFI build script for the SimVX basis_universal UASTC transcoder.

Run once after install (or any time the vendored transcoder is updated):

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

Produces ``_simvx_basis_transcoder.<abi>.so`` next to this file. The runtime
wrapper ``simvx.graphics._native.basis_transcoder`` imports it.

``uv_build`` (the graphics build backend) doesn't run C-extension build hooks
at install time, so this script is invoked manually or via the
``simvx build-textures`` CLI subcommand (mirroring core's ``simvx build-audio``).

The vendored translation unit (``vendor/basis_universal/transcoder/
basisu_transcoder.cpp``) is C++, so the extension is compiled as a C++ TU
(``source_extension=".cpp"``) and the transcoder .cpp is passed as a separate
``sources`` entry so cffi compiles it to its own object and links it.
"""

from __future__ import annotations

import importlib
import os
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
GLUE_SOURCE = HERE / "_simvx_basis_glue.cpp"
# graphics native dir: .../graphics/src/simvx/graphics/_native
# parents[3] == .../graphics ; vendor lives there.
VENDOR_ROOT = (HERE.parents[3] / "vendor" / "basis_universal").resolve()
VENDOR_INCLUDE = VENDOR_ROOT / "transcoder"
TRANSCODER_CPP = VENDOR_INCLUDE / "basisu_transcoder.cpp"

# Define-set for the FULL desktop target matrix (phase T4). Disable the KTX2
# container parser (we feed raw UASTC blocks decompressed Python-side) and every
# UNUSED target, but ENABLE the three families the device probe can pick:
#   * BC7 mode 5            (near-universal desktop)
#   * ASTC LDR 4x4          (mobile/tablet; BASISD_SUPPORT_ASTC)
#   * ETC2 RGBA (EAC_A8)    (older mobile / WebGL-class; BASISD_SUPPORT_ETC2_EAC_A8)
# Re-enabling ASTC + ETC2 grows the .so and adds the ASTC table generation cost;
# acceptable per the full-matrix choice. BASISD_SUPPORT_ASTC_HIGHER_OPAQUE_QUALITY
# stays at its release default (0) so the ASTC tables remain 1x, not 2x.
# ETC2_RGBA needs only EAC_A8 (the alpha block) + the base ETC defines; RG11 is
# unused. KTX2/KTX2_ZSTD stay off (we still feed raw blocks), which also drops
# the Zstd link requirement.
#
# IMPORTANT: changing this set requires a FORCED rebuild of the .so (delete it or
# re-run `simvx build-textures`); uv reuses a cached/installed binary otherwise.
_DEFINES = [
    ("BASISD_SUPPORT_KTX2", "0"),
    ("BASISD_SUPPORT_KTX2_ZSTD", "0"),
    ("BASISD_SUPPORT_BC7", "1"),
    ("BASISD_SUPPORT_BC7_MODE5", "1"),
    ("BASISD_SUPPORT_ASTC", "1"),
    ("BASISD_SUPPORT_PVRTC1", "0"),
    ("BASISD_SUPPORT_PVRTC2", "0"),
    ("BASISD_SUPPORT_ATC", "0"),
    ("BASISD_SUPPORT_FXT1", "0"),
    ("BASISD_SUPPORT_DXT1", "0"),
    ("BASISD_SUPPORT_DXT5A", "0"),
    ("BASISD_SUPPORT_ETC2_EAC_RG11", "0"),
    ("BASISD_SUPPORT_ETC2_EAC_A8", "1"),
]


def _require_build_deps():
    """Validate cffi + setuptools are available before invoking cffi.

    Called from ``build()`` only, never at import time, so this module can be
    imported by docs tooling without the build dependencies installed.
    """
    try:
        cffi = importlib.import_module("cffi")
    except ImportError as exc:
        raise SystemExit(
            "cffi is required to build the basis transcoder extension. "
            "Install with `uv pip install cffi`."
        ) from exc

    try:
        importlib.import_module("setuptools")
    except ImportError as exc:
        raise SystemExit(
            "setuptools is required to build the basis transcoder extension on "
            "Python >= 3.12 (cffi >= 2.0). Install with `uv pip install setuptools` "
            "or `uv run --with setuptools simvx build-textures`."
        ) from exc

    return cffi


def _cdef() -> str:
    """C declarations exposed to Python via cffi (the extern-"C" glue surface)."""
    return """
    void simvx_basis_init(void);
    int simvx_basis_is_available(void);
    int simvx_basis_astc_available(void);
    int simvx_basis_etc2_available(void);
    unsigned int simvx_basis_target_bc7(void);
    unsigned int simvx_basis_target_astc4x4(void);
    unsigned int simvx_basis_target_etc2(void);
    unsigned long long simvx_basis_uastc_transcoded_size(unsigned int width, unsigned int height);
    int simvx_basis_transcode_uastc_to_bc7(const void* uastc_blocks, unsigned int num_blocks,
                                           void* out_bc7, unsigned long long out_size);
    int simvx_basis_transcode_uastc_to_astc4x4(const void* uastc_blocks, unsigned int num_blocks,
                                               void* out_astc, unsigned long long out_size);
    int simvx_basis_transcode_uastc_to_etc2(const void* uastc_blocks, unsigned int num_blocks,
                                            void* out_etc2, unsigned long long out_size);
    """


def _source() -> str:
    """C++ source cffi compiles into the extension: just #include the glue."""
    return f'#include "{GLUE_SOURCE.name}"\n'


def _extra_compile_args() -> list[str]:
    args = ["-O2", "-fvisibility=hidden", "-Wno-unused-function", "-Wno-unused-variable"]
    if sys.platform == "win32":
        args += ["/std:c++17"]
    else:
        args += ["-std=c++17", "-fPIC"]
    return args


def _extra_link_args() -> list[str]:
    # libstdc++ is auto-linked by the C++ compiler driver; just libm on linux.
    if sys.platform.startswith("linux"):
        return ["-lm"]
    return []


[docs] def build(output_dir: str | None = None, *, verbose: bool = True) -> Path: """Compile the extension. Returns the path to the built .so/.dll.""" cffi = _require_build_deps() if not GLUE_SOURCE.exists(): raise FileNotFoundError(f"Missing glue source: {GLUE_SOURCE}") if not TRANSCODER_CPP.exists(): raise FileNotFoundError(f"Missing vendored basisu_transcoder.cpp at {TRANSCODER_CPP}") out_dir = Path(output_dir) if output_dir else HERE out_dir.mkdir(parents=True, exist_ok=True) ffi = cffi.FFI() ffi.cdef(_cdef()) ffi.set_source( "_simvx_basis_transcoder", _source(), sources=[str(TRANSCODER_CPP)], # separate object, linked in include_dirs=[str(VENDOR_INCLUDE), str(HERE)], define_macros=_DEFINES, source_extension=".cpp", # emit a C++ TU (the transcoder is C++) extra_compile_args=_extra_compile_args(), extra_link_args=_extra_link_args(), ) cwd = os.getcwd() os.chdir(out_dir) try: if verbose: print(f"Compiling _simvx_basis_transcoder into {out_dir}/") result = ffi.compile(verbose=verbose) finally: os.chdir(cwd) built = Path(result) if verbose: print(f"Built: {built}") return built
[docs] def main() -> int: try: build() except Exception as exc: print(f"Build failed: {exc}", file=sys.stderr) return 1 return 0
if __name__ == "__main__": sys.exit(main())