Skip to content

Commit e6f0a9a

Browse files
committed
move VIPC stream receiving to background thread
1 parent 53b7ade commit e6f0a9a

9 files changed

Lines changed: 207 additions & 514 deletions

File tree

selfdrive/ui/mici/onroad/augmented_road_view.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def _update_calibration(self):
293293
wide_from_device = rot_from_euler(calib.wideFromDeviceEuler)
294294
self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib
295295

296-
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
296+
def _calc_frame_matrix(self, frame_width: int, frame_height: int, rect: rl.Rectangle) -> np.ndarray:
297297
# Get camera configuration
298298
# TODO: cache with vEgo?
299299
calib_time = ui_state.sm.recv_frame['liveCalibration']

selfdrive/ui/mici/onroad/cameraview.py

Lines changed: 86 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
import numpy as np
33
import pyray as rl
44

5-
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
6-
from openpilot.common.swaglog import cloudlog
5+
from msgq.visionipc import VisionStreamType, VisionBuf
6+
from openpilot.selfdrive.ui.mici.onroad.vipc_thread import VisionIpcThread
7+
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
78
from openpilot.system.hardware import TICI
89
from openpilot.system.ui.lib.application import gui_app
910
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
1011
from openpilot.system.ui.widgets import Widget
11-
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
1212

13-
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
1413

1514
VERSION = """
1615
#version 300 es
@@ -104,30 +103,13 @@
104103
"""
105104

106105

107-
class CameraView(Widget):
108-
def __init__(self, name: str, stream_type: VisionStreamType):
106+
class BaseCameraView(Widget):
107+
def __init__(self, name: str, stream_type: VisionStreamType, fragment_shader: str):
109108
super().__init__()
110-
self._name = name
111-
# Primary stream
112-
self.client = VisionIpcClient(name, stream_type, conflate=True)
113109
self._stream_type = stream_type
114-
self.available_streams: list[VisionStreamType] = []
115-
116-
# Target stream for switching
117-
self._target_client: VisionIpcClient | None = None
118-
self._target_stream_type: VisionStreamType | None = None
119-
self._switching: bool = False
120-
121-
self._texture_needs_update = True
122-
self.last_connection_attempt: float = 0.0
123-
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
110+
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, fragment_shader)
124111
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1
125-
self._engaged_loc = rl.get_shader_location(self.shader, "engaged")
126-
self._engaged_val = rl.ffi.new("int[1]", [1])
127-
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver")
128-
self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0])
129112

130-
self.frame: VisionBuf | None = None
131113
self.texture_y: rl.Texture | None = None
132114
self.texture_uv: rl.Texture | None = None
133115

@@ -148,70 +130,57 @@ def __init__(self, name: str, stream_type: VisionStreamType):
148130
rl.unload_image(temp_image)
149131

150132
ui_state.add_offroad_transition_callback(self._offroad_transition)
133+
self._vipc_thread = VisionIpcThread(name, stream_type)
134+
135+
def start(self):
136+
self._vipc_thread.start()
137+
138+
def stop(self):
139+
self._vipc_thread.stop()
151140

152141
def _offroad_transition(self):
153-
# Reconnect if not first time going onroad
154-
if ui_state.is_onroad() and self.frame is not None:
155-
# Prevent old frames from showing when going onroad. Qt has a separate thread
156-
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
157-
# and only clears internal buffers, not the message queue.
158-
self.frame = None
159-
self.available_streams.clear()
160-
if self.client:
161-
del self.client
162-
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
142+
if ui_state.is_offroad():
143+
self.stop()
144+
else:
145+
self.start()
163146

164147
def _set_placeholder_color(self, color: rl.Color):
165148
"""Set a placeholder color to be drawn when no frame is available."""
166149
self._placeholder_color = color
167150

168151
def switch_stream(self, stream_type: VisionStreamType) -> None:
169-
if self._stream_type == stream_type:
170-
return
171-
172-
if self._switching and self._target_stream_type == stream_type:
173-
return
174-
175-
cloudlog.debug(f'Preparing switch from {self._stream_type} to {stream_type}')
176-
177-
if self._target_client:
178-
del self._target_client
179-
180-
self._target_stream_type = stream_type
181-
self._target_client = VisionIpcClient(self._name, stream_type, conflate=True)
182-
self._switching = True
152+
self._vipc_thread.switch_stream(stream_type)
183153

184154
@property
185155
def stream_type(self) -> VisionStreamType:
186-
return self._stream_type
156+
return self._vipc_thread._stream_type
187157

188158
def close(self) -> None:
159+
self._vipc_thread.stop()
189160
self._clear_textures()
190161

191-
# Clean up EGL texture
192162
if TICI and self.egl_texture:
193163
rl.unload_texture(self.egl_texture)
194164
self.egl_texture = None
195165

196-
# Clean up shader
197166
if self.shader and self.shader.id:
198167
rl.unload_shader(self.shader)
199168
self.shader.id = 0
200169

201-
self.frame = None
202-
self.available_streams.clear()
203-
self.client = None
204-
205170
def __del__(self):
206171
self.close()
207172

208-
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
209-
if not self.frame:
173+
@property
174+
def available_streams(self) -> list[VisionStreamType]:
175+
return self._vipc_thread._available_streams
176+
177+
def _calc_frame_matrix(self, frame_width: int, frame_height: int, rect: rl.Rectangle) -> np.ndarray:
178+
if frame_width == 0 or frame_height == 0:
210179
return np.eye(3)
211180

212181
# Calculate aspect ratios
213182
widget_aspect_ratio = rect.width / rect.height
214-
frame_aspect_ratio = self.frame.width / self.frame.height
183+
frame_aspect_ratio = frame_width / frame_height
215184

216185
# Calculate scaling factors to maintain aspect ratio
217186
zx = min(frame_aspect_ratio / widget_aspect_ratio, 1.0)
@@ -224,32 +193,25 @@ def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
224193
])
225194

226195
def _render(self, rect: rl.Rectangle):
227-
if self._switching:
228-
self._handle_switch()
229-
230-
if not self._ensure_connection():
231-
self._draw_placeholder(rect)
232-
return
233-
234-
# Try to get a new buffer without blocking
235-
buffer = self.client.recv(timeout_ms=0)
236-
if buffer:
237-
self._texture_needs_update = True
238-
self.frame = buffer
239-
elif not self.client.is_connected():
240-
# ensure we clear the displayed frame when the connection is lost
241-
self.frame = None
242-
243-
if not self.frame:
244-
self._draw_placeholder(rect)
245-
return
246-
247-
transform = self._calc_frame_matrix(rect)
248-
src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height))
196+
with self._vipc_thread.lock:
197+
frame = self._vipc_thread.get_frame()
198+
if not frame:
199+
self._draw_placeholder(rect)
200+
return
201+
202+
if self._vipc_thread.just_connected():
203+
self._initialize_textures(frame)
204+
205+
self._draw_frame(frame, rect)
206+
207+
def _draw_frame(self, frame: VisionBuf, rect: rl.Rectangle):
208+
src_rect = rl.Rectangle(0, 0, float(frame.width), float(frame.height))
249209
# Flip driver camera horizontally
250210
if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER:
251211
src_rect.width = -src_rect.width
252212

213+
transform = self._calc_frame_matrix(frame.width, frame.height, rect)
214+
253215
# Calculate scale
254216
scale_x = rect.width * transform[0, 0] # zx
255217
scale_y = rect.height * transform[1, 1] # zy
@@ -265,134 +227,67 @@ def _render(self, rect: rl.Rectangle):
265227

266228
# Render with appropriate method
267229
if TICI:
268-
self._render_egl(src_rect, dst_rect)
230+
self._render_egl(frame, src_rect, dst_rect)
269231
else:
270-
self._render_textures(src_rect, dst_rect)
232+
self._render_textures(frame, src_rect, dst_rect)
271233

272234
def _draw_placeholder(self, rect: rl.Rectangle):
273235
if self._placeholder_color:
274236
rl.draw_rectangle_rec(rect, self._placeholder_color)
275237

276-
def _render_egl(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
238+
def _render_egl(self, frame: VisionBuf, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
277239
"""Render using EGL for direct buffer access"""
278-
if self.frame is None or self.egl_texture is None:
279-
return
280-
281-
idx = self.frame.idx
282-
egl_image = self.egl_images.get(idx)
240+
assert self.egl_texture
283241

284242
# Create EGL image if needed
243+
egl_image = self.egl_images.get(frame.idx)
285244
if egl_image is None:
286-
egl_image = create_egl_image(self.frame.width, self.frame.height, self.frame.stride, self.frame.fd, self.frame.uv_offset)
287-
if egl_image:
288-
self.egl_images[idx] = egl_image
289-
else:
245+
egl_image = create_egl_image(frame.width, frame.height, frame.stride, frame.fd, frame.uv_offset)
246+
if not egl_image:
290247
return
248+
self.egl_images[frame.idx] = egl_image
291249

292250
# Update texture dimensions to match current frame
293-
self.egl_texture.width = self.frame.width
294-
self.egl_texture.height = self.frame.height
251+
self.egl_texture.width = frame.width
252+
self.egl_texture.height = frame.height
295253

296254
# Bind the EGL image to our texture
297255
bind_egl_image_to_texture(self.egl_texture.id, egl_image)
298256

299257
# Render with shader
300258
rl.begin_shader_mode(self.shader)
301-
self._update_texture_color_filtering()
259+
self._update_shader_uniforms()
302260
rl.draw_texture_pro(self.egl_texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
303261
rl.end_shader_mode()
304262

305-
def _render_textures(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
263+
def _render_textures(self, frame: VisionBuf, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
306264
"""Render using texture copies"""
307-
if not self.texture_y or not self.texture_uv or self.frame is None:
308-
return
265+
assert self.texture_y and self.texture_uv
309266

310267
# Update textures with new frame data
311-
if self._texture_needs_update:
312-
y_data = self.frame.data[: self.frame.uv_offset]
313-
uv_data = self.frame.data[self.frame.uv_offset:]
314-
315-
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
316-
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
317-
self._texture_needs_update = False
268+
frame_addr = frame.data.ctypes.data
269+
if getattr(self, '_last_frame_addr', None) != frame_addr:
270+
rl.update_texture(self.texture_y, rl.ffi.cast("void *", frame_addr))
271+
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", frame_addr + frame.uv_offset))
272+
self._last_frame_addr = frame_addr
318273

319274
# Render with shader
320275
rl.begin_shader_mode(self.shader)
321-
self._update_texture_color_filtering()
276+
self._update_shader_uniforms()
322277
rl.set_shader_value_texture(self.shader, self._texture1_loc, self.texture_uv)
323278
rl.draw_texture_pro(self.texture_y, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
324279
rl.end_shader_mode()
325280

326-
def _update_texture_color_filtering(self):
327-
self._engaged_val[0] = 1 if ui_state.status != UIStatus.DISENGAGED else 0
328-
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
329-
rl.set_shader_value(self.shader, self._enhance_driver_loc, self._enhance_driver_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
330-
331-
def _ensure_connection(self) -> bool:
332-
if not self.client.is_connected():
333-
self.frame = None
334-
self.available_streams.clear()
335-
336-
# Throttle connection attempts
337-
current_time = rl.get_time()
338-
if current_time - self.last_connection_attempt < CONNECTION_RETRY_INTERVAL:
339-
return False
340-
self.last_connection_attempt = current_time
341-
342-
if not self.client.connect(False) or not self.client.num_buffers:
343-
return False
344-
345-
cloudlog.debug(f"Connected to {self._name} stream: {self._stream_type}, buffers: {self.client.num_buffers}")
346-
self._initialize_textures()
347-
self.available_streams = self.client.available_streams(self._name, block=False)
348-
349-
return True
350-
351-
def _handle_switch(self) -> None:
352-
"""Check if target stream is ready and switch immediately."""
353-
if not self._target_client or not self._switching:
354-
return
355-
356-
# Try to connect target if needed
357-
if not self._target_client.is_connected():
358-
if not self._target_client.connect(False) or not self._target_client.num_buffers:
359-
return
281+
def _update_shader_uniforms(self):
282+
pass
360283

361-
cloudlog.debug(f"Target stream connected: {self._target_stream_type}")
362-
363-
# Check if target has frames ready
364-
target_frame = self._target_client.recv(timeout_ms=0)
365-
if target_frame:
366-
self.frame = target_frame # Update current frame to target frame
367-
self._complete_switch()
368-
369-
def _complete_switch(self) -> None:
370-
"""Instantly switch to target stream."""
371-
cloudlog.debug(f"Switching to {self._target_stream_type}")
372-
# Clean up current resources
373-
if self.client:
374-
del self.client
375-
376-
# Switch to target
377-
self.client = self._target_client
378-
self._stream_type = self._target_stream_type
379-
self._texture_needs_update = True
380-
381-
# Reset state
382-
self._target_client = None
383-
self._target_stream_type = None
384-
self._switching = False
385-
386-
# Initialize textures for new stream
387-
self._initialize_textures()
388-
389-
def _initialize_textures(self):
390-
self._clear_textures()
391-
if not TICI:
392-
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
393-
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
394-
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
395-
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
284+
def _initialize_textures(self, frame: VisionBuf):
285+
self._clear_textures()
286+
if not TICI:
287+
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(frame.stride),
288+
int(frame.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
289+
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(frame.stride // 2),
290+
int(frame.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
396291

397292
def _clear_textures(self):
398293
if self.texture_y and self.texture_y.id:
@@ -403,15 +298,30 @@ def _clear_textures(self):
403298
rl.unload_texture(self.texture_uv)
404299
self.texture_uv = None
405300

406-
# Clean up EGL resources
407301
if TICI:
408302
for data in self.egl_images.values():
409303
destroy_egl_image(data)
410304
self.egl_images = {}
411305

412306

307+
class CameraView(BaseCameraView):
308+
def __init__(self, name: str, stream_type: VisionStreamType):
309+
super().__init__(name, stream_type, FRAME_FRAGMENT_SHADER)
310+
self._engaged_loc = rl.get_shader_location(self.shader, "engaged")
311+
self._engaged_val = rl.ffi.new("int[1]", [1])
312+
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver")
313+
self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0])
314+
315+
def _update_shader_uniforms(self):
316+
"""Update shader uniforms based on UI state."""
317+
self._engaged_val[0] = 1 if ui_state.status != UIStatus.DISENGAGED else 0
318+
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
319+
rl.set_shader_value(self.shader, self._enhance_driver_loc, self._enhance_driver_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
320+
321+
413322
if __name__ == "__main__":
414323
gui_app.init_window("camera view")
415324
road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
325+
road.start()
416326
for _ in gui_app.render():
417327
road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))

0 commit comments

Comments
 (0)