diff --git a/Core2/plot/Plot_Core.cpp b/Core2/plot/Plot_Core.cpp index 9e3f14d..772b90a 100644 --- a/Core2/plot/Plot_Core.cpp +++ b/Core2/plot/Plot_Core.cpp @@ -166,7 +166,7 @@ std::string limit_state_name(bool available, std::uint32_t state) { if (!available) return "not_applicable"; static constexpr const char* names[]{"frequency_limited", "paint_limited", "render_limited", - "consumer_limited"}; + "consumer_limited", "unlimited"}; return state < std::size(names) ? names[state] : "unknown"; } @@ -460,12 +460,25 @@ void Plot_Core::set_max_render_fps(double fps) { notify_model_dirty(); } +void Plot_Core::clear_max_render_fps() { + if (impl_->mode != Frame_Control_Mode::Low_Latency) + throw std::logic_error("maximum render FPS is only available in low-latency mode"); + std::get>(impl_->scene)->frame_control.clear_frequency_limit(); + notify_model_dirty(); +} + void Plot_Core::set_consumer_feedback(Frame_Consumer_Feedback feedback) { if (impl_->mode != Frame_Control_Mode::Low_Latency) throw std::logic_error("consumer feedback is only available in low-latency mode"); std::get>(impl_->scene)->frame_control.set_consumer_feedback(feedback); } +void Plot_Core::clear_consumer_feedback() { + if (impl_->mode != Frame_Control_Mode::Low_Latency) + throw std::logic_error("consumer feedback is only available in low-latency mode"); + std::get>(impl_->scene)->frame_control.clear_consumer_feedback(); +} + double Plot_Core::max_render_fps() const noexcept { if (impl_->mode != Frame_Control_Mode::Low_Latency) return std::numeric_limits::quiet_NaN(); @@ -527,7 +540,12 @@ Frame_Observer_Snapshot Plot_Core::frame_observer_snapshot() const { snapshot.paint_duration_ns = state.paint_duration_ns; snapshot.render_duration_ns = state.render_duration_ns; snapshot.target_interval_ns = state.target_interval_ns; + snapshot.frequency_limit_enabled = state.frequency_limit_enabled; + snapshot.consumer_feedback_enabled = state.consumer_feedback_enabled; snapshot.bottleneck_duration_ns = state.bottleneck_duration_ns; + snapshot.consumer_sample_interval_ns = state.consumer_sample_interval_ns; + snapshot.consumer_smoothed_interval_ns = state.consumer_smoothed_interval_ns; + snapshot.consumer_variation_ns = state.consumer_variation_ns; snapshot.consumer_interval_ns = state.consumer_interval_ns; snapshot.next_refresh_interval_ns = state.next_refresh_interval_ns; snapshot.end_to_end_ns = state.end_to_end_ns; diff --git a/Core2/plot/Plot_Core.h b/Core2/plot/Plot_Core.h index 2c6eee3..abbb68a 100644 --- a/Core2/plot/Plot_Core.h +++ b/Core2/plot/Plot_Core.h @@ -50,7 +50,12 @@ struct Frame_Observer_Snapshot { std::uint64_t paint_duration_ns{}; std::uint64_t render_duration_ns{}; std::uint64_t target_interval_ns{}; + bool frequency_limit_enabled{}; + bool consumer_feedback_enabled{}; std::uint64_t bottleneck_duration_ns{}; + std::uint64_t consumer_sample_interval_ns{}; + std::uint64_t consumer_smoothed_interval_ns{}; + std::uint64_t consumer_variation_ns{}; std::uint64_t consumer_interval_ns{}; std::uint64_t next_refresh_interval_ns{}; std::uint64_t paint_lease_wait_ns{}; @@ -114,7 +119,9 @@ public: [[nodiscard]] bool view_active() const noexcept; void set_max_render_fps(double fps); + void clear_max_render_fps(); void set_consumer_feedback(Frame_Consumer_Feedback feedback); + void clear_consumer_feedback(); [[nodiscard]] double max_render_fps() const noexcept; [[nodiscard]] Low_Latency_Diagnostics diagnostics() const; [[nodiscard]] Refresh_Control_Snapshot refresh_feedback_snapshot() const; diff --git a/Core2/tests/Core2_Frame_Pipeline_Tests.cpp b/Core2/tests/Core2_Frame_Pipeline_Tests.cpp index 00cbf1f..3f77495 100644 --- a/Core2/tests/Core2_Frame_Pipeline_Tests.cpp +++ b/Core2/tests/Core2_Frame_Pipeline_Tests.cpp @@ -89,12 +89,35 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2 ASSERT_TRUE(plot.prepare_frame()); ASSERT_TRUE(plot.render_prepared_frame()); const auto snapshot = plot.frame_observer_snapshot(); + EXPECT_TRUE(snapshot.frequency_limit_enabled); + EXPECT_TRUE(snapshot.consumer_feedback_enabled); EXPECT_EQ(snapshot.limit_state, "consumer_limited"); EXPECT_EQ(snapshot.target_interval_ns, 10'000'000u); + EXPECT_EQ(snapshot.consumer_sample_interval_ns, 40'000'000u); + EXPECT_EQ(snapshot.consumer_smoothed_interval_ns, 40'000'000u); + EXPECT_EQ(snapshot.consumer_variation_ns, 0u); EXPECT_EQ(snapshot.consumer_interval_ns, 40'000'000u); EXPECT_EQ(snapshot.next_refresh_interval_ns, 40'000'000u); EXPECT_EQ(plot.refresh_feedback_snapshot().next_refresh_interval_ns, 40'000'000u); } +TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) { + Plot_Core plot(Frame_Control_Mode::Low_Latency); + plot.init(); + plot.set_viewport_size({96, 54}); + plot.set_max_render_fps(100.0); + plot.set_consumer_feedback({40'000'000}); + plot.clear_consumer_feedback(); + plot.clear_max_render_fps(); + ASSERT_TRUE(plot.prepare_frame()); + ASSERT_TRUE(plot.render_prepared_frame()); + const auto snapshot = plot.frame_observer_snapshot(); + EXPECT_FALSE(snapshot.frequency_limit_enabled); + EXPECT_FALSE(snapshot.consumer_feedback_enabled); + EXPECT_EQ(snapshot.target_interval_ns, 0u); + EXPECT_EQ(snapshot.consumer_interval_ns, 0u); + EXPECT_EQ(snapshot.next_refresh_interval_ns, snapshot.bottleneck_duration_ns); + EXPECT_TRUE(snapshot.limit_state == "paint_limited" || snapshot.limit_state == "render_limited"); +} TEST(Renderive_Core2_Frame_Pipeline, LowLatencyReplacementRetiresOldFrameWithoutPhantomPendingFrame) { Plot_Core plot(Frame_Control_Mode::Low_Latency); diff --git a/Kernel/src/renderive/frame_control/Frame_Consumer_Feedback.hpp b/Kernel/src/renderive/frame_control/Frame_Consumer_Feedback.hpp index 0cc952a..d2b7956 100644 --- a/Kernel/src/renderive/frame_control/Frame_Consumer_Feedback.hpp +++ b/Kernel/src/renderive/frame_control/Frame_Consumer_Feedback.hpp @@ -1,5 +1,5 @@ #pragma once #include struct Frame_Consumer_Feedback { - std::uint64_t minimum_frame_interval_ns{}; + std::uint64_t observed_interval_ns{}; }; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index bcad607..da7ca37 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -24,7 +24,8 @@ public: frequency_limited, paint_limited, render_limited, - consumer_limited + consumer_limited, + unlimited }; struct Configuration { double frequency_hz{60.0}; @@ -68,10 +69,15 @@ public: struct State { double frequency_hz{60.0}; Limit_State limit_state{Limit_State::frequency_limited}; + bool frequency_limit_enabled{true}; + bool consumer_feedback_enabled{}; std::uint64_t target_interval_ns{}; std::uint64_t paint_duration_ns{}; std::uint64_t render_duration_ns{}; std::uint64_t bottleneck_duration_ns{}; + std::uint64_t consumer_sample_interval_ns{}; + std::uint64_t consumer_smoothed_interval_ns{}; + std::uint64_t consumer_variation_ns{}; std::uint64_t consumer_interval_ns{}; std::uint64_t end_to_end_ns{}; std::uint64_t next_refresh_interval_ns{}; @@ -145,7 +151,9 @@ public: Painter_Lease acquire_painter(); Render_Lease acquire_renderer(); void set_frequency_hz(double frequency_hz); + void clear_frequency_limit(); void set_consumer_feedback(Frame_Consumer_Feedback feedback); + void clear_consumer_feedback(); void swap() override; double frequency_hz() const noexcept override; std::uint64_t next_refresh_interval_ns() const noexcept override; @@ -160,6 +168,7 @@ private: static double checked_frequency_hz(double frequency_hz); std::uint64_t now_ns() const noexcept; void update_refresh_control(); + void update_consumer_estimator(std::uint64_t sample_interval_ns); void update_state(const Frame& frame); void observe(const Observation& observation) noexcept; bool discard_pending_frame_locked(Observation& observation); diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index 0aff315..c578829 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -1,5 +1,6 @@ #pragma once #include +#include #include template Low_Latency_Strategy::Painter_Lease::Painter_Lease(Low_Latency_Strategy& strategy) @@ -221,14 +222,33 @@ template void Low_Latency_Strategy::set_frequency_hz(double frequency_hz) { frequency_hz = checked_frequency_hz(frequency_hz); std::lock_guard lock(state_mutex_); + state_.frequency_limit_enabled = true; state_.frequency_hz = frequency_hz; state_.target_interval_ns = frequency_interval_ns(frequency_hz); update_refresh_control(); } template +void Low_Latency_Strategy::clear_frequency_limit() { + std::lock_guard lock(state_mutex_); + state_.frequency_limit_enabled = false; + state_.frequency_hz = invalid_frequency_hz(); + state_.target_interval_ns = 0; + update_refresh_control(); +} +template void Low_Latency_Strategy::set_consumer_feedback(Frame_Consumer_Feedback feedback) { std::lock_guard lock(state_mutex_); - state_.consumer_interval_ns = feedback.minimum_frame_interval_ns; + update_consumer_estimator(feedback.observed_interval_ns); + update_refresh_control(); +} +template +void Low_Latency_Strategy::clear_consumer_feedback() { + std::lock_guard lock(state_mutex_); + state_.consumer_feedback_enabled = false; + state_.consumer_sample_interval_ns = 0; + state_.consumer_smoothed_interval_ns = 0; + state_.consumer_variation_ns = 0; + state_.consumer_interval_ns = 0; update_refresh_control(); } template @@ -331,20 +351,48 @@ void Low_Latency_Strategy::update_refresh_control( state_.next_refresh_interval_ns = std::max({state_.target_interval_ns, state_.bottleneck_duration_ns, state_.consumer_interval_ns}); - if (state_.consumer_interval_ns > state_.target_interval_ns && - state_.consumer_interval_ns > state_.bottleneck_duration_ns) { + if (state_.consumer_feedback_enabled && state_.consumer_interval_ns != 0 && + state_.consumer_interval_ns >= state_.target_interval_ns && + state_.consumer_interval_ns >= state_.bottleneck_duration_ns) { state_.limit_state = Limit_State::consumer_limited; - } else if (state_.target_interval_ns != 0 && - state_.bottleneck_duration_ns <= state_.target_interval_ns) { + } else if (state_.frequency_limit_enabled && state_.target_interval_ns != 0 && + state_.target_interval_ns >= state_.bottleneck_duration_ns) { state_.limit_state = Limit_State::frequency_limited; - } else if (state_.paint_duration_ns >= state_.render_duration_ns) { - state_.limit_state = Limit_State::paint_limited; + } else if (state_.bottleneck_duration_ns != 0) { + state_.limit_state = state_.paint_duration_ns >= state_.render_duration_ns ? + Limit_State::paint_limited : Limit_State::render_limited; } else { - state_.limit_state = Limit_State::render_limited; + state_.limit_state = Limit_State::unlimited; } update_frame_control_state(state_.frequency_hz, state_.next_refresh_interval_ns); } template +void Low_Latency_Strategy::update_consumer_estimator(std::uint64_t sample_interval_ns) { + state_.consumer_sample_interval_ns = sample_interval_ns; + if (!state_.consumer_feedback_enabled) { + state_.consumer_feedback_enabled = true; + state_.consumer_smoothed_interval_ns = sample_interval_ns; + state_.consumer_variation_ns = 0; + } else { + const std::uint64_t smoothed = state_.consumer_smoothed_interval_ns; + const std::uint64_t deviation = sample_interval_ns >= smoothed ? + sample_interval_ns - smoothed : smoothed - sample_interval_ns; + if (deviation >= state_.consumer_variation_ns) + state_.consumer_variation_ns += (deviation - state_.consumer_variation_ns) / 4; + else + state_.consumer_variation_ns -= (state_.consumer_variation_ns - deviation) / 4; + if (sample_interval_ns >= state_.consumer_smoothed_interval_ns) + state_.consumer_smoothed_interval_ns += (sample_interval_ns - state_.consumer_smoothed_interval_ns) / 8; + else + state_.consumer_smoothed_interval_ns -= (state_.consumer_smoothed_interval_ns - sample_interval_ns) / 8; + } + const auto maximum = std::numeric_limits::max(); + const std::uint64_t variation_margin = state_.consumer_variation_ns > maximum / 4 ? + maximum : state_.consumer_variation_ns * 4; + state_.consumer_interval_ns = state_.consumer_smoothed_interval_ns > maximum - variation_margin ? + maximum : state_.consumer_smoothed_interval_ns + variation_margin; +} +template void Low_Latency_Strategy::update_state(const Frame& frame) { state_.paint_duration_ns = frame.statistics.timing.paint_duration_ns; state_.render_duration_ns = frame.statistics.timing.render_duration_ns; diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp index 1395805..e1be105 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp @@ -73,7 +73,7 @@ TEST(low_latency_pipeline_test, consumer_feedback_limits_refresh_without_knowing EXPECT_EQ(state.target_interval_ns, 10'000'000u); EXPECT_EQ(state.consumer_interval_ns, 40'000'000u); EXPECT_EQ(state.next_refresh_interval_ns, 40'000'000u); - strategy.set_consumer_feedback({}); + strategy.clear_consumer_feedback(); state = strategy.state(); EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); EXPECT_EQ(state.consumer_interval_ns, 0u); diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 048c551..22a1813 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -125,3 +125,65 @@ TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_withou } EXPECT_EQ(strategy.state().target_interval_ns, std::numeric_limits::max()); } +TEST(low_latency_strategy_test, smooths_consumer_feedback_in_kernel) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.set_frequency_hz(1000.0); + strategy.set_consumer_feedback({10'000'000}); + auto state = strategy.state(); + EXPECT_TRUE(state.consumer_feedback_enabled); + EXPECT_EQ(state.consumer_sample_interval_ns, 10'000'000u); + EXPECT_EQ(state.consumer_smoothed_interval_ns, 10'000'000u); + EXPECT_EQ(state.consumer_variation_ns, 0u); + EXPECT_EQ(state.consumer_interval_ns, 10'000'000u); + strategy.set_consumer_feedback({14'000'000}); + state = strategy.state(); + EXPECT_EQ(state.consumer_sample_interval_ns, 14'000'000u); + EXPECT_EQ(state.consumer_smoothed_interval_ns, 10'500'000u); + EXPECT_EQ(state.consumer_variation_ns, 1'000'000u); + EXPECT_EQ(state.consumer_interval_ns, 14'500'000u); + EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited); +} +TEST(low_latency_strategy_test, allows_frequency_and_consumer_limits_to_be_cleared) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.set_frequency_hz(1000.0); + strategy.set_consumer_feedback({20'000'000}); + strategy.clear_consumer_feedback(); + auto state = strategy.state(); + EXPECT_FALSE(state.consumer_feedback_enabled); + EXPECT_EQ(state.consumer_interval_ns, 0u); + EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); + strategy.clear_frequency_limit(); + state = strategy.state(); + EXPECT_FALSE(state.frequency_limit_enabled); + EXPECT_TRUE(std::isnan(state.frequency_hz)); + EXPECT_EQ(state.target_interval_ns, 0u); + EXPECT_EQ(state.next_refresh_interval_ns, 0u); + EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::unlimited); +} +TEST(low_latency_strategy_test, unlimited_mode_runs_at_measured_render_bottleneck) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.clear_frequency_limit(); + strategy.clear_consumer_feedback(); + { + auto frame = strategy.acquire_painter(); + time_source.advance_ns(2'000); + frame->value = 1; + } + { + auto frame = strategy.acquire_renderer(); + ASSERT_TRUE(frame); + time_source.advance_ns(5'000); + } + const auto state = strategy.state(); + EXPECT_EQ(state.target_interval_ns, 0u); + EXPECT_EQ(state.consumer_interval_ns, 0u); + EXPECT_EQ(state.bottleneck_duration_ns, 5'000u); + EXPECT_EQ(state.next_refresh_interval_ns, 5'000u); + EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited); +} diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index 9d44fda..5e00021 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -253,10 +253,12 @@ public: std::max(0.0, last_pixel_receive_age_ms) : 0.0; client_last_pixel_change_age_ms_ = std::isfinite(last_pixel_change_age_ms) ? std::max(0.0, last_pixel_change_age_ms) : 0.0; - if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { + if (frame_mode_ == Gallery_Frame_Mode::Low_Latency && + bool_value(state_, "consumer_feedback_enabled")) { const auto interval_ns = std::max(milliseconds_to_ns(client_frame_round_trip_ms_), milliseconds_to_ns(client_display_interval_ms_)); - plot_.set_consumer_feedback({interval_ns}); + if (interval_ns != 0) + plot_.set_consumer_feedback({interval_ns}); } } @@ -634,7 +636,12 @@ public: {"paint_duration_ns", observer.paint_duration_ns}, {"render_duration_ns", observer.render_duration_ns}, {"target_interval_ns", observer.target_interval_ns}, + {"frequency_limit_enabled", observer.frequency_limit_enabled}, + {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, + {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, + {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, + {"consumer_variation_ns", observer.consumer_variation_ns}, {"consumer_interval_ns", observer.consumer_interval_ns}, {"next_refresh_interval_ns", observer.next_refresh_interval_ns}, {"paint_lease_wait_ns", observer.paint_lease_wait_ns}, @@ -674,11 +681,16 @@ public: {"render_limited", observer.limit_state == "render_limited"}, {"consumer_limited", observer.limit_state == "consumer_limited"}, {"configured_frequency_hz", observer.frequency_hz}, + {"frequency_limit_enabled", observer.frequency_limit_enabled}, + {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, {"consumer_feedback_source", "web_runtime"}, {"target_interval_ns", observer.target_interval_ns}, {"paint_duration_ns", observer.paint_duration_ns}, {"render_duration_ns", observer.render_duration_ns}, {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, + {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, + {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, + {"consumer_variation_ns", observer.consumer_variation_ns}, {"consumer_interval_ns", observer.consumer_interval_ns}, {"scheduler_interval_ns", scheduler_interval_ns}, {"scheduler_frequency_hz", scheduler_interval_ns == 0 ? 0.0 : @@ -933,6 +945,11 @@ private: {"automatic_scheduler_fps", scheduler_interval_ns == 0 ? 0.0 : 1'000'000'000.0 / static_cast(scheduler_interval_ns)}, {"automatic_scheduler_limit_source", low_latency ? observer.limit_state : "not_applicable"}, + {"frequency_limit_enabled", low_latency && observer.frequency_limit_enabled}, + {"consumer_feedback_enabled", low_latency && observer.consumer_feedback_enabled}, + {"consumer_feedback_sample_interval_ns", low_latency ? observer.consumer_sample_interval_ns : 0}, + {"consumer_feedback_smoothed_interval_ns", low_latency ? observer.consumer_smoothed_interval_ns : 0}, + {"consumer_feedback_variation_ns", low_latency ? observer.consumer_variation_ns : 0}, {"consumer_feedback_interval_ns", low_latency ? observer.consumer_interval_ns : 0}, {"backend_render_fps", recent_rate(render_history_, now)}, {"pixel_response_fps", pixel_fps}, @@ -968,6 +985,9 @@ private: {"paint_duration_ns", observer.paint_duration_ns}, {"render_duration_ns", observer.render_duration_ns}, {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, + {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, + {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, + {"consumer_variation_ns", observer.consumer_variation_ns}, {"consumer_interval_ns", observer.consumer_interval_ns}, {"next_refresh_interval_ns", observer.next_refresh_interval_ns}, {"paint_lease_wait_ns", observer.paint_lease_wait_ns}, @@ -1222,8 +1242,14 @@ private: void apply_state() { plot_.set_background_color(color_from_hex(string_value(state_, "background_color"))); - if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) - plot_.set_max_render_fps(number_value(state_, "max_render_fps")); + if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { + if (bool_value(state_, "frequency_limit_enabled")) + plot_.set_max_render_fps(number_value(state_, "max_render_fps")); + else + plot_.clear_max_render_fps(); + if (!bool_value(state_, "consumer_feedback_enabled")) + plot_.clear_consumer_feedback(); + } set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); set_performance_overlay_enabled(plot_, bool_value(state_, "performance_overlay")); if (primary_) { diff --git a/web_server/Gallery_Protocol.cpp b/web_server/Gallery_Protocol.cpp index 93df4d8..58e138f 100644 --- a/web_server/Gallery_Protocol.cpp +++ b/web_server/Gallery_Protocol.cpp @@ -123,9 +123,15 @@ void append_common(std::vector& result, Gallery_Frame_Mode f result.push_back(color("background_color", "背景颜色", "Plot_Core::set_background_color", "#07111f", "Plot_Core")); if (frame_mode == Gallery_Frame_Mode::Low_Latency) { + result.push_back(flag("frequency_limit_enabled", "启用频率上限", + "Plot_Core::set_max_render_fps / clear_max_render_fps", true, + "Low_Latency_Strategy", "关闭后用户频率不参与调度。")); result.push_back(number("max_render_fps", "最大渲染 FPS", "Plot_Core::set_max_render_fps", 30, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false, - "Kernel 接受任意有限正频率;高目标用于触发并观察 Painter 或后台 Render 瓶颈。")); + "仅在启用频率上限时参与调度。")); + result.push_back(flag("consumer_feedback_enabled", "启用消费者反馈", + "Plot_Core::set_consumer_feedback / clear_consumer_feedback", true, + "Low_Latency_Strategy", "关闭后消费者反馈不参与调度。")); } result.push_back(flag("performance_overlay", "性能叠层", "set_performance_overlay_enabled", false, "Plot_Core")); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 0ba80aa..09678b4 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -849,12 +849,14 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers EXPECT_GT(limit.at("bottleneck_duration_ns").get(), 1U); EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("paint_lease_wait_ns")); EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("render_finish_state_wait_ns")); - constexpr std::array observer_fields{ + constexpr std::array observer_fields{ "mode", "last_event", "limit_state", "configured_frequency_hz", "observation_count", "produced_frame_count", "consumed_frame_count", "dropped_frame_count", "failed_operation_count", "pending_frame_count", "latest_sequence", - "paint_duration_ns", "render_duration_ns", "target_interval_ns", - "bottleneck_duration_ns", "consumer_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_ns", + "paint_duration_ns", "render_duration_ns", "target_interval_ns", "frequency_limit_enabled", + "consumer_feedback_enabled", "bottleneck_duration_ns", "consumer_sample_interval_ns", + "consumer_smoothed_interval_ns", "consumer_variation_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" @@ -973,7 +975,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres observer.at("limit_state") == "consumer_limited"); } -TEST(RenderiveWebGallery, WebRuntimeMetricsPublishGenericConsumerFeedbackWithoutTransportFpsCap) { +TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedbackWithoutTransportFpsCap) { Gallery_Plot_Session session(true); ASSERT_EQ(response_json(session.handle(gallery_request( Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), @@ -984,26 +986,36 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishGenericConsumerFeedbackWithout "case_state"); auto observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":20,"presentation_fps":20,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":500,"display_interval_ms":500,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + R"({"category":"event","type":"gallery_observe","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":2,"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"); - EXPECT_EQ(observer.at("configured_frequency_hz"), 1'000'000'000.0); - EXPECT_EQ(observer.at("target_interval_ns"), 1); - EXPECT_EQ(observer.at("consumer_interval_ns"), 500'000'000); - EXPECT_EQ(observer.at("next_refresh_interval_ns"), 500'000'000); + 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); + EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 20'000'000); + EXPECT_EQ(observer.at("consumer_variation_ns"), 0); + 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"), "web_runtime"); - EXPECT_FALSE(limit.contains("web_pixel_stream_fps")); observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":2,"display_interval_ms":20,"overwritten_pixel_frames":1,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + R"({"category":"event","type":"gallery_observe","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"); - EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000); + EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000); + EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 20'250'000); + EXPECT_EQ(observer.at("consumer_variation_ns"), 500'000); + EXPECT_EQ(observer.at("consumer_interval_ns"), 22'250'000); observed = response_json(session.handle(gallery_request( - Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":120,"websocket_buffered_bytes":0,"changed_pixel_frames":30,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":2,"display_interval_ms":8,"overwritten_pixel_frames":1,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + Gallery_Request_Kind::Patch, + R"({"category":"event","type":"gallery_patch","patch":{"consumer_feedback_enabled":false,"frequency_limit_enabled":false}})"))); observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_interval_ns"), 8'000'000); + EXPECT_FALSE(observer.at("frequency_limit_enabled").get()); + EXPECT_FALSE(observer.at("consumer_feedback_enabled").get()); + EXPECT_EQ(observer.at("target_interval_ns"), 0); + EXPECT_EQ(observer.at("consumer_interval_ns"), 0); + EXPECT_TRUE(observer.at("limit_state") == "paint_limited" || + observer.at("limit_state") == "render_limited" || + observer.at("limit_state") == "unlimited"); const auto rejected = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, R"({"category":"event","type":"gallery_patch","patch":{"pixel_stream_fps":30}})"))); diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 8bfe833..69de45e 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -228,7 +228,7 @@ class GalleryCard { 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: "消费者反馈受限", not_applicable: "N/A"}; + 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); @@ -246,20 +246,21 @@ class GalleryCard { this.node.querySelector(".perf-pixel-age").textContent = this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—"; this.updateObserverDashboard(observer); this.updateMotionStatus(); - const setLimitFlag = (selector, active, duration) => { + const setLimitFlag = (selector, enabled, active, duration) => { const flag = this.node.querySelector(selector); flag.dataset.active = String(Boolean(active)); - flag.querySelector("b").textContent = `${active ? "受限" : "未受限"} · ${formatNanoseconds(duration)}`; + flag.querySelector("b").textContent = enabled ? `${active ? "受限" : "未受限"} · ${formatNanoseconds(duration)}` : "已关闭"; }; - setLimitFlag(".limit-frequency", limit.current === "frequency_limited", observer.target_interval_ns); - setLimitFlag(".limit-paint", limit.current === "paint_limited", observer.paint_duration_ns); - setLimitFlag(".limit-render", limit.current === "render_limited", observer.render_duration_ns); - setLimitFlag(".limit-consumer", limit.current === "consumer_limited", observer.consumer_interval_ns); + 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_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_ns", + "bottleneck_duration_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns", + "consumer_variation_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" @@ -267,7 +268,7 @@ class GalleryCard { for (const [name, field] of this.observerFields) { const raw = observer[name] ?? (name === "limit_state" ? "not_applicable" : name === "last_event" ? "none" : 0); field.textContent = nanosecondFields.has(name) ? formatNanoseconds(raw) : - name === "configured_frequency_hz" ? `${Number(raw || 0).toLocaleString()} Hz` : + 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); } @@ -327,8 +328,7 @@ class GalleryCard { this.lastPixelSignature = signature; this.lastPixelReceivedAt = now; if (this.frameRequestStartedAt > 0) { - const sample = Math.max(0, now - this.frameRequestStartedAt); - this.frameRoundTripMs = this.frameRoundTripMs > 0 ? this.frameRoundTripMs * 0.8 + sample * 0.2 : sample; + this.frameRoundTripMs = Math.max(0, now - this.frameRequestStartedAt); this.frameRequestStartedAt = 0; } if (this.latestPixelBuffer !== null) this.overwrittenPixelFrames++; @@ -590,8 +590,7 @@ function connectCatalog() { } function loop(time) { if (lastAnimationFrameAt > 0) { - const sample = Math.max(0, time - lastAnimationFrameAt); - displayIntervalMs = displayIntervalMs > 0 ? displayIntervalMs * 0.8 + sample * 0.2 : sample; + displayIntervalMs = Math.max(0, time - lastAnimationFrameAt); } lastAnimationFrameAt = time; const page = pages.get(activeMode); diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index 34012f0..3c85ff3 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -121,6 +121,9 @@
PaintEvent0 ns
后台渲染0 ns
内部瓶颈0 ns
+
消费原始样本0 ns
+
消费 SRTT0 ns
+
消费 RTTVAR0 ns
消费者反馈0 ns
下次刷新0 ns
端到端延迟0 ns