diff --git a/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.cpp b/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.cpp index 6295d18..dda5e20 100644 --- a/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.cpp +++ b/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.cpp @@ -67,7 +67,10 @@ Throttled_Latest_only::~Throttled_Latest_only() noexcept { Throttled_Latest_only::Start_Result Throttled_Latest_only::start(double frames_per_second) { - return d->start(frames_per_second); + auto& properties_storage = d->get(); + properties_storage.internal.advance(); + const Prop& properties = *properties_storage.internal.use(); + return d->start(properties, state, frames_per_second); } Throttled_Latest_only::Stop_Result @@ -84,13 +87,16 @@ Throttled_Latest_only::Private::Private( sink(std::move(sink)) {} Throttled_Latest_only::Start_Result -Throttled_Latest_only::Private::start(double frames_per_second) { +Throttled_Latest_only::Private::start( + const Prop& properties, + Export_Struct& published_state, + double frames_per_second) { const auto interval = frame_interval(frames_per_second); if (!interval) { return interval.error(); } - const auto prepared = prepare_start(); + const auto prepared = prepare_start(properties, published_state); if (prepared != Start_Result::started) { return prepared; } @@ -99,8 +105,8 @@ Throttled_Latest_only::Private::start(double frames_per_second) { try { scheduled_timer_id = timer_service->schedule_every( *interval, - [this] { - frame_due(); + [this, &published_state] { + frame_due(published_state); }); } catch (...) { @@ -136,7 +142,6 @@ Throttled_Latest_only::Private::stop(Stop_Completion completion) { cancelled_timer_id = *timer_id; phase = Phase::stopping; stop_completion = std::move(completion); - timer_cancel_confirmed = false; } try { @@ -154,7 +159,9 @@ Throttled_Latest_only::Private::stop(Stop_Completion completion) { } Throttled_Latest_only::Start_Result -Throttled_Latest_only::Private::prepare_start() { +Throttled_Latest_only::Private::prepare_start( + const Prop& properties, + Export_Struct& published_state) { { std::lock_guard lock(mutex); switch (phase) { @@ -170,6 +177,9 @@ Throttled_Latest_only::Private::prepare_start() { if (!timer_service || !scene || !sink) { return Start_Result::dependency_unavailable; } + if (!prepare_statistics(properties, published_state)) { + return Start_Result::invalid_statistics_window; + } phase = Phase::starting; } @@ -179,19 +189,27 @@ Throttled_Latest_only::Private::prepare_start() { phase = Phase::stopped; } }); - auto created_frame = scene->create_frame(); - if (!created_frame) { - return Start_Result::frame_unavailable; + std::array, Frame_Count> created_frames; + for (auto& created_frame : created_frames) { + created_frame = scene->create_frame(); + if (!created_frame) { + return Start_Result::frame_unavailable; + } } { std::lock_guard lock(mutex); - if (phase != Phase::starting || frame) { + if (phase != Phase::starting) { std::terminate(); } - frame = std::move(created_frame); - frame_phase = Frame_Phase::idle; - timer_cancel_confirmed = false; + for (std::size_t index = 0; index < Frame_Count; ++index) { + if (frames[index].frame) { + std::terminate(); + } + frames[index] = Frame_Slot{}; + frames[index].frame = std::move(created_frames[index]); + } + next_frame_index = 0; } rollback.release(); return Start_Result::started; @@ -199,33 +217,37 @@ Throttled_Latest_only::Private::prepare_start() { void Throttled_Latest_only::Private::timer_started(Timer_Id id) { std::lock_guard lock(mutex); - if (id == 0 || - phase != Phase::starting || - !frame || - timer_id.has_value()) { + if (id == 0 || phase != Phase::starting || timer_id.has_value()) { std::terminate(); } + for (const auto& slot : frames) { + if (!slot.frame || slot.phase != Frame_Phase::idle) { + std::terminate(); + } + } timer_id = id; phase = Phase::running; } void Throttled_Latest_only::Private::start_failed() { - proxy retired_frame; + std::array, Frame_Count> retired_frames; { std::lock_guard lock(mutex); if (phase != Phase::starting || timer_id) { std::terminate(); } - retired_frame = std::move(frame); - frame_phase = Frame_Phase::idle; + for (std::size_t index = 0; index < Frame_Count; ++index) { + retired_frames[index] = std::move(frames[index].frame); + frames[index] = Frame_Slot{}; + } + next_frame_index = 0; phase = Phase::stopped; } - retired_frame.reset(); } void Throttled_Latest_only::Private::cancel_failed() { std::lock_guard lock(mutex); - if (phase != Phase::stopping || timer_cancel_confirmed) { + if (phase != Phase::stopping || !timer_id) { std::terminate(); } stop_completion = {}; @@ -235,113 +257,167 @@ void Throttled_Latest_only::Private::cancel_failed() { void Throttled_Latest_only::Private::timer_cancelled() { { std::lock_guard lock(mutex); - if (phase != Phase::stopping || timer_cancel_confirmed) { + if (phase != Phase::stopping || !timer_id) { std::terminate(); } timer_id.reset(); - timer_cancel_confirmed = true; } finish_stop_if_ready(); } -void Throttled_Latest_only::Private::frame_due() { +void Throttled_Latest_only::Private::frame_due( + Export_Struct& published_state) { + std::size_t selected_index = Frame_Count; { std::lock_guard lock(mutex); - if (phase != Phase::running || frame_phase != Frame_Phase::idle) { + if (phase != Phase::running) { return; } - frame_phase = Frame_Phase::rendering; + for (std::size_t offset = 0; offset < Frame_Count; ++offset) { + const std::size_t candidate = + (next_frame_index + offset) % Frame_Count; + if (frames[candidate].phase == Frame_Phase::idle) { + selected_index = candidate; + break; + } + } + record_timer_tick(selected_index == Frame_Count, published_state); + if (selected_index == Frame_Count) { + return; + } + Frame_Slot& slot = frames[selected_index]; + slot.phase = Frame_Phase::rendering; + slot.render_start = std::chrono::steady_clock::now(); + next_frame_index = (selected_index + 1) % Frame_Count; } try { scene->render( - frame, - [this](proxy& completed_frame) { - rendered(completed_frame); + frames[selected_index].frame, + [this, selected_index, &published_state]( + proxy& completed_frame) { + rendered(selected_index, published_state, completed_frame); }); } catch (...) { - abandon_render(); + abandon_render(selected_index); throw; } } void Throttled_Latest_only::Private::rendered( + std::size_t slot_index, + Export_Struct& published_state, proxy& completed_frame) { { std::lock_guard lock(mutex); - if (std::addressof(completed_frame) != std::addressof(frame) || - frame_phase != Frame_Phase::rendering) { + if (slot_index >= Frame_Count) { std::terminate(); } - frame_phase = Frame_Phase::sending; + Frame_Slot& slot = frames[slot_index]; + if (std::addressof(completed_frame) != std::addressof(slot.frame) || + slot.phase != Frame_Phase::rendering) { + std::terminate(); + } + slot.render_end = std::chrono::steady_clock::now(); + record_render( + std::chrono::duration_cast( + slot.render_end - slot.render_start), + published_state); + slot.phase = Frame_Phase::sending; + slot.send_start = std::chrono::steady_clock::now(); } try { sink->send( - frame, - [this](proxy& sent_frame) { - sent(sent_frame); + frames[slot_index].frame, + [this, slot_index, &published_state](proxy& sent_frame) { + sent(slot_index, published_state, sent_frame); }); } catch (...) { - abandon_send(); + abandon_send(slot_index); throw; } } void Throttled_Latest_only::Private::sent( + std::size_t slot_index, + Export_Struct& published_state, proxy& completed_frame) { { std::lock_guard lock(mutex); - if (std::addressof(completed_frame) != std::addressof(frame) || - frame_phase != Frame_Phase::sending) { + if (slot_index >= Frame_Count) { std::terminate(); } - frame_phase = Frame_Phase::idle; + Frame_Slot& slot = frames[slot_index]; + if (std::addressof(completed_frame) != std::addressof(slot.frame) || + slot.phase != Frame_Phase::sending) { + std::terminate(); + } + slot.send_end = std::chrono::steady_clock::now(); + record_send( + std::chrono::duration_cast( + slot.send_end - slot.send_start), + std::chrono::duration_cast( + slot.send_end - slot.render_start), + published_state); + slot.phase = Frame_Phase::idle; } finish_stop_if_ready(); } -void Throttled_Latest_only::Private::abandon_render() { +void Throttled_Latest_only::Private::abandon_render( + std::size_t slot_index) { { std::lock_guard lock(mutex); - if (frame_phase != Frame_Phase::rendering) { + if (slot_index >= Frame_Count || + frames[slot_index].phase != Frame_Phase::rendering) { return; } - frame_phase = Frame_Phase::idle; + frames[slot_index].phase = Frame_Phase::idle; } finish_stop_if_ready(); } -void Throttled_Latest_only::Private::abandon_send() { +void Throttled_Latest_only::Private::abandon_send( + std::size_t slot_index) { { std::lock_guard lock(mutex); - if (frame_phase != Frame_Phase::sending) { + if (slot_index >= Frame_Count || + frames[slot_index].phase != Frame_Phase::sending) { return; } - frame_phase = Frame_Phase::idle; + frames[slot_index].phase = Frame_Phase::idle; } finish_stop_if_ready(); } void Throttled_Latest_only::Private::finish_stop_if_ready() { - proxy retired_frame; + std::array, Frame_Count> retired_frames; Stop_Completion completion; { std::lock_guard lock(mutex); - if (phase != Phase::stopping || - !timer_cancel_confirmed || - frame_phase != Frame_Phase::idle) { + if (phase != Phase::stopping || timer_id) { return; } - retired_frame = std::move(frame); + for (const auto& slot : frames) { + if (slot.phase != Frame_Phase::idle) { + return; + } + } + for (std::size_t index = 0; index < Frame_Count; ++index) { + retired_frames[index] = std::move(frames[index].frame); + frames[index] = Frame_Slot{}; + } completion = std::move(stop_completion); - timer_cancel_confirmed = false; + next_frame_index = 0; phase = Phase::stopped; } - retired_frame.reset(); + for (auto& retired_frame : retired_frames) { + retired_frame.reset(); + } try { completion(); } @@ -352,10 +428,11 @@ void Throttled_Latest_only::Private::finish_stop_if_ready() { bool Throttled_Latest_only::Private::destructible() const noexcept { std::lock_guard lock(mutex); - return phase == Phase::stopped && - frame_phase == Frame_Phase::idle && - !frame && - !timer_id && - !stop_completion; + if (phase != Phase::stopped || timer_id || stop_completion) { + return false; + } + return std::ranges::all_of(frames, [](const Frame_Slot& slot) { + return !slot.frame && slot.phase == Frame_Phase::idle; + }); } } diff --git a/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.hpp b/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.hpp index 6d64ae9..b880127 100644 --- a/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.hpp +++ b/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.hpp @@ -1,15 +1,15 @@ #pragma once +#include "function/frame_policy/Frame_Policy.hpp" #include "function/frame_policy/global.hpp" -#include "model/Model.hpp" #include #include namespace aethera { /* - * 固定帧率、最多一帧在途的策略。Scene 创建具体帧;策略持有并重复使用, - * stop completion 执行前会排空 Scene/Sink 借用并销毁该帧。 + * 固定帧率、最多三帧在途的 latest-only 策略。Scene 创建三个具体帧;策略轮转复用, + * 三槽均忙时丢弃本次 tick;stop completion 前排空 Scene/Sink 借用并销毁全部帧。 */ struct Throttled_Latest_only : -Def> { +Def> { enum struct Start_Result : std::uint8_t { started, already_running, @@ -18,7 +18,8 @@ Def> { dependency_unavailable, frame_unavailable, invalid_frames_per_second, - interval_out_of_range + interval_out_of_range, + invalid_statistics_window }; enum struct Stop_Result : std::uint8_t { stopping, @@ -28,7 +29,7 @@ Def> { completion_missing }; using Stop_Completion = std::function; - struct Prop : Prev_Prop {}; + struct Prop : Prev {}; struct Private; Throttled_Latest_only(proxy timer_service, proxy scene, diff --git a/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.ipp b/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.ipp index 53a2da2..ccb7ba9 100644 --- a/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.ipp +++ b/kernel/kernel/include/function/frame_policy/Throttled_Latest_Only.ipp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -16,37 +18,57 @@ struct Throttled_Latest_only::Private : Prev_Private { rendering, sending }; + struct Frame_Slot { + proxy frame; /* 帧池在本槽唯一拥有并循环复用的物理帧。 */ + Frame_Phase phase{Frame_Phase::idle}; /* 本槽当前借用方的唯一权威状态。 */ + std::chrono::steady_clock::time_point render_start; /* 最近一次渲染开始的单调时刻。 */ + std::chrono::steady_clock::time_point render_end; /* 最近一次渲染完成的单调时刻。 */ + std::chrono::steady_clock::time_point send_start; /* 最近一次发送开始的单调时刻。 */ + std::chrono::steady_clock::time_point send_end; /* 最近一次发送完成的单调时刻。 */ + }; + + static constexpr std::size_t Frame_Count{3}; Private(proxy timer_service, proxy scene, proxy sink); - Start_Result start(double frames_per_second); + Start_Result start( + const Prop& properties, + Export_Struct& published_state, + double frames_per_second); Stop_Result stop(Stop_Completion completion); [[nodiscard]] bool destructible() const noexcept; private: - Start_Result prepare_start(); + Start_Result prepare_start( + const Prop& properties, + Export_Struct& published_state); void timer_started(Timer_Id id); void start_failed(); void cancel_failed(); void timer_cancelled(); - void frame_due(); - void rendered(proxy& completed_frame); - void sent(proxy& completed_frame); - void abandon_render(); - void abandon_send(); + void frame_due(Export_Struct& published_state); + void rendered( + std::size_t slot_index, + Export_Struct& published_state, + proxy& completed_frame); + void sent( + std::size_t slot_index, + Export_Struct& published_state, + proxy& completed_frame); + void abandon_render(std::size_t slot_index); + void abandon_send(std::size_t slot_index); void finish_stop_if_ready(); proxy timer_service; /* 周期调度和异步取消的必填业务能力所有权。 */ - proxy scene; /* 帧创建及渲染借用的必填业务能力所有权。 */ + proxy scene; /* 帧池创建及渲染借用的必填业务能力所有权。 */ proxy sink; /* 已渲染帧媒体分派的必填业务能力所有权。 */ - proxy frame; /* 策略创建、复用并在停止完成前销毁的唯一帧。 */ + std::array frames; /* 固定三个槽位的轮转帧池。 */ Stop_Completion stop_completion; /* 本次异步停止的唯一完成回调。 */ std::optional timer_id; /* running/stopping 阶段的周期计时器标识。 */ - mutable std::mutex mutex; /* 保护生命周期和跨线程帧借用阶段。 */ + mutable std::mutex mutex; /* 保护生命周期、槽位借用及统计发布顺序。 */ Phase phase{Phase::stopped}; /* start/stop 生命周期的唯一权威状态。 */ - Frame_Phase frame_phase{Frame_Phase::idle}; /* 唯一帧当前借用方的权威状态。 */ - bool timer_cancel_confirmed{}; /* stopping 阶段的异步取消确认。 */ + std::size_t next_frame_index{}; /* 下次选槽的轮转起点,范围 [0, Frame_Count)。 */ }; } diff --git a/kernel/kernel/include/model/Model.hpp b/kernel/kernel/include/model/Model.hpp index e9fba93..3c6388e 100644 --- a/kernel/kernel/include/model/Model.hpp +++ b/kernel/kernel/include/model/Model.hpp @@ -1,29 +1,120 @@ #pragma once + #include "../concurrent/Concurrent_Storage_with_dirty.hpp" +#include "../concurrent/storage/Tagged_Storage.hpp" + #include +#include + namespace aethera { -template struct Root { - struct Prop {}; +namespace detail { +template +struct Model_Type_List {}; + +template +struct Append_Model_Registrations; +template +struct Model_Tag_Registered; +template +struct Previous_Model_Value; + +struct Empty_Prop {}; +struct Empty_State {}; +} + +/* Tag 负责选择各定义层的 Prop 类型;同一个 Tag 在注册表中只允许出现一次。 */ +struct Prop_Tag { + using Root_Type = detail::Empty_Prop; + template + using Value = typename Layer::Prop; +}; + +/* Tag 负责选择各定义层的 State 类型。 */ +struct State_Tag { + using Root_Type = detail::Empty_State; + template + using Value = typename Layer::State; +}; + +/* 把一个业务 Tag 注册到接受单个值类型的 Concurrent Storage 模板。 */ +template typename Concurrent_Storage> +struct Storage_Registration { + using Tag_Type = Tag; + template + using Storage = Concurrent_Storage; +}; + +template +struct Root { + using Storage_Registrations = detail::Model_Type_List<>; + template + using Prev = typename Tag::Root_Type; + struct Private {}; struct Builder {}; }; -template struct Def : Base { + +/* + * 只扩展类型与存储注册链,不创建运行时 d;供多个业务定义层共同组成最终 Def。 + */ +template +struct Model_Layer : Base { + using Storage_Registrations = typename detail::Append_Model_Registrations< + typename Base::Storage_Registrations, + Registrations...>::Type; + template + using Prev = typename detail::Previous_Model_Value< + Tag, + Base, + detail::Model_Tag_Registered< + Tag, + typename Base::Storage_Registrations>::value>::Type; + + using Prev_Private = typename Base::Private; + using Prev_Builder = typename Base::Builder; +}; + +/* 最终定义层根据完整注册表构造唯一 Private 与全部 Tagged Storage。 */ +template +struct Def : Model_Layer { + using Layer = Model_Layer; + using Storage_Registrations = typename Layer::Storage_Registrations; + using Prev_Private = typename Base::Private; + struct Private; - struct Builder { - using Prop = typename Self::Prop; + struct Builder : Base::Builder { template explicit Builder(Args&&... args); std::unique_ptr build(); - template T> Builder& set_prop(Member Prop::* member, T&& value); + template < + template typename Concurrent_Storage, + typename Tag, + typename... Args> + requires requires( + Concurrent_Storage>& storage, + Args&&... args) { + storage.write(std::forward(args)...); + } + Builder& set(Args&&... args); + std::unique_ptr ret; /* 构建期间唯一拥有尚未发布的对象。 */ }; - template Self& set_prop(Args&&... args); - using Prev_Prop = Base::Prop; - using Prev_Private = Base::Private; - using Prev_Builder = Base::Builder; + + template < + template typename Concurrent_Storage, + typename Tag, + typename... Args> + requires requires( + Concurrent_Storage>& storage, + Args&&... args) { + storage.write(std::forward(args)...); + } + Self& write(Args&&... args); template explicit Def(Args&&... args); - std::unique_ptr d; /* 唯一拥有本定义层的并发属性实现。 */ + + std::unique_ptr d; /* 唯一拥有最终 Private 和完整 Tagged Storage 集。 */ }; } + #include "Model.ipp" diff --git a/kernel/kernel/include/model/Model.ipp b/kernel/kernel/include/model/Model.ipp index 4ade493..fa5bb1a 100644 --- a/kernel/kernel/include/model/Model.ipp +++ b/kernel/kernel/include/model/Model.ipp @@ -1,49 +1,165 @@ #pragma once + +#include #include -namespace aethera { +namespace aethera::detail { +template +requires Value_Writer +void initialize_model_value(Value& target, Fn& fn) { + std::invoke(fn, target); +} + +template +void initialize_model_value( + Value& target, + Member Owner::* member, + T& value) { + target.*member = value; +} + +template +struct Append_Model_Registrations; + +template +struct Append_Model_Registrations< + Model_Type_List, + Registrations...> { + using Type = Model_Type_List; +}; + +template +struct Model_Tag_Registered; + +template +struct Model_Tag_Registered> : + std::bool_constant<( + std::same_as || ...)> {}; + +template +struct Unique_Model_Tags; + +template <> +struct Unique_Model_Tags> : std::true_type {}; + +template +struct Unique_Model_Tags> : + std::bool_constant< + (!std::same_as< + typename Registration::Tag_Type, + typename Rest::Tag_Type> && ...) && + Unique_Model_Tags>::value> {}; + +template +struct Previous_Model_Value; + +template +struct Previous_Model_Value { + using Type = typename Tag::Root_Type; +}; + +template +struct Previous_Model_Value { + using Type = typename Tag::template Value; +}; + +template +struct Attached_Model_Storage { + using Tag = typename Registration::Tag_Type; + using Value = typename Tag::template Value; + using Type = Tagged_Storage< + Tag, + typename Registration::template Storage>; +}; + +template +struct Model_Storage_Set; + +template +struct Model_Storage_Set> : + Tagged_Storage_Set< + typename Attached_Model_Storage::Type...> {}; +} + +namespace aethera { +template +struct Def::Private : + Self::Private, + detail::Model_Storage_Set { + static_assert( + detail::Unique_Model_Tags::value, + "each model storage tag can only be registered once"); -template -struct Def::Private : Self::Private { template explicit Private(Args&&... args) : Self::Private(std::forward(args)...) {} - - Import_Struct_With_Dirty prop; }; -template +template template -Def::Builder::Builder(Args&&... args) : +Def::Builder::Builder(Args&&... args) : ret(std::make_unique(std::forward(args)...)) {} -template +template template -Def::Def(Args&&... args) : +Def::Def(Args&&... args) : d(std::make_unique(std::forward(args)...)) {} -template -std::unique_ptr Def::Builder::build() { +template +std::unique_ptr Def::Builder::build() { return std::move(ret); } -template -template T> -typename Def::Builder& Def::Builder::set_prop( - Member Prop::* member, - T&& value) { - Prop& internal_read = ret->d->prop.internal.internal_read; - Prop& external_write = ret->d->prop.internal.external_write; - internal_read.*member = value; - external_write.*member = std::forward(value); +template +template < + template typename Concurrent_Storage, + typename Tag, + typename... Args> +requires requires( + Concurrent_Storage>& storage, + Args&&... args) { + storage.write(std::forward(args)...); +} +typename Def::Builder& +Def::Builder::set(Args&&... args) { + auto& storage = ret->d->template get(); + using Value = typename Tag::template Value; + static_assert(std::same_as< + std::remove_cvref_t, + Concurrent_Storage>, + "the concurrent storage and tag must name one registration"); + if constexpr (requires(Concurrent_Storage& candidate) { + candidate.internal.internal_read; + candidate.internal.external_write; + }) { + detail::initialize_model_value( + storage.internal.external_write, + args...); + storage.internal.internal_read = storage.internal.external_write; + } else { + storage.write(std::forward(args)...); + } return *this; } -template -template -Self& Def::set_prop(Args&&... args) { - d->prop.write(std::forward(args)...); +template +template < + template typename Concurrent_Storage, + typename Tag, + typename... Args> +requires requires( + Concurrent_Storage>& storage, + Args&&... args) { + storage.write(std::forward(args)...); +} +Self& Def::write(Args&&... args) { + auto& storage = d->template get(); + using Value = typename Tag::template Value; + static_assert(std::same_as< + std::remove_cvref_t, + Concurrent_Storage>, + "the concurrent storage and tag must name one registration"); + storage.write(std::forward(args)...); return static_cast(*this); } - } diff --git a/kernel/kernel/include/model/design.md b/kernel/kernel/include/model/design.md index 41b2211..f545dc2 100644 --- a/kernel/kernel/include/model/design.md +++ b/kernel/kernel/include/model/design.md @@ -2,9 +2,11 @@ | 文件 | 职责 | |------------------|----------------------------------------------| -| `Model.hpp/.ipp` | `Root/Def/Builder` 对象属性层。 | +| `Model.hpp/.ipp` | 按 Tag 组合定义链,并在最终 `Def` 挂接注册存储。 | | `../concurrent/` | Model 使用的并发存储机制;拥有独立设计文档。 | -`Def::Private` 拥有该定义层的 `Import_Struct_With_Dirty`。Builder 同时初始化内部读面和外部写面,建立初始基线且不产生 dirty;运行期 `set_prop(member, value)` 发布整体及对应属性 dirty,回调形式的整对象写入发布整体及全部属性 dirty。 +`Model_Layer` 通过 `Storage_Registration` 注册一条业务值链。每个定义层用 `Prev` 取得前一层值类型并继承;最终 `Def::Private` 根据完整注册表只创建一次 `Tagged_Storage_Set`。同一 Tag 重复注册属于编译期错误。 -属性 binder 只在 `prop.internal.advance()` 之后的同一内部线程阶段查询。外部写入、`advance()` 和 binder 查询按阶段串行,不重叠,因此 dirty revision 与属性表不使用锁或原子。 +Builder 使用 `set(...)`,最终模型使用 `write(...)`。两个模板参数必须命中同一注册项,参数能力直接由对应并发存储的 `write(...)` 重载约束:Struct 可以整对象或按成员写,List 只接受列表批次写入。Builder 对 Struct 同时建立内部读面和外部写面且不产生 dirty;其他存储保留自身写入/消费语义。 + +属性 binder 只在对应 Tag 存储的 `internal.advance()` 之后、同一内部线程阶段查询。外部写入、`advance()` 和 binder 查询按阶段串行且不重叠,因此 dirty revision 与属性表不使用锁或原子。 diff --git a/kernel/kernel/test/function/frame_policy/Throttled_Latest_Only_Tests.cpp b/kernel/kernel/test/function/frame_policy/Throttled_Latest_Only_Tests.cpp index f0ee307..2bb6427 100644 --- a/kernel/kernel/test/function/frame_policy/Throttled_Latest_Only_Tests.cpp +++ b/kernel/kernel/test/function/frame_policy/Throttled_Latest_Only_Tests.cpp @@ -1,18 +1,25 @@ #include "function/frame_policy/Throttled_Latest_Only.hpp" +#include "statistics/Sliding_Statistics.hpp" #include #include +#include #include #include #include -#include #include #include +#include namespace { using namespace std::chrono_literals; +struct Pending_Frame { + std::reference_wrapper> frame; /* 被 Scene/Sink 借用的策略帧。 */ + aethera::FP_Frame_Completion completion; /* 归还同一帧的完成回调所有权。 */ +}; + struct Frame_Policy_Test_Control { void fire_timer() { ASSERT_TRUE(timer_callback); @@ -27,37 +34,31 @@ struct Frame_Policy_Test_Control { } void complete_render() { - ASSERT_TRUE(rendering_frame.has_value()); - auto frame = *rendering_frame; - auto completion = std::move(render_completion); - rendering_frame.reset(); - completion(frame.get()); + ASSERT_FALSE(rendering_frames.empty()); + auto pending = std::move(rendering_frames.front()); + rendering_frames.pop_front(); + pending.completion(pending.frame.get()); } void complete_send() { - ASSERT_TRUE(sending_frame.has_value()); - auto frame = *sending_frame; - auto completion = std::move(send_completion); - sending_frame.reset(); - completion(frame.get()); + ASSERT_FALSE(sending_frames.empty()); + auto pending = std::move(sending_frames.front()); + sending_frames.pop_front(); + pending.completion(pending.frame.get()); } - std::optional>> - rendering_frame; /* Scene 当前借用的策略帧。 */ - std::optional>> - sending_frame; /* Sink 当前借用的策略帧。 */ - aethera::FP_Frame_Completion render_completion;/* Scene 归还帧的完成回调。 */ - aethera::FP_Frame_Completion send_completion; /* Sink 归还帧的完成回调。 */ - aethera::Timer_Callback timer_callback; /* 周期服务保存的帧到期回调。 */ - aethera::Timer_Callback cancel_completion; /* 测试显式完成异步取消的回调。 */ - aethera::proxy* first_frame{}; /* 可空、非拥有的首次帧地址。 */ + std::deque rendering_frames; /* Scene 尚未归还的帧及其回调。 */ + std::deque sending_frames; /* Sink 尚未归还的帧及其回调。 */ + std::vector*> + rendered_frame_addresses; /* 可空、非拥有;记录每次渲染采用的槽位地址。 */ + aethera::Timer_Callback timer_callback; /* 周期服务持有的到期回调。 */ + aethera::Timer_Callback cancel_completion; /* 测试显式完成异步取消的回调。 */ std::chrono::nanoseconds scheduled_interval{}; /* 策略提交给服务的周期。 */ aethera::Timer_Id cancelled_timer_id{}; /* 策略请求取消的计时器标识。 */ std::size_t frames_created{}; /* Scene 创建的物理帧数量。 */ std::size_t frames_alive{}; /* 尚未析构的物理帧数量。 */ - std::size_t render_calls{}; /* 已接受的 Scene 借用次数。 */ - std::size_t send_calls{}; /* 已接受的 Sink 借用次数。 */ - bool reused_same_frame{true}; /* 全部借用是否指向同一策略帧。 */ + std::size_t render_calls{}; /* Scene 已接受的渲染借用次数。 */ + std::size_t send_calls{}; /* Sink 已接受的发送借用次数。 */ bool complete_render_synchronously{}; /* Scene 是否在 render 内立即归还帧。 */ bool complete_send_synchronously{}; /* Sink 是否在 send 内立即归还帧。 */ bool throw_from_send{}; /* Sink 是否以 Unknown Failure 退出分派。 */ @@ -118,19 +119,13 @@ struct Test_Scene { void render( aethera::proxy& frame, aethera::FP_Frame_Completion completion) { - if (control->first_frame == nullptr) { - control->first_frame = std::addressof(frame); - } - control->reused_same_frame = - control->reused_same_frame && - control->first_frame == std::addressof(frame); + control->rendered_frame_addresses.push_back(std::addressof(frame)); ++control->render_calls; if (control->complete_render_synchronously) { completion(frame); return; } - control->rendering_frame = frame; - control->render_completion = std::move(completion); + control->rendering_frames.push_back({frame, std::move(completion)}); } std::shared_ptr control; /* Scene 测试行为的共享控制数据。 */ @@ -140,9 +135,6 @@ struct Test_Sink { void send( aethera::proxy& frame, aethera::FP_Frame_Completion completion) { - control->reused_same_frame = - control->reused_same_frame && - control->first_frame == std::addressof(frame); ++control->send_calls; if (control->throw_from_send) { throw std::runtime_error("test sink failure"); @@ -151,15 +143,16 @@ struct Test_Sink { completion(frame); return; } - control->sending_frame = frame; - control->send_completion = std::move(completion); + control->sending_frames.push_back({frame, std::move(completion)}); } std::shared_ptr control; /* Sink 测试行为的共享控制数据。 */ }; std::unique_ptr make_policy( - const std::shared_ptr& control) { + const std::shared_ptr& control, + bool statistics_enabled = false, + std::size_t statistics_window_size = 120) { auto timer_service = pro::make_proxy< aethera::Periodic_Timer_Service, @@ -168,35 +161,77 @@ std::unique_ptr make_policy( auto sink = pro::make_proxy(control); aethera::Throttled_Latest_only::Builder builder( std::move(timer_service), std::move(scene), std::move(sink)); + builder + .set( + &aethera::Throttled_Latest_only::Prop::statistics_enabled, + statistics_enabled) + .set( + &aethera::Throttled_Latest_only::Prop::statistics_window_size, + statistics_window_size); return builder.build(); } -TEST(Throttled_Latest_Only, Reuses_One_Frame_And_Drops_Busy_Timer_Ticks) { +TEST(Sliding_Statistics, Maintains_Average_P95_And_Deviation_Without_Snapshot) { + aethera::Sliding_Statistics statistics(3); + statistics.add(10.0); + statistics.add(20.0); + statistics.add(30.0); + + auto summary = statistics.summary(); + EXPECT_EQ(summary.sample_count, 3); + EXPECT_DOUBLE_EQ(summary.average, 20.0); + EXPECT_DOUBLE_EQ(summary.p95, 30.0); + EXPECT_NEAR(summary.standard_deviation, 8.164965809, 1e-9); + + statistics.add(40.0); + summary = statistics.summary(); + EXPECT_EQ(summary.sample_count, 3); + EXPECT_DOUBLE_EQ(summary.average, 30.0); + EXPECT_DOUBLE_EQ(summary.p95, 40.0); +} + +TEST(Throttled_Latest_Only, Rotates_Three_Frames_And_Drops_When_All_Are_Busy) { auto control = std::make_shared(); - auto policy = make_policy(control); + auto policy = make_policy(control, true); ASSERT_EQ( policy->start(50.0), aethera::Throttled_Latest_only::Start_Result::started); EXPECT_EQ(control->scheduled_interval, 20ms); - EXPECT_EQ(control->frames_created, 1); - EXPECT_EQ(control->frames_alive, 1); + EXPECT_EQ(control->frames_created, 3); + EXPECT_EQ(control->frames_alive, 3); control->fire_timer(); control->fire_timer(); - EXPECT_EQ(control->render_calls, 1); - EXPECT_EQ(control->send_calls, 0); + control->fire_timer(); + control->fire_timer(); + ASSERT_EQ(control->render_calls, 3); + ASSERT_EQ(control->rendered_frame_addresses.size(), 3); + EXPECT_NE(control->rendered_frame_addresses[0], control->rendered_frame_addresses[1]); + EXPECT_NE(control->rendered_frame_addresses[1], control->rendered_frame_addresses[2]); + EXPECT_NE(control->rendered_frame_addresses[0], control->rendered_frame_addresses[2]); control->complete_render(); control->fire_timer(); - EXPECT_EQ(control->render_calls, 1); - EXPECT_EQ(control->send_calls, 1); + EXPECT_EQ(control->render_calls, 3); control->complete_send(); - control->fire_timer(); - EXPECT_EQ(control->render_calls, 2); - control->complete_render(); - control->complete_send(); - EXPECT_TRUE(control->reused_same_frame); + ASSERT_EQ(control->render_calls, 4); + EXPECT_EQ( + control->rendered_frame_addresses[3], + control->rendered_frame_addresses[0]); + + while (!control->rendering_frames.empty()) { + control->complete_render(); + } + while (!control->sending_frames.empty()) { + control->complete_send(); + } + + policy->state.read([](const aethera::Throttled_Latest_only::State& state) { + EXPECT_EQ(state.timer_ticks, 6); + EXPECT_EQ(state.dropped_timer_ticks, 2); + EXPECT_EQ(state.completed_frames, 4); + }); bool stopped = false; EXPECT_EQ( @@ -204,23 +239,19 @@ TEST(Throttled_Latest_Only, Reuses_One_Frame_And_Drops_Busy_Timer_Ticks) { stopped = true; }), aethera::Throttled_Latest_only::Stop_Result::stopping); - EXPECT_EQ( - control->cancelled_timer_id, - Test_Timer_Service::Timer_Id); - EXPECT_FALSE(stopped); - control->complete_cancel(); EXPECT_TRUE(stopped); EXPECT_EQ(control->frames_alive, 0); } -TEST(Throttled_Latest_Only, Stop_Completes_After_Timer_And_Frame_Borrows_Return) { +TEST(Throttled_Latest_Only, Stop_Drains_All_Frame_Borrows) { auto control = std::make_shared(); auto policy = make_policy(control); ASSERT_EQ( policy->start(60.0), aethera::Throttled_Latest_only::Start_Result::started); control->fire_timer(); + control->fire_timer(); bool stopped = false; ASSERT_EQ( @@ -230,24 +261,62 @@ TEST(Throttled_Latest_Only, Stop_Completes_After_Timer_And_Frame_Borrows_Return) aethera::Throttled_Latest_only::Stop_Result::stopping); control->complete_cancel(); EXPECT_FALSE(stopped); - EXPECT_EQ(control->frames_alive, 1); + EXPECT_EQ(control->frames_alive, 3); + control->complete_render(); control->complete_render(); EXPECT_FALSE(stopped); - EXPECT_EQ(control->send_calls, 1); - + control->complete_send(); + EXPECT_FALSE(stopped); control->complete_send(); EXPECT_TRUE(stopped); EXPECT_EQ(control->frames_alive, 0); + policy->state.read([](const aethera::Throttled_Latest_only::State& state) { + EXPECT_EQ(state.timer_ticks, 0); + EXPECT_EQ(state.completed_frames, 0); + }); } -TEST(Throttled_Latest_Only, Reports_Missing_Dependencies_And_Invalid_Fps) { +TEST(Throttled_Latest_Only, Publishes_Optional_Sliding_Statistics) { + auto control = std::make_shared(); + control->complete_render_synchronously = true; + control->complete_send_synchronously = true; + auto policy = make_policy(control, true, 2); + ASSERT_EQ( + policy->start(60.0), + aethera::Throttled_Latest_only::Start_Result::started); + + control->fire_timer(); + control->fire_timer(); + control->fire_timer(); + + policy->state.read([](const aethera::Throttled_Latest_only::State& state) { + EXPECT_EQ(state.timer_ticks, 3); + EXPECT_EQ(state.dropped_timer_ticks, 0); + EXPECT_EQ(state.completed_frames, 3); + EXPECT_EQ(state.render_time_ns.sample_count, 2); + EXPECT_EQ(state.send_time_ns.sample_count, 2); + EXPECT_EQ(state.end_to_end_time_ns.sample_count, 2); + EXPECT_GE(state.render_time_ns.p95, 0.0); + EXPECT_GE(state.end_to_end_time_ns.standard_deviation, 0.0); + }); + + bool stopped = false; + ASSERT_EQ( + policy->stop([&stopped] { + stopped = true; + }), + aethera::Throttled_Latest_only::Stop_Result::stopping); + control->complete_cancel(); + EXPECT_TRUE(stopped); +} + +TEST(Throttled_Latest_Only, Reports_Invalid_Configuration) { aethera::Throttled_Latest_only::Builder missing_builder( aethera::proxy{}, aethera::proxy{}, aethera::proxy{}); auto missing_dependencies = missing_builder.build(); - EXPECT_EQ( missing_dependencies->start(60.0), aethera::Throttled_Latest_only::Start_Result::dependency_unavailable); @@ -263,9 +332,14 @@ TEST(Throttled_Latest_Only, Reports_Missing_Dependencies_And_Invalid_Fps) { EXPECT_EQ( policy->start(std::numeric_limits::denorm_min()), aethera::Throttled_Latest_only::Start_Result::interval_out_of_range); + + auto invalid_statistics = make_policy(control, true, 0); + EXPECT_EQ( + invalid_statistics->start(60.0), + aethera::Throttled_Latest_only::Start_Result::invalid_statistics_window); } -TEST(Throttled_Latest_Only, Synchronous_Completions_And_Failure_Restore_Frame) { +TEST(Throttled_Latest_Only, Synchronous_Completions_And_Failure_Restore_Slot) { auto control = std::make_shared(); control->complete_render_synchronously = true; control->throw_from_send = true; diff --git a/kernel/kernel/test/model/Model_Tests.cpp b/kernel/kernel/test/model/Model_Tests.cpp index ef7b55e..6d3217f 100644 --- a/kernel/kernel/test/model/Model_Tests.cpp +++ b/kernel/kernel/test/model/Model_Tests.cpp @@ -1,97 +1,128 @@ +#include "concurrent/base/Concurrent_List.hpp" #include "model/Model.hpp" #include -#include +#include +#include namespace { +struct Number_List_Root : std::vector {}; -struct Test_Model : aethera::Def> { - struct Prop : Prev_Prop { +struct Number_List_Tag { + using Root_Type = Number_List_Root; + template + using Value = typename Layer::Numbers; +}; + +template +struct Test_Model_Base : aethera::Model_Layer< + Self, + aethera::Root, + aethera::Storage_Registration< + aethera::Prop_Tag, + aethera::Import_Struct_With_Dirty>, + aethera::Storage_Registration> { + using Layer = aethera::Model_Layer< + Self, + aethera::Root, + aethera::Storage_Registration< + aethera::Prop_Tag, + aethera::Import_Struct_With_Dirty>, + aethera::Storage_Registration>; + using Prev_Private = typename Layer::Prev_Private; + using Prev_Builder = typename Layer::Prev_Builder; + + struct Prop : Layer::template Prev { + int base_number{}; + }; + struct Numbers : Layer::template Prev {}; + struct Private : Prev_Private {}; + struct Builder : Prev_Builder {}; +}; + +struct Test_Model : aethera::Def> { + struct Prop : Prev { int number{}; int other{}; }; + struct Numbers : Prev {}; struct Private : Prev_Private {}; }; -struct Model_Test : testing::Test { - using Storage = aethera::Import_Struct_With_Dirty; - using Member = int Test_Model::Prop::*; +static_assert(std::derived_from< + Test_Model::Prop, + Test_Model_Base::Prop>); +static_assert(std::derived_from< + Test_Model::Numbers, + Test_Model_Base::Numbers>); - static std::unique_ptr build_model() { - Test_Model::Builder builder; - builder.set_prop(&Test_Model::Prop::number, 3) - .set_prop(&Test_Model::Prop::other, 5); - return builder.build(); - } +TEST(Model, Registered_Tags_Build_Independent_Chained_Storages) { + Test_Model::Builder builder; + int struct_initializer_calls{}; + builder + .set< + aethera::Import_Struct_With_Dirty, + aethera::Prop_Tag>( + [&struct_initializer_calls](Test_Model::Prop& properties) { + ++struct_initializer_calls; + properties.base_number = 2; + }) + .set< + aethera::Import_Struct_With_Dirty, + aethera::Prop_Tag>( + &Test_Model::Prop::number, + 3) + .set< + aethera::Import_Struct_With_Dirty, + aethera::Prop_Tag>( + &Test_Model::Prop::other, + 5) + .set( + [](Test_Model::Numbers& numbers) { + numbers.assign({7, 11}); + }); + auto& building_properties = builder.ret->d->get(); + EXPECT_EQ(struct_initializer_calls, 1); + EXPECT_EQ(building_properties.internal.internal_read.base_number, 2); + EXPECT_EQ(building_properties.internal.internal_read.number, 3); + EXPECT_EQ(building_properties.internal.internal_read.other, 5); + EXPECT_EQ(building_properties.internal.external_write.base_number, 2); + EXPECT_EQ(building_properties.internal.external_write.number, 3); + EXPECT_EQ(building_properties.internal.external_write.other, 5); + Test_Model* const building_model = builder.ret.get(); + auto model = builder.build(); + EXPECT_EQ(model.get(), building_model); - Model_Test() - : model(build_model()), - whole_dirty(model->d->prop), - number_dirty(model->d->prop, &Test_Model::Prop::number), - other_dirty(model->d->prop, &Test_Model::Prop::other) {} + auto& properties = model->d->get(); + EXPECT_EQ(properties.internal.internal_read.base_number, 2); + EXPECT_EQ(properties.internal.internal_read.number, 3); + EXPECT_EQ(properties.internal.internal_read.other, 5); - std::unique_ptr model; - aethera::Dirty_Binder whole_dirty; - aethera::Property_Dirty_Binder number_dirty; - aethera::Property_Dirty_Binder other_dirty; -}; + auto& numbers = model->d->get(); + numbers.internal.advance(); + EXPECT_EQ(*numbers.internal.use(), (std::vector{7, 11})); -TEST_F(Model_Test, Builder_Establishes_Both_Faces_Without_Dirty) { - EXPECT_EQ(model->d->prop.internal.use()->number, 3); - EXPECT_EQ(model->d->prop.read(&Test_Model::Prop::number), 3); - EXPECT_EQ(model->d->prop.internal.use()->other, 5); - EXPECT_EQ(model->d->prop.read(&Test_Model::Prop::other), 5); + model->write( + [](Test_Model::Numbers& numbers) { + numbers.assign({13, 17}); + }); + numbers.internal.advance(); - model->d->prop.internal.advance(); - - EXPECT_FALSE(whole_dirty.take_dirty()); - EXPECT_FALSE(number_dirty.take_dirty()); - EXPECT_FALSE(other_dirty.take_dirty()); + EXPECT_EQ(*numbers.internal.use(), (std::vector{13, 17})); } -TEST_F(Model_Test, Member_Set_Prop_Dirties_Whole_And_That_Property_After_Advance) { - Test_Model& returned = model->set_prop(&Test_Model::Prop::number, 7); +TEST(Model, Registered_Struct_Write_Publishes_To_The_Same_Tagged_Storage) { + auto model = Test_Model::Builder{}.build(); - EXPECT_EQ(&returned, model.get()); - EXPECT_EQ(model->d->prop.internal.use()->number, 3); - EXPECT_FALSE(whole_dirty.dirty()); - EXPECT_FALSE(number_dirty.dirty()); + model->write< + aethera::Import_Struct_With_Dirty, + aethera::Prop_Tag>( + &Test_Model::Prop::number, + 13); + auto& properties = model->d->get(); + properties.internal.advance(); - model->d->prop.internal.advance(); - - EXPECT_EQ(model->d->prop.internal.use()->number, 7); - EXPECT_TRUE(whole_dirty.take_dirty()); - EXPECT_TRUE(number_dirty.take_dirty()); - EXPECT_FALSE(other_dirty.take_dirty()); + EXPECT_EQ(properties.internal.use()->number, 13); } - -TEST_F(Model_Test, Callback_Set_Prop_Dirties_Every_Property_After_Advance) { - model->set_prop([](Test_Model::Prop& prop) { - prop.number = 11; - prop.other = 13; - }); - - model->d->prop.internal.advance(); - - EXPECT_EQ(model->d->prop.internal.use()->number, 11); - EXPECT_EQ(model->d->prop.internal.use()->other, 13); - EXPECT_TRUE(whole_dirty.take_dirty()); - EXPECT_TRUE(number_dirty.take_dirty()); - EXPECT_TRUE(other_dirty.take_dirty()); -} - -TEST_F(Model_Test, Empty_Advance_Does_Not_Repeat_Dirty) { - model->set_prop(&Test_Model::Prop::other, 17); - model->d->prop.internal.advance(); - ASSERT_TRUE(whole_dirty.take_dirty()); - ASSERT_TRUE(other_dirty.take_dirty()); - - model->d->prop.internal.advance(); - - EXPECT_FALSE(whole_dirty.take_dirty()); - EXPECT_FALSE(number_dirty.take_dirty()); - EXPECT_FALSE(other_dirty.take_dirty()); -} - }