From 41b81c5c8774f4c578e9a485a42e89651e8aecc4 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 27 Aug 2026 19:58:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=A7=E7=AD=96=E7=95=A5=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project_detail_specification.md | 1 + kernel/src/kernel/Frame_Pacing_Policy.cpp | 39 --- kernel/src/kernel/Frame_Pacing_Policy.hpp | 42 --- .../src/kernel/Frame_Policy/Frame_Policy.cpp | 225 ++++++++++++++++ .../src/kernel/Frame_Policy/Frame_Policy.hpp | 127 +++++++++ .../{ => Frame_Policy}/Frame_Scheduler.cpp | 0 .../{ => Frame_Policy}/Frame_Scheduler.hpp | 0 kernel/src/kernel/frame.cpp | 6 +- kernel/src/kernel/frame.hpp | 11 +- kernel/src/test/render_test.cpp | 41 +-- render_2D/render_2D/base/Frame_2D.cpp | 5 +- render_2D/render_2D/base/Frame_2D.hpp | 3 +- render_3D/render_3D/base/Frame_3D.cpp | 5 +- render_3D/render_3D/base/Frame_3D.hpp | 3 +- .../detail/Gpu_Completion_Service.ipp | 2 +- web_server/src/Plot.cpp | 245 +++++++++++++----- web_server/src/Plot.hpp | 8 +- webapp_gallery/src/app.tsx | 100 ++++++- webapp_gallery/src/styles.css | 22 +- 19 files changed, 685 insertions(+), 200 deletions(-) delete mode 100644 kernel/src/kernel/Frame_Pacing_Policy.cpp delete mode 100644 kernel/src/kernel/Frame_Pacing_Policy.hpp create mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy.cpp create mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy.hpp rename kernel/src/kernel/{ => Frame_Policy}/Frame_Scheduler.cpp (100%) rename kernel/src/kernel/{ => Frame_Policy}/Frame_Scheduler.hpp (100%) diff --git a/Project_detail_specification.md b/Project_detail_specification.md index f9bc0e8..8d88343 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -19,6 +19,7 @@ * `Scene::Private` 是输入事件流的唯一所有者:外部转移事件对象所有权,并发提交到 Def 注册的 MPMC 三缓冲;收集、内部渲染、外部查询分别占用一份队列,Prepare 入口只在交换锁下轮换三者并清空过期查询队列。事件不得独立触发帧。2D 按 Renderable 区域与 Paint 顺序形成接受链,区域默认整个 viewport;3D 无等待提交渲染域,满载时保留当前事件供下一次 Prepare 重试。 * 帧由 Scene 外部创建和持有;每次 `render(frame*)` 只借用该帧并在完成回调返回同一地址。Scene、异步后端和 Web 层只向帧写入固定语义的单调时间点与原始耗时,不保存平均值、分位数或波动等衍生统计。 * Plot 使用服务端帧时钟调用 `Scene::render(frame*)`;`fixed_rate` 只由 Frame_Scheduler 周期时钟驱动,完成回调不得改变其 deadline。`maximum_rate` 是唯一允许完成回调在释放 Plot 准入后异步投递下一帧请求的特殊模式;回调不得直接重入 `render()`、不得同步等待,并且物理帧槽耗尽时只能由帧退役事件解除背压。系统不存在浏览器逐帧 request/ack。Scene 回调返回完成帧后,Web 层在 Render Domain 之外把 2D 原生 BGRA 或 3D 原生 RGBA 编码为 H.264,经 LibDataChannel WebRTC 视频轨直接发布;WebSocket 只承载 SDP/ICE、输入事件和诊断 JSON。多个订阅者共享同一编码结果,关闭视频时 3D 必须使用 diagnostics 输出并跳过 GPU 像素回读。 +* 帧策略及其时钟实现统一归属 `kernel/src/kernel/Frame_Policy`。配置变更和运行事实通过 MPMC 事件流进入唯一 consumer,由 `double_buffer/model.hpp` 发布 `Frame_Policy::State`;查询只读取已发布 State。手动帧捕获必须把帧创建边界的策略 State 绑定到该帧并沿现有帧捕获协议返回,禁止另建策略历史、轮询累加器或重复诊断字段。 * 相机是 3D Scene 组件,只允许定义在 `render_3D/camera`;Kernel 和 2D 不得依赖相机类型。Web 层只为已有 `Camera_3D` 增加协议描述,不复制相机配置。Gallery 服务同一时刻只允许一个页面实例持有;该页面的所有 Plot 连接共享页面令牌,其他标签页或浏览器实例必须被拒绝。 * `Time_Axis` 是时间与 tick 的唯一权威来源;使用层先推进时间轴,再把同一 tick 分发给所有相关数据图元。时间窗口从第一条数据起始终锚定最新 tick,未产生数据的槽位保持背景。 * 图表选区保存两根轴上的数据范围,绘制时才映射为像素;选区作为独立 Renderable 在使用层与图元组合,禁止在各图元内复制选区状态。 diff --git a/kernel/src/kernel/Frame_Pacing_Policy.cpp b/kernel/src/kernel/Frame_Pacing_Policy.cpp deleted file mode 100644 index 6735f01..0000000 --- a/kernel/src/kernel/Frame_Pacing_Policy.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "Frame_Pacing_Policy.hpp" -#include -#include - -namespace aethera { - -Frame_Pacing_Policy::Frame_Pacing_Policy() = default; - -Frame_Pacing_Properties Frame_Pacing_Policy::read() const { - return { - render_enabled_.load(std::memory_order_acquire), - video_enabled_.load(std::memory_order_acquire), - mode_.load(std::memory_order_acquire), - fixed_rate_fps_.load(std::memory_order_acquire)}; -} - -void Frame_Pacing_Policy::set_render_enabled(bool enabled) { - render_enabled_.store(enabled, std::memory_order_release); -} - -void Frame_Pacing_Policy::set_video_enabled(bool enabled) { - video_enabled_.store(enabled, std::memory_order_release); -} - -void Frame_Pacing_Policy::set_mode(Frame_Pacing_Mode mode) { - mode_.store(mode, std::memory_order_release); -} - -void Frame_Pacing_Policy::set_fixed_rate(double fps) { - if (!std::isfinite(fps) || fps <= 0.0) - throw std::invalid_argument("frame pacing fps must be positive"); - fixed_rate_fps_.store(fps, std::memory_order_release); -} - -bool Frame_Pacing_Policy::request_immediate() const noexcept { - return render_enabled_.load(std::memory_order_acquire); -} - -} diff --git a/kernel/src/kernel/Frame_Pacing_Policy.hpp b/kernel/src/kernel/Frame_Pacing_Policy.hpp deleted file mode 100644 index fd26b20..0000000 --- a/kernel/src/kernel/Frame_Pacing_Policy.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include -#include - -namespace aethera { - -enum struct Frame_Pacing_Mode : std::uint8_t { - manual, - fixed_rate, - maximum_rate -}; - -struct Frame_Pacing_Properties { - bool render_enabled{true}; /* 查询时 Plot 是否接受任何帧请求。 */ - bool video_enabled{true}; /* 查询时完成帧是否需要生成视频像素。 */ - Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 查询时服务端帧驱动模式。 */ - double fixed_rate_fps{30.0}; /* fixed_rate 模式目标帧率。 */ -}; - -/* - * 每个 Scene/Plot 独立持有的帧策略。 - * Scheduler 只负责 WHEN;这里负责 WHETHER。 - */ -struct Frame_Pacing_Policy final { - Frame_Pacing_Policy(); - - [[nodiscard]] Frame_Pacing_Properties read() const; - void set_render_enabled(bool enabled); - void set_video_enabled(bool enabled); - void set_mode(Frame_Pacing_Mode mode); - void set_fixed_rate(double fps); - - [[nodiscard]] bool request_immediate() const noexcept; - -private: - std::atomic_bool render_enabled_{true}; /* 当前 Plot 是否接受任何帧请求。 */ - std::atomic_bool video_enabled_{true}; /* 完成帧是否需要生成视频像素。 */ - std::atomic mode_{Frame_Pacing_Mode::fixed_rate}; /* 当前服务端帧驱动模式。 */ - std::atomic fixed_rate_fps_{30.0}; /* fixed_rate 模式目标帧率。 */ -}; - -} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp new file mode 100644 index 0000000..a70a35a --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp @@ -0,0 +1,225 @@ +#include "Frame_Policy.hpp" +#include +#include +#include + +namespace aethera { +namespace { + +std::uint64_t monotonic_nanoseconds( + std::chrono::steady_clock::time_point value = + std::chrono::steady_clock::now()) noexcept { + return static_cast( + std::chrono::duration_cast( + value.time_since_epoch()).count()); +} + +std::uint64_t duration_nanoseconds( + std::chrono::steady_clock::duration value) noexcept { + return static_cast(std::max( + 0, std::chrono::duration_cast(value).count())); +} + +void observe_event(Frame_Policy::State& state, + const detail::Frame_Policy_Event& event) noexcept { + if (event.occurred_ns != 0) { + if (state.observation_started_ns == 0) + state.observation_started_ns = event.occurred_ns; + state.observed_until_ns = std::max( + state.observed_until_ns, event.occurred_ns); + } +} + +void reset_counters(Frame_Policy::State& state, + std::uint64_t occurred_ns) noexcept { + const auto generation = state.generation + 1; + const auto render_enabled = state.render_enabled; + const auto video_enabled = state.video_enabled; + const auto mode = state.mode; + const auto fixed_rate_fps = state.fixed_rate_fps; + state = {}; + state.generation = generation; + state.render_enabled = render_enabled; + state.video_enabled = video_enabled; + state.mode = mode; + state.fixed_rate_fps = fixed_rate_fps; + state.observation_started_ns = occurred_ns; + state.observed_until_ns = occurred_ns; +} + +} + +void Frame_Policy::submit(detail::Frame_Policy_Event event) { + if (event.occurred_ns == 0) event.occurred_ns = monotonic_nanoseconds(); + submit_stream(std::move(event)); +} + +void Frame_Policy::set_render_enabled(bool enabled) { + submit({.type = detail::Frame_Policy_Event_Type::render_enabled, + .enabled = enabled}); +} + +void Frame_Policy::set_video_enabled(bool enabled) { + submit({.type = detail::Frame_Policy_Event_Type::video_enabled, + .enabled = enabled}); +} + +void Frame_Policy::set_mode(Frame_Pacing_Mode mode) { + submit({.type = detail::Frame_Policy_Event_Type::mode, .mode = mode}); +} + +void Frame_Policy::set_fixed_rate(double fps) { + if (!std::isfinite(fps) || fps <= 0.0) + throw std::invalid_argument("frame pacing fps must be positive"); + submit({.type = detail::Frame_Policy_Event_Type::fixed_rate, + .number = fps}); +} + +void Frame_Policy::reset_statistics() { + submit({.type = detail::Frame_Policy_Event_Type::reset}); +} + +void Frame_Policy::record_request( + Frame_Request_Source source, + std::chrono::steady_clock::time_point occurred_at) { + submit({.type = detail::Frame_Policy_Event_Type::request_received, + .source = source, + .occurred_ns = monotonic_nanoseconds(occurred_at)}); +} + +void Frame_Policy::record_request_accepted(Frame_Request_Source source) { + submit({.type = detail::Frame_Policy_Event_Type::request_accepted, + .source = source}); +} + +void Frame_Policy::record_request_coalesced() { + submit({.type = detail::Frame_Policy_Event_Type::request_coalesced}); +} + +void Frame_Policy::record_policy_rejection() { + submit({.type = detail::Frame_Policy_Event_Type::policy_rejected}); +} + +void Frame_Policy::record_frame_slot_backpressure() { + submit({.type = detail::Frame_Policy_Event_Type::frame_slot_backpressure}); +} + +void Frame_Policy::record_scene_rejection() { + submit({.type = detail::Frame_Policy_Event_Type::scene_rejected}); +} + +void Frame_Policy::record_frame_submitted( + std::uint64_t sequence, Frame_Request_Source source, + std::chrono::steady_clock::duration tick_queue_duration) { + submit({.type = detail::Frame_Policy_Event_Type::frame_submitted, + .source = source, + .sequence = sequence, + .duration_ns = duration_nanoseconds(tick_queue_duration)}); +} + +void Frame_Policy::record_frame_completed( + std::uint64_t sequence, + std::chrono::steady_clock::duration completion_latency) { + submit({.type = detail::Frame_Policy_Event_Type::frame_completed, + .sequence = sequence, + .duration_ns = duration_nanoseconds(completion_latency)}); +} + +bool Frame_Policy::consume_events() { + exchange_stream(); + bool consumed{}; + bool configuration_changed{}; + auto& state = double_buffer::detail::Internal_Access::pending_state(this); + access_rendering_stream( + [&](std::span events) { + for (const auto& event : events) { + consumed = true; + observe_event(state, event); + switch (event.type) { + case detail::Frame_Policy_Event_Type::render_enabled: + configuration_changed |= state.render_enabled != event.enabled; + state.render_enabled = event.enabled; + ++state.generation; + break; + case detail::Frame_Policy_Event_Type::video_enabled: + state.video_enabled = event.enabled; + ++state.generation; + break; + case detail::Frame_Policy_Event_Type::mode: + configuration_changed |= state.mode != event.mode; + state.mode = event.mode; + ++state.generation; + break; + case detail::Frame_Policy_Event_Type::fixed_rate: + configuration_changed |= state.fixed_rate_fps != event.number; + state.fixed_rate_fps = event.number; + ++state.generation; + break; + case detail::Frame_Policy_Event_Type::request_received: + ++state.request_count; + if (event.source == Frame_Request_Source::periodic) + ++state.periodic_request_count; + else if (event.source == Frame_Request_Source::immediate) + ++state.immediate_request_count; + else if (event.source == Frame_Request_Source::maximum_rate) + ++state.maximum_rate_request_count; + break; + case detail::Frame_Policy_Event_Type::request_accepted: + ++state.accepted_request_count; + break; + case detail::Frame_Policy_Event_Type::request_coalesced: + ++state.coalesced_request_count; + break; + case detail::Frame_Policy_Event_Type::policy_rejected: + ++state.policy_rejection_count; + break; + case detail::Frame_Policy_Event_Type::frame_slot_backpressure: + ++state.frame_slot_backpressure_count; + break; + case detail::Frame_Policy_Event_Type::scene_rejected: + ++state.scene_rejection_count; + break; + case detail::Frame_Policy_Event_Type::frame_submitted: + ++state.submitted_frame_count; + ++state.active_frame_count; + state.last_frame_sequence = event.sequence; + state.last_request_source = event.source; + state.latest_tick_queue_ns = event.duration_ns; + state.tick_queue_total_ns += event.duration_ns; + state.maximum_tick_queue_ns = std::max( + state.maximum_tick_queue_ns, event.duration_ns); + break; + case detail::Frame_Policy_Event_Type::frame_completed: { + ++state.completed_frame_count; + if (state.active_frame_count != 0) --state.active_frame_count; + state.last_frame_sequence = event.sequence; + state.latest_completion_latency_ns = event.duration_ns; + state.completion_latency_total_ns += event.duration_ns; + state.maximum_completion_latency_ns = std::max( + state.maximum_completion_latency_ns, event.duration_ns); + if (state.first_completion_ns == 0) + state.first_completion_ns = event.occurred_ns; + if (state.last_completion_ns != 0) { + const auto interval = event.occurred_ns - + state.last_completion_ns; + ++state.completion_interval_count; + state.latest_completion_interval_ns = interval; + state.completion_interval_total_ns += interval; + const auto value = static_cast(interval); + state.completion_interval_squared_total_ns2 += + value * value; + } + state.last_completion_ns = event.occurred_ns; + break; + } + case detail::Frame_Policy_Event_Type::reset: + reset_counters(state, event.occurred_ns); + break; + } + } + }); + if (consumed) publish_state(); + return configuration_changed; +} + +} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp new file mode 100644 index 0000000..5e52ae1 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp @@ -0,0 +1,127 @@ +#pragma once +#include "double_buffer/model.hpp" +#include +#include + +namespace aethera { + +enum struct Frame_Pacing_Mode : std::uint8_t { + manual, + fixed_rate, + maximum_rate +}; + +enum struct Frame_Request_Source : std::uint8_t { + unspecified, + periodic, + immediate, + maximum_rate +}; + +namespace detail { +struct Frame_Policy_Event_Stream; +enum struct Frame_Policy_Event_Type : std::uint8_t { + render_enabled, + video_enabled, + mode, + fixed_rate, + request_received, + request_accepted, + request_coalesced, + policy_rejected, + frame_slot_backpressure, + scene_rejected, + frame_submitted, + frame_completed, + reset +}; +struct Frame_Policy_Event { + Frame_Policy_Event_Type type{}; + Frame_Request_Source source{}; + Frame_Pacing_Mode mode{}; + std::uint64_t sequence{}; + std::uint64_t occurred_ns{}; + std::uint64_t duration_ns{}; + double number{}; + bool enabled{}; +}; +} + +/* + * 帧策略的配置、累计事实和发布状态均归属同一个 double-buffer 对象。 + * 任意线程只向 MPMC 事件流提交事实;Plot tick consumer 是唯一归并写者。 + * 查询方只读取 current State,不参与事件归并,也不获取 Plot/Scene 锁。 + */ +struct Frame_Policy final : double_buffer::Def< + Frame_Policy, double_buffer::Root, + double_buffer::Mpmc_Triple_Buffer< + detail::Frame_Policy_Event_Stream, detail::Frame_Policy_Event>> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + std::uint64_t generation{}; + bool render_enabled{true}; + bool video_enabled{true}; + Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; + double fixed_rate_fps{30.0}; + + std::uint64_t observation_started_ns{}; + std::uint64_t observed_until_ns{}; + std::uint64_t request_count{}; + std::uint64_t periodic_request_count{}; + std::uint64_t immediate_request_count{}; + std::uint64_t maximum_rate_request_count{}; + std::uint64_t accepted_request_count{}; + std::uint64_t coalesced_request_count{}; + std::uint64_t policy_rejection_count{}; + std::uint64_t frame_slot_backpressure_count{}; + std::uint64_t scene_rejection_count{}; + std::uint64_t submitted_frame_count{}; + std::uint64_t completed_frame_count{}; + std::uint64_t active_frame_count{}; + + std::uint64_t first_completion_ns{}; + std::uint64_t last_completion_ns{}; + std::uint64_t completion_interval_count{}; + std::uint64_t latest_completion_interval_ns{}; + std::uint64_t completion_interval_total_ns{}; + double completion_interval_squared_total_ns2{}; + std::uint64_t latest_tick_queue_ns{}; + std::uint64_t tick_queue_total_ns{}; + std::uint64_t maximum_tick_queue_ns{}; + std::uint64_t latest_completion_latency_ns{}; + std::uint64_t completion_latency_total_ns{}; + std::uint64_t maximum_completion_latency_ns{}; + std::uint64_t last_frame_sequence{}; + Frame_Request_Source last_request_source{}; + bool operator==(const State&) const = default; + }; + struct Private : Prev_Private {}; + + void set_render_enabled(bool enabled); + void set_video_enabled(bool enabled); + void set_mode(Frame_Pacing_Mode mode); + void set_fixed_rate(double fps); + void reset_statistics(); + + void record_request(Frame_Request_Source source, + std::chrono::steady_clock::time_point occurred_at); + void record_request_accepted(Frame_Request_Source source); + void record_request_coalesced(); + void record_policy_rejection(); + void record_frame_slot_backpressure(); + void record_scene_rejection(); + void record_frame_submitted( + std::uint64_t sequence, Frame_Request_Source source, + std::chrono::steady_clock::duration tick_queue_duration); + void record_frame_completed( + std::uint64_t sequence, + std::chrono::steady_clock::duration completion_latency); + + /* 唯一消费者在短任务中归并全部待处理事件并发布 State。 */ + [[nodiscard]] bool consume_events(); + +private: + void submit(detail::Frame_Policy_Event event); +}; + +} diff --git a/kernel/src/kernel/Frame_Scheduler.cpp b/kernel/src/kernel/Frame_Policy/Frame_Scheduler.cpp similarity index 100% rename from kernel/src/kernel/Frame_Scheduler.cpp rename to kernel/src/kernel/Frame_Policy/Frame_Scheduler.cpp diff --git a/kernel/src/kernel/Frame_Scheduler.hpp b/kernel/src/kernel/Frame_Policy/Frame_Scheduler.hpp similarity index 100% rename from kernel/src/kernel/Frame_Scheduler.hpp rename to kernel/src/kernel/Frame_Policy/Frame_Scheduler.hpp diff --git a/kernel/src/kernel/frame.cpp b/kernel/src/kernel/frame.cpp index 265fada..a0ed490 100644 --- a/kernel/src/kernel/frame.cpp +++ b/kernel/src/kernel/frame.cpp @@ -25,6 +25,7 @@ struct Render_Frame::Private { }; Frame_Identity identity{}; /* 外部帧管理器提供且终生不变的帧身份。 */ Frame_Request_Source request_source{}; /* 当前逻辑帧的策略触发来源。 */ + Frame_Policy::State frame_policy{}; /* 创建逻辑帧时读取的已发布策略状态。 */ std::chrono::steady_clock::time_point created_at{}; /* 所有 elapsed_ns 使用的单调时钟原点。 */ std::uint64_t created_time_unix_ns{}; /* 用于跨进程展示的创建 Unix 时间,单位为纳秒。 */ std::array markers{}; /* 每种时间点首次出现时的 elapsed_ns 加一编码。 */ @@ -37,9 +38,11 @@ Render_Frame::Render_Frame(Frame_Identity identity) : d(std::make_uniqueidentity = identity; d->request_source = source; + d->frame_policy = frame_policy; d->created_at = std::chrono::steady_clock::now(); const auto system_now = std::chrono::system_clock::now().time_since_epoch(); d->created_time_unix_ns = static_cast(std::chrono::duration_cast(system_now).count()); @@ -158,6 +161,7 @@ Taskflow_Frame_Trace Render_Frame::take_taskflow_trace() { Taskflow_Frame_Trace result{}; result.identity = d->identity; result.request_source = d->request_source; + result.frame_policy = d->frame_policy; result.created_time_unix_ns = d->created_time_unix_ns; result.worker_count = d->taskflow_workers.size(); for (std::size_t index = 0; index < marker_count; ++index) { diff --git a/kernel/src/kernel/frame.hpp b/kernel/src/kernel/frame.hpp index e518f8b..aae8a11 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -1,4 +1,5 @@ #pragma once +#include "Frame_Policy/Frame_Policy.hpp" #include "double_buffer/mechanism.hpp" #include #include @@ -9,12 +10,6 @@ namespace detail { struct Taskflow_Frame_Access; } struct Frame_Statistics_Sample; -enum struct Frame_Request_Source : std::uint8_t { - unspecified, - periodic, - immediate, - maximum_rate -}; enum struct Frame_Trace_Marker : std::uint8_t { created, plot_update_started, @@ -128,6 +123,7 @@ struct Taskflow_Task_Trace { struct Taskflow_Frame_Trace { Frame_Identity identity{}; /* 该执行图所属逻辑渲染帧。 */ Frame_Request_Source request_source{}; /* 该逻辑帧由周期、手动或最大吞吐策略触发。 */ + Frame_Policy::State frame_policy{}; /* 帧创建边界读取的已发布策略状态。 */ std::uint64_t created_time_unix_ns{}; /* 帧创建 Unix 时间,单位纳秒。 */ std::size_t worker_count{}; /* 捕获时全局 Executor 的 worker 数。 */ std::vector markers{}; /* 与该帧 DAG 共用时间原点的原始流水线时间点。 */ @@ -150,7 +146,8 @@ public: protected: /* 物理帧槽再次承载新逻辑帧时,重建其唯一身份和诊断时间原点。 */ void begin(Frame_Identity identity, - Frame_Request_Source source = Frame_Request_Source::unspecified) noexcept; + Frame_Request_Source source = Frame_Request_Source::unspecified, + const Frame_Policy::State& frame_policy = {}) noexcept; private: struct Private; friend struct detail::Taskflow_Frame_Access; diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index e4150e3..7baccac 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -1,5 +1,5 @@ #include "scene.hpp" -#include "Frame_Pacing_Policy.hpp" +#include "Frame_Policy/Frame_Policy.hpp" #include #include #include @@ -52,22 +52,31 @@ inline Direct_Renderable::Private& Direct_Renderable::data_for_test() { return static_cast(*d); } -TEST(frame_pacing_policy, independent_properties_do_not_publish_copied_versions) { - aethera::Frame_Pacing_Policy policy; - policy.set_mode(aethera::Frame_Pacing_Mode::maximum_rate); - policy.set_fixed_rate(47.5); - policy.set_video_enabled(false); +TEST(frame_policy, mpmc_events_publish_one_double_buffer_state) { + auto built = aethera::Frame_Policy::Builder{}.build(); + ASSERT_TRUE(built.has_value()); + auto policy = std::move(*built); + policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); + policy->set_fixed_rate(47.5); + policy->set_video_enabled(false); + policy->record_request(aethera::Frame_Request_Source::maximum_rate, + std::chrono::steady_clock::now()); + policy->record_request_accepted(aethera::Frame_Request_Source::maximum_rate); + policy->record_frame_submitted( + 7, aethera::Frame_Request_Source::maximum_rate, + std::chrono::microseconds{250}); + EXPECT_TRUE(policy->consume_events()); - const auto properties = policy.read(); - EXPECT_TRUE(properties.render_enabled); - EXPECT_FALSE(properties.video_enabled); - EXPECT_EQ(properties.mode, aethera::Frame_Pacing_Mode::maximum_rate); - EXPECT_DOUBLE_EQ(properties.fixed_rate_fps, 47.5); - EXPECT_TRUE(policy.request_immediate()); - - policy.set_render_enabled(false); - EXPECT_FALSE(policy.request_immediate()); - EXPECT_THROW(policy.set_fixed_rate(0.0), std::invalid_argument); + const auto& state = policy->read_state(); + EXPECT_TRUE(state.render_enabled); + EXPECT_FALSE(state.video_enabled); + EXPECT_EQ(state.mode, aethera::Frame_Pacing_Mode::maximum_rate); + EXPECT_DOUBLE_EQ(state.fixed_rate_fps, 47.5); + EXPECT_EQ(state.request_count, 1); + EXPECT_EQ(state.accepted_request_count, 1); + EXPECT_EQ(state.submitted_frame_count, 1); + EXPECT_EQ(state.latest_tick_queue_ns, 250'000); + EXPECT_THROW(policy->set_fixed_rate(0.0), std::invalid_argument); } inline Graph_Renderable::Private& Graph_Renderable::data_for_test() { return static_cast(*d); diff --git a/render_2D/render_2D/base/Frame_2D.cpp b/render_2D/render_2D/base/Frame_2D.cpp index 570989b..5221e4f 100644 --- a/render_2D/render_2D/base/Frame_2D.cpp +++ b/render_2D/render_2D/base/Frame_2D.cpp @@ -11,11 +11,12 @@ Frame_2D::Frame_2D(Frame_Identity identity, Pixel_Format output_format) begin(identity, output_format); } void Frame_2D::begin(Frame_Identity identity, Pixel_Format output_format, - Frame_Request_Source source) { + Frame_Request_Source source, + const Frame_Policy::State& frame_policy) { if (std::ranges::find(supported_pixel_formats, output_format) == supported_pixel_formats.end()) throw std::invalid_argument("Frame_2D output pixel format is unsupported"); - Render_Frame::begin(identity, source); + Render_Frame::begin(identity, source, frame_policy); d->output_format = output_format; } Frame_2D::~Frame_2D() = default; diff --git a/render_2D/render_2D/base/Frame_2D.hpp b/render_2D/render_2D/base/Frame_2D.hpp index 4c55ad9..8c8402e 100644 --- a/render_2D/render_2D/base/Frame_2D.hpp +++ b/render_2D/render_2D/base/Frame_2D.hpp @@ -24,7 +24,8 @@ public: /* 保留二维后端图像分配,只重启物理槽承载的新逻辑帧。 */ void begin(Frame_Identity identity, Pixel_Format output_format = native_pixel_format, - Frame_Request_Source source = Frame_Request_Source::unspecified); + Frame_Request_Source source = Frame_Request_Source::unspecified, + const Frame_Policy::State& frame_policy = {}); [[nodiscard]] Pixel_Format output_format() const noexcept; [[nodiscard]] Image_View image() const; /* 按本帧声明的输出格式生成连续像素;原生格式只做必要的行打包。 */ diff --git a/render_3D/render_3D/base/Frame_3D.cpp b/render_3D/render_3D/base/Frame_3D.cpp index bc66fff..b8ecd3b 100644 --- a/render_3D/render_3D/base/Frame_3D.cpp +++ b/render_3D/render_3D/base/Frame_3D.cpp @@ -14,10 +14,11 @@ Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output, begin(identity, output, output_format); } void Frame_3D::begin(Frame_Identity identity, Frame_3D_Output output, - Pixel_Format output_format, Frame_Request_Source source) { + Pixel_Format output_format, Frame_Request_Source source, + const Frame_Policy::State& frame_policy) { if (output_format != native_pixel_format) throw std::invalid_argument("Frame_3D output pixel format is unsupported"); - Render_Frame::begin(identity, source); + Render_Frame::begin(identity, source, frame_policy); d->output = output; d->output_format = output_format; d->extent = {}; diff --git a/render_3D/render_3D/base/Frame_3D.hpp b/render_3D/render_3D/base/Frame_3D.hpp index 8c77c07..4eadfcd 100644 --- a/render_3D/render_3D/base/Frame_3D.hpp +++ b/render_3D/render_3D/base/Frame_3D.hpp @@ -31,7 +31,8 @@ public: void begin(Frame_Identity identity, Frame_3D_Output output = Frame_3D_Output::pixels, Pixel_Format output_format = native_pixel_format, - Frame_Request_Source source = Frame_Request_Source::unspecified); + Frame_Request_Source source = Frame_Request_Source::unspecified, + const Frame_Policy::State& frame_policy = {}); [[nodiscard]] Extent extent() const noexcept; [[nodiscard]] std::span pixels() const noexcept; [[nodiscard]] std::shared_ptr> share_pixels() const noexcept; diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.ipp b/render_3D/render_3D/detail/Gpu_Completion_Service.ipp index 52ad18b..63cad26 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.ipp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.ipp @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include #include diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index e053796..a906553 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -1,8 +1,8 @@ #include "Plot.hpp" #include "Renderable_Adapter.hpp" #include "Taskflow_Trace_Json.hpp" -#include -#include +#include +#include #include #include #include @@ -76,8 +76,8 @@ std::string_view pixel_format_name(render_3d::Pixel_Format format) { throw std::logic_error("unknown 3D pixel format"); } -nlohmann::json frame_policy_schema(const Frame_Pacing_Policy& pacing) { - const auto current = pacing.read(); +nlohmann::json frame_policy_schema(const Frame_Policy& policy) { + const auto& current = policy.read_state(); return { {"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"}, {"fields", nlohmann::json::array({ @@ -107,7 +107,7 @@ nlohmann::json frame_policy_schema(const Frame_Pacing_Policy& pacing) { }; } -nlohmann::json write_frame_policy_prop(Frame_Pacing_Policy& pacing, +nlohmann::json write_frame_policy_prop(Frame_Policy& pacing, std::string_view key, const nlohmann::json& value) { if (key == "render_enabled" || key == "video_enabled") { @@ -142,6 +142,101 @@ nlohmann::json write_frame_policy_prop(Frame_Pacing_Policy& pacing, return {{"success", false}, {"error", "unknown frame runtime property"}}; } +nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { + const auto milliseconds = [](std::uint64_t nanoseconds) { + return static_cast(nanoseconds) / 1'000'000.0; + }; + const auto ratio = [](std::uint64_t numerator, std::uint64_t denominator) { + return denominator == 0 ? 0.0 : + static_cast(numerator) / static_cast(denominator); + }; + const auto observed_ns = state.observed_until_ns > state.observation_started_ns + ? state.observed_until_ns - state.observation_started_ns : 0U; + const auto observed_seconds = static_cast(observed_ns) / 1'000'000'000.0; + const auto rate = [observed_seconds](std::uint64_t count) { + return observed_seconds > 0.0 + ? static_cast(count) / observed_seconds : 0.0; + }; + const auto completion_span = state.last_completion_ns > state.first_completion_ns + ? state.last_completion_ns - state.first_completion_ns : 0U; + const auto effective_fps = completion_span != 0 && state.completed_frame_count > 1 + ? static_cast(state.completed_frame_count - 1U) * + 1'000'000'000.0 / static_cast(completion_span) + : 0.0; + const auto interval_mean = state.completion_interval_count == 0 ? 0.0 : + static_cast(state.completion_interval_total_ns) / + static_cast(state.completion_interval_count); + const auto interval_variance = state.completion_interval_count == 0 ? 0.0 : + std::max(0.0, + state.completion_interval_squared_total_ns2 / + static_cast(state.completion_interval_count) - + interval_mean * interval_mean); + const auto target_achievement = + state.mode == Frame_Pacing_Mode::fixed_rate && state.fixed_rate_fps > 0.0 + ? effective_fps / state.fixed_rate_fps : 0.0; + return { + {"generation", state.generation}, + {"configuration", { + {"mode", pacing_mode_name(state.mode)}, + {"render_enabled", state.render_enabled}, + {"video_enabled", state.video_enabled}, + {"fixed_rate_fps", state.fixed_rate_fps}}}, + {"observation", { + {"duration_ms", milliseconds(observed_ns)}, + {"request_count", state.request_count}, + {"submitted_frame_count", state.submitted_frame_count}, + {"completed_frame_count", state.completed_frame_count}, + {"active_frame_count", state.active_frame_count}}}, + {"throughput", { + {"request_rate_fps", rate(state.request_count)}, + {"submission_rate_fps", rate(state.submitted_frame_count)}, + {"completion_rate_fps", effective_fps}, + {"target_achievement_ratio", target_achievement}, + {"latest_frame_interval_ms", + milliseconds(state.latest_completion_interval_ns)}, + {"average_frame_interval_ms", milliseconds( + state.completion_interval_count == 0 ? 0U : + state.completion_interval_total_ns / + state.completion_interval_count)}, + {"frame_interval_jitter_ms", + std::sqrt(interval_variance) / 1'000'000.0}}}, + {"requests", { + {"periodic", state.periodic_request_count}, + {"immediate", state.immediate_request_count}, + {"maximum_rate", state.maximum_rate_request_count}, + {"accepted", state.accepted_request_count}, + {"coalesced", state.coalesced_request_count}, + {"policy_rejected", state.policy_rejection_count}, + {"frame_slot_backpressure", state.frame_slot_backpressure_count}, + {"scene_rejected", state.scene_rejection_count}, + {"acceptance_ratio", ratio( + state.accepted_request_count, state.request_count)}, + {"coalescing_ratio", ratio( + state.coalesced_request_count, state.request_count)}, + {"backpressure_ratio", ratio( + state.frame_slot_backpressure_count, + state.accepted_request_count)}}}, + {"latency", { + {"latest_tick_queue_ms", milliseconds(state.latest_tick_queue_ns)}, + {"average_tick_queue_ms", milliseconds( + state.submitted_frame_count == 0 ? 0U : + state.tick_queue_total_ns / state.submitted_frame_count)}, + {"maximum_tick_queue_ms", milliseconds(state.maximum_tick_queue_ns)}, + {"latest_completion_ms", milliseconds( + state.latest_completion_latency_ns)}, + {"average_completion_ms", milliseconds( + state.completed_frame_count == 0 ? 0U : + state.completion_latency_total_ns / + state.completed_frame_count)}, + {"maximum_completion_ms", milliseconds( + state.maximum_completion_latency_ns)}}}, + {"last_frame", { + {"sequence", state.last_frame_sequence}, + {"request_source", magic_enum::enum_name( + state.last_request_source)}}} + }; +} + void append_statistic_json(nlohmann::json& output, const Frame_Statistics_State& state) { for (const auto statistic : magic_enum::enum_values()) { @@ -260,6 +355,7 @@ nlohmann::json taskflow_trace_json( {"sequence", trace.identity.sequence}, {"correlation_id", trace.identity.correlation_id}, {"request_source", magic_enum::enum_name(trace.request_source)}, + {"frame_policy", frame_policy_state_json(trace.frame_policy)}, {"created_time_unix_ns", trace.created_time_unix_ns}, {"worker_count", trace.worker_count}, {"markers", std::move(markers)}, @@ -337,6 +433,8 @@ struct Plot::Private { struct Managed_Frame { std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */ + std::chrono::steady_clock::time_point tick_issued_at{}; /* 本逻辑帧请求进入 Plot 的时刻。 */ + std::chrono::steady_clock::time_point submitted_at{}; /* Scene 接受本逻辑帧的时刻。 */ Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */ std::atomic state{Frame_State::available}; /* 本槽唯一生命周期状态。 */ std::atomic_size_t diagnostic_readers{}; /* 无锁诊断读取认领;非零时该槽不可复用。 */ @@ -366,7 +464,7 @@ struct Plot::Private { std::atomic_uint64_t next_stream_id{1}; std::atomic> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */ std::uint64_t next_frame_sequence{1}; - Frame_Pacing_Policy frame_policy{}; + std::unique_ptr frame_policy{}; Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */ static constexpr std::size_t scene_frame_capacity{3}; std::array frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */ @@ -380,12 +478,6 @@ struct Plot::Private { std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */ std::atomic render_admission{Render_Admission_State::ready}; /* Plot 渲染准入及物理槽背压的唯一状态源。 */ std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; - std::atomic_uint64_t received_tick_count{}; /* 页面时钟交付给本 Plot 的 tick 总数。 */ - std::atomic_uint64_t coalesced_tick_count{}; /* 尚未消费时被更新 tick 替换的旧 tick 总数。 */ - std::atomic_uint64_t policy_skip_count{}; /* 帧策略拒绝的 tick 总数。 */ - std::atomic_uint64_t frame_slot_busy_count{}; /* 三个物理帧槽均被占用的提交次数。 */ - std::atomic_uint64_t scene_rejection_count{}; /* Scene 单帧准入拒绝的提交次数。 */ - std::atomic_uint64_t submitted_frame_count{}; /* 成功提交给 Scene 的帧总数。 */ std::atomic_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */ static constexpr std::size_t maximum_taskflow_trace_frames{120}; /* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace, @@ -411,6 +503,10 @@ struct Plot::Private { Private(std::unique_ptr value_scene, std::unique_ptr value_view) : view(std::move(value_view)), scene(std::move(value_scene)) { + auto built_policy = Frame_Policy::Builder{}.build(); + if (!built_policy) + throw std::logic_error("Frame Policy dependency graph is invalid"); + frame_policy = std::move(*built_policy); for (auto& slot : frame_slots) { if constexpr (std::same_as) slot.frame = std::make_unique(Frame_Identity{}); @@ -483,7 +579,7 @@ void Plot::Private::fail(std::exception_ptr failure) noexcept { nlohmann::json Plot::Private::schema() const { auto result = view->schema(); - auto analysis = frame_policy_schema(frame_policy); + auto analysis = frame_policy_schema(*frame_policy); const auto generator = view->data_generator_schema(); if (!generator.is_null()) analysis["data_generator"] = generator; result["frame_analysis"] = std::move(analysis); @@ -537,7 +633,7 @@ void Plot::Private::publish( void Plot::Private::refresh_schedule() { if (!frame_timer.valid()) return; const auto current_consumers = consumers.load(std::memory_order_acquire); - const auto pacing = frame_policy.read(); + const auto& pacing = frame_policy->read_state(); if (!pacing.render_enabled || current_consumers->empty()) { frame_timer.cancel(); return; @@ -553,29 +649,33 @@ void Plot::Private::refresh_schedule() { .issued_at = now, .time_milliseconds = std::chrono::duration( now - clock_origin).count(), - .source = Plot_Render_Tick_Source::maximum_rate}); + .source = Frame_Request_Source::maximum_rate}); arm_tick_consumer(lifetime); } void Plot::Private::submit_tick_request(Plot_Render_Tick tick) { + const auto source = tick.source; + const auto issued_at = tick.issued_at; if (!tick_requests.enqueue(std::move(tick))) throw std::bad_alloc{}; + frame_policy->record_request(source, issued_at); tick_request_generation.fetch_add(1, std::memory_order_release); } void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) { - const auto priority = [](Plot_Render_Tick_Source source) { + const auto priority = [](Frame_Request_Source source) { switch (source) { - case Plot_Render_Tick_Source::periodic: return 0; - case Plot_Render_Tick_Source::maximum_rate: return 1; - case Plot_Render_Tick_Source::immediate: return 2; + case Frame_Request_Source::unspecified: return 0; + case Frame_Request_Source::periodic: return 0; + case Frame_Request_Source::maximum_rate: return 1; + case Frame_Request_Source::immediate: return 2; } return 0; }; if (deferred_tick) { const auto current_priority = priority(deferred_tick->source); const auto next_priority = priority(tick.source); - coalesced_tick_count.fetch_add(1, std::memory_order_relaxed); + frame_policy->record_request_coalesced(); if (current_priority > next_priority || (current_priority == next_priority && deferred_tick->issued_at >= tick.issued_at)) @@ -601,7 +701,7 @@ void Plot::Private::release_render_admission(std::weak_ptr lifetime) { expected, Render_Admission_State::ready, std::memory_order_acq_rel, std::memory_order_acquire)) return; - const auto pacing = frame_policy.read(); + const auto& pacing = frame_policy->read_state(); if (pacing.render_enabled && pacing.mode == Frame_Pacing_Mode::maximum_rate) { const auto current_consumers = consumers.load(std::memory_order_acquire); if (!current_consumers->empty()) { @@ -610,13 +710,14 @@ void Plot::Private::release_render_admission(std::weak_ptr lifetime) { .issued_at = now, .time_milliseconds = std::chrono::duration( now - clock_origin).count(), - .source = Plot_Render_Tick_Source::maximum_rate}); + .source = Frame_Request_Source::maximum_rate}); } } arm_tick_consumer(std::move(lifetime)); } void Plot::Private::consume_tick(std::weak_ptr lifetime) { + if (frame_policy->consume_events()) refresh_schedule(); const auto observed_generation = tick_request_generation.load(std::memory_order_acquire); Plot_Render_Tick requested; @@ -627,6 +728,7 @@ void Plot::Private::consume_tick(std::weak_ptr lifetime) { auto tick = std::exchange(deferred_tick, {}); if (tick) clock_tick(*tick); } + if (frame_policy->consume_events()) refresh_schedule(); tick_task_scheduled.store(false, std::memory_order_release); if (tick_request_generation.load(std::memory_order_acquire) != observed_generation || @@ -637,17 +739,18 @@ void Plot::Private::consume_tick(std::weak_ptr lifetime) { void Plot::Private::clock_tick(const Plot_Render_Tick& tick) { if (terminal_failure.load(std::memory_order_acquire)) return; - const auto pacing = frame_policy.read(); + const auto& pacing = frame_policy->read_state(); const bool accepted = pacing.render_enabled && - (tick.source == Plot_Render_Tick_Source::immediate || - (tick.source == Plot_Render_Tick_Source::periodic && + (tick.source == Frame_Request_Source::immediate || + (tick.source == Frame_Request_Source::periodic && pacing.mode == Frame_Pacing_Mode::fixed_rate) || - (tick.source == Plot_Render_Tick_Source::maximum_rate && + (tick.source == Frame_Request_Source::maximum_rate && pacing.mode == Frame_Pacing_Mode::maximum_rate)); if (!accepted) { - policy_skip_count.fetch_add(1, std::memory_order_relaxed); + frame_policy->record_policy_rejection(); return; } + frame_policy->record_request_accepted(tick.source); render_frame(tick); } @@ -722,7 +825,7 @@ nlohmann::json Plot::Private::trace_response( void Plot::Private::render_frame(Plot_Render_Tick tick) { if (terminal_failure.load(std::memory_order_acquire)) return; const auto streams = stream_snapshot(); - const auto pacing = frame_policy.read(); + const auto& pacing = frame_policy->read_state(); if (!pacing.render_enabled || streams.consumers->empty()) return; auto admission_expected = Render_Admission_State::ready; @@ -773,7 +876,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { break; } if (!managed) { - frame_slot_busy_count.fetch_add(1, std::memory_order_relaxed); + frame_policy->record_frame_slot_backpressure(); keep_latest_tick(tick); /* * 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能 @@ -789,6 +892,8 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { managed->presentation_time = std::chrono::duration_cast( std::chrono::duration(tick.time_milliseconds)); + managed->tick_issued_at = tick.issued_at; + managed->submitted_at = {}; const auto rollback_unsubmitted = [this, slot_index] { auto& slot = frame_slots[slot_index]; auto expected = Frame_State::rendering; @@ -808,28 +913,21 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { const std::uint64_t sequence = next_frame_sequence++; const Frame_Identity identity{ sequence, tick.sequence == 0 ? sequence : tick.sequence}; - const auto request_source = [&] { - switch (tick.source) { - case Plot_Render_Tick_Source::periodic: - return Frame_Request_Source::periodic; - case Plot_Render_Tick_Source::immediate: - return Frame_Request_Source::immediate; - case Plot_Render_Tick_Source::maximum_rate: - return Frame_Request_Source::maximum_rate; - } - return Frame_Request_Source::unspecified; - }(); + const auto request_source = tick.source; + const auto policy_state = + frame_policy->read_state(); Render_Frame* logical_frame{}; if (auto* frame_2d = std::get_if>(&managed->frame)) { (*frame_2d)->begin(identity, Frame_2D::native_pixel_format, - request_source); + request_source, policy_state); logical_frame = frame_2d->get(); } else { auto& frame_3d = std::get>(managed->frame); frame_3d->begin(identity, pacing.video_enabled ? Frame_3D_Output::pixels : Frame_3D_Output::diagnostics, - Frame_3D::native_pixel_format, request_source); + Frame_3D::native_pixel_format, request_source, + policy_state); logical_frame = frame_3d.get(); } taskflow_trace_claimed = mark_taskflow_trace(*logical_frame); @@ -864,13 +962,16 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { Size{static_cast(tick.width), static_cast(tick.height)}); const auto result = (*scene_2d)->render(&output); if (!result) { - scene_rejection_count.fetch_add(1, std::memory_order_relaxed); + frame_policy->record_scene_rejection(); rollback_unsubmitted(); restore_taskflow_trace_claim(); release_render_admission(lifetime); } else { taskflow_trace_claimed = false; - submitted_frame_count.fetch_add(1, std::memory_order_relaxed); + managed->submitted_at = std::chrono::steady_clock::now(); + frame_policy->record_frame_submitted( + sequence, request_source, + managed->submitted_at - managed->tick_issued_at); } return; } @@ -881,10 +982,13 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { const auto result = scene_3d->render(&output); if (result == Render_Scene_3D::Render_Result::submitted) { taskflow_trace_claimed = false; - submitted_frame_count.fetch_add(1, std::memory_order_relaxed); + managed->submitted_at = std::chrono::steady_clock::now(); + frame_policy->record_frame_submitted( + sequence, request_source, + managed->submitted_at - managed->tick_issued_at); return; } - scene_rejection_count.fetch_add(1, std::memory_order_relaxed); + frame_policy->record_scene_rejection(); rollback_unsubmitted(); restore_taskflow_trace_claim(); release_render_admission(lifetime); @@ -918,7 +1022,8 @@ void Plot::Private::publish_completed_frame() { throw std::logic_error("Scene completion graph has no rendering Plot frame"); try { - const auto pacing = frame_policy.read(); + const auto& pacing = + frame_policy->read_state(); const auto identity = frame->identity(); Frame_Identity rendered_identity = identity; std::shared_ptr> pixel_storage; @@ -1132,6 +1237,12 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) { if (!managed) throw std::logic_error("retired frame has no owned Plot slot"); + frame_policy->record_frame_completed( + frame->identity().sequence, + managed->submitted_at.time_since_epoch().count() == 0 + ? std::chrono::steady_clock::duration::zero() + : std::chrono::steady_clock::now() - managed->submitted_at); + const auto statistics_generation_value = statistics_generation.load(std::memory_order_acquire); if (applied_statistics_generation != statistics_generation_value) { @@ -1173,10 +1284,10 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) { /* 物理槽是唯一背压原因;归还任意槽后只解除一次耗尽状态。 */ auto admission = Render_Admission_State::frame_slots_exhausted; - if (render_admission.compare_exchange_strong( - admission, Render_Admission_State::ready, - std::memory_order_acq_rel, std::memory_order_acquire)) - arm_tick_consumer(lifetime); + static_cast(render_admission.compare_exchange_strong( + admission, Render_Admission_State::ready, + std::memory_order_acq_rel, std::memory_order_acquire)); + arm_tick_consumer(lifetime); if (captured_trace) { auto owner = lifetime; @@ -1229,7 +1340,7 @@ void Plot::ensure_started() { .issued_at = tick.issued_at, .sequence = tick.sequence, .time_milliseconds = tick.time_milliseconds, - .source = Plot_Render_Tick_Source::periodic}); + .source = Frame_Request_Source::periodic}); } }); /* @@ -1337,21 +1448,19 @@ void Plot::configure_stream(Stream_Id stream, std::uint32_t width, void Plot::schedule_render(Plot_Render_Tick tick) { ensure_started(); if (d->terminal_failure.load(std::memory_order_acquire)) return; - d->received_tick_count.fetch_add(1, std::memory_order_relaxed); d->submit_tick_request(std::move(tick)); d->arm_tick_consumer(weak_from_this()); } void Plot::render_once() { ensure_started(); - if (!d->frame_policy.request_immediate()) return; const auto now = std::chrono::steady_clock::now(); const auto elapsed = now - d->clock_origin; schedule_render(Plot_Render_Tick{ .issued_at = now, .time_milliseconds = std::chrono::duration(elapsed).count(), - .source = Plot_Render_Tick_Source::immediate}); + .source = Frame_Request_Source::immediate}); } void Plot::submit_input(Plot_Input_Event event) { @@ -1385,8 +1494,9 @@ nlohmann::json Plot::write_prop(std::string_view component, ensure_started(); if (component != "frame-analysis") return d->view->write_prop(component, key, value); - auto result = write_frame_policy_prop(d->frame_policy, key, value); - if (result.value("success", false)) d->refresh_schedule(); + auto result = write_frame_policy_prop(*d->frame_policy, key, value); + if (result.value("success", false)) + d->arm_tick_consumer(weak_from_this()); return result; } @@ -1450,7 +1560,8 @@ nlohmann::json Plot::diagnostics() const { ? 1'000.0 / interval.trimmed_average : 0.0; } - const auto pacing = d->frame_policy.read(); + const auto& pacing = + d->frame_policy->read_state(); const auto stream = d->stream_snapshot(); const auto admission = d->render_admission.load(std::memory_order_acquire); const auto admission_name = [&] { @@ -1479,7 +1590,7 @@ nlohmann::json Plot::diagnostics() const { const std::size_t byte_length = pacing.video_enabled ? static_cast(stream.width) * stream.height * 4U : 0U; nlohmann::json output{ - {"protocol", "aethera.plot.diagnostics"}, {"version", 3}, + {"protocol", "aethera.plot.diagnostics"}, {"version", 4}, {"dimension", is_3d ? "3D" : "2D"}, {"sequence", identity.sequence}, {"correlation_id", identity.correlation_id}, @@ -1495,18 +1606,8 @@ nlohmann::json Plot::diagnostics() const { {"format", format}, {"native_format", native_format}, {"supported_formats", std::move(supported_formats)}, {"byte_length", byte_length}}}, - {"pacing", {{"mode", pacing_mode_name(pacing.mode)}, - {"fixed_rate_fps", pacing.fixed_rate_fps}, - {"render_enabled", pacing.render_enabled}, - {"video_enabled", pacing.video_enabled}}}, - {"plot_scheduler", { - {"received_ticks", d->received_tick_count.load(std::memory_order_relaxed)}, - {"coalesced_ticks", d->coalesced_tick_count.load(std::memory_order_relaxed)}, - {"policy_skips", d->policy_skip_count.load(std::memory_order_relaxed)}, - {"render_admission", admission_name}, - {"frame_slot_busy", d->frame_slot_busy_count.load(std::memory_order_relaxed)}, - {"scene_rejections", d->scene_rejection_count.load(std::memory_order_relaxed)}, - {"submitted_frames", d->submitted_frame_count.load(std::memory_order_relaxed)}}}, + {"frame_policy", frame_policy_state_json(pacing)}, + {"render_admission", admission_name}, {"frame_statistics", std::move(frame_statistics)}, {"input_statistics", std::move(input_statistics)}}; if (is_3d) { @@ -1601,5 +1702,7 @@ nlohmann::json Plot::post_publish_taskflow_trace() const { void Plot::reset_diagnostics() { std::visit([](auto& scene) { scene->reset_diagnostics(); }, d->scene); d->statistics_generation.fetch_add(1, std::memory_order_acq_rel); + d->frame_policy->reset_statistics(); + d->arm_tick_consumer(weak_from_this()); } } diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index 7e31def..fdc2bb8 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -27,19 +27,13 @@ struct Plot_Input_Event { std::uint32_t native_key{}; /* 浏览器原生按键码。 */ bool auto_repeat{}; /* 是否为系统重复按键。 */ }; -enum struct Plot_Render_Tick_Source : std::uint8_t { - periodic, - immediate, - maximum_rate -}; - struct Plot_Render_Tick { std::chrono::steady_clock::time_point issued_at{}; /* 页面帧时钟发布本 tick 的单调时刻;手动帧在提交时填写。 */ std::uint64_t sequence{}; /* Kernel 全局 Frame_Scheduler 时间轴上的关联序号。 */ double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */ std::uint32_t width{320}; /* 当前图在媒体图集中的固定像素宽度。 */ std::uint32_t height{192}; /* 当前图在媒体图集中的固定像素高度。 */ - Plot_Render_Tick_Source source{Plot_Render_Tick_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */ + Frame_Request_Source source{Frame_Request_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */ }; enum struct Plot_Pixel_Layout : std::uint8_t { bgra8, diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 927a20c..1b87ff8 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -39,12 +39,12 @@ type Browser_Input_Statistic = {count: number; latest_ms: number; average_ms: nu maximum_ms: number; sent_count: number; disconnected_count: number; websocket_buffered_bytes: number}; type Browser_Input_Statistics = Record; -type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 3; dimension: "2D" | "3D"; +type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 4; dimension: "2D" | "3D"; sequence: number; correlation_id: number; rendered_sequence: number; rendered_correlation_id: number; generated_time_unix_ms: number; delivery: Frame_Delivery; frame_rate_fps: number; dropped_sequence_count: number; window_capacity: number; pixel: {width: number; height: number; format: Pixel_Format; native_format: Pixel_Format; supported_formats: Pixel_Format[]; byte_length: number}; - pacing: {mode: Frame_Pacing_Mode; fixed_rate_fps: number; render_enabled: boolean; video_enabled: boolean}; + frame_policy: Frame_Policy_State; frame_statistics: Record; input_statistics: Event_Statistics}; type Frame_Sample = {sequence: number; generated_at_ms: number; received_at_ms: number; values: Frame_Stage_Values}; type Video_Playback_Metrics = {frame_rate_fps: number; presented_frames: number; dropped_frames: number; @@ -84,8 +84,21 @@ type Taskflow_Execution_Trace = {native_id: string; node_id: string; worker_id: completed_ms: number; duration_ms: number; cpu_duration_ms: number; cpu_cycles: number; cooperative_wait_ms: number; cpu_time_coarse: boolean; observer_entry_ms: number; observer_exit_ms: number; observer_entry_cpu_ms: number; observer_exit_cpu_ms: number; queue_wait_ms: number}; +type Frame_Policy_State = { + generation: number; + configuration: {mode: Frame_Pacing_Mode; render_enabled: boolean; video_enabled: boolean; fixed_rate_fps: number}; + observation: {duration_ms: number; request_count: number; submitted_frame_count: number; completed_frame_count: number; active_frame_count: number}; + throughput: {request_rate_fps: number; submission_rate_fps: number; completion_rate_fps: number; target_achievement_ratio: number; + latest_frame_interval_ms: number; average_frame_interval_ms: number; frame_interval_jitter_ms: number}; + requests: {periodic: number; immediate: number; maximum_rate: number; accepted: number; coalesced: number; policy_rejected: number; + frame_slot_backpressure: number; scene_rejected: number; acceptance_ratio: number; coalescing_ratio: number; backpressure_ratio: number}; + latency: {latest_tick_queue_ms: number; average_tick_queue_ms: number; maximum_tick_queue_ms: number; + latest_completion_ms: number; average_completion_ms: number; maximum_completion_ms: number}; + last_frame: {sequence: number; request_source: "unspecified" | "periodic" | "immediate" | "maximum_rate"}; +}; type Taskflow_Frame_Trace = {sequence: number; correlation_id: number; created_time_unix_ns: number; worker_count: number; request_source?: "unspecified" | "periodic" | "immediate" | "maximum_rate"; + frame_policy?: Frame_Policy_State; markers?: Record; measurements?: Record; graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]}; type Taskflow_Frame_Response = {protocol: "aethera.taskflow.frames"; version: 1; requested: number; remaining: number; @@ -125,9 +138,9 @@ function gallery_socket_url(path: string, transport: Gallery_Transport_Mode) { function valid_plot_diagnostics(value: unknown): value is Plot_Diagnostics { if (!value || typeof value !== "object") return false; const diagnostics = value as Partial; - return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 3 && + return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 4 && typeof diagnostics.sequence === "number" && Boolean(diagnostics.frame_statistics) && - Boolean(diagnostics.input_statistics) && Boolean(diagnostics.pacing); + Boolean(diagnostics.input_statistics) && Boolean(diagnostics.frame_policy); } function valid_gallery_layout(value: unknown): value is Gallery_Layout { @@ -661,8 +674,8 @@ function use_plot_stream(plot: Plot, p99_server_completion_ms: completion?.p99 ?? 0, frame_rate_fps: server.frame_rate_fps, p95_frame_interval_jitter_ms: interval?.p95 ?? 0, - pacing_mode: server.pacing.mode, - fixed_rate_fps: server.pacing.fixed_rate_fps, + pacing_mode: server.frame_policy.configuration.mode, + fixed_rate_fps: server.frame_policy.configuration.fixed_rate_fps, delivery: server.delivery, video_playback: diagnostics.video_playback }); @@ -1098,10 +1111,78 @@ function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manu on_manual_frame: () => void; on_reset: () => void; }) { const fields = analysis?.fields.filter(field => field.editable) ?? []; + const [frame_count, set_frame_count] = useState(8); + const [response, set_response] = useState(null); + const [frame_index, set_frame_index] = useState(0); + const [capture_busy, set_capture_busy] = useState(false); + const [capture_error, set_capture_error] = useState(""); + const load_samples = useCallback(async () => { + const request = await fetch(plot.taskflow, {cache: "no-store"}); + const value = await request.json() as Taskflow_Frame_Response & {error?: string}; + if (!request.ok) throw new Error(value.error ?? "读取帧策略状态失败"); + set_response(value); + set_frame_index(current => Math.min(current, Math.max(0, value.frames.length - 1))); + }, [plot.taskflow]); + useEffect(() => { + set_response(null); set_frame_index(0); set_capture_error(""); + void load_samples().catch(failure => set_capture_error( + failure instanceof Error ? failure.message : "读取帧策略状态失败")); + }, [load_samples]); + useEffect(() => { + if (!response || response.requested === 0 || response.complete) return; + const timer = window.setInterval(() => void load_samples().catch(failure => + set_capture_error(failure instanceof Error ? failure.message : "读取帧策略状态失败")), 400); + return () => window.clearInterval(timer); + }, [load_samples, response?.requested, response?.complete]); + const capture = async () => { + set_capture_busy(true); set_capture_error(""); set_frame_index(0); + try { + const request = await fetch(plot.taskflow, {method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({frame_count})}); + const value = await request.json() as Taskflow_Frame_Response & {error?: string}; + if (!request.ok) throw new Error(value.error ?? "请求帧策略状态失败"); + set_response(value); + } catch (failure) { + set_capture_error(failure instanceof Error ? failure.message : "请求帧策略状态失败"); + } finally { set_capture_busy(false); } + }; + const frame = response?.frames[frame_index]; + const state = frame?.frame_policy; + const percent = (value: number) => `${(value * 100).toFixed(1)}%`; + const rate = (value: number) => `${value.toFixed(2)} FPS`; + const duration = (value: number) => `${value.toFixed(value < 10 ? 3 : 2)} ms`; return
-
{plot.dimension} 后端帧策略Scene 按当前后端策略渲染;媒体层独立按 30 FPS 读取 latest,并选择 FFmpeg/WebRTC 或 WebSocket 原始像素。浏览器隐藏画面不会停采样。
+
{plot.dimension} 后端帧策略配置和运行统计由 Kernel Frame_Policy 双缓冲状态发布;查询只读取已发布状态,不扫描任务图,也不锁住渲染线程。
{analysis ?
{fields.map(field => on_update(analysis, field, value)}/>)}
:

正在读取帧策略…

} +
+
按完成帧采样策略状态策略状态绑定在对应帧上,与 Taskflow 帧捕获使用同一请求、帧槽和序列号。每个样本表示该帧创建时已经发布的策略状态。
+
+ + {response?.requested ? `${response.captured}/${response.requested} 帧${response.complete ? " · 已完成" : ` · 还需 ${response.remaining} 帧`}` : "只在手动请求后采集;manual 模式需要手动生成相应帧数。"}
+ {capture_error ?

{capture_error}

: null} + {state && frame ? <>
+ 策略代次 {state.generation} · 观察 {duration(state.observation.duration_ms)}
+
+
实际完成帧率{rate(state.throughput.completion_rate_fps)}由完成帧间隔计算;评价最终吞吐
+
提交速率{rate(state.throughput.submission_rate_fps)}进入 Scene 的帧速率
+
目标达成率{state.configuration.mode === "fixed_rate" ? percent(state.throughput.target_achievement_ratio) : "不适用"}实际完成 FPS ÷ fixed_rate 目标
+
帧间隔抖动{duration(state.throughput.frame_interval_jitter_ms)}完成间隔标准差,越低越稳定
+
请求接受率{percent(state.requests.acceptance_ratio)}{state.requests.accepted}/{state.observation.request_count} 个请求进入准入
+
请求合并率{percent(state.requests.coalescing_ratio)}忙碌期间被 latest 合并的比例
+
帧槽背压率{percent(state.requests.backpressure_ratio)}{state.requests.frame_slot_backpressure} 次物理帧槽耗尽
+
平均 tick 排队{duration(state.latency.average_tick_queue_ms)}请求发出至 Scene 提交
+
平均完成延迟{duration(state.latency.average_completion_ms)}Scene 提交至完成帧退役
+
在途帧{state.observation.active_frame_count}已提交但尚未完成退役
+
+
+ {response?.frames.map((sample, index) => { const policy = sample.frame_policy; return policy ? set_frame_index(index)}> : null;})} +
来源完成 FPS间隔抖动排队完成延迟合并 / 背压
#{sample.sequence}{sample.request_source ?? "unspecified"}{rate(policy.throughput.completion_rate_fps)}{duration(policy.throughput.latest_frame_interval_ms)}{duration(policy.throughput.frame_interval_jitter_ms)}{duration(policy.latency.latest_tick_queue_ms)}{duration(policy.latency.latest_completion_ms)}{policy.requests.coalesced} / {policy.requests.frame_slot_backpressure}
+
查看该帧绑定的完整 Frame_Policy State
{JSON.stringify(state, null, 2)}
+ :

尚未采样帧策略状态。

}
; } @@ -1841,6 +1922,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_ sequence: frame?.sequence, correlation_id: frame?.correlation_id, request_source: frame?.request_source, + frame_policy: frame?.frame_policy, markers: frame?.markers ?? {}, measurements: frame?.measurements ?? {}, graph, @@ -2116,6 +2198,7 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state, sequence: frame?.sequence, correlation_id: frame?.correlation_id, request_source: frame?.request_source, + frame_policy: frame?.frame_policy, markers: frame?.markers ?? {}, measurements: frame?.measurements ?? {}, graph, @@ -2707,7 +2790,7 @@ export function App() { if (category === "2D" || category === "3D") return plots.filter(plot => plot.dimension === category); return plots; }, [category, plots]); - const gallery =
AETHERA 渲染实验室

实时图形组件库

+ const gallery =
{selected ? 当前图形 {plot_labels[selected.id] ?? selected.title} : 点击任意图形后,属性和 DAG 节点状态会自动同步。}
-
; const factory = (node: TabNode) => { if (node.getComponent() === "gallery") return gallery; diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index d4b2505..5c80ff1 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -22,12 +22,13 @@ button { color: inherit; } align-items: center; justify-content: space-between; gap: 20px; - min-height: 92px; + min-height: 66px; padding: 16px 4px; border-bottom: 1px solid #24324a; background: #07101bea; backdrop-filter: blur(16px); } +.topbar > nav { flex: none; padding: 0; } h1 { margin: 3px 0 0; font-size: clamp(25px, 3vw, 40px); line-height: 1; letter-spacing: -.045em; } h2 { margin: 4px 0 0; font-size: 23px; letter-spacing: -.03em; } @@ -54,6 +55,25 @@ h2 { margin: 4px 0 0; font-size: 23px; letter-spacing: -.03em; } .selectionName { margin-right: 4px; color: #8296b2; white-space: nowrap; } .selectionName strong { color: #e8f0ff; } +.framePolicyWorkspace { display: grid; align-content: start; gap: 14px; } +.framePolicySampling { display: grid; gap: 14px; } +.framePolicySampleSelector { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: #8296b2; font-size: 11px; } +.framePolicySampleSelector label { display: flex; align-items: center; gap: 8px; } +.framePolicySampleSelector select { min-width: 180px; padding: 7px 9px; color: #d7e4f5; border: 1px solid #2a3b55; border-radius: 7px; background: #091321; } +.framePolicyMetricGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 9px; } +.framePolicyMetricGrid article { min-width: 0; padding: 12px; border: 1px solid #213653; border-radius: 10px; background: #0a1422; } +.framePolicyMetricGrid article > span { display: block; color: #8ca0ba; font-size: 11px; } +.framePolicyMetricGrid article > strong { display: block; margin: 8px 0 7px; color: #5ce4c2; font: 700 19px/1.1 ui-monospace, monospace; } +.framePolicyMetricGrid article > small { display: block; color: #657994; font-size: 10px; line-height: 1.4; } +.framePolicySampleTable { overflow: auto; border: 1px solid #213653; border-radius: 10px; background: #08121f; } +.framePolicySampleTable table { width: 100%; border-collapse: collapse; font-size: 11px; white-space: nowrap; } +.framePolicySampleTable th, .framePolicySampleTable td { padding: 9px 11px; border-bottom: 1px solid #192d46; text-align: right; } +.framePolicySampleTable th:first-child, .framePolicySampleTable td:first-child, +.framePolicySampleTable th:nth-child(2), .framePolicySampleTable td:nth-child(2) { text-align: left; } +.framePolicySampleTable th { position: sticky; top: 0; color: #7890ad; background: #0d1929; } +.framePolicySampleTable tbody tr { color: #b9c9dc; cursor: pointer; } +.framePolicySampleTable tbody tr:hover, .framePolicySampleTable tbody tr.active { color: #e9fff9; background: #14352f; } + nav { display: flex; flex-wrap: wrap; gap: 8px; padding: 18px 0; } .plotGridHost { width: 100%; min-height: 360px; } .react-grid-layout { position: relative; transition: height .2s ease; }