#include #include #include #include #include #include #include #include "renderive/capture/Capture.hpp" #include "renderive/render_graph/Frame_Analysis.hpp" #include "renderive/render_graph/Render_Plan.hpp" #include "renderive/scene/base/Abstract_Frame.hpp" namespace { template concept Exposes_Execution_Function = requires(Node node) { node.function; }; static_assert(!Exposes_Execution_Function); template concept Exposes_Execution_Slot = requires(Frame& frame) { frame.execution_slot(0); }; static_assert(!Exposes_Execution_Slot); Render_Graph make_parallel_graph() { Render_Graph_Builder builder; const auto a = builder.emplace(100, 10, "A Prepare", Render_Node_Kind::prepare); const auto b = builder.emplace(101, 11, "B Prepare", Render_Node_Kind::prepare); const auto c = builder.emplace(102, 12, "C Paint", Render_Node_Kind::paint); builder.precede(a, c); builder.precede(b, c); return std::move(builder).finish(); } } TEST(render_dag_test, one_graph_assigns_dense_execution_slots_and_versions_only_topology) { Render_Plan_History history; const auto first = history.publish(make_parallel_graph()); ASSERT_TRUE(first); ASSERT_EQ(first->graph.nodes.size(), 3u); EXPECT_EQ(first->graph.nodes[0].execution_index, 0u); EXPECT_EQ(first->graph.nodes[1].execution_index, 1u); EXPECT_EQ(first->graph.nodes[2].execution_index, 2u); const auto same = history.publish(make_parallel_graph()); EXPECT_EQ(same->version, first->version); auto changed_graph = make_parallel_graph(); changed_graph.edges.push_back({100, 101}); const auto changed = history.publish(std::move(changed_graph)); EXPECT_GT(changed->version, first->version); EXPECT_EQ(history.find(first->version), first); EXPECT_EQ(history.find(changed->version), changed); } TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_from_frame_slots) { Render_Plan_History history; const auto plan = history.publish(make_parallel_graph()); Frame_Snapshot snapshot; snapshot.frame_id = 1; snapshot.render_plan_version = plan->version; snapshot.render_start_ns = 100; snapshot.render_end_ns = 200; snapshot.node_executions.resize(plan->graph.nodes.size()); auto complete = [&snapshot, &plan](std::size_t slot, std::uint64_t ready, std::uint64_t start, std::uint64_t end, std::uint32_t worker) { auto* execution = &snapshot.node_executions.at(slot); execution->node_id = plan->graph.nodes.at(slot).node_id; execution->ready_time_ns = ready; execution->start_time_ns = start; execution->cpu_end_time_ns = end; execution->end_time_ns = end; execution->worker_id = worker; execution->status = Node_Execution_Status::complete; }; complete(0, 100, 110, 150, 0); complete(1, 100, 115, 145, 1); complete(2, 150, 160, 190, 1); const auto analysis = analyze_frame(*plan, snapshot); ASSERT_EQ(analysis.nodes.size(), 3u); EXPECT_EQ(analysis.nodes[2].dependency_ready_time_ns, 150u); EXPECT_EQ(analysis.nodes[2].scheduler_wait_ns, 10u); EXPECT_EQ(analysis.total_render_duration_ns, 100u); EXPECT_EQ(analysis.total_work_duration_ns, 100u); EXPECT_EQ(analysis.peak_parallelism, 2u); EXPECT_DOUBLE_EQ(analysis.average_parallelism, 1.0); EXPECT_EQ(analysis.parallel_overlap_ns, 30u); EXPECT_EQ(analysis.critical_path, (std::vector{100, 102})); EXPECT_EQ(analysis.critical_path_duration_ns, 70u); EXPECT_DOUBLE_EQ(analysis.nodes[0].work_contribution, 0.4); EXPECT_DOUBLE_EQ(analysis.nodes[0].critical_path_contribution, 40.0 / 70.0); } TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_frames) { Capture_Controller controller; Capture_Repository repository; const auto session_id = controller.capture_frames(2); repository.begin_session(session_id, 2); const auto first = controller.begin_frame(); const auto second = controller.begin_frame(); const auto third = controller.begin_frame(); EXPECT_TRUE(first.capture); EXPECT_TRUE(second.capture); EXPECT_FALSE(third.capture); EXPECT_EQ(first.session_id, session_id); EXPECT_EQ(second.session_id, session_id); controller.finish_frame(first, true); controller.finish_frame(second, true); EXPECT_FALSE(controller.state().enabled()); Render_Plan_History history; const auto plan = history.publish(make_parallel_graph()); for (std::uint64_t frame_id : {1u, 2u}) { auto snapshot = std::make_shared(); snapshot->frame_id = frame_id; snapshot->render_plan_version = plan->version; snapshot->render_start_ns = frame_id * 100; snapshot->render_end_ns = frame_id * 100 + 50; snapshot->node_executions.resize(plan->graph.nodes.size()); for (std::size_t slot = 0; slot < plan->graph.nodes.size(); ++slot) { auto* execution = &snapshot->node_executions.at(slot); execution->node_id = plan->graph.nodes.at(slot).node_id; execution->ready_time_ns = frame_id * 100; execution->start_time_ns = frame_id * 100 + slot * 10; execution->cpu_end_time_ns = execution->start_time_ns + 5; execution->end_time_ns = execution->start_time_ns + 5; execution->worker_id = static_cast(slot); execution->status = Node_Execution_Status::complete; } repository.publish(session_id, snapshot, analyze_frame(*plan, *snapshot)); } const auto session = repository.session(session_id); ASSERT_TRUE(session); EXPECT_EQ(session->requested_count, 2u); EXPECT_EQ(session->captured_count(), 2u); EXPECT_FALSE(session->active()); EXPECT_EQ(session->frames[0].snapshot->frame_id, 1u); EXPECT_EQ(session->frames[1].snapshot->frame_id, 2u); } TEST(render_dag_test, external_wait_contributes_to_wall_path_but_not_cpu_worker_utilization) { Render_Graph_Builder builder; builder.emplace(201, 20, "GPU Render", Render_Node_Kind::render); Render_Plan_History history; const auto plan = history.publish(std::move(builder).finish()); Frame_Snapshot snapshot; snapshot.frame_id = 7; snapshot.render_plan_version = plan->version; snapshot.render_start_ns = 1'000; snapshot.render_end_ns = 1'200; snapshot.node_executions.resize(1); auto& execution = snapshot.node_executions.front(); execution.node_id = 201; execution.ready_time_ns = 1'000; execution.start_time_ns = 1'000; execution.cpu_end_time_ns = 1'020; execution.external_start_time_ns = 1'020; execution.external_end_time_ns = 1'200; execution.end_time_ns = 1'200; execution.worker_id = 3; execution.status = Node_Execution_Status::complete; const auto analysis = analyze_frame(*plan, snapshot); ASSERT_EQ(analysis.nodes.size(), 1U); EXPECT_EQ(analysis.nodes.front().duration_ns, 200U); EXPECT_EQ(analysis.nodes.front().cpu_duration_ns, 20U); EXPECT_EQ(analysis.nodes.front().external_duration_ns, 180U); EXPECT_EQ(analysis.total_work_duration_ns, 20U); EXPECT_EQ(analysis.total_external_duration_ns, 180U); EXPECT_EQ(analysis.critical_path_duration_ns, 200U); ASSERT_EQ(analysis.workers.size(), 1U); EXPECT_EQ(analysis.workers.front().work_duration_ns, 20U); EXPECT_DOUBLE_EQ(analysis.workers.front().utilization, 0.1); } TEST(render_dag_test, failed_capture_ticket_is_retried_until_the_requested_frame_completes) { Capture_Controller controller; const auto session_id = controller.capture_next_frame(); const auto failed = controller.begin_frame(); ASSERT_TRUE(failed.capture); controller.finish_frame(failed, false); EXPECT_TRUE(controller.state().enabled()); EXPECT_EQ(controller.state().remaining_frame_count, 1u); const auto retry = controller.begin_frame(); ASSERT_TRUE(retry.capture); EXPECT_EQ(retry.session_id, session_id); controller.finish_frame(retry, true); EXPECT_FALSE(controller.state().enabled()); EXPECT_FALSE(controller.begin_frame().capture); } TEST(render_dag_test, concurrent_workers_reserve_exactly_one_capture_slot_each) { Capture_Controller controller; constexpr std::size_t requested = 64; controller.capture_frames(requested); std::atomic captured{}; std::vector workers; workers.reserve(256); for (std::size_t index = 0; index < 256; ++index) { workers.emplace_back([&] { const auto ticket = controller.begin_frame(); if (!ticket.capture) return; captured.fetch_add(1, std::memory_order_relaxed); controller.finish_frame(ticket, true); }); } for (auto& worker : workers) worker.join(); EXPECT_EQ(captured.load(std::memory_order_relaxed), requested); EXPECT_FALSE(controller.state().enabled()); }