This commit is contained in:
2026-08-27 17:54:05 +08:00
parent bc0bb24aea
commit 14f5421925
16 changed files with 234 additions and 205 deletions
+150 -85
View File
@@ -3,6 +3,7 @@
#include "Taskflow_Trace_Json.hpp"
#include <Frame_Pacing_Policy.hpp>
#include <Frame_Scheduler.hpp>
#include <concurrentqueue-1.0.5/concurrentqueue.h>
#include <nlohmann/json.hpp>
#include <magic_enum/magic_enum.hpp>
#include <render_2D/plottable/Plottables.hpp>
@@ -51,24 +52,6 @@ std::string exception_description(const std::exception_ptr& failure) {
return "empty Plot failure";
}
struct Frame_Policy final {
public:
[[nodiscard]] Frame_Pacing_Properties read() const { return pacing.read(); }
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] nlohmann::json write_prop(std::string_view key,
const nlohmann::json& value);
[[nodiscard]] bool accept_periodic_tick(double time_milliseconds) {
return pacing.accept_periodic_tick(time_milliseconds);
}
[[nodiscard]] bool request_immediate() { return pacing.request_immediate(); }
[[nodiscard]] double scheduled_rate_fps() const { return pacing.scheduled_rate_fps(); }
void frame_submitted() { pacing.frame_submitted(); }
[[nodiscard]] bool frame_completed() { return pacing.frame_completed(); }
void frame_rejected() { pacing.frame_rejected(); }
private:
Frame_Pacing_Policy pacing{};
};
std::string_view pacing_mode_name(Frame_Pacing_Mode mode) {
const auto name = magic_enum::enum_name(mode);
if (name.empty()) throw std::logic_error("unknown frame pacing mode");
@@ -93,8 +76,8 @@ std::string_view pixel_format_name(render_3d::Pixel_Format format) {
throw std::logic_error("unknown 3D pixel format");
}
nlohmann::json Frame_Policy::schema() const {
const auto current = read();
nlohmann::json frame_policy_schema(const Frame_Pacing_Policy& pacing) {
const auto current = pacing.read();
return {
{"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"},
{"fields", nlohmann::json::array({
@@ -113,19 +96,20 @@ nlohmann::json Frame_Policy::schema() const {
{"options", nlohmann::json::array({
{{"value", "manual"}, {"label", "手动渲染"}},
{{"value", "fixed_rate"}, {"label", "固定频率"}},
{{"value", "maximum_rate"}, {"label", "最大频率"}}
{{"value", "maximum_rate"}, {"label", "最大吞吐"}}
})}},
{{"key", "fixed_rate_fps"}, {"label", "目标帧率"}, {"editor", "number"},
{"editable", true}, {"minimum", 0.1}, {"maximum", 100.0}, {"step", 0.1},
{"description", "当前 Scene 独立目标帧率;周期策略保存在 Kernel Frame_Pacing_Policy"},
{"description", "仅 fixed_rate 使用;maximum_rate 在完成准入释放后异步自驱下一帧"},
{"technical_description", "Independent per-scene target frame rate."},
{"value", current.fixed_rate_fps}}
})}
};
}
nlohmann::json Frame_Policy::write_prop(std::string_view key,
const nlohmann::json& value) {
nlohmann::json write_frame_policy_prop(Frame_Pacing_Policy& pacing,
std::string_view key,
const nlohmann::json& value) {
if (key == "render_enabled" || key == "video_enabled") {
if (!value.is_boolean())
return {{"success", false}, {"error", "frame policy switch requires a boolean"}};
@@ -275,6 +259,7 @@ nlohmann::json taskflow_trace_json(
return {
{"sequence", trace.identity.sequence},
{"correlation_id", trace.identity.correlation_id},
{"request_source", magic_enum::enum_name(trace.request_source)},
{"created_time_unix_ns", trace.created_time_unix_ns},
{"worker_count", trace.worker_count},
{"markers", std::move(markers)},
@@ -344,6 +329,12 @@ struct Plot::Private {
consuming /* 外接 Taskflow 正在消费已发布帧;允许下一帧渲染。 */
};
enum struct Render_Admission_State : std::uint8_t {
ready,
rendering,
frame_slots_exhausted
};
struct Managed_Frame {
std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */
Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */
@@ -375,7 +366,7 @@ struct Plot::Private {
std::atomic_uint64_t next_stream_id{1};
std::atomic<std::shared_ptr<const std::string>> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */
std::uint64_t next_frame_sequence{1};
Frame_Policy frame_policy{};
Frame_Pacing_Policy frame_policy{};
Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */
static constexpr std::size_t scene_frame_capacity{3};
std::array<Managed_Frame, scene_frame_capacity> frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */
@@ -383,15 +374,15 @@ struct Plot::Private {
std::atomic<Managed_Frame*> retired_frames{}; /* 多回调生产、唯一 Taskflow 任务消费。 */
std::atomic_bool retired_frame_task_scheduled{};
Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */
std::atomic<std::shared_ptr<const Plot_Render_Tick>> pending_tick{};
moodycamel::ConcurrentQueue<Plot_Render_Tick> tick_requests{}; /* 多生产者提交、唯一短任务消费的帧请求流。 */
std::optional<Plot_Render_Tick> deferred_tick{}; /* 仅 tick consumer 任务访问的 latest 延后请求。 */
std::atomic_uint64_t tick_request_generation{}; /* 请求入队后推进,用于关闭 consumer 尾部唤醒竞争窗口。 */
std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */
std::atomic_bool render_admission_busy{}; /* view->update/Scene::advance 到 pixel publish 的唯一准入门。 */
std::atomic<Render_Admission_State> render_admission{Render_Admission_State::ready}; /* Plot 渲染准入及物理槽背压的唯一状态源。 */
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
std::atomic_uint64_t received_tick_count{}; /* 页面时钟交付给本 Plot 的 tick 总数。 */
std::atomic_uint64_t coalesced_tick_count{}; /* 尚未消费时被更新 tick 替换的旧 tick 总数。 */
std::atomic_uint64_t policy_skip_count{}; /* 帧策略拒绝的 tick 总数。 */
std::atomic_uint64_t preparation_busy_count{}; /* Render admission 忙时被合并为 latest pending 的 tick。 */
std::atomic_uint64_t deferred_resume_count{}; /* pixel publish 后立即唤醒 latest pending 的次数。 */
std::atomic_uint64_t frame_slot_busy_count{}; /* 三个物理帧槽均被占用的提交次数。 */
std::atomic_uint64_t scene_rejection_count{}; /* Scene 单帧准入拒绝的提交次数。 */
std::atomic_uint64_t submitted_frame_count{}; /* 成功提交给 Scene 的帧总数。 */
@@ -440,7 +431,8 @@ struct Plot::Private {
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] Stream_Snapshot stream_snapshot() const;
void publish(std::shared_ptr<const Plot_Stream_Frame> frame) noexcept;
void defer_tick(const Plot_Render_Tick& tick);
void submit_tick_request(Plot_Render_Tick tick);
void keep_latest_tick(const Plot_Render_Tick& tick);
void arm_tick_consumer(std::weak_ptr<Plot> lifetime);
void release_render_admission(std::weak_ptr<Plot> lifetime);
void consume_tick(std::weak_ptr<Plot> lifetime);
@@ -491,7 +483,7 @@ void Plot::Private::fail(std::exception_ptr failure) noexcept {
nlohmann::json Plot::Private::schema() const {
auto result = view->schema();
auto analysis = frame_policy.schema();
auto analysis = frame_policy_schema(frame_policy);
const auto generator = view->data_generator_schema();
if (!generator.is_null()) analysis["data_generator"] = generator;
result["frame_analysis"] = std::move(analysis);
@@ -545,39 +537,55 @@ void Plot::Private::publish(
void Plot::Private::refresh_schedule() {
if (!frame_timer.valid()) return;
const auto current_consumers = consumers.load(std::memory_order_acquire);
const double fps = frame_policy.scheduled_rate_fps();
if (fps <= 0.0 || current_consumers->empty()) frame_timer.cancel();
else frame_timer.start_periodic(fps);
const auto pacing = frame_policy.read();
if (!pacing.render_enabled || current_consumers->empty()) {
frame_timer.cancel();
return;
}
if (pacing.mode == Frame_Pacing_Mode::fixed_rate) {
frame_timer.start_periodic(pacing.fixed_rate_fps);
return;
}
frame_timer.cancel();
if (pacing.mode != Frame_Pacing_Mode::maximum_rate) return;
const auto now = std::chrono::steady_clock::now();
submit_tick_request(Plot_Render_Tick{
.issued_at = now,
.time_milliseconds = std::chrono::duration<double, std::milli>(
now - clock_origin).count(),
.source = Plot_Render_Tick_Source::maximum_rate});
arm_tick_consumer(lifetime);
}
void Plot::Private::submit_tick_request(Plot_Render_Tick tick) {
if (!tick_requests.enqueue(std::move(tick)))
throw std::bad_alloc{};
tick_request_generation.fetch_add(1, std::memory_order_release);
}
void Plot::Private::defer_tick(const Plot_Render_Tick& tick) {
const auto next = std::make_shared<const Plot_Render_Tick>(tick);
auto current = pending_tick.load(std::memory_order_acquire);
for (;;) {
if (current) {
const bool current_immediate = current->sequence == 0;
const bool next_immediate = tick.sequence == 0;
if ((current_immediate && !next_immediate) ||
(current_immediate == next_immediate &&
current->issued_at >= tick.issued_at))
return;
void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) {
const auto priority = [](Plot_Render_Tick_Source source) {
switch (source) {
case Plot_Render_Tick_Source::periodic: return 0;
case Plot_Render_Tick_Source::maximum_rate: return 1;
case Plot_Render_Tick_Source::immediate: return 2;
}
if (pending_tick.compare_exchange_weak(
current, next, std::memory_order_acq_rel,
std::memory_order_acquire)) {
if (current)
coalesced_tick_count.fetch_add(1, std::memory_order_relaxed);
return 0;
};
if (deferred_tick) {
const auto current_priority = priority(deferred_tick->source);
const auto next_priority = priority(tick.source);
coalesced_tick_count.fetch_add(1, std::memory_order_relaxed);
if (current_priority > next_priority ||
(current_priority == next_priority &&
deferred_tick->issued_at >= tick.issued_at))
return;
}
}
deferred_tick = tick;
}
void Plot::Private::arm_tick_consumer(std::weak_ptr<Plot> lifetime) {
if (terminal_failure.load(std::memory_order_acquire) ||
render_admission_busy.load(std::memory_order_acquire) ||
!pending_tick.load(std::memory_order_acquire))
return;
if (terminal_failure.load(std::memory_order_acquire)) return;
if (tick_task_scheduled.exchange(true, std::memory_order_acq_rel)) return;
aethera::schedule_task("web.plot.tick.consume", [lifetime] {
const auto plot = lifetime.lock();
@@ -588,26 +596,55 @@ void Plot::Private::arm_tick_consumer(std::weak_ptr<Plot> lifetime) {
}
void Plot::Private::release_render_admission(std::weak_ptr<Plot> lifetime) {
if (!render_admission_busy.exchange(false, std::memory_order_acq_rel))
auto expected = Render_Admission_State::rendering;
if (!render_admission.compare_exchange_strong(
expected, Render_Admission_State::ready,
std::memory_order_acq_rel, std::memory_order_acquire))
return;
if (pending_tick.load(std::memory_order_acquire))
deferred_resume_count.fetch_add(1, std::memory_order_relaxed);
const auto pacing = frame_policy.read();
if (pacing.render_enabled && pacing.mode == Frame_Pacing_Mode::maximum_rate) {
const auto current_consumers = consumers.load(std::memory_order_acquire);
if (!current_consumers->empty()) {
const auto now = std::chrono::steady_clock::now();
submit_tick_request(Plot_Render_Tick{
.issued_at = now,
.time_milliseconds = std::chrono::duration<double, std::milli>(
now - clock_origin).count(),
.source = Plot_Render_Tick_Source::maximum_rate});
}
}
arm_tick_consumer(std::move(lifetime));
}
void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
if (!render_admission_busy.load(std::memory_order_acquire)) {
const auto tick = pending_tick.exchange({}, std::memory_order_acq_rel);
const auto observed_generation =
tick_request_generation.load(std::memory_order_acquire);
Plot_Render_Tick requested;
while (tick_requests.try_dequeue(requested))
keep_latest_tick(requested);
if (render_admission.load(std::memory_order_acquire) ==
Render_Admission_State::ready) {
auto tick = std::exchange(deferred_tick, {});
if (tick) clock_tick(*tick);
}
tick_task_scheduled.store(false, std::memory_order_release);
arm_tick_consumer(std::move(lifetime));
if (tick_request_generation.load(std::memory_order_acquire) !=
observed_generation ||
(render_admission.load(std::memory_order_acquire) ==
Render_Admission_State::ready && deferred_tick))
arm_tick_consumer(std::move(lifetime));
}
void Plot::Private::clock_tick(const Plot_Render_Tick& tick) {
if (terminal_failure.load(std::memory_order_acquire)) return;
if (tick.sequence != 0 &&
!frame_policy.accept_periodic_tick(tick.time_milliseconds)) {
const auto pacing = frame_policy.read();
const bool accepted = pacing.render_enabled &&
(tick.source == Plot_Render_Tick_Source::immediate ||
(tick.source == Plot_Render_Tick_Source::periodic &&
pacing.mode == Frame_Pacing_Mode::fixed_rate) ||
(tick.source == Plot_Render_Tick_Source::maximum_rate &&
pacing.mode == Frame_Pacing_Mode::maximum_rate));
if (!accepted) {
policy_skip_count.fetch_add(1, std::memory_order_relaxed);
return;
}
@@ -688,12 +725,12 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
const auto pacing = frame_policy.read();
if (!pacing.render_enabled || streams.consumers->empty()) return;
bool admission_expected = false;
if (!render_admission_busy.compare_exchange_strong(
admission_expected, true, std::memory_order_acq_rel,
auto admission_expected = Render_Admission_State::ready;
if (!render_admission.compare_exchange_strong(
admission_expected, Render_Admission_State::rendering,
std::memory_order_acq_rel,
std::memory_order_acquire)) {
preparation_busy_count.fetch_add(1, std::memory_order_relaxed);
defer_tick(tick);
keep_latest_tick(tick);
return;
}
@@ -737,14 +774,16 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
}
if (!managed) {
frame_slot_busy_count.fetch_add(1, std::memory_order_relaxed);
defer_tick(tick);
keep_latest_tick(tick);
/*
* 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能
* 立即 arm tick consumer,否则会在没有任何槽可用期间形成
* consume -> no slot -> consume 的 Taskflow 任务风暴。真正的唤醒点
* 是 retire_completed_frame:某个 consuming 槽变回 available 后只唤醒一次。
*/
render_admission_busy.store(false, std::memory_order_release);
render_admission.store(
Render_Admission_State::frame_slots_exhausted,
std::memory_order_release);
return;
}
managed->presentation_time =
@@ -769,16 +808,28 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
const std::uint64_t sequence = next_frame_sequence++;
const Frame_Identity identity{
sequence, tick.sequence == 0 ? sequence : tick.sequence};
const auto request_source = [&] {
switch (tick.source) {
case Plot_Render_Tick_Source::periodic:
return Frame_Request_Source::periodic;
case Plot_Render_Tick_Source::immediate:
return Frame_Request_Source::immediate;
case Plot_Render_Tick_Source::maximum_rate:
return Frame_Request_Source::maximum_rate;
}
return Frame_Request_Source::unspecified;
}();
Render_Frame* logical_frame{};
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
(*frame_2d)->begin(identity, Frame_2D::native_pixel_format);
(*frame_2d)->begin(identity, Frame_2D::native_pixel_format,
request_source);
logical_frame = frame_2d->get();
} else {
auto& frame_3d = std::get<std::unique_ptr<Frame_3D>>(managed->frame);
frame_3d->begin(identity,
pacing.video_enabled ? Frame_3D_Output::pixels
: Frame_3D_Output::diagnostics,
Frame_3D::native_pixel_format);
Frame_3D::native_pixel_format, request_source);
logical_frame = frame_3d.get();
}
taskflow_trace_claimed = mark_taskflow_trace(*logical_frame);
@@ -819,10 +870,8 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
release_render_admission(lifetime);
} else {
taskflow_trace_claimed = false;
frame_policy.frame_submitted();
submitted_frame_count.fetch_add(1, std::memory_order_relaxed);
}
if (!result) frame_policy.frame_rejected();
return;
}
auto& output = *std::get<std::unique_ptr<Frame_3D>>(managed->frame);
@@ -832,11 +881,9 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
const auto result = scene_3d->render(&output);
if (result == Render_Scene_3D::Render_Result::submitted) {
taskflow_trace_claimed = false;
frame_policy.frame_submitted();
submitted_frame_count.fetch_add(1, std::memory_order_relaxed);
return;
}
frame_policy.frame_rejected();
scene_rejection_count.fetch_add(1, std::memory_order_relaxed);
rollback_unsubmitted();
restore_taskflow_trace_claim();
@@ -1124,8 +1171,12 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) {
throw std::logic_error("retired Plot frame is not consuming");
latest_statistics_frame.store(managed, std::memory_order_release);
/* 三个消费者槽曾全部占满时,退役一个槽后继续 latest pending。 */
arm_tick_consumer(lifetime);
/* 物理槽是唯一背压原因;归还任意槽后只解除一次耗尽状态。 */
auto admission = Render_Admission_State::frame_slots_exhausted;
if (render_admission.compare_exchange_strong(
admission, Render_Admission_State::ready,
std::memory_order_acq_rel, std::memory_order_acquire))
arm_tick_consumer(lifetime);
if (captured_trace) {
auto owner = lifetime;
@@ -1175,7 +1226,10 @@ void Plot::ensure_started() {
[weak](Frame_Scheduler::Tick tick) {
if (const auto owner = weak.lock()) {
owner->schedule_render(Plot_Render_Tick{
tick.issued_at, tick.sequence, tick.time_milliseconds});
.issued_at = tick.issued_at,
.sequence = tick.sequence,
.time_milliseconds = tick.time_milliseconds,
.source = Plot_Render_Tick_Source::periodic});
}
});
/*
@@ -1284,7 +1338,7 @@ void Plot::schedule_render(Plot_Render_Tick tick) {
ensure_started();
if (d->terminal_failure.load(std::memory_order_acquire)) return;
d->received_tick_count.fetch_add(1, std::memory_order_relaxed);
d->defer_tick(tick);
d->submit_tick_request(std::move(tick));
d->arm_tick_consumer(weak_from_this());
}
@@ -1294,7 +1348,10 @@ void Plot::render_once() {
const auto now = std::chrono::steady_clock::now();
const auto elapsed = now - d->clock_origin;
schedule_render(Plot_Render_Tick{
now, 0, std::chrono::duration<double, std::milli>(elapsed).count()});
.issued_at = now,
.time_milliseconds =
std::chrono::duration<double, std::milli>(elapsed).count(),
.source = Plot_Render_Tick_Source::immediate});
}
void Plot::submit_input(Plot_Input_Event event) {
@@ -1328,7 +1385,7 @@ nlohmann::json Plot::write_prop(std::string_view component,
ensure_started();
if (component != "frame-analysis")
return d->view->write_prop(component, key, value);
auto result = d->frame_policy.write_prop(key, value);
auto result = write_frame_policy_prop(d->frame_policy, key, value);
if (result.value("success", false)) d->refresh_schedule();
return result;
}
@@ -1395,6 +1452,16 @@ nlohmann::json Plot::diagnostics() const {
const auto pacing = d->frame_policy.read();
const auto stream = d->stream_snapshot();
const auto admission = d->render_admission.load(std::memory_order_acquire);
const auto admission_name = [&] {
switch (admission) {
case Private::Render_Admission_State::ready: return "ready";
case Private::Render_Admission_State::rendering: return "rendering";
case Private::Render_Admission_State::frame_slots_exhausted:
return "frame_slots_exhausted";
}
return "unknown";
}();
nlohmann::json supported_formats = nlohmann::json::array();
if (is_3d) {
for (const auto format : Frame_3D::supported_pixel_formats)
@@ -1436,9 +1503,7 @@ nlohmann::json Plot::diagnostics() const {
{"received_ticks", d->received_tick_count.load(std::memory_order_relaxed)},
{"coalesced_ticks", d->coalesced_tick_count.load(std::memory_order_relaxed)},
{"policy_skips", d->policy_skip_count.load(std::memory_order_relaxed)},
{"preparation_busy", d->preparation_busy_count.load(std::memory_order_relaxed)},
{"deferred_resumes", d->deferred_resume_count.load(std::memory_order_relaxed)},
{"render_admission_busy", d->render_admission_busy.load(std::memory_order_relaxed)},
{"render_admission", admission_name},
{"frame_slot_busy", d->frame_slot_busy_count.load(std::memory_order_relaxed)},
{"scene_rejections", d->scene_rejection_count.load(std::memory_order_relaxed)},
{"submitted_frames", d->submitted_frame_count.load(std::memory_order_relaxed)}}},
+7
View File
@@ -27,12 +27,19 @@ struct Plot_Input_Event {
std::uint32_t native_key{}; /* 浏览器原生按键码。 */
bool auto_repeat{}; /* 是否为系统重复按键。 */
};
enum struct Plot_Render_Tick_Source : std::uint8_t {
periodic,
immediate,
maximum_rate
};
struct Plot_Render_Tick {
std::chrono::steady_clock::time_point issued_at{}; /* 页面帧时钟发布本 tick 的单调时刻;手动帧在提交时填写。 */
std::uint64_t sequence{}; /* Kernel 全局 Frame_Scheduler 时间轴上的关联序号。 */
double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */
std::uint32_t width{320}; /* 当前图在媒体图集中的固定像素宽度。 */
std::uint32_t height{192}; /* 当前图在媒体图集中的固定像素高度。 */
Plot_Render_Tick_Source source{Plot_Render_Tick_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */
};
enum struct Plot_Pixel_Layout : std::uint8_t {
bgra8,