Skip to content

Commit 17d977a

Browse files
committed
fix(core): Track initialization status of 3D textures
(cherry picked from commit 0cc48c8)
1 parent 5815bbe commit 17d977a

3 files changed

Lines changed: 311 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ By @beholdnec in [#8505](https://github.com/gfx-rs/wgpu/pull/8505).
211211
- Fix missing dependency feature activations when building wgpu-hal with gles/dx12 in isolation. By @wumpf in [#9325](https://github.com/gfx-rs/wgpu/pull/9325)
212212
- Stencil clear and reference values are now truncated to 8 bits. By @beicause in [#9607](https://github.com/gfx-rs/wgpu/pull/9607).
213213
- Fixed missing initialization of other aspects when writing to a single aspect of a multi-aspect texture. By @andyleiserson in [#9626](https://github.com/gfx-rs/wgpu/pull/9626).
214+
- Fixed incorrect initialization tracking for 3D textures. By @andyleiserson in [#9765](https://github.com/gfx-rs/wgpu/pull/9765).
214215

215216
#### naga
216217

tests/tests/wgpu-gpu/zero_init.rs

Lines changed: 303 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,41 @@ use wgpu_test::{
1212
TestParameters, TestingContext,
1313
};
1414

15+
/// A way to write data into a texture.
16+
#[derive(Clone, Copy)]
17+
#[allow(clippy::enum_variant_names)]
18+
enum WriteMethod {
19+
WriteTexture,
20+
CopyBufferToTexture,
21+
CopyTextureToTexture,
22+
}
23+
24+
impl WriteMethod {
25+
fn name(self) -> &'static str {
26+
match self {
27+
WriteMethod::WriteTexture => "write_texture",
28+
WriteMethod::CopyBufferToTexture => "copy_buffer_to_texture",
29+
WriteMethod::CopyTextureToTexture => "copy_texture_to_texture",
30+
}
31+
}
32+
}
33+
34+
/// A way to read data out of a texture.
35+
#[derive(Clone, Copy)]
36+
enum ReadMethod {
37+
CopyTextureToBuffer,
38+
CopyTextureToTexture,
39+
}
40+
41+
impl ReadMethod {
42+
fn name(self) -> &'static str {
43+
match self {
44+
ReadMethod::CopyTextureToBuffer => "copy_texture_to_buffer",
45+
ReadMethod::CopyTextureToTexture => "copy_texture_to_texture",
46+
}
47+
}
48+
}
49+
1550
pub fn all_tests(vec: &mut Vec<GpuTestInitializer>) {
1651
vec.extend([
1752
COPY_BUFFER_TO_TEXTURE_PLANE0_LEAVES_PLANE1_UNINIT_NV12,
@@ -27,6 +62,10 @@ pub fn all_tests(vec: &mut Vec<GpuTestInitializer>) {
2762
WRITE_TEXTURE_STENCIL_LEAVES_DEPTH_UNINIT_DEPTH24PLUS_STENCIL8,
2863
WRITE_TEXTURE_STENCIL_LEAVES_DEPTH_UNINIT_DEPTH32FLOAT_STENCIL8,
2964
DYNAMIC_OFFSET_BUFFER_BINDING_INIT,
65+
COPY_TEXTURE_TO_BUFFER_3D_SOURCE_ORIGIN_Z_UNINIT,
66+
COPY_TEXTURE_TO_TEXTURE_3D_SOURCE_ORIGIN_Z_UNINIT,
67+
COPY_BUFFER_TO_TEXTURE_3D_DEST_ORIGIN_Z_PARTIAL,
68+
COPY_TEXTURE_TO_TEXTURE_3D_DEST_ORIGIN_Z_PARTIAL,
3069
]);
3170
}
3271

@@ -574,21 +613,6 @@ struct AspectInfo {
574613
bpp: u32,
575614
}
576615

577-
#[derive(Clone, Copy)]
578-
enum WriteMethod {
579-
WriteTexture,
580-
CopyBufferToTexture,
581-
}
582-
583-
impl WriteMethod {
584-
fn name(self) -> &'static str {
585-
match self {
586-
WriteMethod::WriteTexture => "write_texture",
587-
WriteMethod::CopyBufferToTexture => "copy_buffer_to_texture",
588-
}
589-
}
590-
}
591-
592616
async fn check_depth_stencil_write_leaves_other_uninit(
593617
ctx: &TestingContext,
594618
format: TextureFormat,
@@ -746,6 +770,9 @@ async fn check_write_aspect_leaves_other_uninit(
746770
);
747771
ctx.queue.submit(Some(encoder.finish()));
748772
}
773+
WriteMethod::CopyTextureToTexture => {
774+
unreachable!("aspect-init tests do not use copy_texture_to_texture")
775+
}
749776
}
750777

751778
let read_bytes_per_row = read.size.width * read.bpp;
@@ -947,3 +974,264 @@ static DYNAMIC_OFFSET_BUFFER_BINDING_INIT: GpuTestConfiguration = GpuTestConfigu
947974
data[nonzero.unwrap()],
948975
);
949976
});
977+
978+
// Tests of initialization of 3D textures.
979+
//
980+
// Init tracking only operates on array layers, not on depth/volume slices
981+
// of 3D textures. Therefore,
982+
983+
const D3_WIDTH: u32 = 256;
984+
const D3_HEIGHT: u32 = 2;
985+
const D3_DEPTH: u32 = 4;
986+
987+
// A read from a fresh 3D texture as a copy *source* at `origin.z >= 1` must
988+
// trigger initialization of the full texture.
989+
#[gpu_test]
990+
static COPY_TEXTURE_TO_BUFFER_3D_SOURCE_ORIGIN_Z_UNINIT: GpuTestConfiguration =
991+
GpuTestConfiguration::new()
992+
.parameters(TestParameters::default().limits(Limits::downlevel_defaults()))
993+
.run_async(|ctx| async move {
994+
check_3d_copy_source_init(&ctx, ReadMethod::CopyTextureToBuffer).await;
995+
});
996+
997+
#[gpu_test]
998+
static COPY_TEXTURE_TO_TEXTURE_3D_SOURCE_ORIGIN_Z_UNINIT: GpuTestConfiguration =
999+
GpuTestConfiguration::new()
1000+
.parameters(TestParameters::default().limits(Limits::downlevel_defaults()))
1001+
.run_async(|ctx| async move {
1002+
check_3d_copy_source_init(&ctx, ReadMethod::CopyTextureToTexture).await;
1003+
});
1004+
1005+
// The first depth slice must be initialized to zero before a partial copy into a fresh 3D
1006+
// texture with destination `origin.z >= 1`.
1007+
#[gpu_test]
1008+
static COPY_BUFFER_TO_TEXTURE_3D_DEST_ORIGIN_Z_PARTIAL: GpuTestConfiguration =
1009+
GpuTestConfiguration::new()
1010+
.parameters(TestParameters::default().limits(Limits::downlevel_defaults()))
1011+
.run_async(|ctx| async move {
1012+
check_3d_copy_dest_init(&ctx, WriteMethod::CopyBufferToTexture).await;
1013+
});
1014+
1015+
#[gpu_test]
1016+
static COPY_TEXTURE_TO_TEXTURE_3D_DEST_ORIGIN_Z_PARTIAL: GpuTestConfiguration =
1017+
GpuTestConfiguration::new()
1018+
.parameters(TestParameters::default().limits(Limits::downlevel_defaults()))
1019+
.run_async(|ctx| async move {
1020+
check_3d_copy_dest_init(&ctx, WriteMethod::CopyTextureToTexture).await;
1021+
});
1022+
1023+
fn create_3d_texture(ctx: &TestingContext, label: &str, depth: u32) -> Texture {
1024+
ctx.device.create_texture(&TextureDescriptor {
1025+
label: Some(label),
1026+
size: Extent3d {
1027+
width: D3_WIDTH,
1028+
height: D3_HEIGHT,
1029+
depth_or_array_layers: depth,
1030+
},
1031+
mip_level_count: 1,
1032+
sample_count: 1,
1033+
dimension: TextureDimension::D3,
1034+
format: TextureFormat::R8Uint,
1035+
usage: TextureUsages::COPY_SRC | TextureUsages::COPY_DST,
1036+
view_formats: &[],
1037+
})
1038+
}
1039+
1040+
fn d3_buffer_layout() -> TexelCopyBufferLayout {
1041+
TexelCopyBufferLayout {
1042+
offset: 0,
1043+
bytes_per_row: Some(D3_WIDTH),
1044+
rows_per_image: Some(D3_HEIGHT),
1045+
}
1046+
}
1047+
1048+
async fn map_and_read(ctx: &TestingContext, buffer: &Buffer) -> Vec<u8> {
1049+
let slice = buffer.slice(..);
1050+
slice.map_async(MapMode::Read, |_| ());
1051+
ctx.async_poll(PollType::wait_indefinitely()).await.unwrap();
1052+
slice.get_mapped_range().unwrap().to_vec()
1053+
}
1054+
1055+
async fn check_3d_copy_source_init(ctx: &TestingContext, method: ReadMethod) {
1056+
const COPY_Z: u32 = 1;
1057+
let copy_depth = D3_DEPTH - COPY_Z;
1058+
let copy_size = Extent3d {
1059+
width: D3_WIDTH,
1060+
height: D3_HEIGHT,
1061+
depth_or_array_layers: copy_depth,
1062+
};
1063+
1064+
let src = create_3d_texture(ctx, "3d source init test", D3_DEPTH);
1065+
let src_info = TexelCopyTextureInfo {
1066+
texture: &src,
1067+
mip_level: 0,
1068+
origin: Origin3d {
1069+
x: 0,
1070+
y: 0,
1071+
z: COPY_Z,
1072+
},
1073+
aspect: TextureAspect::All,
1074+
};
1075+
1076+
let mut encoder = ctx
1077+
.device
1078+
.create_command_encoder(&CommandEncoderDescriptor { label: None });
1079+
match method {
1080+
ReadMethod::CopyTextureToBuffer => {
1081+
// The copy source is partial (it starts at origin.z = COPY_Z), so the
1082+
// readback can't go through `ReadbackBuffers`, which always copies the
1083+
// full texture from the origin.
1084+
let readback = ctx.device.create_buffer(&BufferDescriptor {
1085+
label: Some("3d source readback"),
1086+
size: (D3_WIDTH * D3_HEIGHT * copy_depth) as u64,
1087+
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
1088+
mapped_at_creation: false,
1089+
});
1090+
encoder.copy_texture_to_buffer(
1091+
src_info,
1092+
TexelCopyBufferInfo {
1093+
buffer: &readback,
1094+
layout: d3_buffer_layout(),
1095+
},
1096+
copy_size,
1097+
);
1098+
ctx.queue.submit(Some(encoder.finish()));
1099+
1100+
let data = map_and_read(ctx, &readback).await;
1101+
let nonzero = data.iter().position(|&b| b != 0);
1102+
assert!(
1103+
nonzero.is_none(),
1104+
"3D texture used as {} source at origin.z={} read back non-zero from \
1105+
never-written memory; first non-zero byte at offset {} = 0x{:02x}",
1106+
method.name(),
1107+
COPY_Z,
1108+
nonzero.unwrap(),
1109+
data[nonzero.unwrap()],
1110+
);
1111+
}
1112+
ReadMethod::CopyTextureToTexture => {
1113+
let dst = create_3d_texture(ctx, "3d source init test dst", copy_depth);
1114+
encoder.copy_texture_to_texture(
1115+
src_info,
1116+
TexelCopyTextureInfo {
1117+
texture: &dst,
1118+
mip_level: 0,
1119+
origin: Origin3d::ZERO,
1120+
aspect: TextureAspect::All,
1121+
},
1122+
copy_size,
1123+
);
1124+
// The full `dst` is read back, so route it through `ReadbackBuffers`.
1125+
let readback_buffers = ReadbackBuffers::new(&ctx.device, &dst);
1126+
readback_buffers.copy_from(&ctx.device, &mut encoder, &dst);
1127+
ctx.queue.submit(Some(encoder.finish()));
1128+
1129+
assert!(
1130+
readback_buffers.are_zero(ctx).await,
1131+
"3D texture used as {} source at origin.z={} read back non-zero from \
1132+
never-written memory",
1133+
method.name(),
1134+
COPY_Z,
1135+
);
1136+
}
1137+
}
1138+
}
1139+
1140+
async fn check_3d_copy_dest_init(ctx: &TestingContext, method: WriteMethod) {
1141+
const DST_Z: u32 = 1;
1142+
const SENTINEL: u8 = 0xAA;
1143+
let slice_bytes = (D3_WIDTH * D3_HEIGHT) as usize;
1144+
let one_slice = Extent3d {
1145+
width: D3_WIDTH,
1146+
height: D3_HEIGHT,
1147+
depth_or_array_layers: 1,
1148+
};
1149+
1150+
let dst = create_3d_texture(ctx, "3d dest init test", D3_DEPTH);
1151+
let dst_info = TexelCopyTextureInfo {
1152+
texture: &dst,
1153+
mip_level: 0,
1154+
origin: Origin3d {
1155+
x: 0,
1156+
y: 0,
1157+
z: DST_Z,
1158+
},
1159+
aspect: TextureAspect::All,
1160+
};
1161+
1162+
let mut encoder = ctx
1163+
.device
1164+
.create_command_encoder(&CommandEncoderDescriptor { label: None });
1165+
match method {
1166+
WriteMethod::CopyBufferToTexture => {
1167+
let src_buffer = ctx.device.create_buffer(&BufferDescriptor {
1168+
label: Some("3d dest init source"),
1169+
size: slice_bytes as u64,
1170+
usage: BufferUsages::COPY_SRC,
1171+
mapped_at_creation: true,
1172+
});
1173+
{
1174+
let mut view = src_buffer.slice(..).get_mapped_range_mut().unwrap();
1175+
view.copy_from_slice(&vec![SENTINEL; slice_bytes]);
1176+
}
1177+
src_buffer.unmap();
1178+
encoder.copy_buffer_to_texture(
1179+
TexelCopyBufferInfo {
1180+
buffer: &src_buffer,
1181+
layout: d3_buffer_layout(),
1182+
},
1183+
dst_info,
1184+
one_slice,
1185+
);
1186+
}
1187+
WriteMethod::CopyTextureToTexture => {
1188+
// Initialize the source, then copy a single slice into the destination at z=1.
1189+
let src_tex = create_3d_texture(ctx, "3d dest init source texture", 1);
1190+
ctx.queue.write_texture(
1191+
TexelCopyTextureInfo {
1192+
texture: &src_tex,
1193+
mip_level: 0,
1194+
origin: Origin3d::ZERO,
1195+
aspect: TextureAspect::All,
1196+
},
1197+
&vec![SENTINEL; slice_bytes],
1198+
d3_buffer_layout(),
1199+
one_slice,
1200+
);
1201+
ctx.queue.submit(None);
1202+
encoder.copy_texture_to_texture(
1203+
TexelCopyTextureInfo {
1204+
texture: &src_tex,
1205+
mip_level: 0,
1206+
origin: Origin3d::ZERO,
1207+
aspect: TextureAspect::All,
1208+
},
1209+
dst_info,
1210+
one_slice,
1211+
);
1212+
}
1213+
WriteMethod::WriteTexture => {
1214+
unreachable!("3D dest-init tests exercise only the encoder copy commands")
1215+
}
1216+
}
1217+
// Submit the partial copy on its own to ensure the init action is applied on its own,
1218+
// and not in combination with an init action for the readback.
1219+
ctx.queue.submit(Some(encoder.finish()));
1220+
1221+
// The whole texture is read back from the origin, so route it through
1222+
// `ReadbackBuffers`. The written slice (z = DST_Z) must keep its sentinel data
1223+
// and every untouched slice must be zero-initialized.
1224+
let readback_buffers = ReadbackBuffers::new(&ctx.device, &dst);
1225+
let mut encoder = ctx
1226+
.device
1227+
.create_command_encoder(&CommandEncoderDescriptor { label: None });
1228+
readback_buffers.copy_from(&ctx.device, &mut encoder, &dst);
1229+
ctx.queue.submit(Some(encoder.finish()));
1230+
1231+
let mut expected = vec![0u8; slice_bytes * D3_DEPTH as usize];
1232+
let written_start = DST_Z as usize * slice_bytes;
1233+
expected[written_start..written_start + slice_bytes].fill(SENTINEL);
1234+
readback_buffers
1235+
.assert_buffer_contents(ctx, &expected)
1236+
.await;
1237+
}

wgpu-core/src/command/transfer.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,12 +654,17 @@ fn handle_texture_init(
654654
copy_size: &Extent3d,
655655
texture: &Arc<Texture>,
656656
) -> Result<(), ClearError> {
657+
let init_layer_range = if texture.desc.dimension == wgt::TextureDimension::D3 {
658+
// Init tracking only considers array layers, not depth/volume slices
659+
0..1
660+
} else {
661+
copy_texture.origin.z..copy_texture.origin.z + copy_size.depth_or_array_layers
662+
};
657663
let init_action = TextureInitTrackerAction {
658664
texture: texture.clone(),
659665
range: TextureInitRange {
660666
mip_range: copy_texture.mip_level..copy_texture.mip_level + 1,
661-
layer_range: copy_texture.origin.z
662-
..(copy_texture.origin.z + copy_size.depth_or_array_layers),
667+
layer_range: init_layer_range,
663668
},
664669
kind: init_kind,
665670
};

0 commit comments

Comments
 (0)