renderViews method
Renders a list of views of this scene onto canvas.
Each RenderView binds a camera to a normalized sub-rectangle of
region (its RenderView.viewport), a RenderView.layerMask, and a
compositing RenderView.order; views are drawn lowest-order first.
This is how split-screen and picture-in-picture are rendered. render
is the single-view convenience over this.
Each view renders into its own offscreen target (its own swapchain texture and transient texture pool), so simultaneous views never share a render target within a frame.
region is the canvas rectangle the views subdivide; it defaults to
the canvas clip bounds. pixelRatio is the logical-to-physical
multiplier for the offscreen render targets (defaults to the view's
device pixel ratio).
The scene is advanced once per call (a single per-frame tick), then every view is rendered from that shared scene state.
Implementation
void renderViews(
List<RenderView> views,
ui.Canvas canvas, {
ui.Rect? region,
double? pixelRatio,
}) {
if (!_readyToRender) {
debugPrint('Flutter Scene is not ready to render. Skipping frame.');
debugPrint(
'You may wait on the Future returned by Scene.initializeStaticResources() before rendering.',
);
return;
}
final drawArea = region ?? canvas.getLocalClipBounds();
if (drawArea.isEmpty || views.isEmpty) {
return;
}
final dpr =
pixelRatio ??
ui.PlatformDispatcher.instance.implicitView?.devicePixelRatio ??
1.0;
// Re-bake the sky-driven environment when its refresh policy says one is
// due. The bake submits its own passes, so like the lazy default-prefilter
// below it must run before this frame's render graph is built.
final skyEnv = skyEnvironment;
if (skyEnv != null) {
final baked = skyEnv.bakeIfDue(DateTime.now());
if (baked != null) {
environment = baked;
}
}
// The web radiance prefilter is degenerate when built on a cold WebGL
// context (before the first frame composites); environments built then
// (the lazily built default below, or any the app built up front) are
// re-baked once a frame has been presented and the context is warm. No-op
// on other backends and after the one-time rebuild. See
// EnvironmentMap.markContextWarmAndRebakeRadiance.
if (_hasPresentedFrame) {
EnvironmentMap.markContextWarmAndRebakeRadiance();
}
// Resolve the IBL environment up front (before building any render
// graph): the default is built lazily here on first use, which submits
// a one-time prefilter pass that must not be nested inside the frame's
// render passes. Doing this in the constructor instead would break the
// OpenGL ES backend, which sets up its context lazily on the raster
// thread only after the first frame.
final environmentMap = environment ?? Material.getDefaultEnvironmentMap();
// Reuse one host buffer across frames; reset() cycles it to the next
// frame's backing storage. Shared by every view this frame.
final transientsBuffer = _transientsBuffer ??= gpu.gpuContext
.createHostBuffer();
transientsBuffer.reset();
// Advance the instance-transform buffer pool to this frame's set (it
// backs the instance-rate vertex buffer, separate from the uniform
// host buffer above; see instance_packing.dart).
instanceTransformBuffers.beginFrame();
// Advance the scene once per frame (not once per view): tick components
// and animations and refresh the flat render list before the passes
// iterate it. Skipped when update() already ran the tick this frame.
if (!_tickedThisFrame) {
final nowMillis = DateTime.now().millisecondsSinceEpoch;
final lastMillis = _lastTickMillis ?? nowMillis;
_tick((nowMillis - lastMillis) / 1000.0);
}
_tickedThisFrame = false;
// Rebuild the spatial culling structure once if the pre-pass changed the
// scene, before the views' render passes query it.
renderScene.rebuildIfDirty();
// The renderer shades a single directional light: the first one
// registered in the graph (the [directionalLight] convenience, or a
// [DirectionalLightComponent] attached to any node).
final lightComponent = renderScene.directionalLights.isEmpty
? null
: renderScene.directionalLights.first;
// Texture-target views render first so screen views (and the HUD)
// composite this frame's captures, the simple form of the
// produce-before-consume rule.
// TODO(rendertarget): order texture views among themselves by
// resource read/write edges once materials can sample render textures.
final textureViews = <RenderView>[
for (final view in this.views)
if (view.target != null) view,
for (final view in views)
if (view.target != null) view,
]..sort((a, b) => a.order.compareTo(b.order));
final now = DateTime.now();
for (final view in textureViews) {
final target = view.target!;
if (!target.shouldUpdate(now)) {
continue;
}
_renderViewToTexture(
view: view,
outputColor: target.acquireNextTexture(),
pixelSize: ui.Size(target.width.toDouble(), target.height.toDouble()),
pool: target.transientTexturePool,
environmentMap: environmentMap,
transientsBuffer: transientsBuffer,
lightComponent: lightComponent,
);
target.markUpdated(now);
}
// Composite lower-order screen views first.
final screenViews = [
for (final view in views)
if (view.target == null) view,
];
final ordered = screenViews.length == 1
? screenViews
: (screenViews..sort((a, b) => a.order.compareTo(b.order)));
for (var i = 0; i < ordered.length; i++) {
final view = ordered[i];
final viewArea = _viewDrawArea(drawArea, view.viewport);
if (viewArea.isEmpty) {
continue;
}
_renderViewToCanvas(
view: view,
canvas: canvas,
drawArea: viewArea,
dpr: dpr,
viewIndex: i,
environmentMap: environmentMap,
transientsBuffer: transientsBuffer,
lightComponent: lightComponent,
);
}
// A frame has now been submitted; the next one runs on a warm context (see
// the rebuild near the environment resolution above).
_hasPresentedFrame = true;
}