Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions thermion_dart/native/src/opengl/linux/LinuxOpenGLContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ class ScopedEglThreadState {
EGLenum _api;
};

static std::string ResolveDrmDevicePath(EGLDisplay display) {
if (display != EGL_NO_DISPLAY) {
auto queryDisplayAttrib =
reinterpret_cast<PFNEGLQUERYDISPLAYATTRIBEXTPROC>(
eglGetProcAddress("eglQueryDisplayAttribEXT"));
auto queryDeviceString =
reinterpret_cast<PFNEGLQUERYDEVICESTRINGEXTPROC>(
eglGetProcAddress("eglQueryDeviceStringEXT"));
EGLAttrib deviceAttribute = 0;
if (queryDisplayAttrib && queryDeviceString &&
queryDisplayAttrib(
display, EGL_DEVICE_EXT, &deviceAttribute)) {
auto device = reinterpret_cast<EGLDeviceEXT>(deviceAttribute);
const char* path = queryDeviceString(
device, EGL_DRM_RENDER_NODE_FILE_EXT);
if (!path || path[0] == '\0') {
path = queryDeviceString(device, EGL_DRM_DEVICE_FILE_EXT);
}
if (path && path[0] != '\0') {
return path;
}
}
}

// Retain the historic default for non-Flutter/headless EGL stacks that do
// not expose EGL_EXT_device_query.
return "/dev/dri/renderD128";
}

class LinuxOpenGLContext::Impl {
public:
~Impl() {
Expand Down Expand Up @@ -117,14 +146,19 @@ class LinuxOpenGLContext::Impl {
explicit Impl(void* borrowedDisplay) {
std::cerr << "[ThermionGL:Context] Initializing EGL/GBM..." << std::endl;

// Step 1: Open DRM render node
_drmFd = open("/dev/dri/renderD128", O_RDWR);
// Step 1: Open the render node backing Flutter's EGLDisplay. Hardcoding
// renderD128 can select a different GPU on multi-GPU systems, making
// the exported DMA-BUF impossible for Flutter to import.
const std::string drmDevicePath = ResolveDrmDevicePath(
static_cast<EGLDisplay>(borrowedDisplay));
_drmFd = open(drmDevicePath.c_str(), O_RDWR);
if (_drmFd < 0) {
_lastError = "Failed to open /dev/dri/renderD128";
LOG_ERROR("Failed to open /dev/dri/renderD128");
_lastError = "Failed to open " + drmDevicePath;
std::cerr << "[ThermionGL:Context] " << _lastError << std::endl;
return;
}
std::cerr << "[ThermionGL:Context] DRM fd=" << _drmFd << std::endl;
std::cerr << "[ThermionGL:Context] DRM device=" << drmDevicePath
<< " fd=" << _drmFd << std::endl;

// Step 2: Create GBM device
_gbmDevice = gbm_create_device(_drmFd);
Expand Down
199 changes: 146 additions & 53 deletions thermion_flutter/thermion_flutter/linux/egl_texture.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cstring>
#include <iostream>
#include <vector>
#include <unistd.h>

#ifndef GL_TEXTURE_EXTERNAL_OES
#define GL_TEXTURE_EXTERNAL_OES 0x8D65
Expand All @@ -26,17 +27,33 @@ static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC s_glEGLImageTargetTexture2DOES = null
struct DeferredReadyResponse {
FlMethodCall* method_call;
int64_t texture_id;
ThermionTextureGL* texture;
};

static gboolean respond_texture_ready(gpointer user_data) {
auto* response = static_cast<DeferredReadyResponse*>(user_data);
g_autoptr(FlValue) result = fl_value_new_int(response->texture_id);
fl_method_call_respond(
response->method_call,
FL_METHOD_RESPONSE(fl_method_success_response_new(result)), nullptr);
if (response->texture->destroyed) {
fl_method_call_respond(
response->method_call,
FL_METHOD_RESPONSE(fl_method_error_response_new(
"DESTROYED",
"Texture destroyed before readiness response", nullptr)),
nullptr);
} else {
g_autoptr(FlValue) result = fl_value_new_int(response->texture_id);
fl_method_call_respond(
response->method_call,
FL_METHOD_RESPONSE(fl_method_success_response_new(result)),
nullptr);
}
return G_SOURCE_REMOVE;
}

static void destroy_deferred_ready_response(gpointer user_data) {
auto* response = static_cast<DeferredReadyResponse*>(user_data);
g_object_unref(response->method_call);
g_object_unref(response->texture);
delete response;
return G_SOURCE_REMOVE;
}

static void ensure_egl_procs() {
Expand All @@ -47,6 +64,21 @@ static void ensure_egl_procs() {
}
}

// Serializes populate() (Flutter's raster thread) against the plugin's
// release_texture() (platform thread). Every populate() return path unlocks.
struct TextureLockGuard {
explicit TextureLockGuard(GMutex& mutex) : mutex(mutex) {
g_mutex_lock(&mutex);
}
~TextureLockGuard() {
g_mutex_unlock(&mutex);
}
TextureLockGuard(const TextureLockGuard&) = delete;
TextureLockGuard& operator=(const TextureLockGuard&) = delete;

GMutex& mutex;
};

G_DEFINE_TYPE(ThermionTextureGL,
thermion_texture_gl,
fl_texture_gl_get_type())
Expand Down Expand Up @@ -84,12 +116,49 @@ thermion_texture_populate(FlTextureGL *texture,

ThermionTextureGL *self = THERMION_TEXTURE_GL(texture);

// Serialize against release_texture() on the platform thread. Either it
// completes first (and the destroyed check below makes populate a no-op)
// or this populate finishes first and the release observes the consumer
// resources it must clean up. Without this lock, a destroy issued while
// populate is importing the DMA-BUF can close the producer's fd mid-import.
TextureLockGuard lockGuard(self->lock);

if (self->destroyed) {
g_set_error(error, g_quark_from_static_string("thermion"), 4,
"Texture destroyed before populate");
return FALSE;
}

// This callback is the only place Flutter guarantees its raster context
// is current. Capture it for owner-aware cleanup for every transport,
// including Vulkan-produced DMA-BUF textures.
EGLContext flutterContext = eglGetCurrentContext();
EGLDisplay flutterDisplay = eglGetCurrentDisplay();
if (flutterContext != EGL_NO_CONTEXT &&
flutterDisplay != EGL_NO_DISPLAY &&
(thermion_flutter_render_context != flutterContext ||
thermion_flutter_render_display != flutterDisplay)) {
thermion_flutter_render_context = flutterContext;
thermion_flutter_render_display = flutterDisplay;
thermion_flutter_render_api = eglQueryAPI();

const char* version = reinterpret_cast<const char*>(
glGetString(GL_VERSION));
if (version) {
if (std::sscanf(version, "OpenGL ES %d.%d",
&thermion_flutter_render_gl_major,
&thermion_flutter_render_gl_minor) != 2) {
std::sscanf(version, "%d.%d",
&thermion_flutter_render_gl_major,
&thermion_flutter_render_gl_minor);
}
}
}

// The bootstrap texture is allocated while Flutter's render context is
// current, solely to capture that context before Filament initializes.
if (self->kind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP) {
if (self->gl_texture_id == 0) {
EGLContext flutterContext = eglGetCurrentContext();
EGLDisplay flutterDisplay = eglGetCurrentDisplay();
if (flutterContext == EGL_NO_CONTEXT ||
flutterDisplay == EGL_NO_DISPLAY) {
g_set_error(error, g_quark_from_static_string("thermion"), 1,
Expand Down Expand Up @@ -132,21 +201,6 @@ thermion_texture_populate(FlTextureGL *texture,

// Capture Flutter's render context for Filament initialization.
// This is the ONLY place where Flutter's render context is current.
thermion_flutter_render_context = flutterContext;
thermion_flutter_render_display = flutterDisplay;
thermion_flutter_render_api = eglQueryAPI();

const char* version = reinterpret_cast<const char*>(
glGetString(GL_VERSION));
if (version) {
if (std::sscanf(version, "OpenGL ES %d.%d",
&thermion_flutter_render_gl_major,
&thermion_flutter_render_gl_minor) != 2) {
std::sscanf(version, "%d.%d",
&thermion_flutter_render_gl_major,
&thermion_flutter_render_gl_minor);
}
}
TRACE( "[DirectPop] Captured Flutter render context=%p display=%p API=0x%x version=%d.%d\n",
(void*)thermion_flutter_render_context,
(void*)thermion_flutter_render_display,
Expand All @@ -158,16 +212,21 @@ thermion_texture_populate(FlTextureGL *texture,
// may immediately initialize another EGL client API when the
// Future completes. Queue the response on Flutter's platform loop
// so this raster callback has fully returned first.
if (self->pending_ready_call) {
for (guint i = 0; i < self->pending_ready_calls->len; i++) {
auto* methodCall = static_cast<FlMethodCall*>(
g_ptr_array_index(self->pending_ready_calls, i));
auto* response = new DeferredReadyResponse{
self->pending_ready_call,
methodCall,
static_cast<int64_t>(self->gl_texture_id),
THERMION_TEXTURE_GL(g_object_ref(self)),
};
self->pending_ready_call = nullptr;
g_idle_add_full(
G_PRIORITY_DEFAULT_IDLE, respond_texture_ready, response,
nullptr);
destroy_deferred_ready_response);
}
// Ownership of every FlMethodCall reference moved to its idle
// response.
g_ptr_array_set_size(self->pending_ready_calls, 0);
}

*target = GL_TEXTURE_2D;
Expand Down Expand Up @@ -259,42 +318,56 @@ thermion_texture_populate(FlTextureGL *texture,
static void thermion_texture_gl_dispose(GObject* object) {
ThermionTextureGL *self = THERMION_TEXTURE_GL(object);

// Clean up any pending deferred method call
if (self->pending_ready_call) {
fl_method_call_respond(self->pending_ready_call,
FL_METHOD_RESPONSE(fl_method_error_response_new(
"DESTROYED", "Texture destroyed before populate", nullptr)), nullptr);
g_object_unref(self->pending_ready_call);
self->pending_ready_call = nullptr;
}

if (self->kind == THERMION_TEXTURE_KIND_CONTEXT_BOOTSTRAP) {
// Bootstrap texture is owned by the plugin and deleted on a context in
// Flutter's share group.
// Nothing to clean up here — just zero out.
self->gl_texture_id = 0;
G_OBJECT_CLASS(thermion_texture_gl_parent_class)->dispose(object);
return;
// Native GL/EGL resources are released explicitly by the plugin while
// their owning contexts are available. GObject disposal only resolves
// outstanding method calls and releases their references. Disposal runs
// on the platform thread for textures that failed to register (never
// visible to the raster thread), so the mutex is not needed here; the
// plugin retains successfully-registered shells instead of unref'ing
// them (see release_texture) precisely so a late populate cannot touch
// a finalized object.
if (self->pending_ready_calls) {
for (guint i = 0; i < self->pending_ready_calls->len; i++) {
auto* methodCall = static_cast<FlMethodCall*>(
g_ptr_array_index(self->pending_ready_calls, i));
fl_method_call_respond(
methodCall,
FL_METHOD_RESPONSE(fl_method_error_response_new(
"DESTROYED", "Texture destroyed before populate",
nullptr)),
nullptr);
g_object_unref(methodCall);
}
g_ptr_array_set_size(self->pending_ready_calls, 0);
g_ptr_array_unref(self->pending_ready_calls);
self->pending_ready_calls = nullptr;
}

if (self->gl_texture_id != 0) {
glDeleteTextures(1, &self->gl_texture_id);
self->gl_texture_id = 0;
if (self->owns_dmabuf_fd && self->dmabuf_fd >= 0) {
close(self->dmabuf_fd);
}
self->dmabuf_fd = -1;
self->owns_dmabuf_fd = FALSE;

if (self->egl_image != EGL_NO_IMAGE_KHR && s_eglDestroyImageKHR) {
EGLDisplay display = eglGetCurrentDisplay();
if (display != EGL_NO_DISPLAY) {
s_eglDestroyImageKHR(display, self->egl_image);
}
self->egl_image = EGL_NO_IMAGE_KHR;
if (self->gl_texture_id != 0 ||
self->egl_image != EGL_NO_IMAGE_KHR) {
std::cerr
<< "[ThermionEGL] Texture disposed before explicit native cleanup"
<< std::endl;
}

G_OBJECT_CLASS(thermion_texture_gl_parent_class)->dispose(object);
}

static void thermion_texture_gl_finalize(GObject* object) {
ThermionTextureGL *self = THERMION_TEXTURE_GL(object);
g_mutex_clear(&self->lock);
G_OBJECT_CLASS(thermion_texture_gl_parent_class)->finalize(object);
}

void thermion_texture_gl_class_init(ThermionTextureGLClass* klass) {
G_OBJECT_CLASS(klass)->dispose = thermion_texture_gl_dispose;
G_OBJECT_CLASS(klass)->finalize = thermion_texture_gl_finalize;
FL_TEXTURE_GL_CLASS(klass)->populate = thermion_texture_populate;
}

Expand All @@ -304,6 +377,7 @@ void thermion_texture_gl_init(ThermionTextureGL* self) {
self->height = 0;
self->registrar = nullptr;
self->dmabuf_fd = -1;
self->owns_dmabuf_fd = FALSE;
self->stride = 0;
self->offset = 0;
self->drm_format = 0;
Expand All @@ -312,7 +386,9 @@ void thermion_texture_gl_init(ThermionTextureGL* self) {
self->initialized = FALSE;
self->surface_id = -1;
self->kind = THERMION_TEXTURE_KIND_DMA_BUF;
self->pending_ready_call = nullptr;
self->pending_ready_calls = g_ptr_array_new();
self->destroyed = FALSE;
g_mutex_init(&self->lock);
}

ThermionTextureGL* thermion_texture_gl_create(
Expand All @@ -324,7 +400,24 @@ ThermionTextureGL* thermion_texture_gl_create(
textureGL->width = info.width;
textureGL->height = info.height;
textureGL->registrar = registrar;
textureGL->dmabuf_fd = info.dmabuf_fd;
// Own a separate descriptor for the consumer-side EGLImage import so the
// producer closing its fd at teardown can never invalidate a concurrent
// or subsequent import. The DMA-BUF memory itself stays alive until every
// descriptor referencing it is closed.
const int consumerFd = dup(info.dmabuf_fd);
if (consumerFd >= 0) {
textureGL->dmabuf_fd = consumerFd;
textureGL->owns_dmabuf_fd = TRUE;
} else {
// Extremely unlikely (fd exhaustion); borrow the producer's
// descriptor instead. It is never closed on the consumer side — the
// producer's surface teardown owns it — and the populate/destroy
// mutex already prevents an import from racing that teardown.
std::cerr << "[ThermionEGL] dup() of dmabuf fd failed; borrowing producer fd"
<< std::endl;
textureGL->dmabuf_fd = info.dmabuf_fd;
textureGL->owns_dmabuf_fd = FALSE;
}
textureGL->stride = info.stride;
textureGL->offset = info.offset;
textureGL->drm_format = info.drm_format;
Expand Down
19 changes: 16 additions & 3 deletions thermion_flutter/thermion_flutter/linux/egl_texture.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ struct _ThermionTextureGL {
uint32_t width;
uint32_t height;
FlTextureRegistrar* registrar;
// dmabuf info for lazy EGL import (DMA-BUF path only)
// dmabuf info for lazy EGL import (DMA-BUF path only). dmabuf_fd is a
// consumer-owned dup() of the producer's descriptor, so producer teardown
// cannot invalidate a concurrent import.
int dmabuf_fd;
// Whether dispose() must close dmabuf_fd (FALSE only when dup() failed at
// creation and the producer's descriptor is merely borrowed).
gboolean owns_dmabuf_fd;
uint32_t stride;
uint32_t offset;
uint32_t drm_format;
Expand All @@ -46,8 +51,16 @@ struct _ThermionTextureGL {
gboolean initialized;
int64_t surface_id; // for Blit() and destruction
ThermionTextureKind kind;
// Deferred "awaitTextureReady" response (stored until populate creates the GL texture)
FlMethodCall* pending_ready_call;
// Deferred awaitTextureReady responses stored until populate creates the
// bootstrap GL texture. Each entry owns a FlMethodCall reference.
GPtrArray* pending_ready_calls;
// Set before native resources are released. Queued idle responses use this
// to avoid publishing a texture ID after cancellation.
gboolean destroyed;
// Serializes populate() (Flutter's raster thread) against the plugin's
// release_texture() (platform thread). Guards pending_ready_calls, the
// lazy import below, and the destroyed flag.
GMutex lock;
};

typedef struct _ThermionTextureGL ThermionTextureGL;
Expand Down
Loading
Loading