fix(linux): GL threading, state management, and animation fixes

This commit is contained in:
edde746
2026-01-05 19:40:04 +01:00
parent a10dfcc004
commit 562b54d1cb
8 changed files with 139 additions and 133 deletions
+10 -3
View File
@@ -135,6 +135,10 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
}
void MpvPlayer::Dispose() {
// Lock mutex to prevent race with OnMpvWakeup callbacks.
// This ensures no new event processing starts during dispose.
std::lock_guard<std::mutex> lock(callback_mutex_);
// Guard against multiple dispose calls (double-free protection)
if (disposed_.exchange(true)) {
return; // Already disposed
@@ -282,15 +286,18 @@ void MpvPlayer::RequestRedraw() {
void MpvPlayer::OnMpvWakeup(void* ctx) {
auto* player = static_cast<MpvPlayer*>(ctx);
// Don't schedule if already disposed
// Don't schedule if already disposed (atomic check for early exit)
if (player->disposed_) return;
// Schedule event processing on the main thread.
g_idle_add(
[](gpointer data) -> gboolean {
auto* player = static_cast<MpvPlayer*>(data);
// Check disposed again when callback runs
if (!player->disposed_) {
// Check disposed - atomic ensures we see updated value.
// Don't lock mutex here - ProcessEvents() calls SendPropertyChange/SendEvent
// which lock callback_mutex_, causing deadlock if we hold it here.
if (!player->disposed_ && player->mpv_) {
player->ProcessEvents();
}
return G_SOURCE_REMOVE;
+67 -26
View File
@@ -31,6 +31,7 @@ static gboolean on_gl_render(GtkGLArea* area,
gpointer user_data);
static void on_gl_realize(GtkGLArea* area, gpointer user_data);
static void on_gl_unrealize(GtkGLArea* area, gpointer user_data);
static void on_gl_resize(GtkGLArea* area, gint width, gint height, gpointer user_data);
static void mpv_plugin_dispose(GObject* object) {
MpvPlugin* self = MPV_PLUGIN(object);
@@ -103,6 +104,7 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
g_signal_connect(gl_area, "render", G_CALLBACK(on_gl_render), self);
g_signal_connect(gl_area, "realize", G_CALLBACK(on_gl_realize), self);
g_signal_connect(gl_area, "unrealize", G_CALLBACK(on_gl_unrealize), self);
g_signal_connect(gl_area, "resize", G_CALLBACK(on_gl_resize), self);
// Set up auto-render to false - we control when to render.
gtk_gl_area_set_auto_render(gl_area, FALSE);
@@ -132,6 +134,17 @@ static gboolean on_gl_render(GtkGLArea* area,
(void)context;
MpvPlugin* self = MPV_PLUGIN(user_data);
// Ensure GL context is current before any GL operations.
// Critical during fullscreen transitions and workspace switches (issue #202).
gtk_gl_area_make_current(area);
// Check for GL context errors (can happen during window state changes)
GError* error = gtk_gl_area_get_error(area);
if (error != nullptr) {
g_warning("MPV Plugin: GL context error in render: %s", error->message);
return FALSE; // Signal failure to GTK
}
if (!self->player || !self->player->IsInitialized() || !self->visible) {
// Clear to transparent when not showing video.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
@@ -152,10 +165,41 @@ static gboolean on_gl_render(GtkGLArea* area,
GLint fbo = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
// Render the video frame.
// Save GL state before MPV render (MPV modifies these and doesn't restore them)
// This prevents GL state pollution that corrupts Flutter's rendering.
GLint prev_viewport[4];
GLint prev_scissor_box[4];
GLboolean prev_blend, prev_scissor_test;
GLint prev_blend_src, prev_blend_dst;
glGetIntegerv(GL_VIEWPORT, prev_viewport);
glGetIntegerv(GL_SCISSOR_BOX, prev_scissor_box);
glGetBooleanv(GL_BLEND, &prev_blend);
glGetBooleanv(GL_SCISSOR_TEST, &prev_scissor_test);
glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
// Set viewport and render the video frame.
glViewport(0, 0, width, height);
self->player->Render(width, height, fbo);
self->player->ClearRedrawFlag();
// Restore GL state after MPV render to prevent Flutter corruption.
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
glScissor(prev_scissor_box[0], prev_scissor_box[1], prev_scissor_box[2], prev_scissor_box[3]);
if (prev_blend) {
glEnable(GL_BLEND);
} else {
glDisable(GL_BLEND);
}
if (prev_scissor_test) {
glEnable(GL_SCISSOR_TEST);
} else {
glDisable(GL_SCISSOR_TEST);
}
glBlendFunc(prev_blend_src, prev_blend_dst);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
return TRUE;
}
@@ -184,6 +228,13 @@ static void on_gl_unrealize(GtkGLArea* area, gpointer user_data) {
gtk_gl_area_make_current(area);
// Check if context is valid before disposing GL resources (issue #202)
GError* error = gtk_gl_area_get_error(area);
if (error != nullptr) {
g_warning("MPV Plugin: GL context error in unrealize: %s", error->message);
}
// Always try to dispose - Dispose() handles its own safety checks
if (self->player) {
self->player->Dispose();
}
@@ -191,6 +242,21 @@ static void on_gl_unrealize(GtkGLArea* area, gpointer user_data) {
g_message("MPV Plugin: GL area unrealized");
}
/// GtkGLArea resize callback.
static void on_gl_resize(GtkGLArea* area,
gint width,
gint height,
gpointer user_data) {
MpvPlugin* self = MPV_PLUGIN(user_data);
(void)width;
(void)height;
// Force a redraw when size changes to prevent lag during resize.
if (self->visible && self->player && self->player->IsInitialized()) {
gtk_gl_area_queue_render(area);
}
}
/// Method call handler.
static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
FlMethodCall* method_call,
@@ -252,9 +318,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
self->initialized = FALSE;
self->visible = FALSE;
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), FALSE);
// Restore Flutter view opacity to 1.0 (may have been set to 0 by setControlsVisible)
if (self->flutter_view != nullptr) {
gtk_widget_set_opacity(self->flutter_view, 1.0);
// Force Flutter view to redraw
gtk_widget_queue_draw(self->flutter_view);
}
@@ -367,29 +431,6 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
gtk_gl_area_queue_render(self->gl_area);
}
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
}
} else if (strcmp(method, "setControlsVisible") == 0) {
// Set Flutter view opacity when controls are hidden/shown.
// This is a workaround for Flutter's lack of transparency support on Linux.
// When controls are hidden, setting opacity to 0 shows only the video
// while keeping the widget interactive for mouse events.
FlValue* controls_visible_value = fl_value_lookup_string(args, "visible");
if (controls_visible_value == nullptr ||
fl_value_get_type(controls_visible_value) != FL_VALUE_TYPE_BOOL) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'visible'", nullptr));
} else {
gboolean controls_visible = fl_value_get_bool(controls_visible_value);
// When controls are hidden, set Flutter view opacity to 0.
// When controls are visible, set opacity to 1.
// Using opacity keeps the widget interactive for mouse events.
if (self->flutter_view != nullptr) {
gtk_widget_set_opacity(self->flutter_view, controls_visible ? 1.0 : 0.0);
}
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
}
} else if (strcmp(method, "isInitialized") == 0) {
+3 -2
View File
@@ -109,8 +109,9 @@ static void my_application_activate(GApplication* application) {
gtk_widget_set_hexpand(GTK_WIDGET(self->flutter_view), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(self->flutter_view), TRUE);
// Enable transparency for the Flutter view.
gtk_widget_set_app_paintable(GTK_WIDGET(self->flutter_view), TRUE);
// Set up RGBA visual for transparency support.
// Note: app_paintable is only set on the window (line 73), not on child widgets,
// to avoid redundant composition passes.
setup_rgba_visual(GTK_WIDGET(self->flutter_view));
// Enable transparent background for the Flutter view.