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::(), 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::(&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::(), count ); assert!(chunks .iter() .all(|chunk| chunk.instance_buffer.size() <= budget)); } let segments = gpu_budget::max_elements::(&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::(), segments ); assert!(chunks .iter() .all(|chunk| chunk.instance_buffer.size() <= budget)); let vertices = vec![ bytemuck::Zeroable::zeroed(); gpu_budget::max_elements_grouped::(&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::(), 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::(&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::(), 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::(&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::(), 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 = ::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 = ::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" ); }