[d3d12,metal,gl] add support for rendering to slices of 3D textures

This commit is contained in:
teoxoy 2025-04-22 21:25:27 +02:00 committed by Teodor Tanasoaia
parent 15477b84a9
commit d714e3d95a
11 changed files with 379 additions and 38 deletions

View File

@ -2,7 +2,7 @@ use wgpu::{
util::{BufferInitDescriptor, DeviceExt},
vertex_attr_array,
};
use wgpu_test::{gpu_test, GpuTestConfiguration, TestParameters, TestingContext};
use wgpu_test::{gpu_test, FailureCase, GpuTestConfiguration, TestParameters, TestingContext};
#[gpu_test]
static DRAW_TO_2D_VIEW: GpuTestConfiguration = GpuTestConfiguration::new()
@ -233,3 +233,188 @@ async fn run_test(
let succeeded = data.iter().all(|b| *b == u8::MAX);
assert!(succeeded);
}
#[gpu_test]
static DRAW_TO_3D_VIEW: GpuTestConfiguration = GpuTestConfiguration::new()
.parameters(
TestParameters::default()
.limits(wgpu::Limits {
max_texture_dimension_3d: 512,
..wgpu::Limits::downlevel_webgl2_defaults()
})
.skip(FailureCase::backend(wgpu::Backends::VULKAN)),
)
.run_async(run_test_3d);
async fn run_test_3d(ctx: TestingContext) {
let vertex_buffer_content: &[f32; 12] = &[
// Triangle 1
-1.0, -1.0, // Bottom left
1.0, 1.0, // Top right
-1.0, 1.0, // Top left
// Triangle 2
-1.0, -1.0, // Bottom left
1.0, -1.0, // Bottom right
1.0, 1.0, // Top right
];
let vertex_buffer = ctx.device.create_buffer_init(&BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(vertex_buffer_content),
usage: wgpu::BufferUsages::VERTEX,
});
let shader_src = "
@vertex
fn vs_main(@location(0) position: vec2f) -> @builtin(position) vec4f {
return vec4f(position, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4f {
return vec4f(1.0);
}
";
let shader = ctx
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(shader_src.into()),
});
let pipeline_desc = wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
buffers: &[wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &vertex_attr_array![0 => Float32x2],
}],
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::R8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
cache: None,
};
let pipeline = ctx.device.create_render_pipeline(&pipeline_desc);
const SIZE: u32 = 512;
const DEPTH: u32 = 2;
const MIPS: u32 = 2;
const fn size_for_mips(mips: u32) -> u64 {
let mut out: u64 = 0;
let mut mip = 0;
while mip < mips {
let size = SIZE as u64 >> mip;
let z = DEPTH as u64 >> mip;
out += size * size * z;
mip += 1;
}
out
}
let out_texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: DEPTH,
},
mip_level_count: MIPS,
sample_count: 1,
dimension: wgpu::TextureDimension::D3,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let readback_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: size_for_mips(MIPS),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
for mip in 0..MIPS {
let out_texture_view = out_texture.create_view(&wgpu::TextureViewDescriptor {
base_mip_level: mip,
mip_level_count: Some(1),
..Default::default()
});
for layer in 0..DEPTH >> mip {
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &out_texture_view,
depth_slice: Some(layer),
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&pipeline);
rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
rpass.draw(0..6, 0..1);
}
}
for mip in 0..MIPS {
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &out_texture,
mip_level: mip,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: size_for_mips(mip),
bytes_per_row: Some(SIZE >> mip),
rows_per_image: Some(SIZE >> mip),
},
},
wgpu::Extent3d {
width: SIZE >> mip,
height: SIZE >> mip,
depth_or_array_layers: DEPTH >> mip,
},
);
}
ctx.queue.submit([encoder.finish()]);
let slice = readback_buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| ());
ctx.async_poll(wgpu::PollType::wait()).await.unwrap();
let data = slice.get_mapped_range();
let succeeded = data.iter().all(|b| *b == u8::MAX);
assert!(succeeded);
}

View File

@ -14,7 +14,7 @@ use crate::command::{
};
use crate::init_tracker::BufferInitTrackerAction;
use crate::pipeline::{RenderPipeline, VertexStep};
use crate::resource::InvalidResourceError;
use crate::resource::{InvalidResourceError, ResourceErrorIdent};
use crate::snatch::SnatchGuard;
use crate::{
api_log,
@ -621,6 +621,18 @@ pub enum ColorAttachmentError {
TooMany { given: usize, limit: usize },
#[error("The total number of bytes per sample in color attachments {total} exceeds the limit {limit}")]
TooManyBytesPerSample { total: u32, limit: u32 },
#[error("Depth slice must be less than {limit} but is {given}")]
DepthSliceLimit { given: u32, limit: u32 },
#[error("Color attachment's view is 3D and requires depth slice to be provided")]
MissingDepthSlice,
#[error("Depth slice was provided but the color attachment's view is not 3D")]
UnneededDepthSlice,
#[error("{view}'s subresource at mip {mip_level} and depth/array layer {depth_or_array_layer} is already attached to this render pass")]
SubresourceOverlap {
view: ResourceErrorIdent,
mip_level: u32,
depth_or_array_layer: u32,
},
}
#[derive(Clone, Debug, Error)]
@ -1096,6 +1108,8 @@ impl<'d> RenderPassInfo<'d> {
});
}
let mut attachment_set = crate::FastHashSet::default();
let mut color_attachments_hal =
ArrayVec::<Option<hal::ColorAttachment<_>>, { hal::MAX_COLOR_ATTACHMENTS }>::new();
for (index, attachment) in color_attachments.iter().enumerate() {
@ -1126,6 +1140,71 @@ impl<'d> RenderPassInfo<'d> {
));
}
if color_view.desc.dimension == TextureViewDimension::D3 {
if let Some(depth_slice) = at.depth_slice {
let mip = color_view.desc.range.base_mip_level;
let mip_size = color_view
.parent
.desc
.size
.mip_level_size(mip, color_view.parent.desc.dimension);
let limit = mip_size.depth_or_array_layers;
if depth_slice >= limit {
return Err(RenderPassErrorInner::ColorAttachment(
ColorAttachmentError::DepthSliceLimit {
given: depth_slice,
limit,
},
));
}
} else {
return Err(RenderPassErrorInner::ColorAttachment(
ColorAttachmentError::MissingDepthSlice,
));
}
} else if at.depth_slice.is_some() {
return Err(RenderPassErrorInner::ColorAttachment(
ColorAttachmentError::UnneededDepthSlice,
));
}
fn check_attachment_overlap(
attachment_set: &mut crate::FastHashSet<(crate::track::TrackerIndex, u32, u32)>,
view: &TextureView,
depth_slice: Option<u32>,
) -> Result<(), ColorAttachmentError> {
let mut insert = |slice| {
let mip_level = view.desc.range.base_mip_level;
if attachment_set.insert((view.tracking_data.tracker_index(), mip_level, slice))
{
Ok(())
} else {
Err(ColorAttachmentError::SubresourceOverlap {
view: view.error_ident(),
mip_level,
depth_or_array_layer: slice,
})
}
};
match view.desc.dimension {
TextureViewDimension::D2 => {
insert(view.desc.range.base_array_layer)?;
}
TextureViewDimension::D2Array => {
for layer in view.selector.layers.clone() {
insert(layer)?;
}
}
TextureViewDimension::D3 => {
insert(depth_slice.unwrap())?;
}
_ => unreachable!(),
};
Ok(())
}
check_attachment_overlap(&mut attachment_set, color_view, at.depth_slice)?;
Self::add_pass_texture_init_actions(
at.load_op,
at.store_op,
@ -1141,6 +1220,8 @@ impl<'d> RenderPassInfo<'d> {
resolve_view.same_device(device)?;
check_multiview(resolve_view)?;
check_attachment_overlap(&mut attachment_set, resolve_view, None)?;
let resolve_location = AttachmentErrorLocation::Color {
index,
resolve: true,

View File

@ -929,8 +929,11 @@ impl Device {
desc.format,
));
}
// Renderable textures can only be 2D
if desc.usage.contains(wgt::TextureUsages::RENDER_ATTACHMENT) {
// Renderable textures can only be 2D on Vulkan (until we implement 3D support)
if self.backend() == wgt::Backend::Vulkan
&& desc.usage.contains(wgt::TextureUsages::RENDER_ATTACHMENT)
{
return Err(CreateTextureError::InvalidDimensionUsages(
wgt::TextureUsages::RENDER_ATTACHMENT,
desc.dimension,
@ -948,6 +951,14 @@ impl Device {
desc.format,
));
}
// Renderable textures can only be 2D or 3D
if desc.usage.contains(wgt::TextureUsages::RENDER_ATTACHMENT) {
return Err(CreateTextureError::InvalidDimensionUsages(
wgt::TextureUsages::RENDER_ATTACHMENT,
desc.dimension,
));
}
}
if desc.format.is_compressed() {
@ -1124,17 +1135,13 @@ impl Device {
let clear_mode = if hal_usage
.intersects(wgt::TextureUses::DEPTH_STENCIL_WRITE | wgt::TextureUses::COLOR_TARGET)
&& desc.dimension == wgt::TextureDimension::D2
{
let (is_color, usage) = if desc.format.is_depth_stencil_format() {
(false, wgt::TextureUses::DEPTH_STENCIL_WRITE)
} else {
(true, wgt::TextureUses::COLOR_TARGET)
};
let dimension = match desc.dimension {
wgt::TextureDimension::D1 => TextureViewDimension::D1,
wgt::TextureDimension::D2 => TextureViewDimension::D2,
wgt::TextureDimension::D3 => unreachable!(),
};
let clear_label = hal_label(
Some("(wgpu internal) clear texture view"),
@ -1149,7 +1156,7 @@ impl Device {
let desc = hal::TextureViewDescriptor {
label: clear_label,
format: $format,
dimension,
dimension: TextureViewDimension::D2,
usage,
range: wgt::ImageSubresourceRange {
aspect: $aspect,
@ -1420,9 +1427,12 @@ impl Device {
break 'error Err(TextureViewNotRenderableReason::Usage(resolved_usage));
}
if !(resolved_dimension == TextureViewDimension::D2
|| resolved_dimension == TextureViewDimension::D2Array)
{
let allowed_view_dimensions = [
TextureViewDimension::D2,
TextureViewDimension::D2Array,
TextureViewDimension::D3,
];
if !allowed_view_dimensions.contains(&resolved_dimension) {
break 'error Err(TextureViewNotRenderableReason::Dimension(
resolved_dimension,
));

View File

@ -73,6 +73,13 @@ impl Drop for super::CommandEncoder {
fn drop(&mut self) {
use crate::CommandEncoder;
unsafe { self.discard_encoding() }
let mut rtv_pool = self.rtv_pool.lock();
for handle in self.temp_rtv_handles.drain(..) {
rtv_pool.free_handle(handle);
}
drop(rtv_pool);
self.counters.command_encoders.sub(1);
}
}
@ -762,13 +769,39 @@ impl crate::CommandEncoder for super::CommandEncoder {
let mut color_views =
[Direct3D12::D3D12_CPU_DESCRIPTOR_HANDLE { ptr: 0 }; crate::MAX_COLOR_ATTACHMENTS];
let mut rtv_pool = self.rtv_pool.lock();
for (rtv, cat) in color_views.iter_mut().zip(desc.color_attachments.iter()) {
if let Some(cat) = cat.as_ref() {
*rtv = cat.target.view.handle_rtv.unwrap().raw;
if cat.target.view.dimension == wgt::TextureViewDimension::D3 {
let desc = Direct3D12::D3D12_RENDER_TARGET_VIEW_DESC {
Format: cat.target.view.raw_format,
ViewDimension: Direct3D12::D3D12_RTV_DIMENSION_TEXTURE3D,
Anonymous: Direct3D12::D3D12_RENDER_TARGET_VIEW_DESC_0 {
Texture3D: Direct3D12::D3D12_TEX3D_RTV {
MipSlice: cat.target.view.mip_slice,
FirstWSlice: cat.depth_slice.unwrap(),
WSize: 1,
},
},
};
let handle = rtv_pool.alloc_handle()?;
unsafe {
self.device.CreateRenderTargetView(
&cat.target.view.texture,
Some(&desc),
handle.raw,
)
};
*rtv = handle.raw;
self.temp_rtv_handles.push(handle);
} else {
*rtv = cat.target.view.handle_rtv.unwrap().raw;
}
} else {
*rtv = self.null_rtv_handle.raw;
}
}
drop(rtv_pool);
let ds_view = desc.depth_stencil_attachment.as_ref().map(|ds| {
if ds.target.usage == wgt::TextureUses::DEPTH_STENCIL_WRITE {
@ -803,8 +836,11 @@ impl crate::CommandEncoder for super::CommandEncoder {
}
if let Some(ref target) = cat.resolve_target {
self.pass.resolves.push(super::PassResolve {
src: cat.target.view.target_base.clone(),
dst: target.view.target_base.clone(),
src: (
cat.target.view.texture.clone(),
cat.target.view.subresource_index,
),
dst: (target.view.texture.clone(), target.view.subresource_index),
format: target.view.raw_format,
});
}

View File

@ -188,7 +188,7 @@ impl super::Device {
},
features,
shared: Arc::new(shared),
rtv_pool: Mutex::new(rtv_pool),
rtv_pool: Arc::new(Mutex::new(rtv_pool)),
dsv_pool: Mutex::new(descriptor::CpuPool::new(
raw.clone(),
Direct3D12::D3D12_DESCRIPTOR_HEAP_TYPE_DSV,
@ -539,10 +539,14 @@ impl crate::Device for super::Device {
Ok(super::TextureView {
raw_format: view_desc.rtv_dsv_format,
aspects: view_desc.aspects,
target_base: (
texture.resource.clone(),
texture.calc_subresource(desc.range.base_mip_level, desc.range.base_array_layer, 0),
dimension: desc.dimension,
texture: texture.resource.clone(),
subresource_index: texture.calc_subresource(
desc.range.base_mip_level,
desc.range.base_array_layer,
0,
),
mip_slice: desc.range.base_mip_level,
handle_srv: if desc.usage.intersects(wgt::TextureUses::RESOURCE) {
match unsafe { view_desc.to_srv() } {
Some(raw_desc) => {
@ -584,7 +588,10 @@ impl crate::Device for super::Device {
} else {
None
},
handle_rtv: if desc.usage.intersects(wgt::TextureUses::COLOR_TARGET) {
handle_rtv: if desc.usage.intersects(wgt::TextureUses::COLOR_TARGET)
&& desc.dimension != wgt::TextureViewDimension::D3
// 3D RTVs must be created in the render pass
{
let raw_desc = unsafe { view_desc.to_rtv() };
let handle = self.rtv_pool.lock().alloc_handle()?;
unsafe {
@ -725,6 +732,8 @@ impl crate::Device for super::Device {
device: self.raw.clone(),
shared: Arc::clone(&self.shared),
mem_allocator: self.mem_allocator.clone(),
rtv_pool: Arc::clone(&self.rtv_pool),
temp_rtv_handles: Vec::new(),
null_rtv_handle: self.null_rtv_handle,
list: None,
free_lists: Vec::new(),

View File

@ -646,7 +646,7 @@ pub struct Device {
features: wgt::Features,
shared: Arc<DeviceShared>,
// CPU only pools
rtv_pool: Mutex<descriptor::CpuPool>,
rtv_pool: Arc<Mutex<descriptor::CpuPool>>,
dsv_pool: Mutex<descriptor::CpuPool>,
srv_uav_pool: Mutex<descriptor::CpuPool>,
// library
@ -798,6 +798,9 @@ pub struct CommandEncoder {
shared: Arc<DeviceShared>,
mem_allocator: Allocator,
rtv_pool: Arc<Mutex<descriptor::CpuPool>>,
temp_rtv_handles: Vec<descriptor::Handle>,
null_rtv_handle: descriptor::Handle,
list: Option<Direct3D12::ID3D12GraphicsCommandList>,
free_lists: Vec<Direct3D12::ID3D12GraphicsCommandList>,
@ -918,8 +921,10 @@ impl Texture {
pub struct TextureView {
raw_format: Dxgi::Common::DXGI_FORMAT,
aspects: crate::FormatAspects,
/// only used by resolve
target_base: (Direct3D12::ID3D12Resource, u32),
dimension: wgt::TextureViewDimension,
texture: Direct3D12::ID3D12Resource,
subresource_index: u32,
mip_slice: u32,
handle_srv: Option<descriptor::Handle>,
handle_uav: Option<descriptor::Handle>,
handle_rtv: Option<descriptor::Handle>,

View File

@ -250,16 +250,10 @@ impl ViewDescriptor {
PlaneSlice: aspects_to_plane(self.aspects),
}
}
wgt::TextureViewDimension::D3 => {
desc.ViewDimension = Direct3D12::D3D12_RTV_DIMENSION_TEXTURE3D;
desc.Anonymous.Texture3D = Direct3D12::D3D12_TEX3D_RTV {
MipSlice: self.mip_level_base,
FirstWSlice: self.array_layer_base,
WSize: self.array_layer_count,
}
}
wgt::TextureViewDimension::Cube | wgt::TextureViewDimension::CubeArray => {
panic!("Unable to view texture as cube RTV")
wgt::TextureViewDimension::D3
| wgt::TextureViewDimension::Cube
| wgt::TextureViewDimension::CubeArray => {
panic!("Unable to view texture as cube or 3D RTV")
}
}
@ -337,7 +331,7 @@ impl ViewDescriptor {
wgt::TextureViewDimension::D3
| wgt::TextureViewDimension::Cube
| wgt::TextureViewDimension::CubeArray => {
panic!("Unable to view texture as cube or 3D RTV")
panic!("Unable to view texture as cube or 3D DSV")
}
}

View File

@ -557,6 +557,7 @@ impl crate::CommandEncoder for super::CommandEncoder {
self.cmd_buffer.commands.push(C::BindAttachment {
attachment,
view: cat.target.view.clone(),
depth_slice: cat.depth_slice,
});
if let Some(ref rat) = cat.resolve_target {
self.state
@ -578,6 +579,7 @@ impl crate::CommandEncoder for super::CommandEncoder {
self.cmd_buffer.commands.push(C::BindAttachment {
attachment,
view: dsat.target.view.clone(),
depth_slice: None,
});
if aspects.contains(crate::FormatAspects::DEPTH)
&& !dsat.depth_ops.contains(crate::AttachmentOps::STORE)

View File

@ -902,6 +902,7 @@ enum Command {
BindAttachment {
attachment: u32,
view: TextureView,
depth_slice: Option<u32>,
},
ResolveAttachment {
attachment: u32,

View File

@ -98,6 +98,7 @@ impl super::Queue {
fbo_target: u32,
attachment: u32,
view: &super::TextureView,
depth_slice: Option<u32>,
) {
match view.inner {
super::TextureInner::Renderbuffer { raw } => {
@ -126,13 +127,18 @@ impl super::Queue {
)
};
} else if is_layered_target(target) {
let layer = if target == glow::TEXTURE_3D {
depth_slice.unwrap() as i32
} else {
view.array_layers.start as i32
};
unsafe {
gl.framebuffer_texture_layer(
fbo_target,
attachment,
Some(raw),
view.mip_levels.start as i32,
view.array_layers.start as i32,
layer,
)
};
} else {
@ -1096,8 +1102,11 @@ impl super::Queue {
C::BindAttachment {
attachment,
ref view,
depth_slice,
} => {
unsafe { self.set_attachment(gl, glow::DRAW_FRAMEBUFFER, attachment, view) };
unsafe {
self.set_attachment(gl, glow::DRAW_FRAMEBUFFER, attachment, view, depth_slice)
};
}
C::ResolveAttachment {
attachment,
@ -1108,7 +1117,13 @@ impl super::Queue {
unsafe { gl.read_buffer(attachment) };
unsafe { gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(self.copy_fbo)) };
unsafe {
self.set_attachment(gl, glow::DRAW_FRAMEBUFFER, glow::COLOR_ATTACHMENT0, dst)
self.set_attachment(
gl,
glow::DRAW_FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
dst,
None,
)
};
unsafe {
gl.blit_framebuffer(

View File

@ -532,6 +532,9 @@ impl crate::CommandEncoder for super::CommandEncoder {
if let Some(at) = at.as_ref() {
let at_descriptor = descriptor.color_attachments().object_at(i as u64).unwrap();
at_descriptor.set_texture(Some(&at.target.view.raw));
if let Some(depth_slice) = at.depth_slice {
at_descriptor.set_depth_plane(depth_slice as u64);
}
if let Some(ref resolve) = at.resolve_target {
//Note: the selection of levels and slices is already handled by `TextureView`
at_descriptor.set_resolve_texture(Some(&resolve.view.raw));