22import numpy as np
33import 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
78from openpilot .system .hardware import TICI
89from openpilot .system .ui .lib .application import gui_app
910from openpilot .system .ui .lib .egl import init_egl , create_egl_image , destroy_egl_image , bind_egl_image_to_texture , EGLImage
1011from 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
1514VERSION = """
1615#version 300 es
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+
413322if __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