diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 2e5c09d..42a27a8 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -244,8 +244,8 @@ private: data().buffer_storage.advance(); data().dependency_graph_storage.advance(); after_advance(); - // after_advance 只修改已提交的 current;由状态缓冲统一刷新下一次编辑基线,具体机制不得自行同步两份状态。 - *data().state.pending = *data().state.current; + // Commit_Double_Buffer::advance() 已为 pending 建立 current 基线;after_advance + // 只向 pending 写本轮派生状态,禁止回写正在供外部查询的 current。 } public: template Tag> diff --git a/kernel/src/kernel/render_common.cpp b/kernel/src/kernel/render_common.cpp index 9adaa40..704a030 100644 --- a/kernel/src/kernel/render_common.cpp +++ b/kernel/src/kernel/render_common.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace aethera { namespace { @@ -45,11 +46,9 @@ private: std::atomic_uint64_t worker_busy_time_ns{}; std::atomic_uint64_t first_task_time_ns{}; std::atomic_uint64_t last_task_time_ns{}; - mutable std::mutex longest_mutex; - std::uint64_t longest_task_time_ns{}; - std::size_t longest_task_hash{}; - std::string longest_task_name; - tf::TaskType longest_task_type{tf::TaskType::UNDEFINED}; + std::atomic_uint64_t longest_task_time_ns{}; + std::atomic_size_t longest_task_hash{}; + std::atomic longest_task_type{tf::TaskType::UNDEFINED}; static std::uint64_t clock_ns(Clock::time_point value) noexcept { return static_cast(std::chrono::duration_cast(value.time_since_epoch()).count()); } @@ -79,7 +78,6 @@ public: } void on_entry(tf::WorkerView worker, tf::TaskView task) override { auto now = Clock::now(); - detail::observe_stage_entry(task.hash_value(), clock_ns(now)); auto& worker_starts = starts[worker.id()]; if (worker_starts.empty()) { worker_busy_starts[worker.id()] = now; @@ -103,7 +101,6 @@ public: } void on_exit(tf::WorkerView worker, tf::TaskView task) override { auto now = Clock::now(); - detail::observe_stage_exit(task.hash_value(), clock_ns(now)); auto& worker_starts = starts[worker.id()]; auto start = worker_starts.back(); worker_starts.pop_back(); @@ -123,14 +120,11 @@ public: update_min(type.min_time_ns, elapsed); update_max(type.max_time_ns, elapsed); } - { - std::lock_guard guard(longest_mutex); - if (longest_task_time_ns < elapsed) { - longest_task_time_ns = elapsed; - longest_task_hash = task.hash_value(); - longest_task_name = task.name(); - longest_task_type = task.type(); - } + auto longest = longest_task_time_ns.load(std::memory_order_relaxed); + if (longest < elapsed && longest_task_time_ns.compare_exchange_strong( + longest, elapsed, std::memory_order_relaxed)) { + longest_task_hash.store(task.hash_value(), std::memory_order_relaxed); + longest_task_type.store(task.type(), std::memory_order_relaxed); } active_tasks.fetch_sub(1, std::memory_order_relaxed); if (worker_starts.empty()) { @@ -187,11 +181,10 @@ public: target.max_task_time_ns = source.max_task_time_ns.load(std::memory_order_relaxed); target.utilization = state.observed_wall_time_ns ? static_cast(target.busy_time_ns) * 100.0 / static_cast(state.observed_wall_time_ns) : 0.0; } - std::lock_guard guard(longest_mutex); - state.longest_task_time_ns = longest_task_time_ns; - state.longest_task_hash = longest_task_hash; - state.longest_task_name = longest_task_name; - state.longest_task_type = longest_task_type; + state.longest_task_time_ns = longest_task_time_ns.load(std::memory_order_relaxed); + state.longest_task_hash = longest_task_hash.load(std::memory_order_relaxed); + state.longest_task_name.clear(); + state.longest_task_type = longest_task_type.load(std::memory_order_relaxed); } }; class Task_Resource : Pinned { @@ -206,6 +199,7 @@ private: Task_Runtime_State state; double_buffer::detail::State_Callback_Storage state_callbacks; std::recursive_mutex state_mutex; + std::atomic_bool state_callback_enabled{}; void create_executor(std::size_t workers, std::shared_ptr worker_interface) { executor = std::make_unique(workers, std::move(worker_interface)); observer = executor->make_observer(); @@ -218,6 +212,7 @@ private: observer = executor->make_observer(); } void publish_state() { + if (!state_callback_enabled.load(std::memory_order_acquire)) return; std::lock_guard guard(state_mutex); observer->write_state(state, executor->num_workers(), executor->num_topologies()); state.active_taskflow_count = active_taskflows.load(std::memory_order_relaxed); @@ -248,7 +243,10 @@ public: while (peak < active && !peak_active_taskflows.compare_exchange_weak(peak, active, std::memory_order_relaxed)) {} auto start = std::chrono::steady_clock::now(); try { - executor->run(taskflow).get(); + if (executor->this_worker()) + executor->corun(taskflow); + else + executor->run(taskflow).get(); } catch (...) { active_taskflows.fetch_sub(1, std::memory_order_relaxed); @@ -262,22 +260,46 @@ public: publish_state(); return elapsed; } + void run(tf::Taskflow& taskflow, std::function completion) { + if (!completion) throw std::invalid_argument("Taskflow completion is empty"); + ensure_executor(); + auto active = active_taskflows.fetch_add(1, std::memory_order_relaxed) + 1; + auto peak = peak_active_taskflows.load(std::memory_order_relaxed); + while (peak < active && !peak_active_taskflows.compare_exchange_weak( + peak, active, std::memory_order_relaxed)) {} + executor->run(taskflow, [this, completion = std::move(completion)]() mutable { + active_taskflows.fetch_sub(1, std::memory_order_relaxed); + completed_taskflows.fetch_add(1, std::memory_order_relaxed); + publish_state(); + completion(); + }); + } + void schedule(std::function task) { + if (!task) throw std::invalid_argument("Taskflow scheduled task is empty"); + ensure_executor(); + executor->silent_async(std::move(task)); + } std::pmr::memory_resource* memory_resource() const noexcept { return memory; } void set_state_callback(std::function callback) { std::lock_guard guard(state_mutex); state_callbacks.template set(std::move(callback)); + state_callback_enabled.store(true, std::memory_order_release); } void clear_state_callback() { std::lock_guard guard(state_mutex); state_callbacks.template clear(); + state_callback_enabled.store(false, std::memory_order_release); } }; } void initialize_runtime(std::size_t workers, std::shared_ptr worker_interface, Pmr pmr) { Task_Resource::instance().initialize(workers, std::move(worker_interface), pmr); } +void schedule_task(std::function task) { + Task_Resource::instance().schedule(std::move(task)); +} namespace detail { void set_runtime_state_callback_impl(std::function callback) { Task_Resource::instance().set_state_callback(std::move(callback)); @@ -288,6 +310,9 @@ void clear_runtime_state_callback_impl() { std::uint64_t run_taskflow(tf::Taskflow& taskflow) { return Task_Resource::instance().run(taskflow); } +void run_taskflow(tf::Taskflow& taskflow, std::function completion) { + Task_Resource::instance().run(taskflow, std::move(completion)); +} std::pmr::memory_resource* task_memory_resource() noexcept { return Task_Resource::instance().memory_resource(); } diff --git a/kernel/src/kernel/render_common.hpp b/kernel/src/kernel/render_common.hpp index 3a5ac94..f3102a8 100644 --- a/kernel/src/kernel/render_common.hpp +++ b/kernel/src/kernel/render_common.hpp @@ -95,6 +95,8 @@ struct Task_Runtime_State : State_Type { void initialize_runtime(std::size_t workers = std::thread::hardware_concurrency(), std::shared_ptr worker_interface = nullptr, Pmr pmr = {}); +/* 把独立业务任务提交给全局 Taskflow worker;任务不得执行阻塞式设备等待。 */ +void schedule_task(std::function task); /* 为全局 Taskflow 运行时状态注册回调;Tag 目前只接受 Task_Runtime_State_Tag。 */ template Tag, std::invocable Callback> void set_runtime_state_callback(Callback&& callback); diff --git a/kernel/src/kernel/render_common.ipp b/kernel/src/kernel/render_common.ipp index a18cc9d..d0c5f7e 100644 --- a/kernel/src/kernel/render_common.ipp +++ b/kernel/src/kernel/render_common.ipp @@ -3,9 +3,8 @@ namespace aethera::detail { void set_runtime_state_callback_impl(std::function callback); void clear_runtime_state_callback_impl(); std::uint64_t run_taskflow(tf::Taskflow& taskflow); +void run_taskflow(tf::Taskflow& taskflow, std::function completion); std::pmr::memory_resource* task_memory_resource() noexcept; -void observe_stage_entry(std::size_t task_hash, std::uint64_t now); -void observe_stage_exit(std::size_t task_hash, std::uint64_t now); } namespace aethera { template Tag, std::invocable Callback> diff --git a/kernel/src/kernel/renderable.cpp b/kernel/src/kernel/renderable.cpp index 787174f..67e8e4e 100644 --- a/kernel/src/kernel/renderable.cpp +++ b/kernel/src/kernel/renderable.cpp @@ -1,81 +1,13 @@ #include "renderable.hpp" -#include -#include namespace aethera { std::optional Renderable::event_routing_distance(const Event& event) const { const auto& data = static_cast(*d); return data.event_routing_distance_run ? data.event_routing_distance_run(this, event) : std::nullopt; } -namespace { -class Stage_Registry : Pinned { -private: - struct Binding { - const tf::Taskflow* owner; - Root* object; - Renderable::Private* data; - detail::Renderable_Stage stage; - detail::Stage_Observer_Point point; - }; - std::unordered_map bindings; - std::mutex mutex; -public: - void bind(const tf::Taskflow* owner, std::size_t task_hash, Root* object, Renderable::Private* data, detail::Renderable_Stage stage, detail::Stage_Observer_Point point) { - std::lock_guard guard(mutex); - bindings.insert_or_assign(task_hash, Binding{owner, object, data, stage, point}); - } - void clear(const tf::Taskflow* owner) { - std::lock_guard guard(mutex); - std::erase_if( - bindings, - [owner](const auto& value) { - return value.second.owner == owner; - } - ); - } - void on_entry(std::size_t task_hash, std::uint64_t now) { - std::lock_guard guard(mutex); - auto current = bindings.find(task_hash); - if (current == bindings.end() || current->second.point != detail::Stage_Observer_Point::End) return; - auto& binding = current->second; - auto& state = *binding.data->dispatch->state.get(binding.object); - if (binding.stage == detail::Renderable_Stage::Prepare) { - if (state.prepare_executed) state.prepare_execution_time_ns = now - state.prepare_execution_time_ns; - } - else if (state.paint_executed) { - state.paint_execution_time_ns = now - state.paint_execution_time_ns; - } - } - void on_exit(std::size_t task_hash, std::uint64_t now) { - std::lock_guard guard(mutex); - auto current = bindings.find(task_hash); - if (current == bindings.end() || current->second.point != detail::Stage_Observer_Point::Begin) return; - auto& binding = current->second; - auto& state = *binding.data->dispatch->state.get(binding.object); - if (binding.stage == detail::Renderable_Stage::Prepare) { - state.prepare_execution_time_ns = state.prepare_executed ? now : 0; - } - else { - state.paint_execution_time_ns = state.paint_executed ? now : 0; - } - } -}; -Stage_Registry& stage_registry() { - static Stage_Registry value; - return value; -} -} -namespace detail { -void observe_stage_entry(std::size_t task_hash, std::uint64_t now) { - stage_registry().on_entry(task_hash, now); -} -void observe_stage_exit(std::size_t task_hash, std::uint64_t now) { - stage_registry().on_exit(task_hash, now); -} -void bind_stage_observer(const tf::Taskflow* owner, std::size_t task_hash, Root* object, Renderable::Private* data, Renderable_Stage stage, Stage_Observer_Point point) { - stage_registry().bind(owner, task_hash, object, data, stage, point); -} -void clear_stage_observers(const tf::Taskflow* owner) { - stage_registry().clear(owner); -} +tf::Taskflow& Renderable::prepare_taskflow() { + return static_cast(*d).prepare_extension; +} +tf::Taskflow& Renderable::paint_taskflow() { + return static_cast(*d).paint_extension; } } diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index e42eb5b..a393140 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -88,6 +88,16 @@ struct Renderable : Def { struct Private; /* 返回该对象参与当前事件竞争时的几何距离;无值表示沿用普通绘制层级路由。 */ [[nodiscard]] std::optional event_routing_distance(const Event& event) const; + /* + * 返回 Prepare 阶段完成后执行的直接 Taskflow 扩展端口。 + * tf::Taskflow 只能在该 Renderable 所属 Scene 没有运行时修改;禁止在图执行期间 emplace/erase/clear。 + */ + [[nodiscard]] tf::Taskflow& prepare_taskflow(); + /* + * 返回 Paint 阶段完成后执行的直接 Taskflow 扩展端口。 + * tf::Taskflow 只能在该 Renderable 所属 Scene 没有运行时修改;禁止在图执行期间 emplace/erase/clear。 + */ + [[nodiscard]] tf::Taskflow& paint_taskflow(); private: /* 数据模式的内部调度入口:执行最终对象 prepare_data(...),再触发各 CRTP 层 after_prepare_data(...)。 */ template diff --git a/kernel/src/kernel/renderable.ipp b/kernel/src/kernel/renderable.ipp index c159650..549c967 100644 --- a/kernel/src/kernel/renderable.ipp +++ b/kernel/src/kernel/renderable.ipp @@ -25,8 +25,9 @@ struct Renderable::Private : Prev_Private { Graph_Builder builder; /* 子图模式构建入口;数据模式为空。 */ }; struct State_Dispatch { - State_Get get; /* 获取最终对象的 Renderable 状态层。 */ - State_Notify notify; /* 发布最终对象的 Renderable 状态。 */ + State_Get current; /* 读取已经发布的 Renderable 状态。 */ + State_Get pending; /* 写入本阶段诊断,由完成节点统一交换发布。 */ + State_Notify publish; /* 阶段完成后交换 State 双缓冲并发布稳定 current。 */ }; struct Dispatch { Stage_Dispatch prepare; /* Prepare 阶段分派。 */ @@ -39,6 +40,8 @@ struct Renderable::Private : Prev_Private { Color_Cache_Visit color_cache_visit{}; /* 最终对象存在 Color_Cache Buffer 时访问本轮写入结果。 */ std::unique_ptr prepare_graph; /* Prepare 子图模式的当前构建产物。 */ std::unique_ptr paint_graph; /* Paint 子图模式的当前构建产物。 */ + tf::Taskflow prepare_extension{}; /* 外部直接续写的 Prepare 完成图;不参与内部子图重建。 */ + tf::Taskflow paint_extension{}; /* 外部直接续写的 Paint 完成图;不参与内部子图重建。 */ bool prepare_graph_built{}; /* Prepare 子图是否至少成功构建过一次。 */ bool paint_graph_built{}; /* Paint 子图是否至少成功构建过一次。 */ /* CRTP 可覆盖:决定已选中子图模式的 Prepare 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */ @@ -190,6 +193,13 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { }, [](Root* root) { auto* value = static_cast(root); + auto& private_data = static_cast(*value->d); + return static_cast(private_data.state.pending); + }, + [](Root* root) { + auto* value = static_cast(root); + auto& private_data = static_cast(*value->d); + private_data.state.advance(); value->template notify_state(); } } @@ -197,16 +207,4 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) { data.dispatch = &dispatch; object->template mark_dirty(); } -namespace detail { -enum class Renderable_Stage { - Prepare, - Paint -}; -enum class Stage_Observer_Point { - Begin, - End -}; -void bind_stage_observer(const tf::Taskflow* owner, std::size_t task_hash, Root* object, Renderable::Private* data, Renderable_Stage stage, Stage_Observer_Point point); -void clear_stage_observers(const tf::Taskflow* owner); -} } diff --git a/kernel/src/kernel/scene.cpp b/kernel/src/kernel/scene.cpp index 7f81a2e..38cd779 100644 --- a/kernel/src/kernel/scene.cpp +++ b/kernel/src/kernel/scene.cpp @@ -1,7 +1,5 @@ #include "scene.hpp" /* 后端共有 Prepare Scene 实现。 */ namespace aethera { Scene::Private::Private() = default; -Scene::Private::~Private() { - if (runtime && runtime->taskflow) detail::clear_stage_observers(runtime->taskflow.get()); -} +Scene::Private::~Private() = default; } diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index 375a15c..44b59ac 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -63,11 +64,12 @@ void Scene::Private::process(Object* object, Callback&& callback) requires std:: template void Scene::Private::process(Object* object, Render_Frame* frame, Callback&& callback) requires std::invocable { auto& private_data = static_cast(*this); - auto& state = static_cast(*private_data.state.current); + auto& state = static_cast(*private_data.state.pending); state.taskflow_execution_time_ns = 0; if (frame) frame->mark(Frame_Trace_Marker::prepare_started); if (runtime->taskflow && !runtime->taskflow->empty()) state.taskflow_execution_time_ns = detail::run_taskflow(*runtime->taskflow); if (frame) frame->mark(Frame_Trace_Marker::prepare_finished); + private_data.state.advance(); object->template notify_state(); Result result; std::invoke(std::forward(callback), std::as_const(result)); @@ -79,7 +81,7 @@ void Scene::Private::after_advance(Object* object, const Prop* current_prop, State_Access current_states) { auto* resource = detail::task_memory_resource(); - auto& scene_state = current_states.get(); + auto& scene_state = pending_states.get(); scene_state.taskflow_rebuilt = false; std::pmr::unordered_set advanced_objects{resource}; advanced_objects.insert(object); @@ -87,18 +89,20 @@ void Scene::Private::after_advance(Object* object, [&](const Dependency_Graph& dependency_graph) { dependency_graph.for_each( [&](const Dependency_Graph::Node& node) { - if (advanced_objects.insert(node.object).second) node.object->advance_object(); + if (advanced_objects.insert(node.object).second) { + node.object->advance_object(); + } } ); } ); + auto prepare_dependencies = object->template current_dependency_graph(); bool taskflow_dirty = !runtime->taskflow; object->template access_pending_dependency_graph( [&](auto& prepare_state) { taskflow_dirty = taskflow_dirty || prepare_state.dirty(); }); if (!taskflow_dirty) return; if (!runtime->taskflow) runtime->taskflow = std::make_unique(); auto& taskflow = *runtime->taskflow; - auto prepare_dependencies = object->template current_dependency_graph(); std::pmr::unordered_set renderables{resource}; prepare_dependencies.for_each_bound( [&](Renderable* renderable, Renderable::Private&) { @@ -106,7 +110,6 @@ void Scene::Private::after_advance(Object* object, } ); scene_state.renderable_count = renderables.size(); - detail::clear_stage_observers(&taskflow); taskflow.clear(); struct Stage_Tasks { tf::Task prepare_entry; /* Prepare 条件任务,作为该阶段依赖入口。 */ @@ -119,7 +122,7 @@ void Scene::Private::after_advance(Object* object, if (!data) continue; auto* dispatch = data->dispatch; auto prepare_if = taskflow.emplace([data, dispatch, root] { - auto& state = *dispatch->state.get(root); + auto& state = *dispatch->state.pending(root); state.prepare_graph_rebuilt = false; state.prepare_execution_time_ns = 0; if (dispatch->prepare.builder) { @@ -138,6 +141,11 @@ void Scene::Private::after_advance(Object* object, bool dirty = root->template dirty(); state.prepare_dirty = dirty; state.prepare_executed = dispatch->prepare.predicate(root, dirty); + if (state.prepare_executed) { + state.prepare_execution_time_ns = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + } return state.prepare_executed ? 0 : 1; }).name("renderable.prepare.condition"); tf::Task prepare_run; @@ -150,15 +158,22 @@ void Scene::Private::after_advance(Object* object, if (dispatch->prepare.run) dispatch->prepare.run(root); }).name("renderable.prepare.data"); } + auto prepare_extension = taskflow.composed_of(data->prepare_extension) + .name("renderable.prepare.extension"); auto prepare_done = taskflow.emplace([dispatch, root] { - auto& state = *dispatch->state.get(root); - if (state.prepare_executed) root->template take_dirty(); - dispatch->state.notify(root); + auto& state = *dispatch->state.pending(root); + if (state.prepare_executed) { + root->template take_dirty(); + const auto finished = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + state.prepare_execution_time_ns = finished - state.prepare_execution_time_ns; + } + dispatch->state.publish(root); }).name("renderable.prepare.complete"); prepare_if.precede(prepare_run, prepare_done); - prepare_run.precede(prepare_done); - detail::bind_stage_observer(&taskflow, prepare_if.hash_value(), root, data, detail::Renderable_Stage::Prepare, detail::Stage_Observer_Point::Begin); - detail::bind_stage_observer(&taskflow, prepare_done.hash_value(), root, data, detail::Renderable_Stage::Prepare, detail::Stage_Observer_Point::End); + prepare_run.precede(prepare_extension); + prepare_extension.precede(prepare_done); stage_tasks.emplace(root, Stage_Tasks{prepare_if, prepare_done}); } auto connect_dependencies = [&](const auto& dependency_graph) { diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index 19f587e..64eb83c 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -93,16 +93,21 @@ TEST(renderable_capability, direct_stages_do_not_allocate_subgraphs) { auto renderable = build_object(); auto scene = build_object(); add_renderable(*scene, renderable.get()); + int prepare_extension_calls{}; + renderable->prepare_taskflow().emplace( + [&] { ++prepare_extension_calls; }).name("test.prepare.extension"); scene->process([](const auto&) {}); auto& data = renderable->data_for_test(); auto& base = static_cast(data); EXPECT_EQ(data.prepare_calls, 1); EXPECT_EQ(data.paint_calls, 0); + EXPECT_EQ(prepare_extension_calls, 1); EXPECT_EQ(base.prepare_graph, nullptr); EXPECT_EQ(base.paint_graph, nullptr); scene->process([](const auto&) {}); EXPECT_EQ(data.prepare_calls, 1); EXPECT_EQ(data.paint_calls, 0); + EXPECT_EQ(prepare_extension_calls, 1); } TEST(renderable_capability, graph_stage_builds_lazily_and_rebuilds_inside_condition) { aethera::initialize_runtime(2); diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index f538564..daf75b1 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -110,7 +110,7 @@ inline void Time_Axis::Private::prepare_data(Attached auto* object) { const auto& prop = static_cast(*private_data.current); object->template accumulate_stream( static_cast(std::max(2, prop.visible_count))); - auto& state = static_cast(*private_data.state.current); + auto& state = static_cast(*private_data.state.pending); state.next_tick = next_tick.load(std::memory_order_relaxed); object->template access_rendering_stream( [&](std::span> samples) { diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index e93391f..fe63f78 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -177,7 +177,7 @@ template void Spectrum::Private::paint_frame(Object* object) { auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.current); - auto& published = static_cast(*private_data.state.current); + auto& published = static_cast(*private_data.state.pending); const auto& frame = object->template current_buffer(); published.sample_count = frame.samples.size(); published.rendered_point_count = 0; diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 13bb0fb..f46ea7c 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -1,13 +1,14 @@ #include "Render_Scene_2D.hpp" /* 二维 Paint Taskflow、缓存失效与合成实现。 */ namespace aethera::render_2d { -Render_Scene_2D::Private::~Private() { - if (paint_taskflow) aethera::detail::clear_stage_observers(paint_taskflow.get()); -} +Render_Scene_2D::Private::~Private() = default; bool Render_Scene_2D::State::operator==(const State&) const = default; bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; Render_Scene_2D::Render_Result Render_Scene_2D::render(Frame_2D* frame) { return static_cast(*d).dispatch->render(this, frame); } void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } +tf::Taskflow& Render_Scene_2D::completion_taskflow() { + return static_cast(*d).completion_graph; +} void Render_Scene_2D::activate_view() { static_cast(*d).dispatch->set_active(this, true); } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index 8633470..da2aa6b 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -33,12 +33,19 @@ struct Render_Scene_2D : Def(Object*)>> attachments{}; /* 仅在 build 期间绑定已构造 Renderable。 */ }; - enum class Render_Result { completed, view_inactive, empty_viewport }; + enum class Render_Result { completed, frame_in_flight, view_inactive, empty_viewport }; using Frame_Callback = std::function; - /* 向调用方拥有的帧合成一次;frame 必须存活到完成回调返回。 */ + /* 向调用方拥有的帧合成一次;必须先安装回调。回调返回前的并发请求返回 frame_in_flight。 */ [[nodiscard]] Render_Result render(Frame_2D* frame); - /* 安装完成帧回调;回调收到的就是对应 render(frame) 传入的对象。 */ + /* 安装完成帧回调;回调收到对应 render(frame) 的对象,返回时 Scene 才释放下一帧准入。 */ void set_frame_callback(Frame_Callback callback); + /* + * 返回最终像素完成后、帧回调前执行的直接 Taskflow。 + * 只能在 Scene 没有运行时修改该图;禁止在执行期间 emplace/erase/clear。 + * completion Taskflow 完成前调用方必须保持本帧像素所有权;节点若启动更晚结束的异步工作, + * 必须捕获拥有像素所有权的对象,禁止只捕获会被下一帧复用的裸 Frame_2D*。 + */ + [[nodiscard]] tf::Taskflow& completion_taskflow(); /* 激活后 render 才会执行。 */ void activate_view(); /* 停止后续 render 调用,不清除最后一帧。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index a4a5516..162b6cd 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -1,6 +1,8 @@ #pragma once +#include #include "../event/Event_Routing_Rules.hpp" #include +#include #include #include #include @@ -57,7 +59,10 @@ struct Render_Scene_2D::Private : Prev_Private { Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */ }; const Dispatch* dispatch{}; /* Builder 绑定最终 Scene 类型后的静态分派表。 */ + std::mutex render_mutex{}; /* 只保护完成回调和单帧准入。 */ + bool frame_in_flight{}; /* render 准入到完成回调返回的唯一状态源。 */ Frame_Callback frame_callback{}; /* 合成完成后的唯一像素发布出口。 */ + tf::Taskflow completion_graph{}; /* 最终像素完成后、发布回调前执行的外部续写图。 */ std::unique_ptr paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */ ~Private(); Frame_2D* active_frame{}; /* 当前同步 process 借用的外部帧;render 返回前清空。 */ @@ -97,7 +102,6 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access if (!rebuild) return; if (!paint_taskflow) paint_taskflow = std::make_unique(); auto& taskflow = *paint_taskflow; - aethera::detail::clear_stage_observers(&taskflow); taskflow.clear(); const auto dependencies = object->template current_dependency_graph(); paint_order.clear(); @@ -139,7 +143,7 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access auto& data = *paint_node.private_data; auto* dispatch = data.dispatch; auto paint_if = taskflow.emplace([this, &data, dispatch, root, index] { - auto& state = *dispatch->state.get(root); + auto& state = *dispatch->state.pending(root); state.paint_graph_rebuilt = false; state.paint_execution_time_ns = 0; if (dispatch->paint.builder) { @@ -155,6 +159,11 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access const bool dirty = root->template dirty(); state.paint_dirty = dirty; state.paint_executed = paint_order[index].paint_requested && dispatch->paint.predicate(root, dirty); + if (state.paint_executed) { + state.paint_execution_time_ns = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + } return state.paint_executed ? 0 : 1; }).name("render_2d.paint.condition"); tf::Task paint_run; @@ -162,16 +171,23 @@ void Render_Scene_2D::Private::after_advance(Object* object, Prop*, State_Access if (!data.paint_graph) data.paint_graph = std::make_unique(); paint_run = taskflow.composed_of(*data.paint_graph).name("render_2d.paint.graph"); } else paint_run = taskflow.emplace([dispatch, root] { if (dispatch->paint.run) dispatch->paint.run(root); }).name("render_2d.paint.data"); + auto paint_extension = taskflow.composed_of(data.paint_extension) + .name("renderable.paint.extension"); auto paint_done = taskflow.emplace([dispatch, root] { - auto& state = *dispatch->state.get(root); - if (state.paint_executed) root->template take_dirty(); - dispatch->state.notify(root); + auto& state = *dispatch->state.pending(root); + if (state.paint_executed) { + root->template take_dirty(); + const auto finished = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + state.paint_execution_time_ns = finished - state.paint_execution_time_ns; + } + dispatch->state.publish(root); }).name("render_2d.paint.complete"); paint_if.precede(paint_run, paint_done); - paint_run.precede(paint_done); + paint_run.precede(paint_extension); + paint_extension.precede(paint_done); if (has_previous) previous.precede(paint_if); - aethera::detail::bind_stage_observer(&taskflow, paint_if.hash_value(), root, &data, aethera::detail::Renderable_Stage::Paint, aethera::detail::Stage_Observer_Point::Begin); - aethera::detail::bind_stage_observer(&taskflow, paint_done.hash_value(), root, &data, aethera::detail::Renderable_Stage::Paint, aethera::detail::Stage_Observer_Point::End); previous = paint_done; if (paint_node.cache_group_last) { Root* cache_owner = paint_node.cache_owner; @@ -283,6 +299,22 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) template Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) { static_cast(detail::Frame_2D_Access::render_target(frame)); + Frame_Callback callback; + { + std::lock_guard lock(render_mutex); + if (!frame_callback) + throw std::logic_error("Render_Scene_2D requires a frame callback before render"); + if (frame_in_flight) return Render_Result::frame_in_flight; + frame_in_flight = true; + callback = frame_callback; + } + struct Admission_Scope { + Private& data; + ~Admission_Scope() { + std::lock_guard lock(data.render_mutex); + data.frame_in_flight = false; + } + } admission{*this}; frame->mark(Frame_Trace_Marker::scene_render_requested); struct Active_Frame_Scope { Frame_2D*& target; /* 最终 Private 的同步 process 帧槽位。 */ @@ -294,8 +326,10 @@ Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, object->process([&] { completed = true; frame->mark(Frame_Trace_Marker::scene_render_finished); + /* 帧准入直到 completion 图和最终回调都结束才释放,因此图运行期间外部帧不会被下一帧复用。 */ + if (!completion_graph.empty()) aethera::detail::run_taskflow(completion_graph); frame->mark(Frame_Trace_Marker::callback_started); - if (frame_callback) frame_callback(frame); + callback(frame); frame->mark(Frame_Trace_Marker::callback_finished); }); if (completed) return Render_Result::completed; @@ -339,7 +373,9 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); - static_cast(*object->d).frame_callback = std::move(callback); + auto& data = static_cast(*object->d); + std::lock_guard lock(data.render_mutex); + data.frame_callback = std::move(callback); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); } }; diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index 32e3b96..c50d842 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -17,6 +17,7 @@ template std::unique_ptr render_frame(Scene* scene) { static std::uint64_t sequence{1}; auto frame = std::make_unique(Frame_Identity{sequence++, 0}); + scene->set_frame_callback([](Frame_2D*) {}); EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed); return frame; } @@ -92,7 +93,17 @@ TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) { prepare.add(axis.get()); paint.add(axis.get()); }).has_value())); + int paint_extension_calls{}; + int completion_calls{}; + axis->paint_taskflow().emplace( + [&] { ++paint_extension_calls; }).name("test.axis.paint.extension"); + scene->completion_taskflow().emplace([&] { + EXPECT_EQ(paint_extension_calls, 1); + ++completion_calls; + }).name("test.scene_2d.completion"); const auto frame = render_frame(scene.get()); + EXPECT_EQ(paint_extension_calls, 1); + EXPECT_EQ(completion_calls, 1); const Image_View view = frame->image(); ASSERT_FALSE(view.empty()); bool contains_color{}; diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index f10b5dd..c74b7f3 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -35,6 +35,7 @@ template void render_once(Scene* scene) { static std::uint64_t sequence{1}; Frame_2D frame{Frame_Identity{sequence++, 0}}; + scene->set_frame_callback([](Frame_2D*) {}); EXPECT_EQ(scene->render(&frame), Render_Scene_2D::Render_Result::completed); } } diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index de4abef..c919429 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -32,6 +32,7 @@ template std::unique_ptr render_frame(Scene* scene) { static std::uint64_t sequence{1}; auto frame = std::make_unique(Frame_Identity{sequence++, 0}); + scene->set_frame_callback([](Frame_2D*) {}); EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed); return frame; } diff --git a/render_3D/render_3D/Gpu_Completion_State.cpp b/render_3D/render_3D/Gpu_Completion_State.cpp new file mode 100644 index 0000000..5a173e0 --- /dev/null +++ b/render_3D/render_3D/Gpu_Completion_State.cpp @@ -0,0 +1,8 @@ +#include "Gpu_Completion_State.hpp" +#include "detail/Gpu_Completion_Service.hpp" + +namespace aethera::render_3d { +Gpu_Completion_State gpu_completion_state() noexcept { + return detail::Gpu_Completion_Service::instance().state(); +} +} diff --git a/render_3D/render_3D/Gpu_Completion_State.hpp b/render_3D/render_3D/Gpu_Completion_State.hpp new file mode 100644 index 0000000..274256c --- /dev/null +++ b/render_3D/render_3D/Gpu_Completion_State.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include + +namespace aethera::render_3d { +struct Gpu_Completion_State_Tag {}; + +/* 只由 GPU Completion Domain 写入并通过双缓冲发布。 */ +struct Gpu_Completion_State : State_Type { + std::size_t capacity{}; + std::size_t in_flight{}; + std::size_t peak_in_flight{}; + std::size_t watched{}; + std::size_t peak_watched{}; + std::size_t active_fences{}; + std::size_t pending_fences{}; + std::uint64_t reservation_count{}; + std::uint64_t completion_count{}; + std::uint64_t cancellation_count{}; + std::uint64_t fence_probe_count{}; + std::uint64_t fence_wait_count{}; + std::uint64_t fence_wait_timeout_count{}; + std::uint64_t fence_wait_total_ns{}; + std::uint64_t fence_wait_max_ns{}; + std::uint64_t callback_total_ns{}; + std::uint64_t callback_max_ns{}; + std::uint64_t callback_failure_count{}; + std::uint64_t backpressure_count{}; + std::uint64_t backpressure_wait_ns{}; + std::uint64_t fault_count{}; + std::uint64_t abandoned_count{}; + bool stopping{}; + bool operator==(const Gpu_Completion_State&) const = default; +}; + +[[nodiscard]] Gpu_Completion_State gpu_completion_state() noexcept; +} diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index fbb3efd..bc39463 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -2,11 +2,12 @@ #include "Datoviz_Visual_Backend.hpp" #include "Gpu_Completion_Service.hpp" #include "Render_Domain.hpp" -#include #include #include #include #include +#include +#include #include #include #include @@ -85,17 +86,15 @@ struct Async_Render_Backend::Implementation std::optional result{}; /* fence 正常交付时的结果。 */ std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */ }; - using Command = std::variant; + struct Stop {}; + using Command = std::variant; - static constexpr std::size_t command_capacity = 64; std::shared_ptr render_domain; /* 同 GPU 唯一的轻量 Queue Submit 域。 */ - std::unique_ptr backend{}; /* 仅准备线程构造、准备、收集和销毁。 */ - std::uint32_t gpu_index{}; /* Datoviz 共享 GPU Context 的设备下标。 */ - bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ - std::vector visual_registrations{}; /* 首帧初始化后释放的稳定 Visual 注册表。 */ - moodycamel::BlockingConcurrentQueue commands{command_capacity}; /* 多生产者到单准备线程的命令入口。 */ - std::thread preparation_thread{}; /* 每 Scene 独立的 Datoviz CPU 准备域。 */ + std::unique_ptr backend{}; /* Builder build() 创建;随后由本 Scene 串行使用。 */ std::optional deferred_submission{}; /* 三槽占满时仅保留的最新画面和历史回调。 */ + std::mutex command_mutex{}; /* 只保护本 Scene 的入队和 drain 所有权。 */ + std::deque commands{}; /* 不同 Scene 可并行,同一 Scene 保持严格串行。 */ + bool drain_scheduled{}; /* 是否已有 Taskflow worker 负责本 Scene。 */ std::mutex callback_mutex{}; /* 保护完成回调替换与 render 捕获。 */ Frame_Callback frame_callback{}; /* 后续 render 捕获的完成出口。 */ std::mutex failure_mutex{}; /* 保护跨线程传播的最后 Unknown Failure。 */ @@ -106,19 +105,19 @@ struct Async_Render_Backend::Implementation Implementation(std::uint32_t gpu_index_value, bool validation_enabled_value, - std::vector visuals) + std::vector visuals, + const Scene_3D_Parameters& initial_scene) : render_domain(Render_Domain::acquire(gpu_index_value)), - gpu_index(gpu_index_value), - validation_enabled(validation_enabled_value), - visual_registrations(std::move(visuals)) {} + backend(std::make_unique( + gpu_index_value, validation_enabled_value, visuals, initial_scene)) {} ~Implementation(); - void start(); void stop() noexcept; - void run() noexcept; void fail(std::exception_ptr value) noexcept; void enqueue(Command command); - void initialize(const Scene_3D_Parameters& parameters); + void drain() noexcept; + void execute(Command command) noexcept; + void finish_stop(); [[nodiscard]] Async_Render_Backend::Submit_Result render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame, Scene::Event_Batch events); @@ -135,35 +134,28 @@ struct Async_Render_Backend::Implementation Async_Render_Backend::Async_Render_Backend( std::uint32_t gpu_index, bool validation_enabled, - std::vector visuals) + std::vector visuals, + const Scene_3D_Parameters& initial_scene) : implementation_(std::make_shared( - gpu_index, validation_enabled, std::move(visuals))) { - implementation_->start(); -} + gpu_index, validation_enabled, std::move(visuals), initial_scene)) {} Async_Render_Backend::~Async_Render_Backend() { implementation_->stop(); } Async_Render_Backend::Implementation::~Implementation() { stop(); - if (preparation_thread.joinable() && - preparation_thread.get_id() == std::this_thread::get_id()) - preparation_thread.detach(); -} -void Async_Render_Backend::Implementation::start() { - auto self = shared_from_this(); - preparation_thread = std::thread([self = std::move(self)] { self->run(); }); } void Async_Render_Backend::Implementation::stop() noexcept { + if (stop_requested.exchange(true, std::memory_order_acq_rel)) return; { std::lock_guard lock(admission_mutex); available.store(false, std::memory_order_release); - stop_requested.store(true, std::memory_order_release); } - if (preparation_thread.joinable() && - preparation_thread.get_id() != std::this_thread::get_id()) - preparation_thread.join(); + try { + enqueue(Stop{}); + } + catch (...) { fail(std::current_exception()); } } void Async_Render_Backend::Implementation::fail( std::exception_ptr value) noexcept { @@ -182,15 +174,33 @@ void Async_Render_Backend::Implementation::fail( catch (...) {} } void Async_Render_Backend::Implementation::enqueue(Command command) { - if (!commands.enqueue(std::move(command))) throw std::bad_alloc{}; + bool schedule{}; + { + std::lock_guard lock(command_mutex); + commands.push_back(std::move(command)); + if (!drain_scheduled) { + drain_scheduled = true; + schedule = true; + } + } + if (!schedule) return; + auto self = shared_from_this(); + aethera::schedule_task([self] { self->drain(); }); } -void Async_Render_Backend::Implementation::initialize( - const Scene_3D_Parameters& parameters) { - if (backend) return; - backend = std::make_unique( - gpu_index, validation_enabled, visual_registrations, parameters); - visual_registrations.clear(); - visual_registrations.shrink_to_fit(); +void Async_Render_Backend::Implementation::drain() noexcept { + for (;;) { + std::optional command; + { + std::lock_guard lock(command_mutex); + if (commands.empty()) { + drain_scheduled = false; + break; + } + command.emplace(std::move(commands.front())); + commands.pop_front(); + } + execute(std::move(*command)); + } } Async_Render_Backend::Submit_Result Async_Render_Backend::Implementation::render( @@ -254,7 +264,6 @@ void Async_Render_Backend::Implementation::accept(Submission submission) { void Async_Render_Backend::Implementation::prepare(Submission submission) { std::optional prepared; try { - initialize(submission.parameters); for (const auto& event : submission.events) dispatch(event, submission.parameters.viewport); submission.events.clear(); @@ -492,52 +501,42 @@ void Async_Render_Backend::Implementation::dispatch( if ((wheel && pointer_valid) || pointer_valid || key) event->accept(); event->mark_dispatch_completed(); } -void Async_Render_Backend::Implementation::run() noexcept { - for (;;) { - Command command; - const bool received = commands.wait_dequeue_timed( - command, std::chrono::milliseconds(10)); - if (received) { - try { - if (auto* submission = std::get_if(&command)) { - accept(std::move(*submission)); - } - else if (auto* completion = std::get_if(&command)) - finish(std::move(*completion)); - } - catch (...) { - fail(std::current_exception()); - if (auto* submission = std::get_if(&command)) { - auto pending = std::make_shared(); - pending->extent = submission->parameters.viewport; - pending->completions = std::move(submission->completions); - try { resolve(std::move(pending), std::nullopt); } - catch (...) { fail(std::current_exception()); } - } - else if (auto* completion = std::get_if(&command)) { - try { resolve(std::move(completion->pending), std::nullopt); } - catch (...) { fail(std::current_exception()); } - } - } +void Async_Render_Backend::Implementation::execute(Command command) noexcept { + try { + if (auto* submission = std::get_if(&command)) + accept(std::move(*submission)); + else if (auto* completion = std::get_if(&command)) + finish(std::move(*completion)); + else + finish_stop(); + } + catch (...) { + fail(std::current_exception()); + if (auto* submission = std::get_if(&command)) { + auto pending = std::make_shared(); + pending->extent = submission->parameters.viewport; + pending->completions = std::move(submission->completions); + try { resolve(std::move(pending), std::nullopt); } + catch (...) { fail(std::current_exception()); } } - // render/event 与 stop 共享 admission_mutex,因此停止后只有已经 - // 提交到 GPU 的完成通知还能到达。只有一次真实的 timed dequeue - // 为空且 backend 已无在途目标,准备域才完成排空并退出。 - if (!received && stop_requested.load(std::memory_order_acquire) && - (!backend || backend->idle())) { - if (deferred_submission) { - auto pending = std::make_shared(); - pending->extent = deferred_submission->parameters.viewport; - pending->completions = - std::move(deferred_submission->completions); - deferred_submission.reset(); - try { resolve(std::move(pending), std::nullopt); } - catch (...) { fail(std::current_exception()); } - } - backend.reset(); - return; + else if (auto* completion = std::get_if(&command)) { + try { resolve(std::move(completion->pending), std::nullopt); } + catch (...) { fail(std::current_exception()); } } } + if (stop_requested.load(std::memory_order_acquire)) finish_stop(); +} + +void Async_Render_Backend::Implementation::finish_stop() { + if (deferred_submission) { + auto pending = std::make_shared(); + pending->extent = deferred_submission->parameters.viewport; + pending->completions = std::move(deferred_submission->completions); + deferred_submission.reset(); + resolve(std::move(pending), std::nullopt); + } + if (backend && !backend->idle()) return; + backend.reset(); } Async_Render_Backend::Submit_Result Async_Render_Backend::render( diff --git a/render_3D/render_3D/detail/Async_Render_Backend.hpp b/render_3D/render_3D/detail/Async_Render_Backend.hpp index d289fec..e9a2ee9 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.hpp @@ -13,7 +13,8 @@ public: backend_unavailable }; Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, - std::vector visuals); + std::vector visuals, + const Scene_3D_Parameters& initial_scene); ~Async_Render_Backend(); Async_Render_Backend(const Async_Render_Backend&) = delete; Async_Render_Backend& operator=(const Async_Render_Backend&) = delete; diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index f56b4f4..3cbb583 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -814,8 +814,7 @@ Datoviz_Visual_Backend::Datoviz_Visual_Backend( std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, const Scene_3D_Parameters& initial_scene) - : preparation_thread_(std::this_thread::get_id()), - targets_(std::make_unique()) { + : targets_(std::make_unique()) { try { render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); std::scoped_lock context_lock(render_context_->api_mutex(), @@ -842,11 +841,6 @@ Datoviz_Visual_Backend::~Datoviz_Visual_Backend() noexcept { try { destroy(); } catch (...) { abandon_resources(); } } -void Datoviz_Visual_Backend::require_domain() const { - if (std::this_thread::get_id() != preparation_thread_) - throw std::logic_error( - "Datoviz Scene preparation may only run on its preparation thread"); -} void Datoviz_Visual_Backend::create_scene( const std::vector& registrations, const Scene_3D_Parameters& initial_scene) { @@ -1327,7 +1321,6 @@ bool Datoviz_Visual_Backend::matches_command_structure( void Datoviz_Visual_Backend::apply( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared, std::uint8_t target_index, bool bind_target) { - require_domain(); if (figure_extent_ != scene.viewport) { if (dvz_figure_resize(figure_, scene.viewport.width, scene.viewport.height) != DVZ_OK) @@ -1747,7 +1740,6 @@ void Datoviz_Visual_Backend::dispatch_pointer( ::aethera::Event_Type event, float x, float y, ::aethera::Mouse_Button mouse_button, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { - require_domain(); std::lock_guard api_lock(render_context_->api_mutex()); input_changed_ = true; if (applied_camera_ && applied_camera_->controller == Camera_Controller::turntable) { @@ -1773,7 +1765,6 @@ void Datoviz_Visual_Backend::dispatch_pointer( void Datoviz_Visual_Backend::dispatch_wheel( float x, float y, float delta_x, float delta_y, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { - require_domain(); std::lock_guard api_lock(render_context_->api_mutex()); input_changed_ = true; if (applied_camera_ && @@ -1786,7 +1777,6 @@ void Datoviz_Visual_Backend::dispatch_wheel( } void Datoviz_Visual_Backend::dispatch_key( const ::aethera::Key_Event& event) { - require_domain(); std::lock_guard api_lock(render_context_->api_mutex()); input_changed_ = true; if (event.key == ::aethera::Key::home && event.type == ::aethera::Event_Type::key_press) { @@ -1936,7 +1926,6 @@ Datoviz_Visual_Backend::reuse_recorded_target( std::optional Datoviz_Visual_Backend::prepare( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback) { - require_domain(); if (scene.viewport.empty()) return std::nullopt; if (auto reused = reuse_recorded_target( scene, visuals, frame_sequence, observe, readback)) @@ -2097,7 +2086,6 @@ void Datoviz_Visual_Backend::submit(Pending_Frame& pending) { } Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect( Pending_Frame pending) { - require_domain(); /* Fence 已完成,读回缓冲与查询池只属于本目标槽;不同 Scene 的映射内存 * 下载可以并行,且不能反向阻塞 Render_Domain 的下一批提交。 */ std::lock_guard target_lock(target_mutex_); @@ -2110,7 +2098,6 @@ Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect( return {pending.extent, std::move(collection.pixels), std::move(pending.trace)}; } void Datoviz_Visual_Backend::discard(Pending_Frame pending) { - require_domain(); std::lock_guard target_lock(target_mutex_); target(pending).discard_after_completion(); } @@ -2157,9 +2144,6 @@ void Datoviz_Visual_Backend::abandon_resources() noexcept { retain_quarantined_context(render_context_); } void Datoviz_Visual_Backend::destroy() { - if (std::this_thread::get_id() != preparation_thread_) - throw std::logic_error( - "Datoviz backend may only be destroyed on its preparation thread"); if (quarantined_) { abandon_resources(); return; diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp index 831469e..1816dce 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -79,7 +79,6 @@ private: std::optional applied{}; /* Last applied metadata and immutable payload snapshot. */ std::vector attributes{}; /* Dynamic payload buffers, one triple region per field. */ }; - void require_domain() const; void create_scene(const std::vector& visuals, const Scene_3D_Parameters& initial_scene); [[nodiscard]] DvzVisual* create_visual(Visual_Family family); @@ -112,7 +111,6 @@ private: [[nodiscard]] Frame_Target& target(const Pending_Frame& pending); void abandon_resources() noexcept; void destroy(); - std::thread::id preparation_thread_; /* Scene API、资源准备与回收的唯一线程。 */ std::shared_ptr render_context_; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */ mutable std::mutex target_mutex_{}; /* 当前 Scene 三个目标槽的唯一生命周期同步源。 */ struct Runtime_Slot { diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index f10d1d5..b4d3af0 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -10,6 +10,7 @@ Gpu_Completion_Service& Gpu_Completion_Service::instance() { return service; } Gpu_Completion_Service::Gpu_Completion_Service() { + publish_state(0, 0); thread_ = std::thread([this] { run(); }); @@ -62,6 +63,13 @@ void Gpu_Completion_Service::update_peak(std::atomic_size_t& peak, !peak.compare_exchange_weak(current, value, std::memory_order_relaxed)) {} } +void Gpu_Completion_Service::update_max(std::atomic_uint64_t& maximum, + std::uint64_t value) noexcept { + std::uint64_t current = maximum.load(std::memory_order_relaxed); + while (current < value && + !maximum.compare_exchange_weak(current, value, + std::memory_order_relaxed)) {} +} void Gpu_Completion_Service::cancel_reserved( const std::shared_ptr& pending) { if (!pending) return; @@ -112,21 +120,42 @@ Gpu_Completion_Service::Prepare_Result Gpu_Completion_Service::prepare( release_slot(); throw; } + reservation_count_.fetch_add(1, std::memory_order_relaxed); wake(); return {Reservation(std::move(pending)), Admission_Result::none}; } -Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const noexcept { - return { - static_cast(default_capacity), - in_flight_.load(std::memory_order_relaxed), - peak_in_flight_.load(std::memory_order_relaxed), - watched_.load(std::memory_order_relaxed), - peak_watched_.load(std::memory_order_relaxed), - backpressure_count_.load(std::memory_order_relaxed), - backpressure_wait_ns_.load(std::memory_order_relaxed), - fault_count_.load(std::memory_order_relaxed), - abandoned_count_.load(std::memory_order_relaxed) - }; +Gpu_Completion_State Gpu_Completion_Service::state() const noexcept { + std::lock_guard lock(state_mutex_); + return *state_.pending; +} +void Gpu_Completion_Service::publish_state(std::size_t active_fences, + std::size_t pending_fences) noexcept { + std::lock_guard lock(state_mutex_); + auto& state = *state_.current; + state.capacity = static_cast(default_capacity); + state.in_flight = in_flight_.load(std::memory_order_relaxed); + state.peak_in_flight = peak_in_flight_.load(std::memory_order_relaxed); + state.watched = watched_.load(std::memory_order_relaxed); + state.peak_watched = peak_watched_.load(std::memory_order_relaxed); + state.active_fences = active_fences; + state.pending_fences = pending_fences; + state.reservation_count = reservation_count_.load(std::memory_order_relaxed); + state.completion_count = completion_count_.load(std::memory_order_relaxed); + state.cancellation_count = cancellation_count_.load(std::memory_order_relaxed); + state.fence_probe_count = fence_probe_count_.load(std::memory_order_relaxed); + state.fence_wait_count = fence_wait_count_.load(std::memory_order_relaxed); + state.fence_wait_timeout_count = fence_wait_timeout_count_.load(std::memory_order_relaxed); + state.fence_wait_total_ns = fence_wait_total_ns_.load(std::memory_order_relaxed); + state.fence_wait_max_ns = fence_wait_max_ns_.load(std::memory_order_relaxed); + state.callback_total_ns = callback_total_ns_.load(std::memory_order_relaxed); + state.callback_max_ns = callback_max_ns_.load(std::memory_order_relaxed); + state.callback_failure_count = callback_failure_count_.load(std::memory_order_relaxed); + state.backpressure_count = backpressure_count_.load(std::memory_order_relaxed); + state.backpressure_wait_ns = backpressure_wait_ns_.load(std::memory_order_relaxed); + state.fault_count = fault_count_.load(std::memory_order_relaxed); + state.abandoned_count = abandoned_count_.load(std::memory_order_relaxed); + state.stopping = stopping_.load(std::memory_order_relaxed); + state_.advance(); } void Gpu_Completion_Service::wake() noexcept { wake_generation_.fetch_add(1, std::memory_order_release); @@ -141,6 +170,7 @@ void Gpu_Completion_Service::run() noexcept { try { active.reserve(static_cast(default_capacity)); std::size_t wait_group_index{}; + auto last_state_publication = std::chrono::steady_clock::time_point{}; const auto wait_age_ns = [](std::chrono::steady_clock::time_point started) { const auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - started) @@ -176,14 +206,24 @@ void Gpu_Completion_Service::run() noexcept { fault_count_.fetch_add(1, std::memory_order_relaxed); abandoned_count_.fetch_add(1, std::memory_order_relaxed); } + const auto callback_started = std::chrono::steady_clock::now(); try { completion(std::move(completion_result)); } catch (...) { + callback_failure_count_.fetch_add(1, std::memory_order_relaxed); try { on_exception(contextual_exception( "delivering GPU completion", std::current_exception())); } catch (...) {} } + const auto callback_elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - callback_started).count(); + if (callback_elapsed > 0) { + const auto elapsed = static_cast(callback_elapsed); + callback_total_ns_.fetch_add(elapsed, std::memory_order_relaxed); + update_max(callback_max_ns_, elapsed); + } + completion_count_.fetch_add(1, std::memory_order_relaxed); } catch (...) { try { @@ -200,6 +240,7 @@ void Gpu_Completion_Service::run() noexcept { wake_generation_.load(std::memory_order_acquire); { std::lock_guard lock(pending_mutex_); while (!pending_.empty()) { active.push_back(std::move(pending_.front())); pending_.pop_front(); } } std::vector groups; + std::size_t reserved_count{}; for (auto iterator = active.begin(); iterator != active.end();) { Pending_Fence::Status status; VkDevice device{VK_NULL_HANDLE}; @@ -215,9 +256,11 @@ void Gpu_Completion_Service::run() noexcept { stopping_.load(std::memory_order_acquire))) { cancel_reserved(*iterator); iterator = active.erase(iterator); + cancellation_count_.fetch_add(1, std::memory_order_relaxed); release_slot(); continue; } + if (status == Pending_Fence::Status::reserved) ++reserved_count; if (status == Pending_Fence::Status::watched) { auto group = std::find_if( groups.begin(), groups.end(), @@ -232,9 +275,19 @@ void Gpu_Completion_Service::run() noexcept { } ++iterator; } + const auto publication_time = std::chrono::steady_clock::now(); + if (last_state_publication == std::chrono::steady_clock::time_point{} || + publication_time - last_state_publication >= state_publication_interval) { + // 域内只按诊断粒度发布双缓冲 State;逐 fence 热路径只更新原子计数。 + publish_state(active.size(), reserved_count); + last_state_publication = publication_time; + } bool pending_empty; { std::lock_guard lock(pending_mutex_); pending_empty = pending_.empty(); } - if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_empty) return; + if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_empty) { + publish_state(0, 0); + return; + } // Probe every watched fence first. A permanently unsignaled fence is // converted into a logical failure after a bounded interval. The // submission is explicitly marked abandoned so its owner can @@ -270,6 +323,7 @@ void Gpu_Completion_Service::run() noexcept { completed_any = true; continue; } + fence_probe_count_.fetch_add(1, std::memory_order_relaxed); const VkResult result = vkGetFenceStatus(device, fence); if (result == VK_NOT_READY) { ++iterator; @@ -296,9 +350,20 @@ void Gpu_Completion_Service::run() noexcept { } wait_group_index %= groups.size(); const Device_Fences& group = groups[wait_group_index++]; + const auto wait_started = std::chrono::steady_clock::now(); + fence_wait_count_.fetch_add(1, std::memory_order_relaxed); const VkResult wait_result = vkWaitForFences( group.device, static_cast(group.fences.size()), group.fences.data(), VK_FALSE, fence_wait_timeout_ns); + const auto wait_elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - wait_started).count(); + if (wait_elapsed > 0) { + const auto elapsed = static_cast(wait_elapsed); + fence_wait_total_ns_.fetch_add(elapsed, std::memory_order_relaxed); + update_max(fence_wait_max_ns_, elapsed); + } + if (wait_result == VK_TIMEOUT) + fence_wait_timeout_count_.fetch_add(1, std::memory_order_relaxed); for (auto iterator = active.begin(); iterator != active.end();) { VkDevice device{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE}; @@ -315,7 +380,10 @@ void Gpu_Completion_Service::run() noexcept { continue; } VkResult result = wait_result; - if (wait_result == VK_SUCCESS || wait_result == VK_TIMEOUT) result = vkGetFenceStatus(device, fence); + if (wait_result == VK_SUCCESS || wait_result == VK_TIMEOUT) { + fence_probe_count_.fetch_add(1, std::memory_order_relaxed); + result = vkGetFenceStatus(device, fence); + } if (result == VK_NOT_READY) { ++iterator; continue; @@ -346,6 +414,7 @@ void Gpu_Completion_Service::run() noexcept { catch (...) {} if (watched) watched_.fetch_sub(1, std::memory_order_relaxed); + cancellation_count_.fetch_add(1, std::memory_order_relaxed); if (on_exception) { try { on_exception(service_failure); } catch (...) {} @@ -364,6 +433,7 @@ void Gpu_Completion_Service::run() noexcept { catch (...) { break; } fail_pending(pending); } + publish_state(0, 0); } } } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.hpp b/render_3D/render_3D/detail/Gpu_Completion_Service.hpp index 5f3a507..2553417 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.hpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.hpp @@ -1,4 +1,6 @@ #pragma once +#include "../Gpu_Completion_State.hpp" +#include #include #include #include @@ -30,17 +32,6 @@ public: VkResult vulkan_result{VK_SUCCESS}; /* Vulkan 原始结果码。 */ std::uint64_t wait_duration_ns{}; /* fence 等待时间,单位为纳秒。 */ }; - struct Statistics { - std::size_t capacity{}; /* 最大并发 reservation 数。 */ - std::size_t in_flight{}; /* 当前 reservation 数。 */ - std::size_t peak_in_flight{}; /* 历史最大 reservation 数。 */ - std::size_t watched{}; /* 当前受监视 fence 数。 */ - std::size_t peak_watched{}; /* 历史最大受监视 fence 数。 */ - std::uint64_t backpressure_count{}; /* 容量不足累计次数。 */ - std::uint64_t backpressure_wait_ns{}; /* 准入累计等待时间,单位为纳秒。 */ - std::uint64_t fault_count{}; /* Vulkan 或超时故障累计次数。 */ - std::uint64_t abandoned_count{}; /* 被隔离的 fence 累计数。 */ - }; using Completion = std::function; using Exception_Handler = std::function; class Reservation final { @@ -69,7 +60,7 @@ public: [[nodiscard]] Prepare_Result prepare(Completion completion, Exception_Handler on_exception, bool observe); - [[nodiscard]] Statistics statistics() const noexcept; + [[nodiscard]] Gpu_Completion_State state() const noexcept; private: struct Pending_Fence { enum class Status { @@ -90,14 +81,19 @@ private: Gpu_Completion_Service(); ~Gpu_Completion_Service(); static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept; + static void update_max(std::atomic_uint64_t& maximum, + std::uint64_t value) noexcept; static void cancel_reserved(const std::shared_ptr& pending); void acquire_slot(); void release_slot() noexcept; + void publish_state(std::size_t active_fences, + std::size_t pending_fences) noexcept; void wake() noexcept; void run() noexcept; static constexpr std::ptrdiff_t default_capacity = 1024; static constexpr std::uint64_t fence_wait_timeout_ns = 1'000'000; static constexpr std::uint64_t maximum_fence_age_ns = 30'000'000'000ULL; + static constexpr auto state_publication_interval = std::chrono::milliseconds(100); std::counting_semaphore slots_{default_capacity}; /* 有界 reservation 槽位。 */ std::mutex pending_mutex_; /* 保护新登记 fence 队列。 */ std::deque> pending_; /* 完成线程尚未分组的 fence。 */ @@ -108,11 +104,24 @@ private: std::atomic_size_t peak_in_flight_{}; /* 历史最大 reservation 数。 */ std::atomic_size_t watched_{}; /* 当前受监视 fence 数。 */ std::atomic_size_t peak_watched_{}; /* 历史最大受监视 fence 数。 */ + std::atomic_uint64_t reservation_count_{}; + std::atomic_uint64_t completion_count_{}; + std::atomic_uint64_t cancellation_count_{}; + std::atomic_uint64_t fence_probe_count_{}; + std::atomic_uint64_t fence_wait_count_{}; + std::atomic_uint64_t fence_wait_timeout_count_{}; + std::atomic_uint64_t fence_wait_total_ns_{}; + std::atomic_uint64_t fence_wait_max_ns_{}; + std::atomic_uint64_t callback_total_ns_{}; + std::atomic_uint64_t callback_max_ns_{}; + std::atomic_uint64_t callback_failure_count_{}; std::atomic_uint64_t backpressure_count_{}; /* 准入背压累计次数。 */ std::atomic_uint64_t backpressure_wait_ns_{}; /* 准入背压累计纳秒。 */ std::atomic_uint64_t fault_count_{}; /* 完成故障累计次数。 */ std::atomic_uint64_t abandoned_count_{}; /* 放弃 fence 累计次数。 */ std::atomic_bool stopping_{}; /* 服务是否正在停止。 */ + mutable std::mutex state_mutex_; /* 只保护域级 State 的指针交换。 */ + double_buffer::Publish_Double_Buffer state_{}; std::thread thread_; /* 专用 fence 完成线程。 */ }; } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index c6972ef..ba52075 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -1,9 +1,18 @@ #include "Render_Scene_3D.hpp" /* 三维异步提交阶段实现。 */ namespace aethera::render_3d { +Render_Scene_3D::Private::~Private() { + /* 先停止后端产生新回调,再等待已经触发的 completion topology 释放借用帧。 */ + backend.reset(); + std::unique_lock lock(completion_mutex); + completion_condition.wait(lock, [this] { return !completion_running; }); +} bool Render_Scene_3D::Prop::operator==(const Prop&) const = default; bool Render_Scene_3D::State::operator==(const State&) const = default; Render_Scene_3D::Render_Result Render_Scene_3D::render(Frame_3D* frame) { return static_cast(*d).dispatch->render(this, frame); } void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } +tf::Taskflow& Render_Scene_3D::completion_taskflow() { + return static_cast(*d).completion_graph; +} void Render_Scene_3D::activate_view() { static_cast(*d).dispatch->set_active(this, true); } void Render_Scene_3D::deactivate_view() { static_cast(*d).dispatch->set_active(this, false); } } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 6bdb6ac..98e3bc0 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -56,12 +56,19 @@ struct Render_Scene_3D : Def; - /* 无等待提交到调用方拥有的帧;frame 必须存活到完成回调返回。 */ + /* 无等待提交到调用方拥有的帧;必须先安装回调,回调返回前只允许一帧在途。 */ [[nodiscard]] Render_Result render(Frame_3D* frame); - /* 安装完成帧回调;回调由 Datoviz Render Domain 线程传回同一外部帧。 */ + /* 安装完成帧回调;回调传回同一外部帧,返回时 Scene 才释放下一帧准入。 */ void set_frame_callback(Frame_Callback callback); + /* + * 返回 GPU 完成并回读像素后、帧回调前执行的直接 Taskflow。 + * 只能在 Scene 没有运行时修改该图;禁止在执行期间 emplace/erase/clear。 + * completion Taskflow 完成前调用方必须保持本帧像素所有权;节点若启动更晚结束的异步工作, + * 必须捕获 share_pixels() 等拥有像素所有权的对象,禁止只捕获会被下一帧复用的裸 Frame_3D*。 + */ + [[nodiscard]] tf::Taskflow& completion_taskflow(); void activate_view(); void deactivate_view(); }; diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 48f1d07..d1f414a 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -1,6 +1,8 @@ #pragma once #include "../detail/Async_Render_Backend.hpp" #include +#include +#include #include namespace aethera::render_3d { namespace detail { @@ -23,6 +25,13 @@ struct Render_Scene_3D::Private : Prev_Private { Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ Active_Run set_active; /* 修改最终 Scene 的活动属性。 */ }; + std::mutex render_mutex{}; /* 只保护完成回调和单帧准入。 */ + bool frame_in_flight{}; /* render 准入到异步完成回调返回的唯一状态源。 */ + Frame_Callback frame_callback{}; /* Scene 的唯一对外完成出口。 */ + tf::Taskflow completion_graph{}; /* GPU 像素完成后、发布回调前执行的外部续写图。 */ + std::mutex completion_mutex{}; /* 只保护 completion 图拓扑的在途生命周期。 */ + std::condition_variable completion_condition{}; + bool completion_running{}; /* 析构等待异步 completion 图结束的唯一状态源。 */ std::shared_ptr backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */ std::shared_ptr paint_context{}; /* Paint 节点读取 Scene 当前 Prop 的生命周期门闩。 */ Root* camera_component{}; /* Builder 绑定的 Camera 组件。 */ @@ -31,6 +40,7 @@ struct Render_Scene_3D::Private : Prev_Private { std::array (*read_axes)(const Root*){}; /* 读取三轴当前配置。 */ Frame_3D* active_frame{}; /* 当前同步 process 借用的外部帧;提交完成后清空。 */ const Dispatch* dispatch{}; /* 最终 Scene 类型对应的静态公开分派表。 */ + ~Private(); /* Builder 内部初始化后端;必须在绑定 Visual Paint 目标之前调用一次。 */ template void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, @@ -157,7 +167,41 @@ void Render_Scene_3D::Private::initialize_backend( read_axes = axes_reader; const auto initial = parameters(object); backend = std::make_shared(gpu_index, validation_enabled, - std::move(visuals)); + std::move(visuals), initial); + backend->set_frame_callback([this](Frame_3D* frame) { + Frame_Callback callback; + { + std::lock_guard lock(render_mutex); + callback = frame_callback; + } + auto finish = [this, frame, callback = std::move(callback)]() mutable { + frame->mark(Frame_Trace_Marker::callback_started); + if (callback) callback(frame); + frame->mark(Frame_Trace_Marker::callback_finished); + { + std::lock_guard lock(render_mutex); + frame_in_flight = false; + } + { + std::lock_guard lock(completion_mutex); + completion_running = false; + } + completion_condition.notify_all(); + }; + if (completion_graph.empty()) { + finish(); + return; + } + { + std::lock_guard lock(completion_mutex); + completion_running = true; + } + /* + * GPU 完成线程只触发 Taskflow topology,不等待编码/发送节点;最终回调在 topology 完成后执行。 + * 帧准入在 finish() 之前始终有效,因此 completion 图运行期间该外部 Frame 不会被下一帧复用。 + */ + aethera::detail::run_taskflow(completion_graph, std::move(finish)); + }); paint_context = std::make_shared>( detail::Scene_Paint_Context{ backend, object, nullptr, initial, @@ -219,20 +263,21 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ Prev_Private::process(object, frame, [&](const Scene::Private::Result&) { frame->mark(Frame_Trace_Marker::paint_started); const auto prepare = object->template current_dependency_graph(); - prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.get(renderable)->prepare_executed) renderable->template mark_dirty(); }); + prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.pending(renderable)->prepare_executed) renderable->template mark_dirty(); }); const auto submit = object->template current_dependency_graph(); const auto result = submit.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { auto* data = view.private_data(node); if (!data) return; Root* root = node.object; auto* dispatch = data->dispatch; - auto& state = *dispatch->state.get(root); + auto& state = *dispatch->state.pending(root); state.paint_graph_rebuilt = false; state.paint_execution_time_ns = 0; const bool dirty = root->template dirty(); state.paint_dirty = dirty; state.paint_executed = dispatch->paint.predicate(root, dirty); if (!state.paint_executed) return; + const auto paint_started = std::chrono::steady_clock::now(); if (dispatch->paint.builder) { if (!data->paint_graph) data->paint_graph = std::make_unique(); const bool rebuild = dispatch->paint.rebuild_predicate(root); @@ -240,8 +285,13 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ state.paint_task_count = data->paint_graph->num_tasks(); if (!data->paint_graph->empty()) aethera::detail::run_taskflow(*data->paint_graph); } else { state.paint_task_count = dispatch->paint.run ? 1 : 0; if (dispatch->paint.run) dispatch->paint.run(root); } + if (!data->paint_extension.empty()) + aethera::detail::run_taskflow(data->paint_extension); root->template take_dirty(); - dispatch->state.notify(root); + state.paint_execution_time_ns = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - paint_started).count()); + dispatch->state.publish(root); }); if (!result) throw std::logic_error("render scene submit graph became invalid during submission"); if (context->visuals->empty()) @@ -257,6 +307,23 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ template Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, Frame_3D* frame) { if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame"); + { + std::lock_guard lock(render_mutex); + if (!frame_callback) + throw std::logic_error("Render_Scene_3D requires a frame callback before render"); + if (frame_in_flight) return Render_Result::frame_in_flight; + frame_in_flight = true; + } + bool submitted{}; + struct Admission_Scope { + Private& data; + bool& submitted; + ~Admission_Scope() { + if (submitted) return; + std::lock_guard lock(data.render_mutex); + data.frame_in_flight = false; + } + } admission{*this, submitted}; frame->mark(Frame_Trace_Marker::scene_render_requested); struct Active_Frame_Scope { Frame_3D*& target; /* 最终 Private 的同步 process 帧槽位。 */ @@ -264,7 +331,6 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, ~Active_Frame_Scope() { target = previous; } } active_frame_scope{active_frame, active_frame}; active_frame = frame; - bool submitted{}; object->process([&] { submitted = true; frame->mark(Frame_Trace_Marker::scene_render_finished); }); if (submitted) return Render_Result::submitted; const auto& prop = object->template read_prop(); @@ -272,6 +338,6 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, if (prop.viewport.empty()) return Render_Result::empty_viewport; return Render_Result::backend_unavailable; } -template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root, Frame_3D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); if (data.backend) data.backend->set_frame_callback(std::move(callback)); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } +template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root, Frame_3D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); std::lock_guard lock(data.render_mutex); data.frame_callback = std::move(callback); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } template void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } } diff --git a/render_3D/tests/Visual_Tests.cpp b/render_3D/tests/Visual_Tests.cpp index a82d99f..58f314a 100644 --- a/render_3D/tests/Visual_Tests.cpp +++ b/render_3D/tests/Visual_Tests.cpp @@ -67,7 +67,15 @@ TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) { Frame_3D unchanged_frame{Frame_Identity{2, 0}}; Frame_3D exchanged_unchanged_frame{Frame_Identity{3, 0}}; Frame_3D* completed_frame{}; - scene->set_frame_callback([&](Frame_3D* value) { completed_frame = value; frame_ready.store(true, std::memory_order_release); }); + std::atomic_size_t completion_calls{}; + scene->completion_taskflow().emplace([&] { + completion_calls.fetch_add(1, std::memory_order_release); + }).name("test.scene_3d.completion"); + scene->set_frame_callback([&](Frame_3D* value) { + EXPECT_GT(completion_calls.load(std::memory_order_acquire), 0u); + completed_frame = value; + frame_ready.store(true, std::memory_order_release); + }); const auto started = std::chrono::steady_clock::now(); EXPECT_EQ(scene->render(&frame), Render_Scene_3D::Render_Result::submitted); const auto submit_duration = std::chrono::steady_clock::now() - started; @@ -86,6 +94,7 @@ TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) { EXPECT_EQ(scene->render(&exchanged_unchanged_frame), Render_Scene_3D::Render_Result::submitted); for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10)); EXPECT_EQ(completed_frame, &exchanged_unchanged_frame); + EXPECT_EQ(completion_calls.load(std::memory_order_acquire), 3u); EXPECT_TRUE(std::ranges::equal(unchanged_frame.pixels(), exchanged_unchanged_frame.pixels())); } catch (const std::exception& error) { diff --git a/web_server/src/Gallery_Plots_2D.cpp b/web_server/src/Gallery_Plots_2D.cpp index 803defa..5820e81 100644 --- a/web_server/src/Gallery_Plots_2D.cpp +++ b/web_server/src/Gallery_Plots_2D.cpp @@ -477,7 +477,7 @@ void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) { (resize_axis(axes), ...); } } -std::shared_ptr make_axes_plot(asio::any_io_executor executor) { +std::shared_ptr make_axes_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); frequency->template set<&Abs_Axis::Prop::unit_text>("Hz"); @@ -544,9 +544,9 @@ std::shared_ptr make_axes_plot(asio::any_io_executor executor) { {"fields", std::move(generator_fields)}}, std::move(generate)}, std::move(frequency), std::move(numeric), std::move(time)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { +std::shared_ptr make_spectrum_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); @@ -606,9 +606,9 @@ std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { State_Field, State_Field>( *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { +std::shared_ptr make_frequency_trace_plot() { constexpr Size canvas{720, 420}; auto time = make_time_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0); @@ -642,9 +642,9 @@ std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">, Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">>( *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { +std::shared_ptr make_sweep_spectrum_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis( @@ -691,9 +691,9 @@ std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">>( *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { +std::shared_ptr make_afterglow_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis( @@ -735,9 +735,9 @@ std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">, Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">>( *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { +std::shared_ptr make_waterfall_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto time = make_time_axis( @@ -787,9 +787,9 @@ std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">>( *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { +std::shared_ptr make_constellation_plot() { constexpr Size canvas{720, 420}; auto horizontal = make_numeric_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}); @@ -838,9 +838,9 @@ std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">>( *constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } -std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { +std::shared_ptr make_selection_overlay_plot() { constexpr Size canvas{720, 420}; auto horizontal = make_numeric_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}); @@ -866,6 +866,6 @@ std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor "selected_region_count", "Number of rectangular regions currently selected.">>( *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } } diff --git a/web_server/src/Gallery_Plots_2D.hpp b/web_server/src/Gallery_Plots_2D.hpp index 6e6a338..cd9605c 100644 --- a/web_server/src/Gallery_Plots_2D.hpp +++ b/web_server/src/Gallery_Plots_2D.hpp @@ -2,12 +2,12 @@ #include "Plot.hpp" namespace aethera::web { -[[nodiscard]] std::shared_ptr make_spectrum_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_axes_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_afterglow_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_waterfall_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_constellation_plot(asio::any_io_executor executor); -[[nodiscard]] std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor); +[[nodiscard]] std::shared_ptr make_spectrum_plot(); +[[nodiscard]] std::shared_ptr make_axes_plot(); +[[nodiscard]] std::shared_ptr make_frequency_trace_plot(); +[[nodiscard]] std::shared_ptr make_sweep_spectrum_plot(); +[[nodiscard]] std::shared_ptr make_afterglow_plot(); +[[nodiscard]] std::shared_ptr make_waterfall_plot(); +[[nodiscard]] std::shared_ptr make_constellation_plot(); +[[nodiscard]] std::shared_ptr make_selection_overlay_plot(); } diff --git a/web_server/src/Gallery_Plots_3D.cpp b/web_server/src/Gallery_Plots_3D.cpp index a5dd0ea..d07bfdb 100644 --- a/web_server/src/Gallery_Plots_3D.cpp +++ b/web_server/src/Gallery_Plots_3D.cpp @@ -375,7 +375,7 @@ struct Scene_Components_3D { template -std::shared_ptr make_visual_plot(asio::any_io_executor executor, std::string label, +std::shared_ptr make_visual_plot(std::string label, Build_Result build_result, Scene_Components_3D components = {}, Data_Generator data_generator = {}, @@ -421,7 +421,7 @@ std::shared_ptr make_visual_plot(asio::any_io_executor executor, std::stri auto view = std::make_unique>( *scene, std::move(camera), std::move(axes), std::move(visual), std::move(label), std::move(data_generator), std::move(markers)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); + return std::make_shared(std::move(scene), std::move(view)); } Color color(std::uint8_t red, std::uint8_t green, std::uint8_t blue, std::uint8_t alpha = 255) { @@ -656,55 +656,55 @@ struct Spectrogram_Data_Generator { }; } -std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Point Visual", Impl::Builder{} +std::shared_ptr make_datoviz_point_plot() { + return make_visual_plot("Point Visual", Impl::Builder{} .set(&Point_Visual::Prop::items, std::vector{{{-0.65F, -0.25F, 0.05F}, color(255, 91, 110), 28.0F}, {{0.0F, 0.58F, 0.25F}, color(82, 226, 190), 34.0F}, {{0.62F, -0.12F, -0.15F}, color(75, 145, 255), 30.0F}}).build()); } -std::shared_ptr make_datoviz_splat_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Splat Visual", Impl::Builder{} +std::shared_ptr make_datoviz_splat_plot() { + return make_visual_plot("Splat Visual", Impl::Builder{} .set(&Splat_Visual::Prop::items, std::vector{{{-0.48F, 0.0F, 0.1F}, color(255, 98, 115, 210), {0.18F, 0.08F}, 0.45F}, {{0.28F, 0.15F, 0.0F}, color(66, 218, 188, 210), {0.12F, 0.22F}, -0.3F}, {{0.15F, -0.38F, 0.2F}, color(76, 132, 255, 210), {0.2F, 0.1F}, 0.9F}}).build()); } -std::shared_ptr make_datoviz_pixel_plot(asio::any_io_executor executor) { +std::shared_ptr make_datoviz_pixel_plot() { std::vector pixels; for (int y = -8; y <= 8; ++y) for (int x = -12; x <= 12; ++x) pixels.push_back({{x / 13.0F, y / 9.0F, 0.12F * std::sin(x * .45F) * std::cos(y * .35F)}, color(static_cast(90 + 6 * (x + 12)), static_cast(100 + 8 * (y + 8)), 230), 5.0F}); - return make_visual_plot(std::move(executor), "Pixel Visual", Impl::Builder{}.set(&Pixel_Visual::Prop::items, std::move(pixels)).build()); + return make_visual_plot("Pixel Visual", Impl::Builder{}.set(&Pixel_Visual::Prop::items, std::move(pixels)).build()); } -std::shared_ptr make_datoviz_marker_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Marker Visual", Impl::Builder{} +std::shared_ptr make_datoviz_marker_plot() { + return make_visual_plot("Marker Visual", Impl::Builder{} .set(&Marker_Visual::Prop::items, std::vector{{{-0.7F, 0.0F, 0.0F}, color(255, 93, 115), 34.0F, 0.0F, Marker_Shape::disc}, {{-0.35F, 0.25F, 0.1F}, color(91, 226, 193), 36.0F, 0.25F, Marker_Shape::square}, {{0.0F, -0.2F, 0.2F}, color(100, 158, 255), 38.0F, 0.5F, Marker_Shape::triangle}, {{0.35F, 0.25F, 0.1F}, color(250, 195, 92), 40.0F, 0.75F, Marker_Shape::diamond}, {{0.7F, 0.0F, 0.0F}, color(201, 132, 255), 42.0F, 1.0F, Marker_Shape::cross}}).build()); } -std::shared_ptr make_datoviz_sphere_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Sphere Visual", Impl::Builder{} +std::shared_ptr make_datoviz_sphere_plot() { + return make_visual_plot("Sphere Visual", Impl::Builder{} .set(&Sphere_Visual::Prop::items, std::vector{{{-0.48F, -0.2F, 0.0F}, color(255, 91, 110), 0.28F}, {{0.08F, 0.25F, 0.18F}, color(82, 226, 190), 0.36F}, {{0.55F, -0.18F, -0.12F}, color(75, 145, 255), 0.24F}}).build()); } -std::shared_ptr make_datoviz_segment_plot(asio::any_io_executor executor) { +std::shared_ptr make_datoviz_segment_plot() { std::vector segments; for (int index = 0; index < 12; ++index) { const float angle = static_cast(index) * std::numbers::pi_v / 6.0F; segments.push_back({{0.0F, 0.0F, 0.0F}, {0.82F * std::cos(angle), 0.82F * std::sin(angle), 0.18F * std::sin(2 * angle)}, color(static_cast(80 + index * 13), static_cast(220 - index * 8), 240), 4.0F}); } - return make_visual_plot(std::move(executor), "Segment Visual", Impl::Builder{}.set(&Segment_Visual::Prop::items, std::move(segments)).build()); + return make_visual_plot("Segment Visual", Impl::Builder{}.set(&Segment_Visual::Prop::items, std::move(segments)).build()); } -std::shared_ptr make_datoviz_vector_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Vector Visual", Impl::Builder{} +std::shared_ptr make_datoviz_vector_plot() { + return make_visual_plot("Vector Visual", Impl::Builder{} .set(&Vector_Visual::Prop::items, std::vector{{{-0.55F, -0.35F, 0.0F}, {0.55F, 0.2F, 0.25F}, color(255, 98, 115), 4.0F}, {{-0.1F, 0.0F, 0.0F}, {0.2F, 0.62F, 0.18F}, color(80, 225, 190), 5.0F}, {{0.35F, -0.25F, 0.0F}, {-0.12F, 0.25F, 0.65F}, color(78, 145, 255), 4.0F}}).build()); } -std::shared_ptr make_datoviz_primitive_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Primitive Visual", Impl::Builder{} +std::shared_ptr make_datoviz_primitive_plot() { + return make_visual_plot("Primitive Visual", Impl::Builder{} .set(&Primitive_Visual::Prop::topology, Primitive_Topology::triangle_list) .set(&Primitive_Visual::Prop::items, std::vector{{{-0.72F, -0.55F, 0.0F}, color(255, 86, 110), {0, 0, 1}}, {{0.72F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}}, {{0.0F, 0.72F, 0.25F}, color(82, 226, 190), {0, 0, 1}}}).build()); } -std::shared_ptr make_datoviz_mesh_plot(asio::any_io_executor executor) { +std::shared_ptr make_datoviz_mesh_plot() { const std::vector mesh{{{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}, {1, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, 0.55F, 0.0F}, color(244, 190, 86), {0, 0, 1}, {0, 1}}}; - return make_visual_plot(std::move(executor), "Mesh Visual", Impl::Builder{}.set(&Mesh_Visual::Prop::items, mesh).build()); + return make_visual_plot("Mesh Visual", Impl::Builder{}.set(&Mesh_Visual::Prop::items, mesh).build()); } -std::shared_ptr make_datoviz_spectrogram_plot(asio::any_io_executor executor) { +std::shared_ptr make_datoviz_spectrogram_plot() { const Spectrogram_Parameters parameters; auto marker_result = Impl::Builder{} .set(&Marker_Visual::Prop::items, std::vector{ @@ -734,46 +734,46 @@ std::shared_ptr make_datoviz_spectrogram_plot(asio::any_io_executor execut parameters.maximum_level_db}, plot::Axis_Scale::linear, "SPL", "dB", 7, 0, true, true, true}}; return make_visual_plot( - std::move(executor), "3D Spectrogram", + "3D Spectrogram", Impl::Builder{} .set(&Mesh_Visual::Prop::items, spectrogram_mesh(parameters)) .build(), std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers)); } -std::shared_ptr make_datoviz_path_plot(asio::any_io_executor executor) { +std::shared_ptr make_datoviz_path_plot() { std::vector path; for (int index = 0; index < 64; ++index) { const float t = static_cast(index) / 63.0F; path.push_back({{-0.9F + 1.8F * t, 0.48F * std::sin(t * 4.0F * std::numbers::pi_v), 0.25F * std::cos(t * 2.0F * std::numbers::pi_v)}, color(static_cast(70 + 170 * t), static_cast(220 - 80 * t), 245), 5.0F}); } - return make_visual_plot(std::move(executor), "Path Visual", Impl::Builder{}.set(&Path_Visual::Prop::items, std::move(path)).build()); + return make_visual_plot("Path Visual", Impl::Builder{}.set(&Path_Visual::Prop::items, std::move(path)).build()); } -std::shared_ptr make_datoviz_image_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Image Visual", Impl::Builder{} +std::shared_ptr make_datoviz_image_plot() { + return make_visual_plot("Image Visual", Impl::Builder{} .set(&Image_Visual::Prop::field_width, 32U).set(&Image_Visual::Prop::field_height, 32U) .set(&Image_Visual::Prop::items, std::vector{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}, color(255, 255, 255)}}).build()); } -std::shared_ptr make_datoviz_labels_plot(asio::any_io_executor executor) { - return make_visual_plot(std::move(executor), "Labels Visual", Impl::Builder{} +std::shared_ptr make_datoviz_labels_plot() { + return make_visual_plot("Labels Visual", Impl::Builder{} .set(&Labels_Visual::Prop::field_width, 8U).set(&Labels_Visual::Prop::field_height, 8U) .set(&Labels_Visual::Prop::items, std::vector