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}