The Goose standard library
The standard library has eighteen modules under stdlib/:
std, dictionary, slot_pool,
slice_pool, vec, math,
os, ffi, binary,
base64, csv, json,
regex, audio, gfx,
physics, ui, and exception.
Import each module by name, for example import std;. The
compiler locates the library in its source tree; use
--stdlib <dir> or GOOSE_STDLIB to select
another location. Everything is written in Goose except the following
parts, all reached through extern fn (spec §7.10):
- the C behind
os(src/runtime/runtime_os.h); - the byte search behind
std'sfind_anyandfind_pair(src/runtime/runtime_impl.h); std'spopcount,clzandctz, andffi's two copies (src/runtime/runtime_ext.h);- libm, behind
math; - the graphics layer behind
gfx, the ui layer behinduiand the PCM mixer behindaudio, which are in the goose_gfx package; - the physics layer behind
physics, which is in the goose_physics package.
Both packages are repositories of their own. The design and its
rationale are described in design/stdlib_design.md and in
the packages' own docs/gfx.md,
docs/ui.md,
docs/audio.md
and docs/physics.md.
This document is the reference.
For text rendering, fonts and game HUDs, start with
ui, which
renders over gfx. For external binary file formats, use binary; from_bytes reads
Goose's own serialization format. Text formats have modules of their
own: json, csv and base64, and regex searches text with regular
expressions.
The library uses these conventions:
- A function that reads or mutates elements in place takes
xs: T[:]; every array kind and every slice coerces to it:sort(arr),sort(arr[1..]). - A function that changes the length takes
xs: A&, the whole array by reference, and instantiates for whatever array kind it is given, provided that kind has the operations used. A grow-only array shrinks through the reference wherever nothing in the caller points into it (spec §5.1), soremove_at(xs, i)works on a[>..]local; the checker rejects the call if a live reference or slice could be invalidated. - Nothing non-fixed is taken by value (spec §4.1):
f(xs)binds by reference, and a function that wants its own copy callscopy(xs). - Fresh arrays come back as
T[>..], built directly in the caller's destination:let ys = filter(xs) { it > 0 };,out.append(map(xs) { … }). - "Maybe an element" is a
T?: a reference into the input, or null for none. Narrow it withif/guard. Positions arei64,-1for none. Two-outcome scalars return a trailingbool:let v, ok = parse_int(s);. - Blocks:
xs.find() { it > 3 },sort(xs) { a, b => a.age < b.age }. A comparator is always "a goes before b". Sorting, heap and extremum functions come with and without a block; without a block they compare with<. - Text output goes into a
u8[>..]&builder the caller passes;str(…)is the fresh-string form. - UFCS applies:
xs.sort(),d.insert(k, v),r.rand_int(6). - A function taking an element by value (
push_n,insert_at,fill,heap_push) cannot take one that contains self-relative references, because those values cannot be copied (spec §3.9). Construct them in place. - The functions that move elements around (
swap_at,reverse,shuffle,sort,stable_sort,fill,copy_into,insert_at,remove_at,swap_remove,retain,dedup, the heap functions) move the elements themselves, including references:reverse(refs)reorders an array of references and leaves what they point to unchanged, and an array of references to values that cannot be copied (spec §3.9) can be sorted too. They store whole elements with.=, as generic code may (spec §3.8). An array of relative references (self-relative orin pool) is reordered the same way. ATlocal or parameter holds the plain reference, and each element the reference is stored back into encodes it again. For a self-relative link, this requires the link to point within that element's root (spec §3.9), which a link moved within its own array does.swap(a, b)exchanges the valuesaandbrefer to.remove_at,swap_removeandheap_popreturn a copy of the removed value; for a reference element, they return a copy of its pointee. A reference element passed by value (insert_at,heap_push) is written&x, which makesTthe reference type.stable_sortsorts an array ofin poollinks, but its temporary array cannot hold copies of self-relative links (spec §3.9). - The
std,dictionary,slot_pool,slice_pool,vec,math, andosnames are global;ffi,binary,base64,csv,json,regex,audio,gfx,physics,uiandexceptionuse their own namespaces, and so do the helpersstdandjsonread and write float text with, infloat_text, andstd's ASCII character classes, inascii. A local namedfillorcountshadows the global function of that name, so a call to that function is an error. A global variable of such a name does not shadow it, because the name in a call resolves past global variables to the functions (spec §11.1). Slice allocator implementation types and helpers useslice_pool_detail; applications should use the public pool operations.
std
Scalars and bits
fn min<T>(a: T, b: T) -> T fn max<T>(a: T, b: T) -> T
fn clamp<T>(x: T, lo: T, hi: T) -> T
fn abs<T>(x: T) -> T // integers; abs(f32) and abs(f64) overloads
fn rem<T>(a: T, b: T) -> T // integers; pairs with `/`
fn div_euclid<T>(a: T, b: T) -> T // integers; pairs with `%`
fn sign<T>(x: T) -> T // -1, 0, 1 at x's type; f32/f64 overloads
fn lerp(a: f64, b: f64, t: f64) -> f64 // and f32
fn next_pow2(x: i64) -> i64 // smallest power of two >= max(x, 1); aborts past 2^62
fn popcount(x: u64) -> i64
fn clz(x: u64) -> i64 // 64 for 0
fn ctz(x: u64) -> i64 // 64 for 0
fn rotl<N>(x: u32, n: N) -> u32 // and u64; rotr rotates right
fn byte_swap(x: u32) -> u32 // and u16, u64
fn is_pow2<T>(x: T) -> bool // integers; false for x <= 0
fn ilog2<T>(x: T) -> i64 // floor(log2(x)); -1 for x <= 0
fn isqrt<T>(x: T) -> T // floor(sqrt(x)); aborts for x < 0
fn gcd<T>(a: T, b: T) -> T // never negative; gcd(0, 0) == 0
fn lcm<T>(a: T, b: T) -> T // never negative; 0 if either is 0
fn swap<T>(a: T&, b: T&) // swap(x, y)These are all checkfree functions or
extern fns, so a checkfree function (spec
§7.14) can call them.
popcount, clz and ctz are
implemented in the runtime. Each is a single instruction where the C
compiler has one (MSVC's popcount needs
/arch:AVX or higher for that), and a loop under TinyCC.
rotl, rotr and byte_swap are the
runtime's as well, and one instruction each under GCC, clang and MSVC;
the rest are Goose.
rotl and rotr take a count of any integer
type modulo the width, so every count is defined and a negative one
rotates the other way: rotl(x, -1) == rotr(x, 1) and
rotl(x, 32) == x for a u32. A narrower value
widens to both overloads equally, so it names the width to rotate at, as
rotl(b as u32, 4); byte_swap likewise takes a
u16, u32 or u64 by its exact
type. ilog2 returns -1 where there is no logarithm, as a
search returns -1 where there is no position, so
ilog2(x) + 1 is the number of bits a positive
x needs.
gcd and lcm take any integer type and work
on the magnitudes: gcd(-12, 18) == 6 and
lcm(-4, 6) == 12. Where the result does not fit in the
type, as gcd(i64.min, 0) (2^63) or lcm of the
u8s 16 and 17, they fail an assert, unsigned
types included; a wrapped value is no divisor or multiple.
isqrt is exact at every u64, without going
through a float.
rem is the remainder of truncating division:
rem(-7, 3) == -1, paired with -7 / 3 == -2.
div_euclid is the quotient paired with Euclidean
%: div_euclid(-7, 3) == -3 and
-7 % 3 == 2. It rounds down for positive divisors and up
for negative ones (div_euclid(-7, -3) == 3). Thus a
coordinate's tile and offset are div_euclid(x, width) and
x % width.
Both functions return the operands' integer type and abort on a zero
divisor. rem(MIN, -1) is zero, including at
i64; div_euclid follows /'s
overflow rules (always abort at i64.min, -1; at narrower
signed widths, debug abort and release wrap). Unsigned rem
and div_euclid are ordinary % and
/. Neither function takes floats; float % is
already the truncating remainder (fmod). The identities
b * (a / b) + rem(a, b) == a and
b * div_euclid(a, b) + a % b == a hold mathematically when
the quotient is representable, but evaluating their intermediate
products can overflow.
Hashing
fn hash(x: i8) -> u64 // ... one overload per integer type
fn hash(x: bool) -> u64 fn hash(x: f32) -> u64 fn hash(x: f64) -> u64
fn hash(s: u8[:]) -> u64 // 8 bytes per multiply; any u8 array coerces
fn hash_combine(seed: u64, h: u64) -> u64These hashes mix all of their input into the low 32 bits. The scalar
overloads and hash_combine are checkfree (spec
§7.14). dictionary masks those bits to find the home slot
and keeps them as the slot's tag. A float hashes as its bits,
-0.0 as 0.0, and an f32 as the
f64 of its value. dictionary also picks up a
hash overload for a user key type, declared globally or in
the key type's namespace (it calls key.hash(), §11.1), and
that overload should do the same, as hash_combine of the
fields' hashes does:
struct key { a: i64, b: i64 }
fn hash(k: key) -> u64 { hash_combine(hash(k.a), hash(k.b)) }Random numbers
struct rng { s: u64 } // splitmix64; the seed is the state
fn rand_u64(r: rng&) -> u64
fn rand_int(r: rng&, n: i64) -> i64 // uniform in [0, n), without modulo bias; asserts n > 0
fn rand_range(r: rng&, size: i64) -> i64 // uniform in [0, size); asserts size > 0
fn rand_range(r: rng&, lo: i64, size: i64) -> i64 // uniform in [lo, lo + size); asserts size > 0
fn rand_flt(r: rng&) -> f64 // uniform in [0, 1), 53 bits
fn rand_f32(r: rng&) -> f32 // uniform in [0, 1), 24 bits
fn rand_gaussian(r: rng&) -> f64 // standard normal distribution: mean 0, variance 1
fn shuffle<T>(xs: T[:], r: rng&) // Fisher-Yatesvar r = rng { os.random_seed() }; // or any fixed seed
let d = r.rand_range(6); // 0 to 5
let roll = r.rand_range(1, 6); // a die: 1 to 6
let height = 170.0 + 7.5 * r.rand_gaussian();rand_range takes a count rather than an end, as a
slice's length does, so rand_range(r, lo, n) has exactly
n results; lo + size - 1 has to fit in
i64. Without lo it starts at 0, the same as
rand_int. rand_flt and rand_f32
take the top 53 or 24 bits of a draw, so every result is a multiple of
2^-53 or 2^-24 and each is equally likely. rand_gaussian
draws from the normal (Gaussian) distribution by Marsaglia's polar
method. It draws pairs from rand_flt until one falls inside
the unit circle, which takes about 1.27 tries on average. The method
yields two draws, but rng keeps only its seed, so the
second one is dropped. The draws from a seed are the same on every run,
and on every platform whose libm log gives the same
results.
Arrays: reading and searching
fn each_rev<T, F>(xs: T[:]) // F(x), last to first
fn each_chunk<T, F>(xs: T[:], n: i64) // F(chunk: T[:], i); the last chunk may be short
fn find<T, F>(xs: T[:]) -> T? // first x with F(x), as a reference
fn find_index<T, F>(xs: T[:]) -> i64
fn position<T>(xs: T[:], v: T) -> i64 // first i with xs[i] == v
fn contains<T>(xs: T[:], v: T) -> bool
fn any<T, F>(xs: T[:]) -> bool fn all<T, F>(xs: T[:]) -> bool
fn count<T, F>(xs: T[:]) -> i64 // number of x with F(x)
fn min<T>(xs: T[:]) -> T // asserts xs.len > 0; min(xs) { a, b => ... } too
fn max<T>(xs: T[:]) -> T
fn min_by<T, F>(xs: T[:]) -> i64 // index of the smallest F(x); -1 if empty
fn max_by<T, F>(xs: T[:]) -> i64
fn last<T>(xs: T[:]) -> T& // asserts xs.len > 0
fn lower_bound<T, F>(xs: T[:]) -> i64 // first i with !F(xs[i]); F true on a prefix
fn binary_search<T>(xs: T[:], v: T) -> i64, bool // sorted by < (or { a, b => ... }): position, found
fn find<T>(xs: T[:], sub: T[:]) -> i64 // first occurrence of a subsequence, -1 if none
fn rfind<T>(xs: T[:], sub: T[:]) -> i64
fn starts_with<T>(xs: T[:], p: T[:]) -> bool fn ends_with<T>(xs: T[:], p: T[:]) -> boolfind/rfind/starts_with/ends_with
are generic over the element, so they are the string functions too:
find(line, "://"). find and rfind
compare a whole subsequence only where its first element matches. (The
function is named position rather than
index_of because index_of is the builtin that
turns an element reference into its index.) min(xs) and
max(xs) are checkfree; the forms that take a
comparator are not, since that would require every comparator to be
checkfree.
let big = xs.find() { it > 100 };
if big { print(big); }
var row = 0;
each_chunk(pixels, width) { line, y => row += line.len; };Byte sets
struct ByteSet { members: bool[256], ... } // a set of bytes, prepared for searching
fn byte_set(members: bool[256]) -> ByteSet
fn byte_set(bytes: u8[:]) -> ByteSet // the bytes listed: byte_set(",\"\n")
fn find_any(s: const u8[:], set: const ByteSet&) -> i64
// first i with s[i] in the set, -1 if none
fn find_pair(s: u8[:], a: const ByteSet&, d: i64, b: const ByteSet&) -> i64
// first i with s[i] in a and s[i + d] in b
// (i + d < s.len), -1 if none; d >= 0These functions scan in the runtime. On x86-64 they test 16 bytes at
a time with SSE2, which every such CPU has. A set that is not at most
three bytes, one range, or the complement of either is looked up in
nibble tables, which needs SSSE3; the runtime checks for it when the
program starts. Elsewhere, and in a JIT run, they test one byte at a
time, and a set of a single byte is searched with the C library's
memchr. byte_set works out how to test a set
by going over all 256 bytes, so make the set once, in a variable,
outside the loop that searches. members is the set, and the
other fields are used by the search. find_pair is the test
for a prefilter that knows two bytes of a match that are d
apart; regex uses it this way with the
two rarest of a pattern's leading bytes.
let special = byte_set(",\"\n");
var pos = 0;
loop {
let k = find_any(text[pos..], special);
if k < 0 { break; }
pos += k;
handle(text[pos]);
pos += 1;
}Arrays: transforming
fn map<T, F>(xs: T[:]) // -> U[>..], U the block's result type
fn filter<T, F>(xs: T[:]) -> T[>..]
fn fold<T, A, F>(xs: T[:], acc: A) -> A // acc = F(acc, x)
fn sum<T>(xs: T[:]) -> T // from default<T>()
fn concat<T>(a: T[:], b: T[:]) -> T[>..]let squares = xs.map() { it * it };
let evens = xs.filter() { it % 2 == 0 };
let total = fold(xs, 0.0) { a, x => a + x };Arrays: in place
fn fill<T>(xs: T[:], v: T)
fn copy_into<T>(dst: T[:], src: T[:]) // equal lengths; first to last
fn reverse<T>(xs: T[:])
fn sort<T>(xs: T[:]) // by <; sort(xs) { a, b => ... } by the block
fn stable_sort<T>(xs: T[:]) // natural merge sort; one temporary of xs.len elements
fn to_lower(s: u8[:]) // ASCII, in place
fn to_upper(s: u8[:])copy_into preserves its element-by-element,
first-to-last semantics. For a checked copy that preserves the source
values when the slices overlap, use the language operation
dst.move_from(src) (fixed-size, copyable elements).
sort is a pattern-defeating quicksort based on pdqsort
and Rust's ipnsort. It is unstable (the order it leaves equal elements
in is unspecified), works in place and does not allocate. Its partition
does not branch on the comparisons, and a run of equal keys takes a
linear pass rather than n log n. Heapsort takes over a range that
partitioning keeps splitting badly, so the worst case is O(n log n).
stable_sort is a natural merge sort: it finds the runs
already in order (or in strictly descending order, which it reverses)
and merges them pass by pass between xs and a temporary of
xs.len elements on a fresh data stack.
Both make a single pass over input that is already in order or
strictly descending, and neither allocates for such input or for 20
elements or fewer. A comparator that is not a strict weak ordering
(<=, or < over floats with NaNs) leaves
the elements in an unspecified order, but the result is always a
permutation of the input, and neither function aborts.
A let array can be sorted because let
prevents rebinding, not element writes. A const array or
read-only slice cannot be sorted (spec §9.5).
Arrays: changing the length
fn push_n<A, T>(xs: A&, v: T, n: i64) // n pushes
fn insert_at<A, T>(xs: A&, i: i64, v: T) // shifts [i..) up; needs push
fn remove_at<A>(xs: A&, i: i64) -> T // shifts down; needs pop
fn swap_remove<A>(xs: A&, i: i64) -> T // O(1), reorders
fn retain<A, F>(xs: A&) // keeps x with F(x), in order; needs resize
fn dedup<A>(xs: A&) // drops adjacent duplicates
fn heap_push<A, T>(xs: A&, v: T) // min-heap by <; block forms of all three
fn heap_pop<A>(xs: A&) -> T
fn make_heap<A>(xs: A&) // O(n)A [>..<] is the natural heap and queue container;
a limited [..k] field works as well.
The heap is binary, with the children of i at
2i + 1 and 2i + 2. Its sifts move a hole
instead of swapping elements. The element being placed waits in a local
while each level moves one element into the hole, and it is stored once,
where the hole stops. heap_pop walks the hole from the root
down to a leaf, along the child that goes first, and then sifts the last
element up from there, as libstdc++ and Rust do. This takes about one
comparison per level, where sifting the element down from the root takes
two. For a comparator that is a strict weak order, every operation
leaves the same layout as the textbook swapping sifts, including for
ties, so elements that tie come out in the same order. A comparator that
reads the heap while it runs sees one slot holding a copy of its
neighbour rather than the element waiting outside.
var q: i64[>..<] = [];
q.heap_push(5); q.heap_push(1);
let smallest = q.heap_pop();Strings
fn format_int<B>(out: B&, v: i64, base: i64, width: i64, padc: u8) // base 2..36, right-aligned; aborts outside it
fn format_uint<B>(out: B&, v: u64, base: i64, width: i64, padc: u8) // B: u8[>..] or u8[..k]
fn format_flt(out: u8[>..]&, v: f64, decimals: i64) // fixed decimals, the exact value rounded half up; inf/-inf/nan as print writes them
fn parse_int(s: u8[:], base: i64 = 10) -> i64, bool // optional sign, whole string; false outside i64 or base 2..36
fn parse_uint(s: u8[:], base: i64 = 10) -> u64, bool // optional '+', whole string; false on '-' or outside u64 or base 2..36
fn parse_flt(s: u8[:]) -> f64, bool // sign, fraction, exponent; correctly rounded
fn format_uleb(out: u8[>..]&, v: i64) // LEB128 (3.6), 1-10 bytes
fn parse_uleb(s: u8[:]) -> i64, i64 // value and byte count; 0, 0 if malformed
fn compare(a: u8[:], b: u8[:]) -> i64 // bytewise: -1, 0, 1
fn trim(s: u8[:]) -> u8[:] // ASCII whitespace; a sub-slice
fn trim_start(s: u8[:]) -> u8[:] fn trim_end(s: u8[:]) -> u8[:]
fn split(s: u8[:], sep: u8) -> const u8[:][>..] // read-only slices into s; empty parts kept
fn split(s: u8[:], sep: u8[:]) -> const u8[:][>..] // matched from the left; an empty sep: [s]
fn each_split<F>(s: u8[:], sep: u8) // F(part); no array built
fn each_split<F>(s: u8[:], sep: u8[:])
fn split_once(s: u8[:], sep: u8) -> const u8[:], const u8[:], bool // before, after the first sep
fn split_once(s: u8[:], sep: u8[:]) -> const u8[:], const u8[:], bool // not found: s, "", false
fn words(s: u8[:]) -> const u8[:][>..] // split at runs of ASCII whitespace; no empty words
fn each_word<F>(s: u8[:]) // F(word)
fn join<T>(out: u8[>..]&, parts: T[:], sep: u8[:]) // T any u8 array/slice type
fn format_replaced(out: u8[>..]&, s: u8[:], old: u8[:], with: u8[:])
fn each_utf8<F>(s: u8[:]) // F(codepoint); malformed bytes give 0xFFFD
fn push_utf8(out: u8[>..]&, cp: i64) // 0xFFFD's bytes for cp outside 0..0x10FFFF
fn ascii::is_digit(c: u8) -> bool // '0'-'9'
fn ascii::is_alpha(c: u8) -> bool // 'a'-'z', 'A'-'Z'
fn ascii::is_alnum(c: u8) -> bool // either
fn ascii::hex_value(c: u8) -> i64 // 0-15 for 0-9, a-f, A-F; -1 otherwiseThe builtins format/str/print
do the plain rendering of scalars and strings; the functions here add
control over the output, and parsing. In base 10,
format_int and format_uint write the same text
as format (after any padding) and run as fast.
format_int and format_uint abort when
base is outside 2..36; parse_int and
parse_uint return false for such a base.
parse_flt gives the double nearest to the decimal, as C's
strtod does, so a float's text from print or
format reads back as the same value. Strings are
u8 arrays: input is a u8[:], output goes to a
u8[>..]& builder, and storage is a u8[]
or u8[..k].
split_once cuts s at the first sep, as Go's
strings.Cut does: the part before and the part after,
read-only slices into s, and true. When sep is not in s it
gives s whole, an empty slice and false, so the first part
is the whole field either way. An empty sep is never found, as in
split.
The character classes are ASCII only, as C's
<ctype.h> is in the C locale and Rust's
u8::is_ascii_digit and its kin are: no byte from 0x80 up is
in any of them. They live in the namespace ascii, which
import std; brings in, so that short, common names like
is_digit stay free in the global one.
let text = "1f8B rest";
var v = 0;
var i = 0;
while i < text.len && ascii::hex_value(text[i]) >= 0 {
v = v * 16 + ascii::hex_value(text[i]);
i++;
}
print(v, " ", i, " ", ascii::is_alpha(text[i + 1])); // 8075 4 trueeach_utf8 decodes as the WHATWG decoder does. A sequence
cut short, an overlong form, a surrogate or a code point past 0x10FFFF
gives one 0xFFFD for each maximal subpart (Unicode §3.9), and the byte
that broke a sequence starts the next one: [0xC3, 'A']
gives 0xFFFD and then A. push_utf8 writes a
surrogate as the three bytes of its own code, which is how
json keeps a lone \u escape.
var line: u8[>..] = [];
format(line, "x=");
format_int(line, x, 16, 8, '0');
each_split(text, '\n') { handle(trim(it)); };
let n, ok = parse_int(trim(field));
let key, value, has_value = split_once(arg, '='); // "-o=out": "-o", "out", true
let cmd = words(input); // "take the sword": ["take", "the", "sword"]
if cmd.len > 1 && cmd[0] == "take" { take(cmd[cmd.len - 1]); }format_uleb and parse_uleb write and read
the length prefix used by serialized images
(design/serialization.md). Writing the prefix separately
lets a program save a bytes_of view without first copying
it into an image. Reading the prefix tells a stream reader how many
payload bytes to expect.
let payload = pool.bytes_of(); // a view, nothing copied
var head: u8[>..] = [];
format_uleb(head, payload.len);
write_file(path, head); append_file(path, payload);format_flt rounds the exact binary value of
v, at any size and to any number of places:
format_flt(out, 0.1, 20) writes
0.10000000000000000555, as C's printf("%.20f")
does, and format_flt(out, 2.675, 2) writes
2.67, since the double nearest 2.675 is just below it. Only
a value that is exactly halfway rounds differently from
printf, which rounds half to even:
format_flt(out, 0.125, 2) writes 0.13. The
plain format writes the shortest text that reads back as
the same value.
exception
Typed exceptions implemented in Goose with
return ... from (spec §7.9), inspired by Lobster's
exception module. Import exception; its functions are
in namespace exception.
fn try<E, Body, Catch>() // Body(); on an error, Catch(error)
fn try<E, Body>() // Body(); discard an error
fn try<E, Body, Catch, Finally>() // as the first, then Finally() on every way out
fn throw(value) // exit the nearest enclosing handlerimport exception;
fn load() {
// This can be any number of calls below try.
exception::throw(str("cannot load item ", 42));
}
exception::try<u8[]>(load) { error => print(error); };E is the error type, supplied explicitly at
try: u8[] for owned text, an integer, or an
application's struct or enum (Error.. for a variable-mode
enum). The thrown value must fit E, which must be usable as
an array element. The body and catch are static function values; Goose
permits one trailing block, so name at least one of the callbacks. The
body's results are discarded; it can update captured output
variables.
The handler returns an empty E[] on success and a
singleton containing the error on failure. This uses the ordinary data
stacks, needs no default E, and allows 0,
false, and "" as errors. The catch runs after
that handler has returned. Calling throw in the catch
therefore throws to an outer try; use
exception::throw(copy(error)) to rethrow an owned
variable-size error. An existing non-fixed value likewise needs
copy when first thrown; fresh constructors and string
slices can be supplied directly. Owned text is copied before the frames
containing it exit.
The language's checks and limitations still apply:
- Every throw must have a handler on every compile-time call path, including throws from a catch. An incompatible error type is a compile error.
- References carried by a long-distance return must point to globals
or static data. Use owned
u8[], not a slice into a local string, and owned fields in error records. - Recursive code can throw to an outer handler. A
trycannot itself be re-entered recursively: its non-fixed handler result would hold a data stack across the recursive call (§7.8). - The checker propagates calls that never resume their caller. A
function declared to return a value may end with
exception::throw(error);, and a throwing branch needs no result. This does not givethrowa value type: use it as a statement or branch, rather thanreturn throw(error);or as an argument that requires a value. Recursive inference is conservative (spec §7.3). - Only calls to
throware caught. Runtime failures,abortandexitremain uncatchable. There is no stack-trace facility.
try<E>(body, catch, finally) runs
finally after the body completes, after a caught throw, and
when a throw or a lexical return aimed at another function
leaves through the call. It is defer finally(); ahead of
the try (reference, "defer"), so finally follows the rules
of a defer body: it cannot leave, and it may not throw, catch, or call
anything that does.
dictionary
struct dictionary<K, V> { count: i64, slots: dictionary_slot<K, V>[>..<] }
fn get<K, V>(d: dictionary<K, V>&, key: K) -> V? // reference to the value, or null
fn contains<K, V>(d: dictionary<K, V>&, key: K) -> bool
fn insert<K, V>(d: dictionary<K, V>&, key: K, val: V) -> bool // overwrites; true if new
fn get_or_insert<K, V>(d: dictionary<K, V>&, key: K, v0: V) -> V&
fn update<K, V, F>(d: dictionary<K, V>&, key: K, v0: V) // F(val&); inserts v0 first if absent
fn remove<K, V>(d: dictionary<K, V>&, key: K) -> bool
fn clear<K, V>(d: dictionary<K, V>&)
fn reserve<K, V>(d: dictionary<K, V>&, n: i64) // room for n entries without a rehash
fn each<K, V, F>(d: dictionary<K, V>&) // F(key, val&); order unspecifiedThe table uses open addressing with linear probing, a power-of-two
capacity and backward-shift deletion. Keys and values are fixed-size; a
key type needs hash and ==. Each slot keeps 31
bits of its key's hash as a tag, so a lookup compares keys only where
the tags agree, and growth and removal do not hash keys again (in tables
of up to 2^31 slots). insert, update and
get_or_insert look a key up once, whether or not it is
there. The table doubles in place at 2/3 load; reserve(n)
sizes it for n entries at that load. Strings are keyed as
u8[:] slices into text the caller keeps
(const u8[:] for literals or views of const
data, §9.5), or as inline u8[..k]. A set is
dictionary<K, bool>.
The slot array is grow-shrink, so a reference into it (a
get or get_or_insert result, or an
each binder) must have its last use before the next
insert, update, remove or
clear. Otherwise the checker reports an error at the
mutation, naming the reference and where it was bound (spec §5.2).
Finish with the reference first, or use update, which does
the lookup and the change in one call:
var counts = dictionary<const u8[:], i32> {};
each_split(text, ' ') { counts.update(it, 0) { it += 1; } };
counts.each() { w, n => if n > 100 { print(w, " ", n); } };
let n = counts.get("the");
if n { print(n); } // the last use of n; counts is free again after itThe checker looks at liveness, not scope: a reference whose last use
comes before the mutation needs no block around it, and a reference
still used after the mutation is an error wherever it was declared. The
key type is const u8[:] because literals and the read-only
views returned by split require it (§9.5). A dictionary
holding writable slices can use dictionary<u8[:], V>,
whether the underlying buffer is bound with let or
var.
A value can be a reference if it is optional: Cell?, or
a link into a pool such as Cell&<u32 in cells>?.
The unused slots hold default<V>(), which a
non-optional reference does not have. insert over an
existing key stores the new reference rather than writing through the
old one. get and get_or_insert return the
stored reference itself, and the blocks of update and
each receive it (V? and V& of
a reference are that reference, §3.8), loaded if it is relative. Writing
through it writes the pointee; replacing it requires an
insert. get returns null both for a null value
and for an absent key; contains tells them apart. Such a
reference points at its pointee rather than into the slot array, so it
may stay live across the next mutation:
var owner = dictionary<i64, Cell?> {};
owner.insert(7, cells[3]);
let c = owner.get(7);
owner.insert(7, cells[4]); // cells[3] is unchanged, and c still refers to it
if c { c.v += 1; }Migrating from reusable
reusable and reusable[] are deprecated;
their builtins are still available. Import slot_pool for
individual slots or slice_pool for contiguous runs.
| Existing spelling | Library spelling |
|---|---|
reusable var p: T[>..] = []; |
var p = slot_pool<T> {}; |
reusable[] var p: T[>..] = []; |
var p = slice_pool<T> {}; |
pool parameter p: T[>..]& |
p: slot_pool<T>& or
p: slice_pool<T>& |
p[i], p.len, for x in p |
p.items[i], p.items.len,
for x in p.items |
u32 in p, Node&<u32 in p> |
u32 in p.items,
Node&<u32 in p.items> |
p.alloc_ref(value) |
same for copyable values;
p.alloc_with() { fresh_value } for in-place
construction |
p.alloc_index(value) |
same for copyable values;
p.alloc_index_with() { fresh_value } for in-place
construction |
p.free(i) |
same; or p.free_ref(node) when the element is not
itself a reference |
p.alloc_slice(n), p.free_slice(s),
p.realloc_slice(s, n) |
same method spellings on slice_pool |
| allocate a slice, then copy into it | p.alloc_copy(source) |
Both libraries keep stable, initialized backing storage.
items.len is the high-water count, including free entries,
and iteration visits those entries. Freed references/views remain valid
and can observe replacement values; logical use-after-free and
double-free detection require application bookkeeping.
recycle_all makes all storage reusable without shrinking it
or clearing values. Do not shrink/replace items or modify
allocator bookkeeping directly.
Wrong-pool slot frees through free_ref retain
index_of's compile-time check. Wrong-pool nonempty slice
frees currently fail a runtime containment check
(slice_pool.free_slice: slice is not from this pool); there
is no compile-time warning yet. Empty slices from any origin are
harmless. Like every check inside the standard library, the pools'
report the line of the program that called them. Eligible
items can be serialized independently, but this omits
allocator state. Whole root owners cannot be serialized or copied to
workers automatically. For flat elements, the worker
snapshot sample copies items and allocator metadata
explicitly into a worker-local owner. Its slice metadata types are
implementation details, so keep that recipe in step with the library
rather than treating it as a stable serialization format.
Pool references can be stored in ordinary structs, captured, returned while their owner lives, and made optional. This is more flexible than builtin pool references. The owning root itself remains local/global-only; nested owning roots are deferred.
slot_pool
import slot_pool; provides stable, reusable slots
through ordinary root-struct fields and functions. Use it instead of the
deprecated reusable language feature.
root struct slot_pool<T> {
items: T[>..] = [],
free_slots: i64[>..<] = []
}
fn alloc_index<T>(pool: slot_pool<T>&, value: T) -> i64
fn alloc_ref<T>(pool: slot_pool<T>&, value: T) -> T&
fn alloc_with<T, F>(pool: slot_pool<T>&) -> T&
fn alloc_index_with<T, F>(pool: slot_pool<T>&) -> i64
fn free<T>(pool: slot_pool<T>&, i: i64)
fn free_ref<T>(pool: slot_pool<T>&, value: T&)
fn recycle_all<T>(pool: slot_pool<T>&)Declare a local or global owner and use items for
ordinary indexing, slicing, iteration and index_of:
import slot_pool;
fn main() {
var pool = slot_pool<i64> {};
let first .= pool.alloc_ref(10);
let index = pool.items.index_of(first);
pool.free(index);
assert(pool.alloc_index(20) == index);
print(first); // 20: the same slot, now reused
}Elements must be fixed-size. No default value is needed: every
allocation supplies a value. alloc_index and
alloc_ref take normally copyable values. An allocation
takes the most recently freed index, or appends when no slot is free.
alloc_index returns its index; alloc_ref
returns a reference to its value. For reference elements,
T& collapses to T as usual; use
alloc_index when the slot's identity is needed.
Whole-element assignment replaces reference-valued elements without
writing through them.
alloc_with and alloc_index_with take a
zero-argument function or trailing block producing T; the
latter returns the chosen index directly. It selects the slot first,
calls the builder once, and constructs the result directly there. This
supports self-relative links and pool-relative self:
import slot_pool;
struct Node { value: i64, me: Node&<u32>, next: Node&<u32>? }
fn main() {
var nodes = slot_pool<Node> {};
let first .= nodes.alloc_with() { Node { value: 1, me: self } };
let second .= nodes.alloc_with() { Node { value: 2, me: self, next: first } };
let index = nodes.items.index_of(second);
nodes.free(index);
nodes.alloc_with() { Node { value: 3, me: self, next: first } };
assert(second.value == 3 && second.me .== second);
}Fresh results may come from other functions. Existing self-relative
values still cannot be copied, and links must point within the receiving
array. A trailing block infers its result type; for a reference-valued
T, pass a named function explicitly returning that
reference type.
Unlike value arguments, the builder runs after a free index has been
removed. A slot freed by the builder stays available for a later
allocation. A fresh slot joins items.len only when
construction completes; a nonlocal return before completion does not
publish it. Reuse is not transactional: a nonlocal exit does not restore
the popped index or undo initializer side effects. The usual
construction rules reject conflicting growth of items
during the builder. Bounds checks on a recycled index run before the
builder.
free checks 0 <= i < items.len in
every build, then records the index. It leaves the element initialized
and does not shrink the array. Existing references remain valid and can
observe a later replacement. Double frees and use of a logically freed
slot are application errors, as with reusable; the pool
does not track occupancy or generations. items.len is the
number of slots ever appended, including free ones, and iteration visits
those slots too.
Treat free_slots as bookkeeping. Do not shrink or
replace items, or edit the freelist directly: doing so can
invalidate the pool's logical state. Ordinary array bounds and lifetime
checks still apply. recycle_all makes every slot available
again in index order, retaining initialized values and backing storage.
It does not shrink items, destroy values, or invalidate
existing references. free_ref combines
index_of with free, retaining its compile-time
provenance check; use indices for reference-valued elements, whose
references name the pointee rather than the pool slot. An eligible
items component can be serialized separately, but its image
omits the freelist.
For a global var pool, compact links and index types can
name its array: Node&<u32 in pool.items>,
u32 in pool.items, and u32 upto pool.items.
Pool-relative values with ordinary links are copyable and work with
alloc_ref. Self-relative values and self
initializers use alloc_with to construct at the allocated
slot. A root owner cannot be copied to a worker; worker-local pools are
allowed. See the
migration plan.
slice_pool
import slice_pool; provides reusable contiguous runs of
fixed-size elements. alloc_slice and
realloc_slice require defaults; alloc_copy
only requires copyable values. An owner is declared as
var pool = slice_pool<T> {};. Use it instead of the
deprecated reusable[] language feature. Both builtins
remain available for existing code; their removal is a separate future
step.
fn alloc_slice<T>(pool: slice_pool<T>&, n: i64) -> T[:]
fn alloc_copy<T>(pool: slice_pool<T>&, src: T[:]) -> T[:]
fn free_slice<T>(pool: slice_pool<T>&, s: T[:])
fn realloc_slice<T>(pool: slice_pool<T>&, s: T[:], n: i64) -> T[:]
fn recycle_all<T>(pool: slice_pool<T>&)
fn trim(pool: slice_pool<u8>&) -> boolimport slice_pool;
fn main() {
var pool = slice_pool<i64> {};
var entries = pool.alloc_slice(3);
entries[0] = 7;
entries = pool.realloc_slice(entries, 5);
print(entries); // [7, 0, 0, 0, 0]
pool.free_slice(entries);
let again = pool.alloc_slice(2);
print(again, " ", pool.items.len); // [0, 0] 5
}Allocation selects the first free span in index order that fits. If
none fits, it grows the backing array, using any free span at the tail
first. Free spans coalesce with their neighbors. An augmented B+ tree of
512-byte nodes handles first-fit search and neighbor lookup with tree
walks and bounded node scans; emptied nodes are reused. The tree code is
the runtime's, reusable[]'s, in a copy that checks what it
reads from nodes and tree, so the policy
matches.
alloc_slice defaults every element it hands out,
including reused elements. alloc_copy initializes directly
from a source slice, without default-filling first. It supports
overlapping views of items and does not free the source.
For example, strings.alloc_copy(text) allocates and copies
a string. realloc_slice preserves the first
min(s.len, n) elements, returns a shortened tail to the
tree, and defaults each added element once. Growth stays in place at the
array's end or when the following free span suffices (including tail
extension). Otherwise it frees the old run and selects the first fit,
copying with overlap-safe ordering. Default expressions must not mutate
or reenter the same pool. Every published backing-array element is
initialized.
free_slice and realloc_slice accept
subranges of a pool run. They check that a nonempty slice is aligned and
wholly within items, even when its origin is only known at
runtime. Empty/default slices can be freed harmlessly; growing an empty
slice allocates a new run. Negative or oversized counts abort in every
build before the tree changes. items.range_index(s)
recovers a nonempty run's index; it returns zero for every empty
slice.
Freeing and shrinking runs do not shrink items or
invalidate old views. A retained view observes later replacements at its
original address; relocation does not retarget it. Double frees,
overlapping live allocations caused by invalid frees, and accessing a
logically freed run are application errors. items.len is
the high-water length, not the count of live elements. Treat
nodes and tree as internal bookkeeping, and do
not shrink or replace items. Ordinary bounds and lifetime
checks still apply to these public fields. The runtime checks every node
number and entry count it reads from them: one a program wrote wrong
aborts with slice pool span tree is corrupt. A span edited
to start past items aborts with
span tree is corrupt from the operation that drew it; other
edited spans can hand elements out twice, as a double free can.
recycle_all makes the entire high-water range one free run
and recycles all metadata nodes. It retains initialized values, backing
storage and existing views; the next allocation starts at index
zero.
trim returns the physical memory of a u8
pool's free runs to the OS (and on Windows their commit charge), as
os::trim_memory does above stack tops: it discards the
complete pages inside every free span, and they read back as zeros, the
value alloc_slice gives them anyway. Free spans have merged
with their neighbors, so only a page that also holds a live element is
kept. Live runs, views and items.len stay as they are, and
the address range stays reserved, so the process's virtual size does not
drop. An allocation that reuses a span takes its pages again. Like
trim_memory, it is explicit: freeing stays as cheap as
before, and a long-running program calls trim after closing
large buffers, not after every free. Reading a freed run is an
application error, but without trim such a read sees the
run's old bytes until the pool reuses it; after trim it
sees zeros, so adding a trim call can change what a program
that still reads freed runs does. On Windows a stale view that reads a
discarded page commits it again, so reading freed runs undoes the trim
for those pages. It is limited to u8 because a discarded
page must read back as a valid element. false means an OS
request failed.
The owner follows the same placement, serialization and worker
restrictions as slot_pool: serialize eligible
items separately, and construct worker-local owners rather
than transferring root owners. alloc_slice constructs
defaults in place, so allocation and free support defaultable
self-relative elements; realloc_slice requires normally
copyable elements, just as the builtin does. Global index/pool-relative
types can name pool.items, as for slot_pool.
Optional references to separately owned storage and declared field
defaults are supported when T has a default.
See the slice-pool benchmark
report for comparisons against reusable[], including
preserved function-call boundaries.
vec
struct vec2<T> { x: T, y: T } struct vec3<T> { x: T, y: T, z: T }
struct vec4<T> { x: T, y: T, z: T, w: T }
type float2/float3/float4 = vec*<f32> double2/3/4 = vec*<f64> int2/3/4 = vec*<i32>
let float3_0 = float3 { 0.0, 0.0, 0.0 }; // _0 and _1 for all nine aliases
fn dot<T>(a: vec3<T>, b: vec3<T>) -> T // all sizes
fn cross<T>(a: vec3<T>, b: vec3<T>) -> vec3<T>
fn length_sq<T>(v: vec3<T>) -> T
fn length<T>(v: vec3<T>) -> T // float vectors
fn normalize<T>(v: vec3<T>) -> vec3<T>
fn distance<T>(a: vec3<T>, b: vec3<T>) -> T
fn lerp<T>(a: vec3<T>, b: vec3<T>, t: T) -> vec3<T>
fn xy<T>(v: vec3<T>) -> vec2<T> // also xy(vec4), xyz(vec4)The functions are all checkfree (spec §7.14).
Elementwise + - * / are built into the language
(a + b, p - q), and so is unary minus on a
float or signed integer vector (-v). * and
/ also broadcast a scalar in either order:
v * 2.0, v / length(v), 1.0 / v;
v *= scale and v /= scale update a vector in
place. The scalar converts to the component type, so an f64
variable scaling a float3 needs an explicit
as f32. Per-component min/max is
written out (min(a.x, b.x)).
math
math provides libm at both widths (sin(x)
picks sin or sinf by the argument's type):
sqrt sin cos tan asin acos atan atan2 exp log log2 log10 pow hypot cbrt exp2 expm1 log1p sinh cosh tanh floor ceil round trunc copysign.
These return floats as C does (as i64 converts).
expm1(x) and log1p(x) are
exp(x) - 1 and log(1 + x) without the loss of
a small x's digits; copysign(x, y) is
x's magnitude with y's sign bit, which tells
-0.0 from 0.0. sqrt is
implemented in the runtime (gs_sqrt,
gs_sqrtf). It gives the same correctly rounded root as C,
and NaN below zero, but without C's errno, so it is the bare square root
instruction where the C compiler allows. The other functions for which C
sets errno (sin to tanh) are libm's own
functions under runtime names (gs_exp,
gs_expf, ...), which gcc and clang are told depend only on
their arguments. Since Goose has no errno, a call does not make the C
compiler reload values that a loop around the call holds in registers.
Both compilers still know them as libm's, so they fold constant
arguments and combine sin and cos of one value
into one sincos call where the C library has it. An integer
argument converts to both widths equally well, so the call has to name
one (sqrt(n as f64)); sqrt(n * 0.5) calls the
f64 version, as sqrt(0.5) does.
mul_add(x, y, z), at both widths, is
x * y + z rounded once where the code is built for FMA and
twice where it is not. Which one a call gets is up to the implementation
(spec §7.12). The C backend rounds once on arm64, on x86-64 built for
AVX2 or later (-march=x86-64-v3, clang-cl's
/arch:AVX2), and in a simd fn's AVX2 and
AVX-512 versions. It rounds twice elsewhere, under MSVC and under the
JIT. x * y + z written out always rounds twice.
The two roundings can give very different results, not just a different last bit:
- Cancellation. Where
x * yandznearly cancel, the fused result keeps the product's low bits and the unfused one loses them, so one can be zero where the other isn't.mul_add(x, x, -(1.0 + 2.0 * (x - 1.0)))withx = 1 + 2^-30is2^-60fused and0.0unfused, and a branch on it goes either way. - Overflow.
mul_add(1.0e308, 2.0, -1.0e308)is1.0e308fused, butinfunfused, because the product overflows before the add.
Inside a simd fn, only the calls Goose emits in that
function's own body fuse: its own code, and the functions Goose inlines
into it. A call in a plain helper that the simd fn calls
keeps the helper's rounding, even if the C compiler inlines the helper
later. Whether Goose inlines a small helper depends on the optimization
level, so moving a mul_add into a helper can change both
the result and the speed between -O0 and -O2.
Marking the helper inline fn keeps its calls fused in
simd callers at every level except -O0, where
Goose inlines nothing. A function value is resolved at compile time, so
a simd fn given mul_add as one
(F(x, y, z) with F = mul_add)
fuses like a direct call.
The module also has:
let PI = 3.141592653589793; let TAU = 6.283185307179586;
let E = 2.718281828459045; let INF = 1.0 / 0.0; let NAN = 0.0 / 0.0;
fn radians(deg: f64) -> f64 fn degrees(rad: f64) -> f64 // and f32
fn is_nan(x: f64) -> bool fn is_inf(x: f64) -> bool // and f32
fn is_finite(x: f64) -> bool // not inf or NaN; and f32
fn fract(x: f64) -> f64 // x - floor(x); and f32
fn inverse_lerp(a: f64, b: f64, v: f64) -> f64 // (v - a) / (b - a); and f32
fn smoothstep(edge0: f64, edge1: f64, x: f64) -> f64 // and f32
fn wrap_angle(x: f64) -> f64 // into [-PI, PI); and f32fract, smoothstep and
inverse_lerp follow GLSL and std's lerp:
fract(x)isx - floor(x), so it is never negative:fract(-0.25)is0.75. A tiny negativexrounds to1.0, as in GLSL.inverse_lerp(a, b, v)undoeslerp(a, b, t): 0 ata, 1 atb, and not clamped, sovpastbgives more than 1. It is NaN or an infinity whena == b.smoothstep(edge0, edge1, x)is 0 up toedge0, 1 fromedge1, and3t² - 2t³of the clampedt = inverse_lerp(edge0, edge1, x)between. Edges in reverse run it backwards. Equal edges, which GLSL leaves undefined, give 0 below the edge, 1 above it and NaN at it.wrap_angle(x)adds or takes whole turns to bring radians into[-PI, PI), as a floored remainder would:PIwraps to-PI, and-0.0stays-0.0. The f32 form's bounds arePIrounded to f32.
Each gives NaN for a NaN argument, and fract and
wrap_angle give NaN for an infinity. NAN's
sign is unspecified. INF and NAN take the type
the context asks for, as PI does
(let inf32: f32 = INF;).
The libm functions are extern fns and the ones above
checkfree, so a checkfree function (spec
§7.14) can call any of math.
os
A small interface to src/runtime/runtime_os.h:
fn read_file(path: const u8[:], out: u8[>..]&) -> bool // appends the whole file
fn write_file(path: const u8[:], data: const u8[:]) -> bool
fn write_file_atomic(path: const u8[:], data: const u8[:]) -> bool // the old file or all of the new
fn append_file(path: const u8[:], data: const u8[:]) -> bool
fn file_exists(path: const u8[:]) -> bool // a file, not a directory
fn delete_file(path: const u8[:]) -> bool
fn rename_file(from: const u8[:], to: const u8[:]) -> bool // replaces `to` atomically
fn make_dir(path: const u8[:]) -> bool // true if one is there afterwards
fn is_dir(path: const u8[:]) -> bool
fn delete_dir(path: const u8[:]) -> bool // an empty one
fn list_dir(path: const u8[:], out: u8[>..]&) -> bool // appends each name and '\n', sorted
fn read_line(out: u8[>..]&) -> bool // stdin, newline stripped; false at end
fn read_stdin(out: u8[>..]&) // everything until end of input
fn read_stdin(out: u8[>..]&, max: i64) -> bool // up to max bytes; false at end
fn write_stdout(s: const u8[:]) fn write_stderr(s: const u8[:]) fn flush_stdout()
fn arg_count() -> i64 fn arg(i: i64, out: u8[>..]&)
fn args() -> u8[:][>..] // indexable; argument 0 is the program
fn env(name: const u8[:], out: u8[>..]&) -> bool
fn trim_memory() -> bool // return unused data-stack pages to the OS
fn resource_dir() -> u8[>..] // absolute directory, trailing separator
fn resource_dir(out: u8[>..]&) -> bool // appends; unchanged on failure
fn time() -> f64 // seconds since the epoch
fn clock() -> f64 // monotonic, high resolution
fn time_ns() -> i64 fn clock_ns() -> i64
fn sleep(seconds: f64) fn sleep_ms(ms: i64)
fn random_seed() -> u64 // entropy, for rngtrim_memory asks the OS to discard complete pages beyond
every current data-stack top in the calling thread program, including
its globals and scratch from scopes that have ended. It preserves live
values, borrowed views, limited-array capacity and virtual-address
reservations; subsequent growth works normally. It affects no other
worker. Use it after a large workload, such as replacing a document,
rather than after every temporary value. Ordinary scope exit keeps pages
available for cheap reuse, while a trimmed page causes a page fault when
the stack grows into it again.
The pages are released immediately. Windows decommits them, which
also returns their commit charge, and Linux drops them from the resident
set at once. macOS replaces the unused suffix of each reservation with
fresh anonymous memory at the same addresses. On all three platforms, a
stack that grows again gets fresh zeroed pages. macOS trimming covers
the full unused reservation so ordinary growth needs no runtime tracking
or signal handling. false means at least one OS request
failed.
Paths are UTF-8 on every platform; on Windows the runtime converts
them to UTF-16 for the system's wide APIs. Environment values and the
program's arguments are UTF-8 too. On Windows the arguments pass through
the ANSI code page first, so they keep only the characters that code
page has. A path is refused, and the call returns false, if
it is longer than 4095 bytes, contains a NUL byte, or (on Windows) is
not valid UTF-8, because a path that was cut short or decoded another
way could name a different file.
read_file appends nothing when it fails.
read_stdin appends the bytes unchanged, on Windows too,
where the C library's text mode would turn each \r\n into a
\n and stop at a Ctrl-Z. read_line strips a
\r before a line's newline, along with the newline.
read_stdin(out, max) appends up to max bytes
and waits until it has them or input ends, so a large pipe can be read
in pieces. It returns false, having appended nothing, when no bytes were
left or max is 0 or less. Interactive input in lines stays
with read_line.
write_file_atomic is for files that must never be seen
half-written, such as saved games. It writes the data to a new file in
the same directory as path, named after path
with a random tag and .tmp added. It then flushes that file
to disk and renames it over path, keeping the permissions
of path on POSIX. A crash or a failure at any point leaves
path with either its old contents or all of the new ones.
On a failure the new file is removed; after a crash it can remain, with
a name ending in .tmp. rename_file does the
same one-step replacement for a file the program wrote itself, within
one volume. On Windows, a file cannot be replaced while another program
has it open, and virus scanners briefly open files that were just
written, so both functions wait up to about a second for such a file
before returning false.
list_dir leaves out . and ..
and sorts the names bytewise, so a listing does not depend on the file
system. A name that cannot be written as one line of UTF-8 (a POSIX name
containing a newline, or a Windows name that is not valid UTF-16) is
left out too. On a failure nothing is appended.
var names: u8[>..] = [];
if list_dir("saves", names) {
each_split(names, '\n') { if it.len > 0 { load_save(str("saves/", it)); } };
}
if !write_file_atomic("saves/slot1.sav", image) { write_stderr("not saved\n"); }Application resources
Ordinary relative paths resolve against the process working
directory, as in C/C++. Goose does not change that directory when it
starts a program. For data distributed with the application,
resource_dir() returns an absolute UTF-8 directory with a
trailing separator, ready to concatenate:
var wad: u8[>..] = [];
if !read_file(str(resource_dir(), "data/map.wad"), wad) {
abort("cannot read map.wad");
}In JIT runs the root is the entry source's directory, regardless of
which imported module calls the helper. In AOT programs it is the
running executable's directory, discovered at runtime rather than
recorded at compilation. In a conventional macOS .app
bundle it is Contents/Resources/. Deploy the data relative
to that root; moving the executable and its data together preserves
resource lookup. An AOT program does not depend on its original source
tree or on the launcher's working directory. A shared library that an
extern "..." { } block names with a path separator, such as
"bin/steam_api64", is opened relative to the same root
(spec §7.10), so a library shipped beside the program is found the same
way as its data.
The string-returning form aborts if the directory cannot be discovered; the builder overload returns false and appends nothing. Native discovery supports Windows, Linux and macOS. This directory is for shipped assets; it is not guaranteed to be writable. User-supplied paths, outputs and saves can continue to use ordinary file operations and their chosen locations.
exit(code) and abort(msg) are builtins, so
that the checker knows they diverge. Subprocesses and networking are not
in v1; they arrive as extern fns when a program needs
them.
ffi
fn cstr(p: u64, out: u8[>..]&) -> bool // the C string at p, its NUL left out; false for null
fn cstr(p: u64) -> u8[>..] // the same, fresh; empty for null
fn bytes(p: u64, n: i64, out: u8[>..]&) -> bool // the n bytes at p; false for null
fn bytes(p: u64, n: i64) -> u8[>..]These functions read memory that a C library owns. A function bound
from a shared library (spec §7.10, reference.md
§24) returns a const char *, or an array that the
library keeps, as a u64, which Goose cannot read through.
These functions copy the data into Goose storage, so foreign memory
never becomes a Goose value without a copy. They trust the pointer and
the length, as every extern call trusts C: a wrong pointer or length is
the same kind of error as in C, and it is not caught. A null pointer
appends nothing and gives the false result.
import ffi;
extern "SDL2" {
fn SDL_Init(flags: u32) -> i32;
fn SDL_GetError() -> u64;
}
if SDL_Init(0x20) < 0 { abort(str("SDL: ", ffi::cstr(SDL_GetError()))); }binary
Checked reads and appending writes of external formats, little- and
big-endian, written in Goose (floats take their bits through small
runtime helpers). import binary; puts the API in namespace
binary. Input is a const u8[:]; returned byte
slices and fixed-width names borrow that input without copying or
allocating. Their lifetimes and read-only access are checked normally.
This API does not interpret Goose serialization images
(from_bytes).
Reads at offsets
fn range_ok(data: const u8[:], at: i64, size: i64) -> bool
fn read_u8(data: const u8[:], at: i64) -> u8, bool
fn read_i8(data: const u8[:], at: i64) -> i8, bool
fn read_u16le(data: const u8[:], at: i64) -> u16, bool // and read_u16be
fn read_i16le(data: const u8[:], at: i64) -> i16, bool // and read_i16be
fn read_u32le(data: const u8[:], at: i64) -> u32, bool // and read_u32be
fn read_i32le(data: const u8[:], at: i64) -> i32, bool // and read_i32be
fn read_u64le(data: const u8[:], at: i64) -> u64, bool // and read_u64be
fn read_i64le(data: const u8[:], at: i64) -> i64, bool // and read_i64be
fn read_f32le(data: const u8[:], at: i64) -> f32, bool // and read_f32be
fn read_f64le(data: const u8[:], at: i64) -> f64, bool // and read_f64be
fn read_bytes(data: const u8[:], at: i64, size: i64) -> const u8[:], bool
fn read_name(data: const u8[:], at: i64, width: i64) -> const u8[:], boolOffsets and sizes are bytes. Negative values, truncated fields and
offsets past the end fail with zero or an empty slice and
false; range checking does not overflow on extreme offsets
or sizes. An empty range at the end is valid. Reads work at unaligned
offsets on either host byte order. The le reads take the
least significant byte first, the be reads the most
significant. A float read keeps the value's bits exactly, NaN payloads
included. read_name validates the entire fixed-width field
and returns the bytes before its first NUL, or all width
bytes if there is no NUL. It does not validate padding or interpret the
bytes as a particular text encoding.
Cursor and bounded subreaders
struct Reader { data: const u8[:], pos: i64 }
fn reader(data: const u8[:]) -> Reader
fn read_u8(r: Reader&) -> u8, bool
fn read_u16le(r: Reader&) -> u16, bool // every offset read has a cursor form
fn read_bytes(r: Reader&, size: i64) -> const u8[:], bool
fn read_name(r: Reader&, width: i64) -> const u8[:], bool
fn skip(r: Reader&, size: i64) -> bool
fn subreader(r: Reader&, size: i64) -> Reader, boolThe cursor holds only its input and byte position. Every read returns
its own success flag. Success advances pos (by the full
width for a name); failure returns zero/empty and leaves
pos unchanged, so a shorter read can be tried next. Set
pos to seek; a negative or out-of-range position makes
reads fail. Callers keep any file offsets or diagnostic context they
need. There is no sticky error state.
subreader consumes a bounded range from its parent and
gives it a new cursor starting at zero. Failure returns an empty reader
and false, leaving the parent in place. Keep the parent in
scope while using its child or returned slices; the current
reference-result analysis can tie their lifetime to the parent.
// data owns the file bytes. The first four bytes give a payload length.
var r = binary::reader(data);
let size, size_ok = binary::read_u32le(r);
if !size_ok { abort("missing payload length"); }
var payload, payload_ok = binary::subreader(r, size);
if !payload_ok { abort("truncated payload"); }
let version, version_ok = binary::read_u16le(payload);
let name, name_ok = binary::read_name(payload, 8);
if !version_ok || !name_ok { abort("truncated payload header"); }
// name borrows data. payload cannot read into the next record.Use offset reads for random access and cursors for sequential
records. Applications still validate signatures, counts, indices and
format-specific limits after checking the read. The WAD loader in 31_mini_doom.goose
shows both styles.
Writes
fn write_u8(out: u8[>..]&, v: u8)
fn write_i8(out: u8[>..]&, v: i8)
fn write_u16le(out: u8[>..]&, v: u16) // and write_u16be
fn write_i16le(out: u8[>..]&, v: i16) // and write_i16be
fn write_u32le(out: u8[>..]&, v: u32) // and write_u32be
fn write_i32le(out: u8[>..]&, v: i32) // and write_i32be
fn write_u64le(out: u8[>..]&, v: u64) // and write_u64be
fn write_i64le(out: u8[>..]&, v: i64) // and write_i64be
fn write_f32le(out: u8[>..]&, v: f32) // and write_f32be
fn write_f64le(out: u8[>..]&, v: f64) // and write_f64beEach write appends the value's bytes to out, in the
order the read of the same name takes them, so a write followed by that
read gives the value back. A float write keeps the value's bits exactly,
NaN payloads included. The writes cannot fail; out grows as
an append does.
var out: u8[>..] = [];
binary::write_u32be(out, 0x89504E47); // a signature, most significant byte first
binary::write_u16le(out, 3);
binary::write_f32le(out, 1.5);
let version, version_ok = binary::read_u16le(out, 4);
assert(version_ok && version == 3);base64
Base64 as RFC 4648 §4 defines it: the standard alphabet
(A-Z, a-z,
0-9, +, /) and
= padding. import base64; puts it in namespace
base64.
fn encode(out: u8[>..]&, src: const u8[:]) // appends 4 characters per 3 bytes, padded
fn decode(out: u8[>..]&, src: const u8[:]) -> bool // appends the bytes; false if src is invaliddecode accepts only whole groups of four characters, the
last of which may end in = or ==. It rejects
line breaks, white space, unpadded text and other alphabets (the
- and _ of the URL-safe alphabet are invalid).
It does not require the bits left over before the padding to be zero, so
Zh== decodes the same as Zg==; most decoders
allow this. On invalid input it returns false and leaves
out as it was.
var text: u8[>..] = [];
base64::encode(text, "foobar"); // "Zm9vYmFy"
var bytes: u8[>..] = [];
if !base64::decode(bytes, text) { abort("not base64"); }The codec computes the alphabet rather than looking it up, so the C
compiler vectorizes both loops. Both are also simd fns
(spec §7.12): when clang builds for x86-64, they run as AVX2 or AVX-512
code on a CPU that supports it, several times faster than with the
baseline SSE2.
csv
Comma-separated values as RFC 4180 defines them.
import csv; puts the reader and the writer's quoting in
namespace csv.
fn each_record<F>(text: const u8[:], sep: u8 = ',') -> bool // F(fields: const u8[:][:]) per record
fn format_field(out: u8[>..]&, s: u8[:], sep: u8 = ',') // s, quoted where it needs to beeach_record passes each record's fields to its block, in
order, as slices of the text. Nothing is copied, and the slice of fields
belongs to the reader and is reused for the next record. A field may be
quoted, and a quoted field may contain separators, line breaks and
doubled quotes, each of which stands for one quote. A quoted field is
passed without its surrounding quotes, with its doubled quotes left as
they are. Since a field that is not quoted cannot contain a quote,
format_replaced(out, field, "\"\"", "\"") appends the text
of any field. Records end at LF or CRLF, except that the last record
does not need to. An empty line is a record of one empty field, and a
header is a record like any other. Spaces around a field are part of it.
sep may be any byte except a quote or a line break, such as
';', or '\t' for tab-separated values.
each_record returns false at the first
malformed record, after passing every earlier record to its block; the
block does not see the malformed record. A record is malformed if it has
a quote in a field that is not quoted, a quoted field with no closing
quote, or anything other than a separator or a line break after a
closing quote. A block's return leaves the function that
called each_record, as with every block.
var total = 0.0;
let ok = csv::each_record(text) { fields =>
let price, valid = parse_flt(fields[2]);
if valid { total += price; }
};
if !ok { abort("malformed CSV"); }format_field appends s as it is or, if
s contains the separator, a quote or a line break, quoted
with its quotes doubled. The caller writes the separators between fields
and the line break after a record.
for row in rows {
csv::format_field(out, row.name);
out.push(',');
format(out, row.count, "\n");
}json
JSON as RFC 8259 defines it, three ways: a writer that appends values
to a builder, a parser that builds a document tree, and a reader that
maps the text directly onto the caller's own types, as a serde derive
does. import json; puts them in namespace
json.
Writing
fn format_string(out: u8[>..]&, s: u8[:]) // quoted, with " \ and control characters escaped
fn format_key(out: u8[>..]&, key: u8[:]) // a quoted key and its colon: "key":
fn format_number(out: u8[>..]&, v: f64) // the shortest form that reads back as v
fn format_int(out: u8[>..]&, v: i64)
fn format_bool(out: u8[>..]&, b: bool)The caller writes the brackets and the commas between values. A
string's bytes from 0x80 up are written unchanged, so UTF-8 text stays
UTF-8. format_number writes the fewest significant digits
that read back as v exactly, in the same layout as
serde_json (1.0, 0.1, 1e-7,
1.5e300, 0.30000000000000004). It writes
null for NaN and the infinities, which JSON has no numbers
for.
out.push('{');
json::format_key(out, "name");
json::format_string(out, player.name);
out.push(',');
json::format_key(out, "pos");
out.push('[');
json::format_number(out, player.x);
out.push(',');
json::format_number(out, player.y);
out.append("]}");The document tree
struct Node { next: Node&<u32>?, first: Node&<u32>?, key: const u8[:], val: Value }
enum Value { Null, Bool { v: bool }, Int { v: i64 }, Num { v: f64 }, Str { s: const u8[:] },
Arr { count: i64 }, Obj { count: i64 } }
fn parse(text: u8[:], doc: Node[>..]&, decoded: u8[>..]&) -> Node?, u8[>..]
// the root, or null and the error
fn member(n: Node?, key: u8[:]) -> Node? // the first member named key; null if none
fn each<F>(n: Node&) // F(child) for each element or member, in order
fn as_f64(n: Node?) -> f64, bool // an Int or a Num
fn as_i64(n: Node?) -> i64, bool // an Int
fn as_bool(n: Node?) -> bool, bool
fn as_string(n: Node?) -> const u8[:], boolparse pushes a node for every value onto
doc, in text order. Each node holds a link to its next
sibling, a link to its first child (for a container), its key (empty in
an array) and its value. Keys and strings are slices of
text. Those that contained escapes are instead slices of
their decoded text, which is pushed onto decoded; nothing
else is copied. A number without a fraction or an exponent that fits an
i64 becomes an Int. Any other number becomes a
Num, correctly rounded, and infinite if it is too large for
an f64. Escapes decode to UTF-8: a surrogate pair becomes
one code point, and a lone surrogate becomes the three bytes that encode
its own code point. Strings are not checked to be valid UTF-8.
On malformed text, parse returns null and a message
naming the byte at which it found the text malformed, such as
json: expected ',' or '}' at byte 41. The nodes before that
point have already been pushed.
The parser is a single loop that keeps the open containers on a stack
of its own, so nesting of any depth can be parsed. member
and the accessors accept a null node, so lookups can be chained.
Duplicate keys stay in the tree, and member finds the
first.
var doc: json::Node[>..] = [];
var decoded: u8[>..] = [];
let root, err = json::parse(text, doc, decoded);
guard root else { abort(err); }
let w, ok = json::as_i64(json::member(json::member(root, "window"), "width"));
let items = json::member(root, "items");
if items {
json::each(items) { item =>
let name, named = json::as_string(json::member(item, "name"));
if named { print(name); }
};
}Reading into your own types
struct Reader { text: const u8[:], pos: i64, error: const u8[:], error_at: i64 }
fn reader(text: const u8[:]) -> Reader
fn each_member<F>(r: Reader&) // F(key) for each member of the object at the cursor
fn each_element<F>(r: Reader&) // F() for each element of the array at the cursor
fn read_number(r: Reader&) -> f64
fn read_int(r: Reader&) -> i64 // no fraction or exponent, and fits an i64
fn read_bool(r: Reader&) -> bool
fn read_string(r: Reader&, scratch: u8[>..]&) -> const u8[:]
fn skip(r: Reader&) // passes over a value of any kind, checking it
fn peek(r: Reader&) -> u8 // the next byte past white space; 0 at the end
fn finish(r: Reader&) -> bool // no error, and nothing but white space left
fn message(r: Reader&) -> u8[>..] // "json: <what> at byte <n>"The reader parses as it goes and builds nothing. Each block reads the
value it is given with the function for the type it expects, and uses
skip to pass over values it does not need.
read_string returns a slice of the text, or of
scratch for a string that contained escapes. Keys are
passed to the block as written, including any escapes, so a key written
with an escape never equals the same key written without one. Errors are
sticky: a read that finds text it does not expect records what it
expected and where, and moves the cursor to the end, so every later read
fails as well and every loop ends. The caller checks once, with
finish, which also rejects anything but white space after
the value. To handle a null where a value is optional,
check peek(r) == 'n' and call skip.
struct Item { id: i64, price: f64 }
fn read_items(text: u8[:]) -> Item[>..] {
var r = json::reader(text);
var items: Item[>..] = [];
json::each_element(r) {
var it = Item { id: 0, price: 0.0 };
json::each_member(r) { key =>
if key == "id" { it.id = json::read_int(r); }
else if key == "price" { it.price = json::read_number(r); }
else { json::skip(r); }
};
items.push(it);
};
if !json::finish(r) { abort(json::message(r)); }
return items;
}regex
Regular expressions with the syntax and the matching rules of Rust's
regex crate, on bytes. import regex; puts them
in namespace regex.
fn compile(pattern: u8[:]) -> Regex // aborts on a malformed pattern
fn compile(pattern: u8[:], err: u8[>..]&) -> Regex // appends the error to err instead
fn find(re: Regex&, text: u8[:]) -> i64, i64 // the first match's start and end; -1, -1
fn matches(re: Regex&, text: u8[:]) -> bool // whether there is a match
fn count(re: Regex&, text: u8[:]) -> i64 // the matches each_match finds
fn each_match<F>(re: Regex&, text: u8[:]) // F(start, end) for each match
fn each_captures<F>(re: Regex&, text: u8[:]) // F(caps: Captures) for each match
fn format_replaced<F>(out: u8[>..]&, re: Regex&, text: u8[:])
// text, each match replaced by F(caps)
struct Captures { text: const u8[:], slots: const i64[:] }
fn group(c: Captures, k: i64) -> const u8[:] // group k's text; empty if it took no partMatching is leftmost-first, as in the crate, Perl and most engines.
The match that starts earliest is chosen, and among the matches starting
there, the one the pattern prefers: alternatives are tried in order, and
repetitions are greedy unless they are lazy. Searches are unanchored.
The iterating functions find matches that do not overlap, from left to
right, and never find an empty match where the previous match ended:
a* on baaa matches at 0..0 and 1..4. Group 0
is the whole match, and the other groups are numbered by their opening
parentheses. Group k spans slots[2k]..slots[2k + 1] of the
text; both slots are -1 for a group that took no part. A repeated group
captures its last iteration. The slice that group returns
is rooted at the Captures the block was given. A view of a
group that has to outlive the block, such as a dictionary key, should
slice the text by the slots instead.
The supported syntax is:
- literal bytes;
.(which matches a newline only unders);- classes
[a-z]and[^...], where a]first or a-first or last is literal; \d \w \sand their complements\D \W \S, also inside classes;- escapes
\n \t \r \f \v \xHHand any punctuation (\.,\\); - groups
(...),(?:...)and named groups(?P<name>...)or(?<name>...), whose names are accepted but not used; - flags
i(letters match either case) ands, written(?i)to apply to the end of the group or(?i:...)to apply within, and(?-i)to turn one off; - alternation
|; * + ? {n} {n,} {n,m}, each made lazy by a?after it.
The classes and the case folding are ASCII, as the crate's are with
Unicode off. Anchors and word boundaries (^ $ \A \z \b \B),
class set operations (&& -- ~~), nested classes,
Unicode classes and the other flags are not supported, and
compile reports them as errors:
regex: anchors are not supported at offset 0 in ^a. The
module's header describes two known differences from the crate: an
alternation whose branches share a repeating prefix is matched as
written, as in Perl and RE2, and groups at the end of a pattern under a
{0} are kept.
compile builds complete DFAs up front: one that finds
where a match ends, and one over the reversed pattern that finds where
it starts. A search therefore takes one table lookup per byte of text it
looks at. When every match has rare leading bytes, or a rare byte after
a part that cannot contain that byte, the search skips through the text
with find_pair or find_any (above) and only
looks where a match can be. Where the pattern gives a group a fixed
place in the match, the group is taken from that place; otherwise it
comes from a backtracker that runs over the match alone.
find, matches and count use
neither. A DFA can need exponentially many states, and a pattern whose
program or DFAs would grow too large ([ab]*a[ab]{20}, or
counted repetitions in the thousands) is reported as an error rather
than compiled. Make the Regex once, outside the loop that
searches with it.
let re = regex::compile("(\\w+)=(\\d+)");
regex::each_captures(re, line) { c =>
let n, ok = parse_int(regex::group(c, 2));
if ok { settings.insert(line[c.slots[2]..c.slots[3]], n); } // a key that outlives c
};
var err: u8[>..] = [];
let user = regex::compile(pattern_from_user, err);
if err.len > 0 { print(err); }
var out: u8[>..] = [];
let field = regex::compile("""\{\{\s*(\w+)\s*\}\}""");
regex::format_replaced(out, field, template) { caps =>
let value = vars.get(regex::group(caps, 1));
if value { value } else { "" }
};gfx
Graphics on SDL3's GPU API, which draws through Direct3D 12, Vulkan
or Metal: a window and its input, buffers, textures, samplers,
pipelines, render and compute passes, and reading results back. The
module is optional: it needs a compiler built with the
third_party/SDL submodule, and a program using it links
what goose --gfx-link msvc|cc prints (a response file:
cl game.c @<it>,
cc game.c -o game @<it>). A compiler built without it
still typechecks and generates C for such a program; only running the
program in-process fails. Everything is in namespace gfx;
samples/27_gfx_cube.goose is a small complete program, and
the package's docs/gfx.md
says how it works.
Text rendering is provided by ui,
including fonts, positioned text and noninteractive game HUDs. Use
ui::label or ui::draw_text, then
ui::render over the gfx scene. Only specialized artwork or
rendering requirements need a separate text renderer.
Shaders
// Code shaders share, in a global each names as a part.
let scene = """
layout(set = 1, binding = 0) uniform Scene { mat4 view_proj; };
""";
// A shader's source after its stage, "vert", "frag" or "comp", or a file,
// .vert, .frag or .comp, relative to this one.
let vs = embed_shader("vert", scene, """
layout(location = 0) in vec3 a_pos;
void main() { gl_Position = view_proj * vec4(a_pos, 1.0); }
""");
let fs = embed_shader("lit.frag");embed_shader is a builtin. The GLSL 450 shader is
compiled when the program is, into SPIR-V, MSL and HLSL at once, and the
result is a const u8[:] of static data to pass to
pipeline or compute_pipeline. Its source is
usually a """ string (spec §2) written at the call. It may
also come in several parts, each a string literal or a let
or const global initialized with one; the parts are joined
as lines, in order. A shader that does not compile is a compile error at
the call. Where there is an offending line of GLSL, the error points at
it, in the program or in the shader's file.
#include "x.glsl" resolves relative to the shader's file,
or, for source written in the program, relative to the program's file.
The dialect is that of cute_spirv (third_party/cute_spirv):
there are no doubles and no geometry or tessellation stages, uniform
blocks have no instance names, and each storage buffer block holds one
runtime array (buffer B { vec4 items[]; };).
Resources go where SDL_GPU expects them, which the compiler checks:
| Stage | set |
holds, each set's bindings numbered from 0 in this order |
|---|---|---|
| vertex | 0 | samplers, then storage textures, then storage buffers (read-only) |
| vertex | 1 | uniform blocks |
| fragment | 2 | samplers, then storage textures, then storage buffers (read-only) |
| fragment | 3 | uniform blocks |
| compute | 0 | samplers, then read-only storage textures, then read-only storage buffers |
| compute | 1 | read-write storage textures, then read-write storage buffers |
| compute | 2 | uniform blocks |
The slot of a bind_* call counts within one
kind: the first storage buffer is slot 0 no matter how many samplers
precede it. A compute shader's read-write resources are instead passed
to begin_compute in binding order.
Device, window and frames
fn open(title: const u8[:], width: i64, height: i64) -> bool // and (..., flags)
fn open_headless(width: i64, height: i64) -> bool // no window; and (..., flags)
fn close()
fn frame() -> bool // ends the frame drawn since the last call; false once asked to close
fn quit() // frame() returns false next
fn flush() // submit everything and wait for the GPU
fn error() -> u8[>..] // why the last failing call failed
fn check() // abort if the program has misused gfx
fn driver() -> u8[>..] // "direct3d12", "vulkan" or "metal"
fn set_title(title: const u8[:])
fn set_fullscreen(on: bool) -> bool fn fullscreen() -> bool fn maximize()
fn screen() -> Texture fn screen_depth() -> Texture fn screen_size() -> int2
fn pixel_density() -> f32 fn display_scale() -> f32
fn time() -> f64 fn delta_time() -> f64 fn frame_count() -> i64Flags for open: WINDOW_RESIZABLE,
WINDOW_HIDDEN, WINDOW_FULLSCREEN,
NO_VSYNC, DEBUG (validation layers where
installed; on in a debug build of the layer). With
GOOSE_GFX_HEADLESS=1 in the environment, open
opens no window; the test runners use this. The screen is an RGBA8
texture the size of the window in pixels, with a depth texture. A frame
draws into it, and frame() shows it. A window has the width
and height that open gives, in the display's own
coordinates, and it has every pixel the display has in that area. On a
high-density display, that is more pixels than its width and height;
screen_size() counts these pixels, and the mouse position
is reported in them.
pixel_density() is the screen's pixels per unit of those
coordinates: 2 on most high-density displays, 1 elsewhere, including
Windows at any scale setting. display_scale() is that times
the system's content scale, what the system draws its own text and
controls at: 2 on a high-density Mac, 1.5 on Windows set to 150%. Both
are SDL's values for the window, 1 when headless or with no window open,
and they change as the window moves between displays, so a program that
follows them reads them each frame. They only report: gfx and ui apply
neither, and how big to draw is the program's choice. For a ui the size
of the system's own, pass display_scale() to
ui::set_scale.
set_fullscreen makes the window borderless fullscreen at
the desktop's resolution, or a window again. It returns false if the
window can't change, and when headless. SDL changes it asynchronously,
so fullscreen() can lag a frame or two behind.
maximize fills the desktop as a window.
guard gfx::open("demo", 1280, 720) else { abort(str("gfx: ", gfx::error())); }
while gfx::frame() {
gfx::begin_screen(float4 { 0.1, 0.1, 0.1, 1.0 });
// bind, uniforms, draw ...
gfx::end_pass();
}
gfx::close();Errors: a call that can fail for reasons outside the program returns
false or a zero handle, and error() says why. A call that
the program should not have made is printed when it happens and skipped,
and the program aborts at the next frame(),
check(), read back or close(). Examples are a
draw with no pipeline bound, a texture the shader samples left unbound,
a released handle, and a uniform struct of the wrong size.
Input
fn key_down(name: const u8[:]) -> bool // SDL's key names: "A", "Space", "Left", "Escape"
fn key_pressed(name: const u8[:]) -> bool // went down since the last frame()
fn key_released(name: const u8[:]) -> bool
fn mouse_down(button: i64) -> bool // MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT
fn mouse_pressed(button: i64) -> bool fn mouse_released(button: i64) -> bool
fn mouse_pos() -> float2 fn mouse_delta() -> float2 // in the screen's pixels
fn mouse_wheel() -> f32 fn mouse_in_window() -> bool // over the window, or dragging from it
fn focused() -> bool // keyboard focus at the last frame()
fn set_mouse_relative(on: bool) -> bool fn mouse_relative() -> bool
fn set_cursor(shape: i64) -> bool // a system cursor, CURSOR_*
fn set_cursor_visible(visible: bool) -> bool fn cursor_visible() -> bool
fn inject_key(name: const u8[:], down: bool) -> bool // as if typed, seen at the next frame()
fn inject_mouse(x: f32, y: f32, button: i64, down: bool) // to pixel x, y; button 0 only moves
fn inject_text(text: const u8[:]) -> bool // an EVENT_TEXT per character
fn inject_focus(focused: bool) -> bool // simulate focus at the next frame()
fn events() -> Event[>..] // the last frame's input, in the order it came
fn text_input(on: bool) // typed text as EVENT_TEXT: off by default
fn scancode(name: const u8[:]) -> i64 fn key_name(scancode: i64) -> u8[>..]
fn clipboard() -> u8[>..] fn set_clipboard(text: const u8[:]) -> bool
fn open_count() -> i64 // times gfx was opened: older handles are goneThe state functions report the state at a frame;
events() reports what happened between two frames, which
text editing needs. The events are:
- key presses and releases, with their scancode, the character their
key types (
keycode) and theMOD_*keys held; - typed characters;
- mouse motion;
- buttons (
MOUSE_*, with X1 and X2), with their click count; - the wheel.
When headless, gfx uses a clipboard of its own rather than the system's.
mouse_pos reports the mouse's position from the moment
the window opens, even before the mouse first moves.
mouse_in_window is true while the mouse is over the window,
and while it holds a button that was pressed there, as SDL reports it.
For a window that opened under the mouse, SDL reports this from the
mouse's first move at the latest. Once the mouse leaves,
mouse_pos stays at the point where it left, off the screen,
so edge scrolling checks both. When headless, the mouse is in the window
when the last injected position is on the screen.
set_mouse_relative(true) confines the mouse to the
window and hides its cursor. mouse_delta() continues to
report movement past the window edges, in screen pixels: use it for
first-person camera turns, rather than mouse_pos(). Setting
a different mode discards queued motion and clears the current delta so
a previously accumulated move cannot turn the camera. Calling it again
with the same mode preserves motion. It returns false on failure,
including a window without keyboard focus; error() gives
the reason.
set_cursor_visible(false) hides the cursor for programs
drawing their own. Its preference applies outside relative mode:
relative mode always hides the cursor, and cursor_visible()
reports that effective state inside the window. The preference is
restored when relative mode is disabled. Hiding a cursor without focus
fails; showing one always works while gfx is open.
set_cursor selects one of the system's cursors, to show
what a click on the thing under the mouse would do:
| Constant | Cursor |
|---|---|
CURSOR_DEFAULT |
the arrow |
CURSOR_TEXT |
a text caret |
CURSOR_POINTER |
a hand, over a link or button |
CURSOR_CROSSHAIR |
a crosshair |
CURSOR_WAIT, CURSOR_PROGRESS |
busy, and busy but still clickable |
CURSOR_MOVE |
moves in four directions |
CURSOR_NOT_ALLOWED |
no action here |
CURSOR_EW_RESIZE, CURSOR_NS_RESIZE |
resizes a side, both ways |
CURSOR_NWSE_RESIZE,
CURSOR_NESW_RESIZE |
resizes a corner, both ways |
CURSOR_N_RESIZE, CURSOR_E_RESIZE,
CURSOR_S_RESIZE, CURSOR_W_RESIZE |
resizes from that side |
CURSOR_NW_RESIZE, CURSOR_NE_RESIZE,
CURSOR_SE_RESIZE, CURSOR_SW_RESIZE |
resizes from that corner |
It returns false for any other value or on failure;
error() gives the reason. The shape persists until changed,
and visibility and relative mode are left as they are. When headless, it
checks the shape and changes nothing on the desktop.
close() restores the default cursor.
focused() reports keyboard focus, which differs from
mouse_in_window(). events() includes
EVENT_FOCUS_GAINED and EVENT_FOCUS_LOST in
order with other input; these kinds have no payload. Losing focus clears
held keys and buttons and mouse/wheel deltas, disables relative mode,
and resets the cursor to visible. Input received while unfocused is
ignored. Focus gain leaves relative mode disabled so a game can pause on
focus loss and wait for a click before recapturing. Held keys/buttons
also get released edges and synthetic up events before
EVENT_FOCUS_LOST, so event-driven UI cannot retain a held
key or a drag across an application switch. close() also
releases the mouse and restores the cursor.
Headless gfx starts focused, with a visible cursor and relative mode
disabled. The setters simulate their state without touching the system
mouse. inject_focus queues the same transitions for
testing; it does not change operating-system focus. Closing and
reopening resets all these states.
Gamepads
fn gamepad_connected(slot: i64) -> bool
fn gamepad_changes() -> i64 // counts connects and disconnects
fn gamepad_type(slot: i64) -> i64 // SDL's SDL_GamepadType, 0 for an empty slot
fn gamepad_down(slot: i64, button: i64) -> bool
fn gamepad_pressed(slot: i64, button: i64) -> bool // went down since the last frame()
fn gamepad_released(slot: i64, button: i64) -> bool
fn gamepad_axis(slot: i64, axis: i64) -> f32 // sticks -1..1, triggers 0..1, no deadzone
fn gamepad_rumble(slot: i64, low: f32, high: f32, ms: i64) -> bool // strengths 0..1Pads take the slots 0 to MAX_GAMEPADS - 1
as they connect, and a slot frees when its pad disconnects, reporting
any buttons it held as released. Watch gamepad_changes to
notice either. Buttons follow SDL's standard layout, named by position:
GAMEPAD_SOUTH is A on Xbox and Cross on PlayStation, then
GAMEPAD_EAST, GAMEPAD_WEST,
GAMEPAD_NORTH, GAMEPAD_BACK,
GAMEPAD_GUIDE, GAMEPAD_START,
GAMEPAD_LEFT_STICK, GAMEPAD_RIGHT_STICK,
GAMEPAD_LEFT_SHOULDER, GAMEPAD_RIGHT_SHOULDER,
GAMEPAD_DPAD_UP, GAMEPAD_DPAD_DOWN,
GAMEPAD_DPAD_LEFT, GAMEPAD_DPAD_RIGHT. Axes
are AXIS_LEFT_X, AXIS_LEFT_Y,
AXIS_RIGHT_X, AXIS_RIGHT_Y,
AXIS_LEFT_TRIGGER, AXIS_RIGHT_TRIGGER.
An empty slot reads as nothing held, an axis of 0, and a rumble that returns false, as does a pad that cannot rumble. A slot, button or axis outside these ranges is a misuse. A machine without gamepad support has no gamepads, which is not an error.
Buffers
struct Buffer { id: u32 }
fn buffer(usage: i64, data: const u8[:]) -> Buffer // and (usage, size, data): zeros after
fn vertex_buffer(data: const u8[:]) -> Buffer // data from bytes_of(vertices)
fn index_buffer(data: const u8[:]) -> Buffer
fn update_buffer(b: Buffer, data: const u8[:]) -> bool // and (b, offset, data)
fn read_buffer<T>(b: Buffer, out: T[>..]&, count: i64) -> bool // appends count elements of T
fn read_buffer(b: Buffer, offset: i64, out: u8[:]) -> bool
fn buffer_size(b: Buffer) -> i64
fn release(b: Buffer)Usage flags: BUFFER_VERTEX, BUFFER_INDEX,
BUFFER_INDIRECT, BUFFER_STORAGE (read by
vertex and fragment shaders), BUFFER_COMPUTE_READ,
BUFFER_COMPUTE_WRITE. Updates and read backs happen between
passes, in order with the draws around them; a read back waits for the
GPU.
Textures and samplers
struct Texture { id: u32 }
struct TextureDesc { kind: i32 = 0, format: i32 = 1, usage: i32 = 1, width: i32, height: i32,
depth: i32 = 1, mips: i32 = 1, samples: i32 = 1 } // TEXTURE_2D, RGBA8, SAMPLED
struct Region { mip: i32 = 0, layer: i32 = 0, x: i32 = 0, y: i32 = 0, z: i32 = 0,
w: i32 = 0, h: i32 = 0, d: i32 = 0 } // w == 0: the whole mip
fn texture(desc: TextureDesc) -> Texture // mips 0: the full chain
fn texture2d(width: i64, height: i64) -> Texture // and (..., format, usage)
fn render_target(width: i64, height: i64, format: i32) -> Texture // COLOR_TARGET | SAMPLED
fn depth_target(width: i64, height: i64) -> Texture // DEPTH, and SAMPLED
fn texture3d(width, height, depth, format, usage) fn texture_cube(size, format, usage)
fn texture_array(width, height, layers, format, usage)
fn load_texture(path: const u8[:], flags: i64) -> Texture // PNG or BMP; LOAD_MIPS, LOAD_SRGB
fn update_texture(t: Texture, data: const u8[:]) -> bool // and (t, region, data)
fn read_texture<T>(t: Texture, out: T[>..]&) -> bool // appends mip 0 as T: u8, float4, ...
fn read_texture(t: Texture, region: Region, out: u8[:]) -> bool
fn read_pixels(t: Texture) -> u8[>..] // mip 0's bytes, top row first
fn save_png(t: Texture, path: const u8[:]) -> bool // RGBA8 or BGRA8
fn screenshot(path: const u8[:]) -> bool // the screen, as shown
fn generate_mips(t: Texture)
fn texture_info(t: Texture) -> TextureDesc
fn release(t: Texture)
struct Sampler { id: u32 }
struct SamplerDesc { min_filter: i32 = 1, mag_filter: i32 = 1, mip_filter: i32 = 1,
wrap_u: i32 = 0, wrap_v: i32 = 0, wrap_w: i32 = 0,
max_anisotropy: f32 = 1.0, compare: i32 = 0 } // LINEAR, REPEAT
fn sampler(desc: SamplerDesc) -> Sampler fn sampler(filter: i32, wrap: i32) -> Sampler
fn shadow_sampler() -> Sampler // for sampler2DShadow
fn release(s: Sampler)Kinds TEXTURE_2D, TEXTURE_2D_ARRAY,
TEXTURE_3D, TEXTURE_CUBE (faces +x, -x, +y,
-y, +z, -z as layers 0 to 5). Usage SAMPLED,
COLOR_TARGET, DEPTH_TARGET,
STORAGE_READ, COMPUTE_READ,
COMPUTE_WRITE, COMPUTE_READ_WRITE; a
multisampled texture is a target only. Formats RGBA8,
BGRA8, RGBA8_SRGB, R8,
RG8, RGBA16F, RGBA32F,
R16F, RG16F, R32F,
RG32F, R32UI, RGBA8UI,
RGB10A2, RG11B10F, and DEPTH16,
DEPTH24, DEPTH32F,
DEPTH24_STENCIL8, DEPTH (32-bit float where it
can also be sampled). Filters NEAREST, LINEAR;
wraps REPEAT, MIRROR, CLAMP;
compares COMPARE_LESS and the like.
Pipelines and drawing
struct Pipeline { id: u32 }
struct PipelineDesc { primitive: i32 = 0, cull: i32 = 0, clockwise: bool = false,
wireframe: bool = false, depth_test: bool = false,
depth_write: bool = false, depth_compare: i32 = 2, blend: i32 = 0,
instance_location: i32 = -1, depth_bias: f32 = 0.0,
depth_bias_slope: f32 = 0.0, formats: u8[16] = [0; 16] }
fn pipeline(vs: const u8[:], fs: const u8[:]) -> Pipeline // and (vs, fs, desc)
fn release(p: Pipeline)
struct PassDesc { color: Texture[4], resolve: Texture[4], depth: Texture, layer: i32 = 0,
mip: i32 = 0, clear_color: bool = true, clear_depth: bool = true,
color_value: float4 = float4 { 0.0, 0.0, 0.0, 1.0 }, depth_value: f32 = 1.0 }
fn begin_pass(desc: PassDesc) -> bool
fn begin_pass(target: Texture, depth: Texture, clear: float4) -> bool // Texture { 0 }: no depth
fn begin_screen(clear: float4) -> bool
fn end_pass()
fn bind(p: Pipeline)
fn bind_vertex_buffer(b: Buffer) // and (slot, b, offset): slot 1 per instance
fn bind_index_buffer(b: Buffer, index_size: i64) // 2 or 4 bytes; and (b, size, offset)
fn bind_texture(stage: i64, slot: i64, t: Texture, s: Sampler) // VERTEX, FRAGMENT, COMPUTE
fn bind_storage_texture(stage: i64, slot: i64, t: Texture)
fn bind_storage_buffer(stage: i64, slot: i64, b: Buffer)
fn uniforms<T>(stage: i64, slot: i64, u: T) // a flat struct as a uniform block
fn push_uniforms(stage: i64, slot: i64, data: const u8[:])
fn viewport(x: f32, y: f32, w: f32, h: f32) fn scissor(x: i64, y: i64, w: i64, h: i64)
fn draw(vertices: i64) fn draw_instanced(vertices: i64, instances: i64)
fn draw_indexed(indices: i64) fn draw_indexed_instanced(indices: i64, instances: i64)
fn draw(vertices, instances, first_vertex, first_instance)
fn draw_indexed(indices, instances, first_index, vertex_offset, first_instance)A pipeline is not tied to the formats it draws into; it is made for
each pass's targets the first time it is bound in one. The vertex
shader's inputs are read in location order, one after another, from one
packed element, laid out as a Goose struct lays out its fields:
struct Vertex { pos: float3, uv: float2 } feeds
layout(location = 0) in vec3 and
layout(location = 1) in vec2. The inputs from
instance_location on come per instance from vertex buffer
1, and formats[location] overrides a location's format
(VERTEX_UBYTE4_NORM for a color as four bytes,
VERTEX_HALF2, and the rest of VERTEX_*).
Primitives TRIANGLES, TRIANGLE_STRIP,
LINES, LINE_STRIP, POINTS; culls
CULL_NONE, CULL_FRONT, CULL_BACK
(front faces counterclockwise unless clockwise); blends
BLEND_NONE, BLEND_ALPHA,
BLEND_ADD, BLEND_PREMULTIPLIED,
BLEND_MULTIPLY.
Uniform blocks are laid out by std140, while Goose structs are
packed. In std140 a vec3 or vec4 member starts on a multiple of 16 bytes
and a block's size rounds up to 16, so the matching struct needs padding
(struct Place { rect: float4, depth: f32, pad 12 }).
Pushing a struct whose size differs from the shader's block is a misuse.
On Direct3D 12 a shader's gl_InstanceIndex does not count a
draw's first_instance, so keep first_instance
at 0 if the shader reads gl_InstanceIndex.
Compute
struct ComputePipeline { id: u32 }
fn compute_pipeline(cs: const u8[:]) -> ComputePipeline
fn begin_compute(rw_buffers: const Buffer[:]) -> bool // and (rw_buffers, rw_textures)
fn bind(p: ComputePipeline)
fn dispatch(x: i64, y: i64, z: i64) // workgroups of the shader's local size
fn end_compute()
fn release(p: ComputePipeline)Matrices
struct mat4 { c0: float4, c1: float4, c2: float4, c3: float4 } // column-major, as GLSL's mat4
let mat4_identity
fn mul(a: mat4, b: mat4) -> mat4 fn mul(m: mat4, v: float4) -> float4
fn translate(t: float3) -> mat4 fn scale(s: float3) -> mat4
fn rotate(axis: float3, angle: f32) -> mat4
fn perspective(fovy: f32, aspect: f32, near: f32, far: f32) -> mat4 // right-handed, depth 0..1
fn ortho(left, right, bottom, top, near, far: f32) -> mat4
fn look_at(eye: float3, target: float3, up: float3) -> mat4Clip space is SDL_GPU's on every backend: y up, depth from 0 to 1, texture coordinates with (0, 0) at the top left.
audio
Procedural PCM sound effects over SDL3, independent of a window or
graphics device. import audio; uses the
audio:: namespace. It is a module of the gfx
package (goose_gfx). It is
compiled into the compiler with -DGOOSE_BUILTIN_LIBS=all
and otherwise built on first use; goose build links it. See
the package's docs/audio.md
for the mixer.
| Function | Behavior |
|---|---|
available() -> bool |
True when the native layer is linked; no device probe. |
open(sample_rate = 48000) -> bool |
Open the default output device, stereo output. |
open_offline(sample_rate = 48000) -> bool |
Open the same mixer without a device or background thread. |
close() |
Stop playback, free every sound, invalidate handles; safe to repeat. |
create_sound(samples: const f32[:], sample_rate = 48000, channels = 1) -> Sound |
Copy mono or interleaved stereo PCM into owned storage. |
release_sound(sound) -> bool |
Stop its voices and free the PCM. Zero is harmless. |
play(sound, volume = 1.0, pan = 0.0, pitch = 1.0, looping = false) -> Voice |
Start an independent voice, allowing overlapping repeats. |
playing(voice) -> bool |
Whether the voice is still mixing. Stale/zero handles return false. |
stop(voice), stop_all() |
Stop one or every voice. Completed/stale/zero voices are harmless. |
set_voice(voice, volume, pan, pitch) -> bool |
Change a live voice without restarting. |
voice_position(voice) -> f64 |
Seconds into a playing voice's sound; -1 when it is not playing. |
seek(voice, seconds: f64) -> bool |
Move a playing voice to that many seconds into its sound. |
render(out: f32[:]) -> bool |
Offline only: write interleaved stereo output and advance playback. |
error() -> u8[>..] |
Most recent failure; successful calls do not clear it. |
error_into(out: u8[:]) -> i64 |
Copy as much error text as fits, returning its full length. |
All calls run on the main thread; calls reachable from
thread_fn are rejected. Sound { id: u64 } and
Voice { id: u64 } are explicit resources. Sound creation
copies the source immediately: Goose can then mutate or free its array.
Releasing a sound stops all its voices. Completed voices recycle
automatically. A new resource never aliases an old handle, even across
close() and open().
There is one mixer, at most MAX_SOUNDS = 256 sounds and
MAX_VOICES = 64 simultaneous voices. Exhaustion returns
zero without stealing a voice. Rates are 8000..192000 Hz. PCM must be
nonempty, finite, in [-1, 1], with complete frames (two
samples per stereo frame). Volume is 0..1; pan is
-1..1, centered at zero, attenuating the opposite channel;
pitch is 0.125..8 and changes speed and duration together.
Mixing uses linear interpolation and clips the final sum to
[-1, 1]. Leave headroom when many effects overlap.
Invalid arguments, stale sounds, unavailable devices and resource
exhaustion return false or a zero handle and set error().
They do not abort. Stopping or querying a completed voice is normal;
updating it returns false. Offline render needs an even
sample count, allows empty output, and advances exactly
out.len / 2 frames. Device mode advances automatically;
playing() describes the mixer, so a little audio already
buffered by the device can remain after completion or stop.
voice_position allows for that buffering: with a device it
reports what the listener is hearing, smoothed between the device's
callbacks, so it can keep something in time with a sound. Offline it is
the mixer's own place. seek past the end wraps a looping
voice and moves any other to its last frame; a negative position fails.
The module has no file decoding, streaming input, recording, or
effects.
import audio;
import math;
import os;
fn main() {
if !audio::open() { print(audio::error()); return; }
var pcm: f32[>..] = [];
for i in 12000 {
let t = i as f64 / 48000.0;
let envelope = 1.0 - i as f64 / 12000.0;
pcm.push((sin(TAU * 440.0 * t) * 0.2 * envelope) as f32);
}
let tone = audio::create_sound(pcm);
let voice = audio::play(tone);
while audio::playing(voice) { sleep_ms(5); }
sleep_ms(100); // Let the device finish the already mixed tail.
audio::close();
}physics
Rigid body physics on Box3D: worlds, bodies, shapes of every kind
Box3D has, nine kinds of joints, contact, sensor, hit, move and joint
events, ray and shape casts and overlap queries, a character mover, and
recording and replay. Like gfx, the module is optional: it
needs a compiler built with the third_party/box3d
submodule, and a program using it links what
goose --physics-link msvc|cc prints
(cl game.c @<it>,
cc game.c -o game @<it>; a program that also uses
gfx adds gfx's response file). A compiler built without it
still typechecks and generates C for such a program; only running the
program in-process fails. Everything is in namespace
physics; samples/28_physics_boxes.goose is a
complete program, and the package's docs/physics.md
says how it works.
The API is Box3D's under Goose names: b3Body_GetPosition
is position(body), b3CreateRevoluteJoint is
create_revolute_joint, a getter uses the property name and
a setter prefixes it with set_. Box3D works in meters,
kilograms and seconds and has no built-in up direction; the default
gravity is -10 along y. Where Box3D takes a callback, this API uses
other forms: events and query results come back as fresh arrays (each
also has an _into form that copies into a slice the caller
provides and returns how many there are), and friction and restitution
mixing is chosen from the MIX_* rules. Custom contact
filters, pre-solve callbacks, debug drawing and the standalone dynamic
tree are not included.
Physics runs on the main thread; a thread_fn reaching it
is a compile error. Box3D spreads a step over worker threads of its own,
as many as WorldDef.workers asks for, and the results are
the same for any number of workers.
Errors
A call that can fail for reasons outside the program, such as a file
that is not there or points that make no hull, returns false or a zero
handle, and error() says why. A call that the program
should not have made is reported when it happens and skipped, and the
program aborts at the next step(),
destroy(world) or check(). Examples are a
destroyed body, a revolute function on a prismatic joint, and a height
field on a dynamic body. Every handle carries a generation, so using a
destroyed object is reported as an error instead of crashing.
fn error() -> u8[>..] fn check() fn misuse_count() -> i64
fn available() -> bool fn version() -> Version // Box3D's
fn byte_count() -> i64 // what Box3D has allocated
fn set_length_units_per_meter(units: f32) // before anything else is made
fn length_units_per_meter() -> f32
fn world_count() -> i64 fn max_world_count() -> i64Values
struct Quat { x: f32, y: f32, z: f32, w: f32 } // a rotation; w the scalar part
struct Transform { p: float3, q: Quat } // rotate by q, then move by p
struct Mat3 { cx: float3, cy: float3, cz: float3 } // by columns
struct AABB { lower: float3, upper: float3 }
struct Plane { normal: float3, offset: f32 }
struct MassData { mass: f32, center: float3, inertia: Mat3 }
struct Sphere { center: float3, radius: f32 }
struct Capsule { center1: float3, center2: float3, radius: f32 }
let quat_identity let transform_identity
fn quat_axis_angle(axis: float3, angle: f32) -> Quat
fn mul(a: Quat, b: Quat) -> Quat // b, then a
fn rotate(q: Quat, v: float3) -> float3 fn inv_rotate(q: Quat, v: float3) -> float3
fn conjugate(q: Quat) -> Quat fn normalize(q: Quat) -> Quat
fn rotation_matrix(q: Quat) -> Mat3
fn transform_point(t: Transform, p: float3) -> float3 // and inv_transform_point
fn mul(a: Transform, b: Transform) -> Transform fn inv_mul(a, b) -> TransformWorlds
struct World { id: u32 }
struct WorldDef { gravity: float3 = { 0, -10, 0 }, restitution_threshold: f32 = 1.0,
hit_event_threshold: f32 = 1.0, contact_hertz: f32 = 30.0,
contact_damping_ratio: f32 = 10.0, contact_speed: f32 = 3.0,
maximum_linear_speed: f32 = 400.0, enable_sleep: bool = true,
enable_continuous: bool = true, workers: i32 = 1, friction_mixing: i32 = 0,
restitution_mixing: i32 = 0, user_data: u64 = 0, capacity: Capacity }
fn create_world() -> World // and (def)
fn destroy(w: World) fn is_valid(w: World) -> bool
fn step(w: World, time_step: f32, sub_steps: i64) // 1/60 and 4 are the usual
fn gravity(w) -> float3 fn set_gravity(w, gravity: float3)
fn enable_sleeping(w, flag) fn sleeping_enabled(w) -> bool // and continuous, warm_starting
fn restitution_threshold(w) -> f32 fn hit_event_threshold(w) -> f32 // and set_...
fn maximum_linear_speed(w) -> f32 fn contact_recycle_distance(w) -> f32 // and set_...
fn set_contact_tuning(w, hertz: f32, damping_ratio: f32, push_speed: f32)
fn set_friction_mixing(w, rule: i64) fn set_restitution_mixing(w, rule: i64)
fn worker_count(w) -> i64 fn set_worker_count(w, count: i64) // 1 to MAX_WORKERS
fn explode(w, def: ExplosionDef) // position, radius, falloff, impulse_per_area, mask_bits
fn bounds(w) -> AABB fn awake_body_count(w) -> i64
fn profile(w) -> Profile fn counters(w) -> Counters fn max_capacity(w) -> Capacity
fn user_data(w) -> u64 fn set_user_data(w, data: u64)At most 128 worlds exist at once. Mixing rules:
MIX_DEFAULT (Box3D's: the geometric mean of the frictions,
the larger restitution), MIX_GEOMETRIC,
MIX_MIN, MIX_MAX, MIX_AVERAGE,
MIX_MULTIPLY.
Bodies
struct Body { id: u64 }
struct BodyDef { body_type: i32 = 0, position: float3, rotation: Quat = quat_identity,
linear_velocity: float3, angular_velocity: float3, linear_damping: f32 = 0.0,
angular_damping: f32 = 0.0, gravity_scale: f32 = 1.0,
sleep_threshold: f32 = 0.05, safety_factor: f32 = 0.5,
motion_locks: MotionLocks, enable_sleep: bool = true, is_awake: bool = true,
is_bullet: bool = false, is_enabled: bool = true,
allow_fast_rotation: bool = false, enable_contact_recycling: bool = true,
user_data: u64 = 0 }
fn create_body(w: World, def: BodyDef) -> Body
fn destroy(b: Body) // with its shapes and joints
fn is_valid(b) -> bool fn world(b) -> World
fn body_type(b) -> i32 fn set_type(b, body_type: i64) // STATIC, KINEMATIC, DYNAMIC
fn name(b) -> u8[>..] fn set_name(b, name: const u8[:])
fn user_data(b) -> u64 fn set_user_data(b, data: u64)
fn position(b) -> float3 fn rotation(b) -> Quat fn transform(b) -> Transform
fn transforms(bodies: const Body[:], out: Transform[:]) // many in one call
fn set_transform(b, position: float3, rotation: Quat) // a teleport
fn local_point(b, p) -> float3 fn world_point(b, p) -> float3 // and local_/world_vector
fn linear_velocity(b) -> float3 fn angular_velocity(b) -> float3 // and set_...
fn local_point_velocity(b, p) -> float3 fn world_point_velocity(b, p) -> float3
fn set_target_transform(b, target: Transform, time_step: f32, wake: bool) // kinematic
fn apply_force(b, force: float3, point: float3, wake: bool) fn apply_force_to_center(b, force, wake)
fn apply_torque(b, torque: float3, wake: bool)
fn apply_linear_impulse(b, impulse, point, wake) fn apply_linear_impulse_to_center(b, impulse, wake)
fn apply_angular_impulse(b, impulse: float3, wake: bool)
fn mass(b) -> f32 fn inverse_mass(b) -> f32 fn rotational_inertia(b) -> Mat3
fn world_inverse_inertia(b) -> Mat3 fn local_center(b) -> float3 fn world_center(b) -> float3
fn mass_data(b) -> MassData fn set_mass_data(b, data: MassData) fn apply_mass_from_shapes(b)
fn linear_damping(b) -> f32 fn angular_damping(b) -> f32 fn gravity_scale(b) -> f32 // and set_...
fn is_awake(b) -> bool fn set_awake(b, awake: bool) // the whole island
fn enable_sleep(b, flag) fn sleep_enabled(b) -> bool
fn sleep_threshold(b) -> f32 fn safety_factor(b) -> f32 // and set_...
fn is_enabled(b) -> bool fn disable(b) fn enable(b)
fn motion_locks(b) -> MotionLocks fn set_motion_locks(b, locks: MotionLocks)
fn is_bullet(b) -> bool fn set_bullet(b, flag: bool)
fn fast_rotation_allowed(b) -> bool fn allow_fast_rotation(b, flag: bool)
fn contact_recycling_enabled(b) -> bool fn enable_contact_recycling(b, flag: bool)
fn enable_hit_events(b, flag: bool) // on each of its shapes
fn shape_count(b) -> i64 fn shapes(b) -> Shape[>..]
fn joint_count(b) -> i64 fn joints(b) -> Joint[>..]
fn contacts(b) -> Manifold[>..] // those touching
fn aabb(b) -> AABB fn min_extent(b) -> f32 fn max_extent(b) -> float3
fn closest_point(b, target: float3) -> float3, f32Queries against one body, placed where the query says:
cast_ray(b, origin, translation, filter, max_fraction, body_transform) -> RayHit,
cast_shape, overlap_shape,
collide_mover(b, origin, mover, filter, body_transform) -> PlaneHit[>..]
and
time_of_impact_mover(b, origin, mover, translation, filter, transform1, transform2) -> ToiHit.
Shapes
struct Shape { id: u64 }
struct ShapeDef { material: SurfaceMaterial, density: f32 = 1000.0, filter: Filter,
is_sensor: bool = false, enable_sensor_events: bool = false,
enable_contact_events: bool = false, enable_hit_events: bool = false,
invoke_contact_creation: bool = true, update_body_mass: bool = true,
enable_speculative_contact: bool = true, explosion_scale: f32 = 1.0,
user_data: u64 = 0 }
struct SurfaceMaterial { friction: f32 = 0.6, restitution: f32 = 0.0,
rolling_resistance: f32 = 0.0, tangent_velocity: float3,
user_material_id: u64 = 0, custom_color: u32 = 0 }
struct Filter { category_bits: u64 = ALL_BITS, mask_bits: u64 = ALL_BITS, group_index: i32 = 0 }
fn create_sphere_shape(b: Body, def: ShapeDef, sphere: Sphere) -> Shape
fn create_capsule_shape(b, def, capsule: Capsule) -> Shape
fn create_box_shape(b, def, half_extents: float3) -> Shape // and (..., frame: Transform)
fn create_hull_shape(b, def, hull: Hull) -> Shape
fn create_transformed_hull_shape(b, def, hull, transform: Transform, scale: float3) -> Shape
fn create_mesh_shape(b, def, mesh: Mesh) -> Shape // and (..., scale, materials)
fn create_height_field_shape(b, def, h: HeightField) -> Shape // static bodies; and (..., materials)
fn create_compound_shape(b, def, c: Compound) -> Shape // static bodies
fn destroy(s: Shape) // and (s, update_body_mass: bool)
fn is_valid(s) -> bool fn shape_type(s) -> i32 fn body(s) -> Body fn world(s) -> World
fn is_sensor(s) -> bool fn name(s) -> u8[>..] fn user_data(s) -> u64 // and set_...
fn density(s) -> f32 fn set_density(s, density: f32, update_body_mass: bool)
fn friction(s) -> f32 fn restitution(s) -> f32 // and set_...
fn surface_material(s) -> SurfaceMaterial fn set_surface_material(s, material)
fn mesh_material_count(s) -> i64 fn mesh_material(s, i) -> SurfaceMaterial // and set_...
fn shape_filter(s) -> Filter fn set_shape_filter(s, filter: Filter, invoke_contacts: bool)
fn enable_contact_events(s, flag) fn contact_events_enabled(s) -> bool // and sensor_, hit_
fn sphere(s) -> Sphere fn capsule(s) -> Capsule fn hull(s) -> Hull // hull(): a copy
fn mesh(s) -> Mesh fn mesh_scale(s) -> float3 fn height_field(s) -> HeightField
fn compound(s) -> Compound
fn set_sphere(s, sphere) fn set_capsule(s, capsule) fn set_hull(s, hull) fn set_mesh(s, mesh, scale)
fn ray_cast(s, origin: float3, translation: float3) -> CastOutput // in the world
fn contacts(s) -> Manifold[>..] fn sensor_overlaps(s) -> Shape[>..]
fn aabb(s) -> AABB fn mass_data(s) -> MassData fn closest_point(s, target) -> float3
fn apply_wind(s, wind: float3, drag: f32, lift: f32, max_speed: f32, wake: bool)Shape types SHAPE_SPHERE, SHAPE_CAPSULE,
SHAPE_HULL, SHAPE_MESH,
SHAPE_HEIGHT_FIELD, SHAPE_COMPOUND. Two shapes
collide when each shape's category is in the other's mask, unless they
share a group index: shapes in a positive group always collide, and
shapes in a negative group never do. A mesh collides only with shapes
that are not meshes. The default density is water's, 1000 kg/m³.
Geometry
Hulls, meshes, height fields and baked compounds are made once and shared by shapes. A shape keeps the geometry it was made from. Destroying a mesh, height field or compound that shapes use releases the handle, and the data is freed when the last shape using it is destroyed. A hull is copied into each world that uses it.
struct Hull { id: u32 } struct Mesh { id: u32 }
struct HeightField { id: u32 } struct Compound { id: u32 }
fn create_hull(points: const float3[:]) -> Hull // zero for points in a plane; and (..., max_vertices)
fn create_box_hull(half_extents: float3) -> Hull // and (..., frame)
fn create_cylinder_hull(height, radius, y_offset: f32, sides: i64) -> Hull
fn create_cone_hull(height, radius1, radius2: f32, slices: i64) -> Hull
fn create_rock_hull(radius: f32) -> Hull
fn transformed(h: Hull, transform: Transform, scale: float3) -> Hull
fn create_mesh(vertices: const float3[:], indices: const i32[:]) -> Mesh
fn create_mesh(vertices, indices, material_indices: const u8[:], def: MeshDef) -> Mesh
fn create_grid_mesh(x_count, z_count: i64, cell_width: f32, material_count: i64, identify_edges: bool) -> Mesh
fn create_wave_mesh(...) fn create_torus_mesh(...) fn create_box_mesh(center, extent, identify_edges)
fn create_hollow_box_mesh(center, extent) fn create_platform_mesh(center, height, top_width, bottom_width)
fn create_height_field(heights: const f32[:], material_indices: const u8[:], def: HeightFieldDef) -> HeightField
fn create_grid_height_field(rows, columns: i64, scale: float3, make_holes: bool) -> HeightField
fn create_wave_height_field(rows, columns, scale, row_frequency, column_frequency, make_holes)
fn create_compound(spheres: const CompoundSphere[:], capsules: const CompoundCapsule[:],
hulls: const CompoundHull[:], meshes: const CompoundMesh[:]) -> Compound
fn destroy(h: Hull) fn is_valid(h: Hull) -> bool fn info(h: Hull) -> HullInfo // each kind
fn vertices(h: Hull) -> float3[>..] fn triangles(h: Hull) -> int3[>..] // and for MeshThe same geometry can also be queried on its own, in its own frame:
compute_mass(g, density) for spheres, capsules and hulls,
compute_aabb(g, transform),
ray_cast(g, origin, translation, max_fraction) -> CastOutput,
overlap(g, transform, points, radius) -> bool and
shape_cast(g, points, radius, translation, max_fraction, can_encroach) -> CastOutput
for each kind (a mesh takes its scale first). Between two point clouds
with radii, there are shape_distance,
shape_cast and time_of_impact over two
Sweeps, with sweep_transform(sweep, time).
Joints
struct Joint { id: u64 }
struct JointDef { body_a: Body, body_b: Body, frame_a: Transform, frame_b: Transform,
collide_connected: bool = false, force_threshold: f32, torque_threshold: f32,
constraint_hertz: f32 = 60.0, constraint_damping_ratio: f32 = 2.0,
draw_scale: f32 = 1.0, user_data: u64 = 0 }
fn joint_def(a: Body, b: Body, anchor: float3) -> JointDef // and (a, b, frame: Transform)
fn local_frame(b: Body, world_frame: Transform) -> Transform
fn create_revolute_joint(w: World, def: RevoluteJointDef) -> RevoluteJoint
// and create_distance_joint, _filter_, _motor_, _parallel_, _prismatic_, _spherical_,
// _weld_, _wheel_: each kind's definition starts with `base: JointDef`.Each joint kind has its own handle type, such as
struct RevoluteJoint { joint: Joint }. Kind-specific
functions take that handle. Functions common to all joints take its
joint field:
fn destroy(j: Joint) // and (j, wake_bodies: bool)
fn is_valid(j) -> bool fn joint_type(j) -> i32 fn body_a(j) -> Body fn body_b(j) -> Body
fn world(j) -> World fn local_frame_a(j) -> Transform // and _b, and set_...
fn collide_connected(j) -> bool fn user_data(j) -> u64 // and set_...
fn wake_bodies(j) fn is_awake(j) -> bool
fn constraint_force(j) -> float3 fn constraint_torque(j) -> float3
fn linear_separation(j) -> f32 fn angular_separation(j) -> f32
fn constraint_tuning(j) -> f32, f32 fn set_constraint_tuning(j, hertz, damping_ratio)
fn force_threshold(j) -> f32 fn torque_threshold(j) -> f32 // and set_...| Kind | What it does | Its own |
|---|---|---|
RevoluteJoint |
a hinge about the frames' z axis | angle,
enable_limit/set_limits/lower_limit/upper_limit,
enable_motor/set_motor_speed/set_max_motor_torque/motor_torque,
enable_spring/set_spring_hertz/set_spring_damping_ratio/set_target_angle |
PrismaticJoint |
a slider along the frames' x axis | translation, speed, limits, a motor
(set_max_motor_force, motor_force), a spring
to set_target_translation |
DistanceJoint |
two anchors a distance apart | rest_length/set_rest_length,
current_length,
set_length_range/min_length/max_length,
a spring with set_spring_force_range, a motor |
SphericalJoint |
a ball and socket | enable_cone_limit/set_cone_limit/cone_angle,
set_twist_limits/twist_angle, a spring to
set_target_rotation, a motor to
set_motor_velocity |
WeldJoint |
two bodies as one | set_linear_hertz, set_angular_hertz and
their damping ratios; 0 hertz is rigid |
WheelJoint |
a wheel on a suspension | enable_suspension/limits,
enable_spin_motor/set_spin_motor_speed/spin_speed,
enable_steering/set_target_steering_angle/steering_angle |
MotorJoint |
drives body b relative to body a | set_linear_velocity, set_angular_velocity,
their limits, and springs |
ParallelJoint |
keeps the frames' z axes parallel | set_spring_hertz,
set_spring_damping_ratio, set_max_torque |
FilterJoint |
only stops the two colliding |
Every setter has its getter. Kinds are JOINT_REVOLUTE
and so on; using a kind's function on a joint of another kind is a
misuse.
Events
Events are read after the step they happened in. Each function returns an array of what the last step produced:
fn contact_begin_events(w) -> ContactEvent[>..] // shape_a, shape_b, contact
fn contact_end_events(w) -> ContactEvent[>..]
fn contact_hit_events(w) -> ContactHitEvent[>..] // point, normal, approach_speed, materials
fn sensor_begin_events(w) -> SensorEvent[>..] // sensor, visitor
fn sensor_end_events(w) -> SensorEvent[>..]
fn body_move_events(w) -> BodyMoveEvent[>..] // transform, body, user_data, fell_asleep
fn joint_events(w) -> JointEvent[>..] // joint, user_data: past its thresholds
struct Contact { ... }
fn is_valid(c: Contact) -> bool fn manifolds(c: Contact) -> Manifold[>..]A shape reports contacts with enable_contact_events,
hits with enable_hit_events (faster than the world's
hit_event_threshold), and sensor visits with
enable_sensor_events on both the sensor and the visitor. A
Manifold has the contact and its shapes, the normal from a
to b, and up to four ManifoldPoints with their anchors,
separation and impulses.
Queries
struct QueryFilter { category_bits: u64 = ALL_BITS, mask_bits: u64 = ALL_BITS }
fn overlap_aabb(w, box: AABB, filter: QueryFilter) -> Shape[>..]
fn overlap_shape(w, origin: float3, points: const float3[:], radius: f32, filter) -> Shape[>..]
fn cast_ray(w, origin: float3, translation: float3, filter) -> RayHit[>..] // nearest first
fn cast_ray_closest(w, origin, translation, filter) -> RayHit // hit false: none
fn cast_shape(w, origin, points, radius, translation, filter) -> RayHit[>..]A query shape is the convex hull of up to
MAX_PROXY_POINTS points around origin, grown
by radius: one point is a sphere, two a capsule, eight the
corners of a box.
A character mover
fn cast_mover(w, origin: float3, mover: Capsule, translation: float3, filter) -> f32
fn collide_mover(w, origin, mover, filter) -> PlaneHit[>..]
fn solve_planes(target_delta: float3, planes: CollisionPlane[:]) -> float3, i64
fn clip_vector(vector: float3, planes: const CollisionPlane[:]) -> float3cast_mover determines how far a capsule can move.
collide_mover returns the planes it touches. Pass these as
CollisionPlanes to solve_planes to find the
allowed move closest to the target. clip_vector removes
velocity components that point into those planes.
Recording and replay
struct Recording { id: u32 } struct Player { id: u32 }
fn create_recording() -> Recording fn load_recording(path: const u8[:]) -> Recording
fn start_recording(w: World, r: Recording) fn stop_recording(w: World)
fn size(r) -> i64 fn bytes(r) -> u8[>..] fn save(r, path: const u8[:]) -> bool
fn validate_replay(r, workers: i64) -> bool // replays it; the same?
fn create_player(r, workers: i64) -> Player
fn step(p: Player) -> bool fn sub_step(p) fn at_pre_step(p) -> bool
fn restart(p) fn seek(p, frame: i64) fn frame(p) -> i64 fn frame_count(p) -> i64
fn at_end(p) -> bool fn diverged(p) -> bool fn diverge_frame(p) -> i64
fn world(p) -> World fn body_count(p) -> i64 fn body(p, index: i64) -> Body
fn info(p) -> PlayerInfo fn set_worker_count(p, count: i64)
fn destroy(r: Recording) fn destroy(p: Player)A recording holds everything a world does between
start_recording and stop_recording. A player
replays it into a world of its own, step by step, and checks that the
result is the same. The recorded bodies exist once the replay has made
them; body(p, k) is the k-th body made.
let world = physics::create_world();
let ground = physics::create_body(world, physics::BodyDef { position: float3 { 0.0, -1.0, 0.0 } });
physics::create_box_shape(ground, physics::ShapeDef {}, float3 { 20.0, 1.0, 20.0 });
let crate = physics::create_body(world, physics::BodyDef { body_type: physics::DYNAMIC,
position: float3 { 0.0, 4.0, 0.0 } });
physics::create_box_shape(crate, physics::ShapeDef {}, float3 { 0.5, 0.5, 0.5 });
for i in 90 { physics::step(world, 1.0 / 60.0, 4); }
print(physics::position(crate).y); // resting on the ground: about 0.5
physics::destroy(world);ui
Windows and widgets on Nuklear, the immediate-mode GUI library, drawn
through gfx: windows, rows and layout spaces, groups,
trees, list views, labels, buttons, check boxes, options, selectables,
sliders, knobs, progress bars, color pickers, properties, text fields
and editors, charts, popups, combo boxes, contextual menus, tooltips,
menus, styles and fonts. Like gfx, the module is optional:
it needs a compiler built with the third_party/nuklear
submodule, and a program using it links what
goose --ui-link msvc|cc prints, together with gfx's if it
renders through gfx (cl app.c @<ui> @<gfx>). A
compiler built without it still typechecks and generates C for such a
program; only running the program in-process fails. Everything is in
namespace ui; samples/29_ui_todo.goose is a
complete program, and the package's docs/ui.md
says how it works.
The API is Nuklear's under Goose names: nk_button_label
is button_label. In immediate mode, the program declares on
each frame which windows and widgets there are, in order, and the same
calls report what the user did: button_label returns true
on the frame the button is clicked, checkbox_label flips
the bool it is given, and slider_float moves
the f32. The program keeps its own data; a context stores
only where windows are and what is open, active or scrolled.
guard gfx::open("sound", 400, 300) else { abort(str("gfx: ", gfx::error())); }
let ctx = ui::create(13.0);
var volume: f32 = 0.5;
var muted = false;
while gfx::frame() {
ui::input_from_gfx(ctx);
if ui::begin(ctx, "Sound", ui::rect(20, 20, 220, 130), ui::WINDOW_TITLE | ui::WINDOW_MOVABLE) {
ui::layout_row_dynamic(ctx, 24.0, 1);
ui::slider_float(ctx, 0.0, volume, 1.0, 0.05);
ui::checkbox_label(ctx, "Muted", muted);
if ui::button_label(ctx, "Reset") { volume = 0.5; }
}
ui::end(ctx);
gfx::begin_screen(float4 { 0.1, 0.1, 0.1, 1.0 });
gfx::end_pass();
ui::render(ctx);
}
ui::destroy(ctx);A frame consists of input (here from gfx), then the windows, each
begin followed by its end whether or not
begin returned true, and then drawing what the windows
built. Inside a window every widget takes the next place in the current
row, so a row must come first (layout_row_*). The ui runs
on the main thread; a thread_fn reaching it is a compile
error.
Errors
A call that can fail for reasons outside the program, such as a font
file that is not there or bytes that are not a font, returns false or a
zero handle, and error() says why. A call that the program
should not have made is printed when it happens and skipped, and the
program aborts at the next input_begin,
convert, render, destroy(context)
or check(). Examples are a widget outside a window, a group
ended that was never begun, a window begun inside another, a key that is
not a valid key, and a destroyed handle. Contexts, font atlases, fonts
and text editors are handles with a generation, so using a destroyed one
is reported as an error instead of crashing.
fn error() -> u8[>..] fn check() fn misuse_count() -> i64 fn available() -> boolContexts and input
struct Context { id: u32 }
fn create(font_height: f32) -> Context // Nuklear's own font; 13 is its native size
fn create(font_height: f32, scale: f32) -> Context // drawn at a scale (set_scale)
fn create_from_file(path: const u8[:], font_height: f32) -> Context // a TrueType font of its own
fn create_from_memory(ttf: const u8[:], font_height: f32) -> Context // from the font's bytes
fn create_context(font: Font) -> Context // a font from a baked atlas
fn destroy(c: Context) fn is_valid(c: Context) -> bool
fn context_atlas(c) -> FontAtlas fn font(c) -> Font // drawn with now
fn clear(c) // ends a frame; input_begin does it for a frame that drew
fn set_scale(c, scale: f32) -> bool fn scale(c) -> f32 // pixels per ui unit
fn set_delta_time(c, seconds: f32) fn delta_time(c) -> f32
fn set_clipboard(c, text: const u8[:]) // what KEY_PASTE pastes
fn copied(c) -> u8[>..], bool // what a text field copied or cut, if any
fn input_begin(c) fn input_end(c)
fn input_motion(c, x: i64, y: i64)
fn input_button(c, button: i64, x: i64, y: i64, down: bool) // BUTTON_LEFT, _MIDDLE, _RIGHT, ...
fn input_scroll(c, amount: float2)
fn input_key(c, key: i64, down: bool) // KEY_*: editing keys and shortcuts
fn input_text(c, text: const u8[:]) // and input_char, input_glyph, input_unicodeThe whole ui can be drawn bigger or smaller with
set_scale. The program picks the scale as it likes, for
example from the window's size, the display, or a setting. The layout
stays the same: windows, rows, fonts and the style keep their sizes in
the ui's own units. The scale is the number of pixels per unit;
convert and render draw at that scale, and
input positions are given in those pixels. A context made by
create or create_from_* has its font baked
again at the scale, so its text stays sharp. (Nuklear's built-in font
imitates a bitmap font and scales well only by whole numbers; a TrueType
font scales well by any amount.) A context made on a font of the
program's own keeps that font's atlas as it is;
bake(atlas, scale) makes the atlas sharp at a scale, and is
called again at each new scale. set_scale is called between
frames, not during one.
Input goes between input_begin and
input_end, before any window, in one batch per frame;
input_from_gfx (below) does all of this. Positions are in
pixels. A key pressed and released in the same frame counts as a press.
Nuklear looks at a frame's keys afterwards, in the order of their
KEY_* values. A button's press or release also zeroes the
frame's mouse motion. The input that widgets see can also be queried
directly:
fn input_mouse(c) -> Mouse // pos, prev, delta, scroll_delta, grab state
fn input_is_mouse_down(c, button) -> bool // and _pressed, _released
fn input_is_key_down(c, key) -> bool // and _pressed, _released
fn input_is_mouse_hovering_rect(c, r: Rect) -> bool // and _prev_, _still_
fn input_has_mouse_click_in_rect(c, button, r) -> bool // where the last click was
fn input_is_mouse_click_in_rect(c, button, r) -> bool // a release there this frame
fn input_is_mouse_hovering_delay_rect(c, r, timer: f32&, delay: f32) -> boolA click is a button going down or up. The has_ queries
look at where the button's last click was, and the is_
queries look for a click in this frame. The _delay queries
count hovering time in a timer the program keeps between frames (also
_still_delay and _still_delay_clicked).
A program that shares its input with the ui, such as a game with keys of its own, can ask which of the two the input belongs to:
fn wants_keyboard(c) -> bool // a text field or a property being typed into has the keys
fn wants_mouse(c) -> bool // over a window or popup taking input, or dragging from oneClicking a text field gives it keyboard focus. It keeps focus until a
click outside it, a call to edit_unfocus, or its window
closing or being omitted from a frame. Moving the mouse does not change
this. The field also stays focused while its window is behind another
window, hidden, or covered by its own popup. It receives no keys until
the window is in front, shown again, or the popup is closed.
The ui owns mouse input while the pointer is over a window or popup,
or while a button pressed there remains held. A button pressed elsewhere
keeps mouse input with the program until release. A window or popup
created with WINDOW_NO_INPUT, such as a message log or
hover information, passes mouse input through to the program. It cannot
be moved, scaled, scrolled, or brought to the front, and its widgets
receive no clicks. Nuklear's item_is_any_active is true
whenever the mouse is over a window, so it cannot determine who owns
keyboard input.
Text rendering and game HUDs
ui provides text rendering for gfx applications as well
as widgets: ui::label uses row/space layout, and
ui::draw_text draws at an explicit rectangle on the current
window's canvas. ui::create(13.0) supplies a built-in font
without an asset file; create_from_file and
create_from_memory supply TrueType fonts. Font measurement
and atlas access are listed under Fonts.
For a HUD, create a dedicated context once, hide its window
background, and use WINDOW_NO_INPUT | WINDOW_NO_SCROLLBAR
with no title or border. The window supplies a canvas and clipping; it
does not need interactive widgets. The following fits inside a gfx
program (after opening gfx):
let hud = ui::create(13.0);
if hud.id == 0 { abort(ui::error()); }
var style = ui::style(hud);
style.window.fixed_background = ui::style_item_hide();
ui::set_style(hud, style);
while gfx::frame() {
ui::input_from_gfx(hud);
if ui::begin(hud, "HUD", ui::rect(0, 0, 320, 80),
ui::WINDOW_NO_INPUT | ui::WINDOW_NO_SCROLLBAR) {
ui::draw_text(hud, ui::rect(12, 12, 280, 24), "Health: 100",
ui::font(hud), ui::Color { 0, 0, 0, 0 },
ui::Color { 255, 255, 255, 255 });
}
ui::end(hud); // Always paired with begin, even when begin returns false.
// Draw the game scene here and end its render pass.
ui::render(hud);
}
ui::destroy(hud); // Before gfx::close().Draw the HUD after the game scene. ui::render uses gfx
internally and retains the scene beneath it. For a resizable full-screen
overlay, update the window's bounds to the viewport; for a custom
renderer, ui can also export its text and geometry commands or converted
triangles. World-space sprites, billboard orientation and sprite
animation belong to the game's sprite renderer; ordinary HUD text can
use this existing font path.
Fonts
struct FontAtlas { id: u32 } struct Font { id: u32 }
fn create_font_atlas() -> FontAtlas fn destroy(a: FontAtlas)
fn add_default_font(a, height: f32) -> Font // ProggyClean
fn add_font_from_file(a, path: const u8[:], height: f32) -> Font
fn add_font_from_memory(a, ttf: const u8[:], height: f32) -> Font
// each also (..., config: FontConfig) and (..., config, ranges: const u32[:])
fn bake(a) -> bool // then the fonts can be used
fn bake(a, scale: f32) -> bool // glyphs scale times the size, for a ui at that scale;
// again at another, fonts kept
fn atlas_size(a) -> int2 fn atlas_pixels(a) -> u8[>..] // RGBA
fn set_texture(a, texture: u32) fn texture(a) -> u32
fn fonts(a) -> Font[>..] fn atlas(f: Font) -> FontAtlas
fn info(f) -> FontInfo // height, ascent, descent, glyph count, fallback
fn text_width(f, text: const u8[:]) -> f32
fn find_glyph(f, codepoint: u32) -> FontGlyph, bool
fn glyph_ranges(which: i64) -> u32[>..] // RANGE_DEFAULT, _CHINESE, _CYRILLIC, _KOREANAn atlas bakes its fonts into one image; a context draws with one of
them and uses the atlas's white texel for its shapes.
FontConfig holds these settings, with Nuklear's defaults:
oversampling, pixel snapping, spacing, the characters to bake
(ranges, a RANGE_* set, or pairs of first and
last character given as an array), the fallback glyph, and
merge_mode, which adds the glyphs to the atlas's first
font. Fonts are added before the atlas is first baked. The atlas keeps
their data, and baking it again at another scale replaces their glyphs
and image in place, so contexts drawing with its fonts keep using them
and draw sharp at the new scale. A context draws its shapes with the
atlas of the font it was made with or last given by
style_set_font; destroying an atlas a context still uses is
an error.
Windows and layout
fn begin(c, title: const u8[:], bounds: Rect, flags: i64) -> bool // WINDOW_* flags
fn begin_titled(c, name, title, bounds, flags) -> bool
fn end(c)
fn window_get_bounds(c) -> Rect // and _position, _size, _content_region, _scroll, ...
fn window_has_focus(c) -> bool fn window_is_hovered(c) -> bool
fn window_is_collapsed(c, name) -> bool // and _closed, _hidden, _active
fn window_set_bounds(c, name, bounds: Rect) // and _position, _size, _focus, set_scroll
fn window_close(c, name) fn window_collapse(c, name, state: i64) // MINIMIZED, MAXIMIZED
fn window_show(c, name, state: i64) // SHOWN, HIDDEN; and the _if forms
fn layout_row_dynamic(c, height: f32, cols: i64) // equal parts of the width
fn layout_row_static(c, height: f32, item_width: i64, cols: i64)
fn layout_row(c, format: i64, height: f32, ratios: const f32[:]) // DYNAMIC, STATIC
fn layout_row_begin(c, format, height, cols) fn layout_row_push(c, value: f32) fn layout_row_end(c)
fn layout_row_template_begin(c, height) // push_dynamic, push_variable(min), push_static(w), _end
fn layout_space_begin(c, format, height, widget_count) fn layout_space_push(c, bounds: Rect)
fn layout_space_end(c) // and _bounds, _to_screen, _to_local
fn layout_set_min_row_height(c, height) fn spacer(c) fn spacing(c, cols)A row lays out the widgets after it, and repeats until the next row
call. begin returns false for a collapsed or hidden window,
and its end still follows. Windows are found by name, so
two windows in one frame cannot share a name.
Groups, trees and list views
fn group_begin(c, title, flags) -> bool fn group_end(c)
fn group_scrolled_begin(c, offset: Scroll, title, flags) -> bool
fn group_scrolled_end(c) -> Scroll fn group_get_scroll(c, id) -> Scroll
fn tree_push(c, type: i64, title, initial: i64) -> bool // TREE_NODE or _TAB, MINIMIZED..
fn tree_pop(c)
fn tree_push_id(c, type, title, initial, id: i64) -> bool // same titles apart
fn tree_image_push(c, type, img: Image, title, initial) -> bool
fn tree_state_push(c, type, title, state: i64&) -> bool fn tree_state_pop(c)
fn tree_element_push(c, type, title, initial, selected: bool&) -> bool fn tree_element_pop(c)
fn list_view_begin(c, id, flags, row_height: i64, row_count: i64) -> ListView, bool
fn list_view_end(c) // build rows view.begin until view.end onlyA group is a scrolled panel inside a window, with its own rows; its end follows only when its begin returned true. A tree node's contents go between a push that returned true and its pop.
Widgets
fn label(c, text: const u8[:], align: i64) // TEXT_LEFT, TEXT_CENTERED, TEXT_RIGHT
fn label_colored(c, text, align, color: Color) fn label_wrap(c, text)
fn value_int(c, prefix, v: i64) // and _bool, _uint, _float, _color_*
fn link_label(c, text, align) -> bool // and _colored, _underline, _styled, ...
fn image(c, img: Image) fn image_color(c, img, color)
fn button_label(c, text) -> bool // clicked this frame
fn button_symbol(c, symbol: i64) -> bool // SYMBOL_*; and _color, _image, _*_label
fn button_label_styled(c, style: const StyleButton&, text) -> bool // and for each kind
fn button_push_behavior(c, behavior: i64) -> bool // BUTTON_DEFAULT, BUTTON_REPEATER
fn button_pop_behavior(c) -> bool
fn checkbox_label(c, text, active: bool&) -> bool // and _align, _flags_label
fn check_label(c, text, active: bool) -> bool // the new state
fn radio_label(c, text, active: bool&) -> bool fn option_label(c, text, active: bool) -> bool
fn selectable_label(c, text, align, value: bool&) -> bool // and _image_, _symbol_; select_*
fn slider_float(c, min: f32, value: f32&, max: f32, step: f32) -> bool // and slider_int
fn slide_float(c, min, value, max, step) -> f32 // and slide_int
fn knob_float(c, min, value: f32&, max, step, zero_direction: i64, dead_zone_degrees: f32) -> bool
fn progress(c, cur: i64&, max: i64, modifiable: bool) -> bool fn prog(c, cur, max, modifiable) -> i64
fn color_picker(c, color: Colorf, format: i64) -> Colorf // RGB, RGBA
fn color_pick(c, color: Colorf&, format) -> bool
fn property_float(c, name, min, value: f32&, max, step, inc_per_pixel: f32) -> bool
// and _int, _double; propertyi, propertyf, propertyd return the value
fn widget(c) -> i64, Rect // a place for a widget of the program's own: WIDGET_*
fn widget_bounds(c) -> Rect // the next widget's place, and _position, _size, ...
fn widget_is_hovered(c) -> bool fn widget_is_mouse_clicked(c, button) -> bool
fn widget_disable_begin(c) fn widget_disable_end(c) // shown dimmed, inertThe _label functions take any Goose string, so a Nuklear
_text function maps to the same call. A property is
dragged, stepped with its arrows or typed into; a name starting with
# is not shown. Sliders and knobs need a non-empty
range.
Text editing
fn edit_string(c, flags: i64, text: T&, filter: i64) -> i64 // u8[..k], u8[..]
fn edit_string(c, flags, buffer: u8[:], len: i64&, filter) -> i64
fn edit_buffer(c, flags, edit: TextEdit, filter) -> i64 // keeps undo
fn edit_focus(c, flags) fn edit_unfocus(c) // the next edit
fn filter_accepts(filter: i64, rune: u32) -> bool
fn create_text_edit() -> TextEdit fn destroy(e: TextEdit)
fn text(e) -> u8[>..] fn set_text(e, text) fn state(e) -> TextEditState
fn set_cursor(e, cursor, select_start, select_end) fn set_mode(e, mode: i64)
fn textedit_text(e, text) fn textedit_delete(e, at, len) fn textedit_delete_selection(e)
fn textedit_select_all(e) fn textedit_cut(e) -> bool fn textedit_paste(e, text) -> bool
fn textedit_undo(e) fn textedit_redo(e)Flags are EDIT_*, with the usual sets
EDIT_SIMPLE, EDIT_FIELD (one line, selectable,
clipboard) and EDIT_BOX (several lines). The result reports
EDIT_ACTIVE or _INACTIVE,
_ACTIVATED and _DEACTIVATED in this frame, and
_COMMITTED for Enter with EDIT_SIG_ENTER. A
field accepts what fits in its text's capacity and passes a
FILTER_* (DEFAULT, ASCII,
FLOAT, DECIMAL, HEX,
OCT, BINARY); a paste accepts what typing
would. A text editor (TextEdit) holds its own text, cursor,
selection, mode and undo history; a field over a buffer keeps no undo
between frames. Editing keys are KEY_*;
KEY_COPY and KEY_CUT hand the selection to
copied, KEY_PASTE pastes
set_clipboard's text.
Popups, combo boxes, menus and tooltips
fn popup_begin(c, type: i64, title, flags, rect: Rect) -> bool // POPUP_STATIC, _DYNAMIC
fn popup_close(c) fn popup_end(c) // and popup_get_scroll, popup_set_scroll
fn combo(c, items: const u8[:][:], selected: i64, item_height: i64, size: float2) -> i64
fn combobox(c, items, selected: i64&, item_height, size) -> bool
// and combo_separator, combo_string over one string of items
fn combo_begin_label(c, selected: const u8[:], size) -> bool // and _color, _symbol, _image
fn combo_item_label(c, text, align) -> bool fn combo_close(c) fn combo_end(c)
fn contextual_begin(c, flags, size: float2, trigger_bounds: Rect) -> bool
fn contextual_item_label(c, text, align) -> bool fn contextual_close(c) fn contextual_end(c)
fn tooltip(c, text) fn tooltip_offset(c, text, position: i64, offset: float2) // TOP_LEFT..
fn tooltip_begin(c, width: f32) -> bool fn tooltip_end(c)
fn do_tooltip(c, text, bounds: Rect) // when hovering bounds; and _delay(.., timer: f32&)
fn menubar_begin(c) fn menubar_end(c) // first in its window
fn menu_begin_label(c, text, align, size: float2) -> bool // and _image, _symbol forms
fn menu_item_label(c, text, align) -> bool fn menu_close(c) fn menu_end(c)A popup, combo box, contextual menu, menu or tooltip is a small
window of its own, drawn above the one it opens from, with rows inside.
Each end follows only a begin that returned true, and none of them opens
inside another. A tooltip is requested before the widget it is about,
when widget_is_hovered is true.
Charts
fn chart_begin(c, type: i64, count: i64, min: f32, max: f32) -> bool // CHART_LINES, _COLUMN
fn chart_begin_colored(c, type, color: Color, active: Color, count, min, max) -> bool
fn chart_add_slot(c, type, count, min, max) // and _colored: another line or columns
fn chart_push(c, value: f32) -> i64 // CHART_HOVERING, CHART_CLICKED
fn chart_push_slot(c, value, slot: i64) -> i64
fn chart_end(c)
fn plot(c, type, values: const f32[:]) // a whole chart, scaled to the valuesStyle and colors
fn style(c) -> Style fn set_style(c, s: const Style&)
fn push_style(c, s) -> bool fn pop_style(c) -> bool
fn style_default(c) fn style_from_table(c, table: const Color[:]) // one per COLOR_*
fn default_color_table() -> Color[>..] fn style_get_color_by_name(color: i64) -> u8[>..]
fn style_set_font(c, f: Font) fn style_push_font(c, f) -> bool fn style_pop_font(c) -> bool
fn style_load_cursor(c, which: i64, cursor: Cursor) // CURSOR_*
fn style_load_all_cursors(c, cursors: const Cursor[:]) // one per CURSOR_*
fn style_set_cursor(c, which: i64) -> bool fn style_show_cursor(c) fn style_hide_cursor(c)
fn style_item_color(color) -> StyleItem // and _image, _nine_slice, _hide
fn rgb(r, g, b: i64) -> Color // and rgba, rgba_u32, rgb_f, rgba_f, rgb_cf, rgb_hex, hsv, hsva, ...
fn color_cf(c: Color) -> Colorf // and color_u32, color_hex_rgba, color_hsv_f, color_hsva_f
fn rgb_factor(c: Color, factor: f32) -> ColorStyle mirrors Nuklear's nk_style field for
field (text, link, buttons, toggles, selectables, sliders, knobs,
progress bars, properties, edits, charts, scroll bars, tabs, combo
boxes, windows), except for its font and cursors. To change the style,
read it, change fields, and set it back. A StyleItem is a
color, an image or a nine-slice image, by kind. Software
cursors are images Nuklear draws at the mouse.
Images and rectangles
struct Rect { x: f32, y: f32, w: f32, h: f32 }
struct Image { texture: u32, w: u16, h: u16, region: u16[4] } // a renderer's texture
fn rect(x, y, w, h) -> Rect // f32 or i64; and recta, rect_pos, rect_size, get_null_rect
fn image_id(texture: u32) -> Image fn texture_image(t: gfx::Texture) -> Image
fn subimage_id(texture, w: u16, h: u16, region: Rect) -> Image // part of a w x h texture
fn nine_slice_id(texture, l, t, r, b: u16) -> NineSlice // and sub9slice_id, with the sizeDrawing and what was drawn
fn stroke_line(c, x0, y0, x1, y1: f32, thickness: f32, color: Color)
// and stroke_curve, _rect, _circle, _arc, _triangle, _polyline, _polygon
fn fill_rect(c, r: Rect, rounding: f32, color)
// and fill_rect_multi_color, _circle, _arc, _triangle, _polygon
fn draw_image(c, r: Rect, img: Image, color) fn draw_nine_slice(c, r, slice: NineSlice, color)
fn draw_text(c, r: Rect, text, font: Font, background: Color, foreground: Color)
fn push_scissor(c, r: Rect)
fn commands(c) -> Command[>..] fn command_points(c) -> int2[>..] fn command_text(c) -> u8[>..]
fn convert(c, config: ConvertConfig) -> i64 // CONVERT_SUCCESS or what went wrong
fn vertices(c) -> Vertex[>..] fn indices(c) -> u32[>..] fn draw_commands(c) -> DrawCommand[>..]
fn null_texture(c) -> u32The canvas functions draw onto the window being built, in screen
coordinates, clipped to the scissor. Together with widget,
they are the way to draw a widget of the program's own. Once a frame's
windows have ended, its drawing is available to any renderer in two
forms:
- Nuklear's commands: one
Commandper shape, image or text, with the points of polygons and the bytes of texts stored beside them; - triangles from
convert: vertices with a position, texture coordinate and color, 32-bit indices, and draw calls that each have a scissor and a texture (shapes use the null texture).
Both stay available until the next frame's input.
Through gfx
fn render(c: Context) // over the screen
fn render(c, target: gfx::Texture) // over a COLOR_TARGET texture
fn render(c, target, config: ConvertConfig)
fn input_from_gfx(c: Context) // after gfx::frame(), before any windowrender converts the frame and draws it with alpha
blending; it is called outside a pass. It uploads each font atlas to a
gfx texture the first time, and releases that texture with the atlas
(released_textures() lists the textures destroyed this way,
for a renderer of the program's own). The resources it keeps (a
pipeline, a sampler and buffers) are made once, and again after gfx is
closed and opened. input_from_gfx passes gfx's last frame
of input to the context, in order:
- the mouse;
- editing keys (Delete, Backspace, Enter, Tab, Home, End, the arrows, Page Up and Down, Escape);
- shortcuts with Ctrl or Command (A, C, X, V, Z, Y);
- typed text (it turns gfx's text input on);
- the frame's duration;
- the clipboard in both directions, pasting the system clipboard's text and putting there what a field copied.
render and input_from_gfx both work in the
screen's pixels, so on a high-density display the ui has as many pixels
as elsewhere, and so looks smaller, until the program sets a scale.
Goose