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- # 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+
414322if __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