Skip to content

[Linux] FlPixelBufferTexture copy_pixels callback never called - Texture rendering fails #175887

@sobuur0

Description

@sobuur0

Steps to reproduce

  1. Create a Flutter Linux desktop application
  2. Implement a custom FlPixelBufferTexture in C using the Flutter Linux embedder
  3. Register the texture with fl_texture_registrar_register_texture()
  4. Mark texture frame available with fl_texture_registrar_mark_texture_frame_available()
  5. Display texture using Flutter's Texture widget
  6. Observe that copy_pixels callback is never invoked

Expected results

The copy_pixels callback function should be called by Flutter's rendering engine when the Texture widget needs to display the pixel data. This allows the native code to provide RGBA pixel buffer data to Flutter for rendering.

Actual results

The copy_pixels callback is never called, despite:

  • Successful texture registration (returns valid texture ID)
  • fl_texture_registrar_mark_texture_frame_available() returning TRUE
  • Texture widget being created and rebuilt multiple times
  • Timer continuously marking texture as available

This results in the Texture widget displaying nothing (transparent/background color only).

Code sample

Code sample

C Plugin Code (linux/runner/texture_test_plugin.c):

#include <flutter_linux/flutter_linux.h>
#include <gtk/gtk.h>
#include <glib.h>

typedef struct
{
    FlPixelBufferTexture parent_instance;
    uint8_t *pixels;
    int width;
    int height;
    gboolean frame_ready;
    GMutex pixel_mutex;
} TestPixelTexture;

typedef struct
{
    FlPixelBufferTextureClass parent_class;
} TestPixelTextureClass;

G_DEFINE_TYPE(TestPixelTexture, test_pixel_texture, fl_pixel_buffer_texture_get_type())

static gboolean test_pixel_texture_copy_pixels(FlPixelBufferTexture *texture,
                                              const uint8_t **out_buffer,
                                              uint32_t *width,
                                              uint32_t *height,
                                              GError **error)
{
    TestPixelTexture *t = (TestPixelTexture *)texture;

    // This debug message is NEVER printed
    g_print("!!! COPY_PIXELS CALLED !!!\n");
    fflush(stdout);

    g_mutex_lock(&t->pixel_mutex);

    if (!t->pixels || !t->frame_ready) {
        static uint8_t red[4] = {255, 0, 0, 255};
        *out_buffer = red;
        *width = 1;
        *height = 1;
        g_mutex_unlock(&t->pixel_mutex);
        return TRUE;
    }

    *out_buffer = t->pixels;
    *width = t->width;
    *height = t->height;

    g_mutex_unlock(&t->pixel_mutex);
    return TRUE;
}

static void test_pixel_texture_class_init(TestPixelTextureClass *klass)
{
    FL_PIXEL_BUFFER_TEXTURE_CLASS(klass)->copy_pixels = test_pixel_texture_copy_pixels;
    G_OBJECT_CLASS(klass)->dispose = test_pixel_texture_dispose;
}

static void test_pixel_texture_init(TestPixelTexture *t)
{
    t->pixels = NULL;
    t->width = 100;
    t->height = 100;
    t->frame_ready = FALSE;
    g_mutex_init(&t->pixel_mutex);
}

static TestPixelTexture *test_pixel_texture_new(int w, int h)
{
    TestPixelTexture *t = (TestPixelTexture *)g_object_new(test_pixel_texture_get_type(), NULL);
    t->width = w;
    t->height = h;
    size_t buffer_size = (size_t)w * h * 4;
    t->pixels = g_malloc0(buffer_size);
    t->frame_ready = FALSE;
    return t;
}

// Method handler that creates and registers texture
static FlMethodResponse *method_init(FlTextureRegistrar *registrar)
{
    static TestPixelTexture *g_test_texture = NULL;

    if (g_test_texture) {
        return FL_METHOD_RESPONSE(fl_method_error_response_new("ALREADY", "already initialized", NULL));
    }

    g_print("=== CREATING TEST TEXTURE ===\n");

    // Create and fill texture with green pixels
    g_test_texture = test_pixel_texture_new(100, 100);

    g_mutex_lock(&g_test_texture->pixel_mutex);
    for (int i = 0; i < 100 * 100 * 4; i += 4) {
        g_test_texture->pixels[i] = 0;     // R
        g_test_texture->pixels[i + 1] = 255; // G
        g_test_texture->pixels[i + 2] = 0;   // B
        g_test_texture->pixels[i + 3] = 255; // A
    }
    g_test_texture->frame_ready = TRUE;
    g_mutex_unlock(&g_test_texture->pixel_mutex);

    // Register texture - THIS WORKS
    int64_t texture_id = fl_texture_registrar_register_texture(registrar, FL_TEXTURE(g_test_texture));
    g_print("Texture registered with ID: %ld\n", texture_id);

    // Mark frame available - THIS WORKS (returns TRUE)
    gboolean result = fl_texture_registrar_mark_texture_frame_available(registrar, FL_TEXTURE(g_test_texture));
    g_print("mark_texture_frame_available returned: %s\n", result ? "TRUE" : "FALSE");

    // Set up timer to continuously mark texture available
    g_timeout_add(1000, (GSourceFunc)texture_timer_callback, registrar);

    g_autoptr(FlValue) result_value = fl_value_new_int(texture_id);
    return FL_METHOD_RESPONSE(fl_method_success_response_new(result_value));
}

Dart Code:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TextureTestWidget(),
    );
  }
}

class TextureTestWidget extends StatefulWidget {
  @override
  _TextureTestWidgetState createState() => _TextureTestWidgetState();
}

class _TextureTestWidgetState extends State<TextureTestWidget> {
  static const MethodChannel _platform = MethodChannel('texture_test');
  int? _textureId;

  @override
  void initState() {
    super.initState();
    _initTexture();
  }

  Future<void> _initTexture() async {
    try {
      final int textureId = await _platform.invokeMethod<int>('init') ?? -1;
      setState(() {
        _textureId = textureId;
      });
      print('Texture initialized with ID: $textureId');
    } catch (e) {
      print('Error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Texture Test')),
      body: _textureId == null
          ? Center(child: CircularProgressIndicator())
          : Container(
              width: double.infinity,
              height: double.infinity,
              color: Colors.blue, // Background to show texture is not rendering
              child: Center(
                child: Container(
                  width: 200,
                  height: 200,
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.red, width: 2),
                  ),
                  child: Texture(textureId: _textureId!),
                ),
              ),
            ),
    );
  }
}

Screenshots or Video

App displays blue background with red border, but the texture area remains empty/transparent instead of showing the expected green color from the pixel buffer. Image

Logs

Logs
=== CREATING TEST TEXTURE ===
Texture registered with ID: 1
mark_texture_frame_available returned: TRUE
Timer: marking texture available again
Timer: marking texture available again
Timer: marking texture available again
[... continues indefinitely, but NEVER shows "!!! COPY_PIXELS CALLED !!!" ]

**Full debug build log:**
```bash
$ fvm flutter build linux --debug
Building Linux application...
✓ Built build/linux/x64/debug/bundle/app_name

The texture registration succeeds, frame availability marking succeeds (returns TRUE), and the Texture widget is created successfully, but Flutter never calls the copy_pixels callback to retrieve the actual pixel data.

This has been tested with:
- Different texture sizes (1x1, 100x100, 1920x1080)
- Both static assignment and dynamic assignment of copy_pixels function
- Continuous frame availability marking via timer
- Multiple Flutter versions (3.29.1, 3.35.4)

The issue appears to be related to GitHub issue #101468 which discusses broader problems with Flutter Linux texture buffer lifecycle management.
</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
[!] Flutter (Channel stable, 3.29.1, on Ubuntu 24.04.3 LTS 6.8.0-83-generic, locale en_US.UTF-8) [772ms]
    • Flutter version 3.29.1 on channel stable at /home/sobuur/fvm/versions/3.29.1
    • Framework revision 09de023485 (7 months ago), 2025-02-28 13:44:05 -0800
    • Engine revision 871f65ac1b
    • Dart version 3.7.0
    • DevTools version 2.42.2

[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0-rc3) [8.3s]
    • Android SDK at /home/sobuur/Android/Sdk
    • Platform android-36, build-tools 36.0.0-rc3
    • ANDROID_HOME = /home/sobuur/Android/Sdk
    • ANDROID_SDK_ROOT = /home/sobuur/Android/Sdk
    • Java binary at: /home/sobuur/Downloads/android-studio-2023.2.1.25-linux/android-studio/jbr/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

[✓] Chrome - develop for the web [492ms]
    • Chrome at google-chrome

[✓] Linux toolchain - develop for Linux desktop [1,374ms]
    • Homebrew clang version 20.1.7
    • cmake version 4.0.3
    • ninja version 1.11.1
    • pkg-config version 1.8.1

[✓] Android Studio (version 2023.2) [449ms]
    • Android Studio at /home/sobuur/Downloads/android-studio-2023.2.1.25-linux/android-studio
    • Flutter plugin version 78.4.1
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)

[✓] VS Code (version 1.95.3) [22ms]
    • VS Code at /usr/share/code
    • Flutter extension version 3.118.0

[✓] Connected device (2 available) [1,333ms]
    • Linux (desktop) • linux  • linux-x64      • Ubuntu 24.04.3 LTS 6.8.0-83-generic
    • Chrome (web)    • chrome • web-javascript • Google Chrome 131.0.6778.69

[✓] Network resources [6.4s]
    • All expected network resources are available.

! Doctor found issues in 2 categories.

Metadata

Metadata

Assignees

No one assigned

    Labels

    a: desktopRunning on desktopengineflutter/engine related. See also e: labels.platform-linuxBuilding on or for Linux specificallyteam-linuxOwned by the Linux platform team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions