Skip to content

fix: crash on scene teardown after changeMaterialTextureMap (MaterialInstance destroyed while attached)#343

Open
codydorsettlynn wants to merge 1 commit into
margelo:mainfrom
codydorsettlynn:fix/change-material-texture-map-teardown-crash
Open

fix: crash on scene teardown after changeMaterialTextureMap (MaterialInstance destroyed while attached)#343
codydorsettlynn wants to merge 1 commit into
margelo:mainfrom
codydorsettlynn:fix/change-material-texture-map-teardown-crash

Conversation

@codydorsettlynn

Copy link
Copy Markdown

What

Fixes a fatal abort (SIGABRT) on scene teardown after RenderableManager.changeMaterialTextureMap() has been used, and fixes a permanent GPU memory leak of the Texture created per call (plausibly a contributor to #297). The teardown crash is the same class of teardown-order problem as #130.

Affects 1.11.0 (shared C++, so both Android and iOS). The bug class exists in earlier versions too, but the filament core bundled with 1.11.0 turned it from a silent leak into a fatal precondition abort.

The bug

changeMaterialTextureMap() duplicates the entity's current MaterialInstance, binds the duplicate via setMaterialInstanceAt, and accumulates every duplicate in _materialInstances, which is only released when RenderableManagerImpl is destroyed. The Texture* created per call is never destroyed at all.

At scene teardown the JS wrappers release in nondeterministic order (withCleanupScope defers releases through InteractionManager + setTimeout). When RenderableManagerWrapper releases before FilamentAssetWrapper, the duplicate instance is destroyed while still attached to the renderable, and filament aborts:

Filament: Precondition in destroy:1308
reason: destroying MaterialInstance "Screen" which is still in use by Renderable (entity=6, instance=2, index=0)
Fatal signal 6 (SIGABRT) in tid ... (FilamentRendere)

Simply skipping the destroy at teardown is not a fix — it trips filament's other precondition when gltfio's ubershader material provider destroys the parent Material during scene destruction (tested, verified in crash logs):

Filament: Precondition in terminate:220
reason: destroying material "base_lit_fade" but 2 instances still alive.

So the duplicates must genuinely be destroyed before destroyMaterials() runs, but never while attached.

Repro

  1. Render a <FilamentScene> + <FilamentView> + <ModelRenderer> with any GLB.
  2. Call renderableManager.changeMaterialTextureMap(entity, materialName, buffer, 'sRGB') at least once (e.g. via <EntitySelector textureMap={...}>).
  3. Unmount the scene (navigate away).
  4. App aborts on the FilamentRendere thread.

The 🎨 Change Materials example screen reproduces this: press "Change Color", then navigate back. It crashes before this fix and tears down cleanly after (logging Restoring original material instance... / Destroying material instance...).

The fix

Two parts, both in RNFRenderableManagerImpl.{h,cpp}:

  1. Detach-then-destroy deleter. The duplicate's shared_ptr deleter dispatches to the renderer thread and, if the instance is still attached to the renderable (teardown path), first restores the slot's original asset-owned MaterialInstance, then destroys the duplicate. This satisfies both preconditions and is safe in any teardown order:

    • Entity still alive → asset (owner of the original) is necessarily alive → restore original, destroy duplicate.
    • Asset already destroyed → hasComponent(entity) is false → destroy duplicate directly.
    • Runtime texture replacement → the renderable already points at the newer duplicate → destroy directly.

    All destruction work runs on the single renderer dispatcher (FIFO), so the destroys land before the scene deleter runs materialProvider->destroyMaterials().

  2. Per-slot tracking replaces the grow-forever vector. Keyed by (entity id, primitive index):

    • _slotMaterialInstances — holds only the currently-attached duplicate per slot; replacing the entry releases (→ destroys) the superseded one.
    • _slotOriginalInstances — the asset-owned instance each slot had before the first replacement (the restore target).
    • _slotTextures — textures created per slot; a superseded texture is destroyed eagerly (fixes the permanent leak).

Verification

  • Compiles clean for arm64-v8a, armeabi-v7a, and x86 via the module's externalNativeBuildDebug (NDK/CMake).
  • Crash reproduced and fix verified on an Android 16 arm64 emulator inside a production app (repeated texture applies + orientation changes, then unmount — teardown completes with the restore/destroy log lines and no filament precondition). This exact change has been shipping in that app via patch-package.
  • iOS untested, but the fix is in shared C++ compiled by both platforms.
  • The 🎨 Change Materials example screen was extended to serve as the regression repro: every press now re-applies the texture map (exercising the superseded-instance/texture destroy path), and navigating back exercises the teardown path.

🤖 Generated with Claude Code

changeMaterialTextureMap duplicated the entity's MaterialInstance and
accumulated every duplicate until the RenderableManager was destroyed.
Because JS wrappers release in nondeterministic order at scene teardown,
a duplicate could be destroyed while still attached to its renderable,
aborting with filament's "destroying MaterialInstance which is still in
use by Renderable" precondition. The Texture created per call was never
destroyed at all (permanent GPU memory leak).

Track instances and textures per (entity, primitive) slot instead. The
duplicate's deleter now detaches a still-attached instance first by
restoring the slot's original asset-owned instance, then destroys the
duplicate on the renderer thread — satisfying both filament teardown
preconditions in any release order. Superseded instances and textures
of a slot are destroyed eagerly when replaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codydorsettlynn

Copy link
Copy Markdown
Author

Claude Fable 5 was responsible for identifying and patching this in my project after some recurrent crashes were identified. This PR implements that patch. Take it or leave it, I just thought I would elevate it here.

@hannojg

hannojg commented Jul 14, 2026

Copy link
Copy Markdown
Member

thanks for sharing, will try to take a look soon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants