From 5a10d31a42297de97f6586648e756f48a401051e Mon Sep 17 00:00:00 2001 From: noituri Date: Mon, 22 Sep 2025 17:16:47 +0200 Subject: [PATCH] Make multi-planar textures renderable --- CHANGELOG.md | 2 + tests/tests/wgpu-gpu/planar_texture/mod.rs | 150 +++++++++++++++++- .../planar_texture_rendering.wgsl | 35 ++++ ...ture.wgsl => planar_texture_sampling.wgsl} | 0 tests/tests/wgpu-validation/api/texture.rs | 84 ++++++++++ wgpu-core/src/command/render.rs | 11 +- wgpu-core/src/conv.rs | 7 +- wgpu-core/src/device/resource.rs | 25 ++- wgpu-hal/src/vulkan/device.rs | 3 +- wgpu-types/src/lib.rs | 26 ++- 10 files changed, 329 insertions(+), 14 deletions(-) create mode 100644 tests/tests/wgpu-gpu/planar_texture/planar_texture_rendering.wgsl rename tests/tests/wgpu-gpu/planar_texture/{planar_texture.wgsl => planar_texture_sampling.wgsl} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c22134b2b6f..c3fdac122d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,8 @@ By @cwfitzgerald in [#8162](https://github.com/gfx-rs/wgpu/pull/8162). - Added support for external textures based on WebGPU's [`GPUExternalTexture`](https://www.w3.org/TR/webgpu/#gpuexternaltexture). These allow shaders to transparently operate on potentially multiplanar source texture data in either RGB or YCbCr formats via WGSL's `texture_external` type. This is gated behind the `Features::EXTERNAL_TEXTURE` feature, which is currently only supported on DX12. By @jamienicol in [#4386](https://github.com/gfx-rs/wgpu/issues/4386). +- Added support for rendering onto multi-planar textures. By @noituri in [#8307](https://github.com/gfx-rs/wgpu/pull/8307). + ### Changes #### General diff --git a/tests/tests/wgpu-gpu/planar_texture/mod.rs b/tests/tests/wgpu-gpu/planar_texture/mod.rs index 961d4b43f64..51f1025850f 100644 --- a/tests/tests/wgpu-gpu/planar_texture/mod.rs +++ b/tests/tests/wgpu-gpu/planar_texture/mod.rs @@ -8,6 +8,7 @@ pub fn all_tests(tests: &mut Vec) { tests.extend([ NV12_TEXTURE_CREATION_SAMPLING, P010_TEXTURE_CREATION_SAMPLING, + NV12_TEXTURE_RENDERING, ]); } @@ -21,7 +22,7 @@ fn test_planar_texture_creation_sampling( let shader = ctx .device - .create_shader_module(wgpu::include_wgsl!("planar_texture.wgsl")); + .create_shader_module(wgpu::include_wgsl!("planar_texture_sampling.wgsl")); let pipeline = ctx .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { @@ -108,6 +109,111 @@ fn test_planar_texture_creation_sampling( ctx.queue.submit(Some(encoder.finish())); } +// Helper function to test rendering onto planar texture. +fn test_planar_texture_rendering( + ctx: &TestingContext, + (y_view, y_format): (&wgpu::TextureView, wgpu::TextureFormat), + (uv_view, uv_format): (&wgpu::TextureView, wgpu::TextureFormat), +) { + let shader = ctx + .device + .create_shader_module(wgpu::include_wgsl!("planar_texture_rendering.wgsl")); + let y_pipeline = ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("y plane pipeline"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_y_main"), + compilation_options: Default::default(), + targets: &[Some(y_format.into())], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + strip_index_format: Some(wgpu::IndexFormat::Uint32), + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let uv_pipeline = ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("uv plane pipeline"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_uv_main"), + compilation_options: Default::default(), + targets: &[Some(uv_format.into())], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + strip_index_format: Some(wgpu::IndexFormat::Uint32), + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + ops: wgpu::Operations::default(), + resolve_target: None, + view: y_view, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&y_pipeline); + rpass.draw(0..3, 0..1); + } + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + ops: wgpu::Operations::default(), + resolve_target: None, + view: uv_view, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&uv_pipeline); + rpass.draw(0..3, 0..1); + } + + ctx.queue.submit(Some(encoder.finish())); +} + /// Ensures that creation and sampling of an NV12 format texture works as /// expected. #[gpu_test] @@ -187,3 +293,45 @@ static P010_TEXTURE_CREATION_SAMPLING: GpuTestConfiguration = GpuTestConfigurati test_planar_texture_creation_sampling(&ctx, &y_view, &uv_view); }); + +/// Ensures that rendering on to NV12 format texture works as expected. +#[gpu_test] +static NV12_TEXTURE_RENDERING: GpuTestConfiguration = GpuTestConfiguration::new() + .parameters( + TestParameters::default() + .features(wgpu::Features::TEXTURE_FORMAT_NV12) + .enable_noop(), + ) + .run_sync(|ctx| { + let size = wgpu::Extent3d { + width: 256, + height: 256, + depth_or_array_layers: 1, + }; + let tex = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: None, + dimension: wgpu::TextureDimension::D2, + size, + format: wgpu::TextureFormat::NV12, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + mip_level_count: 1, + sample_count: 1, + view_formats: &[], + }); + let y_view = tex.create_view(&wgpu::TextureViewDescriptor { + format: Some(wgpu::TextureFormat::R8Unorm), + aspect: wgpu::TextureAspect::Plane0, + ..Default::default() + }); + let uv_view = tex.create_view(&wgpu::TextureViewDescriptor { + format: Some(wgpu::TextureFormat::Rg8Unorm), + aspect: wgpu::TextureAspect::Plane1, + ..Default::default() + }); + + test_planar_texture_rendering( + &ctx, + (&y_view, wgpu::TextureFormat::R8Unorm), + (&uv_view, wgpu::TextureFormat::Rg8Unorm), + ); + }); diff --git a/tests/tests/wgpu-gpu/planar_texture/planar_texture_rendering.wgsl b/tests/tests/wgpu-gpu/planar_texture/planar_texture_rendering.wgsl new file mode 100644 index 00000000000..ddd451c430e --- /dev/null +++ b/tests/tests/wgpu-gpu/planar_texture/planar_texture_rendering.wgsl @@ -0,0 +1,35 @@ +struct VertexOutput { + @builtin(position) position: vec4, +} + +const VERTICES: array, 3> = array, 3>( + vec3(-0.5, 0.0, 0.0), + vec3(0.5, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { + var output: VertexOutput; + output.position = vec4(VERTICES[idx], 1.0); + return output; +} + +@fragment +fn fs_y_main(input: VertexOutput) -> @location(0) f32 { + let color = vec3(1.0); + let conversion_weights = vec3(0.2126, 0.7152, 0.0722); + return clamp(dot(color, conversion_weights), 0.0, 1.0); +} + +@fragment +fn fs_uv_main(input: VertexOutput) -> @location(0) vec2 { + let color = vec3(1.0); + let conversion_weights = mat3x2( + -0.1146, 0.5, + -0.3854, -0.4542, + 0.5, -0.0458, + ); + let conversion_bias = vec2(0.5, 0.5); + return clamp(conversion_weights * color + conversion_bias, vec2(0.0, 0.0), vec2(1.0, 1.0)); +} diff --git a/tests/tests/wgpu-gpu/planar_texture/planar_texture.wgsl b/tests/tests/wgpu-gpu/planar_texture/planar_texture_sampling.wgsl similarity index 100% rename from tests/tests/wgpu-gpu/planar_texture/planar_texture.wgsl rename to tests/tests/wgpu-gpu/planar_texture/planar_texture_sampling.wgsl diff --git a/tests/tests/wgpu-validation/api/texture.rs b/tests/tests/wgpu-validation/api/texture.rs index 01c88c7bdf7..f3104a186f5 100644 --- a/tests/tests/wgpu-validation/api/texture.rs +++ b/tests/tests/wgpu-validation/api/texture.rs @@ -299,6 +299,90 @@ fn planar_texture_bad_size() { } } +/// Ensures that creating a planar textures that support `RENDER_ATTACHMENT` usage +/// is possible. +#[test] +fn planar_texture_render_attachment() { + let required_features = wgpu::Features::TEXTURE_FORMAT_NV12; + let device_desc = wgpu::DeviceDescriptor { + required_features, + ..Default::default() + }; + let (device, _queue) = wgpu::Device::noop(&device_desc); + let size = wgpu::Extent3d { + width: 256, + height: 256, + depth_or_array_layers: 1, + }; + + for (tex_format, view_format, view_aspect) in [ + ( + wgpu::TextureFormat::NV12, + wgpu::TextureFormat::R8Unorm, + wgpu::TextureAspect::Plane0, + ), + ( + wgpu::TextureFormat::NV12, + wgpu::TextureFormat::Rg8Unorm, + wgpu::TextureAspect::Plane1, + ), + ] { + valid(&device, || { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: None, + dimension: wgpu::TextureDimension::D2, + size, + format: tex_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + mip_level_count: 1, + sample_count: 1, + view_formats: &[], + }); + + let _ = texture.create_view(&wgpu::TextureViewDescriptor { + format: Some(view_format), + aspect: view_aspect, + ..Default::default() + }); + }); + } +} + +/// Ensures that creating a planar textures with `RENDER_ATTACHMENT` +/// for non renderable planar formats fails validation. +#[test] +fn planar_texture_render_attachment_unsupported() { + let required_features = + wgpu::Features::TEXTURE_FORMAT_P010 | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM; + let device_desc = wgpu::DeviceDescriptor { + required_features, + ..Default::default() + }; + let (device, _queue) = wgpu::Device::noop(&device_desc); + let size = wgpu::Extent3d { + width: 256, + height: 256, + depth_or_array_layers: 1, + }; + + fail( + &device, + || { + let _ = device.create_texture(&wgpu::TextureDescriptor { + label: None, + dimension: wgpu::TextureDimension::D2, + size, + format: wgpu::TextureFormat::P010, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + mip_level_count: 1, + sample_count: 1, + view_formats: &[], + }); + }, + Some("Texture usages TextureUsages(RENDER_ATTACHMENT) are not allowed on a texture of type P010"), + ); +} + /// Creates a texture and a buffer, and encodes a copy from the texture to the /// buffer. fn encode_copy_texture_to_buffer( diff --git a/wgpu-core/src/command/render.rs b/wgpu-core/src/command/render.rs index bf19edf7921..66062c7ad58 100644 --- a/wgpu-core/src/command/render.rs +++ b/wgpu-core/src/command/render.rs @@ -1193,11 +1193,12 @@ impl RenderPassInfo { }, )?; - if !color_view - .desc - .aspects() - .contains(hal::FormatAspects::COLOR) - { + if !color_view.desc.aspects().intersects( + hal::FormatAspects::COLOR + | hal::FormatAspects::PLANE_0 + | hal::FormatAspects::PLANE_1 + | hal::FormatAspects::PLANE_2, + ) { return Err(RenderPassErrorInner::ColorAttachment( ColorAttachmentError::InvalidFormat(color_view.desc.format), )); diff --git a/wgpu-core/src/conv.rs b/wgpu-core/src/conv.rs index 3aca805659c..62edf149ff5 100644 --- a/wgpu-core/src/conv.rs +++ b/wgpu-core/src/conv.rs @@ -111,7 +111,12 @@ pub fn map_texture_usage( flags.contains(wgt::TextureFormatFeatureFlags::STORAGE_READ_WRITE), ); } - let is_color = aspect.contains(hal::FormatAspects::COLOR); + let is_color = aspect.intersects( + hal::FormatAspects::COLOR + | hal::FormatAspects::PLANE_0 + | hal::FormatAspects::PLANE_1 + | hal::FormatAspects::PLANE_2, + ); u.set( wgt::TextureUses::COLOR_TARGET, usage.contains(wgt::TextureUsages::RENDER_ATTACHMENT) && is_color, diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index 7d5898ecc2d..4c9060d1cd9 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -1327,7 +1327,24 @@ impl Device { }); } - let missing_allowed_usages = desc.usage - format_features.allowed_usages; + let missing_allowed_usages = match desc.format.planes() { + Some(planes) => { + let mut planes_usages = wgt::TextureUsages::all(); + for plane in 0..planes { + let aspect = wgt::TextureAspect::from_plane(plane).unwrap(); + let format = desc.format.aspect_specific_format(aspect).unwrap(); + let format_features = self + .describe_format_features(format) + .map_err(|error| CreateTextureError::MissingFeatures(desc.format, error))?; + + planes_usages &= format_features.allowed_usages; + } + + desc.usage - planes_usages + } + None => desc.usage - format_features.allowed_usages, + }; + if !missing_allowed_usages.is_empty() { // detect downlevel incompatibilities let wgpu_allowed_usages = desc @@ -1692,13 +1709,15 @@ impl Device { )); } - if aspects != hal::FormatAspects::from(texture.desc.format) { + if !texture.desc.format.is_multi_planar_format() + && aspects != hal::FormatAspects::from(texture.desc.format) + { break 'error Err(TextureViewNotRenderableReason::Aspects(aspects)); } Ok(texture .desc - .compute_render_extent(desc.range.base_mip_level)) + .compute_render_extent(desc.range.base_mip_level, desc.range.aspect.to_plane())) }; // filter the usages based on the other criteria diff --git a/wgpu-hal/src/vulkan/device.rs b/wgpu-hal/src/vulkan/device.rs index 1297e57f09b..74ebe002ee4 100644 --- a/wgpu-hal/src/vulkan/device.rs +++ b/wgpu-hal/src/vulkan/device.rs @@ -706,7 +706,8 @@ impl super::Device { } } if desc.format.is_multi_planar_format() { - raw_flags |= vk::ImageCreateFlags::MUTABLE_FORMAT; + raw_flags |= + vk::ImageCreateFlags::MUTABLE_FORMAT | vk::ImageCreateFlags::EXTENDED_USAGE; } let mut vk_info = vk::ImageCreateInfo::default() diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 7674c0a95d8..ade5a59670d 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -2762,6 +2762,17 @@ impl TextureAspect { _ => return None, }) } + + /// Returns the plane for a given texture aspect. + #[must_use] + pub fn to_plane(&self) -> Option { + match self { + TextureAspect::Plane0 => Some(0), + TextureAspect::Plane1 => Some(1), + TextureAspect::Plane2 => Some(2), + _ => None, + } + } } // There are some additional texture format helpers in `wgpu-core/src/conv.rs`, @@ -6372,10 +6383,19 @@ impl TextureDescriptor { /// /// #[must_use] - pub fn compute_render_extent(&self, mip_level: u32) -> Extent3d { + pub fn compute_render_extent(&self, mip_level: u32, plane: Option) -> Extent3d { + let width = self.size.width >> mip_level; + let height = self.size.height >> mip_level; + + let (width, height) = match (self.format, plane) { + (TextureFormat::NV12 | TextureFormat::P010, Some(0)) => (width, height), + (TextureFormat::NV12 | TextureFormat::P010, Some(1)) => (width / 2, height / 2), + _ => (width, height), + }; + Extent3d { - width: u32::max(1, self.size.width >> mip_level), - height: u32::max(1, self.size.height >> mip_level), + width, + height, depth_or_array_layers: 1, } }