mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-22 09:42:10 +08:00
yas
This commit is contained in:
@@ -179,7 +179,12 @@ class HomeLayout(Widget):
|
||||
|
||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||
version_text_width, self.header_rect.height)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
version_font_size = 48
|
||||
version_text_size = measure_text_cached(font, self._version_text, version_font_size)
|
||||
if version_text_size.x > version_rect.width:
|
||||
version_font_size = max(32, int(version_font_size * version_rect.width / version_text_size.x))
|
||||
gui_label(version_rect, self._version_text, version_font_size, rl.WHITE, font_weight=FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
@@ -233,6 +238,17 @@ class HomeLayout(Widget):
|
||||
self._prev_alerts_present = alerts_present
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "openpilot"
|
||||
brand = "StarPilot"
|
||||
description = starpilot_display_description(self.params.get("UpdaterCurrentDescription"))
|
||||
return f"{brand} {description}" if description else brand
|
||||
version_text = f"{brand} {description}" if description else brand
|
||||
|
||||
model_name = self.params.get("DrivingModelName", encoding="utf-8") or self.params.get_default_value("DrivingModelName")
|
||||
if isinstance(model_name, bytes):
|
||||
model_name = model_name.decode("utf-8", errors="ignore")
|
||||
model_name = str(model_name or "").replace("_default", "").replace("(Default)", "").strip()
|
||||
|
||||
if not model_name:
|
||||
model_name = (self.params.get("Model", encoding="utf-8") or
|
||||
self.params.get("DrivingModel", encoding="utf-8") or "").strip()
|
||||
|
||||
return f"{version_text} - {model_name}" if model_name else version_text
|
||||
|
||||
@@ -140,10 +140,7 @@ class CameraView(Widget):
|
||||
|
||||
# Initialize EGL for zero-copy rendering when available.
|
||||
if self._use_egl:
|
||||
# Create a 1x1 pixel placeholder texture for EGL image binding
|
||||
temp_image = rl.gen_image_color(1, 1, rl.BLACK)
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
rl.unload_image(temp_image)
|
||||
self._create_egl_texture()
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
|
||||
@@ -206,11 +203,6 @@ class CameraView(Widget):
|
||||
self._offroad_transition_callback = None
|
||||
self._clear_textures()
|
||||
|
||||
# Clean up EGL texture
|
||||
if self.egl_texture:
|
||||
rl.unload_texture(self.egl_texture)
|
||||
self.egl_texture = None
|
||||
|
||||
# Clean up shader
|
||||
if self.shader and self.shader.id:
|
||||
rl.unload_shader(self.shader)
|
||||
@@ -360,6 +352,9 @@ class CameraView(Widget):
|
||||
return False
|
||||
self.last_connection_attempt = current_time
|
||||
|
||||
# A GL texture can retain the last EGL image after camerad exits. Release
|
||||
# it before connect() frees and replaces the client's imported buffers.
|
||||
self._clear_textures()
|
||||
if not self.client.connect(False) or not self.client.num_buffers:
|
||||
return False
|
||||
|
||||
@@ -390,9 +385,9 @@ class CameraView(Widget):
|
||||
def _complete_switch(self) -> None:
|
||||
"""Instantly switch to target stream."""
|
||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||
# Clean up current resources
|
||||
if self.client:
|
||||
del self.client
|
||||
# Delete the GL texture before releasing the old client. Merely destroying
|
||||
# the EGLImage handle leaves its storage alive while a texture sibling exists.
|
||||
self._clear_textures()
|
||||
|
||||
# Switch to target
|
||||
self.client = self._target_client
|
||||
@@ -409,12 +404,20 @@ class CameraView(Widget):
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not self._use_egl:
|
||||
if self._use_egl:
|
||||
self._create_egl_texture()
|
||||
else:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
|
||||
def _create_egl_texture(self):
|
||||
# A fresh texture has no EGL image sibling from a previous camera client.
|
||||
temp_image = rl.gen_image_color(1, 1, rl.BLACK)
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
rl.unload_image(temp_image)
|
||||
|
||||
def _clear_textures(self):
|
||||
if self.texture_y and self.texture_y.id:
|
||||
rl.unload_texture(self.texture_y)
|
||||
@@ -424,8 +427,13 @@ class CameraView(Widget):
|
||||
rl.unload_texture(self.texture_uv)
|
||||
self.texture_uv = None
|
||||
|
||||
# Clean up EGL resources
|
||||
# Delete the texture first. eglDestroyImageKHR only destroys the EGLImage
|
||||
# handle; the image storage stays alive while a GL texture sibling exists.
|
||||
if self._use_egl:
|
||||
if self.egl_texture and self.egl_texture.id:
|
||||
rl.unload_texture(self.egl_texture)
|
||||
self.egl_texture = None
|
||||
|
||||
for data in self.egl_images.values():
|
||||
destroy_egl_image(data)
|
||||
self.egl_images = {}
|
||||
|
||||
@@ -70,3 +70,54 @@ def test_transition_callback_does_not_retain_camera_view(monkeypatch, module):
|
||||
|
||||
assert view_ref() is None
|
||||
assert callbacks == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module", (mici_cameraview, big_cameraview))
|
||||
def test_stream_switch_releases_graphics_before_old_client(module):
|
||||
events = []
|
||||
|
||||
class FakeClient:
|
||||
pass
|
||||
|
||||
view = module.CameraView.__new__(module.CameraView)
|
||||
view.client = FakeClient()
|
||||
old_client_finalizer = weakref.finalize(view.client, events.append, "client")
|
||||
view._target_client = FakeClient()
|
||||
view._target_stream_type = object()
|
||||
view._stream_type = object()
|
||||
view._switching = True
|
||||
view._texture_needs_update = False
|
||||
view._closed = True
|
||||
view._clear_textures = lambda: events.append("graphics")
|
||||
view._initialize_textures = lambda: events.append("initialize")
|
||||
|
||||
view._complete_switch()
|
||||
gc.collect()
|
||||
|
||||
assert old_client_finalizer.alive is False
|
||||
assert events == ["graphics", "client", "initialize"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module", (mici_cameraview, big_cameraview))
|
||||
def test_egl_cleanup_deletes_texture_before_images(monkeypatch, module):
|
||||
events = []
|
||||
view = module.CameraView.__new__(module.CameraView)
|
||||
view.texture_y = None
|
||||
view.texture_uv = None
|
||||
view.egl_texture = SimpleNamespace(id=7)
|
||||
view.egl_images = {0: object(), 1: object()}
|
||||
view._closed = True
|
||||
|
||||
if module is mici_cameraview:
|
||||
view._use_egl = True
|
||||
else:
|
||||
monkeypatch.setattr(module, "TICI", True)
|
||||
|
||||
monkeypatch.setattr(module.rl, "unload_texture", lambda _texture: events.append("texture"))
|
||||
monkeypatch.setattr(module, "destroy_egl_image", lambda _image: events.append("image"))
|
||||
|
||||
view._clear_textures()
|
||||
|
||||
assert events == ["texture", "image", "image"]
|
||||
assert view.egl_texture is None
|
||||
assert view.egl_images == {}
|
||||
|
||||
@@ -100,11 +100,7 @@ class CameraView(Widget):
|
||||
if TICI:
|
||||
if not init_egl():
|
||||
raise RuntimeError("Failed to initialize EGL")
|
||||
|
||||
# Create a 1x1 pixel placeholder texture for EGL image binding
|
||||
temp_image = rl.gen_image_color(1, 1, rl.BLACK)
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
rl.unload_image(temp_image)
|
||||
self._create_egl_texture()
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
|
||||
@@ -167,11 +163,6 @@ class CameraView(Widget):
|
||||
self._offroad_transition_callback = None
|
||||
self._clear_textures()
|
||||
|
||||
# Clean up EGL texture
|
||||
if TICI and self.egl_texture:
|
||||
rl.unload_texture(self.egl_texture)
|
||||
self.egl_texture = None
|
||||
|
||||
# Clean up shader
|
||||
if self.shader and self.shader.id:
|
||||
rl.unload_shader(self.shader)
|
||||
@@ -311,6 +302,9 @@ class CameraView(Widget):
|
||||
return False
|
||||
self.last_connection_attempt = current_time
|
||||
|
||||
# A GL texture can retain the last EGL image after camerad exits. Release
|
||||
# it before connect() frees and replaces the client's imported buffers.
|
||||
self._clear_textures()
|
||||
if not self.client.connect(False) or not self.client.num_buffers:
|
||||
return False
|
||||
|
||||
@@ -341,9 +335,9 @@ class CameraView(Widget):
|
||||
def _complete_switch(self) -> None:
|
||||
"""Instantly switch to target stream."""
|
||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||
# Clean up current resources
|
||||
if self.client:
|
||||
del self.client
|
||||
# Delete the GL texture before releasing the old client. Merely destroying
|
||||
# the EGLImage handle leaves its storage alive while a texture sibling exists.
|
||||
self._clear_textures()
|
||||
|
||||
# Switch to target
|
||||
self.client = self._target_client
|
||||
@@ -360,12 +354,20 @@ class CameraView(Widget):
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not TICI:
|
||||
if TICI:
|
||||
self._create_egl_texture()
|
||||
else:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
|
||||
def _create_egl_texture(self):
|
||||
# A fresh texture has no EGL image sibling from a previous camera client.
|
||||
temp_image = rl.gen_image_color(1, 1, rl.BLACK)
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
rl.unload_image(temp_image)
|
||||
|
||||
def _clear_textures(self):
|
||||
if self.texture_y and self.texture_y.id:
|
||||
rl.unload_texture(self.texture_y)
|
||||
@@ -375,8 +377,13 @@ class CameraView(Widget):
|
||||
rl.unload_texture(self.texture_uv)
|
||||
self.texture_uv = None
|
||||
|
||||
# Clean up EGL resources
|
||||
# Delete the texture first. eglDestroyImageKHR only destroys the EGLImage
|
||||
# handle; the image storage stays alive while a GL texture sibling exists.
|
||||
if TICI:
|
||||
if self.egl_texture and self.egl_texture.id:
|
||||
rl.unload_texture(self.egl_texture)
|
||||
self.egl_texture = None
|
||||
|
||||
for data in self.egl_images.values():
|
||||
destroy_egl_image(data)
|
||||
self.egl_images = {}
|
||||
|
||||
@@ -2,6 +2,7 @@ import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.starpilot.common.experimental_state import requested_experimental_mode
|
||||
@@ -17,12 +18,23 @@ class ExperimentalModeButton(Widget):
|
||||
|
||||
self.params = Params()
|
||||
self.experimental_mode = requested_experimental_mode(self.params, ui_state.params_memory)
|
||||
self.conditional_mode = self._get_conditional_mode()
|
||||
|
||||
self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width)
|
||||
self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width)
|
||||
|
||||
def show_event(self):
|
||||
self.experimental_mode = requested_experimental_mode(self.params, ui_state.params_memory)
|
||||
self.conditional_mode = self._get_conditional_mode()
|
||||
|
||||
def _get_conditional_mode(self):
|
||||
if self.params.get_bool("SafeMode"):
|
||||
return None
|
||||
if self.params.get_bool("ConditionalExperimental"):
|
||||
return "experimental"
|
||||
if self.params.get_bool("ConditionalChill"):
|
||||
return "chill"
|
||||
return None
|
||||
|
||||
def _get_gradient_colors(self):
|
||||
alpha = 0xCC if self.is_pressed else 0xFF
|
||||
@@ -33,6 +45,40 @@ class ExperimentalModeButton(Widget):
|
||||
return rl.Color(20, 255, 171, alpha), rl.Color(35, 149, 255, alpha)
|
||||
|
||||
def _draw_gradient_background(self, rect):
|
||||
if self.conditional_mode:
|
||||
alpha = 0xCC if self.is_pressed else 0xFF
|
||||
blue = rl.Color(35, 149, 255, alpha)
|
||||
mint = rl.Color(20, 255, 171, alpha)
|
||||
orange = rl.Color(255, 138, 22, alpha)
|
||||
red = rl.Color(219, 56, 34, alpha)
|
||||
if self.conditional_mode == "experimental":
|
||||
dominant_start, dominant_end = blue, mint
|
||||
target_start, target_end = orange, red
|
||||
else:
|
||||
dominant_start, dominant_end = orange, red
|
||||
target_start, target_end = blue, mint
|
||||
|
||||
transition_start = int(rect.x + rect.width * 0.58)
|
||||
transition_end = int(rect.x + rect.width * 0.80)
|
||||
right = int(rect.x + rect.width)
|
||||
dominant_width = transition_start - int(rect.x)
|
||||
target_width = right - transition_end
|
||||
target_progress = 0.67
|
||||
target_visible_end = rl.Color(
|
||||
round(target_start.r + (target_end.r - target_start.r) * target_progress),
|
||||
round(target_start.g + (target_end.g - target_start.g) * target_progress),
|
||||
round(target_start.b + (target_end.b - target_start.b) * target_progress),
|
||||
alpha,
|
||||
)
|
||||
|
||||
rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), dominant_width, int(rect.height),
|
||||
dominant_start, dominant_end)
|
||||
rl.draw_rectangle_gradient_h(transition_start, int(rect.y), transition_end - transition_start, int(rect.height),
|
||||
dominant_end, target_start)
|
||||
rl.draw_rectangle_gradient_h(transition_end, int(rect.y), target_width, int(rect.height),
|
||||
target_start, target_visible_end)
|
||||
return
|
||||
|
||||
start_color, end_color = self._get_gradient_colors()
|
||||
rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
|
||||
start_color, end_color)
|
||||
@@ -49,11 +95,23 @@ class ExperimentalModeButton(Widget):
|
||||
rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color)
|
||||
|
||||
# Draw text label (left aligned)
|
||||
text = tr("EXPERIMENTAL MODE ON") if self.experimental_mode else tr("CHILL MODE ON")
|
||||
text_x = rect.x + self.horizontal_padding
|
||||
text_y = rect.y + rect.height / 2 - 45 * FONT_SCALE // 2 # Center vertically
|
||||
if self.conditional_mode == "experimental":
|
||||
text = tr("CONDITIONAL EXPERIMENTAL")
|
||||
elif self.conditional_mode == "chill":
|
||||
text = tr("CONDITIONAL CHILL")
|
||||
else:
|
||||
text = tr("EXPERIMENTAL MODE ON") if self.experimental_mode else tr("CHILL MODE ON")
|
||||
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK)
|
||||
text_x = rect.x + self.horizontal_padding
|
||||
font = gui_app.font(FontWeight.NORMAL)
|
||||
font_size = 45
|
||||
available_width = line_x - text_x - self.horizontal_padding
|
||||
measured_width = measure_text_cached(font, text, font_size).x
|
||||
if measured_width > available_width:
|
||||
font_size = max(32, int(font_size * available_width / measured_width))
|
||||
text_y = rect.y + rect.height / 2 - font_size * FONT_SCALE // 2 # Center vertically
|
||||
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), font_size, 0, rl.BLACK)
|
||||
|
||||
# Draw icon (right aligned)
|
||||
icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width
|
||||
|
||||
@@ -1035,6 +1035,25 @@ class SpeedLimitVisionDaemon:
|
||||
self.stream_type = None
|
||||
self.stream_name = ""
|
||||
|
||||
def _receive_frame_bgr(self):
|
||||
# Keep VisionBuf and its NumPy view inside this short-lived scope. A local
|
||||
# in run() survives loop iterations and can otherwise retain the old
|
||||
# VisionIpcClient, including all imported camera buffers, while offroad.
|
||||
client = self.client
|
||||
if client is None:
|
||||
return None
|
||||
|
||||
buffer = client.recv()
|
||||
if buffer is None:
|
||||
return None
|
||||
|
||||
data = buffer.data
|
||||
if not data.any():
|
||||
return None
|
||||
|
||||
image = np.frombuffer(data, dtype=np.uint8).reshape((len(data) // client.stride, client.stride))
|
||||
return cv2.cvtColor(image[:client.height * 3 // 2, :client.width], cv2.COLOR_YUV2BGR_NV12)
|
||||
|
||||
@staticmethod
|
||||
def _letterbox(image, shape=(640, 640), color=(114, 114, 114)):
|
||||
image_height, image_width = image.shape[:2]
|
||||
@@ -2526,7 +2545,7 @@ class SpeedLimitVisionDaemon:
|
||||
ratekeeper.keep_time()
|
||||
continue
|
||||
|
||||
buffer = self.client.recv() if self.client is not None else None
|
||||
frame_bgr = self._receive_frame_bgr()
|
||||
self.inference_count += 1
|
||||
inference_started_at = time.monotonic()
|
||||
self.last_frame_process_duration_s = 0.0
|
||||
@@ -2534,7 +2553,7 @@ class SpeedLimitVisionDaemon:
|
||||
self.last_detector_forward_duration_s = 0.0
|
||||
self.last_classifier_forward_count = 0
|
||||
self.last_classifier_forward_duration_s = 0.0
|
||||
if buffer is None or not buffer.data.any():
|
||||
if frame_bgr is None:
|
||||
self.empty_frame_count += 1
|
||||
stale_cleared = self._clear_published_detection_if_stale(now, "empty_frame")
|
||||
if self.published_speed_limit_mph > 0 and not stale_cleared:
|
||||
@@ -2548,8 +2567,6 @@ class SpeedLimitVisionDaemon:
|
||||
ratekeeper.keep_time()
|
||||
continue
|
||||
|
||||
image = np.frombuffer(buffer.data, dtype=np.uint8).reshape((len(buffer.data) // self.client.stride, self.client.stride))
|
||||
frame_bgr = cv2.cvtColor(image[:self.client.height * 3 // 2, :self.client.width], cv2.COLOR_YUV2BGR_NV12)
|
||||
self.current_frame_bgr = frame_bgr
|
||||
|
||||
if detector_due:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from collections import deque
|
||||
import gc
|
||||
import weakref
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -63,6 +65,34 @@ def test_disconnect_camera_releases_client_state():
|
||||
assert daemon.stream_name == ""
|
||||
|
||||
|
||||
def test_receive_frame_does_not_retain_vision_buffer(monkeypatch):
|
||||
buffer_refs = []
|
||||
|
||||
class FakeBuffer:
|
||||
def __init__(self):
|
||||
self.data = np.ones(6, dtype=np.uint8)
|
||||
|
||||
class FakeClient:
|
||||
width = 2
|
||||
height = 2
|
||||
stride = 2
|
||||
|
||||
def recv(self):
|
||||
buffer = FakeBuffer()
|
||||
buffer_refs.append(weakref.ref(buffer))
|
||||
return buffer
|
||||
|
||||
daemon = SpeedLimitVisionDaemon.__new__(SpeedLimitVisionDaemon)
|
||||
daemon.client = FakeClient()
|
||||
monkeypatch.setattr(slv.cv2, "cvtColor", lambda image, _conversion: np.array(image, copy=True))
|
||||
|
||||
frame = daemon._receive_frame_bgr()
|
||||
gc.collect()
|
||||
|
||||
assert frame.shape == (3, 2)
|
||||
assert buffer_refs[0]() is None
|
||||
|
||||
|
||||
def test_published_sign_value_uses_configured_units():
|
||||
imperial_daemon = publishing_daemon(False)
|
||||
metric_daemon = publishing_daemon(True)
|
||||
|
||||
Reference in New Issue
Block a user