CPU builtins
Builtins of easl's CPU runtime: printing, strings, windowing, GPU dispatch, input, textures, audio, MIDI input and output, and video. Most are CPU-only; each entry notes where else it works.
Printing#
print#
(print x: T): ()
Prints any value. u32s print as 1u, i32s bare (1), whole f32s with a trailing dot (2.), strings in double quotes, and arrays space-separated in square brackets.
Strings#
String values exist only in CPU code. Lengths and indices count characters, each one a Unicode codepoint, so (length "é✨") is 2u however many bytes those take, and an accented letter written as a letter plus a combining mark counts as two. String literals take the escapes \n, \t, \r, \", \\, and \ at the end of a line (see strings).
string#
(string x: T): String
Formats any value as print would, without quoting strings: (string 5u) is "5u".
concat#
(concat a: String b: String): String
(concat a: [T] b: [T]): [T]
Joins strings, or runtime-sized arrays. Associative.
substr#
(substr s: String start: u32 end: u32): String
The characters from start up to (not including) end. Out-of-range indices clamp, so this never fails.
get-char#
(get-char s: String index: u32): String
The one-character string at index. An index past the end is a runtime error, like indexing an array.
string->codepoints#
(string->codepoints s: String): [u32]
(codepoints->string codepoints: [u32]): String
Convert between a string and its characters' Unicode codepoints, over the whole string at once, which makes them the fast way to work through long text. A value that isn't a codepoint, like a surrogate or anything past U+10FFFF, becomes the replacement character U+FFFD. ~ does the same conversions: (let [codes: [u32] ~s] ...).
(length s) gives a string's character count, and == / != compare contents.
Files#
The builtins that read or write files (load-image, save-png, load-wav, load-wav-raw, get-wav-sample-rate, save-wav, load-video, load-text, save-text, file-exists?) take a path that, when relative, is relative to the file the call is written in, so a library loads its assets from its own directory wherever it's imported from.
resolve-path#
(resolve-path path: String): String
(resolve-path directory: String path: String): String
With one argument, the absolute path of path, relative to the file the call is written in: a library can hand its callers a path to one of its own files. With two, path relative to directory. Either way an absolute path comes back unchanged. File builtins treat their path argument as if it were wrapped in (resolve-path …).
The one-argument form only works when called directly, since it takes its directory from where the call is written; to pass resolve-path to a higher-order function, use the two-argument form.
current-directory#
(current-directory): String
The absolute directory of the file the call is written in. A helper that wraps a file builtin, in a library, resolves paths against its own directory; to let callers pass paths relative to their files, have it take a directory too, and call it as (save-filtered-png (current-directory) "frame.png"), resolving with (resolve-path directory path) inside.
Like resolve-path's one-argument form, it has to be called directly: it's an error to use it as a value.
Windowing#
spawn-window#
(spawn-window frame-fn: (Fn [] ())): ()
Opens a window and calls frame-fn once per frame until it closes. See the frame loop.
close-window#
(close-window): ()
Closes the window at the end of the current frame, ending the spawn-window loop.
GPU dispatch#
dispatch-render-shaders#
(dispatch-render-shaders vert: V frag: F vertices: u32 | [T]): ()
(dispatch-render-shaders vert: V frag: F vertices: u32 | [T] instances: u32 | [U]): ()
Draws vertices vertices with the vertex shader vert and fragment shader frag, which can be named functions or inline fns. With instances, it draws that many instances of them, and the vertex shader tells them apart with instance-index; the three-argument form draws one. Either count can be an array held by a GPU-bound variable (sized or runtime-sized): its length is the count, and its elements are vertex data for the vertex shader's data parameters, in order (see drawing with vertex data).
dispatch-indexed-render-shaders#
(dispatch-indexed-render-shaders vert: V frag: F indices: [u32]): ()
(dispatch-indexed-render-shaders vert: V frag: F indices: [u32] instances: u32): ()
(dispatch-indexed-render-shaders vert: V frag: F indices: [u32] vertices: [T]): ()
(dispatch-indexed-render-shaders vert: V frag: F indices: [u32] vertices: [T] instances: u32 | [U]): ()
Draws the vertices indices picks, in its order, each as (vertex-index) (and vertex data from that element of vertices). indices and the data arrays are held by GPU-bound variables, sized or runtime-sized. An index of 4294967295u (the largest u32) restarts a strip topology. The draw uses the render settings current when it's dispatched, and goes to the window unless set-render-target redirects it.
dispatch-compute-shader#
(dispatch-compute-shader entry: F workgroup-count: vec3u): ()
(dispatch-compute-shader entry: F workgroup-count: vec3u workgroup-size: vec3u): ()
Runs the compute shader entry over workgroup-count workgroups. Each workgroup runs workgroup-size threads: the size given here, or else the shader's @{workgroup-size N} annotation, or 1. A size given here must be a compile-time constant (a vec3u of literals, or a def holding one), since WGSL fixes it in the shader; easl compiles a version of entry for each size it's dispatched at, so the same function or closure can be dispatched at several sizes, including from inside a higher-order helper. A size can have at most 256 threads in all, with x and y at most 256 and z at most 64.
(defn compute-foreach-n [n: u32 f: (Fn [u32] ())]
(dispatch-compute-shader
(fn []
(let [i (.x (global-invocation-id))]
(when (< i n)
(f i))))
(vec3u (/ (+ n 63u) 64u) 1u 1u)
(vec3u 64u 1u 1u)))
Each component can be at most the GPU's maxComputeWorkgroupsPerDimension (65535 on most devices); a larger count is a runtime error. See CPU and GPU writes.
Window and input queries#
These work in both CPU and GPU code, and return the same value for the whole frame.
window-resolution#
(window-resolution): vec2u
The window's current size in pixels.
window-time#
(window-time): f32
Seconds since the window opened.
window-delta-time#
(window-delta-time): f32
Seconds elapsed between the previous frame and this one.
window-frame-index#
(window-frame-index): u32
The current frame number, counting from 0.
mouse-coords#
(mouse-coords): vec2u
The cursor position in pixels, relative to the window's top-left corner.
mouse-present?#
(mouse-present?): bool
Whether the cursor is currently over the window.
mouse-down?#
(mouse-down?): bool
Whether the primary mouse button is held.
mouse-just-down?#
(mouse-just-down?): bool
Whether the primary mouse button was pressed this frame.
mouse-right-down?#
(mouse-right-down?): bool
(mouse-right-just-down?): bool
Whether the right (secondary) mouse button is held, or was pressed this frame. In the browser, right clicks on the canvas don't open the context menu.
mouse-delta#
(mouse-delta): vec2f
The raw mouse motion since the previous frame, in device units, with +y pointing down. Unlike mouse-coords, it keeps reporting motion while the cursor is captured and pinned in place, which makes it the right input for first-person camera controls.
mouse-scroll-delta#
(mouse-scroll-delta): vec2f
How far the mouse wheel or trackpad scrolled since the previous frame, in pixels (the units of mouse-coords), with +y scrolling down (toward the end of a document) and +x scrolling right. Trackpads report pixels directly; a wheel that scrolls by lines counts 40 logical pixels per line. In the browser, scrolling over the canvas doesn't scroll the page.
capture-mouse#
(capture-mouse): ()
(release-mouse): ()
capture-mouse hides the cursor and locks it to the window (pointer lock), so the mouse can be moved indefinitely in any direction while mouse-delta reports the motion; release-mouse gives it back. Requests take effect at the end of the frame that makes them. Pressing Escape, or the window losing focus, always releases a captured cursor, so a program can never trap it; check mouse-captured? to see whether capture is currently active, e.g. to re-capture on the next click. In the browser, capture is only granted shortly after a click or key press, so request it in response to one. CPU code only.
@cpu
(defn main []
(let [@var yaw 0.]
(spawn-window (fn []
(when (and (mouse-just-down?) (not (mouse-captured?)))
(capture-mouse))
(when (mouse-captured?)
(+= yaw (* 0.002 (.x (mouse-delta)))))))))
mouse-captured?#
(mouse-captured?): bool
Whether capture-mouse currently holds the cursor. Like the other input queries, it reads the state at the start of the frame, so it doesn't change in the frame that calls capture-mouse or release-mouse — it reflects the request from the next frame on.
key-down?#
(key-down? key: String): bool
Whether the named key is held, e.g. (key-down? "a"). Character keys are named by their lowercase character, ignoring modifiers (shift+1 is still "1"). Other keys use these names: " " (or "space"), "shift", "ctrl", "alt", "super" (cmd / the Windows key), "escape", "enter", "tab", "backspace", "delete", "up", "down", "left", and "right". Shader code needs a string literal; CPU code can compute the name, e.g. (key-down? (string i)).
key-just-down?#
(key-just-down? key: String): bool
Whether the named key was pressed this frame. Same key-naming rules as key-down?.
Textures and render targets#
load-image#
(load-image path: String): (Texture2D f32)
Loads a PNG or JPEG into a texture, decoding its colors to linear values. A relative path is relative to the file the call is written in.
blank-texture#
(blank-texture width: u32 height: u32): (Texture2D f32)
(blank-texture size: vec2u): (Texture2D f32)
An empty texture of the given size, e.g. to render into.
Sampler#
(Sampler filter: FilterMode address: AddressMode): Sampler
Constructs a sampler, to assign to a Sampler var. filter is how texture samples between texel centers are read:
FilterMode/Nearest: the nearest texel, for crisp, blocky scaling.FilterMode/Linear: a blend of the surrounding texels, for smooth scaling.
address is how coordinates outside [0, 1] are read, on both axes:
AddressMode/ClampToEdge: the texel at the nearest edge.AddressMode/Repeat: the texture tiles.AddressMode/MirrorRepeat: the texture tiles, flipping every other copy.
A Sampler var that's never assigned is (Sampler FilterMode/Nearest AddressMode/ClampToEdge).
set-render-target#
(set-render-target t: (Texture2D f32)): ()
Sends later dispatch-render-shaders calls (and clears) to t instead of the window, until reset-render-target, across frames too. Later dispatches can sample what was drawn; see render targets.
reset-render-target#
(reset-render-target): ()
Sends later render dispatches back to the window.
Render settings#
How a draw is rasterized and how its output combines with its target. Each setter changes one setting for every later draw, until it's changed again, across frames too; a draw uses the settings current when dispatch-render-shaders is called. Settings are set from CPU code only.
(set-blend src: BlendFactor dst: BlendFactor op: BlendOperation): ()
(set-blend src: BlendFactor dst: BlendFactor op: BlendOperation
alpha-src: BlendFactor alpha-dst: BlendFactor
alpha-op: BlendOperation): ()
(set-blend-constant color: vec4f): ()
(set-depth-test test: DepthTest): ()
(set-depth-write enabled: bool): ()
(set-cull-mode mode: CullMode): ()
(set-front-face face: FrontFace): ()
(set-topology topology: Topology): ()
(reset-render-settings): ()
reset-render-settings restores every setting's default (the render target is separate: see reset-render-target).
set-blend: how a fragment's color (the source) combines with what the target holds (the destination). Each channel becomesop(src-color * src, dst-color * dst). The three-argument form treats alpha like the color channels; the six-argument form gives alpha its own factors and operation. These are WebGPU's blend settings, all of them:BlendFactor/Zero,One,Src,OneMinusSrc,SrcAlpha,OneMinusSrcAlpha,Dst,OneMinusDst,DstAlpha,OneMinusDstAlpha,SrcAlphaSaturated,Constant,OneMinusConstant.SrcandDstweigh each channel by the matching channel,Constantby the blend constant, andSrcAlphaSaturatedbymin(src.a, 1 - dst.a)(by1for alpha).BlendOperation/Add,Subtract(source minus destination),ReverseSubtract,Min,Max.MinandMaxcompare the unweighted colors, ignoring the factors.- The default is no blending: a fragment overwrites the target, alpha included (
BlendFactor/One BlendFactor/Zero BlendOperation/Add). Blending is opt-in because it costs fill rate, and on tile-based GPUs (Apple's) it also turns off hidden-surface removal. - Straight alpha, where a fragment
(vec4f color alpha)covers the target byalpha:(set-blend BlendFactor/SrcAlpha BlendFactor/OneMinusSrcAlpha BlendOperation/Add BlendFactor/One BlendFactor/OneMinusSrcAlpha BlendOperation/Add). Drawing this way into a transparent texture leaves premultiplied contents, so composite such a texture with premultiplied blending,BlendFactor/One BlendFactor/OneMinusSrcAlpha.BlendFactor/One BlendFactor/One BlendOperation/Addis additive.
set-blend-constant: the color theConstantfactors use (default(vec4f 0.)).DepthTest/Off(the default),Less,LessEqual,Greater,GreaterEqual,Equal,Always: whether a fragment is tested against the target's depth buffer, and the comparison its depth must pass against the stored one. A fragment's depth is its position'sz, from0.near to1.far, unless the fragment shader returns a struct with a@{builtin frag-depth}field; withDepthTest/Off, that output is ignored.Offneither tests nor writes depth;Alwayspasses every fragment but still writes depth.set-depth-write: whether a fragment that passes the depth test stores its depth (defaulttrue).CullMode/Off(the default),Back,Front: which triangles to discard by the way they face.FrontFace/CounterClockwise(the default),Clockwise: the winding of a front-facing triangle (also whatfront-facingreports).Topology/TriangleList(the default),TriangleStrip,LineList,LineStrip,PointList: how the vertices form primitives.
Every render target (the window, and each texture) has its own depth buffer, made the first time a draw into it tests depth. The window's starts each frame cleared to 1.; a texture's keeps its contents, like its colors.
clear-target#
(clear-target color: vec4f): ()
Clears the current render target to color, and its depth buffer to 1., in order with the frame's draws. A texture keeps what's drawn into it until it's cleared; the window starts each frame cleared to opaque black, so to use another background, clear it before the frame's first draw.
clear-depth#
(clear-depth): ()
(clear-depth value: f32): ()
Clears the current render target's depth buffer to 1. (or value), leaving its colors.
save-png#
(save-png t: (Texture2D f32) path: String): ()
Saves a texture as a PNG, encoding its linear colors as sRGB, so the file looks like the texture drawn to the window. A relative path is relative to the file the call is written in; missing directories are created. It works on textures the GPU just rendered into:
(set-render-target tex)
(dispatch-render-shaders vertex fragment 3)
(reset-render-target)
(save-png tex "frame.png")
Audio#
See audio in the runtime guide.
start-audio#
(start-audio f: (Fn [] f32)): ()
(start-audio f: (Fn [f32] f32)): ()
Starts (or switches) the audio output stream. f takes no arguments and is called once per sample, returning a sample in [-1, 1]; (audio-time) gives the time in seconds. Usually a closure, whose captured state persists between samples. Each call hands off the function it's given, so call it once or when the sound should change, not every frame.
sample-rate#
(sample-rate): f32
The audio stream's sample rate in Hz. Works in any code, including closure factories that run before the stream starts.
audio-time#
(audio-time): f32
(audio-time period: f32): f32
The stream's clock in seconds. Audio code only.
With a period, the clock loops: it counts up from 0. to period and starts over, giving 0. for a period that isn't positive. Prefer it to (% (audio-time) period): it's computed from the exact number of samples played, so it stays precise however long the stream runs, while an f32 time loses precision as it grows (after a day, it's only precise to about 8ms).
reset-audio-time#
(reset-audio-time): ()
Restarts the audio clock: (audio-time) and (audio-time period) count from 0. again. Called from CPU code, it takes effect from the audio thread's next buffer, which is handy for restarting a patch without restarting the program. Called from audio code, the current sample keeps its time and the next one is at 0.. The clock is the whole stream's, so a reset affects every voice. Stateful closures keep their state; call start-audio with a fresh closure to reset that too.
audio-input#
(audio-input): f32
The current sample from the input device chosen with start-listening, mono and roughly in [-1, 1]; 0. when nothing is listening. Audio code only.
start-listening#
(start-listening): ()
(start-listening-from device: String): ()
Starts capturing audio input from the default input device, or from the first device whose name contains device. There's no resampling, so the input and output devices should share a sample rate.
listenable-sources#
(listenable-sources): [String]
The available input device names, sorted.
load-wav#
(load-wav path: String): [f32]
(load-wav-raw path: String): [i32]
Loads a .wav file as mono samples (multi-channel files are averaged). load-wav gives floats in [-1, 1]; load-wav-raw gives the raw integers (16-bit range). The samples are at the file's own rate — see get-wav-sample-rate. A relative path is relative to the file the call is written in.
(var kick: [f32])
@cpu
(defn main []
(= kick (load-wav "kick.wav"))
(start-audio (fn []
(let [i (u32 (* (audio-time) (sample-rate)))]
(if (< i (array-length kick)) (kick i) 0.))))
(spawn-window (fn [] ())))
get-wav-sample-rate#
(get-wav-sample-rate path: String): f32
A .wav file's sample rate, without loading its samples.
save-wav#
(save-wav path: String samples: [f32] rate: f32): ()
Writes mono samples to a 16-bit PCM .wav file; values outside [-1, 1] clip.
Text files#
load-text#
(load-text path: String): String
The contents of a UTF-8 text file. A file that's missing, unreadable, or not UTF-8 is a runtime error (see file-exists?). A relative path is relative to the file the call is written in. Not available in the browser.
save-text#
(save-text text: String path: String): ()
Writes text to the file at path, replacing any file there and creating any missing directories. A relative path is relative to the file the call is written in. Not available in the browser.
file-exists?#
(file-exists? path: String): bool
Whether path is a file; a directory isn't one. A relative path is relative to the file the call is written in. Not available in the browser.
(let [settings (if (file-exists? "settings.txt")
(load-text "settings.txt")
"defaults")]
(save-text settings "settings.txt"))
MIDI#
MIDI input from every connected device, merged. These work in CPU, audio, and shader code; the CPU and GPU see a per-frame snapshot, the audio thread a per-buffer one. Held notes are MidiNote structs with fields note: u32, velocity: f32, and aftertouch: f32 (all 0.–1.).
down-midi-notes#
(down-midi-notes): [MidiNote]
The currently held notes, in the order they were pressed.
get-midi-note#
(get-midi-note note: u32): (Option MidiNote)
Note number note if it's held (Some), else None.
midi-cc#
(midi-cc controller: u32): f32
The last value of MIDI controller controller (0–127), in 0.–1..
midi-aftertouch#
(midi-aftertouch): f32
Channel pressure, in 0.–1..
midi-pitch-bend#
(midi-pitch-bend): f32
The pitch bend, from -1. to 1., centered at 0..
MIDI output#
MIDI messages sent to every MIDI output port, or to the ones picked with select-midi-output. The send-midi-* builtins work in CPU and audio code; audio code's messages go out at the end of each audio buffer. Values use the same ranges as MIDI input: velocities, controller values, and pressures are 0.–1. and pitch bend -1.–1. (clamped), while note numbers, controllers, programs, and data bytes are 0–127 and channels 0–15 (anything larger is a runtime error). Each send-midi-* builtin takes an optional trailing channel, sending on channel 0 without one. Not supported on the web yet.
send-midi-note-on#
(send-midi-note-on note: u32 velocity: f32 [channel: u32]): ()
(send-midi-note-off note: u32 [channel: u32]): ()
Starts or ends a note. A nonzero velocity always sounds (it never rounds down to 0, which would end the note).
send-midi-cc#
(send-midi-cc controller: u32 value: f32 [channel: u32]): ()
send-midi-pitch-bend#
(send-midi-pitch-bend value: f32 [channel: u32]): ()
send-midi-aftertouch#
(send-midi-aftertouch value: f32 [channel: u32]): ()
(send-midi-note-aftertouch note: u32 value: f32 [channel: u32]): ()
Channel pressure, or one note's pressure.
send-midi-program-change#
(send-midi-program-change program: u32 [channel: u32]): ()
send-midi-message#
(send-midi-message status: u32 [data1: u32 [data2: u32]]): ()
A raw message: a status byte (128–255) followed by exactly as many data bytes as it takes, e.g. (send-midi-message 248u) for a clock tick.
send-midi-sysex#
(send-midi-sysex data: [u32]): ()
A system-exclusive message carrying data, the bytes between its opening 0xF0 and closing 0xF7: (send-midi-sysex ~[0u 32u 41u 2u 12u 14u 1u]). CPU code only.
midi-output-ports#
(midi-output-ports): [String]
(select-midi-output port: String): ()
The available MIDI output port names, sorted. select-midi-output sends later messages only to the ports whose names contain port; it's a runtime error if none does. CPU code only.
Video#
Requires building easl with the video feature and an ffmpeg executable on the path. A Video is a small value — which file, and which frame — so copying one and scrubbing the copy leaves the original alone.
load-video#
(load-video path: String): Video
Opens a video file, positioned at frame 0.
progress-video-frame#
(progress-video-frame v: Video): ()
(jump-to-video-frame v: Video frame: u32): ()
Advances v to its next frame, or to frame frame. v must be a mutable place.
get-current-frame-index#
(get-current-frame-index v: Video): u32
(get-video-length v: Video): u32
(length v: Video): u32
The current frame index, and the number of frames (length is the same as get-video-length).
get-video-frame-texture#
(get-video-frame-texture v: Video): (Texture2D f32)
Decodes the current frame, with linear colors like load-image. Only assignable directly to a texture variable: (= tex (get-video-frame-texture v)).
See also#
- Arrays:
zeroed-arrayandinto-dynamic-arraysize and fill runtime-sized storage buffers;push,insert,remove,reverse, andsubarraybuild new arrays.