#include "scene.hpp" #include #include #include namespace { struct Direct_Renderable : double_buffer::Def { struct Prop : Prev_Prop {}; struct State : Prev_State { bool operator==(const State&) const = default; }; struct Private : Prev_Private { int prepare_calls{}; int paint_calls{}; void prepare_data(double_buffer::Attached auto*) { ++prepare_calls; } void paint(double_buffer::Attached auto*) { ++paint_calls; } }; Private& data_for_test(); }; struct Graph_Renderable : double_buffer::Def { struct Prop : Prev_Prop {}; struct State : Prev_State { bool operator==(const State&) const = default; }; struct Private : Prev_Private { int prepare_calls{}; int paint_calls{}; int prepare_builds{}; bool rebuild_prepare{}; aethera::Task_Graph build_graph(double_buffer::Attached auto*, const Prop&) { ++prepare_builds; aethera::Task_Graph graph{"test.render.graph"}; auto prepare = graph.add("prepare", [this] { ++prepare_calls; }); auto paint = graph.add("paint", [this] { ++paint_calls; }); prepare.precede(paint); return graph; } bool should_rebuild_graph(double_buffer::Attached auto*, const Prop&) { return std::exchange(rebuild_prepare, false); } }; Private& data_for_test(); }; using Direct = Direct_Renderable; using Graph = Graph_Renderable; using Scene = aethera::Scene; inline Direct_Renderable::Private& Direct_Renderable::data_for_test() { return static_cast(*d); } inline Graph_Renderable::Private& Graph_Renderable::data_for_test() { return static_cast(*d); } template std::unique_ptr build_object() { auto result = typename Object_Type::template Builder{}.build(); if (!result) std::terminate(); return std::move(result).value(); } template void add_renderable(Scene& scene, Renderable* renderable) { ASSERT_TRUE(scene.edit_dependency_graph([&](auto& prepare) { prepare.add(renderable); }).has_value()); } } TEST(renderable_capability, direct_stages_do_not_allocate_subgraphs) { aethera::initialize_runtime({.workers = 2}); auto renderable = build_object(); auto scene = build_object(); add_renderable(*scene, renderable.get()); int prepare_extension_calls{}; renderable->taskflow().add( "test.render.extension", [&] { ++prepare_extension_calls; }); scene->process([](const auto&) {}); auto& data = renderable->data_for_test(); auto& base = static_cast(data); EXPECT_EQ(data.prepare_calls, 1); EXPECT_EQ(data.paint_calls, 1); EXPECT_EQ(prepare_extension_calls, 1); EXPECT_TRUE(base.graph.has_value()); scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_calls, 1); EXPECT_EQ(data.paint_calls, 1); EXPECT_EQ(prepare_extension_calls, 1); } TEST(renderable_capability, graph_stage_builds_lazily_and_rebuilds_inside_condition) { aethera::initialize_runtime({.workers = 2}); auto renderable = build_object(); auto scene = build_object(); add_renderable(*scene, renderable.get()); auto& data = renderable->data_for_test(); auto& base = static_cast(data); scene->process([](const auto&) {}); ASSERT_TRUE(base.graph.has_value()); EXPECT_EQ(data.prepare_builds, 1); EXPECT_EQ(data.prepare_calls, 1); scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_builds, 1); EXPECT_EQ(data.prepare_calls, 1); data.rebuild_prepare = true; renderable->mark_dirty(); scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_builds, 2); EXPECT_EQ(data.prepare_calls, 2); } TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundaries) { aethera::initialize_runtime({.workers = 2}); auto renderable = build_object(); auto scene = build_object(); add_renderable(*scene, renderable.get()); int renderable_updates = 0; int scene_updates = 0; int runtime_updates = 0; renderable->set_state_callback([&](const auto& state) { ++renderable_updates; EXPECT_TRUE(state.render_executed); }); scene->set_state_callback([&](const auto& state) { ++scene_updates; EXPECT_GT(state.taskflow_task_count, 0u); }); aethera::set_runtime_state_callback([&](const auto& state) { ++runtime_updates; EXPECT_GE(state.completed_taskflow_count, 1u); EXPECT_GT(state.observed_task_count, 0u); }); scene->process([](const auto&) {}); EXPECT_EQ(renderable_updates, 1); EXPECT_EQ(scene_updates, 1); EXPECT_EQ(runtime_updates, 1); aethera::clear_runtime_state_callback(); } TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) { aethera::initialize_runtime({.workers = 2}); auto renderable = build_object(); auto scene = build_object(); add_renderable(*scene, renderable.get()); std::size_t task_count{}; std::size_t dependency_count{}; int updates{}; scene->set_state_callback([&](const auto& state) { EXPECT_EQ(state.renderable_count, 1u); EXPECT_GT(state.taskflow_task_count, 0u); if (updates == 0) { EXPECT_TRUE(state.taskflow_rebuilt); task_count = state.taskflow_task_count; dependency_count = state.taskflow_dependency_count; } else { EXPECT_FALSE(state.taskflow_rebuilt); EXPECT_EQ(state.taskflow_task_count, task_count); EXPECT_EQ(state.taskflow_dependency_count, dependency_count); } ++updates; }); scene->process([](const auto&) {}); scene->process([](const auto&) {}); EXPECT_EQ(updates, 2); } TEST(scene_condition, render_order_does_not_create_a_false_data_dependency) { aethera::initialize_runtime({.workers = 2}); auto source = build_object(); auto target = build_object(); auto scene = build_object(); ASSERT_TRUE((scene->edit_dependency_graph( [&](auto& graph) { graph.add(source.get()); graph.add(target.get()); graph.add_dependency(target.get(), source.get()); }).has_value())); scene->process([](const auto&) {}); EXPECT_EQ(source->data_for_test().prepare_calls, 1); EXPECT_EQ(target->data_for_test().prepare_calls, 1); scene->process([](const auto&) {}); EXPECT_EQ(source->data_for_test().prepare_calls, 1); EXPECT_EQ(target->data_for_test().prepare_calls, 1); source->mark_dirty(); scene->process([](const auto&) {}); EXPECT_EQ(source->data_for_test().prepare_calls, 2); EXPECT_EQ(target->data_for_test().prepare_calls, 1); } TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity) { aethera::initialize_runtime({.workers = 2}); std::atomic_int completed{}; aethera::Task_Graph child{"test.visual"}; auto prepare = child.add("prepare.samples", [&] { completed.fetch_add(1); }); auto paint = child.add("paint.visual", [&] { completed.fetch_add(1); }); prepare.precede(paint); aethera::Task_Graph frame_graph{"test.frame"}; auto module_task = frame_graph.compose("spectrum", child); auto publish = frame_graph.add("publish.state", [&] { completed.fetch_add(1); }); module_task.precede(publish); aethera::Render_Frame frame{{41, 73}}; frame.mark(aethera::Frame_Trace_Marker::paint_started); frame.mark(aethera::Frame_Trace_Marker::paint_frame_target_started); frame.mark(aethera::Frame_Trace_Marker::paint_frame_target_finished); frame.mark(aethera::Frame_Trace_Marker::paint_finished); frame.request_taskflow_trace(); ASSERT_TRUE(aethera::detail::begin_taskflow_trace(frame)); aethera::detail::run_taskflow(frame_graph, frame, "test.scene.paint"); aethera::detail::finish_taskflow_trace(frame); EXPECT_EQ(completed.load(), 3); const auto trace = frame.take_taskflow_trace(); ASSERT_EQ(trace.graphs.size(), 1u); EXPECT_EQ(trace.identity, (aethera::Frame_Identity{41, 73})); EXPECT_TRUE(std::ranges::contains( trace.markers, aethera::Frame_Trace_Marker::paint_frame_target_started, &aethera::Frame_Trace_Point::marker)); EXPECT_EQ(trace.graphs.front().stage, "test.scene.paint"); EXPECT_TRUE(trace.graphs.front().completed); EXPECT_EQ(std::ranges::count_if(trace.graphs.front().nodes, [](const auto& node) { return node.type != "diagnostic"; }), 4); EXPECT_EQ(std::ranges::count_if(trace.graphs.front().nodes, [](const auto& node) { return node.type == "diagnostic"; }), 5); const auto module = std::ranges::find( trace.graphs.front().nodes, "spectrum", &aethera::Taskflow_Graph_Trace::Node::name); ASSERT_NE(module, trace.graphs.front().nodes.end()); const auto child_prepare = std::ranges::find( trace.graphs.front().nodes, "prepare.samples", &aethera::Taskflow_Graph_Trace::Node::name); ASSERT_NE(child_prepare, trace.graphs.front().nodes.end()); EXPECT_EQ(child_prepare->parent_node_id, module->node_id); EXPECT_EQ(child_prepare->node_id, "test.frame/spectrum/prepare.samples"); std::unordered_set metadata_ids; for (const auto& node : trace.graphs.front().nodes) metadata_ids.insert(node.native_id); ASSERT_FALSE(trace.tasks.empty()); for (const auto& task : trace.tasks) EXPECT_TRUE(metadata_ids.contains(task.native_id)); const auto child_paint = std::ranges::find( trace.graphs.front().nodes, "paint.visual", &aethera::Taskflow_Graph_Trace::Node::name); const auto publish_node = std::ranges::find( trace.graphs.front().nodes, "publish.state", &aethera::Taskflow_Graph_Trace::Node::name); ASSERT_NE(child_paint, trace.graphs.front().nodes.end()); ASSERT_NE(publish_node, trace.graphs.front().nodes.end()); const auto child_paint_execution = std::ranges::find( trace.tasks, child_paint->native_id, &aethera::Taskflow_Task_Trace::native_id); const auto publish_execution = std::ranges::find( trace.tasks, publish_node->native_id, &aethera::Taskflow_Task_Trace::native_id); ASSERT_NE(child_paint_execution, trace.tasks.end()); ASSERT_NE(publish_execution, trace.tasks.end()); EXPECT_NEAR(publish_execution->ready_ms, child_paint_execution->finished_ms, 0.05); EXPECT_LT(publish_execution->queue_wait_ms, 1.0); for (const auto name : {"taskflow.executor.finalize", "taskflow.runtime.bookkeeping", "taskflow.runtime.publish_state", "taskflow.observer.unbind_frame", "taskflow.graph.finish"}) { const auto diagnostic = std::ranges::find( trace.graphs.front().nodes, name, &aethera::Taskflow_Graph_Trace::Node::name); ASSERT_NE(diagnostic, trace.graphs.front().nodes.end()); EXPECT_NE(std::ranges::find(trace.tasks, diagnostic->native_id, &aethera::Taskflow_Task_Trace::native_id), trace.tasks.end()); } }