完美一版

This commit is contained in:
2026-08-22 00:57:37 +08:00
parent e3d3bfd767
commit a8f102bdb1
36 changed files with 1374 additions and 354 deletions
+12 -9
View File
@@ -18,7 +18,7 @@ struct Graph_WebSocket::Private {
};
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Plot> plot) : d(std::make_unique<Private>()) { d->connection = connection; d->plot = std::move(plot); d->owner = connection.get(); }
Graph_WebSocket::~Graph_WebSocket() { close(); }
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::string pixels) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (connection && connection->connected()) connection->send(pixels.data(), pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::shared_ptr<const Plot_Frame_Message> frame) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (!connection || !connection->connected()) return; connection->send(frame->metadata, drogon::WebSocketMessageType::Text); connection->send(frame->pixels.data(), frame->pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
void Graph_WebSocket::receive(std::string_view message) {
const auto json = nlohmann::json::parse(message, nullptr, false);
if (json.is_discarded() || !json.is_object()) return;
@@ -26,14 +26,16 @@ void Graph_WebSocket::receive(std::string_view message) {
if (kind == json.end() || !kind->is_string()
|| (*kind != "frame" && *kind != "input")) return;
try {
Plot_Event event;
Plot_Frame_Request request;
if (const auto value = json.find("request_id"); value != json.end() && value->is_number_unsigned())
request.correlation_id = value->get<std::uint64_t>();
if (const auto value = json.find("time"); value != json.end() && value->is_number())
event.time_milliseconds = value->get<double>();
request.time_milliseconds = value->get<double>();
if (const auto viewport = json.find("viewport"); viewport != json.end() && viewport->is_object()) {
if (const auto value = viewport->find("width"); value != viewport->end() && value->is_number_unsigned())
event.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U);
request.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U);
if (const auto value = viewport->find("height"); value != viewport->end() && value->is_number_unsigned())
event.height = std::clamp(value->get<std::uint32_t>(), 120U, 1080U);
request.height = std::clamp(value->get<std::uint32_t>(), 120U, 1080U);
}
if (*kind == "input") {
const auto input = json.find("event");
@@ -47,8 +49,8 @@ void Graph_WebSocket::receive(std::string_view message) {
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<double>(event.width)) : x;
point.y = viewport_relative ? std::clamp(y, 0.0, static_cast<double>(event.height)) : y;
point.x = viewport_relative ? std::clamp(x, 0.0, static_cast<double>(request.width)) : x;
point.y = viewport_relative ? std::clamp(y, 0.0, static_cast<double>(request.height)) : y;
};
read_point("position", decoded.position, true);
read_point("global_position", decoded.global_position, false);
@@ -62,9 +64,10 @@ void Graph_WebSocket::receive(std::string_view message) {
decoded.key = magic_enum::enum_cast<Key>(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);
event.input = decoded;
d->plot->submit_input(std::move(decoded));
return;
}
d->plot->submit(event);
d->plot->submit_frame(d->owner, std::move(request));
} catch (const nlohmann::json::exception&) {
return;
}
+261 -99
View File
@@ -3,16 +3,17 @@
#include <asio/co_spawn.hpp>
#include <asio/error_code.hpp>
#include <asio/experimental/concurrent_channel.hpp>
#include <asio/post.hpp>
#include <asio/redirect_error.hpp>
#include <asio/strand.hpp>
#include <asio/use_awaitable.hpp>
#include <render_2D/plottable/Plottables.hpp>
#include <render_3D/Render_3D.hpp>
#include <magic_enum/magic_enum.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <deque>
#include <memory>
#include <mutex>
#include <numbers>
@@ -33,23 +34,104 @@ using Frequency_Axis_Object = Impl<Frequency_Axis>;
using Numeric_Axis_Object = Impl<Numeric_Axis>;
using Time_Axis_Object = Impl<Time_Axis>;
using Selection_Object = Impl<Selection_Rectangle_Overlay>;
constexpr std::uint16_t frame_protocol_version{4};
template <typename Integer>
void append_binary(std::string& output, Integer value) {
const auto start = output.size();
output.resize(start + sizeof(Integer));
std::memcpy(output.data() + start, &value, sizeof(Integer));
enum class Frame_Pacing_Mode : std::uint32_t {
manual,
fixed_rate,
minimum_latency,
maximum_rate
};
struct Frame_Pacing_Properties {
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 控制后继 render 请求节奏的策略。 */
double fixed_rate_fps{30.0}; /* 固定频率策略的目标帧率,单位为 FPS。 */
double minimum_latency_headroom{1.25}; /* 最低延迟策略相对 P95 生成耗时的安全系数。 */
};
class Frame_Policy final {
public:
[[nodiscard]] Frame_Pacing_Properties snapshot() const;
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value);
private:
mutable std::mutex mutex; /* 保护异步帧发布与 HTTP 属性编辑读取。 */
Frame_Pacing_Properties pacing{}; /* 可编辑帧调度属性的唯一权威来源。 */
};
std::string_view pacing_mode_name(Frame_Pacing_Mode mode) {
switch (mode) {
case Frame_Pacing_Mode::manual: return "manual";
case Frame_Pacing_Mode::fixed_rate: return "fixed_rate";
case Frame_Pacing_Mode::minimum_latency: return "minimum_latency";
case Frame_Pacing_Mode::maximum_rate: return "maximum_rate";
}
throw std::logic_error("unknown frame pacing mode");
}
std::string encode_frame(Image_View image, std::uint64_t sequence) {
std::optional<Frame_Pacing_Mode> parse_pacing_mode(std::string_view value) {
if (value == "manual") return Frame_Pacing_Mode::manual;
if (value == "fixed_rate") return Frame_Pacing_Mode::fixed_rate;
if (value == "minimum_latency") return Frame_Pacing_Mode::minimum_latency;
if (value == "maximum_rate") return Frame_Pacing_Mode::maximum_rate;
return std::nullopt;
}
Frame_Pacing_Properties Frame_Policy::snapshot() const { std::lock_guard lock(mutex); return pacing; }
nlohmann::json Frame_Policy::schema() const {
std::lock_guard lock(mutex);
nlohmann::json fields = nlohmann::json::array();
fields.push_back({{"key", "pacing_mode"}, {"label", "帧刷新策略"}, {"editor", "select"}, {"editable", true}, {"description", "选择浏览器如何安排下一次 render 调用。"}, {"technical_description", "Controls client-side render cadence using end-to-end samples computed by the browser."}, {"value", pacing_mode_name(pacing.mode)}, {"options", nlohmann::json::array({{{"value", "manual"}, {"label", "手动刷新"}}, {{"value", "fixed_rate"}, {"label", "固定频率"}}, {{"value", "minimum_latency"}, {"label", "最低延迟"}}, {{"value", "maximum_rate"}, {"label", "最高频率"}}})}});
fields.push_back({{"key", "fixed_rate_fps"}, {"label", "固定目标帧率"}, {"editor", "number"}, {"editable", true}, {"description", "固定频率策略下每秒发起的 render 次数。"}, {"technical_description", "Target render request rate used by fixed_rate pacing, in frames per second."}, {"value", pacing.fixed_rate_fps}});
fields.push_back({{"key", "minimum_latency_headroom"}, {"label", "最低延迟余量"}, {"editor", "number"}, {"editable", true}, {"description", "最低延迟策略使用的浏览器端 P95 端到端耗时安全系数。"}, {"technical_description", "Multiplier applied to browser-computed P95 request-to-pixel latency before scheduling the next render request."}, {"value", pacing.minimum_latency_headroom}});
return {{"id", "frame-runtime"}, {"label", "帧策略与诊断"}, {"kind", "runtime"}, {"fields", std::move(fields)}, {"state", nlohmann::json::object()}};
}
nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::json& value) {
std::lock_guard lock(mutex);
if (key == "pacing_mode") {
if (!value.is_string()) return {{"success", false}, {"error", "pacing_mode requires a string"}};
const auto parsed = parse_pacing_mode(value.get_ref<const std::string&>());
if (!parsed) return {{"success", false}, {"error", "unknown frame pacing mode"}};
pacing.mode = *parsed;
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing_mode_name(pacing.mode)}};
}
if (key == "fixed_rate_fps") {
if (!value.is_number()) return {{"success", false}, {"error", "fixed_rate_fps requires a number"}};
const double next = value.get<double>();
if (!std::isfinite(next) || next < 0.1 || next > 240.0) return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 240"}};
pacing.fixed_rate_fps = next;
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing.fixed_rate_fps}};
}
if (key == "minimum_latency_headroom") {
if (!value.is_number()) return {{"success", false}, {"error", "minimum_latency_headroom requires a number"}};
const double next = value.get<double>();
if (!std::isfinite(next) || next < 1.0 || next > 4.0) return {{"success", false}, {"error", "minimum_latency_headroom must be between 1 and 4"}};
pacing.minimum_latency_headroom = next;
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing.minimum_latency_headroom}};
}
return {{"success", false}, {"error", "unknown frame runtime property"}};
}
nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uint32_t height, std::size_t byte_length, const Frame_Pacing_Properties& pacing) {
nlohmann::json markers = nlohmann::json::object();
for (const auto& point : frame.trace_points()) markers[std::string(magic_enum::enum_name(point.marker))] = point.elapsed_ns;
nlohmann::json measurements = nlohmann::json::object();
for (const auto& value : frame.trace_values()) measurements[std::string(magic_enum::enum_name(value.measurement))] = value.value_ns;
const auto identity = frame.identity();
return {{"kind", "frame_metadata"}, {"protocol", "aethera.frame"}, {"version", frame_protocol_version},
{"sequence", identity.sequence}, {"correlation_id", identity.correlation_id},
{"created_time_unix_ms", static_cast<double>(frame.created_time_unix_ns()) / 1'000'000.0},
{"pixel", {{"width", width}, {"height", height}, {"format", "rgba8"}, {"byte_length", byte_length}}},
{"pacing", {{"mode", pacing_mode_name(pacing.mode)}, {"fixed_rate_fps", pacing.fixed_rate_fps}, {"minimum_latency_headroom", pacing.minimum_latency_headroom}}},
{"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)}, {"measurements", std::move(measurements)}}}};
}
std::shared_ptr<const Plot_Frame_Message> encode_frame(Frame_2D* frame, const Frame_Pacing_Properties& pacing) {
frame->mark(Frame_Trace_Marker::websocket_publish_started);
const Image_View image = frame->image();
std::string output;
output.reserve(24 + static_cast<std::size_t>(image.width) * image.height * 4);
append_binary(output, std::uint32_t{0x41544852});
append_binary(output, std::uint16_t{1});
append_binary(output, std::uint16_t{});
append_binary(output, static_cast<std::uint32_t>(image.width));
append_binary(output, static_cast<std::uint32_t>(image.height));
append_binary(output, sequence);
output.reserve(static_cast<std::size_t>(image.width) * image.height * 4);
for (int y = 0; y < image.height; ++y) {
const auto* row = reinterpret_cast<const std::uint8_t*>(
image.data + static_cast<std::ptrdiff_t>(y) * image.stride);
@@ -61,36 +143,42 @@ std::string encode_frame(Image_View image, std::uint64_t sequence) {
output.push_back(static_cast<char>(pixel[3]));
}
}
return output;
frame->mark(Frame_Trace_Marker::websocket_publish_finished);
auto message = std::make_shared<Plot_Frame_Message>();
message->pixels = std::move(output);
message->metadata = frame_metadata(*frame, static_cast<std::uint32_t>(image.width), static_cast<std::uint32_t>(image.height), message->pixels.size(), pacing).dump();
return message;
}
std::string encode_frame(const Pixel_Frame& frame, std::uint64_t sequence) {
std::string output;
output.reserve(24 + frame.rgba8.size());
append_binary(output, std::uint32_t{0x41544852});
append_binary(output, std::uint16_t{1});
append_binary(output, std::uint16_t{});
append_binary(output, frame.extent.width);
append_binary(output, frame.extent.height);
append_binary(output, sequence);
output.append(reinterpret_cast<const char*>(frame.rgba8.data()), frame.rgba8.size());
return output;
std::shared_ptr<const Plot_Frame_Message> encode_frame(Frame_3D* frame, const Frame_Pacing_Properties& pacing) {
frame->mark(Frame_Trace_Marker::websocket_publish_started);
const auto pixels = frame->pixels();
auto message = std::make_shared<Plot_Frame_Message>();
message->pixels.assign(reinterpret_cast<const char*>(pixels.data()), pixels.size());
frame->mark(Frame_Trace_Marker::websocket_publish_finished);
const auto extent = frame->extent();
message->metadata = frame_metadata(*frame, extent.width, extent.height, message->pixels.size(), pacing).dump();
return message;
}
struct Schema_Query { Plot::Json_Handler handler; };
struct Frame_Submission {
const void* owner{}; /* 只向发起 render 的 WebSocket 连接返回完成帧。 */
Plot_Frame_Request request{}; /* 浏览器帧请求及关联标识。 */
};
struct Prop_Write {
std::string component;
std::string key;
nlohmann::json value;
Plot::Json_Handler handler;
};
using Plot_Input = std::variant<Plot_Event, Schema_Query, Prop_Write>;
using Plot_Input = std::variant<Frame_Submission, Schema_Query, Prop_Write>;
template <typename... Owned_Objects>
class Scene_View_Model final : public Plot::Scene_View {
public:
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
std::function<void(const Plot_Event&)> value_update,
std::function<void(const Plot_Frame_Request&)> value_update,
Owned_Objects... owned_objects)
: descriptors(std::move(value_descriptors)),
update_scene(std::move(value_update)),
@@ -108,11 +196,11 @@ public:
result["component"] = component;
return result;
}
void update(const Plot_Event& event) override { update_scene(event); }
void update(const Plot_Frame_Request& request) override { update_scene(request); }
private:
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
std::function<void(const Plot_Event&)> update_scene;
std::function<void(const Plot_Frame_Request&)> update_scene;
std::tuple<Owned_Objects...> objects;
};
@@ -227,7 +315,7 @@ template <typename... Fields, typename Object, typename... Owned_Objects>
std::unique_ptr<Plot::Scene_View> make_scene_view(
Object& object,
Scene_2D& scene,
std::function<void(const Plot_Event&)> update,
std::function<void(const Plot_Frame_Request&)> update,
Owned_Objects&&... owned_objects) {
using Definition = typename Object::Attached_Object;
using Tag = typename Definition::Base_Tag;
@@ -390,9 +478,8 @@ std::shared_ptr<Plot> make_axes_plot(asio::any_io_executor executor) {
paint.add(frequency.get()); paint.add(numeric.get()); paint.add(time.get());
});
if (!topology) throw std::logic_error("axis gallery topology is invalid");
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
if (event.input) return;
constexpr double day_milliseconds = 86'400'000.0;
time->append_time(Time_Of_Day{static_cast<std::int64_t>(
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))});
@@ -425,9 +512,8 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), spectrum.get());
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
std::array<double, 256> samples{};
for (std::size_t i = 0; i < samples.size(); ++i) {
const double x = static_cast<double>(i) / samples.size();
@@ -485,9 +571,8 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), trace.get());
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
if (event.input) return;
constexpr double day_milliseconds = 86'400'000.0;
const auto tick = time->append_time(Time_Of_Day{static_cast<std::int64_t>(
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))});
@@ -525,23 +610,18 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), sweep.get());
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get(), block_index = std::size_t{}](const Plot_Event& event) mutable {
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
const std::size_t bins_per_block = std::max<std::size_t>(1, state.bins_per_block);
if (block_index >= block_count) {
raw->template set<&Sweep_Spectrum::Prop::blocks>(std::vector<std::vector<Plot_Value>>{});
block_index = 0;
}
const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count;
std::vector<double> values(bins_per_block);
for (std::size_t i = 0; i < values.size(); ++i) {
const auto sweep_index = block_index * values.size() + i;
values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002);
}
raw->append_block(values);
++block_index;
};
auto view = make_scene_view<
Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block", "Number of frequency bins stored in each incoming sweep block.">,
@@ -553,10 +633,10 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">,
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.">,
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Current collection of incremental sweep blocks.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of sweep blocks currently retained.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of frequency points retained across all blocks.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest sweep frame.">>(
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of frequency segments that currently contain data.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of points retained by the single composite sweep curve.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the single composite sweep curve.">>(
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -580,9 +660,8 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), afterglow.get());
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (event.input) return;
std::array<double, 192> values{};
for (std::size_t i = 0; i < values.size(); ++i)
values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(
@@ -626,9 +705,8 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), waterfall.get());
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
if (event.input) return;
std::array<double, 192> values{};
for (std::size_t i = 0; i < values.size(); ++i)
values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(
@@ -679,9 +757,8 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
place_selection_over(scene.get(), selection.get(), constellation.get());
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
if (event.input) return;
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
const int anchor_count = static_cast<int>(state.type);
const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4;
@@ -724,7 +801,7 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
.set(&Render_Scene_2D::Prop::view_active, true);
scene_builder.add_renderable(selection.get());
auto scene = *scene_builder.build();
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
};
auto view = make_scene_view<
@@ -743,33 +820,119 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
struct Plot::Private {
using Scene = std::variant<std::unique_ptr<Scene_2D>, std::unique_ptr<Scene_3D>>;
asio::strand<asio::any_io_executor> strand;
asio::experimental::concurrent_channel<void(asio::error_code, Plot_Input)> inputs;
Scene scene;
std::unique_ptr<Scene_View> view;
std::once_flag start_once;
std::mutex handlers_mutex;
std::unordered_map<const void*, Frame_Handler> handlers;
std::atomic_uint64_t frame_sequence{};
using Frame = std::variant<std::unique_ptr<Frame_2D>, std::unique_ptr<Frame_3D>>;
struct Managed_Frame {
const void* owner{}; /* 发起本帧的 WebSocket 连接身份。 */
Plot_Frame_Request request{}; /* 创建本帧的浏览器请求及 viewport。 */
Frame frame{}; /* Web 层唯一拥有并传给 Scene 的外部帧。 */
};
asio::strand<asio::any_io_executor> strand; /* 串行执行帧请求、属性写入和 Schema 查询。 */
asio::experimental::concurrent_channel<void(asio::error_code, Plot_Input)> inputs; /* 不承载输入事件的异步命令通道。 */
std::unique_ptr<Scene_View> view; /* 使用层数据推进及反射描述实现;拥有 Scene 引用的图元。 */
Scene scene; /* 当前 Plot 唯一拥有的 2D 或 3D Scene;析构先于 view 所有图元。 */
std::once_flag start_once; /* 保证回调与协程只安装一次。 */
std::mutex handlers_mutex; /* 保护跨 Drogon 连接线程修改的订阅表。 */
std::unordered_map<const void*, Frame_Handler> handlers; /* 以连接身份索引的完成帧订阅。 */
std::uint64_t next_frame_sequence{1}; /* 下一外部帧使用的单调序号;只在 strand 访问。 */
Frame_Policy frame_policy{}; /* 仅保存可编辑刷新策略,不保存衍生统计。 */
std::optional<Managed_Frame> active_frame{}; /* 当前由 Scene/异步后端借用指针的外部帧。 */
std::deque<const void*> pending_order{}; /* 按首次等待顺序保存连接身份,避免连接间饥饿。 */
std::unordered_map<const void*, Managed_Frame> pending_frames{}; /* 每个连接只保留最新一个尚未提交 Scene 的外部帧。 */
bool frame_in_flight{}; /* Scene 是否已有一次尚未完成回调的帧。 */
template <typename Scene_Object>
Private(asio::any_io_executor executor,
std::unique_ptr<Scene_Object> value_scene,
std::unique_ptr<Scene_View> value_view)
: strand(asio::make_strand(std::move(executor))), inputs(strand, 32),
scene(std::move(value_scene)), view(std::move(value_view)) {}
view(std::move(value_view)), scene(std::move(value_scene)) {}
void publish(std::string pixels) {
std::vector<Frame_Handler> outputs;
void publish(const void* owner, std::shared_ptr<const Plot_Frame_Message> frame) {
Frame_Handler output;
{
std::lock_guard lock(handlers_mutex);
outputs.reserve(handlers.size());
for (const auto& [owner, handler] : handlers) outputs.push_back(handler);
const auto found = handlers.find(owner);
if (found != handlers.end()) output = found->second;
}
for (auto& output : outputs) output(pixels);
if (output) output(std::move(frame));
}
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] Managed_Frame make_frame(Frame_Submission submission);
void request_frame(Frame_Submission submission);
void render_frame(Managed_Frame frame);
void publish_completed_frame(Render_Frame* frame);
void frame_completed();
};
nlohmann::json Plot::Private::schema() const {
auto result = view->schema();
result["components"].push_back(frame_policy.schema());
return result;
}
Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submission) {
const Frame_Identity identity{next_frame_sequence++, submission.request.correlation_id};
if (std::holds_alternative<std::unique_ptr<Scene_2D>>(scene)) return {submission.owner, std::move(submission.request), std::make_unique<Frame_2D>(identity)};
return {submission.owner, std::move(submission.request), std::make_unique<Frame_3D>(identity)};
}
void Plot::Private::request_frame(Frame_Submission submission) {
auto frame = make_frame(std::move(submission));
if (frame_in_flight) {
const auto found = pending_frames.find(frame.owner);
if (found != pending_frames.end()) found->second = std::move(frame);
else {
const auto owner = frame.owner;
pending_order.push_back(owner);
pending_frames.emplace(owner, std::move(frame));
}
return;
}
render_frame(std::move(frame));
}
void Plot::Private::render_frame(Managed_Frame frame) {
frame_in_flight = true;
active_frame = std::move(frame);
auto& request = active_frame->request;
request.width = std::clamp(request.width, 160U, 1920U);
request.height = std::clamp(request.height, 120U, 1080U);
view->update(request);
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(Size{static_cast<int>(request.width), static_cast<int>(request.height)});
const auto result = (*scene_2d)->render(std::get<std::unique_ptr<Frame_2D>>(active_frame->frame).get());
if (result != Render_Scene_2D::Render_Result::completed) asio::post(strand, [this] { frame_completed(); });
return;
}
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{request.width, request.height});
const auto result = scene_3d->render(std::get<std::unique_ptr<Frame_3D>>(active_frame->frame).get());
if (result != Render_Scene_3D::Render_Result::submitted) asio::post(strand, [this] { frame_completed(); });
}
void Plot::Private::publish_completed_frame(Render_Frame* frame) {
if (!active_frame) throw std::logic_error("frame callback has no externally owned active frame");
const auto pacing = frame_policy.snapshot();
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&active_frame->frame); frame_2d && frame_2d->get() == frame) publish(active_frame->owner, encode_frame(frame_2d->get(), pacing));
else if (auto* frame_3d = std::get_if<std::unique_ptr<Frame_3D>>(&active_frame->frame); frame_3d && frame_3d->get() == frame) publish(active_frame->owner, encode_frame(frame_3d->get(), pacing));
else throw std::logic_error("frame callback does not match the externally owned active frame");
frame_completed();
}
void Plot::Private::frame_completed() {
active_frame.reset();
frame_in_flight = false;
while (!pending_order.empty()) {
const auto owner = pending_order.front();
pending_order.pop_front();
auto next = pending_frames.extract(owner);
if (next.empty()) continue;
render_frame(std::move(next.mapped()));
return;
}
}
Plot::Plot(asio::any_io_executor executor,
std::unique_ptr<Scene_2D> scene,
std::unique_ptr<Scene_View> view)
@@ -786,18 +949,18 @@ void Plot::ensure_started() {
std::call_once(d->start_once, [this] {
auto self = shared_from_this();
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) {
(*scene)->set_frame_callback([weak = weak_from_this()](Image_View image) {
(*scene)->set_frame_callback([weak = weak_from_this()](Frame_2D* frame) {
if (auto owner = weak.lock()) {
const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
owner->d->publish(encode_frame(image, sequence));
frame->mark(Frame_Trace_Marker::callback_finished);
asio::post(owner->d->strand, [weak, frame] { if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); });
}
});
} else {
std::get<std::unique_ptr<Scene_3D>>(d->scene)->set_frame_callback(
[weak = weak_from_this()](std::shared_ptr<const Pixel_Frame> frame) {
[weak = weak_from_this()](Frame_3D* frame) {
if (auto owner = weak.lock()) {
const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
owner->d->publish(encode_frame(*frame, sequence));
frame->mark(Frame_Trace_Marker::callback_finished);
asio::post(owner->d->strand, [weak, frame] { if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); });
}
});
}
@@ -808,28 +971,17 @@ void Plot::ensure_started() {
asio::redirect_error(asio::use_awaitable, error));
if (error) co_return;
if (auto* query = std::get_if<Schema_Query>(&input)) {
query->handler(self->d->view->schema());
query->handler(self->d->schema());
continue;
}
if (auto* write = std::get_if<Prop_Write>(&input)) {
write->handler(self->d->view->write_prop(write->component, write->key, write->value));
write->handler(write->component == "frame-runtime"
? self->d->frame_policy.write_prop(write->key, write->value)
: self->d->view->write_prop(write->component, write->key, write->value));
continue;
}
auto event = std::get<Plot_Event>(input);
event.width = std::clamp(event.width, 160U, 1920U);
event.height = std::clamp(event.height, 120U, 1080U);
self->d->view->update(event);
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&self->d->scene)) {
(*scene)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(event.width), static_cast<int>(event.height)});
if (event.input) dispatch_plot_input(**scene, *event.input);
(*scene)->render();
} else {
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(self->d->scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{event.width, event.height});
if (event.input) dispatch_plot_input(*scene_3d, *event.input);
scene_3d->render();
}
auto submission = std::get<Frame_Submission>(input);
self->d->request_frame(std::move(submission));
}
}, [](std::exception_ptr exception) {
if (exception) std::rethrow_exception(exception);
@@ -850,9 +1002,15 @@ void Plot::detach(const void* owner) {
d->handlers.erase(owner);
}
void Plot::submit(Plot_Event event) {
void Plot::submit_frame(const void* owner, Plot_Frame_Request request) {
ensure_started();
static_cast<void>(d->inputs.try_send(asio::error_code{}, Plot_Input{event}));
static_cast<void>(d->inputs.try_send(asio::error_code{}, Plot_Input{Frame_Submission{owner, std::move(request)}}));
}
void Plot::submit_input(Plot_Input_Event event) {
ensure_started();
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) dispatch_plot_input(**scene, event);
else dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene), event);
}
void Plot::async_schema(Json_Handler handler) {
@@ -876,10 +1034,14 @@ std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F},
render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}}));
visual->advance();
auto scene = *Scene_3D::Builder(visual.get())
/* 保留派生 Builder 身份;链式 set() 返回公共 Builder,不能用其 build() 绕过 3D 后端挂接。 */
Scene_3D::Builder scene_builder(visual.get());
scene_builder
.set(&Render_Scene_3D::Prop::viewport, Extent{720, 420})
.set(&Render_Scene_3D::Prop::view_active, true)
.build();
.set(&Render_Scene_3D::Prop::view_active, true);
auto scene_result = scene_builder.build();
if (!scene_result) throw std::logic_error("Datoviz point plot dependency graph is invalid");
auto scene = std::move(scene_result).value();
using Adapter = detail::Renderable_Adapter<Visual_Object,
detail::Prop_Field<&Point_Visual::Prop::transform, "transform", "World transform applied to every point in the visual.">,
detail::Prop_Field<&Point_Visual::Prop::visible, "visible", "Controls whether the point visual participates in scene rendering.">,
@@ -893,7 +1055,7 @@ std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
components.push_back(detail::make_renderable_descriptor("plot", "主绘图组件", "visual", Adapter{*visual}));
auto view = std::make_unique<Scene_View_Model<decltype(visual)>>(
std::move(components),
[](const Plot_Event&) {}, std::move(visual));
[](const Plot_Frame_Request&) {}, std::move(visual));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
}
+13 -9
View File
@@ -6,7 +6,6 @@
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
namespace aethera::web {
@@ -25,15 +24,19 @@ struct Plot_Input_Event {
std::uint32_t native_key{};
bool auto_repeat{};
};
struct Plot_Event {
double time_milliseconds{};
std::uint32_t width{720};
std::uint32_t height{420};
std::optional<Plot_Input_Event> input{};
struct Plot_Frame_Request {
std::uint64_t correlation_id{}; /* 浏览器请求标识;完成帧元数据原样返回。 */
double time_milliseconds{}; /* 使用层推进图表时间轴的本地日内毫秒。 */
std::uint32_t width{720}; /* 请求帧宽度,单位为物理像素。 */
std::uint32_t height{420}; /* 请求帧高度,单位为物理像素。 */
};
struct Plot_Frame_Message {
std::string metadata{}; /* 单帧 JSON 元数据 WebSocket 文本消息。 */
std::string pixels{}; /* 紧随元数据发送的纯 RGBA8 WebSocket 二进制消息。 */
};
class Plot final : public std::enable_shared_from_this<Plot> {
public:
using Frame_Handler = std::function<void(std::string)>;
using Frame_Handler = std::function<void(std::shared_ptr<const Plot_Frame_Message>)>;
using Json_Handler = std::function<void(nlohmann::json)>;
class Scene_View {
public:
@@ -41,7 +44,7 @@ public:
[[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;
virtual void update(const Plot_Event& event) = 0;
virtual void update(const Plot_Frame_Request& request) = 0;
};
Plot(asio::any_io_executor executor,
std::unique_ptr<Impl<render_2d::Render_Scene_2D>> scene,
@@ -54,7 +57,8 @@ public:
Plot& operator=(const Plot&) = delete;
void attach(const void* owner, Frame_Handler handler);
void detach(const void* owner);
void submit(Plot_Event event);
void submit_frame(const void* owner, Plot_Frame_Request request);
void submit_input(Plot_Input_Event event);
void async_schema(Json_Handler handler);
void async_write_prop(std::string component, std::string key, nlohmann::json value, Json_Handler handler);
private:
+2
View File
@@ -299,6 +299,8 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
{"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"},
{"technical_description", std::string(Field::accessor_type::description.view())},
{"value", encode_protocol_value(value)}};
if constexpr (std::same_as<Value, render_3d::Linear_Color>) item["color_channel_scale"] = "normalized";
else if constexpr (std::same_as<Value, Color>) item["color_channel_scale"] = "byte";
if (editable) {
if constexpr (std::is_enum_v<Value>) {
item["options"] = nlohmann::json::array();