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
101 lines
3.0 KiB
C++
101 lines
3.0 KiB
C++
#include "plane_render_executor.h"
|
|
|
|
#include <chrono>
|
|
|
|
namespace mpv {
|
|
|
|
namespace {
|
|
|
|
struct CompletionInvocation {
|
|
PlaneRenderExecutor::Completion completion;
|
|
bool result;
|
|
};
|
|
|
|
gboolean InvokeCompletion(gpointer data) {
|
|
auto* invocation = static_cast<CompletionInvocation*>(data);
|
|
invocation->completion(invocation->result);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
void DestroyCompletionInvocation(gpointer data) { delete static_cast<CompletionInvocation*>(data); }
|
|
|
|
} // namespace
|
|
|
|
PlaneRenderExecutor::Shared::~Shared() {
|
|
if (completion_context != nullptr) g_main_context_unref(completion_context);
|
|
}
|
|
|
|
PlaneRenderExecutor::PlaneRenderExecutor() : shared_(std::make_shared<Shared>()) {
|
|
shared_->completion_context = g_main_context_ref_thread_default();
|
|
thread_ = std::thread(&PlaneRenderExecutor::Run, shared_);
|
|
}
|
|
|
|
PlaneRenderExecutor::~PlaneRenderExecutor() { ShutdownAndJoin(5000); }
|
|
|
|
bool PlaneRenderExecutor::Post(Job job, Completion completion) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(shared_->mutex);
|
|
if (shared_->quitting) return false;
|
|
shared_->jobs.emplace_back(std::move(job), std::move(completion));
|
|
}
|
|
shared_->wake.notify_one();
|
|
return true;
|
|
}
|
|
|
|
bool PlaneRenderExecutor::ShutdownAndJoin(unsigned int timeout_ms) {
|
|
if (!thread_.joinable()) return !abandoned_;
|
|
{
|
|
std::unique_lock<std::mutex> lock(shared_->mutex);
|
|
shared_->quitting = true;
|
|
shared_->wake.notify_all();
|
|
if (!shared_->idle.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this] {
|
|
return shared_->jobs.empty() && !shared_->running_job;
|
|
})) {
|
|
abandoned_ = true;
|
|
}
|
|
}
|
|
if (abandoned_) {
|
|
// The job is wedged inside a call that cannot be interrupted. Joining
|
|
// would hang the caller forever; the thread is cut loose instead, and the
|
|
// caller is told so it can leak, rather than free, whatever the job may
|
|
// still be touching. The worker keeps the shared state alive on its own.
|
|
thread_.detach();
|
|
return false;
|
|
}
|
|
thread_.join();
|
|
return true;
|
|
}
|
|
|
|
void PlaneRenderExecutor::Run(const std::shared_ptr<Shared>& shared) {
|
|
for (;;) {
|
|
Job job;
|
|
Completion completion;
|
|
{
|
|
std::unique_lock<std::mutex> lock(shared->mutex);
|
|
shared->wake.wait(lock, [&shared] { return !shared->jobs.empty() || shared->quitting; });
|
|
// Quitting drains: queued jobs still run, so a shutdown-time job (the
|
|
// EGL unbind) can be posted and then waited for.
|
|
if (shared->jobs.empty()) return;
|
|
job = std::move(shared->jobs.front().first);
|
|
completion = std::move(shared->jobs.front().second);
|
|
shared->jobs.pop_front();
|
|
shared->running_job = true;
|
|
}
|
|
|
|
const bool result = job ? job() : false;
|
|
if (completion) {
|
|
g_main_context_invoke_full(
|
|
shared->completion_context, G_PRIORITY_DEFAULT, InvokeCompletion,
|
|
new CompletionInvocation{std::move(completion), result}, DestroyCompletionInvocation);
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(shared->mutex);
|
|
shared->running_job = false;
|
|
}
|
|
shared->idle.notify_all();
|
|
}
|
|
}
|
|
|
|
} // namespace mpv
|