Skip to content

Building Android app with SkSL warmup introduces rendering bug #102655

@filiph

Description

@filiph

Steps to Reproduce

  1. First, build app below without SkSL shader bundle: flutter build apk
  2. Install APK: adb install ...
  3. Verify that the UI looks as expected.

  1. Now, rebuild with a shader file made with a Pixel 5 device: flutter build apk --bundle-sksl-path warmup_2022-04-27_android_pixel5.sksl.json

warmup_2022-04-27_android_pixel5.sksl.json.zip

  1. Verify that the UI still looks as expected
  2. Now, rebuild with a shader file made with a Nokia 1.3: flutter build apk --bundle-sksl-path warmup_2022-04-27_android_nokia13.sksl.json (this file was created by running the exact same map, mind you)

warmup_2022-04-27_android_nokia13.sksl.json.zip

  1. Install this new APK: adb install ...
  2. Look at the UI now.

Expected results: Same UI as without the shader warmup performance optimization, or with a different SkSL warmup file.

Actual results:

It looks like the shader somehow elides lines. (The grid above is made "roughly drawn" by painting many lines close to each other, in segments.)

I personally find this issue very serious. There's no indication there might be a problem until the app is built. I caught this after submitting the app for review to the stores.

Code sample

Sorry, I don't have time to make a completely standalone example, but at least I'm showing the CustomPainter-using widget that seems to have this problem. I'll try to whittle it down later.

EDIT: I have provided a small main.dart file in a comment below. Here it is:

// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:flutter/material.dart';

import 'dart:math';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: const Center(
        child: AspectRatio(
          aspectRatio: 1 / 1,
          child: RoughGrid(8, 8),
        ),
      ),
    );
  }
}

class RoughGrid extends StatelessWidget {
  final int width;
  final int height;

  const RoughGrid(this.width, this.height, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // This is normally provided from above.
    // ignore: prefer_const_constructors
    final lineColor = Color(0x88000000);

    return Stack(
      fit: StackFit.expand,
      children: [
        // First, "draw" (reveal) the horizontal lines
        TweenAnimationBuilder(
          tween: Tween<double>(begin: 0, end: 1),
          duration: const Duration(milliseconds: 900),
          curve: Curves.easeOutCubic,
          child: RepaintBoundary(
            child: CustomPaint(
              painter: _RoughGridPainter(
                width,
                height,
                lineColor: lineColor,
                paintOnly: Axis.horizontal,
              ),
            ),
          ),
          builder: (BuildContext context, double progress, Widget? child) {
            return ShaderMask(
              // BlendMode.dstIn means that opacity of the linear
              // gradient below will be applied to the child (the horizontal
              // lines).
              blendMode: BlendMode.dstIn,
              shaderCallback: (Rect bounds) {
                // A linear gradient that sweeps from
                // "top-slightly-left-off-center" to
                // "bottom-slightly-right-of-center". This achieves the
                // quick "drawing" of the lines.
                return LinearGradient(
                  begin: const Alignment(-0.1, -1),
                  end: const Alignment(0.1, 1),
                  colors: [
                    Colors.black,
                    Colors.white.withOpacity(0),
                  ],
                  stops: [
                    progress,
                    progress + 0.05,
                  ],
                ).createShader(bounds);
              },
              child: child!,
            );
          },
        ),
        // Same as above, but for vertical lines.
        TweenAnimationBuilder(
          // The tween start's with a negative number to achieve
          // a bit of delay before drawing. This is quite dirty, so maybe
          // optimize later?
          tween: Tween<double>(begin: -1, end: 1),
          // Take longer to draw.
          duration: const Duration(milliseconds: 1200),
          curve: Curves.easeOut,
          child: RepaintBoundary(
            child: CustomPaint(
              painter: _RoughGridPainter(
                width,
                height,
                lineColor: lineColor,
                paintOnly: Axis.vertical,
              ),
            ),
          ),
          builder: (BuildContext context, double progress, Widget? child) {
            return ShaderMask(
              blendMode: BlendMode.dstIn,
              shaderCallback: (Rect bounds) {
                return LinearGradient(
                  begin: const Alignment(-1, -0.1),
                  end: const Alignment(1, 0.1),
                  colors: [
                    Colors.black,
                    Colors.white.withOpacity(0),
                  ],
                  stops: [
                    progress,
                    progress + 0.05,
                  ],
                ).createShader(bounds);
              },
              child: child!,
            );
          },
        ),
      ],
    );
  }
}

class _RoughGridPainter extends CustomPainter {
  final int width;
  final int height;

  final Color lineColor;

  final Axis? paintOnly;

  late final Paint pathPaint = Paint()
    ..colorFilter = ColorFilter.mode(lineColor, BlendMode.srcIn);

  final Random _random = Random();

  _RoughGridPainter(
    this.width,
    this.height, {
    this.lineColor = Colors.black,
    this.paintOnly,
  });

  @override
  void paint(Canvas canvas, Size size) {
    const padding = 10.0;
    const maxCrossDisplacement = 1.5;

    const gridLineThicknessRatio = 0.1;
    final lineThickness =
        size.longestSide / max(width, height) * gridLineThicknessRatio;

    final widthStep = size.width / width;

    // Draw vertical lines.
    if (paintOnly == null || paintOnly == Axis.vertical) {
      for (var i = 1; i < width; i++) {
        _roughLine(
          canvas: canvas,
          start: Offset(i * widthStep, padding),
          direction: Axis.vertical,
          length: size.height - 2 * padding,
          maxLineThickness: lineThickness,
          maxCrossAxisDisplacement: maxCrossDisplacement,
          paint: pathPaint,
          random: _random,
        );
      }
    }

    // Draw horizontal lines.
    final heightStep = size.height / height;
    if (paintOnly == null || paintOnly == Axis.horizontal) {
      for (var i = 1; i < height; i++) {
        _roughLine(
          canvas: canvas,
          start: Offset(padding, i * heightStep),
          direction: Axis.horizontal,
          length: size.width - 2 * padding,
          maxLineThickness: lineThickness,
          maxCrossAxisDisplacement: maxCrossDisplacement,
          paint: pathPaint,
          random: _random,
        );
      }
    }
  }

  @override
  bool shouldRepaint(_RoughGridPainter oldDelegate) {
    return oldDelegate.width != width ||
        oldDelegate.height != height ||
        oldDelegate.paintOnly != paintOnly ||
        oldDelegate.lineColor != lineColor;
  }

  static void _roughLine({
    required Canvas canvas,
    required Offset start,
    required Axis direction,
    required double length,
    required double maxLineThickness,
    required double maxCrossAxisDisplacement,
    required Paint paint,
    Random? random,
  }) {
    const segmentLength = 50.0;
    const brushCount = 7;

    final Offset straightSegment;
    final Offset end;
    if (direction == Axis.horizontal) {
      straightSegment = const Offset(segmentLength, 0);
      end = start + Offset(length, 0);
    } else {
      straightSegment = const Offset(0, segmentLength);
      end = start + Offset(0, length);
    }

    final _random = random ?? Random();
    var angle = _random.nextDouble() * 2 * pi;
    final angleChange = 0.3 + 0.4 * _random.nextDouble();

    // Generate a displacement of "strands" that constitute the whole brush.
    // Each strand will make its own line.
    final strandOffsets = List.generate(brushCount, (index) {
      var angle = _random.nextDouble() * 2 * pi;
      return Offset.fromDirection(
          angle, _random.nextDouble() * maxLineThickness / 3);
    });

    var straightPoint = start;
    final fuzziness = Offset.fromDirection(angle, maxCrossAxisDisplacement);
    var fuzzyPoint = start + fuzziness;

    for (var i = 0; straightPoint != end; i++) {
      angle += angleChange;

      var nextStraightPoint = straightPoint + straightSegment;
      if ((nextStraightPoint - start).distance >= length) {
        nextStraightPoint = end;
      }

      final fuzziness = Offset.fromDirection(angle, maxCrossAxisDisplacement);
      final nextFuzzyPoint = nextStraightPoint + fuzziness;

      if (i == 0 || nextStraightPoint == end) {
        paint.strokeCap = StrokeCap.round;
      } else {
        paint.strokeCap = StrokeCap.butt;
      }

      // Drawing individual "strands" makes the line more natural.
      for (final strandOffset in strandOffsets) {
        paint.strokeWidth = (0.8 + 0.4 * _random.nextDouble()) *
            maxLineThickness /
            brushCount *
            2;
        canvas.drawLine(
            fuzzyPoint + strandOffset, nextFuzzyPoint + strandOffset, paint);
      }

      straightPoint = nextStraightPoint;
      fuzzyPoint = nextFuzzyPoint;
    }
  }
}
Logs
% flutter analyze
Analyzing flutter_game_sample...
No issues found! (ran in 3.3s)
% fvm flutter doctor -v
[✓] Flutter (Channel stable, 2.10.4, on macOS 12.2 21D49 darwin-arm, locale en-US)
    • Flutter version 2.10.4 at /Users/filiph/fvm/versions/stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision c860cba910 (5 weeks ago), 2022-03-25 00:23:12 -0500
    • Engine revision 57d3bac3dd
    • Dart version 2.16.2
    • DevTools version 2.9.2

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc1)
    • Android SDK at /Users/filiph/Library/Android/sdk
    • Platform android-31, build-tools 33.0.0-rc1
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.11+0-b60-7772763)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.3.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.11+0-b60-7772763)

[✓] VS Code (version 1.56.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension can be installed from:
      🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (3 available)
    • Pixel 5 (mobile) • 15131FDD4000QY • android-arm64  • Android 12 (API 32)
    • macOS (desktop)  • macos          • darwin-arm64   • macOS 12.2 21D49 darwin-arm
    • Chrome (web)     • chrome         • web-javascript • Google Chrome 100.0.4896.127

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)c: renderingUI glitches reported at the engine/skia or impeller rendering leveldependency: skiaSkia team may need to help usengineflutter/engine related. See also e: labels.found in release: 2.10Found to occur in 2.10has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions