From e5c646fc5800c089330424d438844d8ffdf182fe Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Sun, 10 May 2026 00:11:56 +0700 Subject: [PATCH] viewer: detach view from RenderManager before destroying it on dispose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ThermionViewerFFI.dispose()` destroyed the underlying Filament view without first removing it from `FFIRenderManager`'s Dart-side `_attachments` map. The map keys each swap-chain entry to a list of (renderOrder, View) tuples; if a view is destroyed while still listed, any later `attach` / `detach` / `_syncViews` call from a *different* viewer will iterate the now-stale tuple, retrieve the freed view's native pointer, and pass it to `RenderManager_setRenderableRenderThread`. Filament then does a `handle_cast` and aborts with the generic "Postcondition: corrupted heap Handle ... tag=(no tag)". Reproduces reliably in multi-viewer Flutter apps when one viewer is disposed concurrently with another viewer being mounted: - Old viewer.dispose() runs (queued onto whatever the host serialises on, e.g. a static `_createSerial` Future chain). - New viewer's createTextureAndBindToView calls renderManager.attach(newView, newSwapChain), which calls `_syncViews()` — this walks every entry in `_attachments`, including the entry containing the just-destroyed old view. - The freed pointer hits Filament's handle table → SIGABRT. Symptom on Android (which is the most multi-viewer-heavy platform in practice) is an immediate crash on toggling viewer count. iOS / macOS happen to work because their host plugins don't require the same reattach pattern, but the bug is real on every platform — the symptom is just less reproducible. Fix: call `FilamentApp.instance!.renderManager.detach(view)` before `View_setScene(view, nullptr)` and `destroyView(view)`. `detach(view)` (no swap chain argument) iterates `_attachments` and removes the view from every entry, then runs `_syncViews()` to push the cleaned state to C++ RenderManager. After that, destroying the view is safe — no other code path will try to dereference its handle. --- .../src/ffi/src/thermion_viewer_ffi.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart index bcbb3af6..d1c781a6 100644 --- a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart +++ b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart @@ -173,6 +173,25 @@ class ThermionViewerFFI extends ThermionViewer { } View_setScene(view.getNativeHandle(), nullptr); + // Detach from any swap chain in the RenderManager BEFORE destroying + // the view. RenderManager's `_attachments` map (Dart side) keys + // each swap-chain entry to a list of (renderOrder, View) tuples; + // if `destroyView` runs while this view is still listed, the next + // `attach`/`detach`/`_syncViews` call from any other viewer will + // pass the dangling native pointer to + // `RenderManager_setRenderableRenderThread`, where Filament does + // a `handle_cast` and aborts with + // "Postcondition: corrupted heap Handle ... tag=(no tag)" — the + // generic symptom of dereferencing a freed handle. + // + // Reproduces in multi-viewer Flutter apps when one viewer is + // disposed while another is concurrently attaching its view: the + // disposing viewer's `View_destroy` runs before the surviving + // viewer's `_syncViews`, the survivor walks `_attachments`, + // pushes the freed pointer to RenderManager, and crashes. + // Detach-before-destroy keeps `_attachments` consistent. + await FilamentApp.instance!.renderManager.detach(view); + await FilamentApp.instance!.destroyScene(scene); await FilamentApp.instance!.destroyView(view);