Files
Aethera/kernel/src/test/render_test.cpp
T
2026-08-27 14:54:06 +08:00

273 lines
12 KiB
C++

#include "scene.hpp"
#include <gtest/gtest.h>
#include <atomic>
#include <unordered_set>
namespace {
struct Direct_Renderable : double_buffer::Def<Direct_Renderable, aethera::Renderable> {
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<Graph_Renderable, aethera::Renderable> {
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<Private&>(*d);
}
inline Graph_Renderable::Private& Graph_Renderable::data_for_test() {
return static_cast<Private&>(*d);
}
template <typename Object_Type>
std::unique_ptr<Object_Type> build_object() {
auto result = typename Object_Type::template Builder<Object_Type>{}.build();
if (!result) std::terminate();
return std::move(result).value();
}
template <typename Renderable>
void add_renderable(Scene& scene, Renderable* renderable) {
ASSERT_TRUE(scene.edit_dependency_graph<aethera::Render_Graph_Tag>([&](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<Direct>();
auto scene = build_object<Scene>();
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<aethera::Renderable::Private&>(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<Graph>();
auto scene = build_object<Scene>();
add_renderable(*scene, renderable.get());
auto& data = renderable->data_for_test();
auto& base = static_cast<aethera::Renderable::Private&>(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<aethera::Render_Graph_Tag>();
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<Direct>();
auto scene = build_object<Scene>();
add_renderable(*scene, renderable.get());
int renderable_updates = 0;
int scene_updates = 0;
int runtime_updates = 0;
renderable->set_state_callback<aethera::Renderable::Base_Tag>([&](const auto& state) {
++renderable_updates;
EXPECT_TRUE(state.render_executed);
});
scene->set_state_callback<aethera::Scene::Base_Tag>([&](const auto& state) {
++scene_updates;
EXPECT_GT(state.taskflow_task_count, 0u);
});
aethera::set_runtime_state_callback<aethera::Task_Runtime_State_Tag>([&](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<aethera::Task_Runtime_State_Tag>();
}
TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) {
aethera::initialize_runtime({.workers = 2});
auto renderable = build_object<Direct>();
auto scene = build_object<Scene>();
add_renderable(*scene, renderable.get());
std::size_t task_count{};
std::size_t dependency_count{};
int updates{};
scene->set_state_callback<aethera::Scene::Base_Tag>([&](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<Direct_Renderable>();
auto target = build_object<Direct_Renderable>();
auto scene = build_object<Scene>();
ASSERT_TRUE((scene->edit_dependency_graph<aethera::Render_Graph_Tag>(
[&](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<aethera::Render_Graph_Tag>();
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<std::uint64_t> 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());
}
}