Files
plezy/linux/runner/mpv/plane_render_executor.h
T
edde746 974806fe80 fix(linux): render the mpv video plane off the GTK main thread
The player UI was choppy and slow while video played, and scrub-bar
thumbnails rarely appeared until the video was paused (worst with 4K
HDR content, whose per-frame tone-map render is expensive). The cause:
mpv's render and the plane's eglSwapBuffers ran on the GTK main thread,
which also rasters Flutter's UI and dispatches input, so every UI
repaint and pointer event waited out the video frame render.

Move the render + swap onto a dedicated plane render thread
(PlaneRenderExecutor). All Wayland protocol state stays on the main
thread: Present() splits into PreparePresent() (gates + frame-callback
request) and CompletePresent() (first-frame scale flush, ack watchdog,
mid-flight hide/rect-loss re-detach). The plugin serializes one job at
a time, defers rect application and HDR transaction starts to the job
completion so a resize never races the swap and a staged colour
transition can never pair an old-colour buffer with a new description,
and drains the worker before disposal - which is what lets
RenderToSurface render without holding native_mutex_. A worker wedged
inside a driver call is abandoned after a bounded wait and the
session's player and plane are deliberately leaked instead of freed
under it. PLEZY_PLANE_RENDER_MAIN_THREAD=1 restores the old inline
behaviour as a temporary escape hatch.

Measured in a headless-sway container with a 4K test file (llvmpipe
inflates render cost the way DV tone-mapping does on real hardware):
idle-playing UI commits went from ~354 ms to the keep-alive's ~100 ms,
and pointer reads from ~185 ms bursts back to input rate; the #2067
hide/backoff/recover log signature is byte-identical.

close #2057
2026-08-23 00:13:43 +02:00

87 lines
3.3 KiB
C++

#ifndef PLEZY_LINUX_MPV_PLANE_RENDER_EXECUTOR_H_
#define PLEZY_LINUX_MPV_PLANE_RENDER_EXECUTOR_H_
#include <glib.h>
#include <condition_variable>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <thread>
#include <utility>
namespace mpv {
// The video plane's render worker (issue #2057).
//
// mpv's render + the plane's eglSwapBuffers used to run on the GTK main
// thread, which also rasters Flutter's UI and dispatches input. A cheap frame
// hides that; a 4K HDR tone-map does not - input reads batch at video-render
// boundaries and every UI repaint waits out the render. This worker exists to
// take exactly that GL work off the main thread. Everything else about the
// plane - Wayland protocol state, watchdogs, colour transitions, geometry -
// deliberately stays on the main thread; see WaylandVideoSurface.
//
// Contract:
// - Jobs run in order on one worker thread and return a bool.
// - Completions run on the GLib main context that was thread-default when
// the executor was constructed, carrying the job's result. They are
// delivered even after ShutdownAndJoin, so they must guard against state
// that has since been torn down (the plugin's generation counter).
// - ShutdownAndJoin drains queued jobs first. A job wedged inside a driver
// call cannot be interrupted; past the timeout the thread is abandoned
// (detached) and false is returned so the caller can leak, rather than
// free, what the job may still touch. The worker owns its state through a
// shared_ptr, never through the executor object, so abandonment leaks that
// state instead of leaving the thread on freed memory.
class PlaneRenderExecutor {
public:
// Runs on the worker thread; the result is handed to the completion.
using Job = std::function<bool()>;
// Runs on the construction-time GLib main context.
using Completion = std::function<void(bool)>;
PlaneRenderExecutor();
~PlaneRenderExecutor();
PlaneRenderExecutor(const PlaneRenderExecutor&) = delete;
PlaneRenderExecutor& operator=(const PlaneRenderExecutor&) = delete;
/// Queues |job|. Returns false only after shutdown has begun, in which case
/// neither the job nor the completion will run.
bool Post(Job job, Completion completion);
/// Stops accepting jobs, waits up to |timeout_ms| for queued jobs to drain,
/// and joins the worker. Returns false when the worker had to be abandoned
/// instead - see the class comment. Idempotent.
bool ShutdownAndJoin(unsigned int timeout_ms);
private:
// Everything the worker touches. Held by shared_ptr from both the executor
// and the worker thread's closure, so an abandoned worker still stands on
// live memory. The completion context reference is owned here and released
// by the destructor - i.e. by whichever side lets go last.
struct Shared {
~Shared();
GMainContext* completion_context = nullptr;
std::mutex mutex;
std::condition_variable wake;
std::condition_variable idle;
std::deque<std::pair<Job, Completion>> jobs;
bool running_job = false;
bool quitting = false;
};
static void Run(const std::shared_ptr<Shared>& shared);
std::shared_ptr<Shared> shared_;
std::thread thread_;
bool abandoned_ = false;
};
} // namespace mpv
#endif // PLEZY_LINUX_MPV_PLANE_RENDER_EXECUTOR_H_