#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" static_assert(std::same_as&>().topology_snapshot()), Scene_Base::Topology_Snapshot>); struct Scene2D_Context_Test_Renderable : Renderable_Test_Harness { explicit Scene2D_Context_Test_Renderable() : Renderable_Test_Harness({.cache_enabled = false}) {} void prepare_for_test(const Prepare_Render_Context&) override { ++render_count; scene_is_2d = dynamic_cast(&scene_for_test()) != nullptr; render_thread = std::this_thread::get_id(); } std::atomic render_count{}; bool scene_is_2d{}; std::thread::id render_thread; }; TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) { Scene2D_Context<> scene; const auto caller_thread = std::this_thread::get_id(); auto first = renderive_Owner::make(); auto second = renderive_Owner::make(); attach_initial(scene, first); scene.render(); scene.wait_for_render(); EXPECT_EQ(first->render_count.load(), 1); EXPECT_TRUE(first->scene_is_2d); EXPECT_NE(first->render_thread, caller_thread); wait_renderable_edit(scene, [first, second](auto& editor) { editor.detach(first); editor.attach(second); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(first->render_count.load(), 1); EXPECT_EQ(second->render_count.load(), 1); EXPECT_EQ(scene.frame_control_strategy().frame_control_state().publish_revision, 2); } struct Scene2D_Final_Cache_Blocking_Renderable : Renderable_Test_Harness { explicit Scene2D_Final_Cache_Blocking_Renderable() : Renderable_Test_Harness({.cache_enabled = false}) {} void prepare_for_test(const Prepare_Render_Context&) override { while (!release.load(std::memory_order_acquire)) { started.store(true, std::memory_order_release); std::this_thread::yield(); } } std::atomic started{}; std::atomic release{}; }; TEST(scene2d_context_test, final_color_cache_access_waits_for_render_completion) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.render(); while (!renderable->started.load(std::memory_order_acquire)) { std::this_thread::yield(); } std::atomic access_started{}; std::atomic cache_accessed{}; std::thread reader([&] { access_started.store(true, std::memory_order_release); scene.with_final_color_cache([&](const Recording_Color_Cache&) { cache_accessed.store(true, std::memory_order_release); }); }); while (!access_started.load(std::memory_order_acquire)) { std::this_thread::yield(); } EXPECT_FALSE(cache_accessed.load(std::memory_order_acquire)); renderable->release.store(true, std::memory_order_release); reader.join(); EXPECT_TRUE(cache_accessed.load(std::memory_order_acquire)); } struct Scene2D_Cached_Renderable : Renderable_Test_Harness { Scene2D_Cached_Renderable(std::uint64_t value) : Renderable_Test_Harness({.cache_enabled = true}), value(value) {} void prepare_for_test(const Prepare_Render_Context&) override { ++render_count; } void build_paint_graph_for_test(Renderable_Graph_Builder& graph) override { graph.emplace("paint", "Paint", [this](const Paint_Render_Context& context) { dynamic_cast(context.color_cache).append(value); }); } std::uint64_t value; int render_count{}; }; TEST(scene2d_context_test, reattached_cached_renderable_rebuilds_removed_color_cache) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(7); attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count, 1); wait_renderable_edit(scene, [renderable](auto& editor) { editor.detach(renderable); editor.attach(renderable); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->render_count, 2); scene.with_final_color_cache([](const Recording_Color_Cache& cache) { EXPECT_EQ(cache.values.size(), 1); EXPECT_EQ(cache.values.at(0), 7); }); } TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) { Scene2D_Context<> scene; auto first_parent = renderive_Owner::make(1); auto second_parent = renderive_Owner::make(2); auto child = renderive_Owner::make(3); with_attach_builder(scene, [&](auto& attach) { attach.attach(first_parent); attach.attach(second_parent); attach.attach(child); attach.set_dependency_parent(child, first_parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(first_parent->render_count, 1); EXPECT_EQ(second_parent->render_count, 1); EXPECT_EQ(child->render_count, 1); wait_renderable_edit(scene, [child, second_parent](auto& editor) { editor.set_dependency_parent(child, second_parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(first_parent->render_count, 1); EXPECT_EQ(second_parent->render_count, 1); EXPECT_EQ(child->render_count, 2); } TEST(scene2d_context_test, multiple_dependency_parents_invalidate_cached_child) { Scene2D_Context<> scene; auto first_parent = renderive_Owner::make(1); auto second_parent = renderive_Owner::make(2); auto child = renderive_Owner::make(3); with_attach_builder(scene, [&](auto& attach) { attach.attach(first_parent); attach.attach(second_parent); attach.attach(child); attach.set_dependency_parent(child, first_parent); attach.add_dependency_parent(child, second_parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 1); first_parent->invalidate_prepare_for_test(); scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 2); second_parent->invalidate_prepare_for_test(); scene.render(); scene.wait_for_render(); EXPECT_EQ(child->render_count, 3); int dependency_edges{}; for (const auto& relation : scene.topology_snapshot().dependency) { if (relation.child.get() == child.get() && relation.parent) { ++dependency_edges; } } EXPECT_EQ(dependency_edges, 2); } TEST(scene2d_context_test, final_color_cache_callback_can_reenter_scene_control_api) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(9); attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); scene.with_final_color_cache([&](const Recording_Color_Cache&) { renderable->set_configuration_for_test({.cache_enabled = false}); }); EXPECT_FALSE(renderable->configuration_for_test().cache_enabled); } TEST(scene2d_context_test, detach_dependency_parent_invalidates_promoted_cached_child) { Scene2D_Context<> scene; auto grandparent = renderive_Owner::make(1); auto parent = renderive_Owner::make(2); auto child = renderive_Owner::make(3); with_attach_builder(scene, [&](auto& attach) { attach.attach(grandparent); attach.attach(parent); attach.attach(child); attach.set_dependency_parent(parent, grandparent); attach.set_dependency_parent(child, parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(grandparent->render_count, 1); EXPECT_EQ(parent->render_count, 1); EXPECT_EQ(child->render_count, 1); wait_renderable_edit(scene, [grandparent, parent, child](auto& editor) { editor.clear_dependency_parent(child); editor.clear_dependency_parent(parent); editor.set_dependency_parent(child, grandparent); editor.detach(parent); }); scene.render(); scene.wait_for_render(); EXPECT_EQ(grandparent->render_count, 1); EXPECT_EQ(parent->render_count, 1); EXPECT_EQ(child->render_count, 2); const auto topology = scene.topology_snapshot(); for (const auto& relation : topology.dependency) { if (relation.child.get() == child.get()) { EXPECT_EQ(relation.parent.get(), grandparent.get()); } } } struct Scene2D_Attach_Throwing_Cache : Color_Cache { explicit Scene2D_Attach_Throwing_Cache(std::pmr::memory_resource&) { if (++construction_count == throw_on_construction) { throw std::runtime_error("cache construction failed"); } } void clear() override {} void composite(const Color_Cache&) override {} inline static int construction_count{}; inline static int throw_on_construction{}; }; TEST(scene2d_context_test, failed_attach_leaves_renderable_fully_detached) { Scene2D_Attach_Throwing_Cache::construction_count = 0; Scene2D_Attach_Throwing_Cache::throw_on_construction = 2; Scene2D_Context, Scene2D_Attach_Throwing_Cache> scene; auto renderable = renderive_Owner::make(); EXPECT_THROW(attach_initial(scene, renderable), 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(attach_initial(scene, renderable)); EXPECT_EQ(scene.renderable_count(), 1); } struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Test_Harness { explicit Scene2D_Viewport_Snapshot_Renderable() = default; void prepare_for_test(const Prepare_Render_Context& context) override { viewport = context.frame.viewport; } Frame_Viewport viewport; }; TEST(scene2d_context_test, published_scene_viewport_is_frozen_for_render) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{320, 180}); scene.publish_frame_state(); scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{640, 360}); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->viewport, (Frame_Viewport{320, 180})); scene.publish_frame_state(); renderable->invalidate_prepare_for_test(); scene.render(); scene.wait_for_render(); EXPECT_EQ(renderable->viewport, (Frame_Viewport{640, 360})); }