fix(linux): GL threading, state management, and animation fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user