Skip to content

[Impeller/flutter_gpu] A render target with only a depth attachment crashes on Vulkan; Metal throws #192535

Description

@dzolotov

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;          // segfault

To reach it from Dart:

  1. flutter create repro && cd repro

  2. add Flutter GPU to pubspec.yaml:

    dependencies:
      flutter_gpu:
        sdk: flutter
  3. 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, FLTEnableFlutterGPU in macos/Runner/Info.plist)

  4. paste the code sample below into lib/main.dart

  5. 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

Activity

  1. added
    in triagePresently being triaged by the triage team
    engineflutter/engine related. See also e: labels.
    e: impellerImpeller rendering backend issues and features requests
    on Sep 10, 2026
  2. mbcorona commented on Sep 10, 2026

    @mbcorona
    Member

    Hi @dzolotov ,

    Thanks for the clear report, the root cause, and the repro.
    Routing to the engine team; noted linked PR #192536

  3. added
    c: crashStack traces logged to the console
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Sep 10, 2026
  4. moved this to 🤔 Needs Triage in Flutter GPUon Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: crashStack traces logged to the consolee: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter-gpuhas reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-fluttergpuOwned by Flutter GPU team

    Type

    No type

    Projects

    • Status
      🤔 Needs Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions