Files
effectcraft/crates/ui-egui/tests/ui_gpu.rs
T
KattandClaude Opus 5.5 a650e6c21d Tests: four failures on Windows (autosave paths, GPU viewer adapter, audio preview race)
- automation / cli autosave restart tests: the MCP instructions embed the
  auto-save state as JSON, so a Windows path's backslashes are escaped there;
  look for the JSON form of the path.
- ui_gpu: egui_kittest prefers a CPU adapter, which on Windows is WARP on
  DirectX 12, where FXC fails to compile the compositor's `pointwise` kernel
  ("compilation aborted unexpectedly"), so the viewer stayed on the CPU and
  the test failed before comparing anything. Build the harness on the adapter
  the headless compositor picked (falling back to egui_kittest's choice).
- ui_audio_scrub (#103 test): with a fixed 2 s comp, a machine with many
  render threads cached the whole preview before the first step, so the sound
  rightly started at once and no silent frame was counted (3 of 24 runs
  failed with 8 copies in parallel). Size the comp to four times the frames
  playback requests per step, so caching takes at least four steps (0 of 24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:36:52 +05:30

89 lines
4.0 KiB
Rust

//! 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();
}
}