diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index 279504c..ff4db37 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -358,7 +358,7 @@ func main() { video string audio string } - // One control channel: grant (empty params) or reconnect (with params). + // Reconnect requests from GUI or automatic retry control := make(chan reconnectParams, 1) videoBridge := playback.NewVideoBridge() @@ -377,7 +377,7 @@ func main() { audioSrc = nil } // Try once. Return error if fails — caller loops back to select - // and can pick up new reconnect params or a new grant. + // and can pick up newer reconnect request. if params.video != "" && params.audio != "" { s, e := source.OpenSameDomainSync(params.domain, params.video, params.audio) if e == nil { @@ -438,7 +438,7 @@ func main() { } go func() { - // Audio-only mode: independent loop, no grant/staged handshake. + // Audio-only mode: independent loop. if audioSrc != nil && syncSrc == nil && videoSrc == nil { for { select { @@ -503,31 +503,34 @@ func main() { } } - // Video (with or without sync) mode: grant/staged handshake. + // Video bridge provides backpressure: only one borrowed frame is in flight. for { - params := <-control - if params.video != "" || params.audio != "" { - // Reconnect request from Connect button or auto-retry. - select { - case <-control: // drain any pending grant - default: - } + select { + case <-ctx.Done(): + return + + case params := <-control: if rerr := reopen(params); rerr != nil { if errors.Is(rerr, context.Canceled) { return } log.Printf("source: reopen failed: %v, retrying", rerr) + select { case <-time.After(500 * time.Millisecond): case <-ctx.Done(): return } + select { case control <- params: default: + // Preserve an already queued, potentially newer request. } } continue + + default: } var videoFrame playback.VideoFrame @@ -538,15 +541,16 @@ func main() { return } log.Printf("source: %v", err) - // Drain any pending grant, then send reconnect. - select { - case <-control: - default: + // Request a reconnect after the read failure. + params := reconnectParams{ + domain: domainStr, + video: videoStr, + audio: audioStr, } select { - case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}: - case <-ctx.Done(): - return + case control <- params: + default: + // Preserve an already queued, potentially newer request. } continue } @@ -570,14 +574,15 @@ func main() { return } log.Printf("source: %v", err) - select { - case <-control: - default: + params := reconnectParams{ + domain: domainStr, + video: videoStr, + audio: audioStr, } select { - case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}: - case <-ctx.Done(): - return + case control <- params: + default: + // Preserve an already queued, potentially newer request. } continue } @@ -604,7 +609,6 @@ func main() { running := true resized := false - granted := false fullscreen := args.IsFullscreen if fullscreen { sdl.SetWindowFullscreen(windowHandler, true) @@ -662,15 +666,6 @@ func main() { } resized = false } - if !granted { - select { - case control <- reconnectParams{}: - granted = true - case <-ctx.Done(): - running = false - continue - } - } var shownIndex uint64 hasFrame := false @@ -697,15 +692,11 @@ func main() { } shownIndex = pendingFrame.Frame.Index - granted = false hasFrame = true } else if frameErr != nil && !errors.Is(frameErr, context.DeadlineExceeded) && !errors.Is(frameErr, context.Canceled) { panic(frameErr) - } else { - // No frame arrived before the deadline. - granted = false } // stats diff --git a/imgui.ini b/imgui.ini index a963c30..65dff5f 100644 --- a/imgui.ini +++ b/imgui.ini @@ -14,7 +14,7 @@ Size=200,200 Collapsed=0 [Window][Connection] -Pos=1250,701 +Pos=500,330 Size=523,153 Collapsed=0 diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..ff5cdcc --- /dev/null +++ b/notes.md @@ -0,0 +1,6 @@ +That makes sense. The GUI is likely more responsive because frame staging is now serialized with rendering: +- The background goroutine no longer copies a large frame into Vulkan-mapped memory concurrently with GUI/render work. +- StageFrame waits for the GPU fence before writing, removing CPU/GPU memory contention and undefined synchronization. +- The bridge creates deterministic backpressure: the source cannot begin another frame until the current payload is staged. +- The main thread now controls the complete render sequence instead of coordinating loosely through two channels. +So we fixed both correctness and scheduling stability without adding another frame copy.