mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-06-08 00:23:52 +00:00
736 lines
31 KiB
Python
736 lines
31 KiB
Python
# basisu_py/transcoder.py
|
|
import numpy as np
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from basisu_py.constants import (
|
|
TranscoderTextureFormat,
|
|
)
|
|
|
|
import importlib
|
|
import ctypes
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Enum to select backend
|
|
# ---------------------------------------------------------------------------
|
|
class TranscoderBackend:
|
|
NATIVE = "native"
|
|
WASM = "wasm"
|
|
AUTO = "auto"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wrapper class storing pointer+handle
|
|
# ---------------------------------------------------------------------------
|
|
@dataclass
|
|
class KTX2Handle:
|
|
ptr: int
|
|
handle: int
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main Transcoder class
|
|
# ---------------------------------------------------------------------------
|
|
class Transcoder:
|
|
def __init__(self, backend=TranscoderBackend.AUTO):
|
|
self._native = None
|
|
self._wasm = None
|
|
self.backend_name = None
|
|
self.backend = None
|
|
|
|
use_native = False
|
|
|
|
# ------------------------------------------------------------------
|
|
# Try native backend first if AUTO or NATIVE
|
|
# ------------------------------------------------------------------
|
|
if backend in (TranscoderBackend.AUTO, TranscoderBackend.NATIVE):
|
|
try:
|
|
native_mod = importlib.import_module("basisu_py.basisu_transcoder_python")
|
|
native_mod.init()
|
|
self._native = native_mod
|
|
self.backend = native_mod
|
|
self.backend_name = "NATIVE"
|
|
use_native = True
|
|
print("[Transcoder] Using native backend")
|
|
except Exception as e:
|
|
if backend == TranscoderBackend.NATIVE:
|
|
# Caller explicitly requested native - fail hard
|
|
raise RuntimeError(f"Native transcoder backend failed: {e}")
|
|
print("[Transcoder] Native backend unavailable, reason:", e)
|
|
self._native = None
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fallback to WASM if native is not being used
|
|
# ------------------------------------------------------------------
|
|
if not use_native:
|
|
try:
|
|
from basisu_py.wasm.wasm_transcoder import BasisuWasmTranscoder
|
|
except Exception as e:
|
|
raise RuntimeError(
|
|
f"WASM backend cannot be imported: {e}\n"
|
|
"Ensure that:\n"
|
|
" - 'wasmtime' is installed\n"
|
|
" - basisu_py/wasm/*.wasm files are present in the install\n"
|
|
)
|
|
|
|
wasm_path = Path(__file__).parent / "wasm" / "basisu_transcoder_module_st.wasm"
|
|
self._wasm = BasisuWasmTranscoder(str(wasm_path))
|
|
self._wasm.load()
|
|
self.backend = self._wasm
|
|
self.backend_name = "WASM"
|
|
print("[Transcoder] Using WASM backend")
|
|
|
|
# Finally, bind the unified API to whichever backend we chose
|
|
self._bind_backend(self.backend)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Unified backend binding (native or wasm)
|
|
# -----------------------------------------------------------------------
|
|
def _bind_backend(self, b):
|
|
self.backend = b
|
|
|
|
# ------------------ memory operations ------------------
|
|
memory_mapping = [
|
|
("_alloc", "alloc"),
|
|
("_free", "free"),
|
|
("_write", "write_memory"),
|
|
("_read", "read_memory"),
|
|
]
|
|
|
|
# ------------------ KTX2 core ------------------
|
|
basis_mapping = [
|
|
# basis_tex_format helpers
|
|
("basis_tex_format_is_xuastc_ldr", "basis_tex_format_is_xuastc_ldr"),
|
|
("basis_tex_format_is_astc_ldr", "basis_tex_format_is_astc_ldr"),
|
|
("basis_tex_format_get_block_width", "basis_tex_format_get_block_width"),
|
|
("basis_tex_format_get_block_height", "basis_tex_format_get_block_height"),
|
|
("basis_tex_format_is_hdr", "basis_tex_format_is_hdr"),
|
|
("basis_tex_format_is_ldr", "basis_tex_format_is_ldr"),
|
|
|
|
# transcoder_texture_format helpers
|
|
("basis_get_bytes_per_block_or_pixel", "basis_get_bytes_per_block_or_pixel"),
|
|
("basis_transcoder_format_has_alpha", "basis_transcoder_format_has_alpha"),
|
|
("basis_transcoder_format_is_hdr", "basis_transcoder_format_is_hdr"),
|
|
("basis_transcoder_format_is_ldr", "basis_transcoder_format_is_ldr"),
|
|
("basis_transcoder_texture_format_is_astc", "basis_transcoder_texture_format_is_astc"),
|
|
("basis_transcoder_format_is_uncompressed", "basis_transcoder_format_is_uncompressed"),
|
|
("basis_get_uncompressed_bytes_per_pixel", "basis_get_uncompressed_bytes_per_pixel"),
|
|
("basis_get_block_width", "basis_get_block_width"),
|
|
("basis_get_block_height", "basis_get_block_height"),
|
|
("basis_get_transcoder_texture_format_from_basis_tex_format","basis_get_transcoder_texture_format_from_basis_tex_format"),
|
|
("basis_is_format_supported", "basis_is_format_supported"),
|
|
("basis_compute_transcoded_image_size_in_bytes","basis_compute_transcoded_image_size_in_bytes"),
|
|
]
|
|
|
|
ktx2_mapping = [
|
|
|
|
("ktx2_open", "ktx2_open"),
|
|
("ktx2_close", "ktx2_close"),
|
|
|
|
("ktx2_get_width", "ktx2_get_width"),
|
|
("ktx2_get_height", "ktx2_get_height"),
|
|
("ktx2_get_levels", "ktx2_get_levels"),
|
|
("ktx2_get_faces", "ktx2_get_faces"),
|
|
("ktx2_get_layers", "ktx2_get_layers"),
|
|
|
|
("ktx2_get_basis_tex_format", "ktx2_get_basis_tex_format"),
|
|
|
|
("ktx2_get_block_width", "ktx2_get_block_width"),
|
|
("ktx2_get_block_height", "ktx2_get_block_height"),
|
|
|
|
("ktx2_has_alpha", "ktx2_has_alpha"),
|
|
|
|
# flags
|
|
("ktx2_is_hdr", "ktx2_is_hdr"),
|
|
("ktx2_is_hdr_4x4", "ktx2_is_hdr_4x4"),
|
|
("ktx2_is_hdr_6x6", "ktx2_is_hdr_6x6"),
|
|
("ktx2_is_ldr", "ktx2_is_ldr"),
|
|
("ktx2_is_srgb", "ktx2_is_srgb"),
|
|
("ktx2_is_etc1s", "ktx2_is_etc1s"),
|
|
("ktx2_is_uastc_ldr_4x4", "ktx2_is_uastc_ldr_4x4"),
|
|
("ktx2_is_xuastc_ldr", "ktx2_is_xuastc_ldr"),
|
|
("ktx2_is_astc_ldr", "ktx2_is_astc_ldr"),
|
|
("ktx2_is_video", "ktx2_is_video"),
|
|
("ktx2_get_ldr_hdr_upconversion_nit_multiplier", "ktx2_get_ldr_hdr_upconversion_nit_multiplier"),
|
|
|
|
# DFD access
|
|
("ktx2_get_dfd_flags", "ktx2_get_dfd_flags"),
|
|
("ktx2_get_dfd_total_samples", "ktx2_get_dfd_total_samples"),
|
|
("ktx2_get_dfd_channel_id0", "ktx2_get_dfd_channel_id0"),
|
|
("ktx2_get_dfd_channel_id1", "ktx2_get_dfd_channel_id1"),
|
|
("ktx2_get_dfd_color_model", "ktx2_get_dfd_color_model"),
|
|
("ktx2_get_dfd_color_primaries", "ktx2_get_dfd_color_primaries"),
|
|
("ktx2_get_dfd_transfer_func", "ktx2_get_dfd_transfer_func"),
|
|
|
|
# per-level info
|
|
("ktx2_get_level_orig_width", "ktx2_get_level_orig_width"),
|
|
("ktx2_get_level_orig_height", "ktx2_get_level_orig_height"),
|
|
("ktx2_get_level_actual_width", "ktx2_get_level_actual_width"),
|
|
("ktx2_get_level_actual_height", "ktx2_get_level_actual_height"),
|
|
|
|
("ktx2_get_level_num_blocks_x", "ktx2_get_level_num_blocks_x"),
|
|
("ktx2_get_level_num_blocks_y", "ktx2_get_level_num_blocks_y"),
|
|
("ktx2_get_level_total_blocks", "ktx2_get_level_total_blocks"),
|
|
|
|
("ktx2_get_level_alpha_flag", "ktx2_get_level_alpha_flag"),
|
|
("ktx2_get_level_iframe_flag", "ktx2_get_level_iframe_flag"),
|
|
|
|
# transcoding
|
|
("ktx2_start_transcoding", "ktx2_start_transcoding"),
|
|
("ktx2_transcode_image_level", "ktx2_transcode_image_level"),
|
|
|
|
# version
|
|
("get_version_fn", "get_version"),
|
|
]
|
|
|
|
# Apply all mappings
|
|
for public_name, backend_name in (memory_mapping + ktx2_mapping + basis_mapping):
|
|
setattr(self, public_name, getattr(b, backend_name))
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Public version query
|
|
# -----------------------------------------------------------------------
|
|
def get_version(self):
|
|
return self.get_version_fn()
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Enable library debug printing to stdout (also set BASISU_FORCE_DEVEL_MESSAGES to 1 in transcoder/basisu.h)
|
|
# -----------------------------------------------------------------------
|
|
def enable_debug_printf(self, flag: bool = True):
|
|
return self.backend.enable_debug_printf(flag)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# KTX2 Handle API: open/close + all queries
|
|
# -----------------------------------------------------------------------
|
|
def open(self, ktx2_bytes: bytes) -> KTX2Handle:
|
|
ptr = self._alloc(len(ktx2_bytes))
|
|
self._write(ptr, ktx2_bytes)
|
|
handle = self.ktx2_open(ptr, len(ktx2_bytes))
|
|
return KTX2Handle(ptr, handle)
|
|
|
|
def close(self, ktx2_handle: KTX2Handle):
|
|
self.ktx2_close(ktx2_handle.handle)
|
|
self._free(ktx2_handle.ptr)
|
|
|
|
# ---- Basic queries ----
|
|
def get_width(self, ktx2_handle: KTX2Handle):
|
|
return self.ktx2_get_width(ktx2_handle.handle)
|
|
|
|
def get_height(self, ktx2_handle: KTX2Handle):
|
|
return self.ktx2_get_height(ktx2_handle.handle)
|
|
|
|
def get_levels(self, ktx2_handle: KTX2Handle):
|
|
return self.ktx2_get_levels(ktx2_handle.handle)
|
|
|
|
def get_faces(self, ktx2_handle: KTX2Handle):
|
|
return self.ktx2_get_faces(ktx2_handle.handle)
|
|
|
|
def get_layers(self, ktx2_handle: KTX2Handle):
|
|
return self.ktx2_get_layers(ktx2_handle.handle)
|
|
|
|
def get_basis_tex_format(self, ktx2_handle: KTX2Handle):
|
|
return self.ktx2_get_basis_tex_format(ktx2_handle.handle)
|
|
|
|
def has_alpha(self, ktx2_handle: KTX2Handle) -> bool:
|
|
"""
|
|
Return true if the KTX2 container has alpha.
|
|
"""
|
|
return bool(self.ktx2_has_alpha(ktx2_handle.handle))
|
|
|
|
# ---- Format flags ----
|
|
def is_hdr(self, ktx2_handle): return bool(self.ktx2_is_hdr(ktx2_handle.handle))
|
|
def is_hdr_4x4(self, ktx2_handle): return bool(self.ktx2_is_hdr_4x4(ktx2_handle.handle))
|
|
def is_hdr_6x6(self, ktx2_handle): return bool(self.ktx2_is_hdr_6x6(ktx2_handle.handle))
|
|
def is_ldr(self, ktx2_handle): return bool(self.ktx2_is_ldr(ktx2_handle.handle))
|
|
def is_srgb(self, ktx2_handle): return bool(self.ktx2_is_srgb(ktx2_handle.handle))
|
|
def is_video(self, ktx2_handle): return bool(self.ktx2_is_video(ktx2_handle.handle))
|
|
def get_ldr_hdr_upconversion_nit_multiplier(self, ktx2_handle): return self.ktx2_get_ldr_hdr_upconversion_nit_multiplier(ktx2_handle.handle)
|
|
def is_etc1s(self, ktx2_handle): return bool(self.ktx2_is_etc1s(ktx2_handle.handle))
|
|
def is_uastc_ldr_4x4(self, ktx2_handle): return bool(self.ktx2_is_uastc_ldr_4x4(ktx2_handle.handle))
|
|
def is_xuastc_ldr(self, ktx2_handle): return bool(self.ktx2_is_xuastc_ldr(ktx2_handle.handle))
|
|
def is_astc_ldr(self, ktx2_handle): return bool(self.ktx2_is_astc_ldr(ktx2_handle.handle))
|
|
|
|
# ---- DFD access
|
|
def get_dfd_flags(self, ktx2_handle): return self.ktx2_get_dfd_flags(ktx2_handle.handle)
|
|
def get_dfd_total_samples(self, ktx2_handle): return self.ktx2_get_dfd_total_samples(ktx2_handle.handle)
|
|
def get_dfd_color_model(self, ktx2_handle): return self.ktx2_get_dfd_color_model(ktx2_handle.handle)
|
|
def get_dfd_color_primaries(self, ktx2_handle): return self.ktx2_get_dfd_color_primaries(ktx2_handle.handle)
|
|
def get_dfd_transfer_func(self, ktx2_handle): return self.ktx2_get_dfd_transfer_func(ktx2_handle.handle)
|
|
def get_dfd_channel_id0(self, ktx2_handle): return self.ktx2_get_dfd_channel_id0(ktx2_handle.handle)
|
|
def get_dfd_channel_id1(self, ktx2_handle): return self.ktx2_get_dfd_channel_id1(ktx2_handle.handle)
|
|
|
|
# ---- Block dimensions ----
|
|
def get_block_width(self, ktx2_handle): return self.ktx2_get_block_width(ktx2_handle.handle)
|
|
def get_block_height(self, ktx2_handle): return self.ktx2_get_block_height(ktx2_handle.handle)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Explicit: start transcoding on an already-open KTX2 file
|
|
# -----------------------------------------------------------------------
|
|
def start_transcoding(self, ktx2_handle: KTX2Handle):
|
|
"""
|
|
Must be called before per-level iframe flags become valid.
|
|
"""
|
|
ok = self.ktx2_start_transcoding(ktx2_handle.handle)
|
|
if not ok:
|
|
raise RuntimeError("start_transcoding() failed")
|
|
return True
|
|
|
|
# ---- Level info ----
|
|
def get_level_orig_width(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_orig_height(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_actual_width(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_actual_width(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_actual_height(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_actual_height(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_num_blocks_x(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_num_blocks_x(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_num_blocks_y(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_num_blocks_y(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_total_blocks(self, ktx2_handle, level, layer=0, face=0):
|
|
return self.ktx2_get_level_total_blocks(ktx2_handle.handle, level, layer, face)
|
|
|
|
def get_level_alpha_flag(self, ktx2_handle, level, layer=0, face=0):
|
|
return bool(self.ktx2_get_level_alpha_flag(ktx2_handle.handle, level, layer, face))
|
|
|
|
def get_level_iframe_flag(self, ktx2_handle, level, layer=0, face=0):
|
|
return bool(self.ktx2_get_level_iframe_flag(ktx2_handle.handle, level, layer, face))
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Low-level: Decode RGBA8 from an already-open KTX2 handle
|
|
# -----------------------------------------------------------------------
|
|
def decode_rgba_handle(self, ktx2_handle: KTX2Handle, level=0, layer=0, face=0):
|
|
"""
|
|
Low-level fast decode. Requires an already-open KTX2Handle.
|
|
Returns HxWx4 uint8 NumPy array.
|
|
"""
|
|
w = self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face)
|
|
h = self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face)
|
|
|
|
out_size = w * h * 4
|
|
out_ptr = self._alloc(out_size)
|
|
|
|
# MUST start transcoding before ANY decode
|
|
ok = self.ktx2_start_transcoding(ktx2_handle.handle)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("start_transcoding failed")
|
|
|
|
ok = self.ktx2_transcode_image_level(
|
|
ktx2_handle.handle,
|
|
level, layer, face,
|
|
out_ptr,
|
|
out_size,
|
|
TranscoderTextureFormat.TF_RGBA32,
|
|
0, 0, 0, -1, -1, 0
|
|
)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("transcode_image_level failed")
|
|
|
|
raw_bytes = self._read(out_ptr, out_size)
|
|
self._free(out_ptr)
|
|
|
|
arr = np.frombuffer(raw_bytes, dtype=np.uint8)
|
|
return arr.reshape((h, w, 4))
|
|
|
|
# -----------------------------------------------------------------------
|
|
# High-level: Decode RGBA8 directly from KTX2 file data
|
|
# -----------------------------------------------------------------------
|
|
def decode_rgba(self, ktx2_bytes: bytes, level=0, layer=0, face=0):
|
|
"""
|
|
High-level convenience decode. Opens the KTX2 file bytes for you.
|
|
"""
|
|
ktx2_handle = self.open(ktx2_bytes)
|
|
try:
|
|
return self.decode_rgba_handle(ktx2_handle, level, layer, face)
|
|
finally:
|
|
self.close(ktx2_handle)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Low-level: Decode HDR (RGBA float32) from open KTX2
|
|
# -----------------------------------------------------------------------
|
|
def decode_rgba_hdr_handle(self, ktx2_handle: KTX2Handle, level=0, layer=0, face=0):
|
|
"""
|
|
Low-level HDR decode. Returns HxWx4 float32 NumPy array.
|
|
"""
|
|
w = self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face)
|
|
h = self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face)
|
|
|
|
bytes_per_pixel = 8 # 4 * half-float
|
|
out_size = w * h * bytes_per_pixel
|
|
out_ptr = self._alloc(out_size)
|
|
|
|
ok = self.ktx2_start_transcoding(ktx2_handle.handle)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("start_transcoding failed")
|
|
|
|
ok = self.ktx2_transcode_image_level(
|
|
ktx2_handle.handle,
|
|
level, layer, face,
|
|
out_ptr,
|
|
out_size,
|
|
TranscoderTextureFormat.TF_RGBA_HALF,
|
|
0, 0, 0, -1, -1, 0
|
|
)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("transcode_image_level failed")
|
|
|
|
raw_bytes = self._read(out_ptr, out_size)
|
|
self._free(out_ptr)
|
|
|
|
arr = np.frombuffer(raw_bytes, dtype=np.float16).astype(np.float32)
|
|
return arr.reshape((h, w, 4))
|
|
|
|
# -----------------------------------------------------------------------
|
|
# High-level: Decode HDR (RGBA float32) from KTX2 file data
|
|
# -----------------------------------------------------------------------
|
|
def decode_rgba_hdr(self, ktx2_bytes: bytes, level=0, layer=0, face=0):
|
|
"""
|
|
High-level convenience HDR decode. Opens the KTX2 file bytes for you.
|
|
"""
|
|
ktx2_handle = self.open(ktx2_bytes)
|
|
try:
|
|
return self.decode_rgba_hdr_handle(ktx2_handle, level, layer, face)
|
|
finally:
|
|
self.close(ktx2_handle)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Low-level: General-purpose transcode using a chosen TranscoderTextureFormat format
|
|
# -----------------------------------------------------------------------
|
|
def transcode_tfmt_handle(self, ktx2_handle: KTX2Handle, tfmt: int,
|
|
level=0, layer=0, face=0, decode_flags=0,
|
|
channel0=-1, channel1=-1):
|
|
"""
|
|
Low-level direct transcoding from an already-open KTX2 handle.
|
|
|
|
Parameters:
|
|
ktx2_handle: KTX2Handle -> already-open KTX2
|
|
tfmt: int -> TranscoderTextureFormat to transcode to (for ASTC: block size and LDR/HDR MUST match the KTX2 file, for HDR: must be a HDR texture format)
|
|
level/layer/face: int -> which image slice to decode
|
|
decode_flags: int -> basist::decode_flags
|
|
row_pitch, rows_in_pixels, channel0, channel1 -> advanced options
|
|
|
|
Returns: bytes (transcoded GPU texture data or uncompressed image)
|
|
"""
|
|
|
|
# Determine actual output size in bytes
|
|
ow = self.ktx2_get_level_orig_width(ktx2_handle.handle, level, layer, face)
|
|
oh = self.ktx2_get_level_orig_height(ktx2_handle.handle, level, layer, face)
|
|
|
|
out_size = self.basis_compute_transcoded_image_size_in_bytes(tfmt, ow, oh)
|
|
if out_size == 0:
|
|
raise RuntimeError("basis_compute_transcoded_image_size_in_bytes returned 0")
|
|
|
|
# print(f"*** ow={ow}, oh={oh}, out_size={out_size}")
|
|
|
|
out_ptr = self._alloc(out_size)
|
|
|
|
# Call transcoder
|
|
ok = self.ktx2_start_transcoding(ktx2_handle.handle)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("start_transcoding failed")
|
|
|
|
ok = self.ktx2_transcode_image_level(
|
|
ktx2_handle.handle,
|
|
level, layer, face,
|
|
out_ptr,
|
|
out_size,
|
|
tfmt,
|
|
decode_flags,
|
|
0,
|
|
0,
|
|
channel0, channel1,
|
|
0 # no per-thread state object
|
|
)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("ktx2_transcode_image_level failed")
|
|
|
|
# Extract bytes
|
|
raw_bytes = self._read(out_ptr, out_size)
|
|
|
|
self._free(out_ptr)
|
|
return raw_bytes
|
|
|
|
# -----------------------------------------------------------------------
|
|
# High-level: General-purpose transcode (opens the KTX2 for you)
|
|
# tfmt: the TranscoderTextureFormat to transcode too
|
|
# -----------------------------------------------------------------------
|
|
def transcode_tfmt(self, ktx2_bytes: bytes, tfmt: int,
|
|
level=0, layer=0, face=0, decode_flags=0,
|
|
channel0=-1, channel1=-1):
|
|
"""
|
|
High-level convenience wrapper for transcode_tfmt_handle().
|
|
Automatically opens/closes the KTX2 file.
|
|
"""
|
|
ktx2_handle = self.open(ktx2_bytes)
|
|
try:
|
|
return self.transcode_tfmt_handle(
|
|
ktx2_handle, tfmt,
|
|
level=level,
|
|
layer=layer,
|
|
face=face,
|
|
decode_flags=decode_flags,
|
|
channel0=channel0,
|
|
channel1=channel1
|
|
)
|
|
finally:
|
|
self.close(ktx2_handle)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Low-level: choose a specific transcoder_texture_format from a family string
|
|
# -----------------------------------------------------------------------
|
|
def choose_transcoder_format(self, ktx2_handle: KTX2Handle, family: str) -> int:
|
|
"""
|
|
Given an already-opened KTX2 and a desired family string, choose a concrete
|
|
TranscoderTextureFormat enum.
|
|
|
|
family: one of:
|
|
"ASTC", "BC1", "BC3", "BC4", "BC5", "BC6H", "BC7",
|
|
"PVRTC1", "PVRTC2",
|
|
"ETC1", "ETC2", "ETC2_EAC_R11", "ETC2_EAC_RG11",
|
|
"ATC", "FXT1",
|
|
"RGBA32", "RGB_HALF", "RGBA_HALF", "RGB_FLOAT", "RGBA_FLOAT",
|
|
"RGB_9E5"
|
|
|
|
Returns:
|
|
int: TranscoderTextureFormat value
|
|
"""
|
|
|
|
s = family.strip().upper().replace(" ", "")
|
|
hdr_tex = self.is_hdr(ktx2_handle)
|
|
has_alpha = self.has_alpha(ktx2_handle)
|
|
basis_fmt = self.get_basis_tex_format(ktx2_handle)
|
|
|
|
tfmt = None
|
|
|
|
# -------------------------------------------------------------------
|
|
# Uncompressed families
|
|
# -------------------------------------------------------------------
|
|
if s in ("RGBA32", "RGBA8", "UNCOMPRESSED"):
|
|
tfmt = TranscoderTextureFormat.TF_RGBA32
|
|
|
|
elif s in ("RGBHALF", "RGB16F", "RGB_FLOAT", "RGBFLOAT"):
|
|
tfmt = TranscoderTextureFormat.TF_RGB_HALF
|
|
|
|
elif s in ("RGBAHALF", "RGBA16F", "RGBA_FLOAT", "RGBAFLOAT"):
|
|
tfmt = TranscoderTextureFormat.TF_RGBA_HALF
|
|
|
|
elif s in ("RGB9E5", "RGB_9E5"):
|
|
tfmt = TranscoderTextureFormat.TF_RGB_9E5
|
|
|
|
# -------------------------------------------------------------------
|
|
# BC families
|
|
# -------------------------------------------------------------------
|
|
elif s == "BC1":
|
|
tfmt = TranscoderTextureFormat.TF_BC1_RGB
|
|
elif s == "BC3":
|
|
tfmt = TranscoderTextureFormat.TF_BC3_RGBA
|
|
elif s == "BC4":
|
|
tfmt = TranscoderTextureFormat.TF_BC4_R
|
|
elif s == "BC5":
|
|
tfmt = TranscoderTextureFormat.TF_BC5_RG
|
|
elif s == "BC6H":
|
|
tfmt = TranscoderTextureFormat.TF_BC6H
|
|
elif s == "BC7":
|
|
tfmt = TranscoderTextureFormat.TF_BC7_RGBA
|
|
|
|
# -------------------------------------------------------------------
|
|
# PVRTC families
|
|
# -------------------------------------------------------------------
|
|
elif s == "PVRTC1":
|
|
tfmt = (TranscoderTextureFormat.TF_PVRTC1_4_RGBA
|
|
if has_alpha else TranscoderTextureFormat.TF_PVRTC1_4_RGB)
|
|
|
|
elif s == "PVRTC2":
|
|
tfmt = (TranscoderTextureFormat.TF_PVRTC2_4_RGBA
|
|
if has_alpha else TranscoderTextureFormat.TF_PVRTC2_4_RGB)
|
|
|
|
# -------------------------------------------------------------------
|
|
# ETC / EAC families
|
|
# -------------------------------------------------------------------
|
|
elif s == "ETC1":
|
|
tfmt = TranscoderTextureFormat.TF_ETC1_RGB
|
|
|
|
elif s == "ETC2":
|
|
tfmt = TranscoderTextureFormat.TF_ETC2_RGBA
|
|
|
|
elif s in ("ETC2_EAC_R11", "EAC_R11"):
|
|
tfmt = TranscoderTextureFormat.TF_ETC2_EAC_R11
|
|
|
|
elif s in ("ETC2_EAC_RG11", "EAC_RG11"):
|
|
tfmt = TranscoderTextureFormat.TF_ETC2_EAC_RG11
|
|
|
|
# -------------------------------------------------------------------
|
|
# ATC / FXT
|
|
# -------------------------------------------------------------------
|
|
elif s == "ATC":
|
|
tfmt = (TranscoderTextureFormat.TF_ATC_RGBA
|
|
if has_alpha else TranscoderTextureFormat.TF_ATC_RGB)
|
|
|
|
elif s == "FXT1":
|
|
tfmt = TranscoderTextureFormat.TF_FXT1_RGB
|
|
|
|
# -------------------------------------------------------------------
|
|
# ASTC family
|
|
# -------------------------------------------------------------------
|
|
elif s == "ASTC":
|
|
# Let BasisU decide correct ASTC format (block size + LDR/HDR)
|
|
tfmt = self.basis_get_transcoder_texture_format_from_basis_tex_format(basis_fmt)
|
|
|
|
else:
|
|
# Unknown family: choose a safe uncompressed default
|
|
if hdr_tex:
|
|
tfmt = TranscoderTextureFormat.TF_RGBA_HALF
|
|
else:
|
|
tfmt = TranscoderTextureFormat.TF_RGBA32
|
|
|
|
# -------------------------------------------------------------------
|
|
# Validate HDR/LDR compatibility (optional but recommended)
|
|
# -------------------------------------------------------------------
|
|
# Use helpers to ensure we don't do HDR->LDR or LDR->HDR accidentally.
|
|
is_tfmt_hdr = self.basis_transcoder_format_is_hdr(tfmt)
|
|
if hdr_tex and not is_tfmt_hdr:
|
|
raise ValueError(f"Requested {family} (LDR transcoder format) for HDR KTX2.")
|
|
if not hdr_tex and is_tfmt_hdr:
|
|
raise ValueError(f"Requested {family} (HDR transcoder format) for LDR KTX2.")
|
|
|
|
return tfmt
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Low-level: General-purpose transcode using a family string
|
|
# from an already opened ktx2 file.
|
|
# Returns:
|
|
# (data_bytes, chosen_tfmt, block_width, block_height)
|
|
# -----------------------------------------------------------------------
|
|
def transcode_handle(
|
|
self,
|
|
ktx2_handle: KTX2Handle,
|
|
family: str,
|
|
level=0,
|
|
layer=0,
|
|
face=0,
|
|
decode_flags=0,
|
|
channel0=-1,
|
|
channel1=-1
|
|
):
|
|
"""
|
|
Low-level direct transcoding from an already-open KTX2 handle,
|
|
using a high-level family string such as:
|
|
"BC7", "BC3", "BC1", "ETC1", "ETC2", "ASTC", "PVRTC1",
|
|
"RGBA32", "RGB_HALF", "RGBA_HALF", "RGB_9E5", etc.
|
|
See choose_transcoder_format().
|
|
Returns:
|
|
(data_bytes, tfmt, block_width, block_height)
|
|
"""
|
|
|
|
# Decide the exact transcoder format (BC1/BC7/etc.)
|
|
tfmt = self.choose_transcoder_format(ktx2_handle, family)
|
|
|
|
# Get original dims of the requested slice
|
|
ow = self.get_level_orig_width(ktx2_handle, level, layer, face)
|
|
oh = self.get_level_orig_height(ktx2_handle, level, layer, face)
|
|
|
|
# Compute correct output size for the chosen format
|
|
out_size = self.basis_compute_transcoded_image_size_in_bytes(tfmt, ow, oh)
|
|
if out_size == 0:
|
|
raise RuntimeError(
|
|
f"Computed output size is 0 for tfmt={tfmt}, dims={ow}x{oh}"
|
|
)
|
|
|
|
# Allocate output buffer
|
|
out_ptr = self._alloc(out_size)
|
|
|
|
# Ensure transcoding tables are ready
|
|
ok = self.ktx2_start_transcoding(ktx2_handle.handle)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("start_transcoding failed")
|
|
|
|
# Perform the transcode
|
|
ok = self.ktx2_transcode_image_level(
|
|
ktx2_handle.handle,
|
|
level, layer, face,
|
|
out_ptr,
|
|
out_size,
|
|
tfmt,
|
|
decode_flags,
|
|
0, # row_pitch_in_blocks_or_pixels
|
|
0, # rows_in_pixels
|
|
channel0,
|
|
channel1,
|
|
0 # no thread-local state
|
|
)
|
|
if not ok:
|
|
self._free(out_ptr)
|
|
raise RuntimeError("ktx2_transcode_image_level failed")
|
|
|
|
# Extract bytes from native/WASM memory
|
|
data_bytes = self._read(out_ptr, out_size)
|
|
|
|
# Free the output buffer
|
|
self._free(out_ptr)
|
|
|
|
# Determine block dims for this texture format
|
|
if self.basis_transcoder_format_is_uncompressed(tfmt):
|
|
bw = None
|
|
bh = None
|
|
else:
|
|
bw = self.basis_get_block_width(tfmt)
|
|
bh = self.basis_get_block_height(tfmt)
|
|
|
|
return data_bytes, tfmt, bw, bh
|
|
|
|
# -----------------------------------------------------------------------
|
|
# High-level: one-shot transcode using a family string
|
|
# directly from ktx2 file data. (Slower if you're transcoding multiple
|
|
# levels/faces/layers.)
|
|
# -----------------------------------------------------------------------
|
|
def transcode(
|
|
self,
|
|
ktx2_bytes: bytes,
|
|
family: str,
|
|
level=0,
|
|
layer=0,
|
|
face=0,
|
|
decode_flags=0,
|
|
channel0=-1,
|
|
channel1=-1
|
|
):
|
|
"""
|
|
High-level version of transcode_handle().
|
|
Calls transcode_handle() internally.
|
|
|
|
Returns:
|
|
(data_bytes, tfmt, block_width, block_height)
|
|
"""
|
|
ktx2_handle = self.open(ktx2_bytes)
|
|
try:
|
|
return self.transcode_handle(
|
|
ktx2_handle,
|
|
family,
|
|
level=level,
|
|
layer=layer,
|
|
face=face,
|
|
decode_flags=decode_flags,
|
|
channel0=channel0,
|
|
channel1=channel1
|
|
)
|
|
finally:
|
|
self.close(ktx2_handle)
|
|
|
|
def tfmt_name(self, tfmt: int):
|
|
return TranscoderTextureFormat(tfmt).name
|