mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 02:27:00 -06:00
- 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>
89 lines
4.0 KiB
Rust
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();
|
|
}
|
|
}
|