Merge branch 'parity-base' into parity/server-leftovers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-13 05:44:19 +02:00
29 changed files with 732 additions and 59 deletions
Generated
+3
View File
@@ -700,6 +700,9 @@ dependencies = [
[[package]]
name = "horizon-core"
version = "0.1.0"
dependencies = [
"libm",
]
[[package]]
name = "horizon-protocol"
+1 -1
View File
@@ -78,7 +78,7 @@ pub fn extract(
|| z > max_z + render_distance;
if !deep_inside && !far_outside {
let alpha = 1.0 - border.distance(x, z) / render_distance;
state.alpha = jclamp(alpha.powf(4.0), 0.0, 1.0);
state.alpha = jclamp(horizon_core::jmath::pow(alpha, 4.0), 0.0, 1.0);
state.tint = status_color(border.status());
} else {
state.alpha = 0.0;
@@ -21,6 +21,7 @@ impl Game<'_> {
let mut quiet_since = Instant::now();
let mut step = 0;
let mut step_at = Instant::now();
let mut chat_at = 0;
let mut position = None;
let mut chunks = 0;
let mut error = None;
@@ -44,6 +45,10 @@ impl Game<'_> {
last_chunks = count;
quiet_since = now;
}
// Each step waits for what it sent to be answered (its chat line,
// the /list reply), however busy the machine, up to 15 s.
let shown = self.remote.as_ref().map_or(0, |r| r.chat.len());
let answered = shown > chat_at || now - step_at > Duration::from_secs(15);
match step {
0 if spawned_at.is_some()
&& count > 0
@@ -53,13 +58,15 @@ impl Game<'_> {
self.send_chat("hello from the horizon client");
step = 1;
step_at = now;
chat_at = shown;
}
1 if now - step_at > Duration::from_millis(1500) => {
1 if answered => {
self.send_chat("/list");
step = 2;
step_at = now;
chat_at = shown;
}
2 if now - step_at > Duration::from_millis(1500) => {
2 if answered => {
step = 3;
break;
}
@@ -742,7 +742,7 @@ impl Gate {
// current order, then a stable sort.
for k in &mut self.kids {
let r = self.random.next_float();
k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32));
k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32));
}
self.kids.sort_by(|a, b| a.2.total_cmp(&b.2));
for k in &mut self.kids {
+5 -5
View File
@@ -6,7 +6,7 @@ use super::{
anim::{Poses, State},
keyframes::Animation,
};
use horizon_core::{geometry::Vec3, random::RandomSource};
use horizon_core::{geometry::Vec3, jmath, random::RandomSource};
use std::collections::HashMap;
// `variant3` is the pose, `variant` the pose the animations last saw,
@@ -96,9 +96,9 @@ fn jump_vector(m: &Mob, target: Vec3, max_velocity: f32, angle: i32) -> Option<V
let r = r2.sqrt();
let y = d.y;
let g = m.gravity();
let sin2 = f64::from(2.0 * angrad).sin();
let cos_sq = f64::from(angrad).cos().powf(2.0);
let (sin, cos) = (f64::from(angrad).sin(), f64::from(angrad).cos());
let sin2 = jmath::sin(f64::from(2.0 * angrad));
let cos_sq = jmath::pow(jmath::cos(f64::from(angrad)), 2.0);
let (sin, cos) = (jmath::sin(f64::from(angrad)), jmath::cos(f64::from(angrad)));
let v0sq = r2 * g / (r * sin2 - 2.0 * y * cos_sq);
if v0sq < 0.0 {
return None;
@@ -108,7 +108,7 @@ fn jump_vector(m: &Mob, target: Vec3, max_velocity: f32, angle: i32) -> Option<V
return None;
}
let (v0r, v0y) = (v0 * cos, v0 * sin);
Some(Vec3::new(v0r * xz.cos(), v0y, v0r * xz.sin()).scale(f64::from(0.95_f32)))
Some(Vec3::new(v0r * jmath::cos(xz), v0y, v0r * jmath::sin(xz)).scale(f64::from(0.95_f32)))
}
/// `LongJump.calculateOptimalJumpVector`: the allowed angles shuffled, the
/// first that reaches the target.
+4 -4
View File
@@ -1,7 +1,7 @@
//! Vanilla's `GoalSelector` (priorities, MOVE/LOOK/JUMP/TARGET control flags,
//! interruption) and the shared goals of `net.minecraft.world.entity.ai.goal`.
use super::{Mob, MobEvent, MobWorld, Seen, Species, Who, world::TargetInfo};
use horizon_core::{geometry::Vec3, random::RandomSource};
use horizon_core::{geometry::Vec3, jmath, random::RandomSource};
use std::sync::Arc;
pub const MOVE: u8 = 1;
@@ -208,8 +208,8 @@ impl Mob {
let yrad = center + f64::from(2.0 * self.random.next_float() - 1.0) * max_rad;
let dist = (min + self.random.next_double().sqrt() * (max - min))
* f64::from(std::f32::consts::SQRT_2);
let xt = -dist * yrad.sin();
let zt = dist * yrad.cos();
let xt = -dist * jmath::sin(yrad);
let zt = dist * jmath::cos(yrad);
if xt.abs() > max || zt.abs() > max {
return None;
}
@@ -934,7 +934,7 @@ impl Goal for RandomLookAround {
}
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
let r = std::f64::consts::TAU * m.random.next_double();
self.rel = [r.cos(), r.sin()];
self.rel = [jmath::cos(r), jmath::sin(r)];
self.time = 20 + m.random.next_int_bound(20).unwrap_or(0);
}
fn every_tick(&self) -> bool {
@@ -550,7 +550,7 @@ impl RunOne {
// order, then a stable sort.
for k in &mut self.kids {
let r = self.random.next_float();
k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32));
k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32));
}
self.kids.sort_by(|a, b| a.2.total_cmp(&b.2));
for k in &mut self.kids {
@@ -1105,7 +1105,7 @@ impl Gate {
// current order, then a stable sort.
for k in &mut self.kids {
let r = self.random.next_float();
k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32));
k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32));
}
self.kids.sort_by(|a, b| a.2.total_cmp(&b.2));
}
@@ -2396,7 +2396,7 @@ impl B {
} => {
for k in kids.iter_mut() {
let r = random.next_float();
k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32));
k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32));
}
kids.sort_by(|a, b| a.2.total_cmp(&b.2));
for k in kids.iter_mut() {
@@ -844,7 +844,7 @@ impl B {
// current order, then a stable sort.
for k in &mut g.kids {
let r = g.random.next_float();
k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32));
k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32));
}
g.kids.sort_by(|a, b| a.2.total_cmp(&b.2));
for k in &mut g.kids {
+2 -2
View File
@@ -298,7 +298,7 @@ fn double_range(v: f64, lo: f64, hi: f64) -> Option<f64> {
(v.total_cmp(&lo).is_ge() && v.total_cmp(&hi).is_le() && !v.is_nan()).then_some(v)
}
fn log_mouse(value: i32) -> f64 {
10f64.powf(f64::from(value) / 100.0)
horizon_core::jmath::pow(10.0, f64::from(value) / 100.0)
}
fn unit_alt(json: &Json) -> Option<f64> {
codec_double(json)
@@ -366,7 +366,7 @@ impl Spec {
}
(Spec::Fov, V::Int(x)) => (30..=110).contains(x).then(|| v.clone()),
(Spec::WheelSensitivity, V::Double(x)) => {
let i = floor_int(x.log10() * 100.0);
let i = floor_int(horizon_core::jmath::log10(*x) * 100.0);
(-200..=100).contains(&i).then(|| V::Double(log_mouse(i)))
}
(Spec::Unit, V::Double(x)) => (*x >= 0.0 && *x <= 1.0).then(|| v.clone()),
+3
View File
@@ -10,6 +10,9 @@ description = "Horizon simulation foundations for the pinned vanilla Java Editio
# Records the call sites of LegacyRandom draws (mob AI parity debugging).
rng-trace = []
[dependencies]
libm = "=0.2.16"
[[bin]]
name = "horizon-parity"
path = "src/bin/parity.rs"
@@ -8,7 +8,7 @@ pub(super) fn java_power(base: f64, exponent: f64) -> f64 {
} else if exponent.is_nan() || (base.abs() == 1.0 && exponent.is_infinite()) {
f64::NAN
} else {
base.powf(exponent)
crate::jmath::pow(base, exponent)
}
}
@@ -83,7 +83,7 @@ impl UnaryOp {
Self::Square => value * value,
Self::Cube => value * value * value,
Self::Sqrt => (value as f64).sqrt() as f32,
Self::Log => (value as f64).ln() as f32,
Self::Log => crate::jmath::log(value as f64) as f32,
Self::PowerBase(base) => java_power(base, value as f64) as f32,
Self::PowerExponent(exponent) => java_power(value as f64, exponent) as f32,
Self::Reciprocal => 1.0 / value,
@@ -208,7 +208,7 @@ fn kernel() -> &'static [f32; 13824] {
let x = ((i / 24) % 24) as f64 - 12.;
let y = (i % 24) as f64 - 12. + 0.5;
let squared = (x * x + y * y) + z * z;
std::f64::consts::E.powf(-squared / 16.) as f32
crate::jmath::pow(std::f64::consts::E, -squared / 16.) as f32
})
})
}
+1 -1
View File
@@ -189,7 +189,7 @@ impl Interval {
Ok(Self::NAI)
} else {
self.maximum(Self::exact(0.0)?)
.map_monotonic(|v| (v as f64).ln() as f32)
.map_monotonic(|v| crate::jmath::log(v as f64) as f32)
}
}
pub fn power(self, exponent: Self) -> Self {
+617
View File
@@ -0,0 +1,617 @@
//! `java.lang.Math` as HotSpot computes it on the machine Horizon runs on.
//!
//! Rust's float functions call the platform's libm, and those round
//! differently: glibc, Apple's libm and fdlibm disagree in the last bit for
//! a few percent of arguments. Vanilla's own results differ between machines
//! too, because HotSpot replaces some `Math` methods with intrinsics:
//!
//! - `atan2`, `atan`, `asin`, `acos` and `hypot` are `StrictMath`'s (fdlibm)
//! everywhere.
//! - On x86-64, `sin`, `cos`, `tan`, `exp`, `log`, `log10` and `pow` are
//! Intel's libm stubs, which round like glibc for nearly every argument;
//! there they use the platform libm, as before.
//! - On AArch64 only `sin` and `cos` are intrinsics: fdlibm's kernels
//! evaluated with fused multiply-adds (`macroAssembler_aarch64_trig.cpp`).
//! Everything else is `StrictMath`.
//!
//! So these functions return what vanilla returns on the same machine, on
//! Linux and on an Apple silicon Mac alike.
/// `Math.sin`.
pub fn sin(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
aarch64::sin(x)
} else {
x.sin()
}
}
/// `Math.cos`.
pub fn cos(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
aarch64::cos(x)
} else {
x.cos()
}
}
/// `Math.tan`.
pub fn tan(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
libm::tan(x)
} else {
x.tan()
}
}
/// `Math.exp`.
pub fn exp(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
libm::exp(x)
} else {
x.exp()
}
}
/// `Math.log`.
pub fn log(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
fdlibm::log(x)
} else {
x.ln()
}
}
/// `Math.log10`.
pub fn log10(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
fdlibm::log10(x)
} else {
x.log10()
}
}
/// `Math.pow`.
pub fn pow(x: f64, y: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
libm::pow(x, y)
} else {
x.powf(y)
}
}
/// `Math.cbrt`.
pub fn cbrt(x: f64) -> f64 {
if cfg!(target_arch = "aarch64") {
fdlibm::cbrt(x)
} else {
x.cbrt()
}
}
/// `Math.atan2`.
pub fn atan2(y: f64, x: f64) -> f64 {
libm::atan2(y, x)
}
/// `Math.atan`.
pub fn atan(x: f64) -> f64 {
libm::atan(x)
}
/// `Math.asin`.
pub fn asin(x: f64) -> f64 {
libm::asin(x)
}
/// `Math.acos`.
pub fn acos(x: f64) -> f64 {
libm::acos(x)
}
/// `Math.hypot`.
pub fn hypot(x: f64, y: f64) -> f64 {
fdlibm::hypot(x, y)
}
fn hi(x: f64) -> i32 {
(x.to_bits() >> 32) as i32
}
fn with_hi(x: f64, high: i32) -> f64 {
f64::from_bits((x.to_bits() & 0xffff_ffff) | (u64::from(high as u32) << 32))
}
/// `java.lang.FdLibm` ports (`StrictMath`) of what the `libm` crate computes
/// differently.
// fdlibm's constants and NaN, verbatim.
#[allow(clippy::excessive_precision, clippy::approx_constant, clippy::eq_op)]
pub mod fdlibm {
use super::{hi, with_hi};
const TWO54: f64 = 1.80143985094819840000e+16;
/// `FdLibm.Log`.
pub fn log(mut x: f64) -> f64 {
const LN2_HI: f64 = 6.93147180369123816490e-01;
const LN2_LO: f64 = 1.90821492927058770002e-10;
const LG1: f64 = 6.666666666666735130e-01;
const LG2: f64 = 3.999999999940941908e-01;
const LG3: f64 = 2.857142874366239149e-01;
const LG4: f64 = 2.222219843214978396e-01;
const LG5: f64 = 1.818357216161805012e-01;
const LG6: f64 = 1.531383769920937332e-01;
const LG7: f64 = 1.479819860511658591e-01;
let mut hx = hi(x);
let lx = x.to_bits() as u32;
let mut k = 0;
if hx < 0x0010_0000 {
if ((hx & 0x7fff_ffff) as u32 | lx) == 0 {
return -TWO54 / 0.0;
}
if hx < 0 {
return (x - x) / 0.0;
}
k -= 54;
x *= TWO54;
hx = hi(x);
}
if hx >= 0x7ff0_0000 {
return x + x;
}
k += (hx >> 20) - 1023;
hx &= 0x000f_ffff;
let i = (hx + 0x9_5f64) & 0x10_0000;
x = with_hi(x, hx | (i ^ 0x3ff0_0000));
k += i >> 20;
let f = x - 1.0;
if (0x000f_ffff & (2 + hx)) < 3 {
if f == 0.0 {
if k == 0 {
return 0.0;
}
let dk = f64::from(k);
return dk * LN2_HI + dk * LN2_LO;
}
let r = f * f * (0.5 - 0.33333333333333333 * f);
if k == 0 {
return f - r;
}
let dk = f64::from(k);
return dk * LN2_HI - ((r - dk * LN2_LO) - f);
}
let s = f / (2.0 + f);
let dk = f64::from(k);
let z = s * s;
let mut i = hx - 0x6_147a;
let w = z * z;
let j = 0x6b851 - hx;
let t1 = w * (LG2 + w * (LG4 + w * LG6));
let t2 = z * (LG1 + w * (LG3 + w * (LG5 + w * LG7)));
i |= j;
let r = t2 + t1;
if i > 0 {
let hfsq = 0.5 * f * f;
if k == 0 {
f - (hfsq - s * (hfsq + r))
} else {
dk * LN2_HI - ((hfsq - (s * (hfsq + r) + dk * LN2_LO)) - f)
}
} else if k == 0 {
f - s * (f - r)
} else {
dk * LN2_HI - ((s * (f - r) - dk * LN2_LO) - f)
}
}
/// `FdLibm.Log10`.
pub fn log10(mut x: f64) -> f64 {
const IVLN10: f64 = 4.34294481903251816668e-01;
const LOG10_2HI: f64 = 3.01029995663611771306e-01;
const LOG10_2LO: f64 = 3.69423907715893078616e-13;
let mut hx = hi(x);
let lx = x.to_bits() as u32;
let mut k = 0;
if hx < 0x0010_0000 {
if ((hx & 0x7fff_ffff) as u32 | lx) == 0 {
return -TWO54 / 0.0;
}
if hx < 0 {
return (x - x) / 0.0;
}
k -= 54;
x *= TWO54;
hx = hi(x);
}
if hx >= 0x7ff0_0000 {
return x + x;
}
k += (hx >> 20) - 1023;
let i = ((k as u32) >> 31) as i32;
hx = (hx & 0x000f_ffff) | ((0x3ff - i) << 20);
let y = f64::from(k + i);
x = with_hi(x, hx);
let z = y * LOG10_2LO + IVLN10 * log(x);
z + y * LOG10_2HI
}
/// `FdLibm.Cbrt`.
pub fn cbrt(mut x: f64) -> f64 {
const B1: i32 = 715094163;
const B2: i32 = 696219795;
const C: f64 = 5.42857142857142815906e-01;
const D: f64 = -7.05306122448979611050e-01;
const E: f64 = 1.41428571428571436819e+00;
const F: f64 = 1.60714285714285720630e+00;
const G: f64 = 3.57142857142857150787e-01;
if x == 0.0 || !x.is_finite() {
return x;
}
let sign = if x < 0.0 { -1.0 } else { 1.0 };
x = x.abs();
let mut t = 0.0f64;
if x < f64::MIN_POSITIVE {
t = 18014398509481984.0; // 2^54
t *= x;
t = with_hi(t, hi(t) / 3 + B2);
} else {
t = with_hi(t, hi(x) / 3 + B1);
}
let mut r = t * t / x;
let mut s = C + r * t;
t *= G + F / (s + E + D / s);
t = f64::from_bits(t.to_bits() & 0xffff_ffff_0000_0000);
t = with_hi(t, hi(t) + 1);
s = t * t;
r = x / s;
let w = t + t;
r = (r - t) / (w + r);
t += t * r;
sign * t
}
/// `FdLibm.Hypot`.
pub fn hypot(x: f64, y: f64) -> f64 {
let mut a = x.abs();
let mut b = y.abs();
if !a.is_finite() || !b.is_finite() {
if a == f64::INFINITY || b == f64::INFINITY {
return f64::INFINITY;
}
return a + b;
}
if b > a {
std::mem::swap(&mut a, &mut b);
}
let mut ha = hi(a);
let mut hb = hi(b);
if ha - hb > 0x3c0_0000 {
return a + b;
}
let mut k = 0;
if a > f64::from_bits(0x5f30_0000_ffff_ffff) {
ha -= 0x2580_0000;
hb -= 0x2580_0000;
a *= f64::from_bits(0x1a70_0000_0000_0000); // 2^-600
b *= f64::from_bits(0x1a70_0000_0000_0000);
k += 600;
}
if b < f64::from_bits(0x20b0_0000_0000_0000) {
// 2^-500
if b < f64::MIN_POSITIVE {
if b == 0.0 {
return a;
}
let t1 = f64::from_bits(0x7fd0_0000_0000_0000); // 2^1022
b *= t1;
a *= t1;
k -= 1022;
} else {
ha += 0x2580_0000;
hb += 0x2580_0000;
a *= f64::from_bits(0x6570_0000_0000_0000); // 2^600
b *= f64::from_bits(0x6570_0000_0000_0000);
k -= 600;
}
}
let mut w = a - b;
if w > b {
let t1 = with_hi(0.0, ha);
let t2 = a - t1;
w = (t1 * t1 - (b * (-b) - t2 * (a + t1))).sqrt();
} else {
a += a;
let y1 = with_hi(0.0, hb);
let y2 = b - y1;
let t1 = with_hi(0.0, ha + 0x0010_0000);
let t2 = a - t1;
w = (t1 * y1 - (w * (-w) - (t1 * y2 + t2 * b))).sqrt();
}
if k != 0 {
f64::from_bits(((1023 + k) as u64) << 52) * w
} else {
w
}
}
}
/// HotSpot's AArch64 `Math.sin` / `Math.cos` stubs
/// (`MacroAssembler::generate_dsin_dcos`): fdlibm with fused multiply-adds.
/// Exposed on every architecture so they can be tested anywhere.
// HotSpot's tables, verbatim.
#[allow(
clippy::excessive_precision,
clippy::approx_constant,
clippy::unreadable_literal
)]
pub mod aarch64 {
use super::hi;
const S: [f64; 6] = [
-1.66666666666666324348e-01,
8.33333333332248946124e-03,
-1.98412698298579493134e-04,
2.75573137070700676789e-06,
-2.50507602534068634195e-08,
1.58969099521155010221e-10,
];
const C: [f64; 6] = [
4.16666666666666019037e-02,
-1.38888888888741095749e-03,
2.48015872894767294178e-05,
-2.75573143513906633035e-07,
2.08757232129817482790e-09,
-1.13596475577881948265e-11,
];
const INVPIO2: f64 = 6.36619772367581382433e-01;
const PIO2_1: f64 = 1.57079632673412561417e+00;
const PIO2_1T: f64 = 6.07710050650619224932e-11;
const PIO2_2: f64 = 6.07710050630396597660e-11;
const PIO2_2T: f64 = 2.02226624879595063154e-21;
const PIO2_3: f64 = 2.02226624871116645580e-21;
const PIO2_3T: f64 = 8.47842766036889956997e-32;
const NPIO2_HW: [i32; 32] = [
0x3FF921FB, 0x400921FB, 0x4012D97C, 0x401921FB, 0x401F6A7A, 0x4022D97C, 0x4025FDBB,
0x402921FB, 0x402C463A, 0x402F6A7A, 0x4031475C, 0x4032D97C, 0x40346B9C, 0x4035FDBB,
0x40378FDB, 0x403921FB, 0x403AB41B, 0x403C463A, 0x403DD85A, 0x403F6A7A, 0x40407E4C,
0x4041475C, 0x4042106C, 0x4042D97C, 0x4043A28C, 0x40446B9C, 0x404534AC, 0x4045FDBB,
0x4046C6CB, 0x40478FDB, 0x404858EB, 0x404921FB,
];
/// `generate_kernel_sin`.
fn kernel_sin(x: f64, y: f64, iy: bool) -> f64 {
let z = x * x;
let v = z * x;
let mut r = z.mul_add(S[5], S[4]);
r = z.mul_add(r, S[3]);
r = z.mul_add(r, S[2]);
r = z.mul_add(r, S[1]);
if !iy {
v.mul_add(z.mul_add(r, S[0]), x)
} else {
let t = (-v).mul_add(r, 0.5 * y);
let t = (-z).mul_add(t, y);
x + v.mul_add(S[0], t)
}
}
/// `generate_kernel_cos`. `ix` is the high word of the *original*
/// argument: the stub reuses the register, so a reduced argument always
/// takes the `qx = 0.28125` branch.
fn kernel_cos(x: f64, y: f64, ix: i32) -> f64 {
let z = x * x;
let mut r = z.mul_add(C[5], C[4]);
r = z.mul_add(r, C[3]);
let xy = x * y;
r = z.mul_add(r, C[2]);
r = z.mul_add(r, C[1]);
let zz = z * z;
r = z.mul_add(r, C[0]);
if ix <= 0x3FD33333 {
let v = (-zz).mul_add(r, xy);
1.0 - 0.5f64.mul_add(z, v)
} else {
let qx = if ix > 0x3fe90000 {
0.28125
} else {
f64::from_bits(u64::from((ix - 0x0020_0000) as u32) << 32)
};
let zr_xy = zz.mul_add(r, -xy);
let h = 0.5f64.mul_add(z, -qx);
let a = 1.0 - qx;
a - (h - zr_xy)
}
}
/// `generate__ieee754_rem_pio2` for |x| <= 2^19·π/2; `None` beyond.
fn rem_pio2(x: f64, ix: i32) -> Option<(i32, f64, f64)> {
if ix < 0x4002d97c {
return Some(if x.to_bits() as i64 >= 0 {
let z = x - PIO2_1;
if ix != 0x3ff921fb {
let y0 = z - PIO2_1T;
(1, y0, (z - y0) - PIO2_1T)
} else {
let z = z - PIO2_2;
let y0 = z - PIO2_2T;
(1, y0, (z - y0) - PIO2_2T)
}
} else {
let z = x + PIO2_1;
if ix != 0x3ff921fb {
let y0 = z + PIO2_1T;
(-1, y0, (z - y0) + PIO2_1T)
} else {
let z = z + PIO2_2;
let y0 = z + PIO2_2T;
(-1, y0, (z - y0) + PIO2_2T)
}
});
}
if ix > 0x413921fb {
return None;
}
let t = x.abs();
let fnv = t.mul_add(INVPIO2, 0.5).trunc();
let n = fnv as i32;
let mut r = (-fnv).mul_add(PIO2_1, t);
let mut w = fnv * PIO2_1T;
let mut y0 = r - w;
if n > 32 || ix == NPIO2_HW[(n - 1) as usize] {
let j = ix >> 20;
let exponent = |y: f64| ((y.to_bits() << 1) >> 53) as i32;
if j - exponent(y0) > 16 {
let t = r;
let w2 = fnv * PIO2_2;
r = t - w2;
w = fnv.mul_add(PIO2_2T, w2 - (t - r));
y0 = r - w;
if j - exponent(y0) > 49 {
let t = r;
let w3 = fnv * PIO2_3;
r = t - w3;
w = fnv.mul_add(PIO2_3T, w3 - (t - r));
y0 = r - w;
}
}
}
let y1 = (r - y0) - w;
Some(if x > 0.0 { (n, y0, y1) } else { (-n, -y0, -y1) })
}
fn sin_cos(x: f64, is_cos: bool) -> f64 {
let ix = hi(x) & 0x7fff_ffff;
if ix < 0x3e400000 {
return if is_cos { 1.0 } else { x };
}
if ix <= 0x3fe921fb {
return if is_cos {
kernel_cos(x, 0.0, ix)
} else {
kernel_sin(x, 0.0, false)
};
}
if ix >= 0x7ff00000 {
return f64::from_bits(0x7ff0_0000_0000_0001);
}
// Beyond 2^19·π/2 the stub's Payne–Hanek reduction isn't ported:
// StrictMath's is used instead.
let Some((n, y0, y1)) = rem_pio2(x, ix) else {
return if is_cos { libm::cos(x) } else { libm::sin(x) };
};
let (value, negate) = if (n & 1 == 1) != is_cos {
(
kernel_cos(y0, y1, ix),
if is_cos { (n ^ (n >> 1)) & 1 } else { n & 2 },
)
} else {
(
kernel_sin(y0, y1, true),
if is_cos { (n ^ (n >> 1)) & 1 } else { n & 2 },
)
};
if negate != 0 { -value } else { value }
}
/// `Math.sin` on AArch64 HotSpot.
pub fn sin(x: f64) -> f64 {
sin_cos(x, false)
}
/// `Math.cos` on AArch64 HotSpot.
pub fn cos(x: f64) -> f64 {
sin_cos(x, true)
}
}
#[cfg(test)]
mod tests {
use super::{aarch64, fdlibm};
fn check1(f: fn(f64) -> f64, (x, expected): (u64, u64)) {
assert_eq!(f(f64::from_bits(x)).to_bits(), expected, "{x:x}");
}
fn check2(f: fn(f64, f64) -> f64, (x, y, expected): (u64, u64, u64)) {
assert_eq!(
f(f64::from_bits(x), f64::from_bits(y)).to_bits(),
expected,
"{x:x} {y:x}"
);
}
#[test]
fn matches_hotspot_on_aarch64() {
// (argument bits, Math result bits) from HotSpot 25 on an Apple M4, each
// one where glibc rounds differently.
// sin
check1(aarch64::sin, (0x3fe63c5ab66b09f0, 0x3fe47d34215b54f7));
check1(aarch64::sin, (0xc0569150fe105e6f, 0xbfe7bc19d65ab55c));
check1(aarch64::sin, (0x4072c3f15b47b5d6, 0xbfef32be0f0a9124));
check1(aarch64::sin, (0x3fee86c6ffce047e, 0x3fea1a47f46cbf28));
// cos
check1(aarch64::cos, (0x3fbb10690eeadb00, 0x3fefd24375dbae28));
check1(aarch64::cos, (0xc0496cc08a65245d, 0x3feab1a44a8022c4));
check1(aarch64::cos, (0x40099262f89e73f8, 0xbfeff3aa148795ce));
check1(aarch64::cos, (0x4001ce4e022387bc, 0xbfe37dd512be4d00));
// log
check1(fdlibm::log, (0x40569150fe105e6f, 0x401202e102f27b78));
check1(fdlibm::log, (0x3ff5dec70f518390, 0x3fd4009cda1f8856));
check1(fdlibm::log, (0x3feb3d17325d493e, 0xbfc49ee8c4866f84));
check1(fdlibm::log, (0x3fe735a40edf77ba, 0xbfd48e1816b18bf2));
// log10
check1(fdlibm::log10, (0x3ff5dec70f518390, 0x3fc15fb52aa5f051));
check1(fdlibm::log10, (0x3feb3d17325d493e, 0xbfb1e933cae02f5f));
check1(fdlibm::log10, (0x3fe735a40edf77ba, 0xbfc1da98cbb8974c));
check1(fdlibm::log10, (0x3fe7febd9ca1bab0, 0xbfc000e8e3b0b56e));
// cbrt
check1(fdlibm::cbrt, (0x401bb21f8568bd0c, 0x3ffe7ee30c7a56f9));
check1(fdlibm::cbrt, (0x3fc59f3a45c8fb80, 0x3fe1b0791e0f8a74));
check1(fdlibm::cbrt, (0xbfdd73cdc693ddab, 0xbfe8b4a6f43f1b73));
check1(fdlibm::cbrt, (0xbfeb0715a8425e10, 0xbfee3f99498da6f6));
// hypot
check2(
fdlibm::hypot,
(0xc0569150fe105e6f, 0xc0056b970bf3b41a, 0x405693db72f461a6),
);
check2(
fdlibm::hypot,
(0x3fdc5999df9270f4, 0xc006c3ad990f72c0, 0x400709de26a1e40a),
);
check2(
fdlibm::hypot,
(0x401bb21f8568bd0c, 0xbffeb93d1a2ef904, 0x401cbdbb623d20c3),
);
check2(
fdlibm::hypot,
(0x4026d31c32ddb50b, 0xc00e7b2771d6867c, 0x40281027bb313aae),
);
// pow
check2(
libm::pow,
(0x3fee04c313607ea6, 0xc00fd156138baf96, 0x3ff4a199ac2c8e9c),
);
check2(
libm::pow,
(0x401bb21f8568bd0c, 0xbffeb93d1a2ef904, 0x3f98ecc16a60feee),
);
check2(
libm::pow,
(0x407383ff427dadbd, 0x3ffdc5d178d00264, 0x40e56695c058d704),
);
check2(
libm::pow,
(0x3fe0c95b66dd6030, 0xc00c9cfde259a3e6, 0x4024194449e9deca),
);
// exp
check1(libm::exp, (0x3fdc5999df9270f4, 0x3ff8eacc53fdbf56));
check1(libm::exp, (0x3fcffff502ecbd80, 0x3ff48b5c78bca894));
check1(libm::exp, (0x3fc59f3a45c8fb80, 0x3ff2f1c6cb35728c));
check1(libm::exp, (0x4056561bf10ba7f6, 0x47fdd21dc9013084));
// tan
check1(libm::tan, (0x40023034f56881f0, 0xbff2e3f762ac9810));
check1(libm::tan, (0xbfebf862877c88ec, 0xbff31f61e3e8539e));
check1(libm::tan, (0xc00e37e234490564, 0xbfe79ca6eee66726));
check1(libm::tan, (0xc075c085c04800b5, 0x3fea18b4a92fcf22));
}
}
+1
View File
@@ -8,6 +8,7 @@ pub mod biome;
pub mod carving;
pub mod density;
pub mod geometry;
pub mod jmath;
pub mod noise;
pub mod paletted;
pub mod position;
+1 -1
View File
@@ -300,7 +300,7 @@ pub fn marsaglia_polar(cache: &mut Option<f64>, mut next_double: impl FnMut() ->
if radius >= 1.0 || radius == 0.0 {
continue;
}
let multiplier = (-2.0 * radius.ln() / radius).sqrt();
let multiplier = (-2.0 * crate::jmath::log(radius) / radius).sqrt();
*cache = Some(y * multiplier);
return x * multiplier;
}
+1 -1
View File
@@ -1092,7 +1092,7 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
let pitch = if mob_head {
1.0
} else {
2f64.powf(f64::from(note - 12) / 12.0) as f32
horizon_core::jmath::pow(2.0, f64::from(note - 12) / 12.0) as f32
};
let sound = match instrument.as_str() {
"custom_head" => "minecraft:ui.button.click".to_owned(),
+2 -1
View File
@@ -315,7 +315,8 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
if charge > 0
&& let Some(faces) = faces
{
let particles = ((f64::from(charge) + 1.0).ln() as f32 / 2.3) as i32 + 1;
let particles =
(horizon_core::jmath::log(f64::from(charge) + 1.0) as f32 / 2.3) as i32 + 1;
let data = (particles << 6) + pack_faces(&faces);
self.level_event(3006, pos, data);
}
@@ -545,9 +545,9 @@ fn speleothem_height(distance: f64, radius: f64, scale: f64, bluntness: f64) ->
distance
};
let r = distance / radius * 0.384;
let part1 = 0.75 * r.powf(1.3333333333333333);
let part2 = r.powf(0.6666666666666666);
let part3 = 0.3333333333333333 * r.ln();
let part1 = 0.75 * horizon_core::jmath::pow(r, 1.3333333333333333);
let part2 = horizon_core::jmath::pow(r, 0.6666666666666666);
let part3 = 0.3333333333333333 * horizon_core::jmath::log(r);
let height = (scale * (part1 - part2 - part3)).max(0.0);
height / 0.384 * radius
}
@@ -38,8 +38,8 @@ pub fn spikes_for_seed(seed: i64) -> Vec<EndSpike> {
2.0 * (-std::f64::consts::PI + (std::f64::consts::PI / 10.0) * f64::from(i));
let size = sizes[i as usize];
EndSpike {
center_x: (42.0 * angle.cos()).floor() as i32,
center_z: (42.0 * angle.sin()).floor() as i32,
center_x: (42.0 * horizon_core::jmath::cos(angle)).floor() as i32,
center_z: (42.0 * horizon_core::jmath::sin(angle)).floor() as i32,
radius: 2 + size / 3,
height: 76 + size * 3,
guarded: size == 1 || size == 2,
+7 -3
View File
@@ -29,7 +29,7 @@ fn ceil(v: f32) -> i32 {
f64::from(v).ceil() as i32
}
fn pow2(v: i32) -> f64 {
f64::from(v).powf(2.0)
horizon_core::jmath::pow(f64::from(v), 2.0)
}
impl IcebergFeature {
pub(crate) fn decode(
@@ -380,7 +380,11 @@ impl IcebergFeature {
fn signed_ellipse(xo: i32, zo: i32, origin: [i32; 3], a: i32, c: i32, angle: f64) -> f64 {
let dx = f64::from(xo - origin[0]);
let dz = f64::from(zo - origin[2]);
((dx * angle.cos() - dz * angle.sin()) / f64::from(a)).powf(2.0)
+ ((dx * angle.sin() + dz * angle.cos()) / f64::from(c)).powf(2.0)
let (sin, cos) = (
horizon_core::jmath::sin(angle),
horizon_core::jmath::cos(angle),
);
horizon_core::jmath::pow((dx * cos - dz * sin) / f64::from(a), 2.0)
+ horizon_core::jmath::pow((dx * sin + dz * cos) / f64::from(c), 2.0)
- 1.0
}
@@ -155,7 +155,10 @@ impl OreFeature {
let spread_xy = self.size as f32 / 8.0;
let max_radius = ((self.size as f32 / 16.0 * 2.0 + 1.0) / 2.0).ceil() as i32;
let (ox, oz) = (f64::from(origin[0]), f64::from(origin[2]));
let (sin, cos) = (f64::from(dir).sin(), f64::from(dir).cos());
let (sin, cos) = (
horizon_core::jmath::sin(f64::from(dir)),
horizon_core::jmath::cos(f64::from(dir)),
);
let spread = f64::from(spread_xy);
let x0 = ox + sin * spread;
let x1 = ox - sin * spread;
@@ -30,7 +30,10 @@ pub fn ring_positions(
let dist = f64::from(4 * distance + distance * circle * 6)
+ (random.next_double() - 0.5) * (f64::from(distance) * 2.5);
let round = |v: f64| (v + 0.5).floor() as i64 as i32;
let (x, z) = (round(angle.cos() * dist), round(angle.sin() * dist));
let (x, z) = (
round(horizon_core::jmath::cos(angle) * dist),
round(horizon_core::jmath::sin(angle) * dist),
);
let mut search = random.fork();
let (cx, cz) = ((x * 16 + 8) >> 2, (z * 16 + 8) >> 2);
let mut found = 0;
@@ -690,8 +690,9 @@ impl TrunkPlacer {
let height = tree_height + 2;
let trunk_height = (f64::from(height) * 0.618).floor() as i32;
Self::place_below_trunk(run, tree, tb::offset(origin, 0, -1, 0))?;
let clusters_per_y =
1.min((1.382 + (1.0 * f64::from(height) / 13.0).powf(2.0)).floor() as i32);
let clusters_per_y = 1.min(
(1.382 + horizon_core::jmath::pow(1.0 * f64::from(height) / 13.0, 2.0)).floor() as i32,
);
let trunk_top = origin[1].wrapping_add(trunk_height);
let mut relative_y = height - 5;
// (attachment position, branch base y)
@@ -704,8 +705,8 @@ impl TrunkPlacer {
let radius =
1.0 * f64::from(shape) * (f64::from(run.random.next_float()) + 0.328);
let angle = f64::from(run.random.next_float() * 2.0f32) * std::f64::consts::PI;
let x = radius * angle.sin() + 0.5;
let z = radius * angle.cos() + 0.5;
let x = radius * horizon_core::jmath::sin(angle) + 0.5;
let z = radius * horizon_core::jmath::cos(angle) + 0.5;
let check_start =
tb::offset(origin, x.floor() as i32, relative_y - 1, z.floor() as i32);
let check_end = tb::offset(check_start, 0, 5, 0);
+1 -1
View File
@@ -32,7 +32,7 @@ Every runner on the machine shares `PARITY_MACHINE_JOBS` slots (default: half th
Harnesses can also run on the Mac mini and the ThinkPad (`tools/parity_hosts.json` describes them; its `default` list is empty, so pass `--hosts macmini,thinkpad` or set `PARITY_HOSTS`, after copying `shared-local/` to `~/code/horizon-parity/shared-local` on each host). The runner mirrors the worktree there with rsync (not `.git`, `.local`, `target` or ignored files) into `~/code/horizon-parity/remote/<host><path>`, whose `.local` links to the vanilla data in `~/code/horizon-parity/shared-local`, and starts `tools/parity.py --worker` over ssh. The worker builds the probes (about 80 s the first time) and runs what it is sent under the same tracer, admitting harnesses against its own free memory. One queue, longest first, feeds every machine, remote ones first: the Mac takes up to 14 harnesses, the desktop 3 and only those under 800 MB. Each pass comes back with the content hash of every file it depended on, so it is keyed on exactly what ran, and logs come back to `.local/parity-runner/logs/`; state, sharing and `gate.py` work as before. If a host can't be reached, or drops out, its harnesses run here.
Harnesses a host can't run are listed in the file: `local_only` (`save_roundtrip` runs the game on a virtual screen) and each host's `exclude`. On macOS, `options` and `chunk_generation` differ because Rust's float functions (`f64::powf` and the like) use the platform libm, which rounds differently from glibc; every other harness gave the same result on the Mac as on Linux. The ThinkPad helps too, gently: Linux, 2 harnesses at `nice 19`, since it hosts the Claude account pool; it finds ALSA through `~/code/horizon-parity/alsa-dev` like the desktop.
Harnesses a host can't run are listed in the file: `local_only` (`save_roundtrip` runs the game on a virtual screen) and each host's `exclude`. The Mac excludes nothing: vanilla's `Math.sin`, `pow`, `log` and the like don't round the same on an Apple silicon Mac as on Linux (HotSpot uses fdlibm there, Intel's stubs on x86-64), and Rust's float functions round like neither (they call the platform libm), so code that mirrors a vanilla `Math` call uses `horizon_core::jmath`, which computes what HotSpot computes on the host architecture. The ThinkPad helps too, gently: Linux, 2 harnesses at `nice 19`, since it hosts the Claude account pool; it finds ALSA through `~/code/horizon-parity/alsa-dev` like the desktop.
### Shards and Rust scopes
@@ -478,9 +478,12 @@ public final class VanillaProtocolClient {
record("play", r.type + " " + json(p.reason()));
break;
}
// The clock goes out every second: after the first, where it
// falls among the joining packets is timing.
if (ordered && !(r.type.equals("minecraft:set_time") && counts.get(r.type) > 1)) {
// MinecraftServer.tickServer syncs the game time every 20 ticks
// (no clocks): whether one lands before the first chunk batch
// depends on when the client joined, so it isn't compared.
boolean periodicTime = packet instanceof net.minecraft.network.protocol.game.ClientboundSetTimePacket p
&& p.clockUpdates().isEmpty();
if (ordered && !periodicTime) {
record("play-start", r.type + " " + summary(r));
}
if (packet instanceof net.minecraft.network.protocol.game.ClientboundCommandsPacket p && counts.get(r.type) == 1) {
+1 -6
View File
@@ -9,12 +9,7 @@
"python": "/opt/homebrew/bin/python3.13",
"rsync": "/opt/homebrew/bin/rsync",
"jobs": 14,
"exclude": {
"options": "Rust's f64::powf uses the platform libm, which rounds differently on macOS",
"chunk_generation": "one ocean ruined portal differs on macOS (platform float math)",
"world_border_render": "the wall's alpha differs by one ulp on macOS (platform float math)",
"mob_ai": "mob movement differs on macOS (platform float math in random positions)"
}
"exclude": {}
},
"thinkpad": {
"ssh": "thinkpad",
+41 -9
View File
@@ -9,9 +9,9 @@ headless client made of vanilla's own codecs (any byte vanilla would not
read is an error), pings each server, joins it in offline mode through
login, configuration and play, accepts the spawn teleport, receives the
chunks around the spawn (their bytes are compared: the world is loaded from
the same files), chats, runs /list, then commands that fail (`/random value
7..7`, `/random value x`, `/trigger nope`) and leaves. The console then runs `list`
and `say` and `stop`. Compared, all of them exactly:
the same files), chats, runs /list and commands that fail (`/random value
7..7`, `/random value x`, `/trigger nope`) and leaves. The console then
runs `list` and `say` and `stop`. Compared, all of them exactly:
- the status response (MOTD, players, version, favicon) and the ping;
- the legacy (pre-1.7, 0xFE) ping reply;
@@ -143,6 +143,36 @@ class Server:
self.log.close()
def settle(server, ticks=600, timeout=600):
"""Waits until every force-loaded chunk is loaded, then for `ticks` game
ticks, so their fluids have finished flowing however slow the machine."""
deadline = time.time() + timeout
offsets = range(-80, 81, 16)
for poll in range(1_000_000):
start = len(server.lines)
for dx in offsets:
for dz in offsets:
server.console(f'execute unless loaded ~{dx} ~ ~{dz} run say unloaded')
server.console(f'say poll {poll}')
server.wait_for(rf'\[Server\] poll {poll}$', timeout=max(1, deadline - time.time()))
if not any(l.endswith('[Server] unloaded') for l in server.lines[start:]):
break
time.sleep(2)
def game_time():
start = len(server.lines)
server.console('time query gametime')
while time.time() < deadline:
for line in server.lines[start:]:
if m := re.search(r'The (?:game )?time is (\d+)', line):
return int(m.group(1))
time.sleep(0.2)
raise AssertionError(f'{server.log_path.name}: no game time in {timeout} s')
until = game_time() + ticks
while game_time() < until:
time.sleep(2)
def legacy_ping(port):
"""The reply to a 1.6 client's 0xFE 0x01 0xFA MC|PingHost query (LegacyQueryHandler)."""
host = 'localhost'
@@ -307,16 +337,17 @@ def main():
server.wait_for(r'Done \(')
server.console('gamerule respawn_radius 0')
server.wait_for(r'respawn_radius', timeout=30)
# No random ticks: which of the chunks around the player have grown
# kelp or spread grass by the time they are sent is timing.
server.console('gamerule random_tick_speed 0')
server.wait_for(r'random_tick_speed', timeout=30)
# Vanilla keeps no spawn chunks: force-load the area the client is
# sent and its neighbours (the console stands at the world spawn) so
# both servers load full chunks instead of finishing proto-chunks.
# Random ticks change blocks while the client is being sent its
# chunks, more of them the busier the machine: none, so both servers
# send the world as it was saved.
server.console('gamerule random_tick_speed 0')
server.wait_for(r'random_tick_speed', timeout=30)
server.console('forceload add ~-80 ~-80 ~80 ~80')
server.wait_for(r'chunks in the Overworld are now force loaded|forceloaded|force loaded', timeout=60)
time.sleep(20)
settle(server)
server.console('forceload remove all')
server.console('save-all flush')
server.wait_for(r'Saved the game', timeout=120)
@@ -361,7 +392,8 @@ def main():
java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],
lines=len(vanilla), output_sha256=lab.digest(build / 'horizon.txt', 'sha256'),
scope='Dedicated server seen by a vanilla-codec client: status, legacy ping, login, configuration, play '
'start, chunks, chat, /list, console list/say/stop, files written. Offline mode.'))
'start (recipes, advancements, command tree), chunks, chat, /list, failing commands\' replies, '
'console list/say/stop, files written. Offline mode.'))
print(f'dedicated server: {len(vanilla)} transcript lines identical')