diff --git a/media_kit_video/linux/gl_render_thread.cc b/media_kit_video/linux/gl_render_thread.cc index 9d5aef029..6c9ba8c1e 100644 --- a/media_kit_video/linux/gl_render_thread.cc +++ b/media_kit_video/linux/gl_render_thread.cc @@ -66,10 +66,6 @@ void GLRenderThread::PostAndWait(std::function task) { wait_cv.wait(lock, [&]() { return done; }); } -bool GLRenderThread::IsCurrentThread() const { - return std::this_thread::get_id() == thread_id_; -} - void GLRenderThread::Run() { // Store thread ID { diff --git a/media_kit_video/linux/include/media_kit_video/gl_render_thread.h b/media_kit_video/linux/include/media_kit_video/gl_render_thread.h index a0be9d115..a0e3c624f 100644 --- a/media_kit_video/linux/include/media_kit_video/gl_render_thread.h +++ b/media_kit_video/linux/include/media_kit_video/gl_render_thread.h @@ -24,12 +24,9 @@ class GLRenderThread { // Post a task to the GL render thread void Post(std::function task); - + // Post a task and wait for completion (synchronous) void PostAndWait(std::function task); - - // Check if we're on the GL render thread - bool IsCurrentThread() const; private: void Run(); diff --git a/media_kit_video/linux/include/media_kit_video/texture_gl.h b/media_kit_video/linux/include/media_kit_video/texture_gl.h index 81a2ffb01..26fdc6143 100644 --- a/media_kit_video/linux/include/media_kit_video/texture_gl.h +++ b/media_kit_video/linux/include/media_kit_video/texture_gl.h @@ -22,30 +22,17 @@ G_DECLARE_FINAL_TYPE(TextureGL, texture_gl, TEXTURE_GL, TEXTURE_GL, FlTextureGL) TextureGL* texture_gl_new(VideoOutput* video_output); -/** - * @brief Checks if texture needs resize and performs it if necessary. - * This manages the mailbox triple buffering - creates/resizes all three buffers. - */ +// Creates/resizes all three mailbox buffers if needed. GL thread only. void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 required_height); -/** - * @brief Renders mpv frame to the back buffer (called from dedicated GL thread). - * Uses mailbox model: renders to back buffer, then swaps with mailbox atomically. - * @return TRUE if rendering was performed, FALSE if skipped. - */ +// Renders an mpv frame to the back buffer. GL thread only. +// Returns TRUE if a frame was rendered. gboolean texture_gl_render(TextureGL* self); -/** - * @brief Publishes the rendered frame using mailbox swap. - * Atomically swaps back buffer with mailbox, old mailbox content becomes new back buffer. - * Called from dedicated GL thread after render finishes. - */ +// Publishes the back buffer into the mailbox (dirty=1). GL thread only. void texture_gl_swap_buffers(TextureGL* self); -/** - * @brief Populates texture with video frame using mailbox model. - * Atomically swaps front buffer with mailbox to get the latest frame. - */ +// Consumer side of the mailbox; called from Flutter's raster thread. gboolean texture_gl_populate_texture(FlTextureGL* texture, guint32* target, guint32* name, diff --git a/media_kit_video/linux/include/media_kit_video/video_output.h b/media_kit_video/linux/include/media_kit_video/video_output.h index 5eb7ba084..eda931afe 100644 --- a/media_kit_video/linux/include/media_kit_video/video_output.h +++ b/media_kit_video/linux/include/media_kit_video/video_output.h @@ -47,57 +47,30 @@ G_DECLARE_FINAL_TYPE(VideoOutput, #define VIDEO_OUTPUT(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), video_output_get_type(), VideoOutput)) -/** - * @brief Creates a new |VideoOutput| instance for given |handle|. - * - * @param texture_registrar |FlTextureRegistrar| reference. - * @param view |FlView| reference. - * @param handle |mpv_handle| reference casted to gint64. - * @param configuration Video output configuration. - * @param gl_render_thread |GLRenderThread| reference for dedicated GL rendering. - * @return VideoOutput* - */ +// Creates a new |VideoOutput| for given |handle| (|mpv_handle| casted to +// gint64). Falls back to S/W rendering if no usable EGL display is found. VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, - FlView* view, gint64 handle, VideoOutputConfiguration configuration, GLRenderThread* gl_render_thread); -/** - * @brief Sets the callback invoked when the texture ID updates i.e. video - * dimensions changes. - * - * @param self |VideoOutput| reference. - * @param texture_update_callback Callback. - * @param texture_update_callback_context Callback context. - */ +// Sets the callback invoked when the texture ID updates i.e. video +// dimensions change. void video_output_set_texture_update_callback( VideoOutput* self, TextureUpdateCallback texture_update_callback, gpointer texture_update_callback_context); -/** - * @brief Sets the required video output size. This forces |VideoOutput| to - * resize the internal OpenGL surface / texture. - * - * @param texture_registrar |FlTextureRegistrar| reference. - * @param width Preferred width of the video. Pass `NULL` for using texture - * dimensions based on video's resolution. - * @param height Preferred height of the video. Pass `NULL` for using texture - * dimensions based on video's resolution. - */ +// Sets the required video output size. Pass 0 to size the texture based on +// the video's own resolution. void video_output_set_size(VideoOutput* self, gint64 width, gint64 height); mpv_render_context* video_output_get_render_context(VideoOutput* self); -GdkGLContext* video_output_get_gdk_gl_context(VideoOutput* self); - EGLDisplay video_output_get_egl_display(VideoOutput* self); EGLContext video_output_get_egl_context(VideoOutput* self); -EGLSurface video_output_get_egl_surface(VideoOutput* self); - GLRenderThread* video_output_get_gl_render_thread(VideoOutput* self); guint8* video_output_get_pixel_buffer(VideoOutput* self); @@ -110,10 +83,7 @@ gint64 video_output_get_texture_id(VideoOutput* self); void video_output_notify_texture_update(VideoOutput* self); +// Schedules resize-check + render on the dedicated GL thread. void video_output_notify_render(VideoOutput* self); -void video_output_check_and_resize(VideoOutput* self); - -void video_output_render(VideoOutput* self); - #endif // VIDEO_OUTPUT_H_ diff --git a/media_kit_video/linux/include/media_kit_video/video_output_manager.h b/media_kit_video/linux/include/media_kit_video/video_output_manager.h index ff6c03ab2..3a651ca5a 100644 --- a/media_kit_video/linux/include/media_kit_video/video_output_manager.h +++ b/media_kit_video/linux/include/media_kit_video/video_output_manager.h @@ -26,51 +26,23 @@ G_DECLARE_FINAL_TYPE(VideoOutputManager, VideoOutputManager)) VideoOutputManager* video_output_manager_new( - FlTextureRegistrar* texture_registrar, - FlView* view); + FlTextureRegistrar* texture_registrar); -/** - * @brief Creates a new |VideoOutput| instance for given |handle|. - * - * @param self |VideoOutputManager| reference. - * @param handle |mpv_handle| reference casted to gint64. - * @param width Preferred width of the video. Pass `NULL` for using texture - * dimensions based on video's resolution. - * @param height Preferred height of the video. Pass `NULL` for using texture - * dimensions based on video's resolution. - * @param enable_hardware_acceleration Whether to enable hardware acceleration. - * @param texture_update_callback Callback invoked when the texture ID updates - * i.e. video dimensions changes. - * @param texture_update_callback_context Context passed to - * |texture_update_callback|. - */ +// Creates a new |VideoOutput| instance for given |handle|. void video_output_manager_create(VideoOutputManager* self, gint64 handle, VideoOutputConfiguration configuration, TextureUpdateCallback texture_update_callback, gpointer texture_update_callback_context); -/** - * @brief Sets the required video output size. This forces |VideoOutput| to - * resize the internal OpenGL surface / texture. - * - * @param texture_registrar |FlTextureRegistrar| reference. - * @param width Preferred width of the video. Pass `NULL` for using texture - * dimensions based on video's resolution. - * @param height Preferred height of the video. Pass `NULL` for using texture - * dimensions based on video's resolution. - */ +// Sets the required video output size. Pass 0 to size the texture based on +// the video's own resolution. void video_output_manager_set_size(VideoOutputManager* self, gint64 handle, gint64 width, gint64 height); -/** - * @brief Disposes |VideoOutput| instance for given |handle|. - * - * @param self |VideoOutputManager| reference. - * @param handle |mpv_handle| reference casted to gint64. - */ +// Disposes |VideoOutput| instance for given |handle|. void video_output_manager_dispose(VideoOutputManager* self, gint64 handle); #endif diff --git a/media_kit_video/linux/media_kit_video_plugin.cc b/media_kit_video/linux/media_kit_video_plugin.cc index da6efa33f..58537d9d8 100644 --- a/media_kit_video/linux/media_kit_video_plugin.cc +++ b/media_kit_video/linux/media_kit_video_plugin.cc @@ -183,10 +183,9 @@ static MediaKitVideoPlugin* media_kit_video_plugin_new( g_object_unref); FlTextureRegistrar* texture_registrar = fl_plugin_registrar_get_texture_registrar(registrar); - FlView* view = fl_plugin_registrar_get_view(registrar); - self->view = view; - self->video_output_manager = - video_output_manager_new(texture_registrar, view); + // |view| is only needed for native fullscreen handling. + self->view = fl_plugin_registrar_get_view(registrar); + self->video_output_manager = video_output_manager_new(texture_registrar); return self; } diff --git a/media_kit_video/linux/texture_gl.cc b/media_kit_video/linux/texture_gl.cc index d46a13d02..82be578f4 100644 --- a/media_kit_video/linux/texture_gl.cc +++ b/media_kit_video/linux/texture_gl.cc @@ -12,93 +12,120 @@ #include #include #include +#include +#include -// Number of buffers for mailbox triple buffering #define NUM_BUFFERS 3 -// Buffer structure for mailbox triple buffering -// Each buffer has its own GPU resources +// One buffer of the mailbox model; each owns its full set of GPU resources. typedef struct { - guint32 fbo; // FBO for mpv rendering - guint32 texture; // Texture attached to FBO (mpv side) - EGLImageKHR egl_image; // EGLImage for sharing between contexts - guint32 flutter_texture; // Flutter's texture bound to EGLImage - gboolean flutter_texture_valid; // Whether Flutter texture is valid - std::atomic render_sync; // Sync created after mpv render (atomic for cross-thread access) + guint32 fbo; // FBO mpv renders into (mpv context) + guint32 texture; // Texture backing the FBO (mpv context) + EGLImageKHR egl_image; // Shares |texture| across contexts + guint32 flutter_texture; // Flutter-side texture bound to |egl_image| + EGLContext flutter_context; // Raster context |flutter_texture| was created in + gboolean flutter_texture_valid; + std::atomic render_sync; // Fence from producer, consumed cross-thread } RenderBuffer; /** - * Mailbox Triple Buffering Model with Drain-Only Consumer: - * - * Three buffers with fixed roles that rotate via atomic pointer swaps: - * - back: Producer (GL thread) renders to this buffer - * - mailbox: Holds the latest complete frame with dirty flag - * - front: Consumer (Flutter main thread) reads from this buffer - * - * Key design: mailbox_state combines index and dirty flag in ONE atomic: - * mailbox_state = (dirty << 8) | index - * This eliminates race conditions between checking dirty and swapping. - * - * Producer workflow (GL thread): - * 1. Render frame to back buffer - * 2. Atomic exchange: put (dirty=1, back_index) into mailbox, get old index - * 3. Old mailbox index becomes new back buffer - * - * Consumer workflow (Flutter main thread) - DRAIN-ONLY: - * 1. Atomic CAS: if dirty=1, swap (dirty=0, front_index) with mailbox - * 2. If CAS succeeds: got new frame, update front_index - * 3. If dirty=0: no new frame, keep current front buffer - * 4. Display front buffer - * - * Drain-only semantics ensures: - * - Consumer only swaps when mailbox has NEW content (dirty=1) - * - Consumer never puts its displayed frame back unless getting new one - * - Producer can always safely overwrite mailbox content - * - Single atomic operation prevents dirty/index race condition + * Mailbox triple buffering with a drain-only consumer (intentional design: + * the producer may overwrite the mailbox frame; dropped frames are expected). + * + * Roles rotate via atomic index swaps: + * - back: producer (GL thread) renders here, owns back_index exclusively + * - mailbox: latest complete frame + dirty flag + * - front: consumer (Flutter raster thread) displays, owns front_index + * + * mailbox_state packs both fields into ONE atomic — (dirty << 8) | index — + * so checking dirty and swapping the index cannot race. + * + * Producer: render to back → exchange (dirty=1, back_index) into mailbox, + * old mailbox index becomes the new back buffer. + * Consumer (drain-only): CAS-swap only when dirty=1; otherwise keep + * displaying the current front buffer. */ struct _TextureGL { FlTextureGL parent_instance; - - // The three buffers for mailbox model + RenderBuffer buffers[NUM_BUFFERS]; - - // Mailbox model: atomic indices for lock-free buffer swapping - // Producer (GL thread) owns back_index exclusively - // Consumer (main thread) owns front_index exclusively - // mailbox uses a combined atomic to avoid race between index and dirty flag - int back_index; // Producer's current buffer (GL thread only) - int front_index; // Consumer's current buffer (main thread only) - // Combined mailbox state: index in lower bits, dirty flag in upper bit - // This ensures atomic swap of both index and dirty flag together - // Encoding: (dirty << 8) | index, where dirty is 0 or 1, index is 0-2 - std::atomic mailbox_state; // Combined: dirty flag (bit 8) + buffer index (bits 0-7) - + + int back_index; // GL thread only + int front_index; // Raster thread only + std::atomic mailbox_state; // (dirty << 8) | index + guint32 current_width; guint32 current_height; gboolean buffers_initialized; gboolean initialization_posted; - std::atomic resizing; // Flag to indicate resize in progress - + std::atomic resizing; + VideoOutput* video_output; }; G_DEFINE_TYPE(TextureGL, texture_gl, fl_texture_gl_get_type()) +// GL texture names are only meaningful inside the share group that created +// them. |flutter_texture|s are created in populate (Flutter's raster context), +// but dispose runs on the platform thread where that context is never +// current — deleting there would leak the real texture, or delete an +// unrelated same-named object in GDK's own context. So dispose parks the +// names here, tagged with their creating context, and the next populate of +// any |TextureGL| reclaims exactly those entries whose context is current. +// The tag is essential with multiple Flutter engines in one process: a bare +// name must never be deleted in another engine's context. +typedef struct { + EGLContext context; // Raster context the name was created in. + guint32 texture; +} RetiredTexture; + +static std::mutex retired_textures_mutex; +static std::vector retired_textures; + +static void retire_flutter_texture(EGLContext context, guint32 texture) { + std::lock_guard lock(retired_textures_mutex); + retired_textures.push_back({context, texture}); +} + +// Must be called with a Flutter raster context current. Entries belonging to +// other contexts (other engines) are left for their own populate. +static void drain_retired_textures() { + EGLContext current_context = eglGetCurrentContext(); + if (current_context == EGL_NO_CONTEXT) { + return; + } + std::vector reclaimable; + { + std::lock_guard lock(retired_textures_mutex); + auto it = retired_textures.begin(); + while (it != retired_textures.end()) { + if (it->context == current_context) { + reclaimable.push_back(it->texture); + it = retired_textures.erase(it); + } else { + ++it; + } + } + } + for (guint32 texture : reclaimable) { + glDeleteTextures(1, &texture); + } +} + static void texture_gl_init(TextureGL* self) { for (int i = 0; i < NUM_BUFFERS; i++) { self->buffers[i].fbo = 0; self->buffers[i].texture = 0; self->buffers[i].egl_image = EGL_NO_IMAGE_KHR; self->buffers[i].flutter_texture = 0; + self->buffers[i].flutter_context = EGL_NO_CONTEXT; self->buffers[i].flutter_texture_valid = FALSE; self->buffers[i].render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_relaxed); } - // Initialize mailbox model indices - // back=0 for producer, front=1 for consumer, mailbox=2 initially (not dirty) + // back=0, front=1, mailbox=2 (not dirty). self->back_index = 0; self->front_index = 1; - // mailbox_state = (dirty << 8) | index = (0 << 8) | 2 = 2 self->mailbox_state.store(2, std::memory_order_relaxed); self->current_width = 1; @@ -114,45 +141,45 @@ static void texture_gl_dispose(GObject* object) { VideoOutput* video_output = self->video_output; GLRenderThread* gl_thread = video_output_get_gl_render_thread(video_output); - // Clean up Flutter's textures (main thread) + // Flutter-side textures belong to the raster context; park them for the + // next populate instead of deleting in whatever context is current here. for (int i = 0; i < NUM_BUFFERS; i++) { if (self->buffers[i].flutter_texture != 0) { - glDeleteTextures(1, &self->buffers[i].flutter_texture); + retire_flutter_texture(self->buffers[i].flutter_context, + self->buffers[i].flutter_texture); self->buffers[i].flutter_texture = 0; + self->buffers[i].flutter_context = EGL_NO_CONTEXT; } } - - // Clean up GPU resources in dedicated GL thread + + // Everything else must be released in the dedicated GL thread. if (video_output != NULL && gl_thread != NULL) { gl_thread->PostAndWait([self, video_output]() { EGLDisplay egl_display = video_output_get_egl_display(video_output); EGLContext egl_context = video_output_get_egl_context(video_output); - - // Clean up all buffers + for (int i = 0; i < NUM_BUFFERS; i++) { RenderBuffer* buf = &self->buffers[i]; - - // Clean up EGLSyncKHR + EGLSyncKHR sync = buf->render_sync.load(std::memory_order_acquire); if (sync != EGL_NO_SYNC_KHR) { eglDestroySyncKHR(egl_display, sync); buf->render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_release); } - - // Clean up EGLImage + if (buf->egl_image != EGL_NO_IMAGE_KHR) { eglDestroyImageKHR(egl_display, buf->egl_image); buf->egl_image = EGL_NO_IMAGE_KHR; } } - - // Clean up mpv's OpenGL resources (in mpv's isolated context) + + // mpv-side GL objects live in the isolated context. if (egl_context != EGL_NO_CONTEXT) { eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context); - + for (int i = 0; i < NUM_BUFFERS; i++) { RenderBuffer* buf = &self->buffers[i]; - + if (buf->texture != 0) { glDeleteTextures(1, &buf->texture); buf->texture = 0; @@ -165,7 +192,7 @@ static void texture_gl_dispose(GObject* object) { } }); } - + self->current_width = 1; self->current_height = 1; self->video_output = NULL; @@ -184,60 +211,56 @@ TextureGL* texture_gl_new(VideoOutput* video_output) { } /** - * Called from the dedicated GL rendering thread. - * Creates or resizes all three buffers for the mailbox model. + * Creates or resizes all three buffers. Runs in the dedicated GL thread. */ void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 required_height) { VideoOutput* video_output = self->video_output; - + if (required_width < 1 || required_height < 1) { return; } - + gboolean first_frame = !self->buffers_initialized; gboolean resize = self->current_width != (guint32)required_width || self->current_height != (guint32)required_height; - + if (!first_frame && !resize) { return; } - + EGLDisplay egl_display = video_output_get_egl_display(video_output); EGLContext egl_context = video_output_get_egl_context(video_output); - - // Switch to mpv's isolated context + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context); - - // Mark as resizing to prevent consumer from accessing buffers + + // Keep the consumer off the buffers while they are being recreated. self->resizing.store(TRUE, std::memory_order_release); - - // Free previous resources for all buffers + for (int i = 0; i < NUM_BUFFERS; i++) { RenderBuffer* buf = &self->buffers[i]; - + if (!first_frame) { - // Wait for any pending GPU work before destroying resources + // Wait for pending GPU work before destroying resources. EGLSyncKHR sync = buf->render_sync.load(std::memory_order_acquire); if (sync != EGL_NO_SYNC_KHR) { - eglClientWaitSyncKHR(egl_display, sync, + eglClientWaitSyncKHR(egl_display, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR); eglDestroySyncKHR(egl_display, sync); buf->render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_release); } - + if (buf->egl_image != EGL_NO_IMAGE_KHR) { eglDestroyImageKHR(egl_display, buf->egl_image); buf->egl_image = EGL_NO_IMAGE_KHR; } - + glDeleteTextures(1, &buf->texture); glDeleteFramebuffers(1, &buf->fbo); } - - // Create FBO and texture for this buffer + glGenFramebuffers(1, &buf->fbo); glBindFramebuffer(GL_FRAMEBUFFER, buf->fbo); - + glGenTextures(1, &buf->texture); glBindTexture(GL_TEXTURE_2D, buf->texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -246,12 +269,10 @@ void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, required_width, required_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - - // Attach texture to FBO + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, buf->texture, 0); - - // Create EGLImage from texture for sharing between contexts + EGLint egl_image_attribs[] = { EGL_NONE }; buf->egl_image = eglCreateImageKHR( egl_display, @@ -259,74 +280,59 @@ void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 EGL_GL_TEXTURE_2D_KHR, (EGLClientBuffer)(guintptr)buf->texture, egl_image_attribs); - + glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); - - // Mark Flutter texture as invalid (needs recreation) + + // Flutter-side texture must be recreated against the new EGLImage. buf->flutter_texture_valid = FALSE; buf->render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_release); } - - // Flush to ensure textures are ready + glFlush(); - - // Reset mailbox model indices + + // Reset mailbox: back=0, front=1, mailbox=2 (not dirty). self->back_index = 0; self->front_index = 1; - // mailbox_state = (dirty << 8) | index = (0 << 8) | 2 = 2 self->mailbox_state.store(2, std::memory_order_release); - - // Mark buffers as initialized and update dimensions + self->buffers_initialized = TRUE; self->current_width = required_width; self->current_height = required_height; - + self->resizing.store(FALSE, std::memory_order_release); } /** - * Renders mpv frame to the back buffer. - * Called from the dedicated GL rendering thread. + * Renders an mpv frame to the back buffer. Runs in the dedicated GL thread. */ gboolean texture_gl_render(TextureGL* self) { VideoOutput* video_output = self->video_output; EGLDisplay egl_display = video_output_get_egl_display(video_output); EGLContext egl_context = video_output_get_egl_context(video_output); mpv_render_context* render_context = video_output_get_render_context(video_output); - + if (!render_context || !self->buffers_initialized) { return FALSE; } - - // Get the back buffer (producer's exclusive buffer) - int back_idx = self->back_index; - RenderBuffer* back_buf = &self->buffers[back_idx]; - + + RenderBuffer* back_buf = &self->buffers[self->back_index]; if (back_buf->fbo == 0) { return FALSE; } - - // Before reusing this buffer, wait for any previous render to complete - // This ensures GPU has finished with this buffer before we overwrite it + + // GPU must be done with this buffer before it is overwritten. EGLSyncKHR old_sync = back_buf->render_sync.exchange(EGL_NO_SYNC_KHR, std::memory_order_acq_rel); if (old_sync != EGL_NO_SYNC_KHR) { - // Wait for previous GPU work to complete, then destroy the sync eglClientWaitSyncKHR(egl_display, old_sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR); eglDestroySyncKHR(egl_display, old_sync); } - - gint32 required_width = self->current_width; - gint32 required_height = self->current_height; - - // Switch to mpv's isolated context for rendering + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context); - - // Bind back buffer's FBO + glBindFramebuffer(GL_FRAMEBUFFER, back_buf->fbo); - - // Render mpv frame to back buffer's texture - mpv_opengl_fbo fbo{(gint32)back_buf->fbo, required_width, required_height, 0}; + mpv_opengl_fbo fbo{(gint32)back_buf->fbo, (gint32)self->current_width, + (gint32)self->current_height, 0}; int flip_y = 0; mpv_render_param params[] = { {MPV_RENDER_PARAM_OPENGL_FBO, &fbo}, @@ -334,39 +340,42 @@ gboolean texture_gl_render(TextureGL* self) { {MPV_RENDER_PARAM_INVALID, NULL}, }; mpv_render_context_render(render_context, params); - - // Unbind FBO glBindFramebuffer(GL_FRAMEBUFFER, 0); - - // Flush to ensure rendering commands are submitted to GPU + + // Submit commands, then publish a fence the consumer synchronizes against. glFlush(); - - // Create sync fence to mark render completion - // Consumer will use this for GPU-side synchronization EGLSyncKHR new_sync = eglCreateSyncKHR(egl_display, EGL_SYNC_FENCE_KHR, NULL); back_buf->render_sync.store(new_sync, std::memory_order_release); - + return TRUE; } /** - * Publishes the rendered frame using mailbox swap. - * Atomically swaps back buffer with mailbox and sets dirty flag in ONE operation. - * Called from dedicated GL thread after render finishes. + * Publishes the rendered frame: atomically moves back_index into the mailbox + * with dirty=1; the old mailbox buffer becomes the new back buffer. */ void texture_gl_swap_buffers(TextureGL* self) { - // Atomic swap with dirty flag set: - // We put our back_index into mailbox with dirty=1 - // We get back the old mailbox index (ignore its dirty flag) - int new_state = (1 << 8) | self->back_index; // dirty=1, index=back_index + int new_state = (1 << 8) | self->back_index; int old_state = self->mailbox_state.exchange(new_state, std::memory_order_acq_rel); - // Extract just the index from old state (ignore dirty flag) self->back_index = old_state & 0xFF; } +// 1x1 placeholder returned while buffers are unavailable. thread_local: each +// engine populates from its own raster thread, so the name never leaks into +// another engine's context. +static guint32 get_dummy_texture() { + static thread_local guint32 dummy_texture = 0; + if (dummy_texture == 0) { + glGenTextures(1, &dummy_texture); + glBindTexture(GL_TEXTURE_2D, dummy_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + } + return dummy_texture; +} + /** - * Populates texture with video frame using mailbox model. - * Called from Flutter's main thread. + * Consumer side of the mailbox. Called from Flutter's raster thread. */ gboolean texture_gl_populate_texture(FlTextureGL* texture, guint32* target, @@ -378,81 +387,70 @@ gboolean texture_gl_populate_texture(FlTextureGL* texture, VideoOutput* video_output = self->video_output; GLRenderThread* gl_thread = video_output_get_gl_render_thread(video_output); EGLDisplay egl_display = video_output_get_egl_display(video_output); - - // Trigger initialization on first call + + // populate is the only place the FlTextureGL contract guarantees Flutter's + // raster context is current — reclaim parked texture names here. + drain_retired_textures(); + + // Kick off buffer initialization on first call. if (!self->initialization_posted && !self->buffers_initialized) { gint64 required_width = video_output_get_width(video_output); gint64 required_height = video_output_get_height(video_output); - + if (required_width > 0 && required_height > 0 && gl_thread) { self->initialization_posted = TRUE; video_output_notify_render(video_output); } } - - // If resize is in progress, return dummy texture + if (self->resizing.load(std::memory_order_acquire)) { *target = GL_TEXTURE_2D; - static guint32 dummy_texture = 0; - if (dummy_texture == 0) { - glGenTextures(1, &dummy_texture); - glBindTexture(GL_TEXTURE_2D, dummy_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(GL_TEXTURE_2D, 0); - } - *name = dummy_texture; + *name = get_dummy_texture(); *width = 1; *height = 1; return TRUE; } - - // Drain-only consumer: only swap if mailbox has new content (dirty flag set) - // Use CAS loop to atomically check dirty and swap in one operation + + // Drain-only: CAS-swap our front buffer into the mailbox only when it holds + // a new frame (dirty bit 8 set); otherwise keep the current front buffer. int current_state = self->mailbox_state.load(std::memory_order_acquire); - while (current_state & 0x100) { // Check dirty flag (bit 8) - // Mailbox has new frame - try to swap - // New state: dirty=0, index=our front_index + while (current_state & 0x100) { int new_state = self->front_index; // dirty=0, index=front_index if (self->mailbox_state.compare_exchange_weak(current_state, new_state, std::memory_order_acq_rel, std::memory_order_acquire)) { - // Swap succeeded - extract the index we got self->front_index = current_state & 0xFF; break; } - // CAS failed, current_state has been updated, retry } - // If dirty was not set, we keep using current front buffer - - // front_index points to the frame we should display - int front_idx = self->front_index; - RenderBuffer* front_buf = &self->buffers[front_idx]; - - // GPU synchronization: ensure producer's rendering is complete before we use the texture - // Take ownership of the sync object atomically + + RenderBuffer* front_buf = &self->buffers[self->front_index]; + + // Take ownership of the producer's fence and wait on it — GPU-side when + // possible (queues the wait in Flutter's command stream without blocking). EGLSyncKHR sync = front_buf->render_sync.exchange(EGL_NO_SYNC_KHR, std::memory_order_acq_rel); if (sync != EGL_NO_SYNC_KHR) { - // Use GPU-side wait for better performance (doesn't block CPU) - // This inserts a wait into Flutter's GL command stream if (epoxy_has_egl_extension(egl_display, "EGL_KHR_wait_sync")) { eglWaitSyncKHR(egl_display, sync, 0); } else { - // Fallback to CPU wait if eglWaitSyncKHR not available eglClientWaitSyncKHR(egl_display, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR); } - // Destroy the sync after use (we own it now) eglDestroySyncKHR(egl_display, sync); } - - // Check if we need to create/recreate Flutter texture for this buffer + + // (Re)bind Flutter's texture to this buffer's EGLImage if invalidated. if (!front_buf->flutter_texture_valid && front_buf->egl_image != EGL_NO_IMAGE_KHR) { - // Delete old texture if exists if (front_buf->flutter_texture != 0) { - glDeleteTextures(1, &front_buf->flutter_texture); + if (front_buf->flutter_context == eglGetCurrentContext()) { + glDeleteTextures(1, &front_buf->flutter_texture); + } else { + retire_flutter_texture(front_buf->flutter_context, + front_buf->flutter_texture); + } } - - // Create Flutter's texture from this buffer's EGLImage + glGenTextures(1, &front_buf->flutter_texture); + front_buf->flutter_context = eglGetCurrentContext(); glBindTexture(GL_TEXTURE_2D, front_buf->flutter_texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -460,31 +458,22 @@ gboolean texture_gl_populate_texture(FlTextureGL* texture, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, front_buf->egl_image); glBindTexture(GL_TEXTURE_2D, 0); - + front_buf->flutter_texture_valid = TRUE; - - // Notify Flutter about texture availability + video_output_notify_texture_update(video_output); } - + *target = GL_TEXTURE_2D; *name = front_buf->flutter_texture; *width = self->current_width; *height = self->current_height; - - // If texture is not valid yet, return dummy texture - if (!front_buf->flutter_texture_valid || front_buf->flutter_texture == 0) { - static guint32 dummy_texture = 0; - if (dummy_texture == 0) { - glGenTextures(1, &dummy_texture); - glBindTexture(GL_TEXTURE_2D, dummy_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(GL_TEXTURE_2D, 0); - } - *name = dummy_texture; + + if (!front_buf->flutter_texture_valid) { + *name = get_dummy_texture(); *width = 1; *height = 1; } - + return TRUE; } diff --git a/media_kit_video/linux/utils.cc b/media_kit_video/linux/utils.cc index 386e81be7..1c3dcb57a 100644 --- a/media_kit_video/linux/utils.cc +++ b/media_kit_video/linux/utils.cc @@ -1,11 +1,10 @@ -// // This file is a part of media_kit -// // (https://github.com/media-kit/media-kit). -// // -// // Copyright © 2021 & onwards, Hitesh Kumar Saini . -// // All rights reserved. -// // Use of this source code is governed by MIT license that can be found in -// the -// // LICENSE file. +// This file is a part of media_kit +// (https://github.com/media-kit/media-kit). +// +// Copyright © 2021 & onwards, Hitesh Kumar Saini . +// All rights reserved. +// Use of this source code is governed by MIT license that can be found in the +// LICENSE file. #include "include/media_kit_video/utils.h" diff --git a/media_kit_video/linux/video_output.cc b/media_kit_video/linux/video_output.cc index 2a2b488e7..59e82961c 100644 --- a/media_kit_video/linux/video_output.cc +++ b/media_kit_video/linux/video_output.cc @@ -12,17 +12,15 @@ #include "include/media_kit_video/gl_render_thread.h" #include -#include #include #include struct _VideoOutput { GObject parent_instance; TextureGL* texture_gl; - EGLDisplay egl_display; /* EGL display for mpv rendering (shared with flutter). */ - EGLConfig egl_config; /* EGL config from Flutter (for compatibility). */ - EGLContext egl_context; /* Isolated EGL context (non-shared). */ - EGLSurface egl_surface; /* Place holder surface for activating egl context */ + EGLDisplay egl_display; /* Same EGLDisplay the Flutter engine renders on. */ + EGLConfig egl_config; + EGLContext egl_context; /* Isolated (non-shared) context for mpv. */ guint8* pixel_buffer; TextureSW* texture_sw; GMutex mutex; /* Only used in S/W rendering. */ @@ -43,7 +41,7 @@ G_DEFINE_TYPE(VideoOutput, video_output, G_TYPE_OBJECT) static void video_output_dispose(GObject* object) { VideoOutput* self = VIDEO_OUTPUT(object); self->destroyed = TRUE; - + // Make sure that no more callbacks are invoked from mpv. if (self->render_context) { mpv_render_context_set_update_callback(self->render_context, NULL, NULL); @@ -53,11 +51,10 @@ static void video_output_dispose(GObject* object) { if (self->texture_gl) { fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture_gl)); - - // Clean up EGL resources in dedicated GL thread + + // EGL resources must be released in the dedicated GL thread. if (self->render_context != NULL || self->egl_context != EGL_NO_CONTEXT) { self->gl_render_thread->PostAndWait([self]() { - // Free mpv_render_context with our isolated EGL context if (self->render_context != NULL) { if (self->egl_context != EGL_NO_CONTEXT) { eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context); @@ -65,15 +62,13 @@ static void video_output_dispose(GObject* object) { mpv_render_context_free(self->render_context); self->render_context = NULL; } - - // Clean up EGL context if (self->egl_context != EGL_NO_CONTEXT) { eglDestroyContext(self->egl_display, self->egl_context); self->egl_context = EGL_NO_CONTEXT; } }); } - + g_object_unref(self->texture_gl); } // S/W @@ -101,7 +96,6 @@ static void video_output_init(VideoOutput* self) { self->egl_display = EGL_NO_DISPLAY; self->egl_config = NULL; self->egl_context = EGL_NO_CONTEXT; - self->egl_surface = EGL_NO_SURFACE; self->texture_sw = NULL; self->pixel_buffer = NULL; self->handle = NULL; @@ -118,7 +112,6 @@ static void video_output_init(VideoOutput* self) { } VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, - FlView* view, gint64 handle, VideoOutputConfiguration configuration, GLRenderThread* gl_render_thread) { @@ -138,46 +131,65 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, #endif gboolean hardware_acceleration_supported = FALSE; - - // Get EGL display and config in main thread (where Flutter context is available) - // Only attempt if hardware acceleration is enabled + if (self->configuration.enable_hardware_acceleration) { - EGLDisplay flutter_display = eglGetCurrentDisplay(); - EGLContext flutter_context = eglGetCurrentContext(); - - if (flutter_display != EGL_NO_DISPLAY && flutter_context != EGL_NO_CONTEXT) { - self->egl_display = flutter_display; - - // Get Flutter's EGL config by querying its context - EGLint config_id = 0; - if (eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id)) { - // Retrieve the actual EGLConfig from the config ID - EGLint num_configs = 0; - EGLint config_attribs[] = { EGL_CONFIG_ID, config_id, EGL_NONE }; - - if (eglChooseConfig(flutter_display, config_attribs, &self->egl_config, 1, &num_configs) && num_configs > 0) { - g_print("media_kit: VideoOutput: Got Flutter's EGL display (%p) and config (ID: %d)\n", - flutter_display, config_id); - - // Create texture_gl in main thread (needed by mpv callback) - self->texture_gl = texture_gl_new(self); - if (!fl_texture_registrar_register_texture( - texture_registrar, FL_TEXTURE(self->texture_gl))) { - g_printerr("media_kit: VideoOutput: Failed to register texture.\n"); - g_object_unref(self->texture_gl); - self->texture_gl = NULL; - self->egl_config = NULL; - } - } else { - g_printerr("media_kit: VideoOutput: Failed to get Flutter's EGL config by ID.\n"); - self->egl_config = NULL; - } + // The GTK embedder (FlOpenGLManager) renders with EGL + GLES2 on both + // X11 and Wayland, but its contexts are never current on the platform + // thread. EGL returns the same EGLDisplay for the same native display, + // so derive the engine's display from the GDK display — same-display is + // all EGLImage/EGLSync sharing requires (mpv's context is non-shared). + GdkDisplay* display = gdk_display_get_default(); + EGLDisplay egl_display = EGL_NO_DISPLAY; + if (epoxy_has_egl_extension(EGL_NO_DISPLAY, "EGL_EXT_platform_base")) { + if (GDK_IS_WAYLAND_DISPLAY(display)) { + egl_display = eglGetPlatformDisplayEXT( + EGL_PLATFORM_WAYLAND_EXT, gdk_wayland_display_get_wl_display(display), NULL); + } else if (GDK_IS_X11_DISPLAY(display)) { + egl_display = eglGetPlatformDisplayEXT( + EGL_PLATFORM_X11_EXT, gdk_x11_display_get_xdisplay(display), NULL); + } + } + + // eglQueryString(EGL_VERSION) != NULL means the engine already + // initialized this display. Otherwise this is not the EGL-based embedder + // (legacy GLX embedders are unsupported); fall through to S/W rendering. + if (egl_display != EGL_NO_DISPLAY && + eglQueryString(egl_display, EGL_VERSION) != NULL) { + // mpv renders into an FBO in a surfaceless context; any GLES2 config + // works since it never backs an actual surface. + const EGLint config_attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE, + }; + EGLint num_configs = 0; + if (eglChooseConfig(egl_display, config_attribs, &self->egl_config, 1, &num_configs) && + num_configs > 0) { + self->egl_display = egl_display; + g_print("media_kit: VideoOutput: Got engine EGL display (%p) with GLES2 config.\n", + egl_display); } else { - g_printerr("media_kit: VideoOutput: Failed to query Flutter's EGL config ID.\n"); + g_printerr("media_kit: VideoOutput: Failed to choose EGL config.\n"); self->egl_config = NULL; } } else { - g_printerr("media_kit: VideoOutput: Failed to get Flutter's EGL display or context.\n"); + g_printerr( + "media_kit: VideoOutput: H/W rendering requires the EGL-based " + "Flutter embedder.\n"); + } + + if (self->egl_display != EGL_NO_DISPLAY && self->egl_config != NULL) { + self->texture_gl = texture_gl_new(self); + if (!fl_texture_registrar_register_texture( + texture_registrar, FL_TEXTURE(self->texture_gl))) { + g_printerr("media_kit: VideoOutput: Failed to register texture.\n"); + g_object_unref(self->texture_gl); + self->texture_gl = NULL; + self->egl_config = NULL; + } } } @@ -187,45 +199,38 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, // Causes frame drops with `pulse` audio output. (SlotSun/dart_simple_live#42) // mpv_set_option_string(self->handle, "video-timing-offset", "0"); - if (self->texture_gl != NULL && - self->egl_display != EGL_NO_DISPLAY && + if (self->texture_gl != NULL && + self->egl_display != EGL_NO_DISPLAY && self->egl_config != NULL) { - - // Bind OpenGL ES API (Flutter uses OpenGL ES on Linux) eglBindAPI(EGL_OPENGL_ES_API); - - // Create an isolated EGL context using Flutter's config - // Using the SAME egl_display and egl_config as Flutter for maximum compatibility + + // Isolated (non-shared) GLES2 context; frames are shared with Flutter + // via EGLImage on the same display, not via context share lists. const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; - - self->egl_context = eglCreateContext(self->egl_display, self->egl_config, + self->egl_context = eglCreateContext(self->egl_display, self->egl_config, EGL_NO_CONTEXT, context_attribs); - + if (self->egl_context != EGL_NO_CONTEXT) { - g_print("media_kit: VideoOutput: Created isolated EGL context: %p (display: %p, using Flutter's config)\n", - self->egl_context, self->egl_display); - - // Make our isolated context current for initialization (surfaceless) + // Surfaceless: mpv only ever renders into FBOs. if (eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context)) { - // Initialize mpv with our isolated EGL context mpv_opengl_init_params gl_init_params{ [](auto, auto name) { return (void*)eglGetProcAddress(name); }, NULL, }; - + mpv_render_param params[] = { {MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_OPENGL}, {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, (void*)&gl_init_params}, {MPV_RENDER_PARAM_INVALID, (void*)0}, {MPV_RENDER_PARAM_INVALID, (void*)0}, }; - - // VAAPI acceleration requires passing X11/Wayland display + + // VAAPI acceleration requires passing X11/Wayland display. GdkDisplay* display = gdk_display_get_default(); if (GDK_IS_WAYLAND_DISPLAY(display)) { params[2].type = MPV_RENDER_PARAM_WL_DISPLAY; @@ -234,7 +239,7 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, params[2].type = MPV_RENDER_PARAM_X11_DISPLAY; params[2].data = gdk_x11_display_get_xdisplay(display); } - + if (mpv_render_context_create(&self->render_context, self->handle, params) == 0) { mpv_render_context_set_update_callback( self->render_context, @@ -243,7 +248,7 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, if (self->destroyed) { return; } - // Asynchronously notify render (don't block mpv thread) + // Asynchronous: must not block mpv's thread. video_output_notify_render(self); }, self); @@ -263,11 +268,8 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, g_printerr("media_kit: VideoOutput: Failed to create isolated EGL context. Error: 0x%x\n", eglGetError()); } } - // If hardware acceleration is not supported or disabled, fall back to software rendering }); - // hardware_acceleration_supported is already set by the lambda - - // If hardware acceleration failed and texture was created, clean it up + if (!hardware_acceleration_supported && self->texture_gl != NULL) { fl_texture_registrar_unregister_texture(texture_registrar, FL_TEXTURE(self->texture_gl)); @@ -275,11 +277,10 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, self->texture_gl = NULL; } #ifdef MPV_RENDER_API_TYPE_SW + // H/W rendering unavailable; fall back to S/W rendering. if (!hardware_acceleration_supported) { g_printerr("media_kit: VideoOutput: S/W rendering.\n"); - // H/W rendering failed. Fallback to S/W rendering. self->pixel_buffer = g_new0(guint8, SW_RENDERING_PIXEL_BUFFER_SIZE); - self->texture_gl = NULL; self->texture_sw = texture_sw_new(self); if (fl_texture_registrar_register_texture(texture_registrar, FL_TEXTURE(self->texture_sw))) { @@ -381,10 +382,6 @@ EGLContext video_output_get_egl_context(VideoOutput* self) { return self->egl_context; } -EGLSurface video_output_get_egl_surface(VideoOutput* self) { - return self->egl_surface; -} - GLRenderThread* video_output_get_gl_render_thread(VideoOutput* self) { return self->gl_render_thread; } @@ -393,16 +390,10 @@ guint8* video_output_get_pixel_buffer(VideoOutput* self) { return self->pixel_buffer; } -gint64 video_output_get_width(VideoOutput* self) { - // Fixed width. - if (self->width) { - return self->width; - } - - // Video resolution dependent width. - gint64 width = 0; - gint64 height = 0; - +// Reads rotation-corrected display dimensions from mpv's video-out-params. +static void video_output_get_video_dimensions(VideoOutput* self, + gint64* out_width, + gint64* out_height) { mpv_node params; mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, ¶ms); @@ -426,12 +417,22 @@ gint64 video_output_get_width(VideoOutput* self) { mpv_free_node_contents(¶ms); } - width = rotate == 0 || rotate == 180 ? dw : dh; - height = rotate == 0 || rotate == 180 ? dh : dw; + *out_width = rotate == 0 || rotate == 180 ? dw : dh; + *out_height = rotate == 0 || rotate == 180 ? dh : dw; +} + +gint64 video_output_get_width(VideoOutput* self) { + // Fixed width. + if (self->width) { + return self->width; + } + + gint64 width = 0; + gint64 height = 0; + video_output_get_video_dimensions(self, &width, &height); if (self->texture_sw != NULL) { - // Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| & - // |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio. + // Clamp to S/W rendering limits while maintaining aspect ratio. if (width >= SW_RENDERING_MAX_WIDTH) { return SW_RENDERING_MAX_WIDTH; } @@ -449,39 +450,12 @@ gint64 video_output_get_height(VideoOutput* self) { return self->height; } - // Video resolution dependent height. gint64 width = 0; gint64 height = 0; - - mpv_node params; - mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, ¶ms); - - int64_t dw = 0, dh = 0, rotate = 0; - if (params.format == MPV_FORMAT_NODE_MAP) { - for (int32_t i = 0; i < params.u.list->num; i++) { - char* key = params.u.list->keys[i]; - auto value = params.u.list->values[i]; - if (value.format == MPV_FORMAT_INT64) { - if (strcmp(key, "dw") == 0) { - dw = value.u.int64; - } - if (strcmp(key, "dh") == 0) { - dh = value.u.int64; - } - if (strcmp(key, "rotate") == 0) { - rotate = value.u.int64; - } - } - } - mpv_free_node_contents(¶ms); - } - - width = rotate == 0 || rotate == 180 ? dw : dh; - height = rotate == 0 || rotate == 180 ? dh : dw; + video_output_get_video_dimensions(self, &width, &height); if (self->texture_sw != NULL) { - // Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| & - // |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio. + // Clamp to S/W rendering limits while maintaining aspect ratio. if (height >= SW_RENDERING_MAX_HEIGHT) { return SW_RENDERING_MAX_HEIGHT; } @@ -516,52 +490,41 @@ void video_output_notify_texture_update(VideoOutput* self) { } } -void video_output_notify_render(VideoOutput* self) { - if (self->destroyed || !self->gl_render_thread) { - return; - } - // Post combined check_and_resize + render task to GL thread (asynchronously) - self->gl_render_thread->Post([self]() { - video_output_check_and_resize(self); - video_output_render(self); - }); -} - -void video_output_check_and_resize(VideoOutput* self) { +// Both run in the dedicated GL thread. +static void video_output_check_and_resize(VideoOutput* self) { if (self->destroyed || !self->texture_gl) { return; } - - TextureGL* texture = self->texture_gl; + gint64 required_width = video_output_get_width(self); gint64 required_height = video_output_get_height(self); - if (required_width < 1 || required_height < 1) { return; } - - // Check if resize is needed through texture_gl - texture_gl_check_and_resize(texture, required_width, required_height); + + texture_gl_check_and_resize(self->texture_gl, required_width, required_height); } -void video_output_render(VideoOutput* self) { +static void video_output_render(VideoOutput* self) { if (self->destroyed) { return; } - - // H/W rendering with triple buffering + if (self->texture_gl && self->render_context) { - // Render to write buffer - gboolean rendered = texture_gl_render(self->texture_gl); - - // Only swap and notify if rendering was actually performed - if (rendered) { - // Publish the rendered frame (update buffer indices) + if (texture_gl_render(self->texture_gl)) { texture_gl_swap_buffers(self->texture_gl); - - // Notify Flutter that a new frame is available fl_texture_registrar_mark_texture_frame_available( self->texture_registrar, FL_TEXTURE(self->texture_gl)); } } } + +void video_output_notify_render(VideoOutput* self) { + if (self->destroyed || !self->gl_render_thread) { + return; + } + self->gl_render_thread->Post([self]() { + video_output_check_and_resize(self); + video_output_render(self); + }); +} diff --git a/media_kit_video/linux/video_output_manager.cc b/media_kit_video/linux/video_output_manager.cc index 454d1f58f..abc4ea9f9 100644 --- a/media_kit_video/linux/video_output_manager.cc +++ b/media_kit_video/linux/video_output_manager.cc @@ -12,8 +12,7 @@ struct _VideoOutputManager { GObject parent_instance; GHashTable* video_outputs; FlTextureRegistrar* texture_registrar; - FlView* view; - GLRenderThread* gl_render_thread; + GLRenderThread* gl_render_thread; /* Shared by all |VideoOutput|s. */ }; G_DEFINE_TYPE(VideoOutputManager, video_output_manager, G_TYPE_OBJECT) @@ -21,7 +20,7 @@ G_DEFINE_TYPE(VideoOutputManager, video_output_manager, G_TYPE_OBJECT) static void video_output_manager_init(VideoOutputManager* self) { self->video_outputs = g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, g_object_unref); - self->gl_render_thread = new GLRenderThread(); // Dedicated GL render thread + self->gl_render_thread = new GLRenderThread(); } static void video_output_manager_dispose(GObject* object) { @@ -36,12 +35,10 @@ static void video_output_manager_class_init(VideoOutputManagerClass* klass) { } VideoOutputManager* video_output_manager_new( - FlTextureRegistrar* texture_registrar, - FlView* view) { + FlTextureRegistrar* texture_registrar) { VideoOutputManager* video_output_manager = VIDEO_OUTPUT_MANAGER( g_object_new(video_output_manager_get_type(), nullptr)); video_output_manager->texture_registrar = texture_registrar; - video_output_manager->view = view; return video_output_manager; } @@ -52,7 +49,7 @@ void video_output_manager_create(VideoOutputManager* self, gpointer texture_update_callback_context) { if (!g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) { g_autoptr(VideoOutput) video_output = video_output_new( - self->texture_registrar, self->view, handle, configuration, self->gl_render_thread); + self->texture_registrar, handle, configuration, self->gl_render_thread); video_output_set_texture_update_callback( video_output, texture_update_callback, texture_update_callback_context); g_hash_table_insert(self->video_outputs, GINT_TO_POINTER(handle),