taskflow 接入异步 datoviz性能监测
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user