Skip to content

[Impeller] Guard the Metal pipeline library maps with a mutex - #193722

Merged
auto-submit[bot] merged 2 commits into
flutter:masterfrom
leehack:impeller-metal-pipeline-library-lock
Oct 6, 2026
Merged

auto-submit[bot] merged 2 commits into
flutter:masterfrom
leehack:impeller-metal-pipeline-library-lock

Conversation

@leehack

@leehack leehack commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

PipelineLibraryMTL reads and writes its pipeline maps without a lock, but Flutter GPU calls GetPipeline on the UI thread while Impeller calls it on the raster thread. A lookup can hit a concurrent insert and crash in unordered_map::find. PipelineLibraryVK already guards its maps with a mutex.

This PR adds a mutex to PipelineLibraryMTL. In GetPipeline it covers only the map lookup and the insert, and is released before the Metal pipeline is created, so draws never wait on pipeline creation.

The new test, RendererTest.PipelineLibraryAllowsConcurrentGetPipeline, looks up a pipeline on one thread while two threads add new ones. It skips GLES.

Verified locally on master (macOS, M4 Max):

Without the lock With the lock
New test, 40 runs 20 pass, 15 fail, 4 hang, 1 crash 40/40 pass
New test under TSan data races in PipelineLibraryMTL, then crash no race on the maps
Repro app from the issue under TSan race reported no race on the maps

The macOS impeller_unittests (CI filter, 2426 passed, 0 failed) and metal_unittests pass, and the format and clang-tidy checks are clean.

#189899 (on master) makes these lookups rarer but does not remove them. SamplerLibraryMTL and PipelineLibraryGLES look similar and will be reported separately.

Fixes #193721

Pre-launch Checklist

Flutter GPU looks up render pipelines from the UI thread
(flutter::gpu::RenderPass::GetOrCreatePipeline) while Impeller creates
pipeline variants on the raster thread. PipelineLibraryVK guards its maps
with a mutex, but PipelineLibraryMTL did not, so a lookup on one thread
could race an insertion or rehash on the other and crash in
unordered_map::find.

Guard pipelines_ and compute_pipelines_ with a mutex. The lock covers only
the map accesses, not pipeline creation, and try_emplace makes concurrent
requests for the same descriptor share one future.

Add RendererTest.PipelineLibraryAllowsConcurrentGetPipeline, which looks
up an existing pipeline on one thread while two other threads create new
variants.
@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels Oct 2, 2026
@leehack
leehack marked this pull request as ready for review October 2, 2026 18:33

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces thread safety to the Metal pipeline library (PipelineLibraryMTL) in Impeller by protecting the pipeline maps (pipelines_ and compute_pipelines_) with a mutex (pipelines_mutex_). It also adds a concurrent unit test (PipelineLibraryAllowsConcurrentGetPipeline) to verify that pipelines can be safely retrieved and queried from multiple threads simultaneously. There are no review comments, and I have no feedback to provide.

@gaaclarke gaaclarke added the team-fluttergpu Owned by Flutter GPU team label Oct 5, 2026
@gaaclarke

Copy link
Copy Markdown
Member

fyi @bdero

@bdero bdero left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for tracking this down. It matches the locking PipelineLibraryVK already does, and releasing the lock before pipeline creation keeps UI-thread draws from waiting on compilation.

@bdero bdero added the CICD Run CI/CD label Oct 5, 2026

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm thanks

abdulawalarif pushed a commit to abdulawalarif/flutter that referenced this pull request Oct 11, 2026
)

`SamplerLibraryMTL`, `SamplerLibraryVK` and `SamplerLibraryGLES` cache
samplers in a plain vector that `GetSampler` scans and then appends to,
with no lock. Flutter GPU calls `GetSampler` on the UI thread
(`RenderPass.bindTexture`) while Impeller calls it on the raster thread
(for example `FramebufferBlendContents::Render`). A scan can read a
buffer that a concurrent `push_back` is reallocating, and two concurrent
inserts can corrupt the vector or drop a sampler that was already
returned.

This PR guards `samplers_` with a mutex in all three backends. On Metal
and Vulkan, the lock covers the lookup and the insert but not sampler
creation, and the insert checks the cache again so concurrent requests
share one sampler. GLES creates no device object, so it holds the lock
for the whole lookup.

The new test, `RendererTest.SamplerLibraryAllowsConcurrentGetSampler`,
starts four threads on a latch, has each request the same 36 samplers,
and checks that every thread gets the same valid sampler for each
descriptor.

Verified on master d03768e: Metal on macOS (M4 Max), and Vulkan
(SwiftShader) and OpenGL ES (Mesa, Xvfb) on Linux x64 with the CI
environment:

| | Without the lock | With the lock |
| --- | --- | --- |
| New test, 40 runs (Metal / Vulkan / GLES) | 0 / 1 / 0 pass | 40/40 on
each |
| New test under TSan | sampler races on all three | no sampler race |
| Repro app from the issue under TSan (Metal) | race in 4 of 6 runs |
none in 6 runs |

The macOS `impeller_unittests` CI filter (2426 passed, 0 failed) and the
Metal backend tests pass, and the format and clang-tidy checks are
clean. On Linux, the full `impeller_unittests` run was stopped early
with 1154 passed and 0 failed, and the runs used the previous revision
of the test, which started its threads with a spin barrier instead of a
latch.

Related: flutter#193721 / flutter#193722 (the same race in `PipelineLibraryMTL`). Both
PRs add a test to `renderer_unittests.cc`; merging them conflicts only
on the `#include <...>` lines.

Fixes flutter#193737

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…r#193722)

`PipelineLibraryMTL` reads and writes its pipeline maps without a lock,
but Flutter GPU calls `GetPipeline` on the UI thread while Impeller
calls it on the raster thread. A lookup can hit a concurrent insert and
crash in `unordered_map::find`. `PipelineLibraryVK` already guards its
maps with a mutex.

This PR adds a mutex to `PipelineLibraryMTL`. In `GetPipeline` it covers
only the map lookup and the insert, and is released before the Metal
pipeline is created, so draws never wait on pipeline creation.

The new test, `RendererTest.PipelineLibraryAllowsConcurrentGetPipeline`,
looks up a pipeline on one thread while two threads add new ones. It
skips GLES.

Verified locally on master (macOS, M4 Max):

| | Without the lock | With the lock |
| --- | --- | --- |
| New test, 40 runs | 20 pass, 15 fail, 4 hang, 1 crash | 40/40 pass |
| New test under TSan | data races in `PipelineLibraryMTL`, then crash |
no race on the maps |
| Repro app from the issue under TSan | race reported | no race on the
maps |

The macOS `impeller_unittests` (CI filter, 2426 passed, 0 failed) and
`metal_unittests` pass, and the format and clang-tidy checks are clean.

flutter#189899 (on master) makes these lookups rarer but does not remove them.
`SamplerLibraryMTL` and `PipelineLibraryGLES` look similar and will be
reported separately.

Fixes flutter#193721

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
)

`SamplerLibraryMTL`, `SamplerLibraryVK` and `SamplerLibraryGLES` cache
samplers in a plain vector that `GetSampler` scans and then appends to,
with no lock. Flutter GPU calls `GetSampler` on the UI thread
(`RenderPass.bindTexture`) while Impeller calls it on the raster thread
(for example `FramebufferBlendContents::Render`). A scan can read a
buffer that a concurrent `push_back` is reallocating, and two concurrent
inserts can corrupt the vector or drop a sampler that was already
returned.

This PR guards `samplers_` with a mutex in all three backends. On Metal
and Vulkan, the lock covers the lookup and the insert but not sampler
creation, and the insert checks the cache again so concurrent requests
share one sampler. GLES creates no device object, so it holds the lock
for the whole lookup.

The new test, `RendererTest.SamplerLibraryAllowsConcurrentGetSampler`,
starts four threads on a latch, has each request the same 36 samplers,
and checks that every thread gets the same valid sampler for each
descriptor.

Verified on master d03768e: Metal on macOS (M4 Max), and Vulkan
(SwiftShader) and OpenGL ES (Mesa, Xvfb) on Linux x64 with the CI
environment:

| | Without the lock | With the lock |
| --- | --- | --- |
| New test, 40 runs (Metal / Vulkan / GLES) | 0 / 1 / 0 pass | 40/40 on
each |
| New test under TSan | sampler races on all three | no sampler race |
| Repro app from the issue under TSan (Metal) | race in 4 of 6 runs |
none in 6 runs |

The macOS `impeller_unittests` CI filter (2426 passed, 0 failed) and the
Metal backend tests pass, and the format and clang-tidy checks are
clean. On Linux, the full `impeller_unittests` run was stopped early
with 1154 passed and 0 failed, and the runs used the previous revision
of the test, which started its threads with a spin barrier instead of a
latch.

Related: flutter#193721 / flutter#193722 (the same race in `PipelineLibraryMTL`). Both
PRs add a test to `renderer_unittests.cc`; merging them conflicts only
on the `#include <...>` lines.

Fixes flutter#193737

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels. team-fluttergpu Owned by Flutter GPU team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Impeller] Metal: PipelineLibraryMTL pipeline map is accessed from two threads without a lock (crash with Flutter GPU)

3 participants