#include #include #include #include #include #include #include #include #include #include #include #include "renderive/renderable/Renderable.hpp" #include "renderive/renderable/base/Renderable_Base_p.hpp" #include "renderive/scene/Scene.hpp" #include "renderive/scene/Scene_Test_Helpers.hpp" namespace { template bool wait_until(Predicate&& predicate) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!predicate()) { if (std::chrono::steady_clock::now() >= deadline) return false; std::this_thread::yield(); } return true; } } struct Scene_Base_Test_Renderable final : Renderable_Base { private: class Impl final : public Renderable_Base::Impl { void prepare(const Prepare_Render_Context& context) override { auto& renderable = static_cast(owner()); renderable.render_sequence = context.frame.render_sequence; ++renderable.render_count; } }; public: explicit Scene_Base_Test_Renderable(bool cache_enabled = false) : Renderable_Base(std::make_unique(), {.cache_enabled = cache_enabled}) {} void invalidate_prepare_for_test() { d_func().invalidate_prepare(); } std::uint64_t render_sequence{}; int render_count{}; }; struct Scene_Base_Throwing_Renderable final : Renderable_Base { private: class Impl final : public Renderable_Base::Impl { void prepare(const Prepare_Render_Context&) override { throw std::runtime_error("render failed"); } }; public: Scene_Base_Throwing_Renderable() : Renderable_Base(std::make_unique(), {.cache_enabled = false}) {} }; struct Scene_Base_Blocking_Renderable final : Renderable_Base { private: class Impl final : public Renderable_Base::Impl { void prepare(const Prepare_Render_Context&) override { auto& renderable = static_cast(owner()); std::unique_lock lock(renderable.mutex); renderable.rendering = true; renderable.condition.notify_all(); renderable.condition.wait(lock, [&renderable] { return renderable.released; }); } }; public: Scene_Base_Blocking_Renderable() : Renderable_Base(std::make_unique(), {.cache_enabled = false}) {} void set_cache_enabled_for_test(bool enabled) { d_func().set_configuration({.cache_enabled = enabled}); } bool cache_enabled_for_test() const { return d_func().configuration.load(std::memory_order_acquire).cache_enabled; } void wait_rendering() { std::unique_lock lock(mutex); condition.wait(lock, [this] { return rendering; }); } void release() { std::lock_guard lock(mutex); released = true; condition.notify_all(); } std::mutex mutex; std::condition_variable condition; bool rendering{}; bool released{}; }; TEST(scene_base_test, attach_builder_is_synchronous_before_runtime) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); { auto builder = scene.attach_builder(); builder.attach(renderable); EXPECT_EQ(scene.renderable_count(), 1u); } scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count, 1); EXPECT_EQ(renderable->render_sequence, 1); } TEST(scene_base_test, attach_builder_can_build_complete_initial_topology) { Scene2D_Context<> scene; auto parent = renderive_Owner::make(); auto child = renderive_Owner::make(); { auto builder = scene.attach_builder(); builder.attach(parent); builder.attach(child); builder.set_display_parent(child, parent); builder.set_dependency_parent(child, parent); } const auto topology = scene.topology_snapshot(); EXPECT_EQ(topology.renderables.size(), 2u); EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) { return relation.child.get() == child.get() && relation.parent.get() == parent.get(); })); EXPECT_TRUE(std::ranges::any_of(topology.dependency, [&](const auto& relation) { return relation.child.get() == child.get() && relation.parent.get() == parent.get(); })); } TEST(scene_base_test, attach_builder_rejects_null_renderable) { Scene2D_Context<> scene; auto builder = scene.attach_builder(); EXPECT_THROW(builder.attach({}), std::invalid_argument); } TEST(scene_base_test, one_runtime_callback_attaches_multiple_renderables) { Scene2D_Context<> scene; auto first = renderive_Owner::make(); auto second = renderive_Owner::make(); auto third = renderive_Owner::make(); wait_renderable_edit(scene, [first, second, third](auto& editor) { editor.attach(first); editor.attach(second); editor.attach(third); }); EXPECT_EQ(scene.renderable_count(), 3u); EXPECT_EQ(scene.topology_snapshot().renderables.size(), 3u); } TEST(scene_base_test, repeated_attach_of_same_owner_keeps_existing_attachment) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); wait_renderable_edit(scene, [renderable](auto& editor) { editor.attach(renderable); editor.attach(renderable); }); EXPECT_EQ(scene.renderable_count(), 1u); } TEST(scene_base_test, attach_builder_is_rejected_after_runtime_starts) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_THROW(scene.attach_builder(), std::logic_error); } TEST(scene_base_test, edit_renderables_waits_for_inflight_frame) { Scene2D_Context<> scene; auto blocking = renderive_Owner::make(); auto added = renderive_Owner::make(); attach_initial(scene, blocking); scene.render(); blocking->wait_rendering(); 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)); blocking->release(); scene.wait_for_render(); EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); })); EXPECT_EQ(added->render_count, 0); scene.render(); scene.wait_for_render(); EXPECT_EQ(added->render_count, 1); } TEST(scene_base_test, one_runtime_edit_can_rewire_and_detach_without_internal_repair) { Scene2D_Context<> scene; auto first_parent = renderive_Owner::make(); auto second_parent = renderive_Owner::make(); auto child = renderive_Owner::make(); with_attach_builder(scene, [&](auto& builder) { builder.attach(first_parent); builder.attach(second_parent); builder.attach(child); builder.set_display_parent(child, first_parent); builder.set_dependency_parent(child, first_parent); }); wait_renderable_edit(scene, [first_parent, second_parent, child](auto& editor) { editor.set_display_parent(child, second_parent); editor.set_dependency_parent(child, second_parent); editor.detach(first_parent); }); const auto topology = scene.topology_snapshot(); EXPECT_EQ(topology.renderables.size(), 2u); EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) { return relation.child.get() == child.get() && relation.parent.get() == second_parent.get(); })); } TEST(scene_base_test, one_runtime_edit_can_clear_detach_attach_and_relate) { Scene2D_Context<> scene; auto old_parent = renderive_Owner::make(); auto replacement = renderive_Owner::make(); auto child = renderive_Owner::make(); with_attach_builder(scene, [&](auto& builder) { builder.attach(old_parent); builder.attach(child); builder.set_display_parent(child, old_parent); builder.set_dependency_parent(child, old_parent); }); wait_renderable_edit(scene, [old_parent, replacement, child](auto& editor) { editor.clear_display_parent(child); editor.clear_dependency_parent(child); editor.detach(old_parent); editor.attach(replacement); editor.set_display_parent(child, replacement); editor.set_dependency_parent(child, replacement); }); const auto topology = scene.topology_snapshot(); EXPECT_EQ(topology.renderables.size(), 2u); EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) { return relation.child.get() == child.get() && relation.parent.get() == replacement.get(); })); EXPECT_TRUE(std::ranges::any_of(topology.dependency, [&](const auto& relation) { return relation.child.get() == child.get() && relation.parent.get() == replacement.get(); })); } TEST(scene_base_test, callback_may_be_temporarily_invalid_when_final_structure_is_valid) { Scene2D_Context<> scene; auto old_parent = renderive_Owner::make(); auto replacement = renderive_Owner::make(); auto child = renderive_Owner::make(); with_attach_builder(scene, [&](auto& builder) { builder.attach(old_parent); builder.attach(child); builder.set_display_parent(child, old_parent); builder.set_dependency_parent(child, old_parent); }); wait_renderable_edit(scene, [old_parent, replacement, child](auto& editor) { editor.detach(old_parent); editor.attach(replacement); editor.set_display_parent(child, replacement); editor.set_dependency_parent(child, replacement); }); const auto topology = scene.topology_snapshot(); EXPECT_EQ(topology.renderables.size(), 2u); EXPECT_TRUE(std::ranges::none_of(topology.renderables, [&](const auto& owner) { return owner.get() == old_parent.get(); })); } TEST(scene_base_test, detached_renderable_cannot_migrate_to_another_scene) { Scene2D_Context<> first_scene; Scene2D_Context<> second_scene; auto renderable = renderive_Owner::make(); attach_initial(first_scene, renderable); wait_renderable_edit(first_scene, [renderable](auto& editor) { editor.detach(renderable); }); EXPECT_EQ(first_scene.renderable_count(), 0u); auto builder = second_scene.attach_builder(); EXPECT_THROW(builder.attach(renderable), std::invalid_argument); } TEST(scene_base_test, dependency_edit_invalidates_cached_child_prepare) { Scene2D_Context<> scene; auto parent = renderive_Owner::make(); auto child = renderive_Owner::make(true); attach_initial(scene, parent, child); scene.render(); scene.wait_for_render(); scene.render(); scene.wait_for_render(); ASSERT_EQ(child->render_count, 1); wait_renderable_edit(scene, [parent, child](auto& editor) { editor.set_dependency_parent(child, parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 2); } TEST(scene_base_test, display_edit_changes_paint_order) { Scene2D_Context<> scene; auto first_parent = renderive_Owner::make(); auto second_parent = renderive_Owner::make(); auto child = renderive_Owner::make(); attach_initial(scene, first_parent, second_parent, child); wait_renderable_edit(scene, [second_parent, child](auto& editor) { editor.set_display_parent(child, second_parent); }); const auto first_order = scene.paint_order_snapshot(); EXPECT_LT(std::ranges::find(first_order, second_parent), std::ranges::find(first_order, child)); wait_renderable_edit(scene, [first_parent, child](auto& editor) { editor.set_display_parent(child, first_parent); }); const auto second_order = scene.paint_order_snapshot(); EXPECT_LT(std::ranges::find(second_order, first_parent), std::ranges::find(second_order, child)); } TEST(scene_base_test, detach_releases_scene_ownership_after_edit_applies) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); std::weak_ptr weak = renderable.share(); attach_initial(scene, renderable); wait_renderable_edit(scene, [renderable](auto& editor) { editor.detach(renderable); }); renderable.reset(); EXPECT_TRUE(wait_until([&] { return weak.expired(); })); EXPECT_EQ(scene.renderable_count(), 0u); } TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); std::weak_ptr weak = renderable.share(); attach_initial(scene, renderable); const auto snapshot = scene.topology_snapshot(); wait_renderable_edit(scene, [renderable](auto& editor) { editor.detach(renderable); }); renderable.reset(); EXPECT_FALSE(weak.expired()); EXPECT_EQ(snapshot.renderables.size(), 1u); EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get()); } TEST(scene_base_test, non_structural_configuration_update_does_not_wait_for_render) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.render(); renderable->wait_rendering(); std::atomic updated{}; std::thread writer([&] { renderable->set_cache_enabled_for_test(true); updated.store(true, std::memory_order_release); }); EXPECT_TRUE(wait_until([&] { return updated.load(std::memory_order_acquire); })); renderable->release(); writer.join(); scene.wait_for_render(); EXPECT_TRUE(renderable->cache_enabled_for_test()); } TEST(scene_base_test, concurrent_render_and_runtime_structure_edits_are_serialized_by_worker) { Scene2D_Context<> scene; auto parent = renderive_Owner::make(); auto child = renderive_Owner::make(); attach_initial(scene, parent, child); std::thread renderer([&] { for (int index = 0; index < 50; ++index) { scene.render(); scene.wait_for_render(); } }); std::thread editor([&] { for (int index = 0; index < 50; ++index) { scene.edit_renderables([parent, child, index](auto& edit) { if (index % 2 == 0) { edit.set_dependency_parent(child, parent); edit.set_display_parent(child, parent); } else { edit.clear_dependency_parent(child); edit.clear_display_parent(child); } }); } }); renderer.join(); editor.join(); scene.render(); scene.wait_for_render(); EXPECT_EQ(scene.renderable_count(), 2u); } TEST(scene_base_test, render_submission_waits_while_edit_callback_is_running) { Scene2D_Context<> scene; std::promise callback_entered; auto callback_entered_future = callback_entered.get_future(); std::promise release_callback; auto release_callback_future = release_callback.get_future().share(); scene.edit_renderables([&](auto&) { callback_entered.set_value(); release_callback_future.wait(); }); callback_entered_future.wait(); std::atomic renderer_started{}; std::atomic render_submitted{}; std::thread renderer([&] { renderer_started.store(true, std::memory_order_release); scene.render(); render_submitted.store(true, std::memory_order_release); }); EXPECT_TRUE(wait_until([&] { return renderer_started.load(std::memory_order_acquire); })); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(render_submitted.load(std::memory_order_acquire)); release_callback.set_value(); renderer.join(); scene.wait_for_render(); EXPECT_TRUE(render_submitted.load(std::memory_order_acquire)); } TEST(scene_base_test, initialization_and_runtime_edit_build_equivalent_plan_shapes) { Scene2D_Context<> initialized_scene; auto initialized_parent = renderive_Owner::make(); auto initialized_child = renderive_Owner::make(); with_attach_builder(initialized_scene, [&](auto& builder) { builder.attach(initialized_parent); builder.attach(initialized_child); builder.set_display_parent(initialized_child, initialized_parent); builder.set_dependency_parent(initialized_child, initialized_parent); }); Scene2D_Context<> edited_scene; auto edited_parent = renderive_Owner::make(); auto edited_child = renderive_Owner::make(); wait_renderable_edit(edited_scene, [edited_parent, edited_child](auto& editor) { editor.attach(edited_parent); editor.attach(edited_child); editor.set_display_parent(edited_child, edited_parent); editor.set_dependency_parent(edited_child, edited_parent); }); initialized_scene.render(); edited_scene.render(); initialized_scene.wait_for_render(); edited_scene.wait_for_render(); const auto initialized_plan = initialized_scene.render_plan_snapshot(); const auto edited_plan = edited_scene.render_plan_snapshot(); ASSERT_TRUE(initialized_plan); ASSERT_TRUE(edited_plan); ASSERT_EQ(initialized_plan->graph.nodes.size(), edited_plan->graph.nodes.size()); std::unordered_map initialized_indices; std::unordered_map edited_indices; for (std::size_t index = 0; index < initialized_plan->graph.nodes.size(); ++index) { const auto& initialized_node = initialized_plan->graph.nodes[index]; const auto& edited_node = edited_plan->graph.nodes[index]; EXPECT_EQ(initialized_node.name, edited_node.name); EXPECT_EQ(initialized_node.kind, edited_node.kind); initialized_indices.emplace(initialized_node.node_id, index); edited_indices.emplace(edited_node.node_id, index); } std::vector> initialized_edges; std::vector> edited_edges; for (const auto& edge : initialized_plan->graph.edges) initialized_edges.emplace_back(initialized_indices.at(edge.from), initialized_indices.at(edge.to)); for (const auto& edge : edited_plan->graph.edges) edited_edges.emplace_back(edited_indices.at(edge.from), edited_indices.at(edge.to)); std::ranges::sort(initialized_edges); std::ranges::sort(edited_edges); EXPECT_EQ(initialized_edges, edited_edges); } TEST(scene_base_test, detached_topology_reference_fails_fast_after_callback) { EXPECT_DEATH( { Scene2D_Context<> scene; auto parent = renderive_Owner::make(); auto child = renderive_Owner::make(); with_attach_builder(scene, [&](auto& builder) { builder.attach(parent); builder.attach(child); builder.set_dependency_parent(child, parent); }); scene.edit_renderables([parent](auto& editor) { editor.detach(parent); }); scene.render(); }, "dependency graph still references detached renderable"); } TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.render(); EXPECT_THROW(scene.wait_for_render(), std::runtime_error); } TEST(scene_base_test, scene_destruction_does_not_throw_after_background_render_failure) { { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.render(); } EXPECT_TRUE(true); } TEST(scene_base_test, all_waiters_receive_the_same_render_failure) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.render(); std::atomic failures{}; auto waiter = [&] { try { scene.wait_for_render(); } catch (const std::runtime_error&) { failures.fetch_add(1, std::memory_order_relaxed); } }; std::thread first(waiter); std::thread second(waiter); first.join(); second.join(); EXPECT_EQ(failures.load(), 2); }