shader_warm_up.dart 9.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:developer';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';

/// Interface for drawing an image to warm up Skia shader compilations.
///
12 13 14 15
/// 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.
16 17 18
///
/// Therefore, we use this during the [PaintingBinding.initInstances] call to
/// move common shader compilations from animation time to startup time. By
19 20 21 22 23
/// 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].
24
///
25
/// To determine whether a draw operation is useful for warming up shaders,
26
/// check whether it improves the slowest frame rasterization time. Also,
27 28
/// tracing with `flutter run --profile --trace-skia` may reveal whether there
/// is shader-compilation-related jank. If there is such jank, some long
29 30
/// `GrGLProgramBuilder::finalize` calls would appear in the middle of an
/// animation. Their parent calls, which look like `XyzOp` (e.g., `FillRecOp`,
31 32 33 34 35 36
/// `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.
37 38
///
/// This warm-up needs to be run on each individual device because the shader
39 40
/// 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
41
/// device-agnostic.
42
///
43
/// If no warm-up is desired (e.g., when the startup latency is crucial), set
44 45
/// [PaintingBinding.shaderWarmUp] either to a custom ShaderWarmUp with an empty
/// [warmUpOnCanvas] or null.
46 47 48 49 50
///
/// See also:
///
///  * [PaintingBinding.shaderWarmUp], the actual instance of [ShaderWarmUp]
///    that's used to warm up the shaders.
51
///  * <https://flutter.dev/docs/perf/rendering/shader>
52
abstract class ShaderWarmUp {
53 54
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
55 56 57 58
  const ShaderWarmUp();

  /// The size of the warm up image.
  ///
59 60 61
  /// The exact size shouldn't matter much as long as all draws are onscreen.
  /// 100x100 is an arbitrary small size that's easy to fit significant draw
  /// calls onto.
62 63
  ///
  /// A custom shader warm up can override this based on targeted devices.
64
  ui.Size get size => const ui.Size(100.0, 100.0);
65 66 67 68 69

  /// 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
70 71 72 73 74 75
  /// process, consider capturing an skp using `flutter screenshot
  /// --observatory-uri=<uri> --type=skia` and analyzing it with
  /// <https://debugger.skia.org/>. Alternatively, one may run the app with
  /// `flutter run --trace-skia` and then examine the raster thread in the
  /// observatory timeline to see which Skia draw operations are commonly used,
  /// and which shader compilations are causing jank.
76
  @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 82
  ///
  /// Currently, this has no effect when [kIsWeb] is true.
83
  Future<void> execute() async {
84 85
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final ui.Canvas canvas = ui.Canvas(recorder);
86
    await warmUpOnCanvas(canvas);
87
    final ui.Picture picture = recorder.endRecording();
88 89 90 91 92 93 94 95
    if (!kIsWeb) { // Picture.toImage is not yet implemented on the web.
      final TimelineTask shaderWarmUpTask = TimelineTask();
      shaderWarmUpTask.start('Warm-up shader');
      try {
        await picture.toImage(size.width.ceil(), size.height.ceil());
      } finally {
        shaderWarmUpTask.finish();
      }
96
    }
97 98 99 100 101 102 103 104
  }
}

/// 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.
105 106 107 108 109 110 111 112 113
///
/// This is used for the default value of [PaintingBinding.shaderWarmUp].
/// Consider setting that static property to a different value before the
/// binding is initialized to change the warm-up sequence.
///
/// See also:
///
///  * [ShaderWarmUp], the base class for shader warm-up objects.
///  * <https://flutter.dev/docs/perf/rendering/shader>
114
class DefaultShaderWarmUp extends ShaderWarmUp {
115 116 117 118
  /// Create an instance of the default shader warm-up logic.
  ///
  /// Since this constructor is `const`, [DefaultShaderWarmUp] can be used as
  /// the default value of parameters.
119 120 121 122
  const DefaultShaderWarmUp({
    this.drawCallSpacing = 0.0,
    this.canvasSize = const ui.Size(100.0, 100.0),
  });
123

124 125 126 127 128 129 130
  /// Distance to place between draw calls for visualizing the draws for
  /// debugging purposes (e.g. 80.0).
  ///
  /// Defaults to 0.0.
  ///
  /// When changing this value, the [canvasSize] must also be changed to
  /// accomodate the bigger canvas.
131 132
  final double drawCallSpacing;

133 134 135
  /// The [size] of the canvas required to paint the shapes in [warmUpOnCanvas].
  ///
  /// When [drawCallSpacing] is 0.0, this should be at least 100.0 by 100.0.
136 137 138 139
  final ui.Size canvasSize;

  @override
  ui.Size get size => canvasSize;
140 141 142 143

  /// Trigger common draw operations on a canvas to warm up GPU shader
  /// compilation cache.
  @override
144
  Future<void> warmUpOnCanvas(ui.Canvas canvas) async {
Dan Field's avatar
Dan Field committed
145
    const ui.RRect rrect = ui.RRect.fromLTRBXY(20.0, 20.0, 60.0, 60.0, 10.0, 10.0);
146 147
    final ui.Path rrectPath = ui.Path()..addRRect(rrect);
    final ui.Path circlePath = ui.Path()..addOval(
148
      ui.Rect.fromCircle(center: const ui.Offset(40.0, 40.0), radius: 20.0)
149 150 151 152 153 154 155 156 157 158 159
    );

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

160 161 162 163 164 165 166 167 168 169 170 171 172
    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];
173 174 175 176 177

    final List<ui.Paint> paints = <ui.Paint>[
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.fill,
178 179 180
      ui.Paint()
        ..isAntiAlias = false
        ..style = ui.PaintingStyle.fill,
181 182 183 184 185 186 187
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.stroke
        ..strokeWidth = 10,
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.stroke
188
        ..strokeWidth = 0.1,  // hairline
189 190 191 192 193
    ];

    // Warm up path stroke and fill shaders.
    for (int i = 0; i < paths.length; i += 1) {
      canvas.save();
194
      for (final ui.Paint paint in paints) {
195
        canvas.drawPath(paths[i], paint);
196
        canvas.translate(drawCallSpacing, 0.0);
197 198
      }
      canvas.restore();
199
      canvas.translate(0.0, drawCallSpacing);
200 201 202 203 204 205
    }

    // Warm up shadow shaders.
    const ui.Color black = ui.Color(0xFF000000);
    canvas.save();
    canvas.drawShadow(rrectPath, black, 10.0, true);
206
    canvas.translate(drawCallSpacing, 0.0);
207 208 209 210
    canvas.drawShadow(rrectPath, black, 10.0, false);
    canvas.restore();

    // Warm up text shaders.
211
    canvas.translate(0.0, drawCallSpacing);
212 213 214 215 216 217
    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));
218 219 220 221 222 223 224

    // Draw a rect inside a rrect with a non-trivial intersection. If the
    // intersection is trivial (e.g., equals the rrect clip), Skia will optimize
    // the clip out.
    //
    // Add an integral or fractional translation to trigger Skia's non-AA or AA
    // optimizations (as did before in normal FillRectOp in rrect clip cases).
225
    for (final double fraction in <double>[0.0, 0.5]) {
226 227 228 229 230 231 232 233 234
      canvas
        ..save()
        ..translate(fraction, fraction)
        ..clipRRect(ui.RRect.fromLTRBR(8, 8, 328, 248, const ui.Radius.circular(16)))
        ..drawRect(const ui.Rect.fromLTRB(10, 10, 320, 240), ui.Paint())
        ..restore();
      canvas.translate(drawCallSpacing, 0.0);
    }
    canvas.translate(0.0, drawCallSpacing);
235 236
  }
}