Repository navigation
[Impeller] Metal: PipelineLibraryMTL pipeline map is accessed from two threads without a lock (crash with Flutter GPU) #193721
Description
Steps to reproduce
- Create a macOS app from the code sample below and enable Flutter GPU (
FLTEnableFlutterGPUinInfo.plist). - Run it. A Flutter GPU scene draws every frame while the overlay keeps creating new Impeller pipeline variants (blurred text shadow, backdrop blur, changing blend mode).
A normal run crashes only rarely. With a ThreadSanitizer engine (tools/gn ... --tsan) the race is reported within seconds.
Expected results
Flutter GPU and Impeller can request pipelines at the same time without a race, as on Vulkan, where PipelineLibraryVK locks its maps.
Actual results
PipelineLibraryMTL reads and writes pipelines_ with no lock (still true on master). Flutter GPU looks pipelines up on the UI thread (RenderPass::Draw → GetPipeline) while the raster thread inserts new variants (ContentContext → CreateVariant → GetPipeline), so a lookup can hit a rehash.
- We hit it once in a release app on 3.47.1:
SIGSEGVinunordered_map::find, while the raster thread was building a pipeline for the first blurred text shadow. - TSan reports the race in the code sample on master.
- An engine test that looks pipelines up while two threads add new ones fails, hangs or crashes without a lock, and passes 40/40 with one.
SamplerLibraryMTL and PipelineLibraryGLES look similar and will be reported separately.
Code sample
pubspec.yaml and lib/main.dart
pubspec.yaml:
name: gpu_pipeline_race
publish_to: none
environment:
sdk: ^3.13.0
dependencies:
flutter:
sdk: flutter
flutter_scene: 0.23.0
vector_math: ^2.4.3
flutter:
uses-material-design: truelib/main.dart:
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart' as vm;
void main() => runApp(const MaterialApp(home: Repro()));
class Repro extends StatefulWidget {
const Repro({super.key});
@override
State<Repro> createState() => _ReproState();
}
class _ReproState extends State<Repro> with SingleTickerProviderStateMixin {
final Scene scene = Scene();
late final Ticker ticker;
int frame = 0;
@override
void initState() {
super.initState();
final geometry = CuboidGeometry(vm.Vector3.all(0.4));
final materials = [UnlitMaterial(), PhysicallyBasedMaterial()];
for (var i = 0; i < 400; i++) {
scene.add(
Node(mesh: Mesh(geometry, materials[i % 2]))
..localTransform = vm.Matrix4.translation(
vm.Vector3((i % 20 - 10) * 0.6, 0, (i ~/ 20 - 10) * 0.6),
),
);
}
ticker = createTicker((_) => setState(() => frame++))..start();
}
@override
void dispose() {
ticker.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mode = BlendMode.values[frame % BlendMode.values.length];
return Stack(
children: [
Positioned.fill(
child: SceneView(
scene,
camera: PerspectiveCamera(
position: vm.Vector3(0, 10, 14),
target: vm.Vector3.zero(),
),
),
),
Positioned(
left: 20,
top: 20,
width: 300,
height: 200,
child: ColorFiltered(
colorFilter: ColorFilter.mode(Colors.teal, mode),
child: ClipRect(
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 1.0 + frame % 20,
sigmaY: 1.0 + frame % 20,
),
blendMode: mode,
child: Text(
'frame $frame',
style: TextStyle(
fontSize: 40,
color: Colors.white,
shadows: [
Shadow(blurRadius: 1.0 + frame % 40, color: Colors.black),
],
),
),
),
),
),
),
],
);
}
}Screenshots or Video
N/A (crash).
Logs
Crash and TSan stacks
Release crash, 3.47.1 (EXC_BAD_ACCESS at 0x3f800000):
Thread 0 Crashed (main / UI thread):
std::unordered_map<PipelineDescriptor, PipelineFuture<...>>::find
impeller::PipelineLibraryMTL::GetPipeline
flutter::gpu::RenderPass::GetOrCreatePipeline
flutter::gpu::RenderPass::Draw
Thread 6 (io.flutter.raster), same moment:
-[AGXG16XFamilyDevice newRenderPipelineStateWithDescriptor:completionHandler:]
impeller::PipelineLibraryMTL::GetPipeline
impeller::Pipeline<PipelineDescriptor>::CreateVariant
impeller::ContentContext::GetTexturePipeline
impeller::GaussianBlurFilterContents::RenderFilter
impeller::TextShadowCache::LookupTSan, code sample on master d03768e:
WARNING: ThreadSanitizer: data race
Read by main thread:
__hash_table::find
impeller::PipelineLibraryMTL::GetPipeline
flutter::gpu::RenderPass::GetOrCreatePipeline
Previous write by thread T7 (raster):
__hash_table::__emplace_unique_key_args
impeller::PipelineLibraryMTL::GetPipeline
impeller::Pipeline<PipelineDescriptor>::CreateVariant
impeller::ContentContext::GetTexturePipelineFlutter Doctor output
flutter doctor -v
[✓] Flutter (Channel stable, 3.47.1, on macOS 26.7 25G229 darwin-arm64, locale en-CA) [1,710ms]
• Flutter version 3.47.1 on channel stable at /Users/<user>/development/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 6655482ec0 (6 weeks ago), 2026-08-19 10:07:23 -0700
• Engine revision 5d53178869
• Dart version 3.13.1
• DevTools version 2.60.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [4.4s]
• Android SDK at /Users/<user>/Library/Android/sdk
• Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
• Platform android-36.1, build-tools 36.1.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.10+-117844308-b1163.108)
• All Android licenses accepted.
[!] Xcode - develop for iOS and macOS (Xcode 27.0) [4.5s]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 27A266a
! iOS 27.0 Simulator not installed; this may be necessary for iOS and macOS development.
To download and install the platform, open Xcode, select Xcode > Settings > Components,
and click the GET button for the required platform.
For more information, please visit:
https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [7ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (3 available) [7.3s]
• iPhone 17 Pro (mobile) • <simulator-udid> • ios • com.apple.CoreSimulator.SimRuntime.iOS-26-4 (simulator)
• macOS (desktop) • macos • darwin-arm64 • macOS 26.7 25G229 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 154.0.8037.95
[✓] Network resources [1,093ms]
• All expected network resources are available.
! Doctor found issues in 1 category.
Activity
[Impeller] Guard the Metal pipeline library maps with a mutex (flutte…
[Impeller] Guard the sampler library caches with a mutex (flutter#193738