diff --git a/crates/effects/tests/sim_golden.rs b/crates/effects/tests/sim_golden.rs
index a6e4e5a2..2a3a7c72 100644
--- a/crates/effects/tests/sim_golden.rs
+++ b/crates/effects/tests/sim_golden.rs
@@ -10,12 +10,16 @@
//! `f32` bits) and compared with the pinned hashes below: any change to the CPU output of these
//! effects, however small, fails the test.
//!
-//! The hashes are exact `f32` bit patterns, so they depend on the platform's libm; they are
-//! pinned for aarch64 macOS (where the gates run). Elsewhere the test only checks that every
-//! case renders deterministically.
+//! The hashes are exact `f32` bit patterns, so they depend on the platform's libm (`sin`, `cos`,
+//! `powf`…); they are pinned for aarch64 macOS (where the gates run). Elsewhere the test only
+//! checks that every case renders deterministically. A case whose frame differs between macOS
+//! releases only through libm also accepts that release's hash ([`LIBM_VARIANTS`]).
//!
//! `SIM_GOLDEN_PRINT=1` prints the table (to re-pin after an intended change);
//! `SIM_GOLDEN_OUT=
` also writes every frame's raw pixels (`f32` little-endian RGBA) there.
+//! To tell a libm difference from a change in the code, render the cases with `SIM_GOLDEN_OUT`
+//! here and at the commit that pinned the hash, and compare the frames: identical frames mean
+//! the code didn't change, and the new hash goes into [`LIBM_VARIANTS`].
use effectcraft_effects::{Buf, EffectCtx, EffectEnv, EffectHost, LayerPixels, Params, apply, default_value, find};
use effectcraft_keyframe::Value;
@@ -316,17 +320,41 @@ fn sim_effects_golden_hashes() {
// Deterministic: a second pass (warm simulation caches) renders the same frames.
assert_eq!(got, hashes(), "simulation effects render deterministically");
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
- let bad: Vec = got
- .iter()
- .zip(GOLDEN)
- .filter(|((n, h), (gn, gh))| n != gn || h != gh)
- .map(|((n, h), (_, gh))| format!("{n}: 0x{h:016x} (pinned 0x{gh:016x})"))
- .collect();
assert_eq!(got.len(), GOLDEN.len(), "case count");
- assert!(bad.is_empty(), "CPU output changed:\n{}", bad.join("\n"));
+ let bad = mismatches(&got, GOLDEN, LIBM_VARIANTS);
+ assert!(bad.is_empty(), "CPU output changed (if it may be libm, see the module docs):\n{}", bad.join("\n"));
}
}
+/// The cases whose hash is neither the pinned one nor one of the case's [`LIBM_VARIANTS`].
+fn mismatches(got: &[(String, u64)], golden: &[(&str, u64)], variants: &[(&str, u64, &str)]) -> Vec {
+ got.iter()
+ .zip(golden)
+ .filter(|((n, h), (gn, gh))| n != gn || (h != gh && !variants.iter().any(|(vn, vh, _)| vn == n && vh == h)))
+ .map(|((n, h), (_, gh))| format!("{n}: 0x{h:016x} (pinned 0x{gh:016x})"))
+ .collect()
+}
+
+/// A libm variant passes for its own case only (#232).
+#[test]
+fn libm_variants_are_accepted_for_their_case_only() {
+ let golden = [("a", 1), ("b", 2)];
+ let variants = [("a", 9, "another libm")];
+ let got = |a: u64, b: u64| vec![("a".to_string(), a), ("b".to_string(), b)];
+ assert!(mismatches(&got(1, 2), &golden, &variants).is_empty());
+ assert!(mismatches(&got(9, 2), &golden, &variants).is_empty(), "the variant");
+ assert_eq!(mismatches(&got(9, 9), &golden, &variants), ["b: 0x0000000000000009 (pinned 0x0000000000000002)"], "not another case's");
+ assert_eq!(mismatches(&got(3, 2), &golden, &variants).len(), 1);
+ assert!(LIBM_VARIANTS.iter().all(|(n, ..)| GOLDEN.iter().any(|(g, _)| g == n)), "every variant names a pinned case");
+}
+
+/// Hashes also accepted for a case: the same frame rendered by the same code with another macOS
+/// release's libm (case, hash, where it was seen).
+const LIBM_VARIANTS: &[(&str, u64, &str)] = &[
+ // #232: the pinning commit (ce6ece4) renders byte-identical frames there.
+ ("ec.sim.ccmrmercury/1/32bpc/t2.2", 0x3823bd73a3cdab0d, "macOS 27.2 (26B5091g), rustc 1.98.0"),
+];
+
/// Pinned hashes (aarch64 macOS).
const GOLDEN: &[(&str, u64)] = &[
("ec.sim.ccrainfall/0/8bpc/t0.5", 0xc9f1eb02dd83841c),
diff --git a/docs/testing.md b/docs/testing.md
index cb90185f..4aa8514a 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -19,7 +19,8 @@ cargo xtask ci # fmt, clippy -D warnings, tests, layers, assets, was
which read the clock are declared time-dependent. `crates/effects/tests/sim_golden.rs` pins
exact pixel hashes of the simulation effects the GPU shares plans with (CC Rainfall … Card
Wipe) at several settings, times, bit depths and resolutions, so any change to their CPU output
- fails (re-pin with `SIM_GOLDEN_PRINT=1` after an intended change; pinned for aarch64 macOS).
+ fails (re-pin with `SIM_GOLDEN_PRINT=1` after an intended change; pinned for aarch64 macOS,
+ with the hashes another macOS release's libm gives for unchanged code in `LIBM_VARIANTS`).
- **Export** (`crates/export/tests`): every format is encoded and decoded back, checking frame
count, size and pixels. When `ffmpeg`/`ffprobe` are installed they are used as an outside
check; they are never linked or shipped.