#include "Plot.hpp" #include "Renderable_Adapter.hpp" #include "Taskflow_Trace_Json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 parse_pacing_mode(std::string_view value) { return magic_enum::enum_cast(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(); 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()); 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(); 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()) { if (statistic == Frame_Statistic::count) continue; const auto& value = state.values[static_cast(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()) { auto& event = output[magic_enum::enum_name(type)]; const auto& values = state.values[static_cast(type)]; for (const auto statistic : magic_enum::enum_values()) { if (statistic == Event_Statistic::count) continue; const auto& value = values[static_cast(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(marker.elapsed_ns) / 1'000'000.0; nlohmann::json graphs = nlohmann::json::array(); std::unordered_map 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 void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { const auto dispatch = [&](auto event) { scene.template submit_stream(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>( input.type); apply_pointer(*event); dispatch(std::move(event)); break; } case Event_Type::wheel: { auto event = scene.template make_event>(); 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(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(input.type)); break; } } } struct Plot::Private { using Scene = std::variant, std::unique_ptr>; using Frame = std::variant, std::unique_ptr>; 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 state{Frame_State::available}; /* 本槽唯一生命周期状态。 */ }; struct Consumer { Stream_Handler handler; std::uint32_t width{}; std::uint32_t height{}; }; using Consumer_Map = std::unordered_map; struct Stream_Snapshot { std::shared_ptr consumers; std::uint32_t width{}; std::uint32_t height{}; }; std::unique_ptr view; std::once_flag start_once; std::weak_ptr lifetime{}; /* 仅用于 completion 后重新投递 Taskflow,避免在 Scene callback 内重入 render。 */ std::atomic> consumers{ std::make_shared()}; /* 低频订阅修改发布不可变版本。 */ std::atomic_uint64_t next_stream_id{1}; std::atomic> 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 frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */ Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */ std::atomic> 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>, 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>, 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> 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 Private(std::unique_ptr value_scene, std::unique_ptr value_view) : view(std::move(value_view)), scene(std::move(value_scene)) { for (auto& slot : frame_slots) { if constexpr (std::same_as) slot.frame = std::make_unique(Frame_Identity{}); else slot.frame = std::make_unique(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 frame) noexcept; void defer_tick(const Plot_Render_Tick& tick); void arm_tick_consumer(std::weak_ptr lifetime); void release_render_admission(std::weak_ptr lifetime); void consume_tick(std::weak_ptr 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 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>, 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>, 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( exception_description(failure)); std::shared_ptr 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( 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(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 frame) noexcept { if (!frame) return; try { const auto snapshot = stream_snapshot(); std::vector 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(*current); for (const auto id : failed_consumers) next->erase(id); std::shared_ptr 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(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 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 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 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>, maximum_taskflow_trace_frames>& slots, const Taskflow_Frame_Trace& value, const nlohmann::json& component_snapshots) { auto trace = std::make_shared( taskflow_trace_json(value, component_snapshots)); auto state = control.load(std::memory_order_acquire); for (;;) { const auto requested = static_cast(state >> 32U); const auto captured = static_cast(state); if (captured >= requested) return; slots[captured].store(trace, std::memory_order_release); const auto next = (static_cast(requested) << 32U) | static_cast(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>, 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(state >> 32U); const auto captured = static_cast(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::duration(tick.time_milliseconds)); const auto rollback_unsubmitted = [this, slot_index] { auto& slot = frame_slots[slot_index]; auto expected = Frame_State::rendering; static_cast(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::max(0, std::chrono::duration_cast(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>(&scene)) { auto& output = *std::get>(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(tick.width), static_cast(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>(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>(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> 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>(&managed->frame)) { pixel_layout = Plot_Pixel_Layout::bgra8; const auto image = (*frame_2d)->image(); width = static_cast(image.width); height = static_cast(image.height); if (pacing.video_enabled) { auto output = (*frame_2d)->output_pixels(); pixel_storage = std::make_shared>( std::move(output.bytes)); width = static_cast(output.width); height = static_cast(output.height); } } else { auto& frame_3d = std::get>(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(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( 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::max(0, std::chrono::duration_cast( 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(frame->identity()) : std::shared_ptr{}; 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 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, std::unique_ptr view) : d(std::make_unique(std::move(scene), std::move(view))) {} Plot::Plot(std::unique_ptr scene, std::unique_ptr view) : d(std::make_unique(std::move(scene), std::move(view))) {} Plot::~Plot() = default; void Plot::attach_scene_completion( std::unique_ptr 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 外接 DAG;Scene 完成 frame_ready 与 trace 收口后,再由 * retired callback 归还物理槽。这样 H264(N) 可与 Render(N+1) 重叠。 */ if (auto* scene = std::get_if>(&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>(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(*current); next->emplace(id, Private::Consumer{handler}); std::shared_ptr 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( 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(*current); next->erase(stream); std::shared_ptr 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(*current); auto& consumer = next->at(stream); consumer.width = width; consumer.height = height; std::shared_ptr 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(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>(&d->scene)) dispatch_plot_input(**scene_2d, event); else dispatch_plot_input(*std::get>(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; if constexpr (std::same_as>) { scene->template access_state( read_scene_statistics); } else { is_3d = true; scene->template access_state( 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(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(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(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(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(control >> 32U); const auto captured = static_cast(control); if (requested != captured) throw std::logic_error("A Taskflow frame trace request is already active"); const auto next = static_cast(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(control >> 32U); const auto captured = static_cast(control); if (requested != captured) throw std::logic_error( "A post-publish Taskflow trace request is already active"); const auto next = static_cast(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 = {}; } }