Repository navigation
[Impeller/flutter_gpu] A render target with only a depth attachment crashes on Vulkan; Metal throws #192535
Description
Steps to reproduce
RenderTarget::IsValid() states the contract — a render target must have a
color attachment at index zero
(engine/src/flutter/impeller/renderer/render_target.cc):
bool RenderTarget::IsValid() const {
// Validate that there is a color attachment at zero index.
if (!HasColorAttachment(0u)) {
VALIDATION_LOG
<< "Render target does not have color attachment at index 0.";
return false;
}GetColorAttachment(0) hands back a default-constructed attachment — with a
null texture — when the target has none:
ColorAttachment RenderTarget::GetColorAttachment(size_t index) const {
if (index == 0) {
if (color0_.has_value()) {
return color0_.value();
}
return ColorAttachment{};
}RenderPassMTL asks whether the target is valid in its constructor
(impeller/renderer/backend/metal/render_pass_mtl.mm:149), so on Metal such a
target fails cleanly. RenderPassVK does not ask, and dereferences that null
texture before anything is validated
(impeller/renderer/backend/vulkan/render_pass_vk.cc):
const ColorAttachment& color0 = render_target_.GetColorAttachment(0);
color_image_vk_ = color0.texture; // null
...
SampleCount sample_count =
color_image_vk_->GetTextureDescriptor().sample_count; // segfaultTo reach it from Dart:
-
flutter create repro && cd repro -
add Flutter GPU to
pubspec.yaml:dependencies: flutter_gpu: sdk: flutter
-
enable it for the app — inside
<application>in
android/app/src/main/AndroidManifest.xml:<meta-data android:name="io.flutter.embedding.android.EnableFlutterGPU" android:value="true" />
(on macOS,
FLTEnableFlutterGPUinmacos/Runner/Info.plist) -
paste the code sample below into
lib/main.dart -
flutter run -d <an Android device>, then the same on macOS
The sample builds one render target with a depth-stencil attachment and no color
attachments — a depth-only pass, which is how a shadow map is drawn — and begins
a render pass with it. No shaders, no pipelines and no draws are involved.
Expected results
What the Metal backend does: a Dart exception the caller can catch, matching the
contract in RenderTarget::IsValid().
threw: Exception: Failed to begin RenderPass
Actual results
On Android/Vulkan the process is killed. No Dart error, nothing to catch.
Tombstone
F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x3c
F DEBUG : Cause: null pointer dereference
F DEBUG : backtrace:
F DEBUG : #00 pc 000000000200cc68 libflutter.so
F DEBUG : #01 pc 0000000001ff23b8 libflutter.so
F DEBUG : #02 pc 0000000001ee6124 libflutter.so
F DEBUG : #03 pc 00000000020a2a58 libflutter.so
F DEBUG : #04 pc 00000000020a35a8 libflutter.so
(InternalFlutterGpu_RenderPass_Begin+4)
F DEBUG : #05 pc 0000000000008244 [anon:dart-code]
Every frame is inside libflutter.so; the graphics driver is not involved.
fault addr 0x3c is the offset of sample_count in the texture descriptor read
from a null pointer.
Why an application builds such a target
Because a shadow map is a depth pass. flutter_gpu cannot sample a depth texture
today (#192449), so an engine either writes depth into a color attachment by
hand — which is what mine does — or reaches for the depth-only target this issue
is about. The second is the natural thing to try, and it kills the process.
Everything else the same application draws works on the same device: this is one
render target shape, not a device that cannot run Impeller.
Suggested fix
Ask the same question the Metal backend asks, before touching color attachment
zero:
: RenderPass(context, target), command_buffer_(std::move(command_buffer)) {
+ if (!render_target_.IsValid()) {
+ is_valid_ = false;
+ return;
+ }
const ColorAttachment& color0 = render_target_.GetColorAttachment(0);CommandBufferVK::OnCreateRenderPass already returns null for an invalid pass,
which flutter_gpu turns into Failed to begin RenderPass — the Metal behaviour.
PR: #192536, with a regression test that
segfaults without the change.
Whether Impeller should support a color-less depth-only pass is a separate
question worth asking: it is how shadow maps are drawn, and both Vulkan and
Metal support such a pass natively. Today RenderTarget::GetRenderTargetSize()
also derives its size from color attachment zero, so supporting it would be more
than removing a check.
Code sample
lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_gpu/gpu.dart' as gpu;
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const ReproApp());
}
class ReproApp extends StatefulWidget {
const ReproApp({super.key});
@override
State<ReproApp> createState() => _ReproAppState();
}
class _ReproAppState extends State<ReproApp> {
String _result = 'running…';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _run());
}
void _run() {
late String result;
try {
final depth = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate,
2048,
2048,
format: gpu.gpuContext.defaultDepthStencilFormat,
);
final target = gpu.RenderTarget(
colorAttachments: const <gpu.ColorAttachment>[],
depthStencilAttachment: gpu.DepthStencilAttachment(
texture: depth,
depthStoreAction: gpu.StoreAction.store,
),
);
final commands = gpu.gpuContext.createCommandBuffer();
// The process dies here on Vulkan. On Metal this throws.
commands.createRenderPass(target);
commands.submit();
result = 'the render pass began';
} catch (error) {
result = 'threw: $error';
}
setState(() => _result = result);
}
@override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(body: Center(child: Text(_result))),
);
}Crashes on a Samsung Galaxy A55 (Android 16, Xclipse, vulkan.samsung.so
24.0.539, arm64, Impeller Vulkan). Throws on macOS with Impeller Metal, from the
same source in the same debug build.
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.47.0, on macOS 27.0 26A5388g darwin-arm64, locale ru-TR)
• Flutter version 3.47.0 on channel stable
• Framework revision 4cf2416426 (4 weeks ago), 2026-08-11 11:53:49 -0700
• Engine revision 5f77625673
• Dart version 3.13.0
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fields🤔 Needs Triage
Activity
mbcorona commentedon Sep 10, 2026
Hi @dzolotov ,
Thanks for the clear report, the root cause, and the repro.
Routing to the engine team; noted linked PR #192536