diff --git a/examples/Shared/src/ChangeMaterials.tsx b/examples/Shared/src/ChangeMaterials.tsx
index f3a715c1..22a09029 100644
--- a/examples/Shared/src/ChangeMaterials.tsx
+++ b/examples/Shared/src/ChangeMaterials.tsx
@@ -9,7 +9,11 @@ import RocketGlb from '@assets/rocket.glb'
const baseColorBlueImage = require('@assets/rocket_BaseColor_Blue.png')
function Renderer() {
- const [showBlue, setShowBlue] = React.useState(false)
+ // Regression test for a crash on scene teardown: after changing a texture map, navigating back
+ // from this screen must not abort with filament's "destroying MaterialInstance which is still
+ // in use by Renderable" precondition. Every press re-applies the texture map, which also
+ // exercises destroying the superseded material instance and texture of a slot.
+ const [applyCount, setApplyCount] = React.useState(0)
const blueBaseColorBuffer = useBuffer({ source: baseColorBlueImage })
const materialName = 'Toy Ship'
@@ -20,18 +24,18 @@ function Renderer() {
- {blueBaseColorBuffer != null && showBlue && (
- <>
+ {blueBaseColorBuffer != null && applyCount > 0 && (
+
- >
+
)}
{
- setShowBlue(true)
+ setApplyCount((count) => count + 1)
}}
/>
diff --git a/package/cpp/core/RNFRenderableManagerImpl.cpp b/package/cpp/core/RNFRenderableManagerImpl.cpp
index b86b4a3f..a493b77a 100644
--- a/package/cpp/core/RNFRenderableManagerImpl.cpp
+++ b/package/cpp/core/RNFRenderableManagerImpl.cpp
@@ -137,22 +137,66 @@ void RenderableManagerImpl::changeMaterialTextureMap(std::shared_ptr slotKey = {entityInstance.getId(), primitiveIndex};
+
+ // Remember the asset-owned instance that was attached before our first replacement on this slot.
+ // The deleter below restores it if the duplicate is destroyed while still attached (teardown),
+ // so the renderable never references a destroyed instance and the duplicate can be destroyed
+ // without hitting filament's "still in use by Renderable" precondition. The original is only
+ // touched when the entity still exists, which implies the asset (its owner) is still alive.
+ MaterialInstance* originalInstance;
+ auto originalIt = _slotOriginalInstances.find(slotKey);
+ if (originalIt == _slotOriginalInstances.end()) {
+ originalInstance = materialInstance;
+ _slotOriginalInstances[slotKey] = originalInstance;
+ } else {
+ originalInstance = originalIt->second;
+ }
+
// The texture might not be loaded yet, but we can already set it on the material instance
auto engine = _engine;
auto dispatcher = _rendererDispatcher;
+ Entity slotEntity = entityInstance;
+ size_t slotIndex = primitiveIndex;
std::shared_ptr newInstance =
- std::shared_ptr(MaterialInstance::duplicate(materialInstance), [engine, dispatcher](MaterialInstance* instance) {
- dispatcher->runAsync([engine, instance]() {
- Logger::log(TAG, "Destroying material instance %p", instance);
- engine->destroy(instance);
- });
- });
+ std::shared_ptr(MaterialInstance::duplicate(materialInstance),
+ [engine, dispatcher, slotEntity, slotIndex, originalInstance](MaterialInstance* instance) {
+ dispatcher->runAsync([engine, slotEntity, slotIndex, originalInstance, instance]() {
+ RenderableManager& renderableManager = engine->getRenderableManager();
+ if (renderableManager.hasComponent(slotEntity)) {
+ RenderableManager::Instance renderable = renderableManager.getInstance(slotEntity);
+ if (slotIndex < renderableManager.getPrimitiveCount(renderable) &&
+ renderableManager.getMaterialInstanceAt(renderable, slotIndex) == instance) {
+ // Still attached (teardown path) — detach by restoring the asset's original instance.
+ // Destroying an attached instance, or letting the material provider destroy the parent
+ // material while duplicates are alive, is a fatal precondition in filament.
+ Logger::log(TAG, "Restoring original material instance on entity before destroy");
+ renderableManager.setMaterialInstanceAt(renderable, slotIndex, originalInstance);
+ }
+ }
+ Logger::log(TAG, "Destroying material instance %p", instance);
+ engine->destroy(instance);
+ });
+ });
auto sampler = TextureSampler(TextureSampler::MinFilter::LINEAR, TextureSampler::MagFilter::LINEAR, TextureSampler::WrapMode::REPEAT);
Texture* texture = createTextureFromBuffer(textureBuffer, textureFlags);
newInstance->setParameter("baseColorMap", texture, sampler);
renderableManager.setMaterialInstanceAt(instance, primitiveIndex, newInstance.get());
- _materialInstances.push_back(newInstance);
+
+ // Replacing the slot entry releases the superseded instance — it is detached now (the renderable
+ // points at newInstance), so its deleter can safely destroy it.
+ _slotMaterialInstances[slotKey] = newInstance;
+ // The superseded texture is no longer sampled by any attached instance; destroy it.
+ auto previousTexture = _slotTextures.find(slotKey);
+ if (previousTexture != _slotTextures.end()) {
+ Texture* oldTexture = previousTexture->second;
+ dispatcher->runAsync([engine, oldTexture]() {
+ Logger::log(TAG, "Destroying texture %p", oldTexture);
+ engine->destroy(oldTexture);
+ });
+ }
+ _slotTextures[slotKey] = texture;
// Load the texture
startUpdateResourceLoading();
diff --git a/package/cpp/core/RNFRenderableManagerImpl.h b/package/cpp/core/RNFRenderableManagerImpl.h
index 5b968c7f..3a94c03e 100644
--- a/package/cpp/core/RNFRenderableManagerImpl.h
+++ b/package/cpp/core/RNFRenderableManagerImpl.h
@@ -15,6 +15,9 @@
#include
#include
+#include
+#include
+
namespace margelo {
using namespace filament;
using namespace gltfio;
@@ -91,8 +94,17 @@ class RenderableManagerImpl {
std::shared_ptr _engine;
std::shared_ptr _rendererDispatcher;
std::shared_ptr _textureProvider;
- // Keep a list of all material instances the RenderableManager creates, so we can clean them up when the RenderableManager is
- std::vector> _materialInstances;
+ // Material instances created by changeMaterialTextureMap, keyed by (entity id, primitive index).
+ // Only the latest instance per slot is attached to the renderable; replacing a slot entry
+ // releases the superseded (now detached) instance, whose deleter destroys it. At manager
+ // teardown the deleter detaches a still-attached instance first (restoring the slot's original
+ // asset-owned instance) — see changeMaterialTextureMap.
+ std::map, std::shared_ptr> _slotMaterialInstances;
+ // The asset-owned instance each slot had before our first replacement (restore target on teardown).
+ std::map, MaterialInstance*> _slotOriginalInstances;
+ // Textures created by changeMaterialTextureMap, keyed the same way. A superseded texture is no
+ // longer sampled by the replacement instance and is destroyed eagerly (previously these leaked).
+ std::map, Texture*> _slotTextures;
std::vector> _debugVerticesList;
private: