use sha2::{Digest, Sha256};
use crate::angle::{
canon_angle_deg, quat_from_euler_xyz, quat_from_rotation_x, quat_from_rotation_y,
quat_from_rotation_z, CANON_ANGLE_DEG_PROBE_VALUES, EULER_XYZ_PROBE_TABLE,
ROTATION_AXIS_PROBE_TABLE,
};
use crate::emit::{fmt_f32, FMT_F32_FIDELITY_PROBE_VALUES};
use crate::law_id::{hex32, same32};
use crate::limits::{
checked_rotation_quat, checked_tpms_fields, ROTATION_QUAT_BOUNDARY_PROBE_INPUTS,
TPMS_VALIDITY_PROBE_INPUTS,
};
const GEOMETRY_DOMAIN_TAG: &[u8] = b"lol.geometry";
fn length_prefixed(h: &mut Sha256, bytes: &[u8]) {
h.update((bytes.len() as u64).to_le_bytes());
h.update(bytes);
}
#[must_use]
pub fn fold_geometry_pins(pins: &[(&str, [u8; 32])]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(crate::law_id::LAW_ID_DOMAIN);
h.update(GEOMETRY_DOMAIN_TAG);
for (name, id) in pins {
length_prefixed(&mut h, name.as_bytes());
h.update(id);
}
h.finalize().into()
}
fn dense_euler_grid(n: usize) -> impl Iterator<Item = (f32, f32, f32)> {
debug_assert!(n >= 2, "need at least 2 steps to include both endpoints");
#[allow(clippy::cast_precision_loss)]
let step = move |i: usize| -> f32 {
let t = i as f32 / (n - 1) as f32; (2.0 * std::f32::consts::PI).mul_add(t, -std::f32::consts::PI)
};
(0..n).flat_map(move |i| {
(0..n).flat_map(move |j| (0..n).map(move |k| (step(i), step(j), step(k))))
})
}
fn signed_zero_euler_lattice() -> impl Iterator<Item = (f32, f32, f32)> {
use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, FRAC_PI_6, PI};
const L: [f32; 10] = [
0.0, -0.0, FRAC_PI_6, -FRAC_PI_6, FRAC_PI_4, -FRAC_PI_4, FRAC_PI_2, -FRAC_PI_2, PI, -PI,
];
L.into_iter().flat_map(|x| {
L.into_iter()
.flat_map(move |y| L.into_iter().map(move |z| (x, y, z)))
})
}
fn euler_xyz_quat_probe_digest_of(f: fn(f32, f32, f32) -> glam::Quat) -> [u8; 32] {
let mut h = Sha256::new();
let mut put = |q: glam::Quat| {
h.update(q.x.to_bits().to_le_bytes());
h.update(q.y.to_bits().to_le_bytes());
h.update(q.z.to_bits().to_le_bytes());
h.update(q.w.to_bits().to_le_bytes());
};
for &((x, y, z), _) in EULER_XYZ_PROBE_TABLE {
put(f(x, y, z));
}
for (x, y, z) in dense_euler_grid(15) {
put(f(x, y, z));
}
for (x, y, z) in signed_zero_euler_lattice() {
put(f(x, y, z));
}
h.finalize().into()
}
#[must_use]
pub fn euler_xyz_quat_probe_digest() -> [u8; 32] {
euler_xyz_quat_probe_digest_of(quat_from_euler_xyz)
}
#[allow(clippy::cast_precision_loss)] fn dense_canon_angle_deg_sweep(n: usize) -> impl Iterator<Item = f32> {
debug_assert!(n >= 2, "need at least 2 steps to include both endpoints");
(0..n).map(move |i| {
let t = i as f32 / (n - 1) as f32; 2160.0_f32.mul_add(t, -1080.0)
})
}
const fn canonical_bits(x: f32) -> u32 {
if x.is_nan() {
0x7fc0_0000
} else {
x.to_bits()
}
}
#[must_use]
pub fn canon_angle_deg_probe_digest() -> [u8; 32] {
let mut h = Sha256::new();
for ° in CANON_ANGLE_DEG_PROBE_VALUES {
h.update(canonical_bits(canon_angle_deg(deg)).to_le_bytes());
}
for deg in dense_canon_angle_deg_sweep(2161) {
h.update(canonical_bits(canon_angle_deg(deg)).to_le_bytes());
}
h.finalize().into()
}
#[must_use]
pub fn rotation_axis_quat_probe_digest() -> [u8; 32] {
let mut h = Sha256::new();
for &(a, ..) in ROTATION_AXIS_PROBE_TABLE {
for q in [
quat_from_rotation_x(a),
quat_from_rotation_y(a),
quat_from_rotation_z(a),
] {
h.update(q.x.to_bits().to_le_bytes());
h.update(q.y.to_bits().to_le_bytes());
h.update(q.z.to_bits().to_le_bytes());
h.update(q.w.to_bits().to_le_bytes());
}
}
h.finalize().into()
}
#[must_use]
pub fn rotation_quat_boundary_probe_digest() -> [u8; 32] {
let mut h = Sha256::new();
for &(x, y, z, w) in ROTATION_QUAT_BOUNDARY_PROBE_INPUTS {
match checked_rotation_quat(x, y, z, w) {
Ok(q) => {
h.update([1u8]);
h.update(q.x.to_bits().to_le_bytes());
h.update(q.y.to_bits().to_le_bytes());
h.update(q.z.to_bits().to_le_bytes());
h.update(q.w.to_bits().to_le_bytes());
}
Err(_) => h.update([0u8]),
}
}
h.finalize().into()
}
#[must_use]
pub fn tpms_validity_probe_digest() -> [u8; 32] {
let mut h = Sha256::new();
for &(scale, thickness) in TPMS_VALIDITY_PROBE_INPUTS {
match checked_tpms_fields(scale, thickness) {
Ok((s, t)) => {
h.update([1u8]);
h.update(s.to_bits().to_le_bytes());
h.update(t.to_bits().to_le_bytes());
}
Err(_) => h.update([0u8]),
}
}
h.finalize().into()
}
#[must_use]
pub fn fmt_f32_fidelity_probe_digest() -> [u8; 32] {
let mut h = Sha256::new();
for &v in FMT_F32_FIDELITY_PROBE_VALUES {
length_prefixed(&mut h, fmt_f32(v).as_bytes());
}
h.finalize().into()
}
const STDLIB_TRANSCENDENTAL_PROBE_LOL: &[&str] = &[
"skadis_hook_s()",
"skadis_hook_j()",
"countersink(4, 6)",
"dovetail(10, 6, 8)",
"phone_dock(80, 100, 6)",
];
#[must_use]
pub fn stdlib_transcendental_probe_digest() -> [u8; 32] {
#[allow(clippy::cast_precision_loss)] let coord = |i: usize| -> f32 { 80.0_f32.mul_add(i as f32 / 16.0, -40.0) };
let mut h = Sha256::new();
for lol in STDLIB_TRANSCENDENTAL_PROBE_LOL {
let node = crate::runtime_parser::parse_lol(lol)
.unwrap_or_else(|e| panic!("stdlib probe {lol:?} does not parse: {e:?}"));
length_prefixed(&mut h, lol.as_bytes());
for i in 0..17 {
for j in 0..17 {
for k in 0..17 {
let p = glam::Vec3::new(coord(i), coord(j), coord(k));
h.update(alice_sdf::eval(&node, p).to_bits().to_le_bytes());
}
}
}
}
h.finalize().into()
}
pub const LOL_GEOMETRY_SEMANTICS_PINS: &[(&str, [u8; 32])] = &[
("alice-det-math", alice_det_math::SEMANTICS_ID),
(
"rotate: angle canonicalisation",
hex32("ecff9d3055efcb57e6cac095b88ed1207310f6f36d42f4945ae07862a792b9a1"),
),
(
"rotate: euler-to-quat arithmetic",
hex32("c9a026178c53c29c8ccdb5413fd1d66ce51e0703ea7ec4e3c82d86ef505030de"),
),
(
"rotate: single-axis quaternion arithmetic",
hex32("9100865661ac31560c6ded8464a4f5d906ce3cbb972730fc836e574f0ca55d14"),
),
(
"rotation quat: unit-length acceptance boundary",
hex32("42bb92427d0f152ecffaab3f3e2e2eb56aac18bca3d7604c3ccbf227197fda41"),
),
(
"tpms: scale/thickness validity range",
hex32("cfb95b75cfc0d47ed745c31e963d37bb6ec5d605d5584ea0b636413f72e008c1"),
),
(
"stdlib: transcendental routing through alice_det_math",
hex32("e161067d3da77c1ad3449f03a5108f8124e9a352513ad922e99fda7429f50d44"),
),
(
"emit: fmt_f32 round-trip fidelity class",
hex32("4744995b02057f35d13e18e149c8f1d9880cf0b3f4ebab3515f5529d99a70553"),
),
];
pub const LOL_GEOMETRY_SEMANTICS_BY_DET_MATH: &[([u8; 32], [u8; 32])] = &[
(
hex32("135c407d4249c1c200e4d1be79a12e848ce5c355fc3a918da965849e697ef000"),
hex32("251e73bf84e00c4c1916184bdcbbb75e5cf3f74b62fd4adcbc3922dd18df907d"),
),
(
hex32("85e0884623b154a3b2b264374907b6cbac09307ca3f9a08339e48c567d3cb315"),
hex32("d1cbba69a1b33ce7363ac178f16ad0cb062699ff8c9c76af7058c6d006bc8a24"),
),
];
pub const LOL_GEOMETRY_SEMANTICS_ID: [u8; 32] =
geometry_semantics_id_for(&alice_det_math::SEMANTICS_ID);
#[must_use]
pub const fn geometry_semantics_id_for(det_math: &[u8; 32]) -> [u8; 32] {
let mut i = 0;
while i < LOL_GEOMETRY_SEMANTICS_BY_DET_MATH.len() {
if same32(&LOL_GEOMETRY_SEMANTICS_BY_DET_MATH[i].0, det_math) {
return LOL_GEOMETRY_SEMANTICS_BY_DET_MATH[i].1;
}
i += 1;
}
panic!("alice-det-math SEMANTICS_ID has no row in LOL_GEOMETRY_SEMANTICS_BY_DET_MATH: add its fold");
}
#[must_use]
pub fn known_geometry_semantics_id(id: &[u8; 32]) -> Option<[u8; 32]> {
LOL_GEOMETRY_SEMANTICS_BY_DET_MATH
.iter()
.find(|(_, g)| g == id)
.map(|(det_math, _)| *det_math)
}
#[cfg(test)]
mod tests {
use super::{euler_xyz_quat_probe_digest, euler_xyz_quat_probe_digest_of};
use std::hint::black_box;
fn parts(x: f32, y: f32, z: f32) -> (f32, f32, f32, f32, f32, f32) {
let (si, ci) = alice_det_math::sin_cos(x * 0.5);
let (sj, cj) = alice_det_math::sin_cos((-y) * 0.5);
let (sh, ch) = alice_det_math::sin_cos(z * 0.5);
(cj, sj, ci * ch, ci * sh, si * ch, si * sh)
}
fn outer_negated_fma(x: f32, y: f32, z: f32) -> glam::Quat {
let (cj, sj, cc, cs, sc, ss) = parts(x, y, z);
glam::Quat::from_xyzw(
cj.mul_add(sc, -(sj * cs)),
-black_box(cj.mul_add(ss, sj * cc)),
cj.mul_add(cs, -(sj * sc)),
cj.mul_add(cc, sj * ss),
)
}
#[allow(clippy::suboptimal_flops)] fn unfused_y(x: f32, y: f32, z: f32) -> glam::Quat {
let (cj, sj, cc, cs, sc, ss) = parts(x, y, z);
glam::Quat::from_xyzw(
cj.mul_add(sc, -(sj * cs)),
-(cj * ss + sj * cc),
cj.mul_add(cs, -(sj * sc)),
cj.mul_add(cc, sj * ss),
)
}
#[allow(clippy::suboptimal_flops)] fn unfused_x(x: f32, y: f32, z: f32) -> glam::Quat {
let (cj, sj, cc, cs, sc, ss) = parts(x, y, z);
glam::Quat::from_xyzw(
cj * sc - sj * cs,
(-cj).mul_add(ss, -(sj * cc)),
cj.mul_add(cs, -(sj * sc)),
cj.mul_add(cc, sj * ss),
)
}
#[allow(
clippy::disallowed_methods,
reason = "test-probe-alternative: the platform libm is the alternative under test, not production code"
)]
fn platform_trig(x: f32, y: f32, z: f32) -> glam::Quat {
let (si, ci) = black_box(x * 0.5).sin_cos();
let (sj, cj) = black_box((-y) * 0.5).sin_cos();
let (sh, ch) = black_box(z * 0.5).sin_cos();
let (cc, cs, sc, ss) = (ci * ch, ci * sh, si * ch, si * sh);
glam::Quat::from_xyzw(
cj.mul_add(sc, -(sj * cs)),
(-cj).mul_add(ss, -(sj * cc)),
cj.mul_add(cs, -(sj * sc)),
cj.mul_add(cc, sj * ss),
)
}
#[test]
fn the_euler_pin_is_the_digest_of_the_real_builder() {
assert_eq!(
euler_xyz_quat_probe_digest_of(crate::angle::quat_from_euler_xyz),
euler_xyz_quat_probe_digest()
);
}
#[test]
fn the_euler_pin_moves_for_each_arithmetic_change() {
let real = euler_xyz_quat_probe_digest();
for (name, alt) in [
(
"outer-negated fma (signed zero)",
outer_negated_fma as fn(f32, f32, f32) -> glam::Quat,
),
("unfused y (signed zero + double rounding)", unfused_y),
("unfused x (fused -> unfused)", unfused_x),
("platform trig instead of alice_det_math", platform_trig),
] {
assert_ne!(
euler_xyz_quat_probe_digest_of(alt),
real,
"{name}: the Euler pin did not move"
);
}
}
#[test]
fn the_signed_zero_lattice_alone_sees_the_zero_sign() {
let lattice_only = |f: fn(f32, f32, f32) -> glam::Quat| -> Vec<u32> {
super::signed_zero_euler_lattice()
.map(|(x, y, z)| f(x, y, z).y.to_bits())
.collect()
};
let real = lattice_only(crate::angle::quat_from_euler_xyz);
let alt = lattice_only(outer_negated_fma);
let differ = real.iter().zip(&alt).filter(|(a, b)| a != b).count();
assert!(differ > 0, "no lattice point separates +0 from -0");
assert!(
real.iter()
.zip(&alt)
.all(|(a, b)| a == b || (a ^ b) == 0x8000_0000),
"outside the zero sign the two forms should agree on the lattice"
);
}
}