Skip to content

Commit f1ccd93

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

File tree

9 files changed

+205
-512
lines changed

9 files changed

+205
-512
lines changed

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: 84 additions & 174 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,14 @@
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-
121110
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)
111+
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, fragment_shader)
124112
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])
129113

130-
self.frame: VisionBuf | None = None
131114
self.texture_y: rl.Texture | None = None
132115
self.texture_uv: rl.Texture | None = None
133116

@@ -148,70 +131,57 @@ def __init__(self, name: str, stream_type: VisionStreamType):
148131
rl.unload_image(temp_image)
149132

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

152142
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)
143+
if ui_state.is_offroad():
144+
self.stop()
145+
else:
146+
self.start()
163147

164148
def _set_placeholder_color(self, color: rl.Color):
165149
"""Set a placeholder color to be drawn when no frame is available."""
166150
self._placeholder_color = color
167151

168152
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
153+
self._vipc_thread.switch_stream(stream_type)
183154

184155
@property
185156
def stream_type(self) -> VisionStreamType:
186-
return self._stream_type
157+
return self._vipc_thread._stream_type
187158

188159
def close(self) -> None:
160+
self._vipc_thread.stop()
189161
self._clear_textures()
190162

191-
# Clean up EGL texture
192163
if TICI and self.egl_texture:
193164
rl.unload_texture(self.egl_texture)
194165
self.egl_texture = None
195166

196-
# Clean up shader
197167
if self.shader and self.shader.id:
198168
rl.unload_shader(self.shader)
199169
self.shader.id = 0
200170

201-
self.frame = None
202-
self.available_streams.clear()
203-
self.client = None
204-
205171
def __del__(self):
206172
self.close()
207173

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

212182
# Calculate aspect ratios
213183
widget_aspect_ratio = rect.width / rect.height
214-
frame_aspect_ratio = self.frame.width / self.frame.height
184+
frame_aspect_ratio = frame_width / frame_height
215185

216186
# Calculate scaling factors to maintain aspect ratio
217187
zx = min(frame_aspect_ratio / widget_aspect_ratio, 1.0)
@@ -224,32 +194,25 @@ def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
224194
])
225195

226196
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))
197+
with self._vipc_thread.lock:
198+
frame = self._vipc_thread.get_frame()
199+
if not frame:
200+
self._draw_placeholder(rect)
201+
return
202+
203+
if self._vipc_thread.just_connected():
204+
self._initialize_textures(frame)
205+
206+
self._draw_frame(frame, rect)
207+
208+
def _draw_frame(self, frame: VisionBuf, rect: rl.Rectangle):
209+
src_rect = rl.Rectangle(0, 0, float(frame.width), float(frame.height))
249210
# Flip driver camera horizontally
250211
if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER:
251212
src_rect.width = -src_rect.width
252213

214+
transform = self._calc_frame_matrix(frame.width, frame.height, rect)
215+
253216
# Calculate scale
254217
scale_x = rect.width * transform[0, 0] # zx
255218
scale_y = rect.height * transform[1, 1] # zy
@@ -265,134 +228,66 @@ def _render(self, rect: rl.Rectangle):
265228

266229
# Render with appropriate method
267230
if TICI:
268-
self._render_egl(src_rect, dst_rect)
231+
self._render_egl(frame, src_rect, dst_rect)
269232
else:
270-
self._render_textures(src_rect, dst_rect)
233+
self._render_textures(frame, src_rect, dst_rect)
271234

272235
def _draw_placeholder(self, rect: rl.Rectangle):
273236
if self._placeholder_color:
274237
rl.draw_rectangle_rec(rect, self._placeholder_color)
275238

276-
def _render_egl(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
239+
def _render_egl(self, frame: VisionBuf, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
277240
"""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)
241+
assert self.egl_texture
283242

284243
# Create EGL image if needed
244+
egl_image = self.egl_images.get(frame.idx)
285245
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:
246+
egl_image = create_egl_image(frame.width, frame.height, frame.stride, frame.fd, frame.uv_offset)
247+
if not egl_image:
290248
return
249+
self.egl_images[frame.idx] = egl_image
291250

292251
# Update texture dimensions to match current frame
293-
self.egl_texture.width = self.frame.width
294-
self.egl_texture.height = self.frame.height
252+
self.egl_texture.width = frame.width
253+
self.egl_texture.height = frame.height
295254

296255
# Bind the EGL image to our texture
297256
bind_egl_image_to_texture(self.egl_texture.id, egl_image)
298257

299258
# Render with shader
300259
rl.begin_shader_mode(self.shader)
301-
self._update_texture_color_filtering()
260+
self._update_shader_uniforms()
302261
rl.draw_texture_pro(self.egl_texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
303262
rl.end_shader_mode()
304263

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

310268
# Update textures with new frame data
311269
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
270+
rl.update_texture(self.texture_y, rl.ffi.cast("void *", frame.data[: frame.uv_offset].ctypes.data))
271+
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", frame.data[frame.uv_offset:].ctypes.data))
272+
# self._texture_needs_update = False
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
360-
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()
281+
def _update_shader_uniforms(self):
282+
pass
388283

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)