Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,29 @@ class FFIFilamentApp extends FilamentApp<Pointer> {

//
Future<Iterable<SwapChain>> getSwapChains() async {
return _swapChains;
// Return a snapshot, not the live list. Returning the live list lets
// mutations from concurrent createSwapChain / destroySwapChain calls
// leak back to callers — which is exactly what happened in
// thermion_flutter's Android `createTextureAndBindToView`:
//
// final swapChains = await FilamentApp.instance!.getSwapChains();
// final swapChain = await FilamentApp.instance!.createSwapChain(...);
// if (swapChains.isNotEmpty) {
// await FilamentApp.instance!.destroySwapChain(swapChains.first);
// }
//
// The intent was "snapshot existing chains, create the new one,
// tear down the old one", but `swapChains` aliased `_swapChains`,
// so after `createSwapChain` appended, `swapChains.first` was the
// *new* swap chain. The plugin then destroyed it and attached the
// view to a freed pointer — the next render hit Filament's
// "SwapChain must remain valid until endFrame is called" assert
// and the process aborted on every viewer mount on Android.
//
// Returning an unmodifiable snapshot fixes this at the API layer
// and makes the function safe regardless of how callers are
// structured.
return List<SwapChain>.unmodifiable(_swapChains);
}

final _hooks = <Future Function()>[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin {
// has been redirected to an internal RT (e.g., in composite highlight mode).
static final _viewRenderTargets = <View, RenderTarget>{};

// Track the SwapChain currently bound to each view (Android only).
// The Android branch of createTextureAndBindToView destroys this view's
// *previous* swap chain after creating the new one. Keying by view
// (rather than iterating FilamentApp's global swap-chain list) is what
// makes multi-viewer apps work — without this, mounting viewer #N
// would destroy viewer #(N-1)'s swap chain and freeze it.
static final _viewSwapChains = <View, SwapChain>{};

// Deferred Filament render target cleanup for Windows resize.
// Old RT stays alive so native can Blit from it during the swap window.
static final _deferredRenderTargets = <(RenderTarget, int)>[];
Expand Down Expand Up @@ -582,17 +590,27 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin {
// the viewport?
// In fact we can probably do this for all platforms
if (Platform.isAndroid) {
final swapChains = await FilamentApp.instance!.getSwapChains();
// The OLD swap chain to destroy is the one *previously bound to
// this view* — not whatever happens to be at the front of
// FilamentApp's global list. Earlier code iterated
// `getSwapChains()` and destroyed `swapChains.first`, which in a
// multi-viewer app destroyed *another* viewer's swap chain, freezing
// its viewport. Tracking per-view in `_viewSwapChains` scopes the
// destroy to this view only.
final oldSwapChain = _viewSwapChains[view];

final swapChain = await FilamentApp.instance!.createSwapChain(
Pointer<Void>.fromAddress(descriptor.windowHandle!),
);

if (swapChains.isNotEmpty) {
await FilamentApp.instance!.destroySwapChain(swapChains.first);
}

await FilamentApp.instance!.renderManager.attach(view, swapChain);
_viewSwapChains[view] = swapChain;

// Destroy the old swap chain after the new one is attached so the
// view never has a window of being unattached.
if (oldSwapChain != null) {
await FilamentApp.instance!.destroySwapChain(oldSwapChain);
}

// On other platforms, if a hardware texture ID is returned, this means
// the texture is immediately available for rendering.
Expand Down Expand Up @@ -732,4 +750,15 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin {

return texture;
}

@override
Future<void> releaseTextureBindingForView(View view) async {
// Only Android keeps per-view swap-chain bookkeeping in this plugin
// (see `_viewSwapChains` and the size-change branch of
// `createTextureAndBindToView`). Other platforms are no-ops.
final swapChain = _viewSwapChains.remove(view);
if (swapChain != null) {
await FilamentApp.instance!.destroySwapChain(swapChain);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ class ThermionFlutterPluginImpl extends ThermionFlutterPlugin {
return newTexture!;
}

@override
Future<void> releaseTextureBindingForView(View view) async {
// No per-view swap-chain bookkeeping on the web plugin. Surfaces
// are managed via the canvas + WebGL context lifecycle.
}

@override
Future<PlatformTextureDescriptor?> createTextureAndBindToView(
View view,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ abstract class ThermionFlutterPlugin {
int height,
);

/// Release plugin-side resources bound to this view (e.g. the per-view
/// `SwapChain` the Android plugin tracks). Call this BEFORE destroying
/// the underlying texture / surface descriptor — otherwise on Android
/// the SurfaceTexture is released first, the swap chain's native
/// window becomes invalid, and Filament's next `eglSwapBuffers` call
/// for that swap chain returns `EGL_BAD_SURFACE` until the widget
/// finishes unmounting.
///
/// Safe to call on platforms that don't track per-view bindings (e.g.
/// the web plugin) — it's a no-op there.
Future<void> releaseTextureBindingForView(View view);

static Future<ThermionViewer> createViewer(
{bool destroySwapchain = true}) async {
_logger.finest("Creating viewer");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,20 @@ class _ThermionWidgetInternalState extends State<ThermionWidgetInternal> {
_debounceTimer?.cancel();
var texture = _texture;
_texture = null;
texture?.destroy();
// Tear down per-view plugin bindings (Android: SwapChain bound to
// this view) BEFORE releasing the underlying texture. The
// SurfaceTexture release frees the swap chain's native window, so
// if the swap chain is still attached to the RenderManager when
// that happens Filament's next render fires `eglSwapBuffers` on a
// null buffer and the emulator/EGL stack logs `EGL_BAD_SURFACE`
// until the widget unmount finishes propagating. Sequencing the
// two awaits inside an unawaited closure keeps `dispose()` synchronous
// for the framework while still ordering the calls.
final view = widget.view;
() async {
await ThermionFlutterPlugin.instance.releaseTextureBindingForView(view);
await texture?.destroy();
}();
}

@override
Expand Down
Loading