Files
OneCAD-Draft/src/scene/pipeline/gpu_tests.rs
T

565 lines
21 KiB
Rust

use super::*;
use crate::scene::model::{mesh_model::MeshLodSet, mesh_model::MeshModel, wire_model::WireModel};
use acadrust::{types::Transform, Handle};
use iced::futures::executor::block_on;
#[test]
#[ignore = "requires a GPU adapter"]
fn block_edits_preserve_cache_coordinates_and_arena_partition() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
// Definition geometry is shared across viewport slots, so it lives on the
// MultiPipeline; a single-slot test supplies its own.
let mut block_geometry = wire_gpu::BlockGeometryCache::default();
let block = |handle: u64, x: f64| WireModel {
name: handle.to_string(),
points: vec![[x as f32, 0.0, 0.0], [x as f32 + 1.0, 0.0, 0.0]],
render_instance: Some(crate::scene::model::instance_model::RenderInstance {
source_id: 1,
translation: [x, 0.0, 0.0],
}),
..Default::default()
};
let first = block(1, 10.0);
let second = block(2, 20.0);
let depth = rustc_hash::FxHashMap::from_iter([(1, [0.1, 0.01]), (2, [0.2, 0.01])]);
let original = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth, &mut block_geometry);
let unchanged = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth, &mut block_geometry);
assert_eq!(original[0].geometry_id(), unchanged[0].geometry_id());
assert_eq!(original[0].instance_count, 2);
let removed = pipeline.upload_block_wires(&device, &queue, &[&second], &depth, &mut block_geometry);
assert_ne!(
original[0].geometry_id(),
removed[0].geometry_id(),
"removing the base instance must replace vertices baked at its old position"
);
let moved = block(2, 30.0);
let moved_gpu = pipeline.upload_block_wires(&device, &queue, &[&moved], &depth, &mut block_geometry);
assert_ne!(removed[0].geometry_id(), moved_gpu[0].geometry_id());
let restored = pipeline.upload_block_wires(&device, &queue, &[&first, &second], &depth, &mut block_geometry);
assert_ne!(moved_gpu[0].geometry_id(), restored[0].geometry_id());
assert_eq!(restored[0].instance_count, 2);
assert_eq!(
block_geometry.len(),
1,
"discard obsolete geometry"
);
let line = WireModel {
render_instance: None,
..first.clone()
};
let changed = [second];
let (regular, _) = wire_arena::split_wires(&changed);
let runs = rustc_hash::FxHashMap::from_iter([(Handle::new(2), regular)]);
for layout in [pipeline.wire_const_bgl.as_ref(), None] {
let mut arena = wire_arena::PersistentWireArena::build(
&device,
&queue,
&[&line],
&depth,
layout,
false,
)
.unwrap();
assert!(arena.patch(
&queue,
&[(Handle::new(2), crate::scene::ChangeKind::Added)],
&runs,
true,
&depth
));
assert_eq!(
arena
.wire_gpus()
.iter()
.map(|gpu| gpu.instance_count)
.sum::<u32>(),
1,
"the block must not also enter the regular arena"
);
}
queue.submit([]);
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
// OCS_GPU_CHUNK_MIB=1 cargo test --lib bounded_gpu_uploads -- --ignored --nocapture
#[test]
#[ignore = "requires a GPU adapter and OCS_GPU_CHUNK_MIB=1"]
fn bounded_gpu_uploads_preserve_geometry_and_instancing() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
eprintln!("GPU regression adapter: {:?}", adapter.get_info());
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let budget = gpu_budget::buffer_budget(&device) as u64;
assert_eq!(budget, 1024 * 1024, "run with OCS_GPU_CHUNK_MIB=1");
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
let line = WireModel {
points: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
..Default::default()
};
let count = gpu_budget::max_storage_elements::<wire_gpu::WireConst>(&device) + 2;
let wires = vec![line; count];
for bgl in [pipeline.wire_const_bgl.as_ref(), None] {
let chunks =
wire_gpu::WireGpu::from_run(&device, &queue, &wires, &Default::default(), false, bgl);
assert!(chunks.len() > 1);
assert_eq!(
chunks
.iter()
.map(|chunk| chunk.instance_count as usize)
.sum::<usize>(),
count
);
assert!(chunks
.iter()
.all(|chunk| chunk.instance_buffer.size() <= budget));
}
let segments = gpu_budget::max_elements::<wire_gpu::WireInstance>(&device) + 1;
let dense = WireModel {
points: (0..=segments).map(|i| [i as f32, 0.0, 0.0]).collect(),
..Default::default()
};
let chunks = wire_gpu::WireGpu::from_run(
&device,
&queue,
&[dense],
&Default::default(),
false,
pipeline.wire_const_bgl.as_ref(),
);
assert!(chunks.len() > 1);
assert_eq!(
chunks
.iter()
.map(|chunk| chunk.instance_count as usize)
.sum::<usize>(),
segments
);
assert!(chunks
.iter()
.all(|chunk| chunk.instance_buffer.size() <= budget));
let vertices = vec![
bytemuck::Zeroable::zeroed();
gpu_budget::max_elements_grouped::<text_gpu::TextVertex>(&device, 6) + 6
];
let text = text_gpu::upload_vertices(&device, &queue, &vertices);
assert_eq!(text.len(), 2);
assert_eq!(
text.iter()
.map(|chunk| chunk.vertex_count as usize)
.sum::<usize>(),
vertices.len()
);
assert!(text
.iter()
.all(|chunk| chunk.vertex_count % 6 == 0 && chunk.vertex_buffer.size() <= budget));
let source = |triangles: usize| {
let mut set = MeshLodSet::from_single(MeshModel {
name: "7".to_owned(),
verts: (0..triangles)
.flat_map(|_| [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
.collect(),
verts_low: Vec::new(),
normals: Vec::new(),
indices: (0..triangles as u32 * 3).collect(),
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color: [1.0; 4],
selected: false,
});
set.prepare_instance_source(Handle::new(7));
set.lods.clear();
set.instance_transform = Some(Transform::identity());
set.instance_handle = Some(Handle::new(101));
set
};
let triangles = gpu_budget::max_elements::<mesh_gpu::MeshVertex>(&device) / 3 + 1;
let first = source(triangles);
let mut second = first.clone();
second.instance_handle = Some(Handle::new(102));
let (mesh, total) = mesh_gpu::build_mesh_batch(&device, &queue, &[first, second]);
assert!(mesh.len() > 1);
assert_eq!(total, (triangles * 2) as u64);
assert_eq!(
mesh.iter()
.map(|chunk| chunk.index_count as usize / 3)
.sum::<usize>(),
triangles
);
for chunk in &mesh {
assert_eq!(
chunk.instance_count, 2,
"large geometry must remain instanced"
);
assert_eq!(chunk.handles.len(), 2);
assert_eq!(chunk.highlight_ranges.len(), 2);
assert!(chunk
.highlight_ranges
.iter()
.all(|range| range.index_count == chunk.index_count && range.instance_start < 2));
for buffer in [
&chunk.vertex_buffer,
&chunk.index_buffer,
&chunk.transp_index_buffer,
&chunk.edge_vertex_buffer,
&chunk.wire_vertex_buffer,
&chunk.instance_buffer,
] {
assert!(
buffer.size() <= budget,
"buffer size {} exceeds {budget}",
buffer.size()
);
}
}
let instance_count = gpu_budget::max_elements::<mesh_gpu::MeshInstanceGpu>(&device) + 1;
let mut repeated = vec![source(1); instance_count];
for (index, set) in repeated.iter_mut().enumerate() {
set.instance_handle = Some(Handle::new(1000 + index as u64));
}
let (mesh, total) = mesh_gpu::build_mesh_batch(&device, &queue, &repeated);
assert_eq!(mesh.len(), 2);
assert_eq!(total, instance_count as u64);
assert_eq!(mesh[0].vertex_buffer, mesh[1].vertex_buffer);
assert_eq!(mesh[0].index_buffer, mesh[1].index_buffer);
assert_eq!(
mesh.iter()
.map(|chunk| chunk.instance_count as usize)
.sum::<usize>(),
instance_count
);
for chunk in &mesh {
assert!(chunk.instance_buffer.size() <= budget);
assert_eq!(chunk.highlight_ranges.len(), chunk.instance_count as usize);
assert!(chunk
.highlight_ranges
.iter()
.all(|range| range.instance_start < chunk.instance_count));
}
queue.submit([]);
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
/// The shadow map is 16 MiB and every slot used to hold one whether or not its
/// visual style enabled shadows. Now it is allocated on the transition, which
/// means the frame bind group is rebuilt while the device is watching — the one
/// path the other GPU tests never take.
#[test]
#[ignore = "requires a GPU adapter"]
fn toggling_shadows_reallocates_without_upsetting_the_device() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
assert!(
pipeline.shadow_full.is_none(),
"a fresh slot must not hold a shadow map"
);
let camera = crate::scene::view::camera::Camera::default();
let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(64.0, 64.0));
let with_shadows = |on: bool| {
let mut u = Uniforms::new(&camera, bounds, false);
u.shadow_params[0] = if on { 1.0 } else { 0.0 };
u
};
// Off → on → off → on, so both transitions are exercised twice and the
// second `on` proves the release did not leave the bind group stale.
for expected in [true, false, true] {
pipeline.upload_uniforms(&device, &queue, &with_shadows(expected));
assert_eq!(
pipeline.shadow_full.is_some(),
expected,
"shadow target should follow shadow_params[0]"
);
}
// Releasing a cold slot gives it back and must rebuild the bind group too.
pipeline.release_heavy_resources(&device);
assert!(pipeline.shadow_full.is_none(), "a released slot holds none");
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
#[test]
#[ignore = "requires a GPU adapter"]
fn urgent_release_preserves_visible_panes_and_frees_cold_slots() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter =
block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).unwrap();
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.unwrap();
let mut pipeline = <MultiPipeline as iced::widget::shader::Pipeline>::new(
&device,
&queue,
wgpu::TextureFormat::Bgra8UnormSrgb,
);
let first = pipeline.resolve_slots(&device, &queue, &[10])[0];
pipeline.inners[first].slot_id = 10;
pipeline.inners[first].ensure_depth_texture(&device, Size::new(512, 512));
let second = pipeline.resolve_slots(&device, &queue, &[20])[0];
pipeline.inners[second].slot_id = 20;
pipeline.inners[second].ensure_depth_texture(&device, Size::new(512, 512));
pipeline.release_idle_slots(&device, &[second], true);
assert_eq!(
pipeline.inners[first].alloc_size,
Size::new(512, 512),
"a sibling pane prepared immediately before this pane is still visible"
);
pipeline
.frame_rendered
.store(true, std::sync::atomic::Ordering::Relaxed);
let next = pipeline.resolve_slots(&device, &queue, &[20]);
assert_eq!(
pipeline.release_idle_slots(&device, &next, true),
0,
"a previous-frame pane may still be prepared later in this frame"
);
pipeline
.frame_rendered
.store(true, std::sync::atomic::Ordering::Relaxed);
let next = pipeline.resolve_slots(&device, &queue, &[20]);
assert_eq!(pipeline.release_idle_slots(&device, &next, true), 1);
assert_eq!(pipeline.inners[first].alloc_size, Size::new(0, 0));
assert_eq!(pipeline.inners[second].alloc_size, Size::new(512, 512));
}
#[test]
#[ignore = "requires a GPU adapter"]
fn late_device_errors_invalidate_uploaded_content_and_targets() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter =
block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())).unwrap();
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.unwrap();
let mut pipeline = <MultiPipeline as iced::widget::shader::Pipeline>::new(
&device,
&queue,
wgpu::TextureFormat::Bgra8UnormSrgb,
);
let inner = &mut pipeline.inners[0];
inner.cached_wire_id = 7;
inner.wire_arena_id = 7;
inner.ensure_depth_texture(&device, Size::new(512, 512));
let before = gpu_errors_seen();
let _rejected = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rejected test allocation"),
size: device.limits().max_buffer_size + 4,
usage: wgpu::BufferUsages::VERTEX,
mapped_at_creation: false,
});
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert!(gpu_errors_seen() > before);
inner.sync_gpu_error_epoch();
assert_eq!(inner.cached_wire_id, u64::MAX);
assert_eq!(inner.wire_arena_id, u64::MAX);
assert_eq!(inner.alloc_size, Size::new(0, 0));
assert_eq!(inner.background_source_id, usize::MAX);
inner.cached_wire_id = 8;
inner.sync_gpu_error_epoch();
assert_eq!(
inner.cached_wire_id, 8,
"the same error must not invalidate twice"
);
}
#[test]
#[ignore = "requires a GPU adapter"]
fn test_selected_circle_arc_ellipse_highlight_overlay() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
let depth = rustc_hash::FxHashMap::default();
let mut circle_wire = WireModel::default();
circle_wire.name = "circle_wire".into();
circle_wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center: [0.0, 0.0, 0.0],
axis_x: [1.0, 0.0, 0.0],
axis_y: [0.0, 1.0, 0.0],
radius: 10.0,
});
let handle = acadrust::Handle::new(100);
let mut selected_handles = rustc_hash::FxHashSet::default();
selected_handles.insert(handle);
let hover_handles = rustc_hash::FxHashSet::default();
let mut index = rustc_hash::FxHashMap::default();
index.insert(handle.value(), vec![0]);
pipeline.wire_handle_index = std::sync::Arc::new(index);
let wires = vec![circle_wire];
pipeline.upload_selected_wires(
&device,
&queue,
&wires,
&selected_handles,
&hover_handles,
&[],
&depth,
Some([0.0, 1.0, 1.0, 1.0]),
);
assert_eq!(pipeline.gpu_selected_circles.len(), 1);
assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1);
assert!(pipeline.gpu_selected_wires.is_empty());
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("test_target"),
size: wgpu::Extent3d {
width: 512,
height: 512,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("test_encoder"),
});
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
false,
false,
);
queue.submit(Some(encoder.finish()));
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
}
#[test]
#[ignore = "requires a GPU adapter"]
fn test_pline_arc_switch_preview_and_render() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb);
let depth = rustc_hash::FxHashMap::default();
use crate::command::CadCommand;
let mut cmd = crate::modules::draw::draw::polyline::PlineCommand::new();
cmd.on_point(glam::DVec3::new(0.0, 0.0, 0.0));
let _res = cmd.on_point(glam::DVec3::new(10.0, 0.0, 0.0));
cmd.set_live_handle(acadrust::Handle::new(1));
cmd.on_text_input("A");
let test_points = [
glam::DVec3::new(10.0, 0.0, 0.0),
glam::DVec3::new(10.0000001, 0.0, 0.0),
glam::DVec3::new(10.0, 0.0000001, 0.0),
glam::DVec3::new(10.0, -0.0000001, 0.0),
glam::DVec3::new(9.9999999, 0.0, 0.0),
glam::DVec3::new(15.0, 5.0, 0.0),
glam::DVec3::new(10.0, 10.0, 0.0),
glam::DVec3::new(5.0, 5.0, 0.0),
glam::DVec3::new(0.0, 0.0, 0.0),
];
pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512));
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("test_target"),
size: wgpu::Extent3d {
width: 512,
height: 512,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
for pt in test_points {
let wire = cmd.on_mouse_move(pt);
if let Some(w) = wire {
pipeline.upload_preview_wires(&device, &queue, &[w], &depth);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("test_encoder"),
});
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
false,
false,
);
queue.submit(Some(encoder.finish()));
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
}
}
}