warmUp method

Future<void> warmUp(
  1. List<RenderView> views, {
  2. bool includeOffscreen = false,
  3. Duration? sliceBudget,
})

Compiles the render pipelines and uploads the GPU resources this scene needs, by encoding one frame offscreen and discarding it, so the first visible frame does not stall while shaders compile or textures upload.

Pass the same views the scene will be shown with: their cameras, anti-aliasing, render targets, and this scene's lights and post-process settings determine which pipeline variants compile, so a warm-up frame must match the real one (the offscreen frame is rendered tiny, since a pipeline's identity does not depend on resolution). Populate the scene first; warm-up encodes whatever is attached when it runs.

A SceneView with warmUp: true calls this before it reveals the scene. Awaiting it is optional and safe to repeat. It awaits initializeStaticResources first, and whatever on-demand shaders and tables the scene and views have started loading (see preload). On backends that compile pipelines lazily on first draw (the common case) this front-loads that cost; on a backend that compiles asynchronously it kicks compilation off without blocking on completion.

Set includeOffscreen to encode every render item once. This costs more during loading, but avoids later pipeline stalls as a moving camera first reaches parts of a large scene.

With sliceBudget, the warm-up builds new pipelines for at most that long per offscreen frame, skips the draws that still need one, and yields to the event loop before the next, until nothing is left. A slow device then keeps answering input and animating its loading screen instead of blocking for the whole compile (Android declares an app that ignores input for ten seconds not responding). A single build longer than the budget still runs whole.

Implementation

Future<void> warmUp(
  List<RenderView> views, {
  bool includeOffscreen = false,
  Duration? sliceBudget,
}) async {
  await initializeStaticResources();
  if (views.isEmpty) {
    return;
  }
  // A frame held for on-demand shaders compiles nothing.
  await deferredResourcesSettled(views);
  // On GLES the engine compiles a pipeline on the raster thread and blocks
  // this thread on the result
  // (`flutter::gpu::RenderPass::GetOrCreatePipeline`), so a raster thread
  // parked in `eglSwapBuffers` would freeze Dart for the whole swap. Await
  // it first so the compiles find it idle.
  await awaitRasterThread();
  // Encode one real frame into a discarded recording. The GPU passes (and so
  // the pipeline compilations and resource uploads) are submitted during
  // rendering; only the final canvas blit is thrown away. A small area is
  // enough because pipeline identity is resolution-independent.
  void encode() {
    // A zero step, so warm-up frames never move the scene's clock before
    // the first real frame (render then skips its wall-clock tick).
    update(0.0);
    final recorder = ui.PictureRecorder();
    final canvas = ui.Canvas(recorder);
    _warmUpIncludeOffscreen = includeOffscreen;
    try {
      renderViews(
        views,
        canvas,
        region: const ui.Rect.fromLTWH(0, 0, 64, 64),
      );
    } finally {
      _warmUpIncludeOffscreen = false;
      recorder.endRecording().dispose();
    }
  }

  if (sliceBudget == null) {
    encode();
    return;
  }
  // Each slice is a whole offscreen frame; stop once one encodes every view
  // and skips nothing, or after a generous cap if something never finishes
  // building. A view paced by GPU backpressure re-presents its last image
  // and resolves no pipelines, so it proves nothing.
  for (var slice = 0; slice < 1000; slice++) {
    // Each slice compiles pipelines after a yield, so rendezvous again.
    if (slice > 0) await awaitRasterThread();
    final paced = _pacedFrameCount;
    withPipelineBuildBudget(sliceBudget, encode);
    if (_pacedFrameCount == paced && deferredPipelineBuilds == 0) return;
    await Future<void>.delayed(const Duration(milliseconds: 16));
  }
}