warmUp method
- List<
RenderView> views, { - bool includeOffscreen = false,
- 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));
}
}