SDF level-set — marching squares
A 160×100 signed distance field sampled at each grid point — two moving circles unioned with a smooth-min operator. Marching squares walks the cells, emitting one line segment per iso-crossing using the 16-case lookup table (5 and 10 disambiguated by corner-average). The math runs on this device; the receipt prices it against the Landauer floor and the TDP envelope.
view the kernel that wrote this receipt crates/mathground-view/src/sdf.rs
//! 2-D SDF level-set — marching squares.
//!
//! Walk a regular `w × h` grid of scalar values, emit one line segment per
//! cell that the iso-value `iso` crosses, using the 16-case marching-squares
//! lookup. Pure compute. V-class L0-closed.
//!
//! Output is a packed `Vec<f32>` of segment endpoints in pixel coordinates
//! relative to the grid: `[x0, y0, x1, y1, x0, y0, x1, y1, ...]`. The JS
//! side strokes them on a canvas.
use serde::{Deserialize, Serialize};
/// Per-frame report from the marching-squares pass.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdfKernelReport {
pub grid_w: u32,
pub grid_h: u32,
pub segments: u32,
pub bit_ops: u64,
}
/// Marching squares on a `w × h` scalar grid, iso-level `iso`. Appends
/// segment endpoints to `out` and returns the kernel report.
///
/// `cells` is row-major: `cells[y * w + x]`. `out` is cleared at entry.
pub fn marching_squares(cells: &[f32], w: u32, h: u32, iso: f32, out: &mut Vec<f32>) -> SdfKernelReport {
out.clear();
let w_us = w as usize;
let h_us = h as usize;
debug_assert_eq!(cells.len(), w_us * h_us);
let mut segments = 0u32;
for y in 0..(h_us - 1) {
for x in 0..(w_us - 1) {
let tl = cells[y * w_us + x];
let tr = cells[y * w_us + (x + 1)];
let bl = cells[(y + 1) * w_us + x];
let br = cells[(y + 1) * w_us + (x + 1)];
let mut idx = 0u8;
if tl > iso { idx |= 1; }
if tr > iso { idx |= 2; }
if br > iso { idx |= 4; }
if bl > iso { idx |= 8; }
let lerp = |a: f32, b: f32| -> f32 {
if (b - a).abs() < 1e-12 {
0.5
} else {
((iso - a) / (b - a)).clamp(0.0, 1.0)
}
};
let fx = x as f32;
let fy = y as f32;
// Edge midpoints (lerped) at:
// top: (fx + t_top, fy)
// right: (fx + 1, fy + t_right)
// bottom: (fx + t_bot, fy + 1)
// left: (fx, fy + t_left)
let t_top = lerp(tl, tr);
let t_right = lerp(tr, br);
let t_bot = lerp(bl, br);
let t_left = lerp(tl, bl);
let p_top = (fx + t_top, fy);
let p_right = (fx + 1.0, fy + t_right);
let p_bot = (fx + t_bot, fy + 1.0);
let p_left = (fx, fy + t_left);
// Push helper.
let mut push = |a: (f32, f32), b: (f32, f32)| {
out.extend_from_slice(&[a.0, a.1, b.0, b.1]);
segments += 1;
};
// 16-case lookup. Ambiguous cases (5, 10) resolved with
// saddle-point disambiguation via average of corners.
match idx {
0 | 15 => {}
1 => push(p_left, p_top),
2 => push(p_top, p_right),
3 => push(p_left, p_right),
4 => push(p_right, p_bot),
5 => {
// Ambiguous: tl and br above iso; tr and bl below.
let avg = 0.25 * (tl + tr + bl + br);
if avg > iso {
push(p_left, p_top);
push(p_right, p_bot);
} else {
push(p_left, p_bot);
push(p_top, p_right);
}
}
6 => push(p_top, p_bot),
7 => push(p_left, p_bot),
8 => push(p_left, p_bot),
9 => push(p_top, p_bot),
10 => {
let avg = 0.25 * (tl + tr + bl + br);
if avg > iso {
push(p_top, p_right);
push(p_left, p_bot);
} else {
push(p_left, p_top);
push(p_right, p_bot);
}
}
11 => push(p_right, p_bot),
12 => push(p_left, p_right),
13 => push(p_top, p_right),
14 => push(p_left, p_top),
_ => unreachable!(),
}
}
}
// Bit-ops estimate: per cell we do ~4 comparisons + up to 4 lerps
// (~6 ops each) + a couple of writes. Round to ~40 f32 ops per cell,
// 32 bit-ops per op.
let cells_visited = ((w_us - 1) * (h_us - 1)) as u64;
let bit_ops = cells_visited * 40 * 32;
SdfKernelReport {
grid_w: w,
grid_h: h,
segments,
bit_ops,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Fill a grid with the SDF of a circle at center, radius r.
/// Positive outside, negative inside (SDF convention).
fn circle_sdf(w: u32, h: u32, cx: f32, cy: f32, r: f32) -> Vec<f32> {
let mut v = Vec::with_capacity((w * h) as usize);
for y in 0..h {
for x in 0..w {
let dx = x as f32 - cx;
let dy = y as f32 - cy;
v.push((dx * dx + dy * dy).sqrt() - r);
}
}
v
}
#[test]
fn empty_grid_under_iso_emits_no_segments() {
let cells = vec![-1.0f32; 16 * 16];
let mut out = Vec::new();
let r = marching_squares(&cells, 16, 16, 0.0, &mut out);
assert_eq!(r.segments, 0);
assert!(out.is_empty());
}
#[test]
fn circle_emits_a_closed_contour() {
let cells = circle_sdf(64, 64, 32.0, 32.0, 16.0);
let mut out = Vec::new();
let r = marching_squares(&cells, 64, 64, 0.0, &mut out);
// Circle of radius 16 → circumference ≈ 100. Expect ~80-120 segments.
assert!(r.segments > 60 && r.segments < 200, "got {} segments", r.segments);
assert_eq!(out.len() as u32, r.segments * 4);
}
#[test]
fn higher_iso_means_smaller_circle() {
let cells = circle_sdf(64, 64, 32.0, 32.0, 20.0);
let mut out = Vec::new();
let r0 = marching_squares(&cells, 64, 64, 0.0, &mut out);
let mut out2 = Vec::new();
let r1 = marching_squares(&cells, 64, 64, 5.0, &mut out2);
// iso = 5 picks the contour at radius 25 — bigger, more segments.
assert!(r1.segments > r0.segments, "{} vs {}", r1.segments, r0.segments);
}
#[test]
fn bit_ops_scale_with_grid_area() {
let cells_a = vec![0.0f32; 16 * 16];
let cells_b = vec![0.0f32; 32 * 32];
let mut out = Vec::new();
let ra = marching_squares(&cells_a, 16, 16, 0.0, &mut out);
out.clear();
let rb = marching_squares(&cells_b, 32, 32, 0.0, &mut out);
let ratio = rb.bit_ops as f64 / ra.bit_ops as f64;
assert!(ratio > 3.5 && ratio < 5.0, "ratio was {ratio}");
}
}
This is the exact Rust file compiled into the WASM module the page just loaded. Every line that runs on your device is here. The receipt is a function of this code, not a bespoke benchmark.
Notes on the receipt
- V-class L0-closed: same field, same iso-level → same segments. The contour is replayable.
- bit-ops / op: ~40 f32 ops per visited cell × 32 bit-ops. One op = one full grid traversal.
- μ apparent: typical for cache-friendly stencil work — comparable to FFT, slightly lower.