583 lines
32 KiB
C++
583 lines
32 KiB
C++
#include "Control_Service.hpp"
|
|
#include "Control_Service.ipp"
|
|
#include "Control_Requests.hpp"
|
|
#include "Protocol_Type.hpp"
|
|
#include "runtime/Datoviz_Observation_Json.hpp"
|
|
#include "runtime/Gallery_Plots.hpp"
|
|
#include "runtime/Input_Event.hpp"
|
|
#include "runtime/Taskflow_Trace_Json.hpp"
|
|
#include <magic_enum/magic_enum.hpp>
|
|
#include <render_2D/module/plottable/Plottables.hpp>
|
|
#include <render_3D/Render_3D.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace aethera::mcp {
|
|
namespace {
|
|
|
|
using Invoke = Tool_Call_Output (*)(Control_Service&, const nlohmann::json&);
|
|
using Schema = nlohmann::json (*)();
|
|
|
|
struct Operation {
|
|
std::string_view name; /* Stable MCP tool name. */
|
|
std::string_view description; /* Business description used for tool selection. */
|
|
Schema schema; /* Protocol input schema producer. */
|
|
Invoke invoke; /* Protocol-independent business entry point. */
|
|
};
|
|
|
|
struct Control_Stop_State final {
|
|
explicit Control_Stop_State(std::size_t count, std::function<void()> value_completion) : remaining(count), completion(std::move(value_completion)) {}
|
|
void retired() noexcept {
|
|
if (remaining.fetch_sub(1, std::memory_order_acq_rel) != 1) return;
|
|
try {
|
|
completion();
|
|
}
|
|
catch (...) {
|
|
std::terminate();
|
|
}
|
|
}
|
|
std::atomic_size_t remaining;
|
|
std::function<void()> completion;
|
|
};
|
|
|
|
template <typename Request>
|
|
[[nodiscard]] nlohmann::json request_schema() {
|
|
return describe_protocol_type<Request>();
|
|
}
|
|
|
|
template <typename Request, typename Callback>
|
|
[[nodiscard]] Tool_Call_Output decode_and_call(const nlohmann::json& arguments, Callback&& callback) {
|
|
try {
|
|
Request request{};
|
|
decode_protocol_value(request, arguments);
|
|
return std::forward<Callback>(callback)(request);
|
|
}
|
|
catch (const nlohmann::json::exception& failure) {
|
|
return {Tool_Call_Result::invalid_arguments, {}, failure.what()};
|
|
}
|
|
catch (const std::invalid_argument& failure) {
|
|
return {Tool_Call_Result::invalid_arguments, {}, failure.what()};
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json statistics_json(const Statistics_Summary& summary) {
|
|
return {{"sample_count", summary.sample_count}, {"average_ns", summary.average}, {"p95_ns", summary.p95}, {"standard_deviation_ns", summary.standard_deviation}};
|
|
}
|
|
|
|
[[nodiscard]] std::string render_error_name(const scene::Render_Error& error) {
|
|
return std::visit([](auto value) { return std::string{magic_enum::enum_name(value)}; }, error);
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) {
|
|
return {
|
|
{"timer_ticks", state.timer_ticks},
|
|
{"dropped_timer_ticks", state.dropped_timer_ticks},
|
|
{"completed_frames", state.completed_frames},
|
|
{"effective_frames_per_second", state.effective_frames_per_second},
|
|
{"render_capacity_fps", state.render_capacity_fps ? nlohmann::json{*state.render_capacity_fps} : nlohmann::json{nullptr}},
|
|
{"send_capacity_fps", state.send_capacity_fps ? nlohmann::json{*state.send_capacity_fps} : nlohmann::json{nullptr}},
|
|
{"render_time", statistics_json(state.render_time_ns)},
|
|
{"send_time", statistics_json(state.send_time_ns)},
|
|
{"end_to_end_time", statistics_json(state.end_to_end_time_ns)},
|
|
{"configuration_error", state.configuration_error ? nlohmann::json{magic_enum::enum_name(*state.configuration_error)} : nlohmann::json{nullptr}},
|
|
{"render_submission_error", state.render_submission_error ? nlohmann::json{render_error_name(*state.render_submission_error)} : nlohmann::json{nullptr}}
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json frame_policy_schema(const Throttled_Latest_only& policy) {
|
|
const auto user_rate = policy.get<Prop_Tag>(&Frame_Policy::Prop::user_frames_per_second);
|
|
return {
|
|
{"id", "frame-analysis"},
|
|
{"label", "Frame pipeline"},
|
|
{"kind", "analysis"},
|
|
{"fields", nlohmann::json::array({
|
|
{{"key", "user_frames_per_second"}, {"editor", "number"}, {"editable", true}, {"minimum", 0.1}, {"value", user_rate ? nlohmann::json{*user_rate} : nlohmann::json{nullptr}}},
|
|
{{"key", "render_rate_limit_enabled"}, {"editor", "boolean"}, {"editable", true}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::render_rate_limit_enabled)}},
|
|
{{"key", "send_rate_limit_enabled"}, {"editor", "boolean"}, {"editable", true}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::send_rate_limit_enabled)}},
|
|
{{"key", "statistics_enabled"}, {"editor", "boolean"}, {"editable", true}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::statistics_enabled)}},
|
|
{{"key", "statistics_window_size"}, {"editor", "integer"}, {"editable", true}, {"minimum", 1}, {"value", policy.get<Prop_Tag>(&Frame_Policy::Prop::statistics_window_size)}}
|
|
})}
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] Tool_Call_Output list_plots(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Empty_Request>(arguments, [&service](const auto&) { return Tool_Call_Output{Tool_Call_Result::ok, service.plot_catalog(), {}}; });
|
|
}
|
|
|
|
[[nodiscard]] Tool_Call_Output plot_schema(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
|
|
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<Plot_Request>(arguments, [&service](const auto& request) {
|
|
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 component_state(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Component_Request>(arguments, [&service](const auto& request) {
|
|
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), {}};
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] Tool_Call_Output write_property(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Write_Property_Request>(arguments, [&service](const auto& request) {
|
|
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"};
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] Tool_Call_Output generate_data(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Generate_Data_Request>(arguments, [&service](const auto& request) {
|
|
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
|
auto result = service.generate_plot_data(request.plot, request.input);
|
|
const bool success = result.value("success", false);
|
|
return Tool_Call_Output{success ? Tool_Call_Result::ok : Tool_Call_Result::rejected, std::move(result), success ? "" : "data generation rejected"};
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] Tool_Call_Output submit_plot_input(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Plot_Input_Request>(arguments, [&service](auto request) {
|
|
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
|
const auto time_milliseconds = request.event.at("time_milliseconds").get<double>();
|
|
service.submit_input(request.plot, web::make_input_event(request.event));
|
|
return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}, {"plot", request.plot}, {"time_milliseconds", time_milliseconds}}, {}};
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] Tool_Call_Output render_plot_at(Control_Service& service, const nlohmann::json& arguments) {
|
|
return decode_and_call<Plot_Render_Request>(arguments, [&service](const auto& request) {
|
|
if (!service.contains_plot(request.plot)) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
|
service.request_frame(request.plot, {.issued_at = std::chrono::steady_clock::now(), .time_milliseconds = request.time_milliseconds, .width = request.width, .height = request.height});
|
|
return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}, {"plot", request.plot}, {"time_milliseconds", request.time_milliseconds}, {"width", request.width}, {"height", request.height}}, {}};
|
|
});
|
|
}
|
|
|
|
constexpr std::array operations{
|
|
Operation{"aethera_plot_list", "List every 2D and 3D gallery plot.", &request_schema<Empty_Request>, &list_plots},
|
|
Operation{"aethera_plot_schema", "Describe a plot and its editable render components.", &request_schema<Plot_Request>, &plot_schema},
|
|
Operation{"aethera_plot_diagnostics", "Read the current adaptive Frame Policy and backend diagnostics.", &request_schema<Plot_Request>, &plot_diagnostics},
|
|
Operation{"aethera_component_state", "Read one render component's published state.", &request_schema<Component_Request>, &component_state},
|
|
Operation{"aethera_component_write", "Write one editable render component property.", &request_schema<Write_Property_Request>, &write_property},
|
|
Operation{"aethera_data_generate", "Generate input data for a gallery plot.", &request_schema<Generate_Data_Request>, &generate_data},
|
|
Operation{"aethera_plot_input", "Submit one timestamped input event without waiting for rendering.", &request_schema<Plot_Input_Request>, &submit_plot_input},
|
|
Operation{"aethera_plot_render_at", "Replace the latest pending plot request consumed by a subsequent policy tick.", &request_schema<Plot_Render_Request>, &render_plot_at}
|
|
};
|
|
|
|
scene::Render_Result map_render_result(const render_2d::Render_Scene_2D_Result& result) {
|
|
if (result) return {};
|
|
return std::visit([](auto error) -> scene::Render_Result {
|
|
using Error = decltype(error);
|
|
if constexpr (std::same_as<Error, render_2d::Render_Scene_2D_State_Result>) {
|
|
switch (error) {
|
|
case render_2d::Render_Scene_2D_State_Result::frame_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::frame_missing});
|
|
case render_2d::Render_Scene_2D_State_Result::completion_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::completion_missing});
|
|
case render_2d::Render_Scene_2D_State_Result::view_inactive: return std::unexpected(scene::Render_Error{scene::Render_State_Result::view_inactive});
|
|
case render_2d::Render_Scene_2D_State_Result::empty_viewport: return std::unexpected(scene::Render_Error{scene::Render_State_Result::empty_viewport});
|
|
}
|
|
std::terminate();
|
|
}
|
|
else {
|
|
return std::unexpected(scene::Render_Error{error});
|
|
}
|
|
}, result.error());
|
|
}
|
|
|
|
scene::Render_Result map_render_result(render_3d::Render_Scene_3D::Render_Result result) {
|
|
using Result = render_3d::Render_Scene_3D::Render_Result;
|
|
switch (result) {
|
|
case Result::submitted: return {};
|
|
case Result::frame_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::frame_missing});
|
|
case Result::completion_missing: return std::unexpected(scene::Render_Error{scene::Render_State_Result::completion_missing});
|
|
case Result::view_inactive: return std::unexpected(scene::Render_Error{scene::Render_State_Result::view_inactive});
|
|
case Result::empty_viewport: return std::unexpected(scene::Render_Error{scene::Render_State_Result::empty_viewport});
|
|
case Result::backend_unavailable: return std::unexpected(scene::Render_Error{scene::Render_State_Result::backend_unavailable});
|
|
}
|
|
std::terminate();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
proxy<scene::Frame> Control_Service::Private::Policy_Scene::Private::create_frame() {
|
|
return entry->create_frame();
|
|
}
|
|
|
|
scene::Render_Result Control_Service::Private::Policy_Scene::Private::render(proxy<scene::Frame>& frame, scene::Render_Completion completion) {
|
|
return entry->render(frame, std::move(completion));
|
|
}
|
|
|
|
Control_Service::Private::Entry::Entry(std::string value_id, web::Gallery_Build build, proxy<frame_policy::Sink> value_sink, std::uint32_t width, std::uint32_t height) : components(std::move(build.first)), scene(std::move(build.second)), sink(std::move(value_sink)), id(std::move(value_id)), output_width(width), output_height(height) {}
|
|
|
|
Control_Service::Private::Entry::~Entry() = default;
|
|
|
|
Control_Service::Private::Entry_Retirement::~Entry_Retirement() noexcept {
|
|
entry.reset();
|
|
if (!completion) return;
|
|
try {
|
|
completion();
|
|
}
|
|
catch (...) {
|
|
std::terminate();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Throttled_Latest_only> Control_Service::Private::Entry::current_policy() const noexcept {
|
|
return policy.load(std::memory_order_acquire);
|
|
}
|
|
|
|
void Control_Service::Private::Entry::start() {
|
|
auto timer = frame_policy::make_timer_service();
|
|
auto scene_endpoint = make_model_proxy<frame_policy::Scene, Policy_Scene>(this);
|
|
Throttled_Latest_only::Builder builder(std::move(timer), std::move(scene_endpoint), std::move(sink));
|
|
builder
|
|
.set<Prop_Tag>(&Frame_Policy::Prop::user_frames_per_second, std::optional<double>{100.0})
|
|
.set<Prop_Tag>(&Frame_Policy::Prop::render_rate_limit_enabled, false)
|
|
.set<Prop_Tag>(&Frame_Policy::Prop::send_rate_limit_enabled, false)
|
|
.set<Prop_Tag>(&Frame_Policy::Prop::statistics_enabled, true)
|
|
.set<Prop_Tag>(&Frame_Policy::Prop::statistics_window_size, std::size_t{120});
|
|
auto next = std::shared_ptr<Throttled_Latest_only>{builder.build().release()};
|
|
const auto started = next->start();
|
|
if (!started) throw std::logic_error("Gallery Frame Policy rejected start");
|
|
policy.store(std::move(next), std::memory_order_release);
|
|
}
|
|
|
|
void Control_Service::Private::Entry::stop(std::shared_ptr<Entry_Retirement> retirement) noexcept {
|
|
auto current = policy.exchange({}, std::memory_order_acq_rel);
|
|
if (!current) return;
|
|
auto lifetime = std::make_shared<Entry_Stop_Lifetime>();
|
|
lifetime->retirement = std::move(retirement);
|
|
lifetime->policy = std::move(current);
|
|
const auto stopped = lifetime->policy->stop([lifetime] {});
|
|
if (!stopped) std::terminate();
|
|
}
|
|
|
|
void Control_Service::Private::Entry::request_frame(web::Gallery_Frame_Request request) {
|
|
if (!std::isfinite(request.time_milliseconds) || request.width == 0 || request.height == 0) throw std::invalid_argument("Gallery frame request is invalid");
|
|
if (request.issued_at == std::chrono::steady_clock::time_point{}) request.issued_at = std::chrono::steady_clock::now();
|
|
requested_frame.store(std::make_shared<const web::Gallery_Frame_Request>(std::move(request)), std::memory_order_release);
|
|
}
|
|
|
|
bool Control_Service::Private::Entry::reserve_trace() noexcept {
|
|
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)) return true;
|
|
return false;
|
|
}
|
|
|
|
void Control_Service::Private::Entry::restore_trace() noexcept {
|
|
taskflow_trace_remaining.fetch_add(1, std::memory_order_release);
|
|
}
|
|
|
|
void Control_Service::Private::Entry::store_trace(const Taskflow_Execution_Trace& trace, const web::Gallery_Frame_Request& request, const nlohmann::json& backend) {
|
|
auto value = web::taskflow_trace_json(trace, request.sequence, request.correlation_id, {}, backend);
|
|
if (const auto current = current_policy()) current->get<State_Tag>([&](const Frame_Policy::State& state) { value["frame_policy"] = frame_policy_state_json(state); });
|
|
auto encoded = std::make_shared<const nlohmann::json>(std::move(value));
|
|
auto control = taskflow_trace_control.load(std::memory_order_acquire);
|
|
for (;;) {
|
|
const auto requested = static_cast<std::uint32_t>(control >> 32U);
|
|
const auto captured = static_cast<std::uint32_t>(control);
|
|
if (captured >= requested) return;
|
|
taskflow_trace_slots[captured].store(encoded, std::memory_order_release);
|
|
const auto next = (static_cast<std::uint64_t>(requested) << 32U) | static_cast<std::uint64_t>(captured + 1U);
|
|
if (taskflow_trace_control.compare_exchange_weak(control, next, std::memory_order_release, std::memory_order_acquire)) return;
|
|
}
|
|
}
|
|
|
|
proxy<scene::Frame> Control_Service::Private::Entry::create_frame() {
|
|
const bool use_3d = std::holds_alternative<std::unique_ptr<render_3d::Render_Scene_3D>>(scene);
|
|
return pro::make_proxy<scene::Frame, web::Gallery_Frame_Slot>(use_3d);
|
|
}
|
|
|
|
scene::Render_Result Control_Service::Private::Entry::render(proxy<scene::Frame>& frame, scene::Render_Completion completion) {
|
|
auto* slot = frame ? proxy_cast<web::Gallery_Frame_Slot>(&*frame) : nullptr;
|
|
if (!slot) return std::unexpected(scene::Render_Error{scene::Render_State_Result::frame_missing});
|
|
if (!completion) return std::unexpected(scene::Render_Error{scene::Render_State_Result::completion_missing});
|
|
auto request_owner = requested_frame.exchange({}, std::memory_order_acq_rel);
|
|
web::Gallery_Frame_Request request = request_owner ? *request_owner : web::Gallery_Frame_Request{};
|
|
if (!request_owner) {
|
|
request.issued_at = std::chrono::steady_clock::now();
|
|
request.time_milliseconds = std::chrono::duration<double, std::milli>(request.issued_at - started_at).count();
|
|
request.width = output_width;
|
|
request.height = output_height;
|
|
}
|
|
request.sequence = next_sequence.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
if (request.correlation_id == 0) request.correlation_id = request.sequence;
|
|
slot->request = request;
|
|
++slot->generation;
|
|
slot->taskflow_trace_reserved = reserve_trace();
|
|
components->update(request);
|
|
auto* policy_frame = std::addressof(frame); /* Non-owning pointer to a stable policy slot; Frame Policy keeps it alive until completion returns. */
|
|
if (auto* native = std::get_if<std::unique_ptr<render_2d::Frame_2D>>(&slot->native)) {
|
|
auto& scene_2d = *std::get<std::unique_ptr<render_2d::Render_Scene_2D>>(scene);
|
|
scene_2d.set<Prop_Tag>(&render_2d::Render_Scene_2D::Prop::viewport, render_2d::Size{static_cast<int>(request.width), static_cast<int>(request.height)});
|
|
(*native)->begin({request.sequence, slot->generation}, render_2d::Frame_2D::native_pixel_format);
|
|
if (slot->taskflow_trace_reserved) (*native)->request_taskflow_trace();
|
|
auto result = map_render_result(scene_2d.render(native->get(), [this, slot, policy_frame, completion = std::move(completion)](render_2d::Frame_2D*, std::exception_ptr failure) mutable {
|
|
if (std::exchange(slot->taskflow_trace_reserved, false)) {
|
|
if (!failure) {
|
|
if (auto trace = std::get<std::unique_ptr<render_2d::Frame_2D>>(slot->native)->take_taskflow_trace()) store_trace(*trace, slot->request);
|
|
else restore_trace();
|
|
}
|
|
else restore_trace();
|
|
}
|
|
completion(*policy_frame, std::move(failure));
|
|
}));
|
|
if (!result && std::exchange(slot->taskflow_trace_reserved, false)) restore_trace();
|
|
return result;
|
|
}
|
|
auto& scene_3d = *std::get<std::unique_ptr<render_3d::Render_Scene_3D>>(scene);
|
|
auto& native = std::get<std::unique_ptr<render_3d::Frame_3D>>(slot->native);
|
|
scene_3d.set<Prop_Tag>(&render_3d::Render_Scene_3D::Prop::viewport, render_3d::Extent{request.width, request.height});
|
|
native->begin({request.sequence, slot->generation}, render_3d::Frame_3D_Output::pixels, render_3d::Frame_3D::native_pixel_format);
|
|
if (slot->taskflow_trace_reserved) native->request_taskflow_trace();
|
|
auto result = map_render_result(scene_3d.render(native.get(), [this, slot, policy_frame, completion = std::move(completion)](render_3d::Frame_3D*, std::exception_ptr failure) mutable {
|
|
auto& completed = std::get<std::unique_ptr<render_3d::Frame_3D>>(slot->native);
|
|
nlohmann::json backend;
|
|
if (!failure) {
|
|
if (auto observation = completed->take_datoviz_observation()) {
|
|
backend = datoviz_observation_json(*observation);
|
|
datoviz_observation.store(std::make_shared<const nlohmann::json>(backend), std::memory_order_release);
|
|
}
|
|
}
|
|
if (std::exchange(slot->taskflow_trace_reserved, false)) {
|
|
if (!failure) {
|
|
if (auto trace = completed->take_taskflow_trace()) store_trace(*trace, slot->request, backend);
|
|
else restore_trace();
|
|
}
|
|
else restore_trace();
|
|
}
|
|
completion(*policy_frame, std::move(failure));
|
|
}));
|
|
if (!result && std::exchange(slot->taskflow_trace_reserved, false)) restore_trace();
|
|
return result;
|
|
}
|
|
|
|
nlohmann::json Control_Service::Private::Entry::schema() const {
|
|
auto result = components->schema();
|
|
if (const auto current = 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 = current_policy();
|
|
if (!current) return {{"protocol", "aethera.plot.diagnostics"}, {"version", 6}, {"available", false}, {"plot", id}};
|
|
nlohmann::json result;
|
|
current->get<State_Tag>([&](const Frame_Policy::State& state) {
|
|
result = {
|
|
{"protocol", "aethera.plot.diagnostics"},
|
|
{"version", 6},
|
|
{"available", true},
|
|
{"plot", id},
|
|
{"dimension", std::holds_alternative<std::unique_ptr<render_3d::Render_Scene_3D>>(scene) ? "3D" : "2D"},
|
|
{"frame_policy", frame_policy_state_json(state)}
|
|
};
|
|
});
|
|
if (const auto observation = datoviz_observation.load(std::memory_order_acquire)) result["datoviz"] = *observation;
|
|
return result;
|
|
}
|
|
|
|
void Control_Service::Private::Entry::reset_diagnostics() noexcept {
|
|
datoviz_observation.store({}, std::memory_order_release);
|
|
}
|
|
|
|
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("Plot 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<std::uint32_t>(control >> 32U);
|
|
const auto captured = static_cast<std::uint32_t>(control);
|
|
if (requested != captured) throw std::logic_error("A Plot Taskflow trace request is already active");
|
|
const auto next = static_cast<std::uint64_t>(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 control = taskflow_trace_control.load(std::memory_order_acquire);
|
|
const auto requested = static_cast<std::uint32_t>(control >> 32U);
|
|
const auto captured = static_cast<std::uint32_t>(control);
|
|
for (std::uint32_t index = 0; index < captured; ++index) if (const auto frame = taskflow_trace_slots[index].load(std::memory_order_acquire)) frames.push_back(*frame);
|
|
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() : d(std::make_unique<Private>()) {}
|
|
|
|
Control_Service::~Control_Service() {
|
|
if (!d || d->stop_started) return;
|
|
try {
|
|
stop([] {});
|
|
}
|
|
catch (...) {
|
|
std::terminate();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Control_Service> Control_Service::create(Gallery_Output output) {
|
|
auto service = std::shared_ptr<Control_Service>(new Control_Service);
|
|
service->d->plots.reserve(web::gallery_plot_definitions().size());
|
|
for (const auto& definition : web::gallery_plot_definitions()) {
|
|
auto sink = output.make_sink ? output.make_sink(definition.id) : proxy<frame_policy::Sink>{};
|
|
/* Headless MCP and partial test catalogs intentionally complete frames without media. */
|
|
if (!sink) sink = make_model_proxy<frame_policy::Sink, Private::Headless_Sink>();
|
|
auto entry = std::make_shared<Private::Entry>(std::string{definition.id}, definition.create(), std::move(sink), output.width, output.height);
|
|
service->d->plots.emplace(entry->id, entry);
|
|
entry->start();
|
|
}
|
|
return service;
|
|
}
|
|
|
|
bool Control_Service::contains_plot(std::string_view id) const noexcept {
|
|
return d->plots.contains(std::string{id});
|
|
}
|
|
|
|
void Control_Service::stop(std::function<void()> completion) {
|
|
if (!completion) throw std::invalid_argument("Control Service stop completion is empty");
|
|
if (d->stop_started) throw std::logic_error("Control Service stop already started");
|
|
d->stop_started = true;
|
|
if (d->plots.empty()) {
|
|
completion();
|
|
return;
|
|
}
|
|
auto state = std::make_shared<Control_Stop_State>(d->plots.size(), std::move(completion));
|
|
std::vector<std::shared_ptr<Private::Entry_Retirement>> retiring;
|
|
retiring.reserve(d->plots.size());
|
|
for (auto& [id, entry] : d->plots) {
|
|
(void)id;
|
|
auto retirement = std::make_shared<Private::Entry_Retirement>();
|
|
retirement->entry = std::move(entry);
|
|
retirement->completion = [state] { state->retired(); };
|
|
retiring.push_back(std::move(retirement));
|
|
}
|
|
d->plots.clear();
|
|
for (const auto& retirement : retiring) retirement->entry->stop(retirement);
|
|
}
|
|
|
|
void Control_Service::submit_input(std::string_view id, std::unique_ptr<Event> 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->scene);
|
|
}
|
|
|
|
void Control_Service::request_frame(std::string_view id, web::Gallery_Frame_Request request) {
|
|
const auto found = d->plots.find(std::string{id});
|
|
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
|
|
found->second->request_frame(std::move(request));
|
|
}
|
|
|
|
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);
|
|
const auto current = entry.current_policy();
|
|
if (!current) return {{"success", false}, {"error", "Frame Policy is stopping"}};
|
|
if (key == "user_frames_per_second") {
|
|
std::optional<double> rate;
|
|
if (!value.is_null()) {
|
|
if (!value.is_number()) return {{"success", false}, {"error", "number or null required"}};
|
|
rate = value.get<double>();
|
|
if (!std::isfinite(*rate) || *rate <= 0.0) return {{"success", false}, {"error", "positive finite frame rate required"}};
|
|
}
|
|
current->set<Prop_Tag>(&Frame_Policy::Prop::user_frames_per_second, rate);
|
|
return {{"success", true}, {"component", component}, {"key", key}, {"value", rate ? nlohmann::json{*rate} : nlohmann::json{nullptr}}};
|
|
}
|
|
if (key == "statistics_window_size") {
|
|
if (!value.is_number_unsigned()) return {{"success", false}, {"error", "positive integer required"}};
|
|
const auto window = value.get<std::size_t>();
|
|
if (window == 0) return {{"success", false}, {"error", "positive integer required"}};
|
|
current->set<Prop_Tag>(&Frame_Policy::Prop::statistics_window_size, window);
|
|
return {{"success", true}, {"component", component}, {"key", key}, {"value", window}};
|
|
}
|
|
if (key == "render_rate_limit_enabled" || key == "send_rate_limit_enabled" || key == "statistics_enabled") {
|
|
if (!value.is_boolean()) return {{"success", false}, {"error", "boolean required"}};
|
|
const bool enabled = value.get<bool>();
|
|
if (key == "render_rate_limit_enabled") current->set<Prop_Tag>(&Frame_Policy::Prop::render_rate_limit_enabled, enabled);
|
|
else if (key == "send_rate_limit_enabled") current->set<Prop_Tag>(&Frame_Policy::Prop::send_rate_limit_enabled, enabled);
|
|
else current->set<Prop_Tag>(&Frame_Policy::Prop::statistics_enabled, enabled);
|
|
return {{"success", true}, {"component", component}, {"key", key}, {"value", enabled}};
|
|
}
|
|
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");
|
|
found->second->reset_diagnostics();
|
|
}
|
|
|
|
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});
|
|
if (found == d->plots.end()) throw std::invalid_argument("unknown plot");
|
|
return found->second->trace();
|
|
}
|
|
|
|
nlohmann::json Control_Service::plot_catalog() const {
|
|
nlohmann::json result = nlohmann::json::array();
|
|
for (const auto& definition : web::gallery_plot_definitions()) result.push_back({{"id", definition.id}, {"title", definition.title}, {"category", definition.category}, {"description", definition.description}, {"dimension", web::plot_dimension_name(definition.dimension)}});
|
|
return result;
|
|
}
|
|
|
|
nlohmann::json Control_Service::tool_catalog() const {
|
|
nlohmann::json result = nlohmann::json::array();
|
|
for (const auto& operation : operations) result.push_back({{"name", operation.name}, {"description", operation.description}, {"inputSchema", operation.schema()}});
|
|
return result;
|
|
}
|
|
|
|
Tool_Call_Output Control_Service::call_tool(std::string_view name, const nlohmann::json& arguments) {
|
|
for (const auto& operation : operations) if (operation.name == name) return operation.invoke(*this, arguments);
|
|
return {Tool_Call_Result::unknown_tool, {}, "unknown tool"};
|
|
}
|
|
|
|
} // namespace aethera::mcp
|