Skip to main content

impeller_hal_vulkan/
resource.rs

1//! Images, their memory, and getting pixels back to the CPU.
2//!
3//! Readback exists for the test apparatus rather than for the frame loop. The
4//! entire golden and conformance corpus renders to an offscreen target and
5//! compares the result, so this path is what turns "the device reported a
6//! capability" into "the device produced these pixels".
7
8use crate::device::VulkanContext;
9use ash::vk;
10use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme};
11use gpu_allocator::MemoryLocation;
12use impeller_hal::{Error, Extent2D, PixelFormat, Result, TextureDescriptor, TextureUsage};
13
14/// An image, its backing memory, and the state needed to transition it.
15///
16/// Ownership is explicit: a texture must be handed back to
17/// [`VulkanContext::destroy_texture`]. Automatic retirement belongs to the
18/// fence waiter, which tracks GPU completion and cannot be built until there
19/// is asynchronous work to track.
20pub struct VulkanTexture {
21    pub(crate) image: vk::Image,
22    pub(crate) memory: TextureMemory,
23    pub(crate) extent: Extent2D,
24    pub(crate) format: PixelFormat,
25    /// The layout the image is currently in, so the next operation knows what
26    /// to transition from.
27    /// The layout the image is currently in.
28    ///
29    /// Behind a cell because this tracks device-side state, not anything Rust
30    /// aliasing rules are about: a texture being *sampled* is borrowed shared,
31    /// and getting it into a readable layout is a transition the recorder has
32    /// to make and record. Requiring a unique borrow for that would mean a
33    /// batch could sample only one texture at a time.
34    pub(crate) layout: std::cell::Cell<vk::ImageLayout>,
35    pub(crate) usage: TextureUsage,
36    /// How many mip levels the image was allocated with, the image included.
37    pub(crate) mip_levels: u32,
38}
39
40/// Where a texture's memory came from.
41///
42/// The distinction matters only at export: a dma-buf hands over a whole
43/// allocation, so an image sharing one with other resources cannot be exported
44/// without exporting them too.
45pub(crate) enum TextureMemory {
46    /// Suballocated from a pool. The common case, and not exportable.
47    Pooled(Allocation),
48    /// A whole allocation of its own, which is what export requires.
49    Dedicated(vk::DeviceMemory),
50    /// Memory somebody else owns, and an image somebody else created.
51    ///
52    /// A swapchain's images are the case: the presentation engine allocates
53    /// them, hands them out, and destroys them with the swapchain. Destroying
54    /// one here would free a handle the engine still holds, so this variant
55    /// exists to say "wrap it, do not own it" — a distinction the type system
56    /// then keeps for us rather than leaving to a comment.
57    Borrowed,
58}
59
60impl VulkanTexture {
61    /// Wrap an image this backend did not create.
62    ///
63    /// The caller keeps ownership: destroying the returned texture releases
64    /// nothing, which is what makes it safe to hand back a swapchain image the
65    /// presentation engine still owns.
66    pub fn wrap_image(
67        image: vk::Image,
68        extent: Extent2D,
69        format: PixelFormat,
70        layout: vk::ImageLayout,
71    ) -> VulkanTexture {
72        VulkanTexture {
73            image,
74            memory: TextureMemory::Borrowed,
75            extent,
76            format,
77            layout: std::cell::Cell::new(layout),
78            // A swapchain image is one level. Nothing here allocated it, so
79            // nothing here may claim it holds more than it was handed.
80            mip_levels: 1,
81            usage: TextureUsage {
82                render_target: true,
83                sampled: false,
84                ..TextureUsage::default()
85            },
86        }
87    }
88
89    pub fn extent(&self) -> Extent2D {
90        self.extent
91    }
92
93    pub fn format(&self) -> PixelFormat {
94        self.format
95    }
96
97    pub fn raw_image(&self) -> vk::Image {
98        self.image
99    }
100
101    pub(crate) fn layout(&self) -> vk::ImageLayout {
102        self.layout.get()
103    }
104
105    /// Record a layout change made by an operation outside this module.
106    pub(crate) fn set_layout(&self, layout: vk::ImageLayout) {
107        self.layout.set(layout);
108    }
109
110    /// Bytes a tightly packed readback of this texture occupies.
111    pub fn byte_size(&self) -> u64 {
112        self.extent.area() * self.format.bytes_per_pixel() as u64
113    }
114}
115
116/// Map a portable format onto Vulkan's enumeration.
117pub(crate) fn vk_format(format: PixelFormat) -> vk::Format {
118    match format {
119        PixelFormat::Rgba8Unorm => vk::Format::R8G8B8A8_UNORM,
120        PixelFormat::Rgba8UnormSrgb => vk::Format::R8G8B8A8_SRGB,
121        PixelFormat::Bgra8Unorm => vk::Format::B8G8R8A8_UNORM,
122        PixelFormat::Bgra8UnormSrgb => vk::Format::B8G8R8A8_SRGB,
123        PixelFormat::Rgb10A2Unorm => vk::Format::A2B10G10R10_UNORM_PACK32,
124        PixelFormat::Rgba16Float => vk::Format::R16G16B16A16_SFLOAT,
125        PixelFormat::R8Unorm => vk::Format::R8_UNORM,
126    }
127}
128
129impl VulkanContext {
130    /// Allocate a texture.
131    ///
132    /// External images are not handled yet: importing a dma-buf needs the
133    /// external-memory plumbing, and accepting the descriptor while ignoring
134    /// its external half would hand back a blank image that silently is not
135    /// the caller's buffer.
136    pub fn create_texture(&mut self, desc: &TextureDescriptor) -> Result<VulkanTexture> {
137        if desc.is_external() {
138            return Err(Error::Unsupported("external image import"));
139        }
140        self.capabilities().check_texture(desc)?;
141        if !self.capabilities().can_allocate(desc.extent) {
142            return Err(Error::LimitExceeded {
143                what: "texture dimension",
144                requested: desc.extent.width.max(desc.extent.height) as u64,
145                limit: self.capabilities().max_texture_size as u64,
146            });
147        }
148        if desc.extent.is_empty() {
149            return Err(Error::Unsupported("zero-sized texture"));
150        }
151
152        let mut usage = vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST;
153        if desc.usage.sampled {
154            usage |= vk::ImageUsageFlags::SAMPLED;
155        }
156        if desc.usage.render_target {
157            usage |= vk::ImageUsageFlags::COLOR_ATTACHMENT;
158        }
159
160        let info = vk::ImageCreateInfo::default()
161            .image_type(vk::ImageType::TYPE_2D)
162            .format(vk_format(desc.format))
163            .extent(vk::Extent3D {
164                width: desc.extent.width,
165                height: desc.extent.height,
166                depth: 1,
167            })
168            .mip_levels(desc.mip_levels.max(1))
169            .array_layers(1)
170            .samples(sample_count_flags(desc.sample_count))
171            .tiling(vk::ImageTiling::OPTIMAL)
172            .usage(usage)
173            .sharing_mode(vk::SharingMode::EXCLUSIVE)
174            .initial_layout(vk::ImageLayout::UNDEFINED);
175
176        let device = self.raw_device().clone();
177        let image = unsafe { device.create_image(&info, None) }
178            .map_err(|e| backend_err("create_image", e))?;
179
180        let requirements = unsafe { device.get_image_memory_requirements(image) };
181        let allocation = self
182            .allocator_mut()
183            .allocate(&AllocationCreateDesc {
184                name: "texture",
185                requirements,
186                location: MemoryLocation::GpuOnly,
187                linear: false,
188                allocation_scheme: AllocationScheme::GpuAllocatorManaged,
189            })
190            .map_err(|e| {
191                unsafe { device.destroy_image(image, None) };
192                Error::OutOfMemory {
193                    what: "texture memory",
194                }
195                .with_detail(e)
196            })?;
197
198        unsafe { device.bind_image_memory(image, allocation.memory(), allocation.offset()) }
199            .map_err(|e| backend_err("bind_image_memory", e))?;
200
201        Ok(VulkanTexture {
202            image,
203            memory: TextureMemory::Pooled(allocation),
204            extent: desc.extent,
205            format: desc.format,
206            layout: std::cell::Cell::new(vk::ImageLayout::UNDEFINED),
207            usage: desc.usage,
208            mip_levels: desc.mip_levels.max(1),
209        })
210    }
211
212    /// Release a texture and its memory.
213    pub fn destroy_texture(&mut self, texture: VulkanTexture) {
214        // Freeing has to match how the memory was obtained, or the pool is told
215        // about an allocation it never made.
216        match texture.memory {
217            TextureMemory::Pooled(allocation) => {
218                let _ = self.allocator_mut().free(allocation);
219            }
220            TextureMemory::Dedicated(memory) => {
221                // SAFETY: nothing else holds this allocation, and every
222                // submission using the image was waited on.
223                unsafe { self.raw_device().free_memory(memory, None) };
224            }
225            // Nothing to free and nothing to destroy: whoever created the
226            // image will do both.
227            TextureMemory::Borrowed => return,
228        }
229        // SAFETY: the caller has given up the texture, and every submission
230        // that used it was waited on before returning from the call that made
231        // it.
232        unsafe { self.raw_device().destroy_image(texture.image, None) };
233    }
234
235    /// Put a texture into the layout the presentation engine reads from.
236    ///
237    /// A presented image is read by something outside this device's command
238    /// stream, so it has to be transitioned rather than left in whatever the
239    /// last pass wanted. Doing it here keeps the layout bookkeeping with
240    /// everything else that knows about layouts, instead of in a presentation
241    /// target that would have to reach into a texture's internals.
242    pub fn transition_for_present(&mut self, texture: &VulkanTexture) -> Result<()> {
243        if texture.layout() == vk::ImageLayout::PRESENT_SRC_KHR {
244            return Ok(());
245        }
246        let device = self.raw_device().clone();
247        let cmd = self.begin_one_shot()?;
248        transition(
249            &device,
250            cmd,
251            texture.raw_image(),
252            texture.layout(),
253            vk::ImageLayout::PRESENT_SRC_KHR,
254        );
255        self.submit_one_shot(cmd)?;
256        texture.set_layout(vk::ImageLayout::PRESENT_SRC_KHR);
257        Ok(())
258    }
259
260    /// Clear a texture to a solid color.
261    ///
262    /// Records, submits, and waits. Batching belongs to the renderer, which
263    /// does not exist yet; doing it here would be an abstraction guessing at
264    /// its caller.
265    pub fn clear_texture(&mut self, texture: &mut VulkanTexture, color: [f32; 4]) -> Result<()> {
266        let device = self.raw_device().clone();
267        let cmd = self.begin_one_shot()?;
268
269        transition(
270            &device,
271            cmd,
272            texture.image,
273            texture.layout.get(),
274            vk::ImageLayout::TRANSFER_DST_OPTIMAL,
275        );
276
277        let clear = vk::ClearColorValue { float32: color };
278        let range = vk::ImageSubresourceRange::default()
279            .aspect_mask(vk::ImageAspectFlags::COLOR)
280            .level_count(vk::REMAINING_MIP_LEVELS)
281            .layer_count(1);
282        unsafe {
283            device.cmd_clear_color_image(
284                cmd,
285                texture.image,
286                vk::ImageLayout::TRANSFER_DST_OPTIMAL,
287                &clear,
288                &[range],
289            );
290        }
291        texture.layout.set(vk::ImageLayout::TRANSFER_DST_OPTIMAL);
292
293        self.submit_one_shot(cmd)
294    }
295
296    /// Copy a texture back to host memory, tightly packed.
297    /// Fill a texture from host memory, tightly packed and top row first.
298    ///
299    /// The mirror of [`Self::read_texture`], and stated in the same layout, so
300    /// a round trip through the pair is the identity. That is what makes it
301    /// testable without a decoder: write known bytes, read them back, compare.
302    ///
303    /// Image *decoding* is out of scope for this project, but getting decoded
304    /// pixels onto the device is not — without this an image shader would have
305    /// nothing to sample but what the renderer itself drew.
306    pub fn write_texture(&mut self, texture: &mut VulkanTexture, pixels: &[u8]) -> Result<()> {
307        let size = texture.byte_size();
308        if pixels.len() as u64 != size {
309            return Err(Error::Backend {
310                backend: "vulkan",
311                detail: format!("texture wants {size} bytes, {} supplied", pixels.len()),
312            });
313        }
314        let device = self.raw_device().clone();
315
316        let buffer_info = vk::BufferCreateInfo::default()
317            .size(size)
318            .usage(vk::BufferUsageFlags::TRANSFER_SRC)
319            .sharing_mode(vk::SharingMode::EXCLUSIVE);
320        let buffer = unsafe { device.create_buffer(&buffer_info, None) }
321            .map_err(|e| backend_err("create_buffer", e))?;
322        let requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
323        let mut allocation = self
324            .allocator_mut()
325            .allocate(&AllocationCreateDesc {
326                name: "upload",
327                requirements,
328                location: MemoryLocation::CpuToGpu,
329                linear: true,
330                allocation_scheme: AllocationScheme::GpuAllocatorManaged,
331            })
332            .map_err(|e| {
333                unsafe { device.destroy_buffer(buffer, None) };
334                Error::OutOfMemory {
335                    what: "upload buffer",
336                }
337                .with_detail(e)
338            })?;
339        if let Err(e) =
340            unsafe { device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) }
341        {
342            let _ = self.allocator_mut().free(allocation);
343            unsafe { device.destroy_buffer(buffer, None) };
344            return Err(backend_err("bind_buffer_memory", e));
345        }
346
347        let staged = match allocation.mapped_slice_mut() {
348            Some(slice) => {
349                slice[..pixels.len()].copy_from_slice(pixels);
350                Ok(())
351            }
352            None => Err(Error::Backend {
353                backend: "vulkan",
354                detail: "upload allocation was not host-visible".into(),
355            }),
356        };
357
358        let outcome = staged.and_then(|()| {
359            let cmd = self.begin_one_shot()?;
360            transition(
361                &device,
362                cmd,
363                texture.image,
364                texture.layout.get(),
365                vk::ImageLayout::TRANSFER_DST_OPTIMAL,
366            );
367            let region = vk::BufferImageCopy::default()
368                .image_subresource(
369                    vk::ImageSubresourceLayers::default()
370                        .aspect_mask(vk::ImageAspectFlags::COLOR)
371                        .layer_count(1),
372                )
373                .image_extent(vk::Extent3D {
374                    width: texture.extent.width,
375                    height: texture.extent.height,
376                    depth: 1,
377                });
378            // SAFETY: the command buffer is recording, the buffer holds the
379            // staged pixels, and the image is in the layout named here.
380            unsafe {
381                device.cmd_copy_buffer_to_image(
382                    cmd,
383                    buffer,
384                    texture.image,
385                    vk::ImageLayout::TRANSFER_DST_OPTIMAL,
386                    &[region],
387                );
388            }
389            texture.layout.set(vk::ImageLayout::TRANSFER_DST_OPTIMAL);
390            // In the same submission as the copy that fed it, so the chain is
391            // never a submission behind the level it was built from.
392            generate_mipmaps(&device, cmd, texture);
393            self.submit_one_shot(cmd)
394        });
395
396        let _ = self.allocator_mut().free(allocation);
397        // SAFETY: the submission above was waited on, so the copy has finished
398        // reading from this buffer.
399        unsafe { device.destroy_buffer(buffer, None) };
400        outcome
401    }
402
403    pub fn read_texture(&mut self, texture: &mut VulkanTexture) -> Result<Vec<u8>> {
404        let size = texture.byte_size();
405        let device = self.raw_device().clone();
406
407        let buffer_info = vk::BufferCreateInfo::default()
408            .size(size)
409            .usage(vk::BufferUsageFlags::TRANSFER_DST)
410            .sharing_mode(vk::SharingMode::EXCLUSIVE);
411        let buffer = unsafe { device.create_buffer(&buffer_info, None) }
412            .map_err(|e| backend_err("create_buffer", e))?;
413        let requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
414        let allocation = self
415            .allocator_mut()
416            .allocate(&AllocationCreateDesc {
417                name: "readback",
418                requirements,
419                location: MemoryLocation::GpuToCpu,
420                linear: true,
421                allocation_scheme: AllocationScheme::GpuAllocatorManaged,
422            })
423            .map_err(|e| {
424                unsafe { device.destroy_buffer(buffer, None) };
425                Error::OutOfMemory {
426                    what: "readback buffer",
427                }
428                .with_detail(e)
429            })?;
430        unsafe { device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) }
431            .map_err(|e| backend_err("bind_buffer_memory", e))?;
432
433        let cmd = self.begin_one_shot()?;
434        transition(
435            &device,
436            cmd,
437            texture.image,
438            texture.layout.get(),
439            vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
440        );
441        let region = vk::BufferImageCopy::default()
442            .image_subresource(
443                vk::ImageSubresourceLayers::default()
444                    .aspect_mask(vk::ImageAspectFlags::COLOR)
445                    .layer_count(1),
446            )
447            .image_extent(vk::Extent3D {
448                width: texture.extent.width,
449                height: texture.extent.height,
450                depth: 1,
451            });
452        unsafe {
453            device.cmd_copy_image_to_buffer(
454                cmd,
455                texture.image,
456                vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
457                buffer,
458                &[region],
459            );
460        }
461        texture.layout.set(vk::ImageLayout::TRANSFER_SRC_OPTIMAL);
462
463        // The buffer and its allocation are released on every path below, so
464        // the submission result is folded in rather than returned early.
465        let pixels = self.submit_one_shot(cmd).and_then(|()| {
466            // The submission was waited on, so the copy has completed and the
467            // mapping is stable for the read.
468            allocation
469                .mapped_slice()
470                .map(|s| s[..size as usize].to_vec())
471                .ok_or(Error::Backend {
472                    backend: "vulkan",
473                    detail: "readback allocation was not host-visible".into(),
474                })
475        });
476
477        let _ = self.allocator_mut().free(allocation);
478        unsafe { device.destroy_buffer(buffer, None) };
479        pixels
480    }
481}
482
483/// Fill every level below the first by halving the one above it.
484///
485/// A blit per level rather than a single downsample of the original, because
486/// that is what a mip chain means: level two is the average of level one and
487/// not a quarter-scale filter over level zero, and the two differ once an image
488/// has any detail near its own resolution. Linear filtering makes each blit the
489/// average of the four texels it covers.
490///
491/// The barriers are per level rather than over the image, which is the one
492/// place in this backend that has to be: each blit reads the level above while
493/// writing the one below, so within a single chain the same image is a transfer
494/// source and a transfer destination at once, in different subresources.
495/// Transitioning the whole image between them would be transitioning a
496/// subresource the blit is still reading.
497///
498/// Costs nothing on a texture with one level -- the loop does not run, and the
499/// caller does not need to ask whether it should.
500fn generate_mipmaps(device: &ash::Device, cmd: vk::CommandBuffer, texture: &VulkanTexture) {
501    if texture.mip_levels <= 1 {
502        return;
503    }
504    let level_range = |level: u32| {
505        vk::ImageSubresourceRange::default()
506            .aspect_mask(vk::ImageAspectFlags::COLOR)
507            .base_mip_level(level)
508            .level_count(1)
509            .layer_count(1)
510    };
511    let barrier = |level: u32, from: vk::ImageLayout, to: vk::ImageLayout| {
512        let barrier = vk::ImageMemoryBarrier::default()
513            .old_layout(from)
514            .new_layout(to)
515            .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
516            .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
517            .image(texture.image)
518            .subresource_range(level_range(level))
519            .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
520            .dst_access_mask(vk::AccessFlags::TRANSFER_READ);
521        // SAFETY: the command buffer is recording and the image outlives the
522        // submission, which the one-shot helper waits on.
523        unsafe {
524            device.cmd_pipeline_barrier(
525                cmd,
526                vk::PipelineStageFlags::TRANSFER,
527                vk::PipelineStageFlags::TRANSFER,
528                vk::DependencyFlags::empty(),
529                &[],
530                &[],
531                &[barrier],
532            );
533        }
534    };
535
536    let mut width = texture.extent.width.max(1) as i32;
537    let mut height = texture.extent.height.max(1) as i32;
538    for level in 1..texture.mip_levels {
539        // The level above becomes readable. It was written either by the upload
540        // copy, for level zero, or by the previous blit.
541        barrier(
542            level - 1,
543            vk::ImageLayout::TRANSFER_DST_OPTIMAL,
544            vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
545        );
546        // An axis that has reached one texel stays there while the other keeps
547        // halving, which is what makes a chain over a long thin image end in a
548        // strip rather than in nothing.
549        let next_width = (width / 2).max(1);
550        let next_height = (height / 2).max(1);
551        let blit = vk::ImageBlit::default()
552            .src_subresource(
553                vk::ImageSubresourceLayers::default()
554                    .aspect_mask(vk::ImageAspectFlags::COLOR)
555                    .mip_level(level - 1)
556                    .layer_count(1),
557            )
558            .src_offsets([
559                vk::Offset3D { x: 0, y: 0, z: 0 },
560                vk::Offset3D {
561                    x: width,
562                    y: height,
563                    z: 1,
564                },
565            ])
566            .dst_subresource(
567                vk::ImageSubresourceLayers::default()
568                    .aspect_mask(vk::ImageAspectFlags::COLOR)
569                    .mip_level(level)
570                    .layer_count(1),
571            )
572            .dst_offsets([
573                vk::Offset3D { x: 0, y: 0, z: 0 },
574                vk::Offset3D {
575                    x: next_width,
576                    y: next_height,
577                    z: 1,
578                },
579            ]);
580        // SAFETY: as above, and both subresources are in the layouts named.
581        unsafe {
582            device.cmd_blit_image(
583                cmd,
584                texture.image,
585                vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
586                texture.image,
587                vk::ImageLayout::TRANSFER_DST_OPTIMAL,
588                &[blit],
589                vk::Filter::LINEAR,
590            );
591        }
592        width = next_width;
593        height = next_height;
594    }
595
596    // Every level but the last is a transfer source now, and the caller's
597    // record of the image's layout says destination. Putting them back is
598    // cheaper than teaching that record about levels, and the transition that
599    // follows -- to whatever samples this -- covers the whole image at once.
600    for level in 0..texture.mip_levels - 1 {
601        barrier(
602            level,
603            vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
604            vk::ImageLayout::TRANSFER_DST_OPTIMAL,
605        );
606    }
607}
608
609/// Insert a full barrier around a layout change.
610///
611/// Deliberately heavy-handed: ALL_COMMANDS on both sides with full access
612/// masks. These are one-shot setup and readback operations, not frame-loop
613/// work, so precision here buys nothing and getting it wrong costs
614/// hard-to-reproduce corruption.
615pub(crate) fn transition(
616    device: &ash::Device,
617    cmd: vk::CommandBuffer,
618    image: vk::Image,
619    from: vk::ImageLayout,
620    to: vk::ImageLayout,
621) {
622    let (src_stage, src_access) = source_scope(from);
623    let (dst_stage, dst_access) = destination_scope(to);
624
625    let barrier = vk::ImageMemoryBarrier::default()
626        .old_layout(from)
627        .new_layout(to)
628        .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
629        .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
630        .image(image)
631        .subresource_range(
632            vk::ImageSubresourceRange::default()
633                .aspect_mask(vk::ImageAspectFlags::COLOR)
634                // Every level, because this transitions an image rather than a
635                // level of one and a chain left half in the old layout is a
636                // validation error waiting for the first minified draw. The
637                // generation below transitions levels one at a time and says so
638                // explicitly; nothing else here has any business splitting them.
639                .level_count(vk::REMAINING_MIP_LEVELS)
640                .layer_count(1),
641        )
642        .src_access_mask(src_access)
643        .dst_access_mask(dst_access);
644
645    unsafe {
646        device.cmd_pipeline_barrier(
647            cmd,
648            src_stage,
649            dst_stage,
650            vk::DependencyFlags::empty(),
651            &[],
652            &[],
653            &[barrier],
654        );
655    }
656}
657
658/// What the image was being used for, given the layout it is leaving.
659///
660/// A barrier names the work it waits for and the work that waits on it. Naming
661/// all of it -- every stage, every access -- is always correct and is what this
662/// did: a full pipeline drain and a full cache flush for every transition. On
663/// the tiled and bandwidth-limited parts this renderer targets that is not a
664/// small waste, and it is avoidable, because a layout says what the image was
665/// for.
666///
667/// Anything unrecognized falls back to naming everything, which is the
668/// conservative direction: a scope too wide costs speed, and one too narrow is
669/// a missing barrier that renders correctly here and wrongly elsewhere.
670fn source_scope(layout: vk::ImageLayout) -> (vk::PipelineStageFlags, vk::AccessFlags) {
671    match layout {
672        // Nothing was in it worth preserving, so there is nothing to wait for.
673        vk::ImageLayout::UNDEFINED | vk::ImageLayout::PREINITIALIZED => (
674            vk::PipelineStageFlags::TOP_OF_PIPE,
675            vk::AccessFlags::empty(),
676        ),
677        vk::ImageLayout::TRANSFER_DST_OPTIMAL => (
678            vk::PipelineStageFlags::TRANSFER,
679            vk::AccessFlags::TRANSFER_WRITE,
680        ),
681        vk::ImageLayout::TRANSFER_SRC_OPTIMAL => (
682            vk::PipelineStageFlags::TRANSFER,
683            vk::AccessFlags::TRANSFER_READ,
684        ),
685
686        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => (
687            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
688            vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
689        ),
690        vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL => (
691            vk::PipelineStageFlags::FRAGMENT_SHADER,
692            vk::AccessFlags::SHADER_READ,
693        ),
694        // Reading is the presentation engine's, which no stage here names and
695        // which the acquire semaphore orders instead.
696        vk::ImageLayout::PRESENT_SRC_KHR => (
697            vk::PipelineStageFlags::BOTTOM_OF_PIPE,
698            vk::AccessFlags::empty(),
699        ),
700        _ => (
701            vk::PipelineStageFlags::ALL_COMMANDS,
702            vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE,
703        ),
704    }
705}
706
707/// What the image is about to be used for, given the layout it is entering.
708fn destination_scope(layout: vk::ImageLayout) -> (vk::PipelineStageFlags, vk::AccessFlags) {
709    match layout {
710        vk::ImageLayout::TRANSFER_DST_OPTIMAL => (
711            vk::PipelineStageFlags::TRANSFER,
712            vk::AccessFlags::TRANSFER_WRITE,
713        ),
714        vk::ImageLayout::TRANSFER_SRC_OPTIMAL => (
715            vk::PipelineStageFlags::TRANSFER,
716            vk::AccessFlags::TRANSFER_READ,
717        ),
718        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => (
719            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
720            vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::COLOR_ATTACHMENT_READ,
721        ),
722        vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL => (
723            vk::PipelineStageFlags::FRAGMENT_SHADER,
724            vk::AccessFlags::SHADER_READ,
725        ),
726        // Handed to the presentation engine, which the present semaphore
727        // orders. Naming a stage here would claim this device does the reading.
728        vk::ImageLayout::PRESENT_SRC_KHR => (
729            vk::PipelineStageFlags::BOTTOM_OF_PIPE,
730            vk::AccessFlags::empty(),
731        ),
732        _ => (
733            vk::PipelineStageFlags::ALL_COMMANDS,
734            vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE,
735        ),
736    }
737}
738
739fn sample_count_flags(count: u32) -> vk::SampleCountFlags {
740    match count {
741        2 => vk::SampleCountFlags::TYPE_2,
742        4 => vk::SampleCountFlags::TYPE_4,
743        8 => vk::SampleCountFlags::TYPE_8,
744        16 => vk::SampleCountFlags::TYPE_16,
745        _ => vk::SampleCountFlags::TYPE_1,
746    }
747}
748
749pub(crate) fn backend_err(what: &str, e: vk::Result) -> Error {
750    Error::Backend {
751        backend: "vulkan",
752        detail: format!("{what}: {e:?}"),
753    }
754}
755
756/// Attach backend detail to an allocator failure without losing the category.
757pub(crate) trait WithDetail {
758    fn with_detail<E: std::fmt::Display>(self, e: E) -> Error;
759}
760
761impl WithDetail for Error {
762    fn with_detail<E: std::fmt::Display>(self, e: E) -> Error {
763        Error::Backend {
764            backend: "vulkan",
765            detail: format!("{self}: {e}"),
766        }
767    }
768}
769
770#[cfg(test)]
771mod barrier_tests {
772    use super::*;
773
774    /// Every layout this renderer actually uses, and the direction it is used in.
775    const USED: &[vk::ImageLayout] = &[
776        vk::ImageLayout::TRANSFER_DST_OPTIMAL,
777        vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
778        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
779        vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
780        vk::ImageLayout::PRESENT_SRC_KHR,
781    ];
782
783    #[test]
784    fn a_known_layout_names_less_than_everything() {
785        // The point of the table is that a transition waits for the work the
786        // image was actually in, not for the whole pipeline to drain. Falling
787        // back to `ALL_COMMANDS` for a layout this renderer uses every frame
788        // would be correct and would quietly cost what the table was written to
789        // save, with nothing failing to say so.
790        for layout in USED {
791            let (src_stage, _) = source_scope(*layout);
792            let (dst_stage, _) = destination_scope(*layout);
793            assert_ne!(
794                src_stage,
795                vk::PipelineStageFlags::ALL_COMMANDS,
796                "{layout:?} falls back to a full drain as a source"
797            );
798            assert_ne!(
799                dst_stage,
800                vk::PipelineStageFlags::ALL_COMMANDS,
801                "{layout:?} falls back to a full drain as a destination"
802            );
803        }
804    }
805
806    #[test]
807    fn an_unknown_layout_names_everything() {
808        // The fallback goes the safe way. A scope too wide costs speed; one too
809        // narrow is a missing barrier, which renders correctly on the device it
810        // was written on.
811        let (stage, access) = source_scope(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
812        assert_eq!(stage, vk::PipelineStageFlags::ALL_COMMANDS);
813        assert!(access.contains(vk::AccessFlags::MEMORY_WRITE));
814    }
815
816    #[test]
817    fn leaving_undefined_waits_for_nothing() {
818        // Nothing in the image was worth preserving, so there is no prior work
819        // to order against -- and waiting for some anyway is the common way a
820        // first-use transition costs a frame's worth of drain.
821        let (stage, access) = source_scope(vk::ImageLayout::UNDEFINED);
822        assert_eq!(stage, vk::PipelineStageFlags::TOP_OF_PIPE);
823        assert!(access.is_empty());
824    }
825}