Validate a wgpu Texture in Flutter

Display a Vulkan wgpu scene in Flutter through DMA-BUF on Intel Wayland.

Introduction

30 Second Summary

A smooth animation can still hide an expensive detour between the graphics hardware and the window. Without visible evidence, you cannot tell whether the path you meant to test is the path actually running.

In this project, you will validate a live wgpu scene inside a Flutter Linux widget through GPU-only presentation. The finished probe identifies the active Vulkan adapter while exercising the shared DMA-BUF surface.

What You'll Build

You will demo a spinning cube inside a Flutter window while its diagnostic panel proves which backend drives the shared surface.

By the end of this project, you'll have:

  • A live wgpu scene that reaches a Flutter texture through a shared EGL surface without CPU pixel readback.
  • A runtime diagnostic panel showing the active backend, Intel device, driver, Flutter texture ID, surface size, and animation state.
  • A repeatable lifecycle test that keeps the cube visible through pause, resume, speed changes, resets, and window resizing.
  • Secret Mission: Retarget the shared-surface pipeline to the built-in particle scene and tune its GPU parameters.

What Do I Need Before Starting?

You need an Intel GPU supported by the installed Mesa Vulkan driver.

Your desktop session must use Wayland.

You also need internet access plus permission to install Arch Linux packages.

Before We Start

Before any setup, commit to validating the wgpu output-to-Flutter presentation boundary on its own. Separating it from the future GStreamer decoder-import boundary lets you isolate failures to one side.

Validate the Toolchain

A zero-copy texture can fail below Flutter. Missing native build tools or graphics drivers make Dart-level debugging misleading.

This step establishes the Arch Linux toolchain. It also validates Rust plus the Intel Vulkan stack.

The final capability gate confirms that your Wayland session can enumerate the intended Intel GPU. That gives the later DMA-BUF and EGL path a known-good host.

In this step, get ready to:
  • Prepare the native Linux build and graphics toolchain.
  • Validate compatible Flutter and Rust versions.
  • Confirm that Vulkan enumerates the intended Intel GPU.
Prepare the native host

The Linux bridge compiles native code before it can share a GPU image with Flutter. It also depends on the system EGL loader plus an Intel Vulkan driver.

  • Confirm the current desktop session type by running this command in a terminal:
loginctl show-session "$XDG_SESSION_ID" -p Type

What does this check?

The command asks the system login manager for the protocol used by your current session. You should see Type=wayland before continuing.

Not running a Wayland session?

Sign out of your desktop session. Select its Wayland option on the login screen before signing in again.

If the session still uses another protocol, help me identify how to start a Wayland session on my Arch Linux desktop.

Installing the package set changes system software. The package manager keeps packages that are already current.

  • Switch to a root shell using the administrator method configured on your Arch Linux system.
  • Install the pinned native package set by running:
pacman -S --needed git clang cmake ninja pkgconf gtk3 libglvnd mesa vulkan-intel vulkan-icd-loader vulkan-tools mesa-utils code

What does this package set provide?

  • The git package supports the source checkout in the next step.
  • The clang, cmake, ninja, and pkgconf packages support the native build.
  • The gtk3 package supplies the Linux desktop toolkit used by Flutter.
  • The libglvnd package supplies the EGL and GL loader libraries.
  • The mesa, vulkan-intel, and vulkan-icd-loader packages provide the Intel graphics runtime.
  • The vulkan-tools and mesa-utils packages provide the capability checks.
  • The code package installs Code - OSS.

The package manager should finish with every requested package installed or already current. That clears the native dependency layer for the texture bridge.

Package installation failing?

Confirm that the shell has root privileges. A normal user shell cannot change the system package set.

If a package download fails, refresh your configured Arch mirrors before retrying the same command. Help me troubleshoot my Arch package installation.

Validate Flutter

The package requires Flutter 3.38.0 or newer. This project targets the current stable release of 3.47.0 or a compatible newer stable version.

  • Check the installed Flutter version by running:
flutter --version

What does this check?

The command prints the Flutter SDK version available through your shell. Use that version to choose the matching path below.

✔️ I see version 3.47.0 or higher

Your Flutter SDK meets the project target. The next check inspects its Linux development dependencies.

  • Check the Flutter development environment by running:
flutter doctor

What does this check?

Flutter Doctor checks whether the SDK can find its development dependencies. The Linux desktop checks should recognize the compiler plus the GTK toolchain you installed.

Flutter is ready when the Linux desktop checks no longer report missing native dependencies.

ⓧ I see an older version

An older SDK can miss APIs expected by the pinned package. Moving to the stable channel aligns the SDK with this project.

  • Switch Flutter to the stable channel by running the first command below.
  • Upgrade the stable SDK by running the second command below:
flutter channel stable
flutter upgrade

What do these commands do?

The first command selects Flutter's stable release channel. The second command downloads the latest stable SDK available to your installation.

The upgrade can take several minutes while Flutter downloads SDK components. Quiet periods during that download are expected.

  • Verify the upgraded SDK plus its Linux environment by running:
flutter --version
flutter doctor

What should you confirm?

The version output should report Flutter 3.47.0 or a compatible newer stable version. The Linux desktop checks should find the native dependencies.

The SDK upgrade is complete once both checks reach those states.

ⓧ Command not found

Your shell cannot find a Flutter SDK. The official manual installation guide covers the download plus shell-specific path configuration.

  • Open the official Flutter manual installation guide.
  • Download the latest stable Linux SDK bundle.
  • Extract the SDK into a directory inside your home folder.
  • Add the SDK's bin directory to PATH using the instructions for your shell.
  • Start a new terminal after updating the shell configuration.
  • Verify the new installation by running:
flutter --version
flutter doctor

What should you confirm?

The first command should report Flutter 3.47.0 or a compatible newer stable version. The second command should find the installed Linux desktop dependencies.

That confirms the SDK is available from new terminal sessions.

Flutter checks still failing?

Confirm that the version shown comes from the SDK you intended to install. An older SDK earlier in PATH can hide the newer installation.

If Flutter Doctor still reports a Linux dependency problem, help me diagnose my Flutter Linux toolchain.

Install Rust and prove Intel Vulkan

The native renderer uses a pinned Rust compiler. The repository's toolchain file selects Rust 1.92.0 after the source is cloned.

The final gate checks the graphics path separately. A successful Vulkan summary must enumerate the intended Intel GPU.

  • Check the currently active Rust compiler by running:
rustc --version

What does this check?

The command confirms whether a Rust compiler is available through your shell. Choose the path that matches the version or error you see.

✔️ I see Rust 1.92.0

Rust 1.92.0 is already active. Your compiler matches the repository's pinned toolchain.

  • Continue to the final capability check below.

ⓧ I see an older version

Your current default compiler can remain installed. Rustup can add the pinned compiler alongside it.

  • Install the pinned Rust toolchain by running:
rustup toolchain install 1.92.0

What does this command do?

Rustup downloads Rust 1.92.0 as an available toolchain. The repository's toolchain file selects it automatically once you enter the cloned source.

The completed installation confirms that the pinned compiler is available without replacing your current default.

ⓧ Command not found

The rustup installer asks you to choose an installation option. Its default option installs the standard Rust toolchain plus the shell path configuration.

  • Install rustup by running this command:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

What does this command do?

The command downloads the official rustup installer over TLS. The installer places the Rust command-line tools in your user environment.

  • Choose the default installation option when the installer prompts you.
  • Start a new terminal after the installation completes.
  • Install the pinned compiler plus verify the active compiler by running:
rustup toolchain install 1.92.0
rustc --version

What should you confirm?

Rustup should confirm that Rust 1.92.0 is installed. The version command confirms that Rust is available through the new terminal.

The pinned compiler is now ready for the repository's toolchain file.

Rust installation not completing?

Open a new terminal if the shell still cannot find Rust after the installer finishes. New shells load the updated command path.

If the pinned toolchain download fails, help me troubleshoot my Rust 1.92.0 installation.

Before the final check, do you expect the Vulkan summary to name your Intel hardware or only a software renderer?

  • Run the final toolchain and GPU capability checks with these commands:
flutter --version
rustc --version
vulkaninfo --summary

How do you read the capability gate?

  • The Flutter output should report 3.47.0 or a compatible newer stable release.
  • The Rust output identifies the compiler currently active in this shell. The successful rustup installation confirms that 1.92.0 is available for the pinned repository.
  • The Vulkan summary should list the intended Intel GPU. A report containing only llvmpipe identifies a software renderer.

No Intel GPU in the Vulkan summary?

Treat a failure to enumerate Vulkan as a hard capability blocker. The same applies when the summary lists only llvmpipe.

Confirm that the vulkan-intel and vulkan-icd-loader packages installed successfully. Help me diagnose why Vulkan cannot see my Intel GPU.

That is the host capability gate cleared. Your next step runs the pinned reference and proves that this graphics stack can present a live wgpu surface inside Flutter.

Run the Zero-Copy Reference

Your toolchain check established that the Intel Vulkan runtime is available in your active Wayland session.

Now you can test whether Flutter can display a GPU surface produced by wgpu. The stock reference keeps its renderer details hidden. This run establishes the visible baseline that the next step will instrument.

In this step, get ready to:
  • Clone the published reference source at its pinned release commit.
  • Fetch the Flutter dependencies while retaining the checked-in Rust lockfile.
  • Prove the shared texture path through its visible spinning cube.
Pin the published reference

Git gives you a reproducible snapshot of the reference source. The exact commit prepared package version 0.2.1.

  • Switch back to the terminal from your toolchain checks.
  • Create the pinned source snapshot on your Desktop by running these commands:
cd ~/Desktop
git clone https://github.com/flowsai/flutter_wgpu_texture.git
cd flutter_wgpu_texture
git checkout -b zero-copy-probe dd8d6b5fbd4e58dcf167f38b72ea88d269f6fbca

What do these commands do?

  • The first line gives the repository a predictable home on your Desktop.
  • Git copies the canonical flutter_wgpu_texture repository into a local folder.
  • The zero-copy-probe branch keeps your project work separate from upstream development.
  • Commit dd8d6b5fbd4e58dcf167f38b72ea88d269f6fbca anchors the project to the source prepared for version 0.2.1.
  • Confirm the active branch and commit by running these checks:
git branch --show-current
git rev-parse HEAD

What do these checks prove?

  • The branch check confirms that later changes stay on your project branch.
  • The commit check confirms that your source matches the published release snapshot.

You should see zero-copy-probe on the first line. You should see dd8d6b5fbd4e58dcf167f38b72ea88d269f6fbca on the second line.

Repository not pinned correctly?

If the clone does not finish, confirm that your internet connection can reach GitHub. Repeat the clone command after the connection recovers.

If the branch or commit differs, return to the flutter_wgpu_texture folder. Repeat the checkout command from above.

Help me diagnose my repository state.

Fetch the Flutter dependencies

The example has its own Dart dependency graph. Its checked-in Cargo lockfile freezes the native Rust dependencies used by this release.

  • Enter the example directory by running this command:
cd example

Why use the example directory?

The example directory contains the Flutter desktop application that exercises the plugin. Running Flutter commands here targets the reference application.

  • Leave rust/Cargo.lock unchanged.
  • Fetch the Flutter dependencies by running:
flutter pub get

What does this dependency step preserve?

Flutter resolves the Dart packages required by the example. The checked-in lockfile keeps the native renderer on wgpu version 0.19.4.

Keeping that resolved dependency set removes moving package versions from your GPU investigation.

The command completes after fetching the required packages. Your terminal remains inside the example directory.

Dependencies not resolving?

If the download stops during a network failure, restore the connection. Repeat the dependency command above.

If Flutter reports an SDK compatibility problem, confirm that the active SDK is version 3.47.0 or a compatible newer stable version.

Help me troubleshoot the dependency output.

Launch the shared texture

The Linux bridge exports a Vulkan image through DMA-BUF.

EGL imports that shared handle into a GL texture. Flutter presents the result through FlTextureGL without CPU pixel readback.

The first native build can take several minutes while Cargo compiles the pinned Rust crates. A quiet terminal during that stretch is expected.

Before you run this, do you expect the validated host toolchain to be enough for the shared surface to initialize?

  • Build and launch the Linux reference application by running:
flutter run -d linux

What does this launch prove?

Flutter builds the Linux desktop host plus the pinned Rust renderer. The plugin then creates a shared GPU image for Flutter to composite.

A continuously moving scene proves that frame notifications keep the shared texture updating.

You'll see a spinning cube inside the Flutter window. The cube should continue moving without a CPU pixel-buffer bridge.

The terminal should include Linux DMA-BUF shared image created:. It should not include DMA-BUF populate failed.

Cube or DMA-BUF diagnostic missing?

If the window opens without the cube, inspect the terminal for a native build or initialization failure. Keep the application in the active Wayland session used for your capability checks.

If you see DMA-BUF populate failed, stop at this capability gate. The native EGL bridge could not import the shared DMA-BUF surface.

Help me diagnose the Linux texture failure.

That is the first visible win: Flutter is presenting a wgpu surface through the Linux sharing path. Next, you'll expose the selected renderer metadata beside the cube.

Make the GPU Path Observable

Your spinning cube is already crossing the Linux DMA-BUF bridge into Flutter. That visible result confirms the shared surface can present frames.

A moving image still leaves one critical question unanswered: which GPU path produced it? The current screen hides the backend.

You will add a diagnostic panel beside the live wgpu texture. The panel reveals whether Vulkan selected the intended Intel device.

In this step, get ready to:
  • Expose renderer metadata beside the live texture.
  • Align the widget test with the renamed probe app.
  • Verify the selected adapter against the Vulkan summary.
Replace the reference shell

The controller exposes runtime metadata through backendInfo, textureId, and size. A listener refreshes the panel when those values change.

  • Stop the running reference app by pressing Ctrl+C in its terminal.
  • In your code editor, select example/lib/main.dart from the cloned flutter_wgpu_texture folder.
  • Select all existing content in example/lib/main.dart.
  • Open the second tab below to access the exact replacement file.

✔️ Awesome, I've got everything!

Use this tab after your example/lib/main.dart file matches the diagnostic probe shown in the other tab.

ⓧ I'd like to double check the full code

Here is the complete example/lib/main.dart file:

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

void main() => runApp(const GpuProbeApp());

class GpuProbeApp extends StatefulWidget {
  const GpuProbeApp({super.key});

  @override
  State<GpuProbeApp> createState() => _GpuProbeAppState();
}

class _GpuProbeAppState extends State<GpuProbeApp> {
  late final FlutterWgpuTextureController controller;

  @override
  void initState() {
    super.initState();
    controller = FlutterWgpuTextureController(sceneType: 'cube');
    controller.addListener(_refresh);
  }

  void _refresh() {
    if (mounted) {
      setState(() {});
    }
  }

  @override
  void dispose() {
    controller.removeListener(_refresh);
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final backend = controller.backendInfo;
    final surfaceSize = controller.size;
    final dimensions = surfaceSize == null
        ? 'initializing'
        : '${surfaceSize.width.toInt()} x ${surfaceSize.height.toInt()}';

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(),
      home: Scaffold(
        backgroundColor: const Color(0xFF10151D),
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text('Zero-Copy wgpu Probe', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
                const SizedBox(height: 6),
                const Text('Spinning Cube', style: TextStyle(fontSize: 16, color: Colors.white70)),
                const SizedBox(height: 18),
                Expanded(child: DecoratedBox(decoration: BoxDecoration(color: const Color(0xFF182231), borderRadius: BorderRadius.circular(20), boxShadow: const [BoxShadow(color: Color(0x66000000), blurRadius: 18, offset: Offset(0, 8))]), child: Padding(padding: const EdgeInsets.all(16), child: FlutterWgpuTexture(controller: controller, placeholder: const ColoredBox(color: Color(0xFF182231), child: Center(child: CircularProgressIndicator())))))),
                const SizedBox(height: 14),
                Text('Initialized: ${controller.isInitialized}'),
                Text('Animating: ${controller.isAnimating}'),
                Text('Backend: ${backend?.backend ?? 'initializing'}'),
                Text('Device: ${backend?.deviceName ?? 'initializing'}'),
                Text('Driver: ${backend?.driver ?? 'initializing'}'),
                Text('Flutter texture ID: ${controller.textureId ?? 'initializing'}'),
                Text('Shared surface: $dimensions'),
                const SizedBox(height: 14),
                Wrap(spacing: 10, runSpacing: 10, children: [
                  FilledButton(onPressed: controller.isInitialized ? controller.startAnimation : null, child: const Text('Start')),
                  OutlinedButton(onPressed: controller.isInitialized ? controller.stopAnimation : null, child: const Text('Stop')),
                  OutlinedButton(onPressed: controller.isInitialized ? () async { await controller.setRotationSpeed(0.35); } : null, child: const Text('Slow')),
                  OutlinedButton(onPressed: controller.isInitialized ? () async { await controller.setRotationSpeed(1.8); } : null, child: const Text('Fast')),
                  OutlinedButton(onPressed: controller.isInitialized ? controller.resetScene : null, child: const Text('Reset')),
                ]),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

This complete file keeps the cube scene active. It adds the diagnostic state and controls around the existing texture widget.

  • Copy the complete file from the second tab.
  • Paste it over the selected content in example/lib/main.dart.
  • Save example/lib/main.dart.

How does the probe expose the path?

  • The FlutterWgpuTextureController keeps the built-in cube scene connected to the native renderer.
  • The _refresh() listener rebuilds the panel when controller state changes.
  • The backend value provides the selected graphics backend.
  • The surfaceSize value provides the current shared-image dimensions.
  • The controls send animation commands to the Rust-owned scene.

Before you launch the probe, which diagnostic values do you expect to stop showing initializing first?

  • Return to the terminal in flutter_wgpu_texture/example.
  • Launch the diagnostic probe by running:
flutter run -d linux

What should you see?

The Flutter window shows the spinning cube above a diagnostic panel. The panel fills in as the native texture initializes.

  • The backend changes to Backend: Vulkan.
  • The device line names an Intel GPU.
  • The driver line shows the active driver string.
  • The texture line shows an assigned Flutter texture ID.
  • The shared-surface line shows numeric dimensions.

Diagnostics stuck on initializing?

  • Confirm that example/lib/main.dart contains the controller.addListener(_refresh) line.
  • Stop the running app if the old reference shell remains visible.
  • Re-run the launch command above after saving the file.

Help me diagnose why my Flutter wgpu probe stays on initializing.

  • Stop the diagnostic probe by pressing Ctrl+C in its terminal.
Update the widget test

The checked-in widget test still targets the original app class. The updated test must load GpuProbeApp and check the new shell labels.

  • Select example/test/widget_test.dart in your code editor.
  • Replace its existing content with the code below:
import 'package:flutter_test/flutter_test.dart';

import 'package:flutter_wgpu_texture_spinning_cube/main.dart';

void main() {
  testWidgets('renders the zero-copy probe shell', (WidgetTester tester) async {
    await tester.pumpWidget(const GpuProbeApp());
    expect(find.text('Zero-Copy wgpu Probe'), findsOneWidget);
    expect(find.text('Spinning Cube'), findsOneWidget);
  });
}

What does this test cover?

  • The test pumps GpuProbeApp as the widget under test.
  • The first expectation checks the probe title.
  • The second expectation checks the cube scene label.
  • Save example/test/widget_test.dart.

✔️ Awesome, I've got everything!

Your widget test now imports GpuProbeApp and checks both probe labels.

ⓧ I'd like to double check the full code

Here is the complete example/test/widget_test.dart file:

import 'package:flutter_test/flutter_test.dart';

import 'package:flutter_wgpu_texture_spinning_cube/main.dart';

void main() {
  testWidgets('renders the zero-copy probe shell', (WidgetTester tester) async {
    await tester.pumpWidget(const GpuProbeApp());
    expect(find.text('Zero-Copy wgpu Probe'), findsOneWidget);
    expect(find.text('Spinning Cube'), findsOneWidget);
  });
}

This full-file reference matches the renamed app shell and its visible scene label.

Match the live adapter

The panel becomes useful when its device name agrees with the system capability check. This comparison connects the live Flutter texture to the intended Intel adapter.

Before you launch the final check, do you expect every diagnostic line to show a runtime value?

  • Return to the terminal in flutter_wgpu_texture/example.
  • Launch the completed probe by running:
flutter run -d linux

What proves the probe is live?

The cube continues to animate while the diagnostic panel displays runtime values. A non-null texture ID confirms that Flutter has registered the native texture.

The surface dimensions identify the shared image currently presented in the widget. These values update from the controller state.

Seeing the wrong adapter?

  • Check whether the device line names Intel hardware.
  • Stop here if the panel identifies only a software renderer.
  • Return to the Vulkan capability gate before continuing with the lifecycle tests.

Help me compare the probe device with my Vulkan adapter.

Before you query the system, do you expect the Intel device name to match the one shown inside the probe?

  • Open another terminal panel while the probe continues running.
  • Display the system Vulkan summary by running:
vulkaninfo --summary

What does this comparison prove?

The summary lists the GPUs available to Vulkan. Its Intel hardware entry should correspond to the device shown in the probe.

Matching names link the moving texture to the validated hardware adapter. The driver string provides another runtime clue when multiple devices are available.

  • Find the Intel hardware device in the Vulkan summary.
  • Compare its name with the value beside Device: in the probe.
  • Confirm that the probe shows Backend: Vulkan.
  • Confirm that the cube remains animated during the comparison.

You now have visible evidence that the shared texture comes from the intended Intel Vulkan path. The moving cube and matching adapter turn a successful picture into a diagnostic result.

That observability gap is closed. Next, you will stress the controls and force the shared surface through resize-driven recreation.

Exercise the Surface Lifecycle

Your Flutter probe now shows a live cube beside the active Vulkan adapter details. That gives you a clear baseline for the presentation path.

A single successful frame can hide stale handles or resize failures. This step stresses the wgpu to DMA-BUF bridge through repeated surface recreation.

You will first check the Dart project. You will then drive the Rust-owned scene through its existing controls.

In this step, get ready to:
  • Run static analysis and the probe shell test.
  • Verify that every animation control affects the cube.
  • Resize the window to recreate the shared surface.
Run the project checks

Static analysis catches invalid API usage before launch. The widget test confirms that the diagnostic shell still renders its expected labels.

Before you run these checks, do you expect the existing diagnostic shell to pass both?

  • Run the analyzer and widget test from the existing example terminal by running these commands:
flutter analyze
flutter test

What Do These Checks Prove?

  • The analysis command checks the project for invalid Dart code and API usage.
  • The widget test mounts GpuProbeApp and checks the two visible shell labels.

Both checks should complete successfully. That confirms the probe source is ready for runtime lifecycle testing.

Checks Not Passing?

  • Review the file path attached to each reported issue.
  • Compare your two source files with the tested references below.
  • Correct each local mismatch before rerunning the checks.

Help me fix the analysis or widget test failure.

✔️ Awesome, I've got everything!

Strong result. Both checks pass, so your diagnostic shell is ready for the lifecycle test.

ⓧ I'd like to double check the full code

  • Compare example/lib/main.dart with this tested reference:
import 'package:flutter/material.dart';
import 'package:flutter_wgpu_texture/flutter_wgpu_texture.dart';

void main() => runApp(const GpuProbeApp());

class GpuProbeApp extends StatefulWidget {
  const GpuProbeApp({super.key});

  @override
  State<GpuProbeApp> createState() => _GpuProbeAppState();
}

class _GpuProbeAppState extends State<GpuProbeApp> {
  late final FlutterWgpuTextureController controller;

  @override
  void initState() {
    super.initState();
    controller = FlutterWgpuTextureController(sceneType: 'cube');
    controller.addListener(_refresh);
  }

  void _refresh() {
    if (mounted) {
      setState(() {});
    }
  }

  @override
  void dispose() {
    controller.removeListener(_refresh);
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final backend = controller.backendInfo;
    final surfaceSize = controller.size;
    final dimensions = surfaceSize == null ? 'initializing' : '${surfaceSize.width.toInt()} x ${surfaceSize.height.toInt()}';
    return MaterialApp(debugShowCheckedModeBanner: false, theme: ThemeData.dark(), home: Scaffold(backgroundColor: const Color(0xFF10151D), body: SafeArea(child: Padding(padding: const EdgeInsets.all(24), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [const Text('Zero-Copy wgpu Probe', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)), const SizedBox(height: 6), const Text('Spinning Cube', style: TextStyle(fontSize: 16, color: Colors.white70)), const SizedBox(height: 18), Expanded(child: DecoratedBox(decoration: BoxDecoration(color: const Color(0xFF182231), borderRadius: BorderRadius.circular(20)), child: Padding(padding: const EdgeInsets.all(16), child: FlutterWgpuTexture(controller: controller, placeholder: const ColoredBox(color: Color(0xFF182231), child: Center(child: CircularProgressIndicator())))))), const SizedBox(height: 14), Text('Initialized: ${controller.isInitialized}'), Text('Animating: ${controller.isAnimating}'), Text('Backend: ${backend?.backend ?? 'initializing'}'), Text('Device: ${backend?.deviceName ?? 'initializing'}'), Text('Driver: ${backend?.driver ?? 'initializing'}'), Text('Flutter texture ID: ${controller.textureId ?? 'initializing'}'), Text('Shared surface: $dimensions'), const SizedBox(height: 14), Wrap(spacing: 10, runSpacing: 10, children: [FilledButton(onPressed: controller.isInitialized ? controller.startAnimation : null, child: const Text('Start')), OutlinedButton(onPressed: controller.isInitialized ? controller.stopAnimation : null, child: const Text('Stop')), OutlinedButton(onPressed: controller.isInitialized ? () async { await controller.setRotationSpeed(0.35); } : null, child: const Text('Slow')), OutlinedButton(onPressed: controller.isInitialized ? () async { await controller.setRotationSpeed(1.8); } : null, child: const Text('Fast')), OutlinedButton(onPressed: controller.isInitialized ? controller.resetScene : null, child: const Text('Reset'))])])))));
  }
}

What Should Match?

The controller keeps sceneType: 'cube' while the widget displays the live texture. The diagnostic lines read state from the same controller.

  • Compare example/test/widget_test.dart with this tested reference:
import 'package:flutter_test/flutter_test.dart';

import 'package:flutter_wgpu_texture_spinning_cube/main.dart';

void main() {
  testWidgets('renders the zero-copy probe shell', (WidgetTester tester) async {
    await tester.pumpWidget(const GpuProbeApp());
    expect(find.text('Zero-Copy wgpu Probe'), findsOneWidget);
    expect(find.text('Spinning Cube'), findsOneWidget);
  });
}

What Does This Test Protect?

The test mounts GpuProbeApp and checks the probe title. It also protects the Spinning Cube scene label.

Test the animation controls

The controls send commands through the existing controller while Flutter keeps the same texture widget mounted. A visible response proves that the native scene still accepts commands across the bridge.

The run command keeps this terminal busy while the probe window is open.

Before you launch the probe, which state do you expect the Animating line to show while the cube is moving?

  • Launch the existing Linux probe from the example terminal by running this command:
flutter run -d linux

What Does This Run Prove?

The command starts the existing desktop probe through the Linux target. The terminal remains attached so you can watch native shared-image diagnostics during the test.

You should see the cube moving with Animating: true in the diagnostic panel.

  • Click Stop.

You should see the cube pause. The panel should change to Animating: false.

  • Click Start.

You should see the cube resume. The panel should return to Animating: true.

  • Click Slow.

You should see the cube rotate more slowly.

  • Click Fast.

You should see the cube rotate faster.

  • Click Reset.

You should see the cube return to its reset state while the Flutter texture remains mounted.

Controls Not Responding?

  • Wait until the diagnostic panel shows Initialized: true before using a control.
  • Check the running terminal for a native failure if the loading indicator remains visible.
  • Confirm that the probe window still shows the cube before testing another control.

Help me debug unresponsive probe controls.

Recreate the shared surface

Resizing changes the dimensions required by the shared image. A healthy bridge replaces the old surface while keeping the cube visible.

Before you resize the window, do you expect the texture ID or the displayed surface dimensions to reveal the change first?

  • Switch back to the running probe window.
  • Drag one outer edge of the window to make the texture area wider.

You should see the shared surface dimensions update. The cube should remain visible.

  • Drag the same edge inward to make the texture area narrower.

You should see the dimensions change again. Rendering should continue inside the resized area.

  • Repeat the resize several times.
  • Switch back to the terminal running the probe.

You should see another Linux DMA-BUF shared image created: message after shared-image recreation. You should not see DMA-BUF populate failed.

Resize Test Failing?

  • Resize the outer application window enough to change the texture area.
  • Check whether the displayed shared surface dimensions change after the drag.
  • Capture the surrounding terminal output if DMA-BUF populate failed appears.

Help me diagnose a DMA-BUF resize failure.

You have now proved that the probe survives control changes and shared-surface recreation. The live cube remains connected to the Intel Vulkan path throughout the lifecycle test.

Secret mission

Retarget the Surface to GPU Particles

Retarget the existing shared surface from the cube to 2,000 animated GPU particles. Add controls for particle size and motion while keeping the same Vulkan DMA-BUF presentation path visible in the diagnostics.

Clean Up Your Resources

Clean Up Your Resources

You can keep the probe available, pause its Flutter process, or delete its local files. This project has no ongoing cloud costs.

Resources you used:

  • The active Flutter desktop process that renders the particle scene, if it is still running.
  • The local flutter_wgpu_texture Git repository containing the zero-copy-probe branch.
  • The probe files at example/lib/main.dart and example/test/widget_test.dart.

Keep everything running

This option preserves the running particle probe plus every local file. Choose it while you are still testing the shared surface.

  • Keep the Flutter process running while you continue testing the particle controls.
  • Keep the zero-copy-probe branch for further experiments.
  • Retain the validated system toolchain for future Flutter Linux projects.

Pause - I'll come back to this later

This option stops GPU rendering while preserving the repository. Your particle probe remains ready for another session.

  • Press Ctrl+C in the terminal that owns the Flutter process.

You should see the Flutter window close. The terminal returns to its prompt.

  • Switch back to the flutter_wgpu_texture/example directory from earlier when you are ready to continue.
  • Restart the preserved particle probe by running:
flutter run -d linux

What Should I See?

Flutter rebuilds the Linux app from the preserved branch. You should see 2,000 animated particles with the Vulkan and shared-surface diagnostics.

Delete - I don't want to use this again

Deleting the repository permanently removes your probe files plus the generated build artifacts. Your system-wide SDKs and graphics drivers remain installed for other projects.

  • Press Ctrl+C in the terminal that owns the Flutter process, if it is still running.

You should see the Flutter window close. The terminal returns to its prompt.

  • Delete the flutter_wgpu_texture repository from its parent directory by running these commands:
# Leave the example folder before removing the repository
cd ../..

# Delete the repository and its generated files
rm -rf flutter_wgpu_texture

# Confirm the repository no longer appears here
ls

What Does This Cleanup Do?

  • The first command moves the terminal out of the repository before deletion.
  • The second command removes the repository plus every file inside it.
  • The final command lists the remaining directories. You should no longer see flutter_wgpu_texture in the output.

Repository Still Listed?

Confirm that you started from flutter_wgpu_texture/example. The relative path depends on that starting location.

If the repository remains, help me check why the local repository was not deleted.

Nice Work!

Nice Work!

Excellent work! Your Flutter probe now displays live wgpu output through the validated Vulkan to DMA-BUF to EGL path without CPU pixel readback.

You've learned how to:

  • Render a live GPU scene inside a Flutter Linux widget through the shared texture bridge.
  • Expose runtime GPU diagnostics for the selected backend. Your probe identifies the Intel device, driver, Flutter texture ID, animation state, plus shared-surface size.
  • Exercise the shared-surface lifecycle through pause, resume, speed, reset, plus window-resize tests. The scene keeps rendering as the shared image is recreated.
  • Secret Mission: Retargeted the same bridge to the particle scene with 2,000 animated particles. The Small and Large controls tune point size. The Calm and Energetic controls tune motion scale.

Ready to quiz yourself?