diff --git a/CMakeLists.txt b/CMakeLists.txt index df54a8e..5daba4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,12 @@ endif () if (RENDERIVE_BUILD_3D) message(FATAL_ERROR "RENDERIVE_BUILD_3D is reserved; the 3D renderer is not implemented") endif () -include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/start.cmake") +if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(third_party "${CMAKE_CURRENT_LIST_DIR}/third_party") +else () + set(third_party "${CMAKE_CURRENT_LIST_DIR}/..") +endif () +include("${third_party}/build_infra/start.cmake") if (RENDERIVE_BUILD_TESTS) enable_testing() endif () @@ -31,4 +36,4 @@ if (RENDERIVE_BUILD_WEB) add_subdirectory(web_server) endif () include("${CMAKE_CURRENT_LIST_DIR}/cmake/RenderivePackage.cmake") -include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/end.cmake") +include("${third_party}/build_infra/end.cmake") diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index ed08db4..7b258a7 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -1,13 +1,25 @@ +set(Renderive_Kernel_dependencies global::taskflow) +if(RENDERIVE_BUILD_TESTS) + list(APPEND Renderive_Kernel_dependencies global::GTest) +endif() +rcl_add_dependency_action_targets(Renderive_Kernel_env ${Renderive_Kernel_dependencies}) +set_target_properties(Renderive_Kernel_env PROPERTIES FOLDER Renderive_Kernel) +library_is_installed_with_rely(Renderive_Kernel_dependencies_installed ${Renderive_Kernel_dependencies}) +if(NOT Renderive_Kernel_dependencies_installed) + rcl_log_append("[FATAL_ERROR] Renderive_Kernel dependencies are not installed. Build Renderive_Kernel_env first") + return() +endif() +rcl_load_dependency_environment(${Renderive_Kernel_dependencies}) find_package(Taskflow CONFIG REQUIRED) find_package(Threads REQUIRED) -add_library(Renderive_Kernel STATIC) -target_sources(Renderive_Kernel PRIVATE - src/renderive/renderable/Renderable_Task_Graph.cpp - src/renderive/renderable/base/Renderable_Base.cpp - src/renderive/scene/base/Scene_Base.cpp -) +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) +endif() +set(Renderive_Kernel_source_dir "${CMAKE_CURRENT_LIST_DIR}/src") +append_glob_source(Renderive_Kernel_sources "${Renderive_Kernel_source_dir}") +add_library(Renderive_Kernel STATIC ${Renderive_Kernel_sources}) target_include_directories(Renderive_Kernel PUBLIC - "$" + "$" ) target_compile_features(Renderive_Kernel PUBLIC cxx_std_20) target_link_libraries(Renderive_Kernel PRIVATE Taskflow::Taskflow Threads::Threads) @@ -15,41 +27,20 @@ if(MSVC) target_compile_options(Renderive_Kernel PRIVATE /utf-8) endif() if(RENDERIVE_BUILD_TESTS) - find_package(GTest CONFIG REQUIRED) - add_executable(Renderive_Kernel_Tests) - target_sources(Renderive_Kernel_Tests PRIVATE - tests/renderive/base/Concepts_Test.cpp - tests/renderive/base/node/Multiway_Node_Test.cpp - tests/renderive/base/observer/Observer_Test.cpp - tests/renderive/base/observer/time_source/Steady_Time_Source_Test.cpp - tests/renderive/base/property/Concepts_Test.cpp - tests/renderive/base/property/Property_Builder_Test.cpp - tests/renderive/base/property/Validated_Value_Test.cpp - tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp - tests/renderive/frame_control/concept/Concepts_Test.cpp - tests/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy_Test.cpp - tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp - tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp - tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp - tests/renderive/real_time_data/base/Real_Time_Data_Base_Test.cpp - tests/renderive/real_time_data/Real_Time_Data_Test.cpp - tests/renderive/renderable/base/Renderable_Base_Test.cpp - tests/renderive/renderable/color/Color_Cache_Test.cpp - tests/renderive/renderable/concept/Renderable_Test.cpp - tests/renderive/renderable/Renderable_Task_Graph_Test.cpp - tests/renderive/scene/base/Scene_Base_Test.cpp - tests/renderive/scene/concept/Scene_Test.cpp - tests/renderive/scene/Scene_Memory_Resource_Test.cpp - tests/renderive/scene/Scene_State_Observer_Test.cpp - tests/renderive/scene/Scene2D_Context_Test.cpp - tests/renderive/scene/Scene2D_Render_Order_Test.cpp - tests/renderive/scene/Scene3D_Context_Test.cpp - tests/renderive/state/base/State_Strategy_Base_Test.cpp - tests/renderive/state/Concepts_Test.cpp - tests/renderive/state/Double_State_Strategy_Test.cpp - tests/renderive/state/Triple_State_Strategy_Test.cpp - tests/renderive/threading/Threading_Contract_Test.cpp - ) - target_link_libraries(Renderive_Kernel_Tests PRIVATE Renderive_Kernel GTest::gtest_main) - add_test(NAME Renderive_Kernel_Tests COMMAND Renderive_Kernel_Tests) + set(Renderive_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") + append_glob_source(Renderive_Kernel_test_sources "${Renderive_Kernel_test_dir}") + foreach(Renderive_Kernel_test_source IN LISTS Renderive_Kernel_test_sources) + if(NOT Renderive_Kernel_test_source MATCHES "\\.(c|cc|cpp|cxx)$") + continue() + endif() + file(RELATIVE_PATH Renderive_Kernel_test_name "${Renderive_Kernel_test_dir}" "${Renderive_Kernel_test_source}") + string(MD5 Renderive_Kernel_test_hash "${Renderive_Kernel_test_name}") + string(SUBSTRING "${Renderive_Kernel_test_hash}" 0 8 Renderive_Kernel_test_hash) + get_filename_component(Renderive_Kernel_test_name "${Renderive_Kernel_test_source}" NAME_WE) + string(MAKE_C_IDENTIFIER "${Renderive_Kernel_test_name}" Renderive_Kernel_test_name) + set(Renderive_Kernel_test_target "Renderive_Kernel_${Renderive_Kernel_test_name}_${Renderive_Kernel_test_hash}") + add_executable("${Renderive_Kernel_test_target}" "${Renderive_Kernel_test_source}") + target_link_libraries("${Renderive_Kernel_test_target}" PRIVATE Renderive_Kernel GTest::gtest_main) + add_test(NAME "${Renderive_Kernel_test_target}" COMMAND "${Renderive_Kernel_test_target}") + endforeach() endif() diff --git a/Qt/CMakeLists.txt b/Qt/CMakeLists.txt index bc68a0a..2b6275a 100644 --- a/Qt/CMakeLists.txt +++ b/Qt/CMakeLists.txt @@ -1,13 +1,29 @@ +set(Renderive_Qt_dependencies) +if(RENDERIVE_BUILD_TESTS) + list(APPEND Renderive_Qt_dependencies global::GTest) +endif() +rcl_add_dependency_action_targets(Renderive_Qt_env ${Renderive_Qt_dependencies}) +set_target_properties(Renderive_Qt_env PROPERTIES FOLDER Renderive_Qt) +library_is_installed_with_rely(Renderive_Qt_dependencies_installed ${Renderive_Qt_dependencies}) +if(NOT Renderive_Qt_dependencies_installed) + rcl_log_append("[FATAL_ERROR] Renderive_Qt dependencies are not installed. Build Renderive_Qt_env first") + return() +endif() +if(Renderive_Qt_dependencies) + rcl_load_dependency_environment(${Renderive_Qt_dependencies}) +endif() find_package(Qt5 CONFIG REQUIRED COMPONENTS Core Gui Widgets) -add_library(Renderive_Qt STATIC) -target_sources(Renderive_Qt PRIVATE - bridge/Qt_Event_Adapter.cpp - bridge/Qt_Image_Adapter.cpp - bridge/Qt_Presentation_Sink.cpp - plot/Explicit_Plot.cpp - plot/Latency_Eager_Plot.cpp - plot/Plot.cpp -) +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) +endif() +if(NOT TARGET Renderive_render_2D) + rcl_log_append("[FATAL_ERROR] Renderive_Qt requires Renderive_render_2D") + return() +endif() +set(Renderive_Qt_source_dir "${CMAKE_CURRENT_LIST_DIR}") +append_glob_source(Renderive_Qt_sources "${Renderive_Qt_source_dir}") +list(FILTER Renderive_Qt_sources EXCLUDE REGEX "/tests/") +add_library(Renderive_Qt STATIC ${Renderive_Qt_sources}) set_target_properties(Renderive_Qt PROPERTIES AUTOMOC ON) target_include_directories(Renderive_Qt PUBLIC "$" @@ -23,15 +39,27 @@ if(MSVC) target_compile_options(Renderive_Qt PRIVATE /utf-8) endif() if(RENDERIVE_BUILD_TESTS) - find_package(GTest CONFIG REQUIRED) - add_executable(Renderive_Qt_Tests tests/Qt_Bridge_Tests.cpp) - target_link_libraries(Renderive_Qt_Tests PRIVATE Renderive_Qt GTest::gtest) - get_filename_component(renderive_qt_runtime_root "${Qt5_DIR}/../../.." ABSOLUTE) - add_test(NAME Renderive_Qt_Tests - COMMAND "${CMAKE_COMMAND}" -E env - "PATH=${renderive_qt_runtime_root}/bin\;$ENV{PATH}" - "QT_QPA_PLATFORM=offscreen" - "QT_QPA_PLATFORM_PLUGIN_PATH=${renderive_qt_runtime_root}/plugins/platforms" - "$" - ) + get_filename_component(Renderive_Qt_runtime_root "${Qt5_DIR}/../../.." ABSOLUTE) + set(Renderive_Qt_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") + append_glob_source(Renderive_Qt_test_sources "${Renderive_Qt_test_dir}") + foreach(Renderive_Qt_test_source IN LISTS Renderive_Qt_test_sources) + if(NOT Renderive_Qt_test_source MATCHES "\\.(c|cc|cpp|cxx)$") + continue() + endif() + file(RELATIVE_PATH Renderive_Qt_test_name "${Renderive_Qt_test_dir}" "${Renderive_Qt_test_source}") + string(MD5 Renderive_Qt_test_hash "${Renderive_Qt_test_name}") + string(SUBSTRING "${Renderive_Qt_test_hash}" 0 8 Renderive_Qt_test_hash) + get_filename_component(Renderive_Qt_test_name "${Renderive_Qt_test_source}" NAME_WE) + string(MAKE_C_IDENTIFIER "${Renderive_Qt_test_name}" Renderive_Qt_test_name) + set(Renderive_Qt_test_target "Renderive_Qt_${Renderive_Qt_test_name}_${Renderive_Qt_test_hash}") + add_executable("${Renderive_Qt_test_target}" "${Renderive_Qt_test_source}") + target_link_libraries("${Renderive_Qt_test_target}" PRIVATE Renderive_Qt GTest::gtest) + add_test(NAME "${Renderive_Qt_test_target}" + COMMAND "${CMAKE_COMMAND}" -E env + "PATH=${Renderive_Qt_runtime_root}/bin\;$ENV{PATH}" + "QT_QPA_PLATFORM=offscreen" + "QT_QPA_PLATFORM_PLUGIN_PATH=${Renderive_Qt_runtime_root}/plugins/platforms" + "$" + ) + endforeach() endif() diff --git a/render_2D/CMakeLists.txt b/render_2D/CMakeLists.txt index 46e1855..21857e5 100644 --- a/render_2D/CMakeLists.txt +++ b/render_2D/CMakeLists.txt @@ -1,16 +1,27 @@ +set(Renderive_render_2D_dependencies global::blend2d) +if(RENDERIVE_BUILD_TESTS) + list(APPEND Renderive_render_2D_dependencies global::GTest) +endif() +rcl_add_dependency_action_targets(Renderive_render_2D_env ${Renderive_render_2D_dependencies}) +set_target_properties(Renderive_render_2D_env PROPERTIES FOLDER Renderive_render_2D) +library_is_installed_with_rely(Renderive_render_2D_dependencies_installed ${Renderive_render_2D_dependencies}) +if(NOT Renderive_render_2D_dependencies_installed) + rcl_log_append("[FATAL_ERROR] Renderive_render_2D dependencies are not installed. Build Renderive_render_2D_env first") + return() +endif() +rcl_load_dependency_environment(${Renderive_render_2D_dependencies}) find_package(blend2d CONFIG REQUIRED) -add_library(Renderive_render_2D STATIC) -target_sources(Renderive_render_2D PRIVATE - axis/Axis.cpp - base/Types.cpp - plot/Plot_Core.cpp - plottable/Heatmaps.cpp - plottable/Overlays.cpp - plottable/Performance_Overlay.cpp - plottable/Spectrum.cpp - render/Blend2D_Cache.cpp - renderable/Renderable.cpp -) +if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) +endif() +if(NOT TARGET Renderive_Kernel) + rcl_log_append("[FATAL_ERROR] Renderive_render_2D requires Renderive_Kernel") + return() +endif() +set(Renderive_render_2D_source_dir "${CMAKE_CURRENT_LIST_DIR}") +append_glob_source(Renderive_render_2D_sources "${Renderive_render_2D_source_dir}") +list(FILTER Renderive_render_2D_sources EXCLUDE REGEX "/tests/") +add_library(Renderive_render_2D STATIC ${Renderive_render_2D_sources}) target_include_directories(Renderive_render_2D PUBLIC "$" ) @@ -20,12 +31,20 @@ if(MSVC) target_compile_options(Renderive_render_2D PRIVATE /utf-8) endif() if(RENDERIVE_BUILD_TESTS) - find_package(GTest CONFIG REQUIRED) - add_executable(Renderive_render_2D_Tests) - target_sources(Renderive_render_2D_Tests PRIVATE - tests/render_2D_Frame_Pipeline_Tests.cpp - tests/render_2D_Integration_Tests.cpp - ) - target_link_libraries(Renderive_render_2D_Tests PRIVATE Renderive_render_2D GTest::gtest_main) - add_test(NAME Renderive_render_2D_Tests COMMAND Renderive_render_2D_Tests) + set(Renderive_render_2D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") + append_glob_source(Renderive_render_2D_test_sources "${Renderive_render_2D_test_dir}") + foreach(Renderive_render_2D_test_source IN LISTS Renderive_render_2D_test_sources) + if(NOT Renderive_render_2D_test_source MATCHES "\\.(c|cc|cpp|cxx)$") + continue() + endif() + file(RELATIVE_PATH Renderive_render_2D_test_name "${Renderive_render_2D_test_dir}" "${Renderive_render_2D_test_source}") + string(MD5 Renderive_render_2D_test_hash "${Renderive_render_2D_test_name}") + string(SUBSTRING "${Renderive_render_2D_test_hash}" 0 8 Renderive_render_2D_test_hash) + get_filename_component(Renderive_render_2D_test_name "${Renderive_render_2D_test_source}" NAME_WE) + string(MAKE_C_IDENTIFIER "${Renderive_render_2D_test_name}" Renderive_render_2D_test_name) + set(Renderive_render_2D_test_target "Renderive_render_2D_${Renderive_render_2D_test_name}_${Renderive_render_2D_test_hash}") + add_executable("${Renderive_render_2D_test_target}" "${Renderive_render_2D_test_source}") + target_link_libraries("${Renderive_render_2D_test_target}" PRIVATE Renderive_render_2D GTest::gtest_main) + add_test(NAME "${Renderive_render_2D_test_target}" COMMAND "${Renderive_render_2D_test_target}") + endforeach() endif() diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index fffcb76..2f9c3dd 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -1,35 +1,45 @@ +set(Renderive_Web_dependencies global::drogon global::spdlog) +if(RENDERIVE_BUILD_TESTS) + list(APPEND Renderive_Web_dependencies global::GTest) +endif() +rcl_add_dependency_action_targets(Renderive_Web_env ${Renderive_Web_dependencies}) +set_target_properties(Renderive_Web_env PROPERTIES FOLDER Renderive_Web) +library_is_installed_with_rely(Renderive_Web_dependencies_installed ${Renderive_Web_dependencies}) +if(NOT Renderive_Web_dependencies_installed) + rcl_log_append("[FATAL_ERROR] Renderive_Web dependencies are not installed. Build Renderive_Web_env first") + return() +endif() function(renderive_find_web_dependencies) if(POLICY CMP0144) cmake_policy(SET CMP0144 NEW) endif() - rcl_load_dependency_environment(global::drogon) + rcl_load_dependency_environment(${Renderive_Web_dependencies}) find_package(Drogon CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) + if(RENDERIVE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + endif() endfunction() renderive_find_web_dependencies() +if(NOT TARGET Renderive_render_2D) + rcl_log_append("[FATAL_ERROR] Renderive_Web requires Renderive_render_2D") + return() +endif() if(NOT TARGET Adminive::Nlohmann) - set(renderive_saved_build_testing "${BUILD_TESTING}") + set(Renderive_Web_saved_build_testing "${BUILD_TESTING}") set(BUILD_TESTING OFF) add_subdirectory( "${CMAKE_CURRENT_LIST_DIR}/../third_party/Adminive" "${CMAKE_CURRENT_BINARY_DIR}/third_party/Adminive" EXCLUDE_FROM_ALL ) - set(BUILD_TESTING "${renderive_saved_build_testing}") - unset(renderive_saved_build_testing) + set(BUILD_TESTING "${Renderive_Web_saved_build_testing}") + unset(Renderive_Web_saved_build_testing) endif() -add_library(Renderive_Web STATIC) -target_sources(Renderive_Web PRIVATE - Gallery_Plot_Session.cpp - Gallery_Protocol.cpp - Gallery_WebSocket_Controller.cpp - Pixel_Frame.cpp - Renderive_WebSocket_Controller.cpp - Web_Event_Adapter.cpp - Web_Performance_Log.cpp - Web_Plot_Session.cpp - Web_Server.cpp -) +set(Renderive_Web_source_dir "${CMAKE_CURRENT_LIST_DIR}") +append_glob_source(Renderive_Web_sources "${Renderive_Web_source_dir}") +list(FILTER Renderive_Web_sources EXCLUDE REGEX "/(app|tests)/") +add_library(Renderive_Web STATIC ${Renderive_Web_sources}) target_include_directories(Renderive_Web PUBLIC "$" ) @@ -39,7 +49,9 @@ target_link_libraries(Renderive_Web PUBLIC Renderive_render_2D Drogon::Drogon PRIVATE Adminive::Nlohmann spdlog::spdlog ) -add_executable(Renderive_Web_Server main.cpp) +set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/app") +append_glob_source(Renderive_Web_Server_sources "${Renderive_Web_Server_source_dir}") +add_executable(Renderive_Web_Server ${Renderive_Web_Server_sources}) target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20) target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web) add_custom_target(Renderive_Web_Assets @@ -54,8 +66,20 @@ if(MSVC) target_compile_options(Renderive_Web_Server PRIVATE /utf-8) endif() if(RENDERIVE_BUILD_TESTS) - find_package(GTest CONFIG REQUIRED) - add_executable(Renderive_Web_Tests tests/Web_Bridge_Tests.cpp) - target_link_libraries(Renderive_Web_Tests PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main) - add_test(NAME Renderive_Web_Tests COMMAND Renderive_Web_Tests) + set(Renderive_Web_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") + append_glob_source(Renderive_Web_test_sources "${Renderive_Web_test_dir}") + foreach(Renderive_Web_test_source IN LISTS Renderive_Web_test_sources) + if(NOT Renderive_Web_test_source MATCHES "\\.(c|cc|cpp|cxx)$") + continue() + endif() + file(RELATIVE_PATH Renderive_Web_test_name "${Renderive_Web_test_dir}" "${Renderive_Web_test_source}") + string(MD5 Renderive_Web_test_hash "${Renderive_Web_test_name}") + string(SUBSTRING "${Renderive_Web_test_hash}" 0 8 Renderive_Web_test_hash) + get_filename_component(Renderive_Web_test_name "${Renderive_Web_test_source}" NAME_WE) + string(MAKE_C_IDENTIFIER "${Renderive_Web_test_name}" Renderive_Web_test_name) + set(Renderive_Web_test_target "Renderive_Web_${Renderive_Web_test_name}_${Renderive_Web_test_hash}") + add_executable("${Renderive_Web_test_target}" "${Renderive_Web_test_source}") + target_link_libraries("${Renderive_Web_test_target}" PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main) + add_test(NAME "${Renderive_Web_test_target}" COMMAND "${Renderive_Web_test_target}") + endforeach() endif() diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index 1b192b9..f7747f0 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -1756,7 +1756,9 @@ struct Gallery_Plot_Session::Impl { return false; } else if constexpr (std::is_same_v) { - return value.kind != Gallery_Request_Kind::Catalog; + return value.kind != Gallery_Request_Kind::Catalog && + value.kind != Gallery_Request_Kind::Refresh && + value.kind != Gallery_Request_Kind::Feedback; } else { return true; @@ -1803,8 +1805,14 @@ struct Gallery_Plot_Session::Impl { Web_Response_Type::Json, Gallery_Protocol::error_json("请先发送 gallery_open") }; + if (request.kind == Gallery_Request_Kind::Feedback) { + if (update_client_metrics(request.message)) { + ++scheduler_revision; + scheduler_condition.notify_all(); + } + return std::nullopt; + } if (request.kind == Gallery_Request_Kind::Refresh) { - update_client_metrics(request.message); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json(scene->case_id(), scene->state(), diff --git a/web_server/Gallery_Protocol.cpp b/web_server/Gallery_Protocol.cpp index e2bc37c..7e6a79f 100644 --- a/web_server/Gallery_Protocol.cpp +++ b/web_server/Gallery_Protocol.cpp @@ -537,6 +537,8 @@ Json control_amis_schema(const Control_Definition& definition) { } if (model.input == "select") { result["type"] = "select"; + result["required"] = true; + result["clearable"] = false; result["options"] = Json::array(); for (const auto& option : definition.options) result["options"].push_back({{"label", option}, {"value", option}}); @@ -544,12 +546,15 @@ Json control_amis_schema(const Control_Definition& definition) { } if (model.input == "color") { result["type"] = "input-color"; + result["required"] = true; + result["clearable"] = false; result["validations"] = {{"matchRegexp", "^#[0-9A-Fa-f]{6}$"}, {"maxLength", 7}}; result["validationErrors"] = {{"matchRegexp", "颜色必须使用 #RRGGBB"}, {"maxLength", "颜色必须使用 #RRGGBB"}}; return result; } if (model.input == "number") { result["type"] = "input-number"; + result["required"] = true; result["min"] = model.minimum; result["max"] = model.maximum; result["step"] = model.step; @@ -578,7 +583,8 @@ Json action_argument_amis_schema(const Action_Definition& definition) { return { {"type", model.argument_input == "number" ? "input-number" : "input-text"}, {"name", "action_" + model.id}, - {"label", model.argument_label.empty() ? "参数" : model.argument_label} + {"label", model.argument_label.empty() ? "参数" : model.argument_label}, + {"required", true} }; } @@ -749,7 +755,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) { diff --git a/web_server/Web_Event.h b/web_server/Web_Event.h index 95a9113..01c6f4d 100644 --- a/web_server/Web_Event.h +++ b/web_server/Web_Event.h @@ -27,7 +27,7 @@ struct Set_Smoothing { bool enabled{}; }; struct Clear_Selection {}; -enum class Gallery_Request_Kind : std::uint8_t { Catalog, Open, Refresh, Patch, Action }; +enum class Gallery_Request_Kind : std::uint8_t { Catalog, Open, Refresh, Feedback, Patch, Action }; struct Gallery_Request { Gallery_Request_Kind kind = Gallery_Request_Kind::Catalog; std::string message; diff --git a/web_server/Web_Event_Adapter.cpp b/web_server/Web_Event_Adapter.cpp index 351ad58..98cee98 100644 --- a/web_server/Web_Event_Adapter.cpp +++ b/web_server/Web_Event_Adapter.cpp @@ -213,6 +213,8 @@ std::optional Web_Event_Adapter::decode(std::string_view message) { return Gallery_Request{Gallery_Request_Kind::Open, std::string(message)}; if (type == "gallery_refresh") return Gallery_Request{Gallery_Request_Kind::Refresh, std::string(message)}; + if (type == "gallery_feedback") + return Gallery_Request{Gallery_Request_Kind::Feedback, std::string(message)}; if (type == "gallery_patch") return Gallery_Request{Gallery_Request_Kind::Patch, std::string(message)}; if (type == "gallery_action") diff --git a/web_server/main.cpp b/web_server/app/main.cpp similarity index 79% rename from web_server/main.cpp rename to web_server/app/main.cpp index 9e74db3..090b828 100644 --- a/web_server/main.cpp +++ b/web_server/app/main.cpp @@ -1,4 +1,4 @@ -#include "Web_Server.h" +#include "web_server/Web_Server.h" #include #include #include @@ -7,21 +7,21 @@ namespace { std::uint16_t parse_port(int argc, char** argv) { constexpr std::uint16_t default_port = 8848; - if (argc != 3 || std::string_view(argv[1]) != "--port") + if(argc != 3 || std::string_view(argv[1]) != "--port") return default_port; unsigned value{}; const std::string_view text(argv[2]); const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); - if (error != std::errc{} || end != text.data() + text.size() || value == 0 || value > 65535) { + if(error != std::errc{} || end != text.data() + text.size() || value == 0 || value > 65535) { std::cerr << "Invalid port: " << text << '\n'; return 0; } return static_cast(value); } -} // namespace +} int main(int argc, char** argv) { const std::uint16_t port = parse_port(argc, argv); - if (port == 0) + if(port == 0) return 2; std::cout << "http://127.0.0.1:8848" << std::endl; const std::filesystem::path executable = std::filesystem::absolute(argv[0]); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index ec8fa2b..55b75a2 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -76,6 +76,11 @@ nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patc Gallery_Request_Kind::Patch, request.dump()))); } +void send_client_feedback(Gallery_Plot_Session& session, std::string_view encoded) { + EXPECT_FALSE(session.handle(gallery_request( + Gallery_Request_Kind::Feedback, std::string(encoded))).has_value()); +} + template bool wait_for_condition(Predicate&& predicate, std::chrono::milliseconds timeout = std::chrono::milliseconds(750)) { @@ -147,6 +152,10 @@ TEST(RenderiveWebBridge, DecodesOnlyEventMessages) { R"({"category":"event","type":"gallery_refresh"})"); ASSERT_TRUE(refresh.has_value()); EXPECT_EQ(std::get(*refresh).kind, Gallery_Request_Kind::Refresh); + const auto feedback = Web_Event_Adapter::decode( + R"({"category":"event","type":"gallery_feedback","client_metrics":{}})"); + ASSERT_TRUE(feedback.has_value()); + EXPECT_EQ(std::get(*feedback).kind, Gallery_Request_Kind::Feedback); const auto bounded = Web_Event_Adapter::decode( R"({"category":"event","type":"resize","width":1e100,"height":-1e100})"); @@ -191,6 +200,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); @@ -310,6 +320,8 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract EXPECT_TRUE(ids.insert(id).second); EXPECT_FALSE(control.at("api").get().empty()); EXPECT_FALSE(control.at("group").get().empty()); + const auto& amis = control.at("amis"); + EXPECT_EQ(amis.at("name"), id); const std::string input = control.at("input"); const auto apply = [&](const nlohmann::json& value) { @@ -325,16 +337,28 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract }; if (input == "boolean") { + EXPECT_EQ(amis.at("type"), "switch"); const bool alternative = !control.at("value").get(); const auto accepted = apply(alternative); ASSERT_TRUE(accepted.candidate.has_value()); EXPECT_EQ(std::get(accepted.candidate->values.at(id)), alternative); expect_rejected(1); } else if (input == "number") { + EXPECT_EQ(amis.at("type"), "input-number"); + EXPECT_TRUE(amis.at("required").get()); const double minimum = control.at("minimum"); const double maximum = control.at("maximum"); const double current = control.at("value"); const double step = std::max(control.at("step").get(), 1e-9); + EXPECT_DOUBLE_EQ(amis.at("min"), minimum); + EXPECT_DOUBLE_EQ(amis.at("max"), maximum); + EXPECT_DOUBLE_EQ(amis.at("step"), control.at("step").get()); + EXPECT_DOUBLE_EQ(amis.at("validations").at("minimum"), minimum); + EXPECT_DOUBLE_EQ(amis.at("validations").at("maximum"), maximum); + if (control.at("integer").get()) { + EXPECT_EQ(amis.at("precision"), 0); + EXPECT_TRUE(amis.at("validations").at("isInt").get()); + } double alternative = current + step; if (alternative > maximum) alternative = current - step; @@ -356,7 +380,11 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract expect_rejected(std::clamp(std::floor(current) + 0.5, minimum + 0.5, maximum - 0.5)); } else if (input == "select") { + EXPECT_EQ(amis.at("type"), "select"); + EXPECT_TRUE(amis.at("required").get()); + EXPECT_FALSE(amis.at("clearable").get()); const auto& options = control.at("options"); + EXPECT_EQ(amis.at("options").size(), options.size()); ASSERT_FALSE(options.empty()); const std::string current = control.at("value"); const auto alternative = std::find_if( @@ -371,6 +399,10 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract expect_rejected("__invalid_gallery_option__"); expect_rejected(7); } else if (input == "color") { + EXPECT_EQ(amis.at("type"), "input-color"); + EXPECT_TRUE(amis.at("required").get()); + EXPECT_FALSE(amis.at("clearable").get()); + EXPECT_EQ(amis.at("validations").at("matchRegexp"), "^#[0-9A-Fa-f]{6}$"); const std::string value = control.at("value") == "#123456" ? "#654321" : "#123456"; const auto accepted = apply(value); @@ -380,6 +412,8 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract expect_rejected(7); } else { ASSERT_EQ(input, "text"); + EXPECT_EQ(amis.at("type"), "input-text"); + EXPECT_EQ(amis.at("validations").at("maxLength"), 160); std::string value = control.at("value").get() + "_qa"; const auto accepted = apply(value); ASSERT_TRUE(accepted.candidate.has_value()); @@ -415,6 +449,13 @@ TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) EXPECT_TRUE(ids.insert(id).second); EXPECT_FALSE(action.at("api").get().empty()); EXPECT_FALSE(action.at("group").get().empty()); + const std::string argument_input = action.at("argument_input"); + if (!argument_input.empty()) { + const auto& amis = action.at("argument_amis"); + EXPECT_EQ(amis.at("name"), "action_" + id); + EXPECT_TRUE(amis.at("required").get()); + EXPECT_EQ(amis.at("type"), argument_input == "number" ? "input-number" : "input-text"); + } Gallery_Plot_Session session; ASSERT_EQ(response_json(session.handle(gallery_request( @@ -796,6 +837,7 @@ TEST(RenderiveWebGallery, BackendPublishesAmisValidationSchemas) { ASSERT_NE(max_fps, controls.end()); const auto& max_fps_amis = max_fps->at("amis"); EXPECT_EQ(max_fps_amis.at("type"), "input-number"); + EXPECT_TRUE(max_fps_amis.at("required").get()); EXPECT_DOUBLE_EQ(max_fps_amis.at("min").get(), 0.01); EXPECT_DOUBLE_EQ(max_fps_amis.at("max").get(), 1'000'000'000.0); EXPECT_DOUBLE_EQ(max_fps_amis.at("validations").at("minimum").get(), 0.01); @@ -804,6 +846,8 @@ TEST(RenderiveWebGallery, BackendPublishesAmisValidationSchemas) { return item.at("id") == "background_color"; }); ASSERT_NE(background, controls.end()); + EXPECT_TRUE(background->at("amis").at("required").get()); + EXPECT_FALSE(background->at("amis").at("clearable").get()); EXPECT_EQ(background->at("amis").at("validations").at("matchRegexp"), "^#[0-9A-Fa-f]{6}$"); const auto patched = Gallery_Protocol::apply_patch( @@ -825,6 +869,7 @@ TEST(RenderiveWebGallery, BackendPublishesAmisValidationSchemas) { const auto& burst_amis = burst->at("argument_amis"); EXPECT_EQ(burst_amis.at("type"), "input-number"); EXPECT_EQ(burst_amis.at("name"), "action_mode_enqueue_burst"); + EXPECT_TRUE(burst_amis.at("required").get()); } TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) { @@ -1082,10 +1127,10 @@ TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundReschedule {"frequency_limit_enabled", false}, {"consumer_feedback_enabled", true} }).at("type"), "case_state"); - auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Refresh, - R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":25,"presentation_fps":25,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":40,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - auto observer = observed.at("telemetry").at("kernel_observer"); + send_client_feedback(session, + R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":25,"presentation_fps":25,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":40,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"); + auto observed = refresh_telemetry(session); + auto observer = observed.at("kernel_observer"); EXPECT_FALSE(observer.at("frequency_limit_enabled").get()); EXPECT_TRUE(observer.at("consumer_feedback_enabled").get()); EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000); @@ -1121,11 +1166,11 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback Gallery_Request_Kind::Patch, R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})"))).at("type"), "case_state"); - auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Refresh, - R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - auto observer = observed.at("telemetry").at("kernel_observer"); - auto limit = observed.at("telemetry").at("low_latency_limit"); + send_client_feedback(session, + R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"); + auto observed = refresh_telemetry(session); + auto observer = observed.at("kernel_observer"); + auto limit = observed.at("low_latency_limit"); EXPECT_TRUE(observer.at("frequency_limit_enabled").get()); EXPECT_TRUE(observer.at("consumer_feedback_enabled").get()); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 20'000'000); @@ -1135,10 +1180,10 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("limit_state"), "consumer_limited"); EXPECT_EQ(limit.at("consumer_feedback_source"), "presentation"); - observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Refresh, - R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - observer = observed.at("telemetry").at("kernel_observer"); + send_client_feedback(session, + R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"); + observed = refresh_telemetry(session); + observer = observed.at("kernel_observer"); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000); EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 21'000'000); EXPECT_EQ(observer.at("consumer_variation_ns"), 125'000); @@ -1173,10 +1218,10 @@ TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndMa {"consumer_presentation_feedback_enabled", false}, {"consumer_manual_feedback_enabled", false} }).at("type"), "case_state"); - auto observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Refresh, - R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":25,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); - auto observer = observed.at("telemetry").at("kernel_observer"); + send_client_feedback(session, + R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":25,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"); + auto observed = refresh_telemetry(session); + auto observer = observed.at("kernel_observer"); EXPECT_EQ(observer.at("consumer_feedback_source"), "pixel"); EXPECT_EQ(observer.at("consumer_pixel_interval_ns"), 40'000'000); EXPECT_EQ(observer.at("consumer_presentation_interval_ns"), 16'000'000); @@ -1290,11 +1335,9 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { ASSERT_TRUE(wait_for_condition([&session] { return successful_render_count(session) >= 4; })); - const auto observed = session.handle(gallery_request( - Gallery_Request_Kind::Refresh, - R"({"category":"event","type":"gallery_refresh","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})")); - ASSERT_TRUE(observed.has_value()); - const auto telemetry = parse_json(observed->payload).at("telemetry"); + send_client_feedback(session, + R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})"); + const auto telemetry = refresh_telemetry(session); const auto& performance = telemetry.at("performance"); EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get()); EXPECT_GE(performance.at("successful_render_count").get(), 4U); diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 587c305..5fb0d09 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -161,6 +161,8 @@ class GallerySession { this.telemetry = {}; this.controlValues = new Map(); this.lastSubmittedControls = new Map(); + this.pendingStateRequests = []; + this.lastFeedbackAt = 0; this.frameCount = 0; this.framePending = false; this.frameTimeout = null; @@ -283,7 +285,8 @@ class GallerySession { if (this.socket !== socket) return; this.socketState = "ready"; this.socketText = "WS 已连接"; - this.send("gallery_open", {case: this.definition.id, frame_mode: this.mode.id}); + this.lastFeedbackAt = 0; + this.sendStateRequest("open", "gallery_open", {case: this.definition.id, frame_mode: this.mode.id}); this.resize(); }); socket.addEventListener("message", event => { @@ -299,6 +302,9 @@ class GallerySession { this.framePending = false; clearTimeout(this.frameTimeout); this.frameTimeout = null; + this.pendingStateRequests.length = 0; + this.lastSubmittedControls.clear(); + this.lastFeedbackAt = 0; this.socketState = "error"; this.socketText = this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见"; this.updateMotionStatus(); @@ -315,6 +321,11 @@ class GallerySession { send(type, payload = {}) { if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(message(type, payload)); } + sendStateRequest(kind, type, payload = {}) { + if (this.socket?.readyState !== WebSocket.OPEN) return; + this.pendingStateRequests.push(kind); + this.socket.send(message(type, payload)); + } receiveJson(raw) { let data; try { @@ -324,19 +335,21 @@ class GallerySession { return; } if (data.type === "error") { + this.pendingStateRequests.shift(); const detail = Object.values(data.field_errors || {})[0] || data.message || "后端拒绝操作"; this.lastSubmittedControls.clear(); notify(detail, true); if (menuSession === this) { menuStatus = detail; - renderMenu(this, true); + updateMenuData(); } return; } if (data.type !== "case_state") return; + const requestKind = this.pendingStateRequests.shift(); this.controls = data.controls?.data || []; this.actions = data.actions?.data || []; - this.telemetry = data.telemetry || {}; + if (requestKind === "open" || requestKind === "refresh") this.telemetry = data.telemetry || {}; this.controlValues.clear(); for (const item of this.controls) this.controlValues.set(item.id, item.value); this.lastSubmittedControls.clear(); @@ -467,10 +480,19 @@ class GallerySession { }, 1500); } refreshBackendState() { - const time = performance.now(); + if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) return; + this.pushClientFeedback(performance.now()); + this.sendStateRequest("refresh", "gallery_refresh"); + } + pushClientFeedback(time) { + this.lastFeedbackAt = time; this.transportFps = this.currentRate(this.transportTimes, time); this.presentationFps = this.currentRate(this.presentationTimes, time); - this.send("gallery_refresh", {client_metrics: this.clientMetrics(time)}); + this.send("gallery_feedback", {client_metrics: this.clientMetrics(time)}); + } + sendClientFeedback(time) { + if (!this.ready || !this.displayVisible() || this.socket?.readyState !== WebSocket.OPEN || time - this.lastFeedbackAt < 650) return; + this.pushClientFeedback(time); } clientMetrics(time = performance.now()) { return { @@ -525,7 +547,7 @@ class GallerySession { const value = normalizeEventValue(payload, id); if (Object.is(this.lastSubmittedControls.get(id), value) || Object.is(this.controlValues.get(id), value)) return; this.lastSubmittedControls.set(id, value); - this.send("gallery_patch", {patch: {[id]: value}}); + this.sendStateRequest("patch", "gallery_patch", {patch: {[id]: value}}); menuStatus = `提交 ${item.api} · 等待后端回读`; } runAction(id, payload) { @@ -536,7 +558,7 @@ class GallerySession { const field = `action_${item.id}`; request.argument = normalizeEventValue(payload, field); } - this.send("gallery_action", request); + this.sendStateRequest("action", "gallery_action", request); menuStatus = `执行 ${item.api}`; if (["mode_render", "mode_dequeue", "mode_cycle"].includes(item.id)) setTimeout(() => this.requestFrame(performance.now(), true), 40); } @@ -555,7 +577,7 @@ class GallerySession { 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 client = this.clientMetrics(); + const client = this.telemetry.client_performance || {}; const observerValues = {}; const clientValues = {}; observerValues.mode = observer.mode || this.mode.id; @@ -571,10 +593,10 @@ class GallerySession { motionText: this.motionText, frameCount: formatInteger(this.frameCount), backendFps: formatFps(performanceData.measured_fps), - transportFps: formatFps(performanceData.pixel_response_fps || this.transportFps), - presentationFps: formatFps(this.presentationFps), - roundTrip: Number(this.frameRoundTripMs || 0).toFixed(2), - overwritten: formatInteger(this.overwrittenPixelFrames), + transportFps: formatFps(performanceData.pixel_response_fps || client.transport_fps), + presentationFps: formatFps(client.presentation_fps), + roundTrip: Number(client.frame_round_trip_ms || 0).toFixed(2), + overwritten: formatInteger(client.overwritten_pixel_frames), renderMs: Number(performanceData.last_render_ms || 0).toFixed(2), encodeMs: Number(performanceData.last_pixel_encode_ms || 0).toFixed(2), bandwidth: Number(performanceData.pixel_payload_megabytes_per_second || 0).toFixed(1), @@ -583,8 +605,8 @@ class GallerySession { points: `${formatInteger(dataShape.input_points)}→${formatInteger(dataShape.rendered_elements)}`, limit: limitDuration === undefined ? limitName : `${limitName} · ${formatNanoseconds(limitDuration)}`, event: observer.last_event || "none", - timeouts: formatInteger(this.frameTimeoutCount), - pixelAge: this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—", + timeouts: formatInteger(client.frame_request_timeout_count), + pixelAge: Number(client.last_pixel_receive_age_ms || 0) > 0 ? `${Number(client.last_pixel_receive_age_ms).toFixed(0)} ms` : "—", limitFrequency: limitTile(observer.frequency_limit_enabled !== false, limit.current === "frequency_limited", observer.target_interval_ns), limitPaint: limitTile(true, limit.current === "paint_limited", observer.paint_duration_ns), limitRender: limitTile(true, limit.current === "render_limited", observer.render_duration_ns), @@ -711,9 +733,10 @@ function controlEventAction(session, item, source) { } function controlSchema(session, item) { const control = {...item.amis}; + const formId = `control_${session.key}_${item.id}`.replace(/[^A-Za-z0-9_-]/g, "_"); const eventName = ["switch", "select", "input-color"].includes(control.type) ? "change" : "blur"; - control.onEvent = {[eventName]: {actions: [controlEventAction(session, item, "context.data")]}}; - return {type: "form", wrapWithPanel: false, className: "rv-control-form", body: [control], actions: []}; + control.onEvent = {[eventName]: {actions: [{actionType: "submit", componentId: formId}]}}; + return {type: "form", id: formId, wrapWithPanel: false, className: "rv-control-form", body: [control], actions: [], onEvent: {submit: {preventDefault: true, actions: [controlEventAction(session, item, "context.data")]}}}; } function buildControlTab(session) { return [...groupItems(session.controls)].map(([group, items]) => ({type: "panel", className: "rv-control-panel", title: group, body: items.map(item => controlSchema(session, item))})); @@ -725,8 +748,8 @@ function actionSchema(session, item) { const description = item.description || ""; const body = [{type: "tpl", tpl: `
${escapeHtml(label)}${escapeHtml(api)}
${description ? `${escapeHtml(description)}` : ""}
`}]; if (item.argument_input) body.push({...item.argument_amis, name: field}); - body.push({type: "button", label: `执行 · ${label}`, level: "primary", onEvent: {click: {actions: [{actionType: "custom", script: `window.RenderiveGallery.runAction(${quote(session.key)},${quote(item.id)},context.data);`}]}}}); - return {type: "form", wrapWithPanel: false, className: "rv-action-form", body, actions: []}; + body.push({type: "button", actionType: "submit", label: `执行 · ${label}`, level: "primary"}); + return {type: "form", wrapWithPanel: false, className: "rv-action-form", body, actions: [], onEvent: {submit: {preventDefault: true, actions: [{actionType: "custom", script: `window.RenderiveGallery.runAction(${quote(session.key)},${quote(item.id)},context.data);`}]}}}; } function buildActionTab(session) { return [...groupItems(session.actions)].map(([group, items]) => ({type: "panel", className: "rv-control-panel", title: group, body: items.map(item => actionSchema(session, item))})); @@ -852,6 +875,7 @@ function loop(time) { session.presentLatest(time); session.updateMotionStatus(time); if (session.mode.id !== "low_latency") session.requestFrame(time); + session.sendClientFeedback(time); } requestAnimationFrame(loop); }