帧策略职责明确

This commit is contained in:
2026-08-29 22:04:25 +08:00
parent 20be348a6a
commit bb6bcec901
29 changed files with 1743 additions and 333 deletions
+16 -1
View File
@@ -52,12 +52,27 @@ if (Aethera_BUILD_TESTS)
set(Aethera_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/src/test")
append_glob_source(Aethera_Kernel_test_sources "${Aethera_Kernel_test_dir}")
add_executable(Aethera_Kernel_exe ${Aethera_Kernel_test_sources})
target_link_libraries(Aethera_Kernel_exe PRIVATE Aethera_Kernel GTest::gtest benchmark::benchmark)
target_link_libraries(Aethera_Kernel_exe PRIVATE
Aethera_Kernel
GTest::gtest
GTest::gmock)
add_test(NAME Aethera_Kernel_exe COMMAND Aethera_Kernel_exe)
set_tests_properties(Aethera_Kernel_exe PROPERTIES
LABELS "Aethera_Kernel"
ENVIRONMENT "Aethera_ERROR_MODE=exception")
list(APPEND Aethera_Kernel_test_targets Aethera_Kernel_exe)
add_executable(Aethera_Frame_Policy_Benchmarks
"${CMAKE_CURRENT_LIST_DIR}/src/benchmark/Frame_Policy_Benchmarks.cpp")
target_link_libraries(Aethera_Frame_Policy_Benchmarks PRIVATE
Aethera_Kernel
benchmark::benchmark)
add_custom_target(Aethera_Frame_Policy_benchmark
COMMAND Aethera_Frame_Policy_Benchmarks
--benchmark_min_time=1s
--benchmark_repetitions=3
--benchmark_report_aggregates_only=true
DEPENDS Aethera_Frame_Policy_Benchmarks
USES_TERMINAL)
endif ()
if (Aethera_BUILD_TESTS)
add_custom_target(Aethera_Kernel_check
@@ -0,0 +1,107 @@
#include "Frame_Policy/Frame_Policy.hpp"
#include "Frame_Policy/Frame_Policy_3D.hpp"
#include <benchmark/benchmark.h>
#include <chrono>
#include <cstdint>
#include <memory>
#include <stdexcept>
namespace {
template <typename Policy>
std::unique_ptr<Policy> build_policy() {
auto built = typename Policy::template Builder<Policy>{}.build();
if (!built) throw std::logic_error("benchmark frame policy build failed");
return std::move(*built);
}
void frame_policy_request_storm(benchmark::State& state) {
auto policy = build_policy<aethera::Frame_Policy>();
const auto origin = std::chrono::steady_clock::now();
std::uint64_t request_sequence{};
constexpr std::uint64_t batch_size = 256;
for (auto _ : state) {
static_cast<void>(_);
for (std::uint64_t index = 0; index < batch_size; ++index) {
++request_sequence;
policy->submit_request({
.issued_at = origin +
std::chrono::nanoseconds(request_sequence),
.time_milliseconds = static_cast<double>(request_sequence),
.source = index + 1 == batch_size
? aethera::Frame_Request_Source::immediate
: aethera::Frame_Request_Source::periodic});
}
static_cast<void>(policy->consume_events());
const auto production = policy->begin_production();
if (!production) {
state.SkipWithError("request storm did not admit one frame");
break;
}
const auto published_at = std::chrono::steady_clock::now();
policy->submit_publication_dispatch({
.started_at = published_at,
.completed_at = published_at,
.sequence = production->sequence,
.asynchronous_feedback_count = 0,
.succeeded = true});
static_cast<void>(policy->consume_events());
}
state.SetItemsProcessed(state.iterations() * batch_size);
}
void frame_policy_slow_sender(benchmark::State& state) {
auto policy = build_policy<aethera::Frame_Policy_3D>();
policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate);
const auto origin = std::chrono::steady_clock::now();
policy->submit_request({
.issued_at = origin,
.source = aethera::Frame_Request_Source::maximum_rate});
static_cast<void>(policy->consume_events());
const auto active = policy->begin_production();
if (!active) {
state.SkipWithError("slow sender setup did not admit a frame");
return;
}
const auto published_at = std::chrono::steady_clock::now();
policy->submit_publication_dispatch({
.started_at = published_at,
.completed_at = published_at,
.sequence = active->sequence,
.asynchronous_feedback_count = 1,
.succeeded = false});
std::uint64_t request_sequence{};
constexpr std::uint64_t batch_size = 256;
for (auto _ : state) {
static_cast<void>(_);
for (std::uint64_t index = 0; index < batch_size; ++index) {
++request_sequence;
policy->submit_request({
.issued_at = origin +
std::chrono::nanoseconds(request_sequence),
.time_milliseconds = static_cast<double>(request_sequence),
.source = aethera::Frame_Request_Source::maximum_rate});
}
static_cast<void>(policy->consume_events());
if (policy->begin_production()) {
state.SkipWithError("delivery window admitted a second frame");
break;
}
}
policy->submit_publication_feedback({
.completed_at = std::chrono::steady_clock::now(),
.sequence = active->sequence,
.succeeded = true});
static_cast<void>(policy->consume_events());
const auto resumed = policy->begin_production();
if (!resumed)
state.SkipWithError("delivery completion did not resume production");
state.SetItemsProcessed(state.iterations() * batch_size);
}
BENCHMARK(frame_policy_request_storm)->UseRealTime();
BENCHMARK(frame_policy_slow_sender)->UseRealTime();
}
BENCHMARK_MAIN();
+80 -29
View File
@@ -1,4 +1,5 @@
#include "Frame_Policy.hpp"
#include "Frame_Policy_Lifecycle.ipp"
#include <algorithm>
#include <cmath>
#include <stdexcept>
@@ -37,12 +38,37 @@ void reset_counters(Frame_Policy::State& state,
const auto pixel_delivery_enabled = state.pixel_delivery_enabled;
const auto mode = state.mode;
const auto fixed_rate_fps = state.fixed_rate_fps;
auto pending_request = std::move(state.pending_request);
const auto next_frame_sequence = state.next_frame_sequence;
const auto in_flight_frame_sequence = state.in_flight_frame_sequence;
const auto publication_started_ns = state.publication_started_ns;
const auto publication_feedback_expected =
state.publication_feedback_expected;
const auto publication_feedback_received =
state.publication_feedback_received;
const auto publication_latest_feedback_ns =
state.publication_latest_feedback_ns;
const auto publication_dispatch_known = state.publication_dispatch_known;
const auto publication_dispatch_succeeded =
state.publication_dispatch_succeeded;
const auto publication_feedback_succeeded =
state.publication_feedback_succeeded;
state = {};
state.generation = generation;
state.render_enabled = render_enabled;
state.pixel_delivery_enabled = pixel_delivery_enabled;
state.mode = mode;
state.fixed_rate_fps = fixed_rate_fps;
state.pending_request = std::move(pending_request);
state.next_frame_sequence = next_frame_sequence;
state.in_flight_frame_sequence = in_flight_frame_sequence;
state.publication_started_ns = publication_started_ns;
state.publication_feedback_expected = publication_feedback_expected;
state.publication_feedback_received = publication_feedback_received;
state.publication_latest_feedback_ns = publication_latest_feedback_ns;
state.publication_dispatch_known = publication_dispatch_known;
state.publication_dispatch_succeeded = publication_dispatch_succeeded;
state.publication_feedback_succeeded = publication_feedback_succeeded;
state.observation_started_ns = occurred_ns;
state.observed_until_ns = occurred_ns;
}
@@ -79,33 +105,63 @@ void Frame_Policy::reset_statistics() {
submit({.type = detail::Frame_Policy_Event_Type::reset});
}
void Frame_Policy::record_request(
Frame_Request_Source source,
std::chrono::steady_clock::time_point occurred_at) {
void Frame_Policy::submit_request(Frame_Request request) {
if (request.issued_at.time_since_epoch().count() == 0)
request.issued_at = std::chrono::steady_clock::now();
const auto source = request.source;
const auto occurred_at = request.issued_at;
auto& data = static_cast<Private&>(*d);
if (!data.requests.enqueue(std::move(request))) throw std::bad_alloc{};
submit({.type = detail::Frame_Policy_Event_Type::request_received,
.source = source,
.occurred_ns = monotonic_nanoseconds(occurred_at)});
}
void Frame_Policy::record_request_accepted(Frame_Request_Source source) {
submit({.type = detail::Frame_Policy_Event_Type::request_accepted,
.source = source});
std::optional<Frame_Production> Frame_Policy::begin_production() {
auto& data = static_cast<Private&>(*d);
auto& state = static_cast<State&>(
double_buffer::detail::Internal_Access::pending_state(this));
auto production = detail::begin_frame_production(data.requests, state);
publish_state<Base_Tag>();
return production;
}
void Frame_Policy::record_request_coalesced() {
submit({.type = detail::Frame_Policy_Event_Type::request_coalesced});
void Frame_Policy::defer_production(Frame_Production production) {
auto& state = static_cast<State&>(
double_buffer::detail::Internal_Access::pending_state(this));
detail::defer_frame_production(state, std::move(production));
publish_state<Base_Tag>();
}
void Frame_Policy::record_policy_rejection() {
submit({.type = detail::Frame_Policy_Event_Type::policy_rejected});
void Frame_Policy::reject_production(std::uint64_t sequence) {
auto& state = static_cast<State&>(
double_buffer::detail::Internal_Access::pending_state(this));
detail::reject_frame_production(state, sequence);
publish_state<Base_Tag>();
}
void Frame_Policy::record_frame_slot_backpressure() {
submit({.type = detail::Frame_Policy_Event_Type::frame_slot_backpressure});
void Frame_Policy::submit_publication_dispatch(
Frame_Publication_Dispatch dispatch) {
if (dispatch.started_at.time_since_epoch().count() == 0 ||
dispatch.completed_at < dispatch.started_at)
throw std::invalid_argument(
"frame publication requires an ordered dispatch interval");
submit({.type = detail::Frame_Policy_Event_Type::publication_dispatched,
.sequence = dispatch.sequence,
.occurred_ns = monotonic_nanoseconds(dispatch.started_at),
.duration_ns = monotonic_nanoseconds(dispatch.completed_at),
.count = dispatch.asynchronous_feedback_count,
.enabled = dispatch.succeeded});
}
void Frame_Policy::record_scene_rejection() {
submit({.type = detail::Frame_Policy_Event_Type::scene_rejected});
void Frame_Policy::submit_publication_feedback(
Frame_Publication_Feedback feedback) {
if (feedback.completed_at.time_since_epoch().count() == 0)
feedback.completed_at = std::chrono::steady_clock::now();
submit({.type = detail::Frame_Policy_Event_Type::publication_feedback,
.sequence = feedback.sequence,
.occurred_ns = monotonic_nanoseconds(feedback.completed_at),
.enabled = feedback.succeeded});
}
void Frame_Policy::record_frame_submitted(
@@ -164,21 +220,6 @@ bool Frame_Policy::consume_events() {
else if (event.source == Frame_Request_Source::maximum_rate)
++state.maximum_rate_request_count;
break;
case detail::Frame_Policy_Event_Type::request_accepted:
++state.accepted_request_count;
break;
case detail::Frame_Policy_Event_Type::request_coalesced:
++state.coalesced_request_count;
break;
case detail::Frame_Policy_Event_Type::policy_rejected:
++state.policy_rejection_count;
break;
case detail::Frame_Policy_Event_Type::frame_slot_backpressure:
++state.frame_slot_backpressure_count;
break;
case detail::Frame_Policy_Event_Type::scene_rejected:
++state.scene_rejection_count;
break;
case detail::Frame_Policy_Event_Type::frame_submitted:
++state.submitted_frame_count;
++state.active_frame_count;
@@ -212,6 +253,16 @@ bool Frame_Policy::consume_events() {
state.last_completion_ns = event.occurred_ns;
break;
}
case detail::Frame_Policy_Event_Type::publication_dispatched:
detail::begin_frame_publication(
state, event.sequence, event.occurred_ns,
event.duration_ns, event.count, event.enabled);
break;
case detail::Frame_Policy_Event_Type::publication_feedback:
detail::observe_publication_feedback(
state, event.sequence, event.occurred_ns,
event.enabled);
break;
case detail::Frame_Policy_Event_Type::reset:
reset_counters(state, event.occurred_ns);
break;
+62 -13
View File
@@ -1,7 +1,9 @@
#pragma once
#include "double_buffer/model.hpp"
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace aethera {
@@ -18,6 +20,35 @@ enum struct Frame_Request_Source : std::uint8_t {
maximum_rate
};
struct Frame_Request {
std::chrono::steady_clock::time_point issued_at{}; /* 请求进入帧策略的单调时刻。 */
std::uint64_t sequence{}; /* 全局帧时钟关联序号;手动请求为 0。 */
double time_milliseconds{}; /* 页面单调时间线上的动画时刻。 */
std::uint32_t width{320}; /* 无媒体消费者时使用的请求宽度。 */
std::uint32_t height{192}; /* 无媒体消费者时使用的请求高度。 */
Frame_Request_Source source{Frame_Request_Source::immediate}; /* 请求来源及合并优先级。 */
bool operator==(const Frame_Request&) const = default;
};
struct Frame_Production {
Frame_Request request{}; /* 已通过策略准入、必须完成或显式退回的生产请求。 */
std::uint64_t sequence{}; /* 帧策略分配的 Plot 单调帧序号。 */
};
struct Frame_Publication_Feedback {
std::chrono::steady_clock::time_point completed_at{}; /* 媒体发布反馈任务实际执行的单调时刻。 */
std::uint64_t sequence{}; /* 被反馈的策略帧序号。 */
bool succeeded{}; /* 至少一个媒体传输是否成功接收该帧。 */
};
struct Frame_Publication_Dispatch {
std::chrono::steady_clock::time_point started_at{}; /* Plot 开始分派完成帧的单调时刻。 */
std::chrono::steady_clock::time_point completed_at{}; /* Plot 完成全部订阅者调用的单调时刻。 */
std::uint64_t sequence{}; /* 被分派的策略帧序号。 */
std::size_t asynchronous_feedback_count{}; /* 已接受并承诺异步反馈的媒体任务数。 */
bool succeeded{}; /* 同步消费者是否至少成功一个。 */
};
namespace detail {
struct Frame_Policy_Event_Stream;
enum struct Frame_Policy_Event_Type : std::uint8_t {
@@ -26,13 +57,10 @@ enum struct Frame_Policy_Event_Type : std::uint8_t {
mode,
fixed_rate,
request_received,
request_accepted,
request_coalesced,
policy_rejected,
frame_slot_backpressure,
scene_rejected,
frame_submitted,
frame_completed,
publication_dispatched,
publication_feedback,
reset
};
struct Frame_Policy_Event {
@@ -42,6 +70,7 @@ struct Frame_Policy_Event {
std::uint64_t sequence{};
std::uint64_t occurred_ns{};
std::uint64_t duration_ns{};
std::uint64_t count{};
double number{};
bool enabled{};
};
@@ -64,6 +93,25 @@ struct Frame_Policy final : double_buffer::Def<
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate};
double fixed_rate_fps{30.0};
std::optional<Frame_Request> pending_request{}; /* 尚未生产时只保留最高优先级的最新请求。 */
std::uint64_t next_frame_sequence{1}; /* 下一次生产准入分配的单调序号。 */
std::uint64_t in_flight_frame_sequence{}; /* 非零时帧策略唯一管理的生产到发布在途帧。 */
std::uint64_t publication_started_ns{}; /* 当前在途帧开始向媒体发布的单调时刻。 */
std::uint64_t publication_feedback_expected{}; /* 本帧异步媒体发布任务总数。 */
std::uint64_t publication_feedback_received{}; /* 已由 Taskflow 节点报告的媒体发布任务数。 */
std::uint64_t publication_latest_feedback_ns{}; /* 最近一次媒体发布反馈完成时刻。 */
bool publication_dispatch_known{}; /* Plot 分派结果是否已由策略消费者归并。 */
bool publication_dispatch_succeeded{}; /* Plot 同步分派阶段是否已有成功消费者。 */
bool publication_feedback_succeeded{}; /* 异步媒体反馈是否已有成功结果。 */
std::uint64_t latest_publication_dispatch_latency_ns{};
std::uint64_t publication_dispatch_latency_total_ns{};
std::uint64_t maximum_publication_dispatch_latency_ns{};
std::uint64_t published_frame_count{}; /* 至少一次发布成功并完成反馈的累计帧数。 */
std::uint64_t publication_failed_count{}; /* 没有媒体消费者发布成功的累计帧数。 */
std::uint64_t latest_publication_latency_ns{}; /* 最近一帧从 Plot 发布到反馈任务的墙钟耗时。 */
std::uint64_t publication_latency_total_ns{};
std::uint64_t maximum_publication_latency_ns{};
std::uint64_t observation_started_ns{};
std::uint64_t observed_until_ns{};
std::uint64_t request_count{};
@@ -95,7 +143,7 @@ struct Frame_Policy final : double_buffer::Def<
Frame_Request_Source last_request_source{};
bool operator==(const State&) const = default;
};
struct Private : Prev_Private {};
struct Private;
void set_render_enabled(bool enabled);
void set_pixel_delivery_enabled(bool enabled);
@@ -103,13 +151,12 @@ struct Frame_Policy final : double_buffer::Def<
void set_fixed_rate(double fps);
void reset_statistics();
void record_request(Frame_Request_Source source,
std::chrono::steady_clock::time_point occurred_at);
void record_request_accepted(Frame_Request_Source source);
void record_request_coalesced();
void record_policy_rejection();
void record_frame_slot_backpressure();
void record_scene_rejection();
void submit_request(Frame_Request request);
[[nodiscard]] std::optional<Frame_Production> begin_production();
void defer_production(Frame_Production production);
void reject_production(std::uint64_t sequence);
void submit_publication_dispatch(Frame_Publication_Dispatch dispatch);
void submit_publication_feedback(Frame_Publication_Feedback feedback);
void record_frame_submitted(
std::uint64_t sequence, Frame_Request_Source source,
std::chrono::steady_clock::duration tick_queue_duration);
@@ -125,3 +172,5 @@ private:
};
}
#include "Frame_Policy.ipp"
@@ -0,0 +1,10 @@
#pragma once
#include <concurrentqueue-1.0.5/concurrentqueue.h>
namespace aethera {
struct Frame_Policy::Private : Prev_Private {
moodycamel::ConcurrentQueue<Frame_Request> requests{}; /* MPMC 请求入口;唯一策略 consumer 归并到 State。 */
};
}
@@ -1,4 +1,5 @@
#include "Frame_Policy_3D.hpp"
#include "Frame_Policy_Lifecycle.ipp"
#include <algorithm>
#include <cmath>
#include <span>
@@ -38,12 +39,37 @@ void reset_common_counters(Frame_Policy::State& state,
const auto pixel_delivery_enabled = state.pixel_delivery_enabled;
const auto mode = state.mode;
const auto fixed_rate_fps = state.fixed_rate_fps;
auto pending_request = std::move(state.pending_request);
const auto next_frame_sequence = state.next_frame_sequence;
const auto in_flight_frame_sequence = state.in_flight_frame_sequence;
const auto publication_started_ns = state.publication_started_ns;
const auto publication_feedback_expected =
state.publication_feedback_expected;
const auto publication_feedback_received =
state.publication_feedback_received;
const auto publication_latest_feedback_ns =
state.publication_latest_feedback_ns;
const auto publication_dispatch_known = state.publication_dispatch_known;
const auto publication_dispatch_succeeded =
state.publication_dispatch_succeeded;
const auto publication_feedback_succeeded =
state.publication_feedback_succeeded;
state = {};
state.generation = generation;
state.render_enabled = render_enabled;
state.pixel_delivery_enabled = pixel_delivery_enabled;
state.mode = mode;
state.fixed_rate_fps = fixed_rate_fps;
state.pending_request = std::move(pending_request);
state.next_frame_sequence = next_frame_sequence;
state.in_flight_frame_sequence = in_flight_frame_sequence;
state.publication_started_ns = publication_started_ns;
state.publication_feedback_expected = publication_feedback_expected;
state.publication_feedback_received = publication_feedback_received;
state.publication_latest_feedback_ns = publication_latest_feedback_ns;
state.publication_dispatch_known = publication_dispatch_known;
state.publication_dispatch_succeeded = publication_dispatch_succeeded;
state.publication_feedback_succeeded = publication_feedback_succeeded;
state.observation_started_ns = occurred_ns;
state.observed_until_ns = occurred_ns;
}
@@ -81,33 +107,65 @@ void Frame_Policy_3D::reset_statistics() {
submit({.type = detail::Frame_Policy_Event_Type::reset});
}
void Frame_Policy_3D::record_request(
Frame_Request_Source source,
std::chrono::steady_clock::time_point occurred_at) {
void Frame_Policy_3D::submit_request(Frame_Request request) {
if (request.issued_at.time_since_epoch().count() == 0)
request.issued_at = std::chrono::steady_clock::now();
const auto source = request.source;
const auto occurred_at = request.issued_at;
auto& data = static_cast<Private&>(*d);
if (!data.requests.enqueue(std::move(request))) throw std::bad_alloc{};
submit({.type = detail::Frame_Policy_Event_Type::request_received,
.source = source,
.occurred_ns = monotonic_nanoseconds_3d(occurred_at)});
}
void Frame_Policy_3D::record_request_accepted(Frame_Request_Source source) {
submit({.type = detail::Frame_Policy_Event_Type::request_accepted,
.source = source});
std::optional<Frame_Production> Frame_Policy_3D::begin_production() {
auto& data = static_cast<Private&>(*d);
auto& state = static_cast<State&>(
double_buffer::detail::Internal_Access::pending_state(this));
auto production = detail::begin_frame_production(
data.requests, state.common);
publish_state<Base_Tag>();
return production;
}
void Frame_Policy_3D::record_request_coalesced() {
submit({.type = detail::Frame_Policy_Event_Type::request_coalesced});
void Frame_Policy_3D::defer_production(Frame_Production production) {
auto& state = static_cast<State&>(
double_buffer::detail::Internal_Access::pending_state(this));
detail::defer_frame_production(
state.common, std::move(production));
publish_state<Base_Tag>();
}
void Frame_Policy_3D::record_policy_rejection() {
submit({.type = detail::Frame_Policy_Event_Type::policy_rejected});
void Frame_Policy_3D::reject_production(std::uint64_t sequence) {
auto& state = static_cast<State&>(
double_buffer::detail::Internal_Access::pending_state(this));
detail::reject_frame_production(state.common, sequence);
publish_state<Base_Tag>();
}
void Frame_Policy_3D::record_frame_slot_backpressure() {
submit({.type = detail::Frame_Policy_Event_Type::frame_slot_backpressure});
void Frame_Policy_3D::submit_publication_dispatch(
Frame_Publication_Dispatch dispatch) {
if (dispatch.started_at.time_since_epoch().count() == 0 ||
dispatch.completed_at < dispatch.started_at)
throw std::invalid_argument(
"3D frame publication requires an ordered dispatch interval");
submit({.type = detail::Frame_Policy_Event_Type::publication_dispatched,
.sequence = dispatch.sequence,
.occurred_ns = monotonic_nanoseconds_3d(dispatch.started_at),
.duration_ns = monotonic_nanoseconds_3d(dispatch.completed_at),
.count = dispatch.asynchronous_feedback_count,
.enabled = dispatch.succeeded});
}
void Frame_Policy_3D::record_scene_rejection() {
submit({.type = detail::Frame_Policy_Event_Type::scene_rejected});
void Frame_Policy_3D::submit_publication_feedback(
Frame_Publication_Feedback feedback) {
if (feedback.completed_at.time_since_epoch().count() == 0)
feedback.completed_at = std::chrono::steady_clock::now();
submit({.type = detail::Frame_Policy_Event_Type::publication_feedback,
.sequence = feedback.sequence,
.occurred_ns = monotonic_nanoseconds_3d(feedback.completed_at),
.enabled = feedback.succeeded});
}
void Frame_Policy_3D::record_frame_submitted(
@@ -203,21 +261,6 @@ bool Frame_Policy_3D::consume_events() {
else if (event.source == Frame_Request_Source::maximum_rate)
++common.maximum_rate_request_count;
break;
case detail::Frame_Policy_Event_Type::request_accepted:
++common.accepted_request_count;
break;
case detail::Frame_Policy_Event_Type::request_coalesced:
++common.coalesced_request_count;
break;
case detail::Frame_Policy_Event_Type::policy_rejected:
++common.policy_rejection_count;
break;
case detail::Frame_Policy_Event_Type::frame_slot_backpressure:
++common.frame_slot_backpressure_count;
break;
case detail::Frame_Policy_Event_Type::scene_rejected:
++common.scene_rejection_count;
break;
case detail::Frame_Policy_Event_Type::frame_submitted:
++common.submitted_frame_count;
++common.active_frame_count;
@@ -253,6 +296,16 @@ bool Frame_Policy_3D::consume_events() {
common.last_completion_ns = event.occurred_ns;
break;
}
case detail::Frame_Policy_Event_Type::publication_dispatched:
detail::begin_frame_publication(
common, event.sequence, event.occurred_ns,
event.duration_ns, event.count, event.enabled);
break;
case detail::Frame_Policy_Event_Type::publication_feedback:
detail::observe_publication_feedback(
common, event.sequence, event.occurred_ns,
event.enabled);
break;
case detail::Frame_Policy_Event_Type::reset:
reset_common_counters(common, event.occurred_ns);
state.overlapped_release_count = 0;
@@ -23,7 +23,7 @@ struct Frame_Policy_3D final : double_buffer::Def<
std::uint64_t last_completed_sequence{}; /* 最近退休的帧序号;未退休时为 0。 */
bool operator==(const State&) const = default;
};
struct Private : Prev_Private {};
struct Private;
static constexpr std::size_t pipeline_capacity{3};
void set_render_enabled(bool enabled);
@@ -32,13 +32,12 @@ struct Frame_Policy_3D final : double_buffer::Def<
void set_fixed_rate(double fps);
void reset_statistics();
void record_request(Frame_Request_Source source,
std::chrono::steady_clock::time_point occurred_at);
void record_request_accepted(Frame_Request_Source source);
void record_request_coalesced();
void record_policy_rejection();
void record_frame_slot_backpressure();
void record_scene_rejection();
void submit_request(Frame_Request request);
[[nodiscard]] std::optional<Frame_Production> begin_production();
void defer_production(Frame_Production production);
void reject_production(std::uint64_t sequence);
void submit_publication_dispatch(Frame_Publication_Dispatch dispatch);
void submit_publication_feedback(Frame_Publication_Feedback feedback);
void record_frame_submitted(
std::uint64_t sequence, Frame_Request_Source source,
std::chrono::steady_clock::duration tick_queue_duration);
@@ -53,3 +52,5 @@ private:
void submit(detail::Frame_Policy_Event event);
};
}
#include "Frame_Policy_3D.ipp"
@@ -0,0 +1,10 @@
#pragma once
#include <concurrentqueue-1.0.5/concurrentqueue.h>
namespace aethera {
struct Frame_Policy_3D::Private : Prev_Private {
moodycamel::ConcurrentQueue<Frame_Request> requests{}; /* MPMC 请求入口;唯一策略 consumer 归并到公共 State。 */
};
}
@@ -0,0 +1,158 @@
#pragma once
#include <algorithm>
#include <stdexcept>
namespace aethera::detail {
inline int frame_request_priority(Frame_Request_Source source) noexcept {
switch (source) {
case Frame_Request_Source::unspecified:
case Frame_Request_Source::periodic:
return 0;
case Frame_Request_Source::maximum_rate:
return 1;
case Frame_Request_Source::immediate:
return 2;
}
return 0;
}
inline void merge_frame_request(Frame_Policy::State& state,
Frame_Request request) {
if (!state.pending_request) {
state.pending_request = std::move(request);
return;
}
++state.coalesced_request_count;
const auto current_priority = frame_request_priority(
state.pending_request->source);
const auto next_priority = frame_request_priority(request.source);
if (current_priority > next_priority ||
(current_priority == next_priority &&
state.pending_request->issued_at >= request.issued_at))
return;
state.pending_request = std::move(request);
}
inline bool frame_request_matches(const Frame_Policy::State& state,
Frame_Request_Source source) noexcept {
return state.render_enabled &&
(source == Frame_Request_Source::immediate ||
(source == Frame_Request_Source::periodic &&
state.mode == Frame_Pacing_Mode::fixed_rate) ||
(source == Frame_Request_Source::maximum_rate &&
state.mode == Frame_Pacing_Mode::maximum_rate));
}
template <typename Queue>
std::optional<Frame_Production> begin_frame_production(
Queue& requests, Frame_Policy::State& state) {
Frame_Request request;
while (requests.try_dequeue(request))
merge_frame_request(state, std::move(request));
if (state.in_flight_frame_sequence != 0 || !state.pending_request)
return std::nullopt;
if (!frame_request_matches(state, state.pending_request->source)) {
state.pending_request.reset();
++state.policy_rejection_count;
return std::nullopt;
}
Frame_Production production{
std::move(*state.pending_request), state.next_frame_sequence++};
state.pending_request.reset();
state.in_flight_frame_sequence = production.sequence;
++state.accepted_request_count;
return production;
}
inline void defer_frame_production(Frame_Policy::State& state,
Frame_Production production) {
if (production.sequence == 0 ||
state.in_flight_frame_sequence != production.sequence)
throw std::logic_error(
"deferred frame production does not own the delivery slot");
state.in_flight_frame_sequence = 0;
++state.frame_slot_backpressure_count;
merge_frame_request(state, std::move(production.request));
}
inline void reject_frame_production(Frame_Policy::State& state,
std::uint64_t sequence) {
if (sequence == 0 || state.in_flight_frame_sequence != sequence)
throw std::logic_error(
"rejected frame production does not own the delivery slot");
state.in_flight_frame_sequence = 0;
++state.scene_rejection_count;
}
inline void finish_frame_publication(Frame_Policy::State& state,
std::uint64_t completed_ns) noexcept {
const bool succeeded = state.publication_dispatch_succeeded ||
state.publication_feedback_succeeded;
if (succeeded) ++state.published_frame_count;
else ++state.publication_failed_count;
const auto latency = completed_ns >= state.publication_started_ns
? completed_ns - state.publication_started_ns
: 0U;
state.latest_publication_latency_ns = latency;
state.publication_latency_total_ns += latency;
state.maximum_publication_latency_ns = std::max(
state.maximum_publication_latency_ns, latency);
state.in_flight_frame_sequence = 0;
state.publication_started_ns = 0;
state.publication_feedback_expected = 0;
state.publication_feedback_received = 0;
state.publication_latest_feedback_ns = 0;
state.publication_dispatch_known = false;
state.publication_dispatch_succeeded = false;
state.publication_feedback_succeeded = false;
}
inline void begin_frame_publication(
Frame_Policy::State& state,
std::uint64_t sequence,
std::uint64_t started_ns,
std::uint64_t completed_ns,
std::size_t asynchronous_feedback_count,
bool dispatch_succeeded) {
if (sequence == 0 || state.in_flight_frame_sequence != sequence)
throw std::logic_error(
"frame publication does not own the in-flight policy frame");
if (state.publication_dispatch_known)
throw std::logic_error("frame publication is already started");
state.publication_started_ns = started_ns;
state.publication_feedback_expected = asynchronous_feedback_count;
state.publication_dispatch_known = true;
state.publication_dispatch_succeeded = dispatch_succeeded;
const auto dispatch_latency = completed_ns >= started_ns
? completed_ns - started_ns
: 0U;
state.latest_publication_dispatch_latency_ns = dispatch_latency;
state.publication_dispatch_latency_total_ns += dispatch_latency;
state.maximum_publication_dispatch_latency_ns = std::max(
state.maximum_publication_dispatch_latency_ns, dispatch_latency);
if (state.publication_feedback_received >=
state.publication_feedback_expected)
finish_frame_publication(
state, std::max(completed_ns,
state.publication_latest_feedback_ns));
}
inline void observe_publication_feedback(
Frame_Policy::State& state,
std::uint64_t sequence,
std::uint64_t completed_ns,
bool succeeded) noexcept {
if (sequence == 0 || state.in_flight_frame_sequence != sequence)
return;
state.publication_feedback_succeeded |= succeeded;
++state.publication_feedback_received;
state.publication_latest_feedback_ns = std::max(
state.publication_latest_feedback_ns, completed_ns);
if (state.publication_dispatch_known &&
state.publication_feedback_received >=
state.publication_feedback_expected)
finish_frame_publication(state, completed_ns);
}
}
+9 -7
View File
@@ -41,16 +41,18 @@ TEST(frame_policy_3d, common_reset_resets_completed_gpu_statistics) {
const auto now = std::chrono::steady_clock::now();
policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate);
policy->record_request(
aethera::Frame_Request_Source::maximum_rate, now);
policy->record_request_accepted(
aethera::Frame_Request_Source::maximum_rate);
policy->submit_request(aethera::Frame_Request{
.issued_at = now,
.source = aethera::Frame_Request_Source::maximum_rate});
static_cast<void>(policy->consume_events());
const auto production = policy->begin_production();
ASSERT_TRUE(production.has_value());
policy->record_frame_submitted(
31, aethera::Frame_Request_Source::maximum_rate,
production->sequence, aethera::Frame_Request_Source::maximum_rate,
std::chrono::microseconds{40});
EXPECT_TRUE(policy->consume_events());
EXPECT_FALSE(policy->consume_events());
policy->record_frame_completed(
31, std::chrono::microseconds{90}, false);
production->sequence, std::chrono::microseconds{90}, false);
const auto& completed = policy->read_state<
aethera::Frame_Policy_3D::Base_Tag>();
@@ -0,0 +1,208 @@
#include "Frame_Policy/Frame_Policy.hpp"
#include "Frame_Policy/Frame_Policy_3D.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <memory>
namespace {
using namespace std::chrono_literals;
class Publication_Task {
public:
MOCK_METHOD(bool, publish, (std::uint64_t sequence));
};
template <typename Policy>
std::unique_ptr<Policy> build_policy() {
auto built = typename Policy::template Builder<Policy>{}.build();
if (!built) throw std::logic_error("test frame policy build failed");
return std::move(*built);
}
template <typename Policy>
const aethera::Frame_Policy::State& common_state(const Policy& policy) {
if constexpr (std::same_as<Policy, aethera::Frame_Policy>)
return policy.template read_state<typename Policy::Base_Tag>();
else
return policy.template read_state<typename Policy::Base_Tag>().common;
}
TEST(frame_policy_lifecycle,
newest_interaction_owns_the_only_production_admission) {
auto policy = build_policy<aethera::Frame_Policy>();
const auto origin = std::chrono::steady_clock::now();
policy->submit_request({
.issued_at = origin,
.source = aethera::Frame_Request_Source::periodic});
policy->submit_request({
.issued_at = origin + 1ms,
.source = aethera::Frame_Request_Source::maximum_rate});
policy->submit_request({
.issued_at = origin + 2ms,
.time_milliseconds = 2.0,
.source = aethera::Frame_Request_Source::immediate});
policy->submit_request({
.issued_at = origin + 3ms,
.source = aethera::Frame_Request_Source::periodic});
static_cast<void>(policy->consume_events());
const auto production = policy->begin_production();
ASSERT_TRUE(production.has_value());
EXPECT_EQ(production->request.source,
aethera::Frame_Request_Source::immediate);
EXPECT_DOUBLE_EQ(production->request.time_milliseconds, 2.0);
const auto& state = common_state(*policy);
EXPECT_EQ(state.request_count, 4u);
EXPECT_EQ(state.coalesced_request_count, 3u);
EXPECT_EQ(state.accepted_request_count, 1u);
EXPECT_EQ(state.in_flight_frame_sequence, production->sequence);
EXPECT_FALSE(state.pending_request.has_value());
}
TEST(frame_policy_lifecycle,
production_stays_closed_until_publication_task_reports_feedback) {
auto policy = build_policy<aethera::Frame_Policy>();
Publication_Task publication;
const auto origin = std::chrono::steady_clock::now();
policy->submit_request({
.issued_at = origin,
.source = aethera::Frame_Request_Source::periodic});
static_cast<void>(policy->consume_events());
const auto first = policy->begin_production();
ASSERT_TRUE(first.has_value());
policy->record_frame_submitted(
first->sequence, first->request.source, 10us);
policy->record_frame_completed(first->sequence, 20us);
policy->submit_request({
.issued_at = origin + 1ms,
.source = aethera::Frame_Request_Source::immediate});
static_cast<void>(policy->consume_events());
EXPECT_FALSE(policy->begin_production().has_value());
policy->submit_publication_dispatch({
.started_at = origin + 2ms,
.completed_at = origin + 2500us,
.sequence = first->sequence,
.asynchronous_feedback_count = 1,
.succeeded = false});
EXPECT_CALL(publication, publish(first->sequence))
.WillOnce(::testing::Return(true));
policy->submit_publication_feedback({
.completed_at = origin + 3ms,
.sequence = first->sequence,
.succeeded = publication.publish(first->sequence)});
static_cast<void>(policy->consume_events());
const auto second = policy->begin_production();
ASSERT_TRUE(second.has_value());
EXPECT_EQ(second->request.source,
aethera::Frame_Request_Source::immediate);
const auto& state = common_state(*policy);
EXPECT_EQ(state.published_frame_count, 1u);
EXPECT_EQ(state.latest_publication_dispatch_latency_ns, 500'000u);
EXPECT_EQ(state.latest_publication_latency_ns, 1'000'000u);
}
TEST(frame_policy_lifecycle,
early_taskflow_feedback_is_reconciled_after_dispatch_registration) {
auto policy = build_policy<aethera::Frame_Policy>();
const auto origin = std::chrono::steady_clock::now();
policy->submit_request({
.issued_at = origin,
.source = aethera::Frame_Request_Source::immediate});
static_cast<void>(policy->consume_events());
const auto production = policy->begin_production();
ASSERT_TRUE(production.has_value());
policy->submit_publication_feedback({
.completed_at = origin + 2ms,
.sequence = production->sequence,
.succeeded = true});
policy->submit_publication_dispatch({
.started_at = origin + 1ms,
.completed_at = origin + 1ms,
.sequence = production->sequence,
.asynchronous_feedback_count = 1,
.succeeded = false});
static_cast<void>(policy->consume_events());
const auto& state = common_state(*policy);
EXPECT_EQ(state.in_flight_frame_sequence, 0u);
EXPECT_EQ(state.published_frame_count, 1u);
EXPECT_EQ(state.latest_publication_latency_ns, 1'000'000u);
}
TEST(frame_policy_lifecycle,
slow_sender_coalesces_request_storm_without_expanding_delivery_window) {
auto policy = build_policy<aethera::Frame_Policy_3D>();
policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate);
const auto origin = std::chrono::steady_clock::now();
policy->submit_request({
.issued_at = origin,
.source = aethera::Frame_Request_Source::maximum_rate});
static_cast<void>(policy->consume_events());
const auto active = policy->begin_production();
ASSERT_TRUE(active.has_value());
policy->submit_publication_dispatch({
.started_at = origin,
.completed_at = origin,
.sequence = active->sequence,
.asynchronous_feedback_count = 1,
.succeeded = false});
constexpr std::uint64_t request_count = 10'000;
for (std::uint64_t index = 0; index < request_count; ++index) {
policy->submit_request({
.issued_at = origin + std::chrono::nanoseconds(index + 1),
.time_milliseconds = static_cast<double>(index),
.source = index + 1 == request_count
? aethera::Frame_Request_Source::immediate
: aethera::Frame_Request_Source::maximum_rate});
}
static_cast<void>(policy->consume_events());
EXPECT_FALSE(policy->begin_production().has_value());
const auto& blocked = common_state(*policy);
ASSERT_TRUE(blocked.pending_request.has_value());
EXPECT_EQ(blocked.pending_request->source,
aethera::Frame_Request_Source::immediate);
EXPECT_EQ(blocked.in_flight_frame_sequence, active->sequence);
EXPECT_EQ(blocked.coalesced_request_count, request_count - 1);
policy->submit_publication_feedback({
.completed_at = origin + std::chrono::milliseconds{1},
.sequence = active->sequence,
.succeeded = true});
static_cast<void>(policy->consume_events());
const auto resumed = policy->begin_production();
ASSERT_TRUE(resumed.has_value());
EXPECT_EQ(resumed->request.source,
aethera::Frame_Request_Source::immediate);
}
TEST(frame_policy_lifecycle,
deferred_physical_slot_returns_the_same_request_to_policy) {
auto policy = build_policy<aethera::Frame_Policy>();
const auto issued_at = std::chrono::steady_clock::now();
policy->submit_request({
.issued_at = issued_at,
.time_milliseconds = 17.0,
.source = aethera::Frame_Request_Source::periodic});
static_cast<void>(policy->consume_events());
auto production = policy->begin_production();
ASSERT_TRUE(production.has_value());
const auto first_sequence = production->sequence;
policy->defer_production(std::move(*production));
const auto retried = policy->begin_production();
ASSERT_TRUE(retried.has_value());
EXPECT_GT(retried->sequence, first_sequence);
EXPECT_EQ(retried->request.issued_at, issued_at);
EXPECT_DOUBLE_EQ(retried->request.time_milliseconds, 17.0);
EXPECT_EQ(common_state(*policy).frame_slot_backpressure_count, 1u);
}
}
+9 -5
View File
@@ -60,13 +60,16 @@ TEST(frame_policy, mpmc_events_publish_one_double_buffer_state) {
policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate);
policy->set_fixed_rate(47.5);
policy->set_pixel_delivery_enabled(false);
policy->record_request(aethera::Frame_Request_Source::maximum_rate,
std::chrono::steady_clock::now());
policy->record_request_accepted(aethera::Frame_Request_Source::maximum_rate);
policy->submit_request(aethera::Frame_Request{
.issued_at = std::chrono::steady_clock::now(),
.source = aethera::Frame_Request_Source::maximum_rate});
static_cast<void>(policy->consume_events());
const auto production = policy->begin_production();
ASSERT_TRUE(production.has_value());
policy->record_frame_submitted(
7, aethera::Frame_Request_Source::maximum_rate,
production->sequence, aethera::Frame_Request_Source::maximum_rate,
std::chrono::microseconds{250});
EXPECT_TRUE(policy->consume_events());
EXPECT_FALSE(policy->consume_events());
const auto& state = policy->read_state<aethera::Frame_Policy::Base_Tag>();
EXPECT_TRUE(state.render_enabled);
@@ -76,6 +79,7 @@ TEST(frame_policy, mpmc_events_publish_one_double_buffer_state) {
EXPECT_EQ(state.request_count, 1);
EXPECT_EQ(state.accepted_request_count, 1);
EXPECT_EQ(state.submitted_frame_count, 1);
EXPECT_EQ(state.in_flight_frame_sequence, production->sequence);
EXPECT_EQ(state.latest_tick_queue_ns, 250'000);
EXPECT_THROW(policy->set_fixed_rate(0.0), std::invalid_argument);
}