Repository navigation
Description
Steps to reproduce
I do not yet have a standalone minimal reproduction. The following steps
reproduce the failure in a larger Windows desktop application that embeds a
native Filament renderer. I am continuing to reduce this to a small plugin/app
sample.
-
Build and run the Windows application in debug mode with Impeller active.
Startup confirms:Using the Impeller rendering backend (OpenGLESSDF). -
Through the C++
TextureRegistrar, register a
kFlutterDesktopGpuSurfaceTexture. Its callback returns a
FlutterDesktopGpuSurfaceDescriptorbacked by a DXGI shared handle for a
D3D11 texture. -
Display the registered texture with a Dart
Texturewidget. A producer
thread continuously callsMarkTextureFrameAvailable; the public Windows
texture-registration API documents this call as valid from any thread. -
Push or pop the route containing the texture. The observed stack reaches
LayerTree::Flatten, consistent with the route/scene snapshot path.
The failure has occurred both several seconds after a route push, while the
texture was animating and without further interaction, and during route pop.
Expected results
Snapshotting or painting the GPU-surface texture should either use the
Impeller external-texture path or reject/skip an unsupported snapshot safely.
The application should not terminate with a native access violation.
Actual results
The raster thread terminates with:
Exception: 0xc0000005
Operation: READ
Address: 0x0
Module: flutter_windows.dll
RVA: +0x2c51f7
Two separate crashes were captured and symbolized against the SDK-cache PDB at
bin/cache/artifacts/engine/windows-x64/flutter_windows.dll.pdb. The PDB
CodeView identity matches the shipped DLL.
flutter_windows.dll+0x2c51f7 GrDirectContext::flush
flutter_windows.dll+0xeb4d1 EmbedderExternalTextureGL::ResolveTextureSkia
flutter_windows.dll+0xeaf0c EmbedderExternalTextureGL::ResolveTexture
flutter_windows.dll+0xeadbb EmbedderExternalTextureGL::Paint
flutter_windows.dll+0x5477a4 TextureLayer::Paint
ContainerLayer::PaintChildren /
TransformLayer::Paint /
ClipShapeLayer::Paint
flutter_windows.dll+0x5450fd LayerTree::Flatten
The faulting thread is Flutter's raster thread, not the platform thread or the
producer thread.
Source correlation
At engine revision
bbd15867c003dc66e678cb3c218649fa8bf914f2:
- Normal
LayerTree::Paintsupplies the frame's Aiks context:
flutter/engine/src/flutter/flow/layers/layer_tree.cc
Lines 117 to 133 in bbd1586
PaintContext context = { // clang-format off .state_stack = state_stack, .canvas = canvas, .gr_context = frame.gr_context(), .dst_color_space = sk_ref_sp(color_space), .view_embedder = frame.view_embedder(), .raster_time = frame.context().raster_time(), .ui_time = frame.context().ui_time(), .texture_registry = frame.context().texture_registry(), #if !SLIMPELLER .raster_cache = cache, #endif // !SLIMPELLER .impeller_enabled = !!frame.aiks_context(), .aiks_context = frame.aiks_context(), // clang-format on }; - The Impeller deferred-image snapshot path calls
LayerTree::Flattenwith
only the bounds and texture registry:
flutter/engine/src/flutter/lib/ui/painting/display_list_deferred_image_gpu_impeller.cc
Lines 170 to 184 in bbd1586
sk_sp<DisplayList> display_list; if (std::holds_alternative<sk_sp<DisplayList>>(content)) { display_list = std::get<sk_sp<DisplayList>>(std::move(content)); } else if (std::holds_alternative<std::unique_ptr<LayerTree>>( content)) { std::unique_ptr<LayerTree> layer_tree = std::get<std::unique_ptr<LayerTree>>(std::move(content)); display_list = layer_tree->Flatten( DlRect::MakeWH(wrapper->size_.width, wrapper->size_.height), snapshot_delegate->GetTextureRegistry()); } auto texture = snapshot_delegate->MakeImpellerSnapshotSync( display_list, wrapper->size_, wrapper->pixel_format_); LayerTree::Flattencreates a paint context with no Aiks context, and its
GrDirectContextargument defaults to null:
flutter/engine/src/flutter/flow/layers/layer_tree.h
Lines 45 to 51 in bbd1586
void Paint(CompositorContext::ScopedFrame& frame, bool ignore_raster_cache = false) const; sk_sp<DisplayList> Flatten( const DlRect& bounds, const std::shared_ptr<TextureRegistry>& texture_registry = nullptr, GrDirectContext* gr_context = nullptr);
flutter/engine/src/flutter/flow/layers/layer_tree.cc
Lines 147 to 192 in bbd1586
sk_sp<DisplayList> LayerTree::Flatten( const DlRect& bounds, const std::shared_ptr<TextureRegistry>& texture_registry, GrDirectContext* gr_context) { TRACE_EVENT0("flutter", "LayerTree::Flatten"); DisplayListBuilder builder(bounds); const FixedRefreshRateStopwatch unused_stopwatch; LayerStateStack preroll_state_stack; // No root surface transformation. So assume identity. preroll_state_stack.set_preroll_delegate(bounds); PrerollContext preroll_context{ // clang-format off #if !SLIMPELLER .raster_cache = nullptr, #endif // !SLIMPELLER .gr_context = gr_context, .view_embedder = nullptr, .state_stack = preroll_state_stack, .dst_color_space = nullptr, .surface_needs_readback = false, .raster_time = unused_stopwatch, .ui_time = unused_stopwatch, .texture_registry = texture_registry, // clang-format on }; LayerStateStack paint_state_stack; paint_state_stack.set_delegate(&builder); PaintContext paint_context = { // clang-format off .state_stack = paint_state_stack, .canvas = &builder, .gr_context = gr_context, .dst_color_space = nullptr, .view_embedder = nullptr, .raster_time = unused_stopwatch, .ui_time = unused_stopwatch, .texture_registry = texture_registry, #if !SLIMPELLER .raster_cache = nullptr, #endif // !SLIMPELLER // clang-format on }; EmbedderExternalTextureGL::ResolveTextureselects the Impeller resolver
only whenaiks_contextis non-null. Otherwise it selects
ResolveTextureSkia, which dereferencescontextwithout a null check:
flutter/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc
Lines 67 to 84 in bbd1586
sk_sp<DlImage> EmbedderExternalTextureGL::ResolveTexture( int64_t texture_id, GrDirectContext* context, impeller::AiksContext* aiks_context, const SkISize& size) { if (!!aiks_context) { return ResolveTextureImpeller(texture_id, aiks_context, size); } else { return ResolveTextureSkia(texture_id, context, size); } } sk_sp<DlImage> EmbedderExternalTextureGL::ResolveTextureSkia( int64_t texture_id, GrDirectContext* context, const SkISize& size) { context->flushAndSubmit(); context->resetContext(kAll_GrBackendState);
This is not an observed “Impeller resolution failed and then fell back to
Skia” sequence; the resolver selection is based directly on whether
aiks_context is present.
The failure is not deterministic on every texture paint. The GL wrapper caches
last_image_, and MarkNewFrameAvailable clears that cache. A flatten pass
can therefore reuse a previously resolved Impeller image, or attempt a new
resolution with the flatten paint context:
flutter/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc
Lines 41 to 76 in bbd1586
| void EmbedderExternalTextureGL::Paint(PaintContext& context, | |
| const DlRect& bounds, | |
| bool freeze, | |
| const DlImageSampling sampling) { | |
| if (last_image_ == nullptr) { | |
| last_image_ = | |
| ResolveTexture(Id(), // | |
| context.gr_context, // | |
| context.aiks_context, // | |
| SkISize::Make(bounds.GetWidth(), bounds.GetHeight()) // | |
| ); | |
| } | |
| DlCanvas* canvas = context.canvas; | |
| const DlPaint* paint = context.paint; | |
| if (last_image_) { | |
| DlRect image_bounds = DlRect::Make(last_image_->GetBounds()); | |
| if (bounds != image_bounds) { | |
| canvas->DrawImageRect(last_image_, image_bounds, bounds, sampling, paint); | |
| } else { | |
| canvas->DrawImage(last_image_, bounds.GetOrigin(), sampling, paint); | |
| } | |
| } | |
| } | |
| sk_sp<DlImage> EmbedderExternalTextureGL::ResolveTexture( | |
| int64_t texture_id, | |
| GrDirectContext* context, | |
| impeller::AiksContext* aiks_context, | |
| const SkISize& size) { | |
| if (!!aiks_context) { | |
| return ResolveTextureImpeller(texture_id, aiks_context, size); | |
| } else { | |
| return ResolveTextureSkia(texture_id, context, size); | |
| } |
flutter/engine/src/flutter/shell/platform/embedder/embedder_external_texture_gl.cc
Lines 192 to 204 in bbd1586
| // |flutter::Texture| | |
| void EmbedderExternalTextureGL::OnGrContextCreated() {} | |
| // |flutter::Texture| | |
| void EmbedderExternalTextureGL::OnGrContextDestroyed() {} | |
| // |flutter::Texture| | |
| void EmbedderExternalTextureGL::MarkNewFrameAvailable() { | |
| last_image_ = nullptr; | |
| } | |
| // |flutter::Texture| | |
| void EmbedderExternalTextureGL::OnTextureUnregistered() {} |
The same relevant routing and null-dereference structure is still present on
Flutter master as of commit
a6266833e3b7e23a6a36ec7df8982c217a881677, although I have not runtime-tested
that revision.
Backend A/B
The same application binary and navigation path were run with only the
renderer selection changed.
To disable Impeller for the control run:
$env:FLUTTER_ENGINE_SWITCHES = '1'
$env:FLUTTER_ENGINE_SWITCH_1 = 'enable-impeller=false'Results:
- Impeller: reproduced after route push and during route pop across
multiple launches/rebuilds. Two failures have matching symbolized stacks. - Skia: survived route push, a 45-second dwell with the texture animating,
route pop, and a second complete enter/exit cycle. - Prior Impeller negative run: one earlier run painted the texture
continuously for more than 10 minutes and then failed only on route pop.
I did not record a formal pass/fail denominator, so this A/B is presented as a
backend discriminator rather than proof that every Impeller run fails.
Code sample
A standalone sample is not yet available. The relevant registration shape is
approximately:
// Representative excerpt only; the texture variant is retained for the
// complete registered lifetime.
flutter::TextureVariant texture = flutter::GpuSurfaceTexture(
kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle,
[](size_t width, size_t height)
-> const FlutterDesktopGpuSurfaceDescriptor* {
return CurrentGpuSurfaceDescriptor();
});
const int64_t texture_id = registrar->RegisterTexture(&texture);
// Called at the producer's frame cadence from a worker thread.
registrar->MarkTextureFrameAvailable(texture_id);The returned descriptor's handle is the DXGI shared handle for the D3D11
texture. Dart displays the returned ID using:
Texture(textureId: textureId)Environment
- Flutter
3.47.0-0.1.pre, beta channel - Framework
bd1e75d918605c91b411e8789fb911e6c9a84534 - Engine
bbd15867c003dc66e678cb3c218649fa8bf914f2 - Windows 11 Pro build 26200, x64
- NVIDIA GeForce RTX 3080, driver 595.95
- Debug build:
flutter build windows --debug - Release/profile behavior has not been tested
- Runtime reproduction on current master/newest beta has not yet been tested
Logs
[IMPORTANT:flutter/shell/platform/embedder/embedder_surface_gl_impeller.cc(126)]
Using the Impeller rendering backend (OpenGLESSDF).
A redacted run log and the complete raw module+offset stack can be attached.
Flutter Doctor output
Doctor output
[!] Flutter (Channel beta, 3.47.0-0.1.pre, on Microsoft Windows [Version 10.0.26200.8875], locale en-US) [388ms]
• Flutter version 3.47.0-0.1.pre on channel beta at D:\<project>\.fvm\flutter_sdk
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision bd1e75d918 (4 weeks ago), 2026-07-09 13:32:07 -0700
• Engine revision bbd15867c0
• Dart version 3.13.0 (build 3.13.0-282.1.beta)
• DevTools version 2.59.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
[✓] Windows Version (11 Pro 64-bit, 25H2, 2009) [1,801ms]
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [1,361ms]
• Android SDK at C:\Users\REDACTED\AppData\Local\Android\Sdk
• Emulator version 36.2.12.0 (build_id 14214601) (CL:N/A)
• Platform android-36.1, build-tools 35.0.0
• ANDROID_HOME = C:\Users\REDACTED\AppData\Local\Android\Sdk
• ANDROID_SDK_ROOT = C:\Users\REDACTED\AppData\Local\Android\Sdk
• Java binary at: C:\Program Files\Android\Android Studio\jbr\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.8+-14018985-b1038.68)
• All Android licenses accepted.
[✓] Chrome - develop for the web [82ms]
• Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
[✓] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.36) [81ms]
• Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
• Visual Studio Community 2022 version 17.14.37502.11
• Windows 10 SDK version 10.0.26100.0
[✓] Connected device (3 available) [417ms]
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.26200.8875]
• Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.76
• Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.105
[✓] Network resources [449ms]
• All expected network resources are available.
! Doctor found issues in 1 category.Additional crash artifacts
I can provide the minidump, exact raw module+offset frames, and run
instrumented engine builds. I have not attached the raw minidump publicly
because it can contain application memory and local filesystem paths.
Related issue
#189555 reports a structurally similar LayerTree::Flatten external-texture
null-context crash on iOS Skia. That report was closed because its reproduced
iOS Skia configuration is no longer supported. This report is distinct:
Windows, active Impeller/OpenGLES, a D3D GPU-surface texture, and an
Impeller-versus-Skia same-binary A/B.
Activity
puneetkukreja98 commentedon Aug 10, 2026
@NH-Mason Thanks for reporting the issue. Could you please provide a minimal reproducible example (MRE) or a sample GitHub repository so we can reproduce it on our end?
Without reproducing it or having a minimal reproducible example (MRE) provided, I cannot forward this to the team.
NH-Mason commentedon Aug 11, 2026
Following up on my own report with a standalone reproduction that has no
third-party rendering stack involved, plus controls and newer-engine results.
Repro: https://github.com/NH-Mason/flutter-190774-repro
flutter pub get flutter run -d windowsThat's the whole thing — no environment variables, no scripts. The app registers
a texture, pushes a route about half a second later, dies on that first
transition, and
flutter runprintsLost connection to device.If you want the faulting stack printed to stderr rather than a silent exit, set
$env:FLUTTER_REPRO_CRASH_TRACE = '1'first; that enables a small vectoredexception handler in the runner which is off by default and is not part of the
bug. The repo also has a
run_repro.ps1that runs the control matrix below andretains logs, but the crash needs none of it.
Environment
3.47.0-0.1.pre(beta)bd1e75d918605c91b411e8789fb911e6c9a84534bbd15867c003dc66e678cb3c218649fa8bf914f2flutter_windows.dllSHA-2567902667F7BE09E606AEC1A8679AD53EC02B1E25CA580F525C114FEE8AD69763532.0.15.9595)Using the Impeller rendering backend (OpenGLESSDF)Module offsets below are only meaningful against this exact engine revision.
What the repro contains
My original case went through a third-party Vulkan renderer, so I replaced the
producer with plain D3D11 in the runner — the smallest thing that still hands
Flutter a DXGI shared-handle GPU surface. It preserves the contract that
mattered:
GetGraphicsAdapter)DXGI_FORMAT_B8G8R8A8_UNORMD3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLEIDXGIResource1::CreateSharedHandleGpuSurfaceTexture(kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle)MarkTextureFrameAvailablefrom a worker thread at ~60 HzOn the Dart side, a
Texturewidget sits under aZoomPageTransitionsBuilderwith
allowSnapshotting: true, and a loop pushes/pops a route every ~1.3s sothe transition snapshots the subtree containing the texture. Dependencies are
flutter,flutter_test,flutter_lintsonly; the texture is a 512×512surface cleared to a cycling solid colour, with no scene or asset loading.
Synchronization, stated explicitly: the producer uses
SHARED | SHARED_NTHANDLEwithout a keyed mutex, callingFlushafter each clear. Idon't believe that's the cause here — the stack matches the null-context path
already identified on this issue — but I'd rather disclose it than have it
inferred.
Expected vs actual
Expected: routes push and pop for 30 seconds, app prints
clean timeout reached; exiting normallyand exits 0.Actual: the process takes an access violation on the first route push and
exits with
0xC0000005.This isn't a teardown artifact, and the source shows why rather than relying on
log timing: route push/pop performs no texture retirement at all. Unregistration
exists only in the native plugin's destructor, so the texture and its producer
are still live and registered throughout the transition that crashes.
Matrix
Each run used a 30-second configured timeout; the crashing run ends earlier.
0xC0000005000Each control ran the full 30 seconds and completed 22 push/pop cycles (22–23
pushes issued, depending on whether the last transition was still in flight at
timeout), so they exercised the same path rather than failing to start. The
Impeller-off log contains zero
Using the Impeller rendering backendlines, confirming the backend actuallychanged.
Two honest caveats about the middle control:
once at registration; the flag only disables the subsequent ~60 Hz worker loop.
notifications — so it can't distinguish new pixel content from the cache
invalidation that
MarkTextureFrameAvailabletriggers.So rather than claiming all three triggers are strictly required: in one
30-second run per configuration, the crash was not observed when
snapshotting, continuous updates/marks, or Impeller was disabled. Each dimension
would need independent isolation and repeats before I'd state it more strongly.
Still reproduces on the current beta
I re-ran the repro unchanged against two newer betas (
-AllowAnyEngineskips therevision pin). Both crash the same way, always a null read:
3.47.0-0.1.prebbd15867c003…READ at 0x0flutter_windows.dll+0x2c51f73.47.0-0.3.preeffb186f3172…READ at 0x0flutter_windows.dll+0x2c51f73.47.0-0.4.pre(current beta)f88005a259ba…READ at 0x0flutter_windows.dll+0x2c55f7On
0.4.prethe whole chain shifts by exactly0x400(+0xeb8d1,+0xeb30c,+0xeb1bbagainst+0xeb4d1,+0xeaf0c,+0xeadbb), consistent with the samecode at a different link offset rather than a different bug.
Retained logs:
logs\cross_beta_0.3.log,logs\cross_beta_0.4.log. Thesecross-version runs were classified on module + fault type only, since exact
offsets are not comparable across engines.
Stack
Below those frames the debugger shows a deeply recursive pattern alternating
between two call sites, consistent with a layer-tree walk. This matches the
LayerTree::Flatten → ResolveTextureSkia → GrDirectContext::flushnull-contextpath already described on this issue.
Caveats:
.ecxrreturnsUnable to get exception context, HRESULT 0x8000FFFF, so the frames come fromthe break-in context at the first-chance AV. There are no public PDBs for
flutter_windows.dll, so the debugger resolves every frame to the nearestexported symbol plus a very large offset — I've quoted
module+offsetonly.I have a minidump but haven't attached it, since it captures process environment
data. Happy to produce a scrubbed one, or run additional configurations on this
hardware, if that would help.
puneetkukreja98 commentedon Aug 12, 2026
@NH-Mason Thanks for the response, I was able to reproduce it using the repo link provided above on latest master.
Findings:
Console logs impeller enabled:
Console logs impeller disabled:
Flutter doctor -v:
Makes sure impeller context's are plumbed through to LayerTree::Flatt…
[CP-stable]Makes sure impeller context's are plumbed through to Layer…
github-actions commentedon Aug 31, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.