@ -345,9 +345,11 @@ print()
# ── 8. End-to-end SDL rendering benchmark ─────────────────────────────────
#
# This section replicates what the app does frame-by-frame:
# 1. GStreamer appsink (same videoscale GstBin as the app) delivers NV12 at 640×480
# 1. GStreamer appsink (same videoscale GstBin as the app) — width-only NV12
# capsfilter so GStreamer preserves the source DAR when choosing height.
# 2. Python memmoves the mapped buffer into a ctypes array ← timed
# 3. SDL_UpdateNVTexture uploads Y + UV planes ← timed
# 3. SDL_UpdateNVTexture uploads Y + UV planes into a lazily-created
# texture whose dimensions match the actual decoded frame. ← timed
# 4. SDL_RenderCopy blits the texture to the window ← timed
#
# Desync and drops will be visible here because we do real SDL rendering.
@ -363,8 +365,7 @@ elif not test_url:
_warn ( " Skipped — no URL. Provide a URL as the first argument. " )
else :
SDL8_SECONDS = 20 # how long to run
SDL8_SCALE_W = 640
SDL8_SCALE_H = 480
SDL8_SCALE_W = 640 # width fed into capsfilter; height derived from source DAR
try :
import ctypes
@ -387,7 +388,7 @@ else:
window = sdl2 . SDL_CreateWindow (
b " R36S playback test " ,
sdl2 . SDL_WINDOWPOS_UNDEFINED , sdl2 . SDL_WINDOWPOS_UNDEFINED ,
SDL8_SCALE_W , SDL8_SCALE_H ,
SDL8_SCALE_W , SDL8_SCALE_W , # square hint; KMSDRM uses native res anyway
sdl2 . SDL_WINDOW_FULLSCREEN_DESKTOP | sdl2 . SDL_WINDOW_SHOWN ,
)
if not window :
@ -411,17 +412,12 @@ else:
sdl2 . SDL_GetWindowSize ( window , ctypes . byref ( w_actual ) , ctypes . byref ( h_actual ) )
print ( f " SDL window size: { w_actual . value } × { h_actual . value } " )
texture = sdl2 . SDL_CreateTexture (
renderer ,
sdl2 . SDL_PIXELFORMAT_NV12 ,
sdl2 . SDL_TEXTUREACCESS_STREAMING ,
SDL8_SCALE_W , SDL8_SCALE_H ,
)
if not texture :
_fail ( f " SDL_CreateTexture NV12: { sdl2 . SDL_GetError ( ) . decode ( ) } " )
raise RuntimeError ( " SDL texture " )
# Texture is created lazily on the first frame so dimensions match
# the actual GStreamer output (height is AR-derived, not fixed).
texture = None
texture_size = ( 0 , 0 ) # (w, h) of the current texture
_ok ( f " SDL init OK — window { w_actual . value } × { h_actual . value } , NV12 texture { SDL8_SCALE_W } × { SDL8_SCALE_H } " )
_ok ( f " SDL init OK — window { w_actual . value } × { h_actual . value } (texture: lazy init) " )
# ── GStreamer pipeline (mirrors _create_appsink) ─────────────────────
gi . require_version ( " GstVideo " , " 1.0 " )
@ -454,6 +450,8 @@ else:
pass
# Build videoscale GstBin (nearest-neighbour) → capsfilter → appsink.
# Only width is fixed in the capsfilter; GStreamer derives height from
# the source's display aspect ratio (same strategy as _create_appsink).
video_sink8 = appsink8
if _hw_active :
scale8 = Gst . ElementFactory . make ( " videoscale " , " vs8 " )
@ -461,7 +459,7 @@ else:
if scale8 and cfilt8 :
scale8 . set_property ( " method " , 0 ) # nearest-neighbour
cfilt8 . set_property ( " caps " , Gst . Caps . from_string (
f " video/x-raw,format=NV12,width= { SDL8_SCALE_W } ,height= { SDL8_SCALE_H } ") )
f " video/x-raw,format=NV12,width= { SDL8_SCALE_W } " ) )
bin8 = Gst . Bin . new ( " vscale-bin8 " )
bin8 . add ( scale8 ) ; bin8 . add ( cfilt8 ) ; bin8 . add ( appsink8 )
scale8 . link ( cfilt8 ) ; cfilt8 . link ( appsink8 )
@ -470,7 +468,7 @@ else:
gp . set_active ( True )
bin8 . add_pad ( gp )
video_sink8 = bin8
print ( f " [pipeline] videoscale(nearest)→ { SDL8_SCALE_W } × { SDL8_SCALE_H } NV12 bin active " )
print ( f " [pipeline] videoscale(nearest) width= { SDL8_SCALE_W } NV12 bin active (height=AR-derived) " )
else :
appsink8 . set_property ( " caps " , Gst . Caps . from_string (
" video/x-raw,format=NV12;video/x-raw,format=BGRA " ) )
@ -620,9 +618,22 @@ else:
fs . dirty = False
frame_n + = 1
# --- Lazy texture creation / resize ---
if texture_size != ( w8 , h8 ) :
if texture :
sdl2 . SDL_DestroyTexture ( texture )
texture = sdl2 . SDL_CreateTexture (
renderer ,
sdl2 . SDL_PIXELFORMAT_NV12 ,
sdl2 . SDL_TEXTUREACCESS_STREAMING ,
w8 , h8 ,
)
texture_size = ( w8 , h8 )
print ( f " [texture] created { w8 } × { h8 } NV12 (AR= { w8 / h8 : .3f } ) " )
# --- SDL_UpdateNVTexture upload ---
t_up0 = time . monotonic ( )
if fmt8 == " NV12 " and y_size8 > 0 :
if fmt8 == " NV12 " and y_size8 > 0 and texture :
y_ptr = ctypes . cast ( arr8 , ctypes . POINTER ( ctypes . c_ubyte ) )
uv_ptr = ctypes . cast (
ctypes . byref ( arr8 , y_size8 ) ,
@ -631,16 +642,25 @@ else:
sdl2 . SDL_UpdateNVTexture (
texture , None , y_ptr , pitch8 , uv_ptr , uv_pitch8 ,
)
els e :
elif textur e :
# BGRA fallback (SW decode path)
pix = ctypes . cast ( arr8 , ctypes . POINTER ( ctypes . c_ubyte ) )
sdl2 . SDL_UpdateTexture ( texture , None , pix , pitch8 )
t_upload = ( time . monotonic ( ) - t_up0 ) * 1e6
# --- SDL_RenderCopy ---
# --- SDL_RenderCopy (letterbox into window) ---
t_r0 = time . monotonic ( )
sdl2 . SDL_RenderClear ( renderer )
sdl2 . SDL_RenderCopy ( renderer , texture , None , None )
if texture :
# Fit frame into window preserving AR (letterbox).
win_w , win_h = w_actual . value , h_actual . value
scale = min ( win_w / w8 , win_h / h8 ) if w8 > 0 and h8 > 0 else 1.0
dw = max ( 1 , int ( w8 * scale ) )
dh = max ( 1 , int ( h8 * scale ) )
dx = ( win_w - dw ) / / 2
dy = ( win_h - dh ) / / 2
dst = sdl2 . SDL_Rect ( dx , dy , dw , dh )
sdl2 . SDL_RenderCopy ( renderer , texture , None , dst )
sdl2 . SDL_RenderPresent ( renderer )
t_render = ( time . monotonic ( ) - t_r0 ) * 1e6
@ -656,7 +676,8 @@ else:
pipeline8 . set_state ( Gst . State . NULL )
eos8 . set ( )
sdl2 . SDL_DestroyTexture ( texture )
if texture :
sdl2 . SDL_DestroyTexture ( texture )
sdl2 . SDL_DestroyRenderer ( renderer )
sdl2 . SDL_DestroyWindow ( window )
sdl2 . SDL_Quit ( )