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)
This commit is contained in:
Nick Fisher
2025-05-29 22:26:42 +08:00
parent c789e75af5
commit 86894cf583
12 changed files with 115 additions and 61 deletions

View File

@@ -215,3 +215,10 @@ extension DartBigIntExtension on int {
return this; return this;
} }
} }
extension Float32ListExtension on Float32List {
Uint8List asUint8List() {
return this.buffer.asUint8List(this.offsetInBytes);
}
}

View File

@@ -10,10 +10,22 @@ const FILAMENT_SINGLE_THREADED = true;
const FILAMENT_WASM = true; const FILAMENT_WASM = true;
const IS_WINDOWS = false; const IS_WINDOWS = false;
final _allocations = <TypedData>{};
Int32List makeInt32List(int length) { Int32List makeInt32List(int length) {
var ptr = malloc<Int32>(length * 4); var ptr = stackAlloc<Int32>(length * 4);
var buf = _NativeLibrary.instance._emscripten_make_int32_buffer(ptr, length); 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<Float32>(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 { extension type _NativeLibrary(JSObject _) implements JSObject {
@@ -34,6 +46,8 @@ extension type _NativeLibrary(JSObject _) implements JSObject {
Pointer<Float64> ptr, int length); Pointer<Float64> ptr, int length);
external Pointer _emscripten_get_byte_offset(JSObject obj); 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 int _emscripten_stack_get_free();
external void _execute_queue(); external void _execute_queue();
@@ -47,8 +61,8 @@ extension type _NativeLibrary(JSObject _) implements JSObject {
extension FreeTypedData<T> on TypedData { extension FreeTypedData<T> on TypedData {
void free() { void free() {
final ptr = Pointer<Void>(this.offsetInBytes); Pointer<Void>(this.offsetInBytes).free();
ptr.free(); _allocations.remove(this);
} }
} }
@@ -64,7 +78,12 @@ Pointer<T> getPointer<T extends NativeType>(TypedData data, JSObject obj) {
return ptr; return ptr;
} }
extension JSBackingBuffer on JSUint8Array { extension JSUint8BackingBuffer on JSUint8Array {
@JS('buffer')
external JSObject buffer;
}
extension JSFloat32BackingBuffer on JSFloat32Array {
@JS('buffer') @JS('buffer')
external JSObject buffer; external JSObject buffer;
} }
@@ -117,7 +136,11 @@ extension Uint8ListExtension on Uint8List {
final bar = final bar =
Uint8ArrayWrapper(NativeLibrary.instance.HEAPU8.buffer, ptr, length) Uint8ArrayWrapper(NativeLibrary.instance.HEAPU8.buffer, ptr, length)
as JSUint8Array; as JSUint8Array;
var now = DateTime.now();
bar.toDart.setRange(0, length, this); bar.toDart.setRange(0, length, this);
var finished = DateTime.now();
print(
"uint8list copy finished in ${finished.millisecondsSinceEpoch - now.millisecondsSinceEpoch}ms");
return ptr; return ptr;
} }
} }
@@ -131,6 +154,11 @@ extension Float32ListExtension on Float32List {
bar.toDart.setRange(0, length, this); bar.toDart.setRange(0, length, this);
return ptr; return ptr;
} }
Uint8List asUint8List() {
var ptr = Pointer<Uint8>(_NativeLibrary.instance._emscripten_get_byte_offset(this.toJS));
return ptr.asTypedList(length * 4);
}
} }
extension Int16ListExtension on Int16List { extension Int16ListExtension on Int16List {
@@ -176,6 +204,10 @@ extension Int32ListExtension on Int32List {
if (this.lengthInBytes == 0) { if (this.lengthInBytes == 0) {
return nullptr; return nullptr;
} }
if (_allocations.contains(this)) {
return Pointer<Int32>(
_NativeLibrary.instance._emscripten_get_byte_offset(this.toJS));
}
try { try {
this.buffer.asUint8List(this.offsetInBytes); this.buffer.asUint8List(this.offsetInBytes);
final ptr = getPointer<Int32>(this, this.toJS); final ptr = getPointer<Int32>(this, this.toJS);
@@ -214,12 +246,21 @@ extension Float64ListExtension on Float64List {
} }
} }
extension AsUint8List on Pointer<Uint8> {
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<Float> { extension AsFloat32List on Pointer<Float> {
Float32List asTypedList(int length) { Float32List asTypedList(int length) {
final start = addr; final start = addr;
final wrapper = final wrapper = Float32ArrayWrapper(
Float32ArrayWrapper(NativeLibrary.instance.HEAPU8.buffer, start, length) NativeLibrary.instance.HEAPF32.buffer, start, length) as JSFloat32Array;
as JSFloat32Array;
return wrapper.toDart; return wrapper.toDart;
} }
} }

View File

@@ -738,11 +738,12 @@ external ffi.Pointer<TLinearImage> Image_createEmpty(
@ffi.Native< @ffi.Native<
ffi.Pointer<TLinearImage> Function( ffi.Pointer<TLinearImage> Function(
ffi.Pointer<ffi.Uint8>, ffi.Size, ffi.Pointer<ffi.Char>)>(isLeaf: true) ffi.Pointer<ffi.Uint8>, ffi.Size, ffi.Pointer<ffi.Char>, ffi.Bool alpha)>(isLeaf: true)
external ffi.Pointer<TLinearImage> Image_decode( external ffi.Pointer<TLinearImage> Image_decode(
ffi.Pointer<ffi.Uint8> data, ffi.Pointer<ffi.Uint8> data,
int length, int length,
ffi.Pointer<ffi.Char> name, ffi.Pointer<ffi.Char> name,
bool alpha
); );
@ffi.Native<ffi.Pointer<ffi.Float> Function(ffi.Pointer<TLinearImage>)>( @ffi.Native<ffi.Pointer<ffi.Float> Function(ffi.Pointer<TLinearImage>)>(
@@ -2367,6 +2368,7 @@ external void Image_createEmptyRenderThread(
ffi.Pointer<ffi.Uint8>, ffi.Pointer<ffi.Uint8>,
ffi.Size, ffi.Size,
ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Bool,
ffi.Pointer< ffi.Pointer<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<TLinearImage>)>>)>(isLeaf: true) ffi.Void Function(ffi.Pointer<TLinearImage>)>>)>(isLeaf: true)
@@ -2374,6 +2376,7 @@ external void Image_decodeRenderThread(
ffi.Pointer<ffi.Uint8> data, ffi.Pointer<ffi.Uint8> data,
int length, int length,
ffi.Pointer<ffi.Char> name, ffi.Pointer<ffi.Char> name,
bool alpha,
ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<TLinearImage>)>> ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<TLinearImage>)>>
onComplete, onComplete,
); );

View File

@@ -393,6 +393,7 @@ extension type NativeLibrary(JSObject _) implements JSObject {
JSFunction f, String signature); JSFunction f, String signature);
external void removeFunction<T>(Pointer<NativeFunction<T>> f); external void removeFunction<T>(Pointer<NativeFunction<T>> f);
external JSUint8Array get HEAPU8; external JSUint8Array get HEAPU8;
external JSFloat32Array get HEAPF32;
external void _Thermion_resizeCanvas( external void _Thermion_resizeCanvas(
int width, int width,
@@ -812,6 +813,7 @@ extension type NativeLibrary(JSObject _) implements JSObject {
Pointer<Uint8> data, Pointer<Uint8> data,
size_t length, size_t length,
Pointer<Char> name, Pointer<Char> name,
bool alpha
); );
external Pointer<Float32> _Image_getBytes( external Pointer<Float32> _Image_getBytes(
Pointer<TLinearImage> tLinearImage, Pointer<TLinearImage> tLinearImage,
@@ -1670,6 +1672,7 @@ extension type NativeLibrary(JSObject _) implements JSObject {
Pointer<Uint8> data, Pointer<Uint8> data,
size_t length, size_t length,
Pointer<Char> name, Pointer<Char> name,
bool alpha,
Pointer<self.NativeFunction<void Function(PointerClass<TLinearImage>)>> Pointer<self.NativeFunction<void Function(PointerClass<TLinearImage>)>>
onComplete, onComplete,
); );
@@ -3071,8 +3074,9 @@ self.Pointer<TLinearImage> Image_decode(
self.Pointer<Uint8> data, self.Pointer<Uint8> data,
Dart__darwin_size_t length, Dart__darwin_size_t length,
self.Pointer<Char> name, self.Pointer<Char> name,
bool alpha
) { ) {
final result = _lib._Image_decode(data, length, name); final result = _lib._Image_decode(data, length, name, alpha);
return self.Pointer<TLinearImage>(result); return self.Pointer<TLinearImage>(result);
} }
@@ -4839,11 +4843,12 @@ void Image_decodeRenderThread(
self.Pointer<Uint8> data, self.Pointer<Uint8> data,
Dart__darwin_size_t length, Dart__darwin_size_t length,
self.Pointer<Char> name, self.Pointer<Char> name,
bool alpha,
self.Pointer<self.NativeFunction<void Function(Pointer<TLinearImage>)>> self.Pointer<self.NativeFunction<void Function(Pointer<TLinearImage>)>>
onComplete, onComplete,
) { ) {
final result = final result =
_lib._Image_decodeRenderThread(data, length, name, onComplete.cast()); _lib._Image_decodeRenderThread(data, length, name, alpha, onComplete.cast());
return result; return result;
} }

View File

@@ -310,6 +310,7 @@ class FFIFilamentApp extends FilamentApp<Pointer> {
TextureSamplerType textureSamplerType = TextureSamplerType.SAMPLER_2D, TextureSamplerType textureSamplerType = TextureSamplerType.SAMPLER_2D,
TextureFormat textureFormat = TextureFormat.RGBA16F, TextureFormat textureFormat = TextureFormat.RGBA16F,
int? importedTextureHandle}) async { int? importedTextureHandle}) async {
var bitmask = flags.fold(0, (a, b) => a | b.value); var bitmask = flags.fold(0, (a, b) => a | b.value);
final texturePtr = await withPointerCallback<TTexture>((cb) { final texturePtr = await withPointerCallback<TTexture>((cb) {
@@ -362,19 +363,28 @@ class FFIFilamentApp extends FilamentApp<Pointer> {
return FFITextureSampler(samplerPtr); 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<LinearImage> decodeImage(Uint8List data, { String name = "image", bool requireAlpha = false}) async {
///
Future<LinearImage> decodeImage(Uint8List data) async {
final name = "image";
late Pointer stackPtr; late Pointer stackPtr;
if (FILAMENT_WASM) { if (FILAMENT_WASM) {
//stackPtr = stackSave(); //stackPtr = stackSave();
} }
var now = DateTime.now();
var ptr = Image_decode( var ptr = Image_decode(
data.address, data.address,
data.length, data.length,
name.toNativeUtf8().cast<Char>(), name.toNativeUtf8().cast<Char>(),
requireAlpha
);
var finished = DateTime.now();
print(
"Image_decode (render thread) finished in ${finished.millisecondsSinceEpoch - now.millisecondsSinceEpoch}ms",
); );
if (FILAMENT_WASM) { if (FILAMENT_WASM) {
@@ -1114,11 +1124,11 @@ class FFIFilamentApp extends FilamentApp<Pointer> {
if (FILAMENT_WASM) { if (FILAMENT_WASM) {
stackPtr = stackSave(); stackPtr = stackSave();
} }
TransformManager_setTransform(transformManager, entity, matrix4ToDouble4x4(transform)); TransformManager_setTransform(
transformManager, entity, matrix4ToDouble4x4(transform));
if (FILAMENT_WASM) { if (FILAMENT_WASM) {
stackRestore(stackPtr); stackRestore(stackPtr);
} }
} }
/// ///
@@ -1129,7 +1139,7 @@ class FFIFilamentApp extends FilamentApp<Pointer> {
if (FILAMENT_WASM) { if (FILAMENT_WASM) {
stackPtr = stackSave(); stackPtr = stackSave();
} }
var transform = double4x4ToMatrix4( var transform = double4x4ToMatrix4(
TransformManager_getWorldTransform(transformManager, entity)); TransformManager_getWorldTransform(transformManager, entity));
if (FILAMENT_WASM) { if (FILAMENT_WASM) {
@@ -1137,5 +1147,4 @@ class FFIFilamentApp extends FilamentApp<Pointer> {
} }
return transform; return transform;
} }
} }

View File

@@ -165,15 +165,10 @@ class FFILinearImage extends LinearImage {
} }
static Future<FFILinearImage> decode(Uint8List data, static Future<FFILinearImage> decode(Uint8List data,
[String name = "image"]) async { {String name = "image", bool requireAlpha = false}) async {
final namePtr = name.toNativeUtf8(); final image = await FilamentApp.instance!
.decodeImage(data, name: name, requireAlpha: requireAlpha);
final imagePtr = await withPointerCallback<TLinearImage>((cb) { return image as FFILinearImage;
Image_decodeRenderThread(
data.address, data.lengthInBytes, namePtr.cast(), cb);
});
return FFILinearImage(imagePtr);
} }
Future<void> destroy() async { Future<void> destroy() async {

View File

@@ -133,7 +133,7 @@ abstract class FilamentApp<T> {
/// ///
/// Decodes the specified image data. /// Decodes the specified image data.
/// ///
Future<LinearImage> decodeImage(Uint8List data); Future<LinearImage> decodeImage(Uint8List data, { String name = "image", bool requireAlpha = false});
/// ///
/// Creates an (empty) imge with the given dimensions. /// Creates an (empty) imge with the given dimensions.

View File

@@ -484,11 +484,17 @@ abstract class LinearImage {
Future<int> getHeight(); Future<int> getHeight();
Future<int> getChannels(); Future<int> 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<Texture> decodeToTexture(Uint8List data, {
static Future<Texture> decodeToTexture(Uint8List data, { TextureFormat textureFormat = TextureFormat.RGB32F, PixelDataFormat pixelDataFormat = PixelDataFormat.RGB, PixelDataType pixelDataType = PixelDataType.FLOAT, int levels = 1}) async { TextureFormat textureFormat = TextureFormat.RGB32F,
final decodedImage = await FilamentApp.instance!.decodeImage(data); 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( final texture = await FilamentApp.instance!.createTexture(
await decodedImage.getWidth(), await decodedImage.getWidth(),

View File

@@ -262,7 +262,7 @@ EMSCRIPTEN_KEEPALIVE TTextureUsage Texture_getUsage(TTexture *tTexture, uint32_t
EMSCRIPTEN_KEEPALIVE void Texture_generateMipMaps(TTexture *tTexture, TEngine *tEngine); 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_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 float *Image_getBytes(TLinearImage *tLinearImage);
EMSCRIPTEN_KEEPALIVE void Image_destroy(TLinearImage *tLinearImage); EMSCRIPTEN_KEEPALIVE void Image_destroy(TLinearImage *tLinearImage);
EMSCRIPTEN_KEEPALIVE uint32_t Image_getWidth(TLinearImage *tLinearImage); EMSCRIPTEN_KEEPALIVE uint32_t Image_getWidth(TLinearImage *tLinearImage);

View File

@@ -172,7 +172,7 @@ namespace thermion
// Image methods // Image methods
void Image_createEmptyRenderThread(uint32_t width, uint32_t height, uint32_t channel, void (*onComplete)(TLinearImage *)); 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_getBytesRenderThread(TLinearImage *tLinearImage, void (*onComplete)(float *));
void Image_destroyRenderThread(TLinearImage *tLinearImage, uint32_t requestId, VoidCallback onComplete); void Image_destroyRenderThread(TLinearImage *tLinearImage, uint32_t requestId, VoidCallback onComplete);
void Image_getWidthRenderThread(TLinearImage *tLinearImage, void (*onComplete)(uint32_t)); void Image_getWidthRenderThread(TLinearImage *tLinearImage, void (*onComplete)(uint32_t));

View File

@@ -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(); auto start = std::chrono::high_resolution_clock::now();
int width, height, channels; 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) { if (!imgData) {
ERROR("Failed to decode image"); ERROR("Failed to decode image");
@@ -56,7 +58,7 @@ namespace thermion
LinearImage *linearImage; LinearImage *linearImage;
if(channels == 4) { if(alpha) {
linearImage = new LinearImage(toLinearWithAlpha<uint8_t>( linearImage = new LinearImage(toLinearWithAlpha<uint8_t>(
width, width,
height, height,
@@ -75,7 +77,7 @@ namespace thermion
auto end = std::chrono::high_resolution_clock::now(); auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(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()) if (!linearImage->isValid())
{ {
@@ -383,28 +385,14 @@ namespace thermion
switch (bufferFormat) switch (bufferFormat)
{ {
case PixelBufferDescriptor::PixelDataFormat::RGB: case PixelBufferDescriptor::PixelDataFormat::RGB:
case PixelBufferDescriptor::PixelDataFormat::RGBA: case PixelBufferDescriptor::PixelDataFormat::RGBA:
{ case PixelBufferDescriptor::PixelDataFormat::RGB_INTEGER:
size_t expectedSize = width * height * channels * sizeof(float); case PixelBufferDescriptor::PixelDataFormat::RGBA_INTEGER:
if (size != expectedSize) break;
{ default:
Log("Size mismatch (expected %lu, got %lu)", expectedSize, size); Log("Unsupported buffer format type : %d", bufferFormat);
return false; 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 // the texture upload is async, so we need to copy the buffer

View File

@@ -755,12 +755,12 @@ extern "C"
auto fut = _renderThread->add_task(lambda); 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<void()> lambda( std::packaged_task<void()> lambda(
[=]() mutable [=]() mutable
{ {
auto image = Image_decode(data, length, name); auto image = Image_decode(data, length, name, alpha);
PROXY(onComplete(image)); PROXY(onComplete(image));
}); });
auto fut = _renderThread->add_task(lambda); auto fut = _renderThread->add_task(lambda);