shader_warm_up.dart 7.36 KB
Newer Older
1 2 3 4
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7 8 9 10 11 12
import 'dart:developer';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';

/// Interface for drawing an image to warm up Skia shader compilations.
///
13 14 15 16
/// When Skia first sees a certain type of draw operation on the GPU, it needs
/// to compile the corresponding shader. The compilation can be slow (20ms-
/// 200ms). Having that time as startup latency is often better than having
/// jank in the middle of an animation.
17 18 19
///
/// Therefore, we use this during the [PaintingBinding.initInstances] call to
/// move common shader compilations from animation time to startup time. By
20 21 22 23 24
/// default, a [DefaultShaderWarmUp] is used. If needed, app developers can
/// create a custom [ShaderWarmUp] subclass and hand it to
/// [PaintingBinding.shaderWarmUp] (so it replaces [DefaultShaderWarmUp])
/// before [PaintingBinding.initInstances] is called. Usually, that can be
/// done before calling [runApp].
25
///
26
/// To determine whether a draw operation is useful for warming up shaders,
27 28 29
/// check whether it improves the slowest GPU frame. Also, tracing with
/// `flutter run --profile --trace-skia` may reveal whether there is shader-
/// compilation-related jank. If there is such jank, some long
30 31
/// `GrGLProgramBuilder::finalize` calls would appear in the middle of an
/// animation. Their parent calls, which look like `XyzOp` (e.g., `FillRecOp`,
32 33 34 35 36 37 38
/// `CircularRRectOp`) would suggest Xyz draw operations are causing the
/// shaders to be compiled. A useful shader warm-up draw operation would
/// eliminate such long compilation calls in the animation. To double-check
/// the warm-up, trace with `flutter run --profile --trace-skia --start-
/// paused`. The `GrGLProgramBuilder` with the associated `XyzOp` should
/// appear during startup rather than in the middle of a later animation.

39 40
///
/// This warm-up needs to be run on each individual device because the shader
41 42
/// compilation depends on the specific GPU hardware and driver a device has. It
/// can't be pre-computed during the Flutter engine compilation as the engine is
43
/// device-agnostic.
44
///
45
/// If no warm-up is desired (e.g., when the startup latency is crucial), set
46 47
/// [PaintingBinding.shaderWarmUp] either to a custom ShaderWarmUp with an empty
/// [warmUpOnCanvas] or null.
48 49 50 51 52
///
/// See also:
///
///  * [PaintingBinding.shaderWarmUp], the actual instance of [ShaderWarmUp]
///    that's used to warm up the shaders.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
abstract class ShaderWarmUp {
  /// Allow const constructors for subclasses.
  const ShaderWarmUp();

  /// The size of the warm up image.
  ///
  /// The exact size shouldn't matter much as long as it's not too far away from
  /// the target device's screen. 1024x1024 is a good choice as it is within an
  /// order of magnitude of most devices.
  ///
  /// A custom shader warm up can override this based on targeted devices.
  ui.Size get size => const ui.Size(1024.0, 1024.0);

  /// Trigger draw operations on a given canvas to warm up GPU shader
  /// compilation cache.
  ///
  /// To decide which draw operations to be added to your custom warm up
  /// process, try capture an skp using `flutter screenshot --observatory-
  /// port=<port> --type=skia` and analyze it with https://debugger.skia.org.
  /// Alternatively, one may run the app with `flutter run --trace-skia` and
  /// then examine the GPU thread in the observatory timeline to see which
  /// Skia draw operations are commonly used, and which shader compilations
  /// are causing janks.
  @protected
77
  Future<void> warmUpOnCanvas(ui.Canvas canvas);
78 79 80

  /// Construct an offscreen image of [size], and execute [warmUpOnCanvas] on a
  /// canvas associated with that image.
81
  Future<void> execute() async {
82 83 84
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final ui.Canvas canvas = ui.Canvas(recorder);

85
    await warmUpOnCanvas(canvas);
86 87 88 89

    final ui.Picture picture = recorder.endRecording();
    final TimelineTask shaderWarmUpTask = TimelineTask();
    shaderWarmUpTask.start('Warm-up shader');
90 91
    await picture.toImage(size.width.ceil(), size.height.ceil());
    shaderWarmUpTask.finish();
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
  }
}

/// Default way of warming up Skia shader compilations.
///
/// The draw operations being warmed up here are decided according to Flutter
/// engineers' observation and experience based on the apps and the performance
/// issues seen so far.
class DefaultShaderWarmUp extends ShaderWarmUp {
  /// Allow [DefaultShaderWarmUp] to be used as the default value of parameters.
  const DefaultShaderWarmUp();

  /// Trigger common draw operations on a canvas to warm up GPU shader
  /// compilation cache.
  @override
107
  Future<void> warmUpOnCanvas(ui.Canvas canvas) async {
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
    final ui.RRect rrect = ui.RRect.fromLTRBXY(20.0, 20.0, 60.0, 60.0, 10.0, 10.0);
    final ui.Path rrectPath = ui.Path()..addRRect(rrect);

    final ui.Path circlePath = ui.Path()..addOval(
        ui.Rect.fromCircle(center: const ui.Offset(40.0, 40.0), radius: 20.0)
    );

    // The following path is based on
    // https://skia.org/user/api/SkCanvas_Reference#SkCanvas_drawPath
    final ui.Path path = ui.Path();
    path.moveTo(20.0, 60.0);
    path.quadraticBezierTo(60.0, 20.0, 60.0, 60.0);
    path.close();
    path.moveTo(60.0, 20.0);
    path.quadraticBezierTo(60.0, 60.0, 20.0, 60.0);

124 125 126 127 128 129 130 131 132 133 134 135 136
    final ui.Path convexPath = ui.Path();
    convexPath.moveTo(20.0, 30.0);
    convexPath.lineTo(40.0, 20.0);
    convexPath.lineTo(60.0, 30.0);
    convexPath.lineTo(60.0, 60.0);
    convexPath.lineTo(20.0, 60.0);
    convexPath.close();

    // Skia uses different shaders based on the kinds of paths being drawn and
    // the associated paint configurations. According to our experience and
    // tracing, drawing the following paths/paints generates various of
    // shaders that are commonly used.
    final List<ui.Path> paths = <ui.Path>[rrectPath, circlePath, path, convexPath];
137 138 139 140 141

    final List<ui.Paint> paints = <ui.Paint>[
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.fill,
142 143 144
      ui.Paint()
        ..isAntiAlias = false
        ..style = ui.PaintingStyle.fill,
145 146 147 148 149 150 151
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.stroke
        ..strokeWidth = 10,
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.stroke
152
        ..strokeWidth = 0.1,  // hairline
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    ];

    // Warm up path stroke and fill shaders.
    for (int i = 0; i < paths.length; i += 1) {
      canvas.save();
      for (ui.Paint paint in paints) {
        canvas.drawPath(paths[i], paint);
        canvas.translate(80.0, 0.0);
      }
      canvas.restore();
      canvas.translate(0.0, 80.0);
    }

    // Warm up shadow shaders.
    const ui.Color black = ui.Color(0xFF000000);
    canvas.save();
    canvas.drawShadow(rrectPath, black, 10.0, true);
    canvas.translate(80.0, 0.0);
    canvas.drawShadow(rrectPath, black, 10.0, false);
    canvas.restore();

    // Warm up text shaders.
    canvas.translate(0.0, 80.0);
    final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
      ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
    )..pushStyle(ui.TextStyle(color: black))..addText('_');
    final ui.Paragraph paragraph = paragraphBuilder.build()
      ..layout(const ui.ParagraphConstraints(width: 60.0));
    canvas.drawParagraph(paragraph, const ui.Offset(20.0, 20.0));
  }
}