From 87443cd34d8db902a5e665b9ef679f09899a1c1a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 14 Oct 2025 00:45:03 -0700 Subject: [PATCH] raylib: background onboarding texture loading (#36343) * this seems best so far * better * clean up * debug * debug * clean up * final --- selfdrive/ui/layouts/onboarding.py | 40 +++++++++++++++++++++--------- system/ui/lib/application.py | 12 ++++++--- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/layouts/onboarding.py b/selfdrive/ui/layouts/onboarding.py index bccb36712c..f4ab1a05c6 100644 --- a/selfdrive/ui/layouts/onboarding.py +++ b/selfdrive/ui/layouts/onboarding.py @@ -1,5 +1,6 @@ import os import re +import threading from enum import IntEnum import pyray as rl @@ -38,15 +39,24 @@ class TrainingGuide(Widget): self._completed_callback = completed_callback self._step = 0 - self._load_images() + self._load_image_paths() - def _load_images(self): - self._images = [] + # Load first image now so we show something immediately + self._textures = [gui_app.texture(self._image_paths[0])] + self._image_objs = [] + + threading.Thread(target=self._preload_thread, daemon=True).start() + + def _load_image_paths(self): paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)] paths = sorted(paths, key=lambda x: int(re.search(r'\d+', x).group())) - for fn in paths: - path = os.path.join(BASEDIR, "selfdrive/assets/training", fn) - self._images.append(gui_app.texture(path)) + self._image_paths = [os.path.join(BASEDIR, "selfdrive/assets/training", fn) for fn in paths] + + def _preload_thread(self): + # PNG loading is slow in raylib, so we preload in a thread and upload to GPU in main thread + # We've already loaded the first image on init + for path in self._image_paths[1:]: + self._image_objs.append(gui_app._load_image_from_path(path)) def _handle_mouse_release(self, mouse_pos): if rl.check_collision_point_rec(mouse_pos, STEP_RECTS[self._step]): @@ -57,30 +67,36 @@ class TrainingGuide(Widget): ui_state.params.put_bool("RecordFront", yes) # Restart training? - elif self._step == len(self._images) - 1: + elif self._step == len(self._image_paths) - 1: if rl.check_collision_point_rec(mouse_pos, RESTART_TRAINING_RECT): self._step = -1 self._step += 1 # Finished? - if self._step >= len(self._images): + if self._step >= len(self._image_paths): self._step = 0 if self._completed_callback: self._completed_callback() + def _update_state(self): + if len(self._image_objs): + self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0))) + def _render(self, _): - rl.draw_texture(self._images[self._step], 0, 0, rl.WHITE) + # Safeguard against fast tapping + step = min(self._step, len(self._textures) - 1) + rl.draw_texture(self._textures[step], 0, 0, rl.WHITE) # progress bar - if 0 < self._step < len(STEP_RECTS) - 1: + if 0 < step < len(STEP_RECTS) - 1: h = 20 - w = int((self._step / (len(STEP_RECTS) - 1)) * self._rect.width) + w = int((step / (len(STEP_RECTS) - 1)) * self._rect.width) rl.draw_rectangle(int(self._rect.x), int(self._rect.y + self._rect.height - h), w, h, rl.Color(70, 91, 234, 255)) if DEBUG: - rl.draw_rectangle_lines_ex(STEP_RECTS[self._step], 3, rl.RED) + rl.draw_rectangle_lines_ex(STEP_RECTS[step], 3, rl.RED) return -1 diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 1925122d0b..4a62c996c7 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -196,13 +196,14 @@ class GuiApplication: return self._textures[cache_key] with as_file(ASSETS_DIR.joinpath(asset_path)) as fspath: - texture_obj = self._load_texture_from_image(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + image_obj = self._load_image_from_path(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + texture_obj = self._load_texture_from_image(image_obj) self._textures[cache_key] = texture_obj return texture_obj - def _load_texture_from_image(self, image_path: str, width: int | None = None, height: int | None = None, - alpha_premultiply=False, keep_aspect_ratio=True): - """Load and resize a texture, storing it for later automatic unloading.""" + def _load_image_from_path(self, image_path: str, width: int | None = None, height: int | None = None, + alpha_premultiply: bool = False, keep_aspect_ratio: bool = True) -> rl.Image: + """Load and resize an image, storing it for later automatic unloading.""" image = rl.load_image(image_path) if alpha_premultiply: @@ -225,7 +226,10 @@ class GuiApplication: rl.image_resize(image, new_width, new_height) else: rl.image_resize(image, width, height) + return image + def _load_texture_from_image(self, image: rl.Image) -> rl.Texture: + """Send image to GPU and unload original image.""" texture = rl.load_texture_from_image(image) # Set texture filtering to smooth the result rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)