mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-06-13 10:49:14 +00:00
adding new files
This commit is contained in:
59
python/README.md
Normal file
59
python/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
Python support is still new and coming online, but is entirely functional.
|
||||
The library's pure C (WASM friendly) API's are completely exposed to Python.
|
||||
|
||||
The Python integration first tries to use native .so's in the basisu_py
|
||||
directory. If they don't exist, it tries the slower and single threaded WASM
|
||||
fallbacks under basisu_py/wasm, which requires wasmtime for Python to be
|
||||
installed. Some tests require an input.ktx2 or test.ktx2 to be in the current
|
||||
directory.
|
||||
|
||||
Building:
|
||||
|
||||
Under the repo's root directory - build the native SO's:
|
||||
|
||||
```
|
||||
mkdir build_python
|
||||
cd build_python
|
||||
cmake -DBASISU_BUILD_PYTHON=ON ..
|
||||
make
|
||||
```
|
||||
|
||||
Build the WASM modules (see README_WASI.md file for instructions on how to
|
||||
install the WASI SDK, which is required):
|
||||
|
||||
```
|
||||
mkdir build_wasm_st
|
||||
cd build_wasm_st
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk.cmake -DCMAKE_BUILD_TYPE=Release -DBASISU_WASM_THREADING=OFF
|
||||
make
|
||||
```
|
||||
|
||||
Running tests:
|
||||
|
||||
The tests assume the current directory is "python". Run them like this:
|
||||
|
||||
Higher-level tests:
|
||||
|
||||
python3 -m tests.test_backend_loading
|
||||
python3 -m tests.test_basic_wasm_selection
|
||||
python3 -m tests.test_basic_backend_selection
|
||||
python3 -m tests.test_basic_decode
|
||||
python3 -m tests.test_basic_transcode
|
||||
python3 -m tests.test_compress_swirl
|
||||
python3 -m tests.test_compress_swirl_hdr
|
||||
python3 -m tests.test_transcoder_astc
|
||||
python3 -m tests.test_transcoder_backend_loading
|
||||
python3 -m tests.test_transcoder_end_to_end
|
||||
python3 -m tests.test_transcoder_end_to_end_hdr
|
||||
python3 -m tests.test_transcoder_helpers
|
||||
|
||||
Low-level tests (used while bringing up the codec):
|
||||
|
||||
python3 -m lowlevel_test_native.basic_test
|
||||
python3 -m lowlevel_test_native.test_transcoder_basic
|
||||
python3 -m lowlevel_test_native.example_capi_python
|
||||
|
||||
python3 -m lowlevel_test_wasm.basic_test
|
||||
python3 -m lowlevel_test_wasm.compress_test
|
||||
python3 -m lowlevel_test_wasm.compress_test_float
|
||||
|
||||
85
python/README_win.md
Normal file
85
python/README_win.md
Normal file
@@ -0,0 +1,85 @@
|
||||
Windows Native Python Build Instructions
|
||||
========================================
|
||||
|
||||
This project uses pybind11 to build Python .pyd extension modules on Windows.
|
||||
Because Windows installs multiple Python versions, and pybind11 currently only
|
||||
supports up to Python 3.12, you must follow these steps exactly.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Visual Studio Developer Command Prompt (VS C++ Build Tools installed)
|
||||
- Python 3.12 (pybind11 does NOT support 3.13+ at the time of writing)
|
||||
- pybind11 installed into Python 3.12
|
||||
|
||||
Check installed Python versions:
|
||||
py -0
|
||||
|
||||
If Python 3.12 is missing:
|
||||
winget install Python.Python.3.12
|
||||
|
||||
Install pybind11 for Python 3.12:
|
||||
py -3.12 -m pip install pybind11
|
||||
|
||||
IMPORTANT:
|
||||
You must build AND run with the same Python interpreter version (3.12).
|
||||
|
||||
Building the .pyd Modules
|
||||
-------------------------
|
||||
Open the "Developer Command Prompt for Visual Studio".
|
||||
|
||||
From the project root:
|
||||
|
||||
mkdir build_python_win
|
||||
cd build_python_win
|
||||
|
||||
Run CMake using the exact path to python.exe for Python 3.12:
|
||||
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DBASISU_BUILD_PYTHON=ON -DBASISU_BUILD_WASM=OFF -DPYTHON_EXECUTABLE="C:\Users\<YOU>\AppData\Local\Programs\Python\Python312\python.exe" ..
|
||||
|
||||
Build:
|
||||
|
||||
cmake --build . --config Release
|
||||
|
||||
Output files will be created in:
|
||||
|
||||
python/basisu_py/basisu_python.pyd
|
||||
python/basisu_py/basisu_transcoder_python.pyd
|
||||
|
||||
Running the Modules
|
||||
-------------------
|
||||
Always run using Python 3.12:
|
||||
|
||||
py -3.12
|
||||
|
||||
Inside Python:
|
||||
|
||||
import basisu_py
|
||||
print("Modules loaded OK.")
|
||||
|
||||
While in the "python" directory:
|
||||
|
||||
py -m tests.test_backend_loading
|
||||
|
||||
WASM Backend (Optional)
|
||||
-----------------------
|
||||
Install wasmtime:
|
||||
|
||||
py -3.12 -m pip install wasmtime
|
||||
|
||||
Ensure these files exist:
|
||||
|
||||
python/basisu_py/wasm/*.wasm
|
||||
|
||||
Common Problems
|
||||
---------------
|
||||
1. "pybind11 not found"
|
||||
-> Installed into wrong Python version. Use:
|
||||
py -3.12 -m pip install pybind11
|
||||
|
||||
2. "Python config failure"
|
||||
-> You are using Python 3.13 or 3.14. Must use Python 3.12.
|
||||
|
||||
3. Modules not loading
|
||||
-> You must run them with the same interpreter used to build them:
|
||||
py -3.12
|
||||
|
||||
83
python/astc_writer.py
Normal file
83
python/astc_writer.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# astc_writer.py
|
||||
#
|
||||
# Minimal ASTC writer that mirrors the C/C++ write_astc_file() logic from example_capi.c.
|
||||
# Writes a valid single-slice 2D ASTC texture file (no array slices, no 3D, no mips).
|
||||
#
|
||||
# Usage:
|
||||
# from astc_writer import write_astc_file
|
||||
# write_astc_file("output.astc", blocks, block_width, block_height, width, height)
|
||||
#
|
||||
# "blocks" must be a bytes-like object containing the full ASTC block data
|
||||
# using 16 bytes per block (standard ASTC block size).
|
||||
|
||||
|
||||
def write_astc_file(
|
||||
filename: str,
|
||||
blocks: bytes,
|
||||
block_width: int,
|
||||
block_height: int,
|
||||
width: int,
|
||||
height: int
|
||||
) -> None:
|
||||
"""
|
||||
Write an ASTC file to disk.
|
||||
|
||||
Parameters:
|
||||
filename : Output filename ("something.astc")
|
||||
blocks : Bytes-like object containing ASTC blocks (16 bytes per block)
|
||||
block_width : ASTC block width (e.g. 4-12)
|
||||
block_height : ASTC block height (e.g. 4-12)
|
||||
width : Original image width in pixels
|
||||
height : Original image height in pixels
|
||||
|
||||
Notes:
|
||||
- ASTC files use 2D blocks; depth is always 1.
|
||||
- Block layout goes row-major: (num_blocks_y num_blocks_x) blocks.
|
||||
- No mipmaps are stored in this format.
|
||||
"""
|
||||
|
||||
# Validate block dimensions
|
||||
if block_width < 4 or block_width > 12:
|
||||
raise ValueError(f"ASTC block_width {block_width} out of range (412)")
|
||||
if block_height < 4 or block_height > 12:
|
||||
raise ValueError(f"ASTC block_height {block_height} out of range (412)")
|
||||
|
||||
# Compute block grid
|
||||
num_blocks_x = (width + block_width - 1) // block_width
|
||||
num_blocks_y = (height + block_height - 1) // block_height
|
||||
total_blocks = num_blocks_x * num_blocks_y
|
||||
expected_size = total_blocks * 16 # 16 bytes per ASTC block (always)
|
||||
|
||||
if len(blocks) != expected_size:
|
||||
raise ValueError(
|
||||
f"ASTC block buffer incorrect size: expected {expected_size}, got {len(blocks)}"
|
||||
)
|
||||
|
||||
# Write file
|
||||
with open(filename, "wb") as f:
|
||||
# ASTC magic number (0x13AB A15C)
|
||||
f.write(bytes([0x13, 0xAB, 0xA1, 0x5C]))
|
||||
|
||||
# Block dims: x, y, z (z=1)
|
||||
f.write(bytes([
|
||||
block_width & 0xFF,
|
||||
block_height & 0xFF,
|
||||
1
|
||||
]))
|
||||
|
||||
# ASTC stores width/height/depth as 24-bit LE
|
||||
def write_24bit_le(v: int):
|
||||
f.write(bytes([
|
||||
v & 0xFF,
|
||||
(v >> 8) & 0xFF,
|
||||
(v >> 16) & 0xFF
|
||||
]))
|
||||
|
||||
write_24bit_le(width)
|
||||
write_24bit_le(height)
|
||||
write_24bit_le(1) # depth
|
||||
|
||||
# Write actual block payload
|
||||
f.write(blocks)
|
||||
|
||||
print(f"[ASTC Writer] Wrote: {filename} ({width}x{height}, {block_width}x{block_height} blocks)")
|
||||
109
python/basisu_encoder_pybind11.cpp
Normal file
109
python/basisu_encoder_pybind11.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
// File: basisu_encoder_pybind11.cpp
|
||||
// pybind11 native bindings for the compressor's pure C API basisu_wasm_api.h
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// include the basisu compression plain C API
|
||||
#include "../encoder/basisu_wasm_api.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
// Convert wasm_bool_t (uint32_t) ? Python bool
|
||||
static inline bool to_bool(uint32_t v) { return v != 0; }
|
||||
|
||||
PYBIND11_MODULE(basisu_python, m) {
|
||||
m.doc() = "Native Basis Universal encoder (pybind11 binding over basisu_wasm_api)";
|
||||
|
||||
//
|
||||
// Initialization / Version
|
||||
//
|
||||
m.def("init", &bu_init, "Initialize the BasisU codec library");
|
||||
m.def("get_version", &bu_get_version, "Return BASISU_LIB_VERSION");
|
||||
|
||||
//
|
||||
// Memory allocation helpers
|
||||
//
|
||||
m.def("alloc", &bu_alloc,
|
||||
"Allocate memory inside native heap and return pointer as uint64");
|
||||
m.def("free", &bu_free,
|
||||
"Free previously allocated pointer");
|
||||
|
||||
//
|
||||
// Compression params handles
|
||||
//
|
||||
m.def("new_params", &bu_new_comp_params,
|
||||
"Create a new comp_params struct inside native heap");
|
||||
m.def("delete_params",
|
||||
[](uint64_t h) { return to_bool(bu_delete_comp_params(h)); },
|
||||
"Destroy a comp_params struct");
|
||||
|
||||
m.def("params_clear",
|
||||
[](uint64_t h) { return to_bool(bu_comp_params_clear(h)); },
|
||||
"Clear comp_params struct");
|
||||
|
||||
//
|
||||
// Image upload API
|
||||
//
|
||||
m.def("set_image_rgba32",
|
||||
[](uint64_t params, uint32_t index,
|
||||
uint64_t img_ptr, uint32_t w, uint32_t h, uint32_t pitch) {
|
||||
return to_bool(bu_comp_params_set_image_rgba32(
|
||||
params, index, img_ptr, w, h, pitch));
|
||||
},
|
||||
"Set 8-bit RGBA32 image into parameters");
|
||||
|
||||
m.def("set_image_float_rgba",
|
||||
[](uint64_t params, uint32_t index,
|
||||
uint64_t img_ptr, uint32_t w, uint32_t h, uint32_t pitch) {
|
||||
return to_bool(bu_comp_params_set_image_float_rgba(
|
||||
params, index, img_ptr, w, h, pitch));
|
||||
},
|
||||
"Set float32 RGBA image into parameters");
|
||||
|
||||
//
|
||||
// Compression
|
||||
//
|
||||
m.def("compress",
|
||||
[](uint64_t params,
|
||||
int tex_format,
|
||||
int quality,
|
||||
int effort,
|
||||
uint64_t flags,
|
||||
float rdo_quality)
|
||||
{
|
||||
return to_bool(bu_compress_texture(
|
||||
params, tex_format, quality, effort, flags, rdo_quality));
|
||||
},
|
||||
py::arg("params"),
|
||||
py::arg("tex_format"),
|
||||
py::arg("quality"),
|
||||
py::arg("effort"),
|
||||
py::arg("flags"),
|
||||
py::arg("rdo_quality") = 0.0f
|
||||
);
|
||||
|
||||
//
|
||||
// Output blob access
|
||||
//
|
||||
m.def("get_comp_data_size",
|
||||
&bu_comp_params_get_comp_data_size,
|
||||
"Return size (bytes) of compressed output");
|
||||
m.def("get_comp_data_ofs",
|
||||
&bu_comp_params_get_comp_data_ofs,
|
||||
"Return pointer (uint64) to compressed output buffer");
|
||||
|
||||
// Memory read/write
|
||||
m.def("read_memory",
|
||||
[](uint64_t ptr, uint32_t size) {
|
||||
return py::bytes((const char*)ptr, size);
|
||||
},
|
||||
"Read `size` bytes starting at native memory address `ptr`");
|
||||
|
||||
m.def("write_memory",
|
||||
[](uint64_t dest_ptr, py::buffer src) {
|
||||
py::buffer_info info = src.request();
|
||||
memcpy((void*)dest_ptr, info.ptr, info.size * info.itemsize);
|
||||
},
|
||||
"Write bytes/buffer-like object into native memory at address `ptr`");
|
||||
}
|
||||
2
python/basisu_py/MANIFEST.in
Normal file
2
python/basisu_py/MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
||||
recursive-include basisu_py *.py *.so *.wasm
|
||||
include README.md
|
||||
5
python/basisu_py/READMD.md
Normal file
5
python/basisu_py/READMD.md
Normal file
@@ -0,0 +1,5 @@
|
||||
This is the Python support directory for the Basis Universal KTX2 compressor
|
||||
and transcoder modules.
|
||||
|
||||
License: Apache 2.0
|
||||
|
||||
35
python/basisu_py/__init__.py
Normal file
35
python/basisu_py/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
basisu_py
|
||||
=========
|
||||
Python bindings for the Basis Universal encoder and transcoder, with
|
||||
automatic fallback between native C++ extensions and WASM modules.
|
||||
|
||||
Main entry points:
|
||||
- Transcoder : basisu_py.transcoder.Transcoder
|
||||
- Encoder : basisu_py.codec.Encoder
|
||||
- constants : basisu_py.constants
|
||||
"""
|
||||
|
||||
from .codec import Encoder
|
||||
from .transcoder import Transcoder, KTX2Handle
|
||||
from .constants import (
|
||||
BasisTexFormat,
|
||||
BasisQuality,
|
||||
BasisEffort,
|
||||
BasisFlags,
|
||||
TranscoderTextureFormat,
|
||||
TranscodeDecodeFlags,
|
||||
)
|
||||
|
||||
# What the package publicly exposes
|
||||
__all__ = [
|
||||
"Encoder",
|
||||
"Transcoder",
|
||||
"KTX2Handle",
|
||||
"BasisTexFormat",
|
||||
"BasisQuality",
|
||||
"BasisEffort",
|
||||
"BasisFlags",
|
||||
"TranscoderTextureFormat",
|
||||
"TranscodeDecodeFlags",
|
||||
]
|
||||
BIN
python/basisu_py/basisu_python.cpython-312-x86_64-linux-gnu.so
Normal file
BIN
python/basisu_py/basisu_python.cpython-312-x86_64-linux-gnu.so
Normal file
Binary file not shown.
BIN
python/basisu_py/basisu_python.pyd
Normal file
BIN
python/basisu_py/basisu_python.pyd
Normal file
Binary file not shown.
Binary file not shown.
BIN
python/basisu_py/basisu_transcoder_python.pyd
Normal file
BIN
python/basisu_py/basisu_transcoder_python.pyd
Normal file
Binary file not shown.
222
python/basisu_py/codec.py
Normal file
222
python/basisu_py/codec.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# basisu_py/codec.py
|
||||
|
||||
import importlib
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import ctypes
|
||||
|
||||
from .constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
|
||||
from pathlib import Path
|
||||
|
||||
class EncoderBackend:
|
||||
NATIVE = "native"
|
||||
WASM = "wasm"
|
||||
AUTO = "auto"
|
||||
|
||||
class Encoder:
|
||||
|
||||
def __init__(self, backend=EncoderBackend.AUTO):
|
||||
self.backend = backend
|
||||
self._native = None
|
||||
self._wasm = None
|
||||
self.backend_name = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Try native first (AUTO or NATIVE modes)
|
||||
# ------------------------------------------------------------------
|
||||
if backend in (EncoderBackend.AUTO, EncoderBackend.NATIVE):
|
||||
try:
|
||||
import basisu_py.basisu_python as native_encoder
|
||||
native_encoder.init()
|
||||
|
||||
self._native = native_encoder
|
||||
self._wasm = None
|
||||
self.backend_name = "NATIVE"
|
||||
|
||||
print("[Encoder] Using native backend")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
if backend == EncoderBackend.NATIVE:
|
||||
raise RuntimeError(
|
||||
f"[Encoder] Native backend requested but unavailable: {e}"
|
||||
)
|
||||
print("[Encoder] Native unavailable; falling back to WASM:", e)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fallback to WASM (AUTO or explicitly WASM)
|
||||
# ------------------------------------------------------------------
|
||||
try:
|
||||
from basisu_py.wasm.wasm_encoder import BasisuWasmEncoder
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"[Encoder] WASM backend cannot be imported: {e}\n"
|
||||
"Make sure wasmtime is installed and basisu_py/wasm/*.wasm exist."
|
||||
)
|
||||
|
||||
wasm_path = Path(__file__).parent / "wasm" / "basisu_module_st.wasm"
|
||||
self._wasm = BasisuWasmEncoder(str(wasm_path))
|
||||
self._wasm.load()
|
||||
self._native = None
|
||||
self.backend_name = "WASM"
|
||||
|
||||
print("[Encoder] Using WASM backend")
|
||||
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------
|
||||
def compress(self,
|
||||
image,
|
||||
format=-1,
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB | BasisFlags.THREADED | BasisFlags.XUASTC_LDR_FULL_ZSTD):
|
||||
|
||||
rgba_bytes, w, h, is_hdr = self._convert_input_to_rgba_bytes(image)
|
||||
|
||||
# Auto-select format if user passed -1
|
||||
if format == -1:
|
||||
if is_hdr:
|
||||
format = BasisTexFormat.cUASTC_HDR_6x6
|
||||
else:
|
||||
format = BasisTexFormat.cXUASTC_LDR_6x6
|
||||
|
||||
if self._native:
|
||||
return self._compress_native(rgba_bytes, w, h, format, quality, effort, flags, is_hdr)
|
||||
else:
|
||||
return self._compress_wasm(rgba_bytes, w, h, format, quality, effort, flags, is_hdr)
|
||||
|
||||
def compress_float32(self, arr, **kwargs):
|
||||
if not isinstance(arr, np.ndarray) or arr.dtype != np.float32:
|
||||
raise ValueError("compress_float32 requires float32 NumPy HxWx4 array")
|
||||
|
||||
return self.compress(arr, **kwargs)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Native backend
|
||||
# ------------------------------------------------------
|
||||
def _compress_native(self, bytes_data, w, h, fmt, quality, effort, flags, is_hdr=False):
|
||||
enc = self._native
|
||||
|
||||
params = enc.new_params()
|
||||
|
||||
try:
|
||||
buf_ptr = enc.alloc(len(bytes_data))
|
||||
|
||||
# Write raw bytes (uint8 or float32)
|
||||
ctypes.memmove(buf_ptr, bytes_data, len(bytes_data))
|
||||
|
||||
if is_hdr:
|
||||
ok = enc.set_image_float_rgba(params, 0, buf_ptr, w, h, w * 16) # 4 floats = 16 bytes per pixel
|
||||
else:
|
||||
ok = enc.set_image_rgba32(params, 0, buf_ptr, w, h, w * 4)
|
||||
|
||||
if not ok:
|
||||
raise RuntimeError("Native encoder: set_image failed (HDR or LDR)")
|
||||
|
||||
ok = enc.compress(params, fmt, quality, effort, flags, 0.0)
|
||||
if not ok:
|
||||
raise RuntimeError("Native encoder: compress() failed")
|
||||
|
||||
size = enc.get_comp_data_size(params)
|
||||
ofs = enc.get_comp_data_ofs(params)
|
||||
blob = enc.read_memory(ofs, size)
|
||||
return blob
|
||||
|
||||
finally:
|
||||
enc.delete_params(params)
|
||||
if buf_ptr:
|
||||
enc.free(buf_ptr)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# WASM backend
|
||||
# ------------------------------------------------------
|
||||
def _compress_wasm(self, bytes_data, w, h, fmt, quality, effort, flags, is_hdr=False):
|
||||
enc = self._wasm
|
||||
|
||||
params = enc.new_params()
|
||||
|
||||
try:
|
||||
buf_ptr = enc.alloc(len(bytes_data))
|
||||
enc.write_bytes(buf_ptr, bytes_data)
|
||||
|
||||
if is_hdr:
|
||||
ok = enc.set_image_float_rgba(params, 0, buf_ptr, w, h, w * 16)
|
||||
else:
|
||||
ok = enc.set_image_rgba32(params, 0, buf_ptr, w, h, w * 4)
|
||||
|
||||
if not ok:
|
||||
raise RuntimeError("WASM encoder: set_image failed (HDR or LDR)")
|
||||
|
||||
ok = enc.compress(params, fmt, quality, effort, flags, 0.0)
|
||||
if not ok:
|
||||
raise RuntimeError("WASM encoder: compress() failed")
|
||||
|
||||
size = enc.get_comp_data_size(params)
|
||||
ofs = enc.get_comp_data_ofs(params)
|
||||
blob = enc.read_bytes(ofs, size)
|
||||
return blob
|
||||
|
||||
finally:
|
||||
enc.delete_params(params)
|
||||
if buf_ptr:
|
||||
enc.free(buf_ptr)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Image conversion
|
||||
# ------------------------------------------------------
|
||||
def _convert_input_to_rgba_bytes(self, image):
|
||||
"""
|
||||
Accept:
|
||||
- Pillow Image (LDR) -> returns uint8 bytes
|
||||
- NumPy uint8 LDR -> returns uint8 bytes
|
||||
- NumPy float32 HDR -> returns float32 bytes
|
||||
Returns (bytes, width, height, is_hdr)
|
||||
"""
|
||||
|
||||
# Pillow image -> LDR
|
||||
if isinstance(image, Image.Image):
|
||||
image = image.convert("RGBA")
|
||||
arr = np.array(image, dtype=np.uint8)
|
||||
h, w = arr.shape[:2]
|
||||
return arr.tobytes(), w, h, False
|
||||
|
||||
# NumPy array
|
||||
elif isinstance(image, np.ndarray):
|
||||
|
||||
# HDR float32 image
|
||||
if image.dtype == np.float32:
|
||||
if image.ndim != 3 or image.shape[2] not in (3,4):
|
||||
raise ValueError("HDR NumPy image must be HxWx3 or HxWx4 float32")
|
||||
|
||||
h, w, c = image.shape
|
||||
|
||||
# Expand RGB -> RGBA if needed
|
||||
if c == 3:
|
||||
alpha = np.ones((h, w, 1), dtype=np.float32)
|
||||
arr = np.concatenate([image, alpha], axis=2)
|
||||
else:
|
||||
arr = image
|
||||
|
||||
return arr.tobytes(), w, h, True
|
||||
|
||||
# LDR uint8 image
|
||||
if image.dtype == np.uint8:
|
||||
if image.ndim != 3 or image.shape[2] not in (3,4):
|
||||
raise ValueError("LDR NumPy image must be HxWx3 or HxWx4 uint8")
|
||||
|
||||
h, w, c = image.shape
|
||||
|
||||
if c == 3:
|
||||
alpha = np.full((h, w, 1), 255, dtype=np.uint8)
|
||||
arr = np.concatenate([image, alpha], axis=2)
|
||||
else:
|
||||
arr = image
|
||||
|
||||
return arr.tobytes(), w, h, False
|
||||
|
||||
raise ValueError("NumPy image must be uint8 (LDR) or float32 (HDR)")
|
||||
|
||||
else:
|
||||
raise TypeError("compress() expects Pillow Image or NumPy array")
|
||||
183
python/basisu_py/constants.py
Normal file
183
python/basisu_py/constants.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# basisu_constants.py
|
||||
|
||||
# ============================================================
|
||||
# .KTX2/.basis file types
|
||||
# basist::basis_tex_format
|
||||
# ============================================================
|
||||
class BasisTexFormat:
|
||||
# Original LDR formats
|
||||
cETC1S = 0
|
||||
cUASTC_LDR_4x4 = 1
|
||||
|
||||
# HDR
|
||||
cUASTC_HDR_4x4 = 2
|
||||
cASTC_HDR_6x6 = 3
|
||||
cUASTC_HDR_6x6 = 4
|
||||
|
||||
# XUASTC supercompressed LDR formats
|
||||
cXUASTC_LDR_4x4 = 5
|
||||
cXUASTC_LDR_5x4 = 6
|
||||
cXUASTC_LDR_5x5 = 7
|
||||
cXUASTC_LDR_6x5 = 8
|
||||
cXUASTC_LDR_6x6 = 9
|
||||
cXUASTC_LDR_8x5 = 10
|
||||
cXUASTC_LDR_8x6 = 11
|
||||
cXUASTC_LDR_10x5 = 12
|
||||
cXUASTC_LDR_10x6 = 13
|
||||
cXUASTC_LDR_8x8 = 14
|
||||
cXUASTC_LDR_10x8 = 15
|
||||
cXUASTC_LDR_10x10= 16
|
||||
cXUASTC_LDR_12x10= 17
|
||||
cXUASTC_LDR_12x12= 18
|
||||
|
||||
# Standard ASTC LDR
|
||||
cASTC_LDR_4x4 = 19
|
||||
cASTC_LDR_5x4 = 20
|
||||
cASTC_LDR_5x5 = 21
|
||||
cASTC_LDR_6x5 = 22
|
||||
cASTC_LDR_6x6 = 23
|
||||
cASTC_LDR_8x5 = 24
|
||||
cASTC_LDR_8x6 = 25
|
||||
cASTC_LDR_10x5 = 26
|
||||
cASTC_LDR_10x6 = 27
|
||||
cASTC_LDR_8x8 = 28
|
||||
cASTC_LDR_10x8 = 29
|
||||
cASTC_LDR_10x10= 30
|
||||
cASTC_LDR_12x10= 31
|
||||
cASTC_LDR_12x12= 32
|
||||
|
||||
# ============================================================
|
||||
# Unified quality level: 1-100 (higher=better quality, 100 disables some codec options)
|
||||
# ============================================================
|
||||
class BasisQuality:
|
||||
MIN = 1
|
||||
MAX = 100
|
||||
|
||||
# ============================================================
|
||||
# Unified effort level: 0-10 (0=fastest, 10=very slow, higher=slower but higher potential quality/more features utilized)
|
||||
# ============================================================
|
||||
class BasisEffort:
|
||||
MIN = 0
|
||||
MAX = 10
|
||||
|
||||
SUPER_FAST = 0
|
||||
FAST = 2
|
||||
NORMAL = 5
|
||||
DEFAULT = 2
|
||||
SLOW = 8
|
||||
VERY_SLOW = 10
|
||||
|
||||
# ============================================================
|
||||
# C-style API flags
|
||||
# ============================================================
|
||||
class BasisFlags:
|
||||
NONE = 0
|
||||
USE_OPENCL = 1 << 8
|
||||
THREADED = 1 << 9
|
||||
DEBUG_OUTPUT = 1 << 10
|
||||
|
||||
KTX2_OUTPUT = 1 << 11
|
||||
KTX2_UASTC_ZSTD = 1 << 12
|
||||
|
||||
SRGB = 1 << 13
|
||||
GEN_MIPS_CLAMP = 1 << 14
|
||||
GEN_MIPS_WRAP = 1 << 15
|
||||
|
||||
Y_FLIP = 1 << 16
|
||||
|
||||
PRINT_STATS = 1 << 18
|
||||
PRINT_STATUS = 1 << 19
|
||||
|
||||
DEBUG_IMAGES = 1 << 20
|
||||
|
||||
REC2020 = 1 << 21
|
||||
VALIDATE_OUTPUT = 1 << 22
|
||||
|
||||
XUASTC_LDR_FULL_ARITH = 0
|
||||
XUASTC_LDR_HYBRID = 1 << 23
|
||||
XUASTC_LDR_FULL_ZSTD = 2 << 23
|
||||
XUASTC_LDR_SYNTAX_SHIFT = 23
|
||||
XUASTC_LDR_SYNTAX_MASK = 3
|
||||
|
||||
TEXTURE_TYPE_2D = 0 << 25
|
||||
TEXTURE_TYPE_2D_ARRAY = 1 << 25
|
||||
TEXTURE_TYPE_CUBEMAP_ARRAY = 2 << 25
|
||||
TEXTURE_TYPE_VIDEO_FRAMES = 3 << 25
|
||||
TEXTURE_TYPE_SHIFT = 25
|
||||
TEXTURE_TYPE_MASK = 3
|
||||
|
||||
VERBOSE = PRINT_STATS | PRINT_STATUS
|
||||
MIPMAP_CLAMP = GEN_MIPS_CLAMP
|
||||
MIPMAP_WRAP = GEN_MIPS_WRAP
|
||||
|
||||
# ============================================================
|
||||
# Transcoder Texture Formats (GPU block formats)
|
||||
# basist::transcoder_texture_format
|
||||
# ============================================================
|
||||
class TranscoderTextureFormat:
|
||||
TF_ETC1_RGB = 0
|
||||
TF_ETC2_RGBA = 1
|
||||
TF_BC1_RGB = 2
|
||||
TF_BC3_RGBA = 3
|
||||
TF_BC4_R = 4
|
||||
TF_BC5_RG = 5
|
||||
TF_BC7_RGBA = 6
|
||||
|
||||
TF_PVRTC1_4_RGB = 8
|
||||
TF_PVRTC1_4_RGBA = 9
|
||||
|
||||
TF_ASTC_LDR_4X4_RGBA = 10
|
||||
TF_ATC_RGB = 11
|
||||
TF_ATC_RGBA = 12
|
||||
|
||||
# Uncompressed
|
||||
TF_RGBA32 = 13
|
||||
TF_RGB565 = 14
|
||||
TF_BGR565 = 15
|
||||
TF_RGBA4444 = 16
|
||||
|
||||
TF_FXT1_RGB = 17
|
||||
TF_PVRTC2_4_RGB = 18
|
||||
TF_PVRTC2_4_RGBA = 19
|
||||
|
||||
TF_ETC2_EAC_R11 = 20
|
||||
TF_ETC2_EAC_RG11 = 21
|
||||
TF_BC6H = 22
|
||||
|
||||
TF_ASTC_HDR_4X4_RGBA = 23
|
||||
|
||||
TF_RGB_HALF = 24
|
||||
TF_RGBA_HALF = 25
|
||||
TF_RGB_9E5 = 26
|
||||
TF_ASTC_HDR_6X6_RGBA = 27
|
||||
|
||||
TF_ASTC_LDR_5X4_RGBA = 28
|
||||
TF_ASTC_LDR_5X5_RGBA = 29
|
||||
TF_ASTC_LDR_6X5_RGBA = 30
|
||||
TF_ASTC_LDR_6X6_RGBA = 31
|
||||
TF_ASTC_LDR_8X5_RGBA = 32
|
||||
TF_ASTC_LDR_8X6_RGBA = 33
|
||||
TF_ASTC_LDR_10X5_RGBA = 34
|
||||
TF_ASTC_LDR_10X6_RGBA = 35
|
||||
TF_ASTC_LDR_8X8_RGBA = 36
|
||||
TF_ASTC_LDR_10X8_RGBA = 37
|
||||
TF_ASTC_LDR_10X10_RGBA= 38
|
||||
TF_ASTC_LDR_12X10_RGBA= 39
|
||||
TF_ASTC_LDR_12X12_RGBA= 40
|
||||
|
||||
TOTAL = 41
|
||||
|
||||
# ============================================================
|
||||
# Transcoder Decode Flags
|
||||
# ============================================================
|
||||
class TranscodeDecodeFlags:
|
||||
PVRTC_DECODE_TO_NEXT_POW2 = 2
|
||||
TRANSCODE_ALPHA_TO_OPAQUE = 4
|
||||
BC1_FORBID_THREE_COLOR_BLOCKS = 8
|
||||
OUTPUT_HAS_ALPHA_INDICES = 16
|
||||
HIGH_QUALITY = 32
|
||||
NO_ETC1S_CHROMA_FILTERING = 64
|
||||
NO_DEBLOCK_FILTERING = 128
|
||||
STRONGER_DEBLOCK_FILTERING = 256
|
||||
FORCE_DEBLOCK_FILTERING = 512
|
||||
XUASTC_LDR_DISABLE_FAST_BC7_TRANSCODING = 1024
|
||||
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
|
||||
1
python/basisu_py/wasm/__init__.py
Normal file
1
python/basisu_py/wasm/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Purposely empty
|
||||
BIN
python/basisu_py/wasm/basisu_module_mt.wasm
Normal file
BIN
python/basisu_py/wasm/basisu_module_mt.wasm
Normal file
Binary file not shown.
BIN
python/basisu_py/wasm/basisu_module_st.wasm
Normal file
BIN
python/basisu_py/wasm/basisu_module_st.wasm
Normal file
Binary file not shown.
BIN
python/basisu_py/wasm/basisu_transcoder_module_mt.wasm
Normal file
BIN
python/basisu_py/wasm/basisu_transcoder_module_mt.wasm
Normal file
Binary file not shown.
BIN
python/basisu_py/wasm/basisu_transcoder_module_st.wasm
Normal file
BIN
python/basisu_py/wasm/basisu_transcoder_module_st.wasm
Normal file
Binary file not shown.
126
python/basisu_py/wasm/wasm_encoder.py
Normal file
126
python/basisu_py/wasm/wasm_encoder.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# basisu_py/wasm/wasm_encoder.py
|
||||
|
||||
import wasmtime
|
||||
import ctypes
|
||||
|
||||
from ..constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
|
||||
|
||||
|
||||
class BasisuWasmEncoder:
|
||||
def __init__(self, wasm_path):
|
||||
self.wasm_path = wasm_path
|
||||
self.engine = None
|
||||
self.store = None
|
||||
self.memory = None
|
||||
self.exports = None
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Initialize WASM + WASI
|
||||
# ------------------------------------------------------
|
||||
def _init_engine(self):
|
||||
self.engine = wasmtime.Engine()
|
||||
self.store = wasmtime.Store(self.engine)
|
||||
|
||||
wasi = wasmtime.WasiConfig()
|
||||
wasi.argv = ["basisu-wasm"]
|
||||
wasi.inherit_stdout()
|
||||
wasi.inherit_stderr()
|
||||
self.store.set_wasi(wasi)
|
||||
|
||||
def load(self):
|
||||
self._init_engine()
|
||||
|
||||
module = wasmtime.Module.from_file(self.engine, self.wasm_path)
|
||||
linker = wasmtime.Linker(self.engine)
|
||||
linker.define_wasi()
|
||||
|
||||
instance = linker.instantiate(self.store, module)
|
||||
self.exports = instance.exports(self.store)
|
||||
self.memory = self.exports["memory"]
|
||||
|
||||
# Initialize if present
|
||||
if "bu_init" in self.exports:
|
||||
self.exports["bu_init"](self.store)
|
||||
|
||||
print("[WASM Encoder] Loaded:", self.wasm_path)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Access raw linear memory buffer
|
||||
# ------------------------------------------------------
|
||||
def _buf(self):
|
||||
raw_ptr = self.memory.data_ptr(self.store)
|
||||
size = self.memory.data_len(self.store)
|
||||
addr = ctypes.addressof(raw_ptr.contents)
|
||||
return (ctypes.c_ubyte * size).from_address(addr)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Version
|
||||
# ------------------------------------------------------
|
||||
def get_version(self):
|
||||
return self.exports["bu_get_version"](self.store)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Memory alloc/free
|
||||
# ------------------------------------------------------
|
||||
def alloc(self, size):
|
||||
return self.exports["bu_alloc"](self.store, size)
|
||||
|
||||
def free(self, ptr):
|
||||
self.exports["bu_free"](self.store, ptr)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Params
|
||||
# ------------------------------------------------------
|
||||
def new_params(self):
|
||||
return self.exports["bu_new_comp_params"](self.store)
|
||||
|
||||
def delete_params(self, params):
|
||||
return self.exports["bu_delete_comp_params"](self.store, params)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Image input
|
||||
# ------------------------------------------------------
|
||||
def set_image_rgba32(self, params, index, ptr, w, h, pitch):
|
||||
return self.exports["bu_comp_params_set_image_rgba32"](
|
||||
self.store, params, index, ptr, w, h, pitch
|
||||
)
|
||||
|
||||
def set_image_float_rgba(self, params, index, ptr, w, h, pitch):
|
||||
return self.exports["bu_comp_params_set_image_float_rgba"](
|
||||
self.store, params, index, ptr, w, h, pitch
|
||||
)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Compression
|
||||
# ------------------------------------------------------
|
||||
def compress(self, params, fmt, quality, effort, flags, rdo):
|
||||
return bool(self.exports["bu_compress_texture"](
|
||||
self.store, params, fmt, quality, effort, flags, rdo
|
||||
))
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Output blob
|
||||
# ------------------------------------------------------
|
||||
def get_comp_data_size(self, params):
|
||||
return self.exports["bu_comp_params_get_comp_data_size"](self.store, params)
|
||||
|
||||
def get_comp_data_ofs(self, params):
|
||||
return self.exports["bu_comp_params_get_comp_data_ofs"](self.store, params)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Raw memory I/O
|
||||
# ------------------------------------------------------
|
||||
def write_bytes(self, ptr, data):
|
||||
buf = self._buf()
|
||||
buf[ptr:ptr + len(data)] = data
|
||||
|
||||
def read_bytes(self, ptr, size):
|
||||
buf = self._buf()
|
||||
return bytes(buf[ptr:ptr + size])
|
||||
|
||||
# NEW unified names:
|
||||
def write_memory(self, ptr, data):
|
||||
self.write_bytes(ptr, data)
|
||||
|
||||
def read_memory(self, ptr, size):
|
||||
return self.read_bytes(ptr, size)
|
||||
326
python/basisu_py/wasm/wasm_transcoder.py
Normal file
326
python/basisu_py/wasm/wasm_transcoder.py
Normal file
@@ -0,0 +1,326 @@
|
||||
# basisu_py/wasm/wasm_transcoder.py
|
||||
|
||||
import wasmtime
|
||||
import ctypes
|
||||
|
||||
|
||||
class BasisuWasmTranscoder:
|
||||
"""
|
||||
Lowest-level WASM transcoder wrapper.
|
||||
Direct mapping to basisu_wasm_transcoder_api.h/.cpp
|
||||
|
||||
NOTE:
|
||||
- This layer does NOT interpret formats or block sizes.
|
||||
- It only wraps the raw C API (bt_* and basis_* exports).
|
||||
- Higher-level logic (TranscoderCore, Transcoder) will build on top.
|
||||
"""
|
||||
|
||||
def __init__(self, wasm_path: str):
|
||||
self.wasm_path = wasm_path
|
||||
self.engine = None
|
||||
self.store = None
|
||||
self.memory = None
|
||||
self.exports = None
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Internal: initialize WASM + WASI
|
||||
# ------------------------------------------------------
|
||||
def _init_engine(self):
|
||||
self.engine = wasmtime.Engine()
|
||||
self.store = wasmtime.Store(self.engine)
|
||||
|
||||
wasi = wasmtime.WasiConfig()
|
||||
wasi.argv = ["basisu-transcoder"]
|
||||
wasi.inherit_stdout()
|
||||
wasi.inherit_stderr()
|
||||
self.store.set_wasi(wasi)
|
||||
|
||||
def load(self):
|
||||
self._init_engine()
|
||||
|
||||
module = wasmtime.Module.from_file(self.engine, self.wasm_path)
|
||||
linker = wasmtime.Linker(self.engine)
|
||||
linker.define_wasi()
|
||||
|
||||
instance = linker.instantiate(self.store, module)
|
||||
self.exports = instance.exports(self.store)
|
||||
self.memory = self.exports["memory"]
|
||||
|
||||
# Mandatory transcoder init
|
||||
if "bt_init" in self.exports:
|
||||
self.exports["bt_init"](self.store)
|
||||
|
||||
print("[WASM Transcoder] Loaded:", self.wasm_path)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Linear memory access helpers
|
||||
# ------------------------------------------------------
|
||||
def _buf(self):
|
||||
raw_ptr = self.memory.data_ptr(self.store)
|
||||
size = self.memory.data_len(self.store)
|
||||
addr = ctypes.addressof(raw_ptr.contents)
|
||||
return (ctypes.c_ubyte * size).from_address(addr)
|
||||
|
||||
def write_bytes(self, ptr: int, data: bytes):
|
||||
buf = self._buf()
|
||||
buf[ptr:ptr + len(data)] = data
|
||||
|
||||
def read_bytes(self, ptr: int, num: int) -> bytes:
|
||||
buf = self._buf()
|
||||
return bytes(buf[ptr:ptr + num])
|
||||
|
||||
# NEW unified names:
|
||||
def write_memory(self, ptr, data):
|
||||
self.write_bytes(ptr, data)
|
||||
|
||||
def read_memory(self, ptr, size):
|
||||
return self.read_bytes(ptr, size)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Memory alloc/free
|
||||
# ------------------------------------------------------
|
||||
def alloc(self, size: int) -> int:
|
||||
return self.exports["bt_alloc"](self.store, size)
|
||||
|
||||
def free(self, ptr: int):
|
||||
return self.exports["bt_free"](self.store, ptr)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# High-level functions: version, init, debug
|
||||
# ------------------------------------------------------
|
||||
def get_version(self) -> int:
|
||||
return self.exports["bt_get_version"](self.store)
|
||||
|
||||
def enable_debug_printf(self, flag: bool = True):
|
||||
return self.exports["bt_enable_debug_printf"](self.store, 1 if flag else 0)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# basis_tex_format helpers
|
||||
# ------------------------------------------------------
|
||||
def basis_tex_format_is_xuastc_ldr(self, basis_tex_fmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_tex_format_is_xuastc_ldr"](self.store, basis_tex_fmt_u32))
|
||||
|
||||
def basis_tex_format_is_astc_ldr(self, basis_tex_fmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_tex_format_is_astc_ldr"](self.store, basis_tex_fmt_u32))
|
||||
|
||||
def basis_tex_format_get_block_width(self, basis_tex_fmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_tex_format_get_block_width"](self.store, basis_tex_fmt_u32)
|
||||
|
||||
def basis_tex_format_get_block_height(self, basis_tex_fmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_tex_format_get_block_height"](self.store, basis_tex_fmt_u32)
|
||||
|
||||
def basis_tex_format_is_hdr(self, basis_tex_fmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_tex_format_is_hdr"](self.store, basis_tex_fmt_u32))
|
||||
|
||||
def basis_tex_format_is_ldr(self, basis_tex_fmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_tex_format_is_ldr"](self.store, basis_tex_fmt_u32))
|
||||
|
||||
# ------------------------------------------------------
|
||||
# transcoder_texture_format helpers
|
||||
# ------------------------------------------------------
|
||||
def basis_get_bytes_per_block_or_pixel(self, tfmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_get_bytes_per_block_or_pixel"](self.store, tfmt_u32)
|
||||
|
||||
def basis_transcoder_format_has_alpha(self, tfmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_transcoder_format_has_alpha"](self.store, tfmt_u32))
|
||||
|
||||
def basis_transcoder_format_is_hdr(self, tfmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_transcoder_format_is_hdr"](self.store, tfmt_u32))
|
||||
|
||||
def basis_transcoder_format_is_ldr(self, tfmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_transcoder_format_is_ldr"](self.store, tfmt_u32))
|
||||
|
||||
def basis_transcoder_texture_format_is_astc(self, tfmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_transcoder_texture_format_is_astc"](self.store, tfmt_u32))
|
||||
|
||||
def basis_transcoder_format_is_uncompressed(self, tfmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_transcoder_format_is_uncompressed"](self.store, tfmt_u32))
|
||||
|
||||
def basis_get_uncompressed_bytes_per_pixel(self, tfmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_get_uncompressed_bytes_per_pixel"](self.store, tfmt_u32)
|
||||
|
||||
def basis_get_block_width(self, tfmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_get_block_width"](self.store, tfmt_u32)
|
||||
|
||||
def basis_get_block_height(self, tfmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_get_block_height"](self.store, tfmt_u32)
|
||||
|
||||
def basis_get_transcoder_texture_format_from_basis_tex_format(self, basis_tex_fmt_u32: int) -> int:
|
||||
return self.exports["bt_basis_get_transcoder_texture_format_from_basis_tex_format"](self.store, basis_tex_fmt_u32)
|
||||
|
||||
def basis_is_format_supported(self, tfmt_u32: int, basis_tex_fmt_u32: int) -> bool:
|
||||
return bool(self.exports["bt_basis_is_format_supported"](self.store, tfmt_u32, basis_tex_fmt_u32))
|
||||
|
||||
def basis_compute_transcoded_image_size_in_bytes(self, tfmt_u32: int, orig_width: int, orig_height: int) -> int:
|
||||
return self.exports["bt_basis_compute_transcoded_image_size_in_bytes"](
|
||||
self.store, tfmt_u32, orig_width, orig_height
|
||||
)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# KTX2 handle management
|
||||
# ------------------------------------------------------
|
||||
def ktx2_open(self, data_ptr: int, data_len: int) -> int:
|
||||
return self.exports["bt_ktx2_open"](self.store, data_ptr, data_len)
|
||||
|
||||
def ktx2_close(self, handle: int):
|
||||
return self.exports["bt_ktx2_close"](self.store, handle)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Basic KTX2 metadata
|
||||
# ------------------------------------------------------
|
||||
def ktx2_get_width(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_width"](self.store, handle)
|
||||
|
||||
def ktx2_get_height(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_height"](self.store, handle)
|
||||
|
||||
def ktx2_get_levels(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_levels"](self.store, handle)
|
||||
|
||||
def ktx2_get_faces(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_faces"](self.store, handle)
|
||||
|
||||
def ktx2_get_layers(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_layers"](self.store, handle)
|
||||
|
||||
def ktx2_get_basis_tex_format(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_basis_tex_format"](self.store, handle)
|
||||
|
||||
# KTX2 format checks
|
||||
def ktx2_is_etc1s(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_etc1s"](self.store, handle))
|
||||
|
||||
def ktx2_is_uastc_ldr_4x4(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_uastc_ldr_4x4"](self.store, handle))
|
||||
|
||||
def ktx2_is_hdr(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_hdr"](self.store, handle))
|
||||
|
||||
def ktx2_is_hdr_4x4(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_hdr_4x4"](self.store, handle))
|
||||
|
||||
def ktx2_is_hdr_6x6(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_hdr_6x6"](self.store, handle))
|
||||
|
||||
def ktx2_is_ldr(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_ldr"](self.store, handle))
|
||||
|
||||
def ktx2_is_astc_ldr(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_astc_ldr"](self.store, handle))
|
||||
|
||||
def ktx2_is_xuastc_ldr(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_xuastc_ldr"](self.store, handle))
|
||||
|
||||
def ktx2_get_block_width(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_block_width"](self.store, handle)
|
||||
|
||||
def ktx2_get_block_height(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_block_height"](self.store, handle)
|
||||
|
||||
def ktx2_has_alpha(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_has_alpha"](self.store, handle))
|
||||
|
||||
def ktx2_get_dfd_color_model(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_color_model"](self.store, handle)
|
||||
|
||||
def ktx2_get_dfd_color_primaries(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_color_primaries"](self.store, handle)
|
||||
|
||||
def ktx2_get_dfd_transfer_func(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_transfer_func"](self.store, handle)
|
||||
|
||||
def ktx2_is_srgb(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_srgb"](self.store, handle))
|
||||
|
||||
def ktx2_get_dfd_flags(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_flags"](self.store, handle)
|
||||
|
||||
def ktx2_get_dfd_total_samples(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_total_samples"](self.store, handle)
|
||||
|
||||
def ktx2_get_dfd_channel_id0(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_channel_id0"](self.store, handle)
|
||||
|
||||
def ktx2_get_dfd_channel_id1(self, handle: int) -> int:
|
||||
return self.exports["bt_ktx2_get_dfd_channel_id1"](self.store, handle)
|
||||
|
||||
def ktx2_is_video(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_is_video"](self.store, handle))
|
||||
|
||||
def ktx2_get_ldr_hdr_upconversion_nit_multiplier(self, handle: int) -> float:
|
||||
return self.exports["bt_ktx2_get_ldr_hdr_upconversion_nit_multiplier"](self.store, handle)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Per-level metadata
|
||||
# ------------------------------------------------------
|
||||
def ktx2_get_level_orig_width(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_orig_width"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_orig_height(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_orig_height"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_actual_width(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_actual_width"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_actual_height(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_actual_height"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_num_blocks_x(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_num_blocks_x"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_num_blocks_y(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_num_blocks_y"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_total_blocks(self, h, lvl, layer, face) -> int:
|
||||
return self.exports["bt_ktx2_get_level_total_blocks"](self.store, h, lvl, layer, face)
|
||||
|
||||
def ktx2_get_level_alpha_flag(self, h, lvl, layer, face) -> bool:
|
||||
return bool(self.exports["bt_ktx2_get_level_alpha_flag"](self.store, h, lvl, layer, face))
|
||||
|
||||
def ktx2_get_level_iframe_flag(self, h, lvl, layer, face) -> bool:
|
||||
return bool(self.exports["bt_ktx2_get_level_iframe_flag"](self.store, h, lvl, layer, face))
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Transcoding control
|
||||
# ------------------------------------------------------
|
||||
def ktx2_start_transcoding(self, handle: int) -> bool:
|
||||
return bool(self.exports["bt_ktx2_start_transcoding"](self.store, handle))
|
||||
|
||||
def ktx2_create_transcode_state(self) -> int:
|
||||
return self.exports["bt_ktx2_create_transcode_state"](self.store)
|
||||
|
||||
def ktx2_destroy_transcode_state(self, handle: int):
|
||||
return self.exports["bt_ktx2_destroy_transcode_state"](self.store, handle)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Actual transcoding call
|
||||
# ------------------------------------------------------
|
||||
def ktx2_transcode_image_level(
|
||||
self,
|
||||
ktx2_handle: int,
|
||||
level_index: int,
|
||||
layer_index: int,
|
||||
face_index: int,
|
||||
output_block_mem_ofs: int,
|
||||
output_blocks_buf_size_in_blocks_or_pixels: int,
|
||||
transcoder_texture_format_u32: int,
|
||||
decode_flags: int,
|
||||
output_row_pitch_in_blocks_or_pixels: int,
|
||||
output_rows_in_pixels: int,
|
||||
channel0: int,
|
||||
channel1: int,
|
||||
state_handle: int,
|
||||
) -> bool:
|
||||
return bool(self.exports["bt_ktx2_transcode_image_level"](
|
||||
self.store,
|
||||
ktx2_handle,
|
||||
level_index, layer_index, face_index,
|
||||
output_block_mem_ofs,
|
||||
output_blocks_buf_size_in_blocks_or_pixels,
|
||||
transcoder_texture_format_u32,
|
||||
decode_flags,
|
||||
output_row_pitch_in_blocks_or_pixels,
|
||||
output_rows_in_pixels,
|
||||
channel0, channel1,
|
||||
state_handle
|
||||
))
|
||||
264
python/basisu_transcoder_pybind11.cpp
Normal file
264
python/basisu_transcoder_pybind11.cpp
Normal file
@@ -0,0 +1,264 @@
|
||||
// File: basisu_transcoder_pybind11.cpp
|
||||
// pybind11 native bindings for the transcoder's pure C API basisu_wasm_transcoder_api.h
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "../encoder/basisu_wasm_transcoder_api.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
// wasm_bool_t is uint32_t — convert to Python bool
|
||||
static inline bool to_bool(wasm_bool_t v) { return v != 0; }
|
||||
|
||||
PYBIND11_MODULE(basisu_transcoder_python, m) {
|
||||
m.doc() = "Native Basis Universal transcoder (pybind11 binding over basisu_wasm_transcoder_api)";
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// High-level functions
|
||||
// ------------------------------------------------------------------------
|
||||
m.def("get_version", &bt_get_version,
|
||||
"Get BasisU transcoder version");
|
||||
|
||||
m.def("enable_debug_printf",
|
||||
[](bool flag) { bt_enable_debug_printf(flag ? 1u : 0u); },
|
||||
"Enable or disable debug printf output");
|
||||
|
||||
m.def("init", &bt_init,
|
||||
"Initialize transcoder library");
|
||||
|
||||
m.def("alloc", &bt_alloc,
|
||||
"Allocate a buffer, returns uint64 offset/pointer");
|
||||
m.def("free", &bt_free,
|
||||
"Free a buffer allocated by bt_alloc");
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// basis_tex_format helpers
|
||||
// ------------------------------------------------------------------------
|
||||
m.def("basis_tex_format_is_xuastc_ldr",
|
||||
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_xuastc_ldr(fmt)); });
|
||||
|
||||
m.def("basis_tex_format_is_astc_ldr",
|
||||
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_astc_ldr(fmt)); });
|
||||
|
||||
m.def("basis_tex_format_get_block_width",
|
||||
&bt_basis_tex_format_get_block_width);
|
||||
|
||||
m.def("basis_tex_format_get_block_height",
|
||||
&bt_basis_tex_format_get_block_height);
|
||||
|
||||
m.def("basis_tex_format_is_hdr",
|
||||
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_hdr(fmt)); });
|
||||
|
||||
m.def("basis_tex_format_is_ldr",
|
||||
[](uint32_t fmt) { return to_bool(bt_basis_tex_format_is_ldr(fmt)); });
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// transcoder_texture_format helpers
|
||||
// ------------------------------------------------------------------------
|
||||
m.def("basis_get_bytes_per_block_or_pixel",
|
||||
&bt_basis_get_bytes_per_block_or_pixel);
|
||||
|
||||
m.def("basis_transcoder_format_has_alpha",
|
||||
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_has_alpha(tfmt)); });
|
||||
|
||||
m.def("basis_transcoder_format_is_hdr",
|
||||
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_is_hdr(tfmt)); });
|
||||
|
||||
m.def("basis_transcoder_format_is_ldr",
|
||||
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_is_ldr(tfmt)); });
|
||||
|
||||
m.def("basis_transcoder_texture_format_is_astc",
|
||||
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_texture_format_is_astc(tfmt)); });
|
||||
|
||||
m.def("basis_transcoder_format_is_uncompressed",
|
||||
[](uint32_t tfmt) { return to_bool(bt_basis_transcoder_format_is_uncompressed(tfmt)); });
|
||||
|
||||
m.def("basis_get_uncompressed_bytes_per_pixel",
|
||||
&bt_basis_get_uncompressed_bytes_per_pixel);
|
||||
|
||||
m.def("basis_get_block_width",
|
||||
&bt_basis_get_block_width);
|
||||
|
||||
m.def("basis_get_block_height",
|
||||
&bt_basis_get_block_height);
|
||||
|
||||
m.def("basis_get_transcoder_texture_format_from_basis_tex_format",
|
||||
&bt_basis_get_transcoder_texture_format_from_basis_tex_format);
|
||||
|
||||
m.def("basis_is_format_supported",
|
||||
[](uint32_t tfmt, uint32_t basis_fmt) {
|
||||
return to_bool(bt_basis_is_format_supported(tfmt, basis_fmt));
|
||||
});
|
||||
|
||||
m.def("basis_compute_transcoded_image_size_in_bytes",
|
||||
&bt_basis_compute_transcoded_image_size_in_bytes);
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// KTX2 open/close & basic info
|
||||
// ------------------------------------------------------------------------
|
||||
m.def("ktx2_open", &bt_ktx2_open,
|
||||
"Open a KTX2 image from memory; returns handle");
|
||||
|
||||
m.def("ktx2_close", &bt_ktx2_close,
|
||||
"Close a previously opened KTX2 handle");
|
||||
|
||||
m.def("ktx2_get_width", &bt_ktx2_get_width);
|
||||
m.def("ktx2_get_height", &bt_ktx2_get_height);
|
||||
m.def("ktx2_get_levels", &bt_ktx2_get_levels);
|
||||
m.def("ktx2_get_faces", &bt_ktx2_get_faces);
|
||||
m.def("ktx2_get_layers", &bt_ktx2_get_layers);
|
||||
|
||||
m.def("ktx2_get_basis_tex_format", &bt_ktx2_get_basis_tex_format);
|
||||
|
||||
m.def("ktx2_is_etc1s",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_etc1s(h)); });
|
||||
|
||||
m.def("ktx2_is_uastc_ldr_4x4",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_uastc_ldr_4x4(h)); });
|
||||
|
||||
m.def("ktx2_is_hdr",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_hdr(h)); });
|
||||
|
||||
m.def("ktx2_is_hdr_4x4",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_hdr_4x4(h)); });
|
||||
|
||||
m.def("ktx2_is_hdr_6x6",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_hdr_6x6(h)); });
|
||||
|
||||
m.def("ktx2_is_ldr",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_ldr(h)); });
|
||||
|
||||
m.def("ktx2_is_astc_ldr",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_astc_ldr(h)); });
|
||||
|
||||
m.def("ktx2_is_xuastc_ldr",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_xuastc_ldr(h)); });
|
||||
|
||||
m.def("ktx2_get_block_width", &bt_ktx2_get_block_width);
|
||||
|
||||
m.def("ktx2_get_block_height", &bt_ktx2_get_block_height);
|
||||
|
||||
m.def("ktx2_has_alpha",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_has_alpha(h)); });
|
||||
|
||||
m.def("ktx2_get_dfd_color_model", &bt_ktx2_get_dfd_color_model);
|
||||
m.def("ktx2_get_dfd_color_primaries", &bt_ktx2_get_dfd_color_primaries);
|
||||
m.def("ktx2_get_dfd_transfer_func", &bt_ktx2_get_dfd_transfer_func);
|
||||
|
||||
m.def("ktx2_is_srgb",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_srgb(h)); });
|
||||
|
||||
m.def("ktx2_get_dfd_flags", &bt_ktx2_get_dfd_flags);
|
||||
m.def("ktx2_get_dfd_total_samples", &bt_ktx2_get_dfd_total_samples);
|
||||
m.def("ktx2_get_dfd_channel_id0", &bt_ktx2_get_dfd_channel_id0);
|
||||
m.def("ktx2_get_dfd_channel_id1", &bt_ktx2_get_dfd_channel_id1);
|
||||
|
||||
m.def("ktx2_is_video",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_is_video(h)); });
|
||||
|
||||
m.def("ktx2_get_ldr_hdr_upconversion_nit_multiplier",
|
||||
&bt_ktx2_get_ldr_hdr_upconversion_nit_multiplier);
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// KTX2 per-level info
|
||||
// ------------------------------------------------------------------------
|
||||
m.def("ktx2_get_level_orig_width",
|
||||
&bt_ktx2_get_level_orig_width);
|
||||
|
||||
m.def("ktx2_get_level_orig_height",
|
||||
&bt_ktx2_get_level_orig_height);
|
||||
|
||||
m.def("ktx2_get_level_actual_width",
|
||||
&bt_ktx2_get_level_actual_width);
|
||||
|
||||
m.def("ktx2_get_level_actual_height",
|
||||
&bt_ktx2_get_level_actual_height);
|
||||
|
||||
m.def("ktx2_get_level_num_blocks_x",
|
||||
&bt_ktx2_get_level_num_blocks_x);
|
||||
|
||||
m.def("ktx2_get_level_num_blocks_y",
|
||||
&bt_ktx2_get_level_num_blocks_y);
|
||||
|
||||
m.def("ktx2_get_level_total_blocks",
|
||||
&bt_ktx2_get_level_total_blocks);
|
||||
|
||||
m.def("ktx2_get_level_alpha_flag",
|
||||
[](uint64_t h, uint32_t level, uint32_t layer, uint32_t face) {
|
||||
return to_bool(bt_ktx2_get_level_alpha_flag(h, level, layer, face));
|
||||
});
|
||||
|
||||
m.def("ktx2_get_level_iframe_flag",
|
||||
[](uint64_t h, uint32_t level, uint32_t layer, uint32_t face) {
|
||||
return to_bool(bt_ktx2_get_level_iframe_flag(h, level, layer, face));
|
||||
});
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Transcoding state and operations
|
||||
// ------------------------------------------------------------------------
|
||||
m.def("ktx2_start_transcoding",
|
||||
[](uint64_t h) { return to_bool(bt_ktx2_start_transcoding(h)); });
|
||||
|
||||
m.def("ktx2_create_transcode_state",
|
||||
&bt_ktx2_create_transcode_state);
|
||||
|
||||
m.def("ktx2_destroy_transcode_state",
|
||||
&bt_ktx2_destroy_transcode_state);
|
||||
|
||||
m.def("ktx2_transcode_image_level",
|
||||
[](uint64_t ktx2_handle,
|
||||
uint32_t level_index, uint32_t layer_index, uint32_t face_index,
|
||||
uint64_t out_mem_ofs,
|
||||
uint32_t out_blocks_or_pixels,
|
||||
uint32_t transcoder_texture_format_u32,
|
||||
uint32_t decode_flags,
|
||||
uint32_t row_pitch_blocks_or_pixels,
|
||||
uint32_t rows_in_pixels,
|
||||
int channel0, int channel1,
|
||||
uint64_t state_handle)
|
||||
{
|
||||
return to_bool(bt_ktx2_transcode_image_level(
|
||||
ktx2_handle,
|
||||
level_index, layer_index, face_index,
|
||||
out_mem_ofs,
|
||||
out_blocks_or_pixels,
|
||||
transcoder_texture_format_u32,
|
||||
decode_flags,
|
||||
row_pitch_blocks_or_pixels,
|
||||
rows_in_pixels,
|
||||
channel0, channel1,
|
||||
state_handle));
|
||||
},
|
||||
py::arg("ktx2_handle"),
|
||||
py::arg("level_index"),
|
||||
py::arg("layer_index"),
|
||||
py::arg("face_index"),
|
||||
py::arg("output_block_mem_ofs"),
|
||||
py::arg("output_blocks_buf_size_in_blocks_or_pixels"),
|
||||
py::arg("transcoder_texture_format_u32"),
|
||||
py::arg("decode_flags"),
|
||||
py::arg("output_row_pitch_in_blocks_or_pixels") = 0,
|
||||
py::arg("output_rows_in_pixels") = 0,
|
||||
py::arg("channel0") = -1,
|
||||
py::arg("channel1") = -1,
|
||||
py::arg("state_handle") = 0);
|
||||
|
||||
m.def("read_memory",
|
||||
[](uint64_t ptr, uint32_t size) {
|
||||
return py::bytes((const char*)ptr, size);
|
||||
},
|
||||
"Read `size` bytes starting at native memory address `ptr`");
|
||||
|
||||
m.def("write_memory",
|
||||
[](uint64_t dest_ptr, py::buffer src) {
|
||||
py::buffer_info info = src.request();
|
||||
memcpy((void*)dest_ptr, info.ptr, info.size * info.itemsize);
|
||||
},
|
||||
"Write bytes/buffer-like object into native memory at address `ptr`");
|
||||
}
|
||||
332
python/dds_writer.py
Normal file
332
python/dds_writer.py
Normal file
@@ -0,0 +1,332 @@
|
||||
# dds_writer.py
|
||||
#
|
||||
# Minimal DDS writer that mirrors the C/C++ save_dds() implementation you provided.
|
||||
# It writes a DX9-style DDS header, and optionally a DX10 extension header,
|
||||
# followed by the raw compressed blocks.
|
||||
#
|
||||
# No mipmaps, no cubes, no 3D volumes – exactly like the original C code.
|
||||
|
||||
import struct
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FourCC helper (same as PIXEL_FMT_FOURCC macro)
|
||||
# ---------------------------------------------------------------------------
|
||||
def make_fourcc(a: str, b: str, c: str, d: str) -> int:
|
||||
return (ord(a) |
|
||||
(ord(b) << 8) |
|
||||
(ord(c) << 16) |
|
||||
(ord(d) << 24))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DDS-related constants (only the ones we actually use)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# DDSD flags
|
||||
DDSD_CAPS = 0x00000001
|
||||
DDSD_HEIGHT = 0x00000002
|
||||
DDSD_WIDTH = 0x00000004
|
||||
DDSD_PIXELFORMAT= 0x00001000
|
||||
DDSD_LINEARSIZE = 0x00080000
|
||||
|
||||
# DDPF flags
|
||||
DDPF_FOURCC = 0x00000004
|
||||
|
||||
# DDSCAPS flags
|
||||
DDSCAPS_TEXTURE = 0x00001000
|
||||
|
||||
# DXGI_FORMAT subset (values must match the C enum)
|
||||
class DXGI_FORMAT:
|
||||
UNKNOWN = 0
|
||||
BC1_UNORM = 71
|
||||
BC3_UNORM = 77
|
||||
BC4_UNORM = 80
|
||||
BC5_UNORM = 83
|
||||
# You can add more as needed; for DX10 header we just write the integer value.
|
||||
|
||||
# DX10 resource dimension
|
||||
class D3D10_RESOURCE_DIMENSION:
|
||||
UNKNOWN = 0
|
||||
BUFFER = 1
|
||||
TEXTURE1D = 2
|
||||
TEXTURE2D = 3
|
||||
TEXTURE3D = 4
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DDS writer class
|
||||
# ---------------------------------------------------------------------------
|
||||
class DDSWriter:
|
||||
"""
|
||||
Python port of the C save_dds() function.
|
||||
|
||||
Usage:
|
||||
writer = DDSWriter()
|
||||
ok = writer.save_dds(
|
||||
filename="out.dds",
|
||||
width=width,
|
||||
height=height,
|
||||
blocks=bc_data, # bytes or bytearray
|
||||
pixel_format_bpp=4, # e.g. 4 for BC1, 8 for BC3/4/5/etc.
|
||||
dxgi_format=DXGI_FORMAT.BC1_UNORM,
|
||||
srgb=False,
|
||||
force_dx10_header=False,
|
||||
)
|
||||
"""
|
||||
|
||||
DDS_MAGIC = b"DDS " # same as fwrite("DDS ", 4, 1, pFile);
|
||||
|
||||
def save_dds(
|
||||
self,
|
||||
filename: str,
|
||||
width: int,
|
||||
height: int,
|
||||
blocks: Union[bytes, bytearray, memoryview],
|
||||
pixel_format_bpp: int,
|
||||
dxgi_format: int,
|
||||
srgb: bool = False,
|
||||
force_dx10_header: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Port of:
|
||||
bool save_dds(const char* pFilename,
|
||||
uint32_t width, uint32_t height,
|
||||
const void* pBlocks,
|
||||
uint32_t pixel_format_bpp,
|
||||
DXGI_FORMAT dxgi_format,
|
||||
bool srgb,
|
||||
bool force_dx10_header);
|
||||
|
||||
The 'blocks' buffer is written as-is (up to computed linear size).
|
||||
"""
|
||||
|
||||
# srgb is intentionally unused in the original C code (commented logic).
|
||||
_ = srgb
|
||||
|
||||
# Open file like the C code
|
||||
try:
|
||||
f = open(filename, "wb")
|
||||
except OSError:
|
||||
print(f"Failed creating file {filename}!", file=sys.stderr)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Write the "DDS " magic
|
||||
f.write(self.DDS_MAGIC)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Build DDSURFACEDESC2 equivalent
|
||||
# -----------------------------------------------------------------
|
||||
# We'll pack DDSURFACEDESC2 as 31 uint32's (124 bytes) in little-endian:
|
||||
# struct DDSURFACEDESC2 {
|
||||
# uint32 dwSize;
|
||||
# uint32 dwFlags;
|
||||
# uint32 dwHeight;
|
||||
# uint32 dwWidth;
|
||||
# uint32 lPitch_or_dwLinearSize;
|
||||
# uint32 dwBackBufferCount;
|
||||
# uint32 dwMipMapCount;
|
||||
# uint32 dwAlphaBitDepth;
|
||||
# uint32 dwUnused0;
|
||||
# uint32 lpSurface;
|
||||
# DDCOLORKEY unused0; (2 * uint32)
|
||||
# DDCOLORKEY unused1; (2 * uint32)
|
||||
# DDCOLORKEY unused2; (2 * uint32)
|
||||
# DDCOLORKEY unused3; (2 * uint32)
|
||||
# DDPIXELFORMAT ddpfPixelFormat; (8 * uint32)
|
||||
# DDSCAPS2 ddsCaps; (4 * uint32)
|
||||
# uint32 dwUnused1;
|
||||
# };
|
||||
|
||||
dwSize = 124 # sizeof(DDSURFACEDESC2)
|
||||
|
||||
dwFlags = (
|
||||
DDSD_WIDTH |
|
||||
DDSD_HEIGHT |
|
||||
DDSD_PIXELFORMAT |
|
||||
DDSD_CAPS
|
||||
)
|
||||
|
||||
dwWidth = int(width)
|
||||
dwHeight = int(height)
|
||||
|
||||
# lPitch (actually LinearSize for compressed formats), same as:
|
||||
# (((dwWidth + 3) & ~3) * ((dwHeight + 3) & ~3) * pixel_format_bpp) >> 3;
|
||||
lPitch = (
|
||||
((dwWidth + 3) & ~3)
|
||||
* ((dwHeight + 3) & ~3)
|
||||
* int(pixel_format_bpp)
|
||||
) >> 3
|
||||
|
||||
dwFlags |= DDSD_LINEARSIZE
|
||||
|
||||
dwBackBufferCount = 0
|
||||
dwMipMapCount = 0
|
||||
dwAlphaBitDepth = 0
|
||||
dwUnused0 = 0
|
||||
lpSurface = 0
|
||||
|
||||
# DDCOLORKEY unused0..3, all zero
|
||||
ddcolorkey_zero = [0, 0] * 4 # 4 DDCOLORKEY structs
|
||||
|
||||
# DDPIXELFORMAT
|
||||
# struct DDPIXELFORMAT {
|
||||
# uint32 dwSize;
|
||||
# uint32 dwFlags;
|
||||
# uint32 dwFourCC;
|
||||
# uint32 dwRGBBitCount;
|
||||
# uint32 dwRBitMask;
|
||||
# uint32 dwGBitMask;
|
||||
# uint32 dwBBitMask;
|
||||
# uint32 dwRGBAlphaBitMask;
|
||||
# };
|
||||
ddpf_dwSize = 32
|
||||
ddpf_dwFlags = DDPF_FOURCC
|
||||
ddpf_dwFourCC = 0
|
||||
ddpf_dwRGBBitCount = 0
|
||||
ddpf_dwRBitMask = 0
|
||||
ddpf_dwGBitMask = 0
|
||||
ddpf_dwBBitMask = 0
|
||||
ddpf_dwRGBAlphaBitMask = 0
|
||||
|
||||
# DDSCAPS2
|
||||
# struct DDSCAPS2 {
|
||||
# uint32 dwCaps;
|
||||
# uint32 dwCaps2;
|
||||
# uint32 dwCaps3;
|
||||
# uint32 dwCaps4;
|
||||
# };
|
||||
ddsCaps_dwCaps = DDSCAPS_TEXTURE
|
||||
ddsCaps_dwCaps2 = 0
|
||||
ddsCaps_dwCaps3 = 0
|
||||
ddsCaps_dwCaps4 = 0
|
||||
|
||||
dwUnused1 = 0
|
||||
|
||||
# Decide whether to use legacy FourCC (DXT1/DXT5/ATI1/ATI2) or DX10 header
|
||||
use_legacy = (
|
||||
not force_dx10_header and
|
||||
dxgi_format in (
|
||||
DXGI_FORMAT.BC1_UNORM,
|
||||
DXGI_FORMAT.BC3_UNORM,
|
||||
DXGI_FORMAT.BC4_UNORM,
|
||||
DXGI_FORMAT.BC5_UNORM,
|
||||
)
|
||||
)
|
||||
|
||||
if use_legacy:
|
||||
if dxgi_format == DXGI_FORMAT.BC1_UNORM:
|
||||
ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '1')
|
||||
elif dxgi_format == DXGI_FORMAT.BC3_UNORM:
|
||||
ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '5')
|
||||
elif dxgi_format == DXGI_FORMAT.BC4_UNORM:
|
||||
ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '1')
|
||||
elif dxgi_format == DXGI_FORMAT.BC5_UNORM:
|
||||
ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '2')
|
||||
else:
|
||||
# Write DX10 header, FourCC = "DX10"
|
||||
ddpf_dwFourCC = make_fourcc('D', 'X', '1', '0')
|
||||
|
||||
# Build the 31 uint32's for DDSURFACEDESC2
|
||||
header_values = [
|
||||
dwSize,
|
||||
dwFlags,
|
||||
dwHeight,
|
||||
dwWidth,
|
||||
lPitch,
|
||||
dwBackBufferCount,
|
||||
dwMipMapCount,
|
||||
dwAlphaBitDepth,
|
||||
dwUnused0,
|
||||
lpSurface,
|
||||
]
|
||||
|
||||
header_values.extend(ddcolorkey_zero) # 8 uint32's
|
||||
|
||||
ddpf_values = [
|
||||
ddpf_dwSize,
|
||||
ddpf_dwFlags,
|
||||
ddpf_dwFourCC,
|
||||
ddpf_dwRGBBitCount,
|
||||
ddpf_dwRBitMask,
|
||||
ddpf_dwGBitMask,
|
||||
ddpf_dwBBitMask,
|
||||
ddpf_dwRGBAlphaBitMask,
|
||||
]
|
||||
header_values.extend(ddpf_values) # 8 uint32's
|
||||
|
||||
ddsCaps_values = [
|
||||
ddsCaps_dwCaps,
|
||||
ddsCaps_dwCaps2,
|
||||
ddsCaps_dwCaps3,
|
||||
ddsCaps_dwCaps4,
|
||||
]
|
||||
header_values.extend(ddsCaps_values) # 4 uint32's
|
||||
|
||||
header_values.append(dwUnused1) # final uint32
|
||||
|
||||
if len(header_values) != 31:
|
||||
raise RuntimeError("Internal error: DDSURFACEDESC2 must contain 31 uint32's")
|
||||
|
||||
# Pack and write DDSURFACEDESC2
|
||||
dds_header = struct.pack("<31I", *header_values)
|
||||
f.write(dds_header)
|
||||
|
||||
# If needed, write the DX10 header (DDS_HEADER_DXT10)
|
||||
if not use_legacy:
|
||||
# struct DDS_HEADER_DXT10 {
|
||||
# DXGI_FORMAT dxgiFormat;
|
||||
# D3D10_RESOURCE_DIMENSION resourceDimension;
|
||||
# uint32 miscFlag;
|
||||
# uint32 arraySize;
|
||||
# uint32 miscFlags2;
|
||||
# };
|
||||
dxgiFormat = int(dxgi_format)
|
||||
resourceDimension = D3D10_RESOURCE_DIMENSION.TEXTURE2D
|
||||
miscFlag = 0
|
||||
arraySize = 1
|
||||
miscFlags2 = 0
|
||||
|
||||
dxt10_header = struct.pack(
|
||||
"<5I",
|
||||
dxgiFormat,
|
||||
resourceDimension,
|
||||
miscFlag,
|
||||
arraySize,
|
||||
miscFlags2,
|
||||
)
|
||||
f.write(dxt10_header)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Write the actual texture data blocks (pBlocks)
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# C code: fwrite(pBlocks, desc.lPitch, 1, pFile);
|
||||
# i.e. write exactly lPitch bytes.
|
||||
data = memoryview(blocks)
|
||||
if len(data) < lPitch:
|
||||
raise ValueError(
|
||||
f"blocks buffer too small: need at least {lPitch} bytes, got {len(data)}"
|
||||
)
|
||||
f.write(data[:lPitch])
|
||||
|
||||
except Exception as e:
|
||||
# Mimic the C-style error reporting as much as practical
|
||||
print(f"Failed writing to DDS file {filename}: {e}", file=sys.stderr)
|
||||
try:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
# Close file
|
||||
try:
|
||||
f.close()
|
||||
except OSError:
|
||||
print(f"Failed closing DDS file {filename}!", file=sys.stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
413
python/explode_ktx2_file.py
Normal file
413
python/explode_ktx2_file.py
Normal file
@@ -0,0 +1,413 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
explode_ktx2_file.py
|
||||
FULL LDR/HDR KTX2 EXPLODER + FULL API INTROSPECTION + ASTC + BC7/BC6H OUTPUT
|
||||
|
||||
Usage:
|
||||
python3 explode_ktx2_file.py input.ktx2
|
||||
python3 explode_ktx2_file.py input.ktx2 --info-only
|
||||
"""
|
||||
|
||||
# Python Dependencies (beyond basisu_py):
|
||||
# numpy
|
||||
# pillow
|
||||
# imageio (v3+)
|
||||
# wasmtime
|
||||
#
|
||||
# System Dependencies:
|
||||
# OpenImageIO ("oiiotool") -- required for EXR output
|
||||
#
|
||||
# Install Python deps:
|
||||
# pip install numpy pillow imageio wasmtime
|
||||
#
|
||||
# On Ubuntu:
|
||||
# sudo apt install openimageio-tools
|
||||
#
|
||||
# On macOS (Homebrew):
|
||||
# brew install openimageio
|
||||
|
||||
import sys
|
||||
import os
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import tempfile
|
||||
import imageio.v3 as iio
|
||||
from PIL import Image
|
||||
|
||||
from basisu_py import Transcoder
|
||||
from basisu_py.constants import TranscoderTextureFormat as TF
|
||||
|
||||
# Writers located in same directory as this script
|
||||
from astc_writer import write_astc_file
|
||||
from dds_writer import DDSWriter
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# File-writing helpers
|
||||
# ============================================================================
|
||||
def save_exr(path, rgba32f):
|
||||
"""
|
||||
Save float32 RGBA as EXR if possible.
|
||||
If oiiotool is not available, save TIFF instead (Windows-safe).
|
||||
"""
|
||||
import numpy as np
|
||||
import imageio.v3 as iio
|
||||
import subprocess, tempfile, os
|
||||
|
||||
# Write temp TIFF
|
||||
with tempfile.NamedTemporaryFile(suffix=".tiff", delete=False) as tmp:
|
||||
temp_path = tmp.name
|
||||
|
||||
iio.imwrite(temp_path, rgba32f.astype(np.float32))
|
||||
|
||||
# Try EXR via oiiotool
|
||||
try:
|
||||
subprocess.run(["oiiotool", temp_path, "-o", path], check=True)
|
||||
os.remove(temp_path)
|
||||
print(" Wrote EXR:", path)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
# --- FALLBACK: save TIFF ---
|
||||
fallback_path = path + ".tiff"
|
||||
|
||||
# Windows cannot overwrite files via rename(), so remove first
|
||||
if os.path.exists(fallback_path):
|
||||
os.remove(fallback_path)
|
||||
|
||||
# os.replace() always overwrites
|
||||
os.replace(temp_path, fallback_path)
|
||||
|
||||
print(" [Fallback] Wrote TIFF instead:", fallback_path)
|
||||
|
||||
|
||||
def save_png(path, rgba8):
|
||||
img = Image.fromarray(rgba8, mode="RGBA")
|
||||
img.save(path)
|
||||
print(f" PNG saved: {path}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Pretty header
|
||||
# ============================================================================
|
||||
def print_header(title):
|
||||
print("\n" + "=" * 90)
|
||||
print(title)
|
||||
print("=" * 90)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Full top-level metadata dump (ALL API)
|
||||
# ============================================================================
|
||||
def dump_all_top_level(t, h):
|
||||
print_header("TOP-LEVEL KTX2 METADATA FULL API")
|
||||
|
||||
print("Backend :", t.backend_name)
|
||||
print("Version :", t.get_version())
|
||||
print("Width :", t.get_width(h))
|
||||
print("Height :", t.get_height(h))
|
||||
print("Levels :", t.get_levels(h))
|
||||
print("Faces :", t.get_faces(h))
|
||||
|
||||
layers = t.get_layers(h)
|
||||
eff_layers = layers if layers > 0 else 1
|
||||
print("Layers (raw) :", layers)
|
||||
print("Layers (effective) :", eff_layers)
|
||||
|
||||
fmt = t.get_basis_tex_format(h)
|
||||
print("\nBasisTexFormat :", fmt)
|
||||
|
||||
print("\nKTX2 Format Flags:")
|
||||
print(" is_etc1s :", t.is_etc1s(h))
|
||||
print(" is_uastc_ldr_4x4 :", t.is_uastc_ldr_4x4(h))
|
||||
print(" is_xuastc_ldr :", t.is_xuastc_ldr(h))
|
||||
print(" is_astc_ldr :", t.is_astc_ldr(h))
|
||||
print(" is_hdr :", t.is_hdr(h))
|
||||
print(" is_hdr_4x4 :", t.is_hdr_4x4(h))
|
||||
print(" is_hdr_6x6 :", t.is_hdr_6x6(h))
|
||||
print(" is_ldr :", t.is_ldr(h))
|
||||
print(" is_srgb :", t.is_srgb(h))
|
||||
print(" is_video :", t.is_video(h))
|
||||
print(" has_alpha :", t.has_alpha(h))
|
||||
|
||||
print("\nBlock Info:")
|
||||
print(" block_width :", t.get_block_width(h))
|
||||
print(" block_height :", t.get_block_height(h))
|
||||
|
||||
print("\nDFD Info:")
|
||||
print(" color_model :", t.get_dfd_color_model(h))
|
||||
print(" color_primaries :", t.get_dfd_color_primaries(h))
|
||||
print(" transfer_func :", t.get_dfd_transfer_func(h))
|
||||
print(" flags :", t.get_dfd_flags(h))
|
||||
print(" total_samples :", t.get_dfd_total_samples(h))
|
||||
print(" channel_id0 :", t.get_dfd_channel_id0(h))
|
||||
print(" channel_id1 :", t.get_dfd_channel_id1(h))
|
||||
|
||||
if t.is_hdr(h):
|
||||
print(" hdr_nit_multiplier :", t.get_ldr_hdr_upconversion_nit_multiplier(h))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BasisTexFormat helpers
|
||||
# ============================================================================
|
||||
def dump_basis_tex_format_helpers(t, h):
|
||||
print_header("BasisTexFormat HELPERS (FULL)")
|
||||
|
||||
fmt = t.get_basis_tex_format(h)
|
||||
print("basis_tex_format:", fmt)
|
||||
|
||||
print("is_xuastc_ldr :", t.basis_tex_format_is_xuastc_ldr(fmt))
|
||||
print("is_astc_ldr :", t.basis_tex_format_is_astc_ldr(fmt))
|
||||
print("block width :", t.basis_tex_format_get_block_width(fmt))
|
||||
print("block height :", t.basis_tex_format_get_block_height(fmt))
|
||||
print("is_hdr :", t.basis_tex_format_is_hdr(fmt))
|
||||
print("is_ldr :", t.basis_tex_format_is_ldr(fmt))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Level / Layer / Face metadata dump
|
||||
# ============================================================================
|
||||
def dump_per_level_info(t, h):
|
||||
print_header("PER-LEVEL / PER-LAYER / PER-FACE METADATA")
|
||||
|
||||
levels = t.get_levels(h)
|
||||
faces = t.get_faces(h)
|
||||
layers = t.get_layers(h)
|
||||
if layers == 0:
|
||||
layers = 1
|
||||
|
||||
for level in range(levels):
|
||||
for layer in range(layers):
|
||||
for face in range(faces):
|
||||
print(f"\nLevel={level}, Layer={layer}, Face={face}")
|
||||
print(" orig_width :", t.get_level_orig_width(h, level, layer, face))
|
||||
print(" orig_height :", t.get_level_orig_height(h, level, layer, face))
|
||||
print(" actual_width :", t.get_level_actual_width(h, level, layer, face))
|
||||
print(" actual_height:", t.get_level_actual_height(h, level, layer, face))
|
||||
print(" blocks_x :", t.get_level_num_blocks_x(h, level, layer, face))
|
||||
print(" blocks_y :", t.get_level_num_blocks_y(h, level, layer, face))
|
||||
print(" total_blocks :", t.get_level_total_blocks(h, level, layer, face))
|
||||
print(" alpha_flag :", t.get_level_alpha_flag(h, level, layer, face))
|
||||
print(" iframe_flag :", t.get_level_iframe_flag(h, level, layer, face))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ASTC Selection
|
||||
# ============================================================================
|
||||
def choose_astc_format(t, h):
|
||||
fmt = t.get_basis_tex_format(h)
|
||||
tfmt = t.basis_get_transcoder_texture_format_from_basis_tex_format(fmt)
|
||||
bw = t.basis_get_block_width(tfmt)
|
||||
bh = t.basis_get_block_height(tfmt)
|
||||
|
||||
print_header("ASTC SELECTION")
|
||||
print("ASTC TF:", tfmt)
|
||||
print(f"Block dims: {bw}x{bh}")
|
||||
return tfmt, bw, bh
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BC Format Selection
|
||||
# ============================================================================
|
||||
def choose_bc_format(t, h):
|
||||
if t.is_hdr(h):
|
||||
print_header("HDR -> BC6H")
|
||||
return TF.TF_BC6H, 8, 95 # DXGI_FORMAT_BC6H_UF16
|
||||
else:
|
||||
print_header("LDR -> BC7")
|
||||
return TF.TF_BC7_RGBA, 8, 98 # DXGI_FORMAT_BC7_UNORM
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Full explode transcoding (using handle API + per-level dims)
|
||||
# ============================================================================
|
||||
def explode_transcode(t, h):
|
||||
levels = t.get_levels(h)
|
||||
faces = t.get_faces(h)
|
||||
layers = t.get_layers(h)
|
||||
if layers == 0:
|
||||
layers = 1
|
||||
|
||||
astc_tfmt, astc_bw, astc_bh = choose_astc_format(t, h)
|
||||
bc_tfmt, bc_bpp, bc_dxgi = choose_bc_format(t, h)
|
||||
|
||||
ddsw = DDSWriter()
|
||||
print_header("BEGIN EXPLODE TRANSCODING (handle API)")
|
||||
|
||||
for level in range(levels):
|
||||
for layer in range(layers):
|
||||
for face in range(faces):
|
||||
|
||||
print(f"\n- Level={level} Layer={layer} Face={face}")
|
||||
|
||||
ow = t.get_level_orig_width(h, level, layer, face)
|
||||
oh = t.get_level_orig_height(h, level, layer, face)
|
||||
print(f" Level orig dims: {ow}x{oh}")
|
||||
|
||||
# ASTC
|
||||
astc_blocks = t.transcode_tfmt_handle(
|
||||
h, astc_tfmt,
|
||||
level=level, layer=layer, face=face,
|
||||
decode_flags=0, channel0=-1, channel1=-1
|
||||
)
|
||||
astc_name = f"astc_L{level}_Y{layer}_F{face}.astc"
|
||||
write_astc_file(astc_name, astc_blocks, astc_bw, astc_bh, ow, oh)
|
||||
print(" ASTC saved:", astc_name)
|
||||
|
||||
# BC6H / BC7
|
||||
bc_blocks = t.transcode_tfmt_handle(
|
||||
h, bc_tfmt,
|
||||
level=level, layer=layer, face=face,
|
||||
decode_flags=0, channel0=-1, channel1=-1
|
||||
)
|
||||
if t.is_hdr(h):
|
||||
dds_name = f"bc6h_L{level}_Y{layer}_F{face}.dds"
|
||||
else:
|
||||
dds_name = f"bc7_L{level}_Y{layer}_F{face}.dds"
|
||||
|
||||
ddsw.save_dds(
|
||||
dds_name,
|
||||
width=ow, height=oh,
|
||||
blocks=bc_blocks,
|
||||
pixel_format_bpp=bc_bpp,
|
||||
dxgi_format=bc_dxgi,
|
||||
srgb=False,
|
||||
force_dx10_header=True,
|
||||
)
|
||||
print(" DDS saved :", dds_name)
|
||||
|
||||
print_header("EXPLODE TRANSCODING COMPLETE")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Decode each (Level, Layer, Face) to PNG or EXR
|
||||
# ============================================================================
|
||||
def explode_decode_images(t, h):
|
||||
print_header("BEGIN EXPLODE IMAGE DECODE (PNG/EXR)")
|
||||
|
||||
levels = t.get_levels(h)
|
||||
faces = t.get_faces(h)
|
||||
layers = t.get_layers(h)
|
||||
if layers == 0:
|
||||
layers = 1
|
||||
|
||||
hdr = t.is_hdr(h)
|
||||
|
||||
for level in range(levels):
|
||||
for layer in range(layers):
|
||||
for face in range(faces):
|
||||
|
||||
print(f"\n- Decode Level={level} Layer={layer} Face={face}")
|
||||
|
||||
ow = t.get_level_orig_width(h, level, layer, face)
|
||||
oh = t.get_level_orig_height(h, level, layer, face)
|
||||
|
||||
if hdr:
|
||||
rgba32f = t.decode_rgba_hdr_handle(h, level, layer, face)
|
||||
outname = f"exr_L{level}_Y{layer}_F{face}.exr"
|
||||
save_exr(outname, rgba32f)
|
||||
else:
|
||||
rgba8 = t.decode_rgba_handle(h, level, layer, face)
|
||||
outname = f"png_L{level}_Y{layer}_F{face}.png"
|
||||
save_png(outname, rgba8)
|
||||
|
||||
print_header("IMAGE DECODE COMPLETE")
|
||||
|
||||
def dump_transcoder_texture_format_helpers(t):
|
||||
print_header("TranscoderTextureFormat HELPERS (FULL)")
|
||||
|
||||
test_formats = [
|
||||
# uncompressed
|
||||
TF.TF_RGBA32, TF.TF_RGB565, TF.TF_BGR565,
|
||||
TF.TF_RGBA4444, TF.TF_RGB_HALF, TF.TF_RGBA_HALF, TF.TF_RGB_9E5,
|
||||
|
||||
# basic compressed
|
||||
TF.TF_ETC1_RGB, TF.TF_ETC2_RGBA,
|
||||
TF.TF_BC1_RGB, TF.TF_BC3_RGBA,
|
||||
TF.TF_BC4_R, TF.TF_BC5_RG,
|
||||
TF.TF_BC7_RGBA, TF.TF_BC6H,
|
||||
TF.TF_ETC2_EAC_R11, TF.TF_ETC2_EAC_RG11,
|
||||
TF.TF_FXT1_RGB,
|
||||
TF.TF_PVRTC1_4_RGB, TF.TF_PVRTC1_4_RGBA,
|
||||
TF.TF_PVRTC2_4_RGB, TF.TF_PVRTC2_4_RGBA,
|
||||
TF.TF_ATC_RGB, TF.TF_ATC_RGBA,
|
||||
|
||||
# HDR ASTC
|
||||
TF.TF_ASTC_HDR_4X4_RGBA,
|
||||
TF.TF_ASTC_HDR_6X6_RGBA,
|
||||
|
||||
# LDR ASTC
|
||||
TF.TF_ASTC_LDR_4X4_RGBA,
|
||||
TF.TF_ASTC_LDR_5X4_RGBA, TF.TF_ASTC_LDR_5X5_RGBA,
|
||||
TF.TF_ASTC_LDR_6X5_RGBA, TF.TF_ASTC_LDR_6X6_RGBA,
|
||||
TF.TF_ASTC_LDR_8X5_RGBA, TF.TF_ASTC_LDR_8X6_RGBA,
|
||||
TF.TF_ASTC_LDR_10X5_RGBA, TF.TF_ASTC_LDR_10X6_RGBA,
|
||||
TF.TF_ASTC_LDR_8X8_RGBA, TF.TF_ASTC_LDR_10X8_RGBA,
|
||||
TF.TF_ASTC_LDR_10X10_RGBA, TF.TF_ASTC_LDR_12X10_RGBA,
|
||||
TF.TF_ASTC_LDR_12X12_RGBA,
|
||||
]
|
||||
|
||||
for tfmt in test_formats:
|
||||
print(f"\nTF={tfmt}")
|
||||
print(" has_alpha :", t.basis_transcoder_format_has_alpha(tfmt))
|
||||
print(" is_hdr :", t.basis_transcoder_format_is_hdr(tfmt))
|
||||
print(" is_ldr :", t.basis_transcoder_format_is_ldr(tfmt))
|
||||
print(" is_astc :", t.basis_transcoder_texture_format_is_astc(tfmt))
|
||||
print(" is_uncompressed :", t.basis_transcoder_format_is_uncompressed(tfmt))
|
||||
print(" bytes/block :", t.basis_get_bytes_per_block_or_pixel(tfmt))
|
||||
print(" block_width :", t.basis_get_block_width(tfmt))
|
||||
print(" block_height :", t.basis_get_block_height(tfmt))
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python explode_ktx2_file.py input.ktx2 [--info-only] [--print-tf]")
|
||||
return 1
|
||||
|
||||
args = sys.argv[1:]
|
||||
info_only = "--info-only" in args
|
||||
print_tf = "--print-tf" in args or "--transcoder-formats" in args
|
||||
|
||||
# Determine input filename
|
||||
input_file = None
|
||||
for a in args:
|
||||
if not a.startswith("--"):
|
||||
input_file = a
|
||||
break
|
||||
|
||||
if input_file is None:
|
||||
print("Error: No input file provided.")
|
||||
return 1
|
||||
|
||||
ktx_bytes = open(input_file, "rb").read()
|
||||
|
||||
t = Transcoder()
|
||||
h = t.open(ktx_bytes)
|
||||
t.start_transcoding(h)
|
||||
|
||||
# Full metadata
|
||||
dump_all_top_level(t, h)
|
||||
dump_basis_tex_format_helpers(t, h)
|
||||
dump_per_level_info(t, h)
|
||||
|
||||
# Optional TF helpers
|
||||
if print_tf:
|
||||
dump_transcoder_texture_format_helpers(t)
|
||||
|
||||
if info_only:
|
||||
print_header("INFO-ONLY MODE NO FILES WRITTEN")
|
||||
t.close(h)
|
||||
return 0
|
||||
|
||||
# Full output
|
||||
explode_transcode(t, h)
|
||||
explode_decode_images(t, h)
|
||||
|
||||
t.close(h)
|
||||
print("Success")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
1
python/lowlevel_test_native/__init__.py
Normal file
1
python/lowlevel_test_native/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# __init__.py
|
||||
127
python/lowlevel_test_native/basic_test.py
Normal file
127
python/lowlevel_test_native/basic_test.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# basic_test.py
|
||||
import sys
|
||||
sys.path.append("basisu_py") # make sure Python can load the .so
|
||||
|
||||
import basisu_python as bu
|
||||
from constants import *
|
||||
|
||||
import ctypes
|
||||
import math
|
||||
|
||||
def generate_swirl_rgba8(width, height):
|
||||
"""
|
||||
Generate a smooth colorful swirl procedural RGBA8 test image.
|
||||
Returns: a ctypes array of type (c_ubyte * (width * height * 4))
|
||||
"""
|
||||
pixel_count = width * height * 4
|
||||
img = (ctypes.c_ubyte * pixel_count)()
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
i = (y * width + x) * 4
|
||||
|
||||
dx = x - width / 2
|
||||
dy = y - height / 2
|
||||
|
||||
dist = math.hypot(dx, dy)
|
||||
angle = math.atan2(dy, dx)
|
||||
|
||||
# Color swirl pattern
|
||||
r = int((math.sin(dist * 0.15) * 0.5 + 0.5) * 255)
|
||||
g = int((math.sin(angle * 3.0) * 0.5 + 0.5) * 255)
|
||||
b = int((math.cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5) * 255)
|
||||
|
||||
img[i + 0] = r & 255
|
||||
img[i + 1] = g & 255
|
||||
img[i + 2] = b & 255
|
||||
img[i + 3] = 255
|
||||
|
||||
return img
|
||||
|
||||
def generate_test_pattern_rgba8(width, height):
|
||||
"""
|
||||
Generate a simple deterministic RGBA8 test pattern:
|
||||
R = x
|
||||
G = y
|
||||
B = x^y
|
||||
A = 255
|
||||
"""
|
||||
import ctypes
|
||||
|
||||
pixel_count = width * height * 4
|
||||
img = (ctypes.c_ubyte * pixel_count)()
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
i = (y * width + x) * 4
|
||||
|
||||
img[i + 0] = x & 0xFF
|
||||
img[i + 1] = y & 0xFF
|
||||
img[i + 2] = (x ^ y) & 0xFF
|
||||
img[i + 3] = 255
|
||||
|
||||
return img
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# BasisU compression test (NATIVE C++)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
print("Native BasisU version:", bu.get_version())
|
||||
bu.init()
|
||||
|
||||
# Create comp params
|
||||
params = bu.new_params()
|
||||
print("Params handle:", params)
|
||||
|
||||
# Create RGBA8 swirl (64 x 64)
|
||||
W, H = 512, 512
|
||||
pixel_count = W * H * 4
|
||||
|
||||
# Generate swirl image in PYTHON memory
|
||||
|
||||
img = generate_swirl_rgba8(W, H)
|
||||
#img = generate_test_pattern_rgba8(W, H)
|
||||
|
||||
# Allocate memory inside NATIVE C++ heap
|
||||
img_ptr = bu.alloc(pixel_count)
|
||||
|
||||
# Copy Python swirl image ? C++ heap buffer
|
||||
ctypes.memmove(img_ptr, img, pixel_count)
|
||||
|
||||
# Set into BasisU
|
||||
pitch = W * 4
|
||||
ok = bu.set_image_rgba32(params, 0, img_ptr, W, H, pitch)
|
||||
print("Set image:", ok)
|
||||
|
||||
# Compress (UASTC LDR 4x4 = 1)
|
||||
ok = bu.compress(
|
||||
params,
|
||||
BasisTexFormat.cASTC_LDR_4x4, # basis_tex_format
|
||||
BasisQuality.MAX, # quality
|
||||
BasisEffort.DEFAULT, # effort
|
||||
BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB | BasisFlags.THREADED | BasisFlags.DEBUG_OUTPUT | BasisFlags.VERBOSE, # flags
|
||||
0.0 # rdo
|
||||
)
|
||||
print("Compress:", ok)
|
||||
|
||||
# Retrieve compressed data
|
||||
size = bu.get_comp_data_size(params)
|
||||
ofs = bu.get_comp_data_ofs(params)
|
||||
|
||||
print("Output size =", size, "ptr =", ofs)
|
||||
|
||||
# Copy bytes out of native memory
|
||||
byte_ptr = ctypes.cast(ofs, ctypes.POINTER(ctypes.c_ubyte))
|
||||
blob = bytes(byte_ptr[i] for i in range(size))
|
||||
|
||||
print("First 16 bytes:", blob[:16])
|
||||
|
||||
# Save to KTX2
|
||||
with open("out_native.ktx2", "wb") as f:
|
||||
f.write(blob)
|
||||
|
||||
print("Saved out_native.ktx2")
|
||||
|
||||
# Cleanup
|
||||
bu.delete_params(params)
|
||||
bu.free(img_ptr)
|
||||
481
python/lowlevel_test_native/example_capi_python.py
Normal file
481
python/lowlevel_test_native/example_capi_python.py
Normal file
@@ -0,0 +1,481 @@
|
||||
#!/usr/bin/env python3
|
||||
# example_capi_python.py
|
||||
#
|
||||
# Simple Python port of example_capi.c using native C++ pybind11 bindings:
|
||||
# - basisu_python (encoder)
|
||||
# - basisu_transcoder_python (transcoder)
|
||||
#
|
||||
# Requires:
|
||||
# basisu_py/basisu_python*.so
|
||||
# basisu_py/basisu_transcoder_python*.so
|
||||
# basisu_py/constants.py
|
||||
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
import ctypes
|
||||
|
||||
# Make sure Python can see the native .so's and the shared constants
|
||||
sys.path.append("basisu_py")
|
||||
|
||||
import basisu_python as bu
|
||||
import basisu_transcoder_python as bt
|
||||
from constants import BasisTexFormat, BasisFlags
|
||||
from constants import TranscoderTextureFormat as TF
|
||||
from constants import TranscodeDecodeFlags as DF
|
||||
|
||||
TRUE = 1
|
||||
FALSE = 0
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Utility: write raw bytes to a file
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def write_blob_to_file(filename: str, data: bytes) -> int:
|
||||
print(f"write_blob_to_file: writing {len(data)} bytes to {filename!r}")
|
||||
if not filename or data is None:
|
||||
print(" ERROR: invalid filename or data")
|
||||
return FALSE
|
||||
|
||||
try:
|
||||
with open(filename, "wb") as f:
|
||||
f.write(data)
|
||||
print(" OK")
|
||||
return TRUE
|
||||
except OSError as e:
|
||||
print(" ERROR:", e)
|
||||
return FALSE
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TGA writer (24/32bpp) - port of write_tga_image()
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def write_tga_image(filename: str, w: int, h: int, has_alpha: bool, pixels_rgba_ptr: int) -> int:
|
||||
"""
|
||||
filename: path to TGA file
|
||||
w, h: image dimensions
|
||||
has_alpha: True for 32bpp, False for 24bpp
|
||||
pixels_rgba_ptr: C pointer (uint64) to RGBA or RGB data in native heap
|
||||
"""
|
||||
print(f"write_tga_image: {filename!r}, {w}x{h}, has_alpha={has_alpha}, ptr=0x{pixels_rgba_ptr:x}")
|
||||
if not filename or pixels_rgba_ptr == 0 or w <= 0 or h <= 0:
|
||||
print(" ERROR: invalid args")
|
||||
return -1
|
||||
|
||||
bytes_per_pixel = 4 if has_alpha else 3
|
||||
row_bytes = w * bytes_per_pixel
|
||||
total_bytes = row_bytes * h
|
||||
|
||||
# Create a ctypes buffer that views the native memory
|
||||
SrcArrayType = ctypes.c_ubyte * total_bytes
|
||||
src = SrcArrayType.from_address(pixels_rgba_ptr)
|
||||
|
||||
try:
|
||||
with open(filename, "wb") as f:
|
||||
header = bytearray(18)
|
||||
header[2] = 2 # uncompressed true-color
|
||||
header[12] = w & 0xFF
|
||||
header[13] = (w >> 8) & 0xFF
|
||||
header[14] = h & 0xFF
|
||||
header[15] = (h >> 8) & 0xFF
|
||||
header[16] = 32 if has_alpha else 24
|
||||
header[17] = 8 if has_alpha else 0 # bottom-left origin (with or without alpha)
|
||||
|
||||
f.write(header)
|
||||
|
||||
# temp row buffer for BGRA/BGR
|
||||
row_buf = bytearray(row_bytes)
|
||||
|
||||
# TGA expects rows bottom-to-top
|
||||
for y in range(h):
|
||||
src_y = h - 1 - y
|
||||
row_start = src_y * row_bytes
|
||||
src_row = src[row_start:row_start + row_bytes]
|
||||
|
||||
if has_alpha:
|
||||
# RGBA -> BGRA
|
||||
for x in range(w):
|
||||
si = x*4
|
||||
di = x*4
|
||||
row_buf[di + 0] = src_row[si + 2] # B
|
||||
row_buf[di + 1] = src_row[si + 1] # G
|
||||
row_buf[di + 2] = src_row[si + 0] # R
|
||||
row_buf[di + 3] = src_row[si + 3] # A
|
||||
else:
|
||||
# RGB -> BGR
|
||||
for x in range(w):
|
||||
si = x*3
|
||||
di = x*3
|
||||
row_buf[di + 0] = src_row[si + 2] # B
|
||||
row_buf[di + 1] = src_row[si + 1] # G
|
||||
row_buf[di + 2] = src_row[si + 0] # R
|
||||
|
||||
f.write(row_buf)
|
||||
|
||||
print(" Wrote TGA:", filename)
|
||||
return 0
|
||||
except OSError as e:
|
||||
print(" ERROR writing TGA:", e)
|
||||
return -2
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ASTC writer - port of write_astc_file()
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def write_astc_file(filename: str,
|
||||
blocks_ptr: int,
|
||||
block_width: int,
|
||||
block_height: int,
|
||||
dim_x: int,
|
||||
dim_y: int) -> int:
|
||||
print(f"write_astc_file: {filename!r}, block={block_width}x{block_height}, dim={dim_x}x{dim_y}, ptr=0x{blocks_ptr:x}")
|
||||
if not filename or blocks_ptr == 0:
|
||||
print(" ERROR: invalid filename or pointer")
|
||||
return 0
|
||||
|
||||
assert dim_x > 0 and dim_y > 0
|
||||
assert 4 <= block_width <= 12
|
||||
assert 4 <= block_height <= 12
|
||||
|
||||
num_blocks_x = (dim_x + block_width - 1) // block_width
|
||||
num_blocks_y = (dim_y + block_height - 1) // block_height
|
||||
total_blocks = num_blocks_x * num_blocks_y
|
||||
total_bytes = total_blocks * 16 # 16 bytes per ASTC block
|
||||
|
||||
print(f" num_blocks_x={num_blocks_x}, num_blocks_y={num_blocks_y}, total_blocks={total_blocks}, total_bytes={total_bytes}")
|
||||
|
||||
# View native memory
|
||||
BlockArray = ctypes.c_ubyte * total_bytes
|
||||
src = BlockArray.from_address(blocks_ptr)
|
||||
|
||||
try:
|
||||
with open(filename, "wb") as f:
|
||||
# Magic
|
||||
f.write(bytes([0x13, 0xAB, 0xA1, 0x5C]))
|
||||
|
||||
# Block dimensions x,y,z (=1)
|
||||
f.write(bytes([block_width & 0xFF, block_height & 0xFF, 1]))
|
||||
|
||||
# dim_x (24-bit LE)
|
||||
f.write(bytes([dim_x & 0xFF, (dim_x >> 8) & 0xFF, (dim_x >> 16) & 0xFF]))
|
||||
|
||||
# dim_y (24-bit LE)
|
||||
f.write(bytes([dim_y & 0xFF, (dim_y >> 8) & 0xFF, (dim_y >> 16) & 0xFF]))
|
||||
|
||||
# dim_z = 1 (24-bit LE)
|
||||
f.write(bytes([1, 0, 0]))
|
||||
|
||||
# Block data
|
||||
f.write(bytes(src))
|
||||
|
||||
print(" Wrote ASTC:", filename)
|
||||
return 1
|
||||
except OSError as e:
|
||||
print(" ERROR writing ASTC:", e)
|
||||
return 0
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Procedural RGBA pattern (ported & fixed version)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def create_pretty_rgba_pattern(w: int, h: int) -> bytes:
|
||||
print(f"create_pretty_rgba_pattern: {w}x{h}")
|
||||
if w <= 0 or h <= 0:
|
||||
return None
|
||||
|
||||
out = bytearray(w * h * 4)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
fx = x / float(w)
|
||||
fy = y / float(h)
|
||||
|
||||
# Colorful plasma-type formula
|
||||
v = math.sin(fx * 12.0 + fy * 4.0)
|
||||
v += math.sin(fy * 9.0 - fx * 6.0)
|
||||
v += math.sin((fx + fy) * 7.0)
|
||||
v = v * 0.25 + 0.5 # scale 0..1
|
||||
|
||||
L = 1.5
|
||||
|
||||
r = int(round(255.0 * math.sin(v * 6.28) * L))
|
||||
g = int(round(255.0 * (1.0 - v) * L))
|
||||
b = int(round(255.0 * v * L))
|
||||
|
||||
if r < 0: r = 0
|
||||
elif r > 255: r = 255
|
||||
if g < 0: g = 0
|
||||
elif g > 255: g = 255
|
||||
if b < 0: b = 0
|
||||
elif b > 255: b = 255
|
||||
|
||||
i = (y * w + x) * 4
|
||||
out[i+0] = r
|
||||
out[i+1] = g
|
||||
out[i+2] = b
|
||||
out[i+3] = 255
|
||||
|
||||
return bytes(out)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Transcode a KTX2 blob (ported from transcode_ktx2_file)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def transcode_ktx2_file(ktx2_data: bytes) -> int:
|
||||
if not ktx2_data:
|
||||
print("transcode_ktx2_file: empty data")
|
||||
return FALSE
|
||||
|
||||
size = len(ktx2_data)
|
||||
print(f"transcode_ktx2_file: size={size} bytes")
|
||||
|
||||
if size > 0xFFFFFFFF:
|
||||
print(" ERROR: size too large for 32-bit length")
|
||||
return FALSE
|
||||
|
||||
# Allocate memory in transcoder heap and copy KTX2 data
|
||||
ktx2_data_ofs = bt.alloc(size)
|
||||
if not ktx2_data_ofs:
|
||||
print(" ERROR: bt.alloc failed")
|
||||
return FALSE
|
||||
|
||||
print(f" KTX2 data allocated at 0x{ktx2_data_ofs:x}")
|
||||
ctypes.memmove(ktx2_data_ofs, ktx2_data, size)
|
||||
|
||||
# Open KTX2
|
||||
ktx2_handle = bt.ktx2_open(ktx2_data_ofs, size)
|
||||
if not ktx2_handle:
|
||||
print(" ERROR: bt.ktx2_open failed")
|
||||
bt.free(ktx2_data_ofs)
|
||||
return FALSE
|
||||
|
||||
print(f" KTX2 handle = 0x{ktx2_handle:x}")
|
||||
|
||||
if not bt.ktx2_is_ldr(ktx2_handle):
|
||||
print(" ERROR: This sample only handles LDR KTX2 files")
|
||||
bt.ktx2_close(ktx2_handle)
|
||||
bt.free(ktx2_data_ofs)
|
||||
return FALSE
|
||||
|
||||
if not bt.ktx2_start_transcoding(ktx2_handle):
|
||||
print(" ERROR: bt.ktx2_start_transcoding failed")
|
||||
bt.ktx2_close(ktx2_handle)
|
||||
bt.free(ktx2_data_ofs)
|
||||
return FALSE
|
||||
|
||||
width = bt.ktx2_get_width(ktx2_handle)
|
||||
height = bt.ktx2_get_height(ktx2_handle)
|
||||
levels = bt.ktx2_get_levels(ktx2_handle)
|
||||
faces = bt.ktx2_get_faces(ktx2_handle)
|
||||
layers = bt.ktx2_get_layers(ktx2_handle)
|
||||
|
||||
basis_tex_format = bt.ktx2_get_basis_tex_format(ktx2_handle)
|
||||
block_width = bt.ktx2_get_block_width(ktx2_handle)
|
||||
block_height = bt.ktx2_get_block_height(ktx2_handle)
|
||||
is_srgb = bt.ktx2_is_srgb(ktx2_handle)
|
||||
|
||||
print(f"KTX2 Dimensions: {width}x{height}, Levels={levels}, Faces={faces}, Layers={layers}")
|
||||
print(f"basis_tex_format: {basis_tex_format}")
|
||||
print(f"Block dimensions: {block_width}x{block_height}")
|
||||
print(f"is sRGB: {is_srgb}")
|
||||
|
||||
if layers < 1:
|
||||
layers = 1
|
||||
|
||||
assert width >= 1 and height >= 1
|
||||
assert levels >= 1
|
||||
assert faces in (1, 6)
|
||||
|
||||
# Optional: separate transcode state (thread-local)
|
||||
trans_state = bt.ktx2_create_transcode_state()
|
||||
print(f"trans_state handle = 0x{trans_state:x}")
|
||||
|
||||
for level_index in range(levels):
|
||||
for layer_index in range(layers):
|
||||
for face_index in range(faces):
|
||||
print(f"- Level {level_index}, layer {layer_index}, face {face_index}")
|
||||
ow = bt.ktx2_get_level_orig_width(ktx2_handle, level_index, layer_index, face_index)
|
||||
oh = bt.ktx2_get_level_orig_height(ktx2_handle, level_index, layer_index, face_index)
|
||||
aw = bt.ktx2_get_level_actual_width(ktx2_handle, level_index, layer_index, face_index)
|
||||
ah = bt.ktx2_get_level_actual_height(ktx2_handle, level_index, layer_index, face_index)
|
||||
nbx = bt.ktx2_get_level_num_blocks_x(ktx2_handle, level_index, layer_index, face_index)
|
||||
nby = bt.ktx2_get_level_num_blocks_y(ktx2_handle, level_index, layer_index, face_index)
|
||||
tblocks = bt.ktx2_get_level_total_blocks(ktx2_handle, level_index, layer_index, face_index)
|
||||
alpha_flag = bt.ktx2_get_level_alpha_flag(ktx2_handle, level_index, layer_index, face_index)
|
||||
iframe_flag = bt.ktx2_get_level_iframe_flag(ktx2_handle, level_index, layer_index, face_index)
|
||||
|
||||
print(f" Orig dimensions: {ow}x{oh}, actual: {aw}x{ah}")
|
||||
print(f" Block dims: {nbx}x{nby}, total blocks: {tblocks}")
|
||||
print(f" Alpha={alpha_flag}, I-frame={iframe_flag}")
|
||||
|
||||
# 1) Transcode to RGBA32 and write TGA
|
||||
tga_name = f"transcoded_{level_index}_{layer_index}_{face_index}.tga"
|
||||
trans_size_rgba = bt.basis_compute_transcoded_image_size_in_bytes(TF.TF_RGBA32, ow, oh)
|
||||
assert trans_size_rgba > 0
|
||||
rgba_ofs = bt.alloc(trans_size_rgba)
|
||||
print(f" RGBA buf ofs=0x{rgba_ofs:x}, size={trans_size_rgba}")
|
||||
|
||||
decode_flags = 0
|
||||
ok = bt.ktx2_transcode_image_level(
|
||||
ktx2_handle,
|
||||
level_index, layer_index, face_index,
|
||||
rgba_ofs,
|
||||
trans_size_rgba,
|
||||
TF.TF_RGBA32,
|
||||
decode_flags,
|
||||
0, 0, -1, -1,
|
||||
trans_state
|
||||
)
|
||||
print(" ktx2_transcode_image_level(RGBA32):", ok)
|
||||
if not ok:
|
||||
bt.free(rgba_ofs)
|
||||
bt.ktx2_destroy_transcode_state(trans_state)
|
||||
bt.ktx2_close(ktx2_handle)
|
||||
bt.free(ktx2_data_ofs)
|
||||
return FALSE
|
||||
|
||||
write_tga_image(tga_name, ow, oh, True, rgba_ofs)
|
||||
bt.free(rgba_ofs)
|
||||
|
||||
# 2) Transcode to ASTC and write .astc file
|
||||
astc_name = f"transcoded_{level_index}_{layer_index}_{face_index}.astc"
|
||||
target_tf = bt.basis_get_transcoder_texture_format_from_basis_tex_format(basis_tex_format)
|
||||
print(f" Target ASTC TF={target_tf}")
|
||||
|
||||
trans_size_astc = bt.basis_compute_transcoded_image_size_in_bytes(target_tf, ow, oh)
|
||||
assert trans_size_astc > 0
|
||||
astc_ofs = bt.alloc(trans_size_astc)
|
||||
print(f" ASTC buf ofs=0x{astc_ofs:x}, size={trans_size_astc}")
|
||||
|
||||
ok = bt.ktx2_transcode_image_level(
|
||||
ktx2_handle,
|
||||
level_index, layer_index, face_index,
|
||||
astc_ofs,
|
||||
trans_size_astc,
|
||||
target_tf,
|
||||
0, 0, 0, -1, -1,
|
||||
trans_state
|
||||
)
|
||||
print(" ktx2_transcode_image_level(ASTC):", ok)
|
||||
if not ok:
|
||||
bt.free(astc_ofs)
|
||||
bt.ktx2_destroy_transcode_state(trans_state)
|
||||
bt.ktx2_close(ktx2_handle)
|
||||
bt.free(ktx2_data_ofs)
|
||||
return FALSE
|
||||
|
||||
write_astc_file(astc_name, astc_ofs, block_width, block_height, ow, oh)
|
||||
bt.free(astc_ofs)
|
||||
|
||||
bt.ktx2_destroy_transcode_state(trans_state)
|
||||
bt.ktx2_close(ktx2_handle)
|
||||
bt.free(ktx2_data_ofs)
|
||||
|
||||
print("transcode_ktx2_file: success")
|
||||
return TRUE
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# main() equivalent
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
print("example_capi_python:")
|
||||
|
||||
# Init encoder (which initializes transcoder)
|
||||
print("Calling bu.init() ...")
|
||||
bu.init()
|
||||
|
||||
print("Calling bt.init() ...")
|
||||
bt.init()
|
||||
|
||||
# Optional debug control if bound
|
||||
if hasattr(bu, "enable_debug_printf"):
|
||||
print("Disabling debug printf from encoder")
|
||||
bu.enable_debug_printf(False)
|
||||
|
||||
# Generate test image
|
||||
W, H = 512, 512
|
||||
src_image = create_pretty_rgba_pattern(W, H)
|
||||
if src_image is None:
|
||||
print("ERROR: create_pretty_rgba_pattern failed")
|
||||
return 1
|
||||
|
||||
# Save test image for inspection
|
||||
print("Writing test_image.tga ...")
|
||||
# use Python-level TGA writer by allocating a temporary native buffer
|
||||
tmp_ofs = bt.alloc(len(src_image))
|
||||
ctypes.memmove(tmp_ofs, src_image, len(src_image))
|
||||
write_tga_image("test_image.tga", W, H, True, tmp_ofs)
|
||||
bt.free(tmp_ofs)
|
||||
|
||||
# Compress to KTX2
|
||||
print("Creating comp_params ...")
|
||||
comp_params = bu.new_params()
|
||||
print(" comp_params handle:", comp_params)
|
||||
|
||||
img_ofs = bu.alloc(W * H * 4)
|
||||
print(f"Allocated encoder image buffer at 0x{img_ofs:x}")
|
||||
ctypes.memmove(img_ofs, src_image, W * H * 4)
|
||||
|
||||
print("Calling bu.comp_params_set_image_rgba32(...)")
|
||||
ok = bu.set_image_rgba32(comp_params, 0, img_ofs, W, H, W * 4)
|
||||
print(" set_image_rgba32:", ok)
|
||||
if not ok:
|
||||
print("ERROR: bu_comp_params_set_image_rgba32 failed")
|
||||
return 1
|
||||
|
||||
bu.free(img_ofs)
|
||||
|
||||
print("Compressing to XUASTC LDR 8x5 KTX2 ...")
|
||||
basis_tex_format = BasisTexFormat.cXUASTC_LDR_8x5
|
||||
quality_level = 85
|
||||
effort_level = 2
|
||||
flags = (BasisFlags.KTX2_OUTPUT |
|
||||
BasisFlags.SRGB |
|
||||
BasisFlags.THREADED |
|
||||
BasisFlags.GEN_MIPS_CLAMP |
|
||||
BasisFlags.PRINT_STATS |
|
||||
BasisFlags.PRINT_STATUS)
|
||||
|
||||
ok = bu.compress(comp_params,
|
||||
tex_format=basis_tex_format,
|
||||
quality=quality_level,
|
||||
effort=effort_level,
|
||||
flags=flags,
|
||||
rdo_quality=0.0)
|
||||
print(" bu.compress:", ok)
|
||||
if not ok:
|
||||
print("ERROR: bu_compress_texture failed")
|
||||
return 1
|
||||
|
||||
comp_size = bu.get_comp_data_size(comp_params)
|
||||
print("Compressed size:", comp_size)
|
||||
if comp_size == 0:
|
||||
print("ERROR: bu_comp_params_get_comp_data_size failed")
|
||||
return 1
|
||||
|
||||
comp_ofs = bu.get_comp_data_ofs(comp_params)
|
||||
print(f"Compressed data ptr=0x{comp_ofs:x}")
|
||||
|
||||
# Copy compressed data into Python bytes
|
||||
CompArray = ctypes.c_ubyte * comp_size
|
||||
comp_buf = CompArray.from_address(comp_ofs)
|
||||
comp_bytes = bytes(comp_buf)
|
||||
|
||||
print("Writing test.ktx2 ...")
|
||||
if not write_blob_to_file("test.ktx2", comp_bytes):
|
||||
print("ERROR: write_blob_to_file failed")
|
||||
return 1
|
||||
|
||||
# Transcode using the native transcoder API
|
||||
print("Now transcoding test.ktx2 via C API ...")
|
||||
if not transcode_ktx2_file(comp_bytes):
|
||||
print("ERROR: transcode_ktx2_file failed")
|
||||
return 1
|
||||
|
||||
bu.delete_params(comp_params)
|
||||
|
||||
print("Success")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
24
python/lowlevel_test_native/test_transcoder_basic.py
Normal file
24
python/lowlevel_test_native/test_transcoder_basic.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# test_transcoder_basic.py
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Make sure Python can find the .so file
|
||||
sys.path.append("basisu_py") # Adjust if needed
|
||||
|
||||
try:
|
||||
import basisu_transcoder_python as bt
|
||||
except ImportError as e:
|
||||
print("Failed to import basisu_transcoder_python:", e)
|
||||
raise
|
||||
|
||||
print("Successfully loaded basisu_transcoder_python")
|
||||
|
||||
# Call bt_get_version() via the pybind11 binding
|
||||
try:
|
||||
version = bt.get_version()
|
||||
print("Transcoder version:", version)
|
||||
except Exception as e:
|
||||
print("Error calling bt_get_version:", e)
|
||||
raise
|
||||
|
||||
print("Basic transcoder test complete.")
|
||||
1
python/lowlevel_test_wasm/__init__.py
Normal file
1
python/lowlevel_test_wasm/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# __init__.py
|
||||
58
python/lowlevel_test_wasm/basic_test.py
Normal file
58
python/lowlevel_test_wasm/basic_test.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import wasmtime
|
||||
import ctypes
|
||||
|
||||
# --- Engine ---
|
||||
engine = wasmtime.Engine()
|
||||
|
||||
# --- Store ---
|
||||
store = wasmtime.Store(engine)
|
||||
|
||||
# --- WASI config ---
|
||||
wasi = wasmtime.WasiConfig()
|
||||
wasi.argv = ["basisu_module_st"]
|
||||
wasi.inherit_stdout() # <-- tell WASI to use the host stdout
|
||||
wasi.inherit_stderr()
|
||||
store.set_wasi(wasi)
|
||||
|
||||
# --- Load module ---
|
||||
module = wasmtime.Module.from_file(engine, "basisu_py/wasm/basisu_module_st.wasm")
|
||||
|
||||
# --- Linker + WASI ---
|
||||
linker = wasmtime.Linker(engine)
|
||||
linker.define_wasi()
|
||||
|
||||
# --- Instantiate ---
|
||||
instance = linker.instantiate(store, module)
|
||||
print("Single-threaded WASM instantiated OK")
|
||||
|
||||
# --- Exports ---
|
||||
exports = instance.exports(store)
|
||||
|
||||
get_version = exports["bu_get_version"]
|
||||
alloc = exports["bu_alloc"]
|
||||
free = exports["bu_free"]
|
||||
memory = exports["memory"]
|
||||
|
||||
# --- Version ---
|
||||
version = get_version(store)
|
||||
print("Version =", version)
|
||||
|
||||
# --- Alloc ---
|
||||
ptr = alloc(store, 64)
|
||||
print("Allocated ptr =", ptr)
|
||||
|
||||
# --- Access WASM memory properly ---
|
||||
data_len = memory.data_len(store)
|
||||
raw_ptr = memory.data_ptr(store) # ctypes pointer
|
||||
addr = ctypes.addressof(raw_ptr.contents) # convert to integer pointer
|
||||
|
||||
# Create a byte array view into WASM memory
|
||||
buf = (ctypes.c_ubyte * data_len).from_address(addr)
|
||||
|
||||
# Write TEST at allocated ptr
|
||||
buf[ptr : ptr + 4] = b"TEST"
|
||||
print("Wrote TEST into WASM memory.")
|
||||
|
||||
# --- Free ---
|
||||
free(store, ptr)
|
||||
print("Memory free OK.")
|
||||
148
python/lowlevel_test_wasm/basisu_wasm.py
Normal file
148
python/lowlevel_test_wasm/basisu_wasm.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# basisu_wasm.py
|
||||
import wasmtime
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
sys.path.append("basisu_py") # our shared .py files
|
||||
|
||||
from constants import *
|
||||
|
||||
class BasisuWasm:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.engine = None
|
||||
self.store = None
|
||||
self.memory = None
|
||||
self.exports = None
|
||||
|
||||
# -----------------------------------------------
|
||||
# Internal helper: build WASI + Wasmtime engine
|
||||
# -----------------------------------------------
|
||||
def _init_engine(self):
|
||||
self.engine = wasmtime.Engine()
|
||||
self.store = wasmtime.Store(self.engine)
|
||||
|
||||
wasi = wasmtime.WasiConfig()
|
||||
wasi.argv = ["basisu"]
|
||||
wasi.inherit_stdout()
|
||||
wasi.inherit_stderr()
|
||||
self.store.set_wasi(wasi)
|
||||
|
||||
return wasi
|
||||
|
||||
# -----------------------------------------------
|
||||
# Create linker and instantiate WASM module
|
||||
# -----------------------------------------------
|
||||
def load(self):
|
||||
self._init_engine()
|
||||
|
||||
module = wasmtime.Module.from_file(self.engine, self.path)
|
||||
linker = wasmtime.Linker(self.engine)
|
||||
linker.define_wasi()
|
||||
|
||||
instance = linker.instantiate(self.store, module)
|
||||
|
||||
self.exports = instance.exports(self.store)
|
||||
self.memory = self.exports["memory"]
|
||||
|
||||
if "bu_init" in self.exports:
|
||||
self.exports["bu_init"](self.store)
|
||||
|
||||
print("WASM loaded:", self.path)
|
||||
|
||||
# -----------------------------------------------
|
||||
# Read/write WASM linear memory via ctypes
|
||||
# -----------------------------------------------
|
||||
def _wasm_buf(self):
|
||||
raw_ptr = self.memory.data_ptr(self.store)
|
||||
length = self.memory.data_len(self.store)
|
||||
addr = ctypes.addressof(raw_ptr.contents)
|
||||
return (ctypes.c_ubyte * length).from_address(addr)
|
||||
|
||||
# -----------------------------------------------
|
||||
# Exported API accessors
|
||||
# -----------------------------------------------
|
||||
def init(self):
|
||||
return self.exports["bu_init"](self.store)
|
||||
|
||||
def version(self):
|
||||
return self.exports["bu_get_version"](self.store)
|
||||
|
||||
def alloc(self, size):
|
||||
return self.exports["bu_alloc"](self.store, size)
|
||||
|
||||
def free(self, ptr):
|
||||
return self.exports["bu_free"](self.store, ptr)
|
||||
|
||||
def new_params(self):
|
||||
return self.exports["bu_new_comp_params"](self.store)
|
||||
|
||||
def delete_params(self, ptr):
|
||||
return self.exports["bu_delete_comp_params"](self.store, ptr)
|
||||
|
||||
def set_image_rgba32(self, params, image_index, img_ptr, w, h, pitch):
|
||||
return self.exports["bu_comp_params_set_image_rgba32"](
|
||||
self.store, params, image_index, img_ptr, w, h, pitch
|
||||
)
|
||||
|
||||
def set_image_float_rgba(self, params, image_index, img_ptr, w, h, pitch):
|
||||
return self.exports["bu_comp_params_set_image_float_rgba"](
|
||||
self.store, params, image_index, img_ptr, w, h, pitch
|
||||
)
|
||||
|
||||
# Normally quality_level controls the quality.
|
||||
# If quality_level==-1, then rdo_quality (a low-level parameter) directly
|
||||
# controls each codec's quality setting. Normally set to 0.
|
||||
|
||||
def compress_texture_lowlevel(self, params,
|
||||
tex_format,
|
||||
quality_level,
|
||||
effort_level,
|
||||
flags_and_quality,
|
||||
rdo_quality):
|
||||
|
||||
return self.exports["bu_compress_texture"](
|
||||
self.store,
|
||||
params,
|
||||
tex_format,
|
||||
quality_level,
|
||||
effort_level,
|
||||
flags_and_quality,
|
||||
rdo_quality
|
||||
)
|
||||
|
||||
def compress(self, params,
|
||||
tex_format=BasisTexFormat.cUASTC_LDR_4x4,
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.NONE,
|
||||
rdo_quality=0.0):
|
||||
|
||||
return bool(self.compress_texture_lowlevel(
|
||||
params,
|
||||
tex_format,
|
||||
quality,
|
||||
effort,
|
||||
flags,
|
||||
rdo_quality
|
||||
))
|
||||
|
||||
def get_comp_data_ofs(self, params):
|
||||
return self.exports["bu_comp_params_get_comp_data_ofs"](self.store, params)
|
||||
|
||||
def get_comp_data_size(self, params):
|
||||
return self.exports["bu_comp_params_get_comp_data_size"](self.store, params)
|
||||
|
||||
# -----------------------------------------------
|
||||
# Copy bytes into WASM memory
|
||||
# -----------------------------------------------
|
||||
def write_bytes(self, wasm_ptr, data: bytes):
|
||||
buf = self._wasm_buf()
|
||||
buf[wasm_ptr:wasm_ptr+len(data)] = data
|
||||
|
||||
# -----------------------------------------------
|
||||
# Read bytes from WASM memory
|
||||
# -----------------------------------------------
|
||||
def read_bytes(self, wasm_ptr, size):
|
||||
buf = self._wasm_buf()
|
||||
return bytes(buf[wasm_ptr:wasm_ptr+size])
|
||||
63
python/lowlevel_test_wasm/compress_test.py
Normal file
63
python/lowlevel_test_wasm/compress_test.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# compress_test.py
|
||||
from .basisu_wasm import *
|
||||
|
||||
# === Load WASM ===
|
||||
codec = BasisuWasm("basisu_py/wasm/basisu_module_st.wasm")
|
||||
codec.load()
|
||||
|
||||
print("Version =", codec.version())
|
||||
|
||||
# === Build test image ===
|
||||
W, H = 256, 256
|
||||
BYTES_PER_PIXEL = 4
|
||||
pitch = W * BYTES_PER_PIXEL
|
||||
|
||||
img = bytearray(W * H * 4)
|
||||
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
i = (y * W + x) * 4
|
||||
img[i + 0] = x & 0xFF # R
|
||||
img[i + 1] = y & 0xFF # G
|
||||
img[i + 2] = (x ^ y) & 0xFF # B
|
||||
img[i + 3] = 255 # A
|
||||
|
||||
# === Upload image to WASM memory ===
|
||||
img_ptr = codec.alloc(len(img))
|
||||
codec.write_bytes(img_ptr, img)
|
||||
|
||||
# === Create comp_params ===
|
||||
params = codec.new_params()
|
||||
|
||||
# === Set image into comp_params ===
|
||||
ok = codec.set_image_rgba32(params, 0, img_ptr, W, H, pitch)
|
||||
print("Set image:", ok)
|
||||
|
||||
# === Compress ===
|
||||
ok = codec.compress(
|
||||
params,
|
||||
tex_format=BasisTexFormat.cUASTC_LDR_4x4,
|
||||
quality=100,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB,
|
||||
rdo_quality=0.0
|
||||
)
|
||||
print("Compress result:", ok)
|
||||
|
||||
# === Retrieve compressed blob ===
|
||||
ofs = codec.get_comp_data_ofs(params)
|
||||
size = codec.get_comp_data_size(params)
|
||||
print("Output size =", size)
|
||||
|
||||
comp_data = codec.read_bytes(ofs, size)
|
||||
print("First 16 bytes:", comp_data[:16])
|
||||
|
||||
# === Save to KTX2 ===
|
||||
with open("test.ktx2", "wb") as f:
|
||||
f.write(comp_data)
|
||||
|
||||
print("File written: test.ktx2")
|
||||
|
||||
# === Cleanup ===
|
||||
codec.delete_params(params)
|
||||
codec.free(img_ptr)
|
||||
76
python/lowlevel_test_wasm/compress_test_float.py
Normal file
76
python/lowlevel_test_wasm/compress_test_float.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# compress_test_float.py
|
||||
|
||||
from .basisu_wasm import BasisuWasm, BasisTexFormat, BasisEffort, BasisFlags, BasisQuality
|
||||
import struct # for packing floats
|
||||
|
||||
# === Load WASM ===
|
||||
codec = BasisuWasm("basisu_py/wasm/basisu_module_st.wasm")
|
||||
codec.load()
|
||||
|
||||
print("Version =", codec.version())
|
||||
|
||||
# === Build a 256x256 FLOAT RGBA image ===
|
||||
W, H = 256, 256
|
||||
BYTES_PER_PIXEL = 16 # float32 * 4
|
||||
pitch = W * BYTES_PER_PIXEL
|
||||
|
||||
# Float image stored as bytearray of packed floats
|
||||
img = bytearray(W * H * BYTES_PER_PIXEL)
|
||||
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
# Create some float HDR gradient pattern
|
||||
r = float(x) / W # 0.0 ? 1.0
|
||||
g = float(y) / H # 0.0 ? 1.0
|
||||
b = float(x ^ y) / 255.0 # quirky pattern
|
||||
a = 1.0
|
||||
|
||||
i = (y * W + x) * 4
|
||||
|
||||
# pack into img bytearray
|
||||
struct.pack_into("ffff", img, i*4, r, g, b, a)
|
||||
|
||||
print("Created FLOAT RGBA image.")
|
||||
|
||||
# === Upload to WASM memory ===
|
||||
img_ptr = codec.alloc(len(img))
|
||||
codec.write_bytes(img_ptr, img)
|
||||
print("Copied float image into WASM heap at", img_ptr)
|
||||
|
||||
# === Create params ===
|
||||
params = codec.new_params()
|
||||
|
||||
# === Set FLOAT RGBA image ===
|
||||
ok = codec.set_image_float_rgba(params, 0, img_ptr, W, H, pitch)
|
||||
print("Set float RGBA:", ok)
|
||||
|
||||
# === Compress using HDR UASTC 4x4 ===
|
||||
ok = codec.compress(
|
||||
params,
|
||||
tex_format=BasisTexFormat.cUASTC_HDR_4x4,
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.REC2020, # optional: HDR color space
|
||||
rdo_quality=0.0
|
||||
)
|
||||
|
||||
print("Compression result:", ok)
|
||||
|
||||
# === Retrieve compressed HDR KTX2 ===
|
||||
ofs = codec.get_comp_data_ofs(params)
|
||||
size = codec.get_comp_data_size(params)
|
||||
|
||||
print("Output size =", size)
|
||||
data = codec.read_bytes(ofs, size)
|
||||
|
||||
print("First 16 bytes:", data[:16])
|
||||
|
||||
# === Save to test_hdr.ktx2 ===
|
||||
with open("test_hdr.ktx2", "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
print("Wrote test_hdr.ktx2")
|
||||
|
||||
# === Cleanup ===
|
||||
codec.delete_params(params)
|
||||
codec.free(img_ptr)
|
||||
44
python/pyproject.toml
Normal file
44
python/pyproject.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=65", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "basisu-py"
|
||||
version = "0.2.0"
|
||||
description = "Python bindings for Basis Universal encoder/transcoder v2.x with native + WASM backend"
|
||||
authors = [
|
||||
{ name = "Binomial LLC", email = "stephanie@binomial.info" }
|
||||
]
|
||||
license = { text = "Apache 2.0" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"Pillow",
|
||||
"imageio>=2.22",
|
||||
"wasmtime",
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: C++",
|
||||
"Operating System :: OS Independent",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["basisu_py*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
basisu_py = [
|
||||
"*.so",
|
||||
"*.pyd",
|
||||
"*.py",
|
||||
"wasm/*.wasm",
|
||||
"wasm/*.py",
|
||||
"README.md",
|
||||
]
|
||||
1
python/tests/__init__.py
Normal file
1
python/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# python/tests/__init__.py
|
||||
70
python/tests/test_backend_loading.py
Normal file
70
python/tests/test_backend_loading.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from basisu_py.codec import Encoder, EncoderBackend
|
||||
from basisu_py.constants import BasisTexFormat
|
||||
|
||||
print("========== BACKEND LOADING TEST ==========\n")
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 1. Test native backend (if available)
|
||||
# --------------------------------------------------------------
|
||||
print("Testing native backend...")
|
||||
|
||||
try:
|
||||
enc_native = Encoder(backend=EncoderBackend.NATIVE)
|
||||
print(" [OK] Native backend loaded")
|
||||
except Exception as e:
|
||||
print(" [FAIL] Native backend failed to load:", e)
|
||||
enc_native = None
|
||||
|
||||
# If native loaded, test very basic functionality
|
||||
if enc_native:
|
||||
try:
|
||||
version = enc_native._native.get_version()
|
||||
print(f" Native get_version() ? {version}")
|
||||
|
||||
ptr = enc_native._native.alloc(16)
|
||||
print(f" Native alloc() returned ptr = {ptr}")
|
||||
|
||||
enc_native._native.free(ptr)
|
||||
print(f" Native free() OK")
|
||||
|
||||
print(" [OK] Native basic operations working.\n")
|
||||
except Exception as e:
|
||||
print(" [FAIL] Native operations error:", e)
|
||||
else:
|
||||
print(" Skipping native basic operations.\n")
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 2. Test WASM backend
|
||||
# --------------------------------------------------------------
|
||||
print("\nTesting WASM backend...")
|
||||
|
||||
try:
|
||||
enc_wasm = Encoder(backend=EncoderBackend.WASM)
|
||||
print(" [OK] WASM backend loaded")
|
||||
except Exception as e:
|
||||
print(" [FAIL] WASM backend failed to load:", e)
|
||||
enc_wasm = None
|
||||
|
||||
# If WASM loaded, test basic methods
|
||||
if enc_wasm and enc_wasm._wasm is not None:
|
||||
try:
|
||||
version = enc_wasm._wasm.get_version()
|
||||
print(f" WASM get_version() ? {version}")
|
||||
|
||||
ptr = enc_wasm._wasm.alloc(16)
|
||||
print(f" WASM alloc() returned ptr = {ptr}")
|
||||
|
||||
enc_wasm._wasm.free(ptr)
|
||||
print(f" WASM free() OK")
|
||||
|
||||
print(" [OK] WASM basic operations working.\n")
|
||||
except Exception as e:
|
||||
print(" [FAIL] WASM operations error:", e)
|
||||
else:
|
||||
print(" Skipping WASM basic operations.\n")
|
||||
|
||||
print("\n========== DONE ==========\n")
|
||||
7
python/tests/test_basic_backend_selection.py
Normal file
7
python/tests/test_basic_backend_selection.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from basisu_py import Encoder
|
||||
|
||||
enc = Encoder() # AUTO mode
|
||||
print("Encoder backend:", enc.backend)
|
||||
print("Native loaded:", enc._native is not None)
|
||||
print("WASM loaded:", enc._wasm is not None)
|
||||
print("Version:", enc._native.get_version() if enc._native else enc._wasm.get_version())
|
||||
19
python/tests/test_basic_decode.py
Normal file
19
python/tests/test_basic_decode.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from basisu_py import Transcoder
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
# Load input file
|
||||
with open("test.ktx2", "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
# Decode (AUTO backend)
|
||||
t = Transcoder()
|
||||
rgba = t.decode_rgba(data) # returns HxWx4 uint8 NumPy array
|
||||
|
||||
print("Decoded:", rgba.shape, rgba.dtype)
|
||||
|
||||
# Convert to Pillow Image and save
|
||||
img = Image.fromarray(rgba, mode="RGBA")
|
||||
img.save("decoded.png")
|
||||
|
||||
print("Wrote decoded.png")
|
||||
10
python/tests/test_basic_transcode.py
Normal file
10
python/tests/test_basic_transcode.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from basisu_py import Transcoder
|
||||
|
||||
with open("test.ktx2", "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
t = Transcoder() # AUTO backend
|
||||
img = t.decode_rgba(data)
|
||||
|
||||
print("Decoded shape:", img.shape)
|
||||
print("dtype:", img.dtype)
|
||||
6
python/tests/test_basic_wasm_selection.py
Normal file
6
python/tests/test_basic_wasm_selection.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from basisu_py import Transcoder
|
||||
from basisu_py.transcoder import TranscoderBackend
|
||||
|
||||
t = Transcoder(backend=TranscoderBackend.WASM)
|
||||
print("Backend:", t.backend_name)
|
||||
t.decode_rgba(open("test.ktx2","rb").read())
|
||||
82
python/tests/test_compress_swirl.py
Normal file
82
python/tests/test_compress_swirl.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from math import sin, cos, atan2, hypot
|
||||
|
||||
from basisu_py.codec import Encoder, EncoderBackend
|
||||
from basisu_py.constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Procedural swirl pattern (RGBA8)
|
||||
# --------------------------------------------------------------
|
||||
def make_swirl_image(w=256, h=256):
|
||||
arr = np.zeros((h, w, 4), dtype=np.uint8)
|
||||
|
||||
cx = w / 2.0
|
||||
cy = h / 2.0
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
dx = x - cx
|
||||
dy = y - cy
|
||||
|
||||
dist = hypot(dx, dy)
|
||||
angle = atan2(dy, dx)
|
||||
|
||||
r = int((sin(dist * 0.15) * 0.5 + 0.5) * 255)
|
||||
g = int((sin(angle * 3.0) * 0.5 + 0.5) * 255)
|
||||
b = int((cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5) * 255)
|
||||
|
||||
arr[y, x] = (r, g, b, 255)
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Test encode using a given backend
|
||||
# --------------------------------------------------------------
|
||||
def compress_swirl(backend, outfile):
|
||||
print(f"\n========== Testing {backend} backend ==========")
|
||||
|
||||
# Build procedural image
|
||||
swirl = make_swirl_image(256, 256)
|
||||
print("Generated swirl image:", swirl.shape)
|
||||
|
||||
# Create encoder
|
||||
enc = Encoder(backend=backend)
|
||||
|
||||
# Compress
|
||||
blob = enc.compress(
|
||||
swirl,
|
||||
format=BasisTexFormat.cUASTC_LDR_4x4,
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB
|
||||
)
|
||||
|
||||
print(f"Compressed blob size: {len(blob)} bytes")
|
||||
|
||||
# Save output
|
||||
with open(outfile, "wb") as f:
|
||||
f.write(blob)
|
||||
|
||||
print(f"Wrote: {outfile}")
|
||||
print("==============================================")
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Main
|
||||
# --------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
# Test native backend
|
||||
try:
|
||||
compress_swirl(EncoderBackend.NATIVE, "swirl_native.ktx2")
|
||||
except Exception as e:
|
||||
print("Native backend ERROR:", e)
|
||||
|
||||
# Test WASM backend
|
||||
try:
|
||||
compress_swirl(EncoderBackend.WASM, "swirl_wasm.ktx2")
|
||||
except Exception as e:
|
||||
print("WASM backend ERROR:", e)
|
||||
75
python/tests/test_compress_swirl_hdr.py
Normal file
75
python/tests/test_compress_swirl_hdr.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
from math import sin, cos, atan2, hypot
|
||||
from basisu_py.codec import Encoder, EncoderBackend
|
||||
from basisu_py.constants import BasisTexFormat, BasisQuality, BasisEffort, BasisFlags
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Procedural HDR swirl pattern (float32 RGBA)
|
||||
# --------------------------------------------------------------
|
||||
def make_hdr_swirl_image(w=256, h=256):
|
||||
arr = np.zeros((h, w, 4), dtype=np.float32)
|
||||
|
||||
cx = w / 2.0
|
||||
cy = h / 2.0
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
dx = x - cx
|
||||
dy = y - cy
|
||||
dist = hypot(dx, dy)
|
||||
angle = atan2(dy, dx)
|
||||
|
||||
r = (sin(dist * 0.15) * 0.5 + 0.5)
|
||||
g = (sin(angle * 3.0) * 0.5 + 0.5)
|
||||
b = (cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5)
|
||||
|
||||
arr[y, x] = (r, g, b, 1.0) # full alpha
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Test encode using a given backend
|
||||
# --------------------------------------------------------------
|
||||
def compress_hdr_swirl(backend, outfile):
|
||||
print(f"\n========== Testing HDR {backend} backend ==========")
|
||||
|
||||
hdr = make_hdr_swirl_image(256, 256)
|
||||
print("Generated HDR swirl image:", hdr.shape, hdr.dtype)
|
||||
|
||||
enc = Encoder(backend=backend)
|
||||
|
||||
blob = enc.compress(
|
||||
hdr,
|
||||
format=-1, # auto-select HDR (UASTC_HDR_4x4)
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB
|
||||
)
|
||||
|
||||
print(f"Compressed blob size: {len(blob)} bytes")
|
||||
|
||||
with open(outfile, "wb") as f:
|
||||
f.write(blob)
|
||||
|
||||
print(f"Wrote: {outfile}")
|
||||
print("==============================================")
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Main
|
||||
# --------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
# Native backend
|
||||
try:
|
||||
compress_hdr_swirl(EncoderBackend.NATIVE, "hdr_swirl_native.ktx2")
|
||||
except Exception as e:
|
||||
print("Native HDR backend ERROR:", e)
|
||||
|
||||
# WASM backend
|
||||
try:
|
||||
compress_hdr_swirl(EncoderBackend.WASM, "hdr_swirl_wasm.ktx2")
|
||||
except Exception as e:
|
||||
print("WASM HDR backend ERROR:", e)
|
||||
18
python/tests/test_transcoder_astc.py
Normal file
18
python/tests/test_transcoder_astc.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from basisu_py import Transcoder
|
||||
from astc_writer import write_astc_file
|
||||
|
||||
# Load a .ktx2
|
||||
data = open("input.ktx2", "rb").read()
|
||||
t = Transcoder()
|
||||
|
||||
# Transcode to ASTC
|
||||
h = t.open(data)
|
||||
bw = t.get_block_width(h) # or basis_get_block_width(astc_tfmt)
|
||||
bh = t.get_block_height(h)
|
||||
tfmt = t.basis_get_transcoder_texture_format_from_basis_tex_format(
|
||||
t.get_basis_tex_format(h)
|
||||
)
|
||||
|
||||
blocks = t.transcode_tfmt(data, tfmt)
|
||||
write_astc_file("output.astc", blocks, bw, bh, t.get_width(h), t.get_height(h))
|
||||
t.close(h)
|
||||
72
python/tests/test_transcoder_backend_loading.py
Normal file
72
python/tests/test_transcoder_backend_loading.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from basisu_py.transcoder import Transcoder, TranscoderBackend
|
||||
from basisu_py.constants import BasisTexFormat
|
||||
|
||||
print("========== TESTING TRANSCODER BACKENDS ==========\n")
|
||||
|
||||
# Load some test data (ensure test.ktx2 exists)
|
||||
try:
|
||||
test_data = open("test.ktx2", "rb").read()
|
||||
print("[INFO] Loaded test.ktx2")
|
||||
except FileNotFoundError:
|
||||
print("[ERROR] test.ktx2 not found. Create one first via encoder tests.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 1. Test NATIVE backend
|
||||
# -------------------------------------------------------------------
|
||||
print("\n--- Testing NATIVE transcoder backend ---")
|
||||
|
||||
try:
|
||||
t_native = Transcoder(TranscoderBackend.NATIVE)
|
||||
print(" [OK] Native backend loaded")
|
||||
|
||||
version = t_native.get_version()
|
||||
print(f" Native get_version() = {version}")
|
||||
|
||||
# Open KTX2
|
||||
raw = t_native.open(test_data)
|
||||
print(" [OK] Opened KTX2 (native)")
|
||||
|
||||
# Query some basic properties
|
||||
print(" Width :", t_native.get_width(raw))
|
||||
print(" Height:", t_native.get_height(raw))
|
||||
print(" Levels:", t_native.get_levels(raw))
|
||||
|
||||
# Cleanup
|
||||
t_native.close(raw)
|
||||
print(" [OK] Native transcoder basic operations working.")
|
||||
|
||||
except Exception as e:
|
||||
print(" [FAIL] Native transcoder error:", e)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 2. Test WASM backend
|
||||
# -------------------------------------------------------------------
|
||||
print("\n--- Testing WASM transcoder backend ---")
|
||||
|
||||
try:
|
||||
t_wasm = Transcoder(TranscoderBackend.WASM)
|
||||
print(" [OK] WASM backend loaded")
|
||||
|
||||
version = t_wasm.get_version()
|
||||
print(f" WASM get_version() = {version}")
|
||||
|
||||
raw = t_wasm.open(test_data)
|
||||
print(" [OK] Opened KTX2 (wasm)")
|
||||
|
||||
print(" Width :", t_wasm.get_width(raw))
|
||||
print(" Height:", t_wasm.get_height(raw))
|
||||
print(" Levels:", t_wasm.get_levels(raw))
|
||||
|
||||
t_wasm.close(raw)
|
||||
print(" [OK] WASM transcoder basic operations working.")
|
||||
|
||||
except Exception as e:
|
||||
print(" [FAIL] WASM transcoder error:", e)
|
||||
|
||||
|
||||
print("\n========== DONE ==========")
|
||||
154
python/tests/test_transcoder_end_to_end.py
Normal file
154
python/tests/test_transcoder_end_to_end.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Full end-to-end transcoder test with automatic fallback.
|
||||
|
||||
- Generates a swirl image
|
||||
- Compresses it using native OR WASM (AUTO mode)
|
||||
- Writes test.ktx2
|
||||
- Decodes it using whichever backends are available:
|
||||
* AUTO (native if present, otherwise WASM)
|
||||
* Native (if available)
|
||||
* WASM (if available)
|
||||
- Produces PNG outputs for all successful backends
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from math import sin, cos, atan2, hypot
|
||||
from PIL import Image
|
||||
import sys
|
||||
|
||||
from basisu_py.codec import Encoder, EncoderBackend
|
||||
from basisu_py.transcoder import Transcoder, TranscoderBackend
|
||||
from basisu_py.constants import (
|
||||
BasisTexFormat,
|
||||
BasisQuality,
|
||||
BasisEffort,
|
||||
BasisFlags,
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Create an RGBA swirl test image
|
||||
# -------------------------------------------------------------------
|
||||
def make_swirl(w=256, h=256):
|
||||
arr = np.zeros((h, w, 4), dtype=np.uint8)
|
||||
|
||||
cx, cy = w / 2.0, h / 2.0
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
dx, dy = x - cx, y - cy
|
||||
dist = hypot(dx, dy)
|
||||
angle = atan2(dy, dx)
|
||||
|
||||
r = int((sin(dist * 0.15) * 0.5 + 0.5) * 255)
|
||||
g = int((sin(angle * 3.0) * 0.5 + 0.5) * 255)
|
||||
b = int((cos(dist * 0.10 + angle * 2.0) * 0.5 + 0.5) * 255)
|
||||
|
||||
arr[y, x] = (r, g, b, 255)
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Try loading transcoder with a backend, return (success, transcoder)
|
||||
# -------------------------------------------------------------------
|
||||
def try_transcoder(backend):
|
||||
try:
|
||||
t = Transcoder(backend)
|
||||
print(f"[OK] Loaded transcoder backend '{backend}' ({t.backend_name})")
|
||||
return True, t
|
||||
except Exception as e:
|
||||
print(f"[SKIP] Backend '{backend}' unavailable:", e)
|
||||
return False, None
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Try loading encoder with a backend, return blob or None
|
||||
# -------------------------------------------------------------------
|
||||
def try_encoder(backend, img):
|
||||
try:
|
||||
enc = Encoder(backend)
|
||||
print(f"[OK] Loaded encoder backend '{backend}' ({enc.backend_name})")
|
||||
except Exception as e:
|
||||
print(f"[SKIP] Encoder backend '{backend}' unavailable:", e)
|
||||
return None
|
||||
|
||||
try:
|
||||
print(f"[Test] Compressing swirl -> KTX2 using {enc.backend_name}...")
|
||||
blob = enc.compress(
|
||||
img,
|
||||
format=-1,
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT | BasisFlags.SRGB
|
||||
)
|
||||
return blob
|
||||
except Exception as e:
|
||||
print(f"[FAIL] Compression failed on backend '{backend}':", e)
|
||||
return None
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Decode blob with a given transcoder
|
||||
# -------------------------------------------------------------------
|
||||
def decode_with_backend(name, t, blob):
|
||||
try:
|
||||
rgba = t.decode_rgba(blob)
|
||||
outname = f"decoded_{name}.png"
|
||||
Image.fromarray(rgba, mode="RGBA").save(outname)
|
||||
print(f" --> {name}: decoded successfully, wrote {outname}")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] decode_rgba on backend '{name}':", e)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Main test
|
||||
# -------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
print("========== BasisU End-to-End Compression & Transcoding Test ==========")
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Generate swirl test
|
||||
# -------------------------------------------------------
|
||||
img = make_swirl(256, 256)
|
||||
print("[Test] Generated swirl:", img.shape)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Try AUTO encoder (native if available, else WASM)
|
||||
# -------------------------------------------------------
|
||||
blob = try_encoder(EncoderBackend.AUTO, img)
|
||||
if blob is None:
|
||||
print("[FAIL] Could not encode using AUTO backend; aborting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Save test.ktx2
|
||||
with open("test.ktx2", "wb") as f:
|
||||
f.write(blob)
|
||||
print("[Test] Wrote: test.ktx2")
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Test transcoding using AUTO
|
||||
# -------------------------------------------------------
|
||||
print("\n[Test] Decoding via AUTO backend...")
|
||||
ok_auto, t_auto = try_transcoder(TranscoderBackend.AUTO)
|
||||
if ok_auto:
|
||||
decode_with_backend("auto", t_auto, blob)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Test NATIVE explicitly (if available)
|
||||
# -------------------------------------------------------
|
||||
print("\n[Test] Decoding via NATIVE backend...")
|
||||
ok_native, t_native = try_transcoder(TranscoderBackend.NATIVE)
|
||||
if ok_native:
|
||||
decode_with_backend("native", t_native, blob)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Test WASM explicitly (if available)
|
||||
# -------------------------------------------------------
|
||||
print("\n[Test] Decoding via WASM backend...")
|
||||
ok_wasm, t_wasm = try_transcoder(TranscoderBackend.WASM)
|
||||
if ok_wasm:
|
||||
decode_with_backend("wasm", t_wasm, blob)
|
||||
|
||||
print("\n========== DONE ==========")
|
||||
175
python/tests/test_transcoder_end_to_end_hdr.py
Normal file
175
python/tests/test_transcoder_end_to_end_hdr.py
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HDR End-to-End Compression & Transcoding Test
|
||||
Works on all platforms:
|
||||
- native if available
|
||||
- WASM fallback otherwise
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from math import sin, cos, atan2, hypot
|
||||
from PIL import Image
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import imageio.v3 as iio
|
||||
|
||||
from basisu_py.codec import Encoder, EncoderBackend
|
||||
from basisu_py.transcoder import Transcoder, TranscoderBackend
|
||||
from basisu_py.constants import (
|
||||
BasisTexFormat,
|
||||
BasisQuality,
|
||||
BasisEffort,
|
||||
BasisFlags
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Save EXR using TIFF temp + oiiotool (as required)
|
||||
# -------------------------------------------------------------------
|
||||
def save_exr(path, rgba32f):
|
||||
"""
|
||||
Save float32 RGBA as EXR if possible.
|
||||
If oiiotool is not available, save TIFF instead (Windows-safe).
|
||||
"""
|
||||
import numpy as np
|
||||
import imageio.v3 as iio
|
||||
import subprocess, tempfile, os
|
||||
|
||||
# Write temp TIFF
|
||||
with tempfile.NamedTemporaryFile(suffix=".tiff", delete=False) as tmp:
|
||||
temp_path = tmp.name
|
||||
|
||||
iio.imwrite(temp_path, rgba32f.astype(np.float32))
|
||||
|
||||
# Try EXR via oiiotool
|
||||
try:
|
||||
subprocess.run(["oiiotool", temp_path, "-o", path], check=True)
|
||||
os.remove(temp_path)
|
||||
print(" Wrote EXR:", path)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
# --- FALLBACK: save TIFF ---
|
||||
fallback_path = path + ".tiff"
|
||||
|
||||
# Windows cannot overwrite files via rename(), so remove first
|
||||
if os.path.exists(fallback_path):
|
||||
os.remove(fallback_path)
|
||||
|
||||
# os.replace() always overwrites
|
||||
os.replace(temp_path, fallback_path)
|
||||
|
||||
print(" [Fallback] Wrote TIFF instead:", fallback_path)
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Generate HDR swirl image (float32)
|
||||
# -------------------------------------------------------------------
|
||||
def make_swirl_hdr(w=256, h=256):
|
||||
arr = np.zeros((h, w, 4), dtype=np.float32)
|
||||
cx, cy = w / 2.0, h / 2.0
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
dx, dy = x - cx, y - cy
|
||||
dist = hypot(dx, dy)
|
||||
angle = atan2(dy, dx)
|
||||
|
||||
# HDR values range up to about 4.0
|
||||
r = (sin(dist * 0.08) * 0.5 + 0.5) * 4.0
|
||||
g = (sin(angle * 2.0) * 0.5 + 0.5) * 4.0
|
||||
b = (cos(dist * 0.06 + angle * 1.5) * 0.5 + 0.5) * 4.0
|
||||
|
||||
arr[y, x] = (r, g, b, 1.0)
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Try loading a transcoder backend
|
||||
# -------------------------------------------------------------------
|
||||
def try_transcoder(name, backend):
|
||||
try:
|
||||
t = Transcoder(backend)
|
||||
print(f"[OK] Loaded transcoder backend '{name}' ({t.backend_name})")
|
||||
return t
|
||||
except Exception as e:
|
||||
print(f"[SKIP] Backend '{name}' unavailable:", e)
|
||||
return None
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# MAIN
|
||||
# -------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
print("========== HDR End-to-End Compression & Transcoding Test ==========")
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Create HDR test image
|
||||
# -------------------------------------------------------
|
||||
img_hdr = make_swirl_hdr(256, 256)
|
||||
print("[HDR] swirl:", img_hdr.shape, img_hdr.dtype)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# ENCODE using AUTO backend (native ? or WASM)
|
||||
# -------------------------------------------------------
|
||||
try:
|
||||
enc = Encoder(EncoderBackend.AUTO)
|
||||
print(f"[HDR] Encoder backend = {enc.backend_name}")
|
||||
except Exception as e:
|
||||
print("[FATAL] Could not create encoder:", e)
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
print("[HDR] Compressing HDR swirl -> test_hdr.ktx2...")
|
||||
ktx2_blob = enc.compress(
|
||||
img_hdr,
|
||||
format=-1, # auto-select HDR format
|
||||
quality=BasisQuality.MAX,
|
||||
effort=BasisEffort.DEFAULT,
|
||||
flags=BasisFlags.KTX2_OUTPUT
|
||||
)
|
||||
print(" KTX2 size:", len(ktx2_blob))
|
||||
open("test_hdr.ktx2", "wb").write(ktx2_blob)
|
||||
print(" Wrote test_hdr.ktx2")
|
||||
except Exception as e:
|
||||
print("[FATAL] Encoding failed:", e)
|
||||
exit(1)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# DECODE using AUTO (native ? or WASM)
|
||||
# -------------------------------------------------------
|
||||
t_auto = try_transcoder("AUTO", TranscoderBackend.AUTO)
|
||||
if t_auto:
|
||||
try:
|
||||
hdr = t_auto.decode_rgba_hdr(ktx2_blob)
|
||||
print(" AUTO decoded:", hdr.shape, hdr.dtype)
|
||||
save_exr("decoded_auto_hdr.exr", hdr)
|
||||
except Exception as e:
|
||||
print(" [FAIL] AUTO decode failed:", e)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# DECODE using NATIVE if available
|
||||
# -------------------------------------------------------
|
||||
t_native = try_transcoder("NATIVE", TranscoderBackend.NATIVE)
|
||||
if t_native:
|
||||
try:
|
||||
hdr_n = t_native.decode_rgba_hdr(ktx2_blob)
|
||||
print(" Native decoded:", hdr_n.shape, hdr_n.dtype)
|
||||
save_exr("decoded_native_hdr.exr", hdr_n)
|
||||
except Exception as e:
|
||||
print(" [FAIL] Native decode failed:", e)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# DECODE using WASM if available
|
||||
# -------------------------------------------------------
|
||||
t_wasm = try_transcoder("WASM", TranscoderBackend.WASM)
|
||||
if t_wasm:
|
||||
try:
|
||||
hdr_w = t_wasm.decode_rgba_hdr(ktx2_blob)
|
||||
print(" WASM decoded:", hdr_w.shape, hdr_w.dtype)
|
||||
save_exr("decoded_wasm_hdr.exr", hdr_w)
|
||||
except Exception as e:
|
||||
print(" [FAIL] WASM decode failed:", e)
|
||||
|
||||
print("\n========== DONE ==========")
|
||||
190
python/tests/test_transcoder_helpers.py
Normal file
190
python/tests/test_transcoder_helpers.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import numpy as np
|
||||
|
||||
from basisu_py.transcoder import Transcoder, TranscoderBackend
|
||||
from basisu_py.constants import BasisTexFormat, TranscoderTextureFormat
|
||||
|
||||
print("========== TESTING TRANSCODER HELPERS & METADATA ==========\n")
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Load test KTX2 file
|
||||
# ----------------------------------------------------------------------------
|
||||
try:
|
||||
ktx2_bytes = open("test.ktx2", "rb").read()
|
||||
print("[INFO] Loaded test.ktx2")
|
||||
except FileNotFoundError:
|
||||
print("[ERROR] test.ktx2 not found. Run encoder tests first.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Utility: run helper tests on a given backend
|
||||
# ----------------------------------------------------------------------------
|
||||
def test_backend(name, backend):
|
||||
print(f"\n=== Testing {name} backend ===")
|
||||
|
||||
try:
|
||||
t = Transcoder(backend)
|
||||
except Exception as e:
|
||||
print(f"[FAIL] Could not initialize {name} backend:", e)
|
||||
return
|
||||
|
||||
print(f"[OK] {name} backend loaded")
|
||||
|
||||
# Version
|
||||
try:
|
||||
ver = t.get_version()
|
||||
print(f" version = {ver}")
|
||||
except Exception as e:
|
||||
print(" [FAIL] get_version() error:", e)
|
||||
return
|
||||
|
||||
# enable_debug_printf
|
||||
try:
|
||||
t.enable_debug_printf(True)
|
||||
except Exception as e:
|
||||
print(" [FAIL] enable_debug_printf() failed")
|
||||
return
|
||||
|
||||
# Open KTX2
|
||||
try:
|
||||
raw = t.open(ktx2_bytes)
|
||||
print(" [OK] open() success")
|
||||
except Exception as e:
|
||||
print(" [FAIL] open() failed:", e)
|
||||
return
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# KTX2 top-level metadata
|
||||
# ----------------------------------------------------------------------
|
||||
try:
|
||||
w = t.get_width(raw)
|
||||
h = t.get_height(raw)
|
||||
lv = t.get_levels(raw)
|
||||
fc = t.get_faces(raw)
|
||||
la = t.get_layers(raw)
|
||||
fmt = t.get_basis_tex_format(raw)
|
||||
|
||||
print(f" Width = {w}")
|
||||
print(f" Height = {h}")
|
||||
print(f" Levels = {lv}")
|
||||
print(f" Faces = {fc}")
|
||||
print(f" Layers = {la}")
|
||||
print(f" basis_tex_format = {fmt}")
|
||||
print(f" has_alpha = {t.has_alpha(raw)}")
|
||||
print(f" is_hdr = {t.is_hdr(raw)}")
|
||||
print(f" is_ldr = {t.is_ldr(raw)}")
|
||||
print(f" is_srgb = {t.is_srgb(raw)}")
|
||||
print(f" is_etc1s = {t.is_etc1s(raw)}")
|
||||
print(f" is_uastc_ldr_4x4 = {t.is_uastc_ldr_4x4(raw)}")
|
||||
print(f" is_xuastc_ldr = {t.is_xuastc_ldr(raw)}")
|
||||
print(f" is_astc_ldr = {t.is_astc_ldr(raw)}")
|
||||
print(f" block dims = {t.get_block_width(raw)} x {t.get_block_height(raw)}")
|
||||
|
||||
except Exception as e:
|
||||
print(" [FAIL] get_* metadata error:", e)
|
||||
t.close(raw)
|
||||
return
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Per-level metadata for each mipmap
|
||||
# ----------------------------------------------------------------------
|
||||
print("\n -- Level Metadata --")
|
||||
for level in range(lv):
|
||||
try:
|
||||
ow = t.get_level_orig_width(raw, level)
|
||||
oh = t.get_level_orig_height(raw, level)
|
||||
nbx = t.get_level_num_blocks_x(raw, level)
|
||||
nby = t.get_level_num_blocks_y(raw, level)
|
||||
tb = t.get_level_total_blocks(raw, level)
|
||||
af = t.get_level_alpha_flag(raw, level)
|
||||
ff = t.get_level_iframe_flag(raw, level)
|
||||
|
||||
print(f" Level {level}: orig={ow}x{oh}, blocks={nbx}x{nby}, total={tb}, alpha={af}, iframe={ff}")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] Level {level} metadata error:", e)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Test ALL basis_tex_format helpers on the file's format
|
||||
# ----------------------------------------------------------------------
|
||||
print("\n -- basis_tex_format helpers --")
|
||||
|
||||
try:
|
||||
print(f" is_xuastc_ldr = {t.basis_tex_format_is_xuastc_ldr(fmt)}")
|
||||
print(f" is_astc_ldr = {t.basis_tex_format_is_astc_ldr(fmt)}")
|
||||
print(f" block W/H = {t.basis_tex_format_get_block_width(fmt)} x "
|
||||
f"{t.basis_tex_format_get_block_height(fmt)}")
|
||||
print(f" is_hdr = {t.basis_tex_format_is_hdr(fmt)}")
|
||||
print(f" is_ldr = {t.basis_tex_format_is_ldr(fmt)}")
|
||||
except Exception as e:
|
||||
print(" [FAIL] basis_tex_format_* error:", e)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Test transcoder_texture_format helpers using a few common formats
|
||||
# ----------------------------------------------------------------------
|
||||
print("\n -- transcoder_texture_format helpers --")
|
||||
|
||||
test_formats = [
|
||||
TranscoderTextureFormat.TF_RGBA32,
|
||||
TranscoderTextureFormat.TF_RGBA_HALF,
|
||||
TranscoderTextureFormat.TF_BC7_RGBA,
|
||||
TranscoderTextureFormat.TF_ETC1_RGB,
|
||||
]
|
||||
|
||||
for tfmt in test_formats:
|
||||
try:
|
||||
print(f" Format {tfmt}: hdr={t.basis_transcoder_format_is_hdr(tfmt)}, "
|
||||
f"ldr={t.basis_transcoder_format_is_ldr(tfmt)}, "
|
||||
f"has_alpha={t.basis_transcoder_format_has_alpha(tfmt)}, "
|
||||
f"uncompressed={t.basis_transcoder_format_is_uncompressed(tfmt)}, "
|
||||
f"bytes/pixel or block={t.basis_get_bytes_per_block_or_pixel(tfmt)}")
|
||||
except Exception as e:
|
||||
print(" [FAIL] transcoder_texture_format_* error:", e)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Compute transcode buffer sizes
|
||||
# ----------------------------------------------------------------------
|
||||
print("\n -- compute_transcoded_image_size_in_bytes --")
|
||||
try:
|
||||
for tfmt in test_formats:
|
||||
sz = t.basis_compute_transcoded_image_size_in_bytes(tfmt, w, h)
|
||||
print(f" Format {tfmt}: size = {sz}")
|
||||
except Exception as e:
|
||||
print(" [FAIL] size computation error:", e)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Decode RGBA (LDR)
|
||||
# ----------------------------------------------------------------------
|
||||
print("\n -- decode_rgba --")
|
||||
try:
|
||||
img_rgba = t.decode_rgba(ktx2_bytes)
|
||||
print(f" decode_rgba: shape={img_rgba.shape}, dtype={img_rgba.dtype}")
|
||||
except Exception as e:
|
||||
print(" [FAIL] decode_rgba error:", e)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Decode HDR if applicable
|
||||
# ----------------------------------------------------------------------
|
||||
if t.is_hdr(raw):
|
||||
print("\n -- decode_rgba_hdr --")
|
||||
try:
|
||||
img_hdr = t.decode_rgba_hdr(ktx2_bytes)
|
||||
print(f" decode_rgba_hdr: shape={img_hdr.shape}, dtype={img_hdr.dtype}")
|
||||
except Exception as e:
|
||||
print(" [FAIL] decode_rgba_hdr error:", e)
|
||||
else:
|
||||
print(" Texture is LDR; skipping decode_rgba_hdr().")
|
||||
|
||||
# Cleanup
|
||||
t.close(raw)
|
||||
print(f"\n=== {name} backend OK ===\n")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Run tests for both backends
|
||||
# ----------------------------------------------------------------------------
|
||||
test_backend("NATIVE", TranscoderBackend.NATIVE)
|
||||
test_backend("WASM", TranscoderBackend.WASM)
|
||||
|
||||
print("\n========== DONE ==========\n")
|
||||
Reference in New Issue
Block a user