Repository navigation
Description
Steps to reproduce
- Run a Flutter app with Impeller on Vulkan, with
VK_LAYER_KHRONOS_validationenabled and synchronization validation on (VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation,VK_VALIDATION_VALIDATE_SYNC=1). - Start the app.
Found on a Raspberry Pi 5 / CM5 (V3D 7.1, Mesa 26.2.1, validation layer 1.4.309) running Impeller Vulkan through a custom embedder. All three messages appeared within about 30 seconds of every launch.
Expected results
No synchronization hazards reported.
Actual results
Three hazards logged at startup.
BlitPassVK::OnCopyTextureToBufferCommand: the barrier that moves the source image toTransferSrcnames shader reads in the vertex and fragment stages as its destination. The next access isvkCmdCopyImageToBuffer, a transfer read.BlitPassVK::OnGenerateMipmapCommand: the barrier that moves all mip levels toTransferDstallows only transfer reads. The next accesses arevkCmdBlitImagewrites into levels 1..N.RenderPassBuilderVK::Build: the incomingVK_SUBPASS_EXTERNALdependency covers only color attachment output and fragment shader stages. A depth/stencil attachment's clear runs in the fragment test stages, so it isn't ordered after the attachment's layout transition.
Code sample
The hazards come from the engine's barrier setup, not from app code. In our app all three appeared at every launch. The readback and mipmap hazards need a texture readback and a mipmapped texture respectively, which our app performs at startup. (I'm not sure why, frankly, it's not explicit and I'm not familiar enough with graphics to know what those are)
Screenshots or Video
Logs
Logs
Validation Error: [ SYNC-HAZARD-READ-AFTER-WRITE ] vkCmdCopyImageToBuffer(): READ_AFTER_WRITE hazard detected. vkCmdCopyImageToBuffer reads VkImage 0x…, which was previously written during an image layout transition initiated by vkCmdPipelineBarrier. The current synchronization allows VK_ACCESS_2_SHADER_READ_BIT accesses at VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT|VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, but to prevent this hazard, it must allow VK_ACCESS_2_TRANSFER_READ_BIT accesses at VK_PIPELINE_STAGE_2_COPY_BIT.
Validation Error: [ SYNC-HAZARD-WRITE-AFTER-WRITE ] vkCmdBlitImage(): WRITE_AFTER_WRITE hazard detected. vkCmdBlitImage writes to VkImage 0x…, which was previously written during an image layout transition initiated by vkCmdPipelineBarrier. The current synchronization allows VK_ACCESS_2_TRANSFER_READ_BIT accesses at VK_PIPELINE_STAGE_2_COPY_BIT|VK_PIPELINE_STAGE_2_RESOLVE_BIT|VK_PIPELINE_STAGE_2_BLIT_BIT, but to prevent this hazard, it must allow VK_ACCESS_2_TRANSFER_WRITE_BIT accesses at VK_PIPELINE_STAGE_2_BLIT_BIT.
Validation Error: [ SYNC-HAZARD-WRITE-AFTER-WRITE ] vkCmdBeginRenderPass(): WRITE_AFTER_WRITE hazard detected: attachment loadOp access is not synchronized with the attachment layout transition. vkCmdBeginRenderPass clears the depth aspect of attachment 2 in subpass 0 (VkImageView 0x…, loadOp VK_ATTACHMENT_LOAD_OP_CLEAR), which was previously written during an image layout transition initiated by the same command. The current synchronization allows VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT accesses at VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, but to prevent this hazard, it must allow VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT accesses at VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT.Flutter Doctor output
Doctor output
This is the development machine. The hazards were found on a Raspberry Pi 5 / CM5 running Linux arm64, where the engine (built from this same 3.47.1 revision, 5d53178869) runs under a custom embedder (flutter-pi), so flutter doctor there wouldn't show the device either. The code involved is unchanged on master.
[✓] Flutter (Channel stable, 3.47.1, on macOS 15.7.1 24G231 darwin-arm64, locale en-US) [243ms]
• Flutter version 3.47.1 on channel stable at /Users/jpo/dev/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 6655482ec0 (9 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, enable-macos-arm64-only
[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,636ms]
• Android SDK at /Users/jpo/Library/Android/sdk
• Emulator version 35.5.10.0 (build_id 13402964) (CL:N/A)
• Platform android-36, build-tools 36.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 26.0.1) [1,659ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17A400
• 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) [10.2s]
• iPhone (wireless) (mobile) • <redacted> • ios • iOS 26.6.2 23G90
• macOS (desktop) • macos • darwin-arm64 • macOS 15.7.1 24G231 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 155.0.8059.85
[✓] Network resources [300ms]
• All expected network resources are available.
! Doctor found issues in 1 category.
Activity
suryaKommana2662 commentedon Oct 22, 2026
@jpohhhh Thanks for the detailed report and synchronization validation logs. I couldn't verify the issue in my Chromebook Linux container, as the required Vulkan validation environment is not currently available. Routing this to the
team-enginefor further investigation.gaaclarke commentedon Oct 26, 2026
We should test this out to see if it's legitimate or a false positive. Ideally things like this would get flagged automatically on CI.
andywolff commentedon Oct 27, 2026
Synchronization validation is disabled by default, so SYNC-HAZARD-* warnings are never generated. We could enable it in CI by setting VK_VALIDATION_VALIDATE_SYNC=1 or VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT, but I gather that the reason it's disabled by default is that it adds a lot of CPU cost (AI suggests it could be a 5x-10x slowdown). In @jpohhhh 's PRs they add individual unit tests which validate behavior on mocks, which will catch regressions in those specific areas. But it won't catch undiscovered sync hazards. Perhaps we should set up a new CI shard with sync validation enabled which runs a set of tests we select which exercise the areas of engine code most likely to contain sync hazards
andywolff commentedon Oct 28, 2026
Filed #195502 with my recommendation on how to cover this sort of thing
piotrbobinski-droid commentedon Nov 4, 2026
This hazard is visible on real hardware, not just under sync validation: hazard 1 (
OnCopyTextureToBufferCommand) corrupts pixels on a Mali GPU.Device: Samsung Galaxy Tab Active5 (Exynos 1380, Mali-G68 r38p1), Android 16, Impeller Vulkan, Flutter master
7b0a985e54(3.50.0-1.0.pre-209).What we see: an app renders text glyphs with
Picture.toImage()and reads them back right away withImage.toByteData(format: ImageByteFormat.rawRgba). A meaningful share of those first reads come back wrong, in three shapes:toImagerender;To tell them apart, we stamped a per-render nonce into the green and blue channels of every pixel (through a colour-matrix
saveLayer) and checked the whole read-back:ui.Image, ~1 frame later* Run A used an earlier version of the stamp, whose consecutive nonces were too close to tell a read of the immediately preceding render apart. Its count is a lower bound. Run B's stamp detects reads of any of the previous 83 renders.
The image itself is fine: sampling it on the GPU (
flutter_gpuTexture.fromImage) never showed the problem over hundreds of renders. A second read a frame later always matched. (Caveat:Texture.fromImagecurrently runsvkDeviceWaitIdlewhile it wraps the image, through the throwawayAiksContextinGetTextureFromImage. That observation shows the render completes correctly, not that sampling is synchronized on its own.) Both fit the barrier described above:copyImageToBufferruns in the transfer stage, but the barrier before it only orders shader reads (dst_stagevertex|fragment,dst_accessshader read). So the copy can run before the render's writes and the layout transition land.On Adreno 810 (also Vulkan) and Adreno 512 (OpenGL ES) the same code never read bad data.
Our workaround until #194991 lands: validate every read-back against the nonce, and re-read after a frame when it doesn't match.
[Impeller] Allow transfer writes after the Vulkan mipmap layout barri…
[Impeller] Use the transfer stage for the Vulkan readback barrier (fl…
jpohhhh commentedon Nov 5, 2026
@piotrbobinski-droid The readback fix landed in #194991 (merged Nov 5), so toImage → toByteData on Vulkan should be fixed on master now.