impeller_core/execute.rs
1//! Rendering a recording, pass by pass.
2//!
3//! Here rather than in the crate that wraps a backend, because a recording is
4//! defined here and executing one is nothing more than reading it: a layer is a
5//! target allocated at that pass's size, rendered into, and sampled by a later
6//! pass. Nothing about that is backend-specific, so it is written once and
7//! generic over the HAL. It had been private to the public crate, which left
8//! the test harness with the choice of duplicating it or staying at the batch
9//! level where layers cannot be expressed.
10
11use crate::canvas::{Pass, Recording, TextureSource};
12use crate::ramp::RAMP_WIDTH;
13use impeller_hal::{
14 Error, Extent2D, Hal, HalContext, HalTexture, PixelFormat, Result, TextureDescriptor,
15};
16
17/// The textures a recording needs that it did not arrive with.
18///
19/// Layer targets are rendered and baked gradients are uploaded, both per
20/// submission and both released with it. Grouped because they are the same
21/// thing from the caller's side -- resources this frame owns and must destroy
22/// after the work that reads them -- and because a caller that has to submit
23/// the root pass itself would otherwise be handed two lists and trusted to
24/// keep them together.
25pub struct Transient<H: Hal> {
26 /// One per non-root pass, in pass order.
27 pub layers: Vec<H::Texture>,
28 /// One per entry in [`Recording::ramps`], in that order.
29 pub ramps: Vec<H::Texture>,
30}
31
32impl<H: Hal> Transient<H> {
33 fn empty() -> Self {
34 Self {
35 layers: Vec::new(),
36 ramps: Vec::new(),
37 }
38 }
39
40 /// Everything this frame owns, as one list to release when it is done.
41 ///
42 /// For a caller that keeps the resources alive past this call -- a
43 /// presentation target holding them until its fence signals -- where a
44 /// texture came from stops mattering the moment the submission is made.
45 /// What remains is when it is safe to free, and that answer is the same for
46 /// all of them.
47 pub fn into_textures(self) -> Vec<H::Texture> {
48 let mut all = self.layers;
49 all.extend(self.ramps);
50 all
51 }
52
53 /// Release everything, in any order: nothing here refers to anything else.
54 pub fn destroy(self, ctx: &mut H::Context)
55 where
56 H::Context: HalContext<Hal = H>,
57 {
58 for texture in self.layers.into_iter().chain(self.ramps) {
59 ctx.destroy_texture(texture);
60 }
61 }
62}
63
64/// Upload every baked gradient the recording carries.
65///
66/// The format is linear half-float, and the argument the sRGB one rested on is
67/// answered rather than overridden. That argument was about how to spend eight
68/// bits: eight bits of *linear* color band visibly in the darks, so spacing
69/// them through the transfer function put them where the eye reads them. Half
70/// has no fixed quantum -- its precision is relative, about eleven bits of
71/// mantissa at every magnitude -- so there are no longer eight bits to spend
72/// well, and the perceptual spacing was buying what the format now gives
73/// everywhere.
74///
75/// What it also gives is range. A gradient stop outside the sRGB primaries has
76/// a component outside zero to one, and eight bits through a transfer function
77/// had nowhere to put one.
78///
79/// Sampled and never drawn into, which is worth saying rather than leaving to
80/// a backend to assume: a ramp asked for a color attachment it had no use for,
81/// and a backend that builds one per texture would refuse a format it could
82/// perfectly well have sampled.
83fn upload_ramps<H: Hal>(ctx: &mut H::Context, recording: &Recording) -> Result<Vec<H::Texture>>
84where
85 H::Context: HalContext<Hal = H>,
86{
87 let mut uploaded = Vec::with_capacity(recording.ramps.len());
88 for ramp in &recording.ramps {
89 let extent = Extent2D::new(RAMP_WIDTH as u32, 1);
90 let outcome = ctx
91 .create_texture(&TextureDescriptor::sampled(
92 extent,
93 PixelFormat::Rgba16Float,
94 ))
95 .and_then(
96 |mut texture| match ctx.write_texture(&mut texture, &ramp.texels) {
97 Ok(()) => Ok(texture),
98 Err(e) => {
99 ctx.destroy_texture(texture);
100 Err(e)
101 }
102 },
103 );
104 match outcome {
105 Ok(texture) => uploaded.push(texture),
106 Err(e) => {
107 for texture in uploaded {
108 ctx.destroy_texture(texture);
109 }
110 return Err(e);
111 }
112 }
113 }
114 Ok(uploaded)
115}
116
117/// Resolve one pass's slot table against the caller's images and the layers
118/// rendered so far.
119///
120/// A slot means something different in each pass, since a layer occupies one
121/// alongside the caller's images and the two number independently. Resolving
122/// per pass is what lets that work.
123pub fn resolve_sources<'a, H: Hal>(
124 pass: &Pass,
125 images: &[&'a H::Texture],
126 transient: &'a Transient<H>,
127) -> Result<Vec<&'a H::Texture>> {
128 let mut table = Vec::with_capacity(pass.sources.len());
129 for source in &pass.sources {
130 match source {
131 TextureSource::Image(slot) => {
132 table.push(*images.get(*slot as usize).ok_or(Error::Unsupported(
133 "a paint samples an image the caller did not supply",
134 ))?);
135 }
136 // Earlier by construction, so this is always already rendered. A
137 // recording that named a later one would be malformed rather than
138 // merely out of order.
139 TextureSource::Layer(pass_index) => {
140 table.push(transient.layers.get(*pass_index).ok_or(Error::Unsupported(
141 "a layer is composited before it is rendered",
142 ))?);
143 }
144 // Uploaded before any pass runs, so unlike a layer this cannot be
145 // named too early. A miss means the recording and its ramp table
146 // disagree, which is malformed rather than merely out of order.
147 TextureSource::Ramp(index) => {
148 table.push(transient.ramps.get(*index).ok_or(Error::Unsupported(
149 "a gradient names a ramp the recording does not carry",
150 ))?);
151 }
152 }
153 }
154 Ok(table)
155}
156
157/// Render every pass except the root, returning the layer targets in order.
158///
159/// Split out from [`execute`] so that a caller who has to submit the root pass
160/// itself can still get the layers rendered. That is not a hypothetical: the
161/// root is the pass that lands in a presentable image, and presenting one means
162/// submitting it gated on the acquire and present semaphores — which is a
163/// different call from the one that renders offscreen and waits. Without this
164/// split, a frame with layers could be rendered offscreen and never presented.
165///
166/// The returned textures are the caller's to destroy, and must outlive the
167/// submission that samples them.
168pub fn execute_layers<H: Hal>(
169 ctx: &mut H::Context,
170 recording: &Recording,
171 images: &[&H::Texture],
172 format: PixelFormat,
173) -> Result<Transient<H>>
174where
175 H::Context: HalContext<Hal = H>,
176{
177 // Gradients first: a layer pass can sample one, so they have to exist
178 // before any pass runs rather than before the root pass.
179 let mut transient = Transient::<H>::empty();
180 transient.ramps = upload_ramps::<H>(ctx, recording)?;
181
182 // Layers rendered in order, so one is always finished before the pass that
183 // samples it -- which is the order a recording stores them in, since a
184 // layer is filed when it is restored and cannot be composited before that.
185 for pass in &recording.passes[..recording.passes.len() - 1] {
186 let outcome = resolve_sources::<H>(pass, images, &transient).and_then(|table| {
187 let mut target =
188 ctx.create_texture(&TextureDescriptor::offscreen(pass.extent, format))?;
189 let result =
190 ctx.submit_batch_textured(&mut target, &pass.batch, pass.descriptor, &table);
191 Ok((target, result))
192 });
193 match outcome {
194 Ok((target, result)) => {
195 // The target joins the list whether or not the draw succeeded,
196 // so a failure releases it with the rest rather than leaking.
197 transient.layers.push(target);
198 if let Err(e) = result {
199 transient.destroy(ctx);
200 return Err(e);
201 }
202 }
203 Err(e) => {
204 transient.destroy(ctx);
205 return Err(e);
206 }
207 }
208 }
209 Ok(transient)
210}
211
212/// Render every pass of a recording, layers first, root into the surface.
213///
214/// `images` are the caller's own textures, addressed by the slot a paint named.
215///
216/// Layer targets are allocated per call and released before returning. Pooling
217/// them was assumed to be worth doing and is not: creating and destroying a
218/// 512x512 target measures 0.6 microseconds against 53 for the submission that
219/// draws into it, so a pool would save under two percent of what a pass costs
220/// and would owe an invalidation rule in exchange.
221pub fn execute<H: Hal>(
222 ctx: &mut H::Context,
223 surface: &mut H::Texture,
224 recording: &Recording,
225 images: &[&H::Texture],
226) -> Result<()>
227where
228 H::Context: HalContext<Hal = H>,
229{
230 let transient = execute_layers::<H>(ctx, recording, images, surface.format().intermediate())?;
231 let root = recording.root();
232 let outcome = resolve_sources::<H>(root, images, &transient)
233 .and_then(|table| ctx.submit_batch_textured(surface, &root.batch, root.descriptor, &table));
234 transient.destroy(ctx);
235 outcome
236}
237
238/// Render a recording into a texture this function allocates, and read it back.
239///
240/// The offscreen path, which is what a comparison run wants: no surface, no
241/// presentation, just the pixels. The target is released whichever way the
242/// render went, so a failing recording does not leak a texture into the rest of
243/// a run.
244pub fn render_offscreen<H: Hal>(
245 ctx: &mut H::Context,
246 recording: &Recording,
247 images: &[&H::Texture],
248) -> Result<Vec<u8>>
249where
250 H::Context: HalContext<Hal = H>,
251{
252 render_offscreen_into::<H>(ctx, recording, images, PixelFormat::Rgba8Unorm)
253}
254
255/// The same, into a target of the caller's choosing.
256///
257/// Separate rather than a parameter on the call above, because every caller in
258/// this workspace wants eight bits and threading the answer through all of them
259/// to serve the one that does not is the wrong way round. What this exists for
260/// is a target that can hold a color the sRGB primaries cannot describe, which
261/// needs a floating-point format and nothing else here does.
262///
263/// The bytes come back in whatever the format packs them as -- half-floats for
264/// `Rgba16Float` -- rather than converted to anything.
265pub fn render_offscreen_into<H: Hal>(
266 ctx: &mut H::Context,
267 recording: &Recording,
268 images: &[&H::Texture],
269 format: PixelFormat,
270) -> Result<Vec<u8>>
271where
272 H::Context: HalContext<Hal = H>,
273{
274 let mut target = ctx.create_texture(&TextureDescriptor::offscreen(recording.extent, format))?;
275 let outcome = execute::<H>(ctx, &mut target, recording, images)
276 .and_then(|()| ctx.read_texture(&mut target));
277 ctx.destroy_texture(target);
278 outcome
279}
280
281/// Render a recording's layers, then submit its root without waiting.
282///
283/// The shape a display path needs. Everything but the root is a prerequisite of
284/// it rather than part of the frame's pacing, so those go through the waiting
285/// submission and are ordered before the root by having finished; only the root
286/// produces a fence, because the fence is what a page flip is gated on and the
287/// root is the pass that fills the image being flipped.
288///
289/// Both halves of the return travel together until the fence retires: the layer
290/// targets are what the submission is still sampling. Handing them back rather
291/// than hiding them is deliberate — a caller that owns the fence is the only
292/// one that knows when it retires.
293pub fn execute_deferred<H: Hal>(
294 ctx: &mut H::Context,
295 surface: &mut H::Texture,
296 recording: &Recording,
297 images: &[&H::Texture],
298) -> Result<(H::Fence, Transient<H>)>
299where
300 H::Context: HalContext<Hal = H>,
301{
302 let transient = execute_layers::<H>(ctx, recording, images, surface.format().intermediate())?;
303 let root = recording.root();
304 let outcome = resolve_sources::<H>(root, images, &transient).and_then(|table| {
305 ctx.submit_batch_deferred_textured(surface, &root.batch, root.descriptor, &table)
306 });
307 match outcome {
308 Ok(fence) => Ok((fence, transient)),
309 Err(e) => {
310 // No fence means nothing is reading them, so this is the moment.
311 transient.destroy(ctx);
312 Err(e)
313 }
314 }
315}