import atexit import os import time import pyray as rl from enum import IntEnum from openpilot.common.basedir import BASEDIR from openpilot.common.swaglog import cloudlog DEFAULT_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 DEBUG_FPS = os.getenv("DEBUG_FPS") == '1' STRICT_MODE = os.getenv("STRICT_MODE") == '1' DEFAULT_TEXT_SIZE = 60 FONT_DIR = os.path.join(BASEDIR, "selfdrive/assets/fonts") class FontWeight(IntEnum): BLACK = 0 BOLD = 1 EXTRA_BOLD = 2 EXTRA_LIGHT = 3 MEDIUM = 4 NORMAL = 5 SEMI_BOLD = 6 THIN = 7 class GuiApplication: def __init__(self, width: int, height: int): self._fonts: dict[FontWeight, rl.Font] = {} self._width = width self._height = height self._textures: list[rl.Texture] = [] self._target_fps: int = DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() def init_window(self, title: str, fps: int=DEFAULT_FPS): atexit.register(self.close) # Automatically call close() on exit rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT | rl.ConfigFlags.FLAG_VSYNC_HINT) rl.init_window(self._width, self._height, title) rl.set_target_fps(fps) self._target_fps = fps self._set_styles() self._load_fonts() def load_texture_from_image(self, file_name: str, width: int, height: int): """Load and resize a texture, storing it for later automatic unloading.""" image = rl.load_image(file_name) rl.image_resize(image, width, height) texture = rl.load_texture_from_image(image) # Set texture filtering to smooth the result rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) rl.unload_image(image) self._textures.append(texture) return texture def close(self): for texture in self._textures: rl.unload_texture(texture) for font in self._fonts.values(): rl.unload_font(font) rl.close_window() def render(self): while not rl.window_should_close(): rl.begin_drawing() rl.clear_background(rl.BLACK) yield if DEBUG_FPS: rl.draw_fps(10, 10) rl.end_drawing() self._monitor_fps() def font(self, font_wight: FontWeight=FontWeight.NORMAL): return self._fonts[font_wight] @property def width(self): return self._width @property def height(self): return self._height def _load_fonts(self): font_files = ( "Inter-Black.ttf", "Inter-Bold.ttf", "Inter-ExtraBold.ttf", "Inter-ExtraLight.ttf", "Inter-Medium.ttf", "Inter-Regular.ttf", "Inter-SemiBold.ttf", "Inter-Thin.ttf" ) for index, font_file in enumerate(font_files): font = rl.load_font_ex(os.path.join(FONT_DIR, font_file), 120, None, 0) rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) self._fonts[index] = font rl.gui_set_font(self._fonts[FontWeight.NORMAL]) def _set_styles(self): rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK)) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(rl.Color(200, 200, 200, 255))) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.Color(30, 30, 30, 255))) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) def _monitor_fps(self): fps = rl.get_fps() # Log FPS drop below threshold at regular intervals if fps < self._target_fps * FPS_DROP_THRESHOLD: current_time = time.monotonic() if current_time - self._last_fps_log_time >= FPS_LOG_INTERVAL: cloudlog.warning(f"FPS dropped below {self._target_fps}: {fps}") self._last_fps_log_time = current_time # Strict mode: terminate UI if FPS drops too much if STRICT_MODE and fps < self._target_fps * FPS_CRITICAL_THRESHOLD: cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.") os._exit(1) gui_app = GuiApplication(2160, 1080)