rustlane 0.1.1

ISPC-style SPMD programming model for Rust: natural control flow lowered to masked SIMD by proc macros over nightly std::simd
Documentation
#![feature(portable_simd)]

use core::simd::Mask;
use rustlane::prelude::*;

const N: usize = 4;
type Vf = Varying<f32, N>;
type Vi = Varying<i32, N>;

fn foreach_scale(a: &[f32], out: &mut [f32]) {
    let __exec = AllOn;
    let _ = __exec;
    {
        let __n = a.len();
        let mut __base = 0usize;
        while __base + N <= __n {
            let __exec = AllOn;
            let i = LinearIndex::<N>::new(__base);
            let x = a.spmd_read(i, __exec);
            out.spmd_write(i, __exec, x * 2.0f32 + 1.0f32);
            __base += N;
        }
        if __base < __n {
            let __exec = VMask::<N>::first(__n - __base);
            let i = LinearIndex::<N>::new(__base);
            let x = a.spmd_read(i, __exec);
            out.spmd_write(i, __exec, x * 2.0f32 + 1.0f32);
        }
    }
}

#[test]
fn foreach_template_main_and_tail() {
    let a: Vec<f32> = (0..6).map(|i| i as f32).collect();
    let mut out = vec![0.0f32; 6];
    foreach_scale(&a, &mut out);
    let want: Vec<f32> = a.iter().map(|x| x * 2.0 + 1.0).collect();
    assert_eq!(out, want);
}

fn if_else_compound(x: Vf, t: f32) -> (Vf, Vf) {
    let __exec = AllOn;
    let mut y = Varying::<f32, N>::splat(1.0);
    let mut z = Varying::<f32, N>::splat(0.0);
    {
        let __c = x.spmd_gt(t);
        let __exec1 = __exec.and_cond(__c);
        if __exec1.should_branch() {
            y.masked_assign(__exec1, y + x);
        }
        let __exec1 = __exec.and_not_cond(__c);
        if __exec1.should_branch() {
            y.masked_assign(__exec1, 0.0f32);
        }
    }
    {
        let __exec = AllOn;
        z.masked_assign(__exec, y + 1.0f32);
    }
    (y, z)
}

#[test]
fn if_else_and_unmasked_templates() {
    let x = Vf::from_array([1.0, 3.0, 0.5, 4.0]);
    let (y, z) = if_else_compound(x, 2.0);
    assert_eq!(y.to_array(), [0.0, 4.0, 0.0, 5.0]);
    assert_eq!(z.to_array(), [1.0, 5.0, 1.0, 6.0]);
}

fn if_else_uniform(flag: bool) -> Vf {
    let __exec = AllOn;
    let mut y = Varying::<f32, N>::splat(0.0);
    {
        let __c = flag;
        let __exec1 = __exec.and_cond(__c);
        if __exec1.should_branch() {
            y.masked_assign(__exec1, 1.0f32);
        }
        let __exec1 = __exec.and_not_cond(__c);
        if __exec1.should_branch() {
            y.masked_assign(__exec1, -1.0f32);
        }
    }
    y
}

#[test]
fn if_else_uniform_template() {
    assert_eq!(if_else_uniform(true).to_array(), [1.0; N]);
    assert_eq!(if_else_uniform(false).to_array(), [-1.0; N]);
}

fn masked_scatter_increment(hist: &mut [i32], idx: Vi, sel: Vi) {
    let __exec = AllOn;
    {
        let __c = sel.spmd_gt(0);
        let __exec1 = __exec.and_cond(__c);
        if __exec1.should_branch() {
            let __t = hist.spmd_read(idx, __exec1) + 1;
            hist.spmd_write(idx, __exec1, __t);
        }
    }
}

#[test]
fn gather_modify_scatter_template() {
    let mut hist = [10i32, 20, 30, 40, 50];
    masked_scatter_increment(
        &mut hist,
        Vi::from_array([4, 0, 2, 1]),
        Vi::from_array([1, 0, 5, -1]),
    );
    assert_eq!(hist, [10, 20, 31, 40, 51]);
}

#[test]
fn logical_and_cast_and_literal_index_emissions() {
    let p = Vf::from_array([0.5, 1.5, 2.5, 3.5]);
    let inside = p.spmd_ge(1.0f32).spmd_and(|| p.spmd_le(3.0f32));
    assert_eq!(inside.to_array(), [false, true, true, false]);
    let outside = inside.spmd_not();
    assert_eq!(outside.to_array(), [true, false, false, true]);
    let m = true.spmd_and(|| p.spmd_gt(2.0f32));
    assert_eq!(m.to_array(), [false, false, true, true]);

    let v: Varying<i32, N> = SpmdCast::<i32>::spmd_cast(p);
    assert_eq!(v.to_array(), [0, 1, 2, 3]);
    let u: f32 = SpmdCast::<f32>::spmd_cast(3i32);
    assert_eq!(u, 3.0);

    let coef = [2.0f32, 0.5, 0.25, 0.125];
    let c0 = coef.spmd_read(0, AllOn);
    assert_eq!(c0, 2.0);

    let xoffsets = [0i32, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3];
    let o = LinearIndex::<N>::new(4);
    let xo = xoffsets.spmd_read(o, AllOn);
    assert_eq!(xo.to_array(), [2, 3, 2, 3]);
    let xg = xoffsets.spmd_read(Vi::from_array([0, 5, 10, 15]), AllOn);
    assert_eq!(xg.to_array(), [0, 3, 0, 3]);
}

fn stencil_row(a: &[f32], out: &mut [f32], base: usize, c0: f32, c1: f32) {
    let __exec = AllOn;
    let index = LinearIndex::<N>::new(base);
    let center = a.spmd_read(index, __exec);
    let right = a.spmd_read(index + 1, __exec);
    let left = a.spmd_read(index + (-1), __exec);
    out.spmd_write(index, __exec, center * c0 + (right + left) * c1);
}

#[test]
fn stencil_linear_offsets() {
    let a: Vec<f32> = (0..8).map(|i| i as f32).collect();
    let mut out = vec![0.0f32; 8];
    stencil_row(&a, &mut out, 2, 2.0, 0.5);
    for i in 2..6 {
        let want = a[i] * 2.0 + (a[i + 1] + a[i - 1]) * 0.5;
        assert_eq!(out[i], want, "element {i}");
    }
    let li = LinearIndex::<N>::new(2);
    let demoted: Vi = li + Vi::splat(0);
    let g = a.spmd_read(demoted, AllOn);
    let l = a.spmd_read(li, AllOn);
    assert_eq!(g.to_array(), l.to_array());
}

const STEPS: usize = 8;

fn binomial_shape_allon(s: Vf) -> Vf {
    let __exec = AllOn;
    let mut v = [Varying::<f32, N>::splat(0.0); STEPS];
    for j in 0..STEPS {
        v.spmd_write(j, __exec, s * (j as f32));
    }
    for j in (0..STEPS - 1).rev() {
        for k in 0..=j {
            let t = (v.spmd_read(k, __exec) + v.spmd_read(k + 1, __exec)) * 0.5f32;
            v.spmd_write(k, __exec, t);
        }
    }
    v.spmd_read(0, __exec)
}

fn binomial_shape_masked(s: Vf, active: usize) -> Vf {
    let __exec = VMask::<N>::first(active);
    let mut v = [Varying::<f32, N>::splat(0.0); STEPS];
    for j in 0..STEPS {
        v.spmd_write(j, __exec, s * (j as f32));
    }
    for j in (0..STEPS - 1).rev() {
        for k in 0..=j {
            let t = (v.spmd_read(k, __exec) + v.spmd_read(k + 1, __exec)) * 0.5f32;
            v.spmd_write(k, __exec, t);
        }
    }
    v.spmd_read(0, __exec)
}

#[test]
fn binomial_local_varying_array() {
    let s = Vf::from_array([1.0, 2.0, 3.0, 4.0]);
    let full = binomial_shape_allon(s);
    let tail = binomial_shape_masked(s, 2);
    let f = full.to_array();
    let t = tail.to_array();
    assert_eq!(t[0], f[0]);
    assert_eq!(t[1], f[1]);
    assert_eq!(t[2], 0.0);
    assert_eq!(t[3], 0.0);
    for lane in 0..N {
        let mut v = [0.0f32; STEPS];
        for (j, slot) in v.iter_mut().enumerate() {
            *slot = s.to_array()[lane] * (j as f32);
        }
        for j in (0..STEPS - 1).rev() {
            for k in 0..=j {
                v[k] = 0.5 * (v[k] + v[k + 1]);
            }
        }
        assert_eq!(f[lane], v[0], "lane {lane}");
    }
}

fn tri_hit_shape(b1: Vf, t: Vf, mint: f32, maxt: f32) -> Mask<i32, N> {
    let __exec = AllOn;
    let mut hit: Mask<i32, N> = Mask::splat(true);
    {
        let __c = b1.spmd_lt(0.0f32).spmd_or(|| b1.spmd_gt(1.0f32));
        let __exec1 = __exec.and_cond(__c);
        if __exec1.should_branch() {
            hit.masked_assign(__exec1, false);
        }
    }
    {
        let __c = t.spmd_lt(mint).spmd_or(|| t.spmd_gt(maxt));
        let __exec1 = __exec.and_cond(__c);
        if __exec1.should_branch() {
            hit.masked_assign(__exec1, false);
        }
    }
    hit
}

#[test]
fn varying_bool_local_mask_lvalue() {
    let b1 = Vf::from_array([0.5, -0.1, 0.9, 1.2]);
    let t = Vf::from_array([1.0, 1.0, 99.0, 1.0]);
    let hit = tri_hit_shape(b1, t, 0.0, 10.0);
    assert_eq!(hit.to_array(), [true, false, false, false]);

    fn intersect_shape(t0: Vf, t1: Vf) -> Mask<i32, N> {
        let __exec = AllOn;
        let mut __ret: Mask<i32, N> = Default::default();
        let mut __fn = EnterLoopN::<N>::enter_loop_n(__exec);
        {
            let __c = t0.spmd_le(t1);
            let __exec1 = __exec.and_cond(__c);
            if __exec1.should_branch() {
                __ret.masked_assign(__exec1, true);
                if __exec1.is_statically_uniform() {
                    return __ret;
                }
                __fn.remove(__exec1);
            }
        }
        let __exec = __exec.refresh(&__fn);
        __ret.masked_assign(__exec, false);
        __ret
    }
    let r = intersect_shape(Vf::from_array([0.0, 5.0, 1.0, 9.0]), Vf::splat(4.0));
    assert_eq!(r.to_array(), [true, false, true, false]);
}

fn raymarch_shape(
    density: &[f32],
    nx: i32,
    ny: i32,
    t1: Vf,
    cut: f32,
    vx: Vi,
    vy: Vi,
    vz: Vi,
) -> Vf {
    let __exec = AllOn;
    let mut tau = Varying::<f32, N>::splat(0.0);
    let mut t = Varying::<f32, N>::splat(0.0);

    let x = math::clamp(vx, Vi::splat(0), Vi::splat(nx - 1));
    let y = math::clamp(vy, Vi::splat(0), Vi::splat(ny - 1));
    let z = math::clamp(vz, Vi::splat(0), Vi::splat(1));
    let idx = z * (nx * ny) + y * nx + x;

    {
        let mut __loop = __exec.enter_loop(t.spmd_lt(t1));
        loop {
            if !__loop.any() {
                break;
            }
            let __exec = __loop.current();
            {
                let __c = tau.spmd_gt(cut);
                let __exec1 = __exec.and_cond(__c);
                if __exec1.should_branch() {
                    if __exec1.is_statically_uniform() {
                        break;
                    }
                    __loop.remove(__exec1);
                }
            }
            let __exec = __exec.refresh(&__loop);
            tau.masked_assign(__exec, tau + density.spmd_read(idx, __exec));
            t.masked_assign(__exec, t + 1.0f32);
            let __c = t.spmd_lt(t1);
            __loop = __loop.and_cond(__c);
        }
    }
    math::exp(-tau)
}

#[test]
fn volume_while_break_clamp_gather() {
    let density: Vec<f32> = (0..16).map(|i| 0.1 * i as f32).collect();
    let (nx, ny) = (4, 2);
    let vx = Vi::from_array([-1, 1, 5, 2]);
    let vy = Vi::from_array([0, 3, 1, 0]);
    let vz = Vi::from_array([0, 0, 7, 1]);
    let t1 = Vf::from_array([0.0, 1.0, 2.0, 3.0]);
    let got = raymarch_shape(&density, nx, ny, t1, 1.0e9, vx, vy, vz);

    let cl = |v: i32, hi: i32| v.clamp(0, hi);
    for lane in 0..N {
        let x = cl(vx.to_array()[lane], nx - 1);
        let y = cl(vy.to_array()[lane], ny - 1);
        let z = cl(vz.to_array()[lane], 1);
        let d = density[(z * nx * ny + y * nx + x) as usize];
        let steps = t1.to_array()[lane] as i32;
        let tau = d * steps.max(0) as f32;
        let want = (-tau).exp();
        let g = got.to_array()[lane];
        assert!((g - want).abs() < 1e-5, "lane {lane}: {g} vs {want}");
    }
}

#[test]
fn ao_scatter_add_shape() {
    let mut image = [0.0f32; 2];
    let offset = Vi::from_array([0, 0, 1, 1]);
    let ret = Vf::from_array([0.25, 0.5, 1.0, 2.0]);
    {
        let __exec = AllOn;
        memory::scatter_add(&mut image, offset, __exec, ret);
    }
    assert_eq!(image, [0.75, 3.0]);

    let mut image = [0.0f32; 2];
    let m = VMask::<N>(Mask::from_array([true, false, true, false]));
    memory::scatter_add(&mut image, offset, m, ret);
    assert_eq!(image, [0.25, 1.0]);
}

#[test]
fn ao_rng_math_shape() {
    let y0 = 3i32;
    let pi_lane = reduce::lanes_iota::<N>();
    let seed_i = pi_lane + (y0 << (pi_lane & 15));
    let seeds: Varying<u32, N> = SpmdCast::<u32>::spmd_cast(seed_i);
    let mut rng = rng::RNGState::<N>::new(seeds);

    let theta = math::sqrt(rng.frandom());
    let phi = rng.frandom() * (2.0f32 * core::f32::consts::PI);
    let x = math::cos(phi) * theta;
    let y = math::sin(phi) * theta;
    let z = math::sqrt(math::abs(Varying::splat(1.0f32) - theta * theta));
    let len2 = x * x + y * y + z * z;
    for l in len2.to_array() {
        assert!((l - 1.0).abs() < 1e-4, "unit dir, got {l}");
    }
}