diff --git a/tsd/apps/interactive/mpiViewer/DistributedSceneController.cpp b/tsd/apps/interactive/mpiViewer/DistributedSceneController.cpp index 3f86452f6..32b05d6a8 100644 --- a/tsd/apps/interactive/mpiViewer/DistributedSceneController.cpp +++ b/tsd/apps/interactive/mpiViewer/DistributedSceneController.cpp @@ -136,7 +136,7 @@ void DistributedSceneController::shutdown() MPI_Barrier(MPI_COMM_WORLD); auto d = m_anari.device; - m_ctx->anari.releaseRenderIndex(d); + m_ctx->anari.releaseRenderIndex(m_ctx->tsd.scene, d); anari::release(d, m_anari.hdriLight); anari::release(d, m_anari.renderer); anari::release(d, m_anari.camera); @@ -291,4 +291,4 @@ void DistributedSceneController::executeFrame_render() MPI_Barrier(MPI_COMM_WORLD); } -} // namespace tsd::mpi_viewer \ No newline at end of file +} // namespace tsd::mpi_viewer diff --git a/tsd/apps/interactive/network/client/tsdRemoteViewer.cpp b/tsd/apps/interactive/network/client/tsdRemoteViewer.cpp index e992e93e4..c8dc30943 100644 --- a/tsd/apps/interactive/network/client/tsdRemoteViewer.cpp +++ b/tsd/apps/interactive/network/client/tsdRemoteViewer.cpp @@ -35,7 +35,7 @@ struct Application : public TSDApplication void connect(); void disconnect(); - std::unique_ptr m_updateDelegate; + tsd::network::NetworkUpdateDelegate *m_updateDelegate{nullptr}; tsd::ui::imgui::RemoteViewport *m_viewport{nullptr}; std::shared_ptr m_client; std::string m_host{"127.0.0.1"}; @@ -52,8 +52,9 @@ Application::Application() m_client = std::make_shared(); - m_updateDelegate = std::make_unique( - &ctx->tsd.scene, m_client.get()); + m_updateDelegate = + ctx->tsd.scene.updateDelegate().emplace( + &ctx->tsd.scene, m_client.get()); ctx->tsd.animationMgr.setTimeChangedCallback([this](float time) { if (m_timeUpdatesEnabled) @@ -100,11 +101,14 @@ Application::Application() m_timeUpdatesEnabled = true; }); - ctx->tsd.scene.setUpdateDelegate(m_updateDelegate.get()); ctx->tsd.sceneLoadComplete = false; } -Application::~Application() = default; +Application::~Application() +{ + if (m_updateDelegate) + appContext()->tsd.scene.updateDelegate().erase(m_updateDelegate); +} anari_viewer::WindowArray Application::setupWindows() { diff --git a/tsd/apps/interactive/network/mpiServer/DistributedRenderServer.cpp b/tsd/apps/interactive/network/mpiServer/DistributedRenderServer.cpp index 425270c78..042b93499 100644 --- a/tsd/apps/interactive/network/mpiServer/DistributedRenderServer.cpp +++ b/tsd/apps/interactive/network/mpiServer/DistributedRenderServer.cpp @@ -176,7 +176,7 @@ void DistributedRenderServer::run(short port) } m_camera = {}; - m_ctx.anari.releaseRenderIndex(m_device); + m_ctx.anari.releaseRenderIndex(m_ctx.tsd.scene, m_device); m_ctx.anari.releaseAllDevices(); MPI_Barrier(MPI_COMM_WORLD); @@ -256,7 +256,7 @@ void DistributedRenderServer::setup_ANARIDevice() return v.get() == myRank; return true; // no mpiRank tag → global (camera, renderer, etc.) }); - m_ctx.anari.getUpdateDelegate().signalObjectFilteringChanged(); + m_ctx.tsd.scene.updateDelegate().signalObjectFilteringChanged(); m_camera = scene.defaultCamera(); m_renderers = scene.renderersOfDevice(m_libName).empty() diff --git a/tsd/apps/interactive/network/server/RenderServer.cpp b/tsd/apps/interactive/network/server/RenderServer.cpp index b2af71742..6f82c1ad3 100644 --- a/tsd/apps/interactive/network/server/RenderServer.cpp +++ b/tsd/apps/interactive/network/server/RenderServer.cpp @@ -105,7 +105,7 @@ void RenderServer::run(short port) m_server->removeAllHandlers(); m_camera = {}; - m_ctx.anari.releaseRenderIndex(m_device); + m_ctx.anari.releaseRenderIndex(m_ctx.tsd.scene, m_device); m_ctx.anari.releaseAllDevices(); } diff --git a/tsd/apps/tutorial/tsdTutorialMultiRender.cpp b/tsd/apps/tutorial/tsdTutorialMultiRender.cpp index df1517034..8708328a5 100644 --- a/tsd/apps/tutorial/tsdTutorialMultiRender.cpp +++ b/tsd/apps/tutorial/tsdTutorialMultiRender.cpp @@ -72,17 +72,15 @@ int main() // Setup render indexes // - tsd::rendering::MultiUpdateDelegate ud; + auto &ud = scene.updateDelegate(); auto *rIdx1 = ud.emplace( scene, deviceName, device); auto *rIdx2 = ud.emplace( scene, deviceName, device); - rIdx1->populate(false); - rIdx2->populate(false); - - scene.setUpdateDelegate(&ud); + rIdx1->populate(); + rIdx2->populate(); // Create camera // diff --git a/tsd/src/anari_tsd/Device.cpp b/tsd/src/anari_tsd/Device.cpp index 08f293ad3..81ba0859c 100644 --- a/tsd/src/anari_tsd/Device.cpp +++ b/tsd/src/anari_tsd/Device.cpp @@ -243,7 +243,6 @@ void Device::initDevice() libraryName.c_str()); state->deviceName = libraryName; - state->scene->setUpdateDelegate(&state->anari.getUpdateDelegate()); } } diff --git a/tsd/src/anari_tsd/World.cpp b/tsd/src/anari_tsd/World.cpp index d0540e0d5..266cae937 100644 --- a/tsd/src/anari_tsd/World.cpp +++ b/tsd/src/anari_tsd/World.cpp @@ -25,7 +25,7 @@ World::~World() { auto *s = deviceState(); if (!s->usingExternalScene()) - s->anari.releaseRenderIndex(s->device); + s->anari.releaseRenderIndex(*s->scene, s->device); s->scene->removeLayer(m_layerName); } diff --git a/tsd/src/tsd/app/ANARIDeviceManager.cpp b/tsd/src/tsd/app/ANARIDeviceManager.cpp index 075794391..9c32b555c 100644 --- a/tsd/src/tsd/app/ANARIDeviceManager.cpp +++ b/tsd/src/tsd/app/ANARIDeviceManager.cpp @@ -7,6 +7,8 @@ // tsd_rendering #include "tsd/rendering/index/RenderIndexAllLayers.hpp" #include "tsd/rendering/index/RenderIndexFlatRegistry.hpp" +// std +#include namespace tsd::app { @@ -146,36 +148,72 @@ tsd::rendering::RenderIndex *ANARIDeviceManager::acquireRenderIndex( tsd::scene::Scene &c, tsd::core::Token n, anari::Device d) { auto &liveIdx = m_rIdxs[d]; + if (liveIdx.scene && liveIdx.scene != &c) { + tsd::core::logError( + "ANARIDeviceManager::acquireRenderIndex() scene mismatch for device %p" + " (existing=%p, requested=%p)", + (void *)d, + (void *)liveIdx.scene, + (void *)&c); + return nullptr; + } + if (liveIdx.refCount == 0) { + liveIdx.scene = &c; switch (renderIndexKind()) { case RenderIndexKind::FLAT: liveIdx.idx = - m_delegate.emplace(c, n, d); + c.updateDelegate().emplace( + c, n, d); break; case RenderIndexKind::ALL_LAYERS: default: liveIdx.idx = - m_delegate.emplace(c, n, d); + c.updateDelegate().emplace( + c, n, d); break; } - liveIdx.idx->populate(false); + liveIdx.idx->populate(); } liveIdx.refCount++; return liveIdx.idx; } -void ANARIDeviceManager::releaseRenderIndex(anari::Device d) +void ANARIDeviceManager::releaseRenderIndex( + tsd::scene::Scene &c, anari::Device d) { - auto &liveIdx = m_rIdxs[d]; + auto itr = m_rIdxs.find(d); + if (itr == m_rIdxs.end()) + return; + + auto &liveIdx = itr->second; + if (liveIdx.scene && liveIdx.scene != &c) { + tsd::core::logError( + "ANARIDeviceManager::releaseRenderIndex() scene mismatch for device %p" + " (existing=%p, requested=%p)", + (void *)d, + (void *)liveIdx.scene, + (void *)&c); + return; + } + if (liveIdx.refCount == 0) return; - else if (liveIdx.refCount == 1) - m_delegate.erase(liveIdx.idx); - liveIdx.refCount--; + if (--liveIdx.refCount == 0) { + c.updateDelegate().erase(liveIdx.idx); + m_rIdxs.erase(itr); + } } void ANARIDeviceManager::releaseAllDevices() { + for (auto &itr : m_rIdxs) { + auto &liveIdx = itr.second; + if (liveIdx.scene && liveIdx.idx) + liveIdx.scene->updateDelegate().erase(liveIdx.idx); + } + m_rIdxs.clear(); + for (auto &d : m_loadedDevices) { if (d.second) anari::release(d.second, d.second); @@ -183,11 +221,6 @@ void ANARIDeviceManager::releaseAllDevices() m_loadedDevices.clear(); } -tsd::scene::MultiUpdateDelegate &ANARIDeviceManager::getUpdateDelegate() -{ - return m_delegate; -} - void ANARIDeviceManager::setRenderIndexKind(RenderIndexKind k) { m_settings.renderIndexKind = k; diff --git a/tsd/src/tsd/app/ANARIDeviceManager.h b/tsd/src/tsd/app/ANARIDeviceManager.h index f81409c43..00fb3e754 100644 --- a/tsd/src/tsd/app/ANARIDeviceManager.h +++ b/tsd/src/tsd/app/ANARIDeviceManager.h @@ -23,14 +23,14 @@ using DeviceInitParam = std::pair; /* * Manages the lifecycle of ANARI devices and their associated RenderIndex - * instances; loads libraries on demand, reference-counts render indices per - * device, and fans update delegate signals to all active render indices. + * instances; loads libraries on demand and reference-counts one scene-owned + * RenderIndex per ANARI device. * * Example: * ANARIDeviceManager mgr; * auto device = mgr.loadDevice("visrtx"); * auto *idx = mgr.acquireRenderIndex(scene, deviceToken, device); - * mgr.releaseRenderIndex(device); + * mgr.releaseRenderIndex(scene, device); */ struct ANARIDeviceManager { @@ -45,11 +45,9 @@ struct ANARIDeviceManager const anari::Extensions *loadDeviceExtensions(const std::string &libName); tsd::rendering::RenderIndex *acquireRenderIndex( tsd::scene::Scene &c, tsd::core::Token deviceName, anari::Device device); - void releaseRenderIndex(anari::Device device); + void releaseRenderIndex(tsd::scene::Scene &c, anari::Device device); void releaseAllDevices(); - tsd::scene::MultiUpdateDelegate &getUpdateDelegate(); - void setRenderIndexKind(RenderIndexKind k); RenderIndexKind renderIndexKind() const; @@ -60,11 +58,11 @@ struct ANARIDeviceManager const bool *m_verboseFlag{nullptr}; struct LiveAnariIndex { + tsd::scene::Scene *scene{nullptr}; int refCount{0}; tsd::rendering::RenderIndex *idx{nullptr}; }; std::map m_rIdxs; - tsd::scene::MultiUpdateDelegate m_delegate; std::map m_loadedDevices; std::map m_loadedDeviceExtensions; std::vector m_libraryList; diff --git a/tsd/src/tsd/app/Context.cpp b/tsd/src/tsd/app/Context.cpp index 6986581a7..3b0905921 100644 --- a/tsd/src/tsd/app/Context.cpp +++ b/tsd/src/tsd/app/Context.cpp @@ -18,10 +18,7 @@ namespace tsd::app { TSDState::TSDState() : animationMgr(&scene) {} -Context::Context() : anari(&m_logging.verbose) -{ - tsd.scene.setUpdateDelegate(&anari.getUpdateDelegate()); -} +Context::Context() : anari(&m_logging.verbose) {} Context::~Context() { @@ -248,7 +245,7 @@ void Context::setSelected(tsd::scene::LayerNodeRef node) void Context::setSelected(const std::vector &nodes) { tsd.selectedNodes = nodes; - anari.getUpdateDelegate().signalObjectFilteringChanged(); + tsd.scene.updateDelegate().signalObjectFilteringChanged(); } void Context::setSelected(const tsd::scene::Object *obj) @@ -304,7 +301,7 @@ void Context::addToSelection(tsd::scene::LayerNodeRef node) } tsd.selectedNodes.push_back(node); - anari.getUpdateDelegate().signalObjectFilteringChanged(); + tsd.scene.updateDelegate().signalObjectFilteringChanged(); } void Context::removeFromSelection(tsd::scene::LayerNodeRef node) @@ -312,7 +309,7 @@ void Context::removeFromSelection(tsd::scene::LayerNodeRef node) auto it = std::find(tsd.selectedNodes.begin(), tsd.selectedNodes.end(), node); if (it != tsd.selectedNodes.end()) { tsd.selectedNodes.erase(it); - anari.getUpdateDelegate().signalObjectFilteringChanged(); + tsd.scene.updateDelegate().signalObjectFilteringChanged(); } } @@ -326,7 +323,7 @@ void Context::clearSelected() { if (!tsd.selectedNodes.empty()) { tsd.selectedNodes.clear(); - anari.getUpdateDelegate().signalObjectFilteringChanged(); + tsd.scene.updateDelegate().signalObjectFilteringChanged(); } } std::vector Context::getParentOnlySelectedNodes() diff --git a/tsd/src/tsd/app/renderAnimationSequence.cpp b/tsd/src/tsd/app/renderAnimationSequence.cpp index ae616a80f..0ac9ece52 100644 --- a/tsd/src/tsd/app/renderAnimationSequence.cpp +++ b/tsd/src/tsd/app/renderAnimationSequence.cpp @@ -64,8 +64,9 @@ void renderAnimationSequence(Context &ctx, } anari::commitParameters(d, d); - auto renderIndex = - std::make_unique(scene, libName, d); + auto *renderIndex = + scene.updateDelegate().emplace( + scene, libName, d); renderIndex->populate(); // Validate camera — resolve index // @@ -78,6 +79,7 @@ void renderAnimationSequence(Context &ctx, if (!cameraRef) { tsd::core::logError( "[renderAnimationSequence] No camera at index %zu", camIdx); + scene.updateDelegate().erase(renderIndex); anari::release(d, d); return; } @@ -189,6 +191,7 @@ void renderAnimationSequence(Context &ctx, // Restore animation state // animMgr.setAnimationFrame(savedFrame); + scene.updateDelegate().erase(renderIndex); } } // namespace tsd::app diff --git a/tsd/src/tsd/rendering/index/RenderIndex.cpp b/tsd/src/tsd/rendering/index/RenderIndex.cpp index a94a3924c..c30491c94 100644 --- a/tsd/src/tsd/rendering/index/RenderIndex.cpp +++ b/tsd/src/tsd/rendering/index/RenderIndex.cpp @@ -86,7 +86,7 @@ void RenderIndex::logCacheInfo() const logStatus(" renderers: %zu", m_cache.renderer.size()); } -void RenderIndex::populate(bool setAsUpdateDelegate) +void RenderIndex::populate() { m_cache.clear(); @@ -108,9 +108,6 @@ void RenderIndex::populate(bool setAsUpdateDelegate) createANARICacheObjects(db.camera, m_cache.camera); createANARICacheObjects(db.renderer, m_cache.renderer); - if (setAsUpdateDelegate) - m_ctx->setUpdateDelegate(this); - updateWorld(); } @@ -223,7 +220,7 @@ void RenderIndex::signalRemoveAllObjects() void RenderIndex::signalInvalidateCachedObjects() { signalRemoveAllObjects(); - populate(false); // always 'false' as this may already be the delegate + populate(); updateWorld(); } diff --git a/tsd/src/tsd/rendering/index/RenderIndex.hpp b/tsd/src/tsd/rendering/index/RenderIndex.hpp index b344e19bb..5a7beffaa 100644 --- a/tsd/src/tsd/rendering/index/RenderIndex.hpp +++ b/tsd/src/tsd/rendering/index/RenderIndex.hpp @@ -24,7 +24,8 @@ struct RenderToAnariObjectsVisitor; * subclasses decide how layers are mapped to world instances. * * Example: - * auto idx = std::make_unique(scene, device); + * auto idx = scene.updateDelegate().emplace( + * scene, deviceName, device); * idx->populate(); * anari::World world = idx->world(); */ @@ -42,7 +43,9 @@ struct RenderIndex : public BaseUpdateDelegate void logCacheInfo() const; - void populate(bool setAsUpdateDelegate = true); + // Bootstrap or fully rebuild this index from the current Scene snapshot. + // This does not register the index as a Scene update delegate. + void populate(); virtual void setFilterFunction(RenderIndexFilterFcn f); virtual bool isFlat() const = 0; diff --git a/tsd/src/tsd/rendering/index/RenderIndexAllLayers.hpp b/tsd/src/tsd/rendering/index/RenderIndexAllLayers.hpp index 9212d4d25..d8aad0ce4 100644 --- a/tsd/src/tsd/rendering/index/RenderIndexAllLayers.hpp +++ b/tsd/src/tsd/rendering/index/RenderIndexAllLayers.hpp @@ -15,9 +15,10 @@ namespace tsd::rendering { * gathering. * * Example: - * RenderIndexAllLayers idx(scene, deviceToken, anariDevice); - * idx.populate(); - * idx.setFilterFunction([](auto *o){ return o->name() != "hidden"; }); + * auto *idx = scene.updateDelegate().emplace( + * scene, deviceToken, anariDevice); + * idx->populate(); + * idx->setFilterFunction([](auto *o){ return o->name() != "hidden"; }); */ struct RenderIndexAllLayers : public RenderIndex { diff --git a/tsd/src/tsd/rendering/index/RenderIndexFlatRegistry.hpp b/tsd/src/tsd/rendering/index/RenderIndexFlatRegistry.hpp index 2da9e76b9..a86d4b32d 100644 --- a/tsd/src/tsd/rendering/index/RenderIndexFlatRegistry.hpp +++ b/tsd/src/tsd/rendering/index/RenderIndexFlatRegistry.hpp @@ -13,8 +13,9 @@ namespace tsd::rendering { * collections. * * Example: - * RenderIndexFlatRegistry idx(scene, deviceToken, anariDevice); - * idx.populate(); + * auto *idx = scene.updateDelegate().emplace( + * scene, deviceToken, anariDevice); + * idx->populate(); */ struct RenderIndexFlatRegistry : public RenderIndex { diff --git a/tsd/src/tsd/scene/README.md b/tsd/src/tsd/scene/README.md index bd41b3a04..287530798 100644 --- a/tsd/src/tsd/scene/README.md +++ b/tsd/src/tsd/scene/README.md @@ -15,7 +15,9 @@ object concepts and layered scene-graph instancing. use-count tracking for cleanup/garbage collection. - Update notifications: `BaseUpdateDelegate` and `MultiUpdateDelegate` forward object/layer/animation - changes to rendering, networking, or UI subsystems. + changes to rendering, networking, or UI subsystems. Every `Scene` + intrinsically owns a `MultiUpdateDelegate` root exposed via + `scene.updateDelegate()`. - Animation system: time-step arrays and keyframe channels for parameter and transform interpolation. diff --git a/tsd/src/tsd/scene/Scene.cpp b/tsd/src/tsd/scene/Scene.cpp index 2f47e40ba..af9cb39bf 100644 --- a/tsd/src/tsd/scene/Scene.cpp +++ b/tsd/src/tsd/scene/Scene.cpp @@ -7,9 +7,42 @@ // std #include #include +#if defined(__cpp_rtti) || defined(__GXX_RTTI) || defined(_CPPRTTI) +#include +#endif namespace tsd::scene { +namespace { + +const char *delegateTypeName(const BaseUpdateDelegate &delegate) +{ +#if defined(__cpp_rtti) || defined(__GXX_RTTI) || defined(_CPPRTTI) + return typeid(delegate).name(); +#else + return ""; +#endif +} + +void logRemainingDelegates(const MultiUpdateDelegate &delegate) +{ + if (delegate.size() == 0) + return; + + logError("Scene::~Scene(): %zu update delegate(s) still registered", + delegate.size()); + + for (size_t i = 0; i < delegate.size(); i++) { + auto *child = delegate.get(i); + logError(" delegate[%zu]: type=%s ptr=%p", + i, + child ? delegateTypeName(*child) : "", + (const void *)child); + } +} + +} // namespace + std::string objectDBInfo(const ObjectDatabase &db) { std::stringstream ss; @@ -79,10 +112,11 @@ Scene::Scene() Scene::~Scene() { + logRemainingDelegates(m_updateDelegate); + m_updateDelegate.clear(); + m_defaultObjects.material.reset(); m_defaultObjects.camera.reset(); - - m_updateDelegate = nullptr; m_layers.clear(); auto reportObjectUsages = [&](auto &array) { @@ -356,8 +390,7 @@ void Scene::removeObject(const Object *_o) auto &o = *_o; - if (m_updateDelegate) - m_updateDelegate->signalObjectRemoved(&o); + m_updateDelegate.signalObjectRemoved(&o); const auto type = o.type(); const auto index = o.index(); @@ -403,8 +436,7 @@ void Scene::removeObject(const Object *_o) void Scene::removeAllObjects() { - if (m_updateDelegate) - m_updateDelegate->signalRemoveAllObjects(); + m_updateDelegate.signalRemoveAllObjects(); m_defaultObjects.material.reset(); m_defaultObjects.camera.reset(); @@ -465,32 +497,14 @@ void Scene::removeRenderersForDevice(Token deviceName) removeObject(r.get()); } -BaseUpdateDelegate *Scene::updateDelegate() const +MultiUpdateDelegate &Scene::updateDelegate() { return m_updateDelegate; } -void Scene::setUpdateDelegate(BaseUpdateDelegate *ud) +const MultiUpdateDelegate &Scene::updateDelegate() const { - m_updateDelegate = ud; - - auto setDelegateOnObjects = [&](auto &array) { - foreach_item_const(array, [&](auto *o) { - if (o) - o->setUpdateDelegate(ud); - }); - }; - - setDelegateOnObjects(m_db.array); - setDelegateOnObjects(m_db.light); - setDelegateOnObjects(m_db.surface); - setDelegateOnObjects(m_db.geometry); - setDelegateOnObjects(m_db.material); - setDelegateOnObjects(m_db.sampler); - setDelegateOnObjects(m_db.volume); - setDelegateOnObjects(m_db.field); - setDelegateOnObjects(m_db.camera); - setDelegateOnObjects(m_db.renderer); + return m_updateDelegate; } const ObjectDatabase &Scene::objectDB() const @@ -524,8 +538,7 @@ Layer *Scene::addLayer(Token name) auto &ls = m_layers[name]; if (!ls.ptr) { ls.ptr.reset(new Layer(this, name.str())); - if (m_updateDelegate) - m_updateDelegate->signalLayerAdded(ls.ptr.get()); + m_updateDelegate.signalLayerAdded(ls.ptr.get()); m_numActiveLayers++; } return ls.ptr.get(); @@ -595,8 +608,7 @@ void Scene::removeLayer(Token name) { if (!m_layers.contains(name)) return; - if (m_updateDelegate) - m_updateDelegate->signalLayerRemoved(m_layers[name].ptr.get()); + m_updateDelegate.signalLayerRemoved(m_layers[name].ptr.get()); if (m_layers[name].active) m_numActiveLayers--; m_layers.erase(name); @@ -606,10 +618,7 @@ void Scene::removeLayer(const Layer *layer) { for (size_t i = 0; i < m_layers.size(); i++) { if (m_layers.at_index(i).second.ptr.get() == layer) { - if (m_updateDelegate) { - m_updateDelegate->signalLayerRemoved( - m_layers.at_index(i).second.ptr.get()); - } + m_updateDelegate.signalLayerRemoved(m_layers.at_index(i).second.ptr.get()); m_layers.erase(i); return; } @@ -619,8 +628,7 @@ void Scene::removeLayer(const Layer *layer) void Scene::removeAllLayers() { for (auto itr = m_layers.begin(); itr != m_layers.end(); itr++) { - if (m_updateDelegate) - m_updateDelegate->signalLayerRemoved(itr->second.ptr.get()); + m_updateDelegate.signalLayerRemoved(itr->second.ptr.get()); } m_layers.clear(); @@ -719,32 +727,28 @@ void Scene::signalLayerStructureChanged(const Layer *l) { if (m_inLayerBatch) m_batchedLayerUpdates.push_back(l); - else if (m_updateDelegate) - m_updateDelegate->signalLayerStructureUpdated(l); + else + m_updateDelegate.signalLayerStructureUpdated(l); } void Scene::signalLayerTransformChanged(const Layer *l) { - if (m_updateDelegate) - m_updateDelegate->signalLayerTransformUpdated(l); + m_updateDelegate.signalLayerTransformUpdated(l); } void Scene::signalActiveLayersChanged() { - if (m_updateDelegate) - m_updateDelegate->signalActiveLayersChanged(); + m_updateDelegate.signalActiveLayersChanged(); } void Scene::signalObjectParameterUseCountZero(const Object *obj) { - if (m_updateDelegate) - m_updateDelegate->signalObjectParameterUseCountZero(obj); + m_updateDelegate.signalObjectParameterUseCountZero(obj); } void Scene::signalObjectLayerUseCountZero(const Object *obj) { - if (m_updateDelegate) - m_updateDelegate->signalObjectLayerUseCountZero(obj); + m_updateDelegate.signalObjectLayerUseCountZero(obj); } void Scene::removeUnusedObjects(bool includeRenderersAndCameras) @@ -867,8 +871,7 @@ void Scene::defragmentObjectStorage() updateObjectHeldIndex(m_db.renderer); // Signal updates to any delegates // - if (m_updateDelegate) - m_updateDelegate->signalInvalidateCachedObjects(); + m_updateDelegate.signalInvalidateCachedObjects(); } void Scene::cleanupScene() @@ -900,10 +903,8 @@ ArrayRef Scene::createArrayImpl(anari::DataType type, retval->m_scene = this; retval->m_index = retval.index(); - if (m_updateDelegate) { - retval->setUpdateDelegate(m_updateDelegate); - m_updateDelegate->signalObjectAdded(retval.data()); - } + retval->setUpdateDelegate(&m_updateDelegate); + m_updateDelegate.signalObjectAdded(retval.data()); return retval; } diff --git a/tsd/src/tsd/scene/Scene.hpp b/tsd/src/tsd/scene/Scene.hpp index c7b1c17ba..b36231fdf 100644 --- a/tsd/src/tsd/scene/Scene.hpp +++ b/tsd/src/tsd/scene/Scene.hpp @@ -5,6 +5,7 @@ #include "tsd/scene/DefragCallback.hpp" #include "tsd/scene/Layer.hpp" +#include "tsd/scene/UpdateDelegate.hpp" #include "tsd/scene/objects/Array.hpp" #include "tsd/scene/objects/Camera.hpp" #include "tsd/scene/objects/Geometry.hpp" @@ -37,8 +38,6 @@ void load_Scene(scene::Scene &, core::DataNode &, animation::AnimationManager *) namespace tsd::scene { -struct BaseUpdateDelegate; - /* * Flat collection of ObjectPool instances, one per ANARI object type; serves * as the canonical storage for all scene objects indexed by their pool slot. @@ -79,8 +78,8 @@ using LayerMap = FlatMap; /* * Central scene container that owns all ANARI-typed objects, a named layer - * hierarchy, animations, and an optional update delegate; the main entry point - * for creating, retrieving, and removing scene content. + * hierarchy, animations, and an intrinsic MultiUpdateDelegate root; the main + * entry point for creating, retrieving, and removing scene content. * * Example: * Scene scene; @@ -147,8 +146,8 @@ struct Scene std::vector renderersOfDevice(Token deviceName) const; void removeRenderersForDevice(Token deviceName); - BaseUpdateDelegate *updateDelegate() const; - void setUpdateDelegate(BaseUpdateDelegate *ud); + MultiUpdateDelegate &updateDelegate(); + const MultiUpdateDelegate &updateDelegate() const; const ObjectDatabase &objectDB() const; @@ -253,7 +252,7 @@ struct Scene ObjectUsePtr camera; } m_defaultObjects; - BaseUpdateDelegate *m_updateDelegate{nullptr}; + MultiUpdateDelegate m_updateDelegate; LayerMap m_layers; size_t m_numActiveLayers{0}; @@ -410,10 +409,8 @@ inline ObjectPoolRef Scene::createObjectImpl( auto retval = iv.emplace(std::forward(args)...); retval->m_scene = this; retval->m_index = retval.index(); - if (m_updateDelegate) { - retval->setUpdateDelegate(m_updateDelegate); - m_updateDelegate->signalObjectAdded(retval.data()); - } + retval->setUpdateDelegate(&m_updateDelegate); + m_updateDelegate.signalObjectAdded(retval.data()); return retval; } diff --git a/tsd/src/tsd/scene/UpdateDelegate.hpp b/tsd/src/tsd/scene/UpdateDelegate.hpp index cabfa5f2e..cc85fc501 100644 --- a/tsd/src/tsd/scene/UpdateDelegate.hpp +++ b/tsd/src/tsd/scene/UpdateDelegate.hpp @@ -28,7 +28,7 @@ struct Parameter; * { scheduleUpload(o, p); } * // ... implement remaining pure virtuals ... * }; - * scene.setUpdateDelegate(&myDelegate); + * scene.updateDelegate().emplace(); */ struct BaseUpdateDelegate { @@ -64,8 +64,7 @@ struct BaseUpdateDelegate * events. * * Example: - * EmptyUpdateDelegate noop; - * scene.setUpdateDelegate(&noop); + * scene.updateDelegate().emplace(); */ struct EmptyUpdateDelegate : public BaseUpdateDelegate { @@ -98,10 +97,9 @@ struct EmptyUpdateDelegate : public BaseUpdateDelegate * out to each of them; enables multiple independent systems to observe a Scene. * * Example: - * MultiUpdateDelegate multi; + * auto &multi = scene.updateDelegate(); * multi.emplace(); * multi.emplace(); - * scene.setUpdateDelegate(&multi); */ struct MultiUpdateDelegate : public BaseUpdateDelegate { @@ -152,4 +150,4 @@ inline T *MultiUpdateDelegate::emplace(Args &&...args) return (T *)m_delegates.back().get(); } -} // namespace tsd::scene \ No newline at end of file +} // namespace tsd::scene diff --git a/tsd/src/tsd/scripting/README.md b/tsd/src/tsd/scripting/README.md index 520ae1255..c36f8e7f1 100644 --- a/tsd/src/tsd/scripting/README.md +++ b/tsd/src/tsd/scripting/README.md @@ -76,7 +76,7 @@ tsd.io.generateMaterialOrb(scene) -- Batch rendering local device = tsd.render.loadDevice("visrtx") local ri = tsd.render.createRenderIndex(scene, device) -ri:populate() +ri:populate() -- bootstrap the scene-owned live render index local cam = tsd.CameraSetup.new() cam.position, cam.direction, cam.up = tsd.float3(0, 0, 5), tsd.float3(0, 0, -1), tsd.float3(0, 1, 0) cam.fovy, cam.aspect = 45.0, 16/9 diff --git a/tsd/src/tsd/scripting/bindings/RenderBindings.cpp b/tsd/src/tsd/scripting/bindings/RenderBindings.cpp index 219226763..2f7f22b5f 100644 --- a/tsd/src/tsd/scripting/bindings/RenderBindings.cpp +++ b/tsd/src/tsd/scripting/bindings/RenderBindings.cpp @@ -118,17 +118,17 @@ void registerRenderBindings(sol::state &lua) render["createRenderIndex"] = [](scene::Scene &scene, std::shared_ptr dev) - -> std::shared_ptr { + -> rendering::RenderIndexAllLayers * { if (!dev || !dev->device) { throw std::runtime_error("createRenderIndex: device handle is null"); } - return std::make_shared( + return scene.updateDelegate().emplace( scene, dev->libraryName, dev->device); }; render["getWorldBounds"] = [](std::shared_ptr dev, - std::shared_ptr index, + rendering::RenderIndexAllLayers *index, sol::this_state s) -> sol::table { if (!dev || !dev->device) { throw std::runtime_error("getWorldBounds: device handle is null"); @@ -171,7 +171,7 @@ void registerRenderBindings(sol::state &lua) [](int width, int height, std::shared_ptr dev, - std::shared_ptr index, + rendering::RenderIndexAllLayers *index, const LuaCameraSetup &camera, sol::optional rendererParams) -> std::shared_ptr { diff --git a/tsd/src/tsd/scripting/tsd.lua b/tsd/src/tsd/scripting/tsd.lua index 684f60677..4dc66b2d8 100644 --- a/tsd/src/tsd/scripting/tsd.lua +++ b/tsd/src/tsd/scripting/tsd.lua @@ -723,6 +723,8 @@ local RenderIndex = {} ---@return tsd.RenderIndex function RenderIndex.new(...) end +--- Bootstrap or rebuild this render index from the current scene snapshot. +--- This does not register the render index for live scene updates. function RenderIndex:populate() end ---@return any @@ -1137,7 +1139,7 @@ tsd.render = {} ---@return tsd.AnariDevice function tsd.render.loadDevice(libraryName) end ---- Create a render index for a scene. +--- Create a scene-owned render index for live scene updates. --- Throws if `device` is nil or invalid. ---@param scene tsd.Scene ---@param device tsd.AnariDevice diff --git a/tsd/src/tsd/ui/imgui/Application.cpp b/tsd/src/tsd/ui/imgui/Application.cpp index 03d7b0540..6835c21e8 100644 --- a/tsd/src/tsd/ui/imgui/Application.cpp +++ b/tsd/src/tsd/ui/imgui/Application.cpp @@ -862,7 +862,7 @@ void Application::teardownUsdDevice() return; tsd::core::logStatus("tearing down USD device..."); auto d = m_usdDevice.device; - m_ctx.anari.releaseRenderIndex(d); + m_ctx.anari.releaseRenderIndex(m_ctx.tsd.scene, d); anari::release(d, m_usdDevice.frame); anari::release(d, d); m_usdDevice.device = nullptr; @@ -921,7 +921,7 @@ void Application::teardownTsdDevice() return; tsd::core::logStatus("tearing down TSD device..."); auto d = m_tsdDevice.device; - m_ctx.anari.releaseRenderIndex(d); + m_ctx.anari.releaseRenderIndex(m_ctx.tsd.scene, d); anari::release(d, m_tsdDevice.frame); anari::release(d, d); m_tsdDevice.device = nullptr; diff --git a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp index a721de28e..edc53aaf1 100644 --- a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp +++ b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp @@ -565,7 +565,7 @@ void CameraPoses::renderInterpolatedPath() // Cleanup pipeline.reset(); - ctx->anari.releaseRenderIndex(d); + ctx->anari.releaseRenderIndex(scene, d); anari::release(d, c); anari::release(d, r); anari::release(d, d); diff --git a/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.cpp b/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.cpp index 78bd6425d..0fdeb7424 100644 --- a/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.cpp +++ b/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.cpp @@ -19,18 +19,25 @@ MultiDeviceViewport::MultiDeviceViewport( MultiDeviceViewport::~MultiDeviceViewport() { + auto &scene = appContext()->tsd.scene; + auto &anari = appContext()->anari; + for (size_t i = 0; i < m_cameras.size(); i++) { auto *ri = getRenderIndex(i); + if (!ri) + continue; auto d = ri->device(); auto c = m_cameras[i]; auto r = m_rud.renderers[i]; + anari.releaseRenderIndex(scene, d); anari::release(d, c); anari::release(d, r); } m_rud.renderers.clear(); m_rud.devices.clear(); + m_renderIndices.clear(); } void MultiDeviceViewport::buildUI() @@ -124,6 +131,7 @@ void MultiDeviceViewport::setLibrary(const std::string &libName) tsd::core::logStatus("[multi-viewport] creating cameras and renderers..."); + adm.setRenderIndexKind(tsd::app::RenderIndexKind::AllLayers); for (size_t i = 0; i < scene.numberOfLayers(); i++) { auto *l = scene.layer(i); auto d = devices[i]; @@ -135,6 +143,7 @@ void MultiDeviceViewport::setLibrary(const std::string &libName) ri->setIncludedLayers({l}); m_cameras.push_back(c); + m_renderIndices.push_back(ri); m_rud.devices.push_back(d); m_rud.renderers.push_back(r); @@ -199,8 +208,7 @@ void MultiDeviceViewport::loadSettings(tsd::core::DataNode &root) tsd::rendering::RenderIndexAllLayers *MultiDeviceViewport::getRenderIndex( size_t i) const { - auto &delegate = appContext()->anari.getUpdateDelegate(); - return (tsd::rendering::RenderIndexAllLayers *)delegate.get(i); + return m_renderIndices.at(i); } void MultiDeviceViewport::getSceneBounds(tsd::math::float3 boundsOut[2]) const diff --git a/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.h b/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.h index 0470c7823..d3b35736a 100644 --- a/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.h +++ b/tsd/src/tsd/ui/imgui/windows/MultiDeviceViewport.h @@ -55,6 +55,7 @@ struct MultiDeviceViewport : public Window anari::DataType m_format{ANARI_UFIXED8_RGBA_SRGB}; std::vector m_cameras; + std::vector m_renderIndices; tsd::scene::Object m_rendererObject; struct RendererUpdateDelegate : public tsd::scene::EmptyUpdateDelegate @@ -89,4 +90,4 @@ struct MultiDeviceViewport : public Window float m_resolutionScale{1.f}; }; -} // namespace tsd::ui::imgui \ No newline at end of file +} // namespace tsd::ui::imgui diff --git a/tsd/src/tsd/ui/imgui/windows/Viewport.cpp b/tsd/src/tsd/ui/imgui/windows/Viewport.cpp index b85673a7a..b856afe45 100644 --- a/tsd/src/tsd/ui/imgui/windows/Viewport.cpp +++ b/tsd/src/tsd/ui/imgui/windows/Viewport.cpp @@ -495,7 +495,7 @@ void Viewport::teardownDevice() m_outputPass = nullptr; m_saveToFilePass = nullptr; - appContext()->anari.releaseRenderIndex(m_device); + appContext()->anari.releaseRenderIndex(appContext()->tsd.scene, m_device); m_rIdx = nullptr; m_libName.clear(); diff --git a/tsd/tests/CMakeLists.txt b/tsd/tests/CMakeLists.txt index 9fb867973..7d71f2565 100644 --- a/tsd/tests/CMakeLists.txt +++ b/tsd/tests/CMakeLists.txt @@ -21,6 +21,7 @@ project_add_executable( test_Object.cpp test_ObjectUsePtr.cpp test_Parameter.cpp + test_Scene.cpp test_Token.cpp ) project_link_libraries( @@ -46,4 +47,5 @@ add_test(NAME tsd::Math COMMAND ${PROJECT_NAME} "[Math]" ) add_test(NAME tsd::Object COMMAND ${PROJECT_NAME} "[Object]" ) add_test(NAME tsd::ObjectUsePtr COMMAND ${PROJECT_NAME} "[ObjectUsePtr]" ) add_test(NAME tsd::Parameter COMMAND ${PROJECT_NAME} "[Parameter]" ) +add_test(NAME tsd::Scene COMMAND ${PROJECT_NAME} "[Scene]" ) add_test(NAME tsd::Token COMMAND ${PROJECT_NAME} "[Token]" ) diff --git a/tsd/tests/test_Scene.cpp b/tsd/tests/test_Scene.cpp new file mode 100644 index 000000000..9a303705a --- /dev/null +++ b/tsd/tests/test_Scene.cpp @@ -0,0 +1,101 @@ +// Copyright 2024-2026 NVIDIA Corporation +// SPDX-License-Identifier: Apache-2.0 + +// catch +#include "catch.hpp" +// tsd +#include "tsd/scene/Scene.hpp" +#include "tsd/scene/UpdateDelegate.hpp" + +namespace { + +struct CountingDelegate : public tsd::scene::EmptyUpdateDelegate +{ + CountingDelegate(int *objectAddedCount) : m_objectAddedCount(objectAddedCount) + {} + + void signalObjectAdded(const tsd::scene::Object *) override + { + if (m_objectAddedCount) + (*m_objectAddedCount)++; + } + + int *m_objectAddedCount{nullptr}; +}; + +} // namespace + +SCENARIO("tsd::scene::Scene owns an intrinsic update delegate root", "[Scene]") +{ + GIVEN("A scene") + { + tsd::scene::Scene scene; + auto &delegate = scene.updateDelegate(); + + THEN("The scene starts with an empty MultiUpdateDelegate root") + { + REQUIRE(delegate.size() == 0); + } + + THEN("Const and non-const accessors return the same delegate root") + { + const auto &constScene = scene; + REQUIRE(&constScene.updateDelegate() == &delegate); + } + + THEN("Registering a child delegate observes new scene-owned objects and arrays") + { + int objectAddedCount = 0; + auto *countingDelegate = scene.updateDelegate().emplace( + &objectAddedCount); + auto geometry = scene.createObject( + tsd::scene::tokens::geometry::sphere); + auto array = scene.createArray(ANARI_FLOAT32, 4); + + REQUIRE(geometry); + REQUIRE(array); + REQUIRE(countingDelegate != nullptr); + REQUIRE(objectAddedCount == 2); + } + } +} + +SCENARIO("tsd::scene::Scene delegate registration controls live signaling", + "[Scene]") +{ + GIVEN("A scene with an explicitly registered child delegate") + { + tsd::scene::Scene scene; + int objectAddedCount = 0; + auto *delegate = scene.updateDelegate().emplace( + &objectAddedCount); + + REQUIRE(scene.updateDelegate().size() == 1); + + WHEN("A new scene object is created while the delegate is registered") + { + auto geometry = scene.createObject( + tsd::scene::tokens::geometry::sphere); + + THEN("The delegate receives the object-added signal") + { + REQUIRE(geometry); + REQUIRE(objectAddedCount == 1); + } + } + + WHEN("The delegate is erased before more scene changes occur") + { + scene.updateDelegate().erase(delegate); + auto geometry = scene.createObject( + tsd::scene::tokens::geometry::sphere); + + THEN("The root becomes empty and no further signals are delivered") + { + REQUIRE(geometry); + REQUIRE(scene.updateDelegate().size() == 0); + REQUIRE(objectAddedCount == 0); + } + } + } +}