Files
Aethera/mcp/core/runtime/Plot.cpp
T
2026-08-28 10:11:40 +08:00

1866 lines
83 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Plot.hpp"
#include "Renderable_Adapter.hpp"
#include "Taskflow_Trace_Json.hpp"
#include <Frame_Policy/Frame_Policy.hpp>
#include <Frame_Policy/Frame_Policy_3D.hpp>
#include <Frame_Policy/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>
#include <render_3D/Render_3D.hpp>
#include <render_3D/Gpu_Completion_State.hpp>
#include <render_common.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <concepts>
#include <exception>
#include <initializer_list>
#include <limits>
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
namespace aethera::web {
namespace {
using namespace render_2d;
using namespace render_3d;
using Scene_2D = Render_Scene_2D;
using Scene_3D = Render_Scene_3D;
constexpr std::uint16_t plot_stream_protocol_version{9};
constexpr std::size_t diagnostic_window_capacity{600};
std::string exception_description(const std::exception_ptr& failure) {
try {
if (failure) std::rethrow_exception(failure);
}
catch (const std::exception& error) {
return error.what();
}
catch (...) {
return "non-standard Plot failure";
}
return "empty Plot failure";
}
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");
return name;
}
std::optional<Frame_Pacing_Mode> parse_pacing_mode(std::string_view value) {
return magic_enum::enum_cast<Frame_Pacing_Mode>(value);
}
std::string_view pixel_format_name(render_2d::Pixel_Format format) {
const auto name = magic_enum::enum_name(format);
if (name.empty()) throw std::logic_error("unknown 2D pixel format");
return name;
}
std::string_view pixel_format_name(render_3d::Pixel_Format format) {
switch (format) {
case render_3d::Pixel_Format::rgba8_unorm:
return "rgba8";
}
throw std::logic_error("unknown 3D pixel format");
}
const Frame_Policy::State& frame_policy_common_state(
const Frame_Policy& policy) noexcept {
return policy.read_state<Frame_Policy::Base_Tag>();
}
const Frame_Policy::State& frame_policy_common_state(
const Frame_Policy_3D& policy) noexcept {
return policy.read_state<Frame_Policy_3D::Base_Tag>().common;
}
template <typename Policy>
nlohmann::json frame_policy_schema(const Policy& policy) {
const auto& current = frame_policy_common_state(policy);
return {
{"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"},
{"fields", nlohmann::json::array({
{{"key", "render_enabled"}, {"label", "持续渲染与采样"}, {"editor", "boolean"},
{"editable", true}, {"description", "控制当前 Scene 的周期刷新;画面隐藏不会修改此项。"},
{"technical_description", "Authoritative per-scene periodic render switch."},
{"value", current.render_enabled}},
{{"key", "video_enabled"}, {"label", "图集视频传输"}, {"editor", "boolean"},
{"editable", true}, {"description", "控制完成帧是否进入页面级采样器;2D BGRA 与 3D RGBA 均保持原生格式。"},
{"technical_description", "Authoritative tile publication switch for the shared gallery video."},
{"value", current.video_enabled}},
{{"key", "pacing_mode"}, {"label", "服务端帧策略"}, {"editor", "select"},
{"editable", true}, {"description", "只控制 Scene::render(Frame*) 的调用节奏;Scene 的 Frame 所有权与接口保持不变。"},
{"technical_description", "Per-scene frame pacing policy backed by the Kernel scheduler."},
{"value", pacing_mode_name(current.mode)},
{"options", nlohmann::json::array({
{{"value", "manual"}, {"label", "手动渲染"}},
{{"value", "fixed_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", "仅 fixed_rate 使用;maximum_rate 在完成准入释放后异步自驱下一帧。"},
{"technical_description", "Independent per-scene target frame rate."},
{"value", current.fixed_rate_fps}}
})}
};
}
template <typename Policy>
nlohmann::json write_frame_policy_prop(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"}};
const bool target = value.get<bool>();
if (key == "render_enabled") pacing.set_render_enabled(target);
else pacing.set_video_enabled(target);
return {{"success", true}, {"component", "frame-analysis"}, {"key", key},
{"value", target}};
}
if (key == "pacing_mode") {
if (!value.is_string())
return {{"success", false}, {"error", "pacing_mode requires a string"}};
const auto parsed = parse_pacing_mode(value.get_ref<const std::string&>());
if (!parsed)
return {{"success", false}, {"error", "unknown frame pacing mode"}};
pacing.set_mode(*parsed);
return {{"success", true}, {"component", "frame-analysis"}, {"key", key},
{"value", pacing_mode_name(*parsed)}};
}
if (key == "fixed_rate_fps") {
if (!value.is_number())
return {{"success", false}, {"error", "fixed_rate_fps requires a number"}};
const double next = value.get<double>();
if (!std::isfinite(next) || next < 0.1 || next > 100.0)
return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 100"}};
pacing.set_fixed_rate(next);
return {{"success", true}, {"component", "frame-analysis"}, {"key", key},
{"value", next}};
}
return {{"success", false}, {"error", "unknown frame runtime property"}};
}
nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) {
const auto milliseconds = [](std::uint64_t nanoseconds) {
return static_cast<double>(nanoseconds) / 1'000'000.0;
};
const auto ratio = [](std::uint64_t numerator, std::uint64_t denominator) {
return denominator == 0 ? 0.0 :
static_cast<double>(numerator) / static_cast<double>(denominator);
};
const auto observed_ns = state.observed_until_ns > state.observation_started_ns
? state.observed_until_ns - state.observation_started_ns : 0U;
const auto observed_seconds = static_cast<double>(observed_ns) / 1'000'000'000.0;
const auto rate = [observed_seconds](std::uint64_t count) {
return observed_seconds > 0.0
? static_cast<double>(count) / observed_seconds : 0.0;
};
const auto completion_span = state.last_completion_ns > state.first_completion_ns
? state.last_completion_ns - state.first_completion_ns : 0U;
const auto effective_fps = completion_span != 0 && state.completed_frame_count > 1
? static_cast<double>(state.completed_frame_count - 1U) *
1'000'000'000.0 / static_cast<double>(completion_span)
: 0.0;
const auto interval_mean = state.completion_interval_count == 0 ? 0.0 :
static_cast<double>(state.completion_interval_total_ns) /
static_cast<double>(state.completion_interval_count);
const auto interval_variance = state.completion_interval_count == 0 ? 0.0 :
std::max(0.0,
state.completion_interval_squared_total_ns2 /
static_cast<double>(state.completion_interval_count) -
interval_mean * interval_mean);
const auto target_achievement =
state.mode == Frame_Pacing_Mode::fixed_rate && state.fixed_rate_fps > 0.0
? effective_fps / state.fixed_rate_fps : 0.0;
return {
{"generation", state.generation},
{"configuration", {
{"mode", pacing_mode_name(state.mode)},
{"render_enabled", state.render_enabled},
{"video_enabled", state.video_enabled},
{"fixed_rate_fps", state.fixed_rate_fps}}},
{"observation", {
{"duration_ms", milliseconds(observed_ns)},
{"request_count", state.request_count},
{"submitted_frame_count", state.submitted_frame_count},
{"completed_frame_count", state.completed_frame_count},
{"active_frame_count", state.active_frame_count}}},
{"throughput", {
{"request_rate_fps", rate(state.request_count)},
{"submission_rate_fps", rate(state.submitted_frame_count)},
{"completion_rate_fps", effective_fps},
{"target_achievement_ratio", target_achievement},
{"latest_frame_interval_ms",
milliseconds(state.latest_completion_interval_ns)},
{"average_frame_interval_ms", milliseconds(
state.completion_interval_count == 0 ? 0U :
state.completion_interval_total_ns /
state.completion_interval_count)},
{"frame_interval_jitter_ms",
std::sqrt(interval_variance) / 1'000'000.0}}},
{"requests", {
{"periodic", state.periodic_request_count},
{"immediate", state.immediate_request_count},
{"maximum_rate", state.maximum_rate_request_count},
{"accepted", state.accepted_request_count},
{"coalesced", state.coalesced_request_count},
{"policy_rejected", state.policy_rejection_count},
{"frame_slot_backpressure", state.frame_slot_backpressure_count},
{"scene_rejected", state.scene_rejection_count},
{"acceptance_ratio", ratio(
state.accepted_request_count, state.request_count)},
{"coalescing_ratio", ratio(
state.coalesced_request_count, state.request_count)},
{"backpressure_ratio", ratio(
state.frame_slot_backpressure_count,
state.accepted_request_count)}}},
{"latency", {
{"latest_tick_queue_ms", milliseconds(state.latest_tick_queue_ns)},
{"average_tick_queue_ms", milliseconds(
state.submitted_frame_count == 0 ? 0U :
state.tick_queue_total_ns / state.submitted_frame_count)},
{"maximum_tick_queue_ms", milliseconds(state.maximum_tick_queue_ns)},
{"latest_completion_ms", milliseconds(
state.latest_completion_latency_ns)},
{"average_completion_ms", milliseconds(
state.completed_frame_count == 0 ? 0U :
state.completion_latency_total_ns /
state.completed_frame_count)},
{"maximum_completion_ms", milliseconds(
state.maximum_completion_latency_ns)}}},
{"last_frame", {
{"sequence", state.last_frame_sequence},
{"request_source", magic_enum::enum_name(
state.last_request_source)}}}
};
}
void append_statistic_json(nlohmann::json& output,
const Frame_Statistics_State& state) {
for (const auto statistic : magic_enum::enum_values<Frame_Statistic>()) {
if (statistic == Frame_Statistic::count) continue;
const auto& value =
state.values[static_cast<std::size_t>(statistic)];
if (value.count == 0) continue;
output[magic_enum::enum_name(statistic)] = {
{"count", value.count}, {"latest", value.latest},
{"minimum", value.minimum}, {"maximum", value.maximum},
{"average", value.average},
{"trimmed_average", value.trimmed_average},
{"variability", value.variability}, {"p50", value.p50},
{"p95", value.p95}, {"p99", value.p99}};
}
}
void append_event_statistics_json(nlohmann::json& output,
const Event_Statistics_State& state) {
for (const auto type : magic_enum::enum_values<Event_Type>()) {
auto& event = output[magic_enum::enum_name(type)];
const auto& values = state.values[static_cast<std::size_t>(type)];
for (const auto statistic : magic_enum::enum_values<Event_Statistic>()) {
if (statistic == Event_Statistic::count) continue;
const auto& value = values[static_cast<std::size_t>(statistic)];
if (value.count == 0) continue;
event[magic_enum::enum_name(statistic)] = {
{"count", value.count}, {"latest", value.latest},
{"minimum", value.minimum}, {"maximum", value.maximum},
{"average", value.average},
{"trimmed_average", value.trimmed_average},
{"variability", value.variability}, {"p50", value.p50},
{"p95", value.p95}, {"p99", value.p99}};
}
if (event.empty()) output.erase(std::string{magic_enum::enum_name(type)});
}
}
nlohmann::json datoviz_observation_json(
const Datoviz_Frame_Observation& value) {
const auto milliseconds = [](std::uint64_t nanoseconds) {
return static_cast<double>(nanoseconds) / 1'000'000.0;
};
nlohmann::json result{
{"render_sequence", value.render_sequence},
{"path", magic_enum::enum_name(value.path)},
{"gpu_timing_requested", value.gpu_timing_requested},
{"readback_requested", value.readback_requested},
{"timings_ms", {
{"render_domain_queue_wait",
milliseconds(value.render_domain_queue_wait_ns)},
{"apply", milliseconds(value.apply_ns)},
{"emit", milliseconds(value.emit_ns)},
{"execute", milliseconds(value.execute_ns)},
{"submit", milliseconds(value.submit_ns)},
{"gpu_fence_wait", milliseconds(value.gpu_fence_wait_ns)},
{"readback", milliseconds(value.readback_ns)}}},
{"traffic", {
{"uploaded_bytes", value.uploaded_bytes},
{"readback_bytes", value.readback_bytes}}},
{"frame_plan", {
{"resource_version", value.artifact_resource_version},
{"frame_index", value.artifact_frame_index},
{"status", value.artifact_status}}},
{"validation", {
{"performed", value.validation_performed},
{"ok", value.validation_ok},
{"code", value.validation_code},
{"command_index", value.validation_command_index}}}
};
if (value.gpu) {
result["gpu_ms"] = {
{"render", milliseconds(value.gpu->render_ns)},
{"transition", milliseconds(value.gpu->transition_ns)},
{"copy", milliseconds(value.gpu->copy_ns)},
{"total", milliseconds(value.gpu->total_ns)}};
}
if (!value.artifact_json.empty())
result["frame_plan"]["artifact_json"] = value.artifact_json;
return result;
}
}
nlohmann::json taskflow_trace_json(
const Taskflow_Frame_Trace& trace,
const nlohmann::json& captured_components,
const nlohmann::json& captured_backend) {
nlohmann::json markers = nlohmann::json::object();
for (const auto& marker : trace.markers)
markers[magic_enum::enum_name(marker.marker)] =
static_cast<double>(marker.elapsed_ns) / 1'000'000.0;
nlohmann::json measurements = nlohmann::json::object();
for (const auto& measurement : trace.measurements)
measurements[magic_enum::enum_name(measurement.measurement)] =
static_cast<double>(measurement.value_ns) / 1'000'000.0;
nlohmann::json graphs = nlohmann::json::array();
std::unordered_map<std::uint64_t, std::string> node_ids;
for (const auto& graph : trace.graphs) {
nlohmann::json nodes = nlohmann::json::array();
for (const auto& node : graph.nodes) {
node_ids.emplace(node.native_id, node.node_id);
nlohmann::json predecessors = nlohmann::json::array();
for (const auto native_id : node.predecessors)
predecessors.push_back(std::to_string(native_id));
nlohmann::json successors = nlohmann::json::array();
for (const auto native_id : node.successors)
successors.push_back(std::to_string(native_id));
nlohmann::json attributes = nlohmann::json::object();
for (const auto& [key, value] : node.attributes)
attributes[key] = value;
nlohmann::json encoded{
{"native_id", std::to_string(node.native_id)}, {"id", node.node_id},
{"parent_id", node.parent_node_id}, {"name", node.name},
{"type", node.type}, {"predecessors", std::move(predecessors)},
{"successors", std::move(successors)},
{"attributes", std::move(attributes)}};
const auto owner = encoded["attributes"].value(
"owner_component", std::string{});
if (!owner.empty() && captured_components.contains(owner)) {
const auto& captured = captured_components.at(owner);
encoded["owner"] = {
{"component", owner},
{"label", captured.value("label", owner)},
{"kind", captured.value("kind", std::string{})}};
encoded["prop"] = captured.value("prop", nlohmann::json::object());
encoded["state"] = captured.value("state", nlohmann::json::object());
}
nodes.push_back(std::move(encoded));
}
graphs.push_back({
{"stage", graph.stage}, {"name", graph.taskflow_name},
{"submitted_ms", graph.submitted_ms},
{"finished_ms", graph.finished_ms},
{"completed", graph.completed}, {"nodes", std::move(nodes)}});
}
nlohmann::json executions = nlohmann::json::array();
for (const auto& task : trace.tasks) {
const auto found = node_ids.find(task.native_id);
executions.push_back({
{"native_id", std::to_string(task.native_id)},
{"node_id", found == node_ids.end() ? std::string{} : found->second},
{"worker_id", task.worker_id},
{"worker_queue_size", task.worker_queue_size},
{"worker_queue_capacity", task.worker_queue_capacity},
{"ready_ms", task.ready_ms}, {"entered_ms", task.entered_ms},
{"started_ms", task.started_ms}, {"finished_ms", task.finished_ms},
{"completed_ms", task.completed_ms},
{"duration_ms", task.duration_ms},
{"cooperative_wait_ms", task.cooperative_wait_ms},
{"observer_entry_ms", task.observer_entry_ms},
{"observer_exit_ms", task.observer_exit_ms},
{"queue_wait_ms", task.queue_wait_ms}});
}
nlohmann::json result{
{"sequence", trace.identity.sequence},
{"correlation_id", trace.identity.correlation_id},
{"request_source", magic_enum::enum_name(trace.request_source)},
{"frame_policy", frame_policy_state_json(trace.frame_policy)},
{"created_time_unix_ns", trace.created_time_unix_ns},
{"worker_count", trace.worker_count},
{"markers", std::move(markers)},
{"measurements", std::move(measurements)},
{"graphs", std::move(graphs)},
{"executions", std::move(executions)}};
if (!captured_backend.empty()) result["datoviz"] = captured_backend;
return result;
}
namespace {
template <typename Scene_Object>
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
const auto dispatch = [&](auto event) {
scene.template submit_stream<aethera::Scene_Event_Stream_Tag>(std::move(event));
};
const auto apply_pointer = [&](auto& event) {
event.position = input.position;
event.global_position = input.global_position;
event.button = input.button;
event.buttons = input.buttons;
event.modifiers = input.modifiers;
};
switch (input.type) {
case Event_Type::pointer_move:
case Event_Type::pointer_press:
case Event_Type::pointer_release: {
auto event = scene.template make_event<Basic_Pointer_Event<Point_F>>(
input.type);
apply_pointer(*event);
dispatch(std::move(event));
break;
}
case Event_Type::wheel: {
auto event = scene.template make_event<Basic_Wheel_Event<Point_F>>();
apply_pointer(*event);
event->pixel_delta_x = input.pixel_delta_x;
event->pixel_delta_y = input.pixel_delta_y;
event->angle_delta_x = input.angle_delta_x;
event->angle_delta_y = input.angle_delta_y;
dispatch(std::move(event));
break;
}
case Event_Type::key_press:
case Event_Type::key_release: {
auto event = scene.template make_event<Key_Event>(input.type);
event->key = input.key;
event->native_key = input.native_key;
event->modifiers = input.modifiers;
event->auto_repeat = input.auto_repeat;
dispatch(std::move(event));
break;
}
default:
dispatch(scene.template make_event<Event>(input.type));
break;
}
}
}
struct Plot::Private {
using Scene = std::variant<std::unique_ptr<Scene_2D>, std::unique_ptr<Scene_3D>>;
using Frame = std::variant<std::unique_ptr<Frame_2D>, std::unique_ptr<Frame_3D>>;
enum struct Frame_State : std::uint8_t {
available,
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 的时刻。 */
std::chrono::steady_clock::time_point submitted_at{}; /* Scene 接受本逻辑帧的时刻。 */
Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */
std::atomic<Frame_State> state{Frame_State::available}; /* 本槽唯一生命周期状态。 */
std::atomic_size_t diagnostic_readers{}; /* 无锁诊断读取认领;非零时该槽不可复用。 */
std::uint64_t statistics_generation{}; /* 与本槽中完成帧统计共同发布。 */
Frame_Statistics_State statistics{}; /* 统计结果归属当前完成帧,不在 Plot 复制。 */
std::atomic<Managed_Frame*> retired_next{}; /* 无锁退役队列的槽内侵入链接。 */
};
struct Consumer {
Stream_Handler handler;
std::uint32_t width{};
std::uint32_t height{};
};
using Consumer_Map = std::unordered_map<Stream_Id, Consumer>;
struct Stream_Snapshot {
std::shared_ptr<const Consumer_Map> consumers;
std::uint32_t width{};
std::uint32_t height{};
};
std::unique_ptr<Scene_View> view;
std::once_flag start_once;
std::weak_ptr<Plot> lifetime{}; /* 仅用于 completion 后重新投递 Taskflow,避免在 Scene callback 内重入 render。 */
std::atomic<std::shared_ptr<const Consumer_Map>> consumers{
std::make_shared<const Consumer_Map>()}; /* 低频订阅修改发布不可变版本。 */
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 只有轻量时间轮节点,不持有线程。 */
static constexpr std::size_t scene_frame_capacity{3};
std::array<Managed_Frame, scene_frame_capacity> frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */
std::atomic<Managed_Frame*> latest_statistics_frame{}; /* 只定位权威帧槽,不保存统计副本。 */
std::atomic<Managed_Frame*> retired_frames{}; /* 多回调生产、唯一 Taskflow 任务消费。 */
std::atomic_bool retired_frame_task_scheduled{};
Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */
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<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};
/* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace,
* GET 直接读取已发布槽位,不复制或重排整个历史容器。 */
std::atomic_uint64_t taskflow_trace_control{};
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames> taskflow_trace_slots{};
std::atomic_size_t post_publish_trace_remaining{}; /* 仅捕获 publish 后外接 DAG 的剩余样本。 */
std::atomic_uint64_t post_publish_trace_control{}; /* 高 32 位 requested,低 32 位 captured。 */
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames> post_publish_trace_slots{};
Task_Node completion_tail{}; /* Scene 图内固定停在 plot.frame.publish。 */
Task_Graph post_publish_graph{"plot.post_publish"}; /* publish 后外接 DAG;不再占用 Scene render admission。 */
Task_Node post_publish_tail{};
bool has_post_publish_tail{};
std::atomic_bool post_publish_busy{};
std::vector<std::unique_ptr<Task_Graph>> completion_extensions{}; /* 生命周期覆盖 post-publish module 借用。 */
Frame_Statistics_Accumulator completed_frame_statistics{diagnostic_window_capacity};
std::uint64_t applied_statistics_generation{}; /* 仅完成帧退役任务读写。 */
std::atomic_uint64_t statistics_generation{}; /* reset 只推进代次,不触碰单写者累加器。 */
template <typename Scene_Object>
Private(std::unique_ptr<Scene_Object> value_scene,
std::unique_ptr<Scene_View> value_view)
: view(std::move(value_view)), scene(std::move(value_scene)) {
if constexpr (std::same_as<Scene_Object, Scene_2D>) {
auto built_policy = Frame_Policy::Builder<Frame_Policy>{}.build();
if (!built_policy)
throw std::logic_error("2D Frame Policy dependency graph is invalid");
frame_policy_2d = std::move(*built_policy);
}
else {
auto built_policy =
Frame_Policy_3D::Builder<Frame_Policy_3D>{}.build();
if (!built_policy)
throw std::logic_error("3D Frame Policy dependency graph is invalid");
frame_policy_3d = std::move(*built_policy);
frame_policy_3d->set_mode(Frame_Pacing_Mode::maximum_rate);
static_cast<void>(frame_policy_3d->consume_events());
}
for (auto& slot : frame_slots) {
if constexpr (std::same_as<Scene_Object, Scene_2D>)
slot.frame = std::make_unique<Frame_2D>(Frame_Identity{});
else
slot.frame = std::make_unique<Frame_3D>(Frame_Identity{});
}
if constexpr (std::same_as<Scene_Object, Scene_2D>) {
auto& completion = std::get<std::unique_ptr<Scene_2D>>(scene)
->completion_taskflow();
completion_tail = completion.add("plot.frame.publish", [this] {
publish_completed_frame();
});
completion_tail.describe("owner", "plot")
.describe("stage", "completed pixels publish");
}
}
template <typename Callback>
decltype(auto) with_frame_policy(Callback&& callback) {
if (frame_policy_2d)
return std::forward<Callback>(callback)(*frame_policy_2d);
return std::forward<Callback>(callback)(*frame_policy_3d);
}
template <typename Callback>
decltype(auto) with_frame_policy(Callback&& callback) const {
if (frame_policy_2d)
return std::forward<Callback>(callback)(*frame_policy_2d);
return std::forward<Callback>(callback)(*frame_policy_3d);
}
[[nodiscard]] const Frame_Policy::State& pacing_state() const noexcept {
if (frame_policy_2d)
return frame_policy_2d->read_state<Frame_Policy::Base_Tag>();
return frame_policy_3d->read_state<Frame_Policy_3D::Base_Tag>().common;
}
[[nodiscard]] bool consume_frame_policy_events() {
const bool configuration_changed = with_frame_policy(
[](auto& policy) { return policy.consume_events(); });
if (frame_policy_3d)
static_cast<void>(frame_policy_3d->consume_3d_events());
return configuration_changed;
}
[[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);
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);
void refresh_schedule();
void clock_tick(const Plot_Render_Tick& tick);
void render_frame(Plot_Render_Tick tick);
void publish_completed_frame(Render_Frame* completed = nullptr);
void consume_completed_frame(Render_Frame* frame);
void retire_completed_frame(Render_Frame* frame);
void consume_retired_frames();
void finalize_retired_frame(Render_Frame* frame);
void arm_retired_frame_consumer();
void attach_completion(std::unique_ptr<Task_Graph> completion);
[[nodiscard]] bool mark_taskflow_trace(Render_Frame& frame);
[[nodiscard]] bool mark_post_publish_taskflow_trace();
void store_trace(std::atomic_uint64_t& control,
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots,
const Taskflow_Frame_Trace& trace,
const nlohmann::json& captured_components = {},
const nlohmann::json& captured_backend = {});
[[nodiscard]] nlohmann::json trace_response(
const std::atomic_uint64_t& control,
const std::atomic_size_t& remaining,
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots) const;
void fail(std::exception_ptr failure) noexcept;
};
void Plot::Private::fail(std::exception_ptr failure) noexcept {
try {
auto description = std::make_shared<const std::string>(
exception_description(failure));
std::shared_ptr<const std::string> empty;
if (!terminal_failure.compare_exchange_strong(
empty, description, std::memory_order_acq_rel,
std::memory_order_acquire)) return;
const auto output = std::make_shared<const Plot_Stream_Frame>(
Plot_Stream_Frame{nlohmann::json{
{"kind", "plot_error"},
{"protocol", "aethera.video.frame"},
{"version", plot_stream_protocol_version},
{"message", *description}
}.dump(), {}});
publish(std::move(output));
}
catch (...) {}
}
nlohmann::json Plot::Private::schema() const {
auto result = view->schema();
auto analysis = with_frame_policy(
[](const auto& policy) { return frame_policy_schema(policy); });
if (frame_policy_3d) {
analysis["fields"].push_back({
{"key", "pipeline_capacity"}, {"label", "3D 帧槽容量"},
{"editor", "integer"}, {"editable", false}, {"value", 3},
{"description", "3D 独立策略最多允许三帧处于 Prepare、GPU 和退休阶段。"},
{"technical_description", "3D-only submitted-driven pipeline capacity; 2D policy is unchanged."}
});
}
const auto generator = view->data_generator_schema();
if (!generator.is_null()) analysis["data_generator"] = generator;
result["frame_analysis"] = std::move(analysis);
return result;
}
Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const {
Stream_Snapshot result;
result.consumers = consumers.load(std::memory_order_acquire);
for (const auto& [id, consumer] : *result.consumers) {
static_cast<void>(id);
if (consumer.width == 0 || consumer.height == 0) continue;
result.width = std::max(result.width, consumer.width);
result.height = std::max(result.height, consumer.height);
}
result.width = std::clamp(result.width == 0 ? 320U : result.width, 160U, 1920U) & ~1U;
result.height = std::clamp(result.height == 0 ? 192U : result.height, 120U, 1080U) & ~1U;
return result;
}
void Plot::Private::publish(
std::shared_ptr<const Plot_Stream_Frame> frame) noexcept {
if (!frame) return;
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);
}
catch (...) {
failed_consumers.push_back(id);
}
}
if (failed_consumers.empty()) return;
auto current = consumers.load(std::memory_order_acquire);
for (;;) {
auto next = std::make_shared<Consumer_Map>(*current);
for (const auto id : failed_consumers) next->erase(id);
std::shared_ptr<const Consumer_Map> desired = next;
if (consumers.compare_exchange_weak(
current, desired, std::memory_order_release,
std::memory_order_acquire))
break;
}
}
catch (...) {}
}
void Plot::Private::refresh_schedule() {
if (!frame_timer.valid()) return;
const auto current_consumers = consumers.load(std::memory_order_acquire);
const auto& pacing = pacing_state();
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 = 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{};
with_frame_policy([&](auto& policy) {
policy.record_request(source, issued_at);
});
tick_request_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;
}
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] {
const auto plot = lifetime.lock();
if (!plot) return;
try { plot->d->consume_tick(lifetime); }
catch (...) { plot->d->fail(std::current_exception()); }
});
}
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;
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{
.issued_at = now,
.time_milliseconds = std::chrono::duration<double, std::milli>(
now - clock_origin).count(),
.source = Frame_Request_Source::maximum_rate});
}
}
arm_tick_consumer(std::move(lifetime));
}
void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
if (consume_frame_policy_events()) refresh_schedule();
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);
}
if (consume_frame_policy_events()) refresh_schedule();
tick_task_scheduled.store(false, std::memory_order_release);
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;
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) {
if (taskflow_trace_remaining.compare_exchange_weak(
remaining, remaining - 1, std::memory_order_acq_rel,
std::memory_order_acquire)) {
frame.request_taskflow_trace();
return true;
}
}
return false;
}
bool Plot::Private::mark_post_publish_taskflow_trace() {
auto remaining = post_publish_trace_remaining.load(std::memory_order_acquire);
while (remaining != 0) {
if (post_publish_trace_remaining.compare_exchange_weak(
remaining, remaining - 1, std::memory_order_acq_rel,
std::memory_order_acquire))
return true;
}
return false;
}
void Plot::Private::store_trace(
std::atomic_uint64_t& control,
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots,
const Taskflow_Frame_Trace& value,
const nlohmann::json& captured_components,
const nlohmann::json& captured_backend) {
auto trace = std::make_shared<const nlohmann::json>(
taskflow_trace_json(value, captured_components, captured_backend));
auto state = control.load(std::memory_order_acquire);
for (;;) {
const auto requested = static_cast<std::uint32_t>(state >> 32U);
const auto captured = static_cast<std::uint32_t>(state);
if (captured >= requested) return;
slots[captured].store(trace, std::memory_order_release);
const auto next = (static_cast<std::uint64_t>(requested) << 32U) |
static_cast<std::uint64_t>(captured + 1U);
if (control.compare_exchange_weak(
state, next, std::memory_order_release,
std::memory_order_acquire))
return;
}
}
nlohmann::json Plot::Private::trace_response(
const std::atomic_uint64_t& control,
const std::atomic_size_t& remaining,
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots) const {
nlohmann::json frames = nlohmann::json::array();
const auto state = control.load(std::memory_order_acquire);
const auto requested = static_cast<std::uint32_t>(state >> 32U);
const auto captured = static_cast<std::uint32_t>(state);
for (std::uint32_t index = 0; index < captured; ++index)
if (const auto trace = slots[index].load(std::memory_order_acquire))
frames.push_back(*trace);
const auto left = remaining.load(std::memory_order_acquire);
return {
{"protocol", "aethera.taskflow.frames"}, {"version", 1},
{"requested", requested}, {"remaining", left},
{"captured", frames.size()},
{"complete", requested != 0 && frames.size() == requested},
{"frames", std::move(frames)}};
}
void Plot::Private::render_frame(Plot_Render_Tick tick) {
if (terminal_failure.load(std::memory_order_acquire)) return;
const auto streams = stream_snapshot();
const auto& pacing = pacing_state();
if (!pacing.render_enabled || streams.consumers->empty()) 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);
return;
}
std::size_t slot_index{};
Managed_Frame* managed{};
/*
* 只有 rendering 槽受 Scene 不可重入门约束;consuming 槽表示上一帧
* 已经完成 Plot 像素发布,外接 H264/WebRTC 仍可继续持有该物理帧的
* 诊断生命周期。只要还有 available 槽,下一帧即可进入。
*/
for (std::size_t index = 0; index < frame_slots.size(); ++index) {
auto* candidate = &frame_slots[index];
auto* published = candidate;
const bool was_latest = latest_statistics_frame.compare_exchange_strong(
published, nullptr, std::memory_order_acq_rel,
std::memory_order_acquire);
if (candidate->diagnostic_readers.load(std::memory_order_acquire) != 0) {
if (was_latest) {
Managed_Frame* empty{};
static_cast<void>(latest_statistics_frame.compare_exchange_strong(
empty, candidate, std::memory_order_release,
std::memory_order_relaxed));
}
continue;
}
auto expected = Frame_State::available;
if (!candidate->state.compare_exchange_strong(
expected, Frame_State::rendering,
std::memory_order_acq_rel, std::memory_order_acquire)) {
if (was_latest) {
Managed_Frame* empty{};
static_cast<void>(latest_statistics_frame.compare_exchange_strong(
empty, candidate, std::memory_order_release,
std::memory_order_relaxed));
}
continue;
}
slot_index = index;
managed = &frame_slots[index];
break;
}
if (!managed) {
with_frame_policy([](auto& policy) {
policy.record_frame_slot_backpressure();
});
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;
}
managed->presentation_time =
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::duration<double, std::milli>(tick.time_milliseconds));
managed->tick_issued_at = tick.issued_at;
managed->submitted_at = {};
const auto rollback_unsubmitted = [this, slot_index] {
auto& slot = frame_slots[slot_index];
auto expected = Frame_State::rendering;
static_cast<void>(slot.state.compare_exchange_strong(
expected, Frame_State::available, std::memory_order_acq_rel,
std::memory_order_acquire));
};
bool taskflow_trace_claimed{};
const auto restore_taskflow_trace_claim = [this, &taskflow_trace_claimed] {
if (!std::exchange(taskflow_trace_claimed, false)) return;
taskflow_trace_remaining.fetch_add(1, std::memory_order_release);
};
try {
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};
const auto request_source = tick.source;
const auto& policy_state = pacing_state();
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,
request_source, policy_state);
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, request_source,
policy_state);
logical_frame = frame_3d.get();
}
taskflow_trace_claimed = mark_taskflow_trace(*logical_frame);
/*
* 各图的采样、网格构造和属性读取都在 Plot 自己的准备域完成。
* 进入 Scene::render 后只剩已经准备好的 Visual 批次与轻量提交;
* 共享 Render Domain 不承担业务数据生成。
*/
logical_frame->mark(Frame_Trace_Marker::plot_update_started);
const auto update_started = std::chrono::steady_clock::now();
view->update(tick);
const auto update_elapsed = std::chrono::steady_clock::now() - update_started;
logical_frame->mark(Frame_Trace_Marker::plot_update_finished);
const auto tick_queue_elapsed = tick.issued_at.time_since_epoch().count() == 0
? std::chrono::steady_clock::duration::zero()
: update_started - tick.issued_at;
const auto record_plot_measurements = [&](Render_Frame& frame) {
const auto nanoseconds = [](std::chrono::steady_clock::duration duration) {
return static_cast<std::uint64_t>(std::max<std::int64_t>(0,
std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count()));
};
frame.record(Frame_Trace_Measurement::plot_tick_queue_ns,
nanoseconds(tick_queue_elapsed));
frame.record(Frame_Trace_Measurement::plot_update_ns,
nanoseconds(update_elapsed));
};
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
auto& output = *std::get<std::unique_ptr<Frame_2D>>(managed->frame);
record_plot_measurements(output);
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(tick.width), static_cast<int>(tick.height)});
const auto result = (*scene_2d)->render(&output);
if (!result) {
with_frame_policy([](auto& policy) {
policy.record_scene_rejection();
});
rollback_unsubmitted();
restore_taskflow_trace_claim();
release_render_admission(lifetime);
} else {
taskflow_trace_claimed = false;
managed->submitted_at = std::chrono::steady_clock::now();
with_frame_policy([&](auto& policy) {
policy.record_frame_submitted(
sequence, request_source,
managed->submitted_at - managed->tick_issued_at);
});
}
return;
}
auto& output = *std::get<std::unique_ptr<Frame_3D>>(managed->frame);
record_plot_measurements(output);
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height});
const auto result = scene_3d->render(&output);
if (result == Render_Scene_3D::Render_Result::submitted) {
taskflow_trace_claimed = false;
managed->submitted_at = std::chrono::steady_clock::now();
with_frame_policy([&](auto& policy) {
policy.record_frame_submitted(
sequence, request_source,
managed->submitted_at - managed->tick_issued_at);
});
return;
}
with_frame_policy([](auto& policy) {
policy.record_scene_rejection();
});
rollback_unsubmitted();
restore_taskflow_trace_claim();
release_render_admission(lifetime);
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);
throw;
}
}
void Plot::Private::publish_completed_frame(Render_Frame* completed) {
Render_Frame* frame{};
Managed_Frame* managed{};
for (std::size_t index = 0; index < frame_slots.size(); ++index) {
if (frame_slots[index].state.load(std::memory_order_acquire) !=
Frame_State::rendering)
continue;
if (managed)
throw std::logic_error("Plot has multiple frames in Scene rendering");
frame = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); },
frame_slots[index].frame);
if (completed && frame != completed) continue;
managed = &frame_slots[index];
if (completed) break;
}
if (!managed)
throw std::logic_error("Scene completion graph has no rendering Plot frame");
const auto& pacing = pacing_state();
const auto identity = frame->identity();
Frame_Identity rendered_identity = identity;
std::shared_ptr<const std::vector<std::byte>> pixel_storage;
Plot_Pixel_Layout pixel_layout{Plot_Pixel_Layout::rgba8};
std::uint32_t width{};
std::uint32_t height{};
if (auto *frame_2d =
std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
pixel_layout = Plot_Pixel_Layout::bgra8;
const auto image = (*frame_2d)->image();
width = static_cast<std::uint32_t>(image.width);
height = static_cast<std::uint32_t>(image.height);
if (pacing.video_enabled) {
auto output = (*frame_2d)->output_pixels();
pixel_storage = std::make_shared<const std::vector<std::byte>>(
std::move(output.bytes));
width = static_cast<std::uint32_t>(output.width);
height = static_cast<std::uint32_t>(output.height);
}
} else {
auto &frame_3d = std::get<std::unique_ptr<Frame_3D>>(managed->frame);
rendered_identity = frame_3d->rendered_identity();
const auto extent = frame_3d->extent();
width = extent.width;
height = extent.height;
if (pacing.video_enabled && frame_3d->output() == Frame_3D_Output::pixels)
pixel_storage = frame_3d->share_pixels();
}
auto pixels = std::make_shared<const Plot_Pixel_Frame>(Plot_Pixel_Frame{
std::move(pixel_storage), pixel_layout, managed->presentation_time,
identity.sequence, identity.correlation_id, rendered_identity.sequence,
rendered_identity.correlation_id, width, height});
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));
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)
.count())));
auto expected = Frame_State::rendering;
if (!managed->state.compare_exchange_strong(
expected, Frame_State::consuming, std::memory_order_acq_rel,
std::memory_order_acquire))
throw std::logic_error("Plot frame left rendering before pixel publish");
}
void Plot::Private::consume_completed_frame(Render_Frame* frame) {
if (!frame)
throw std::invalid_argument("Plot received a null completed frame");
Managed_Frame* managed{};
for (auto& slot : frame_slots) {
auto* address = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); },
slot.frame);
if (address != frame) continue;
if (slot.state.load(std::memory_order_acquire) != Frame_State::consuming)
throw std::logic_error("completed Plot frame was not published");
managed = &slot;
break;
}
if (!managed)
throw std::logic_error("frame callback has no owned Plot frame");
/*
* Scene 已在调用本 callback 前释放自己的 render admission;这里同步
* 释放 Plot 的 view/update 门,并立刻唤醒 busy 期间保留的 latest tick。
* 之后外接 DAG 仍在当前 Frame trace 内执行,但不会阻塞下一帧渲染。
*/
if (std::holds_alternative<std::unique_ptr<Frame_2D>>(managed->frame))
release_render_admission(lifetime);
if (post_publish_graph.empty()) return;
bool expected = false;
if (!post_publish_busy.compare_exchange_strong(
expected, true, std::memory_order_acq_rel,
std::memory_order_acquire))
return;
/*
* 外接图异步提交;上一轮尚未完成时直接合并到 sampler 内的 latest,绝不
* 在 Taskflow Worker 内等待。诊断使用独立 Render_Frame 保存外接图观察
* 窗口,因此 Scene 的物理帧可立即退役并被下一次渲染复用。
*/
const bool local_post_publish_trace = mark_post_publish_taskflow_trace();
auto trace_frame = local_post_publish_trace
? std::make_shared<Render_Frame>(frame->identity())
: std::shared_ptr<Render_Frame>{};
bool local_trace_started{};
if (trace_frame) {
trace_frame->request_taskflow_trace();
local_trace_started =
aethera::detail::begin_taskflow_trace(*trace_frame);
}
const auto weak = lifetime;
auto completion = [weak, trace_frame, local_trace_started] {
const auto owner = weak.lock();
if (!owner) return;
if (local_trace_started) {
aethera::detail::finish_taskflow_trace(*trace_frame);
owner->d->store_trace(
owner->d->post_publish_trace_control,
owner->d->post_publish_trace_slots,
trace_frame->take_taskflow_trace());
}
owner->d->post_publish_busy.store(false, std::memory_order_release);
};
try {
if (trace_frame)
aethera::detail::run_taskflow(
post_publish_graph, *trace_frame, "plot.post_publish",
std::move(completion));
else
aethera::detail::run_taskflow(
post_publish_graph, std::move(completion));
}
catch (...) {
if (local_trace_started)
aethera::detail::finish_taskflow_trace(*trace_frame);
if (local_post_publish_trace)
post_publish_trace_remaining.fetch_add(1, std::memory_order_release);
post_publish_busy.store(false, std::memory_order_release);
throw;
}
}
void Plot::Private::arm_retired_frame_consumer() {
bool expected{};
if (!retired_frame_task_scheduled.compare_exchange_strong(
expected, true, std::memory_order_acq_rel,
std::memory_order_acquire)) return;
const auto weak = lifetime;
schedule_task("web.plot.frame.retire", [weak] {
if (const auto owner = weak.lock())
owner->d->consume_retired_frames();
});
}
void Plot::Private::consume_retired_frames() {
for (;;) {
auto* list = retired_frames.exchange(nullptr, std::memory_order_acq_rel);
if (!list) break;
std::vector<Managed_Frame*> frames;
while (list) {
auto* next = list->retired_next.exchange(
nullptr, std::memory_order_relaxed);
frames.push_back(list);
list = next;
}
std::ranges::sort(frames, {}, [](const Managed_Frame* managed) {
return std::visit(
[](const auto& value) { return value->identity().sequence; },
managed->frame);
});
for (auto* managed : frames) {
auto* frame = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); },
managed->frame);
finalize_retired_frame(frame);
}
}
retired_frame_task_scheduled.store(false, std::memory_order_release);
if (retired_frames.load(std::memory_order_acquire))
arm_retired_frame_consumer();
}
void Plot::Private::retire_completed_frame(Render_Frame* frame) {
if (!frame)
throw std::invalid_argument("Plot received a null retired frame");
Managed_Frame* managed{};
for (auto& slot : frame_slots) {
auto* address = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); },
slot.frame);
if (address != frame) continue;
managed = &slot;
break;
}
if (!managed)
throw std::logic_error("retired frame has no owned Plot slot");
auto* head = retired_frames.load(std::memory_order_relaxed);
do {
managed->retired_next.store(head, std::memory_order_relaxed);
} while (!retired_frames.compare_exchange_weak(
head, managed, std::memory_order_release,
std::memory_order_relaxed));
arm_retired_frame_consumer();
}
void Plot::Private::finalize_retired_frame(Render_Frame* frame) {
Managed_Frame* managed{};
for (auto& slot : frame_slots) {
auto* address = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); },
slot.frame);
if (address == frame) {
managed = &slot;
break;
}
}
if (!managed)
throw std::logic_error("retired frame has no owned Plot slot");
with_frame_policy([&](auto& policy) {
policy.record_frame_completed(
frame->identity().sequence,
managed->submitted_at.time_since_epoch().count() == 0
? std::chrono::steady_clock::duration::zero()
: std::chrono::steady_clock::now() - managed->submitted_at);
});
const auto statistics_generation_value =
statistics_generation.load(std::memory_order_acquire);
if (applied_statistics_generation != statistics_generation_value) {
completed_frame_statistics.reset();
applied_statistics_generation = statistics_generation_value;
}
managed->statistics = completed_frame_statistics.submit(*frame);
managed->statistics_generation = statistics_generation_value;
std::optional<Taskflow_Frame_Trace> captured_trace;
nlohmann::json captured_components;
nlohmann::json captured_backend;
if (frame->taskflow_trace_requested()) {
captured_trace.emplace(frame->take_taskflow_trace());
std::unordered_set<std::uint64_t> executed_nodes;
for (const auto& execution : captured_trace->tasks)
executed_nodes.insert(execution.native_id);
std::vector<std::string> executed_components;
for (const auto& graph : captured_trace->graphs) {
for (const auto& node : graph.nodes) {
if (!executed_nodes.contains(node.native_id)) continue;
const auto owner = std::ranges::find(
node.attributes, "owner_component",
&std::pair<std::string, std::string>::first);
if (owner == node.attributes.end() || owner->second.empty() ||
std::ranges::find(executed_components, owner->second) !=
executed_components.end()) continue;
executed_components.push_back(owner->second);
}
}
captured_components = view->capture_components(executed_components);
if (auto* frame_3d = dynamic_cast<render_3d::Frame_3D*>(frame)) {
if (auto observation = frame_3d->take_datoviz_observation())
captured_backend = datoviz_observation_json(*observation);
}
}
auto expected = Frame_State::consuming;
if (!managed->state.compare_exchange_strong(
expected, Frame_State::available, std::memory_order_acq_rel,
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);
if (captured_trace) {
auto owner = lifetime;
schedule_task("plot.taskflow.serialize", [owner,
trace = std::move(*captured_trace),
components = std::move(captured_components),
backend = std::move(captured_backend)]() mutable {
if (const auto plot = owner.lock())
plot->d->store_trace(
plot->d->taskflow_trace_control,
plot->d->taskflow_trace_slots, trace, components,
backend);
});
}
}
void Plot::Private::attach_completion(
std::unique_ptr<Task_Graph> completion) {
if (!completion || completion->empty())
throw std::invalid_argument("Plot completion pipeline is empty");
completion_extensions.push_back(std::move(completion));
auto extension = post_publish_graph.compose(
completion_extensions.back()->name(), *completion_extensions.back());
extension.describe("owner", "plot")
.describe("stage", "post-publish frame pipeline extension");
if (has_post_publish_tail) post_publish_tail.precede(extension);
post_publish_tail = std::move(extension);
has_post_publish_tail = true;
}
Plot::Plot(std::unique_ptr<Scene_2D> scene,
std::unique_ptr<Scene_View> view)
: d(std::make_unique<Private>(std::move(scene), std::move(view))) {}
Plot::Plot(std::unique_ptr<Scene_3D> scene,
std::unique_ptr<Scene_View> view)
: d(std::make_unique<Private>(std::move(scene), std::move(view))) {}
Plot::~Plot() = default;
void Plot::attach_scene_completion(
std::unique_ptr<Task_Graph> completion) {
d->attach_completion(std::move(completion));
}
void Plot::ensure_started() {
std::call_once(d->start_once, [this] {
const auto weak = weak_from_this();
d->lifetime = weak;
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{
.issued_at = tick.issued_at,
.sequence = tick.sequence,
.time_milliseconds = tick.time_milliseconds,
.source = Frame_Request_Source::periodic});
}
});
/*
* 2D 的 callback 在 Scene render admission 已释放后运行:先执行所有
* post-publish 外接 DAGScene 完成 frame_ready 与 trace 收口后,再由
* retired callback 归还物理槽。这样 H264(N) 可与 Render(N+1) 重叠。
*/
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) {
(*scene)->set_frame_callback([weak](Frame_2D* frame) {
if (auto owner = weak.lock()) {
try { owner->d->consume_completed_frame(frame); }
catch (...) { owner->d->fail(std::current_exception()); }
}
});
(*scene)->set_frame_retired_callback([weak](Frame_2D* frame) {
if (auto owner = weak.lock()) {
try { owner->d->retire_completed_frame(frame); }
catch (...) { owner->d->fail(std::current_exception()); }
}
});
} else {
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(d->scene);
scene_3d->set_submitted_frame_callback(
[weak](Frame_3D* frame, bool overlaps_gpu) {
if (auto owner = weak.lock()) {
try {
owner->d->frame_policy_3d->record_gpu_submitted(
frame->identity().sequence, overlaps_gpu);
owner->d->release_render_admission(weak);
}
catch (...) { owner->d->fail(std::current_exception()); }
}
});
scene_3d->set_frame_callback(
[weak](Frame_3D* frame) {
if (auto owner = weak.lock()) {
try {
owner->d->publish_completed_frame(frame);
owner->d->frame_policy_3d->record_gpu_completed(
frame->identity().sequence);
owner->d->consume_completed_frame(frame);
owner->d->retire_completed_frame(frame);
}
catch (...) { owner->d->fail(std::current_exception()); }
}
});
}
/* callback 必须先于周期时钟安装,避免首帧在初始化窗口进入 Scene。 */
d->refresh_schedule();
});
}
Plot::Stream_Id Plot::subscribe(Stream_Handler handler) {
if (!handler)
throw std::invalid_argument("Plot subscription requires a handler");
ensure_started();
const auto id = d->next_stream_id.fetch_add(1, std::memory_order_relaxed);
const auto notification = handler;
auto current = d->consumers.load(std::memory_order_acquire);
for (;;) {
auto next = std::make_shared<Private::Consumer_Map>(*current);
next->emplace(id, Private::Consumer{handler});
std::shared_ptr<const Private::Consumer_Map> desired = next;
if (d->consumers.compare_exchange_weak(
current, desired, std::memory_order_release,
std::memory_order_acquire))
break;
}
d->refresh_schedule();
if (d->terminal_failure.load(std::memory_order_acquire)) {
const auto failure = d->terminal_failure.load(std::memory_order_acquire);
try {
notification(std::make_shared<const Plot_Stream_Frame>(
Plot_Stream_Frame{nlohmann::json{
{"kind", "plot_error"},
{"protocol", "aethera.video.frame"},
{"version", plot_stream_protocol_version},
{"message", failure ? *failure : "Plot unavailable"}
}.dump(), {}}));
}
catch (...) {
unsubscribe(id);
}
}
return id;
}
void Plot::unsubscribe(Stream_Id stream) {
auto current = d->consumers.load(std::memory_order_acquire);
while (current->contains(stream)) {
auto next = std::make_shared<Private::Consumer_Map>(*current);
next->erase(stream);
std::shared_ptr<const Private::Consumer_Map> desired = next;
if (d->consumers.compare_exchange_weak(
current, desired, std::memory_order_release,
std::memory_order_acquire))
break;
}
d->refresh_schedule();
}
void Plot::configure_stream(Stream_Id stream, std::uint32_t width,
std::uint32_t height) {
auto current = d->consumers.load(std::memory_order_acquire);
for (;;) {
const auto found = current->find(stream);
if (found == current->end()) return;
auto next = std::make_shared<Private::Consumer_Map>(*current);
auto& consumer = next->at(stream);
consumer.width = width;
consumer.height = height;
std::shared_ptr<const Private::Consumer_Map> desired = next;
if (d->consumers.compare_exchange_weak(
current, desired, std::memory_order_release,
std::memory_order_acquire))
return;
}
}
void Plot::schedule_render(Plot_Render_Tick tick) {
ensure_started();
if (d->terminal_failure.load(std::memory_order_acquire)) return;
d->submit_tick_request(std::move(tick));
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{
.issued_at = now,
.time_milliseconds =
std::chrono::duration<double, std::milli>(elapsed).count(),
.source = Frame_Request_Source::immediate});
}
void Plot::submit_input(Plot_Input_Event event) {
ensure_started();
if (d->terminal_failure.load(std::memory_order_acquire)) return;
/*
* WebSocket 线程只向 Scene 的当前事件缓冲追加一个由 Scene
* memory_resource 分配的基类指针。Prepare 边界交换完整批次,
* Scene 在 Renderable 完成消费时按 Event_Type 增量统计,并随自身
* State 双缓冲发布;Web 层只在低频 diagnostics 请求中读取结果。
*/
try {
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene))
dispatch_plot_input(**scene_2d, event);
else
dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene), event);
}
catch (...) {
d->fail(std::current_exception());
}
}
nlohmann::json Plot::schema() {
ensure_started();
return d->schema();
}
nlohmann::json Plot::write_prop(std::string_view component,
std::string_view key,
const nlohmann::json& value) {
ensure_started();
if (component != "frame-analysis")
return d->view->write_prop(component, key, value);
auto result = d->with_frame_policy([&](auto& policy) {
return write_frame_policy_prop(policy, key, value);
});
if (result.value("success", false))
d->arm_tick_consumer(weak_from_this());
return result;
}
nlohmann::json Plot::component_state(std::string_view component) const {
return d->view->component_state(component);
}
nlohmann::json Plot::generate_data(const nlohmann::json& input) {
ensure_started();
return d->view->generate_data(input);
}
nlohmann::json Plot::diagnostics() const {
nlohmann::json frame_statistics = nlohmann::json::object();
nlohmann::json input_statistics = nlohmann::json::object();
Frame_Identity identity{};
std::uint64_t created_time_unix_ns{};
std::uint64_t dropped_sequences{};
double frame_rate{};
bool is_3d{};
const auto read_scene_statistics = [&](const auto& state) {
append_event_statistics_json(input_statistics, state.event_statistics);
};
std::visit([&](const auto& scene) {
using Scene_Pointer = std::remove_cvref_t<decltype(scene)>;
if constexpr (std::same_as<Scene_Pointer, std::unique_ptr<Scene_2D>>) {
scene->template access_state<Render_Scene_2D::Base_Tag>(
read_scene_statistics);
} else {
is_3d = true;
scene->template access_state<Render_Scene_3D::Base_Tag>(
read_scene_statistics);
}
}, d->scene);
{
const auto generation =
d->statistics_generation.load(std::memory_order_acquire);
auto* completed_frame =
d->latest_statistics_frame.load(std::memory_order_acquire);
Frame_Statistics_State statistics{};
if (completed_frame) {
completed_frame->diagnostic_readers.fetch_add(
1, std::memory_order_acq_rel);
if (d->latest_statistics_frame.load(std::memory_order_acquire) ==
completed_frame &&
completed_frame->state.load(std::memory_order_acquire) ==
Private::Frame_State::available &&
completed_frame->statistics_generation == generation)
statistics = completed_frame->statistics;
completed_frame->diagnostic_readers.fetch_sub(
1, std::memory_order_release);
}
append_statistic_json(frame_statistics, statistics);
identity = statistics.identity;
created_time_unix_ns = statistics.created_time_unix_ns;
dropped_sequences = statistics.dropped_sequences;
const auto& interval = statistics.values[
static_cast<std::size_t>(Frame_Statistic::frame_interval_ms)];
frame_rate = interval.trimmed_average > 0.0
? 1'000.0 / interval.trimmed_average : 0.0;
}
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";
}();
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));
} else {
for (const auto format : Frame_2D::supported_pixel_formats)
supported_formats.push_back(pixel_format_name(format));
}
const auto format = is_3d
? pixel_format_name(Frame_3D::native_pixel_format)
: pixel_format_name(Frame_2D::native_pixel_format);
const auto native_format = is_3d
? pixel_format_name(Frame_3D::native_pixel_format)
: pixel_format_name(Frame_2D::native_pixel_format);
const std::size_t byte_length = pacing.video_enabled
? static_cast<std::size_t>(stream.width) * stream.height * 4U : 0U;
nlohmann::json output{
{"protocol", "aethera.plot.diagnostics"}, {"version", 4},
{"dimension", is_3d ? "3D" : "2D"},
{"sequence", identity.sequence},
{"correlation_id", identity.correlation_id},
{"rendered_sequence", identity.sequence},
{"rendered_correlation_id", identity.correlation_id},
{"generated_time_unix_ms",
static_cast<double>(created_time_unix_ns) / 1'000'000.0},
{"delivery", pacing.video_enabled ? "gallery-video" : "diagnostics"},
{"frame_rate_fps", frame_rate},
{"dropped_sequence_count", dropped_sequences},
{"window_capacity", diagnostic_window_capacity},
{"pixel", {{"width", stream.width}, {"height", stream.height},
{"format", format}, {"native_format", native_format},
{"supported_formats", std::move(supported_formats)},
{"byte_length", byte_length}}},
{"frame_policy", frame_policy_state_json(pacing)},
{"render_admission", admission_name},
{"frame_statistics", std::move(frame_statistics)},
{"input_statistics", std::move(input_statistics)}};
if (is_3d) {
const auto& pipeline = d->frame_policy_3d->read_state<
Frame_Policy_3D::Base_Tag>();
output["frame_policy"]["three_dimensional_pipeline"] = {
{"capacity", pipeline.pipeline_capacity},
{"gpu_submission_count", pipeline.gpu_submission_count},
{"overlapped_release_count", pipeline.overlapped_release_count},
{"completion_gated_release_count",
pipeline.completion_gated_release_count},
{"gpu_completion_count", pipeline.gpu_completion_count},
{"gpu_in_flight", pipeline.gpu_in_flight},
{"peak_gpu_in_flight", pipeline.peak_gpu_in_flight},
{"last_submitted_sequence", pipeline.last_submitted_sequence},
{"last_completed_sequence", pipeline.last_completed_sequence}
};
const auto gpu = gpu_completion_state();
const auto milliseconds = [](std::uint64_t nanoseconds) {
return static_cast<double>(nanoseconds) / 1'000'000.0;
};
output["gpu_completion_domain"] = {
{"capacity", gpu.capacity}, {"in_flight", gpu.in_flight},
{"peak_in_flight", gpu.peak_in_flight}, {"watched", gpu.watched},
{"peak_watched", gpu.peak_watched},
{"active_fences", gpu.active_fences},
{"pending_fences", gpu.pending_fences},
{"reservation_count", gpu.reservation_count},
{"completion_count", gpu.completion_count},
{"cancellation_count", gpu.cancellation_count},
{"fence_probe_count", gpu.fence_probe_count},
{"fence_wait_count", gpu.fence_wait_count},
{"fence_wait_timeout_count", gpu.fence_wait_timeout_count},
{"fence_wait_total_ms", milliseconds(gpu.fence_wait_total_ns)},
{"fence_wait_max_ms", milliseconds(gpu.fence_wait_max_ns)},
{"callback_total_ms", milliseconds(gpu.callback_total_ns)},
{"callback_max_ms", milliseconds(gpu.callback_max_ns)},
{"callback_failure_count", gpu.callback_failure_count},
{"backpressure_count", gpu.backpressure_count},
{"fault_count", gpu.fault_count},
{"abandoned_count", gpu.abandoned_count}};
}
if (const auto failure = d->terminal_failure.load(std::memory_order_acquire))
output["terminal_failure"] = *failure;
return output;
}
void Plot::request_taskflow_trace(std::size_t frame_count) {
if (frame_count == 0 ||
frame_count > Private::maximum_taskflow_trace_frames)
throw std::invalid_argument("Taskflow trace frame_count must be between 1 and 120");
ensure_started();
auto control = d->taskflow_trace_control.load(std::memory_order_acquire);
for (;;) {
const auto requested = static_cast<std::uint32_t>(control >> 32U);
const auto captured = static_cast<std::uint32_t>(control);
if (requested != captured)
throw std::logic_error("A Taskflow frame trace request is already active");
const auto next = static_cast<std::uint64_t>(frame_count) << 32U;
if (d->taskflow_trace_control.compare_exchange_weak(
control, next, std::memory_order_release,
std::memory_order_acquire))
break;
}
for (auto& slot : d->taskflow_trace_slots)
slot.store({}, std::memory_order_release);
d->taskflow_trace_remaining.store(frame_count, std::memory_order_release);
}
nlohmann::json Plot::taskflow_trace() const {
return d->trace_response(d->taskflow_trace_control,
d->taskflow_trace_remaining,
d->taskflow_trace_slots);
}
void Plot::request_post_publish_taskflow_trace(std::size_t frame_count) {
if (frame_count == 0 ||
frame_count > Private::maximum_taskflow_trace_frames)
throw std::invalid_argument(
"Taskflow post-publish trace frame_count must be between 1 and 120");
ensure_started();
auto control = d->post_publish_trace_control.load(std::memory_order_acquire);
for (;;) {
const auto requested = static_cast<std::uint32_t>(control >> 32U);
const auto captured = static_cast<std::uint32_t>(control);
if (requested != captured)
throw std::logic_error(
"A post-publish Taskflow trace request is already active");
const auto next = static_cast<std::uint64_t>(frame_count) << 32U;
if (d->post_publish_trace_control.compare_exchange_weak(
control, next, std::memory_order_release,
std::memory_order_acquire))
break;
}
for (auto& slot : d->post_publish_trace_slots)
slot.store({}, std::memory_order_release);
d->post_publish_trace_remaining.store(frame_count, std::memory_order_release);
}
nlohmann::json Plot::post_publish_taskflow_trace() const {
return d->trace_response(d->post_publish_trace_control,
d->post_publish_trace_remaining,
d->post_publish_trace_slots);
}
void Plot::reset_diagnostics() {
std::visit([](auto& scene) { scene->reset_diagnostics(); }, d->scene);
d->statistics_generation.fetch_add(1, std::memory_order_acq_rel);
d->with_frame_policy([](auto& policy) { policy.reset_statistics(); });
if (d->frame_policy_3d) d->frame_policy_3d->reset_3d_statistics();
d->arm_tick_consumer(weak_from_this());
}
}