feat(lang): L7 memory is safe unless it says unsafe

The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.

What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 12:53:27 +03:00
parent 9259808f80
commit b0b0b62bce
70 changed files with 69189 additions and 64569 deletions

View file

@ -215,6 +215,36 @@ way in its own files. What is still built into the compiler is the namespaces th
`Math`, `Text`, `List`, `Vector`, `Color`, `Time`, `Date` - and the few methods that choose their `Math`, `Text`, `List`, `Vector`, `Color`, `Time`, `Date` - and the few methods that choose their
target by an argument's type (`Audio.play` of a handle or a name). target by an argument's type (`Audio.play` of a handle or a name).
### Memory is safe unless it says `unsafe`
The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and
`pointers(n)` make `n` zeroed elements of a `[]int`, `[]float`, `[]fixed`, `[]double` or
`[]pointer`, and the type names `words`, `floats` and the rest mean those slices. Every index is
checked against the length, so running off the end stops the program at that line instead of
writing into whatever lies next. `buffer(n)` is `n` zeroed bytes, a `[]byte`; `text_of(b, n)` makes
text of the first `n`; `Fs.read_bytes(path)` and `Fs.write_bytes(path, b, n)` move them to and from
a file; `view(xs, start, count)` is part of a slice sharing its storage, checked once when it is
made (make one where the buffer is made - each is a small allocation).
What is left is raw memory, and is refused outside `unsafe { }` or an `unsafe function`:
`bytes(n)`, indexing a `pointer` or `bytes`, `free`, `resize`, `Memory.*`, `file_read` and
`file_write`, `data_of(xs)` (a slice's first element, for C), and calling an `extern` C function.
A slice passed to an extern goes as its elements' address, never its header.
`unsafe` itself is for the platform: the runtime, a package from the toolchain or
`ludic_modules`, and what those import from beside them, all of which are unsafe throughout. A
project's own files may write it only when the build says `--unsafe` (`ludic build --unsafe`) -
the compiler and its tools build that way; a game is written against APIs and does not.
```ludic
# doc-check: skip — a fragment
let px = buffer(w * h * 3) # a []byte: bounds-checked
px[0] = 255
Fs.write_bytes("shot.raw", px, len(px))
let hp = floats(3) # a []float
hp[2] = 1.5
```
## Models (entity kinds) ## Models (entity kinds)
An `model` names a *kind* of entity and the fixed set of properties it An `model` names a *kind* of entity and the fixed set of properties it

14
changes/unsafe.md Normal file
View file

@ -0,0 +1,14 @@
bump: minor
type: feature
**Memory is safe unless it says `unsafe` (L7).** `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)`
and `pointers(n)` are bounds-checked, zero-filled slices now, and the type names mean those slices;
`buffer(n)` is a `[]byte`, with `text_of`, `Fs.read_bytes`, `Fs.write_bytes` and `view(xs, start,
count)` beside it. `bytes(n)`, indexing a raw pointer, `free`, `resize`, `Memory.*`, the raw file
calls, `data_of` and calling a C `extern` are refused outside `unsafe { }` / `unsafe function`, and
a project's own files may write `unsafe` only with `--unsafe`; the runtime and packages are the
platform and are unsafe throughout. A slice passed to an extern goes as its data.
Making the buffers slices found, and this release fixes: `Sync`'s atomics operating on a slice's
header rather than its cell; `words(n)` handing back uninitialised memory; `input.ludic` keeping a
stick's fixed axes in an int buffer; truetype's fixed outlines allocated as ints; skinning's float
matrices typed as ints. Rendering is byte-identical, and a frame costs the same.

View file

@ -7,55 +7,57 @@
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
program TiledP0 { program TiledP0 {
entry { entry {
# --- base64 decode: RFC 4648 test vectors (every padding remainder) --- unsafe { # raw buffers on purpose: built with --unsafe (L7)
if Base64.decode("") == "" { print(1) } # --- base64 decode: RFC 4648 test vectors (every padding remainder) ---
if Base64.decode("Zg==") == "f" { print(2) } if Base64.decode("") == "" { print(1) }
if Base64.decode("Zm8=") == "fo" { print(3) } if Base64.decode("Zg==") == "f" { print(2) }
if Base64.decode("Zm9v") == "foo" { print(4) } if Base64.decode("Zm8=") == "fo" { print(3) }
if Base64.decode("Zm9vYg==") == "foob" { print(5) } if Base64.decode("Zm9v") == "foo" { print(4) }
if Base64.decode("Zm9vYmE=") == "fooba" { print(6) } if Base64.decode("Zm9vYg==") == "foob" { print(5) }
if Base64.decode("Zm9vYmFy") == "foobar" { print(7) } if Base64.decode("Zm9vYmE=") == "fooba" { print(6) }
# decode ignores embedded whitespace/newlines (as TMX <data> carries) if Base64.decode("Zm9vYmFy") == "foobar" { print(7) }
if Base64.decode("Zm9v\n YmFy") == "foobar" { print(8) } # decode ignores embedded whitespace/newlines (as TMX <data> carries)
if Base64.decode("Zm9v\n YmFy") == "foobar" { print(8) }
# --- base64 encode round-trips --- # --- base64 encode round-trips ---
if Base64.encode("foobar") == "Zm9vYmFy" { print(9) } if Base64.encode("foobar") == "Zm9vYmFy" { print(9) }
if Base64.encode("f") == "Zg==" { print(10) } if Base64.encode("f") == "Zg==" { print(10) }
if Base64.decode(Base64.encode("the quick brown fox")) == "the quick brown fox" { print(11) } if Base64.decode(Base64.encode("the quick brown fox")) == "the quick brown fox" { print(11) }
# --- XML reader over a TSX element tree --- # --- XML reader over a TSX element tree ---
let tsx = "<?xml version='1.0' encoding='UTF-8'?>\n<!-- a comment -->\n<tileset version='1.10' name='sheet &amp; co' tilewidth='16' tileheight='16' tilecount='4' columns='2'>\n <image source='sheet.png' width='32' height='32'/>\n <tile id='0'><properties><property name='solid' type='bool' value='true'/></properties></tile>\n <data encoding='csv'>1,2,3,4</data>\n</tileset>\n" let tsx = "<?xml version='1.0' encoding='UTF-8'?>\n<!-- a comment -->\n<tileset version='1.10' name='sheet &amp; co' tilewidth='16' tileheight='16' tilecount='4' columns='2'>\n <image source='sheet.png' width='32' height='32'/>\n <tile id='0'><properties><property name='solid' type='bool' value='true'/></properties></tile>\n <data encoding='csv'>1,2,3,4</data>\n</tileset>\n"
let root = Xml.parse(tsx) let root = Xml.parse(tsx)
if Xml.tag(root) == "tileset" { print(12) } if Xml.tag(root) == "tileset" { print(12) }
if Xml.attr(root, "name") == "sheet & co" { print(13) } # entity decode if Xml.attr(root, "name") == "sheet & co" { print(13) } # entity decode
if Xml.attr_int(root, "tilewidth", 0) == 16 { print(14) } if Xml.attr_int(root, "tilewidth", 0) == 16 { print(14) }
if Xml.attr_int(root, "tilecount", 0) == 4 { print(15) } if Xml.attr_int(root, "tilecount", 0) == 4 { print(15) }
let img = Xml.find(root, "image") let img = Xml.find(root, "image")
if Xml.attr(img, "source") == "sheet.png" { print(16) } if Xml.attr(img, "source") == "sheet.png" { print(16) }
let tile = Xml.find(root, "tile") let tile = Xml.find(root, "tile")
let props = Xml.find(tile, "properties") let props = Xml.find(tile, "properties")
let prop = Xml.find(props, "property") let prop = Xml.find(props, "property")
if Xml.attr(prop, "value") == "true" { print(17) } if Xml.attr(prop, "value") == "true" { print(17) }
let data = Xml.find(root, "data") let data = Xml.find(root, "data")
if Xml.text(data) == "1,2,3,4" { print(18) } if Xml.text(data) == "1,2,3,4" { print(18) }
# --- gzip framing: base64 -> bytes -> gunzip round-trips --- # --- gzip framing: base64 -> bytes -> gunzip round-trips ---
let gz = "H4sIAAAAAAAC//NIzcnJ11EIycxJTVFIr8osUCjPL8pJUVQwMDQyNjE1M7ewVCjJSFUoLM1MzlZIKsovz1NIy6/QAwCLdXOzOAAAAA==" let gz = "H4sIAAAAAAAC//NIzcnJ11EIycxJTVFIr8osUCjPL8pJUVQwMDQyNjE1M7ewVCjJSFUoLM1MzlZIKsovz1NIy6/QAwCLdXOzOAAAAA=="
let comp = bytes(256) let comp = bytes(256)
let clen = b64_decode(gz, comp) let clen = b64_decode(gz, comp)
let out = bytes(256) let out = bytes(256)
let n = z_gunzip(comp, clen, out, 256) let n = z_gunzip(comp, clen, out, 256)
out[n] = 0 out[n] = 0
if n == 56 { print(19) } if n == 56 { print(19) }
if out == "Hello, Tiled gzip world! 0123456789 the quick brown fox." { print(20) } if out == "Hello, Tiled gzip world! 0123456789 the quick brown fox." { print(20) }
# --- zlib still round-trips through the same path (regression) --- # --- zlib still round-trips through the same path (regression) ---
let zl = "eJzzSM3JyddRCMnMSU1RSK/KLFAozy/KSVFUMDA0MjYxNTO3sFQoyUhVKCzNTM5WSCrKL89TSMuv0AMAA10SJA==" let zl = "eJzzSM3JyddRCMnMSU1RSK/KLFAozy/KSVFUMDA0MjYxNTO3sFQoyUhVKCzNTM5WSCrKL89TSMuv0AMAA10SJA=="
let zc = bytes(256) let zc = bytes(256)
let zclen = b64_decode(zl, zc) let zclen = b64_decode(zl, zc)
let zout = bytes(256) let zout = bytes(256)
let zn = z_uncompress(zc, zclen, zout, 256) let zn = z_uncompress(zc, zclen, zout, 256)
zout[zn] = 0 zout[zn] = 0
if zout == "Hello, Tiled gzip world! 0123456789 the quick brown fox." { print(21) } if zout == "Hello, Tiled gzip world! 0123456789 the quick brown fox." { print(21) }
}
} }
} }

View file

@ -6,11 +6,11 @@ program UdpDemo {
let pa = Udp.port(a) let pa = Udp.port(a)
let pb = Udp.port(b) let pb = Udp.port(b)
let lo = Udp.ip("127.0.0.1") let lo = Udp.ip("127.0.0.1")
let out = Memory.bytes(16) let out = buffer(16)
Memory.poke(out, 0, 104) out[0] = 104
Memory.poke(out, 1, 105) out[1] = 105
let sent = Udp.send(a, lo, pb, out, 2) let sent = Udp.send(a, lo, pb, out, 2)
let inb = Memory.bytes(64) let inb = buffer(64)
var got = 0 var got = 0
var tries = 0 var tries = 0
while got == 0 and tries < 200000 { got = Udp.recv(b, inb, 64); tries += 1 } while got == 0 and tries < 200000 { got = Udp.recv(b, inb, 64); tries += 1 }
@ -20,7 +20,7 @@ program UdpDemo {
tries = 0 tries = 0
while back == 0 and tries < 200000 { back = Udp.recv(a, out, 16); tries += 1 } while back == 0 and tries < 200000 { back = Udp.recv(a, out, 16); tries += 1 }
let bad = Udp.ip("1.2.3") == 0 and Udp.ip("300.1.1.1") == 0 let bad = Udp.ip("1.2.3") == 0 and Udp.ip("300.1.1.1") == 0
print(`{sent} {got} {Memory.peek(inb, 0)} {Memory.peek(inb, 1)} {from_ok} {back} {Udp.ip_text(lo)} {bad} {Udp.recv(a, out, 16)} {pa > 0}`) print(`{sent} {got} {inb[0]} {inb[1]} {from_ok} {back} {Udp.ip_text(lo)} {bad} {Udp.recv(a, out, 16)} {pa > 0}`)
Udp.close(a) Udp.close(a)
Udp.close(b) Udp.close(b)
} }

View file

@ -8,17 +8,19 @@
# them back through the loopback: prints 4, then 10 20 30 42. # them back through the loopback: prints 4, then 10 20 30 42.
program NetEcho { program NetEcho {
entry { entry {
let out = bytes(4) unsafe { # raw buffers on purpose: built with --unsafe (L7)
out[0] = '\n' let out = bytes(4)
out[1] = 20 out[0] = '\n'
out[2] = 30 out[1] = 20
out[3] = '*' out[2] = 30
net_send(0, out, 4) # onto the wire (the built-in loopback) out[3] = '*'
net_send(0, out, 4) # onto the wire (the built-in loopback)
let inb = bytes(64) let inb = bytes(64)
let n = net_poll(inb, 64) # take the next datagram back off let n = net_poll(inb, 64) # take the next datagram back off
print(n) # 4 print(n) # 4
var i = 0 var i = 0
while i < n { print(inb[i]); i += 1 } # 10 20 30 42 while i < n { print(inb[i]); i += 1 } # 10 20 30 42
}
} }
} }

View file

@ -15,15 +15,17 @@ program NetSnapshot {
model Unit { Health } model Unit { Health }
entry { entry {
spawn Unit { Health { hp: 50, max: 100 } } unsafe { # raw buffers on purpose: built with --unsafe (L7)
let buf = bytes(world_size()) spawn Unit { Health { hp: 50, max: 100 } }
for (Health) in query [Health, {Unit}] { let buf = bytes(world_size())
print(Health.hp) # 50 for (Health) in query [Health, {Unit}] {
let n = world_save(buf) # snapshot the whole world print(Health.hp) # 50
Health.hp = 7 let n = world_save(buf) # snapshot the whole world
print(Health.hp) # 7 Health.hp = 7
world_load(buf, n) # roll the world back print(Health.hp) # 7
print(Health.hp) # 50 — restored from bytes world_load(buf, n) # roll the world back
print(Health.hp) # 50 — restored from bytes
}
} }
} }
} }

View file

@ -22,15 +22,15 @@ program VkCompute {
var dev: pointer = null var dev: pointer = null
function fail(what: string, r: int) -> void { print(`vulkan: {what} failed, VkResult {r}`); quit() } unsafe function fail(what: string, r: int) -> void { print(`vulkan: {what} failed, VkResult {r}`); quit() }
function handle(out: bytes) -> long { return vk_get_i64(out, 0) } unsafe function handle(out: bytes) -> long { return vk_get_i64(out, 0) }
function has_ext(props: bytes, n: int, want: string) -> bool { unsafe function has_ext(props: bytes, n: int, want: string) -> bool {
for i in 0 .. n { for i in 0 .. n {
if string(vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)) == want { return true } if string(vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)) == want { return true }
} }
return false return false
} }
function read_all(path: string) -> bytes { unsafe function read_all(path: string) -> bytes {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if f == null { return null } if f == null { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)

View file

@ -16,11 +16,11 @@ program VkProbe {
const API_1_0: int = 4194304 # VK_MAKE_API_VERSION(0, 1, 0, 0) const API_1_0: int = 4194304 # VK_MAKE_API_VERSION(0, 1, 0, 0)
function api_text(v: int) -> string { return `{(v >> 22) & 127}.{(v >> 12) & 1023}.{v & 4095}` } unsafe function api_text(v: int) -> string { return `{(v >> 22) & 127}.{(v >> 12) & 1023}.{v & 4095}` }
function yes(b: bool) -> string { if b { return "yes" }; return "no " } unsafe function yes(b: bool) -> string { if b { return "yes" }; return "no " }
# a name in an array of VkExtensionProperties # a name in an array of VkExtensionProperties
function has_ext(props: bytes, n: int, want: string) -> bool { unsafe function has_ext(props: bytes, n: int, want: string) -> bool {
for i in 0 .. n { for i in 0 .. n {
let at = vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName) let at = vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)
if string(at) == want { return true } if string(at) == want { return true }

View file

@ -14,7 +14,7 @@ program VkResources {
model Anchor { Marker } model Anchor { Marker }
var failures: int = 0 var failures: int = 0
function check(what: string, ok: bool) -> void { unsafe function check(what: string, ok: bool) -> void {
if ok { print(`vulkan: {what} ok`) } else { print(`vulkan: {what} FAILED`); failures += 1 } if ok { print(`vulkan: {what} ok`) } else { print(`vulkan: {what} FAILED`); failures += 1 }
} }

View file

@ -21,16 +21,16 @@ program VkTriangle {
const H: int = 240 const H: int = 240
const FMT: int = VK_FORMAT_R8G8B8A8_UNORM const FMT: int = VK_FORMAT_R8G8B8A8_UNORM
function fail(what: string, r: int) -> void { print(`vulkan: {what} failed, VkResult {r}`); quit() } unsafe function fail(what: string, r: int) -> void { print(`vulkan: {what} failed, VkResult {r}`); quit() }
function handle(out: bytes) -> long { return vk_get_i64(out, 0) } unsafe function handle(out: bytes) -> long { return vk_get_i64(out, 0) }
function has_ext(props: bytes, n: int, want: string) -> bool { unsafe function has_ext(props: bytes, n: int, want: string) -> bool {
for i in 0 .. n { for i in 0 .. n {
if string(vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)) == want { return true } if string(vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)) == want { return true }
} }
return false return false
} }
var spv_len: int = 0 var spv_len: int = 0
function read_all(path: string) -> bytes { unsafe function read_all(path: string) -> bytes {
let f = file_open(path, "rb") let f = file_open(path, "rb")
if f == null { return null } if f == null { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)
@ -43,7 +43,7 @@ program VkTriangle {
return b return b
} }
# the first memory type the resource allows that has every property wanted # the first memory type the resource allows that has every property wanted
function mem_type(mp: bytes, allowed: int, want: int) -> int { unsafe function mem_type(mp: bytes, allowed: int, want: int) -> int {
for t in 0 .. vk_get_i32(mp, VkPhysicalDeviceMemoryProperties_memoryTypeCount) { for t in 0 .. vk_get_i32(mp, VkPhysicalDeviceMemoryProperties_memoryTypeCount) {
let pf = vk_get_i32(mp, VkPhysicalDeviceMemoryProperties_memoryTypes + t * VkMemoryType_sizeof + VkMemoryType_propertyFlags) let pf = vk_get_i32(mp, VkPhysicalDeviceMemoryProperties_memoryTypes + t * VkMemoryType_sizeof + VkMemoryType_propertyFlags)
if ((allowed >> t) & 1) == 1 and (pf & want) == want { return t } if ((allowed >> t) & 1) == 1 and (pf & want) == want { return t }
@ -51,14 +51,14 @@ program VkTriangle {
return -1 return -1
} }
# one colour image, one mip, one layer # one colour image, one mip, one layer
function color_range(at: bytes, off: int) -> void { unsafe function color_range(at: bytes, off: int) -> void {
vk_put_i32(at, off + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) vk_put_i32(at, off + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
vk_put_i32(at, off + VkImageSubresourceRange_baseMipLevel, 0) vk_put_i32(at, off + VkImageSubresourceRange_baseMipLevel, 0)
vk_put_i32(at, off + VkImageSubresourceRange_levelCount, 1) vk_put_i32(at, off + VkImageSubresourceRange_levelCount, 1)
vk_put_i32(at, off + VkImageSubresourceRange_baseArrayLayer, 0) vk_put_i32(at, off + VkImageSubresourceRange_baseArrayLayer, 0)
vk_put_i32(at, off + VkImageSubresourceRange_layerCount, 1) vk_put_i32(at, off + VkImageSubresourceRange_layerCount, 1)
} }
function barrier(cb: pointer, image: long, old_layout: int, new_layout: int, src_access: int, dst_access: int, src_stage: int, dst_stage: int) -> void { unsafe function barrier(cb: pointer, image: long, old_layout: int, new_layout: int, src_access: int, dst_access: int, src_stage: int, dst_stage: int) -> void {
let b = bytes(VkImageMemoryBarrier_sizeof) let b = bytes(VkImageMemoryBarrier_sizeof)
vk_zero(b, VkImageMemoryBarrier_sizeof) vk_zero(b, VkImageMemoryBarrier_sizeof)
vk_put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER) vk_put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER)
@ -72,7 +72,7 @@ program VkTriangle {
color_range(b, VkImageMemoryBarrier_subresourceRange) color_range(b, VkImageMemoryBarrier_subresourceRange)
vk_cmd_pipeline_barrier(cb, src_stage, dst_stage, 0, 0, null, 0, null, 1, b) vk_cmd_pipeline_barrier(cb, src_stage, dst_stage, 0, 0, null, 0, null, 1, b)
} }
function shader_module(dev: pointer, path: string) -> long { unsafe function shader_module(dev: pointer, path: string) -> long {
let spv = read_all(path) let spv = read_all(path)
if spv == null { print(`vulkan: no SPIR-V at {path} (compile vk_triangle.slang with slangc)`); quit() } if spv == null { print(`vulkan: no SPIR-V at {path} (compile vk_triangle.slang with slangc)`); quit() }
let smci = bytes(VkShaderModuleCreateInfo_sizeof) let smci = bytes(VkShaderModuleCreateInfo_sizeof)

View file

@ -17,7 +17,7 @@ var prefs_n: int = 0
namespace Prefs { namespace Prefs {
internal function init() -> void { internal function init() -> void {
if prefs_names != null { return } if prefs_names != null { return }
prefs_names = bytes(PREFS_MAX * 8) prefs_names = pointers(PREFS_MAX)
prefs_vals = words(PREFS_MAX) prefs_vals = words(PREFS_MAX)
prefs_n = 0 prefs_n = 0
} }

View file

@ -19,6 +19,17 @@ const F_HALF: int = 0x3F000000
const PI: float = 3.1415927 const PI: float = 3.1415927
const F_PI: int = 0x40490FDB const F_PI: int = 0x40490FDB
# the n 4x4 matrices laid end to end in `m`, a view of each. Views are made once, where their
# buffer is: a view is a small allocation, and one per matrix per frame is one nobody frees.
function m4_views(m: floats, n: int) -> [][]float {
let out = new [][]float
var i = 0
while i < n {
push(out, view(m, i * 16, 16))
i += 1
}
return out
}
function fl(x: fixed) -> float { return float(x) } function fl(x: fixed) -> float { return float(x) }
function fi(n: int) -> float { return float(n) } function fi(n: int) -> float { return float(n) }
function fr(n: int, d: int) -> float { return float(n) / float(d) } function fr(n: int, d: int) -> float { return float(n) / float(d) }

View file

@ -241,19 +241,19 @@ function gpu_screenshot(path: string) -> bool { if gpu_kind == GPU_VK { return g
var gpu_u_tmp: words = null var gpu_u_tmp: words = null
function gpu_tmp() -> words { if gpu_u_tmp == null { gpu_u_tmp = words(4) }; return gpu_u_tmp } function gpu_tmp() -> words { if gpu_u_tmp == null { gpu_u_tmp = words(4) }; return gpu_u_tmp }
# float bits (IEEE singles in an int), like every other number in the renderer # float bits (IEEE singles in an int), like every other number in the renderer
function u_f(loc: int, v: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(v); gvk_u_set(loc, t, 4, 1); return }; let t = gpu_tmp(); t[0] = float_bits(v); gl_uniform1fv(loc, 1, t) } function u_f(loc: int, v: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(v); gvk_u_set(loc, data_of(t), 4, 1); return }; let t = gpu_tmp(); t[0] = float_bits(v); gl_uniform1fv(loc, 1, t) }
function u_f2(loc: int, x: float, y: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gvk_u_set(loc, t, 8, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gl_uniform2fv(loc, 1, t) } function u_f2(loc: int, x: float, y: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gvk_u_set(loc, data_of(t), 8, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gl_uniform2fv(loc, 1, t) }
function u_f3(loc: int, x: float, y: float, z: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gvk_u_set(loc, t, 12, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gl_uniform3fv(loc, 1, t) } function u_f3(loc: int, x: float, y: float, z: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gvk_u_set(loc, data_of(t), 12, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gl_uniform3fv(loc, 1, t) }
function u_f4(loc: int, x: float, y: float, z: float, w: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gvk_u_set(loc, t, 16, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gl_uniform4fv(loc, 1, t) } function u_f4(loc: int, x: float, y: float, z: float, w: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gvk_u_set(loc, data_of(t), 16, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gl_uniform4fv(loc, 1, t) }
function u_v3(loc: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 12, 1); return }; gl_uniform3fv(loc, 1, v) } function u_v3(loc: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(v), 12, 1); return }; gl_uniform3fv(loc, 1, v) }
function u_fv(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 4, n); return }; gl_uniform1fv(loc, n, v) } function u_fv(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(v), 4, n); return }; gl_uniform1fv(loc, n, v) }
# n vec4s from 4n float bits. Not u_fv with 4n: on Vulkan an array element is copied at the size # n vec4s from 4n float bits. Not u_fv with 4n: on Vulkan an array element is copied at the size
# given and placed at the array's stride, so a vec4 array fed floats got one float per element - # given and placed at the array's stride, so a vec4 array fed floats got one float per element -
# which drew the chunked grass with every tile at a nonsense corner and zero blades a cell. # which drew the chunked grass with every tile at a nonsense corner and zero blades a cell.
function u_f4v(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 16, n); return }; gl_uniform4fv(loc, n, v) } function u_f4v(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(v), 16, n); return }; gl_uniform4fv(loc, n, v) }
function u_mat4(loc: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, m, 64, 1); return }; gl_uniform_matrix4fv(loc, 1, 0, m) } function u_mat4(loc: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(m), 64, 1); return }; gl_uniform_matrix4fv(loc, 1, 0, m) }
function u_mat4n(loc: int, n: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, m, 64, n); return }; gl_uniform_matrix4fv(loc, n, 0, m) } function u_mat4n(loc: int, n: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(m), 64, n); return }; gl_uniform_matrix4fv(loc, n, 0, m) }
function u_i(loc: int, v: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = v; gvk_u_set(loc, t, 4, 1); return }; gl_uniform1i(loc, v) } function u_i(loc: int, v: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = v; gvk_u_set(loc, data_of(t), 4, 1); return }; gl_uniform1i(loc, v) }
# ---- what this machine can do ---------------------------------------------------- # ---- what this machine can do ----------------------------------------------------
# The advanced graphics features are Windows features: the Vulkan renderer, ray tracing, # The advanced graphics features are Windows features: the Vulkan renderer, ray tracing,

View file

@ -105,7 +105,7 @@ function grass_blade_mesh(rows: int) -> Mesh {
idx[q * 6] = b; idx[q * 6 + 1] = b + 1; idx[q * 6 + 2] = b + 2 idx[q * 6] = b; idx[q * 6 + 1] = b + 1; idx[q * 6 + 2] = b + 2
idx[q * 6 + 3] = b + 1; idx[q * 6 + 4] = b + 3; idx[q * 6 + 5] = b + 2 idx[q * 6 + 3] = b + 1; idx[q * 6 + 4] = b + 3; idx[q * 6 + 5] = b + 2
} }
gpu_mesh_indices(m, idx, nq * 6 * 4, 4) gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4)
free(idx) free(idx)
m.count = nq * 6 m.count = nq * 6
gpu_mesh_done(m) gpu_mesh_done(m)
@ -270,7 +270,7 @@ function grass_flush() -> void {
if grass_n == 0 { return } if grass_n == 0 { return }
var p = grass_prog var p = grass_prog
let mesh = grass_mesh_live() let mesh = grass_mesh_live()
if mesh { p = grass_mesh_prog } else { gpu_buffer_upload(grass_cmds, grass_n * 20, grass_rec, GPU_DYNAMIC) } if mesh { p = grass_mesh_prog } else { gpu_buffer_upload(grass_cmds, grass_n * 20, data_of(grass_rec), GPU_DYNAMIC) }
for b in 0 .. grass_band_n { for b in 0 .. grass_band_n {
let s = grass_band_start[b] let s = grass_band_start[b]
var e = grass_n var e = grass_n

View file

@ -52,7 +52,7 @@ function mesh_grid(n: int, half: float) -> Mesh {
k += 6 k += 6
} }
} }
gpu_mesh_indices(m, idx, ni * 4, 4) gpu_mesh_indices(m, data_of(idx), ni * 4, 4)
free(idx) free(idx)
m.count = ni m.count = ni
gpu_mesh_done(m) gpu_mesh_done(m)
@ -81,7 +81,7 @@ function mesh_card() -> Mesh {
free(v) free(v)
let idx = words(6) let idx = words(6)
idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3 idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3
gpu_mesh_indices(m, idx, 24, 4) gpu_mesh_indices(m, data_of(idx), 24, 4)
free(idx) free(idx)
m.count = 6 m.count = 6
gpu_mesh_done(m) gpu_mesh_done(m)

View file

@ -58,9 +58,17 @@ function q_rotate(o: floats, q: floats, v: floats) -> void {
} }
# pitch about X, yaw about Y, roll about Z, composed as yaw * pitch * roll # pitch about X, yaw about Y, roll about Z, composed as yaw * pitch * roll
var q_scratch: floats = null var q_scratch: floats = null
var q_sy: floats = null # views of q_scratch's second, third and fourth
var q_sz: floats = null
var q_st: floats = null
function q_euler(o: floats, pitch: float, yaw: float, roll: float) -> void { function q_euler(o: floats, pitch: float, yaw: float, roll: float) -> void {
if q_scratch == null { q_scratch = floats(16) } if q_scratch == null {
let qx = q_scratch; let qy = mem_off(q_scratch, 16); let qz = mem_off(q_scratch, 32); let t = mem_off(q_scratch, 48) q_scratch = floats(16)
q_sy = view(q_scratch, 4, 4)
q_sz = view(q_scratch, 8, 4)
q_st = view(q_scratch, 12, 4)
}
let qx = q_scratch; let qy = q_sy; let qz = q_sz; let t = q_st
q_axis_angle(qx, 1.0, 0.0, 0.0, pitch) q_axis_angle(qx, 1.0, 0.0, 0.0, pitch)
q_axis_angle(qy, 0.0, 1.0, 0.0, yaw) q_axis_angle(qy, 0.0, 1.0, 0.0, yaw)
q_axis_angle(qz, 0.0, 0.0, 1.0, roll) q_axis_angle(qz, 0.0, 0.0, 1.0, roll)

View file

@ -35,3 +35,4 @@ import "grass.ludic"
import "water.ludic" import "water.ludic"
import "streamline.ludic" import "streamline.ludic"
import "render.ludic" import "render.ludic"
import "safe_api.ludic"

View file

@ -0,0 +1,41 @@
# safe_api.ludic — the renderer's raw buffers, as a program may hold them (L7). The renderer reads
# glTF accessors, the screen and PNG files into raw memory, which is its business; a game gets the
# same data as slices, bounds-checked, and never frees anything. Each of these copies or hands over
# at the boundary, so nothing raw crosses it.
# an accessor's float components (gltf_count elements of gltf_comps floats), as floats
function gltf_accessor_floats(idx: int) -> floats {
let p = gltf_accessor(idx)
let n = gltf_count * gltf_comps
let out = floats(n)
for i in 0 .. n { out[i] = float_from_bits(mem_get_f32_bits(p, i)) }
free(p)
return out
}
# how many elements an accessor holds, without reading them
function gltf_accessor_count(idx: int) -> int {
let acc = value_at(value_get(gltf_doc, "accessors"), idx)
return jint(acc, "count", 0)
}
# the presented frame as RGB8, bottom row first, into `out` (at least w * h * 3 bytes)
function gpu_read_screen_bytes(w: int, h: int, out: []byte) -> bool {
if out == null or len(out) < w * h * 3 { return false }
gpu_read_screen(w, h, data_of(out))
return true
}
# a PNG's samples (tex_w x tex_h x tex_channels, 8 or 16 bits): into `reuse` when it is large
# enough - a map swap decodes the same size again - else into a new buffer. null if unreadable.
function png_decode_bytes(path: string, reuse: []byte) -> []byte {
let p = png_decode(path)
if p == null { return null }
let n = tex_w * tex_h * tex_channels * (tex_depth / 8)
var out = reuse
if out == null or len(out) < n { out = buffer(n) }
for i in 0 .. n { out[i] = p[i] }
free(p)
return out
}
# upload samples laid out as the last decode left them (tex_w, tex_h, tex_channels, tex_depth)
function tex_upload_bytes(px: []byte, srgb: bool, mips: bool) -> int {
return tex_upload(data_of(px), srgb, mips)
}

View file

@ -116,7 +116,7 @@ function model_cross_card() -> Model {
let idx = words(12) let idx = words(12)
idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3 idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3
idx[6] = 4; idx[7] = 5; idx[8] = 6; idx[9] = 4; idx[10] = 6; idx[11] = 7 idx[6] = 4; idx[7] = 5; idx[8] = 6; idx[9] = 4; idx[10] = 6; idx[11] = 7
gpu_mesh_indices(m, idx, 48, 4) gpu_mesh_indices(m, data_of(idx), 48, 4)
free(idx) free(idx)
m.count = 12 m.count = 12
gpu_mesh_done(m) gpu_mesh_done(m)
@ -194,7 +194,7 @@ function model_lupine() -> Model {
gpu_mesh_vertices(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC) gpu_mesh_vertices(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC)
sc_model_layout(m) sc_model_layout(m)
free(v) free(v)
gpu_mesh_indices(m, idx, nq * 6 * 4, 4) gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4)
free(idx) free(idx)
m.count = nq * 6 m.count = nq * 6
gpu_mesh_done(m) gpu_mesh_done(m)
@ -267,7 +267,7 @@ function model_lupine_dense() -> Model {
gpu_mesh_vertices(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC) gpu_mesh_vertices(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC)
sc_model_layout(m) sc_model_layout(m)
free(v) free(v)
gpu_mesh_indices(m, idx, nq * 6 * 4, 4) gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4)
free(idx) free(idx)
m.count = nq * 6 m.count = nq * 6
gpu_mesh_done(m) gpu_mesh_done(m)
@ -315,7 +315,7 @@ function model_blade() -> Model {
idx[k + 3] = a + 1; idx[k + 4] = a + 3; idx[k + 5] = a + 2 idx[k + 3] = a + 1; idx[k + 4] = a + 3; idx[k + 5] = a + 2
k += 6 k += 6
} }
gpu_mesh_indices(m, idx, ni * 4, 4) gpu_mesh_indices(m, data_of(idx), ni * 4, 4)
free(idx) free(idx)
m.count = ni m.count = ni
gpu_mesh_done(m) gpu_mesh_done(m)
@ -667,7 +667,7 @@ function layer_gpu_prepare(l: Layer) -> bool {
} }
let n = l.n_lods let n = l.n_lods
let cap = l.count let cap = l.count
gpu_buffer_upload(l.g_src, cap * INST_FLOATS * 4, l.inst, GPU_STATIC) gpu_buffer_upload(l.g_src, cap * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
gpu_buffer_upload(l.g_dst, (n + 1) * cap * INST_FLOATS * 4, null, GPU_DYNAMIC) gpu_buffer_upload(l.g_dst, (n + 1) * cap * INST_FLOATS * 4, null, GPU_DYNAMIC)
if l.g_arena == null { layer_arena_build(l) } if l.g_arena == null { layer_arena_build(l) }
let n_mat = len(l.g_arena) let n_mat = len(l.g_arena)
@ -690,14 +690,14 @@ function layer_gpu_prepare(l: Layer) -> bool {
} }
} }
} }
gpu_buffer_upload(l.g_cmds, SC_RECS * SC_REC_W, rec, GPU_DYNAMIC) gpu_buffer_upload(l.g_cmds, SC_RECS * SC_REC_W, data_of(rec), GPU_DYNAMIC)
let zeros = words(5) let zeros = words(5)
for i in 0 .. 5 { zeros[i] = 0 } for i in 0 .. 5 { zeros[i] = 0 }
gpu_buffer_upload(l.g_counts, 20, zeros, GPU_DYNAMIC) gpu_buffer_upload(l.g_counts, 20, data_of(zeros), GPU_DYNAMIC)
free(rec); free(zeros) free(rec); free(zeros)
# the card casts every instance, as on the CPU path (layer_grid_build uploads this there) # the card casts every instance, as on the CPU path (layer_grid_build uploads this there)
l.n_sh = l.count l.n_sh = l.count
gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, l.inst, GPU_STATIC) gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
l.g_n = l.count l.g_n = l.count
l.g_on = true l.g_on = true
return true return true
@ -715,7 +715,7 @@ function layer_gpu_cull(l: Layer) -> void {
pr[32] = float_bits(l.lods[0].height * 2.0); pr[33] = float_bits(4.0) pr[32] = float_bits(l.lods[0].height * 2.0); pr[33] = float_bits(4.0)
let bufs = words(4) let bufs = words(4)
bufs[0] = l.g_src; bufs[1] = l.g_dst; bufs[2] = l.g_cmds; bufs[3] = l.g_counts bufs[0] = l.g_src; bufs[1] = l.g_dst; bufs[2] = l.g_cmds; bufs[3] = l.g_counts
gpu_dispatch(sc_cull_prog, pr, 144, bufs, 1) gpu_dispatch(sc_cull_prog, data_of(pr), 144, bufs, 1)
free(pr); free(bufs) free(pr); free(bufs)
} }
@ -762,7 +762,7 @@ function layer_grid_build(l: Layer, cell: float) -> void {
free(cellof); free(fill) free(cellof); free(fill)
if l.vis == null { l.vis = floats(l.cap * INST_FLOATS) } if l.vis == null { l.vis = floats(l.cap * INST_FLOATS) }
l.n_sh = l.count l.n_sh = l.count
gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, l.inst, GPU_STATIC) gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
} }
# gather the instances of the cells the camera can see (and that are within cull) # gather the instances of the cells the camera can see (and that are within cull)
@ -848,7 +848,7 @@ function layer_partition_lods(l: Layer, src: floats, total: int) -> void {
# casters: the whole (gathered) set from the shadow buffer, unless the impostor casts # casters: the whole (gathered) set from the shadow buffer, unless the impostor casts
if l.gcell == 0.0 { if l.gcell == 0.0 {
l.n_sh = total l.n_sh = total
if total > 0 { gpu_buffer_upload(l.sh_buf, total * INST_FLOATS * 4, src, GPU_DYNAMIC) } if total > 0 { gpu_buffer_upload(l.sh_buf, total * INST_FLOATS * 4, data_of(src), GPU_DYNAMIC) }
} }
free(counts); free(start); free(fill) free(counts); free(start); free(fill)
} }
@ -866,7 +866,7 @@ function layer_update(l: Layer) -> void {
# per-instance loop — one upload, and the same buffer casts its shadows. # per-instance loop — one upload, and the same buffer casts its shadows.
if l.streamed and l.imp == null and l.near == 0.0 and l.n_lods <= 1 { if l.streamed and l.imp == null and l.near == 0.0 and l.n_lods <= 1 {
l.n_near = l.count; l.n_far = 0; l.n_sh = l.count l.n_near = l.count; l.n_far = 0; l.n_sh = l.count
gpu_buffer_upload(l.buf, l.count * INST_FLOATS * 4, l.inst, GPU_DYNAMIC) gpu_buffer_upload(l.buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC)
prof_layer_add(gl_now_us() - t_lu, l.count * INST_FLOATS * 4) prof_layer_add(gl_now_us() - t_lu, l.count * INST_FLOATS * 4)
return return
} }
@ -916,10 +916,10 @@ function layer_update(l: Layer) -> void {
if l.gcell == 0.0 { if l.gcell == 0.0 {
l.n_sh = l.count l.n_sh = l.count
if l.count > 0 { if l.count > 0 {
gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, l.inst, GPU_DYNAMIC) gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC)
} }
} }
gpu_buffer_upload(l.buf, nn * INST_FLOATS * 4, tmp, GPU_DYNAMIC) gpu_buffer_upload(l.buf, nn * INST_FLOATS * 4, data_of(tmp), GPU_DYNAMIC)
if nf > 0 { if nf > 0 {
gpu_buffer_upload(l.imp_buf, nf * INST_FLOATS * 4, mem_off(tmp, (far_off - nf * INST_FLOATS) * 4), GPU_DYNAMIC) gpu_buffer_upload(l.imp_buf, nf * INST_FLOATS * 4, mem_off(tmp, (far_off - nf * INST_FLOATS) * 4), GPU_DYNAMIC)
} }

View file

@ -11,7 +11,8 @@ const SHADOW_CASCADES: int = 5
var sh_tex: int = 0 var sh_tex: int = 0
var sh_fbo: int = 0 var sh_fbo: int = 0
var sh_vp: floats = null # 4 x 16 float bits var sh_vp: floats = null # the cascades' view-projections, 16 floats each
var sh_vp_v: [][]float = null # a view of each
var sh_split: floats = null # view-space far distance of each cascade var sh_split: floats = null # view-space far distance of each cascade
var sh_range: floats = null # 4 light-frustum depth extents (metres) var sh_range: floats = null # 4 light-frustum depth extents (metres)
var sh_texel: floats = null # 4 shadow texel sizes (metres) var sh_texel: floats = null # 4 shadow texel sizes (metres)
@ -29,6 +30,7 @@ function shadow_init() -> void {
gpu_fb_no_color() gpu_fb_no_color()
gpu_fb_bind(0) gpu_fb_bind(0)
sh_vp = floats(16 * SHADOW_CASCADES) sh_vp = floats(16 * SHADOW_CASCADES)
sh_vp_v = m4_views(sh_vp, SHADOW_CASCADES)
sh_split = floats(SHADOW_CASCADES) sh_split = floats(SHADOW_CASCADES)
sh_range = floats(SHADOW_CASCADES) sh_range = floats(SHADOW_CASCADES)
sh_texel = floats(SHADOW_CASCADES) sh_texel = floats(SHADOW_CASCADES)
@ -141,7 +143,7 @@ function shadow_fit(c: int, near: float, far: float) -> void {
free(out); free(eye); free(up); free(center); free(corners) free(out); free(eye); free(up); free(center); free(corners)
} }
function shadow_cascade_vp(c: int) -> floats { return mem_off(sh_vp, c * 64) } function shadow_cascade_vp(c: int) -> floats { return sh_vp_v[c] }
# render every cascade; `draw` happens through terrain_draw_shadow + the scene's casters # render every cascade; `draw` happens through terrain_draw_shadow + the scene's casters
@ -211,7 +213,7 @@ function shadow_dump() -> void {
let buf = floats(n * SHADOW_CASCADES) let buf = floats(n * SHADOW_CASCADES)
gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex) gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE) gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE)
gpu_tex_read(GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, buf) gpu_tex_read(GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, data_of(buf))
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE) gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE)
if sh_probe_x != 0.0 { if sh_probe_x != 0.0 {
let vp = shadow_cascade_vp(2) let vp = shadow_cascade_vp(2)

View file

@ -26,11 +26,14 @@ property Skin {
names: []string, names: []string,
pose_r: floats, # 4 per node: the pose rotation, model frame pose_r: floats, # 4 per node: the pose rotation, model frame
pose_t: floats, # 3 per node: the pose offset, model frame (metres) pose_t: floats, # 3 per node: the pose offset, model frame (metres)
gmat: words, # 16 per node: global matrix this pose gmat: floats, # 16 per node: global matrix this pose
gmat_v: [][]float, # a view of each node's matrix in gmat
n_joints: int = 0, n_joints: int = 0,
joints: words, # node index per joint joints: words, # node index per joint
inv_bind: words, # 16 per joint inv_bind: floats, # 16 per joint
inv_v: [][]float, # a view of each joint's inverse bind matrix
bones: floats, # 16 per joint: what the vertex shader skins with bones: floats, # 16 per joint: what the vertex shader skins with
bones_v: [][]float, # a view of each joint's matrix in bones
tmp_l: floats, tmp_l: floats,
tmp_q: floats, tmp_q: floats,
tmp_a: floats, tmp_a: floats,
@ -76,7 +79,8 @@ function skin_load(idx: int) -> Skin {
sk.n_nodes = n sk.n_nodes = n
sk.par = words(n); sk.walk = words(n) sk.par = words(n); sk.walk = words(n)
sk.rest_t = floats(n * 3); sk.rest_r = floats(n * 4); sk.rest_s = floats(n * 3); sk.rest_g = floats(n * 4) sk.rest_t = floats(n * 3); sk.rest_r = floats(n * 4); sk.rest_s = floats(n * 3); sk.rest_g = floats(n * 4)
sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = words(n * 16) sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = floats(n * 16)
sk.gmat_v = m4_views(sk.gmat, n)
sk.names = new []string sk.names = new []string
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3) sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3)
for i in 0 .. n { sk.par[i] = -1 } for i in 0 .. n { sk.par[i] = -1 }
@ -123,15 +127,17 @@ function skin_load(idx: int) -> Skin {
if nj > SKIN_MAX_JOINTS { print(`skin: {nj} joints, only the first {SKIN_MAX_JOINTS} are used`); nj = SKIN_MAX_JOINTS } if nj > SKIN_MAX_JOINTS { print(`skin: {nj} joints, only the first {SKIN_MAX_JOINTS} are used`); nj = SKIN_MAX_JOINTS }
sk.n_joints = nj sk.n_joints = nj
sk.joints = words(nj) sk.joints = words(nj)
sk.inv_bind = words(nj * 16) sk.inv_bind = floats(nj * 16)
sk.inv_v = m4_views(sk.inv_bind, nj)
sk.bones = floats(nj * 16) sk.bones = floats(nj * 16)
sk.bones_v = m4_views(sk.bones, nj)
for j in 0 .. nj { sk.joints[j] = value_as_int(value_at(jl, j)) } for j in 0 .. nj { sk.joints[j] = value_as_int(value_at(jl, j)) }
if value_has(skv, "inverseBindMatrices") != 0 { if value_has(skv, "inverseBindMatrices") != 0 {
let ib = gltf_accessor(value_as_int(value_get(skv, "inverseBindMatrices"))) let ib = gltf_accessor(value_as_int(value_get(skv, "inverseBindMatrices")))
for i in 0 .. nj * 16 { sk.inv_bind[i] = mem_get_f32_bits(ib, i) } for i in 0 .. nj * 16 { sk.inv_bind[i] = float_from_bits(mem_get_f32_bits(ib, i)) }
free(ib) free(ib)
} else { } else {
for j in 0 .. nj { m4_identity(mem_off(sk.inv_bind, j * 64)) } for j in 0 .. nj { m4_identity(sk.inv_v[j]) }
} }
skin_reset(sk) skin_reset(sk)
skin_pose(sk) skin_pose(sk)
@ -144,7 +150,7 @@ function skin_find(sk: Skin, name: string) -> int {
print(`skin: no node {name}`) print(`skin: no node {name}`)
return -1 return -1
} }
function skin_mat(sk: Skin, node: int) -> floats { return mem_off(sk.gmat, node * 64) } function skin_mat(sk: Skin, node: int) -> floats { return sk.gmat_v[node] }
# back to the rest pose # back to the rest pose
function skin_reset(sk: Skin) -> void { function skin_reset(sk: Skin) -> void {
@ -192,7 +198,7 @@ function skin_pose(sk: Skin) -> void {
else { m4_copy(skin_mat(sk, i), sk.tmp_l) } else { m4_copy(skin_mat(sk, i), sk.tmp_l) }
} }
for j in 0 .. sk.n_joints { for j in 0 .. sk.n_joints {
m4_mul(mem_off(sk.bones, j * 64), skin_mat(sk, sk.joints[j]), mem_off(sk.inv_bind, j * 64)) m4_mul(sk.bones_v[j], skin_mat(sk, sk.joints[j]), sk.inv_v[j])
} }
} }
@ -209,10 +215,12 @@ function skin_clone(src: Skin) -> Skin {
sk.n_nodes = src.n_nodes; sk.par = src.par; sk.walk = src.walk sk.n_nodes = src.n_nodes; sk.par = src.par; sk.walk = src.walk
sk.rest_t = src.rest_t; sk.rest_r = src.rest_r; sk.rest_s = src.rest_s; sk.rest_g = src.rest_g sk.rest_t = src.rest_t; sk.rest_r = src.rest_r; sk.rest_s = src.rest_s; sk.rest_g = src.rest_g
sk.names = src.names sk.names = src.names
sk.n_joints = src.n_joints; sk.joints = src.joints; sk.inv_bind = src.inv_bind sk.n_joints = src.n_joints; sk.joints = src.joints; sk.inv_bind = src.inv_bind; sk.inv_v = src.inv_v
let n = src.n_nodes let n = src.n_nodes
sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = words(n * 16) sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = floats(n * 16)
sk.gmat_v = m4_views(sk.gmat, n)
sk.bones = floats(src.n_joints * 16) sk.bones = floats(src.n_joints * 16)
sk.bones_v = m4_views(sk.bones, src.n_joints)
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3) sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3)
skin_reset(sk) skin_reset(sk)
skin_pose(sk) skin_pose(sk)

View file

@ -261,7 +261,7 @@ function stream_update(s: Stream, cam_x: float, cam_z: float) -> void {
} }
if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(s, cx, cz, c) { if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(s, cx, cz, c) {
let tg = gl_now_us() let tg = gl_now_us()
mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), c.data, c.count * INST_FLOATS * 4) mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), data_of(c.data), c.count * INST_FLOATS * 4)
l.count += c.count l.count += c.count
stream_us_gather = stream_us_gather + (gl_now_us() - tg) stream_us_gather = stream_us_gather + (gl_now_us() - tg)
} }

View file

@ -270,7 +270,7 @@ function terrain_generate() -> void {
ter_heights = floats(TERRAIN_RES * TERRAIN_RES) ter_heights = floats(TERRAIN_RES * TERRAIN_RES)
gpu_tex_bind(GPU_TEX2D, ter_height_tex) gpu_tex_bind(GPU_TEX2D, ter_height_tex)
gpu_pixel_store(GL_PACK_ALIGNMENT, 4) gpu_pixel_store(GL_PACK_ALIGNMENT, 4)
gpu_tex_read(GPU_TEX2D, GL_RED, GL_FLOAT, ter_heights) gpu_tex_read(GPU_TEX2D, GL_RED, GL_FLOAT, data_of(ter_heights))
gpu_fb_bind(0) gpu_fb_bind(0)
gpu_fb_free(fbo) gpu_fb_free(fbo)
gpu_program_free(p) gpu_program_free(p)
@ -663,7 +663,7 @@ function cdlod_init() -> void {
k += 6 k += 6
} }
} }
gpu_mesh_indices(m, idx, ni * 4, 4) gpu_mesh_indices(m, data_of(idx), ni * 4, 4)
free(idx) free(idx)
m.count = ni m.count = ni
gpu_mesh_done(m) gpu_mesh_done(m)

View file

@ -389,7 +389,7 @@ function tex_load_hdr(path: pointer) -> int {
let id = gpu_tex_new() let id = gpu_tex_new()
gpu_tex_bind(GPU_TEX2D, id) gpu_tex_bind(GPU_TEX2D, id)
gpu_pixel_store(GL_UNPACK_ALIGNMENT, 4) gpu_pixel_store(GL_UNPACK_ALIGNMENT, 4)
gpu_tex_image2d(GL_RGB16F, tex_w, tex_h, GL_RGB, GL_FLOAT, px) gpu_tex_image2d(GL_RGB16F, tex_w, tex_h, GL_RGB, GL_FLOAT, data_of(px))
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT) gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
@ -417,7 +417,7 @@ function tex_max(tex: int, w: int, h: int, tag: pointer) -> void {
let buf = floats(w * h * 4) let buf = floats(w * h * 4)
gpu_tex_bind(GPU_TEX2D, tex) gpu_tex_bind(GPU_TEX2D, tex)
gpu_pixel_store(GL_PACK_ALIGNMENT, 4) gpu_pixel_store(GL_PACK_ALIGNMENT, 4)
gpu_tex_read(GPU_TEX2D, GL_RGBA, GL_FLOAT, buf) gpu_tex_read(GPU_TEX2D, GL_RGBA, GL_FLOAT, data_of(buf))
var best = 0.0; var bx = 0; var by = 0 var best = 0.0; var bx = 0; var by = 0
var i = 0 var i = 0
while i < w * h { while i < w * h {

View file

@ -21,6 +21,8 @@ var wt_wade_s: float = 0.0
var water_refl: Target = null # the world mirrored in the surface, half resolution var water_refl: Target = null # the world mirrored in the surface, half resolution
var water_refl_div: int = 2 # R3D_REFLDIV overrides: 2 = half res, 4 = quarter var water_refl_div: int = 2 # R3D_REFLDIV overrides: 2 = half res, 4 = quarter
var water_saved: floats = null # the real camera's matrices, restored after the pass var water_saved: floats = null # the real camera's matrices, restored after the pass
var water_saved_vp: floats = null # views of its second and third matrices
var water_saved_ivp: floats = null
var water_dumped: bool = false var water_dumped: bool = false
# Several still-water planes, each at its own level over its own bounds: the sea round an # Several still-water planes, each at its own level over its own bounds: the sea round an
# island and a lake a hundred metres above it cannot be one surface. Each draws the same way; # island and a lake a hundred metres above it cannot be one surface. Each draws the same way;
@ -49,11 +51,13 @@ function water_reflection_pass() -> void {
# rendered at the window's size would be paying for pixels the water never samples # rendered at the window's size would be paying for pixels the water never samples
water_refl = target_new(post_w / water_refl_div, post_h / water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR) water_refl = target_new(post_w / water_refl_div, post_h / water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
water_saved = floats(16 * 4 + 3) water_saved = floats(16 * 4 + 3)
water_saved_vp = view(water_saved, 16, 16)
water_saved_ivp = view(water_saved, 32, 16)
} }
# save the camera # save the camera
m4_copy(water_saved, cam_view) m4_copy(water_saved, cam_view)
m4_copy(mem_off(water_saved, 64), cam_vp) m4_copy(water_saved_vp, cam_vp)
m4_copy(mem_off(water_saved, 128), cam_inv_vp) m4_copy(water_saved_ivp, cam_inv_vp)
let sx = cam_pos[0]; let sy = cam_pos[1]; let sz = cam_pos[2] let sx = cam_pos[0]; let sy = cam_pos[1]; let sz = cam_pos[2]
# the mirrored camera: view' = view * R, R reflecting y about the surface (y' = 2L - y). # the mirrored camera: view' = view * R, R reflecting y about the surface (y' = 2L - y).
# R has determinant -1, so the winding flips (front faces culled below) and the image # R has determinant -1, so the winding flips (front faces culled below) and the image
@ -98,8 +102,8 @@ function water_reflection_pass() -> void {
# restore # restore
r3d_clip_y = -2147483600.0 r3d_clip_y = -2147483600.0
m4_copy(cam_view, water_saved) m4_copy(cam_view, water_saved)
m4_copy(cam_vp, mem_off(water_saved, 64)) m4_copy(cam_vp, water_saved_vp)
m4_copy(cam_inv_vp, mem_off(water_saved, 128)) m4_copy(cam_inv_vp, water_saved_ivp)
v3_set(cam_pos, sx, sy, sz) v3_set(cam_pos, sx, sy, sz)
free(eye); free(fwd); free(up); free(at) free(eye); free(fwd); free(up); free(at)
gpu_fb_bind(0) gpu_fb_bind(0)

View file

@ -21,8 +21,8 @@ var ing_qty: words = null
function craft_ensure() -> void { function craft_ensure() -> void {
if recipe_reg == null { if recipe_reg == null {
recipe_reg = Dict.new() recipe_reg = Dict.new()
recipe_out = bytes(CRAFT_MAX * 8); recipe_outqty = words(CRAFT_MAX) recipe_out = pointers(CRAFT_MAX); recipe_outqty = words(CRAFT_MAX)
ing_rid = words(CRAFT_ING_MAX); ing_name = bytes(CRAFT_ING_MAX * 8); ing_qty = words(CRAFT_ING_MAX) ing_rid = words(CRAFT_ING_MAX); ing_name = pointers(CRAFT_ING_MAX); ing_qty = words(CRAFT_ING_MAX)
} }
} }

View file

@ -23,8 +23,8 @@ function dlg_ensure() -> void {
if dlg_reg == null { if dlg_reg == null {
dlg_reg = Dict.new() dlg_reg = Dict.new()
dlg_tree_start = words(DLG_TREE_MAX) dlg_tree_start = words(DLG_TREE_MAX)
dlg_node_tree = words(DLG_NODE_MAX); dlg_node_text = bytes(DLG_NODE_MAX * 8) dlg_node_tree = words(DLG_NODE_MAX); dlg_node_text = pointers(DLG_NODE_MAX)
dlg_choice_node = words(DLG_CHOICE_MAX); dlg_choice_label = bytes(DLG_CHOICE_MAX * 8); dlg_choice_next = words(DLG_CHOICE_MAX) dlg_choice_node = words(DLG_CHOICE_MAX); dlg_choice_label = pointers(DLG_CHOICE_MAX); dlg_choice_next = words(DLG_CHOICE_MAX)
} }
} }

View file

@ -65,10 +65,10 @@ function atlas_init() -> void {
at_spr_sy = words(ATLAS_MAX_SPR) at_spr_sy = words(ATLAS_MAX_SPR)
at_spr_w = words(ATLAS_MAX_SPR) at_spr_w = words(ATLAS_MAX_SPR)
at_spr_h = words(ATLAS_MAX_SPR) at_spr_h = words(ATLAS_MAX_SPR)
at_name_str = bytes(ATLAS_MAX_NAME * 8) # a pointer (8 bytes) per name slot at_name_str = pointers(ATLAS_MAX_NAME) # a pointer (8 bytes) per name slot
at_name_id = words(ATLAS_MAX_NAME) at_name_id = words(ATLAS_MAX_NAME)
at_q_name = bytes(ATLAS_MAX_QUEUE * 8) at_q_name = pointers(ATLAS_MAX_QUEUE)
at_q_path = bytes(ATLAS_MAX_QUEUE * 8) at_q_path = pointers(ATLAS_MAX_QUEUE)
} }
# register (name -> atlas id) in the name table so Assets.get / Sprite.named find it. # register (name -> atlas id) in the name table so Assets.get / Sprite.named find it.
@ -376,7 +376,7 @@ function assets_load_one(name: pointer, path: pointer) -> void {
return return
} }
if path_has_suffix(path, ".ttf") or path_has_suffix(path, ".ttc") { if path_has_suffix(path, ".ttf") or path_has_suffix(path, ".ttc") {
if at_font_name == null { at_font_name = bytes(ATLAS_MAX_FONT * 8); at_font_id = words(ATLAS_MAX_FONT) } if at_font_name == null { at_font_name = pointers(ATLAS_MAX_FONT); at_font_id = words(ATLAS_MAX_FONT) }
if at_font_n < ATLAS_MAX_FONT { if at_font_n < ATLAS_MAX_FONT {
at_font_name[at_font_n] = name at_font_name[at_font_n] = name
at_font_id[at_font_n] = rt_font_load(path) at_font_id[at_font_n] = rt_font_load(path)

View file

@ -31,9 +31,9 @@ var snd_bank_n: int = 0
function audio_init() -> void { function audio_init() -> void {
if snd_ready { return } if snd_ready { return }
snd_tab = bytes(AUDIO_CAP * 8) # one 8-byte pointer slot per handle snd_tab = pointers(AUDIO_CAP) # one 8-byte pointer slot per handle
fill(snd_tab, 0, AUDIO_CAP * 8) # malloc does not zero; empty slots must read null fill(snd_tab, 0, AUDIO_CAP * 8) # malloc does not zero; empty slots must read null
snd_bank_name = bytes(AUDIO_CAP * 8) snd_bank_name = pointers(AUDIO_CAP)
snd_bank_id = words(AUDIO_CAP) snd_bank_id = words(AUDIO_CAP)
snd_ready = true snd_ready = true
} }

View file

@ -0,0 +1,43 @@
# bytes.ludic — L7: bytes a program can hold without raw memory. A `[]byte` from buffer(n) is
# bounds-checked like any slice; these turn one into text and move one to and from a file. The
# runtime is the platform, so the raw calls underneath (file_read, the NUL-terminated copy) are
# its business, not the caller's.
# the first n bytes of b as text (it stops at b's end, and the text ends at a zero byte if one comes first)
function text_of(b: []byte, n: int) -> string {
var k = n
if b == null { k = 0 }
if k > len(b) { k = len(b) }
if k < 0 { k = 0 }
let out = bytes(k + 1)
var i = 0
while i < k {
out[i] = b[i]
i += 1
}
out[k] = 0
return out
}
# a whole file as bytes - through the asset pack when the file is in one - or null
function fs_read_bytes(path: string) -> []byte {
let f = file_open(path, "rb")
if f == null { return null }
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let b = buffer(n)
let got = file_read(f, b, n)
file_close(f)
if got < n { return view(b, 0, got) }
return b
}
# the first `count` bytes of b to a file, replacing it; false when it cannot be written whole
function fs_write_bytes(path: string, b: []byte, count: int) -> bool {
var k = count
if k > len(b) { k = len(b) }
let f = file_open(path, "wb")
if f == null { return false }
let put = file_write(f, b, k)
file_close(f)
return put == k
}

View file

@ -522,7 +522,7 @@ function rt_running() -> bool {
if is_windowed() { if is_windowed() {
return win_running() return win_running()
} }
return rt_alive return rt_alive != 0
} }
# ---- writing the frame out ------------------------------------------------ # ---- writing the frame out ------------------------------------------------
@ -792,7 +792,7 @@ function rt_status_text() -> pointer {
# only it knows their shape. Everything below belongs to the runtime, so the # only it knows their shape. Everything below belongs to the runtime, so the
# runtime writes it — same order both ways. # runtime writes it — same order both ways.
function rt_save_state(f: pointer) -> void { function rt_save_state(f: pointer) -> void {
let w: words = bytes(16) let w: words = words(4)
w[0] = rt_rng w[0] = rt_rng
w[1] = rt_mapw w[1] = rt_mapw
w[2] = rt_maph w[2] = rt_maph
@ -805,7 +805,7 @@ function rt_save_state(f: pointer) -> void {
} }
function rt_load_state(f: pointer) -> void { function rt_load_state(f: pointer) -> void {
let w: words = bytes(16) let w: words = words(4)
file_read(f, w, 16) file_read(f, w, 16)
rt_rng = w[0] rt_rng = w[0]
rt_mapw = w[1] rt_mapw = w[1]

View file

@ -27,7 +27,7 @@ property Dict { keys: pointers, vals: words, used: words, cap: int = 0, count: i
# (re)allocate the three parallel arrays to `cap` slots, all cleared # (re)allocate the three parallel arrays to `cap` slots, all cleared
function dict_init(d: Dict, cap: int) -> void { function dict_init(d: Dict, cap: int) -> void {
d.cap = cap; d.count = 0 d.cap = cap; d.count = 0
d.keys = bytes(cap * 8); fill(d.keys, 0, cap * 8) # null slots d.keys = pointers(cap); fill(d.keys, 0, cap * 8) # null slots
d.vals = words(cap); fill(d.vals, 0, cap * 4) d.vals = words(cap); fill(d.vals, 0, cap * 4)
d.used = words(cap); fill(d.used, 0, cap * 4) d.used = words(cap); fill(d.used, 0, cap * 4)
} }

View file

@ -203,7 +203,7 @@ var gl_log_buf: string = null
# Compile one shader stage from source; 0 (and the info log on stdout) on failure. # Compile one shader stage from source; 0 (and the info log on stdout) on failure.
function gl_shader(kind: int, src: pointer) -> int { function gl_shader(kind: int, src: pointer) -> int {
let id = gl_create_shader(kind) let id = gl_create_shader(kind)
var srcs: pointers = bytes(8) var srcs: pointers = pointers(1)
srcs[0] = src srcs[0] = src
gl_shader_source(id, 1, srcs, null) gl_shader_source(id, 1, srcs, null)
gl_compile_shader(id) gl_compile_shader(id)
@ -286,3 +286,8 @@ function gl_framebuffer() -> int {
gl_gen_framebuffers(1, ids) gl_gen_framebuffers(1, ids)
return ids[0] return ids[0]
} }
# Time.now_us / Time.sleep_us (runtime/native/namespaces.ludic): the platform's clock, as an API a
# program reaches without calling C (L7)
function time_now_us() -> long { return gl_now_us() }
function time_sleep_us(us: long) -> void { gl_sleep_us(us) }

View file

@ -48,9 +48,9 @@ function http_init() -> void {
h_res = words(HTTP_SLOTS) h_res = words(HTTP_SLOTS)
h_status = words(HTTP_SLOTS) h_status = words(HTTP_SLOTS)
h_blen = words(HTTP_SLOTS) h_blen = words(HTTP_SLOTS)
h_req = bytes(HTTP_SLOTS * 8) h_req = pointers(HTTP_SLOTS)
h_body = bytes(HTTP_SLOTS * 8) h_body = pointers(HTTP_SLOTS)
h_hdr = bytes(HTTP_SLOTS * 8) h_hdr = pointers(HTTP_SLOTS)
h_save = words(HTTP_SLOTS) h_save = words(HTTP_SLOTS)
var i = 0 var i = 0
while i < HTTP_SLOTS { while i < HTTP_SLOTS {

View file

@ -13,7 +13,7 @@ const IMG_MAX: int = 32
const SPR_MAX: int = 160 const SPR_MAX: int = 160
const SPR_SZ: int = 16 const SPR_SZ: int = 16
var img_px: pointers = null # IMG_MAX pointers to RGBA buffers var img_px: [][]int = null # IMG_MAX RGBA buffers
var img_w: words = null var img_w: words = null
var img_h: words = null var img_h: words = null
var img_n: int = 0 var img_n: int = 0
@ -22,7 +22,12 @@ var spr_px: words = null # SPR_MAX * 16 * 16 RGBA pixels, one block
var spr_n: int = 0 var spr_n: int = 0
function rt_image_init() -> void { function rt_image_init() -> void {
img_px = bytes(IMG_MAX * 8) img_px = new [][]int
var pi0 = 0
while pi0 < IMG_MAX {
push(img_px, null)
pi0 += 1
}
img_w = words(IMG_MAX) img_w = words(IMG_MAX)
img_h = words(IMG_MAX) img_h = words(IMG_MAX)
spr_px = words(SPR_MAX * SPR_SZ * SPR_SZ) spr_px = words(SPR_MAX * SPR_SZ * SPR_SZ)

View file

@ -70,7 +70,7 @@ const Z_FAST: int = 10
const Z_FASTSZ: int = 1024 # 1 << Z_FAST const Z_FASTSZ: int = 1024 # 1 << Z_FAST
const Z_SYMS: int = 1040 # 16 + Z_FASTSZ: where the symbols start const Z_SYMS: int = 1040 # 16 + Z_FASTSZ: where the symbols start
function z_table_new(nsym: int) -> pointer { function z_table_new(nsym: int) -> words {
return words((Z_SYMS + nsym)) return words((Z_SYMS + nsym))
} }
@ -238,7 +238,7 @@ function z_stored(out: pointer, at: int, cap: int) -> int {
return w return w
} }
function z_codes(out: pointer, at: int, cap: int, lit: pointer, dist: pointer) -> int { function z_codes(out: pointer, at: int, cap: int, lit: words, dist: words) -> int {
var w = at var w = at
var sym = z_decode(lit) var sym = z_decode(lit)
while sym != 256 { while sym != 256 {
@ -273,7 +273,7 @@ function z_codes(out: pointer, at: int, cap: int, lit: pointer, dist: pointer) -
return w return w
} }
function z_fixed_tables(lit: pointer, dist: pointer) -> void { function z_fixed_tables(lit: words, dist: words) -> void {
let lengths: words = words(288) let lengths: words = words(288)
for i in 0 .. 144 { lengths[i] = 8 } for i in 0 .. 144 { lengths[i] = 8 }
for i in 144 .. 256 { lengths[i] = 9 } for i in 144 .. 256 { lengths[i] = 9 }
@ -285,7 +285,7 @@ function z_fixed_tables(lit: pointer, dist: pointer) -> void {
free(lengths) free(lengths)
} }
function z_dynamic_tables(lit: pointer, dist: pointer) -> int { function z_dynamic_tables(lit: words, dist: words) -> int {
let nlen = z_bits(5) + 257 let nlen = z_bits(5) + 257
let ndist = z_bits(5) + 1 let ndist = z_bits(5) + 1
let ncode = z_bits(4) + 4 let ncode = z_bits(4) + 4

View file

@ -39,7 +39,7 @@ var input_pos: int = 0 # replay / record cursor
function input_init() -> void { function input_init() -> void {
if input_names == null { if input_names == null {
input_names = bytes(INPUT_MAX_ACT * 8) # a pointer (8 bytes) per action slot input_names = pointers(INPUT_MAX_ACT) # a pointer (8 bytes) per action slot
input_keys = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) input_keys = words(INPUT_MAX_ACT * INPUT_MAX_KEYS)
input_pads = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) # #83 pad buttons per action (+1 encoded) input_pads = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) # #83 pad buttons per action (+1 encoded)
} }
@ -236,7 +236,7 @@ var in_wheel: int = 0 # wheel delta this frame
var in_pad_conn: words = null # IN_PADS var in_pad_conn: words = null # IN_PADS
var in_pad_btn: words = null # IN_PADS var in_pad_btn: words = null # IN_PADS
var in_pad_btn0: words = null # IN_PADS — pad button mask last frame (edges) — #83 var in_pad_btn0: words = null # IN_PADS — pad button mask last frame (edges) — #83
var in_pad_axis: words = null # IN_PADS * IN_AXES (fixed) var in_pad_axis: fixeds = null # IN_PADS * IN_AXES
# touch points: active flag, x, y each # touch points: active flag, x, y each
var in_touch_on: words = null # IN_TOUCH var in_touch_on: words = null # IN_TOUCH
var in_touch_x: words = null # IN_TOUCH var in_touch_x: words = null # IN_TOUCH
@ -253,7 +253,7 @@ function in_init() -> void {
in_pad_conn = words(IN_PADS) in_pad_conn = words(IN_PADS)
in_pad_btn = words(IN_PADS) in_pad_btn = words(IN_PADS)
in_pad_btn0 = words(IN_PADS) in_pad_btn0 = words(IN_PADS)
in_pad_axis = words(IN_PADS * IN_AXES) in_pad_axis = fixeds(IN_PADS * IN_AXES)
in_touch_on = words(IN_TOUCH) in_touch_on = words(IN_TOUCH)
in_touch_x = words(IN_TOUCH) in_touch_x = words(IN_TOUCH)
in_touch_y = words(IN_TOUCH) in_touch_y = words(IN_TOUCH)

View file

@ -415,14 +415,14 @@ var ch_lock: pointers = null # a mutex per channel
function sy_init() -> void { function sy_init() -> void {
if sy_ready { return } if sy_ready { return }
mx_used = words(SYNC_MUTEX); fill(mx_used, 0, SYNC_MUTEX * 4) mx_used = words(SYNC_MUTEX); fill(mx_used, 0, SYNC_MUTEX * 4)
mx_obj = bytes(SYNC_MUTEX * 8); fill(mx_obj, 0, SYNC_MUTEX * 8) mx_obj = pointers(SYNC_MUTEX); fill(mx_obj, 0, SYNC_MUTEX * 8)
at_used = words(SYNC_ATOMIC); fill(at_used, 0, SYNC_ATOMIC * 4) at_used = words(SYNC_ATOMIC); fill(at_used, 0, SYNC_ATOMIC * 4)
at_cell = bytes(SYNC_ATOMIC * 8); fill(at_cell, 0, SYNC_ATOMIC * 8) at_cell = pointers(SYNC_ATOMIC); fill(at_cell, 0, SYNC_ATOMIC * 8)
ch_used = words(SYNC_CHAN); fill(ch_used, 0, SYNC_CHAN * 4) ch_used = words(SYNC_CHAN); fill(ch_used, 0, SYNC_CHAN * 4)
ch_head = words(SYNC_CHAN); fill(ch_head, 0, SYNC_CHAN * 4) ch_head = words(SYNC_CHAN); fill(ch_head, 0, SYNC_CHAN * 4)
ch_count = words(SYNC_CHAN); fill(ch_count, 0, SYNC_CHAN * 4) ch_count = words(SYNC_CHAN); fill(ch_count, 0, SYNC_CHAN * 4)
ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4) ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4)
ch_lock = bytes(SYNC_CHAN * 8); fill(ch_lock, 0, SYNC_CHAN * 8) ch_lock = pointers(SYNC_CHAN); fill(ch_lock, 0, SYNC_CHAN * 8)
sy_ready = true sy_ready = true
} }
@ -467,7 +467,7 @@ function sync_atomic() -> int {
while i < SYNC_ATOMIC { while i < SYNC_ATOMIC {
if at_used[i] == 0 { if at_used[i] == 0 {
at_used[i] = 1 at_used[i] = 1
if at_cell[i] == null { at_cell[i] = words(1) } if at_cell[i] == null { at_cell[i] = data_of(words(1)) } # the atomics take the cell's address, not its slice
thr_store(at_cell[i], 0) thr_store(at_cell[i], 0)
return i + 1 return i + 1
} }

View file

@ -124,7 +124,7 @@ function light_isqrt(n: int) -> int {
# sqrt(v)*2^8. Used by the normal-map N·L (unit vectors), where inputs are <= 1. # sqrt(v)*2^8. Used by the normal-map N·L (unit vectors), where inputs are <= 1.
function light_fsqrt(v: fixed) -> fixed { function light_fsqrt(v: fixed) -> fixed {
if v <= 0 { return fixed(0) } if v <= 0 { return fixed(0) }
return light_isqrt(v) << 8 return light_isqrt(as_int(v)) << 8
} }
# ---- tier controls (globals) ---------------------------------------------- # ---- tier controls (globals) ----------------------------------------------

View file

@ -542,3 +542,11 @@ namespace Tiled {
alias world_count(world) = tiled_world_count alias world_count(world) = tiled_world_count
alias world_map(world, index) = tiled_world_map alias world_map(world, index) = tiled_world_map
} }
namespace Time {
alias now_us() = time_now_us # a microsecond clock, for measuring - not the game's time
alias sleep_us(us) = time_sleep_us
}
namespace Fs {
alias read_bytes(path) = fs_read_bytes # the whole file as a []byte, or null
alias write_bytes(path, data, count) = fs_write_bytes
}

View file

@ -109,7 +109,7 @@ function proc_cmdline(path: pointer, args: []pointer) -> pointer {
function process_spawn(path: pointer, args: []pointer) -> int { function process_spawn(path: pointer, args: []pointer) -> int {
if (path == null) or (path[0] == 0) { return -1 } if (path == null) or (path[0] == 0) { return -1 }
let n = len(args) let n = len(args)
var argv: pointers = bytes((n + 2) * 8) var argv: pointers = pointers(n + 2)
argv[0] = path argv[0] = path
var i = 0 var i = 0
while i < n { while i < n {

View file

@ -43,7 +43,7 @@ var anim_nclips: int = 0
function anim_clip_init() -> void { function anim_clip_init() -> void {
if anim_clip_names == null { if anim_clip_names == null {
anim_clip_names = bytes(ANIM_MAX_CLIPS * 8) # a pointer (8 bytes) per slot anim_clip_names = pointers(ANIM_MAX_CLIPS) # a pointer (8 bytes) per slot
anim_clip_fps = words(ANIM_MAX_CLIPS) anim_clip_fps = words(ANIM_MAX_CLIPS)
anim_clip_frames = words(ANIM_MAX_CLIPS) anim_clip_frames = words(ANIM_MAX_CLIPS)
anim_clip_mode = words(ANIM_MAX_CLIPS) anim_clip_mode = words(ANIM_MAX_CLIPS)

View file

@ -64,7 +64,7 @@ var gc_adv: words = null
var gc_bmp: pointers = null var gc_bmp: pointers = null
function rt_tt_init() -> void { function rt_tt_init() -> void {
tt_data = bytes(TT_MAX * 8) tt_data = pointers(TT_MAX)
tt_size = words(TT_MAX) tt_size = words(TT_MAX)
tt_upem = words(TT_MAX) tt_upem = words(TT_MAX)
tt_nglyf = words(TT_MAX) tt_nglyf = words(TT_MAX)
@ -79,16 +79,16 @@ function rt_tt_init() -> void {
tt_cmap = words(TT_MAX) tt_cmap = words(TT_MAX)
tt_cfmt = words(TT_MAX) tt_cfmt = words(TT_MAX)
ol_x = words(TT_PTS) ol_x = fixeds(TT_PTS)
ol_y = words(TT_PTS) ol_y = fixeds(TT_PTS)
ol_on = words(TT_PTS) ol_on = words(TT_PTS)
ol_ends = words(256) ol_ends = words(256)
ed_x0 = words(TT_EDGES) ed_x0 = fixeds(TT_EDGES)
ed_y0 = words(TT_EDGES) ed_y0 = fixeds(TT_EDGES)
ed_x1 = words(TT_EDGES) ed_x1 = fixeds(TT_EDGES)
ed_y1 = words(TT_EDGES) ed_y1 = fixeds(TT_EDGES)
sc_x = words(TT_EDGES) sc_x = fixeds(TT_EDGES)
sc_d = words(TT_EDGES) sc_d = words(TT_EDGES)
gc_used = words(TT_GC) gc_used = words(TT_GC)
@ -100,7 +100,7 @@ function rt_tt_init() -> void {
gc_ox = words(TT_GC) gc_ox = words(TT_GC)
gc_oy = words(TT_GC) gc_oy = words(TT_GC)
gc_adv = words(TT_GC) gc_adv = words(TT_GC)
gc_bmp = bytes(TT_GC * 8) gc_bmp = pointers(TT_GC)
fill(gc_used, 0, TT_GC * 4) fill(gc_used, 0, TT_GC * 4)
} }

View file

@ -55,13 +55,15 @@ function udp_open(port: int) -> int {
# the port a socket is bound to (the one the system picked, for port 0) # the port a socket is bound to (the one the system picked, for port 0)
function udp_port(h: int) -> int { return lu_udp_port(h) } function udp_port(h: int) -> int { return lu_udp_port(h) }
# one datagram of `n` bytes from `buf` to ip:port: the bytes sent, or -1 # one datagram of `n` bytes from `buf` to ip:port: the bytes sent, or -1
function udp_send(h: int, ip: int, port: int, buf: pointer, n: int) -> int { function udp_send(h: int, ip: int, port: int, buf: []byte, n: int) -> int {
if (buf == null) or (n <= 0) { return -1 } if (buf == null) or (n <= 0) or (n > len(buf)) { return -1 }
return lu_udp_send(h, ip, port, buf, n) return lu_udp_send(h, ip, port, buf, n)
} }
# the next waiting datagram, up to `cap` bytes, into `buf`: its length, or 0 when none waits # the next waiting datagram, up to `cap` bytes, into `buf`: its length, or 0 when none waits
function udp_recv(h: int, buf: pointer, cap: int) -> int { function udp_recv(h: int, buf: []byte, cap0: int) -> int {
udp_init() udp_init()
var cap = cap0
if buf != null and cap > len(buf) { cap = len(buf) }
if (buf == null) or (cap <= 0) { return 0 } if (buf == null) or (cap <= 0) { return 0 }
let n = lu_udp_recv(h, buf, cap, u_ipport) let n = lu_udp_recv(h, buf, cap, u_ipport)
if (n > 0) and (h >= 1) and (h <= UDP_HANDLES) { if (n > 0) and (h >= 1) and (h <= UDP_HANDLES) {

View file

@ -108,7 +108,7 @@ function rt_ui_init() -> void {
ui_fired = words(UI_MAX) ui_fired = words(UI_MAX)
ui_hasdyn = words(UI_MAX) ui_hasdyn = words(UI_MAX)
ui_dyn = bytes(UI_MAX * 96) ui_dyn = bytes(UI_MAX * 96)
ui_text = bytes(UI_MAX * 8) ui_text = pointers(UI_MAX)
} }
# ---- build-time interface (called by compiler-emitted code) --------------- # ---- build-time interface (called by compiler-emitted code) ---------------

View file

@ -107,6 +107,13 @@ function first_arg_is_text(e: Node) -> bool {
return t == "string" return t == "string"
} }
# a namespace that computes inline also takes the methods an `alias` gives it (L6): Time.now_us
# is declared in runtime/native/namespaces.ludic beside Time.now, which the compiler computes
function emit_alias_or_fail(ns: pointer, meth: pointer, e: Node) -> Val {
let al = ns_alias_find(ns, meth)
if al < 0 { perr(`unknown builtin {ns}.{meth}`) }
return emit_alias_call(al, e)
}
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val { function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# `Prop.of(e)` / `Prop.has(e)` — typed access to one entity's component, the # `Prop.of(e)` / `Prop.has(e)` — typed access to one entity's component, the
# same binding a query loop makes but for an entity handle held in a variable. # same binding a query loop makes but for an entity handle held in a variable.
@ -120,19 +127,19 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace. # bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
if (ns == "Math") { if (ns == "Math") {
if is_math_ns(meth) { return emit_math_ns(meth, e) } if is_math_ns(meth) { return emit_math_ns(meth, e) }
perr(`unknown builtin Math.{meth}`) return emit_alias_or_fail("Math", meth, e)
} }
if (ns == "Text") { if (ns == "Text") {
if is_text_ns(meth) { return emit_text_ns(meth, e) } if is_text_ns(meth) { return emit_text_ns(meth, e) }
perr(`unknown builtin Text.{meth}`) return emit_alias_or_fail("Text", meth, e)
} }
if (ns == "List") { if (ns == "List") {
if is_list_ns(meth) { return emit_list_ns(meth, e) } if is_list_ns(meth) { return emit_list_ns(meth, e) }
perr(`unknown builtin List.{meth}`) return emit_alias_or_fail("List", meth, e)
} }
if (ns == "Ease") { if (ns == "Ease") {
if is_ease_ns(meth) { return emit_ease_ns(meth, e) } if is_ease_ns(meth) { return emit_ease_ns(meth, e) }
perr(`unknown builtin Ease.{meth}`) return emit_alias_or_fail("Ease", meth, e)
} }
if (ns == "Anim") { if (ns == "Anim") {
if is_anim_ns(meth) { return emit_anim_ns(meth, e) } if is_anim_ns(meth) { return emit_anim_ns(meth, e) }
@ -148,87 +155,87 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
} }
if (ns == "Collision") { if (ns == "Collision") {
if is_collide_ns(meth) { return emit_collide_ns(meth, e) } if is_collide_ns(meth) { return emit_collide_ns(meth, e) }
perr(`unknown builtin Collision.{meth}`) return emit_alias_or_fail("Collision", meth, e)
} }
if (ns == "Memory") { if (ns == "Memory") {
if is_mem_ns(meth) { return emit_mem_ns(meth, e) } if is_mem_ns(meth) { return emit_mem_ns(meth, e) }
perr(`unknown builtin Memory.{meth}`) return emit_alias_or_fail("Memory", meth, e)
} }
if (ns == "Color") { if (ns == "Color") {
if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) } if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) }
perr(`unknown builtin Color.{meth}`) return emit_alias_or_fail("Color", meth, e)
} }
if (ns == "Time") { if (ns == "Time") {
if is_time_ns(meth) { return emit_time_ns(meth, e) } if is_time_ns(meth) { return emit_time_ns(meth, e) }
perr(`unknown builtin Time.{meth}`) return emit_alias_or_fail("Time", meth, e)
} }
if (ns == "Hash") { if (ns == "Hash") {
if is_hash_ns(meth) { return emit_hash_ns(meth, e) } if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
perr(`unknown builtin Hash.{meth}`) return emit_alias_or_fail("Hash", meth, e)
} }
if (ns == "Crypto") { if (ns == "Crypto") {
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) } if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
perr(`unknown builtin Crypto.{meth}`) return emit_alias_or_fail("Crypto", meth, e)
} }
if (ns == "Uuid") { if (ns == "Uuid") {
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) } if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
perr(`unknown builtin Uuid.{meth}`) return emit_alias_or_fail("Uuid", meth, e)
} }
if (ns == "Noise") { if (ns == "Noise") {
if is_noise_ns(meth) { return emit_noise_ns(meth, e) } if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
perr(`unknown builtin Noise.{meth}`) return emit_alias_or_fail("Noise", meth, e)
} }
if (ns == "Log") { if (ns == "Log") {
if is_log_ns(meth) { return emit_log_ns(meth, e) } if is_log_ns(meth) { return emit_log_ns(meth, e) }
perr(`unknown builtin Log.{meth}`) return emit_alias_or_fail("Log", meth, e)
} }
if (ns == "Os") { if (ns == "Os") {
if is_os_ns(meth) { return emit_os_ns(meth, e) } if is_os_ns(meth) { return emit_os_ns(meth, e) }
perr(`unknown builtin Os.{meth}`) return emit_alias_or_fail("Os", meth, e)
} }
if (ns == "Unicode") { if (ns == "Unicode") {
if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) } if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) }
perr(`unknown builtin Unicode.{meth}`) return emit_alias_or_fail("Unicode", meth, e)
} }
if (ns == "Fs") { if (ns == "Fs") {
if is_fs_ns(meth) { return emit_fs_ns(meth, e) } if is_fs_ns(meth) { return emit_fs_ns(meth, e) }
perr(`unknown builtin Fs.{meth}`) return emit_alias_or_fail("Fs", meth, e)
} }
if (ns == "Path") { if (ns == "Path") {
if is_path_ns(meth) { return emit_path_ns(meth, e) } if is_path_ns(meth) { return emit_path_ns(meth, e) }
perr(`unknown builtin Path.{meth}`) return emit_alias_or_fail("Path", meth, e)
} }
if (ns == "Mime") { if (ns == "Mime") {
if is_mime_ns(meth) { return emit_mime_ns(meth, e) } if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
perr(`unknown builtin Mime.{meth}`) return emit_alias_or_fail("Mime", meth, e)
} }
if (ns == "Vector") { if (ns == "Vector") {
if is_vector_ns(meth) { return emit_vector_ns(meth, e) } if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
perr(`unknown builtin Vector.{meth}`) return emit_alias_or_fail("Vector", meth, e)
} }
if (ns == "IVec2") { if (ns == "IVec2") {
if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) } if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) }
perr(`unknown builtin IVec2.{meth}`) return emit_alias_or_fail("IVec2", meth, e)
} }
if (ns == "Rect") { if (ns == "Rect") {
if is_rect_ns(meth) { return emit_rect_ns(meth, e) } if is_rect_ns(meth) { return emit_rect_ns(meth, e) }
perr(`unknown builtin Rect.{meth}`) return emit_alias_or_fail("Rect", meth, e)
} }
if (ns == "Duration") { if (ns == "Duration") {
if is_duration_ns(meth) { return emit_duration_ns(meth, e) } if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
perr(`unknown builtin Duration.{meth}`) return emit_alias_or_fail("Duration", meth, e)
} }
if (ns == "Date") { if (ns == "Date") {
if is_date_ns(meth) { return emit_date_ns(meth, e) } if is_date_ns(meth) { return emit_date_ns(meth, e) }
perr(`unknown builtin Date.{meth}`) return emit_alias_or_fail("Date", meth, e)
} }
if (ns == "DateTime") { if (ns == "DateTime") {
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) } if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
perr(`unknown builtin DateTime.{meth}`) return emit_alias_or_fail("DateTime", meth, e)
} }
if (ns == "Clock") { if (ns == "Clock") {
if is_clock_ns(meth) { return emit_clock_ns(meth, e) } if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
perr(`unknown builtin Clock.{meth}`) return emit_alias_or_fail("Clock", meth, e)
} }
# #80 — entity pool stats. The ECS allocator already recycles freed entity slots # #80 — entity pool stats. The ECS allocator already recycles freed entity slots
# through a freelist (L_alloc pops @L_freen before growing @L_entc), and component # through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
@ -287,7 +294,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if g_windowed { emit(` call void @app_{meth}()\n`) } if g_windowed { emit(` call void @app_{meth}()\n`) }
return val("0", "void") return val("0", "void")
} }
perr(`unknown builtin App.{meth}`) return emit_alias_or_fail("App", meth, e)
} }
if (ns == "Pool") { if (ns == "Pool") {
if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities
@ -298,7 +305,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
let fr = emit_bind("load i32, ptr @L_freen") let fr = emit_bind("load i32, ptr @L_freen")
return val(emit_bind(`sub i32 {ec}, {fr}`), "int") return val(emit_bind(`sub i32 {ec}, {fr}`), "int")
} }
perr(`unknown builtin Pool.{meth}`) return emit_alias_or_fail("Pool", meth, e)
} }
# L6: a method declared by `alias` in a namespace block - the engine's own in # L6: a method declared by `alias` in a namespace block - the engine's own in
# runtime/native/namespaces.ludic, a package's in its files - is a call to its target # runtime/native/namespaces.ludic, a package's in its files - is a call to its target
@ -341,7 +348,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
let nm = emit_expr(e.kids[0]) let nm = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`), "entity") return val(emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`), "entity")
} }
perr(`unknown builtin Prefab.{meth}`) return emit_alias_or_fail("Prefab", meth, e)
} }
# Camera.* — the world-space camera: a draw offset threaded through the render # Camera.* — the world-space camera: a draw offset threaded through the render
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from # path (runtime/native/core.ludic). set/follow move it; shake jitters it from
@ -613,11 +620,17 @@ function emit_call(e: Node) -> Val {
let w = emit_bind(`zext i32 {n.code} to i64`) let w = emit_bind(`zext i32 {n.code} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), "pointer") return val(emit_bind(`call ptr @malloc(i64 {w})`), "pointer")
} }
if (name == "words") { # words(n): allocate n 32-bit words if (name == "words") { return emit_sized_slice("int", emit_expr(e.kids[0])) } # words(n): n zeroed ints
let n = emit_expr(e.kids[0]) if (name == "buffer") and (find_fn(name) == null) { return emit_sized_slice("byte", emit_expr(e.kids[0])) } # buffer(n): n zeroed bytes (L7)
let by = emit_bind(`mul i32 {n.code}, 4`) if (name == "fixeds") and (find_fn(name) == null) { return emit_sized_slice("fixed", emit_expr(e.kids[0])) }
let w = emit_bind(`zext i32 {by} to i64`) if (name == "pointers") and (find_fn(name) == null) { return emit_sized_slice("pointer", emit_expr(e.kids[0])) }
return val(emit_bind(`call ptr @malloc(i64 {w})`), "words") # view(xs, start, count): `count` elements of xs from `start`, sharing its storage - checked
# against xs's length once, when it is made, and bounds-checked like any slice after (L7)
if (name == "view") { return emit_view(e) }
# data_of(xs): the address of a slice's first element, for handing to C (unsafe, L7)
if (name == "data_of") {
let sv = emit_expr(e.kids[0])
return val(emit_bind(`load ptr, ptr {slice_field(sv.code, 0)}`), "pointer")
} }
if (name == "fixed") { if (name == "fixed") {
let a = emit_expr(e.kids[0]) let a = emit_expr(e.kids[0])
@ -648,7 +661,7 @@ function emit_call(e: Node) -> Val {
if ((name == "floats") or (name == "doubles")) and (find_fn(name) == null) { if ((name == "floats") or (name == "doubles")) and (find_fn(name) == null) {
var ft = "float" var ft = "float"
if (name == "doubles") { ft = "double" } if (name == "doubles") { ft = "double" }
return emit_fp_buffer(ft, emit_expr(e.kids[0])) return emit_sized_slice(ft, emit_expr(e.kids[0]))
} }
if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") } if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
# --- the testing framework's assertions (see emit_test_runner) -------------- # --- the testing framework's assertions (see emit_test_runner) --------------
@ -854,9 +867,12 @@ function emit_call(e: Node) -> Val {
let eatys = new []pointer let eatys = new []pointer
var ei = 0 var ei = 0
while ei < len(e.kids) { while ei < len(e.kids) {
let v = emit_expr(e.kids[ei]) var v = emit_expr(e.kids[ei])
# a C function wants a buffer's elements, never a Ludic slice's header
if is_slice_ty(v.ty) { v = val(emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`), "pointer") }
var pty = v.ty var pty = v.ty
if ei < len(eptys) { pty = eptys[ei] } if ei < len(eptys) { pty = eptys[ei] }
if is_slice_ty(pty) { pty = "pointer" }
push(eargs, coerce_code(v, pty)) push(eargs, coerce_code(v, pty))
push(eatys, pty) push(eatys, pty)
ei += 1 ei += 1

View file

@ -86,6 +86,7 @@ function emit_header() -> void {
emith("; Ludic (self-hosted) -> LLVM IR\n") emith("; Ludic (self-hosted) -> LLVM IR\n")
emith("declare i32 @printf(ptr, ...)\n") emith("declare i32 @printf(ptr, ...)\n")
emith("declare ptr @malloc(i64)\n") emith("declare ptr @malloc(i64)\n")
emith("declare ptr @calloc(i64, i64)\n")
emith("declare ptr @realloc(ptr, i64)\n") emith("declare ptr @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n") emith("declare void @free(ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n") emith("declare i64 @fread(ptr, i64, i64, ptr)\n")

View file

@ -42,7 +42,7 @@ function emit_intrinsic(name: pointer, e: Node) -> Val {
return val(emit_bind(`call ptr @lp_pak_open(ptr {p}, ptr {m})`), "pointer") return val(emit_bind(`call ptr @lp_pak_open(ptr {p}, ptr {m})`), "pointer")
} }
if (name == "file_read") or (name == "file_write") { if (name == "file_read") or (name == "file_write") {
let f = arg_code(e, 0); let b = arg_code(e, 1); let n = arg_code(e, 2) let f = arg_code(e, 0); let b = raw_expr(e.kids[1]).code; let n = arg_code(e, 2)
let w = emit_bind(`zext i32 {n} to i64`) let w = emit_bind(`zext i32 {n} to i64`)
var fn2 = "@fread" var fn2 = "@fread"
if (name == "file_write") { fn2 = "@fwrite" } if (name == "file_write") { fn2 = "@fwrite" }

View file

@ -35,16 +35,23 @@ function is_intrinsic2(name: pointer) -> bool {
function emit_intrinsic2(name: pointer, e: Node) -> Val { function emit_intrinsic2(name: pointer, e: Node) -> Val {
if (name == "free") { if (name == "free") {
let p = arg_code(e, 0); emit(" call void @free(ptr "); emit(p); emit(")\n"); return val("0", "void") let fv = emit_expr(e.kids[0])
# a slice is its elements and its header: both go (L7)
if is_slice_ty(fv.ty) {
let fd = emit_bind(`load ptr, ptr {slice_field(fv.code, 0)}`)
emit(` call void @free(ptr {fd})\n`)
}
emit(` call void @free(ptr {fv.code})\n`)
return val("0", "void")
} }
if (name == "fill") { if (name == "fill") {
let p = arg_code(e, 0); let v = arg_code(e, 1); let n = arg_code(e, 2) let p = raw_expr(e.kids[0]).code; let v = arg_code(e, 1); let n = arg_code(e, 2)
let w = emit_bind(`zext i32 {n} to i64`) let w = emit_bind(`zext i32 {n} to i64`)
emit(" call ptr @memset(ptr "); emit(p); emit(", i32 "); emit(v); emit(", i64 "); emit(w); emit(")\n") emit(" call ptr @memset(ptr "); emit(p); emit(", i32 "); emit(v); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void") return val("0", "void")
} }
if (name == "offset") { if (name == "offset") {
let p = arg_code(e, 0); let n = arg_code(e, 1) let p = raw_expr(e.kids[0]).code; let n = arg_code(e, 1)
let g = nreg() let g = nreg()
emit(" "); emit(g); emit(" = getelementptr inbounds i8, ptr "); emit(p); emit(", i32 "); emit(n); emit("\n") emit(" "); emit(g); emit(" = getelementptr inbounds i8, ptr "); emit(p); emit(", i32 "); emit(n); emit("\n")
return val(g, "pointer") return val(g, "pointer")

View file

@ -19,28 +19,28 @@ function emit_mem_ns(meth: pointer, e: Node) -> Val {
let n = emit_expr(e.kids[0]) let n = emit_expr(e.kids[0])
let by = emit_bind(`mul i32 {n.code}, 4`) let by = emit_bind(`mul i32 {n.code}, 4`)
let w = emit_bind(`zext i32 {by} to i64`) let w = emit_bind(`zext i32 {by} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), "words") return emit_sized_slice("int", n)
} }
if (meth == "copy") { # copy n bytes src -> dst if (meth == "copy") { # copy n bytes src -> dst
let dst = emit_expr(e.kids[0]); let src = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2]) let dst = raw_expr(e.kids[0]); let src = raw_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let w = emit_bind(`zext i32 {n.code} to i64`) let w = emit_bind(`zext i32 {n.code} to i64`)
emit(" call ptr @memcpy(ptr "); emit(dst.code); emit(", ptr "); emit(src.code); emit(", i64 "); emit(w); emit(")\n") emit(" call ptr @memcpy(ptr "); emit(dst.code); emit(", ptr "); emit(src.code); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void") return val("0", "void")
} }
if (meth == "fill") { # set n bytes of buf to value v if (meth == "fill") { # set n bytes of buf to value v
let buf = emit_expr(e.kids[0]); let v = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2]) let buf = raw_expr(e.kids[0]); let v = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let w = emit_bind(`zext i32 {n.code} to i64`) let w = emit_bind(`zext i32 {n.code} to i64`)
emit(" call ptr @memset(ptr "); emit(buf.code); emit(", i32 "); emit(v.code); emit(", i64 "); emit(w); emit(")\n") emit(" call ptr @memset(ptr "); emit(buf.code); emit(", i32 "); emit(v.code); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void") return val("0", "void")
} }
if (meth == "peek") { # read one byte at buf[i], 0..255 if (meth == "peek") { # read one byte at buf[i], 0..255
let buf = emit_expr(e.kids[0]); let i = emit_expr(e.kids[1]) let buf = raw_expr(e.kids[0]); let i = emit_expr(e.kids[1])
let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`) let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`)
let c = emit_bind(`load i8, ptr {p}`) let c = emit_bind(`load i8, ptr {p}`)
return val(emit_bind(`zext i8 {c} to i32`), "int") return val(emit_bind(`zext i8 {c} to i32`), "int")
} }
# poke: write the low byte of v at buf[i] # poke: write the low byte of v at buf[i]
let buf = emit_expr(e.kids[0]); let i = emit_expr(e.kids[1]); let v = emit_expr(e.kids[2]) let buf = raw_expr(e.kids[0]); let i = emit_expr(e.kids[1]); let v = emit_expr(e.kids[2])
let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`) let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`)
let b = emit_bind(`trunc i32 {v.code} to i8`) let b = emit_bind(`trunc i32 {v.code} to i8`)
emit(" store i8 "); emit(b); emit(", ptr "); emit(p); emit("\n") emit(" store i8 "); emit(b); emit(", ptr "); emit(p); emit("\n")

View file

@ -59,6 +59,54 @@ function emit_new_slice(ty: pointer) -> Val {
return val(h, ty) return val(h, ty)
} }
# words(n), floats(n), doubles(n): a slice of n zeroed elements, its length n (L7)
function emit_sized_slice(el: pointer, n: Val) -> Val {
let h = emit_new_slice("[]" + el)
let esz = emit_sizeof(llty(el))
let nn = emit_bind(`zext i32 {n.code} to i64`)
let data = emit_bind(`call ptr @calloc(i64 {nn}, i64 {esz})`)
emit(` store ptr {data}, ptr {slice_field(h.code, 0)}\n`)
emit(` store i32 {n.code}, ptr {slice_field(h.code, 1)}\n`)
emit(` store i32 {n.code}, ptr {slice_field(h.code, 2)}\n`)
return h
}
function emit_view(e: Node) -> Val {
let xs = emit_expr(e.kids[0])
if not is_slice_ty(xs.ty) { perr(`view takes a slice, and this is {xs.ty}`) }
let st = emit_expr(e.kids[1])
let ct = emit_expr(e.kids[2])
let el = slice_elem(xs.ty)
let ln = emit_bind(`load i32, ptr {slice_field(xs.code, 1)}`)
let endv = emit_bind(`add i32 {st.code}, {ct.code}`)
let ok1 = emit_bind(`icmp ule i32 {endv}, {ln}`)
let ok2 = emit_bind(`icmp sge i32 {st.code}, 0`)
let ok3 = emit_bind(`icmp sge i32 {ct.code}, 0`)
let ok12 = emit_bind(`and i1 {ok1}, {ok2}`)
let ok = emit_bind(`and i1 {ok12}, {ok3}`)
let lok = lbl("vwok")
let lbad = lbl("vwbad")
emit(` br i1 {ok}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
g_uses_bounds = true
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: view past the end: from `)
let bse = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {endv}, i32 {ln})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
emit(`{lok}:\n`)
let d0 = emit_bind(`load ptr, ptr {slice_field(xs.code, 0)}`)
let d1 = emit_bind(`getelementptr inbounds {llty(el)}, ptr {d0}, i32 {st.code}`)
let h = emit_new_slice(xs.ty)
emit(` store ptr {d1}, ptr {slice_field(h.code, 0)}\n`)
emit(` store i32 {ct.code}, ptr {slice_field(h.code, 1)}\n`)
emit(` store i32 {ct.code}, ptr {slice_field(h.code, 2)}\n`)
return h
}
# an expression that a raw-memory intrinsic reads as an address: a slice gives its elements (L7)
function raw_expr(n: Node) -> Val {
let v = emit_expr(n)
if is_slice_ty(v.ty) { return val(emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`), "pointer") }
return v
}
function slice_field(h: pointer, i: int) -> pointer { function slice_field(h: pointer, i: int) -> pointer {
let r = nreg() let r = nreg()
emit(" "); emit(r); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h) emit(" "); emit(r); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h)

View file

@ -146,6 +146,7 @@ function block_ends(b: Node) -> bool {
} }
function stmt_ends(st: Node) -> bool { function stmt_ends(st: Node) -> bool {
if st.kind == S_RETURN { return true } if st.kind == S_RETURN { return true }
if st.kind == S_UNSAFE { return block_ends(st.a) }
if st.kind == S_IF { if st.kind == S_IF {
if st.c == null or not block_ends(st.b) { return false } if st.c == null or not block_ends(st.b) { return false }
if st.c.kind == S_IF { return stmt_ends(st.c) } if st.c.kind == S_IF { return stmt_ends(st.c) }
@ -377,6 +378,7 @@ function emit_stmt(st: Node) -> void {
if st.kind == S_SPAWN { let se = emit_spawn(st); return } if st.kind == S_SPAWN { let se = emit_spawn(st); return }
if st.kind == S_DESPAWN { emit_despawn(st); return } if st.kind == S_DESPAWN { emit_despawn(st); return }
if st.kind == S_TOGGLE { emit_toggle(st); return } if st.kind == S_TOGGLE { emit_toggle(st); return }
if st.kind == S_UNSAFE { emit_block(st.a); return }
if st.kind == S_ATTACH { emit_attach(st); return } if st.kind == S_ATTACH { emit_attach(st); return }
if st.kind == S_DETACH { emit_detach(st); return } if st.kind == S_DETACH { emit_detach(st); return }
if st.kind == S_EMIT { let v = emit_emit(st); return } # statement form: discard the flag if st.kind == S_EMIT { let v = emit_emit(st); return } # statement form: discard the flag

View file

@ -84,6 +84,37 @@ function ck_builtin(e: Node, name: pointer) -> pointer {
ck_walk_args(e) ck_walk_args(e)
return "int" return "int"
} }
if (name == "view") and len(e.kids) == 3 {
let vt = ck_expr(e.kids[0])
ck_give("int", ck_expr(e.kids[1]), e.kids[1], "the start of a view")
ck_give("int", ck_expr(e.kids[2]), e.kids[2], "the length of a view")
if not ck_unknown(vt) and not is_slice_ty(vt) { ck_err("kind", e, `view takes a slice, and this is {ck_a(vt)}`) }
return vt
}
if (name == "bytes") or (name == "offset") {
ck_walk_args(e)
return "pointer"
}
if (name == "data_of") {
ck_walk_args(e)
return "pointer"
}
if (name == "words") {
ck_walk_args(e)
return "[]int"
}
if (name == "buffer") {
ck_walk_args(e)
return "[]byte"
}
if (name == "fixeds") or (name == "pointers") {
ck_walk_args(e)
return "[]" + name[0 .. len(name) - 1]
}
if (name == "floats") or (name == "doubles") {
ck_walk_args(e)
return "[]" + name[0 .. len(name) - 1]
}
if (name == "float_from_bits") { if (name == "float_from_bits") {
ck_walk_args(e) ck_walk_args(e)
return "float" return "float"
@ -107,11 +138,20 @@ function ck_call(e: Node) -> pointer {
let b = c.a let b = c.a
if b.kind == E_ID and ck_local(b.s) < 0 and ck_global(b.s) == null { if b.kind == E_ID and ck_local(b.s) < 0 and ck_global(b.s) == null {
# an alias (L6) is its target, labels and all, so its arguments are checked in full # an alias (L6) is its target, labels and all, so its arguments are checked in full
if (b.s == "Memory") { ck_raw(e, `Memory.{c.s}`) }
let al = ns_alias_find(b.s, c.s) let al = ns_alias_find(b.s, c.s)
if al >= 0 { if al >= 0 {
var tf = ck_fn(g_al_target[al]) var tf = ck_fn(g_al_target[al])
if tf == null { tf = ck_extern(g_al_target[al]) } if tf == null {
if tf != null { return ck_call_alias(e, `{b.s}.{c.s}`, al, tf) } tf = ck_extern(g_al_target[al])
if tf != null { ck_raw(e, `{b.s}.{c.s}, a C function`) }
}
if tf != null {
ck_extern_arg = ck_extern(g_al_target[al]) != null
let at = ck_call_alias(e, `{b.s}.{c.s}`, al, tf)
ck_extern_arg = false
return at
}
ck_walk_args(e) ck_walk_args(e)
return "?" return "?"
} }
@ -141,6 +181,7 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
ck_walk_args(e) ck_walk_args(e)
return "?" return "?"
} }
if ck_raw_builtin(name) { ck_raw(e, `{name}()`) }
let bt = ck_builtin(e, name) let bt = ck_builtin(e, name)
if bt != null { return bt } if bt != null { return bt }
let f = ck_fn(name) let f = ck_fn(name)
@ -148,7 +189,13 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
let gt = gen_template(g_gen_fns, name) let gt = gen_template(g_gen_fns, name)
if gt != null { return gen_call(e, name, gt) } if gt != null { return gen_call(e, name, gt) }
let x = ck_extern(name) let x = ck_extern(name)
if x != null { return ck_call_fn(e, name, x) } if x != null {
ck_raw(e, `the C function {name}`)
ck_extern_arg = true
let xt = ck_call_fn(e, name, x)
ck_extern_arg = false
return xt
}
let g = ck_global(name) let g = ck_global(name)
if g != null and is_fn_type(g.ty) { return ck_call_sig(e, name, g.ty) } if g != null and is_fn_type(g.ty) { return ck_call_sig(e, name, g.ty) }
ck_walk_args(e) ck_walk_args(e)

View file

@ -53,5 +53,13 @@ function ck_err(cat: pointer, n: Node, msg: pointer) -> void {
function ck_give(to: pointer, from: pointer, e: Node, what: pointer) -> void { function ck_give(to: pointer, from: pointer, e: Node, what: pointer) -> void {
let cat = ck_mismatch(to, from, e) let cat = ck_mismatch(to, from, e)
if cat == null { return } if cat == null { return }
if (cat == "slice-pointer") {
ck_err(cat, e, `{what} wants a pointer and this is {ck_a(from)}: take the slice itself, or data_of(xs) in unsafe code`)
return
}
if (cat == "pointer-slice") {
ck_err(cat, e, `{what} wants {ck_a(to)} and this is a pointer: a slice is made with words(n), floats(n) or new, not from an address`)
return
}
ck_err(cat, e, `{what} wants {ck_a(to)} and this is {ck_a(from)}`) ck_err(cat, e, `{what} wants {ck_a(to)} and this is {ck_a(from)}`)
} }

View file

@ -24,7 +24,7 @@ function ck_any(n: Node) -> void {
if n == null { return } if n == null { return }
if n.kind == N_BLOCK { ck_block(n); return } if n.kind == N_BLOCK { ck_block(n); return }
if n.kind == E_FINIT { ck_expr(n.a); return } if n.kind == E_FINIT { ck_expr(n.a); return }
if (n.kind >= S_LET and n.kind <= S_BECOME) or n.kind == S_EMIT { ck_stmt(n); return } if (n.kind >= S_LET and n.kind <= S_BECOME) or n.kind == S_EMIT or n.kind == S_UNSAFE { ck_stmt(n); return }
ck_expr(n) ck_expr(n)
} }
function ck_expr(e: Node) -> pointer { function ck_expr(e: Node) -> pointer {
@ -125,6 +125,7 @@ function ck_member(e: Node) -> pointer {
function ck_index_of(e: Node) -> pointer { function ck_index_of(e: Node) -> pointer {
let bt = ck_expr(e.a) let bt = ck_expr(e.a)
let it = ck_expr(e.b) let it = ck_expr(e.b)
if ck_is_raw(bt) { ck_raw(e, `indexing {ck_a(bt)}`) }
if not ck_unknown(it) and not ck_is_int(it) { ck_err("index", e.b, `an index wants an int and this is {ck_a(it)}`) } if not ck_unknown(it) and not ck_is_int(it) { ck_err("index", e.b, `an index wants an int and this is {ck_a(it)}`) }
if ck_unknown(bt) { return "?" } if ck_unknown(bt) { return "?" }
if is_slice_ty(bt) { return slice_elem(bt) } if is_slice_ty(bt) { return slice_elem(bt) }

View file

@ -107,6 +107,13 @@ function ck_stmt(s: Node) -> void {
if k == S_EXPR { ck_expr(s.a); return } if k == S_EXPR { ck_expr(s.a); return }
if k == S_MATCH { ck_match(s); return } if k == S_MATCH { ck_match(s); return }
if k == S_EMIT { ck_emit(s); return } if k == S_EMIT { ck_emit(s); return }
if k == S_UNSAFE {
ck_unsafe_here(s)
ck_unsafe += 1
ck_block(s.a)
ck_unsafe -= 1
return
}
} }
function ck_fn_body(d: Node) -> void { function ck_fn_body(d: Node) -> void {
let m = ck_mark() let m = ck_mark()
@ -117,7 +124,12 @@ function ck_fn_body(d: Node) -> void {
} }
ck_ret = d.ty ck_ret = d.ty
if ck_ret == null { ck_ret = "void" } if ck_ret == null { ck_ret = "void" }
if d.uns == 1 {
ck_unsafe_here(d)
ck_unsafe += 1
}
ck_block(d.a) ck_block(d.a)
if d.uns == 1 { ck_unsafe -= 1 }
ck_ret = "void" ck_ret = "void"
ck_pop(m) ck_pop(m)
} }
@ -140,7 +152,7 @@ function check_program() -> void {
gen_collect() gen_collect()
ck_index() ck_index()
var i = 0 var i = 0
while i < g_prog_user_end { while i < len(prog) {
let d = prog[i] let d = prog[i]
if d.kind == N_FN { ck_fn_body(d) } if d.kind == N_FN { ck_fn_body(d) }
if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) } if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) }

View file

@ -3,6 +3,7 @@
# agrees with everything, so the checker only ever reports a mix-up it can prove. "null" is the # agrees with everything, so the checker only ever reports a mix-up it can prove. "null" is the
# type of the null literal, and `pointer` is untyped: numbers, bools, text, records, slices and # type of the null literal, and `pointer` is untyped: numbers, bools, text, records, slices and
# functions are kept apart, and a pointer is trusted to be whatever it is given as. # functions are kept apart, and a pointer is trusted to be whatever it is given as.
var ck_extern_arg: bool = false # an extern's arguments are being given: a slice goes as its data
function ck_unknown(t: pointer) -> bool { return t == null or (t == "?") } function ck_unknown(t: pointer) -> bool { return t == null or (t == "?") }
function ck_is_int(t: pointer) -> bool { function ck_is_int(t: pointer) -> bool {
if (t == "int") or (t == "long") or (t == "byte") or (t == "i64") or (t == "u8") { return true } if (t == "int") or (t == "long") or (t == "byte") or (t == "i64") or (t == "u8") { return true }
@ -72,6 +73,17 @@ function ck_mismatch(to: pointer, from: pointer, e: Node) -> pointer {
if ck_is_num(from) { return "kind" } if ck_is_num(from) { return "kind" }
# `pointer` is the untyped reference, C's void *: it goes anywhere a reference does, and a # `pointer` is the untyped reference, C's void *: it goes anywhere a reference does, and a
# reference goes into it. What it may hold is L7's question (unsafe), not this pass's. # reference goes into it. What it may hold is L7's question (unsafe), not this pass's.
# `bytes` is a raw buffer, a pointer by another name: the same rules
if (to == "bytes") and is_slice_ty(from) {
if ck_extern_arg { return null }
return "slice-pointer"
}
if (from == "bytes") and is_slice_ty(to) { return "pointer-slice" }
if (to == "pointer") and is_slice_ty(from) {
if ck_extern_arg { return null }
return "slice-pointer"
}
if (from == "pointer") and is_slice_ty(to) { return "pointer-slice" }
if (to == "pointer") or (from == "pointer") { return null } if (to == "pointer") or (from == "pointer") { return null }
if (to == "string") { return "kind" } if (to == "string") { return "kind" }
if ck_is_rec(to) and ck_is_rec(from) { return "record" } if ck_is_rec(to) and ck_is_rec(from) { return "record" }

View file

@ -0,0 +1,29 @@
# check_unsafe.ludic — L7: raw memory is `unsafe`. A bytes() buffer, indexing a bare pointer,
# data_of(a slice), free, resize, the raw file calls, Memory.* and calling a C function (an
# `extern`) are refused outside an `unsafe { }` block or an `unsafe function`. The typed buffers -
# words(n), floats(n) - are slices, bounds-checked, and need none of it.
# And `unsafe` itself is only for the files that are the platform: the runtime, a package the
# toolchain or ludic_modules provides, and what they import beside them - a project's own files
# may write it only when the build says --unsafe. A game is written against APIs, not memory.
var ck_unsafe: int = 0 # how many unsafe blocks and functions enclose this point
# words(n), floats(n) and the rest are slices now - bounds-checked, safe - so what is raw is a
# bare pointer indexed, and the builtins that hand out or take back addresses
function ck_is_raw(t: pointer) -> bool { return (t == "pointer") or (t == "bytes") }
function ck_raw_builtin(name: pointer) -> bool {
if (name == "bytes") or (name == "data_of") or (name == "free") or (name == "resize") { return true }
return (name == "file_read") or (name == "file_write")
}
# the platform - the runtime and the packages - is raw memory by trade: its files are unsafe
# throughout, and the rule is for a project's own code
function ck_raw(n: Node, what: pointer) -> void {
if ck_unsafe > 0 { return }
if n != null and n.file != null and unsafe_trusted(n.file) { return }
ck_err("unsafe", n, `{what} is raw memory: it belongs inside unsafe {{ }}, and a game reaches it through an API`)
}
# an unsafe block or function where the file may not have one
function ck_unsafe_here(n: Node) -> void {
if n == null or n.file == null { return }
if unsafe_trusted(n.file) { return }
ck_err("unsafe-block", n, "unsafe is for the runtime and packages; this file may use it only when the build says --unsafe")
}

View file

@ -37,6 +37,7 @@ const E_TRY: int = 52 # try EXPR else { ... } — recover a fallible
# a=the fallible (result-typed) expression b=else block (its # a=the fallible (result-typed) expression b=else block (its
# trailing expression is the fallback) line=source line # trailing expression is the fallback) line=source line
const E_LIST: int = 54 # [a, b, c] — a slice literal; kids=the elements, all of one type const E_LIST: int = 54 # [a, b, c] — a slice literal; kids=the elements, all of one type
const S_UNSAFE: int = 56 # unsafe { ... } — raw memory allowed inside (L7); a = the block
const E_FNREF: int = 55 # fn name — a top-level function as a value (s=the name); a worker entry point const E_FNREF: int = 55 # fn name — a top-level function as a value (s=the name); a worker entry point
# statements # statements
const S_LET: int = 10 const S_LET: int = 10
@ -87,6 +88,7 @@ property Node {
file: pointer = null # the source file the node was parsed from (for diagnostics) file: pointer = null # the source file the node was parsed from (for diagnostics)
vis: int = 0 # L3: 1 when the declaration is `export`ed from its module vis: int = 0 # L3: 1 when the declaration is `export`ed from its module
tps: pointer = null # L5: a generic declaration's type parameters, "T|U"; null when not generic tps: pointer = null # L5: a generic declaration's type parameters, "T|U"; null when not generic
uns: int = 0 # L7: 1 on an `unsafe function`
} }
# every node remembers where it was parsed (file + the line of the token the # every node remembers where it was parsed (file + the line of the token the

View file

@ -17,6 +17,21 @@ var g_float_files: []pointer = new []pointer
# L3 modules: `module NAME` at the top of a file names the module it and everything it imports # L3 modules: `module NAME` at the top of a file names the module it and everything it imports
# belong to, until an import names its own; `friend module NAME` may see every module's private # belong to, until an import names its own; `friend module NAME` may see every module's private
# names (a test harness). A file in no module - the runtime, a program's root - is public. # names (a test harness). A file in no module - the runtime, a program's root - is public.
# L7: the files that may write `unsafe` - the runtime, a package from the toolchain or
# ludic_modules, and what those import from beside them. A project's own files may only
# with --unsafe (g_unsafe_all).
var g_uses_bytes: bool = false # text_of / Fs.read_bytes / Fs.write_bytes: splice bytes.ludic
var g_trusted_files: []pointer = new []pointer
var g_unsafe_all: bool = false
function unsafe_trusted(f: pointer) -> bool {
if g_unsafe_all { return true }
var i = 0
while i < len(g_trusted_files) {
if (g_trusted_files[i] == f) { return true }
i += 1
}
return false
}
var g_mod_file: []pointer = new []pointer var g_mod_file: []pointer = new []pointer
var g_mod_name: []pointer = new []pointer var g_mod_name: []pointer = new []pointer
var g_mod_friends: []pointer = new []pointer var g_mod_friends: []pointer = new []pointer
@ -92,6 +107,16 @@ function eat_id() -> pointer {
function skipnl() -> void { while toks[pi].kind == TK_NL { pi += 1 } } function skipnl() -> void { while toks[pi].kind == TK_NL { pi += 1 } }
# a type: `[]T` slice, `fn(T, U) -> R` function, or a plain name (int/ptr/str/bool/struct) # a type: `[]T` slice, `fn(T, U) -> R` function, or a plain name (int/ptr/str/bool/struct)
# L7: the typed buffers are slices - a length, a bounds check on every index, and a place in the
# checker - so `floats` is `[]float`, `words` is `[]int`, and so on. `bytes()` stays raw.
function buffer_slice_ty(t: pointer) -> pointer {
if (t == "floats") { return "[]float" }
if (t == "words") { return "[]int" }
if (t == "fixeds") { return "[]fixed" }
if (t == "doubles") { return "[]double" }
if (t == "pointers") { return "[]pointer" }
return t
}
function ptype() -> pointer { function ptype() -> pointer {
if (toks[pi].text == "fn") and (toks[pi + 1].text == "(") { if (toks[pi].text == "fn") and (toks[pi + 1].text == "(") {
pi += 1 pi += 1
@ -125,7 +150,7 @@ function ptype() -> pointer {
} }
let tn = eat_id() let tn = eat_id()
if is_op("<") { return gen_type_args(tn) } if is_op("<") { return gen_type_args(tn) }
return tn return buffer_slice_ty(tn)
} }
# ---- expressions ----------------------------------------------------------- # ---- expressions -----------------------------------------------------------
@ -267,6 +292,7 @@ function p_primary() -> Node {
n.b = block() n.b = block()
return n return n
} }
if (t.text == "text_of") { g_uses_bytes = true }
let n = node(E_ID); n.s = t.text; pi += 1; return n let n = node(E_ID); n.s = t.text; pi += 1; return n
} }
if is_op("(") { pi += 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e } if is_op("(") { pi += 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
@ -279,6 +305,7 @@ function p_postfix() -> Node {
while true { while true {
if is_op(".") { pi += 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m if is_op(".") { pi += 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m
if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand
if e.a.kind == E_ID and e.a.s == "Fs" and (e.s == "read_bytes" or e.s == "write_bytes") { g_uses_bytes = true }
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand
if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand
@ -488,6 +515,12 @@ function stmt_body() -> Node {
pi += 1; n.a = expr() pi += 1; n.a = expr()
return n return n
} }
if (t.text == "unsafe") and (toks[pi + 1].text == "{") { # L7: raw memory allowed inside
let un = node(S_UNSAFE)
pi += 1
un.a = block()
return un
}
if (t.text == "break") { pi += 1; return node(S_BREAK) } if (t.text == "break") { pi += 1; return node(S_BREAK) }
if (t.text == "continue") { pi += 1; return node(S_CONTINUE) } if (t.text == "continue") { pi += 1; return node(S_CONTINUE) }
if (t.text == "cancel") { pi += 1; return node(S_CANCEL) } # veto a cancellable event if (t.text == "cancel") { pi += 1; return node(S_CANCEL) } # veto a cancellable event
@ -980,8 +1013,14 @@ function parse_one_decl() -> void {
if is_id("namespace") { parse_namespace(); return } # #76 namespace block if is_id("namespace") { parse_namespace(); return } # #76 namespace block
if is_id("var") { push(prog, parse_var()); return } if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return } if is_id("const") { push(prog, parse_const()); return }
var is_unsafe = false
if is_id("unsafe") and (toks[pi + 1].text == "function") { # L7: an unsafe function
pi += 1
is_unsafe = true
}
if is_id("function") { if is_id("function") {
let f = parse_fn() let f = parse_fn()
if is_unsafe { f.uns = 1 }
if is_det { push(g_det_names, f.s) } if is_det { push(g_det_names, f.s) }
if is_export { f.ival = 1 } if is_export { f.ival = 1 }
if (sys_phase != null) { push(g_mod_sys_fn, f.s); push(g_mod_sys_phase, sys_phase) } # #64: register at load if (sys_phase != null) { push(g_mod_sys_fn, f.s); push(g_mod_sys_phase, sys_phase) } # #64: register at load
@ -1111,6 +1150,7 @@ function do_import(rel: pointer) -> void {
# a module reaches as far as its own files: a package found through $LUDIC_HOME or # a module reaches as far as its own files: a package found through $LUDIC_HOME or
# ludic_modules is not beside its importer and keeps its own module (or none) # ludic_modules is not beside its importer and keeps its own module (or none)
let beside = full == join_path(cur_dir, rel) let beside = full == join_path(cur_dir, rel)
if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { push(g_trusted_files, full) }
if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) } if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
if (src == null) { perr(`cannot open import {full}`) } if (src == null) { perr(`cannot open import {full}`) }
# the audio runtime can arrive through atlas.ludic's own import or the Assets # the audio runtime can arrive through atlas.ludic's own import or the Assets
@ -1138,6 +1178,9 @@ function maybe_splice_runtime() -> void {
# L6: the engine's namespaces that are aliases of runtime functions, declared in Ludic # L6: the engine's namespaces that are aliases of runtime functions, declared in Ludic
cur_dir = "" cur_dir = ""
do_import("runtime/native/namespaces.ludic") do_import("runtime/native/namespaces.ludic")
# L7: the byte buffer's API, when a program reads or writes bytes (it opens files through
# the asset pack, and a program that does not should not carry the pack's machinery)
if g_uses_bytes { do_import("runtime/native/bytes.ludic") }
cur_dir = saved cur_dir = saved
# a game (has systems/components) links the Ludic runtime. # a game (has systems/components) links the Ludic runtime.
if has_ecs() { if has_ecs() {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -147,6 +147,7 @@ entry {
else if a == "--windowed" { want = 1 } else if a == "--windowed" { want = 1 }
else if a == "--headless" { want = 2 } else if a == "--headless" { want = 2 }
else if a == "--emit-llvm" { emit_ir = true } else if a == "--emit-llvm" { emit_ir = true }
else if a == "--unsafe" { g_unsafe_all = true } # L7: this program's own files may write `unsafe`
else if a == "--emit-module" { g_emit_module = true; emit_ir = true } # issue #64: prebuilt binary module IR else if a == "--emit-module" { g_emit_module = true; emit_ir = true } # issue #64: prebuilt binary module IR
else if a == "--fmt" { fmt = true } else if a == "--fmt" { fmt = true }
else if a == "--save-temps" { save = true } else if a == "--save-temps" { save = true }

View file

@ -47,17 +47,17 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
var flags = "--windowed" var flags = "--windowed"
if mode == 2 { flags = "--headless" } if mode == 2 { flags = "--headless" }
if save { flags = flags + " --save-temps" } if save { flags = flags + " --save-temps" }
return shq(`{ludicc()} {flags}{title_flag()} {src} -o {out}`) return shq(`{ludicc()} {flags}{title_flag()}{unsafe_flag()} {src} -o {out}`)
} }
if mode == 2 { if mode == 2 {
if not shq(`{ludicc()} --headless{title_flag()} {src} --emit-llvm -o {ll}`) { return false } if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false } if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) } if not save { shell(`rm -f {ll}`) }
return true return true
} }
if not shq(`{ludicc()} --windowed{title_flag()} {src} --emit-llvm -o {ll}`) { return false } if not shq(`{ludicc()} --windowed{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false }
# audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the # audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the
# canonical `ludicc -o` path links it only when Audio.* is used. # canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll` let cocoa = `{home}runtime/native/cocoa.ll`
@ -70,6 +70,12 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
# ` --title "<app name>"` when the project's package.ludic names the app, so the window # ` --title "<app name>"` when the project's package.ludic names the app, so the window
# opens under that name rather than the `program` name; "" otherwise, and for a name the # opens under that name rather than the `program` name; "" otherwise, and for a name the
# shell line could not carry safely. # shell line could not carry safely.
# ` --unsafe` when the build said so: the project's own files may write `unsafe` (L7)
var g_unsafe_build: bool = false
function unsafe_flag() -> pointer {
if g_unsafe_build { return " --unsafe" }
return ""
}
function title_flag() -> pointer { function title_flag() -> pointer {
let name = manifest_app(read_root_manifest(), "name") let name = manifest_app(read_root_manifest(), "name")
if name == "" { return "" } if name == "" { return "" }

View file

@ -275,6 +275,7 @@ function parse_build_args(start: int) -> pointer {
if a == "--headless" { g_mode = 2 } if a == "--headless" { g_mode = 2 }
else if a == "--windowed" { g_mode = 1 } else if a == "--windowed" { g_mode = 1 }
else if a == "--save-temps" { g_save = true } else if a == "--save-temps" { g_save = true }
else if a == "--unsafe" { g_unsafe_build = true }
else if a == "-o" { else if a == "-o" {
ai += 1 ai += 1
if ai < arg_count() { g_out = arg(ai) } if ai < arg_count() { g_out = arg(ai) }

View file

@ -66,6 +66,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/check/check_stmt.ludic") push(f, "selfhost/check/check_stmt.ludic")
push(f, "selfhost/check/check_gen.ludic") push(f, "selfhost/check/check_gen.ludic")
push(f, "selfhost/check/check_gen_call.ludic") push(f, "selfhost/check/check_gen_call.ludic")
push(f, "selfhost/check/check_unsafe.ludic")
push(f, "selfhost/backend/emit_stmt.ludic") push(f, "selfhost/backend/emit_stmt.ludic")
push(f, "selfhost/backend/game/emit_ecs.ludic") push(f, "selfhost/backend/game/emit_ecs.ludic")
push(f, "selfhost/backend/game/emit_query.ludic") push(f, "selfhost/backend/game/emit_query.ludic")
@ -116,7 +117,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
if not write_selfhost_src(src) { err("ludic-dev: cannot write build/selfhost.ludic\n"); return 1 } if not write_selfhost_src(src) { err("ludic-dev: cannot write build/selfhost.ludic\n"); return 1 }
let ll = `{outbin}.ll` let ll = `{outbin}.ll`
# say why when it fails: it used to exit 1 with nothing on the screen # say why when it fails: it used to exit 1 with nothing on the screen
if not shq(`{lc} {src} > {ll} 2>{tmp_dir()}/shb.err`) { if not shq(`{lc} --unsafe {src} > {ll} 2>{tmp_dir()}/shb.err`) {
shell(`rm -f {ll}`) shell(`rm -f {ll}`)
err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`)) err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`))
return 1 return 1
@ -142,9 +143,16 @@ function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
# the quiet core, reused by the test suites; returns true on success. On failure # the quiet core, reused by the test suites; returns true on success. On failure
# the self-host/link diagnostics are left in tmp_path("gb.err"). # the self-host/link diagnostics are left in tmp_path("gb.err").
# an example that is a raw-memory program on purpose (a decoder's ABI, a Vulkan demo) is built
# with --unsafe (L7); the suite sets this around those cases only
var g_case_unsafe: bool = false
function case_unsafe_flag() -> pointer {
if g_case_unsafe { return " --unsafe" }
return ""
}
function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool { function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
let ll = `{outbin}.ll` let ll = `{outbin}.ll`
if not shq(`{shbin} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false } if not shq(`{shbin}{case_unsafe_flag()} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false }
if not shq(`{cc()} -O2 {ll}{threads_link_flags(ll)} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false } if not shq(`{cc()} -O2 {ll}{threads_link_flags(ll)} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false }
shell(`rm -f {ll}`) shell(`rm -f {ll}`)
return true return true
@ -168,11 +176,11 @@ function cmd_bootstrap() -> int {
if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 } if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 }
print(" stage0: bin/ludicc -> gen1 (self-host compiler)") print(" stage0: bin/ludicc -> gen1 (self-host compiler)")
if not shq("build/boot/gen1 build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 } if not shq("build/boot/gen1 --unsafe build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 }
if not shq(`{cc()} build/boot/gen2.ll -o build/boot/gen2 2>/dev/null`) { print("FAIL: gen2 assemble"); return 1 } if not shq(`{cc()} build/boot/gen2.ll -o build/boot/gen2 2>/dev/null`) { print("FAIL: gen2 assemble"); return 1 }
print(` stage1: gen1 -> gen2.ll ({line_count("build/boot/gen2.ll")} lines) -> gen2`) print(` stage1: gen1 -> gen2.ll ({line_count("build/boot/gen2.ll")} lines) -> gen2`)
if not shq("build/boot/gen2 build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 } if not shq("build/boot/gen2 --unsafe build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 }
print(` stage2: gen2 -> gen3.ll ({line_count("build/boot/gen3.ll")} lines)`) print(` stage2: gen2 -> gen3.ll ({line_count("build/boot/gen3.ll")} lines)`)
if shq("cmp -s build/boot/gen2.ll build/boot/gen3.ll") { if shq("cmp -s build/boot/gen2.ll build/boot/gen3.ll") {
@ -194,7 +202,7 @@ function cmd_bootstrap_cfree() -> int {
if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 } if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 }
print(" seed.ll --clang--> sh_seed (no C compiler used)") print(" seed.ll --clang--> sh_seed (no C compiler used)")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
if not shq("build/cfree/sh_seed build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 } if not shq("build/cfree/sh_seed --unsafe build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 }
print(` sh_seed compiles selfhost.ludic -> out.ll ({line_count("build/cfree/out.ll")} lines)`) print(` sh_seed compiles selfhost.ludic -> out.ll ({line_count("build/cfree/out.ll")} lines)`)
if shq("cmp -s build/cfree/out.ll selfhost/ludicc.seed.ll") { if shq("cmp -s build/cfree/out.ll selfhost/ludicc.seed.ll") {
print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler") print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler")
@ -214,17 +222,17 @@ function cmd_reseed() -> int {
shell("mkdir -p build/cfree") shell("mkdir -p build/cfree")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 } if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) { if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) {
if not shq("build/cfree/sh_old build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 } if not shq("build/cfree/sh_old --unsafe build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 }
if not shq(`{cc()} build/cfree/step1.ll -o build/cfree/sh_new`) { print("FAIL: assemble sh_new"); return 1 } if not shq(`{cc()} build/cfree/step1.ll -o build/cfree/sh_new`) { print("FAIL: assemble sh_new"); return 1 }
if not shq("build/cfree/sh_new build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 } if not shq("build/cfree/sh_new --unsafe build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
# and the Windows seed, from the same compiler: a Windows checkout bootstraps from it # and the Windows seed, from the same compiler: a Windows checkout bootstraps from it
if not shq("build/cfree/sh_new --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 } if not shq("build/cfree/sh_new --unsafe --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
} else { } else {
print("seed does not build; reseeding from bin/ludicc") print("seed does not build; reseeding from bin/ludicc")
ensure_ludicc() ensure_ludicc()
if cmd_selfhost_build("bin/ludicc", "build/cfree/sh_c") != 0 { print("FAIL: build from bin/ludicc"); return 1 } if cmd_selfhost_build("bin/ludicc", "build/cfree/sh_c") != 0 { print("FAIL: build from bin/ludicc"); return 1 }
if not shq("build/cfree/sh_c build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 } if not shq("build/cfree/sh_c --unsafe build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
if not shq("build/cfree/sh_c --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 } if not shq("build/cfree/sh_c --unsafe --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
} }
print(`reseeded: {line_count("selfhost/ludicc.seed.ll")} lines`) print(`reseeded: {line_count("selfhost/ludicc.seed.ll")} lines`)
return 0 return 0

View file

@ -7,7 +7,7 @@ function sh_case(name: pointer, exp: pointer) -> void {
let ll = `{tmp_dir()}/sh_{name}.ll` let ll = `{tmp_dir()}/sh_{name}.ll`
let er = `{tmp_dir()}/sh_{name}.err` let er = `{tmp_dir()}/sh_{name}.err`
let bn = `{tmp_dir()}/sh_{name}` let bn = `{tmp_dir()}/sh_{name}`
if not shq(`bin/ludicc selfhost/tests/{name}.ludic > {ll} 2>{er}`) { if not shq(`bin/ludicc --unsafe selfhost/tests/{name}.ludic > {ll} 2>{er}`) {
bad2(`{name}: self-host errored`, capture_line(`head -1 {er}`)); return bad2(`{name}: self-host errored`, capture_line(`head -1 {er}`)); return
} }
if not shq(`{cc()} {ll} -o {bn} 2>/dev/null`) { bad(`{name}: IR did not assemble`); return } if not shq(`{cc()} {ll} -o {bn} 2>/dev/null`) { bad(`{name}: IR did not assemble`); return }

View file

@ -477,7 +477,7 @@ function net_case(path: pointer, exp: pointer) -> void {
let nm = flat(path) let nm = flat(path)
let ll = `{tmp_dir()}/n_{nm}.ll` let ll = `{tmp_dir()}/n_{nm}.ll`
let log = `{tmp_dir()}/n_{nm}.out` let log = `{tmp_dir()}/n_{nm}.out`
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) { if not shq(`bin/ludicc{case_unsafe_flag()} --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
let t = capture_line(`tail -1 {log}`) let t = capture_line(`tail -1 {log}`)
bad2(path, `build ({t})`); return bad2(path, `build ({t})`); return
} }
@ -647,7 +647,9 @@ function cmd_dev_test() -> int {
feat_case("library/query", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "query.ludic (Query count/first/nearest/within — ECS spatial queries over the reflection ABI)") feat_case("library/query", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "query.ludic (Query count/first/nearest/within — ECS spatial queries over the reflection ABI)")
feat_case("library/reflect", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "reflect.ludic (Reflect prop/field enumeration + type + get/set/has/kind — runtime reflection over the world schema)") feat_case("library/reflect", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "reflect.ludic (Reflect prop/field enumeration + type + get/set/has/kind — runtime reflection over the world schema)")
feat_case("library/serialize", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "serialize.ludic (Value tree + Json encode/parse + Reflect.serialize/apply — bit-exact save/load; issue #44)") feat_case("library/serialize", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "serialize.ludic (Value tree + Json encode/parse + Reflect.serialize/apply — bit-exact save/load; issue #44)")
g_case_unsafe = true # it drives the decoders' raw buffers
feat_case("library/tiled_p0", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21", "tiled_p0.ludic (Tiled P0: Xml reader + Base64 decode/encode + gzip framing over inflate; issue #67)") feat_case("library/tiled_p0", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21", "tiled_p0.ludic (Tiled P0: Xml reader + Base64 decode/encode + gzip framing over inflate; issue #67)")
g_case_unsafe = false
feat_case("library/tiled_p05", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30", "tiled_p05.ludic (Tiled P0.5: TMX/TSX reader -> same Value-tree intermediate as TMJ; Kenney sampleMap.tmx; issue #68)") feat_case("library/tiled_p05", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30", "tiled_p05.ludic (Tiled P0.5: TMX/TSX reader -> same Value-tree intermediate as TMJ; Kenney sampleMap.tmx; issue #68)")
feat_case("library/tiled_p1", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p1.ludic (Tiled P1: rt_tmap model + GID resolver/flip decode + render; loads+draws Kenney map identically from .tmx and .tmj; issue #69)") feat_case("library/tiled_p1", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p1.ludic (Tiled P1: rt_tmap model + GID resolver/flip decode + render; loads+draws Kenney map identically from .tmx and .tmj; issue #69)")
feat_case("library/tiled_p2", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p2.ludic (Tiled P2: per-tile objectgroup / property-convention / collision-layer -> Solids feed; A* baseline; issue #70)") feat_case("library/tiled_p2", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p2.ludic (Tiled P2: per-tile objectgroup / property-convention / collision-layer -> Solids feed; A* baseline; issue #70)")
@ -700,8 +702,10 @@ function cmd_dev_test() -> int {
# asserted from its own `entry`. The transport is the compiler's built-in # asserted from its own `entry`. The transport is the compiler's built-in
# loopback, so a networked game runs with zero foreign code. # loopback, so a networked game runs with zero foreign code.
net_case("events/mod_events", "10 32 42") net_case("events/mod_events", "10 32 42")
g_case_unsafe = true # the transport ABI takes raw bytes
net_case("networking/net_echo", "4 10 20 30 42") net_case("networking/net_echo", "4 10 20 30 42")
net_case("networking/net_snapshot", "50 7 50") net_case("networking/net_snapshot", "50 7 50")
g_case_unsafe = false
net_case("networking/net_sync", "12 3 4 50 999") net_case("networking/net_sync", "12 3 4 50 999")
net_case("networking/net_owner", "-1 7 0 1") net_case("networking/net_owner", "-1 7 0 1")
net_case("networking/net_rpc", "0 8") net_case("networking/net_rpc", "0 8")

View file

@ -6,7 +6,7 @@
function build_tool(name: pointer, src: pointer) -> bool { function build_tool(name: pointer, src: pointer) -> bool {
let ll = `build/{name}.ll` let ll = `build/{name}.ll`
let out = `bin/{exe_name(name)}` let out = `bin/{exe_name(name)}`
if not shq(`bin/{exe_name("ludicc")} {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false } if not shq(`bin/{exe_name("ludicc")} --unsafe {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
# write to a temp then move, so a running bin/ludic can rebuild itself in place # write to a temp then move, so a running bin/ludic can rebuild itself in place
# (Windows lets a running executable be renamed, never replaced, so the old one # (Windows lets a running executable be renamed, never replaced, so the old one
# steps aside first) # steps aside first)

View file

@ -29,7 +29,7 @@ property VkMap { keys: pointers, vals: words, cap: int = 0, count: int = 0 }
function vkm_new() -> VkMap { function vkm_new() -> VkMap {
let m = new VkMap let m = new VkMap
m.cap = 16384 m.cap = 16384
m.keys = bytes(m.cap * 8) m.keys = pointers(m.cap)
for i in 0 .. m.cap { m.keys[i] = null } for i in 0 .. m.cap { m.keys[i] = null }
m.vals = words(m.cap) m.vals = words(m.cap)
return m return m
@ -45,7 +45,7 @@ function vkm_slot(m: VkMap, key: pointer) -> int {
function vkm_grow(m: VkMap) -> void { function vkm_grow(m: VkMap) -> void {
let ok = m.keys; let ov = m.vals; let oc = m.cap let ok = m.keys; let ov = m.vals; let oc = m.cap
m.cap = oc * 2 m.cap = oc * 2
m.keys = bytes(m.cap * 8) m.keys = pointers(m.cap)
for i in 0 .. m.cap { m.keys[i] = null } for i in 0 .. m.cap { m.keys[i] = null }
m.vals = words(m.cap) m.vals = words(m.cap)
m.count = 0 m.count = 0