Make multi-planar textures renderable (#8307)

This commit is contained in:
Mikołaj Radkowski 2025-10-29 18:33:48 +01:00 committed by GitHub
parent 7a2afeb014
commit e7fcb94888
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 358 additions and 15 deletions

View File

@ -84,6 +84,7 @@ SamplerDescriptor {
- Texture now has `from_custom`. By @R-Cramer4 in [#8315](https://github.com/gfx-rs/wgpu/pull/8315).
- Using both the wgpu command encoding APIs and `CommandEncoder::as_hal_mut` on the same encoder will now result in a panic.
- Allow `include_spirv!` and `include_spirv_raw!` macros to be used in constants and statics. By @clarfonthey in [#8250](https://github.com/gfx-rs/wgpu/pull/8250).
- Added support for rendering onto multi-planar textures. By @noituri in [#8307](https://github.com/gfx-rs/wgpu/pull/8307).
### Added/New Features

View File

@ -8,6 +8,7 @@ pub fn all_tests(tests: &mut Vec<GpuTestInitializer>) {
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 {
@ -105,7 +106,112 @@ fn test_planar_texture_creation_sampling(
rpass.set_bind_group(0, &bind_group, &[]);
rpass.draw(0..4, 0..1);
drop(rpass);
ctx.queue.submit(Some(encoder.finish()));
ctx.queue.submit([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([encoder.finish()]);
}
/// Ensures that creation and sampling of an NV12 format texture works as
@ -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),
);
});

View File

@ -0,0 +1,35 @@
struct VertexOutput {
@builtin(position) position: vec4<f32>,
}
const VERTICES: array<vec3<f32>, 3> = array<vec3<f32>, 3>(
vec3<f32>(-0.5, 0.0, 0.0),
vec3<f32>(0.5, 0.0, 0.0),
vec3<f32>(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<f32>(1.0);
let conversion_weights = vec3<f32>(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<f32> {
let color = vec3<f32>(1.0);
let conversion_weights = mat3x2<f32>(
-0.1146, 0.5,
-0.3854, -0.4542,
0.5, -0.0458,
);
let conversion_bias = vec2<f32>(0.5, 0.5);
return clamp(conversion_weights * color + conversion_bias, vec2(0.0, 0.0), vec2(1.0, 1.0));
}

View File

@ -287,6 +287,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(

View File

@ -1207,11 +1207,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),
));

View File

@ -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,

View File

@ -1355,7 +1355,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
@ -1722,13 +1739,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

View File

@ -590,7 +590,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()

View File

@ -134,6 +134,32 @@ fn test_uniqueness_in_texture_format_list() {
assert_eq!(duplicated, vec![]);
}
#[test]
fn test_compute_render_extent() {
for format in TEXTURE_FORMAT_LIST {
let desc = wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 1280,
height: 720,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::empty(),
view_formats: &[],
};
if format.is_multi_planar_format() {
let _ = desc.compute_render_extent(0, Some(0));
} else {
let _ = desc.compute_render_extent(0, None);
}
}
}
pub fn max_texture_format_string_size() -> usize {
TEXTURE_FORMAT_LIST
.into_iter()

View File

@ -2772,6 +2772,17 @@ impl TextureAspect {
_ => return None,
})
}
/// Returns the plane for a given texture aspect.
#[must_use]
pub fn to_plane(&self) -> Option<u32> {
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`,
@ -6416,10 +6427,22 @@ impl<L, V> TextureDescriptor<L, V> {
///
/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-compute-render-extent>
#[must_use]
pub fn compute_render_extent(&self, mip_level: u32) -> Extent3d {
pub fn compute_render_extent(&self, mip_level: u32, plane: Option<u32>) -> 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),
_ => {
debug_assert!(!self.format.is_multi_planar_format());
(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,
}
}