Skip to main content

impeller/
context.rs

1//! A rendering context, over whichever backend is available.
2
3use crate::backend::{no_backend, Backend, BackendPreference};
4use impeller_core::Recording;
5use impeller_hal::{
6    Capabilities, Error, Extent2D, HalContext, PixelFormat, Result, RuntimeProgram,
7};
8
9/// A device to render with.
10///
11/// Which backend is behind it is decided at creation and reported by
12/// [`Context::backend`]. Callers branch on [`Context::capabilities`] rather than
13/// on which backend it turned out to be: a device without fence export is the
14/// same problem whether it is Vulkan or GLES.
15// The variants differ in size by a couple of kilobytes, and boxing the larger
16// one would even them out. That trade is backwards here: a process creates one
17// context and keeps it for its lifetime, so the saving is a one-off couple of
18// kilobytes, while the cost is an indirection on the way to every backend call
19// in the frame loop.
20#[allow(clippy::large_enum_variant)]
21pub enum Context {
22    #[cfg(feature = "vulkan")]
23    Vulkan(impeller_hal_vulkan::VulkanContext),
24    #[cfg(feature = "gles")]
25    Gles(impeller_hal_gles::GlesContext),
26}
27
28impl Context {
29    /// Create a context, choosing a backend.
30    pub fn new(preference: BackendPreference) -> Result<Self> {
31        let mut attempts = Vec::new();
32
33        #[cfg(feature = "vulkan")]
34        if matches!(
35            preference,
36            BackendPreference::Auto | BackendPreference::Vulkan
37        ) {
38            match impeller_hal_vulkan::VulkanContext::new(
39                impeller_hal_vulkan::DevicePreference::Auto,
40            ) {
41                Ok(ctx) => return Ok(Self::Vulkan(ctx)),
42                Err(e) => attempts.push((Backend::Vulkan, e)),
43            }
44        }
45
46        #[cfg(feature = "gles")]
47        if matches!(
48            preference,
49            BackendPreference::Auto | BackendPreference::Gles
50        ) {
51            match impeller_hal_gles::GlesContext::new(impeller_hal_gles::DisplayTarget::Surfaceless)
52            {
53                Ok(ctx) => return Ok(Self::Gles(ctx)),
54                Err(e) => attempts.push((Backend::Gles, e)),
55            }
56        }
57
58        let _ = preference;
59        Err(no_backend(attempts))
60    }
61
62    pub fn backend(&self) -> Backend {
63        match self {
64            #[cfg(feature = "vulkan")]
65            Self::Vulkan(_) => Backend::Vulkan,
66            #[cfg(feature = "gles")]
67            Self::Gles(_) => Backend::Gles,
68        }
69    }
70
71    pub fn capabilities(&self) -> &Capabilities {
72        match self {
73            #[cfg(feature = "vulkan")]
74            Self::Vulkan(ctx) => HalContext::capabilities(ctx),
75            #[cfg(feature = "gles")]
76            Self::Gles(ctx) => HalContext::capabilities(ctx),
77        }
78    }
79
80    /// Create a surface to draw into.
81    pub fn create_surface(&mut self, extent: Extent2D, format: PixelFormat) -> Result<Surface> {
82        match self {
83            #[cfg(feature = "vulkan")]
84            Self::Vulkan(ctx) => Ok(Surface::Vulkan(HalContext::create_texture(
85                ctx,
86                &impeller_hal::TextureDescriptor::offscreen(extent, format),
87            )?)),
88            #[cfg(feature = "gles")]
89            Self::Gles(ctx) => Ok(Surface::Gles(HalContext::create_texture(
90                ctx,
91                &impeller_hal::TextureDescriptor::offscreen(extent, format),
92            )?)),
93        }
94    }
95
96    /// Draw a recording into a surface.
97    pub fn draw(&mut self, surface: &mut Surface, recording: &Recording) -> Result<()> {
98        self.draw_with_images(surface, recording, &[])
99    }
100
101    /// Draw a recording whose paints sample images.
102    ///
103    /// `images` is the table a [`Shader::Image`] slot indexes, in slot order.
104    /// A recording carries slots rather than images because it is produced
105    /// without touching a device; supplying the table here is what resolves
106    /// them.
107    ///
108    /// The surface is borrowed mutably and the images immutably, so a recording
109    /// cannot sample the surface it draws into. That restriction is real, and
110    /// having the compiler state it beats discovering it as a picture that
111    /// differs by driver.
112    ///
113    /// [`Shader::Image`]: impeller_core::Shader::Image
114    pub fn draw_with_images(
115        &mut self,
116        surface: &mut Surface,
117        recording: &Recording,
118        images: &[&Image],
119    ) -> Result<()> {
120        match (self, surface) {
121            #[cfg(feature = "vulkan")]
122            (Self::Vulkan(ctx), Surface::Vulkan(texture)) => {
123                let images = vulkan_textures(images)?;
124                impeller_core::execute::<impeller_hal_vulkan::VulkanHal>(
125                    ctx, texture, recording, &images,
126                )
127            }
128            #[cfg(feature = "gles")]
129            (Self::Gles(ctx), Surface::Gles(texture)) => {
130                let images = gles_textures(images)?;
131                impeller_core::execute::<impeller_hal_gles::GlesHal>(
132                    ctx, texture, recording, &images,
133                )
134            }
135            // A surface belongs to the context that made it; pairing one with
136            // another context would use a handle the device never allocated.
137            #[allow(unreachable_patterns)]
138            _ => Err(Error::Unsupported(
139                "this surface was created by a different context",
140            )),
141        }
142    }
143
144    /// Register a fragment program a caller's own build produced.
145    ///
146    /// Returns the index to name it by, which is what goes into the `program`
147    /// of [`Shader::RuntimeEffect`]. Indices are per context: a recording that
148    /// names one is bound to the context that registered it, in exactly the
149    /// way one naming a texture slot is bound to the textures supplied
150    /// beside it.
151    ///
152    /// Nothing here compiles a shader. The payload for the backend in use has
153    /// to be present -- SPIR-V for Vulkan, GLSL ES for GLES -- and one built
154    /// for the other backend alone is refused rather than silently ignored.
155    /// See the architecture document for why a single source translated on
156    /// load was declined.
157    ///
158    /// [`Shader::RuntimeEffect`]: impeller_core::Shader::RuntimeEffect
159    pub fn register_program(&mut self, program: &RuntimeProgram) -> Result<u32> {
160        match self {
161            #[cfg(feature = "vulkan")]
162            Self::Vulkan(ctx) => ctx.register_program(program),
163            #[cfg(feature = "gles")]
164            Self::Gles(ctx) => ctx.register_program(program),
165        }
166    }
167
168    /// Allocate an image this context can sample.
169    ///
170    /// The format decides how the bytes written into it are read, and the
171    /// choice is not cosmetic -- though it now points the other way. Color
172    /// inside the renderer is sRGB-encoded, and an sRGB format decodes on
173    /// sample. A picture's bytes are sRGB-encoded,
174    /// because that is what every image file holds, and the pipeline wants them
175    /// encoded -- so a picture wants a *plain* format, and
176    /// [`PixelFormat::Rgba8UnormSrgb`] would decode it into light that nothing
177    /// downstream is expecting and wash it pale. Plain is also right for data
178    /// that is not color at all: coverage, a mask, a lookup table, anything
179    /// whose numbers mean themselves.
180    ///
181    /// This advice was the reverse of itself while the renderer worked in
182    /// light, and neither choice is detectable afterwards, which is why it is
183    /// stated here. Both produce an image; only one produces the right
184    /// picture.
185    pub fn create_image(&mut self, extent: Extent2D, format: PixelFormat) -> Result<Image> {
186        let descriptor = impeller_hal::TextureDescriptor::offscreen(extent, format);
187        match self {
188            #[cfg(feature = "vulkan")]
189            Self::Vulkan(ctx) => HalContext::create_texture(ctx, &descriptor).map(Image::Vulkan),
190            #[cfg(feature = "gles")]
191            Self::Gles(ctx) => HalContext::create_texture(ctx, &descriptor).map(Image::Gles),
192        }
193    }
194
195    /// An image with a full mip chain, for drawing smaller than its own size.
196    ///
197    /// The levels are filled when the image is written, not here, and the
198    /// caller supplies only the image itself: a chain built by any rule other
199    /// than the backend's own would sample differently on the two backends,
200    /// which is the one thing this renderer's tests exist to rule out.
201    ///
202    /// A third again in memory, which is why it is a separate call rather than
203    /// what every image gets. Only [`Sampling::Mipmap`] reads past the first
204    /// level, so an image created this way and drawn at any other quality costs
205    /// the memory and nothing else.
206    ///
207    /// [`Sampling::Mipmap`]: crate::Sampling::Mipmap
208    pub fn create_mipmapped_image(
209        &mut self,
210        extent: Extent2D,
211        format: PixelFormat,
212    ) -> Result<Image> {
213        let descriptor = impeller_hal::TextureDescriptor::mipmapped(extent, format);
214        match self {
215            #[cfg(feature = "vulkan")]
216            Self::Vulkan(ctx) => HalContext::create_texture(ctx, &descriptor).map(Image::Vulkan),
217            #[cfg(feature = "gles")]
218            Self::Gles(ctx) => HalContext::create_texture(ctx, &descriptor).map(Image::Gles),
219        }
220    }
221
222    /// Fill an image from host memory, tightly packed and top row first.
223    ///
224    /// Decoding is out of scope for this crate; bring `image` or another
225    /// decoder and hand the pixels here.
226    pub fn write_image(&mut self, image: &mut Image, pixels: &[u8]) -> Result<()> {
227        match (self, image) {
228            #[cfg(feature = "vulkan")]
229            (Self::Vulkan(ctx), Image::Vulkan(texture)) => {
230                HalContext::write_texture(ctx, texture, pixels)
231            }
232            #[cfg(feature = "gles")]
233            (Self::Gles(ctx), Image::Gles(texture)) => {
234                HalContext::write_texture(ctx, texture, pixels)
235            }
236            #[allow(unreachable_patterns)]
237            _ => Err(Error::Unsupported(
238                "this image was created by a different context",
239            )),
240        }
241    }
242
243    /// Release an image.
244    ///
245    /// Explicit for the same reason a surface is: the memory belongs to the
246    /// context, which an image cannot reach from its own `Drop`.
247    pub fn destroy_image(&mut self, image: Image) {
248        match (self, image) {
249            #[cfg(feature = "vulkan")]
250            (Self::Vulkan(ctx), Image::Vulkan(texture)) => {
251                HalContext::destroy_texture(ctx, texture)
252            }
253            #[cfg(feature = "gles")]
254            (Self::Gles(ctx), Image::Gles(texture)) => HalContext::destroy_texture(ctx, texture),
255            #[allow(unreachable_patterns)]
256            _ => {}
257        }
258    }
259
260    /// Read a surface back, tightly packed, in whatever its format holds.
261    ///
262    /// Four bytes per pixel for the eight-bit formats, which is every surface a
263    /// caller is likely to ask for; eight for `Rgba16Float`, as four
264    /// half-floats. Nothing is converted on the way out -- the bytes are what
265    /// the target holds, and a caller who chose a format wider than eight bits
266    /// did so in order to read what it kept.
267    pub fn read(&mut self, surface: &mut Surface) -> Result<Vec<u8>> {
268        match (self, surface) {
269            #[cfg(feature = "vulkan")]
270            (Self::Vulkan(ctx), Surface::Vulkan(texture)) => HalContext::read_texture(ctx, texture),
271            #[cfg(feature = "gles")]
272            (Self::Gles(ctx), Surface::Gles(texture)) => HalContext::read_texture(ctx, texture),
273            #[allow(unreachable_patterns)]
274            _ => Err(Error::Unsupported(
275                "this surface was created by a different context",
276            )),
277        }
278    }
279
280    /// Release a surface.
281    ///
282    /// Explicit because the memory belongs to the context, which a surface
283    /// cannot reach from its own `Drop`.
284    pub fn destroy_surface(&mut self, surface: Surface) {
285        match (self, surface) {
286            #[cfg(feature = "vulkan")]
287            (Self::Vulkan(ctx), Surface::Vulkan(texture)) => {
288                HalContext::destroy_texture(ctx, texture)
289            }
290            #[cfg(feature = "gles")]
291            (Self::Gles(ctx), Surface::Gles(texture)) => HalContext::destroy_texture(ctx, texture),
292            #[allow(unreachable_patterns)]
293            _ => {}
294        }
295    }
296}
297
298/// Something to draw into.
299/// A texture a paint can sample.
300///
301/// Distinct from a [`Surface`] in the type system even though both are textures
302/// underneath, because the two are used in opposite directions and mixing them
303/// up is exactly the mistake worth making impossible: a surface is drawn into,
304/// an image is read from.
305pub enum Image {
306    #[cfg(feature = "vulkan")]
307    Vulkan(impeller_hal_vulkan::VulkanTexture),
308    #[cfg(feature = "gles")]
309    Gles(impeller_hal_gles::GlesTexture),
310}
311
312impl Image {
313    pub fn extent(&self) -> Extent2D {
314        match self {
315            #[cfg(feature = "vulkan")]
316            Self::Vulkan(texture) => texture.extent(),
317            #[cfg(feature = "gles")]
318            Self::Gles(texture) => texture.extent(),
319        }
320    }
321}
322
323/// Unwrap a table of images to one backend's textures.
324///
325/// An image from another backend is an error rather than a skip: the slots are
326/// positional, so dropping one would silently shift every slot after it.
327#[cfg(feature = "vulkan")]
328fn vulkan_textures<'a>(
329    images: &[&'a Image],
330) -> Result<Vec<&'a impeller_hal_vulkan::VulkanTexture>> {
331    images
332        .iter()
333        .map(|image| match image {
334            Image::Vulkan(texture) => Ok(texture),
335            #[allow(unreachable_patterns)]
336            _ => Err(Error::Unsupported(
337                "this image was created by a different context",
338            )),
339        })
340        .collect()
341}
342
343#[cfg(feature = "gles")]
344fn gles_textures<'a>(images: &[&'a Image]) -> Result<Vec<&'a impeller_hal_gles::GlesTexture>> {
345    images
346        .iter()
347        .map(|image| match image {
348            Image::Gles(texture) => Ok(texture),
349            #[allow(unreachable_patterns)]
350            _ => Err(Error::Unsupported(
351                "this image was created by a different context",
352            )),
353        })
354        .collect()
355}
356
357pub enum Surface {
358    #[cfg(feature = "vulkan")]
359    Vulkan(impeller_hal_vulkan::VulkanTexture),
360    #[cfg(feature = "gles")]
361    Gles(impeller_hal_gles::GlesTexture),
362}
363
364impl Surface {
365    pub fn extent(&self) -> Extent2D {
366        match self {
367            #[cfg(feature = "vulkan")]
368            Self::Vulkan(texture) => texture.extent(),
369            #[cfg(feature = "gles")]
370            Self::Gles(texture) => texture.extent(),
371        }
372    }
373
374    pub fn format(&self) -> PixelFormat {
375        match self {
376            #[cfg(feature = "vulkan")]
377            Self::Vulkan(texture) => texture.format(),
378            #[cfg(feature = "gles")]
379            Self::Gles(texture) => texture.format(),
380        }
381    }
382}