Skip to main content

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}