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
14 changes: 9 additions & 5 deletions examples/Shared/src/ChangeMaterials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -20,18 +24,18 @@ function Renderer() {
<DefaultLight />

<Model source={RocketGlb} translate={[0, -1, 0]}>
{blueBaseColorBuffer != null && showBlue && (
<>
{blueBaseColorBuffer != null && applyCount > 0 && (
<React.Fragment key={applyCount}>
<EntitySelector byName="Tip" textureMap={{ materialName, textureSource: blueBaseColorBuffer }} />
<EntitySelector byName="Wings" textureMap={{ materialName, textureSource: blueBaseColorBuffer }} />
</>
</React.Fragment>
)}
</Model>
</FilamentView>
<Button
title="Change Color"
onPress={() => {
setShowBlue(true)
setApplyCount((count) => count + 1)
}}
/>
</SafeAreaView>
Expand Down
58 changes: 51 additions & 7 deletions package/cpp/core/RNFRenderableManagerImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,66 @@ void RenderableManagerImpl::changeMaterialTextureMap(std::shared_ptr<EntityWrapp
// This material instance still belongs to the original asset and will be cleaned up when the asset is destroyed
MaterialInstance* materialInstance = renderableManager.getMaterialInstanceAt(instance, primitiveIndex);

std::pair<uint32_t, size_t> 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<MaterialInstance> newInstance =
std::shared_ptr<MaterialInstance>(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>(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();
Expand Down
16 changes: 14 additions & 2 deletions package/cpp/core/RNFRenderableManagerImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
#include <filament/RenderableManager.h>
#include <gltfio/TextureProvider.h>

#include <map>
#include <utility>

namespace margelo {
using namespace filament;
using namespace gltfio;
Expand Down Expand Up @@ -91,8 +94,17 @@ class RenderableManagerImpl {
std::shared_ptr<Engine> _engine;
std::shared_ptr<Dispatcher> _rendererDispatcher;
std::shared_ptr<TextureProvider> _textureProvider;
// Keep a list of all material instances the RenderableManager creates, so we can clean them up when the RenderableManager is
std::vector<std::shared_ptr<MaterialInstance>> _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::pair<uint32_t, size_t>, std::shared_ptr<MaterialInstance>> _slotMaterialInstances;
// The asset-owned instance each slot had before our first replacement (restore target on teardown).
std::map<std::pair<uint32_t, size_t>, 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<std::pair<uint32_t, size_t>, Texture*> _slotTextures;
std::vector<std::unique_ptr<DebugVertex[]>> _debugVerticesList;

private:
Expand Down