Fix periodic tvOS playback stutter from render stalls on mpvQueue

Build 264 stutters for a split second every ~10 seconds. The periodic
playback-stats task fetches ~30 debug properties in one ~190ms block on
mpvQueue, and MPVClient.render() wrapped mpv_render_context_render in
mpvQueue.sync - so the render thread queued behind the fetch and stalled
~190ms, at exactly the stats interval. Build 261 predates the stats
task, which is why it was unaffected.

libmpv's render API is documented thread-safe relative to the client
API, so render-context calls no longer go through mpvQueue:

- New renderContextLock protects renderContext lifecycle vs use;
  render(), renderWithDepth(), renderSoftware(), reportSwap(),
  shouldRenderUpdateFrame() and hasRenderContext use it
- Render-update callbacks dispatch on a dedicated renderEventQueue so
  frame-ready notifications can't queue behind property/command work
- destroyRenderContext takes mpvQueue then the lock, still excluding
  in-flight renders during teardown
- The stats task skips its property fetch on iOS/tvOS unless verbose
  MPV logging is on (macOS keeps it for the render watchdog)
- performRender slow-frame warnings now break down render vs present
  time and report frame-delivery gaps for future triage
This commit is contained in:
Arkadiusz Fal
2026-07-19 00:53:06 +02:00
parent c3bb01e862
commit 6aa5fd8ee7
3 changed files with 150 additions and 76 deletions

View File

@@ -836,9 +836,23 @@ final class MPVRenderView: UIView {
private var slowFrameCount: UInt64 = 0
private var lastSlowFrameWarning: Date = .distantPast
/// Debug: start time of the previous performRender, to spot gaps where mpv
/// stopped delivering frames (core-side stall vs. GL-side slow frame).
private var lastRenderStartMediaTime: CFTimeInterval = 0
private var lastGapWarning: Date = .distantPast
private func performRender() {
defer { isRendering = false }
let frameStart = Date()
let tFrameStart = CACurrentMediaTime()
let gapSincePrevFrame = lastRenderStartMediaTime > 0 ? tFrameStart - lastRenderStartMediaTime : 0
lastRenderStartMediaTime = tFrameStart
if hasRenderedFirstFrame, gapSincePrevFrame > 0.25,
Date().timeIntervalSince(lastGapWarning) > 2 {
lastGapWarning = Date()
MPVLogging.warn("performRender: frame delivery gap",
details: "gap=\(Int(gapSincePrevFrame * 1000))ms since previous frame")
}
guard let eaglContext, let mpvClient, framebuffer != 0 else {
// Log when render is skipped due to missing resources (rare but important)
@@ -894,6 +908,8 @@ final class MPVRenderView: UIView {
mpvClient.render(fbo: GLint(framebuffer), width: renderWidth, height: renderHeight)
}
let tRenderEnd = CACurrentMediaTime()
// Present the renderbuffer (skip when PiP is active - main view is hidden anyway)
if !isPiPActive {
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), colorRenderbuffer)
@@ -922,8 +938,10 @@ final class MPVRenderView: UIView {
slowFrameCount += 1
if Date().timeIntervalSince(lastSlowFrameWarning) > 5 {
lastSlowFrameWarning = Date()
let renderMs = Int((tRenderEnd - tFrameStart) * 1000)
let presentMs = Int((CACurrentMediaTime() - tRenderEnd) * 1000)
MPVLogging.warn("performRender: slow frame",
details: "duration=\(Int(frameDuration * 1000))ms slowFramesSinceLastWarning=\(slowFrameCount) size=\(renderWidth)x\(renderHeight)")
details: "duration=\(Int(frameDuration * 1000))ms render=\(renderMs)ms present+swap=\(presentMs)ms gapBefore=\(Int(gapSincePrevFrame * 1000))ms slowFramesSinceLastWarning=\(slowFrameCount) size=\(renderWidth)x\(renderHeight)")
slowFrameCount = 0
}
}