#include #include #include #include #include #include #include #include #include #include "renderive/renderable/Renderable.hpp" #include "renderive/scene/Scene.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 : Renderable_Base { explicit Scene_Base_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {} void prepare(const Prepare_Render_Context& context) override { render_sequence = context.frame.render_sequence; ++render_count; } std::uint64_t render_sequence{}; int render_count{}; }; TEST(scene_base_test, publishes_renderable_collection_before_background_render) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); scene.attach_renderable(renderable); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count, 1); EXPECT_EQ(renderable->render_sequence, 1); EXPECT_EQ(scene.renderable_count(), 1); } struct Scene_Base_Throwing_Renderable : Renderable_Base { explicit Scene_Base_Throwing_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { throw std::runtime_error("render failed"); } }; TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); scene.attach_renderable(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 = std::make_shared(scene); scene.attach_renderable(renderable); scene.render(); } EXPECT_TRUE(true); } struct Scene_Base_Blocking_Renderable : Renderable_Base { explicit Scene_Base_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { std::unique_lock lock(mutex); rendering = true; condition.notify_all(); condition.wait(lock, [this] { return released; }); } 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, scene_model_mutation_does_not_wait_for_background_render) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); scene.attach_renderable(renderable); scene.render(); renderable->wait_rendering(); std::atomic mutation_finished{}; std::thread mutation([&] { scene.set_renderable_configuration(renderable, {.cache_enabled = true}); mutation_finished.store(true, std::memory_order_release); }); std::this_thread::yield(); EXPECT_TRUE(wait_until([&] { return mutation_finished.load(std::memory_order_acquire); })); renderable->release(); mutation.join(); EXPECT_TRUE(mutation_finished.load(std::memory_order_acquire)); EXPECT_TRUE(renderable->configuration().cache_enabled); } TEST(scene_base_test, concurrent_render_and_topology_updates_remain_serialized) { Scene2D_Context<> scene; auto parent = std::make_shared(scene); auto child = std::make_shared(scene); scene.attach_renderable(parent); scene.attach_renderable(child); std::thread renderer([&] { for (int index = 0; index < 100; ++index) { scene.render(); scene.wait_for_render(); } }); std::thread mutator([&] { for (int index = 0; index < 100; ++index) { if (index % 2 == 0) { scene.set_dependency_parent(child, parent); scene.set_display_parent(child, parent); } else { scene.clear_dependency_parent(child); scene.clear_display_parent(child); } scene.set_renderable_configuration(child, {.cache_enabled = index % 2 == 0}); } }); renderer.join(); mutator.join(); EXPECT_EQ(scene.renderable_count(), 2); } TEST(scene_base_test, detach_releases_renderable_from_the_live_model) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); std::weak_ptr weak = renderable; scene.attach_renderable(renderable); scene.render(); scene.wait_for_render(); scene.detach_renderable(renderable); renderable.reset(); EXPECT_TRUE(weak.expired()); EXPECT_EQ(scene.renderable_count(), 0); } TEST(scene_base_test, attach_rejects_null_renderable) { Scene2D_Context<> scene; EXPECT_THROW(scene.attach_renderable({}), std::invalid_argument); } TEST(scene_base_test, topology_rejects_unattached_renderables) { Scene2D_Context<> scene; 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); scene.attach_renderable(parent); auto detached_child = std::make_shared(scene); EXPECT_THROW(scene.set_dependency_parent(detached_child, parent), std::invalid_argument); } TEST(scene_base_test, setting_same_topology_parent_is_a_noop) { Scene2D_Context<> scene; auto parent = std::make_shared(scene); auto child = std::make_shared(scene); scene.attach_renderable(parent); scene.attach_renderable(child); scene.set_dependency_parent(child, parent); const auto revision = child->prepare_revision(); EXPECT_NO_THROW(scene.set_dependency_parent(child, parent)); EXPECT_EQ(child->prepare_revision(), revision); const auto topology = scene.topology_snapshot(); EXPECT_EQ(topology.dependency.size(), 2); bool found{}; for (const auto& relation : topology.dependency) { if (relation.child.get() == child.get()) { EXPECT_EQ(relation.parent.get(), parent.get()); found = true; } } EXPECT_TRUE(found); } TEST(scene_base_test, display_graph_rejects_cycles) { Scene2D_Context<> scene; auto first = std::make_shared(scene); auto second = std::make_shared(scene); auto third = std::make_shared(scene); scene.attach_renderable(first); scene.attach_renderable(second); 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); } TEST(scene_base_test, atomic_attach_can_place_existing_display_children) { Scene2D_Context<> scene; auto parent = std::make_shared(scene); auto axis = std::make_shared(scene); auto renderable = std::make_shared(scene); scene.attach_renderable(parent); scene.attach_renderable(axis); scene.attach_renderable(renderable, { .display_parents = {parent}, .display_children = {axis}, .dependency_parents = {parent, axis} }); const auto topology = scene.topology_snapshot(); const auto contains = [](const auto& relationships, const auto& child, const auto& parent_value) { return std::ranges::any_of(relationships, [&](const auto& relationship) { return relationship.child.get() == child.get() && relationship.parent.get() == parent_value.get(); }); }; EXPECT_TRUE(contains(topology.display, renderable, parent)); EXPECT_TRUE(contains(topology.display, axis, renderable)); EXPECT_TRUE(contains(topology.dependency, renderable, parent)); EXPECT_TRUE(contains(topology.dependency, renderable, axis)); } TEST(scene_base_test, failed_atomic_attach_rolls_back_every_topology) { Scene2D_Context<> scene; auto parent = std::make_shared(scene); auto renderable = std::make_shared(scene); scene.attach_renderable(parent); EXPECT_THROW(scene.attach_renderable(renderable, { .display_parents = {parent}, .display_children = {parent}, .dependency_parents = {parent} }), std::invalid_argument); EXPECT_EQ(scene.renderable_count(), 1u); EXPECT_NO_THROW(scene.attach_renderable(renderable, { .display_parents = {parent}, .dependency_parents = {parent} })); 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); const auto snapshot = scene.topology_snapshot(); scene.detach_renderable(renderable); renderable.reset(); EXPECT_FALSE(weak.expired()); EXPECT_EQ(snapshot.renderables.size(), 1); EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get()); } TEST(scene_base_test, topology_snapshot_is_safe_during_topology_updates) { Scene2D_Context<> scene; auto parent = std::make_shared(scene); auto child = std::make_shared(scene); scene.attach_renderable(parent); scene.attach_renderable(child); std::thread writer([&] { for (int index = 0; index < 500; ++index) { if (index % 2 == 0) { scene.set_dependency_parent(child, parent); scene.set_display_parent(child, parent); } else { scene.clear_dependency_parent(child); scene.clear_display_parent(child); } } }); for (int index = 0; index < 500; ++index) { const auto snapshot = scene.topology_snapshot(); EXPECT_EQ(snapshot.renderables.size(), 2); EXPECT_EQ(snapshot.display.size(), 2); EXPECT_EQ(snapshot.dependency.size(), 2); } writer.join(); } TEST(scene_base_test, all_waiters_receive_the_same_render_failure) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); scene.attach_renderable(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); } struct Scene_Base_Throw_Once_Renderable : Renderable_Base { explicit Scene_Base_Throw_Once_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { if (render_count.fetch_add(1, std::memory_order_acq_rel) == 0) { throw std::runtime_error("first render failed"); } } std::atomic render_count{}; }; TEST(scene_base_test, unobserved_render_failure_survives_next_successful_submission) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); scene.attach_renderable(renderable); scene.render(); scene.render(); EXPECT_THROW(scene.wait_for_render(), std::runtime_error); EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 2); scene.render(); EXPECT_NO_THROW(scene.wait_for_render()); EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 3); } struct Scene_Base_Render_Execution_Renderable : Renderable_Base { explicit Scene_Base_Render_Execution_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {} void prepare(const Prepare_Render_Context&) override { scene().wait_for_render(); wait_returned.store(true, std::memory_order_release); try { scene().render(); } catch (const std::logic_error&) { nested_render_rejected.store(true, std::memory_order_release); } try { scene().set_renderable_configuration(shared_from_this(), {.cache_enabled = true}); mutation_completed.store(true, std::memory_order_release); } catch (...) { } } std::atomic wait_returned{}; std::atomic nested_render_rejected{}; std::atomic mutation_completed{}; }; TEST(scene_base_test, taskflow_execution_can_mutate_live_model_but_cannot_submit_nested_render) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); scene.attach_renderable(renderable); scene.render(); scene.wait_for_render(); EXPECT_TRUE(renderable->wait_returned.load(std::memory_order_acquire)); EXPECT_TRUE(renderable->nested_render_rejected.load(std::memory_order_acquire)); EXPECT_TRUE(renderable->mutation_completed.load(std::memory_order_acquire)); }