diff --git a/crates/engine/src/doc_path.rs b/crates/engine/src/doc_path.rs index 1f8e10e8..00d6c33f 100644 --- a/crates/engine/src/doc_path.rs +++ b/crates/engine/src/doc_path.rs @@ -50,9 +50,7 @@ pub fn resolve(stored: &str, project: Option<&str>) -> Result - { + Refused::Invalid if project.is_none() && craft_docpath::resolve(stored, None).is_ok_and(|p| cfg!(windows) || !is_windows_rooted(&p)) => { NotFollowed::NoProjectFolder } why => NotFollowed::Refused(why), diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index 24f8c961..79ef77b5 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -79,6 +79,35 @@ pub(crate) fn on_gl() -> bool { gpu().is_some_and(|g| g.ctx.name.ends_with("(Gl)")) } +/// The test device is Mesa's software rasteriser, llvmpipe: on OpenGL, or as lavapipe on Vulkan +/// (the Linux CI gate). +pub(crate) fn on_llvmpipe() -> bool { + gpu().is_some_and(|g| g.ctx.name.starts_with("llvmpipe")) +} + +thread_local! { + static LLVMPIPE_FLIPS: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// Until the guard drops, comparisons on lavapipe get [`GL_BOUNDARY_FLIPS`] too. Only the tests +/// that measured such flips there opt in (CC HexTile cell edges, CC Threshold RGB thresholds); +/// every other comparison on the Linux gate stays exact. +pub(crate) fn allow_llvmpipe_boundary_flips() -> impl Drop { + struct Reset; + impl Drop for Reset { + fn drop(&mut self) { + LLVMPIPE_FLIPS.with(|f| f.set(false)); + } + } + LLVMPIPE_FLIPS.with(|f| f.set(true)); + Reset +} + +/// The boundary-flip allowance applies: OpenGL, or llvmpipe in a test that opted in. +fn boundary_flips_allowed() -> bool { + on_gl() || (LLVMPIPE_FLIPS.with(std::cell::Cell::get) && on_llvmpipe()) +} + /// On GL, the fraction of pixels allowed past the tolerance: pixels on a decision boundary (a /// hex cell's edge, a threshold, a corner pin's coverage edge, a median's rank tie) may land on /// the other side, since GL drivers may divide through a reciprocal and GLSL's `fma` need not be @@ -255,7 +284,7 @@ pub(crate) fn check(label: &str, d: Option, allow: f64) { let frac = d.over as f64 / d.total as f64; // Quantised depths: rounding-boundary flips (see the module docs). let (allow, cap) = if d.quantized && allow == 0.0 { (0.003, 4.0 / 255.0 + 1e-6) } else { (allow, f32::INFINITY) }; - let (allow, cap) = if on_gl() { (allow.max(GL_BOUNDARY_FLIPS), f32::INFINITY) } else { (allow, cap) }; + let (allow, cap) = if boundary_flips_allowed() { (allow.max(GL_BOUNDARY_FLIPS), f32::INFINITY) } else { (allow, cap) }; assert!(frac <= allow && d.max <= cap, "{label}: {} of {} pixels over tolerance (max {:.6}); worst {:?}", d.over, d.total, d.max, d.worst); if d.over > 0 { eprintln!("{label}: {} of {} pixels over tolerance (max {:.6})", d.over, d.total, d.max); @@ -454,7 +483,7 @@ pub(crate) fn effect_direct(id: &str, vals: &[(&str, Value)], adjustment: bool) let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect"); assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry"); let d = diff(&cpu.img, &out.img, 1e-3); - let allow = if on_gl() { (d.total as f64 * GL_BOUNDARY_FLIPS) as usize } else { 0 }; + let allow = if boundary_flips_allowed() { (d.total as f64 * GL_BOUNDARY_FLIPS) as usize } else { 0 }; assert!( d.over <= allow, "{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}", diff --git a/crates/gpu/src/tests_fx_pixel2.rs b/crates/gpu/src/tests_fx_pixel2.rs index 899955b8..4a7886af 100644 --- a/crates/gpu/src/tests_fx_pixel2.rs +++ b/crates/gpu/src/tests_fx_pixel2.rs @@ -12,7 +12,7 @@ use effectcraft_project::{BitDepth, MaskMode}; use effectcraft_render::RenderOpts; use effectcraft_time::Tick; -use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern, set}; +use crate::tests::{Scene, allow_llvmpipe_boundary_flips, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern, set}; pub(crate) fn e(v: u32) -> Value { Value::Enum(v) @@ -193,6 +193,7 @@ fn composites_and_colour() { #[test] fn stylize() { + let _flips = allow_llvmpipe_boundary_flips(); for (done, flags) in [(30.0, [true, true, true, false]), (55.0, [false, true, false, true]), (70.0, [true, false, false, false]), (10.0, [false, false, true, true])] { diff --git a/crates/gpu/src/tests_fx_stylize.rs b/crates/gpu/src/tests_fx_stylize.rs index 44e156ae..20d76efa 100644 --- a/crates/gpu/src/tests_fx_stylize.rs +++ b/crates/gpu/src/tests_fx_stylize.rs @@ -6,7 +6,7 @@ use effectcraft_project::BitDepth; use effectcraft_render::RenderOpts; use effectcraft_time::Tick; -use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; +use crate::tests::{Scene, allow_llvmpipe_boundary_flips, c, check, compare_at, effect_case, n, opts, set}; fn e(v: u32) -> Value { Value::Enum(v) @@ -22,6 +22,7 @@ fn pt(x: f64, y: f64) -> Value { #[test] fn posterize_thresholds_strobe_vignette() { + let _flips = allow_llvmpipe_boundary_flips(); effect_case("ec.stylize.posterize", &[("level", n(4.0))]); effect_case("ec.stylize.posterize", &[("level", n(9.0))]); effect_case("ec.stylize.threshold", &[("level", n(100.0))]); diff --git a/crates/gpu/src/tests_fx_warp.rs b/crates/gpu/src/tests_fx_warp.rs index 39bcba13..1482c268 100644 --- a/crates/gpu/src/tests_fx_warp.rs +++ b/crates/gpu/src/tests_fx_warp.rs @@ -2,6 +2,8 @@ //! Emboss, Cartoon) vs the CPU effects (the oracle): direct on a buffer at full and half //! resolution, as adjustment, and composited at 8 and 32 bpc. +use std::io::Write; + use effectcraft_effects::MaskShape; use effectcraft_keyframe::{ShapePath, Value}; use effectcraft_project::build::{self, Ids}; @@ -9,7 +11,7 @@ use effectcraft_project::{BitDepth, MaskMode}; use effectcraft_render::RenderOpts; use effectcraft_time::Tick; -use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern}; +use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, on_llvmpipe, opts, pattern}; fn e(v: u32) -> Value { Value::Enum(v) @@ -182,6 +184,12 @@ fn smear_and_reshape() { ], ]; for vals in reshape { + if on_llvmpipe() && vals.iter().any(|(k, _)| *k == "boundaryMask") { + // Straight to stderr: libtest captures `eprintln!` of passing tests, and the skip + // should show in the log. + let _ = writeln!(std::io::stderr(), "tests_fx_warp::smear_and_reshape: skipping ec.distort.reshape {vals:?} on llvmpipe (crashes lavapipe, #2238)"); + continue; + } direct_with_masks("ec.distort.reshape", vals); composite_with_masks("ec.distort.reshape", vals); }