From 86894cf58318e58481318b0cbf3df824c9850919 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Thu, 29 May 2025 22:26:42 +0800 Subject: [PATCH] add alpha/requireAlpha argument when decoding images remove size checks from setImage (we are assuming the caller knows the correct size) This also adds some monkey-patch methods to js_interop to convert Float32List to a UInt8List (but use this with care, because it will only work for emscripten-allocated objects) --- thermion_dart/lib/src/bindings/src/ffi.dart | 7 +++ .../lib/src/bindings/src/js_interop.dart | 57 ++++++++++++++++--- .../src/bindings/src/thermion_dart_ffi.g.dart | 5 +- .../src/thermion_dart_js_interop.g.dart | 9 ++- .../src/implementation/ffi_filament_app.dart | 25 +++++--- .../src/implementation/ffi_texture.dart | 13 ++--- .../filament/src/interface/filament_app.dart | 2 +- .../src/filament/src/interface/texture.dart | 12 +++- thermion_dart/native/include/c_api/TTexture.h | 2 +- .../c_api/ThermionDartRenderThreadApi.h | 2 +- thermion_dart/native/src/c_api/TTexture.cpp | 38 +++++-------- .../src/c_api/ThermionDartRenderThreadApi.cpp | 4 +- 12 files changed, 115 insertions(+), 61 deletions(-) diff --git a/thermion_dart/lib/src/bindings/src/ffi.dart b/thermion_dart/lib/src/bindings/src/ffi.dart index c0af1e25..178ea93d 100644 --- a/thermion_dart/lib/src/bindings/src/ffi.dart +++ b/thermion_dart/lib/src/bindings/src/ffi.dart @@ -215,3 +215,10 @@ extension DartBigIntExtension on int { return this; } } + +extension Float32ListExtension on Float32List { + + Uint8List asUint8List() { + return this.buffer.asUint8List(this.offsetInBytes); + } +} \ No newline at end of file diff --git a/thermion_dart/lib/src/bindings/src/js_interop.dart b/thermion_dart/lib/src/bindings/src/js_interop.dart index 1da5a373..8e06eac9 100644 --- a/thermion_dart/lib/src/bindings/src/js_interop.dart +++ b/thermion_dart/lib/src/bindings/src/js_interop.dart @@ -10,10 +10,22 @@ const FILAMENT_SINGLE_THREADED = true; const FILAMENT_WASM = true; const IS_WINDOWS = false; +final _allocations = {}; + Int32List makeInt32List(int length) { - var ptr = malloc(length * 4); + var ptr = stackAlloc(length * 4); var buf = _NativeLibrary.instance._emscripten_make_int32_buffer(ptr, length); - return buf.toDart; + var int32List = buf.toDart; + _allocations.add(int32List); + return int32List; +} + +Float32List makeFloat32List(int length) { + var ptr = stackAlloc(length * 4); + var buf = _NativeLibrary.instance._emscripten_make_f32_buffer(ptr, length); + var f32List = buf.toDart; + _allocations.add(f32List); + return f32List; } extension type _NativeLibrary(JSObject _) implements JSObject { @@ -34,6 +46,8 @@ extension type _NativeLibrary(JSObject _) implements JSObject { Pointer ptr, int length); external Pointer _emscripten_get_byte_offset(JSObject obj); + external int _emscripten_stack_get_base(); + external Pointer _emscripten_stack_get_current(); external int _emscripten_stack_get_free(); external void _execute_queue(); @@ -47,8 +61,8 @@ extension type _NativeLibrary(JSObject _) implements JSObject { extension FreeTypedData on TypedData { void free() { - final ptr = Pointer(this.offsetInBytes); - ptr.free(); + Pointer(this.offsetInBytes).free(); + _allocations.remove(this); } } @@ -64,7 +78,12 @@ Pointer getPointer(TypedData data, JSObject obj) { return ptr; } -extension JSBackingBuffer on JSUint8Array { +extension JSUint8BackingBuffer on JSUint8Array { + @JS('buffer') + external JSObject buffer; +} + +extension JSFloat32BackingBuffer on JSFloat32Array { @JS('buffer') external JSObject buffer; } @@ -117,7 +136,11 @@ extension Uint8ListExtension on Uint8List { final bar = Uint8ArrayWrapper(NativeLibrary.instance.HEAPU8.buffer, ptr, length) as JSUint8Array; + var now = DateTime.now(); bar.toDart.setRange(0, length, this); + var finished = DateTime.now(); + print( + "uint8list copy finished in ${finished.millisecondsSinceEpoch - now.millisecondsSinceEpoch}ms"); return ptr; } } @@ -131,6 +154,11 @@ extension Float32ListExtension on Float32List { bar.toDart.setRange(0, length, this); return ptr; } + + Uint8List asUint8List() { + var ptr = Pointer(_NativeLibrary.instance._emscripten_get_byte_offset(this.toJS)); + return ptr.asTypedList(length * 4); + } } extension Int16ListExtension on Int16List { @@ -176,6 +204,10 @@ extension Int32ListExtension on Int32List { if (this.lengthInBytes == 0) { return nullptr; } + if (_allocations.contains(this)) { + return Pointer( + _NativeLibrary.instance._emscripten_get_byte_offset(this.toJS)); + } try { this.buffer.asUint8List(this.offsetInBytes); final ptr = getPointer(this, this.toJS); @@ -214,12 +246,21 @@ extension Float64ListExtension on Float64List { } } +extension AsUint8List on Pointer { + Uint8List asTypedList(int length) { + final start = addr; + final wrapper = + Uint8ArrayWrapper(NativeLibrary.instance.HEAPU8.buffer, start, length) + as JSUint8Array; + return wrapper.toDart; + } +} + extension AsFloat32List on Pointer { Float32List asTypedList(int length) { final start = addr; - final wrapper = - Float32ArrayWrapper(NativeLibrary.instance.HEAPU8.buffer, start, length) - as JSFloat32Array; + final wrapper = Float32ArrayWrapper( + NativeLibrary.instance.HEAPF32.buffer, start, length) as JSFloat32Array; return wrapper.toDart; } } diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index 44f0d235..b37c00cf 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -738,11 +738,12 @@ external ffi.Pointer Image_createEmpty( @ffi.Native< ffi.Pointer Function( - ffi.Pointer, ffi.Size, ffi.Pointer)>(isLeaf: true) + ffi.Pointer, ffi.Size, ffi.Pointer, ffi.Bool alpha)>(isLeaf: true) external ffi.Pointer Image_decode( ffi.Pointer data, int length, ffi.Pointer name, + bool alpha ); @ffi.Native Function(ffi.Pointer)>( @@ -2367,6 +2368,7 @@ external void Image_createEmptyRenderThread( ffi.Pointer, ffi.Size, ffi.Pointer, + ffi.Bool, ffi.Pointer< ffi.NativeFunction< ffi.Void Function(ffi.Pointer)>>)>(isLeaf: true) @@ -2374,6 +2376,7 @@ external void Image_decodeRenderThread( ffi.Pointer data, int length, ffi.Pointer name, + bool alpha, ffi.Pointer)>> onComplete, ); diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index ef592057..4456a072 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -393,6 +393,7 @@ extension type NativeLibrary(JSObject _) implements JSObject { JSFunction f, String signature); external void removeFunction(Pointer> f); external JSUint8Array get HEAPU8; + external JSFloat32Array get HEAPF32; external void _Thermion_resizeCanvas( int width, @@ -812,6 +813,7 @@ extension type NativeLibrary(JSObject _) implements JSObject { Pointer data, size_t length, Pointer name, + bool alpha ); external Pointer _Image_getBytes( Pointer tLinearImage, @@ -1670,6 +1672,7 @@ extension type NativeLibrary(JSObject _) implements JSObject { Pointer data, size_t length, Pointer name, + bool alpha, Pointer)>> onComplete, ); @@ -3071,8 +3074,9 @@ self.Pointer Image_decode( self.Pointer data, Dart__darwin_size_t length, self.Pointer name, + bool alpha ) { - final result = _lib._Image_decode(data, length, name); + final result = _lib._Image_decode(data, length, name, alpha); return self.Pointer(result); } @@ -4839,11 +4843,12 @@ void Image_decodeRenderThread( self.Pointer data, Dart__darwin_size_t length, self.Pointer name, + bool alpha, self.Pointer)>> onComplete, ) { final result = - _lib._Image_decodeRenderThread(data, length, name, onComplete.cast()); + _lib._Image_decodeRenderThread(data, length, name, alpha, onComplete.cast()); return result; } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index 793cb84a..58f3f455 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -310,6 +310,7 @@ class FFIFilamentApp extends FilamentApp { TextureSamplerType textureSamplerType = TextureSamplerType.SAMPLER_2D, TextureFormat textureFormat = TextureFormat.RGBA16F, int? importedTextureHandle}) async { + var bitmask = flags.fold(0, (a, b) => a | b.value); final texturePtr = await withPointerCallback((cb) { @@ -362,19 +363,28 @@ class FFIFilamentApp extends FilamentApp { return FFITextureSampler(samplerPtr); } + /// Decodes the image data into a native LinearImage (floating point). + /// If [requireAlpha] is true, the decoded image will always contain an + /// alpha channel (even if the original image did not contain one). /// - /// - /// - Future decodeImage(Uint8List data) async { - final name = "image"; + Future decodeImage(Uint8List data, { String name = "image", bool requireAlpha = false}) async { + late Pointer stackPtr; if (FILAMENT_WASM) { //stackPtr = stackSave(); } + var now = DateTime.now(); + var ptr = Image_decode( data.address, data.length, name.toNativeUtf8().cast(), + requireAlpha + ); + + var finished = DateTime.now(); + print( + "Image_decode (render thread) finished in ${finished.millisecondsSinceEpoch - now.millisecondsSinceEpoch}ms", ); if (FILAMENT_WASM) { @@ -1114,11 +1124,11 @@ class FFIFilamentApp extends FilamentApp { if (FILAMENT_WASM) { stackPtr = stackSave(); } - TransformManager_setTransform(transformManager, entity, matrix4ToDouble4x4(transform)); + TransformManager_setTransform( + transformManager, entity, matrix4ToDouble4x4(transform)); if (FILAMENT_WASM) { stackRestore(stackPtr); } - } /// @@ -1129,7 +1139,7 @@ class FFIFilamentApp extends FilamentApp { if (FILAMENT_WASM) { stackPtr = stackSave(); } - + var transform = double4x4ToMatrix4( TransformManager_getWorldTransform(transformManager, entity)); if (FILAMENT_WASM) { @@ -1137,5 +1147,4 @@ class FFIFilamentApp extends FilamentApp { } return transform; } - } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_texture.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_texture.dart index 2d5bc51e..96adb51e 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_texture.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_texture.dart @@ -165,15 +165,10 @@ class FFILinearImage extends LinearImage { } static Future decode(Uint8List data, - [String name = "image"]) async { - final namePtr = name.toNativeUtf8(); - - final imagePtr = await withPointerCallback((cb) { - Image_decodeRenderThread( - data.address, data.lengthInBytes, namePtr.cast(), cb); - }); - - return FFILinearImage(imagePtr); + {String name = "image", bool requireAlpha = false}) async { + final image = await FilamentApp.instance! + .decodeImage(data, name: name, requireAlpha: requireAlpha); + return image as FFILinearImage; } Future destroy() async { diff --git a/thermion_dart/lib/src/filament/src/interface/filament_app.dart b/thermion_dart/lib/src/filament/src/interface/filament_app.dart index a66a1485..ad0eef14 100644 --- a/thermion_dart/lib/src/filament/src/interface/filament_app.dart +++ b/thermion_dart/lib/src/filament/src/interface/filament_app.dart @@ -133,7 +133,7 @@ abstract class FilamentApp { /// /// Decodes the specified image data. /// - Future decodeImage(Uint8List data); + Future decodeImage(Uint8List data, { String name = "image", bool requireAlpha = false}); /// /// Creates an (empty) imge with the given dimensions. diff --git a/thermion_dart/lib/src/filament/src/interface/texture.dart b/thermion_dart/lib/src/filament/src/interface/texture.dart index a19ced43..f37f7658 100644 --- a/thermion_dart/lib/src/filament/src/interface/texture.dart +++ b/thermion_dart/lib/src/filament/src/interface/texture.dart @@ -484,11 +484,17 @@ abstract class LinearImage { Future getHeight(); Future getChannels(); + /// Decodes the image contained in [data] and returns a texture of + /// the corresponding size with the image set as mip-level 0. /// /// - /// - static Future decodeToTexture(Uint8List data, { TextureFormat textureFormat = TextureFormat.RGB32F, PixelDataFormat pixelDataFormat = PixelDataFormat.RGB, PixelDataType pixelDataType = PixelDataType.FLOAT, int levels = 1}) async { - final decodedImage = await FilamentApp.instance!.decodeImage(data); + static Future decodeToTexture(Uint8List data, { + TextureFormat textureFormat = TextureFormat.RGB32F, + PixelDataFormat pixelDataFormat = PixelDataFormat.RGB, + PixelDataType pixelDataType = PixelDataType.FLOAT, + int levels = 1, + bool requireAlpha = false}) async { + final decodedImage = await FilamentApp.instance!.decodeImage(data, requireAlpha: requireAlpha); final texture = await FilamentApp.instance!.createTexture( await decodedImage.getWidth(), diff --git a/thermion_dart/native/include/c_api/TTexture.h b/thermion_dart/native/include/c_api/TTexture.h index 76991a01..360d3beb 100644 --- a/thermion_dart/native/include/c_api/TTexture.h +++ b/thermion_dart/native/include/c_api/TTexture.h @@ -262,7 +262,7 @@ EMSCRIPTEN_KEEPALIVE TTextureUsage Texture_getUsage(TTexture *tTexture, uint32_t EMSCRIPTEN_KEEPALIVE void Texture_generateMipMaps(TTexture *tTexture, TEngine *tEngine); EMSCRIPTEN_KEEPALIVE TLinearImage *Image_createEmpty(uint32_t width,uint32_t height,uint32_t channel); -EMSCRIPTEN_KEEPALIVE TLinearImage *Image_decode(uint8_t* data, size_t length, const char* name); +EMSCRIPTEN_KEEPALIVE TLinearImage *Image_decode(uint8_t* data, size_t length, const char* name, bool alpha); EMSCRIPTEN_KEEPALIVE float *Image_getBytes(TLinearImage *tLinearImage); EMSCRIPTEN_KEEPALIVE void Image_destroy(TLinearImage *tLinearImage); EMSCRIPTEN_KEEPALIVE uint32_t Image_getWidth(TLinearImage *tLinearImage); diff --git a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h index 4a1f3e23..d36c0daf 100644 --- a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h +++ b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h @@ -172,7 +172,7 @@ namespace thermion // Image methods void Image_createEmptyRenderThread(uint32_t width, uint32_t height, uint32_t channel, void (*onComplete)(TLinearImage *)); - void Image_decodeRenderThread(uint8_t* data, size_t length, const char* name, void (*onComplete)(TLinearImage *)); + void Image_decodeRenderThread(uint8_t* data, size_t length, const char* name, bool alpha, void (*onComplete)(TLinearImage *)); void Image_getBytesRenderThread(TLinearImage *tLinearImage, void (*onComplete)(float *)); void Image_destroyRenderThread(TLinearImage *tLinearImage, uint32_t requestId, VoidCallback onComplete); void Image_getWidthRenderThread(TLinearImage *tLinearImage, void (*onComplete)(uint32_t)); diff --git a/thermion_dart/native/src/c_api/TTexture.cpp b/thermion_dart/native/src/c_api/TTexture.cpp index fce288c3..f43870d8 100644 --- a/thermion_dart/native/src/c_api/TTexture.cpp +++ b/thermion_dart/native/src/c_api/TTexture.cpp @@ -40,14 +40,16 @@ namespace thermion } - EMSCRIPTEN_KEEPALIVE TLinearImage *Image_decode(uint8_t *data, size_t length, const char *name = "image") + EMSCRIPTEN_KEEPALIVE TLinearImage *Image_decode(uint8_t *data, size_t length, const char *name = "image", bool alpha = true) { auto start = std::chrono::high_resolution_clock::now(); int width, height, channels; + + TRACE("Loading image from buffer of length %lu bytes (alpha : %s)", length, alpha ? "true" : "false"); - uint8_t *imgData = stbi_load_from_memory(data, length, &width, &height, &channels, 0); + uint8_t *imgData = stbi_load_from_memory(data, length, &width, &height, &channels, alpha ? 4 : 3); if (!imgData) { ERROR("Failed to decode image"); @@ -56,7 +58,7 @@ namespace thermion LinearImage *linearImage; - if(channels == 4) { + if(alpha) { linearImage = new LinearImage(toLinearWithAlpha( width, height, @@ -75,7 +77,7 @@ namespace thermion auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast(end - start); - TRACE("Image decoded successfully in %lld ms", duration.count()); + TRACE("Image decoded successfully in %lld ms (%dx%dx%d)", duration.count(), width, height, channels); if (!linearImage->isValid()) { @@ -383,28 +385,14 @@ namespace thermion switch (bufferFormat) { - case PixelBufferDescriptor::PixelDataFormat::RGB: - case PixelBufferDescriptor::PixelDataFormat::RGBA: - { - size_t expectedSize = width * height * channels * sizeof(float); - if (size != expectedSize) - { - Log("Size mismatch (expected %lu, got %lu)", expectedSize, size); + case PixelBufferDescriptor::PixelDataFormat::RGB: + case PixelBufferDescriptor::PixelDataFormat::RGBA: + case PixelBufferDescriptor::PixelDataFormat::RGB_INTEGER: + case PixelBufferDescriptor::PixelDataFormat::RGBA_INTEGER: + break; + default: + Log("Unsupported buffer format type : %d", bufferFormat); return false; - } - break; - } - case PixelBufferDescriptor::PixelDataFormat::RGB_INTEGER: - case PixelBufferDescriptor::PixelDataFormat::RGBA_INTEGER: - if (size != width * height * channels * sizeof(uint8_t)) - { - Log("Size mismatch"); - // return false; - } - break; - default: - Log("Unsupported buffer format type : %d", bufferFormat); - return false; } // the texture upload is async, so we need to copy the buffer diff --git a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp index a4548e8e..5f1373cc 100644 --- a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp +++ b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp @@ -755,12 +755,12 @@ extern "C" auto fut = _renderThread->add_task(lambda); } - EMSCRIPTEN_KEEPALIVE void Image_decodeRenderThread(uint8_t *data, size_t length, const char *name, void (*onComplete)(TLinearImage *)) + EMSCRIPTEN_KEEPALIVE void Image_decodeRenderThread(uint8_t *data, size_t length, const char *name, bool alpha, void (*onComplete)(TLinearImage *)) { std::packaged_task lambda( [=]() mutable { - auto image = Image_decode(data, length, name); + auto image = Image_decode(data, length, name, alpha); PROXY(onComplete(image)); }); auto fut = _renderThread->add_task(lambda);