529 lines
23 KiB
C++
529 lines
23 KiB
C++
#include "Gallery_Plot_Session.h"
|
|
#include "Gallery_Protocol.h"
|
|
#include "common/Gallery_Scene_Interface.h"
|
|
#include "render_2D/Gallery_Scene2D.h"
|
|
#include "render_3D/Gallery_Scene3D.h"
|
|
#include <renderive/scheduling/Scheduler.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
namespace renderive::web {
|
|
namespace {
|
|
Gallery_View_Input gallery_input(const Event& event) {
|
|
Gallery_View_Input input;
|
|
switch (event.type) {
|
|
case Event_Type::Show:
|
|
input.type = Gallery_View_Input_Type::Show;
|
|
break;
|
|
case Event_Type::Hide:
|
|
input.type = Gallery_View_Input_Type::Hide;
|
|
break;
|
|
case Event_Type::Leave:
|
|
input.type = Gallery_View_Input_Type::Leave;
|
|
break;
|
|
default:
|
|
input.type = Gallery_View_Input_Type::Leave;
|
|
break;
|
|
}
|
|
return input;
|
|
}
|
|
Gallery_Mouse_Button gallery_mouse_button(Mouse_Button button) {
|
|
switch (button) {
|
|
case Mouse_Button::None:
|
|
return Gallery_Mouse_Button::None;
|
|
case Mouse_Button::Left:
|
|
return Gallery_Mouse_Button::Left;
|
|
case Mouse_Button::Right:
|
|
return Gallery_Mouse_Button::Right;
|
|
case Mouse_Button::Middle:
|
|
return Gallery_Mouse_Button::Middle;
|
|
}
|
|
return Gallery_Mouse_Button::None;
|
|
}
|
|
Gallery_Key gallery_key(Key key) {
|
|
switch (key) {
|
|
case Key::Escape:
|
|
return Gallery_Key::Escape;
|
|
case Key::Enter:
|
|
return Gallery_Key::Enter;
|
|
case Key::Space:
|
|
return Gallery_Key::Space;
|
|
case Key::Delete:
|
|
return Gallery_Key::Delete;
|
|
case Key::Backspace:
|
|
return Gallery_Key::Backspace;
|
|
case Key::Left:
|
|
return Gallery_Key::Left;
|
|
case Key::Right:
|
|
return Gallery_Key::Right;
|
|
case Key::Up:
|
|
return Gallery_Key::Up;
|
|
case Key::Down:
|
|
return Gallery_Key::Down;
|
|
case Key::Unknown:
|
|
return Gallery_Key::Unknown;
|
|
}
|
|
return Gallery_Key::Unknown;
|
|
}
|
|
Gallery_Pointer_Input gallery_input(const Pointer_Event& event) {
|
|
Gallery_Pointer_Input input;
|
|
if (event.type == Event_Type::Pointer_Press)
|
|
input.type = Gallery_Pointer_Input_Type::Press;
|
|
else if (event.type == Event_Type::Pointer_Release)
|
|
input.type = Gallery_Pointer_Input_Type::Release;
|
|
else
|
|
input.type = Gallery_Pointer_Input_Type::Move;
|
|
input.x = event.position.x;
|
|
input.y = event.position.y;
|
|
input.global_x = event.global_position.x;
|
|
input.global_y = event.global_position.y;
|
|
input.button = gallery_mouse_button(event.button);
|
|
input.buttons = event.buttons;
|
|
input.modifiers = static_cast<std::uint8_t>(event.modifiers);
|
|
return input;
|
|
}
|
|
Gallery_Wheel_Input gallery_input(const Wheel_Event& event) {
|
|
Gallery_Wheel_Input input;
|
|
input.x = event.position.x;
|
|
input.y = event.position.y;
|
|
input.global_x = event.global_position.x;
|
|
input.global_y = event.global_position.y;
|
|
input.button = gallery_mouse_button(event.button);
|
|
input.buttons = event.buttons;
|
|
input.modifiers = static_cast<std::uint8_t>(event.modifiers);
|
|
input.angle_delta_x = event.angle_delta_x;
|
|
input.angle_delta_y = event.angle_delta_y;
|
|
input.pixel_delta_x = event.pixel_delta_x;
|
|
input.pixel_delta_y = event.pixel_delta_y;
|
|
return input;
|
|
}
|
|
Gallery_Key_Input gallery_input(const Key_Event& event) {
|
|
Gallery_Key_Input input;
|
|
input.type = event.type == Event_Type::Key_Press ? Gallery_Key_Input_Type::Press : Gallery_Key_Input_Type::Release;
|
|
input.key = gallery_key(event.key);
|
|
input.native_key = event.native_key;
|
|
input.modifiers = static_cast<std::uint8_t>(event.modifiers);
|
|
input.auto_repeat = event.auto_repeat;
|
|
return input;
|
|
}
|
|
std::unique_ptr<Gallery_Scene_Interface> make_gallery_scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) {
|
|
if (case_id.starts_with("datoviz_"))
|
|
return make_gallery_scene_3d(session_id, std::move(case_id), mode, automatic_low_latency);
|
|
return make_gallery_scene_2d(session_id, std::move(case_id), mode, automatic_low_latency);
|
|
}
|
|
}
|
|
struct Gallery_Plot_Session::Impl : std::enable_shared_from_this<Gallery_Plot_Session::Impl> {
|
|
using Clock = std::chrono::steady_clock;
|
|
class Automatic_Render_Scheduler final {
|
|
public:
|
|
static Automatic_Render_Scheduler& instance() {
|
|
static Automatic_Render_Scheduler scheduler;
|
|
return scheduler;
|
|
}
|
|
void arm(std::uint64_t session_id, std::weak_ptr<Impl> session, Clock::time_point deadline) {
|
|
std::lock_guard lock(mutex_);
|
|
if (const auto current = entries_.find(session_id); current != entries_.end()) {
|
|
schedule_.erase(current->second);
|
|
entries_.erase(current);
|
|
}
|
|
const auto iterator = schedule_.emplace(deadline, Entry{session_id, std::move(session)});
|
|
entries_.emplace(session_id, iterator);
|
|
condition_.notify_one();
|
|
}
|
|
void disarm(std::uint64_t session_id) noexcept {
|
|
std::lock_guard lock(mutex_);
|
|
const auto current = entries_.find(session_id);
|
|
if (current == entries_.end())
|
|
return;
|
|
schedule_.erase(current->second);
|
|
entries_.erase(current);
|
|
condition_.notify_one();
|
|
}
|
|
private:
|
|
struct Entry {
|
|
std::uint64_t session_id{};
|
|
std::weak_ptr<Impl> session;
|
|
};
|
|
using Schedule = std::multimap<Clock::time_point, Entry>;
|
|
Automatic_Render_Scheduler() : thread_([this](std::stop_token stop) { run(stop); }) {}
|
|
~Automatic_Render_Scheduler() {
|
|
thread_.request_stop();
|
|
condition_.notify_all();
|
|
}
|
|
void run(std::stop_token stop) noexcept {
|
|
std::unique_lock lock(mutex_);
|
|
while (!stop.stop_requested()) {
|
|
if (schedule_.empty()) {
|
|
condition_.wait(lock, [this, &stop] {
|
|
return stop.stop_requested() || !schedule_.empty();
|
|
});
|
|
continue;
|
|
}
|
|
const auto deadline = schedule_.begin()->first;
|
|
if (condition_.wait_until(lock, deadline) != std::cv_status::timeout)
|
|
continue;
|
|
if (schedule_.empty() || schedule_.begin()->first > Clock::now())
|
|
continue;
|
|
auto iterator = schedule_.begin();
|
|
Entry entry = std::move(iterator->second);
|
|
entries_.erase(entry.session_id);
|
|
schedule_.erase(iterator);
|
|
lock.unlock();
|
|
if (auto session = entry.session.lock())
|
|
session->automatic_render_due();
|
|
lock.lock();
|
|
}
|
|
}
|
|
std::mutex mutex_;
|
|
std::condition_variable condition_;
|
|
Schedule schedule_;
|
|
std::unordered_map<std::uint64_t, Schedule::iterator> entries_;
|
|
std::jthread thread_;
|
|
};
|
|
explicit Impl(bool enable_automatic_low_latency)
|
|
: automatic_low_latency(enable_automatic_low_latency), session_id(next_session_id()) {}
|
|
~Impl() {
|
|
if (automatic_low_latency)
|
|
Automatic_Render_Scheduler::instance().disarm(session_id);
|
|
}
|
|
static std::uint64_t next_session_id() noexcept {
|
|
static std::atomic<std::uint64_t> next{1};
|
|
return next.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
bool update_client_metrics(std::string_view message) {
|
|
if (!scene)
|
|
return false;
|
|
const auto root = nlohmann::json::parse(message, nullptr, false);
|
|
if (root.is_discarded() || !root.is_object() || !root.contains("client_metrics") ||
|
|
!root["client_metrics"].is_object())
|
|
return false;
|
|
const auto& metrics = root["client_metrics"];
|
|
const auto finite_metric = [&metrics](std::string_view name) {
|
|
const auto iterator = metrics.find(std::string(name));
|
|
if (iterator == metrics.end() || !iterator->is_number())
|
|
return 0.0;
|
|
const double value = iterator->get<double>();
|
|
return std::isfinite(value) ? value : 0.0;
|
|
};
|
|
std::uint64_t buffered_bytes{};
|
|
if (const auto iterator = metrics.find("websocket_buffered_bytes");
|
|
iterator != metrics.end() && iterator->is_number_unsigned()) {
|
|
buffered_bytes = iterator->get<std::uint64_t>();
|
|
}
|
|
const auto unsigned_metric = [&metrics](std::string_view name) {
|
|
const auto iterator = metrics.find(std::string(name));
|
|
if (iterator == metrics.end() || !iterator->is_number_integer())
|
|
return std::uint64_t{};
|
|
const auto value = iterator->get<std::int64_t>();
|
|
return value > 0 ? static_cast<std::uint64_t>(value) : std::uint64_t{};
|
|
};
|
|
Gallery_Client_Performance performance;
|
|
performance.transport_fps = finite_metric("transport_fps");
|
|
performance.presentation_fps = finite_metric("presentation_fps");
|
|
performance.websocket_buffered_bytes = buffered_bytes;
|
|
performance.changed_pixel_frames = unsigned_metric("changed_pixel_frames");
|
|
performance.duplicate_pixel_frames = unsigned_metric("duplicate_pixel_frames");
|
|
performance.frame_request_timeout_count =
|
|
unsigned_metric("frame_request_timeout_count");
|
|
performance.frame_round_trip_ms = finite_metric("frame_round_trip_ms");
|
|
performance.frame_round_trip_average_ms =
|
|
finite_metric("frame_round_trip_average_ms");
|
|
performance.frame_round_trip_deviation_ms =
|
|
finite_metric("frame_round_trip_deviation_ms");
|
|
performance.frame_round_trip_p95_ms = finite_metric("frame_round_trip_p95_ms");
|
|
performance.frame_round_trip_p99_ms = finite_metric("frame_round_trip_p99_ms");
|
|
performance.display_interval_ms = finite_metric("display_interval_ms");
|
|
performance.display_interval_average_ms =
|
|
finite_metric("display_interval_average_ms");
|
|
performance.display_interval_latest_ms =
|
|
finite_metric("display_interval_latest_ms");
|
|
performance.display_interval_p95_ms = finite_metric("display_interval_p95_ms");
|
|
performance.display_interval_p99_ms = finite_metric("display_interval_p99_ms");
|
|
performance.display_interval_deviation_ms =
|
|
finite_metric("display_interval_deviation_ms");
|
|
performance.overwritten_pixel_frames =
|
|
unsigned_metric("overwritten_pixel_frames");
|
|
performance.last_pixel_receive_age_ms =
|
|
finite_metric("last_pixel_receive_age_ms");
|
|
performance.last_pixel_change_age_ms =
|
|
finite_metric("last_pixel_change_age_ms");
|
|
scene->set_client_metrics(performance);
|
|
return true;
|
|
}
|
|
[[nodiscard]] std::unique_lock<std::mutex> acquire_foreground_lock() {
|
|
return std::unique_lock<std::mutex>(mutex);
|
|
}
|
|
void arm_automatic_render_locked() noexcept {
|
|
if (!automatic_low_latency)
|
|
return;
|
|
auto& scheduler = Automatic_Render_Scheduler::instance();
|
|
if (!scene || !scene->can_render_automatically() || render_task_pending) {
|
|
scheduler.disarm(session_id);
|
|
return;
|
|
}
|
|
try {
|
|
const auto interval = std::chrono::nanoseconds(
|
|
std::max<std::uint64_t>(1, scene->kernel_refresh_interval_ns()));
|
|
const auto deadline = last_render_started ? *last_render_started + interval : Clock::now();
|
|
scheduler.arm(session_id, weak_from_this(), deadline);
|
|
} catch (...) {
|
|
scheduler.disarm(session_id);
|
|
}
|
|
}
|
|
void automatic_render_due() {
|
|
std::uint64_t generation{};
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (!automatic_low_latency || !scene || !scene->can_render_automatically() || render_task_pending)
|
|
return;
|
|
render_task_pending = true;
|
|
generation = scene_generation;
|
|
}
|
|
auto self = shared_from_this();
|
|
try {
|
|
renderive::scheduling::enqueue_task([self = std::move(self), generation] {
|
|
self->run_automatic_render(generation);
|
|
});
|
|
} catch (...) {
|
|
std::lock_guard lock(mutex);
|
|
render_task_pending = false;
|
|
arm_automatic_render_locked();
|
|
}
|
|
}
|
|
void run_automatic_render(std::uint64_t generation) noexcept {
|
|
const auto started = Clock::now();
|
|
try {
|
|
std::lock_guard lock(mutex);
|
|
if (generation == scene_generation && scene && scene->can_render_automatically()) {
|
|
(void)scene->render_latest_frame();
|
|
last_render_started = started;
|
|
}
|
|
render_task_pending = false;
|
|
arm_automatic_render_locked();
|
|
} catch (...) {
|
|
std::lock_guard lock(mutex);
|
|
render_task_pending = false;
|
|
arm_automatic_render_locked();
|
|
}
|
|
}
|
|
static bool affects_render_schedule(const Web_Event& event) {
|
|
return std::visit(
|
|
[](const auto& value) {
|
|
using T = std::decay_t<decltype(value)>;
|
|
if constexpr (std::is_same_v<T, Frame_Request>) {
|
|
return false;
|
|
}
|
|
else if constexpr (std::is_same_v<T, Gallery_Request>) {
|
|
return value.kind != Gallery_Request_Kind::Catalog &&
|
|
value.kind != Gallery_Request_Kind::Observe &&
|
|
value.kind != Gallery_Request_Kind::Refresh &&
|
|
value.kind != Gallery_Request_Kind::Reset_Monitoring;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
},
|
|
event);
|
|
}
|
|
std::unique_ptr<Gallery_Scene_Interface> scene;
|
|
std::mutex mutex;
|
|
std::optional<Clock::time_point> last_render_started;
|
|
std::uint64_t scene_generation{};
|
|
bool automatic_low_latency{};
|
|
bool render_task_pending{};
|
|
std::uint64_t session_id{};
|
|
std::optional<Web_Response> handle_gallery(const Gallery_Request& request) {
|
|
if (request.kind == Gallery_Request_Kind::Catalog)
|
|
return Web_Response{Web_Response_Type::Json, Gallery_Protocol::catalog_json()};
|
|
if (request.kind == Gallery_Request_Kind::Open) {
|
|
const auto open = Gallery_Protocol::open_request(request.message);
|
|
if (!open)
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json("未知的 gallery case 或 frame_mode", "case")
|
|
};
|
|
scene = make_gallery_scene(session_id, open->case_id, open->frame_mode, automatic_low_latency);
|
|
++scene_generation;
|
|
last_render_started.reset();
|
|
arm_automatic_render_locked();
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(),
|
|
scene->telemetry_json(),
|
|
"Renderive/Kernel 独立画布已创建",
|
|
scene->frame_mode(), false)
|
|
};
|
|
}
|
|
if (!scene)
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json("请先发送 gallery_open")
|
|
};
|
|
if (request.kind == Gallery_Request_Kind::Observe) {
|
|
if (update_client_metrics(request.message))
|
|
arm_automatic_render_locked();
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::observer_json(
|
|
scene->case_id(), scene->frame_mode(), scene->telemetry_json())
|
|
};
|
|
}
|
|
if (request.kind == Gallery_Request_Kind::Refresh) {
|
|
if (update_client_metrics(request.message))
|
|
arm_automatic_render_locked();
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::case_json_from_controls(
|
|
scene->case_id(), scene->controls().dump(),
|
|
scene->telemetry_json(), "观察数据已手动刷新",
|
|
scene->frame_mode(), true)
|
|
};
|
|
}
|
|
if (request.kind == Gallery_Request_Kind::Reset_Monitoring) {
|
|
scene->reset_monitoring();
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::case_json_from_controls(
|
|
scene->case_id(), scene->controls().dump(),
|
|
scene->telemetry_json(), "监测滑动窗口已重置",
|
|
scene->frame_mode(), true)
|
|
};
|
|
}
|
|
if (request.kind == Gallery_Request_Kind::Patch) {
|
|
const auto patch = Gallery_Protocol::control_patch_request(request.message);
|
|
if (!patch)
|
|
return Web_Response{Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json("gallery_patch format is invalid")};
|
|
const auto result = scene->apply_patch(
|
|
patch->target, nlohmann::json::parse(patch->patch_json));
|
|
if (!result.success)
|
|
return Web_Response{Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json(result.message)};
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(),
|
|
scene->telemetry_json(),
|
|
"控件 API 已由后端应用",
|
|
scene->frame_mode(), false)
|
|
};
|
|
}
|
|
const auto action = Gallery_Protocol::action_request(request.message);
|
|
if (!action)
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json("gallery_action 格式无效")
|
|
};
|
|
if (!Gallery_Protocol::action_available(scene->case_id(), scene->frame_mode(),
|
|
action->id))
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json(
|
|
"动作不属于当前画布或帧策略", "action")
|
|
};
|
|
if (action->id == "reset") {
|
|
const std::string id = scene->case_id();
|
|
const auto mode = scene->frame_mode();
|
|
scene = make_gallery_scene(session_id, id, mode, automatic_low_latency);
|
|
++scene_generation;
|
|
last_render_started.reset();
|
|
arm_automatic_render_locked();
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::case_json_from_controls(id, scene->controls().dump(),
|
|
scene->telemetry_json(),
|
|
"本图已恢复后端默认值", mode, false)
|
|
};
|
|
}
|
|
bool recognized{};
|
|
const std::string notice = scene->action(*action, recognized);
|
|
if (!recognized)
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::error_json("动作不属于当前画布或参数无效",
|
|
"action")
|
|
};
|
|
scene->record_action_notice_if_empty(notice);
|
|
return Web_Response{
|
|
Web_Response_Type::Json,
|
|
Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(),
|
|
scene->telemetry_json(), notice,
|
|
scene->frame_mode(), false)
|
|
};
|
|
}
|
|
std::optional<Web_Response> handle_frame_request() {
|
|
const auto request_started = std::chrono::steady_clock::now();
|
|
std::optional<std::string> pixels;
|
|
{
|
|
auto lock = acquire_foreground_lock();
|
|
if (!scene)
|
|
return std::nullopt;
|
|
const auto encode_started = std::chrono::steady_clock::now();
|
|
pixels = scene->encode_latest_pixels();
|
|
const auto encode_finished = std::chrono::steady_clock::now();
|
|
if (pixels)
|
|
scene->record_pixel_response(request_started, encode_started, encode_finished,
|
|
pixels->size());
|
|
}
|
|
if (!pixels)
|
|
return std::nullopt;
|
|
return Web_Response{Web_Response_Type::Pixels, std::move(*pixels)};
|
|
}
|
|
std::optional<Web_Response> handle(const Web_Event& event) {
|
|
if (std::holds_alternative<Frame_Request>(event))
|
|
return handle_frame_request();
|
|
const bool reschedule = affects_render_schedule(event);
|
|
std::optional<Web_Response> response;
|
|
{
|
|
auto lock = acquire_foreground_lock();
|
|
response = std::visit(
|
|
[this](const auto& value) -> std::optional<Web_Response> {
|
|
using T = std::decay_t<decltype(value)>;
|
|
if constexpr (std::is_same_v<T, Gallery_Request>) {
|
|
return handle_gallery(value);
|
|
}
|
|
else if constexpr (std::is_same_v<T, Frame_Request>) {
|
|
return std::nullopt;
|
|
}
|
|
else if constexpr (std::is_same_v<T, Viewport_Resize>) {
|
|
if (scene)
|
|
scene->resize(value.size.width, value.size.height);
|
|
}
|
|
else if constexpr (std::is_same_v<T, Event>) {
|
|
if (scene)
|
|
scene->dispatch(Gallery_Input_Event{gallery_input(value)});
|
|
}
|
|
else if constexpr (Event_Object<T>) {
|
|
if (scene)
|
|
scene->dispatch(Gallery_Input_Event{gallery_input(value)});
|
|
}
|
|
return std::nullopt;
|
|
},
|
|
event);
|
|
if (reschedule)
|
|
arm_automatic_render_locked();
|
|
}
|
|
return response;
|
|
}
|
|
};
|
|
Gallery_Plot_Session::Gallery_Plot_Session() : Gallery_Plot_Session(false) {}
|
|
Gallery_Plot_Session::Gallery_Plot_Session(bool automatic_low_latency)
|
|
: impl_(std::make_shared<Impl>(automatic_low_latency)) {}
|
|
Gallery_Plot_Session::~Gallery_Plot_Session() = default;
|
|
std::optional<Web_Response> Gallery_Plot_Session::handle(const Web_Event& event) {
|
|
return impl_->handle(event);
|
|
}
|
|
} // namespace renderive::web
|