1383 lines
61 KiB
C++
1383 lines
61 KiB
C++
#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 外接 DAG;Scene 完成 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 = {};
|
||
}
|
||
}
|