"""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())