diff --git a/cpp/BasisFile.cpp b/cpp/BasisFile.cpp new file mode 100644 index 0000000..58c4ed9 --- /dev/null +++ b/cpp/BasisFile.cpp @@ -0,0 +1,208 @@ +#include "BasisFile.h" +#include +#include +#include + +#define BASIS_MAGIC 0xDEADBEE1 + +using namespace basist; +using namespace basisu; + +namespace facebook::react { + +BasisFile::BasisFile(jsi::Runtime &rt, const jsi::ArrayBuffer& buffer) : m_file([&]() { + size_t byteLength = buffer.size(rt); + return basisu::vector(byteLength); +}()), m_magic(0) { + size_t byteLength = buffer.size(rt); + m_file.resize(byteLength); + std::memcpy(m_file.data(), buffer.data(rt), byteLength); + + if (!m_transcoder.validate_header(m_file.data(), m_file.size())) { + throw std::runtime_error("Invalid Basis file header"); + } + + // Initialized after validation + m_magic = BASIS_MAGIC; +} + +uint32_t BasisFile::getHasAlpha() { + if (m_magic != BASIS_MAGIC) return 0; + + basist::basisu_image_level_info li; + if (!m_transcoder.get_image_level_info(m_file.data(), m_file.size(), li, 0, 0)) + return 0; + + return li.m_alpha_flag; +} + +uint32_t BasisFile::getNumImages() { + if (m_magic != BASIS_MAGIC) return 0; + return m_transcoder.get_total_images(m_file.data(), m_file.size()); +} + +uint32_t BasisFile::getNumLevels(uint32_t image_index) +{ + assert(m_magic == BASIS_MAGIC); + if (m_magic != BASIS_MAGIC) + return 0; + + basisu_image_info ii; + if (!m_transcoder.get_image_info(m_file.data(), m_file.size(), ii, image_index)) + return 0; + + return ii.m_total_levels; +} + +uint32_t BasisFile::getImageTranscodedSizeInBytes(uint32_t image_index, uint32_t level_index, uint32_t format) { + if (m_magic != BASIS_MAGIC) return 0; + if (format >= static_cast(basist::transcoder_texture_format::cTFTotalTextureFormats)) return 0; + + uint32_t orig_width, orig_height, total_blocks; + if (!m_transcoder.get_image_level_desc(m_file.data(), m_file.size(), image_index, level_index, orig_width, orig_height, total_blocks)) + return 0; + + const basist::transcoder_texture_format transcoder_format = static_cast(format); + + if (basist::basis_transcoder_format_is_uncompressed(transcoder_format)) { + const uint32_t bytes_per_pixel = basist::basis_get_uncompressed_bytes_per_pixel(transcoder_format); + return orig_width * orig_height * bytes_per_pixel; + } else { + const uint32_t bytes_per_block = basist::basis_get_bytes_per_block_or_pixel(transcoder_format); + + if (transcoder_format == basist::transcoder_texture_format::cTFPVRTC1_4_RGB || + transcoder_format == basist::transcoder_texture_format::cTFPVRTC1_4_RGBA) { + const uint32_t width = (orig_width + 3) & ~3; + const uint32_t height = (orig_height + 3) & ~3; + return (std::max(8U, width) * std::max(8U, height) * 4 + 7) / 8; + } + + return total_blocks * bytes_per_block; + } +} + +bool BasisFile::isUASTC() { + if (m_magic != BASIS_MAGIC) return false; + return m_transcoder.get_tex_format(m_file.data(), m_file.size()) == basist::basis_tex_format::cUASTC4x4; +} + +bool BasisFile::isHDR() { + if (m_magic != BASIS_MAGIC) return false; + return m_transcoder.get_tex_format(m_file.data(), m_file.size()) == basist::basis_tex_format::cUASTC_HDR_4x4; +} + +uint32_t BasisFile::startTranscoding() { + if (m_magic != BASIS_MAGIC) return 0; + return m_transcoder.start_transcoding(m_file.data(), m_file.size()); +} + +uint32_t BasisFile::transcodeImage(jsi::Runtime& rt, + jsi::Object& destination, + uint32_t image_index, + uint32_t level_index, + uint32_t format, + uint32_t unused, + uint32_t get_alpha_for_opaque_formats) { + + assert(m_magic == BASIS_MAGIC); + if (m_magic != BASIS_MAGIC) + return 0; + + if (format >= (int)transcoder_texture_format::cTFTotalTextureFormats) + return 0; + + const transcoder_texture_format transcoder_format = static_cast(format); + + uint32_t orig_width, orig_height, total_blocks; + if (!m_transcoder.get_image_level_desc(m_file.data(), m_file.size(), image_index, level_index, orig_width, orig_height, total_blocks)) + return 0; + + basisu::vector dst_data; + + uint32_t flags = get_alpha_for_opaque_formats ? cDecodeFlagsTranscodeAlphaDataToOpaqueFormats : 0; + + uint32_t status; + + if (basis_transcoder_format_is_uncompressed(transcoder_format)) + { + const uint32_t bytes_per_pixel = basis_get_uncompressed_bytes_per_pixel(transcoder_format); + const uint32_t bytes_per_line = orig_width * bytes_per_pixel; + const uint32_t bytes_per_slice = bytes_per_line * orig_height; + + dst_data.resize(bytes_per_slice); + + status = m_transcoder.transcode_image_level( + m_file.data(), m_file.size(), image_index, level_index, + dst_data.data(), orig_width * orig_height, + transcoder_format, + flags, + orig_width, + nullptr, + orig_height); + } + else + { + uint32_t bytes_per_block = basis_get_bytes_per_block_or_pixel(transcoder_format); + + uint32_t required_size = total_blocks * bytes_per_block; + + if (transcoder_format == transcoder_texture_format::cTFPVRTC1_4_RGB || transcoder_format == transcoder_texture_format::cTFPVRTC1_4_RGBA) + { + // For PVRTC1, Basis only writes (or requires) total_blocks * bytes_per_block. But GL requires extra padding for very small textures: + // https://www.khronos.org/registry/OpenGL/extensions/IMG/IMG_texture_compression_pvrtc.txt + // The transcoder will clear the extra bytes followed the used blocks to 0. + const uint32_t width = (orig_width + 3) & ~3; + const uint32_t height = (orig_height + 3) & ~3; + required_size = (std::max(8U, width) * std::max(8U, height) * 4 + 7) / 8; + assert(required_size >= total_blocks * bytes_per_block); + } + + dst_data.resize(required_size); + + status = m_transcoder.transcode_image_level( + m_file.data(), m_file.size(), image_index, level_index, + dst_data.data(), dst_data.size() / bytes_per_block, + static_cast(format), + flags); + } + + auto arrayBuffer = destination.getArrayBuffer(rt); + + auto outputBuffer = jsi::ArrayBuffer(std::move(arrayBuffer)); + memcpy(outputBuffer.data(rt), dst_data.data(), dst_data.size()); + destination.setProperty(rt, jsi::PropNameID::forAscii(rt, "buffer"), outputBuffer); + + return status; +} + +uint32_t BasisFile::getImageHeight(uint32_t image_index, uint32_t level_index) { + if (m_magic != BASIS_MAGIC) return 0; + + uint32_t orig_width, orig_height, total_blocks; + if (!m_transcoder.get_image_level_desc(m_file.data(), m_file.size(), image_index, level_index, orig_width, orig_height, total_blocks)) + return 0; + + return orig_height; +} + +uint32_t BasisFile::getImageWidth(uint32_t image_index, uint32_t level_index) { + assert(m_magic == BASIS_MAGIC); + if (m_magic != BASIS_MAGIC) + return 0; + + uint32_t orig_width, orig_height, total_blocks; + if (!m_transcoder.get_image_level_desc(m_file.data(), m_file.size(), image_index, level_index, orig_width, orig_height, total_blocks)) + return 0; + + return orig_width; +} + +void BasisFile::close() { + assert(m_magic == BASIS_MAGIC); + if (m_magic != BASIS_MAGIC) + return; + + m_file.clear(); +} + +} // namespace facebook::react diff --git a/cpp/BasisFile.h b/cpp/BasisFile.h new file mode 100644 index 0000000..eb154e1 --- /dev/null +++ b/cpp/BasisFile.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "rn_basis_universal/transcoder/basisu_transcoder.h" +#include +#include + +using namespace basist; +using namespace basisu; + +namespace facebook::react { + +class BasisFile : public jsi::NativeState { +public: + BasisFile(jsi::Runtime &rt, const jsi::ArrayBuffer& buffer); + + void close(); + uint32_t getHasAlpha(); + uint32_t getNumImages(); + uint32_t getNumLevels(uint32_t image_index); + uint32_t getImageWidth(uint32_t image_index, uint32_t level_index); + uint32_t getImageHeight(uint32_t image_index, uint32_t level_index); + uint32_t getImageTranscodedSizeInBytes(uint32_t image_index, uint32_t level_index, uint32_t format); + bool isUASTC(); + bool isHDR(); + uint32_t startTranscoding(); + uint32_t transcodeImage(jsi::Runtime& rt, + jsi::Object& destination, + uint32_t image_index, + uint32_t level_index, + uint32_t format, + uint32_t unused, + uint32_t get_alpha_for_opaque_formats); + +private: + int m_magic; + basisu_transcoder m_transcoder; + basisu::vector m_file; +}; + +} diff --git a/cpp/react-native-basis-universal.cpp b/cpp/react-native-basis-universal.cpp index 3ffe1a5..d1b979a 100644 --- a/cpp/react-native-basis-universal.cpp +++ b/cpp/react-native-basis-universal.cpp @@ -1,6 +1,6 @@ #include "react-native-basis-universal.h" #include "KTX2File.h" -#include +#include "BasisFile.h" #define DEFINE_BASIS_ENCODER_PARAMS_SETTER(func_name, param_name, param_type) \ void ReactNativeBasisUniversal::func_name(jsi::Runtime &rt, jsi::Object handle, param_type flag) { \ @@ -49,6 +49,15 @@ std::shared_ptr tryGetKTX2Handle(jsi::Runtime& rt, jsi::Object& kt2xHa return ktx2file; } +std::shared_ptr tryGetBasisFileHandle(jsi::Runtime& rt, jsi::Object& basisFileHandle) { + if (!basisFileHandle.hasNativeState(rt)) { + return nullptr; + } + + auto basisFile = std::dynamic_pointer_cast(basisFileHandle.getNativeState(rt)); + return basisFile; +} + ReactNativeBasisUniversal::ReactNativeBasisUniversal(std::shared_ptr jsInvoker) : NativeBasisUniversalCxxSpecJSI(jsInvoker) {} @@ -396,7 +405,6 @@ int ReactNativeBasisUniversal::getImageTranscodedSizeInBytes(jsi::Runtime &rt, j return ktx2Handle->getImageTranscodedSizeInBytes(levelIndex, layerIndex, faceIndex, format); } -// TODO: Used in IREngine int ReactNativeBasisUniversal::transcodeImage(jsi::Runtime &rt, jsi::Object handle, jsi::Object dst, int levelIndex, int layerIndex, int faceIndex, int format, int getAlphaForOpaqueFormats, int channel0, int channel1) { auto ktx2Handle = tryGetKTX2Handle(rt, handle); return ktx2Handle->transcodeImage(rt, @@ -421,4 +429,84 @@ int ReactNativeBasisUniversal::getKeyValue(jsi::Runtime &rt, jsi::Object handle, return 0; } +// Basis File + +jsi::Object ReactNativeBasisUniversal::createBasisFile(jsi::Runtime &rt, jsi::Object data) { + jsi::Object basisObject{rt}; + basisObject.setNativeState(rt, std::make_shared(rt, data.getArrayBuffer(rt))); + return basisObject; +} + +void ReactNativeBasisUniversal::closeBasisFile(jsi::Runtime &rt, jsi::Object handle) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + fileHandle->close(); +} + +bool ReactNativeBasisUniversal::getHasAlphaBasisFile(jsi::Runtime &rt, jsi::Object handle) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->getHasAlpha(); +} + +bool ReactNativeBasisUniversal::isUASTCBasisFile(jsi::Runtime &rt, jsi::Object handle) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->isUASTC(); +} + +bool ReactNativeBasisUniversal::isHDRBasisFile(jsi::Runtime &rt, jsi::Object handle) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->isHDR(); +} + +int ReactNativeBasisUniversal::getNumImagesBasisFile(jsi::Runtime &rt, jsi::Object handle) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->getNumImages(); +} + +int ReactNativeBasisUniversal::getNumLevels(jsi::Runtime &rt, jsi::Object handle, int imageIndex) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->getNumLevels(imageIndex); +} + +int ReactNativeBasisUniversal::getImageWidthBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->getImageWidth(imageIndex, levelIndex); +} + +int ReactNativeBasisUniversal::getImageHeightBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->getImageHeight(imageIndex, levelIndex); +} + +int ReactNativeBasisUniversal::getImageTranscodedSizeInBytesBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex, int format) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->getImageTranscodedSizeInBytes(imageIndex, levelIndex, format); +} + +bool ReactNativeBasisUniversal::startTranscodingBasisFile(jsi::Runtime &rt, jsi::Object handle) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->startTranscoding(); +} + +bool ReactNativeBasisUniversal::transcodeImageBasisFile(jsi::Runtime &rt, jsi::Object handle, jsi::Object dst, int imageIndex, int levelIndex, int format, int unused, int getAlphaForOpaqueFormats) { + auto fileHandle = tryGetBasisFileHandle(rt, handle); + return fileHandle->transcodeImage(rt, dst, imageIndex, levelIndex, format, unused, getAlphaForOpaqueFormats); +} + +jsi::Object ReactNativeBasisUniversal::getFileDescBasisFile(jsi::Runtime &rt, jsi::Object handle) { + // TODO: Implement getFileDescBasisFile (Not used in IREngine) + return jsi::Object(rt); +} + +jsi::Object ReactNativeBasisUniversal::getImageDescBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex) { + // TODO: Implement getImageDescBasisFile (Not used in IREngine) + return jsi::Object(rt); +} + +jsi::Object ReactNativeBasisUniversal::getImageLevelDescBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex) { + // TODO: Implement getImageLevelDescBasisFile (Not used in IREngine) + return jsi::Object(rt); +} + + + } diff --git a/cpp/react-native-basis-universal.h b/cpp/react-native-basis-universal.h index ed8769c..1afa829 100644 --- a/cpp/react-native-basis-universal.h +++ b/cpp/react-native-basis-universal.h @@ -131,7 +131,22 @@ class ReactNativeBasisUniversal : public NativeBasisUniversalCxxSpecJSI { bool startTranscoding(jsi::Runtime &rt, jsi::Object handle) override; int transcodeImage(jsi::Runtime &rt, jsi::Object handle, jsi::Object dst, int levelIndex, int layerIndex, int faceIndex, int format, int getAlphaForOpaqueFormats, int channel0, int channel1) override; - + // Basis file + virtual jsi::Object createBasisFile(jsi::Runtime &rt, jsi::Object data) override; + void closeBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + bool getHasAlphaBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + bool isUASTCBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + bool isHDRBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + int getNumImagesBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + int getNumLevels(jsi::Runtime &rt, jsi::Object handle, int imageIndex) override; + int getImageWidthBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex) override; + int getImageHeightBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex) override; + int getImageTranscodedSizeInBytesBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex, int format) override; + bool startTranscodingBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + bool transcodeImageBasisFile(jsi::Runtime &rt, jsi::Object handle, jsi::Object dst, int imageIndex, int levelIndex, int format, int unused, int getAlphaForOpaqueFormats) override; + jsi::Object getFileDescBasisFile(jsi::Runtime &rt, jsi::Object handle) override; + jsi::Object getImageDescBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex) override; + jsi::Object getImageLevelDescBasisFile(jsi::Runtime &rt, jsi::Object handle, int imageIndex, int levelIndex) override; private: bool basis_initialized_flag; diff --git a/example/src/App.tsx b/example/src/App.tsx index 09fe10c..ebb258f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -14,6 +14,7 @@ import { initializeBasis, BasisEncoder, KTX2File, + BasisFile, } from '@callstack/react-native-basis-universal'; import RNFetchBlob from 'react-native-blob-util'; @@ -127,8 +128,14 @@ function dumpKTX2FileDesc(ktx2File: KTX2File) { console.log('------'); } -const BlobImage = ({ arrayBuffer }: { arrayBuffer?: Uint8Array | null }) => { - if (!arrayBuffer) { +const BlobImage = ({ + arrayBuffer, + isTranscoding, +}: { + arrayBuffer?: Uint8Array | null; + isTranscoding: boolean; +}) => { + if (!arrayBuffer || isTranscoding) { return null; } @@ -173,6 +180,65 @@ const BasisEncoderPlayground = () => { })); }; + const transcode = async () => { + if (!image) { + Alert.alert('No image to transcode'); + return; + } + initializeBasis(); + + const t0 = performance.now(); + const basisFile = new BasisFile(new Uint8Array(image)); + + const width = basisFile.getImageWidth(0, 0); + const height = basisFile.getImageHeight(0, 0); + const images = basisFile.getNumImages(); + const levels = basisFile.getNumLevels(0); + const has_alpha = basisFile.getHasAlpha(); + const is_hdr = basisFile.isHDR(); + console.log({ + width, + height, + images, + levels, + has_alpha, + is_hdr, + }); + + if (!basisFile.startTranscoding()) { + console.log('startTranscoding failed'); + console.warn('startTranscoding failed'); + basisFile.close(); + basisFile.delete(); + return; + } + + const format = 22; // cTFBC6H + const dstSize = basisFile.getImageTranscodedSizeInBytes(0, 0, format); + const dst = new Uint8Array(dstSize); + + console.log('Dst output', dst.slice(0, 100)); + console.log('Dst size: ', dstSize); + + if (!basisFile.transcodeImage(dst, 0, 0, format, 0, 0)) { + console.log('basisFile.transcodeImage failed'); + console.warn('transcodeImage failed'); + basisFile.close(); + basisFile.delete(); + + return; + } + + const t1 = performance.now(); + console.log('Transcode took', (t1 - t0) / 1000, 'seconds'); + + console.log('Dst output after', dst.slice(0, 100)); + console.log('Dst size: ', dstSize); + + basisFile.close(); + basisFile.delete(); + }; + const encode = async () => { try { if (!image) { @@ -301,6 +367,8 @@ const BasisEncoderPlayground = () => { } }; + const isTranscoding = file.endsWith('.basis'); + return ( { + {Object.entries(options).map(([key, value]) => ( @@ -321,9 +390,12 @@ const BasisEncoderPlayground = () => { ))} -