帧策略职责明确
This commit is contained in:
@@ -27,13 +27,13 @@ public:
|
||||
struct Data_Generator {
|
||||
nlohmann::json schema;
|
||||
std::function<nlohmann::json(const nlohmann::json &)> generate;
|
||||
std::function<void(const nlohmann::json &, const Plot_Render_Tick &)> advance;
|
||||
std::function<void(const nlohmann::json &, const Frame_Request &)> advance;
|
||||
explicit operator bool() const noexcept {
|
||||
return static_cast<bool>(generate);
|
||||
}
|
||||
};
|
||||
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
|
||||
std::function<void(const Plot_Render_Tick &, bool)> value_update,
|
||||
std::function<void(const Frame_Request &, bool)> value_update,
|
||||
Data_Generator value_data_generator,
|
||||
Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)),
|
||||
update_scene(std::move(value_update)),
|
||||
@@ -85,14 +85,14 @@ public:
|
||||
std::memory_order_release);
|
||||
return result;
|
||||
}
|
||||
void update(const Plot_Render_Tick& request) override {
|
||||
void update(const Frame_Request& request) override {
|
||||
const auto input = generated_data.load(std::memory_order_acquire);
|
||||
if (input && data_generator.advance) data_generator.advance(*input, request);
|
||||
update_scene(request, !input);
|
||||
}
|
||||
private:
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
||||
std::function<void(const Plot_Render_Tick &, bool)> update_scene;
|
||||
std::function<void(const Frame_Request &, bool)> update_scene;
|
||||
Data_Generator data_generator;
|
||||
std::atomic<std::shared_ptr<const nlohmann::json>> generated_data{}; /* 成功生成后发布不可变参数;渲染任务按同一配置持续产生压力数据。 */
|
||||
std::tuple<Owned_Objects...> objects;
|
||||
@@ -420,7 +420,7 @@ template <typename... Fields, typename Object, typename... Owned_Objects>
|
||||
std::unique_ptr<Plot::Scene_View> make_scene_view(
|
||||
Object & object,
|
||||
Scene_2D & scene,
|
||||
std::function < void(const Plot_Render_Tick &, bool) > update,
|
||||
std::function < void(const Frame_Request &, bool) > update,
|
||||
Owned_Objects &&... owned_objects) {
|
||||
using Definition = typename Object::Attached_Object;
|
||||
using Tag = typename Definition::Base_Tag;
|
||||
@@ -456,7 +456,7 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
|
||||
return generate_2d_data(object, input);
|
||||
},
|
||||
[&object](const nlohmann::json& input,
|
||||
const Plot_Render_Tick& tick) {
|
||||
const Frame_Request& tick) {
|
||||
const auto interval = generator_count(
|
||||
input, "update_every_n_frames", 100'000);
|
||||
if (tick.sequence % interval != 0) return;
|
||||
@@ -555,7 +555,7 @@ std::shared_ptr<Plot> make_axes_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
frequency = not_null{frequency.get()},
|
||||
numeric = not_null{numeric.get()},
|
||||
time = not_null{time.get()}](const Plot_Render_Tick& event, bool) {
|
||||
time = not_null{time.get()}](const Frame_Request& event, bool) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
|
||||
constexpr double day_milliseconds = 86'400'000.0;
|
||||
time->append_time(Time_Of_Day{
|
||||
@@ -630,7 +630,7 @@ std::shared_ptr<Plot> make_spectrum_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
raw = not_null{spectrum.get()},
|
||||
frequency = not_null{frequency.get()},
|
||||
vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) {
|
||||
vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (!demo_data) return;
|
||||
std::array < double, 256 > samples{};
|
||||
@@ -695,7 +695,7 @@ std::shared_ptr<Plot> make_frequency_trace_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
raw = not_null{trace.get()},
|
||||
time = not_null{time.get()},
|
||||
vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) {
|
||||
vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
||||
if (!demo_data) return;
|
||||
constexpr double day_milliseconds = 86'400'000.0;
|
||||
@@ -739,7 +739,7 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
raw = not_null{sweep.get()},
|
||||
frequency = not_null{frequency.get()},
|
||||
vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) {
|
||||
vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (!demo_data) return;
|
||||
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
|
||||
@@ -793,7 +793,7 @@ std::shared_ptr<Plot> make_afterglow_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
raw = not_null{afterglow.get()},
|
||||
frequency = not_null{frequency.get()},
|
||||
vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) {
|
||||
vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (!demo_data) return;
|
||||
std::array < double, 192 > values{};
|
||||
@@ -841,7 +841,7 @@ std::shared_ptr<Plot> make_waterfall_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
raw = not_null{waterfall.get()},
|
||||
frequency = not_null{frequency.get()},
|
||||
time = not_null{time.get()}](const Plot_Render_Tick& event, bool demo_data) {
|
||||
time = not_null{time.get()}](const Frame_Request& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
||||
if (!demo_data) return;
|
||||
constexpr double day_milliseconds = 86'400'000.0;
|
||||
@@ -900,7 +900,7 @@ std::shared_ptr<Plot> make_constellation_plot() {
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
raw = not_null{constellation.get()},
|
||||
horizontal = not_null{horizontal.get()},
|
||||
vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) {
|
||||
vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
if (!demo_data) return;
|
||||
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
|
||||
@@ -952,7 +952,7 @@ std::shared_ptr<Plot> make_selection_overlay_plot() {
|
||||
.build();
|
||||
auto update = [scene = not_null{scene.get()},
|
||||
horizontal = not_null{horizontal.get()},
|
||||
vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool) {
|
||||
vertical = not_null{vertical.get()}](const Frame_Request& event, bool) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
|
||||
@@ -233,7 +233,7 @@ struct Random_Data_Generator {
|
||||
}
|
||||
template <typename Visual_Object>
|
||||
void update(Visual_Object& visual, Axes_3D&, Marker_Visual*,
|
||||
const Plot_Render_Tick& request) {
|
||||
const Frame_Request& request) {
|
||||
using Definition = typename Visual_Object::Attached_Object;
|
||||
using Prop = typename Definition::Prop;
|
||||
const auto sequence = request.sequence != 0
|
||||
@@ -436,11 +436,11 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
void update(const Plot_Render_Tick& request) override {
|
||||
void update(const Frame_Request& request) override {
|
||||
if constexpr (requires(Data_Generator& generator, Visual_Object& visual,
|
||||
Axes_Object& axes,
|
||||
Marker_Object* markers,
|
||||
const Plot_Render_Tick& frame) {
|
||||
const Frame_Request& frame) {
|
||||
generator.update(visual, axes, markers, frame);
|
||||
}) {
|
||||
data_generator_.update(
|
||||
@@ -757,7 +757,7 @@ struct Spectrogram_Data_Generator {
|
||||
}
|
||||
void update(Mesh_Visual& visual, Axes_3D&,
|
||||
Marker_Visual* markers,
|
||||
const Plot_Render_Tick& request) {
|
||||
const Frame_Request& request) {
|
||||
if (!parameters.animation_enabled ||
|
||||
request.sequence % parameters.update_every_n_frames != 0)
|
||||
return;
|
||||
|
||||
+132
-140
@@ -211,6 +211,39 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) {
|
||||
{"active_frame_count", state.active_frame_count}
|
||||
}
|
||||
},
|
||||
{
|
||||
"lifecycle", {
|
||||
{"pending_request", state.pending_request.has_value()},
|
||||
{"in_flight_window", 1},
|
||||
{"in_flight_frame_sequence", state.in_flight_frame_sequence},
|
||||
{"publication_feedback_expected",
|
||||
state.publication_feedback_expected},
|
||||
{"publication_feedback_received",
|
||||
state.publication_feedback_received},
|
||||
{"publication_feedback_remaining",
|
||||
state.publication_feedback_expected >
|
||||
state.publication_feedback_received
|
||||
? state.publication_feedback_expected -
|
||||
state.publication_feedback_received
|
||||
: 0},
|
||||
{"latest_publication_dispatch_ms",
|
||||
static_cast<double>(
|
||||
state.latest_publication_dispatch_latency_ns) /
|
||||
1'000'000.0},
|
||||
{"maximum_publication_dispatch_ms",
|
||||
static_cast<double>(
|
||||
state.maximum_publication_dispatch_latency_ns) /
|
||||
1'000'000.0},
|
||||
{"published_frame_count", state.published_frame_count},
|
||||
{"publication_failed_count", state.publication_failed_count},
|
||||
{"latest_publication_ms",
|
||||
static_cast<double>(state.latest_publication_latency_ns) /
|
||||
1'000'000.0},
|
||||
{"maximum_publication_ms",
|
||||
static_cast<double>(state.maximum_publication_latency_ns) /
|
||||
1'000'000.0}
|
||||
}
|
||||
},
|
||||
{
|
||||
"throughput", {
|
||||
{"request_rate_fps", rate(state.request_count)},
|
||||
@@ -613,11 +646,6 @@ struct Plot::Private {
|
||||
rendering, /* Scene::advance -> Plot pixel publish,不可重入。 */
|
||||
consuming /* 外接 Taskflow 正在消费已发布帧;允许下一帧渲染。 */
|
||||
};
|
||||
enum struct Render_Admission_State : std::uint8_t {
|
||||
ready,
|
||||
rendering,
|
||||
frame_slots_exhausted
|
||||
};
|
||||
struct Managed_Frame {
|
||||
std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */
|
||||
std::chrono::steady_clock::time_point tick_issued_at{}; /* 本逻辑帧请求进入 Plot 的时刻。 */
|
||||
@@ -647,9 +675,12 @@ struct Plot::Private {
|
||||
std::atomic<std::shared_ptr<const Consumer_Map>> consumers{
|
||||
std::make_shared<const Consumer_Map>()
|
||||
}; /* 低频订阅修改发布不可变版本。 */
|
||||
struct Publication_Dispatch {
|
||||
std::size_t asynchronous_feedback_count{};
|
||||
bool succeeded{};
|
||||
};
|
||||
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};
|
||||
std::unique_ptr<Frame_Policy> frame_policy_2d{};
|
||||
std::unique_ptr<Frame_Policy_3D> frame_policy_3d{};
|
||||
Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */
|
||||
@@ -658,11 +689,8 @@ struct Plot::Private {
|
||||
std::atomic<Managed_Frame*> latest_statistics_frame{}; /* 只定位权威帧槽,不保存统计副本。 */
|
||||
std::atomic<Managed_Frame*> retired_frames{}; /* 完成回调返回、唯一帧策略写者消费的物理帧。 */
|
||||
Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */
|
||||
moodycamel::ConcurrentQueue<Plot_Render_Tick> tick_requests{}; /* 多生产者提交、唯一短任务消费的帧请求流。 */
|
||||
std::optional<Plot_Render_Tick> deferred_tick{}; /* 仅 tick consumer 任务访问的 latest 延后请求。 */
|
||||
std::atomic_uint64_t consumer_work_generation{}; /* tick 或退休帧入队后推进,关闭 consumer 尾部唤醒竞争窗口。 */
|
||||
std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */
|
||||
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_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */
|
||||
static constexpr std::size_t maximum_taskflow_trace_frames{120};
|
||||
@@ -717,17 +745,16 @@ 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 submit_tick_request(Plot_Render_Tick tick);
|
||||
void keep_latest_tick(const Plot_Render_Tick& tick);
|
||||
[[nodiscard]] Publication_Dispatch publish(
|
||||
std::shared_ptr<const Plot_Stream_Frame> frame) noexcept;
|
||||
void submit_frame_request(Frame_Request request);
|
||||
void arm_tick_consumer(std::weak_ptr<Plot> lifetime);
|
||||
void release_render_admission(std::weak_ptr<Plot> lifetime);
|
||||
void request_next_maximum_rate_frame();
|
||||
void retain_frame_policy_lifetime();
|
||||
void release_frame_policy_lifetime_if_idle();
|
||||
void consume_tick(std::weak_ptr<Plot> lifetime);
|
||||
void refresh_schedule();
|
||||
void clock_tick(const Plot_Render_Tick& tick);
|
||||
void render_frame(Plot_Render_Tick tick);
|
||||
void render_frame(Frame_Production production);
|
||||
void publish_completed_frame(not_null<Render_Frame*> completed);
|
||||
void consume_completed_frame(not_null<Render_Frame*> frame);
|
||||
void retire_completed_frame(not_null<Render_Frame*> frame);
|
||||
@@ -766,7 +793,7 @@ void Plot::Private::fail(std::exception_ptr failure) noexcept {
|
||||
}.dump(),
|
||||
{}
|
||||
});
|
||||
publish(std::move(output));
|
||||
static_cast<void>(publish(std::move(output)));
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
@@ -802,22 +829,32 @@ Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const {
|
||||
result.height = std::clamp(result.height == 0 ? 192U : result.height, 120U, 1080U) & ~1U;
|
||||
return result;
|
||||
}
|
||||
void Plot::Private::publish(
|
||||
Plot::Private::Publication_Dispatch Plot::Private::publish(
|
||||
std::shared_ptr<const Plot_Stream_Frame> frame) noexcept {
|
||||
if (!frame) return;
|
||||
Publication_Dispatch dispatch;
|
||||
if (!frame) return dispatch;
|
||||
try {
|
||||
const auto snapshot = stream_snapshot();
|
||||
std::vector<Stream_Id> failed_consumers;
|
||||
for (const auto& [id, consumer] : *snapshot.consumers) {
|
||||
if (!consumer.handler) continue;
|
||||
try {
|
||||
consumer.handler(frame);
|
||||
switch (consumer.handler(frame)) {
|
||||
case Plot_Frame_Publication::ignored:
|
||||
break;
|
||||
case Plot_Frame_Publication::completed:
|
||||
dispatch.succeeded = true;
|
||||
break;
|
||||
case Plot_Frame_Publication::asynchronous:
|
||||
++dispatch.asynchronous_feedback_count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
failed_consumers.push_back(id);
|
||||
}
|
||||
}
|
||||
if (failed_consumers.empty()) return;
|
||||
if (failed_consumers.empty()) return dispatch;
|
||||
auto current = consumers.load(std::memory_order_acquire);
|
||||
for (;;) {
|
||||
auto next = std::make_shared<Consumer_Map>(*current);
|
||||
@@ -830,6 +867,7 @@ void Plot::Private::publish(
|
||||
}
|
||||
}
|
||||
catch (...) {}
|
||||
return dispatch;
|
||||
}
|
||||
void Plot::Private::refresh_schedule() {
|
||||
if (!frame_timer.valid()) return;
|
||||
@@ -846,50 +884,24 @@ void Plot::Private::refresh_schedule() {
|
||||
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{
|
||||
submit_frame_request(Frame_Request{
|
||||
.issued_at = now,
|
||||
.time_milliseconds = std::chrono::duration<double, std::milli>(
|
||||
now - clock_origin).count(),
|
||||
.source = Frame_Request_Source::maximum_rate
|
||||
});
|
||||
arm_tick_consumer(lifetime);
|
||||
}
|
||||
void Plot::Private::submit_tick_request(Plot_Render_Tick tick) {
|
||||
const auto source = tick.source;
|
||||
const auto issued_at = tick.issued_at;
|
||||
if (!tick_requests.enqueue(std::move(tick))) throw std::bad_alloc{};
|
||||
void Plot::Private::submit_frame_request(Frame_Request request) {
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.record_request(source, issued_at);
|
||||
policy.submit_request(std::move(request));
|
||||
});
|
||||
consumer_work_generation.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) {
|
||||
const auto priority = [](Frame_Request_Source source) {
|
||||
switch (source) {
|
||||
case Frame_Request_Source::unspecified: return 0;
|
||||
case Frame_Request_Source::periodic: return 0;
|
||||
case Frame_Request_Source::maximum_rate: return 1;
|
||||
case Frame_Request_Source::immediate: return 2;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
if (deferred_tick) {
|
||||
const auto current_priority = priority(deferred_tick->source);
|
||||
const auto next_priority = priority(tick.source);
|
||||
with_frame_policy([](auto& policy) {
|
||||
policy.record_request_coalesced();
|
||||
});
|
||||
if (current_priority > next_priority ||
|
||||
(current_priority == next_priority &&
|
||||
deferred_tick->issued_at >= tick.issued_at))
|
||||
return;
|
||||
}
|
||||
deferred_tick = tick;
|
||||
arm_tick_consumer(lifetime);
|
||||
}
|
||||
void Plot::Private::arm_tick_consumer(std::weak_ptr<Plot> lifetime) {
|
||||
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] {
|
||||
aethera::schedule_task("frame.policy.consume", [lifetime] {
|
||||
const auto plot = lifetime.lock();
|
||||
if (!plot) return;
|
||||
try {
|
||||
@@ -900,18 +912,13 @@ void Plot::Private::arm_tick_consumer(std::weak_ptr<Plot> lifetime) {
|
||||
}
|
||||
});
|
||||
}
|
||||
void Plot::Private::release_render_admission(std::weak_ptr<Plot> lifetime) {
|
||||
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;
|
||||
void Plot::Private::request_next_maximum_rate_frame() {
|
||||
const auto& pacing = pacing_state();
|
||||
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{
|
||||
submit_frame_request(Frame_Request{
|
||||
.issued_at = now,
|
||||
.time_milliseconds = std::chrono::duration<double, std::milli>(
|
||||
now - clock_origin).count(),
|
||||
@@ -919,7 +926,6 @@ void Plot::Private::release_render_admission(std::weak_ptr<Plot> lifetime) {
|
||||
});
|
||||
}
|
||||
}
|
||||
arm_tick_consumer(std::move(lifetime));
|
||||
}
|
||||
void Plot::Private::retain_frame_policy_lifetime() {
|
||||
if (frame_policy_lifetime.load(std::memory_order_acquire)) return;
|
||||
@@ -944,43 +950,18 @@ void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
|
||||
consumer_work_generation.load(std::memory_order_acquire);
|
||||
consume_retired_frames();
|
||||
if (consume_frame_policy_events()) refresh_schedule();
|
||||
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);
|
||||
}
|
||||
auto production = with_frame_policy([](auto& policy) {
|
||||
return policy.begin_production();
|
||||
});
|
||||
if (production) render_frame(std::move(*production));
|
||||
consume_retired_frames();
|
||||
if (consume_frame_policy_events()) refresh_schedule();
|
||||
tick_task_scheduled.store(false, std::memory_order_release);
|
||||
if (consumer_work_generation.load(std::memory_order_acquire) !=
|
||||
observed_generation ||
|
||||
retired_frames.load(std::memory_order_acquire) ||
|
||||
(render_admission.load(std::memory_order_acquire) ==
|
||||
Render_Admission_State::ready && deferred_tick))
|
||||
retired_frames.load(std::memory_order_acquire))
|
||||
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;
|
||||
const auto& pacing = pacing_state();
|
||||
const bool accepted = pacing.render_enabled &&
|
||||
(tick.source == Frame_Request_Source::immediate ||
|
||||
(tick.source == Frame_Request_Source::periodic &&
|
||||
pacing.mode == Frame_Pacing_Mode::fixed_rate) ||
|
||||
(tick.source == Frame_Request_Source::maximum_rate &&
|
||||
pacing.mode == Frame_Pacing_Mode::maximum_rate));
|
||||
if (!accepted) {
|
||||
with_frame_policy([](auto& policy) {
|
||||
policy.record_policy_rejection();
|
||||
});
|
||||
return;
|
||||
}
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.record_request_accepted(tick.source);
|
||||
});
|
||||
render_frame(tick);
|
||||
}
|
||||
bool Plot::Private::mark_taskflow_trace(Render_Frame& frame) {
|
||||
auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire);
|
||||
while (remaining != 0) {
|
||||
@@ -1035,22 +1016,24 @@ nlohmann::json Plot::Private::trace_response(
|
||||
{"frames", std::move(frames)}
|
||||
};
|
||||
}
|
||||
void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
if (terminal_failure.load(std::memory_order_acquire)) return;
|
||||
void Plot::Private::render_frame(Frame_Production production) {
|
||||
const auto sequence = production.sequence;
|
||||
auto tick = std::move(production.request);
|
||||
if (terminal_failure.load(std::memory_order_acquire)) {
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.reject_production(sequence);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const auto streams = stream_snapshot();
|
||||
const auto& pacing = pacing_state();
|
||||
const bool direct_diagnostics_frame =
|
||||
streams.consumers->empty() &&
|
||||
tick.source == Frame_Request_Source::immediate;
|
||||
if (!pacing.render_enabled ||
|
||||
(streams.consumers->empty() && !direct_diagnostics_frame))
|
||||
return;
|
||||
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)) {
|
||||
keep_latest_tick(tick);
|
||||
if (streams.consumers->empty() && !direct_diagnostics_frame) {
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.reject_production(sequence);
|
||||
});
|
||||
return;
|
||||
}
|
||||
std::size_t slot_index{};
|
||||
@@ -1092,19 +1075,15 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
break;
|
||||
}
|
||||
if (!managed) {
|
||||
with_frame_policy([](auto& policy) {
|
||||
policy.record_frame_slot_backpressure();
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.defer_production(std::move(production));
|
||||
});
|
||||
keep_latest_tick(tick);
|
||||
/*
|
||||
* 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能
|
||||
* 立即 arm tick consumer,否则会在没有任何槽可用期间形成
|
||||
* consume -> no slot -> consume 的 Taskflow 任务风暴。真正的唤醒点
|
||||
* 是 retire_completed_frame:某个 consuming 槽变回 available 后只唤醒一次。
|
||||
*/
|
||||
render_admission.store(
|
||||
Render_Admission_State::frame_slots_exhausted,
|
||||
std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
retain_frame_policy_lifetime();
|
||||
@@ -1135,7 +1114,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
tick.width = streams.width;
|
||||
tick.height = streams.height;
|
||||
}
|
||||
const std::uint64_t sequence = next_frame_sequence++;
|
||||
const Frame_Identity identity{
|
||||
sequence, tick.sequence == 0 ? sequence : tick.sequence
|
||||
};
|
||||
@@ -1201,12 +1179,12 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
}
|
||||
});
|
||||
if (!result) {
|
||||
with_frame_policy([](auto& policy) {
|
||||
policy.record_scene_rejection();
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.reject_production(sequence);
|
||||
});
|
||||
rollback_unsubmitted();
|
||||
restore_taskflow_trace_claim();
|
||||
release_render_admission(lifetime);
|
||||
request_next_maximum_rate_frame();
|
||||
}
|
||||
else {
|
||||
taskflow_trace_claimed = false;
|
||||
@@ -1230,7 +1208,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
.submitted = [weak](not_null<Frame_3D*>, bool) {
|
||||
if (auto owner = weak.lock()) {
|
||||
try {
|
||||
owner->d->release_render_admission(weak);
|
||||
owner->d->request_next_maximum_rate_frame();
|
||||
}
|
||||
catch (...) {
|
||||
owner->d->fail(std::current_exception());
|
||||
@@ -1260,18 +1238,22 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
with_frame_policy([](auto& policy) {
|
||||
policy.record_scene_rejection();
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.reject_production(sequence);
|
||||
});
|
||||
rollback_unsubmitted();
|
||||
restore_taskflow_trace_claim();
|
||||
release_render_admission(lifetime);
|
||||
request_next_maximum_rate_frame();
|
||||
if (result == Render_Scene_3D::Render_Result::backend_unavailable) throw std::runtime_error("3D render backend became unavailable before submission");
|
||||
}
|
||||
catch (...) {
|
||||
rollback_unsubmitted();
|
||||
restore_taskflow_trace_claim();
|
||||
release_render_admission(lifetime);
|
||||
const auto active_sequence = pacing_state().in_flight_frame_sequence;
|
||||
if (active_sequence == sequence)
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.reject_production(sequence);
|
||||
});
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -1330,11 +1312,21 @@ void Plot::Private::publish_completed_frame(
|
||||
const auto published = std::make_shared<const Plot_Stream_Frame>(
|
||||
Plot_Stream_Frame{{}, std::move(pixels)});
|
||||
const auto publish_started = std::chrono::steady_clock::now();
|
||||
publish(std::move(published));
|
||||
const auto dispatch = publish(std::move(published));
|
||||
const auto publish_completed = std::chrono::steady_clock::now();
|
||||
with_frame_policy([&](auto& policy) {
|
||||
policy.submit_publication_dispatch({
|
||||
.started_at = publish_started,
|
||||
.completed_at = publish_completed,
|
||||
.sequence = identity.sequence,
|
||||
.asynchronous_feedback_count =
|
||||
dispatch.asynchronous_feedback_count,
|
||||
.succeeded = dispatch.succeeded});
|
||||
});
|
||||
frame->record(Frame_Trace_Measurement::plot_publish_ns,
|
||||
static_cast<std::uint64_t>(std::max<std::int64_t>(
|
||||
0, std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - publish_started)
|
||||
publish_completed - publish_started)
|
||||
.count())));
|
||||
auto expected = Frame_State::rendering;
|
||||
if (!managed->state.compare_exchange_strong(
|
||||
@@ -1359,7 +1351,7 @@ void Plot::Private::consume_completed_frame(not_null<Render_Frame*> frame) {
|
||||
/* Scene 完成即释放 Plot admission。媒体采样属于 Gallery 自己的独立时钟和
|
||||
* DAG,不再借 Plot 保存一套 post-publish 管线状态。 */
|
||||
if (std::holds_alternative<std::unique_ptr<Frame_2D>>(managed->frame))
|
||||
release_render_admission(lifetime);
|
||||
request_next_maximum_rate_frame();
|
||||
}
|
||||
void Plot::Private::consume_retired_frames() {
|
||||
for (;;) {
|
||||
@@ -1482,11 +1474,6 @@ void Plot::Private::finalize_retired_frame(not_null<Render_Frame*> frame) {
|
||||
std::memory_order_acquire))
|
||||
throw std::logic_error("retired Plot frame is not consuming");
|
||||
latest_statistics_frame.store(managed, std::memory_order_release);
|
||||
/* 物理槽是唯一背压原因;归还任意槽后只解除一次耗尽状态。 */
|
||||
auto admission = Render_Admission_State::frame_slots_exhausted;
|
||||
static_cast<void>(render_admission.compare_exchange_strong(
|
||||
admission, Render_Admission_State::ready,
|
||||
std::memory_order_acq_rel, std::memory_order_acquire));
|
||||
arm_tick_consumer(lifetime);
|
||||
release_frame_policy_lifetime_if_idle();
|
||||
if (captured_trace) {
|
||||
@@ -1515,7 +1502,7 @@ void Plot::ensure_started() {
|
||||
d->frame_timer = Frame_Scheduler::instance().make_timer(
|
||||
[weak](Frame_Scheduler::Tick tick) {
|
||||
if (const auto owner = weak.lock()) {
|
||||
owner->schedule_render(Plot_Render_Tick{
|
||||
owner->schedule_render(Frame_Request{
|
||||
.issued_at = tick.issued_at,
|
||||
.sequence = tick.sequence,
|
||||
.time_milliseconds = tick.time_milliseconds,
|
||||
@@ -1592,22 +1579,31 @@ void Plot::configure_stream(Stream_Id stream, std::uint32_t width,
|
||||
return;
|
||||
}
|
||||
}
|
||||
void Plot::schedule_render(Plot_Render_Tick tick) {
|
||||
if (!std::isfinite(tick.time_milliseconds) ||
|
||||
tick.time_milliseconds < 0.0)
|
||||
void Plot::schedule_render(Frame_Request request) {
|
||||
if (!std::isfinite(request.time_milliseconds) ||
|
||||
request.time_milliseconds < 0.0)
|
||||
throw std::invalid_argument(
|
||||
"render time_milliseconds must be finite and non-negative");
|
||||
if (tick.width == 0 || tick.height == 0) throw std::invalid_argument("render viewport must be non-zero");
|
||||
if (request.width == 0 || request.height == 0) throw std::invalid_argument("render viewport must be non-zero");
|
||||
ensure_started();
|
||||
if (d->terminal_failure.load(std::memory_order_acquire)) return;
|
||||
d->submit_tick_request(std::move(tick));
|
||||
d->submit_frame_request(std::move(request));
|
||||
}
|
||||
void Plot::submit_publication_feedback(
|
||||
Frame_Publication_Feedback feedback) {
|
||||
ensure_started();
|
||||
if (d->terminal_failure.load(std::memory_order_acquire)) return;
|
||||
d->with_frame_policy([&](auto& policy) {
|
||||
policy.submit_publication_feedback(std::move(feedback));
|
||||
});
|
||||
d->consumer_work_generation.fetch_add(1, std::memory_order_release);
|
||||
d->arm_tick_consumer(weak_from_this());
|
||||
}
|
||||
void Plot::render_once() {
|
||||
ensure_started();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto elapsed = now - d->clock_origin;
|
||||
schedule_render(Plot_Render_Tick{
|
||||
schedule_render(Frame_Request{
|
||||
.issued_at = now,
|
||||
.time_milliseconds =
|
||||
std::chrono::duration<double, std::milli>(elapsed).count(),
|
||||
@@ -1728,15 +1724,11 @@ nlohmann::json Plot::diagnostics() const {
|
||||
}
|
||||
const auto& pacing = d->pacing_state();
|
||||
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";
|
||||
}();
|
||||
const auto lifecycle_name = pacing.in_flight_frame_sequence != 0
|
||||
? "publication_in_flight"
|
||||
: pacing.pending_request
|
||||
? "request_pending"
|
||||
: "ready";
|
||||
nlohmann::json supported_formats = nlohmann::json::array();
|
||||
if (is_3d) {
|
||||
for (const auto format : Frame_3D::supported_pixel_formats) supported_formats.push_back(pixel_format_name(format));
|
||||
@@ -1783,7 +1775,7 @@ nlohmann::json Plot::diagnostics() const {
|
||||
}
|
||||
},
|
||||
{"frame_policy", frame_policy_state_json(pacing)},
|
||||
{"render_admission", admission_name},
|
||||
{"frame_lifecycle", lifecycle_name},
|
||||
{"frame_statistics", std::move(frame_statistics)},
|
||||
{"input_statistics", std::move(input_statistics)}
|
||||
};
|
||||
|
||||
+10
-11
@@ -27,14 +27,6 @@ struct Plot_Input_Event {
|
||||
std::uint32_t native_key{}; /* 浏览器原生按键码。 */
|
||||
bool auto_repeat{}; /* 是否为系统重复按键。 */
|
||||
};
|
||||
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}; /* 当前图在媒体图集中的固定像素高度。 */
|
||||
Frame_Request_Source source{Frame_Request_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */
|
||||
};
|
||||
enum struct Plot_Pixel_Layout : std::uint8_t {
|
||||
bgra8,
|
||||
rgba8
|
||||
@@ -54,10 +46,16 @@ struct Plot_Stream_Frame {
|
||||
std::string notification{}; /* 仅用于终止错误等低频控制通知;正常帧为空。 */
|
||||
std::shared_ptr<const Plot_Pixel_Frame> pixels{}; /* 每帧完成进度及其可选图集像素。 */
|
||||
};
|
||||
enum struct Plot_Frame_Publication : std::uint8_t {
|
||||
ignored, /* 订阅者不消费本像素帧,不参与发布反馈。 */
|
||||
completed, /* 订阅回调返回时已同步完成业务消费。 */
|
||||
asynchronous /* 订阅者已接管发布,并会执行帧策略反馈任务。 */
|
||||
};
|
||||
struct Plot final : public std::enable_shared_from_this<Plot> {
|
||||
public:
|
||||
using Stream_Id = std::uint64_t;
|
||||
using Stream_Handler = std::function<void(std::shared_ptr<const Plot_Stream_Frame>)>;
|
||||
using Stream_Handler = std::function<Plot_Frame_Publication(
|
||||
std::shared_ptr<const Plot_Stream_Frame>)>;
|
||||
using Json_Handler = std::function<void(nlohmann::json)>;
|
||||
struct Scene_View {
|
||||
public:
|
||||
@@ -72,7 +70,7 @@ public:
|
||||
const std::vector<std::string>& components) const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0;
|
||||
virtual void update(const Plot_Render_Tick& tick) = 0;
|
||||
virtual void update(const Frame_Request& request) = 0;
|
||||
};
|
||||
Plot(std::unique_ptr<render_2d::Render_Scene_2D> scene,
|
||||
std::unique_ptr<Scene_View> view);
|
||||
@@ -84,7 +82,8 @@ public:
|
||||
[[nodiscard]] Stream_Id subscribe(Stream_Handler handler);
|
||||
void unsubscribe(Stream_Id stream);
|
||||
void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height);
|
||||
void schedule_render(Plot_Render_Tick tick);
|
||||
void schedule_render(Frame_Request request);
|
||||
void submit_publication_feedback(Frame_Publication_Feedback feedback);
|
||||
void render_once();
|
||||
void submit_input(Plot_Input_Event event);
|
||||
[[nodiscard]] nlohmann::json schema();
|
||||
|
||||
Reference in New Issue
Block a user