This commit is contained in:
2026-09-01 09:31:16 +08:00
parent aedef4f60b
commit f15402251d
8 changed files with 666 additions and 252 deletions
@@ -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<Prop_Tag>();
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<State>& 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<State>& 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<proxy<FP_Frame>, 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<FP_Frame> retired_frame;
std::array<proxy<FP_Frame>, 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<State>& 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<FP_Frame>& completed_frame) {
rendered(completed_frame);
frames[selected_index].frame,
[this, selected_index, &published_state](
proxy<FP_Frame>& 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<State>& published_state,
proxy<FP_Frame>& 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<std::chrono::nanoseconds>(
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<FP_Frame>& sent_frame) {
sent(sent_frame);
frames[slot_index].frame,
[this, slot_index, &published_state](proxy<FP_Frame>& 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<State>& published_state,
proxy<FP_Frame>& 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<std::chrono::nanoseconds>(
slot.send_end - slot.send_start),
std::chrono::duration_cast<std::chrono::nanoseconds>(
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<FP_Frame> retired_frame;
std::array<proxy<FP_Frame>, 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;
});
}
}
@@ -1,15 +1,15 @@
#pragma once
#include "function/frame_policy/Frame_Policy.hpp"
#include "function/frame_policy/global.hpp"
#include "model/Model.hpp"
#include <cstdint>
#include <functional>
namespace aethera {
/*
* 固定帧率、最多帧在途的策略。Scene 创建具体帧;策略持有并重复使用,
* stop completion 执行前会排空 Scene/Sink 借用并销毁帧。
* 固定帧率、最多帧在途的 latest-only 策略。Scene 创建三个具体帧;策略轮转复用,
* 三槽均忙时丢弃本次 tickstop completion 排空 Scene/Sink 借用并销毁全部帧。
*/
struct Throttled_Latest_only :
Def<Throttled_Latest_only, Root<Throttled_Latest_only>> {
Def<Throttled_Latest_only, Frame_Policy<Throttled_Latest_only>> {
enum struct Start_Result : std::uint8_t {
started,
already_running,
@@ -18,7 +18,8 @@ Def<Throttled_Latest_only, Root<Throttled_Latest_only>> {
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<Throttled_Latest_only, Root<Throttled_Latest_only>> {
completion_missing
};
using Stop_Completion = std::function<void()>;
struct Prop : Prev_Prop {};
struct Prop : Prev<Prop_Tag> {};
struct Private;
Throttled_Latest_only(proxy<Periodic_Timer_Service> timer_service,
proxy<FP_Scene> scene,
@@ -1,5 +1,7 @@
#pragma once
#include <array>
#include <chrono>
#include <mutex>
#include <optional>
@@ -16,37 +18,57 @@ struct Throttled_Latest_only::Private : Prev_Private {
rendering,
sending
};
struct Frame_Slot {
proxy<FP_Frame> 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<Periodic_Timer_Service> timer_service,
proxy<FP_Scene> scene,
proxy<FP_Sink> sink);
Start_Result start(double frames_per_second);
Start_Result start(
const Prop& properties,
Export_Struct<State>& 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<State>& published_state);
void timer_started(Timer_Id id);
void start_failed();
void cancel_failed();
void timer_cancelled();
void frame_due();
void rendered(proxy<FP_Frame>& completed_frame);
void sent(proxy<FP_Frame>& completed_frame);
void abandon_render();
void abandon_send();
void frame_due(Export_Struct<State>& published_state);
void rendered(
std::size_t slot_index,
Export_Struct<State>& published_state,
proxy<FP_Frame>& completed_frame);
void sent(
std::size_t slot_index,
Export_Struct<State>& published_state,
proxy<FP_Frame>& completed_frame);
void abandon_render(std::size_t slot_index);
void abandon_send(std::size_t slot_index);
void finish_stop_if_ready();
proxy<Periodic_Timer_Service> timer_service; /* 周期调度和异步取消的必填业务能力所有权。 */
proxy<FP_Scene> scene; /* 帧创建及渲染借用的必填业务能力所有权。 */
proxy<FP_Scene> scene; /* 帧创建及渲染借用的必填业务能力所有权。 */
proxy<FP_Sink> sink; /* 已渲染帧媒体分派的必填业务能力所有权。 */
proxy<FP_Frame> frame; /* 策略创建、复用并在停止完成前销毁的唯一帧。 */
std::array<Frame_Slot, Frame_Count> frames; /* 固定三个槽位的轮转帧池。 */
Stop_Completion stop_completion; /* 本次异步停止的唯一完成回调。 */
std::optional<Timer_Id> 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)。 */
};
}
+102 -11
View File
@@ -1,29 +1,120 @@
#pragma once
#include "../concurrent/Concurrent_Storage_with_dirty.hpp"
#include "../concurrent/storage/Tagged_Storage.hpp"
#include <memory>
#include <type_traits>
namespace aethera {
template <typename Self> struct Root {
struct Prop {};
namespace detail {
template <typename... Types>
struct Model_Type_List {};
template <typename List, typename... Registrations>
struct Append_Model_Registrations;
template <typename Tag, typename List>
struct Model_Tag_Registered;
template <typename Tag, typename Base, bool Registered>
struct Previous_Model_Value;
struct Empty_Prop {};
struct Empty_State {};
}
/* Tag 负责选择各定义层的 Prop 类型;同一个 Tag 在注册表中只允许出现一次。 */
struct Prop_Tag {
using Root_Type = detail::Empty_Prop;
template <typename Layer>
using Value = typename Layer::Prop;
};
/* Tag 负责选择各定义层的 State 类型。 */
struct State_Tag {
using Root_Type = detail::Empty_State;
template <typename Layer>
using Value = typename Layer::State;
};
/* 把一个业务 Tag 注册到接受单个值类型的 Concurrent Storage 模板。 */
template <typename Tag, template <typename...> typename Concurrent_Storage>
struct Storage_Registration {
using Tag_Type = Tag;
template <typename Value>
using Storage = Concurrent_Storage<Value>;
};
template <typename Self>
struct Root {
using Storage_Registrations = detail::Model_Type_List<>;
template <typename Tag>
using Prev = typename Tag::Root_Type;
struct Private {};
struct Builder {};
};
template <typename Self, typename Base> struct Def : Base {
/*
* 只扩展类型与存储注册链,不创建运行时 d;供多个业务定义层共同组成最终 Def。
*/
template <typename Self, typename Base, typename... Registrations>
struct Model_Layer : Base {
using Storage_Registrations = typename detail::Append_Model_Registrations<
typename Base::Storage_Registrations,
Registrations...>::Type;
template <typename Tag>
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 <typename Self, typename Base, typename... Registrations>
struct Def : Model_Layer<Self, Base, Registrations...> {
using Layer = Model_Layer<Self, Base, Registrations...>;
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 <typename... Args>
explicit Builder(Args&&... args);
std::unique_ptr<Self> build();
template <Member_Value Member, Assignable_To<Member&> T> Builder& set_prop(Member Prop::* member, T&& value);
template <
template <typename...> typename Concurrent_Storage,
typename Tag,
typename... Args>
requires requires(
Concurrent_Storage<typename Tag::template Value<Self>>& storage,
Args&&... args) {
storage.write(std::forward<Args>(args)...);
}
Builder& set(Args&&... args);
std::unique_ptr<Self> ret; /* 构建期间唯一拥有尚未发布的对象。 */
};
template <typename... Args> Self& set_prop(Args&&... args);
using Prev_Prop = Base::Prop;
using Prev_Private = Base::Private;
using Prev_Builder = Base::Builder;
template <
template <typename...> typename Concurrent_Storage,
typename Tag,
typename... Args>
requires requires(
Concurrent_Storage<typename Tag::template Value<Self>>& storage,
Args&&... args) {
storage.write(std::forward<Args>(args)...);
}
Self& write(Args&&... args);
template <typename... Args>
explicit Def(Args&&... args);
std::unique_ptr<Private> d; /* 唯一拥有本定义层的并发属性实现。 */
std::unique_ptr<Private> d; /* 唯一拥有最终 Private 和完整 Tagged Storage 集。 */
};
}
#include "Model.ipp"
+141 -25
View File
@@ -1,49 +1,165 @@
#pragma once
#include <functional>
#include <utility>
namespace aethera {
namespace aethera::detail {
template <typename Value, typename Fn>
requires Value_Writer<Fn&, Value>
void initialize_model_value(Value& target, Fn& fn) {
std::invoke(fn, target);
}
template <typename Value, typename Owner, typename Member, typename T>
void initialize_model_value(
Value& target,
Member Owner::* member,
T& value) {
target.*member = value;
}
template <typename List, typename... Registrations>
struct Append_Model_Registrations;
template <typename... Existing, typename... Registrations>
struct Append_Model_Registrations<
Model_Type_List<Existing...>,
Registrations...> {
using Type = Model_Type_List<Existing..., Registrations...>;
};
template <typename Tag, typename List>
struct Model_Tag_Registered;
template <typename Tag, typename... Registrations>
struct Model_Tag_Registered<Tag, Model_Type_List<Registrations...>> :
std::bool_constant<(
std::same_as<Tag, typename Registrations::Tag_Type> || ...)> {};
template <typename List>
struct Unique_Model_Tags;
template <>
struct Unique_Model_Tags<Model_Type_List<>> : std::true_type {};
template <typename Registration, typename... Rest>
struct Unique_Model_Tags<Model_Type_List<Registration, Rest...>> :
std::bool_constant<
(!std::same_as<
typename Registration::Tag_Type,
typename Rest::Tag_Type> && ...) &&
Unique_Model_Tags<Model_Type_List<Rest...>>::value> {};
template <typename Tag, typename Base, bool Registered>
struct Previous_Model_Value;
template <typename Tag, typename Base>
struct Previous_Model_Value<Tag, Base, false> {
using Type = typename Tag::Root_Type;
};
template <typename Tag, typename Base>
struct Previous_Model_Value<Tag, Base, true> {
using Type = typename Tag::template Value<Base>;
};
template <typename Self, typename Registration>
struct Attached_Model_Storage {
using Tag = typename Registration::Tag_Type;
using Value = typename Tag::template Value<Self>;
using Type = Tagged_Storage<
Tag,
typename Registration::template Storage<Value>>;
};
template <typename Self, typename Registrations>
struct Model_Storage_Set;
template <typename Self, typename... Registrations>
struct Model_Storage_Set<Self, Model_Type_List<Registrations...>> :
Tagged_Storage_Set<
typename Attached_Model_Storage<Self, Registrations>::Type...> {};
}
namespace aethera {
template <typename Self, typename Base, typename... Registrations>
struct Def<Self, Base, Registrations...>::Private :
Self::Private,
detail::Model_Storage_Set<Self, Storage_Registrations> {
static_assert(
detail::Unique_Model_Tags<Storage_Registrations>::value,
"each model storage tag can only be registered once");
template <typename Self, typename Base>
struct Def<Self, Base>::Private : Self::Private {
template <typename... Args>
explicit Private(Args&&... args) :
Self::Private(std::forward<Args>(args)...) {}
Import_Struct_With_Dirty<typename Self::Prop> prop;
};
template <typename Self, typename Base>
template <typename Self, typename Base, typename... Registrations>
template <typename... Args>
Def<Self, Base>::Builder::Builder(Args&&... args) :
Def<Self, Base, Registrations...>::Builder::Builder(Args&&... args) :
ret(std::make_unique<Self>(std::forward<Args>(args)...)) {}
template <typename Self, typename Base>
template <typename Self, typename Base, typename... Registrations>
template <typename... Args>
Def<Self, Base>::Def(Args&&... args) :
Def<Self, Base, Registrations...>::Def(Args&&... args) :
d(std::make_unique<Private>(std::forward<Args>(args)...)) {}
template <typename Self, typename Base>
std::unique_ptr<Self> Def<Self, Base>::Builder::build() {
template <typename Self, typename Base, typename... Registrations>
std::unique_ptr<Self> Def<Self, Base, Registrations...>::Builder::build() {
return std::move(ret);
}
template <typename Self, typename Base>
template <Member_Value Member, Assignable_To<Member&> T>
typename Def<Self, Base>::Builder& Def<Self, Base>::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<T>(value);
template <typename Self, typename Base, typename... Registrations>
template <
template <typename...> typename Concurrent_Storage,
typename Tag,
typename... Args>
requires requires(
Concurrent_Storage<typename Tag::template Value<Self>>& storage,
Args&&... args) {
storage.write(std::forward<Args>(args)...);
}
typename Def<Self, Base, Registrations...>::Builder&
Def<Self, Base, Registrations...>::Builder::set(Args&&... args) {
auto& storage = ret->d->template get<Tag>();
using Value = typename Tag::template Value<Self>;
static_assert(std::same_as<
std::remove_cvref_t<decltype(storage)>,
Concurrent_Storage<Value>>,
"the concurrent storage and tag must name one registration");
if constexpr (requires(Concurrent_Storage<Value>& candidate) {
candidate.internal.internal_read;
candidate.internal.external_write;
}) {
detail::initialize_model_value<Value>(
storage.internal.external_write,
args...);
storage.internal.internal_read = storage.internal.external_write;
} else {
storage.write(std::forward<Args>(args)...);
}
return *this;
}
template <typename Self, typename Base>
template <typename... Args>
Self& Def<Self, Base>::set_prop(Args&&... args) {
d->prop.write(std::forward<Args>(args)...);
template <typename Self, typename Base, typename... Registrations>
template <
template <typename...> typename Concurrent_Storage,
typename Tag,
typename... Args>
requires requires(
Concurrent_Storage<typename Tag::template Value<Self>>& storage,
Args&&... args) {
storage.write(std::forward<Args>(args)...);
}
Self& Def<Self, Base, Registrations...>::write(Args&&... args) {
auto& storage = d->template get<Tag>();
using Value = typename Tag::template Value<Self>;
static_assert(std::same_as<
std::remove_cvref_t<decltype(storage)>,
Concurrent_Storage<Value>>,
"the concurrent storage and tag must name one registration");
storage.write(std::forward<Args>(args)...);
return static_cast<Self&>(*this);
}
}
+5 -3
View File
@@ -2,9 +2,11 @@
| 文件 | 职责 |
|------------------|----------------------------------------------|
| `Model.hpp/.ipp` | `Root/Def/Builder` 对象属性层。 |
| `Model.hpp/.ipp` | 按 Tag 组合定义链,并在最终 `Def` 挂接注册存储。 |
| `../concurrent/` | Model 使用的并发存储机制;拥有独立设计文档。 |
`Def::Private` 拥有该定义层的 `Import_Struct_With_Dirty<Prop>`。Builder 同时初始化内部读面和外部写面,建立初始基线且不产生 dirty;运行期 `set_prop(member, value)` 发布整体及对应属性 dirty,回调形式的整对象写入发布整体及全部属性 dirty
`Model_Layer` 通过 `Storage_Registration<Tag, Concurrent_Storage>` 注册一条业务值链。每个定义层用 `Prev<Tag>` 取得前一层值类型并继承;最终 `Def::Private` 根据完整注册表只创建一次 `Tagged_Storage_Set`。同一 Tag 重复注册属于编译期错误
属性 binder 只在 `prop.internal.advance()` 之后的同一内部线程阶段查询。外部写入、`advance()` 和 binder 查询按阶段串行,不重叠,因此 dirty revision 与属性表不使用锁或原子
Builder 使用 `set<Concurrent_Storage, Tag>(...)`,最终模型使用 `write<Concurrent_Storage, Tag>(...)`。两个模板参数必须命中同一注册项,参数能力直接由对应并发存储的 `write(...)` 重载约束:Struct 可以整对象或按成员写,List 只接受列表批次写入。Builder 对 Struct 同时建立内部读面和外部写面且不产生 dirty;其他存储保留自身写入/消费语义
属性 binder 只在对应 Tag 存储的 `internal.advance()` 之后、同一内部线程阶段查询。外部写入、`advance()` 和 binder 查询按阶段串行且不重叠,因此 dirty revision 与属性表不使用锁或原子。
@@ -1,18 +1,25 @@
#include "function/frame_policy/Throttled_Latest_Only.hpp"
#include "statistics/Sliding_Statistics.hpp"
#include <gtest/gtest.h>
#include <chrono>
#include <deque>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
namespace {
using namespace std::chrono_literals;
struct Pending_Frame {
std::reference_wrapper<aethera::proxy<aethera::FP_Frame>> 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<std::reference_wrapper<aethera::proxy<aethera::FP_Frame>>>
rendering_frame; /* Scene 当前借用的策略帧。 */
std::optional<std::reference_wrapper<aethera::proxy<aethera::FP_Frame>>>
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<aethera::FP_Frame>* first_frame{}; /* 可空、非拥有的首次帧地址。 */
std::deque<Pending_Frame> rendering_frames; /* Scene 尚未归还的帧及其回调。 */
std::deque<Pending_Frame> sending_frames; /* Sink 尚未归还的帧及其回调。 */
std::vector<aethera::proxy<aethera::FP_Frame>*>
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<aethera::FP_Frame>& 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<Frame_Policy_Test_Control> control; /* Scene 测试行为的共享控制数据。 */
@@ -140,9 +135,6 @@ struct Test_Sink {
void send(
aethera::proxy<aethera::FP_Frame>& 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<Frame_Policy_Test_Control> control; /* Sink 测试行为的共享控制数据。 */
};
std::unique_ptr<aethera::Throttled_Latest_only> make_policy(
const std::shared_ptr<Frame_Policy_Test_Control>& control) {
const std::shared_ptr<Frame_Policy_Test_Control>& 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<aethera::Throttled_Latest_only> make_policy(
auto sink = pro::make_proxy<aethera::FP_Sink, Test_Sink>(control);
aethera::Throttled_Latest_only::Builder builder(
std::move(timer_service), std::move(scene), std::move(sink));
builder
.set<aethera::Import_Struct_With_Dirty, aethera::Prop_Tag>(
&aethera::Throttled_Latest_only::Prop::statistics_enabled,
statistics_enabled)
.set<aethera::Import_Struct_With_Dirty, aethera::Prop_Tag>(
&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<Frame_Policy_Test_Control>();
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<Frame_Policy_Test_Control>();
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<Frame_Policy_Test_Control>();
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::Periodic_Timer_Service>{},
aethera::proxy<aethera::FP_Scene>{},
aethera::proxy<aethera::FP_Sink>{});
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<double>::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<Frame_Policy_Test_Control>();
control->complete_render_synchronously = true;
control->throw_from_send = true;
+104 -73
View File
@@ -1,97 +1,128 @@
#include "concurrent/base/Concurrent_List.hpp"
#include "model/Model.hpp"
#include <gtest/gtest.h>
#include <memory>
#include <concepts>
#include <vector>
namespace {
struct Number_List_Root : std::vector<int> {};
struct Test_Model : aethera::Def<Test_Model, aethera::Root<Test_Model>> {
struct Prop : Prev_Prop {
struct Number_List_Tag {
using Root_Type = Number_List_Root;
template <typename Layer>
using Value = typename Layer::Numbers;
};
template <typename Self>
struct Test_Model_Base : aethera::Model_Layer<
Self,
aethera::Root<Self>,
aethera::Storage_Registration<
aethera::Prop_Tag,
aethera::Import_Struct_With_Dirty>,
aethera::Storage_Registration<Number_List_Tag, aethera::Import_List>> {
using Layer = aethera::Model_Layer<
Self,
aethera::Root<Self>,
aethera::Storage_Registration<
aethera::Prop_Tag,
aethera::Import_Struct_With_Dirty>,
aethera::Storage_Registration<Number_List_Tag, aethera::Import_List>>;
using Prev_Private = typename Layer::Prev_Private;
using Prev_Builder = typename Layer::Prev_Builder;
struct Prop : Layer::template Prev<aethera::Prop_Tag> {
int base_number{};
};
struct Numbers : Layer::template Prev<Number_List_Tag> {};
struct Private : Prev_Private {};
struct Builder : Prev_Builder {};
};
struct Test_Model : aethera::Def<Test_Model, Test_Model_Base<Test_Model>> {
struct Prop : Prev<aethera::Prop_Tag> {
int number{};
int other{};
};
struct Numbers : Prev<Number_List_Tag> {};
struct Private : Prev_Private {};
};
struct Model_Test : testing::Test {
using Storage = aethera::Import_Struct_With_Dirty<Test_Model::Prop>;
using Member = int Test_Model::Prop::*;
static_assert(std::derived_from<
Test_Model::Prop,
Test_Model_Base<Test_Model>::Prop>);
static_assert(std::derived_from<
Test_Model::Numbers,
Test_Model_Base<Test_Model>::Numbers>);
static std::unique_ptr<Test_Model> 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<aethera::Import_List, Number_List_Tag>(
[](Test_Model::Numbers& numbers) {
numbers.assign({7, 11});
});
auto& building_properties = builder.ret->d->get<aethera::Prop_Tag>();
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<aethera::Prop_Tag>();
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<Test_Model> model;
aethera::Dirty_Binder<Storage> whole_dirty;
aethera::Property_Dirty_Binder<Storage, Member> number_dirty;
aethera::Property_Dirty_Binder<Storage, Member> other_dirty;
};
auto& numbers = model->d->get<Number_List_Tag>();
numbers.internal.advance();
EXPECT_EQ(*numbers.internal.use(), (std::vector<int>{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<aethera::Import_List, Number_List_Tag>(
[](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<int>{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<aethera::Prop_Tag>();
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());
}
}