makepad/platform/script/compute/std/field.splash
Admin 5ccd7f4e8e script (compute): Splash modules and the kernel stdlib, plus host components -- a kernel can import modules. A module is Splash text of top-level fns and consts (module.rs). The host resolves each import to its text and passes (path, source) pairs to kernel::compile_with_modules; use std.curve.* brings names in unqualified, use std.field as f and use lib("id", "rev") as x give a name, and a qualified call to a std module (curve.sample(..)) needs no use. A module's items are renamed by its alias and its own references follow, with locals shadowing them. Modules may import modules, and cycles and missing modules are errors at the use. A kernel's own names shadow imported ones, and two globs bringing one name are an error. Errors inside a module are reported at the kernel's call, like the prelude's; syntax errors name the module, line and column. There is no privileged tier: a library or AI-written module compiles to exactly the AIR of the same stdlib function (tested). Library items a kernel does not reach are pruned before lowering, so an unused import costs nothing (this now also drops unreached prelude tables). The parser gains use items, const, "strings" (only for lib()), m.f(..) qualified calls and x: f64 annotations. The stdlib (std/*.splash): std.shared is SX1's shared stdlib (knob, rot2, luma, palette, hsv2rgb, sd_*, bayer4, eases; exported as module::SHARED_STD for the shader compiler, in scope by default); std.curve (arc-length sampling over a cumulative table, point_at, spline_at, bezier2, arc, offset2); std.field (marching squares: cell_case, segments, segment, with segment tangent and midpoint for labels; grad); std.poly (area, winding, contains, perimeter, centroid); std.ease (Motion's ease formulas in f64 rounded once, bit-exact for the polynomial, circ, back and bounce curves; spring); std.anim (tween, keys, stepped, spring_to, pulse, pulse_sum, follow, accelerate); std.cam (project, to_screen, unproject, sphere/box visibility); std.beat (beat_at, time_of_beat, next_downbeat, word_progress, word_at, hit, env); std.search (bounded binary search). Host components (host.rs) are a registry of the few heavy Rust functions a kernel may call: typed scalar parameters and results, buffer slices (buffer, word offset, word length) with a maximum length and write permission, a worst-case cost (Program::cost charges it per call), a host-memory touch count and a determinism tier (portable kernels call only tier X). A call is the new AIR statement CallHost: the function is a compile-time registry index, and ir::validate checks the index, arity, types, results defined once, and that each slice names a bound buffer with the right permission; audio programs never call out. At run time slices are clamped to their buffers and maxima and seen as HostSlices (word access through atomics, no Rust references over kernel memory); a panic or error zeroes the results and sets ctx K_HOST_ERR (RunStats::host_error), never unwinding into generated code. The scalar backend calls one fixed trampoline around which it saves and restores the caller-saved registers; NEON declines host calls, and a written slice keeps the kernel on one thread. The registry is append-only and host-extensible (host::register at start-up: text.outline registers from the text crate). One built-in proves it: poly.triangulate (ear clipping of a simple polygon of up to 256 points, tier X). The ctx gains K_HOST_ERR at word 22 (K_PARAMS moves to 23). audio_aot/src/fuse.rs gets a two-line hunk for the new Item::Use (audio shaders still accept and ignore use). Checked: cargo test --release -p makepad-script-compute -p makepad-script-audio-aot (audio 21 with the 36 golden hashes unchanged). New: modules 6 (a library module and a kernel's own fn compile to the stdlib's exact AIR; resolution, shadowing, glob conflicts, nesting, cycles, error spans; unused imports cost nothing; marching squares bit-equal to a Rust reference at three levels; curves, polygons, Motion's eases bit-exact, keyframes, pulses, beats, camera; the shared stdlib in kernels). host 6 (native == interpreter with values live across calls; a panicking function sets the error word with zeroed results; hostile offsets and lengths clamped at buffer ends and the maximum; contract errors; triangulating a concave and a convex polygon covers their areas exactly; validator rejections). Existing suites green: neon 4, portable 4, sched 5, sched_alloc 1, soundness 14, kernels 3, admission 5, compiler_fuzz 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:18:33 +02:00

79 lines
3.6 KiB
Text

// Scalar fields on grids: `grid` is a row-major f32 buffer of w columns
// and h rows (cell units). Isolines are marching squares, one cell per
// element: a kernel emits `segments(..)` records per cell, deterministic
// and element-local.
// Bilinear sample and normal (the prelude's, under field names).
fn sample(grid, w, h, x, z) { sample_grid(grid, w, h, x, z) }
fn normal(grid, w, h, x, z, cell) { grid_normal(grid, w, h, x, z, cell) }
// The gradient at (x, z) by central differences, per cell unit.
fn grad(grid, w, h, x, z) {
let dx = sample_grid(grid, w, h, x + 1.0, z) - sample_grid(grid, w, h, x - 1.0, z)
let dz = sample_grid(grid, w, h, x, z + 1.0) - sample_grid(grid, w, h, x, z - 1.0)
vec2(dx, dz) * 0.5
}
// Marching-squares edges per case: two segments of (edge, edge), -1 for
// none; edges 0: a-b (bottom), 1: b-c (right), 2: c-d (top), 3: d-a (left),
// corners a (cx, cz), b (cx+1, cz), c (cx+1, cz+1), d (cx, cz+1); a corner
// at or above the level sets its bit (a 1, b 2, c 4, d 8). The saddles 5
// and 10 are resolved by the cell centre.
let MS_EDGES: int = [
-1, -1, -1, -1, 3, 0, -1, -1, 0, 1, -1, -1, 3, 1, -1, -1,
1, 2, -1, -1, 3, 0, 1, 2, 0, 2, -1, -1, 3, 2, -1, -1,
2, 3, -1, -1, 0, 2, -1, -1, 0, 1, 2, 3, 1, 2, -1, -1,
1, 3, -1, -1, 0, 1, -1, -1, 3, 0, -1, -1, -1, -1, -1, -1,
]
fn corner(grid, w, h, cx, cz, k) {
let x = min(cx + (if k == 1 || k == 2 { 1 } else { 0 }), w - 1)
let z = min(cz + (if k >= 2 { 1 } else { 0 }), h - 1)
grid[z * w + x]
}
// The case of cell (cx, cz), 0..15. A saddle whose centre is at or above
// the level joins its two high corners, which is the other saddle's pair of
// cuts (5 <-> 10).
fn cell_case(grid, w, h, level, cx, cz) {
let a = corner(grid, w, h, cx, cz, 0)
let b = corner(grid, w, h, cx, cz, 1)
let c = corner(grid, w, h, cx, cz, 2)
let d = corner(grid, w, h, cx, cz, 3)
let m = (if a >= level { 1 } else { 0 }) + (if b >= level { 2 } else { 0 }) + (if c >= level { 4 } else { 0 }) + (if d >= level { 8 } else { 0 })
let centre = (a + b + c + d) * 0.25
if m == 5 && centre >= level { 10 } elif m == 10 && centre >= level { 5 } else { m }
}
// Isoline segments crossing cell (cx, cz) at `level`: 0, 1 or 2.
fn segments(grid, w, h, level, cx, cz) {
let m = cell_case(grid, w, h, level, cx, cz)
if m == 0 || m == 15 { 0 } elif m == 5 || m == 10 { 2 } else { 1 }
}
// Where the level crosses edge e of the cell (cell units, x and z).
fn edge_point(grid, w, h, level, cx, cz, e) {
let p = corner(grid, w, h, cx, cz, e)
let q = corner(grid, w, h, cx, cz, (e + 1) % 4)
let t = if q != p { clamp((level - p) / (q - p), 0.0, 1.0) } else { 0.5 }
let x0 = float(cx) + (if e == 1 || e == 2 { 1.0 } else { 0.0 })
let z0 = float(cz) + (if e >= 2 { 1.0 } else { 0.0 })
let x1 = float(cx) + (if e == 0 || e == 1 { 1.0 } else { 0.0 })
let z1 = float(cz) + (if e == 1 || e == 2 { 1.0 } else { 0.0 })
vec2(mix(x0, x1, t), mix(z0, z1, t))
}
// Segment k (0 or 1) of the cell: vec4(x0, z0, x1, z1) in cell units.
fn segment(grid, w, h, level, cx, cz, k) {
let m = cell_case(grid, w, h, level, cx, cz)
let e0 = MS_EDGES[m * 4 + k * 2]
let e1 = MS_EDGES[m * 4 + k * 2 + 1]
let p = edge_point(grid, w, h, level, cx, cz, max(e0, 0))
let q = edge_point(grid, w, h, level, cx, cz, max(e1, 0))
vec4(p.x, p.y, q.x, q.y)
}
// The unit direction of a segment (a label's baseline) and its midpoint
// (the label's anchor).
fn segment_tangent(s) { normalize(vec2(s.z - s.x, s.w - s.y)) }
fn segment_mid(s) { vec2(s.x + s.z, s.y + s.w) * 0.5 }