diff --git a/NotoSansCJK-Regular.otf b/NotoSansCJK-Regular.otf deleted file mode 100644 index 9bb532c..0000000 Binary files a/NotoSansCJK-Regular.otf and /dev/null differ diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 1a2a517..e5504d6 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -48,6 +48,7 @@ struct Action_Model { std::string argument_input; std::string argument_label; double argument_default{}; + bool request_frame{}; }; struct Control_Definition { @@ -403,6 +404,13 @@ Action_Definition action(std::string id, std::string label, std::string api, argument_default}}; } +Action_Definition frame_action(std::string id, std::string label, std::string api, + std::string group) { + auto result = action(std::move(id), std::move(label), std::move(api), std::move(group)); + result.model.request_frame = true; + return result; +} + std::vector actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) { std::vector result{ @@ -418,30 +426,30 @@ std::vector actions(std::string_view case_id, "Manual_Refresh_Strategy")); result.push_back(action("mode_refresh", "提交手动刷新", "Manual_Refresh_Strategy::refresh / Plot_Core::refresh_manual_frame", "Manual_Refresh_Strategy")); - result.push_back(action("mode_render", "渲染已刷新帧", "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", - "Manual_Refresh_Strategy")); + result.push_back(frame_action("mode_render", "渲染已刷新帧", "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Manual_Refresh_Strategy")); result.push_back(action("mode_discard", "丢弃待刷新帧", "Plot_Core::discard_pending_frame", "Manual_Refresh_Strategy")); - result.push_back(action("mode_cycle", "执行完整手动帧周期", "Plot_Core::render_frame", - "Manual_Refresh_Strategy")); + result.push_back(frame_action("mode_cycle", "执行完整手动帧周期", "Plot_Core::render_frame", + "Manual_Refresh_Strategy")); } else if (frame_mode == Gallery_Frame_Mode::Low_Latency) { result.push_back(action("mode_prepare", "发布低延迟帧", "Low_Latency_Strategy::acquire_painter / Plot_Core::prepare_frame", "Low_Latency_Strategy")); - result.push_back(action("mode_render", "消费最新帧", "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", - "Low_Latency_Strategy")); + result.push_back(frame_action("mode_render", "消费最新帧", "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Low_Latency_Strategy")); result.push_back(action("mode_discard", "丢弃陈旧帧", "Plot_Core::discard_pending_frame", "Low_Latency_Strategy")); - result.push_back(action("mode_cycle", "执行低延迟帧周期", "Plot_Core::render_frame", - "Low_Latency_Strategy")); + result.push_back(frame_action("mode_cycle", "执行低延迟帧周期", "Plot_Core::render_frame", + "Low_Latency_Strategy")); } else { result.push_back(action("mode_enqueue", "压入一帧", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame", "Flow_Refresh_Strategy")); result.push_back(action("mode_enqueue_burst", "批量压入回放队列", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame × N", "Flow_Refresh_Strategy", {}, "number", "帧数", 8)); - result.push_back(action("mode_dequeue", "消费队首帧", "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", - "Flow_Refresh_Strategy")); - result.push_back(action("mode_cycle", "执行回放帧周期", "Plot_Core::render_frame", - "Flow_Refresh_Strategy")); + result.push_back(frame_action("mode_dequeue", "消费队首帧", "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame", + "Flow_Refresh_Strategy")); + result.push_back(frame_action("mode_cycle", "执行回放帧周期", "Plot_Core::render_frame", + "Flow_Refresh_Strategy")); } if (case_id == "axis_lab") { result.push_back(action("append_time", "追加时间点", "Time_Axis::append_time / tick_to_time", "Time_Axis")); @@ -651,7 +659,8 @@ struct Type_Descriptor { ADMINIVE_FIELD_LABEL(T, group, "分组"), ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"), ADMINIVE_FIELD_LABEL(T, argument_label, "参数名称"), - ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值")) + ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值"), + ADMINIVE_FIELD_LABEL(T, request_frame, "执行后请求像素帧")) .label("后端动作菜单"); } }; @@ -689,7 +698,7 @@ using gallery_detail::Json; Json protocol_base() { return {{"category", "gallery"}, {"protocol", "renderive.control-gallery"}, - {"protocol_version", 3}}; + {"protocol_version", 4}}; } Json case_contract(std::string_view case_id) { @@ -719,23 +728,192 @@ std::optional parse_frame_mode(std::string_view value) { return std::nullopt; } +Json dashboard_field(std::string_view label, std::string_view source, + std::string_view format = "integer", int digits = -1) { + Json result{{"label", label}, {"source", source}, {"format", format}}; + if (digits >= 0) + result["digits"] = digits; + return result; +} + +Json mapped_dashboard_field(std::string_view label, std::string_view source, + std::string_view format, std::string_view value_map) { + Json result = dashboard_field(label, source, format); + result["value_map"] = value_map; + return result; +} + +Json dashboard_contract() { + Json point_pair{{"label", "输入→绘制"}, {"format", "pair"}, + {"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})}, + {"separator", "→"}}; + Json bottleneck = mapped_dashboard_field("当前瓶颈", "low_latency_limit.current", + "duration_enum", "limit_state"); + bottleneck["duration_sources"] = { + {"frequency_limited", "kernel_observer.target_interval_ns"}, + {"paint_limited", "kernel_observer.paint_duration_ns"}, + {"render_limited", "kernel_observer.render_duration_ns"}, + {"consumer_limited", "kernel_observer.consumer_interval_ns"} + }; + Json configured_frequency = dashboard_field("配置频率", "kernel_observer.configured_frequency_hz", + "frequency"); + configured_frequency["enabled_source"] = "kernel_observer.frequency_limit_enabled"; + configured_frequency["disabled_label"] = "已关闭"; + Json dashboard{ + {"value_maps", { + {"enabled", {{"true", "启用"}, {"false", "关闭"}}}, + {"consumer_feedback_source", { + {"disabled", "总开关关闭"}, {"none", "无"}, {"pixel", "像素响应"}, + {"presentation", "浏览器呈现"}, {"manual", "手动"} + }}, + {"limit_state", { + {"frequency_limited", "Kernel 频率受限"}, {"paint_limited", "PaintEvent 受限"}, + {"render_limited", "后台渲染受限"}, {"consumer_limited", "消费者反馈受限"}, + {"unlimited", "无限制"}, {"not_applicable", "N/A"} + }} + }}, + {"performance", { + {"fields", Json::array({ + dashboard_field("后端渲染 FPS", "performance.measured_fps", "fixed", 1), + dashboard_field("像素响应 FPS", "performance.pixel_response_fps", "fixed", 1), + dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fixed", 1), + dashboard_field("WS 往返 ms", "client_performance.frame_round_trip_ms", "fixed", 2), + dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"), + dashboard_field("Core 渲染 ms", "performance.last_render_ms", "fixed", 2), + dashboard_field("像素编码 ms", "performance.last_pixel_encode_ms", "fixed", 2), + dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1), + dashboard_field("Kernel 待处理", "kernel_observer.pending_frame_count"), + dashboard_field("Kernel 丢弃", "kernel_observer.dropped_frame_count"), + std::move(point_pair), std::move(bottleneck), + dashboard_field("观察事件", "kernel_observer.last_event", "text"), + dashboard_field("像素超时", "client_performance.frame_request_timeout_count"), + dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0) + })} + }}, + {"menu_views", { + {"observer", {{"source", "kernel_observer"}}}, + {"performance", Json::object()} + }}, + {"limits", { + {"aria_label", "低延迟限速来源"}, {"title", "限速来源"}, + {"current_source", "low_latency_limit.current"}, + {"active_label", "当前瓶颈"}, {"inactive_label", "未受限"}, + {"disabled_label", "已关闭"}, + {"fields", Json::array({ + {{"label", "Kernel 用户频率"}, {"active_value", "frequency_limited"}, + {"duration_source", "kernel_observer.target_interval_ns"}, + {"enabled_source", "kernel_observer.frequency_limit_enabled"}}, + {{"label", "Kernel PaintEvent"}, {"active_value", "paint_limited"}, + {"duration_source", "kernel_observer.paint_duration_ns"}}, + {{"label", "Kernel 后台渲染"}, {"active_value", "render_limited"}, + {"duration_source", "kernel_observer.render_duration_ns"}}, + {{"label", "消费者反馈"}, {"active_value", "consumer_limited"}, + {"duration_source", "kernel_observer.consumer_interval_ns"}, + {"enabled_source", "kernel_observer.consumer_feedback_enabled"}} + })} + }}, + {"observer", { + {"aria_label", "Kernel 低延迟全量统计"}, + {"header", { + {"prefix", "KERNEL"}, {"suffix", "OBSERVER"}, {"event_label", "事件"}, + {"mode", dashboard_field("", "kernel_observer.mode", "text")}, + {"limit", mapped_dashboard_field("", "kernel_observer.limit_state", "enum", "limit_state")}, + {"event", dashboard_field("", "kernel_observer.last_event", "text")} + }}, + {"sections", Json::array({ + {{"class_name", "observer-counters"}, {"fields", Json::array({ + std::move(configured_frequency), + dashboard_field("观察次数", "kernel_observer.observation_count"), + dashboard_field("最新序号", "kernel_observer.latest_sequence"), + dashboard_field("发布", "kernel_observer.produced_frame_count"), + dashboard_field("完成", "kernel_observer.consumed_frame_count"), + dashboard_field("待处理", "kernel_observer.pending_frame_count"), + dashboard_field("丢弃", "kernel_observer.dropped_frame_count"), + dashboard_field("失败", "kernel_observer.failed_operation_count") + })}}, + {{"class_name", "latency-summary"}, {"fields", Json::array({ + dashboard_field("目标间隔", "kernel_observer.target_interval_ns", "nanoseconds"), + dashboard_field("PaintEvent", "kernel_observer.paint_duration_ns", "nanoseconds"), + dashboard_field("后台渲染", "kernel_observer.render_duration_ns", "nanoseconds"), + dashboard_field("内部瓶颈", "kernel_observer.bottleneck_duration_ns", "nanoseconds"), + mapped_dashboard_field("消费者总开关", "kernel_observer.consumer_feedback_master_enabled", "enum", "enabled"), + mapped_dashboard_field("Kernel 反馈有效", "kernel_observer.consumer_feedback_enabled", "enum", "enabled"), + mapped_dashboard_field("像素响应反馈", "kernel_observer.consumer_pixel_feedback_enabled", "enum", "enabled"), + mapped_dashboard_field("浏览器呈现反馈", "kernel_observer.consumer_presentation_feedback_enabled", "enum", "enabled"), + mapped_dashboard_field("手动消费者反馈", "kernel_observer.consumer_manual_feedback_enabled", "enum", "enabled"), + dashboard_field("手动消费者 FPS", "kernel_observer.consumer_manual_fps", "fps", 2), + mapped_dashboard_field("生效反馈来源", "kernel_observer.consumer_feedback_source", "flags", "consumer_feedback_source"), + dashboard_field("像素响应周期", "kernel_observer.consumer_pixel_interval_ns", "nanoseconds"), + dashboard_field("浏览器呈现周期", "kernel_observer.consumer_presentation_interval_ns", "nanoseconds"), + dashboard_field("手动消费者周期", "kernel_observer.consumer_manual_interval_ns", "nanoseconds"), + dashboard_field("消费者原始采样", "kernel_observer.consumer_sample_interval_ns", "nanoseconds"), + dashboard_field("消费者平滑周期", "kernel_observer.consumer_smoothed_interval_ns", "nanoseconds"), + dashboard_field("消费者抖动", "kernel_observer.consumer_variation_ns", "nanoseconds"), + dashboard_field("消费者安全期限", "kernel_observer.consumer_safety_interval_ns", "nanoseconds"), + dashboard_field("消费者限速周期", "kernel_observer.consumer_interval_ns", "nanoseconds"), + dashboard_field("消费者限速 FPS", "kernel_observer.consumer_interval_ns", "inverse_fps", 2), + dashboard_field("下次刷新", "kernel_observer.next_refresh_interval_ns", "nanoseconds"), + Json{{"label", "端到端延迟"}, {"source", "kernel_observer.end_to_end_ns"}, + {"format", "nanoseconds"}, {"cell_class", "critical"}} + })}}, + {{"class_name", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"}, + {"fields", Json::array({ + dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3), + dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3), + dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3), + dashboard_field("RAF P95-P50 抖动", "client_performance.display_jitter_ms", "milliseconds", 3), + dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3), + dashboard_field("像素响应 FPS", "client_performance.transport_fps", "fps", 2), + dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fps", 2), + dashboard_field("WS 缓冲", "client_performance.websocket_buffered_bytes", "bytes"), + dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"), + dashboard_field("变化像素帧", "client_performance.changed_pixel_frames"), + dashboard_field("重复像素帧", "client_performance.duplicate_pixel_frames"), + dashboard_field("像素请求超时", "client_performance.frame_request_timeout_count"), + dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 3), + dashboard_field("最近变化龄", "client_performance.last_pixel_change_age_ms", "milliseconds", 3) + })}}, + {{"class_name", "latency-details"}, {"aria_label", "Kernel 各阶段等待耗时"}, + {"fields", Json::array({ + dashboard_field("Painter Lease 等待", "kernel_observer.paint_lease_wait_ns", "nanoseconds"), + dashboard_field("Paint State 等待", "kernel_observer.paint_state_wait_ns", "nanoseconds"), + dashboard_field("Publish State 等待", "kernel_observer.publish_state_wait_ns", "nanoseconds"), + dashboard_field("Ready 等待", "kernel_observer.ready_wait_ns", "nanoseconds"), + dashboard_field("开始渲染时帧龄", "kernel_observer.frame_age_at_render_ns", "nanoseconds"), + dashboard_field("Render Lease 等待", "kernel_observer.render_lease_wait_ns", "nanoseconds"), + dashboard_field("Render State 等待", "kernel_observer.render_state_wait_ns", "nanoseconds"), + dashboard_field("Render Finish 等待", "kernel_observer.render_finish_state_wait_ns", "nanoseconds"), + dashboard_field("回放队列等待", "kernel_observer.queue_wait_ns", "nanoseconds") + })}} + })} + }} + }; + return dashboard; +} + Json frame_mode_contract(Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: return {{"id", "manual"}, {"title", "手动刷新"}, {"strategy", "Manual_Refresh_Strategy"}, {"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"}, - {"automatic", false}, {"order", 10}}; + {"automatic", false}, {"request_after_response", false}, + {"request_on_animation_frame", false}, {"observer_visible", false}, + {"frame_button_label", "手动刷新一帧"}, {"accent", "#ffd166aa"}, {"order", 10}}; case Gallery_Frame_Mode::Low_Latency: return {{"id", "low_latency"}, {"title", "低延迟"}, {"strategy", "Low_Latency_Strategy"}, {"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"}, - {"automatic", true}, {"order", 20}}; + {"automatic", true}, {"request_after_response", true}, + {"request_on_animation_frame", false}, {"observer_visible", true}, + {"frame_button_label", "立即刷新"}, {"accent", "#45ddbeaa"}, {"order", 20}}; case Gallery_Frame_Mode::Playback: return {{"id", "playback"}, {"title", "回放队列"}, {"strategy", "Flow_Refresh_Strategy"}, {"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"}, - {"automatic", true}, {"order", 30}}; + {"automatic", true}, {"request_after_response", true}, + {"request_on_animation_frame", true}, {"observer_visible", false}, + {"frame_button_label", "消费下一帧"}, {"accent", "#6aa9ffaa"}, {"order", 30}}; } return Json::object(); } @@ -747,6 +925,13 @@ std::string Gallery_Protocol::catalog_json() { result["type"] = "catalog"; result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp1"}, {"http_api", false}, {"socket_per_canvas", true}}; + result["navigation"] = { + {"default_mode", "low_latency"}, {"all_categories_label", "全部"}, + {"catalog_loaded_text", "Kernel 策略目录已加载"}, + {"hero_eyebrow", "REAL KERNEL STRATEGIES"}, + {"hero_title", "对称策略页面,同一批控件,直接比较"} + }; + result["dashboard"] = dashboard_contract(); result["case_descriptor"] = adminive::to_descriptor_json(); result["frame_modes"] = Json::array(); diff --git a/web_server/app/Gallery_Protocol.h b/web_server/app/Gallery_Protocol.h index d28713d..c9141d6 100644 --- a/web_server/app/Gallery_Protocol.h +++ b/web_server/app/Gallery_Protocol.h @@ -1,88 +1,65 @@ #pragma once - #include #include #include #include #include #include - namespace renderive::web { - enum class Gallery_Frame_Mode : std::uint8_t { Manual, Low_Latency, Playback }; - struct Gallery_Open_Request { std::string case_id; Gallery_Frame_Mode frame_mode = Gallery_Frame_Mode::Low_Latency; }; - using Gallery_Value = std::variant; - struct Gallery_State { std::map> values; }; - struct Gallery_Patch_Result { std::optional candidate; std::string response_json; }; - struct Gallery_Action_Request { std::string id; std::optional argument; }; - class Gallery_Protocol final { public: [[nodiscard]] static std::string catalog_json(); [[nodiscard]] static bool is_case(std::string_view case_id); [[nodiscard]] static Gallery_State default_state(std::string_view case_id); - [[nodiscard]] static Gallery_State default_state( - std::string_view case_id, Gallery_Frame_Mode frame_mode); - [[nodiscard]] static std::string case_json( - std::string_view case_id, - const Gallery_State& state, - std::string_view telemetry_json = "{}", - std::string_view notice = {}); - [[nodiscard]] static std::string case_json( - std::string_view case_id, - const Gallery_State& state, - std::string_view telemetry_json, - std::string_view notice, - Gallery_Frame_Mode frame_mode); - [[nodiscard]] static std::string error_json( - std::string_view message, - std::string_view field = {}); - [[nodiscard]] static std::string observer_json( - std::string_view case_id, - Gallery_Frame_Mode frame_mode, - std::string_view telemetry_json); - [[nodiscard]] static Gallery_Patch_Result apply_patch( - std::string_view case_id, - const Gallery_State& current, - std::string_view message); - [[nodiscard]] static Gallery_Patch_Result apply_patch( - std::string_view case_id, - const Gallery_State& current, - std::string_view message, - Gallery_Frame_Mode frame_mode); - [[nodiscard]] static std::optional open_request( - std::string_view message); - [[nodiscard]] static std::optional action_request( - std::string_view message); + [[nodiscard]] static Gallery_State default_state(std::string_view case_id, Gallery_Frame_Mode frame_mode); + [[nodiscard]] static std::string case_json(std::string_view case_id, + const Gallery_State& state, + std::string_view telemetry_json = "{}", + std::string_view notice = {}); + [[nodiscard]] static std::string case_json(std::string_view case_id, + const Gallery_State& state, + std::string_view telemetry_json, + std::string_view notice, + Gallery_Frame_Mode frame_mode); + [[nodiscard]] static std::string error_json(std::string_view message, + std::string_view field = {}); + [[nodiscard]] static std::string observer_json(std::string_view case_id, + Gallery_Frame_Mode frame_mode, + std::string_view telemetry_json); + [[nodiscard]] static Gallery_Patch_Result apply_patch(std::string_view case_id, + const Gallery_State& current, + std::string_view message); + [[nodiscard]] static Gallery_Patch_Result apply_patch(std::string_view case_id, + const Gallery_State& current, + std::string_view message, + Gallery_Frame_Mode frame_mode); + [[nodiscard]] static std::optional open_request(std::string_view message); + [[nodiscard]] static std::optional action_request(std::string_view message); [[nodiscard]] static std::size_t control_count(std::string_view case_id); - [[nodiscard]] static std::size_t control_count( - std::string_view case_id, Gallery_Frame_Mode frame_mode); + [[nodiscard]] static std::size_t control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode); [[nodiscard]] static std::size_t action_count(std::string_view case_id); - [[nodiscard]] static std::size_t action_count( - std::string_view case_id, Gallery_Frame_Mode frame_mode); - [[nodiscard]] static bool action_available( - std::string_view case_id, Gallery_Frame_Mode frame_mode, - std::string_view action_id); + [[nodiscard]] static std::size_t action_count(std::string_view case_id, Gallery_Frame_Mode frame_mode); + [[nodiscard]] static bool action_available(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view action_id); }; - } // namespace renderive::web diff --git a/web_server/app/Gallery_WebSocket_Controller.h b/web_server/app/Gallery_WebSocket_Controller.h index 7ae209b..edf2658 100644 --- a/web_server/app/Gallery_WebSocket_Controller.h +++ b/web_server/app/Gallery_WebSocket_Controller.h @@ -1,22 +1,14 @@ #pragma once - #include - namespace renderive::web { - class Gallery_WebSocket_Controller final - : public drogon::WebSocketController { +: public drogon::WebSocketController { public: - void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, - std::string&& message, - const drogon::WebSocketMessageType& type) override; - void handleNewConnection(const drogon::HttpRequestPtr& request, - const drogon::WebSocketConnectionPtr& connection) override; + void handleNewMessage(const drogon::WebSocketConnectionPtr& connection, std::string&& message, const drogon::WebSocketMessageType& type) override; + void handleNewConnection(const drogon::HttpRequestPtr& request, const drogon::WebSocketConnectionPtr& connection) override; void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override; - WS_PATH_LIST_BEGIN - WS_PATH_ADD("/renderive/gallery"); + WS_PATH_ADD("/renderive/gallery"); WS_PATH_LIST_END }; - } // namespace renderive::web diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 2455d0c..85847eb 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -191,6 +191,7 @@ TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) { EXPECT_EQ(catalog.at("category"), "gallery"); EXPECT_EQ(catalog.at("type"), "catalog"); EXPECT_EQ(catalog.at("protocol"), "renderive.control-gallery"); + EXPECT_EQ(catalog.at("protocol_version"), 4); EXPECT_EQ(catalog.at("case_descriptor").at("protocol"), "adminive.resource"); EXPECT_EQ(catalog.at("cases").size(), 8U); EXPECT_EQ(catalog.at("frame_modes").size(), 3U); @@ -201,7 +202,48 @@ TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) { EXPECT_EQ(catalog.at("coverage").at("frequency_modes"), nlohmann::json::array({"spectrum", "afterglow", "sweep_spectrum"})); EXPECT_EQ(catalog.at("coverage").at("image_interpolation_modes"), - nlohmann::json::array({"Nearest", "Bilinear", "Bicubic"})); + nlohmann::json::array({"Nearest", "Bilinear", "Bicubic"})); +} + +TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { + const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json()); + const auto& dashboard = catalog.at("dashboard"); + EXPECT_EQ(dashboard.at("performance").at("fields").size(), 15U); + EXPECT_EQ(dashboard.at("limits").at("fields").size(), 4U); + EXPECT_EQ(dashboard.at("observer").at("sections").size(), 4U); + std::set client_sources; + for (const auto& section : dashboard.at("observer").at("sections")) { + for (const auto& field : section.at("fields")) { + const std::string source = field.at("source").get(); + if (source.starts_with("client_performance.")) + client_sources.insert(source); + } + } + constexpr std::array client_fields{ + "display_interval_latest_ms", "display_interval_ms", "display_interval_p95_ms", + "display_jitter_ms", "frame_round_trip_ms", "transport_fps", "presentation_fps", + "websocket_buffered_bytes", "overwritten_pixel_frames", "changed_pixel_frames", + "duplicate_pixel_frames", "frame_request_timeout_count", "last_pixel_receive_age_ms", + "last_pixel_change_age_ms" + }; + EXPECT_EQ(client_sources.size(), client_fields.size()); + for (const auto field : client_fields) + EXPECT_TRUE(client_sources.contains("client_performance." + std::string(field))) << field; + for (const auto& mode : catalog.at("frame_modes")) { + EXPECT_TRUE(mode.contains("frame_button_label")); + EXPECT_TRUE(mode.contains("request_after_response")); + EXPECT_TRUE(mode.contains("request_on_animation_frame")); + EXPECT_TRUE(mode.contains("observer_visible")); + EXPECT_TRUE(mode.contains("accent")); + } + const auto state = Gallery_Protocol::default_state("spectrum", Gallery_Frame_Mode::Manual); + const auto contract = parse_json(Gallery_Protocol::case_json( + "spectrum", state, "{}", {}, Gallery_Frame_Mode::Manual)); + const auto action = std::find_if(contract.at("actions").at("data").begin(), + contract.at("actions").at("data").end(), + [](const auto& item) { return item.at("id") == "mode_render"; }); + ASSERT_NE(action, contract.at("actions").at("data").end()); + EXPECT_TRUE(action->at("request_frame")); } TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) { diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 869e862..56b7aa6 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -6,6 +6,7 @@ const elements = { connection: $("connection-status"), filters: $("category-filter"), modeTabs: $("mode-tabs"), streamToggle: $("toggle-streams"), pageCount: $("page-count"), caseCount: $("case-count"), canvasCount: $("canvas-count"), apiCount: $("api-count"), modeDescription: $("mode-description"), + heroEyebrow: $("hero-eyebrow"), heroTitle: $("hero-title"), menu: $("context-menu"), menuTitle: $("menu-title"), menuComponent: $("menu-component"), menuDescription: $("menu-description"), menuBody: $("menu-body"), menuStatus: $("menu-status"), menuClose: $("menu-close"), menuTabs: [...document.querySelectorAll(".menu-tabs button")], toast: $("toast") @@ -21,8 +22,10 @@ const socketUrl = `${location.protocol === "https:" ? "wss" : "ws"}://${socketHo const pages = new Map(); let definitions = []; let modes = []; -let activeMode = "low_latency"; -let activeCategory = "全部"; +let navigation = {}; +let dashboard = {}; +let activeMode = ""; +let activeCategory = ""; let activeCard = null; let activeTab = "controls"; let streamsPaused = false; @@ -67,6 +70,48 @@ function formatNanoseconds(value) { if (nanoseconds < 1_000_000) return `${(nanoseconds / 1_000).toFixed(2)} µs`; return `${(nanoseconds / 1_000_000).toFixed(3)} ms`; } +function valueAtPath(root, path) { + return path.split(".").reduce((value, key) => value?.[key], root); +} +function formatDashboardField(field, telemetry) { + const format = field.format || "text"; + const raw = valueAtPath(telemetry, field.source || "") ?? field.default ?? (format === "text" ? "" : 0); + const number = Number(raw) || 0; + const digits = field.digits ?? 0; + const valueMap = dashboard.value_maps?.[field.value_map] || {}; + if (format === "fixed") return {text: number.toFixed(digits), title: String(number)}; + if (format === "integer") return {text: number.toLocaleString(), title: String(number)}; + if (format === "milliseconds") return {text: `${number.toFixed(digits)} ms`, title: `${number} ms`}; + if (format === "fps") return {text: `${number.toFixed(digits)} FPS`, title: `${number} FPS`}; + if (format === "bytes") return {text: `${number.toLocaleString()} B`, title: `${number} B`}; + if (format === "nanoseconds") return {text: formatNanoseconds(number), title: `${number.toLocaleString()} ns`}; + if (format === "frequency") { + if (valueAtPath(telemetry, field.enabled_source) === false) + return {text: field.disabled_label, title: field.disabled_label}; + return {text: `${number.toLocaleString()} Hz`, title: `${number} Hz`}; + } + if (format === "inverse_fps") { + const fps = number > 0 ? 1e9 / number : 0; + return {text: `${fps.toFixed(digits)} FPS`, title: `${fps} FPS`}; + } + if (format === "pair") { + const values = field.sources.map(source => Number(valueAtPath(telemetry, source) || 0).toLocaleString()); + return {text: values.join(field.separator || " / "), title: values.join(field.separator || " / ")}; + } + if (format === "enum" || format === "duration_enum") { + const key = String(raw); + const label = valueMap[key] || key; + const durationSource = field.duration_sources?.[key]; + const duration = durationSource === undefined ? undefined : valueAtPath(telemetry, durationSource); + return duration === undefined ? {text: label, title: key} : + {text: `${label} · ${formatNanoseconds(duration)}`, title: `${key} · ${Number(duration || 0).toLocaleString()} ns`}; + } + if (format === "flags") { + const text = String(raw).split(field.separator || "+").map(value => valueMap[value] || value).join(field.joiner || " + "); + return {text, title: String(raw)}; + } + return {text: String(raw), title: String(raw)}; +} function updateDisplayTiming(time) { if (lastAnimationFrameAt > 0) { displayIntervalLatestMs = Math.max(0, time - lastAnimationFrameAt); @@ -126,9 +171,11 @@ class GalleryCard { this.context = this.canvas.getContext("2d", {alpha: false}); this.socketState = this.node.querySelector(".card-socket"); this.motionStatus = this.node.querySelector(".motion-status"); - this.observerFields = new Map([...this.node.querySelectorAll("[data-observer-field]")] - .map(field => [field.dataset.observerField, field])); + this.dashboardBindings = []; + this.limitBindings = []; this.frameLabel = this.node.querySelector(".card-frames"); + this.node.dataset.observerVisible = String(mode.observer_visible); + this.node.style.setProperty("--mode-accent", mode.accent); this.node.querySelector(".card-category").textContent = definition.category; this.node.querySelector(".card-title").textContent = definition.title; this.node.querySelector(".card-description").textContent = definition.description; @@ -136,7 +183,7 @@ class GalleryCard { this.node.querySelector(".card-controls").textContent = definition.control_count_by_mode?.[mode.id] ?? "—"; this.node.querySelector(".card-actions").textContent = definition.action_count_by_mode?.[mode.id] ?? "—"; const frameButton = this.node.querySelector(".frame-button"); - frameButton.textContent = mode.id === "manual" ? "手动刷新一帧" : mode.id === "playback" ? "消费下一帧" : "立即刷新"; + frameButton.textContent = mode.frame_button_label; frameButton.addEventListener("click", () => this.requestFrame(performance.now(), true)); this.node.querySelector(".open-menu").addEventListener("click", event => { const rect = event.currentTarget.getBoundingClientRect(); @@ -156,13 +203,67 @@ class GalleryCard { this.syncActivity(); }, {rootMargin: "160px"}); this.intersectionObserver.observe(this.node); + this.installDashboard(); + } + createDashboardCell(field, valueTag, labelTag, valueFirst) { + const cell = document.createElement("div"); + if (field.cell_class) cell.className = field.cell_class; + cell.dataset.format = field.format; + const value = document.createElement(valueTag); + const label = document.createElement(labelTag); + label.textContent = field.label; + cell.append(...(valueFirst ? [value, label] : [label, value])); + this.dashboardBindings.push({field, node: value}); + return cell; + } + createDashboardValue(field, tagName) { + const node = document.createElement(tagName); + this.dashboardBindings.push({field, node}); + return node; + } + installDashboard() { + const performanceStrip = this.node.querySelector(".performance-strip"); + performanceStrip.replaceChildren(...dashboard.performance.fields.map(field => + this.createDashboardCell(field, "dt", "dd", true))); + const limitFlags = this.node.querySelector(".limit-flags"); + limitFlags.ariaLabel = dashboard.limits.aria_label; + const limitTitle = document.createElement("strong"); + limitTitle.textContent = dashboard.limits.title; + const limitNodes = dashboard.limits.fields.map(field => { + const node = document.createElement("span"); + const value = document.createElement("b"); + node.append(`${field.label}:`, value); + this.limitBindings.push({field, node, value}); + return node; + }); + limitFlags.replaceChildren(limitTitle, ...limitNodes); + const observerPanel = this.node.querySelector(".kernel-observer-panel"); + observerPanel.ariaLabel = dashboard.observer.aria_label; + const headerContract = dashboard.observer.header; + const header = document.createElement("header"); + const identity = document.createElement("div"); + const name = document.createElement("span"); + name.append(`${headerContract.prefix} `, this.createDashboardValue(headerContract.mode, "b"), ` ${headerContract.suffix}`); + identity.append(name, this.createDashboardValue(headerContract.limit, "strong")); + const event = document.createElement("div"); + event.append(`${headerContract.event_label} `, this.createDashboardValue(headerContract.event, "b")); + header.append(identity, event); + const sections = dashboard.observer.sections.map(sectionContract => { + const section = document.createElement("div"); + section.className = sectionContract.class_name; + if (sectionContract.aria_label) section.ariaLabel = sectionContract.aria_label; + section.replaceChildren(...sectionContract.fields.map(field => + this.createDashboardCell(field, "b", "small", false))); + return section; + }); + observerPanel.replaceChildren(header, ...sections); } setSocketState(state, text) { this.socketState.dataset.state = state; this.socketState.querySelector("span").textContent = text; } categoryVisible() { - return activeCategory === "全部" || this.definition.category === activeCategory; + return activeCategory === navigation.all_categories_label || this.definition.category === activeCategory; } displayVisible() { return !document.hidden && this.mode.id === activeMode && this.categoryVisible() && this.intersecting; @@ -227,7 +328,7 @@ class GalleryCard { } if (data.type === "observer_state") { this.telemetry = data.telemetry || {}; - this.updatePerformance(); + this.updateDashboard(); if (activeCard === this && ["observer", "performance"].includes(activeTab)) renderMenuBody(); return; } @@ -242,107 +343,28 @@ class GalleryCard { this.node.querySelector(".card-controls").textContent = this.controls.length; this.node.querySelector(".card-actions").textContent = this.actions.length; this.setSocketState("ready", `${data.frame_mode?.strategy || "Core2"} 在线`); - this.updatePerformance(); + this.updateDashboard(); if (data.notice && !data.notice.includes("已创建")) toast(`${this.definition.title}:${data.notice}`); if (activeCard === this) { elements.menuStatus.textContent = data.notice || "后端状态已回读"; renderMenuBody(); } } - updatePerformance() { - const performanceData = this.telemetry.performance || {}; - const observer = this.telemetry.kernel_observer || {}; - const dataShape = this.telemetry.data_shape || {}; - const limit = this.telemetry.low_latency_limit || {}; - const limitNames = {frequency_limited: "Kernel 频率受限", paint_limited: "PaintEvent 受限", render_limited: "后台渲染受限", consumer_limited: "消费者反馈受限", unlimited: "无限制", not_applicable: "N/A"}; - this.node.querySelector(".perf-fps").textContent = Number(performanceData.measured_fps || 0).toFixed(1); - this.node.querySelector(".perf-transport-fps").textContent = Number(performanceData.pixel_response_fps || 0).toFixed(1); - this.node.querySelector(".perf-present-fps").textContent = Number(this.presentationFps || 0).toFixed(1); - this.node.querySelector(".perf-rtt").textContent = Number(this.frameRoundTripMs || 0).toFixed(2); - this.node.querySelector(".perf-overwritten").textContent = this.overwrittenPixelFrames.toLocaleString(); - this.node.querySelector(".perf-render").textContent = Number(performanceData.last_render_ms || 0).toFixed(2); - this.node.querySelector(".perf-encode").textContent = Number(performanceData.last_pixel_encode_ms || 0).toFixed(2); - this.node.querySelector(".perf-bandwidth").textContent = Number(performanceData.pixel_payload_megabytes_per_second || 0).toFixed(1); - this.node.querySelector(".perf-pending").textContent = observer.pending_frame_count ?? 0; - this.node.querySelector(".perf-dropped").textContent = observer.dropped_frame_count ?? 0; - this.node.querySelector(".perf-points").textContent = `${Number(dataShape.input_points || 0).toLocaleString()}→${Number(dataShape.rendered_elements || 0).toLocaleString()}`; - const limitDurations = {frequency_limited: observer.target_interval_ns, paint_limited: observer.paint_duration_ns, render_limited: observer.render_duration_ns, consumer_limited: observer.consumer_interval_ns}; - const limitName = limitNames[limit.current] || limit.current || "N/A"; - const limitDuration = limitDurations[limit.current]; - const limitValue = limitDuration === undefined ? limitName : `${limitName} · ${formatNanoseconds(limitDuration)}`; - const limitNode = this.node.querySelector(".perf-limit"); - limitNode.textContent = limitValue; - limitNode.title = limitDuration === undefined ? limitName : `${limitName} · ${Number(limitDuration || 0).toLocaleString()} ns`; - this.node.querySelector(".perf-event").textContent = observer.last_event || "none"; - this.node.querySelector(".perf-timeouts").textContent = this.frameTimeoutCount.toLocaleString(); - this.node.querySelector(".perf-pixel-age").textContent = this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—"; - this.updateObserverDashboard(observer); - this.updateClientDashboard(this.telemetry.client_performance || {}); + updateDashboard() { + for (const binding of this.dashboardBindings) { + const formatted = formatDashboardField(binding.field, this.telemetry); + binding.node.textContent = formatted.text; + binding.node.title = formatted.title; + } + const currentLimit = valueAtPath(this.telemetry, dashboard.limits.current_source); + for (const binding of this.limitBindings) { + const enabled = binding.field.enabled_source === undefined || + Boolean(valueAtPath(this.telemetry, binding.field.enabled_source)); + const active = currentLimit === binding.field.active_value; + const status = enabled ? (active ? dashboard.limits.active_label : dashboard.limits.inactive_label) : + dashboard.limits.disabled_label; + const duration = valueAtPath(this.telemetry, binding.field.duration_source) || 0; + binding.node.dataset.active = String(active); + binding.value.textContent = `${status} · ${formatNanoseconds(duration)}`; + } this.updateMotionStatus(); - const setLimitFlag = (selector, enabled, active, duration) => { - const flag = this.node.querySelector(selector); - flag.dataset.active = String(Boolean(active)); - flag.querySelector("b").textContent = enabled ? `${active ? "当前瓶颈" : "未受限"} · ${formatNanoseconds(duration)}` : `已关闭 · ${formatNanoseconds(duration)}`; - }; - setLimitFlag(".limit-frequency", observer.frequency_limit_enabled !== false, limit.current === "frequency_limited", observer.target_interval_ns); - setLimitFlag(".limit-paint", true, limit.current === "paint_limited", observer.paint_duration_ns); - setLimitFlag(".limit-render", true, limit.current === "render_limited", observer.render_duration_ns); - setLimitFlag(".limit-consumer", Boolean(observer.consumer_feedback_enabled), limit.current === "consumer_limited", observer.consumer_interval_ns); - } - updateObserverDashboard(observer) { - const nanosecondFields = new Set([ - "paint_duration_ns", "render_duration_ns", "target_interval_ns", - "bottleneck_duration_ns", "consumer_pixel_interval_ns", "consumer_presentation_interval_ns", - "consumer_manual_interval_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns", - "consumer_variation_ns", "consumer_safety_interval_ns", "consumer_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_ns", - "paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns", - "frame_age_at_render_ns", "render_lease_wait_ns", "render_state_wait_ns", - "render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns" - ]); - const booleanFields = new Set([ - "consumer_feedback_master_enabled", "consumer_feedback_enabled", - "consumer_pixel_feedback_enabled", "consumer_presentation_feedback_enabled", - "consumer_manual_feedback_enabled" - ]); - for (const [name, field] of this.observerFields) { - const raw = observer[name] ?? (name === "limit_state" ? "not_applicable" : - name === "last_event" || name === "consumer_feedback_source" ? "none" : 0); - if (name === "consumer_effective_fps") { - const interval = Number(observer.consumer_interval_ns || 0); - field.textContent = interval > 0 ? `${(1e9 / interval).toFixed(2)} FPS` : "0 FPS"; - field.title = interval > 0 ? `${1e9 / interval} FPS` : "0 FPS"; - continue; - } - if (name === "consumer_manual_fps") { - field.textContent = `${Number(raw || 0).toFixed(2)} FPS`; - field.title = `${Number(raw || 0)} FPS`; - continue; - } - if (name === "consumer_feedback_source") { - const names = {disabled: "总开关关闭", none: "无", pixel: "像素响应", - presentation: "浏览器呈现", manual: "手动"}; - field.textContent = String(raw).split("+").map(value => names[value] || value).join(" + "); - field.title = String(raw); - continue; - } - if (booleanFields.has(name)) { - field.textContent = raw ? "启用" : "关闭"; - field.title = String(Boolean(raw)); - continue; - } - field.textContent = nanosecondFields.has(name) ? formatNanoseconds(raw) : - name === "configured_frequency_hz" ? (observer.frequency_limit_enabled === false ? "已关闭" : `${Number(raw || 0).toLocaleString()} Hz`) : - typeof raw === "number" ? raw.toLocaleString() : String(raw); - field.title = nanosecondFields.has(name) ? `${Number(raw || 0).toLocaleString()} ns` : String(raw); - } - } - updateClientDashboard(client) { - for (const field of this.node.querySelectorAll("[data-client-field]")) { - const name = field.dataset.clientField; - const raw = Number(client[name] || 0); - if (name.endsWith("_fps")) field.textContent = `${raw.toFixed(2)} FPS`; - else if (name.endsWith("_ms")) field.textContent = `${raw.toFixed(3)} ms`; - else if (name === "websocket_buffered_bytes") field.textContent = `${raw.toLocaleString()} B`; - else field.textContent = raw.toLocaleString(); - field.title = String(raw); - } } pixelSignature(buffer, width, height, stride) { const bytes = new Uint8Array(buffer, 16, stride * height); @@ -409,7 +431,7 @@ class GalleryCard { this.frameCount++; this.frameLabel.textContent = this.frameCount.toLocaleString(); this.updateMotionStatus(now); - this.requestFrame(now); + if (this.mode.request_after_response) this.requestFrame(now); } presentLatest(time) { const buffer = this.latestPixelBuffer; @@ -446,7 +468,7 @@ class GalleryCard { } requestFrame(time, explicit = false) { if ((!explicit && !this.streamActive()) || (explicit && !this.displayVisible()) || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return; - if (!explicit && this.mode.id === "manual") return; + if (!explicit && !this.mode.automatic) return; this.frameRequestStartedAt = time; this.framePending = true; this.send("frame"); @@ -542,7 +564,7 @@ function installNavigation() { button.addEventListener("click", () => selectMode(mode.id)); return button; })); - const categories = ["全部", ...new Set(definitions.map(item => item.category))]; + const categories = [navigation.all_categories_label, ...new Set(definitions.map(item => item.category))]; elements.filters.replaceChildren(...categories.map(category => { const button = document.createElement("button"); button.type = "button"; button.textContent = category; @@ -641,7 +663,7 @@ function renderAction(item) { if (argument) payload.argument = item.argument_input === "number" ? Number(argument.value) : argument.value; card.send("gallery_action", payload); elements.menuStatus.textContent = `执行 ${item.api}`; - if (["mode_render", "mode_dequeue", "mode_cycle"].includes(item.id)) setTimeout(() => card.requestFrame(performance.now(), true), 40); + if (item.request_frame) setTimeout(() => card.requestFrame(performance.now(), true), 40); }); controls.append(button); row.append(copy, controls); return row; } @@ -662,16 +684,24 @@ function renderData(value) { function renderMenuBody() { if (!activeCard) return; if (activeTab === "actions") renderGroups(activeCard.actions, renderAction); - else if (activeTab === "observer") renderData(activeCard.telemetry.kernel_observer || {}); - else if (activeTab === "performance") renderData({performance: activeCard.telemetry.performance || {}, client_performance: activeCard.telemetry.client_performance || {}, low_latency_limit: activeCard.telemetry.low_latency_limit || {current: "not_applicable"}, data_shape: activeCard.telemetry.data_shape || {}, overlay: {enabled: activeCard.telemetry.performance_overlay_enabled, lines: activeCard.telemetry.performance_overlay_lines}, frame_mode: activeCard.telemetry.frame_mode}); - else renderGroups(activeCard.controls, renderControl); + else if (activeTab === "controls") renderGroups(activeCard.controls, renderControl); + else { + const view = dashboard.menu_views[activeTab]; + renderData(view.source ? valueAtPath(activeCard.telemetry, view.source) || {} : activeCard.telemetry); + } } function buildCatalog(data) { definitions = [...(data.cases || [])].sort((a, b) => a.order - b.order); modes = [...(data.frame_modes || [])].sort((a, b) => a.order - b.order); + navigation = data.navigation; + dashboard = data.dashboard; + activeMode = navigation.default_mode; + activeCategory = navigation.all_categories_label; if (!modes.length) throw new Error("后端没有返回帧策略目录"); elements.pages.replaceChildren(); + elements.heroEyebrow.textContent = navigation.hero_eyebrow; + elements.heroTitle.textContent = navigation.hero_title; elements.pageCount.textContent = data.coverage?.page_count ?? modes.length; elements.caseCount.textContent = data.coverage?.case_count ?? definitions.length; elements.canvasCount.textContent = data.coverage?.canvas_count ?? definitions.length * modes.length; @@ -679,7 +709,7 @@ function buildCatalog(data) { installNavigation(); if (!modes.some(mode => mode.id === activeMode)) activeMode = modes[0].id; selectMode(activeMode); - setConnection("ready", "三种 Kernel 策略目录已加载"); + setConnection("ready", navigation.catalog_loaded_text); } function connectCatalog() { const socket = new WebSocket(socketUrl); @@ -700,7 +730,7 @@ function loop(time) { for (const card of page?.cards || []) { card.presentLatest(time); card.updateMotionStatus(time); - if (card.mode.id !== "low_latency") card.requestFrame(time); + if (card.mode.request_on_animation_frame) card.requestFrame(time); card.observe(time); } requestAnimationFrame(loop); diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index a45bd59..0a96a4c 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -4,6 +4,7 @@ Renderive Core2 全控件性能画廊 + @@ -20,12 +21,12 @@
-

THREE REAL KERNEL STRATEGIES

-

三套对称页面,同一批控件,直接比较

+

+

所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。

-
3
帧策略页
+
帧策略页
每页控件
独立场景
手测入口
@@ -77,97 +78,9 @@
右键:本控件全部 API
创建 Kernel Scene
-
-
0.0
后端渲染 FPS
-
0.0
像素响应 FPS
-
0.0
浏览器呈现 FPS
-
0.00
WS 往返 ms
-
0
未呈现覆盖
-
0.00
Core 渲染 ms
-
0.00
像素编码 ms
-
0.0
响应负载 MB/s
-
0
Kernel 待处理
-
0
Kernel 丢弃
-
0→0
输入→绘制
-
N/A
当前瓶颈
-
none
观察事件
-
0
像素超时
-
最近像素龄
-
-
- 限速来源 - Kernel 用户频率:未受限 - Kernel PaintEvent:未受限 - Kernel 后台渲染:未受限 - 消费者反馈:未受限 -
-
-
-
KERNEL low_latency OBSERVERfrequency_limited
-
事件 none
-
-
-
配置频率0 Hz
-
观察次数0
-
最新序号0
-
发布0
-
完成0
-
待处理0
-
丢弃0
-
失败0
-
-
-
目标间隔0 ns
-
PaintEvent0 ns
-
后台渲染0 ns
-
内部瓶颈0 ns
-
消费者总开关关闭
-
Kernel 反馈有效关闭
-
像素响应反馈关闭
-
浏览器呈现反馈关闭
-
手动消费者反馈关闭
-
手动消费者 FPS0 FPS
-
生效反馈来源none
-
像素响应周期0 ns
-
浏览器呈现周期0 ns
-
手动消费者周期0 ns
-
消费者原始采样0 ns
-
消费者平滑周期0 ns
-
消费者抖动0 ns
-
消费者安全期限0 ns
-
消费者限速周期0 ns
-
消费者限速 FPS0 FPS
-
下次刷新0 ns
-
端到端延迟0 ns
-
-
-
RAF 最新周期0 ms
-
RAF 中位周期0 ms
-
RAF P95 周期0 ms
-
RAF P95-P50 抖动0 ms
-
WS 往返0 ms
-
像素响应 FPS0 FPS
-
浏览器呈现 FPS0 FPS
-
WS 缓冲0 B
-
未呈现覆盖0
-
变化像素帧0
-
重复像素帧0
-
像素请求超时0
-
最近像素龄0 ms
-
最近变化龄0 ms
-
-
-
Painter Lease 等待0 ns
-
Paint State 等待0 ns
-
Publish State 等待0 ns
-
Ready 等待0 ns
-
开始渲染时帧龄0 ns
-
Render Lease 等待0 ns
-
Render State 等待0 ns
-
Render Finish 等待0 ns
-
回放队列等待0 ns
-
-
+
+
+

属性
动作
0
像素帧
diff --git a/webapp_gallery/styles.css b/webapp_gallery/styles.css index 6a4ddb7..b124991 100644 --- a/webapp_gallery/styles.css +++ b/webapp_gallery/styles.css @@ -39,7 +39,7 @@ button:hover { border-color: var(--accent); } button:active { transform: transla .hero > div > p:last-child { margin: 0; max-width: 750px; color: var(--muted); line-height: 1.7; } .metrics { display: grid; grid-template-columns: repeat(4,1fr); gap: 1px; margin: 0; border: 1px solid var(--line); background: var(--line); } .metrics div { padding: 17px; background: var(--panel); } .metrics dt { color: var(--text); font: 750 clamp(20px,3vw,33px)/1 ui-monospace,monospace; } .metrics dd { margin: 8px 0 0; color: var(--muted); font-size: 10px; } -.mode-tabs { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; max-width: 1680px; margin: auto; padding: 0 clamp(18px,4vw,56px) 18px; } +.mode-tabs { display: grid; grid-template-columns: repeat(auto-fit,minmax(220px,1fr)); gap: 10px; max-width: 1680px; margin: auto; padding: 0 clamp(18px,4vw,56px) 18px; } .mode-tabs button { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; color: var(--muted); text-align: left; } .mode-tabs button span { color: var(--text); font-weight: 750; } .mode-tabs button code { font-size: 10px; } .mode-tabs button.active { border-color: var(--accent); background: #123029; box-shadow: inset 0 0 0 1px #45ddbe35; } @@ -53,12 +53,12 @@ button:hover { border-color: var(--accent); } button:active { transform: transla .gallery { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 18px; padding: 0 clamp(18px,4vw,56px) 70px; } .loading-card { display: grid; place-items: center; gap: 12px; min-height: 360px; margin: 0 clamp(18px,4vw,56px); border: 1px dashed var(--strong); color: var(--muted); } .loader { width: 18px; height: 18px; border: 2px solid var(--strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } -.plot-card { min-width: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 13px; background: linear-gradient(180deg,#ffffff08,transparent 30%),var(--panel); box-shadow: 0 20px 55px #0000002e; } -.plot-card[data-mode="manual"] { border-top-color: #ffd166aa; }.plot-card[data-mode="low_latency"] { border-top-color: #45ddbeaa; }.plot-card[data-mode="playback"] { border-top-color: #6aa9ffaa; }.plot-card[hidden] { display:none; } +.plot-card { min-width: 0; overflow: hidden; border: 1px solid var(--line); border-top-color: var(--mode-accent); border-radius: 13px; background: linear-gradient(180deg,#ffffff08,transparent 30%),var(--panel); box-shadow: 0 20px 55px #0000002e; } +.plot-card[hidden] { display:none; } .card-header, .card-footer { display:flex; align-items:center; justify-content:space-between; gap:15px; padding:13px 15px; }.card-header { border-bottom:1px solid var(--line); }.card-category { display:block; margin-bottom:4px; color:var(--blue); font:700 9px/1 ui-monospace,monospace; letter-spacing:.1em; }.card-title { margin:0; font-size:17px; } .canvas-shell { position:relative; height:clamp(245px,27vw,350px); outline:none; background:#060a11; cursor:crosshair; }.canvas-shell:focus-visible { box-shadow:inset 0 0 0 2px var(--accent); }.canvas-shell canvas { display:block; width:100%; height:100%; }.canvas-hint { position:absolute; right:9px; bottom:8px; padding:5px 7px; border:1px solid #ffffff1e; border-radius:5px; color:#ffffffa8; background:#04070bc9; font:9px/1 ui-monospace,monospace; pointer-events:none; }.canvas-loading { position:absolute; inset:0; display:grid; place-content:center; justify-items:center; gap:9px; color:var(--muted); background:#080d15; }.plot-card[data-ready="true"] .canvas-loading { display:none; } -.performance-strip { display:grid; grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); gap:1px; margin:0; border-block:1px solid var(--line); background:var(--line); }.performance-strip div { min-width:0; padding:9px 8px; background:#0c1118; }.performance-strip dt { min-width:0; overflow:visible; color:var(--accent); font:700 11px/1.35 ui-monospace,monospace; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }.performance-strip .perf-event,.performance-strip .perf-limit { font-size:10px; }.performance-strip .perf-limit-cell { grid-column:auto; }.performance-strip dd { margin:5px 0 0; color:var(--muted); font-size:8px; line-height:1.35; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } -.limit-flags { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; padding:7px 10px; border-bottom:1px solid var(--line); color:var(--muted); background:#090e15; font:9px/1.35 ui-monospace,monospace; }.limit-flags strong { grid-column:1/-1; color:var(--text); }.limit-flags span { min-width:0; padding:6px 7px; border:1px solid var(--line); border-radius:4px; line-height:1.45; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }.limit-flags span[data-active="true"] { border-color:#ffb84d88; color:#ffcf82; background:#ff9d1712; }.limit-flags b { color:inherit; white-space:normal; overflow-wrap:anywhere; }.plot-card:not([data-mode="low_latency"]) .limit-flags { display:none; } +.performance-strip { display:grid; grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); gap:1px; margin:0; border-block:1px solid var(--line); background:var(--line); }.performance-strip div { min-width:0; padding:9px 8px; background:#0c1118; }.performance-strip dt { min-width:0; overflow:visible; color:var(--accent); font:700 11px/1.35 ui-monospace,monospace; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }.performance-strip [data-format="text"] dt,.performance-strip [data-format="duration_enum"] dt { font-size:10px; }.performance-strip dd { margin:5px 0 0; color:var(--muted); font-size:8px; line-height:1.35; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } +.limit-flags { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; padding:7px 10px; border-bottom:1px solid var(--line); color:var(--muted); background:#090e15; font:9px/1.35 ui-monospace,monospace; }.limit-flags strong { grid-column:1/-1; color:var(--text); }.limit-flags span { min-width:0; padding:6px 7px; border:1px solid var(--line); border-radius:4px; line-height:1.45; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }.limit-flags span[data-active="true"] { border-color:#ffb84d88; color:#ffcf82; background:#ff9d1712; }.limit-flags b { color:inherit; white-space:normal; overflow-wrap:anywhere; }.plot-card[data-observer-visible="false"] .limit-flags { display:none; } .kernel-observer-panel { border-bottom:1px solid var(--line); background:#080d14; font-family:ui-monospace,monospace; } .kernel-observer-panel > header { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 11px; border-bottom:1px solid var(--line); color:var(--muted); font-size:8px; flex-wrap:wrap; } .kernel-observer-panel > header div:first-child { display:flex; align-items:center; gap:9px; min-width:0; flex-wrap:wrap; }.kernel-observer-panel > header span { color:var(--accent); letter-spacing:.08em; overflow-wrap:anywhere; }.kernel-observer-panel > header strong,.kernel-observer-panel > header b { color:var(--text); font-weight:700; overflow-wrap:anywhere; } @@ -68,7 +68,7 @@ button:hover { border-color: var(--accent); } button:active { transform: transla .kernel-observer-panel small { display:block; min-width:0; overflow:visible; margin-bottom:5px; color:var(--muted); font-size:7px; line-height:1.35; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } .kernel-observer-panel b { display:block; min-width:0; overflow:visible; color:#b7c7dc; font-size:9px; line-height:1.35; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } .latency-summary b { color:var(--accent); font-size:10px; }.latency-summary .critical { background:#10201e; }.latency-summary .critical b { color:#72f3d9; } -.plot-card:not([data-mode="low_latency"]) .kernel-observer-panel { display:none; } +.plot-card[data-observer-visible="false"] .kernel-observer-panel { display:none; } .card-meta { display:grid; grid-template-columns:1fr auto; gap:17px; padding:13px 15px; }.card-description { margin:0; color:var(--muted); font-size:11px; line-height:1.55; }.card-meta dl { display:flex; margin:0; }.card-meta dl div { min-width:52px; padding-left:11px; border-left:1px solid var(--line); }.card-meta dt { font:700 14px/1 ui-monospace,monospace; }.card-meta dd { margin:5px 0 0; color:var(--muted); font-size:9px; } .card-footer { border-top:1px solid var(--line); background:#0000001e; }.card-footer code { color:var(--accent); font-size:10px; }.card-footer > div { display:flex; gap:7px; }.open-menu,.frame-button { padding:7px 9px; color:var(--muted); font-size:10px; }.frame-button { color:var(--text); } .context-menu { position:fixed; z-index:100; width:min(570px,calc(100vw - 24px)); max-height:min(840px,calc(100vh - 24px)); overflow:hidden; border:1px solid var(--strong); border-radius:12px; background:#0f131afa; box-shadow:0 30px 90px #0000009e; backdrop-filter:blur(22px); }.context-menu[hidden] { display:none; }