引入 oneTBB 删除taskflow
This commit is contained in:
+15
-6
@@ -1,5 +1,5 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/rely.cmake)
|
||||
set(Renderive_Kernel_dependencies global::taskflow render_kernel::oneTBB)
|
||||
set(Renderive_Kernel_dependencies render_kernel::oneTBB)
|
||||
if (RENDERIVE_BUILD_TESTS)
|
||||
set(Renderive_Kernel_test_targets)
|
||||
list(APPEND Renderive_Kernel_dependencies global::GTest)
|
||||
@@ -12,7 +12,6 @@ if (Renderive_Kernel_dependencies_installed)
|
||||
return()
|
||||
endif ()
|
||||
rcl_load_dependency_environment(${Renderive_Kernel_dependencies})
|
||||
find_package(Taskflow CONFIG REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(TBB CONFIG REQUIRED)
|
||||
if (RENDERIVE_BUILD_TESTS)
|
||||
@@ -25,9 +24,18 @@ target_include_directories(Renderive_Kernel PUBLIC
|
||||
"$<BUILD_INTERFACE:${Renderive_Kernel_source_dir}>"
|
||||
)
|
||||
target_compile_features(Renderive_Kernel PUBLIC cxx_std_20)
|
||||
target_link_libraries(Renderive_Kernel PRIVATE Taskflow::Taskflow)
|
||||
target_link_libraries(Renderive_Kernel PRIVATE TBB::tbb)
|
||||
target_link_libraries(Renderive_Kernel PUBLIC TBB::tbb)
|
||||
target_link_libraries(Renderive_Kernel PRIVATE Threads::Threads)
|
||||
function(renderive_stage_kernel_runtime target)
|
||||
if (WIN32)
|
||||
add_custom_command(TARGET "${target}" POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<TARGET_FILE:TBB::tbb>"
|
||||
"$<TARGET_FILE_DIR:${target}>"
|
||||
COMMENT "Staging oneTBB runtime for ${target}"
|
||||
VERBATIM)
|
||||
endif ()
|
||||
endfunction()
|
||||
if (MSVC)
|
||||
target_compile_options(Renderive_Kernel PRIVATE /utf-8)
|
||||
endif ()
|
||||
@@ -47,10 +55,11 @@ if (RENDERIVE_BUILD_TESTS)
|
||||
add_executable("${Renderive_Kernel_test_target}" "${Renderive_Kernel_test_source}")
|
||||
target_include_directories("${Renderive_Kernel_test_target}" PRIVATE
|
||||
"${Renderive_Kernel_test_dir}"
|
||||
"${CMAKE_SOURCE_DIR}/render_2D"
|
||||
"${CMAKE_SOURCE_DIR}/render_3D"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../render_2D"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../render_3D"
|
||||
)
|
||||
target_link_libraries("${Renderive_Kernel_test_target}" PRIVATE Renderive_Kernel GTest::gtest_main)
|
||||
renderive_stage_kernel_runtime("${Renderive_Kernel_test_target}")
|
||||
add_test(NAME "${Renderive_Kernel_test_target}" COMMAND "${Renderive_Kernel_test_target}")
|
||||
set_tests_properties("${Renderive_Kernel_test_target}" PROPERTIES LABELS "Renderive_Kernel")
|
||||
list(APPEND Renderive_Kernel_test_targets "${Renderive_Kernel_test_target}")
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
include_guard(GLOBAL)
|
||||
block()
|
||||
rcl_init(render_kernel)
|
||||
set(oneTBB_option ${base_options})
|
||||
list(APPEND oneTBB_option
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
-DTBB_BUILD=ON
|
||||
-DTBB_INSTALL=ON
|
||||
-DTBB_TEST=OFF
|
||||
-DTBB_EXAMPLES=OFF
|
||||
-DTBB_STRICT=OFF
|
||||
-DTBB4PY_BUILD=OFF
|
||||
-DTBBMALLOC_BUILD=OFF
|
||||
-DTBBMALLOC_PROXY_BUILD=OFF
|
||||
)
|
||||
_register_git_cmake_library(render_kernel::oneTBB
|
||||
"https://github.com/uxlfoundation/oneTBB.git"
|
||||
"v2023.1.0"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <oneapi/tbb/concurrent_queue.h>
|
||||
#include <memory_resource>
|
||||
#include <stdexcept>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include "renderive/base/Concepts.hpp"
|
||||
@@ -93,8 +95,9 @@ public:
|
||||
Memory_Resource_Unique_Ptr<Frame> frame_;
|
||||
std::unique_lock<Mutex> lease_lock_;
|
||||
};
|
||||
explicit Flow_Refresh_Strategy(Observer observer = {});
|
||||
Flow_Refresh_Strategy(std::pmr::memory_resource& memory_resource, Observer observer = {});
|
||||
static constexpr std::size_t default_capacity = 64;
|
||||
explicit Flow_Refresh_Strategy(Observer observer = {}, std::size_t capacity = default_capacity);
|
||||
Flow_Refresh_Strategy(std::pmr::memory_resource& memory_resource, Observer observer = {}, std::size_t capacity = default_capacity);
|
||||
Painter_Lease acquire_painter();
|
||||
Render_Lease acquire_renderer();
|
||||
void swap() override;
|
||||
@@ -110,7 +113,8 @@ private:
|
||||
inline static thread_local Flow_Refresh_Strategy* rendering_strategy_{};
|
||||
std::pmr::memory_resource* const memory_resource_;
|
||||
Observer observer_;
|
||||
std::pmr::deque<Memory_Resource_Unique_Ptr<Frame>> frames_;
|
||||
using Frame_Ptr = Memory_Resource_Unique_Ptr<Frame>;
|
||||
oneapi::tbb::concurrent_bounded_queue<Frame_Ptr> frames_;
|
||||
std::uint64_t next_sequence_{};
|
||||
std::uint64_t real_time_data_update_sequence_{};
|
||||
Real_Time_Data_Update_State last_real_time_data_update_;
|
||||
|
||||
@@ -19,13 +19,13 @@ Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Painter_Lease::~Painter_Lea
|
||||
frame_->statistics.paint_end_time_ns = strategy_->now_ns();
|
||||
frame_->statistics.paint_duration_ns = frame_->statistics.paint_end_time_ns - frame_->statistics.paint_begin_time_ns;
|
||||
frame_->statistics.queue_enter_time_ns = strategy_->now_ns();
|
||||
const Statistics statistics = frame_->statistics;
|
||||
strategy_->frames_.push(std::move(frame_));
|
||||
Observation observation;
|
||||
{
|
||||
std::lock_guard<Mutex> lock(strategy_->state_mutex_);
|
||||
const Statistics statistics = frame_->statistics;
|
||||
strategy_->frames_.push_back(std::move(frame_));
|
||||
++strategy_->state_.enqueued_frame_count;
|
||||
strategy_->state_.pending_frame_count = strategy_->frames_.size();
|
||||
strategy_->state_.pending_frame_count = static_cast<std::size_t>(std::max<std::ptrdiff_t>(0, strategy_->frames_.size()));
|
||||
strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0);
|
||||
observation = {Observation_Event::enqueued, statistics, strategy_->state_, strategy_->last_real_time_data_update_};
|
||||
}
|
||||
@@ -69,20 +69,17 @@ Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Render_Lease::Render_Lease(
|
||||
lease_lock_ = std::unique_lock<Mutex>(strategy.render_mutex_);
|
||||
previous_render_strategy_ = std::exchange(rendering_strategy_, &strategy);
|
||||
Observation observation;
|
||||
bool has_frame{};
|
||||
const bool has_frame = strategy.frames_.try_pop(frame_);
|
||||
{
|
||||
std::lock_guard<Mutex> lock(strategy.state_mutex_);
|
||||
if (!strategy.frames_.empty()) {
|
||||
frame_ = std::move(strategy.frames_.front());
|
||||
strategy.frames_.pop_front();
|
||||
if (has_frame) {
|
||||
frame_->statistics.queue_leave_time_ns = strategy.now_ns();
|
||||
frame_->statistics.queue_wait_ns = frame_->statistics.queue_leave_time_ns - frame_->statistics.queue_enter_time_ns;
|
||||
frame_->statistics.render_begin_time_ns = strategy.now_ns();
|
||||
++strategy.state_.dequeued_frame_count;
|
||||
strategy.state_.pending_frame_count = strategy.frames_.size();
|
||||
strategy.state_.pending_frame_count = static_cast<std::size_t>(std::max<std::ptrdiff_t>(0, strategy.frames_.size()));
|
||||
strategy.update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0);
|
||||
observation = {Observation_Event::dequeued, frame_->statistics, strategy.state_, strategy.last_real_time_data_update_};
|
||||
has_frame = true;
|
||||
} else {
|
||||
++strategy.state_.empty_acquire_count;
|
||||
strategy.state_.pending_frame_count = 0;
|
||||
@@ -110,7 +107,7 @@ Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Render_Lease::~Render_Lease
|
||||
{
|
||||
std::lock_guard<Mutex> lock(strategy_->state_mutex_);
|
||||
++strategy_->state_.rendered_frame_count;
|
||||
strategy_->state_.pending_frame_count = strategy_->frames_.size();
|
||||
strategy_->state_.pending_frame_count = static_cast<std::size_t>(std::max<std::ptrdiff_t>(0, strategy_->frames_.size()));
|
||||
strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0);
|
||||
observation = {Observation_Event::rendered, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_};
|
||||
frame_.reset();
|
||||
@@ -148,11 +145,15 @@ auto Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Render_Lease::get() co
|
||||
return frame_.get();
|
||||
}
|
||||
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
|
||||
Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Flow_Refresh_Strategy(Observer observer)
|
||||
: Flow_Refresh_Strategy(*std::pmr::get_default_resource(), std::move(observer)) {}
|
||||
Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Flow_Refresh_Strategy(Observer observer, std::size_t capacity)
|
||||
: Flow_Refresh_Strategy(*std::pmr::get_default_resource(), std::move(observer), capacity) {}
|
||||
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
|
||||
Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Flow_Refresh_Strategy(std::pmr::memory_resource& memory_resource, Observer observer)
|
||||
: Frame_Control_Strategy_Base(), memory_resource_(&memory_resource), observer_(std::move(observer)), frames_(&memory_resource) {}
|
||||
Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::Flow_Refresh_Strategy(std::pmr::memory_resource& memory_resource, Observer observer, std::size_t capacity)
|
||||
: Frame_Control_Strategy_Base(), memory_resource_(&memory_resource), observer_(std::move(observer)) {
|
||||
if (capacity == 0)
|
||||
throw std::invalid_argument("flow refresh queue capacity must be greater than zero");
|
||||
frames_.set_capacity(static_cast<typename decltype(frames_)::size_type>(capacity));
|
||||
}
|
||||
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
|
||||
auto Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::acquire_painter() -> Painter_Lease {
|
||||
return Painter_Lease(*this);
|
||||
@@ -179,14 +180,13 @@ auto Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::frame_control_state()
|
||||
}
|
||||
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
|
||||
std::size_t Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::pending_frame_count() const {
|
||||
std::lock_guard<Mutex> lock(state_mutex_);
|
||||
return frames_.size();
|
||||
return static_cast<std::size_t>(std::max<std::ptrdiff_t>(0, frames_.size()));
|
||||
}
|
||||
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
|
||||
auto Flow_Refresh_Strategy<Scene_Frame, Mutex, Observer>::state() const -> State {
|
||||
std::lock_guard<Mutex> lock(state_mutex_);
|
||||
State result = state_;
|
||||
result.pending_frame_count = frames_.size();
|
||||
result.pending_frame_count = static_cast<std::size_t>(std::max<std::ptrdiff_t>(0, frames_.size()));
|
||||
return result;
|
||||
}
|
||||
template <class Scene_Frame, Mutex_Type Mutex, class Observer>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "Render_Graph_Runtime.hpp"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
@@ -8,347 +7,169 @@
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <oneapi/tbb/flow_graph.h>
|
||||
#include "renderive/scheduling/detail/OneTBB_Runtime.hpp"
|
||||
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};
|
||||
struct Render_Graph_Runtime::State : std::enable_shared_from_this<Render_Graph_Runtime::State> {
|
||||
using Message = oneapi::tbb::flow::continue_msg;
|
||||
using Ready_Node = oneapi::tbb::flow::continue_node<Message>;
|
||||
using Execute_Node_Type = oneapi::tbb::flow::async_node<Message, Message>;
|
||||
struct Node {
|
||||
std::unique_ptr<Ready_Node> ready;
|
||||
std::unique_ptr<Execute_Node_Type> execute;
|
||||
bool root{};
|
||||
};
|
||||
|
||||
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()) {
|
||||
State(const Render_Plan& plan, std::span<Node_Execution* const> execution_slots, Execute_Node execute)
|
||||
: executions(execution_slots.begin(), execution_slots.end()), execute_node(std::move(execute)) {
|
||||
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());
|
||||
executions.resize(plan.graph.nodes.size());
|
||||
if (executions.size() != plan.graph.nodes.size())
|
||||
throw std::invalid_argument("render graph execution slot count differs from plan");
|
||||
nodes.resize(plan.graph.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");
|
||||
indices.reserve(plan.graph.nodes.size());
|
||||
std::vector<std::size_t> predecessor_count(plan.graph.nodes.size());
|
||||
for (const auto& node : plan.graph.nodes)
|
||||
indices.emplace(node.node_id, node.execution_index);
|
||||
for (const auto& edge : plan.graph.edges)
|
||||
++predecessor_count.at(indices.at(edge.to));
|
||||
for (std::size_t index = 0; index < nodes.size(); ++index) {
|
||||
nodes[index].root = predecessor_count[index] == 0;
|
||||
nodes[index].ready = std::make_unique<Ready_Node>(graph, [this, index](const Message&) {
|
||||
mark_ready(index);
|
||||
return Message{};
|
||||
});
|
||||
nodes[index].execute = std::make_unique<Execute_Node_Type>(graph, oneapi::tbb::flow::unlimited, [this, index](const Message&, Execute_Node_Type::gateway_type& gateway) {
|
||||
run_node(index, gateway);
|
||||
});
|
||||
oneapi::tbb::flow::make_edge(*nodes[index].ready, *nodes[index].execute);
|
||||
}
|
||||
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;
|
||||
const std::size_t from = indices.at(edge.from);
|
||||
const std::size_t to = indices.at(edge.to);
|
||||
oneapi::tbb::flow::make_edge(*nodes[from].execute, *nodes[to].ready);
|
||||
}
|
||||
|
||||
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")));
|
||||
}
|
||||
if (started.exchange(true, std::memory_order_acq_rel))
|
||||
throw std::logic_error("render graph runtime already executed");
|
||||
auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena();
|
||||
arena.execute([this] {
|
||||
for (auto& node : nodes) {
|
||||
if (node.root)
|
||||
node.ready->try_put(Message{});
|
||||
}
|
||||
}
|
||||
submit(ready);
|
||||
|
||||
graph.wait_for_all();
|
||||
});
|
||||
std::exception_ptr error;
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
completion.wait(lock, [this] { return terminal; });
|
||||
std::lock_guard lock(error_mutex);
|
||||
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)
|
||||
void mark_ready(std::size_t index) noexcept {
|
||||
if (failed.load(std::memory_order_acquire))
|
||||
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 {
|
||||
void run_node(std::size_t index, Execute_Node_Type::gateway_type& gateway) noexcept {
|
||||
if (failed.load(std::memory_order_acquire))
|
||||
return;
|
||||
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;
|
||||
}
|
||||
if (auto* execution = executions[index]) {
|
||||
execution->start_time_ns = render_clock_now_ns();
|
||||
execution->worker_id = renderive::scheduling::detail::OneTBB_Runtime::instance().current_worker_id();
|
||||
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();
|
||||
fail(index, std::current_exception(), render_clock_now_ns());
|
||||
return;
|
||||
}
|
||||
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);
|
||||
complete(index, cpu_end);
|
||||
gateway.try_put(Message{});
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
gateway.reserve_wait();
|
||||
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));
|
||||
});
|
||||
auto* gateway_ptr = &gateway;
|
||||
result.operation().on_complete([self = std::move(self), gateway_ptr, index](std::exception_ptr error) {
|
||||
const std::uint64_t end = render_clock_now_ns();
|
||||
if (error) {
|
||||
self->fail(index, std::move(error), end);
|
||||
} else {
|
||||
self->complete_external(index, end);
|
||||
gateway_ptr->try_put(Message{});
|
||||
}
|
||||
gateway_ptr->release_wait();
|
||||
});
|
||||
} catch (...) {
|
||||
complete_external(index, std::current_exception());
|
||||
fail(index, std::current_exception(), render_clock_now_ns());
|
||||
gateway.release_wait();
|
||||
}
|
||||
}
|
||||
|
||||
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 complete(std::size_t index, std::uint64_t end) noexcept {
|
||||
if (auto* execution = executions[index]) {
|
||||
execution->cpu_end_time_ns = end;
|
||||
execution->end_time_ns = end;
|
||||
execution->status = Node_Execution_Status::complete;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
void complete_external(std::size_t index, std::uint64_t end) noexcept {
|
||||
if (auto* execution = executions[index]) {
|
||||
execution->external_end_time_ns = end;
|
||||
execution->end_time_ns = end;
|
||||
execution->status = Node_Execution_Status::complete;
|
||||
}
|
||||
}
|
||||
void fail(std::size_t index, std::exception_ptr error, std::uint64_t end) noexcept {
|
||||
failed.store(true, std::memory_order_release);
|
||||
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;
|
||||
if (execution->status == Node_Execution_Status::waiting_external)
|
||||
execution->external_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;
|
||||
std::lock_guard lock(error_mutex);
|
||||
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;
|
||||
oneapi::tbb::flow::graph graph;
|
||||
std::vector<Node> nodes;
|
||||
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::mutex error_mutex;
|
||||
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{};
|
||||
std::atomic_bool started{};
|
||||
std::atomic_bool failed{};
|
||||
};
|
||||
|
||||
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(const Render_Plan& plan, std::span<Node_Execution* const> executions, Execute_Node execute_node)
|
||||
: state_(std::make_shared<State>(plan, executions, std::move(execute_node))) {
|
||||
}
|
||||
Render_Graph_Runtime::~Render_Graph_Runtime() = default;
|
||||
|
||||
void Render_Graph_Runtime::execute() {
|
||||
state_->execute();
|
||||
}
|
||||
|
||||
} // namespace renderive::render_graph::detail
|
||||
}
|
||||
|
||||
@@ -1,41 +1,24 @@
|
||||
#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);
|
||||
using Execute_Node = std::function<Node_Execution_Result(std::size_t execution_index, Node_Execution_Metrics* metrics)>;
|
||||
Render_Graph_Runtime(const Render_Plan& plan, std::span<Node_Execution* const> executions, Execute_Node execute_node);
|
||||
~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
|
||||
}
|
||||
|
||||
@@ -13,23 +13,62 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <taskflow/taskflow.hpp>
|
||||
#include <oneapi/tbb/concurrent_queue.h>
|
||||
#include <oneapi/tbb/flow_graph.h>
|
||||
|
||||
#include "renderive/renderable/Render_Frame_Completion.hpp"
|
||||
#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/scheduling/detail/OneTBB_Runtime.hpp"
|
||||
#include "renderive/state/base/State_Strategy_Base.hpp"
|
||||
|
||||
static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0");
|
||||
|
||||
class Scene_Base::Execution_Context {
|
||||
public:
|
||||
static tf::Executor& executor() {
|
||||
static tf::Executor executor;
|
||||
return executor;
|
||||
using Message = oneapi::tbb::flow::continue_msg;
|
||||
using Operation = std::function<void()>;
|
||||
Execution_Context()
|
||||
: node_(graph_, oneapi::tbb::flow::serial, [this](const Message&) {
|
||||
execute_one();
|
||||
return Message{};
|
||||
}) {
|
||||
operations_.set_capacity(default_capacity);
|
||||
}
|
||||
~Execution_Context() {
|
||||
wait();
|
||||
}
|
||||
void submit(Operation operation) {
|
||||
operations_.push(std::move(operation));
|
||||
schedule();
|
||||
}
|
||||
void wait() noexcept {
|
||||
try {
|
||||
auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena();
|
||||
arena.execute([this] { graph_.wait_for_all(); });
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
private:
|
||||
void schedule() {
|
||||
if (scheduled_.exchange(true, std::memory_order_acq_rel))
|
||||
return;
|
||||
auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena();
|
||||
arena.execute([this] { node_.try_put(Message{}); });
|
||||
}
|
||||
void execute_one() {
|
||||
Operation operation;
|
||||
if (operations_.try_pop(operation))
|
||||
operation();
|
||||
scheduled_.store(false, std::memory_order_release);
|
||||
if (!operations_.empty())
|
||||
schedule();
|
||||
}
|
||||
static constexpr std::size_t default_capacity = 64;
|
||||
oneapi::tbb::flow::graph graph_;
|
||||
oneapi::tbb::concurrent_bounded_queue<Operation> operations_;
|
||||
oneapi::tbb::flow::function_node<Message, Message> node_;
|
||||
std::atomic_bool scheduled_{};
|
||||
};
|
||||
|
||||
class Scene_Base::Render_Execution_Scope {
|
||||
@@ -53,14 +92,12 @@ Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource,
|
||||
: scene_lifetime_(std::make_shared<Scene_Lifetime>(*this)),
|
||||
memory_domain_(std::allocate_shared<Scene_Memory_Domain>(std::pmr::polymorphic_allocator<Scene_Memory_Domain>(&upstream_memory_resource), upstream_memory_resource)),
|
||||
impl_(std::move(impl)),
|
||||
execution_context_(std::make_unique<Execution_Context>()),
|
||||
renderables_(&memory_domain_->resource()),
|
||||
dependency_resolver_(memory_domain_->resource()),
|
||||
color_caches_(&memory_domain_->resource()),
|
||||
task_(memory_domain_->resource()),
|
||||
renderable_edit_queue_(&memory_domain_->resource()),
|
||||
composite_begin_node_id_(allocate_renderive_node_id()),
|
||||
scene_render_node_id_(allocate_renderive_node_id()) {
|
||||
worker_ = std::thread([this] { render_loop(); });
|
||||
}
|
||||
Scene_Base::~Scene_Base() {
|
||||
shutdown();
|
||||
@@ -310,50 +347,51 @@ void Scene_Base::submit_render(Abstract_Frame* frame) {
|
||||
pending_exception_ = nullptr;
|
||||
pending_exception_observed_ = false;
|
||||
}
|
||||
if (!pending_exception_ && current_completion_ &&
|
||||
current_completion_->completed && current_completion_->exception &&
|
||||
!current_completion_->observed) {
|
||||
if (!pending_exception_ && current_completion_ && current_completion_->completed && current_completion_->exception && !current_completion_->observed)
|
||||
pending_exception_ = current_completion_->exception;
|
||||
}
|
||||
|
||||
auto& strategy = frame_control_strategy();
|
||||
strategy.swap();
|
||||
auto snapshot = frame ? std::exchange(frame->published_snapshot_, {})
|
||||
: nullptr;
|
||||
auto snapshot = frame ? std::exchange(frame->published_snapshot_, {}) : nullptr;
|
||||
if (!snapshot)
|
||||
snapshot = capture_live_frame();
|
||||
snapshot->next_refresh_interval_ns_ =
|
||||
strategy.frame_control_state().next_refresh_interval_ns;
|
||||
snapshot->next_refresh_interval_ns_ = strategy.frame_control_state().next_refresh_interval_ns;
|
||||
if (render_sequence_ == std::numeric_limits<std::uint64_t>::max())
|
||||
throw std::overflow_error("render sequence exhausted");
|
||||
snapshot->render_sequence = ++render_sequence_;
|
||||
|
||||
Render_Task task(memory_resource());
|
||||
task.frame = frame
|
||||
? Render_Task::Frame{std::ref(*frame)}
|
||||
: Render_Task::Frame{std::make_shared<Abstract_Frame>()};
|
||||
task.plan = compile_render_plan(*snapshot, task);
|
||||
task.topology =
|
||||
std::make_shared<Topology_Snapshot>(topology_snapshot());
|
||||
task.completion = std::make_shared<Render_Completion>();
|
||||
auto task = std::make_shared<Render_Task>(memory_resource());
|
||||
task->frame = frame ? Render_Task::Frame{std::ref(*frame)} : Render_Task::Frame{std::make_shared<Abstract_Frame>()};
|
||||
task->plan = compile_render_plan(*snapshot, *task);
|
||||
task->topology = std::make_shared<Topology_Snapshot>(topology_snapshot());
|
||||
task->completion = std::make_shared<Render_Completion>();
|
||||
snapshot->capture_ticket_ = capture_controller_.begin_frame();
|
||||
task.snapshot = std::move(snapshot);
|
||||
current_completion_ = task.completion;
|
||||
task->snapshot = std::move(snapshot);
|
||||
current_completion_ = task->completion;
|
||||
const Observation submitted_observation{
|
||||
Observation_Event::render_submitted, d_func().now_ns(),
|
||||
task.snapshot->render_sequence, task.snapshot->scene_state_revision_,
|
||||
task.snapshot->renderables.size(), task.topology, task.snapshot,
|
||||
task.plan};
|
||||
task_ = std::move(task);
|
||||
task_pending_ = true;
|
||||
Observation_Event::render_submitted, d_func().now_ns(), task->snapshot->render_sequence,
|
||||
task->snapshot->scene_state_revision_, task->snapshot->renderables.size(), task->topology,
|
||||
task->snapshot, task->plan};
|
||||
++pending_operations_;
|
||||
task_lock.unlock();
|
||||
|
||||
Scene_Base* previous_observer_scene =
|
||||
std::exchange(active_submitted_observer_scene_, this);
|
||||
Scene_Base* previous_observer_scene = std::exchange(active_submitted_observer_scene_, this);
|
||||
d_func().dispatch(submitted_observation);
|
||||
active_submitted_observer_scene_ = previous_observer_scene;
|
||||
task_ready_.notify_one();
|
||||
|
||||
try {
|
||||
execution_context_->submit([this, task = std::move(task)] {
|
||||
execute_render_task(task);
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
--pending_operations_;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
});
|
||||
} catch (...) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
--pending_operations_;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
throw;
|
||||
}
|
||||
std::size_t deferred_render_count{};
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
@@ -364,7 +402,7 @@ void Scene_Base::submit_render(Abstract_Frame* frame) {
|
||||
}
|
||||
|
||||
void Scene_Base::wait_for_render() {
|
||||
if (is_render_worker_thread() || active_submitted_observer_scene_ == this)
|
||||
if (is_render_execution_context() || active_submitted_observer_scene_ == this)
|
||||
return;
|
||||
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
|
||||
const auto completion = current_completion_;
|
||||
@@ -386,16 +424,37 @@ void Scene_Base::wait_for_render() {
|
||||
std::rethrow_exception(exception);
|
||||
}
|
||||
void Scene_Base::enqueue_renderable_edit(std::function<void()> edit) {
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
enqueue_renderable_edit_locked(std::move(edit));
|
||||
submit_operation([this, edit = std::move(edit)]() mutable {
|
||||
execute_renderable_edit(std::move(edit));
|
||||
});
|
||||
}
|
||||
void Scene_Base::enqueue_renderable_edit_locked(std::function<void()> edit) {
|
||||
if (stop_)
|
||||
throw std::logic_error("scene is shutting down");
|
||||
runtime_started_ = true;
|
||||
renderable_edit_queue_.push_back({std::move(edit)});
|
||||
task_ready_.notify_one();
|
||||
void Scene_Base::submit_operation(std::function<void()> operation) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
if (shutting_down_)
|
||||
throw std::logic_error("scene is shutting down");
|
||||
runtime_started_ = true;
|
||||
++pending_operations_;
|
||||
}
|
||||
try {
|
||||
execution_context_->submit([this, operation = std::move(operation)]() mutable {
|
||||
operation();
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
--pending_operations_;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
});
|
||||
} catch (...) {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
--pending_operations_;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void Scene_Base::cleanup_detached_topology_locked() {
|
||||
for (const Renderable_Id id : dependency_resolver_.ids()) {
|
||||
if (renderables_.contains(id))
|
||||
@@ -469,13 +528,15 @@ void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) {
|
||||
return;
|
||||
owner = iterator->second;
|
||||
}
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
if (!runtime_started_) {
|
||||
owner->d_func().reset_render_graph();
|
||||
notify_model_dirty();
|
||||
return;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
if (!runtime_started_) {
|
||||
owner->d_func().reset_render_graph();
|
||||
notify_model_dirty();
|
||||
return;
|
||||
}
|
||||
}
|
||||
enqueue_renderable_edit_locked([owner = std::move(owner)] {
|
||||
enqueue_renderable_edit([owner = std::move(owner)] {
|
||||
owner->d_func().reset_render_graph();
|
||||
owner->d_func().notify_scene_model_dirty();
|
||||
});
|
||||
@@ -727,10 +788,6 @@ std::vector<Plan_Version_Statistics> Scene_Base::plan_version_statistics(
|
||||
return analyze_plan_versions(analyses);
|
||||
}
|
||||
|
||||
std::size_t Scene_Base::task_executor_worker_count() noexcept {
|
||||
return Execution_Context::executor().num_workers();
|
||||
}
|
||||
|
||||
std::pmr::memory_resource& Scene_Base::memory_resource() const noexcept {
|
||||
return memory_domain_->resource();
|
||||
}
|
||||
@@ -824,128 +881,75 @@ std::uint64_t Scene_Base::Impl::now_ns() const noexcept {
|
||||
}
|
||||
|
||||
std::unique_lock<std::recursive_mutex> Scene_Base::lock_render_idle() {
|
||||
if (is_render_worker_thread())
|
||||
throw std::logic_error(
|
||||
"render-idle operation is not allowed during render execution");
|
||||
if (is_render_execution_context())
|
||||
throw std::logic_error("render-idle operation is not allowed during render execution");
|
||||
if (active_submitted_observer_scene_ == this)
|
||||
throw std::logic_error(
|
||||
"render-idle operation is not allowed during render submission observation");
|
||||
throw std::logic_error("render-idle operation is not allowed during render submission observation");
|
||||
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
|
||||
render_completed_.wait(lock, [this] {
|
||||
return !task_pending_ && !rendering_ && renderable_edit_queue_.empty() && !editing_renderables_;
|
||||
});
|
||||
render_completed_.wait(lock, [this] { return pending_operations_ == 0; });
|
||||
return lock;
|
||||
}
|
||||
|
||||
bool Scene_Base::is_render_worker_thread() const noexcept {
|
||||
return active_execution_scene_ == this ||
|
||||
(worker_.joinable() &&
|
||||
std::this_thread::get_id() == worker_.get_id());
|
||||
bool Scene_Base::is_render_execution_context() const noexcept {
|
||||
return active_execution_scene_ == this;
|
||||
}
|
||||
|
||||
bool Scene_Base::consume_model_dirty() noexcept {
|
||||
return model_dirty_.exchange(false, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
void Scene_Base::shutdown() noexcept {
|
||||
{
|
||||
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
|
||||
render_completed_.wait(lock, [this] {
|
||||
return !task_pending_ && !rendering_ && renderable_edit_queue_.empty() && !editing_renderables_;
|
||||
});
|
||||
stop_ = true;
|
||||
if (shutting_down_)
|
||||
return;
|
||||
shutting_down_ = true;
|
||||
render_completed_.wait(lock, [this] { return pending_operations_ == 0; });
|
||||
}
|
||||
task_ready_.notify_one();
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
execution_context_->wait();
|
||||
{
|
||||
std::lock_guard lock(model_mutex_);
|
||||
for (const auto& [id, renderable] : renderables_) {
|
||||
static_cast<void>(id);
|
||||
renderable->d_func().real_time_data_state->attached.store(
|
||||
false, std::memory_order_release);
|
||||
renderable->d_func().real_time_data_state->attached.store(false, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
scene_lifetime_->invalidate();
|
||||
}
|
||||
|
||||
void Scene_Base::render_loop() {
|
||||
for (;;) {
|
||||
Render_Task task(memory_resource());
|
||||
std::vector<Renderable_Edit_Command> edits;
|
||||
{
|
||||
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
|
||||
task_ready_.wait(lock, [this] {
|
||||
return stop_ || task_pending_ || !renderable_edit_queue_.empty();
|
||||
});
|
||||
if (stop_)
|
||||
return;
|
||||
if (task_pending_) {
|
||||
task = std::move(task_);
|
||||
task_pending_ = false;
|
||||
rendering_ = true;
|
||||
} else {
|
||||
editing_renderables_ = true;
|
||||
edits.reserve(renderable_edit_queue_.size());
|
||||
while (!renderable_edit_queue_.empty()) {
|
||||
edits.push_back(std::move(renderable_edit_queue_.front()));
|
||||
renderable_edit_queue_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!edits.empty()) {
|
||||
{
|
||||
std::lock_guard lock(model_mutex_);
|
||||
Scene_Base* previous = std::exchange(active_renderable_edit_scene_, this);
|
||||
for (auto& command : edits) {
|
||||
try {
|
||||
command.execute();
|
||||
cleanup_detached_topology_locked();
|
||||
validate_structure_locked();
|
||||
} catch (...) {
|
||||
active_renderable_edit_scene_ = previous;
|
||||
structure_fail_fast("runtime renderable edit", std::current_exception());
|
||||
}
|
||||
}
|
||||
active_renderable_edit_scene_ = previous;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
|
||||
editing_renderables_ = false;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
continue;
|
||||
}
|
||||
const auto& snapshot = *task.snapshot;
|
||||
d_func().dispatch({Observation_Event::render_started, d_func().now_ns(),
|
||||
snapshot.render_sequence,
|
||||
snapshot.scene_state_revision_,
|
||||
snapshot.renderables.size(), task.topology,
|
||||
task.snapshot, task.plan});
|
||||
std::exception_ptr exception;
|
||||
try {
|
||||
execute_render_graph(task);
|
||||
d_func().dispatch({Observation_Event::render_completed, d_func().now_ns(),
|
||||
snapshot.render_sequence,
|
||||
snapshot.scene_state_revision_,
|
||||
snapshot.renderables.size(), task.topology,
|
||||
task.snapshot, task.plan});
|
||||
} catch (...) {
|
||||
exception = std::current_exception();
|
||||
d_func().dispatch({Observation_Event::render_failed, d_func().now_ns(),
|
||||
snapshot.render_sequence,
|
||||
snapshot.scene_state_revision_,
|
||||
snapshot.renderables.size(), task.topology,
|
||||
task.snapshot, task.plan});
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(task_mutex_);
|
||||
task.completion->exception = exception;
|
||||
task.completion->completed = true;
|
||||
rendering_ = false;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
void Scene_Base::execute_renderable_edit(std::function<void()> edit) {
|
||||
std::lock_guard lock(model_mutex_);
|
||||
Scene_Base* previous = std::exchange(active_renderable_edit_scene_, this);
|
||||
try {
|
||||
edit();
|
||||
cleanup_detached_topology_locked();
|
||||
validate_structure_locked();
|
||||
} catch (...) {
|
||||
active_renderable_edit_scene_ = previous;
|
||||
structure_fail_fast("runtime renderable edit", std::current_exception());
|
||||
}
|
||||
active_renderable_edit_scene_ = previous;
|
||||
}
|
||||
void Scene_Base::execute_render_task(std::shared_ptr<Render_Task> task) {
|
||||
Render_Execution_Scope scope(*this);
|
||||
const auto& snapshot = *task->snapshot;
|
||||
d_func().dispatch({Observation_Event::render_started, d_func().now_ns(), snapshot.render_sequence,
|
||||
snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology,
|
||||
task->snapshot, task->plan});
|
||||
std::exception_ptr exception;
|
||||
try {
|
||||
execute_render_graph(*task);
|
||||
d_func().dispatch({Observation_Event::render_completed, d_func().now_ns(), snapshot.render_sequence,
|
||||
snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology,
|
||||
task->snapshot, task->plan});
|
||||
} catch (...) {
|
||||
exception = std::current_exception();
|
||||
d_func().dispatch({Observation_Event::render_failed, d_func().now_ns(), snapshot.render_sequence,
|
||||
snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology,
|
||||
task->snapshot, task->plan});
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(task_mutex_);
|
||||
task->completion->exception = exception;
|
||||
task->completion->completed = true;
|
||||
}
|
||||
render_completed_.notify_all();
|
||||
}
|
||||
|
||||
std::shared_ptr<const Render_Plan> Scene_Base::compile_render_plan(
|
||||
@@ -1159,7 +1163,6 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
|
||||
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, &execution_slots](
|
||||
@@ -1212,15 +1215,6 @@ void Scene_Base::execute_render_graph(Render_Task& task) {
|
||||
Scene_Render_Context{snapshot, metrics, diagnostics});
|
||||
}
|
||||
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();
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
#include <condition_variable>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
@@ -174,7 +172,6 @@ public:
|
||||
[[nodiscard]] std::vector<Plan_Version_Statistics> plan_version_statistics(
|
||||
Capture_Session_Id session_id) const;
|
||||
|
||||
[[nodiscard]] static std::size_t task_executor_worker_count() noexcept;
|
||||
[[nodiscard]] std::pmr::memory_resource& memory_resource() const noexcept;
|
||||
[[nodiscard]] std::pmr::memory_resource& upstream_memory_resource() const noexcept;
|
||||
Frame_Control_Strategy_Base& frame_control_strategy();
|
||||
@@ -204,7 +201,7 @@ protected:
|
||||
virtual std::uint64_t acquire_scene_state();
|
||||
virtual void capture_scene_state(Frame_Render_Snapshot& snapshot) const;
|
||||
std::unique_lock<std::recursive_mutex> lock_render_idle();
|
||||
[[nodiscard]] bool is_render_worker_thread() const noexcept;
|
||||
[[nodiscard]] bool is_render_execution_context() const noexcept;
|
||||
bool consume_model_dirty() noexcept;
|
||||
void invalidate_renderables();
|
||||
void shutdown() noexcept;
|
||||
@@ -244,10 +241,6 @@ private:
|
||||
std::shared_ptr<Render_Completion> completion;
|
||||
};
|
||||
|
||||
struct Renderable_Edit_Command {
|
||||
std::function<void()> execute;
|
||||
};
|
||||
|
||||
class Execution_Context;
|
||||
class Render_Execution_Scope;
|
||||
|
||||
@@ -258,7 +251,7 @@ private:
|
||||
};
|
||||
|
||||
void submit_render(Abstract_Frame* frame);
|
||||
void enqueue_renderable_edit_locked(std::function<void()> edit);
|
||||
void submit_operation(std::function<void()> operation);
|
||||
void request_render_graph_rebuild(Renderable_Base& renderable);
|
||||
[[nodiscard]] std::shared_ptr<Frame_Render_Snapshot> snapshot_live_model();
|
||||
[[nodiscard]] std::shared_ptr<Frame_Render_Snapshot>
|
||||
@@ -266,7 +259,8 @@ private:
|
||||
void publish_frame_state_locked();
|
||||
std::shared_ptr<const Render_Plan> compile_render_plan(
|
||||
const Frame_Render_Snapshot& snapshot, Render_Task& task);
|
||||
void render_loop();
|
||||
void execute_render_task(std::shared_ptr<Render_Task> task);
|
||||
void execute_renderable_edit(std::function<void()> edit);
|
||||
void execute_render_graph(Render_Task& task);
|
||||
void validate_renderable_scene(const Renderable_Base& renderable) const;
|
||||
[[nodiscard]] bool is_renderable_attached_locked(
|
||||
@@ -286,6 +280,7 @@ private:
|
||||
std::shared_ptr<Scene_Lifetime> scene_lifetime_;
|
||||
std::shared_ptr<Scene_Memory_Domain> memory_domain_;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
std::unique_ptr<Execution_Context> execution_context_;
|
||||
std::atomic_bool model_dirty_{true};
|
||||
std::pmr::unordered_map<Renderable_Id, Renderable> renderables_;
|
||||
renderive::scene::dependency::Dependency_Resolver<Renderable_Id>
|
||||
@@ -294,21 +289,15 @@ private:
|
||||
mutable std::mutex model_mutex_;
|
||||
|
||||
mutable std::recursive_mutex task_mutex_;
|
||||
std::condition_variable_any task_ready_;
|
||||
std::condition_variable_any render_completed_;
|
||||
std::thread worker_;
|
||||
Render_Task task_;
|
||||
std::pmr::deque<Renderable_Edit_Command> renderable_edit_queue_;
|
||||
std::shared_ptr<Render_Completion> current_completion_;
|
||||
std::exception_ptr pending_exception_;
|
||||
bool pending_exception_observed_{};
|
||||
std::size_t deferred_render_count_{};
|
||||
std::uint64_t render_sequence_{};
|
||||
bool task_pending_{};
|
||||
bool rendering_{};
|
||||
bool editing_renderables_{};
|
||||
std::size_t pending_operations_{};
|
||||
bool runtime_started_{};
|
||||
bool stop_{};
|
||||
bool shutting_down_{};
|
||||
|
||||
const Render_Node_Id composite_begin_node_id_;
|
||||
const Render_Node_Id scene_render_node_id_;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
namespace renderive::scheduling {
|
||||
struct Scheduler_Statistics {
|
||||
std::size_t concurrency{};
|
||||
std::size_t active_workers{};
|
||||
std::size_t peak_workers{};
|
||||
std::size_t active_external_threads{};
|
||||
std::size_t peak_external_threads{};
|
||||
std::uint64_t worker_entry_count{};
|
||||
std::uint64_t worker_exit_count{};
|
||||
std::uint64_t external_entry_count{};
|
||||
std::uint64_t external_exit_count{};
|
||||
};
|
||||
[[nodiscard]] std::size_t scheduler_concurrency() noexcept;
|
||||
[[nodiscard]] Scheduler_Statistics scheduler_statistics() noexcept;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "OneTBB_Runtime.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <oneapi/tbb/task_scheduler_observer.h>
|
||||
namespace renderive::scheduling::detail {
|
||||
struct OneTBB_Runtime::Counters {
|
||||
std::atomic_size_t active_workers{};
|
||||
std::atomic_size_t peak_workers{};
|
||||
std::atomic_size_t active_external_threads{};
|
||||
std::atomic_size_t peak_external_threads{};
|
||||
std::atomic_uint64_t worker_entry_count{};
|
||||
std::atomic_uint64_t worker_exit_count{};
|
||||
std::atomic_uint64_t external_entry_count{};
|
||||
std::atomic_uint64_t external_exit_count{};
|
||||
};
|
||||
namespace {
|
||||
void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept {
|
||||
std::size_t current = peak.load(std::memory_order_relaxed);
|
||||
while (current < value) {
|
||||
if (peak.compare_exchange_weak(current, value, std::memory_order_relaxed))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
class OneTBB_Runtime::Observer final : public oneapi::tbb::task_scheduler_observer {
|
||||
public:
|
||||
Observer(oneapi::tbb::task_arena& arena, Counters& counters)
|
||||
: oneapi::tbb::task_scheduler_observer(arena), counters_(counters) {
|
||||
observe(true);
|
||||
}
|
||||
~Observer() override {
|
||||
observe(false);
|
||||
}
|
||||
void on_scheduler_entry(bool is_worker) override {
|
||||
if (is_worker) {
|
||||
counters_.worker_entry_count.fetch_add(1, std::memory_order_relaxed);
|
||||
const std::size_t active = counters_.active_workers.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
update_peak(counters_.peak_workers, active);
|
||||
return;
|
||||
}
|
||||
counters_.external_entry_count.fetch_add(1, std::memory_order_relaxed);
|
||||
const std::size_t active = counters_.active_external_threads.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
update_peak(counters_.peak_external_threads, active);
|
||||
}
|
||||
void on_scheduler_exit(bool is_worker) override {
|
||||
if (is_worker) {
|
||||
counters_.worker_exit_count.fetch_add(1, std::memory_order_relaxed);
|
||||
counters_.active_workers.fetch_sub(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
counters_.external_exit_count.fetch_add(1, std::memory_order_relaxed);
|
||||
counters_.active_external_threads.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
private:
|
||||
Counters& counters_;
|
||||
};
|
||||
OneTBB_Runtime& OneTBB_Runtime::instance() {
|
||||
static OneTBB_Runtime runtime;
|
||||
return runtime;
|
||||
}
|
||||
OneTBB_Runtime::OneTBB_Runtime()
|
||||
: counters_(std::make_unique<Counters>()), arena_(oneapi::tbb::task_arena::automatic) {
|
||||
arena_.initialize();
|
||||
observer_ = std::make_unique<Observer>(arena_, *counters_);
|
||||
}
|
||||
OneTBB_Runtime::~OneTBB_Runtime() = default;
|
||||
oneapi::tbb::task_arena& OneTBB_Runtime::arena() noexcept {
|
||||
return arena_;
|
||||
}
|
||||
std::uint32_t OneTBB_Runtime::current_worker_id() const noexcept {
|
||||
const int index = oneapi::tbb::this_task_arena::current_thread_index();
|
||||
if (index < 0)
|
||||
return std::numeric_limits<std::uint32_t>::max();
|
||||
return static_cast<std::uint32_t>(index);
|
||||
}
|
||||
Scheduler_Statistics OneTBB_Runtime::statistics() const noexcept {
|
||||
return {
|
||||
static_cast<std::size_t>(arena_.max_concurrency()),
|
||||
counters_->active_workers.load(std::memory_order_relaxed),
|
||||
counters_->peak_workers.load(std::memory_order_relaxed),
|
||||
counters_->active_external_threads.load(std::memory_order_relaxed),
|
||||
counters_->peak_external_threads.load(std::memory_order_relaxed),
|
||||
counters_->worker_entry_count.load(std::memory_order_relaxed),
|
||||
counters_->worker_exit_count.load(std::memory_order_relaxed),
|
||||
counters_->external_entry_count.load(std::memory_order_relaxed),
|
||||
counters_->external_exit_count.load(std::memory_order_relaxed)
|
||||
};
|
||||
}
|
||||
}
|
||||
namespace renderive::scheduling {
|
||||
std::size_t scheduler_concurrency() noexcept {
|
||||
return detail::OneTBB_Runtime::instance().statistics().concurrency;
|
||||
}
|
||||
Scheduler_Statistics scheduler_statistics() noexcept {
|
||||
return detail::OneTBB_Runtime::instance().statistics();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <oneapi/tbb/task_arena.h>
|
||||
#include <utility>
|
||||
#include "renderive/scheduling/Scheduler.hpp"
|
||||
namespace renderive::scheduling::detail {
|
||||
class OneTBB_Runtime final {
|
||||
public:
|
||||
static OneTBB_Runtime& instance();
|
||||
oneapi::tbb::task_arena& arena() noexcept;
|
||||
template <class F>
|
||||
void enqueue(F&& task) {
|
||||
arena_.enqueue(std::forward<F>(task));
|
||||
}
|
||||
[[nodiscard]] std::uint32_t current_worker_id() const noexcept;
|
||||
[[nodiscard]] Scheduler_Statistics statistics() const noexcept;
|
||||
private:
|
||||
class Observer;
|
||||
OneTBB_Runtime();
|
||||
~OneTBB_Runtime();
|
||||
OneTBB_Runtime(const OneTBB_Runtime&) = delete;
|
||||
OneTBB_Runtime& operator=(const OneTBB_Runtime&) = delete;
|
||||
struct Counters;
|
||||
std::unique_ptr<Counters> counters_;
|
||||
oneapi::tbb::task_arena arena_;
|
||||
std::unique_ptr<Observer> observer_;
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
@@ -35,7 +36,7 @@ TEST(flow_refresh_strategy_test, returns_empty_lease_when_queue_is_empty) {
|
||||
EXPECT_EQ(strategy.state().empty_acquire_count, 1);
|
||||
}
|
||||
TEST(flow_refresh_strategy_test, accepts_multiple_concurrent_producers_without_losing_frames) {
|
||||
Flow_Refresh_Test_Strategy strategy;
|
||||
Flow_Refresh_Test_Strategy strategy(Observer_State<>{}, 512);
|
||||
constexpr int producer_count = 4;
|
||||
constexpr int frames_per_producer = 100;
|
||||
std::vector<std::thread> producers;
|
||||
@@ -63,6 +64,35 @@ TEST(flow_refresh_strategy_test, accepts_multiple_concurrent_producers_without_l
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(flow_refresh_strategy_test, bounded_queue_applies_backpressure_until_consumer_releases_capacity) {
|
||||
Flow_Refresh_Test_Strategy strategy(Observer_State<>{}, 1);
|
||||
{
|
||||
auto frame = strategy.acquire_painter();
|
||||
frame->value = 1;
|
||||
}
|
||||
std::atomic<bool> second_enqueued{};
|
||||
std::thread producer([&] {
|
||||
{
|
||||
auto frame = strategy.acquire_painter();
|
||||
frame->value = 2;
|
||||
}
|
||||
second_enqueued.store(true, std::memory_order_release);
|
||||
second_enqueued.notify_all();
|
||||
});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
EXPECT_FALSE(second_enqueued.load(std::memory_order_acquire));
|
||||
{
|
||||
auto frame = strategy.acquire_renderer();
|
||||
ASSERT_TRUE(frame);
|
||||
EXPECT_EQ(frame->value, 1);
|
||||
}
|
||||
second_enqueued.wait(false, std::memory_order_acquire);
|
||||
producer.join();
|
||||
auto frame = strategy.acquire_renderer();
|
||||
ASSERT_TRUE(frame);
|
||||
EXPECT_EQ(frame->value, 2);
|
||||
}
|
||||
TEST(flow_refresh_strategy_test, concurrent_consumer_does_not_underflow_pending_count) {
|
||||
Flow_Refresh_Test_Strategy strategy;
|
||||
constexpr int producer_count = 4;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -16,58 +15,10 @@
|
||||
#include "renderive/render_graph/External_Operation.hpp"
|
||||
#include "renderive/render_graph/Render_Plan.hpp"
|
||||
#include "renderive/render_graph/detail/Render_Graph_Runtime.hpp"
|
||||
#include "renderive/scheduling/detail/OneTBB_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(
|
||||
@@ -124,7 +75,6 @@ TEST(external_operation_test,
|
||||
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);
|
||||
@@ -141,17 +91,11 @@ TEST(render_graph_runtime_test,
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -166,7 +110,6 @@ TEST(render_graph_runtime_test,
|
||||
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{};
|
||||
@@ -182,11 +125,7 @@ TEST(render_graph_runtime_test,
|
||||
}
|
||||
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 {
|
||||
@@ -211,7 +150,6 @@ TEST(render_graph_runtime_test,
|
||||
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;
|
||||
@@ -224,15 +162,11 @@ TEST(render_graph_runtime_test,
|
||||
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([&] {
|
||||
renderive::scheduling::detail::OneTBB_Runtime::instance().enqueue([&] {
|
||||
{
|
||||
std::lock_guard lock(probe_mutex);
|
||||
probe_ran = true;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include "renderive/scheduling/Scheduler.hpp"
|
||||
#include "renderive/scheduling/detail/OneTBB_Runtime.hpp"
|
||||
TEST(scheduler_test, reports_oneTBB_runtime_activity) {
|
||||
const auto before = renderive::scheduling::scheduler_statistics();
|
||||
EXPECT_GT(before.concurrency, 0U);
|
||||
std::mutex mutex;
|
||||
std::condition_variable completed;
|
||||
bool done{};
|
||||
renderive::scheduling::detail::OneTBB_Runtime::instance().enqueue([&] {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
done = true;
|
||||
}
|
||||
completed.notify_one();
|
||||
});
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
completed.wait(lock, [&] { return done; });
|
||||
}
|
||||
const auto after = renderive::scheduling::scheduler_statistics();
|
||||
EXPECT_EQ(after.concurrency, before.concurrency);
|
||||
EXPECT_GE(after.worker_entry_count, before.worker_entry_count);
|
||||
EXPECT_GE(after.peak_workers, 1U);
|
||||
}
|
||||
@@ -55,6 +55,7 @@ if (RENDERIVE_BUILD_TESTS)
|
||||
set(Renderive_Qt_test_target "Renderive_Qt_${Renderive_Qt_test_name}_${Renderive_Qt_test_hash}")
|
||||
add_executable("${Renderive_Qt_test_target}" "${Renderive_Qt_test_source}")
|
||||
target_link_libraries("${Renderive_Qt_test_target}" PRIVATE Renderive_Qt GTest::gtest)
|
||||
renderive_stage_kernel_runtime("${Renderive_Qt_test_target}")
|
||||
add_test(NAME "${Renderive_Qt_test_target}"
|
||||
COMMAND "${CMAKE_COMMAND}" -E env
|
||||
"PATH=${Renderive_Qt_runtime_root}/bin\;$ENV{PATH}"
|
||||
|
||||
@@ -46,6 +46,7 @@ if (RENDERIVE_BUILD_TESTS)
|
||||
set(Renderive_render_2D_test_target "Renderive_render_2D_${Renderive_render_2D_test_name}_${Renderive_render_2D_test_hash}")
|
||||
add_executable("${Renderive_render_2D_test_target}" "${Renderive_render_2D_test_source}")
|
||||
target_link_libraries("${Renderive_render_2D_test_target}" PRIVATE Renderive_render_2D GTest::gtest_main)
|
||||
renderive_stage_kernel_runtime("${Renderive_render_2D_test_target}")
|
||||
add_test(NAME "${Renderive_render_2D_test_target}" COMMAND "${Renderive_render_2D_test_target}")
|
||||
set_tests_properties("${Renderive_render_2D_test_target}" PROPERTIES LABELS "Renderive_render_2D")
|
||||
list(APPEND Renderive_render_2D_test_targets "${Renderive_render_2D_test_target}")
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
#include "renderive/scheduling/Scheduler.hpp"
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
using Afterglow_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
|
||||
@@ -102,7 +103,7 @@ void Afterglow::Impl::build_prepare_graph(Renderable_Graph_Builder& builder) {
|
||||
: 0;
|
||||
const int partition_count = partitioner.graph_partition_count(
|
||||
state.partition_mode, state.partition_count.get(),
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 4096);
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()), work_size, 4096);
|
||||
const auto prepare = add_prepare_task(
|
||||
builder, "prepare", "Prepare Afterglow",
|
||||
[this, partition_count](const Prepare_Render_Context& context) {
|
||||
@@ -171,7 +172,7 @@ void Afterglow::Impl::build_paint_graph(Renderable_Graph_Builder& builder) {
|
||||
: 0;
|
||||
const int partition_count = partitioner.graph_partition_count(
|
||||
state.partition_mode, state.partition_count.get(),
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 4096);
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()), work_size, 4096);
|
||||
const auto paint_image = add_paint_task(
|
||||
builder, "paint", "Paint Afterglow",
|
||||
[this](Painter& painter, const Paint_Render_Context& context) {
|
||||
@@ -284,7 +285,7 @@ void Afterglow::Impl::render_frame_completed(
|
||||
const auto& state = output.properties;
|
||||
if (partitioner.finish(
|
||||
state.partition_mode, output.active_partitions, target_interval_ns,
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()),
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()),
|
||||
output.work_size, 4096))
|
||||
render_graph_changed();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "renderive/scheduling/Scheduler.hpp"
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
struct Spectrum_Interaction {
|
||||
@@ -353,7 +354,7 @@ void Spectrum::Impl::build_prepare_graph(
|
||||
: 0;
|
||||
const int partition_count = partitioner.graph_partition_count(
|
||||
state.partition_mode, state.partition_count.get(),
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 128);
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()), work_size, 128);
|
||||
const auto prepare = add_prepare_task(
|
||||
builder, "prepare", "Prepare Spectrum",
|
||||
[this, partition_count](const Prepare_Render_Context& context) {
|
||||
@@ -394,7 +395,7 @@ void Spectrum::Impl::build_paint_graph(
|
||||
: 0;
|
||||
const int partition_count = partitioner.graph_partition_count(
|
||||
state.partition_mode, state.partition_count.get(),
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 128);
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()), work_size, 128);
|
||||
const auto paint = add_paint_task(
|
||||
builder, "paint", "Paint Spectrum",
|
||||
[this, partition_count](Painter& painter,
|
||||
@@ -599,7 +600,7 @@ void Spectrum::Impl::render_frame_completed(
|
||||
const auto& state = output.properties;
|
||||
if (partitioner.finish(
|
||||
state.partition_mode, output.active_partitions, target_interval_ns,
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()),
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()),
|
||||
output.work_size, 128))
|
||||
render_graph_changed();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include "renderive/scheduling/Scheduler.hpp"
|
||||
namespace renderive::detail {
|
||||
namespace {
|
||||
struct Waterfall_Row {
|
||||
@@ -164,7 +165,7 @@ void Waterfall::Impl::build_prepare_graph(Renderable_Graph_Builder& builder) {
|
||||
state, published_rows, frequency_axis->transform(view));
|
||||
const int partition_count = partitioner.graph_partition_count(
|
||||
state.partition_mode, state.partition_count.get(),
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 4096);
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()), work_size, 4096);
|
||||
const auto prepare = add_prepare_task(
|
||||
builder, "prepare", "Prepare Waterfall",
|
||||
[this, partition_count](const Prepare_Render_Context& context) {
|
||||
@@ -214,7 +215,7 @@ void Waterfall::Impl::build_paint_graph(Renderable_Graph_Builder& builder) {
|
||||
state, published_rows, frequency_axis->transform(view));
|
||||
const int count = partitioner.graph_partition_count(
|
||||
state.partition_mode, state.partition_count.get(),
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()), work_size, 4096);
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()), work_size, 4096);
|
||||
if (count == 0) {
|
||||
builder.precede(builder.find("prepare"), paint_image);
|
||||
}
|
||||
@@ -327,7 +328,7 @@ void Waterfall::Impl::render_frame_completed(
|
||||
const auto& state = output.properties;
|
||||
if (partitioner.finish(
|
||||
state.partition_mode, output.active_partitions, target_interval_ns,
|
||||
static_cast<int>(Scene_Base::task_executor_worker_count()),
|
||||
static_cast<int>(renderive::scheduling::scheduler_concurrency()),
|
||||
output.work_size, 4096))
|
||||
render_graph_changed();
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public:
|
||||
Frame_Control frame_control;
|
||||
template <class Function>
|
||||
void with_final_color_cache(Function&& function) {
|
||||
if (this->is_render_worker_thread()) {
|
||||
if (this->is_render_execution_context()) {
|
||||
function(static_cast<const Cache&>(final_color_cache_));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ function(renderive_stage_render_3D_runtime target)
|
||||
if (NOT TARGET "${target}")
|
||||
message(FATAL_ERROR "Cannot stage render_3D runtime for missing target: ${target}")
|
||||
endif ()
|
||||
renderive_stage_kernel_runtime("${target}")
|
||||
if (WIN32)
|
||||
get_property(Renderive_render_3D_runtime_files GLOBAL PROPERTY
|
||||
RENDERIVE_RENDER_3D_RUNTIME_FILES)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 线程与数据边界
|
||||
|
||||
Renderive 负责状态发布、实时数据交换、Taskflow CPU 数据准备、事件入口、帧调度和像素帧发布。Datoviz 的 scene/visual mutation、FramePlan emission、DRP2 execution、GPU submission 与 readback 全部归单一 `Render_Domain` 所有。
|
||||
Renderive 负责状态发布、实时数据交换、oneTBB CPU 数据准备、事件入口、帧调度和像素帧发布。Datoviz 的 scene/visual mutation、FramePlan emission、DRP2 execution、GPU submission 与 readback 全部归单一 `Render_Domain` 所有。
|
||||
|
||||
当前帧数据流:
|
||||
|
||||
@@ -10,7 +10,7 @@ Renderive 负责状态发布、实时数据交换、Taskflow CPU 数据准备、
|
||||
Scene / Renderable state ----+
|
||||
Point bulk data -------------+-- frame publication snapshot
|
||||
|
|
||||
+-- Taskflow: per-renderable CPU prepare
|
||||
+-- oneTBB Flow Graph: per-renderable CPU prepare
|
||||
| |
|
||||
Kernel Event -----------------+ +-- immutable Prepared_Point
|
||||
|
|
||||
@@ -22,14 +22,14 @@ Kernel Event -----------------+ +-- immutable Prepared_Point
|
||||
immutable Pixel_Frame
|
||||
```
|
||||
|
||||
Taskflow worker 不调用 `dvz_*`。Datoviz FramePlan 继续负责 GPU upload/render/copy/readback 依赖,Renderive RenderPlan 只负责应用级 CPU prepare 依赖和末端单一 render node。
|
||||
oneTBB worker 不调用 `dvz_*`。Datoviz FramePlan 继续负责 GPU upload/render/copy/readback 依赖,Renderive RenderPlan 只负责应用级 CPU prepare 依赖和末端单一 render node。
|
||||
|
||||
## 帧快照
|
||||
|
||||
- `Scene_Base::publish_frame_state(Abstract_Frame&)` 在 frame-control 的发布边界交换 Scene、Renderable 与实时数据。
|
||||
- `Frame_Render_Snapshot` 保存对应帧的 Scene 状态、Renderable 元数据和类型安全的大数据共享快照。
|
||||
- Manual、Low Latency、Playback 保存同一类快照;Playback 出队不会读取后来版本的数据。
|
||||
- Taskflow prepare 只从帧快照读取输入并发布不可变 prepared output。
|
||||
- oneTBB prepare 只从帧快照读取输入并发布不可变 prepared output。
|
||||
- Scene render node 等待所有末端 prepare 依赖,然后在 `Render_Domain` 串行消费 prepared output。
|
||||
|
||||
## Point 封装
|
||||
@@ -37,7 +37,7 @@ Taskflow worker 不调用 `dvz_*`。Datoviz FramePlan 继续负责 GPU upload/re
|
||||
- `Point_Visual` 的小状态由 Renderable 状态策略管理。
|
||||
- 点集的唯一权威来源是 `Multi_Double_Buffer_Strategy`;帧只持有已发布版本的不可变共享快照。
|
||||
- positions、colors、sizes、sigma、angles、radii、normals 等派生数组只存在于 `Prepared_Point`,不保存为 Visual 成员。
|
||||
- `Point_Scene` 复用 Kernel `Scene3D_Context`、frame-control、RenderPlan、Taskflow、capture 和 observer。
|
||||
- `Point_Scene` 复用 Kernel `Scene3D_Context`、frame-control、RenderPlan、oneTBB Flow Graph、capture 和 observer。
|
||||
- Pointer、wheel、key 直接保留 Kernel Event 语义并投递到 `Render_Domain`,没有第二套输入命令或收集队列。
|
||||
- 后端只应用 prepared data,并负责 Datoviz/Vulkan 资源和不可变 `Pixel_Frame` 输出。
|
||||
|
||||
@@ -49,7 +49,7 @@ Taskflow worker 不调用 `dvz_*`。Datoviz FramePlan 继续负责 GPU upload/re
|
||||
| Point 大批量数据 | `Point_Visual` 双缓冲策略 | 不可变共享快照 |
|
||||
| viewport、clear color、visual family | `Scene3D_Context` 状态策略 | 发布帧快照 |
|
||||
| 输入 | Kernel Event | `Render_Domain` 直接处理 |
|
||||
| CPU 派生数组 | 当前帧 Taskflow prepare | `Prepared_Point` |
|
||||
| CPU 派生数组 | 当前帧 oneTBB prepare | `Prepared_Point` |
|
||||
| Datoviz/Vulkan 资源 | `Datoviz_Visual_Backend` | 仅 `Render_Domain` |
|
||||
| 输出像素 | 最近完成的 render node | `Point_Scene::latest_frame()` |
|
||||
|
||||
|
||||
+21
-9
@@ -1,6 +1,9 @@
|
||||
param(
|
||||
[string]$DatovizBuildDirectory,
|
||||
[string]$CTestExecutable = 'ctest'
|
||||
[string]$CTestExecutable = 'ctest',
|
||||
[ValidateSet('Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel')]
|
||||
[string]$BuildType = 'Debug',
|
||||
[string]$Toolchain = 'vs2022'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -8,10 +11,14 @@ $ErrorActionPreference = 'Stop'
|
||||
$render3DRoot = $PSScriptRoot
|
||||
$workspaceRoot = Split-Path -Parent $render3DRoot
|
||||
$aeRoot = Split-Path -Parent (Split-Path -Parent $workspaceRoot)
|
||||
|
||||
$cacheVariant = "${Toolchain}_${BuildType}"
|
||||
$pthreadsRuntimeDirectory = Join-Path $aeRoot "cached_external_library\${cacheVariant}_render_3D\install\PThreads4W\x86_64\${BuildType}\bin"
|
||||
$oneTBBRuntimeDirectory = Join-Path $aeRoot "cached_external_library\${cacheVariant}_render_kernel\install\oneTBB\bin"
|
||||
$windowsSdkRuntimeDirectory = 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64'
|
||||
$runtimeDirectories = @(
|
||||
(Join-Path $aeRoot 'cached_external_library\vs2022_Debug_render_3D\install\PThreads4W\x86_64\Debug\bin')
|
||||
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64'
|
||||
$pthreadsRuntimeDirectory
|
||||
$oneTBBRuntimeDirectory
|
||||
$windowsSdkRuntimeDirectory
|
||||
)
|
||||
|
||||
$vulkanSdkRoot = if ($env:VULKAN_SDK) {
|
||||
@@ -20,9 +27,13 @@ $vulkanSdkRoot = if ($env:VULKAN_SDK) {
|
||||
'C:\VulkanSDK\1.4.357.0'
|
||||
}
|
||||
$runtimeDirectories += Join-Path $vulkanSdkRoot 'Bin'
|
||||
$pthreadRuntimeName = if ($BuildType -eq 'Debug') { 'pthreadVC3d.dll' } else { 'pthreadVC3.dll' }
|
||||
$oneTBBRuntimeName = if ($BuildType -eq 'Debug') { 'tbb12_debug.dll' } else { 'tbb12.dll' }
|
||||
$shadercRuntimeLibrary = Join-Path $vulkanSdkRoot 'Bin\shaderc_shared.dll'
|
||||
$runtimeLibraries = @(
|
||||
(Join-Path $runtimeDirectories[0] 'pthreadVC3d.dll')
|
||||
(Join-Path $vulkanSdkRoot 'Bin\shaderc_shared.dll')
|
||||
(Join-Path $pthreadsRuntimeDirectory $pthreadRuntimeName)
|
||||
(Join-Path $oneTBBRuntimeDirectory $oneTBBRuntimeName)
|
||||
$shadercRuntimeLibrary
|
||||
)
|
||||
$linkDirectories = @(
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\lib\x64'
|
||||
@@ -60,7 +71,7 @@ foreach ($linkDirectory in $linkDirectories) {
|
||||
$env:LIB = $libEntries -join ';'
|
||||
|
||||
$env:VULKAN_SDK = $vulkanSdkRoot
|
||||
$env:DVZ_SHADERC_RUNTIME_LIBRARY = $runtimeLibraries[1]
|
||||
$env:DVZ_SHADERC_RUNTIME_LIBRARY = $shadercRuntimeLibrary
|
||||
$env:DVZ_WHEEL_RUNTIME_DIRS = Join-Path $vulkanSdkRoot 'Bin'
|
||||
|
||||
# Datoviz's upstream tests use POSIX-style absolute /tmp paths. On Windows those paths resolve
|
||||
@@ -72,8 +83,9 @@ $env:TEMP = $windowsTempDirectory
|
||||
$env:TMP = $windowsTempDirectory
|
||||
|
||||
Write-Host 'Renderive render_3D runtime environment is ready.'
|
||||
Write-Host " PThreads4W: $($runtimeDirectories[0])"
|
||||
Write-Host " Windows SDK: $($runtimeDirectories[1])"
|
||||
Write-Host " PThreads4W: $pthreadsRuntimeDirectory"
|
||||
Write-Host " oneTBB: $oneTBBRuntimeDirectory"
|
||||
Write-Host " Windows SDK: $windowsSdkRuntimeDirectory"
|
||||
Write-Host " Link roots: $($linkDirectories -join '; ')"
|
||||
Write-Host " Vulkan SDK: $vulkanSdkRoot"
|
||||
Write-Host " shaderc: $env:DVZ_SHADERC_RUNTIME_LIBRARY"
|
||||
|
||||
@@ -25,7 +25,7 @@ struct Captured_Point {
|
||||
std::uint64_t data_revision{};
|
||||
};
|
||||
|
||||
// Immutable, frame-local CPU output. Taskflow workers create this data; only
|
||||
// Immutable, frame-local CPU output. oneTBB workers create this data; only
|
||||
// the Datoviz render-domain task consumes it.
|
||||
struct Prepared_Point {
|
||||
Point_State state;
|
||||
|
||||
Reference in New Issue
Block a user