From 2674ef6e6f8b5cf11631dd44460d452ea277fcd0 Mon Sep 17 00:00:00 2001 From: Mark Wahnish Date: Mon, 27 Apr 2026 21:38:15 -0400 Subject: [PATCH 1/4] fix: For hot reloads on web, the render loop pthread wasn't exiting. Every hot reload would spawn another pthread, until an out of memory exception occurred. --- .../native/src/rendering/RenderThread.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/thermion_dart/native/src/rendering/RenderThread.cpp b/thermion_dart/native/src/rendering/RenderThread.cpp index 0808969d..f707a9fa 100644 --- a/thermion_dart/native/src/rendering/RenderThread.cpp +++ b/thermion_dart/native/src/rendering/RenderThread.cpp @@ -26,9 +26,15 @@ bool loopExitTimeValid = false; static void mainLoop(void* arg) { auto *rt = static_cast(arg); - if (!rt->mStop) { - rt->iter(); + // If the render thread has been asked to stop, break the loop and exit immediately. + if (rt->mStop) { + emscripten_cancel_main_loop(); + loopExitTime = std::chrono::high_resolution_clock::now(); + loopExitTimeValid = true; + return; } + + rt->iter(); loopExitTime = std::chrono::high_resolution_clock::now(); loopExitTimeValid = true; } @@ -82,7 +88,11 @@ RenderThread::~RenderThread() _tasks.pop_front(); task(); } - #ifndef __EMSCRIPTEN__ + + #ifdef __EMSCRIPTEN__ + // Waiting for the main loop to exit before continuing + pthread_join(t, nullptr); + #else t->join(); delete t; #endif From 93b0f3f26703d185a5af1016a79a07e26fa88883 Mon Sep 17 00:00:00 2001 From: Mark Wahnish Date: Mon, 27 Apr 2026 21:39:23 -0400 Subject: [PATCH 2/4] fix: Race condition - In _tick(), if FilamentApp.instance?.render() is called when native assets have been freed but FilamentApp.instance is not null a crash occurs. Nulling the instance prior to destroying the render thread prevents this race condition --- .../lib/src/filament/src/implementation/ffi_filament_app.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3118d933..57d0d0b2 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 @@ -289,14 +289,14 @@ class FFIFilamentApp extends FilamentApp { await renderManager.detachAll(swapChain); await destroySwapChain(swapChain); } + + FilamentApp.instance = null; renderManager.destroy(); await withVoidCallback((requestId, cb) async { Engine_destroyRenderThread(engine, requestId, cb); }); RenderThread_destroy(); - - FilamentApp.instance = null; for (final callback in _onDestroy) { await callback.call(); } From 9216bd957156649d74729516b2ee83592c43ae58 Mon Sep 17 00:00:00 2001 From: Mark Wahnish Date: Mon, 27 Apr 2026 21:41:37 -0400 Subject: [PATCH 3/4] Fix: In some cases, the wasm callback queue gets interrupted (hot reloads). This can cause the app to freeze. This fix automatically polls the queue so we are not reliant on the runtime. --- .../lib/src/bindings/src/js_interop.dart | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/thermion_dart/lib/src/bindings/src/js_interop.dart b/thermion_dart/lib/src/bindings/src/js_interop.dart index 03aba0e7..29832196 100644 --- a/thermion_dart/lib/src/bindings/src/js_interop.dart +++ b/thermion_dart/lib/src/bindings/src/js_interop.dart @@ -103,6 +103,12 @@ Future> withPointerCallback( func.call(onComplete_interopFnPtr.cast()); + + while (!completer.isCompleted) { + _NativeLibrary.instance._execute_queue(); + await Future.delayed(Duration(milliseconds: 1)); + } + var ptr = await completer.future; onComplete_interopFnPtr.dispose(); @@ -120,7 +126,11 @@ Future withBoolCallback( final onComplete_interopFnPtr = callback.addFunction(); func.call(onComplete_interopFnPtr.cast()); - await completer.future; + + while (!completer.isCompleted) { + _NativeLibrary.instance._execute_queue(); + await Future.delayed(Duration(milliseconds: 1)); + } return completer.future; } @@ -134,20 +144,26 @@ Future withFloatCallback( }; var ptr = callback.addFunction(); func.call(ptr); - await completer.future; + while (!completer.isCompleted) { + _NativeLibrary.instance._execute_queue(); + await Future.delayed(Duration(milliseconds: 1)); + } return completer.future; } Future withIntCallback( Function(Pointer>) func) async { - final completer = Completer(); + final completer = Completer(); // ignore: prefer_function_declarations_over_variables void Function(int) callback = (int result) { completer.complete(result); }; var ptr = callback.addFunction(); func.call(ptr); - await completer.future; + while (!completer.isCompleted) { + _NativeLibrary.instance._execute_queue(); + await Future.delayed(Duration(milliseconds: 1)); + } return completer.future; } From 3f907686b0d0e2ab24fd74b1aaca18ce312e9a68 Mon Sep 17 00:00:00 2001 From: Mark Wahnish Date: Mon, 27 Apr 2026 21:41:55 -0400 Subject: [PATCH 4/4] Fix: Making Thermion handle reinitialization better on web. In particular, there was a crash that could happen when the Thermion Widget was rebuilt --- .../src/thermion_flutter_plugin_web.dart | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_web.dart b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_web.dart index 063edd93..2a0dbc0a 100644 --- a/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_web.dart +++ b/thermion_flutter/thermion_flutter/lib/src/platform/src/thermion_flutter_plugin_web.dart @@ -17,6 +17,7 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { static final _descriptors = []; static final _destroyed = []; static final _logger = Logger('ThermionFlutterPluginImpl'); + static int? _frameRequestId; static Future loadAsset(String path) async { if (path.startsWith("file://")) { @@ -48,13 +49,37 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { } _destroyed.clear(); - window.requestAnimationFrame(_tick.toJS); + _frameRequestId = window.requestAnimationFrame(_tick.toJS); + } + + static void _ensureFrameLoopRunning() { + _frameRequestId ??= window.requestAnimationFrame(_tick.toJS); + } + + + static void _resetWebState() { + if (_frameRequestId != null) { + window.cancelAnimationFrame(_frameRequestId!); + _frameRequestId = null; + } + + _stackPtr = null; + swapChain = null; + _descriptors.clear(); + _destroyed.clear(); } static SwapChain? swapChain; @override Future initialize({bool destroySwapchain = true}) async { + if (FilamentApp.instance != null && swapChain != null) { + // Hot reload re-enters initialize without disposing the existing web + // engine. Reuse the live app instead of spawning another em-pthread. + _ensureFrameLoopRunning(); + return swapChain!; + } + HTMLCanvasElement? canvas; // first, try and initialize bindings to see if the user has included thermion_dart.js manually in index.html try { @@ -118,6 +143,10 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { sharedContext: null, uberArchivePath: options.uberarchivePath); await FFIFilamentApp.create(config: config); + // resetting the web state when the app is destroyed + (FilamentApp.instance as FFIFilamentApp).onDestroy(() async { + _resetWebState(); + }); // Use createSwapChain with nullptr to render to the canvas's default // framebuffer (framebuffer 0). createHeadlessSwapChain creates an offscreen @@ -126,7 +155,7 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin { print("Created 1x1 headless swapchain"); - window.requestAnimationFrame(_tick.toJS); + _ensureFrameLoopRunning(); return swapChain!; }