diff --git a/common/filter_simple.py b/common/filter_simple.py index 9ea6fe3070..8a7105063d 100644 --- a/common/filter_simple.py +++ b/common/filter_simple.py @@ -1,16 +1,21 @@ class FirstOrderFilter: def __init__(self, x0, rc, dt, initialized=True): self.x = x0 - self.dt = dt + self._dt = dt self.update_alpha(rc) self.initialized = initialized + def update_dt(self, dt): + self._dt = dt + self.update_alpha(self._rc) + def update_alpha(self, rc): - self.alpha = self.dt / (rc + self.dt) + self._rc = rc + self._alpha = self._dt / (self._rc + self._dt) def update(self, x): if self.initialized: - self.x = (1. - self.alpha) * self.x + self.alpha * x + self.x = (1. - self._alpha) * self.x + self._alpha * x else: self.initialized = True self.x = x diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 777d2f4c3f..0a8fadea90 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -1,6 +1,7 @@ import pyray as rl from enum import IntEnum import cereal.messaging as messaging +from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH from openpilot.selfdrive.ui.layouts.home import HomeLayout from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType @@ -9,6 +10,10 @@ from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets import Widget +ONROAD_FPS = 20 +OFFROAD_FPS = 60 + + class MainState(IntEnum): HOME = 0 SETTINGS = 1 @@ -25,6 +30,8 @@ class MainLayout(Widget): self._current_mode = MainState.HOME self._prev_onroad = False + gui_app.set_target_fps(OFFROAD_FPS) + # Initialize layouts self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()} @@ -74,6 +81,9 @@ class MainLayout(Widget): self._current_mode = layout self._layouts[self._current_mode].show_event() + # No need to draw onroad faster than source (model at 20Hz) and prevents screen tearing + gui_app.set_target_fps(ONROAD_FPS if self._current_mode == MainState.ONROAD else OFFROAD_FPS) + def open_settings(self, panel_type: PanelType): self._layouts[MainState.SETTINGS].set_current_panel(panel_type) self._set_current_layout(MainState.SETTINGS) diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index d84e16cb54..4c036873d0 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -7,7 +7,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import DEFAULT_FPS +from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon from openpilot.system.ui.widgets import Widget @@ -48,7 +48,7 @@ class ModelRenderer(Widget): super().__init__() self._longitudinal_control = False self._experimental_mode = False - self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / DEFAULT_FPS) + self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps) self._prev_allow_throttle = True self._lane_line_probs = np.zeros(4, dtype=np.float32) self._road_edge_stds = np.zeros(2, dtype=np.float32) @@ -277,6 +277,7 @@ class ModelRenderer(Widget): return allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control + self._blend_filter.update_dt(1 / gui_app.target_fps) self._blend_filter.update(int(allow_throttle)) if self._experimental_mode: diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 13532620cb..39a65a9191 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -9,7 +9,6 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params, UnknownKeyName from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState -from openpilot.system.ui.lib.application import DEFAULT_FPS from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app @@ -153,7 +152,7 @@ class Device: self._offroad_brightness: int = BACKLIGHT_OFFROAD self._last_brightness: int = 0 - self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / DEFAULT_FPS) + self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / gui_app.target_fps) self._brightness_thread: threading.Thread | None = None def reset_interactive_timeout(self, timeout: int = -1) -> None: @@ -190,6 +189,7 @@ class Device: clipped_brightness = float(np.clip(100 * clipped_brightness, 10, 100)) + self._brightness_filter.update_dt(1 / gui_app.target_fps) brightness = round(self._brightness_filter.update(clipped_brightness)) if not self._awake: brightness = 0 diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 132d78e5a2..481fd98045 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -11,10 +11,10 @@ from enum import StrEnum from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE, PC, TICI +from openpilot.system.hardware import HARDWARE, PC from openpilot.common.realtime import Ratekeeper -DEFAULT_FPS = int(os.getenv("FPS", 20 if TICI else 60)) +_DEFAULT_FPS = int(os.getenv("FPS", "60")) FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions @@ -130,7 +130,7 @@ class GuiApplication: self._scaled_height = int(self._height * self._scale) self._render_texture: rl.RenderTexture | None = None self._textures: dict[str, rl.Texture] = {} - self._target_fps: int = DEFAULT_FPS + self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() self._window_close_requested = False self._trace_log_callback = None @@ -145,7 +145,7 @@ class GuiApplication: def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = DEFAULT_FPS): + def init_window(self, title: str, fps: int = _DEFAULT_FPS): atexit.register(self.close) # Automatically call close() on exit HARDWARE.set_display_power(True) @@ -164,15 +164,22 @@ class GuiApplication: rl.set_mouse_scale(1 / self._scale, 1 / self._scale) self._render_texture = rl.load_render_texture(self._width, self._height) rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) - rl.set_target_fps(fps) - self._target_fps = fps + self.set_target_fps(fps) self._set_styles() self._load_fonts() if not PC: self._mouse.start() + @property + def target_fps(self): + return self._target_fps + + def set_target_fps(self, fps: int): + self._target_fps = fps + rl.set_target_fps(fps) + def set_modal_overlay(self, overlay, callback: Callable | None = None): self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 119901da40..986d7158de 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -5,7 +5,7 @@ from typing import cast import pyray as rl from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app, DEFAULT_FPS +from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType from openpilot.system.ui.widgets import Widget @@ -50,10 +50,12 @@ class NavButton(Widget): super().__init__() self.text = text self.set_rect(rl.Rectangle(0, 0, 400, 100)) - self._x_pos_filter = FirstOrderFilter(0.0, 0.05, 1 / DEFAULT_FPS, initialized=False) - self._y_pos_filter = FirstOrderFilter(0.0, 0.05, 1 / DEFAULT_FPS, initialized=False) + self._x_pos_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps, initialized=False) + self._y_pos_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps, initialized=False) def set_position(self, x: float, y: float) -> None: + self._x_pos_filter.update_dt(1 / gui_app.target_fps) + self._y_pos_filter.update_dt(1 / gui_app.target_fps) x = self._x_pos_filter.update(x) y = self._y_pos_filter.update(y) changed = (self._rect.x != x or self._rect.y != y)