#include "render_2D/scene/Scene_Context.hpp" #include #include #include #include #include #include #include #include #include #include #include "renderive/renderable/Renderable.hpp" #include "renderive/renderable/Renderable_Test_Harness.hpp" #include "renderive/scene/Scene.hpp" #include "Scene_Test_Helpers.hpp" namespace { using namespace std::chrono_literals; class Gate { public: void arrive_and_wait() { std::unique_lock lock(mutex_); arrived_ = true; condition_.notify_all(); condition_.wait(lock, [this] { return open_; }); } void wait_until_arrived() { std::unique_lock lock(mutex_); ASSERT_TRUE(condition_.wait_for(lock, 2s, [this] { return arrived_; })); } void open() { std::lock_guard lock(mutex_); open_ = true; condition_.notify_all(); } private: std::mutex mutex_; std::condition_variable condition_; bool arrived_{}; bool open_{}; }; template bool wait_until(Predicate&& predicate) { const auto deadline = std::chrono::steady_clock::now() + 2s; while (!predicate()) { if (std::chrono::steady_clock::now() >= deadline) return false; std::this_thread::yield(); } return true; } class Counting_Renderable : public Renderable_Test_Harness { public: Counting_Renderable() : Renderable_Test_Harness({.cache_enabled = false}) {} std::atomic prepare_count{}; protected: void prepare_for_test(const Prepare_Render_Context&) override { prepare_count.fetch_add(1, std::memory_order_release); } }; class Blocking_Renderable : public Counting_Renderable { public: Blocking_Renderable(std::shared_ptr gate, int block_on_call = 1, std::atomic* destroyed = nullptr) : gate_(std::move(gate)), block_on_call_(block_on_call), destroyed_(destroyed) {} ~Blocking_Renderable() override { if (destroyed_) destroyed_->store(true, std::memory_order_release); } protected: void prepare_for_test(const Prepare_Render_Context&) override { const int call = prepare_count.fetch_add(1, std::memory_order_acq_rel) + 1; if (call == block_on_call_) gate_->arrive_and_wait(); } private: std::shared_ptr gate_; int block_on_call_{}; std::atomic* destroyed_{}; }; TEST(dynamic_renderable_lifecycle_test, edit_waits_until_current_frame_finishes) { Scene2D_Context<> scene; auto gate = std::make_shared(); auto current = renderive_Owner::make(gate); auto added = renderive_Owner::make(); attach_initial(scene, current); scene.render(); gate->wait_until_arrived(); std::atomic edited{}; scene.edit_renderables([added, &edited](auto& editor) { editor.attach(added); edited.store(true, std::memory_order_release); }); EXPECT_FALSE(edited.load(std::memory_order_acquire)); gate->open(); scene.wait_for_render(); EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); EXPECT_EQ(added->prepare_count.load(std::memory_order_acquire), 0); scene.render(); scene.wait_for_render(); EXPECT_EQ(added->prepare_count.load(std::memory_order_acquire), 1); } TEST(dynamic_renderable_lifecycle_test, detach_waits_until_current_frame_finishes_before_releasing_scene_ownership) { Scene2D_Context<> scene; auto gate = std::make_shared(); std::atomic destroyed{}; auto renderable = renderive_Owner::make(gate, 1, &destroyed); std::weak_ptr weak = renderable.share(); attach_initial(scene, renderable); scene.render(); gate->wait_until_arrived(); std::atomic edited{}; scene.edit_renderables([renderable, &edited](auto& editor) { editor.detach(renderable); edited.store(true, std::memory_order_release); }); renderable.reset(); EXPECT_FALSE(destroyed.load(std::memory_order_acquire)); gate->open(); scene.wait_for_render(); EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); EXPECT_TRUE(wait_until([&] { return destroyed.load(std::memory_order_acquire); })); EXPECT_TRUE(weak.expired()); EXPECT_EQ(scene.renderable_count(), 0u); } TEST(dynamic_renderable_lifecycle_test, reparent_edit_changes_only_the_next_frame_dependency_plan) { Scene2D_Context<> scene; auto first_gate = std::make_shared(); auto second_gate = std::make_shared(); auto first_parent = renderive_Owner::make(first_gate, 1); auto second_parent = renderive_Owner::make(second_gate, 2); auto child = renderive_Owner::make(); { auto attach = scene.attach_builder(); attach.attach(first_parent); attach.attach(second_parent); attach.attach(child); attach.set_dependency_parent(child, first_parent); } scene.render(); first_gate->wait_until_arrived(); std::atomic edited{}; scene.edit_renderables([child, second_parent, &edited](auto& editor) { editor.set_dependency_parent(child, second_parent); edited.store(true, std::memory_order_release); }); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 0); first_gate->open(); scene.wait_for_render(); EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); 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); second_gate->open(); scene.wait_for_render(); EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 2); } class Rebuildable_Renderable final : public Renderable_Test_Harness { public: explicit Rebuildable_Renderable(std::shared_ptr first_graph_gate) : Renderable_Test_Harness({.cache_enabled = false}), first_graph_gate_(std::move(first_graph_gate)) {} void publish_second_graph() { graph_version_.store(2, std::memory_order_release); rebuild_render_graph_for_test(); } std::vector executions() const { std::lock_guard lock(executions_mutex_); return executions_; } protected: void build_prepare_graph_for_test(Renderable_Graph_Builder& builder) override { const int version = graph_version_.load(std::memory_order_acquire); builder.emplace("versioned_prepare", version == 1 ? "Graph A" : "Graph B", [this, version](const Prepare_Render_Context&) { if (version == 1) first_graph_gate_->arrive_and_wait(); std::lock_guard lock(executions_mutex_); executions_.push_back(version); }); } private: std::shared_ptr first_graph_gate_; std::atomic graph_version_{1}; mutable std::mutex executions_mutex_; std::vector executions_; }; TEST(dynamic_renderable_lifecycle_test, graph_rebuild_uses_the_same_no_inflight_frame_barrier) { Scene2D_Context<> scene; auto gate = std::make_shared(); auto renderable = renderive_Owner::make(gate); attach_initial(scene, renderable); scene.render(); gate->wait_until_arrived(); renderable->publish_second_graph(); gate->open(); scene.wait_for_render(); EXPECT_EQ(renderable->executions(), std::vector({1})); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->executions(), std::vector({1, 2})); } class Sequence_Parent final : public Renderable_Test_Harness { public: explicit Sequence_Parent(std::atomic& completed_sequence) : Renderable_Test_Harness({.cache_enabled = false}), completed_sequence_(completed_sequence) {} protected: void prepare_for_test(const Prepare_Render_Context& context) override { completed_sequence_.store(context.frame.render_sequence, std::memory_order_release); } private: std::atomic& completed_sequence_; }; class Sequence_Child final : public Renderable_Test_Harness { public: Sequence_Child(std::atomic& parent_sequence, std::atomic& violations) : Renderable_Test_Harness({.cache_enabled = false}), parent_sequence_(parent_sequence), violations_(violations) {} protected: void prepare_for_test(const Prepare_Render_Context& context) override { if (parent_sequence_.load(std::memory_order_acquire) != context.frame.render_sequence) violations_.fetch_add(1, std::memory_order_relaxed); } private: std::atomic& parent_sequence_; std::atomic& violations_; }; TEST(dynamic_renderable_lifecycle_test, one_runtime_callback_can_apply_a_complete_relationship_change) { Scene2D_Context<> scene; std::atomic parent_sequence{}; std::atomic violations{}; auto parent = renderive_Owner::make(parent_sequence); auto child = renderive_Owner::make(parent_sequence, violations); attach_initial(scene, parent); wait_renderable_edit(scene, [parent, child](auto& editor) { editor.attach(child); editor.set_dependency_parent(child, parent); editor.set_display_parent(child, parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(violations.load(std::memory_order_acquire), 0); wait_renderable_edit(scene, [child](auto& editor) { editor.clear_dependency_parent(child); editor.clear_display_parent(child); editor.detach(child); }); EXPECT_EQ(scene.renderable_count(), 1u); } TEST(dynamic_renderable_lifecycle_test, queued_edits_and_render_submission_preserve_complete_topologies) { Scene2D_Context<> scene; std::atomic parent_sequence{}; std::atomic violations{}; auto parent = renderive_Owner::make(parent_sequence); attach_initial(scene, parent); constexpr int iterations = 200; for (int index = 0; index < iterations; ++index) { auto child = renderive_Owner::make(parent_sequence, violations); scene.edit_renderables([parent, child](auto& editor) { editor.attach(child); editor.set_dependency_parent(child, parent); editor.set_display_parent(child, parent); }); scene.edit_renderables([child](auto& editor) { editor.clear_dependency_parent(child); editor.clear_display_parent(child); editor.detach(child); }); } scene.render(); scene.wait_for_render(); EXPECT_EQ(violations.load(std::memory_order_acquire), 0); EXPECT_EQ(scene.renderable_count(), 1u); } TEST(dynamic_renderable_lifecycle_test, captures_before_and_after_runtime_attach_keep_both_plan_versions_resolvable) { Scene2D_Context<> scene; auto first = renderive_Owner::make(); auto second = renderive_Owner::make(); attach_initial(scene, first); const auto request = scene.capture_frames(2); ASSERT_TRUE(request); const Capture_Session_Id session_id = request.session_id; scene.render(); scene.wait_for_render(); wait_renderable_edit(scene, [second](auto& editor) { editor.attach(second); }); scene.render(); scene.wait_for_render(); const auto session = scene.capture_session(session_id); ASSERT_TRUE(session); ASSERT_EQ(session->frames.size(), 2u); const auto first_version = session->frames[0].snapshot->render_plan_version; const auto second_version = session->frames[1].snapshot->render_plan_version; EXPECT_NE(first_version, second_version); const auto first_plan = scene.find_render_plan(first_version); const auto second_plan = scene.find_render_plan(second_version); ASSERT_TRUE(first_plan); ASSERT_TRUE(second_plan); EXPECT_TRUE(std::ranges::none_of(first_plan->graph.nodes, [&](const Render_Node& node) { return node.owner_id == second->renderable_id(); })); EXPECT_TRUE(std::ranges::any_of(second_plan->graph.nodes, [&](const Render_Node& node) { return node.owner_id == second->renderable_id(); })); } }