Files
Aethera/web_server/src/Plot.cpp
T
2026-08-27 16:36:29 +08:00

1383 lines
61 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_Pacing_Policy.hpp>
#include <Frame_Scheduler.hpp>
#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 <mutex>
#include <optional>
#include <span>
#include <stdexcept>
#include <unordered_map>
#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";
}
struct Frame_Policy final {
public:
[[nodiscard]] Frame_Pacing_Properties read() const { return pacing.read(); }
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] nlohmann::json write_prop(std::string_view key,
const nlohmann::json& value);
[[nodiscard]] bool accept_periodic_tick(double time_milliseconds) {
return pacing.accept_periodic_tick(time_milliseconds);
}
[[nodiscard]] bool request_immediate() { return pacing.request_immediate(); }
[[nodiscard]] double scheduled_rate_fps() const { return pacing.scheduled_rate_fps(); }
void frame_submitted() { pacing.frame_submitted(); }
[[nodiscard]] bool frame_completed() { return pacing.frame_completed(); }
void frame_rejected() { pacing.frame_rejected(); }
private:
Frame_Pacing_Policy pacing{};
};
std::string_view pacing_mode_name(Frame_Pacing_Mode mode) {
const auto name = magic_enum::enum_name(mode);
if (name.empty()) throw std::logic_error("unknown frame pacing mode");
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");
}
nlohmann::json Frame_Policy::schema() const {
const auto current = read();
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", "当前 Scene 独立目标帧率;周期策略保存在 Kernel Frame_Pacing_Policy。"},
{"technical_description", "Independent per-scene target frame rate."},
{"value", current.fixed_rate_fps}}
})}
};
}
nlohmann::json Frame_Policy::write_prop(std::string_view key,
const nlohmann::json& value) {
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"}};
}
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 taskflow_trace_json(
const Taskflow_Frame_Trace& trace,
const nlohmann::json& component_snapshots) {
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 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() && component_snapshots.contains(owner)) {
const auto& snapshot = component_snapshots.at(owner);
encoded["owner"] = {
{"component", owner},
{"label", snapshot.value("label", owner)},
{"kind", snapshot.value("kind", std::string{})}};
encoded["prop"] = snapshot.value("prop", nlohmann::json::object());
encoded["state"] = snapshot.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},
{"cpu_duration_ms", task.cpu_duration_ms},
{"cpu_cycles", task.cpu_cycles},
{"cooperative_wait_ms", task.cooperative_wait_ms},
{"cpu_time_coarse", task.cpu_time_coarse},
{"observer_entry_ms", task.observer_entry_ms},
{"observer_exit_ms", task.observer_exit_ms},
{"observer_entry_cpu_ms", task.observer_entry_cpu_ms},
{"observer_exit_cpu_ms", task.observer_exit_cpu_ms},
{"queue_wait_ms", task.queue_wait_ms}});
}
return {
{"sequence", trace.identity.sequence},
{"correlation_id", trace.identity.correlation_id},
{"created_time_unix_ns", trace.created_time_unix_ns},
{"worker_count", trace.worker_count},
{"markers", std::move(markers)},
{"graphs", std::move(graphs)},
{"executions", std::move(executions)}};
}
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 正在消费已发布帧;允许下一帧渲染。 */
};
struct Managed_Frame {
std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */
Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */
std::atomic<Frame_State> state{Frame_State::available}; /* 本槽唯一生命周期状态。 */
};
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};
Frame_Policy frame_policy{};
Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */
static constexpr std::size_t scene_frame_capacity{3};
std::array<Managed_Frame, scene_frame_capacity> frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */
Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */
std::atomic<std::shared_ptr<const Plot_Render_Tick>> pending_tick{};
std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */
std::atomic_bool render_admission_busy{}; /* view->update/Scene::advance 到 pixel publish 的唯一准入门。 */
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
std::atomic_uint64_t received_tick_count{}; /* 页面时钟交付给本 Plot 的 tick 总数。 */
std::atomic_uint64_t coalesced_tick_count{}; /* 尚未消费时被更新 tick 替换的旧 tick 总数。 */
std::atomic_uint64_t policy_skip_count{}; /* 帧策略拒绝的 tick 总数。 */
std::atomic_uint64_t preparation_busy_count{}; /* Render admission 忙时被合并为 latest pending 的 tick。 */
std::atomic_uint64_t deferred_resume_count{}; /* pixel publish 后立即唤醒 latest pending 的次数。 */
std::atomic_uint64_t frame_slot_busy_count{}; /* 三个物理帧槽均被占用的提交次数。 */
std::atomic_uint64_t scene_rejection_count{}; /* Scene 单帧准入拒绝的提交次数。 */
std::atomic_uint64_t submitted_frame_count{}; /* 成功提交给 Scene 的帧总数。 */
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};
Frame_Statistics_State completed_frame_statistics_state{};
mutable std::mutex completed_frame_statistics_mutex{};
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)) {
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{});
}
auto& completion = std::visit(
[](auto& scene_value) -> Task_Graph& {
return scene_value->completion_taskflow();
}, scene);
completion_tail = completion.add("plot.frame.publish", [this] {
publish_completed_frame();
});
completion_tail.describe("owner", "plot")
.describe("stage", "completed pixels publish");
}
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] Stream_Snapshot stream_snapshot() const;
void publish(std::shared_ptr<const Plot_Stream_Frame> frame) noexcept;
void defer_tick(const Plot_Render_Tick& tick);
void 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();
void consume_completed_frame(Render_Frame* frame);
void retire_completed_frame(Render_Frame* frame);
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& component_snapshots = {});
[[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 = frame_policy.schema();
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 double fps = frame_policy.scheduled_rate_fps();
if (fps <= 0.0 || current_consumers->empty()) frame_timer.cancel();
else frame_timer.start_periodic(fps);
}
void Plot::Private::defer_tick(const Plot_Render_Tick& tick) {
const auto next = std::make_shared<const Plot_Render_Tick>(tick);
auto current = pending_tick.load(std::memory_order_acquire);
for (;;) {
if (current) {
const bool current_immediate = current->sequence == 0;
const bool next_immediate = tick.sequence == 0;
if ((current_immediate && !next_immediate) ||
(current_immediate == next_immediate &&
current->issued_at >= tick.issued_at))
return;
}
if (pending_tick.compare_exchange_weak(
current, next, std::memory_order_acq_rel,
std::memory_order_acquire)) {
if (current)
coalesced_tick_count.fetch_add(1, std::memory_order_relaxed);
return;
}
}
}
void Plot::Private::arm_tick_consumer(std::weak_ptr<Plot> lifetime) {
if (terminal_failure.load(std::memory_order_acquire) ||
render_admission_busy.load(std::memory_order_acquire) ||
!pending_tick.load(std::memory_order_acquire))
return;
if (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) {
if (!render_admission_busy.exchange(false, std::memory_order_acq_rel))
return;
if (pending_tick.load(std::memory_order_acquire))
deferred_resume_count.fetch_add(1, std::memory_order_relaxed);
arm_tick_consumer(std::move(lifetime));
}
void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
if (!render_admission_busy.load(std::memory_order_acquire)) {
const auto tick = pending_tick.exchange({}, std::memory_order_acq_rel);
if (tick) clock_tick(*tick);
}
tick_task_scheduled.store(false, std::memory_order_release);
arm_tick_consumer(std::move(lifetime));
}
void Plot::Private::clock_tick(const Plot_Render_Tick& tick) {
if (terminal_failure.load(std::memory_order_acquire)) return;
if (tick.sequence != 0 &&
!frame_policy.accept_periodic_tick(tick.time_milliseconds)) {
policy_skip_count.fetch_add(1, std::memory_order_relaxed);
return;
}
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& component_snapshots) {
auto trace = std::make_shared<const nlohmann::json>(
taskflow_trace_json(value, component_snapshots));
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 = frame_policy.read();
if (!pacing.render_enabled || streams.consumers->empty()) return;
bool admission_expected = false;
if (!render_admission_busy.compare_exchange_strong(
admission_expected, true, std::memory_order_acq_rel,
std::memory_order_acquire)) {
preparation_busy_count.fetch_add(1, std::memory_order_relaxed);
defer_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 expected = Frame_State::available;
if (!frame_slots[index].state.compare_exchange_strong(
expected, Frame_State::rendering,
std::memory_order_acq_rel, std::memory_order_acquire))
continue;
slot_index = index;
managed = &frame_slots[index];
break;
}
if (!managed) {
frame_slot_busy_count.fetch_add(1, std::memory_order_relaxed);
defer_tick(tick);
/*
* 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能
* 立即 arm tick consumer,否则会在没有任何槽可用期间形成
* consume -> no slot -> consume 的 Taskflow 任务风暴。真正的唤醒点
* 是 retire_completed_frame:某个 consuming 槽变回 available 后只唤醒一次。
*/
render_admission_busy.store(false, std::memory_order_release);
return;
}
managed->presentation_time =
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::duration<double, std::milli>(tick.time_milliseconds));
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;
/*
* 各图的采样、网格构造和属性快照都在 Plot 自己的准备域完成。
* 进入 Scene::render 后只剩已经准备好的 Visual 批次与轻量提交;
* 共享 Render Domain 不承担业务数据生成。
*/
const auto update_started = std::chrono::steady_clock::now();
view->update(tick);
const auto update_elapsed = std::chrono::steady_clock::now() - update_started;
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));
};
const std::uint64_t sequence = next_frame_sequence++;
const Frame_Identity identity{sequence, tick.sequence == 0 ? sequence : tick.sequence};
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
auto& output = *std::get<std::unique_ptr<Frame_2D>>(managed->frame);
output.begin(identity, Frame_2D::native_pixel_format);
taskflow_trace_claimed = mark_taskflow_trace(output);
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) {
scene_rejection_count.fetch_add(1, std::memory_order_relaxed);
rollback_unsubmitted();
restore_taskflow_trace_claim();
release_render_admission(lifetime);
} else {
taskflow_trace_claimed = false;
frame_policy.frame_submitted();
submitted_frame_count.fetch_add(1, std::memory_order_relaxed);
}
if (!result) frame_policy.frame_rejected();
return;
}
auto& output = *std::get<std::unique_ptr<Frame_3D>>(managed->frame);
output.begin(identity, pacing.video_enabled ? Frame_3D_Output::pixels
: Frame_3D_Output::diagnostics,
Frame_3D::native_pixel_format);
taskflow_trace_claimed = mark_taskflow_trace(output);
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;
frame_policy.frame_submitted();
submitted_frame_count.fetch_add(1, std::memory_order_relaxed);
return;
}
frame_policy.frame_rejected();
scene_rejection_count.fetch_add(1, std::memory_order_relaxed);
rollback_unsubmitted();
restore_taskflow_trace_claim();
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* 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);
managed = &frame_slots[index];
}
if (!managed)
throw std::logic_error("Scene completion graph has no rendering Plot frame");
try {
const auto pacing = frame_policy.read();
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");
}
catch (...) {
throw;
}
}
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 内执行,但不会阻塞下一帧渲染。
*/
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::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");
{
std::lock_guard lock(completed_frame_statistics_mutex);
completed_frame_statistics_state = completed_frame_statistics.submit(*frame);
}
if (frame->taskflow_trace_requested())
store_trace(taskflow_trace_control, taskflow_trace_slots,
frame->take_taskflow_trace(), view->component_snapshots());
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 pending。 */
arm_tick_consumer(lifetime);
}
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{
tick.issued_at, tick.sequence, tick.time_milliseconds});
}
});
/*
* 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 {
std::get<std::unique_ptr<Scene_3D>>(d->scene)->set_frame_callback(
[weak](Frame_3D* frame) {
if (auto owner = weak.lock()) {
try {
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->received_tick_count.fetch_add(1, std::memory_order_relaxed);
d->defer_tick(tick);
d->arm_tick_consumer(weak_from_this());
}
void Plot::render_once() {
ensure_started();
if (!d->frame_policy.request_immediate()) return;
const auto now = std::chrono::steady_clock::now();
const auto elapsed = now - d->clock_origin;
schedule_render(Plot_Render_Tick{
now, 0, std::chrono::duration<double, std::milli>(elapsed).count()});
}
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->frame_policy.write_prop(key, value);
if (result.value("success", false)) d->refresh_schedule();
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);
{
std::lock_guard lock(d->completed_frame_statistics_mutex);
const auto statistics = d->completed_frame_statistics_state;
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->frame_policy.read();
const auto stream = d->stream_snapshot();
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", 3},
{"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}}},
{"pacing", {{"mode", pacing_mode_name(pacing.mode)},
{"fixed_rate_fps", pacing.fixed_rate_fps},
{"render_enabled", pacing.render_enabled},
{"video_enabled", pacing.video_enabled}}},
{"plot_scheduler", {
{"received_ticks", d->received_tick_count.load(std::memory_order_relaxed)},
{"coalesced_ticks", d->coalesced_tick_count.load(std::memory_order_relaxed)},
{"policy_skips", d->policy_skip_count.load(std::memory_order_relaxed)},
{"preparation_busy", d->preparation_busy_count.load(std::memory_order_relaxed)},
{"deferred_resumes", d->deferred_resume_count.load(std::memory_order_relaxed)},
{"render_admission_busy", d->render_admission_busy.load(std::memory_order_relaxed)},
{"frame_slot_busy", d->frame_slot_busy_count.load(std::memory_order_relaxed)},
{"scene_rejections", d->scene_rejection_count.load(std::memory_order_relaxed)},
{"submitted_frames", d->submitted_frame_count.load(std::memory_order_relaxed)}}},
{"frame_statistics", std::move(frame_statistics)},
{"input_statistics", std::move(input_statistics)}};
if (is_3d) {
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);
std::lock_guard lock(d->completed_frame_statistics_mutex);
d->completed_frame_statistics.reset();
d->completed_frame_statistics_state = {};
}
}