From b964dcdcd98fd7f638d8fc2fabddf0d30cd8c13f Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 13 Aug 2026 08:45:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=88=90=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Frame_Strategy_Observer.hpp | 6 +- .../renderable/Render_Frame_Completion.hpp | 2 +- .../renderable/base/Renderable_Base.cpp | 6 +- .../renderable/base/Renderable_Base.hpp | 4 +- .../src/renderive/scene/base/Scene_Base.cpp | 460 ++++++++++++------ .../src/renderive/scene/base/Scene_Base.hpp | 99 +++- .../real_time_data/Real_Time_Data_Test.cpp | 28 ++ .../renderable/base/Renderable_Base_Test.cpp | 2 +- .../Dynamic_Renderable_Lifecycle_Test.cpp | 62 ++- .../scene/Render_Plan_Execution_Test.cpp | 3 + .../renderive/scene/Scene2D_Context_Test.cpp | 4 +- .../renderive/scene/Scene3D_Context_Test.cpp | 2 +- .../renderive/scene/base/Scene_Base_Test.cpp | 61 ++- .../threading/Threading_Contract_Test.cpp | 13 +- render_2D/plottable/Afterglow.cpp | 2 +- render_2D/plottable/Afterglow.h | 2 +- render_2D/plottable/Spectrum.cpp | 2 +- render_2D/plottable/Spectrum.h | 2 +- render_2D/plottable/Waterfall.cpp | 2 +- render_2D/plottable/Waterfall.h | 2 +- render_2D/renderable/Renderable.cpp | 3 +- render_2D/renderable/Renderable.h | 2 +- render_2D/scene/Scene.h | 11 +- 23 files changed, 553 insertions(+), 227 deletions(-) diff --git a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp index 1df5ac5..306fd6f 100644 --- a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp +++ b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp @@ -51,14 +51,14 @@ public: } for (std::size_t index = 0; index < entries->size(); ++index) { const auto& entry = (*entries)[index]; - if (!entry->active.load(std::memory_order_acquire) || !entry->target->renderable_alive.load(std::memory_order_acquire)) { + if (!entry->active.load(std::memory_order_acquire) || !entry->target->renderable_alive.load(std::memory_order_acquire) || !entry->target->attached.load(std::memory_order_acquire)) { continue; } const auto& lifetime = entry->target->scene_lifetime; bool first_for_scene = true; for (std::size_t previous = 0; previous < index; ++previous) { const auto& previous_entry = (*entries)[previous]; - if (previous_entry->active.load(std::memory_order_acquire) && previous_entry->target->renderable_alive.load(std::memory_order_acquire) && previous_entry->target->scene_lifetime.get() == lifetime.get()) { + if (previous_entry->active.load(std::memory_order_acquire) && previous_entry->target->renderable_alive.load(std::memory_order_acquire) && previous_entry->target->attached.load(std::memory_order_acquire) && previous_entry->target->scene_lifetime.get() == lifetime.get()) { first_for_scene = false; break; } @@ -69,7 +69,7 @@ public: bool discard_stale_frame{}; if (observation.state.retention == Real_Time_Data_Retention::latest) { for (const auto& value : *entries) { - if (value->active.load(std::memory_order_acquire) && value->target->renderable_alive.load(std::memory_order_acquire) && value->target->scene_lifetime.get() == lifetime.get() && value->target->discard_stale_frame_on_latest_data_update.load(std::memory_order_acquire)) { + if (value->active.load(std::memory_order_acquire) && value->target->renderable_alive.load(std::memory_order_acquire) && value->target->attached.load(std::memory_order_acquire) && value->target->scene_lifetime.get() == lifetime.get() && value->target->discard_stale_frame_on_latest_data_update.load(std::memory_order_acquire)) { discard_stale_frame = true; break; } diff --git a/Kernel/src/renderive/renderable/Render_Frame_Completion.hpp b/Kernel/src/renderive/renderable/Render_Frame_Completion.hpp index d6195ae..2fc9d7f 100644 --- a/Kernel/src/renderive/renderable/Render_Frame_Completion.hpp +++ b/Kernel/src/renderive/renderable/Render_Frame_Completion.hpp @@ -8,5 +8,5 @@ public: virtual ~Render_Frame_Completion() = default; private: friend class Scene_Base; - virtual void render_frame_completed(std::uint64_t target_interval_ns) noexcept = 0; + virtual void render_frame_completed(std::uint64_t target_interval_ns) = 0; }; diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp index 799c7c5..4fe6314 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -16,6 +16,7 @@ void advance_revision(std::atomic& revision) noexcept { Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration) : memory_domain_(scene.memory_domain_), real_time_data_state_(std::allocate_shared(Scene_Memory_Allocator{memory_domain_}, scene.scene_lifetime_)), discard_stale_frame_on_latest_data_update(real_time_data_state_->discard_stale_frame_on_latest_data_update), renderable_id_(scene.allocate_renderable_id()), composite_node_id_(scene.allocate_node_id()), configuration_(configuration) {} Renderable_Base::~Renderable_Base() { + real_time_data_state_->attached.store(false, std::memory_order_release); real_time_data_state_->renderable_alive.store(false, std::memory_order_release); } void Renderable_Base::invalidate_prepare() noexcept { @@ -48,7 +49,10 @@ bool Renderable_Base::paint_cache_valid() const noexcept { painted_revision() == revision && painted_prepare_revision_.load(std::memory_order_acquire) == prepared_revision(); } -void Renderable_Base::rebuild_render_graph() noexcept { +void Renderable_Base::rebuild_render_graph() { + scene().request_render_graph_rebuild(*this); +} +void Renderable_Base::reset_render_graph() noexcept { { std::lock_guard lock(render_graph_mutex_); render_graph_.reset(); diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp index bcb5bb5..47464e0 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp @@ -24,6 +24,7 @@ private: explicit Real_Time_Data_State(std::shared_ptr scene_lifetime) : scene_lifetime(std::move(scene_lifetime)) {} std::shared_ptr scene_lifetime; std::atomic renderable_alive{true}; + std::atomic attached{}; std::atomic discard_stale_frame_on_latest_data_update{}; }; std::shared_ptr memory_domain_; @@ -50,7 +51,7 @@ protected: virtual void build_prepare_graph(Renderable_Graph_Builder& builder); virtual void build_paint_graph(Renderable_Graph_Builder& builder); virtual void prepare(const Prepare_Render_Context& context); - void rebuild_render_graph() noexcept; + void rebuild_render_graph(); [[nodiscard]] std::shared_ptr render_graph(); [[nodiscard]] static Render_State_View render_state_view() noexcept; [[nodiscard]] Real_Time_Data_Binding bind_real_time_data(Real_Time_Data_Base& data); @@ -64,6 +65,7 @@ private: void register_real_time_data(Real_Time_Data_Base& data); void unregister_real_time_data(Real_Time_Data_Base& data) noexcept; void publish_real_time_data(); + void reset_render_graph() noexcept; const Renderable_Id renderable_id_; const Render_Node_Id composite_node_id_; mutable std::mutex configuration_mutex_; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 94ccf90..700c7aa 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -312,6 +312,7 @@ Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource) dependency_topology_(memory_domain_->resource()), color_caches_(&memory_domain_->resource()), task_(memory_domain_->resource()), + mutation_queue_(&memory_domain_->resource()), composite_begin_node_id_(allocate_node_id()) { worker_ = std::thread([this] { render_loop(); }); } @@ -319,7 +320,63 @@ Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource) Scene_Base::~Scene_Base() { shutdown(); } - +void Scene_Base::Mutation::wait() const { + if (!state_) + return; + std::unique_lock lock(state_->mutex); + if (!state_->completed && state_->scene && state_->scene->is_render_worker_thread()) + throw std::logic_error("scene mutation wait is not allowed from render execution"); + state_->condition.wait(lock, [this] { return state_->completed; }); + if (state_->exception) + std::rethrow_exception(state_->exception); +} +bool Scene_Base::Mutation::ready() const { + if (!state_) + return true; + std::lock_guard lock(state_->mutex); + return state_->completed; +} +Scene_Base::Attach_Builder::Attach_Builder(Scene_Base& scene) + : scene_(&scene), task_lock_(scene.task_mutex_) { + if (scene.runtime_started_) + throw std::logic_error("scene attach builder is only available before runtime starts"); +} +void Scene_Base::Attach_Builder::attach_renderable(Renderable renderable, + Attach_Relationships relationships) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_attach_renderable_locked(std::move(renderable), std::move(relationships)); +} +void Scene_Base::Attach_Builder::set_display_parent(const Renderable& child, + const Renderable& parent) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_set_display_parent_locked(child, parent); +} +void Scene_Base::Attach_Builder::add_display_parent(const Renderable& child, + const Renderable& parent) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_add_display_parent_locked(child, parent); +} +void Scene_Base::Attach_Builder::clear_display_parent(const Renderable& child) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_clear_display_parent_locked(child); +} +void Scene_Base::Attach_Builder::set_dependency_parent(const Renderable& child, + const Renderable& parent) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_set_dependency_parent_locked(child, parent); +} +void Scene_Base::Attach_Builder::add_dependency_parent(const Renderable& child, + const Renderable& parent) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_add_dependency_parent_locked(child, parent); +} +void Scene_Base::Attach_Builder::clear_dependency_parent(const Renderable& child) { + std::lock_guard lock(scene_->model_mutex_); + scene_->apply_clear_dependency_parent_locked(child); +} +Scene_Base::Attach_Builder Scene_Base::attach_builder() { + return Attach_Builder(*this); +} void Scene_Base::render() { submit_render(nullptr); } @@ -335,6 +392,7 @@ void Scene_Base::submit_render(Abstract_Frame* frame) { return; } auto task_lock = lock_render_idle(); + runtime_started_ = true; if (pending_exception_observed_) { pending_exception_ = nullptr; pending_exception_observed_ = false; @@ -410,6 +468,14 @@ void Scene_Base::wait_for_render() { if (exception) std::rethrow_exception(exception); } +void Scene_Base::wait_for_mutations() { + if (is_render_worker_thread()) + throw std::logic_error("scene mutation wait is not allowed from render execution"); + std::unique_lock lock(task_mutex_); + render_completed_.wait(lock, [this] { + return mutation_queue_.empty() && !mutating_; + }); +} std::vector Scene_Base::relationship_ids_locked( const std::vector& renderables) const { @@ -424,184 +490,244 @@ std::vector Scene_Base::relationship_ids_locked( } return ids; } - -void Scene_Base::attach_renderable(Renderable renderable, - Attach_Relationships relationships) { +Scene_Base::Mutation Scene_Base::enqueue_mutation(std::function operation, + Mutation_Callback callback) { + std::lock_guard lock(task_mutex_); + return enqueue_mutation_locked(std::move(operation), std::move(callback)); +} +Scene_Base::Mutation Scene_Base::enqueue_mutation_locked(std::function operation, + Mutation_Callback callback) { + if (stop_) + throw std::logic_error("scene is shutting down"); + runtime_started_ = true; + auto completion = std::make_shared(*this); + mutation_queue_.push_back({std::move(operation), std::move(callback), completion}); + task_ready_.notify_one(); + return Mutation(std::move(completion)); +} +void Scene_Base::complete_mutation(Mutation_Command& command, + std::exception_ptr exception) noexcept { + { + std::lock_guard lock(command.completion->mutex); + command.completion->exception = exception; + command.completion->completed = true; + command.completion->scene = nullptr; + } + command.completion->condition.notify_all(); +} +void Scene_Base::apply_attach_renderable_locked(Renderable renderable, + Attach_Relationships relationships) { if (!renderable) throw std::invalid_argument("renderable is null"); validate_renderable_scene(*renderable); - { - std::lock_guard lock(model_mutex_); - if (is_renderable_attached_locked(renderable)) - return; - const auto display_parents = - relationship_ids_locked(relationships.display_parents); - const auto display_children = - relationship_ids_locked(relationships.display_children); - const auto dependency_parents = - relationship_ids_locked(relationships.dependency_parents); - auto cache = make_renderable_color_cache(); - const Renderable_Id id = renderable->renderable_id(); - bool display_attached{}; - bool dependency_attached{}; - bool renderable_inserted{}; - try { - dependency_topology_.attach(id, dependency_parents); - dependency_attached = true; - display_topology_.attach(id, display_parents, display_children); - display_attached = true; - if (!renderables_.try_emplace(id, renderable).second) - throw std::logic_error("renderable id is already attached"); - renderable_inserted = true; - if (cache && !color_caches_.try_emplace(id, std::move(cache)).second) - throw std::logic_error("renderable color cache is already attached"); - } catch (...) { - color_caches_.erase(id); - if (renderable_inserted) - renderables_.erase(id); - if (display_attached) - display_topology_.erase(id); - if (dependency_attached) - dependency_topology_.erase(id); - throw; - } - renderable->invalidate_prepare(); + if (is_renderable_attached_locked(renderable)) + return; + const auto display_parents = relationship_ids_locked(relationships.display_parents); + const auto display_children = relationship_ids_locked(relationships.display_children); + const auto dependency_parents = relationship_ids_locked(relationships.dependency_parents); + auto cache = make_renderable_color_cache(); + const Renderable_Id id = renderable->renderable_id(); + bool display_attached{}; + bool dependency_attached{}; + bool renderable_inserted{}; + try { + dependency_topology_.attach(id, dependency_parents); + dependency_attached = true; + display_topology_.attach(id, display_parents, display_children); + display_attached = true; + if (!renderables_.try_emplace(id, renderable).second) + throw std::logic_error("renderable id is already attached"); + renderable_inserted = true; + if (cache && !color_caches_.try_emplace(id, std::move(cache)).second) + throw std::logic_error("renderable color cache is already attached"); + } catch (...) { + color_caches_.erase(id); + if (renderable_inserted) + renderables_.erase(id); + if (display_attached) + display_topology_.erase(id); + if (dependency_attached) + dependency_topology_.erase(id); + throw; } + renderable->real_time_data_state_->attached.store(true, std::memory_order_release); + renderable->invalidate_prepare(); notify_model_dirty(); } - -void Scene_Base::detach_renderable(const Renderable& renderable) { +void Scene_Base::apply_detach_renderable_locked(const Renderable& renderable) { if (!renderable) throw std::invalid_argument("renderable is null"); validate_renderable_scene(*renderable); - bool detached{}; - { - std::lock_guard lock(model_mutex_); - if (!is_renderable_attached_locked(renderable)) - return; - const Renderable_Id id = renderable->renderable_id(); - display_topology_.detach_and_promote(id); - const auto dependency_children = - dependency_topology_.detach_and_promote(id); - for (const Renderable_Id child : dependency_children) { - if (const auto iterator = renderables_.find(child); - iterator != renderables_.end()) - iterator->second->invalidate_prepare(); - } - color_caches_.erase(id); - renderables_.erase(id); - detached = true; + if (!is_renderable_attached_locked(renderable)) + return; + const Renderable_Id id = renderable->renderable_id(); + renderable->real_time_data_state_->attached.store(false, std::memory_order_release); + display_topology_.detach_and_promote(id); + const auto dependency_children = dependency_topology_.detach_and_promote(id); + for (const Renderable_Id child : dependency_children) { + if (const auto iterator = renderables_.find(child); iterator != renderables_.end()) + iterator->second->invalidate_prepare(); } - if (detached) - notify_model_dirty(); + color_caches_.erase(id); + renderables_.erase(id); + notify_model_dirty(); } - -void Scene_Base::set_display_parent(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_set_display_parent_locked(const Renderable& child, + const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("display relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); - changed = display_topology_.replace_parents( - child->renderable_id(), {parent->renderable_id()}); - } - if (changed) + validate_renderable_attached_locked(child); + validate_renderable_attached_locked(parent); + if (display_topology_.replace_parents(child->renderable_id(), {parent->renderable_id()})) notify_model_dirty(); } - -void Scene_Base::add_display_parent(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_add_display_parent_locked(const Renderable& child, + const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("display relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); - changed = display_topology_.add_parent(child->renderable_id(), - parent->renderable_id()); - } - if (changed) + validate_renderable_attached_locked(child); + validate_renderable_attached_locked(parent); + if (display_topology_.add_parent(child->renderable_id(), parent->renderable_id())) notify_model_dirty(); } - -void Scene_Base::clear_display_parent(const Renderable& child) { +void Scene_Base::apply_clear_display_parent_locked(const Renderable& child) { if (!child) throw std::invalid_argument("display child is null"); validate_renderable_scene(*child); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - validate_renderable_attached_locked(child); - changed = display_topology_.clear_parents(child->renderable_id()); - } - if (changed) + validate_renderable_attached_locked(child); + if (display_topology_.clear_parents(child->renderable_id())) notify_model_dirty(); } - -void Scene_Base::set_dependency_parent(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_set_dependency_parent_locked(const Renderable& child, + const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("dependency relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); - changed = dependency_topology_.replace_parents( - child->renderable_id(), {parent->renderable_id()}); - if (changed) - child->invalidate_prepare(); - } - if (changed) - notify_model_dirty(); + validate_renderable_attached_locked(child); + validate_renderable_attached_locked(parent); + if (!dependency_topology_.replace_parents(child->renderable_id(), {parent->renderable_id()})) + return; + child->invalidate_prepare(); + notify_model_dirty(); } - -void Scene_Base::add_dependency_parent(const Renderable& child, - const Renderable& parent) { +void Scene_Base::apply_add_dependency_parent_locked(const Renderable& child, + const Renderable& parent) { if (!child || !parent) throw std::invalid_argument("dependency relationship endpoint is null"); validate_renderable_scene(*child); validate_renderable_scene(*parent); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - validate_renderable_attached_locked(child); - validate_renderable_attached_locked(parent); - changed = dependency_topology_.add_parent(child->renderable_id(), - parent->renderable_id()); - if (changed) - child->invalidate_prepare(); - } - if (changed) - notify_model_dirty(); + validate_renderable_attached_locked(child); + validate_renderable_attached_locked(parent); + if (!dependency_topology_.add_parent(child->renderable_id(), parent->renderable_id())) + return; + child->invalidate_prepare(); + notify_model_dirty(); } - -void Scene_Base::clear_dependency_parent(const Renderable& child) { +void Scene_Base::apply_clear_dependency_parent_locked(const Renderable& child) { if (!child) throw std::invalid_argument("dependency child is null"); validate_renderable_scene(*child); - bool changed{}; - { - std::lock_guard lock(model_mutex_); - validate_renderable_attached_locked(child); - changed = dependency_topology_.clear_parents(child->renderable_id()); - if (changed) - child->invalidate_prepare(); - } - if (changed) - notify_model_dirty(); + validate_renderable_attached_locked(child); + if (!dependency_topology_.clear_parents(child->renderable_id())) + return; + child->invalidate_prepare(); + notify_model_dirty(); +} +Scene_Base::Mutation Scene_Base::attach_renderable(Renderable renderable, + Attach_Relationships relationships, + Mutation_Callback callback) { + if (!renderable) + throw std::invalid_argument("renderable is null"); + validate_renderable_scene(*renderable); + return enqueue_mutation( + [this, renderable = std::move(renderable), relationships = std::move(relationships)]() mutable { + apply_attach_renderable_locked(std::move(renderable), std::move(relationships)); + }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::detach_renderable(const Renderable& renderable, + Mutation_Callback callback) { + if (!renderable) + throw std::invalid_argument("renderable is null"); + validate_renderable_scene(*renderable); + return enqueue_mutation([this, renderable] { apply_detach_renderable_locked(renderable); }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::set_display_parent(const Renderable& child, + const Renderable& parent, + Mutation_Callback callback) { + if (!child || !parent) + throw std::invalid_argument("display relationship endpoint is null"); + validate_renderable_scene(*child); + validate_renderable_scene(*parent); + return enqueue_mutation([this, child, parent] { apply_set_display_parent_locked(child, parent); }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::add_display_parent(const Renderable& child, + const Renderable& parent, + Mutation_Callback callback) { + if (!child || !parent) + throw std::invalid_argument("display relationship endpoint is null"); + validate_renderable_scene(*child); + validate_renderable_scene(*parent); + return enqueue_mutation([this, child, parent] { apply_add_display_parent_locked(child, parent); }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::clear_display_parent(const Renderable& child, + Mutation_Callback callback) { + if (!child) + throw std::invalid_argument("display child is null"); + validate_renderable_scene(*child); + return enqueue_mutation([this, child] { apply_clear_display_parent_locked(child); }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::set_dependency_parent(const Renderable& child, + const Renderable& parent, + Mutation_Callback callback) { + if (!child || !parent) + throw std::invalid_argument("dependency relationship endpoint is null"); + validate_renderable_scene(*child); + validate_renderable_scene(*parent); + return enqueue_mutation([this, child, parent] { apply_set_dependency_parent_locked(child, parent); }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::add_dependency_parent(const Renderable& child, + const Renderable& parent, + Mutation_Callback callback) { + if (!child || !parent) + throw std::invalid_argument("dependency relationship endpoint is null"); + validate_renderable_scene(*child); + validate_renderable_scene(*parent); + return enqueue_mutation([this, child, parent] { apply_add_dependency_parent_locked(child, parent); }, + std::move(callback)); +} +Scene_Base::Mutation Scene_Base::clear_dependency_parent(const Renderable& child, + Mutation_Callback callback) { + if (!child) + throw std::invalid_argument("dependency child is null"); + validate_renderable_scene(*child); + return enqueue_mutation([this, child] { apply_clear_dependency_parent_locked(child); }, + std::move(callback)); +} +void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) { + validate_renderable_scene(renderable); + std::unique_lock lock(task_mutex_); + if (!runtime_started_) { + renderable.reset_render_graph(); + notify_model_dirty(); + return; + } + auto owner = renderable.shared_from_this(); + static_cast(enqueue_mutation_locked([this, owner = std::move(owner)] { + owner->reset_render_graph(); + notify_model_dirty(); + }, {})); } - void Scene_Base::publish_frame_state() { auto task_lock = lock_render_idle(); if (auto* state = dynamic_cast(this)) @@ -867,7 +993,7 @@ std::unique_lock Scene_Base::lock_render_idle() { "render-idle operation is not allowed during render submission observation"); std::unique_lock lock(task_mutex_); render_completed_.wait(lock, [this] { - return !task_pending_ && !rendering_; + return !task_pending_ && !rendering_ && mutation_queue_.empty() && !mutating_; }); return lock; } @@ -886,27 +1012,75 @@ void Scene_Base::shutdown() noexcept { { std::unique_lock lock(task_mutex_); render_completed_.wait(lock, [this] { - return !task_pending_ && !rendering_; + return !task_pending_ && !rendering_ && mutation_queue_.empty() && !mutating_; }); stop_ = true; } task_ready_.notify_one(); if (worker_.joinable()) worker_.join(); + { + std::lock_guard lock(model_mutex_); + for (const auto& [id, renderable] : renderables_) { + static_cast(id); + renderable->real_time_data_state_->attached.store(false, std::memory_order_release); + } + } scene_lifetime_->invalidate(); } void Scene_Base::render_loop() { for (;;) { Render_Task task(memory_resource()); + std::vector mutations; { std::unique_lock lock(task_mutex_); - task_ready_.wait(lock, [this] { return stop_ || task_pending_; }); + task_ready_.wait(lock, [this] { + return stop_ || task_pending_ || !mutation_queue_.empty(); + }); if (stop_) return; - task = std::move(task_); - task_pending_ = false; - rendering_ = true; + if (task_pending_) { + task = std::move(task_); + task_pending_ = false; + rendering_ = true; + } else { + mutating_ = true; + mutations.reserve(mutation_queue_.size()); + while (!mutation_queue_.empty()) { + mutations.push_back(std::move(mutation_queue_.front())); + mutation_queue_.pop_front(); + } + } + } + if (!mutations.empty()) { + std::vector exceptions(mutations.size()); + { + std::lock_guard lock(model_mutex_); + for (std::size_t index = 0; index < mutations.size(); ++index) { + try { + mutations[index].operation(); + } catch (...) { + exceptions[index] = std::current_exception(); + } + } + } + for (std::size_t index = 0; index < mutations.size(); ++index) + complete_mutation(mutations[index], exceptions[index]); + { + std::lock_guard lock(task_mutex_); + mutating_ = false; + } + render_completed_.notify_all(); + for (std::size_t index = 0; index < mutations.size(); ++index) { + if (mutations[index].callback) { + try { + mutations[index].callback(exceptions[index]); + } catch (...) { + } + } + } + continue; } const auto& snapshot = *task.snapshot; observe_scene({Observation_Event::render_started, observer_now_ns(), diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index 0b1b4cb..875da47 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ private: public: using Renderable = std::shared_ptr; using Const_Renderable = std::shared_ptr; + using Mutation_Callback = std::function; struct Attach_Relationships { std::vector display_parents; @@ -50,6 +52,46 @@ public: std::vector dependency_parents; }; + class Mutation { + public: + Mutation() = default; + void wait() const; + [[nodiscard]] bool ready() const; + private: + struct State { + explicit State(Scene_Base& scene) : scene(&scene) {} + Scene_Base* scene{}; + mutable std::mutex mutex; + std::condition_variable condition; + std::exception_ptr exception; + bool completed{}; + }; + explicit Mutation(std::shared_ptr state) : state_(std::move(state)) {} + std::shared_ptr state_; + friend class Scene_Base; + }; + + class Attach_Builder { + public: + Attach_Builder(const Attach_Builder&) = delete; + Attach_Builder& operator=(const Attach_Builder&) = delete; + Attach_Builder(Attach_Builder&&) noexcept = default; + Attach_Builder& operator=(Attach_Builder&&) noexcept = default; + void attach_renderable(Renderable renderable, + Attach_Relationships relationships = {}); + void set_display_parent(const Renderable& child, const Renderable& parent); + void add_display_parent(const Renderable& child, const Renderable& parent); + void clear_display_parent(const Renderable& child); + void set_dependency_parent(const Renderable& child, const Renderable& parent); + void add_dependency_parent(const Renderable& child, const Renderable& parent); + void clear_dependency_parent(const Renderable& child); + private: + explicit Attach_Builder(Scene_Base& scene); + Scene_Base* scene_{}; + std::unique_lock task_lock_; + friend class Scene_Base; + }; + enum class Observation_Event { render_submitted, render_started, @@ -87,18 +129,28 @@ public: void render(); void render(Abstract_Frame& frame); void wait_for_render(); + void wait_for_mutations(); void publish_frame_state(); void notify_model_dirty() noexcept; - void attach_renderable(Renderable renderable, - Attach_Relationships relationships = {}); - void detach_renderable(const Renderable& renderable); - void set_display_parent(const Renderable& child, const Renderable& parent); - void add_display_parent(const Renderable& child, const Renderable& parent); - void clear_display_parent(const Renderable& child); - void set_dependency_parent(const Renderable& child, const Renderable& parent); - void add_dependency_parent(const Renderable& child, const Renderable& parent); - void clear_dependency_parent(const Renderable& child); + Attach_Builder attach_builder(); + Mutation attach_renderable(Renderable renderable, + Attach_Relationships relationships = {}, + Mutation_Callback callback = {}); + Mutation detach_renderable(const Renderable& renderable, + Mutation_Callback callback = {}); + Mutation set_display_parent(const Renderable& child, const Renderable& parent, + Mutation_Callback callback = {}); + Mutation add_display_parent(const Renderable& child, const Renderable& parent, + Mutation_Callback callback = {}); + Mutation clear_display_parent(const Renderable& child, + Mutation_Callback callback = {}); + Mutation set_dependency_parent(const Renderable& child, const Renderable& parent, + Mutation_Callback callback = {}); + Mutation add_dependency_parent(const Renderable& child, const Renderable& parent, + Mutation_Callback callback = {}); + Mutation clear_dependency_parent(const Renderable& child, + Mutation_Callback callback = {}); void set_renderable_configuration(const Renderable& renderable, Renderable_Configuration configuration); @@ -211,6 +263,12 @@ private: std::shared_ptr completion; }; + struct Mutation_Command { + std::function operation; + Mutation_Callback callback; + std::shared_ptr completion; + }; + class Execution_Context; class Render_Execution_Scope; @@ -221,6 +279,26 @@ private: }; void submit_render(Abstract_Frame* frame); + [[nodiscard]] Mutation enqueue_mutation(std::function operation, + Mutation_Callback callback); + [[nodiscard]] Mutation enqueue_mutation_locked(std::function operation, + Mutation_Callback callback); + void complete_mutation(Mutation_Command& command, + std::exception_ptr exception) noexcept; + void apply_attach_renderable_locked(Renderable renderable, + Attach_Relationships relationships); + void apply_detach_renderable_locked(const Renderable& renderable); + void apply_set_display_parent_locked(const Renderable& child, + const Renderable& parent); + void apply_add_display_parent_locked(const Renderable& child, + const Renderable& parent); + void apply_clear_display_parent_locked(const Renderable& child); + void apply_set_dependency_parent_locked(const Renderable& child, + const Renderable& parent); + void apply_add_dependency_parent_locked(const Renderable& child, + const Renderable& parent); + void apply_clear_dependency_parent_locked(const Renderable& child); + void request_render_graph_rebuild(Renderable_Base& renderable); [[nodiscard]] std::shared_ptr snapshot_live_model(); std::shared_ptr compile_render_plan( const Frame_Render_Snapshot& snapshot, Render_Task& task); @@ -252,6 +330,7 @@ private: std::condition_variable_any render_completed_; std::thread worker_; Render_Task task_; + std::pmr::deque mutation_queue_; std::shared_ptr current_completion_; std::exception_ptr pending_exception_; bool pending_exception_observed_{}; @@ -259,6 +338,8 @@ private: std::uint64_t render_sequence_{}; bool task_pending_{}; bool rendering_{}; + bool mutating_{}; + bool runtime_started_{}; bool stop_{}; std::atomic next_renderable_id_{1}; diff --git a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp index 5b0f249..3af1e53 100644 --- a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -95,6 +95,7 @@ TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) auto history = std::make_shared(); auto renderable = std::make_shared(With_Real_Time_Data(latest, history), scene); renderable->discard_stale_frame_on_latest_data_update = true; + scene.attach_renderable(renderable).wait(); { auto frame = scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); @@ -133,6 +134,8 @@ TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_d auto second = std::make_shared(With_Real_Time_Data(latest), second_scene); first->discard_stale_frame_on_latest_data_update = true; second->discard_stale_frame_on_latest_data_update = true; + first_scene.attach_renderable(first).wait(); + second_scene.attach_renderable(second).wait(); { auto frame = first_scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); @@ -144,6 +147,7 @@ TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_d latest->update(1); EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + first_scene.detach_renderable(first).wait(); first.reset(); { auto frame = second_scene.frame_control.acquire_painter(); @@ -153,6 +157,26 @@ TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_d EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 2); } +TEST(real_time_data_attachment_test, detached_renderable_no_longer_drives_frame_strategy) { + Scene2D_Context<> scene; + auto latest = std::make_shared(); + auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + renderable->discard_stale_frame_on_latest_data_update = true; + scene.attach_renderable(renderable).wait(); + { + auto frame = scene.frame_control.acquire_painter(); + ASSERT_TRUE(frame); + } + latest->update(1); + EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); + scene.detach_renderable(renderable).wait(); + { + auto frame = scene.frame_control.acquire_painter(); + ASSERT_TRUE(frame); + } + latest->update(2); + EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1); +} TEST(real_time_data_attachment_test, unbind_is_synchronized_with_concurrent_updates) { Scene2D_Context<> scene; auto latest = std::make_shared(); @@ -205,6 +229,7 @@ TEST(real_time_data_attachment_test, ignores_updates_after_bound_scene_is_destro { Scene2D_Context<> scene; renderable = std::make_shared(With_Real_Time_Data(latest), scene); + scene.attach_renderable(renderable).wait(); latest->update(1); EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1); } @@ -219,6 +244,8 @@ TEST(real_time_data_attachment_test, notifies_each_scene_once_for_one_source_upd auto second = std::make_shared(With_Real_Time_Data(latest), scene); first->discard_stale_frame_on_latest_data_update = true; second->discard_stale_frame_on_latest_data_update = true; + scene.attach_renderable(first).wait(); + scene.attach_renderable(second).wait(); { auto frame = scene.frame_control.acquire_painter(); ASSERT_TRUE(frame); @@ -261,6 +288,7 @@ TEST(real_time_data_attachment_test, frame_strategy_observer_can_reacquire_scene Scene2D_Context scene(std::move(observer_state), configuration); auto latest = std::make_shared(); auto renderable = std::make_shared(With_Real_Time_Data(latest), scene); + scene.attach_renderable(renderable).wait(); std::atomic reacquired{}; observer_data->callback = [&] { reacquired.store(&renderable->scene() == &scene, std::memory_order_release); diff --git a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp index 1978c8f..06c62ea 100644 --- a/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp +++ b/Kernel/tests/renderive/renderable/base/Renderable_Base_Test.cpp @@ -128,7 +128,7 @@ struct Renderable_Base_Render_Graph_Rebuild_Test_Renderable : Renderable_Base { std::shared_ptr graph_snapshot() { return render_graph(); } - void request_graph_rebuild() noexcept { + void request_graph_rebuild() { rebuild_render_graph(); } std::atomic build_count{}; diff --git a/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp b/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp index 016f96c..97a4fae 100644 --- a/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp +++ b/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp @@ -95,57 +95,50 @@ private: }; TEST(dynamic_renderable_lifecycle_test, - attach_during_render_is_non_blocking_and_visible_on_the_next_frame) { + attach_during_render_is_queued_until_the_current_frame_finishes) { Scene2D_Context<> scene; auto gate = std::make_shared(); auto current = std::make_shared(scene, gate); auto attached = std::make_shared(scene); - scene.attach_renderable(current); - + scene.attach_renderable(current).wait(); scene.render(); gate->wait_until_arrived(); - std::atomic attach_finished{}; - std::thread controller([&] { - scene.attach_renderable(attached); - attach_finished.store(true, std::memory_order_release); + std::atomic callback_called{}; + auto mutation = scene.attach_renderable(attached, {}, [&](std::exception_ptr exception) { + EXPECT_FALSE(exception); + callback_called.store(true, std::memory_order_release); }); - EXPECT_TRUE(wait_until([&] { return attach_finished.load(std::memory_order_acquire); })); + EXPECT_FALSE(mutation.ready()); + EXPECT_FALSE(callback_called.load(std::memory_order_acquire)); EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 0); - gate->open(); - controller.join(); scene.wait_for_render(); + mutation.wait(); + EXPECT_TRUE(wait_until([&] { return callback_called.load(std::memory_order_acquire); })); EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 0); - scene.render(); scene.wait_for_render(); EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 1); } TEST(dynamic_renderable_lifecycle_test, - detach_during_render_keeps_the_frame_owner_alive_until_execution_finishes) { + detach_during_render_waits_for_the_frame_before_releasing_scene_ownership) { Scene2D_Context<> scene; auto gate = std::make_shared(); std::atomic destroyed{}; auto renderable = std::make_shared(scene, gate, 1, &destroyed); std::weak_ptr weak = renderable; - scene.attach_renderable(renderable); - + scene.attach_renderable(renderable).wait(); scene.render(); gate->wait_until_arrived(); - std::atomic detach_finished{}; - std::thread controller([&] { - scene.detach_renderable(renderable); - detach_finished.store(true, std::memory_order_release); - }); - EXPECT_TRUE(wait_until([&] { return detach_finished.load(std::memory_order_acquire); })); - controller.join(); + auto mutation = scene.detach_renderable(renderable); + EXPECT_FALSE(mutation.ready()); renderable.reset(); EXPECT_FALSE(destroyed.load(std::memory_order_acquire)); EXPECT_FALSE(weak.expired()); - gate->open(); scene.wait_for_render(); + mutation.wait(); EXPECT_TRUE(wait_until([&] { return destroyed.load(std::memory_order_acquire); })); EXPECT_TRUE(weak.expired()); EXPECT_EQ(scene.renderable_count(), 0u); @@ -164,15 +157,16 @@ TEST(dynamic_renderable_lifecycle_test, scene.attach_renderable(child, { .dependency_parents = {first_parent} }); - + scene.wait_for_mutations(); scene.render(); first_gate->wait_until_arrived(); - scene.set_dependency_parent(child, second_parent); + auto reparent = scene.set_dependency_parent(child, second_parent); + EXPECT_FALSE(reparent.ready()); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 0); first_gate->open(); scene.wait_for_render(); + reparent.wait(); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 1); - scene.render(); second_gate->wait_until_arrived(); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 1); @@ -258,9 +252,9 @@ TEST(dynamic_renderable_lifecycle_test, scene.render(); scene.wait_for_render(); + scene.wait_for_mutations(); EXPECT_EQ(renderable->prepare_count.load(std::memory_order_acquire), 1); EXPECT_EQ(scene.renderable_count(), 0u); - scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->prepare_count.load(std::memory_order_acquire), 1); @@ -327,21 +321,25 @@ TEST(dynamic_renderable_lifecycle_test, Scene2D_Context, Tracking_Color_Cache> scene; auto gate = std::make_shared(); auto renderable = std::make_shared(scene, gate); - scene.attach_renderable(renderable); + scene.attach_renderable(renderable).wait(); ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 2); Tracking_Color_Cache::watched_instance.store(2, std::memory_order_release); - scene.render(); gate->wait_until_arrived(); - scene.detach_renderable(renderable); + auto detach = scene.detach_renderable(renderable); + auto attach = scene.attach_renderable(renderable); + EXPECT_FALSE(detach.ready()); + EXPECT_FALSE(attach.ready()); EXPECT_FALSE(Tracking_Color_Cache::watched_destroyed.load(std::memory_order_acquire)); - scene.attach_renderable(renderable); - ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 3); + EXPECT_EQ(Tracking_Color_Cache::next_instance_id.load(), 2); gate->open(); scene.wait_for_render(); + detach.wait(); + attach.wait(); EXPECT_TRUE(wait_until([] { return Tracking_Color_Cache::watched_destroyed.load(std::memory_order_acquire); })); + ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 3); scene.with_final_color_cache([](const Tracking_Color_Cache& cache) { EXPECT_EQ(cache.samples, std::vector({2})); }); @@ -446,7 +444,7 @@ TEST(dynamic_renderable_lifecycle_test, controller.join(); controls_done.store(true, std::memory_order_release); renderer.join(); - + scene.wait_for_mutations(); EXPECT_EQ(failures.load(std::memory_order_acquire), 0); EXPECT_EQ(violations.load(std::memory_order_acquire), 0); EXPECT_EQ(scene.renderable_count(), 2u); diff --git a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp index a245194..57d8ef0 100644 --- a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp +++ b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp @@ -248,18 +248,21 @@ TEST(render_plan_execution_test, logical_node_ids_are_stable_and_retired_ids_are const Render_Node_Id retired_chunk = first->graph.nodes[2].node_id; renderable->set_chunk_count(2); + scene.wait_for_mutations(); const auto unchanged = renderable->graph_snapshot(); EXPECT_EQ(unchanged->graph.nodes[0].node_id, root); EXPECT_EQ(unchanged->graph.nodes[1].node_id, first_chunk); EXPECT_EQ(unchanged->graph.nodes[2].node_id, retired_chunk); renderable->set_chunk_count(1); + scene.wait_for_mutations(); const auto reduced = renderable->graph_snapshot(); ASSERT_EQ(reduced->graph.nodes.size(), 2u); EXPECT_EQ(reduced->graph.nodes[0].node_id, root); EXPECT_EQ(reduced->graph.nodes[1].node_id, first_chunk); renderable->set_chunk_count(2); + scene.wait_for_mutations(); const auto expanded = renderable->graph_snapshot(); ASSERT_EQ(expanded->graph.nodes.size(), 3u); EXPECT_EQ(expanded->graph.nodes[0].node_id, root); diff --git a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp index 307715b..b7f41c8 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp @@ -207,11 +207,11 @@ TEST(scene2d_context_test, failed_attach_leaves_renderable_fully_detached) { Scene2D_Attach_Throwing_Cache::throw_on_construction = 2; Scene2D_Context, Scene2D_Attach_Throwing_Cache> scene; auto renderable = std::make_shared(scene); - EXPECT_THROW(scene.attach_renderable(renderable), std::runtime_error); + EXPECT_THROW(scene.attach_renderable(renderable).wait(), std::runtime_error); EXPECT_EQ(scene.renderable_count(), 0); EXPECT_TRUE(scene.topology_snapshot().renderables.empty()); Scene2D_Attach_Throwing_Cache::throw_on_construction = 0; - EXPECT_NO_THROW(scene.attach_renderable(renderable)); + EXPECT_NO_THROW(scene.attach_renderable(renderable).wait()); EXPECT_EQ(scene.renderable_count(), 1); } struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Base { diff --git a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp index 127b986..f0eb3fc 100644 --- a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp @@ -65,7 +65,7 @@ TEST(scene3d_context_test, detach_parent_promotes_children_and_invalidates_depen scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 1); - scene.detach_renderable(parent); + scene.detach_renderable(parent).wait(); const auto topology = scene.topology_snapshot(); bool display_checked{}; bool dependency_checked{}; diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index 89827a2..f83d542 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -144,7 +144,7 @@ TEST(scene_base_test, detach_releases_renderable_from_the_live_model) { scene.attach_renderable(renderable); scene.render(); scene.wait_for_render(); - scene.detach_renderable(renderable); + scene.detach_renderable(renderable).wait(); renderable.reset(); EXPECT_TRUE(weak.expired()); EXPECT_EQ(scene.renderable_count(), 0); @@ -158,11 +158,11 @@ TEST(scene_base_test, topology_rejects_unattached_renderables) { auto parent = std::make_shared(scene); auto child = std::make_shared(scene); scene.attach_renderable(child); - EXPECT_THROW(scene.set_dependency_parent(child, parent), std::invalid_argument); - EXPECT_THROW(scene.set_display_parent(child, parent), std::invalid_argument); + EXPECT_THROW(scene.set_dependency_parent(child, parent).wait(), std::invalid_argument); + EXPECT_THROW(scene.set_display_parent(child, parent).wait(), std::invalid_argument); scene.attach_renderable(parent); auto detached_child = std::make_shared(scene); - EXPECT_THROW(scene.set_dependency_parent(detached_child, parent), std::invalid_argument); + EXPECT_THROW(scene.set_dependency_parent(detached_child, parent).wait(), std::invalid_argument); } TEST(scene_base_test, setting_same_topology_parent_is_a_noop) { Scene2D_Context<> scene; @@ -170,9 +170,9 @@ TEST(scene_base_test, setting_same_topology_parent_is_a_noop) { auto child = std::make_shared(scene); scene.attach_renderable(parent); scene.attach_renderable(child); - scene.set_dependency_parent(child, parent); + scene.set_dependency_parent(child, parent).wait(); const auto revision = child->prepare_revision(); - EXPECT_NO_THROW(scene.set_dependency_parent(child, parent)); + EXPECT_NO_THROW(scene.set_dependency_parent(child, parent).wait()); EXPECT_EQ(child->prepare_revision(), revision); const auto topology = scene.topology_snapshot(); EXPECT_EQ(topology.dependency.size(), 2); @@ -195,7 +195,24 @@ TEST(scene_base_test, display_graph_rejects_cycles) { scene.attach_renderable(third); scene.set_display_parent(second, first); scene.add_display_parent(third, second); - EXPECT_THROW(scene.add_display_parent(first, third), std::invalid_argument); + EXPECT_THROW(scene.add_display_parent(first, third).wait(), std::invalid_argument); +} +TEST(scene_base_test, mutation_callback_receives_apply_failure) { + Scene2D_Context<> scene; + auto first = std::make_shared(scene); + auto second = std::make_shared(scene); + scene.attach_renderable(first).wait(); + scene.attach_renderable(second).wait(); + scene.set_display_parent(second, first).wait(); + std::atomic callback_called{}; + std::atomic callback_failed{}; + auto mutation = scene.set_display_parent(first, second, [&](std::exception_ptr exception) { + callback_failed.store(static_cast(exception), std::memory_order_release); + callback_called.store(true, std::memory_order_release); + }); + EXPECT_THROW(mutation.wait(), std::invalid_argument); + EXPECT_TRUE(wait_until([&] { return callback_called.load(std::memory_order_acquire); })); + EXPECT_TRUE(callback_failed.load(std::memory_order_acquire)); } TEST(scene_base_test, atomic_attach_can_place_existing_display_children) { Scene2D_Context<> scene; @@ -209,7 +226,7 @@ TEST(scene_base_test, atomic_attach_can_place_existing_display_children) { .display_children = {axis}, .dependency_parents = {parent, axis} }); - + scene.wait_for_mutations(); const auto topology = scene.topology_snapshot(); const auto contains = [](const auto& relationships, const auto& child, const auto& parent_value) { @@ -233,21 +250,21 @@ TEST(scene_base_test, failed_atomic_attach_rolls_back_every_topology) { .display_parents = {parent}, .display_children = {parent}, .dependency_parents = {parent} - }), std::invalid_argument); + }).wait(), std::invalid_argument); EXPECT_EQ(scene.renderable_count(), 1u); EXPECT_NO_THROW(scene.attach_renderable(renderable, { .display_parents = {parent}, .dependency_parents = {parent} - })); + }).wait()); EXPECT_EQ(scene.renderable_count(), 2u); } TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); std::weak_ptr weak = renderable; - scene.attach_renderable(renderable); + scene.attach_renderable(renderable).wait(); const auto snapshot = scene.topology_snapshot(); - scene.detach_renderable(renderable); + scene.detach_renderable(renderable).wait(); renderable.reset(); EXPECT_FALSE(weak.expired()); EXPECT_EQ(snapshot.renderables.size(), 1); @@ -259,6 +276,7 @@ TEST(scene_base_test, topology_snapshot_is_safe_during_topology_updates) { auto child = std::make_shared(scene); scene.attach_renderable(parent); scene.attach_renderable(child); + scene.wait_for_mutations(); std::thread writer([&] { for (int index = 0; index < 500; ++index) { if (index % 2 == 0) { @@ -339,6 +357,25 @@ struct Scene_Base_Render_Execution_Renderable : Renderable_Base { std::atomic nested_render_rejected{}; std::atomic mutation_completed{}; }; + +TEST(scene_base_test, attach_builder_is_synchronous_before_runtime) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + { + auto builder = scene.attach_builder(); + builder.attach_renderable(renderable); + EXPECT_EQ(scene.renderable_count(), 1u); + } + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->render_count, 1); +} +TEST(scene_base_test, attach_builder_is_rejected_after_runtime_starts) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable).wait(); + EXPECT_THROW(scene.attach_builder(), std::logic_error); +} TEST(scene_base_test, taskflow_execution_can_mutate_live_model_but_cannot_submit_nested_render) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); diff --git a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp index b35e928..7566516 100644 --- a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp +++ b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp @@ -90,7 +90,7 @@ struct Threading_Test_Render_Graph_Renderable : Renderable_Base { executed->fetch_add(1, std::memory_order_relaxed); }); } - void request_graph_rebuild() noexcept { + void request_graph_rebuild() { rebuild_render_graph(); } std::atomic* executed; @@ -534,32 +534,29 @@ TEST(threading_contract_test, scene_serializes_multiple_render_submitters_withou EXPECT_EQ(executed.load(std::memory_order_acquire), render_count); EXPECT_EQ(maximum_sequence.load(std::memory_order_acquire), render_count); } -TEST(threading_contract_test, scene_render_and_render_graph_rebuild_can_run_concurrently) { +TEST(threading_contract_test, scene_render_and_render_graph_rebuild_are_serialized_by_mutation_barrier) { Scene2D_Context<> scene; constexpr int render_count = 300; std::atomic executed{}; auto renderable = std::make_shared(scene, executed); - scene.attach_renderable(renderable); + scene.attach_renderable(renderable).wait(); std::atomic start{}; - std::atomic renderer_done{}; std::thread renderer([&] { wait_start(start); for (int index = 0; index < render_count; ++index) { scene.render(); scene.wait_for_render(); } - renderer_done.store(true, std::memory_order_release); }); std::thread rebuilder([&] { wait_start(start); - while (!renderer_done.load(std::memory_order_acquire)) { + for (int index = 0; index < render_count; ++index) renderable->request_graph_rebuild(); - std::this_thread::yield(); - } }); start.store(true, std::memory_order_release); renderer.join(); rebuilder.join(); + scene.wait_for_mutations(); EXPECT_EQ(executed.load(std::memory_order_acquire), render_count); } TEST(threading_contract_test, real_time_data_updates_can_race_with_scene_destruction) { diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index bfbd9aa..60a50d9 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -256,7 +256,7 @@ void Afterglow_Control::paint_render_frame(Painter& painter) { } void Afterglow_Control::render_frame_completed( - std::uint64_t target_interval_ns) noexcept { + std::uint64_t target_interval_ns) { const auto& output = impl_->prepare_buffer; if (!output.valid || !is_visible()) return; diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h index ffd03e7..16a0b0e 100644 --- a/render_2D/plottable/Afterglow.h +++ b/render_2D/plottable/Afterglow.h @@ -45,7 +45,7 @@ private: void normalize_render_frame(); void color_partition(int partition_index); void paint_render_frame(Painter& painter); - void render_frame_completed(std::uint64_t target_interval_ns) noexcept override; + void render_frame_completed(std::uint64_t target_interval_ns) override; void publish() override; }; } diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index f43f962..b9899cd 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -533,7 +533,7 @@ void Spectrum_Control::paint_overlay(Painter& painter, } void Spectrum_Control::render_frame_completed( - std::uint64_t target_interval_ns) noexcept { + std::uint64_t target_interval_ns) { const auto& output = impl_->prepare_buffer; if (!output.valid || !is_visible()) return; diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h index 429f1bf..75636f7 100644 --- a/render_2D/plottable/Spectrum.h +++ b/render_2D/plottable/Spectrum.h @@ -78,7 +78,7 @@ private: const Render_State_View& state); void paint_background(Painter& painter, const Render_State_View& state); void paint_overlay(Painter& painter, const Render_State_View& state); - void render_frame_completed(std::uint64_t target_interval_ns) noexcept override; + void render_frame_completed(std::uint64_t target_interval_ns) override; void publish() override; }; } diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index a7d03e0..adfbee8 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -279,7 +279,7 @@ void Waterfall_Control::paint_render_frame(Painter& painter, } void Waterfall_Control::render_frame_completed( - std::uint64_t target_interval_ns) noexcept { + std::uint64_t target_interval_ns) { const auto& output = impl_->prepare_buffer; if (!output.valid || !is_visible()) return; diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h index b92eeb1..a1fe9c3 100644 --- a/render_2D/plottable/Waterfall.h +++ b/render_2D/plottable/Waterfall.h @@ -50,7 +50,7 @@ private: std::unique_ptr impl_; void prepare_render_frame(const Render_State_View& state, int graph_partition_count); void paint_render_frame(Painter& painter, const Render_State_View& state); - void render_frame_completed(std::uint64_t target_interval_ns) noexcept override; + void render_frame_completed(std::uint64_t target_interval_ns) override; void render_partition(int partition_index); void publish() override; }; diff --git a/render_2D/renderable/Renderable.cpp b/render_2D/renderable/Renderable.cpp index e4a64fa..627991d 100644 --- a/render_2D/renderable/Renderable.cpp +++ b/render_2D/renderable/Renderable.cpp @@ -114,9 +114,8 @@ void Renderable::paint_changed() noexcept { scene().notify_model_dirty(); } -void Renderable::render_graph_changed() noexcept { +void Renderable::render_graph_changed() { rebuild_render_graph(); - scene().notify_model_dirty(); } } // namespace renderive diff --git a/render_2D/renderable/Renderable.h b/render_2D/renderable/Renderable.h index e0884fa..1fe4c5d 100644 --- a/render_2D/renderable/Renderable.h +++ b/render_2D/renderable/Renderable.h @@ -84,7 +84,7 @@ protected: Paint_Task_Function function); void changed() noexcept; void paint_changed() noexcept; - void render_graph_changed() noexcept; + void render_graph_changed(); private: void observe_state(Renderable_Observer_Event event, std::uint64_t time_ns, diff --git a/render_2D/scene/Scene.h b/render_2D/scene/Scene.h index c064269..087975a 100644 --- a/render_2D/scene/Scene.h +++ b/render_2D/scene/Scene.h @@ -209,7 +209,8 @@ public: return; auto root = detail::make_renderable_group(*this, true); root->set_object_name("root"); - this->attach_renderable(root); + auto builder = this->attach_builder(); + builder.attach_renderable(root); } [[nodiscard]] std::shared_ptr root_renderable() const { const auto topology = this->topology_snapshot(); @@ -235,10 +236,12 @@ public: attach_renderable_node(renderable, parent); return renderable; } - void remove_renderable(const std::shared_ptr& renderable) { + ::Scene_Base::Mutation remove_renderable( + const std::shared_ptr& renderable, + ::Scene_Base::Mutation_Callback callback = {}) { if (!renderable || renderable == root_renderable()) - return; - this->detach_renderable(renderable); + return {}; + return this->detach_renderable(renderable, std::move(callback)); } void set_background_color(Color color) { if (background_color() == color)