|
|
|
|
@ -744,4 +744,244 @@ else:
|
|
|
|
|
_warn(f"sdl2 Python bindings not available: {exc}") |
|
|
|
|
_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() |
|
|
|
|
|