Skip to main content

impeller_hal_vulkan/
render.rs

1//! Render passes, pipelines, and drawing.
2//!
3//! Draws are accumulated into a [`Batch`] and submitted together: one geometry
4//! upload, one render pass, one submission for the whole thing. A draw per
5//! submission is fine for a test and hopeless for a frame loop, where the
6//! fixed cost of beginning a pass and waiting on a fence dwarfs the drawing.
7
8use crate::device::VulkanContext;
9use crate::resource::{backend_err, transition, vk_format, VulkanTexture};
10use ash::vk;
11use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme};
12use gpu_allocator::MemoryLocation;
13use impeller_hal::{
14    Batch, BlendMode, ClipRole, Error, Extent2D, PassDescriptor, Result, TextureDescriptor,
15    TextureUsage,
16};
17use std::collections::HashMap;
18
19/// A render pass is distinguished by the attachment it targets and whether it
20/// clears, because the load operation is baked into the object.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub(crate) struct RenderPassKey {
23    pub(crate) format: vk::Format,
24    pub(crate) clears: bool,
25    /// Sample count of the attachment being rendered into. Above one, the pass
26    /// carries a resolve attachment as well.
27    pub(crate) samples: u32,
28    /// Whether the pass leaves its target ready for presentation.
29    ///
30    /// Part of the key because it changes an attachment's final layout, and
31    /// two passes whose attachments differ are not compatible.
32    pub(crate) presents: bool,
33    /// The stencil attachment's format, or `None` for a pass that has none.
34    ///
35    /// Part of the key rather than a flag because passes are only compatible
36    /// when their attachment formats match, so a pipeline built against a pass
37    /// with a stencil cannot be used with one without.
38    pub(crate) stencil: Option<vk::Format>,
39}
40
41/// A pipeline is distinguished by format and blend state.
42///
43/// Not by load operation: render passes with matching attachment formats and
44/// sample counts are compatible, so one pipeline is usable with both the
45/// clearing and the preserving pass. That compatibility is what lets a single
46/// batch mix blend modes inside one pass.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub(crate) struct PipelineKey {
49    pub(crate) format: vk::Format,
50    pub(crate) blend: BlendMode,
51    /// Rasterization sample count, which is baked into a pipeline and must
52    /// match the render pass it is used with.
53    pub(crate) samples: u32,
54    /// The stencil attachment's format, or `None` where the pass has none.
55    pub(crate) stencil: Option<vk::Format>,
56    /// Which fragment program this pipeline runs.
57    ///
58    /// `None` is the built-in shader every material shares. `Some` names a
59    /// caller's, registered with the context -- which is the whole of what a
60    /// runtime effect is here: a pipeline built from a module this renderer
61    /// did not know about when it was built.
62    pub(crate) program: Option<u32>,
63    /// What this pipeline does with the stencil.
64    ///
65    /// The value compared against is dynamic state rather than part of the key,
66    /// so a clip stack of any depth costs three pipelines rather than three per
67    /// level. Only the operation differs between them, and that is baked in.
68    pub(crate) role: ClipRole,
69}
70
71/// Cached pipeline objects, built on demand and held for the context's life.
72///
73/// Pipeline creation costs milliseconds, so building one per draw would put
74/// exactly the compilation stall in the frame loop that compiling everything
75/// ahead of time is meant to avoid.
76#[derive(Default)]
77pub(crate) struct PipelineCache {
78    pub(crate) layout: Option<vk::PipelineLayout>,
79    pub(crate) render_passes: HashMap<RenderPassKey, vk::RenderPass>,
80    pub(crate) pipelines: HashMap<PipelineKey, vk::Pipeline>,
81}
82
83impl PipelineCache {
84    pub(crate) fn render_pass(&self, key: RenderPassKey) -> Option<vk::RenderPass> {
85        self.render_passes.get(&key).copied()
86    }
87
88    pub(crate) fn pipeline(&self, key: PipelineKey) -> Option<vk::Pipeline> {
89        self.pipelines.get(&key).copied()
90    }
91
92    pub(crate) fn layout(&self) -> Option<vk::PipelineLayout> {
93        self.layout
94    }
95
96    pub(crate) fn destroy(&mut self, device: &ash::Device) {
97        // SAFETY: called from context teardown, after the queue has drained.
98        unsafe {
99            for pipeline in self.pipelines.values() {
100                device.destroy_pipeline(*pipeline, None);
101            }
102            for pass in self.render_passes.values() {
103                device.destroy_render_pass(*pass, None);
104            }
105            if let Some(layout) = self.layout {
106                device.destroy_pipeline_layout(layout, None);
107            }
108        }
109        self.pipelines.clear();
110        self.render_passes.clear();
111        self.layout = None;
112    }
113}
114
115impl VulkanContext {
116    /// Draw a single set of triangles. A convenience over [`Batch`].
117    ///
118    /// The material is in linear space with **straight alpha**; it is
119    /// premultiplied on the way to the target, which holds premultiplied color.
120    pub fn draw_indexed(
121        &mut self,
122        target: &mut VulkanTexture,
123        vertices: &[[f32; 2]],
124        indices: &[u32],
125        material: impeller_hal::Material,
126        blend: BlendMode,
127        clear: Option<[f32; 4]>,
128    ) -> Result<()> {
129        let mut batch = Batch::new();
130        batch.push(vertices, indices, material, blend)?;
131        self.submit_batch(
132            target,
133            &batch,
134            PassDescriptor {
135                clear,
136                samples: 1,
137                viewport: None,
138            },
139        )
140    }
141
142    /// Record and submit a whole batch as one render pass.
143    pub fn submit_batch(
144        &mut self,
145        target: &mut VulkanTexture,
146        batch: &Batch,
147        pass: PassDescriptor,
148    ) -> Result<()> {
149        self.submit_batch_textured(target, batch, pass, &[])
150    }
151
152    /// Record and submit a whole batch as one render pass, with a texture table
153    /// its image paints index.
154    pub fn submit_batch_textured(
155        &mut self,
156        target: &mut VulkanTexture,
157        batch: &Batch,
158        pass: PassDescriptor,
159        textures: &[&VulkanTexture],
160    ) -> Result<()> {
161        let format = vk_format(target.format());
162        let clear = pass.clear;
163
164        self.capabilities().check_blend_modes(batch)?;
165        if !pass.samples.is_power_of_two() {
166            return Err(Error::Unsupported("sample count is not a power of two"));
167        }
168        if !self.capabilities().sample_counts.supports(pass.samples) {
169            return Err(Error::Unsupported("sample count not supported by device"));
170        }
171        if pass.is_multisampled() && clear.is_none() {
172            // A multisample pass renders into a transient buffer and resolves
173            // out of it. Preserving would mean seeding that buffer with the
174            // target's existing contents, and there is no reverse of a resolve
175            // to do it with -- it would take a full-screen draw. Refusing is
176            // better than silently discarding what the target held.
177            return Err(Error::Unsupported(
178                "a multisampled pass must clear: give the pass a clear color, \
179                 or draw it at one sample. Preserving would need a \
180                 resolved-to-multisample copy, which this technique has no \
181                 reverse of",
182            ));
183        }
184
185        // Nothing to draw still clears, which is what a caller submitting an
186        // empty scene expects. With no clear either, there is nothing to do.
187        if batch.is_empty() {
188            return match clear {
189                Some(c) => self.clear_texture(target, c),
190                None => Ok(()),
191            };
192        }
193
194        let stencil_format = self.stencil_format_for(batch)?;
195        let pass_key = RenderPassKey {
196            format,
197            clears: clear.is_some(),
198            samples: pass.samples,
199            presents: false,
200            stencil: stencil_format,
201        };
202        let render_pass = self.ensure_render_pass(pass_key)?;
203        for draw in batch.draws() {
204            self.ensure_pipeline(
205                PipelineKey {
206                    format,
207                    program: draw.material.program(),
208                    blend: draw.blend,
209                    samples: pass.samples,
210                    stencil: stencil_format,
211                    role: draw.stencil.role,
212                },
213                render_pass,
214            )?;
215        }
216
217        let bindings = crate::sampling::build(self, batch, textures)?;
218
219        let device = self.raw_device().clone();
220        let vertex_buffer = self.upload(
221            cast_bytes(batch.vertices()),
222            vk::BufferUsageFlags::VERTEX_BUFFER,
223        )?;
224        let index_buffer = self.upload(
225            cast_bytes(batch.indices()),
226            vk::BufferUsageFlags::INDEX_BUFFER,
227        )?;
228        let (material_buffer, materials) = crate::materials::build(self, batch, target.format())?;
229
230        let result = self.record_batch(
231            &device,
232            target,
233            format,
234            render_pass,
235            batch,
236            vertex_buffer.buffer,
237            index_buffer.buffer,
238            pass,
239            stencil_format,
240            &bindings,
241            &materials,
242            textures,
243        );
244
245        self.release(vertex_buffer);
246        self.release(index_buffer);
247        self.release(material_buffer);
248        materials.destroy(&device);
249        bindings.destroy(self);
250        result
251    }
252
253    #[allow(clippy::too_many_arguments)]
254    fn record_batch(
255        &mut self,
256        device: &ash::Device,
257        target: &mut VulkanTexture,
258        format: vk::Format,
259        render_pass: vk::RenderPass,
260        batch: &Batch,
261        vertex_buffer: vk::Buffer,
262        index_buffer: vk::Buffer,
263        pass: PassDescriptor,
264        stencil_format: Option<vk::Format>,
265        bindings: &crate::sampling::Bindings,
266        materials: &crate::materials::Materials,
267        sampled: &[&VulkanTexture],
268    ) -> Result<()> {
269        let layout = self.pipeline_cache().layout().expect("ensured above");
270        let extent = target.extent();
271        let clear = pass.clear;
272
273        let view_info = vk::ImageViewCreateInfo::default()
274            .image(target.raw_image())
275            .view_type(vk::ImageViewType::TYPE_2D)
276            .format(format)
277            .subresource_range(
278                vk::ImageSubresourceRange::default()
279                    .aspect_mask(vk::ImageAspectFlags::COLOR)
280                    .level_count(1)
281                    .layer_count(1),
282            );
283        let view = unsafe { device.create_image_view(&view_info, None) }
284            .map_err(|e| backend_err("create_image_view", e))?;
285
286        // The multisample buffer is transient: rendered into, resolved out of,
287        // and discarded. Allocating it per submission is wasteful and will move
288        // into the frame's resource pool once one exists; keeping it here for
289        // now avoids a cache whose invalidation rules nothing yet needs.
290        let multisample = if pass.is_multisampled() {
291            match self.create_multisample_buffer(target, pass.samples) {
292                Ok(ms) => Some(ms),
293                Err(e) => {
294                    unsafe { device.destroy_image_view(view, None) };
295                    return Err(e);
296                }
297            }
298        } else {
299            None
300        };
301
302        // Matches the multisample buffer in lifetime and in reason: written
303        // during the pass, never read afterwards, discarded at the end.
304        let stencil = match stencil_format {
305            Some(format) => match crate::stencil::create(self, extent, pass.samples, format) {
306                Ok(buffer) => Some(buffer),
307                Err(e) => {
308                    unsafe { device.destroy_image_view(view, None) };
309                    if let Some((tex, ms_view)) = multisample {
310                        unsafe { device.destroy_image_view(ms_view, None) };
311                        self.destroy_texture(tex);
312                    }
313                    return Err(e);
314                }
315            },
316            None => None,
317        };
318
319        let mut attachments: Vec<vk::ImageView> = match &multisample {
320            // Order matches the render pass: the multisample attachment first,
321            // then the resolve target the caller reads.
322            Some((_, ms_view)) => vec![*ms_view, view],
323            None => vec![view],
324        };
325        if let Some(stencil) = &stencil {
326            attachments.push(stencil.view);
327        }
328        let fb_info = vk::FramebufferCreateInfo::default()
329            .render_pass(render_pass)
330            .attachments(&attachments)
331            .width(extent.width)
332            .height(extent.height)
333            .layers(1);
334        let framebuffer = match unsafe { device.create_framebuffer(&fb_info, None) } {
335            Ok(fb) => fb,
336            Err(e) => {
337                unsafe { device.destroy_image_view(view, None) };
338                if let Some((tex, ms_view)) = multisample {
339                    unsafe { device.destroy_image_view(ms_view, None) };
340                    self.destroy_texture(tex);
341                }
342                if let Some(stencil) = stencil {
343                    stencil.destroy(self);
344                }
345                return Err(backend_err("create_framebuffer", e));
346            }
347        };
348
349        let previous_layout = target.layout();
350        let outcome = (|| -> Result<()> {
351            let cmd = self.begin_one_shot()?;
352
353            // Before the render pass begins, since a layout transition is not
354            // legal inside one. Whatever left these in a color-attachment or
355            // transfer layout, a shader cannot read them there.
356            crate::sampling::transition_for_sampling(device, cmd, sampled);
357
358            if clear.is_none() {
359                // Loading requires the attachment already be in the layout the
360                // render pass declares, and the texture may be sitting in
361                // whatever a previous readback left it in.
362                transition(
363                    device,
364                    cmd,
365                    target.raw_image(),
366                    previous_layout,
367                    vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
368                );
369            }
370
371            // One clear value per attachment, even though the resolve target's
372            // load operation discards it. The stencil starts at zero, which is
373            // the depth unclipped content draws at, so a pass with no clip
374            // draws behaves exactly as one with no stencil attachment.
375            let mut clear_values = vec![vk::ClearValue {
376                color: vk::ClearColorValue {
377                    float32: clear.unwrap_or([0.0; 4]),
378                },
379            }];
380            clear_values.resize(attachments.len(), vk::ClearValue::default());
381            let clear_values = &clear_values[..];
382            let area = vk::Rect2D {
383                offset: vk::Offset2D { x: 0, y: 0 },
384                extent: vk::Extent2D {
385                    width: extent.width,
386                    height: extent.height,
387                },
388            };
389            let begin = vk::RenderPassBeginInfo::default()
390                .render_pass(render_pass)
391                .framebuffer(framebuffer)
392                .render_area(area)
393                .clear_values(clear_values);
394
395            let viewport = pass_viewport(pass, extent);
396
397            unsafe {
398                device.cmd_begin_render_pass(cmd, &begin, vk::SubpassContents::INLINE);
399                device.cmd_set_viewport(cmd, 0, &[viewport]);
400                device.cmd_set_scissor(cmd, 0, &[area]);
401                device.cmd_bind_vertex_buffers(cmd, 0, &[vertex_buffer], &[0]);
402                device.cmd_bind_index_buffer(cmd, index_buffer, 0, vk::IndexType::UINT32);
403
404                let recording = PassRecording {
405                    device,
406                    cmd,
407                    layout,
408                    area,
409                    bindings,
410                    materials,
411                };
412                let mut state = RecordedState::at_pass_start(area);
413                for (index, draw) in batch.draws().iter().enumerate() {
414                    let pipeline = self
415                        .pipeline_cache()
416                        .pipeline(PipelineKey {
417                            format,
418                            program: draw.material.program(),
419                            blend: draw.blend,
420                            samples: pass.samples,
421                            stencil: stencil_format,
422                            role: draw.stencil.role,
423                        })
424                        .expect("ensured above");
425                    record_draw(&recording, draw, index, pipeline, &mut state);
426                }
427
428                device.cmd_end_render_pass(cmd);
429            }
430
431            self.submit_one_shot(cmd)
432        })();
433
434        // SAFETY: the submission above was waited on, so neither object is in
435        // use regardless of the result.
436        unsafe {
437            device.destroy_framebuffer(framebuffer, None);
438            device.destroy_image_view(view, None);
439        }
440        if let Some((tex, ms_view)) = multisample {
441            unsafe { device.destroy_image_view(ms_view, None) };
442            self.destroy_texture(tex);
443        }
444        if let Some(stencil) = stencil {
445            stencil.destroy(self);
446        }
447
448        if outcome.is_ok() {
449            // The render pass declares this as its final layout, so the next
450            // operation must transition from here rather than from whatever
451            // the texture held before.
452            target.set_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
453        }
454        outcome
455    }
456
457    /// Allocate a transient multisample color buffer matching `target`.
458    /// The stencil format this submission needs, or `None` if it needs none.
459    ///
460    /// Resolved once per submission rather than per draw so that every key
461    /// built from it agrees, and refused up front where the clip stack is
462    /// deeper than a stencil can distinguish: past that point the value wraps
463    /// to zero and the clip admits everything it was meant to exclude, which is
464    /// a picture rather than an error.
465    fn stencil_format_for(&self, batch: &Batch) -> Result<Option<vk::Format>> {
466        if !batch.uses_stencil() {
467            return Ok(None);
468        }
469        batch.check_clip_depth()?;
470        crate::stencil::stencil_format(self).map(Some)
471    }
472
473    fn create_multisample_buffer(
474        &mut self,
475        target: &VulkanTexture,
476        samples: u32,
477    ) -> Result<(VulkanTexture, vk::ImageView)> {
478        let desc = TextureDescriptor {
479            extent: target.extent(),
480            format: target.format(),
481            usage: TextureUsage {
482                render_target: true,
483                ..TextureUsage::default()
484            },
485            sample_count: samples,
486            // A multisample buffer is resolved, never sampled, so a chain over
487            // it would be levels nothing reads.
488            mip_levels: 1,
489            #[cfg(unix)]
490            external: None,
491        };
492        let texture = self.create_texture(&desc)?;
493
494        let device = self.raw_device().clone();
495        let info = vk::ImageViewCreateInfo::default()
496            .image(texture.raw_image())
497            .view_type(vk::ImageViewType::TYPE_2D)
498            .format(vk_format(target.format()))
499            .subresource_range(
500                vk::ImageSubresourceRange::default()
501                    .aspect_mask(vk::ImageAspectFlags::COLOR)
502                    .level_count(1)
503                    .layer_count(1),
504            );
505        match unsafe { device.create_image_view(&info, None) } {
506            Ok(view) => Ok((texture, view)),
507            Err(e) => {
508                self.destroy_texture(texture);
509                Err(backend_err("create_image_view", e))
510            }
511        }
512    }
513
514    /// Submit a batch without waiting, returning a fence that signals when the
515    /// GPU is finished.
516    ///
517    /// The waiting form is right for tests and setup. A frame loop needs this
518    /// one: it hands the caller something to attach to a page flip, or to wait
519    /// on while the next frame is already being recorded. Everything the
520    /// submission still needs — the command buffer and the pass's transient
521    /// framebuffer objects — travels with the fence, because none of it can be
522    /// released until the work completes.
523    ///
524    /// The returned fence must be handed back to [`Self::retire_fence`].
525    pub fn submit_batch_deferred(
526        &mut self,
527        target: &mut VulkanTexture,
528        batch: &Batch,
529        pass: PassDescriptor,
530    ) -> Result<crate::fence::VulkanFence> {
531        self.submit_batch_deferred_synchronized(target, batch, pass, &[], Default::default())
532    }
533
534    /// Submit without waiting, gating the work on semaphores the caller owns.
535    ///
536    /// What a swapchain needs: the render must not touch the image before
537    /// acquisition has signalled, and presentation must not read it before the
538    /// render has. Neither is expressible with a fence, because both are
539    /// device-side orderings that no one should be blocking a thread to
540    /// enforce.
541    /// `textures` is the table a [`impeller_hal::Material::Image`] slot indexes, exactly as
542    /// the waiting form takes it. A frame that composites a layer needs it:
543    /// the pass that lands in a presentable image is the one that samples the
544    /// layer, so a deferred submission that could not sample anything meant a
545    /// frame with layers could be rendered offscreen and never presented.
546    pub fn submit_batch_deferred_synchronized(
547        &mut self,
548        target: &mut VulkanTexture,
549        batch: &Batch,
550        pass: PassDescriptor,
551        textures: &[&VulkanTexture],
552        sync: crate::device::FrameSync<'_>,
553    ) -> Result<crate::fence::VulkanFence> {
554        if pass.is_multisampled() {
555            // The transient multisample buffer would have to outlive the
556            // submission too. Supporting it means putting it in the fence
557            // alongside the rest, which is worth doing when a caller needs it.
558            return Err(Error::Unsupported(
559                "deferred submission of a multisampled pass",
560            ));
561        }
562        let format = vk_format(target.format());
563        if batch.is_empty() {
564            return Err(Error::Unsupported("deferred submission of an empty batch"));
565        }
566
567        let stencil_format = self.stencil_format_for(batch)?;
568        let pass_key = RenderPassKey {
569            format,
570            clears: pass.clear.is_some(),
571            samples: 1,
572            presents: sync.presents,
573            stencil: stencil_format,
574        };
575        let render_pass = self.ensure_render_pass(pass_key)?;
576        for draw in batch.draws() {
577            self.ensure_pipeline(
578                PipelineKey {
579                    format,
580                    program: draw.material.program(),
581                    blend: draw.blend,
582                    samples: 1,
583                    stencil: stencil_format,
584                    role: draw.stencil.role,
585                },
586                render_pass,
587            )?;
588        }
589
590        let device = self.raw_device().clone();
591        let vertex_buffer = self.upload(
592            cast_bytes(batch.vertices()),
593            vk::BufferUsageFlags::VERTEX_BUFFER,
594        )?;
595        let index_buffer = self.upload(
596            cast_bytes(batch.indices()),
597            vk::BufferUsageFlags::INDEX_BUFFER,
598        )?;
599
600        let result = self.record_deferred(
601            &device,
602            target,
603            format,
604            render_pass,
605            batch,
606            vertex_buffer.buffer,
607            index_buffer.buffer,
608            pass,
609            stencil_format,
610            textures,
611            sync,
612        );
613
614        // The geometry buffers are host-visible and were fully written before
615        // submission, so releasing them here would free memory the GPU is still
616        // reading. They are retained until the fence retires.
617        match result {
618            Ok(mut fence) => {
619                // Held by this fence rather than by the context, so that
620                // retiring one frame cannot free another's geometry.
621                fence.retained.push(vertex_buffer);
622                fence.retained.push(index_buffer);
623                Ok(fence)
624            }
625            Err(e) => {
626                self.release(vertex_buffer);
627                self.release(index_buffer);
628                Err(e)
629            }
630        }
631    }
632
633    /// Release a deferred submission once its work has completed.
634    pub fn retire_fence(&mut self, mut fence: crate::fence::VulkanFence) {
635        // Waits for the submission, then releases what it held -- in that
636        // order, since the buffers are what the submission was reading.
637        fence.retire();
638        for buffer in std::mem::take(&mut fence.retained) {
639            self.release(buffer);
640        }
641        // After the wait inside `retire`, so nothing is still reading the sets.
642        if let Some(bindings) = fence.bindings.take() {
643            bindings.destroy(self);
644        }
645        if let Some(materials) = fence.materials.take() {
646            materials.destroy(&self.raw_device().clone());
647        }
648    }
649
650    #[allow(clippy::too_many_arguments)]
651    fn record_deferred(
652        &mut self,
653        device: &ash::Device,
654        target: &mut VulkanTexture,
655        format: vk::Format,
656        render_pass: vk::RenderPass,
657        batch: &Batch,
658        vertex_buffer: vk::Buffer,
659        index_buffer: vk::Buffer,
660        pass: PassDescriptor,
661        stencil_format: Option<vk::Format>,
662        textures: &[&VulkanTexture],
663        sync: crate::device::FrameSync<'_>,
664    ) -> Result<crate::fence::VulkanFence> {
665        // The pool these sets come from cannot be destroyed while the command
666        // buffer reading them is in flight, and a deferred submission is in
667        // flight for as long as the caller likes -- so the bindings travel with
668        // the fence, alongside the framebuffer and view they sit next to here.
669        let bindings = crate::sampling::build(self, batch, textures)?;
670        let (material_buffer, materials) = crate::materials::build(self, batch, target.format())?;
671        let layout = self.pipeline_cache().layout().expect("ensured above");
672        let extent = target.extent();
673
674        let view_info = vk::ImageViewCreateInfo::default()
675            .image(target.raw_image())
676            .view_type(vk::ImageViewType::TYPE_2D)
677            .format(format)
678            .subresource_range(
679                vk::ImageSubresourceRange::default()
680                    .aspect_mask(vk::ImageAspectFlags::COLOR)
681                    .level_count(1)
682                    .layer_count(1),
683            );
684        let view = unsafe { device.create_image_view(&view_info, None) }
685            .map_err(|e| backend_err("create_image_view", e))?;
686
687        let attachments = [view];
688        let fb_info = vk::FramebufferCreateInfo::default()
689            .render_pass(render_pass)
690            .attachments(&attachments)
691            .width(extent.width)
692            .height(extent.height)
693            .layers(1);
694        let framebuffer = match unsafe { device.create_framebuffer(&fb_info, None) } {
695            Ok(fb) => fb,
696            Err(e) => {
697                unsafe { device.destroy_image_view(view, None) };
698                return Err(backend_err("create_framebuffer", e));
699            }
700        };
701
702        // Pipelines are resolved before recording so the loop below needs
703        // nothing from the cache, which keeps the recording free of borrows on
704        // the context while it holds the command buffer.
705        let pipelines: Vec<vk::Pipeline> = batch
706            .draws()
707            .iter()
708            .map(|draw| {
709                self.pipeline_cache()
710                    .pipeline(PipelineKey {
711                        format,
712                        program: draw.material.program(),
713                        blend: draw.blend,
714                        samples: 1,
715                        stencil: stencil_format,
716                        role: draw.stencil.role,
717                    })
718                    .expect("ensured above")
719            })
720            .collect();
721
722        let previous_layout = target.layout();
723        let cmd = self.begin_one_shot()?;
724
725        // SAFETY: every object below belongs to this device and the command
726        // buffer is in the recording state.
727        unsafe {
728            // Before the render pass begins, since a layout transition is not
729            // legal inside one. A layer target sits in the color-attachment
730            // layout its own pass left it in, which a shader cannot read.
731            crate::sampling::transition_for_sampling(device, cmd, textures);
732
733            if pass.clear.is_none() {
734                transition(
735                    device,
736                    cmd,
737                    target.raw_image(),
738                    previous_layout,
739                    vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
740                );
741            }
742
743            let clear_values = [vk::ClearValue {
744                color: vk::ClearColorValue {
745                    float32: pass.clear.unwrap_or([0.0; 4]),
746                },
747            }];
748            let area = vk::Rect2D {
749                offset: vk::Offset2D { x: 0, y: 0 },
750                extent: vk::Extent2D {
751                    width: extent.width,
752                    height: extent.height,
753                },
754            };
755            let begin = vk::RenderPassBeginInfo::default()
756                .render_pass(render_pass)
757                .framebuffer(framebuffer)
758                .render_area(area)
759                .clear_values(&clear_values);
760            let viewport = pass_viewport(pass, extent);
761
762            device.cmd_begin_render_pass(cmd, &begin, vk::SubpassContents::INLINE);
763            device.cmd_set_viewport(cmd, 0, &[viewport]);
764            device.cmd_set_scissor(cmd, 0, &[area]);
765            device.cmd_bind_vertex_buffers(cmd, 0, &[vertex_buffer], &[0]);
766            device.cmd_bind_index_buffer(cmd, index_buffer, 0, vk::IndexType::UINT32);
767
768            let recording = PassRecording {
769                device,
770                cmd,
771                layout,
772                area,
773                bindings: &bindings,
774                materials: &materials,
775            };
776            let mut state = RecordedState::at_pass_start(area);
777            for (index, (draw, pipeline)) in batch.draws().iter().zip(&pipelines).enumerate() {
778                record_draw(&recording, draw, index, *pipeline, &mut state);
779            }
780            device.cmd_end_render_pass(cmd);
781        }
782
783        let mut fence = self.submit_exportable(cmd, framebuffer, view, sync)?;
784        fence.bindings = Some(bindings);
785        fence.materials = Some(materials);
786        fence.retained.push(material_buffer);
787        target.set_layout(if sync.presents {
788            vk::ImageLayout::PRESENT_SRC_KHR
789        } else {
790            vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL
791        });
792        Ok(fence)
793    }
794
795    fn ensure_render_pass(&mut self, key: RenderPassKey) -> Result<vk::RenderPass> {
796        if let Some(pass) = self.pipeline_cache().render_pass(key) {
797            return Ok(pass);
798        }
799        let device = self.raw_device().clone();
800        let pass = build_render_pass(&device, key)?;
801        self.pipeline_cache_mut().render_passes.insert(key, pass);
802        Ok(pass)
803    }
804
805    fn ensure_pipeline(&mut self, key: PipelineKey, render_pass: vk::RenderPass) -> Result<()> {
806        if self.pipeline_cache().pipeline(key).is_some() {
807            return Ok(());
808        }
809        let device = self.raw_device().clone();
810        let layout = match self.pipeline_cache().layout() {
811            Some(l) => l,
812            None => {
813                // Built here rather than lazily beside the sampler, because
814                // every pipeline declares the set whether it samples or not
815                // and so cannot be created without it.
816                let descriptors = self.descriptor_layout()?;
817                let materials = self.material_layout()?;
818                let l = build_pipeline_layout(&device, descriptors, materials)?;
819                self.pipeline_cache_mut().layout = Some(l);
820                l
821            }
822        };
823        // Cloned out of the registry before building, because the build needs
824        // the device mutably and a borrowed payload would hold the context.
825        let fragment = match key.program {
826            Some(id) => Some(self.runtime_program(id)?.to_vec()),
827            None => None,
828        };
829        let pipeline = build_pipeline(&device, key, render_pass, layout, fragment.as_deref())?;
830        self.pipeline_cache_mut().pipelines.insert(key, pipeline);
831        Ok(())
832    }
833
834    pub(crate) fn upload(
835        &mut self,
836        bytes: &[u8],
837        usage: vk::BufferUsageFlags,
838    ) -> Result<StagedBuffer> {
839        let device = self.raw_device().clone();
840        let info = vk::BufferCreateInfo::default()
841            .size(bytes.len().max(1) as u64)
842            .usage(usage)
843            .sharing_mode(vk::SharingMode::EXCLUSIVE);
844        let buffer = unsafe { device.create_buffer(&info, None) }
845            .map_err(|e| backend_err("create_buffer", e))?;
846
847        let requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
848        let mut allocation = self
849            .allocator_mut()
850            .allocate(&AllocationCreateDesc {
851                name: "upload",
852                requirements,
853                // Host-visible so the write below needs no staging copy.
854                //
855                // Every per-frame buffer comes through here -- vertices,
856                // indices and materials alike -- and each is allocated, filled
857                // and freed within the submission. This comment used to say
858                // that geometry went through a ring allocator instead. There
859                // is no ring allocator; there never was.
860                location: MemoryLocation::CpuToGpu,
861                linear: true,
862                allocation_scheme: AllocationScheme::GpuAllocatorManaged,
863            })
864            .map_err(|e| {
865                unsafe { device.destroy_buffer(buffer, None) };
866                Error::Backend {
867                    backend: "vulkan",
868                    detail: format!("upload allocation: {e}"),
869                }
870            })?;
871
872        unsafe { device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) }
873            .map_err(|e| backend_err("bind_buffer_memory", e))?;
874
875        let slice = allocation.mapped_slice_mut().ok_or(Error::Backend {
876            backend: "vulkan",
877            detail: "upload allocation was not host-visible".into(),
878        })?;
879        slice[..bytes.len()].copy_from_slice(bytes);
880
881        Ok(StagedBuffer { buffer, allocation })
882    }
883
884    pub(crate) fn release(&mut self, staged: StagedBuffer) {
885        let _ = self.allocator_mut().free(staged.allocation);
886        // SAFETY: every submission using this buffer was waited on before the
887        // call that made it returned.
888        unsafe { self.raw_device().destroy_buffer(staged.buffer, None) };
889    }
890}
891
892pub(crate) struct StagedBuffer {
893    pub(crate) buffer: vk::Buffer,
894    pub(crate) allocation: Allocation,
895}
896
897/// Where a pass's clip space lands in the target.
898///
899/// Without a viewport of its own a pass covers the target exactly, which is
900/// what every pass wants whose geometry was recorded against the target it
901/// renders into. A layer that narrowed its target after recording carries one,
902/// and it keeps the extent the geometry was recorded against so that clip
903/// space still maps to the same device pixels -- the offset slides those
904/// pixels so the wanted sub-rectangle lands on the smaller target, which crops
905/// rather than scales.
906///
907/// The offset is normally negative, which Vulkan allows: `viewportBoundsRange`
908/// is at least [-2 * maxViewportDimension, 2 * maxViewportDimension - 1], and
909/// an offset here never exceeds the size of the recorded space.
910fn pass_viewport(pass: PassDescriptor, extent: Extent2D) -> vk::Viewport {
911    let (offset, size) = match pass.viewport {
912        Some(v) => (v.offset, v.extent),
913        None => ([0.0, 0.0], extent),
914    };
915    vk::Viewport {
916        x: offset[0],
917        y: offset[1],
918        width: size.width as f32,
919        height: size.height as f32,
920        min_depth: 0.0,
921        max_depth: 1.0,
922    }
923}
924
925fn build_render_pass(device: &ash::Device, key: RenderPassKey) -> Result<vk::RenderPass> {
926    // Clearing makes prior contents irrelevant, so the attachment can declare
927    // UNDEFINED and skip a transition. Loading must declare the layout the
928    // image is actually in, and the caller transitions it there first.
929    let (load_op, initial_layout) = if key.clears {
930        (vk::AttachmentLoadOp::CLEAR, vk::ImageLayout::UNDEFINED)
931    } else {
932        (
933            vk::AttachmentLoadOp::LOAD,
934            vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
935        )
936    };
937    let color = vk::AttachmentDescription::default()
938        .format(key.format)
939        .samples(sample_flags(key.samples))
940        .load_op(load_op)
941        // Multisample contents are consumed by the resolve and never read
942        // again, so storing them would cost bandwidth for nothing. On a tiler
943        // that is the difference between the multisample buffer staying in
944        // tile memory and being written out to main memory.
945        .store_op(if key.samples > 1 {
946            vk::AttachmentStoreOp::DONT_CARE
947        } else {
948            vk::AttachmentStoreOp::STORE
949        })
950        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
951        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
952        .initial_layout(initial_layout)
953        .final_layout(final_layout(key, false));
954
955    // The resolve target is the texture the caller reads. Its prior contents
956    // are irrelevant because the resolve overwrites every pixel the pass
957    // touched.
958    let resolve = vk::AttachmentDescription::default()
959        .format(key.format)
960        .samples(vk::SampleCountFlags::TYPE_1)
961        .load_op(vk::AttachmentLoadOp::DONT_CARE)
962        .store_op(vk::AttachmentStoreOp::STORE)
963        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
964        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
965        .initial_layout(vk::ImageLayout::UNDEFINED)
966        .final_layout(final_layout(key, true));
967
968    // Cleared at pass start and discarded at the end. No pass reads what
969    // another left here: a clip stack that outlives a pass -- which one does
970    // when a backdrop filter cuts the pass under it -- is rebuilt by drawing
971    // the narrowings again, not by carrying the buffer across. So on a tiler
972    // discarding keeps it in tile memory, and the clear is what every pass
973    // starts from.
974    let stencil = key.stencil.map(|format| {
975        vk::AttachmentDescription::default()
976            .format(format)
977            .samples(sample_flags(key.samples))
978            .load_op(vk::AttachmentLoadOp::DONT_CARE)
979            .store_op(vk::AttachmentStoreOp::DONT_CARE)
980            .stencil_load_op(vk::AttachmentLoadOp::CLEAR)
981            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
982            .initial_layout(vk::ImageLayout::UNDEFINED)
983            .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
984    });
985
986    let mut attachments: Vec<vk::AttachmentDescription> = if key.samples > 1 {
987        vec![color, resolve]
988    } else {
989        vec![color]
990    };
991    // Last, so the color and resolve attachments keep the indices they had and
992    // the framebuffer built alongside this needs no reordering.
993    let stencil_index = attachments.len() as u32;
994    if let Some(stencil) = stencil {
995        attachments.push(stencil);
996    }
997
998    let color_refs = [vk::AttachmentReference::default()
999        .attachment(0)
1000        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
1001    let resolve_refs = [vk::AttachmentReference::default()
1002        .attachment(1)
1003        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
1004    let stencil_ref = vk::AttachmentReference::default()
1005        .attachment(stencil_index)
1006        .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
1007
1008    let mut subpass = vk::SubpassDescription::default()
1009        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
1010        .color_attachments(&color_refs);
1011    if key.samples > 1 {
1012        subpass = subpass.resolve_attachments(&resolve_refs);
1013    }
1014    if key.stencil.is_some() {
1015        subpass = subpass.depth_stencil_attachment(&stencil_ref);
1016    }
1017    let subpasses = [subpass];
1018    // The attachment's layout transition happens as part of beginning the pass,
1019    // and nothing orders it against work outside the pass unless this says so.
1020    // Waiting on a semaphore does not: a wait with a destination stage of
1021    // color-attachment output orders the *draws* after it, while the transition
1022    // is free to run before the wait completes.
1023    //
1024    // That is not theoretical. A swapchain image is acquired, the acquire
1025    // signals a semaphore, the render waits on it -- and the render pass could
1026    // still transition the image while the presentation engine was reading it.
1027    // Synchronization validation reports it as a write-after-read hazard
1028    // against `vkAcquireNextImageKHR`, and no picture on this driver ever
1029    // showed it.
1030    //
1031    // Both directions, because a pass both begins and ends with a transition:
1032    // the first waits for prior color output before transitioning in, the
1033    // second makes this pass's writes visible before whatever reads the target
1034    // next, which is presentation or a later pass sampling it.
1035    // Every stage that touches an attachment here, not only the color one. A
1036    // stencil attachment is transitioned and then cleared by its load
1037    // operation, and those are two writes that need ordering exactly as much
1038    // as the color ones do -- reported as a write-after-write against the
1039    // pass's own layout transition when they are left out.
1040    let attachment_stages = vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
1041        | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS
1042        | vk::PipelineStageFlags::LATE_FRAGMENT_TESTS;
1043    let attachment_writes = vk::AccessFlags::COLOR_ATTACHMENT_WRITE
1044        | vk::AccessFlags::COLOR_ATTACHMENT_READ
1045        | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE
1046        | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ;
1047    let dependencies = [
1048        vk::SubpassDependency::default()
1049            .src_subpass(vk::SUBPASS_EXTERNAL)
1050            .dst_subpass(0)
1051            .src_stage_mask(attachment_stages)
1052            .dst_stage_mask(attachment_stages)
1053            .src_access_mask(vk::AccessFlags::empty())
1054            .dst_access_mask(attachment_writes),
1055        vk::SubpassDependency::default()
1056            .src_subpass(0)
1057            .dst_subpass(vk::SUBPASS_EXTERNAL)
1058            .src_stage_mask(attachment_stages)
1059            .dst_stage_mask(vk::PipelineStageFlags::FRAGMENT_SHADER)
1060            .src_access_mask(
1061                vk::AccessFlags::COLOR_ATTACHMENT_WRITE
1062                    | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
1063            )
1064            .dst_access_mask(vk::AccessFlags::SHADER_READ),
1065    ];
1066    let info = vk::RenderPassCreateInfo::default()
1067        .attachments(&attachments)
1068        .subpasses(&subpasses)
1069        .dependencies(&dependencies);
1070
1071    unsafe { device.create_render_pass(&info, None) }
1072        .map_err(|e| backend_err("create_render_pass", e))
1073}
1074
1075/// The layout an attachment is left in.
1076///
1077/// A multisampled pass writes into the multisample attachment and resolves into
1078/// the target, so it is the resolve attachment the caller reads and the resolve
1079/// attachment that has to be ready to present. The multisample one is discarded
1080/// either way.
1081fn final_layout(key: RenderPassKey, is_resolve: bool) -> vk::ImageLayout {
1082    let read_by_the_caller = is_resolve || key.samples == 1;
1083    if key.presents && read_by_the_caller {
1084        vk::ImageLayout::PRESENT_SRC_KHR
1085    } else {
1086        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL
1087    }
1088}
1089
1090pub(crate) fn sample_flags(count: u32) -> vk::SampleCountFlags {
1091    match count {
1092        2 => vk::SampleCountFlags::TYPE_2,
1093        4 => vk::SampleCountFlags::TYPE_4,
1094        8 => vk::SampleCountFlags::TYPE_8,
1095        16 => vk::SampleCountFlags::TYPE_16,
1096        _ => vk::SampleCountFlags::TYPE_1,
1097    }
1098}
1099
1100fn build_pipeline_layout(
1101    device: &ash::Device,
1102    descriptor_layout: vk::DescriptorSetLayout,
1103    material_layout: vk::DescriptorSetLayout,
1104) -> Result<vk::PipelineLayout> {
1105    // Two sets: the first carries the texture an image paint samples, the
1106    // second the paint itself. One layout serves every pipeline, since they
1107    // all declare the same two -- which is also why a solid fill still binds a
1108    // texture it never reads.
1109    //
1110    // Separate sets rather than two bindings in one, because the texture set
1111    // includes a placeholder owned by the context and outliving any single
1112    // submission, while the paint's buffer is built per submission.
1113    let set_layouts = [descriptor_layout, material_layout];
1114    let info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts);
1115    unsafe { device.create_pipeline_layout(&info, None) }
1116        .map_err(|e| backend_err("create_pipeline_layout", e))
1117}
1118
1119fn build_pipeline(
1120    device: &ash::Device,
1121    key: PipelineKey,
1122    render_pass: vk::RenderPass,
1123    layout: vk::PipelineLayout,
1124    fragment: Option<&[u32]>,
1125) -> Result<vk::Pipeline> {
1126    // The vertex stage is always this renderer's own: a runtime effect
1127    // replaces what a fragment does with a paint, not how geometry reaches
1128    // clip space, and letting it replace the latter would mean every effect
1129    // restating a convention it has no reason to know.
1130    let module_info = vk::ShaderModuleCreateInfo::default().code(impeller_shaders::SOLID_SPV);
1131    let module = unsafe { device.create_shader_module(&module_info, None) }
1132        .map_err(|e| backend_err("create_shader_module", e))?;
1133    let fragment_module = match fragment {
1134        Some(code) => {
1135            let info = vk::ShaderModuleCreateInfo::default().code(code);
1136            Some(
1137                unsafe { device.create_shader_module(&info, None) }
1138                    .map_err(|e| backend_err("create_shader_module", e))?,
1139            )
1140        }
1141        None => None,
1142    };
1143    let fs_module = fragment_module.unwrap_or(module);
1144
1145    let vs_name = c"vs_main";
1146    let fs_name = c"fs_main";
1147    let stages = [
1148        vk::PipelineShaderStageCreateInfo::default()
1149            .stage(vk::ShaderStageFlags::VERTEX)
1150            .module(module)
1151            .name(vs_name),
1152        vk::PipelineShaderStageCreateInfo::default()
1153            .stage(vk::ShaderStageFlags::FRAGMENT)
1154            .module(fs_module)
1155            .name(fs_name),
1156    ];
1157
1158    let bindings = [vk::VertexInputBindingDescription::default()
1159        .binding(0)
1160        .stride(std::mem::size_of::<impeller_hal::Vertex>() as u32)
1161        .input_rate(vk::VertexInputRate::VERTEX)];
1162    // Offsets from the type rather than from arithmetic over the field widths.
1163    // Two backends describe this one struct to two APIs, and a description that
1164    // is wrong the same way in both is the failure nothing here would catch --
1165    // the pixels are compared against the other backend. Neither of them gets
1166    // to restate the layout.
1167    let attributes = [
1168        vk::VertexInputAttributeDescription::default()
1169            .location(0)
1170            .binding(0)
1171            .format(vk::Format::R32G32B32_SFLOAT)
1172            .offset(std::mem::offset_of!(impeller_hal::Vertex, position) as u32),
1173        vk::VertexInputAttributeDescription::default()
1174            .location(1)
1175            .binding(0)
1176            .format(vk::Format::R32G32_SFLOAT)
1177            .offset(std::mem::offset_of!(impeller_hal::Vertex, uv) as u32),
1178        vk::VertexInputAttributeDescription::default()
1179            .location(2)
1180            .binding(0)
1181            .format(vk::Format::R32G32B32A32_SFLOAT)
1182            .offset(std::mem::offset_of!(impeller_hal::Vertex, color) as u32),
1183    ];
1184    let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
1185        .vertex_binding_descriptions(&bindings)
1186        .vertex_attribute_descriptions(&attributes);
1187
1188    let assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
1189        .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
1190    let viewport_state = vk::PipelineViewportStateCreateInfo::default()
1191        .viewport_count(1)
1192        .scissor_count(1);
1193    let raster = vk::PipelineRasterizationStateCreateInfo::default()
1194        // No culling: 2D geometry arrives in whatever winding tessellation
1195        // produced, and discarding half of it by winding would drop triangles
1196        // that are perfectly visible.
1197        .cull_mode(vk::CullModeFlags::NONE)
1198        .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
1199        .polygon_mode(vk::PolygonMode::FILL)
1200        .line_width(1.0);
1201    let multisample = vk::PipelineMultisampleStateCreateInfo::default()
1202        .rasterization_samples(sample_flags(key.samples));
1203
1204    // Factors come from the shared table rather than being restated here, so
1205    // the two backends cannot disagree about what a mode means. Color arrives
1206    // premultiplied from the shader, which is what that table assumes: source-
1207    // over is ONE rather than SRC_ALPHA, since using SRC_ALPHA against an
1208    // already-scaled source applies alpha twice and darkens every translucent
1209    // edge.
1210    // A clip draw contributes shape, not color. Masking every channel rather
1211    // than relying on a blend mode that happens to discard the source keeps the
1212    // clip geometry from touching the target even where blending is exotic.
1213    let attachment = vk::PipelineColorBlendAttachmentState::default().color_write_mask(
1214        if key.role.writes_color() {
1215            vk::ColorComponentFlags::RGBA
1216        } else {
1217            vk::ColorComponentFlags::empty()
1218        },
1219    );
1220    let blend_attachments = [match key.blend.factors() {
1221        _ if key.blend.is_plain_write() => {
1222            // Writing the source outright needs no blend unit at all, which is
1223            // worth switching off rather than expressing as ONE and ZERO.
1224            attachment.blend_enable(false)
1225        }
1226        Some(factors) => {
1227            let src = vk_blend_factor(factors.src);
1228            let dst = vk_blend_factor(factors.dst);
1229            attachment
1230                .blend_enable(true)
1231                .src_color_blend_factor(src)
1232                .dst_color_blend_factor(dst)
1233                .color_blend_op(vk::BlendOp::ADD)
1234                // The same factors for alpha as for color: with premultiplied
1235                // color the alpha channel is not a special case, and giving it
1236                // different factors is what breaks compositing a layer onto
1237                // something else.
1238                .src_alpha_blend_factor(src)
1239                .dst_alpha_blend_factor(dst)
1240                .alpha_blend_op(vk::BlendOp::ADD)
1241        }
1242        // An advanced mode names the whole equation in the blend op, so the
1243        // factors are ignored by the hardware entirely. They are still left at
1244        // ONE and ZERO rather than whatever the defaults happen to be, so a
1245        // driver that reads them cannot produce something arbitrary.
1246        None => attachment
1247            .blend_enable(true)
1248            .src_color_blend_factor(vk::BlendFactor::ONE)
1249            .dst_color_blend_factor(vk::BlendFactor::ZERO)
1250            .color_blend_op(vk_advanced_blend_op(key.blend))
1251            .src_alpha_blend_factor(vk::BlendFactor::ONE)
1252            .dst_alpha_blend_factor(vk::BlendFactor::ZERO)
1253            .alpha_blend_op(vk_advanced_blend_op(key.blend)),
1254    }];
1255    let mut blend =
1256        vk::PipelineColorBlendStateCreateInfo::default().attachments(&blend_attachments);
1257
1258    // Both sides arrive premultiplied, and saying so is what makes the hardware
1259    // divide the alpha back out before evaluating the blend function; claiming
1260    // otherwise renders every translucent advanced blend at the wrong strength.
1261    // UNCORRELATED is the overlap model the compositing specification defines,
1262    // so it is what the reference formula in the HAL agrees with.
1263    let mut advanced = vk::PipelineColorBlendAdvancedStateCreateInfoEXT::default()
1264        .src_premultiplied(true)
1265        .dst_premultiplied(true)
1266        .blend_overlap(vk::BlendOverlapEXT::UNCORRELATED);
1267    if key.blend.is_advanced() {
1268        blend = blend.push_next(&mut advanced);
1269    }
1270
1271    // The stencil operations differ per role and are baked in; the value they
1272    // compare against is the clip depth, which is dynamic so that nesting
1273    // deeper costs a state change rather than another pipeline.
1274    let stencil_op = vk::StencilOpState::default()
1275        .compare_op(vk::CompareOp::EQUAL)
1276        .fail_op(vk::StencilOp::KEEP)
1277        .depth_fail_op(vk::StencilOp::KEEP)
1278        .pass_op(match key.role {
1279            ClipRole::Content => vk::StencilOp::KEEP,
1280            // Clamping rather than wrapping: at the top of the range a wrap
1281            // lands back on zero and admits everything the clip excluded, where
1282            // a clamp merely fails to narrow further. Neither is correct, and
1283            // depth is checked against the limit before recording, so this is
1284            // the safer of two states that should not arise.
1285            ClipRole::Narrow => vk::StencilOp::INCREMENT_AND_CLAMP,
1286            ClipRole::Widen => vk::StencilOp::DECREMENT_AND_CLAMP,
1287        })
1288        .compare_mask(0xff)
1289        .write_mask(if key.role.writes_stencil() { 0xff } else { 0 });
1290    let depth_stencil = vk::PipelineDepthStencilStateCreateInfo::default()
1291        .stencil_test_enable(key.stencil.is_some())
1292        .front(stencil_op)
1293        .back(stencil_op);
1294
1295    let dynamic_states = [
1296        vk::DynamicState::VIEWPORT,
1297        vk::DynamicState::SCISSOR,
1298        vk::DynamicState::STENCIL_REFERENCE,
1299    ];
1300    let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
1301
1302    let info = vk::GraphicsPipelineCreateInfo::default()
1303        .stages(&stages)
1304        .vertex_input_state(&vertex_input)
1305        .input_assembly_state(&assembly)
1306        .viewport_state(&viewport_state)
1307        .rasterization_state(&raster)
1308        .multisample_state(&multisample)
1309        .depth_stencil_state(&depth_stencil)
1310        .color_blend_state(&blend)
1311        .dynamic_state(&dynamic)
1312        .layout(layout)
1313        .render_pass(render_pass)
1314        .subpass(0);
1315
1316    let created =
1317        unsafe { device.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) };
1318
1319    // The shader modules are only needed during creation.
1320    unsafe { device.destroy_shader_module(module, None) };
1321    if let Some(fragment) = fragment_module {
1322        unsafe { device.destroy_shader_module(fragment, None) };
1323    }
1324
1325    match created {
1326        Ok(pipelines) => Ok(pipelines[0]),
1327        Err((_, e)) => Err(backend_err("create_graphics_pipelines", e)),
1328    }
1329}
1330
1331/// What every draw in one pass shares.
1332struct PassRecording<'a> {
1333    device: &'a ash::Device,
1334    cmd: vk::CommandBuffer,
1335    layout: vk::PipelineLayout,
1336    /// The whole render area, which is also what an unclipped draw scissors to.
1337    area: vk::Rect2D,
1338    bindings: &'a crate::sampling::Bindings,
1339    materials: &'a crate::materials::Materials,
1340}
1341
1342/// What has already been recorded, so each piece is set only where it changes.
1343///
1344/// Draws stay in submission order, and consecutive draws sharing a pipeline or
1345/// a clip are the common case rather than the exception.
1346struct RecordedState {
1347    pipeline: Option<vk::Pipeline>,
1348    descriptor_set: Option<vk::DescriptorSet>,
1349    scissor: vk::Rect2D,
1350    /// `None` until the first draw sets it, since a pass begins with the
1351    /// reference undefined rather than at any particular value.
1352    stencil_reference: Option<u32>,
1353}
1354
1355impl RecordedState {
1356    /// The state a pass begins in, having just set the scissor to the whole
1357    /// render area.
1358    fn at_pass_start(area: vk::Rect2D) -> Self {
1359        Self {
1360            pipeline: None,
1361            descriptor_set: None,
1362            scissor: area,
1363            stencil_reference: None,
1364        }
1365    }
1366}
1367
1368/// Record the per-draw state and the draw itself.
1369///
1370/// Shared between the waiting and deferred submission paths, which differ only
1371/// in how they resolve a pipeline. Keeping this in one place is what stops the
1372/// two from drifting: a scissor set in one and not the other would clip
1373/// correctly right up until a caller started presenting its frames.
1374///
1375/// # Safety
1376///
1377/// `cmd` must be recording, inside a render pass compatible with `pipeline`,
1378/// with the vertex and index buffers the draw's indices address already bound.
1379unsafe fn record_draw(
1380    pass: &PassRecording,
1381    draw: &impeller_hal::BatchDraw,
1382    index: usize,
1383    pipeline: vk::Pipeline,
1384    state: &mut RecordedState,
1385) {
1386    let PassRecording {
1387        device,
1388        cmd,
1389        layout,
1390        area,
1391        bindings,
1392        materials,
1393    } = *pass;
1394    unsafe {
1395        if state.pipeline != Some(pipeline) {
1396            device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, pipeline);
1397            state.pipeline = Some(pipeline);
1398        }
1399
1400        // Vulkan's framebuffer origin is the top-left corner, which is the
1401        // convention `Scissor` is already stated in, so this is a widening
1402        // rather than a transformation. Clamping to the render area is not
1403        // optional: a scissor reaching outside it is invalid, and a clip
1404        // computed against a target the caller resized is exactly how that
1405        // happens.
1406        let wanted = match draw.clip {
1407            Some(clip) => {
1408                let clip = clip.clamped_to(impeller_hal::Extent2D::new(
1409                    area.extent.width,
1410                    area.extent.height,
1411                ));
1412                vk::Rect2D {
1413                    offset: vk::Offset2D {
1414                        x: clip.x as i32,
1415                        y: clip.y as i32,
1416                    },
1417                    extent: vk::Extent2D {
1418                        width: clip.width,
1419                        height: clip.height,
1420                    },
1421                }
1422            }
1423            None => area,
1424        };
1425        if state.scissor != wanted {
1426            device.cmd_set_scissor(cmd, 0, &[wanted]);
1427            state.scissor = wanted;
1428        }
1429
1430        // Bound for every draw, not only the ones that sample: the shader
1431        // declares the texture whatever the paint is, and a descriptor a
1432        // pipeline statically uses must be bound even where the branch reading
1433        // it is unreachable. A draw that samples nothing gets a placeholder.
1434        let set = bindings.set_for(draw.material.texture_slots());
1435        if state.descriptor_set != Some(set) {
1436            device.cmd_bind_descriptor_sets(
1437                cmd,
1438                vk::PipelineBindPoint::GRAPHICS,
1439                layout,
1440                0,
1441                &[set],
1442                &[],
1443            );
1444            state.descriptor_set = Some(set);
1445        }
1446
1447        // The clip depth is dynamic state, so nesting deeper is a state change
1448        // rather than another pipeline. It is set even for a pass with no
1449        // stencil attachment, where it is ignored -- writing it unconditionally
1450        // is cheaper than deciding, and leaves no path where a pipeline that
1451        // does test the stencil runs against a reference nobody set.
1452        let reference = draw.stencil.reference;
1453        if state.stencil_reference != Some(reference) {
1454            device.cmd_set_stencil_reference(cmd, vk::StencilFaceFlags::FRONT_AND_BACK, reference);
1455            state.stencil_reference = Some(reference);
1456        }
1457
1458        // One set for the whole batch, rebound per draw with a different
1459        // offset into it. The set itself never changes, but a dynamic offset
1460        // travels with the binding call, so this is a rebind rather than a
1461        // separate command -- and cheaper than the descriptor update per draw
1462        // that a non-dynamic binding would need.
1463        device.cmd_bind_descriptor_sets(
1464            cmd,
1465            vk::PipelineBindPoint::GRAPHICS,
1466            layout,
1467            1,
1468            &[materials.set()],
1469            &[materials.offset(index)],
1470        );
1471        device.cmd_draw_indexed(cmd, draw.index_count, 1, draw.first_index, 0, 0);
1472    }
1473}
1474
1475/// Translate an advanced blend mode to the blend op that names its equation.
1476///
1477/// Every arm is a mode the extension defines with the same formula as the
1478/// compositing specification, which is what lets the conformance tests check
1479/// the hardware against the reference in the HAL. Panicking on a Porter-Duff
1480/// mode is right: callers reach this only through the `None` arm of `factors`,
1481/// so arriving here with one means the two classifications have diverged, and
1482/// guessing an op would turn that into a wrong picture instead of a crash.
1483fn vk_advanced_blend_op(mode: impeller_hal::BlendMode) -> vk::BlendOp {
1484    use impeller_hal::BlendMode;
1485    match mode {
1486        BlendMode::Multiply => vk::BlendOp::MULTIPLY_EXT,
1487        BlendMode::Screen => vk::BlendOp::SCREEN_EXT,
1488        BlendMode::Overlay => vk::BlendOp::OVERLAY_EXT,
1489        BlendMode::Darken => vk::BlendOp::DARKEN_EXT,
1490        BlendMode::Lighten => vk::BlendOp::LIGHTEN_EXT,
1491        BlendMode::ColorDodge => vk::BlendOp::COLORDODGE_EXT,
1492        BlendMode::ColorBurn => vk::BlendOp::COLORBURN_EXT,
1493        BlendMode::HardLight => vk::BlendOp::HARDLIGHT_EXT,
1494        BlendMode::SoftLight => vk::BlendOp::SOFTLIGHT_EXT,
1495        BlendMode::Difference => vk::BlendOp::DIFFERENCE_EXT,
1496        BlendMode::Exclusion => vk::BlendOp::EXCLUSION_EXT,
1497        // The non-separable four. Named HSL by the extension for the attributes
1498        // they exchange, which is the same thing the compositing specification
1499        // calls hue, saturation, color and luminosity.
1500        BlendMode::Hue => vk::BlendOp::HSL_HUE_EXT,
1501        BlendMode::Saturation => vk::BlendOp::HSL_SATURATION_EXT,
1502        BlendMode::Color => vk::BlendOp::HSL_COLOR_EXT,
1503        BlendMode::Luminosity => vk::BlendOp::HSL_LUMINOSITY_EXT,
1504        other => unreachable!("{other} is not an advanced blend mode"),
1505    }
1506}
1507
1508/// Translate a portable blend factor.
1509fn vk_blend_factor(factor: impeller_hal::BlendFactor) -> vk::BlendFactor {
1510    use impeller_hal::BlendFactor;
1511    match factor {
1512        BlendFactor::Zero => vk::BlendFactor::ZERO,
1513        BlendFactor::One => vk::BlendFactor::ONE,
1514        BlendFactor::SrcAlpha => vk::BlendFactor::SRC_ALPHA,
1515        BlendFactor::OneMinusSrcAlpha => vk::BlendFactor::ONE_MINUS_SRC_ALPHA,
1516        BlendFactor::DstAlpha => vk::BlendFactor::DST_ALPHA,
1517        BlendFactor::OneMinusDstAlpha => vk::BlendFactor::ONE_MINUS_DST_ALPHA,
1518        BlendFactor::DstColor => vk::BlendFactor::DST_COLOR,
1519    }
1520}
1521
1522/// Reinterpret a slice of plain data as bytes.
1523pub(crate) fn cast_bytes<T>(slice: &[T]) -> &[u8] {
1524    // SAFETY: T is a plain-data type here ([f32; 2], [f32; 4] or u32), every
1525    // byte of it is initialized, and the returned slice borrows the same memory
1526    // for a shorter-or-equal lifetime.
1527    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const u8, std::mem::size_of_val(slice)) }
1528}