From 05a79fc3519b4bae3dd5ebb3c273b91aa8ed1cb9 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 29 Aug 2026 03:03:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/src/kernel/Taskflow_Frame_Access.hpp | 5 +- kernel/src/kernel/frame.cpp | 11 +- kernel/src/kernel/frame.hpp | 7 + kernel/src/kernel/frame_statistics.hpp | 1 + kernel/src/kernel/render_common.cpp | 72 ++- kernel/src/kernel/render_common.hpp | 6 +- kernel/src/test/render_test.cpp | 29 +- mcp/core/runtime/Plot.cpp | 7 + mcp/tests/Control_Path_Benchmarks.cpp | 596 ++++++++++++------ render_2D/render_2D/render/Blend2D_Cache.cpp | 21 +- render_3D/render_3D/scene/Render_Scene_3D.ipp | 154 +++-- .../scene/detail/Datoviz_Scene_Common.ipp | 95 ++- .../scene/detail/Datoviz_Scene_Resources.ipp | 1 + .../datoviz/include/datoviz/drp2/runtime.h | 22 +- .../datoviz/include/datoviz/vklite/commands.h | 24 + .../third_party/datoviz/src/drp2/_runtime.h | 4 +- .../third_party/datoviz/src/drp2/backend.c | 11 +- render_3D/third_party/datoviz/src/drp2/pass.c | 9 +- .../third_party/datoviz/src/drp2/runtime.c | 23 +- .../third_party/datoviz/src/drp2/transfer.c | 15 +- .../third_party/datoviz/src/vklite/commands.c | 47 +- webapp_gallery/src/app.tsx | 11 +- webapp_gallery/src/styles.css | 1 + 23 files changed, 840 insertions(+), 332 deletions(-) diff --git a/kernel/src/kernel/Taskflow_Frame_Access.hpp b/kernel/src/kernel/Taskflow_Frame_Access.hpp index e94b6d2..30d3964 100644 --- a/kernel/src/kernel/Taskflow_Frame_Access.hpp +++ b/kernel/src/kernel/Taskflow_Frame_Access.hpp @@ -3,6 +3,7 @@ #include "Task_Graph.hpp" #include "ownership.hpp" #include +#include #include namespace aethera::detail { @@ -24,7 +25,9 @@ struct Taskflow_Frame_Access { Render_Frame& frame, std::size_t worker, std::uint64_t native_id, std::size_t queue_size, std::size_t queue_capacity, Clock::time_point entered, Clock::time_point started, - Clock::time_point finished, std::uint64_t cooperative_wait_ns); + Clock::time_point finished, std::uint64_t cooperative_wait_ns, + std::span> + cooperative_waits); static void finish_task_observer( Render_Frame& frame, std::size_t worker, std::size_t task, Clock::time_point completed) noexcept; diff --git a/kernel/src/kernel/frame.cpp b/kernel/src/kernel/frame.cpp index e99794d..2d64591 100644 --- a/kernel/src/kernel/frame.cpp +++ b/kernel/src/kernel/frame.cpp @@ -123,6 +123,7 @@ Frame_Statistics_Sample Render_Frame::statistics() const { Frame_Statistic::backend_apply_ms, Frame_Statistic::backend_plan_ms, Frame_Statistic::backend_execute_ms, + Frame_Statistic::backend_submit_queue_ms, Frame_Statistic::backend_submit_ms, Frame_Statistic::gpu_completion_observation_ms, Frame_Statistic::completion_task_queue_ms, @@ -135,6 +136,7 @@ Frame_Statistics_Sample Render_Frame::statistics() const { Frame_Trace_Measurement::backend_apply_ns, Frame_Trace_Measurement::backend_plan_ns, Frame_Trace_Measurement::backend_execute_ns, + Frame_Trace_Measurement::backend_submit_queue_ns, Frame_Trace_Measurement::backend_submit_ns, Frame_Trace_Measurement::gpu_completion_observation_ns, Frame_Trace_Measurement::completion_task_queue_ns, @@ -363,7 +365,9 @@ std::size_t detail::Taskflow_Frame_Access::append_task( Render_Frame& frame, std::size_t worker, std::uint64_t native_id, std::size_t queue_size, std::size_t queue_capacity, Clock::time_point entered, Clock::time_point started, - Clock::time_point finished, std::uint64_t cooperative_wait_ns) { + Clock::time_point finished, std::uint64_t cooperative_wait_ns, + std::span> + cooperative_waits) { auto& data = *frame.d; if (worker >= data.taskflow_workers.size()) return std::numeric_limits::max(); @@ -381,6 +385,11 @@ std::size_t detail::Taskflow_Frame_Access::append_task( trace.completed_ms = trace.finished_ms; trace.duration_ms = std::max(0.0, trace.finished_ms - trace.started_ms); trace.cooperative_wait_ms = static_cast(cooperative_wait_ns) / 1'000'000.0; + trace.cooperative_waits.reserve(cooperative_waits.size()); + for (const auto& [wait_started, wait_finished] : cooperative_waits) { + trace.cooperative_waits.push_back({ + elapsed_ms(wait_started), elapsed_ms(wait_finished)}); + } trace.observer_entry_ms = std::max(0.0, trace.started_ms - trace.entered_ms); data.taskflow_workers[worker].tasks.push_back(std::move(trace)); return data.taskflow_workers[worker].tasks.size() - 1; diff --git a/kernel/src/kernel/frame.hpp b/kernel/src/kernel/frame.hpp index cfeaf8d..bacef52 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace aethera { namespace detail { @@ -53,6 +54,7 @@ enum struct Frame_Trace_Measurement : std::uint8_t { backend_apply_ns, backend_plan_ns, backend_execute_ns, + backend_submit_queue_ns, backend_submit_ns, gpu_completion_observation_ns, completion_task_queue_ns, @@ -99,6 +101,10 @@ struct Taskflow_Graph_Trace { bool completed{}; /* 对应 topology 是否已经结束。 */ }; struct Taskflow_Task_Trace { + struct Cooperative_Wait { + double started_ms{}; /* 相对帧创建时刻的协作让出起点。 */ + double finished_ms{}; /* 相对帧创建时刻的协作让出终点。 */ + }; std::uint64_t native_id{}; /* TaskView::hash_value() 返回的原生 Node 身份。 */ std::size_t worker_id{}; /* 执行该任务的 Executor worker。 */ std::size_t worker_queue_size{}; /* on_entry 时的原生 worker queue_size。 */ @@ -109,6 +115,7 @@ struct Taskflow_Task_Trace { double completed_ms{}; /* Observer on_exit 与按帧追踪写入全部结束的时间。 */ double duration_ms{}; /* 仅任务体 started 到 finished 的持续时间。 */ double cooperative_wait_ms{}; /* Task_Graph::corun/corun_until 主动让出 Worker 的墙钟时间。 */ + std::vector cooperative_waits{}; /* 时间线中可直接覆盖到任务体上的让出区间。 */ double observer_entry_ms{}; /* on_entry 诊断本身的耗时。 */ double observer_exit_ms{}; /* on_exit 诊断与按帧追踪写入的耗时。 */ double ready_ms{}; /* 前驱完成或根 run 提交后的估算就绪时间。 */ diff --git a/kernel/src/kernel/frame_statistics.hpp b/kernel/src/kernel/frame_statistics.hpp index b7e74c9..0ef8b2c 100644 --- a/kernel/src/kernel/frame_statistics.hpp +++ b/kernel/src/kernel/frame_statistics.hpp @@ -19,6 +19,7 @@ enum struct Frame_Statistic : std::uint8_t { backend_apply_ms, backend_plan_ms, backend_execute_ms, + backend_submit_queue_ms, backend_submit_ms, gpu_completion_observation_ms, completion_task_queue_ms, diff --git a/kernel/src/kernel/render_common.cpp b/kernel/src/kernel/render_common.cpp index 95d6702..9a5b18f 100644 --- a/kernel/src/kernel/render_common.cpp +++ b/kernel/src/kernel/render_common.cpp @@ -115,12 +115,15 @@ private: std::atomic longest_task_type{tf::TaskType::UNDEFINED}; }; struct Start_Record { + using Wait_Record = std::pair; Clock::time_point entered{}; /* Observer on_entry 进入时间。 */ Clock::time_point started{}; /* on_entry 完成、任务体即将执行的时间。 */ Clock::time_point segment_started{}; /* 当前连续独占 Worker 片段的起点。 */ - std::uint64_t maximum_segment_ns{}; /* 已结束连续独占片段的最大墙钟。 */ + std::uint64_t active_time_ns{}; /* 已结束独占 Worker 片段的累计墙钟。 */ Clock::time_point cooperative_wait_started{}; /* 主动 corun 让出 Worker 的墙钟起点。 */ std::uint64_t cooperative_wait_ns{}; /* 已累计 cooperative wait 墙钟。 */ + std::array cooperative_waits{}; /* 按帧时间线的固定容量让出区间。 */ + std::size_t cooperative_wait_count{}; Render_Frame* frame{}; /* 进入任务时唯一活动的按帧捕获。 */ std::size_t queue_size{}; /* 进入任务时 worker 队列深度。 */ std::size_t queue_capacity{}; /* 进入任务时 worker 队列容量。 */ @@ -129,7 +132,6 @@ private: bool cooperatively_suspended{}; /* 外层任务是否主动让出 Worker 执行子图。 */ }; std::vector> starts; - std::vector worker_busy_starts; std::unique_ptr worker_statistics; std::size_t worker_statistics_count{}; std::uint64_t worker_occupation_limit_ns{}; /* 单节点连续非 CPU 等待 Worker 的上限。 */ @@ -248,12 +250,17 @@ private: const auto elapsed = static_cast( std::chrono::duration_cast( now - active.segment_started).count()); - active.maximum_segment_ns = std::max( - active.maximum_segment_ns, elapsed); + active.active_time_ns += elapsed; } active.segment_started = {}; - if (active.cooperative_wait_started == Clock::time_point{}) + const bool begins_wait = + active.cooperative_wait_started == Clock::time_point{}; + if (begins_wait) active.cooperative_wait_started = now; + if (begins_wait && active.frame != nullptr && + active.cooperative_wait_count < active.cooperative_waits.size()) { + active.cooperative_waits[active.cooperative_wait_count++] = {now, {}}; + } active.cooperatively_suspended = true; worker_statistics[worker].active_segment_started_ns.store( 0, std::memory_order_release); @@ -273,6 +280,11 @@ private: std::chrono::duration_cast( now - active.cooperative_wait_started).count()); active.cooperative_wait_started = {}; + if (active.cooperative_wait_count != 0) { + auto& wait = active.cooperative_waits[ + active.cooperative_wait_count - 1]; + if (wait.second == Clock::time_point{}) wait.second = now; + } } active.cooperatively_suspended = false; active.segment_started = now; @@ -309,7 +321,6 @@ public: } void set_up(std::size_t workers) override { starts.resize(workers); - worker_busy_starts.resize(workers); worker_statistics = std::make_unique(workers); worker_statistics_count = workers; for (auto& worker : starts) worker.reserve(32); @@ -343,12 +354,10 @@ public: const auto elapsed = static_cast( std::chrono::duration_cast( now - parent.segment_started).count()); - parent.maximum_segment_ns = std::max( - parent.maximum_segment_ns, elapsed); + parent.active_time_ns += elapsed; parent.segment_started = {}; } } - if (worker_starts.empty()) worker_busy_starts[worker.id()] = now; const auto queue_size = worker.queue_size(); const auto queue_capacity = worker.queue_capacity(); Start_Record record{}; @@ -392,6 +401,11 @@ public: std::chrono::duration_cast( finished - active.cooperative_wait_started).count()); active.cooperative_wait_started = {}; + if (active.cooperative_wait_count != 0) { + auto& wait = active.cooperative_waits[ + active.cooperative_wait_count - 1]; + if (wait.second == Clock::time_point{}) wait.second = finished; + } } auto start = active; worker_starts.pop_back(); @@ -402,28 +416,32 @@ public: const auto segment = static_cast( std::chrono::duration_cast( finished - start.segment_started).count()); - start.maximum_segment_ns = std::max( - start.maximum_segment_ns, segment); + start.active_time_ns += segment; } + const auto occupied = elapsed >= start.cooperative_wait_ns + ? elapsed - start.cooperative_wait_ns + : 0; auto& worker_state = worker_statistics[worker.id()]; worker_state.active_task_reported.store(true, std::memory_order_release); worker_state.task_count.fetch_add(1, std::memory_order_relaxed); - worker_state.task_time_ns.fetch_add(elapsed, std::memory_order_relaxed); - update_min(worker_state.min_task_time_ns, elapsed); - update_max(worker_state.max_task_time_ns, elapsed); + worker_state.task_time_ns.fetch_add(occupied, std::memory_order_relaxed); + worker_state.busy_time_ns.fetch_add( + start.active_time_ns, std::memory_order_relaxed); + update_min(worker_state.min_task_time_ns, occupied); + update_max(worker_state.max_task_time_ns, occupied); auto type_index = task_type_index(task.type()); if (type_index < worker_state.task_types.size()) { auto& type = worker_state.task_types[type_index]; type.count.fetch_add(1, std::memory_order_relaxed); - type.total_time_ns.fetch_add(elapsed, std::memory_order_relaxed); - update_min(type.min_time_ns, elapsed); - update_max(type.max_time_ns, elapsed); + type.total_time_ns.fetch_add(occupied, std::memory_order_relaxed); + update_min(type.min_time_ns, occupied); + update_max(type.max_time_ns, occupied); } auto longest = worker_state.longest_task_time_ns.load( std::memory_order_relaxed); - if (longest < elapsed && worker_state.longest_task_time_ns.compare_exchange_strong( - longest, elapsed, std::memory_order_relaxed)) { + if (longest < occupied && worker_state.longest_task_time_ns.compare_exchange_strong( + longest, occupied, std::memory_order_relaxed)) { worker_state.longest_task_hash.store(task.hash_value(), std::memory_order_relaxed); worker_state.longest_task_type.store(task.type(), std::memory_order_relaxed); } @@ -435,7 +453,9 @@ public: *start.frame, worker.id(), static_cast(task.hash_value()), start.queue_size, start.queue_capacity, start.entered, - start.started, finished, start.cooperative_wait_ns); + start.started, finished, start.cooperative_wait_ns, + std::span{start.cooperative_waits}.first( + start.cooperative_wait_count)); } catch (...) { /* Observer 不能让按需诊断分配失败改变渲染任务的完成语义。 */ @@ -448,10 +468,6 @@ public: *start.frame, worker.id(), *trace_task, completed); } if (worker_starts.empty()) { - auto busy = static_cast( - std::chrono::duration_cast( - completed - worker_busy_starts[worker.id()]).count()); - worker_state.busy_time_ns.fetch_add(busy, std::memory_order_relaxed); worker_state.active_task_hash.store(0, std::memory_order_relaxed); worker_state.active_task_started_ns.store(0, std::memory_order_relaxed); worker_state.active_segment_started_ns.store(0, @@ -547,8 +563,10 @@ public: target.max_observed_queue_capacity = source.max_queue_capacity.load(std::memory_order_relaxed); target.active_task_hash = source.active_task_hash.load(std::memory_order_relaxed); const auto active_started = source.active_task_started_ns.load(std::memory_order_relaxed); - target.active_task_time_ns = active_started && read_time_ns >= active_started - ? read_time_ns - active_started + const auto active_segment = source.active_segment_started_ns.load( + std::memory_order_acquire); + target.active_task_time_ns = active_segment && read_time_ns >= active_segment + ? read_time_ns - active_segment : 0; const auto active_type = source.active_task_type.load(std::memory_order_relaxed); target.active_task_type = active_started @@ -584,7 +602,7 @@ public: state.active_task_count += active_depth; state.peak_active_task_count += source.peak_active_depth.load( std::memory_order_relaxed); - state.active_worker_count += active_depth != 0; + state.active_worker_count += active_segment != 0; state.observed_task_count += target.task_count; state.named_task_count += source.named_task_count.load( std::memory_order_relaxed); diff --git a/kernel/src/kernel/render_common.hpp b/kernel/src/kernel/render_common.hpp index 4e8e849..7657df2 100644 --- a/kernel/src/kernel/render_common.hpp +++ b/kernel/src/kernel/render_common.hpp @@ -48,10 +48,10 @@ struct Task_Worker_State { std::size_t peak_observed_queue_size{}; std::size_t max_observed_queue_capacity{}; std::uint64_t active_task_hash{}; /* 当前最内层原生任务身份;空闲时为 0。 */ - std::uint64_t active_task_time_ns{}; /* 当前任务从 on_entry 到本次读取已经持续的时间。 */ + std::uint64_t active_task_time_ns{}; /* 当前任务连续占用 Worker 片段已经持续的时间;协作让出时为 0。 */ std::string active_task_type{}; /* 当前任务的 Taskflow 原生 TaskType;空闲时为空。 */ - std::uint64_t task_time_ns{}; - std::uint64_t busy_time_ns{}; + std::uint64_t task_time_ns{}; /* 已完成任务体墙钟之和,不含 cooperative wait。 */ + std::uint64_t busy_time_ns{}; /* Worker 实际执行任务体片段的墙钟并集,不含 cooperative wait。 */ std::uint64_t cpu_time_ns{}; /* Worker最外层任务活跃区间内累计的线程 CPU 时间。 */ std::uint64_t non_cpu_time_ns{}; /* busy_time_ns 减去 cpu_time_ns;只表示未计费墙钟,不推断锁或抢占。 */ std::uint64_t idle_time_ns{}; diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index 7baccac..af8becd 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -188,6 +188,7 @@ TEST(scene_condition, render_order_does_not_create_a_false_data_dependency) { TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity) { aethera::initialize_runtime({.workers = 2}); std::atomic_int completed{}; + std::atomic_bool cooperative_ready{}; aethera::Task_Graph child{"test.visual"}; child.describe("owner_kind", "renderable") .describe("owner_component", "spectrum"); @@ -196,8 +197,18 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity) prepare.precede(paint); aethera::Task_Graph frame_graph{"test.frame"}; auto module_task = frame_graph.compose("spectrum", child); + auto cooperative = frame_graph.add("wait.cooperative", [&] { + aethera::schedule_task("test.cooperative.release", [&] { + cooperative_ready.store(true, std::memory_order_release); + }); + aethera::Task_Graph::corun_until([&] { + return cooperative_ready.load(std::memory_order_acquire); + }); + completed.fetch_add(1); + }); auto publish = frame_graph.add("publish.state", [&] { completed.fetch_add(1); }); - module_task.precede(publish); + module_task.precede(cooperative); + cooperative.precede(publish); aethera::Render_Frame frame{{41, 73}}; frame.mark(aethera::Frame_Trace_Marker::paint_started); @@ -210,7 +221,7 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity) aethera::detail::run_taskflow(frame_graph, frame, "test.scene.paint"); aethera::detail::finish_taskflow_trace(frame); - EXPECT_EQ(completed.load(), 3); + EXPECT_EQ(completed.load(), 4); const auto trace = frame.take_taskflow_trace(); ASSERT_EQ(trace.graphs.size(), 1u); EXPECT_EQ(trace.identity, (aethera::Frame_Identity{41, 73})); @@ -228,7 +239,7 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity) EXPECT_TRUE(trace.graphs.front().completed); EXPECT_EQ(std::ranges::count_if(trace.graphs.front().nodes, [](const auto& node) { return node.type != "diagnostic"; - }), 4); + }), 5); EXPECT_EQ(std::ranges::count_if(trace.graphs.front().nodes, [](const auto& node) { return node.type == "diagnostic"; }), 5); @@ -248,6 +259,18 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity) EXPECT_TRUE(std::ranges::contains( child_prepare->attributes, std::pair{"owner_component", "spectrum"})); + const auto cooperative_node = std::ranges::find( + trace.graphs.front().nodes, "wait.cooperative", + &aethera::Taskflow_Graph_Trace::Node::name); + ASSERT_NE(cooperative_node, trace.graphs.front().nodes.end()); + const auto cooperative_execution = std::ranges::find( + trace.tasks, cooperative_node->native_id, + &aethera::Taskflow_Task_Trace::native_id); + ASSERT_NE(cooperative_execution, trace.tasks.end()); + ASSERT_EQ(cooperative_execution->cooperative_waits.size(), 1u); + EXPECT_GE(cooperative_execution->cooperative_wait_ms, 0.0); + EXPECT_GE(cooperative_execution->cooperative_waits.front().finished_ms, + cooperative_execution->cooperative_waits.front().started_ms); std::unordered_set metadata_ids; for (const auto& node : trace.graphs.front().nodes) diff --git a/mcp/core/runtime/Plot.cpp b/mcp/core/runtime/Plot.cpp index e635f19..b53b5c4 100644 --- a/mcp/core/runtime/Plot.cpp +++ b/mcp/core/runtime/Plot.cpp @@ -458,6 +458,12 @@ nlohmann::json taskflow_trace_json( nlohmann::json executions = nlohmann::json::array(); for (const auto& task : trace.tasks) { const auto found = node_ids.find(task.native_id); + nlohmann::json cooperative_waits = nlohmann::json::array(); + for (const auto& wait : task.cooperative_waits) { + cooperative_waits.push_back({ + {"started_ms", wait.started_ms}, + {"finished_ms", wait.finished_ms}}); + } executions.push_back({ {"native_id", std::to_string(task.native_id)}, {"node_id", found == node_ids.end() ? std::string{} : found->second}, @@ -469,6 +475,7 @@ nlohmann::json taskflow_trace_json( {"completed_ms", task.completed_ms}, {"duration_ms", task.duration_ms}, {"cooperative_wait_ms", task.cooperative_wait_ms}, + {"cooperative_waits", std::move(cooperative_waits)}, {"observer_entry_ms", task.observer_entry_ms}, {"observer_exit_ms", task.observer_exit_ms}, {"queue_wait_ms", task.queue_wait_ms} diff --git a/mcp/tests/Control_Path_Benchmarks.cpp b/mcp/tests/Control_Path_Benchmarks.cpp index 9b6e5db..d5cedb8 100644 --- a/mcp/tests/Control_Path_Benchmarks.cpp +++ b/mcp/tests/Control_Path_Benchmarks.cpp @@ -1,80 +1,27 @@ #include #include #include +#include #include #include #include -#include #include +#include #include #include +#include +#include #include #include #include +#include +#include #include #include namespace aethera::mcp::benchmarks { namespace { -[[nodiscard]] Plot_Input_Request timestamped_drag_request( - Event_Type type = Event_Type::pointer_move) { - Plot_Input_Request request; - request.plot = "datoviz_point"; - request.event.type = type; - request.event.time_milliseconds = 1'000.0; - request.event.position = {240.0, 180.0}; - request.event.global_position = {240.0, 180.0}; - request.event.button = Mouse_Button::left; - request.event.buttons = 1; - return request; -} - -void decode_timestamped_drag(benchmark::State& state) { - const auto encoded = encode_protocol_value(timestamped_drag_request()); - for ([[maybe_unused]] auto iteration : state) { - Plot_Input_Request decoded; - decode_protocol_value(decoded, encoded); - benchmark::DoNotOptimize(decoded.event.time_milliseconds); - benchmark::ClobberMemory(); - } - state.SetItemsProcessed(state.iterations()); -} - -void decode_timed_render(benchmark::State& state) { - const auto encoded = encode_protocol_value( - Plot_Render_Request{"datoviz_point", 1'000.0, 720, 420}); - for ([[maybe_unused]] auto iteration : state) { - Plot_Render_Request decoded; - decode_protocol_value(decoded, encoded); - benchmark::DoNotOptimize(decoded.time_milliseconds); - benchmark::ClobberMemory(); - } - state.SetItemsProcessed(state.iterations()); -} - -class Control_Path : public benchmark::Fixture { -public: - void SetUp(const benchmark::State&) override { - service = Control_Service::create(); - } - - void TearDown(const benchmark::State&) override { - service.reset(); - } - -protected: - std::shared_ptr service; -}; - -constexpr std::array plot_3d_ids{ - "datoviz_point", "datoviz_splat", "datoviz_pixel", "datoviz_marker", - "datoviz_sphere", "datoviz_segment", "datoviz_vector", - "datoviz_primitive", "datoviz_mesh", "datoviz_spectrogram", - "datoviz_path", "datoviz_image", "datoviz_labels", "datoviz_glyph", - "datoviz_text", "datoviz_volume"}; -constexpr std::size_t splat_plot_index{1}; - enum struct Input_Workload : std::uint8_t { steady, drag, @@ -82,17 +29,156 @@ enum struct Input_Workload : std::uint8_t { mixed }; +struct Benchmark_Configuration { + std::vector plot_ids{}; /* 本进程并发打开的 Gallery Plot ID,直接引用 Gallery 权威定义。 */ + Input_Workload input{Input_Workload::mixed}; /* 本进程向所选图提交的交互负载。 */ + std::uint32_t width{720}; /* 每个输出流的像素宽度。 */ + std::uint32_t height{420}; /* 每个输出流的像素高度。 */ + std::uint32_t input_rate_hz{120}; /* 输入批次目标频率;零表示 steady。 */ + double duration_seconds{10.0}; /* 单次测量的明确墙钟时长。 */ +}; + +enum struct Parse_Benchmark_Arguments_Result : std::uint8_t { + configured, + help_requested, + invalid_argument +}; + +Benchmark_Configuration configuration{}; +std::string configuration_error{}; + +[[nodiscard]] bool append_plot_id(std::string_view id) { + if (std::ranges::find(configuration.plot_ids, id) != + configuration.plot_ids.end()) return true; + if (!web::find_gallery_plot_definition(id)) return false; + configuration.plot_ids.push_back(id); + return true; +} + +void append_dimension(web::Plot_Dimension dimension) { + for (const auto& definition : web::gallery_plot_definitions()) + if (definition.dimension == dimension) + static_cast(append_plot_id(definition.id)); +} + +[[nodiscard]] bool select_plots(std::string_view specification) { + configuration.plot_ids.clear(); + while (!specification.empty()) { + const auto separator = specification.find(','); + const auto token = specification.substr(0, separator); + if (token == "2d") append_dimension(web::Plot_Dimension::two_d); + else if (token == "3d") append_dimension(web::Plot_Dimension::three_d); + else if (token == "all") { + append_dimension(web::Plot_Dimension::two_d); + append_dimension(web::Plot_Dimension::three_d); + } + else if (token.empty() || !append_plot_id(token)) return false; + if (separator == std::string_view::npos) break; + specification.remove_prefix(separator + 1U); + } + return !configuration.plot_ids.empty(); +} + +[[nodiscard]] bool parse_unsigned(std::string_view text, + std::uint32_t& value) { + const auto* begin = text.data(); + const auto* end = begin + text.size(); + const auto result = std::from_chars(begin, end, value); + return result.ec == std::errc{} && result.ptr == end; +} + +[[nodiscard]] bool parse_duration(std::string_view text, double& value) { + const auto* begin = text.data(); + const auto* end = begin + text.size(); + const auto result = std::from_chars(begin, end, value); + return result.ec == std::errc{} && result.ptr == end && + value > 0.0 && value <= 3'600.0; +} + +[[nodiscard]] Parse_Benchmark_Arguments_Result parse_arguments( + int& argc, char** argv) { + configuration = {}; + if (!select_plots("3d")) { + configuration_error = "the Gallery has no 3D plots"; + return Parse_Benchmark_Arguments_Result::invalid_argument; + } + int retained_count{1}; + for (int index = 1; index < argc; ++index) { + const std::string_view argument{argv[index]}; + if (argument == "--aethera_help") + return Parse_Benchmark_Arguments_Result::help_requested; + const auto value_after = [&](std::string_view prefix) + -> std::optional { + if (!argument.starts_with(prefix)) return std::nullopt; + return argument.substr(prefix.size()); + }; + if (const auto value = value_after("--aethera_plots=")) { + if (!select_plots(*value)) { + configuration_error = "invalid --aethera_plots selection: " + + std::string{*value}; + return Parse_Benchmark_Arguments_Result::invalid_argument; + } + continue; + } + if (const auto value = value_after("--aethera_input=")) { + if (*value == "steady") configuration.input = Input_Workload::steady; + else if (*value == "drag") configuration.input = Input_Workload::drag; + else if (*value == "wheel") configuration.input = Input_Workload::wheel; + else if (*value == "mixed") configuration.input = Input_Workload::mixed; + else { + configuration_error = "invalid --aethera_input value: " + + std::string{*value}; + return Parse_Benchmark_Arguments_Result::invalid_argument; + } + continue; + } + if (const auto value = value_after("--aethera_size=")) { + const auto separator = value->find('x'); + if (separator == std::string_view::npos || + !parse_unsigned(value->substr(0, separator), configuration.width) || + !parse_unsigned(value->substr(separator + 1U), configuration.height) || + configuration.width == 0 || configuration.height == 0) { + configuration_error = "invalid --aethera_size value: " + + std::string{*value}; + return Parse_Benchmark_Arguments_Result::invalid_argument; + } + continue; + } + if (const auto value = value_after("--aethera_input_rate=")) { + if (!parse_unsigned(*value, configuration.input_rate_hz) || + configuration.input_rate_hz == 0) { + configuration_error = "invalid --aethera_input_rate value: " + + std::string{*value}; + return Parse_Benchmark_Arguments_Result::invalid_argument; + } + continue; + } + if (const auto value = value_after("--aethera_duration=")) { + if (!parse_duration(*value, configuration.duration_seconds)) { + configuration_error = "invalid --aethera_duration value: " + + std::string{*value}; + return Parse_Benchmark_Arguments_Result::invalid_argument; + } + continue; + } + argv[retained_count++] = argv[index]; + } + argc = retained_count; + return Parse_Benchmark_Arguments_Result::configured; +} + struct Concurrent_Workload_State { - std::array completed{}; /* Per-Plot publish counts for this benchmark interval. */ + std::vector> completed{}; /* 所选 Plot 各自的发布计数器;回调共享稳定所有权。 */ }; [[nodiscard]] Plot_Input_Request concurrent_input_request( Input_Workload workload, std::size_t plot_index, std::uint64_t sequence) { Plot_Input_Request request; - request.plot = plot_3d_ids[plot_index]; + request.plot = configuration.plot_ids[plot_index]; request.event.time_milliseconds = 1'000.0 + - static_cast(sequence) * (1'000.0 / 120.0); + static_cast(sequence) * + (1'000.0 / static_cast(configuration.input_rate_hz)); request.event.position = { 360.0 + static_cast( static_cast((sequence + plot_index * 7U) % 121U) - 60), @@ -112,7 +198,7 @@ struct Concurrent_Workload_State { return request; } -class Concurrent_3D : public benchmark::Fixture { +class Configured_Plots : public benchmark::Fixture { public: void SetUp(const benchmark::State&) override { if (shared_service) { @@ -124,19 +210,25 @@ public: } shared_service = Control_Service::create(); shared_workload = std::make_shared(); - shared_plots.reserve(plot_3d_ids.size()); - shared_streams.reserve(plot_3d_ids.size()); - for (std::size_t index = 0; index < plot_3d_ids.size(); ++index) { - auto plot = shared_service->find_plot(plot_3d_ids[index]); - if (!plot) throw std::logic_error("concurrent 3D benchmark Plot is unavailable"); + shared_workload->completed.reserve(configuration.plot_ids.size()); + shared_plots.reserve(configuration.plot_ids.size()); + shared_streams.reserve(configuration.plot_ids.size()); + for (std::size_t index = 0; index < configuration.plot_ids.size(); + ++index) { + auto plot = shared_service->find_plot(configuration.plot_ids[index]); + if (!plot) + throw std::logic_error("configured benchmark Plot is unavailable"); + auto completed = std::make_shared(0); + shared_workload->completed.push_back(completed); const auto stream = plot->subscribe( - [workload = shared_workload, index]( + [completed = std::move(completed)]( std::shared_ptr frame) { if (!frame || !frame->pixels) return; - workload->completed[index].fetch_add( + completed->fetch_add( 1, std::memory_order_relaxed); }); - plot->configure_stream(stream, 720, 420); + plot->configure_stream( + stream, configuration.width, configuration.height); shared_plots.push_back(std::move(plot)); shared_streams.push_back(stream); } @@ -163,7 +255,7 @@ protected: Input_Workload input, std::uint64_t sequence, std::optional pointer_type = std::nullopt) { for (std::size_t plot_index = 0; - plot_index < plot_3d_ids.size(); ++plot_index) { + plot_index < configuration.plot_ids.size(); ++plot_index) { const bool wheel = input == Input_Workload::wheel || (input == Input_Workload::mixed && (plot_index & 1U) != 0U); if (pointer_type && wheel) continue; @@ -181,42 +273,49 @@ protected: return true; } - void run(benchmark::State& state, Input_Workload input) { + void run(benchmark::State& state) { + const auto input = configuration.input; for (auto& count : workload->completed) - count.store(0, std::memory_order_relaxed); + count->store(0, std::memory_order_relaxed); for (const auto& plot : plots) plot->reset_diagnostics(); if (input == Input_Workload::drag || input == Input_Workload::mixed) { if (!submit_input_batch( input, 0, Event_Type::pointer_press)) { - state.SkipWithError("16-Plot pointer press batch was rejected"); + state.SkipWithError("configured Plot pointer press batch was rejected"); return; } } const auto runtime_begin = service->call_tool( "aethera_task_runtime", nlohmann::json::object()); const auto started = std::chrono::steady_clock::now(); - constexpr auto input_period = std::chrono::nanoseconds{ - 1'000'000'000 / 120}; + const auto deadline = started + std::chrono::duration_cast< + std::chrono::steady_clock::duration>( + std::chrono::duration{configuration.duration_seconds}); + const auto input_period = std::chrono::nanoseconds{ + 1'000'000'000 / configuration.input_rate_hz}; auto next_input = started + input_period; std::uint64_t input_batches{}; std::uint64_t input_requests{}; std::uint32_t input_poll{}; for ([[maybe_unused]] auto iteration : state) { - benchmark::DoNotOptimize( - workload->completed[splat_plot_index].load( - std::memory_order_relaxed)); - benchmark::ClobberMemory(); - if ((++input_poll & 0x3FFU) != 0U) continue; - const auto now = std::chrono::steady_clock::now(); - if (input == Input_Workload::steady) continue; - if (now < next_input) continue; - ++input_batches; - if (!submit_input_batch(input, input_batches)) { - state.SkipWithError("16-Plot interaction batch was rejected"); - break; + while (std::chrono::steady_clock::now() < deadline) { + benchmark::DoNotOptimize( + workload->completed.front()->load( + std::memory_order_relaxed)); + benchmark::ClobberMemory(); + if ((++input_poll & 0x3FFU) != 0U) continue; + const auto now = std::chrono::steady_clock::now(); + if (input == Input_Workload::steady || now < next_input) + continue; + ++input_batches; + if (!submit_input_batch(input, input_batches)) { + state.SkipWithError( + "configured Plot interaction batch was rejected"); + break; + } + input_requests += configuration.plot_ids.size(); + do next_input += input_period; while (next_input <= now); } - input_requests += plot_3d_ids.size(); - do next_input += input_period; while (next_input <= now); } const auto finished = std::chrono::steady_clock::now(); if (input == Input_Workload::drag || input == Input_Workload::mixed) { @@ -229,20 +328,17 @@ protected: std::uint64_t total_completed{}; double minimum_fps = std::numeric_limits::max(); double maximum_fps{}; - double splat_fps{}; for (std::size_t index = 0; index < plots.size(); ++index) { - const auto count = workload->completed[index].load( + const auto count = workload->completed[index]->load( std::memory_order_relaxed); total_completed += count; const double fps = elapsed_seconds > 0.0 ? static_cast(count) / elapsed_seconds : 0.0; minimum_fps = std::min(minimum_fps, fps); maximum_fps = std::max(maximum_fps, fps); - if (index == splat_plot_index) splat_fps = fps; - const auto diagnostics = plots[index]->diagnostics(); const auto& policy = diagnostics.at("frame_policy"); - const auto prefix = "p" + std::to_string(index) + "_"; + const auto prefix = std::string{configuration.plot_ids[index]} + "/"; state.counters[prefix + "fps"] = fps; state.counters[prefix + "requested"] = policy.at("observation").at("request_count").get(); @@ -258,16 +354,63 @@ protected: policy.at("requests").at("frame_slot_backpressure").get(); state.counters[prefix + "completion_ms"] = policy.at("latency").at("average_completion_ms").get(); + state.counters[prefix + "completion_max_ms"] = + policy.at("latency").at("maximum_completion_ms").get(); const auto& frame_statistics = diagnostics.at("frame_statistics"); - const auto statistic_average = [&](std::string_view name) { + const auto statistic_value = [&](std::string_view name, + std::string_view field) { const auto found = frame_statistics.find(name); return found == frame_statistics.end() - ? 0.0 : found->at("average").get(); + ? 0.0 : found->at(field).get(); }; state.counters[prefix + "backend_queue_ms"] = - statistic_average("backend_queue_ms"); + statistic_value("backend_queue_ms", "average"); + state.counters[prefix + "backend_queue_p95_ms"] = + statistic_value("backend_queue_ms", "p95"); + state.counters[prefix + "backend_queue_max_ms"] = + statistic_value("backend_queue_ms", "maximum"); + state.counters[prefix + "backend_apply_ms"] = + statistic_value("backend_apply_ms", "average"); + state.counters[prefix + "backend_apply_p95_ms"] = + statistic_value("backend_apply_ms", "p95"); + state.counters[prefix + "backend_apply_max_ms"] = + statistic_value("backend_apply_ms", "maximum"); state.counters[prefix + "backend_plan_ms"] = - statistic_average("backend_plan_ms"); + statistic_value("backend_plan_ms", "average"); + state.counters[prefix + "backend_plan_p95_ms"] = + statistic_value("backend_plan_ms", "p95"); + state.counters[prefix + "backend_plan_max_ms"] = + statistic_value("backend_plan_ms", "maximum"); + state.counters[prefix + "backend_execute_ms"] = + statistic_value("backend_execute_ms", "average"); + state.counters[prefix + "backend_execute_p95_ms"] = + statistic_value("backend_execute_ms", "p95"); + state.counters[prefix + "backend_execute_max_ms"] = + statistic_value("backend_execute_ms", "maximum"); + state.counters[prefix + "backend_submit_queue_ms"] = + statistic_value("backend_submit_queue_ms", "average"); + state.counters[prefix + "backend_submit_queue_p95_ms"] = + statistic_value("backend_submit_queue_ms", "p95"); + state.counters[prefix + "backend_submit_queue_max_ms"] = + statistic_value("backend_submit_queue_ms", "maximum"); + state.counters[prefix + "backend_submit_ms"] = + statistic_value("backend_submit_ms", "average"); + state.counters[prefix + "backend_submit_p95_ms"] = + statistic_value("backend_submit_ms", "p95"); + state.counters[prefix + "backend_submit_max_ms"] = + statistic_value("backend_submit_ms", "maximum"); + state.counters[prefix + "gpu_total_ms"] = + statistic_value("gpu_total_ms", "average"); + state.counters[prefix + "gpu_total_p95_ms"] = + statistic_value("gpu_total_ms", "p95"); + state.counters[prefix + "readback_ms"] = + statistic_value("readback_ms", "average"); + state.counters[prefix + "scene_render_ms"] = + statistic_value("scene_render_ms", "average"); + state.counters[prefix + "event_dispatch_ms"] = + statistic_value("event_dispatch_ms", "average"); + state.counters[prefix + "frame_interval_p95_ms"] = + statistic_value("frame_interval_ms", "p95"); } const auto runtime_end = service->call_tool( "aethera_task_runtime", nlohmann::json::object()); @@ -287,7 +430,6 @@ protected: ? static_cast(total_completed) / elapsed_seconds : 0.0; state.counters["minimum_plot_fps"] = minimum_fps; state.counters["maximum_plot_fps"] = maximum_fps; - state.counters["splat_fps"] = splat_fps; state.counters["fairness_pct"] = maximum_fps > 0.0 ? minimum_fps / maximum_fps * 100.0 : 0.0; state.counters["worker_busy_pct"] = wall_ns > 0.0 && worker_count > 0.0 @@ -312,105 +454,199 @@ private: std::vector streams{}; }; -BENCHMARK_DEFINE_F(Concurrent_3D, Warmup)(benchmark::State& state) { - run(state, Input_Workload::steady); +BENCHMARK_DEFINE_F(Configured_Plots, Run)(benchmark::State& state) { + run(state); } -BENCHMARK_DEFINE_F(Concurrent_3D, Steady)(benchmark::State& state) { - run(state, Input_Workload::steady); -} -BENCHMARK_DEFINE_F(Concurrent_3D, All_Plots_Drag)(benchmark::State& state) { - run(state, Input_Workload::drag); -} -BENCHMARK_DEFINE_F(Concurrent_3D, All_Plots_Wheel)(benchmark::State& state) { - run(state, Input_Workload::wheel); -} -BENCHMARK_DEFINE_F(Concurrent_3D, All_Plots_Mixed)(benchmark::State& state) { - run(state, Input_Workload::mixed); -} +/* Fixture 静态持有所选 Plot;Google Benchmark 校准不会反复重建其渲染资源。 */ +BENCHMARK_REGISTER_F(Configured_Plots, Run) + ->Iterations(1) + ->UseRealTime(); -BENCHMARK_DEFINE_F(Control_Path, Timestamped_Drag_Admission)( - benchmark::State& state) { - auto request = timestamped_drag_request(Event_Type::pointer_press); - auto arguments = encode_protocol_value(request); - const auto press = service->call_tool("aethera_plot_input", arguments); - if (press.result != Tool_Call_Result::ok) { - state.SkipWithError("timestamped pointer press was rejected"); - return; +class Plot_Console_Reporter final : public benchmark::ConsoleReporter { +public: + bool ReportContext(const Context& context) override { + return ConsoleReporter::ReportContext(context); } - arguments["event"]["type"] = "pointer_move"; - double time_milliseconds = request.event.time_milliseconds; - for ([[maybe_unused]] auto iteration : state) { - time_milliseconds += 1'000.0 / 120.0; - arguments["event"]["time_milliseconds"] = time_milliseconds; - const auto result = service->call_tool( - "aethera_plot_input", arguments); - if (result.result != Tool_Call_Result::ok) { - state.SkipWithError("timestamped pointer move was rejected"); - break; + void ReportRuns(const std::vector& reports) override { + auto& output = GetOutputStream(); + const auto flags = output.flags(); + const auto precision = output.precision(); + output << std::fixed << std::setprecision(2); + for (const auto& report : reports) { + if (report.skipped != benchmark::internal::NotSkipped) { + output << report.benchmark_name() << ": " + << report.skip_message << '\n'; + continue; + } + const auto counter = [&](std::string_view name) { + const auto found = report.counters.find(std::string{name}); + return found == report.counters.end() ? 0.0 : found->second.value; + }; + output << "\n" << report.benchmark_name() + << " elapsed=" << report.real_accumulated_time + << " s frames=" + << static_cast(counter("aggregate_fps") * + report.real_accumulated_time) + << "\n"; + output << std::left + << std::setw(23) << "Plot" + << std::right + << std::setw(9) << "FPS" + << std::setw(11) << "Done ms" + << std::setw(11) << "Max ms" + << std::setw(11) << "Scene ms" + << std::setw(11) << "Event ms" + << std::setw(11) << "Queue95" + << std::setw(11) << "QueueMax" + << std::setw(11) << "Apply95" + << std::setw(11) << "ApplyMax" + << std::setw(11) << "Plan95" + << std::setw(11) << "PlanMax" + << std::setw(11) << "Exec95" + << std::setw(11) << "ExecMax" + << std::setw(11) << "SubQ95" + << std::setw(11) << "SubQMax" + << std::setw(11) << "Submit95" + << std::setw(11) << "SubmitMax" + << std::setw(10) << "GPU ms" + << std::setw(10) << "Read ms" + << std::setw(10) << "Backpr." + << std::setw(9) << "Reject" << '\n'; + for (const auto id : configuration.plot_ids) { + const auto prefix = std::string{id} + "/"; + output << std::left << std::setw(23) << id << std::right + << std::setw(9) << counter(prefix + "fps") + << std::setw(11) << counter(prefix + "completion_ms") + << std::setw(11) << counter(prefix + "completion_max_ms") + << std::setw(11) << counter(prefix + "scene_render_ms") + << std::setw(11) << counter(prefix + "event_dispatch_ms") + << std::setw(11) << counter(prefix + "backend_queue_p95_ms") + << std::setw(11) << counter(prefix + "backend_queue_max_ms") + << std::setw(11) << counter(prefix + "backend_apply_p95_ms") + << std::setw(11) << counter(prefix + "backend_apply_max_ms") + << std::setw(11) << counter(prefix + "backend_plan_p95_ms") + << std::setw(11) << counter(prefix + "backend_plan_max_ms") + << std::setw(11) << counter(prefix + "backend_execute_p95_ms") + << std::setw(11) << counter(prefix + "backend_execute_max_ms") + << std::setw(11) << counter(prefix + "backend_submit_queue_p95_ms") + << std::setw(11) << counter(prefix + "backend_submit_queue_max_ms") + << std::setw(11) << counter(prefix + "backend_submit_p95_ms") + << std::setw(11) << counter(prefix + "backend_submit_max_ms") + << std::setw(10) << counter(prefix + "gpu_total_ms") + << std::setw(10) << counter(prefix + "readback_ms") + << std::setw(10) << counter(prefix + "slot_backpressure") + << std::setw(9) << counter(prefix + "scene_rejected") + << '\n'; + } + output << "aggregate_fps=" << counter("aggregate_fps") + << " min_fps=" << counter("minimum_plot_fps") + << " max_fps=" << counter("maximum_plot_fps") + << " fairness=" << counter("fairness_pct") << "%" + << " input_batches=" << counter("input_batches") + << " input_requests/s=" << counter("input_request_rate") + << " worker_busy=" << counter("worker_busy_pct") << "%" + << " worker_cpu=" << counter("worker_cpu_pct") << "%\n"; } - benchmark::DoNotOptimize(result.result); + output.flags(flags); + output.precision(precision); } - state.SetItemsProcessed(state.iterations()); +}; - const auto runtime = service->call_tool( - "aethera_task_runtime", nlohmann::json::object()); - if (runtime.result != Tool_Call_Result::ok) { - state.SkipWithError("Taskflow runtime diagnostics were rejected"); - return; - } - const auto milliseconds = [](const nlohmann::json& value, - std::string_view key) { - return static_cast(value.at(key).get()) / - 1'000'000.0; - }; - state.counters["taskflow_wall_ms"] = - milliseconds(runtime.content, "observed_wall_time_ns"); - state.counters["worker_busy_ms"] = - milliseconds(runtime.content, "worker_busy_time_ns"); - state.counters["worker_cpu_ms"] = - milliseconds(runtime.content, "worker_cpu_time_ns"); - state.counters["worker_utilization_pct"] = - runtime.content.at("worker_utilization").get(); - state.counters["worker_cpu_utilization_pct"] = - runtime.content.at("worker_cpu_utilization").get(); - state.counters["active_taskflows"] = - static_cast(runtime.content.at("active_taskflows").get()); - state.counters["completed_taskflows"] = - static_cast(runtime.content.at("completed_taskflows").get()); - state.SetLabel(runtime.content.at("longest_task").at("name").get()); +void print_help() { + std::cout << + "Aethera concurrent Plot benchmark options:\n" + " --aethera_plots=3d|2d|all|id[,id...]\n" + " Groups and IDs may be mixed, for example: 2d,datoviz_mesh\n" + " --aethera_input=steady|drag|wheel|mixed\n" + " --aethera_size=WIDTHxHEIGHT\n" + " --aethera_input_rate=HZ\n" + " --aethera_duration=SECONDS (default: 10, maximum: 3600)\n" + " --aethera_help\n" + "Google Benchmark output options remain available, for example:\n" + " --benchmark_format=json\n\n" + "Available Gallery Plot IDs:\n"; + for (const auto& definition : web::gallery_plot_definitions()) + std::cout << " " << definition.id << " (" + << web::plot_dimension_name(definition.dimension) << ")\n"; } -BENCHMARK(decode_timestamped_drag); -BENCHMARK(decode_timed_render); -BENCHMARK_REGISTER_F(Control_Path, Timestamped_Drag_Admission) - ->Iterations(512); -/* Fixture 静态持有 16 个 Plot;MinTime 校准不会重建 Datoviz Scene。 */ -BENCHMARK_REGISTER_F(Concurrent_3D, Warmup) - ->MinTime(10.0)->UseRealTime(); -BENCHMARK_REGISTER_F(Concurrent_3D, Steady) - ->MinTime(10.0)->UseRealTime(); -BENCHMARK_REGISTER_F(Concurrent_3D, All_Plots_Drag) - ->MinTime(10.0)->UseRealTime(); -BENCHMARK_REGISTER_F(Concurrent_3D, All_Plots_Wheel) - ->MinTime(10.0)->UseRealTime(); -BENCHMARK_REGISTER_F(Concurrent_3D, All_Plots_Mixed) - ->MinTime(10.0)->UseRealTime(); +[[nodiscard]] Parse_Benchmark_Arguments_Result configure_benchmark( + int& argc, char** argv) { + return parse_arguments(argc, argv); +} -void shutdown_concurrent_3d_benchmark() { - Concurrent_3D::shutdown(); +[[nodiscard]] const std::string& benchmark_configuration_error() { + return configuration_error; +} + +void describe_configuration() { + std::string plots; + for (const auto id : configuration.plot_ids) { + if (!plots.empty()) plots += ','; + plots += id; + } + const auto input = [&] { + switch (configuration.input) { + case Input_Workload::steady: return "steady"; + case Input_Workload::drag: return "drag"; + case Input_Workload::wheel: return "wheel"; + case Input_Workload::mixed: return "mixed"; + } + return "unknown"; + }(); + benchmark::AddCustomContext("aethera_plots", std::move(plots)); + benchmark::AddCustomContext("aethera_input", input); + benchmark::AddCustomContext( + "aethera_size", std::to_string(configuration.width) + "x" + + std::to_string(configuration.height)); + benchmark::AddCustomContext( + "aethera_input_rate_hz", std::to_string(configuration.input_rate_hz)); + benchmark::AddCustomContext( + "aethera_duration_seconds", + std::to_string(configuration.duration_seconds)); +} + +void shutdown_configured_benchmark() { + Configured_Plots::shutdown(); } } } int main(int argc, char** argv) { + bool structured_display{}; + for (int index = 1; index < argc; ++index) { + const std::string_view argument{argv[index]}; + constexpr std::string_view format_prefix{"--benchmark_format="}; + if (argument.starts_with(format_prefix) && + argument.substr(format_prefix.size()) != "console") + structured_display = true; + } + const auto configured = + aethera::mcp::benchmarks::configure_benchmark(argc, argv); + if (configured == aethera::mcp::benchmarks:: + Parse_Benchmark_Arguments_Result::help_requested) { + aethera::mcp::benchmarks::print_help(); + return 0; + } + if (configured == aethera::mcp::benchmarks:: + Parse_Benchmark_Arguments_Result::invalid_argument) { + std::cerr << aethera::mcp::benchmarks::benchmark_configuration_error() + << "\nUse --aethera_help to list valid selections.\n"; + return 2; + } aethera::initialize_runtime({}); benchmark::Initialize(&argc, argv); if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 2; - benchmark::RunSpecifiedBenchmarks(); - aethera::mcp::benchmarks::shutdown_concurrent_3d_benchmark(); + aethera::mcp::benchmarks::describe_configuration(); + if (structured_display) benchmark::RunSpecifiedBenchmarks(); + else { + aethera::mcp::benchmarks::Plot_Console_Reporter reporter; + benchmark::RunSpecifiedBenchmarks(&reporter); + } + aethera::mcp::benchmarks::shutdown_configured_benchmark(); benchmark::Shutdown(); return 0; } diff --git a/render_2D/render_2D/render/Blend2D_Cache.cpp b/render_2D/render_2D/render/Blend2D_Cache.cpp index bfc372f..f83c5f3 100644 --- a/render_2D/render_2D/render/Blend2D_Cache.cpp +++ b/render_2D/render_2D/render/Blend2D_Cache.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -150,16 +149,16 @@ void Painter::Private::load_font_face(BLFontFace& face, std::initializer_list faces; - static std::once_flag once; - std::call_once(once, [] { - load_font_face(faces[0], {"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simsun.ttc", - "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf"}); - load_font_face(faces[1], {"C:/Windows/Fonts/msyhbd.ttc", "C:/Windows/Fonts/segoeuib.ttf", - "C:/Windows/Fonts/arialbd.ttf"}); - load_font_face(faces[2], {"C:/Windows/Fonts/segoeuii.ttf", "C:/Windows/Fonts/ariali.ttf"}); - load_font_face(faces[3], {"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arialbi.ttf"}); - }); + thread_local std::array faces = [] { + std::array result; + load_font_face(result[0], {"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simsun.ttc", + "C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf"}); + load_font_face(result[1], {"C:/Windows/Fonts/msyhbd.ttc", "C:/Windows/Fonts/segoeuib.ttf", + "C:/Windows/Fonts/arialbd.ttf"}); + load_font_face(result[2], {"C:/Windows/Fonts/segoeuii.ttf", "C:/Windows/Fonts/ariali.ttf"}); + load_font_face(result[3], {"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arialbi.ttf"}); + return result; + }(); const std::size_t index = (font.weight >= 600 ? 1u : 0u) | (font.italic ? 2u : 0u); return faces[index] ? faces[index] : faces[0]; } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index e797e35..07d9952 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -16,7 +16,6 @@ #include #include #include - struct DvzController; struct DvzDrp2CommandStream; struct DvzDrp2Runtime; @@ -29,30 +28,27 @@ struct DvzPinnedReadout; struct DvzPointerGestureHandler; struct DvzScene; struct DvzSceneFrameArtifact; - -namespace aethera { namespace render_3d { namespace detail { - +namespace aethera { +namespace render_3d { +namespace detail { struct Datoviz_Render_Context; struct Datoviz_Frame_Target; struct Datoviz_Frame_Target_Set; - /* Scene 帧槽从录制到 GPU 完成期间携带的唯一状态。 */ struct Datoviz_Pending_Frame { - std::uintptr_t device{}; /* Datoviz Device 的非拥有句柄值。 */ - std::uintptr_t fence{}; /* 本帧完成 Fence 的非拥有句柄值。 */ - Extent extent{}; /* 本帧离屏目标尺寸。 */ - std::uint8_t target_index{}; /* Scene 三槽中的目标下标。 */ - std::uint64_t target_generation{}; /* 防止槽重建后的陈旧引用命中。 */ + std::uintptr_t device{}; /* Datoviz Device 的非拥有句柄值。 */ + std::uintptr_t fence{}; /* 本帧完成 Fence 的非拥有句柄值。 */ + Extent extent{}; /* 本帧离屏目标尺寸。 */ + std::uint8_t target_index{}; /* Scene 三槽中的目标下标。 */ + std::uint64_t target_generation{}; /* 防止槽重建后的陈旧引用命中。 */ Datoviz_Frame_Observation observation{}; /* 本帧后端执行事实。 */ }; - /* 同一个 Scene 帧完成后的交付状态,不拆成独立模块。 */ struct Datoviz_Completed_Frame { Extent extent{}; std::vector pixels{}; Datoviz_Frame_Observation observation{}; }; - /* * 3D 后端核心资源所有权: * 1. render_context_ 独占 GPU 上下文入口,并串行投递 Vulkan 工作。 @@ -66,7 +62,6 @@ struct Scene_Datoviz_State { ~Scene_Datoviz_State(); Scene_Datoviz_State(const Scene_Datoviz_State&) = delete; Scene_Datoviz_State& operator=(const Scene_Datoviz_State&) = delete; - /* 一次性建立 GPU、Vulkan 池、三槽 Runtime 和 Datoviz Scene 资源。 */ void initialize(std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, @@ -137,46 +132,41 @@ struct Scene_Datoviz_State { /* 正常路径按所有权逆序销毁;故障隔离路径只转移资源,不在线程上等待。 */ void abandon_resources() noexcept; void destroy(); - private: struct Runtime_Slot { - owner runtime{}; /* 本目标槽独占的 DRP2 Runtime。 */ + owner runtime{}; /* 本目标槽独占的 DRP2 Runtime。 */ owner emitter{}; /* 本目标槽独占的帧计划 emitter。 */ }; - std::shared_ptr render_context_{}; /* 本 Scene 独占 GPU 上下文的共享销毁闸门。 */ - std::uintptr_t command_pool_{}; /* 本 Scene 独占 Vulkan Command Pool。 */ - std::uintptr_t descriptor_pool_{}; /* 本 Scene 独占 Vulkan Descriptor Pool。 */ + std::uintptr_t command_pool_{}; /* 本 Scene 独占 Vulkan Command Pool。 */ + std::uintptr_t descriptor_pool_{}; /* 本 Scene 独占 Vulkan Descriptor Pool。 */ std::array runtime_slots_{}; /* 三个固定目标槽的 Runtime。 */ - owner scene_{}; /* Datoviz Scene 所有权。 */ - DvzFigure* figure_{}; /* 可空、非拥有;由 scene_ 拥有。 */ - DvzPanel* panel_{}; /* 可空、非拥有;由 scene_ 拥有。 */ - DvzVisual* axes_visual_{}; /* 可空、非拥有;由 scene_ 拥有。 */ - DvzText* axes_text_{}; /* 可空、非拥有;由 scene_ 拥有。 */ - Extent figure_extent_{}; /* 当前原生 Figure 尺寸。 */ - std::uint64_t command_revision_{1}; /* 原生命令结构版本。 */ + owner scene_{}; /* Datoviz Scene 所有权。 */ + DvzFigure* figure_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + DvzPanel* panel_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + DvzVisual* axes_visual_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + DvzText* axes_text_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + Extent figure_extent_{}; /* 当前原生 Figure 尺寸。 */ + std::uint64_t command_revision_{1}; /* 原生命令结构版本。 */ std::optional applied_camera_{}; /* 已应用相机,仅用于结构复用判定。 */ std::optional> applied_axes_{}; /* 已应用坐标轴。 */ - std::atomic_bool quarantined_{}; /* Unknown Failure 后是否禁止销毁在途资源。 */ - + std::atomic_bool quarantined_{}; /* Unknown Failure 后是否禁止销毁在途资源。 */ std::vector visuals_{}; /* 注册顺序稳定的具体 Visual 集合。 */ std::vector> external_buffers_{}; /* Visual 三槽属性 Buffer 唯一所有权。 */ - owner item_interaction_{}; /* item query 能力所有权。 */ - owner hover_readout_{}; /* 当前 hover readout 所有权。 */ - owner camera_controller_{}; /* 相机控制器所有权。 */ - owner input_router_{}; /* 输入路由所有权。 */ + owner hover_readout_{}; /* 当前 hover readout 所有权。 */ + owner camera_controller_{}; /* 相机控制器所有权。 */ + owner input_router_{}; /* 输入路由所有权。 */ owner gesture_handler_{}; /* 指针手势处理器所有权。 */ - std::uint64_t controller_revision_{1}; /* 已应用控制器输入版本。 */ - bool input_changed_{}; /* 当前录制周期是否消费了输入。 */ - - mutable std::mutex target_mutex_{}; /* 仅保护三槽在提交与回读边界的生命周期。 */ + std::uint64_t controller_revision_{1}; /* 已应用控制器输入版本。 */ + bool input_changed_{}; /* 当前录制周期是否消费了输入。 */ + mutable std::mutex target_mutex_{}; /* 仅保护三槽在提交与回读边界的生命周期。 */ std::unique_ptr targets_{}; /* 三槽唯一所有权。 */ - std::uint64_t target_generation_{}; /* 目标槽重建代数。 */ + std::uint64_t target_generation_{}; /* 目标槽重建代数。 */ }; - -}}} // namespace aethera::render_3d::detail - +} +} +} // namespace aethera::render_3d::detail #include "../detail/Gpu_Completion_Service.hpp" #include #include @@ -224,6 +214,9 @@ inline void publish_datoviz_observation( if (observation.execute_ns) frame->record(Frame_Trace_Measurement::backend_execute_ns, observation.execute_ns); + if (observation.queue_submit_wait_ns) + frame->record(Frame_Trace_Measurement::backend_submit_queue_ns, + observation.queue_submit_wait_ns); if (observation.submit_ns) frame->record(Frame_Trace_Measurement::backend_submit_ns, observation.submit_ns); @@ -272,19 +265,19 @@ struct Render_Scene_3D::Private : Prev_Private, }; struct Frame_Context { std::atomic phase{Frame_Phase::available}; /* 当前借用帧在 Scene 内的唯一生命周期阶段。 */ - Frame_3D* frame{}; /* 可空、非拥有借用;available 时为空。 */ - Event_Batch events{}; /* 本帧 Prepare 消费的输入批次。 */ - bool trace_started{}; /* 本帧是否已绑定 Taskflow trace。 */ - bool overlaps_gpu{}; /* 本帧资源是否允许 CPU Prepare 与 GPU 重叠。 */ - std::atomic_bool cpu_finished{}; /* frame DAG 已完成 CPU 部分。 */ - std::atomic_bool gpu_submitted{}; /* Vulkan queue submit 已完成。 */ - std::atomic_bool collection_ready{}; /* completion_result/failure 已 release 发布给 Scene completion task。 */ - std::atomic_bool collection_completed{}; /* target collect 与 Frame 数据附加已完成。 */ + Frame_3D* frame{}; /* 可空、非拥有借用;available 时为空。 */ + Event_Batch events{}; /* 本帧 Prepare 消费的输入批次。 */ + bool trace_started{}; /* 本帧是否已绑定 Taskflow trace。 */ + bool overlaps_gpu{}; /* 本帧资源是否允许 CPU Prepare 与 GPU 重叠。 */ + std::atomic_bool cpu_finished{}; /* frame DAG 已完成 CPU 部分。 */ + std::atomic_bool gpu_submitted{}; /* Vulkan queue submit 已完成。 */ + std::atomic_bool collection_ready{}; /* completion_result/failure 已 release 发布给 Scene completion task。 */ + std::atomic_bool collection_completed{}; /* target collect 与 Frame 数据附加已完成。 */ std::chrono::steady_clock::time_point completion_observed_at{}; /* fence 被非阻塞探测为完成的时刻。 */ std::optional completion_result{}; /* 本物理帧唯一 Vulkan 完成结果。 */ - std::exception_ptr completion_failure{}; /* 本物理帧的完成边界 Unknown Failure。 */ - std::exception_ptr failure{}; /* Scene 最终向调用方交付的 Unknown Failure。 */ - Frame_Callbacks callbacks{}; /* 仅绑定本次借用帧;返还前移出并清空。 */ + std::exception_ptr completion_failure{}; /* 本物理帧的完成边界 Unknown Failure。 */ + std::exception_ptr failure{}; /* Scene 最终向调用方交付的 Unknown Failure。 */ + Frame_Callbacks callbacks{}; /* 仅绑定本次借用帧;返还前移出并清空。 */ std::optional datoviz_frame{}; }; @@ -538,7 +531,7 @@ void Render_Scene_3D::Private::render_datoviz( if (!visuals || visuals->empty()) throw std::invalid_argument("3D Scene requires prepared Visual data"); if (!backend_available.load(std::memory_order_acquire)) throw std::runtime_error("3D Scene resources are unavailable"); const not_null frame{context->frame}; - auto reservation = detail::Gpu_Completion_Service::instance().prepare( + auto ret = detail::Gpu_Completion_Service::instance().prepare( [this, object, context](detail::Gpu_Completion_Service::Result result) { observe_datoviz_completion( object, context, std::move(result), {}); @@ -548,8 +541,8 @@ void Render_Scene_3D::Private::render_datoviz( object, context, {}, std::move(failure)); }, frame->taskflow_trace_requested()); - if (!reservation) { - if (reservation.result == detail::Gpu_Completion_Service:: + if (!ret) { + if (ret.result == detail::Gpu_Completion_Service:: Admission_Result::capacity_exhausted) throw std::runtime_error( "GPU completion capacity is exhausted"); @@ -586,7 +579,7 @@ void Render_Scene_3D::Private::render_datoviz( frame->mark(Frame_Trace_Marker::backend_submit_queued); auto completion_reservation = std::make_shared< detail::Gpu_Completion_Service::Reservation>( - std::move(reservation.reservation)); + std::move(ret.reservation)); submit( *context->datoviz_frame, [this, object, context, frame, @@ -684,12 +677,12 @@ void Render_Scene_3D::Private::collect_datoviz( auto& observation = context->datoviz_frame->observation; if (result) observation.gpu_completion_observation_ns = - result->wait_duration_ns; + result->wait_duration_ns; observation.completion_task_queue_ns = - static_cast(std::max( - 0, std::chrono::duration_cast( - completion_started - context->completion_observed_at) - .count())); + static_cast(std::max( + 0, std::chrono::duration_cast( + completion_started - context->completion_observed_at) + .count())); if (failure) std::rethrow_exception(std::move(failure)); if (!result || result->error != detail::Gpu_Completion_Service::Completion_Error::none) { @@ -785,10 +778,8 @@ void Render_Scene_3D::Private::try_release_prepare( std::memory_order_acquire)) return; prepare_active.store(false, std::memory_order_release); - if (context->callbacks.submitted) - context->callbacks.submitted(context->frame, context->overlaps_gpu); - if (context->collection_completed.load(std::memory_order_acquire)) - arm_completion(object); + if (context->callbacks.submitted) context->callbacks.submitted(context->frame, context->overlaps_gpu); + if (context->collection_completed.load(std::memory_order_acquire)) arm_completion(object); } template void Render_Scene_3D::Private::arm_completion(Object* object) { @@ -801,7 +792,9 @@ void Render_Scene_3D::Private::arm_completion(Object* object) { aethera::schedule_task( "render_3d.scene.complete", [this, object] { - try { consume_completion(object); } + try { + consume_completion(object); + } catch (...) { completion_busy.store(false, std::memory_order_release); fail_datoviz(std::current_exception()); @@ -827,16 +820,15 @@ void Render_Scene_3D::Private::consume_completion(Object* object) { } if (collecting) { if (!collecting->collection_ready.exchange( - false, std::memory_order_acq_rel)) + false, std::memory_order_acq_rel)) continue; collect_datoviz(object, collecting); continue; } - Frame_Context* selected{}; for (auto& context : frame_contexts) { if (context.phase.load(std::memory_order_acquire) != - Frame_Phase::submitted || + Frame_Phase::submitted || !context.collection_completed.load(std::memory_order_acquire)) continue; if (!selected || context.frame->identity().sequence < @@ -846,22 +838,21 @@ void Render_Scene_3D::Private::consume_completion(Object* object) { if (!selected) { completion_busy.store(false, std::memory_order_release); if (std::ranges::any_of(frame_contexts, [](const auto& context) { - return context.collection_ready.load( - std::memory_order_acquire) || - (context.phase.load(std::memory_order_acquire) == - Frame_Phase::submitted && - context.collection_completed.load( - std::memory_order_acquire)); - })) + return context.collection_ready.load( + std::memory_order_acquire) || + (context.phase.load(std::memory_order_acquire) == + Frame_Phase::submitted && + context.collection_completed.load( + std::memory_order_acquire)); + })) arm_completion(object); return; } auto expected = Frame_Phase::submitted; if (!selected->phase.compare_exchange_strong( - expected, Frame_Phase::completing, - std::memory_order_acq_rel, std::memory_order_acquire)) + expected, Frame_Phase::completing, + std::memory_order_acq_rel, std::memory_order_acquire)) continue; - const auto frame = not_null{selected->frame}; frame->mark(Frame_Trace_Marker::paint_finished); frame->mark(Frame_Trace_Marker::scene_render_finished); @@ -919,7 +910,7 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { * 每帧主链只有三个业务阶段: * scene.begin 推进 Camera/Axes、读取本帧参数并取得事件; * scene.renderables 执行各 Visual 的 Prepare/发布; - * scene.backend.submit 把不可变提交数据交给 Datoviz 三槽流水线。 + * scene.backend.prepare 应用数据、生成帧计划并录制命令;真正的 Vulkan submit 随后异步入队。 * GPU 完成和像素读回不挂在这条 Taskflow 上,由 Gpu_Completion_Service 异步回到 completion_graph。 */ if (frame_taskflow) return; @@ -960,7 +951,7 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { renderables.describe("dimension", "3D") .describe("stage", "visual graph") .describe("execution_domain", "Taskflow workers"); - auto submit = graph.add("scene.backend.submit", [this, object] { + auto submit = graph.add("scene.backend.prepare", [this, object] { const auto execution = active_context; if (!execution || !execution->frame) throw std::logic_error("3D submit lost its frame context"); auto context = std::static_pointer_cast< @@ -976,7 +967,7 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { }); submit.describe("dimension", "3D") .describe("backend", "Datoviz") - .describe("stage", "backend submission"); + .describe("stage", "backend prepare and asynchronous submission"); begin.precede(renderables); renderables.precede(submit); } @@ -1043,8 +1034,7 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( double_buffer::detail::Internal_Access::current_prop(object)); if (!prop.view_active) return reject_frame(Render_Result::view_inactive); if (prop.viewport.empty()) return reject_frame(Render_Result::empty_viewport); - if (!backend_available.load(std::memory_order_acquire)) - return reject_frame(Render_Result::backend_unavailable); + if (!backend_available.load(std::memory_order_acquire)) return reject_frame(Render_Result::backend_unavailable); ensure_frame_taskflow(object); } catch (...) { diff --git a/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp b/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp index 09776b2..9b02e63 100644 --- a/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp +++ b/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp @@ -2,6 +2,7 @@ #include "../Render_Scene_3D.hpp" #include "../../detail/Exception.hpp" +#include #include #include #include @@ -139,9 +140,101 @@ public: if (!command) throw std::invalid_argument("empty Datoviz queue command"); if (!submissions_.enqueue(std::move(command))) throw std::bad_alloc{}; submission_generation_.fetch_add(1, std::memory_order_release); - arm_submission_drain(); + try { arm_submission_drain(); } + catch (...) { + /* 命令已经进入唯一队列,不能让调用方按“未提交”路径释放其资源。 */ + std::terminate(); + } + } + void route_immediate_submissions(DvzDrp2RuntimeConfig& configuration) noexcept { + dvz_drp2_runtime_vklite_submit_strategy( + &configuration, &Datoviz_Render_Context::submit_immediate, this); } private: + struct Immediate_Submission final { + explicit Immediate_Submission(VkDevice device_value) noexcept : + device(device_value) {} + ~Immediate_Submission() { + if (fence != VK_NULL_HANDLE) vkDestroyFence(device, fence, nullptr); + } + VkDevice device{VK_NULL_HANDLE}; /* 非拥有;Datoviz_Render_Context 保证其生命周期。 */ + VkFence fence{VK_NULL_HANDLE}; /* 本次即时提交独占所有权。 */ + std::array commands{}; + VkSubmitInfo2 submit{}; + std::atomic_bool submitted{}; + std::atomic_int32_t submission_result{VK_NOT_READY}; + std::atomic_int32_t completion_result{VK_NOT_READY}; + }; + static std::int32_t submit_immediate( + void* user_data, VkQueue queue, + const VkSubmitInfo2* submit_info) noexcept { + auto* context = static_cast(user_data); + if (context == nullptr || queue == VK_NULL_HANDLE || submit_info == nullptr) + return VK_ERROR_INITIALIZATION_FAILED; + try { return context->submit_immediate(queue, *submit_info); } + catch (const std::bad_alloc&) { return VK_ERROR_OUT_OF_HOST_MEMORY; } + catch (...) { return VK_ERROR_INITIALIZATION_FAILED; } + } + std::int32_t submit_immediate( + VkQueue queue, const VkSubmitInfo2& source) { + if (source.commandBufferInfoCount == 0 || + source.commandBufferInfoCount > DVZ_MAX_SWAPCHAIN_IMAGES || + source.waitSemaphoreInfoCount != 0 || + source.signalSemaphoreInfoCount != 0) + return VK_ERROR_INITIALIZATION_FAILED; + auto submission = std::make_shared( + dvz_device_handle(dvz_gpu_ctx_device(gpu_context_))); + std::copy_n(source.pCommandBufferInfos, source.commandBufferInfoCount, + submission->commands.begin()); + submission->submit = source; + submission->submit.pCommandBufferInfos = submission->commands.data(); + const VkFenceCreateInfo fence_info{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + const VkResult fence_result = vkCreateFence( + submission->device, &fence_info, nullptr, &submission->fence); + if (fence_result != VK_SUCCESS) return fence_result; + try { + enqueue_submission([submission, queue]() noexcept { + const VkResult result = vkQueueSubmit2( + queue, 1, &submission->submit, submission->fence); + submission->submission_result.store( + result, std::memory_order_relaxed); + submission->submitted.store(true, std::memory_order_release); + }); + } + catch (...) { + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + try { + /* + * 一个协作等待同时覆盖“排到唯一 VkQueue 提交入口”和“Fence 完成”。 + * corun_until 会让当前 Worker 执行其它就绪节点,观察器把整段标为 + * cooperative wait;命令与 Fence 在返回前仍由 submission 独占。 + */ + Task_Graph::corun_until([submission]() noexcept { + if (!submission->submitted.load(std::memory_order_acquire)) + return false; + const auto submitted = static_cast( + submission->submission_result.load(std::memory_order_relaxed)); + if (submitted != VK_SUCCESS) { + submission->completion_result.store( + submitted, std::memory_order_relaxed); + return true; + } + const VkResult completed = vkGetFenceStatus( + submission->device, submission->fence); + if (completed == VK_NOT_READY) return false; + submission->completion_result.store( + completed, std::memory_order_relaxed); + return true; + }); + } + catch (...) { + /* 已入队命令的栈外资源不能提前释放;违反 Worker 前置条件时立即终止。 */ + std::terminate(); + } + return submission->completion_result.load(std::memory_order_relaxed); + } Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) { DvzGpuCtxConfig configuration = dvz_gpu_ctx_config(); dvz_gpu_ctx_config_validation(&configuration, validation_enabled); diff --git a/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp b/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp index 21b96b6..c13a38e 100644 --- a/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp +++ b/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp @@ -63,6 +63,7 @@ void Scene_Datoviz_State::initialize( dvz_drp2_runtime_vklite_pools( &runtime_configuration, command_pool_, descriptor_pool_); + render_context_->route_immediate_submissions(runtime_configuration); for (auto& slot : runtime_slots_) { slot.runtime = dvz_drp2_runtime_vklite(&runtime_configuration); slot.emitter = dvz_frame_plan_emitter(); diff --git a/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h b/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h index 117b425..ac0b195 100644 --- a/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h +++ b/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h @@ -22,6 +22,7 @@ #include "datoviz/common/macros.h" #include "datoviz/drp2/types.h" +#include "datoviz/vklite/commands.h" @@ -56,6 +57,8 @@ struct DvzDrp2RuntimeConfig DvzVma* allocator; uintptr_t command_pool; uintptr_t descriptor_pool; + DvzCommandSubmitStrategy submit_strategy; + void* submit_user_data; bool semantic_only; }; @@ -89,6 +92,18 @@ DVZ_EXPORT void dvz_drp2_runtime_vklite_pools( DvzDrp2RuntimeConfig* config, uintptr_t command_pool, uintptr_t descriptor_pool); + +/** + * Route immediate DRP2 command batches through the host queue authority. + * + * @param config runtime configuration + * @param strategy required submission strategy + * @param user_data nullable borrowed state that must outlive the runtime + */ +DVZ_EXPORT void dvz_drp2_runtime_vklite_submit_strategy( + DvzDrp2RuntimeConfig* config, DvzCommandSubmitStrategy strategy, void* user_data); + + /** * Return a default external-buffer descriptor. * @@ -125,7 +140,9 @@ DVZ_EXPORT DvzDrp2RuntimeConfig dvz_drp2_runtime_get_config(const DvzDrp2Runtime /** * Destroy a DRP2 runtime. * - * Vklite-backed runtimes wait for submitted device work before releasing owned backend resources. + * Standalone vklite runtimes wait for submitted device work before releasing owned backend + * resources. When a host submit strategy is configured, that strategy owns queue completion and + * the host must retire all borrowed frame work before destroying the runtime. * * @param runtime the runtime */ @@ -138,7 +155,8 @@ DVZ_EXPORT void dvz_drp2_runtime_destroy(DvzDrp2Runtime* runtime); * * This releases runtime-owned objects while keeping the runtime itself and its * borrowed device/allocator configuration alive for reuse. Vklite-backed - * runtimes wait for submitted device work before releasing owned backend resources. + * standalone runtimes wait for submitted device work before releasing owned backend resources. + * A configured host submit strategy owns queue completion and borrowed-frame retirement. * * @param runtime the runtime */ diff --git a/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h b/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h index d7ae746..e2b3dd2 100644 --- a/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h +++ b/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h @@ -39,6 +39,14 @@ typedef struct DvzCommands DvzCommands; typedef struct DvzDevice DvzDevice; typedef struct DvzQueue DvzQueue; +/* + * Submit one immediate command batch through the host application's queue authority. + * user_data is nullable borrowed state and must remain valid until the strategy returns. + * The strategy returns only after the submitted command buffers are safe to release. + */ +typedef int32_t (*DvzCommandSubmitStrategy)( + void* user_data, VkQueue queue, const VkSubmitInfo2* submit_info); + /*************************************************************************************************/ @@ -232,6 +240,22 @@ DVZ_EXPORT int dvz_cmd_submit_result(DvzCommands* cmds); +/** + * Submit a command buffer through an explicitly supplied queue strategy. + * + * This is the integration path for runtimes whose queue is serialized by a host scheduler. + * The strategy must return only after the command buffers are safe to release. + * + * @param cmds the commands wrapper + * @param strategy required submission strategy + * @param user_data nullable borrowed state forwarded to the strategy + * @return 0 on success, non-zero on Vulkan or state failure + */ +DVZ_EXPORT int dvz_cmd_submit_strategy_result( + DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data); + + + /** * Submit a command buffer on its queue. * diff --git a/render_3D/third_party/datoviz/src/drp2/_runtime.h b/render_3D/third_party/datoviz/src/drp2/_runtime.h index d60aa61..928d4bf 100644 --- a/render_3D/third_party/datoviz/src/drp2/_runtime.h +++ b/render_3D/third_party/datoviz/src/drp2/_runtime.h @@ -106,6 +106,8 @@ struct DvzDrp2Runtime DvzVma* allocator; VkCommandPool command_pool; VkDescriptorPool descriptor_pool; + DvzCommandSubmitStrategy submit_strategy; + void* submit_user_data; bool semantic_only; Drp2RuntimeState* semantic_state; #if DVZ_DRP2_HAS_VKLITE @@ -326,7 +328,7 @@ DvzCommands* _vklite_borrowed_frame_commands_create( DvzDevice* device, VkCommandBuffer command_buffer); void _vklite_borrowed_frame_commands_free(DvzCommands* cmds); DvzDrp2ValidationResult _vklite_owned_commands_end_submit( - DvzCommands* cmds, uint32_t command_index); + DvzDrp2Runtime* runtime, DvzCommands* cmds, uint32_t command_index); VkImageLayout _vklite_texture_access_layout(Drp2TextureAccess access); void _vklite_texture_access_scope( Drp2TextureAccess access, VkPipelineStageFlags2* stage, VkAccessFlags2* access_mask); diff --git a/render_3D/third_party/datoviz/src/drp2/backend.c b/render_3D/third_party/datoviz/src/drp2/backend.c index 2b7411e..7d4c09b 100644 --- a/render_3D/third_party/datoviz/src/drp2/backend.c +++ b/render_3D/third_party/datoviz/src/drp2/backend.c @@ -512,12 +512,19 @@ void _vklite_borrowed_frame_commands_free(DvzCommands* cmds) * @return DRP2 validation result */ DvzDrp2ValidationResult -_vklite_owned_commands_end_submit(DvzCommands* cmds, uint32_t command_index) +_vklite_owned_commands_end_submit( + DvzDrp2Runtime* runtime, DvzCommands* cmds, uint32_t command_index) { + ANN(runtime); ANN(cmds); if (dvz_cmd_end_result(cmds) != 0) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); - if (dvz_cmd_submit_result(cmds) != 0) + const int submit_result = runtime->submit_strategy != NULL + ? dvz_cmd_submit_strategy_result( + cmds, runtime->submit_strategy, + runtime->submit_user_data) + : dvz_cmd_submit_result(cmds); + if (submit_result != 0) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); return _drp2_ok(); } diff --git a/render_3D/third_party/datoviz/src/drp2/pass.c b/render_3D/third_party/datoviz/src/drp2/pass.c index 46c8b2d..5968962 100644 --- a/render_3D/third_party/datoviz/src/drp2/pass.c +++ b/render_3D/third_party/datoviz/src/drp2/pass.c @@ -1428,7 +1428,8 @@ DvzDrp2ValidationResult _vklite_resource_barrier( dvz_barrier_buffer_access(bbuf, VK_ACCESS_2_SHADER_WRITE_BIT, dst_access); dvz_cmd_barriers(cmds, &barriers); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(cmds, command_index); + DvzDrp2ValidationResult result = + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); _vklite_owned_commands_destroy(cmds); return result; } @@ -1504,7 +1505,8 @@ _vklite_end_render_pass(Drp2VkliteState* state, uint64_t pass_id, uint32_t comma if (!pass->borrowed_commands) { DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(pass->commands, command_index); + _vklite_owned_commands_end_submit( + state->runtime, pass->commands, command_index); if (!result.ok) { _vklite_destroy_object_slot(state, pass); @@ -1538,7 +1540,8 @@ _vklite_end_compute_pass(Drp2VkliteState* state, uint64_t pass_id, uint32_t comm if (pass == NULL || pass->kind != DRP2_OBJECT_COMPUTE_PASS || pass->commands == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(pass->commands, command_index); + DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit( + state->runtime, pass->commands, command_index); if (!result.ok) { _vklite_destroy_object_slot(state, pass); diff --git a/render_3D/third_party/datoviz/src/drp2/runtime.c b/render_3D/third_party/datoviz/src/drp2/runtime.c index d7fe745..09342ac 100644 --- a/render_3D/third_party/datoviz/src/drp2/runtime.c +++ b/render_3D/third_party/datoviz/src/drp2/runtime.c @@ -115,7 +115,13 @@ static uint32_t _min_u32(uint32_t a, uint32_t b) static void _runtime_wait_backend_idle(DvzDrp2Runtime* runtime) { ANN(runtime); - if (!runtime->semantic_only && runtime->device != NULL) + /* + * A host submit strategy returns only after each immediate batch is complete and owns the + * queue/lifetime retirement boundary. Reintroducing vkDeviceWaitIdle() here would both duplicate + * that authority and block the host thread during otherwise safe destruction. + */ + if (!runtime->semantic_only && runtime->device != NULL && + runtime->submit_strategy == NULL) dvz_device_wait(runtime->device); } #endif @@ -219,6 +225,17 @@ void dvz_drp2_runtime_vklite_pools( +void dvz_drp2_runtime_vklite_submit_strategy( + DvzDrp2RuntimeConfig* config, DvzCommandSubmitStrategy strategy, void* user_data) +{ + ANN(config); + ANN(strategy); + config->submit_strategy = strategy; + config->submit_user_data = user_data; +} + + + DvzDrp2ExternalBufferDesc dvz_drp2_external_buffer_desc(void) { return (DvzDrp2ExternalBufferDesc){ @@ -251,6 +268,8 @@ DvzDrp2Runtime* dvz_drp2_runtime_vklite(const DvzDrp2RuntimeConfig* cfg) runtime->allocator = cfg->allocator; runtime->command_pool = (VkCommandPool)cfg->command_pool; runtime->descriptor_pool = (VkDescriptorPool)cfg->descriptor_pool; + runtime->submit_strategy = cfg->submit_strategy; + runtime->submit_user_data = cfg->submit_user_data; runtime->semantic_only = cfg->semantic_only; return runtime; } @@ -272,6 +291,8 @@ DvzDrp2RuntimeConfig dvz_drp2_runtime_get_config(const DvzDrp2Runtime* runtime) cfg.allocator = runtime->allocator; cfg.command_pool = (uintptr_t)runtime->command_pool; cfg.descriptor_pool = (uintptr_t)runtime->descriptor_pool; + cfg.submit_strategy = runtime->submit_strategy; + cfg.submit_user_data = runtime->submit_user_data; cfg.semantic_only = runtime->semantic_only; return cfg; } diff --git a/render_3D/third_party/datoviz/src/drp2/transfer.c b/render_3D/third_party/datoviz/src/drp2/transfer.c index 668c4c0..ac1fd9f 100644 --- a/render_3D/third_party/datoviz/src/drp2/transfer.c +++ b/render_3D/third_party/datoviz/src/drp2/transfer.c @@ -403,7 +403,8 @@ DvzDrp2ValidationResult _vklite_write_texture( dvz_cmd_copy_buffer_to_image( cmds, dvz_buffer_handle(staging), 0, dvz_image_handle(texture->images, 0), _vklite_texture_access_layout(DRP2_TEXTURE_ACCESS_TRANSFER_WRITE), ®ion); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(cmds, command_index); + DvzDrp2ValidationResult result = + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -446,7 +447,8 @@ DvzDrp2ValidationResult _vklite_copy_buffer_to_buffer( vkCmdCopyBuffer( dvz_commands_handle(cmds), dvz_buffer_handle(src->buffer), dvz_buffer_handle(dst->buffer), 1, ®ion); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(cmds, command_index); + DvzDrp2ValidationResult result = + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -495,7 +497,8 @@ DvzDrp2ValidationResult _vklite_copy_buffer_to_texture( cmds, dvz_buffer_handle(src->buffer), command->u.copy_buffer_to_texture.src_offset, dvz_image_handle(dst->images, 0), _vklite_texture_access_layout(DRP2_TEXTURE_ACCESS_TRANSFER_WRITE), ®ion); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(cmds, command_index); + DvzDrp2ValidationResult result = + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -541,7 +544,8 @@ DvzDrp2ValidationResult _vklite_copy_texture_to_buffer( cmds, dvz_image_handle(src->images, 0), _vklite_texture_access_layout(DRP2_TEXTURE_ACCESS_TRANSFER_READ), ®ion, dvz_buffer_handle(dst->buffer), command->u.copy_texture_to_buffer.dst_offset); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(cmds, command_index); + DvzDrp2ValidationResult result = + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -597,7 +601,8 @@ DvzDrp2ValidationResult _vklite_copy_texture_to_texture( _vklite_transition_image_access(cmds, src, DRP2_TEXTURE_ACCESS_TRANSFER_READ); _vklite_transition_image_access(cmds, dst, DRP2_TEXTURE_ACCESS_TRANSFER_WRITE); dvz_cmd_copy_image(cmds, copy); - DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit(cmds, command_index); + DvzDrp2ValidationResult result = + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); if (!result.ok) { dvz_image_copy_free(copy); diff --git a/render_3D/third_party/datoviz/src/vklite/commands.c b/render_3D/third_party/datoviz/src/vklite/commands.c index 01e368b..b40aaed 100644 --- a/render_3D/third_party/datoviz/src/vklite/commands.c +++ b/render_3D/third_party/datoviz/src/vklite/commands.c @@ -367,7 +367,8 @@ void dvz_cmd_release(DvzCommands* cmds) * @param cmds the commands wrapper * @return 0 on success, non-zero on Vulkan or state failure */ -int dvz_cmd_submit_result(DvzCommands* cmds) +static int _cmd_submit_result( + DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data) { ANN(cmds); ASSERT(cmds->count > 0); @@ -390,9 +391,6 @@ int dvz_cmd_submit_result(DvzCommands* cmds) DvzQueue* queue = cmds->queue; ANN(queue); - // NOTE: inefficient device-level wait. - dvz_device_wait(device); - VkQueue vk_queue = dvz_queue_handle(queue); ANNVK(vk_queue); @@ -410,20 +408,55 @@ int dvz_cmd_submit_result(DvzCommands* cmds) .commandBufferInfoCount = cmds->count, .pCommandBufferInfos = submit_cmds, }; - VkResult res = vkQueueSubmit2(vk_queue, 1, &info, VK_NULL_HANDLE); + VkResult res = VK_SUCCESS; + if (strategy != NULL) + { + res = (VkResult)strategy(user_data, vk_queue, &info); + } + else + { + /* Standalone vklite has no host scheduler. Keep its fallback local to this batch. */ + VkFenceCreateInfo fence_info = {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence = VK_NULL_HANDLE; + res = vkCreateFence(dvz_device_handle(device), &fence_info, NULL, &fence); + if (res == VK_SUCCESS) + res = vkQueueSubmit2(vk_queue, 1, &info, fence); + if (res == VK_SUCCESS) + res = vkWaitForFences(dvz_device_handle(device), 1, &fence, VK_TRUE, UINT64_MAX); + if (fence != VK_NULL_HANDLE) + vkDestroyFence(dvz_device_handle(device), fence, NULL); + } if (res != VK_SUCCESS) { vk_result_check(res, __FILE__, __LINE__); return 1; } - // Wait. - dvz_queue_wait(queue); return 0; } +int dvz_cmd_submit_result(DvzCommands* cmds) +{ + return _cmd_submit_result(cmds, NULL, NULL); +} + + + +int dvz_cmd_submit_strategy_result( + DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data) +{ + if (strategy == NULL) + { + log_error("command submission strategy is required"); + return 1; + } + return _cmd_submit_result(cmds, strategy, user_data); +} + + + /** * Submit a command buffer on its queue. * diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index de5814f..eb4d0b7 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -82,6 +82,7 @@ type Taskflow_Graph_Trace = {stage: string; name: string; submitted_ms: number; type Taskflow_Execution_Trace = {native_id: string; node_id: string; worker_id: number; worker_queue_size: number; worker_queue_capacity: number; ready_ms: number; entered_ms: number; started_ms: number; finished_ms: number; completed_ms: number; duration_ms: number; cooperative_wait_ms: number; + cooperative_waits: Array<{started_ms: number; finished_ms: number}>; observer_entry_ms: number; observer_exit_ms: number; queue_wait_ms: number}; type Frame_Policy_State = { generation: number; @@ -2056,7 +2057,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_ type Taskflow_Timeline_Group = TimelineGroupBase & { node_name: string; worker_id?: number; started_ms?: number; type?: string; }; -type Taskflow_Timeline_Phase = "lifecycle" | "topology" | "queue" | "observer_entry" | "body" | "observer_exit" | "completion_tail"; +type Taskflow_Timeline_Phase = "lifecycle" | "topology" | "queue" | "observer_entry" | "body" | "cooperative_wait" | "observer_exit" | "completion_tail"; type Taskflow_Timeline_Item = TimelineItemBase & { phase: Taskflow_Timeline_Phase; node_id?: string; }; @@ -2234,7 +2235,13 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state, add_item(`${group}-entry`, group, "observer_entry", execution.entered_ms, execution.started_ms, `entry ${milliseconds(execution.observer_entry_ms)}`, node_name, diagnostic_detail, node?.id); add_item(`${group}-body`, group, "body", execution.started_ms, execution.finished_ms, - `执行 ${milliseconds(execution.duration_ms)}`, node_name, diagnostic_detail, node?.id); + `任务墙钟 ${milliseconds(execution.duration_ms)} · Worker 占用 ${milliseconds(Math.max(0, execution.duration_ms - execution.cooperative_wait_ms))}`, + node_name, diagnostic_detail, node?.id); + (execution.cooperative_waits ?? []).forEach((wait, wait_index) => + add_item(`${group}-cooperative-wait-${wait_index}`, group, "cooperative_wait", + wait.started_ms, wait.finished_ms, + `让出 Worker ${milliseconds(Math.max(0, wait.finished_ms - wait.started_ms))}`, + node_name, `${diagnostic_detail}\nTask_Graph::corun_until cooperative wait`, node?.id)); add_item(`${group}-exit`, group, "observer_exit", execution.finished_ms, execution.completed_ms, `exit ${milliseconds(execution.observer_exit_ms)}`, node_name, diagnostic_detail, node?.id); }); diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index 6370a44..bdb9dbd 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -323,6 +323,7 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; } .taskflowTimelineItem-queue { background: #8b5c24 !important; } .taskflowTimelineItem-observer_entry, .taskflowTimelineItem-observer_exit { background: #67468b !important; } .taskflowTimelineItem-body { background: #197462 !important; } +.taskflowTimelineItem-cooperative_wait { background: #d4922f !important; z-index: 4 !important; } .taskflowTimelineItem-completion_tail { background: #78354b !important; } .taskflowTimelineMarker { width: 2px !important; z-index: 8; pointer-events: none; } .taskflowTimelineMarkerSubmitted { background: #e0a65d; }