shader_warm_up.dart 8.54 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
abstract class ShaderWarmUp {
  /// Allow const constructors for subclasses.
  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 70 71 72 73 74 75 76

  /// 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
  }
}

/// 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.
102 103 104 105
  const DefaultShaderWarmUp({
    this.drawCallSpacing = 0.0,
    this.canvasSize = const ui.Size(100.0, 100.0),
  });
106 107 108 109 110 111 112 113 114 115 116

  /// Constant that can be used to space out draw calls for visualizing the draws
  /// for debugging purposes (example: 80.0).  Be sure to also change your canvas
  /// size.
  final double drawCallSpacing;

  /// Value that returned by this.size to control canvas size where draws happen.
  final ui.Size canvasSize;

  @override
  ui.Size get size => canvasSize;
117 118 119 120

  /// Trigger common draw operations on a canvas to warm up GPU shader
  /// compilation cache.
  @override
121
  Future<void> warmUpOnCanvas(ui.Canvas canvas) async {
Dan Field's avatar
Dan Field committed
122
    const ui.RRect rrect = ui.RRect.fromLTRBXY(20.0, 20.0, 60.0, 60.0, 10.0, 10.0);
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    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);

138 139 140 141 142 143 144 145 146 147 148 149 150
    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];
151 152 153 154 155

    final List<ui.Paint> paints = <ui.Paint>[
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.fill,
156 157 158
      ui.Paint()
        ..isAntiAlias = false
        ..style = ui.PaintingStyle.fill,
159 160 161 162 163 164 165
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.stroke
        ..strokeWidth = 10,
      ui.Paint()
        ..isAntiAlias = true
        ..style = ui.PaintingStyle.stroke
166
        ..strokeWidth = 0.1,  // hairline
167 168 169 170 171 172 173
    ];

    // 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);
174
        canvas.translate(drawCallSpacing, 0.0);
175 176
      }
      canvas.restore();
177
      canvas.translate(0.0, drawCallSpacing);
178 179 180 181 182 183
    }

    // Warm up shadow shaders.
    const ui.Color black = ui.Color(0xFF000000);
    canvas.save();
    canvas.drawShadow(rrectPath, black, 10.0, true);
184
    canvas.translate(drawCallSpacing, 0.0);
185 186 187 188
    canvas.drawShadow(rrectPath, black, 10.0, false);
    canvas.restore();

    // Warm up text shaders.
189
    canvas.translate(0.0, drawCallSpacing);
190 191 192 193 194 195
    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));
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212

    // 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).
    for (double fraction in <double>[0.0, 0.5]) {
      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);
213 214
  }
}