mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-06-08 08:33:53 +00:00
adding new files
This commit is contained in:
735
python/basisu_py/transcoder.py
Normal file
735
python/basisu_py/transcoder.py
Normal file
@@ -0,0 +1,735 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user