Browse Source

test: add section 9 HUD overhead benchmark (draw_playback timing)

main
Matteo Benedetto 1 week ago
parent
commit
6ef8f4722c
  1. 240
      tests/test_video_playback_device.py

240
tests/test_video_playback_device.py

@ -744,4 +744,244 @@ else:
_warn(f"sdl2 Python bindings not available: {exc}") _warn(f"sdl2 Python bindings not available: {exc}")
_warn("Install: conda install -c conda-forge pysdl2") _warn("Install: conda install -c conda-forge pysdl2")
# ── 9. HUD overhead benchmark ──────────────────────────────────────────────
#
# Measures draw_playback() cost per frame using a synthetic 640×360 NV12
# frame so no GStreamer pipeline is needed. Vsync is disabled so phases
# are timed without vsync-wait interference.
#
# Phases:
# upload_us — SDL_UpdateNVTexture (synthetic NV12 frame)
# video_us — SDL_RenderClear + SDL_RenderCopy (letterboxed video rect)
# hud_us — screens.draw_playback() (all TTF_RenderUTF8_Blended calls)
# present_us — SDL_RenderPresent (no vsync → near-zero on HW renderer)
#
# The "hud_us" line is the key number: add it to the section-8 total to get
# the estimated real-app per-frame cost. If hud_us pushes the combined
# total past 41.7 ms the HUD is causing frame drops.
_section("9. HUD overhead per frame (draw_playback benchmark)")
if SKIP_SDL:
_warn("Skipped (--nosection8 flag)")
else:
try:
import ctypes
import statistics
import types
import sdl2
import sdl2.sdlttf as _ttf9
from r36s_dlna_browser.ui import screens as _screens9, theme as _theme9
# ── SDL + TTF init (no vsync so phases are timed cleanly) ────────────
sdl2.SDL_SetHint(b"SDL_VIDEODRIVER", b"kmsdrm,offscreen")
sdl2.SDL_SetHint(b"SDL_AUDIODRIVER", b"alsa,dummy")
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
_ttf9.TTF_Init()
_win9 = sdl2.SDL_CreateWindow(
b"S9-HUD",
sdl2.SDL_WINDOWPOS_UNDEFINED, sdl2.SDL_WINDOWPOS_UNDEFINED,
640, 640,
sdl2.SDL_WINDOW_FULLSCREEN_DESKTOP | sdl2.SDL_WINDOW_SHOWN,
)
# Accelerated renderer, NO PRESENTVSYNC — we want clean per-phase timings.
_ren9 = sdl2.SDL_CreateRenderer(_win9, -1, sdl2.SDL_RENDERER_ACCELERATED)
if not _ren9:
_ren9 = sdl2.SDL_CreateRenderer(_win9, -1, sdl2.SDL_RENDERER_SOFTWARE)
_ww9 = ctypes.c_int(0); _wh9 = ctypes.c_int(0)
sdl2.SDL_GetWindowSize(_win9, ctypes.byref(_ww9), ctypes.byref(_wh9))
_layout9 = _theme9.get_layout(_ww9.value, _wh9.value)
print(f" Window: {_ww9.value}×{_wh9.value} "
f"HUD top={_layout9.playback_hud_top}px bottom={_layout9.playback_hud_bottom}px")
# ── Font (same search order as the app) ──────────────────────────────
_font9 = None
for _fp9 in _theme9.FONT_SEARCH_PATHS:
try:
_f9 = _ttf9.TTF_OpenFont(_fp9.encode(), _layout9.playback_font_size)
if _f9:
_font9 = _f9
print(f" Font: {_fp9} @ {_layout9.playback_font_size}pt")
break
except Exception:
pass
if not _font9:
_warn("No font found — HUD text will be empty (still measures SDL overhead)")
# ── Icons (optional — mirrors app icon loading) ──────────────────────
_icons9: dict = {}
try:
import sdl2.sdlimage as _img9
_img9.IMG_Init(_img9.IMG_INIT_PNG)
_icon_map = [
("hud-play", "hud-play"),
("hud-pause", "hud-pause"),
("hud-stop", "hud-stop"),
("hud-seek", "hud-seek"),
("hud-volume", "hud-volume"),
("hud-display", "hud-display"),
]
for _ifname, _ikey in _icon_map:
_ipath = _theme9.ICONS_DIR / f"{_ifname}.png"
if _ipath.exists():
_isurf = _img9.IMG_Load(str(_ipath).encode())
if _isurf:
_itex = sdl2.SDL_CreateTextureFromSurface(_ren9, _isurf)
sdl2.SDL_FreeSurface(_isurf)
if _itex:
_icons9[_ikey] = _itex
print(f" Icons loaded: {len(_icons9)} / {len(_icon_map)}")
except ImportError:
_warn("sdl2.sdlimage not available — icons skipped (icons=None)")
# ── Synthetic 640×360 NV12 frame (black) ────────────────────────────
_nv12_w, _nv12_h = 640, 360
_y_size9 = _nv12_w * _nv12_h
_uv_size9 = _y_size9 // 2
# Y=0 (black luma), UV=0x80 (neutral chroma) → solid black frame.
_nv12_data9 = b'\x00' * _y_size9 + b'\x80' * _uv_size9
_nv12_buf9 = (ctypes.c_ubyte * len(_nv12_data9)).from_buffer_copy(_nv12_data9)
_y_ptr9 = ctypes.cast(_nv12_buf9, ctypes.POINTER(ctypes.c_ubyte))
_uv_ptr9 = ctypes.cast(ctypes.byref(_nv12_buf9, _y_size9),
ctypes.POINTER(ctypes.c_ubyte))
_tex9 = sdl2.SDL_CreateTexture(
_ren9,
sdl2.SDL_PIXELFORMAT_NV12,
sdl2.SDL_TEXTUREACCESS_STREAMING,
_nv12_w, _nv12_h,
)
# Letterbox dst rect (same logic as the app render path).
_sc9 = min(_ww9.value / _nv12_w, _wh9.value / _nv12_h)
_dw9 = max(1, int(_nv12_w * _sc9)); _dh9 = max(1, int(_nv12_h * _sc9))
_dx9 = (_ww9.value - _dw9) // 2; _dy9 = (_wh9.value - _dh9) // 2
_dst9 = sdl2.SDL_Rect(_dx9, _dy9, _dw9, _dh9)
print(f" Synthetic frame: {_nv12_w}×{_nv12_h} dst rect: {_dw9}×{_dh9} @ ({_dx9},{_dy9})")
# ── Mock playback state ──────────────────────────────────────────────
_state9 = types.SimpleNamespace(
playback_hud_visible=True,
playback_paused=False,
playback_duration=3600.0,
playback_position=42.0,
playback_volume=80,
playback_buffer_percent=100,
playback_resolution="1920×1080",
playback_backend="gstreamer",
playback_title="Test Video — A Long Title That May Require Ellipsis Fitting.mkv",
playback_hud_mode=_theme9.PLAYBACK_HUD_PINNED,
)
# ── Benchmark loop ───────────────────────────────────────────────────
WARMUP9 = 30
FRAMES9 = 300
_upload9: list[float] = []
_video9: list[float] = []
_hud9: list[float] = []
_pres9: list[float] = []
print(f" Running {WARMUP9 + FRAMES9} frames (warmup={WARMUP9}) …")
for _fn9 in range(WARMUP9 + FRAMES9):
# Advance position so time-text changes each frame (exercises _fit_text).
_state9.playback_position = 42.0 + _fn9 * (1.0 / 24.0)
# Phase 1 — NV12 texture upload.
_t0 = time.monotonic()
sdl2.SDL_UpdateNVTexture(
_tex9, None, _y_ptr9, _nv12_w, _uv_ptr9, _nv12_w
)
_t_upload = (time.monotonic() - _t0) * 1e6
# Phase 2 — RenderClear + RenderCopy (video frame, no HUD).
_t0 = time.monotonic()
sdl2.SDL_SetRenderDrawColor(_ren9, 0, 0, 0, 255)
sdl2.SDL_RenderClear(_ren9)
sdl2.SDL_RenderCopy(_ren9, _tex9, None, _dst9)
_t_video = (time.monotonic() - _t0) * 1e6
# Phase 3 — draw_playback() — all TTF_RenderUTF8_Blended + fill-rects.
_t0 = time.monotonic()
_screens9.draw_playback(_ren9, _font9, _state9, _layout9, _icons9 or None)
_t_hud = (time.monotonic() - _t0) * 1e6
# Phase 4 — Present (no vsync → should be near-zero on HW renderer).
_t0 = time.monotonic()
sdl2.SDL_RenderPresent(_ren9)
_t_pres = (time.monotonic() - _t0) * 1e6
if _fn9 >= WARMUP9:
_upload9.append(_t_upload)
_video9.append(_t_video)
_hud9.append(_t_hud)
_pres9.append(_t_pres)
# Drain events.
_ev9 = sdl2.SDL_Event()
while sdl2.SDL_PollEvent(ctypes.byref(_ev9)):
pass
# Cleanup.
for _itex_v in _icons9.values():
sdl2.SDL_DestroyTexture(_itex_v)
if _tex9:
sdl2.SDL_DestroyTexture(_tex9)
if _font9:
_ttf9.TTF_CloseFont(_font9)
_ttf9.TTF_Quit()
sdl2.SDL_DestroyRenderer(_ren9)
sdl2.SDL_DestroyWindow(_win9)
sdl2.SDL_Quit()
# ── Report ────────────────────────────────────────────────────────────
_budget9 = 1_000_000 / 24 # µs per frame @ 24 fps
def _stat9(label, samples):
if not samples:
print(f" {label:48s}: no samples")
return
mn = statistics.mean(samples)
mx = max(samples)
p95 = sorted(samples)[int(len(samples) * 0.95)]
print(f" {label:48s}: mean {mn:6.0f} µs p95 {p95:6.0f} µs max {mx:6.0f} µs ({mn/_budget9*100:.1f}%)")
print()
print(" --- Section 9 HUD Timing Report ---")
print(f" Frames measured (excl warmup) : {len(_hud9)}")
_stat9("SDL_UpdateNVTexture (synthetic NV12)", _upload9)
_stat9("SDL_RenderClear+RenderCopy (video)", _video9)
_stat9("draw_playback() — TTF+fills+icons", _hud9)
_stat9("SDL_RenderPresent (no vsync)", _pres9)
_hud_mean = statistics.mean(_hud9) if _hud9 else 0
_total9_mean = (statistics.mean(_upload9) + statistics.mean(_video9) +
_hud_mean + statistics.mean(_pres9)) if _upload9 else 0
print(f"\n {'TOTAL (upload+video+HUD+present)':48s}: {_total9_mean:.0f} µs "
f"({_total9_mean/_budget9*100:.1f}% of 41.7ms budget — no vsync wait)")
# Estimate real-app cost: section-8 measured memmove+upload+render ≈ 8550 µs
# that path had no HUD; add hud_mean to get the estimated combined overhead.
_s8_baseline = 8550 # µs, from last known section-8 run
_estimated_full = _s8_baseline + _hud_mean
print(f" {'Estimated S8 + HUD combined':48s}: {_estimated_full:.0f} µs "
f"({_estimated_full/_budget9*100:.1f}% of 41.7ms budget)\n")
if _hud_mean > 15_000:
_warn(f"HUD mean {_hud_mean:.0f} µs > 15 ms — very likely causing frame drops!")
elif _hud_mean > 8_000:
_warn(f"HUD mean {_hud_mean:.0f} µs — significant overhead; monitor for drops at 24 fps")
elif _hud_mean > 3_000:
_warn(f"HUD mean {_hud_mean:.0f} µs — moderate overhead; combined budget may be tight")
else:
_ok(f"HUD overhead {_hud_mean:.0f} µs — not a bottleneck")
except ImportError as exc:
_warn(f"sdl2 / sdlttf / screens not available: {exc}")
print() print()

Loading…
Cancel
Save