Files
Renderive/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp
2026-08-13 15:17:02 +08:00

379 lines
16 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#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<void()> prepare_action;
std::function<void()> paint_action;
std::atomic<int> prepare_count{};
std::atomic<int> 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<const Renderable_Graph> 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<Pipeline_Renderable>::make();
auto child = renderive_Owner<Pipeline_Renderable>::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<Pipeline_Renderable>::make();
auto slow = renderive_Owner<Pipeline_Renderable>::make();
std::mutex mutex;
std::condition_variable condition;
bool slow_prepare_started{};
bool release_slow{};
std::atomic<bool> 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<Pipeline_Renderable>::make();
auto child = renderive_Owner<Pipeline_Renderable>::make();
std::mutex mutex;
std::condition_variable condition;
bool parent_paint_started{};
bool release_parent_paint{};
std::atomic<bool> 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_are_independent_and_prune_nodes) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Pipeline_Renderable>::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());
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_GT(cached_plan->version, first_plan->version);
EXPECT_EQ(std::count_if(cached_plan->graph.nodes.begin(), cached_plan->graph.nodes.end(),
[&](const Render_Node& value) {
return value.owner_id == renderable->renderable_id() &&
value.kind != Render_Node_Kind::composite;
}), 0);
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_GT(paint_plan->version, cached_plan->version);
EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id,
paint_id);
EXPECT_EQ(std::count_if(paint_plan->graph.nodes.begin(), paint_plan->graph.nodes.end(),
[&](const Render_Node& value) {
return value.owner_id == renderable->renderable_id() &&
value.kind == Render_Node_Kind::prepare;
}), 0);
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_GT(rebuilt_plan->version, paint_plan->version);
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<Dynamic_Renderable>::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<Pipeline_Renderable>::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, measures_capture_off_and_on_cost_separately) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Pipeline_Renderable>::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::nanoseconds>(
std::chrono::steady_clock::now() - start).count();
};
render_frames(warmup_count);
const auto capture_off_ns = render_frames(frame_count);
const Capture_Session_Id session_id = scene.capture_frames(frame_count);
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<Pipeline_Renderable>::make(true);
attach_initial(scene, renderable);
constexpr std::size_t capture_count = 20;
const Capture_Session_Id session_id = scene.capture_frames(capture_count);
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_NE(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<Pipeline_Renderable>::make();
std::atomic<bool> fail_once{true};
renderable->paint_action = [&] {
if (fail_once.exchange(false))
throw std::runtime_error("paint failed");
};
attach_initial(scene, renderable);
const Capture_Session_Id session_id = scene.capture_next_frame();
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());
}