Skip to content

Commit 5e61986

Browse files
committed
move VIPC stream receiving to background thread
1 parent f78bacf commit 5e61986

9 files changed

Lines changed: 205 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: 84 additions & 175 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,31 +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-
# TODO: implement a receiver and connect thread
111-
self._name = name
112-
# Primary stream
113-
self.client = VisionIpcClient(name, stream_type, conflate=True)
114109
self._stream_type = stream_type
115-
self.available_streams: list[VisionStreamType] = []
116-
117-
# Target stream for switching
118-
self._target_client: VisionIpcClient | None = None
119-
self._target_stream_type: VisionStreamType | None = None
120-
self._switching: bool = False
121-
122110
self._texture_needs_update = True
123-
self.last_connection_attempt: float = 0.0
124-
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
111+
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, fragment_shader)
125112
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1
126-
self._engaged_loc = rl.get_shader_location(self.shader, "engaged")
127-
self._engaged_val = rl.ffi.new("int[1]", [1])
128-
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver")
129-
self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0])
130113

131-
self.frame: VisionBuf | None = None
132114
self.texture_y: rl.Texture | None = None
133115
self.texture_uv: rl.Texture | None = None
134116

@@ -149,70 +131,57 @@ def __init__(self, name: str, stream_type: VisionStreamType):
149131
rl.unload_image(temp_image)
150132

151133
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()
152141

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

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

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

185155
@property
186156
def stream_type(self) -> VisionStreamType:
187-
return self._stream_type
157+
return self._vipc_thread._stream_type
188158

189159
def close(self) -> None:
160+
self._vipc_thread.stop()
190161
self._clear_textures()
191162

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

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

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

209-
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
210-
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:
211180
return np.eye(3)
212181

213182
# Calculate aspect ratios
214183
widget_aspect_ratio = rect.width / rect.height
215-
frame_aspect_ratio = self.frame.width / self.frame.height
184+
frame_aspect_ratio = frame_width / frame_height
216185

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

227196
def _render(self, rect: rl.Rectangle):
228-
if self._switching:
229-
self._handle_switch()
230-
231-
if not self._ensure_connection():
232-
self._draw_placeholder(rect)
233-
return
234-
235-
# Try to get a new buffer without blocking
236-
buffer = self.client.recv(timeout_ms=0)
237-
if buffer:
238-
self._texture_needs_update = True
239-
self.frame = buffer
240-
elif not self.client.is_connected():
241-
# ensure we clear the displayed frame when the connection is lost
242-
self.frame = None
243-
244-
if not self.frame:
245-
self._draw_placeholder(rect)
246-
return
247-
248-
transform = self._calc_frame_matrix(rect)
249-
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))
250210
# Flip driver camera horizontally
251211
if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER:
252212
src_rect.width = -src_rect.width
253213

214+
transform = self._calc_frame_matrix(frame.width, frame.height, rect)
215+
254216
# Calculate scale
255217
scale_x = rect.width * transform[0, 0] # zx
256218
scale_y = rect.height * transform[1, 1] # zy
@@ -266,134 +228,66 @@ def _render(self, rect: rl.Rectangle):
266228

267229
# Render with appropriate method
268230
if TICI:
269-
self._render_egl(src_rect, dst_rect)
231+
self._render_egl(frame, src_rect, dst_rect)
270232
else:
271-
self._render_textures(src_rect, dst_rect)
233+
self._render_textures(frame, src_rect, dst_rect)
272234

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

277-
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:
278240
"""Render using EGL for direct buffer access"""
279-
if self.frame is None or self.egl_texture is None:
280-
return
281-
282-
idx = self.frame.idx
283-
egl_image = self.egl_images.get(idx)
241+
assert self.egl_texture
284242

285243
# Create EGL image if needed
244+
egl_image = self.egl_images.get(frame.idx)
286245
if egl_image is None:
287-
egl_image = create_egl_image(self.frame.width, self.frame.height, self.frame.stride, self.frame.fd, self.frame.uv_offset)
288-
if egl_image:
289-
self.egl_images[idx] = egl_image
290-
else:
246+
egl_image = create_egl_image(frame.width, frame.height, frame.stride, frame.fd, frame.uv_offset)
247+
if not egl_image:
291248
return
249+
self.egl_images[frame.idx] = egl_image
292250

293251
# Update texture dimensions to match current frame
294-
self.egl_texture.width = self.frame.width
295-
self.egl_texture.height = self.frame.height
252+
self.egl_texture.width = frame.width
253+
self.egl_texture.height = frame.height
296254

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

300258
# Render with shader
301259
rl.begin_shader_mode(self.shader)
302-
self._update_texture_color_filtering()
260+
self._update_shader_uniforms()
303261
rl.draw_texture_pro(self.egl_texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
304262
rl.end_shader_mode()
305263

306-
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:
307265
"""Render using texture copies"""
308-
if not self.texture_y or not self.texture_uv or self.frame is None:
309-
return
266+
assert self.texture_y and self.texture_uv
310267

311268
# Update textures with new frame data
312269
if self._texture_needs_update:
313-
y_data = self.frame.data[: self.frame.uv_offset]
314-
uv_data = self.frame.data[self.frame.uv_offset:]
315-
316-
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
317-
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
318-
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
319273

320274
# Render with shader
321275
rl.begin_shader_mode(self.shader)
322-
self._update_texture_color_filtering()
276+
self._update_shader_uniforms()
323277
rl.set_shader_value_texture(self.shader, self._texture1_loc, self.texture_uv)
324278
rl.draw_texture_pro(self.texture_y, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
325279
rl.end_shader_mode()
326280

327-
def _update_texture_color_filtering(self):
328-
self._engaged_val[0] = 1 if ui_state.status != UIStatus.DISENGAGED else 0
329-
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
330-
rl.set_shader_value(self.shader, self._enhance_driver_loc, self._enhance_driver_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
331-
332-
def _ensure_connection(self) -> bool:
333-
if not self.client.is_connected():
334-
self.frame = None
335-
self.available_streams.clear()
336-
337-
# Throttle connection attempts
338-
current_time = rl.get_time()
339-
if current_time - self.last_connection_attempt < CONNECTION_RETRY_INTERVAL:
340-
return False
341-
self.last_connection_attempt = current_time
342-
343-
if not self.client.connect(False) or not self.client.num_buffers:
344-
return False
345-
346-
cloudlog.debug(f"Connected to {self._name} stream: {self._stream_type}, buffers: {self.client.num_buffers}")
347-
self._initialize_textures()
348-
self.available_streams = self.client.available_streams(self._name, block=False)
349-
350-
return True
351-
352-
def _handle_switch(self) -> None:
353-
"""Check if target stream is ready and switch immediately."""
354-
if not self._target_client or not self._switching:
355-
return
356-
357-
# Try to connect target if needed
358-
if not self._target_client.is_connected():
359-
if not self._target_client.connect(False) or not self._target_client.num_buffers:
360-
return
361-
362-
cloudlog.debug(f"Target stream connected: {self._target_stream_type}")
363-
364-
# Check if target has frames ready
365-
target_frame = self._target_client.recv(timeout_ms=0)
366-
if target_frame:
367-
self.frame = target_frame # Update current frame to target frame
368-
self._complete_switch()
369-
370-
def _complete_switch(self) -> None:
371-
"""Instantly switch to target stream."""
372-
cloudlog.debug(f"Switching to {self._target_stream_type}")
373-
# Clean up current resources
374-
if self.client:
375-
del self.client
376-
377-
# Switch to target
378-
self.client = self._target_client
379-
self._stream_type = self._target_stream_type
380-
self._texture_needs_update = True
381-
382-
# Reset state
383-
self._target_client = None
384-
self._target_stream_type = None
385-
self._switching = False
386-
387-
# Initialize textures for new stream
388-
self._initialize_textures()
281+
def _update_shader_uniforms(self):
282+
pass
389283

390-
def _initialize_textures(self):
391-
self._clear_textures()
392-
if not TICI:
393-
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
394-
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
395-
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
396-
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))
397291

398292
def _clear_textures(self):
399293
if self.texture_y and self.texture_y.id:
@@ -404,15 +298,30 @@ def _clear_textures(self):
404298
rl.unload_texture(self.texture_uv)
405299
self.texture_uv = None
406300

407-
# Clean up EGL resources
408301
if TICI:
409302
for data in self.egl_images.values():
410303
destroy_egl_image(data)
411304
self.egl_images = {}
412305

413306

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+
414322
if __name__ == "__main__":
415323
gui_app.init_window("camera view")
416324
road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
325+
road.start()
417326
for _ in gui_app.render():
418327
road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))

0 commit comments

Comments
 (0)