更新
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#include "Frame_Policy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace aethera {
|
||||
namespace {
|
||||
std::optional<double> capacity_fps(
|
||||
const Sliding_Statistics& statistics) noexcept {
|
||||
const double average = statistics.summary().average;
|
||||
if (!std::isfinite(average) || average <= 0.0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return 1'000'000'000.0 / average;
|
||||
}
|
||||
}
|
||||
|
||||
Frame_Policy::Private::Active_Statistics::Active_Statistics(
|
||||
std::size_t window_size) :
|
||||
render(window_size),
|
||||
send(window_size),
|
||||
end_to_end(window_size) {}
|
||||
|
||||
bool Frame_Policy::Private::prepare_run(
|
||||
const Prop& properties,
|
||||
Export_Struct<State>& published_state) {
|
||||
const bool timing_required =
|
||||
properties.statistics_enabled ||
|
||||
properties.render_rate_limit_enabled ||
|
||||
properties.send_rate_limit_enabled;
|
||||
if (timing_required && properties.statistics_window_size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
statistics.reset();
|
||||
user_rate_limit = properties.user_frames_per_second;
|
||||
render_rate_limit_enabled = properties.render_rate_limit_enabled;
|
||||
send_rate_limit_enabled = properties.send_rate_limit_enabled;
|
||||
statistics_enabled = properties.statistics_enabled;
|
||||
timer_ticks = 0;
|
||||
dropped_timer_ticks = 0;
|
||||
completed_frames = 0;
|
||||
if (timing_required) {
|
||||
statistics.emplace(properties.statistics_window_size);
|
||||
}
|
||||
publish(published_state);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Frame_Policy::Private::record_timer_tick(
|
||||
bool dropped,
|
||||
Export_Struct<State>& published_state) {
|
||||
++timer_ticks;
|
||||
if (dropped) {
|
||||
++dropped_timer_ticks;
|
||||
}
|
||||
publish(published_state);
|
||||
}
|
||||
|
||||
void Frame_Policy::Private::record_render(
|
||||
std::chrono::nanoseconds duration,
|
||||
Export_Struct<State>& published_state) {
|
||||
if (statistics) {
|
||||
statistics->render.add(static_cast<double>(duration.count()));
|
||||
}
|
||||
publish(published_state);
|
||||
}
|
||||
|
||||
void Frame_Policy::Private::record_send(
|
||||
std::chrono::nanoseconds send_duration,
|
||||
std::chrono::nanoseconds end_to_end_duration,
|
||||
Export_Struct<State>& published_state) {
|
||||
if (statistics) {
|
||||
statistics->send.add(static_cast<double>(send_duration.count()));
|
||||
statistics->end_to_end.add(
|
||||
static_cast<double>(end_to_end_duration.count()));
|
||||
}
|
||||
++completed_frames;
|
||||
publish(published_state);
|
||||
}
|
||||
|
||||
double Frame_Policy::Private::effective_frames_per_second() const noexcept {
|
||||
double effective = user_rate_limit.value_or(
|
||||
Default_Frames_Per_Second);
|
||||
bool has_limit = user_rate_limit.has_value();
|
||||
if (statistics && render_rate_limit_enabled) {
|
||||
if (const auto capacity = capacity_fps(statistics->render)) {
|
||||
effective = has_limit ? std::min(effective, *capacity) : *capacity;
|
||||
has_limit = true;
|
||||
}
|
||||
}
|
||||
if (statistics && send_rate_limit_enabled) {
|
||||
if (const auto capacity = capacity_fps(statistics->send)) {
|
||||
effective = has_limit ? std::min(effective, *capacity) : *capacity;
|
||||
has_limit = true;
|
||||
}
|
||||
}
|
||||
return has_limit ? effective : Default_Frames_Per_Second;
|
||||
}
|
||||
|
||||
void Frame_Policy::Private::publish(
|
||||
Export_Struct<State>& published_state) {
|
||||
State& output = *published_state.internal.use();
|
||||
output.timer_ticks = timer_ticks;
|
||||
output.dropped_timer_ticks = dropped_timer_ticks;
|
||||
output.completed_frames = completed_frames;
|
||||
output.render_capacity_fps =
|
||||
statistics && render_rate_limit_enabled
|
||||
? capacity_fps(statistics->render)
|
||||
: std::nullopt;
|
||||
output.send_capacity_fps =
|
||||
statistics && send_rate_limit_enabled
|
||||
? capacity_fps(statistics->send)
|
||||
: std::nullopt;
|
||||
output.effective_frames_per_second = effective_frames_per_second();
|
||||
output.render_time_ns = statistics && statistics_enabled
|
||||
? statistics->render.summary()
|
||||
: Statistics_Summary{};
|
||||
output.send_time_ns = statistics && statistics_enabled
|
||||
? statistics->send.summary()
|
||||
: Statistics_Summary{};
|
||||
output.end_to_end_time_ns = statistics && statistics_enabled
|
||||
? statistics->end_to_end.summary()
|
||||
: Statistics_Summary{};
|
||||
published_state.internal.advance();
|
||||
}
|
||||
}
|
||||
@@ -10,65 +10,33 @@
|
||||
#include <optional>
|
||||
|
||||
namespace aethera {
|
||||
/* 所有帧策略共享的统计配置、公开状态和统计实现,不拥有具体调度状态。 */
|
||||
template <typename Self>
|
||||
/* 所有帧策略共享的自适应限速配置、公开状态和统计实现,不拥有具体调度状态。 */
|
||||
struct Frame_Policy : Model_Layer<
|
||||
Self,
|
||||
Root<Self>,
|
||||
Storage_Registration<Prop_Tag, Import_Struct_With_Dirty>> {
|
||||
using Layer = Model_Layer<
|
||||
Self,
|
||||
Root<Self>,
|
||||
Storage_Registration<Prop_Tag, Import_Struct_With_Dirty>>;
|
||||
using Prev_Private = typename Layer::Prev_Private;
|
||||
using Prev_Builder = typename Layer::Prev_Builder;
|
||||
Root,
|
||||
Storage_Registration<Prop_Tag, Import_Struct_With_Dirty>,
|
||||
Storage_Registration<State_Tag, Export_Struct>> {
|
||||
static constexpr double Default_Frames_Per_Second{30.0};
|
||||
|
||||
struct Prop : Layer::template Prev<Prop_Tag> {
|
||||
bool statistics_enabled{}; /* true 时采集并发布滑动统计。 */
|
||||
std::size_t statistics_window_size{120}; /* 启用统计时的样本窗口容量。 */
|
||||
struct Prop : Prev<Prop_Tag> {
|
||||
std::optional<double> user_frames_per_second; /* 有值时作为用户帧率上限;无值时不参与限速。 */
|
||||
bool render_rate_limit_enabled{}; /* true 时用渲染平均耗时推导理论生成上限。 */
|
||||
bool send_rate_limit_enabled{}; /* true 时用发送平均耗时推导理论发送上限。 */
|
||||
bool statistics_enabled{}; /* true 时对外发布完整滑动统计。 */
|
||||
std::size_t statistics_window_size{120}; /* 统计或理论限速启用时的样本窗口容量。 */
|
||||
};
|
||||
struct State {
|
||||
std::uint64_t timer_ticks{}; /* 已处理的周期到期次数。 */
|
||||
std::uint64_t dropped_timer_ticks{}; /* 到期时三帧均忙而丢弃的次数。 */
|
||||
std::uint64_t completed_frames{}; /* 已完成媒体发送的帧数。 */
|
||||
Statistics_Summary render_time_ns; /* 渲染耗时滑动统计,单位纳秒。 */
|
||||
Statistics_Summary send_time_ns; /* 发送耗时滑动统计,单位纳秒。 */
|
||||
Statistics_Summary end_to_end_time_ns; /* 渲染开始至发送完成耗时统计,单位纳秒。 */
|
||||
};
|
||||
struct Private : Prev_Private {
|
||||
struct Active_Statistics {
|
||||
explicit Active_Statistics(std::size_t window_size);
|
||||
|
||||
Sliding_Statistics render; /* 渲染耗时窗口。 */
|
||||
Sliding_Statistics send; /* 发送耗时窗口。 */
|
||||
Sliding_Statistics end_to_end; /* 渲染开始至发送结束耗时窗口。 */
|
||||
};
|
||||
|
||||
bool prepare_statistics(
|
||||
const Prop& properties,
|
||||
Export_Struct<State>& published_state);
|
||||
void record_timer_tick(
|
||||
bool dropped,
|
||||
Export_Struct<State>& published_state);
|
||||
void record_render(
|
||||
std::chrono::nanoseconds duration,
|
||||
Export_Struct<State>& published_state);
|
||||
void record_send(
|
||||
std::chrono::nanoseconds send_duration,
|
||||
std::chrono::nanoseconds end_to_end_duration,
|
||||
Export_Struct<State>& published_state);
|
||||
|
||||
private:
|
||||
void publish(Export_Struct<State>& published_state);
|
||||
|
||||
std::optional<Active_Statistics> statistics; /* 存在即表示本次运行启用了统计。 */
|
||||
std::uint64_t timer_ticks{}; /* 本次运行累计的到期次数。 */
|
||||
std::uint64_t dropped_timer_ticks{}; /* 本次运行累计的忙碌丢弃次数。 */
|
||||
std::uint64_t completed_frames{}; /* 本次运行累计的完成帧数。 */
|
||||
struct State : Prev<State_Tag> {
|
||||
std::uint64_t timer_ticks{}; /* 已处理的调度到期次数。 */
|
||||
std::uint64_t dropped_timer_ticks{}; /* 到期时三帧均忙而丢弃的次数。 */
|
||||
std::uint64_t completed_frames{}; /* 已完成媒体发送的帧数。 */
|
||||
std::optional<double> render_capacity_fps; /* 有渲染样本时推导出的理论最大生成帧率。 */
|
||||
std::optional<double> send_capacity_fps; /* 有发送样本时推导出的理论最大发送帧率。 */
|
||||
double effective_frames_per_second{Default_Frames_Per_Second}; /* 当前有效限速来源的最小值。 */
|
||||
Statistics_Summary render_time_ns; /* 渲染耗时滑动统计,单位纳秒。 */
|
||||
Statistics_Summary send_time_ns; /* 发送耗时滑动统计,单位纳秒。 */
|
||||
Statistics_Summary end_to_end_time_ns; /* 渲染开始至发送完成耗时统计,单位纳秒。 */
|
||||
};
|
||||
struct Private;
|
||||
struct Builder : Prev_Builder {};
|
||||
|
||||
Export_Struct<State> state; /* 由策略互斥串行发布的帧策略运行统计。 */
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
namespace aethera {
|
||||
template <typename Self>
|
||||
Frame_Policy<Self>::Private::Active_Statistics::Active_Statistics(
|
||||
std::size_t window_size) :
|
||||
render(window_size),
|
||||
send(window_size),
|
||||
end_to_end(window_size) {}
|
||||
struct Frame_Policy::Private : Prev_Private {
|
||||
struct Active_Statistics {
|
||||
explicit Active_Statistics(std::size_t window_size);
|
||||
|
||||
template <typename Self>
|
||||
bool Frame_Policy<Self>::Private::prepare_statistics(
|
||||
const Prop& properties,
|
||||
Export_Struct<State>& published_state) {
|
||||
if (properties.statistics_enabled && properties.statistics_window_size == 0) {
|
||||
return false;
|
||||
}
|
||||
Sliding_Statistics render; /* 渲染耗时窗口。 */
|
||||
Sliding_Statistics send; /* 发送耗时窗口。 */
|
||||
Sliding_Statistics end_to_end; /* 渲染开始至发送结束耗时窗口。 */
|
||||
};
|
||||
|
||||
statistics.reset();
|
||||
timer_ticks = 0;
|
||||
dropped_timer_ticks = 0;
|
||||
completed_frames = 0;
|
||||
if (properties.statistics_enabled) {
|
||||
statistics.emplace(properties.statistics_window_size);
|
||||
}
|
||||
publish(published_state);
|
||||
return true;
|
||||
}
|
||||
bool prepare_run(
|
||||
const Prop& properties,
|
||||
Export_Struct<State>& published_state);
|
||||
void record_timer_tick(
|
||||
bool dropped,
|
||||
Export_Struct<State>& published_state);
|
||||
void record_render(
|
||||
std::chrono::nanoseconds duration,
|
||||
Export_Struct<State>& published_state);
|
||||
void record_send(
|
||||
std::chrono::nanoseconds send_duration,
|
||||
std::chrono::nanoseconds end_to_end_duration,
|
||||
Export_Struct<State>& published_state);
|
||||
[[nodiscard]] double effective_frames_per_second() const noexcept;
|
||||
|
||||
template <typename Self>
|
||||
void Frame_Policy<Self>::Private::record_timer_tick(
|
||||
bool dropped,
|
||||
Export_Struct<State>& published_state) {
|
||||
if (!statistics) {
|
||||
return;
|
||||
}
|
||||
++timer_ticks;
|
||||
if (dropped) {
|
||||
++dropped_timer_ticks;
|
||||
}
|
||||
publish(published_state);
|
||||
}
|
||||
private:
|
||||
void publish(Export_Struct<State>& published_state);
|
||||
|
||||
template <typename Self>
|
||||
void Frame_Policy<Self>::Private::record_render(
|
||||
std::chrono::nanoseconds duration,
|
||||
Export_Struct<State>& published_state) {
|
||||
if (!statistics) {
|
||||
return;
|
||||
}
|
||||
statistics->render.add(static_cast<double>(duration.count()));
|
||||
publish(published_state);
|
||||
}
|
||||
|
||||
template <typename Self>
|
||||
void Frame_Policy<Self>::Private::record_send(
|
||||
std::chrono::nanoseconds send_duration,
|
||||
std::chrono::nanoseconds end_to_end_duration,
|
||||
Export_Struct<State>& published_state) {
|
||||
if (!statistics) {
|
||||
return;
|
||||
}
|
||||
statistics->send.add(static_cast<double>(send_duration.count()));
|
||||
statistics->end_to_end.add(
|
||||
static_cast<double>(end_to_end_duration.count()));
|
||||
++completed_frames;
|
||||
publish(published_state);
|
||||
}
|
||||
|
||||
template <typename Self>
|
||||
void Frame_Policy<Self>::Private::publish(
|
||||
Export_Struct<State>& published_state) {
|
||||
State& output = *published_state.internal.use();
|
||||
output.timer_ticks = timer_ticks;
|
||||
output.dropped_timer_ticks = dropped_timer_ticks;
|
||||
output.completed_frames = completed_frames;
|
||||
output.render_time_ns = statistics
|
||||
? statistics->render.summary()
|
||||
: Statistics_Summary{};
|
||||
output.send_time_ns = statistics
|
||||
? statistics->send.summary()
|
||||
: Statistics_Summary{};
|
||||
output.end_to_end_time_ns = statistics
|
||||
? statistics->end_to_end.summary()
|
||||
: Statistics_Summary{};
|
||||
published_state.internal.advance();
|
||||
}
|
||||
std::optional<Active_Statistics> statistics; /* 存在即表示本次运行需要耗时窗口。 */
|
||||
std::optional<double> user_rate_limit; /* 当前运行采用的用户帧率上限。 */
|
||||
bool render_rate_limit_enabled{}; /* 当前运行是否采用渲染理论上限。 */
|
||||
bool send_rate_limit_enabled{}; /* 当前运行是否采用发送理论上限。 */
|
||||
bool statistics_enabled{}; /* 当前运行是否向 State 发布完整统计。 */
|
||||
std::uint64_t timer_ticks{}; /* 本次运行累计的到期次数。 */
|
||||
std::uint64_t dropped_timer_ticks{}; /* 本次运行累计的忙碌丢弃次数。 */
|
||||
std::uint64_t completed_frames{}; /* 本次运行累计的完成帧数。 */
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ std::expected<std::chrono::nanoseconds, Throttled_Latest_only::Start_Result>
|
||||
frame_interval(double frames_per_second) noexcept {
|
||||
using Start_Result = Throttled_Latest_only::Start_Result;
|
||||
if (!std::isfinite(frames_per_second) || frames_per_second <= 0.0) {
|
||||
return std::unexpected(Start_Result::invalid_frames_per_second);
|
||||
return std::unexpected(Start_Result::invalid_user_frame_rate);
|
||||
}
|
||||
|
||||
constexpr long double Nanoseconds_Per_Second = 1'000'000'000.0L;
|
||||
@@ -51,10 +51,17 @@ frame_interval(double frames_per_second) noexcept {
|
||||
static_cast<std::chrono::nanoseconds::rep>(
|
||||
std::max(1.0L, std::ceil(interval)))};
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds positive_duration(
|
||||
std::chrono::steady_clock::duration duration) noexcept {
|
||||
return std::max(
|
||||
std::chrono::nanoseconds{1},
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(duration));
|
||||
}
|
||||
}
|
||||
|
||||
Throttled_Latest_only::Throttled_Latest_only(
|
||||
proxy<Periodic_Timer_Service> timer_service,
|
||||
proxy<Timer_Service> timer_service,
|
||||
proxy<FP_Scene> scene,
|
||||
proxy<FP_Sink> sink) :
|
||||
Def(std::move(timer_service), std::move(scene), std::move(sink)) {}
|
||||
@@ -66,11 +73,11 @@ Throttled_Latest_only::~Throttled_Latest_only() noexcept {
|
||||
}
|
||||
|
||||
Throttled_Latest_only::Start_Result
|
||||
Throttled_Latest_only::start(double frames_per_second) {
|
||||
Throttled_Latest_only::start() {
|
||||
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);
|
||||
return d->start(properties, d->get<State_Tag>());
|
||||
}
|
||||
|
||||
Throttled_Latest_only::Stop_Result
|
||||
@@ -79,7 +86,7 @@ Throttled_Latest_only::stop(Stop_Completion completion) {
|
||||
}
|
||||
|
||||
Throttled_Latest_only::Private::Private(
|
||||
proxy<Periodic_Timer_Service> timer_service,
|
||||
proxy<Timer_Service> timer_service,
|
||||
proxy<FP_Scene> scene,
|
||||
proxy<FP_Sink> sink) :
|
||||
timer_service(std::move(timer_service)),
|
||||
@@ -89,9 +96,10 @@ Throttled_Latest_only::Private::Private(
|
||||
Throttled_Latest_only::Start_Result
|
||||
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);
|
||||
Export_Struct<State>& published_state) {
|
||||
const auto interval = frame_interval(
|
||||
properties.user_frames_per_second.value_or(
|
||||
Frame_Policy::Default_Frames_Per_Second));
|
||||
if (!interval) {
|
||||
return interval.error();
|
||||
}
|
||||
@@ -177,7 +185,7 @@ Throttled_Latest_only::Private::prepare_start(
|
||||
if (!timer_service || !scene || !sink) {
|
||||
return Start_Result::dependency_unavailable;
|
||||
}
|
||||
if (!prepare_statistics(properties, published_state)) {
|
||||
if (!prepare_run(properties, published_state)) {
|
||||
return Start_Result::invalid_statistics_window;
|
||||
}
|
||||
phase = Phase::starting;
|
||||
@@ -282,13 +290,17 @@ void Throttled_Latest_only::Private::frame_due(
|
||||
}
|
||||
}
|
||||
record_timer_tick(selected_index == Frame_Count, published_state);
|
||||
if (selected_index == Frame_Count) {
|
||||
return;
|
||||
if (selected_index != Frame_Count) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (selected_index == Frame_Count) {
|
||||
reschedule_next_frame();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -301,8 +313,29 @@ void Throttled_Latest_only::Private::frame_due(
|
||||
}
|
||||
catch (...) {
|
||||
abandon_render(selected_index);
|
||||
reschedule_next_frame();
|
||||
throw;
|
||||
}
|
||||
reschedule_next_frame();
|
||||
}
|
||||
|
||||
void Throttled_Latest_only::Private::reschedule_next_frame() {
|
||||
Timer_Id scheduled_timer_id{};
|
||||
std::chrono::nanoseconds interval{};
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (phase != Phase::running || !timer_id) {
|
||||
return;
|
||||
}
|
||||
scheduled_timer_id = *timer_id;
|
||||
const auto calculated = frame_interval(
|
||||
effective_frames_per_second());
|
||||
if (!calculated) {
|
||||
std::terminate();
|
||||
}
|
||||
interval = *calculated;
|
||||
}
|
||||
timer_service->reschedule(scheduled_timer_id, interval);
|
||||
}
|
||||
|
||||
void Throttled_Latest_only::Private::rendered(
|
||||
@@ -321,8 +354,7 @@ void Throttled_Latest_only::Private::rendered(
|
||||
}
|
||||
slot.render_end = std::chrono::steady_clock::now();
|
||||
record_render(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
slot.render_end - slot.render_start),
|
||||
positive_duration(slot.render_end - slot.render_start),
|
||||
published_state);
|
||||
slot.phase = Frame_Phase::sending;
|
||||
slot.send_start = std::chrono::steady_clock::now();
|
||||
@@ -357,10 +389,8 @@ void Throttled_Latest_only::Private::sent(
|
||||
}
|
||||
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),
|
||||
positive_duration(slot.send_end - slot.send_start),
|
||||
positive_duration(slot.send_end - slot.render_start),
|
||||
published_state);
|
||||
slot.phase = Frame_Phase::idle;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
#include <functional>
|
||||
namespace aethera {
|
||||
/*
|
||||
* 固定帧率、最多三帧在途的 latest-only 策略。Scene 创建三个具体帧;策略轮转复用,
|
||||
* 自适应限速、最多三帧在途的 latest-only 策略。Scene 创建三个具体帧;策略轮转复用,
|
||||
* 三槽均忙时丢弃本次 tick;stop completion 前排空 Scene/Sink 借用并销毁全部帧。
|
||||
*/
|
||||
struct Throttled_Latest_only :
|
||||
Def<Throttled_Latest_only, Frame_Policy<Throttled_Latest_only>> {
|
||||
Def<Throttled_Latest_only, Frame_Policy> {
|
||||
enum struct Start_Result : std::uint8_t {
|
||||
started,
|
||||
already_running,
|
||||
@@ -17,7 +17,7 @@ Def<Throttled_Latest_only, Frame_Policy<Throttled_Latest_only>> {
|
||||
stop_in_progress,
|
||||
dependency_unavailable,
|
||||
frame_unavailable,
|
||||
invalid_frames_per_second,
|
||||
invalid_user_frame_rate,
|
||||
interval_out_of_range,
|
||||
invalid_statistics_window
|
||||
};
|
||||
@@ -31,11 +31,11 @@ Def<Throttled_Latest_only, Frame_Policy<Throttled_Latest_only>> {
|
||||
using Stop_Completion = std::function<void()>;
|
||||
struct Prop : Prev<Prop_Tag> {};
|
||||
struct Private;
|
||||
Throttled_Latest_only(proxy<Periodic_Timer_Service> timer_service,
|
||||
Throttled_Latest_only(proxy<Timer_Service> timer_service,
|
||||
proxy<FP_Scene> scene,
|
||||
proxy<FP_Sink> sink);
|
||||
~Throttled_Latest_only() noexcept;
|
||||
Start_Result start(double frames_per_second);
|
||||
Start_Result start();
|
||||
Stop_Result stop(Stop_Completion completion);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,14 +29,13 @@ struct Throttled_Latest_only::Private : Prev_Private {
|
||||
|
||||
static constexpr std::size_t Frame_Count{3};
|
||||
|
||||
Private(proxy<Periodic_Timer_Service> timer_service,
|
||||
Private(proxy<Timer_Service> timer_service,
|
||||
proxy<FP_Scene> scene,
|
||||
proxy<FP_Sink> sink);
|
||||
|
||||
Start_Result start(
|
||||
const Prop& properties,
|
||||
Export_Struct<State>& published_state,
|
||||
double frames_per_second);
|
||||
Export_Struct<State>& published_state);
|
||||
Stop_Result stop(Stop_Completion completion);
|
||||
[[nodiscard]] bool destructible() const noexcept;
|
||||
|
||||
@@ -49,6 +48,7 @@ private:
|
||||
void cancel_failed();
|
||||
void timer_cancelled();
|
||||
void frame_due(Export_Struct<State>& published_state);
|
||||
void reschedule_next_frame();
|
||||
void rendered(
|
||||
std::size_t slot_index,
|
||||
Export_Struct<State>& published_state,
|
||||
@@ -61,7 +61,7 @@ private:
|
||||
void abandon_send(std::size_t slot_index);
|
||||
void finish_stop_if_ready();
|
||||
|
||||
proxy<Periodic_Timer_Service> timer_service; /* 周期调度和异步取消的必填业务能力所有权。 */
|
||||
proxy<Timer_Service> timer_service; /* 动态重设期限和异步取消的必填业务能力所有权。 */
|
||||
proxy<FP_Scene> scene; /* 帧池创建及渲染借用的必填业务能力所有权。 */
|
||||
proxy<FP_Sink> sink; /* 已渲染帧媒体分派的必填业务能力所有权。 */
|
||||
std::array<Frame_Slot, Frame_Count> frames; /* 固定三个槽位的轮转帧池。 */
|
||||
|
||||
@@ -1,120 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include "../concurrent/Concurrent_Storage_with_dirty.hpp"
|
||||
#include "../concurrent/storage/Tagged_Storage.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
namespace aethera {
|
||||
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;
|
||||
|
||||
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;
|
||||
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;
|
||||
template <typename Layer> using Value = typename Layer::State;
|
||||
};
|
||||
|
||||
/* 把一个业务 Tag 注册到接受单个值类型的 Concurrent Storage 模板。 */
|
||||
template <typename Tag, template <typename...> typename Concurrent_Storage>
|
||||
struct Storage_Registration {
|
||||
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 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;
|
||||
|
||||
template <typename Tag> using Prev = typename Tag::Root_Type;
|
||||
struct Private {};
|
||||
struct Builder {};
|
||||
};
|
||||
|
||||
/*
|
||||
* 只扩展类型与存储注册链,不创建运行时 d;供多个业务定义层共同组成最终 Def。
|
||||
*/
|
||||
template <typename Self, typename Base, typename... Registrations>
|
||||
struct Model_Layer : Base {
|
||||
template <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<
|
||||
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...>;
|
||||
template <typename Self, typename Base, typename... Registrations> struct Def : Model_Layer<Base, Registrations...> {
|
||||
using Layer = Model_Layer<Base, Registrations...>;
|
||||
using Storage_Registrations = typename Layer::Storage_Registrations;
|
||||
using Prev_Private = typename Base::Private;
|
||||
|
||||
struct Private;
|
||||
struct Builder : Base::Builder {
|
||||
template <typename... Args>
|
||||
explicit Builder(Args&&... args);
|
||||
template <typename... Args> explicit Builder(Args&&... args);
|
||||
std::unique_ptr<Self> build();
|
||||
template <
|
||||
template <typename...> typename Concurrent_Storage,
|
||||
typename Tag,
|
||||
typename... Args>
|
||||
requires requires(
|
||||
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 <
|
||||
template <typename...> typename Concurrent_Storage,
|
||||
typename Tag,
|
||||
typename... Args>
|
||||
requires requires(
|
||||
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);
|
||||
|
||||
template <
|
||||
template <typename...> typename Concurrent_Storage,
|
||||
typename Tag,
|
||||
typename... Args> requires requires(
|
||||
const Concurrent_Storage<typename Tag::template Value<Self>>& storage,
|
||||
Args&&... args) {
|
||||
storage.read(std::forward<Args>(args)...);
|
||||
}
|
||||
const Self& read(Args&&... args) const;
|
||||
template <typename... Args> explicit Def(Args&&... args);
|
||||
std::unique_ptr<Private> d; /* 唯一拥有最终 Private 和完整 Tagged Storage 集。 */
|
||||
};
|
||||
}
|
||||
|
||||
#include "Model.ipp"
|
||||
|
||||
@@ -162,4 +162,25 @@ Self& Def<Self, Base, Registrations...>::write(Args&&... args) {
|
||||
storage.write(std::forward<Args>(args)...);
|
||||
return static_cast<Self&>(*this);
|
||||
}
|
||||
|
||||
template <typename Self, typename Base, typename... Registrations>
|
||||
template <
|
||||
template <typename...> typename Concurrent_Storage,
|
||||
typename Tag,
|
||||
typename... Args>
|
||||
requires requires(
|
||||
const Concurrent_Storage<typename Tag::template Value<Self>>& storage,
|
||||
Args&&... args) {
|
||||
storage.read(std::forward<Args>(args)...);
|
||||
}
|
||||
const Self& Def<Self, Base, Registrations...>::read(Args&&... args) const {
|
||||
const 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.read(std::forward<Args>(args)...);
|
||||
return static_cast<const Self&>(*this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
|
||||
`Model_Layer` 通过 `Storage_Registration<Tag, Concurrent_Storage>` 注册一条业务值链。每个定义层用 `Prev<Tag>` 取得前一层值类型并继承;最终 `Def::Private` 根据完整注册表只创建一次 `Tagged_Storage_Set`。同一 Tag 重复注册属于编译期错误。
|
||||
|
||||
Builder 使用 `set<Concurrent_Storage, Tag>(...)`,最终模型使用 `write<Concurrent_Storage, Tag>(...)`。两个模板参数必须命中同一注册项,参数能力直接由对应并发存储的 `write(...)` 重载约束:Struct 可以整对象或按成员写,List 只接受列表批次写入。Builder 对 Struct 同时建立内部读面和外部写面且不产生 dirty;其他存储保留自身写入/消费语义。
|
||||
Builder 使用 `set<Concurrent_Storage, Tag>(...)`,最终模型使用 `write<Concurrent_Storage, Tag>(...)` 和 `read<Concurrent_Storage, Tag>(...)`。两个模板参数必须命中同一注册项,参数能力直接由对应并发存储的 `write/read(...)` 重载约束:Struct 可以整对象或按成员读写,List 保留自身批次语义。Builder 对 Struct 同时建立内部读面和外部写面且不产生 dirty;其他存储保留自身写入/消费语义。
|
||||
|
||||
属性 binder 只在对应 Tag 存储的 `internal.advance()` 之后、同一内部线程阶段查询。外部写入、`advance()` 和 binder 查询按阶段串行且不重叠,因此 dirty revision 与属性表不使用锁或原子。
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -53,7 +55,8 @@ struct Frame_Policy_Test_Control {
|
||||
rendered_frame_addresses; /* 可空、非拥有;记录每次渲染采用的槽位地址。 */
|
||||
aethera::Timer_Callback timer_callback; /* 周期服务持有的到期回调。 */
|
||||
aethera::Timer_Callback cancel_completion; /* 测试显式完成异步取消的回调。 */
|
||||
std::chrono::nanoseconds scheduled_interval{}; /* 策略提交给服务的周期。 */
|
||||
std::chrono::nanoseconds scheduled_interval{}; /* 策略提交给服务的初始期限。 */
|
||||
std::chrono::nanoseconds rescheduled_interval{}; /* 策略最近一次计算出的下一帧延迟。 */
|
||||
aethera::Timer_Id cancelled_timer_id{}; /* 策略请求取消的计时器标识。 */
|
||||
std::size_t frames_created{}; /* Scene 创建的物理帧数量。 */
|
||||
std::size_t frames_alive{}; /* 尚未析构的物理帧数量。 */
|
||||
@@ -65,6 +68,14 @@ struct Frame_Policy_Test_Control {
|
||||
};
|
||||
|
||||
struct Test_Timer_Service {
|
||||
aethera::Timer_Id schedule_after(
|
||||
std::chrono::nanoseconds interval,
|
||||
aethera::Timer_Callback callback) {
|
||||
control->scheduled_interval = interval;
|
||||
control->timer_callback = std::move(callback);
|
||||
return Timer_Id;
|
||||
}
|
||||
|
||||
aethera::Timer_Id schedule_every(
|
||||
std::chrono::nanoseconds interval,
|
||||
aethera::Timer_Callback callback) {
|
||||
@@ -80,6 +91,13 @@ struct Test_Timer_Service {
|
||||
control->cancel_completion = std::move(completion);
|
||||
}
|
||||
|
||||
void reschedule(
|
||||
aethera::Timer_Id id,
|
||||
std::chrono::nanoseconds interval) {
|
||||
EXPECT_EQ(id, Timer_Id);
|
||||
control->rescheduled_interval = interval;
|
||||
}
|
||||
|
||||
static constexpr aethera::Timer_Id Timer_Id{7};
|
||||
std::shared_ptr<Frame_Policy_Test_Control> control; /* 服务调用记录的共享所有权。 */
|
||||
};
|
||||
@@ -151,17 +169,29 @@ struct Test_Sink {
|
||||
|
||||
std::unique_ptr<aethera::Throttled_Latest_only> make_policy(
|
||||
const std::shared_ptr<Frame_Policy_Test_Control>& control,
|
||||
std::optional<double> user_frames_per_second = std::nullopt,
|
||||
bool render_rate_limit_enabled = false,
|
||||
bool send_rate_limit_enabled = false,
|
||||
bool statistics_enabled = false,
|
||||
std::size_t statistics_window_size = 120) {
|
||||
auto timer_service =
|
||||
pro::make_proxy<
|
||||
aethera::Periodic_Timer_Service,
|
||||
aethera::Timer_Service,
|
||||
Test_Timer_Service>(control);
|
||||
auto scene = pro::make_proxy<aethera::FP_Scene, Test_Scene>(control);
|
||||
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::user_frames_per_second,
|
||||
user_frames_per_second)
|
||||
.set<aethera::Import_Struct_With_Dirty, aethera::Prop_Tag>(
|
||||
&aethera::Throttled_Latest_only::Prop::render_rate_limit_enabled,
|
||||
render_rate_limit_enabled)
|
||||
.set<aethera::Import_Struct_With_Dirty, aethera::Prop_Tag>(
|
||||
&aethera::Throttled_Latest_only::Prop::send_rate_limit_enabled,
|
||||
send_rate_limit_enabled)
|
||||
.set<aethera::Import_Struct_With_Dirty, aethera::Prop_Tag>(
|
||||
&aethera::Throttled_Latest_only::Prop::statistics_enabled,
|
||||
statistics_enabled)
|
||||
@@ -192,9 +222,9 @@ TEST(Sliding_Statistics, Maintains_Average_P95_And_Deviation_Without_Snapshot) {
|
||||
|
||||
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, true);
|
||||
auto policy = make_policy(control, 50.0, false, false, true);
|
||||
ASSERT_EQ(
|
||||
policy->start(50.0),
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::started);
|
||||
EXPECT_EQ(control->scheduled_interval, 20ms);
|
||||
EXPECT_EQ(control->frames_created, 3);
|
||||
@@ -227,11 +257,12 @@ TEST(Throttled_Latest_Only, Rotates_Three_Frames_And_Drops_When_All_Are_Busy) {
|
||||
control->complete_send();
|
||||
}
|
||||
|
||||
policy->state.read([](const aethera::Throttled_Latest_only::State& state) {
|
||||
policy->read<aethera::Export_Struct, aethera::State_Tag>(
|
||||
[](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(
|
||||
@@ -246,9 +277,9 @@ TEST(Throttled_Latest_Only, Rotates_Three_Frames_And_Drops_When_All_Are_Busy) {
|
||||
|
||||
TEST(Throttled_Latest_Only, Stop_Drains_All_Frame_Borrows) {
|
||||
auto control = std::make_shared<Frame_Policy_Test_Control>();
|
||||
auto policy = make_policy(control);
|
||||
auto policy = make_policy(control, 60.0);
|
||||
ASSERT_EQ(
|
||||
policy->start(60.0),
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::started);
|
||||
control->fire_timer();
|
||||
control->fire_timer();
|
||||
@@ -271,26 +302,124 @@ TEST(Throttled_Latest_Only, Stop_Drains_All_Frame_Borrows) {
|
||||
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);
|
||||
});
|
||||
policy->read<aethera::Export_Struct, aethera::State_Tag>(
|
||||
[](const aethera::Throttled_Latest_only::State& state) {
|
||||
EXPECT_EQ(state.timer_ticks, 2);
|
||||
EXPECT_EQ(state.completed_frames, 2);
|
||||
});
|
||||
}
|
||||
|
||||
TEST(Throttled_Latest_Only, Sending_Frame_Does_Not_Block_Next_Render) {
|
||||
auto control = std::make_shared<Frame_Policy_Test_Control>();
|
||||
auto policy = make_policy(control, 60.0);
|
||||
ASSERT_EQ(
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::started);
|
||||
|
||||
control->fire_timer();
|
||||
control->complete_render();
|
||||
ASSERT_EQ(control->sending_frames.size(), 1);
|
||||
control->fire_timer();
|
||||
|
||||
EXPECT_EQ(control->sending_frames.size(), 1);
|
||||
EXPECT_EQ(control->rendering_frames.size(), 1);
|
||||
EXPECT_EQ(control->render_calls, 2);
|
||||
|
||||
control->complete_render();
|
||||
control->complete_send();
|
||||
control->complete_send();
|
||||
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, Defaults_To_Thirty_Frames_Per_Second) {
|
||||
auto control = std::make_shared<Frame_Policy_Test_Control>();
|
||||
auto policy = make_policy(control);
|
||||
ASSERT_EQ(
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::started);
|
||||
EXPECT_EQ(control->scheduled_interval, 33333334ns);
|
||||
|
||||
control->fire_timer();
|
||||
EXPECT_EQ(control->rescheduled_interval, 33333334ns);
|
||||
control->complete_render();
|
||||
control->complete_send();
|
||||
policy->read<aethera::Export_Struct, aethera::State_Tag>(
|
||||
[](const aethera::Throttled_Latest_only::State& state) {
|
||||
EXPECT_DOUBLE_EQ(state.effective_frames_per_second, 30.0);
|
||||
EXPECT_FALSE(state.render_capacity_fps.has_value());
|
||||
EXPECT_FALSE(state.send_capacity_fps.has_value());
|
||||
});
|
||||
|
||||
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, Combines_Optional_Adaptive_Rate_Limits) {
|
||||
auto control = std::make_shared<Frame_Policy_Test_Control>();
|
||||
auto policy = make_policy(control, std::nullopt, true, true);
|
||||
ASSERT_EQ(
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::started);
|
||||
EXPECT_EQ(control->scheduled_interval, 33333334ns);
|
||||
|
||||
control->fire_timer();
|
||||
EXPECT_EQ(control->rescheduled_interval, 33333334ns);
|
||||
control->complete_render();
|
||||
control->complete_send();
|
||||
control->fire_timer();
|
||||
|
||||
policy->read<aethera::Export_Struct, aethera::State_Tag>(
|
||||
[](const aethera::Throttled_Latest_only::State& state) {
|
||||
ASSERT_TRUE(state.render_capacity_fps.has_value());
|
||||
ASSERT_TRUE(state.send_capacity_fps.has_value());
|
||||
EXPECT_DOUBLE_EQ(
|
||||
state.effective_frames_per_second,
|
||||
std::min(
|
||||
*state.render_capacity_fps,
|
||||
*state.send_capacity_fps));
|
||||
});
|
||||
EXPECT_GT(control->rescheduled_interval, 0ns);
|
||||
|
||||
control->complete_render();
|
||||
control->complete_send();
|
||||
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, 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);
|
||||
auto policy = make_policy(control, 60.0, false, false, true, 2);
|
||||
ASSERT_EQ(
|
||||
policy->start(60.0),
|
||||
policy->start(),
|
||||
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) {
|
||||
policy->read<aethera::Export_Struct, aethera::State_Tag>(
|
||||
[](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);
|
||||
@@ -299,7 +428,7 @@ TEST(Throttled_Latest_Only, Publishes_Optional_Sliding_Statistics) {
|
||||
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(
|
||||
@@ -313,29 +442,48 @@ TEST(Throttled_Latest_Only, Publishes_Optional_Sliding_Statistics) {
|
||||
|
||||
TEST(Throttled_Latest_Only, Reports_Invalid_Configuration) {
|
||||
aethera::Throttled_Latest_only::Builder missing_builder(
|
||||
aethera::proxy<aethera::Periodic_Timer_Service>{},
|
||||
aethera::proxy<aethera::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),
|
||||
missing_dependencies->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::dependency_unavailable);
|
||||
EXPECT_EQ(
|
||||
missing_dependencies->stop({}),
|
||||
aethera::Throttled_Latest_only::Stop_Result::completion_missing);
|
||||
|
||||
auto control = std::make_shared<Frame_Policy_Test_Control>();
|
||||
auto policy = make_policy(control);
|
||||
auto policy = make_policy(control, 0.0);
|
||||
EXPECT_EQ(
|
||||
policy->start(0.0),
|
||||
aethera::Throttled_Latest_only::Start_Result::invalid_frames_per_second);
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::invalid_user_frame_rate);
|
||||
auto tiny_rate = make_policy(
|
||||
control,
|
||||
std::numeric_limits<double>::denorm_min());
|
||||
EXPECT_EQ(
|
||||
policy->start(std::numeric_limits<double>::denorm_min()),
|
||||
tiny_rate->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::interval_out_of_range);
|
||||
|
||||
auto invalid_statistics = make_policy(control, true, 0);
|
||||
auto invalid_statistics = make_policy(
|
||||
control,
|
||||
60.0,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
0);
|
||||
EXPECT_EQ(
|
||||
invalid_statistics->start(60.0),
|
||||
invalid_statistics->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::invalid_statistics_window);
|
||||
auto invalid_adaptive_window = make_policy(
|
||||
control,
|
||||
std::nullopt,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0);
|
||||
EXPECT_EQ(
|
||||
invalid_adaptive_window->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::invalid_statistics_window);
|
||||
}
|
||||
|
||||
@@ -343,9 +491,9 @@ 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;
|
||||
auto policy = make_policy(control);
|
||||
auto policy = make_policy(control, 60.0);
|
||||
ASSERT_EQ(
|
||||
policy->start(60.0),
|
||||
policy->start(),
|
||||
aethera::Throttled_Latest_only::Start_Result::started);
|
||||
|
||||
EXPECT_THROW(control->fire_timer(), std::runtime_error);
|
||||
|
||||
@@ -15,33 +15,21 @@ struct Number_List_Tag {
|
||||
using Value = typename Layer::Numbers;
|
||||
};
|
||||
|
||||
template <typename Self>
|
||||
struct Test_Model_Base : aethera::Model_Layer<
|
||||
Self,
|
||||
aethera::Root<Self>,
|
||||
aethera::Root,
|
||||
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> {
|
||||
struct Prop : Prev<aethera::Prop_Tag> {
|
||||
int base_number{};
|
||||
};
|
||||
struct Numbers : Layer::template Prev<Number_List_Tag> {};
|
||||
struct Numbers : 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 Test_Model : aethera::Def<Test_Model, Test_Model_Base> {
|
||||
struct Prop : Prev<aethera::Prop_Tag> {
|
||||
int number{};
|
||||
int other{};
|
||||
@@ -52,10 +40,10 @@ struct Test_Model : aethera::Def<Test_Model, Test_Model_Base<Test_Model>> {
|
||||
|
||||
static_assert(std::derived_from<
|
||||
Test_Model::Prop,
|
||||
Test_Model_Base<Test_Model>::Prop>);
|
||||
Test_Model_Base::Prop>);
|
||||
static_assert(std::derived_from<
|
||||
Test_Model::Numbers,
|
||||
Test_Model_Base<Test_Model>::Numbers>);
|
||||
Test_Model_Base::Numbers>);
|
||||
|
||||
TEST(Model, Registered_Tags_Build_Independent_Chained_Storages) {
|
||||
Test_Model::Builder builder;
|
||||
|
||||
Reference in New Issue
Block a user