1853 lines
85 KiB
C++
1853 lines
85 KiB
C++
#include "Plot.hpp"
|
|
#include "Renderable_Adapter.hpp"
|
|
#include "Taskflow_Trace_Json.hpp"
|
|
#include <Frame_Policy/Frame_Policy.hpp>
|
|
#include <Frame_Policy/Frame_Policy_3D.hpp>
|
|
#include <Frame_Policy/Frame_Scheduler.hpp>
|
|
#include <concurrentqueue-1.0.5/concurrentqueue.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <magic_enum/magic_enum.hpp>
|
|
#include <render_2D/plottable/Plottables.hpp>
|
|
#include <render_3D/Render_3D.hpp>
|
|
#include <render_3D/detail/Gpu_Completion_Service.hpp>
|
|
#include <render_common.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <concepts>
|
|
#include <exception>
|
|
#include <initializer_list>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <variant>
|
|
#include <vector>
|
|
namespace aethera::web {
|
|
namespace {
|
|
using namespace render_2d;
|
|
using namespace render_3d;
|
|
using Scene_2D = Render_Scene_2D;
|
|
using Scene_3D = Render_Scene_3D;
|
|
constexpr std::uint16_t plot_stream_protocol_version{9};
|
|
constexpr std::size_t diagnostic_window_capacity{600};
|
|
std::string exception_description(const std::exception_ptr& failure) {
|
|
try {
|
|
if (failure) std::rethrow_exception(failure);
|
|
}
|
|
catch (const std::exception& error) {
|
|
return error.what();
|
|
}
|
|
catch (...) {
|
|
return "non-standard Plot failure";
|
|
}
|
|
return "empty Plot failure";
|
|
}
|
|
std::string_view pacing_mode_name(Frame_Pacing_Mode mode) {
|
|
const auto name = magic_enum::enum_name(mode);
|
|
if (name.empty()) throw std::logic_error("unknown frame pacing mode");
|
|
return name;
|
|
}
|
|
std::optional<Frame_Pacing_Mode> parse_pacing_mode(std::string_view value) {
|
|
return magic_enum::enum_cast<Frame_Pacing_Mode>(value);
|
|
}
|
|
std::string_view pixel_format_name(render_2d::Pixel_Format format) {
|
|
const auto name = magic_enum::enum_name(format);
|
|
if (name.empty()) throw std::logic_error("unknown 2D pixel format");
|
|
return name;
|
|
}
|
|
std::string_view pixel_format_name(render_3d::Pixel_Format format) {
|
|
switch (format) {
|
|
case render_3d::Pixel_Format::rgba8_unorm: return "rgba8";
|
|
}
|
|
throw std::logic_error("unknown 3D pixel format");
|
|
}
|
|
const Frame_Policy::State& frame_policy_common_state(
|
|
const Frame_Policy& policy) noexcept {
|
|
return policy.read_state<Frame_Policy::Base_Tag>();
|
|
}
|
|
const Frame_Policy::State& frame_policy_common_state(
|
|
const Frame_Policy_3D& policy) noexcept {
|
|
return policy.read_state<Frame_Policy_3D::Base_Tag>().common;
|
|
}
|
|
template <typename Policy>
|
|
nlohmann::json frame_policy_schema(const Policy& policy) {
|
|
const auto& current = frame_policy_common_state(policy);
|
|
return {
|
|
{"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"},
|
|
{
|
|
"fields", nlohmann::json::array({
|
|
{
|
|
{"key", "render_enabled"}, {"label", "持续渲染与采样"}, {"editor", "boolean"},
|
|
{"editable", true}, {"description", "控制当前 Scene 的周期刷新;画面隐藏不会修改此项。"},
|
|
{"technical_description", "Authoritative per-scene periodic render switch."},
|
|
{"value", current.render_enabled}
|
|
},
|
|
{
|
|
{"key", "pixel_delivery_enabled"}, {"label", "图集像素传输"}, {"editor", "boolean"},
|
|
{"editable", true}, {"description", "控制完成帧是否进入页面级采样器;2D BGRA 与 3D RGBA 均保持原生格式。"},
|
|
{"technical_description", "Authoritative tile publication switch for the shared gallery pixel stream."},
|
|
{"value", current.pixel_delivery_enabled}
|
|
},
|
|
{
|
|
{"key", "pacing_mode"}, {"label", "服务端帧策略"}, {"editor", "select"},
|
|
{"editable", true}, {"description", "只控制 Scene::render(Frame*) 的调用节奏;Scene 的 Frame 所有权与接口保持不变。"},
|
|
{"technical_description", "Per-scene frame pacing policy backed by the Kernel scheduler."},
|
|
{"value", pacing_mode_name(current.mode)},
|
|
{
|
|
"options", nlohmann::json::array({
|
|
{{"value", "manual"}, {"label", "手动渲染"}},
|
|
{{"value", "fixed_rate"}, {"label", "固定频率"}},
|
|
{{"value", "maximum_rate"}, {"label", "最大吞吐"}}
|
|
})
|
|
}
|
|
},
|
|
{
|
|
{"key", "fixed_rate_fps"}, {"label", "目标帧率"}, {"editor", "number"},
|
|
{"editable", true}, {"minimum", 0.1}, {"maximum", 100.0}, {"step", 0.1},
|
|
{"description", "仅 fixed_rate 使用;maximum_rate 在完成准入释放后异步自驱下一帧。"},
|
|
{"technical_description", "Independent per-scene target frame rate."},
|
|
{"value", current.fixed_rate_fps}
|
|
}
|
|
})
|
|
}
|
|
};
|
|
}
|
|
template <typename Policy>
|
|
nlohmann::json write_frame_policy_prop(Policy& pacing,
|
|
std::string_view key,
|
|
const nlohmann::json& value) {
|
|
if (key == "render_enabled" || key == "pixel_delivery_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_pixel_delivery_enabled(target);
|
|
return {
|
|
{"success", true}, {"component", "frame-analysis"}, {"key", key},
|
|
{"value", target}
|
|
};
|
|
}
|
|
if (key == "pacing_mode") {
|
|
if (!value.is_string()) return {{"success", false}, {"error", "pacing_mode requires a string"}};
|
|
const auto parsed = parse_pacing_mode(value.get_ref<const std::string&>());
|
|
if (!parsed) return {{"success", false}, {"error", "unknown frame pacing mode"}};
|
|
pacing.set_mode(*parsed);
|
|
return {
|
|
{"success", true}, {"component", "frame-analysis"}, {"key", key},
|
|
{"value", pacing_mode_name(*parsed)}
|
|
};
|
|
}
|
|
if (key == "fixed_rate_fps") {
|
|
if (!value.is_number()) return {{"success", false}, {"error", "fixed_rate_fps requires a number"}};
|
|
const double next = value.get<double>();
|
|
if (!std::isfinite(next) || next < 0.1 || next > 100.0) return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 100"}};
|
|
pacing.set_fixed_rate(next);
|
|
return {
|
|
{"success", true}, {"component", "frame-analysis"}, {"key", key},
|
|
{"value", next}
|
|
};
|
|
}
|
|
return {{"success", false}, {"error", "unknown frame runtime property"}};
|
|
}
|
|
nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) {
|
|
const auto milliseconds = [](std::uint64_t nanoseconds) {
|
|
return static_cast<double>(nanoseconds) / 1'000'000.0;
|
|
};
|
|
const auto ratio = [](std::uint64_t numerator, std::uint64_t denominator) {
|
|
return denominator == 0 ? 0.0 : static_cast<double>(numerator) / static_cast<double>(denominator);
|
|
};
|
|
const auto observed_ns = state.observed_until_ns > state.observation_started_ns
|
|
? state.observed_until_ns - state.observation_started_ns
|
|
: 0U;
|
|
const auto observed_seconds = static_cast<double>(observed_ns) / 1'000'000'000.0;
|
|
const auto rate = [observed_seconds](std::uint64_t count) {
|
|
return observed_seconds > 0.0
|
|
? static_cast<double>(count) / observed_seconds
|
|
: 0.0;
|
|
};
|
|
const auto completion_span = state.last_completion_ns > state.first_completion_ns
|
|
? state.last_completion_ns - state.first_completion_ns
|
|
: 0U;
|
|
const auto effective_fps = completion_span != 0 && state.completed_frame_count > 1
|
|
? static_cast<double>(state.completed_frame_count - 1U) *
|
|
1'000'000'000.0 / static_cast<double>(completion_span)
|
|
: 0.0;
|
|
const auto interval_mean = state.completion_interval_count == 0
|
|
? 0.0
|
|
: static_cast<double>(state.completion_interval_total_ns) /
|
|
static_cast<double>(state.completion_interval_count);
|
|
const auto interval_variance = state.completion_interval_count == 0
|
|
? 0.0
|
|
: std::max(0.0,
|
|
state.completion_interval_squared_total_ns2 /
|
|
static_cast<double>(state.completion_interval_count) -
|
|
interval_mean * interval_mean);
|
|
const auto target_achievement =
|
|
state.mode == Frame_Pacing_Mode::fixed_rate && state.fixed_rate_fps > 0.0
|
|
? effective_fps / state.fixed_rate_fps
|
|
: 0.0;
|
|
return {
|
|
{"generation", state.generation},
|
|
{
|
|
"configuration", {
|
|
{"mode", pacing_mode_name(state.mode)},
|
|
{"render_enabled", state.render_enabled},
|
|
{"pixel_delivery_enabled", state.pixel_delivery_enabled},
|
|
{"fixed_rate_fps", state.fixed_rate_fps}
|
|
}
|
|
},
|
|
{
|
|
"observation", {
|
|
{"duration_ms", milliseconds(observed_ns)},
|
|
{"request_count", state.request_count},
|
|
{"submitted_frame_count", state.submitted_frame_count},
|
|
{"completed_frame_count", state.completed_frame_count},
|
|
{"active_frame_count", state.active_frame_count}
|
|
}
|
|
},
|
|
{
|
|
"throughput", {
|
|
{"request_rate_fps", rate(state.request_count)},
|
|
{"submission_rate_fps", rate(state.submitted_frame_count)},
|
|
{"completion_rate_fps", effective_fps},
|
|
{"target_achievement_ratio", target_achievement},
|
|
{
|
|
"latest_frame_interval_ms",
|
|
milliseconds(state.latest_completion_interval_ns)
|
|
},
|
|
{
|
|
"average_frame_interval_ms", milliseconds(
|
|
state.completion_interval_count == 0
|
|
? 0U
|
|
: state.completion_interval_total_ns /
|
|
state.completion_interval_count)
|
|
},
|
|
{
|
|
"frame_interval_jitter_ms",
|
|
std::sqrt(interval_variance) / 1'000'000.0
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"requests", {
|
|
{"periodic", state.periodic_request_count},
|
|
{"immediate", state.immediate_request_count},
|
|
{"maximum_rate", state.maximum_rate_request_count},
|
|
{"accepted", state.accepted_request_count},
|
|
{"coalesced", state.coalesced_request_count},
|
|
{"policy_rejected", state.policy_rejection_count},
|
|
{"frame_slot_backpressure", state.frame_slot_backpressure_count},
|
|
{"scene_rejected", state.scene_rejection_count},
|
|
{
|
|
"acceptance_ratio", ratio(
|
|
state.accepted_request_count, state.request_count)
|
|
},
|
|
{
|
|
"coalescing_ratio", ratio(
|
|
state.coalesced_request_count, state.request_count)
|
|
},
|
|
{
|
|
"backpressure_ratio", ratio(
|
|
state.frame_slot_backpressure_count,
|
|
state.accepted_request_count)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"latency", {
|
|
{"latest_tick_queue_ms", milliseconds(state.latest_tick_queue_ns)},
|
|
{
|
|
"average_tick_queue_ms", milliseconds(
|
|
state.submitted_frame_count == 0 ? 0U : state.tick_queue_total_ns / state.submitted_frame_count)
|
|
},
|
|
{"maximum_tick_queue_ms", milliseconds(state.maximum_tick_queue_ns)},
|
|
{
|
|
"latest_completion_ms", milliseconds(
|
|
state.latest_completion_latency_ns)
|
|
},
|
|
{
|
|
"average_completion_ms", milliseconds(
|
|
state.completed_frame_count == 0
|
|
? 0U
|
|
: state.completion_latency_total_ns /
|
|
state.completed_frame_count)
|
|
},
|
|
{
|
|
"maximum_completion_ms", milliseconds(
|
|
state.maximum_completion_latency_ns)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"last_frame", {
|
|
{"sequence", state.last_frame_sequence},
|
|
{
|
|
"request_source", magic_enum::enum_name(
|
|
state.last_request_source)
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
void append_statistic_json(nlohmann::json& output,
|
|
const Frame_Statistics_State& state) {
|
|
for (const auto statistic : magic_enum::enum_values<Frame_Statistic>()) {
|
|
if (statistic == Frame_Statistic::count) continue;
|
|
const auto& value =
|
|
state.values[static_cast<std::size_t>(statistic)];
|
|
if (value.count == 0) continue;
|
|
output[magic_enum::enum_name(statistic)] = {
|
|
{"count", value.count}, {"latest", value.latest},
|
|
{"minimum", value.minimum}, {"maximum", value.maximum},
|
|
{"average", value.average},
|
|
{"trimmed_average", value.trimmed_average},
|
|
{"variability", value.variability}, {"p50", value.p50},
|
|
{"p95", value.p95}, {"p99", value.p99}
|
|
};
|
|
}
|
|
}
|
|
void append_event_statistics_json(nlohmann::json& output,
|
|
const Event_Statistics_State& state) {
|
|
for (const auto type : magic_enum::enum_values<Event_Type>()) {
|
|
auto& event = output[magic_enum::enum_name(type)];
|
|
const auto& values = state.values[static_cast<std::size_t>(type)];
|
|
for (const auto statistic : magic_enum::enum_values<Event_Statistic>()) {
|
|
if (statistic == Event_Statistic::count) continue;
|
|
const auto& value = values[static_cast<std::size_t>(statistic)];
|
|
if (value.count == 0) continue;
|
|
event[magic_enum::enum_name(statistic)] = {
|
|
{"count", value.count}, {"latest", value.latest},
|
|
{"minimum", value.minimum}, {"maximum", value.maximum},
|
|
{"average", value.average},
|
|
{"trimmed_average", value.trimmed_average},
|
|
{"variability", value.variability}, {"p50", value.p50},
|
|
{"p95", value.p95}, {"p99", value.p99}
|
|
};
|
|
}
|
|
if (event.empty()) output.erase(std::string{magic_enum::enum_name(type)});
|
|
}
|
|
}
|
|
nlohmann::json datoviz_observation_json(
|
|
const Datoviz_Frame_Observation& value) {
|
|
const auto milliseconds = [](std::uint64_t nanoseconds) {
|
|
return static_cast<double>(nanoseconds) / 1'000'000.0;
|
|
};
|
|
nlohmann::json result{
|
|
{"render_sequence", value.render_sequence},
|
|
{"path", magic_enum::enum_name(value.path)},
|
|
{"gpu_timing_requested", value.gpu_timing_requested},
|
|
{"readback_requested", value.readback_requested},
|
|
{"controller_input_applied", value.controller_input_applied},
|
|
{
|
|
"prepare_released_after_submission",
|
|
value.prepare_released_after_submission
|
|
},
|
|
{
|
|
"timings_ms", {
|
|
{
|
|
"queue_submit_wait",
|
|
milliseconds(value.queue_submit_wait_ns)
|
|
},
|
|
{"target_acquire", milliseconds(value.target_acquire_ns)},
|
|
{"structure_check", milliseconds(value.structure_check_ns)},
|
|
{"apply", milliseconds(value.apply_ns)},
|
|
{"query", milliseconds(value.query_ns)},
|
|
{"runtime_plan", milliseconds(value.runtime_plan_ns)},
|
|
{"runtime_execute", milliseconds(value.runtime_execute_ns)},
|
|
{"mvp_update", milliseconds(value.mvp_update_ns)},
|
|
{"frame_begin", milliseconds(value.frame_begin_ns)},
|
|
{"frame_plan", milliseconds(value.frame_plan_ns)},
|
|
{"external_register", milliseconds(value.external_register_ns)},
|
|
{"frame_attach", milliseconds(value.frame_attach_ns)},
|
|
{"frame_execute", milliseconds(value.frame_execute_ns)},
|
|
{"frame_finish", milliseconds(value.frame_finish_ns)},
|
|
{"drp_validation", milliseconds(value.drp_validation_ns)},
|
|
{"drp_state", milliseconds(value.drp_state_ns)},
|
|
{"drp_buffer_create", milliseconds(value.drp_buffer_create_ns)},
|
|
{"drp_texture_create", milliseconds(value.drp_texture_create_ns)},
|
|
{"drp_shader_create", milliseconds(value.drp_shader_create_ns)},
|
|
{"drp_shader_compile", milliseconds(value.drp_shader_compile_ns)},
|
|
{
|
|
"drp_shader_module_create",
|
|
milliseconds(value.drp_shader_module_create_ns)
|
|
},
|
|
{"drp_pipeline_create", milliseconds(value.drp_pipeline_create_ns)},
|
|
{"drp_binding_create", milliseconds(value.drp_binding_create_ns)},
|
|
{"drp_upload", milliseconds(value.drp_upload_ns)},
|
|
{"drp_transfer", milliseconds(value.drp_transfer_ns)},
|
|
{"drp_record", milliseconds(value.drp_record_ns)},
|
|
{"submit", milliseconds(value.submit_ns)},
|
|
{
|
|
"gpu_completion_observation",
|
|
milliseconds(value.gpu_completion_observation_ns)
|
|
},
|
|
{
|
|
"completion_task_queue",
|
|
milliseconds(value.completion_task_queue_ns)
|
|
},
|
|
{"readback", milliseconds(value.readback_ns)}
|
|
}
|
|
},
|
|
{
|
|
"traffic", {
|
|
{"uploaded_bytes", value.uploaded_bytes},
|
|
{"readback_bytes", value.readback_bytes},
|
|
{"pipeline_create_count", value.drp_pipeline_create_count}
|
|
}
|
|
},
|
|
{
|
|
"frame_plan", {
|
|
{"resource_version", value.artifact_resource_version},
|
|
{"frame_index", value.artifact_frame_index},
|
|
{"status", value.artifact_status}
|
|
}
|
|
},
|
|
{
|
|
"validation", {
|
|
{"performed", value.validation_performed},
|
|
{"ok", value.validation_ok},
|
|
{"code", value.validation_code},
|
|
{"command_index", value.validation_command_index}
|
|
}
|
|
}
|
|
};
|
|
if (value.gpu) {
|
|
result["gpu_ms"] = {
|
|
{"render", milliseconds(value.gpu->render_ns)},
|
|
{"transition", milliseconds(value.gpu->transition_ns)},
|
|
{"copy", milliseconds(value.gpu->copy_ns)},
|
|
{"total", milliseconds(value.gpu->total_ns)}
|
|
};
|
|
}
|
|
if (!value.artifact_json.empty()) result["frame_plan"]["artifact_json"] = value.artifact_json;
|
|
return result;
|
|
}
|
|
}
|
|
nlohmann::json taskflow_trace_json(
|
|
const Taskflow_Frame_Trace& trace,
|
|
const nlohmann::json& captured_components,
|
|
const nlohmann::json& captured_backend) {
|
|
nlohmann::json markers = nlohmann::json::object();
|
|
for (const auto& marker : trace.markers)
|
|
markers[magic_enum::enum_name(marker.marker)] =
|
|
static_cast<double>(marker.elapsed_ns) / 1'000'000.0;
|
|
nlohmann::json measurements = nlohmann::json::object();
|
|
for (const auto& measurement : trace.measurements)
|
|
measurements[magic_enum::enum_name(measurement.measurement)] =
|
|
static_cast<double>(measurement.value_ns) / 1'000'000.0;
|
|
nlohmann::json graphs = nlohmann::json::array();
|
|
std::unordered_map<std::uint64_t, std::string> node_ids;
|
|
for (const auto& graph : trace.graphs) {
|
|
nlohmann::json nodes = nlohmann::json::array();
|
|
for (const auto& node : graph.nodes) {
|
|
node_ids.emplace(node.native_id, node.node_id);
|
|
nlohmann::json predecessors = nlohmann::json::array();
|
|
for (const auto native_id : node.predecessors) predecessors.push_back(std::to_string(native_id));
|
|
nlohmann::json successors = nlohmann::json::array();
|
|
for (const auto native_id : node.successors) successors.push_back(std::to_string(native_id));
|
|
nlohmann::json attributes = nlohmann::json::object();
|
|
for (const auto& [key, value] : node.attributes) attributes[key] = value;
|
|
nlohmann::json encoded{
|
|
{"native_id", std::to_string(node.native_id)}, {"id", node.node_id},
|
|
{"parent_id", node.parent_node_id}, {"name", node.name},
|
|
{"type", node.type}, {"predecessors", std::move(predecessors)},
|
|
{"successors", std::move(successors)},
|
|
{"attributes", std::move(attributes)}
|
|
};
|
|
const auto owner = encoded["attributes"].value(
|
|
"owner_component", std::string{});
|
|
if (!owner.empty() && captured_components.contains(owner)) {
|
|
const auto& captured = captured_components.at(owner);
|
|
encoded["owner"] = {
|
|
{"component", owner},
|
|
{"label", captured.value("label", owner)},
|
|
{"kind", captured.value("kind", std::string{})}
|
|
};
|
|
encoded["prop"] = captured.value("prop", nlohmann::json::object());
|
|
encoded["state"] = captured.value("state", nlohmann::json::object());
|
|
}
|
|
nodes.push_back(std::move(encoded));
|
|
}
|
|
graphs.push_back({
|
|
{"stage", graph.stage}, {"name", graph.taskflow_name},
|
|
{"submitted_ms", graph.submitted_ms},
|
|
{"finished_ms", graph.finished_ms},
|
|
{"completed", graph.completed}, {"nodes", std::move(nodes)}
|
|
});
|
|
}
|
|
nlohmann::json executions = nlohmann::json::array();
|
|
for (const auto& task : trace.tasks) {
|
|
const auto found = node_ids.find(task.native_id);
|
|
nlohmann::json cooperative_waits = nlohmann::json::array();
|
|
for (const auto& wait : task.cooperative_waits) {
|
|
cooperative_waits.push_back({
|
|
{"started_ms", wait.started_ms},
|
|
{"finished_ms", wait.finished_ms}});
|
|
}
|
|
executions.push_back({
|
|
{"native_id", std::to_string(task.native_id)},
|
|
{"node_id", found == node_ids.end() ? std::string{} : found->second},
|
|
{"worker_id", task.worker_id},
|
|
{"worker_queue_size", task.worker_queue_size},
|
|
{"worker_queue_capacity", task.worker_queue_capacity},
|
|
{"ready_ms", task.ready_ms}, {"entered_ms", task.entered_ms},
|
|
{"started_ms", task.started_ms}, {"finished_ms", task.finished_ms},
|
|
{"completed_ms", task.completed_ms},
|
|
{"duration_ms", task.duration_ms},
|
|
{"cooperative_wait_ms", task.cooperative_wait_ms},
|
|
{"cooperative_waits", std::move(cooperative_waits)},
|
|
{"observer_entry_ms", task.observer_entry_ms},
|
|
{"observer_exit_ms", task.observer_exit_ms},
|
|
{"queue_wait_ms", task.queue_wait_ms}
|
|
});
|
|
}
|
|
nlohmann::json result{
|
|
{"sequence", trace.identity.sequence},
|
|
{"correlation_id", trace.identity.correlation_id},
|
|
{"request_source", magic_enum::enum_name(trace.request_source)},
|
|
{"frame_policy", frame_policy_state_json(trace.frame_policy)},
|
|
{"created_time_unix_ns", trace.created_time_unix_ns},
|
|
{"worker_count", trace.worker_count},
|
|
{"markers", std::move(markers)},
|
|
{"measurements", std::move(measurements)},
|
|
{"graphs", std::move(graphs)},
|
|
{"executions", std::move(executions)}
|
|
};
|
|
if (!captured_backend.empty()) result["datoviz"] = captured_backend;
|
|
return result;
|
|
}
|
|
namespace {
|
|
[[nodiscard]] Event_Timeline_Time input_timeline_time(
|
|
double time_milliseconds) {
|
|
constexpr long double nanoseconds_per_millisecond{1'000'000.0L};
|
|
constexpr long double maximum_milliseconds =
|
|
static_cast<long double>(std::numeric_limits<std::uint64_t>::max()) /
|
|
nanoseconds_per_millisecond;
|
|
if (!std::isfinite(time_milliseconds) || time_milliseconds < 0.0 ||
|
|
static_cast<long double>(time_milliseconds) > maximum_milliseconds)
|
|
throw std::invalid_argument(
|
|
"input time_milliseconds must be finite, non-negative and representable");
|
|
return Event_Timeline_Time{
|
|
static_cast<std::uint64_t>(
|
|
static_cast<long double>(time_milliseconds) *
|
|
nanoseconds_per_millisecond)
|
|
};
|
|
}
|
|
template <typename Scene_Object>
|
|
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
|
|
const auto occurred_at = input_timeline_time(input.time_milliseconds);
|
|
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, occurred_at);
|
|
apply_pointer(*event);
|
|
dispatch(std::move(event));
|
|
break;
|
|
}
|
|
case Event_Type::wheel: {
|
|
auto event = scene.template make_event<Basic_Wheel_Event<Point_F>>(
|
|
occurred_at);
|
|
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, occurred_at);
|
|
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, occurred_at));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
struct Plot::Private {
|
|
using Scene = std::variant<std::unique_ptr<Scene_2D>, std::unique_ptr<Scene_3D>>;
|
|
using Frame = std::variant<std::unique_ptr<Frame_2D>, std::unique_ptr<Frame_3D>>;
|
|
enum struct Frame_State : std::uint8_t {
|
|
available,
|
|
rendering, /* Scene::advance -> Plot pixel publish,不可重入。 */
|
|
consuming /* 外接 Taskflow 正在消费已发布帧;允许下一帧渲染。 */
|
|
};
|
|
enum struct Render_Admission_State : std::uint8_t {
|
|
ready,
|
|
rendering,
|
|
frame_slots_exhausted
|
|
};
|
|
struct Managed_Frame {
|
|
std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */
|
|
std::chrono::steady_clock::time_point tick_issued_at{}; /* 本逻辑帧请求进入 Plot 的时刻。 */
|
|
std::chrono::steady_clock::time_point submitted_at{}; /* Scene 接受本逻辑帧的时刻。 */
|
|
Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */
|
|
std::atomic<Frame_State> state{Frame_State::available}; /* 本槽唯一生命周期状态。 */
|
|
std::atomic_size_t diagnostic_readers{}; /* 无锁诊断读取认领;非零时该槽不可复用。 */
|
|
std::uint64_t statistics_generation{}; /* 与本槽中完成帧统计共同发布。 */
|
|
Frame_Statistics_State statistics{}; /* 统计结果归属当前完成帧,不在 Plot 复制。 */
|
|
std::atomic<Managed_Frame*> retired_next{}; /* 无锁退役队列的槽内侵入链接。 */
|
|
};
|
|
struct Consumer {
|
|
Stream_Handler handler;
|
|
std::uint32_t width{};
|
|
std::uint32_t height{};
|
|
};
|
|
using Consumer_Map = std::unordered_map<Stream_Id, Consumer>;
|
|
struct Stream_Snapshot {
|
|
std::shared_ptr<const Consumer_Map> consumers;
|
|
std::uint32_t width{};
|
|
std::uint32_t height{};
|
|
};
|
|
std::unique_ptr<Scene_View> view;
|
|
std::once_flag start_once;
|
|
std::weak_ptr<Plot> lifetime{}; /* 仅用于 completion 后重新投递 Taskflow,避免在 Scene callback 内重入 render。 */
|
|
std::atomic<std::shared_ptr<Plot>> frame_policy_lifetime{}; /* 任一物理帧被借用期间由策略保活整个 Plot;最后一槽归还后释放。 */
|
|
std::atomic<std::shared_ptr<const Consumer_Map>> consumers{
|
|
std::make_shared<const Consumer_Map>()
|
|
}; /* 低频订阅修改发布不可变版本。 */
|
|
std::atomic_uint64_t next_stream_id{1};
|
|
std::atomic<std::shared_ptr<const std::string>> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */
|
|
std::uint64_t next_frame_sequence{1};
|
|
std::unique_ptr<Frame_Policy> frame_policy_2d{};
|
|
std::unique_ptr<Frame_Policy_3D> frame_policy_3d{};
|
|
Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */
|
|
static constexpr std::size_t scene_frame_capacity{3};
|
|
std::array<Managed_Frame, scene_frame_capacity> frame_slots{}; /* 帧策略拥有并反复调度的稳定三缓冲;Scene 只借用。 */
|
|
std::atomic<Managed_Frame*> latest_statistics_frame{}; /* 只定位权威帧槽,不保存统计副本。 */
|
|
std::atomic<Managed_Frame*> retired_frames{}; /* 完成回调返回、唯一帧策略写者消费的物理帧。 */
|
|
Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */
|
|
moodycamel::ConcurrentQueue<Plot_Render_Tick> tick_requests{}; /* 多生产者提交、唯一短任务消费的帧请求流。 */
|
|
std::optional<Plot_Render_Tick> deferred_tick{}; /* 仅 tick consumer 任务访问的 latest 延后请求。 */
|
|
std::atomic_uint64_t consumer_work_generation{}; /* tick 或退休帧入队后推进,关闭 consumer 尾部唤醒竞争窗口。 */
|
|
std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */
|
|
std::atomic<Render_Admission_State> render_admission{Render_Admission_State::ready}; /* Plot 渲染准入及物理槽背压的唯一状态源。 */
|
|
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
|
|
std::atomic_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */
|
|
static constexpr std::size_t maximum_taskflow_trace_frames{120};
|
|
/* 高 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{};
|
|
Frame_Statistics_Accumulator completed_frame_statistics{diagnostic_window_capacity};
|
|
std::uint64_t applied_statistics_generation{}; /* 仅完成帧退役任务读写。 */
|
|
std::atomic_uint64_t statistics_generation{}; /* reset 只推进代次,不触碰单写者累加器。 */
|
|
template <typename Scene_Object>
|
|
Private(std::unique_ptr<Scene_Object> value_scene,
|
|
std::unique_ptr<Scene_View> value_view) : view(std::move(value_view)), scene(std::move(value_scene)) {
|
|
if constexpr (std::same_as<Scene_Object, Scene_2D>) {
|
|
auto built_policy = Frame_Policy::Builder<Frame_Policy>{}.build();
|
|
if (!built_policy) throw std::logic_error("2D Frame Policy dependency graph is invalid");
|
|
frame_policy_2d = std::move(*built_policy);
|
|
}
|
|
else {
|
|
auto built_policy =
|
|
Frame_Policy_3D::Builder<Frame_Policy_3D>{}.build();
|
|
if (!built_policy) throw std::logic_error("3D Frame Policy dependency graph is invalid");
|
|
frame_policy_3d = std::move(*built_policy);
|
|
frame_policy_3d->set_mode(Frame_Pacing_Mode::maximum_rate);
|
|
static_cast<void>(frame_policy_3d->consume_events());
|
|
}
|
|
for (auto& slot : frame_slots) {
|
|
if constexpr (std::same_as<Scene_Object, Scene_2D>) slot.frame = std::make_unique<Frame_2D>(Frame_Identity{});
|
|
else slot.frame = std::make_unique<Frame_3D>(Frame_Identity{});
|
|
}
|
|
}
|
|
template <typename Callback>
|
|
decltype(auto) with_frame_policy(Callback&& callback) {
|
|
if (frame_policy_2d) return std::forward<Callback>(callback)(*frame_policy_2d);
|
|
return std::forward<Callback>(callback)(*frame_policy_3d);
|
|
}
|
|
template <typename Callback>
|
|
decltype(auto) with_frame_policy(Callback&& callback) const {
|
|
if (frame_policy_2d) return std::forward<Callback>(callback)(*frame_policy_2d);
|
|
return std::forward<Callback>(callback)(*frame_policy_3d);
|
|
}
|
|
[[nodiscard]] const Frame_Policy::State& pacing_state() const noexcept {
|
|
if (frame_policy_2d) return frame_policy_2d->read_state<Frame_Policy::Base_Tag>();
|
|
return frame_policy_3d->read_state<Frame_Policy_3D::Base_Tag>().common;
|
|
}
|
|
[[nodiscard]] bool consume_frame_policy_events() {
|
|
return with_frame_policy(
|
|
[](auto& policy) {
|
|
return policy.consume_events();
|
|
});
|
|
}
|
|
[[nodiscard]] nlohmann::json schema() const;
|
|
[[nodiscard]] Stream_Snapshot stream_snapshot() const;
|
|
void publish(std::shared_ptr<const Plot_Stream_Frame> frame) noexcept;
|
|
void submit_tick_request(Plot_Render_Tick tick);
|
|
void keep_latest_tick(const Plot_Render_Tick& tick);
|
|
void arm_tick_consumer(std::weak_ptr<Plot> lifetime);
|
|
void release_render_admission(std::weak_ptr<Plot> lifetime);
|
|
void retain_frame_policy_lifetime();
|
|
void release_frame_policy_lifetime_if_idle();
|
|
void consume_tick(std::weak_ptr<Plot> lifetime);
|
|
void refresh_schedule();
|
|
void clock_tick(const Plot_Render_Tick& tick);
|
|
void render_frame(Plot_Render_Tick tick);
|
|
void publish_completed_frame(not_null<Render_Frame*> completed);
|
|
void consume_completed_frame(not_null<Render_Frame*> frame);
|
|
void retire_completed_frame(not_null<Render_Frame*> frame);
|
|
void consume_retired_frames();
|
|
void finalize_retired_frame(not_null<Render_Frame*> frame);
|
|
[[nodiscard]] bool mark_taskflow_trace(Render_Frame& frame);
|
|
void store_trace(std::atomic_uint64_t& control,
|
|
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
|
maximum_taskflow_trace_frames>& slots,
|
|
const Taskflow_Frame_Trace& trace,
|
|
const nlohmann::json& captured_components = {},
|
|
const nlohmann::json& captured_backend = {});
|
|
[[nodiscard]] nlohmann::json trace_response(
|
|
const std::atomic_uint64_t& control,
|
|
const std::atomic_size_t& remaining,
|
|
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
|
maximum_taskflow_trace_frames>& slots) const;
|
|
void fail(std::exception_ptr failure) noexcept;
|
|
};
|
|
void Plot::Private::fail(std::exception_ptr failure) noexcept {
|
|
try {
|
|
auto description = std::make_shared<const std::string>(
|
|
exception_description(failure));
|
|
std::shared_ptr<const std::string> empty;
|
|
if (!terminal_failure.compare_exchange_strong(
|
|
empty, description, std::memory_order_acq_rel,
|
|
std::memory_order_acquire))
|
|
return;
|
|
const auto output = std::make_shared<const Plot_Stream_Frame>(
|
|
Plot_Stream_Frame{
|
|
nlohmann::json{
|
|
{"kind", "plot_error"},
|
|
{"protocol", "aethera.plot.stream"},
|
|
{"version", plot_stream_protocol_version},
|
|
{"message", *description}
|
|
}.dump(),
|
|
{}
|
|
});
|
|
publish(std::move(output));
|
|
}
|
|
catch (...) {}
|
|
}
|
|
nlohmann::json Plot::Private::schema() const {
|
|
auto result = view->schema();
|
|
auto analysis = with_frame_policy(
|
|
[](const auto& policy) {
|
|
return frame_policy_schema(policy);
|
|
});
|
|
if (frame_policy_3d) {
|
|
analysis["fields"].push_back({
|
|
{"key", "pipeline_capacity"}, {"label", "3D 帧槽容量"},
|
|
{"editor", "integer"}, {"editable", false}, {"value", 3},
|
|
{"description", "3D 独立策略最多允许三帧处于 Prepare、GPU 和退休阶段。"},
|
|
{"technical_description", "3D-only submitted-driven pipeline capacity; 2D policy is unchanged."}
|
|
});
|
|
}
|
|
const auto generator = view->data_generator_schema();
|
|
if (!generator.is_null()) analysis["data_generator"] = generator;
|
|
result["frame_analysis"] = std::move(analysis);
|
|
return result;
|
|
}
|
|
Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const {
|
|
Stream_Snapshot result;
|
|
result.consumers = consumers.load(std::memory_order_acquire);
|
|
for (const auto& [id, consumer] : *result.consumers) {
|
|
static_cast<void>(id);
|
|
if (consumer.width == 0 || consumer.height == 0) continue;
|
|
result.width = std::max(result.width, consumer.width);
|
|
result.height = std::max(result.height, consumer.height);
|
|
}
|
|
result.width = std::clamp(result.width == 0 ? 320U : result.width, 160U, 1920U) & ~1U;
|
|
result.height = std::clamp(result.height == 0 ? 192U : result.height, 120U, 1080U) & ~1U;
|
|
return result;
|
|
}
|
|
void Plot::Private::publish(
|
|
std::shared_ptr<const Plot_Stream_Frame> frame) noexcept {
|
|
if (!frame) return;
|
|
try {
|
|
const auto snapshot = stream_snapshot();
|
|
std::vector<Stream_Id> failed_consumers;
|
|
for (const auto& [id, consumer] : *snapshot.consumers) {
|
|
if (!consumer.handler) continue;
|
|
try {
|
|
consumer.handler(frame);
|
|
}
|
|
catch (...) {
|
|
failed_consumers.push_back(id);
|
|
}
|
|
}
|
|
if (failed_consumers.empty()) return;
|
|
auto current = consumers.load(std::memory_order_acquire);
|
|
for (;;) {
|
|
auto next = std::make_shared<Consumer_Map>(*current);
|
|
for (const auto id : failed_consumers) next->erase(id);
|
|
std::shared_ptr<const Consumer_Map> desired = next;
|
|
if (consumers.compare_exchange_weak(
|
|
current, desired, std::memory_order_release,
|
|
std::memory_order_acquire))
|
|
break;
|
|
}
|
|
}
|
|
catch (...) {}
|
|
}
|
|
void Plot::Private::refresh_schedule() {
|
|
if (!frame_timer.valid()) return;
|
|
const auto current_consumers = consumers.load(std::memory_order_acquire);
|
|
const auto& pacing = pacing_state();
|
|
if (!pacing.render_enabled || current_consumers->empty()) {
|
|
frame_timer.cancel();
|
|
return;
|
|
}
|
|
if (pacing.mode == Frame_Pacing_Mode::fixed_rate) {
|
|
frame_timer.start_periodic(pacing.fixed_rate_fps);
|
|
return;
|
|
}
|
|
frame_timer.cancel();
|
|
if (pacing.mode != Frame_Pacing_Mode::maximum_rate) return;
|
|
const auto now = std::chrono::steady_clock::now();
|
|
submit_tick_request(Plot_Render_Tick{
|
|
.issued_at = now,
|
|
.time_milliseconds = std::chrono::duration<double, std::milli>(
|
|
now - clock_origin).count(),
|
|
.source = Frame_Request_Source::maximum_rate
|
|
});
|
|
arm_tick_consumer(lifetime);
|
|
}
|
|
void Plot::Private::submit_tick_request(Plot_Render_Tick tick) {
|
|
const auto source = tick.source;
|
|
const auto issued_at = tick.issued_at;
|
|
if (!tick_requests.enqueue(std::move(tick))) throw std::bad_alloc{};
|
|
with_frame_policy([&](auto& policy) {
|
|
policy.record_request(source, issued_at);
|
|
});
|
|
consumer_work_generation.fetch_add(1, std::memory_order_release);
|
|
}
|
|
void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) {
|
|
const auto priority = [](Frame_Request_Source source) {
|
|
switch (source) {
|
|
case Frame_Request_Source::unspecified: return 0;
|
|
case Frame_Request_Source::periodic: return 0;
|
|
case Frame_Request_Source::maximum_rate: return 1;
|
|
case Frame_Request_Source::immediate: return 2;
|
|
}
|
|
return 0;
|
|
};
|
|
if (deferred_tick) {
|
|
const auto current_priority = priority(deferred_tick->source);
|
|
const auto next_priority = priority(tick.source);
|
|
with_frame_policy([](auto& policy) {
|
|
policy.record_request_coalesced();
|
|
});
|
|
if (current_priority > next_priority ||
|
|
(current_priority == next_priority &&
|
|
deferred_tick->issued_at >= tick.issued_at))
|
|
return;
|
|
}
|
|
deferred_tick = tick;
|
|
}
|
|
void Plot::Private::arm_tick_consumer(std::weak_ptr<Plot> lifetime) {
|
|
if (terminal_failure.load(std::memory_order_acquire)) return;
|
|
if (tick_task_scheduled.exchange(true, std::memory_order_acq_rel)) return;
|
|
aethera::schedule_task("web.plot.tick.consume", [lifetime] {
|
|
const auto plot = lifetime.lock();
|
|
if (!plot) return;
|
|
try {
|
|
plot->d->consume_tick(lifetime);
|
|
}
|
|
catch (...) {
|
|
plot->d->fail(std::current_exception());
|
|
}
|
|
});
|
|
}
|
|
void Plot::Private::release_render_admission(std::weak_ptr<Plot> lifetime) {
|
|
auto expected = Render_Admission_State::rendering;
|
|
if (!render_admission.compare_exchange_strong(
|
|
expected, Render_Admission_State::ready,
|
|
std::memory_order_acq_rel, std::memory_order_acquire))
|
|
return;
|
|
const auto& pacing = pacing_state();
|
|
if (pacing.render_enabled && pacing.mode == Frame_Pacing_Mode::maximum_rate) {
|
|
const auto current_consumers = consumers.load(std::memory_order_acquire);
|
|
if (!current_consumers->empty()) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
submit_tick_request(Plot_Render_Tick{
|
|
.issued_at = now,
|
|
.time_milliseconds = std::chrono::duration<double, std::milli>(
|
|
now - clock_origin).count(),
|
|
.source = Frame_Request_Source::maximum_rate
|
|
});
|
|
}
|
|
}
|
|
arm_tick_consumer(std::move(lifetime));
|
|
}
|
|
void Plot::Private::retain_frame_policy_lifetime() {
|
|
if (frame_policy_lifetime.load(std::memory_order_acquire)) return;
|
|
const auto owner = lifetime.lock();
|
|
if (!owner)
|
|
throw std::logic_error(
|
|
"Plot frame policy cannot retain an expired Plot");
|
|
std::shared_ptr<Plot> empty;
|
|
static_cast<void>(frame_policy_lifetime.compare_exchange_strong(
|
|
empty, owner, std::memory_order_release, std::memory_order_acquire));
|
|
}
|
|
void Plot::Private::release_frame_policy_lifetime_if_idle() {
|
|
if (std::ranges::any_of(frame_slots, [](const Managed_Frame& slot) {
|
|
return slot.state.load(std::memory_order_acquire) !=
|
|
Frame_State::available;
|
|
}))
|
|
return;
|
|
frame_policy_lifetime.store({}, std::memory_order_release);
|
|
}
|
|
void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
|
|
const auto observed_generation =
|
|
consumer_work_generation.load(std::memory_order_acquire);
|
|
consume_retired_frames();
|
|
if (consume_frame_policy_events()) refresh_schedule();
|
|
Plot_Render_Tick requested;
|
|
while (tick_requests.try_dequeue(requested)) keep_latest_tick(requested);
|
|
if (render_admission.load(std::memory_order_acquire) ==
|
|
Render_Admission_State::ready) {
|
|
auto tick = std::exchange(deferred_tick, {});
|
|
if (tick) clock_tick(*tick);
|
|
}
|
|
consume_retired_frames();
|
|
if (consume_frame_policy_events()) refresh_schedule();
|
|
tick_task_scheduled.store(false, std::memory_order_release);
|
|
if (consumer_work_generation.load(std::memory_order_acquire) !=
|
|
observed_generation ||
|
|
retired_frames.load(std::memory_order_acquire) ||
|
|
(render_admission.load(std::memory_order_acquire) ==
|
|
Render_Admission_State::ready && deferred_tick))
|
|
arm_tick_consumer(std::move(lifetime));
|
|
}
|
|
void Plot::Private::clock_tick(const Plot_Render_Tick& tick) {
|
|
if (terminal_failure.load(std::memory_order_acquire)) return;
|
|
const auto& pacing = pacing_state();
|
|
const bool accepted = pacing.render_enabled &&
|
|
(tick.source == Frame_Request_Source::immediate ||
|
|
(tick.source == Frame_Request_Source::periodic &&
|
|
pacing.mode == Frame_Pacing_Mode::fixed_rate) ||
|
|
(tick.source == Frame_Request_Source::maximum_rate &&
|
|
pacing.mode == Frame_Pacing_Mode::maximum_rate));
|
|
if (!accepted) {
|
|
with_frame_policy([](auto& policy) {
|
|
policy.record_policy_rejection();
|
|
});
|
|
return;
|
|
}
|
|
with_frame_policy([&](auto& policy) {
|
|
policy.record_request_accepted(tick.source);
|
|
});
|
|
render_frame(tick);
|
|
}
|
|
bool Plot::Private::mark_taskflow_trace(Render_Frame& frame) {
|
|
auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire);
|
|
while (remaining != 0) {
|
|
if (taskflow_trace_remaining.compare_exchange_weak(
|
|
remaining, remaining - 1, std::memory_order_acq_rel,
|
|
std::memory_order_acquire)) {
|
|
frame.request_taskflow_trace();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
void Plot::Private::store_trace(
|
|
std::atomic_uint64_t& control,
|
|
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
|
maximum_taskflow_trace_frames>& slots,
|
|
const Taskflow_Frame_Trace& value,
|
|
const nlohmann::json& captured_components,
|
|
const nlohmann::json& captured_backend) {
|
|
auto trace = std::make_shared<const nlohmann::json>(
|
|
taskflow_trace_json(value, captured_components, captured_backend));
|
|
auto state = control.load(std::memory_order_acquire);
|
|
for (;;) {
|
|
const auto requested = static_cast<std::uint32_t>(state >> 32U);
|
|
const auto captured = static_cast<std::uint32_t>(state);
|
|
if (captured >= requested) return;
|
|
slots[captured].store(trace, std::memory_order_release);
|
|
const auto next = (static_cast<std::uint64_t>(requested) << 32U) |
|
|
static_cast<std::uint64_t>(captured + 1U);
|
|
if (control.compare_exchange_weak(
|
|
state, next, std::memory_order_release,
|
|
std::memory_order_acquire))
|
|
return;
|
|
}
|
|
}
|
|
nlohmann::json Plot::Private::trace_response(
|
|
const std::atomic_uint64_t& control,
|
|
const std::atomic_size_t& remaining,
|
|
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
|
maximum_taskflow_trace_frames>& slots) const {
|
|
nlohmann::json frames = nlohmann::json::array();
|
|
const auto state = control.load(std::memory_order_acquire);
|
|
const auto requested = static_cast<std::uint32_t>(state >> 32U);
|
|
const auto captured = static_cast<std::uint32_t>(state);
|
|
for (std::uint32_t index = 0; index < captured; ++index) if (const auto trace = slots[index].load(std::memory_order_acquire)) frames.push_back(*trace);
|
|
const auto left = remaining.load(std::memory_order_acquire);
|
|
return {
|
|
{"protocol", "aethera.taskflow.frames"}, {"version", 1},
|
|
{"requested", requested}, {"remaining", left},
|
|
{"captured", frames.size()},
|
|
{"complete", requested != 0 && frames.size() == requested},
|
|
{"frames", std::move(frames)}
|
|
};
|
|
}
|
|
void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
|
if (terminal_failure.load(std::memory_order_acquire)) return;
|
|
const auto streams = stream_snapshot();
|
|
const auto& pacing = pacing_state();
|
|
const bool direct_diagnostics_frame =
|
|
streams.consumers->empty() &&
|
|
tick.source == Frame_Request_Source::immediate;
|
|
if (!pacing.render_enabled ||
|
|
(streams.consumers->empty() && !direct_diagnostics_frame))
|
|
return;
|
|
auto admission_expected = Render_Admission_State::ready;
|
|
if (!render_admission.compare_exchange_strong(
|
|
admission_expected, Render_Admission_State::rendering,
|
|
std::memory_order_acq_rel,
|
|
std::memory_order_acquire)) {
|
|
keep_latest_tick(tick);
|
|
return;
|
|
}
|
|
std::size_t slot_index{};
|
|
Managed_Frame* managed{};
|
|
/*
|
|
* 只有 rendering 槽受 Scene 不可重入门约束;consuming 槽表示上一帧
|
|
* 已经完成 Plot 像素发布,外接 Drogon 图库流仍可继续持有该物理帧的
|
|
* 诊断生命周期。只要还有 available 槽,下一帧即可进入。
|
|
*/
|
|
for (std::size_t index = 0; index < frame_slots.size(); ++index) {
|
|
auto* candidate = &frame_slots[index];
|
|
auto* published = candidate;
|
|
const bool was_latest = latest_statistics_frame.compare_exchange_strong(
|
|
published, nullptr, std::memory_order_acq_rel,
|
|
std::memory_order_acquire);
|
|
if (candidate->diagnostic_readers.load(std::memory_order_acquire) != 0) {
|
|
if (was_latest) {
|
|
Managed_Frame* empty{};
|
|
static_cast<void>(latest_statistics_frame.compare_exchange_strong(
|
|
empty, candidate, std::memory_order_release,
|
|
std::memory_order_relaxed));
|
|
}
|
|
continue;
|
|
}
|
|
auto expected = Frame_State::available;
|
|
if (!candidate->state.compare_exchange_strong(
|
|
expected, Frame_State::rendering,
|
|
std::memory_order_acq_rel, std::memory_order_acquire)) {
|
|
if (was_latest) {
|
|
Managed_Frame* empty{};
|
|
static_cast<void>(latest_statistics_frame.compare_exchange_strong(
|
|
empty, candidate, std::memory_order_release,
|
|
std::memory_order_relaxed));
|
|
}
|
|
continue;
|
|
}
|
|
slot_index = index;
|
|
managed = &frame_slots[index];
|
|
break;
|
|
}
|
|
if (!managed) {
|
|
with_frame_policy([](auto& policy) {
|
|
policy.record_frame_slot_backpressure();
|
|
});
|
|
keep_latest_tick(tick);
|
|
/*
|
|
* 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能
|
|
* 立即 arm tick consumer,否则会在没有任何槽可用期间形成
|
|
* consume -> no slot -> consume 的 Taskflow 任务风暴。真正的唤醒点
|
|
* 是 retire_completed_frame:某个 consuming 槽变回 available 后只唤醒一次。
|
|
*/
|
|
render_admission.store(
|
|
Render_Admission_State::frame_slots_exhausted,
|
|
std::memory_order_release);
|
|
return;
|
|
}
|
|
retain_frame_policy_lifetime();
|
|
managed->presentation_time =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(
|
|
std::chrono::duration<double, std::milli>(tick.time_milliseconds));
|
|
managed->tick_issued_at = tick.issued_at;
|
|
managed->submitted_at = {};
|
|
const auto rollback_unsubmitted = [this, slot_index] {
|
|
auto& slot = frame_slots[slot_index];
|
|
auto expected = Frame_State::rendering;
|
|
static_cast<void>(slot.state.compare_exchange_strong(
|
|
expected, Frame_State::available, std::memory_order_acq_rel,
|
|
std::memory_order_acquire));
|
|
release_frame_policy_lifetime_if_idle();
|
|
};
|
|
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 {
|
|
if (streams.consumers->empty()) {
|
|
tick.width = std::clamp(tick.width, 160U, 1920U) & ~1U;
|
|
tick.height = std::clamp(tick.height, 120U, 1080U) & ~1U;
|
|
}
|
|
else {
|
|
tick.width = streams.width;
|
|
tick.height = streams.height;
|
|
}
|
|
const std::uint64_t sequence = next_frame_sequence++;
|
|
const Frame_Identity identity{
|
|
sequence, tick.sequence == 0 ? sequence : tick.sequence
|
|
};
|
|
const auto request_source = tick.source;
|
|
const auto& policy_state = pacing_state();
|
|
Render_Frame* logical_frame{};
|
|
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
|
|
(*frame_2d)->begin(identity, Frame_2D::native_pixel_format,
|
|
request_source, policy_state);
|
|
logical_frame = frame_2d->get();
|
|
}
|
|
else {
|
|
auto& frame_3d = std::get<std::unique_ptr<Frame_3D>>(managed->frame);
|
|
frame_3d->begin(identity,
|
|
pacing.pixel_delivery_enabled
|
|
? Frame_3D_Output::pixels
|
|
: Frame_3D_Output::diagnostics,
|
|
Frame_3D::native_pixel_format, request_source,
|
|
policy_state);
|
|
logical_frame = frame_3d.get();
|
|
}
|
|
taskflow_trace_claimed = mark_taskflow_trace(*logical_frame);
|
|
/*
|
|
* 各图的采样、网格构造和属性读取都在 Plot 自己的准备域完成。
|
|
* 进入 Scene::render 后只剩已经准备好的 Visual 批次与轻量提交;
|
|
* 共享 Render Domain 不承担业务数据生成。
|
|
*/
|
|
logical_frame->mark(Frame_Trace_Marker::plot_update_started);
|
|
const auto update_started = std::chrono::steady_clock::now();
|
|
view->update(tick);
|
|
const auto update_elapsed = std::chrono::steady_clock::now() - update_started;
|
|
logical_frame->mark(Frame_Trace_Marker::plot_update_finished);
|
|
const auto tick_queue_elapsed = tick.issued_at.time_since_epoch().count() == 0
|
|
? std::chrono::steady_clock::duration::zero()
|
|
: update_started - tick.issued_at;
|
|
const auto record_plot_measurements = [&](Render_Frame& frame) {
|
|
const auto nanoseconds = [](std::chrono::steady_clock::duration duration) {
|
|
return static_cast<std::uint64_t>(std::max<std::int64_t>(0,
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count()));
|
|
};
|
|
frame.record(Frame_Trace_Measurement::plot_tick_queue_ns,
|
|
nanoseconds(tick_queue_elapsed));
|
|
frame.record(Frame_Trace_Measurement::plot_update_ns,
|
|
nanoseconds(update_elapsed));
|
|
};
|
|
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
|
|
auto& output = *std::get<std::unique_ptr<Frame_2D>>(managed->frame);
|
|
record_plot_measurements(output);
|
|
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(
|
|
Size{static_cast<int>(tick.width), static_cast<int>(tick.height)});
|
|
const auto result = (*scene_2d)->render(
|
|
&output,
|
|
[weak = lifetime](not_null<Frame_2D*> frame) {
|
|
if (auto owner = weak.lock()) {
|
|
try {
|
|
owner->d->publish_completed_frame(frame);
|
|
owner->d->consume_completed_frame(frame);
|
|
owner->d->retire_completed_frame(frame);
|
|
}
|
|
catch (...) {
|
|
owner->d->fail(std::current_exception());
|
|
}
|
|
}
|
|
});
|
|
if (!result) {
|
|
with_frame_policy([](auto& policy) {
|
|
policy.record_scene_rejection();
|
|
});
|
|
rollback_unsubmitted();
|
|
restore_taskflow_trace_claim();
|
|
release_render_admission(lifetime);
|
|
}
|
|
else {
|
|
taskflow_trace_claimed = false;
|
|
managed->submitted_at = std::chrono::steady_clock::now();
|
|
with_frame_policy([&](auto& policy) {
|
|
policy.record_frame_submitted(
|
|
sequence, request_source,
|
|
managed->submitted_at - managed->tick_issued_at);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
auto& output = *std::get<std::unique_ptr<Frame_3D>>(managed->frame);
|
|
record_plot_measurements(output);
|
|
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
|
|
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height});
|
|
const auto weak = lifetime;
|
|
const auto result = scene_3d->render(
|
|
&output,
|
|
Render_Scene_3D::Frame_Callbacks{
|
|
.submitted = [weak](not_null<Frame_3D*>, bool) {
|
|
if (auto owner = weak.lock()) {
|
|
try {
|
|
owner->d->release_render_admission(weak);
|
|
}
|
|
catch (...) {
|
|
owner->d->fail(std::current_exception());
|
|
}
|
|
}
|
|
},
|
|
.completed = [weak](not_null<Frame_3D*> frame) {
|
|
if (auto owner = weak.lock()) {
|
|
try {
|
|
owner->d->publish_completed_frame(frame);
|
|
owner->d->consume_completed_frame(frame);
|
|
owner->d->retire_completed_frame(frame);
|
|
}
|
|
catch (...) {
|
|
owner->d->fail(std::current_exception());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (result == Render_Scene_3D::Render_Result::submitted) {
|
|
taskflow_trace_claimed = false;
|
|
managed->submitted_at = std::chrono::steady_clock::now();
|
|
with_frame_policy([&](auto& policy) {
|
|
policy.record_frame_submitted(
|
|
sequence, request_source,
|
|
managed->submitted_at - managed->tick_issued_at);
|
|
});
|
|
return;
|
|
}
|
|
with_frame_policy([](auto& policy) {
|
|
policy.record_scene_rejection();
|
|
});
|
|
rollback_unsubmitted();
|
|
restore_taskflow_trace_claim();
|
|
release_render_admission(lifetime);
|
|
if (result == Render_Scene_3D::Render_Result::backend_unavailable) throw std::runtime_error("3D render backend became unavailable before submission");
|
|
}
|
|
catch (...) {
|
|
rollback_unsubmitted();
|
|
restore_taskflow_trace_claim();
|
|
release_render_admission(lifetime);
|
|
throw;
|
|
}
|
|
}
|
|
void Plot::Private::publish_completed_frame(
|
|
not_null<Render_Frame*> completed) {
|
|
Managed_Frame* managed{};
|
|
for (auto& slot : frame_slots) {
|
|
const auto frame = std::visit(
|
|
[](const auto& value) -> not_null<Render_Frame*> {
|
|
return not_null{value.get()};
|
|
},
|
|
slot.frame);
|
|
if (frame.get() != completed.get()) continue;
|
|
managed = &slot;
|
|
break;
|
|
}
|
|
if (!managed) throw std::logic_error("completed frame has no owning Plot policy slot");
|
|
if (managed->state.load(std::memory_order_acquire) !=
|
|
Frame_State::rendering)
|
|
throw std::logic_error("completed Plot frame is not rendering");
|
|
const auto frame = completed;
|
|
const auto& pacing = pacing_state();
|
|
const auto identity = frame->identity();
|
|
Frame_Identity rendered_identity = identity;
|
|
std::shared_ptr<const std::vector<std::byte>> pixel_storage;
|
|
Plot_Pixel_Layout pixel_layout{Plot_Pixel_Layout::rgba8};
|
|
std::uint32_t width{};
|
|
std::uint32_t height{};
|
|
if (auto* frame_2d =
|
|
std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
|
|
pixel_layout = Plot_Pixel_Layout::bgra8;
|
|
const auto image = (*frame_2d)->image();
|
|
width = static_cast<std::uint32_t>(image.width);
|
|
height = static_cast<std::uint32_t>(image.height);
|
|
if (pacing.pixel_delivery_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.pixel_delivery_enabled && frame_3d->output() == Frame_3D_Output::pixels) pixel_storage = frame_3d->share_pixels();
|
|
}
|
|
auto pixels = std::make_shared<const Plot_Pixel_Frame>(Plot_Pixel_Frame{
|
|
std::move(pixel_storage), pixel_layout, managed->presentation_time,
|
|
identity.sequence, identity.correlation_id, rendered_identity.sequence,
|
|
rendered_identity.correlation_id, width, height
|
|
});
|
|
const auto published = std::make_shared<const Plot_Stream_Frame>(
|
|
Plot_Stream_Frame{{}, std::move(pixels)});
|
|
const auto publish_started = std::chrono::steady_clock::now();
|
|
publish(std::move(published));
|
|
frame->record(Frame_Trace_Measurement::plot_publish_ns,
|
|
static_cast<std::uint64_t>(std::max<std::int64_t>(
|
|
0, std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
std::chrono::steady_clock::now() - publish_started)
|
|
.count())));
|
|
auto expected = Frame_State::rendering;
|
|
if (!managed->state.compare_exchange_strong(
|
|
expected, Frame_State::consuming, std::memory_order_acq_rel,
|
|
std::memory_order_acquire))
|
|
throw std::logic_error("Plot frame left rendering before pixel publish");
|
|
}
|
|
void Plot::Private::consume_completed_frame(not_null<Render_Frame*> frame) {
|
|
Managed_Frame* managed{};
|
|
for (auto& slot : frame_slots) {
|
|
const auto address = std::visit(
|
|
[](const auto& value) -> not_null<Render_Frame*> {
|
|
return not_null{value.get()};
|
|
},
|
|
slot.frame);
|
|
if (address.get() != frame.get()) 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 完成即释放 Plot admission。媒体采样属于 Gallery 自己的独立时钟和
|
|
* DAG,不再借 Plot 保存一套 post-publish 管线状态。 */
|
|
if (std::holds_alternative<std::unique_ptr<Frame_2D>>(managed->frame))
|
|
release_render_admission(lifetime);
|
|
}
|
|
void Plot::Private::consume_retired_frames() {
|
|
for (;;) {
|
|
auto* list = retired_frames.exchange(nullptr, std::memory_order_acq_rel);
|
|
if (!list) break;
|
|
std::vector<Managed_Frame*> frames;
|
|
while (list) {
|
|
auto* next = list->retired_next.exchange(
|
|
nullptr, std::memory_order_relaxed);
|
|
frames.push_back(list);
|
|
list = next;
|
|
}
|
|
std::ranges::sort(frames, {}, [](const Managed_Frame* managed) {
|
|
return std::visit(
|
|
[](const auto& value) {
|
|
return value->identity().sequence;
|
|
},
|
|
managed->frame);
|
|
});
|
|
for (auto* managed : frames) {
|
|
auto* frame = std::visit(
|
|
[](const auto& value) -> Render_Frame* {
|
|
return value.get();
|
|
},
|
|
managed->frame);
|
|
finalize_retired_frame(frame);
|
|
}
|
|
}
|
|
}
|
|
void Plot::Private::retire_completed_frame(not_null<Render_Frame*> frame) {
|
|
Managed_Frame* managed{};
|
|
for (auto& slot : frame_slots) {
|
|
const auto address = std::visit(
|
|
[](const auto& value) -> not_null<Render_Frame*> {
|
|
return not_null{value.get()};
|
|
},
|
|
slot.frame);
|
|
if (address.get() != frame.get()) continue;
|
|
managed = &slot;
|
|
break;
|
|
}
|
|
if (!managed) throw std::logic_error("retired frame has no owned Plot slot");
|
|
auto* head = retired_frames.load(std::memory_order_relaxed);
|
|
do {
|
|
managed->retired_next.store(head, std::memory_order_relaxed);
|
|
}
|
|
while (!retired_frames.compare_exchange_weak(
|
|
head, managed, std::memory_order_release,
|
|
std::memory_order_relaxed));
|
|
consumer_work_generation.fetch_add(1, std::memory_order_release);
|
|
arm_tick_consumer(lifetime);
|
|
}
|
|
void Plot::Private::finalize_retired_frame(not_null<Render_Frame*> frame) {
|
|
Managed_Frame* managed{};
|
|
for (auto& slot : frame_slots) {
|
|
const auto address = std::visit(
|
|
[](const auto& value) -> not_null<Render_Frame*> {
|
|
return not_null{value.get()};
|
|
},
|
|
slot.frame);
|
|
if (address.get() == frame.get()) {
|
|
managed = &slot;
|
|
break;
|
|
}
|
|
}
|
|
if (!managed) throw std::logic_error("retired frame has no owned Plot slot");
|
|
const auto completion_latency =
|
|
managed->submitted_at.time_since_epoch().count() == 0
|
|
? std::chrono::steady_clock::duration::zero()
|
|
: std::chrono::steady_clock::now() - managed->submitted_at;
|
|
std::optional<Datoviz_Frame_Observation> datoviz_observation;
|
|
if (auto* frame_3d = dynamic_cast<Frame_3D*>(frame.get())) {
|
|
datoviz_observation = frame_3d->take_datoviz_observation();
|
|
if (!datoviz_observation)
|
|
throw std::logic_error(
|
|
"completed 3D frame has no Datoviz observation");
|
|
frame_policy_3d->record_frame_completed(
|
|
frame->identity().sequence, completion_latency,
|
|
datoviz_observation->prepare_released_after_submission);
|
|
}
|
|
else {
|
|
frame_policy_2d->record_frame_completed(
|
|
frame->identity().sequence, completion_latency);
|
|
}
|
|
const auto statistics_generation_value =
|
|
statistics_generation.load(std::memory_order_acquire);
|
|
if (applied_statistics_generation != statistics_generation_value) {
|
|
completed_frame_statistics.reset();
|
|
applied_statistics_generation = statistics_generation_value;
|
|
}
|
|
managed->statistics = completed_frame_statistics.submit(*frame);
|
|
managed->statistics_generation = statistics_generation_value;
|
|
std::optional<Taskflow_Frame_Trace> captured_trace;
|
|
nlohmann::json captured_components;
|
|
nlohmann::json captured_backend;
|
|
if (frame->taskflow_trace_requested()) {
|
|
captured_trace.emplace(frame->take_taskflow_trace());
|
|
std::unordered_set<std::uint64_t> executed_nodes;
|
|
for (const auto& execution : captured_trace->tasks) executed_nodes.insert(execution.native_id);
|
|
std::vector<std::string> executed_components;
|
|
for (const auto& graph : captured_trace->graphs) {
|
|
for (const auto& node : graph.nodes) {
|
|
if (!executed_nodes.contains(node.native_id)) continue;
|
|
const auto owner = std::ranges::find(
|
|
node.attributes, "owner_component",
|
|
&std::pair<std::string, std::string>::first);
|
|
if (owner == node.attributes.end() || owner->second.empty() ||
|
|
std::ranges::find(executed_components, owner->second) !=
|
|
executed_components.end())
|
|
continue;
|
|
executed_components.push_back(owner->second);
|
|
}
|
|
}
|
|
captured_components = view->capture_components(executed_components);
|
|
if (datoviz_observation) captured_backend = datoviz_observation_json(*datoviz_observation);
|
|
}
|
|
auto expected = Frame_State::consuming;
|
|
if (!managed->state.compare_exchange_strong(
|
|
expected, Frame_State::available, std::memory_order_acq_rel,
|
|
std::memory_order_acquire))
|
|
throw std::logic_error("retired Plot frame is not consuming");
|
|
latest_statistics_frame.store(managed, std::memory_order_release);
|
|
/* 物理槽是唯一背压原因;归还任意槽后只解除一次耗尽状态。 */
|
|
auto admission = Render_Admission_State::frame_slots_exhausted;
|
|
static_cast<void>(render_admission.compare_exchange_strong(
|
|
admission, Render_Admission_State::ready,
|
|
std::memory_order_acq_rel, std::memory_order_acquire));
|
|
arm_tick_consumer(lifetime);
|
|
release_frame_policy_lifetime_if_idle();
|
|
if (captured_trace) {
|
|
auto owner = lifetime;
|
|
schedule_task("plot.taskflow.serialize", [owner,
|
|
trace = std::move(*captured_trace),
|
|
components = std::move(captured_components),
|
|
backend = std::move(captured_backend)]() mutable {
|
|
if (const auto plot = owner.lock())
|
|
plot->d->store_trace(
|
|
plot->d->taskflow_trace_control,
|
|
plot->d->taskflow_trace_slots, trace, components,
|
|
backend);
|
|
});
|
|
}
|
|
}
|
|
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::ensure_started() {
|
|
std::call_once(d->start_once, [this] {
|
|
const auto weak = weak_from_this();
|
|
d->lifetime = weak;
|
|
d->frame_timer = Frame_Scheduler::instance().make_timer(
|
|
[weak](Frame_Scheduler::Tick tick) {
|
|
if (const auto owner = weak.lock()) {
|
|
owner->schedule_render(Plot_Render_Tick{
|
|
.issued_at = tick.issued_at,
|
|
.sequence = tick.sequence,
|
|
.time_milliseconds = tick.time_milliseconds,
|
|
.source = Frame_Request_Source::periodic
|
|
});
|
|
}
|
|
});
|
|
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.plot.stream"},
|
|
{"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) {
|
|
if (!std::isfinite(tick.time_milliseconds) ||
|
|
tick.time_milliseconds < 0.0)
|
|
throw std::invalid_argument(
|
|
"render time_milliseconds must be finite and non-negative");
|
|
if (tick.width == 0 || tick.height == 0) throw std::invalid_argument("render viewport must be non-zero");
|
|
ensure_started();
|
|
if (d->terminal_failure.load(std::memory_order_acquire)) return;
|
|
d->submit_tick_request(std::move(tick));
|
|
d->arm_tick_consumer(weak_from_this());
|
|
}
|
|
void Plot::render_once() {
|
|
ensure_started();
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const auto elapsed = now - d->clock_origin;
|
|
schedule_render(Plot_Render_Tick{
|
|
.issued_at = now,
|
|
.time_milliseconds =
|
|
std::chrono::duration<double, std::milli>(elapsed).count(),
|
|
.source = Frame_Request_Source::immediate
|
|
});
|
|
}
|
|
void Plot::submit_input(Plot_Input_Event event) {
|
|
static_cast<void>(input_timeline_time(event.time_milliseconds));
|
|
ensure_started();
|
|
if (d->terminal_failure.load(std::memory_order_acquire)) return;
|
|
/*
|
|
* WebSocket 线程只向 Scene 的当前事件缓冲追加一个由 Scene
|
|
* memory_resource 分配的基类指针。Prepare 边界交换完整批次,
|
|
* Scene 在 Renderable 完成消费时按 Event_Type 增量统计,并随自身
|
|
* State 双缓冲发布;Web 层只在低频 diagnostics 请求中读取结果。
|
|
*/
|
|
try {
|
|
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) dispatch_plot_input(**scene_2d, event);
|
|
else dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene), event);
|
|
}
|
|
catch (...) {
|
|
d->fail(std::current_exception());
|
|
}
|
|
}
|
|
nlohmann::json Plot::schema() {
|
|
ensure_started();
|
|
return d->schema();
|
|
}
|
|
nlohmann::json Plot::write_prop(std::string_view component,
|
|
std::string_view key,
|
|
const nlohmann::json& value) {
|
|
ensure_started();
|
|
if (component != "frame-analysis") return d->view->write_prop(component, key, value);
|
|
auto result = d->with_frame_policy([&](auto& policy) {
|
|
return write_frame_policy_prop(policy, key, value);
|
|
});
|
|
if (result.value("success", false)) d->arm_tick_consumer(weak_from_this());
|
|
return result;
|
|
}
|
|
nlohmann::json Plot::component_state(std::string_view component) const {
|
|
return d->view->component_state(component);
|
|
}
|
|
nlohmann::json Plot::generate_data(const nlohmann::json& input) {
|
|
ensure_started();
|
|
return d->view->generate_data(input);
|
|
}
|
|
nlohmann::json Plot::diagnostics() const {
|
|
nlohmann::json frame_statistics = nlohmann::json::object();
|
|
nlohmann::json input_statistics = nlohmann::json::object();
|
|
Frame_Identity identity{};
|
|
std::uint64_t created_time_unix_ns{};
|
|
std::uint64_t dropped_sequences{};
|
|
std::uint32_t completed_width{};
|
|
std::uint32_t completed_height{};
|
|
double frame_rate{};
|
|
bool is_3d{};
|
|
const auto read_scene_statistics = [&](const auto& state) {
|
|
append_event_statistics_json(input_statistics, state.event_statistics);
|
|
};
|
|
std::visit([&](const auto& scene) {
|
|
using Scene_Pointer = std::remove_cvref_t<decltype(scene)>;
|
|
if constexpr (std::same_as<Scene_Pointer, std::unique_ptr<Scene_2D>>) {
|
|
scene->template access_state<Render_Scene_2D::Base_Tag>(
|
|
read_scene_statistics);
|
|
}
|
|
else {
|
|
is_3d = true;
|
|
scene->template access_state<Render_Scene_3D::Base_Tag>(
|
|
read_scene_statistics);
|
|
}
|
|
}, d->scene);
|
|
{
|
|
const auto generation =
|
|
d->statistics_generation.load(std::memory_order_acquire);
|
|
auto* completed_frame =
|
|
d->latest_statistics_frame.load(std::memory_order_acquire);
|
|
Frame_Statistics_State statistics{};
|
|
if (completed_frame) {
|
|
completed_frame->diagnostic_readers.fetch_add(
|
|
1, std::memory_order_acq_rel);
|
|
if (d->latest_statistics_frame.load(std::memory_order_acquire) ==
|
|
completed_frame &&
|
|
completed_frame->state.load(std::memory_order_acquire) ==
|
|
Private::Frame_State::available &&
|
|
completed_frame->statistics_generation == generation) {
|
|
statistics = completed_frame->statistics;
|
|
std::visit([&](const auto& frame) {
|
|
using Frame_Pointer =
|
|
std::remove_cvref_t<decltype(frame)>;
|
|
if constexpr (std::same_as<
|
|
Frame_Pointer,
|
|
std::unique_ptr<Frame_2D>>) {
|
|
const auto image = frame->image();
|
|
completed_width = static_cast<std::uint32_t>(
|
|
std::max(0, image.width));
|
|
completed_height = static_cast<std::uint32_t>(
|
|
std::max(0, image.height));
|
|
}
|
|
else {
|
|
const auto extent = frame->extent();
|
|
completed_width = extent.width;
|
|
completed_height = extent.height;
|
|
}
|
|
}, completed_frame->frame);
|
|
}
|
|
completed_frame->diagnostic_readers.fetch_sub(
|
|
1, std::memory_order_release);
|
|
}
|
|
append_statistic_json(frame_statistics, statistics);
|
|
identity = statistics.identity;
|
|
created_time_unix_ns = statistics.created_time_unix_ns;
|
|
dropped_sequences = statistics.dropped_sequences;
|
|
const auto& interval = statistics.values[
|
|
static_cast<std::size_t>(Frame_Statistic::frame_interval_ms)];
|
|
frame_rate = interval.trimmed_average > 0.0
|
|
? 1'000.0 / interval.trimmed_average
|
|
: 0.0;
|
|
}
|
|
const auto& pacing = d->pacing_state();
|
|
const auto stream = d->stream_snapshot();
|
|
const auto admission = d->render_admission.load(std::memory_order_acquire);
|
|
const auto admission_name = [&] {
|
|
switch (admission) {
|
|
case Private::Render_Admission_State::ready: return "ready";
|
|
case Private::Render_Admission_State::rendering: return "rendering";
|
|
case Private::Render_Admission_State::frame_slots_exhausted: return "frame_slots_exhausted";
|
|
}
|
|
return "unknown";
|
|
}();
|
|
nlohmann::json supported_formats = nlohmann::json::array();
|
|
if (is_3d) {
|
|
for (const auto format : Frame_3D::supported_pixel_formats) supported_formats.push_back(pixel_format_name(format));
|
|
}
|
|
else {
|
|
for (const auto format : Frame_2D::supported_pixel_formats) supported_formats.push_back(pixel_format_name(format));
|
|
}
|
|
const auto format = is_3d
|
|
? pixel_format_name(Frame_3D::native_pixel_format)
|
|
: pixel_format_name(Frame_2D::native_pixel_format);
|
|
const auto native_format = is_3d
|
|
? pixel_format_name(Frame_3D::native_pixel_format)
|
|
: pixel_format_name(Frame_2D::native_pixel_format);
|
|
const auto pixel_width = completed_width == 0
|
|
? stream.width
|
|
: completed_width;
|
|
const auto pixel_height = completed_height == 0
|
|
? stream.height
|
|
: completed_height;
|
|
const std::size_t byte_length = pacing.pixel_delivery_enabled
|
|
? static_cast<std::size_t>(pixel_width) * pixel_height * 4U
|
|
: 0U;
|
|
nlohmann::json output{
|
|
{"protocol", "aethera.plot.diagnostics"}, {"version", 4},
|
|
{"dimension", is_3d ? "3D" : "2D"},
|
|
{"sequence", identity.sequence},
|
|
{"correlation_id", identity.correlation_id},
|
|
{"rendered_sequence", identity.sequence},
|
|
{"rendered_correlation_id", identity.correlation_id},
|
|
{
|
|
"generated_time_unix_ms",
|
|
static_cast<double>(created_time_unix_ns) / 1'000'000.0
|
|
},
|
|
{"delivery", pacing.pixel_delivery_enabled ? "gallery-pixels" : "diagnostics"},
|
|
{"frame_rate_fps", frame_rate},
|
|
{"dropped_sequence_count", dropped_sequences},
|
|
{"window_capacity", diagnostic_window_capacity},
|
|
{
|
|
"pixel", {
|
|
{"width", pixel_width}, {"height", pixel_height},
|
|
{"format", format}, {"native_format", native_format},
|
|
{"supported_formats", std::move(supported_formats)},
|
|
{"byte_length", byte_length}
|
|
}
|
|
},
|
|
{"frame_policy", frame_policy_state_json(pacing)},
|
|
{"render_admission", admission_name},
|
|
{"frame_statistics", std::move(frame_statistics)},
|
|
{"input_statistics", std::move(input_statistics)}
|
|
};
|
|
if (is_3d) {
|
|
const auto& pipeline = d->frame_policy_3d->read_state<
|
|
Frame_Policy_3D::Base_Tag>();
|
|
output["frame_policy"]["three_dimensional_pipeline"] = {
|
|
{"capacity", Frame_Policy_3D::pipeline_capacity},
|
|
{"overlapped_release_count", pipeline.overlapped_release_count},
|
|
{
|
|
"completion_gated_release_count",
|
|
pipeline.completion_gated_release_count
|
|
},
|
|
{"gpu_completion_count", pipeline.gpu_completion_count},
|
|
{"last_completed_sequence", pipeline.last_completed_sequence}
|
|
};
|
|
const auto& gpu = render_3d::detail::Gpu_Completion_Service::instance().
|
|
read_state<render_3d::detail::Gpu_Completion_Service::Base_Tag>();
|
|
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::reset_diagnostics() {
|
|
std::visit([](auto& scene) {
|
|
scene->template update_state<&Scene::State::event_statistics>(
|
|
Event_Statistics_State{});
|
|
}, d->scene);
|
|
d->statistics_generation.fetch_add(1, std::memory_order_acq_rel);
|
|
d->with_frame_policy([](auto& policy) {
|
|
policy.reset_statistics();
|
|
});
|
|
d->arm_tick_consumer(weak_from_this());
|
|
}
|
|
}
|