//! D3D12 samplers are fun and we're doing a decent amount of polyfilling with them. //! //! Do some tests to ensure things are working correctly and nothing gets mad. use wgpu_test::{did_fail, gpu_test, valid, GpuTestConfiguration, TestParameters, TestingContext}; // A number large enough to likely cause sampler caches to run out of space // on some devices. const PROBABLY_PROBLEMATIC_SAMPLER_COUNT: u32 = 8 * 1024; #[gpu_test] static SAMPLER_DEDUPLICATION: GpuTestConfiguration = GpuTestConfiguration::new().run_sync(sampler_deduplication); // Create a large number of samplers from the same two descriptors. // // Sampler deduplication in the backend should ensure this doesn't cause any issues. fn sampler_deduplication(ctx: TestingContext) { // Create 2 different sampler descriptors let desc1 = wgpu::SamplerDescriptor { label: Some("sampler1"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, lod_min_clamp: 0.0, lod_max_clamp: 100.0, compare: None, anisotropy_clamp: 1, border_color: None, }; let desc2 = wgpu::SamplerDescriptor { label: Some("sampler2"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, lod_min_clamp: 0.0, lod_max_clamp: 100.0, compare: None, anisotropy_clamp: 1, border_color: None, }; // Now create a bunch of samplers with these descriptors let samplers = (0..PROBABLY_PROBLEMATIC_SAMPLER_COUNT) .map(|i| { let desc = if i % 2 == 0 { &desc1 } else { &desc2 }; valid(&ctx.device, || ctx.device.create_sampler(desc)) }) .collect::>(); drop(samplers); } #[gpu_test] static SAMPLER_CREATION_FAILURE: GpuTestConfiguration = GpuTestConfiguration::new().run_sync(sampler_creation_failure); /// We want to test that sampler creation properly fails when we hit internal sampler /// cache limits. As we don't actually know what the limit is, we first create as many /// samplers as we can until we get the first failure. /// /// This failure being caught ensures that the error catching machinery on samplers /// is working as expected. /// /// We then clear all samplers and poll the device, which should leave the caches /// completely empty. /// /// We then try to create the same number of samplers to ensure the cache was entirely /// cleared. fn sampler_creation_failure(ctx: TestingContext) { let desc = wgpu::SamplerDescriptor { label: Some("sampler1"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, lod_min_clamp: 0.0, lod_max_clamp: 100.0, compare: None, anisotropy_clamp: 1, border_color: None, }; let mut sampler_storage = Vec::with_capacity(PROBABLY_PROBLEMATIC_SAMPLER_COUNT as usize); for i in 0..PROBABLY_PROBLEMATIC_SAMPLER_COUNT { let (failed, sampler) = did_fail(&ctx.device, || { ctx.device.create_sampler(&wgpu::SamplerDescriptor { lod_min_clamp: i as f32 * 0.01, ..desc }) }); if failed { break; } sampler_storage.push(sampler); } let failed_count = sampler_storage.len(); sampler_storage.clear(); ctx.device.poll(wgpu::PollType::Wait).unwrap(); for i in 0..failed_count { valid(&ctx.device, || { eprintln!("Trying to create sampler {}", i); let sampler = ctx.device.create_sampler(&wgpu::SamplerDescriptor { lod_min_clamp: i as f32 * 0.01, // Change the max clamp to ensure the sampler is using different cache slots from // the previous run. lod_max_clamp: 200.0, ..desc }); sampler_storage.push(sampler); }); } } const SINGLE_GROUP_BINDINGS: &str = r#" @group(0) @binding(0) var texture: texture_2d; @group(0) @binding(1) var sampler0: sampler; @group(0) @binding(2) var sampler1: sampler; @group(0) @binding(3) var sampler2: sampler; @group(1) @binding(0) var results: array; "#; const MULTI_GROUP_BINDINGS: &str = r#" @group(0) @binding(0) var texture: texture_2d; @group(0) @binding(1) var sampler0: sampler; @group(1) @binding(0) var sampler1: sampler; @group(2) @binding(0) var sampler2: sampler; @group(3) @binding(0) var results: array; "#; const SAMPLER_CODE: &str = r#" @compute @workgroup_size(1, 1, 1) fn cs_main() { // When sampling a 2x2 texture at the bottom left, we can change the address mode // on S/T to get different values. This allows us to make sure the right sampler // is being used. results[0] = textureSampleLevel(texture, sampler0, vec2f(0.0, 1.0), 0.0); results[1] = textureSampleLevel(texture, sampler1, vec2f(0.0, 1.0), 0.0); results[2] = textureSampleLevel(texture, sampler2, vec2f(0.0, 1.0), 0.0); } "#; enum GroupType { Single, Multi, } #[gpu_test] static SAMPLER_SINGLE_BIND_GROUP: GpuTestConfiguration = GpuTestConfiguration::new() .parameters( TestParameters::default() .test_features_limits() // In OpenGL textures cannot be used with multiple samplers. .skip(wgpu_test::FailureCase::backend(wgpu::Backends::GL)), ) .run_sync(|ctx| sampler_bind_group(ctx, GroupType::Single)); #[gpu_test] static SAMPLER_MULTI_BIND_GROUP: GpuTestConfiguration = GpuTestConfiguration::new() .parameters( TestParameters::default() .test_features_limits() // In OpenGL textures cannot be used with multiple samplers. .skip(wgpu_test::FailureCase::backend(wgpu::Backends::GL)), ) .run_sync(|ctx| sampler_bind_group(ctx, GroupType::Multi)); fn sampler_bind_group(ctx: TestingContext, group_type: GroupType) { let bindings = match group_type { GroupType::Single => SINGLE_GROUP_BINDINGS, GroupType::Multi => MULTI_GROUP_BINDINGS, }; let full_shader = format!("{}\n{}", bindings, SAMPLER_CODE); let module = ctx .device .create_shader_module(wgpu::ShaderModuleDescriptor { source: wgpu::ShaderSource::Wgsl(full_shader.into()), label: None, }); let mut bind_group_layouts = Vec::new(); match group_type { GroupType::Single => { let bgl = ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("combination_bgl"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); bind_group_layouts.push(bgl); } GroupType::Multi => { let bgl0 = ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("multiple_bgl0"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let bgl1 = ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("multiple_bgl1"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }], }); let bgl2 = ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("multiple_bgl2"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }], }); bind_group_layouts.push(bgl0); bind_group_layouts.push(bgl1); bind_group_layouts.push(bgl2); } } let output_bgl = ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("output_bgl"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: false }, has_dynamic_offset: false, min_binding_size: None, }, count: None, }], }); let mut bgl_references: Vec<_> = bind_group_layouts.iter().collect(); bgl_references.push(&output_bgl); let pipeline_layout = ctx .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("pipeline_layout"), bind_group_layouts: &bgl_references, push_constant_ranges: &[], }); let input_image = ctx.device.create_texture(&wgpu::TextureDescriptor { label: Some("input_image"), size: wgpu::Extent3d { width: 2, height: 2, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); let input_image_view = input_image.create_view(&wgpu::TextureViewDescriptor::default()); let image_data: [u8; 16] = [ 255, 0, 0, 255, /* */ 0, 255, 0, 255, // 0, 0, 255, 255, /* */ 255, 255, 255, 255, // ]; ctx.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &input_image, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, &image_data, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(8), rows_per_image: None, }, wgpu::Extent3d { width: 2, height: 2, depth_or_array_layers: 1, }, ); let address_modes = [ ( wgpu::AddressMode::ClampToEdge, wgpu::AddressMode::ClampToEdge, ), (wgpu::AddressMode::Repeat, wgpu::AddressMode::ClampToEdge), (wgpu::AddressMode::ClampToEdge, wgpu::AddressMode::Repeat), ]; let samplers = address_modes.map(|(address_mode_u, address_mode_v)| { ctx.device.create_sampler(&wgpu::SamplerDescriptor { label: None, address_mode_u, address_mode_v, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest, lod_min_clamp: 0.0, lod_max_clamp: 100.0, compare: None, anisotropy_clamp: 1, border_color: None, }) }); let mut bind_groups = Vec::new(); match group_type { GroupType::Single => { let bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("combination_bg"), layout: &bind_group_layouts[0], entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&input_image_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&samplers[0]), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&samplers[1]), }, wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&samplers[2]), }, ], }); bind_groups.push(bg); } GroupType::Multi => { let bg0 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("multiple_bg0"), layout: &bind_group_layouts[0], entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&input_image_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&samplers[0]), }, ], }); let bg1 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("multiple_bg1"), layout: &bind_group_layouts[1], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(&samplers[1]), }], }); let bg2 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("multiple_bg2"), layout: &bind_group_layouts[2], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(&samplers[2]), }], }); bind_groups.push(bg0); bind_groups.push(bg1); bind_groups.push(bg2); } } let output_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { label: Some("output_buffer"), size: 48, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false, }); let transfer_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { label: Some("transfer_buffer"), size: 48, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); let output_bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("output_bg"), layout: &output_bgl, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &output_buffer, offset: 0, size: None, }), }], }); let mut bg_references = bind_groups.iter().collect::>(); bg_references.push(&output_bg); let pipeline = ctx .device .create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some("pipeline"), layout: Some(&pipeline_layout), module: &module, entry_point: Some("cs_main"), cache: None, compilation_options: Default::default(), }); let mut encoder = ctx .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("encoder"), }); { let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: None, timestamp_writes: None, }); cpass.set_pipeline(&pipeline); for (i, &bg) in bg_references.iter().enumerate() { cpass.set_bind_group(i as u32, bg, &[]); } cpass.dispatch_workgroups(1, 1, 1); } encoder.copy_buffer_to_buffer(&output_buffer, 0, &transfer_buffer, 0, 48); ctx.queue.submit([encoder.finish()]); let buffer_slice = transfer_buffer.slice(..); buffer_slice.map_async(wgpu::MapMode::Read, |_| {}); ctx.device.poll(wgpu::PollType::Wait).unwrap(); let buffer_data = buffer_slice.get_mapped_range(); let f32_buffer: &[f32] = bytemuck::cast_slice(&buffer_data); let correct_values: [f32; 12] = [ 0.0, 0.0, 1.0, 1.0, // 0.5, 0.5, 1.0, 1.0, // 0.5, 0.0, 0.5, 1.0, // ]; let iter = f32_buffer.iter().zip(correct_values.iter()); for (&result, &value) in iter { approx::assert_relative_eq!(result, value, max_relative = 0.02); } }