You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							146 lines
						
					
					
						
							4.3 KiB
						
					
					
				
			
		
		
	
	
							146 lines
						
					
					
						
							4.3 KiB
						
					
					
				| 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
 | |
| DEFAULT_TEXT_COLOR = rl.Color(200, 200, 200, 255)
 | |
| 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):
 | |
|     if not rl.is_window_ready():
 | |
|       return
 | |
| 
 | |
|     for texture in self._textures:
 | |
|       rl.unload_texture(texture)
 | |
|     self._textures = []
 | |
| 
 | |
|     for font in self._fonts.values():
 | |
|       rl.unload_font(font)
 | |
|     self._fonts = []
 | |
| 
 | |
|     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(DEFAULT_TEXT_COLOR))
 | |
|     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)
 | |
| 
 |