From 7216a1703f2b05c187a1d62d19f585e62d3dceea Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sun, 30 Aug 2026 00:38:56 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=AE=9A=E4=B9=89=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E8=81=8C=E8=B4=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + Project_detail_specification.md | 22 +- .../src/benchmark/Frame_Policy_Benchmarks.cpp | 138 +- .../Frame_Policy/Fixed_Rate_Frame_Policy.cpp | 64 + .../Frame_Policy/Fixed_Rate_Frame_Policy.hpp | 24 + .../src/kernel/Frame_Policy/Frame_Policy.cpp | 698 +++++--- .../src/kernel/Frame_Policy/Frame_Policy.hpp | 294 ++-- .../src/kernel/Frame_Policy/Frame_Policy.ipp | 130 +- .../kernel/Frame_Policy/Frame_Policy_3D.cpp | 323 ---- .../kernel/Frame_Policy/Frame_Policy_3D.hpp | 56 - .../kernel/Frame_Policy/Frame_Policy_3D.ipp | 10 - .../Frame_Policy/Frame_Policy_Lifecycle.ipp | 158 -- .../Frame_Policy/Manual_Frame_Policy.cpp | 20 + .../Frame_Policy/Manual_Frame_Policy.hpp | 18 + .../Maximum_Rate_Frame_Policy.cpp | 39 + .../Maximum_Rate_Frame_Policy.hpp | 21 + .../double_buffer/Mpmc_Triple_Buffer.hpp | 7 + kernel/src/kernel/double_buffer/model.hpp | 6 + kernel/src/kernel/frame.cpp | 6 +- kernel/src/kernel/frame.hpp | 11 +- kernel/src/kernel/scene.hpp | 18 + kernel/src/test/Frame_Policy_3D_Test.cpp | 75 - .../src/test/Frame_Policy_Lifecycle_Test.cpp | 402 ++--- kernel/src/test/render_test.cpp | 30 - mcp/core/runtime/Plot.cpp | 1504 +++++++---------- mcp/core/runtime/Plot.hpp | 79 +- mcp/tests/Event_Latency_Benchmarks.cpp | 68 +- render_2D/render_2D/base/Frame_2D.cpp | 5 +- render_2D/render_2D/base/Frame_2D.hpp | 3 +- render_2D/render_2D/scene/Render_Scene_2D.cpp | 22 + render_2D/render_2D/scene/Render_Scene_2D.hpp | 6 +- render_3D/render_3D/base/Frame_3D.cpp | 5 +- render_3D/render_3D/base/Frame_3D.hpp | 3 +- render_3D/render_3D/scene/Render_Scene_3D.cpp | 32 + render_3D/render_3D/scene/Render_Scene_3D.hpp | 5 +- 绘制优化.md | 386 ----- 36 files changed, 2052 insertions(+), 2638 deletions(-) create mode 100644 kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.cpp create mode 100644 kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.hpp delete mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp delete mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp delete mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp delete mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp create mode 100644 kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.cpp create mode 100644 kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.hpp create mode 100644 kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.cpp create mode 100644 kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.hpp delete mode 100644 kernel/src/test/Frame_Policy_3D_Test.cpp delete mode 100644 绘制优化.md diff --git a/AGENTS.md b/AGENTS.md index b71b642..a8421e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ ## 设计约束 +复杂的对象 都使用 D:\ae\proj\Aethera\kernel\src\kernel\double_buffer\model.hpp 并且使用其提供的线程模型 + `` 除非特别说明不允许写任何同步等待代码。 除非特别说明不允许写任何同步等待代码。 diff --git a/Project_detail_specification.md b/Project_detail_specification.md index 61e618e..4c8f9b1 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -25,17 +25,19 @@ 三缓冲;收集、内部渲染、外部查询分别占用一份队列,Prepare 入口只在交换锁下轮换三者并清空过期查询队列。事件不得独立触发帧。2D 按 Renderable 区域与 Paint 顺序形成接受链,区域默认整个 viewport;3D 无等待提交渲染域,满载时保留当前事件供下一次 Prepare 重试。 -* 帧由 Scene 外部创建和持有;每次 `render(frame*)` 只借用该帧并在完成回调返回同一地址。Scene、异步后端和 Web +* 帧由 Frame_Policy 创建、持有和复用;每次 `render(frame*)` 只借用该帧并在完成回调返回同一地址。Scene、异步后端和 Web 层只向帧写入固定语义的单调时间点与原始耗时,不保存平均值、分位数或波动等衍生统计。 -* Plot 使用服务端帧时钟调用 `Scene::render(frame*)`;`fixed_rate` 只由 Frame_Scheduler 周期时钟驱动,完成回调不得改变其 - deadline。`maximum_rate` 是唯一允许完成回调在释放 Plot 准入后异步投递下一帧请求的特殊模式;回调不得直接重入 `render()` - 、不得同步等待,并且物理帧槽耗尽时只能由帧退役机制解除背压。Scene 回调发布 2D 原生 BGRA 或 3D 原生 RGBA;每个 Plot - 只服从自身 Frame_Policy。Gallery 仅按 Plot 完成帧的到达顺序更新对应图集槽位,不拥有采样时钟、帧准入、合并或丢帧策略; - 每次槽位更新均在唯一串行 Taskflow 媒体 DAG 中组成图集并由 FFmpeg/libx264 编码 H.264 Annex-B。Drogon WebSocket 只传输压缩 - access unit,不得用在途窗口反向跳过已通过 Plot 策略的帧;浏览器由 WebCodecs 解码后按槽位提交各 Plot Canvas。Plot 输入和控制也统一使用 Drogon WebSocket。 -* 帧策略及其时钟实现统一归属 `kernel/src/kernel/Frame_Policy`。配置变更和运行事实通过 MPMC 事件流进入唯一 consumer,由 - `double_buffer/model.hpp` 发布 `Frame_Policy::State`;查询只读取已发布 State。手动帧捕获必须把帧创建边界的策略 State - 绑定到该帧并沿现有帧捕获协议返回,禁止另建策略历史、轮询累加器或重复诊断字段。 +* Frame_Policy 借用抽象 Scene 并调用 `render(frame*)`;`fixed_rate` 只由其具体 Policy 子类持有的 Frame_Scheduler 周期时钟驱动, + 完成回调不得改变 deadline。`maximum_rate` 子类只在 Scene 开放后续生产或 Policy 回收物理帧槽后异步投递下一次生产任务; + 回调不得直接重入 `render()`、不得同步等待。Scene 回调发布 2D 原生 BGRA 或 3D 原生 RGBA;每个 Plot 只服从自身 + Frame_Policy。Gallery 仅按 Plot 完成帧的到达顺序更新对应图集槽位,不拥有采样时钟、帧准入、合并或丢帧策略; 每次槽位更新均在唯一串行 + Taskflow 媒体 DAG 中组成图集并由 FFmpeg/libx264 编码 H.264 Annex-B。Drogon WebSocket 只传输压缩 access + unit,不得用在途窗口反向跳过已通过 Plot 策略的帧;浏览器由 WebCodecs 解码后按槽位提交各 Plot Canvas。Plot 输入和控制也统一使用 + Drogon WebSocket。 +* 帧策略及其时钟实现统一归属 `kernel/src/kernel/Frame_Policy`。Manual、Fixed-rate 和 Maximum-rate 分别由具体 Policy 子类表达, + 禁止在基类中再保存模式联合体。Policy 直接接收 Scene 回调并通过 `double_buffer/model.hpp` 发布自身 State,不建立事件流或 + Taskflow consumer。模式替换必须调用旧 Policy 的异步 `stop(callback)`;停止排空全部 Scene 借用和媒体分派后,在回调中创建 新 + Policy。禁止同步等待、策略状态快照、策略历史、轮询累加器或重复诊断字段。 * 相机是 3D Scene 组件,只允许定义在 `render_3D/camera`;Kernel 和 2D 不得依赖相机类型。Web 层只为已有 `Camera_3D` 增加协议描述,不复制相机配置。Gallery 服务同一时刻只允许一个页面实例持有;该页面的所有 Plot 连接共享页面令牌,其他标签页或浏览器实例必须被拒绝。 * `Time_Axis` 是时间与 tick 的唯一权威来源;使用层先推进时间轴,再把同一 tick 分发给所有相关数据图元。时间窗口从第一条数据起始终锚定最新 diff --git a/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp b/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp index 75d5d88..3e34fc8 100644 --- a/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp +++ b/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp @@ -1,106 +1,62 @@ -#include "Frame_Policy/Frame_Policy.hpp" -#include "Frame_Policy/Frame_Policy_3D.hpp" +#include "Frame_Policy/Manual_Frame_Policy.hpp" #include -#include -#include #include -#include namespace { -template -std::unique_ptr build_policy() { - auto built = typename Policy::template Builder{}.build(); - if (!built) throw std::logic_error("benchmark frame policy build failed"); - return std::move(*built); +struct Benchmark_Frame final : aethera::Render_Frame { + Benchmark_Frame() : Render_Frame({}) {} +}; + +struct Benchmark_Scene final : aethera::Render_Frame_Scene { + aethera::Render_Frame_Result render_frame( + aethera::not_null frame, + aethera::Render_Frame_Callbacks callbacks) override { + if (callbacks.submitted) callbacks.submitted(frame, false); + if (callbacks.completed) callbacks.completed(frame); + return aethera::Render_Frame_Result::submitted; + } +}; + +std::shared_ptr make_policy( + Benchmark_Scene& scene) { + return std::make_shared( + aethera::Frame_Policy::Dependencies{ + .scene = &scene, + .create_frame = [] { + return std::make_unique(); + }, + .prepare_frame = []( + aethera::not_null, + const aethera::Frame_Production&) {}, + .publish_frame = []( + aethera::not_null, + const aethera::Frame_Production&) { + const auto now = std::chrono::steady_clock::now(); + return aethera::Frame_Publication_Dispatch{ + .started_at = now, + .completed_at = now, + .succeeded = true}; + }, + .retain_owner = [] { return std::make_shared(1); }, + .report_failure = [](std::exception_ptr) {}, + .frame_capacity = 3}); } -void frame_policy_request_storm(benchmark::State& state) { - auto policy = build_policy(); - const auto origin = std::chrono::steady_clock::now(); - std::uint64_t request_sequence{}; - constexpr std::uint64_t batch_size = 256; +void frame_policy_state_access(benchmark::State& state) { + Benchmark_Scene scene; + const auto policy = make_policy(scene); for (auto _ : state) { static_cast(_); - for (std::uint64_t index = 0; index < batch_size; ++index) { - ++request_sequence; - policy->submit_request({ - .issued_at = origin + - std::chrono::nanoseconds(request_sequence), - .time_milliseconds = static_cast(request_sequence), - .source = index + 1 == batch_size - ? aethera::Frame_Request_Source::immediate - : aethera::Frame_Request_Source::periodic}); - } - static_cast(policy->consume_events()); - const auto production = policy->begin_production(); - if (!production) { - state.SkipWithError("request storm did not admit one frame"); - break; - } - const auto published_at = std::chrono::steady_clock::now(); - policy->submit_publication_dispatch({ - .started_at = published_at, - .completed_at = published_at, - .sequence = production->sequence, - .asynchronous_feedback_count = 0, - .succeeded = true}); - static_cast(policy->consume_events()); + policy->access_state( + [](const aethera::Frame_Policy::State& current) { + benchmark::DoNotOptimize( + current.observation.completed_frame_count); + }); } - state.SetItemsProcessed(state.iterations() * batch_size); } -void frame_policy_slow_sender(benchmark::State& state) { - auto policy = build_policy(); - policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); - const auto origin = std::chrono::steady_clock::now(); - policy->submit_request({ - .issued_at = origin, - .source = aethera::Frame_Request_Source::maximum_rate}); - static_cast(policy->consume_events()); - const auto active = policy->begin_production(); - if (!active) { - state.SkipWithError("slow sender setup did not admit a frame"); - return; - } - const auto published_at = std::chrono::steady_clock::now(); - policy->submit_publication_dispatch({ - .started_at = published_at, - .completed_at = published_at, - .sequence = active->sequence, - .asynchronous_feedback_count = 1, - .succeeded = false}); - std::uint64_t request_sequence{}; - constexpr std::uint64_t batch_size = 256; - for (auto _ : state) { - static_cast(_); - for (std::uint64_t index = 0; index < batch_size; ++index) { - ++request_sequence; - policy->submit_request({ - .issued_at = origin + - std::chrono::nanoseconds(request_sequence), - .time_milliseconds = static_cast(request_sequence), - .source = aethera::Frame_Request_Source::maximum_rate}); - } - static_cast(policy->consume_events()); - if (policy->begin_production()) { - state.SkipWithError("delivery window admitted a second frame"); - break; - } - } - policy->submit_publication_feedback({ - .completed_at = std::chrono::steady_clock::now(), - .sequence = active->sequence, - .succeeded = true}); - static_cast(policy->consume_events()); - const auto resumed = policy->begin_production(); - if (!resumed) - state.SkipWithError("delivery completion did not resume production"); - state.SetItemsProcessed(state.iterations() * batch_size); -} - -BENCHMARK(frame_policy_request_storm)->UseRealTime(); -BENCHMARK(frame_policy_slow_sender)->UseRealTime(); +BENCHMARK(frame_policy_state_access)->UseRealTime(); } diff --git a/kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.cpp b/kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.cpp new file mode 100644 index 0000000..28f876c --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.cpp @@ -0,0 +1,64 @@ +#include "Fixed_Rate_Frame_Policy.hpp" +#include "Frame_Scheduler.hpp" +#include +#include +#include + +namespace aethera { + +struct Fixed_Rate_Frame_Policy::Private { + Frame_Scheduler::Timer timer{}; /* 本 Policy 的轻量周期节点。 */ + std::atomic frames_per_second{30.0}; /* 固定频率唯一配置来源。 */ +}; + +Fixed_Rate_Frame_Policy::Fixed_Rate_Frame_Policy( + Dependencies dependencies, double frames_per_second) + : Frame_Policy(std::move(dependencies)), fixed(std::make_unique()) { + set_frame_rate(frames_per_second); +} +Fixed_Rate_Frame_Policy::~Fixed_Rate_Frame_Policy() = default; +Frame_Policy_Type Fixed_Rate_Frame_Policy::type() const noexcept { + return Frame_Policy_Type::fixed_rate; +} +std::string_view Fixed_Rate_Frame_Policy::name() const noexcept { + return "fixed_rate"; +} +void Fixed_Rate_Frame_Policy::start() { + const auto weak = weak_from_this(); + fixed->timer = Frame_Scheduler::instance().make_timer( + [weak](Frame_Scheduler::Tick tick) { + const auto policy = weak.lock(); + if (!policy) return; + auto* fixed_policy = static_cast( + policy.get()); + if (fixed_policy->read_state().lifecycle != + Frame_Policy_Lifecycle::running) return; + fixed_policy->request_automatic_frame(Frame_Request{ + .issued_at = tick.issued_at, + .sequence = tick.sequence, + .time_milliseconds = tick.time_milliseconds, + .source = Frame_Request_Source::periodic}); + }); + fixed->timer.start_periodic( + fixed->frames_per_second.load(std::memory_order_acquire)); +} +void Fixed_Rate_Frame_Policy::stop(Stop_Callback completed) { + fixed->timer.cancel(); + begin_stop(std::move(completed)); +} +void Fixed_Rate_Frame_Policy::set_frame_rate(double frames_per_second) { + if (!std::isfinite(frames_per_second) || frames_per_second <= 0.0) + throw std::invalid_argument("fixed frame rate must be positive"); + fixed->frames_per_second.store( + frames_per_second, std::memory_order_release); + if (fixed->timer.valid() && + read_state().lifecycle == + Frame_Policy_Lifecycle::running) + fixed->timer.start_periodic(frames_per_second); +} +double Fixed_Rate_Frame_Policy::frame_rate() const noexcept { + return fixed->frames_per_second.load(std::memory_order_acquire); +} +void Fixed_Rate_Frame_Policy::production_capacity_available() {} + +} diff --git a/kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.hpp new file mode 100644 index 0000000..01889b3 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Fixed_Rate_Frame_Policy.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "Frame_Policy.hpp" +#include + +namespace aethera { + +class Fixed_Rate_Frame_Policy final : public Frame_Policy { +public: + explicit Fixed_Rate_Frame_Policy(Dependencies dependencies, + double frames_per_second = 30.0); + ~Fixed_Rate_Frame_Policy() override; + [[nodiscard]] Frame_Policy_Type type() const noexcept override; + [[nodiscard]] std::string_view name() const noexcept override; + void start() override; + void stop(Stop_Callback completed) override; + void set_frame_rate(double frames_per_second); + [[nodiscard]] double frame_rate() const noexcept; +private: + struct Private; + std::unique_ptr fixed; /* 固定频率时钟的唯一所有权。 */ + void production_capacity_available() override; +}; + +} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp index 4297136..bc48305 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp @@ -1,8 +1,14 @@ #include "Frame_Policy.hpp" -#include "Frame_Policy_Lifecycle.ipp" +#include #include +#include #include +#include +#include +#include #include +#include +#include namespace aethera { namespace { @@ -21,256 +27,476 @@ std::uint64_t duration_nanoseconds( 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); +} + +std::uint64_t Frame_Policy::Private::submission_generation( + const Frame_Policy* object) const noexcept { + return object->stream_submission_generation< + detail::Frame_Policy_Request_Stream_Tag>() + + object->stream_submission_generation< + detail::Frame_Policy_Scene_Stream_Tag>() + + object->stream_submission_generation< + detail::Frame_Policy_Control_Stream_Tag>() + + object->stream_submission_generation< + detail::Frame_Policy_Feedback_Stream_Tag>(); } + +void Frame_Policy::Private::schedule(not_null object) { + bool idle{}; + if (!consumer_active.compare_exchange_strong( + idle, true, std::memory_order_acq_rel, + std::memory_order_acquire)) + return; + const auto weak = object->weak_from_this(); + try { + schedule_task("frame.policy.consume", [weak] { + const auto policy = weak.lock(); + if (!policy) return; + auto& data = double_buffer::detail::Internal_Access::layer< + Frame_Policy>(policy.get()); + data.consume(not_null{policy.get()}); + }); + } + catch (...) { + consumer_active.store(false, std::memory_order_release); + runtime->report_failure(std::current_exception()); + } + } + +void Frame_Policy::Private::submit_scene(not_null object, + detail::Frame_Policy_Scene_Message message) { + object->submit_stream( + std::move(message)); + schedule(object); + } + +void Frame_Policy::Private::drain_requests(not_null object, + Frame_Policy::State& state) { + object->exchange_stream(); + object->access_rendering_stream< + detail::Frame_Policy_Request_Stream_Tag>( + [&](std::span requests) { + const auto& prop = double_buffer::detail::Internal_Access:: + current_prop(object.get()); + for (auto& request : requests) { + if (state.lifecycle != Frame_Policy_Lifecycle::running || + !prop.render_enabled) { + ++state.observation.policy_rejection_count; + continue; + } + runtime->coalesce(std::move(request)); + } + }); + } + +void Frame_Policy::Private::drain_controls(not_null object, + Frame_Policy::State& state) { + object->exchange_stream(); + object->access_rendering_stream< + detail::Frame_Policy_Control_Stream_Tag>( + [&](std::span controls) { + for (auto& control : controls) { + std::visit([&](auto& value) { + using Value = std::remove_cvref_t; + if constexpr (std::same_as< + Value, + detail::Frame_Policy_Reset_Statistics>) { + ++runtime->reset_generation; + if (runtime->active_count() == 0) { + state.observation = {}; + state.observation.generation = + runtime->reset_generation; + runtime->statistics.reset(); + } + } + else if constexpr (std::same_as< + Value, + detail::Frame_Policy_Stop>) { + if (state.lifecycle != + Frame_Policy_Lifecycle::running) + return; + runtime->stop_callback = + std::move(value.completed); + runtime->pending_request.reset(); + state.lifecycle = + Frame_Policy_Lifecycle::stopping; + } + }, control); + } + }); + object->exchange_stream(); + object->access_rendering_stream< + detail::Frame_Policy_Feedback_Stream_Tag>( + [](std::span) {}); + } + +void Frame_Policy::Private::record_submission( + Frame_Slot& slot, Frame_Policy::State& state) { + ++state.observation.request_count; + ++state.observation.accepted_request_count; + ++state.observation.submitted_frame_count; + switch (slot.production.request.source) { + case Frame_Request_Source::periodic: + ++state.observation.periodic_request_count; + break; + case Frame_Request_Source::maximum_rate: + ++state.observation.maximum_rate_request_count; + break; + case Frame_Request_Source::immediate: + case Frame_Request_Source::unspecified: + ++state.observation.immediate_request_count; + break; + } + } + +void Frame_Policy::Private::complete_frame( + not_null object, Frame_Slot& slot, + std::chrono::steady_clock::time_point completed_at, + Frame_Policy::State& state, + bool& capacity_available) { + slot.state = Frame_State::publishing; + Frame_Publication_Dispatch dispatch{}; + try { + dispatch = runtime->publish_frame( + not_null{slot.frame.get()}, slot.production); + } + catch (...) { + runtime->report_failure(std::current_exception()); + } + auto& observation = state.observation; + const auto observed_ns = monotonic_nanoseconds(completed_at); + if (observation.generation != runtime->reset_generation) { + observation = {}; + observation.generation = runtime->reset_generation; + runtime->statistics.reset(); + } + if (observation.observation_started_ns == 0) + observation.observation_started_ns = observed_ns; + observation.observed_until_ns = observed_ns; + ++observation.completed_frame_count; + const auto submitted_at = + slot.submitted_at.time_since_epoch().count() == 0 + ? completed_at : slot.submitted_at; + const auto queue_ns = duration_nanoseconds( + submitted_at - slot.production.request.issued_at); + const auto completion_ns = duration_nanoseconds( + completed_at - submitted_at); + observation.latest_tick_queue_ns = queue_ns; + observation.tick_queue_total_ns += queue_ns; + observation.maximum_tick_queue_ns = std::max( + observation.maximum_tick_queue_ns, queue_ns); + observation.latest_completion_latency_ns = completion_ns; + observation.completion_latency_total_ns += completion_ns; + observation.maximum_completion_latency_ns = std::max( + observation.maximum_completion_latency_ns, completion_ns); + if (observation.first_completion_ns == 0) + observation.first_completion_ns = observed_ns; + if (observation.last_completion_ns != 0) { + const auto interval = observed_ns - observation.last_completion_ns; + ++observation.completion_interval_count; + observation.latest_completion_interval_ns = interval; + observation.completion_interval_total_ns += interval; + const auto value = static_cast(interval); + observation.completion_interval_squared_total_ns2 += value * value; + } + observation.last_completion_ns = observed_ns; + observation.last_frame_sequence = slot.production.sequence; + observation.last_request_source = slot.production.request.source; + observation.frame_statistics = runtime->statistics.submit(*slot.frame); + observation.pixel_width = dispatch.pixel_width; + observation.pixel_height = dispatch.pixel_height; + if (slot.released_after_submission) + ++observation.overlapped_release_count; + else + ++observation.completion_gated_release_count; + if (dispatch.completed_at >= dispatch.started_at && + dispatch.started_at.time_since_epoch().count() != 0) { + const auto publication_ns = duration_nanoseconds( + dispatch.completed_at - dispatch.started_at); + observation.latest_publication_dispatch_latency_ns = publication_ns; + observation.publication_dispatch_latency_total_ns += publication_ns; + observation.maximum_publication_dispatch_latency_ns = std::max( + observation.maximum_publication_dispatch_latency_ns, + publication_ns); + } + if (dispatch.succeeded || dispatch.asynchronous_feedback_count != 0) + ++observation.published_frame_count; + else + ++observation.publication_failed_count; + + slot.owner_lifetime.reset(); + slot.state = Frame_State::available; + if (!slot.released_after_submission) { + slot.producer_release_requested = true; + if (slot.render_returned && runtime->production_active) { + runtime->production_active = false; + capacity_available = true; + } + } + } + +void Frame_Policy::Private::drain_scene(not_null object, + Frame_Policy::State& state, + bool& capacity_available) { + object->exchange_stream(); + object->access_rendering_stream( + [&](std::span messages) { + for (auto& message : messages) { + std::visit([&](auto& value) { + using Value = std::remove_cvref_t; + if constexpr (std::same_as< + Value, + detail::Frame_Policy_Scene_Submitted>) { + auto* slot = runtime->find_frame(value.frame); + if (!slot) + throw std::logic_error( + "Scene submitted an unowned policy frame"); + if (slot->state == Frame_State::available) return; + const bool first_submission = + slot->submitted_at.time_since_epoch().count() == 0; + slot->submitted_at = value.submitted_at; + slot->released_after_submission = + value.released_after_submission; + slot->state = Frame_State::scene_borrowed; + if (first_submission) record_submission(*slot, state); + if (!value.released_after_submission) return; + slot->producer_release_requested = true; + if (slot->render_returned && + runtime->production_active) { + runtime->production_active = false; + capacity_available = true; + } + } + else if constexpr (std::same_as< + Value, + detail::Frame_Policy_Scene_Completed>) { + auto* slot = runtime->find_frame(value.frame); + if (!slot) + throw std::logic_error( + "Scene completed an unowned policy frame"); + if (slot->state == Frame_State::available) return; + complete_frame(object, *slot, value.completed_at, + state, capacity_available); + } + else if constexpr (std::same_as< + Value, + detail::Frame_Policy_Render_Returned>) { + auto* slot = runtime->find_frame(value.frame); + if (!slot) + throw std::logic_error( + "Scene returned an unowned policy frame"); + slot->render_returned = true; + if (value.result == Render_Frame_Result::submitted) { + if (slot->state == Frame_State::preparing) { + slot->submitted_at = + std::chrono::steady_clock::now(); + slot->state = Frame_State::scene_borrowed; + record_submission(*slot, state); + } + if (slot->producer_release_requested && + runtime->production_active) { + runtime->production_active = false; + capacity_available = true; + } + return; + } + if (slot->state != Frame_State::available) { + slot->owner_lifetime.reset(); + slot->state = Frame_State::available; + ++state.observation.scene_rejection_count; + } + if (runtime->production_active) { + runtime->production_active = false; + capacity_available = true; + } + } + }, message); + } + }); + } + +void Frame_Policy::Private::produce(not_null object, + const Frame_Policy::Prop& prop, + Frame_Policy::State& state) { + if (runtime->production_active || !runtime->pending_request || + state.lifecycle != Frame_Policy_Lifecycle::running || + !prop.render_enabled) + return; + auto* slot = runtime->acquire_frame(); + if (!slot) { + ++state.observation.frame_slot_backpressure_count; + return; + } + auto request = std::move(*runtime->pending_request); + runtime->pending_request.reset(); + runtime->production_active = true; + slot->owner_lifetime = runtime->retain_owner(); + slot->production = Frame_Production{ + std::move(request), runtime->next_sequence++}; + slot->submitted_at = {}; + slot->render_returned = false; + slot->producer_release_requested = false; + slot->released_after_submission = false; + try { + runtime->prepare_frame( + not_null{slot->frame.get()}, slot->production); + const auto weak = object->weak_from_this(); + const auto result = runtime->scene->render_frame( + not_null{slot->frame.get()}, + Render_Frame_Callbacks{ + .submitted = [weak](not_null frame, + bool released) { + const auto policy = weak.lock(); + if (!policy) return; + auto& data = double_buffer::detail::Internal_Access:: + layer(policy.get()); + data.submit_scene( + not_null{policy.get()}, + detail::Frame_Policy_Scene_Submitted{ + frame, std::chrono::steady_clock::now(), + released}); + }, + .completed = [weak](not_null frame) { + const auto policy = weak.lock(); + if (!policy) return; + auto& data = double_buffer::detail::Internal_Access:: + layer(policy.get()); + data.submit_scene( + not_null{policy.get()}, + detail::Frame_Policy_Scene_Completed{ + frame, std::chrono::steady_clock::now()}); + }}); + submit_scene( + object, + detail::Frame_Policy_Render_Returned{ + not_null{slot->frame.get()}, result}); + } + catch (...) { + slot->owner_lifetime.reset(); + slot->state = Frame_State::available; + runtime->production_active = false; + runtime->report_failure(std::current_exception()); + } + } + +void Frame_Policy::Private::finish_stop(Frame_Policy::State& state) { + if (state.lifecycle != Frame_Policy_Lifecycle::stopping || + runtime->production_active || runtime->active_count() != 0) + return; + state.lifecycle = Frame_Policy_Lifecycle::stopped; + runtime->pending_request.reset(); + auto callback = std::move(runtime->stop_callback); + if (callback) callback(); + } + +void Frame_Policy::Private::consume(not_null object) noexcept { + auto observed_generation = submission_generation(object.get()); + try { + for (;;) { + object->advance(); + auto& state = double_buffer::detail::Internal_Access:: + pending_state(object.get()); + drain_controls(object, state); + drain_requests(object, state); + bool capacity_available{}; + drain_scene(object, state, capacity_available); + const auto& prop = double_buffer::detail::Internal_Access:: + current_prop(object.get()); + produce(object, prop, state); + state.observation.active_frame_count = runtime->active_count(); + finish_stop(state); + object->publish_state(); + if (capacity_available && + state.lifecycle == Frame_Policy_Lifecycle::running) + object->production_capacity_available(); + const auto current_generation = + submission_generation(object.get()); + const bool can_produce = runtime->pending_request && + !runtime->production_active && + runtime->active_count() < runtime->frames.size() && + state.lifecycle == Frame_Policy_Lifecycle::running && + prop.render_enabled; + if (current_generation == observed_generation && !can_produce) { + observed_generation = current_generation; + break; + } + observed_generation = current_generation; + } + } + catch (...) { + runtime->report_failure(std::current_exception()); + } + consumer_active.store(false, std::memory_order_release); + if (submission_generation(object.get()) != observed_generation) + schedule(object); + } +Frame_Policy::Frame_Policy(Dependencies dependencies) { + const auto frame_capacity = dependencies.frame_capacity; + auto attached = std::make_unique< + double_buffer::detail::Attached_Private>( + this, memory_resource()); + d = attached.release(); + auto& private_data = + double_buffer::detail::Internal_Access::layer(this); + private_data.bind_private_crtp(this); + bind_object_crtp(); + private_data.initialize(std::move(dependencies)); + auto& prop = double_buffer::detail::Internal_Access::pending_prop(this); + prop.frame_capacity = frame_capacity; + advance(); } -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 pixel_delivery_enabled = state.pixel_delivery_enabled; - const auto mode = state.mode; - const auto fixed_rate_fps = state.fixed_rate_fps; - auto pending_request = std::move(state.pending_request); - const auto next_frame_sequence = state.next_frame_sequence; - const auto in_flight_frame_sequence = state.in_flight_frame_sequence; - const auto publication_started_ns = state.publication_started_ns; - const auto publication_feedback_expected = - state.publication_feedback_expected; - const auto publication_feedback_received = - state.publication_feedback_received; - const auto publication_latest_feedback_ns = - state.publication_latest_feedback_ns; - const auto publication_dispatch_known = state.publication_dispatch_known; - const auto publication_dispatch_succeeded = - state.publication_dispatch_succeeded; - const auto publication_feedback_succeeded = - state.publication_feedback_succeeded; - state = {}; - state.generation = generation; - state.render_enabled = render_enabled; - state.pixel_delivery_enabled = pixel_delivery_enabled; - state.mode = mode; - state.fixed_rate_fps = fixed_rate_fps; - state.pending_request = std::move(pending_request); - state.next_frame_sequence = next_frame_sequence; - state.in_flight_frame_sequence = in_flight_frame_sequence; - state.publication_started_ns = publication_started_ns; - state.publication_feedback_expected = publication_feedback_expected; - state.publication_feedback_received = publication_feedback_received; - state.publication_latest_feedback_ns = publication_latest_feedback_ns; - state.publication_dispatch_known = publication_dispatch_known; - state.publication_dispatch_succeeded = publication_dispatch_succeeded; - state.publication_feedback_succeeded = publication_feedback_succeeded; - state.observation_started_ns = occurred_ns; - state.observed_until_ns = occurred_ns; +Frame_Policy::~Frame_Policy() = default; + +void Frame_Policy::request_frame(Frame_Request request) { + request.source = Frame_Request_Source::immediate; + request_automatic_frame(std::move(request)); } -} - -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_pixel_delivery_enabled(bool enabled) { - submit({.type = detail::Frame_Policy_Event_Type::pixel_delivery_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::submit_request(Frame_Request request) { +void Frame_Policy::request_automatic_frame(Frame_Request request) { + if (!std::isfinite(request.time_milliseconds) || + request.time_milliseconds < 0.0) + throw std::invalid_argument( + "frame request time must be finite and non-negative"); + if (request.width == 0 || request.height == 0) + throw std::invalid_argument("frame request extent must be non-zero"); if (request.issued_at.time_since_epoch().count() == 0) request.issued_at = std::chrono::steady_clock::now(); - const auto source = request.source; - const auto occurred_at = request.issued_at; - auto& data = static_cast(*d); - if (!data.requests.enqueue(std::move(request))) throw std::bad_alloc{}; - submit({.type = detail::Frame_Policy_Event_Type::request_received, - .source = source, - .occurred_ns = monotonic_nanoseconds(occurred_at)}); + submit_stream( + std::move(request)); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } -std::optional Frame_Policy::begin_production() { - auto& data = static_cast(*d); - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - auto production = detail::begin_frame_production(data.requests, state); - publish_state(); - return production; -} - -void Frame_Policy::defer_production(Frame_Production production) { - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - detail::defer_frame_production(state, std::move(production)); - publish_state(); -} - -void Frame_Policy::reject_production(std::uint64_t sequence) { - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - detail::reject_frame_production(state, sequence); - publish_state(); -} - -void Frame_Policy::submit_publication_dispatch( - Frame_Publication_Dispatch dispatch) { - if (dispatch.started_at.time_since_epoch().count() == 0 || - dispatch.completed_at < dispatch.started_at) +void Frame_Policy::begin_stop(Stop_Callback completed) { + if (!completed) throw std::invalid_argument( - "frame publication requires an ordered dispatch interval"); - submit({.type = detail::Frame_Policy_Event_Type::publication_dispatched, - .sequence = dispatch.sequence, - .occurred_ns = monotonic_nanoseconds(dispatch.started_at), - .duration_ns = monotonic_nanoseconds(dispatch.completed_at), - .count = dispatch.asynchronous_feedback_count, - .enabled = dispatch.succeeded}); + "Frame Policy stop requires a completion callback"); + submit_stream( + detail::Frame_Policy_Stop{std::move(completed)}); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } void Frame_Policy::submit_publication_feedback( - Frame_Publication_Feedback feedback) { - if (feedback.completed_at.time_since_epoch().count() == 0) - feedback.completed_at = std::chrono::steady_clock::now(); - submit({.type = detail::Frame_Policy_Event_Type::publication_feedback, - .sequence = feedback.sequence, - .occurred_ns = monotonic_nanoseconds(feedback.completed_at), - .enabled = feedback.succeeded}); + Frame_Publication_Feedback feedback) noexcept { + try { + submit_stream( + std::move(feedback)); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); + } + catch (...) { + double_buffer::detail::Internal_Access::layer(this). + runtime->report_failure(std::current_exception()); + } } -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::pixel_delivery_enabled: - state.pixel_delivery_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::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::publication_dispatched: - detail::begin_frame_publication( - state, event.sequence, event.occurred_ns, - event.duration_ns, event.count, event.enabled); - break; - case detail::Frame_Policy_Event_Type::publication_feedback: - detail::observe_publication_feedback( - state, event.sequence, event.occurred_ns, - event.enabled); - break; - case detail::Frame_Policy_Event_Type::reset: - reset_counters(state, event.occurred_ns); - break; - } - } - }); - if (consumed) publish_state(); - return configuration_changed; +void Frame_Policy::reset_statistics() { + submit_stream( + detail::Frame_Policy_Reset_Statistics{}); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } } diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp index 68c184e..a23463b 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp @@ -1,174 +1,194 @@ #pragma once -#include "double_buffer/model.hpp" +#include +#include +#include +#include +#include #include #include #include -#include +#include +#include +#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 -}; +enum struct Frame_Policy_Lifecycle : std::uint8_t { running, stopping, stopped }; +enum struct Frame_Policy_Type : std::uint8_t { manual, fixed_rate, maximum_rate }; struct Frame_Request { - std::chrono::steady_clock::time_point issued_at{}; /* 请求进入帧策略的单调时刻。 */ - std::uint64_t sequence{}; /* 全局帧时钟关联序号;手动请求为 0。 */ - double time_milliseconds{}; /* 页面单调时间线上的动画时刻。 */ - std::uint32_t width{320}; /* 无媒体消费者时使用的请求宽度。 */ - std::uint32_t height{192}; /* 无媒体消费者时使用的请求高度。 */ - Frame_Request_Source source{Frame_Request_Source::immediate}; /* 请求来源及合并优先级。 */ + std::chrono::steady_clock::time_point issued_at{}; + std::uint64_t sequence{}; + double time_milliseconds{}; + std::uint32_t width{320}; + std::uint32_t height{192}; + Frame_Request_Source source{Frame_Request_Source::immediate}; bool operator==(const Frame_Request&) const = default; }; struct Frame_Production { - Frame_Request request{}; /* 已通过策略准入、必须完成或显式退回的生产请求。 */ - std::uint64_t sequence{}; /* 帧策略分配的 Plot 单调帧序号。 */ + Frame_Request request{}; + std::uint64_t sequence{}; }; struct Frame_Publication_Feedback { - std::chrono::steady_clock::time_point completed_at{}; /* 媒体发布反馈任务实际执行的单调时刻。 */ - std::uint64_t sequence{}; /* 被反馈的策略帧序号。 */ - bool succeeded{}; /* 至少一个媒体传输是否成功接收该帧。 */ + std::chrono::steady_clock::time_point completed_at{}; + std::uint64_t sequence{}; + bool succeeded{}; }; struct Frame_Publication_Dispatch { - std::chrono::steady_clock::time_point started_at{}; /* Plot 开始分派完成帧的单调时刻。 */ - std::chrono::steady_clock::time_point completed_at{}; /* Plot 完成全部订阅者调用的单调时刻。 */ - std::uint64_t sequence{}; /* 被分派的策略帧序号。 */ - std::size_t asynchronous_feedback_count{}; /* 已接受并承诺异步反馈的媒体任务数。 */ - bool succeeded{}; /* 同步消费者是否至少成功一个。 */ + std::chrono::steady_clock::time_point started_at{}; + std::chrono::steady_clock::time_point completed_at{}; + std::size_t asynchronous_feedback_count{}; + bool succeeded{}; + std::uint32_t pixel_width{}; + std::uint32_t pixel_height{}; }; +struct Frame_Policy_State { + std::uint64_t generation{}; + 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 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 latest_publication_dispatch_latency_ns{}; + std::uint64_t publication_dispatch_latency_total_ns{}; + std::uint64_t maximum_publication_dispatch_latency_ns{}; + std::uint64_t published_frame_count{}; + std::uint64_t publication_failed_count{}; + std::uint64_t last_frame_sequence{}; + Frame_Request_Source last_request_source{}; + std::uint64_t overlapped_release_count{}; + std::uint64_t completion_gated_release_count{}; + Frame_Statistics_State frame_statistics{}; + std::uint32_t pixel_width{}; + std::uint32_t pixel_height{}; + bool operator==(const Frame_Policy_State&) const = default; +}; + +using Frame_Policy_Stop_Callback = std::move_only_function; + namespace detail { -struct Frame_Policy_Event_Stream; -enum struct Frame_Policy_Event_Type : std::uint8_t { - render_enabled, - pixel_delivery_enabled, - mode, - fixed_rate, - request_received, - frame_submitted, - frame_completed, - publication_dispatched, - publication_feedback, - reset +struct Frame_Policy_Request_Stream_Tag {}; +struct Frame_Policy_Scene_Stream_Tag {}; +struct Frame_Policy_Control_Stream_Tag {}; +struct Frame_Policy_Feedback_Stream_Tag {}; + +struct Frame_Policy_Scene_Submitted { + not_null frame; + std::chrono::steady_clock::time_point submitted_at{}; + bool released_after_submission{}; }; -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{}; - std::uint64_t count{}; - double number{}; - bool enabled{}; +struct Frame_Policy_Scene_Completed { + not_null frame; + std::chrono::steady_clock::time_point completed_at{}; }; +struct Frame_Policy_Render_Returned { + not_null frame; + Render_Frame_Result result{Render_Frame_Result::backend_unavailable}; +}; +using Frame_Policy_Scene_Message = std::variant< + std::monostate, + Frame_Policy_Scene_Submitted, + Frame_Policy_Scene_Completed, + Frame_Policy_Render_Returned>; + +struct Frame_Policy_Reset_Statistics {}; +struct Frame_Policy_Stop { Frame_Policy_Stop_Callback completed{}; }; +using Frame_Policy_Control = std::variant< + std::monostate, + Frame_Policy_Reset_Statistics, + Frame_Policy_Stop>; } -/* - * 帧策略的配置、累计事实和发布状态均归属同一个 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{}; +class Frame_Policy : + public double_buffer::Def< + Frame_Policy, + double_buffer::Root, + double_buffer::Mpmc_Triple_Buffer< + detail::Frame_Policy_Request_Stream_Tag, Frame_Request>, + double_buffer::Mpmc_Triple_Buffer< + detail::Frame_Policy_Scene_Stream_Tag, + detail::Frame_Policy_Scene_Message>, + double_buffer::Mpmc_Triple_Buffer< + detail::Frame_Policy_Control_Stream_Tag, + detail::Frame_Policy_Control>, + double_buffer::Mpmc_Triple_Buffer< + detail::Frame_Policy_Feedback_Stream_Tag, + Frame_Publication_Feedback>>, + public std::enable_shared_from_this { +public: + using Stop_Callback = Frame_Policy_Stop_Callback; + using Frame_Creator = std::move_only_function()>; + using Frame_Preparer = std::move_only_function, const Frame_Production&)>; + using Frame_Publisher = std::move_only_function, const Frame_Production&)>; + using Owner_Retainer = std::move_only_function()>; + using Failure_Handler = std::move_only_function; + + struct Prop : Prev_Prop { + std::size_t frame_capacity{3}; bool render_enabled{true}; bool pixel_delivery_enabled{true}; - Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; - double fixed_rate_fps{30.0}; - - std::optional pending_request{}; /* 尚未生产时只保留最高优先级的最新请求。 */ - std::uint64_t next_frame_sequence{1}; /* 下一次生产准入分配的单调序号。 */ - std::uint64_t in_flight_frame_sequence{}; /* 非零时帧策略唯一管理的生产到发布在途帧。 */ - std::uint64_t publication_started_ns{}; /* 当前在途帧开始向媒体发布的单调时刻。 */ - std::uint64_t publication_feedback_expected{}; /* 本帧异步媒体发布任务总数。 */ - std::uint64_t publication_feedback_received{}; /* 已由 Taskflow 节点报告的媒体发布任务数。 */ - std::uint64_t publication_latest_feedback_ns{}; /* 最近一次媒体发布反馈完成时刻。 */ - bool publication_dispatch_known{}; /* Plot 分派结果是否已由策略消费者归并。 */ - bool publication_dispatch_succeeded{}; /* Plot 同步分派阶段是否已有成功消费者。 */ - bool publication_feedback_succeeded{}; /* 异步媒体反馈是否已有成功结果。 */ - std::uint64_t latest_publication_dispatch_latency_ns{}; - std::uint64_t publication_dispatch_latency_total_ns{}; - std::uint64_t maximum_publication_dispatch_latency_ns{}; - std::uint64_t published_frame_count{}; /* 至少一次发布成功并完成反馈的累计帧数。 */ - std::uint64_t publication_failed_count{}; /* 没有媒体消费者发布成功的累计帧数。 */ - std::uint64_t latest_publication_latency_ns{}; /* 最近一帧从 Plot 发布到反馈任务的墙钟耗时。 */ - std::uint64_t publication_latency_total_ns{}; - std::uint64_t maximum_publication_latency_ns{}; - - 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 Prop&) const = default; + }; + struct State : Prev_State { + Frame_Policy_State observation{}; + Frame_Policy_Lifecycle lifecycle{Frame_Policy_Lifecycle::running}; bool operator==(const State&) const = default; }; struct Private; - void set_render_enabled(bool enabled); - void set_pixel_delivery_enabled(bool enabled); - void set_mode(Frame_Pacing_Mode mode); - void set_fixed_rate(double fps); + struct Dependencies { + not_null scene; + Frame_Creator create_frame{}; + Frame_Preparer prepare_frame{}; + Frame_Publisher publish_frame{}; + Owner_Retainer retain_owner{}; + Failure_Handler report_failure{}; + std::size_t frame_capacity{3}; + }; + + virtual ~Frame_Policy(); + Frame_Policy(const Frame_Policy&) = delete; + Frame_Policy& operator=(const Frame_Policy&) = delete; + [[nodiscard]] virtual Frame_Policy_Type type() const noexcept = 0; + [[nodiscard]] virtual std::string_view name() const noexcept = 0; + virtual void start() = 0; + virtual void stop(Stop_Callback completed) = 0; + void request_frame(Frame_Request request); + void submit_publication_feedback(Frame_Publication_Feedback feedback) noexcept; void reset_statistics(); - void submit_request(Frame_Request request); - [[nodiscard]] std::optional begin_production(); - void defer_production(Frame_Production production); - void reject_production(std::uint64_t sequence); - void submit_publication_dispatch(Frame_Publication_Dispatch dispatch); - void submit_publication_feedback(Frame_Publication_Feedback feedback); - 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); +protected: + explicit Frame_Policy(Dependencies dependencies); + void request_automatic_frame(Frame_Request request); + void begin_stop(Stop_Callback completed); + virtual void production_capacity_available() = 0; }; } diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp b/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp index 72adcb8..cf66954 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp @@ -1,10 +1,136 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include namespace aethera { struct Frame_Policy::Private : Prev_Private { - moodycamel::ConcurrentQueue requests{}; /* MPMC 请求入口;唯一策略 consumer 归并到 State。 */ + enum struct Frame_State : std::uint8_t { + available, + preparing, + scene_borrowed, + publishing + }; + struct Frame_Slot { + std::unique_ptr frame{}; + Frame_State state{Frame_State::available}; + Frame_Production production{}; + std::chrono::steady_clock::time_point submitted_at{}; + std::shared_ptr owner_lifetime{}; + bool render_returned{}; + bool producer_release_requested{}; + bool released_after_submission{}; + }; + struct Runtime { + not_null scene; + Frame_Creator create_frame{}; + Frame_Preparer prepare_frame{}; + Frame_Publisher publish_frame{}; + Owner_Retainer retain_owner{}; + Failure_Handler report_failure{}; + std::vector> frames{}; + std::optional pending_request{}; + Frame_Statistics_Accumulator statistics{600}; + Stop_Callback stop_callback{}; + std::uint64_t next_sequence{1}; + std::uint64_t reset_generation{}; + bool production_active{}; + + explicit Runtime(Dependencies dependencies) + : scene(dependencies.scene), + create_frame(std::move(dependencies.create_frame)), + prepare_frame(std::move(dependencies.prepare_frame)), + publish_frame(std::move(dependencies.publish_frame)), + retain_owner(std::move(dependencies.retain_owner)), + report_failure(std::move(dependencies.report_failure)) { + if (!create_frame || !prepare_frame || !publish_frame || + !retain_owner || !report_failure) + throw std::invalid_argument( + "Frame Policy requires complete runtime dependencies"); + if (dependencies.frame_capacity == 0) + throw std::invalid_argument( + "Frame Policy requires at least one frame slot"); + frames.reserve(dependencies.frame_capacity); + for (std::size_t index = 0; + index < dependencies.frame_capacity; ++index) { + auto slot = std::make_unique(); + slot->frame = create_frame(); + if (!slot->frame) + throw std::logic_error( + "Frame Policy creator returned an empty frame"); + frames.push_back(std::move(slot)); + } + } + + [[nodiscard]] Frame_Slot* find_frame( + not_null frame) noexcept { + const auto found = std::ranges::find_if( + frames, [frame](const auto& slot) { + return slot->frame.get() == frame.get(); + }); + return found == frames.end() ? nullptr : found->get(); + } + [[nodiscard]] std::size_t active_count() const noexcept { + return static_cast(std::ranges::count_if( + frames, [](const auto& slot) { + return slot->state != Frame_State::available; + })); + } + [[nodiscard]] Frame_Slot* acquire_frame() noexcept { + const auto found = std::ranges::find_if( + frames, [](const auto& slot) { + return slot->state == Frame_State::available; + }); + if (found == frames.end()) return nullptr; + (*found)->state = Frame_State::preparing; + return found->get(); + } + void coalesce(Frame_Request request) { + if (pending_request && + pending_request->source == Frame_Request_Source::immediate && + request.source != Frame_Request_Source::immediate) + return; + pending_request = std::move(request); + } + }; + + std::unique_ptr runtime{}; + std::atomic_bool consumer_active{}; + + void initialize(Dependencies dependencies) { + runtime = std::make_unique(std::move(dependencies)); + } + template + void after_prop_set(Frame_Policy* object, Member Owner::*, + double_buffer::Prop_Access) { + schedule(not_null{object}); + } + [[nodiscard]] std::uint64_t submission_generation( + const Frame_Policy* object) const noexcept; + void schedule(not_null object); + void submit_scene(not_null object, + detail::Frame_Policy_Scene_Message message); + void drain_requests(not_null object, + Frame_Policy::State& state); + void drain_controls(not_null object, + Frame_Policy::State& state); + void record_submission(Frame_Slot& slot, Frame_Policy::State& state); + void complete_frame(not_null object, Frame_Slot& slot, + std::chrono::steady_clock::time_point completed_at, + Frame_Policy::State& state, + bool& capacity_available); + void drain_scene(not_null object, + Frame_Policy::State& state, + bool& capacity_available); + void produce(not_null object, + const Frame_Policy::Prop& prop, + Frame_Policy::State& state); + void finish_stop(Frame_Policy::State& state); + void consume(not_null object) noexcept; }; } diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp deleted file mode 100644 index 68db079..0000000 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp +++ /dev/null @@ -1,323 +0,0 @@ -#include "Frame_Policy_3D.hpp" -#include "Frame_Policy_Lifecycle.ipp" -#include -#include -#include -#include -#include - -namespace aethera { -namespace { - -std::uint64_t monotonic_nanoseconds_3d( - 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_3d( - std::chrono::steady_clock::duration value) noexcept { - return static_cast(std::max( - 0, std::chrono::duration_cast(value).count())); -} - -void observe_common_event(Frame_Policy::State& state, - const detail::Frame_Policy_Event& event) noexcept { - if (event.occurred_ns == 0) return; - 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_common_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 pixel_delivery_enabled = state.pixel_delivery_enabled; - const auto mode = state.mode; - const auto fixed_rate_fps = state.fixed_rate_fps; - auto pending_request = std::move(state.pending_request); - const auto next_frame_sequence = state.next_frame_sequence; - const auto in_flight_frame_sequence = state.in_flight_frame_sequence; - const auto publication_started_ns = state.publication_started_ns; - const auto publication_feedback_expected = - state.publication_feedback_expected; - const auto publication_feedback_received = - state.publication_feedback_received; - const auto publication_latest_feedback_ns = - state.publication_latest_feedback_ns; - const auto publication_dispatch_known = state.publication_dispatch_known; - const auto publication_dispatch_succeeded = - state.publication_dispatch_succeeded; - const auto publication_feedback_succeeded = - state.publication_feedback_succeeded; - state = {}; - state.generation = generation; - state.render_enabled = render_enabled; - state.pixel_delivery_enabled = pixel_delivery_enabled; - state.mode = mode; - state.fixed_rate_fps = fixed_rate_fps; - state.pending_request = std::move(pending_request); - state.next_frame_sequence = next_frame_sequence; - state.in_flight_frame_sequence = in_flight_frame_sequence; - state.publication_started_ns = publication_started_ns; - state.publication_feedback_expected = publication_feedback_expected; - state.publication_feedback_received = publication_feedback_received; - state.publication_latest_feedback_ns = publication_latest_feedback_ns; - state.publication_dispatch_known = publication_dispatch_known; - state.publication_dispatch_succeeded = publication_dispatch_succeeded; - state.publication_feedback_succeeded = publication_feedback_succeeded; - state.observation_started_ns = occurred_ns; - state.observed_until_ns = occurred_ns; -} - -} - -void Frame_Policy_3D::submit(detail::Frame_Policy_Event event) { - if (event.occurred_ns == 0) - event.occurred_ns = monotonic_nanoseconds_3d(); - submit_stream(std::move(event)); -} - -void Frame_Policy_3D::set_render_enabled(bool enabled) { - submit({.type = detail::Frame_Policy_Event_Type::render_enabled, - .enabled = enabled}); -} - -void Frame_Policy_3D::set_pixel_delivery_enabled(bool enabled) { - submit({.type = detail::Frame_Policy_Event_Type::pixel_delivery_enabled, - .enabled = enabled}); -} - -void Frame_Policy_3D::set_mode(Frame_Pacing_Mode mode) { - submit({.type = detail::Frame_Policy_Event_Type::mode, .mode = mode}); -} - -void Frame_Policy_3D::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_3D::reset_statistics() { - submit({.type = detail::Frame_Policy_Event_Type::reset}); -} - -void Frame_Policy_3D::submit_request(Frame_Request request) { - if (request.issued_at.time_since_epoch().count() == 0) - request.issued_at = std::chrono::steady_clock::now(); - const auto source = request.source; - const auto occurred_at = request.issued_at; - auto& data = static_cast(*d); - if (!data.requests.enqueue(std::move(request))) throw std::bad_alloc{}; - submit({.type = detail::Frame_Policy_Event_Type::request_received, - .source = source, - .occurred_ns = monotonic_nanoseconds_3d(occurred_at)}); -} - -std::optional Frame_Policy_3D::begin_production() { - auto& data = static_cast(*d); - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - auto production = detail::begin_frame_production( - data.requests, state.common); - publish_state(); - return production; -} - -void Frame_Policy_3D::defer_production(Frame_Production production) { - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - detail::defer_frame_production( - state.common, std::move(production)); - publish_state(); -} - -void Frame_Policy_3D::reject_production(std::uint64_t sequence) { - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - detail::reject_frame_production(state.common, sequence); - publish_state(); -} - -void Frame_Policy_3D::submit_publication_dispatch( - Frame_Publication_Dispatch dispatch) { - if (dispatch.started_at.time_since_epoch().count() == 0 || - dispatch.completed_at < dispatch.started_at) - throw std::invalid_argument( - "3D frame publication requires an ordered dispatch interval"); - submit({.type = detail::Frame_Policy_Event_Type::publication_dispatched, - .sequence = dispatch.sequence, - .occurred_ns = monotonic_nanoseconds_3d(dispatch.started_at), - .duration_ns = monotonic_nanoseconds_3d(dispatch.completed_at), - .count = dispatch.asynchronous_feedback_count, - .enabled = dispatch.succeeded}); -} - -void Frame_Policy_3D::submit_publication_feedback( - Frame_Publication_Feedback feedback) { - if (feedback.completed_at.time_since_epoch().count() == 0) - feedback.completed_at = std::chrono::steady_clock::now(); - submit({.type = detail::Frame_Policy_Event_Type::publication_feedback, - .sequence = feedback.sequence, - .occurred_ns = monotonic_nanoseconds_3d(feedback.completed_at), - .enabled = feedback.succeeded}); -} - -void Frame_Policy_3D::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_3d(tick_queue_duration)}); -} - -void Frame_Policy_3D::record_frame_completed( - std::uint64_t sequence, - std::chrono::steady_clock::duration completion_latency, - bool prepare_released_after_submission) { - const auto occurred_ns = monotonic_nanoseconds_3d(); - const auto duration_ns = duration_nanoseconds_3d(completion_latency); - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - auto& common = state.common; - if (common.observation_started_ns == 0) - common.observation_started_ns = occurred_ns; - common.observed_until_ns = std::max( - common.observed_until_ns, occurred_ns); - ++common.completed_frame_count; - if (common.active_frame_count != 0) --common.active_frame_count; - common.last_frame_sequence = sequence; - common.latest_completion_latency_ns = duration_ns; - common.completion_latency_total_ns += duration_ns; - common.maximum_completion_latency_ns = std::max( - common.maximum_completion_latency_ns, duration_ns); - if (common.first_completion_ns == 0) - common.first_completion_ns = occurred_ns; - if (common.last_completion_ns != 0) { - const auto interval = occurred_ns - common.last_completion_ns; - ++common.completion_interval_count; - common.latest_completion_interval_ns = interval; - common.completion_interval_total_ns += interval; - const auto value = static_cast(interval); - common.completion_interval_squared_total_ns2 += value * value; - } - common.last_completion_ns = occurred_ns; - - ++state.gpu_completion_count; - state.last_completed_sequence = sequence; - if (prepare_released_after_submission) - ++state.overlapped_release_count; - else - ++state.completion_gated_release_count; - publish_state(); -} - -bool Frame_Policy_3D::consume_events() { - exchange_stream(); - bool consumed{}; - bool configuration_changed{}; - auto& state = static_cast( - double_buffer::detail::Internal_Access::pending_state(this)); - auto& common = state.common; - access_rendering_stream( - [&](std::span events) { - for (const auto& event : events) { - consumed = true; - observe_common_event(common, event); - switch (event.type) { - case detail::Frame_Policy_Event_Type::render_enabled: - configuration_changed |= - common.render_enabled != event.enabled; - common.render_enabled = event.enabled; - ++common.generation; - break; - case detail::Frame_Policy_Event_Type::pixel_delivery_enabled: - common.pixel_delivery_enabled = event.enabled; - ++common.generation; - break; - case detail::Frame_Policy_Event_Type::mode: - configuration_changed |= common.mode != event.mode; - common.mode = event.mode; - ++common.generation; - break; - case detail::Frame_Policy_Event_Type::fixed_rate: - configuration_changed |= - common.fixed_rate_fps != event.number; - common.fixed_rate_fps = event.number; - ++common.generation; - break; - case detail::Frame_Policy_Event_Type::request_received: - ++common.request_count; - if (event.source == Frame_Request_Source::periodic) - ++common.periodic_request_count; - else if (event.source == Frame_Request_Source::immediate) - ++common.immediate_request_count; - else if (event.source == Frame_Request_Source::maximum_rate) - ++common.maximum_rate_request_count; - break; - case detail::Frame_Policy_Event_Type::frame_submitted: - ++common.submitted_frame_count; - ++common.active_frame_count; - common.last_frame_sequence = event.sequence; - common.last_request_source = event.source; - common.latest_tick_queue_ns = event.duration_ns; - common.tick_queue_total_ns += event.duration_ns; - common.maximum_tick_queue_ns = std::max( - common.maximum_tick_queue_ns, event.duration_ns); - break; - case detail::Frame_Policy_Event_Type::frame_completed: { - ++common.completed_frame_count; - if (common.active_frame_count != 0) - --common.active_frame_count; - common.last_frame_sequence = event.sequence; - common.latest_completion_latency_ns = event.duration_ns; - common.completion_latency_total_ns += event.duration_ns; - common.maximum_completion_latency_ns = std::max( - common.maximum_completion_latency_ns, - event.duration_ns); - if (common.first_completion_ns == 0) - common.first_completion_ns = event.occurred_ns; - if (common.last_completion_ns != 0) { - const auto interval = event.occurred_ns - - common.last_completion_ns; - ++common.completion_interval_count; - common.latest_completion_interval_ns = interval; - common.completion_interval_total_ns += interval; - const auto value = static_cast(interval); - common.completion_interval_squared_total_ns2 += - value * value; - } - common.last_completion_ns = event.occurred_ns; - break; - } - case detail::Frame_Policy_Event_Type::publication_dispatched: - detail::begin_frame_publication( - common, event.sequence, event.occurred_ns, - event.duration_ns, event.count, event.enabled); - break; - case detail::Frame_Policy_Event_Type::publication_feedback: - detail::observe_publication_feedback( - common, event.sequence, event.occurred_ns, - event.enabled); - break; - case detail::Frame_Policy_Event_Type::reset: - reset_common_counters(common, event.occurred_ns); - state.overlapped_release_count = 0; - state.completion_gated_release_count = 0; - state.gpu_completion_count = 0; - state.last_completed_sequence = 0; - break; - } - } - }); - if (consumed) publish_state(); - return configuration_changed; -} - -} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp deleted file mode 100644 index 8d94233..0000000 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include "Frame_Policy.hpp" -#include - -namespace aethera { - -/* - * 3D 策略是 Frame_Policy 的独立状态层。它复用公共请求/吞吐事实,但不改变 - * 2D Frame_Policy 的默认模式和完成释放语义。GPU 后端只把物理事实写入 - * Frame_3D;调用方的唯一策略写者在帧借用结束回调后读取并发布本 State。 - */ -struct Frame_Policy_3D final : double_buffer::Def< - Frame_Policy_3D, 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 { - Frame_Policy::State common{}; /* 3D 策略拥有的唯一公共节奏状态,不别名 2D 存储。 */ - std::uint64_t overlapped_release_count{}; /* 提交后即开放下一次 Prepare 的累计帧数。 */ - std::uint64_t completion_gated_release_count{}; /* 因共享资源而等 GPU 退休后开放的累计帧数。 */ - std::uint64_t gpu_completion_count{}; /* fence 正常或失败退休的累计帧数。 */ - std::uint64_t last_completed_sequence{}; /* 最近退休的帧序号;未退休时为 0。 */ - bool operator==(const State&) const = default; - }; - struct Private; - static constexpr std::size_t pipeline_capacity{3}; - - void set_render_enabled(bool enabled); - void set_pixel_delivery_enabled(bool enabled); - void set_mode(Frame_Pacing_Mode mode); - void set_fixed_rate(double fps); - void reset_statistics(); - - void submit_request(Frame_Request request); - [[nodiscard]] std::optional begin_production(); - void defer_production(Frame_Production production); - void reject_production(std::uint64_t sequence); - void submit_publication_dispatch(Frame_Publication_Dispatch dispatch); - void submit_publication_feedback(Frame_Publication_Feedback feedback); - void record_frame_submitted( - std::uint64_t sequence, Frame_Request_Source source, - std::chrono::steady_clock::duration tick_queue_duration); - /* 仅由调用方的唯一帧策略写者在 Scene 结束借用后调用并直接发布。 */ - void record_frame_completed( - std::uint64_t sequence, - std::chrono::steady_clock::duration completion_latency, - bool prepare_released_after_submission); - [[nodiscard]] bool consume_events(); - -private: - void submit(detail::Frame_Policy_Event event); -}; -} - -#include "Frame_Policy_3D.ipp" diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp deleted file mode 100644 index 14a2a39..0000000 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once -#include - -namespace aethera { - -struct Frame_Policy_3D::Private : Prev_Private { - moodycamel::ConcurrentQueue requests{}; /* MPMC 请求入口;唯一策略 consumer 归并到公共 State。 */ -}; - -} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp b/kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp deleted file mode 100644 index a6afdf1..0000000 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp +++ /dev/null @@ -1,158 +0,0 @@ -#pragma once -#include -#include - -namespace aethera::detail { - -inline int frame_request_priority(Frame_Request_Source source) noexcept { - switch (source) { - case Frame_Request_Source::unspecified: - case Frame_Request_Source::periodic: - return 0; - case Frame_Request_Source::maximum_rate: - return 1; - case Frame_Request_Source::immediate: - return 2; - } - return 0; -} - -inline void merge_frame_request(Frame_Policy::State& state, - Frame_Request request) { - if (!state.pending_request) { - state.pending_request = std::move(request); - return; - } - ++state.coalesced_request_count; - const auto current_priority = frame_request_priority( - state.pending_request->source); - const auto next_priority = frame_request_priority(request.source); - if (current_priority > next_priority || - (current_priority == next_priority && - state.pending_request->issued_at >= request.issued_at)) - return; - state.pending_request = std::move(request); -} - -inline bool frame_request_matches(const Frame_Policy::State& state, - Frame_Request_Source source) noexcept { - return state.render_enabled && - (source == Frame_Request_Source::immediate || - (source == Frame_Request_Source::periodic && - state.mode == Frame_Pacing_Mode::fixed_rate) || - (source == Frame_Request_Source::maximum_rate && - state.mode == Frame_Pacing_Mode::maximum_rate)); -} - -template -std::optional begin_frame_production( - Queue& requests, Frame_Policy::State& state) { - Frame_Request request; - while (requests.try_dequeue(request)) - merge_frame_request(state, std::move(request)); - if (state.in_flight_frame_sequence != 0 || !state.pending_request) - return std::nullopt; - if (!frame_request_matches(state, state.pending_request->source)) { - state.pending_request.reset(); - ++state.policy_rejection_count; - return std::nullopt; - } - Frame_Production production{ - std::move(*state.pending_request), state.next_frame_sequence++}; - state.pending_request.reset(); - state.in_flight_frame_sequence = production.sequence; - ++state.accepted_request_count; - return production; -} - -inline void defer_frame_production(Frame_Policy::State& state, - Frame_Production production) { - if (production.sequence == 0 || - state.in_flight_frame_sequence != production.sequence) - throw std::logic_error( - "deferred frame production does not own the delivery slot"); - state.in_flight_frame_sequence = 0; - ++state.frame_slot_backpressure_count; - merge_frame_request(state, std::move(production.request)); -} - -inline void reject_frame_production(Frame_Policy::State& state, - std::uint64_t sequence) { - if (sequence == 0 || state.in_flight_frame_sequence != sequence) - throw std::logic_error( - "rejected frame production does not own the delivery slot"); - state.in_flight_frame_sequence = 0; - ++state.scene_rejection_count; -} - -inline void finish_frame_publication(Frame_Policy::State& state, - std::uint64_t completed_ns) noexcept { - const bool succeeded = state.publication_dispatch_succeeded || - state.publication_feedback_succeeded; - if (succeeded) ++state.published_frame_count; - else ++state.publication_failed_count; - const auto latency = completed_ns >= state.publication_started_ns - ? completed_ns - state.publication_started_ns - : 0U; - state.latest_publication_latency_ns = latency; - state.publication_latency_total_ns += latency; - state.maximum_publication_latency_ns = std::max( - state.maximum_publication_latency_ns, latency); - state.in_flight_frame_sequence = 0; - state.publication_started_ns = 0; - state.publication_feedback_expected = 0; - state.publication_feedback_received = 0; - state.publication_latest_feedback_ns = 0; - state.publication_dispatch_known = false; - state.publication_dispatch_succeeded = false; - state.publication_feedback_succeeded = false; -} - -inline void begin_frame_publication( - Frame_Policy::State& state, - std::uint64_t sequence, - std::uint64_t started_ns, - std::uint64_t completed_ns, - std::size_t asynchronous_feedback_count, - bool dispatch_succeeded) { - if (sequence == 0 || state.in_flight_frame_sequence != sequence) - throw std::logic_error( - "frame publication does not own the in-flight policy frame"); - if (state.publication_dispatch_known) - throw std::logic_error("frame publication is already started"); - state.publication_started_ns = started_ns; - state.publication_feedback_expected = asynchronous_feedback_count; - state.publication_dispatch_known = true; - state.publication_dispatch_succeeded = dispatch_succeeded; - const auto dispatch_latency = completed_ns >= started_ns - ? completed_ns - started_ns - : 0U; - state.latest_publication_dispatch_latency_ns = dispatch_latency; - state.publication_dispatch_latency_total_ns += dispatch_latency; - state.maximum_publication_dispatch_latency_ns = std::max( - state.maximum_publication_dispatch_latency_ns, dispatch_latency); - if (state.publication_feedback_received >= - state.publication_feedback_expected) - finish_frame_publication( - state, std::max(completed_ns, - state.publication_latest_feedback_ns)); -} - -inline void observe_publication_feedback( - Frame_Policy::State& state, - std::uint64_t sequence, - std::uint64_t completed_ns, - bool succeeded) noexcept { - if (sequence == 0 || state.in_flight_frame_sequence != sequence) - return; - state.publication_feedback_succeeded |= succeeded; - ++state.publication_feedback_received; - state.publication_latest_feedback_ns = std::max( - state.publication_latest_feedback_ns, completed_ns); - if (state.publication_dispatch_known && - state.publication_feedback_received >= - state.publication_feedback_expected) - finish_frame_publication(state, completed_ns); -} - -} diff --git a/kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.cpp b/kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.cpp new file mode 100644 index 0000000..678a830 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.cpp @@ -0,0 +1,20 @@ +#include "Manual_Frame_Policy.hpp" + +namespace aethera { + +Manual_Frame_Policy::Manual_Frame_Policy(Dependencies dependencies) + : Frame_Policy(std::move(dependencies)) {} +Manual_Frame_Policy::~Manual_Frame_Policy() = default; +Frame_Policy_Type Manual_Frame_Policy::type() const noexcept { + return Frame_Policy_Type::manual; +} +std::string_view Manual_Frame_Policy::name() const noexcept { + return "manual"; +} +void Manual_Frame_Policy::start() {} +void Manual_Frame_Policy::stop(Stop_Callback completed) { + begin_stop(std::move(completed)); +} +void Manual_Frame_Policy::production_capacity_available() {} + +} diff --git a/kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.hpp new file mode 100644 index 0000000..71d6988 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Manual_Frame_Policy.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "Frame_Policy.hpp" + +namespace aethera { + +class Manual_Frame_Policy final : public Frame_Policy { +public: + explicit Manual_Frame_Policy(Dependencies dependencies); + ~Manual_Frame_Policy() override; + [[nodiscard]] Frame_Policy_Type type() const noexcept override; + [[nodiscard]] std::string_view name() const noexcept override; + void start() override; + void stop(Stop_Callback completed) override; +private: + void production_capacity_available() override; +}; + +} diff --git a/kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.cpp b/kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.cpp new file mode 100644 index 0000000..02e0323 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.cpp @@ -0,0 +1,39 @@ +#include "Maximum_Rate_Frame_Policy.hpp" + +namespace aethera { + +struct Maximum_Rate_Frame_Policy::Private { + std::chrono::steady_clock::time_point clock_origin{ + std::chrono::steady_clock::now()}; /* 自动帧动画时间原点。 */ +}; + +Maximum_Rate_Frame_Policy::Maximum_Rate_Frame_Policy( + Dependencies dependencies) + : Frame_Policy(std::move(dependencies)), + maximum(std::make_unique()) {} +Maximum_Rate_Frame_Policy::~Maximum_Rate_Frame_Policy() = default; +Frame_Policy_Type Maximum_Rate_Frame_Policy::type() const noexcept { + return Frame_Policy_Type::maximum_rate; +} +std::string_view Maximum_Rate_Frame_Policy::name() const noexcept { + return "maximum_rate"; +} +void Maximum_Rate_Frame_Policy::start() { + production_capacity_available(); +} +void Maximum_Rate_Frame_Policy::stop(Stop_Callback completed) { + begin_stop(std::move(completed)); +} +void Maximum_Rate_Frame_Policy::production_capacity_available() { + if (read_state().lifecycle != + Frame_Policy_Lifecycle::running || + !read_prop().render_enabled) return; + const auto now = std::chrono::steady_clock::now(); + request_automatic_frame(Frame_Request{ + .issued_at = now, + .time_milliseconds = std::chrono::duration( + now - maximum->clock_origin).count(), + .source = Frame_Request_Source::maximum_rate}); +} + +} diff --git a/kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.hpp new file mode 100644 index 0000000..2489023 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Maximum_Rate_Frame_Policy.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "Frame_Policy.hpp" +#include + +namespace aethera { + +class Maximum_Rate_Frame_Policy final : public Frame_Policy { +public: + explicit Maximum_Rate_Frame_Policy(Dependencies dependencies); + ~Maximum_Rate_Frame_Policy() override; + [[nodiscard]] Frame_Policy_Type type() const noexcept override; + [[nodiscard]] std::string_view name() const noexcept override; + void start() override; + void stop(Stop_Callback completed) override; +private: + struct Private; + std::unique_ptr maximum; /* 最大速率生产时间线的唯一所有权。 */ + void production_capacity_available() override; +}; + +} diff --git a/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp b/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp index 3245361..bdf3e2d 100644 --- a/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp +++ b/kernel/src/kernel/double_buffer/Mpmc_Triple_Buffer.hpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -36,6 +38,10 @@ public: void submit(Value value) { std::shared_lock lock(exchange_lock_); if (!collecting_->enqueue(std::move(value))) throw std::bad_alloc{}; + submission_generation_.fetch_add(1, std::memory_order_release); + } + [[nodiscard]] std::uint64_t submission_generation() const noexcept { + return submission_generation_.load(std::memory_order_acquire); } void advance() { std::unique_lock lock(exchange_lock_); @@ -121,6 +127,7 @@ private: } } mutable std::shared_mutex exchange_lock_; /* Serializes role exchange and stable role inspection. */ + std::atomic_uint64_t submission_generation_{}; /* Producer wake generation; payload authority remains the three queues. */ mutable Queue first_{}; /* First physical MPMC queue; role changes only under exchange_lock_. */ mutable Queue second_{}; /* Second physical MPMC queue; role changes only under exchange_lock_. */ mutable Queue third_{}; /* Third physical MPMC queue; role changes only under exchange_lock_. */ diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 1672fcc..d5cb585 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -69,6 +69,7 @@ public: template Tag> decltype(auto) pending_buffer() { return private_data().template pending_buffer(); } template void submit_stream(Input&& input) { private_data().template submit_stream(std::forward(input)); } + template [[nodiscard]] std::uint64_t stream_submission_generation() const noexcept { return private_data().template stream_submission_generation(); } template void exchange_stream() { private_data().template exchange_stream(); } template void accumulate_stream(std::size_t capacity) { private_data().template accumulate_stream(capacity); } template void accumulate_stream(Predicate&& retain) { private_data().template accumulate_stream(std::forward(retain)); } @@ -136,6 +137,7 @@ struct Def : Base, detail::Public_Access { using Access::set; using Access::set_state_callback; using Access::submit_stream; + using Access::stream_submission_generation; using Access::update_prop; using Access::update_state; using Access::validate_dependency_graph; @@ -433,6 +435,10 @@ public: data().mpmc_triple_buffer_storage.template get().submit(std::forward(input)); } template Tag> + [[nodiscard]] std::uint64_t stream_submission_generation() const noexcept { + return data().mpmc_triple_buffer_storage.template get().submission_generation(); + } + template Tag> void exchange_stream() { data().mpmc_triple_buffer_storage.template get().advance(); } diff --git a/kernel/src/kernel/frame.cpp b/kernel/src/kernel/frame.cpp index 3a96316..2db730f 100644 --- a/kernel/src/kernel/frame.cpp +++ b/kernel/src/kernel/frame.cpp @@ -25,7 +25,6 @@ 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 加一编码。 */ @@ -38,11 +37,9 @@ 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()); @@ -265,7 +262,6 @@ 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 741d9b8..03c86f6 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -1,5 +1,4 @@ #pragma once -#include "Frame_Policy/Frame_Policy.hpp" #include "double_buffer/mechanism.hpp" #include #include @@ -7,6 +6,12 @@ #include #include namespace aethera { +enum struct Frame_Request_Source : std::uint8_t { + unspecified, + periodic, + immediate, + maximum_rate +}; namespace detail { struct Taskflow_Frame_Access; } @@ -175,7 +180,6 @@ 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 共用时间原点的原始流水线时间点。 */ @@ -198,8 +202,7 @@ public: protected: /* 物理帧槽再次承载新逻辑帧时,重建其唯一身份和诊断时间原点。 */ void begin(Frame_Identity identity, - Frame_Request_Source source = Frame_Request_Source::unspecified, - const Frame_Policy::State& frame_policy = {}) noexcept; + Frame_Request_Source source = Frame_Request_Source::unspecified) noexcept; private: struct Private; friend struct detail::Taskflow_Frame_Access; diff --git a/kernel/src/kernel/scene.hpp b/kernel/src/kernel/scene.hpp index ffddf96..00b8892 100644 --- a/kernel/src/kernel/scene.hpp +++ b/kernel/src/kernel/scene.hpp @@ -3,8 +3,26 @@ #include "renderable.hpp" #include #include +#include #include namespace aethera { +enum struct Render_Frame_Result : std::uint8_t { + submitted, + frame_pipeline_busy, + view_inactive, + empty_viewport, + backend_unavailable, + incompatible_frame +}; +struct Render_Frame_Callbacks { + std::function, bool)> submitted{}; /* Scene 已提交该借用帧;bool 表示是否允许并行准备下一帧。 */ + std::function)> completed{}; /* Scene 清除全部借用并完成帧数据附加。 */ +}; +struct Render_Frame_Scene { + virtual ~Render_Frame_Scene() = default; + [[nodiscard]] virtual Render_Frame_Result render_frame( + not_null frame, Render_Frame_Callbacks callbacks) = 0; +}; struct Scene_Event_Stream_Tag {}; /* Scene 状态标签,用于访问和订阅 Scene::State。 */ /* diff --git a/kernel/src/test/Frame_Policy_3D_Test.cpp b/kernel/src/test/Frame_Policy_3D_Test.cpp deleted file mode 100644 index b12fcf7..0000000 --- a/kernel/src/test/Frame_Policy_3D_Test.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "Frame_Policy/Frame_Policy_3D.hpp" -#include -#include -#include - -namespace { - -TEST(frame_policy_3d, completed_frame_publishes_frame_attached_gpu_facts) { - auto built = aethera::Frame_Policy_3D::Builder< - aethera::Frame_Policy_3D>{}.build(); - ASSERT_TRUE(built.has_value()); - auto policy = std::move(*built); - - policy->record_frame_submitted( - 11, aethera::Frame_Request_Source::maximum_rate, - std::chrono::microseconds{40}); - policy->record_frame_submitted( - 12, aethera::Frame_Request_Source::maximum_rate, - std::chrono::microseconds{50}); - static_cast(policy->consume_events()); - policy->record_frame_completed( - 11, std::chrono::microseconds{90}, true); - - const auto& state = policy->read_state< - aethera::Frame_Policy_3D::Base_Tag>(); - EXPECT_EQ(aethera::Frame_Policy_3D::pipeline_capacity, 3u); - EXPECT_EQ(state.overlapped_release_count, 1u); - EXPECT_EQ(state.completion_gated_release_count, 0u); - EXPECT_EQ(state.gpu_completion_count, 1u); - EXPECT_EQ(state.last_completed_sequence, 11u); - EXPECT_EQ(state.common.submitted_frame_count, 2u); - EXPECT_EQ(state.common.completed_frame_count, 1u); - EXPECT_EQ(state.common.active_frame_count, 1u); -} - -TEST(frame_policy_3d, common_reset_resets_completed_gpu_statistics) { - auto built = aethera::Frame_Policy_3D::Builder< - aethera::Frame_Policy_3D>{}.build(); - ASSERT_TRUE(built.has_value()); - auto policy = std::move(*built); - - const auto now = std::chrono::steady_clock::now(); - policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); - policy->submit_request(aethera::Frame_Request{ - .issued_at = now, - .source = aethera::Frame_Request_Source::maximum_rate}); - static_cast(policy->consume_events()); - const auto production = policy->begin_production(); - ASSERT_TRUE(production.has_value()); - policy->record_frame_submitted( - production->sequence, aethera::Frame_Request_Source::maximum_rate, - std::chrono::microseconds{40}); - EXPECT_FALSE(policy->consume_events()); - policy->record_frame_completed( - production->sequence, std::chrono::microseconds{90}, false); - - const auto& completed = policy->read_state< - aethera::Frame_Policy_3D::Base_Tag>(); - EXPECT_EQ(completed.common.completed_frame_count, 1u); - EXPECT_EQ(completed.completion_gated_release_count, 1u); - EXPECT_EQ(completed.gpu_completion_count, 1u); - - policy->reset_statistics(); - static_cast(policy->consume_events()); - const auto& reset = policy->read_state< - aethera::Frame_Policy_3D::Base_Tag>(); - EXPECT_EQ(reset.common.request_count, 0u); - EXPECT_EQ(reset.common.completed_frame_count, 0u); - EXPECT_EQ(reset.overlapped_release_count, 0u); - EXPECT_EQ(reset.completion_gated_release_count, 0u); - EXPECT_EQ(reset.gpu_completion_count, 0u); - EXPECT_EQ(reset.last_completed_sequence, 0u); -} - -} diff --git a/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp b/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp index 2d346f4..f7c4559 100644 --- a/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp +++ b/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp @@ -1,208 +1,228 @@ -#include "Frame_Policy/Frame_Policy.hpp" -#include "Frame_Policy/Frame_Policy_3D.hpp" -#include +#include "Frame_Policy/Fixed_Rate_Frame_Policy.hpp" +#include "Frame_Policy/Manual_Frame_Policy.hpp" +#include "Frame_Policy/Maximum_Rate_Frame_Policy.hpp" +#include +#include +#include #include -#include -#include #include namespace { -using namespace std::chrono_literals; - -class Publication_Task { -public: - MOCK_METHOD(bool, publish, (std::uint64_t sequence)); +struct Test_Frame final : aethera::Render_Frame { + explicit Test_Frame(aethera::Frame_Identity identity) + : Render_Frame(identity) {} }; -template -std::unique_ptr build_policy() { - auto built = typename Policy::template Builder{}.build(); - if (!built) throw std::logic_error("test frame policy build failed"); - return std::move(*built); -} +struct Test_Scene final : aethera::Render_Frame_Scene { + struct Borrow { + aethera::Render_Frame* frame{}; /* Policy 拥有、Scene 临时借用。 */ + aethera::Render_Frame_Callbacks callbacks{}; /* 对应借用帧的直接回调。 */ + }; + std::array borrows{}; + std::atomic_size_t submitted{}; + bool submit_during_render{true}; + bool complete_during_render{}; -template -const aethera::Frame_Policy::State& common_state(const Policy& policy) { - if constexpr (std::same_as) - return policy.template read_state(); - else - return policy.template read_state().common; -} - -TEST(frame_policy_lifecycle, - newest_interaction_owns_the_only_production_admission) { - auto policy = build_policy(); - const auto origin = std::chrono::steady_clock::now(); - policy->submit_request({ - .issued_at = origin, - .source = aethera::Frame_Request_Source::periodic}); - policy->submit_request({ - .issued_at = origin + 1ms, - .source = aethera::Frame_Request_Source::maximum_rate}); - policy->submit_request({ - .issued_at = origin + 2ms, - .time_milliseconds = 2.0, - .source = aethera::Frame_Request_Source::immediate}); - policy->submit_request({ - .issued_at = origin + 3ms, - .source = aethera::Frame_Request_Source::periodic}); - static_cast(policy->consume_events()); - - const auto production = policy->begin_production(); - ASSERT_TRUE(production.has_value()); - EXPECT_EQ(production->request.source, - aethera::Frame_Request_Source::immediate); - EXPECT_DOUBLE_EQ(production->request.time_milliseconds, 2.0); - const auto& state = common_state(*policy); - EXPECT_EQ(state.request_count, 4u); - EXPECT_EQ(state.coalesced_request_count, 3u); - EXPECT_EQ(state.accepted_request_count, 1u); - EXPECT_EQ(state.in_flight_frame_sequence, production->sequence); - EXPECT_FALSE(state.pending_request.has_value()); -} - -TEST(frame_policy_lifecycle, - production_stays_closed_until_publication_task_reports_feedback) { - auto policy = build_policy(); - Publication_Task publication; - const auto origin = std::chrono::steady_clock::now(); - policy->submit_request({ - .issued_at = origin, - .source = aethera::Frame_Request_Source::periodic}); - static_cast(policy->consume_events()); - const auto first = policy->begin_production(); - ASSERT_TRUE(first.has_value()); - - policy->record_frame_submitted( - first->sequence, first->request.source, 10us); - policy->record_frame_completed(first->sequence, 20us); - policy->submit_request({ - .issued_at = origin + 1ms, - .source = aethera::Frame_Request_Source::immediate}); - static_cast(policy->consume_events()); - EXPECT_FALSE(policy->begin_production().has_value()); - - policy->submit_publication_dispatch({ - .started_at = origin + 2ms, - .completed_at = origin + 2500us, - .sequence = first->sequence, - .asynchronous_feedback_count = 1, - .succeeded = false}); - EXPECT_CALL(publication, publish(first->sequence)) - .WillOnce(::testing::Return(true)); - policy->submit_publication_feedback({ - .completed_at = origin + 3ms, - .sequence = first->sequence, - .succeeded = publication.publish(first->sequence)}); - static_cast(policy->consume_events()); - - const auto second = policy->begin_production(); - ASSERT_TRUE(second.has_value()); - EXPECT_EQ(second->request.source, - aethera::Frame_Request_Source::immediate); - const auto& state = common_state(*policy); - EXPECT_EQ(state.published_frame_count, 1u); - EXPECT_EQ(state.latest_publication_dispatch_latency_ns, 500'000u); - EXPECT_EQ(state.latest_publication_latency_ns, 1'000'000u); -} - -TEST(frame_policy_lifecycle, - early_taskflow_feedback_is_reconciled_after_dispatch_registration) { - auto policy = build_policy(); - const auto origin = std::chrono::steady_clock::now(); - policy->submit_request({ - .issued_at = origin, - .source = aethera::Frame_Request_Source::immediate}); - static_cast(policy->consume_events()); - const auto production = policy->begin_production(); - ASSERT_TRUE(production.has_value()); - - policy->submit_publication_feedback({ - .completed_at = origin + 2ms, - .sequence = production->sequence, - .succeeded = true}); - policy->submit_publication_dispatch({ - .started_at = origin + 1ms, - .completed_at = origin + 1ms, - .sequence = production->sequence, - .asynchronous_feedback_count = 1, - .succeeded = false}); - static_cast(policy->consume_events()); - - const auto& state = common_state(*policy); - EXPECT_EQ(state.in_flight_frame_sequence, 0u); - EXPECT_EQ(state.published_frame_count, 1u); - EXPECT_EQ(state.latest_publication_latency_ns, 1'000'000u); -} - -TEST(frame_policy_lifecycle, - slow_sender_coalesces_request_storm_without_expanding_delivery_window) { - auto policy = build_policy(); - policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); - const auto origin = std::chrono::steady_clock::now(); - policy->submit_request({ - .issued_at = origin, - .source = aethera::Frame_Request_Source::maximum_rate}); - static_cast(policy->consume_events()); - const auto active = policy->begin_production(); - ASSERT_TRUE(active.has_value()); - policy->submit_publication_dispatch({ - .started_at = origin, - .completed_at = origin, - .sequence = active->sequence, - .asynchronous_feedback_count = 1, - .succeeded = false}); - - constexpr std::uint64_t request_count = 10'000; - for (std::uint64_t index = 0; index < request_count; ++index) { - policy->submit_request({ - .issued_at = origin + std::chrono::nanoseconds(index + 1), - .time_milliseconds = static_cast(index), - .source = index + 1 == request_count - ? aethera::Frame_Request_Source::immediate - : aethera::Frame_Request_Source::maximum_rate}); + aethera::Render_Frame_Result render_frame( + aethera::not_null frame, + aethera::Render_Frame_Callbacks callbacks) override { + const auto index = submitted.load(std::memory_order_relaxed); + borrows[index] = Borrow{frame.get(), std::move(callbacks)}; + submitted.store(index + 1, std::memory_order_release); + if (submit_during_render && borrows[index].callbacks.submitted) + borrows[index].callbacks.submitted(frame, true); + if (complete_during_render && borrows[index].callbacks.completed) + borrows[index].callbacks.completed(frame); + return aethera::Render_Frame_Result::submitted; } - static_cast(policy->consume_events()); - EXPECT_FALSE(policy->begin_production().has_value()); - const auto& blocked = common_state(*policy); - ASSERT_TRUE(blocked.pending_request.has_value()); - EXPECT_EQ(blocked.pending_request->source, - aethera::Frame_Request_Source::immediate); - EXPECT_EQ(blocked.in_flight_frame_sequence, active->sequence); - EXPECT_EQ(blocked.coalesced_request_count, request_count - 1); +}; - policy->submit_publication_feedback({ - .completed_at = origin + std::chrono::milliseconds{1}, - .sequence = active->sequence, - .succeeded = true}); - static_cast(policy->consume_events()); - const auto resumed = policy->begin_production(); - ASSERT_TRUE(resumed.has_value()); - EXPECT_EQ(resumed->request.source, - aethera::Frame_Request_Source::immediate); +struct Policy_Probe { + Test_Scene scene{}; + std::atomic_size_t created{}; + std::atomic_size_t prepared{}; + std::atomic_size_t published{}; + std::atomic_bool failed{}; + + aethera::Frame_Policy::Dependencies dependencies() { + return { + .scene = &scene, + .create_frame = [this] { + created.fetch_add(1, std::memory_order_relaxed); + return std::make_unique(aethera::Frame_Identity{}); + }, + .prepare_frame = [this]( + aethera::not_null, + const aethera::Frame_Production&) { + prepared.fetch_add(1, std::memory_order_release); + }, + .publish_frame = [this]( + aethera::not_null, + const aethera::Frame_Production&) { + published.fetch_add(1, std::memory_order_release); + const auto now = std::chrono::steady_clock::now(); + return aethera::Frame_Publication_Dispatch{ + .started_at = now, + .completed_at = now, + .succeeded = true}; + }, + .retain_owner = [] { return std::make_shared(1); }, + .report_failure = [this](std::exception_ptr) { + failed.store(true, std::memory_order_release); + }, + .frame_capacity = 3}; + } +}; + +template +void cooperate_until(Predicate predicate) { + aethera::Task_Graph driver{"test.frame_policy.cooperate"}; + driver.add("cooperate", [predicate = std::move(predicate)]() mutable { + aethera::Task_Graph::corun_until(std::move(predicate)); + }); + static_cast(aethera::detail::run_taskflow(driver)); +} + +TEST(frame_policy_lifecycle, concrete_policy_types_expose_only_their_mode) { + Policy_Probe manual_probe; + Policy_Probe fixed_probe; + Policy_Probe maximum_probe; + auto manual = std::make_shared( + manual_probe.dependencies()); + auto fixed = std::make_shared( + fixed_probe.dependencies(), 47.5); + auto maximum = std::make_shared( + maximum_probe.dependencies()); + + EXPECT_EQ(manual->type(), aethera::Frame_Policy_Type::manual); + EXPECT_EQ(fixed->type(), aethera::Frame_Policy_Type::fixed_rate); + EXPECT_EQ(maximum->type(), aethera::Frame_Policy_Type::maximum_rate); + EXPECT_DOUBLE_EQ(fixed->frame_rate(), 47.5); + EXPECT_THROW(fixed->set_frame_rate(0.0), std::invalid_argument); + EXPECT_EQ(manual_probe.created.load(), 3u); + EXPECT_EQ(fixed_probe.created.load(), 3u); + EXPECT_EQ(maximum_probe.created.load(), 3u); +} + +TEST(frame_policy_lifecycle, manual_policy_owns_reuses_and_publishes_frame) { + aethera::initialize_runtime({.workers = 4}); + Policy_Probe probe; + probe.scene.complete_during_render = true; + auto policy = std::make_shared( + probe.dependencies()); + policy->start(); + policy->request_frame(aethera::Frame_Request{ + .issued_at = std::chrono::steady_clock::now(), + .time_milliseconds = 1.0}); + cooperate_until([&] { + return probe.published.load(std::memory_order_acquire) == 1; + }); + + aethera::Frame_Policy_State state{}; + policy->access_state( + [&](const aethera::Frame_Policy::State& current) { + state = current.observation; + }); + EXPECT_EQ(state.completed_frame_count, 1u); + EXPECT_EQ(state.published_frame_count, 1u); + EXPECT_EQ(policy->read_state(). + observation.active_frame_count, 0u); + EXPECT_FALSE(probe.failed.load(std::memory_order_acquire)); } TEST(frame_policy_lifecycle, - deferred_physical_slot_returns_the_same_request_to_policy) { - auto policy = build_policy(); - const auto issued_at = std::chrono::steady_clock::now(); - policy->submit_request({ - .issued_at = issued_at, - .time_milliseconds = 17.0, - .source = aethera::Frame_Request_Source::periodic}); - static_cast(policy->consume_events()); - auto production = policy->begin_production(); - ASSERT_TRUE(production.has_value()); - const auto first_sequence = production->sequence; - policy->defer_production(std::move(*production)); + maximum_rate_fills_three_slots_and_stop_callback_waits_for_all_frames) { + aethera::initialize_runtime({.workers = 4}); + Policy_Probe probe; + auto policy = std::make_shared( + probe.dependencies()); + policy->start(); + cooperate_until([&] { + return probe.scene.submitted.load(std::memory_order_acquire) == 3; + }); + EXPECT_EQ(policy->read_state(). + observation.active_frame_count, 3u); - const auto retried = policy->begin_production(); - ASSERT_TRUE(retried.has_value()); - EXPECT_GT(retried->sequence, first_sequence); - EXPECT_EQ(retried->request.issued_at, issued_at); - EXPECT_DOUBLE_EQ(retried->request.time_milliseconds, 17.0); - EXPECT_EQ(common_state(*policy).frame_slot_backpressure_count, 1u); + bool stopped{}; + policy->stop([&] { stopped = true; }); + EXPECT_FALSE(stopped); + EXPECT_EQ(policy->read_state().lifecycle, + aethera::Frame_Policy_Lifecycle::stopping); + for (std::size_t index = 0; index < 3; ++index) { + auto& borrow = probe.scene.borrows[index]; + ASSERT_NE(borrow.frame, nullptr); + borrow.callbacks.completed(aethera::not_null{borrow.frame}); + } + EXPECT_TRUE(stopped); + EXPECT_EQ(policy->read_state().lifecycle, + aethera::Frame_Policy_Lifecycle::stopped); + EXPECT_EQ(policy->read_state(). + observation.active_frame_count, 0u); + EXPECT_EQ(probe.published.load(std::memory_order_acquire), 3u); +} + +TEST(frame_policy_lifecycle, + burst_requests_are_coalesced_while_scene_owns_cpu_production) { + aethera::initialize_runtime({.workers = 4}); + Policy_Probe probe; + probe.scene.submit_during_render = false; + auto policy = std::make_shared( + probe.dependencies()); + policy->start(); + policy->request_frame(aethera::Frame_Request{ + .issued_at = std::chrono::steady_clock::now(), + .time_milliseconds = 1.0}); + cooperate_until([&] { + return probe.scene.submitted.load(std::memory_order_acquire) == 1; + }); + + for (std::uint64_t index = 0; index < 64; ++index) { + policy->request_frame(aethera::Frame_Request{ + .issued_at = std::chrono::steady_clock::now(), + .sequence = index + 2, + .time_milliseconds = static_cast(index + 2)}); + } + std::atomic_bool burst_dispatched{}; + aethera::schedule_task("test.frame_policy.burst.dispatched", [&] { + burst_dispatched.store(true, std::memory_order_release); + }); + cooperate_until([&] { + return burst_dispatched.load(std::memory_order_acquire); + }); + EXPECT_EQ(probe.scene.submitted.load(std::memory_order_acquire), 1u); + EXPECT_EQ(probe.prepared.load(std::memory_order_acquire), 1u); + + auto& first = probe.scene.borrows[0]; + ASSERT_NE(first.frame, nullptr); + first.callbacks.submitted(aethera::not_null{first.frame}, false); + first.callbacks.completed(aethera::not_null{first.frame}); + cooperate_until([&] { + return probe.scene.submitted.load(std::memory_order_acquire) == 2; + }); + EXPECT_EQ(probe.prepared.load(std::memory_order_acquire), 2u); + + auto& second = probe.scene.borrows[1]; + ASSERT_NE(second.frame, nullptr); + second.callbacks.submitted(aethera::not_null{second.frame}, false); + second.callbacks.completed(aethera::not_null{second.frame}); + cooperate_until([&] { + return probe.published.load(std::memory_order_acquire) == 2; + }); + bool stopped{}; + policy->stop([&] { stopped = true; }); + EXPECT_TRUE(stopped); +} + +TEST(frame_policy_lifecycle, idle_stop_calls_completion_once) { + Policy_Probe probe; + auto policy = std::make_shared( + probe.dependencies()); + std::size_t calls{}; + policy->stop([&] { ++calls; }); + EXPECT_EQ(calls, 1u); + EXPECT_EQ(policy->read_state().lifecycle, + aethera::Frame_Policy_Lifecycle::stopped); } } diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index ec19edf..78951bf 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -53,36 +53,6 @@ inline Direct_Renderable::Private& Direct_Renderable::data_for_test() { return static_cast(*d); } -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_pixel_delivery_enabled(false); - policy->submit_request(aethera::Frame_Request{ - .issued_at = std::chrono::steady_clock::now(), - .source = aethera::Frame_Request_Source::maximum_rate}); - static_cast(policy->consume_events()); - const auto production = policy->begin_production(); - ASSERT_TRUE(production.has_value()); - policy->record_frame_submitted( - production->sequence, aethera::Frame_Request_Source::maximum_rate, - std::chrono::microseconds{250}); - EXPECT_FALSE(policy->consume_events()); - - const auto& state = policy->read_state(); - EXPECT_TRUE(state.render_enabled); - EXPECT_FALSE(state.pixel_delivery_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.in_flight_frame_sequence, production->sequence); - 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/mcp/core/runtime/Plot.cpp b/mcp/core/runtime/Plot.cpp index bb16762..15178f6 100644 --- a/mcp/core/runtime/Plot.cpp +++ b/mcp/core/runtime/Plot.cpp @@ -2,9 +2,9 @@ #include "Renderable_Adapter.hpp" #include "Taskflow_Trace_Json.hpp" #include -#include -#include -#include +#include +#include +#include #include #include #include @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -49,13 +50,13 @@ std::string exception_description(const std::exception_ptr& failure) { } return "empty Plot failure"; } -std::string_view pacing_mode_name(Frame_Pacing_Mode mode) { +std::string_view pacing_mode_name(Frame_Policy_Type mode) { const auto name = magic_enum::enum_name(mode); if (name.empty()) throw std::logic_error("unknown frame pacing mode"); return name; } -std::optional parse_pacing_mode(std::string_view value) { - return magic_enum::enum_cast(value); +std::optional parse_pacing_mode(std::string_view value) { + return magic_enum::enum_cast(value); } std::string_view pixel_format_name(render_2d::Pixel_Format format) { const auto name = magic_enum::enum_name(format); @@ -68,17 +69,8 @@ std::string_view pixel_format_name(render_3d::Pixel_Format format) { } throw std::logic_error("unknown 3D pixel format"); } -const Frame_Policy::State& frame_policy_common_state( - const Frame_Policy& policy) noexcept { - return policy.read_state(); -} -const Frame_Policy::State& frame_policy_common_state( - const Frame_Policy_3D& policy) noexcept { - return policy.read_state().common; -} -template -nlohmann::json frame_policy_schema(const Policy& policy) { - const auto& current = frame_policy_common_state(policy); +nlohmann::json frame_policy_schema(const Frame_Policy& policy) { + const auto* fixed = dynamic_cast(&policy); return { {"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"}, { @@ -87,19 +79,19 @@ nlohmann::json frame_policy_schema(const Policy& policy) { {"key", "render_enabled"}, {"label", "持续渲染与采样"}, {"editor", "boolean"}, {"editable", true}, {"description", "控制当前 Scene 的周期刷新;画面隐藏不会修改此项。"}, {"technical_description", "Authoritative per-scene periodic render switch."}, - {"value", current.render_enabled} + {"value", policy.get<&Frame_Policy::Prop::render_enabled>()} }, { {"key", "pixel_delivery_enabled"}, {"label", "图集像素传输"}, {"editor", "boolean"}, {"editable", true}, {"description", "控制完成帧是否进入页面级采样器;2D BGRA 与 3D RGBA 均保持原生格式。"}, {"technical_description", "Authoritative tile publication switch for the shared gallery pixel stream."}, - {"value", current.pixel_delivery_enabled} + {"value", policy.get<&Frame_Policy::Prop::pixel_delivery_enabled>()} }, { {"key", "pacing_mode"}, {"label", "服务端帧策略"}, {"editor", "select"}, {"editable", true}, {"description", "只控制 Scene::render(Frame*) 的调用节奏;Scene 的 Frame 所有权与接口保持不变。"}, {"technical_description", "Per-scene frame pacing policy backed by the Kernel scheduler."}, - {"value", pacing_mode_name(current.mode)}, + {"value", pacing_mode_name(policy.type())}, { "options", nlohmann::json::array({ {{"value", "manual"}, {"label", "手动渲染"}}, @@ -113,41 +105,34 @@ nlohmann::json frame_policy_schema(const Policy& policy) { {"editable", true}, {"minimum", 0.1}, {"maximum", 100.0}, {"step", 0.1}, {"description", "仅 fixed_rate 使用;maximum_rate 在完成准入释放后异步自驱下一帧。"}, {"technical_description", "Independent per-scene target frame rate."}, - {"value", current.fixed_rate_fps} + {"value", fixed ? fixed->frame_rate() : 30.0} } }) } }; } -template -nlohmann::json write_frame_policy_prop(Policy& pacing, +nlohmann::json write_frame_policy_prop(Frame_Policy& pacing, std::string_view key, const nlohmann::json& value) { if (key == "render_enabled" || key == "pixel_delivery_enabled") { if (!value.is_boolean()) return {{"success", false}, {"error", "frame policy switch requires a boolean"}}; const bool target = value.get(); - if (key == "render_enabled") pacing.set_render_enabled(target); - else pacing.set_pixel_delivery_enabled(target); + if (key == "render_enabled") + pacing.set<&Frame_Policy::Prop::render_enabled>(target); + else + pacing.set<&Frame_Policy::Prop::pixel_delivery_enabled>(target); return { {"success", true}, {"component", "frame-analysis"}, {"key", key}, {"value", target} }; } - if (key == "pacing_mode") { - if (!value.is_string()) return {{"success", false}, {"error", "pacing_mode requires a string"}}; - const auto parsed = parse_pacing_mode(value.get_ref()); - if (!parsed) return {{"success", false}, {"error", "unknown frame pacing mode"}}; - pacing.set_mode(*parsed); - return { - {"success", true}, {"component", "frame-analysis"}, {"key", key}, - {"value", pacing_mode_name(*parsed)} - }; - } if (key == "fixed_rate_fps") { if (!value.is_number()) return {{"success", false}, {"error", "fixed_rate_fps requires a number"}}; const double next = value.get(); if (!std::isfinite(next) || next < 0.1 || next > 100.0) return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 100"}}; - pacing.set_fixed_rate(next); + auto* fixed = dynamic_cast(&pacing); + if (!fixed) return {{"success", false}, {"error", "fixed_rate_fps belongs to Fixed_Rate_Frame_Policy"}}; + fixed->set_frame_rate(next); return { {"success", true}, {"component", "frame-analysis"}, {"key", key}, {"value", next} @@ -155,7 +140,8 @@ nlohmann::json write_frame_policy_prop(Policy& pacing, } return {{"success", false}, {"error", "unknown frame runtime property"}}; } -nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { +nlohmann::json frame_policy_state_json(const Frame_Policy_State& state, + const Frame_Policy& policy) { const auto milliseconds = [](std::uint64_t nanoseconds) { return static_cast(nanoseconds) / 1'000'000.0; }; @@ -188,18 +174,18 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { 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; + const auto* fixed = dynamic_cast(&policy); + const auto fixed_rate_fps = fixed ? fixed->frame_rate() : 0.0; + const auto target_achievement = fixed_rate_fps > 0.0 + ? effective_fps / fixed_rate_fps : 0.0; return { {"generation", state.generation}, { "configuration", { - {"mode", pacing_mode_name(state.mode)}, - {"render_enabled", state.render_enabled}, - {"pixel_delivery_enabled", state.pixel_delivery_enabled}, - {"fixed_rate_fps", state.fixed_rate_fps} + {"mode", pacing_mode_name(policy.type())}, + {"render_enabled", policy.get<&Frame_Policy::Prop::render_enabled>()}, + {"pixel_delivery_enabled", policy.get<&Frame_Policy::Prop::pixel_delivery_enabled>()}, + {"fixed_rate_fps", fixed_rate_fps} } }, { @@ -213,19 +199,9 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { }, { "lifecycle", { - {"pending_request", state.pending_request.has_value()}, - {"in_flight_window", 1}, - {"in_flight_frame_sequence", state.in_flight_frame_sequence}, - {"publication_feedback_expected", - state.publication_feedback_expected}, - {"publication_feedback_received", - state.publication_feedback_received}, - {"publication_feedback_remaining", - state.publication_feedback_expected > - state.publication_feedback_received - ? state.publication_feedback_expected - - state.publication_feedback_received - : 0}, + {"state", magic_enum::enum_name(policy.read_state().lifecycle)}, + {"in_flight_window", policy.get<&Frame_Policy::Prop::frame_capacity>()}, + {"active_frame_count", state.active_frame_count}, {"latest_publication_dispatch_ms", static_cast( state.latest_publication_dispatch_latency_ns) / @@ -236,12 +212,10 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { 1'000'000.0}, {"published_frame_count", state.published_frame_count}, {"publication_failed_count", state.publication_failed_count}, - {"latest_publication_ms", - static_cast(state.latest_publication_latency_ns) / - 1'000'000.0}, - {"maximum_publication_ms", - static_cast(state.maximum_publication_latency_ns) / - 1'000'000.0} + {"latest_publication_ms", static_cast( + state.latest_publication_dispatch_latency_ns) / 1'000'000.0}, + {"maximum_publication_ms", static_cast( + state.maximum_publication_dispatch_latency_ns) / 1'000'000.0} } }, { @@ -273,7 +247,7 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { {"immediate", state.immediate_request_count}, {"maximum_rate", state.maximum_rate_request_count}, {"accepted", state.accepted_request_count}, - {"coalesced", state.coalesced_request_count}, + {"coalesced", 0}, {"policy_rejected", state.policy_rejection_count}, {"frame_slot_backpressure", state.frame_slot_backpressure_count}, {"scene_rejected", state.scene_rejection_count}, @@ -283,7 +257,7 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { }, { "coalescing_ratio", ratio( - state.coalesced_request_count, state.request_count) + 0, state.request_count) }, { "backpressure_ratio", ratio( @@ -560,7 +534,6 @@ 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)}, @@ -638,60 +611,25 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { } } } -struct Plot::Private { +struct Plot::Private : Prev_Private { using Scene = std::variant, std::unique_ptr>; - using Frame = std::variant, std::unique_ptr>; - enum struct Frame_State : std::uint8_t { - available, - rendering, /* Scene::advance -> Plot pixel publish,不可重入。 */ - consuming /* 外接 Taskflow 正在消费已发布帧;允许下一帧渲染。 */ - }; - 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{}; /* 无锁诊断读取认领;非零时该槽不可复用。 */ - std::uint64_t statistics_generation{}; /* 与本槽中完成帧统计共同发布。 */ - Frame_Statistics_State statistics{}; /* 统计结果归属当前完成帧,不在 Plot 复制。 */ - std::atomic retired_next{}; /* 无锁退役队列的槽内侵入链接。 */ - }; - struct Consumer { - Stream_Handler handler; - std::uint32_t width{}; - std::uint32_t height{}; - }; - using Consumer_Map = std::unordered_map; struct Stream_Snapshot { - std::shared_ptr consumers; + detail::Plot_Consumer_Map consumers{}; std::uint32_t width{}; std::uint32_t height{}; }; std::unique_ptr view; std::once_flag start_once; - std::weak_ptr lifetime{}; /* 仅用于 completion 后重新投递 Taskflow,避免在 Scene callback 内重入 render。 */ - std::atomic> frame_policy_lifetime{}; /* 任一物理帧被借用期间由策略保活整个 Plot;最后一槽归还后释放。 */ - std::atomic> consumers{ - std::make_shared() - }; /* 低频订阅修改发布不可变版本。 */ + std::weak_ptr lifetime{}; /* Policy 回调只借用 Plot;在途帧另行保活。 */ struct Publication_Dispatch { std::size_t asynchronous_feedback_count{}; bool succeeded{}; }; std::atomic_uint64_t next_stream_id{1}; - std::atomic> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */ - std::unique_ptr frame_policy_2d{}; - std::unique_ptr frame_policy_3d{}; - Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */ - static constexpr std::size_t scene_frame_capacity{3}; - std::array frame_slots{}; /* 帧策略拥有并反复调度的稳定三缓冲;Scene 只借用。 */ - std::atomic latest_statistics_frame{}; /* 只定位权威帧槽,不保存统计副本。 */ - std::atomic retired_frames{}; /* 完成回调返回、唯一帧策略写者消费的物理帧。 */ - Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */ - std::atomic_uint64_t consumer_work_generation{}; /* tick 或退休帧入队后推进,关闭 consumer 尾部唤醒竞争窗口。 */ - std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */ - std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; + Scene scene; /* Plot 独占 Scene。 */ + std::atomic> frame_policy{}; /* 模型 Private 独占当前 Policy。 */ + std::atomic_bool consumer_active{}; + std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; /* 手动帧的 Plot 动画时间原点。 */ std::atomic_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */ static constexpr std::size_t maximum_taskflow_trace_frames{120}; /* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace, @@ -699,67 +637,26 @@ struct Plot::Private { std::atomic_uint64_t taskflow_trace_control{}; std::array>, maximum_taskflow_trace_frames> taskflow_trace_slots{}; - Frame_Statistics_Accumulator completed_frame_statistics{diagnostic_window_capacity}; - std::uint64_t applied_statistics_generation{}; /* 仅完成帧退役任务读写。 */ - std::atomic_uint64_t statistics_generation{}; /* reset 只推进代次,不触碰单写者累加器。 */ template - Private(std::unique_ptr value_scene, - std::unique_ptr value_view) : view(std::move(value_view)), scene(std::move(value_scene)) { - if constexpr (std::same_as) { - auto built_policy = Frame_Policy::Builder{}.build(); - if (!built_policy) throw std::logic_error("2D Frame Policy dependency graph is invalid"); - frame_policy_2d = std::move(*built_policy); - } - else { - auto built_policy = - Frame_Policy_3D::Builder{}.build(); - if (!built_policy) throw std::logic_error("3D Frame Policy dependency graph is invalid"); - frame_policy_3d = std::move(*built_policy); - frame_policy_3d->set_mode(Frame_Pacing_Mode::maximum_rate); - static_cast(frame_policy_3d->consume_events()); - } - for (auto& slot : frame_slots) { - if constexpr (std::same_as) slot.frame = std::make_unique(Frame_Identity{}); - else slot.frame = std::make_unique(Frame_Identity{}); - } - } - template - decltype(auto) with_frame_policy(Callback&& callback) { - if (frame_policy_2d) return std::forward(callback)(*frame_policy_2d); - return std::forward(callback)(*frame_policy_3d); - } - template - decltype(auto) with_frame_policy(Callback&& callback) const { - if (frame_policy_2d) return std::forward(callback)(*frame_policy_2d); - return std::forward(callback)(*frame_policy_3d); - } - [[nodiscard]] const Frame_Policy::State& pacing_state() const noexcept { - if (frame_policy_2d) return frame_policy_2d->read_state(); - return frame_policy_3d->read_state().common; - } - [[nodiscard]] bool consume_frame_policy_events() { - return with_frame_policy( - [](auto& policy) { - return policy.consume_events(); - }); + void initialize(std::unique_ptr value_scene, + std::unique_ptr value_view) { + view = std::move(value_view); + scene = std::move(value_scene); } + void schedule(not_null object); + void consume(not_null object) noexcept; + [[nodiscard]] std::shared_ptr create_frame_policy( + Frame_Policy_Type type, double fixed_rate_fps = 30.0); + void replace_frame_policy(Frame_Policy_Type type); + void prepare_policy_frame(not_null frame, + const Frame_Production& production); + [[nodiscard]] Frame_Publication_Dispatch publish_policy_frame( + not_null frame, + const Frame_Production& production); [[nodiscard]] nlohmann::json schema() const; [[nodiscard]] Stream_Snapshot stream_snapshot() const; [[nodiscard]] Publication_Dispatch publish( std::shared_ptr frame) noexcept; - void submit_frame_request(Frame_Request request); - void arm_tick_consumer(std::weak_ptr lifetime); - void request_next_maximum_rate_frame(); - void retain_frame_policy_lifetime(); - void release_frame_policy_lifetime_if_idle(); - void consume_tick(std::weak_ptr lifetime); - void refresh_schedule(); - void render_frame(Frame_Production production); - void publish_completed_frame(not_null completed); - void consume_completed_frame(not_null frame); - void retire_completed_frame(not_null frame); - void consume_retired_frames(); - void finalize_retired_frame(not_null frame); [[nodiscard]] bool mark_taskflow_trace(Render_Frame& frame); void store_trace(std::atomic_uint64_t& control, std::array>, @@ -774,39 +671,221 @@ struct Plot::Private { maximum_taskflow_trace_frames>& slots) const; void fail(std::exception_ptr failure) noexcept; }; +void Plot::Private::schedule(not_null object) { + bool idle{}; + if (!consumer_active.compare_exchange_strong( + idle, true, std::memory_order_acq_rel, + std::memory_order_acquire)) + return; + const auto weak = object->weak_from_this(); + try { + schedule_task("plot.consume", [weak] { + const auto plot = weak.lock(); + if (!plot) return; + auto& data = double_buffer::detail::Internal_Access::layer( + plot.get()); + data.consume(not_null{plot.get()}); + }); + } + catch (...) { + consumer_active.store(false, std::memory_order_release); + fail(std::current_exception()); + } +} + +void Plot::Private::consume(not_null object) noexcept { + const auto generation = [&] { + return object->stream_submission_generation< + detail::Plot_Input_Stream_Tag>() + + object->stream_submission_generation< + detail::Plot_Render_Stream_Tag>() + + object->stream_submission_generation< + detail::Plot_Publication_Feedback_Stream_Tag>() + + object->stream_submission_generation< + detail::Plot_Control_Stream_Tag>(); + }; + auto observed_generation = generation(); + try { + for (;;) { + object->advance(); + auto& state = double_buffer::detail::Internal_Access:: + pending_state(object.get()); + std::vector> failures; + + object->exchange_stream(); + object->access_rendering_stream( + [&](std::span controls) { + for (auto& control : controls) { + std::visit([&](auto& value) { + using Value = std::remove_cvref_t; + if constexpr (std::same_as) { + state.consumers.insert_or_assign( + value.id, detail::Plot_Consumer{ + std::move(value.handler), 0, 0}); + } + else if constexpr (std::same_as) { + state.consumers.erase(value.id); + } + else if constexpr (std::same_as< + Value, + detail::Plot_Configure_Stream>) { + const auto found = state.consumers.find(value.id); + if (found == state.consumers.end()) return; + found->second.width = value.width; + found->second.height = value.height; + } + else if constexpr (std::same_as< + Value, + detail::Plot_Replace_Frame_Policy>) { + const auto current = frame_policy.load( + std::memory_order_acquire); + if (!current || state.frame_policy_switching || + current->type() == value.type) + return; + double fixed_rate_fps{30.0}; + if (const auto* fixed = dynamic_cast< + const Fixed_Rate_Frame_Policy*>( + current.get())) + fixed_rate_fps = fixed->frame_rate(); + state.frame_policy_switching = true; + const auto weak = lifetime; + current->stop( + [weak, type = value.type, + fixed_rate_fps] { + const auto owner = weak.lock(); + if (!owner) return; + owner->submit_stream< + detail::Plot_Control_Stream_Tag>( + detail::Plot_Frame_Policy_Stopped{ + type, fixed_rate_fps}); + auto& data = double_buffer::detail:: + Internal_Access::layer( + owner.get()); + data.schedule(not_null{owner.get()}); + }); + } + else if constexpr (std::same_as< + Value, + detail::Plot_Frame_Policy_Stopped>) { + auto replacement = create_frame_policy( + value.type, value.fixed_rate_fps); + frame_policy.store( + replacement, std::memory_order_release); + state.frame_policy_type = value.type; + state.frame_policy_switching = false; + replacement->start(); + } + else if constexpr (std::same_as< + Value, + detail::Plot_Failure>) { + if (state.terminal_failure || !value.description) + return; + state.terminal_failure = value.description; + failures.push_back(value.description); + } + }, control); + } + }); + + object->exchange_stream(); + object->access_rendering_stream( + [&](std::span inputs) { + for (const auto& input : inputs) { + try { + if (auto* scene_2d = std::get_if< + std::unique_ptr>(&scene)) + dispatch_plot_input(**scene_2d, input); + else + dispatch_plot_input( + *std::get>(scene), + input); + } + catch (...) { + failures.push_back( + std::make_shared( + exception_description( + std::current_exception()))); + } + } + }); + + const auto policy = frame_policy.load(std::memory_order_acquire); + object->exchange_stream(); + object->access_rendering_stream< + detail::Plot_Publication_Feedback_Stream_Tag>( + [&](std::span feedback) { + if (!policy) return; + for (auto& item : feedback) + policy->submit_publication_feedback(std::move(item)); + }); + object->exchange_stream(); + object->access_rendering_stream( + [&](std::span requests) { + if (!policy || state.terminal_failure) return; + for (auto& request : requests) + policy->request_frame(std::move(request)); + }); + + if (!failures.empty() && !state.terminal_failure) + state.terminal_failure = failures.front(); + object->publish_state(); + for (const auto& failure : failures) { + if (!failure) continue; + const auto output = std::make_shared( + Plot_Stream_Frame{ + nlohmann::json{ + {"kind", "plot_error"}, + {"protocol", "aethera.plot.stream"}, + {"version", plot_stream_protocol_version}, + {"message", *failure}} + .dump(), + {}}); + static_cast(publish(std::move(output))); + break; + } + const auto current_generation = generation(); + if (current_generation == observed_generation) { + observed_generation = current_generation; + break; + } + observed_generation = current_generation; + } + } + catch (...) { + try { + const auto failure = std::make_shared( + exception_description(std::current_exception())); + object->submit_stream( + detail::Plot_Failure{failure}); + } + catch (...) {} + } + consumer_active.store(false, std::memory_order_release); + if (generation() != observed_generation) schedule(object); +} + void Plot::Private::fail(std::exception_ptr failure) noexcept { try { - auto description = std::make_shared( + const auto description = std::make_shared( exception_description(failure)); - std::shared_ptr empty; - if (!terminal_failure.compare_exchange_strong( - empty, description, std::memory_order_acq_rel, - std::memory_order_acquire)) - return; - const auto output = std::make_shared( - Plot_Stream_Frame{ - nlohmann::json{ - {"kind", "plot_error"}, - {"protocol", "aethera.plot.stream"}, - {"version", plot_stream_protocol_version}, - {"message", *description} - }.dump(), - {} - }); - static_cast(publish(std::move(output))); + const auto owner = lifetime.lock(); + if (!owner) return; + owner->submit_stream( + detail::Plot_Failure{description}); + schedule(not_null{owner.get()}); } catch (...) {} } nlohmann::json Plot::Private::schema() const { auto result = view->schema(); - auto analysis = with_frame_policy( - [](const auto& policy) { - return frame_policy_schema(policy); - }); - if (frame_policy_3d) { + const auto policy = frame_policy.load(std::memory_order_acquire); + if (!policy) return result; + auto analysis = frame_policy_schema(*policy); + if (std::holds_alternative>(scene)) { analysis["fields"].push_back({ {"key", "pipeline_capacity"}, {"label", "3D 帧槽容量"}, - {"editor", "integer"}, {"editable", false}, {"value", 3}, + {"editor", "integer"}, {"editable", false}, + {"value", policy->get<&Frame_Policy::Prop::frame_capacity>()}, {"description", "3D 独立策略最多允许三帧处于 Prepare、GPU 和退休阶段。"}, {"technical_description", "3D-only submitted-driven pipeline capacity; 2D policy is unchanged."} }); @@ -818,8 +897,12 @@ nlohmann::json Plot::Private::schema() const { } Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const { Stream_Snapshot result; - result.consumers = consumers.load(std::memory_order_acquire); - for (const auto& [id, consumer] : *result.consumers) { + if (const auto owner = lifetime.lock()) + owner->access_state( + [&](const Plot::State& state) { + result.consumers = state.consumers; + }); + for (const auto& [id, consumer] : result.consumers) { static_cast(id); if (consumer.width == 0 || consumer.height == 0) continue; result.width = std::max(result.width, consumer.width); @@ -836,7 +919,7 @@ Plot::Private::Publication_Dispatch Plot::Private::publish( try { const auto snapshot = stream_snapshot(); std::vector failed_consumers; - for (const auto& [id, consumer] : *snapshot.consumers) { + for (const auto& [id, consumer] : snapshot.consumers) { if (!consumer.handler) continue; try { switch (consumer.handler(frame)) { @@ -855,113 +938,16 @@ Plot::Private::Publication_Dispatch Plot::Private::publish( } } if (failed_consumers.empty()) return dispatch; - auto current = consumers.load(std::memory_order_acquire); - for (;;) { - auto next = std::make_shared(*current); - for (const auto id : failed_consumers) next->erase(id); - std::shared_ptr desired = next; - if (consumers.compare_exchange_weak( - current, desired, std::memory_order_release, - std::memory_order_acquire)) - break; + if (const auto owner = lifetime.lock()) { + for (const auto id : failed_consumers) + owner->submit_stream( + detail::Plot_Unsubscribe{id}); + schedule(not_null{owner.get()}); } } catch (...) {} return dispatch; } -void Plot::Private::refresh_schedule() { - if (!frame_timer.valid()) return; - const auto current_consumers = consumers.load(std::memory_order_acquire); - const auto& pacing = pacing_state(); - if (!pacing.render_enabled || current_consumers->empty()) { - frame_timer.cancel(); - return; - } - if (pacing.mode == Frame_Pacing_Mode::fixed_rate) { - frame_timer.start_periodic(pacing.fixed_rate_fps); - return; - } - frame_timer.cancel(); - if (pacing.mode != Frame_Pacing_Mode::maximum_rate) return; - const auto now = std::chrono::steady_clock::now(); - submit_frame_request(Frame_Request{ - .issued_at = now, - .time_milliseconds = std::chrono::duration( - now - clock_origin).count(), - .source = Frame_Request_Source::maximum_rate - }); -} -void Plot::Private::submit_frame_request(Frame_Request request) { - with_frame_policy([&](auto& policy) { - policy.submit_request(std::move(request)); - }); - consumer_work_generation.fetch_add(1, std::memory_order_release); - arm_tick_consumer(lifetime); -} -void Plot::Private::arm_tick_consumer(std::weak_ptr lifetime) { - if (terminal_failure.load(std::memory_order_acquire)) return; - if (tick_task_scheduled.exchange(true, std::memory_order_acq_rel)) return; - aethera::schedule_task("frame.policy.consume", [lifetime] { - const auto plot = lifetime.lock(); - if (!plot) return; - try { - plot->d->consume_tick(lifetime); - } - catch (...) { - plot->d->fail(std::current_exception()); - } - }); -} -void Plot::Private::request_next_maximum_rate_frame() { - const auto& pacing = pacing_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()) { - const auto now = std::chrono::steady_clock::now(); - submit_frame_request(Frame_Request{ - .issued_at = now, - .time_milliseconds = std::chrono::duration( - now - clock_origin).count(), - .source = Frame_Request_Source::maximum_rate - }); - } - } -} -void Plot::Private::retain_frame_policy_lifetime() { - if (frame_policy_lifetime.load(std::memory_order_acquire)) return; - const auto owner = lifetime.lock(); - if (!owner) - throw std::logic_error( - "Plot frame policy cannot retain an expired Plot"); - std::shared_ptr empty; - static_cast(frame_policy_lifetime.compare_exchange_strong( - empty, owner, std::memory_order_release, std::memory_order_acquire)); -} -void Plot::Private::release_frame_policy_lifetime_if_idle() { - if (std::ranges::any_of(frame_slots, [](const Managed_Frame& slot) { - return slot.state.load(std::memory_order_acquire) != - Frame_State::available; - })) - return; - frame_policy_lifetime.store({}, std::memory_order_release); -} -void Plot::Private::consume_tick(std::weak_ptr lifetime) { - const auto observed_generation = - consumer_work_generation.load(std::memory_order_acquire); - consume_retired_frames(); - if (consume_frame_policy_events()) refresh_schedule(); - auto production = with_frame_policy([](auto& policy) { - return policy.begin_production(); - }); - if (production) render_frame(std::move(*production)); - consume_retired_frames(); - if (consume_frame_policy_events()) refresh_schedule(); - tick_task_scheduled.store(false, std::memory_order_release); - if (consumer_work_generation.load(std::memory_order_acquire) != - observed_generation || - retired_frames.load(std::memory_order_acquire)) - arm_tick_consumer(std::move(lifetime)); -} bool Plot::Private::mark_taskflow_trace(Render_Frame& frame) { auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire); while (remaining != 0) { @@ -1016,443 +1002,192 @@ nlohmann::json Plot::Private::trace_response( {"frames", std::move(frames)} }; } -void Plot::Private::render_frame(Frame_Production production) { - const auto sequence = production.sequence; - auto tick = std::move(production.request); - if (terminal_failure.load(std::memory_order_acquire)) { - with_frame_policy([&](auto& policy) { - policy.reject_production(sequence); - }); - return; - } - const auto streams = stream_snapshot(); - const auto& pacing = pacing_state(); - const bool direct_diagnostics_frame = - streams.consumers->empty() && - tick.source == Frame_Request_Source::immediate; - if (streams.consumers->empty() && !direct_diagnostics_frame) { - with_frame_policy([&](auto& policy) { - policy.reject_production(sequence); - }); - return; - } - std::size_t slot_index{}; - Managed_Frame* managed{}; - /* - * 只有 rendering 槽受 Scene 不可重入门约束;consuming 槽表示上一帧 - * 已经完成 Plot 像素发布,外接 Drogon 图库流仍可继续持有该物理帧的 - * 诊断生命周期。只要还有 available 槽,下一帧即可进入。 - */ - for (std::size_t index = 0; index < frame_slots.size(); ++index) { - auto* candidate = &frame_slots[index]; - auto* published = candidate; - const bool was_latest = latest_statistics_frame.compare_exchange_strong( - published, nullptr, std::memory_order_acq_rel, - std::memory_order_acquire); - if (candidate->diagnostic_readers.load(std::memory_order_acquire) != 0) { - if (was_latest) { - Managed_Frame* empty{}; - static_cast(latest_statistics_frame.compare_exchange_strong( - empty, candidate, std::memory_order_release, - std::memory_order_relaxed)); - } - continue; - } - auto expected = Frame_State::available; - if (!candidate->state.compare_exchange_strong( - expected, Frame_State::rendering, - std::memory_order_acq_rel, std::memory_order_acquire)) { - if (was_latest) { - Managed_Frame* empty{}; - static_cast(latest_statistics_frame.compare_exchange_strong( - empty, candidate, std::memory_order_release, - std::memory_order_relaxed)); - } - continue; - } - slot_index = index; - managed = &frame_slots[index]; - break; - } - if (!managed) { - with_frame_policy([&](auto& policy) { - policy.defer_production(std::move(production)); - }); - /* - * 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能 - * 立即 arm tick consumer,否则会在没有任何槽可用期间形成 - * consume -> no slot -> consume 的 Taskflow 任务风暴。真正的唤醒点 - * 是 retire_completed_frame:某个 consuming 槽变回 available 后只唤醒一次。 - */ - return; - } - retain_frame_policy_lifetime(); - 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; - static_cast(slot.state.compare_exchange_strong( - expected, Frame_State::available, std::memory_order_acq_rel, - std::memory_order_acquire)); - release_frame_policy_lifetime_if_idle(); - }; - bool taskflow_trace_claimed{}; - const auto restore_taskflow_trace_claim = [this, &taskflow_trace_claimed] { - if (!std::exchange(taskflow_trace_claimed, false)) return; - taskflow_trace_remaining.fetch_add(1, std::memory_order_release); - }; - try { - if (streams.consumers->empty()) { - tick.width = std::clamp(tick.width, 160U, 1920U) & ~1U; - tick.height = std::clamp(tick.height, 120U, 1080U) & ~1U; - } - else { - tick.width = streams.width; - tick.height = streams.height; - } - const Frame_Identity identity{ - sequence, tick.sequence == 0 ? sequence : tick.sequence - }; - const auto request_source = tick.source; - const auto& policy_state = pacing_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, policy_state); - logical_frame = frame_2d->get(); - } - else { - auto& frame_3d = std::get>(managed->frame); - frame_3d->begin(identity, - pacing.pixel_delivery_enabled - ? Frame_3D_Output::pixels - : Frame_3D_Output::diagnostics, - Frame_3D::native_pixel_format, request_source, - policy_state); - logical_frame = frame_3d.get(); - } - taskflow_trace_claimed = mark_taskflow_trace(*logical_frame); - /* - * 各图的采样、网格构造和属性读取都在 Plot 自己的准备域完成。 - * 进入 Scene::render 后只剩已经准备好的 Visual 批次与轻量提交; - * 共享 Render Domain 不承担业务数据生成。 - */ - logical_frame->mark(Frame_Trace_Marker::plot_update_started); - const auto update_started = std::chrono::steady_clock::now(); - view->update(tick); - const auto update_elapsed = std::chrono::steady_clock::now() - update_started; - logical_frame->mark(Frame_Trace_Marker::plot_update_finished); - const auto tick_queue_elapsed = tick.issued_at.time_since_epoch().count() == 0 - ? std::chrono::steady_clock::duration::zero() - : update_started - tick.issued_at; - const auto record_plot_measurements = [&](Render_Frame& frame) { - const auto nanoseconds = [](std::chrono::steady_clock::duration duration) { - return static_cast(std::max(0, - std::chrono::duration_cast(duration).count())); - }; - frame.record(Frame_Trace_Measurement::plot_tick_queue_ns, - nanoseconds(tick_queue_elapsed)); - frame.record(Frame_Trace_Measurement::plot_update_ns, - nanoseconds(update_elapsed)); - }; - if (auto* scene_2d = std::get_if>(&scene)) { - auto& output = *std::get>(managed->frame); - record_plot_measurements(output); - (*scene_2d)->set<&Render_Scene_2D::Prop::viewport>( - Size{static_cast(tick.width), static_cast(tick.height)}); - const auto result = (*scene_2d)->render( - &output, - [weak = lifetime](not_null frame) { - if (auto owner = weak.lock()) { - try { - owner->d->publish_completed_frame(frame); - owner->d->consume_completed_frame(frame); - owner->d->retire_completed_frame(frame); - } - catch (...) { - owner->d->fail(std::current_exception()); - } - } - }); - if (!result) { - with_frame_policy([&](auto& policy) { - policy.reject_production(sequence); - }); - rollback_unsubmitted(); - restore_taskflow_trace_claim(); - request_next_maximum_rate_frame(); - } - else { - taskflow_trace_claimed = false; - managed->submitted_at = std::chrono::steady_clock::now(); - with_frame_policy([&](auto& policy) { - policy.record_frame_submitted( - sequence, request_source, - managed->submitted_at - managed->tick_issued_at); - }); - } - return; - } - auto& output = *std::get>(managed->frame); - record_plot_measurements(output); - auto& scene_3d = std::get>(scene); - scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height}); - const auto weak = lifetime; - const auto result = scene_3d->render( - &output, - Render_Scene_3D::Frame_Callbacks{ - .submitted = [weak](not_null, bool) { - if (auto owner = weak.lock()) { - try { - owner->d->request_next_maximum_rate_frame(); - } - catch (...) { - owner->d->fail(std::current_exception()); - } - } - }, - .completed = [weak](not_null frame) { - if (auto owner = weak.lock()) { - try { - owner->d->publish_completed_frame(frame); - owner->d->consume_completed_frame(frame); - owner->d->retire_completed_frame(frame); - } - catch (...) { - owner->d->fail(std::current_exception()); - } - } - } - }); - if (result == Render_Scene_3D::Render_Result::submitted) { - taskflow_trace_claimed = false; - managed->submitted_at = std::chrono::steady_clock::now(); - with_frame_policy([&](auto& policy) { - policy.record_frame_submitted( - sequence, request_source, - managed->submitted_at - managed->tick_issued_at); - }); - return; - } - with_frame_policy([&](auto& policy) { - policy.reject_production(sequence); - }); - rollback_unsubmitted(); - restore_taskflow_trace_claim(); - request_next_maximum_rate_frame(); - if (result == Render_Scene_3D::Render_Result::backend_unavailable) throw std::runtime_error("3D render backend became unavailable before submission"); - } - catch (...) { - rollback_unsubmitted(); - restore_taskflow_trace_claim(); - const auto active_sequence = pacing_state().in_flight_frame_sequence; - if (active_sequence == sequence) - with_frame_policy([&](auto& policy) { - policy.reject_production(sequence); - }); - throw; + +std::shared_ptr Plot::Private::create_frame_policy( + Frame_Policy_Type type, double fixed_rate_fps) { + auto* scene_base = std::visit( + [](auto& value) -> Render_Frame_Scene* { return value.get(); }, + scene); + const bool is_3d = + std::holds_alternative>(scene); + Frame_Policy::Dependencies dependencies{ + .scene = not_null{scene_base}, + .create_frame = [is_3d]() -> std::unique_ptr { + if (is_3d) + return std::make_unique(Frame_Identity{}); + return std::make_unique(Frame_Identity{}); + }, + .prepare_frame = [weak = lifetime]( + not_null frame, + const Frame_Production& production) { + if (const auto owner = weak.lock()) + double_buffer::detail::Internal_Access::layer( + owner.get()).prepare_policy_frame(frame, production); + }, + .publish_frame = [weak = lifetime]( + not_null frame, + const Frame_Production& production) { + if (const auto owner = weak.lock()) + return double_buffer::detail::Internal_Access::layer( + owner.get()).publish_policy_frame(frame, production); + return Frame_Publication_Dispatch{}; + }, + .retain_owner = [weak = lifetime]() -> std::shared_ptr { + return weak.lock(); + }, + .report_failure = [weak = lifetime](std::exception_ptr failure) { + if (const auto owner = weak.lock()) + double_buffer::detail::Internal_Access::layer( + owner.get()).fail(std::move(failure)); + }, + .frame_capacity = 3}; + switch (type) { + case Frame_Policy_Type::manual: + return std::make_shared( + std::move(dependencies)); + case Frame_Policy_Type::fixed_rate: + return std::make_shared( + std::move(dependencies), fixed_rate_fps); + case Frame_Policy_Type::maximum_rate: + return std::make_shared( + std::move(dependencies)); } + throw std::logic_error("unknown Frame Policy type"); } -void Plot::Private::publish_completed_frame( - not_null completed) { - Managed_Frame* managed{}; - for (auto& slot : frame_slots) { - const auto frame = std::visit( - [](const auto& value) -> not_null { - return not_null{value.get()}; - }, - slot.frame); - if (frame.get() != completed.get()) continue; - managed = &slot; - break; + +void Plot::Private::replace_frame_policy(Frame_Policy_Type type) { + const auto owner = lifetime.lock(); + if (!owner) return; + owner->submit_stream( + detail::Plot_Replace_Frame_Policy{type}); + schedule(not_null{owner.get()}); +} + +void Plot::Private::prepare_policy_frame( + not_null frame, + const Frame_Production& production) { + auto request = production.request; + const auto streams = stream_snapshot(); + if (!streams.consumers.empty()) { + request.width = streams.width; + request.height = streams.height; } - if (!managed) throw std::logic_error("completed frame has no owning Plot policy slot"); - if (managed->state.load(std::memory_order_acquire) != - Frame_State::rendering) - throw std::logic_error("completed Plot frame is not rendering"); - const auto frame = completed; - const auto& pacing = pacing_state(); + else { + request.width = std::clamp(request.width, 160U, 1920U) & ~1U; + request.height = std::clamp(request.height, 120U, 1080U) & ~1U; + } + const Frame_Identity identity{ + production.sequence, + request.sequence == 0 ? production.sequence : request.sequence}; + if (auto* output = dynamic_cast(frame.get())) { + output->begin(identity, Frame_2D::native_pixel_format, + request.source); + std::get>(scene)->set< + &Render_Scene_2D::Prop::viewport>(Size{ + static_cast(request.width), + static_cast(request.height)}); + } + else if (auto* output = dynamic_cast(frame.get())) { + const auto policy = frame_policy.load(std::memory_order_acquire); + if (!policy) throw std::logic_error("Plot has no Frame Policy"); + output->begin( + identity, + policy->get<&Frame_Policy::Prop::pixel_delivery_enabled>() + ? Frame_3D_Output::pixels + : Frame_3D_Output::diagnostics, + Frame_3D::native_pixel_format, request.source); + std::get>(scene)->set< + &Render_Scene_3D::Prop::viewport>( + Extent{request.width, request.height}); + } + else { + throw std::logic_error( + "Plot Policy created an unsupported Render_Frame type"); + } + static_cast(mark_taskflow_trace(*frame)); + frame->mark(Frame_Trace_Marker::plot_update_started); + const auto update_started = std::chrono::steady_clock::now(); + view->update(request); + const auto update_elapsed = + std::chrono::steady_clock::now() - update_started; + frame->mark(Frame_Trace_Marker::plot_update_finished); + const auto tick_queue_elapsed = + request.issued_at.time_since_epoch().count() == 0 + ? std::chrono::steady_clock::duration::zero() + : update_started - request.issued_at; + const auto nanoseconds = [](std::chrono::steady_clock::duration value) { + return static_cast(std::max( + 0, std::chrono::duration_cast(value) + .count())); + }; + frame->record(Frame_Trace_Measurement::plot_tick_queue_ns, + nanoseconds(tick_queue_elapsed)); + frame->record(Frame_Trace_Measurement::plot_update_ns, + nanoseconds(update_elapsed)); +} + +Frame_Publication_Dispatch Plot::Private::publish_policy_frame( + not_null frame, + const Frame_Production& production) { + const auto policy = frame_policy.load(std::memory_order_acquire); + if (!policy) throw std::logic_error("Plot has no Frame Policy"); const auto identity = frame->identity(); Frame_Identity rendered_identity = identity; std::shared_ptr> pixel_storage; Plot_Pixel_Layout pixel_layout{Plot_Pixel_Layout::rgba8}; std::uint32_t width{}; std::uint32_t height{}; - if (auto* frame_2d = - std::get_if>(&managed->frame)) { + std::optional datoviz_observation; + if (auto* output = dynamic_cast(frame.get())) { pixel_layout = Plot_Pixel_Layout::bgra8; - const auto image = (*frame_2d)->image(); - width = static_cast(image.width); - height = static_cast(image.height); - if (pacing.pixel_delivery_enabled) { - auto output = (*frame_2d)->output_pixels(); - pixel_storage = std::make_shared>( - std::move(output.bytes)); - width = static_cast(output.width); - height = static_cast(output.height); + const auto image = output->image(); + width = static_cast(std::max(0, image.width)); + height = static_cast(std::max(0, image.height)); + if (policy->get<&Frame_Policy::Prop::pixel_delivery_enabled>()) { + auto pixels = output->output_pixels(); + width = static_cast(pixels.width); + height = static_cast(pixels.height); + pixel_storage = + std::make_shared>( + std::move(pixels.bytes)); } } - else { - auto& frame_3d = std::get>(managed->frame); - rendered_identity = frame_3d->rendered_identity(); - const auto extent = frame_3d->extent(); + else if (auto* output = dynamic_cast(frame.get())) { + rendered_identity = output->rendered_identity(); + const auto extent = output->extent(); width = extent.width; height = extent.height; - if (pacing.pixel_delivery_enabled && frame_3d->output() == Frame_3D_Output::pixels) pixel_storage = frame_3d->share_pixels(); + if (policy->get<&Frame_Policy::Prop::pixel_delivery_enabled>() && + output->output() == Frame_3D_Output::pixels) + pixel_storage = output->share_pixels(); + datoviz_observation = output->take_datoviz_observation(); } + const auto presentation_time = + std::chrono::duration_cast( + std::chrono::duration( + production.request.time_milliseconds)); auto pixels = std::make_shared(Plot_Pixel_Frame{ - std::move(pixel_storage), pixel_layout, managed->presentation_time, - identity.sequence, identity.correlation_id, rendered_identity.sequence, - rendered_identity.correlation_id, width, height - }); - const auto published = std::make_shared( + std::move(pixel_storage), pixel_layout, presentation_time, + identity.sequence, identity.correlation_id, + rendered_identity.sequence, rendered_identity.correlation_id, + width, height}); + auto output = std::make_shared( Plot_Stream_Frame{{}, std::move(pixels)}); - const auto publish_started = std::chrono::steady_clock::now(); - const auto dispatch = publish(std::move(published)); - const auto publish_completed = std::chrono::steady_clock::now(); - with_frame_policy([&](auto& policy) { - policy.submit_publication_dispatch({ - .started_at = publish_started, - .completed_at = publish_completed, - .sequence = identity.sequence, - .asynchronous_feedback_count = - dispatch.asynchronous_feedback_count, - .succeeded = dispatch.succeeded}); - }); - frame->record(Frame_Trace_Measurement::plot_publish_ns, - static_cast(std::max( - 0, std::chrono::duration_cast( - publish_completed - publish_started) - .count()))); - auto expected = Frame_State::rendering; - if (!managed->state.compare_exchange_strong( - expected, Frame_State::consuming, std::memory_order_acq_rel, - std::memory_order_acquire)) - throw std::logic_error("Plot frame left rendering before pixel publish"); -} -void Plot::Private::consume_completed_frame(not_null frame) { - Managed_Frame* managed{}; - for (auto& slot : frame_slots) { - const auto address = std::visit( - [](const auto& value) -> not_null { - return not_null{value.get()}; - }, - slot.frame); - if (address.get() != frame.get()) continue; - if (slot.state.load(std::memory_order_acquire) != Frame_State::consuming) throw std::logic_error("completed Plot frame was not published"); - managed = &slot; - break; - } - if (!managed) throw std::logic_error("frame callback has no owned Plot frame"); - /* Scene 完成即释放 Plot admission。媒体采样属于 Gallery 自己的独立时钟和 - * DAG,不再借 Plot 保存一套 post-publish 管线状态。 */ - if (std::holds_alternative>(managed->frame)) - request_next_maximum_rate_frame(); -} -void Plot::Private::consume_retired_frames() { - for (;;) { - auto* list = retired_frames.exchange(nullptr, std::memory_order_acq_rel); - if (!list) break; - std::vector frames; - while (list) { - auto* next = list->retired_next.exchange( - nullptr, std::memory_order_relaxed); - frames.push_back(list); - list = next; - } - std::ranges::sort(frames, {}, [](const Managed_Frame* managed) { - return std::visit( - [](const auto& value) { - return value->identity().sequence; - }, - managed->frame); - }); - for (auto* managed : frames) { - auto* frame = std::visit( - [](const auto& value) -> Render_Frame* { - return value.get(); - }, - managed->frame); - finalize_retired_frame(frame); - } - } -} -void Plot::Private::retire_completed_frame(not_null frame) { - Managed_Frame* managed{}; - for (auto& slot : frame_slots) { - const auto address = std::visit( - [](const auto& value) -> not_null { - return not_null{value.get()}; - }, - slot.frame); - if (address.get() != frame.get()) continue; - managed = &slot; - break; - } - if (!managed) throw std::logic_error("retired frame has no owned Plot slot"); - auto* head = retired_frames.load(std::memory_order_relaxed); - do { - managed->retired_next.store(head, std::memory_order_relaxed); - } - while (!retired_frames.compare_exchange_weak( - head, managed, std::memory_order_release, - std::memory_order_relaxed)); - consumer_work_generation.fetch_add(1, std::memory_order_release); - arm_tick_consumer(lifetime); -} -void Plot::Private::finalize_retired_frame(not_null frame) { - Managed_Frame* managed{}; - for (auto& slot : frame_slots) { - const auto address = std::visit( - [](const auto& value) -> not_null { - return not_null{value.get()}; - }, - slot.frame); - if (address.get() == frame.get()) { - managed = &slot; - break; - } - } - if (!managed) throw std::logic_error("retired frame has no owned Plot slot"); - const auto completion_latency = - managed->submitted_at.time_since_epoch().count() == 0 - ? std::chrono::steady_clock::duration::zero() - : std::chrono::steady_clock::now() - managed->submitted_at; - std::optional datoviz_observation; - if (auto* frame_3d = dynamic_cast(frame.get())) { - datoviz_observation = frame_3d->take_datoviz_observation(); - if (!datoviz_observation) - throw std::logic_error( - "completed 3D frame has no Datoviz observation"); - frame_policy_3d->record_frame_completed( - frame->identity().sequence, completion_latency, - datoviz_observation->prepare_released_after_submission); - } - else { - frame_policy_2d->record_frame_completed( - frame->identity().sequence, completion_latency); - } - const auto statistics_generation_value = - statistics_generation.load(std::memory_order_acquire); - if (applied_statistics_generation != statistics_generation_value) { - completed_frame_statistics.reset(); - applied_statistics_generation = statistics_generation_value; - } - managed->statistics = completed_frame_statistics.submit(*frame); - managed->statistics_generation = statistics_generation_value; - std::optional captured_trace; - nlohmann::json captured_components; - nlohmann::json captured_backend; + const auto started_at = std::chrono::steady_clock::now(); + const auto dispatch = publish(std::move(output)); + const auto completed_at = std::chrono::steady_clock::now(); + frame->record( + Frame_Trace_Measurement::plot_publish_ns, + static_cast(std::max( + 0, std::chrono::duration_cast( + completed_at - started_at).count()))); if (frame->taskflow_trace_requested()) { - captured_trace.emplace(frame->take_taskflow_trace()); + auto trace = frame->take_taskflow_trace(); std::unordered_set executed_nodes; - for (const auto& execution : captured_trace->tasks) executed_nodes.insert(execution.native_id); + for (const auto& execution : trace.tasks) + executed_nodes.insert(execution.native_id); std::vector executed_components; - for (const auto& graph : captured_trace->graphs) { + for (const auto& graph : trace.graphs) { for (const auto& node : graph.nodes) { if (!executed_nodes.contains(node.native_id)) continue; const auto owner = std::ranges::find( @@ -1460,77 +1195,79 @@ void Plot::Private::finalize_retired_frame(not_null frame) { &std::pair::first); if (owner == node.attributes.end() || owner->second.empty() || std::ranges::find(executed_components, owner->second) != - executed_components.end()) + executed_components.end()) continue; executed_components.push_back(owner->second); } } - captured_components = view->capture_components(executed_components); - if (datoviz_observation) captured_backend = datoviz_observation_json(*datoviz_observation); - } - auto expected = Frame_State::consuming; - if (!managed->state.compare_exchange_strong( - expected, Frame_State::available, std::memory_order_acq_rel, - std::memory_order_acquire)) - throw std::logic_error("retired Plot frame is not consuming"); - latest_statistics_frame.store(managed, std::memory_order_release); - arm_tick_consumer(lifetime); - release_frame_policy_lifetime_if_idle(); - if (captured_trace) { - auto owner = lifetime; - schedule_task("plot.taskflow.serialize", [owner, - trace = std::move(*captured_trace), - components = std::move(captured_components), - backend = std::move(captured_backend)]() mutable { - if (const auto plot = owner.lock()) - plot->d->store_trace( - plot->d->taskflow_trace_control, - plot->d->taskflow_trace_slots, trace, components, - backend); - }); + auto components = view->capture_components(executed_components); + nlohmann::json backend; + if (datoviz_observation) + backend = datoviz_observation_json(*datoviz_observation); + store_trace(taskflow_trace_control, taskflow_trace_slots, trace, + components, backend); } + return Frame_Publication_Dispatch{ + .started_at = started_at, + .completed_at = completed_at, + .asynchronous_feedback_count = + dispatch.asynchronous_feedback_count, + .succeeded = dispatch.succeeded, + .pixel_width = width, + .pixel_height = height}; } + Plot::Plot(std::unique_ptr scene, - std::unique_ptr view) : d(std::make_unique(std::move(scene), std::move(view))) {} + std::unique_ptr view) { + auto attached = std::make_unique< + double_buffer::detail::Attached_Private>( + this, memory_resource()); + d = attached.release(); + auto& data = double_buffer::detail::Internal_Access::layer(this); + data.bind_private_crtp(this); + bind_object_crtp(); + data.initialize(std::move(scene), std::move(view)); + advance(); +} Plot::Plot(std::unique_ptr scene, - std::unique_ptr view) : d(std::make_unique(std::move(scene), std::move(view))) {} + std::unique_ptr view) { + auto attached = std::make_unique< + double_buffer::detail::Attached_Private>( + this, memory_resource()); + d = attached.release(); + auto& data = double_buffer::detail::Internal_Access::layer(this); + data.bind_private_crtp(this); + bind_object_crtp(); + data.initialize(std::move(scene), std::move(view)); + double_buffer::detail::Internal_Access::pending_prop(this). + initial_policy = Frame_Policy_Type::maximum_rate; + advance(); +} Plot::~Plot() = default; void Plot::ensure_started() { - std::call_once(d->start_once, [this] { - const auto weak = weak_from_this(); - d->lifetime = weak; - d->frame_timer = Frame_Scheduler::instance().make_timer( - [weak](Frame_Scheduler::Tick tick) { - if (const auto owner = weak.lock()) { - owner->schedule_render(Frame_Request{ - .issued_at = tick.issued_at, - .sequence = tick.sequence, - .time_milliseconds = tick.time_milliseconds, - .source = Frame_Request_Source::periodic - }); - } - }); - d->refresh_schedule(); + auto& data = double_buffer::detail::Internal_Access::layer(this); + std::call_once(data.start_once, [this, &data] { + data.lifetime = weak_from_this(); + const auto type = get<&Plot::Prop::initial_policy>(); + auto policy = data.create_frame_policy(type); + data.frame_policy.store(policy, std::memory_order_release); + auto& state = double_buffer::detail::Internal_Access::pending_state(this); + state.frame_policy_type = type; + publish_state(); + policy->start(); }); } Plot::Stream_Id Plot::subscribe(Stream_Handler handler) { if (!handler) throw std::invalid_argument("Plot subscription requires a handler"); ensure_started(); - const auto id = d->next_stream_id.fetch_add(1, std::memory_order_relaxed); + auto& data = double_buffer::detail::Internal_Access::layer(this); + const auto id = data.next_stream_id.fetch_add(1, std::memory_order_relaxed); const auto notification = handler; - auto current = d->consumers.load(std::memory_order_acquire); - for (;;) { - auto next = std::make_shared(*current); - next->emplace(id, Private::Consumer{handler}); - std::shared_ptr desired = next; - if (d->consumers.compare_exchange_weak( - current, desired, std::memory_order_release, - std::memory_order_acquire)) - break; - } - d->refresh_schedule(); - if (d->terminal_failure.load(std::memory_order_acquire)) { - const auto failure = d->terminal_failure.load(std::memory_order_acquire); + submit_stream( + detail::Plot_Subscribe{id, std::move(handler)}); + data.schedule(not_null{this}); + const auto failure = read_state().terminal_failure; + if (failure) { try { notification(std::make_shared( Plot_Stream_Frame{ @@ -1550,34 +1287,19 @@ Plot::Stream_Id Plot::subscribe(Stream_Handler handler) { return id; } void Plot::unsubscribe(Stream_Id stream) { - auto current = d->consumers.load(std::memory_order_acquire); - while (current->contains(stream)) { - auto next = std::make_shared(*current); - next->erase(stream); - std::shared_ptr desired = next; - if (d->consumers.compare_exchange_weak( - current, desired, std::memory_order_release, - std::memory_order_acquire)) - break; - } - d->refresh_schedule(); + ensure_started(); + submit_stream( + detail::Plot_Unsubscribe{stream}); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } void Plot::configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height) { - auto current = d->consumers.load(std::memory_order_acquire); - for (;;) { - const auto found = current->find(stream); - if (found == current->end()) return; - auto next = std::make_shared(*current); - auto& consumer = next->at(stream); - consumer.width = width; - consumer.height = height; - std::shared_ptr desired = next; - if (d->consumers.compare_exchange_weak( - current, desired, std::memory_order_release, - std::memory_order_acquire)) - return; - } + ensure_started(); + submit_stream( + detail::Plot_Configure_Stream{stream, width, height}); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } void Plot::schedule_render(Frame_Request request) { if (!std::isfinite(request.time_milliseconds) || @@ -1586,23 +1308,25 @@ void Plot::schedule_render(Frame_Request request) { "render time_milliseconds must be finite and non-negative"); if (request.width == 0 || request.height == 0) throw std::invalid_argument("render viewport must be non-zero"); ensure_started(); - if (d->terminal_failure.load(std::memory_order_acquire)) return; - d->submit_frame_request(std::move(request)); + if (read_state().terminal_failure) return; + submit_stream(std::move(request)); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } void Plot::submit_publication_feedback( Frame_Publication_Feedback feedback) { ensure_started(); - if (d->terminal_failure.load(std::memory_order_acquire)) return; - d->with_frame_policy([&](auto& policy) { - policy.submit_publication_feedback(std::move(feedback)); - }); - d->consumer_work_generation.fetch_add(1, std::memory_order_release); - d->arm_tick_consumer(weak_from_this()); + if (read_state().terminal_failure) return; + submit_stream( + std::move(feedback)); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } void Plot::render_once() { ensure_started(); const auto now = std::chrono::steady_clock::now(); - const auto elapsed = now - d->clock_origin; + const auto& data = double_buffer::detail::Internal_Access::layer(this); + const auto elapsed = now - data.clock_origin; schedule_render(Frame_Request{ .issued_at = now, .time_milliseconds = @@ -1613,44 +1337,55 @@ void Plot::render_once() { void Plot::submit_input(Plot_Input_Event event) { static_cast(input_timeline_time(event.time_milliseconds)); ensure_started(); - if (d->terminal_failure.load(std::memory_order_acquire)) return; - /* - * WebSocket 线程只向 Scene 的当前事件缓冲追加一个由 Scene - * memory_resource 分配的基类指针。Prepare 边界交换完整批次, - * Scene 在 Renderable 完成消费时按 Event_Type 增量统计,并随自身 - * State 双缓冲发布;Web 层只在低频 diagnostics 请求中读取结果。 - */ - try { - if (auto* scene_2d = std::get_if>(&d->scene)) dispatch_plot_input(**scene_2d, event); - else dispatch_plot_input(*std::get>(d->scene), event); - } - catch (...) { - d->fail(std::current_exception()); - } + if (read_state().terminal_failure) return; + submit_stream(std::move(event)); + double_buffer::detail::Internal_Access::layer(this). + schedule(not_null{this}); } nlohmann::json Plot::schema() { ensure_started(); - return d->schema(); + return double_buffer::detail::Internal_Access::layer(this).schema(); } nlohmann::json Plot::write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) { ensure_started(); - if (component != "frame-analysis") return d->view->write_prop(component, key, value); - auto result = d->with_frame_policy([&](auto& policy) { - return write_frame_policy_prop(policy, key, value); - }); - if (result.value("success", false)) d->arm_tick_consumer(weak_from_this()); - return result; + auto& data = double_buffer::detail::Internal_Access::layer(this); + if (component != "frame-analysis") + return data.view->write_prop(component, key, value); + if (key == "pacing_mode") { + if (!value.is_string()) + return {{"success", false}, + {"error", "pacing_mode requires a string"}}; + const auto parsed = parse_pacing_mode( + value.get_ref()); + if (!parsed) + return {{"success", false}, + {"error", "unknown frame pacing mode"}}; + data.replace_frame_policy(*parsed); + return {{"success", true}, + {"component", "frame-analysis"}, + {"key", key}, + {"value", pacing_mode_name(*parsed)}}; + } + const auto policy = data.frame_policy.load(std::memory_order_acquire); + if (!policy) return {{"success", false}, {"error", "Plot has no Frame Policy"}}; + return write_frame_policy_prop(*policy, key, value); } nlohmann::json Plot::component_state(std::string_view component) const { - return d->view->component_state(component); + return double_buffer::detail::Internal_Access::layer(this). + view->component_state(component); } nlohmann::json Plot::generate_data(const nlohmann::json& input) { ensure_started(); - return d->view->generate_data(input); + return double_buffer::detail::Internal_Access::layer(this). + view->generate_data(input); } nlohmann::json Plot::diagnostics() const { + const auto& data = double_buffer::detail::Internal_Access::layer(this); + const auto policy = data.frame_policy.load(std::memory_order_acquire); + if (!policy) return {{"protocol", "aethera.plot.diagnostics"}, + {"version", 4}, {"available", false}}; nlohmann::json frame_statistics = nlohmann::json::object(); nlohmann::json input_statistics = nlohmann::json::object(); Frame_Identity identity{}; @@ -1674,61 +1409,27 @@ nlohmann::json Plot::diagnostics() const { scene->template access_state( read_scene_statistics); } - }, d->scene); - { - const auto generation = - d->statistics_generation.load(std::memory_order_acquire); - auto* completed_frame = - d->latest_statistics_frame.load(std::memory_order_acquire); - Frame_Statistics_State statistics{}; - if (completed_frame) { - completed_frame->diagnostic_readers.fetch_add( - 1, std::memory_order_acq_rel); - if (d->latest_statistics_frame.load(std::memory_order_acquire) == - completed_frame && - completed_frame->state.load(std::memory_order_acquire) == - Private::Frame_State::available && - completed_frame->statistics_generation == generation) { - statistics = completed_frame->statistics; - std::visit([&](const auto& frame) { - using Frame_Pointer = - std::remove_cvref_t; - if constexpr (std::same_as< - Frame_Pointer, - std::unique_ptr>) { - const auto image = frame->image(); - completed_width = static_cast( - std::max(0, image.width)); - completed_height = static_cast( - std::max(0, image.height)); - } - else { - const auto extent = frame->extent(); - completed_width = extent.width; - completed_height = extent.height; - } - }, completed_frame->frame); - } - completed_frame->diagnostic_readers.fetch_sub( - 1, std::memory_order_release); - } - append_statistic_json(frame_statistics, statistics); - identity = statistics.identity; - created_time_unix_ns = statistics.created_time_unix_ns; - dropped_sequences = statistics.dropped_sequences; - const auto& interval = statistics.values[ - static_cast(Frame_Statistic::frame_interval_ms)]; - frame_rate = interval.trimmed_average > 0.0 - ? 1'000.0 / interval.trimmed_average - : 0.0; - } - const auto& pacing = d->pacing_state(); - const auto stream = d->stream_snapshot(); - const auto lifecycle_name = pacing.in_flight_frame_sequence != 0 - ? "publication_in_flight" - : pacing.pending_request - ? "request_pending" - : "ready"; + }, data.scene); + Frame_Policy_State pacing{}; + policy->access_state( + [&](const Frame_Policy::State& state) { + pacing = state.observation; + }); + const auto& statistics = pacing.frame_statistics; + append_statistic_json(frame_statistics, statistics); + identity = statistics.identity; + created_time_unix_ns = statistics.created_time_unix_ns; + dropped_sequences = statistics.dropped_sequences; + completed_width = pacing.pixel_width; + completed_height = pacing.pixel_height; + const auto& interval = statistics.values[ + static_cast(Frame_Statistic::frame_interval_ms)]; + frame_rate = interval.trimmed_average > 0.0 + ? 1'000.0 / interval.trimmed_average + : 0.0; + const auto stream = data.stream_snapshot(); + const auto lifecycle_name = magic_enum::enum_name( + policy->read_state().lifecycle); nlohmann::json supported_formats = nlohmann::json::array(); if (is_3d) { for (const auto format : Frame_3D::supported_pixel_formats) supported_formats.push_back(pixel_format_name(format)); @@ -1748,7 +1449,8 @@ nlohmann::json Plot::diagnostics() const { const auto pixel_height = completed_height == 0 ? stream.height : completed_height; - const std::size_t byte_length = pacing.pixel_delivery_enabled + const std::size_t byte_length = policy->get< + &Frame_Policy::Prop::pixel_delivery_enabled>() ? static_cast(pixel_width) * pixel_height * 4U : 0U; nlohmann::json output{ @@ -1762,7 +1464,9 @@ nlohmann::json Plot::diagnostics() const { "generated_time_unix_ms", static_cast(created_time_unix_ns) / 1'000'000.0 }, - {"delivery", pacing.pixel_delivery_enabled ? "gallery-pixels" : "diagnostics"}, + {"delivery", policy->get< + &Frame_Policy::Prop::pixel_delivery_enabled>() + ? "gallery-pixels" : "diagnostics"}, {"frame_rate_fps", frame_rate}, {"dropped_sequence_count", dropped_sequences}, {"window_capacity", diagnostic_window_capacity}, @@ -1774,23 +1478,21 @@ nlohmann::json Plot::diagnostics() const { {"byte_length", byte_length} } }, - {"frame_policy", frame_policy_state_json(pacing)}, + {"frame_policy", frame_policy_state_json(pacing, *policy)}, {"frame_lifecycle", lifecycle_name}, {"frame_statistics", std::move(frame_statistics)}, {"input_statistics", std::move(input_statistics)} }; if (is_3d) { - const auto& pipeline = d->frame_policy_3d->read_state< - Frame_Policy_3D::Base_Tag>(); output["frame_policy"]["three_dimensional_pipeline"] = { - {"capacity", Frame_Policy_3D::pipeline_capacity}, - {"overlapped_release_count", pipeline.overlapped_release_count}, + {"capacity", policy->get<&Frame_Policy::Prop::frame_capacity>()}, + {"overlapped_release_count", pacing.overlapped_release_count}, { "completion_gated_release_count", - pipeline.completion_gated_release_count + pacing.completion_gated_release_count }, - {"gpu_completion_count", pipeline.gpu_completion_count}, - {"last_completed_sequence", pipeline.last_completed_sequence} + {"gpu_completion_count", pacing.completed_frame_count}, + {"last_completed_sequence", pacing.last_frame_sequence} }; const auto& gpu = render_3d::detail::Gpu_Completion_Service::instance(). read_state(); @@ -1819,7 +1521,8 @@ nlohmann::json Plot::diagnostics() const { {"abandoned_count", gpu.abandoned_count} }; } - if (const auto failure = d->terminal_failure.load(std::memory_order_acquire)) output["terminal_failure"] = *failure; + if (const auto failure = read_state().terminal_failure) + output["terminal_failure"] = *failure; return output; } void Plot::request_taskflow_trace(std::size_t frame_count) { @@ -1827,34 +1530,35 @@ void Plot::request_taskflow_trace(std::size_t frame_count) { frame_count > Private::maximum_taskflow_trace_frames) throw std::invalid_argument("Taskflow trace frame_count must be between 1 and 120"); ensure_started(); - auto control = d->taskflow_trace_control.load(std::memory_order_acquire); + auto& data = double_buffer::detail::Internal_Access::layer(this); + auto control = data.taskflow_trace_control.load(std::memory_order_acquire); for (;;) { const auto requested = static_cast(control >> 32U); const auto captured = static_cast(control); if (requested != captured) throw std::logic_error("A Taskflow frame trace request is already active"); const auto next = static_cast(frame_count) << 32U; - if (d->taskflow_trace_control.compare_exchange_weak( + if (data.taskflow_trace_control.compare_exchange_weak( control, next, std::memory_order_release, std::memory_order_acquire)) break; } - for (auto& slot : d->taskflow_trace_slots) slot.store({}, std::memory_order_release); - d->taskflow_trace_remaining.store(frame_count, std::memory_order_release); + for (auto& slot : data.taskflow_trace_slots) + slot.store({}, std::memory_order_release); + data.taskflow_trace_remaining.store(frame_count, std::memory_order_release); } nlohmann::json Plot::taskflow_trace() const { - return d->trace_response(d->taskflow_trace_control, - d->taskflow_trace_remaining, - d->taskflow_trace_slots); + const auto& data = double_buffer::detail::Internal_Access::layer(this); + return data.trace_response(data.taskflow_trace_control, + data.taskflow_trace_remaining, + data.taskflow_trace_slots); } void Plot::reset_diagnostics() { + auto& data = double_buffer::detail::Internal_Access::layer(this); std::visit([](auto& scene) { scene->template update_state<&Scene::State::event_statistics>( Event_Statistics_State{}); - }, d->scene); - d->statistics_generation.fetch_add(1, std::memory_order_acq_rel); - d->with_frame_policy([](auto& policy) { - policy.reset_statistics(); - }); - d->arm_tick_consumer(weak_from_this()); + }, data.scene); + if (const auto policy = data.frame_policy.load(std::memory_order_acquire)) + policy->reset_statistics(); } } diff --git a/mcp/core/runtime/Plot.hpp b/mcp/core/runtime/Plot.hpp index 213a421..9330379 100644 --- a/mcp/core/runtime/Plot.hpp +++ b/mcp/core/runtime/Plot.hpp @@ -1,4 +1,6 @@ #pragma once +#include +#include #include #include #include @@ -9,6 +11,8 @@ #include #include #include +#include +#include #include namespace aethera::web { struct Plot_Input_Event { @@ -51,12 +55,77 @@ enum struct Plot_Frame_Publication : std::uint8_t { completed, /* 订阅回调返回时已同步完成业务消费。 */ asynchronous /* 订阅者已接管发布,并会执行帧策略反馈任务。 */ }; -struct Plot final : public std::enable_shared_from_this { +using Plot_Stream_Id = std::uint64_t; +using Plot_Stream_Handler = std::function)>; + +namespace detail { +struct Plot_Input_Stream_Tag {}; +struct Plot_Render_Stream_Tag {}; +struct Plot_Publication_Feedback_Stream_Tag {}; +struct Plot_Control_Stream_Tag {}; + +struct Plot_Consumer { + Plot_Stream_Handler handler{}; + std::uint32_t width{}; + std::uint32_t height{}; +}; +using Plot_Consumer_Map = std::unordered_map; +struct Plot_Subscribe { + Plot_Stream_Id id{}; + Plot_Stream_Handler handler{}; +}; +struct Plot_Unsubscribe { Plot_Stream_Id id{}; }; +struct Plot_Configure_Stream { + Plot_Stream_Id id{}; + std::uint32_t width{}; + std::uint32_t height{}; +}; +struct Plot_Replace_Frame_Policy { Frame_Policy_Type type{}; }; +struct Plot_Frame_Policy_Stopped { + Frame_Policy_Type type{}; + double fixed_rate_fps{30.0}; +}; +struct Plot_Failure { std::shared_ptr description{}; }; +using Plot_Control = std::variant< + std::monostate, + Plot_Subscribe, + Plot_Unsubscribe, + Plot_Configure_Stream, + Plot_Replace_Frame_Policy, + Plot_Frame_Policy_Stopped, + Plot_Failure>; +} + +struct Plot final : + double_buffer::Def< + Plot, + double_buffer::Root, + double_buffer::Mpmc_Triple_Buffer< + detail::Plot_Input_Stream_Tag, Plot_Input_Event>, + double_buffer::Mpmc_Triple_Buffer< + detail::Plot_Render_Stream_Tag, Frame_Request>, + double_buffer::Mpmc_Triple_Buffer< + detail::Plot_Publication_Feedback_Stream_Tag, + Frame_Publication_Feedback>, + double_buffer::Mpmc_Triple_Buffer< + detail::Plot_Control_Stream_Tag, detail::Plot_Control>>, + public std::enable_shared_from_this { public: - using Stream_Id = std::uint64_t; - using Stream_Handler = std::function)>; + using Stream_Id = Plot_Stream_Id; + using Stream_Handler = Plot_Stream_Handler; using Json_Handler = std::function; + struct Prop : Prev_Prop { + Frame_Policy_Type initial_policy{Frame_Policy_Type::fixed_rate}; + bool operator==(const Prop&) const = default; + }; + struct State : Prev_State { + detail::Plot_Consumer_Map consumers{}; + std::shared_ptr terminal_failure{}; + Frame_Policy_Type frame_policy_type{Frame_Policy_Type::fixed_rate}; + bool frame_policy_switching{}; + }; + struct Private; struct Scene_View { public: virtual ~Scene_View() = default; @@ -98,8 +167,6 @@ public: [[nodiscard]] nlohmann::json taskflow_trace() const; void reset_diagnostics(); private: - struct Private; void ensure_started(); - std::unique_ptr d; }; } diff --git a/mcp/tests/Event_Latency_Benchmarks.cpp b/mcp/tests/Event_Latency_Benchmarks.cpp index fdcd218..09f4ebe 100644 --- a/mcp/tests/Event_Latency_Benchmarks.cpp +++ b/mcp/tests/Event_Latency_Benchmarks.cpp @@ -31,6 +31,8 @@ struct Configuration { std::uint32_t width{720}; std::uint32_t height{420}; std::uint32_t samples{12}; + bool three_d_only{}; + bool manual_policy{true}; }; struct Summary { @@ -289,12 +291,16 @@ void run_event_latency(benchmark::State& state) { try { for (const auto& definition : definitions) { + if (configuration.three_d_only && + definition.dimension != web::Plot_Dimension::three_d) + continue; auto plot = service->find_plot(definition.id); if (!plot) throw std::runtime_error( "Gallery plot is unavailable: " + std::string{definition.id}); - configure_manual_backend_plot(plot); + if (configuration.manual_policy) + configure_manual_backend_plot(plot); auto probe = std::make_shared(); active.push_back({std::move(plot), 0, std::move(probe)}); published_results.push_back({ @@ -302,8 +308,9 @@ void run_event_latency(benchmark::State& state) { } std::string configuration_failure; - Task_Graph configuration_barrier{"mcp.event-latency.configure"}; - configuration_barrier.add("await.manual-policy", [&] { + if (configuration.manual_policy) { + Task_Graph configuration_barrier{"mcp.event-latency.configure"}; + configuration_barrier.add("await.manual-policy", [&] { const auto deadline = Clock::now() + std::chrono::seconds{10}; Task_Graph::corun_until([&] { const bool ready = std::ranges::all_of( @@ -318,10 +325,30 @@ void run_event_latency(benchmark::State& state) { if (Clock::now() < deadline) return false; configuration_failure = "timed out applying manual backend frame policy"; + for (std::size_t index = 0; index < active.size(); ++index) { + const auto& item = active[index]; + const auto diagnostics = item.plot->diagnostics(); + const auto& policy = diagnostics.at("frame_policy"); + const auto& policy_configuration = + policy.at("configuration"); + if (policy_configuration.at("mode") == "manual") + continue; + const auto& lifecycle = policy.at("lifecycle"); + configuration_failure += "\n " + + published_results[index].id + + ": mode=" + + policy_configuration.at("mode").get() + + ", lifecycle=" + + lifecycle.at("state").get() + + ", active=" + std::to_string( + lifecycle.at("active_frame_count"). + get()); + } return true; }); - }); - aethera::detail::run_taskflow(configuration_barrier); + }); + aethera::detail::run_taskflow(configuration_barrier); + } if (!configuration_failure.empty()) throw std::runtime_error(configuration_failure); @@ -500,6 +527,8 @@ BENCHMARK(run_event_latency)->Iterations(1)->UseRealTime(); constexpr std::string_view samples_prefix{ "--aethera_event_samples="}; constexpr std::string_view size_prefix{"--aethera_size="}; + constexpr std::string_view plots_prefix{"--aethera_plots="}; + constexpr std::string_view policy_prefix{"--aethera_policy="}; if (argument.starts_with(samples_prefix)) { if (!parse_unsigned(argument.substr(samples_prefix.size()), configuration.samples) || @@ -523,6 +552,26 @@ BENCHMARK(run_event_latency)->Iterations(1)->UseRealTime(); } continue; } + if (argument.starts_with(plots_prefix)) { + const auto value = argument.substr(plots_prefix.size()); + if (value == "all") configuration.three_d_only = false; + else if (value == "3d") configuration.three_d_only = true; + else { + std::cerr << "--aethera_plots requires all or 3d\n"; + return false; + } + continue; + } + if (argument.starts_with(policy_prefix)) { + const auto value = argument.substr(policy_prefix.size()); + if (value == "manual") configuration.manual_policy = true; + else if (value == "current") configuration.manual_policy = false; + else { + std::cerr << "--aethera_policy requires manual or current\n"; + return false; + } + continue; + } argv[retained++] = argv[index]; } argc = retained; @@ -538,7 +587,14 @@ int main(int argc, char** argv) { aethera::initialize_runtime({}); benchmark::Initialize(&argc, argv); if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 2; - benchmark::AddCustomContext("aethera_plots", "all"); + benchmark::AddCustomContext( + "aethera_plots", + aethera::mcp::event_latency_benchmarks::configuration.three_d_only + ? "3d" : "all"); + benchmark::AddCustomContext( + "aethera_policy", + aethera::mcp::event_latency_benchmarks::configuration.manual_policy + ? "manual" : "current"); benchmark::AddCustomContext( "aethera_event_samples", std::to_string( diff --git a/render_2D/render_2D/base/Frame_2D.cpp b/render_2D/render_2D/base/Frame_2D.cpp index 490dfd1..2a3167e 100644 --- a/render_2D/render_2D/base/Frame_2D.cpp +++ b/render_2D/render_2D/base/Frame_2D.cpp @@ -11,12 +11,11 @@ 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, - const Frame_Policy::State& frame_policy) { + Frame_Request_Source source) { 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, frame_policy); + Render_Frame::begin(identity, source); 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 5b30fa1..7944f64 100644 --- a/render_2D/render_2D/base/Frame_2D.hpp +++ b/render_2D/render_2D/base/Frame_2D.hpp @@ -26,8 +26,7 @@ public: /* 保留二维后端图像分配,只重启物理槽承载的新逻辑帧。 */ void begin(Frame_Identity identity, Pixel_Format output_format = native_pixel_format, - Frame_Request_Source source = Frame_Request_Source::unspecified, - const Frame_Policy::State& frame_policy = {}); + Frame_Request_Source source = Frame_Request_Source::unspecified); [[nodiscard]] Pixel_Format output_format() const noexcept; [[nodiscard]] Image_View image() const; /* 按本帧声明的输出格式生成连续像素;原生格式只做必要的行打包。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 3d53ac6..0d061c0 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -8,6 +8,28 @@ Render_Scene_2D::Render_Result_Type Render_Scene_2D::render( return double_buffer::detail::Internal_Access::get(this).render( this, frame, std::move(callback)); } + +Render_Frame_Result Render_Scene_2D::render_frame( + not_null frame, Render_Frame_Callbacks callbacks) { + auto* concrete = dynamic_cast(frame.get()); + if (!concrete) return Render_Frame_Result::incompatible_frame; + const auto result = render( + not_null{concrete}, + [callbacks = std::move(callbacks)](not_null completed) mutable { + if (callbacks.submitted) callbacks.submitted(completed, false); + if (callbacks.completed) callbacks.completed(completed); + }); + if (result) return Render_Frame_Result::submitted; + switch (result.error()) { + case Render_Result::frame_in_flight: + return Render_Frame_Result::frame_pipeline_busy; + case Render_Result::view_inactive: + return Render_Frame_Result::view_inactive; + case Render_Result::empty_viewport: + return Render_Frame_Result::empty_viewport; + } + return Render_Frame_Result::backend_unavailable; +} void Render_Scene_2D::activate_view() { double_buffer::detail::Internal_Access::get(this).set_view_active(this, true); } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index 32a240e..0dad579 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -10,7 +10,8 @@ namespace aethera::render_2d { /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ struct Render_Scene_2D : Def> { + Dependency_Graph_Type>, + Render_Frame_Scene { struct Prop : Prev_Prop { Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ @@ -38,6 +39,9 @@ struct Render_Scene_2D : Def frame, Frame_Callback callback); + [[nodiscard]] Render_Frame_Result render_frame( + not_null frame, + Render_Frame_Callbacks callbacks) override; /* * 向调用方拥有的 Frame 合成一次。Scene 只借用传入地址;全部绘制、 * completion Taskflow 和 Trace 收口并清除借用后,唯一回调通知调用方。 diff --git a/render_3D/render_3D/base/Frame_3D.cpp b/render_3D/render_3D/base/Frame_3D.cpp index efd9e60..146148f 100644 --- a/render_3D/render_3D/base/Frame_3D.cpp +++ b/render_3D/render_3D/base/Frame_3D.cpp @@ -16,11 +16,10 @@ 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, - const Frame_Policy::State& frame_policy) { + Pixel_Format output_format, Frame_Request_Source source) { if (output_format != native_pixel_format) throw std::invalid_argument("Frame_3D output pixel format is unsupported"); - Render_Frame::begin(identity, source, frame_policy); + Render_Frame::begin(identity, source); 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 47a7300..ea353ad 100644 --- a/render_3D/render_3D/base/Frame_3D.hpp +++ b/render_3D/render_3D/base/Frame_3D.hpp @@ -38,8 +38,7 @@ 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, - const Frame_Policy::State& frame_policy = {}); + Frame_Request_Source source = Frame_Request_Source::unspecified); [[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/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index 48d62b3..eea8be5 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -7,4 +7,36 @@ Render_Scene_3D::Render_Result Render_Scene_3D::render( return double_buffer::detail::Internal_Access::get(this).render( this, frame, std::move(callbacks)); } + +Render_Frame_Result Render_Scene_3D::render_frame( + not_null frame, Render_Frame_Callbacks callbacks) { + auto* concrete = dynamic_cast(frame.get()); + if (!concrete) return Render_Frame_Result::incompatible_frame; + const auto result = render( + not_null{concrete}, + Frame_Callbacks{ + .submitted = [callback = std::move(callbacks.submitted)]( + not_null submitted, + bool released_after_submission) mutable { + if (callback) + callback(submitted, released_after_submission); + }, + .completed = [callback = std::move(callbacks.completed)]( + not_null completed) mutable { + if (callback) callback(completed); + }}); + switch (result) { + case Render_Result::submitted: + return Render_Frame_Result::submitted; + case Render_Result::frame_pipeline_busy: + return Render_Frame_Result::frame_pipeline_busy; + case Render_Result::view_inactive: + return Render_Frame_Result::view_inactive; + case Render_Result::empty_viewport: + return Render_Frame_Result::empty_viewport; + case Render_Result::backend_unavailable: + return Render_Frame_Result::backend_unavailable; + } + return Render_Frame_Result::backend_unavailable; +} } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index f15dd60..2675001 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -13,7 +13,7 @@ namespace aethera::render_3d { namespace detail { struct Datoviz_Visual_Operations; } /* 执行公共 Renderable 图,并把结果无等待地提交给 Datoviz 渲染域。 */ -struct Render_Scene_3D : Def { +struct Render_Scene_3D : Def, Render_Frame_Scene { struct Prop : Prev_Prop { Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */ Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */ @@ -59,6 +59,9 @@ struct Render_Scene_3D : Def { using Render_Result_Type = Render_Result; using Render_Frame_Type = Frame_3D; [[nodiscard]] Render_Result render(not_null frame, Frame_Callbacks callbacks); + [[nodiscard]] Render_Frame_Result render_frame( + not_null frame, + Render_Frame_Callbacks callbacks) override; /* * 无等待写入调用方拥有的 Frame;Scene 始终只借用地址。最多三个借用可在 * CPU Prepare、GPU 和读回阶段重叠,完成全部写入并清除借用后调用唯一完成回调。 diff --git a/绘制优化.md b/绘制优化.md deleted file mode 100644 index d7de1ff..0000000 --- a/绘制优化.md +++ /dev/null @@ -1,386 +0,0 @@ -可以实现,而且你这个“天然分区并行”的场景很适合这么做。但要把边界说清楚:这不是 Blend2D 官方提供的 `BLImageView` 功能,而是利用 -`BLImage::create_from_data()` 给同一块外部 framebuffer 建多个独立的 `BLImage` 包装。Blend2D 官方明确支持外部 -`pixel_data + stride` 创建 image。 ([Blend2D][1]) - -比如最终 framebuffer 是 1920×1080 PRGB32: - -```cpp -struct Framebuffer { - uint8_t* data; - int width; - int height; - intptr_t stride; -}; - -BLImage make_view( - Framebuffer& fb, - int x, - int y, - int width, - int height) -{ - BLImage image; - uint8_t* ptr = fb.data + y * fb.stride + x * 4; - - image.create_from_data( - width, - height, - BL_FORMAT_PRGB32, - ptr, - fb.stride, - BL_DATA_ACCESS_RW); - - return image; -} -``` - -比如: - -```text -1920 × 1080 framebuffer - -┌──────────────┬──────────────┐ -│ view A │ view B │ -│ 960×540 │ 960×540 │ -├──────────────┼──────────────┤ -│ view C │ view D │ -│ 960×540 │ 960×540 │ -└──────────────┴──────────────┘ -``` - -然后: - -```cpp -auto a = make_view(fb, 0, 0, 960, 540); -auto b = make_view(fb, 960, 0, 960, 540); -auto c = make_view(fb, 0, 540, 960, 540); -auto d = make_view(fb, 960, 540, 960, 540); -``` - -Taskflow: - -```text -worker 0 → BLContext(a) -worker 1 → BLContext(b) -worker 2 → BLContext(c) -worker 3 → BLContext(d) -``` - -四个 context 都可以是同步单线程 context: - -```cpp -BLContext ctx(a); -``` - -Blend2D 文档说明这种普通 `BLContext(image)` 就是 single-threaded synchronous rendering。 ([Blend2D][2]) - -最关键的是为什么它可以绕开“一个 image 只能有一个 renderer”。 - -因为 Blend2D 的 exclusive-access 管理针对的是它自己的 image data 对象。`BLContext::begin()` 会增加目标 image 的 writer -count,并保证同一个 image data 不被另一个 renderer 同时使用。 ([Blend2D][2]) - -而你这里创建的是: - -```text -BLImage A → external-memory wrapper A -BLImage B → external-memory wrapper B -BLImage C → external-memory wrapper C -``` - -它们不是: - -```cpp -BLImage b = a; -``` - -这种 weak copy。 - -每次都是独立调用: - -```cpp -create_from_data(...) -``` - -所以 Blend2D 看到的是三个不同 image data 对象。 - -只是: - -```text -物理内存: - -A.data ─┐ -B.data ─┼→ 同一个 framebuffer allocation -C.data ─┘ -``` - -Blend2D并不知道这些外部内存背后属于同一 allocation。 - -因此真正的线程安全责任就在你这里。 - -你必须保证: - -```text -A 实际写入地址集合 -∩ -B 实际写入地址集合 -= -∅ -``` - -如果做到这一点,从 C++ 内存访问角度就是正常的并行写不同内存。 - -你需要特别注意几个问题。 - -第一,`stride` 必须是父 framebuffer 的 stride。 - -例如: - -```text -parent width = 1920 -PRGB32 = 4 bytes -stride = 7680 -``` - -B 虽然只有 960 宽: - -```cpp -pixel_data = base + 960 * 4; -``` - -但: - -```cpp -stride = 1920 * 4; -``` - -仍然不能写成: - -```cpp -stride = 960 * 4; -``` - -否则下一行就跑错位置。 - -`BLImageData` 本身也是这么定义的:`pixel_data` 是图像左上角,`stride` 是每行之间的字节跨度。 ([Blend2D][3]) - -第二,view 范围必须真的落在 framebuffer 内: - -```text -x >= 0 -y >= 0 - -x + width <= framebuffer.width -y + height <= framebuffer.height -``` - -这个检查我会放在你创建 surface/view 的最外层,而不是每次绘制都检查。 - -第三,也是比较重要的:不要让每个 view 都释放同一个 framebuffer。 - -`create_from_data()` 有: - -```cpp -destroy_func -user_data -``` - -用于外部内存生命周期管理。 ([Blend2D][1]) - -如果你这样干: - -```text -view A destroy → free(framebuffer) -view B destroy → free(framebuffer) -``` - -就是 double-free。 - -你这种结构最简单的是: - -```cpp -image.create_from_data( - ..., - BL_DATA_ACCESS_RW, - nullptr, - nullptr); -``` - -由你的 framebuffer owner 自己负责生命周期: - -```text -Framebuffer - owns memory - - ├── ImageView A - ├── ImageView B - ├── ImageView C - └── ImageView D -``` - -保证: - -```text -所有 ImageView / BLContext 销毁 - ↓ -Framebuffer 才销毁 -``` - -如果必须跨生命周期,就自己搞共享 owner/refcount。 - -第四,不要 weak-copy view 再期待两个 renderer 并行: - -```cpp -BLImage a = make_view(...); -BLImage b = a; -``` - -这不是两个 view。 - -这是共享同一个 Blend2D image data。Blend2D 的 `BLImage` copy 是 weak-copy,会共享底层 image data。 ([Blend2D][1]) - -应该每个 region 独立: - -```cpp -BLImage a; -a.create_from_data(...); - -BLImage b; -b.create_from_data(...); -``` - -第五,我建议你的 view 自己建立局部坐标系。 - -比如 Plot B 在最终 framebuffer: - -```text -x = 960 -y = 0 -w = 960 -h = 540 -``` - -但是它拿到的 BLImage 是: - -```text -0 .. 959 -0 .. 539 -``` - -所以 Plot 内部正常画: - -```cpp -ctx.fill_rect(BLRect(0, 0, 960, 540), ...); -``` - -不需要知道自己在父 framebuffer 里是 `(960, 0)`。 - -这对你的 Scene/Plot 抽象非常干净: - -```text -Parent framebuffer coordinates - ↓ -Surface/View - ↓ -local coordinates - ↓ -Plot renderer -``` - -还有一个容易担心但实际上比较好的点:Blend2D 会裁剪到当前 image target。 - -所以一个 960×540 的 view,即使你的 path 坐标跑到: - -```text -x = 1500 -``` - -Blend2D target 本身只有: - -```text -960 × 540 -``` - -正常 raster 不应该越过 target memory 定义的逻辑宽度去写相邻区域。 - -也就是说 view 本身实际上同时给了你一个天然 clip boundary。 - -所以我会把你的最终结构做成: - -```cpp -class Surface { -public: - BLImage image; - int x; - int y; - int width; - int height; -}; -``` - -然后: - -```text -Framebuffer - │ - ├── Surface Plot0 - │ └── BLImage(external framebuffer region) - │ - ├── Surface Plot1 - │ └── BLImage(external framebuffer region) - │ - └── Surface Plot2 - └── BLImage(external framebuffer region) -``` - -Taskflow: - -```text - framebuffer - - ┌──────────┬──────────┐ - │ Plot A │ Plot B │ - ├──────────┼──────────┤ - │ Plot C │ Plot D │ - └──────────┴──────────┘ - ▲ ▲ - │ │ - Task A Task B -``` - -这种情况下不存在: - -```text -Image A -Image B -Image C - ↓ -composite -``` - -每个任务第一次 rasterize 就已经直接落到最终 framebuffer。 - -不过我要保留一个工程上的判断: - -**这是利用 Blend2D external-image API 实现的物理 framebuffer 分区,并不是 Blend2D 文档明确承诺的“多个 external `BLImage` -指向同一 allocation 的不同区域并发渲染”高级用法。** - -Blend2D 官方承诺的是 external memory image 本身可用,以及一个 context 对其 image data 独占;它不会理解你几个独立 external -image 的 backing allocations 是否重叠。 ([Blend2D][1]) - -所以我会把它当成: - -```text -可实现 -+ -原理成立 -+ -需要你自己严格保证 region 不重叠 -+ -最好做一次 TSAN + 压力测试 + framebuffer guard 测试 -``` - -对于你这种“一个大图天然分成多个 Plot”的场景,我认为值得做,而且比“各画一张临时图最后再 blit”更符合你追求后台性能的方向。 - -[1]: https://blend2d.com/doc/classBLImage.html?utm_source=chatgpt.com "API - Docs - Blend2D" -[2]: https://blend2d.com/doc/classBLContext.html?utm_source=chatgpt.com "API - Docs - Blend2D" -[3]: https://blend2d.com/doc/structBLImageData.html?utm_source=chatgpt.com "API - Docs - Blend2D"