Repository navigation
Description
Steps to reproduce
-
Create an otherwise empty Flutter project and add the files in the Code sample section.
-
From the project root, start ChromeDriver in one terminal:
chromedriver --port=4444
-
In another terminal, run:
flutter pub get flutter drive \ --driver=test_driver/integration_test.dart \ --target=integration_test/shader_dispose_probe_test.dart \ -d chrome --release
-
Check the printed center-pixel RGBA values for each lifecycle case.
Expected results
The same recorded red-shader rectangle should sample as opaque red in all three cases, including when the shader is disposed after drawRect but before Picture.toImage, if the recorded Picture still needs the shader to render.
The Shader.dispose API documentation says disposal releases the shader's resources, but underlying memory is not freed while another object still needs it. Its example describes a Picture using an ImageShader; it does not explicitly guarantee the same lifetime behavior for a FragmentShader. This report asks whether the observed FragmentShader behavior is expected.
Actual results
Verified with Flutter 3.44.8 and Chrome 150.0.7871.187 on macOS:
| Lifecycle | Center RGBA |
|---|---|
Dispose before toImage |
(0, 0, 0, 255) |
Dispose after toImage |
(255, 0, 0, 255) |
| Retain through sampling | (255, 0, 0, 255) |
Code sample
Code sample
pubspec.yaml
name: shader_dispose_repro
publish_to: none
environment:
sdk: ^3.10.4
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter:
shaders:
- shaders/dispose_probe.fragshaders/dispose_probe.frag
#include <flutter/runtime_effect.glsl>
uniform vec4 color;
out vec4 fragColor;
void main() { fragColor = color; }integration_test/shader_dispose_probe_test.dart
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('sample shader-backed pictures', (tester) async {
final program = await ui.FragmentProgram.fromAsset(
'shaders/dispose_probe.frag',
);
Future<List<int>> sample(String lifecycle) async {
final shader = program.fragmentShader()..setFloat(0, 1);
shader.setFloat(1, 0);
shader.setFloat(2, 0);
shader.setFloat(3, 1);
final recorder = ui.PictureRecorder();
ui.Canvas(recorder).drawRect(
const ui.Rect.fromLTWH(0, 0, 32, 32),
ui.Paint()..shader = shader,
);
final picture = recorder.endRecording();
late ui.Image image;
if (lifecycle == 'dispose-before-toImage') {
shader.dispose();
image = await picture.toImage(32, 32);
} else {
image = await picture.toImage(32, 32);
if (lifecycle == 'dispose-after-toImage') shader.dispose();
}
final data = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
final pixel = [
for (var i = 0; i < 4; i++) data!.getUint8((16 * 32 + 16) * 4 + i),
];
print('$lifecycle center RGBA: $pixel');
image.dispose();
picture.dispose();
if (lifecycle == 'retain') shader.dispose();
return pixel;
}
for (final lifecycle in [
'dispose-before-toImage',
'dispose-after-toImage',
'retain',
]) {
await sample(lifecycle);
}
});
}test_driver/integration_test.dart
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.6.2 25G83 darwin-arm64, locale en-US) [447ms]
• Flutter version 3.44.8 on channel stable at /Users/agent/repos/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 058e0af2c2 (9 weeks ago), 2026-07-23 10:56:21 -0700
• Engine revision 0cd610717b
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Chrome - develop for the web [5ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Network resources [256ms]
• All expected network resources are available.
Activity
mbcorona commentedon Sep 25, 2026
Thanks for reporting @traison-diedrich .
routing to the web team for further investigation.
vi-k commentedon Oct 19, 2026
This also happens on screen, not only with
Picture.toImage, and I think I found the cause.Where it comes from. On CanvasKit,
CkFragmentShaderkeeps its uniforms in WASM memory (mallocFloat32List) and passes that array toRuntimeEffect.makeShaderingetSkShader. Thecanvaskit.jsglue copies the uniforms only for an array that is not already in WASM memory (!g._ckis passed as the "own the uniforms" flag), so for this array theSkShaderrefers to the same memory.CkFragmentShader.dispose()then callsfree(floats). The picture is rasterized after it has been recorded, so a shader disposed of right after the draw call reads freed memory, and whatever was allocated there next. The code is the same in 3.27.0 and 3.47.0.Smaller sample. Two boxes, red on the left and blue on the right, each filled by its own shader, which is disposed of right after
drawRect:shaders/solid.frag, listed underflutter: shaders:inpubspec.yaml:flutter build weband open it: both boxes are blue, because the freed uniforms of the red shader are reused by the blue one. Built with--dart-define=DISPOSE=false, the boxes are red and blue. Influtter testthe same painter draws red and blue withdispose().dispose()dispose()Flutter 3.47.0 (stable), Chrome 155.0.8059.80, macOS 26.6.2.
In a real app this shows as flicker: a package of mine draws a shadow with a fragment shader in a
CustomPainterand disposes of the shader after each paint. On the web the shadow changed colour from frame to frame, from whatever happened to be allocated in the freed block. TheShader.disposedocs say that the underlying memory is retained while another object that has not been disposed still needs it, so I'd expect the recorded picture to keep the uniforms alive, as it does off the web.[web] Fix CanvasKit fragment shader uniforms being freed or mutated a…
[web] Fix CanvasKit fragment shader uniforms being freed or mutated a…