use zenfg::{
AccessRole, BufferDesc, BufferRange, BufferTextureCopyLocation, ClearBufferOp,
ColorAttachmentOps, CompileOptions, CulledNodeReason, DependencyKind, FrameGraph,
FrameGraphError, HazardKind, ImportBufferOptions, ImportTextureOptions, InitialContents,
ReportLevel, RootReason, TextureCopyLocation, TextureDesc, TextureViewDesc, UsagePolicy,
WriteContents,
};
fn full_options() -> CompileOptions {
CompileOptions::full_report()
}
#[test]
fn texture_readback_preserves_only_actual_padding() {
for (width, height, depth, row_stride, image_rows, size, preserve) in [
(2, 2, 1, 8, None, 16, false),
(1, 1, 1, 256, Some(8), 4, false),
(2, 2, 1, 8, Some(8), 16, false),
(1, 2, 1, 256, None, 260, true),
(1, 1, 2, 256, Some(4), 1028, true),
(1, 2, 2, 4, Some(2), 16, false),
] {
for initialized in [false, true] {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc =
TextureDesc::new_2d("source", width, height, wgpu::TextureFormat::Rgba8Unorm);
desc.size.depth_or_array_layers = depth;
let source = frame
.import_texture(desc, ImportTextureOptions::new(InitialContents::Defined))
.unwrap();
let destination = frame
.create_buffer(BufferDesc::new("destination", 4 + size))
.unwrap();
if initialized {
frame
.clear_buffer("initialize", destination, BufferRange::new(4, size))
.unwrap();
}
let mut copy = frame.copy_pass("readback");
copy.copy_texture_to_buffer(
TextureCopyLocation::new(source),
BufferTextureCopyLocation::new(
destination,
wgpu::TexelCopyBufferLayout {
offset: 4,
bytes_per_row: Some(row_stride),
rows_per_image: image_rows,
},
),
wgpu::Extent3d {
width,
height,
depth_or_array_layers: depth,
},
)
.unwrap();
copy.finish().unwrap();
frame
.mark_buffer_root(destination, BufferRange::new(4, size), RootReason::Output)
.unwrap();
let result = frame.compile(full_options());
if preserve && !initialized {
assert_eq!(result.unwrap_err().code(), "FG1002");
continue;
}
let compiled = result.unwrap();
let report = compiled.report().unwrap().full.as_ref().unwrap();
let expected = if preserve {
vec!["initialize", "readback"]
} else {
vec!["readback"]
};
assert_eq!(
report
.nodes
.iter()
.map(|node| node.label.as_str())
.collect::<Vec<_>>(),
expected
);
assert_eq!(report.accesses.last().unwrap().consumes_previous, preserve);
}
}
}
#[test]
fn ranged_roots_resolve_initial_contents_multiple_producers_and_duplicates() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.import_buffer(
BufferDesc::new("history", 32),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
let mut producers = Vec::new();
for offset in [0, 8] {
let mut pass = frame.command_pass("write");
pass.set_side_effect(false);
let _ = pass
.storage_buffer_write(
buffer,
BufferRange::new(offset, 8),
WriteContents::Overwrite,
)
.unwrap();
producers.push(pass.finish().unwrap());
}
for range in [
BufferRange::whole(),
BufferRange::new(0, 32),
BufferRange::new(0, 16),
BufferRange::new(16, 16),
] {
frame
.mark_buffer_root(buffer, range, RootReason::Output)
.unwrap();
}
let compiled = frame.compile(full_options()).unwrap();
let roots = &compiled.report().unwrap().full.as_ref().unwrap().roots;
assert_eq!(roots.len(), 3);
assert_eq!(roots[0].resolution.producer_node_ids, producers);
assert!(roots[0].resolution.uses_initial_contents);
assert_eq!(roots[1].resolution.producer_node_ids, producers);
assert!(!roots[1].resolution.uses_initial_contents);
assert!(roots[2].resolution.producer_node_ids.is_empty());
assert!(roots[2].resolution.uses_initial_contents);
}
#[test]
fn root_ranges_reject_empty_but_empty_accesses_remain_valid() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame.create_buffer(BufferDesc::new("data", 16)).unwrap();
for range in [
BufferRange::new(0, 0),
BufferRange::new(16, 0),
BufferRange::new(8, 9),
] {
assert_eq!(
frame
.mark_buffer_root(buffer, range, RootReason::Output)
.unwrap_err()
.code(),
"FG1103"
);
}
let mut pass = frame.command_pass("empty-read");
let _ = pass
.storage_buffer_read(buffer, BufferRange::new(0, 0))
.unwrap();
pass.finish().unwrap();
frame.compile(full_options()).unwrap();
}
#[test]
fn root_coverage_is_checked_without_reports() {
for options in [CompileOptions::default(), full_options()] {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.create_buffer(BufferDesc::new("undefined", 16))
.unwrap();
frame
.mark_buffer_root(buffer, BufferRange::whole(), RootReason::Output)
.unwrap();
assert_eq!(frame.compile(options).unwrap_err().code(), "FG1004");
}
}
#[test]
fn recording_descriptor_queries_return_snapshotted_and_normalized_metadata() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let texture = frame
.create_texture(TextureDesc {
label: "array".into(),
size: wgpu::Extent3d {
width: 8,
height: 8,
depth_or_array_layers: 6,
},
mip_level_count: 4,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
view_formats: vec![wgpu::TextureFormat::Rgba8UnormSrgb],
usage: UsagePolicy::Infer,
})
.unwrap();
let buffer = frame.create_buffer(BufferDesc::new("data", 64)).unwrap();
let view = frame
.create_texture_view(
texture,
TextureViewDesc {
label: "layers".into(),
format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
dimension: Some(wgpu::TextureViewDimension::D2Array),
base_mip_level: 1,
mip_level_count: None,
base_array_layer: 2,
array_layer_count: Some(3),
..Default::default()
},
)
.unwrap();
assert_eq!(frame.texture_desc(texture).unwrap().label, "array");
assert_eq!(frame.buffer_desc(buffer).unwrap().size, 64);
let normalized = frame.texture_view_desc(view).unwrap();
assert_eq!(normalized.label, "layers");
assert_eq!(normalized.format, wgpu::TextureFormat::Rgba8UnormSrgb);
assert_eq!(normalized.dimension, wgpu::TextureViewDimension::D2Array);
assert_eq!(normalized.base_mip_level, 1);
assert_eq!(normalized.mip_level_count, 3);
assert_eq!(normalized.base_array_layer, 2);
assert_eq!(normalized.array_layer_count, Some(3));
}
#[test]
fn d3_view_descriptor_normalizes_without_array_layers() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let texture = frame
.create_texture(TextureDesc {
label: "volume".into(),
size: wgpu::Extent3d {
width: 8,
height: 4,
depth_or_array_layers: 4,
},
mip_level_count: 3,
sample_count: 1,
dimension: wgpu::TextureDimension::D3,
format: wgpu::TextureFormat::R8Unorm,
view_formats: vec![],
usage: UsagePolicy::Infer,
})
.unwrap();
let view = frame
.create_texture_view(texture, TextureViewDesc::default())
.unwrap();
let normalized = frame.texture_view_desc(view).unwrap();
assert_eq!(normalized.dimension, wgpu::TextureViewDimension::D3);
assert_eq!(normalized.mip_level_count, 3);
assert_eq!(normalized.base_array_layer, 0);
assert_eq!(normalized.array_layer_count, None);
}
#[test]
fn roots_enforce_resource_kind_origin_and_readback_usage() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let transient = frame
.create_buffer(BufferDesc::new("transient", 16))
.unwrap();
let error = frame
.mark_buffer_root(transient, BufferRange::whole(), RootReason::PersistentState)
.unwrap_err();
assert_eq!(error.code(), "FG1107");
let imported = frame
.import_buffer(
BufferDesc {
label: "readback".into(),
size: 16,
usage: UsagePolicy::Fixed(
wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
),
},
ImportBufferOptions {
initial_contents: InitialContents::Defined,
exposed_usage: Some(wgpu::BufferUsages::COPY_DST),
},
)
.unwrap();
assert_eq!(
frame
.mark_readback(imported, BufferRange::whole())
.unwrap_err()
.code(),
"FG1107"
);
let texture = frame
.import_texture(
TextureDesc::new_2d("texture", 1, 1, wgpu::TextureFormat::Rgba8Unorm),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
assert_eq!(
frame
.mark_texture_root(texture, RootReason::Readback)
.unwrap_err()
.code(),
"FG1107"
);
assert_eq!(
frame
.mark_buffer_root(imported, BufferRange::whole(), RootReason::Present)
.unwrap_err()
.code(),
"FG1107"
);
}
#[test]
fn native_texture_role_capabilities_are_deferred_even_when_the_node_would_be_culled() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let texture = frame
.create_texture(TextureDesc::new_2d(
"srgb-storage",
4,
4,
wgpu::TextureFormat::Rgba8UnormSrgb,
))
.unwrap();
let mut pass = frame.compute_pass("unused-invalid-storage");
let _ = pass
.storage_texture_write(texture, WriteContents::Overwrite)
.unwrap();
pass.finish().unwrap();
frame.compile(full_options()).unwrap();
}
#[test]
fn native_texture_sample_count_capabilities_are_deferred_even_when_the_node_would_be_culled() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc = TextureDesc::new_2d("compressed-msaa", 4, 4, wgpu::TextureFormat::Bc1RgbaUnorm);
desc.sample_count = 4;
let texture = frame
.import_texture(desc, ImportTextureOptions::new(InitialContents::Defined))
.unwrap();
let mut pass = frame.compute_pass("unused-invalid-sample-count");
let _ = pass.sampled_texture(texture).unwrap();
pass.finish().unwrap();
frame.compile(full_options()).unwrap();
}
#[test]
fn texture_descriptors_enforce_device_independent_webgpu_structure() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut zero_samples =
TextureDesc::new_2d("invalid-samples", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
zero_samples.sample_count = 0;
assert!(matches!(
frame.create_texture(zero_samples),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
for sample_count in [2, 8] {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc =
TextureDesc::new_2d("native-sample-count", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
desc.sample_count = sample_count;
assert!(frame.create_texture(desc).is_ok());
}
for (label, dimension, size, mip_level_count, sample_count) in [
(
"invalid-1d-height",
wgpu::TextureDimension::D1,
wgpu::Extent3d {
width: 8,
height: 2,
depth_or_array_layers: 1,
},
1,
1,
),
(
"invalid-1d-layers",
wgpu::TextureDimension::D1,
wgpu::Extent3d {
width: 8,
height: 1,
depth_or_array_layers: 2,
},
1,
1,
),
(
"invalid-3d-samples",
wgpu::TextureDimension::D3,
wgpu::Extent3d {
width: 8,
height: 8,
depth_or_array_layers: 8,
},
1,
4,
),
(
"invalid-msaa-mips",
wgpu::TextureDimension::D2,
wgpu::Extent3d {
width: 8,
height: 8,
depth_or_array_layers: 1,
},
2,
4,
),
] {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let desc = TextureDesc {
label: label.into(),
size,
mip_level_count,
sample_count,
dimension,
format: wgpu::TextureFormat::Rgba8Unorm,
view_formats: vec![],
usage: UsagePolicy::Infer,
};
assert!(matches!(
frame.create_texture(desc),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
}
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut multisample_array =
TextureDesc::new_2d("multisample-array", 8, 8, wgpu::TextureFormat::Rgba8Unorm);
multisample_array.sample_count = 4;
multisample_array.size.depth_or_array_layers = 2;
assert!(frame.create_texture(multisample_array).is_ok());
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let depth_1d = TextureDesc {
label: "invalid-1d-depth".into(),
size: wgpu::Extent3d {
width: 8,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Depth32Float,
view_formats: vec![],
usage: UsagePolicy::Infer,
};
assert!(matches!(
frame.create_texture(depth_1d),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
}
#[test]
fn retained_transient_texture_usage_enforces_native_descriptor_constraints() {
{
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let desc = TextureDesc {
label: "one-dimensional-attachment".into(),
size: wgpu::Extent3d {
width: 4,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8Unorm,
view_formats: vec![],
usage: UsagePolicy::Infer,
};
let texture = frame.create_texture(desc).unwrap();
let mut pass = frame.render_pass("invalid-render");
pass.set_side_effect(true);
let _ = pass
.color_attachment(texture, ColorAttachmentOps::clear_store(wgpu::Color::BLACK))
.unwrap();
pass.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
}
{
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc = TextureDesc::new_2d(
"multisampled-storage",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
);
desc.sample_count = 4;
let texture = frame.create_texture(desc).unwrap();
let mut pass = frame.compute_pass("write-storage");
pass.set_side_effect(true);
let _ = pass
.storage_texture_write(texture, WriteContents::Overwrite)
.unwrap();
pass.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
}
{
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let usage =
wgpu::TextureUsages::TRANSIENT_ATTACHMENT | wgpu::TextureUsages::RENDER_ATTACHMENT;
let mut desc = TextureDesc::new_2d(
"invalid-memoryless-color",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
);
desc.usage = UsagePolicy::Fixed(usage);
assert!(matches!(
frame.create_texture(desc),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
let imported_desc =
TextureDesc::new_2d("memoryless-import", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
assert!(matches!(
frame.import_texture(
imported_desc,
ImportTextureOptions {
initial_contents: InitialContents::Defined,
exposed_usage: Some(usage),
},
),
Err(FrameGraphError::InvalidResourceDescriptor { .. })
));
}
}
#[test]
fn native_view_format_compatibility_is_deferred_but_categories_are_preserved() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc = TextureDesc::new_2d("color", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
desc.view_formats = vec![wgpu::TextureFormat::Rgba16Float];
let texture = frame.create_texture(desc).unwrap();
frame
.create_texture_view(
texture,
TextureViewDesc {
format: Some(wgpu::TextureFormat::Rgba16Float),
..Default::default()
},
)
.unwrap();
let mut invalid_desc =
TextureDesc::new_2d("invalid-category", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
invalid_desc.view_formats = vec![wgpu::TextureFormat::Depth32Float];
let error = frame.create_texture(invalid_desc).unwrap_err();
assert!(error.to_string().contains("format category"));
}
#[test]
fn native_texture_copy_format_compatibility_is_deferred() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_texture(
TextureDesc::new_2d("source", 4, 4, wgpu::TextureFormat::Rgba8Unorm),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
let destination = frame
.import_texture(
TextureDesc::new_2d("destination", 4, 4, wgpu::TextureFormat::Bgra8Unorm),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
let mut copy = frame.copy_pass("format-mismatch-copy");
copy.copy_texture_to_texture(
TextureCopyLocation::new(source),
TextureCopyLocation::new(destination),
wgpu::Extent3d {
width: 4,
height: 4,
depth_or_array_layers: 1,
},
)
.unwrap();
copy.finish().unwrap();
frame
.mark_texture_root(destination, RootReason::Output)
.unwrap();
frame.compile(full_options()).unwrap();
}
#[test]
fn read_only_depth_consumes_defined_contents_without_producing_a_value() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let depth = frame
.create_texture(TextureDesc::new_2d(
"depth",
4,
4,
wgpu::TextureFormat::Depth32Float,
))
.unwrap();
let mut writer = frame.render_pass("depth-writer");
let _ = writer
.depth_attachment(depth, zenfg::DepthAttachmentOps::clear_store(1.0))
.unwrap();
writer.finish().unwrap();
let mut reader = frame.render_pass("depth-reader");
reader.set_side_effect(true);
let _ = reader.depth_attachment_read_only(depth).unwrap();
reader.finish().unwrap();
let compiled = frame.compile(full_options()).unwrap();
let full = compiled.report().unwrap().full.as_ref().unwrap();
let reader = full
.nodes
.iter()
.find(|node| node.label == "depth-reader")
.unwrap();
let access = full
.accesses
.iter()
.find(|access| access.pass == reader.id && access.role == AccessRole::DepthAttachment)
.unwrap();
assert_eq!(access.mode, zenfg::AccessMode::Read);
assert!(access.consumes_previous);
assert!(!access.produces_value);
assert!(full.dependencies.iter().any(|dependency| {
dependency.to == reader.id
&& dependency.kind == DependencyKind::Value
&& dependency.hazard == HazardKind::Raw
}));
}
#[test]
fn resolve_target_is_an_overwrite_value_with_render_attachment_usage() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut source_desc = TextureDesc::new_2d("msaa", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
source_desc.sample_count = 4;
let source = frame.create_texture(source_desc).unwrap();
let resolved = frame
.create_texture(TextureDesc::new_2d(
"resolved",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut pass = frame.render_pass("resolve");
let _ = pass
.color_attachment_with_resolve(
source,
resolved,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
)
.unwrap();
pass.finish().unwrap();
frame
.mark_texture_root(resolved, RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(
compiled.resource_usage(source.id()),
Some(zenfg::ResourceUsage::Texture(
wgpu::TextureUsages::RENDER_ATTACHMENT
))
);
assert_eq!(
compiled.resource_usage(source.id()),
compiled.resource_usage(resolved.id())
);
let full = compiled.report().unwrap().full.as_ref().unwrap();
let accesses = full
.accesses
.iter()
.filter(|access| access.role == AccessRole::ColorAttachment)
.collect::<Vec<_>>();
assert_eq!(accesses.len(), 2);
let resolve_access = accesses
.iter()
.find(|access| access.resource == resolved.id())
.unwrap();
assert!(!resolve_access.consumes_previous);
assert!(resolve_access.produces_value);
}
#[test]
fn resolve_source_sample_count_validation_is_deferred_to_native_webgpu() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.create_texture(TextureDesc::new_2d(
"single-sampled-source",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let target = frame
.create_texture(TextureDesc::new_2d(
"target",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut pass = frame.render_pass("invalid-resolve");
let _ = pass
.color_attachment_with_resolve(
source,
target,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
)
.unwrap();
pass.finish().unwrap();
}
#[test]
fn resolve_target_sample_count_validation_is_deferred_to_native_webgpu() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut source_desc = TextureDesc::new_2d("source", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
source_desc.sample_count = 4;
let source = frame.create_texture(source_desc.clone()).unwrap();
source_desc.label = "target".into();
let target = frame.create_texture(source_desc).unwrap();
let mut pass = frame.render_pass("invalid-resolve-target-samples");
let _ = pass
.color_attachment_with_resolve(
source,
target,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
)
.unwrap();
pass.finish().unwrap();
}
#[test]
fn resolve_extent_and_format_validation_is_deferred_to_native_webgpu() {
for target_desc in [
TextureDesc::new_2d("wrong-extent", 2, 4, wgpu::TextureFormat::Rgba8Unorm),
TextureDesc::new_2d("wrong-format", 4, 4, wgpu::TextureFormat::Bgra8Unorm),
] {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut source_desc = TextureDesc::new_2d("source", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
source_desc.sample_count = 4;
let source = frame.create_texture(source_desc).unwrap();
let target = frame.create_texture(target_desc).unwrap();
let mut pass = frame.render_pass("invalid-resolve-compatibility");
let _ = pass
.color_attachment_with_resolve(
source,
target,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
)
.unwrap();
pass.finish().unwrap();
}
}
#[test]
fn resolve_rejects_aliasing_source_and_target() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc = TextureDesc::new_2d("aliased", 4, 4, wgpu::TextureFormat::Rgba8Unorm);
desc.sample_count = 4;
let texture = frame.create_texture(desc).unwrap();
let mut pass = frame.render_pass("aliased-resolve");
let error = pass
.color_attachment_with_resolve(
texture,
texture,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
)
.unwrap_err();
assert_eq!(error.code(), "FG1105");
}
#[test]
fn read_only_depth_requires_defined_contents_even_if_side_effect_rooted() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let depth = frame
.create_texture(TextureDesc::new_2d(
"undefined-depth",
4,
4,
wgpu::TextureFormat::Depth32Float,
))
.unwrap();
let mut pass = frame.render_pass("read-undefined-depth");
pass.set_side_effect(true);
let _ = pass.depth_attachment_read_only(depth).unwrap();
pass.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()).unwrap_err(),
FrameGraphError::ReadBeforeWrite { .. }
));
}
#[test]
fn debug_groups_report_node_and_resource_provenance() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.with_debug_group(" Mesh ", |frame| {
let buffer = frame.create_buffer(BufferDesc::new("buffer", 4))?;
frame.with_debug_group("Visibility", |frame| {
let mut pass = frame.compute_pass("write");
let _ = pass.storage_buffer_write(
buffer,
BufferRange::whole(),
WriteContents::Overwrite,
)?;
pass.finish()?;
Ok(())
})?;
Ok(buffer)
})
.unwrap();
frame
.mark_buffer_root(buffer, BufferRange::whole(), RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
let full = compiled.report().unwrap().full.as_ref().unwrap();
assert_eq!(
full.debug_groups
.iter()
.map(|group| group.label.as_str())
.collect::<Vec<_>>(),
["Mesh", "Visibility"]
);
assert_eq!(full.debug_groups[1].parent, Some(full.debug_groups[0].id));
assert_eq!(full.nodes[0].debug_group, Some(full.debug_groups[1].id));
assert_eq!(full.resources[0].debug_group, Some(full.debug_groups[0].id));
}
#[test]
fn debug_group_stack_errors_are_stable() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
assert_eq!(frame.pop_debug_group().unwrap_err().code(), "FG2004");
assert_eq!(frame.push_debug_group(" ").unwrap_err().code(), "FG2003");
frame.push_debug_group("open").unwrap();
assert_eq!(
frame.compile(CompileOptions::default()).unwrap_err().code(),
"FG2005"
);
}
#[test]
fn same_named_sibling_groups_keep_distinct_ids_and_culled_provenance() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let first = frame.push_debug_group("Repeated").unwrap();
frame.pop_debug_group().unwrap();
let second = frame.push_debug_group("Repeated").unwrap();
let buffer = frame.create_buffer(BufferDesc::new("culled", 4)).unwrap();
let mut pass = frame.compute_pass("culled-write");
let _ = pass
.storage_buffer_write(buffer, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
pass.finish().unwrap();
frame.pop_debug_group().unwrap();
let compiled = frame.compile(full_options()).unwrap();
let full = compiled.report().unwrap().full.as_ref().unwrap();
assert_ne!(first, second);
assert_eq!(full.debug_groups[0].label, full.debug_groups[1].label);
assert_eq!(full.culled_nodes.len(), 1);
assert_eq!(full.culled_nodes[0].debug_group, Some(second));
assert_eq!(
full.culled_nodes[0].reason,
CulledNodeReason::NotReachableFromRoot
);
assert_eq!(full.culled_nodes[0].recording_order, 0);
}
#[test]
fn reports_preserve_original_order_across_retained_and_culled_nodes() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let unused = frame.create_buffer(BufferDesc::new("unused", 4)).unwrap();
let mut first = frame.compute_pass("recorded-first");
let _ = first
.storage_buffer_write(unused, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
first.finish().unwrap();
let imported = frame
.import_buffer(
BufferDesc::new("input", 4),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
let mut second = frame.command_pass("recorded-second");
let _ = second
.storage_buffer_read(imported, BufferRange::whole())
.unwrap();
second.finish_command(|_| Ok(())).unwrap();
let compiled = frame.compile(full_options()).unwrap();
let report = compiled.report().unwrap().full.as_ref().unwrap();
assert_eq!(report.culled_nodes[0].recording_order, 0);
assert_eq!(report.nodes[0].recording_order, 1);
assert_eq!(report.nodes[0].label, "recorded-second");
}
#[test]
fn linear_value_chain_retains_producers_and_infers_usage() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let input = frame.create_buffer(BufferDesc::new("input", 64)).unwrap();
let output = frame.create_buffer(BufferDesc::new("output", 64)).unwrap();
let mut producer = frame.compute_pass("producer");
let _ = producer
.storage_buffer_write(input, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
producer.finish().unwrap();
let mut consumer = frame.compute_pass("consumer");
let _ = consumer
.storage_buffer_read(input, BufferRange::whole())
.unwrap();
let _ = consumer
.storage_buffer_write(output, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
consumer.finish().unwrap();
frame
.mark_buffer_root(output, BufferRange::whole(), RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
let report = compiled.report().unwrap();
assert_eq!(report.summary.retained_node_count, 2);
let full = report.full.as_ref().unwrap();
assert!(
full.dependencies
.iter()
.any(|edge| { edge.kind == DependencyKind::Value && edge.hazard == HazardKind::Raw })
);
assert_eq!(
compiled.resource_usage(input.id()).unwrap().bits(),
wgpu::BufferUsages::STORAGE.bits() as u64
);
}
#[test]
fn overwrite_culls_an_unused_previous_writer_despite_waw_ordering() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame.create_buffer(BufferDesc::new("buffer", 16)).unwrap();
for label in ["old", "new"] {
let mut pass = frame.compute_pass(label);
let _ = pass
.storage_buffer_write(buffer, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
pass.finish().unwrap();
}
frame
.mark_buffer_root(buffer, BufferRange::whole(), RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
let report = compiled.report().unwrap();
assert_eq!(report.summary.retained_node_count, 1);
assert_eq!(report.full.as_ref().unwrap().nodes[0].label, "new");
assert!(
report
.full
.as_ref()
.unwrap()
.dependencies
.iter()
.any(|edge| {
edge.kind == DependencyKind::Ordering && edge.hazard == HazardKind::Waw
})
);
}
#[test]
fn partial_buffer_coverage_is_validated_exactly() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame.create_buffer(BufferDesc::new("buffer", 8)).unwrap();
let mut write = frame.compute_pass("write-prefix");
let _ = write
.storage_buffer_write(buffer, BufferRange::new(0, 4), WriteContents::Overwrite)
.unwrap();
write.finish().unwrap();
let mut read = frame.command_pass("read-all");
let _ = read
.storage_buffer_read(buffer, BufferRange::whole())
.unwrap();
read.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::ReadBeforeWrite { .. })
));
}
#[test]
fn imported_defined_contents_can_be_read_without_a_graph_producer() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.import_buffer(
BufferDesc::new("external", 16),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
let mut pass = frame.command_pass("consume-external");
let _ = pass.uniform_buffer(buffer, BufferRange::whole()).unwrap();
pass.finish().unwrap();
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.retained_node_count(), 1);
assert_eq!(compiled.report().unwrap().summary.value_count, 1);
}
#[test]
fn preserve_requires_defined_contents() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame.create_buffer(BufferDesc::new("buffer", 16)).unwrap();
let mut pass = frame.compute_pass("preserve");
let _ = pass
.storage_buffer_write(buffer, BufferRange::whole(), WriteContents::Preserve)
.unwrap();
pass.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::PreserveBeforeWrite { .. })
));
}
#[test]
fn attachment_discard_invalidates_only_the_selected_view() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc = TextureDesc::new_2d("color", 32, 32, wgpu::TextureFormat::Rgba8Unorm);
desc.mip_level_count = 2;
let texture = frame.create_texture(desc).unwrap();
let mip0 = frame
.create_texture_view(
texture,
TextureViewDesc {
label: "mip0".into(),
mip_level_count: Some(1),
..Default::default()
},
)
.unwrap();
let mip1 = frame
.create_texture_view(
texture,
TextureViewDesc {
label: "mip1".into(),
base_mip_level: 1,
mip_level_count: Some(1),
..Default::default()
},
)
.unwrap();
let mut discard = frame.render_pass("discard-mip0");
let _ = discard
.color_attachment(mip0, ColorAttachmentOps::clear_discard(wgpu::Color::BLACK))
.unwrap();
discard.finish().unwrap();
let mut produce = frame.render_pass("produce-mip1");
let _ = produce
.color_attachment(mip1, ColorAttachmentOps::clear_store(wgpu::Color::BLACK))
.unwrap();
produce.finish().unwrap();
frame.mark_texture_root(mip1, RootReason::Output).unwrap();
assert!(frame.compile(full_options()).is_ok());
}
#[test]
fn storage_defaults_to_one_mip_and_attachments_reject_multi_mip_views() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut desc = TextureDesc::new_2d("mipped", 32, 32, wgpu::TextureFormat::Rgba8Unorm);
desc.mip_level_count = 3;
let texture = frame.create_texture(desc).unwrap();
let mut storage = frame.compute_pass("storage-mip0");
let _ = storage
.storage_texture_write(texture, WriteContents::Overwrite)
.unwrap();
storage.finish().unwrap();
let full_view = frame
.create_texture_view(
texture,
TextureViewDesc {
label: "all-mips".into(),
..Default::default()
},
)
.unwrap();
let mut render = frame.render_pass("invalid-attachment");
assert!(matches!(
render.color_attachment(
full_view,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
),
Err(FrameGraphError::InvalidTextureView { .. })
));
render.finish().unwrap();
let mip0 = frame
.create_texture_view(
texture,
TextureViewDesc {
label: "mip0".into(),
mip_level_count: Some(1),
..Default::default()
},
)
.unwrap();
frame.mark_texture_root(mip0, RootReason::Output).unwrap();
let compiled = frame.compile(full_options()).unwrap();
let storage_access = compiled.report().unwrap().full.as_ref().unwrap().accesses[0].clone();
let zenfg::ResourceRange::Texture(regions) = storage_access.range else {
panic!("expected a texture range");
};
assert_eq!(regions.len(), 1);
assert_eq!(regions[0].base_mip_level, 0);
}
#[test]
fn read_after_discard_is_structured_error() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let texture = frame
.create_texture(TextureDesc::new_2d(
"color",
16,
16,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut discard = frame.render_pass("discard");
let _ = discard
.color_attachment(
texture,
ColorAttachmentOps::clear_discard(wgpu::Color::BLACK),
)
.unwrap();
discard.finish().unwrap();
let mut read = frame.command_pass("read");
let _ = read.sampled_texture(texture).unwrap();
read.finish().unwrap();
let error = frame.compile(CompileOptions::default()).unwrap_err();
assert_eq!(error.code(), "FG1003");
assert!(matches!(error, FrameGraphError::ReadAfterDiscard { .. }));
}
#[test]
fn fixed_usage_is_checked_after_culling() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let culled = frame
.create_buffer(BufferDesc {
label: "culled-fixed".into(),
size: 16,
usage: UsagePolicy::Fixed(wgpu::BufferUsages::COPY_DST),
})
.unwrap();
let mut unused = frame.compute_pass("culled-storage-write");
let _ = unused
.storage_buffer_write(culled, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
unused.finish().unwrap();
assert!(frame.compile(CompileOptions::default()).is_ok());
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.create_buffer(BufferDesc {
label: "fixed".into(),
size: 16,
usage: UsagePolicy::Fixed(wgpu::BufferUsages::COPY_DST),
})
.unwrap();
let mut retained = frame.command_pass("retained-storage-write");
let _ = retained
.storage_buffer_write(buffer, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
retained.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::UsageMismatch { .. })
));
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.import_buffer(
BufferDesc {
label: "fixed".into(),
size: 16,
usage: UsagePolicy::Infer,
},
ImportBufferOptions {
initial_contents: InitialContents::Defined,
exposed_usage: Some(wgpu::BufferUsages::COPY_DST),
},
)
.unwrap();
let mut retained = frame.command_pass("retained-storage-read");
let _ = retained
.storage_buffer_read(buffer, BufferRange::whole())
.unwrap();
retained.finish().unwrap();
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::UsageMismatch { .. })
));
}
#[test]
fn declarative_copy_and_clear_nodes_participate_in_content_flow() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_buffer(
BufferDesc::new("source", 16),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
let copied = frame.create_buffer(BufferDesc::new("copied", 16)).unwrap();
let cleared = frame.create_buffer(BufferDesc::new("cleared", 16)).unwrap();
let mut copy = frame.copy_pass("copy");
copy.copy_buffer_to_buffer(source, 0, copied, 0, 16)
.unwrap();
copy.finish().unwrap();
frame
.clear_buffer("clear", cleared, BufferRange::whole())
.unwrap();
let mut consume = frame.command_pass("consume");
let _ = consume
.storage_buffer_read(copied, BufferRange::whole())
.unwrap();
let _ = consume
.storage_buffer_read(cleared, BufferRange::whole())
.unwrap();
consume.finish().unwrap();
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.retained_node_count(), 3);
assert_eq!(
compiled.resource_usage(copied.id()).unwrap().bits(),
(wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE).bits() as u64
);
assert_eq!(
compiled.resource_usage(cleared.id()).unwrap().bits(),
(wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE).bits() as u64
);
}
#[test]
fn grouped_clear_records_ordered_precise_accesses() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let first = frame.create_buffer(BufferDesc::new("first", 16)).unwrap();
let second = frame.create_buffer(BufferDesc::new("second", 16)).unwrap();
let clear = frame
.clear_buffers(
"clear-both",
[
ClearBufferOp::new(first, BufferRange::new(4, 4)),
ClearBufferOp::new(second, BufferRange::new(8, 8)),
],
)
.unwrap();
frame
.mark_buffer_root(first, BufferRange::new(4, 4), RootReason::Output)
.unwrap();
frame
.mark_buffer_root(second, BufferRange::new(8, 8), RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
let report = compiled.report().unwrap().full.as_ref().unwrap();
assert_eq!(report.nodes[0].id, clear);
assert_eq!(report.nodes[0].recording_order, 0);
let accesses = report
.accesses
.iter()
.filter(|access| access.pass == clear)
.collect::<Vec<_>>();
assert_eq!(accesses.len(), 2);
assert_eq!(accesses[0].resource, first.id());
assert_eq!(accesses[0].role, AccessRole::BufferCopyDst);
assert_eq!(accesses[1].resource, second.id());
assert_eq!(accesses[1].role, AccessRole::BufferCopyDst);
}
#[test]
fn failed_grouped_clear_preserves_original_recording_error() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame.create_buffer(BufferDesc::new("buffer", 16)).unwrap();
let error = frame
.clear_buffers(
"conflicting-clear",
[
ClearBufferOp::new(buffer, BufferRange::new(0, 8)),
ClearBufferOp::new(buffer, BufferRange::new(4, 8)),
],
)
.unwrap_err();
assert!(matches!(error, FrameGraphError::ConflictingAccesses { .. }));
let compile_error = match frame.compile(CompileOptions::default()) {
Ok(_) => panic!("failed clear should prevent compilation"),
Err(error) => error,
};
assert_eq!(compile_error, error);
}
#[test]
fn clear_and_copy_native_alignment_validation_is_deferred() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame
.import_buffer(
BufferDesc::new("buffer", 1024),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
frame
.clear_buffer("misaligned-clear", buffer, BufferRange::new(2, 4))
.unwrap();
let error = frame
.clear_buffers("empty-clear", core::iter::empty())
.unwrap_err();
assert_eq!(error.code(), "FG1106");
let texture = frame
.import_texture(
TextureDesc::new_2d("texture", 4, 4, wgpu::TextureFormat::Rgba8Unorm),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
let mut copy = frame.copy_pass("misaligned-copy");
copy.copy_buffer_to_texture(
BufferTextureCopyLocation::new(
buffer,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(4),
},
),
TextureCopyLocation::new(texture),
wgpu::Extent3d {
width: 4,
height: 4,
depth_or_array_layers: 1,
},
)
.unwrap();
copy.finish().unwrap();
let compiled = frame.compile(full_options()).unwrap();
let report = compiled.report().unwrap().full.as_ref().unwrap();
let source_access = report
.accesses
.iter()
.find(|access| access.resource == buffer.id() && access.role == AccessRole::BufferCopySrc)
.unwrap();
assert!(matches!(
source_access.range,
zenfg::ResourceRange::Buffer(BufferRange {
offset: 0,
size: Some(64),
})
));
}
#[test]
fn zero_sized_copies_are_native_noops_without_graph_value_effects() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer_src = frame
.import_buffer(
BufferDesc::new("buffer-src", 64),
ImportBufferOptions {
initial_contents: InitialContents::Undefined,
exposed_usage: Some(wgpu::BufferUsages::COPY_SRC),
},
)
.unwrap();
let buffer_dst = frame
.create_buffer(BufferDesc::new("buffer-dst", 64))
.unwrap();
let texture_src = frame
.import_texture(
TextureDesc::new_2d("texture-src", 4, 4, wgpu::TextureFormat::Rgba8Unorm),
ImportTextureOptions {
initial_contents: InitialContents::Undefined,
exposed_usage: Some(wgpu::TextureUsages::COPY_SRC),
},
)
.unwrap();
let texture_dst = frame
.create_texture(TextureDesc::new_2d(
"texture-dst",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let readback = frame
.create_buffer(BufferDesc::new("readback", 64))
.unwrap();
let layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: None,
rows_per_image: None,
};
let mut copy = frame.copy_pass("no-op-copies");
copy.set_side_effect(true);
copy.copy_buffer_to_buffer(buffer_src, 0, buffer_dst, 0, 0)
.unwrap();
copy.copy_buffer_to_texture(
BufferTextureCopyLocation::new(buffer_src, layout),
TextureCopyLocation::new(texture_dst),
wgpu::Extent3d {
width: 0,
height: 1,
depth_or_array_layers: 1,
},
)
.unwrap();
copy.copy_texture_to_buffer(
TextureCopyLocation::new(texture_src),
BufferTextureCopyLocation::new(readback, layout),
wgpu::Extent3d {
width: 1,
height: 0,
depth_or_array_layers: 1,
},
)
.unwrap();
copy.copy_texture_to_texture(
TextureCopyLocation::new(texture_src),
TextureCopyLocation::new(texture_dst),
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 0,
},
)
.unwrap();
copy.finish().unwrap();
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.retained_node_count(), 1);
assert_eq!(
compiled.resource_usage(buffer_src.id()),
Some(zenfg::ResourceUsage::Buffer(wgpu::BufferUsages::COPY_SRC))
);
assert_eq!(
compiled.resource_usage(buffer_dst.id()),
Some(zenfg::ResourceUsage::Buffer(wgpu::BufferUsages::COPY_DST))
);
assert_eq!(
compiled.resource_usage(texture_src.id()),
Some(zenfg::ResourceUsage::Texture(wgpu::TextureUsages::COPY_SRC))
);
assert_eq!(
compiled.resource_usage(texture_dst.id()),
Some(zenfg::ResourceUsage::Texture(wgpu::TextureUsages::COPY_DST))
);
assert!(
compiled
.report()
.unwrap()
.full
.as_ref()
.unwrap()
.accesses
.is_empty()
);
for resource in [buffer_dst.id(), texture_dst.id(), readback.id()] {
let report = compiled
.report()
.unwrap()
.full
.as_ref()
.unwrap()
.resources
.iter()
.find(|entry| entry.id == resource)
.unwrap();
assert!(report.lifetime.is_some());
assert!(report.allocation.is_some());
}
}
#[test]
fn zero_sized_copy_does_not_define_an_output_root() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_texture(
TextureDesc::new_2d("source", 4, 4, wgpu::TextureFormat::Rgba8Unorm),
ImportTextureOptions {
initial_contents: InitialContents::Defined,
exposed_usage: Some(wgpu::TextureUsages::COPY_SRC),
},
)
.unwrap();
let destination = frame
.create_texture(TextureDesc::new_2d(
"destination",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut copy = frame.copy_pass("empty-copy");
copy.copy_texture_to_texture(
TextureCopyLocation::new(source),
TextureCopyLocation::new(destination),
wgpu::Extent3d {
width: 0,
height: 4,
depth_or_array_layers: 1,
},
)
.unwrap();
copy.finish().unwrap();
frame
.mark_texture_root(destination, RootReason::Output)
.unwrap();
assert!(matches!(
frame.compile(full_options()),
Err(FrameGraphError::RootReferencesUndefinedContents { .. })
));
}
#[test]
fn zero_sized_buffer_copy_does_not_define_an_output_root() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_buffer(
BufferDesc::new("source", 64),
ImportBufferOptions {
initial_contents: InitialContents::Defined,
exposed_usage: Some(wgpu::BufferUsages::COPY_SRC),
},
)
.unwrap();
let destination = frame
.create_buffer(BufferDesc::new("destination", 64))
.unwrap();
let mut copy = frame.copy_pass("empty-copy");
copy.copy_buffer_to_buffer(source, 0, destination, 0, 0)
.unwrap();
copy.finish().unwrap();
frame
.mark_buffer_root(destination, BufferRange::whole(), RootReason::Output)
.unwrap();
assert!(matches!(
frame.compile(full_options()),
Err(FrameGraphError::RootReferencesUndefinedContents { .. })
));
}
#[test]
fn zero_sized_copies_still_validate_logical_endpoints() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_buffer(
BufferDesc::new("source", 64),
ImportBufferOptions {
initial_contents: InitialContents::Defined,
exposed_usage: Some(wgpu::BufferUsages::COPY_SRC),
},
)
.unwrap();
let destination = frame
.create_buffer(BufferDesc::new("destination", 64))
.unwrap();
let mut copy = frame.copy_pass("empty-copy-out-of-bounds");
assert!(matches!(
copy.copy_buffer_to_buffer(source, 65, destination, 0, 0),
Err(FrameGraphError::InvalidBufferRange { .. })
));
copy.copy_buffer_to_buffer(source, 64, destination, 64, 0)
.unwrap();
copy.finish().unwrap();
assert!(frame.compile(full_options()).is_ok());
}
#[test]
fn buffer_texture_copies_require_a_known_footprint_but_texture_copies_do_not() {
let extent = wgpu::Extent3d {
width: 4,
height: 4,
depth_or_array_layers: 1,
};
let layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: None,
rows_per_image: None,
};
{
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let buffer = frame.create_buffer(BufferDesc::new("upload", 64)).unwrap();
let depth = frame
.create_texture(TextureDesc::new_2d(
"depth24plus",
4,
4,
wgpu::TextureFormat::Depth24Plus,
))
.unwrap();
let color = frame
.create_texture(TextureDesc::new_2d(
"color",
4,
4,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut copy = frame.copy_pass("depth-upload");
let error = match copy.copy_buffer_to_texture(
BufferTextureCopyLocation::new(buffer, layout),
TextureCopyLocation::new(depth),
extent,
) {
Ok(_) => panic!("depth24plus buffer copies must be rejected"),
Err(error) => error,
};
assert!(matches!(
error,
FrameGraphError::InvalidNodeOperation { .. }
));
assert!(
error
.to_string()
.contains("no buffer-texture copy footprint")
);
assert_eq!(error.resource(), Some(depth.id()));
copy.copy_buffer_to_texture(
BufferTextureCopyLocation::new(buffer, layout),
TextureCopyLocation::new(color),
extent,
)
.unwrap();
copy.finish().unwrap();
}
{
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let depth = frame
.import_texture(
TextureDesc::new_2d("depth24plus", 4, 4, wgpu::TextureFormat::Depth24Plus),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
let buffer = frame
.create_buffer(BufferDesc::new("readback", 64))
.unwrap();
let mut copy = frame.copy_pass("depth-readback");
let error = match copy.copy_texture_to_buffer(
TextureCopyLocation::new(depth),
BufferTextureCopyLocation::new(buffer, layout),
extent,
) {
Ok(_) => panic!("depth24plus buffer copies must be rejected"),
Err(error) => error,
};
assert!(matches!(
error,
FrameGraphError::InvalidNodeOperation { .. }
));
assert!(
error
.to_string()
.contains("no buffer-texture copy footprint")
);
}
{
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_texture(
TextureDesc::new_2d("source-depth", 4, 4, wgpu::TextureFormat::Depth24Plus),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
let destination = frame
.create_texture(TextureDesc::new_2d(
"destination-depth",
4,
4,
wgpu::TextureFormat::Depth24Plus,
))
.unwrap();
let mut copy = frame.copy_pass("depth-copy");
copy.copy_texture_to_texture(
TextureCopyLocation::new(source),
TextureCopyLocation::new(destination),
extent,
)
.unwrap();
copy.finish().unwrap();
frame
.mark_texture_root(destination, RootReason::Output)
.unwrap();
frame.compile(full_options()).unwrap();
}
}
#[test]
fn partial_texture_copy_preserves_the_previous_subresource_value() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_buffer(
BufferDesc::new("source", 1024),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
let destination = frame
.import_texture(
TextureDesc::new_2d("destination", 4, 4, wgpu::TextureFormat::Rgba8Unorm),
ImportTextureOptions::new(InitialContents::Defined),
)
.unwrap();
let mut copy = frame.copy_pass("partial-copy");
copy.copy_buffer_to_texture(
BufferTextureCopyLocation::new(
source,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(256),
rows_per_image: Some(4),
},
),
TextureCopyLocation::new(destination),
wgpu::Extent3d {
width: 2,
height: 4,
depth_or_array_layers: 1,
},
)
.unwrap();
copy.finish().unwrap();
frame
.mark_texture_root(destination, RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
let destination_access = compiled
.report()
.unwrap()
.full
.as_ref()
.unwrap()
.accesses
.iter()
.find(|access| access.role == AccessRole::TextureCopyDst)
.unwrap();
assert!(destination_access.consumes_previous);
}
#[test]
fn a_surface_must_be_defined_before_present() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let surface = frame
.import_surface_texture(
TextureDesc::new_2d("surface", 16, 16, wgpu::TextureFormat::Bgra8Unorm),
Some(wgpu::TextureUsages::RENDER_ATTACHMENT),
)
.unwrap();
let mut clear = frame.render_pass("clear-surface");
let _ = clear
.color_attachment(surface, ColorAttachmentOps::clear_store(wgpu::Color::BLACK))
.unwrap();
clear.finish().unwrap();
frame.mark_present(surface).unwrap();
assert_eq!(
frame
.compile(CompileOptions::default())
.unwrap()
.retained_node_count(),
1
);
}
#[test]
fn non_overlapping_lifetimes_alias_and_overlapping_lifetimes_do_not() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let a = frame.create_buffer(BufferDesc::new("a", 100)).unwrap();
let b = frame.create_buffer(BufferDesc::new("b", 100)).unwrap();
for (label, buffer) in [("a", a), ("b", b)] {
let mut pass = frame.compute_pass(label);
let _ = pass
.storage_buffer_write(buffer, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
pass.finish().unwrap();
frame
.mark_buffer_root(buffer, BufferRange::whole(), RootReason::Output)
.unwrap();
}
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.allocations().len(), 1);
assert_eq!(compiled.allocations()[0].estimated_byte_size, 128);
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let a = frame.create_buffer(BufferDesc::new("a", 100)).unwrap();
let b = frame.create_buffer(BufferDesc::new("b", 100)).unwrap();
let mut first = frame.compute_pass("a");
let _ = first
.storage_buffer_write(a, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
first.finish().unwrap();
let mut second = frame.compute_pass("a-to-b");
let _ = second.storage_buffer_read(a, BufferRange::whole()).unwrap();
let _ = second
.storage_buffer_write(b, BufferRange::whole(), WriteContents::Overwrite)
.unwrap();
second.finish().unwrap();
frame
.mark_buffer_root(b, BufferRange::whole(), RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.allocations().len(), 2);
}
#[test]
fn texture_allocation_key_covers_every_physical_creation_field() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut descriptors = Vec::new();
descriptors.push(TextureDesc::new_2d(
"base",
8,
8,
wgpu::TextureFormat::Rgba8Unorm,
));
descriptors.push(TextureDesc::new_2d(
"format",
8,
8,
wgpu::TextureFormat::Rg8Unorm,
));
descriptors.push(TextureDesc::new_2d(
"extent",
16,
8,
wgpu::TextureFormat::Rgba8Unorm,
));
let mut mips = TextureDesc::new_2d("mips", 8, 8, wgpu::TextureFormat::Rgba8Unorm);
mips.mip_level_count = 2;
descriptors.push(mips);
let mut samples = TextureDesc::new_2d("samples", 8, 8, wgpu::TextureFormat::Rgba8Unorm);
samples.sample_count = 4;
descriptors.push(samples);
let mut view_formats =
TextureDesc::new_2d("view-formats", 8, 8, wgpu::TextureFormat::Rgba8Unorm);
view_formats.view_formats = vec![wgpu::TextureFormat::Rgba8UnormSrgb];
descriptors.push(view_formats);
for descriptor in descriptors {
let texture = frame.create_texture(descriptor).unwrap();
let view = frame
.create_texture_view(
texture,
TextureViewDesc {
mip_level_count: Some(1),
array_layer_count: Some(1),
..Default::default()
},
)
.unwrap();
let mut pass = frame.render_pass("write");
let _ = pass
.color_attachment(view, ColorAttachmentOps::clear_store(wgpu::Color::BLACK))
.unwrap();
pass.finish().unwrap();
frame.mark_texture_root(view, RootReason::Output).unwrap();
}
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.allocations().len(), 6);
}
#[test]
fn texture_view_format_order_is_canonicalized_for_allocation_aliasing() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let formats = [
vec![
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureFormat::Rgba8UnormSrgb,
],
vec![
wgpu::TextureFormat::Rgba8UnormSrgb,
wgpu::TextureFormat::Rgba8Unorm,
],
];
for (index, view_formats) in formats.into_iter().enumerate() {
let mut descriptor = TextureDesc::new_2d(
format!("texture-{index}"),
8,
8,
wgpu::TextureFormat::Rgba8Unorm,
);
descriptor.view_formats = view_formats;
let texture = frame.create_texture(descriptor).unwrap();
let mut pass = frame.render_pass(format!("write-{index}"));
let _ = pass
.color_attachment(texture, ColorAttachmentOps::clear_store(wgpu::Color::BLACK))
.unwrap();
pass.finish().unwrap();
frame
.mark_texture_root(texture, RootReason::Output)
.unwrap();
}
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(compiled.allocations().len(), 1);
}
#[test]
fn execution_segments_preserve_external_boundaries() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
frame.command_pass("before").finish().unwrap();
frame.external_submission("external").finish().unwrap();
frame.command_pass("after").finish().unwrap();
let compiled = frame.compile(full_options()).unwrap();
let segments = compiled.execution_segments();
assert_eq!(segments.len(), 3);
assert_eq!(
segments[1].kind,
zenfg::ExecutionSegmentKind::ExternalSubmission
);
}
#[test]
fn dropped_pass_poisoning_is_reported_at_compile() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
drop(frame.compute_pass("forgotten"));
assert!(matches!(
frame.compile(CompileOptions::default()),
Err(FrameGraphError::UnclosedPass { .. })
));
}
#[test]
fn handles_from_another_graph_are_rejected_at_runtime() {
let mut a = FrameGraph::new();
let mut b = FrameGraph::new();
let mut frame_a = a.begin_frame();
let foreign = frame_a
.create_buffer(BufferDesc::new("foreign", 4))
.unwrap();
let mut frame_b = b.begin_frame();
let mut pass = frame_b.compute_pass("bad");
assert!(matches!(
pass.storage_buffer_read(foreign, BufferRange::whole()),
Err(FrameGraphError::ForeignHandle { .. })
));
}
#[test]
fn summary_and_none_reports_have_expected_shapes() {
let mut graph = FrameGraph::new();
let frame = graph.begin_frame();
let compiled = frame
.compile(CompileOptions {
report_level: ReportLevel::Summary,
})
.unwrap();
assert!(compiled.report().unwrap().full.is_none());
drop(compiled);
let frame = graph.begin_frame();
let compiled = frame.compile(CompileOptions::default()).unwrap();
assert!(compiled.report().is_none());
}
#[test]
fn explicit_view_dimensions_validate_resolved_remaining_layers() {
use wgpu::TextureViewDimension::{Cube, CubeArray, D2, D2Array};
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let mut descriptor = TextureDesc::new_2d("array", 8, 8, wgpu::TextureFormat::Rgba8Unorm);
descriptor.size.depth_or_array_layers = 12;
let texture = frame.create_texture(descriptor).unwrap();
let view_desc = |dimension, base_array_layer, array_layer_count| TextureViewDesc {
dimension: Some(dimension),
base_array_layer,
array_layer_count,
..Default::default()
};
assert!(matches!(
frame.create_texture_view(texture, view_desc(D2, 0, None)),
Err(FrameGraphError::InvalidTextureView { .. })
));
let last = frame
.create_texture_view(texture, view_desc(D2, 11, None))
.unwrap();
assert_eq!(
frame.texture_view_desc(last).unwrap().array_layer_count,
Some(1)
);
let explicit = frame
.create_texture_view(texture, view_desc(D2, 0, Some(1)))
.unwrap();
assert_eq!(
frame.texture_view_desc(explicit).unwrap().array_layer_count,
Some(1)
);
assert!(matches!(
frame.create_texture_view(texture, view_desc(Cube, 0, None)),
Err(FrameGraphError::InvalidTextureView { .. })
));
let cube = frame
.create_texture_view(texture, view_desc(Cube, 6, None))
.unwrap();
assert_eq!(
frame.texture_view_desc(cube).unwrap().array_layer_count,
Some(6)
);
let cube_array = frame
.create_texture_view(texture, view_desc(CubeArray, 0, None))
.unwrap();
assert_eq!(
frame
.texture_view_desc(cube_array)
.unwrap()
.array_layer_count,
Some(12)
);
assert!(matches!(
frame.create_texture_view(texture, view_desc(CubeArray, 1, None)),
Err(FrameGraphError::InvalidTextureView { .. })
));
let tail = frame
.create_texture_view(texture, view_desc(D2Array, 5, None))
.unwrap();
assert_eq!(
frame.texture_view_desc(tail).unwrap().array_layer_count,
Some(7)
);
assert!(matches!(
frame.create_texture_view(texture, view_desc(D2Array, 12, None)),
Err(FrameGraphError::InvalidTextureView { .. })
));
}
#[test]
fn failed_compound_copy_rolls_back_accesses_and_keeps_builder_usable() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let source = frame
.import_buffer(
BufferDesc::new("source", 16),
ImportBufferOptions::new(InitialContents::Defined),
)
.unwrap();
let destination = frame
.create_buffer(BufferDesc::new("destination", 16))
.unwrap();
let mut pass = frame.copy_pass("recover-copy");
let first = pass
.buffer_copy_dst(
destination,
BufferRange::new(0, 4),
WriteContents::Overwrite,
)
.unwrap();
let error = pass
.copy_buffer_to_buffer(source, 0, destination, 0, 4)
.err()
.unwrap();
assert_eq!(error.code(), "FG1105");
let recovered = pass
.buffer_copy_dst(source, BufferRange::new(0, 4), WriteContents::Overwrite)
.unwrap();
assert_eq!(recovered.access_id().get(), first.access_id().get() + 1);
pass.finish().unwrap();
frame
.mark_buffer_root(source, BufferRange::new(0, 4), RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
let full = compiled.report().unwrap().full.as_ref().unwrap();
assert_eq!(full.accesses.len(), 2);
assert!(
full.accesses
.iter()
.all(|access| access.role == AccessRole::BufferCopyDst)
);
}
#[test]
fn failed_resolve_declaration_rolls_back_source_access() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let texture = frame
.create_texture(TextureDesc::new_2d(
"color",
8,
8,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut pass = frame.render_pass("recover-resolve");
let error = pass
.color_attachment_with_resolve(
texture,
texture,
ColorAttachmentOps::clear_store(wgpu::Color::BLACK),
)
.unwrap_err();
assert_eq!(error.code(), "FG1105");
let _ = pass
.color_attachment(texture, ColorAttachmentOps::clear_store(wgpu::Color::BLACK))
.unwrap();
pass.finish_render(|_| Ok(())).unwrap();
frame
.mark_texture_root(texture, RootReason::Output)
.unwrap();
let compiled = frame.compile(full_options()).unwrap();
assert_eq!(
compiled
.report()
.unwrap()
.full
.as_ref()
.unwrap()
.accesses
.len(),
1
);
}
#[test]
fn native_render_attachment_validation_is_deferred_and_preserves_graph_errors() {
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let texture = frame
.create_texture(TextureDesc::new_2d(
"color",
8,
8,
wgpu::TextureFormat::Rgba8Unorm,
))
.unwrap();
let mut pass = frame.render_pass("invalid-clear");
let _ = pass
.color_attachment(
texture,
ColorAttachmentOps::clear_store(wgpu::Color {
r: f64::NAN,
g: 0.0,
b: 0.0,
a: 1.0,
}),
)
.unwrap();
pass.finish_render(|_| Ok(())).unwrap();
frame.compile(full_options()).unwrap();
}