#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 { class Pipeline_Renderable : public Renderable_Test_Harness { public: explicit Pipeline_Renderable(bool cached = false) : Renderable_Test_Harness({.cache_enabled = cached}) {} std::function prepare_action; std::function paint_action; std::atomic prepare_count{}; std::atomic paint_count{}; protected: void build_prepare_graph_for_test(Renderable_Graph_Builder& builder) override { builder.emplace("prepare", "Prepare", [this](const Prepare_Render_Context&) { ++prepare_count; if (prepare_action) prepare_action(); }); } void build_paint_graph_for_test(Renderable_Graph_Builder& builder) override { const auto paint = builder.emplace( "paint", "Paint", [this](const Paint_Render_Context&) { ++paint_count; if (paint_action) paint_action(); }); builder.precede(builder.find("prepare"), paint); } }; class Dynamic_Renderable : public Renderable_Test_Harness { public: explicit Dynamic_Renderable() = default; void set_chunk_count(std::size_t count) { chunk_count_ = count; rebuild_render_graph_for_test(); } std::shared_ptr graph_snapshot() { return render_graph_for_test(); } protected: void build_prepare_graph_for_test(Renderable_Graph_Builder& builder) override { const auto root = builder.emplace("prepare", "Prepare", [](const Prepare_Render_Context&) {}); for (std::size_t index = 0; index < chunk_count_; ++index) { const auto chunk = builder.emplace( "chunk:" + std::to_string(index), "Chunk " + std::to_string(index), [](const Prepare_Render_Context&) {}); builder.precede(root, chunk); } } private: std::size_t chunk_count_{2}; }; class Alternate_Frame final : public Abstract_Frame { }; const Render_Node& node(const Render_Plan& plan, std::uint64_t owner, Render_Node_Kind kind) { const auto iterator = std::find_if(plan.graph.nodes.begin(), plan.graph.nodes.end(), [owner, kind](const Render_Node& value) { return value.owner_id == owner && value.kind == kind; }); if (iterator == plan.graph.nodes.end()) throw std::logic_error("render node not found"); return *iterator; } bool has_edge(const Render_Plan& plan, Render_Node_Id from, Render_Node_Id to) { return std::find(plan.graph.edges.begin(), plan.graph.edges.end(), Render_Edge{from, to}) != plan.graph.edges.end(); } } TEST(render_plan_execution_test, dependency_edges_connect_prepare_only_and_layer_edges_connect_composite) { Scene2D_Context<> scene; auto parent = renderive_Owner::make(); auto child = renderive_Owner::make(); with_attach_builder(scene, [&](auto& attach) { attach.attach(parent); attach.attach(child); attach.set_dependency_parent(child, parent); attach.set_display_parent(child, parent); }); scene.render(); scene.wait_for_render(); const auto plan = scene.render_plan_snapshot(); ASSERT_TRUE(plan); const auto& parent_prepare = node(*plan, parent->renderable_id(), Render_Node_Kind::prepare); const auto& parent_paint = node(*plan, parent->renderable_id(), Render_Node_Kind::paint); const auto& parent_composite = node(*plan, parent->renderable_id(), Render_Node_Kind::composite); const auto& child_prepare = node(*plan, child->renderable_id(), Render_Node_Kind::prepare); const auto& child_composite = node(*plan, child->renderable_id(), Render_Node_Kind::composite); EXPECT_TRUE(has_edge(*plan, parent_prepare.node_id, child_prepare.node_id)); EXPECT_FALSE(has_edge(*plan, parent_paint.node_id, child_prepare.node_id)); EXPECT_TRUE(has_edge(*plan, parent_composite.node_id, child_composite.node_id)); } #ifndef RENDERIVE_TASKFLOW_MOCK TEST(render_plan_execution_test, local_paint_can_run_while_an_unrelated_prepare_is_blocked) { Scene2D_Context<> scene; auto fast = renderive_Owner::make(); auto slow = renderive_Owner::make(); std::mutex mutex; std::condition_variable condition; bool slow_prepare_started{}; bool release_slow{}; std::atomic fast_painted{}; slow->prepare_action = [&] { std::unique_lock lock(mutex); slow_prepare_started = true; condition.notify_all(); condition.wait(lock, [&] { return release_slow; }); }; fast->paint_action = [&] { fast_painted.store(true, std::memory_order_release); }; attach_initial(scene, fast, slow); scene.render(); { std::unique_lock lock(mutex); condition.wait(lock, [&] { return slow_prepare_started; }); } for (int attempt = 0; attempt < 10'000 && !fast_painted.load(std::memory_order_acquire); ++attempt) std::this_thread::yield(); EXPECT_TRUE(fast_painted.load(std::memory_order_acquire)); { std::lock_guard lock(mutex); release_slow = true; } condition.notify_all(); scene.wait_for_render(); } TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_paint) { Scene2D_Context<> scene; auto parent = renderive_Owner::make(); auto child = renderive_Owner::make(); std::mutex mutex; std::condition_variable condition; bool parent_paint_started{}; bool release_parent_paint{}; std::atomic child_prepared{}; parent->paint_action = [&] { std::unique_lock lock(mutex); parent_paint_started = true; condition.notify_all(); condition.wait(lock, [&] { return release_parent_paint; }); }; child->prepare_action = [&] { child_prepared.store(true, std::memory_order_release); }; with_attach_builder(scene, [&](auto& attach) { attach.attach(parent); attach.attach(child); attach.set_dependency_parent(child, parent); }); scene.render(); { std::unique_lock lock(mutex); condition.wait(lock, [&] { return parent_paint_started; }); } for (int attempt = 0; attempt < 10'000 && !child_prepared.load(std::memory_order_acquire); ++attempt) std::this_thread::yield(); EXPECT_TRUE(child_prepared.load(std::memory_order_acquire)); { std::lock_guard lock(mutex); release_parent_paint = true; } condition.notify_all(); scene.wait_for_render(); } #endif TEST(render_plan_execution_test, prepare_and_paint_cache_validity_do_not_recompile_render_plan) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(true); attach_initial(scene, renderable); scene.render(); scene.wait_for_render(); EXPECT_TRUE(renderable->prepare_cache_valid_for_test()); EXPECT_TRUE(renderable->paint_cache_valid_for_test()); EXPECT_EQ(renderable->prepare_count.load(), 1); EXPECT_EQ(renderable->paint_count.load(), 1); const auto first_plan = scene.render_plan_snapshot(); const Render_Node_Id prepare_id = node(*first_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id; const Render_Node_Id paint_id = node(*first_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id; scene.render(); scene.wait_for_render(); const auto cached_plan = scene.render_plan_snapshot(); EXPECT_EQ(cached_plan, first_plan); EXPECT_EQ(renderable->prepare_count.load(), 1); EXPECT_EQ(renderable->paint_count.load(), 1); EXPECT_EQ(node(*cached_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id, prepare_id); EXPECT_EQ(node(*cached_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id, paint_id); renderable->invalidate_paint_for_test(); EXPECT_TRUE(renderable->prepare_cache_valid_for_test()); EXPECT_FALSE(renderable->paint_cache_valid_for_test()); scene.render(); scene.wait_for_render(); const auto paint_plan = scene.render_plan_snapshot(); EXPECT_EQ(paint_plan, first_plan); EXPECT_EQ(renderable->prepare_count.load(), 1); EXPECT_EQ(renderable->paint_count.load(), 2); EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id, prepare_id); EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id, paint_id); renderable->invalidate_prepare_for_test(); EXPECT_FALSE(renderable->prepare_cache_valid_for_test()); EXPECT_FALSE(renderable->paint_cache_valid_for_test()); scene.render(); scene.wait_for_render(); const auto rebuilt_plan = scene.render_plan_snapshot(); EXPECT_EQ(rebuilt_plan, first_plan); EXPECT_EQ(renderable->prepare_count.load(), 2); EXPECT_EQ(renderable->paint_count.load(), 3); EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id, prepare_id); EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id, paint_id); } TEST(render_plan_execution_test, logical_node_ids_are_stable_and_retired_ids_are_never_reused) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); const auto first = renderable->graph_snapshot(); ASSERT_EQ(first->graph.nodes.size(), 3u); const Render_Node_Id root = first->graph.nodes[0].node_id; const Render_Node_Id first_chunk = first->graph.nodes[1].node_id; const Render_Node_Id retired_chunk = first->graph.nodes[2].node_id; renderable->set_chunk_count(2); scene.render(); scene.wait_for_render(); 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.render(); scene.wait_for_render(); 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.render(); scene.wait_for_render(); const auto expanded = renderable->graph_snapshot(); ASSERT_EQ(expanded->graph.nodes.size(), 3u); EXPECT_EQ(expanded->graph.nodes[0].node_id, root); EXPECT_EQ(expanded->graph.nodes[1].node_id, first_chunk); EXPECT_NE(expanded->graph.nodes[2].node_id, retired_chunk); EXPECT_GT(expanded->graph.nodes[2].node_id, retired_chunk); } TEST(render_plan_execution_test, capture_off_keeps_abstract_frame_without_execution_timing) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); Abstract_Frame frame; scene.render(frame); scene.wait_for_render(); EXPECT_FALSE(frame.completed_snapshot()); } TEST(render_plan_execution_test, abstract_frame_lifetime_is_not_borrowed_by_async_render) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); std::mutex mutex; std::condition_variable condition; bool paint_started{}; bool release_paint{}; renderable->paint_action = [&] { std::unique_lock lock(mutex); paint_started = true; condition.notify_all(); condition.wait(lock, [&] { return release_paint; }); }; attach_initial(scene, renderable); { Abstract_Frame frame; scene.render(frame); std::unique_lock lock(mutex); condition.wait(lock, [&] { return paint_started; }); } { std::lock_guard lock(mutex); release_paint = true; } condition.notify_all(); EXPECT_NO_THROW(scene.wait_for_render()); } TEST(render_plan_execution_test, measures_capture_off_and_on_cost_separately) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); attach_initial(scene, renderable); constexpr std::size_t warmup_count = 32; constexpr std::size_t frame_count = 512; const auto render_frames = [&scene](std::size_t count) { const auto start = std::chrono::steady_clock::now(); for (std::size_t index = 0; index < count; ++index) { scene.render(); scene.wait_for_render(); } return std::chrono::duration_cast( std::chrono::steady_clock::now() - start).count(); }; render_frames(warmup_count); const auto capture_off_ns = render_frames(frame_count); const auto request = scene.capture_frames(frame_count); ASSERT_TRUE(request); const Capture_Session_Id session_id = request.session_id; const auto capture_on_ns = render_frames(frame_count); const auto session = scene.capture_session(session_id); ASSERT_TRUE(session); ASSERT_EQ(session->captured_count(), frame_count); const auto capture_off_per_frame = capture_off_ns / frame_count; const auto capture_on_per_frame = capture_on_ns / frame_count; std::cout << "[ PERF ] capture_off_ns_per_frame=" << capture_off_per_frame << " capture_on_ns_per_frame=" << capture_on_per_frame << '\n'; EXPECT_GT(capture_off_per_frame, 0); EXPECT_GT(capture_on_per_frame, 0); } TEST(render_plan_execution_test, capture_requests_publish_exact_immutable_abstract_frame_snapshots) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(true); attach_initial(scene, renderable); constexpr std::size_t capture_count = 20; const auto request = scene.capture_frames(capture_count); ASSERT_TRUE(request); const Capture_Session_Id session_id = request.session_id; Alternate_Frame alternate_frame; scene.render(alternate_frame); scene.wait_for_render(); for (std::size_t index = 1; index < capture_count + 1; ++index) { scene.render(); scene.wait_for_render(); } const auto session = scene.capture_session(session_id); ASSERT_TRUE(session); ASSERT_EQ(session->captured_count(), capture_count); EXPECT_FALSE(session->active()); ASSERT_TRUE(alternate_frame.completed_snapshot()); EXPECT_EQ(alternate_frame.completed_snapshot(), session->frames.front().snapshot); ASSERT_GE(session->frames.size(), 2u); EXPECT_EQ(session->frames[0].snapshot->render_plan_version, session->frames[1].snapshot->render_plan_version); for (const auto& frame : session->frames) { ASSERT_TRUE(frame.snapshot); EXPECT_FALSE(frame.snapshot->node_executions.empty()); EXPECT_EQ(frame.snapshot->render_plan_version, frame.analysis.render_plan_version); EXPECT_TRUE(scene.find_render_plan(frame.snapshot->render_plan_version)); } } TEST(render_plan_execution_test, failed_render_discards_abstract_frame_capture_and_retries_ticket) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); std::atomic fail_once{true}; renderable->paint_action = [&] { if (fail_once.exchange(false)) throw std::runtime_error("paint failed"); }; attach_initial(scene, renderable); const auto request = scene.capture_next_frame(); ASSERT_TRUE(request); const Capture_Session_Id session_id = request.session_id; Alternate_Frame failed_frame; scene.render(failed_frame); EXPECT_THROW(scene.wait_for_render(), std::runtime_error); EXPECT_FALSE(failed_frame.completed_snapshot()); EXPECT_TRUE(scene.capture_state().enabled()); Alternate_Frame completed_frame; scene.render(completed_frame); scene.wait_for_render(); const auto session = scene.capture_session(session_id); ASSERT_TRUE(session); ASSERT_EQ(session->captured_count(), 1u); EXPECT_EQ(completed_frame.completed_snapshot(), session->frames.front().snapshot); EXPECT_FALSE(scene.capture_state().enabled()); }