Merge branch 'lang/foundations' into r3d/fog-wall
This commit is contained in:
commit
f8d811aa97
78 changed files with 43349 additions and 40047 deletions
8
changes/baked-textures.md
Normal file
8
changes/baked-textures.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Textures a game baked at build time are read before the PNG.** `png_decode` first takes
|
||||
`assets/baked/png/<path>.tex` (a game's `ludic bake` output: the samples, ready to upload), and a cut-out
|
||||
load (`tex_load_ex` with dilate) first takes `assets/baked/cutouts/<path>.bc7` (padded as `tex_dilate`
|
||||
pads, then BC7 with its mips) - so a boot decodes, pads and converts nothing it can take ready-made. Both
|
||||
are ludic.base's baked form, read by hand (baked_tex.ludic: the "LBAK" header, the key and version); a
|
||||
missing or stale one falls back to the PNG as before. `tex_load_dds_at` reads a .dds at an offset.
|
||||
8
changes/dilate-parallel.md
Normal file
8
changes/dilate-parallel.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
bump: patch
|
||||
type: performance
|
||||
**Cut-out edge padding runs on every core.** `tex_dilate`'s passes hand their rows, sixteen at a time, to
|
||||
`Job.parallel_for`: within a pass a row writes only its own still-masked texels and reads only
|
||||
neighbours the mask already let go, so the bytes are the ones the single-threaded loop made. The worker
|
||||
is handed plain buffers in a `DilateJob` and makes nothing. `tex_dilate_bytes` is the slice-taking
|
||||
form (safe_api.ludic), and `examples/rendering/dilate.ludic` holds the result against the old loop
|
||||
(prints DILATE OK). It was 206 ms of the main thread in a Maroon Lake boot.
|
||||
8
changes/inflate-context.md
Normal file
8
changes/inflate-context.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Inflate in a caller-owned context.** `ZInflate` holds everything one inflate works in - the bit reader,
|
||||
the RFC's length and distance tables, and every Huffman table and scratch list a block builds - made once
|
||||
by `z_inflate_new()` and rebuilt in place, so an inflate allocates nothing and each thread that inflates
|
||||
holds a context of its own. `z_inflate_in(z, src, len, out, cap)`, `z_uncompress_in` (zlib) and
|
||||
`z_gunzip_in` (gzip) take the context; `z_inflate` / `z_uncompress` / `z_gunzip` keep their signatures,
|
||||
working in the one context `RtInflateState` holds, so no caller changes.
|
||||
10
changes/zones.md
Normal file
10
changes/zones.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic.zones`: where a world is simulated.** A disc per player, `(x, z, r)` with `r = zones_radius(fog)`
|
||||
(the fog distance plus 40 m, never under 150 m, 900 m with no fog), overlapping discs merged into
|
||||
clusters with a bounding circle, and `zones_contains` / `zones_nearest` for everything that asks; fixed
|
||||
arrays, nothing made per frame. The packages that simulate now ask their world port whether a place is
|
||||
simulated, with a default of everywhere: `WildlifeWorld.active` (an animal outside is frozen - its
|
||||
state kept, no dice drawn), `NpcWorld.active` (a walker outside is frozen, and nobody is born outside),
|
||||
`VehicleWorld.active` (a moored boat outside is left where it lies) and `ThingsWorld.restocks` (a
|
||||
morning restocks only inside).
|
||||
27
examples/library/ui_first_show.ludic
Normal file
27
examples/library/ui_first_show.ludic
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# ui_first_show.ludic - a component shown for the first time once the fence is judging (a prompt's key
|
||||
# hint mid-walk) makes its instance's props and model: declared, once per instance, and the failing
|
||||
# fence lets it through; hidden and shown again, the instance is reused and nothing more is made:
|
||||
# bad 0 shown 3
|
||||
import "ludic.ui"
|
||||
import "ui_first_show_parts/Cell.ludic"
|
||||
import "ui_first_show_parts/Host.ludic"
|
||||
program UiFirstShow {
|
||||
numbers float
|
||||
function frame(ui_st: mut UiState) -> UiNode {
|
||||
let root: UiNode = ui_nodes(ui_st, "Host", null)
|
||||
ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0)
|
||||
return root
|
||||
}
|
||||
entry (ui_st: mut UiState) {
|
||||
var shown = 0
|
||||
for f in 0 .. 1400 {
|
||||
let root = frame(ui_st)
|
||||
if f == 700 or f == 900 or f == 1000 or f == 1100 or f == 1200 {
|
||||
ui_press(ui_st, root.children[0].children[0])
|
||||
if f != 900 and f != 1100 { shown += 1 }
|
||||
}
|
||||
Mem.frame()
|
||||
}
|
||||
print(`bad {Mem.bad_frames()} shown {shown}`)
|
||||
}
|
||||
}
|
||||
1
examples/library/ui_first_show_parts/Cell.lss
Normal file
1
examples/library/ui_first_show_parts/Cell.lss
Normal file
|
|
@ -0,0 +1 @@
|
|||
.cell { width: 100px; height: 40px }
|
||||
3
examples/library/ui_first_show_parts/Cell.ludic
Normal file
3
examples/library/ui_first_show_parts/Cell.ludic
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
component Cell {
|
||||
prop caption: string = ""
|
||||
}
|
||||
3
examples/library/ui_first_show_parts/Cell.xml
Normal file
3
examples/library/ui_first_show_parts/Cell.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div class="item">
|
||||
<button class="cell" on-click="emit click"><p>{caption}</p></button>
|
||||
</div>
|
||||
1
examples/library/ui_first_show_parts/Host.lss
Normal file
1
examples/library/ui_first_show_parts/Host.lss
Normal file
|
|
@ -0,0 +1 @@
|
|||
.t { width: 60px; height: 30px }
|
||||
4
examples/library/ui_first_show_parts/Host.ludic
Normal file
4
examples/library/ui_first_show_parts/Host.ludic
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
component Host {
|
||||
state on: bool = false
|
||||
on flip() { on = not on }
|
||||
}
|
||||
4
examples/library/ui_first_show_parts/Host.xml
Normal file
4
examples/library/ui_first_show_parts/Host.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div>
|
||||
<button class="t" on-click="flip()">t</button>
|
||||
<if test="{on}"><Cell caption="a"/></if>
|
||||
</div>
|
||||
55
examples/library/ui_select_disabled.ludic
Normal file
55
examples/library/ui_select_disabled.ludic
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# ui_select_disabled.ludic - <option disabled="{...}">: a value on a disabled option is set to the
|
||||
# nearest enabled one, the arrow toward disabled options is drawn disabled, and the arrows step past
|
||||
# them (a host's cap on the fog): clamped 2 next-on 0 next 0 prev 2
|
||||
import "ludic.ui"
|
||||
program UiSelectDisabled {
|
||||
numbers float
|
||||
state SelState { v: int = 4 }
|
||||
view V (sel_st: mut SelState) {
|
||||
v = sel_st.v
|
||||
on set(x: int) { sel_st.v = x }
|
||||
}
|
||||
const PAGE: string = "<ui><screen name='s'><select label='Fog' value='{v}' on-change='set(event.value)'><option value='{0}'>0</option><option value='{1}'>1</option><option value='{2}'>2</option><option value='{3}' disabled='{v >= 0}'>3</option><option value='{4}' disabled='{true}'>4</option></select></screen></ui>"
|
||||
function frame(ui_st: mut UiState, x: float, y: float, down: bool) -> UiNode {
|
||||
let i = new UiInput
|
||||
i.x = x
|
||||
i.y = y
|
||||
i.down = down
|
||||
ui_input(ui_st, i)
|
||||
ui_show(ui_st, "s", view_v(), 0.0, 0.0, 400.0, 400.0)
|
||||
let root: UiNode = ui_nodes(ui_st, "s", view_v())
|
||||
ui_place(ui_st, root, 0.0, 0.0, 400.0, 400.0)
|
||||
return root
|
||||
}
|
||||
function part(n: UiNode, cls: string) -> UiNode {
|
||||
for i in 0 .. len(n.children) {
|
||||
if List.contains(n.children[i].classes, cls) { return n.children[i] }
|
||||
let f = part(n.children[i], cls)
|
||||
if f != null { return f }
|
||||
}
|
||||
return null
|
||||
}
|
||||
function click(ui_st: mut UiState, cls: string) -> void {
|
||||
let root = frame(ui_st, -1.0, -1.0, false)
|
||||
let p = part(root, cls)
|
||||
let x = p.x + p.cw / 2.0
|
||||
let y = p.y + p.ch / 2.0
|
||||
frame(ui_st, x, y, false)
|
||||
frame(ui_st, x, y, true)
|
||||
frame(ui_st, x, y, false)
|
||||
frame(ui_st, x, y, false)
|
||||
}
|
||||
entry (ui_st: mut UiState, sel_st: SelState) {
|
||||
ui_backend(ui_st, new UiBackend)
|
||||
ui_load_text(ui_st, PAGE, "s.xml")
|
||||
frame(ui_st, -1.0, -1.0, false)
|
||||
let root = frame(ui_st, -1.0, -1.0, false)
|
||||
let clamped = sel_st.v
|
||||
let nx = part(root, "ui-next")
|
||||
let at_cap = `{nx.enabled}`
|
||||
click(ui_st, "ui-next")
|
||||
let wrapped = sel_st.v
|
||||
click(ui_st, "ui-prev")
|
||||
print(`clamped {clamped} next-on {at_cap} next {wrapped} prev {sel_st.v}`)
|
||||
}
|
||||
}
|
||||
77
examples/rendering/dilate.ludic
Normal file
77
examples/rendering/dilate.ludic
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# dilate.ludic - render3d's cut-out padding, now a pass's rows on every core, gives exactly the
|
||||
# bytes the one-thread version gave: the old loop is kept here as the oracle, over a patterned atlas
|
||||
#
|
||||
# bin/ludic build examples/rendering/dilate.ludic --headless && ./build/dilate_headless (prints DILATE OK)
|
||||
program Dilate {
|
||||
numbers float
|
||||
import "ludic.render3d/r3d.ludic"
|
||||
|
||||
# the dilate as it was, one row after another
|
||||
function oracle(px: []byte, w: int, h: int, c: int, thresh: int, passes: int) -> void {
|
||||
let mask = buffer(w * h)
|
||||
for i in 0 .. w * h { let o = i * c; if px[o] + px[o + 1] + px[o + 2] < thresh { mask[i] = 1 } else { mask[i] = 0 } }
|
||||
let next = buffer(w * h)
|
||||
for pass in 0 .. passes {
|
||||
for q in 0 .. w * h { next[q] = mask[q] }
|
||||
for y in 0 .. h {
|
||||
for x in 0 .. w {
|
||||
let k = y * w + x
|
||||
if mask[k] == 1 {
|
||||
var r = 0; var g = 0; var b = 0; var n = 0
|
||||
if x > 0 and mask[k - 1] == 0 { let o = (k - 1) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if x < w - 1 and mask[k + 1] == 0 { let o = (k + 1) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if y > 0 and mask[k - w] == 0 { let o = (k - w) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if y < h - 1 and mask[k + w] == 0 { let o = (k + w) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if n > 0 { let o = k * c; px[o] = r / n; px[o + 1] = g / n; px[o + 2] = b / n; next[k] = 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
for q in 0 .. w * h { mask[q] = next[q] }
|
||||
}
|
||||
}
|
||||
|
||||
# blades and blobs of colour on black, different in every row and column
|
||||
function atlas(w: int, h: int, c: int) -> []byte {
|
||||
let px = buffer(w * h * c)
|
||||
for y in 0 .. h {
|
||||
for x in 0 .. w {
|
||||
let o = (y * w + x) * c
|
||||
let lit = ((x * 7 + y * 13) % 29 < 6) or ((x / 9 + y / 11) % 5 == 0)
|
||||
for k in 0 .. c { px[o + k] = 0 }
|
||||
if lit {
|
||||
px[o] = (x * 3 + y) % 256
|
||||
px[o + 1] = (x + y * 5) % 256
|
||||
px[o + 2] = (x * y) % 256
|
||||
}
|
||||
if c == 4 { px[o + 3] = 255 }
|
||||
}
|
||||
}
|
||||
return px
|
||||
}
|
||||
|
||||
handler Boot(render3d_st: mut Render3dState) phase Start {
|
||||
var ok = true
|
||||
for c in 3 .. 5 {
|
||||
let w = 173
|
||||
let h = 101
|
||||
let a = atlas(w, h, c)
|
||||
let b = atlas(w, h, c)
|
||||
render3d_st.tex_w = w
|
||||
render3d_st.tex_h = h
|
||||
render3d_st.tex_channels = c
|
||||
render3d_st.tex_depth = 8
|
||||
tex_dilate_bytes(render3d_st, a, 60, 24)
|
||||
oracle(b, w, h, c, 60, 24)
|
||||
let orig = atlas(w, h, c)
|
||||
var same = 0
|
||||
var moved = 0
|
||||
for i in 0 .. w * h * c {
|
||||
if a[i] == b[i] { same += 1 }
|
||||
if a[i] != orig[i] { moved += 1 }
|
||||
}
|
||||
if same != w * h * c or moved == 0 { ok = false } # the same bytes, and the padding did pad
|
||||
}
|
||||
if ok { print("DILATE OK") } else { print("DILATE FAILED") }
|
||||
quit()
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
|
|||
| [ludic.crafting](ludic.crafting/README.md) | recipes: what goes in and out, where and how long, making, refunding, hold-to-make |
|
||||
| [ludic.effects](ludic.effects/README.md) | timed modifiers that run down in game time (a meal's warmth, a drink's legs) |
|
||||
| [ludic.fire](ludic.fire/README.md) | a camp fire: fuel, the rain on it, warmth at a distance, whether it will cook |
|
||||
| [ludic.zones](ludic.zones/README.md) | where the world is simulated: a disc per player from their fog, merged into clusters |
|
||||
| [ludic.fishing](ludic.fishing/README.md) | a rod at the water: cast, bite, the reel's fight, the landing; species as an open registry |
|
||||
| [ludic.hints](ludic.hints/README.md) | a rail of what is true now, most urgent first, each a card that teaches it and can be muted by key |
|
||||
| [ludic.i18n](ludic.i18n/README.md) | a game in any language: gettext `.po` files, patterns with holes, plurals, a mod folder, a font per language |
|
||||
|
|
|
|||
66
packages/ludic.base/baked.ludic
Normal file
66
packages/ludic.base/baked.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# baked.ludic - what is deterministic made at build time (`ludic bake`) by the game's own code, and
|
||||
# read at run time. Every baked file: a 32-byte header - "LBAK", the header's format, the bake's
|
||||
# generator version, the payload's offset, a hash of its inputs, the payload's length - its key, and
|
||||
# the payload, whose layout is the bake's own. The runtime checks the header, never the inputs:
|
||||
# `ludic bake --check` holds a baked file to its inputs at build time.
|
||||
export const BAKE_FORMAT: int = 1
|
||||
const BAKE_HEAD: int = 32
|
||||
|
||||
export state BakeState {
|
||||
said: []string = new []string # the keys a dev build has said it made at run time
|
||||
}
|
||||
|
||||
# a map's bake: assets/baked/maps/<map>/<file>, which ships in that map's own pack (.lmap), never the
|
||||
# game's; anything else: assets/baked/<file>, in the game's pack
|
||||
export function baked_path(map: string, file: string) -> string {
|
||||
if len(map) == 0 { return "assets/baked/" + file }
|
||||
return "assets/baked/maps/" + map + "/" + file
|
||||
}
|
||||
|
||||
# the payload of a baked file that is this bake at this version, or null (missing, not a baked file,
|
||||
# another format, another key or version: the caller makes it at run time)
|
||||
export function baked_open(path: string, key: string, version: int) -> []byte {
|
||||
let b = Fs.read_bytes(path)
|
||||
if b == null or len(b) < BAKE_HEAD { return null }
|
||||
let off = bk_head_ok(b, len(b), key, version)
|
||||
if off < 0 or long(off) + bk_u64(b, 24) > long(len(b)) { return null }
|
||||
return view(b, off, int(bk_u64(b, 24))) # the payload, in place: no copy of a large bake
|
||||
}
|
||||
|
||||
# the payload's offset when `b` (its first `have` bytes) is this bake's header and key, else -1
|
||||
function bk_head_ok(b: []byte, have: int, key: string, version: int) -> int {
|
||||
if have < BAKE_HEAD or b[0] != 76 or b[1] != 66 or b[2] != 65 or b[3] != 75 { return -1 } # "LBAK"
|
||||
if bk_u32(b, 4) != BAKE_FORMAT or bk_u32(b, 8) != version { return -1 }
|
||||
let off = bk_u32(b, 12)
|
||||
if off < BAKE_HEAD + len(key) + 1 or have < BAKE_HEAD + len(key) + 1 { return -1 }
|
||||
for i in 0 .. len(key) { if b[BAKE_HEAD + i] != key[i] { return -1 } }
|
||||
if b[BAKE_HEAD + len(key)] != 0 { return -1 }
|
||||
return off
|
||||
}
|
||||
|
||||
# a dev build's line, once a key: the bake is missing or stale, so it is made at run time
|
||||
export function bake_missing(bake_st: mut BakeState, key: string) -> void {
|
||||
for i in 0 .. len(bake_st.said) { if bake_st.said[i] == key { return } }
|
||||
push(bake_st.said, key)
|
||||
print(`bake: {key} missing or stale - made at run time (ludic bake)`)
|
||||
}
|
||||
|
||||
# a baked file written: its header, key and `n` bytes of payload; the directories made
|
||||
export function bake_write(path: string, key: string, version: int, inputs_hash: long, payload: []byte, n: int) -> bool {
|
||||
let off = BAKE_HEAD + ((len(key) + 1 + 7) / 8) * 8
|
||||
let b = buffer(off + n)
|
||||
b[0] = 76
|
||||
b[1] = 66
|
||||
b[2] = 65
|
||||
b[3] = 75
|
||||
bk_put32(b, 4, BAKE_FORMAT)
|
||||
bk_put32(b, 8, version)
|
||||
bk_put32(b, 12, off)
|
||||
bk_put64(b, 16, inputs_hash)
|
||||
bk_put64(b, 24, long(n))
|
||||
for i in 0 .. len(key) { b[BAKE_HEAD + i] = key[i] }
|
||||
for i in 0 .. n { b[off + i] = payload[i] }
|
||||
let slash = bk_last_slash(path)
|
||||
if slash > 0 { Fs.mkdir(path[0 .. slash]) }
|
||||
return Fs.write_bytes(path, b, off + n)
|
||||
}
|
||||
40
packages/ludic.base/baked_hash.ludic
Normal file
40
packages/ludic.base/baked_hash.ludic
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# baked_hash.ludic - a bake's inputs as one number, and the header's little-endian fields. The hash
|
||||
# is FNV-1a 64 over each input in order: its path's bytes, a 0, then its bytes (tools/bake/baked.py
|
||||
# and the check compute the same)
|
||||
const BK_FNV_OFFSET: long = -3750763034362895579 # 0xcbf29ce484222325
|
||||
const BK_FNV_PRIME: long = 1099511628211
|
||||
|
||||
# `inputs` space-separated, as a Bakes row lists them; a missing input hashes as its path alone
|
||||
export function bake_inputs_hash(inputs: string) -> long {
|
||||
var h = BK_FNV_OFFSET
|
||||
var a = 0
|
||||
while a < len(inputs) {
|
||||
while a < len(inputs) and inputs[a] == 32 { a += 1 }
|
||||
var e = a
|
||||
while e < len(inputs) and inputs[e] != 32 { e += 1 }
|
||||
if e > a {
|
||||
let p = inputs[a .. e]
|
||||
for i in 0 .. len(p) { h = bk_fnv(h, p[i]) }
|
||||
h = bk_fnv(h, 0)
|
||||
let b = Fs.read_bytes(p)
|
||||
if b != null { for i in 0 .. len(b) { h = bk_fnv(h, b[i]) } }
|
||||
}
|
||||
a = e
|
||||
}
|
||||
return h
|
||||
}
|
||||
function bk_fnv(h: long, c: int) -> long { return (h ^ long(c & 255)) * BK_FNV_PRIME }
|
||||
|
||||
function bk_u32(b: []byte, at: int) -> int { return (b[at] & 255) | ((b[at + 1] & 255) << 8) | ((b[at + 2] & 255) << 16) | ((b[at + 3] & 255) << 24) }
|
||||
function bk_u64(b: []byte, at: int) -> long { return long(bk_u32(b, at)) & 4294967295 | (long(bk_u32(b, at + 4)) << 32) }
|
||||
function bk_put32(b: []byte, at: int, v: int) -> void {
|
||||
for i in 0 .. 4 { b[at + i] = (v >> (i * 8)) & 255 }
|
||||
}
|
||||
function bk_put64(b: []byte, at: int, v: long) -> void {
|
||||
for i in 0 .. 8 { b[at + i] = int((v >> long(i * 8)) & 255) }
|
||||
}
|
||||
function bk_last_slash(p: string) -> int {
|
||||
var k = -1
|
||||
for i in 0 .. len(p) { if p[i] == 47 { k = i } }
|
||||
return k
|
||||
}
|
||||
72
packages/ludic.base/baked_stream.ludic
Normal file
72
packages/ludic.base/baked_stream.ludic
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# baked_stream.ludic - a large bake read a piece at a time (placements by chunk, terrain by tile): the
|
||||
# header checked once, the file kept open, and each read into the caller's own buffer at a payload
|
||||
# offset, so a ring streaming chunks in makes nothing per chunk. Pack-aware (file_open)
|
||||
export property BakedFile {
|
||||
f: pointer = null
|
||||
off: int = 0 # where the payload starts in the file
|
||||
n: long = 0 # the payload's length
|
||||
}
|
||||
|
||||
# this bake's file kept open for reading, or null (missing, or another format, key or version)
|
||||
@alloc_ok("once per baked file opened: its header and record")
|
||||
export function baked_open_range(path: string, key: string, version: int) -> BakedFile {
|
||||
let f = file_open(path, "rb")
|
||||
if f == null { return null }
|
||||
let want = 32 + len(key) + 1
|
||||
let h = buffer(want)
|
||||
if file_read(f, h, want) != want or bk_head_ok(h, want, key, version) < 0 {
|
||||
file_close(f)
|
||||
return null
|
||||
}
|
||||
let bf = new BakedFile
|
||||
bf.f = f
|
||||
bf.off = bk_u32(h, 12)
|
||||
bf.n = bk_u64(h, 24)
|
||||
return bf
|
||||
}
|
||||
|
||||
# `n` bytes from payload offset `at` into `into` (from its start): the count read, 0 on a failure;
|
||||
# held to the buffer and to the payload
|
||||
export function baked_read(bf: BakedFile, at: int, n: int, into: []byte) -> int {
|
||||
if bf == null or bf.f == null or at < 0 or n <= 0 { return 0 }
|
||||
var k = n
|
||||
if k > len(into) { k = len(into) }
|
||||
if long(at) + long(k) > bf.n { k = int(bf.n - long(at)) }
|
||||
if k <= 0 { return 0 }
|
||||
if file_seek(bf.f, bf.off + at, 0) != 0 { return 0 }
|
||||
return file_read(bf.f, into, k)
|
||||
}
|
||||
|
||||
export function baked_len(bf: BakedFile) -> long {
|
||||
if bf == null { return 0 }
|
||||
return bf.n
|
||||
}
|
||||
|
||||
export function baked_close(bf: BakedFile) -> void {
|
||||
if bf == null or bf.f == null { return }
|
||||
file_close(bf.f)
|
||||
bf.f = null
|
||||
}
|
||||
|
||||
# what a baked file says it is, its header alone read (the stale check: key, version, inputs hash)
|
||||
export property BakedHead {
|
||||
key: string = ""
|
||||
version: int = 0
|
||||
inputs: long = 0
|
||||
}
|
||||
@alloc_ok("the stale check: once a baked file")
|
||||
export function baked_head(path: string) -> BakedHead {
|
||||
let f = file_open(path, "rb")
|
||||
if f == null { return null }
|
||||
let h = buffer(288)
|
||||
let got = file_read(f, h, 288)
|
||||
file_close(f)
|
||||
if got < 32 or h[0] != 76 or h[1] != 66 or h[2] != 65 or h[3] != 75 or bk_u32(h, 4) != BAKE_FORMAT { return null }
|
||||
var e = 32
|
||||
while e < got and h[e] != 0 { e += 1 }
|
||||
let r = new BakedHead
|
||||
r.key = intern(text_of(view(h, 32, e - 32), e - 32))
|
||||
r.version = bk_u32(h, 8)
|
||||
r.inputs = bk_u64(h, 16)
|
||||
return r
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ import "queue_ring.ludic"
|
|||
import "text_buf.ludic"
|
||||
import "text_intern.ludic"
|
||||
import "text_ring.ludic"
|
||||
import "baked.ludic"
|
||||
import "baked_hash.ludic"
|
||||
import "baked_stream.ludic"
|
||||
import "rng.ludic"
|
||||
import "save.ludic"
|
||||
import "save_fields.ludic"
|
||||
|
|
|
|||
47
packages/ludic.base/tests/baked_test.ludic
Normal file
47
packages/ludic.base/tests/baked_test.ludic
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# baked_test.ludic - a baked file round-trips its payload; another version or key reads as null; the
|
||||
# inputs hash is FNV-1a 64 over path, 0 and bytes (tools/bake/baked.py computes the same)
|
||||
import "ludic.base"
|
||||
program BakedTest {
|
||||
function dir() -> string { return Os.temp_dir() + "/ludic-baked-test" }
|
||||
test "a baked file's payload comes back, for its key and version only" {
|
||||
let p = buffer(5)
|
||||
for i in 0 .. 5 { p[i] = 65 + i }
|
||||
let path = dir() + "/a/b.bin"
|
||||
expect(bake_write(path, "tiles", 3, 42, p, 5))
|
||||
let b = baked_open(path, "tiles", 3)
|
||||
expect(b != null)
|
||||
expect_eq(len(b), 5)
|
||||
expect_eq(b[4], 69)
|
||||
expect(baked_open(path, "tiles", 4) == null)
|
||||
expect(baked_open(path, "tile", 3) == null)
|
||||
expect(baked_open(dir() + "/none.bin", "tiles", 3) == null)
|
||||
}
|
||||
test "a streamed bake reads a piece of its payload at an offset into the caller's buffer" {
|
||||
let p = buffer(10)
|
||||
for i in 0 .. 10 { p[i] = 48 + i }
|
||||
let path = dir() + "/s.bin"
|
||||
expect(bake_write(path, "chunks", 1, 0, p, 10))
|
||||
let bf = baked_open_range(path, "chunks", 1)
|
||||
expect(bf != null)
|
||||
expect(baked_len(bf) == 10)
|
||||
let into = buffer(4)
|
||||
expect_eq(baked_read(bf, 6, 4, into), 4)
|
||||
expect_eq(into[0], 54)
|
||||
expect_eq(baked_read(bf, 8, 4, into), 2)
|
||||
baked_close(bf)
|
||||
expect(baked_open_range(path, "chunks", 2) == null)
|
||||
}
|
||||
|
||||
test "the inputs hash follows the path and the bytes" {
|
||||
Fs.write_text(dir() + "/in.txt", "abc")
|
||||
let h1 = bake_inputs_hash(dir() + "/in.txt")
|
||||
Fs.write_text(dir() + "/in.txt", "abd")
|
||||
expect(bake_inputs_hash(dir() + "/in.txt") != h1)
|
||||
expect(bake_inputs_hash("") == -3750763034362895579)
|
||||
}
|
||||
test "a dev build says a missing bake once" (bake_st: mut BakeState) {
|
||||
bake_missing(bake_st, "k")
|
||||
bake_missing(bake_st, "k")
|
||||
expect_eq(len(bake_st.said), 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,3 +10,4 @@ import "build.ludic"
|
|||
import "query.ludic"
|
||||
import "crowd.ludic"
|
||||
import "resident.ludic"
|
||||
import "resident_count.ludic"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,23 @@ export function nav_tiles_keep(nav_st: NavState, kind: int, xs: []float, zs: []f
|
|||
}
|
||||
return changed
|
||||
}
|
||||
# the same ring by a distance the game answers: near(x, z) is how far (x, z) is from what must be
|
||||
# loaded (0 inside it). A tile comes in when its centre is within pad of that, and goes only past
|
||||
# pad + hyst, so a player at an edge does not read one in and out; how many changed
|
||||
export function nav_tiles_keep_near(nav_st: NavState, kind: int, near: fn(float, float) -> float, pad: float, hyst: float) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
let f = nav_st.nv_files[kind]
|
||||
if h == null or f == null { return 0 }
|
||||
var changed = 0
|
||||
for i in 0 .. nvc_index_count(h) {
|
||||
nvc_index_tile(h, i, nav_st.nv_tile)
|
||||
let m = near(nav_st.nv_tile[0], nav_st.nv_tile[1]) - pad
|
||||
let is_in = nav_st.nv_tile[2] > 0.5
|
||||
if is_in and m > hyst { changed += nvc_index_out(h, i) }
|
||||
if not is_in and m < 0.0 { changed += nv_tile_read(h, f, i) }
|
||||
}
|
||||
return changed
|
||||
}
|
||||
function nv_tile_read(h: pointer, f: pointer, i: int) -> int {
|
||||
let size = nvc_index_size(h, i)
|
||||
let buf = nvc_stage(size)
|
||||
|
|
@ -62,24 +79,3 @@ function nv_tile_read(h: pointer, f: pointer, i: int) -> int {
|
|||
}
|
||||
return nvc_index_in(h, i, buf, size)
|
||||
}
|
||||
|
||||
# how many of kind's tiles are in, and (nav_tiles_bytes) how many bytes those hold
|
||||
export function nav_tiles_in(nav_st: NavState, kind: int) -> int { return nv_tiles_sum(nav_st, kind, false) }
|
||||
export function nav_tiles_bytes(nav_st: NavState, kind: int) -> int { return nv_tiles_sum(nav_st, kind, true) }
|
||||
function nv_tiles_sum(nav_st: NavState, kind: int, bytes: bool) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return 0 }
|
||||
var n = 0
|
||||
for i in 0 .. nvc_index_count(h) {
|
||||
nvc_index_tile(h, i, nav_st.nv_tile)
|
||||
if nav_st.nv_tile[2] > 0.5 and bytes { n += int(nav_st.nv_tile[3]) }
|
||||
if nav_st.nv_tile[2] > 0.5 and not bytes { n += 1 }
|
||||
}
|
||||
return n
|
||||
}
|
||||
# how many tiles kind's file holds in all
|
||||
export function nav_tiles_all(nav_st: NavState, kind: int) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return 0 }
|
||||
return nvc_index_count(h)
|
||||
}
|
||||
|
|
|
|||
21
packages/ludic.nav/resident_count.ludic
Normal file
21
packages/ludic.nav/resident_count.ludic
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# resident_count.ludic - what a file-backed mesh holds now: its tiles in, their bytes, and how many it has
|
||||
# how many of kind's tiles are in, and (nav_tiles_bytes) how many bytes those hold
|
||||
export function nav_tiles_in(nav_st: NavState, kind: int) -> int { return nv_tiles_sum(nav_st, kind, false) }
|
||||
export function nav_tiles_bytes(nav_st: NavState, kind: int) -> int { return nv_tiles_sum(nav_st, kind, true) }
|
||||
function nv_tiles_sum(nav_st: NavState, kind: int, bytes: bool) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return 0 }
|
||||
var n = 0
|
||||
for i in 0 .. nvc_index_count(h) {
|
||||
nvc_index_tile(h, i, nav_st.nv_tile)
|
||||
if nav_st.nv_tile[2] > 0.5 and bytes { n += int(nav_st.nv_tile[3]) }
|
||||
if nav_st.nv_tile[2] > 0.5 and not bytes { n += 1 }
|
||||
}
|
||||
return n
|
||||
}
|
||||
# how many tiles kind's file holds in all
|
||||
export function nav_tiles_all(nav_st: NavState, kind: int) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return 0 }
|
||||
return nvc_index_count(h)
|
||||
}
|
||||
47
packages/ludic.nav/tests/keep_near_test.ludic
Normal file
47
packages/ludic.nav/tests/keep_near_test.ludic
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# keep_near_test.ludic - a file-backed mesh's tiles kept by a distance the game answers (its players'
|
||||
# zones): a zone round one corner brings in its tile only, growing it brings in its neighbours, an
|
||||
# edge nudged back and forth changes nothing, and a zone moved far away lets them all go
|
||||
import "ludic.nav"
|
||||
import "ludic.base"
|
||||
import "fakes/meadow.ludic"
|
||||
program KeepNearTest {
|
||||
numbers float
|
||||
|
||||
state NearState {
|
||||
x: float = 0.0
|
||||
z: float = 0.0
|
||||
r: float = 0.0
|
||||
}
|
||||
# metres from (x, z) to the zone's edge, 0 inside: what ludic.zones' zones_nearest answers
|
||||
function near(near_st: NearState, x: float, z: float) -> float {
|
||||
let d = Math.sqrt((x - near_st.x) * (x - near_st.x) + (z - near_st.z) * (z - near_st.z))
|
||||
return Math.max(d - near_st.r, 0.0)
|
||||
}
|
||||
function in_zone(nav_st: NavState) -> int { return nav_tiles_keep_near(nav_st, NAV_PERSON, fn near, 46.0, 200.0) }
|
||||
|
||||
test "tiles come in within the zone, an edge nudged back and forth changes nothing, and a far zone lets them go" (nav_st: mut NavState, near_st: mut NearState) {
|
||||
let g = ground(128, 0)
|
||||
expect(nav_tiled(nav_st, NAV_PERSON, 0.0, 0.0, 64.0, 16, 4096))
|
||||
for tz in 0 .. 2 { for tx in 0 .. 2 { expect(nav_tile_build(nav_st, NAV_PERSON, tx, tz, g, new NavConfig) > 0) } }
|
||||
let path = Os.temp_dir() + "/ludic_nav_near.navmesh"
|
||||
expect(nav_save_file(nav_st, NAV_PERSON, path))
|
||||
nav_reset(nav_st)
|
||||
expect(nav_load_index(nav_st, NAV_PERSON, path))
|
||||
near_st.x = 10.0
|
||||
near_st.z = 10.0
|
||||
near_st.r = 5.0
|
||||
expect_eq(in_zone(nav_st), 1)
|
||||
expect_eq(nav_tiles_in(nav_st, NAV_PERSON), 1)
|
||||
near_st.r = 80.0
|
||||
expect_eq(in_zone(nav_st), 3)
|
||||
expect_eq(nav_tiles_in(nav_st, NAV_PERSON), 4)
|
||||
for k in 0 .. 20 {
|
||||
near_st.r = 5.0 + float(k % 2) * 100.0
|
||||
expect_eq(in_zone(nav_st), 0)
|
||||
}
|
||||
near_st.x = 2000.0
|
||||
expect_eq(in_zone(nav_st), 4)
|
||||
expect_eq(nav_tiles_in(nav_st, NAV_PERSON), 0)
|
||||
nav_reset(nav_st)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ export function npc_spawn(npc_st: mut NpcState, kind: int) -> NpcPerson {
|
|||
let start = k.start
|
||||
start(p)
|
||||
} else { npc_start_near(npc_st, p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
|
||||
if not NpcWorld.active(p.x, p.z) { return null } # nobody is born outside every zone; the slot waits
|
||||
p.active = true
|
||||
npc_st.np_n += 1
|
||||
NpcWorld.born(p)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export port NpcWorld {
|
|||
water: fn(float, float) -> float = fn nw_dry # the water's surface there
|
||||
slope: fn(float, float) -> float = fn nw_zero2
|
||||
allowed: fn(float, float) -> bool = fn nw_yes2 # a person may stop here (not the player's camp)
|
||||
active: fn(float, float) -> bool = fn nw_yes2 # simulated here: outside, a walker is frozen and none is born
|
||||
# (x, z, r, feet, head): true when a still thing takes a body of radius r there; pushed_x / _z say where
|
||||
push: fn(float, float, float, float, float) -> bool = fn nw_no_push
|
||||
pushed_x: fn() -> float = fn nw_asked_x
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export function npc_tick(npc_st: mut NpcState, dt: float) -> void {
|
|||
np_census(npc_st)
|
||||
for i in 0 .. len(npc_st.np_walkers) {
|
||||
let p = npc_st.np_walkers[i]
|
||||
if not p.active or np_leaves(npc_st, p) { continue }
|
||||
if not p.active or not NpcWorld.active(p.x, p.z) or np_leaves(npc_st, p) { continue }
|
||||
p.goal_speed = 0.0
|
||||
if p.act == NPCA_WALK { npc_walk(npc_st, p, dt) } else { npc_act(npc_st, p, dt) }
|
||||
npc_notice(npc_st, p)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ export property PhysPose {
|
|||
function ph_add(physics_st: mut PhysicsState, shape: int, x: float, y: float, z: float, yaw: float, motion: int, layer: int, mass: float) -> int {
|
||||
let s = ph_shape(physics_st, shape)
|
||||
if physics_st.ph_world == null or s == null { return -1 }
|
||||
return jph_body_add(physics_st.ph_world, s, x, y, z, yaw, motion, layer, mass, 0.6, 0.1)
|
||||
let id = jph_body_add(physics_st.ph_world, s, x, y, z, yaw, motion, layer, mass, 0.6, 0.1)
|
||||
# 25.5a: a world full to the bodies it was opened for is a failure, never a thing quietly not there
|
||||
if id < 0 and jph_world_bodies(physics_st.ph_world) >= physics_st.ph_max { Mem.over("a physics body past phys_open's max_bodies") }
|
||||
return id
|
||||
}
|
||||
|
||||
# the ground: a heightfield or a mesh at the world's origin
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import "state.ludic"
|
|||
import "fact_pool.ludic"
|
||||
import "shape_ids.ludic"
|
||||
import "owned.ludic"
|
||||
import "world.ludic"
|
||||
import "shapes.ludic"
|
||||
import "bodies.ludic"
|
||||
import "queries.ludic"
|
||||
|
|
|
|||
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
# native.ludic - the shim's symbols (native/shim/jph_shim.cpp). A world and a shape are handles
|
||||
# the package keeps; a body is an int id. None of this is exported.
|
||||
extern function jph_world_new(max_bodies: int, threads: int) -> pointer = "jph_world_new"
|
||||
extern function jph_world_new(max_bodies: int, threads: int, max_pairs: int, max_contacts: int, temp_kb: int) -> pointer = "jph_world_new"
|
||||
extern function jph_world_free(w: pointer) -> void = "jph_world_free"
|
||||
extern function jph_world_gravity(w: pointer, y: float) -> void = "jph_world_gravity"
|
||||
extern function jph_world_step(w: pointer, dt: float, sub: int) -> int = "jph_world_step"
|
||||
|
|
|
|||
|
|
@ -93,11 +93,11 @@ public:
|
|||
|
||||
struct World {
|
||||
// pairs and contacts are for what MOVES: a valley of sixty thousand still trunks makes none
|
||||
// between them, so these are Jolt's own sizes, not the body count's; the temporary memory falls
|
||||
// back to the heap rather than aborting a step
|
||||
World(int max_bodies, int threads)
|
||||
: temp(32 * 1024 * 1024), jobs(cMaxPhysicsJobs, cMaxPhysicsBarriers, threads) {
|
||||
sys.Init(max_bodies, 0, 65536, 20480, bpl, ovb, olp);
|
||||
// between them, so they are sized for the moving things, not the body count; the temporary memory
|
||||
// falls back to the heap rather than aborting a step. All four are the package's (phys_open)
|
||||
World(int max_bodies, int threads, int max_pairs, int max_contacts, int temp_kb)
|
||||
: temp(size_t(temp_kb) * 1024), jobs(cMaxPhysicsJobs, cMaxPhysicsBarriers, threads) {
|
||||
sys.Init(max_bodies, 0, max_pairs, max_contacts, bpl, ovb, olp);
|
||||
sys.SetContactListener(&contacts);
|
||||
}
|
||||
TempAllocatorImplWithMallocFallback temp;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// jph_world.inl - a physics world: made, stepped, emptied of its contacts, freed
|
||||
|
||||
// a world for up to max_bodies; threads 0 means the machine's cores less one, at most four
|
||||
JPH_SHIM void *jph_world_new(int max_bodies, int threads) {
|
||||
// a world for up to max_bodies, max_pairs touching pairs and max_contacts contact constraints, with a
|
||||
// temp heap of temp_kb; threads 0 means the machine's cores less one, at most four
|
||||
JPH_SHIM void *jph_world_new(int max_bodies, int threads, int max_pairs, int max_contacts, int temp_kb) {
|
||||
if (jph_init() != 0) return nullptr;
|
||||
if (threads <= 0) {
|
||||
int hw = int(std::thread::hardware_concurrency()) - 1;
|
||||
threads = hw < 1 ? 1 : (hw > 4 ? 4 : hw);
|
||||
}
|
||||
World *w = new World(max_bodies, threads);
|
||||
World *w = new World(max_bodies, threads, max_pairs, max_contacts, temp_kb);
|
||||
w->sys.SetGravity(Vec3(0.0f, -9.81f, 0.0f));
|
||||
return w;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,5 @@
|
|||
# shapes.ludic - the world opened and closed, and shapes: made in C, kept here, named by an int.
|
||||
# A shape may be used by any number of bodies; phys_close frees them all.
|
||||
|
||||
# a world for up to max_bodies (threads 0: the cores less one, at most four); false if C refused
|
||||
export function phys_open(physics_st: mut PhysicsState, max_bodies: int, threads: int) -> bool {
|
||||
phys_close(physics_st)
|
||||
ph_buffers()
|
||||
physics_st.ph_world = jph_world_new(max_bodies, threads)
|
||||
ph_own_reset(physics_st, max_bodies)
|
||||
physics_st.ph_gen += 1
|
||||
physics_st.ph_acc = 0.0
|
||||
physics_st.ph_steps = 0
|
||||
return physics_st.ph_world != null
|
||||
}
|
||||
|
||||
export function phys_is_open(physics_st: PhysicsState) -> bool { return physics_st.ph_world != null }
|
||||
# which world this is: a body, shape or walker id kept from an older one means nothing now
|
||||
export function phys_generation(physics_st: PhysicsState) -> int { return physics_st.ph_gen }
|
||||
|
||||
# everything let go: the world, every body in it and every shape (a world swap, a quit); the lists
|
||||
# that held them are emptied and kept, since nothing is given back
|
||||
export function phys_close(physics_st: mut PhysicsState) -> void {
|
||||
if physics_st.ph_walkers != null {
|
||||
for i in 0 .. len(physics_st.ph_walkers) {
|
||||
if physics_st.ph_walkers[i] != null { jph_char_free(physics_st.ph_walkers[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_walkers != null { List.clear(physics_st.ph_walkers) }
|
||||
if physics_st.ph_world != null { jph_world_free(physics_st.ph_world) }
|
||||
physics_st.ph_world = null
|
||||
if physics_st.ph_shapes != null {
|
||||
for i in 0 .. len(physics_st.ph_shapes) {
|
||||
if physics_st.ph_shapes[i] != null { jph_shape_free(physics_st.ph_shapes[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_shapes != null { List.clear(physics_st.ph_shapes) }
|
||||
List.clear(physics_st.ph_spare)
|
||||
physics_st.ph_nf = 0
|
||||
if physics_st.ph_facts != null { q_clear(physics_st.ph_facts) }
|
||||
}
|
||||
# shapes.ludic - shapes: made in C, kept here, named by an int. A shape may be used by any number of
|
||||
# bodies; phys_close (world.ludic) frees them all.
|
||||
|
||||
# a crate: half extents in metres
|
||||
@creates(PhysShape)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export state PhysicsState {
|
|||
ph_step: float = 0.016666668 # seconds a step
|
||||
ph_sub: int = 1 # collision steps in each
|
||||
ph_max_steps: int = 4 # a slow frame catches up this far and no further
|
||||
ph_max: int = 0 # the bodies the world was opened for: one more is a failure, not a -1
|
||||
ph_acc: float = 0.0
|
||||
ph_steps: int = 0
|
||||
ph_gen: int = 0 # how many worlds have been opened: an id from an older one is stale
|
||||
|
|
@ -75,3 +76,9 @@ export function phys_config(physics_st: mut PhysicsState, step: float, sub: int,
|
|||
physics_st.ph_hard = hard
|
||||
}
|
||||
function ph_facts__new() -> Queue<PhysFact> { return queue_new("physics.facts") }
|
||||
|
||||
# what moves, sized: touching pairs, contact constraints and the step's temp heap (KB). A valley's still
|
||||
# things make no pairs between them; drops, boats and the walkers do. Past a size a step fails loudly
|
||||
export const PH_PAIRS: int = 4096
|
||||
export const PH_CONTACTS: int = 2048
|
||||
export const PH_TEMP_KB: int = 4096
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export function phys_step(physics_st: mut PhysicsState) -> void {
|
|||
if physics_st.ph_world == null { return }
|
||||
ph_buffers()
|
||||
ph_buoy_all(physics_st)
|
||||
jph_world_step(physics_st.ph_world, physics_st.ph_step, physics_st.ph_sub)
|
||||
# 25.5a: Jolt's pairs or contacts full (PH_PAIRS, PH_CONTACTS) is a failure: past it things pass through
|
||||
if jph_world_step(physics_st.ph_world, physics_st.ph_step, physics_st.ph_sub) != 0 { Mem.over("a physics step past phys_open's pairs or contacts") }
|
||||
physics_st.ph_steps += 1
|
||||
ph_contacts(physics_st)
|
||||
}
|
||||
|
|
|
|||
25
packages/ludic.physics/tests/jolt_floor_test.ludic
Normal file
25
packages/ludic.physics/tests/jolt_floor_test.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# jolt_floor_test.ludic - what an empty world costs in Jolt's own heap at the game's size (plan 25):
|
||||
# the floor under every body, from the body table, the broad phase, the contact caches and the temp heap
|
||||
import "ludic.physics"
|
||||
import "ludic.base"
|
||||
program JoltFloorTest {
|
||||
numbers float
|
||||
function floor_at(physics_st: mut PhysicsState, bodies: int) -> long {
|
||||
expect(phys_open(physics_st, 64, 1))
|
||||
phys_close(physics_st)
|
||||
let before = phys_heap_bytes()
|
||||
expect(phys_open(physics_st, bodies, 0))
|
||||
let f = phys_heap_bytes() - before
|
||||
phys_close(physics_st)
|
||||
return f
|
||||
}
|
||||
|
||||
# it was 51839 KB at 200000 bodies with 65536 pairs, 20480 contacts and a 32 MB temp heap
|
||||
test "an empty world at the game's size costs under 16 MB" (physics_st: mut PhysicsState) {
|
||||
let a = floor_at(physics_st, 200000)
|
||||
let b = floor_at(physics_st, 98304)
|
||||
print(`jolt floor: {a / long(1024)} KB at 200000 bodies, {b / long(1024)} KB at 98304`)
|
||||
expect(b < long(16 * 1024 * 1024))
|
||||
expect(a > b)
|
||||
}
|
||||
}
|
||||
43
packages/ludic.physics/world.ludic
Normal file
43
packages/ludic.physics/world.ludic
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# world.ludic - the world opened and closed: its sizes, and everything in it let go at once
|
||||
|
||||
# a world for up to max_bodies (threads 0: the cores less one, at most four); false if C refused.
|
||||
# Pairs, contacts and the temp heap are sized for the things that MOVE (PH_PAIRS, ...)
|
||||
export function phys_open(physics_st: mut PhysicsState, max_bodies: int, threads: int) -> bool { return phys_open_sized(physics_st, max_bodies, threads, PH_PAIRS, PH_CONTACTS, PH_TEMP_KB) }
|
||||
# the same with Jolt's other three sizes given: touching pairs, contact constraints, the temp heap in KB
|
||||
export function phys_open_sized(physics_st: mut PhysicsState, max_bodies: int, threads: int, max_pairs: int, max_contacts: int, temp_kb: int) -> bool {
|
||||
phys_close(physics_st)
|
||||
ph_buffers()
|
||||
physics_st.ph_world = jph_world_new(max_bodies, threads, max_pairs, max_contacts, temp_kb)
|
||||
physics_st.ph_max = max_bodies
|
||||
ph_own_reset(physics_st, max_bodies)
|
||||
physics_st.ph_gen += 1
|
||||
physics_st.ph_acc = 0.0
|
||||
physics_st.ph_steps = 0
|
||||
return physics_st.ph_world != null
|
||||
}
|
||||
|
||||
export function phys_is_open(physics_st: PhysicsState) -> bool { return physics_st.ph_world != null }
|
||||
# which world this is: a body, shape or walker id kept from an older one means nothing now
|
||||
export function phys_generation(physics_st: PhysicsState) -> int { return physics_st.ph_gen }
|
||||
|
||||
# everything let go: the world, every body in it and every shape (a world swap, a quit); the lists
|
||||
# that held them are emptied and kept, since nothing is given back
|
||||
export function phys_close(physics_st: mut PhysicsState) -> void {
|
||||
if physics_st.ph_walkers != null {
|
||||
for i in 0 .. len(physics_st.ph_walkers) {
|
||||
if physics_st.ph_walkers[i] != null { jph_char_free(physics_st.ph_walkers[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_walkers != null { List.clear(physics_st.ph_walkers) }
|
||||
if physics_st.ph_world != null { jph_world_free(physics_st.ph_world) }
|
||||
physics_st.ph_world = null
|
||||
if physics_st.ph_shapes != null {
|
||||
for i in 0 .. len(physics_st.ph_shapes) {
|
||||
if physics_st.ph_shapes[i] != null { jph_shape_free(physics_st.ph_shapes[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_shapes != null { List.clear(physics_st.ph_shapes) }
|
||||
List.clear(physics_st.ph_spare)
|
||||
physics_st.ph_nf = 0
|
||||
if physics_st.ph_facts != null { q_clear(physics_st.ph_facts) }
|
||||
}
|
||||
75
packages/ludic.render3d/baked_tex.ludic
Normal file
75
packages/ludic.render3d/baked_tex.ludic
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# baked_tex.ludic - textures a game baked at build time (`ludic bake`, assets/baked/), read before the PNG
|
||||
# they came from: a boot decodes, pads and converts nothing it can take ready-made. The file is ludic.base's
|
||||
# baked form (a 32-byte "LBAK" header, then the payload), read by hand here since render3d uses no package.
|
||||
# Missing, or of another key or version, and the PNG is decoded as it always was.
|
||||
# assets/baked/png/<path under assets/>.tex a PNG's samples: u32 w, h, channels, depth, then them
|
||||
# assets/baked/cutouts/<path under assets/>.bc7 a cut-out atlas, padded as tex_dilate pads it: a .dds
|
||||
const BAKED_PNG_KEY: string = "png_sheets" # the Bakes row (Maroon Lake's bakes.lres) and the key in its header
|
||||
const BAKED_PNG_VERSION: int = 1
|
||||
const BAKED_CUT_KEY: string = "cutouts"
|
||||
const BAKED_CUT_VERSION: int = 1
|
||||
|
||||
function baked_u32(b: []byte, o: int) -> int { return b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24) }
|
||||
|
||||
# where the bake of `path` (a PNG under assets/) lives, or "" for any other path
|
||||
@alloc_ok("a load: the baked file's name")
|
||||
function baked_tex_path(path: string, dir: string, ext: string) -> string {
|
||||
let n = len(path)
|
||||
if n < 12 or path[0 .. 7] != "assets/" or path[n - 4 .. n] != ".png" { return "" }
|
||||
return `assets/baked/{dir}/{path[7 .. n - 4]}{ext}`
|
||||
}
|
||||
|
||||
# the payload's offset in a baked file of this key and version, or -1 (not one, or another's)
|
||||
function baked_payload(b: []byte, key: string, version: int) -> int {
|
||||
if b == null or len(b) < 40 { return -1 }
|
||||
if b[0] != 76 or b[1] != 66 or b[2] != 65 or b[3] != 75 { return -1 } # "LBAK"
|
||||
if baked_u32(b, 4) != 1 or baked_u32(b, 8) != version { return -1 }
|
||||
let off = baked_u32(b, 12)
|
||||
if off < 32 + len(key) or off > len(b) { return -1 }
|
||||
for i in 0 .. len(key) { if b[32 + i] != key[i] { return -1 } }
|
||||
if b[32 + len(key)] != 0 { return -1 }
|
||||
return off
|
||||
}
|
||||
|
||||
# a PNG's samples as the bake left them (tex_w .. tex_depth set, the caller frees them), or null
|
||||
@alloc_ok("a load: the baked file read, its samples kept by the caller")
|
||||
function baked_png(render3d_st: mut Render3dState, path: pointer) -> pointer {
|
||||
let bp = baked_tex_path(path, "png", ".tex")
|
||||
if bp == "" or not Fs.exists(bp) { return null }
|
||||
let b = Fs.read_bytes(bp)
|
||||
let off = baked_payload(b, BAKED_PNG_KEY, BAKED_PNG_VERSION)
|
||||
if off < 0 or off + 16 > len(b) {
|
||||
if b != null { free(b) }
|
||||
print(`r3d: {bp} is not this build's bake; {path} is decoded`)
|
||||
return null
|
||||
}
|
||||
let w = baked_u32(b, off)
|
||||
let h = baked_u32(b, off + 4)
|
||||
let ch = baked_u32(b, off + 8)
|
||||
let depth = baked_u32(b, off + 12)
|
||||
let n = w * h * ch * (depth / 8)
|
||||
if w < 1 or h < 1 or ch < 1 or ch > 4 or (depth != 8 and depth != 16) or off + 16 + n > len(b) {
|
||||
free(b)
|
||||
return null
|
||||
}
|
||||
let out = bytes(n + 8)
|
||||
mem_copy(out, mem_off(data_of(b), off + 16), n)
|
||||
free(b)
|
||||
render3d_st.tex_w = w; render3d_st.tex_h = h; render3d_st.tex_channels = ch; render3d_st.tex_depth = depth
|
||||
return out
|
||||
}
|
||||
|
||||
# a cut-out atlas as the bake padded and compressed it (a texture id), or 0
|
||||
@alloc_ok("a load: the baked file read and let go")
|
||||
function baked_cutout(render3d_st: mut Render3dState, path: pointer, srgb: bool) -> int {
|
||||
if not render3d_st.gvk_has_bc { return 0 }
|
||||
let bp = baked_tex_path(path, "cutouts", ".bc7")
|
||||
if bp == "" or not Fs.exists(bp) { return 0 }
|
||||
let b = Fs.read_bytes(bp)
|
||||
let off = baked_payload(b, BAKED_CUT_KEY, BAKED_CUT_VERSION)
|
||||
var id = 0
|
||||
if off >= 0 { id = tex_load_dds_at(render3d_st, b, off, srgb) }
|
||||
if b != null { free(b) }
|
||||
if id == 0 { print(`r3d: {bp} is not this build's bake; {path} is padded here`) }
|
||||
return id
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import "prof.ludic"
|
|||
import "drawstats.ludic"
|
||||
import "programs.ludic"
|
||||
import "texture.ludic"
|
||||
import "baked_tex.ludic"
|
||||
import "mesh.ludic"
|
||||
import "gpu_vk_draw.ludic"
|
||||
import "camera.ludic"
|
||||
|
|
|
|||
|
|
@ -39,3 +39,8 @@ function png_decode_bytes(render3d_st: mut Render3dState, path: string, reuse: [
|
|||
function tex_upload_bytes(render3d_st: mut Render3dState, px: []byte, srgb: bool, mips: bool) -> int {
|
||||
return tex_upload(render3d_st, data_of(px), srgb, mips)
|
||||
}
|
||||
# cut-out edge padding over samples laid out as the last decode left them (tex_dilate)
|
||||
function tex_dilate_bytes(render3d_st: Render3dState, px: []byte, thresh: int, passes: int) -> void {
|
||||
if px == null or len(px) < render3d_st.tex_w * render3d_st.tex_h * render3d_st.tex_channels { return }
|
||||
tex_dilate(render3d_st, data_of(px), thresh, passes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@ function png_unfilter(raw: pointer, cur: int, prev: int, stride: int, fbpp: int,
|
|||
|
||||
@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)")
|
||||
function png_decode(render3d_st: mut Render3dState, path: pointer) -> pointer {
|
||||
let bk = baked_png(render3d_st, path) # a build that baked it (baked_tex.ludic)
|
||||
if bk != null { return bk }
|
||||
let d = r3d_read_file(render3d_st, path)
|
||||
if d == null { print(`png: cannot read {path}`); return null }
|
||||
let size = render3d_st.tex_file_len
|
||||
|
|
@ -198,30 +200,51 @@ function tex_dilate(render3d_st: Render3dState, px: pointer, thresh: int, passes
|
|||
var i = 0
|
||||
while i < w * h { let o = i * c; if px[o] + px[o + 1] + px[o + 2] < thresh { mask[i] = 1 } else { mask[i] = 0 }; i += 1 }
|
||||
let next = bytes(w * h)
|
||||
let j = new DilateJob
|
||||
j.px = px; j.mask = mask; j.next = next; j.w = w; j.h = h; j.c = c
|
||||
let blocks = (h + TEX_DILATE_ROWS - 1) / TEX_DILATE_ROWS
|
||||
for pass in 0 .. passes {
|
||||
mem_copy(next, mask, w * h)
|
||||
var y = 0
|
||||
while y < h {
|
||||
var x = 0
|
||||
while x < w {
|
||||
let k = y * w + x
|
||||
if mask[k] == 1 {
|
||||
var r = 0; var g = 0; var b = 0; var n = 0
|
||||
if x > 0 and mask[k - 1] == 0 { let o = (k - 1) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if x < w - 1 and mask[k + 1] == 0 { let o = (k + 1) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if y > 0 and mask[k - w] == 0 { let o = (k - w) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if y < h - 1 and mask[k + w] == 0 { let o = (k + w) * c; r += px[o]; g += px[o + 1]; b += px[o + 2]; n += 1 }
|
||||
if n > 0 { let o = k * c; px[o] = r / n; px[o + 1] = g / n; px[o + 2] = b / n; next[k] = 0 }
|
||||
}
|
||||
x += 1
|
||||
}
|
||||
y += 1
|
||||
}
|
||||
Job.parallel_for(blocks, fn tex_dilate_rows, j)
|
||||
mem_copy(mask, next, w * h)
|
||||
}
|
||||
free(mask); free(next)
|
||||
}
|
||||
|
||||
# A pass's rows on every core. Within a pass a row writes only its own still-masked texels and reads
|
||||
# only neighbours the mask already let go, which no row writes this pass - so rows are independent and
|
||||
# the result is the one row-by-row order gave. The worker is handed plain buffers and makes nothing.
|
||||
const TEX_DILATE_ROWS: int = 16
|
||||
property DilateJob {
|
||||
px: pointer = null
|
||||
mask: pointer = null
|
||||
next: pointer = null
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
c: int = 0
|
||||
}
|
||||
function tex_dilate_rows(b: int, j: DilateJob) -> void {
|
||||
let w = j.w; let c = j.c; let px = j.px; let mask = j.mask; let next = j.next
|
||||
var y = b * TEX_DILATE_ROWS
|
||||
let y1 = min(j.h, y + TEX_DILATE_ROWS)
|
||||
while y < y1 {
|
||||
var x = 0
|
||||
while x < w {
|
||||
let k = y * w + x
|
||||
if mask[k] == 1 {
|
||||
var r = 0; var g = 0; var bl = 0; var n = 0
|
||||
if x > 0 and mask[k - 1] == 0 { let o = (k - 1) * c; r += px[o]; g += px[o + 1]; bl += px[o + 2]; n += 1 }
|
||||
if x < w - 1 and mask[k + 1] == 0 { let o = (k + 1) * c; r += px[o]; g += px[o + 1]; bl += px[o + 2]; n += 1 }
|
||||
if y > 0 and mask[k - w] == 0 { let o = (k - w) * c; r += px[o]; g += px[o + 1]; bl += px[o + 2]; n += 1 }
|
||||
if y < j.h - 1 and mask[k + w] == 0 { let o = (k + w) * c; r += px[o]; g += px[o + 1]; bl += px[o + 2]; n += 1 }
|
||||
if n > 0 { let o = k * c; px[o] = r / n; px[o + 1] = g / n; px[o + 2] = bl / n; next[k] = 0 }
|
||||
}
|
||||
x += 1
|
||||
}
|
||||
y += 1
|
||||
}
|
||||
}
|
||||
|
||||
# Upload the last-decoded samples as a 2D texture. srgb: colour data (8-bit only).
|
||||
function tex_upload(render3d_st: mut Render3dState, px: pointer, srgb: bool, mips: bool) -> int {
|
||||
let id = gpu_tex_new(render3d_st)
|
||||
|
|
@ -289,6 +312,13 @@ function tex_load_ex__t(render3d_st: mut Render3dState, path: pointer, srgb: boo
|
|||
return t
|
||||
}
|
||||
}
|
||||
if dilate > 0 {
|
||||
let cut = baked_cutout(render3d_st, path, srgb) # padded and compressed at build time
|
||||
if cut != 0 {
|
||||
tex_note_size(render3d_st, cut)
|
||||
return cut
|
||||
}
|
||||
}
|
||||
let px = png_decode(render3d_st, path)
|
||||
if px == null { return 0 }
|
||||
if dilate > 0 { tex_dilate(render3d_st, px, 60, dilate) }
|
||||
|
|
|
|||
|
|
@ -44,14 +44,16 @@ function tex_load_dds_bytes(render3d_st: mut Render3dState, b: []byte, srgb: boo
|
|||
render3d_st.gvk_tag = was
|
||||
return r
|
||||
}
|
||||
function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb: bool) -> int {
|
||||
if len(b) < 148 or b[0] != 68 or b[1] != 68 or b[2] != 83 or b[3] != 32 { return 0 }
|
||||
if b[84] != 68 or b[85] != 88 or b[86] != 49 or b[87] != 48 { return 0 } # "DX10"
|
||||
let h = dds_u32(b, 12)
|
||||
let w = dds_u32(b, 16)
|
||||
var levels = dds_u32(b, 28)
|
||||
function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb: bool) -> int { return tex_load_dds_at(render3d_st, b, 0, srgb) }
|
||||
# a .dds that starts `at` bytes into b (a baked file's payload, baked_tex.ludic)
|
||||
function tex_load_dds_at(render3d_st: mut Render3dState, b: []byte, at0: int, srgb: bool) -> int {
|
||||
if len(b) < at0 + 148 or b[at0] != 68 or b[at0 + 1] != 68 or b[at0 + 2] != 83 or b[at0 + 3] != 32 { return 0 }
|
||||
if b[at0 + 84] != 68 or b[at0 + 85] != 88 or b[at0 + 86] != 49 or b[at0 + 87] != 48 { return 0 } # "DX10"
|
||||
let h = dds_u32(b, at0 + 12)
|
||||
let w = dds_u32(b, at0 + 16)
|
||||
var levels = dds_u32(b, at0 + 28)
|
||||
if levels < 1 { levels = 1 }
|
||||
let dxgi = dds_u32(b, 128)
|
||||
let dxgi = dds_u32(b, at0 + 128)
|
||||
var ifmt = 0
|
||||
var block = 16
|
||||
if dxgi == DDS_BC7 or dxgi == DDS_BC7_SRGB { ifmt = R3D_BC7; if srgb { ifmt = R3D_BC7_SRGB } }
|
||||
|
|
@ -65,13 +67,13 @@ function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb:
|
|||
at += max((max(w >> l, 1) + 3) / 4, 1) * max((max(h >> l, 1) + 3) / 4, 1) * block
|
||||
}
|
||||
offs[levels] = at
|
||||
if 148 + at > len(b) {
|
||||
if at0 + 148 + at > len(b) {
|
||||
free(offs)
|
||||
return 0
|
||||
}
|
||||
let id = gpu_tex_new(render3d_st)
|
||||
gpu_tex_bind(render3d_st, GPU_TEX2D, id)
|
||||
let ok = gpu_tex_compressed(render3d_st, ifmt, w, h, levels, mem_off(data_of(b), 148), offs)
|
||||
let ok = gpu_tex_compressed(render3d_st, ifmt, w, h, levels, mem_off(data_of(b), at0 + 148), offs)
|
||||
free(offs)
|
||||
if not ok {
|
||||
gpu_tex_free(render3d_st, id)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function things_restock(things_st: ThingsState) -> void {
|
|||
let all = things_all(things_st)
|
||||
for i in 0 .. len(all) {
|
||||
let t = all[i]
|
||||
if thing_is_put_down(t) { continue }
|
||||
if thing_is_put_down(t) or not ThingsWorld.restocks(t.x, t.z) { continue }
|
||||
if not ThingKinds[t.kind].keeps { t.used = 0 }
|
||||
if not t.active { thing_show(t) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export port ThingsWorld {
|
|||
placed: fn(Thing) -> void = fn th_nothing # a Thing exists now: draw it
|
||||
removed: fn(Thing) -> void = fn th_nothing # it is gone for good: stop drawing it
|
||||
shown: fn(Thing) -> void = fn th_nothing # its `active` changed: show or hide it
|
||||
restocks: fn(float, float) -> bool = fn th_yes2 # a morning restocks here (inside a zone)
|
||||
moved: fn(Thing) -> void = fn th_nothing # it stands somewhere else now
|
||||
keeps: fn(Thing) -> bool = fn th_kind_saves # the save's section carries it
|
||||
put_down: fn(Thing) -> bool = fn th_kind_put_down # set down by someone: a restock leaves it be
|
||||
|
|
@ -12,5 +13,6 @@ export port ThingsWorld {
|
|||
}
|
||||
function th_flat(x: float, z: float) -> float { return 0.0 }
|
||||
function th_nothing(t: Thing) -> void { }
|
||||
function th_yes2(x: float, z: float) -> bool { return true }
|
||||
function th_kind_saves(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].saved }
|
||||
function th_kind_put_down(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].put_down }
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export state UiState {
|
|||
nt_ev_at: int = 0
|
||||
nt_nums: []Val = nt_nums_new() # fired.ludic: the numbers a scroll or a slider reports, a ring
|
||||
nt_num_at: int = 0
|
||||
ct_clamp_n: []UiNode = new []UiNode # controls_opts.ludic: selects whose value a build clamped
|
||||
ct_clamp_i: []int = new []int
|
||||
hd_ev: Val = hd_ev_new() # hold.ludic: the held control's event, filled in place
|
||||
cv_texts: []string = new []string # units_pieces.ludic: a value's var() pieces, kept
|
||||
tx_pieces: [][]string = new [][]string # eval_text.ludic: a text's pieces, a list a depth
|
||||
|
|
|
|||
|
|
@ -5,17 +5,8 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
|
|||
let cls = k.cls
|
||||
uc_load(ui_st, cls)
|
||||
let it = in_get(ui_st, key, cls)
|
||||
if it.props == null { it.props = Value.object() }
|
||||
let props = it.props
|
||||
Value.clear(props)
|
||||
for i in 0 .. len(k.keys) {
|
||||
let a = k.keys[i]
|
||||
if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(ui_st, k.vals[i], e)) }
|
||||
}
|
||||
cls.props(it.ptr, props)
|
||||
let ce = env_new(ui_st, null)
|
||||
if it.model == null { it.model = Value.object() }
|
||||
ce.model = cls.model(it.ptr, it.model)
|
||||
if it.fresh { ce.model = bd_first_fill(ui_st, it, k, e) } else { ce.model = bd_fill(ui_st, it, k, e) }
|
||||
ce.inst = it
|
||||
ce.ctx = e.ctx
|
||||
ce.slot = k
|
||||
|
|
@ -34,6 +25,26 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
|
|||
bd_kids(ui_st, root, ce, key, out)
|
||||
if len(out) > at { bd_pass_down(ui_st, out[at], k, e) }
|
||||
}
|
||||
# an instance's props and model, filled in place into the objects it keeps (a reused one's are kept)
|
||||
function bd_fill(ui_st: mut UiState, it: UiInst, k: UiTpl, e: UiEnv) -> Val {
|
||||
if it.props == null { it.props = Value.object() }
|
||||
let props = it.props
|
||||
Value.clear(props)
|
||||
for i in 0 .. len(k.keys) {
|
||||
let a = k.keys[i]
|
||||
if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(ui_st, k.vals[i], e)) }
|
||||
}
|
||||
it.cls.props(it.ptr, props)
|
||||
if it.model == null { it.model = Value.object() }
|
||||
return it.cls.model(it.ptr, it.model)
|
||||
}
|
||||
# a new instance's first fill makes its props and model objects and every Value in them - once per
|
||||
# instance, which in_reuse bounds (a prompt's key hint first shown mid-walk was judged as a frame's keep)
|
||||
@alloc_ok("a new component instance's first build: its props and model, then kept or reused")
|
||||
function bd_first_fill(ui_st: mut UiState, it: UiInst, k: UiTpl, e: UiEnv) -> Val {
|
||||
it.fresh = false
|
||||
return bd_fill(ui_st, it, k, e)
|
||||
}
|
||||
# <Card class="wide" style="margin: 4px" id="c"> puts those on the card's own root element; when that
|
||||
# root is itself a component, each user in turn adds theirs (the outermost's id wins)
|
||||
function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ function ct_activate(ui_st: mut UiState, n: UiNode) -> void {
|
|||
function ct_step(ui_st: mut UiState, n: UiNode, d: int) -> void {
|
||||
let t = ct_type(n)
|
||||
if t == "select" and n.opts != null and len(n.opts) > 0 {
|
||||
let at = (ct_option_at(n, n.opts) + d + len(n.opts)) % len(n.opts)
|
||||
let ov = ui_attr(n.opts[at], "value")
|
||||
if Value.kind(ov) == 0 { ct_change(ui_st, n, Value.int(at)) } else { ct_change(ui_st, n, ov) }
|
||||
let from = ct_option_at(n, n.opts)
|
||||
let at = ct_opt_step(n.opts, from, d) # past a disabled option
|
||||
if at != from { ct_opt_pick(ui_st, n, n.opts, at) }
|
||||
}
|
||||
if t == "number" { ct_num_step(ui_st, n, d) }
|
||||
if t == "range" {
|
||||
|
|
|
|||
|
|
@ -67,21 +67,6 @@ function ct_build_range(ui_st: mut UiState, n: UiNode) -> void {
|
|||
ui_kept(n.children, track)
|
||||
ui_kept(n.children, ct_part(ui_st, n, "ui-value", ct_value_shown(ui_st, n)))
|
||||
}
|
||||
function ct_build_select(ui_st: mut UiState, n: UiNode) -> void {
|
||||
@alloc_ok("once per pooled node: its list is kept and cleared")
|
||||
if n.opts == null { n.opts = new []UiNode } # its options moved to a list the node keeps, not a new one
|
||||
let opts = n.opts
|
||||
List.clear(opts)
|
||||
for i in 0 .. len(n.children) { ui_kept(opts, n.children[i]) }
|
||||
List.clear(n.children)
|
||||
ct_label(ui_st, n)
|
||||
var shown = ""
|
||||
let at = ct_option_at(n, opts)
|
||||
if at >= 0 { shown = opts[at].text }
|
||||
ui_kept(n.children, ct_part(ui_st, n, "ui-prev", "<"))
|
||||
ui_kept(n.children, ct_part(ui_st, n, "ui-value", shown))
|
||||
ui_kept(n.children, ct_part(ui_st, n, "ui-next", ">"))
|
||||
}
|
||||
# which option the select's value names: an option's value="...", else its place
|
||||
function ct_option_at(n: UiNode, opts: []UiNode) -> int {
|
||||
let nv = ui_attr(n, "value")
|
||||
|
|
|
|||
76
packages/ludic.ui/controls_opts.ludic
Normal file
76
packages/ludic.ui/controls_opts.ludic
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# controls_opts.ludic - <option disabled="{...}">: a select steps past a disabled option (arrows, a
|
||||
# press, the pad), a value naming one is shown and set as the nearest enabled option, and the arrow
|
||||
# whose next option is disabled is drawn disabled - the options that way are off (a host's cap)
|
||||
function ct_opt_on(opts: []UiNode, i: int) -> bool { return i >= 0 and i < len(opts) and opts[i].enabled }
|
||||
|
||||
# from `at` d steps at a time, wrapping, to the first enabled option; `at` itself when none is
|
||||
function ct_opt_step(opts: []UiNode, at: int, d: int) -> int {
|
||||
let n = len(opts)
|
||||
var i = at
|
||||
for k in 0 .. n {
|
||||
i = (i + d + n) % n
|
||||
if opts[i].enabled { return i }
|
||||
}
|
||||
return at
|
||||
}
|
||||
|
||||
# the enabled option nearest `at` (the lower of two as near); -1 when none is
|
||||
function ct_opt_nearest(opts: []UiNode, at: int) -> int {
|
||||
if ct_opt_on(opts, at) { return at }
|
||||
for r in 1 .. len(opts) {
|
||||
if ct_opt_on(opts, at - r) { return at - r }
|
||||
if ct_opt_on(opts, at + r) { return at + r }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
# a select's value on a disabled option: the nearest enabled one is what it shows, and is set
|
||||
# (set once the build is done: an event fired while the tree is built is cleared with the frame's)
|
||||
function ct_opt_clamp(ui_st: mut UiState, n: UiNode, opts: []UiNode, at: int) -> int {
|
||||
if at < 0 or ct_opt_on(opts, at) { return at }
|
||||
let to = ct_opt_nearest(opts, at)
|
||||
if to >= 0 {
|
||||
ui_kept(ui_st.ct_clamp_n, n)
|
||||
ui_kept(ui_st.ct_clamp_i, to)
|
||||
}
|
||||
return to
|
||||
}
|
||||
# the clamps a build found, set now that the frame's events are cleared; stale ones are dropped
|
||||
function ct_opt_flush(ui_st: mut UiState) -> void {
|
||||
for i in 0 .. len(ui_st.ct_clamp_n) {
|
||||
let n = ui_st.ct_clamp_n[i]
|
||||
if n.opts != null and ui_st.ct_clamp_i[i] < len(n.opts) { ct_opt_pick(ui_st, n, n.opts, ui_st.ct_clamp_i[i]) }
|
||||
}
|
||||
ct_opt_forget(ui_st)
|
||||
}
|
||||
function ct_opt_forget(ui_st: mut UiState) -> void {
|
||||
List.clear(ui_st.ct_clamp_n)
|
||||
List.clear(ui_st.ct_clamp_i)
|
||||
}
|
||||
function ct_opt_pick(ui_st: mut UiState, n: UiNode, opts: []UiNode, at: int) -> void {
|
||||
let ov = ui_attr(opts[at], "value")
|
||||
if Value.kind(ov) == 0 { ct_change(ui_st, n, Value.int(at)) } else { ct_change(ui_st, n, ov) }
|
||||
}
|
||||
|
||||
# a select: its label, < and >, and the option its value names between them (<option>s kept aside)
|
||||
function ct_build_select(ui_st: mut UiState, n: UiNode) -> void {
|
||||
@alloc_ok("once per pooled node: its list is kept and cleared")
|
||||
if n.opts == null { n.opts = new []UiNode } # its options moved to a list the node keeps, not a new one
|
||||
let opts = n.opts
|
||||
List.clear(opts)
|
||||
for i in 0 .. len(n.children) { ui_kept(opts, n.children[i]) }
|
||||
List.clear(n.children)
|
||||
ct_label(ui_st, n)
|
||||
var shown = ""
|
||||
let at = ct_opt_clamp(ui_st, n, opts, ct_option_at(n, opts))
|
||||
if at >= 0 { shown = opts[at].text }
|
||||
let prev = ct_part(ui_st, n, "ui-prev", "<")
|
||||
let next = ct_part(ui_st, n, "ui-next", ">")
|
||||
if at >= 0 and len(opts) > 1 {
|
||||
prev.enabled = ct_opt_on(opts, (at - 1 + len(opts)) % len(opts))
|
||||
next.enabled = ct_opt_on(opts, (at + 1) % len(opts))
|
||||
}
|
||||
ui_kept(n.children, prev)
|
||||
ui_kept(n.children, ct_part(ui_st, n, "ui-value", shown))
|
||||
ui_kept(n.children, next)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
|
||||
in_read(ui_st)
|
||||
ct_opt_forget(ui_st) # a clamp found by a build this frame did not show
|
||||
dv_before(ui_st)
|
||||
ui_st.an_frame += 1
|
||||
ui_viewport(ui_st, w, h)
|
||||
|
|
@ -12,6 +13,7 @@ export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: flo
|
|||
if root == null { return }
|
||||
ui_place(ui_st, root, x, y, w, h)
|
||||
nt_fired_clear(ui_st)
|
||||
ct_opt_flush(ui_st) # a select's value on a disabled option, set now
|
||||
sc_undo(root)
|
||||
sc_update(ui_st, root)
|
||||
fr_pointer(ui_st, root)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import "fired.ludic"
|
|||
import "native_room.ludic"
|
||||
import "controls.ludic"
|
||||
import "controls_build.ludic"
|
||||
import "controls_opts.ludic"
|
||||
import "controls_parts.ludic"
|
||||
import "controls_key.ludic"
|
||||
import "controls_key_mouse.ludic"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ property UiInst {
|
|||
seen: int = 0
|
||||
props: Val = null # its props, filled again each frame into the same object
|
||||
model: Val = null # and its model, filled in place by the class's model()
|
||||
fresh: bool = false # made, not reused: its first build makes its props and model
|
||||
}
|
||||
# builds before an unmounted instance is mounted again: the tree that last named it is good for two
|
||||
const IN_SPARE_AFTER: int = 2
|
||||
|
|
@ -41,6 +42,7 @@ function in_reuse(ui_st: mut UiState, k: UiClass) -> UiInst {
|
|||
let it = new UiInst
|
||||
it.cls = k
|
||||
it.ptr = k.make()
|
||||
it.fresh = true
|
||||
return it
|
||||
}
|
||||
# after a build of `screen`: its instances the build did not reach are unmounted
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export port VehicleWorld {
|
|||
made: fn(Vehicle) -> void = fn ve__nothing # a new vehicle: draw it
|
||||
placed: fn(Vehicle) -> void = fn ve__nothing # it moved: put its model there
|
||||
gone: fn(Vehicle) -> void = fn ve__nothing # its owner left: take it away
|
||||
active: fn(float, float) -> bool = fn ve__yes2 # simulated here: outside, a moored boat is left as it is
|
||||
}
|
||||
|
||||
# a boat's hull in the world's physics (Maroon Lake: a buoyant Jolt body), named by the vehicle's
|
||||
|
|
@ -70,6 +71,7 @@ export state VehiclesState {
|
|||
function ve__dry(x: float, z: float) -> float { return -10000.0 }
|
||||
function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 }
|
||||
function ve__zero() -> float { return 0.0 }
|
||||
function ve__yes2(x: float, z: float) -> bool { return true }
|
||||
function ve__next(vehicles_st: mut VehiclesState) -> int {
|
||||
vehicles_st.ve_ids += 1
|
||||
return vehicles_st.ve_ids
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export function vehicles_tick(vehicles_st: VehiclesState, dt: float) -> void {
|
|||
if w > 0.0 { VehicleHull.drift(v.nid, -Math.sin(wy) * w, -Math.cos(wy) * w) }
|
||||
continue
|
||||
}
|
||||
if not VehicleWorld.active(v.x, v.z) { continue } # outside every zone: left where it lies
|
||||
ve_hull_read(v)
|
||||
ve_hold(v)
|
||||
VehicleWorld.placed(v)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export port WildlifeWorld {
|
|||
taught: fn() -> bool = fn wl_no0 # food carries further, feeders mind less
|
||||
about: fn(WildAnimal) -> int = fn wl_about # WILD_ABOUT, _AWAY or _GONE
|
||||
ranges: fn(int) -> int = fn wl_two # ranges a species gets on this map
|
||||
active: fn(float, float) -> bool = fn wl_yes2 # simulated here: outside, an animal is frozen
|
||||
per_range: fn(int) -> int = fn wl_three # and how many live in each
|
||||
spot: fn(WildAnimal, bool, WildSpot) -> bool = fn wl_no_spot # water (true) or forage for it
|
||||
lures: fn() -> int = fn wl_none0 # how many things to ask `lure` about
|
||||
|
|
@ -62,6 +63,7 @@ function wl_no0() -> bool { return false }
|
|||
function wl_no1(p: int) -> bool { return false }
|
||||
function wl_no2(x: float, z: float) -> bool { return false }
|
||||
function wl_yes1(u: int) -> bool { return true }
|
||||
function wl_yes2(x: float, z: float) -> bool { return true }
|
||||
function wl_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true }
|
||||
function wl_no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false }
|
||||
function wl_asked_x(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_ask_x }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# run.ludic - one step of every animal: the port says whether it is about at this hour; anything
|
||||
# within 900 m of a player thinks; the gait follows the ground covered; player 0 sees what is near
|
||||
# run.ludic - one step of every animal inside a zone (WildlifeWorld.active): the port says whether it
|
||||
# is about at this hour; within 900 m of a player it thinks; player 0 sees what is near
|
||||
export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: float) -> void {
|
||||
wl_seed_scan(wildlife_st, dt)
|
||||
let all = wildlife_all(wildlife_st)
|
||||
|
|
@ -9,6 +9,7 @@ export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: flo
|
|||
|
||||
function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void {
|
||||
if not a.alive { return }
|
||||
if not WildlifeWorld.active(a.x, a.z) { return } # outside every zone: its state kept, no dice drawn
|
||||
let about = WildlifeWorld.about(a)
|
||||
if about == WILD_AWAY {
|
||||
a.shown = false
|
||||
|
|
|
|||
16
packages/ludic.zones/README.md
Normal file
16
packages/ludic.zones/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# ludic.zones
|
||||
|
||||
Where a world is simulated: a disc per player - `(x, z, r)`, `r = zones_radius(fog)`, that is the
|
||||
fog distance plus 40 m, never under 150 m nor over 900 m, and 900 m with no fog - and the discs that overlap merged
|
||||
into clusters with a bounding circle. Uses `ludic.base` and nothing else.
|
||||
|
||||
- `zones_set(pid, x, z, r)`, `zones_clear(pid)`, `zones_reset()`; `zones_merge()` once a frame
|
||||
after the discs are set, for the clusters.
|
||||
- `zones_contains(x, z)`: inside any disc. `zones_nearest(x, z)`: metres to the nearest disc's
|
||||
edge, 0 inside. With no disc set, everywhere is inside (0) - a world before its first frame
|
||||
freezes nothing.
|
||||
- `zones_clusters()`, `zones_cluster_x / _z / _r / _n(c)`, `zones_cluster_of(pid)`.
|
||||
|
||||
The rules it keeps: fixed arrays of `ZONES_MAX` (16) slots, nothing made after the state; the
|
||||
questions allocate nothing. A world gates its ticks on `zones_contains` so what is outside every
|
||||
zone keeps its state and draws no dice.
|
||||
41
packages/ludic.zones/ask.ludic
Normal file
41
packages/ludic.zones/ask.ludic
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# ask.ludic - the questions: inside any disc, how far outside the nearest, and the clusters. With no
|
||||
# disc set yet (a world before its first frame) everywhere is inside, so nothing freezes by accident
|
||||
export function zones_any(zones_st: ZonesState) -> bool {
|
||||
for i in 0 .. ZONES_MAX { if zones_st.zn_on[i] != 0 { return true } }
|
||||
return false
|
||||
}
|
||||
|
||||
export function zones_contains(zones_st: ZonesState, x: float, z: float) -> bool {
|
||||
var any = false
|
||||
for i in 0 .. ZONES_MAX {
|
||||
if zones_st.zn_on[i] == 0 { continue }
|
||||
any = true
|
||||
let dx = x - zones_st.zn_x[i]
|
||||
let dz = z - zones_st.zn_z[i]
|
||||
if dx * dx + dz * dz <= zones_st.zn_r[i] * zones_st.zn_r[i] { return true }
|
||||
}
|
||||
return not any
|
||||
}
|
||||
|
||||
# metres from (x, z) to the edge of the nearest disc: 0 inside one (and 0 with no disc set)
|
||||
export function zones_nearest(zones_st: ZonesState, x: float, z: float) -> float {
|
||||
var best = -1.0
|
||||
for i in 0 .. ZONES_MAX {
|
||||
if zones_st.zn_on[i] == 0 { continue }
|
||||
let dx = x - zones_st.zn_x[i]
|
||||
let dz = z - zones_st.zn_z[i]
|
||||
let d = Math.max(Math.sqrt(dx * dx + dz * dz) - zones_st.zn_r[i], 0.0)
|
||||
if best < 0.0 or d < best { best = d }
|
||||
}
|
||||
if best < 0.0 { return 0.0 }
|
||||
return best
|
||||
}
|
||||
|
||||
export function zones_on(zones_st: ZonesState, pid: int) -> bool { return pid >= 0 and pid < ZONES_MAX and zones_st.zn_on[pid] != 0 }
|
||||
export function zones_r(zones_st: ZonesState, pid: int) -> float { return zones_st.zn_r[pid] }
|
||||
export function zones_cluster_of(zones_st: ZonesState, pid: int) -> int { return zones_st.zn_cl[pid] }
|
||||
export function zones_clusters(zones_st: ZonesState) -> int { return zones_st.zn_ncl }
|
||||
export function zones_cluster_x(zones_st: ZonesState, c: int) -> float { return zones_st.zn_cx[c] }
|
||||
export function zones_cluster_z(zones_st: ZonesState, c: int) -> float { return zones_st.zn_cz[c] }
|
||||
export function zones_cluster_r(zones_st: ZonesState, c: int) -> float { return zones_st.zn_cr[c] }
|
||||
export function zones_cluster_n(zones_st: ZonesState, c: int) -> int { return zones_st.zn_cn[c] }
|
||||
8
packages/ludic.zones/index.ludic
Normal file
8
packages/ludic.zones/index.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# ludic.zones - a disc per player (x, z, r), the discs that overlap merged into clusters with a
|
||||
# bounding circle, and the questions a world asks of them; fixed arrays, nothing made per frame
|
||||
module ludic_zones uses ludic_base
|
||||
numbers float
|
||||
import "ludic.base"
|
||||
import "state.ludic"
|
||||
import "merge.ludic"
|
||||
import "ask.ludic"
|
||||
70
packages/ludic.zones/merge.ludic
Normal file
70
packages/ludic.zones/merge.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# merge.ludic - discs that overlap (or touch) are one cluster, transitively; a cluster's bounding
|
||||
# circle is centred on its discs' box and reaches the farthest edge
|
||||
export function zones_merge(zones_st: mut ZonesState) -> void {
|
||||
for i in 0 .. ZONES_MAX { zones_st.zn_up[i] = i }
|
||||
for i in 0 .. ZONES_MAX {
|
||||
if zones_st.zn_on[i] == 0 { continue }
|
||||
for j in i + 1 .. ZONES_MAX { if zones_st.zn_on[j] != 0 and zn_touch(zones_st, i, j) { zn_join(zones_st, i, j) } }
|
||||
}
|
||||
zones_st.zn_ncl = 0
|
||||
for i in 0 .. ZONES_MAX { zones_st.zn_cl[i] = -1 }
|
||||
for i in 0 .. ZONES_MAX {
|
||||
if zones_st.zn_on[i] == 0 { continue }
|
||||
let root = zn_root(zones_st, i)
|
||||
if zones_st.zn_cl[root] < 0 {
|
||||
zones_st.zn_cl[root] = zones_st.zn_ncl
|
||||
zones_st.zn_ncl += 1
|
||||
}
|
||||
zones_st.zn_cl[i] = zones_st.zn_cl[root]
|
||||
}
|
||||
for c in 0 .. zones_st.zn_ncl { zn_bound(zones_st, c) }
|
||||
}
|
||||
|
||||
function zn_touch(zones_st: ZonesState, i: int, j: int) -> bool {
|
||||
let dx = zones_st.zn_x[i] - zones_st.zn_x[j]
|
||||
let dz = zones_st.zn_z[i] - zones_st.zn_z[j]
|
||||
let rr = zones_st.zn_r[i] + zones_st.zn_r[j]
|
||||
return dx * dx + dz * dz <= rr * rr
|
||||
}
|
||||
|
||||
function zn_root(zones_st: ZonesState, i: int) -> int {
|
||||
var k = i
|
||||
while zones_st.zn_up[k] != k { k = zones_st.zn_up[k] }
|
||||
return k
|
||||
}
|
||||
|
||||
function zn_join(zones_st: mut ZonesState, i: int, j: int) -> void {
|
||||
let a = zn_root(zones_st, i)
|
||||
let b = zn_root(zones_st, j)
|
||||
if a < b { zones_st.zn_up[b] = a }
|
||||
if b < a { zones_st.zn_up[a] = b }
|
||||
}
|
||||
|
||||
function zn_bound(zones_st: mut ZonesState, c: int) -> void {
|
||||
var x0 = 1000000000.0
|
||||
var x1 = -1000000000.0
|
||||
var z0 = 1000000000.0
|
||||
var z1 = -1000000000.0
|
||||
var n = 0
|
||||
for i in 0 .. ZONES_MAX {
|
||||
if zones_st.zn_on[i] == 0 or zones_st.zn_cl[i] != c { continue }
|
||||
x0 = Math.min(x0, zones_st.zn_x[i] - zones_st.zn_r[i])
|
||||
x1 = Math.max(x1, zones_st.zn_x[i] + zones_st.zn_r[i])
|
||||
z0 = Math.min(z0, zones_st.zn_z[i] - zones_st.zn_r[i])
|
||||
z1 = Math.max(z1, zones_st.zn_z[i] + zones_st.zn_r[i])
|
||||
n += 1
|
||||
}
|
||||
let cx = (x0 + x1) * 0.5
|
||||
let cz = (z0 + z1) * 0.5
|
||||
var r = 0.0
|
||||
for i in 0 .. ZONES_MAX {
|
||||
if zones_st.zn_on[i] == 0 or zones_st.zn_cl[i] != c { continue }
|
||||
let dx = zones_st.zn_x[i] - cx
|
||||
let dz = zones_st.zn_z[i] - cz
|
||||
r = Math.max(r, Math.sqrt(dx * dx + dz * dz) + zones_st.zn_r[i])
|
||||
}
|
||||
zones_st.zn_cx[c] = cx
|
||||
zones_st.zn_cz[c] = cz
|
||||
zones_st.zn_cr[c] = r
|
||||
zones_st.zn_cn[c] = n
|
||||
}
|
||||
5
packages/ludic.zones/package.ludic
Normal file
5
packages/ludic.zones/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# ludic.zones - where the world is simulated: a disc per player, overlapping discs merged into
|
||||
# clusters, asked whether a point is inside any. Uses ludic.base and nothing else.
|
||||
package "ludic.zones"
|
||||
version "0.1.0"
|
||||
kind source
|
||||
41
packages/ludic.zones/state.ludic
Normal file
41
packages/ludic.zones/state.ludic
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# state.ludic - the discs by player slot and the clusters the last merge made
|
||||
export const ZONES_MAX: int = 16 # player slots a world can hold (at least PLY_MAX)
|
||||
export const ZONES_NO_FOG: float = 900.0 # the radius of a player who sees to the horizon
|
||||
export const ZONES_PAST_FOG: float = 40.0 # simulated this far past what a player can see
|
||||
export const ZONES_LEAST: float = 150.0 # and never less than this
|
||||
|
||||
export state ZonesState {
|
||||
zn_on: words = words(ZONES_MAX)
|
||||
zn_x: []float = floats(ZONES_MAX)
|
||||
zn_z: []float = floats(ZONES_MAX)
|
||||
zn_r: []float = floats(ZONES_MAX)
|
||||
zn_cl: words = words(ZONES_MAX) # each disc's cluster, after zones_merge
|
||||
zn_up: words = words(ZONES_MAX) # the merge's union-find
|
||||
zn_ncl: int = 0
|
||||
zn_cx: []float = floats(ZONES_MAX) # each cluster's bounding circle
|
||||
zn_cz: []float = floats(ZONES_MAX)
|
||||
zn_cr: []float = floats(ZONES_MAX)
|
||||
zn_cn: words = words(ZONES_MAX) # how many discs it holds
|
||||
}
|
||||
|
||||
# a player's radius from how far they see: fog_dist <= 0 is no fog; never past no fog's, so a far fog
|
||||
# never keeps more of the world than none does
|
||||
export function zones_radius(fog_dist: float) -> float {
|
||||
if fog_dist <= 0.0 { return ZONES_NO_FOG }
|
||||
return Math.min(Math.max(fog_dist + ZONES_PAST_FOG, ZONES_LEAST), ZONES_NO_FOG)
|
||||
}
|
||||
|
||||
export function zones_set(zones_st: mut ZonesState, pid: int, x: float, z: float, r: float) -> void {
|
||||
if pid < 0 or pid >= ZONES_MAX { return }
|
||||
zones_st.zn_on[pid] = 1
|
||||
zones_st.zn_x[pid] = x
|
||||
zones_st.zn_z[pid] = z
|
||||
zones_st.zn_r[pid] = r
|
||||
}
|
||||
|
||||
export function zones_clear(zones_st: mut ZonesState, pid: int) -> void { if pid >= 0 and pid < ZONES_MAX { zones_st.zn_on[pid] = 0 } }
|
||||
|
||||
export function zones_reset(zones_st: mut ZonesState) -> void {
|
||||
for i in 0 .. ZONES_MAX { zones_st.zn_on[i] = 0 }
|
||||
zones_st.zn_ncl = 0
|
||||
}
|
||||
54
packages/ludic.zones/tests/zones_test.ludic
Normal file
54
packages/ludic.zones/tests/zones_test.ludic
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# zones_test.ludic - discs merged into clusters (near players one, far ones two, a player leaving
|
||||
# splits the chain), inside or not, how far to the nearest edge, and the radius a fog gives
|
||||
import "ludic.zones"
|
||||
import "ludic.base"
|
||||
program ZonesTest {
|
||||
numbers float
|
||||
|
||||
test "two near players are one cluster, two far apart are two" (zones_st: mut ZonesState) {
|
||||
zones_reset(zones_st)
|
||||
zones_set(zones_st, 0, 0.0, 0.0, 200.0)
|
||||
zones_set(zones_st, 1, 300.0, 0.0, 150.0)
|
||||
zones_merge(zones_st)
|
||||
expect_eq(zones_clusters(zones_st), 1)
|
||||
expect_eq(zones_cluster_of(zones_st, 0), zones_cluster_of(zones_st, 1))
|
||||
expect_eq(zones_cluster_n(zones_st, 0), 2)
|
||||
expect(zones_cluster_r(zones_st, 0) >= 325.0)
|
||||
zones_set(zones_st, 1, 1000.0, 0.0, 150.0)
|
||||
zones_merge(zones_st)
|
||||
expect_eq(zones_clusters(zones_st), 2)
|
||||
expect(zones_cluster_of(zones_st, 0) != zones_cluster_of(zones_st, 1))
|
||||
}
|
||||
|
||||
test "a player leaving the middle of a chain splits it" (zones_st: mut ZonesState) {
|
||||
zones_reset(zones_st)
|
||||
zones_set(zones_st, 0, 0.0, 0.0, 200.0)
|
||||
zones_set(zones_st, 3, 350.0, 0.0, 200.0)
|
||||
zones_set(zones_st, 5, 700.0, 0.0, 200.0)
|
||||
zones_merge(zones_st)
|
||||
expect_eq(zones_clusters(zones_st), 1)
|
||||
zones_clear(zones_st, 3)
|
||||
zones_merge(zones_st)
|
||||
expect_eq(zones_clusters(zones_st), 2)
|
||||
expect(zones_cluster_of(zones_st, 0) != zones_cluster_of(zones_st, 5))
|
||||
}
|
||||
|
||||
test "inside, the distance to the nearest edge, and nothing set is everywhere" (zones_st: mut ZonesState) {
|
||||
zones_reset(zones_st)
|
||||
expect(zones_contains(zones_st, 5000.0, 5000.0))
|
||||
expect(zones_nearest(zones_st, 5000.0, 5000.0) == 0.0)
|
||||
zones_set(zones_st, 2, 100.0, 100.0, 150.0)
|
||||
expect(zones_contains(zones_st, 200.0, 100.0))
|
||||
expect(not zones_contains(zones_st, 300.0, 100.0))
|
||||
expect(zones_nearest(zones_st, 200.0, 100.0) == 0.0)
|
||||
expect(Math.abs(zones_nearest(zones_st, 350.0, 100.0) - 100.0) < 0.001)
|
||||
}
|
||||
|
||||
test "a player's radius: past the fog, never under the least nor over no fog's" (zones_st: mut ZonesState) {
|
||||
expect(zones_radius(0.0) == ZONES_NO_FOG)
|
||||
expect(zones_radius(30.0) == ZONES_LEAST)
|
||||
expect(zones_radius(500.0) == 540.0)
|
||||
expect(zones_radius(1000.0) == ZONES_NO_FOG) # a kilometre's fog keeps no more than none
|
||||
expect(zones_radius(860.0) == ZONES_NO_FOG)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,54 +15,93 @@
|
|||
# megabytes of texture at load, and there the table is worth its 2 KB.
|
||||
# ============================================================================
|
||||
|
||||
# ---- bit reader (DEFLATE packs bits least-significant-first) ---------------
|
||||
export state RtInflateState {
|
||||
z_src: pointer = null
|
||||
z_len: int = 0
|
||||
z_pos: int = 0
|
||||
z_bitbuf: int = 0
|
||||
z_bitcnt: int = 0
|
||||
z_err: int = 0
|
||||
z_lbase: words = null
|
||||
z_lext: words = null
|
||||
z_dbase: words = null
|
||||
z_dext: words = null
|
||||
# ---- the context ------------------------------------------------------------
|
||||
# Everything one inflate works in: the bit reader, the RFC's length and distance tables, and every
|
||||
# table and scratch list a block builds, made once with the context (z_inflate_new) and rebuilt in
|
||||
# place - so an inflate allocates nothing, and each thread that inflates holds a context of its own.
|
||||
# RtInflateState keeps one for the callers that inflate on the program's own thread.
|
||||
export property ZInflate {
|
||||
src: pointer = null
|
||||
len: int = 0
|
||||
pos: int = 0
|
||||
bitbuf: int = 0
|
||||
bitcnt: int = 0
|
||||
err: int = 0
|
||||
lbase: words = null
|
||||
lext: words = null
|
||||
dbase: words = null
|
||||
dext: words = null
|
||||
lit: words = null
|
||||
dist: words = null
|
||||
clen: words = null
|
||||
lengths: words = null
|
||||
dl: words = null
|
||||
offs: words = null
|
||||
firstc: words = null
|
||||
}
|
||||
|
||||
function z_start(rt_inflate_st: mut RtInflateState, src: pointer, len: int) -> void {
|
||||
z_tables_once(rt_inflate_st)
|
||||
rt_inflate_st.z_src = src
|
||||
rt_inflate_st.z_len = len
|
||||
rt_inflate_st.z_pos = 0
|
||||
rt_inflate_st.z_bitbuf = 0
|
||||
rt_inflate_st.z_bitcnt = 0
|
||||
rt_inflate_st.z_err = 0
|
||||
export state RtInflateState {
|
||||
z_ctx: ZInflate = z_inflate_new()
|
||||
}
|
||||
|
||||
@alloc_ok("once per context: every table an inflate works in, rebuilt in place ever after")
|
||||
export function z_inflate_new() -> ZInflate {
|
||||
let z = new ZInflate
|
||||
z.lbase = words(29)
|
||||
z.lext = words(29)
|
||||
for s in 0 .. 29 {
|
||||
z.lbase[s] = z_len_base(s)
|
||||
z.lext[s] = z_len_extra(s)
|
||||
}
|
||||
z.dbase = words(30)
|
||||
z.dext = words(30)
|
||||
for s in 0 .. 30 {
|
||||
z.dbase[s] = z_dist_base(s)
|
||||
z.dext[s] = z_dist_extra(s)
|
||||
}
|
||||
z.lit = z_table_new(288)
|
||||
z.dist = z_table_new(30)
|
||||
z.clen = z_table_new(19)
|
||||
z.lengths = words(320)
|
||||
z.dl = words(32)
|
||||
z.offs = words(16)
|
||||
z.firstc = words(17)
|
||||
return z
|
||||
}
|
||||
|
||||
function z_start(z: ZInflate, src: pointer, len: int) -> void {
|
||||
z.src = src
|
||||
z.len = len
|
||||
z.pos = 0
|
||||
z.bitbuf = 0
|
||||
z.bitcnt = 0
|
||||
z.err = 0
|
||||
}
|
||||
|
||||
# Fill the bit buffer to at least `n` bits without consuming any (n <= 16, so the
|
||||
# buffer never shifts a byte past bit 15 and cannot reach the sign bit).
|
||||
function z_need(rt_inflate_st: mut RtInflateState, n: int) -> void {
|
||||
while rt_inflate_st.z_bitcnt < n {
|
||||
if rt_inflate_st.z_pos >= rt_inflate_st.z_len { return }
|
||||
rt_inflate_st.z_bitbuf = (rt_inflate_st.z_bitbuf | (rt_inflate_st.z_src[rt_inflate_st.z_pos] << rt_inflate_st.z_bitcnt))
|
||||
rt_inflate_st.z_pos += 1
|
||||
rt_inflate_st.z_bitcnt += 8
|
||||
function z_need(z: ZInflate, n: int) -> void {
|
||||
while z.bitcnt < n {
|
||||
if z.pos >= z.len { return }
|
||||
z.bitbuf = (z.bitbuf | (z.src[z.pos] << z.bitcnt))
|
||||
z.pos += 1
|
||||
z.bitcnt += 8
|
||||
}
|
||||
}
|
||||
|
||||
function z_bits(rt_inflate_st: mut RtInflateState, need: int) -> int {
|
||||
var val = rt_inflate_st.z_bitbuf
|
||||
while rt_inflate_st.z_bitcnt < need {
|
||||
if rt_inflate_st.z_pos >= rt_inflate_st.z_len {
|
||||
rt_inflate_st.z_err = 1
|
||||
function z_bits(z: ZInflate, need: int) -> int {
|
||||
var val = z.bitbuf
|
||||
while z.bitcnt < need {
|
||||
if z.pos >= z.len {
|
||||
z.err = 1
|
||||
return 0
|
||||
}
|
||||
val = (val | (rt_inflate_st.z_src[rt_inflate_st.z_pos] << rt_inflate_st.z_bitcnt))
|
||||
rt_inflate_st.z_pos += 1
|
||||
rt_inflate_st.z_bitcnt += 8
|
||||
val = (val | (z.src[z.pos] << z.bitcnt))
|
||||
z.pos += 1
|
||||
z.bitcnt += 8
|
||||
}
|
||||
rt_inflate_st.z_bitbuf = (val >> need)
|
||||
rt_inflate_st.z_bitcnt -= need
|
||||
z.bitbuf = (val >> need)
|
||||
z.bitcnt -= need
|
||||
return (val & ((1 << need) - 1))
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +120,7 @@ function z_table_new(nsym: int) -> words {
|
|||
}
|
||||
|
||||
# lengths[i] = code length of symbol i (0 = symbol unused)
|
||||
function z_table_build(table: words, lengths: words, n: int) -> void {
|
||||
function z_table_build(z: ZInflate, table: words, lengths: words, n: int) -> void {
|
||||
for i in 0 .. 16 {
|
||||
table[i] = 0
|
||||
}
|
||||
|
|
@ -91,7 +130,7 @@ function z_table_build(table: words, lengths: words, n: int) -> void {
|
|||
}
|
||||
table[0] = 0 # length 0 means "not present"
|
||||
# offset of each length's first symbol
|
||||
let offs: words = words(16)
|
||||
let offs = z.offs
|
||||
offs[1] = 0
|
||||
for l in 1 .. 15 {
|
||||
offs[l + 1] = offs[l] + table[l]
|
||||
|
|
@ -103,14 +142,13 @@ function z_table_build(table: words, lengths: words, n: int) -> void {
|
|||
offs[l] += 1
|
||||
}
|
||||
}
|
||||
free(offs)
|
||||
|
||||
# ---- the fast lookup ----
|
||||
for i in 0 .. Z_FASTSZ {
|
||||
table[16 + i] = 0
|
||||
}
|
||||
# first canonical code of each length
|
||||
let firstc: words = words(17)
|
||||
let firstc = z.firstc
|
||||
var code = 0
|
||||
for l in 1 .. 16 {
|
||||
code = ((code + table[l - 1]) << 1)
|
||||
|
|
@ -143,17 +181,16 @@ function z_table_build(table: words, lengths: words, n: int) -> void {
|
|||
k += 1
|
||||
}
|
||||
}
|
||||
free(firstc)
|
||||
}
|
||||
|
||||
function z_decode(rt_inflate_st: mut RtInflateState, table: words) -> int {
|
||||
z_need(rt_inflate_st, Z_FAST)
|
||||
if rt_inflate_st.z_bitcnt >= Z_FAST {
|
||||
let e = table[16 + (rt_inflate_st.z_bitbuf & (Z_FASTSZ - 1))]
|
||||
function z_decode(z: ZInflate, table: words) -> int {
|
||||
z_need(z, Z_FAST)
|
||||
if z.bitcnt >= Z_FAST {
|
||||
let e = table[16 + (z.bitbuf & (Z_FASTSZ - 1))]
|
||||
if e != 0 {
|
||||
let l = (e >> 16)
|
||||
rt_inflate_st.z_bitbuf = (rt_inflate_st.z_bitbuf >> l)
|
||||
rt_inflate_st.z_bitcnt -= l
|
||||
z.bitbuf = (z.bitbuf >> l)
|
||||
z.bitcnt -= l
|
||||
return (e & 65535)
|
||||
}
|
||||
}
|
||||
|
|
@ -162,7 +199,7 @@ function z_decode(rt_inflate_st: mut RtInflateState, table: words) -> int {
|
|||
var first = 0
|
||||
var index = 0
|
||||
for len in 1 .. 16 {
|
||||
code = (code | z_bits(rt_inflate_st, 1))
|
||||
code = (code | z_bits(z, 1))
|
||||
let count = table[len]
|
||||
if code - first < count {
|
||||
return table[Z_SYMS + index + (code - first)]
|
||||
|
|
@ -171,7 +208,7 @@ function z_decode(rt_inflate_st: mut RtInflateState, table: words) -> int {
|
|||
first = ((first + count) << 1)
|
||||
code = (code << 1)
|
||||
}
|
||||
rt_inflate_st.z_err = 1
|
||||
z.err = 1
|
||||
return -1
|
||||
}
|
||||
|
||||
|
|
@ -202,49 +239,33 @@ function z_dist_extra(sym: int) -> int {
|
|||
return (sym - 2) / 2
|
||||
}
|
||||
|
||||
# The RFC tables above are pure functions of the symbol; compute them once rather
|
||||
# than dividing per match.
|
||||
|
||||
function z_tables_once(rt_inflate_st: mut RtInflateState) -> void {
|
||||
if rt_inflate_st.z_lbase != null { return }
|
||||
rt_inflate_st.z_lbase = words(29)
|
||||
rt_inflate_st.z_lext = words(29)
|
||||
for s in 0 .. 29 {
|
||||
rt_inflate_st.z_lbase[s] = z_len_base(s)
|
||||
rt_inflate_st.z_lext[s] = z_len_extra(s)
|
||||
}
|
||||
rt_inflate_st.z_dbase = words(30)
|
||||
rt_inflate_st.z_dext = words(30)
|
||||
for s in 0 .. 30 {
|
||||
rt_inflate_st.z_dbase[s] = z_dist_base(s)
|
||||
rt_inflate_st.z_dext[s] = z_dist_extra(s)
|
||||
}
|
||||
}
|
||||
# The RFC tables above are pure functions of the symbol, computed once per context (z_inflate_new)
|
||||
# rather than divided per match.
|
||||
|
||||
# ---- block decoders -------------------------------------------------------
|
||||
# `out` is the destination window; returns the new write position, or -1.
|
||||
function z_stored(rt_inflate_st: mut RtInflateState, out: pointer, at: int, cap: int) -> int {
|
||||
rt_inflate_st.z_bitbuf = 0
|
||||
rt_inflate_st.z_bitcnt = 0 # stored blocks are byte-aligned
|
||||
if rt_inflate_st.z_pos + 4 > rt_inflate_st.z_len { return -1 }
|
||||
let n = rt_inflate_st.z_src[rt_inflate_st.z_pos] + (rt_inflate_st.z_src[rt_inflate_st.z_pos + 1] << 8)
|
||||
rt_inflate_st.z_pos += 4 # LEN then its one's complement
|
||||
function z_stored(z: ZInflate, out: pointer, at: int, cap: int) -> int {
|
||||
z.bitbuf = 0
|
||||
z.bitcnt = 0 # stored blocks are byte-aligned
|
||||
if z.pos + 4 > z.len { return -1 }
|
||||
let n = z.src[z.pos] + (z.src[z.pos + 1] << 8)
|
||||
z.pos += 4 # LEN then its one's complement
|
||||
var w = at
|
||||
for i in 0 .. n {
|
||||
if rt_inflate_st.z_pos >= rt_inflate_st.z_len { return -1 }
|
||||
if z.pos >= z.len { return -1 }
|
||||
if w >= cap { return -1 }
|
||||
out[w] = rt_inflate_st.z_src[rt_inflate_st.z_pos]
|
||||
out[w] = z.src[z.pos]
|
||||
w += 1
|
||||
rt_inflate_st.z_pos += 1
|
||||
z.pos += 1
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
function z_codes(rt_inflate_st: mut RtInflateState, out: pointer, at: int, cap: int, lit: words, dist: words) -> int {
|
||||
function z_codes(z: ZInflate, out: pointer, at: int, cap: int, lit: words, dist: words) -> int {
|
||||
var w = at
|
||||
var sym = z_decode(rt_inflate_st, lit)
|
||||
var sym = z_decode(z, lit)
|
||||
while sym != 256 {
|
||||
if rt_inflate_st.z_err != 0 { return -1 }
|
||||
if z.err != 0 { return -1 }
|
||||
if sym < 0 { return -1 }
|
||||
if sym < 256 {
|
||||
if w >= cap { return -1 }
|
||||
|
|
@ -254,11 +275,11 @@ function z_codes(rt_inflate_st: mut RtInflateState, out: pointer, at: int, cap:
|
|||
if sym > 256 {
|
||||
let s = sym - 257
|
||||
if s >= 29 { return -1 }
|
||||
let length = rt_inflate_st.z_lbase[s] + z_bits(rt_inflate_st, rt_inflate_st.z_lext[s])
|
||||
let d = z_decode(rt_inflate_st, dist)
|
||||
let length = z.lbase[s] + z_bits(z, z.lext[s])
|
||||
let d = z_decode(z, dist)
|
||||
if d < 0 { return -1 }
|
||||
if d >= 30 { return -1 }
|
||||
let distance = rt_inflate_st.z_dbase[d] + z_bits(rt_inflate_st, rt_inflate_st.z_dext[d])
|
||||
let distance = z.dbase[d] + z_bits(z, z.dext[d])
|
||||
if distance > w { return -1 }
|
||||
if w + length > cap { return -1 } # bounds once, not per byte
|
||||
var sp = w - distance
|
||||
|
|
@ -270,44 +291,43 @@ function z_codes(rt_inflate_st: mut RtInflateState, out: pointer, at: int, cap:
|
|||
k += 1
|
||||
}
|
||||
}
|
||||
sym = z_decode(rt_inflate_st, lit)
|
||||
sym = z_decode(z, lit)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
function z_fixed_tables(lit: words, dist: words) -> void {
|
||||
let lengths: words = words(288)
|
||||
function z_fixed_tables(z: ZInflate, lit: words, dist: words) -> void {
|
||||
let lengths = z.lengths
|
||||
for i in 0 .. 144 { lengths[i] = 8 }
|
||||
for i in 144 .. 256 { lengths[i] = 9 }
|
||||
for i in 256 .. 280 { lengths[i] = 7 }
|
||||
for i in 280 .. 288 { lengths[i] = 8 }
|
||||
z_table_build(lit, lengths, 288)
|
||||
z_table_build(z, lit, lengths, 288)
|
||||
for i in 0 .. 30 { lengths[i] = 5 }
|
||||
z_table_build(dist, lengths, 30)
|
||||
free(lengths)
|
||||
z_table_build(z, dist, lengths, 30)
|
||||
}
|
||||
|
||||
function z_dynamic_tables(rt_inflate_st: mut RtInflateState, lit: words, dist: words) -> int {
|
||||
let nlen = z_bits(rt_inflate_st, 5) + 257
|
||||
let ndist = z_bits(rt_inflate_st, 5) + 1
|
||||
let ncode = z_bits(rt_inflate_st, 4) + 4
|
||||
function z_dynamic_tables(z: ZInflate, lit: words, dist: words) -> int {
|
||||
let nlen = z_bits(z, 5) + 257
|
||||
let ndist = z_bits(z, 5) + 1
|
||||
let ncode = z_bits(z, 4) + 4
|
||||
if nlen > 286 { return 0 }
|
||||
if ndist > 30 { return 0 }
|
||||
|
||||
let lengths: words = words(320)
|
||||
let lengths = z.lengths
|
||||
for i in 0 .. 19 { lengths[i] = 0 }
|
||||
# the code-length alphabet is transmitted in this fixed permutation
|
||||
# 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 — biased by '0' so it is one literal
|
||||
let order = "@AB08796:5;4<3=2>1?"
|
||||
for i in 0 .. ncode {
|
||||
lengths[order[i] - 48] = z_bits(rt_inflate_st, 3)
|
||||
lengths[order[i] - 48] = z_bits(z, 3)
|
||||
}
|
||||
let clen = z_table_new(19)
|
||||
z_table_build(clen, lengths, 19)
|
||||
let clen = z.clen
|
||||
z_table_build(z, clen, lengths, 19)
|
||||
|
||||
var n = 0
|
||||
while n < nlen + ndist {
|
||||
let sym = z_decode(rt_inflate_st, clen)
|
||||
let sym = z_decode(z, clen)
|
||||
if sym < 0 { return 0 }
|
||||
if sym < 16 {
|
||||
lengths[n] = sym
|
||||
|
|
@ -319,10 +339,10 @@ function z_dynamic_tables(rt_inflate_st: mut RtInflateState, lit: words, dist: w
|
|||
if sym == 16 {
|
||||
if n == 0 { return 0 }
|
||||
prev = lengths[n - 1]
|
||||
rep = 3 + z_bits(rt_inflate_st, 2)
|
||||
rep = 3 + z_bits(z, 2)
|
||||
}
|
||||
if sym == 17 { rep = 3 + z_bits(rt_inflate_st, 3) }
|
||||
if sym == 18 { rep = 11 + z_bits(rt_inflate_st, 7) }
|
||||
if sym == 17 { rep = 3 + z_bits(z, 3) }
|
||||
if sym == 18 { rep = 11 + z_bits(z, 7) }
|
||||
for k in 0 .. rep {
|
||||
if n < 320 {
|
||||
lengths[n] = prev
|
||||
|
|
@ -331,51 +351,51 @@ function z_dynamic_tables(rt_inflate_st: mut RtInflateState, lit: words, dist: w
|
|||
}
|
||||
}
|
||||
}
|
||||
z_table_build(lit, lengths, nlen)
|
||||
z_table_build(z, lit, lengths, nlen)
|
||||
# the distance lengths follow the literal ones in the same buffer
|
||||
let dl: words = words(32)
|
||||
let dl = z.dl
|
||||
for i in 0 .. ndist { dl[i] = lengths[nlen + i] }
|
||||
z_table_build(dist, dl, ndist)
|
||||
free(dl)
|
||||
free(lengths)
|
||||
free(clen)
|
||||
z_table_build(z, dist, dl, ndist)
|
||||
return 1
|
||||
}
|
||||
|
||||
# Inflate a raw DEFLATE stream. Returns bytes written, or -1.
|
||||
function z_inflate(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int {
|
||||
z_start(rt_inflate_st, src, len)
|
||||
let lit = z_table_new(288)
|
||||
let dist = z_table_new(30)
|
||||
# Inflate a raw DEFLATE stream in context z. Returns bytes written, or -1.
|
||||
export function z_inflate_in(z: ZInflate, src: pointer, len: int, out: pointer, cap: int) -> int {
|
||||
z_start(z, src, len)
|
||||
let lit = z.lit
|
||||
let dist = z.dist
|
||||
var w = 0
|
||||
var final = 0
|
||||
while final == 0 {
|
||||
final = z_bits(rt_inflate_st, 1)
|
||||
let btype = z_bits(rt_inflate_st, 2)
|
||||
if rt_inflate_st.z_err != 0 { return -1 }
|
||||
if btype == 0 { w = z_stored(rt_inflate_st, out, w, cap) }
|
||||
final = z_bits(z, 1)
|
||||
let btype = z_bits(z, 2)
|
||||
if z.err != 0 { return -1 }
|
||||
if btype == 0 { w = z_stored(z, out, w, cap) }
|
||||
if btype == 1 {
|
||||
z_fixed_tables(lit, dist)
|
||||
w = z_codes(rt_inflate_st, out, w, cap, lit, dist)
|
||||
z_fixed_tables(z, lit, dist)
|
||||
w = z_codes(z, out, w, cap, lit, dist)
|
||||
}
|
||||
if btype == 2 {
|
||||
if z_dynamic_tables(rt_inflate_st, lit, dist) == 0 { return -1 }
|
||||
w = z_codes(rt_inflate_st, out, w, cap, lit, dist)
|
||||
if z_dynamic_tables(z, lit, dist) == 0 { return -1 }
|
||||
w = z_codes(z, out, w, cap, lit, dist)
|
||||
}
|
||||
if btype == 3 { return -1 }
|
||||
if w < 0 { return -1 }
|
||||
}
|
||||
free(lit)
|
||||
free(dist)
|
||||
return w
|
||||
}
|
||||
|
||||
# the program thread's inflate, in the context its state holds
|
||||
function z_inflate(rt_inflate_st: RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int { return z_inflate_in(rt_inflate_st.z_ctx, src, len, out, cap) }
|
||||
function z_uncompress(rt_inflate_st: RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int { return z_uncompress_in(rt_inflate_st.z_ctx, src, len, out, cap) }
|
||||
function z_gunzip(rt_inflate_st: RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int { return z_gunzip_in(rt_inflate_st.z_ctx, src, len, out, cap) }
|
||||
|
||||
# zlib wrapper (RFC 1950): two header bytes, then DEFLATE, then Adler-32.
|
||||
function z_uncompress(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int {
|
||||
export function z_uncompress_in(z: ZInflate, src: pointer, len: int, out: pointer, cap: int) -> int {
|
||||
if len < 2 { return -1 }
|
||||
let cmf = src[0]
|
||||
if (cmf & 15) != 8 { return -1 }
|
||||
return z_inflate(rt_inflate_st, offset(src, 2), len - 2, out, cap)
|
||||
return z_inflate_in(z, offset(src, 2), len - 2, out, cap)
|
||||
}
|
||||
|
||||
# gzip framing (RFC 1952): a 10-byte header (magic 1f 8b, CM=8, FLG, 4-byte MTIME,
|
||||
|
|
@ -384,7 +404,7 @@ function z_uncompress(rt_inflate_st: mut RtInflateState, src: pointer, len: int,
|
|||
# the header + optional fields, inflate the body, and ignore the trailer — the
|
||||
# CRC is a redundancy check, not needed to decode (PNG likewise ignores ancillary
|
||||
# CRCs). Returns bytes written, or -1.
|
||||
function z_gunzip(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int {
|
||||
export function z_gunzip_in(z: ZInflate, src: pointer, len: int, out: pointer, cap: int) -> int {
|
||||
if len < 18 { return -1 } # 10 header + 8 trailer minimum
|
||||
if src[0] != 31 { return -1 } # 0x1f
|
||||
if src[1] != 139 { return -1 } # 0x8b
|
||||
|
|
@ -406,5 +426,5 @@ function z_gunzip(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out
|
|||
}
|
||||
if (flg & 2) != 0 { pos += 2 } # FHCRC: 2-byte header CRC
|
||||
if pos + 8 > len { return -1 }
|
||||
return z_inflate(rt_inflate_st, offset(src, pos), len - pos - 8, out, cap)
|
||||
return z_inflate_in(z, offset(src, pos), len - pos - 8, out, cap)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ function is_fs_ns(meth: pointer) -> bool {
|
|||
if (meth == "exists") or (meth == "is_dir") or (meth == "read_text") { return true }
|
||||
if (meth == "write_text") or (meth == "append_text") or (meth == "remove") { return true }
|
||||
if (meth == "size") or (meth == "mkdir") or (meth == "copy") or (meth == "list") { return true }
|
||||
if (meth == "mount") or (meth == "unmount") { return true }
|
||||
return false
|
||||
}
|
||||
function is_path_ns(meth: pointer) -> bool {
|
||||
|
|
@ -55,6 +56,9 @@ function emit_fs_ns(meth: pointer, e: Node) -> Val {
|
|||
use_pak()
|
||||
let a = emit_expr(e.kids[0])
|
||||
if (meth == "exists") { return val(emit_bind(`call i32 @lp_fs_exists(ptr {a.code})`), "bool") }
|
||||
# a pack (.lpak / .lmap) mounted at run time over the packs.index ones, searched first; and let go
|
||||
if (meth == "mount") { return val(emit_bind(`call i32 @lp_pak_mount(ptr {a.code})`), "bool") }
|
||||
if (meth == "unmount") { return val(emit_bind(`call i32 @lp_pak_unmount(ptr {a.code})`), "bool") }
|
||||
if (meth == "is_dir") { return val(emit_bind(`call i32 @lp_fs_is_dir(ptr {a.code})`), "bool") }
|
||||
if (meth == "read_text") { return val(emit_bind(`call ptr @lp_fs_read_text(ptr {a.code})`), "string") }
|
||||
if (meth == "remove") { return val(emit_bind(`call i32 @lp_fs_remove(ptr {a.code})`), "bool") }
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ function emit_pak_prelude() -> void {
|
|||
emith("declare i64 @lseek(i32, i64, i32)\n")
|
||||
if not g_mmap_declared { emith("declare ptr @mmap(ptr, i64, i32, i32, i32, i64)\n"); g_mmap_declared = true }
|
||||
emith("declare ptr @fmemopen(ptr, i64, ptr)\n")
|
||||
emith("declare i32 @munmap(ptr, i64)\n")
|
||||
emith("declare i32 @chdir(ptr)\n")
|
||||
emith("declare i32 @_NSGetExecutablePath(ptr, ptr)\n")
|
||||
}
|
||||
|
|
@ -109,6 +110,8 @@ function emit_pak_prelude() -> void {
|
|||
# up to 8 packs may be mounted; the base pointer of each mapping, in mount order
|
||||
emith("@L_pak_base = internal global [8 x ptr] zeroinitializer\n")
|
||||
emith("@L_pak_n = internal global i32 0\n")
|
||||
emith("@L_pak_len = internal global [8 x i64] zeroinitializer\n") # each mapping's length, for unmount
|
||||
emith("@L_pak_path = internal global [8 x ptr] zeroinitializer\n") # and the path it was mounted from
|
||||
# the splash, named by packs.index and served out of the pack like any asset
|
||||
emith("@L_pak_splash = internal global ptr null\n")
|
||||
emith("@L_pak_splashbg = internal global i32 0\n")
|
||||
|
|
@ -128,6 +131,7 @@ function emit_pak_prelude() -> void {
|
|||
}
|
||||
|
||||
emit_pak_mount()
|
||||
emit_pak_unmount()
|
||||
emit_pak_lookup()
|
||||
emit_pak_open()
|
||||
emit_pak_boot()
|
||||
|
|
@ -157,11 +161,36 @@ function emit_pak_mount() -> void {
|
|||
# "LPAK" read as a little-endian i32
|
||||
emith("chk:\n %m = load i32, ptr %base\n %okm = icmp eq i32 %m, 1262571596\n br i1 %okm, label %keep, label %clo\n")
|
||||
emith("keep:\n %slot = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %n\n store ptr %base, ptr %slot\n")
|
||||
# its length and path too, so Fs.unmount can find it and give the mapping back
|
||||
emith(" %lslot = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %n\n store i64 %end, ptr %lslot\n")
|
||||
emith(" %pcopy = call ptr @lp_fs_strdup(ptr %path)\n %pslot = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %n\n store ptr %pcopy, ptr %pslot\n")
|
||||
emith(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @L_pak_n\n %c1 = call i32 @close(i32 %fd)\n ret i32 1\n")
|
||||
emith("clo:\n %c2 = call i32 @close(i32 %fd)\n ret i32 0\n")
|
||||
emith("no:\n ret i32 0\n}\n")
|
||||
}
|
||||
|
||||
# unmount(path): the pack mounted from exactly that path given back and taken out of the search, the
|
||||
# ones after it moving up a slot; 1 when it was mounted. A map pack downloaded at run time comes and
|
||||
# goes (Fs.mount / Fs.unmount); a FILE* still open over one of its entries must be closed first
|
||||
function emit_pak_unmount() -> void {
|
||||
emith("define i32 @lp_pak_unmount(ptr %path) {\n")
|
||||
emith("entry:\n %n = load i32, ptr @L_pak_n\n %ip = alloca i32\n store i32 0, ptr %ip\n br label %find\n")
|
||||
emith("find:\n %i = load i32, ptr %ip\n %end = icmp sge i32 %i, %n\n br i1 %end, label %no, label %cmp\n")
|
||||
emith("cmp:\n %pp = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %i\n %p = load ptr, ptr %pp\n %isnull = icmp eq ptr %p, null\n br i1 %isnull, label %next, label %str\n")
|
||||
emith("str:\n %c = call i32 @strcmp(ptr %p, ptr %path)\n %same = icmp eq i32 %c, 0\n br i1 %same, label %drop, label %next\n")
|
||||
emith("next:\n %i1 = add i32 %i, 1\n store i32 %i1, ptr %ip\n br label %find\n")
|
||||
emith("drop:\n %bp = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %i\n %base = load ptr, ptr %bp\n %lp = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %i\n %len = load i64, ptr %lp\n")
|
||||
if g_target_win { emith(" %u = call i32 @UnmapViewOfFile(ptr %base)\n") } else { emith(" %u = call i32 @munmap(ptr %base, i64 %len)\n") }
|
||||
emith(" call void @lp_free(ptr %p)\n %jp = alloca i32\n store i32 %i, ptr %jp\n br label %shift\n")
|
||||
emith("shift:\n %j = load i32, ptr %jp\n %j1 = add i32 %j, 1\n %more = icmp slt i32 %j1, %n\n br i1 %more, label %mv, label %done\n")
|
||||
emith("mv:\n %sb = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %j1\n %vb = load ptr, ptr %sb\n %db = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %j\n store ptr %vb, ptr %db\n")
|
||||
emith(" %sl = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %j1\n %vl = load i64, ptr %sl\n %dl = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %j\n store i64 %vl, ptr %dl\n")
|
||||
emith(" %sp = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %j1\n %vp = load ptr, ptr %sp\n %dp = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %j\n store ptr %vp, ptr %dp\n")
|
||||
emith(" store i32 %j1, ptr %jp\n br label %shift\n")
|
||||
emith("done:\n %n1 = sub i32 %n, 1\n store i32 %n1, ptr @L_pak_n\n %last = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %n1\n store ptr null, ptr %last\n ret i32 1\n")
|
||||
emith("no:\n ret i32 0\n}\n")
|
||||
}
|
||||
|
||||
# ---- lookup -----------------------------------------------------------------
|
||||
|
||||
function emit_pak_lookup() -> void {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ function emit_win_prelude() -> void {
|
|||
emith("declare i32 @FindClose(ptr)\n")
|
||||
emith("declare ptr @CreateFileMappingA(ptr, ptr, i32, i32, i32, ptr)\n")
|
||||
emith("declare ptr @MapViewOfFile(ptr, i32, i32, i32, i64)\n")
|
||||
emith("declare i32 @UnmapViewOfFile(ptr)\n")
|
||||
emith("declare i32 @CloseHandle(ptr)\n")
|
||||
emith("declare i32 @GetModuleFileNameA(ptr, ptr, i32)\n")
|
||||
emith("declare i32 @GetTempPathA(i32, ptr)\n")
|
||||
|
|
|
|||
39932
selfhost/ludicc.seed.ll
39932
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -280,7 +280,11 @@ entry {
|
|||
# -Wno-override-module: the emitted IR carries no target triple, so clang
|
||||
# substitutes the host's and warns about it on every single build. There is
|
||||
# nothing to act on — the host default is exactly what we want — so it is off.
|
||||
var cmd = `{cc} -O2 -Wno-override-module {ll}`
|
||||
# $LUDIC_OPT=1 is a developer's faster build (never a bundle's: ludic bundle sets 2)
|
||||
let opt = getenv_or("LUDIC_OPT", "2")
|
||||
let objs = split_compile(ll, out, opt) # on every core when it can (split_build.ludic)
|
||||
var cmd = `{cc} -O{opt} -Wno-override-module {ll}`
|
||||
if objs != null { cmd = `{cc} -O{opt} -Wno-override-module{objs}` }
|
||||
if g_target_win {
|
||||
# The Windows window: win32.ll (user32, gdi32 for the software present, xinput
|
||||
# for pads), and audio_win.ll when the program uses Audio.*, as on macOS.
|
||||
|
|
@ -389,6 +393,7 @@ entry {
|
|||
var line = cmd
|
||||
if host_is_windows() { line = `"{cmd}"` }
|
||||
let rc = run(line)
|
||||
if objs != null { split_clean(out) }
|
||||
if not save { remove_file(ll) }
|
||||
if rc != 0 { die("ludicc: link failed\n") }
|
||||
if natives_out != null {
|
||||
|
|
|
|||
87
selfhost/split_build.ludic
Normal file
87
selfhost/split_build.ludic
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# split_build.ludic - the program's IR compiled on every core: llvm-split cuts it into N parts, one
|
||||
# clang each runs at once, and the link takes their objects where it took the one .ll. Most of a
|
||||
# build was one clang -O2 on one file (the game: 32 s of 41). Needs an llvm-split and a clang of
|
||||
# the same LLVM (a newer LLVM's parts do not read in an older clang): $LUDIC_LLVM (a bin directory),
|
||||
# else Homebrew's. Anything missing, or a part that does not compile, falls back to the one clang.
|
||||
|
||||
function split_there(p: pointer) -> bool {
|
||||
let f = file_open(p, "rb")
|
||||
if f == null { return false }
|
||||
file_close(f)
|
||||
return true
|
||||
}
|
||||
|
||||
# the LLVM bin directory to split with, or null (Windows' shell cannot run the parts at once yet)
|
||||
function split_dir() -> pointer {
|
||||
if host_is_windows() { return null }
|
||||
if getenv_or("LUDIC_SPLIT", "1") == "0" { return null }
|
||||
let d = getenv_or("LUDIC_LLVM", "/opt/homebrew/opt/llvm/bin")
|
||||
if not split_there(`{d}/llvm-split`) or not split_there(`{d}/clang`) { return null }
|
||||
return d
|
||||
}
|
||||
|
||||
# how many parts: $LUDIC_JOBS, else the cores, no more than the free memory allows at 1.5 GB a clang
|
||||
function split_jobs(tmp: pointer) -> int {
|
||||
let j = getenv("LUDIC_JOBS")
|
||||
if j != null { return split_int(j) }
|
||||
let probe = `{tmp}.cores`
|
||||
let aw = "awk '/Pages free/{f=$3} /Pages inactive/{i=$3} END{print int((f+i)*p/1073741824)}'"
|
||||
run(`echo $(sysctl -n hw.ncpu) $(vm_stat | {aw} p=$(sysctl -n hw.pagesize)) > "{probe}" 2>/dev/null`)
|
||||
let f = file_open(probe, "rb")
|
||||
if f == null { return 1 }
|
||||
let buf = bytes(64)
|
||||
let n = file_read(f, buf, 63)
|
||||
file_close(f)
|
||||
remove_file(`"{probe}"`)
|
||||
if n <= 0 { return 1 }
|
||||
buf[n] = 0
|
||||
var cores = 0
|
||||
var i = 0
|
||||
while i < n and buf[i] >= '0' and buf[i] <= '9' { cores = cores * 10 + (buf[i] - '0'); i += 1 }
|
||||
while i < n and buf[i] == ' ' { i += 1 }
|
||||
var gb = 0
|
||||
while i < n and buf[i] >= '0' and buf[i] <= '9' { gb = gb * 10 + (buf[i] - '0'); i += 1 }
|
||||
let by_mem = (gb * 2) / 3
|
||||
if by_mem < cores { return by_mem }
|
||||
return cores
|
||||
}
|
||||
function split_int(s: pointer) -> int {
|
||||
var v = 0
|
||||
var i = 0
|
||||
while i < len(s) and s[i] >= '0' and s[i] <= '9' { v = v * 10 + (s[i] - '0'); i += 1 }
|
||||
return v
|
||||
}
|
||||
|
||||
# the IR at `ll` compiled in parts at -O`opt`: their objects, quoted and space-separated, for the
|
||||
# link to take in place of the .ll; null when it was not split (the caller compiles the .ll whole)
|
||||
function split_compile(ll: pointer, out: pointer, opt: pointer) -> pointer {
|
||||
let d = split_dir()
|
||||
if d == null { return null }
|
||||
let n = split_jobs(out)
|
||||
if n < 2 { return null }
|
||||
let pre = `{out}.part`
|
||||
if run(`"{d}/llvm-split" -j {n} -o "{pre}" "{ll}"`) != 0 { return null }
|
||||
var sh = ""
|
||||
var i = 0
|
||||
while i < n {
|
||||
sh = sh + `"{d}/clang" -x ir -O{opt} -mmacosx-version-min=11.0 -Wno-override-module -Wno-missing-sysroot -c "{pre}{itoa(i)}" -o "{pre}{itoa(i)}.o" & `
|
||||
i += 1
|
||||
}
|
||||
run(`/bin/sh -c '{sh}wait'`)
|
||||
var objs = ""
|
||||
var ok = true
|
||||
i = 0
|
||||
while i < n {
|
||||
remove_file(`"{pre}{itoa(i)}"`)
|
||||
if not split_there(`{pre}{itoa(i)}.o`) { ok = false }
|
||||
objs = objs + ` "{pre}{itoa(i)}.o"`
|
||||
i += 1
|
||||
}
|
||||
if not ok {
|
||||
split_clean(out)
|
||||
return null
|
||||
}
|
||||
return objs
|
||||
}
|
||||
# the parts' objects, once linked (or when a part failed)
|
||||
function split_clean(out: pointer) -> void { run(`rm -f "{out}".part*.o`) }
|
||||
|
|
@ -54,7 +54,12 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
|
||||
if mode == 2 {
|
||||
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)}{native_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
let objs = split_objs(ll, out) # on every core when it can (split.ludic)
|
||||
var input = ll
|
||||
if objs != "" { input = objs }
|
||||
let linked = shq(`{cc()} -O{split_opt()} {input}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`)
|
||||
if objs != "" { shell(`rm -f {objs}`) }
|
||||
if not linked { return false }
|
||||
g_built_natives = native_libs_of(ll)
|
||||
if not save { shell(`rm -f {ll}`) }
|
||||
return true
|
||||
|
|
@ -65,7 +70,12 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
# canonical `ludicc -o` path links it only when Audio.* is used.
|
||||
let cocoa = `{home}runtime/native/cocoa.ll`
|
||||
let audio = `{home}runtime/native/audio.ll`
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
let objs = split_objs(ll, out)
|
||||
var input = ll
|
||||
if objs != "" { input = objs }
|
||||
let linked = shq(`{cc()} -O{split_opt()} {input} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`)
|
||||
if objs != "" { shell(`rm -f {objs}`) }
|
||||
if not linked { return false }
|
||||
g_built_natives = native_libs_of(ll)
|
||||
if not save { shell(`rm -f {ll}`) }
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
# app category "public.app-category.adventure-games"
|
||||
# app copyright "(c) 2026 Workshopsoft"
|
||||
# app min_macos "12.0"
|
||||
# app env "MallocLargeCache=0" (repeatable: LSEnvironment, what Launch Services starts it with)
|
||||
#
|
||||
# Only `app name` is really needed; everything else has a defensible default
|
||||
# derived from the manifest.
|
||||
|
|
@ -187,6 +188,7 @@ function info_plist(m: Manifest, exe: pointer, has_icon: bool) -> pointer {
|
|||
# A game renders at the display's real resolution; without this the window is
|
||||
# upscaled from 1x and everything drawn in it is soft on any Retina screen.
|
||||
s = s + plist_bool("NSHighResolutionCapable", true)
|
||||
s = s + plist_env(m)
|
||||
s = s + "</dict>" + nl()
|
||||
s = s + "</plist>" + nl()
|
||||
return s
|
||||
|
|
@ -204,6 +206,24 @@ function plist_str(k: pointer, v: pointer) -> pointer {
|
|||
return ` <key>{k}</key>` + nl() + ` <string>{xml_escape(v)}</string>` + nl()
|
||||
}
|
||||
|
||||
# every `app env "K=V"`: the environment Launch Services starts the app with (Finder, `open`, the
|
||||
# Dock) - a switch a library reads only as a process starts, such as libmalloc's MallocLargeCache
|
||||
function plist_env(m: Manifest) -> pointer {
|
||||
var body = ""
|
||||
var i = 0
|
||||
while i + 1 < len(m.app) {
|
||||
if m.app[i] == "env" {
|
||||
let kv = m.app[i + 1]
|
||||
var eq = 0
|
||||
while eq < len(kv) and kv[eq] != '=' { eq += 1 }
|
||||
if eq > 0 and eq < len(kv) { body = body + plist_str(kv[0..eq], kv[eq + 1..len(kv)]) }
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
if body == "" { return "" }
|
||||
return " <key>LSEnvironment</key>" + nl() + " <dict>" + nl() + body + " </dict>" + nl()
|
||||
}
|
||||
|
||||
function plist_bool(k: pointer, v: bool) -> pointer {
|
||||
var t = "<false/>"
|
||||
if v { t = "<true/>" }
|
||||
|
|
@ -337,6 +357,7 @@ function cmd_bundle_windows() -> int {
|
|||
let staged = tmp_path("bundle_exe")
|
||||
var flags = "--windowed --gui"
|
||||
if res != "" { flags = flags + ` --link "{res}"` }
|
||||
Os.set_env("LUDIC_OPT", "2") # a shipped game is -O2, whatever a developer's shell says
|
||||
if not shq(`{ludicc()} {flags}{title_flag()} {entry} --natives-out {staged}.natives -o {staged}`) { return 1 }
|
||||
if not shq(`cp "{staged}.exe" "{root}/{name}.exe"`) {
|
||||
err("ludic bundle: could not place the executable\n")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
program LudicDev {
|
||||
import "prelude.ludic"
|
||||
import "build.ludic"
|
||||
import "split.ludic"
|
||||
import "toolchain.ludic"
|
||||
import "selfhost.ludic"
|
||||
import "tools.ludic"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
program Ludic {
|
||||
import "prelude.ludic"
|
||||
import "build.ludic"
|
||||
import "split.ludic"
|
||||
import "project.ludic"
|
||||
import "pkg.ludic"
|
||||
import "assets.ludic"
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/backend/stdlib/emit_net.ludic")
|
||||
push(f, "selfhost/backend/game/emit_ui.ludic")
|
||||
push(f, "selfhost/backend/emit_decl.ludic")
|
||||
push(f, "selfhost/split_build.ludic")
|
||||
push(f, "selfhost/main.ludic")
|
||||
return f
|
||||
}
|
||||
|
|
|
|||
49
tools/ludic-cli/split.ludic
Normal file
49
tools/ludic-cli/split.ludic
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# split.ludic - `ludic build` on every core: the program's IR cut into N parts (llvm-split), a clang
|
||||
# each at once, their objects linked where the one .ll was (the game: clang was 32 s of a 41 s
|
||||
# build, on one core). ludicc's own link does the same (selfhost/split_build.ludic). It needs an
|
||||
# llvm-split and a clang of one LLVM - $LUDIC_LLVM, else Homebrew's - or it compiles the .ll whole.
|
||||
|
||||
function split_opt() -> pointer { return getenv_or("LUDIC_OPT", "2") } # 1: a developer's faster build
|
||||
|
||||
# " a.o b.o ..." for the link, or "" when not split (the caller compiles the .ll as before)
|
||||
function split_objs(ll: pointer, out: pointer) -> pointer {
|
||||
if host_windows() or getenv_or("LUDIC_SPLIT", "1") == "0" { return "" }
|
||||
let d = getenv_or("LUDIC_LLVM", "/opt/homebrew/opt/llvm/bin")
|
||||
if not is_exec(`"{d}/llvm-split"`) or not is_exec(`"{d}/clang"`) { return "" }
|
||||
let n = split_n()
|
||||
if n < 2 { return "" }
|
||||
let pre = tmp_path(`{file_of_path(out)}.part`)
|
||||
if not shq(`"{d}/llvm-split" -j {n} -o "{pre}" "{ll}"`) { return "" }
|
||||
var sh = ""
|
||||
for i in 0 .. n {
|
||||
sh = sh + `"{d}/clang" -x ir -O{split_opt()} -mmacosx-version-min=11.0 -Wno-override-module -Wno-missing-sysroot -c "{pre}{i}" -o "{pre}{i}.o" & `
|
||||
}
|
||||
shell(`{sh}wait`)
|
||||
var objs = ""
|
||||
var ok = true
|
||||
for i in 0 .. n {
|
||||
shell(`rm -f "{pre}{i}"`)
|
||||
if not file_exists(`"{pre}{i}.o"`) { ok = false }
|
||||
objs = objs + ` "{pre}{i}.o"`
|
||||
}
|
||||
if ok { return objs }
|
||||
shell(`rm -f {objs}`)
|
||||
return ""
|
||||
}
|
||||
|
||||
# $LUDIC_JOBS, else the cores - no more than the free memory allows at 1.5 GB a clang
|
||||
function split_n() -> int {
|
||||
let j = getenv("LUDIC_JOBS")
|
||||
if j != null { return s_to_int(j) }
|
||||
let aw = "awk '/Pages free/{f=$3} /Pages inactive/{i=$3} END{print int((f+i)*p/1073741824)}'"
|
||||
let line = capture_line(`echo $(sysctl -n hw.ncpu) $(vm_stat | {aw} p=$(sysctl -n hw.pagesize))`)
|
||||
var cores = 0
|
||||
var i = 0
|
||||
while i < len(line) and line[i] >= '0' and line[i] <= '9' { cores = cores * 10 + (line[i] - '0'); i += 1 }
|
||||
while i < len(line) and line[i] == ' ' { i += 1 }
|
||||
var gb = 0
|
||||
while i < len(line) and line[i] >= '0' and line[i] <= '9' { gb = gb * 10 + (line[i] - '0'); i += 1 }
|
||||
let by_mem = (gb * 2) / 3
|
||||
if by_mem < cores { return by_mem }
|
||||
return cores
|
||||
}
|
||||
|
|
@ -370,6 +370,9 @@ function bundle_case() -> void {
|
|||
# the identifier is derived from the package path when the manifest omits it
|
||||
let id = capture_line(`plutil -extract CFBundleIdentifier raw "{app}/Contents/Info.plist" 2>/dev/null`)
|
||||
if id != "io.workshopsoft.probe-app" { bad2(lbl, `bundle id came out [{id}]`); return }
|
||||
# `app env` is what Launch Services starts it with (a Finder or `open` start)
|
||||
let env = capture_line(`plutil -extract LSEnvironment.MallocLargeCache raw "{app}/Contents/Info.plist" 2>/dev/null`)
|
||||
if env != "0" { bad2(lbl, `LSEnvironment.MallocLargeCache came out [{env}]`); return }
|
||||
if not shq(`codesign --verify "{app}" > /dev/null 2>&1`) { bad2(lbl, "the signature does not verify"); return }
|
||||
# and it runs from a directory with none of its assets, reading them from the pack
|
||||
let got = s_trim(capture(`cd / && "{app}/Contents/MacOS/Probe App" 2>&1`))
|
||||
|
|
@ -388,6 +391,7 @@ function bundle_manifest_src() -> pointer {
|
|||
s = s + "app name \"Probe App\"" + nl()
|
||||
s = s + "app icon \"assets/icon.png\"" + nl()
|
||||
s = s + "app copyright \"(c) 2026 A & B\"" + nl()
|
||||
s = s + "app env \"MallocLargeCache=0\"" + nl()
|
||||
s = s + "pack \"assets\"" + nl()
|
||||
return s
|
||||
}
|
||||
|
|
@ -1248,6 +1252,8 @@ function cmd_dev_test() -> int {
|
|||
controller_case("library/ui_emit_click", "", "got 2 | pressed: got 1", "ui_emit_click.ludic (L11: a component's `emit click` reaches its user's on-click, by the mouse and by a press)")
|
||||
controller_case("library/ui_select_arrows", "", "prev 0 next next 2", "ui_select_arrows.ludic (L11: a select's < steps back and its > forward)")
|
||||
controller_case("library/ui_pointer_through", "", "pressed 1, hovered #4a5058; > hovered 1", "ui_pointer_through.ludic (L11: the pointer looks through a root with no size to a positioned panel's button; a select's > answers :hover)")
|
||||
controller_case("library/ui_first_show", "", "bad 0 shown 3", "ui_first_show.ludic (L11: a component first shown once the fence is judging makes its props and model declared, once per instance; shown again, it is reused)")
|
||||
controller_case("library/ui_select_disabled", "", "clamped 2 next-on 0 next 0 prev 2", "ui_select_disabled.ludic (L11: <option disabled>: a value on one is set to the nearest enabled, the arrow toward them drawn disabled, the arrows step past them)")
|
||||
controller_case("library/ui_submit", "", "[hi] chat '' | [hi][yo][Ada] chat '' name 'Ada'", "ui_submit.ludic (L11: on-submit on a text field by Enter or A, clear-on-submit, the focus kept)")
|
||||
controller_case("library/ui_zoom", "", "plain 100 pad 10 text 16 h 20 | half 50 pad 5 text 8 h 10 | half of twice 100 | fit 50 em 16", "ui_zoom.ludic (L11: zoom scales a subtree's lengths, text and layout, multiplies, and takes min() and a length over a length)")
|
||||
controller_case("library/ui_hold", "", "31 frames 500 ms t 500 | 121 frames 500 ms t 500 | pad 16 frames 250 ms t 250 | dududu", "ui_hold.ludic (L11: on-hold every frame a button is held, by the pointer or A, with dt and t on the clock at any rate)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue