diff --git a/kernel/src/kernel/scene.hpp b/kernel/src/kernel/scene.hpp index 00b8892..084f8f6 100644 --- a/kernel/src/kernel/scene.hpp +++ b/kernel/src/kernel/scene.hpp @@ -48,6 +48,8 @@ struct Scene : Def [[nodiscard]] std::shared_ptr make_event(Arguments&&... arguments); [[nodiscard]] Task_Graph& completion_taskflow(); +protected: + void observe_event(not_null event); }; } #include "scene.ipp" diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index b1bb9e2..73a9449 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -1,4 +1,5 @@ #pragma once +#include #include "Task_Graph_Internal.hpp" #include #include @@ -73,9 +74,12 @@ template std::shared_ptr Scene::make_event(Arguments&&... arguments) { auto event = std::allocate_shared( allocator(), std::forward(arguments)...); - event->observe_with(&static_cast(*d).event_statistics); + observe_event(not_null{event.get()}); return event; } +inline void Scene::observe_event(not_null event) { + event->observe_with(&static_cast(*d).event_statistics); +} template void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable { process(object, nullptr, std::forward(callback)); diff --git a/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp b/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp index f7c4559..d066e2e 100644 --- a/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp +++ b/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp @@ -84,6 +84,14 @@ void cooperate_until(Predicate predicate) { static_cast(aethera::detail::run_taskflow(driver)); } +[[nodiscard]] aethera::Frame_Policy::State policy_state( + const aethera::Frame_Policy& policy) { + aethera::Frame_Policy::State result{}; + policy.access_state( + [&](const aethera::Frame_Policy::State& state) { result = state; }); + return result; +} + TEST(frame_policy_lifecycle, concrete_policy_types_expose_only_their_mode) { Policy_Probe manual_probe; Policy_Probe fixed_probe; @@ -116,7 +124,7 @@ TEST(frame_policy_lifecycle, manual_policy_owns_reuses_and_publishes_frame) { .issued_at = std::chrono::steady_clock::now(), .time_milliseconds = 1.0}); cooperate_until([&] { - return probe.published.load(std::memory_order_acquire) == 1; + return policy_state(*policy).observation.published_frame_count == 1; }); aethera::Frame_Policy_State state{}; @@ -144,21 +152,25 @@ TEST(frame_policy_lifecycle, EXPECT_EQ(policy->read_state(). observation.active_frame_count, 3u); - bool stopped{}; - policy->stop([&] { stopped = true; }); - EXPECT_FALSE(stopped); - EXPECT_EQ(policy->read_state().lifecycle, - aethera::Frame_Policy_Lifecycle::stopping); + std::atomic_bool stopped{}; + policy->stop([&] { stopped.store(true, std::memory_order_release); }); + cooperate_until([&] { + return policy_state(*policy).lifecycle == + aethera::Frame_Policy_Lifecycle::stopping; + }); + EXPECT_FALSE(stopped.load(std::memory_order_acquire)); for (std::size_t index = 0; index < 3; ++index) { auto& borrow = probe.scene.borrows[index]; ASSERT_NE(borrow.frame, nullptr); borrow.callbacks.completed(aethera::not_null{borrow.frame}); } - EXPECT_TRUE(stopped); - EXPECT_EQ(policy->read_state().lifecycle, - aethera::Frame_Policy_Lifecycle::stopped); - EXPECT_EQ(policy->read_state(). - observation.active_frame_count, 0u); + cooperate_until([&] { + return stopped.load(std::memory_order_acquire) && + policy_state(*policy).lifecycle == + aethera::Frame_Policy_Lifecycle::stopped; + }); + EXPECT_TRUE(stopped.load(std::memory_order_acquire)); + EXPECT_EQ(policy_state(*policy).observation.active_frame_count, 0u); EXPECT_EQ(probe.published.load(std::memory_order_acquire), 3u); } @@ -209,19 +221,21 @@ TEST(frame_policy_lifecycle, cooperate_until([&] { return probe.published.load(std::memory_order_acquire) == 2; }); - bool stopped{}; - policy->stop([&] { stopped = true; }); - EXPECT_TRUE(stopped); + std::atomic_bool stopped{}; + policy->stop([&] { stopped.store(true, std::memory_order_release); }); + cooperate_until([&] { return stopped.load(std::memory_order_acquire); }); + EXPECT_TRUE(stopped.load(std::memory_order_acquire)); } TEST(frame_policy_lifecycle, idle_stop_calls_completion_once) { Policy_Probe probe; auto policy = std::make_shared( probe.dependencies()); - std::size_t calls{}; - policy->stop([&] { ++calls; }); - EXPECT_EQ(calls, 1u); - EXPECT_EQ(policy->read_state().lifecycle, + std::atomic_size_t calls{}; + policy->stop([&] { calls.fetch_add(1, std::memory_order_release); }); + cooperate_until([&] { return calls.load(std::memory_order_acquire) == 1; }); + EXPECT_EQ(calls.load(std::memory_order_acquire), 1u); + EXPECT_EQ(policy_state(*policy).lifecycle, aethera::Frame_Policy_Lifecycle::stopped); } diff --git a/mcp/core/Control_Requests.hpp b/mcp/core/Control_Requests.hpp index b9f4ac9..ab9741b 100644 --- a/mcp/core/Control_Requests.hpp +++ b/mcp/core/Control_Requests.hpp @@ -1,6 +1,5 @@ #pragma once -#include "runtime/Plot.hpp" #include #include #include @@ -33,7 +32,7 @@ struct Generate_Data_Request { struct Plot_Input_Request { std::string plot; /* 接收输入的 Gallery Plot。 */ - web::Plot_Input_Event event; /* 带生产者时间的完整输入事件。 */ + nlohmann::json event; /* 仅在协议边界存在的具体事件 JSON。 */ }; struct Plot_Render_Request { diff --git a/mcp/core/Control_Service.cpp b/mcp/core/Control_Service.cpp index 7d24875..df32bd8 100644 --- a/mcp/core/Control_Service.cpp +++ b/mcp/core/Control_Service.cpp @@ -3,10 +3,24 @@ #include "Control_Requests.hpp" #include "Protocol_Type.hpp" #include "runtime/Gallery_Plots.hpp" +#include "runtime/Datoviz_Observation_Json.hpp" +#include "runtime/Input_Event.hpp" +#include "runtime/Taskflow_Trace_Json.hpp" +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include +#include +#include #include +#include #include namespace aethera::mcp { @@ -41,6 +55,149 @@ template } } +[[nodiscard]] std::string_view pacing_mode_name(Frame_Policy_Type type) { + const auto name = magic_enum::enum_name(type); + if (name.empty()) throw std::logic_error("unknown frame policy type"); + return name; +} + +[[nodiscard]] nlohmann::json frame_policy_schema( + const Frame_Policy& policy) { + const auto* fixed = dynamic_cast(&policy); + return { + {"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, + {"kind", "analysis"}, + {"fields", nlohmann::json::array({ + {{"key", "render_enabled"}, {"editor", "boolean"}, + {"editable", true}, + {"value", policy.get<&Frame_Policy::Prop::render_enabled>()}}, + {{"key", "pixel_delivery_enabled"}, {"editor", "boolean"}, + {"editable", true}, + {"value", policy.get<&Frame_Policy::Prop::pixel_delivery_enabled>()}}, + {{"key", "pacing_mode"}, {"editor", "select"}, + {"editable", true}, {"value", pacing_mode_name(policy.type())}, + {"options", nlohmann::json::array({ + {{"value", "manual"}, {"label", "手动渲染"}}, + {{"value", "fixed_rate"}, {"label", "固定帧率"}}, + {{"value", "maximum_rate"}, {"label", "最大吞吐"}}})}}, + {{"key", "fixed_rate_fps"}, {"editor", "number"}, + {"editable", true}, {"minimum", 0.1}, {"maximum", 100.0}, + {"value", fixed ? fixed->frame_rate() : 100.0}} + })} + }; +} + +[[nodiscard]] nlohmann::json frame_policy_state_json( + const Frame_Policy_State& state, const Frame_Policy& policy) { + const auto milliseconds = [](std::uint64_t value) { + return static_cast(value) / 1'000'000.0; + }; + const auto observed_ns = state.observed_until_ns > state.observation_started_ns + ? state.observed_until_ns - state.observation_started_ns : 0U; + const auto observed_seconds = static_cast(observed_ns) / 1e9; + const auto rate = [observed_seconds](std::uint64_t count) { + return observed_seconds == 0.0 ? 0.0 : + static_cast(count) / observed_seconds; + }; + const auto* fixed = dynamic_cast(&policy); + const double target = fixed ? fixed->frame_rate() : 0.0; + const auto completion_span = + state.last_completion_ns > state.first_completion_ns + ? state.last_completion_ns - state.first_completion_ns : 0U; + const double completed_fps = completion_span != 0 && + state.completed_frame_count > 1 + ? static_cast(state.completed_frame_count - 1) * 1e9 / + static_cast(completion_span) + : 0.0; + return { + {"generation", state.generation}, + {"configuration", { + {"mode", pacing_mode_name(policy.type())}, + {"render_enabled", policy.get<&Frame_Policy::Prop::render_enabled>()}, + {"pixel_delivery_enabled", policy.get<&Frame_Policy::Prop::pixel_delivery_enabled>()}, + {"fixed_rate_fps", target}}}, + {"observation", { + {"duration_ms", milliseconds(observed_ns)}, + {"request_count", state.request_count}, + {"submitted_frame_count", state.submitted_frame_count}, + {"completed_frame_count", state.completed_frame_count}, + {"active_frame_count", state.active_frame_count}}}, + {"lifecycle", { + {"state", magic_enum::enum_name( + policy.read_state().lifecycle)}, + {"active_frame_count", state.active_frame_count}, + {"published_frame_count", state.published_frame_count}, + {"publication_failed_count", state.publication_failed_count}}}, + {"throughput", { + {"request_rate_fps", rate(state.request_count)}, + {"submission_rate_fps", rate(state.submitted_frame_count)}, + {"completion_rate_fps", completed_fps}, + {"target_achievement_ratio", target == 0.0 ? 0.0 : completed_fps / target}, + {"latest_frame_interval_ms", milliseconds(state.latest_completion_interval_ns)}}}, + {"latency", { + {"latest_tick_queue_ms", milliseconds(state.latest_tick_queue_ns)}, + {"average_tick_queue_ms", milliseconds( + state.submitted_frame_count == 0 ? 0U : + state.tick_queue_total_ns / state.submitted_frame_count)}, + {"maximum_tick_queue_ms", milliseconds(state.maximum_tick_queue_ns)}, + {"latest_completion_ms", milliseconds(state.latest_completion_latency_ns)}, + {"average_completion_ms", milliseconds( + state.completed_frame_count == 0 ? 0U : + state.completion_latency_total_ns / + state.completed_frame_count)}, + {"maximum_completion_ms", milliseconds(state.maximum_completion_latency_ns)}}}, + {"requests", { + {"periodic", state.periodic_request_count}, + {"immediate", state.immediate_request_count}, + {"maximum_rate", state.maximum_rate_request_count}, + {"accepted", state.accepted_request_count}, + {"policy_rejected", state.policy_rejection_count}, + {"frame_slot_backpressure", state.frame_slot_backpressure_count}, + {"scene_rejected", state.scene_rejection_count}}}, + {"last_frame", { + {"sequence", state.last_frame_sequence}, + {"request_source", magic_enum::enum_name(state.last_request_source)}}} + }; +} + +void append_frame_statistics_json( + nlohmann::json& output, const Frame_Statistics_State& state) { + for (const auto statistic : magic_enum::enum_values()) { + if (statistic == Frame_Statistic::count) continue; + const auto& value = state.values[static_cast(statistic)]; + if (value.count == 0) continue; + output[magic_enum::enum_name(statistic)] = { + {"count", value.count}, {"latest", value.latest}, + {"minimum", value.minimum}, {"maximum", value.maximum}, + {"average", value.average}, + {"trimmed_average", value.trimmed_average}, + {"variability", value.variability}, {"p50", value.p50}, + {"p95", value.p95}, {"p99", value.p99}}; + } +} + +void append_event_statistics_json( + nlohmann::json& output, const Event_Statistics_State& state) { + for (const auto type : magic_enum::enum_values()) { + auto& event = output[magic_enum::enum_name(type)]; + const auto& values = state.values[static_cast(type)]; + for (const auto statistic : magic_enum::enum_values()) { + if (statistic == Event_Statistic::count) continue; + const auto& value = values[static_cast(statistic)]; + if (value.count == 0) continue; + event[magic_enum::enum_name(statistic)] = { + {"count", value.count}, {"latest", value.latest}, + {"minimum", value.minimum}, {"maximum", value.maximum}, + {"average", value.average}, + {"trimmed_average", value.trimmed_average}, + {"variability", value.variability}, {"p50", value.p50}, + {"p95", value.p95}, {"p99", value.p99}}; + } + if (event.empty()) + output.erase(std::string{magic_enum::enum_name(type)}); + } +} + [[nodiscard]] Tool_Call_Output list_plots( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto&) { @@ -51,27 +208,29 @@ template [[nodiscard]] Tool_Call_Output plot_schema( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - return Tool_Call_Output{Tool_Call_Result::ok, plot->schema(), {}}; + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + return Tool_Call_Output{Tool_Call_Result::ok, + service.plot_schema(request.plot), {}}; }); } [[nodiscard]] Tool_Call_Output plot_diagnostics( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - return Tool_Call_Output{Tool_Call_Result::ok, plot->diagnostics(), {}}; + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + return Tool_Call_Output{Tool_Call_Result::ok, + service.plot_diagnostics(request.plot), {}}; }); } [[nodiscard]] Tool_Call_Output reset_plot_diagnostics( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - plot->reset_diagnostics(); + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + service.reset_plot_diagnostics(request.plot); return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}}, {}}; }); } @@ -79,9 +238,10 @@ template [[nodiscard]] Tool_Call_Output component_state( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - auto result = plot->component_state(request.component); + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + auto result = service.plot_component_state( + request.plot, request.component); if (!result.value("success", true)) return Tool_Call_Output{Tool_Call_Result::rejected, std::move(result), "unknown component"}; return Tool_Call_Output{Tool_Call_Result::ok, std::move(result), {}}; @@ -91,10 +251,10 @@ template [[nodiscard]] Tool_Call_Output write_property( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - auto result = plot->write_prop( - request.component, request.property, request.value); + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + auto result = service.write_plot_property( + request.plot, request.component, request.property, request.value); const bool success = result.value("success", false); return Tool_Call_Output{success ? Tool_Call_Result::ok : Tool_Call_Result::rejected, std::move(result), success ? "" : "property write rejected"}; @@ -104,10 +264,10 @@ template [[nodiscard]] Tool_Call_Output generate_data( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; return Tool_Call_Output{Tool_Call_Result::ok, - plot->generate_data(request.input), {}}; + service.generate_plot_data(request.plot, request.input), {}}; }); } @@ -115,12 +275,13 @@ template Control_Service& service, const nlohmann::json& arguments) { return decode_and_call( arguments, [&service](auto request) { - const auto plot = service.find_plot(request.plot); - if (!plot) + if (!service.contains_plot(request.plot)) return Tool_Call_Output{ Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - const auto time_milliseconds = request.event.time_milliseconds; - plot->submit_input(std::move(request.event)); + const auto time_milliseconds = + request.event.at("time_milliseconds").get(); + service.submit_input( + request.plot, web::make_input_event(request.event)); return Tool_Call_Output{ Tool_Call_Result::ok, {{"accepted", true}, @@ -133,11 +294,10 @@ template Control_Service& service, const nlohmann::json& arguments) { return decode_and_call( arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) + if (!service.contains_plot(request.plot)) return Tool_Call_Output{ Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - plot->schedule_render(Frame_Request{ + service.request_frame(request.plot, Frame_Request{ .issued_at = std::chrono::steady_clock::now(), .time_milliseconds = request.time_milliseconds, .width = request.width, @@ -156,11 +316,11 @@ template Control_Service& service, const nlohmann::json& arguments) { return decode_and_call( arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) + if (!service.contains_plot(request.plot)) return Tool_Call_Output{ Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - plot->request_taskflow_trace(request.frame_count); + service.request_plot_taskflow_trace( + request.plot, request.frame_count); return Tool_Call_Output{ Tool_Call_Result::ok, {{"accepted", true}, {"plot", request.plot}, @@ -173,22 +333,24 @@ template Control_Service& service, const nlohmann::json& arguments) { return decode_and_call( arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) + if (!service.contains_plot(request.plot)) return Tool_Call_Output{ Tool_Call_Result::unknown_plot, {}, "unknown plot"}; return Tool_Call_Output{ - Tool_Call_Result::ok, plot->taskflow_trace(), {}}; + Tool_Call_Result::ok, + service.plot_taskflow_trace(request.plot), {}}; }); } [[nodiscard]] Tool_Call_Output begin_benchmark( Control_Service& service, const nlohmann::json& arguments) { return decode_and_call(arguments, [&service](const auto& request) { - const auto plot = service.find_plot(request.plot); - if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - plot->reset_diagnostics(); - plot->render_once(); + if (!service.contains_plot(request.plot)) + return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + service.reset_plot_diagnostics(request.plot); + service.request_frame(request.plot, Frame_Request{ + .issued_at = std::chrono::steady_clock::now(), + .source = Frame_Request_Source::immediate}); return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}, {"plot", request.plot}, {"status_tool", "aethera_benchmark_read"}}, {}}; @@ -296,21 +458,513 @@ constexpr std::array operations{ } -Control_Service::Control_Service() : d(std::make_unique()) {} -Control_Service::~Control_Service() = default; +Control_Service::Private::Entry::Entry( + std::string value_id, web::Gallery_Build build, + Gallery_Output value_output) + : components(std::move(build.first)), id(std::move(value_id)), + output(std::move(value_output)) { + runtime.first = std::move(build.second); +} -std::shared_ptr Control_Service::create() { - auto service = std::shared_ptr(new Control_Service); +Control_Service::Private::Entry::~Entry() = default; + +std::shared_ptr +Control_Service::Private::Entry::policy() const noexcept { + return runtime.second.load(std::memory_order_acquire); +} + +void Control_Service::Private::Entry::fail( + std::exception_ptr failure) noexcept { + try { + std::string description{"unknown Gallery runtime failure"}; + try { + if (failure) std::rethrow_exception(failure); + } + catch (const std::exception& error) { description = error.what(); } + catch (...) { description = "non-standard Gallery runtime failure"; } + terminal_failure.store( + std::make_shared(std::move(description)), + std::memory_order_release); + } + catch (...) {} +} + +std::shared_ptr +Control_Service::Private::Entry::create_policy( + Frame_Policy_Type type, double fixed_rate_fps) { + auto* scene = std::visit( + [](auto& value) -> Render_Frame_Scene* { return value.get(); }, + runtime.first); + const bool is_3d = std::holds_alternative< + std::unique_ptr>(runtime.first); + const auto weak = weak_from_this(); + Frame_Policy::Dependencies dependencies{ + .scene = not_null{scene}, + .create_frame = [is_3d]() -> std::unique_ptr { + if (is_3d) + return std::make_unique(Frame_Identity{}); + return std::make_unique(Frame_Identity{}); + }, + .prepare_frame = [weak](not_null frame, + const Frame_Production& production) { + if (const auto owner = weak.lock()) + owner->prepare_frame(frame, production); + }, + .publish_frame = [weak](not_null frame, + const Frame_Production& production) { + if (const auto owner = weak.lock()) + return owner->publish_frame(frame, production); + return Frame_Publication_Dispatch{}; + }, + .retain_owner = [weak]() -> std::shared_ptr { + return weak.lock(); + }, + .report_failure = [weak](std::exception_ptr failure) { + if (const auto owner = weak.lock()) + owner->fail(std::move(failure)); + }, + .frame_capacity = 3}; + switch (type) { + case Frame_Policy_Type::manual: + return std::make_shared(std::move(dependencies)); + case Frame_Policy_Type::fixed_rate: + return std::make_shared( + std::move(dependencies), fixed_rate_fps); + case Frame_Policy_Type::maximum_rate: + return std::make_shared( + std::move(dependencies)); + } + throw std::logic_error("unknown Frame Policy type"); +} + +void Control_Service::Private::Entry::start() { + auto next = create_policy(Frame_Policy_Type::fixed_rate, 100.0); + runtime.second.store(next, std::memory_order_release); + next->start(); +} + +void Control_Service::Private::Entry::stop() noexcept { + auto current = runtime.second.exchange({}, std::memory_order_acq_rel); + if (!current) return; + try { + current->stop([owner = shared_from_this(), current]() mutable { + current.reset(); + owner.reset(); + }); + } + catch (...) { fail(std::current_exception()); } +} + +void Control_Service::Private::Entry::replace_policy( + Frame_Policy_Type type) { + auto current = runtime.second.exchange({}, std::memory_order_acq_rel); + if (!current) throw std::logic_error("Frame Policy switch is active"); + const auto* fixed = dynamic_cast( + current.get()); + const auto fixed_rate_fps = fixed ? fixed->frame_rate() : 100.0; + current->stop([ + owner = shared_from_this(), current, type, fixed_rate_fps]() mutable { + try { + auto next = owner->create_policy(type, fixed_rate_fps); + owner->runtime.second.store(next, std::memory_order_release); + next->start(); + } + catch (...) { owner->fail(std::current_exception()); } + current.reset(); + }); +} + +void Control_Service::Private::Entry::prepare_frame( + not_null frame, const Frame_Production& production) { + auto request = production.request; + request.width = output.width; + request.height = output.height; + const Frame_Identity identity{ + production.sequence, + request.sequence == 0 ? production.sequence : request.sequence}; + if (auto* frame_2d = dynamic_cast(frame.get())) { + frame_2d->begin(identity, render_2d::Frame_2D::native_pixel_format, + request.source); + std::get>(runtime.first) + ->set<&render_2d::Render_Scene_2D::Prop::viewport>( + render_2d::Size{static_cast(request.width), + static_cast(request.height)}); + } + else if (auto* frame_3d = dynamic_cast(frame.get())) { + frame_3d->begin(identity, render_3d::Frame_3D_Output::pixels, + render_3d::Frame_3D::native_pixel_format, + request.source); + std::get>(runtime.first) + ->set<&render_3d::Render_Scene_3D::Prop::viewport>( + render_3d::Extent{request.width, request.height}); + } + else { + throw std::logic_error("Gallery policy created an unsupported Frame"); + } + auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire); + while (remaining != 0 && + !taskflow_trace_remaining.compare_exchange_weak( + remaining, remaining - 1, std::memory_order_acq_rel, + std::memory_order_acquire)) {} + if (remaining != 0) frame->request_taskflow_trace(); + frame->mark(Frame_Trace_Marker::plot_update_started); + const auto started = std::chrono::steady_clock::now(); + components->update(request); + frame->mark(Frame_Trace_Marker::plot_update_finished); + const auto elapsed = std::chrono::steady_clock::now() - started; + frame->record(Frame_Trace_Measurement::plot_update_ns, + static_cast(std::max(0, + std::chrono::duration_cast(elapsed) + .count()))); +} + +Frame_Publication_Dispatch +Control_Service::Private::Entry::publish_frame( + not_null frame, const Frame_Production& production) { + const auto current = policy(); + const bool deliver_pixels = current && current->get< + &Frame_Policy::Prop::pixel_delivery_enabled>(); + const auto identity = frame->identity(); + auto rendered = identity; + std::shared_ptr> storage; + std::optional datoviz_observation; + web::Gallery_Pixel_Layout layout{web::Gallery_Pixel_Layout::rgba8}; + std::uint32_t width{}, height{}; + if (auto* frame_2d = dynamic_cast(frame.get())) { + layout = web::Gallery_Pixel_Layout::bgra8; + const auto image = frame_2d->image(); + width = static_cast(std::max(0, image.width)); + height = static_cast(std::max(0, image.height)); + if (deliver_pixels) { + auto pixels = frame_2d->output_pixels(); + width = static_cast(pixels.width); + height = static_cast(pixels.height); + storage = std::make_shared>( + std::move(pixels.bytes)); + } + } + else if (auto* frame_3d = dynamic_cast(frame.get())) { + rendered = frame_3d->rendered_identity(); + datoviz_observation = frame_3d->take_datoviz_observation(); + const auto extent = frame_3d->extent(); + width = extent.width; + height = extent.height; + if (deliver_pixels) storage = frame_3d->share_pixels(); + } + const auto timestamp = std::chrono::duration_cast( + std::chrono::duration( + production.request.time_milliseconds)); + auto pixels = std::make_shared( + web::Gallery_Pixel_Frame{ + std::move(storage), layout, timestamp, identity.sequence, + identity.correlation_id, rendered.sequence, + rendered.correlation_id, width, height}); + auto publication = std::make_shared( + web::Gallery_Frame{std::move(pixels)}); + const auto started = std::chrono::steady_clock::now(); + auto result = web::Gallery_Frame_Publication::ignored; + if (output.publish) result = output.publish(id, std::move(publication)); + const auto completed = std::chrono::steady_clock::now(); + frame->record(Frame_Trace_Measurement::plot_publish_ns, + static_cast(std::max(0, + std::chrono::duration_cast( + completed - started).count()))); + if (frame->taskflow_trace_requested()) { + const auto trace_value = frame->take_taskflow_trace(); + std::unordered_set executed; + for (const auto& task : trace_value.tasks) + executed.insert(task.native_id); + std::vector component_ids; + for (const auto& graph : trace_value.graphs) + for (const auto& node : graph.nodes) { + if (!executed.contains(node.native_id)) continue; + const auto found = std::ranges::find( + node.attributes, "owner_component", + &std::pair::first); + if (found != node.attributes.end() && !found->second.empty() && + std::ranges::find(component_ids, found->second) == + component_ids.end()) + component_ids.push_back(found->second); + } + nlohmann::json captured_backend; + if (datoviz_observation) + captured_backend = datoviz_observation_json(*datoviz_observation); + const auto encoded = std::make_shared( + web::taskflow_trace_json(trace_value, + components->capture_components(component_ids), + captured_backend)); + auto state = taskflow_trace_control.load(std::memory_order_acquire); + for (;;) { + const auto requested = static_cast(state >> 32U); + const auto captured = static_cast(state); + if (captured >= requested) break; + taskflow_trace_slots[captured].store(encoded, + std::memory_order_release); + const auto next = (static_cast(requested) << 32U) | + static_cast(captured + 1U); + if (taskflow_trace_control.compare_exchange_weak( + state, next, std::memory_order_release, + std::memory_order_acquire)) + break; + } + } + return { + .started_at = started, + .completed_at = completed, + .asynchronous_feedback_count = + result == web::Gallery_Frame_Publication::asynchronous ? 1U : 0U, + .succeeded = result == web::Gallery_Frame_Publication::completed, + .pixel_width = width, + .pixel_height = height}; +} + +nlohmann::json Control_Service::Private::Entry::schema() const { + auto result = components->schema(); + if (const auto current = policy()) { + auto analysis = frame_policy_schema(*current); + const auto generator = components->data_generator_schema(); + if (!generator.is_null()) analysis["data_generator"] = generator; + result["frame_analysis"] = std::move(analysis); + } + return result; +} + +nlohmann::json Control_Service::Private::Entry::diagnostics() const { + const auto current = policy(); + if (!current) + return {{"protocol", "aethera.plot.diagnostics"}, {"version", 5}, + {"available", false}}; + Frame_Policy_State observation{}; + current->access_state( + [&](const Frame_Policy::State& state) { + observation = state.observation; + }); + nlohmann::json input_statistics = nlohmann::json::object(); + nlohmann::json frame_statistics = nlohmann::json::object(); + append_frame_statistics_json( + frame_statistics, observation.frame_statistics); + std::visit([&](const auto& scene) { + scene->template access_state( + [&](const Scene::State& state) { + append_event_statistics_json( + input_statistics, state.event_statistics); + }); + }, runtime.first); + nlohmann::json result{ + {"protocol", "aethera.plot.diagnostics"}, {"version", 5}, + {"available", true}, {"plot", id}, + {"dimension", std::holds_alternative< + std::unique_ptr>(runtime.first) + ? "3D" : "2D"}, + {"sequence", observation.last_frame_sequence}, + {"pixel", {{"width", observation.pixel_width}, + {"height", observation.pixel_height}}}, + {"frame_policy", frame_policy_state_json(observation, *current)}, + {"frame_lifecycle", magic_enum::enum_name( + current->read_state().lifecycle)}, + {"frame_statistics", std::move(frame_statistics)}, + {"input_statistics", std::move(input_statistics)}}; + if (const auto failure = terminal_failure.load(std::memory_order_acquire)) + result["terminal_failure"] = *failure; + return result; +} + +void Control_Service::Private::Entry::request_trace( + std::size_t frame_count) { + if (frame_count == 0 || frame_count > maximum_taskflow_trace_frames) + throw std::invalid_argument( + "Taskflow trace frame_count must be between 1 and 120"); + auto control = taskflow_trace_control.load(std::memory_order_acquire); + for (;;) { + const auto requested = static_cast(control >> 32U); + const auto captured = static_cast(control); + if (requested != captured) + throw std::logic_error("A Taskflow trace request is active"); + const auto next = static_cast(frame_count) << 32U; + if (taskflow_trace_control.compare_exchange_weak( + control, next, std::memory_order_release, + std::memory_order_acquire)) + break; + } + for (auto& slot : taskflow_trace_slots) + slot.store({}, std::memory_order_release); + taskflow_trace_remaining.store(frame_count, std::memory_order_release); +} + +nlohmann::json Control_Service::Private::Entry::trace() const { + nlohmann::json frames = nlohmann::json::array(); + const auto state = taskflow_trace_control.load(std::memory_order_acquire); + const auto requested = static_cast(state >> 32U); + const auto captured = static_cast(state); + for (std::uint32_t index = 0; index < captured; ++index) + if (const auto value = + taskflow_trace_slots[index].load(std::memory_order_acquire)) + frames.push_back(*value); + return {{"protocol", "aethera.taskflow.frames"}, {"version", 1}, + {"requested", requested}, + {"remaining", taskflow_trace_remaining.load( + std::memory_order_acquire)}, + {"captured", frames.size()}, + {"complete", requested != 0 && frames.size() == requested}, + {"frames", std::move(frames)}}; +} + +Control_Service::Control_Service(Gallery_Output output) + : d(std::make_unique()) { + d->output = std::move(output); +} + +Control_Service::~Control_Service() { + for (const auto& [id, entry] : d->plots) { + static_cast(id); + entry->stop(); + } +} + +std::shared_ptr Control_Service::create( + Gallery_Output output) { + auto service = std::shared_ptr( + new Control_Service(std::move(output))); service->d->plots.reserve(web::gallery_plot_definitions().size()); - for (const auto& definition : web::gallery_plot_definitions()) - service->d->plots.emplace(definition.id, definition.create()); + for (const auto& definition : web::gallery_plot_definitions()) { + auto entry = std::make_shared( + std::string{definition.id}, definition.create(), + service->d->output); + service->d->plots.emplace(entry->id, entry); + entry->start(); + } return service; } -std::shared_ptr Control_Service::find_plot( +bool Control_Service::contains_plot(std::string_view id) const noexcept { + return d->plots.contains(std::string{id}); +} + +void Control_Service::submit_input( + std::string_view id, Scene::Event_Pointer event) { + if (!event) throw std::invalid_argument("Gallery input event is null"); + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + std::visit([event = std::move(event)](auto& scene) mutable { + scene->submit_event(std::move(event)); + }, found->second->runtime.first); +} + +void Control_Service::request_frame( + std::string_view id, Frame_Request request) { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + const auto current = found->second->policy(); + if (!current) throw std::logic_error("Frame Policy switch is active"); + current->request_frame(std::move(request)); +} + +void Control_Service::submit_publication_feedback( + std::string_view id, Frame_Publication_Feedback feedback) noexcept { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) return; + if (const auto current = found->second->policy()) + current->submit_publication_feedback(std::move(feedback)); +} + +nlohmann::json Control_Service::plot_schema(std::string_view id) const { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + return found->second->schema(); +} + +nlohmann::json Control_Service::write_plot_property( + std::string_view id, std::string_view component, std::string_view key, + const nlohmann::json& value) { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + auto& entry = *found->second; + if (component != "frame-analysis") + return entry.components->write_prop(component, key, value); + if (key == "pacing_mode") { + if (!value.is_string()) + return {{"success", false}, + {"error", "pacing_mode requires a string"}}; + const auto parsed = magic_enum::enum_cast( + value.get_ref()); + if (!parsed) + return {{"success", false}, + {"error", "unknown frame pacing mode"}}; + entry.replace_policy(*parsed); + return {{"success", true}, {"component", component}, {"key", key}, + {"value", pacing_mode_name(*parsed)}}; + } + const auto current = entry.policy(); + if (!current) + return {{"success", false}, {"error", "Frame Policy switch is active"}}; + if (key == "render_enabled" || key == "pixel_delivery_enabled") { + if (!value.is_boolean()) + return {{"success", false}, {"error", "boolean required"}}; + const auto enabled = value.get(); + if (key == "render_enabled") + current->set<&Frame_Policy::Prop::render_enabled>(enabled); + else + current->set<&Frame_Policy::Prop::pixel_delivery_enabled>(enabled); + return {{"success", true}, {"component", component}, {"key", key}, + {"value", enabled}}; + } + if (key == "fixed_rate_fps") { + if (!value.is_number()) + return {{"success", false}, {"error", "number required"}}; + const auto rate = value.get(); + auto* fixed = dynamic_cast(current.get()); + if (!fixed || !std::isfinite(rate) || rate < 0.1 || rate > 100.0) + return {{"success", false}, {"error", "invalid fixed rate"}}; + fixed->set_frame_rate(rate); + return {{"success", true}, {"component", component}, {"key", key}, + {"value", rate}}; + } + return {{"success", false}, {"error", "unknown frame property"}}; +} + +nlohmann::json Control_Service::plot_component_state( + std::string_view id, std::string_view component) const { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + return found->second->components->component_state(component); +} + +nlohmann::json Control_Service::generate_plot_data( + std::string_view id, const nlohmann::json& input) { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + return found->second->components->generate_data(input); +} + +nlohmann::json Control_Service::plot_diagnostics(std::string_view id) const { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + return found->second->diagnostics(); +} + +void Control_Service::reset_plot_diagnostics(std::string_view id) { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + std::visit([](auto& scene) { + scene->template update_state<&Scene::State::event_statistics>( + Event_Statistics_State{}); + }, found->second->runtime.first); + if (const auto current = found->second->policy()) + current->reset_statistics(); +} + +void Control_Service::request_plot_taskflow_trace( + std::string_view id, std::size_t frame_count) { + const auto found = d->plots.find(std::string{id}); + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + found->second->request_trace(frame_count); +} + +nlohmann::json Control_Service::plot_taskflow_trace( std::string_view id) const { const auto found = d->plots.find(std::string{id}); - return found == d->plots.end() ? nullptr : found->second; + if (found == d->plots.end()) throw std::invalid_argument("unknown plot"); + return found->second->trace(); } nlohmann::json Control_Service::plot_catalog() const { diff --git a/mcp/core/Control_Service.hpp b/mcp/core/Control_Service.hpp index ccc1eab..0bd4d53 100644 --- a/mcp/core/Control_Service.hpp +++ b/mcp/core/Control_Service.hpp @@ -1,16 +1,20 @@ #pragma once +#include "runtime/Gallery_Runtime.hpp" +#include #include #include #include #include #include -namespace aethera::web { -struct Plot; -} - namespace aethera::mcp { +struct Gallery_Output { + std::uint32_t width{720}; + std::uint32_t height{420}; + web::Gallery_Frame_Publisher publish{}; +}; + enum struct Tool_Call_Result : std::uint8_t { ok, unknown_tool, @@ -26,12 +30,30 @@ struct Tool_Call_Output { }; struct Control_Service final { - static std::shared_ptr create(); + static std::shared_ptr create( + Gallery_Output output = {}); ~Control_Service(); Control_Service(const Control_Service&) = delete; Control_Service& operator=(const Control_Service&) = delete; - [[nodiscard]] std::shared_ptr find_plot( + [[nodiscard]] bool contains_plot(std::string_view id) const noexcept; + void submit_input(std::string_view id, Scene::Event_Pointer event); + void request_frame(std::string_view id, Frame_Request request); + void submit_publication_feedback( + std::string_view id, Frame_Publication_Feedback feedback) noexcept; + [[nodiscard]] nlohmann::json plot_schema(std::string_view id) const; + [[nodiscard]] nlohmann::json write_plot_property( + std::string_view id, std::string_view component, + std::string_view key, const nlohmann::json& value); + [[nodiscard]] nlohmann::json plot_component_state( + std::string_view id, std::string_view component) const; + [[nodiscard]] nlohmann::json generate_plot_data( + std::string_view id, const nlohmann::json& input); + [[nodiscard]] nlohmann::json plot_diagnostics(std::string_view id) const; + void reset_plot_diagnostics(std::string_view id); + void request_plot_taskflow_trace( + std::string_view id, std::size_t frame_count); + [[nodiscard]] nlohmann::json plot_taskflow_trace( std::string_view id) const; [[nodiscard]] nlohmann::json plot_catalog() const; [[nodiscard]] nlohmann::json tool_catalog() const; @@ -39,7 +61,7 @@ struct Control_Service final { std::string_view name, const nlohmann::json& arguments); private: - Control_Service(); + explicit Control_Service(Gallery_Output output); struct Private; std::unique_ptr d; }; diff --git a/mcp/core/Control_Service.ipp b/mcp/core/Control_Service.ipp index 85c8222..1839c5c 100644 --- a/mcp/core/Control_Service.ipp +++ b/mcp/core/Control_Service.ipp @@ -1,11 +1,51 @@ #pragma once -#include "runtime/Plot.hpp" +#include "runtime/Gallery_Runtime.hpp" +#include +#include +#include #include namespace aethera::mcp { struct Control_Service::Private { - std::unordered_map> plots; /* Plot 唯一实例注册表。 */ + struct Entry : std::enable_shared_from_this { + static constexpr std::size_t maximum_taskflow_trace_frames{120}; + + std::unique_ptr components{}; + web::Gallery_Runtime runtime{}; + std::string id{}; + Gallery_Output output{}; + std::atomic> terminal_failure{}; + std::atomic_size_t taskflow_trace_remaining{}; + std::atomic_uint64_t taskflow_trace_control{}; + std::array>, + maximum_taskflow_trace_frames> taskflow_trace_slots{}; + + Entry(std::string value_id, web::Gallery_Build build, + Gallery_Output value_output); + ~Entry(); + Entry(const Entry&) = delete; + Entry& operator=(const Entry&) = delete; + void start(); + void stop() noexcept; + [[nodiscard]] std::shared_ptr policy() const noexcept; + [[nodiscard]] std::shared_ptr create_policy( + Frame_Policy_Type type, double fixed_rate_fps); + void replace_policy(Frame_Policy_Type type); + void prepare_frame(not_null frame, + const Frame_Production& production); + [[nodiscard]] Frame_Publication_Dispatch publish_frame( + not_null frame, + const Frame_Production& production); + void fail(std::exception_ptr failure) noexcept; + [[nodiscard]] nlohmann::json schema() const; + [[nodiscard]] nlohmann::json diagnostics() const; + void request_trace(std::size_t frame_count); + [[nodiscard]] nlohmann::json trace() const; + }; + + Gallery_Output output{}; + std::unordered_map> plots; }; } diff --git a/mcp/core/Protocol_Type.ipp b/mcp/core/Protocol_Type.ipp index 4f4fc1b..72a00cb 100644 --- a/mcp/core/Protocol_Type.ipp +++ b/mcp/core/Protocol_Type.ipp @@ -98,7 +98,9 @@ struct Json_Value_Type { static void decode(nlohmann::json& value, const nlohmann::json& input) { value = input; } - [[nodiscard]] static nlohmann::json describe() { return {}; } + [[nodiscard]] static nlohmann::json describe() { + return {{"type", "object"}, {"additionalProperties", true}}; + } [[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; } diff --git a/mcp/core/runtime/Datoviz_Observation_Json.cpp b/mcp/core/runtime/Datoviz_Observation_Json.cpp new file mode 100644 index 0000000..6f5df0a --- /dev/null +++ b/mcp/core/runtime/Datoviz_Observation_Json.cpp @@ -0,0 +1,109 @@ +#include "Datoviz_Observation_Json.hpp" + +#include +#include + +namespace aethera::mcp { + +nlohmann::json datoviz_observation_json( + const render_3d::Datoviz_Frame_Observation& value) { + const auto milliseconds = [](std::uint64_t nanoseconds) { + return static_cast(nanoseconds) / 1'000'000.0; + }; + nlohmann::json result{ + {"render_sequence", value.render_sequence}, + {"path", magic_enum::enum_name(value.path)}, + {"gpu_timing_requested", value.gpu_timing_requested}, + {"readback_requested", value.readback_requested}, + {"controller_input_applied", value.controller_input_applied}, + { + "prepare_released_after_submission", + value.prepare_released_after_submission + }, + { + "timings_ms", { + {"queue_submit_wait", milliseconds(value.queue_submit_wait_ns)}, + {"target_acquire", milliseconds(value.target_acquire_ns)}, + {"structure_check", milliseconds(value.structure_check_ns)}, + {"apply", milliseconds(value.apply_ns)}, + {"query", milliseconds(value.query_ns)}, + {"runtime_plan", milliseconds(value.runtime_plan_ns)}, + {"runtime_plan_cpu", milliseconds(value.runtime_plan_cpu_ns)}, + {"runtime_execute", milliseconds(value.runtime_execute_ns)}, + {"runtime_execute_cpu", milliseconds(value.runtime_execute_cpu_ns)}, + {"mvp_update", milliseconds(value.mvp_update_ns)}, + {"frame_begin", milliseconds(value.frame_begin_ns)}, + {"frame_plan", milliseconds(value.frame_plan_ns)}, + {"frame_plan_cpu", milliseconds(value.frame_plan_cpu_ns)}, + {"emit_replay_dirty", milliseconds(value.emit_replay_dirty_ns)}, + {"emit_layout", milliseconds(value.emit_layout_ns)}, + {"emit_prepare", milliseconds(value.emit_prepare_ns)}, + {"emit_plan_build", milliseconds(value.emit_plan_build_ns)}, + {"emit_contract", milliseconds(value.emit_contract_ns)}, + {"emit_stream", milliseconds(value.emit_stream_ns)}, + {"emit_stream_freeze", milliseconds(value.emit_stream_freeze_ns)}, + {"emit_commit", milliseconds(value.emit_commit_ns)}, + {"emit_plan_reset", milliseconds(value.emit_plan_reset_ns)}, + {"emit_artifact_create", milliseconds(value.emit_artifact_create_ns)}, + {"emit_artifact_freeze", milliseconds(value.emit_artifact_freeze_ns)}, + {"emit_packet_encode", milliseconds(value.emit_packet_encode_ns)}, + {"external_register", milliseconds(value.external_register_ns)}, + {"frame_attach", milliseconds(value.frame_attach_ns)}, + {"frame_execute", milliseconds(value.frame_execute_ns)}, + {"frame_execute_cpu", milliseconds(value.frame_execute_cpu_ns)}, + {"frame_finish", milliseconds(value.frame_finish_ns)}, + {"drp_validation", milliseconds(value.drp_validation_ns)}, + {"drp_state", milliseconds(value.drp_state_ns)}, + {"drp_buffer_create", milliseconds(value.drp_buffer_create_ns)}, + {"drp_texture_create", milliseconds(value.drp_texture_create_ns)}, + {"drp_shader_create", milliseconds(value.drp_shader_create_ns)}, + {"drp_shader_compile", milliseconds(value.drp_shader_compile_ns)}, + {"drp_shader_module_create", milliseconds(value.drp_shader_module_create_ns)}, + {"drp_pipeline_create", milliseconds(value.drp_pipeline_create_ns)}, + {"drp_binding_create", milliseconds(value.drp_binding_create_ns)}, + {"drp_upload", milliseconds(value.drp_upload_ns)}, + {"drp_transfer", milliseconds(value.drp_transfer_ns)}, + {"drp_record", milliseconds(value.drp_record_ns)}, + {"submit", milliseconds(value.submit_ns)}, + {"gpu_completion_observation", milliseconds(value.gpu_completion_observation_ns)}, + {"completion_task_queue", milliseconds(value.completion_task_queue_ns)}, + {"readback", milliseconds(value.readback_ns)} + } + }, + { + "traffic", { + {"uploaded_bytes", value.uploaded_bytes}, + {"readback_bytes", value.readback_bytes}, + {"pipeline_create_count", value.drp_pipeline_create_count} + } + }, + { + "frame_plan", { + {"resource_version", value.artifact_resource_version}, + {"frame_index", value.artifact_frame_index}, + {"status", value.artifact_status} + } + }, + { + "validation", { + {"performed", value.validation_performed}, + {"ok", value.validation_ok}, + {"code", value.validation_code}, + {"command_index", value.validation_command_index} + } + } + }; + if (value.gpu) { + result["gpu_ms"] = { + {"render", milliseconds(value.gpu->render_ns)}, + {"transition", milliseconds(value.gpu->transition_ns)}, + {"copy", milliseconds(value.gpu->copy_ns)}, + {"total", milliseconds(value.gpu->total_ns)} + }; + } + if (!value.artifact_json.empty()) + result["frame_plan"]["artifact_json"] = value.artifact_json; + return result; +} + +} diff --git a/mcp/core/runtime/Datoviz_Observation_Json.hpp b/mcp/core/runtime/Datoviz_Observation_Json.hpp new file mode 100644 index 0000000..6b324cf --- /dev/null +++ b/mcp/core/runtime/Datoviz_Observation_Json.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace aethera::mcp { + +[[nodiscard]] nlohmann::json datoviz_observation_json( + const render_3d::Datoviz_Frame_Observation& observation); + +} diff --git a/mcp/core/runtime/Gallery_Plots.hpp b/mcp/core/runtime/Gallery_Plots.hpp index 273832a..4a87e2c 100644 --- a/mcp/core/runtime/Gallery_Plots.hpp +++ b/mcp/core/runtime/Gallery_Plots.hpp @@ -15,7 +15,7 @@ struct Plot_Definition { std::string_view category; /* Catalog 分组名称。 */ std::string_view description; /* Plot 能力说明。 */ Plot_Dimension dimension{Plot_Dimension::two_d}; /* 渲染后端维度。 */ - std::shared_ptr (*create)(); /* 创建该 Plot 唯一实例的工厂。 */ + Gallery_Build (*create)(); /* 创建组件所有者与借用组件的 Scene。 */ }; [[nodiscard]] std::span diff --git a/mcp/core/runtime/Gallery_Plots_2D.cpp b/mcp/core/runtime/Gallery_Plots_2D.cpp index b576348..b7bc01c 100644 --- a/mcp/core/runtime/Gallery_Plots_2D.cpp +++ b/mcp/core/runtime/Gallery_Plots_2D.cpp @@ -22,7 +22,7 @@ using Numeric_Axis_Object = Numeric_Axis; using Time_Axis_Object = Time_Axis; using Selection_Object = Selection_Rectangle_Overlay; template -struct Scene_View_Model final : public Plot::Scene_View { +struct Gallery_Component_Model final : public Gallery_Component { public: struct Data_Generator { nlohmann::json schema; @@ -32,7 +32,7 @@ public: return static_cast(generate); } }; - Scene_View_Model(std::vector> value_descriptors, + Gallery_Component_Model(std::vector> value_descriptors, std::function value_update, Data_Generator value_data_generator, Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)), @@ -417,7 +417,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { } } template -std::unique_ptr make_scene_view( +std::unique_ptr make_scene_view( Object & object, Scene_2D & scene, std::function < void(const Frame_Request &, bool) > update, @@ -447,10 +447,10 @@ std::unique_ptr make_scene_view( } }; (append_owned(owned_objects), ...); - return std::make_unique...>>( + return std::make_unique...>>( std::move(components), std::move(update), - typename Scene_View_Model...>::Data_Generator{ + typename Gallery_Component_Model...>::Data_Generator{ generator_2d_schema(), [&object](const nlohmann::json& input) { return generate_2d_data(object, input); @@ -534,7 +534,7 @@ void resize_axes(not_null scene, Size viewport, (resize_axis(axes), ...); } } -std::shared_ptr make_axes_plot() { +Gallery_Build make_axes_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); frequency->template set<&Abs_Axis::Prop::unit_text>("Hz"); @@ -595,9 +595,9 @@ std::shared_ptr make_axes_plot() { return Json{{"success", false}, {"error", error.what()}}; } }; - auto view = std::make_unique>( + auto view = std::make_unique>( std::move(components), std::move(update), - Scene_View_Model::Data_Generator{ + Gallery_Component_Model::Data_Generator{ Json{ {"label", "生成坐标轴压力数据"}, {"description", "按时间样本规模和三个业务坐标范围生成可重复的坐标轴压力负载。"}, @@ -606,9 +606,9 @@ std::shared_ptr make_axes_plot() { std::move(generate), {} }, std::move(frequency), std::move(numeric), std::move(time)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_spectrum_plot() { +Gallery_Build make_spectrum_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); @@ -672,9 +672,9 @@ std::shared_ptr make_spectrum_plot() { State_Field, State_Field>( *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_frequency_trace_plot() { +Gallery_Build make_frequency_trace_plot() { constexpr Size canvas{720, 420}; auto time = make_time_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0); @@ -713,9 +713,9 @@ std::shared_ptr make_frequency_trace_plot() { Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">, Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">>( *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_sweep_spectrum_plot() { +Gallery_Build make_sweep_spectrum_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis( @@ -766,9 +766,9 @@ std::shared_ptr make_sweep_spectrum_plot() { Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">>( *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_afterglow_plot() { +Gallery_Build make_afterglow_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto vertical = make_numeric_axis( @@ -815,9 +815,9 @@ std::shared_ptr make_afterglow_plot() { Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">, Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">>( *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_waterfall_plot() { +Gallery_Build make_waterfall_plot() { constexpr Size canvas{720, 420}; auto frequency = make_frequency_axis(); auto time = make_time_axis( @@ -873,9 +873,9 @@ std::shared_ptr make_waterfall_plot() { Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">>( *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_constellation_plot() { +Gallery_Build make_constellation_plot() { constexpr Size canvas{720, 420}; auto horizontal = make_numeric_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}); @@ -934,9 +934,9 @@ std::shared_ptr make_constellation_plot() { Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">>( *constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } -std::shared_ptr make_selection_overlay_plot() { +Gallery_Build make_selection_overlay_plot() { constexpr Size canvas{720, 420}; auto horizontal = make_numeric_axis( Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}); @@ -965,6 +965,6 @@ std::shared_ptr make_selection_overlay_plot() { "selected_region_count", "Number of rectangular regions currently selected.">>( *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } } diff --git a/mcp/core/runtime/Gallery_Plots_2D.hpp b/mcp/core/runtime/Gallery_Plots_2D.hpp index cd9605c..2e7206b 100644 --- a/mcp/core/runtime/Gallery_Plots_2D.hpp +++ b/mcp/core/runtime/Gallery_Plots_2D.hpp @@ -1,13 +1,13 @@ #pragma once -#include "Plot.hpp" +#include "Gallery_Runtime.hpp" namespace aethera::web { -[[nodiscard]] std::shared_ptr make_spectrum_plot(); -[[nodiscard]] std::shared_ptr make_axes_plot(); -[[nodiscard]] std::shared_ptr make_frequency_trace_plot(); -[[nodiscard]] std::shared_ptr make_sweep_spectrum_plot(); -[[nodiscard]] std::shared_ptr make_afterglow_plot(); -[[nodiscard]] std::shared_ptr make_waterfall_plot(); -[[nodiscard]] std::shared_ptr make_constellation_plot(); -[[nodiscard]] std::shared_ptr make_selection_overlay_plot(); +[[nodiscard]] Gallery_Build make_spectrum_plot(); +[[nodiscard]] Gallery_Build make_axes_plot(); +[[nodiscard]] Gallery_Build make_frequency_trace_plot(); +[[nodiscard]] Gallery_Build make_sweep_spectrum_plot(); +[[nodiscard]] Gallery_Build make_afterglow_plot(); +[[nodiscard]] Gallery_Build make_waterfall_plot(); +[[nodiscard]] Gallery_Build make_constellation_plot(); +[[nodiscard]] Gallery_Build make_selection_overlay_plot(); } diff --git a/mcp/core/runtime/Gallery_Plots_3D.cpp b/mcp/core/runtime/Gallery_Plots_3D.cpp index eb657bc..6d9d544 100644 --- a/mcp/core/runtime/Gallery_Plots_3D.cpp +++ b/mcp/core/runtime/Gallery_Plots_3D.cpp @@ -272,12 +272,12 @@ struct Random_Data_Generator { } }; template -struct Visual_Scene_View final : public Plot::Scene_View { +struct Visual_Gallery_Component final : public Gallery_Component { public: using Camera_Object = Camera_3D; using Axes_Object = Axes_3D; using Marker_Object = Marker_Visual; - Visual_Scene_View(Scene_3D& scene, std::unique_ptr camera, + Visual_Gallery_Component(Scene_3D& scene, std::unique_ptr camera, std::unique_ptr axes, std::unique_ptr visual, std::string label, Data_Generator data_generator = {}, @@ -473,7 +473,7 @@ Build_Result initialize_visual_frame( } template -std::shared_ptr make_visual_plot(std::string label, +Gallery_Build make_visual_plot(std::string label, Build_Result build_result, Scene_Components_3D components = {}, Data_Generator data_generator = {}, @@ -515,10 +515,10 @@ std::shared_ptr make_visual_plot(std::string label, auto scene_result = scene_builder.build(); if (!scene_result) throw std::logic_error("3D Gallery scene dependency graph is invalid"); auto scene = std::move(scene_result).value(); - auto view = std::make_unique>( + auto view = std::make_unique>( *scene, std::move(camera), std::move(axes), std::move(visual), std::move(label), std::move(data_generator), std::move(markers)); - return std::make_shared(std::move(scene), std::move(view)); + return {std::move(view), Gallery_Scene{std::move(scene)}}; } Color color(std::uint8_t red, std::uint8_t green, std::uint8_t blue, std::uint8_t alpha = 255) { return {red, green, blue, alpha}; @@ -778,19 +778,19 @@ struct Spectrogram_Data_Generator { } }; } -std::shared_ptr make_datoviz_point_plot() { +Gallery_Build make_datoviz_point_plot() { return make_visual_plot("Point Visual", initialize_visual_frame( Point_Visual::Builder{}.build(), Point_Frame{{{{-0.65F, -0.25F, 0.05F}, color(255, 91, 110), 28.0F}, {{0.0F, 0.58F, 0.25F}, color(82, 226, 190), 34.0F}, {{0.62F, -0.12F, -0.15F}, color(75, 145, 255), 30.0F}}})); } -std::shared_ptr make_datoviz_splat_plot() { +Gallery_Build make_datoviz_splat_plot() { return make_visual_plot("Splat Visual", initialize_visual_frame( Splat_Visual::Builder{}.build(), Splat_Frame{{{{-0.48F, 0.0F, 0.1F}, color(255, 98, 115, 210), {0.18F, 0.08F}, 0.45F}, {{0.28F, 0.15F, 0.0F}, color(66, 218, 188, 210), {0.12F, 0.22F}, -0.3F}, {{0.15F, -0.38F, 0.2F}, color(76, 132, 255, 210), {0.2F, 0.1F}, 0.9F}}})); } -std::shared_ptr make_datoviz_pixel_plot() { +Gallery_Build make_datoviz_pixel_plot() { std::vector pixels; for (int y = -8; y <= 8; ++y) for (int x = -12; x <= 12; ++x) pixels.push_back({{x / 13.0F, y / 9.0F, 0.12F * std::sin(x * .45F) * std::cos(y * .35F)}, color(static_cast(90 + 6 * (x + 12)), static_cast(100 + 8 * (y + 8)), 230), 5.0F}); return make_visual_plot("Pixel Visual", @@ -798,19 +798,19 @@ std::shared_ptr make_datoviz_pixel_plot() { Pixel_Visual::Builder{}.build(), Pixel_Frame{std::move(pixels)})); } -std::shared_ptr make_datoviz_marker_plot() { +Gallery_Build make_datoviz_marker_plot() { return make_visual_plot("Marker Visual", initialize_visual_frame( Marker_Visual::Builder{}.build(), Marker_Frame{{{{-0.7F, 0.0F, 0.0F}, color(255, 93, 115), 34.0F, 0.0F, Marker_Shape::disc}, {{-0.35F, 0.25F, 0.1F}, color(91, 226, 193), 36.0F, 0.25F, Marker_Shape::square}, {{0.0F, -0.2F, 0.2F}, color(100, 158, 255), 38.0F, 0.5F, Marker_Shape::triangle}, {{0.35F, 0.25F, 0.1F}, color(250, 195, 92), 40.0F, 0.75F, Marker_Shape::diamond}, {{0.7F, 0.0F, 0.0F}, color(201, 132, 255), 42.0F, 1.0F, Marker_Shape::cross}}})); } -std::shared_ptr make_datoviz_sphere_plot() { +Gallery_Build make_datoviz_sphere_plot() { return make_visual_plot("Sphere Visual", initialize_visual_frame( Sphere_Visual::Builder{}.build(), Sphere_Frame{{{{-0.48F, -0.2F, 0.0F}, color(255, 91, 110), 0.28F}, {{0.08F, 0.25F, 0.18F}, color(82, 226, 190), 0.36F}, {{0.55F, -0.18F, -0.12F}, color(75, 145, 255), 0.24F}}})); } -std::shared_ptr make_datoviz_segment_plot() { +Gallery_Build make_datoviz_segment_plot() { std::vector segments; for (int index = 0; index < 12; ++index) { const float angle = static_cast(index) * std::numbers::pi_v / 6.0F; @@ -821,25 +821,25 @@ std::shared_ptr make_datoviz_segment_plot() { Segment_Visual::Builder{}.build(), Segment_Frame{std::move(segments)})); } -std::shared_ptr make_datoviz_vector_plot() { +Gallery_Build make_datoviz_vector_plot() { return make_visual_plot("Vector Visual", initialize_visual_frame( Vector_Visual::Builder{}.build(), Vector_Frame{{{{-0.55F, -0.35F, 0.0F}, {0.55F, 0.2F, 0.25F}, color(255, 98, 115), 4.0F}, {{-0.1F, 0.0F, 0.0F}, {0.2F, 0.62F, 0.18F}, color(80, 225, 190), 5.0F}, {{0.35F, -0.25F, 0.0F}, {-0.12F, 0.25F, 0.65F}, color(78, 145, 255), 4.0F}}})); } -std::shared_ptr make_datoviz_primitive_plot() { +Gallery_Build make_datoviz_primitive_plot() { return make_visual_plot("Primitive Visual", initialize_visual_frame( Primitive_Visual::Builder{}.build(), Primitive_Frame{{{{-0.72F, -0.55F, 0.0F}, color(255, 86, 110), {0, 0, 1}}, {{0.72F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}}, {{0.0F, 0.72F, 0.25F}, color(82, 226, 190), {0, 0, 1}}}})); } -std::shared_ptr make_datoviz_mesh_plot() { +Gallery_Build make_datoviz_mesh_plot() { const std::vector mesh{{{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}, {1, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, 0.55F, 0.0F}, color(244, 190, 86), {0, 0, 1}, {0, 1}}}; return make_visual_plot("Mesh Visual", initialize_visual_frame( Mesh_Visual::Builder{}.build(), Mesh_Frame{mesh})); } -std::shared_ptr make_datoviz_spectrogram_plot() { +Gallery_Build make_datoviz_spectrogram_plot() { const Spectrogram_Parameters parameters; auto marker_result = initialize_visual_frame( Marker_Visual::Builder{} @@ -897,7 +897,7 @@ std::shared_ptr make_datoviz_spectrogram_plot() { Mesh_Frame{spectrogram_mesh(parameters)}), std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers)); } -std::shared_ptr make_datoviz_path_plot() { +Gallery_Build make_datoviz_path_plot() { std::vector path; for (int index = 0; index < 64; ++index) { const float t = static_cast(index) / 63.0F; @@ -908,7 +908,7 @@ std::shared_ptr make_datoviz_path_plot() { Path_Visual::Builder{}.build(), Path_Frame{std::move(path)})); } -std::shared_ptr make_datoviz_image_plot() { +Gallery_Build make_datoviz_image_plot() { std::vector pixels(32U * 32U); for (std::uint32_t y = 0; y < 32U; ++y) for (std::uint32_t x = 0; x < 32U; ++x) @@ -920,7 +920,7 @@ std::shared_ptr make_datoviz_image_plot() { Image_Frame{{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}}}, 32U, 32U, std::move(pixels)})); } -std::shared_ptr make_datoviz_labels_plot() { +Gallery_Build make_datoviz_labels_plot() { std::vector labels(8U * 8U); for (std::uint32_t y = 0; y < 8U; ++y) for (std::uint32_t x = 0; x < 8U; ++x) @@ -932,19 +932,19 @@ std::shared_ptr make_datoviz_labels_plot() { Labels_Frame{{{{-0.55F, 0.28F, 0.0F}, {72, 34}, {0, 0, 0.5F, 0.5F}}, {{0.5F, 0.25F, 0.1F}, {72, 34}, {0.5F, 0, 1, 0.5F}}, {{-0.45F, -0.32F, 0.1F}, {72, 34}, {0, 0.5F, 0.5F, 1}}, {{0.55F, -0.3F, 0.0F}, {72, 34}, {0.5F, 0.5F, 1, 1}}}, 8U, 8U, std::move(labels)})); } -std::shared_ptr make_datoviz_glyph_plot() { +Gallery_Build make_datoviz_glyph_plot() { return make_visual_plot("Glyph Visual", initialize_visual_frame( Glyph_Visual::Builder{}.build(), Glyph_Frame{{{{-0.55F, -0.15F, 0.0F}, {-0.18F, -0.18F, 0.18F, 0.18F}, {0, 0, 1, 1}, color(255, 100, 120), -0.2F}, {{0.0F, 0.22F, 0.1F}, {-0.22F, -0.22F, 0.22F, 0.22F}, {0, 0, 1, 1}, color(82, 226, 190), 0.25F}, {{0.55F, -0.15F, 0.0F}, {-0.2F, -0.2F, 0.2F, 0.2F}, {0, 0, 1, 1}, color(80, 145, 255), 0.55F}}})); } -std::shared_ptr make_datoviz_text_plot() { +Gallery_Build make_datoviz_text_plot() { return make_visual_plot("Text Visual", initialize_visual_frame( Text_Visual::Builder{}.build(), Text_Frame{{{{-0.72F, 0.28F, 0.0F}, "Aethera", color(82, 226, 190), 28.0F}, {{-0.62F, -0.15F, 0.1F}, "Datoviz Visual", color(90, 155, 255), 22.0F}}})); } -std::shared_ptr make_datoviz_volume_plot() { +Gallery_Build make_datoviz_volume_plot() { std::vector voxels(16U * 16U * 16U); for (std::size_t index = 0; index < voxels.size(); ++index) voxels[index].value = static_cast(index % 256U) / 255.0F; return make_visual_plot("Volume Visual", diff --git a/mcp/core/runtime/Gallery_Plots_3D.hpp b/mcp/core/runtime/Gallery_Plots_3D.hpp index e91ec20..c1bb6af 100644 --- a/mcp/core/runtime/Gallery_Plots_3D.hpp +++ b/mcp/core/runtime/Gallery_Plots_3D.hpp @@ -1,21 +1,21 @@ #pragma once -#include "Plot.hpp" +#include "Gallery_Runtime.hpp" namespace aethera::web { -[[nodiscard]] std::shared_ptr make_datoviz_point_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_splat_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_pixel_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_marker_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_sphere_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_segment_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_vector_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_primitive_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_mesh_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_spectrogram_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_path_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_image_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_labels_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_glyph_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_text_plot(); -[[nodiscard]] std::shared_ptr make_datoviz_volume_plot(); +[[nodiscard]] Gallery_Build make_datoviz_point_plot(); +[[nodiscard]] Gallery_Build make_datoviz_splat_plot(); +[[nodiscard]] Gallery_Build make_datoviz_pixel_plot(); +[[nodiscard]] Gallery_Build make_datoviz_marker_plot(); +[[nodiscard]] Gallery_Build make_datoviz_sphere_plot(); +[[nodiscard]] Gallery_Build make_datoviz_segment_plot(); +[[nodiscard]] Gallery_Build make_datoviz_vector_plot(); +[[nodiscard]] Gallery_Build make_datoviz_primitive_plot(); +[[nodiscard]] Gallery_Build make_datoviz_mesh_plot(); +[[nodiscard]] Gallery_Build make_datoviz_spectrogram_plot(); +[[nodiscard]] Gallery_Build make_datoviz_path_plot(); +[[nodiscard]] Gallery_Build make_datoviz_image_plot(); +[[nodiscard]] Gallery_Build make_datoviz_labels_plot(); +[[nodiscard]] Gallery_Build make_datoviz_glyph_plot(); +[[nodiscard]] Gallery_Build make_datoviz_text_plot(); +[[nodiscard]] Gallery_Build make_datoviz_volume_plot(); } diff --git a/mcp/core/runtime/Gallery_Runtime.hpp b/mcp/core/runtime/Gallery_Runtime.hpp new file mode 100644 index 0000000..3681f79 --- /dev/null +++ b/mcp/core/runtime/Gallery_Runtime.hpp @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::web { + +enum struct Gallery_Pixel_Layout : std::uint8_t { bgra8, rgba8 }; + +struct Gallery_Pixel_Frame { + std::shared_ptr> pixels{}; + Gallery_Pixel_Layout layout{Gallery_Pixel_Layout::rgba8}; + std::chrono::microseconds presentation_time{}; + std::uint64_t sequence{}; + std::uint64_t correlation_id{}; + std::uint64_t rendered_sequence{}; + std::uint64_t rendered_correlation_id{}; + std::uint32_t width{}; + std::uint32_t height{}; +}; + +struct Gallery_Frame { + std::shared_ptr pixels{}; +}; + +enum struct Gallery_Frame_Publication : std::uint8_t { + ignored, + completed, + asynchronous +}; + +using Gallery_Frame_Publisher = std::function)>; + +struct Gallery_Component { + virtual ~Gallery_Component() = default; + [[nodiscard]] virtual nlohmann::json schema() const = 0; + [[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 capture_components( + const std::vector& components) 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 Frame_Request& request) = 0; +}; + +using Gallery_Scene = std::variant< + std::unique_ptr, + std::unique_ptr>; +using Gallery_Build = std::pair< + std::unique_ptr, Gallery_Scene>; + +/* One authoritative Scene and its current policy. */ +using Gallery_Runtime = std::pair< + Gallery_Scene, std::atomic>>; + +} diff --git a/mcp/core/runtime/Input_Event.cpp b/mcp/core/runtime/Input_Event.cpp new file mode 100644 index 0000000..0356285 --- /dev/null +++ b/mcp/core/runtime/Input_Event.cpp @@ -0,0 +1,134 @@ +#include "Input_Event.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::web { +namespace { + +[[nodiscard]] Event_Timeline_Time input_timeline_time(double milliseconds) { + constexpr long double nanoseconds_per_millisecond{1'000'000.0L}; + constexpr long double maximum_milliseconds = + static_cast(std::numeric_limits::max()) / + nanoseconds_per_millisecond; + if (!std::isfinite(milliseconds) || milliseconds < 0.0 || + static_cast(milliseconds) > maximum_milliseconds) + throw std::invalid_argument( + "input time_milliseconds must be finite, non-negative and representable"); + return Event_Timeline_Time{static_cast( + static_cast(milliseconds) * + nanoseconds_per_millisecond)}; +} + +void read_point(const nlohmann::json& input, std::string_view key, + Input_Point& point, Input_Viewport viewport, + bool viewport_relative) { + const auto value = input.find(key); + if (value == input.end() || !value->is_object()) return; + const auto x = value->value("x", 0.0); + const auto y = value->value("y", 0.0); + point.x = viewport_relative && viewport.width != 0 + ? std::clamp(x, 0.0, static_cast(viewport.width)) : x; + point.y = viewport_relative && viewport.height != 0 + ? std::clamp(y, 0.0, static_cast(viewport.height)) : y; +} + +void read_pointer(const nlohmann::json& input, + Basic_Pointer_Event& event, + Input_Viewport viewport) { + read_point(input, "position", event.position, viewport, true); + read_point(input, "global_position", event.global_position, viewport, false); + event.button = magic_enum::enum_cast( + input.value("button", std::string{"none"})).value_or(Mouse_Button::none); + event.buttons = static_cast( + std::clamp(input.value("buttons", 0), 0, 255)); + event.modifiers = static_cast( + std::clamp(input.value("modifiers", 0), 0, 15)); +} + +} + +Input_Resize_Event::Input_Resize_Event(Event_Timeline_Time occurred_at) + : Event(Event_Type::resize, occurred_at) {} + +Input_Pointer_Event::Input_Pointer_Event( + Event_Type type, Event_Timeline_Time occurred_at) + : Basic_Pointer_Event(type, occurred_at) {} + +Input_Wheel_Event::Input_Wheel_Event(Event_Timeline_Time occurred_at) + : Basic_Wheel_Event(occurred_at) {} + +Input_Key_Event::Input_Key_Event( + Event_Type type, Event_Timeline_Time occurred_at) + : Key_Event(type, occurred_at) {} + +std::shared_ptr make_input_event( + const nlohmann::json& input, Input_Viewport viewport) { + if (!input.is_object()) + throw std::invalid_argument("input event must be an object"); + const auto type = magic_enum::enum_cast( + input.value("type", std::string{})); + if (!type) throw std::invalid_argument("input event type is unknown"); + const auto occurred_at = input_timeline_time( + input.at("time_milliseconds").get()); + + switch (*type) { + case Event_Type::resize: { + auto event = std::make_shared(occurred_at); + if (const auto size = input.find("old_size"); + size != input.end() && size->is_object()) { + event->old_size.width = size->value("width", 0U); + event->old_size.height = size->value("height", 0U); + } + if (const auto size = input.find("new_size"); + size != input.end() && size->is_object()) { + event->new_size.width = size->value("width", 0U); + event->new_size.height = size->value("height", 0U); + } + return event; + } + case Event_Type::pointer_move: + case Event_Type::pointer_press: + case Event_Type::pointer_release: { + auto event = std::make_shared(*type, occurred_at); + read_pointer(input, *event, viewport); + return event; + } + case Event_Type::wheel: { + auto event = std::make_shared(occurred_at); + read_pointer(input, *event, viewport); + event->pixel_delta_x = std::clamp( + input.value("pixel_delta_x", 0.0), -4096.0, 4096.0); + event->pixel_delta_y = std::clamp( + input.value("pixel_delta_y", 0.0), -4096.0, 4096.0); + event->angle_delta_x = std::clamp( + input.value("angle_delta_x", 0.0), -120.0, 120.0); + event->angle_delta_y = std::clamp( + input.value("angle_delta_y", 0.0), -120.0, 120.0); + return event; + } + case Event_Type::key_press: + case Event_Type::key_release: { + auto event = std::make_shared(*type, occurred_at); + event->key = magic_enum::enum_cast( + input.value("key", std::string{"unknown"})).value_or(Key::unknown); + event->native_key = input.value("native_key", 0U); + event->modifiers = static_cast( + std::clamp(input.value("modifiers", 0), 0, 15)); + event->auto_repeat = input.value("auto_repeat", false); + return event; + } + case Event_Type::show: + case Event_Type::hide: + case Event_Type::leave: + return std::make_shared(*type, occurred_at); + } + throw std::logic_error("unhandled input event type"); +} + +} diff --git a/mcp/core/runtime/Input_Event.hpp b/mcp/core/runtime/Input_Event.hpp new file mode 100644 index 0000000..1f7063b --- /dev/null +++ b/mcp/core/runtime/Input_Event.hpp @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include +#include + +namespace aethera::web { + +struct Input_Point { + double x{}; /* Plot 局部或全局像素横坐标。 */ + double y{}; /* Plot 局部或全局像素纵坐标。 */ +}; + +struct Input_Size { + std::uint32_t width{}; /* 输入事件携带的视口宽度,单位为像素。 */ + std::uint32_t height{}; /* 输入事件携带的视口高度,单位为像素。 */ +}; + +struct Input_Resize_Event final : Event { + explicit Input_Resize_Event(Event_Timeline_Time occurred_at); + Input_Size old_size{}; /* 调整前的视口尺寸;协议未提供时为零。 */ + Input_Size new_size{}; /* 调整后的视口尺寸;协议未提供时为零。 */ +}; + +struct Input_Pointer_Event final : Basic_Pointer_Event { + Input_Pointer_Event(Event_Type type, Event_Timeline_Time occurred_at); +}; + +struct Input_Wheel_Event final : Basic_Wheel_Event { + explicit Input_Wheel_Event(Event_Timeline_Time occurred_at); +}; + +struct Input_Key_Event final : Key_Event { + Input_Key_Event(Event_Type type, Event_Timeline_Time occurred_at); +}; + +struct Input_Viewport { + std::uint32_t width{}; /* 可选的局部横坐标裁剪宽度,零表示不裁剪。 */ + std::uint32_t height{}; /* 可选的局部纵坐标裁剪高度,零表示不裁剪。 */ +}; + +[[nodiscard]] std::shared_ptr make_input_event( + const nlohmann::json& input, Input_Viewport viewport = {}); + +} diff --git a/mcp/core/runtime/Plot.cpp b/mcp/core/runtime/Plot.cpp deleted file mode 100644 index 15178f6..0000000 --- a/mcp/core/runtime/Plot.cpp +++ /dev/null @@ -1,1564 +0,0 @@ -#include "Plot.hpp" -#include "Renderable_Adapter.hpp" -#include "Taskflow_Trace_Json.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace aethera::web { -namespace { -using namespace render_2d; -using namespace render_3d; -using Scene_2D = Render_Scene_2D; -using Scene_3D = Render_Scene_3D; -constexpr std::uint16_t plot_stream_protocol_version{9}; -constexpr std::size_t diagnostic_window_capacity{600}; -std::string exception_description(const std::exception_ptr& failure) { - try { - if (failure) std::rethrow_exception(failure); - } - catch (const std::exception& error) { - return error.what(); - } - catch (...) { - return "non-standard Plot failure"; - } - return "empty Plot failure"; -} -std::string_view pacing_mode_name(Frame_Policy_Type mode) { - const auto name = magic_enum::enum_name(mode); - if (name.empty()) throw std::logic_error("unknown frame pacing mode"); - return name; -} -std::optional parse_pacing_mode(std::string_view value) { - return magic_enum::enum_cast(value); -} -std::string_view pixel_format_name(render_2d::Pixel_Format format) { - const auto name = magic_enum::enum_name(format); - if (name.empty()) throw std::logic_error("unknown 2D pixel format"); - return name; -} -std::string_view pixel_format_name(render_3d::Pixel_Format format) { - switch (format) { - case render_3d::Pixel_Format::rgba8_unorm: return "rgba8"; - } - throw std::logic_error("unknown 3D pixel format"); -} -nlohmann::json frame_policy_schema(const Frame_Policy& policy) { - const auto* fixed = dynamic_cast(&policy); - return { - {"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"}, - { - "fields", nlohmann::json::array({ - { - {"key", "render_enabled"}, {"label", "持续渲染与采样"}, {"editor", "boolean"}, - {"editable", true}, {"description", "控制当前 Scene 的周期刷新;画面隐藏不会修改此项。"}, - {"technical_description", "Authoritative per-scene periodic render switch."}, - {"value", policy.get<&Frame_Policy::Prop::render_enabled>()} - }, - { - {"key", "pixel_delivery_enabled"}, {"label", "图集像素传输"}, {"editor", "boolean"}, - {"editable", true}, {"description", "控制完成帧是否进入页面级采样器;2D BGRA 与 3D RGBA 均保持原生格式。"}, - {"technical_description", "Authoritative tile publication switch for the shared gallery pixel stream."}, - {"value", policy.get<&Frame_Policy::Prop::pixel_delivery_enabled>()} - }, - { - {"key", "pacing_mode"}, {"label", "服务端帧策略"}, {"editor", "select"}, - {"editable", true}, {"description", "只控制 Scene::render(Frame*) 的调用节奏;Scene 的 Frame 所有权与接口保持不变。"}, - {"technical_description", "Per-scene frame pacing policy backed by the Kernel scheduler."}, - {"value", pacing_mode_name(policy.type())}, - { - "options", nlohmann::json::array({ - {{"value", "manual"}, {"label", "手动渲染"}}, - {{"value", "fixed_rate"}, {"label", "固定频率"}}, - {{"value", "maximum_rate"}, {"label", "最大吞吐"}} - }) - } - }, - { - {"key", "fixed_rate_fps"}, {"label", "目标帧率"}, {"editor", "number"}, - {"editable", true}, {"minimum", 0.1}, {"maximum", 100.0}, {"step", 0.1}, - {"description", "仅 fixed_rate 使用;maximum_rate 在完成准入释放后异步自驱下一帧。"}, - {"technical_description", "Independent per-scene target frame rate."}, - {"value", fixed ? fixed->frame_rate() : 30.0} - } - }) - } - }; -} -nlohmann::json write_frame_policy_prop(Frame_Policy& pacing, - std::string_view key, - const nlohmann::json& value) { - if (key == "render_enabled" || key == "pixel_delivery_enabled") { - if (!value.is_boolean()) return {{"success", false}, {"error", "frame policy switch requires a boolean"}}; - const bool target = value.get(); - if (key == "render_enabled") - pacing.set<&Frame_Policy::Prop::render_enabled>(target); - else - pacing.set<&Frame_Policy::Prop::pixel_delivery_enabled>(target); - return { - {"success", true}, {"component", "frame-analysis"}, {"key", key}, - {"value", target} - }; - } - if (key == "fixed_rate_fps") { - if (!value.is_number()) return {{"success", false}, {"error", "fixed_rate_fps requires a number"}}; - const double next = value.get(); - if (!std::isfinite(next) || next < 0.1 || next > 100.0) return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 100"}}; - auto* fixed = dynamic_cast(&pacing); - if (!fixed) return {{"success", false}, {"error", "fixed_rate_fps belongs to Fixed_Rate_Frame_Policy"}}; - fixed->set_frame_rate(next); - return { - {"success", true}, {"component", "frame-analysis"}, {"key", key}, - {"value", next} - }; - } - return {{"success", false}, {"error", "unknown frame runtime property"}}; -} -nlohmann::json frame_policy_state_json(const Frame_Policy_State& state, - const Frame_Policy& policy) { - const auto milliseconds = [](std::uint64_t nanoseconds) { - return static_cast(nanoseconds) / 1'000'000.0; - }; - const auto ratio = [](std::uint64_t numerator, std::uint64_t denominator) { - return denominator == 0 ? 0.0 : static_cast(numerator) / static_cast(denominator); - }; - const auto observed_ns = state.observed_until_ns > state.observation_started_ns - ? state.observed_until_ns - state.observation_started_ns - : 0U; - const auto observed_seconds = static_cast(observed_ns) / 1'000'000'000.0; - const auto rate = [observed_seconds](std::uint64_t count) { - return observed_seconds > 0.0 - ? static_cast(count) / observed_seconds - : 0.0; - }; - const auto completion_span = state.last_completion_ns > state.first_completion_ns - ? state.last_completion_ns - state.first_completion_ns - : 0U; - const auto effective_fps = completion_span != 0 && state.completed_frame_count > 1 - ? static_cast(state.completed_frame_count - 1U) * - 1'000'000'000.0 / static_cast(completion_span) - : 0.0; - const auto interval_mean = state.completion_interval_count == 0 - ? 0.0 - : static_cast(state.completion_interval_total_ns) / - static_cast(state.completion_interval_count); - const auto interval_variance = state.completion_interval_count == 0 - ? 0.0 - : std::max(0.0, - state.completion_interval_squared_total_ns2 / - static_cast(state.completion_interval_count) - - interval_mean * interval_mean); - const auto* fixed = dynamic_cast(&policy); - const auto fixed_rate_fps = fixed ? fixed->frame_rate() : 0.0; - const auto target_achievement = fixed_rate_fps > 0.0 - ? effective_fps / fixed_rate_fps : 0.0; - return { - {"generation", state.generation}, - { - "configuration", { - {"mode", pacing_mode_name(policy.type())}, - {"render_enabled", policy.get<&Frame_Policy::Prop::render_enabled>()}, - {"pixel_delivery_enabled", policy.get<&Frame_Policy::Prop::pixel_delivery_enabled>()}, - {"fixed_rate_fps", fixed_rate_fps} - } - }, - { - "observation", { - {"duration_ms", milliseconds(observed_ns)}, - {"request_count", state.request_count}, - {"submitted_frame_count", state.submitted_frame_count}, - {"completed_frame_count", state.completed_frame_count}, - {"active_frame_count", state.active_frame_count} - } - }, - { - "lifecycle", { - {"state", magic_enum::enum_name(policy.read_state().lifecycle)}, - {"in_flight_window", policy.get<&Frame_Policy::Prop::frame_capacity>()}, - {"active_frame_count", state.active_frame_count}, - {"latest_publication_dispatch_ms", - static_cast( - state.latest_publication_dispatch_latency_ns) / - 1'000'000.0}, - {"maximum_publication_dispatch_ms", - static_cast( - state.maximum_publication_dispatch_latency_ns) / - 1'000'000.0}, - {"published_frame_count", state.published_frame_count}, - {"publication_failed_count", state.publication_failed_count}, - {"latest_publication_ms", static_cast( - state.latest_publication_dispatch_latency_ns) / 1'000'000.0}, - {"maximum_publication_ms", static_cast( - state.maximum_publication_dispatch_latency_ns) / 1'000'000.0} - } - }, - { - "throughput", { - {"request_rate_fps", rate(state.request_count)}, - {"submission_rate_fps", rate(state.submitted_frame_count)}, - {"completion_rate_fps", effective_fps}, - {"target_achievement_ratio", target_achievement}, - { - "latest_frame_interval_ms", - milliseconds(state.latest_completion_interval_ns) - }, - { - "average_frame_interval_ms", milliseconds( - state.completion_interval_count == 0 - ? 0U - : state.completion_interval_total_ns / - state.completion_interval_count) - }, - { - "frame_interval_jitter_ms", - std::sqrt(interval_variance) / 1'000'000.0 - } - } - }, - { - "requests", { - {"periodic", state.periodic_request_count}, - {"immediate", state.immediate_request_count}, - {"maximum_rate", state.maximum_rate_request_count}, - {"accepted", state.accepted_request_count}, - {"coalesced", 0}, - {"policy_rejected", state.policy_rejection_count}, - {"frame_slot_backpressure", state.frame_slot_backpressure_count}, - {"scene_rejected", state.scene_rejection_count}, - { - "acceptance_ratio", ratio( - state.accepted_request_count, state.request_count) - }, - { - "coalescing_ratio", ratio( - 0, state.request_count) - }, - { - "backpressure_ratio", ratio( - state.frame_slot_backpressure_count, - state.accepted_request_count) - } - } - }, - { - "latency", { - {"latest_tick_queue_ms", milliseconds(state.latest_tick_queue_ns)}, - { - "average_tick_queue_ms", milliseconds( - state.submitted_frame_count == 0 ? 0U : state.tick_queue_total_ns / state.submitted_frame_count) - }, - {"maximum_tick_queue_ms", milliseconds(state.maximum_tick_queue_ns)}, - { - "latest_completion_ms", milliseconds( - state.latest_completion_latency_ns) - }, - { - "average_completion_ms", milliseconds( - state.completed_frame_count == 0 - ? 0U - : state.completion_latency_total_ns / - state.completed_frame_count) - }, - { - "maximum_completion_ms", milliseconds( - state.maximum_completion_latency_ns) - } - } - }, - { - "last_frame", { - {"sequence", state.last_frame_sequence}, - { - "request_source", magic_enum::enum_name( - state.last_request_source) - } - } - } - }; -} -void append_statistic_json(nlohmann::json& output, - const Frame_Statistics_State& state) { - for (const auto statistic : magic_enum::enum_values()) { - if (statistic == Frame_Statistic::count) continue; - const auto& value = - state.values[static_cast(statistic)]; - if (value.count == 0) continue; - output[magic_enum::enum_name(statistic)] = { - {"count", value.count}, {"latest", value.latest}, - {"minimum", value.minimum}, {"maximum", value.maximum}, - {"average", value.average}, - {"trimmed_average", value.trimmed_average}, - {"variability", value.variability}, {"p50", value.p50}, - {"p95", value.p95}, {"p99", value.p99} - }; - } -} -void append_event_statistics_json(nlohmann::json& output, - const Event_Statistics_State& state) { - for (const auto type : magic_enum::enum_values()) { - auto& event = output[magic_enum::enum_name(type)]; - const auto& values = state.values[static_cast(type)]; - for (const auto statistic : magic_enum::enum_values()) { - if (statistic == Event_Statistic::count) continue; - const auto& value = values[static_cast(statistic)]; - if (value.count == 0) continue; - event[magic_enum::enum_name(statistic)] = { - {"count", value.count}, {"latest", value.latest}, - {"minimum", value.minimum}, {"maximum", value.maximum}, - {"average", value.average}, - {"trimmed_average", value.trimmed_average}, - {"variability", value.variability}, {"p50", value.p50}, - {"p95", value.p95}, {"p99", value.p99} - }; - } - if (event.empty()) output.erase(std::string{magic_enum::enum_name(type)}); - } -} -nlohmann::json datoviz_observation_json( - const Datoviz_Frame_Observation& value) { - const auto milliseconds = [](std::uint64_t nanoseconds) { - return static_cast(nanoseconds) / 1'000'000.0; - }; - nlohmann::json result{ - {"render_sequence", value.render_sequence}, - {"path", magic_enum::enum_name(value.path)}, - {"gpu_timing_requested", value.gpu_timing_requested}, - {"readback_requested", value.readback_requested}, - {"controller_input_applied", value.controller_input_applied}, - { - "prepare_released_after_submission", - value.prepare_released_after_submission - }, - { - "timings_ms", { - { - "queue_submit_wait", - milliseconds(value.queue_submit_wait_ns) - }, - {"target_acquire", milliseconds(value.target_acquire_ns)}, - {"structure_check", milliseconds(value.structure_check_ns)}, - {"apply", milliseconds(value.apply_ns)}, - {"query", milliseconds(value.query_ns)}, - {"runtime_plan", milliseconds(value.runtime_plan_ns)}, - {"runtime_plan_cpu", milliseconds(value.runtime_plan_cpu_ns)}, - {"runtime_execute", milliseconds(value.runtime_execute_ns)}, - {"runtime_execute_cpu", milliseconds(value.runtime_execute_cpu_ns)}, - {"mvp_update", milliseconds(value.mvp_update_ns)}, - {"frame_begin", milliseconds(value.frame_begin_ns)}, - {"frame_plan", milliseconds(value.frame_plan_ns)}, - {"frame_plan_cpu", milliseconds(value.frame_plan_cpu_ns)}, - {"emit_replay_dirty", milliseconds(value.emit_replay_dirty_ns)}, - {"emit_layout", milliseconds(value.emit_layout_ns)}, - {"emit_prepare", milliseconds(value.emit_prepare_ns)}, - {"emit_plan_build", milliseconds(value.emit_plan_build_ns)}, - {"emit_contract", milliseconds(value.emit_contract_ns)}, - {"emit_stream", milliseconds(value.emit_stream_ns)}, - {"emit_stream_freeze", milliseconds(value.emit_stream_freeze_ns)}, - {"emit_commit", milliseconds(value.emit_commit_ns)}, - {"emit_plan_reset", milliseconds(value.emit_plan_reset_ns)}, - {"emit_artifact_create", milliseconds(value.emit_artifact_create_ns)}, - {"emit_artifact_freeze", milliseconds(value.emit_artifact_freeze_ns)}, - {"emit_packet_encode", milliseconds(value.emit_packet_encode_ns)}, - {"external_register", milliseconds(value.external_register_ns)}, - {"frame_attach", milliseconds(value.frame_attach_ns)}, - {"frame_execute", milliseconds(value.frame_execute_ns)}, - {"frame_execute_cpu", milliseconds(value.frame_execute_cpu_ns)}, - {"frame_finish", milliseconds(value.frame_finish_ns)}, - {"drp_validation", milliseconds(value.drp_validation_ns)}, - {"drp_state", milliseconds(value.drp_state_ns)}, - {"drp_buffer_create", milliseconds(value.drp_buffer_create_ns)}, - {"drp_texture_create", milliseconds(value.drp_texture_create_ns)}, - {"drp_shader_create", milliseconds(value.drp_shader_create_ns)}, - {"drp_shader_compile", milliseconds(value.drp_shader_compile_ns)}, - { - "drp_shader_module_create", - milliseconds(value.drp_shader_module_create_ns) - }, - {"drp_pipeline_create", milliseconds(value.drp_pipeline_create_ns)}, - {"drp_binding_create", milliseconds(value.drp_binding_create_ns)}, - {"drp_upload", milliseconds(value.drp_upload_ns)}, - {"drp_transfer", milliseconds(value.drp_transfer_ns)}, - {"drp_record", milliseconds(value.drp_record_ns)}, - {"submit", milliseconds(value.submit_ns)}, - { - "gpu_completion_observation", - milliseconds(value.gpu_completion_observation_ns) - }, - { - "completion_task_queue", - milliseconds(value.completion_task_queue_ns) - }, - {"readback", milliseconds(value.readback_ns)} - } - }, - { - "traffic", { - {"uploaded_bytes", value.uploaded_bytes}, - {"readback_bytes", value.readback_bytes}, - {"pipeline_create_count", value.drp_pipeline_create_count} - } - }, - { - "frame_plan", { - {"resource_version", value.artifact_resource_version}, - {"frame_index", value.artifact_frame_index}, - {"status", value.artifact_status} - } - }, - { - "validation", { - {"performed", value.validation_performed}, - {"ok", value.validation_ok}, - {"code", value.validation_code}, - {"command_index", value.validation_command_index} - } - } - }; - if (value.gpu) { - result["gpu_ms"] = { - {"render", milliseconds(value.gpu->render_ns)}, - {"transition", milliseconds(value.gpu->transition_ns)}, - {"copy", milliseconds(value.gpu->copy_ns)}, - {"total", milliseconds(value.gpu->total_ns)} - }; - } - if (!value.artifact_json.empty()) result["frame_plan"]["artifact_json"] = value.artifact_json; - return result; -} -} -nlohmann::json taskflow_trace_json( - const Taskflow_Frame_Trace& trace, - const nlohmann::json& captured_components, - const nlohmann::json& captured_backend) { - nlohmann::json markers = nlohmann::json::object(); - for (const auto& marker : trace.markers) - markers[magic_enum::enum_name(marker.marker)] = - static_cast(marker.elapsed_ns) / 1'000'000.0; - nlohmann::json measurements = nlohmann::json::object(); - for (const auto& measurement : trace.measurements) - measurements[magic_enum::enum_name(measurement.measurement)] = - static_cast(measurement.value_ns) / 1'000'000.0; - nlohmann::json graphs = nlohmann::json::array(); - std::unordered_map node_ids; - for (const auto& graph : trace.graphs) { - nlohmann::json nodes = nlohmann::json::array(); - for (const auto& node : graph.nodes) { - node_ids.emplace(node.native_id, node.node_id); - nlohmann::json predecessors = nlohmann::json::array(); - for (const auto native_id : node.predecessors) predecessors.push_back(std::to_string(native_id)); - nlohmann::json successors = nlohmann::json::array(); - for (const auto native_id : node.successors) successors.push_back(std::to_string(native_id)); - nlohmann::json attributes = nlohmann::json::object(); - for (const auto& [key, value] : node.attributes) attributes[key] = value; - nlohmann::json encoded{ - {"native_id", std::to_string(node.native_id)}, {"id", node.node_id}, - {"parent_id", node.parent_node_id}, {"name", node.name}, - {"type", node.type}, {"predecessors", std::move(predecessors)}, - {"successors", std::move(successors)}, - {"attributes", std::move(attributes)} - }; - const auto owner = encoded["attributes"].value( - "owner_component", std::string{}); - if (!owner.empty() && captured_components.contains(owner)) { - const auto& captured = captured_components.at(owner); - encoded["owner"] = { - {"component", owner}, - {"label", captured.value("label", owner)}, - {"kind", captured.value("kind", std::string{})} - }; - encoded["prop"] = captured.value("prop", nlohmann::json::object()); - encoded["state"] = captured.value("state", nlohmann::json::object()); - } - nodes.push_back(std::move(encoded)); - } - graphs.push_back({ - {"stage", graph.stage}, {"name", graph.taskflow_name}, - {"submitted_ms", graph.submitted_ms}, - {"finished_ms", graph.finished_ms}, - {"completed", graph.completed}, {"nodes", std::move(nodes)} - }); - } - nlohmann::json executions = nlohmann::json::array(); - for (const auto& task : trace.tasks) { - const auto found = node_ids.find(task.native_id); - nlohmann::json cooperative_waits = nlohmann::json::array(); - for (const auto& wait : task.cooperative_waits) { - cooperative_waits.push_back({ - {"started_ms", wait.started_ms}, - {"finished_ms", wait.finished_ms}}); - } - executions.push_back({ - {"native_id", std::to_string(task.native_id)}, - {"node_id", found == node_ids.end() ? std::string{} : found->second}, - {"worker_id", task.worker_id}, - {"worker_queue_size", task.worker_queue_size}, - {"worker_queue_capacity", task.worker_queue_capacity}, - {"ready_ms", task.ready_ms}, {"entered_ms", task.entered_ms}, - {"started_ms", task.started_ms}, {"finished_ms", task.finished_ms}, - {"completed_ms", task.completed_ms}, - {"duration_ms", task.duration_ms}, - {"cooperative_wait_ms", task.cooperative_wait_ms}, - {"cooperative_waits", std::move(cooperative_waits)}, - {"observer_entry_ms", task.observer_entry_ms}, - {"observer_exit_ms", task.observer_exit_ms}, - {"queue_wait_ms", task.queue_wait_ms} - }); - } - nlohmann::json result{ - {"sequence", trace.identity.sequence}, - {"correlation_id", trace.identity.correlation_id}, - {"request_source", magic_enum::enum_name(trace.request_source)}, - {"created_time_unix_ns", trace.created_time_unix_ns}, - {"worker_count", trace.worker_count}, - {"markers", std::move(markers)}, - {"measurements", std::move(measurements)}, - {"graphs", std::move(graphs)}, - {"executions", std::move(executions)} - }; - if (!captured_backend.empty()) result["datoviz"] = captured_backend; - return result; -} -namespace { -[[nodiscard]] Event_Timeline_Time input_timeline_time( - double time_milliseconds) { - constexpr long double nanoseconds_per_millisecond{1'000'000.0L}; - constexpr long double maximum_milliseconds = - static_cast(std::numeric_limits::max()) / - nanoseconds_per_millisecond; - if (!std::isfinite(time_milliseconds) || time_milliseconds < 0.0 || - static_cast(time_milliseconds) > maximum_milliseconds) - throw std::invalid_argument( - "input time_milliseconds must be finite, non-negative and representable"); - return Event_Timeline_Time{ - static_cast( - static_cast(time_milliseconds) * - nanoseconds_per_millisecond) - }; -} -template -void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { - const auto occurred_at = input_timeline_time(input.time_milliseconds); - const auto dispatch = [&](auto event) { - scene.template submit_stream(std::move(event)); - }; - const auto apply_pointer = [&](auto& event) { - event.position = input.position; - event.global_position = input.global_position; - event.button = input.button; - event.buttons = input.buttons; - event.modifiers = input.modifiers; - }; - switch (input.type) { - case Event_Type::pointer_move: - case Event_Type::pointer_press: - case Event_Type::pointer_release: { - auto event = scene.template make_event>( - input.type, occurred_at); - apply_pointer(*event); - dispatch(std::move(event)); - break; - } - case Event_Type::wheel: { - auto event = scene.template make_event>( - occurred_at); - apply_pointer(*event); - event->pixel_delta_x = input.pixel_delta_x; - event->pixel_delta_y = input.pixel_delta_y; - event->angle_delta_x = input.angle_delta_x; - event->angle_delta_y = input.angle_delta_y; - dispatch(std::move(event)); - break; - } - case Event_Type::key_press: - case Event_Type::key_release: { - auto event = scene.template make_event( - input.type, occurred_at); - event->key = input.key; - event->native_key = input.native_key; - event->modifiers = input.modifiers; - event->auto_repeat = input.auto_repeat; - dispatch(std::move(event)); - break; - } - default: dispatch(scene.template make_event(input.type, occurred_at)); - break; - } -} -} -struct Plot::Private : Prev_Private { - using Scene = std::variant, std::unique_ptr>; - struct Stream_Snapshot { - detail::Plot_Consumer_Map consumers{}; - std::uint32_t width{}; - std::uint32_t height{}; - }; - std::unique_ptr view; - std::once_flag start_once; - std::weak_ptr lifetime{}; /* Policy 回调只借用 Plot;在途帧另行保活。 */ - struct Publication_Dispatch { - std::size_t asynchronous_feedback_count{}; - bool succeeded{}; - }; - std::atomic_uint64_t next_stream_id{1}; - Scene scene; /* Plot 独占 Scene。 */ - std::atomic> frame_policy{}; /* 模型 Private 独占当前 Policy。 */ - std::atomic_bool consumer_active{}; - std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; /* 手动帧的 Plot 动画时间原点。 */ - std::atomic_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */ - static constexpr std::size_t maximum_taskflow_trace_frames{120}; - /* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace, - * GET 直接读取已发布槽位,不复制或重排整个历史容器。 */ - std::atomic_uint64_t taskflow_trace_control{}; - std::array>, - maximum_taskflow_trace_frames> taskflow_trace_slots{}; - template - void initialize(std::unique_ptr value_scene, - std::unique_ptr value_view) { - view = std::move(value_view); - scene = std::move(value_scene); - } - void schedule(not_null object); - void consume(not_null object) noexcept; - [[nodiscard]] std::shared_ptr create_frame_policy( - Frame_Policy_Type type, double fixed_rate_fps = 30.0); - void replace_frame_policy(Frame_Policy_Type type); - void prepare_policy_frame(not_null frame, - const Frame_Production& production); - [[nodiscard]] Frame_Publication_Dispatch publish_policy_frame( - not_null frame, - const Frame_Production& production); - [[nodiscard]] nlohmann::json schema() const; - [[nodiscard]] Stream_Snapshot stream_snapshot() const; - [[nodiscard]] Publication_Dispatch publish( - std::shared_ptr frame) noexcept; - [[nodiscard]] bool mark_taskflow_trace(Render_Frame& frame); - void store_trace(std::atomic_uint64_t& control, - std::array>, - maximum_taskflow_trace_frames>& slots, - const Taskflow_Frame_Trace& trace, - const nlohmann::json& captured_components = {}, - const nlohmann::json& captured_backend = {}); - [[nodiscard]] nlohmann::json trace_response( - const std::atomic_uint64_t& control, - const std::atomic_size_t& remaining, - const std::array>, - maximum_taskflow_trace_frames>& slots) const; - void fail(std::exception_ptr failure) noexcept; -}; -void Plot::Private::schedule(not_null object) { - bool idle{}; - if (!consumer_active.compare_exchange_strong( - idle, true, std::memory_order_acq_rel, - std::memory_order_acquire)) - return; - const auto weak = object->weak_from_this(); - try { - schedule_task("plot.consume", [weak] { - const auto plot = weak.lock(); - if (!plot) return; - auto& data = double_buffer::detail::Internal_Access::layer( - plot.get()); - data.consume(not_null{plot.get()}); - }); - } - catch (...) { - consumer_active.store(false, std::memory_order_release); - fail(std::current_exception()); - } -} - -void Plot::Private::consume(not_null object) noexcept { - const auto generation = [&] { - return object->stream_submission_generation< - detail::Plot_Input_Stream_Tag>() + - object->stream_submission_generation< - detail::Plot_Render_Stream_Tag>() + - object->stream_submission_generation< - detail::Plot_Publication_Feedback_Stream_Tag>() + - object->stream_submission_generation< - detail::Plot_Control_Stream_Tag>(); - }; - auto observed_generation = generation(); - try { - for (;;) { - object->advance(); - auto& state = double_buffer::detail::Internal_Access:: - pending_state(object.get()); - std::vector> failures; - - object->exchange_stream(); - object->access_rendering_stream( - [&](std::span controls) { - for (auto& control : controls) { - std::visit([&](auto& value) { - using Value = std::remove_cvref_t; - if constexpr (std::same_as) { - state.consumers.insert_or_assign( - value.id, detail::Plot_Consumer{ - std::move(value.handler), 0, 0}); - } - else if constexpr (std::same_as) { - state.consumers.erase(value.id); - } - else if constexpr (std::same_as< - Value, - detail::Plot_Configure_Stream>) { - const auto found = state.consumers.find(value.id); - if (found == state.consumers.end()) return; - found->second.width = value.width; - found->second.height = value.height; - } - else if constexpr (std::same_as< - Value, - detail::Plot_Replace_Frame_Policy>) { - const auto current = frame_policy.load( - std::memory_order_acquire); - if (!current || state.frame_policy_switching || - current->type() == value.type) - return; - double fixed_rate_fps{30.0}; - if (const auto* fixed = dynamic_cast< - const Fixed_Rate_Frame_Policy*>( - current.get())) - fixed_rate_fps = fixed->frame_rate(); - state.frame_policy_switching = true; - const auto weak = lifetime; - current->stop( - [weak, type = value.type, - fixed_rate_fps] { - const auto owner = weak.lock(); - if (!owner) return; - owner->submit_stream< - detail::Plot_Control_Stream_Tag>( - detail::Plot_Frame_Policy_Stopped{ - type, fixed_rate_fps}); - auto& data = double_buffer::detail:: - Internal_Access::layer( - owner.get()); - data.schedule(not_null{owner.get()}); - }); - } - else if constexpr (std::same_as< - Value, - detail::Plot_Frame_Policy_Stopped>) { - auto replacement = create_frame_policy( - value.type, value.fixed_rate_fps); - frame_policy.store( - replacement, std::memory_order_release); - state.frame_policy_type = value.type; - state.frame_policy_switching = false; - replacement->start(); - } - else if constexpr (std::same_as< - Value, - detail::Plot_Failure>) { - if (state.terminal_failure || !value.description) - return; - state.terminal_failure = value.description; - failures.push_back(value.description); - } - }, control); - } - }); - - object->exchange_stream(); - object->access_rendering_stream( - [&](std::span inputs) { - for (const auto& input : inputs) { - try { - if (auto* scene_2d = std::get_if< - std::unique_ptr>(&scene)) - dispatch_plot_input(**scene_2d, input); - else - dispatch_plot_input( - *std::get>(scene), - input); - } - catch (...) { - failures.push_back( - std::make_shared( - exception_description( - std::current_exception()))); - } - } - }); - - const auto policy = frame_policy.load(std::memory_order_acquire); - object->exchange_stream(); - object->access_rendering_stream< - detail::Plot_Publication_Feedback_Stream_Tag>( - [&](std::span feedback) { - if (!policy) return; - for (auto& item : feedback) - policy->submit_publication_feedback(std::move(item)); - }); - object->exchange_stream(); - object->access_rendering_stream( - [&](std::span requests) { - if (!policy || state.terminal_failure) return; - for (auto& request : requests) - policy->request_frame(std::move(request)); - }); - - if (!failures.empty() && !state.terminal_failure) - state.terminal_failure = failures.front(); - object->publish_state(); - for (const auto& failure : failures) { - if (!failure) continue; - const auto output = std::make_shared( - Plot_Stream_Frame{ - nlohmann::json{ - {"kind", "plot_error"}, - {"protocol", "aethera.plot.stream"}, - {"version", plot_stream_protocol_version}, - {"message", *failure}} - .dump(), - {}}); - static_cast(publish(std::move(output))); - break; - } - const auto current_generation = generation(); - if (current_generation == observed_generation) { - observed_generation = current_generation; - break; - } - observed_generation = current_generation; - } - } - catch (...) { - try { - const auto failure = std::make_shared( - exception_description(std::current_exception())); - object->submit_stream( - detail::Plot_Failure{failure}); - } - catch (...) {} - } - consumer_active.store(false, std::memory_order_release); - if (generation() != observed_generation) schedule(object); -} - -void Plot::Private::fail(std::exception_ptr failure) noexcept { - try { - const auto description = std::make_shared( - exception_description(failure)); - const auto owner = lifetime.lock(); - if (!owner) return; - owner->submit_stream( - detail::Plot_Failure{description}); - schedule(not_null{owner.get()}); - } - catch (...) {} -} -nlohmann::json Plot::Private::schema() const { - auto result = view->schema(); - const auto policy = frame_policy.load(std::memory_order_acquire); - if (!policy) return result; - auto analysis = frame_policy_schema(*policy); - if (std::holds_alternative>(scene)) { - analysis["fields"].push_back({ - {"key", "pipeline_capacity"}, {"label", "3D 帧槽容量"}, - {"editor", "integer"}, {"editable", false}, - {"value", policy->get<&Frame_Policy::Prop::frame_capacity>()}, - {"description", "3D 独立策略最多允许三帧处于 Prepare、GPU 和退休阶段。"}, - {"technical_description", "3D-only submitted-driven pipeline capacity; 2D policy is unchanged."} - }); - } - const auto generator = view->data_generator_schema(); - if (!generator.is_null()) analysis["data_generator"] = generator; - result["frame_analysis"] = std::move(analysis); - return result; -} -Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const { - Stream_Snapshot result; - if (const auto owner = lifetime.lock()) - owner->access_state( - [&](const Plot::State& state) { - result.consumers = state.consumers; - }); - for (const auto& [id, consumer] : result.consumers) { - static_cast(id); - if (consumer.width == 0 || consumer.height == 0) continue; - result.width = std::max(result.width, consumer.width); - result.height = std::max(result.height, consumer.height); - } - result.width = std::clamp(result.width == 0 ? 320U : result.width, 160U, 1920U) & ~1U; - result.height = std::clamp(result.height == 0 ? 192U : result.height, 120U, 1080U) & ~1U; - return result; -} -Plot::Private::Publication_Dispatch Plot::Private::publish( - std::shared_ptr frame) noexcept { - Publication_Dispatch dispatch; - if (!frame) return dispatch; - try { - const auto snapshot = stream_snapshot(); - std::vector failed_consumers; - for (const auto& [id, consumer] : snapshot.consumers) { - if (!consumer.handler) continue; - try { - switch (consumer.handler(frame)) { - case Plot_Frame_Publication::ignored: - break; - case Plot_Frame_Publication::completed: - dispatch.succeeded = true; - break; - case Plot_Frame_Publication::asynchronous: - ++dispatch.asynchronous_feedback_count; - break; - } - } - catch (...) { - failed_consumers.push_back(id); - } - } - if (failed_consumers.empty()) return dispatch; - if (const auto owner = lifetime.lock()) { - for (const auto id : failed_consumers) - owner->submit_stream( - detail::Plot_Unsubscribe{id}); - schedule(not_null{owner.get()}); - } - } - catch (...) {} - return dispatch; -} -bool Plot::Private::mark_taskflow_trace(Render_Frame& frame) { - auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire); - while (remaining != 0) { - if (taskflow_trace_remaining.compare_exchange_weak( - remaining, remaining - 1, std::memory_order_acq_rel, - std::memory_order_acquire)) { - frame.request_taskflow_trace(); - return true; - } - } - return false; -} -void Plot::Private::store_trace( - std::atomic_uint64_t& control, - std::array>, - maximum_taskflow_trace_frames>& slots, - const Taskflow_Frame_Trace& value, - const nlohmann::json& captured_components, - const nlohmann::json& captured_backend) { - auto trace = std::make_shared( - taskflow_trace_json(value, captured_components, captured_backend)); - auto state = control.load(std::memory_order_acquire); - for (;;) { - const auto requested = static_cast(state >> 32U); - const auto captured = static_cast(state); - if (captured >= requested) return; - slots[captured].store(trace, std::memory_order_release); - const auto next = (static_cast(requested) << 32U) | - static_cast(captured + 1U); - if (control.compare_exchange_weak( - state, next, std::memory_order_release, - std::memory_order_acquire)) - return; - } -} -nlohmann::json Plot::Private::trace_response( - const std::atomic_uint64_t& control, - const std::atomic_size_t& remaining, - const std::array>, - maximum_taskflow_trace_frames>& slots) const { - nlohmann::json frames = nlohmann::json::array(); - const auto state = control.load(std::memory_order_acquire); - const auto requested = static_cast(state >> 32U); - const auto captured = static_cast(state); - for (std::uint32_t index = 0; index < captured; ++index) if (const auto trace = slots[index].load(std::memory_order_acquire)) frames.push_back(*trace); - const auto left = remaining.load(std::memory_order_acquire); - return { - {"protocol", "aethera.taskflow.frames"}, {"version", 1}, - {"requested", requested}, {"remaining", left}, - {"captured", frames.size()}, - {"complete", requested != 0 && frames.size() == requested}, - {"frames", std::move(frames)} - }; -} - -std::shared_ptr Plot::Private::create_frame_policy( - Frame_Policy_Type type, double fixed_rate_fps) { - auto* scene_base = std::visit( - [](auto& value) -> Render_Frame_Scene* { return value.get(); }, - scene); - const bool is_3d = - std::holds_alternative>(scene); - Frame_Policy::Dependencies dependencies{ - .scene = not_null{scene_base}, - .create_frame = [is_3d]() -> std::unique_ptr { - if (is_3d) - return std::make_unique(Frame_Identity{}); - return std::make_unique(Frame_Identity{}); - }, - .prepare_frame = [weak = lifetime]( - not_null frame, - const Frame_Production& production) { - if (const auto owner = weak.lock()) - double_buffer::detail::Internal_Access::layer( - owner.get()).prepare_policy_frame(frame, production); - }, - .publish_frame = [weak = lifetime]( - not_null frame, - const Frame_Production& production) { - if (const auto owner = weak.lock()) - return double_buffer::detail::Internal_Access::layer( - owner.get()).publish_policy_frame(frame, production); - return Frame_Publication_Dispatch{}; - }, - .retain_owner = [weak = lifetime]() -> std::shared_ptr { - return weak.lock(); - }, - .report_failure = [weak = lifetime](std::exception_ptr failure) { - if (const auto owner = weak.lock()) - double_buffer::detail::Internal_Access::layer( - owner.get()).fail(std::move(failure)); - }, - .frame_capacity = 3}; - switch (type) { - case Frame_Policy_Type::manual: - return std::make_shared( - std::move(dependencies)); - case Frame_Policy_Type::fixed_rate: - return std::make_shared( - std::move(dependencies), fixed_rate_fps); - case Frame_Policy_Type::maximum_rate: - return std::make_shared( - std::move(dependencies)); - } - throw std::logic_error("unknown Frame Policy type"); -} - -void Plot::Private::replace_frame_policy(Frame_Policy_Type type) { - const auto owner = lifetime.lock(); - if (!owner) return; - owner->submit_stream( - detail::Plot_Replace_Frame_Policy{type}); - schedule(not_null{owner.get()}); -} - -void Plot::Private::prepare_policy_frame( - not_null frame, - const Frame_Production& production) { - auto request = production.request; - const auto streams = stream_snapshot(); - if (!streams.consumers.empty()) { - request.width = streams.width; - request.height = streams.height; - } - else { - request.width = std::clamp(request.width, 160U, 1920U) & ~1U; - request.height = std::clamp(request.height, 120U, 1080U) & ~1U; - } - const Frame_Identity identity{ - production.sequence, - request.sequence == 0 ? production.sequence : request.sequence}; - if (auto* output = dynamic_cast(frame.get())) { - output->begin(identity, Frame_2D::native_pixel_format, - request.source); - std::get>(scene)->set< - &Render_Scene_2D::Prop::viewport>(Size{ - static_cast(request.width), - static_cast(request.height)}); - } - else if (auto* output = dynamic_cast(frame.get())) { - const auto policy = frame_policy.load(std::memory_order_acquire); - if (!policy) throw std::logic_error("Plot has no Frame Policy"); - output->begin( - identity, - policy->get<&Frame_Policy::Prop::pixel_delivery_enabled>() - ? Frame_3D_Output::pixels - : Frame_3D_Output::diagnostics, - Frame_3D::native_pixel_format, request.source); - std::get>(scene)->set< - &Render_Scene_3D::Prop::viewport>( - Extent{request.width, request.height}); - } - else { - throw std::logic_error( - "Plot Policy created an unsupported Render_Frame type"); - } - static_cast(mark_taskflow_trace(*frame)); - frame->mark(Frame_Trace_Marker::plot_update_started); - const auto update_started = std::chrono::steady_clock::now(); - view->update(request); - const auto update_elapsed = - std::chrono::steady_clock::now() - update_started; - frame->mark(Frame_Trace_Marker::plot_update_finished); - const auto tick_queue_elapsed = - request.issued_at.time_since_epoch().count() == 0 - ? std::chrono::steady_clock::duration::zero() - : update_started - request.issued_at; - const auto nanoseconds = [](std::chrono::steady_clock::duration value) { - return static_cast(std::max( - 0, std::chrono::duration_cast(value) - .count())); - }; - frame->record(Frame_Trace_Measurement::plot_tick_queue_ns, - nanoseconds(tick_queue_elapsed)); - frame->record(Frame_Trace_Measurement::plot_update_ns, - nanoseconds(update_elapsed)); -} - -Frame_Publication_Dispatch Plot::Private::publish_policy_frame( - not_null frame, - const Frame_Production& production) { - const auto policy = frame_policy.load(std::memory_order_acquire); - if (!policy) throw std::logic_error("Plot has no Frame Policy"); - const auto identity = frame->identity(); - Frame_Identity rendered_identity = identity; - std::shared_ptr> pixel_storage; - Plot_Pixel_Layout pixel_layout{Plot_Pixel_Layout::rgba8}; - std::uint32_t width{}; - std::uint32_t height{}; - std::optional datoviz_observation; - if (auto* output = dynamic_cast(frame.get())) { - pixel_layout = Plot_Pixel_Layout::bgra8; - const auto image = output->image(); - width = static_cast(std::max(0, image.width)); - height = static_cast(std::max(0, image.height)); - if (policy->get<&Frame_Policy::Prop::pixel_delivery_enabled>()) { - auto pixels = output->output_pixels(); - width = static_cast(pixels.width); - height = static_cast(pixels.height); - pixel_storage = - std::make_shared>( - std::move(pixels.bytes)); - } - } - else if (auto* output = dynamic_cast(frame.get())) { - rendered_identity = output->rendered_identity(); - const auto extent = output->extent(); - width = extent.width; - height = extent.height; - if (policy->get<&Frame_Policy::Prop::pixel_delivery_enabled>() && - output->output() == Frame_3D_Output::pixels) - pixel_storage = output->share_pixels(); - datoviz_observation = output->take_datoviz_observation(); - } - const auto presentation_time = - std::chrono::duration_cast( - std::chrono::duration( - production.request.time_milliseconds)); - auto pixels = std::make_shared(Plot_Pixel_Frame{ - std::move(pixel_storage), pixel_layout, presentation_time, - identity.sequence, identity.correlation_id, - rendered_identity.sequence, rendered_identity.correlation_id, - width, height}); - auto output = std::make_shared( - Plot_Stream_Frame{{}, std::move(pixels)}); - const auto started_at = std::chrono::steady_clock::now(); - const auto dispatch = publish(std::move(output)); - const auto completed_at = std::chrono::steady_clock::now(); - frame->record( - Frame_Trace_Measurement::plot_publish_ns, - static_cast(std::max( - 0, std::chrono::duration_cast( - completed_at - started_at).count()))); - if (frame->taskflow_trace_requested()) { - auto trace = frame->take_taskflow_trace(); - std::unordered_set executed_nodes; - for (const auto& execution : trace.tasks) - executed_nodes.insert(execution.native_id); - std::vector executed_components; - for (const auto& graph : trace.graphs) { - for (const auto& node : graph.nodes) { - if (!executed_nodes.contains(node.native_id)) continue; - const auto owner = std::ranges::find( - node.attributes, "owner_component", - &std::pair::first); - if (owner == node.attributes.end() || owner->second.empty() || - std::ranges::find(executed_components, owner->second) != - executed_components.end()) - continue; - executed_components.push_back(owner->second); - } - } - auto components = view->capture_components(executed_components); - nlohmann::json backend; - if (datoviz_observation) - backend = datoviz_observation_json(*datoviz_observation); - store_trace(taskflow_trace_control, taskflow_trace_slots, trace, - components, backend); - } - return Frame_Publication_Dispatch{ - .started_at = started_at, - .completed_at = completed_at, - .asynchronous_feedback_count = - dispatch.asynchronous_feedback_count, - .succeeded = dispatch.succeeded, - .pixel_width = width, - .pixel_height = height}; -} - -Plot::Plot(std::unique_ptr scene, - std::unique_ptr view) { - auto attached = std::make_unique< - double_buffer::detail::Attached_Private>( - this, memory_resource()); - d = attached.release(); - auto& data = double_buffer::detail::Internal_Access::layer(this); - data.bind_private_crtp(this); - bind_object_crtp(); - data.initialize(std::move(scene), std::move(view)); - advance(); -} -Plot::Plot(std::unique_ptr scene, - std::unique_ptr view) { - auto attached = std::make_unique< - double_buffer::detail::Attached_Private>( - this, memory_resource()); - d = attached.release(); - auto& data = double_buffer::detail::Internal_Access::layer(this); - data.bind_private_crtp(this); - bind_object_crtp(); - data.initialize(std::move(scene), std::move(view)); - double_buffer::detail::Internal_Access::pending_prop(this). - initial_policy = Frame_Policy_Type::maximum_rate; - advance(); -} -Plot::~Plot() = default; -void Plot::ensure_started() { - auto& data = double_buffer::detail::Internal_Access::layer(this); - std::call_once(data.start_once, [this, &data] { - data.lifetime = weak_from_this(); - const auto type = get<&Plot::Prop::initial_policy>(); - auto policy = data.create_frame_policy(type); - data.frame_policy.store(policy, std::memory_order_release); - auto& state = double_buffer::detail::Internal_Access::pending_state(this); - state.frame_policy_type = type; - publish_state(); - policy->start(); - }); -} -Plot::Stream_Id Plot::subscribe(Stream_Handler handler) { - if (!handler) throw std::invalid_argument("Plot subscription requires a handler"); - ensure_started(); - auto& data = double_buffer::detail::Internal_Access::layer(this); - const auto id = data.next_stream_id.fetch_add(1, std::memory_order_relaxed); - const auto notification = handler; - submit_stream( - detail::Plot_Subscribe{id, std::move(handler)}); - data.schedule(not_null{this}); - const auto failure = read_state().terminal_failure; - if (failure) { - try { - notification(std::make_shared( - Plot_Stream_Frame{ - nlohmann::json{ - {"kind", "plot_error"}, - {"protocol", "aethera.plot.stream"}, - {"version", plot_stream_protocol_version}, - {"message", failure ? *failure : "Plot unavailable"} - }.dump(), - {} - })); - } - catch (...) { - unsubscribe(id); - } - } - return id; -} -void Plot::unsubscribe(Stream_Id stream) { - ensure_started(); - submit_stream( - detail::Plot_Unsubscribe{stream}); - double_buffer::detail::Internal_Access::layer(this). - schedule(not_null{this}); -} -void Plot::configure_stream(Stream_Id stream, std::uint32_t width, - std::uint32_t height) { - ensure_started(); - submit_stream( - detail::Plot_Configure_Stream{stream, width, height}); - double_buffer::detail::Internal_Access::layer(this). - schedule(not_null{this}); -} -void Plot::schedule_render(Frame_Request request) { - if (!std::isfinite(request.time_milliseconds) || - request.time_milliseconds < 0.0) - throw std::invalid_argument( - "render time_milliseconds must be finite and non-negative"); - if (request.width == 0 || request.height == 0) throw std::invalid_argument("render viewport must be non-zero"); - ensure_started(); - if (read_state().terminal_failure) return; - submit_stream(std::move(request)); - double_buffer::detail::Internal_Access::layer(this). - schedule(not_null{this}); -} -void Plot::submit_publication_feedback( - Frame_Publication_Feedback feedback) { - ensure_started(); - if (read_state().terminal_failure) return; - submit_stream( - std::move(feedback)); - double_buffer::detail::Internal_Access::layer(this). - schedule(not_null{this}); -} -void Plot::render_once() { - ensure_started(); - const auto now = std::chrono::steady_clock::now(); - const auto& data = double_buffer::detail::Internal_Access::layer(this); - const auto elapsed = now - data.clock_origin; - schedule_render(Frame_Request{ - .issued_at = now, - .time_milliseconds = - std::chrono::duration(elapsed).count(), - .source = Frame_Request_Source::immediate - }); -} -void Plot::submit_input(Plot_Input_Event event) { - static_cast(input_timeline_time(event.time_milliseconds)); - ensure_started(); - if (read_state().terminal_failure) return; - submit_stream(std::move(event)); - double_buffer::detail::Internal_Access::layer(this). - schedule(not_null{this}); -} -nlohmann::json Plot::schema() { - ensure_started(); - return double_buffer::detail::Internal_Access::layer(this).schema(); -} -nlohmann::json Plot::write_prop(std::string_view component, - std::string_view key, - const nlohmann::json& value) { - ensure_started(); - auto& data = double_buffer::detail::Internal_Access::layer(this); - if (component != "frame-analysis") - return data.view->write_prop(component, key, value); - if (key == "pacing_mode") { - if (!value.is_string()) - return {{"success", false}, - {"error", "pacing_mode requires a string"}}; - const auto parsed = parse_pacing_mode( - value.get_ref()); - if (!parsed) - return {{"success", false}, - {"error", "unknown frame pacing mode"}}; - data.replace_frame_policy(*parsed); - return {{"success", true}, - {"component", "frame-analysis"}, - {"key", key}, - {"value", pacing_mode_name(*parsed)}}; - } - const auto policy = data.frame_policy.load(std::memory_order_acquire); - if (!policy) return {{"success", false}, {"error", "Plot has no Frame Policy"}}; - return write_frame_policy_prop(*policy, key, value); -} -nlohmann::json Plot::component_state(std::string_view component) const { - return double_buffer::detail::Internal_Access::layer(this). - view->component_state(component); -} -nlohmann::json Plot::generate_data(const nlohmann::json& input) { - ensure_started(); - return double_buffer::detail::Internal_Access::layer(this). - view->generate_data(input); -} -nlohmann::json Plot::diagnostics() const { - const auto& data = double_buffer::detail::Internal_Access::layer(this); - const auto policy = data.frame_policy.load(std::memory_order_acquire); - if (!policy) return {{"protocol", "aethera.plot.diagnostics"}, - {"version", 4}, {"available", false}}; - nlohmann::json frame_statistics = nlohmann::json::object(); - nlohmann::json input_statistics = nlohmann::json::object(); - Frame_Identity identity{}; - std::uint64_t created_time_unix_ns{}; - std::uint64_t dropped_sequences{}; - std::uint32_t completed_width{}; - std::uint32_t completed_height{}; - double frame_rate{}; - bool is_3d{}; - const auto read_scene_statistics = [&](const auto& state) { - append_event_statistics_json(input_statistics, state.event_statistics); - }; - std::visit([&](const auto& scene) { - using Scene_Pointer = std::remove_cvref_t; - if constexpr (std::same_as>) { - scene->template access_state( - read_scene_statistics); - } - else { - is_3d = true; - scene->template access_state( - read_scene_statistics); - } - }, data.scene); - Frame_Policy_State pacing{}; - policy->access_state( - [&](const Frame_Policy::State& state) { - pacing = state.observation; - }); - const auto& statistics = pacing.frame_statistics; - append_statistic_json(frame_statistics, statistics); - identity = statistics.identity; - created_time_unix_ns = statistics.created_time_unix_ns; - dropped_sequences = statistics.dropped_sequences; - completed_width = pacing.pixel_width; - completed_height = pacing.pixel_height; - const auto& interval = statistics.values[ - static_cast(Frame_Statistic::frame_interval_ms)]; - frame_rate = interval.trimmed_average > 0.0 - ? 1'000.0 / interval.trimmed_average - : 0.0; - const auto stream = data.stream_snapshot(); - const auto lifecycle_name = magic_enum::enum_name( - policy->read_state().lifecycle); - nlohmann::json supported_formats = nlohmann::json::array(); - if (is_3d) { - for (const auto format : Frame_3D::supported_pixel_formats) supported_formats.push_back(pixel_format_name(format)); - } - else { - for (const auto format : Frame_2D::supported_pixel_formats) supported_formats.push_back(pixel_format_name(format)); - } - const auto format = is_3d - ? pixel_format_name(Frame_3D::native_pixel_format) - : pixel_format_name(Frame_2D::native_pixel_format); - const auto native_format = is_3d - ? pixel_format_name(Frame_3D::native_pixel_format) - : pixel_format_name(Frame_2D::native_pixel_format); - const auto pixel_width = completed_width == 0 - ? stream.width - : completed_width; - const auto pixel_height = completed_height == 0 - ? stream.height - : completed_height; - const std::size_t byte_length = policy->get< - &Frame_Policy::Prop::pixel_delivery_enabled>() - ? static_cast(pixel_width) * pixel_height * 4U - : 0U; - nlohmann::json output{ - {"protocol", "aethera.plot.diagnostics"}, {"version", 4}, - {"dimension", is_3d ? "3D" : "2D"}, - {"sequence", identity.sequence}, - {"correlation_id", identity.correlation_id}, - {"rendered_sequence", identity.sequence}, - {"rendered_correlation_id", identity.correlation_id}, - { - "generated_time_unix_ms", - static_cast(created_time_unix_ns) / 1'000'000.0 - }, - {"delivery", policy->get< - &Frame_Policy::Prop::pixel_delivery_enabled>() - ? "gallery-pixels" : "diagnostics"}, - {"frame_rate_fps", frame_rate}, - {"dropped_sequence_count", dropped_sequences}, - {"window_capacity", diagnostic_window_capacity}, - { - "pixel", { - {"width", pixel_width}, {"height", pixel_height}, - {"format", format}, {"native_format", native_format}, - {"supported_formats", std::move(supported_formats)}, - {"byte_length", byte_length} - } - }, - {"frame_policy", frame_policy_state_json(pacing, *policy)}, - {"frame_lifecycle", lifecycle_name}, - {"frame_statistics", std::move(frame_statistics)}, - {"input_statistics", std::move(input_statistics)} - }; - if (is_3d) { - output["frame_policy"]["three_dimensional_pipeline"] = { - {"capacity", policy->get<&Frame_Policy::Prop::frame_capacity>()}, - {"overlapped_release_count", pacing.overlapped_release_count}, - { - "completion_gated_release_count", - pacing.completion_gated_release_count - }, - {"gpu_completion_count", pacing.completed_frame_count}, - {"last_completed_sequence", pacing.last_frame_sequence} - }; - const auto& gpu = render_3d::detail::Gpu_Completion_Service::instance(). - read_state(); - const auto milliseconds = [](std::uint64_t nanoseconds) { - return static_cast(nanoseconds) / 1'000'000.0; - }; - output["gpu_completion_domain"] = { - {"capacity", gpu.capacity}, {"in_flight", gpu.in_flight}, - {"peak_in_flight", gpu.peak_in_flight}, {"watched", gpu.watched}, - {"peak_watched", gpu.peak_watched}, - {"active_fences", gpu.active_fences}, - {"pending_fences", gpu.pending_fences}, - {"reservation_count", gpu.reservation_count}, - {"completion_count", gpu.completion_count}, - {"cancellation_count", gpu.cancellation_count}, - {"fence_probe_count", gpu.fence_probe_count}, - {"fence_wait_count", gpu.fence_wait_count}, - {"fence_wait_timeout_count", gpu.fence_wait_timeout_count}, - {"fence_wait_total_ms", milliseconds(gpu.fence_wait_total_ns)}, - {"fence_wait_max_ms", milliseconds(gpu.fence_wait_max_ns)}, - {"callback_total_ms", milliseconds(gpu.callback_total_ns)}, - {"callback_max_ms", milliseconds(gpu.callback_max_ns)}, - {"callback_failure_count", gpu.callback_failure_count}, - {"backpressure_count", gpu.backpressure_count}, - {"fault_count", gpu.fault_count}, - {"abandoned_count", gpu.abandoned_count} - }; - } - if (const auto failure = read_state().terminal_failure) - output["terminal_failure"] = *failure; - return output; -} -void Plot::request_taskflow_trace(std::size_t frame_count) { - if (frame_count == 0 || - frame_count > Private::maximum_taskflow_trace_frames) - throw std::invalid_argument("Taskflow trace frame_count must be between 1 and 120"); - ensure_started(); - auto& data = double_buffer::detail::Internal_Access::layer(this); - auto control = data.taskflow_trace_control.load(std::memory_order_acquire); - for (;;) { - const auto requested = static_cast(control >> 32U); - const auto captured = static_cast(control); - if (requested != captured) throw std::logic_error("A Taskflow frame trace request is already active"); - const auto next = static_cast(frame_count) << 32U; - if (data.taskflow_trace_control.compare_exchange_weak( - control, next, std::memory_order_release, - std::memory_order_acquire)) - break; - } - for (auto& slot : data.taskflow_trace_slots) - slot.store({}, std::memory_order_release); - data.taskflow_trace_remaining.store(frame_count, std::memory_order_release); -} -nlohmann::json Plot::taskflow_trace() const { - const auto& data = double_buffer::detail::Internal_Access::layer(this); - return data.trace_response(data.taskflow_trace_control, - data.taskflow_trace_remaining, - data.taskflow_trace_slots); -} -void Plot::reset_diagnostics() { - auto& data = double_buffer::detail::Internal_Access::layer(this); - std::visit([](auto& scene) { - scene->template update_state<&Scene::State::event_statistics>( - Event_Statistics_State{}); - }, data.scene); - if (const auto policy = data.frame_policy.load(std::memory_order_acquire)) - policy->reset_statistics(); -} -} diff --git a/mcp/core/runtime/Plot.hpp b/mcp/core/runtime/Plot.hpp deleted file mode 100644 index 9330379..0000000 --- a/mcp/core/runtime/Plot.hpp +++ /dev/null @@ -1,172 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace aethera::web { -struct Plot_Input_Event { - Event_Type type{Event_Type::pointer_move}; /* 输入事件业务类型。 */ - double time_milliseconds{}; /* 输入生产者单调时间线上的事件发生时刻。 */ - render_2d::Point_F position{}; /* Plot 像素坐标。 */ - render_2d::Point_F global_position{}; /* 浏览器屏幕像素坐标。 */ - Mouse_Button button{Mouse_Button::none}; /* 本次变化涉及的鼠标按键。 */ - Mouse_Button_Mask buttons{}; /* 事件产生时保持按下的按键集合。 */ - Keyboard_Modifier modifiers{Keyboard_Modifier::none}; /* 事件产生时的修饰键集合。 */ - double pixel_delta_x{}; /* 水平高精度滚轮增量。 */ - double pixel_delta_y{}; /* 垂直高精度滚轮增量。 */ - double angle_delta_x{}; /* 水平离散滚轮增量。 */ - double angle_delta_y{}; /* 垂直离散滚轮增量。 */ - Key key{Key::unknown}; /* 标准化键盘按键。 */ - std::uint32_t native_key{}; /* 浏览器原生按键码。 */ - bool auto_repeat{}; /* 是否为系统重复按键。 */ -}; -enum struct Plot_Pixel_Layout : std::uint8_t { - bgra8, - rgba8 -}; -struct Plot_Pixel_Frame { - std::shared_ptr> pixels{}; /* 可选的不可变原生像素;停传输时为空但完成身份仍有效。 */ - Plot_Pixel_Layout layout{Plot_Pixel_Layout::rgba8}; /* 像素真实通道布局,由产生帧的后端标注。 */ - std::chrono::microseconds presentation_time{}; /* 页面媒体时钟上的显示时间戳。 */ - std::uint64_t sequence{}; /* 对应 Plot 完成帧的局部单调序号。 */ - std::uint64_t correlation_id{}; /* 触发本帧的页面级时钟序号;手动帧等于局部序号。 */ - std::uint64_t rendered_sequence{}; /* 实际产生当前像素的 Scene/GPU 提交帧序号。 */ - std::uint64_t rendered_correlation_id{}; /* 实际画面对应的页面级时钟序号。 */ - std::uint32_t width{}; /* 原生像素宽度。 */ - std::uint32_t height{}; /* 原生像素高度。 */ -}; -struct Plot_Stream_Frame { - std::string notification{}; /* 仅用于终止错误等低频控制通知;正常帧为空。 */ - std::shared_ptr pixels{}; /* 每帧完成进度及其可选图集像素。 */ -}; -enum struct Plot_Frame_Publication : std::uint8_t { - ignored, /* 订阅者不消费本像素帧,不参与发布反馈。 */ - completed, /* 订阅回调返回时已同步完成业务消费。 */ - asynchronous /* 订阅者已接管发布,并会执行帧策略反馈任务。 */ -}; -using Plot_Stream_Id = std::uint64_t; -using Plot_Stream_Handler = std::function)>; - -namespace detail { -struct Plot_Input_Stream_Tag {}; -struct Plot_Render_Stream_Tag {}; -struct Plot_Publication_Feedback_Stream_Tag {}; -struct Plot_Control_Stream_Tag {}; - -struct Plot_Consumer { - Plot_Stream_Handler handler{}; - std::uint32_t width{}; - std::uint32_t height{}; -}; -using Plot_Consumer_Map = std::unordered_map; -struct Plot_Subscribe { - Plot_Stream_Id id{}; - Plot_Stream_Handler handler{}; -}; -struct Plot_Unsubscribe { Plot_Stream_Id id{}; }; -struct Plot_Configure_Stream { - Plot_Stream_Id id{}; - std::uint32_t width{}; - std::uint32_t height{}; -}; -struct Plot_Replace_Frame_Policy { Frame_Policy_Type type{}; }; -struct Plot_Frame_Policy_Stopped { - Frame_Policy_Type type{}; - double fixed_rate_fps{30.0}; -}; -struct Plot_Failure { std::shared_ptr description{}; }; -using Plot_Control = std::variant< - std::monostate, - Plot_Subscribe, - Plot_Unsubscribe, - Plot_Configure_Stream, - Plot_Replace_Frame_Policy, - Plot_Frame_Policy_Stopped, - Plot_Failure>; -} - -struct Plot final : - double_buffer::Def< - Plot, - double_buffer::Root, - double_buffer::Mpmc_Triple_Buffer< - detail::Plot_Input_Stream_Tag, Plot_Input_Event>, - double_buffer::Mpmc_Triple_Buffer< - detail::Plot_Render_Stream_Tag, Frame_Request>, - double_buffer::Mpmc_Triple_Buffer< - detail::Plot_Publication_Feedback_Stream_Tag, - Frame_Publication_Feedback>, - double_buffer::Mpmc_Triple_Buffer< - detail::Plot_Control_Stream_Tag, detail::Plot_Control>>, - public std::enable_shared_from_this { -public: - using Stream_Id = Plot_Stream_Id; - using Stream_Handler = Plot_Stream_Handler; - using Json_Handler = std::function; - struct Prop : Prev_Prop { - Frame_Policy_Type initial_policy{Frame_Policy_Type::fixed_rate}; - bool operator==(const Prop&) const = default; - }; - struct State : Prev_State { - detail::Plot_Consumer_Map consumers{}; - std::shared_ptr terminal_failure{}; - Frame_Policy_Type frame_policy_type{Frame_Policy_Type::fixed_rate}; - bool frame_policy_switching{}; - }; - struct Private; - struct Scene_View { - public: - virtual ~Scene_View() = default; - [[nodiscard]] virtual nlohmann::json schema() const = 0; - [[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 capture_components( - const std::vector& components) 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 Frame_Request& request) = 0; - }; - Plot(std::unique_ptr scene, - std::unique_ptr view); - Plot(std::unique_ptr scene, - std::unique_ptr view); - ~Plot(); - Plot(const Plot&) = delete; - Plot& operator=(const Plot&) = delete; - [[nodiscard]] Stream_Id subscribe(Stream_Handler handler); - void unsubscribe(Stream_Id stream); - void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height); - void schedule_render(Frame_Request request); - void submit_publication_feedback(Frame_Publication_Feedback feedback); - void render_once(); - void submit_input(Plot_Input_Event event); - [[nodiscard]] nlohmann::json schema(); - [[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。 */ - void request_taskflow_trace(std::size_t frame_count); - [[nodiscard]] nlohmann::json taskflow_trace() const; - void reset_diagnostics(); -private: - void ensure_started(); -}; -} diff --git a/mcp/core/runtime/Taskflow_Trace_Json.cpp b/mcp/core/runtime/Taskflow_Trace_Json.cpp new file mode 100644 index 0000000..445cc7a --- /dev/null +++ b/mcp/core/runtime/Taskflow_Trace_Json.cpp @@ -0,0 +1,101 @@ +#include "Taskflow_Trace_Json.hpp" +#include +#include +#include +#include + +namespace aethera::web { + +nlohmann::json taskflow_trace_json( + const Taskflow_Frame_Trace& trace, + const nlohmann::json& captured_components, + const nlohmann::json& captured_backend) { + nlohmann::json markers = nlohmann::json::object(); + for (const auto& marker : trace.markers) + markers[magic_enum::enum_name(marker.marker)] = + static_cast(marker.elapsed_ns) / 1'000'000.0; + nlohmann::json measurements = nlohmann::json::object(); + for (const auto& measurement : trace.measurements) + measurements[magic_enum::enum_name(measurement.measurement)] = + static_cast(measurement.value_ns) / 1'000'000.0; + nlohmann::json graphs = nlohmann::json::array(); + std::unordered_map node_ids; + for (const auto& graph : trace.graphs) { + nlohmann::json nodes = nlohmann::json::array(); + for (const auto& node : graph.nodes) { + node_ids.emplace(node.native_id, node.node_id); + nlohmann::json predecessors = nlohmann::json::array(); + for (const auto native_id : node.predecessors) + predecessors.push_back(std::to_string(native_id)); + nlohmann::json successors = nlohmann::json::array(); + for (const auto native_id : node.successors) + successors.push_back(std::to_string(native_id)); + nlohmann::json attributes = nlohmann::json::object(); + for (const auto& [key, value] : node.attributes) + attributes[key] = value; + nlohmann::json encoded{ + {"native_id", std::to_string(node.native_id)}, + {"id", node.node_id}, {"parent_id", node.parent_node_id}, + {"name", node.name}, {"type", node.type}, + {"predecessors", std::move(predecessors)}, + {"successors", std::move(successors)}, + {"attributes", std::move(attributes)}}; + const auto owner = encoded["attributes"].value( + "owner_component", std::string{}); + if (!owner.empty() && captured_components.contains(owner)) { + const auto& captured = captured_components.at(owner); + encoded["owner"] = { + {"component", owner}, + {"label", captured.value("label", owner)}, + {"kind", captured.value("kind", std::string{})}}; + encoded["prop"] = captured.value( + "prop", nlohmann::json::object()); + encoded["state"] = captured.value( + "state", nlohmann::json::object()); + } + nodes.push_back(std::move(encoded)); + } + graphs.push_back({ + {"stage", graph.stage}, {"name", graph.taskflow_name}, + {"submitted_ms", graph.submitted_ms}, + {"finished_ms", graph.finished_ms}, + {"completed", graph.completed}, {"nodes", std::move(nodes)}}); + } + nlohmann::json executions = nlohmann::json::array(); + for (const auto& task : trace.tasks) { + const auto found = node_ids.find(task.native_id); + nlohmann::json waits = nlohmann::json::array(); + for (const auto& wait : task.cooperative_waits) + waits.push_back({{"started_ms", wait.started_ms}, + {"finished_ms", wait.finished_ms}}); + executions.push_back({ + {"native_id", std::to_string(task.native_id)}, + {"node_id", found == node_ids.end() ? std::string{} : found->second}, + {"worker_id", task.worker_id}, + {"worker_queue_size", task.worker_queue_size}, + {"worker_queue_capacity", task.worker_queue_capacity}, + {"ready_ms", task.ready_ms}, {"entered_ms", task.entered_ms}, + {"started_ms", task.started_ms}, {"finished_ms", task.finished_ms}, + {"completed_ms", task.completed_ms}, + {"duration_ms", task.duration_ms}, + {"cooperative_wait_ms", task.cooperative_wait_ms}, + {"cooperative_waits", std::move(waits)}, + {"observer_entry_ms", task.observer_entry_ms}, + {"observer_exit_ms", task.observer_exit_ms}, + {"queue_wait_ms", task.queue_wait_ms}}); + } + nlohmann::json result{ + {"sequence", trace.identity.sequence}, + {"correlation_id", trace.identity.correlation_id}, + {"request_source", magic_enum::enum_name(trace.request_source)}, + {"created_time_unix_ns", trace.created_time_unix_ns}, + {"worker_count", trace.worker_count}, + {"markers", std::move(markers)}, + {"measurements", std::move(measurements)}, + {"graphs", std::move(graphs)}, + {"executions", std::move(executions)}}; + if (!captured_backend.empty()) result["datoviz"] = captured_backend; + return result; +} + +} diff --git a/mcp/core/runtime/Taskflow_Trace_Json.hpp b/mcp/core/runtime/Taskflow_Trace_Json.hpp index 39db0b0..4ebdbda 100644 --- a/mcp/core/runtime/Taskflow_Trace_Json.hpp +++ b/mcp/core/runtime/Taskflow_Trace_Json.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include namespace aethera::web { [[nodiscard]] nlohmann::json taskflow_trace_json( diff --git a/mcp/tests/Control_Path_Benchmarks.cpp b/mcp/tests/Control_Path_Benchmarks.cpp index a560622..8e45951 100644 --- a/mcp/tests/Control_Path_Benchmarks.cpp +++ b/mcp/tests/Control_Path_Benchmarks.cpp @@ -176,25 +176,27 @@ struct Concurrent_Workload_State { std::uint64_t sequence) { Plot_Input_Request request; request.plot = configuration.plot_ids[plot_index]; - request.event.time_milliseconds = 1'000.0 + - static_cast(sequence) * - (1'000.0 / static_cast(configuration.input_rate_hz)); - request.event.position = { - 360.0 + static_cast( - static_cast((sequence + plot_index * 7U) % 121U) - 60), - 210.0 + static_cast( - static_cast((sequence * 3U + plot_index * 11U) % 81U) - 40)}; - request.event.global_position = request.event.position; + const nlohmann::json position{ + {"x", 360.0 + static_cast( + static_cast((sequence + plot_index * 7U) % 121U) - 60)}, + {"y", 210.0 + static_cast( + static_cast((sequence * 3U + plot_index * 11U) % 81U) - 40)}}; + request.event = { + {"time_milliseconds", 1'000.0 + static_cast(sequence) * + (1'000.0 / static_cast(configuration.input_rate_hz))}, + {"position", position}, {"global_position", position} + }; if (workload == Input_Workload::wheel || (workload == Input_Workload::mixed && (plot_index & 1U) != 0U)) { - request.event.type = Event_Type::wheel; - request.event.pixel_delta_y = (sequence & 1U) != 0U ? 120.0 : -120.0; - request.event.angle_delta_y = request.event.pixel_delta_y; + const auto delta = (sequence & 1U) != 0U ? 120.0 : -120.0; + request.event["type"] = "wheel"; + request.event["pixel_delta_y"] = delta; + request.event["angle_delta_y"] = delta; return request; } - request.event.type = Event_Type::pointer_move; - request.event.button = Mouse_Button::left; - request.event.buttons = 1; + request.event["type"] = "pointer_move"; + request.event["button"] = "left"; + request.event["buttons"] = 1; return request; } @@ -204,50 +206,40 @@ public: if (shared_service) { service = shared_service; workload = shared_workload; - plots = shared_plots; - streams = shared_streams; return; } - shared_service = Control_Service::create(); shared_workload = std::make_shared(); shared_workload->completed.reserve(configuration.plot_ids.size()); - shared_plots.reserve(configuration.plot_ids.size()); - shared_streams.reserve(configuration.plot_ids.size()); for (std::size_t index = 0; index < configuration.plot_ids.size(); ++index) { - auto plot = shared_service->find_plot(configuration.plot_ids[index]); - if (!plot) - throw std::logic_error("configured benchmark Plot is unavailable"); auto completed = std::make_shared(0); shared_workload->completed.push_back(completed); - const auto stream = plot->subscribe( - [completed = std::move(completed)]( - std::shared_ptr frame) { - if (!frame || !frame->pixels) - return web::Plot_Frame_Publication::ignored; - completed->fetch_add( - 1, std::memory_order_relaxed); - return web::Plot_Frame_Publication::completed; - }); - plot->configure_stream( - stream, configuration.width, configuration.height); - shared_plots.push_back(std::move(plot)); - shared_streams.push_back(stream); } + shared_service = Control_Service::create(Gallery_Output{ + .width = configuration.width, + .height = configuration.height, + .publish = [workload = shared_workload]( + std::string_view id, + std::shared_ptr frame) { + const auto found = std::ranges::find( + configuration.plot_ids, id); + if (found == configuration.plot_ids.end() || !frame || + !frame->pixels) + return web::Gallery_Frame_Publication::ignored; + const auto index = static_cast( + found - configuration.plot_ids.begin()); + workload->completed[index]->fetch_add( + 1, std::memory_order_relaxed); + return web::Gallery_Frame_Publication::completed; + }}); service = shared_service; workload = shared_workload; - plots = shared_plots; - streams = shared_streams; } void TearDown(const benchmark::State&) override {} static void shutdown() { if (!shared_workload) return; - for (std::size_t index = 0; index < shared_plots.size(); ++index) - shared_plots[index]->unsubscribe(shared_streams[index]); - shared_plots.clear(); - shared_streams.clear(); shared_service.reset(); shared_workload.reset(); } @@ -264,8 +256,8 @@ protected: auto request = concurrent_input_request( input, plot_index, sequence); if (pointer_type) { - request.event.type = *pointer_type; - request.event.buttons = *pointer_type == Event_Type::pointer_release + request.event["type"] = magic_enum::enum_name(*pointer_type); + request.event["buttons"] = *pointer_type == Event_Type::pointer_release ? 0 : 1; } const auto result = service->call_tool( @@ -279,9 +271,9 @@ protected: const auto input = configuration.input; for (auto& count : workload->completed) count->store(0, std::memory_order_relaxed); - for (const auto& plot : plots) { - plot->reset_diagnostics(); - plot->request_taskflow_trace(1); + for (const auto id : configuration.plot_ids) { + service->reset_plot_diagnostics(id); + service->request_plot_taskflow_trace(id, 1); } if (input == Input_Workload::drag || input == Input_Workload::mixed) { if (!submit_input_batch( @@ -333,7 +325,8 @@ protected: std::uint64_t total_completed{}; double minimum_fps = std::numeric_limits::max(); double maximum_fps{}; - for (std::size_t index = 0; index < plots.size(); ++index) { + for (std::size_t index = 0; + index < configuration.plot_ids.size(); ++index) { const auto count = workload->completed[index]->load( std::memory_order_relaxed); total_completed += count; @@ -341,7 +334,8 @@ protected: ? static_cast(count) / elapsed_seconds : 0.0; minimum_fps = std::min(minimum_fps, fps); maximum_fps = std::max(maximum_fps, fps); - const auto diagnostics = plots[index]->diagnostics(); + const auto diagnostics = service->plot_diagnostics( + configuration.plot_ids[index]); const auto& policy = diagnostics.at("frame_policy"); const auto prefix = std::string{configuration.plot_ids[index]} + "/"; state.counters[prefix + "fps"] = fps; @@ -520,8 +514,10 @@ protected: double longest_sampled_busy_ms{}; std::string longest_sampled_busy_node; std::string longest_sampled_busy_plot; - for (std::size_t plot_index = 0; plot_index < plots.size(); ++plot_index) { - const auto trace = plots[plot_index]->taskflow_trace(); + for (std::size_t plot_index = 0; + plot_index < configuration.plot_ids.size(); ++plot_index) { + const auto trace = service->plot_taskflow_trace( + configuration.plot_ids[plot_index]); if (!trace.value("complete", false)) continue; for (const auto& frame : trace.at("frames")) for (const auto& execution : frame.at("executions")) { @@ -553,12 +549,8 @@ protected: private: inline static std::shared_ptr shared_service{}; inline static std::shared_ptr shared_workload{}; - inline static std::vector> shared_plots{}; - inline static std::vector shared_streams{}; std::shared_ptr service{}; std::shared_ptr workload{}; - std::vector> plots{}; - std::vector streams{}; }; BENCHMARK_DEFINE_F(Configured_Plots, Run)(benchmark::State& state) { diff --git a/mcp/tests/Event_Latency_Benchmarks.cpp b/mcp/tests/Event_Latency_Benchmarks.cpp index 09f4ebe..2786a24 100644 --- a/mcp/tests/Event_Latency_Benchmarks.cpp +++ b/mcp/tests/Event_Latency_Benchmarks.cpp @@ -88,8 +88,7 @@ struct Plot_Probe { }; struct Active_Plot { - std::shared_ptr plot{}; - web::Plot::Stream_Id stream{}; + std::string id{}; std::shared_ptr probe{}; }; @@ -113,27 +112,27 @@ std::vector published_results{}; ? static_cast(end - begin) / 1'000'000.0 : 0.0; } -[[nodiscard]] web::Plot_Input_Event make_event( +[[nodiscard]] nlohmann::json make_event( Event_Type type, std::size_t plot_index, std::uint64_t batch) { - web::Plot_Input_Event event{}; - event.type = type; - event.time_milliseconds = static_cast(steady_time_ns()) / - 1'000'000.0; - event.position = { - 120.0 + static_cast((batch * 13U + plot_index * 7U) % 400U), - 80.0 + static_cast((batch * 11U + plot_index * 5U) % 240U)}; - event.global_position = event.position; - event.button = Mouse_Button::left; - event.buttons = type == Event_Type::pointer_release ? 0U : 1U; - event.modifiers = Keyboard_Modifier::control; - event.pixel_delta_x = 3.0; - event.pixel_delta_y = (batch & 1U) == 0U ? 120.0 : -120.0; - event.angle_delta_x = event.pixel_delta_x; - event.angle_delta_y = event.pixel_delta_y; - event.key = Key::space; - event.native_key = 32; - event.auto_repeat = (batch & 1U) != 0U; - return event; + const nlohmann::json position{ + {"x", 120.0 + static_cast( + (batch * 13U + plot_index * 7U) % 400U)}, + {"y", 80.0 + static_cast( + (batch * 11U + plot_index * 5U) % 240U)}}; + const auto wheel_delta = (batch & 1U) == 0U ? 120.0 : -120.0; + return { + {"type", magic_enum::enum_name(type)}, + {"time_milliseconds", + static_cast(steady_time_ns()) / 1'000'000.0}, + {"position", position}, {"global_position", position}, + {"button", "left"}, + {"buttons", type == Event_Type::pointer_release ? 0U : 1U}, + {"modifiers", static_cast(Keyboard_Modifier::control)}, + {"pixel_delta_x", 3.0}, {"pixel_delta_y", wheel_delta}, + {"angle_delta_x", 3.0}, {"angle_delta_y", wheel_delta}, + {"key", "space"}, {"native_key", 32U}, + {"auto_repeat", (batch & 1U) != 0U} + }; } [[nodiscard]] const nlohmann::json& event_statistic( @@ -154,15 +153,12 @@ std::vector published_results{}; return *found_statistic; } -void configure_manual_backend_plot(const std::shared_ptr& plot) { - const auto manual = plot->write_prop( +void configure_manual_backend_plot( + Control_Service& service, std::string_view id) { + const auto manual = service.write_plot_property(id, "frame-analysis", "pacing_mode", "manual"); if (!manual.value("success", false)) throw std::runtime_error("failed to select manual frame pacing"); - const auto diagnostics_only = plot->write_prop( - "frame-analysis", "pixel_delivery_enabled", false); - if (!diagnostics_only.value("success", false)) - throw std::runtime_error("failed to disable backend pixel readback"); } [[nodiscard]] std::uint64_t correlation_id( @@ -283,7 +279,6 @@ public: void run_event_latency(benchmark::State& state) { published_results.clear(); - auto service = Control_Service::create(); const auto definitions = web::gallery_plot_definitions(); std::vector active; active.reserve(definitions.size()); @@ -294,18 +289,37 @@ void run_event_latency(benchmark::State& state) { if (configuration.three_d_only && definition.dimension != web::Plot_Dimension::three_d) continue; - auto plot = service->find_plot(definition.id); - if (!plot) - throw std::runtime_error( - "Gallery plot is unavailable: " + - std::string{definition.id}); - if (configuration.manual_policy) - configure_manual_backend_plot(plot); auto probe = std::make_shared(); - active.push_back({std::move(plot), 0, std::move(probe)}); + active.push_back({std::string{definition.id}, std::move(probe)}); published_results.push_back({ std::string{definition.id}, definition.dimension, {}}); } + auto service = Control_Service::create(Gallery_Output{ + .width = configuration.width, + .height = configuration.height, + .publish = [&active]( + std::string_view id, + std::shared_ptr frame) { + const auto found = std::ranges::find(active, id, + &Active_Plot::id); + if (found == active.end() || !frame || !frame->pixels) + return web::Gallery_Frame_Publication::ignored; + const auto expected = found->probe->expected_correlation.load( + std::memory_order_acquire); + if (expected != 0 && + frame->pixels->correlation_id == expected) { + std::uint64_t incomplete{}; + static_cast(found->probe->completed_steady_ns. + compare_exchange_strong( + incomplete, steady_time_ns(), + std::memory_order_release, + std::memory_order_relaxed)); + } + return web::Gallery_Frame_Publication::completed; + }}); + if (configuration.manual_policy) + for (const auto& item : active) + configure_manual_backend_plot(*service, item.id); std::string configuration_failure; if (configuration.manual_policy) { @@ -314,10 +328,20 @@ void run_event_latency(benchmark::State& state) { const auto deadline = Clock::now() + std::chrono::seconds{10}; Task_Graph::corun_until([&] { const bool ready = std::ranges::all_of( - active, [](const Active_Plot& item) { - const auto diagnostics = item.plot->diagnostics(); + active, [&service](const Active_Plot& item) { + const auto diagnostics = + service->plot_diagnostics(item.id); + if (!diagnostics.contains("frame_policy")) + return false; const auto& policy = diagnostics.at("frame_policy"). at("configuration"); + if (policy.at("mode") == "manual" && + policy.at("pixel_delivery_enabled").get()) { + static_cast(service->write_plot_property( + item.id, "frame-analysis", + "pixel_delivery_enabled", false)); + return false; + } return policy.at("mode") == "manual" && !policy.at("pixel_delivery_enabled").get(); }); @@ -327,7 +351,8 @@ void run_event_latency(benchmark::State& state) { "timed out applying manual backend frame policy"; for (std::size_t index = 0; index < active.size(); ++index) { const auto& item = active[index]; - const auto diagnostics = item.plot->diagnostics(); + const auto diagnostics = + service->plot_diagnostics(item.id); const auto& policy = diagnostics.at("frame_policy"); const auto& policy_configuration = policy.at("configuration"); @@ -352,30 +377,8 @@ void run_event_latency(benchmark::State& state) { if (!configuration_failure.empty()) throw std::runtime_error(configuration_failure); - for (auto& item : active) { - const auto probe = item.probe; - item.stream = item.plot->subscribe( - [probe](std::shared_ptr frame) { - if (!frame || !frame->pixels) - return web::Plot_Frame_Publication::ignored; - const auto expected = probe->expected_correlation.load( - std::memory_order_acquire); - if (expected == 0 || - frame->pixels->correlation_id != expected) - return web::Plot_Frame_Publication::completed; - std::uint64_t incomplete{}; - static_cast( - probe->completed_steady_ns.compare_exchange_strong( - incomplete, steady_time_ns(), - std::memory_order_release, - std::memory_order_relaxed)); - return web::Plot_Frame_Publication::completed; - }); - item.plot->configure_stream( - item.stream, configuration.width, configuration.height); - } - - for (auto& item : active) item.plot->reset_diagnostics(); + for (auto& item : active) + service->reset_plot_diagnostics(item.id); std::vector batch_timing(active.size()); std::string failure; Task_Graph coordinator{"mcp.event-latency"}; @@ -413,7 +416,7 @@ void run_event_latency(benchmark::State& state) { return; } } - item.plot->schedule_render(aethera::Frame_Request{ + service->request_frame(item.id, aethera::Frame_Request{ .issued_at = Clock::now(), .sequence = correlation, .time_milliseconds = @@ -441,7 +444,8 @@ void run_event_latency(benchmark::State& state) { plot_index < active.size(); ++plot_index) { const auto completed = active[plot_index].probe-> completed_steady_ns.load(std::memory_order_acquire); - const auto diagnostics = active[plot_index].plot->diagnostics(); + const auto diagnostics = service->plot_diagnostics( + active[plot_index].id); for (std::size_t event_index = 0; event_index < event_count; ++event_index) { auto& result = published_results[plot_index]. @@ -506,8 +510,6 @@ void run_event_latency(benchmark::State& state) { catch (const std::exception& error) { state.SkipWithError(error.what()); } - - for (auto& item : active) item.plot->unsubscribe(item.stream); } BENCHMARK(run_event_latency)->Iterations(1)->UseRealTime(); diff --git a/mcp/tests/Protocol_Type_Tests.cpp b/mcp/tests/Protocol_Type_Tests.cpp index aa6fc7c..4da2977 100644 --- a/mcp/tests/Protocol_Type_Tests.cpp +++ b/mcp/tests/Protocol_Type_Tests.cpp @@ -70,12 +70,11 @@ TEST(Protocol_Type, Non_Aggregate_Render_Type_Has_Explicit_Matcher) { EXPECT_EQ(decoded, color_map); } -TEST(Protocol_Type, Timestamped_Input_Request_Is_Described_Recursively) { +TEST(Protocol_Type, Input_Event_Remains_A_Json_Object_At_The_Protocol_Boundary) { const auto schema = describe_protocol_type(); const auto& event = schema.at("properties").at("event"); - EXPECT_TRUE(event.at("properties").contains("time_milliseconds")); - EXPECT_TRUE(event.at("properties").contains("position")); - EXPECT_TRUE(event.at("properties").contains("type")); + EXPECT_EQ(event.at("type"), "object"); + EXPECT_TRUE(event.at("additionalProperties")); } TEST(Protocol_Type, Render_Time_Request_Uses_The_Caller_Timeline) { diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 0d061c0..e1a2743 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -3,6 +3,12 @@ namespace aethera::render_2d { Render_Scene_2D::Private::~Private() = default; bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; +void Render_Scene_2D::submit_event(Event_Pointer event) { + if (!event) throw std::invalid_argument("2D Scene input event is null"); + observe_event(not_null{event.get()}); + submit_stream(std::move(event)); +} + Render_Scene_2D::Render_Result_Type Render_Scene_2D::render( not_null frame, Frame_Callback callback) { return double_buffer::detail::Internal_Access::get(this).render( diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index 0dad579..0f5388a 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -37,6 +37,7 @@ struct Render_Scene_2D : Def)>; using Render_Result_Type = std::expected; using Render_Frame_Type = Frame_2D; + void submit_event(Event_Pointer event); [[nodiscard]] Render_Result_Type render( not_null frame, Frame_Callback callback); [[nodiscard]] Render_Frame_Result render_frame( diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index eea8be5..b14407e 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -2,6 +2,12 @@ namespace aethera::render_3d { bool Render_Scene_3D::Prop::operator==(const Prop&) const = default; +void Render_Scene_3D::submit_event(Event_Pointer event) { + if (!event) throw std::invalid_argument("3D Scene input event is null"); + observe_event(not_null{event.get()}); + submit_stream(std::move(event)); +} + Render_Scene_3D::Render_Result Render_Scene_3D::render( not_null frame, Frame_Callbacks callbacks) { return double_buffer::detail::Internal_Access::get(this).render( diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 2675001..536caad 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -58,6 +58,7 @@ struct Render_Scene_3D : Def, Render_Frame_Scene { }; using Render_Result_Type = Render_Result; using Render_Frame_Type = Frame_3D; + void submit_event(Event_Pointer event); [[nodiscard]] Render_Result render(not_null frame, Frame_Callbacks callbacks); [[nodiscard]] Render_Frame_Result render_frame( not_null frame, diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index 2b22e5c..f2fa3a8 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -1,5 +1,5 @@ #include "Graph_WebSocket.hpp" -#include +#include #include #include #include @@ -21,48 +21,26 @@ std::uint32_t input_dimension(std::uint32_t value, std::uint32_t minimum, } struct Graph_WebSocket::Private { - std::shared_ptr plot; /* 该控制连接绑定的 Plot。 */ - Send_Handler send_handler; /* Drogon 文本投递入口。 */ - Plot::Stream_Id stream{}; /* Plot 完成通知订阅标识。 */ + std::shared_ptr control; + std::string plot_id; std::atomic_bool attached{}; /* start 成功后为真并保证 close 单调执行。 */ std::atomic_uint64_t viewport{(std::uint64_t{320} << 32U) | 192U}; }; -Graph_WebSocket::Graph_WebSocket(std::shared_ptr plot, - Send_Handler send_handler) +Graph_WebSocket::Graph_WebSocket( + std::shared_ptr control, std::string plot_id) : d(std::make_unique()) { - if (!plot || !send_handler) + if (!control || plot_id.empty() || !control->contains_plot(plot_id)) throw std::invalid_argument( - "plot WebSocket requires a Plot and send handler"); - d->plot = std::move(plot); - d->send_handler = std::move(send_handler); + "plot WebSocket requires a known Gallery id"); + d->control = std::move(control); + d->plot_id = std::move(plot_id); } Graph_WebSocket::~Graph_WebSocket() { close(); } void Graph_WebSocket::start() { - if (d->attached.exchange(true, std::memory_order_acq_rel)) return; - const auto weak = weak_from_this(); - d->stream = d->plot->subscribe( - [weak](std::shared_ptr frame) { - if (const auto socket = weak.lock()) - return socket->deliver_frame(std::move(frame)) - ? Plot_Frame_Publication::completed - : Plot_Frame_Publication::ignored; - return Plot_Frame_Publication::ignored; - }); -} - -bool Graph_WebSocket::deliver_frame( - std::shared_ptr frame) { - if (!frame || frame->notification.empty() || - !d->attached.load(std::memory_order_acquire)) - return false; - try { - d->send_handler(frame->notification); - return true; - } - catch (...) { return false; } + d->attached.store(true, std::memory_order_release); } void Graph_WebSocket::receive(std::string_view message) { @@ -86,76 +64,33 @@ void Graph_WebSocket::receive(std::string_view message) { return; } if (kind == "manual_render") { - d->plot->render_once(); + d->control->request_frame(d->plot_id, Frame_Request{ + .issued_at = std::chrono::steady_clock::now(), + .source = Frame_Request_Source::immediate}); return; } if (kind != "input") return; const auto input = json.find("event"); if (input == json.end() || !input->is_object()) return; - const auto type = magic_enum::enum_cast( - input->value("type", std::string{})); - if (!type) return; - const auto source_time = input->find("time_milliseconds"); - if (source_time == input->end() || !source_time->is_number()) return; - const auto time_milliseconds = source_time->get(); - if (!std::isfinite(time_milliseconds) || time_milliseconds < 0.0) - return; const auto viewport = d->viewport.load(std::memory_order_acquire); const auto width = static_cast(viewport >> 32U); const auto height = static_cast(viewport); - Plot_Input_Event decoded; - decoded.type = *type; - decoded.time_milliseconds = time_milliseconds; - const auto read_point = [&](std::string_view key, - render_2d::Point_F& point, - bool viewport_relative) { - const auto value = input->find(key); - if (value == input->end() || !value->is_object()) return; - const auto x = value->value("x", 0.0); - const auto y = value->value("y", 0.0); - point.x = viewport_relative - ? std::clamp(x, 0.0, static_cast(width)) : x; - point.y = viewport_relative - ? std::clamp(y, 0.0, static_cast(height)) : y; - }; - read_point("position", decoded.position, true); - read_point("global_position", decoded.global_position, false); - decoded.button = magic_enum::enum_cast( - input->value("button", std::string{"none"})) - .value_or(Mouse_Button::none); - decoded.buttons = static_cast( - std::clamp(input->value("buttons", 0), 0, 255)); - decoded.modifiers = static_cast( - std::clamp(input->value("modifiers", 0), 0, 15)); - decoded.pixel_delta_x = std::clamp( - input->value("pixel_delta_x", 0.0), -4096.0, 4096.0); - decoded.pixel_delta_y = std::clamp( - input->value("pixel_delta_y", 0.0), -4096.0, 4096.0); - decoded.angle_delta_x = std::clamp( - input->value("angle_delta_x", 0.0), -120.0, 120.0); - decoded.angle_delta_y = std::clamp( - input->value("angle_delta_y", 0.0), -120.0, 120.0); - decoded.key = magic_enum::enum_cast( - input->value("key", std::string{"unknown"})) - .value_or(Key::unknown); - decoded.native_key = input->value("native_key", 0U); - decoded.auto_repeat = input->value("auto_repeat", false); - d->plot->submit_input(std::move(decoded)); + d->control->submit_input(d->plot_id, make_input_event( + *input, Input_Viewport{width, height})); } catch (const nlohmann::json::exception&) {} catch (...) {} } void Graph_WebSocket::close() noexcept { - if (!d->attached.exchange(false, std::memory_order_acq_rel)) return; - try { d->plot->unsubscribe(d->stream); } - catch (...) {} + d->attached.store(false, std::memory_order_release); } -Graph_WebSocket_Controller::Graph_WebSocket_Controller(Plot_Resolver resolver) - : resolve_plot(std::move(resolver)) { - if (!resolve_plot) - throw std::invalid_argument("plot WebSocket requires a plot resolver"); +Graph_WebSocket_Controller::Graph_WebSocket_Controller( + std::shared_ptr value_control) + : control(std::move(value_control)) { + if (!control) + throw std::invalid_argument("plot WebSocket requires Control Service"); } void Graph_WebSocket_Controller::initPathRouting() { @@ -167,21 +102,14 @@ void Graph_WebSocket_Controller::handleNewConnection( const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) { try { - auto plot = resolve_plot(graph_id_from_path(request->path())); - if (!plot) { + const auto plot_id = std::string{graph_id_from_path(request->path())}; + if (!control->contains_plot(plot_id)) { connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera plot"); return; } - const auto weak_connection = - std::weak_ptr{connection}; auto socket = std::make_shared( - std::move(plot), [weak_connection](std::string message) { - if (const auto target = weak_connection.lock(); - target && target->connected()) - target->send(std::move(message), - drogon::WebSocketMessageType::Text); - }); + control, std::move(plot_id)); connection->setContext(socket); connection->setPingMessage( "aethera-gallery-plot", std::chrono::seconds(20)); diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp index 841c1cf..2d6669a 100644 --- a/web_server/src/Graph_WebSocket.hpp +++ b/web_server/src/Graph_WebSocket.hpp @@ -1,7 +1,6 @@ #pragma once -#include +#include #include -#include #include #include #include @@ -12,8 +11,8 @@ namespace aethera::web { struct Graph_WebSocket final : std::enable_shared_from_this { public: - using Send_Handler = std::function; - Graph_WebSocket(std::shared_ptr plot, Send_Handler send_handler); + Graph_WebSocket(std::shared_ptr control, + std::string plot_id); ~Graph_WebSocket(); Graph_WebSocket(const Graph_WebSocket&) = delete; Graph_WebSocket& operator=(const Graph_WebSocket&) = delete; @@ -22,16 +21,14 @@ public: void close() noexcept; private: - [[nodiscard]] bool deliver_frame( - std::shared_ptr frame); struct Private; std::unique_ptr d; }; struct Graph_WebSocket_Controller final : drogon::WebSocketController { - using Plot_Resolver = std::function(std::string_view)>; - explicit Graph_WebSocket_Controller(Plot_Resolver resolver); + explicit Graph_WebSocket_Controller( + std::shared_ptr control); void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) override; @@ -42,7 +39,7 @@ struct Graph_WebSocket_Controller final static void initPathRouting(); private: - Plot_Resolver resolve_plot; + std::shared_ptr control; }; } diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp index 0f6142b..142fd2c 100644 --- a/web_server/src/Web_Server.cpp +++ b/web_server/src/Web_Server.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -107,17 +108,23 @@ nlohmann::json merge_gallery_media_trace(nlohmann::json plot_trace, } int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) { - auto control = mcp::Control_Service::create(); - + auto control_slot = std::make_shared>>(); std::vector gallery_2d; std::vector gallery_3d; gallery_2d.reserve(8); gallery_3d.reserve(16); for (const auto& definition : gallery_plot_definitions()) { - auto plot = control->find_plot(definition.id); + const auto id = std::string{definition.id}; (definition.dimension == Plot_Dimension::three_d ? gallery_3d : gallery_2d) - .push_back({std::string{definition.id}, std::move(plot)}); + .push_back({id, [control_slot, id]( + Frame_Publication_Feedback feedback) { + if (const auto control = control_slot->load( + std::memory_order_acquire)) + control->submit_publication_feedback( + id, std::move(feedback)); + }}); } const auto entry_order = [](const auto& left, const auto& right) { return left.id < right.id; @@ -163,10 +170,22 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) }; add_media_groups("2d", std::move(gallery_2d)); add_media_groups("3d", std::move(gallery_3d)); - auto resolve_plot = [control](std::string_view id) { - return control->find_plot(id); - }; - auto websocket = std::make_shared(resolve_plot); + auto control = mcp::Control_Service::create(mcp::Gallery_Output{ + .width = 720, + .height = 420, + .publish = [gallery_streams, plot_media_group]( + std::string_view id, + std::shared_ptr frame) { + const auto group = plot_media_group->find(std::string{id}); + if (group == plot_media_group->end()) + return Gallery_Frame_Publication::ignored; + const auto stream = gallery_streams->find(group->second); + if (stream == gallery_streams->end()) + return Gallery_Frame_Publication::ignored; + return stream->second->accept_frame(id, std::move(frame)); + }}); + control_slot->store(control, std::memory_order_release); + auto websocket = std::make_shared(control); auto gallery_websocket = std::make_shared( [gallery_streams](std::string_view id) { @@ -212,17 +231,16 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id) { - auto plot = control->find_plot(plot_id); - if (!plot) { + if (!control->contains_plot(plot_id)) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } if (request->method() == drogon::Delete) { - plot->reset_diagnostics(); + control->reset_plot_diagnostics(plot_id); callback(json_response({{"success", true}})); return; } - callback(json_response(plot->diagnostics())); + callback(json_response(control->plot_diagnostics(plot_id))); }, {drogon::Get, drogon::Delete}); app.registerHandler("/taskflow/diagnostics", [control]( @@ -237,8 +255,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id) { - auto plot = control->find_plot(plot_id); - if (!plot) { + if (!control->contains_plot(plot_id)) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } @@ -266,14 +283,14 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) return state.value("requested", std::size_t{}) > state.value("captured", std::size_t{}); }; - if (trace_active(plot->taskflow_trace()) || + if (trace_active(control->plot_taskflow_trace(plot_id)) || trace_active(media_stream->taskflow_trace())) throw std::logic_error( "A Taskflow frame trace request is already active"); media_stream->request_taskflow_trace(frame_count); - plot->request_taskflow_trace(frame_count); + control->request_plot_taskflow_trace(plot_id, frame_count); } - auto output = plot->taskflow_trace(); + auto output = control->plot_taskflow_trace(plot_id); const auto media = media_stream->taskflow_trace(); output = merge_gallery_media_trace(std::move(output), media); const bool plot_complete = output.value("complete", false); @@ -317,12 +334,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) const drogon::HttpRequestPtr&, std::function&& callback, std::string plot_id) { - auto plot = control->find_plot(plot_id); - if (!plot) { + if (!control->contains_plot(plot_id)) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } - try { callback(json_response(plot->schema())); } + try { callback(json_response(control->plot_schema(plot_id))); } catch (const std::exception& failure) { callback(error_response(drogon::k500InternalServerError, failure.what())); @@ -335,8 +351,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) std::string plot_id, std::string component, std::string key) { - auto plot = control->find_plot(plot_id); - if (!plot) { + if (!control->contains_plot(plot_id)) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } @@ -348,7 +363,8 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) return; } try { - callback(json_response(plot->write_prop(component, key, value))); + callback(json_response(control->write_plot_property( + plot_id, component, key, value))); } catch (const std::exception& failure) { callback(error_response(drogon::k500InternalServerError, @@ -361,13 +377,12 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) std::function&& callback, std::string plot_id, std::string component) { - auto plot = control->find_plot(plot_id); - if (!plot) { + if (!control->contains_plot(plot_id)) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } try { - auto result = plot->component_state(component); + auto result = control->plot_component_state(plot_id, component); if (result.value("success", true)) callback(json_response(std::move(result))); else callback(error_response(drogon::k404NotFound, result.value("error", "unknown component"))); @@ -382,8 +397,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id) { - auto plot = control->find_plot(plot_id); - if (!plot) { + if (!control->contains_plot(plot_id)) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } @@ -398,7 +412,10 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(error_response(drogon::k400BadRequest, "data generation input must be an object")); return; } - try { callback(json_response(plot->generate_data(input))); } + try { + callback(json_response( + control->generate_plot_data(plot_id, input))); + } catch (const std::exception& failure) { callback(error_response(drogon::k500InternalServerError, failure.what())); diff --git a/web_server/src/media/Gallery_Video_Stream.cpp b/web_server/src/media/Gallery_Video_Stream.cpp index adc748c..971bd3a 100644 --- a/web_server/src/media/Gallery_Video_Stream.cpp +++ b/web_server/src/media/Gallery_Video_Stream.cpp @@ -52,7 +52,8 @@ void Gallery_Video_Stream::Private::initialize( source_ids.reserve(plots.size()); sources.reserve(plots.size()); for (auto& plot : plots) { - if (!plot.plot) throw std::invalid_argument("gallery video source has no Plot"); + if (plot.id.empty()) + throw std::invalid_argument("gallery video source has no id"); source_ids.push_back(plot.id); sources.push_back(Source{std::move(plot)}); } @@ -170,7 +171,8 @@ void Gallery_Video_Stream::Private::release_pending_frames() noexcept { try { if (pending.slot < sources.size() && pending.frame && pending.frame->pixels) - sources[pending.slot].entry.plot->submit_publication_feedback({ + if (sources[pending.slot].entry.publish_completed) + sources[pending.slot].entry.publish_completed({ .completed_at = std::chrono::steady_clock::now(), .sequence = pending.frame->pixels->sequence, .succeeded = false}); @@ -185,8 +187,8 @@ void Gallery_Video_Stream::Private::fail( if (active_frame && !active_frame->publication_feedback_submitted && active_frame->source_slot < sources.size()) { try { - sources[active_frame->source_slot].entry.plot - ->submit_publication_feedback({ + if (sources[active_frame->source_slot].entry.publish_completed) + sources[active_frame->source_slot].entry.publish_completed({ .completed_at = std::chrono::steady_clock::now(), .sequence = active_frame->plot_frame_sequence, .succeeded = false}); @@ -207,12 +209,12 @@ void Gallery_Video_Stream::Private::fail( } catch (...) {} } -Plot_Frame_Publication Gallery_Video_Stream::Private::accept_frame( - std::size_t slot, std::shared_ptr frame) { +Gallery_Frame_Publication Gallery_Video_Stream::Private::accept_frame( + std::size_t slot, std::shared_ptr frame) { if (stopping.load(std::memory_order_acquire) || failed.load(std::memory_order_acquire) || !frame || !frame->pixels || !frame->pixels->pixels || !consumer_accepts()) - return Plot_Frame_Publication::ignored; + return Gallery_Frame_Publication::ignored; Pending_Plot_Frame pending{ slot, std::move(frame), std::chrono::steady_clock::now(), @@ -222,12 +224,12 @@ Plot_Frame_Publication Gallery_Video_Stream::Private::accept_frame( if (stopping.load(std::memory_order_acquire) || failed.load(std::memory_order_acquire)) { release_pending_frames(); - return Plot_Frame_Publication::ignored; + return Gallery_Frame_Publication::ignored; } received_frame_count.fetch_add(1, std::memory_order_relaxed); media_work_generation.fetch_add(1, std::memory_order_release); arm_media_task(object->weak_from_this()); - return Plot_Frame_Publication::asynchronous; + return Gallery_Frame_Publication::asynchronous; } void Gallery_Video_Stream::Private::update_metrics( const Active_Media_Frame& frame, @@ -367,7 +369,8 @@ void Gallery_Video_Stream::Private::begin_media_frame() { pending.slot, pending.frame->pixels); if (accepted == detail::Gallery_Frame_Atlas::Accept_Frame_Result::invalid_frame) { - sources[pending.slot].entry.plot->submit_publication_feedback({ + if (sources[pending.slot].entry.publish_completed) + sources[pending.slot].entry.publish_completed({ .completed_at = std::chrono::steady_clock::now(), .sequence = pending.frame->pixels->sequence, .succeeded = false}); @@ -429,7 +432,8 @@ void Gallery_Video_Stream::Private::submit_publication_feedback() { if (active_frame->source_slot >= sources.size()) throw std::logic_error( "gallery publication feedback source is invalid"); - sources[active_frame->source_slot].entry.plot->submit_publication_feedback({ + if (sources[active_frame->source_slot].entry.publish_completed) + sources[active_frame->source_slot].entry.publish_completed({ .completed_at = std::chrono::steady_clock::now(), .sequence = active_frame->plot_frame_sequence, .succeeded = active_frame->publication_succeeded}); @@ -456,9 +460,28 @@ std::shared_ptr Gallery_Video_Stream::create( "gallery video stream definition validation failed"); auto result = std::shared_ptr(std::move(*built)); static_cast(*result->d).initialize(std::move(plots)); - result->bind_plots(); + result->initialize_pipeline(); return result; } +Gallery_Frame_Publication Gallery_Video_Stream::accept_frame( + std::string_view plot_id, std::shared_ptr frame) { + auto& data = static_cast(*d); + const auto found = std::ranges::find_if( + data.sources, [plot_id](const Private::Source& source) { + return source.entry.id == plot_id; + }); + if (found == data.sources.end()) + return Gallery_Frame_Publication::ignored; + try { + return data.accept_frame( + static_cast(found - data.sources.begin()), + std::move(frame)); + } + catch (...) { + data.fail(std::current_exception()); + return Gallery_Frame_Publication::ignored; + } +} Gallery_Video_Stream::Gallery_Video_Stream() = default; Gallery_Video_Stream::~Gallery_Video_Stream() { shutdown(); @@ -523,7 +546,7 @@ void Gallery_Video_Stream::Private::arm_media_task( fail(std::current_exception()); } } -void Gallery_Video_Stream::bind_plots() { +void Gallery_Video_Stream::initialize_pipeline() { auto& data = static_cast(*d); const auto weak = weak_from_this(); if (data.sources.empty()) @@ -592,26 +615,6 @@ void Gallery_Video_Stream::bind_plots() { publication_feedback.precede(complete); data.media_graph = media; - for (std::size_t slot = 0; slot < data.sources.size(); ++slot) { - auto& source = data.sources[slot]; - source.stream = source.entry.plot->subscribe( - [weak, slot](std::shared_ptr frame) { - const auto owner = weak.lock(); - if (!owner) return Plot_Frame_Publication::ignored; - auto& owner_data = static_cast(*owner->d); - if (owner_data.stopping.load(std::memory_order_acquire)) - return Plot_Frame_Publication::ignored; - try { - return owner_data.accept_frame(slot, std::move(frame)); - } - catch (...) { - owner_data.fail(std::current_exception()); - return Plot_Frame_Publication::ignored; - } - }); - source.entry.plot->configure_stream( - source.stream, tile_width, tile_height); - } } Gallery_Video_Stream::Stream_Id Gallery_Video_Stream::subscribe( std::string connection, Stream_Handler handler, Transport_Readiness readiness, @@ -685,14 +688,6 @@ void Gallery_Video_Stream::request_video_key_frame() { void Gallery_Video_Stream::shutdown() noexcept { auto& data = static_cast(*d); if (data.stopping.exchange(true, std::memory_order_acq_rel)) return; - for (auto& source : data.sources) { - if (source.stream == 0) continue; - try { - source.entry.plot->unsubscribe(source.stream); - } - catch (...) {} - source.stream = 0; - } for (auto& consumer : data.consumers) consumer.store({}, std::memory_order_release); data.release_pending_frames(); diff --git a/web_server/src/media/Gallery_Video_Stream.hpp b/web_server/src/media/Gallery_Video_Stream.hpp index 0e78af5..3f00178 100644 --- a/web_server/src/media/Gallery_Video_Stream.hpp +++ b/web_server/src/media/Gallery_Video_Stream.hpp @@ -1,5 +1,5 @@ #pragma once -#include +#include #include "Encoded_Video_Frame.hpp" #include #include @@ -18,8 +18,8 @@ struct Gallery_Stream_Frame { struct Gallery_Video_Stream : Def, std::enable_shared_from_this { struct Plot_Entry { - std::string id; /* 图集布局和浏览器裁剪使用的业务图标识。 */ - std::shared_ptr plot; /* 当前图实际 Scene 与界面之间的连接对象。 */ + std::string id; + std::function publish_completed; }; struct Prop : Prev_Prop {}; struct State : Prev_State { @@ -39,6 +39,8 @@ struct Gallery_Video_Stream : Def, Gallery_Video_Stream& operator=(const Gallery_Video_Stream&) = delete; [[nodiscard]] static std::shared_ptr create( std::vector plots); + [[nodiscard]] Gallery_Frame_Publication accept_frame( + std::string_view plot_id, std::shared_ptr frame); [[nodiscard]] Stream_Id subscribe(std::string connection, Stream_Handler handler, Transport_Readiness readiness, @@ -51,7 +53,7 @@ struct Gallery_Video_Stream : Def, void request_taskflow_trace(std::size_t frame_count); [[nodiscard]] nlohmann::json taskflow_trace() const; private: - void bind_plots(); + void initialize_pipeline(); }; } #include "Gallery_Video_Stream.ipp" diff --git a/web_server/src/media/Gallery_Video_Stream.ipp b/web_server/src/media/Gallery_Video_Stream.ipp index 338d1a2..9e5fe25 100644 --- a/web_server/src/media/Gallery_Video_Stream.ipp +++ b/web_server/src/media/Gallery_Video_Stream.ipp @@ -15,8 +15,7 @@ struct Gallery_Video_Stream::Private : Prev_Private { using Object = Gallery_Video_Stream; struct Source { - Plot_Entry entry; /* 图集槽位关联的实际 Plot。 */ - Plot::Stream_Id stream{}; /* Plot 完成帧的唯一订阅标识。 */ + Plot_Entry entry; }; struct Consumer { @@ -29,7 +28,7 @@ struct Gallery_Video_Stream::Private : Prev_Private { struct Pending_Plot_Frame { std::size_t slot{}; /* 该完成帧所属的稳定图集槽位。 */ - std::shared_ptr frame{}; /* 持有像素帧以及其隐藏的 Plot 交付生命周期。 */ + std::shared_ptr frame{}; std::chrono::steady_clock::time_point enqueued_at{}; /* 进入媒体串行队列的单调时刻。 */ std::chrono::nanoseconds source_time_unix{}; /* 进入媒体串行队列的 Unix 时间。 */ }; @@ -103,8 +102,8 @@ struct Gallery_Video_Stream::Private : Prev_Private { std::string notification) noexcept; void release_pending_frames() noexcept; void fail(std::exception_ptr failure) noexcept; - [[nodiscard]] Plot_Frame_Publication accept_frame( - std::size_t slot, std::shared_ptr frame); + [[nodiscard]] Gallery_Frame_Publication accept_frame( + std::size_t slot, std::shared_ptr frame); void arm_media_task(std::weak_ptr lifetime); void begin_media_frame(); void encode_media_frame(); diff --git a/web_server/src/media/detail/FFmpeg_Frame_Transport.cpp b/web_server/src/media/detail/FFmpeg_Frame_Transport.cpp index 8f52d17..fa77068 100644 --- a/web_server/src/media/detail/FFmpeg_Frame_Transport.cpp +++ b/web_server/src/media/detail/FFmpeg_Frame_Transport.cpp @@ -24,7 +24,7 @@ std::optional FFmpeg_Frame_Transport::encode( std::span pixels, std::uint32_t width, std::uint32_t height, - Plot_Pixel_Layout layout, + Gallery_Pixel_Layout layout, std::uint64_t sequence, std::chrono::microseconds presentation_time, std::chrono::nanoseconds source_time_unix) { @@ -32,7 +32,7 @@ std::optional FFmpeg_Frame_Transport::encode( d->encoder.request_key_frame(); return d->encoder.encode( pixels, width, height, - layout == Plot_Pixel_Layout::bgra8 + layout == Gallery_Pixel_Layout::bgra8 ? Video_Pixel_Layout::bgra : Video_Pixel_Layout::rgba, sequence, presentation_time, source_time_unix); diff --git a/web_server/src/media/detail/FFmpeg_Frame_Transport.hpp b/web_server/src/media/detail/FFmpeg_Frame_Transport.hpp index b9b90d8..5c0a486 100644 --- a/web_server/src/media/detail/FFmpeg_Frame_Transport.hpp +++ b/web_server/src/media/detail/FFmpeg_Frame_Transport.hpp @@ -1,6 +1,6 @@ #pragma once #include "H264_Encoder.hpp" -#include +#include #include #include #include @@ -23,7 +23,7 @@ public: std::span pixels, std::uint32_t width, std::uint32_t height, - Plot_Pixel_Layout layout, + Gallery_Pixel_Layout layout, std::uint64_t sequence, std::chrono::microseconds presentation_time, std::chrono::nanoseconds source_time_unix); diff --git a/web_server/src/media/detail/Gallery_Frame_Atlas.cpp b/web_server/src/media/detail/Gallery_Frame_Atlas.cpp index 9e6be31..2641d4c 100644 --- a/web_server/src/media/detail/Gallery_Frame_Atlas.cpp +++ b/web_server/src/media/detail/Gallery_Frame_Atlas.cpp @@ -9,9 +9,9 @@ namespace aethera::web::media::detail { struct Gallery_Frame_Atlas::Private { struct Source { - std::atomic> + std::atomic> latest_completion{}; /* 逻辑完成进度的唯一权威来源。 */ - std::atomic> + std::atomic> latest_pixels{}; /* 可编码真实像素的唯一权威来源。 */ std::atomic_uint64_t completion_count{}; std::atomic_uint64_t rendered_frame_count{}; @@ -71,7 +71,7 @@ Gallery_Frame_Atlas::Gallery_Frame_Atlas( Gallery_Frame_Atlas::~Gallery_Frame_Atlas() = default; Gallery_Frame_Atlas::Accept_Frame_Result Gallery_Frame_Atlas::accept_frame( - std::size_t slot, std::shared_ptr frame) { + std::size_t slot, std::shared_ptr frame) { if (slot >= d->sources.size()) throw std::out_of_range("gallery atlas source slot is invalid"); const auto expected = static_cast(d->description.tile_width) * @@ -121,8 +121,8 @@ Gallery_Atlas_Composition Gallery_Frame_Atlas::compose() { result.width = d->description.width; result.height = d->description.height; const auto layout = d->layout.load(std::memory_order_acquire); - result.layout = layout < 0 ? Plot_Pixel_Layout::rgba8 - : static_cast(layout); + result.layout = layout < 0 ? Gallery_Pixel_Layout::rgba8 + : static_cast(layout); result.rejected_frame_count = d->rejected_frame_count.exchange(0, std::memory_order_acq_rel); result.sources.reserve(d->sources.size()); diff --git a/web_server/src/media/detail/Gallery_Frame_Atlas.hpp b/web_server/src/media/detail/Gallery_Frame_Atlas.hpp index 615873f..1f68e47 100644 --- a/web_server/src/media/detail/Gallery_Frame_Atlas.hpp +++ b/web_server/src/media/detail/Gallery_Frame_Atlas.hpp @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include #include @@ -34,7 +34,7 @@ struct Gallery_Atlas_Source_Progress { }; struct Gallery_Atlas_Composition { std::span pixels{}; /* 在下一次 compose 前有效的连续原生布局图集。 */ - Plot_Pixel_Layout layout{Plot_Pixel_Layout::rgba8}; /* 本图集全部来源共同的像素布局。 */ + Gallery_Pixel_Layout layout{Gallery_Pixel_Layout::rgba8}; std::uint32_t width{}; /* 本次图集像素宽度。 */ std::uint32_t height{}; /* 本次图集像素高度。 */ std::uint32_t fresh_tile_count{}; /* 本次实际复制了新完成帧的槽位数。 */ @@ -56,7 +56,7 @@ public: Gallery_Frame_Atlas(const Gallery_Frame_Atlas&) = delete; Gallery_Frame_Atlas& operator=(const Gallery_Frame_Atlas&) = delete; [[nodiscard]] Accept_Frame_Result accept_frame( - std::size_t slot, std::shared_ptr frame); + std::size_t slot, std::shared_ptr frame); [[nodiscard]] Gallery_Atlas_Composition compose(); [[nodiscard]] Gallery_Atlas_Description describe() const; private: diff --git a/web_server/tests/Event_Latency_Benchmarks.cpp b/web_server/tests/Event_Latency_Benchmarks.cpp index 16ef758..255e8f4 100644 --- a/web_server/tests/Event_Latency_Benchmarks.cpp +++ b/web_server/tests/Event_Latency_Benchmarks.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace aethera::web::event_latency_benchmarks { namespace { @@ -89,7 +90,7 @@ struct Media_Probe { }; struct Active_Plot { - std::shared_ptr plot{}; + std::string id{}; std::shared_ptr input{}; std::shared_ptr video{}; media::Gallery_Video_Stream::Stream_Id video_subscription{}; @@ -131,8 +132,9 @@ std::vector published_results{}; } [[nodiscard]] std::uint64_t event_statistic_count( - const std::shared_ptr& plot, Event_Type type) { - const auto diagnostics = plot->diagnostics(); + const mcp::Control_Service& control, std::string_view id, + Event_Type type) { + const auto diagnostics = control.plot_diagnostics(id); const auto* statistic = find_event_statistic( diagnostics, type, "total_ms"); return statistic ? statistic->value("count", 0ULL) : 0ULL; @@ -181,21 +183,16 @@ std::vector published_results{}; }.dump(); } -void configure_plot(const std::shared_ptr& plot) { - if (!plot) throw std::invalid_argument("web benchmark Plot is null"); - if (!plot->write_prop( +void configure_plot(mcp::Control_Service& control, std::string_view id) { + if (!control.write_plot_property(id, "frame-analysis", "pacing_mode", "manual").value( "success", false)) throw std::runtime_error("failed to select manual Plot policy"); - if (!plot->write_prop( - "frame-analysis", "pixel_delivery_enabled", true).value( - "success", false)) - throw std::runtime_error("failed to enable Plot pixel delivery"); } [[nodiscard]] bool manual_pixel_policy_applied( - const std::shared_ptr& plot) { - const auto diagnostics = plot->diagnostics(); + const mcp::Control_Service& control, std::string_view id) { + const auto diagnostics = control.plot_diagnostics(id); const auto policy = diagnostics.find("frame_policy"); if (policy == diagnostics.end() || !policy->is_object()) return false; const auto state = policy->find("configuration"); @@ -300,19 +297,26 @@ void print_results() { void run_event_latency(benchmark::State& state) { published_results.clear(); - auto control = mcp::Control_Service::create(); std::vector active; try { const auto definitions = selected_definitions(); + auto control_slot = std::make_shared>>(); + auto videos = std::make_shared>>(); active.reserve(definitions.size()); published_results.reserve(definitions.size()); for (const auto& definition : definitions) { - auto plot = control->find_plot(definition.id); - configure_plot(plot); + const auto id = std::string{definition.id}; auto probe = std::make_shared(); auto video = media::Gallery_Video_Stream::create({ - {std::string{definition.id}, plot}}); + {id, [control_slot, id](Frame_Publication_Feedback feedback) { + if (const auto control = control_slot->load()) + control->submit_publication_feedback( + id, std::move(feedback)); + }}}); + videos->emplace(id, video); const auto video_subscription = video->subscribe( "web-event-latency-" + std::string{definition.id}, [probe](media::Gallery_Stream_Frame frame) { @@ -328,25 +332,38 @@ void run_event_latency(benchmark::State& state) { }, [] { return true; }, [] { return nlohmann::json::object(); }); - auto input = std::make_shared( - plot, [](std::string) {}); - input->start(); - input->receive(nlohmann::json{ - {"kind", "stream"}, - {"viewport", { - {"width", configuration.width}, - {"height", configuration.height}}} - }.dump()); - active.push_back({std::move(plot), std::move(input), + active.push_back({id, {}, std::move(video), video_subscription, std::move(probe)}); published_results.push_back({ std::string{definition.id}, definition.dimension, {}}); } + auto control = mcp::Control_Service::create(mcp::Gallery_Output{ + .width = configuration.width, + .height = configuration.height, + .publish = [videos]( + std::string_view id, + std::shared_ptr frame) { + const auto found = videos->find(std::string{id}); + return found == videos->end() + ? Gallery_Frame_Publication::ignored + : found->second->accept_frame(id, std::move(frame)); + }}); + control_slot->store(control); + for (auto& item : active) { + configure_plot(*control, item.id); + item.input = std::make_shared(control, item.id); + item.input->start(); + item.input->receive(nlohmann::json{ + {"kind", "stream"}, + {"viewport", {{"width", configuration.width}, + {"height", configuration.height}}} + }.dump()); + } cooperatively_wait("manual Plot policies", [&] { - return std::ranges::all_of(active, [](const Active_Plot& item) { - return manual_pixel_policy_applied(item.plot); + return std::ranges::all_of(active, [&control](const Active_Plot& item) { + return manual_pixel_policy_applied(*control, item.id); }); }, std::chrono::seconds{10}); @@ -364,7 +381,8 @@ void run_event_latency(benchmark::State& state) { return false; return true; }, std::chrono::seconds{60}); - for (auto& item : active) item.plot->reset_diagnostics(); + for (auto& item : active) + control->reset_plot_diagnostics(item.id); std::vector timings(active.size()); std::string failure; @@ -383,7 +401,7 @@ void run_event_latency(benchmark::State& state) { item.probe->frame_count.load( std::memory_order_acquire); timing.statistic_count_before = event_statistic_count( - item.plot, type); + *control, item.id, type); const auto message = input_message( type, plot_index, sample); timing.submitted_unix_ns = system_time_ns(); @@ -403,7 +421,7 @@ void run_event_latency(benchmark::State& state) { frame_count.load(std::memory_order_acquire) > timings[index].encoded_count_before; const auto observed = event_statistic_count( - active[index].plot, type) > + *control, active[index].id, type) > timings[index].statistic_count_before; complete = complete && encoded && observed; } @@ -425,7 +443,8 @@ void run_event_latency(benchmark::State& state) { published_time_unix_ns.load( std::memory_order_acquire); const auto diagnostics = - active[plot_index].plot->diagnostics(); + control->plot_diagnostics( + active[plot_index].id); auto& result = published_results[plot_index]. events[event_index]; result.websocket_receive_ms.add(milliseconds( diff --git a/web_server/tests/Gallery_Frame_Atlas_Tests.cpp b/web_server/tests/Gallery_Frame_Atlas_Tests.cpp index 24b8373..e6ec242 100644 --- a/web_server/tests/Gallery_Frame_Atlas_Tests.cpp +++ b/web_server/tests/Gallery_Frame_Atlas_Tests.cpp @@ -7,7 +7,7 @@ namespace aethera::web::media::detail { namespace { -std::shared_ptr solid_frame( +std::shared_ptr solid_frame( std::uint64_t sequence, std::uint32_t width, std::uint32_t height, std::byte value) { auto pixels = std::make_shared>( @@ -16,16 +16,16 @@ std::shared_ptr solid_frame( (*pixels)[offset] = value; (*pixels)[offset + 3U] = std::byte{255}; } - return std::make_shared(Plot_Pixel_Frame{ - std::move(pixels), Plot_Pixel_Layout::rgba8, {}, sequence, + return std::make_shared(Gallery_Pixel_Frame{ + std::move(pixels), Gallery_Pixel_Layout::rgba8, {}, sequence, sequence, sequence, sequence, width, height}); } -std::shared_ptr completion_frame( +std::shared_ptr completion_frame( std::uint64_t sequence, std::uint64_t rendered_sequence, std::uint32_t width, std::uint32_t height) { - return std::make_shared(Plot_Pixel_Frame{ - {}, Plot_Pixel_Layout::rgba8, {}, sequence, sequence, + return std::make_shared(Gallery_Pixel_Frame{ + {}, Gallery_Pixel_Layout::rgba8, {}, sequence, sequence, rendered_sequence, rendered_sequence, width, height}); } } diff --git a/web_server/tests/Gallery_Plots_Tests.cpp b/web_server/tests/Gallery_Plots_Tests.cpp index ee38526..f1e1452 100644 --- a/web_server/tests/Gallery_Plots_Tests.cpp +++ b/web_server/tests/Gallery_Plots_Tests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -29,5 +30,19 @@ TEST(Gallery_Plots, Catalog_Is_Complete_And_Unique) { EXPECT_TRUE(has_3d); EXPECT_FALSE(find_gallery_plot_definition("not-a-plot")); } + +TEST(Gallery_Plots, Concrete_Scenes_Submit_Input_Through_Final_Storage) { + for (const auto id : {std::string_view{"axes"}, + std::string_view{"datoviz_splat"}}) { + const auto* definition = find_gallery_plot_definition(id); + ASSERT_NE(definition, nullptr); + auto build = definition->create(); + auto event = std::make_shared( + Event_Type::pointer_move, Event_Timeline_Time{}); + std::visit([event = std::move(event)](auto& scene) mutable { + scene->submit_event(std::move(event)); + }, build.second); + } +} } } diff --git a/web_server/tests/Gallery_Video_Stream_Tests.cpp b/web_server/tests/Gallery_Video_Stream_Tests.cpp index 498ff30..40638c2 100644 --- a/web_server/tests/Gallery_Video_Stream_Tests.cpp +++ b/web_server/tests/Gallery_Video_Stream_Tests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -31,16 +32,15 @@ void corun_until_or_throw(std::string_view operation, if (!failure.empty()) throw std::runtime_error(failure); } -void configure_manual_pixel_delivery(const std::shared_ptr& plot) { - ASSERT_TRUE(plot); - ASSERT_TRUE(plot->write_prop( +void configure_manual_pixel_delivery( + const std::shared_ptr& control, + std::string_view id) { + ASSERT_TRUE(control->write_plot_property(id, "frame-analysis", "pacing_mode", "manual").value( "success", false)); - ASSERT_TRUE(plot->write_prop( - "frame-analysis", "pixel_delivery_enabled", true).value( - "success", false)); ASSERT_NO_THROW(corun_until_or_throw("manual Plot policy", [&] { - const auto diagnostics = plot->diagnostics(); + const auto diagnostics = control->plot_diagnostics(id); + if (!diagnostics.contains("frame_policy")) return false; const auto& configuration = diagnostics.at("frame_policy").at( "configuration"); return configuration.at("mode") == "manual" && @@ -48,10 +48,12 @@ void configure_manual_pixel_delivery(const std::shared_ptr& plot) { })); } -void schedule_manual_frame(const std::shared_ptr& plot, +void schedule_manual_frame( + const std::shared_ptr& control, + std::string_view id, std::uint64_t correlation_sequence) { const auto now = Clock::now(); - plot->schedule_render(Frame_Request{ + control->request_frame(id, Frame_Request{ .issued_at = now, .sequence = correlation_sequence, .time_milliseconds = @@ -71,14 +73,35 @@ TEST(Gallery_Video_Stream, return definition.dimension == Plot_Dimension::two_d; }); ASSERT_NE(found, definitions.end()); - auto plot_a = found->create(); - auto plot_b = found->create(); - configure_manual_pixel_delivery(plot_a); - configure_manual_pixel_delivery(plot_b); - + const auto second = std::ranges::find_if( + found + 1, definitions.end(), [](const Plot_Definition& definition) { + return definition.dimension == Plot_Dimension::two_d; + }); + ASSERT_NE(second, definitions.end()); + const std::string plot_a{found->id}; + const std::string plot_b{second->id}; + auto control_slot = std::make_shared>>(); auto stream = Gallery_Video_Stream::create({ - {"plot-a", plot_a}, {"plot-b", plot_b}}); + {plot_a, [control_slot, plot_a](Frame_Publication_Feedback feedback) { + if (const auto control = control_slot->load()) + control->submit_publication_feedback( + plot_a, std::move(feedback)); + }}, + {plot_b, [control_slot, plot_b](Frame_Publication_Feedback feedback) { + if (const auto control = control_slot->load()) + control->submit_publication_feedback( + plot_b, std::move(feedback)); + }}}); ASSERT_TRUE(stream); + auto control = mcp::Control_Service::create(mcp::Gallery_Output{ + .publish = [stream](std::string_view id, + std::shared_ptr frame) { + return stream->accept_frame(id, std::move(frame)); + }}); + control_slot->store(control); + configure_manual_pixel_delivery(control, plot_a); + configure_manual_pixel_delivery(control, plot_b); constexpr std::size_t frame_count{5}; std::array sequences{}; std::atomic_size_t received{}; @@ -99,21 +122,21 @@ TEST(Gallery_Video_Stream, [] { return true; }, [] { return nlohmann::json::object(); }); - const std::array, frame_count> schedule{ + const std::array schedule{ plot_a, plot_a, plot_b, plot_a, plot_b}; stream->request_taskflow_trace(1); for (std::size_t index = 0; index < schedule.size(); ++index) { - const auto published_before = schedule[index]->diagnostics() + const auto published_before = control->plot_diagnostics(schedule[index]) .at("frame_policy").at("lifecycle") .at("published_frame_count").get(); - schedule_manual_frame(schedule[index], index + 1U); + schedule_manual_frame(control, schedule[index], index + 1U); ASSERT_NO_THROW(corun_until_or_throw("encoded Plot frame", [&] { return received.load(std::memory_order_acquire) > index; })); ASSERT_NO_THROW(corun_until_or_throw( "frame policy publication feedback", [&] { - const auto diagnostics = schedule[index]->diagnostics(); - return diagnostics.at("frame_lifecycle") == "ready" && + const auto diagnostics = control->plot_diagnostics(schedule[index]); + return diagnostics.at("frame_lifecycle") == "running" && diagnostics.at("frame_policy").at("lifecycle") .at("published_frame_count") > published_before; }));