优化
This commit is contained in:
@@ -18,6 +18,7 @@ struct Task_Graph::Private {
|
||||
};
|
||||
explicit Private(std::string graph_name)
|
||||
: taskflow(std::move(graph_name)) {}
|
||||
std::vector<std::pair<std::string, std::string>> attributes{};
|
||||
tf::Taskflow taskflow{}; /* 第三方 Taskflow 仅存在于本实现单元。 */
|
||||
std::vector<Node> nodes{}; /* 与原生 graph 插入顺序一致的业务节点。 */
|
||||
std::unordered_map<std::string, std::size_t> name_counts{}; /* 同名业务节点的下一个序号。 */
|
||||
@@ -31,7 +32,16 @@ struct Task_Graph::Private {
|
||||
: base + "#" + std::to_string(occurrence);
|
||||
}
|
||||
void collect_nodes(std::string_view parent,
|
||||
const std::vector<std::pair<std::string, std::string>>& inherited,
|
||||
std::vector<Taskflow_Graph_Trace::Node>& result) const {
|
||||
auto graph_attributes = inherited;
|
||||
for (const auto& attribute : attributes) {
|
||||
const auto found = std::ranges::find(
|
||||
graph_attributes, attribute.first,
|
||||
&std::pair<std::string, std::string>::first);
|
||||
if (found == graph_attributes.end()) graph_attributes.push_back(attribute);
|
||||
else found->second = attribute.second;
|
||||
}
|
||||
for (const auto& source : nodes) {
|
||||
Taskflow_Graph_Trace::Node node{};
|
||||
node.native_id = static_cast<std::uint64_t>(source.task.hash_value());
|
||||
@@ -46,7 +56,14 @@ struct Task_Graph::Private {
|
||||
node.parent_node_id = std::string(parent);
|
||||
node.name = source.name;
|
||||
node.type = std::string(tf::to_string(source.task.type()));
|
||||
node.attributes = source.attributes;
|
||||
node.attributes = graph_attributes;
|
||||
for (const auto& attribute : source.attributes) {
|
||||
const auto found = std::ranges::find(
|
||||
node.attributes, attribute.first,
|
||||
&std::pair<std::string, std::string>::first);
|
||||
if (found == node.attributes.end()) node.attributes.push_back(attribute);
|
||||
else found->second = attribute.second;
|
||||
}
|
||||
source.task.for_each_predecessor([&](tf::Task value) {
|
||||
node.predecessors.push_back(
|
||||
static_cast<std::uint64_t>(value.hash_value()));
|
||||
@@ -57,7 +74,8 @@ struct Task_Graph::Private {
|
||||
});
|
||||
const auto module_id = node.node_id;
|
||||
result.push_back(std::move(node));
|
||||
if (source.child) source.child->collect_nodes(module_id, result);
|
||||
if (source.child)
|
||||
source.child->collect_nodes(module_id, node.attributes, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +101,7 @@ void* Task_Graph_Access::native_storage(Task_Graph& graph) noexcept {
|
||||
std::vector<Taskflow_Graph_Trace::Node> Task_Graph_Access::nodes(
|
||||
const Task_Graph& graph) {
|
||||
std::vector<Taskflow_Graph_Trace::Node> result;
|
||||
graph.d->collect_nodes({}, result);
|
||||
graph.d->collect_nodes({}, {}, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -146,6 +164,7 @@ Task_Graph& Task_Graph::operator=(Task_Graph&& other) noexcept {
|
||||
}
|
||||
d->taskflow = std::move(other.d->taskflow);
|
||||
d->nodes = std::move(other.d->nodes);
|
||||
d->attributes = std::move(other.d->attributes);
|
||||
d->name_counts = std::move(other.d->name_counts);
|
||||
++d->generation;
|
||||
return *this;
|
||||
@@ -214,6 +233,18 @@ Task_Node Task_Graph::compose(std::string task_name, Task_Graph& child) {
|
||||
Task_Node::Private{d, d->nodes.size() - 1, d->generation})};
|
||||
}
|
||||
|
||||
Task_Graph& Task_Graph::describe(std::string key, std::string value) {
|
||||
if (key.empty())
|
||||
throw std::invalid_argument("Task graph attribute key is empty");
|
||||
const auto found = std::ranges::find(
|
||||
d->attributes, key, &std::pair<std::string, std::string>::first);
|
||||
if (found == d->attributes.end())
|
||||
d->attributes.emplace_back(std::move(key), std::move(value));
|
||||
else
|
||||
found->second = std::move(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Task_Graph::clear() {
|
||||
d->taskflow.clear();
|
||||
d->nodes.clear();
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
Task_Node add_condition(std::string name, std::function<int()> work);
|
||||
/* 添加模块节点并引用 child;child 必须活到本图完成执行。 */
|
||||
Task_Node compose(std::string name, Task_Graph& child);
|
||||
Task_Graph& describe(std::string key, std::string value);
|
||||
void clear();
|
||||
[[nodiscard]] bool empty() const noexcept;
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
|
||||
+1
-113
@@ -66,7 +66,7 @@ void Render_Frame::record(Frame_Trace_Measurement measurement, std::uint64_t val
|
||||
std::uint64_t expected{};
|
||||
d->measurements[index].compare_exchange_strong(expected, encode_present_value(value_ns), std::memory_order_release, std::memory_order_relaxed);
|
||||
}
|
||||
Frame_Statistics_Sample Render_Frame::statistics(Frame_Dimension dimension) const {
|
||||
Frame_Statistics_Sample Render_Frame::statistics() const {
|
||||
std::array<std::optional<double>, marker_count> markers{};
|
||||
for (std::size_t index = 0; index < marker_count; ++index) {
|
||||
const auto encoded = d->markers[index].load(std::memory_order_acquire);
|
||||
@@ -108,16 +108,10 @@ Frame_Statistics_Sample Render_Frame::statistics(Frame_Dimension dimension) cons
|
||||
result.set(Frame_Statistic::event_dispatch_ms, interval(
|
||||
Frame_Trace_Marker::event_dispatch_started,
|
||||
Frame_Trace_Marker::event_dispatch_finished));
|
||||
result.set(Frame_Statistic::prepare_ms, interval(
|
||||
Frame_Trace_Marker::prepare_started, Frame_Trace_Marker::prepare_finished));
|
||||
result.set(Frame_Statistic::paint_ms, interval(
|
||||
Frame_Trace_Marker::paint_started, Frame_Trace_Marker::paint_finished));
|
||||
result.set(Frame_Statistic::backend_queue_ms, interval(
|
||||
Frame_Trace_Marker::backend_queue_entered, Frame_Trace_Marker::backend_queue_left));
|
||||
result.set(Frame_Statistic::gpu_submission_ms, interval(
|
||||
Frame_Trace_Marker::gpu_submitted, Frame_Trace_Marker::gpu_completed));
|
||||
result.set(Frame_Statistic::readback_stage_ms, interval(
|
||||
Frame_Trace_Marker::readback_started, Frame_Trace_Marker::readback_finished));
|
||||
result.set(Frame_Statistic::callback_ms, interval(
|
||||
Frame_Trace_Marker::callback_started, Frame_Trace_Marker::callback_finished));
|
||||
|
||||
@@ -146,112 +140,6 @@ Frame_Statistics_Sample Render_Frame::statistics(Frame_Dimension dimension) cons
|
||||
for (std::size_t index = 0; index < measurement_statistics.size(); ++index)
|
||||
result.set(measurement_statistics[index], measurement(measurement_keys[index]));
|
||||
|
||||
double remaining = marker(Frame_Trace_Marker::frame_ready);
|
||||
const auto take = [&](double requested) {
|
||||
const auto value = std::min(remaining, std::max(0.0, requested));
|
||||
remaining -= value;
|
||||
return value;
|
||||
};
|
||||
const double scene_time = interval(Frame_Trace_Marker::scene_render_started,
|
||||
Frame_Trace_Marker::scene_render_finished);
|
||||
const double event_time = std::min(scene_time, interval(
|
||||
Frame_Trace_Marker::event_dispatch_started,
|
||||
Frame_Trace_Marker::event_dispatch_finished));
|
||||
const double prepare_time = std::min(std::max(0.0, scene_time - event_time), interval(
|
||||
Frame_Trace_Marker::prepare_started, Frame_Trace_Marker::prepare_finished));
|
||||
const double paint_time = std::min(
|
||||
std::max(0.0, scene_time - event_time - prepare_time), interval(
|
||||
Frame_Trace_Marker::paint_started, Frame_Trace_Marker::paint_finished));
|
||||
|
||||
if (dimension == Frame_Dimension::two_dimensional) {
|
||||
result.set(Frame_Statistic::pipeline_2d_event_ms, take(event_time));
|
||||
result.set(Frame_Statistic::pipeline_2d_prepare_ms, take(prepare_time));
|
||||
result.set(Frame_Statistic::pipeline_2d_paint_ms, paint_time);
|
||||
double paint_remaining = paint_time;
|
||||
const auto take_paint = [&](Frame_Trace_Marker first,
|
||||
Frame_Trace_Marker last) {
|
||||
const double value = std::min(paint_remaining, interval(first, last));
|
||||
paint_remaining -= value;
|
||||
return take(value);
|
||||
};
|
||||
result.set(Frame_Statistic::pipeline_2d_frame_target_ms, take_paint(
|
||||
Frame_Trace_Marker::paint_frame_target_started,
|
||||
Frame_Trace_Marker::paint_frame_target_finished));
|
||||
result.set(Frame_Statistic::pipeline_2d_background_ms, take_paint(
|
||||
Frame_Trace_Marker::paint_background_started,
|
||||
Frame_Trace_Marker::paint_background_finished));
|
||||
result.set(Frame_Statistic::pipeline_2d_cache_targets_ms, take_paint(
|
||||
Frame_Trace_Marker::paint_cache_targets_started,
|
||||
Frame_Trace_Marker::paint_cache_targets_finished));
|
||||
result.set(Frame_Statistic::pipeline_2d_taskflow_ms, take_paint(
|
||||
Frame_Trace_Marker::paint_taskflow_started,
|
||||
Frame_Trace_Marker::paint_taskflow_finished));
|
||||
result.set(Frame_Statistic::pipeline_2d_paint_coordination_ms,
|
||||
take(paint_remaining));
|
||||
result.set(Frame_Statistic::pipeline_2d_scene_coordination_ms,
|
||||
take(std::max(0.0, scene_time - event_time - prepare_time - paint_time)));
|
||||
result.set(Frame_Statistic::pipeline_2d_callback_ms, take(interval(
|
||||
Frame_Trace_Marker::callback_started, Frame_Trace_Marker::callback_finished)));
|
||||
result.set(Frame_Statistic::pipeline_2d_frame_handoff_ms, remaining);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.set(Frame_Statistic::pipeline_3d_event_ms, take(event_time));
|
||||
result.set(Frame_Statistic::pipeline_3d_prepare_ms, take(prepare_time));
|
||||
result.set(Frame_Statistic::pipeline_3d_submit_graph_ms, take(paint_time));
|
||||
result.set(Frame_Statistic::pipeline_3d_scene_coordination_ms,
|
||||
take(std::max(0.0, scene_time - event_time - prepare_time - paint_time)));
|
||||
const double scene_finished = marker(Frame_Trace_Marker::scene_render_finished);
|
||||
const double queue_entered = marker(Frame_Trace_Marker::backend_queue_entered);
|
||||
const double backend_prepare_started = marker(Frame_Trace_Marker::backend_prepare_started);
|
||||
const double backend_prepare_finished = marker(Frame_Trace_Marker::backend_prepare_finished);
|
||||
const double submit_queued = marker(Frame_Trace_Marker::backend_submit_queued);
|
||||
const double queue_left = marker(Frame_Trace_Marker::backend_queue_left);
|
||||
result.set(Frame_Statistic::pipeline_3d_prepare_queue_ms, take(std::max(
|
||||
0.0, backend_prepare_started - std::max(scene_finished, queue_entered))));
|
||||
double preparation_window = std::max(0.0,
|
||||
backend_prepare_finished - backend_prepare_started);
|
||||
const auto take_preparation = [&](Frame_Trace_Measurement key) {
|
||||
const double value = std::min(preparation_window,
|
||||
std::max(0.0, measurement(key)));
|
||||
preparation_window -= value;
|
||||
return take(value);
|
||||
};
|
||||
result.set(Frame_Statistic::pipeline_3d_backend_apply_ms,
|
||||
take_preparation(Frame_Trace_Measurement::backend_apply_ns));
|
||||
result.set(Frame_Statistic::pipeline_3d_backend_plan_ms,
|
||||
take_preparation(Frame_Trace_Measurement::backend_plan_ns));
|
||||
result.set(Frame_Statistic::pipeline_3d_backend_execute_ms,
|
||||
take_preparation(Frame_Trace_Measurement::backend_execute_ns));
|
||||
result.set(Frame_Statistic::pipeline_3d_backend_commands_ms, take(preparation_window));
|
||||
result.set(Frame_Statistic::pipeline_3d_backend_queue_ms,
|
||||
take(std::max(0.0, queue_left - submit_queued)));
|
||||
const double gpu_submitted = marker(Frame_Trace_Marker::gpu_submitted);
|
||||
double submit_window = std::max(0.0, gpu_submitted - queue_left);
|
||||
const double measured_submit = std::min(submit_window, std::max(
|
||||
0.0, measurement(Frame_Trace_Measurement::backend_submit_ns)));
|
||||
result.set(Frame_Statistic::pipeline_3d_backend_submit_ms, take(measured_submit));
|
||||
submit_window -= measured_submit;
|
||||
result.set(Frame_Statistic::pipeline_3d_submit_handoff_ms, take(submit_window));
|
||||
double gpu_window = interval(Frame_Trace_Marker::gpu_submitted,
|
||||
Frame_Trace_Marker::gpu_completed);
|
||||
const auto take_gpu = [&](Frame_Trace_Measurement key) {
|
||||
const double value = std::min(gpu_window, std::max(0.0, measurement(key)));
|
||||
gpu_window -= value;
|
||||
return take(value);
|
||||
};
|
||||
result.set(Frame_Statistic::pipeline_3d_gpu_render_ms,
|
||||
take_gpu(Frame_Trace_Measurement::gpu_render_ns));
|
||||
result.set(Frame_Statistic::pipeline_3d_gpu_transition_ms,
|
||||
take_gpu(Frame_Trace_Measurement::gpu_transition_ns));
|
||||
result.set(Frame_Statistic::pipeline_3d_gpu_copy_ms,
|
||||
take_gpu(Frame_Trace_Measurement::gpu_copy_ns));
|
||||
result.set(Frame_Statistic::pipeline_3d_gpu_sync_ms, take(gpu_window));
|
||||
result.set(Frame_Statistic::pipeline_3d_readback_ms, take(interval(
|
||||
Frame_Trace_Marker::readback_started, Frame_Trace_Marker::readback_finished)));
|
||||
result.set(Frame_Statistic::pipeline_3d_callback_ms, take(interval(
|
||||
Frame_Trace_Marker::callback_started, Frame_Trace_Marker::callback_finished)));
|
||||
result.set(Frame_Statistic::pipeline_3d_completion_handoff_ms, remaining);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace aethera {
|
||||
namespace detail {
|
||||
struct Taskflow_Frame_Access;
|
||||
}
|
||||
enum struct Frame_Dimension : std::uint8_t;
|
||||
struct Frame_Statistics_Sample;
|
||||
enum struct Frame_Trace_Marker : std::uint8_t {
|
||||
created,
|
||||
@@ -131,7 +130,7 @@ public:
|
||||
[[nodiscard]] std::uint64_t created_time_unix_ns() const noexcept;
|
||||
void mark(Frame_Trace_Marker marker) noexcept;
|
||||
void record(Frame_Trace_Measurement measurement, std::uint64_t value_ns) noexcept;
|
||||
[[nodiscard]] Frame_Statistics_Sample statistics(Frame_Dimension dimension) const;
|
||||
[[nodiscard]] Frame_Statistics_Sample statistics() const;
|
||||
void request_taskflow_trace() noexcept;
|
||||
[[nodiscard]] bool taskflow_trace_requested() const noexcept;
|
||||
[[nodiscard]] Taskflow_Frame_Trace take_taskflow_trace();
|
||||
|
||||
@@ -203,8 +203,8 @@ Frame_Statistics_Accumulator::Frame_Statistics_Accumulator(std::size_t capacity)
|
||||
}
|
||||
|
||||
const Frame_Statistics_State& Frame_Statistics_Accumulator::submit(
|
||||
const Render_Frame& frame, Frame_Dimension dimension) {
|
||||
auto sample = frame.statistics(dimension);
|
||||
const Render_Frame& frame) {
|
||||
auto sample = frame.statistics();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto frame_identity = frame.identity();
|
||||
if (previous_completion_ != std::chrono::steady_clock::time_point{}) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
namespace aethera {
|
||||
enum struct Frame_Dimension : std::uint8_t { two_dimensional, three_dimensional };
|
||||
enum struct Frame_Statistic : std::uint8_t {
|
||||
plot_tick_queue_ms,
|
||||
plot_update_ms,
|
||||
@@ -15,11 +14,8 @@ enum struct Frame_Statistic : std::uint8_t {
|
||||
server_completion_ms,
|
||||
scene_render_ms,
|
||||
event_dispatch_ms,
|
||||
prepare_ms,
|
||||
paint_ms,
|
||||
backend_queue_ms,
|
||||
gpu_submission_ms,
|
||||
readback_stage_ms,
|
||||
callback_ms,
|
||||
backend_apply_ms,
|
||||
backend_plan_ms,
|
||||
@@ -31,36 +27,6 @@ enum struct Frame_Statistic : std::uint8_t {
|
||||
gpu_copy_ms,
|
||||
gpu_total_ms,
|
||||
readback_ms,
|
||||
pipeline_2d_event_ms,
|
||||
pipeline_2d_prepare_ms,
|
||||
pipeline_2d_paint_ms,
|
||||
pipeline_2d_frame_target_ms,
|
||||
pipeline_2d_background_ms,
|
||||
pipeline_2d_cache_targets_ms,
|
||||
pipeline_2d_taskflow_ms,
|
||||
pipeline_2d_paint_coordination_ms,
|
||||
pipeline_2d_scene_coordination_ms,
|
||||
pipeline_2d_callback_ms,
|
||||
pipeline_2d_frame_handoff_ms,
|
||||
pipeline_3d_event_ms,
|
||||
pipeline_3d_prepare_ms,
|
||||
pipeline_3d_submit_graph_ms,
|
||||
pipeline_3d_scene_coordination_ms,
|
||||
pipeline_3d_prepare_queue_ms,
|
||||
pipeline_3d_backend_apply_ms,
|
||||
pipeline_3d_backend_plan_ms,
|
||||
pipeline_3d_backend_execute_ms,
|
||||
pipeline_3d_backend_commands_ms,
|
||||
pipeline_3d_backend_queue_ms,
|
||||
pipeline_3d_backend_submit_ms,
|
||||
pipeline_3d_submit_handoff_ms,
|
||||
pipeline_3d_gpu_render_ms,
|
||||
pipeline_3d_gpu_transition_ms,
|
||||
pipeline_3d_gpu_copy_ms,
|
||||
pipeline_3d_gpu_sync_ms,
|
||||
pipeline_3d_readback_ms,
|
||||
pipeline_3d_callback_ms,
|
||||
pipeline_3d_completion_handoff_ms,
|
||||
frame_interval_ms,
|
||||
count
|
||||
};
|
||||
@@ -155,8 +121,7 @@ struct Frame_Statistics_State {
|
||||
struct Frame_Statistics_Accumulator final {
|
||||
public:
|
||||
explicit Frame_Statistics_Accumulator(std::size_t capacity = 600);
|
||||
[[nodiscard]] const Frame_Statistics_State& submit(
|
||||
const Render_Frame& frame, Frame_Dimension dimension);
|
||||
[[nodiscard]] const Frame_Statistics_State& submit(const Render_Frame& frame);
|
||||
void reset() noexcept;
|
||||
private:
|
||||
std::vector<Sliding_Statistics> values_;
|
||||
|
||||
@@ -31,16 +31,8 @@ concept Renderable_Object = Attached<T> && std::derived_from<T, Renderable>;
|
||||
struct Renderable : Def<Renderable, Root> {
|
||||
/* Renderable 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */
|
||||
struct Prop : Prev_Prop {};
|
||||
/* Renderable 每次 Scene 执行后发布的业务图状态与统计。 */
|
||||
struct State : Prev_State {
|
||||
bool render_dirty{}; /* 条件判断时观察到的 Render_Graph_Tag dirty 状态。 */
|
||||
bool render_executed{}; /* 本次 Scene 执行是否运行了业务图。 */
|
||||
bool graph_rebuilt{}; /* 本次 advance 是否重建了业务图。 */
|
||||
std::size_t task_count{}; /* 当前业务图的任务数。 */
|
||||
std::uint64_t execution_time_ns{}; /* 本次业务图实际执行耗时,单位为纳秒。 */
|
||||
/* 支持测试、快照比较和变更检测的逐字段相等比较。 */
|
||||
bool operator==(const State&) const = default;
|
||||
};
|
||||
/* 任务拓扑和执行状态由逐帧 Taskflow trace 发布,不进入业务 State。 */
|
||||
struct State : Prev_State {};
|
||||
/* 完整声明、内部派发以及 Prepare/Paint CRTP 能力契约见 renderable.ipp 中的 Renderable::Private。 */
|
||||
struct Private;
|
||||
/* 返回该对象参与当前事件竞争时的几何距离;无值表示沿用普通绘制层级路由。 */
|
||||
|
||||
@@ -12,20 +12,20 @@ struct Renderable::Private : Prev_Private {
|
||||
using Build_Graph_Run = std::optional<Task_Graph> (*)(Root*);
|
||||
using Graph_Rebuild_Run = bool (*)(Root*);
|
||||
using Render_Predicate_Run = bool (*)(Root*, bool);
|
||||
using Pending_State_Run = void* (*)(Root*);
|
||||
using Publish_State_Run = void (*)(Root*);
|
||||
|
||||
std::string business_name_value{};
|
||||
std::string diagnostic_component_id{}; /* 按需诊断使用的组件语义 ID;不属于业务状态。 */
|
||||
Build_Graph_Run build_graph_run{};
|
||||
Graph_Rebuild_Run graph_rebuild_run{};
|
||||
Render_Predicate_Run render_predicate_run{};
|
||||
Pending_State_Run pending_state_run{};
|
||||
Publish_State_Run publish_state_run{};
|
||||
Event_Run event_run{};
|
||||
Event_Routing_Distance_Run event_routing_distance_run{};
|
||||
Color_Cache_Visit color_cache_visit{};
|
||||
std::optional<Task_Graph> graph{};
|
||||
Task_Graph extension{"renderable.extension"};
|
||||
bool render_graph_executed{}; /* 当前 Scene 帧的条件节点是否选择了本对象业务图。 */
|
||||
|
||||
[[nodiscard]] std::string_view business_name() const noexcept {
|
||||
return business_name_value;
|
||||
@@ -39,9 +39,6 @@ struct Renderable::Private : Prev_Private {
|
||||
[[nodiscard]] bool should_render(Root* object, bool dirty) const {
|
||||
return render_predicate_run ? render_predicate_run(object, dirty) : dirty;
|
||||
}
|
||||
[[nodiscard]] void* pending_renderable_state(Root* object) const {
|
||||
return pending_state_run(object);
|
||||
}
|
||||
void publish_renderable_state(Root* object) const {
|
||||
publish_state_run(object);
|
||||
}
|
||||
@@ -102,10 +99,6 @@ void Renderable::Private::bind_private_crtp(Object* object) {
|
||||
return private_data.should_render(value, state, dirty);
|
||||
return dirty;
|
||||
};
|
||||
data.pending_state_run = [](Root* root) -> void* {
|
||||
auto* value = static_cast<Object*>(root);
|
||||
return &double_buffer::detail::Internal_Access::pending_state(value);
|
||||
};
|
||||
data.publish_state_run = [](Root* root) {
|
||||
auto* value = static_cast<Object*>(root);
|
||||
double_buffer::detail::Internal_Access::publish_state<Renderable::Base_Tag>(value);
|
||||
@@ -125,9 +118,8 @@ void Renderable::Private::bind_private_crtp(Object* object) {
|
||||
if constexpr (std::derived_from<Cache, Color_Cache>) {
|
||||
data.color_cache_visit = [](Root* root, void* context, Color_Cache_Visitor visitor) {
|
||||
auto* value = static_cast<Object*>(root);
|
||||
const auto& render_state = static_cast<const State&>(
|
||||
double_buffer::detail::Internal_Access::current_state(value));
|
||||
if (render_state.render_executed)
|
||||
if (double_buffer::detail::Internal_Access::get(value)
|
||||
.render_graph_executed)
|
||||
visitor(context, double_buffer::detail::Internal_Access::pending_buffer<Color_Cache>(value));
|
||||
else
|
||||
visitor(context, double_buffer::detail::Internal_Access::current_buffer<Color_Cache>(value));
|
||||
|
||||
@@ -20,17 +20,9 @@ struct Scene : Def<Scene, Root,
|
||||
using Event_Report_Batch = std::pmr::vector<Event_Report_Pointer>;
|
||||
/* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */
|
||||
struct Prop : Prev_Prop {};
|
||||
/* Scene 每次 process(...) 后发布的总图结构与执行统计。 */
|
||||
/* Scene 只发布输入事件业务统计;任务图结构与执行数据来自逐帧 trace。 */
|
||||
struct State : Prev_State {
|
||||
Event_Statistics_State event_statistics{}; /* 输入进入 Scene 到 Renderable 消费完成的分类统计。 */
|
||||
bool taskflow_rebuilt{}; /* 本次 process(...) 前的 advance 是否重新构建了总 Taskflow。 */
|
||||
std::size_t renderable_count{}; /* Prepare 数据依赖图中的 Renderable 数量。 */
|
||||
std::size_t taskflow_task_count{}; /* 当前 Prepare Taskflow 中的任务节点数量。 */
|
||||
std::size_t taskflow_dependency_count{}; /* 当前 Prepare Taskflow 中的直接依赖边数量。 */
|
||||
std::size_t taskflow_max_predecessors{}; /* 当前 Prepare Taskflow 中单个任务的最大直接前驱数量。 */
|
||||
std::size_t taskflow_max_successors{}; /* 当前 Prepare Taskflow 中单个任务的最大直接后继数量。 */
|
||||
std::uint64_t taskflow_execution_time_ns{}; /* 本次 process(...) 执行 Prepare Taskflow 的耗时,单位为纳秒;没有任务时为 0。 */
|
||||
/* 支持测试、快照比较和变更检测的逐字段相等比较。 */
|
||||
bool operator==(const State&) const = default;
|
||||
};
|
||||
/* 完整声明、字段及可覆盖 CRTP hook 见 scene.ipp 中的 Scene::Private。 */
|
||||
|
||||
+24
-44
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
#include "Task_Graph_Internal.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -65,19 +64,19 @@ void Scene::Private::process(Object* object, Callback&& callback) requires std::
|
||||
}
|
||||
template <Attached Object, typename Callback>
|
||||
void Scene::Private::process(Object* object, Render_Frame* frame, Callback&& callback) requires std::invocable<Callback, const Result&> {
|
||||
auto& state = static_cast<State&>(
|
||||
double_buffer::detail::Internal_Access::pending_state(object));
|
||||
state.taskflow_execution_time_ns = 0;
|
||||
if (frame) frame->mark(Frame_Trace_Marker::prepare_started);
|
||||
if (runtime->taskflow && !runtime->taskflow->empty())
|
||||
state.taskflow_execution_time_ns = frame && frame->taskflow_trace_requested()
|
||||
? detail::run_taskflow(*runtime->taskflow, *frame, "scene.renderables")
|
||||
: detail::run_taskflow(*runtime->taskflow);
|
||||
if (frame && frame->taskflow_trace_requested())
|
||||
(void)detail::run_taskflow(*runtime->taskflow, *frame, "scene.renderables");
|
||||
else
|
||||
(void)detail::run_taskflow(*runtime->taskflow);
|
||||
if (frame) frame->mark(Frame_Trace_Marker::prepare_finished);
|
||||
double_buffer::detail::Internal_Access::current_dependency_graph<Render_Graph_Tag>(object).for_each_bound(
|
||||
[](Renderable* renderable, Renderable::Private& data) {
|
||||
data.publish_renderable_state(renderable);
|
||||
});
|
||||
auto& state = static_cast<State&>(
|
||||
double_buffer::detail::Internal_Access::pending_state(object));
|
||||
state.event_statistics = event_statistics.state();
|
||||
double_buffer::detail::Internal_Access::publish_state<Scene::Base_Tag>(object);
|
||||
Result result;
|
||||
@@ -90,8 +89,6 @@ void Scene::Private::after_advance(Object* object,
|
||||
const Prop* current_prop,
|
||||
State_Access<State> current_states) {
|
||||
auto* resource = detail::task_memory_resource();
|
||||
auto& scene_state = pending_states.get<Scene::Base_Tag>();
|
||||
scene_state.taskflow_rebuilt = false;
|
||||
std::pmr::unordered_set<Root*> advanced_objects{resource};
|
||||
advanced_objects.insert(object);
|
||||
double_buffer::detail::Internal_Access::for_each_current_dependency_graph(object,
|
||||
@@ -112,16 +109,17 @@ void Scene::Private::after_advance(Object* object,
|
||||
[&](auto& graph_state) { taskflow_dirty = taskflow_dirty || graph_state.dirty(); });
|
||||
render_dependencies.for_each_bound(
|
||||
[&](Renderable* renderable, Renderable::Private& data) {
|
||||
auto& state = *static_cast<Renderable::State*>(
|
||||
data.pending_renderable_state(renderable));
|
||||
state.graph_rebuilt = false;
|
||||
if (!data.graph || data.should_rebuild_runtime_graph(renderable)) {
|
||||
data.graph = data.build_runtime_graph(renderable);
|
||||
state.graph_rebuilt = true;
|
||||
if (data.graph) {
|
||||
data.graph->describe("owner_kind", "renderable");
|
||||
if (!data.diagnostic_component_id.empty())
|
||||
data.graph->describe("owner_component",
|
||||
data.diagnostic_component_id);
|
||||
}
|
||||
double_buffer::detail::Internal_Access::mark_dirty<Render_Graph_Tag>(
|
||||
renderable);
|
||||
}
|
||||
state.task_count = data.graph ? data.graph->size() : 0;
|
||||
});
|
||||
if (!taskflow_dirty) return;
|
||||
if (!runtime->taskflow) runtime->taskflow = std::make_unique<Task_Graph>("scene.render");
|
||||
@@ -132,7 +130,6 @@ void Scene::Private::after_advance(Object* object,
|
||||
renderables.insert(renderable);
|
||||
}
|
||||
);
|
||||
scene_state.renderable_count = renderables.size();
|
||||
taskflow.clear();
|
||||
struct Render_Tasks {
|
||||
Task_Node entry;
|
||||
@@ -149,32 +146,26 @@ void Scene::Private::after_advance(Object* object,
|
||||
if (!data) continue;
|
||||
const auto task_prefix = std::string(data->business_name()) + ".render";
|
||||
auto render_if = taskflow.add_condition(task_prefix + ".condition", [data, root] {
|
||||
auto& state = *static_cast<Renderable::State*>(data->pending_renderable_state(root));
|
||||
state.execution_time_ns = 0;
|
||||
const bool dirty =
|
||||
double_buffer::detail::Internal_Access::dirty<Render_Graph_Tag>(root);
|
||||
state.render_dirty = dirty;
|
||||
state.render_executed = data->should_render(root, dirty);
|
||||
if (state.render_executed) {
|
||||
state.execution_time_ns = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
return state.render_executed ? 0 : 1;
|
||||
data->render_graph_executed = data->should_render(root, dirty);
|
||||
return data->render_graph_executed ? 0 : 1;
|
||||
});
|
||||
Task_Node render_run = data->graph
|
||||
? taskflow.compose(task_prefix + ".graph", *data->graph)
|
||||
: taskflow.add(task_prefix + ".empty", [] {});
|
||||
auto render_done = taskflow.add(task_prefix + ".complete", [data, root] {
|
||||
auto& state = *static_cast<Renderable::State*>(data->pending_renderable_state(root));
|
||||
if (state.render_executed) {
|
||||
if (data->render_graph_executed)
|
||||
(void)double_buffer::detail::Internal_Access::take_dirty<Render_Graph_Tag>(root);
|
||||
const auto finished = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
state.execution_time_ns = finished - state.execution_time_ns;
|
||||
}
|
||||
});
|
||||
const auto identify_renderable_task = [data](Task_Node& node) {
|
||||
node.describe("owner_kind", "renderable");
|
||||
if (!data->diagnostic_component_id.empty())
|
||||
node.describe("owner_component", data->diagnostic_component_id);
|
||||
};
|
||||
identify_renderable_task(render_if);
|
||||
identify_renderable_task(render_run);
|
||||
identify_renderable_task(render_done);
|
||||
render_if.precede(render_run);
|
||||
render_if.precede(render_done);
|
||||
if (data->extension.empty()) {
|
||||
@@ -183,6 +174,7 @@ void Scene::Private::after_advance(Object* object,
|
||||
else {
|
||||
auto extension = taskflow.compose(
|
||||
task_prefix + ".extension", data->extension);
|
||||
identify_renderable_task(extension);
|
||||
render_run.precede(extension);
|
||||
extension.precede(render_done);
|
||||
}
|
||||
@@ -214,19 +206,7 @@ void Scene::Private::after_advance(Object* object,
|
||||
);
|
||||
};
|
||||
connect_dependencies(render_dependencies);
|
||||
scene_state.taskflow_task_count = taskflow.size();
|
||||
scene_state.taskflow_dependency_count = 0;
|
||||
scene_state.taskflow_max_predecessors = 0;
|
||||
scene_state.taskflow_max_successors = 0;
|
||||
for (const auto& node : detail::Task_Graph_Access::nodes(taskflow)) {
|
||||
scene_state.taskflow_dependency_count += node.successors.size();
|
||||
scene_state.taskflow_max_predecessors = std::max(
|
||||
scene_state.taskflow_max_predecessors, node.predecessors.size());
|
||||
scene_state.taskflow_max_successors = std::max(
|
||||
scene_state.taskflow_max_successors, node.successors.size());
|
||||
}
|
||||
double_buffer::detail::Internal_Access::access_pending_dependency_graph<Render_Graph_Tag>(object,
|
||||
[](auto& graph_state) { if (graph_state.dirty()) graph_state.take_dirty(); });
|
||||
scene_state.taskflow_rebuilt = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +113,12 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie
|
||||
int scene_updates = 0;
|
||||
int runtime_updates = 0;
|
||||
renderable->set_state_callback<aethera::Renderable::Base_Tag>([&](const auto& state) {
|
||||
(void)state;
|
||||
++renderable_updates;
|
||||
EXPECT_TRUE(state.render_executed);
|
||||
});
|
||||
scene->set_state_callback<aethera::Scene::Base_Tag>([&](const auto& state) {
|
||||
(void)state;
|
||||
++scene_updates;
|
||||
EXPECT_GT(state.taskflow_task_count, 0u);
|
||||
});
|
||||
aethera::set_runtime_state_callback<aethera::Task_Runtime_State_Tag>([&](const auto& state) {
|
||||
++runtime_updates;
|
||||
@@ -131,34 +131,6 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie
|
||||
EXPECT_EQ(runtime_updates, 1);
|
||||
aethera::clear_runtime_state_callback<aethera::Task_Runtime_State_Tag>();
|
||||
}
|
||||
TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) {
|
||||
aethera::initialize_runtime({.workers = 2});
|
||||
auto renderable = build_object<Direct>();
|
||||
auto scene = build_object<Scene>();
|
||||
add_renderable(*scene, renderable.get());
|
||||
std::size_t task_count{};
|
||||
std::size_t dependency_count{};
|
||||
int updates{};
|
||||
scene->set_state_callback<aethera::Scene::Base_Tag>([&](const auto& state) {
|
||||
EXPECT_EQ(state.renderable_count, 1u);
|
||||
EXPECT_GT(state.taskflow_task_count, 0u);
|
||||
if (updates == 0) {
|
||||
EXPECT_TRUE(state.taskflow_rebuilt);
|
||||
task_count = state.taskflow_task_count;
|
||||
dependency_count = state.taskflow_dependency_count;
|
||||
}
|
||||
else {
|
||||
EXPECT_FALSE(state.taskflow_rebuilt);
|
||||
EXPECT_EQ(state.taskflow_task_count, task_count);
|
||||
EXPECT_EQ(state.taskflow_dependency_count, dependency_count);
|
||||
}
|
||||
++updates;
|
||||
});
|
||||
scene->process([](const auto&) {});
|
||||
scene->process([](const auto&) {});
|
||||
EXPECT_EQ(updates, 2);
|
||||
}
|
||||
|
||||
TEST(scene_condition, render_order_does_not_create_a_false_data_dependency) {
|
||||
aethera::initialize_runtime({.workers = 2});
|
||||
auto source = build_object<Direct_Renderable>();
|
||||
@@ -189,6 +161,8 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity)
|
||||
aethera::initialize_runtime({.workers = 2});
|
||||
std::atomic_int completed{};
|
||||
aethera::Task_Graph child{"test.visual"};
|
||||
child.describe("owner_kind", "renderable")
|
||||
.describe("owner_component", "spectrum");
|
||||
auto prepare = child.add("prepare.samples", [&] { completed.fetch_add(1); });
|
||||
auto paint = child.add("paint.visual", [&] { completed.fetch_add(1); });
|
||||
prepare.precede(paint);
|
||||
@@ -233,6 +207,12 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity)
|
||||
ASSERT_NE(child_prepare, trace.graphs.front().nodes.end());
|
||||
EXPECT_EQ(child_prepare->parent_node_id, module->node_id);
|
||||
EXPECT_EQ(child_prepare->node_id, "test.frame/spectrum/prepare.samples");
|
||||
EXPECT_TRUE(std::ranges::contains(
|
||||
child_prepare->attributes,
|
||||
std::pair<std::string, std::string>{"owner_kind", "renderable"}));
|
||||
EXPECT_TRUE(std::ranges::contains(
|
||||
child_prepare->attributes,
|
||||
std::pair<std::string, std::string>{"owner_component", "spectrum"}));
|
||||
|
||||
std::unordered_set<std::uint64_t> metadata_ids;
|
||||
for (const auto& node : trace.graphs.front().nodes)
|
||||
|
||||
@@ -9,6 +9,7 @@ struct Afterglow::Private : Prev_Private {
|
||||
detail::Raster_Layout layout{}; /* 两根轴决定的色块矩阵布局。 */
|
||||
std::vector<Plot_Ratio> intensity{}; /* 历史频谱衰减累加后的未归一化强度。 */
|
||||
std::vector<Pixel> pixels{}; /* 归一化强度经色图转换后的像素矩阵。 */
|
||||
std::vector<Plot_Ratio> partition_maxima{}; /* 各独占分块在累加后产生的归约输入。 */
|
||||
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
|
||||
Plot_Ratio maximum{1.0}; /* 本帧强度归一化分母,最小为 1。 */
|
||||
bool valid{}; /* 轴布局和输入数据是否足以生成色块。 */
|
||||
@@ -84,7 +85,9 @@ inline Task_Graph Afterglow::Private::build_graph(Afterglow* object, const Prop&
|
||||
auto normalize = graph.add("prepare.normalize", [this] {
|
||||
normalize_frame();
|
||||
});
|
||||
auto colored = graph.add("prepare.color.complete", [] {});
|
||||
if (partition_count == 0) begin.precede(normalize);
|
||||
normalize.precede(colored);
|
||||
for (Plot_Partition_Count index = 0; index < partition_count; ++index) {
|
||||
auto accumulate = graph.add("prepare.accumulate", [this, object, index] {
|
||||
accumulate_partition(object, index);
|
||||
@@ -98,7 +101,8 @@ inline Task_Graph Afterglow::Private::build_graph(Afterglow* object, const Prop&
|
||||
begin.precede(accumulate);
|
||||
accumulate.precede(normalize);
|
||||
normalize.precede(color);
|
||||
color.precede(paint);
|
||||
color.precede(colored);
|
||||
colored.precede(paint);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
@@ -135,6 +139,8 @@ inline void Afterglow::Private::prepare_frame(Afterglow* object) {
|
||||
prepared.intensity.assign(cells, 0.0);
|
||||
prepared.pixels.assign(cells, 0);
|
||||
}
|
||||
prepared.partition_maxima.assign(
|
||||
detail::raster_partition_count(graph_partition_grid), 1.0);
|
||||
prepared.valid = true;
|
||||
}
|
||||
inline void Afterglow::Private::accumulate_partition(Afterglow* object, Plot_Partition_Count index) {
|
||||
@@ -181,9 +187,21 @@ inline void Afterglow::Private::accumulate_partition(Afterglow* object, Plot_Par
|
||||
}
|
||||
}
|
||||
}
|
||||
Plot_Ratio maximum{1.0};
|
||||
for (int y = partition.first_y; y < partition.last_y; ++y)
|
||||
for (int x = partition.first_x; x < partition.last_x; ++x) {
|
||||
const auto [column, row] =
|
||||
detail::raster_coordinates(prepared.layout, x, y);
|
||||
maximum = std::max(maximum, prepared.intensity[
|
||||
static_cast<std::size_t>(row) * columns + column]);
|
||||
}
|
||||
prepared.partition_maxima[index] = maximum;
|
||||
}
|
||||
inline void Afterglow::Private::normalize_frame() {
|
||||
if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max<Plot_Ratio>(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end()));
|
||||
prepared.maximum = prepared.valid && !prepared.partition_maxima.empty()
|
||||
? *std::max_element(prepared.partition_maxima.begin(),
|
||||
prepared.partition_maxima.end())
|
||||
: 1.0;
|
||||
}
|
||||
inline void Afterglow::Private::color_partition(Afterglow* object, Plot_Partition_Count index) {
|
||||
if (!prepared.valid) return;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include "common/Curve_Plot.hpp"
|
||||
namespace aethera::render_2d {
|
||||
struct Spectrum::Private : Prev_Private {
|
||||
@@ -25,7 +27,7 @@ struct Spectrum::Private : Prev_Private {
|
||||
struct Prepared {
|
||||
std::vector<Prepared_Partition> partitions{}; /* Prepare 子图各分块的独立输出。 */
|
||||
std::vector<Prepared_Marker> markers{}; /* 中心频率及自定义频率标记。 */
|
||||
std::vector<Prepared_Extreme> extremes{}; /* 当前帧启用的最大值和最小值标记。 */
|
||||
std::array<std::optional<Prepared_Extreme>, 2> extremes{}; /* 最大/最小值由两个独立 Prepare 节点写入。 */
|
||||
Rect_F sweep_region{}; /* 扫频背景在画布中的矩形。 */
|
||||
Size canvas_size{}; /* 所属 Scene viewport 决定的颜色层尺寸。 */
|
||||
bool valid{}; /* 两根轴与画布是否足以生成绘制数据。 */
|
||||
@@ -36,6 +38,7 @@ struct Spectrum::Private : Prev_Private {
|
||||
Prepared prepared{}; /* 当前权威 State、Frame 和轴状态推导出的 Paint 输入。 */
|
||||
std::vector<Spectrum_Power> maxima{}; /* 当前样本历史逐点最大值;只由 Prepare 更新。 */
|
||||
std::vector<Spectrum_Power> minima{}; /* 当前样本历史逐点最小值;只由 Prepare 更新。 */
|
||||
bool hold_history_reset{}; /* prepare.frame 是否已用当前帧初始化整段保持值。 */
|
||||
std::size_t graph_partition_count{};
|
||||
/* Builder 内部绑定 Scene 与两根轴;三个来源都必须比 Spectrum 生命周期更长。 */
|
||||
void bind_render_sources(Frequency_Object* frequency_axis_value, Power_Object* power_axis_value);
|
||||
@@ -46,6 +49,8 @@ struct Spectrum::Private : Prev_Private {
|
||||
[[nodiscard]] Task_Graph build_graph(Spectrum* object, const Prop& state);
|
||||
[[nodiscard]] bool should_rebuild_graph(Spectrum* object, const Prop& state);
|
||||
void prepare_frame(Spectrum* object, std::size_t partition_count);
|
||||
void update_hold_partition(Spectrum* object, std::size_t partition_index);
|
||||
void prepare_extreme(Spectrum* object, bool maximum);
|
||||
void prepare_partition(Spectrum* object, std::size_t partition_index);
|
||||
void begin_paint(Spectrum* object);
|
||||
void paint_partition(Spectrum* object, std::size_t partition_index);
|
||||
@@ -99,15 +104,35 @@ inline Task_Graph Spectrum::Private::build_graph(Spectrum* object, const Prop& s
|
||||
auto prepare_begin = graph.add("prepare.frame", [this, object, partition_count] {
|
||||
prepare_frame(object, partition_count);
|
||||
});
|
||||
auto hold_complete = graph.add("prepare.hold.complete", [] {});
|
||||
std::vector<Task_Node> hold_partitions;
|
||||
hold_partitions.reserve(partition_count);
|
||||
for (std::size_t index = 0; index < partition_count; ++index) {
|
||||
auto partition = graph.add("prepare.hold.partition", [this, object, index] {
|
||||
update_hold_partition(object, index);
|
||||
});
|
||||
prepare_begin.precede(partition);
|
||||
partition.precede(hold_complete);
|
||||
hold_partitions.push_back(partition);
|
||||
}
|
||||
if (hold_partitions.empty()) prepare_begin.precede(hold_complete);
|
||||
std::vector<Task_Node> prepared_partitions;
|
||||
prepared_partitions.reserve(partition_count);
|
||||
for (std::size_t index = 0; index < partition_count; ++index) {
|
||||
auto partition = graph.add("prepare.partition", [this, object, index] {
|
||||
prepare_partition(object, index);
|
||||
});
|
||||
prepare_begin.precede(partition);
|
||||
hold_complete.precede(partition);
|
||||
prepared_partitions.push_back(partition);
|
||||
}
|
||||
auto maximum = graph.add("prepare.extreme.maximum", [this, object] {
|
||||
prepare_extreme(object, true);
|
||||
});
|
||||
auto minimum = graph.add("prepare.extreme.minimum", [this, object] {
|
||||
prepare_extreme(object, false);
|
||||
});
|
||||
prepare_begin.precede(maximum);
|
||||
prepare_begin.precede(minimum);
|
||||
auto paint_begin = graph.add("paint.begin", [this, object] {
|
||||
begin_paint(object);
|
||||
});
|
||||
@@ -123,6 +148,8 @@ inline Task_Graph Spectrum::Private::build_graph(Spectrum* object, const Prop& s
|
||||
paint_begin.precede(partition);
|
||||
partition.precede(overlays);
|
||||
}
|
||||
maximum.precede(overlays);
|
||||
minimum.precede(overlays);
|
||||
if (partition_count == 0) paint_begin.precede(overlays);
|
||||
return graph;
|
||||
}
|
||||
@@ -130,16 +157,11 @@ inline void Spectrum::Private::prepare_frame(Spectrum* object, std::size_t parti
|
||||
auto& private_data = double_buffer::detail::Internal_Access::get(object);
|
||||
const auto& state = static_cast<const Prop&>(double_buffer::detail::Internal_Access::current_prop(object));
|
||||
const auto& frame = double_buffer::detail::Internal_Access::current_buffer<Spectrum_Frame_Tag>(object);
|
||||
if (maxima.size() != frame.samples.size()) {
|
||||
hold_history_reset = maxima.size() != frame.samples.size();
|
||||
if (hold_history_reset) {
|
||||
maxima = frame.samples;
|
||||
minima = frame.samples;
|
||||
}
|
||||
else {
|
||||
for (std::size_t index = 0; index < frame.samples.size(); ++index) {
|
||||
maxima[index] = std::max(maxima[index], frame.samples[index]);
|
||||
minima[index] = std::min(minima[index], frame.samples[index]);
|
||||
}
|
||||
}
|
||||
const auto& scene_state = this->template read_current_prop<Render_Scene_2D::Base_Tag>(scene);
|
||||
const auto& frequency_layout = this->template read_current_prop<Abs_Axis::Base_Tag>(frequency_axis);
|
||||
const auto& power_layout = this->template read_current_prop<Abs_Axis::Base_Tag>(power_axis);
|
||||
@@ -156,19 +178,55 @@ inline void Spectrum::Private::prepare_frame(Spectrum* object, std::size_t parti
|
||||
const Marker_Style style = static_cast<Spectrum_Marker_Index>(index) == state.selected_marker ? Marker_Style::selected : Marker_Style::normal;
|
||||
prepared.markers.push_back({detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), style});
|
||||
}
|
||||
if (!frame.samples.empty() && (state.max_marker_visible || state.min_marker_visible)) {
|
||||
const Spectrum_Interpolation_Ratio denominator = frame.samples.size() > 1 ? static_cast<Spectrum_Interpolation_Ratio>(frame.samples.size() - 1) : 1.0;
|
||||
const auto prepare_extreme = [&](bool maximum) {
|
||||
const auto iterator = maximum ? std::max_element(frame.samples.begin(), frame.samples.end()) : std::min_element(frame.samples.begin(), frame.samples.end());
|
||||
const auto index = static_cast<std::size_t>(std::distance(frame.samples.begin(), iterator));
|
||||
const Spectrum_Frequency frequency = state.frequency_range.origin + state.frequency_range.length() * static_cast<Spectrum_Interpolation_Ratio>(index) / denominator;
|
||||
prepared.extremes.push_back({detail::map_plot_point(frequency_axis, frequency, power_axis, *iterator, frequency_layout.orientation), maximum});
|
||||
};
|
||||
if (state.max_marker_visible) prepare_extreme(true);
|
||||
if (state.min_marker_visible) prepare_extreme(false);
|
||||
}
|
||||
prepared.valid = true;
|
||||
}
|
||||
inline void Spectrum::Private::update_hold_partition(
|
||||
Spectrum* object, std::size_t partition_index) {
|
||||
if (hold_history_reset || graph_partition_count == 0) return;
|
||||
const auto& frame = double_buffer::detail::Internal_Access::
|
||||
current_buffer<Spectrum_Frame_Tag>(object);
|
||||
const std::size_t first = frame.samples.size() * partition_index /
|
||||
graph_partition_count;
|
||||
const std::size_t last = frame.samples.size() * (partition_index + 1) /
|
||||
graph_partition_count;
|
||||
for (std::size_t index = first; index < last; ++index) {
|
||||
maxima[index] = std::max(maxima[index], frame.samples[index]);
|
||||
minima[index] = std::min(minima[index], frame.samples[index]);
|
||||
}
|
||||
}
|
||||
inline void Spectrum::Private::prepare_extreme(Spectrum* object, bool maximum) {
|
||||
const auto& state = static_cast<const Prop&>(
|
||||
double_buffer::detail::Internal_Access::current_prop(object));
|
||||
const std::size_t slot = maximum ? 0 : 1;
|
||||
if (!prepared.valid || (maximum ? !state.max_marker_visible
|
||||
: !state.min_marker_visible)) {
|
||||
prepared.extremes[slot].reset();
|
||||
return;
|
||||
}
|
||||
const auto& frame = double_buffer::detail::Internal_Access::
|
||||
current_buffer<Spectrum_Frame_Tag>(object);
|
||||
if (frame.samples.empty()) {
|
||||
prepared.extremes[slot].reset();
|
||||
return;
|
||||
}
|
||||
const auto iterator = maximum
|
||||
? std::max_element(frame.samples.begin(), frame.samples.end())
|
||||
: std::min_element(frame.samples.begin(), frame.samples.end());
|
||||
const auto index = static_cast<std::size_t>(
|
||||
std::distance(frame.samples.begin(), iterator));
|
||||
const Spectrum_Interpolation_Ratio denominator = frame.samples.size() > 1
|
||||
? static_cast<Spectrum_Interpolation_Ratio>(frame.samples.size() - 1)
|
||||
: 1.0;
|
||||
const Spectrum_Frequency frequency = state.frequency_range.origin +
|
||||
state.frequency_range.length() *
|
||||
static_cast<Spectrum_Interpolation_Ratio>(index) / denominator;
|
||||
const auto& frequency_layout = this->template read_current_prop<
|
||||
Abs_Axis::Base_Tag>(frequency_axis);
|
||||
prepared.extremes[slot] = Prepared_Extreme{
|
||||
detail::map_plot_point(frequency_axis, frequency, power_axis, *iterator,
|
||||
frequency_layout.orientation),
|
||||
maximum};
|
||||
}
|
||||
inline void Spectrum::Private::prepare_partition(Spectrum* object, std::size_t partition_index) {
|
||||
if (!prepared.valid || partition_index >= prepared.partitions.size()) return;
|
||||
auto& private_data = double_buffer::detail::Internal_Access::get(object);
|
||||
@@ -233,8 +291,9 @@ inline void Spectrum::Private::paint_overlays(Spectrum* object) {
|
||||
if (pen.enabled()) painter.line(marker.first, marker.second, pen);
|
||||
}
|
||||
for (const auto& extreme : prepared.extremes) {
|
||||
const Pen& pen = extreme.maximum ? state.max_pen : state.min_pen;
|
||||
painter.circle(extreme.point, 3.0, pen, Brush{pen.color, Brush_Style::solid});
|
||||
if (!extreme) continue;
|
||||
const Pen& pen = extreme->maximum ? state.max_pen : state.min_pen;
|
||||
painter.circle(extreme->point, 3.0, pen, Brush{pen.color, Brush_Style::solid});
|
||||
}
|
||||
}
|
||||
template <typename Object, typename Owner, typename Member, typename Prop_Type>
|
||||
|
||||
@@ -9,9 +9,14 @@
|
||||
namespace aethera::render_2d {
|
||||
struct Waterfall::Private : Prev_Private {
|
||||
struct Prepared {
|
||||
struct Tile {
|
||||
detail::Raster_Partition source{}; /* 含插值 halo 的独占不可变源区域。 */
|
||||
Rect_F target{}; /* source 在完整热图目标矩形中的精确投影。 */
|
||||
std::vector<Pixel> pixels{};
|
||||
};
|
||||
std::vector<std::shared_ptr<const Waterfall_Row>> rows_by_slot{}; /* 时间槽到本帧源行的唯一映射;空槽保持空指针。 */
|
||||
detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */
|
||||
std::vector<Pixel> pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */
|
||||
std::vector<Tile> tiles{}; /* 每个任务只写、随后只读自己的插值源块。 */
|
||||
Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */
|
||||
std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */
|
||||
Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */
|
||||
@@ -143,7 +148,7 @@ inline void Waterfall::Private::prepare_frame(Waterfall* object) {
|
||||
return;
|
||||
}
|
||||
prepared.source_first = selection->first;
|
||||
prepared.pixels.assign(static_cast<std::size_t>(prepared.layout.width) * prepared.layout.height, 0);
|
||||
prepared.tiles.resize(detail::raster_partition_count(graph_partition_grid));
|
||||
const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0;
|
||||
const Axis_Coordinate first_center = time_range.origin + direction * 0.5;
|
||||
prepared.rows_by_slot.resize(static_cast<std::size_t>(time_slots));
|
||||
@@ -174,10 +179,33 @@ inline void Waterfall::Private::prepare_frame(Waterfall* object) {
|
||||
inline void Waterfall::Private::prepare_partition(Waterfall* object, Plot_Partition_Count index) {
|
||||
if (!prepared.valid) return;
|
||||
const auto& state = this->template read_current_prop<Waterfall::Base_Tag>(object);
|
||||
const detail::Raster_Partition partition = detail::raster_partition(
|
||||
const detail::Raster_Partition core = detail::raster_partition(
|
||||
prepared.layout, index, graph_partition_grid);
|
||||
for (int y = partition.first_y; y < partition.last_y; ++y) {
|
||||
for (int x = partition.first_x; x < partition.last_x; ++x) {
|
||||
if (core.empty() || index >= prepared.tiles.size()) return;
|
||||
const int halo = state.interpolation_mode == Image_Interpolation_Mode::nearest
|
||||
? 0 : state.interpolation_mode == Image_Interpolation_Mode::bilinear ? 1 : 2;
|
||||
auto& tile = prepared.tiles[index];
|
||||
tile.source = {
|
||||
std::max(0, core.first_x - halo),
|
||||
std::min(prepared.layout.width, core.last_x + halo),
|
||||
std::max(0, core.first_y - halo),
|
||||
std::min(prepared.layout.height, core.last_y + halo)};
|
||||
const int tile_width = tile.source.last_x - tile.source.first_x;
|
||||
const int tile_height = tile.source.last_y - tile.source.first_y;
|
||||
tile.pixels.assign(static_cast<std::size_t>(tile_width) * tile_height, 0);
|
||||
const Rect_F target = prepared.layout.target.normalized();
|
||||
const auto project_x = [&](int boundary) {
|
||||
return target.x + target.width * boundary / prepared.layout.width;
|
||||
};
|
||||
const auto project_y = [&](int boundary) {
|
||||
return target.y + target.height * boundary / prepared.layout.height;
|
||||
};
|
||||
const double left = project_x(tile.source.first_x);
|
||||
const double top = project_y(tile.source.first_y);
|
||||
tile.target = {left, top, project_x(tile.source.last_x) - left,
|
||||
project_y(tile.source.last_y) - top};
|
||||
for (int y = tile.source.first_y; y < tile.source.last_y; ++y) {
|
||||
for (int x = tile.source.first_x; x < tile.source.last_x; ++x) {
|
||||
const auto [column, slot] =
|
||||
detail::raster_coordinates(prepared.layout, x, y);
|
||||
if (slot < 0 || static_cast<std::size_t>(slot) >=
|
||||
@@ -188,8 +216,8 @@ inline void Waterfall::Private::prepare_partition(Waterfall* object, Plot_Partit
|
||||
if (!row) continue;
|
||||
const std::size_t source = static_cast<std::size_t>(
|
||||
prepared.source_first + column);
|
||||
prepared.pixels[static_cast<std::size_t>(y) *
|
||||
prepared.layout.width + x] =
|
||||
tile.pixels[static_cast<std::size_t>(y - tile.source.first_y) *
|
||||
tile_width + (x - tile.source.first_x)] =
|
||||
premultiply(state.color_map.sample(
|
||||
detail::normalized_plot_value(row->values[source],
|
||||
state.power_range)));
|
||||
@@ -200,13 +228,19 @@ inline void Waterfall::Private::paint_partition(Waterfall* object,
|
||||
Plot_Partition_Count index) {
|
||||
const auto& state = this->template read_current_prop<Waterfall::Base_Tag>(object);
|
||||
if (!prepared.valid || index >=
|
||||
detail::raster_partition_count(graph_partition_grid))
|
||||
detail::raster_partition_count(graph_partition_grid) ||
|
||||
index >= prepared.tiles.size())
|
||||
return;
|
||||
const Rect_F region = detail::raster_paint_region(
|
||||
prepared.layout, index, graph_partition_grid);
|
||||
if (region.empty()) return;
|
||||
const auto& tile = prepared.tiles[index];
|
||||
const int tile_width = tile.source.last_x - tile.source.first_x;
|
||||
const int tile_height = tile.source.last_y - tile.source.first_y;
|
||||
if (tile_width <= 0 || tile_height <= 0 || tile.pixels.empty()) return;
|
||||
detail::Painter painter(this->paint_surface(), prepared.canvas, region);
|
||||
detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode);
|
||||
painter.heatmap(tile.target, tile_width, tile_height, tile.pixels,
|
||||
state.interpolation_mode);
|
||||
}
|
||||
inline void Waterfall::Private::paint_tooltip(Waterfall* object) {
|
||||
if (!prepared.valid || prepared.tooltip_text.empty()) return;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "Render_Scene_2D.hpp" /* 二维 Paint Taskflow、批次事件路由、缓存失效与合成实现。 */
|
||||
namespace aethera::render_2d {
|
||||
Render_Scene_2D::Private::~Private() = default;
|
||||
bool Render_Scene_2D::State::operator==(const State&) const = default;
|
||||
bool Render_Scene_2D::Prop::operator==(const Prop&) const = default;
|
||||
|
||||
Render_Scene_2D::Render_Result_Type Render_Scene_2D::render(Frame_2D* frame) {
|
||||
@@ -19,8 +18,8 @@ void Render_Scene_2D::activate_view() {
|
||||
void Render_Scene_2D::deactivate_view() {
|
||||
double_buffer::detail::Internal_Access::get(this).set_view_active(this, false);
|
||||
}
|
||||
void Render_Scene_2D::reset_frame_statistics() {
|
||||
double_buffer::detail::Internal_Access::get(this).reset_frame_statistics(this);
|
||||
void Render_Scene_2D::reset_diagnostics() {
|
||||
double_buffer::detail::Internal_Access::get(this).reset_diagnostics(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "../base/Renderable_2D.hpp"
|
||||
#include "../render/Blend2D_Cache.hpp"
|
||||
#include <scene.hpp>
|
||||
#include <frame_statistics.hpp>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -17,10 +16,7 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
|
||||
bool view_active{}; /* 视图是否接受 render(frame*) 产生新的完成帧。 */
|
||||
bool operator==(const Prop&) const;
|
||||
};
|
||||
struct State : Prev_State {
|
||||
Frame_Statistics_State frame_statistics{};
|
||||
bool operator==(const State&) const;
|
||||
};
|
||||
struct State : Prev_State {};
|
||||
/* 完整声明、合成顺序和 CRTP 分派见 Render_Scene_2D.ipp。 */
|
||||
struct Private;
|
||||
template <typename Object>
|
||||
@@ -57,7 +53,7 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
|
||||
*/
|
||||
void activate_view();
|
||||
void deactivate_view();
|
||||
void reset_frame_statistics();
|
||||
void reset_diagnostics();
|
||||
/* 激活后 render 才会执行。 */
|
||||
/* 停止后续 render 调用,不清除最后一帧。 */
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include "../event/Event_Routing_Rules.hpp"
|
||||
#include <stdexcept>
|
||||
#include <mutex>
|
||||
@@ -65,9 +64,7 @@ struct Render_Scene_2D::Private : Prev_Private {
|
||||
bool frame_in_flight{}; /* Scene::advance 到像素发布完成的唯一不可重入准入状态。 */
|
||||
Frame_Callback frame_callback{}; /* Plot publish 后的外接消费出口。 */
|
||||
Frame_Callback frame_retired_callback{}; /* 全链路 trace 完成后的物理帧归还出口。 */
|
||||
Frame_Statistics_Accumulator frame_statistics{}; /* Scene 内部增量计算;State 只发布定长统计结果。 */
|
||||
std::unique_ptr<Task_Graph> frame_taskflow{};
|
||||
std::chrono::steady_clock::time_point active_render_started{};
|
||||
~Private();
|
||||
Frame_2D* active_frame{}; /* 异步帧 DAG 借用的外部帧;完成回调前保持存活。 */
|
||||
Blend2D_Cache* frame_target{}; /* 当前异步帧的最终颜色层;帧 DAG 完成后清空。 */
|
||||
@@ -90,7 +87,7 @@ struct Render_Scene_2D::Private : Prev_Private {
|
||||
template <Attached Object> void set_frame_callback(Object* object, Frame_Callback callback);
|
||||
template <Attached Object> void set_frame_retired_callback(Object* object, Frame_Callback callback);
|
||||
template <Attached Object> void set_view_active(Object* object, bool active);
|
||||
template <Attached Object> void reset_frame_statistics(Object* object);
|
||||
template <Attached Object> void reset_diagnostics(Object* object);
|
||||
/* CRTP 覆盖:Builder 挂接最终 Private 后安装二维 Scene 的无虚函数业务分派。 */
|
||||
template <Attached Object>
|
||||
void bind_private_crtp(Object* object);
|
||||
@@ -208,6 +205,8 @@ void Render_Scene_2D::Private::ensure_frame_taskflow(Object* object) {
|
||||
runtime->taskflow = std::make_unique<Task_Graph>("scene.render");
|
||||
frame_taskflow = std::make_unique<Task_Graph>("render_2d.frame");
|
||||
auto& graph = *frame_taskflow;
|
||||
graph.describe("owner_kind", "scene")
|
||||
.describe("owner_component", "scene");
|
||||
auto begin = graph.add("scene.begin", [this, object] {
|
||||
auto* frame = active_frame;
|
||||
if (!frame) throw std::logic_error("2D frame DAG lost its active frame");
|
||||
@@ -229,8 +228,6 @@ void Render_Scene_2D::Private::ensure_frame_taskflow(Object* object) {
|
||||
});
|
||||
auto& state = static_cast<State&>(
|
||||
double_buffer::detail::Internal_Access::pending_state(object));
|
||||
state.taskflow_execution_time_ns = 0;
|
||||
active_render_started = std::chrono::steady_clock::now();
|
||||
auto& target = detail::Frame_2D_Access::render_target(frame);
|
||||
frame_target = ⌖
|
||||
frame->mark(Frame_Trace_Marker::prepare_started);
|
||||
@@ -310,9 +307,6 @@ void Render_Scene_2D::Private::ensure_frame_taskflow(Object* object) {
|
||||
auto finish = graph.add("scene.render.complete", [this, object] {
|
||||
auto& state = static_cast<State&>(
|
||||
double_buffer::detail::Internal_Access::pending_state(object));
|
||||
state.taskflow_execution_time_ns = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - active_render_started).count());
|
||||
state.event_statistics = event_statistics.state();
|
||||
active_frame->mark(Frame_Trace_Marker::paint_taskflow_finished);
|
||||
active_frame->mark(Frame_Trace_Marker::paint_finished);
|
||||
@@ -393,9 +387,6 @@ Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) {
|
||||
[](Renderable* renderable, Renderable::Private& data) {
|
||||
data.publish_renderable_state(renderable);
|
||||
});
|
||||
auto& scene_state = static_cast<State&>(double_buffer::detail::Internal_Access::pending_state(object));
|
||||
scene_state.frame_statistics = frame_statistics.submit(
|
||||
*frame, Frame_Dimension::two_dimensional);
|
||||
double_buffer::detail::Internal_Access::publish_state<Render_Scene_2D::Base_Tag>(object);
|
||||
|
||||
active_frame = nullptr;
|
||||
@@ -489,13 +480,11 @@ void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport,
|
||||
}
|
||||
}
|
||||
template <Attached Object>
|
||||
void Render_Scene_2D::Private::reset_frame_statistics(Object* object) {
|
||||
double_buffer::detail::Internal_Access::publish_state<Render_Scene_2D::Base_Tag, &State::frame_statistics>(object,
|
||||
void Render_Scene_2D::Private::reset_diagnostics(Object* object) {
|
||||
double_buffer::detail::Internal_Access::publish_state<Scene::Base_Tag, &Scene::State::event_statistics>(object,
|
||||
[this](State_Access<typename Object::State> states) {
|
||||
frame_statistics.reset();
|
||||
event_statistics.reset();
|
||||
auto& state = states.template get<Render_Scene_2D::Base_Tag>();
|
||||
state.frame_statistics = {};
|
||||
auto& state = states.template get<Scene::Base_Tag>();
|
||||
state.event_statistics = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <render_2D/axis/Axis.hpp>
|
||||
#include <render_2D/base/Frame_2D.hpp>
|
||||
#include <render_2D/plottable/Constellation_Diagram.hpp>
|
||||
#include <render_2D/plottable/Afterglow.hpp>
|
||||
#include <render_2D/plottable/Spectrum.hpp>
|
||||
#include <render_2D/plottable/Waterfall.hpp>
|
||||
#include <render_2D/scene/Render_Scene_2D.hpp>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -40,6 +42,7 @@ int main() {
|
||||
using Spectrum_Object = Spectrum;
|
||||
using Constellation_Object = Constellation_Diagram;
|
||||
using Waterfall_Object = Waterfall;
|
||||
using Afterglow_Object = Afterglow;
|
||||
using Scene_Object = Render_Scene_2D;
|
||||
|
||||
initialize_runtime({.workers = 4});
|
||||
@@ -49,11 +52,15 @@ int main() {
|
||||
auto* time = build_object<Time>();
|
||||
auto* waterfall = build_object<Waterfall_Object>(
|
||||
frequency, time, Plot_Partition_Grid{2, 2});
|
||||
auto* afterglow = build_object<Afterglow_Object>(
|
||||
frequency, power, Plot_Partition_Grid{2, 2});
|
||||
auto* q_axis = build_object<Power>();
|
||||
auto* constellation = build_object<Constellation_Object>(
|
||||
power, q_axis, 3u);
|
||||
auto* spectrum_painted = new std::atomic_bool{};
|
||||
auto* topology_valid = new std::atomic_bool{true};
|
||||
auto* second_frame_valid = new std::atomic_bool{};
|
||||
auto* trace_valid = new std::atomic_bool{true};
|
||||
spectrum->taskflow().add("test.spectrum.render.complete", [spectrum_painted] {
|
||||
spectrum_painted->store(true, std::memory_order_release);
|
||||
});
|
||||
@@ -70,6 +77,7 @@ int main() {
|
||||
typename Scene_Object::template Builder<Scene_Object> scene_builder;
|
||||
scene_builder.add_renderable(spectrum);
|
||||
scene_builder.add_renderable(waterfall);
|
||||
scene_builder.add_renderable(afterglow);
|
||||
scene_builder.add_renderable(constellation);
|
||||
auto scene_result = scene_builder.build();
|
||||
if (!scene_result) return 2;
|
||||
@@ -110,6 +118,13 @@ int main() {
|
||||
std::make_shared<const Waterfall_Row>(Waterfall_Row{
|
||||
second_tick, std::vector<Plot_Value>(64, -30.0)}));
|
||||
waterfall->mark_dirty<Render_Graph_Tag>();
|
||||
afterglow->set<&Afterglow::Prop::frequency_range>(
|
||||
Axis_Range{0.0, 100.0});
|
||||
afterglow->set<&Afterglow::Prop::power_range>(Axis_Range{-100.0, 0.0});
|
||||
afterglow->set<&Afterglow::Prop::power_point_size>(32u);
|
||||
afterglow->submit_stream<Afterglow_Stream_Tag>(
|
||||
std::make_shared<const std::vector<Plot_Value>>(64, -40.0));
|
||||
afterglow->mark_dirty<Render_Graph_Tag>();
|
||||
constellation->set<&Constellation_Diagram::Prop::i_range>(
|
||||
Axis_Range{-1.0, 1.0});
|
||||
constellation->set<&Constellation_Diagram::Prop::q_range>(
|
||||
@@ -122,27 +137,40 @@ int main() {
|
||||
scene->activate_view();
|
||||
auto* frame = new Frame_2D(Frame_Identity{1, 0});
|
||||
auto* resized_partition_frame = new Frame_2D(Frame_Identity{2, 0});
|
||||
frame->request_taskflow_trace();
|
||||
resized_partition_frame->request_taskflow_trace();
|
||||
scene->set_frame_retired_callback(
|
||||
[resized_partition_frame, second_frame_valid, trace_valid](
|
||||
Frame_2D* retired) {
|
||||
const auto trace = retired->take_taskflow_trace();
|
||||
bool spectrum_hold{};
|
||||
bool afterglow_reduce{};
|
||||
bool afterglow_color_barrier{};
|
||||
for (const auto& graph : trace.graphs)
|
||||
for (const auto& node : graph.nodes) {
|
||||
spectrum_hold = spectrum_hold ||
|
||||
node.name == "prepare.hold.partition";
|
||||
afterglow_reduce = afterglow_reduce ||
|
||||
node.name == "prepare.normalize";
|
||||
afterglow_color_barrier = afterglow_color_barrier ||
|
||||
node.name == "prepare.color.complete";
|
||||
}
|
||||
if (!spectrum_hold || !afterglow_reduce ||
|
||||
!afterglow_color_barrier)
|
||||
trace_valid->store(false, std::memory_order_relaxed);
|
||||
if (retired == resized_partition_frame)
|
||||
std::_Exit(second_frame_valid->load(std::memory_order_relaxed) &&
|
||||
trace_valid->load(std::memory_order_relaxed)
|
||||
? 0
|
||||
: 1);
|
||||
});
|
||||
scene->set_frame_callback(
|
||||
[scene, spectrum, waterfall, constellation, frequency, power, frame,
|
||||
[scene, spectrum, waterfall, afterglow, constellation, frame,
|
||||
resized_partition_frame, spectrum_painted,
|
||||
topology_valid](Frame_2D* completed) {
|
||||
const auto& spectrum_state =
|
||||
spectrum->read_state<Renderable::Base_Tag>();
|
||||
const auto& waterfall_state =
|
||||
waterfall->read_state<Renderable::Base_Tag>();
|
||||
const auto& constellation_state =
|
||||
constellation->read_state<Renderable::Base_Tag>();
|
||||
topology_valid, second_frame_valid](Frame_2D* completed) {
|
||||
const bool valid = completed == frame &&
|
||||
spectrum_state.render_executed &&
|
||||
spectrum_state.task_count == 11 &&
|
||||
waterfall_state.render_executed &&
|
||||
waterfall_state.task_count == 10 &&
|
||||
constellation_state.render_executed &&
|
||||
constellation_state.task_count == 5 &&
|
||||
spectrum->read_state<Spectrum::Base_Tag>().sample_count ==
|
||||
512 &&
|
||||
frequency->read_state<Renderable::Base_Tag>().render_executed &&
|
||||
power->read_state<Renderable::Base_Tag>().render_executed &&
|
||||
topology_valid->load(std::memory_order_relaxed) &&
|
||||
contains_color(completed->image());
|
||||
if (!valid) std::_Exit(1);
|
||||
@@ -150,34 +178,23 @@ int main() {
|
||||
spectrum->set<&Spectrum::Prop::partition_count>(2u);
|
||||
waterfall->set<&Waterfall::Prop::partition_grid>(
|
||||
Plot_Partition_Grid{3, 2});
|
||||
afterglow->set<&Afterglow::Prop::partition_grid>(
|
||||
Plot_Partition_Grid{3, 2});
|
||||
afterglow->submit_stream<Afterglow_Stream_Tag>(
|
||||
std::make_shared<const std::vector<Plot_Value>>(64, -20.0));
|
||||
afterglow->mark_dirty<Render_Graph_Tag>();
|
||||
constellation->set<
|
||||
&Constellation_Diagram::Prop::partition_count>(2u);
|
||||
spectrum_painted->store(false, std::memory_order_relaxed);
|
||||
scene->set_frame_callback(
|
||||
[spectrum, waterfall, constellation, frequency, power,
|
||||
resized_partition_frame,
|
||||
[resized_partition_frame, second_frame_valid,
|
||||
topology_valid](Frame_2D* next) {
|
||||
const auto& next_spectrum =
|
||||
spectrum->read_state<Renderable::Base_Tag>();
|
||||
const auto& next_waterfall =
|
||||
waterfall->read_state<Renderable::Base_Tag>();
|
||||
const auto& next_constellation =
|
||||
constellation->read_state<Renderable::Base_Tag>();
|
||||
const bool resized_valid =
|
||||
next == resized_partition_frame &&
|
||||
next_spectrum.graph_rebuilt &&
|
||||
next_spectrum.task_count == 7 &&
|
||||
next_waterfall.graph_rebuilt &&
|
||||
next_waterfall.task_count == 14 &&
|
||||
next_constellation.graph_rebuilt &&
|
||||
next_constellation.task_count == 4 &&
|
||||
frequency->read_state<Renderable::Base_Tag>()
|
||||
.render_executed &&
|
||||
power->read_state<Renderable::Base_Tag>()
|
||||
.render_executed &&
|
||||
topology_valid->load(std::memory_order_relaxed) &&
|
||||
contains_color(next->image());
|
||||
std::_Exit(resized_valid ? 0 : 1);
|
||||
second_frame_valid->store(resized_valid,
|
||||
std::memory_order_relaxed);
|
||||
});
|
||||
if (!scene->render(resized_partition_frame)) std::_Exit(2);
|
||||
});
|
||||
|
||||
@@ -141,6 +141,81 @@ TEST(partition_configuration,
|
||||
}
|
||||
}
|
||||
|
||||
TEST(partition_configuration,
|
||||
bilinear_tiles_with_source_halo_match_one_complete_heatmap) {
|
||||
const Size canvas{137, 93};
|
||||
const aethera::render_2d::detail::Raster_Layout layout{
|
||||
19, 13, Rect_F{3.25, 5.5, 128.5, 80.25}, false, false, true};
|
||||
std::vector<Pixel> pixels(
|
||||
static_cast<std::size_t>(layout.width) * layout.height);
|
||||
for (int y = 0; y < layout.height; ++y)
|
||||
for (int x = 0; x < layout.width; ++x)
|
||||
pixels[static_cast<std::size_t>(y) * layout.width + x] =
|
||||
0xff000000u | (static_cast<Pixel>(x * 13) << 16u) |
|
||||
(static_cast<Pixel>(y * 17) << 8u);
|
||||
|
||||
Blend2D_Cache expected_cache;
|
||||
expected_cache.ensure_size(canvas);
|
||||
expected_cache.clear();
|
||||
{
|
||||
aethera::render_2d::detail::Painter painter(expected_cache, canvas);
|
||||
aethera::render_2d::detail::paint_raster(
|
||||
painter, layout, pixels, Image_Interpolation_Mode::bilinear);
|
||||
}
|
||||
|
||||
Blend2D_Cache actual_cache;
|
||||
actual_cache.ensure_size(canvas);
|
||||
actual_cache.clear();
|
||||
constexpr Plot_Partition_Grid grid{4, 3};
|
||||
const Rect_F target = layout.target.normalized();
|
||||
for (Plot_Partition_Count index = 0;
|
||||
index < aethera::render_2d::detail::raster_partition_count(grid);
|
||||
++index) {
|
||||
const auto core = aethera::render_2d::detail::raster_partition(
|
||||
layout, index, grid);
|
||||
const aethera::render_2d::detail::Raster_Partition source{
|
||||
std::max(0, core.first_x - 1),
|
||||
std::min(layout.width, core.last_x + 1),
|
||||
std::max(0, core.first_y - 1),
|
||||
std::min(layout.height, core.last_y + 1)};
|
||||
const int width = source.last_x - source.first_x;
|
||||
const int height = source.last_y - source.first_y;
|
||||
std::vector<Pixel> tile(static_cast<std::size_t>(width) * height);
|
||||
for (int y = source.first_y; y < source.last_y; ++y)
|
||||
std::copy_n(pixels.begin() + static_cast<std::ptrdiff_t>(y) *
|
||||
layout.width + source.first_x,
|
||||
width,
|
||||
tile.begin() + static_cast<std::ptrdiff_t>(
|
||||
y - source.first_y) * width);
|
||||
const auto project_x = [&](int boundary) {
|
||||
return target.x + target.width * boundary / layout.width;
|
||||
};
|
||||
const auto project_y = [&](int boundary) {
|
||||
return target.y + target.height * boundary / layout.height;
|
||||
};
|
||||
const double left = project_x(source.first_x);
|
||||
const double top = project_y(source.first_y);
|
||||
aethera::render_2d::detail::Painter painter(
|
||||
actual_cache, canvas,
|
||||
aethera::render_2d::detail::raster_paint_region(layout, index, grid));
|
||||
painter.heatmap(
|
||||
{left, top, project_x(source.last_x) - left,
|
||||
project_y(source.last_y) - top},
|
||||
width, height, tile, Image_Interpolation_Mode::bilinear);
|
||||
}
|
||||
const Image_View expected = expected_cache.view();
|
||||
const Image_View actual = actual_cache.view();
|
||||
for (int y = 0; y < expected.height; ++y) {
|
||||
const auto* expected_row = reinterpret_cast<const Pixel*>(
|
||||
expected.data + static_cast<std::ptrdiff_t>(y) * expected.stride);
|
||||
const auto* actual_row = reinterpret_cast<const Pixel*>(
|
||||
actual.data + static_cast<std::ptrdiff_t>(y) * actual.stride);
|
||||
for (int x = 0; x < expected.width; ++x)
|
||||
EXPECT_EQ(actual_row[x], expected_row[x])
|
||||
<< "tile seam mismatch at " << x << ", " << y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(partition_configuration,
|
||||
raster_grid_covers_each_matrix_cell_exactly_once) {
|
||||
const aethera::render_2d::detail::Raster_Layout layout{
|
||||
|
||||
@@ -185,7 +185,6 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) {
|
||||
render_once(scene.get());
|
||||
EXPECT_EQ(diagram->access_rendering_stream<Constellation_Stream_Tag>(
|
||||
[](std::span<Constellation_Point> points) { return points.size(); }), 1u);
|
||||
EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().task_count, 1u);
|
||||
}
|
||||
|
||||
TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) {
|
||||
|
||||
@@ -142,8 +142,6 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
|
||||
EXPECT_TRUE(cache_graph.depends_on(spectrum.get(), power.get()));
|
||||
auto output_frame = render_frame(scene.get());
|
||||
EXPECT_GT(spectrum->read_state<Spectrum::Base_Tag>().rendered_point_count, 0u);
|
||||
const auto& render_state = spectrum->read_state<Renderable::Base_Tag>();
|
||||
EXPECT_EQ(render_state.task_count, 9u);
|
||||
const Image_View frame = output_frame->image();
|
||||
ASSERT_FALSE(frame.empty());
|
||||
EXPECT_TRUE(contains_color(frame));
|
||||
@@ -158,23 +156,16 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
|
||||
frequency->set<&Numeric_Axis::Prop::precision>(3);
|
||||
EXPECT_FALSE(spectrum->dirty<Render_Cache_Tag>());
|
||||
output_frame = render_frame(scene.get());
|
||||
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().render_executed);
|
||||
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().render_executed);
|
||||
EXPECT_TRUE(power->read_state<Renderable::Base_Tag>().render_executed);
|
||||
EXPECT_TRUE(contains_color(output_frame->image()));
|
||||
frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 120.0});
|
||||
EXPECT_TRUE(spectrum->dirty<Render_Cache_Tag>());
|
||||
output_frame = render_frame(scene.get());
|
||||
EXPECT_FALSE(spectrum->dirty<Render_Cache_Tag>());
|
||||
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().render_executed);
|
||||
spectrum->set<&Spectrum::Prop::partition_count>(2u);
|
||||
spectrum->pending_buffer<Spectrum_Frame_Tag>() =
|
||||
Spectrum_Frame{std::vector<Spectrum_Power>(std::begin(samples), std::end(samples))};
|
||||
spectrum->mark_dirty<Render_Graph_Tag>();
|
||||
output_frame = render_frame(scene.get());
|
||||
const auto& rebuilt_state = spectrum->read_state<Renderable::Base_Tag>();
|
||||
EXPECT_TRUE(rebuilt_state.graph_rebuilt);
|
||||
EXPECT_EQ(rebuilt_state.task_count, 7u);
|
||||
EXPECT_TRUE(contains_color(output_frame->image()));
|
||||
const Size resized_canvas{200, 140};
|
||||
scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas);
|
||||
@@ -183,7 +174,6 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
|
||||
EXPECT_FALSE(power->dirty<Render_Graph_Tag>());
|
||||
output_frame = render_frame(scene.get());
|
||||
EXPECT_EQ(scene->read_prop<Render_Scene_2D::Base_Tag>().viewport, resized_canvas);
|
||||
EXPECT_TRUE(frequency->read_state<Renderable::Base_Tag>().render_executed);
|
||||
const Image_View resized_frame = output_frame->image();
|
||||
EXPECT_EQ(resized_frame.width, resized_canvas.width);
|
||||
EXPECT_EQ(resized_frame.height, resized_canvas.height);
|
||||
@@ -225,7 +215,6 @@ TEST(spectrum_data, repeatedly_publishes_new_samples_during_long_running_render)
|
||||
const auto hash = image_hash(frame->image());
|
||||
if (frame_index != 0 && hash != previous_hash) ++changed_frames;
|
||||
previous_hash = hash;
|
||||
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().render_executed);
|
||||
EXPECT_EQ(spectrum->read_state<Spectrum::Base_Tag>().sample_count, 128u);
|
||||
}
|
||||
EXPECT_GT(changed_frames, 700u);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "Render_Scene_3D.hpp" /* 三维事件计时边界与异步提交阶段实现。 */
|
||||
namespace aethera::render_3d {
|
||||
bool Render_Scene_3D::Prop::operator==(const Prop&) const = default;
|
||||
bool Render_Scene_3D::State::operator==(const State&) const = default;
|
||||
|
||||
Render_Scene_3D::Render_Result Render_Scene_3D::render(Frame_3D* frame) {
|
||||
return double_buffer::detail::Internal_Access::get(this).render(this, frame);
|
||||
@@ -15,7 +14,7 @@ void Render_Scene_3D::activate_view() {
|
||||
void Render_Scene_3D::deactivate_view() {
|
||||
double_buffer::detail::Internal_Access::get(this).set_view_active(this, false);
|
||||
}
|
||||
void Render_Scene_3D::reset_frame_statistics() {
|
||||
double_buffer::detail::Internal_Access::get(this).reset_frame_statistics(this);
|
||||
void Render_Scene_3D::reset_diagnostics() {
|
||||
double_buffer::detail::Internal_Access::get(this).reset_diagnostics(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "../detail/Backend_Types.hpp"
|
||||
#include "../visual/Visuals.hpp"
|
||||
#include <scene.hpp>
|
||||
#include <frame_statistics.hpp>
|
||||
#include <array>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
@@ -20,10 +19,7 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene> {
|
||||
bool view_active{true}; /* 是否接受 render(frame*) 产生新的完成帧。 */
|
||||
bool operator==(const Prop&) const;
|
||||
};
|
||||
struct State : Prev_State {
|
||||
Frame_Statistics_State frame_statistics{};
|
||||
bool operator==(const State&) const;
|
||||
};
|
||||
struct State : Prev_State {};
|
||||
struct Private;
|
||||
template <typename Object>
|
||||
struct Builder : Prev_Builder<Object> {
|
||||
@@ -70,7 +66,7 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene> {
|
||||
*/
|
||||
void activate_view();
|
||||
void deactivate_view();
|
||||
void reset_frame_statistics();
|
||||
void reset_diagnostics();
|
||||
};
|
||||
}
|
||||
#include "Render_Scene_3D.ipp"
|
||||
|
||||
@@ -19,7 +19,6 @@ struct Scene_Paint_Context {
|
||||
};
|
||||
}
|
||||
struct Render_Scene_3D::Private : Prev_Private {
|
||||
Frame_Statistics_Accumulator frame_statistics{}; /* 完成线程在 Scene 状态发布临界区内更新。 */
|
||||
std::mutex render_mutex{}; /* 只保护完成回调和单帧准入。 */
|
||||
bool frame_in_flight{}; /* render 准入到异步完成回调返回的唯一状态源。 */
|
||||
Frame_Callback frame_callback{}; /* Scene 的唯一对外完成出口。 */
|
||||
@@ -33,7 +32,6 @@ struct Render_Scene_3D::Private : Prev_Private {
|
||||
std::unique_ptr<Task_Graph> frame_taskflow{};
|
||||
Event_Batch active_events{};
|
||||
bool active_trace{};
|
||||
std::chrono::steady_clock::time_point active_render_started{};
|
||||
/* Builder 内部初始化后端;必须在绑定 Visual Paint 目标之前调用一次。 */
|
||||
template <Attached Object>
|
||||
void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled,
|
||||
@@ -48,7 +46,7 @@ struct Render_Scene_3D::Private : Prev_Private {
|
||||
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_3D* frame);
|
||||
template <Attached Object> void set_frame_callback(Object* object, Frame_Callback callback);
|
||||
template <Attached Object> void set_view_active(Object* object, bool active);
|
||||
template <Attached Object> void reset_frame_statistics(Object* object);
|
||||
template <Attached Object> void reset_diagnostics(Object* object);
|
||||
};
|
||||
template <typename Object>
|
||||
Render_Scene_3D::Builder<Object>::Builder() : Base() {}
|
||||
@@ -169,12 +167,10 @@ void Render_Scene_3D::Private::complete_frame(Object* object, Frame_3D* frame) {
|
||||
if (callback) callback(frame);
|
||||
frame->mark(Frame_Trace_Marker::callback_finished);
|
||||
frame->mark(Frame_Trace_Marker::frame_ready);
|
||||
double_buffer::detail::Internal_Access::publish_state<Render_Scene_3D::Base_Tag,
|
||||
&State::frame_statistics>(object,
|
||||
double_buffer::detail::Internal_Access::publish_state<Scene::Base_Tag,
|
||||
&Scene::State::event_statistics>(object,
|
||||
[this, frame](State_Access<typename Object::State> states) {
|
||||
auto& state = states.template get<Render_Scene_3D::Base_Tag>();
|
||||
state.frame_statistics = frame_statistics.submit(
|
||||
*frame, Frame_Dimension::three_dimensional);
|
||||
auto& state = states.template get<Scene::Base_Tag>();
|
||||
state.event_statistics = event_statistics.state();
|
||||
});
|
||||
auto context = std::static_pointer_cast<
|
||||
@@ -214,6 +210,8 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) {
|
||||
|
||||
frame_taskflow = std::make_unique<Task_Graph>("render_3d.frame");
|
||||
auto& graph = *frame_taskflow;
|
||||
graph.describe("owner_kind", "scene")
|
||||
.describe("owner_component", "scene");
|
||||
auto begin = graph.add("scene.begin", [this, object] {
|
||||
if (!active_frame)
|
||||
throw std::logic_error("3D frame DAG lost its active frame");
|
||||
@@ -239,7 +237,6 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) {
|
||||
context->frame = active_frame;
|
||||
camera_component->advance_object();
|
||||
axes_component->advance_object();
|
||||
active_render_started = std::chrono::steady_clock::now();
|
||||
active_frame->mark(Frame_Trace_Marker::prepare_started);
|
||||
active_frame->mark(Frame_Trace_Marker::prepare_finished);
|
||||
active_frame->mark(Frame_Trace_Marker::paint_started);
|
||||
@@ -259,9 +256,6 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) {
|
||||
"render scene published an incomplete Visual batch");
|
||||
auto& state = static_cast<State&>(
|
||||
double_buffer::detail::Internal_Access::pending_state(object));
|
||||
state.taskflow_execution_time_ns = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - active_render_started).count());
|
||||
state.event_statistics = event_statistics.state();
|
||||
context->backend->render(
|
||||
context->visuals, context->parameters, active_frame,
|
||||
@@ -287,12 +281,6 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) {
|
||||
template <Attached Object>
|
||||
Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, Frame_3D* frame) {
|
||||
if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame");
|
||||
const Prop& prop = static_cast<const Prop&>(
|
||||
double_buffer::detail::Internal_Access::current_prop(object));
|
||||
if (!prop.view_active) return Render_Result::view_inactive;
|
||||
if (prop.viewport.empty()) return Render_Result::empty_viewport;
|
||||
if (!backend || !backend->available()) return Render_Result::backend_unavailable;
|
||||
ensure_frame_taskflow(object);
|
||||
{
|
||||
std::lock_guard lock(render_mutex);
|
||||
if (!frame_callback)
|
||||
@@ -301,6 +289,31 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object,
|
||||
frame_in_flight = true;
|
||||
active_frame = frame;
|
||||
}
|
||||
const auto reject_frame = [this](Render_Result result) {
|
||||
active_frame = nullptr;
|
||||
std::lock_guard lock(render_mutex);
|
||||
frame_in_flight = false;
|
||||
return result;
|
||||
};
|
||||
try {
|
||||
/*
|
||||
* Plot 在 render() 前写入 viewport,Visual 也可能在同一帧更新属性。
|
||||
* 在创建或执行 frame_taskflow 前一次性推进完整 Scene,确保公共
|
||||
* Render_Graph_Tag 已构建并且 frame DAG 引用的是同一权威运行图。
|
||||
*/
|
||||
double_buffer::detail::Internal_Access::advance(object);
|
||||
const Prop& prop = static_cast<const Prop&>(
|
||||
double_buffer::detail::Internal_Access::current_prop(object));
|
||||
if (!prop.view_active) return reject_frame(Render_Result::view_inactive);
|
||||
if (prop.viewport.empty()) return reject_frame(Render_Result::empty_viewport);
|
||||
if (!backend || !backend->available())
|
||||
return reject_frame(Render_Result::backend_unavailable);
|
||||
ensure_frame_taskflow(object);
|
||||
}
|
||||
catch (...) {
|
||||
reject_frame(Render_Result::backend_unavailable);
|
||||
throw;
|
||||
}
|
||||
frame->mark(Frame_Trace_Marker::scene_render_requested);
|
||||
active_trace = aethera::detail::begin_taskflow_trace(*frame);
|
||||
try {
|
||||
@@ -332,14 +345,11 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object,
|
||||
return Render_Result::submitted;
|
||||
}
|
||||
template <Attached Object>
|
||||
void Render_Scene_3D::Private::reset_frame_statistics(Object* object) {
|
||||
auto& data = static_cast<typename Object::Private&>(*this);
|
||||
double_buffer::detail::Internal_Access::publish_state<Render_Scene_3D::Base_Tag, &State::frame_statistics>(object,
|
||||
[&data](State_Access<typename Object::State> states) {
|
||||
data.frame_statistics.reset();
|
||||
data.event_statistics.reset();
|
||||
auto& state = states.template get<Render_Scene_3D::Base_Tag>();
|
||||
state.frame_statistics = {};
|
||||
void Render_Scene_3D::Private::reset_diagnostics(Object* object) {
|
||||
double_buffer::detail::Internal_Access::publish_state<Scene::Base_Tag, &Scene::State::event_statistics>(object,
|
||||
[this](State_Access<typename Object::State> states) {
|
||||
event_statistics.reset();
|
||||
auto& state = states.template get<Scene::Base_Tag>();
|
||||
state.event_statistics = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
nlohmann::json schema() const override {
|
||||
nlohmann::json components = nlohmann::json::array();
|
||||
for (const auto& descriptor : descriptors) components.push_back(descriptor->schema());
|
||||
return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}};
|
||||
return {{"protocol", "aethera.plot.inspector"}, {"version", 3}, {"components", std::move(components)}};
|
||||
}
|
||||
nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
|
||||
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) {
|
||||
@@ -53,6 +53,20 @@ public:
|
||||
result["component"] = component;
|
||||
return result;
|
||||
}
|
||||
nlohmann::json component_state(std::string_view component) const override {
|
||||
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) {
|
||||
return item->id() == component;
|
||||
});
|
||||
if (found == descriptors.end())
|
||||
return {{"success", false}, {"error", "unknown component"}};
|
||||
return (*found)->state();
|
||||
}
|
||||
nlohmann::json component_snapshots() const override {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
for (const auto& descriptor : descriptors)
|
||||
result[std::string(descriptor->id())] = descriptor->snapshot();
|
||||
return result;
|
||||
}
|
||||
nlohmann::json data_generator_schema() const override {
|
||||
if (!data_generator) return nullptr;
|
||||
return data_generator.schema;
|
||||
@@ -85,15 +99,8 @@ template <typename Object, typename... Fields>
|
||||
std::unique_ptr<detail::Renderable_Descriptor> make_renderable_component(
|
||||
std::string id, std::string label, std::string kind, Object& object) {
|
||||
using Definition = typename Object::Attached_Object;
|
||||
using Tag = typename Definition::Base_Tag;
|
||||
using State = typename Definition::State;
|
||||
using Adapter = detail::Renderable_Adapter<Object, Fields...,
|
||||
detail::Prop_Field < &Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">,
|
||||
detail::State_Field<Tag, &State::render_dirty, "render_dirty", "Whether the render graph was dirty when evaluated.">,
|
||||
detail::State_Field<Tag, &State::render_executed, "render_executed", "Whether the render graph executed during the latest scene cycle.">,
|
||||
detail::State_Field<Tag, &State::graph_rebuilt, "graph_rebuilt", "Whether the render graph was rebuilt during the latest cycle.">,
|
||||
detail::State_Field<Tag, &State::task_count, "task_count", "Number of tasks in the current render graph.">,
|
||||
detail::State_Field<Tag, &State::execution_time_ns, "execution_time_ns", "Measured render graph execution time in nanoseconds."> >;
|
||||
detail::Prop_Field < &Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">>;
|
||||
return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object});
|
||||
}
|
||||
template <typename Scene_Object>
|
||||
@@ -103,14 +110,7 @@ std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object
|
||||
using Adapter = detail::Renderable_Adapter<Scene_Object,
|
||||
detail::Prop_Field < &Prop::viewport, "viewport", "Final scene viewport in physical pixels.">,
|
||||
detail::Prop_Field < &Prop::background, "background", "Scene clear color." >,
|
||||
detail::Prop_Field < &Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds."> >;
|
||||
detail::Prop_Field < &Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
|
||||
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
|
||||
}
|
||||
template <typename Axis_Object>
|
||||
|
||||
@@ -225,10 +225,7 @@ public:
|
||||
using State = typename Definition::State;
|
||||
using Scene_Adapter = detail::Renderable_Adapter<Scene_3D,
|
||||
detail::Prop_Field < &Render_Scene_3D::Prop::clear_color, "clear_color", "Linear scene clear color.">,
|
||||
detail::Prop_Field < &Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
|
||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds."> >;
|
||||
detail::Prop_Field < &Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
|
||||
using Visual_Adapter = detail::Renderable_Adapter<Visual_Object,
|
||||
detail::Prop_Field < &Prop::transform, "transform", "World transform applied to the complete visual.">,
|
||||
detail::Prop_Field < &Prop::visible, "visible", "Whether the visual participates in rendering." >,
|
||||
@@ -287,7 +284,7 @@ public:
|
||||
[[nodiscard]] nlohmann::json schema() const override {
|
||||
nlohmann::json components = nlohmann::json::array();
|
||||
for (const auto& descriptor : descriptors_) components.push_back(descriptor->schema());
|
||||
return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}};
|
||||
return {{"protocol", "aethera.plot.inspector"}, {"version", 3}, {"components", std::move(components)}};
|
||||
}
|
||||
[[nodiscard]] nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
|
||||
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
|
||||
@@ -298,6 +295,20 @@ public:
|
||||
result["component"] = component;
|
||||
return result;
|
||||
}
|
||||
[[nodiscard]] nlohmann::json component_state(std::string_view component) const override {
|
||||
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
|
||||
return descriptor->id() == component;
|
||||
});
|
||||
if (found == descriptors_.end())
|
||||
return {{"success", false}, {"error", "unknown component"}};
|
||||
return (*found)->state();
|
||||
}
|
||||
[[nodiscard]] nlohmann::json component_snapshots() const override {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
for (const auto& descriptor : descriptors_)
|
||||
result[std::string(descriptor->id())] = descriptor->snapshot();
|
||||
return result;
|
||||
}
|
||||
[[nodiscard]] nlohmann::json data_generator_schema() const override {
|
||||
if constexpr (std::same_as<Data_Generator, Random_Data_Generator>) {
|
||||
using Definition = typename Visual_Object::Attached_Object;
|
||||
|
||||
+38
-20
@@ -198,7 +198,9 @@ void append_event_statistics_json(nlohmann::json& output,
|
||||
|
||||
}
|
||||
|
||||
nlohmann::json taskflow_trace_json(const Taskflow_Frame_Trace& trace) {
|
||||
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)] =
|
||||
@@ -218,12 +220,24 @@ nlohmann::json taskflow_trace_json(const Taskflow_Frame_Trace& trace) {
|
||||
nlohmann::json attributes = nlohmann::json::object();
|
||||
for (const auto& [key, value] : node.attributes)
|
||||
attributes[key] = value;
|
||||
nodes.push_back({
|
||||
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)}});
|
||||
{"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},
|
||||
@@ -374,11 +388,11 @@ struct Plot::Private {
|
||||
/* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace,
|
||||
* GET 直接读取已发布槽位,不复制或重排整个历史容器。 */
|
||||
std::atomic_uint64_t taskflow_trace_control{};
|
||||
std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
|
||||
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 Taskflow_Frame_Trace>>,
|
||||
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。 */
|
||||
@@ -428,13 +442,14 @@ struct Plot::Private {
|
||||
[[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 Taskflow_Frame_Trace>>,
|
||||
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
||||
maximum_taskflow_trace_frames>& slots,
|
||||
const Taskflow_Frame_Trace& trace);
|
||||
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 Taskflow_Frame_Trace>>,
|
||||
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
||||
maximum_taskflow_trace_frames>& slots) const;
|
||||
void fail(std::exception_ptr failure) noexcept;
|
||||
};
|
||||
@@ -610,10 +625,12 @@ bool Plot::Private::mark_post_publish_taskflow_trace() {
|
||||
|
||||
void Plot::Private::store_trace(
|
||||
std::atomic_uint64_t& control,
|
||||
std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
|
||||
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
|
||||
maximum_taskflow_trace_frames>& slots,
|
||||
const Taskflow_Frame_Trace& value) {
|
||||
auto trace = std::make_shared<const Taskflow_Frame_Trace>(value);
|
||||
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);
|
||||
@@ -632,7 +649,7 @@ void Plot::Private::store_trace(
|
||||
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 Taskflow_Frame_Trace>>,
|
||||
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);
|
||||
@@ -640,7 +657,7 @@ nlohmann::json Plot::Private::trace_response(
|
||||
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(taskflow_trace_json(*trace));
|
||||
frames.push_back(*trace);
|
||||
const auto left = remaining.load(std::memory_order_acquire);
|
||||
return {
|
||||
{"protocol", "aethera.taskflow.frames"}, {"version", 1},
|
||||
@@ -961,15 +978,12 @@ void Plot::Private::retire_completed_frame(Render_Frame* frame) {
|
||||
|
||||
{
|
||||
std::lock_guard lock(completed_frame_statistics_mutex);
|
||||
completed_frame_statistics_state = completed_frame_statistics.submit(
|
||||
*frame, std::holds_alternative<std::unique_ptr<Frame_2D>>(managed->frame)
|
||||
? Frame_Dimension::two_dimensional
|
||||
: Frame_Dimension::three_dimensional);
|
||||
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());
|
||||
frame->take_taskflow_trace(), view->component_snapshots());
|
||||
|
||||
auto expected = Frame_State::consuming;
|
||||
if (!managed->state.compare_exchange_strong(
|
||||
@@ -1174,6 +1188,10 @@ nlohmann::json Plot::write_prop(std::string_view component,
|
||||
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);
|
||||
@@ -1234,7 +1252,7 @@ nlohmann::json Plot::diagnostics() const {
|
||||
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", 2},
|
||||
{"protocol", "aethera.plot.diagnostics"}, {"version", 3},
|
||||
{"dimension", is_3d ? "3D" : "2D"},
|
||||
{"sequence", identity.sequence},
|
||||
{"correlation_id", identity.correlation_id},
|
||||
@@ -1356,7 +1374,7 @@ nlohmann::json Plot::post_publish_taskflow_trace() const {
|
||||
}
|
||||
|
||||
void Plot::reset_diagnostics() {
|
||||
std::visit([](auto& scene) { scene->reset_frame_statistics(); }, d->scene);
|
||||
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 = {};
|
||||
|
||||
@@ -65,6 +65,9 @@ public:
|
||||
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view component,
|
||||
std::string_view key,
|
||||
const nlohmann::json& value) = 0;
|
||||
[[nodiscard]] virtual nlohmann::json component_state(
|
||||
std::string_view component) const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json component_snapshots() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0;
|
||||
virtual void update(const Plot_Render_Tick& tick) = 0;
|
||||
@@ -86,6 +89,7 @@ public:
|
||||
[[nodiscard]] nlohmann::json write_prop(std::string_view component,
|
||||
std::string_view key,
|
||||
const nlohmann::json& value);
|
||||
[[nodiscard]] nlohmann::json component_state(std::string_view component) const;
|
||||
[[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input);
|
||||
[[nodiscard]] nlohmann::json diagnostics() const;
|
||||
/* 清空旧捕获并请求接下来实际完成的 frame_count 帧 Task DAG。 */
|
||||
|
||||
@@ -23,6 +23,8 @@ public:
|
||||
virtual ~Renderable_Descriptor() = default;
|
||||
[[nodiscard]] virtual std::string_view id() const noexcept = 0;
|
||||
[[nodiscard]] virtual nlohmann::json schema() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json state() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json snapshot() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) = 0;
|
||||
};
|
||||
template <typename Adapter>
|
||||
@@ -31,6 +33,8 @@ public:
|
||||
Renderable_Descriptor_Model(std::string id, std::string label, std::string kind, Adapter adapter);
|
||||
[[nodiscard]] std::string_view id() const noexcept override;
|
||||
[[nodiscard]] nlohmann::json schema() const override;
|
||||
[[nodiscard]] nlohmann::json state() const override;
|
||||
[[nodiscard]] nlohmann::json snapshot() const override;
|
||||
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override;
|
||||
private:
|
||||
std::string component_id;
|
||||
|
||||
@@ -44,6 +44,11 @@ template <typename Object, typename... Fields>
|
||||
struct Renderable_Adapter final : public structive::Property_Object<Renderable_Adapter<Object, Fields...>, structive::No_Lock_Policy> {
|
||||
public:
|
||||
explicit Renderable_Adapter(Object& value) : object(&value) {}
|
||||
void identify(std::string_view id) {
|
||||
if constexpr (std::derived_from<Object, aethera::Renderable>)
|
||||
double_buffer::detail::Internal_Access::layer<aethera::Renderable>(object)
|
||||
.diagnostic_component_id = id;
|
||||
}
|
||||
private:
|
||||
template <typename Adapter, typename Field>
|
||||
friend struct Renderable_Field_Accessor;
|
||||
@@ -287,15 +292,7 @@ inline std::string_view protocol_field_label(std::string_view key) {
|
||||
{"selected_regions", "已选区域"}, {"cache_enabled", "启用绘制缓存"},
|
||||
{"transform", "空间变换"}, {"visible", "是否可见"},
|
||||
{"depth_test", "深度测试"}, {"items", "项目数据"},
|
||||
{"render_dirty", "渲染图待更新"},
|
||||
{"render_executed", "渲染图已执行"},
|
||||
{"graph_rebuilt", "渲染图已重建"},
|
||||
{"task_count", "任务数量"},
|
||||
{"execution_time_ns", "渲染耗时"},
|
||||
{"taskflow_rebuilt", "场景任务图已重建"}, {"renderable_count", "可渲染对象数量"},
|
||||
{"taskflow_task_count", "场景任务数量"}, {"taskflow_dependency_count", "任务依赖数量"},
|
||||
{"taskflow_max_predecessors", "最大前驱数量"}, {"taskflow_max_successors", "最大后继数量"},
|
||||
{"taskflow_execution_time_ns", "场景执行耗时"}, {"next_tick", "下一时间刻度"},
|
||||
{"next_tick", "下一时间刻度"},
|
||||
{"sample_count", "样本数量"}, {"rendered_point_count", "已绘制点数量"},
|
||||
{"selectable_marker_count", "可选标记数量"}, {"stored_block_count", "已保存数据块数量"},
|
||||
{"stored_point_count", "已保存数据点数量"}, {"history_count", "历史帧数量"},
|
||||
@@ -312,11 +309,9 @@ inline std::string_view protocol_field_label(std::string_view key) {
|
||||
template <typename Adapter>
|
||||
nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::string_view label, std::string_view kind) {
|
||||
nlohmann::json fields = nlohmann::json::array();
|
||||
nlohmann::json state = nlohmann::json::object();
|
||||
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
|
||||
using Field = std::remove_cvref_t<decltype(field)>;
|
||||
using Value = typename Field::value_type;
|
||||
const auto value = field.accessor.read(adapter);
|
||||
const bool editable = field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop;
|
||||
const auto field_label = protocol_field_label(field.key());
|
||||
std::string_view editor = protocol_editor<Value>();
|
||||
@@ -327,9 +322,9 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
|
||||
{"key", field.key()}, {"label", field_label}, {"editor", editor},
|
||||
{"editable", editable},
|
||||
{"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"},
|
||||
{"technical_description", std::string(Field::accessor_type::description.view())},
|
||||
{"value", encode_protocol_value(value)}
|
||||
{"technical_description", std::string(Field::accessor_type::description.view())}
|
||||
};
|
||||
if (editable) item["value"] = encode_protocol_value(field.accessor.read(adapter));
|
||||
if constexpr (std::same_as<Value, render_3d::Linear_Color>) item["color_channel_scale"] = "normalized";
|
||||
else if constexpr (std::same_as<Value, Color>) item["color_channel_scale"] = "byte";
|
||||
if (editable) {
|
||||
@@ -341,17 +336,23 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
state[field.key()] = encode_protocol_value(value);
|
||||
}
|
||||
fields.push_back(std::move(item));
|
||||
});
|
||||
return {
|
||||
{"id", id}, {"label", label}, {"kind", kind},
|
||||
{"fields", std::move(fields)}, {"state", std::move(state)}
|
||||
{"fields", std::move(fields)}
|
||||
};
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json adapter_values(const Adapter& adapter, Renderable_Field_Role role) {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
|
||||
if (field.template attribute<Renderable_Field_Role_Category>().value == role)
|
||||
result[field.key()] = encode_protocol_value(field.accessor.read(adapter));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const nlohmann::json& input) {
|
||||
nlohmann::json result{{"success", false}, {"key", key}};
|
||||
const bool found = structive::visit_schema_property(structive::type_descriptor<Adapter>(), key, [&](auto, const auto& field) {
|
||||
@@ -382,7 +383,9 @@ nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const
|
||||
}
|
||||
template <typename Adapter>
|
||||
Renderable_Descriptor_Model<Adapter>::Renderable_Descriptor_Model(
|
||||
std::string id, std::string label, std::string kind, Adapter value) : component_id(std::move(id)), component_label(std::move(label)), component_kind(std::move(kind)), adapter(std::move(value)) {}
|
||||
std::string id, std::string label, std::string kind, Adapter value) : component_id(std::move(id)), component_label(std::move(label)), component_kind(std::move(kind)), adapter(std::move(value)) {
|
||||
adapter.identify(component_id);
|
||||
}
|
||||
template <typename Adapter>
|
||||
std::string_view Renderable_Descriptor_Model<Adapter>::id() const noexcept {
|
||||
return component_id;
|
||||
@@ -392,6 +395,19 @@ nlohmann::json Renderable_Descriptor_Model<Adapter>::schema() const {
|
||||
return adapter_schema(adapter, component_id, component_label, component_kind);
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::state() const {
|
||||
return {{"protocol", "aethera.component.state"}, {"version", 1},
|
||||
{"component", component_id},
|
||||
{"state", adapter_values(adapter, Renderable_Field_Role::state)}};
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::snapshot() const {
|
||||
return {{"component", component_id}, {"label", component_label},
|
||||
{"kind", component_kind},
|
||||
{"prop", adapter_values(adapter, Renderable_Field_Role::prop)},
|
||||
{"state", adapter_values(adapter, Renderable_Field_Role::state)}};
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::write_prop(std::string_view key, const nlohmann::json& value) {
|
||||
return write_adapter_prop(adapter, key, value);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
|
||||
namespace aethera::web {
|
||||
[[nodiscard]] nlohmann::json taskflow_trace_json(
|
||||
const Taskflow_Frame_Trace& trace);
|
||||
const Taskflow_Frame_Trace& trace,
|
||||
const nlohmann::json& component_snapshots = {});
|
||||
}
|
||||
|
||||
@@ -442,6 +442,28 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
|
||||
}
|
||||
}, {drogon::Put});
|
||||
|
||||
app.registerHandler("/plot/{1}/component/{2}/state", [plots](
|
||||
const drogon::HttpRequestPtr&,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
|
||||
std::string plot_id,
|
||||
std::string component) {
|
||||
auto plot = find_plot(*plots, plot_id);
|
||||
if (!plot) {
|
||||
callback(error_response(drogon::k404NotFound, "unknown plot"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
auto result = plot->component_state(component);
|
||||
if (result.value("success", true)) callback(json_response(std::move(result)));
|
||||
else callback(error_response(drogon::k404NotFound,
|
||||
result.value("error", "unknown component")));
|
||||
}
|
||||
catch (const std::exception& failure) {
|
||||
callback(error_response(drogon::k500InternalServerError,
|
||||
failure.what()));
|
||||
}
|
||||
}, {drogon::Get});
|
||||
|
||||
app.registerHandler("/plot/{1}/data/generate", [plots](
|
||||
const drogon::HttpRequestPtr& request,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
|
||||
|
||||
Generated
-26
@@ -14,7 +14,6 @@
|
||||
"@mui/material": "^6.4.8",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"dayjs": "^1.11.23",
|
||||
"echarts": "^6.1.0",
|
||||
"elkjs": "^0.9.3",
|
||||
"flexlayout-react": "^0.10.5",
|
||||
"interactjs": "^1.10.27",
|
||||
@@ -2664,16 +2663,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
|
||||
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.405",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz",
|
||||
@@ -3915,12 +3904,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -4285,15 +4268,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
|
||||
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"@mui/material": "^6.4.8",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"dayjs": "^1.11.23",
|
||||
"echarts": "^6.1.0",
|
||||
"elkjs": "^0.9.3",
|
||||
"flexlayout-react": "^0.10.5",
|
||||
"interactjs": "^1.10.27",
|
||||
|
||||
+80
-351
@@ -9,30 +9,23 @@ import {Background, Controls, MarkerType, MiniMap, Position, ReactFlow,
|
||||
import Timeline, {CustomMarker, DateHeader, SidebarHeader, TimelineHeaders,
|
||||
TimelineMarkers, type TimelineGroupBase, type TimelineItemBase}
|
||||
from "react-calendar-timeline";
|
||||
import * as echarts from "echarts/core";
|
||||
import {LineChart} from "echarts/charts";
|
||||
import {DataZoomComponent, GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
|
||||
import {CanvasRenderer} from "echarts/renderers";
|
||||
import type {EChartsType} from "echarts/core";
|
||||
import "flexlayout-react/style/alpha_dark.css";
|
||||
import "react-grid-layout/css/styles.css";
|
||||
import "react-resizable/css/styles.css";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import "react-calendar-timeline/style.css";
|
||||
|
||||
echarts.use([LineChart, DataZoomComponent, GridComponent, LegendComponent, TooltipComponent, CanvasRenderer]);
|
||||
|
||||
type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; media: string; schema: string; diagnostics: string; taskflow: string};
|
||||
type Option = {value: string; label: string};
|
||||
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "partition-grid" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "json";
|
||||
type Color_Channel_Scale = "normalized" | "byte";
|
||||
type Field = {key: string; label: string; description: string; technical_description?: string; editor: Editor; editable: boolean; value: unknown; options?: Option[]; color_channel_scale?: Color_Channel_Scale; minimum?: number; maximum?: number; step?: number};
|
||||
type Component = {id: string; label: string; kind: string; fields: Field[]; state: Record<string, unknown>};
|
||||
type Component = {id: string; label: string; kind: string; fields: Field[]};
|
||||
type Data_Generator = {label: string; description: string; fields: Field[]};
|
||||
type Plot_Execution_Policy = {visible: boolean};
|
||||
type Plot_Execution_Policies = Record<string, Plot_Execution_Policy>;
|
||||
type Frame_Analysis = Omit<Component, "state"> & {data_generator?: Data_Generator};
|
||||
type Schema = {protocol: "aethera.plot.inspector"; version: 2; components: Component[]; frame_analysis: Frame_Analysis};
|
||||
type Schema = {protocol: "aethera.plot.inspector"; version: 3; components: Component[]; frame_analysis: Frame_Analysis};
|
||||
type Stream_Status = "IDLE" | "CONNECTING" | "LIVE" | "OFFLINE";
|
||||
type Gallery_Transport_Mode = "ffmpeg" | "websocket_pixels";
|
||||
type Frame_Pacing_Mode = "manual" | "fixed_rate" | "maximum_rate";
|
||||
@@ -46,7 +39,7 @@ type Browser_Input_Statistic = {count: number; latest_ms: number; average_ms: nu
|
||||
maximum_ms: number; sent_count: number; disconnected_count: number;
|
||||
websocket_buffered_bytes: number};
|
||||
type Browser_Input_Statistics = Record<string, Browser_Input_Statistic>;
|
||||
type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 2; dimension: "2D" | "3D";
|
||||
type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 3; dimension: "2D" | "3D";
|
||||
sequence: number; correlation_id: number; rendered_sequence: number; rendered_correlation_id: number;
|
||||
generated_time_unix_ms: number; delivery: Frame_Delivery; frame_rate_fps: number; dropped_sequence_count: number;
|
||||
window_capacity: number; pixel: {width: number; height: number; format: Pixel_Format; native_format: Pixel_Format;
|
||||
@@ -81,10 +74,9 @@ type Frame_Diagnostics = {server: Plot_Diagnostics; samples: Frame_Sample[]; vid
|
||||
type Frame_Metrics = {sequence: number; generated_time_unix_ms: number; server_completion_ms: number; average_server_completion_ms: number;
|
||||
p95_server_completion_ms: number; p99_server_completion_ms: number; frame_rate_fps: number; p95_frame_interval_jitter_ms: number;
|
||||
pacing_mode: Frame_Pacing_Mode; fixed_rate_fps: number; delivery: Frame_Delivery; video_playback: Video_Playback_Metrics};
|
||||
type Stage_Statistic = "average" | "variability" | "p95" | "p99";
|
||||
type Stage_Unit = "value" | "percentage";
|
||||
type Taskflow_Node_Trace = {native_id: string; id: string; parent_id: string; name: string; type: string;
|
||||
predecessors: string[]; successors: string[]; attributes?: Record<string, string>};
|
||||
predecessors: string[]; successors: string[]; attributes?: Record<string, string>;
|
||||
owner?: {component: string; label: string; kind: string}; prop?: Record<string, unknown>; state?: Record<string, unknown>};
|
||||
type Taskflow_Graph_Trace = {stage: string; name: string; submitted_ms: number; finished_ms: number;
|
||||
completed: boolean; nodes: Taskflow_Node_Trace[]};
|
||||
type Taskflow_Execution_Trace = {native_id: string; node_id: string; worker_id: number; worker_queue_size: number;
|
||||
@@ -131,7 +123,7 @@ function gallery_socket_url(path: string, transport: Gallery_Transport_Mode) {
|
||||
function valid_plot_diagnostics(value: unknown): value is Plot_Diagnostics {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const diagnostics = value as Partial<Plot_Diagnostics>;
|
||||
return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 2 &&
|
||||
return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 3 &&
|
||||
typeof diagnostics.sequence === "number" && Boolean(diagnostics.frame_statistics) &&
|
||||
Boolean(diagnostics.input_statistics) && Boolean(diagnostics.pacing);
|
||||
}
|
||||
@@ -1010,231 +1002,6 @@ function use_active_component(schema: Schema | null) {
|
||||
return {components, selected, set_active};
|
||||
}
|
||||
|
||||
function format_metric(value: number, field: Field) {
|
||||
if (field.key === "generated_time_unix_ms") return new Date(value).toLocaleTimeString("zh-CN", {hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, fractionalSecondDigits: 3});
|
||||
if (field.key.endsWith("_time_ns")) return `${(value / 1_000_000).toFixed(3)} ms`;
|
||||
if (field.key.endsWith("_ms")) return `${value.toFixed(3)} ms`;
|
||||
if (field.key.endsWith("_fps")) return `${value.toFixed(1)} FPS`;
|
||||
return Number.isInteger(value) ? value.toLocaleString("zh-CN") : value.toLocaleString("zh-CN", {maximumFractionDigits: 3});
|
||||
}
|
||||
|
||||
type Pipeline_Stage_Definition = [string, string, string];
|
||||
const pipeline_2d_definitions: Pipeline_Stage_Definition[] = [
|
||||
["pipeline_2d_event_ms", "2D 事件分发", "Scene 将当前输入事件分发给二维 Renderable 的耗时。"],
|
||||
["pipeline_2d_prepare_ms", "2D 数据准备", "二维 Prepare 依赖图更新缓存、坐标映射和绘制数据的耗时。"],
|
||||
["pipeline_2d_frame_target_ms", "2D 帧目标准备", "调整主帧尺寸并清空完整 Blend2D 像素目标的耗时。"],
|
||||
["pipeline_2d_background_ms", "2D 背景填充", "向主帧目标填充 Scene 背景色的耗时。"],
|
||||
["pipeline_2d_cache_targets_ms", "2D 缓存目标准备", "检查缓存组并清空本帧需要重建的 Renderable 缓存目标。"],
|
||||
["pipeline_2d_taskflow_ms", "2D Taskflow 墙钟", "从提交 2D Paint Taskflow 到同步完成的墙钟时间,包含节点执行和真实 Executor 排队。"],
|
||||
["pipeline_2d_paint_coordination_ms", "2D Paint 阶段衔接", "Paint 总区间中不属于帧目标、背景、缓存目标和 Taskflow 的轻量衔接。"],
|
||||
["pipeline_2d_scene_coordination_ms", "2D Scene 编排", "Scene 渲染区间内除事件、Prepare、Paint 外的依赖图编排耗时。"],
|
||||
["pipeline_2d_callback_ms", "2D 完成帧提取", "同步二维帧完成回调中取得连续 RGBA8 像素并建立不可变完成帧的耗时。"],
|
||||
["pipeline_2d_frame_handoff_ms", "2D 帧建立与完成发布", "Render_Frame 建立、Scene 入口以及完成帧交给页面图集之间尚未由独立 marker 覆盖的衔接耗时。"]
|
||||
];
|
||||
const pipeline_3d_definitions: Pipeline_Stage_Definition[] = [
|
||||
["pipeline_3d_event_ms", "3D 事件入队", "Scene 将输入事件提交到本 Scene 独立 Datoviz 准备域的耗时。"],
|
||||
["pipeline_3d_prepare_ms", "3D Visual Prepare", "将 Visual items 转换为不可变 Prepared_Visual GPU 字段的耗时;数据变化时执行。"],
|
||||
["pipeline_3d_submit_graph_ms", "3D Submit 图", "Submit 依赖图把 Prepared_Visual 入队到异步后端的耗时。"],
|
||||
["pipeline_3d_scene_coordination_ms", "3D Scene 编排", "三维 Scene 同步阶段中除事件、Prepare、Submit 外的依赖图编排耗时。"],
|
||||
["pipeline_3d_prepare_queue_ms", "准备域排队", "Scene 发布不可变 Visual 快照后,等待本 Scene 独立 Datoviz CPU 准备线程的耗时。"],
|
||||
["pipeline_3d_backend_apply_ms", "Datoviz Apply", "把本帧 Visual 与 Scene 参数应用到 Datoviz 对象的 CPU 耗时。"],
|
||||
["pipeline_3d_backend_plan_ms", "Datoviz Plan", "Datoviz 生成本帧 GPU 命令计划的 CPU 耗时。"],
|
||||
["pipeline_3d_backend_execute_ms", "Datoviz Execute", "Datoviz 执行命令构建的 CPU 耗时。"],
|
||||
["pipeline_3d_backend_commands_ms", "准备域其余工作", "每 Scene 准备域中除 Apply、Plan、Execute 外的命令录制和交互处理耗时。"],
|
||||
["pipeline_3d_backend_queue_ms", "GPU 提交域排队", "命令已录制完成后,等待同 GPU 单线程 Queue Submit 域的耗时。"],
|
||||
["pipeline_3d_backend_submit_ms", "GPU Queue Submit", "共享提交域执行已经录制好的 Vulkan Queue Submit 的 CPU 耗时。"],
|
||||
["pipeline_3d_submit_handoff_ms", "提交衔接", "共享提交域接管命令到 fence 开始监视之间未落入 Queue Submit 测量的轻量衔接。"],
|
||||
["pipeline_3d_gpu_render_ms", "GPU Render", "GPU 执行渲染通道的设备时间。"],
|
||||
["pipeline_3d_gpu_transition_ms", "GPU 资源转换", "GPU 图像布局和资源状态转换的设备时间。"],
|
||||
["pipeline_3d_gpu_copy_ms", "GPU 回读复制", "GPU 将渲染结果复制到可回读资源的设备时间。"],
|
||||
["pipeline_3d_gpu_sync_ms", "GPU 同步等待", "GPU 提交到完成区间扣除已测设备阶段后的 fence/调度时间。"],
|
||||
["pipeline_3d_readback_ms", "3D CPU 回读", "GPU 完成后由 Datoviz 收集并复制 RGBA 像素的耗时。"],
|
||||
["pipeline_3d_callback_ms", "3D 完成帧提取", "异步后端完成回调中共享原生 RGBA8 回读所有权并建立不可变完成帧的耗时。"],
|
||||
["pipeline_3d_completion_handoff_ms", "3D 完成发布衔接", "GPU 回读、回调与完成帧交给页面图集之间尚未由 trace marker 单独覆盖的调度耗时。"]
|
||||
];
|
||||
const pipeline_definitions = (dimension: Plot["dimension"], delivery: Frame_Delivery) => {
|
||||
const core = (dimension === "2D" ? pipeline_2d_definitions : pipeline_3d_definitions).map(definition => {
|
||||
if (delivery === "gallery-video") return definition;
|
||||
const [key] = definition;
|
||||
if (key === "pipeline_3d_gpu_copy_ms") return [key, "GPU 回读复制(跳过)", "诊断帧不复制像素到回读资源;该项应接近零。"] as Pipeline_Stage_Definition;
|
||||
if (key === "pipeline_3d_readback_ms") return [key, "3D 完成收集", "收集 GPU 完成状态和时间戳、但不下载 RGBA 像素的耗时。"] as Pipeline_Stage_Definition;
|
||||
return definition;
|
||||
});
|
||||
return core;
|
||||
};
|
||||
|
||||
function diagnostic_value(value: number, key: string) {
|
||||
if (key === "payload_megabytes") return `${value.toFixed(2)} MiB`;
|
||||
return `${value.toFixed(3)} ms`;
|
||||
}
|
||||
|
||||
function Frame_Timeline_Chart({diagnostics, dimension, paused, on_context_menu}: {diagnostics: Frame_Diagnostics; dimension: Plot["dimension"]; paused: boolean; on_context_menu: (event: React.MouseEvent<HTMLDivElement>) => void}) {
|
||||
const host_ref = useRef<HTMLDivElement>(null);
|
||||
const chart_ref = useRef<EChartsType | null>(null);
|
||||
useEffect(() => {
|
||||
if (!host_ref.current) return;
|
||||
const chart = echarts.init(host_ref.current, "dark", {renderer: "canvas"});
|
||||
chart_ref.current = chart;
|
||||
const observer = new ResizeObserver(() => chart.resize());
|
||||
observer.observe(host_ref.current);
|
||||
return () => { observer.disconnect(); chart.dispose(); chart_ref.current = null; };
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const chart = chart_ref.current;
|
||||
if (!chart) return;
|
||||
const visible_samples = diagnostics.samples;
|
||||
const total_series: [string, string, string] =
|
||||
["server_completion_ms", "服务端完成流水线", "#5ce4c2"];
|
||||
const pixel_delivery = diagnostics.server.delivery === "gallery-video";
|
||||
const series_keys: Array<[string, string, string]> = dimension === "2D" ? [
|
||||
total_series,
|
||||
["pipeline_2d_prepare_ms", "2D Prepare", "#62a8ff"],
|
||||
["pipeline_2d_frame_target_ms", "帧目标准备", "#79d5ff"],
|
||||
["pipeline_2d_cache_targets_ms", "缓存目标准备", "#f4bd63"],
|
||||
["pipeline_2d_taskflow_ms", "Paint Taskflow", "#ef9f55"],
|
||||
["pipeline_2d_scene_coordination_ms", "Scene 编排", "#ff7d9c"],
|
||||
["pipeline_2d_frame_handoff_ms", "完成发布衔接", "#b998ff"]
|
||||
] : [
|
||||
total_series,
|
||||
["pipeline_3d_prepare_ms", "Visual Prepare", "#62a8ff"],
|
||||
["pipeline_3d_prepare_queue_ms", "准备域排队", "#f4bd63"],
|
||||
["pipeline_3d_backend_queue_ms", "GPU 提交排队", "#ef9f55"],
|
||||
["pipeline_3d_gpu_render_ms", "GPU Render", "#ff7d9c"],
|
||||
pixel_delivery ? ["pipeline_3d_readback_ms", "CPU 回读", "#b998ff"] : ["pipeline_3d_gpu_sync_ms", "GPU 同步", "#b998ff"]
|
||||
];
|
||||
chart.setOption({
|
||||
backgroundColor: "transparent",
|
||||
animation: false,
|
||||
color: series_keys.map(([, , color]) => color),
|
||||
tooltip: {trigger: "axis", valueFormatter: (value: unknown) => `${Number(value).toFixed(3)} ms`},
|
||||
legend: {top: 0, textStyle: {color: "#91a5c0", fontSize: 10}},
|
||||
grid: {left: 45, right: 12, top: 34, bottom: paused ? 54 : 28},
|
||||
dataZoom: paused ? [{type: "inside", start: 70, end: 100}, {type: "slider", height: 18, bottom: 5, start: 70, end: 100}] : [],
|
||||
xAxis: {type: "category", name: "帧", data: visible_samples.map(sample => sample.sequence), axisLabel: {color: "#71839e", hideOverlap: true}},
|
||||
yAxis: {type: "value", name: "ms", min: 0, axisLabel: {color: "#71839e"}, splitLine: {lineStyle: {color: "#1d304a"}}},
|
||||
series: series_keys.map(([key, name]) => ({name, type: "line", showSymbol: false, connectNulls: false,
|
||||
data: visible_samples.map(sample => Number.isFinite(sample.values[key]) ? sample.values[key] : null), lineStyle: {width: 1.5}}))
|
||||
}, true);
|
||||
}, [diagnostics, dimension, paused]);
|
||||
return <div className="frameChartHost" onContextMenu={on_context_menu} title="右键暂停实时视图并缩放查看历史样本">
|
||||
{paused ? <span className="chartPausedBadge">已暂停视图 · 采样仍在继续</span> : null}
|
||||
<div className="frameTimelineChart" ref={host_ref} role="img" aria-label="帧流水线耗时波形图"/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Frame_Diagnostics_View({diagnostics, dimension, on_reset}: {diagnostics: Frame_Diagnostics | null; dimension: Plot["dimension"]; on_reset: () => void}) {
|
||||
const [copied, set_copied] = useState(false);
|
||||
const [stage_statistic_mode, set_stage_statistic_mode] = useState<Stage_Statistic>("average");
|
||||
const [stage_unit, set_stage_unit] = useState<Stage_Unit>("value");
|
||||
const [paused, set_paused] = useState(false);
|
||||
const [snapshot, set_snapshot] = useState<Frame_Diagnostics | null>(null);
|
||||
const [context_menu, set_context_menu] = useState<{x: number; y: number} | null>(null);
|
||||
useEffect(() => {
|
||||
const close = () => set_context_menu(null);
|
||||
window.addEventListener("pointerdown", close);
|
||||
return () => window.removeEventListener("pointerdown", close);
|
||||
}, []);
|
||||
const displayed = paused ? snapshot : diagnostics;
|
||||
if (!displayed) return <section className="diagnosticEmpty"><strong>等待流水线样本</strong><span>采样独立于图形面板可见性;收到第一帧后开始统计。</span></section>;
|
||||
const copy = async () => { await navigator.clipboard.writeText(JSON.stringify(displayed.server, null, 2)); set_copied(true); window.setTimeout(() => set_copied(false), 1200); };
|
||||
const definitions = pipeline_definitions(dimension, displayed.server.delivery);
|
||||
const stage_values = definitions.map(([key, label, description]) => {
|
||||
const statistic = displayed.server.frame_statistics[key];
|
||||
return [key, label, description, statistic?.[stage_statistic_mode] ?? Number.NaN] as const;
|
||||
});
|
||||
const total_value = stage_values.reduce((sum, [, , , value]) => sum + (Number.isFinite(value) ? value : 0), 0);
|
||||
const statistic_labels: Record<Stage_Statistic, string> = {average: "滑动平均", variability: "波动", p95: "P95", p99: "P99"};
|
||||
const pixel_delivery = displayed.server.delivery === "gallery-video";
|
||||
const completion = "完成帧发布给页面图集";
|
||||
const server_completion = displayed.server.frame_statistics.server_completion_ms;
|
||||
const frame_interval = displayed.server.frame_statistics.frame_interval_ms;
|
||||
const input = displayed.server.input_statistics;
|
||||
const browser_input = displayed.browser_input_statistics;
|
||||
const event_labels: Record<string, string> = {
|
||||
pointer_press: "指针按下", pointer_move: "指针拖动", pointer_release: "指针释放",
|
||||
wheel: "滚轮", key_press: "按键", key_release: "松键",
|
||||
leave: "指针离开", resize: "尺寸变化", show: "显示", hide: "隐藏"
|
||||
};
|
||||
const event_latency_rows = Object.entries(input)
|
||||
.filter(([, statistics]) => statistics.total_ms?.count)
|
||||
.map(([type, statistics]) => ({type, label: event_labels[type] ?? type,
|
||||
queue: statistics.queue_wait_ms, dispatch: statistics.dispatch_ms,
|
||||
total: statistics.total_ms}));
|
||||
const browser_event_latency_rows = Object.entries(browser_input)
|
||||
.filter(([, statistic]) => statistic.count)
|
||||
.map(([type, statistic]) => ({type, label: event_labels[type] ?? type,
|
||||
statistic}));
|
||||
const poll_intervals = displayed.samples.slice(1).map((sample, index) =>
|
||||
sample.received_at_ms - displayed.samples[index].received_at_ms);
|
||||
const summaries: Array<[string, string, string]> = [
|
||||
["逻辑完成闭环率", `${displayed.server.frame_rate_fps.toFixed(1)} FPS`, "由服务端滑动帧间隔的 5% 裁剪平均计算;诊断请求频率不会改变该结果。"],
|
||||
["诊断请求间隔", `${average(poll_intervals).toFixed(0)} ms`, "浏览器独立 GET 服务端统计快照的实际平均间隔;默认一秒一次。"],
|
||||
["浏览器解码帧率", `${displayed.video_playback.frame_rate_fps.toFixed(1)} FPS`, "由共享 WebRTC receiver 的 framesDecoded 增量统计;它不受显示器刷新率限制,可直接判断图集视频是否稳定收到一百帧。"],
|
||||
["浏览器累计解码", displayed.video_playback.presented_frames.toLocaleString("zh-CN"), `共享 WebRTC receiver 已解码帧数;readyState=${displayed.video_playback.ready_state}。`],
|
||||
["浏览器累计丢帧", displayed.video_playback.dropped_frames.toLocaleString("zh-CN"), "浏览器 inbound-rtp 统计报告的累计丢弃视频帧。"],
|
||||
["WebRTC 抖动缓冲", `${displayed.video_playback.jitter_buffer_ms.toFixed(2)} ms`, "浏览器 inbound-rtp 的累计 jitterBufferDelay / jitterBufferEmittedCount;这是服务端消费后到浏览器可播放之间的重要延迟。"],
|
||||
["浏览器解码处理", `${displayed.video_playback.decode_processing_ms.toFixed(2)} ms`, "浏览器 inbound-rtp 的累计 totalProcessingDelay / framesDecoded。"],
|
||||
["预计呈现等待", `${displayed.video_playback.estimated_playout_delay_ms.toFixed(2)} ms`, "浏览器估计的当前 RTP 帧距实际播放时刻的等待;浏览器不提供该字段时为 0。"],
|
||||
["浏览器冻结次数", displayed.video_playback.freeze_count.toLocaleString("zh-CN"), "浏览器 inbound-rtp 报告的累计视频冻结次数。"],
|
||||
["服务端完成平均", `${(server_completion?.average ?? 0).toFixed(2)} ms`, `服务端滑动窗口中从创建帧到${completion}的平均耗时。`],
|
||||
["服务端完成裁剪平均", `${(server_completion?.trimmed_average ?? 0).toFixed(2)} ms`, "服务端舍弃窗口两端各 5% 极端样本后的平均耗时。"],
|
||||
["服务端完成 P50", `${(server_completion?.p50 ?? 0).toFixed(2)} ms`, "一半样本不超过该服务端完成耗时。"],
|
||||
["服务端完成 P95", `${(server_completion?.p95 ?? 0).toFixed(2)} ms`, "95% 样本不超过该服务端完成耗时,用于观察长尾。"],
|
||||
["服务端完成 P99", `${(server_completion?.p99 ?? 0).toFixed(2)} ms`, "99% 样本不超过该服务端完成耗时,用于观察极端长尾。"],
|
||||
["帧间隔平均", `${(frame_interval?.average ?? 0).toFixed(2)} ms`, `服务端相邻两次${completion}的滑动平均间隔。`],
|
||||
["帧间隔波动", `${(frame_interval?.variability ?? 0).toFixed(2)} ms`, "服务端帧间隔窗口的总体标准差。"],
|
||||
["拖动事件总延迟 P95", `${(input.pointer_move?.total_ms?.p95 ?? 0).toFixed(2)} ms`, "pointer_move 进入 Scene 到 Renderable 完成消费的 P95;不等待编码或画面显示。"],
|
||||
["拖动事件等帧 P95", `${(input.pointer_move?.queue_wait_ms?.p95 ?? 0).toFixed(2)} ms`, "pointer_move 等待下一次 Scene Prepare 消费的 P95。"],
|
||||
["逻辑序列缺口", displayed.server.dropped_sequence_count.toLocaleString("zh-CN"), "服务端滑动统计期间未完成的逻辑帧序号数量。"]
|
||||
];
|
||||
const open_context_menu = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
set_context_menu({x: event.clientX, y: event.clientY});
|
||||
};
|
||||
const toggle_pause = () => {
|
||||
if (paused) { set_paused(false); set_snapshot(null); }
|
||||
else { set_snapshot(diagnostics); set_paused(true); }
|
||||
set_context_menu(null);
|
||||
};
|
||||
const reset = () => { set_paused(false); set_snapshot(null); set_context_menu(null); on_reset(); };
|
||||
return <section className="frameDiagnosticPanel">
|
||||
<div className="diagnosticNotice" title="暂停只冻结分析视图,不会停止服务端帧时钟与样本采集。">当前为{pixel_delivery ? "WebRTC H.264 视频" : "后台诊断"}模式,浏览器保留 {displayed.samples.length} 个低频快照点;服务端窗口含 {server_completion?.count ?? 0} 个真实帧样本。{pixel_delivery ? "视频轨与按需诊断 GET 相互独立。" : "后台诊断仍执行真实渲染,但跳过 H.264 编码与视频发送。"}</div>
|
||||
<dl className="frameDiagnosticSummary">{summaries.map(([label, value, description]) => <div key={label} title={description}><dt>{label}</dt><dd>{value}</dd></div>)}</dl>
|
||||
<section className="frameStagePanel"><header><div><strong>输入事件分类延迟</strong><code>服务端滑动统计</code></div></header>
|
||||
<dl>{event_latency_rows.length ? event_latency_rows.flatMap(row => [
|
||||
<div key={`${row.type}-queue`} title={`${row.type} 从进入 Scene 三缓冲到本帧开始分发。`}><dt>{row.label} · 等帧 P95</dt><dd>{(row.queue?.p95 ?? 0).toFixed(2)} ms</dd></div>,
|
||||
<div key={`${row.type}-dispatch`} title={`${row.type} 在 Renderable 接受链中的处理耗时。`}><dt>{row.label} · 分发 P95</dt><dd>{(row.dispatch?.p95 ?? 0).toFixed(2)} ms</dd></div>,
|
||||
<div key={`${row.type}-total`} title={`${row.type} 进入 Scene 到 Renderable 完成消费;不含视频呈现。`}><dt>{row.label} · 总计 P95</dt><dd>{(row.total?.p95 ?? 0).toFixed(2)} ms</dd></div>
|
||||
]) : <div><dt>等待输入</dt><dd>--</dd></div>}</dl></section>
|
||||
<section className="frameStagePanel"><header><div><strong>浏览器事件调度延迟</strong><code>按诊断周期读取</code></div></header>
|
||||
<dl>{browser_event_latency_rows.length ? browser_event_latency_rows.flatMap(row => [
|
||||
<div key={`${row.type}-browser-latest`} title="操作系统事件的 DOM timeStamp 到 JavaScript 监听器开始执行。"><dt>{row.label} · 最近</dt><dd>{row.statistic.latest_ms.toFixed(2)} ms</dd></div>,
|
||||
<div key={`${row.type}-browser-average`} title="本次统计周期以来浏览器主线程调度延迟的平均值。"><dt>{row.label} · 平均</dt><dd>{row.statistic.average_ms.toFixed(2)} ms</dd></div>,
|
||||
<div key={`${row.type}-browser-maximum`} title="该事件类型观察到的最大浏览器主线程调度延迟,可直接暴露一两秒的卡顿。"><dt>{row.label} · 最大</dt><dd>{row.statistic.maximum_ms.toFixed(2)} ms</dd></div>,
|
||||
<div key={`${row.type}-browser-delivery`} title="事件监听器尝试发送后,图形控制 WebSocket 处于 OPEN 与非 OPEN 状态的次数。"><dt>{row.label} · 已发/断连丢弃</dt><dd>{row.statistic.sent_count} / {row.statistic.disconnected_count}</dd></div>,
|
||||
<div key={`${row.type}-browser-buffered`} title="发送该事件时 WebSocket 曾观察到的最大待发送字节数。"><dt>{row.label} · WS 积压峰值</dt><dd>{row.statistic.websocket_buffered_bytes.toLocaleString("zh-CN")} B</dd></div>
|
||||
]) : <div><dt>等待输入</dt><dd>--</dd></div>}</dl></section>
|
||||
<Frame_Timeline_Chart diagnostics={displayed} dimension={dimension} paused={paused} on_context_menu={open_context_menu}/>
|
||||
<section className="frameStagePanel"><header><div><strong title="互斥阶段由服务端从同一帧时间线计算,各阶段占比之和约为 100%。">流水线阶段统计</strong><code>#{displayed.server.sequence} / 时钟 {displayed.server.correlation_id}</code></div>
|
||||
<div className="stageStatisticControls" aria-label="帧阶段统计显示方式">
|
||||
<div className="stageSegmented" role="group" aria-label="统计口径">{(["average", "variability", "p95", "p99"] as Stage_Statistic[]).map(mode =>
|
||||
<button key={mode} title={{average: "各帧该阶段耗时的算术平均。", variability: "各帧该阶段耗时的标准差。", p95: "95% 样本不超过此值。", p99: "99% 样本不超过此值。"}[mode]} aria-pressed={stage_statistic_mode === mode} className={stage_statistic_mode === mode ? "active" : ""} onClick={() => set_stage_statistic_mode(mode)}>{statistic_labels[mode]}</button>)}</div>
|
||||
<div className="stageSegmented" role="group" aria-label="显示单位">{(["value", "percentage"] as Stage_Unit[]).map(unit =>
|
||||
<button key={unit} title={unit === "value" ? "显示阶段耗时(毫秒)。" : "按当前统计口径归一化;所有互斥阶段合计约 100%。"} aria-pressed={stage_unit === unit} className={stage_unit === unit ? "active" : ""} onClick={() => set_stage_unit(unit)}>{unit === "value" ? "数值" : "百分比"}</button>)}</div>
|
||||
</div></header>
|
||||
<div className="pipelineDirection" aria-label={`${dimension} 帧流水线方向`}><strong>服务端帧时钟建帧</strong>{definitions.map(([key, label, description]) => <span key={key} title={description}><i>→</i>{label}</span>)}<span><i>→</i><strong>完成帧进入页面图集</strong></span></div>
|
||||
<dl>{stage_values.map(([key, label, description, value]) => <div key={key} title={description}><dt>{label}</dt><dd>{!Number.isFinite(value) ? "--"
|
||||
: stage_unit === "percentage" ? `${(total_value > 0 ? value / total_value * 100 : 0).toFixed(1)}%`
|
||||
: diagnostic_value(value, key)}</dd></div>)}</dl></section>
|
||||
<details className="rawState"><summary title="查看本次按需取得的服务端滑动统计快照。">服务端统计 JSON</summary><div className="jsonStateHeader"><span>协议 v{displayed.server.version}</span><button onClick={() => void copy()}>{copied ? "已复制" : "复制 JSON"}</button></div>
|
||||
<pre className="stateJson">{JSON.stringify(displayed.server, null, 2)}</pre></details>
|
||||
{context_menu ? <div className="diagnosticContextMenu" style={{left: context_menu.x, top: context_menu.y}} onPointerDown={event => event.stopPropagation()}>
|
||||
<button onClick={toggle_pause}>{paused ? "继续实时查看" : "暂停视图并详细查看"}</button>
|
||||
<button onClick={reset}>清空并从头统计</button>
|
||||
</div> : null}
|
||||
</section>;
|
||||
}
|
||||
|
||||
const camera_mode_descriptions: Record<string, {label: string; usage: string}> = {
|
||||
turntable: {label: "Turntable 环绕", usage: "左键环绕目标,右键或中键平移,滚轮改变观察距离。"},
|
||||
arcball: {label: "Arcball 自由旋转", usage: "拖动虚拟轨迹球进行无固定水平面的自由旋转,滚轮缩放。"},
|
||||
@@ -1253,17 +1020,42 @@ function Camera_Mode_Guide({fields}: {fields: Field[]}) {
|
||||
|
||||
function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise<void>}) {
|
||||
const {components, selected, set_active} = use_active_component(schema);
|
||||
const [mode, set_mode] = useState<"prop" | "state">("prop");
|
||||
const [state, set_state] = useState<Record<string, unknown> | null>(null);
|
||||
const [state_busy, set_state_busy] = useState(false);
|
||||
const [state_error, set_state_error] = useState("");
|
||||
const component = components.find(item => item.id === selected);
|
||||
const fields = component?.fields.filter(field => field.editable) ?? [];
|
||||
const count = components.reduce((sum, item) => sum + item.fields.filter(field => field.editable).length, 0);
|
||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="属性编辑" count={count} busy={busy} on_refresh={on_refresh}/>
|
||||
<Component_Tabs components={components} selected={selected} on_select={set_active}/><div className="workspaceBody">
|
||||
useEffect(() => { set_state(null); set_state_error(""); }, [plot.id, selected]);
|
||||
const request_state = async () => {
|
||||
if (!component) return;
|
||||
set_state_busy(true); set_state_error("");
|
||||
try {
|
||||
const request = await fetch(`/plot/${encodeURIComponent(plot.id)}/component/${encodeURIComponent(component.id)}/state`, {cache: "no-store"});
|
||||
const result = await request.json() as {state?: Record<string, unknown>; error?: string};
|
||||
if (!request.ok) throw new Error(result.error ?? "读取组件状态失败");
|
||||
set_state(result.state ?? {});
|
||||
} catch (failure) {
|
||||
set_state_error(failure instanceof Error ? failure.message : "读取组件状态失败");
|
||||
} finally { set_state_busy(false); }
|
||||
};
|
||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="组件属性与状态" count={count} busy={busy} on_refresh={on_refresh}/>
|
||||
<Component_Tabs components={components} selected={selected} on_select={set_active}/>
|
||||
<div className="componentInspectorMode" role="group" aria-label="组件数据视图">
|
||||
<button className={mode === "prop" ? "active" : ""} onClick={() => set_mode("prop")}>编辑属性</button>
|
||||
<button className={mode === "state" ? "active" : ""} onClick={() => set_mode("state")}>查看状态</button>
|
||||
</div><div className="workspaceBody">
|
||||
{busy && !schema ? <p className="muted">正在读取组件信息…</p> : component ? <section className="componentContent"><div className="sectionIntro">
|
||||
<strong>{component.label}</strong><span>{fields.length} 个可编辑属性</span>
|
||||
{component.kind === "camera" ? <button className="componentAction" onClick={() => window.dispatchEvent(new CustomEvent("aethera-reset-camera", {detail: {plot_id: plot.id}}))}>复位到初始视角</button> : null}</div>
|
||||
{component.kind === "camera" ? <Camera_Mode_Guide fields={fields}/> : null}
|
||||
<div className="propGrid">
|
||||
{fields.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(component, field, value)}/>)}</div></section> : null}</div></section>;
|
||||
<strong>{component.label}</strong><span>{mode === "prop" ? `${fields.length} 个可编辑属性` : "状态只在手动请求时采集"}</span>
|
||||
{mode === "prop" && component.kind === "camera" ? <button className="componentAction" onClick={() => window.dispatchEvent(new CustomEvent("aethera-reset-camera", {detail: {plot_id: plot.id}}))}>复位到初始视角</button> : null}
|
||||
{mode === "state" ? <button className="componentAction" disabled={state_busy} onClick={() => void request_state()}>{state_busy ? "读取中…" : "请求当前状态"}</button> : null}</div>
|
||||
{mode === "prop" ? <>{component.kind === "camera" ? <Camera_Mode_Guide fields={fields}/> : null}
|
||||
<div className="propGrid">{fields.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(component, field, value)}/>)}</div></>
|
||||
: <div className="componentStateView">{state_error ? <p className="taskflowError">{state_error}</p> : null}
|
||||
{state === null ? <p className="muted">尚未请求。普通 Schema 刷新不会采集状态。</p>
|
||||
: Object.keys(state).length ? <pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
: <p className="muted">该组件当前没有额外发布状态。</p>}</div>}</section> : null}</div></section>;
|
||||
}
|
||||
|
||||
function Data_Generator_View({plot, generator, on_generated}: {plot: Plot; generator: Data_Generator; on_generated: () => void}) {
|
||||
@@ -1317,11 +1109,6 @@ function Data_Generation_Pane({plot, analysis, busy, on_refresh, on_generated}:
|
||||
: <section className="analysisSection"><strong>当前图形没有原始数据生成能力</strong><p className="muted">此页只编辑当前选中图的后端 Schema 数据输入。</p></section>}</div></section>;
|
||||
}
|
||||
|
||||
function Frame_Statistics_Pane({plot, diagnostics, busy, on_refresh, on_reset}: {plot: Plot; diagnostics: Frame_Diagnostics | null; busy: boolean; on_refresh: () => void; on_reset: () => void}) {
|
||||
return <section className="workspacePane"><Workspace_Header plot={plot} label={`${plot.dimension} 帧流水线统计`} count={diagnostics?.samples.length ?? 0} busy={busy} on_refresh={on_refresh}/>
|
||||
<div className="workspaceBody"><Frame_Diagnostics_View key={plot.id} diagnostics={diagnostics} dimension={plot.dimension} on_reset={on_reset}/></div></section>;
|
||||
}
|
||||
|
||||
const taskflow_layout = new ELK();
|
||||
|
||||
function milliseconds(value: number) {
|
||||
@@ -1655,15 +1442,6 @@ const taskflow_graph_metric_definitions = [
|
||||
{key: "longest_unattributed_duration", label: "最长节点未归因墙时", unit: "milliseconds", description: "每帧单节点最大的未归因墙钟。"}
|
||||
] as const;
|
||||
type Taskflow_Graph_Metric_Key = typeof taskflow_graph_metric_definitions[number]["key"];
|
||||
const paint_metric_definitions = [
|
||||
{key: "total", label: "同帧 Paint 总墙钟"},
|
||||
{key: "frame_target", label: "帧目标准备"},
|
||||
{key: "background", label: "背景填充"},
|
||||
{key: "cache_targets", label: "缓存目标准备"},
|
||||
{key: "taskflow", label: "Paint Taskflow 墙钟"},
|
||||
{key: "coordination", label: "Paint 阶段衔接"}
|
||||
] as const;
|
||||
type Paint_Metric_Key = typeof paint_metric_definitions[number]["key"];
|
||||
type Taskflow_Node_Stability = "stable" | "variable" | "missing" | "long_tail";
|
||||
type Taskflow_Node_Aggregate = {
|
||||
node: Taskflow_Node_Trace; present: number; executed: number; frames: number;
|
||||
@@ -1674,7 +1452,6 @@ type Taskflow_Node_Aggregate = {
|
||||
type Taskflow_Graph_Aggregate = {
|
||||
key: string; graph: Taskflow_Graph_Trace; frames: number; samples: number; topology_variants: number;
|
||||
metrics: Record<Taskflow_Graph_Metric_Key, Distribution_Statistics>;
|
||||
paint_metrics: Record<Paint_Metric_Key, Distribution_Statistics> | null;
|
||||
completed_frames: number; cpu_time_coarse_frames: number;
|
||||
nodes: Map<string, Taskflow_Node_Aggregate>; edge_presence: Map<string, number>;
|
||||
};
|
||||
@@ -1699,36 +1476,6 @@ function taskflow_stage_key(graph: Taskflow_Graph_Trace) {
|
||||
return `${graph.stage}\u0000${graph.name}`;
|
||||
}
|
||||
|
||||
function frame_paint_analysis(frame?: Taskflow_Frame_Trace) {
|
||||
const markers = frame?.markers;
|
||||
if (!markers) return null;
|
||||
const interval = (first: string, last: string) =>
|
||||
Math.max(0, (markers[last] ?? 0) - (markers[first] ?? 0));
|
||||
const total = interval("paint_started", "paint_finished");
|
||||
const frame_target = interval("paint_frame_target_started", "paint_frame_target_finished");
|
||||
const background = interval("paint_background_started", "paint_background_finished");
|
||||
const cache_targets = interval("paint_cache_targets_started", "paint_cache_targets_finished");
|
||||
const taskflow = interval("paint_taskflow_started", "paint_taskflow_finished");
|
||||
const ranges = [
|
||||
[markers.paint_frame_target_started, markers.paint_frame_target_finished],
|
||||
[markers.paint_background_started, markers.paint_background_finished],
|
||||
[markers.paint_cache_targets_started, markers.paint_cache_targets_finished],
|
||||
[markers.paint_taskflow_started, markers.paint_taskflow_finished]
|
||||
].filter((range): range is [number, number] =>
|
||||
typeof range[0] === "number" && typeof range[1] === "number" && range[1] >= range[0])
|
||||
.sort((left, right) => left[0] - right[0]);
|
||||
let occupied = 0;
|
||||
let merged_start = ranges[0]?.[0] ?? 0;
|
||||
let merged_end = ranges[0]?.[1] ?? 0;
|
||||
for (const [start, end] of ranges.slice(1)) {
|
||||
if (start <= merged_end) merged_end = Math.max(merged_end, end);
|
||||
else { occupied += merged_end - merged_start; merged_start = start; merged_end = end; }
|
||||
}
|
||||
if (ranges.length) occupied += merged_end - merged_start;
|
||||
return {total, frame_target, background, cache_targets, taskflow,
|
||||
coordination: Math.max(0, total - occupied)};
|
||||
}
|
||||
|
||||
function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string): Taskflow_Graph_Aggregate | null {
|
||||
const samples = frames.flatMap(frame => frame.graphs
|
||||
.filter(graph => taskflow_stage_key(graph) === key)
|
||||
@@ -1742,8 +1489,6 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
|
||||
const topology_signatures = new Set<string>();
|
||||
const metric_samples = Object.fromEntries(taskflow_graph_metric_definitions.map(
|
||||
definition => [definition.key, [] as number[]])) as Record<Taskflow_Graph_Metric_Key, number[]>;
|
||||
const paint_metric_samples = Object.fromEntries(paint_metric_definitions.map(
|
||||
definition => [definition.key, [] as number[]])) as Record<Paint_Metric_Key, number[]>;
|
||||
let completed_frames = 0;
|
||||
let cpu_time_coarse_frames = 0;
|
||||
for (const {frame, graph} of samples) {
|
||||
@@ -1755,11 +1500,6 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
|
||||
metric_samples[definition.key].push(analysis[definition.key]);
|
||||
if (analysis.cpu_time_coarse) ++cpu_time_coarse_frames;
|
||||
if (graph.completed) ++completed_frames;
|
||||
if (graph.stage === "render_2d.frame") {
|
||||
const paint = frame_paint_analysis(frame);
|
||||
if (paint) for (const definition of paint_metric_definitions)
|
||||
paint_metric_samples[definition.key].push(paint[definition.key]);
|
||||
}
|
||||
for (const node of graph.nodes) {
|
||||
const current = accumulated.get(node.id) ?? {template: node, present: 0, executed: 0,
|
||||
duration: [], queue: [], cpu: [], cycles: [], cooperative_wait: [], cpu_time_coarse: false,
|
||||
@@ -1821,12 +1561,8 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
|
||||
const metrics = Object.fromEntries(taskflow_graph_metric_definitions.map(
|
||||
definition => [definition.key, distribution_statistics(metric_samples[definition.key])])) as
|
||||
Record<Taskflow_Graph_Metric_Key, Distribution_Statistics>;
|
||||
const paint_metrics = paint_metric_samples.total.length === 0 ? null :
|
||||
Object.fromEntries(paint_metric_definitions.map(definition => [definition.key,
|
||||
distribution_statistics(paint_metric_samples[definition.key])])) as
|
||||
Record<Paint_Metric_Key, Distribution_Statistics>;
|
||||
return {key, frames: frames.length, samples: samples.length, topology_variants: topology_signatures.size,
|
||||
metrics, paint_metrics, completed_frames, cpu_time_coarse_frames, nodes, edge_presence: edges,
|
||||
metrics, completed_frames, cpu_time_coarse_frames, nodes, edge_presence: edges,
|
||||
graph: {...first, submitted_ms: 0, finished_ms: metrics.wall_time.average, nodes: graph_nodes}};
|
||||
}
|
||||
|
||||
@@ -1848,12 +1584,9 @@ function taskflow_distribution_value(statistic: Distribution_Statistics,
|
||||
}
|
||||
|
||||
function taskflow_component_state(node: Taskflow_Node_Trace, components: Component[]) {
|
||||
const candidates = [node.attributes?.renderable, node.attributes?.owner, node.name.split(".")[0], node.name]
|
||||
.filter((value): value is string => Boolean(value)).map(value => value.toLowerCase());
|
||||
return components.find(component => {
|
||||
const identities = [component.id, component.label, component.kind].map(value => value.toLowerCase());
|
||||
return identities.some(identity => candidates.some(candidate => candidate === identity || candidate.includes(identity) || identity.includes(candidate)));
|
||||
})?.state;
|
||||
void components;
|
||||
if (!node.owner) return null;
|
||||
return {owner: node.owner, prop: node.prop ?? {}, state: node.state ?? {}};
|
||||
}
|
||||
|
||||
function taskflow_render_domain_state(node: Taskflow_Node_Trace, frame?: Taskflow_Frame_Trace) {
|
||||
@@ -2071,7 +1804,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
|
||||
graphs: frame?.graphs ?? [graph],
|
||||
aggregate: aggregate ? {frames: aggregate.frames, samples: aggregate.samples,
|
||||
topology_variants: aggregate.topology_variants,
|
||||
metrics: aggregate.metrics, paint_metrics: aggregate.paint_metrics,
|
||||
metrics: aggregate.metrics,
|
||||
completed_frames: aggregate.completed_frames,
|
||||
cpu_time_coarse_frames: aggregate.cpu_time_coarse_frames,
|
||||
nodes: Object.fromEntries(aggregate.nodes)} : undefined,
|
||||
@@ -2147,6 +1880,9 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
|
||||
const [copy_state, set_copy_state] = useState("复制时间线 JSON");
|
||||
const [line_height, set_line_height] = useState(52);
|
||||
const [visible_ranges, set_visible_ranges] = useState<Record<string, {start: number; end: number}>>({});
|
||||
const [type_visibility, set_type_visibility] = useState<Record<string, boolean>>({condition: false});
|
||||
const [hot_only, set_hot_only] = useState(false);
|
||||
const node_types = useMemo(() => [...new Set(graph.nodes.map(node => node.type.toLowerCase()))].sort(), [graph]);
|
||||
const fullscreen = controlled_fullscreen ?? local_fullscreen;
|
||||
const set_fullscreen = (value: boolean) => on_fullscreen_change ? on_fullscreen_change(value) : set_local_fullscreen(value);
|
||||
const origin = Date.UTC(2000, 0, 1);
|
||||
@@ -2154,8 +1890,13 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
|
||||
const model = useMemo(() => {
|
||||
const native_ids = new Set(graph.nodes.map(node => node.native_id));
|
||||
const node_by_native_id = new Map(graph.nodes.map(node => [node.native_id, node]));
|
||||
const rows = executions.filter(execution => native_ids.has(execution.native_id))
|
||||
.sort((left, right) => left.started_ms - right.started_ms || left.worker_id - right.worker_id);
|
||||
const candidate_rows = executions.filter(execution => native_ids.has(execution.native_id));
|
||||
const maximum_duration = Math.max(0, ...candidate_rows.map(execution => execution.duration_ms));
|
||||
const rows = candidate_rows.filter(execution => {
|
||||
const type = node_by_native_id.get(execution.native_id)?.type.toLowerCase() ?? "unknown";
|
||||
if (!(type_visibility[type] ?? type !== "condition")) return false;
|
||||
return !hot_only || execution.duration_ms >= maximum_duration * .1;
|
||||
}).sort((left, right) => left.started_ms - right.started_ms || left.worker_id - right.worker_id);
|
||||
const groups: Taskflow_Timeline_Group[] = [{
|
||||
id: "topology", title: <div className="taskflowTimelineGroupTitle"><strong>Topology</strong>
|
||||
<span>{graph.stage}</span></div>, node_name: graph.name, type: "topology"
|
||||
@@ -2195,29 +1936,37 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
|
||||
started_ms: execution.started_ms, type: node?.type,
|
||||
title: <div className="taskflowTimelineGroupTitle" title={node_name}>
|
||||
<strong>{taskflow_node_name(node_name)}</strong>
|
||||
{node?.owner ? <span>{node.owner.label} · {node.owner.component}</span> : null}
|
||||
<span>{diagnostic ? "Runtime" : `W${execution.worker_id}`} · 开始 +{execution.started_ms.toFixed(3)} ms · {node?.type ?? "task"}</span>
|
||||
</div>});
|
||||
const detail = diagnostic ? `${node_name} · Runtime tail` : `${node_name} · Worker ${execution.worker_id}`;
|
||||
const diagnostic_detail = detail + (node?.owner
|
||||
? `\nowner: ${node.owner.label} (${node.owner.component})\nprop: ${JSON.stringify(node.prop ?? {})}\nstate: ${JSON.stringify(node.state ?? {})}`
|
||||
: "");
|
||||
if (diagnostic) {
|
||||
add_item(`${group}-runtime`, group, "completion_tail", execution.started_ms,
|
||||
execution.finished_ms, `${taskflow_node_name(node_name)} ${milliseconds(execution.duration_ms)}`,
|
||||
node_name, detail);
|
||||
node_name, diagnostic_detail);
|
||||
return;
|
||||
}
|
||||
add_item(`${group}-queue`, group, "queue", execution.ready_ms, execution.entered_ms,
|
||||
`排队 ${milliseconds(execution.queue_wait_ms)}`, node_name, detail);
|
||||
`排队 ${milliseconds(execution.queue_wait_ms)}`, node_name, diagnostic_detail);
|
||||
add_item(`${group}-entry`, group, "observer_entry", execution.entered_ms, execution.started_ms,
|
||||
`entry ${milliseconds(execution.observer_entry_ms)}`, node_name, detail);
|
||||
`entry ${milliseconds(execution.observer_entry_ms)}`, node_name, diagnostic_detail);
|
||||
add_item(`${group}-body`, group, "body", execution.started_ms, execution.finished_ms,
|
||||
`执行 ${milliseconds(execution.duration_ms)}`, node_name, detail);
|
||||
`执行 ${milliseconds(execution.duration_ms)}`, node_name, diagnostic_detail);
|
||||
add_item(`${group}-exit`, group, "observer_exit", execution.finished_ms, execution.completed_ms,
|
||||
`exit ${milliseconds(execution.observer_exit_ms)}`, node_name, detail);
|
||||
`exit ${milliseconds(execution.observer_exit_ms)}`, node_name, diagnostic_detail);
|
||||
});
|
||||
const extent = Math.max(.1, graph.finished_ms,
|
||||
...rows.flatMap(row => [row.completed_ms, row.finished_ms]));
|
||||
const snapshots = [...new Map(rows.map(row => node_by_native_id.get(row.native_id))
|
||||
.filter((node): node is Taskflow_Node_Trace => Boolean(node?.owner))
|
||||
.map(node => [node.id, node])).values()];
|
||||
return {groups, items, end: origin + extent * 1.06 * scale,
|
||||
span: Math.max(.1, extent * 1.06) * scale, executions: rows.length, last_completed, last_task_completed};
|
||||
}, [graph, executions]);
|
||||
span: Math.max(.1, extent * 1.06) * scale, executions: rows.length,
|
||||
last_completed, last_task_completed, snapshots};
|
||||
}, [graph, executions, type_visibility, hot_only]);
|
||||
const timeline_view_key = `${graph.stage}:${graph.name}`;
|
||||
const visible_range = visible_ranges[timeline_view_key];
|
||||
const timeline_time_props = visible_range
|
||||
@@ -2266,6 +2015,17 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
|
||||
<span>零点为 Render_Frame 创建时刻 · {model.executions} 次执行</span>
|
||||
</div>
|
||||
</header>
|
||||
<div className="taskflowTimelineFilters" aria-label="时间线节点过滤">
|
||||
{node_types.map(type => <label key={type}><input type="checkbox"
|
||||
checked={type_visibility[type] ?? type !== "condition"}
|
||||
onChange={event => set_type_visibility(current => ({...current, [type]: event.target.checked}))}/>{type}</label>)}
|
||||
<label><input type="checkbox" checked={hot_only} onChange={event => set_hot_only(event.target.checked)}/>仅显示热点(≥ 最长任务 10%)</label>
|
||||
</div>
|
||||
{model.snapshots.length ? <details className="taskflowTimelineSnapshots"><summary>时间线节点属性 / 状态({model.snapshots.length})</summary>
|
||||
<div>{model.snapshots.map(node => <article key={node.id}><strong>{taskflow_node_name(node.name)}</strong>
|
||||
<span>{node.owner?.label} · {node.owner?.component}</span>
|
||||
<pre>{JSON.stringify({prop: node.prop ?? {}, state: node.state ?? {}}, null, 2)}</pre></article>)}</div>
|
||||
</details> : null}
|
||||
<div className="taskflowTimelineViewport">
|
||||
<Timeline<Taskflow_Timeline_Item, Taskflow_Timeline_Group>
|
||||
key={timeline_view_key} groups={model.groups} items={model.items} keys={taskflow_timeline_keys}
|
||||
@@ -2378,7 +2138,6 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
|
||||
? aggregate_taskflow_graph(response.frames, stage_key) : null, [response, stage_key]);
|
||||
const graph_analysis = useMemo(() => graph && frame
|
||||
? taskflow_graph_analysis(graph, frame.executions) : null, [graph, frame]);
|
||||
const paint_analysis = useMemo(() => frame_paint_analysis(frame), [frame]);
|
||||
const displayed_view_mode = fullscreen_view === "dag" ? "single" : fullscreen_view === "timeline" ? "timeline" : view_mode;
|
||||
const switch_fullscreen_view = (next: Taskflow_Fullscreen_View) => {
|
||||
set_view_mode(next === "dag" ? "single" : "timeline");
|
||||
@@ -2429,11 +2188,6 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
|
||||
return <div key={definition.key} title={`${definition.description} 样本 ${statistic.count};标准差 ${taskflow_metric_value(statistic.variability, definition.unit)};P95 ${taskflow_metric_value(statistic.p95, definition.unit)};P99 ${taskflow_metric_value(statistic.p99, definition.unit)}。`}>
|
||||
<dt>{definition.label}</dt><dd>{taskflow_distribution_value(statistic, definition.unit)}</dd></div>;
|
||||
})}
|
||||
{aggregate.paint_metrics ? paint_metric_definitions.map(definition => {
|
||||
const statistic = aggregate.paint_metrics![definition.key];
|
||||
return <div key={`paint-${definition.key}`} title={`2D Paint 单帧字段的跨帧统计;样本 ${statistic.count};标准差 ${milliseconds(statistic.variability)};P95 ${milliseconds(statistic.p95)};P99 ${milliseconds(statistic.p99)}。`}>
|
||||
<dt>{definition.label}</dt><dd>{taskflow_distribution_value(statistic, "milliseconds")}</dd></div>;
|
||||
}) : null}
|
||||
</dl><Taskflow_Dag graph={aggregate.graph} executions={[]} aggregate={aggregate} components={components} gallery_state={gallery_state}/></>
|
||||
: displayed_view_mode === "single" && graph && graph_analysis ? <><dl className="taskflowGraphSummary">
|
||||
<div><dt>业务阶段</dt><dd>{graph.stage}</dd></div><div><dt>Taskflow 节点</dt><dd>{graph_analysis.node_count}</dd></div>
|
||||
@@ -2466,14 +2220,6 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
|
||||
<div title="同一层节点没有相互依赖,可以并行;串行图的并行层数量为零。"><dt>依赖层 / 并行层</dt><dd>{graph_analysis.layer_count} / {graph_analysis.parallel_layer_count}</dd></div>
|
||||
<div title={graph_analysis.longest?.node_id}><dt>最长执行节点</dt><dd>{graph_analysis.longest ? milliseconds(graph_analysis.longest.duration_ms) : "--"}</dd></div>
|
||||
<div title={graph_analysis.longest_unattributed?.node_id}><dt>最长未归因墙时节点</dt><dd>{graph_analysis.longest_unattributed ? `${taskflow_node_name(graph_analysis.longest_unattributed.node_id.split("/").at(-1) ?? graph_analysis.longest_unattributed.node_id)} · ${graph_analysis.longest_unattributed.cpu_time_coarse ? "≈ " : ""}${milliseconds(Math.max(0, graph_analysis.longest_unattributed.duration_ms - graph_analysis.longest_unattributed.cooperative_wait_ms - graph_analysis.longest_unattributed.cpu_duration_ms))}` : "--"}</dd></div>
|
||||
{graph.stage === "render_2d.paint" && paint_analysis ? <>
|
||||
<div title="同一物理帧 paint_started 到 paint_finished 的完整区间。"><dt>同帧 Paint 总墙钟</dt><dd>{milliseconds(paint_analysis.total)}</dd></div>
|
||||
<div title="主帧 ensure_size 与完整像素清屏。"><dt>帧目标准备</dt><dd>{milliseconds(paint_analysis.frame_target)}</dd></div>
|
||||
<div title="主帧 Scene 背景色填充。"><dt>背景填充</dt><dd>{milliseconds(paint_analysis.background)}</dd></div>
|
||||
<div title="缓存组检查以及失效缓存目标的完整清屏。"><dt>缓存目标准备</dt><dd>{milliseconds(paint_analysis.cache_targets)}</dd></div>
|
||||
<div title="同一帧从 run_taskflow 提交到同步完成;应与 Topology 墙钟接近。"><dt>Taskflow marker 墙钟</dt><dd>{milliseconds(paint_analysis.taskflow)}</dd></div>
|
||||
<div title="Paint 总墙钟减去帧目标、背景、缓存目标与 Paint Taskflow 区间的并集;允许背景分支和缓存分支并行重叠。"><dt>Paint 阶段衔接</dt><dd>{milliseconds(paint_analysis.coordination)}</dd></div>
|
||||
</> : null}
|
||||
<div><dt>状态</dt><dd>{graph.completed ? "完成" : "未完成"}</dd></div>
|
||||
</dl><Taskflow_Dag graph={graph} executions={frame.executions} frame={frame} components={components} gallery_state={gallery_state}
|
||||
fullscreen={fullscreen_view === "dag"} on_fullscreen_change={value => set_fullscreen_view(value ? "dag" : null)}
|
||||
@@ -2689,7 +2435,7 @@ function Gallery_Grid({plots, selected, policies, layout_scope, galleries, on_po
|
||||
</Responsive> : null}</div>;
|
||||
}
|
||||
|
||||
const workspace_layout_key = "aethera-flexlayout-v6";
|
||||
const workspace_layout_key = "aethera-flexlayout-v7";
|
||||
const gallery_transport_key = "aethera-gallery-transport-v1";
|
||||
|
||||
function load_gallery_transport(): Gallery_Transport_Mode {
|
||||
@@ -2747,7 +2493,6 @@ const default_workspace_layout: IJsonModel = {
|
||||
]}
|
||||
]},
|
||||
{type: "tabset", id: "inspector-set", weight: 30, minWidth: 380, children: [
|
||||
{type: "tab", id: "frame-statistics-tab", name: "帧流水线统计", component: "frame-statistics", enableClose: false, enableScrollbars: false, minWidth: 360, minHeight: 260},
|
||||
{type: "tab", id: "taskflow-frame-tab", name: "Taskflow 帧分析", component: "taskflow-frame", enableClose: false, enableScrollbars: false, minWidth: 480, minHeight: 320}
|
||||
]}
|
||||
]}
|
||||
@@ -2767,7 +2512,6 @@ export function App() {
|
||||
const gallery_videos = use_gallery_videos(plots, gallery_transport);
|
||||
const [execution_policies, set_execution_policies] = useState<Plot_Execution_Policies>({});
|
||||
const [schema, set_schema] = useState<Schema | null>(null);
|
||||
const [frame_diagnostics, set_frame_diagnostics] = useState<Frame_Diagnostics | null>(null);
|
||||
const [schema_busy, set_schema_busy] = useState(false);
|
||||
const [layout_model, set_layout_model] = useState(load_workspace_model);
|
||||
const [gallery_layout_revision, set_gallery_layout_revision] = useState(0);
|
||||
@@ -2782,20 +2526,6 @@ export function App() {
|
||||
useEffect(() => {
|
||||
if (!selected && plots[0]) set_selected(plots[0]);
|
||||
}, [plots, selected?.id]);
|
||||
useEffect(() => {
|
||||
set_frame_diagnostics(null);
|
||||
const receive = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{plot_id: string; diagnostics: Frame_Diagnostics}>).detail;
|
||||
if (detail?.plot_id === selected?.id) set_frame_diagnostics(detail.diagnostics);
|
||||
};
|
||||
const clear = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{plot_id: string}>).detail;
|
||||
if (detail?.plot_id === selected?.id) set_frame_diagnostics(null);
|
||||
};
|
||||
window.addEventListener("aethera-frame-diagnostics", receive);
|
||||
window.addEventListener("aethera-frame-diagnostics-cleared", clear);
|
||||
return () => { window.removeEventListener("aethera-frame-diagnostics", receive); window.removeEventListener("aethera-frame-diagnostics-cleared", clear); };
|
||||
}, [selected?.id]);
|
||||
const load_schema = useCallback(async (show_busy: boolean) => {
|
||||
if (!selected) return;
|
||||
const request = ++schema_request.current;
|
||||
@@ -2851,7 +2581,6 @@ export function App() {
|
||||
window.dispatchEvent(new CustomEvent("aethera-reset-frame-diagnostics", {detail: {plot_id: selected.id}}));
|
||||
window.dispatchEvent(new CustomEvent("aethera-manual-frame", {detail: {plot_id: selected.id}}));
|
||||
}}/></aside>;
|
||||
if (node.getComponent() === "frame-statistics") return <aside className="inspector" aria-label="帧流水线统计"><Frame_Statistics_Pane plot={selected} diagnostics={frame_diagnostics} busy={schema_busy} on_refresh={() => void load_schema(true)} on_reset={reset_frame_diagnostics}/></aside>;
|
||||
if (node.getComponent() === "taskflow-frame") return <aside className="inspector" aria-label="Taskflow 帧分析"><Taskflow_Frame_Pane plot={selected} components={schema?.components ?? []}/></aside>;
|
||||
return <div className="emptyPane">未知工作区面板。</div>;
|
||||
};
|
||||
|
||||
@@ -101,6 +101,10 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
|
||||
.componentTabs { display: flex; flex: 0 0 auto; flex-wrap: nowrap; gap: 6px; overflow-x: auto; padding: 9px 12px; border-bottom: 1px solid #20314b; background: #091321; }
|
||||
.componentTabs button { flex: 0 0 auto; padding: 8px 11px; color: #8fa2bd; border: 1px solid #273b59; border-radius: 8px; background: #0d192a; cursor: pointer; }
|
||||
.componentTabs button:hover, .componentTabs button.active { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; }
|
||||
.componentInspectorMode { display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid #20314b; background: #0b1524; }
|
||||
.componentInspectorMode button { padding: 7px 11px; color: #91a5c0; border: 1px solid #304664; border-radius: 7px; background: #101c2d; cursor: pointer; }
|
||||
.componentInspectorMode button.active { color: #062019; border-color: #5ce4c2; background: #5ce4c2; }
|
||||
.componentStateView pre { overflow: auto; margin: 0; padding: 12px; color: #c9d8eb; border: 1px solid #263d59; border-radius: 8px; background: #08111e; font: 11px/1.55 ui-monospace, monospace; }
|
||||
.componentContent { min-width: 0; }
|
||||
.sectionIntro, .stateToolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; color: #7f93ae; }
|
||||
.sectionIntro strong { color: #dce8f8; font-size: 16px; }
|
||||
@@ -153,34 +157,7 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
|
||||
.generatorActions button:disabled { opacity: .45; cursor: default; }
|
||||
.analysisStatus { margin: 10px 0 0; color: #8eb6aa; font-size: 11px; }
|
||||
|
||||
.frameDiagnosticPanel { display: grid; gap: 14px; min-width: 0; }
|
||||
.diagnosticNotice { padding: 10px 12px; color: #8eabca; border: 1px solid #29435e; border-radius: 9px; background: #0c1a2a; font-size: 11px; line-height: 1.55; }
|
||||
.frameDiagnosticSummary { display: grid; grid-template-columns: repeat(auto-fit, minmax(122px, 1fr)); gap: 8px; margin: 0; }
|
||||
.frameDiagnosticSummary > div { min-width: 0; padding: 10px; border: 1px solid #213653; border-radius: 9px; background: #0a1422; }
|
||||
.frameDiagnosticSummary dt { color: #71839e; font-size: 10px; }
|
||||
.frameDiagnosticSummary dd { margin: 5px 0 0; color: #5ce4c2; font: 700 14px/1.2 ui-monospace, monospace; font-variant-numeric: tabular-nums; }
|
||||
.frameChartHost { position: relative; min-width: 0; }
|
||||
.frameTimelineChart { width: 100%; height: 300px; min-height: 240px; border: 1px solid #213653; border-radius: 10px; background: #08111e; }
|
||||
.chartPausedBadge { position: absolute; z-index: 2; top: 8px; right: 10px; padding: 4px 8px; color: #161008; border-radius: 99px; background: #f4bd63; font-size: 10px; pointer-events: none; }
|
||||
.diagnosticContextMenu { position: fixed; z-index: 10000; display: grid; min-width: 190px; overflow: hidden; padding: 5px; border: 1px solid #3c5779; border-radius: 9px; background: #101d2f; box-shadow: 0 14px 36px #000a; }
|
||||
.diagnosticContextMenu button { padding: 9px 11px; color: #cfdded; border: 0; border-radius: 6px; background: transparent; text-align: left; cursor: pointer; }
|
||||
.diagnosticContextMenu button:hover { color: #06110f; background: #5ce4c2; }
|
||||
.frameStagePanel { overflow: hidden; border: 1px solid #213653; border-radius: 10px; background: #0a1422; }
|
||||
.frameStagePanel > header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 11px 13px; border-bottom: 1px solid #1d304a; background: #101d2f; }
|
||||
.frameStagePanel > header code { color: #71839e; font: 10px/1 ui-monospace, monospace; }
|
||||
.frameStagePanel > header > div:first-child { display: flex; flex-direction: column; gap: 5px; }
|
||||
.pipelineDirection { display: flex; align-items: stretch; gap: 0; overflow-x: auto; padding: 10px 12px; color: #8fa2bd; border-bottom: 1px solid #1d304a; background: #091321; font-size: 10px; white-space: nowrap; }
|
||||
.pipelineDirection > strong, .pipelineDirection > span { display: inline-flex; align-items: center; }
|
||||
.pipelineDirection span i { margin: 0 7px; color: #5ce4c2; font-style: normal; }
|
||||
.stageStatisticControls { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; }
|
||||
.stageSegmented { display: inline-flex; padding: 2px; border: 1px solid #294463; border-radius: 7px; background: #091321; }
|
||||
.stageSegmented button { min-width: 42px; padding: 5px 8px; border: 0; border-radius: 5px; background: transparent; color: #8397b3; font-size: 10px; cursor: pointer; }
|
||||
.stageSegmented button:hover { color: #dce8f8; background: #152943; }
|
||||
.stageSegmented button.active { color: #06131d; background: #5ce4c2; }
|
||||
.frameStagePanel dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); margin: 0; }
|
||||
.frameStagePanel dl > div { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 12px; border-right: 1px solid #172942; border-bottom: 1px solid #172942; }
|
||||
.frameStagePanel dt { color: #91a5c0; font-size: 11px; }
|
||||
.frameStagePanel dd { min-width: 0; margin: 0; color: #dce8f8; font: 11px/1.25 ui-monospace, monospace; font-variant-numeric: tabular-nums; text-align: right; white-space: normal; overflow-wrap: anywhere; }
|
||||
.diagnosticEmpty { display: grid; min-height: 180px; place-content: center; gap: 8px; padding: 24px; color: #71839e; border: 1px dashed #29435e; border-radius: 10px; text-align: center; }
|
||||
.diagnosticEmpty strong { color: #cbd8ea; }
|
||||
|
||||
@@ -196,6 +173,16 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
|
||||
.taskflowViewMode button { padding: 7px 9px; color: #91a5c0; border: 0; border-right: 1px solid #304664; background: #101c2d; cursor: pointer; }
|
||||
.taskflowViewMode button:last-child { border-right: 0; }
|
||||
.taskflowViewMode button.active { color: #062019; background: #5ce4c2; }
|
||||
.taskflowTimelineFilters { display: flex; flex-wrap: wrap; gap: 8px 14px; padding: 9px 11px; color: #91a5c0; border: 1px solid #29435e; border-radius: 8px; background: #0c1a2a; font-size: 11px; }
|
||||
.taskflowTimelineFilters label { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.taskflowTimelineFilters input { accent-color: #5ce4c2; }
|
||||
.taskflowTimelineSnapshots { padding: 9px 11px; color: #91a5c0; border: 1px solid #29435e; border-radius: 8px; background: #091321; }
|
||||
.taskflowTimelineSnapshots > summary { cursor: pointer; }
|
||||
.taskflowTimelineSnapshots > div { display: grid; gap: 8px; margin-top: 9px; }
|
||||
.taskflowTimelineSnapshots article { display: grid; gap: 4px; padding: 8px; border: 1px solid #203651; border-radius: 7px; background: #08111e; }
|
||||
.taskflowTimelineSnapshots article strong { color: #dce8f8; }
|
||||
.taskflowTimelineSnapshots article span { color: #6f87a6; font-size: 10px; }
|
||||
.taskflowTimelineSnapshots pre { overflow: auto; margin: 3px 0 0; color: #b9cce3; font: 10px/1.45 ui-monospace, monospace; }
|
||||
.taskflowError { margin: 0; padding: 10px 12px; color: #ff9bae; border: 1px solid #71334a; border-radius: 8px; background: #27101a; }
|
||||
.taskflowGraphSummary, .taskflowRuntimeSummary { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; margin: 0; }
|
||||
.taskflowGraphSummary > div, .taskflowRuntimeSummary > div { min-width: 0; padding: 10px; border: 1px solid #213653; border-radius: 9px; background: #0a1422; }
|
||||
|
||||
Reference in New Issue
Block a user