From 3f58c3b57d88f4826f4c7282cf09dc0c72b4c0e5 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Fri, 24 Apr 2026 17:07:44 -0700 Subject: [PATCH] Implement batch callback system for scene cache incremental updates. (#9935) * Implement batch callback system for scene cache incremental updates. This change adds a batch callback mechanism to track changes in component managers and the entity manager, avoiding full cache rebuilds in the future scene cache implementation. 1. SingleInstanceComponentManager: - Added SingleInstanceComponentManagerBase to handle callbacks without template bloat. - Implemented batch delivery using Slice and a fixed-size array of 16 for dirty entities to avoid heap allocations. - Added compile-time option for sorted insertion (default to linear). 2. Manager Integration: - RenderableManager: Added notifications to setters. - LightManager: Added notifications to setters - TransformManager: Handled transactions by deferring notifications until commit, and supported hierarchical updates in children. 3. EntityManager: - Added a new batching callback API for entity destruction. - Used std::function for callbacks to allow thread-safe copying of the callback list outside the lock. - Simple accumulation in loop and flush when full, no bypass. 4. Tests: - Added formal unit tests for all managers and entity manager callbacks. --- filament/include/filament/LightManager.h | 8 +- filament/src/components/LightManager.cpp | 105 ++++++++--- filament/src/components/LightManager.h | 10 + filament/src/components/RenderableManager.cpp | 59 +++--- filament/src/components/RenderableManager.h | 89 ++++++--- filament/src/components/TransformManager.cpp | 6 + filament/src/components/TransformManager.h | 10 + filament/test/filament_test.cpp | 124 +++++++++++++ libs/utils/CMakeLists.txt | 2 + libs/utils/include/utils/EntityManager.h | 27 +++ .../utils/SingleInstanceComponentManager.h | 109 ++++++++--- libs/utils/src/EntityManager.cpp | 12 ++ libs/utils/src/EntityManagerImpl.h | 83 ++++++++- .../src/SingleInstanceComponentManager.cpp | 74 ++++++++ libs/utils/test/test_Entity.cpp | 40 ++++ .../test_SingleInstanceComponentManager.cpp | 171 ++++++++++++++++++ 16 files changed, 821 insertions(+), 108 deletions(-) create mode 100644 libs/utils/src/SingleInstanceComponentManager.cpp create mode 100644 libs/utils/test/test_SingleInstanceComponentManager.cpp diff --git a/filament/include/filament/LightManager.h b/filament/include/filament/LightManager.h index a50d06693f..61eb61913a 100644 --- a/filament/include/filament/LightManager.h +++ b/filament/include/filament/LightManager.h @@ -693,7 +693,7 @@ public: * @param i Instance of the component obtained from getInstance(). * @return true is this light is a type of directional light */ - inline bool isDirectional(Instance i) const noexcept { + bool isDirectional(Instance const i) const noexcept { Type const type = getType(i); return type == Type::DIRECTIONAL || type == Type::SUN; } @@ -704,7 +704,7 @@ public: * @param i Instance of the component obtained from getInstance(). * @return true is this light is a type of point light */ - inline bool isPointLight(Instance i) const noexcept { + bool isPointLight(Instance const i) const noexcept { return getType(i) == Type::POINT; } @@ -714,7 +714,7 @@ public: * @param i Instance of the component obtained from getInstance(). * @return true is this light is a type of spot light */ - inline bool isSpotLight(Instance i) const noexcept { + bool isSpotLight(Instance const i) const noexcept { Type const type = getType(i); return type == Type::SPOT || type == Type::FOCUSED_SPOT; } @@ -811,7 +811,7 @@ public: * * @see Builder.intensity(float watts, float efficiency) */ - void setIntensity(Instance i, float watts, float efficiency) noexcept { + void setIntensity(Instance const i, float const watts, float const efficiency) noexcept { setIntensity(i, watts * 683.0f * efficiency); } diff --git a/filament/src/components/LightManager.cpp b/filament/src/components/LightManager.cpp index 6b3d29b634..f767739119 100644 --- a/filament/src/components/LightManager.cpp +++ b/filament/src/components/LightManager.cpp @@ -19,14 +19,12 @@ #include "components/LightManager.h" #include "details/Engine.h" -#include "utils/ostream.h" #include #include #include #include -#include #include #include @@ -255,6 +253,7 @@ void FLightManager::setShadowOptions(Instance const i, ShadowOptions const& opti params.options.shadowNearHint = std::max(options.shadowNearHint, 0.0f); params.options.shadowFarHint = std::max(options.shadowFarHint, 0.0f); params.options.vsm.blurWidth = std::max(0.0f, options.vsm.blurWidth); + mManager.notifyChange(getEntity(i)); } void FLightManager::setLightChannel(Instance const i, unsigned int const channel, bool const enable) noexcept { @@ -262,8 +261,12 @@ void FLightManager::setLightChannel(Instance const i, unsigned int const channel if (channel < 8) { auto& manager = mManager; const uint8_t mask = 1u << channel; - manager[i].channels &= ~mask; - manager[i].channels |= enable ? mask : 0u; + uint8_t const currentChannels = manager[i].channels; + uint8_t const newChannels = (currentChannels & ~mask) | (enable ? mask : 0u); + if (currentChannels != newChannels) { + manager[i].channels = newChannels; + manager.notifyChange(getEntity(i)); + } } } } @@ -282,21 +285,30 @@ bool FLightManager::getLightChannel(Instance const i, unsigned int const channel void FLightManager::setLocalPosition(Instance const i, const float3& position) noexcept { if (i) { auto& manager = mManager; - manager[i].position = position; + if (manager[i].position != position) { + manager[i].position = position; + manager.notifyChange(getEntity(i)); + } } } void FLightManager::setLocalDirection(Instance const i, float3 const direction) noexcept { if (i) { auto& manager = mManager; - manager[i].direction = direction; + if (manager[i].direction != direction) { + manager[i].direction = direction; + manager.notifyChange(getEntity(i)); + } } } void FLightManager::setColor(Instance const i, const LinearColor& color) noexcept { if (i) { auto& manager = mManager; - manager[i].color = color; + if (manager[i].color != color) { + manager[i].color = color; + manager.notifyChange(getEntity(i)); + } } } @@ -325,7 +337,7 @@ void FLightManager::setIntensity(Instance const i, float const intensity, Intens break; case Type::FOCUSED_SPOT: { - SpotParams& spotParams = manager[i].spotParams; + SpotParams const& spotParams = manager[i].spotParams; float const cosOuter = std::sqrt(spotParams.cosOuterSquared); if (unit == IntensityUnit::LUMEN_LUX) { // li = lp / (2 * pi * (1 - cos(cone_outer / 2))) @@ -337,7 +349,6 @@ void FLightManager::setIntensity(Instance const i, float const intensity, Intens // lp = li * (2 * pi * (1 - cos(cone_outer / 2))) luminousPower = luminousIntensity * (f::TAU * (1.0f - cosOuter)); } - spotParams.luminousPower = luminousPower; break; } case Type::SPOT: @@ -351,7 +362,22 @@ void FLightManager::setIntensity(Instance const i, float const intensity, Intens } break; } - manager[i].intensity = luminousIntensity; + + bool changed = false; + if (manager[i].intensity != luminousIntensity) { + manager[i].intensity = luminousIntensity; + changed = true; + } + if (type == Type::FOCUSED_SPOT) { + SpotParams& spotParams = manager[i].spotParams; + if (spotParams.luminousPower != luminousPower) { + spotParams.luminousPower = luminousPower; + changed = true; + } + } + if (changed) { + manager.notifyChange(getEntity(i)); + } } } @@ -359,9 +385,14 @@ void FLightManager::setFalloff(Instance const i, float const falloff) noexcept { auto& manager = mManager; if (i && !isDirectionalLight(i)) { float const sqFalloff = falloff * falloff; + float const squaredFallOffInv = sqFalloff > 0.0f ? (1 / sqFalloff) : 0; SpotParams& spotParams = manager[i].spotParams; - manager[i].squaredFallOffInv = sqFalloff > 0.0f ? (1 / sqFalloff) : 0; - spotParams.radius = falloff; + + if (manager[i].squaredFallOffInv != squaredFallOffInv || spotParams.radius != falloff) { + manager[i].squaredFallOffInv = squaredFallOffInv; + spotParams.radius = falloff; + manager.notifyChange(getEntity(i)); + } } } @@ -380,20 +411,27 @@ void FLightManager::setSpotLightCone(Instance const i, float const inner, float float const cosOuterSquared = cosOuter * cosOuter; float const scale = 1.0f / std::max(1.0f / 1024.0f, cosInner - cosOuter); float const offset = -cosOuter * scale; + float2 const scaleOffset = { scale, offset }; SpotParams& spotParams = manager[i].spotParams; - spotParams.outerClamped = outerClamped; - spotParams.cosOuterSquared = cosOuterSquared; - spotParams.sinInverse = 1.0f / std::sin(outerClamped); - spotParams.scaleOffset = { scale, offset }; + if (spotParams.outerClamped != outerClamped || + spotParams.cosOuterSquared != cosOuterSquared || + spotParams.scaleOffset != scaleOffset) { + + spotParams.outerClamped = outerClamped; + spotParams.cosOuterSquared = cosOuterSquared; + spotParams.sinInverse = 1.0f / std::sin(outerClamped); + spotParams.scaleOffset = { scale, offset }; - // we need to recompute the luminous intensity - Type const type = getLightType(i).type; - if (type == Type::FOCUSED_SPOT) { - // li = lp / (2 * pi * (1 - cos(cone_outer / 2))) - float const luminousPower = spotParams.luminousPower; - float const luminousIntensity = luminousPower / (f::TAU * (1.0f - cosOuter)); - manager[i].intensity = luminousIntensity; + // we need to recompute the luminous intensity + Type const type = getLightType(i).type; + if (type == Type::FOCUSED_SPOT) { + // li = lp / (2 * pi * (1 - cos(cone_outer / 2))) + float const luminousPower = spotParams.luminousPower; + float const luminousIntensity = luminousPower / (f::TAU * (1.0f - cosOuter)); + manager[i].intensity = luminousIntensity; + } + manager.notifyChange(getEntity(i)); } } } @@ -401,26 +439,39 @@ void FLightManager::setSpotLightCone(Instance const i, float const inner, float void FLightManager::setSunAngularRadius(Instance const i, float angularRadius) noexcept { if (i && isSunLight(i)) { angularRadius = clamp(angularRadius, 0.25f, 20.0f); - mManager[i].sunAngularRadius = angularRadius * f::DEG_TO_RAD; + float const newRadius = angularRadius * f::DEG_TO_RAD; + if (mManager[i].sunAngularRadius != newRadius) { + mManager[i].sunAngularRadius = newRadius; + mManager.notifyChange(getEntity(i)); + } } } void FLightManager::setSunHaloSize(Instance const i, float const haloSize) noexcept { if (i && isSunLight(i)) { - mManager[i].sunHaloSize = haloSize; + if (mManager[i].sunHaloSize != haloSize) { + mManager[i].sunHaloSize = haloSize; + mManager.notifyChange(getEntity(i)); + } } } void FLightManager::setSunHaloFalloff(Instance const i, float const haloFalloff) noexcept { if (i && isSunLight(i)) { - mManager[i].sunHaloFalloff = haloFalloff; + if (mManager[i].sunHaloFalloff != haloFalloff) { + mManager[i].sunHaloFalloff = haloFalloff; + mManager.notifyChange(getEntity(i)); + } } } void FLightManager::setShadowCaster(Instance const i, bool const shadowCaster) noexcept { if (i) { LightType& lightType = mManager[i].lightType; - lightType.shadowCaster = shadowCaster; + if (lightType.shadowCaster != shadowCaster) { + lightType.shadowCaster = shadowCaster; + mManager.notifyChange(getEntity(i)); + } } } diff --git a/filament/src/components/LightManager.h b/filament/src/components/LightManager.h index 19826e54ac..4a214373a3 100644 --- a/filament/src/components/LightManager.h +++ b/filament/src/components/LightManager.h @@ -80,6 +80,16 @@ public: void prepare(backend::DriverApi& driver) const noexcept; + void registerChangeCallback(void const* token, utils::SingleInstanceComponentManagerBase::ChangeCallback callback) noexcept { + mManager.registerChangeCallback(token, std::move(callback)); + } + void unregisterChangeCallback(void const* token) noexcept { + mManager.unregisterChangeCallback(token); + } + void flushNotifications() noexcept { + mManager.flushNotifications(); + } + struct LightType { Type type : 3; bool shadowCaster : 1; diff --git a/filament/src/components/RenderableManager.cpp b/filament/src/components/RenderableManager.cpp index 8f5b2f3634..64b9d47e58 100644 --- a/filament/src/components/RenderableManager.cpp +++ b/filament/src/components/RenderableManager.cpp @@ -342,15 +342,15 @@ void RenderableManager::BuilderDetails::processBoneIndicesAndWights(Engine& engi size_t maxPairsCount = 0; //size of texture, number of bone pairs size_t maxPairsCountPerVertex = 0; //maximum of number of bone per vertex - for (auto& bonePair: mBonePairs) { - auto primitiveIndex = bonePair.first; + for (auto const& bonePair: mBonePairs) { + auto const primitiveIndex = bonePair.first; auto entries = mEntries; FILAMENT_CHECK_PRECONDITION(primitiveIndex < entries.size() && primitiveIndex >= 0) << "[primitive @ " << primitiveIndex << "] primitiveindex is out of size (" << entries.size() << ")"; - auto entry = mEntries[primitiveIndex]; + auto const entry = mEntries[primitiveIndex]; auto bonePairsForPrimitive = bonePair.second; - auto vertexCount = entry.vertices->getVertexCount(); + auto const vertexCount = entry.vertices->getVertexCount(); FILAMENT_CHECK_PRECONDITION(bonePairsForPrimitive.size() == vertexCount) << "[primitive @ " << primitiveIndex << "] bone indices and weights pairs count (" << bonePairsForPrimitive.size() << ") must be equal to vertex count (" @@ -373,7 +373,7 @@ void RenderableManager::BuilderDetails::processBoneIndicesAndWights(Engine& engi mBoneIndicesAndWeights = utils::FixedCapacityVector(maxPairsCount); // temporary indices and weights for one vertex auto const tempPairs = std::make_unique(maxPairsCountPerVertex); - for (auto& bonePair: mBonePairs) { + for (auto const& bonePair: mBonePairs) { auto primitiveIndex = bonePair.first; auto bonePairsForPrimitive = bonePair.second; if (bonePairsForPrimitive.empty()) { @@ -388,8 +388,8 @@ void RenderableManager::BuilderDetails::processBoneIndicesAndWights(Engine& engi size_t tempPairCount = 0; double boneWeightsSum = 0; for (size_t k = 0; k < bonePairsForPrimitive[iVertex].size(); k++) { - auto boneWeight = bonePairsForPrimitive[iVertex][k][1]; - auto boneIndex = bonePairsForPrimitive[iVertex][k][0]; + auto const boneWeight = bonePairsForPrimitive[iVertex][k][1]; + auto const boneIndex = bonePairsForPrimitive[iVertex][k][0]; FILAMENT_CHECK_PRECONDITION(boneWeight >= 0) << "[entity=" << entity.getId() << ", primitive @ " << primitiveIndex << "] bone weight (" << boneWeight << ") of vertex=" << iVertex @@ -432,7 +432,7 @@ void RenderableManager::BuilderDetails::processBoneIndicesAndWights(Engine& engi #endif // prepare data for vertex attributes - auto offset = iVertex * 4; + auto const offset = iVertex * 4; // set attributes, indices and weights, for <= 4 pairs for (size_t j = 0, c = std::min((int)tempPairCount, 4); j < c; j++) { skinJoints[j + offset] = uint16_t(tempPairs[j][0]); @@ -525,7 +525,7 @@ RenderableManager::Builder::Result RenderableManager::Builder::build(Engine& eng } // we want a feature level violation to be a hard error (exception if enabled, or crash) - int activeFeatureLevel = static_cast(engine.getActiveFeatureLevel()); + int const activeFeatureLevel = static_cast(engine.getActiveFeatureLevel()); FILAMENT_CHECK_PRECONDITION(downcast(engine).hasFeatureLevel(material->getFeatureLevel())) << "Material \"" << material->getName().c_str_safe() << "\" has feature level " << static_cast(material->getFeatureLevel()) @@ -715,7 +715,7 @@ void FRenderableManager::create( BufferUsage::DYNAMIC), .count = targetCount }; - Slice primitives = mManager[ci].primitives; + Slice const primitives = mManager[ci].primitives; mManager[ci].morphTargetBuffer = morphTargetBuffer; if (builder->mMorphTargetBuffer) { for (size_t i = 0; i < entryCount; ++i) { @@ -728,7 +728,7 @@ void FRenderableManager::create( // morphWeights uniform array to avoid crash on adreno gpu. if (UTILS_UNLIKELY(targetCount == 0 && driver.isWorkaroundNeeded(Workaround::ADRENO_UNIFORM_ARRAY_CRASH))) { - float initWeights[1] = { 0 }; + float const initWeights[1] = { 0 }; setMorphWeights(ci, initWeights, 1, 0); } } @@ -745,7 +745,7 @@ void FRenderableManager::destroy(Entity const e, DriverApi& driver) noexcept { } } -void FRenderableManager::clientDestroy(utils::Entity e) noexcept { +void FRenderableManager::clientDestroy(Entity e) noexcept { destroy(e, mEngine.getDriverApi()); } @@ -811,7 +811,7 @@ void FRenderableManager::setMaterialInstanceAt(Instance const instance, uint8_t size_t const primitiveIndex, FMaterialInstance const* mi) { assert_invariant(mi); if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { FMaterial const* material = mi->getMaterial(); @@ -839,7 +839,7 @@ void FRenderableManager::setMaterialInstanceAt(Instance const instance, uint8_t void FRenderableManager::clearMaterialInstanceAt(Instance instance, uint8_t level, size_t primitiveIndex) { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { primitives[primitiveIndex].setMaterialInstance(nullptr); } @@ -849,7 +849,7 @@ void FRenderableManager::clearMaterialInstanceAt(Instance instance, uint8_t leve MaterialInstance* FRenderableManager::getMaterialInstanceAt( Instance const instance, uint8_t const level, size_t const primitiveIndex) const noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { // We store the material instance as const because we don't want to change it internally // but when the user queries it, we want to allow them to call setParameter() @@ -862,7 +862,7 @@ MaterialInstance* FRenderableManager::getMaterialInstanceAt( void FRenderableManager::setBlendOrderAt(Instance const instance, uint8_t const level, size_t const primitiveIndex, uint16_t const order) noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { primitives[primitiveIndex].setBlendOrder(order); } @@ -872,7 +872,7 @@ void FRenderableManager::setBlendOrderAt(Instance const instance, uint8_t const uint16_t FRenderableManager::getBlendOrderAt(Instance const instance, uint8_t const level, size_t const primitiveIndex) const noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { return primitives[primitiveIndex].getBlendOrder(); } @@ -883,7 +883,7 @@ uint16_t FRenderableManager::getBlendOrderAt(Instance const instance, uint8_t co void FRenderableManager::setGlobalBlendOrderEnabledAt(Instance const instance, uint8_t const level, size_t const primitiveIndex, bool const enabled) noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { primitives[primitiveIndex].setGlobalBlendOrderEnabled(enabled); } @@ -893,7 +893,7 @@ void FRenderableManager::setGlobalBlendOrderEnabledAt(Instance const instance, u bool FRenderableManager::isGlobalBlendOrderEnabledAt(Instance const instance, uint8_t const level, size_t const primitiveIndex) const noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { return primitives[primitiveIndex].isGlobalBlendOrderEnabled(); } @@ -904,7 +904,7 @@ bool FRenderableManager::isGlobalBlendOrderEnabledAt(Instance const instance, ui AttributeBitset FRenderableManager::getEnabledAttributesAt( Instance const instance, uint8_t const level, size_t const primitiveIndex) const noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { return primitives[primitiveIndex].getEnabledAttributes(); } @@ -916,7 +916,7 @@ void FRenderableManager::setGeometryAt(Instance const instance, uint8_t const le PrimitiveType const type, FVertexBuffer* vertices, FIndexBuffer* indices, size_t const offset, size_t const count) noexcept { if (instance) { - Slice primitives = getRenderPrimitives(instance, level); + Slice const primitives = getRenderPrimitives(instance, level); if (primitiveIndex < primitives.size()) { primitives[primitiveIndex].set(mHwRenderPrimitiveFactory, mEngine.getDriverApi(), type, vertices, indices, offset, count); @@ -924,6 +924,13 @@ void FRenderableManager::setGeometryAt(Instance const instance, uint8_t const le } } +void FRenderableManager::setPrimitives(Instance const instance, Slice primitives) noexcept { + if (instance) { + mManager[instance].primitives = primitives; + mManager.notifyChange(getEntity(instance)); + } +} + void FRenderableManager::setBones(Instance const ci, Bone const* UTILS_RESTRICT transforms, size_t boneCount, size_t const offset) { if (ci) { @@ -1021,7 +1028,7 @@ void FRenderableManager::setMorphTargetBufferOffsetAt(Instance const instance, u size_t const offset) { if (instance) { assert_invariant(mManager[instance].morphTargetBuffer); - Slice primitives = mManager[instance].primitives; + Slice const primitives = mManager[instance].primitives; if (primitiveIndex < primitives.size()) { primitives[primitiveIndex].setMorphingBufferOffset(offset); } @@ -1047,8 +1054,12 @@ void FRenderableManager::setLightChannel(Instance const ci, unsigned int const c if (ci) { if (channel < 8) { const uint8_t mask = 1u << channel; - mManager[ci].lightChannels &= ~mask; - mManager[ci].lightChannels |= enable ? mask : 0u; + uint8_t const currentChannels = mManager[ci].lightChannels; + uint8_t const newChannels = (currentChannels & ~mask) | (enable ? mask : 0u); + if (currentChannels != newChannels) { + mManager[ci].lightChannels = newChannels; + mManager.notifyChange(getEntity(ci)); + } } } } diff --git a/filament/src/components/RenderableManager.h b/filament/src/components/RenderableManager.h index 3517b6a6a9..a985dea270 100644 --- a/filament/src/components/RenderableManager.h +++ b/filament/src/components/RenderableManager.h @@ -150,10 +150,20 @@ public: inline void setFogEnabled(Instance instance, bool enable) noexcept; inline bool getFogEnabled(Instance instance) const noexcept; - inline void setPrimitives(Instance instance, + void setPrimitives(Instance instance, utils::Slice primitives) noexcept; inline void setSkinning(Instance instance, bool enable); + + void registerChangeCallback(void const* token, utils::SingleInstanceComponentManagerBase::ChangeCallback callback) noexcept { + mManager.registerChangeCallback(token, std::move(callback)); + } + void unregisterChangeCallback(void const* token) noexcept { + mManager.unregisterChangeCallback(token); + } + void flushNotifications() noexcept { + mManager.flushNotifications(); + } void setBones(Instance instance, Bone const* transforms, size_t boneCount, size_t offset = 0); void setBones(Instance instance, math::mat4f const* transforms, size_t boneCount, size_t offset = 0); void setSkinningBuffer(Instance instance, FSkinningBuffer* skinningBuffer, @@ -335,10 +345,13 @@ FILAMENT_DOWNCAST(RenderableManager) void FRenderableManager::setAxisAlignedBoundingBox(Instance const instance, const Box& aabb) { if (instance) { FILAMENT_CHECK_PRECONDITION( - static_cast(mManager[instance].visibility).geometryType == - GeometryType::DYNAMIC) + static_cast(mManager[instance].visibility).geometryType == GeometryType::DYNAMIC) << "This renderable has staticBounds enabled; its AABB cannot change."; - mManager[instance].aabb = aabb; + Box& state = mManager[instance].aabb; + if (state.center != aabb.center || state.halfExtent != aabb.halfExtent) { + state = aabb; + mManager.notifyChange(getEntity(instance)); + } } } @@ -346,62 +359,92 @@ void FRenderableManager::setLayerMask(Instance const instance, uint8_t const select, uint8_t const values) noexcept { if (instance) { uint8_t& layers = mManager[instance].layers; - layers = (layers & ~select) | (values & select); + uint8_t const newLayers = (layers & ~select) | (values & select); + if (layers != newLayers) { + layers = newLayers; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setLayerMask(Instance const instance, uint8_t const layerMask) noexcept { if (instance) { - mManager[instance].layers = layerMask; + if (mManager[instance].layers != layerMask) { + mManager[instance].layers = layerMask; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setPriority(Instance const instance, uint8_t const priority) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.priority = std::min(priority, uint8_t(0x7)); + uint8_t const newPriority = std::min(priority, uint8_t(0x7)); + if (visibility.priority != newPriority) { + visibility.priority = newPriority; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setChannel(Instance const instance, uint8_t const channel) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.channel = std::min(channel, uint8_t(CONFIG_RENDERPASS_CHANNEL_COUNT - 1)); + uint8_t const newChannel = std::min(channel, uint8_t(CONFIG_RENDERPASS_CHANNEL_COUNT - 1)); + if (visibility.channel != newChannel) { + visibility.channel = newChannel; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setCastShadows(Instance const instance, bool const enable) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.castShadows = enable; + if (visibility.castShadows != enable) { + visibility.castShadows = enable; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setReceiveShadows(Instance const instance, bool const enable) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.receiveShadows = enable; + if (visibility.receiveShadows != enable) { + visibility.receiveShadows = enable; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setScreenSpaceContactShadows(Instance const instance, bool const enable) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.screenSpaceContactShadows = enable; + if (visibility.screenSpaceContactShadows != enable) { + visibility.screenSpaceContactShadows = enable; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setCulling(Instance const instance, bool const enable) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.culling = enable; + if (visibility.culling != enable) { + visibility.culling = enable; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setFogEnabled(Instance const instance, bool const enable) noexcept { if (instance) { Visibility& visibility = mManager[instance].visibility; - visibility.fog = enable; + if (visibility.fog != enable) { + visibility.fog = enable; + mManager.notifyChange(getEntity(instance)); + } } } @@ -411,35 +454,35 @@ bool FRenderableManager::getFogEnabled(RenderableManager::Instance const instanc void FRenderableManager::setSkinning(Instance const instance, bool const enable) { if (instance) { - Visibility& visibility = mManager[instance].visibility; + Visibility const& visibility = mManager[instance].visibility; FILAMENT_CHECK_PRECONDITION(visibility.geometryType != GeometryType::STATIC || !enable) << "Skinning can't be used with STATIC geometry"; Skinning& skinning = mManager[instance].skinning; - skinning.skinning = enable; + if (skinning.skinning != enable) { + skinning.skinning = enable; + mManager.notifyChange(getEntity(instance)); + } } } void FRenderableManager::setMorphing(Instance const instance, Builder::MorphType const type) { if (instance) { - Visibility& visibility = mManager[instance].visibility; + Visibility const& visibility = mManager[instance].visibility; FILAMENT_CHECK_PRECONDITION( visibility.geometryType != GeometryType::STATIC || type != MorphType::NONE) << "Morphing can't be used with STATIC geometry"; Skinning& skinning = mManager[instance].skinning; - skinning.morphType = type; + if (skinning.morphType != type) { + skinning.morphType = type; + mManager.notifyChange(getEntity(instance)); + } } } -void FRenderableManager::setPrimitives(Instance const instance, - utils::Slice primitives) noexcept { - if (instance) { - mManager[instance].primitives = primitives; - } -} FRenderableManager::Visibility FRenderableManager::getVisibility(Instance const instance) const noexcept { diff --git a/filament/src/components/TransformManager.cpp b/filament/src/components/TransformManager.cpp index 6a90c19599..f0fda2fbdd 100644 --- a/filament/src/components/TransformManager.cpp +++ b/filament/src/components/TransformManager.cpp @@ -207,6 +207,8 @@ void FTransformManager::updateNodeTransform(Instance const i) noexcept { manager[parent].worldTranslationLo, manager[i].localTranslationLo, mAccurateTranslations); + manager.notifyChange(getEntity(i)); + // update our children's world transforms Instance const child = manager[i].firstChild; if (UTILS_UNLIKELY(child)) { // assume we don't have a hierarchy in the common case @@ -246,6 +248,8 @@ void FTransformManager::computeAllWorldTransforms() noexcept { manager[parent].world, manager[i].local, manager[parent].worldTranslationLo, manager[i].localTranslationLo, accurate); + + manager.notifyChange(getEntity(i)); } } @@ -382,6 +386,8 @@ void FTransformManager::transformChildren(Sim& manager, Instance i) noexcept { manager[parent].worldTranslationLo, manager[i].localTranslationLo, accurate); + manager.notifyChange(getEntity(i)); + // assume we don't have a deep hierarchy Instance const child = manager[i].firstChild; if (UTILS_UNLIKELY(child)) { diff --git a/filament/src/components/TransformManager.h b/filament/src/components/TransformManager.h index b985d471f8..9894a3b172 100644 --- a/filament/src/components/TransformManager.h +++ b/filament/src/components/TransformManager.h @@ -101,6 +101,16 @@ public: void gc(utils::EntityManager& em) noexcept; + void registerChangeCallback(void const* token, utils::SingleInstanceComponentManagerBase::ChangeCallback callback) noexcept { + mManager.registerChangeCallback(token, std::move(callback)); + } + void unregisterChangeCallback(void const* token) noexcept { + mManager.unregisterChangeCallback(token); + } + void flushNotifications() noexcept { + mManager.flushNotifications(); + } + utils::Slice getWorldTransforms() const noexcept { return mManager.slice(); } diff --git a/filament/test/filament_test.cpp b/filament/test/filament_test.cpp index 3374adbdfd..3a2e84a752 100644 --- a/filament/test/filament_test.cpp +++ b/filament/test/filament_test.cpp @@ -36,6 +36,9 @@ #include #include +#include +#include + #include "Allocators.h" #include "details/Material.h" #include "details/Camera.h" @@ -301,6 +304,127 @@ TEST(FilamentTest, TransformManager) { EXPECT_EQ(c, tcm.getChildCount(newParent)); } +TEST(FilamentTest, RenderableManagerCallback) { + FEngine* engine = downcast(Engine::create(Engine::Backend::NOOP)); + auto& rm = engine->getRenderableManager(); + EntityManager& em = EntityManager::get(); + Entity e = em.create(); + + int callbackCount = 0; + auto callback = [&](Slice entities) { + callbackCount += entities.size(); + }; + + rm.registerChangeCallback(&rm, std::move(callback)); + + // Test addComponent + RenderableManager::Builder(1).boundingBox({{0,0,0},{1,1,1}}).build(*engine, e); + rm.flushNotifications(); + EXPECT_GT(callbackCount, 0); + + callbackCount = 0; + + // Test modify + auto instance = rm.getInstance(e); + rm.setLayerMask(instance, 0x2); + rm.flushNotifications(); + EXPECT_EQ(callbackCount, 1); + + callbackCount = 0; + + // Test removeComponent + rm.destroy(e, engine->getDriverApi()); + rm.flushNotifications(); + EXPECT_EQ(callbackCount, 1); + + rm.unregisterChangeCallback(&rm); + em.destroy(e); + Engine::destroy((Engine**)&engine); +} + +TEST(FilamentTest, LightManagerCallback) { + FEngine* engine = downcast(Engine::create(Engine::Backend::NOOP)); + auto& lm = engine->getLightManager(); + EntityManager& em = EntityManager::get(); + Entity e = em.create(); + + int callbackCount = 0; + auto callback = [&](Slice entities) { + callbackCount += entities.size(); + }; + + lm.registerChangeCallback(&lm, std::move(callback)); + + // Test addComponent + LightManager::Builder(LightManager::Type::POINT).build(*engine, e); + lm.flushNotifications(); + EXPECT_GT(callbackCount, 0); + + callbackCount = 0; + + // Test modify + auto instance = lm.getInstance(e); + lm.setIntensity(instance, 2000.0f, FLightManager::IntensityUnit::LUMEN_LUX); + lm.flushNotifications(); + EXPECT_EQ(callbackCount, 1); + + callbackCount = 0; + + // Test removeComponent + lm.destroy(e); + lm.flushNotifications(); + EXPECT_EQ(callbackCount, 1); + + lm.unregisterChangeCallback(&lm); + em.destroy(e); + Engine::destroy((Engine**)&engine); +} + +TEST(FilamentTest, TransformManagerCallback) { + FEngine* engine = downcast(Engine::create(Engine::Backend::NOOP)); + auto& tcm = engine->getTransformManager(); + EntityManager& em = EntityManager::get(); + Entity e = em.create(); + + int callbackCount = 0; + auto callback = [&](Slice entities) { + callbackCount += entities.size(); + }; + + tcm.registerChangeCallback(&tcm, std::move(callback)); + + // Test addComponent + tcm.create(e); + tcm.flushNotifications(); + EXPECT_GT(callbackCount, 0); + + callbackCount = 0; + + // Test modify without transaction + auto instance = tcm.getInstance(e); + auto t = mat4f::translation(float3{ 1, 2, 3 }); + tcm.setTransform(instance, t); + tcm.flushNotifications(); + EXPECT_EQ(callbackCount, 1); + + callbackCount = 0; + + // Test transaction + tcm.openLocalTransformTransaction(); + tcm.setTransform(instance, mat4f::translation(float3{ 4, 5, 6 })); + tcm.flushNotifications(); + EXPECT_EQ(callbackCount, 0); + + tcm.commitLocalTransformTransaction(); + tcm.flushNotifications(); + EXPECT_EQ(callbackCount, 1); + + tcm.unregisterChangeCallback(&tcm); + tcm.destroy(e); + em.destroy(e); + Engine::destroy((Engine**)&engine); +} + TEST(FilamentTest, UniformInterfaceBlock) { BufferInterfaceBlock::Builder b; diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index f09efcbad2..b9ccf0cfa5 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -87,6 +87,7 @@ set(SRCS src/Panic.cpp src/Path.cpp src/Profiler.cpp + src/SingleInstanceComponentManager.cpp src/sstream.cpp src/string.cpp src/ThreadUtils.cpp @@ -196,6 +197,7 @@ if (FILAMENT_BUILD_TESTING) test/test_QuadTreeArray.cpp test/test_RangeMap.cpp test/test_RefCountedMap.cpp + test/test_SingleInstanceComponentManager.cpp test/test_Slice.cpp test/test_StructureOfArrays.cpp test/test_sstream.cpp diff --git a/libs/utils/include/utils/EntityManager.h b/libs/utils/include/utils/EntityManager.h index ccefa06bbb..819d5c138f 100644 --- a/libs/utils/include/utils/EntityManager.h +++ b/libs/utils/include/utils/EntityManager.h @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -33,6 +34,8 @@ #include #endif +#include + namespace utils { class UTILS_PUBLIC EntityManager { @@ -48,6 +51,30 @@ public: virtual ~Listener() noexcept; }; + using ChangeCallback = std::function)>; + + /** + * Registers a callback to be triggered when entities are destroyed. + * The callback receives a batch of destroyed entities. + * Thread safe. + * @param token A unique identifier for the listener (e.g., 'this' pointer). + * @param callback The callback to invoke. + */ + void registerChangeCallback(void const* token, ChangeCallback callback) noexcept; + + /** + * Unregisters a callback. + * Thread safe. + * @param token The token used during registration. + */ + void unregisterChangeCallback(void const* token) noexcept; + + /** + * Flushes any pending notifications to listeners. + * Thread safe. + */ + void flushNotifications() noexcept; + // maximum number of entities that can exist at the same time static size_t getMaxEntityCount() noexcept { // because index 0 is reserved, we only have 2^GENERATION_SHIFT - 1 valid indices diff --git a/libs/utils/include/utils/SingleInstanceComponentManager.h b/libs/utils/include/utils/SingleInstanceComponentManager.h index fc820ca2a2..1e981c0fa4 100644 --- a/libs/utils/include/utils/SingleInstanceComponentManager.h +++ b/libs/utils/include/utils/SingleInstanceComponentManager.h @@ -21,10 +21,15 @@ #include #include #include +#include +#include #include #include +#include +#include + #include #include #include @@ -33,6 +38,66 @@ namespace utils { class EntityManager; +/** + * Base class for SingleInstanceComponentManager to handle callbacks without template bloat. + */ +class UTILS_PUBLIC SingleInstanceComponentManagerBase { +public: + using ChangeCallback = Invocable)>; + + static constexpr bool USE_SORTED_DIRTY_ARRAY = false; + + /** + * Registers a callback to be triggered when components are added, removed, or modified. + * @param token A unique identifier for the listener (e.g., 'this' pointer). + * @param callback The callback to invoke. + * @note Registering the same token multiple times will result in multiple + * registrations and the callback being invoked multiple times. + */ + void registerChangeCallback(void const* token, ChangeCallback callback) noexcept; + + /** + * Unregisters a callback. + * @param token The token used during registration. + * @note If the token is not registered, this operation is a safe no-op. + * If the token was registered multiple times, all instances will be removed. + */ + void unregisterChangeCallback(void const* token) noexcept; + + /** + * Flushes any pending notifications to listeners. + * This is called automatically when the internal buffer is full, + * but can also be called manually (e.g., at the end of a frame). + */ + void flushNotifications() noexcept; + + /** + * Records a change for the given entity. + * Flushes notifications if the internal buffer becomes full. + */ + void notifyChange(Entity e) noexcept; + +protected: + SingleInstanceComponentManagerBase() noexcept = default; + ~SingleInstanceComponentManagerBase() noexcept = default; + + SingleInstanceComponentManagerBase(const SingleInstanceComponentManagerBase&) = delete; + SingleInstanceComponentManagerBase& operator=(const SingleInstanceComponentManagerBase&) = delete; + SingleInstanceComponentManagerBase(SingleInstanceComponentManagerBase&&) noexcept = default; + SingleInstanceComponentManagerBase& operator=(SingleInstanceComponentManagerBase&&) noexcept = default; + +private: + struct CallbackInfo { + void const* token; + ChangeCallback callback; + }; + + static constexpr size_t MAX_DIRTY_COUNT = 16; + Entity mDirtyEntities[MAX_DIRTY_COUNT]; + size_t mDirtyCount = 0; + std::vector mChangeCallbacks; +}; + /* * Helper class to create single instance component managers. * @@ -45,14 +110,12 @@ class EntityManager; * */ template -class UTILS_PUBLIC SingleInstanceComponentManager { -private: - +class UTILS_PUBLIC SingleInstanceComponentManager : public SingleInstanceComponentManagerBase { // this is just to avoid using std::default_random_engine, since we're in a public header. class default_random_engine { uint32_t mState = 1u; // must be 0 < seed < 0x7fffffff public: - inline uint32_t operator()() noexcept { + uint32_t operator()() noexcept { return mState = uint32_t((uint64_t(mState) * 48271u) % 0x7fffffffu); } }; @@ -60,41 +123,36 @@ private: protected: static constexpr size_t ENTITY_INDEX = sizeof ... (Elements); - public: using SoA = StructureOfArrays; - using Structure = typename SoA::Structure; - using Instance = EntityInstanceBase::Type; SingleInstanceComponentManager() noexcept { // We always start with a dummy entry because index=0 is reserved. The component // at index = 0, is guaranteed to be default-initialized. - // Sub-classes can use this to their advantage. + // Subclasses can use this to their advantage. mData.push_back(Structure{}); } - SingleInstanceComponentManager(SingleInstanceComponentManager&&) noexcept {/* = default */} - SingleInstanceComponentManager& operator=(SingleInstanceComponentManager&&) noexcept {/* = default */} ~SingleInstanceComponentManager() noexcept = default; - // not copyable SingleInstanceComponentManager(SingleInstanceComponentManager const& rhs) = delete; SingleInstanceComponentManager& operator=(SingleInstanceComponentManager const& rhs) = delete; - + SingleInstanceComponentManager(SingleInstanceComponentManager&&) noexcept = default; + SingleInstanceComponentManager& operator=(SingleInstanceComponentManager&&) = default; // returns true if the given Entity has a component of this Manager - bool hasComponent(Entity e) const noexcept { + bool hasComponent(Entity const e) const noexcept { return getInstance(e) != 0; } // Get instance of this Entity to be used to retrieve components UTILS_NOINLINE - Instance getInstance(Entity e) const noexcept { + Instance getInstance(Entity const e) const noexcept { auto const& map = mInstanceMap; // find() generates quite a bit of code - auto pos = map.find(e); + auto const pos = map.find(e); return pos != map.end() ? pos->second : 0; } @@ -112,18 +170,18 @@ public: return data() + 1; } - Entity getEntity(Instance i) const noexcept { + Entity getEntity(Instance const i) const noexcept { return elementAt(i); } // Add a component to the given Entity. If the entity already has a component from this // manager, this function is a no-op. // This invalidates all pointers components. - inline Instance addComponent(Entity e); + Instance addComponent(Entity e); // Removes a component from the given entity. // This invalidates all pointers components. - inline Instance removeComponent(Entity e); + Instance removeComponent(Entity e); // return the first instance Instance begin() const noexcept { return 1u; } @@ -205,7 +263,7 @@ protected: } // swap only internals - void swap(Instance i, Instance j) noexcept { + void swap(Instance const i, Instance const j) noexcept { assert(i); assert(j); if (i && j) { @@ -230,7 +288,7 @@ protected: } template - void gc(const EntityManager& em, size_t ratio, + void gc(const EntityManager& em, size_t const ratio, REMOVE&& removeComponent) noexcept { Entity const* const pEntities = begin(); size_t count = getComponentCount(); @@ -249,11 +307,10 @@ protected: } removeComponent(entity); aliveInARow = 0; - count--; + --count; } } -protected: SoA mData; private: @@ -274,6 +331,7 @@ SingleInstanceComponentManager::addComponent(Entity e) { // index 0 is used when the component doesn't exist ci = Instance(mData.size() - 1); mInstanceMap[e] = ci; + notifyChange(e); } else { // if the entity already has this component, just return its instance ci = mInstanceMap[e]; @@ -286,9 +344,9 @@ SingleInstanceComponentManager::addComponent(Entity e) { // Keep these outside of the class because CLion has trouble parsing them template typename SingleInstanceComponentManager::Instance -SingleInstanceComponentManager::removeComponent(Entity e) { +SingleInstanceComponentManager::removeComponent(Entity const e) { auto& map = mInstanceMap; - auto pos = map.find(e); + auto const pos = map.find(e); if (UTILS_LIKELY(pos != map.end())) { size_t index = pos->second; assert(index != 0); @@ -300,11 +358,12 @@ SingleInstanceComponentManager::removeComponent(Entity e) { p[index] = std::move(p[last]); }); - Entity lastEntity = mData.template elementAt(index); + Entity const lastEntity = mData.template elementAt(index); map[lastEntity] = index; } mData.pop_back(); map.erase(pos); + notifyChange(e); return last; } return 0; diff --git a/libs/utils/src/EntityManager.cpp b/libs/utils/src/EntityManager.cpp index fda7cfeea9..afe84bb424 100644 --- a/libs/utils/src/EntityManager.cpp +++ b/libs/utils/src/EntityManager.cpp @@ -57,6 +57,18 @@ void EntityManager::unregisterListener(Listener* l) noexcept { static_cast(this)->unregisterListener(l); } +void EntityManager::registerChangeCallback(void const* token, ChangeCallback callback) noexcept { + static_cast(this)->registerChangeCallback(token, std::move(callback)); +} + +void EntityManager::unregisterChangeCallback(void const* token) noexcept { + static_cast(this)->unregisterChangeCallback(token); +} + +void EntityManager::flushNotifications() noexcept { + static_cast(this)->flushNotifications(); +} + size_t EntityManager::getEntityCount() const noexcept { return static_cast(this)->getEntityCount(); } diff --git a/libs/utils/src/EntityManagerImpl.h b/libs/utils/src/EntityManagerImpl.h index f385a8a7c4..8d94ec0e94 100644 --- a/libs/utils/src/EntityManagerImpl.h +++ b/libs/utils/src/EntityManagerImpl.h @@ -20,10 +20,12 @@ #include #include -#include -#include +#include #include +#include #include +#include +#include #include @@ -31,14 +33,19 @@ #include #endif +#include +#include +#include +#include #include +#include #include // for std::lock_guard +#include #include - namespace utils { -static constexpr const size_t MIN_FREE_INDICES = 1024; +static constexpr size_t MIN_FREE_INDICES = 1024; class UTILS_PRIVATE EntityManagerImpl : public EntityManager { public: @@ -62,7 +69,7 @@ public: void create(size_t n, Entity* entities) { Entity::Type index{}; auto& freeList = mFreeList; - uint8_t* const gens = mGens; + uint8_t const* const gens = mGens; // this must be thread-safe, acquire the free-list mutex std::lock_guard const lock(mFreeListLock); @@ -121,6 +128,17 @@ public: #if FILAMENT_UTILS_TRACK_ENTITIES mDebugActiveEntities.erase(entities[i]); #endif + mDirtyEntities[mDirtyCount++] = entities[i]; + if (mDirtyCount == MAX_DIRTY_COUNT) { + Entity localBuffer[MAX_DIRTY_COUNT]; + std::copy(mDirtyEntities, mDirtyEntities + MAX_DIRTY_COUNT, localBuffer); + mDirtyCount = 0; + lock.unlock(); + + triggerChangeCallbacks(localBuffer, MAX_DIRTY_COUNT); + + lock.lock(); + } } } lock.unlock(); @@ -142,6 +160,33 @@ public: mListeners.erase(l); } + void registerChangeCallback(void const* token, ChangeCallback callback) noexcept { + std::lock_guard const lock(mListenerLock); + mChangeCallbacks.push_back({token, std::move(callback)}); + } + + void unregisterChangeCallback(void const* token) noexcept { + std::lock_guard const lock(mListenerLock); + mChangeCallbacks.erase( + std::remove_if(mChangeCallbacks.begin(), mChangeCallbacks.end(), + [token](auto const& info) { return info.token == token; }), + mChangeCallbacks.end()); + } + + void flushNotifications() noexcept { + std::unique_lock lock(mFreeListLock); + if (mDirtyCount > 0) { + Entity localBuffer[MAX_DIRTY_COUNT]; + assert_invariant(mDirtyCount <= MAX_DIRTY_COUNT); + std::copy(mDirtyEntities, mDirtyEntities + mDirtyCount, localBuffer); + size_t const count = mDirtyCount; + mDirtyCount = 0; + lock.unlock(); + + triggerChangeCallbacks(localBuffer, count); + } + } + #if FILAMENT_UTILS_TRACK_ENTITIES std::vector getActiveEntities() const { std::vector result(mDebugActiveEntities.size()); @@ -162,6 +207,24 @@ public: #endif private: + std::vector getChangeCallbacks() const noexcept { + std::lock_guard const lock(mListenerLock); + std::vector result; + result.reserve(mChangeCallbacks.size()); + for (auto const& info : mChangeCallbacks) { + result.push_back(info.callback); + } + return result; + } + + void triggerChangeCallbacks(Entity const* entities, size_t n) const noexcept { + auto const callbacks = getChangeCallbacks(); + Slice const slice(entities, n); + for (auto const& callback : callbacks) { + callback(slice); + } + } + FixedCapacityVector getListeners() const noexcept { std::lock_guard const lock(mListenerLock); tsl::robin_set const& listeners = mListeners; @@ -180,6 +243,16 @@ private: mutable Mutex mListenerLock; tsl::robin_set mListeners; + struct CallbackInfo { + void const* token; + ChangeCallback callback; + }; + std::vector mChangeCallbacks; + + static constexpr size_t MAX_DIRTY_COUNT = 16; + Entity mDirtyEntities[MAX_DIRTY_COUNT]; + size_t mDirtyCount = 0; + #if FILAMENT_UTILS_TRACK_ENTITIES tsl::robin_map mDebugActiveEntities; #endif diff --git a/libs/utils/src/SingleInstanceComponentManager.cpp b/libs/utils/src/SingleInstanceComponentManager.cpp new file mode 100644 index 0000000000..729d05e5f2 --- /dev/null +++ b/libs/utils/src/SingleInstanceComponentManager.cpp @@ -0,0 +1,74 @@ +/* +* Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include + +namespace utils { + +void SingleInstanceComponentManagerBase::registerChangeCallback( + void const* token, ChangeCallback callback) noexcept { + mChangeCallbacks.push_back({ token, std::move(callback) }); +} + +void SingleInstanceComponentManagerBase::unregisterChangeCallback( + void const* token) noexcept { + mChangeCallbacks.erase( + std::remove_if(mChangeCallbacks.begin(), mChangeCallbacks.end(), + [token](auto const& info) { return info.token == token; }), + mChangeCallbacks.end()); +} + +void SingleInstanceComponentManagerBase::notifyChange(Entity const e) noexcept { + if constexpr (USE_SORTED_DIRTY_ARRAY) { + auto const it = std::lower_bound(mDirtyEntities, mDirtyEntities + mDirtyCount, e); + if (it != mDirtyEntities + mDirtyCount && *it == e) { + return; + } + size_t const idx = it - mDirtyEntities; + for (size_t i = mDirtyCount; i > idx; --i) { + mDirtyEntities[i] = mDirtyEntities[i - 1]; + } + mDirtyEntities[idx] = e; + mDirtyCount++; + } else { + for (size_t i = 0; i < mDirtyCount; ++i) { + if (mDirtyEntities[i] == e) { + return; + } + } + mDirtyEntities[mDirtyCount++] = e; + } + if (mDirtyCount == MAX_DIRTY_COUNT) { + flushNotifications(); + } +} + +void SingleInstanceComponentManagerBase::flushNotifications() noexcept { + if (mDirtyCount > 0) { + Slice const slice(mDirtyEntities, mDirtyCount); + for (auto const& [token, callback] : mChangeCallbacks) { + callback(slice); + } + mDirtyCount = 0; + } +} + +} // namespace utils diff --git a/libs/utils/test/test_Entity.cpp b/libs/utils/test/test_Entity.cpp index bc0a0708ae..4f666e172c 100644 --- a/libs/utils/test/test_Entity.cpp +++ b/libs/utils/test/test_Entity.cpp @@ -176,3 +176,43 @@ TEST(EntityTest, NameComponent) { cm.gc(em); } + +TEST(EntityTest, Callback) { + EntityManagerImpl em; + Entity entities[20]; + em.create(20, entities); + + std::vector notifiedEntities; + auto callback = [&](Slice slice) { + for (auto e : slice) { + notifiedEntities.push_back(e); + } + }; + + em.registerChangeCallback(&em, std::move(callback)); + + // Test small batch (<= 16) + em.destroy(10, entities); + em.flushNotifications(); + EXPECT_EQ(notifiedEntities.size(), 10); + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(notifiedEntities[i], entities[i]); + } + + notifiedEntities.clear(); + + // Test large batch (> 16) + Entity largeBatch[17]; + em.create(17, largeBatch); + em.destroy(17, largeBatch); + em.flushNotifications(); + EXPECT_EQ(notifiedEntities.size(), 17); + for (int i = 0; i < 17; ++i) { + EXPECT_EQ(notifiedEntities[i], largeBatch[i]); + } + + em.unregisterChangeCallback(&em); + + // Cleanup remaining + em.destroy(10, entities + 10); +} diff --git a/libs/utils/test/test_SingleInstanceComponentManager.cpp b/libs/utils/test/test_SingleInstanceComponentManager.cpp new file mode 100644 index 0000000000..f4f0ca29a5 --- /dev/null +++ b/libs/utils/test/test_SingleInstanceComponentManager.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +using namespace utils; + +// A dummy component for testing +struct DummyComponent { + int value = 0; +}; + +class TestManager : public SingleInstanceComponentManager { +public: + using SingleInstanceComponentManager::SingleInstanceComponentManager; + + void setValue(Instance i, int v) { + if (elementAt<0>(i).value != v) { + elementAt<0>(i).value = v; + notifyChange(getEntity(i)); + } + } +}; + +TEST(SingleInstanceComponentManagerTest, BatchCallbackTest) { + EntityManager& em = EntityManager::get(); + TestManager manager; + + std::vector notifiedEntities; + + auto callback = [&](Slice entities) { + for (auto e : entities) { + notifiedEntities.push_back(e); + } + }; + + manager.registerChangeCallback(&manager, std::move(callback)); + + // Test addComponent (should be batched, not flushed yet) + Entity e1 = em.create(); + auto i1 = manager.addComponent(e1); + + EXPECT_TRUE(notifiedEntities.empty()); + + // Manual flush + manager.flushNotifications(); + EXPECT_EQ(notifiedEntities.size(), 1); + EXPECT_EQ(notifiedEntities[0], e1); + + notifiedEntities.clear(); + + // Test auto-flush when full (16 elements) + std::vector entities; + for (int i = 0; i < 16; ++i) { + Entity e = em.create(); + entities.push_back(e); + manager.addComponent(e); + } + + // The 16th add should have triggered a flush! + EXPECT_EQ(notifiedEntities.size(), 16); + if constexpr (SingleInstanceComponentManagerBase::USE_SORTED_DIRTY_ARRAY) { + std::sort(entities.begin(), entities.end()); + } + for (int i = 0; i < 16; ++i) { + EXPECT_EQ(notifiedEntities[i], entities[i]); + } + + notifiedEntities.clear(); + + // Test change check in setValue + auto i_last = manager.getInstance(entities.back()); + manager.setValue(i_last, 42); // Value changed + manager.flushNotifications(); + EXPECT_EQ(notifiedEntities.size(), 1); + EXPECT_EQ(notifiedEntities[0], entities.back()); + + notifiedEntities.clear(); + + manager.setValue(i_last, 42); // Value not changed + manager.flushNotifications(); + EXPECT_TRUE(notifiedEntities.empty()); // Should not notify + + notifiedEntities.clear(); + + // Test de-duplication in notifyChange + manager.setValue(i_last, 43); + manager.setValue(i_last, 44); // Modifying same entity again! + manager.flushNotifications(); + EXPECT_EQ(notifiedEntities.size(), 1); // Should only appear ONCE in the batch! + + manager.unregisterChangeCallback(&manager); + + // Cleanup + for (auto e : entities) { + manager.removeComponent(e); + em.destroy(e); + } + manager.removeComponent(e1); + em.destroy(e1); +} + +TEST(SingleInstanceComponentManagerTest, MultiCallbackTest) { + EntityManager& em = EntityManager::get(); + TestManager manager; + + int count1 = 0; + int count2 = 0; + auto cb1 = [&](Slice) { count1++; }; + auto cb2 = [&](Slice) { count2++; }; + + int token1 = 1; + int token2 = 2; + manager.registerChangeCallback(&token1, std::move(cb1)); + manager.registerChangeCallback(&token2, std::move(cb2)); + + Entity e1 = em.create(); + manager.addComponent(e1); + manager.flushNotifications(); + + EXPECT_EQ(count1, 1); + EXPECT_EQ(count2, 1); + + manager.unregisterChangeCallback(&token1); + + Entity e2 = em.create(); + manager.addComponent(e2); + manager.flushNotifications(); + + EXPECT_EQ(count1, 1); // Should not increase + EXPECT_EQ(count2, 2); // Should increase + + manager.unregisterChangeCallback(&token2); + + em.destroy(e1); + em.destroy(e2); +} + +TEST(SingleInstanceComponentManagerTest, DuplicateCallbackTest) { + EntityManager& em = EntityManager::get(); + TestManager manager; + + int count = 0; + + // We must create two separate lambdas because Invocable is move-only + auto cb1 = [&](Slice) { count++; }; + auto cb2 = [&](Slice) { count++; }; + + int token = 1; + manager.registerChangeCallback(&token, std::move(cb1)); + manager.registerChangeCallback(&token, std::move(cb2)); + + Entity e = em.create(); + manager.addComponent(e); + manager.flushNotifications(); + + EXPECT_EQ(count, 2); // Both should be called + + manager.unregisterChangeCallback(&token); + + Entity e2 = em.create(); + manager.addComponent(e2); + manager.flushNotifications(); + + EXPECT_EQ(count, 2); // Should not increase after unregister + + em.destroy(e); + em.destroy(e2); +}