viewer: detach view from RenderManager before destroying it on dispose#170
Open
mushogenshin wants to merge 1 commit into
Open
viewer: detach view from RenderManager before destroying it on dispose#170mushogenshin wants to merge 1 commit into
mushogenshin wants to merge 1 commit into
Conversation
`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.
This was referenced May 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
`ThermionViewerFFI.dispose()` destroys the underlying Filament `View` but doesn't first remove it from `FFIRenderManager`'s Dart-side `_attachments` map:
```dart
@OverRide
Future dispose() async {
...
View_setScene(view.getNativeHandle(), nullptr);
await FilamentApp.instance!.destroyScene(scene);
await FilamentApp.instance!.destroyView(view); // ← view destroyed without detaching
...
}
```
`_attachments` keys each swap chain 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)
```
Reproduction
Multi-viewer Flutter app where one viewer is disposed concurrently with another viewer being mounted:
Symptom on Android (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
Insert `await FilamentApp.instance!.renderManager.detach(view)` before `destroyView(view)` in `ThermionViewerFFI.dispose()`. `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.
Verification
Related