From 0dcf1fbb35490877eb339365e85078c5eb2cd692 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Tue, 11 Aug 2026 11:01:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=B6=E6=9E=84=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/datoviz.cmake | 51 ++++++++++++++++ cmake/pthreads4w.cmake | 36 +++++++++++ main.cmake | 35 +++++++++++ web_server/Gallery_Plot_Session.cpp | 86 ++++++++++----------------- web_server/tests/Web_Bridge_Tests.cpp | 79 ++++++++++++++++++++++++ webapp_gallery/app.js | 20 +++++-- 6 files changed, 249 insertions(+), 58 deletions(-) create mode 100644 cmake/datoviz.cmake create mode 100644 cmake/pthreads4w.cmake diff --git a/cmake/datoviz.cmake b/cmake/datoviz.cmake new file mode 100644 index 0000000..e29b65f --- /dev/null +++ b/cmake/datoviz.cmake @@ -0,0 +1,51 @@ +include_guard(GLOBAL) +block() + set(datoviz_opts + -DDVZ_BUILD_CORE=ON + -DDVZ_BUILD_CONTROLLER=ON + -DDVZ_BUILD_VK=ON + -DDVZ_BUILD_CANVAS=ON + -DDVZ_BUILD_DRP2=ON + -DDVZ_BUILD_WEBGPU=OFF + -DDVZ_BUILD_SCENE=ON + -DDVZ_BUILD_APP=ON + -DDVZ_BUILD_GUI=OFF + -DDVZ_BUILD_TESTING=OFF + -DDVZ_BUILD_EXAMPLES=OFF + -DDVZ_INSTALL=ON + -DDVZ_VENDORED_DEPS=ON + -DDVZ_ENABLE_CUDA=OFF + -DDVZ_ENABLE_KVAZAAR=OFF + -DDVZ_ENABLE_QT_BRIDGE=OFF + -DDVZ_ENABLE_SHADERC=OFF + -DDVZ_WITH_FREETYPE=OFF + -DDVZ_WITH_MSDF_ATLAS=OFF + -DDVZ_WITH_MSDF_SVG=OFF + -DDVZ_WITH_ZLIB=OFF + -DDVZ_WITH_GLFW=ON + ) + _register_git_cmake_library(renderive::datoviz + "https://github.com/datoviz/datoviz.git" + "v0.4.0rc2" + ) + rcl_cmake_library_set_cmake_options(renderive::datoviz ${datoviz_opts}) + if (WIN32 AND NOT MINGW) + rcl_cmake_library_set_deps(renderive::datoviz renderive::pthreads4w) + endif () + rcl_get_effective_install_dir(renderive::datoviz DATOVIZ_ROOT) + set(DATOVIZ_CMAKE_DIR "${DATOVIZ_ROOT}/lib/cmake/datoviz") + rcl_cmake_library_set_other_use_opt(renderive::datoviz + "-Ddatoviz_DIR=\"${DATOVIZ_CMAKE_DIR}\"" + "-DDatoviz_DIR=\"${DATOVIZ_CMAKE_DIR}\"" + ) + rcl_cmake_library_set_init_script(renderive::datoviz + "set(DATOVIZ_ROOT \"${DATOVIZ_ROOT}\")" + "set(datoviz_DIR \"${DATOVIZ_CMAKE_DIR}\")" + "set(Datoviz_DIR \"${DATOVIZ_CMAKE_DIR}\")" + ) + rcl_cmake_library_set_clear_script(renderive::datoviz + "unset(DATOVIZ_ROOT)" + "unset(datoviz_DIR)" + "unset(Datoviz_DIR)" + ) +endblock() \ No newline at end of file diff --git a/cmake/pthreads4w.cmake b/cmake/pthreads4w.cmake new file mode 100644 index 0000000..02876f5 --- /dev/null +++ b/cmake/pthreads4w.cmake @@ -0,0 +1,36 @@ +include_guard(GLOBAL) +block() + _register_git_cmake_library(renderive::pthreads4w + "https://github.com/GerHobbelt/pthread-win32.git" + "v3.0.3.1" + ) + rcl_get_effective_install_dir(renderive::pthreads4w PTHREADS4W_ROOT) + set(PTHREADS4W_INCLUDE_DIR "${PTHREADS4W_ROOT}/include") + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(PTHREADS4W_LIBRARY "${PTHREADS4W_ROOT}/lib/libpthreadVC3d.lib") + else () + set(PTHREADS4W_LIBRARY "${PTHREADS4W_ROOT}/lib/libpthreadVC3.lib") + endif () + set(PTHREADS4W_CONFIG_DIR "${CMAKE_CURRENT_LIST_DIR}/pthreads4w_config") + set(pthreads4w_opts + -DENABLE_TESTS=OFF + "-DDLLDEST=${PTHREADS4W_ROOT}/bin" + "-DLIBDEST=${PTHREADS4W_ROOT}/lib" + "-DHDRDEST=${PTHREADS4W_ROOT}/include" + ) + rcl_cmake_library_set_cmake_options(renderive::pthreads4w ${pthreads4w_opts}) + rcl_cmake_library_set_other_use_opt(renderive::pthreads4w + "-DPThreads4W_DIR=\"${PTHREADS4W_CONFIG_DIR}\"" + "-DPThreads4W_INCLUDE_DIR=\"${PTHREADS4W_INCLUDE_DIR}\"" + "-DPThreads4W_LIBRARY=\"${PTHREADS4W_LIBRARY}\"" + ) + rcl_cmake_library_set_init_script(renderive::pthreads4w + "set(PThreads4W_DIR \"${PTHREADS4W_CONFIG_DIR}\")" + "set(PThreads4W_INCLUDE_DIR \"${PTHREADS4W_INCLUDE_DIR}\")" + ) + rcl_cmake_library_set_clear_script(renderive::pthreads4w + "unset(PThreads4W_DIR)" + "unset(PThreads4W_INCLUDE_DIR)" + "unset(PThreads4W_LIBRARY)" + ) +endblock() \ No newline at end of file diff --git a/main.cmake b/main.cmake index 5c8e38a..3cce43d 100644 --- a/main.cmake +++ b/main.cmake @@ -1,4 +1,7 @@ include_guard(GLOBAL) +#rcl_init("renderive") +#include(${CMAKE_CURRENT_LIST_DIR}/cmake/pthreads4w.cmake) +#include(${CMAKE_CURRENT_LIST_DIR}/cmake/datoviz.cmake) set(renderive_dependencies global::GTest global::blend2d @@ -6,8 +9,10 @@ set(renderive_dependencies global::mpmcqueue global::drogon global::libdatachannel + # renderive::datoviz ${Psc_Core_dependencies} ) +#rcl_generate_registered_cmake_library() rcl_load_dependency_environment(${renderive_dependencies}) library_is_installed_with_rely(renderive_dependencies_installed ${renderive_dependencies}) if (NOT renderive_dependencies_installed) @@ -131,3 +136,33 @@ if (NOT TARGET Adminive::Nlohmann) unset(renderive_saved_build_testing) endif () include(${CMAKE_CURRENT_LIST_DIR}/web.cmake) +set(renderive_package_includes + "^.*$" +) +set(renderive_package_excludes + # "^[.]git/.*$" + # "^[.]idea/.*$" + "^data/.*$" + "^performance/.*$" + "^cmake-build-[^/]*/.*$" + "^(.*/)?node_modules/.*$" + "^(.*/)?.git/.*$" + "^(.*/)?wwwroot/.*$" + "^(.*/)?dist/.*$" + "^(.*/)?htdocs/.*$" + "^(.*/)?model/.*$" + "^(.*/)?.idea/.*$" + "^(.*/)?.playwright-cli/.*$" + "^(.*/)?performance/.*$" + "^(.*/)?dll_source_lzy/.*$" + "^.*[.]tsbuildinfo$" + "^.*[.]zip$" + "^.*[.]otf$" +) +_add_project_zip_target( + renderive_package_zip + "${CMAKE_CURRENT_LIST_DIR}/renderive_package.zip" + "${CMAKE_SOURCE_DIR}" + renderive_package_includes + renderive_package_excludes +) \ No newline at end of file diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index 5e00021..e328e7d 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -279,8 +279,14 @@ public: } void set_state(Gallery_State state) { + const bool render_schedule_changed = frame_mode_ == Gallery_Frame_Mode::Low_Latency && + (bool_value(state_, "frequency_limit_enabled") != bool_value(state, "frequency_limit_enabled") || + number_value(state_, "max_render_fps") != number_value(state, "max_render_fps") || + bool_value(state_, "consumer_feedback_enabled") != bool_value(state, "consumer_feedback_enabled")); state_ = std::move(state); apply_state(); + if (render_schedule_changed) + render_history_.clear(); } void resize(Size size) { @@ -1572,75 +1578,47 @@ struct Gallery_Plot_Session::Impl { void run_automatic_renderer(std::stop_token stop) { using Clock = std::chrono::steady_clock; std::unique_lock lock(mutex); - auto next_deadline = Clock::now(); - std::uint64_t active_revision = std::numeric_limits::max(); + std::optional last_render_started; + std::uint64_t active_generation = std::numeric_limits::max(); while (!stop.stop_requested()) { scheduler_condition.wait(lock, [this, &stop] { - return stop.stop_requested() || - (scene && scene->can_render_automatically()); + return stop.stop_requested() || (scene && scene->can_render_automatically()); }); if (stop.stop_requested()) break; + if (active_generation != scene_generation) { + active_generation = scene_generation; + last_render_started.reset(); + } if (foreground_waiters.load(std::memory_order_acquire) != 0) { scheduler_condition.wait(lock, [this, &stop] { - return stop.stop_requested() || - foreground_waiters.load(std::memory_order_acquire) == 0; + return stop.stop_requested() || foreground_waiters.load(std::memory_order_acquire) == 0; }); continue; } - const auto revision = scheduler_revision; - const auto cycle_started = Clock::now(); - if (active_revision != revision) { - active_revision = revision; - next_deadline = cycle_started; - } - (void)scene->render_latest_frame(); - const auto interval = std::chrono::nanoseconds( - std::max(1, scene->kernel_refresh_interval_ns())); - if (next_deadline + interval < cycle_started) - next_deadline = cycle_started; - next_deadline += interval; - const auto deadline = next_deadline; - // A render that already consumed the whole interval used to let the - // worker immediately reacquire the session mutex. Heavy scenes - // (notably Constellation) could then starve pixel pulls, observer - // requests and shutdown indefinitely. Explicitly hand the mutex to - // registered foreground waiters; otherwise yield without using a - // timed wait (which Windows may round to ~15 ms). - if (Clock::now() >= deadline) { - if (foreground_waiters.load(std::memory_order_acquire) != 0) { - scheduler_condition.wait(lock, [this, &stop] { - return stop.stop_requested() || - foreground_waiters.load(std::memory_order_acquire) == 0; + const auto interval = std::chrono::nanoseconds(std::max(1, scene->kernel_refresh_interval_ns())); + const auto now = Clock::now(); + const auto deadline = last_render_started ? *last_render_started + interval : now; + if (now < deadline) { + const auto spin_window = std::min(interval / 4, std::chrono::duration_cast(std::chrono::microseconds(250))); + const auto coarse_deadline = deadline > now + spin_window ? deadline - spin_window : now; + if (now < coarse_deadline) { + scheduler_condition.wait_until(lock, coarse_deadline, [this, &stop, revision, active_generation] { + return stop.stop_requested() || scheduler_revision != revision || scene_generation != active_generation || + foreground_waiters.load(std::memory_order_acquire) != 0 || !scene || !scene->can_render_automatically(); }); - } else { - lock.unlock(); - std::this_thread::yield(); - lock.lock(); + continue; } + lock.unlock(); + while (!stop.stop_requested() && Clock::now() < deadline) + std::this_thread::yield(); + lock.lock(); continue; } - const auto spin_window = std::min( - interval / 4, - std::chrono::duration_cast( - std::chrono::microseconds(250))); - const auto coarse_deadline = deadline > cycle_started + spin_window ? - deadline - spin_window : cycle_started; - if (Clock::now() < coarse_deadline) { - const bool interrupted = scheduler_condition.wait_until( - lock, coarse_deadline, [this, &stop, revision] { - return stop.stop_requested() || scheduler_revision != revision || - foreground_waiters.load(std::memory_order_acquire) != 0; - }); - if (interrupted) - continue; - } - - lock.unlock(); - while (!stop.stop_requested() && Clock::now() < deadline) - std::this_thread::yield(); - lock.lock(); + const auto started = Clock::now(); + (void)scene->render_latest_frame(); + last_render_started = started; } } diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 09678b4..dfa1a6f 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -975,6 +975,85 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres observer.at("limit_state") == "consumer_limited"); } +TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEvents) { + Gallery_Plot_Session session(true); + ASSERT_EQ(response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), + "case_state"); + const auto patched = patch_controls(session, { + {"frequency_limit_enabled", true}, + {"max_render_fps", 30.0}, + {"consumer_feedback_enabled", false} + }); + ASSERT_EQ(patched.at("type"), "case_state"); + const auto& patched_observer = patched.at("telemetry").at("kernel_observer"); + EXPECT_TRUE(patched_observer.at("frequency_limit_enabled").get()); + EXPECT_FALSE(patched_observer.at("consumer_feedback_enabled").get()); + EXPECT_EQ(patched_observer.at("target_interval_ns"), 33'333'333); + ASSERT_TRUE(wait_for_condition([&session] { + return successful_render_count(session) >= 3; + })); + const auto pointer = Web_Event_Adapter::decode( + R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})"); + ASSERT_TRUE(pointer.has_value()); + const auto before = successful_render_count(session); + const auto started = std::chrono::steady_clock::now(); + const auto deadline = started + std::chrono::milliseconds(450); + while (std::chrono::steady_clock::now() < deadline) { + EXPECT_FALSE(session.handle(*pointer).has_value()); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + const auto elapsed = std::chrono::steady_clock::now() - started; + const auto after = successful_render_count(session); + const auto rendered = after - before; + EXPECT_GT(rendered, 0U); + EXPECT_LE(rendered, 16U); + const auto telemetry = observe_telemetry(session); + const auto& observer = telemetry.at("kernel_observer"); + EXPECT_EQ(observer.at("target_interval_ns"), 33'333'333); + EXPECT_GE(observer.at("next_refresh_interval_ns").get(), 33'333'333U); + EXPECT_GE(elapsed, std::chrono::milliseconds(450)); +} + +TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundRescheduleEvents) { + Gallery_Plot_Session session(true); + ASSERT_EQ(response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), + "case_state"); + ASSERT_EQ(patch_controls(session, { + {"frequency_limit_enabled", false}, + {"consumer_feedback_enabled", true} + }).at("type"), "case_state"); + auto observed = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Observe, + R"({"category":"event","type":"gallery_observe","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"); + 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); + ASSERT_TRUE(wait_for_condition([&session] { + return successful_render_count(session) >= 3; + })); + const auto pointer = Web_Event_Adapter::decode( + R"({"category":"event","type":"pointer_move","x":320,"y":180,"button":"none","buttons":0,"modifiers":0})"); + ASSERT_TRUE(pointer.has_value()); + const auto before = successful_render_count(session); + const auto started = std::chrono::steady_clock::now(); + const auto deadline = started + std::chrono::milliseconds(450); + while (std::chrono::steady_clock::now() < deadline) { + EXPECT_FALSE(session.handle(*pointer).has_value()); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + const auto after = successful_render_count(session); + const auto rendered = after - before; + EXPECT_GT(rendered, 0U); + EXPECT_LE(rendered, 14U); + observed = observe_telemetry(session); + observer = observed.at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000); + EXPECT_GE(observer.at("next_refresh_interval_ns").get(), 40'000'000U); +} + TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedbackWithoutTransportFpsCap) { Gallery_Plot_Session session(true); ASSERT_EQ(response_json(session.handle(gallery_request( diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 69de45e..3caee94 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -498,6 +498,7 @@ function openMenu(card, x, y) { } function closeMenu() { elements.menu.hidden = true; activeCard = null; } function renderControl(item) { + const card = activeCard; const row = document.createElement("div"); row.className = "control-row"; const copy = document.createElement("div"); copy.className = "control-copy"; const label = document.createElement("label"); label.textContent = item.label; @@ -513,11 +514,22 @@ function renderControl(item) { if (item.input === "number") { input.min = item.minimum; input.max = item.maximum; input.step = item.step || "any"; } } input.className = "control-input"; input.title = item.description || item.api; - input.addEventListener("change", () => { + let submitTimer = 0; + const submit = () => { + clearTimeout(submitTimer); submitTimer = 0; const value = item.input === "boolean" ? input.checked : item.input === "number" ? Number(input.value) : input.value; - activeCard.send("gallery_patch", {patch: {[item.id]: value}}); - elements.menuStatus.textContent = `调用 ${item.api}`; - }); + if (item.input === "number" && (input.value === "" || !Number.isFinite(value))) return; + card.send("gallery_patch", {patch: {[item.id]: value}}); + elements.menuStatus.textContent = `提交 ${item.api} · 等待后端回读`; + }; + if (item.input === "number") { + input.addEventListener("input", () => { + clearTimeout(submitTimer); + submitTimer = setTimeout(submit, 120); + }); + input.addEventListener("change", submit); + input.addEventListener("keydown", event => { if (event.key === "Enter") submit(); }); + } else input.addEventListener("change", submit); row.append(copy, input); return row; } function renderAction(item) {