//! The viewer on the GPU compositor (egui_kittest with wgpu): frames composited on the GPU match //! the CPU viewer. Skips without a GPU adapter (CI). Set `EC_SNAPSHOT_DIR` to keep PNGs. use effectcraft_engine::Session; use effectcraft_ui_egui::EffectcraftApp; use egui_kittest::Harness; use serde_json::json; fn settle(h: &mut Harness<'_, EffectcraftApp>) { for _ in 0..600 { h.step(); if h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false) { break; } std::thread::sleep(std::time::Duration::from_millis(20)); } for _ in 0..4 { h.step(); } } /// egui_kittest's wgpu setup on the adapter the headless compositor picked (`name (Backend)`), /// like the desktop app's window. Left alone, egui_kittest prefers a CPU adapter, which on /// Windows is WARP on DirectX 12, where FXC fails to compile the compositor's kernels: the /// viewer would stay on the CPU and the comparison would never run. fn same_adapter(name: String) -> eframe::egui_wgpu::WgpuSetup { let mut setup = egui_kittest::wgpu::default_wgpu_setup(); if let eframe::egui_wgpu::WgpuSetup::CreateNew(c) = &mut setup { let fallback = c.native_adapter_selector.take(); c.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| { let named = |a: &&eframe::wgpu::Adapter| format!("{} ({:?})", a.get_info().name, a.get_info().backend) == name; match (adapters.iter().find(named), &fallback) { (Some(a), _) => Ok(a.clone()), (None, Some(f)) => f(adapters, surface), (None, None) => adapters.first().cloned().ok_or_else(|| "No adapter found".to_owned()), } })); } setup } #[test] fn viewer_draws_gpu_frames_that_match_the_cpu() { let Some(probe) = effectcraft_gpu::Gpu::headless() else { eprintln!("no GPU adapter: skipping"); return; }; let adapter = effectcraft_engine::render::Accelerator::name(&probe); eprintln!("headless compositor adapter: {adapter}"); // egui-wgpu creates its GL device with WebGL2's limits, which the compositor declines. let gl = adapter.ends_with("(Gl)"); drop(probe); let mut s = Session::default(); s.execute("file.openDemoProject", json!({})).unwrap(); s.execute("time.set", json!({"time": 2.0})).unwrap(); let mut h = Harness::builder() .with_size(egui::vec2(1400.0, 900.0)) .with_pixels_per_point(1.0) .wgpu_setup(same_adapter(adapter)) .build_eframe(|_| EffectcraftApp::new(s)); settle(&mut h); if gl && h.state().gpu_adapter().is_none() { eprintln!("egui-wgpu's GL device has WebGL2 limits: the viewer stays on the CPU, skipping"); return; } assert!(h.state().gpu_adapter().is_some(), "GPU compositor on egui-wgpu's device"); eprintln!("viewer compositor adapter: {:?}", h.state().gpu_adapter()); let gpu_px = h.state_mut().viewer_pixels().expect("read back the GPU frame"); let gpu_shot = h.render().expect("render"); // Mercury Software Only: the same frame from the CPU. h.state_mut().session.execute("render.backend", json!({"backend": "cpu"})).unwrap(); settle(&mut h); let cpu_px = h.state_mut().viewer_pixels().expect("CPU frame"); let cpu_shot = h.render().expect("render"); assert_eq!(gpu_px.size, cpu_px.size); let mut worst = 0i32; let mut over = 0usize; for (a, b) in gpu_px.pixels.iter().zip(&cpu_px.pixels) { let d = a.to_array().iter().zip(b.to_array()).map(|(x, y)| (*x as i32 - y as i32).abs()).max().unwrap_or(0); worst = worst.max(d); over += usize::from(d > 1); } assert!(over * 1000 <= gpu_px.pixels.len() * 3 && worst <= 4, "viewer GPU vs CPU: {over} pixels > 1/255, worst {worst}/255"); if let Ok(d) = std::env::var("EC_SNAPSHOT_DIR") { gpu_shot.save(format!("{d}/ui_gpu_viewer.png")).unwrap(); cpu_shot.save(format!("{d}/ui_cpu_viewer.png")).unwrap(); } }