// Copyright (c) 2022 The vulkano developers // Licensed under the Apache License, Version 2.0 // or the MIT // license , // at your option. All files in the project carrying such // notice may not be copied, modified, or distributed except // according to those terms. //! Conversion from sampled YCbCr image data to RGB shader data. //! //! A sampler YCbCr conversion is an object that assists a sampler when converting from YCbCr //! formats and/or YCbCr texel input data. It is used to read frames of video data within a shader, //! possibly to apply it as texture on a rendered primitive. Sampler YCbCr conversion can only be //! used with certain formats, and conversely, some formats require the use of a sampler YCbCr //! conversion to be sampled at all. //! //! A sampler YCbCr conversion can only be used with a combined image sampler descriptor in a //! descriptor set. The conversion must be attached on both the image view and sampler in the //! descriptor, and the sampler must be included in the descriptor set layout as an immutable //! sampler. //! //! # Examples //! //! ``` //! # let device: std::sync::Arc = return; //! # let image_data: Vec = return; //! # let queue: std::sync::Arc = return; //! # let memory_allocator: vulkano::memory::allocator::StandardMemoryAllocator = return; //! # let descriptor_set_allocator: vulkano::descriptor_set::allocator::StandardDescriptorSetAllocator = return; //! # let mut command_buffer_builder: vulkano::command_buffer::AutoCommandBufferBuilder = return; //! use vulkano::descriptor_set::{PersistentDescriptorSet, WriteDescriptorSet}; //! use vulkano::descriptor_set::layout::{DescriptorSetLayout, DescriptorSetLayoutBinding, DescriptorSetLayoutCreateInfo, DescriptorType}; //! use vulkano::format::Format; //! use vulkano::image::{ImmutableImage, ImageCreateFlags, ImageDimensions, ImageUsage, MipmapsCount}; //! use vulkano::image::view::{ImageView, ImageViewCreateInfo}; //! use vulkano::sampler::{Sampler, SamplerCreateInfo}; //! use vulkano::sampler::ycbcr::{SamplerYcbcrConversion, SamplerYcbcrConversionCreateInfo, SamplerYcbcrModelConversion}; //! use vulkano::shader::ShaderStage; //! //! let conversion = SamplerYcbcrConversion::new(device.clone(), SamplerYcbcrConversionCreateInfo { //! format: Some(Format::G8_B8_R8_3PLANE_420_UNORM), //! ycbcr_model: SamplerYcbcrModelConversion::YcbcrIdentity, //! ..Default::default() //! }) //! .unwrap(); //! //! let sampler = Sampler::new(device.clone(), SamplerCreateInfo { //! sampler_ycbcr_conversion: Some(conversion.clone()), //! ..Default::default() //! }) //! .unwrap(); //! //! let descriptor_set_layout = DescriptorSetLayout::new( //! device.clone(), //! DescriptorSetLayoutCreateInfo { //! bindings: [( //! 0, //! DescriptorSetLayoutBinding { //! stages: ShaderStage::Fragment.into(), //! immutable_samplers: vec![sampler], //! ..DescriptorSetLayoutBinding::descriptor_type(DescriptorType::CombinedImageSampler) //! }, //! )] //! .into(), //! ..Default::default() //! }, //! ).unwrap(); //! //! let image = ImmutableImage::from_iter( //! &memory_allocator, //! image_data, //! ImageDimensions::Dim2d { width: 1920, height: 1080, array_layers: 1 }, //! MipmapsCount::One, //! Format::G8_B8_R8_3PLANE_420_UNORM, //! &mut command_buffer_builder, //! ).unwrap(); //! //! let create_info = ImageViewCreateInfo { //! sampler_ycbcr_conversion: Some(conversion.clone()), //! ..ImageViewCreateInfo::from_image(&image) //! }; //! let image_view = ImageView::new(image, create_info).unwrap(); //! //! let descriptor_set = PersistentDescriptorSet::new( //! &descriptor_set_allocator, //! descriptor_set_layout.clone(), //! [WriteDescriptorSet::image_view(0, image_view)], //! ).unwrap(); //! ``` use crate::{ device::{Device, DeviceOwned}, format::{ChromaSampling, Format, FormatFeatures, NumericType}, macros::{impl_id_counter, vulkan_enum}, sampler::{ComponentMapping, ComponentSwizzle, Filter}, OomError, RequirementNotMet, RequiresOneOf, Version, VulkanError, VulkanObject, }; use std::{ error::Error, fmt::{Display, Error as FmtError, Formatter}, mem::MaybeUninit, num::NonZeroU64, ptr, sync::Arc, }; /// Describes how sampled image data should converted from a YCbCr representation to an RGB one. #[derive(Debug)] pub struct SamplerYcbcrConversion { handle: ash::vk::SamplerYcbcrConversion, device: Arc, id: NonZeroU64, format: Option, ycbcr_model: SamplerYcbcrModelConversion, ycbcr_range: SamplerYcbcrRange, component_mapping: ComponentMapping, chroma_offset: [ChromaLocation; 2], chroma_filter: Filter, force_explicit_reconstruction: bool, } impl SamplerYcbcrConversion { /// Creates a new `SamplerYcbcrConversion`. /// /// The [`sampler_ycbcr_conversion`](crate::device::Features::sampler_ycbcr_conversion) /// feature must be enabled on the device. pub fn new( device: Arc, create_info: SamplerYcbcrConversionCreateInfo, ) -> Result, SamplerYcbcrConversionCreationError> { let SamplerYcbcrConversionCreateInfo { format, ycbcr_model, ycbcr_range, component_mapping, chroma_offset, chroma_filter, force_explicit_reconstruction, _ne: _, } = create_info; if !device.enabled_features().sampler_ycbcr_conversion { return Err(SamplerYcbcrConversionCreationError::RequirementNotMet { required_for: "`SamplerYcbcrConversion::new`", requires_one_of: RequiresOneOf { features: &["sampler_ycbcr_conversion"], ..Default::default() }, }); } let format = match format { Some(f) => f, None => { return Err(SamplerYcbcrConversionCreationError::FormatMissing); } }; // VUID-VkSamplerYcbcrConversionCreateInfo-format-parameter format.validate_device(&device)?; // VUID-VkSamplerYcbcrConversionCreateInfo-ycbcrModel-parameter ycbcr_model.validate_device(&device)?; // VUID-VkSamplerYcbcrConversionCreateInfo-ycbcrRange-parameter ycbcr_range.validate_device(&device)?; // VUID-VkComponentMapping-r-parameter component_mapping.r.validate_device(&device)?; // VUID-VkComponentMapping-g-parameter component_mapping.g.validate_device(&device)?; // VUID-VkComponentMapping-b-parameter component_mapping.b.validate_device(&device)?; // VUID-VkComponentMapping-a-parameter component_mapping.a.validate_device(&device)?; for offset in chroma_offset { // VUID-VkSamplerYcbcrConversionCreateInfo-xChromaOffset-parameter // VUID-VkSamplerYcbcrConversionCreateInfo-yChromaOffset-parameter offset.validate_device(&device)?; } // VUID-VkSamplerYcbcrConversionCreateInfo-chromaFilter-parameter chroma_filter.validate_device(&device)?; // VUID-VkSamplerYcbcrConversionCreateInfo-format-04061 if !format .type_color() .map_or(false, |ty| ty == NumericType::UNORM) { return Err(SamplerYcbcrConversionCreationError::FormatNotUnorm); } // Use unchecked, because all validation has been done above. let potential_format_features = unsafe { device .physical_device() .format_properties_unchecked(format) .potential_format_features() }; // VUID-VkSamplerYcbcrConversionCreateInfo-format-01650 if !potential_format_features.intersects( FormatFeatures::MIDPOINT_CHROMA_SAMPLES | FormatFeatures::COSITED_CHROMA_SAMPLES, ) { return Err(SamplerYcbcrConversionCreationError::FormatNotSupported); } if let Some(chroma_sampling @ (ChromaSampling::Mode422 | ChromaSampling::Mode420)) = format.ycbcr_chroma_sampling() { let chroma_offsets_to_check = match chroma_sampling { ChromaSampling::Mode420 => &chroma_offset[0..2], ChromaSampling::Mode422 => &chroma_offset[0..1], _ => unreachable!(), }; for offset in chroma_offsets_to_check { match offset { ChromaLocation::CositedEven => { // VUID-VkSamplerYcbcrConversionCreateInfo-xChromaOffset-01651 if !potential_format_features .intersects(FormatFeatures::COSITED_CHROMA_SAMPLES) { return Err( SamplerYcbcrConversionCreationError::FormatChromaOffsetNotSupported, ); } } ChromaLocation::Midpoint => { // VUID-VkSamplerYcbcrConversionCreateInfo-xChromaOffset-01652 if !potential_format_features .intersects(FormatFeatures::MIDPOINT_CHROMA_SAMPLES) { return Err( SamplerYcbcrConversionCreationError::FormatChromaOffsetNotSupported, ); } } } } // VUID-VkSamplerYcbcrConversionCreateInfo-components-02581 let g_ok = component_mapping.g_is_identity(); // VUID-VkSamplerYcbcrConversionCreateInfo-components-02582 let a_ok = component_mapping.a_is_identity() || matches!( component_mapping.a, ComponentSwizzle::One | ComponentSwizzle::Zero ); // VUID-VkSamplerYcbcrConversionCreateInfo-components-02583 // VUID-VkSamplerYcbcrConversionCreateInfo-components-02584 // VUID-VkSamplerYcbcrConversionCreateInfo-components-02585 let rb_ok1 = component_mapping.r_is_identity() && component_mapping.b_is_identity(); let rb_ok2 = matches!(component_mapping.r, ComponentSwizzle::Blue) && matches!(component_mapping.b, ComponentSwizzle::Red); if !(g_ok && a_ok && (rb_ok1 || rb_ok2)) { return Err(SamplerYcbcrConversionCreationError::FormatInvalidComponentMapping); } } let components_bits = { let bits = format.components(); component_mapping .component_map() .map(move |i| i.map(|i| bits[i])) }; // VUID-VkSamplerYcbcrConversionCreateInfo-ycbcrModel-01655 if ycbcr_model != SamplerYcbcrModelConversion::RgbIdentity && !components_bits[0..3] .iter() .all(|b| b.map_or(false, |b| b != 0)) { return Err(SamplerYcbcrConversionCreationError::YcbcrModelInvalidComponentMapping); } // VUID-VkSamplerYcbcrConversionCreateInfo-ycbcrRange-02748 if ycbcr_range == SamplerYcbcrRange::ItuNarrow { // TODO: Spec doesn't say how many bits `Zero` and `One` are considered to have, so // just skip them for now. for &bits in components_bits[0..3].iter().flatten() { if bits < 8 { return Err(SamplerYcbcrConversionCreationError::YcbcrRangeFormatNotEnoughBits); } } } // VUID-VkSamplerYcbcrConversionCreateInfo-forceExplicitReconstruction-01656 if force_explicit_reconstruction && !potential_format_features.intersects(FormatFeatures:: SAMPLED_IMAGE_YCBCR_CONVERSION_CHROMA_RECONSTRUCTION_EXPLICIT_FORCEABLE) { return Err( SamplerYcbcrConversionCreationError::FormatForceExplicitReconstructionNotSupported, ); } match chroma_filter { Filter::Nearest => (), Filter::Linear => { // VUID-VkSamplerYcbcrConversionCreateInfo-chromaFilter-01657 if !potential_format_features .intersects(FormatFeatures::SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER) { return Err( SamplerYcbcrConversionCreationError::FormatLinearFilterNotSupported, ); } } Filter::Cubic => { return Err(SamplerYcbcrConversionCreationError::CubicFilterNotSupported); } } let create_info = ash::vk::SamplerYcbcrConversionCreateInfo { format: format.into(), ycbcr_model: ycbcr_model.into(), ycbcr_range: ycbcr_range.into(), components: component_mapping.into(), x_chroma_offset: chroma_offset[0].into(), y_chroma_offset: chroma_offset[1].into(), chroma_filter: chroma_filter.into(), force_explicit_reconstruction: force_explicit_reconstruction as ash::vk::Bool32, ..Default::default() }; let handle = unsafe { let fns = device.fns(); let create_sampler_ycbcr_conversion = if device.api_version() >= Version::V1_1 { fns.v1_1.create_sampler_ycbcr_conversion } else { fns.khr_sampler_ycbcr_conversion .create_sampler_ycbcr_conversion_khr }; let mut output = MaybeUninit::uninit(); create_sampler_ycbcr_conversion( device.handle(), &create_info, ptr::null(), output.as_mut_ptr(), ) .result() .map_err(VulkanError::from)?; output.assume_init() }; Ok(Arc::new(SamplerYcbcrConversion { handle, device, id: Self::next_id(), format: Some(format), ycbcr_model, ycbcr_range, component_mapping, chroma_offset, chroma_filter, force_explicit_reconstruction, })) } /// Creates a new `SamplerYcbcrConversion` from a raw object handle. /// /// # Safety /// /// - `handle` must be a valid Vulkan object handle created from `device`. /// - `create_info` must match the info used to create the object. /// - `create_info.format` must be `Some`. #[inline] pub unsafe fn from_handle( device: Arc, handle: ash::vk::SamplerYcbcrConversion, create_info: SamplerYcbcrConversionCreateInfo, ) -> Arc { let SamplerYcbcrConversionCreateInfo { format, ycbcr_model, ycbcr_range, component_mapping, chroma_offset, chroma_filter, force_explicit_reconstruction, _ne: _, } = create_info; Arc::new(SamplerYcbcrConversion { handle, device, id: Self::next_id(), format, ycbcr_model, ycbcr_range, component_mapping, chroma_offset, chroma_filter, force_explicit_reconstruction, }) } /// Returns the chroma filter used by the conversion. #[inline] pub fn chroma_filter(&self) -> Filter { self.chroma_filter } /// Returns the chroma offsets used by the conversion. #[inline] pub fn chroma_offset(&self) -> [ChromaLocation; 2] { self.chroma_offset } /// Returns the component mapping of the conversion. #[inline] pub fn component_mapping(&self) -> ComponentMapping { self.component_mapping } /// Returns whether the conversion has forced explicit reconstruction to be enabled. #[inline] pub fn force_explicit_reconstruction(&self) -> bool { self.force_explicit_reconstruction } /// Returns the format that the conversion was created for. #[inline] pub fn format(&self) -> Option { self.format } /// Returns the YCbCr model of the conversion. #[inline] pub fn ycbcr_model(&self) -> SamplerYcbcrModelConversion { self.ycbcr_model } /// Returns the YCbCr range of the conversion. #[inline] pub fn ycbcr_range(&self) -> SamplerYcbcrRange { self.ycbcr_range } /// Returns whether `self` is equal or identically defined to `other`. #[inline] pub fn is_identical(&self, other: &SamplerYcbcrConversion) -> bool { self.handle == other.handle || { let &Self { handle: _, device: _, id: _, format, ycbcr_model, ycbcr_range, component_mapping, chroma_offset, chroma_filter, force_explicit_reconstruction, } = self; format == other.format && ycbcr_model == other.ycbcr_model && ycbcr_range == other.ycbcr_range && component_mapping == other.component_mapping && chroma_offset == other.chroma_offset && chroma_filter == other.chroma_filter && force_explicit_reconstruction == other.force_explicit_reconstruction } } } impl Drop for SamplerYcbcrConversion { #[inline] fn drop(&mut self) { unsafe { let fns = self.device.fns(); let destroy_sampler_ycbcr_conversion = if self.device.api_version() >= Version::V1_1 { fns.v1_1.destroy_sampler_ycbcr_conversion } else { fns.khr_sampler_ycbcr_conversion .destroy_sampler_ycbcr_conversion_khr }; destroy_sampler_ycbcr_conversion(self.device.handle(), self.handle, ptr::null()); } } } unsafe impl VulkanObject for SamplerYcbcrConversion { type Handle = ash::vk::SamplerYcbcrConversion; #[inline] fn handle(&self) -> Self::Handle { self.handle } } unsafe impl DeviceOwned for SamplerYcbcrConversion { #[inline] fn device(&self) -> &Arc { &self.device } } impl_id_counter!(SamplerYcbcrConversion); /// Error that can happen when creating a `SamplerYcbcrConversion`. #[derive(Clone, Debug, PartialEq, Eq)] pub enum SamplerYcbcrConversionCreationError { /// Not enough memory. OomError(OomError), RequirementNotMet { required_for: &'static str, requires_one_of: RequiresOneOf, }, /// The `Cubic` filter was specified. CubicFilterNotSupported, /// No format was specified when one was required. FormatMissing, /// The format has a color type other than `UNORM`. FormatNotUnorm, /// The format does not support sampler YCbCr conversion. FormatNotSupported, /// The format does not support the chosen chroma offsets. FormatChromaOffsetNotSupported, /// The component mapping was not valid for use with the chosen format. FormatInvalidComponentMapping, /// The format does not support `force_explicit_reconstruction`. FormatForceExplicitReconstructionNotSupported, /// The format does not support the `Linear` filter. FormatLinearFilterNotSupported, /// The component mapping was not valid for use with the chosen YCbCr model. YcbcrModelInvalidComponentMapping, /// For the chosen `ycbcr_range`, the R, G or B components being read from the `format` do not /// have the minimum number of required bits. YcbcrRangeFormatNotEnoughBits, } impl Error for SamplerYcbcrConversionCreationError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { SamplerYcbcrConversionCreationError::OomError(err) => Some(err), _ => None, } } } impl Display for SamplerYcbcrConversionCreationError { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { match self { Self::OomError(_) => write!(f, "not enough memory available"), Self::RequirementNotMet { required_for, requires_one_of, } => write!( f, "a requirement was not met for: {}; requires one of: {}", required_for, requires_one_of, ), Self::CubicFilterNotSupported => { write!(f, "the `Cubic` filter was specified") } Self::FormatMissing => { write!(f, "no format was specified when one was required") } Self::FormatNotUnorm => { write!(f, "the format has a color type other than `UNORM`") } Self::FormatNotSupported => { write!(f, "the format does not support sampler YCbCr conversion") } Self::FormatChromaOffsetNotSupported => { write!(f, "the format does not support the chosen chroma offsets") } Self::FormatInvalidComponentMapping => write!( f, "the component mapping was not valid for use with the chosen format", ), Self::FormatForceExplicitReconstructionNotSupported => write!( f, "the format does not support `force_explicit_reconstruction`", ), Self::FormatLinearFilterNotSupported => { write!(f, "the format does not support the `Linear` filter") } Self::YcbcrModelInvalidComponentMapping => write!( f, "the component mapping was not valid for use with the chosen YCbCr model", ), Self::YcbcrRangeFormatNotEnoughBits => write!( f, "for the chosen `ycbcr_range`, the R, G or B components being read from the \ `format` do not have the minimum number of required bits", ), } } } impl From for SamplerYcbcrConversionCreationError { fn from(err: OomError) -> SamplerYcbcrConversionCreationError { SamplerYcbcrConversionCreationError::OomError(err) } } impl From for SamplerYcbcrConversionCreationError { fn from(err: VulkanError) -> SamplerYcbcrConversionCreationError { match err { err @ VulkanError::OutOfHostMemory => { SamplerYcbcrConversionCreationError::OomError(OomError::from(err)) } err @ VulkanError::OutOfDeviceMemory => { SamplerYcbcrConversionCreationError::OomError(OomError::from(err)) } _ => panic!("unexpected error: {:?}", err), } } } impl From for SamplerYcbcrConversionCreationError { fn from(err: RequirementNotMet) -> Self { Self::RequirementNotMet { required_for: err.required_for, requires_one_of: err.requires_one_of, } } } /// Parameters to create a new `SamplerYcbcrConversion`. #[derive(Clone, Debug)] pub struct SamplerYcbcrConversionCreateInfo { /// The image view format that this conversion will read data from. The conversion cannot be /// used with image views of any other format. /// /// The format must support YCbCr conversions, meaning that its `FormatFeatures` must support /// at least one of `cosited_chroma_samples` or `midpoint_chroma_samples`. /// /// If this is set to a format that has chroma subsampling (contains `422` or `420` in the name) /// then `component_mapping` is restricted as follows: /// - `g` must be identity swizzled. /// - `a` must be identity swizzled or `Zero` or `One`. /// - `r` and `b` must be identity swizzled or mapped to each other. /// /// Compatibility notice: currently, this value must be `Some`, but future additions may allow /// `None` as a valid value as well. /// /// The default value is `None`. pub format: Option, /// The conversion between the input color model and the output RGB color model. /// /// If this is not set to `RgbIdentity`, then the `r`, `g` and `b` components of /// `component_mapping` must not be `Zero` or `One`, and the component being read must exist in /// `format` (must be represented as a nonzero number of bits). /// /// The default value is [`RgbIdentity`](SamplerYcbcrModelConversion::RgbIdentity). pub ycbcr_model: SamplerYcbcrModelConversion, /// If `ycbcr_model` is not `RgbIdentity`, specifies the range expansion of the input values /// that should be used. /// /// If this is set to `ItuNarrow`, then the `r`, `g` and `b` components of `component_mapping` /// must each map to a component of `format` that is represented with at least 8 bits. /// /// The default value is [`ItuFull`](SamplerYcbcrRange::ItuFull). pub ycbcr_range: SamplerYcbcrRange, /// The mapping to apply to the components of the input format, before color model conversion /// and range expansion. /// /// The default value is [`ComponentMapping::identity()`]. pub component_mapping: ComponentMapping, /// For formats with chroma subsampling and a `Linear` filter, specifies the sampled location /// for the subsampled components, in the x and y direction. /// /// The value is ignored if the filter is `Nearest` or the corresponding axis is not chroma /// subsampled. If the value is not ignored, the format must support the chosen mode. /// /// The default value is [`CositedEven`](ChromaLocation::CositedEven) for both axes. pub chroma_offset: [ChromaLocation; 2], /// For formats with chroma subsampling, specifies the filter used for reconstructing the chroma /// components to full resolution. /// /// The `Cubic` filter is not supported. If `Linear` is used, the format must support it. /// /// The default value is [`Nearest`](Filter::Nearest). pub chroma_filter: Filter, /// Forces explicit reconstruction if the implementation does not use it by default. The format /// must support it. See /// [the spec](https://registry.khronos.org/vulkan/specs/1.2-extensions/html/chap16.html#textures-chroma-reconstruction) /// for more information. /// /// The default value is `false`. pub force_explicit_reconstruction: bool, pub _ne: crate::NonExhaustive, } impl Default for SamplerYcbcrConversionCreateInfo { #[inline] fn default() -> Self { Self { format: None, ycbcr_model: SamplerYcbcrModelConversion::RgbIdentity, ycbcr_range: SamplerYcbcrRange::ItuFull, component_mapping: ComponentMapping::identity(), chroma_offset: [ChromaLocation::CositedEven; 2], chroma_filter: Filter::Nearest, force_explicit_reconstruction: false, _ne: crate::NonExhaustive(()), } } } vulkan_enum! { #[non_exhaustive] /// The conversion between the color model of the source image and the color model of the shader. SamplerYcbcrModelConversion = SamplerYcbcrModelConversion(i32); /// The input values are already in the shader's model, and are passed through unmodified. RgbIdentity = RGB_IDENTITY, /// The input values are only range expanded, no other modifications are done. YcbcrIdentity = YCBCR_IDENTITY, /// The input values are converted according to the /// [ITU-R BT.709](https://en.wikipedia.org/wiki/Rec._709) standard. Ycbcr709 = YCBCR_709, /// The input values are converted according to the /// [ITU-R BT.601](https://en.wikipedia.org/wiki/Rec._601) standard. Ycbcr601 = YCBCR_601, /// The input values are converted according to the /// [ITU-R BT.2020](https://en.wikipedia.org/wiki/Rec._2020) standard. Ycbcr2020 = YCBCR_2020, } vulkan_enum! { #[non_exhaustive] /// How the numeric range of the input data is converted. SamplerYcbcrRange = SamplerYcbcrRange(i32); /// The input values cover the full numeric range, and are interpreted according to the ITU /// "full range" rules. ItuFull = ITU_FULL, /// The input values cover only a subset of the numeric range, with the remainder reserved as /// headroom/footroom. The values are interpreted according to the ITU "narrow range" rules. ItuNarrow = ITU_NARROW, } vulkan_enum! { #[non_exhaustive] /// For formats with chroma subsampling, the location where the chroma components are sampled, /// relative to the luma component. ChromaLocation = ChromaLocation(i32); /// The chroma components are sampled at the even luma coordinate. CositedEven = COSITED_EVEN, /// The chroma components are sampled at the midpoint between the even luma coordinate and /// the next higher odd luma coordinate. Midpoint = MIDPOINT, } #[cfg(test)] mod tests { use super::{SamplerYcbcrConversion, SamplerYcbcrConversionCreationError}; use crate::RequiresOneOf; #[test] fn feature_not_enabled() { let (device, _queue) = gfx_dev_and_queue!(); let r = SamplerYcbcrConversion::new(device, Default::default()); match r { Err(SamplerYcbcrConversionCreationError::RequirementNotMet { requires_one_of: RequiresOneOf { features, .. }, .. }) if features.contains(&"sampler_ycbcr_conversion") => (), _ => panic!(), } } }