taskflow 接入异步 datoviz性能监测

This commit is contained in:
2026-08-15 02:12:03 +08:00
parent 2c7c8abd4f
commit 282768e0ea
36 changed files with 1890 additions and 234 deletions
@@ -0,0 +1,26 @@
#pragma once
#include "Frame_Strategy_Observer.hpp"
struct Double_Buffer_Frame_Observer {
static constexpr bool enabled = true;
template <class Target>
decltype(auto) bind(Target& target) {
return observer_.bind(target);
}
template <class Observation>
void observe(const Observation& observation) noexcept {
if (observation.event != Observation::Event::cache_updated)
return;
observer_.observe({
Real_Time_Data_Observation_Event::updated,
{this, Real_Time_Data_Retention::latest,
observation.cache_update_count, observation.time_ns,
observation.cache_update_count, 1}});
}
private:
Frame_Strategy_Real_Time_Data_Observer observer_;
};
@@ -1,6 +1,7 @@
#pragma once
#include "base/Real_Time_Data_Base.hpp"
#include "Attach_Real_Time_Data.hpp"
#include "Double_Buffer_Frame_Observer.hpp"
#include "Frame_Strategy_Observer.hpp"
#include "History_Real_Time_Data.hpp"
#include "Observation.hpp"
@@ -0,0 +1,125 @@
#include "External_Operation.hpp"
#include <mutex>
#include <stdexcept>
#include <utility>
struct External_Operation::State {
enum class Status {
pending,
completed,
failed
};
std::mutex mutex;
Completion completion;
std::exception_ptr error;
Status status{Status::pending};
bool subscribed{};
};
External_Operation::External_Operation(std::shared_ptr<State> state) noexcept
: state_(std::move(state)) {}
void External_Operation::on_complete(Completion completion) const {
if (!state_)
throw std::logic_error("external operation is empty");
if (!completion)
throw std::invalid_argument("external operation completion is empty");
std::exception_ptr error;
bool invoke{};
{
std::lock_guard lock(state_->mutex);
if (state_->subscribed)
throw std::logic_error(
"external operation already has a completion subscriber");
state_->subscribed = true;
if (state_->status == State::Status::pending) {
state_->completion = std::move(completion);
return;
}
error = state_->error;
invoke = true;
}
if (invoke)
completion(std::move(error));
}
External_Operation::operator bool() const noexcept {
return static_cast<bool>(state_);
}
External_Operation_Source::External_Operation_Source()
: state_(std::make_shared<External_Operation::State>()) {}
External_Operation External_Operation_Source::operation() const noexcept {
return External_Operation(state_);
}
bool External_Operation_Source::complete() noexcept {
return finish(false, {});
}
bool External_Operation_Source::fail(std::exception_ptr error) noexcept {
if (!error) {
try {
throw std::runtime_error("external operation failed");
} catch (...) {
error = std::current_exception();
}
}
return finish(true, std::move(error));
}
bool External_Operation_Source::finish(
bool failed, std::exception_ptr error) noexcept {
if (!state_)
return false;
External_Operation::Completion completion;
std::exception_ptr completion_error;
{
std::lock_guard lock(state_->mutex);
if (state_->status != External_Operation::State::Status::pending)
return false;
state_->status = failed
? External_Operation::State::Status::failed
: External_Operation::State::Status::completed;
state_->error = std::move(error);
completion_error = state_->error;
completion = std::move(state_->completion);
}
if (completion) {
try {
completion(std::move(completion_error));
} catch (...) {
}
}
return true;
}
Node_Execution_Result::Node_Execution_Result(
std::optional<External_Operation> operation) noexcept
: operation_(std::move(operation)) {}
Node_Execution_Result Node_Execution_Result::completed() noexcept {
return Node_Execution_Result(std::nullopt);
}
Node_Execution_Result Node_Execution_Result::external(
External_Operation operation) {
if (!operation)
throw std::invalid_argument("external node result has no operation");
return Node_Execution_Result(std::move(operation));
}
bool Node_Execution_Result::is_external() const noexcept {
return operation_.has_value();
}
const External_Operation& Node_Execution_Result::operation() const {
if (!operation_)
throw std::logic_error("completed node result has no external operation");
return *operation_;
}
@@ -0,0 +1,60 @@
#pragma once
#include <exception>
#include <functional>
#include <memory>
#include <optional>
class External_Operation_Source;
class External_Operation {
public:
using Completion = std::function<void(std::exception_ptr)>;
External_Operation() = default;
void on_complete(Completion completion) const;
[[nodiscard]] explicit operator bool() const noexcept;
private:
struct State;
explicit External_Operation(std::shared_ptr<State> state) noexcept;
std::shared_ptr<State> state_;
friend class External_Operation_Source;
};
class External_Operation_Source {
public:
External_Operation_Source();
External_Operation_Source(const External_Operation_Source&) = delete;
External_Operation_Source& operator=(const External_Operation_Source&) = delete;
External_Operation_Source(External_Operation_Source&&) noexcept = default;
External_Operation_Source& operator=(External_Operation_Source&&) noexcept = default;
[[nodiscard]] External_Operation operation() const noexcept;
[[nodiscard]] bool complete() noexcept;
[[nodiscard]] bool fail(std::exception_ptr error) noexcept;
private:
[[nodiscard]] bool finish(bool failed, std::exception_ptr error) noexcept;
std::shared_ptr<External_Operation::State> state_;
};
class Node_Execution_Result {
public:
[[nodiscard]] static Node_Execution_Result completed() noexcept;
[[nodiscard]] static Node_Execution_Result external(
External_Operation operation);
[[nodiscard]] bool is_external() const noexcept;
[[nodiscard]] const External_Operation& operation() const;
private:
explicit Node_Execution_Result(
std::optional<External_Operation> operation) noexcept;
std::optional<External_Operation> operation_;
};
@@ -80,11 +80,14 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram
auto& analysis = result.nodes[node.execution_index];
analysis.node_id = node.node_id;
analysis.duration_ns = execution.duration_ns();
analysis.cpu_duration_ns = execution.cpu_duration_ns();
analysis.external_duration_ns = execution.external_duration_ns();
analysis.start_offset_ns = subtract_saturated(execution.start_time_ns,
frame.render_start_ns);
analysis.end_offset_ns = subtract_saturated(execution.end_time_ns,
frame.render_start_ns);
result.total_work_duration_ns += analysis.duration_ns;
result.total_work_duration_ns += analysis.cpu_duration_ns;
result.total_external_duration_ns += analysis.external_duration_ns;
}
std::vector<std::vector<std::size_t>> predecessors(plan.graph.nodes.size());
@@ -116,20 +119,18 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram
std::vector<std::size_t> path_parent(plan.graph.nodes.size(),
std::numeric_limits<std::size_t>::max());
for (std::size_t index : topological) {
std::uint64_t dependency_ready = frame.render_start_ns;
std::uint64_t parent_path{};
for (std::size_t predecessor : predecessors[index]) {
dependency_ready = std::max(dependency_ready,
frame.node_executions[predecessor].end_time_ns);
if (path_duration[predecessor] > parent_path) {
parent_path = path_duration[predecessor];
path_parent[index] = predecessor;
}
}
auto& analysis = result.nodes[index];
analysis.dependency_ready_time_ns = dependency_ready;
analysis.scheduler_wait_ns = frame.node_executions[index].start_time_ns >= dependency_ready
? frame.node_executions[index].start_time_ns - dependency_ready
const auto& execution = frame.node_executions[index];
analysis.dependency_ready_time_ns = execution.ready_time_ns;
analysis.scheduler_wait_ns = execution.start_time_ns >= execution.ready_time_ns
? execution.start_time_ns - execution.ready_time_ns
: 0;
path_duration[index] = parent_path + analysis.duration_ns;
}
@@ -147,7 +148,8 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram
for (auto& node : result.nodes) {
node.work_contribution = result.total_work_duration_ns == 0
? 0.0
: static_cast<double>(node.duration_ns) / result.total_work_duration_ns;
: static_cast<double>(node.cpu_duration_ns) /
result.total_work_duration_ns;
node.critical_path_contribution = !node.on_critical_path ||
result.critical_path_duration_ns == 0
? 0.0
@@ -162,11 +164,11 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram
std::unordered_map<std::uint32_t, std::uint64_t> worker_work;
events.reserve(frame.node_executions.size() * 2);
for (const auto& execution : frame.node_executions) {
if (execution.duration_ns() == 0)
if (execution.cpu_duration_ns() == 0)
continue;
events.push_back({execution.start_time_ns, 1});
events.push_back({execution.end_time_ns, -1});
worker_work[execution.worker_id] += execution.duration_ns();
events.push_back({execution.cpu_end_time_ns, -1});
worker_work[execution.worker_id] += execution.cpu_duration_ns();
}
std::sort(events.begin(), events.end(), [](const Event& left, const Event& right) {
return left.time < right.time || (left.time == right.time && left.delta < right.delta);
@@ -9,6 +9,8 @@
struct Node_Frame_Analysis {
Render_Node_Id node_id{};
std::uint64_t duration_ns{};
std::uint64_t cpu_duration_ns{};
std::uint64_t external_duration_ns{};
std::uint64_t start_offset_ns{};
std::uint64_t end_offset_ns{};
std::uint64_t dependency_ready_time_ns{};
@@ -30,6 +32,7 @@ struct Frame_Analysis {
std::uint64_t total_render_duration_ns{};
std::uint64_t critical_path_duration_ns{};
std::uint64_t total_work_duration_ns{};
std::uint64_t total_external_duration_ns{};
std::uint64_t parallel_overlap_ns{};
std::size_t peak_parallelism{};
double average_parallelism{};
@@ -0,0 +1,354 @@
#include "Render_Graph_Runtime.hpp"
#include <condition_variable>
#include <exception>
#include <limits>
#include <mutex>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>
namespace renderive::render_graph::detail {
struct Render_Graph_Runtime::State
: std::enable_shared_from_this<Render_Graph_Runtime::State> {
struct Node_State {
std::size_t remaining_predecessors{};
Node_Execution_Status status{Node_Execution_Status::pending};
};
State(const Render_Plan& plan,
std::span<Node_Execution* const> execution_slots,
Execute_Node execute,
Schedule scheduler,
Current_Worker_Id worker_id)
: nodes(plan.graph.nodes.size()),
successors(plan.graph.nodes.size()),
executions(execution_slots.begin(), execution_slots.end()),
execute_node(std::move(execute)),
schedule(std::move(scheduler)),
current_worker_id(std::move(worker_id)),
unfinished_nodes(plan.graph.nodes.size()) {
if (!execute_node)
throw std::invalid_argument("render graph node executor is empty");
if (!schedule)
throw std::invalid_argument("render graph scheduler is empty");
if (!current_worker_id)
throw std::invalid_argument("render graph worker id source is empty");
if (!executions.empty() && executions.size() != nodes.size())
throw std::invalid_argument(
"render graph execution slot count differs from plan");
if (executions.empty())
executions.resize(nodes.size());
std::vector<bool> occupied(nodes.size());
std::unordered_map<Render_Node_Id, std::size_t> indices;
indices.reserve(nodes.size());
for (const auto& node : plan.graph.nodes) {
if (node.execution_index >= nodes.size() ||
occupied[node.execution_index])
throw std::invalid_argument(
"render plan execution indices are not dense and unique");
occupied[node.execution_index] = true;
if (!indices.emplace(node.node_id, node.execution_index).second)
throw std::invalid_argument(
"render plan contains duplicate node ids");
}
for (const auto& edge : plan.graph.edges) {
const auto from = indices.find(edge.from);
const auto to = indices.find(edge.to);
if (from == indices.end() || to == indices.end())
throw std::invalid_argument(
"render plan edge references an unknown node");
successors[from->second].push_back(to->second);
++nodes[to->second].remaining_predecessors;
}
std::vector<std::size_t> remaining;
remaining.reserve(nodes.size());
std::vector<std::size_t> topological;
topological.reserve(nodes.size());
for (const auto& node : nodes)
remaining.push_back(node.remaining_predecessors);
for (std::size_t index = 0; index < remaining.size(); ++index) {
if (remaining[index] == 0)
topological.push_back(index);
}
for (std::size_t cursor = 0; cursor < topological.size(); ++cursor) {
for (const std::size_t successor : successors[topological[cursor]]) {
if (--remaining[successor] == 0)
topological.push_back(successor);
}
}
if (topological.size() != nodes.size())
throw std::invalid_argument("render plan contains a cycle");
}
void execute() {
std::vector<std::size_t> ready;
{
std::lock_guard lock(mutex);
if (started)
throw std::logic_error("render graph runtime already executed");
started = true;
if (nodes.empty()) {
terminal = true;
} else {
for (std::size_t index = 0; index < nodes.size(); ++index) {
if (nodes[index].remaining_predecessors == 0)
make_ready_locked(index, ready);
}
if (ready.empty()) {
fail_graph_locked(std::make_exception_ptr(
std::logic_error("render plan contains a cycle")));
}
}
}
submit(ready);
std::exception_ptr error;
{
std::unique_lock lock(mutex);
completion.wait(lock, [this] { return terminal; });
error = first_exception;
}
if (error)
std::rethrow_exception(error);
}
void make_ready_locked(std::size_t index,
std::vector<std::size_t>& ready) {
auto& node = nodes.at(index);
if (failed || node.status != Node_Execution_Status::pending)
return;
node.status = Node_Execution_Status::ready;
if (auto* execution = executions[index]) {
execution->ready_time_ns = render_clock_now_ns();
execution->status = Node_Execution_Status::ready;
}
ready.push_back(index);
}
void submit(const std::vector<std::size_t>& ready) noexcept {
for (const std::size_t index : ready)
submit(index);
}
void submit(std::size_t index) noexcept {
{
std::lock_guard lock(mutex);
if (failed || nodes[index].status != Node_Execution_Status::ready) {
finish_if_terminal_locked();
return;
}
++active_tasks;
}
try {
auto self = shared_from_this();
schedule([self = std::move(self), index] {
self->run_node(index);
});
} catch (...) {
std::lock_guard lock(mutex);
--active_tasks;
fail_node_locked(index, std::current_exception(),
render_clock_now_ns());
finish_if_terminal_locked();
}
}
void run_node(std::size_t index) noexcept {
Node_Execution_Metrics* metrics{};
{
std::lock_guard lock(mutex);
if (failed || nodes[index].status != Node_Execution_Status::ready) {
--active_tasks;
finish_if_terminal_locked();
return;
}
nodes[index].status = Node_Execution_Status::running;
if (auto* execution = executions[index]) {
execution->start_time_ns = render_clock_now_ns();
try {
execution->worker_id = current_worker_id();
} catch (...) {
execution->worker_id =
std::numeric_limits<std::uint32_t>::max();
}
execution->status = Node_Execution_Status::running;
metrics = &execution->metrics;
}
}
Node_Execution_Result result = Node_Execution_Result::completed();
std::exception_ptr error;
try {
result = execute_node(index, metrics);
} catch (...) {
error = std::current_exception();
}
const std::uint64_t cpu_end = render_clock_now_ns();
if (error) {
std::lock_guard lock(mutex);
--active_tasks;
fail_node_locked(index, std::move(error), cpu_end);
finish_if_terminal_locked();
return;
}
if (!result.is_external()) {
complete_synchronous(index, cpu_end);
return;
}
{
std::lock_guard lock(mutex);
--active_tasks;
auto& node = nodes[index];
node.status = Node_Execution_Status::waiting_external;
++waiting_external;
if (auto* execution = executions[index]) {
execution->cpu_end_time_ns = cpu_end;
execution->external_start_time_ns = cpu_end;
execution->status = Node_Execution_Status::waiting_external;
}
}
try {
auto self = shared_from_this();
result.operation().on_complete(
[self = std::move(self), index](std::exception_ptr completion_error) {
self->complete_external(index, std::move(completion_error));
});
} catch (...) {
complete_external(index, std::current_exception());
}
}
void complete_synchronous(std::size_t index,
std::uint64_t cpu_end) noexcept {
std::vector<std::size_t> ready;
{
std::lock_guard lock(mutex);
--active_tasks;
auto& node = nodes[index];
node.status = Node_Execution_Status::complete;
if (auto* execution = executions[index]) {
execution->cpu_end_time_ns = cpu_end;
execution->end_time_ns = cpu_end;
execution->status = Node_Execution_Status::complete;
}
--unfinished_nodes;
unlock_successors_locked(index, ready);
finish_if_terminal_locked();
}
submit(ready);
}
void complete_external(std::size_t index,
std::exception_ptr error) noexcept {
std::vector<std::size_t> ready;
{
std::lock_guard lock(mutex);
auto& node = nodes[index];
if (node.status != Node_Execution_Status::waiting_external)
return;
--waiting_external;
const std::uint64_t end = render_clock_now_ns();
if (auto* execution = executions[index]) {
execution->external_end_time_ns = end;
execution->end_time_ns = end;
}
if (error) {
fail_node_locked(index, std::move(error), end);
} else {
node.status = Node_Execution_Status::complete;
if (auto* execution = executions[index])
execution->status = Node_Execution_Status::complete;
--unfinished_nodes;
unlock_successors_locked(index, ready);
}
finish_if_terminal_locked();
}
submit(ready);
}
void unlock_successors_locked(std::size_t index,
std::vector<std::size_t>& ready) {
if (failed)
return;
for (const std::size_t successor : successors[index]) {
auto& state = nodes[successor];
if (state.remaining_predecessors == 0)
continue;
--state.remaining_predecessors;
if (state.remaining_predecessors == 0)
make_ready_locked(successor, ready);
}
}
void fail_node_locked(std::size_t index, std::exception_ptr error,
std::uint64_t end) {
auto& node = nodes[index];
node.status = Node_Execution_Status::failed;
if (auto* execution = executions[index]) {
if (execution->start_time_ns == 0)
execution->start_time_ns = end;
if (execution->cpu_end_time_ns == 0)
execution->cpu_end_time_ns = end;
execution->end_time_ns = end;
execution->status = Node_Execution_Status::failed;
}
fail_graph_locked(std::move(error));
}
void fail_graph_locked(std::exception_ptr error) {
failed = true;
if (!first_exception)
first_exception = std::move(error);
}
void finish_if_terminal_locked() {
if (terminal)
return;
if ((!failed && unfinished_nodes == 0) ||
(failed && active_tasks == 0 && waiting_external == 0)) {
terminal = true;
completion.notify_all();
}
}
std::vector<Node_State> nodes;
std::vector<std::vector<std::size_t>> successors;
std::vector<Node_Execution*> executions;
Execute_Node execute_node;
Schedule schedule;
Current_Worker_Id current_worker_id;
std::mutex mutex;
std::condition_variable completion;
std::exception_ptr first_exception;
std::size_t unfinished_nodes{};
std::size_t active_tasks{};
std::size_t waiting_external{};
bool started{};
bool failed{};
bool terminal{};
};
Render_Graph_Runtime::Render_Graph_Runtime(
const Render_Plan& plan,
std::span<Node_Execution* const> executions,
Execute_Node execute_node,
Schedule schedule,
Current_Worker_Id current_worker_id)
: state_(std::make_shared<State>(
plan, executions, std::move(execute_node), std::move(schedule),
std::move(current_worker_id))) {}
Render_Graph_Runtime::~Render_Graph_Runtime() = default;
void Render_Graph_Runtime::execute() {
state_->execute();
}
} // namespace renderive::render_graph::detail
@@ -0,0 +1,41 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <span>
#include "renderive/render_graph/External_Operation.hpp"
#include "renderive/render_graph/Render_Plan.hpp"
#include "renderive/scene/base/Abstract_Frame.hpp"
namespace renderive::render_graph::detail {
class Render_Graph_Runtime final {
public:
using Execute_Node = std::function<Node_Execution_Result(
std::size_t execution_index, Node_Execution_Metrics* metrics)>;
using Schedule = std::function<void(std::function<void()>)>;
using Current_Worker_Id = std::function<std::uint32_t()>;
Render_Graph_Runtime(const Render_Plan& plan,
std::span<Node_Execution* const> executions,
Execute_Node execute_node,
Schedule schedule,
Current_Worker_Id current_worker_id);
~Render_Graph_Runtime();
Render_Graph_Runtime(const Render_Graph_Runtime&) = delete;
Render_Graph_Runtime& operator=(const Render_Graph_Runtime&) = delete;
Render_Graph_Runtime(Render_Graph_Runtime&&) = delete;
Render_Graph_Runtime& operator=(Render_Graph_Runtime&&) = delete;
void execute();
private:
struct State;
std::shared_ptr<State> state_;
};
} // namespace renderive::render_graph::detail
@@ -23,6 +23,18 @@ std::uint64_t Node_Execution::duration_ns() const noexcept {
return end_time_ns >= start_time_ns ? end_time_ns - start_time_ns : 0;
}
std::uint64_t Node_Execution::cpu_duration_ns() const noexcept {
return cpu_end_time_ns >= start_time_ns
? cpu_end_time_ns - start_time_ns
: 0;
}
std::uint64_t Node_Execution::external_duration_ns() const noexcept {
return external_end_time_ns >= external_start_time_ns
? external_end_time_ns - external_start_time_ns
: 0;
}
std::uint64_t Frame_Snapshot::render_duration_ns() const noexcept {
return render_end_ns >= render_start_ns ? render_end_ns - render_start_ns : 0;
}
@@ -8,7 +8,9 @@
enum class Node_Execution_Status : std::uint8_t {
pending,
ready,
running,
waiting_external,
complete,
failed
};
@@ -19,9 +21,26 @@ enum class Node_Metric_Kind : std::uint8_t {
prepared_cells,
pixel_count,
primitive_count,
queue_wait_ns,
apply_duration_ns,
plan_emit_duration_ns,
backend_execute_duration_ns,
submit_duration_ns,
gpu_fence_wait_ns,
gpu_render_duration_ns,
gpu_copy_duration_ns,
readback_duration_ns,
backend_resource_version,
backend_frame_index,
backend_artifact_status,
backend_validation_code,
backend_validation_command,
backend_artifact_json_bytes,
count
};
static_assert(static_cast<std::size_t>(Node_Metric_Kind::count) <= 32);
struct Node_Execution_Metrics {
void set(Node_Metric_Kind kind, std::uint64_t value) noexcept;
[[nodiscard]] bool contains(Node_Metric_Kind kind) const noexcept;
@@ -32,12 +51,18 @@ struct Node_Execution_Metrics {
struct Node_Execution {
Render_Node_Id node_id{};
std::uint64_t ready_time_ns{};
std::uint64_t start_time_ns{};
std::uint64_t cpu_end_time_ns{};
std::uint64_t external_start_time_ns{};
std::uint64_t external_end_time_ns{};
std::uint64_t end_time_ns{};
std::uint32_t worker_id{};
Node_Execution_Status status{Node_Execution_Status::pending};
Node_Execution_Metrics metrics;
[[nodiscard]] std::uint64_t duration_ns() const noexcept;
[[nodiscard]] std::uint64_t cpu_duration_ns() const noexcept;
[[nodiscard]] std::uint64_t external_duration_ns() const noexcept;
};
struct Frame_Snapshot {
+68 -85
View File
@@ -19,6 +19,7 @@
#include "renderive/renderable/Renderive_Id_Allocator.hpp"
#include "renderive/renderable/base/Renderable_Base_p.hpp"
#include "renderive/renderable/color/Color_Cache.hpp"
#include "renderive/render_graph/detail/Render_Graph_Runtime.hpp"
#include "renderive/state/base/State_Strategy_Base.hpp"
static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0");
@@ -923,7 +924,7 @@ void Scene_Base::render_loop() {
task.snapshot, task.plan});
std::exception_ptr exception;
try {
execute_taskflow(task);
execute_render_graph(task);
d_func().dispatch({Observation_Event::render_completed, d_func().now_ns(),
snapshot.render_sequence,
snapshot.scene_state_revision_,
@@ -1108,7 +1109,7 @@ std::shared_ptr<const Render_Plan> Scene_Base::compile_render_plan(
Execution_Binding{{}, std::nullopt,
Scene_Render_Node_Function{
[renderer](const Scene_Render_Context& context) {
renderer->render_scene(context);
return renderer->render_scene(context);
}}});
for (const Render_Node_Id terminal : terminals)
append_edge(terminal, scene_render_node_id_);
@@ -1128,7 +1129,7 @@ std::shared_ptr<const Render_Plan> Scene_Base::compile_render_plan(
return plan;
}
void Scene_Base::execute_taskflow(Render_Task& task) {
void Scene_Base::execute_render_graph(Render_Task& task) {
Abstract_Frame* const frame = std::visit(
[]<class Frame>(Frame& value) -> Abstract_Frame* {
if constexpr (std::same_as<Frame, std::shared_ptr<Abstract_Frame>>)
@@ -1148,93 +1149,75 @@ void Scene_Base::execute_taskflow(Render_Task& task) {
state.paint_buffer_->clear();
}
tf::Taskflow taskflow;
std::vector<tf::Task> tasks;
tasks.reserve(task.plan->graph.nodes.size());
const bool capture = capture_ticket.capture;
frame->begin_render(snapshot.render_sequence, *task.plan, capture,
capture ? render_clock_now_ns() : 0);
frame_active = true;
for (const auto& node : task.plan->graph.nodes) {
tasks.push_back(taskflow.emplace([
this, &task, &snapshot, frame, node
](tf::Runtime& runtime) {
Node_Execution* execution =
frame->execution_slot(node.execution_index);
Node_Execution_Metrics* metrics =
execution ? &execution->metrics : nullptr;
const auto run = [&] {
const auto& binding =
task.execution_bindings.at(node.execution_index);
switch (node.kind) {
case Render_Node_Kind::prepare: {
const auto& state = snapshot.renderables.at(
binding.renderable_index.value());
std::get<Prepare_Render_Node_Function>(binding.function)(
Prepare_Render_Context{snapshot, state, metrics});
break;
}
case Render_Node_Kind::paint: {
const auto& state = snapshot.renderables.at(
binding.renderable_index.value());
if (!state.paint_buffer_)
throw std::logic_error(
"paint node has no frame color cache");
std::get<Paint_Render_Node_Function>(binding.function)(
Paint_Render_Context{snapshot, state,
*state.paint_buffer_, metrics});
break;
}
case Render_Node_Kind::composite: {
const Renderable_Frame_State* state =
binding.renderable_index
? &snapshot.renderables.at(
*binding.renderable_index)
: nullptr;
const Color_Cache* cache =
state && state->paint_buffer_
? state->paint_buffer_.get()
: nullptr;
std::get<Composite_Render_Node_Function>(binding.function)(
Composite_Render_Context{snapshot, state, cache,
metrics});
break;
}
case Render_Node_Kind::render:
std::get<Scene_Render_Node_Function>(binding.function)(
Scene_Render_Context{snapshot, metrics});
break;
}
};
if (!execution) {
Render_Execution_Scope scope(*this);
run();
return;
}
execution->worker_id =
static_cast<std::uint32_t>(runtime.worker().id());
execution->start_time_ns = render_clock_now_ns();
execution->status = Node_Execution_Status::running;
Render_Execution_Scope scope(*this);
try {
run();
execution->end_time_ns = render_clock_now_ns();
execution->status = Node_Execution_Status::complete;
} catch (...) {
execution->end_time_ns = render_clock_now_ns();
execution->status = Node_Execution_Status::failed;
throw;
}
}).name(node.name));
}
std::unordered_map<Render_Node_Id, std::size_t> node_indices;
node_indices.reserve(task.plan->graph.nodes.size());
std::vector<Node_Execution*> execution_slots(
task.plan->graph.nodes.size());
for (const auto& node : task.plan->graph.nodes)
node_indices.emplace(node.node_id, node.execution_index);
for (const auto& edge : task.plan->graph.edges)
tasks.at(node_indices.at(edge.from))
.precede(tasks.at(node_indices.at(edge.to)));
Execution_Context::executor().run(taskflow).get();
execution_slots[node.execution_index] =
frame->execution_slot(node.execution_index);
auto& executor = Execution_Context::executor();
renderive::render_graph::detail::Render_Graph_Runtime runtime(
*task.plan, execution_slots,
[this, &task, &snapshot](
std::size_t execution_index,
Node_Execution_Metrics* metrics) -> Node_Execution_Result {
const auto& node = task.plan->graph.nodes.at(execution_index);
const auto& binding =
task.execution_bindings.at(execution_index);
Render_Execution_Scope scope(*this);
switch (node.kind) {
case Render_Node_Kind::prepare: {
const auto& state = snapshot.renderables.at(
binding.renderable_index.value());
std::get<Prepare_Render_Node_Function>(binding.function)(
Prepare_Render_Context{snapshot, state, metrics});
return Node_Execution_Result::completed();
}
case Render_Node_Kind::paint: {
const auto& state = snapshot.renderables.at(
binding.renderable_index.value());
if (!state.paint_buffer_)
throw std::logic_error(
"paint node has no frame color cache");
std::get<Paint_Render_Node_Function>(binding.function)(
Paint_Render_Context{snapshot, state,
*state.paint_buffer_, metrics});
return Node_Execution_Result::completed();
}
case Render_Node_Kind::composite: {
const Renderable_Frame_State* state =
binding.renderable_index
? &snapshot.renderables.at(
*binding.renderable_index)
: nullptr;
const Color_Cache* cache = state && state->paint_buffer_
? state->paint_buffer_.get()
: nullptr;
std::get<Composite_Render_Node_Function>(binding.function)(
Composite_Render_Context{snapshot, state, cache,
metrics});
return Node_Execution_Result::completed();
}
case Render_Node_Kind::render:
return std::get<Scene_Render_Node_Function>(binding.function)(
Scene_Render_Context{snapshot, metrics});
}
throw std::logic_error("unknown render node kind");
},
[&executor](std::function<void()> function) {
executor.silent_async(std::move(function));
},
[&executor] {
const int worker = executor.this_worker_id();
return worker < 0
? std::numeric_limits<std::uint32_t>::max()
: static_cast<std::uint32_t>(worker);
});
runtime.execute();
for (const auto& state : snapshot.renderables) {
if (state.prepare_required)
@@ -23,6 +23,7 @@
#include "renderive/renderable/Renderable_Graph_Builder.hpp"
#include "renderive/renderable/base/Renderable_Base.hpp"
#include "renderive/renderable/base/Renderive_Owner.hpp"
#include "renderive/render_graph/External_Operation.hpp"
#include "renderive/render_graph/Render_Plan.hpp"
#include "renderive/scene/Inheritance.hpp"
#include "renderive/scene/dependency/Dependency_Resolver.hpp"
@@ -56,7 +57,8 @@ public:
class Scene_Renderer {
public:
virtual ~Scene_Renderer() = default;
virtual void render_scene(const Scene_Render_Context& context) = 0;
virtual Node_Execution_Result render_scene(
const Scene_Render_Context& context) = 0;
};
class Scene_Base {
@@ -215,7 +217,7 @@ private:
using Composite_Render_Node_Function =
std::function<void(const Composite_Render_Context&)>;
using Scene_Render_Node_Function =
std::function<void(const Scene_Render_Context&)>;
std::function<Node_Execution_Result(const Scene_Render_Context&)>;
using Execution_Function =
std::variant<Prepare_Render_Node_Function, Paint_Render_Node_Function,
Composite_Render_Node_Function,
@@ -265,7 +267,7 @@ private:
std::shared_ptr<const Render_Plan> compile_render_plan(
const Frame_Render_Snapshot& snapshot, Render_Task& task);
void render_loop();
void execute_taskflow(Render_Task& task);
void execute_render_graph(Render_Task& task);
void validate_renderable_scene(const Renderable_Base& renderable) const;
[[nodiscard]] bool is_renderable_attached_locked(
const Renderable& renderable) const;
@@ -67,18 +67,21 @@ TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_f
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 start,
std::uint64_t end, std::uint32_t worker) {
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, 110, 150, 0);
complete(1, 115, 145, 1);
complete(2, 160, 190, 1);
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);
@@ -124,7 +127,9 @@ TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_
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<std::uint32_t>(slot);
execution->status = Node_Execution_Status::complete;
@@ -141,6 +146,43 @@ TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_
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();
@@ -0,0 +1,234 @@
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <exception>
#include <functional>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <thread>
#include <vector>
#include "renderive/render_graph/External_Operation.hpp"
#include "renderive/render_graph/Render_Plan.hpp"
#include "renderive/render_graph/detail/Render_Graph_Runtime.hpp"
namespace {
class Single_Worker_Scheduler final {
public:
Single_Worker_Scheduler()
: worker_([this] { run(); }) {}
~Single_Worker_Scheduler() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
ready_.notify_all();
worker_.join();
}
void schedule(std::function<void()> function) {
{
std::lock_guard lock(mutex_);
if (stopping_)
throw std::logic_error("scheduler is stopping");
queue_.push_back(std::move(function));
}
ready_.notify_one();
}
private:
void run() {
for (;;) {
std::function<void()> function;
{
std::unique_lock lock(mutex_);
ready_.wait(lock, [this] {
return stopping_ || !queue_.empty();
});
if (stopping_ && queue_.empty())
return;
function = std::move(queue_.front());
queue_.pop_front();
}
function();
}
}
std::mutex mutex_;
std::condition_variable ready_;
std::deque<std::function<void()>> queue_;
std::thread worker_;
bool stopping_{};
};
std::shared_ptr<const Render_Plan> two_node_plan() {
Render_Graph_Builder builder;
const auto submit = builder.emplace(
301, 30, "Submit GPU", Render_Node_Kind::render);
const auto publish = builder.emplace(
302, 30, "Publish Frame", Render_Node_Kind::composite);
builder.precede(submit, publish);
Render_Plan_History history;
return history.publish(std::move(builder).finish());
}
std::vector<Node_Execution*> execution_slots(
const Render_Plan& plan, std::vector<Node_Execution>& storage) {
storage.resize(plan.graph.nodes.size());
std::vector<Node_Execution*> result(storage.size());
for (const auto& node : plan.graph.nodes) {
storage[node.execution_index].node_id = node.node_id;
result[node.execution_index] = &storage[node.execution_index];
}
return result;
}
} // namespace
TEST(external_operation_test, completion_before_subscription_is_delivered_once) {
External_Operation_Source source;
const auto operation = source.operation();
EXPECT_TRUE(source.complete());
EXPECT_FALSE(source.complete());
int completion_count{};
operation.on_complete([&](std::exception_ptr error) {
EXPECT_FALSE(error);
++completion_count;
});
EXPECT_EQ(completion_count, 1);
}
TEST(render_graph_runtime_test,
external_successor_stays_blocked_until_operation_completes) {
const auto plan = two_node_plan();
Single_Worker_Scheduler scheduler;
External_Operation_Source source;
std::vector<Node_Execution> execution_storage;
auto slots = execution_slots(*plan, execution_storage);
std::atomic<bool> submit_started{};
std::atomic<bool> publish_executed{};
renderive::render_graph::detail::Render_Graph_Runtime runtime(
*plan, slots,
[&](std::size_t index, Node_Execution_Metrics*) {
if (index == 0) {
submit_started.store(true, std::memory_order_release);
submit_started.notify_all();
return Node_Execution_Result::external(source.operation());
}
publish_executed.store(true, std::memory_order_release);
return Node_Execution_Result::completed();
},
[&](std::function<void()> function) {
scheduler.schedule(std::move(function));
},
[] { return 0U; });
std::thread execution([&] { runtime.execute(); });
submit_started.wait(false, std::memory_order_acquire);
EXPECT_FALSE(publish_executed.load(std::memory_order_acquire));
EXPECT_EQ(execution_storage[0].status,
Node_Execution_Status::waiting_external);
EXPECT_TRUE(source.complete());
execution.join();
EXPECT_TRUE(publish_executed.load(std::memory_order_acquire));
EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::complete);
EXPECT_EQ(execution_storage[1].status, Node_Execution_Status::complete);
EXPECT_GT(execution_storage[0].cpu_end_time_ns, 0U);
EXPECT_GE(execution_storage[0].external_end_time_ns,
execution_storage[0].external_start_time_ns);
}
TEST(render_graph_runtime_test,
failed_external_node_prevents_its_successor_from_running) {
const auto plan = two_node_plan();
Single_Worker_Scheduler scheduler;
External_Operation_Source source;
std::atomic<bool> submit_started{};
std::atomic<bool> publish_executed{};
std::exception_ptr graph_error;
renderive::render_graph::detail::Render_Graph_Runtime runtime(
*plan, {},
[&](std::size_t index, Node_Execution_Metrics*) {
if (index == 0) {
submit_started.store(true, std::memory_order_release);
submit_started.notify_all();
return Node_Execution_Result::external(source.operation());
}
publish_executed.store(true, std::memory_order_release);
return Node_Execution_Result::completed();
},
[&](std::function<void()> function) {
scheduler.schedule(std::move(function));
},
[] { return 0U; });
std::thread execution([&] {
try {
runtime.execute();
} catch (...) {
graph_error = std::current_exception();
}
});
submit_started.wait(false, std::memory_order_acquire);
EXPECT_TRUE(source.fail(
std::make_exception_ptr(std::runtime_error("GPU submission failed"))));
execution.join();
ASSERT_TRUE(graph_error);
EXPECT_THROW(std::rethrow_exception(graph_error), std::runtime_error);
EXPECT_FALSE(publish_executed.load(std::memory_order_acquire));
}
TEST(render_graph_runtime_test,
pending_external_operation_does_not_occupy_scheduler_worker) {
Render_Graph_Builder builder;
builder.emplace(401, 40, "GPU Fence", Render_Node_Kind::render);
Render_Plan_History history;
const auto plan = history.publish(std::move(builder).finish());
Single_Worker_Scheduler scheduler;
External_Operation_Source source;
std::atomic<bool> external_started{};
std::mutex probe_mutex;
std::condition_variable probe_completed;
bool probe_ran{};
renderive::render_graph::detail::Render_Graph_Runtime runtime(
*plan, {},
[&](std::size_t, Node_Execution_Metrics*) {
external_started.store(true, std::memory_order_release);
external_started.notify_all();
return Node_Execution_Result::external(source.operation());
},
[&](std::function<void()> function) {
scheduler.schedule(std::move(function));
},
[] { return 0U; });
std::thread execution([&] { runtime.execute(); });
external_started.wait(false, std::memory_order_acquire);
scheduler.schedule([&] {
{
std::lock_guard lock(probe_mutex);
probe_ran = true;
}
probe_completed.notify_one();
});
{
std::unique_lock lock(probe_mutex);
EXPECT_TRUE(probe_completed.wait_for(
lock, std::chrono::seconds(1), [&] { return probe_ran; }));
}
EXPECT_TRUE(source.complete());
execution.join();
}
@@ -40,8 +40,10 @@ struct Scene3D_Prepared_Data_Renderable : Renderable_Test_Harness {
struct Scene3D_Render_Node_Test_Scene final
: Scene3D_Context<>, Scene_Renderer {
void render_scene(const Scene_Render_Context& context) override {
Node_Execution_Result render_scene(
const Scene_Render_Context& context) override {
prepared = context.prepared<Prepared_Mesh_Count>(owner);
return Node_Execution_Result::completed();
}
Renderable_Id owner{};
@@ -96,9 +98,11 @@ struct Scene3D_Playback_Snapshot_Test_Scene final
wait_for_render();
}
void render_scene(const Scene_Render_Context& context) override {
Node_Execution_Result render_scene(
const Scene_Render_Context& context) override {
rendered.push_back(
context.frame.scene_state<Scene3D_Historical_State>().value);
return Node_Execution_Result::completed();
}
std::vector<int> rendered;