// Copyright 2014 The Flutter Authors. All rights reserved.
// 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';

import 'debug.dart';

/// Interface for drawing an image to warm up Skia shader compilations.
///
/// 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.
///
/// Therefore, we use this during the [PaintingBinding.initInstances] call to
/// move common shader compilations from animation time to startup time. If
/// needed, app developers can create a custom [ShaderWarmUp] subclass and
/// hand it to [PaintingBinding.shaderWarmUp] before
/// [PaintingBinding.initInstances] is called. Usually, that can be done before
/// calling [runApp].
///
/// To determine whether a draw operation is useful for warming up shaders,
/// check whether it improves the slowest frame rasterization time. Also,
/// tracing with `flutter run --profile --trace-skia` may reveal whether there
/// is shader-compilation-related jank. If there is such jank, some long
/// `GrGLProgramBuilder::finalize` calls would appear in the middle of an
/// animation. Their parent calls, which look like `XyzOp` (e.g., `FillRecOp`,
/// `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.
///
/// This warm-up needs to be run on each individual device because the shader
/// 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
/// device-agnostic.
///
/// If no warm-up is desired (e.g., when the startup latency is crucial), set
/// [PaintingBinding.shaderWarmUp] either to a custom ShaderWarmUp with an empty
/// [warmUpOnCanvas] or null.
///
/// See also:
///
///  * [PaintingBinding.shaderWarmUp], the actual instance of [ShaderWarmUp]
///    that's used to warm up the shaders.
///  * <https://flutter.dev/docs/perf/rendering/shader>
abstract class ShaderWarmUp {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const ShaderWarmUp();

  /// The size of the warm up image.
  ///
  /// 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.
  ///
  /// A custom shader warm up can override this based on targeted devices.
  ui.Size get size => const ui.Size(100.0, 100.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, consider capturing an skp using `flutter screenshot
  /// --vm-service-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
  /// Flutter DevTools timeline to see which Skia draw operations are commonly used,
  /// and which shader compilations are causing jank.
  @protected
  Future<void> warmUpOnCanvas(ui.Canvas canvas);

  /// Construct an offscreen image of [size], and execute [warmUpOnCanvas] on a
  /// canvas associated with that image.
  ///
  /// Currently, this has no effect when [kIsWeb] is true.
  Future<void> execute() async {
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final ui.Canvas canvas = ui.Canvas(recorder);
    await warmUpOnCanvas(canvas);
    final ui.Picture picture = recorder.endRecording();
    assert(debugCaptureShaderWarmUpPicture(picture));
    if (!kIsWeb || isCanvasKit) { // Picture.toImage is not yet implemented on the web.
      TimelineTask? debugShaderWarmUpTask;
      if (!kReleaseMode) {
        debugShaderWarmUpTask = TimelineTask()..start('Warm-up shader');
      }
      try {
        final ui.Image image = await picture.toImage(size.width.ceil(), size.height.ceil());
        assert(debugCaptureShaderWarmUpImage(image));
        image.dispose();
      } finally {
        if (!kReleaseMode) {
          debugShaderWarmUpTask!.finish();
        }
      }
    }
    picture.dispose();
  }
}
