Merge branch 'lang/foundations' into lang/nav

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 23:37:52 +03:00
commit d9b20f6eec
76 changed files with 29862 additions and 26177 deletions

View file

@ -0,0 +1,9 @@
bump: minor
type: feature
**The built-in ECS grows.** Every component was a fixed array of 1024 slots, so a game past 1024
entities could not have them (and until the last release silently corrupted memory trying). The
per-entity stores are heap blocks now, doubled by `L_grow` as entities outgrow them, the new slots
zero: 100 000 entities spawn and query. `Prop.has` bounds against the live capacity and
`Pool.capacity` answers it. A snapshot (`save`/`load`, `world_save`/`world_load`) records its slot
count first and a load grows to it before reading the stores back, so a snapshot's size follows the
world's instead of a fixed 1024. A mod's registered components grow with the rest.

15
changes/vulkan-leaks.md Normal file
View file

@ -0,0 +1,15 @@
bump: patch
type: fix
**The Vulkan renderer no longer leaks memory as it loads and draws.** Every texture upload
malloc'd a CPU copy of its pixels and never freed it (236 MB over the valley's boot, and again on
every model loaded later); the pixels are now converted straight into the mapped staging buffer.
The per-level and per-layer views made for mipmapped and layered targets outlived their texture,
and on MoltenVK a view keeps its Metal texture alive, so every rebuild of the screen-sized targets
left the last set in memory; a released texture now takes its views with it, and the cache is
keyed by numbers rather than a string built on every call. The Vulkan structs filled for a draw, a
pass, a barrier, a descriptor set or a buffer (about sixty call sites, several per draw) come from
a reused 1 MB scratch ring instead of a malloc each; the descriptor-set cache and the retired
buffer list are emptied and refilled rather than replaced; and on macOS the frame drains an
autorelease pool (`Vk.frame_pool()`), which MoltenVK and the layer rely on. `R3D_VK_PROF` now
also prints, every 120 frames, the Vulkan objects made and destroyed by kind and the length of
every cache, so a kind made each frame and never destroyed shows at once.

View file

@ -0,0 +1,9 @@
bump: minor
type: feature
**A `Job.parallel_for` worker may read a state but not change one.** Every pool thread runs the
worker at once, and the runtime hands each call the same state, so a worker that took a state as
`mut` raced on it with nothing to say so. The compiler now refuses it (`fn bump: a worker runs on
every core at once, so it may read CountState but not change it`): a result goes into `ctx`, a
shared count through a `Sync` handle kept in the state. A function's signature already says what it
touches, so this is the whole check - the same one a parallel scheduler needs to run two systems at
once. `examples/library/threads.ludic` keeps its total in the words it hands the worker.

View file

@ -19,7 +19,9 @@ started on first use) and the calling thread, so each index runs exactly once, i
on (`words`, `bytes` or a `pointer`). A worker computes on what it was handed and writes only its own on (`words`, `bytes` or a `pointer`). A worker computes on what it was handed and writes only its own
index's results: it must not `spawn`, `despawn`, `push` onto a list another thread can see, or use index's results: it must not `spawn`, `despawn`, `push` onto a list another thread can see, or use
Http or Audio. `spawn` and `despawn` on a worker stop the program with a located message. Shared Http or Audio. `spawn` and `despawn` on a worker stop the program with a located message. Shared
counters go through `Sync.add`, shared totals behind a `Sync.mutex`. counters go through `Sync.add`, shared totals behind a `Sync.mutex`. A worker may take states, but
only to read them: every thread runs it at once, so a worker that takes one as `mut` is refused at
compile time (a result goes into `ctx`, a count through a `Sync` handle kept in the state).
```ludic ```ludic
program Squares { program Squares {

View file

@ -9,17 +9,18 @@ program Threads {
state ThreadsState { state ThreadsState {
calls: int = 0 # an atomic handle, made on the main thread before any work calls: int = 0 # an atomic handle, made on the main thread before any work
lock: int = 0 # a mutex handle lock: int = 0 # a mutex handle
total: long = 0 # guarded by `lock`
} }
# out[i] = i * i: every index written exactly once, by whichever thread claimed it # out[i] = i * i: every index written exactly once, by whichever thread claimed it
function square(i: int, out: words) -> void { out[i] = i * i } function square(i: int, out: words) -> void { out[i] = i * i }
# count the call atomically, and add i to a shared total under the mutex # count the call atomically, and add i to a shared total under the mutex. A worker may read a
function tally(threads_st: mut ThreadsState, i: int, ctx: pointer) -> void { # state (the handles in it) but never change one - every thread runs it at once - so the total
# lives in what the worker is handed
function tally(threads_st: ThreadsState, i: int, total: words) -> void {
Sync.add(threads_st.calls, 1) Sync.add(threads_st.calls, 1)
Sync.lock(threads_st.lock) Sync.lock(threads_st.lock)
threads_st.total = threads_st.total + i total[0] = total[0] + i
Sync.unlock(threads_st.lock) Sync.unlock(threads_st.lock)
} }
@ -43,10 +44,11 @@ program Threads {
threads_st.calls = Sync.atomic() threads_st.calls = Sync.atomic()
threads_st.lock = Sync.mutex() threads_st.lock = Sync.mutex()
Job.parallel_for(N, fn tally, null) let total = words(1)
Job.parallel_for(N, fn tally, total)
if Sync.get(threads_st.calls) == N { print(4) } if Sync.get(threads_st.calls) == N { print(4) }
let want: long = N * (N - 1) / 2 let want = N * (N - 1) / 2
if threads_st.total == want { print(5) } if total[0] == want { print(5) }
let ran = words(N) let ran = words(N)
Job.parallel_for(N, fn placed, ran) Job.parallel_for(N, fn placed, ran)

View file

@ -192,7 +192,7 @@ program Smooth {
handler Draw(render3d_st: mut Render3dState, smooth_st: mut SmoothState) phase Render { handler Draw(render3d_st: mut Render3dState, smooth_st: mut SmoothState) phase Render {
r3d_frame(render3d_st, float(Time.elapsed())) r3d_frame(render3d_st, float(Time.elapsed()))
smooth_st.frame += 1 smooth_st.frame += 1
if smooth_st.frame == smooth_st.shot_at { Gl.screenshot(path: "build/smooth.ppm") } if smooth_st.frame == smooth_st.shot_at { r3d_screenshot(render3d_st, "build/smooth.ppm") }
Gl.swap() r3d_present(render3d_st)
} }
} }

View file

@ -50,6 +50,60 @@ import "ludic.base"
| `def Systems key { ... }`, `SYS_<KEY>`, `SYS_COUNT` | a system declared from any module; the list starts from these | | `def Systems key { ... }`, `SYS_<KEY>`, `SYS_COUNT` | a system declared from any module; the list starts from these |
| `core_init_all()`, `core_reset_all()`, `core_tick_all(t)`, `core_save_all() -> Val`, `core_load_all(v)` | the runner: in the order added, tick phase by phase | | `core_init_all()`, `core_reset_all()`, `core_tick_all(t)`, `core_save_all() -> Val`, `core_load_all(v)` | the runner: in the order added, tick phase by phase |
## Entities: `Table<T>` and its indexes
A mechanic that keeps many of something (Things on the ground, animals, drops) keeps them as rows
of a `Table<T>` in its state rather than as a list it scans. The table is data-oriented and
allocates nothing per query:
- **Rows are dense.** Hot data is columns - `tb.f[c]` (floats) and `tb.i[c]` (ints), one value per
row - and `tb.rec[row]` is a record `T` for everything cold. A removal moves the last row into
the gap (`tb_remove`), so a sweep over `0 .. tb_len(tb)` touches contiguous memory.
- **Handles go stale.** `tb_add` returns a handle: a 22-bit slot and a 9-bit generation. `tb_row(tb,
h)` is -1 once the entity is removed, even after the slot is reused. Keep handles, never rows.
- **Indexes are kept by the setters.** `tb_set_f`, `tb_set_xz`, `tb_set_i` (or writing `tb.f` /
`tb.i` directly and then `tb_refile`) stamp the row's change tick and refile it in every index
that reads that column, in O(1). An index is never a frame behind.
- `tb_grid(tb, cx, cz, gate, cell)` - a spatial hash over two float columns, gated by an int
column (a row is filed while it is non-zero: "active"). `tb_nearest(tb, g, x, z, maxr, mc, mv)`
searches rings outward and stops at the first ring that cannot hold anything nearer;
`tb_within(..., out)` fills a caller's `words`. Buckets double as the rows grow.
- `tb_index(tb, col, gate)` - a cached query: the rows holding each value of an int column (a
kind). `ix_rows(ix, v)`, `ix_count(ix, v)`, `ix_first(ix, v)`.
- `tb_nearest_of` / `tb_within_of` plan between the two: a rare kind is scanned from its own
list, a common one searched by rings.
- **Change detection.** `tb_advance(tb)` moves the table to its next tick; `tb_changed_since(tb,
tick, out)` and `tb_added_since` name the rows written since - what a save or a message needs to
send a delta instead of everything.
- **Observers.** `tb_on_add(tb, fn f)` / `tb_on_remove(tb, fn f)`: `f(handle)` (its states supplied,
like a system's) is told each handle as its row is made and before it goes.
- **Groups and chunks.** `tb_remove_all(tb, ix, v)` removes every row an index files under one
value - a kind, or a chunk of the world a stream lets go (`chunk_of(x, z, size)` packs a chunk's
cell into an int). An index keeps a small value (below 1024) in its own slot and gives a larger,
sparse one (a chunk's packed cell) a slot through a map, so each value costs one list.
- **Change detection is per 64-row block too**: `tb_changed_since` skips a block nobody wrote
since. A row moved into a removal's gap keeps its own tick; the removal is the observer's.
- **Parallel work reads, never writes, a state.** `Job.parallel_for(n, fn work, ctx)` over a
table's columns runs on every core; the compiler refuses a worker that takes a state as `mut`
(a result goes into `ctx`, a shared count through a `Sync` handle kept in the state).
- **Stable ids.** `IntMap` (`imap_new`, `imap_put`, `imap_get(m, k, none)`, `imap_del`) maps an
id kept in a save or a message to a handle without a scan.
Ludic frees nothing a safe program allocates, so a query that built a list per call leaked every
frame. Every question here writes into a buffer the caller keeps. What each costs, against a
`[]Record` list scanned (`M4 Pro`, one thread):
| | 10 000 | 100 000 | 1 000 000 |
| --- | --- | --- | --- |
| nearest, any | 0.37 us (list 30) | 1.5 us (list 307) | 7.2 us (list 3075) |
| nearest of a kind (1 in 40) | 1.4 us (list 5.8) | 3.3 us (list 56) | 12 us (list 864) |
| by id | 0.1 us (list 1.7) | 0.09 us (list 17) | 1.5 us (list 324) |
| within 30 m | 0.9 us | 1.5 us | 6.9 us |
| a move, refiled | 11 ns | 14 ns | 44 ns |
`tests/ecs_fuzz_test.ludic` holds the grid and the kind index against a scan through thousands of
random adds, removes, moves, kind changes and gate flips.
## A toy mechanic ## A toy mechanic
```ludic ```ludic

View file

@ -0,0 +1,79 @@
# ludic.base/ecs_grid.ludic - a spatial hash over two float columns: square cells hashed into
# buckets, each a doubly linked list of rows, so filing, moving and unfiling a row is O(1)
export property Grid {
cx: int = 0
cz: int = 1
gate: int = -1 # an int column: a row is filed only while it is non-zero (-1: always)
inv: float = 0.0625 # 1 / the cell's side
mask: int = 255 # buckets - 1, a power of two less one
head: words = null # bucket -> row + 1, 0 empty
next: words = null # row -> the next row in its bucket + 1
prev: words = null # row -> the previous row + 1
key: words = null # row -> its packed cell, -1 while not filed
count: int = 0
}
function grid_cell(g: Grid, v: float) -> int { return int(Math.floor(v * g.inv)) }
function grid_key(ix: int, iz: int) -> int { return ((ix & 32767) << 15) | (iz & 32767) }
function grid_bucket(g: Grid, ix: int, iz: int) -> int { return ((ix * 73856093) ^ (iz * 19349663)) & g.mask }
function grid_make(cx: int, cz: int, gate: int, cell: float, rows: int) -> Grid {
let g = new Grid
g.cx = cx
g.cz = cz
g.gate = gate
g.inv = 1.0 / cell
g.head = words(g.mask + 1)
g.next = words(0)
g.prev = words(0)
g.key = words(0)
for r in 0 .. rows { grid_grow(g) }
return g
}
# index a table spatially by columns cx and cz, in cells `cell` wide; its rows are filed at once
export function tb_grid<T>(tb: Table<T>, cx: int, cz: int, gate: int, cell: float) -> Grid {
let g = grid_make(cx, cz, gate, cell, tb.n)
push(tb.grids, g)
for r in 0 .. tb.n { tb_grid_refile(tb, g, r) }
return g
}
export function grid_count(g: Grid) -> int { return g.count }
function grid_grow(g: Grid) -> void {
push(g.next, 0)
push(g.prev, 0)
push(g.key, -1)
}
function grid_shrink(g: Grid) -> void {
List.pop(g.next)
List.pop(g.prev)
List.pop(g.key)
}
function grid_link(g: Grid, r: int, k: int, b: int) -> void {
let h = g.head[b]
g.next[r] = h
g.prev[r] = 0
if h > 0 { g.prev[h - 1] = r + 1 }
g.head[b] = r + 1
g.key[r] = k
g.count += 1
}
function grid_unlink(g: Grid, r: int) -> void {
let k = g.key[r]
if k < 0 { return }
let p = g.prev[r]
let n = g.next[r]
if p > 0 { g.next[p - 1] = n } else { g.head[grid_bucket(g, grid_key_x(k), grid_key_z(k))] = n }
if n > 0 { g.prev[n - 1] = p }
g.key[r] = -1
g.count -= 1
}
# a packed key back to its cell (sign-extended from 15 bits)
function grid_key_x(k: int) -> int { return ((k >> 15) & 32767) - (((k >> 29) & 1) * 32768) }
function grid_key_z(k: int) -> int { return (k & 32767) - (((k >> 14) & 1) * 32768) }

View file

@ -0,0 +1,40 @@
# ludic.base/ecs_grid_file.ludic - where a row is filed follows its columns: a row that has not
# left its cell is not touched, a moved one is relinked, and the buckets double as the rows grow
function grid_refile(g: Grid, r: int, x: float, z: float, on: bool) -> void {
if not on {
grid_unlink(g, r)
return
}
let ix = grid_cell(g, x)
let iz = grid_cell(g, z)
let k = grid_key(ix, iz)
if g.key[r] == k { return }
grid_unlink(g, r)
grid_link(g, r, k, grid_bucket(g, ix, iz))
if g.count > (g.mask + 1) * 2 { grid_rehash(g) }
}
# row `from` becomes row `to` (which is not filed): its neighbours and its bucket follow it
function grid_moved(g: Grid, from: int, to: int) -> void {
let k = g.key[from]
g.key[to] = k
if k < 0 { return }
let p = g.prev[from]
let n = g.next[from]
g.prev[to] = p
g.next[to] = n
if p > 0 { g.next[p - 1] = to + 1 } else { g.head[grid_bucket(g, grid_key_x(k), grid_key_z(k))] = to + 1 }
if n > 0 { g.prev[n - 1] = to + 1 }
g.key[from] = -1
}
# twice the buckets, every filed row linked again where its key now hashes
function grid_rehash(g: Grid) -> void {
g.mask = g.mask * 2 + 1
g.head = words(g.mask + 1)
g.count = 0
for r in 0 .. len(g.key) {
let k = g.key[r]
if k >= 0 { grid_link(g, r, k, grid_bucket(g, grid_key_x(k), grid_key_z(k))) }
}
}

View file

@ -0,0 +1,93 @@
# ludic.base/ecs_grid_query.ludic - the nearest row: rings of cells outward from the point, stopping
# at the first ring that cannot hold anything nearer. A match is an int column equal to a value
# (column -1: any), or a predicate on the row's record. A question: it writes nothing but locals.
const GRID_RINGS: int = 24 # past this many rings a scan of the filed rows is cheaper
const GRID_FAR: float = 1000000000000.0
# the nearest filed row to (x, z) within maxr (0: any distance) matching (mc, mv), or -1
export function tb_nearest<T>(tb: Table<T>, g: Grid, x: float, z: float, maxr: float, mc: int, mv: int) -> int {
if g.count == 0 { return -1 }
var mcol: words = null
if mc >= 0 { mcol = tb.i[mc] }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
var best = -1
var bd = grid_limit(maxr)
let cs = 1.0 / g.inv
let ix0 = grid_cell(g, x)
let iz0 = grid_cell(g, z)
var ring = 0
while ring <= GRID_RINGS {
if ring > 0 and grid_ring_past(ring, cs, bd) { return best }
let span = grid_ring_n(ring)
for k in 0 .. span {
best = grid_visit(g, xs, zs, mcol, mv, x, z, grid_ring_x(ix0, ring, k), grid_ring_z(iz0, ring, k), best, bd)
if best >= 0 { bd = grid_d2(xs, zs, best, x, z) }
}
ring += 1
}
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) < bd {
best = row
bd = grid_d2(xs, zs, row, x, z)
}
}
return best
}
function grid_limit(maxr: float) -> float {
if maxr > 0.0 { return maxr * maxr }
return GRID_FAR
}
# the nearest a point of ring `ring` can be is (ring - 1) cells: past the best, nothing there wins
function grid_ring_past(ring: int, cs: float, bd: float) -> bool {
let near = float(ring - 1) * cs
return near * near > bd
}
function grid_d2(xs: floats, zs: floats, row: int, x: float, z: float) -> float {
let dx = xs[row] - x
let dz = zs[row] - z
return dx * dx + dz * dz
}
# a ring's cells in a fixed order: 1 for ring 0, else 8 * ring round the square's edge
function grid_ring_n(ring: int) -> int {
if ring == 0 { return 1 }
return 8 * ring
}
function grid_ring_x(ix0: int, ring: int, k: int) -> int {
if ring == 0 { return ix0 }
let side = 2 * ring
if k < side { return ix0 - ring + k }
if k < 2 * side { return ix0 + ring }
if k < 3 * side { return ix0 + ring - (k - 2 * side) }
return ix0 - ring
}
function grid_ring_z(iz0: int, ring: int, k: int) -> int {
if ring == 0 { return iz0 }
let side = 2 * ring
if k < side { return iz0 - ring }
if k < 2 * side { return iz0 - ring + (k - side) }
if k < 3 * side { return iz0 + ring }
return iz0 + ring - (k - 3 * side)
}
# one cell: its bucket's rows that are really in it and match; the better of them and `best`
function grid_visit(g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, ix: int, iz: int, best: int, bd: float) -> int {
let k = grid_key(ix, iz)
var b = best
var d = bd
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k and (mcol == null or mcol[row] == mv) {
let d2 = grid_d2(xs, zs, row, x, z)
if d2 < d {
d = d2
b = row
}
}
r = g.next[row]
}
return b
}

View file

@ -0,0 +1,101 @@
# ludic.base/ecs_grid_where.ludic - the grid's questions put to the rows' records: the nearest one a
# predicate keeps, and every record within a radius into the caller's list. The predicate is called
# with the record only; it may read anything, and must change nothing the grid indexes.
export function tb_nearest_where<T>(tb: Table<T>, g: Grid, x: float, z: float, maxr: float, keep: fn(T) -> bool) -> int {
if g.count == 0 { return -1 }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
var best = -1
var bd = grid_limit(maxr)
let cs = 1.0 / g.inv
let ix0 = grid_cell(g, x)
let iz0 = grid_cell(g, z)
var ring = 0
while ring <= GRID_RINGS {
if ring > 0 and grid_ring_past(ring, cs, bd) { return best }
for k in 0 .. grid_ring_n(ring) {
best = grid_visit_where(tb, g, xs, zs, x, z, grid_ring_x(ix0, ring, k), grid_ring_z(iz0, ring, k), best, bd, keep)
if best >= 0 { bd = grid_d2(xs, zs, best, x, z) }
}
ring += 1
}
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and grid_d2(xs, zs, row, x, z) < bd and keep(tb.rec[row]) {
best = row
bd = grid_d2(xs, zs, row, x, z)
}
}
return best
}
function grid_visit_where<T>(tb: Table<T>, g: Grid, xs: floats, zs: floats, x: float, z: float, ix: int, iz: int, best: int, bd: float, keep: fn(T) -> bool) -> int {
let k = grid_key(ix, iz)
var b = best
var d = bd
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k {
let d2 = grid_d2(xs, zs, row, x, z)
if d2 < d and keep(tb.rec[row]) {
d = d2
b = row
}
}
r = g.next[row]
}
return b
}
# every filed record within r of (x, z) matching (mc, mv), appended to `out` (cleared first) - the
# caller's list, kept in the caller's state and reused, so a frame's question allocates nothing
export function tb_within_recs<T>(tb: Table<T>, g: Grid, x: float, z: float, r: float, mc: int, mv: int, out: []T) -> int {
List.clear(out)
if g.count == 0 { return 0 }
var mcol: words = null
if mc >= 0 { mcol = tb.i[mc] }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
let ix0 = grid_cell(g, x - r)
let ix1 = grid_cell(g, x + r)
let iz0 = grid_cell(g, z - r)
let iz1 = grid_cell(g, z + r)
let cells = (ix1 - ix0 + 1) * (iz1 - iz0 + 1)
if cells > g.count or cells < 0 {
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) }
}
return len(out)
}
for ix in ix0 .. ix1 + 1 {
for iz in iz0 .. iz1 + 1 { grid_recs_cell(tb, g, xs, zs, mcol, mv, x, z, r * r, ix, iz, out) }
}
return len(out)
}
function grid_recs_cell<T>(tb: Table<T>, g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, r2: float, ix: int, iz: int, out: []T) -> void {
let k = grid_key(ix, iz)
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r2 { push(out, tb.rec[row]) }
r = g.next[row]
}
}
# every record of kind v (an index over column ix.col) within r, into `out`: a rare kind from its list
export function tb_within_of_recs<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: float, z: float, r: float, v: int, out: []T) -> int {
let n = ix_count(ix, v)
if n > PLAN_LIST_MAX { return tb_within_recs(tb, g, x, z, r, ix.col, v, out) }
List.clear(out)
if n == 0 { return 0 }
let rows = ix.lists[v]
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
for k in 0 .. n {
let row = rows[k]
if g.gate >= 0 and tb.i[g.gate][row] == 0 { continue }
if grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) }
}
return len(out)
}

View file

@ -0,0 +1,48 @@
# ludic.base/ecs_grid_within.ludic - every filed row within r of (x, z) matching (mc, mv), into
# `out` (cleared first), in no order; the count is returned. A radius wider than the rows are many
# is answered by one scan of them instead of by cells.
export function tb_within<T>(tb: Table<T>, g: Grid, x: float, z: float, r: float, mc: int, mv: int, out: words) -> int {
List.clear(out)
if g.count == 0 { return 0 }
var mcol: words = null
if mc >= 0 { mcol = tb.i[mc] }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
let ix0 = grid_cell(g, x - r)
let ix1 = grid_cell(g, x + r)
let iz0 = grid_cell(g, z - r)
let iz1 = grid_cell(g, z + r)
let cells = (ix1 - ix0 + 1) * (iz1 - iz0 + 1)
if cells > g.count or cells < 0 {
grid_within_scan(g, xs, zs, mcol, mv, x, z, r * r, out)
return len(out)
}
for ix in ix0 .. ix1 + 1 {
for iz in iz0 .. iz1 + 1 { grid_within_cell(g, xs, zs, mcol, mv, x, z, r * r, ix, iz, out) }
}
return len(out)
}
function grid_within_cell(g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, r2: float, ix: int, iz: int, out: words) -> void {
let k = grid_key(ix, iz)
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k and (mcol == null or mcol[row] == mv) {
let dx = xs[row] - x
let dz = zs[row] - z
if dx * dx + dz * dz <= r2 { push(out, row) }
}
r = g.next[row]
}
}
function grid_within_scan(g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, r2: float, out: words) -> void {
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) {
let dx = xs[row] - x
let dz = zs[row] - z
if dx * dx + dz * dz <= r2 { push(out, row) }
}
}
}

View file

@ -0,0 +1,20 @@
# ludic.base/ecs_group.ludic - a group is every row an index files under one value: a kind, or a
# chunk of the world, loaded and let go together. A chunk's number packs its two cell coordinates.
export function chunk_of(x: float, z: float, size: float) -> int {
let cx = int(Math.floor(x / size))
let cz = int(Math.floor(z / size))
return ((cx & 32767) << 15) | (cz & 32767)
}
# every row filed under v in ix removed (the observers told of each); how many went. The index's
# own list shrinks as each goes, so it is read from its end until it is empty.
export function tb_remove_all<T>(tb: Table<T>, ix: IntIndex, v: int) -> int {
var n = 0
let rows = ix_rows(ix, v)
while len(rows) > 0 {
let h = tb.ent[rows[len(rows) - 1]]
if not tb_remove(tb, h) { return n }
n += 1
}
return n
}

View file

@ -0,0 +1,18 @@
# ludic.base/ecs_hooks.ludic - observers on a table: told each handle as its row is made, and before
# it is removed, so what depends on a table (a drawing, a message) follows it without a scan
# an observer: a function handed an entity's handle (its states are supplied, like a system's)
export property TbHook {
run: fn(int) -> void = null
}
# f is told every handle added from here on, and every handle about to be removed
export function tb_on_add<T>(tb: Table<T>, f: fn(int) -> void) -> void {
let hk = new TbHook
hk.run = f
push(tb.on_add, hk)
}
export function tb_on_remove<T>(tb: Table<T>, f: fn(int) -> void) -> void {
let hk = new TbHook
hk.run = f
push(tb.on_remove, hk)
}

View file

@ -0,0 +1,83 @@
# ludic.base/ecs_index.ludic - a cached query: for each value of an int column (a kind), the rows
# holding it, kept current by the table's setters, so "every fire" is a list and "how many" a length
export property IntIndex {
col: int = 0
gate: int = -1 # an int column: a row is filed only while it is non-zero (-1: always)
lists: []words = null # value -> its rows
pos: words = null # row -> where it stands in its value's list, -1 not filed
val: words = null # row -> the slot of the value it is filed under
empty: words = null # what a value with no rows answers
sparse: IntMap = null # a value past IX_DENSE (a chunk's packed cell) to its list's slot
}
export function tb_index<T>(tb: Table<T>, col: int, gate: int) -> IntIndex {
let ix = new IntIndex
ix.col = col
ix.gate = gate
ix.lists = new []words
ix.pos = words(0)
ix.val = words(0)
ix.empty = words(0)
ix.sparse = imap_new()
for r in 0 .. tb.n { ix_grow(ix) }
push(tb.idx, ix)
for r in 0 .. tb.n { tb_ix_refile(tb, ix, r) }
return ix
}
# the rows filed under v - the index's own list: read it, never push to it
export function ix_rows(ix: IntIndex, v: int) -> words {
let s = ix_slot(ix, v, false)
if s < 0 { return ix.empty }
return ix.lists[s]
}
export function ix_count(ix: IntIndex, v: int) -> int { return len(ix_rows(ix, v)) }
# the first row filed under v, or -1
export function ix_first(ix: IntIndex, v: int) -> int {
let rows = ix_rows(ix, v)
if len(rows) == 0 { return -1 }
return rows[0]
}
function ix_grow(ix: IntIndex) -> void {
push(ix.pos, -1)
push(ix.val, -1)
}
function ix_shrink(ix: IntIndex) -> void {
List.pop(ix.pos)
List.pop(ix.val)
}
function ix_file(ix: IntIndex, r: int, v: int) -> void {
let s = ix_slot(ix, v, true)
if ix.pos[r] >= 0 and ix.val[r] == s { return }
ix_unfile(ix, r)
if s < 0 { return }
ix.pos[r] = len(ix.lists[s])
ix.val[r] = s
push(ix.lists[s], r)
}
# out of its list: the list's last row takes its place
function ix_unfile(ix: IntIndex, r: int) -> void {
let p = ix.pos[r]
if p < 0 { return }
let l = ix.lists[ix.val[r]]
let last = List.pop(l)
if last != r {
l[p] = last
ix.pos[last] = p
}
ix.pos[r] = -1
ix.val[r] = -1
}
# row `from` becomes row `to` (which is not filed)
function ix_moved(ix: IntIndex, from: int, to: int) -> void {
let p = ix.pos[from]
ix.pos[to] = p
ix.val[to] = ix.val[from]
if p >= 0 { ix.lists[ix.val[from]][p] = to }
ix.pos[from] = -1
ix.val[from] = -1
}

View file

@ -0,0 +1,24 @@
# ludic.base/ecs_index_slot.ludic - where a value's list lives: a small value (a kind) is its own
# slot; a large one (a chunk's packed cell) is given the next slot through a map, so a sparse
# value costs one list and not every list up to it
const IX_DENSE: int = 1024 # a value below this is its own slot; above, one is given it
# the slot value v's list is in, -1 when it has none (and `make` did not ask for one)
function ix_slot(ix: IntIndex, v: int, make: bool) -> int {
if v < 0 { return -1 }
if v < IX_DENSE {
if v >= len(ix.lists) {
if not make { return -1 }
while len(ix.lists) <= v { push(ix.lists, words(0)) }
}
return v
}
var s = imap_get(ix.sparse, v, -1)
if s < 0 and make {
while len(ix.lists) < IX_DENSE { push(ix.lists, words(0)) }
s = len(ix.lists)
push(ix.lists, words(0))
imap_put(ix.sparse, v, s)
}
return s
}

View file

@ -0,0 +1,79 @@
# ludic.base/ecs_map.ludic - IntMap: a key (0 or more) to a value, open addressing with linear
# probing; what turns a stable id kept in a save or a message into a handle without a scan
export property IntMap {
keys: words = null # -1 empty, -2 a key that was deleted
vals: words = null
mask: int = 63
n: int = 0
used: int = 0 # live and deleted slots: what makes probing long
}
export function imap_new() -> IntMap {
let m = new IntMap
imap_alloc(m, 64)
return m
}
function imap_alloc(m: IntMap, cap: int) -> void {
m.keys = words(cap)
m.vals = words(cap)
for i in 0 .. cap { m.keys[i] = -1 }
m.mask = cap - 1
m.n = 0
m.used = 0
}
function imap_slot(m: IntMap, k: int) -> int { return ((k * -1640531535) >> 7) & m.mask }
# the value under k, or `none` when there is none
export function imap_get(m: IntMap, k: int, none: int) -> int {
var s = imap_slot(m, k)
while m.keys[s] != -1 {
if m.keys[s] == k { return m.vals[s] }
s = (s + 1) & m.mask
}
return none
}
export function imap_put(m: IntMap, k: int, v: int) -> void {
if (m.used + 1) * 4 > (m.mask + 1) * 3 { imap_grow(m) }
var s = imap_slot(m, k)
var tomb = -1
while m.keys[s] != -1 {
if m.keys[s] == k {
m.vals[s] = v
return
}
if m.keys[s] == -2 and tomb < 0 { tomb = s }
s = (s + 1) & m.mask
}
if tomb >= 0 { s = tomb } else { m.used += 1 }
m.keys[s] = k
m.vals[s] = v
m.n += 1
}
export function imap_del(m: IntMap, k: int) -> bool {
var s = imap_slot(m, k)
while m.keys[s] != -1 {
if m.keys[s] == k {
m.keys[s] = -2
m.n -= 1
return true
}
s = (s + 1) & m.mask
}
return false
}
export function imap_len(m: IntMap) -> int { return m.n }
export function imap_clear(m: IntMap) -> void { imap_alloc(m, m.mask + 1) }
function imap_grow(m: IntMap) -> void {
let ok = m.keys
let ov = m.vals
var cap = m.mask + 1
if m.n * 2 > cap { cap = cap * 2 }
imap_alloc(m, cap)
for i in 0 .. len(ok) { if ok[i] >= 0 { imap_put(m, ok[i], ov[i]) } }
}

View file

@ -0,0 +1,46 @@
# ludic.base/ecs_plan.ludic - a query that has both a grid and a kind index picks the cheaper way:
# a rare kind's own list scanned, a common one found by rings with the kind as the match
const PLAN_LIST_MAX: int = 192 # a kind with at most this many rows is scanned from its list
export function tb_nearest_of<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: float, z: float, maxr: float, v: int) -> int {
let n = ix_count(ix, v)
if n == 0 { return -1 }
if n > PLAN_LIST_MAX { return tb_nearest(tb, g, x, z, maxr, ix.col, v) }
let rows = ix.lists[v]
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
var best = -1
var bd = 1000000000000.0
if maxr > 0.0 { bd = maxr * maxr }
for k in 0 .. n {
let r = rows[k]
if g.gate >= 0 and tb.i[g.gate][r] == 0 { continue }
let dx = xs[r] - x
let dz = zs[r] - z
let d2 = dx * dx + dz * dz
if d2 < bd or (d2 == bd and best < 0) {
bd = d2
best = r
}
}
return best
}
# within r of (x, z), of kind v, into `out`
export function tb_within_of<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: float, z: float, r: float, v: int, out: words) -> int {
let n = ix_count(ix, v)
if n > PLAN_LIST_MAX { return tb_within(tb, g, x, z, r, ix.col, v, out) }
List.clear(out)
if n == 0 { return 0 }
let rows = ix.lists[v]
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
for k in 0 .. n {
let row = rows[k]
if g.gate >= 0 and tb.i[g.gate][row] == 0 { continue }
let dx = xs[row] - x
let dz = zs[row] - z
if dx * dx + dz * dz <= r * r { push(out, row) }
}
return len(out)
}

View file

@ -0,0 +1,96 @@
# ludic.base/ecs_table.ludic - a Table<T>: entities as dense rows of hot columns (floats, words)
# and one record each for what is cold. A handle is a slot and a generation, so a stale one misses.
export const ECS_SLOT: int = 4194303 # the low 22 bits of a handle: its slot
export const ECS_GEN: int = 511 # the next 9: how many times the slot was reused
export const ECS_NONE: int = -1
export property Table<T> {
n: int = 0
rec: []T = null
ent: words = null # row -> handle
f: []floats = null # hot float columns, a value per row each
i: []words = null # hot int columns
added: words = null # row -> the tick it was added at
changed: words = null # row -> the tick a setter last wrote it at
blk: words = null # 64-row block -> the newest tick any row in it was added or written at
tick: int = 1
slot_gen: words = null # slot -> generation
slot_row: words = null # slot -> row, -1 while free
free: words = null
grids: []Grid = null
idx: []IntIndex = null
on_add: []TbHook = null # observers: told a handle as its row is made, and before it goes
on_remove: []TbHook = null
}
# a table of nf float and ni int columns, every value 0 in a new row
export function table_new<T>(nf: int, ni: int) -> Table<T> {
let tb = new Table<T>
tb.rec = new []T
tb.ent = words(0)
tb.f = new []floats
tb.i = new []words
for c in 0 .. nf { push(tb.f, floats(0)) }
for c in 0 .. ni { push(tb.i, words(0)) }
tb.added = words(0)
tb.changed = words(0)
tb.blk = words(0)
tb.slot_gen = words(0)
tb.slot_row = words(0)
tb.free = words(0)
tb.grids = new []Grid
tb.idx = new []IntIndex
tb.on_add = new []TbHook
tb.on_remove = new []TbHook
return tb
}
export function tb_len<T>(tb: Table<T>) -> int { return tb.n }
export function tb_rec<T>(tb: Table<T>, row: int) -> T { return tb.rec[row] }
export function tb_handle<T>(tb: Table<T>, row: int) -> int { return tb.ent[row] }
export function tb_tick<T>(tb: Table<T>) -> int { return tb.tick }
# the next tick: what is written from here on is newer than anything before
export function tb_advance<T>(tb: Table<T>) -> void { tb.tick += 1 }
# the row a handle names, or -1 when it was removed (or the slot has been reused since)
export function tb_row<T>(tb: Table<T>, h: int) -> int {
if h < 0 { return -1 }
let s = h & ECS_SLOT
if s >= len(tb.slot_gen) or tb.slot_gen[s] != ((h >> 22) & ECS_GEN) { return -1 }
return tb.slot_row[s]
}
export function tb_alive<T>(tb: Table<T>, h: int) -> bool { return tb_row(tb, h) >= 0 }
# a new row holding `r`, every column 0 and filed in no index until a setter says where it is
export function tb_add<T>(tb: Table<T>, r: T) -> int {
var s = 0
if len(tb.free) > 0 { s = List.pop(tb.free) } else {
s = len(tb.slot_gen)
push(tb.slot_gen, 0)
push(tb.slot_row, -1)
}
tb.slot_row[s] = tb.n
let h = s | (tb.slot_gen[s] << 22)
push(tb.rec, r)
push(tb.ent, h)
push(tb.added, tb.tick)
push(tb.changed, tb.tick)
for c in 0 .. len(tb.f) { push(tb.f[c], 0.0) }
for c in 0 .. len(tb.i) { push(tb.i[c], 0) }
tb.n += 1
for g in 0 .. len(tb.grids) { grid_grow(tb.grids[g]) }
for k in 0 .. len(tb.idx) { ix_grow(tb.idx[k]) }
tb_stamp(tb, tb.n - 1)
for k in 0 .. len(tb.on_add) { tb.on_add[k].run(h) }
return h
}
# the row's tick, and its block's: what change detection reads
function tb_stamp<T>(tb: Table<T>, row: int) -> void {
tb.changed[row] = tb.tick
let b = row >> 6
while len(tb.blk) <= b { push(tb.blk, 0) }
tb.blk[b] = tb.tick
}

View file

@ -0,0 +1,49 @@
# ludic.base/ecs_table_remove.ludic - removing a row moves the last one into its place, so the
# rows stay dense; every index is told, and the slot's generation moves on
export function tb_remove<T>(tb: Table<T>, h: int) -> bool {
if tb_row(tb, h) < 0 { return false }
for k in 0 .. len(tb.on_remove) { tb.on_remove[k].run(h) }
let r = tb_row(tb, h)
if r < 0 { return false }
for g in 0 .. len(tb.grids) { grid_unlink(tb.grids[g], r) }
for k in 0 .. len(tb.idx) { ix_unfile(tb.idx[k], r) }
let last = tb.n - 1
if r != last { tb_move(tb, last, r) }
tb_pop(tb)
let s = h & ECS_SLOT
tb.slot_row[s] = -1
tb.slot_gen[s] = (tb.slot_gen[s] + 1) & ECS_GEN
push(tb.free, s)
return true
}
function tb_move<T>(tb: Table<T>, from: int, to: int) -> void {
tb.rec[to] = tb.rec[from]
tb.ent[to] = tb.ent[from]
tb.added[to] = tb.added[from]
tb.changed[to] = tb.changed[from]
for c in 0 .. len(tb.f) { tb.f[c][to] = tb.f[c][from] }
for c in 0 .. len(tb.i) { tb.i[c][to] = tb.i[c][from] }
tb.slot_row[tb.ent[to] & ECS_SLOT] = to
let b = to >> 6
if tb.blk[b] < tb.changed[to] { tb.blk[b] = tb.changed[to] }
for g in 0 .. len(tb.grids) { grid_moved(tb.grids[g], from, to) }
for k in 0 .. len(tb.idx) { ix_moved(tb.idx[k], from, to) }
}
function tb_pop<T>(tb: Table<T>) -> void {
List.pop(tb.rec)
List.pop(tb.ent)
List.pop(tb.added)
List.pop(tb.changed)
for c in 0 .. len(tb.f) { List.pop(tb.f[c]) }
for c in 0 .. len(tb.i) { List.pop(tb.i[c]) }
for g in 0 .. len(tb.grids) { grid_shrink(tb.grids[g]) }
for k in 0 .. len(tb.idx) { ix_shrink(tb.idx[k]) }
tb.n -= 1
}
# every row gone; the slots keep their generations, so an old handle still misses
export function tb_clear<T>(tb: Table<T>) -> void {
while tb.n > 0 { tb_remove(tb, tb.ent[tb.n - 1]) }
}

View file

@ -0,0 +1,76 @@
# ludic.base/ecs_table_set.ludic - the setters: a write stamps the row's tick and refiles it in
# every index that reads that column, so an index is never a frame behind what it indexes
export function tb_f<T>(tb: Table<T>, c: int, row: int) -> float { return tb.f[c][row] }
export function tb_i<T>(tb: Table<T>, c: int, row: int) -> int { return tb.i[c][row] }
export function tb_set_f<T>(tb: Table<T>, c: int, row: int, v: float) -> void {
tb.f[c][row] = v
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) {
let gr = tb.grids[g]
if gr.cx == c or gr.cz == c { tb_grid_refile(tb, gr, row) }
}
}
# two float columns at once, filed once: a move
export function tb_set_xz<T>(tb: Table<T>, cx: int, cz: int, row: int, x: float, z: float) -> void {
tb.f[cx][row] = x
tb.f[cz][row] = z
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) {
let gr = tb.grids[g]
if gr.cx == cx or gr.cz == cz or gr.cx == cz or gr.cz == cx { tb_grid_refile(tb, gr, row) }
}
}
export function tb_set_i<T>(tb: Table<T>, c: int, row: int, v: int) -> void {
tb.i[c][row] = v
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) { if tb.grids[g].gate == c { tb_grid_refile(tb, tb.grids[g], row) } }
for k in 0 .. len(tb.idx) {
let ix = tb.idx[k]
if ix.col == c or ix.gate == c { tb_ix_refile(tb, ix, row) }
}
}
# a row written without a setter (tb.f / tb.i directly, for speed) is refiled everywhere by this
export function tb_refile<T>(tb: Table<T>, row: int) -> void {
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) { tb_grid_refile(tb, tb.grids[g], row) }
for k in 0 .. len(tb.idx) { tb_ix_refile(tb, tb.idx[k], row) }
}
export function tb_mark<T>(tb: Table<T>, row: int) -> void { tb_stamp(tb, row) }
function tb_gate_on<T>(tb: Table<T>, gate: int, row: int) -> bool { return gate < 0 or tb.i[gate][row] != 0 }
function tb_grid_refile<T>(tb: Table<T>, g: Grid, row: int) -> void {
grid_refile(g, row, tb.f[g.cx][row], tb.f[g.cz][row], tb_gate_on(tb, g.gate, row))
}
function tb_ix_refile<T>(tb: Table<T>, ix: IntIndex, row: int) -> void {
if tb_gate_on(tb, ix.gate, row) { ix_file(ix, row, tb.i[ix.col][row]) } else { ix_unfile(ix, row) }
}
# change detection: the rows a setter wrote after `since`, into `out` (cleared first); no allocation
export function tb_changed_since<T>(tb: Table<T>, since: int, out: words) -> int {
List.clear(out)
for b in 0 .. len(tb.blk) {
if tb.blk[b] <= since { continue }
var end = b * 64 + 64
if end > tb.n { end = tb.n }
for r in b * 64 .. end { if tb.changed[r] > since { push(out, r) } }
}
return len(out)
}
export function tb_added_since<T>(tb: Table<T>, since: int, out: words) -> int {
List.clear(out)
for b in 0 .. len(tb.blk) {
if tb.blk[b] <= since { continue }
var end = b * 64 + 64
if end > tb.n { end = tb.n }
for r in b * 64 .. end { if tb.added[r] > since { push(out, r) } }
}
return len(out)
}

View file

@ -8,3 +8,17 @@ import "rng.ludic"
import "save.ludic" import "save.ludic"
import "save_fields.ludic" import "save_fields.ludic"
import "system.ludic" import "system.ludic"
import "ecs_table.ludic"
import "ecs_hooks.ludic"
import "ecs_table_remove.ludic"
import "ecs_table_set.ludic"
import "ecs_grid.ludic"
import "ecs_grid_file.ludic"
import "ecs_grid_query.ludic"
import "ecs_grid_within.ludic"
import "ecs_grid_where.ludic"
import "ecs_index.ludic"
import "ecs_index_slot.ludic"
import "ecs_map.ludic"
import "ecs_plan.ludic"
import "ecs_group.ludic"

View file

@ -3,6 +3,8 @@
export property Queue<T> { export property Queue<T> {
items: []T = null items: []T = null
tag: QueueTag = null tag: QueueTag = null
none: []T = null # what draining an empty queue hands back, never pushed to
spare: []T = null # the list the last drain handed out: cleared and reused by the next
} }
# what a queue says about itself, whatever it holds: its name and how many facts wait in it # what a queue says about itself, whatever it holds: its name and how many facts wait in it
export property QueueTag { export property QueueTag {
@ -22,6 +24,8 @@ export function queue_new<T>(name: string) -> Queue<T> {
q.items = new []T q.items = new []T
q.tag = new QueueTag q.tag = new QueueTag
q.tag.name = name q.tag.name = name
q.none = new []T
q.spare = new []T
return q return q
} }
@ -30,18 +34,25 @@ export function q_push<T>(q: Queue<T>, v: T) -> void {
q.tag.pending = len(q.items) q.tag.pending = len(q.items)
} }
# the facts in the order they were pushed; the queue is empty afterwards # the facts in the order they were pushed; the queue is empty afterwards. Ludic frees nothing, so
# a drain allocates nothing: the queue keeps two lists and hands one out while the other fills, and
# an empty drain hands back one shared empty list. What a drain returns is good until the NEXT
# drain of the same queue - read it, never push to it, never keep it.
export function q_drain<T>(q: Queue<T>) -> []T { export function q_drain<T>(q: Queue<T>) -> []T {
if len(q.items) == 0 { return q.none }
let out = q.items let out = q.items
q.items = new []T List.clear(q.spare)
q.items = q.spare
q.spare = out
q.tag.pending = 0 q.tag.pending = 0
return out return out
} }
export function q_len<T>(q: Queue<T>) -> int { return len(q.items) } export function q_len<T>(q: Queue<T>) -> int { return len(q.items) }
# in place: the live list was never handed out (a drain gives its list away and starts another)
export function q_clear<T>(q: Queue<T>) -> void { export function q_clear<T>(q: Queue<T>) -> void {
q.items = new []T List.clear(q.items)
q.tag.pending = 0 q.tag.pending = 0
} }

View file

@ -0,0 +1,130 @@
# ecs_fuzz_test.ludic - thousands of random adds, removes, moves, kind changes and gate flips, and
# after every burst the grid and the kind index answer exactly what a scan of the rows answers
import "ludic.base"
program EcsFuzzTest {
numbers float
property Toy { n: int = 0 }
const CX: int = 0
const CZ: int = 1
const KIND: int = 0
const ON: int = 1
const KINDS: int = 6
state FuzzState {
seed: int = 12345
}
function rnd(fz: mut FuzzState, n: int) -> int {
fz.seed = (fz.seed * 1103515245 + 12345) & 2147483647
return (fz.seed >> 8) % n
}
function rpos(fz: mut FuzzState) -> float { return float(rnd(fz, 40000)) / 10.0 - 2000.0 }
function step(fz: mut FuzzState, tb: Table<Toy>) -> void {
let op = rnd(fz, 10)
if op < 4 or tb_len(tb) == 0 {
let toy = new Toy
toy.n = rnd(fz, 5)
let r = tb_row(tb, tb_add(tb, toy))
tb_set_xz(tb, CX, CZ, r, rpos(fz), rpos(fz))
tb_set_i(tb, KIND, r, rnd(fz, KINDS))
tb_set_i(tb, ON, r, rnd(fz, 4) % 3)
return
}
let r = rnd(fz, tb_len(tb))
if op < 6 { tb_remove(tb, tb_handle(tb, r)) } else if op < 8 {
tb_set_xz(tb, CX, CZ, r, tb_f(tb, CX, r) + float(rnd(fz, 200)) - 100.0, tb_f(tb, CZ, r) + float(rnd(fz, 200)) - 100.0)
} else if op < 9 { tb_set_i(tb, KIND, r, rnd(fz, KINDS)) } else { tb_set_i(tb, ON, r, 1 - tb_i(tb, ON, r)) }
}
# the scan's answer: the smallest squared distance among on rows matching, or -1
function brute_d2(tb: Table<Toy>, x: float, z: float, maxr: float, kind: int) -> float {
var best = -1.0
for r in 0 .. tb_len(tb) {
if tb_i(tb, ON, r) == 0 or (kind >= 0 and tb_i(tb, KIND, r) != kind) { continue }
let dx = tb_f(tb, CX, r) - x
let dz = tb_f(tb, CZ, r) - z
let d2 = dx * dx + dz * dz
if (maxr <= 0.0 or d2 <= maxr * maxr) and (best < 0.0 or d2 < best) { best = d2 }
}
return best
}
function keep_even(t: Toy) -> bool { return t.n % 2 == 0 }
function brute_even(tb: Table<Toy>, x: float, z: float, maxr: float) -> float {
var best = -1.0
for r in 0 .. tb_len(tb) {
if tb_i(tb, ON, r) == 0 or tb_rec(tb, r).n % 2 != 0 { continue }
let dx = tb_f(tb, CX, r) - x
let dz = tb_f(tb, CZ, r) - z
let d2 = dx * dx + dz * dz
if (maxr <= 0.0 or d2 < maxr * maxr) and (best < 0.0 or d2 < best) { best = d2 }
}
return best
}
function where_check(tb: Table<Toy>, g: Grid, ix: IntIndex, x: float, z: float, maxr: float, kind: int, recs: []Toy) -> void {
let want = brute_even(tb, x, z, maxr)
let row = tb_nearest_where(tb, g, x, z, maxr, fn keep_even)
if want < 0.0 { expect_eq(row, -1) } else {
let dx = tb_f(tb, CX, row) - x
let dz = tb_f(tb, CZ, row) - z
expect(dx * dx + dz * dz == want)
}
expect_eq(tb_within_recs(tb, g, x, z, 150.0, -1, 0, recs), brute_within(tb, x, z, 150.0, -1))
if kind >= 0 { expect_eq(tb_within_of_recs(tb, g, ix, x, z, 150.0, kind, recs), brute_within(tb, x, z, 150.0, kind)) }
}
function brute_within(tb: Table<Toy>, x: float, z: float, r: float, kind: int) -> int {
var n = 0
for i in 0 .. tb_len(tb) {
if tb_i(tb, ON, i) == 0 or (kind >= 0 and tb_i(tb, KIND, i) != kind) { continue }
let dx = tb_f(tb, CX, i) - x
let dz = tb_f(tb, CZ, i) - z
if dx * dx + dz * dz <= r * r { n += 1 }
}
return n
}
function check(fz: mut FuzzState, tb: Table<Toy>, g: Grid, ix: IntIndex, out: words, recs: []Toy) -> void {
for q in 0 .. 20 {
let x = rpos(fz)
let z = rpos(fz)
let kind = rnd(fz, KINDS + 1) - 1
var maxr = 0.0
if q % 2 == 1 { maxr = float(rnd(fz, 600)) }
var mc = KIND
if kind < 0 { mc = -1 }
let want = brute_d2(tb, x, z, maxr, kind)
let row = tb_nearest(tb, g, x, z, maxr, mc, kind)
if want < 0.0 { expect_eq(row, -1) } else {
expect(row >= 0)
let dx = tb_f(tb, CX, row) - x
let dz = tb_f(tb, CZ, row) - z
expect(dx * dx + dz * dz == want)
}
where_check(tb, g, ix, x, z, maxr, kind, recs)
let rad = float(rnd(fz, 400))
expect_eq(tb_within(tb, g, x, z, rad, mc, kind, out), brute_within(tb, x, z, rad, kind))
}
for k in 0 .. KINDS { expect_eq(ix_count(ix, k), brute_within(tb, 0.0, 0.0, 100000.0, k)) }
}
function fuzz_case(fz: mut FuzzState) -> void {
let tb: Table<Toy> = table_new(2, 2)
let g = tb_grid(tb, CX, CZ, ON, 16.0)
let ix = tb_index(tb, KIND, ON)
let out = words(0)
let recs = new []Toy
for burst in 0 .. 60 {
for s in 0 .. 150 { step(fz, tb) }
check(fz, tb, g, ix, out, recs)
}
tb_clear(tb)
expect_eq(grid_count(g), 0)
expect_eq(ix_count(ix, 1), 0)
}
test "the grid and the kind index agree with a scan through 9000 random changes" (fz: mut FuzzState) { fuzz_case(fz) }
}

View file

@ -0,0 +1,186 @@
# ecs_test.ludic - Table<T>: handles that go stale, dense rows after a removal, the setters keeping
# the grid and the kind index current, change ticks, and IntMap. Each case is a function because a
# generic call is not resolved inside a `test` body yet.
import "ludic.base"
program EcsTest {
numbers float
property Toy { name: string = "" }
const CX: int = 0
const CZ: int = 1
const KIND: int = 0
const ON: int = 1
state HookState {
added: int = 0
gone: int = 0
last: int = -1
}
function seen_add(hook_st: mut HookState, h: int) -> void {
hook_st.added += 1
hook_st.last = h
}
function seen_gone(hook_st: mut HookState, h: int) -> void { hook_st.gone += 1 }
function toy(name: string) -> Toy {
let t = new Toy
t.name = name
return t
}
function put(tb: Table<Toy>, name: string, x: float, z: float, kind: int) -> int {
let h = tb_add(tb, toy(name))
let r = tb_row(tb, h)
tb_set_xz(tb, CX, CZ, r, x, z)
tb_set_i(tb, KIND, r, kind)
tb_set_i(tb, ON, r, 1)
return h
}
function handles_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let a = put(tb, "a", 0.0, 0.0, 1)
let b = put(tb, "b", 1.0, 0.0, 1)
let c = put(tb, "c", 2.0, 0.0, 2)
expect_eq(tb_len(tb), 3)
expect(tb_remove(tb, a))
expect(not tb_alive(tb, a))
expect(not tb_remove(tb, a))
expect_eq(tb_len(tb), 2)
expect_eq(tb_rec(tb, tb_row(tb, c)).name, "c")
expect_eq(tb_rec(tb, tb_row(tb, b)).name, "b")
let d = put(tb, "d", 3.0, 0.0, 1)
expect(d != a)
expect_eq(d & ECS_SLOT, a & ECS_SLOT)
expect(not tb_alive(tb, a))
expect_eq(tb_rec(tb, tb_row(tb, d)).name, "d")
}
function grid_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let g = tb_grid(tb, CX, CZ, ON, 16.0)
let near = put(tb, "near", 5.0, 5.0, 1)
put(tb, "far", 500.0, -300.0, 1)
let fire = put(tb, "fire", -40.0, 2.0, 2)
expect_eq(tb_handle(tb, tb_nearest(tb, g, 0.0, 0.0, 0.0, -1, 0)), near)
expect_eq(tb_handle(tb, tb_nearest(tb, g, 0.0, 0.0, 0.0, KIND, 2)), fire)
expect_eq(tb_nearest(tb, g, 0.0, 0.0, 10.0, KIND, 2), -1)
tb_set_i(tb, ON, tb_row(tb, near), 0)
expect_eq(tb_handle(tb, tb_nearest(tb, g, 0.0, 0.0, 0.0, KIND, 1)), tb_handle(tb, tb_nearest(tb, g, 499.0, -300.0, 0.0, -1, 0)))
let out = words(0)
expect_eq(tb_within(tb, g, 0.0, 0.0, 50.0, -1, 0, out), 1)
tb_set_i(tb, ON, tb_row(tb, near), 1)
expect_eq(tb_within(tb, g, 0.0, 0.0, 50.0, -1, 0, out), 2)
tb_set_xz(tb, CX, CZ, tb_row(tb, fire), 900.0, 900.0)
expect_eq(tb_within(tb, g, 0.0, 0.0, 50.0, -1, 0, out), 1)
}
function index_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let ix = tb_index(tb, KIND, ON)
let a = put(tb, "a", 0.0, 0.0, 3)
put(tb, "b", 0.0, 0.0, 3)
put(tb, "c", 0.0, 0.0, 5)
expect_eq(ix_count(ix, 3), 2)
expect_eq(ix_count(ix, 5), 1)
expect_eq(ix_count(ix, 99), 0)
tb_set_i(tb, ON, tb_row(tb, a), 0)
expect_eq(ix_count(ix, 3), 1)
tb_remove(tb, a)
tb_set_i(tb, KIND, ix_first(ix, 3), 5)
expect_eq(ix_count(ix, 3), 0)
expect_eq(ix_count(ix, 5), 2)
}
function ticks_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let a = put(tb, "a", 0.0, 0.0, 1)
put(tb, "b", 0.0, 0.0, 1)
let seen = tb_tick(tb)
tb_advance(tb)
let out = words(0)
expect_eq(tb_changed_since(tb, seen, out), 0)
tb_set_f(tb, CX, tb_row(tb, a), 4.0)
expect_eq(tb_changed_since(tb, seen, out), 1)
expect_eq(out[0], tb_row(tb, a))
expect_eq(tb_added_since(tb, seen, out), 0)
}
function map_case() -> void {
let m = imap_new()
for k in 0 .. 5000 { imap_put(m, k * 7, k) }
expect_eq(imap_len(m), 5000)
expect_eq(imap_get(m, 700, -1), 100)
expect_eq(imap_get(m, 701, -1), -1)
for k in 0 .. 2500 { imap_del(m, k * 14) }
expect_eq(imap_len(m), 2500)
expect_eq(imap_get(m, 14, -1), -1)
expect_eq(imap_get(m, 21, -1), 3)
imap_put(m, 14, 9)
expect_eq(imap_get(m, 14, -1), 9)
}
function hooks_case(hook_st: HookState) -> void {
let tb: Table<Toy> = table_new(2, 2)
tb_on_add(tb, fn seen_add)
tb_on_remove(tb, fn seen_gone)
let a = put(tb, "a", 0.0, 0.0, 1)
expect_eq(hook_st.last, a)
put(tb, "b", 0.0, 0.0, 1)
tb_remove(tb, a)
tb_remove(tb, a)
expect_eq(hook_st.added, 2)
expect_eq(hook_st.gone, 1)
}
# 300 rows over five blocks; a change in one block is found, the untouched blocks skipped
function blocks_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
for i in 0 .. 300 { put(tb, "x", float(i), 0.0, 1) }
let seen = tb_tick(tb)
tb_advance(tb)
let out = words(0)
expect_eq(tb_changed_since(tb, seen, out), 0)
tb_set_f(tb, CX, 200, 9.0)
tb_set_f(tb, CX, 7, 9.0)
expect_eq(tb_changed_since(tb, seen, out), 2)
expect_eq(out[0], 7)
expect_eq(out[1], 200)
let before = tb_tick(tb)
tb_advance(tb)
tb_remove(tb, tb_handle(tb, 3))
expect_eq(tb_changed_since(tb, before, out), 0) # a row moved into a gap was not written
expect_eq(tb_changed_since(tb, seen, out), 2) # a removal is told by tb_on_remove
}
# values far apart (a chunk's packed cell) each take one list, not every list up to them
function sparse_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let ix = tb_index(tb, KIND, -1)
let a = chunk_of(9000.0, -9000.0, 256.0)
let b = chunk_of(-512.0, 700.0, 256.0)
expect(a > 100000 and b > 100000)
put(tb, "a", 0.0, 0.0, a)
put(tb, "b", 0.0, 0.0, b)
let c = put(tb, "c", 0.0, 0.0, a)
put(tb, "d", 0.0, 0.0, 3)
expect_eq(ix_count(ix, a), 2)
expect_eq(ix_count(ix, b), 1)
expect_eq(ix_count(ix, 3), 1)
expect(len(ix.lists) < 1100)
tb_set_i(tb, KIND, tb_row(tb, c), b)
expect_eq(ix_count(ix, a), 1)
expect_eq(ix_count(ix, b), 2)
expect_eq(tb_remove_all(tb, ix, b), 2)
expect_eq(ix_count(ix, b), 0)
expect_eq(tb_len(tb), 2)
}
test "a sparse value takes one list" () { sparse_case() }
test "observers hear each add, and a remove once" (hook_st: HookState) { hooks_case(hook_st) }
test "change detection skips the blocks nobody wrote" () { blocks_case() }
test "a removed handle misses, rows stay dense, a reused slot is a new handle" () { handles_case() }
test "the grid answers nearest and within, gated, after moves" () { grid_case() }
test "the kind index follows the setters, the gate and removals" () { index_case() }
test "change ticks name the rows written since" () { ticks_case() }
test "IntMap puts, finds, deletes and grows" () { map_case() }
}

View file

@ -67,7 +67,44 @@ program QueueTest {
expect_eq(len(core_undrained(tags)), 0) expect_eq(len(core_undrained(tags)), 0)
} }
# an empty drain allocates nothing and hands back the same empty list; a drained list is the
# caller's, untouched by what is pushed or cleared after it
function empty_case() -> void {
let q: Queue<int> = queue_new("quiet")
let a = q_drain(q)
let b = q_drain(q)
expect_eq(len(a), 0)
expect(a == b)
q_push(q, 5)
expect_eq(len(a), 0)
let got = q_drain(q)
q_push(q, 6)
q_clear(q)
expect_eq(len(got), 1)
expect_eq(got[0], 5)
expect_eq(q_len(q), 0)
expect_eq(len(q_drain(q)), 0)
}
test "a queue drains in the order it was pushed" () { fifo_case() } test "a queue drains in the order it was pushed" () { fifo_case() }
# two lists, reused: a drained list is the caller's until the next drain of that queue
function reuse_case() -> void {
let q: Queue<int> = queue_new("reused")
q_push(q, 1)
let a = q_drain(q)
q_push(q, 2)
expect_eq(a[0], 1)
let b = q_drain(q)
expect_eq(b[0], 2)
q_push(q, 3)
let c = q_drain(q)
expect(c == a)
expect_eq(len(c), 1)
expect_eq(c[0], 3)
}
test "a drain reuses the queue's two lists" () { reuse_case() }
test "an empty drain allocates nothing, and a drained list is the caller's" () { empty_case() }
test "a queue holds records" () { record_case() } test "a queue holds records" () { record_case() }
test "clear drops what is waiting" () { clear_case() } test "clear drops what is waiting" () { clear_case() }
test "a queue's element type is told by the queue alone" () { infer_case() } test "a queue's element type is told by the queue alone" () { infer_case() }

View file

@ -30,6 +30,7 @@ property Actor {
cast_hidden: bool = false, cast_hidden: bool = false,
casts: bool = true, casts: bool = true,
id: int = 0, id: int = 0,
row: int = -1, # where it stands in ac_actors, so it leaves in O(1); -1 off the stage
cutout: bool = false, # alpha-tested (a flame's cards) cutout: bool = false, # alpha-tested (a flame's cards)
emissive: float = 0.0, # float bits: self-lit strength emissive: float = 0.0, # float bits: self-lit strength
skin: Skin, # this instance's own pose (skin_clone); null: the model's skin: Skin, # this instance's own pose (skin_clone); null: the model's
@ -82,11 +83,27 @@ function actor_init(render3d_st: mut Render3dState) -> void {
render3d_st.ac_out_cut = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n") render3d_st.ac_out_cut = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n")
render3d_st.ac_actors = new []Actor render3d_st.ac_actors = new []Actor
} }
# off the stage: the last actor takes its row, so the list stays dense and nothing is rebuilt
# (it was a new list of every other actor per removal, never freed)
function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void { function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void {
if render3d_st.ac_actors == null { return } let l = render3d_st.ac_actors
let keep = new []Actor if l == null or a == null { return }
for i in 0 .. len(render3d_st.ac_actors) { if render3d_st.ac_actors[i].id != a.id { push(keep, render3d_st.ac_actors[i]) } } let r = a.row
render3d_st.ac_actors = keep if r < 0 or r >= len(l) or l[r].id != a.id { return }
let last = List.pop(l)
if last.id != a.id {
l[r] = last
last.row = r
}
a.row = -1
}
# back on the stage (an actor kept across a world swap)
function actor_keep(render3d_st: mut Render3dState, a: Actor) -> void {
if a == null { return }
if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor }
a.row = len(render3d_st.ac_actors)
push(render3d_st.ac_actors, a)
} }
# colour one named part of the model (a material name from the file) # colour one named part of the model (a material name from the file)
function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void { function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void {
@ -114,8 +131,7 @@ function actor_new(render3d_st: mut Render3dState, model: Model) -> Actor {
render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id
if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 } if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 }
a.cull = 450.0 a.cull = 450.0
if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor } actor_keep(render3d_st, a)
push(render3d_st.ac_actors, a)
return a return a
} }
@ -396,6 +412,8 @@ function ac_caster_count(render3d_st: mut Render3dState, a: Actor) -> void {
# every actor off the stage at once, for a world being replaced. Actors own no GL objects; # every actor off the stage at once, for a world being replaced. Actors own no GL objects;
# their models belong to whoever loaded them. # their models belong to whoever loaded them.
function actor_clear_all(render3d_st: mut Render3dState) -> void { function actor_clear_all(render3d_st: mut Render3dState) -> void {
render3d_st.ac_actors = new []Actor if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor }
for i in 0 .. len(render3d_st.ac_actors) { render3d_st.ac_actors[i].row = -1 }
List.clear(render3d_st.ac_actors)
outline_clear(render3d_st) outline_clear(render3d_st)
} }

View file

@ -189,6 +189,9 @@ export state Render3dState {
gvk_pool: long = 0 gvk_pool: long = 0
gvk_fence: bytes = null gvk_fence: bytes = null
gvk_frame_fence: bytes = null # the presented frame still on the GPU (one frame in flight) gvk_frame_fence: bytes = null # the presented frame still on the GPU (one frame in flight)
gvk_tmp_buf: pointer = null # the per-call scratch ring (gvk_tmp)
gvk_tmp_off: int = 0
gvk_seen: words = null # gvk_draw's vertex buffers already bound, reused
gvk_frame_pending: bool = false gvk_frame_pending: bool = false
gvk_frame_pending_cb: pointer = null gvk_frame_pending_cb: pointer = null
gvk_frame_pending_no: int = -1 # which frame it is (gvk_frame_no when it was submitted) gvk_frame_pending_no: int = -1 # which frame it is (gvk_frame_no when it was submitted)
@ -197,6 +200,20 @@ export state Render3dState {
gvk_retired_frame: []int = null # the last frame that read each retired buffer gvk_retired_frame: []int = null # the last frame that read each retired buffer
gvk_labels: bool = false # R3D_VK_LABELS: each profiled pass is a debug label (Metal System Trace) gvk_labels: bool = false # R3D_VK_LABELS: each profiled pass is a debug label (Metal System Trace)
gvk_inflight: int = -1 gvk_inflight: int = -1
gvk_nopool: bool = false
# R3D_VK_PROF: Vulkan objects made and destroyed, reported every 120 frames
gvk_mk_img: int = 0
gvk_mk_view: int = 0
gvk_mk_buf: int = 0
gvk_mk_mem: int = 0
gvk_mk_smp: int = 0
gvk_mk_set: int = 0
gvk_mk_cmd: int = 0
gvk_mk_dpool: int = 0
gvk_mk_x_img: int = 0
gvk_mk_x_view: int = 0
gvk_mk_x_buf: int = 0
gvk_mk_x_mem: int = 0
gvk_label_depth: int = 0 # -1 until asked: 1 leaves the presented frame in flight gvk_label_depth: int = 0 # -1 until asked: 1 leaves the presented frame in flight
gvk_has_hqr: bool = false gvk_has_hqr: bool = false
gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
@ -269,8 +286,9 @@ export state Render3dState {
gvk_screen_depth: int = 0 gvk_screen_depth: int = 0
gvk_screen_w: int = 0 gvk_screen_w: int = 0
gvk_screen_h: int = 0 gvk_screen_h: int = 0
gvk_layer_views: []string = null # "tex:layer" -> index into gvk_layer_view gvk_layer_views: []int = null # generation * 4096 + layer, beside gvk_layer_view_tex
gvk_layer_view: []long = null gvk_layer_view: []long = null
gvk_layer_view_tex: []int = null # the texture each cached view is of, so a release drops them
gvk_fb_ncolor: words = null # per framebuffer: colour slots drawn (draw buffers; 0 = none) gvk_fb_ncolor: words = null # per framebuffer: colour slots drawn (draw buffers; 0 = none)
gvk_hdr_want: bool = false # r3d_hdr: the setting gvk_hdr_want: bool = false # r3d_hdr: the setting
gvk_hdr_on: bool = false # the swapchain is HDR10 now gvk_hdr_on: bool = false # the swapchain is HDR10 now
@ -377,6 +395,11 @@ export state Render3dState {
gg_mesh: []Mesh = null gg_mesh: []Mesh = null
gg_n: int = 0 gg_n: int = 0
gg_max: int = 0 gg_max: int = 0
gg_rb: words = null # the per-frame dispatch arguments, made once (grass_gpu.ludic)
gg_cb: words = null
gg_bufs: words = null
gg_texs: words = null
gg_pr: words = null
grass_band_mesh: []Mesh = null # per band: the blade it draws (rows fall with distance) grass_band_mesh: []Mesh = null # per band: the blade it draws (rows fall with distance)
grass_mesh3: Mesh = null # three rows: the middle distance grass_mesh3: Mesh = null # three rows: the middle distance
grass_mesh2: Mesh = null # two rows, one quad: far enough that the arch is under a pixel grass_mesh2: Mesh = null # two rows, one quad: far enough that the arch is under a pixel

View file

@ -201,7 +201,7 @@ function gpu_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> vo
function gpu_query_begin(render3d_st: mut Render3dState, id: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_begin(render3d_st, id); return }; gl_begin_query(GL_TIME_ELAPSED, id) } function gpu_query_begin(render3d_st: mut Render3dState, id: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_begin(render3d_st, id); return }; gl_begin_query(GL_TIME_ELAPSED, id) }
function gpu_query_end(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_end(render3d_st); return }; gl_end_query(GL_TIME_ELAPSED) } function gpu_query_end(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_end(render3d_st); return }; gl_end_query(GL_TIME_ELAPSED) }
# true once the query has its result; the nanoseconds (low 32 bits) are then in out[0] # true once the query has its result; the nanoseconds (low 32 bits) are then in out[0]
function gpu_query_result(render3d_st: Render3dState, id: int, out: words) -> bool { function gpu_query_result(render3d_st: mut Render3dState, id: int, out: words) -> bool {
if render3d_st.gpu_kind == GPU_VK { return gvk_query_result(render3d_st, id, out) } if render3d_st.gpu_kind == GPU_VK { return gvk_query_result(render3d_st, id, out) }
gl_get_query_objectiv(id, GL_QUERY_RESULT_AVAILABLE, out) gl_get_query_objectiv(id, GL_QUERY_RESULT_AVAILABLE, out)
if out[0] == 0 { return false } if out[0] == 0 { return false }

View file

@ -292,12 +292,13 @@ function gvk_alloc(render3d_st: mut Render3dState, req: bytes, want: int) -> lon
if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(render3d_st, allowed, 0) } if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(render3d_st, allowed, 0) }
let zero: long = 0 let zero: long = 0
if t < 0 { print(`r3d: vulkan: no memory type for properties {want}`); return zero } if t < 0 { print(`r3d: vulkan: no memory type for properties {want}`); return zero }
let mai = bytes(VkMemoryAllocateInfo_sizeof) let mai = gvk_tmp(render3d_st, VkMemoryAllocateInfo_sizeof)
Vk.zero(mai, VkMemoryAllocateInfo_sizeof) Vk.zero(mai, VkMemoryAllocateInfo_sizeof)
Vk.put_i32(mai, VkMemoryAllocateInfo_sType, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO) Vk.put_i32(mai, VkMemoryAllocateInfo_sType, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO)
Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, Vk.get_i64(req, VkMemoryRequirements_size)) Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, Vk.get_i64(req, VkMemoryRequirements_size))
Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t) Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t)
let out = bytes(8) let out = gvk_tmp(render3d_st, 8)
render3d_st.gvk_mk_mem += 1
let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out) let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out)
if r != VK_SUCCESS { print(`r3d: vulkan: vkAllocateMemory failed (VkResult {r})`); return zero } if r != VK_SUCCESS { print(`r3d: vulkan: vkAllocateMemory failed (VkResult {r})`); return zero }
render3d_st.gvk_n_allocs += 1 render3d_st.gvk_n_allocs += 1
@ -331,20 +332,21 @@ function gvk_mem_init(render3d_st: mut Render3dState) -> void {
# one vkAllocateMemory of `size` bytes of memory type t, mapped when host-visible; 0 on failure # one vkAllocateMemory of `size` bytes of memory type t, mapped when host-visible; 0 on failure
function gvk_mem_raw(render3d_st: mut Render3dState, t: int, size: int, host: bool, out_map: []pointer) -> long { function gvk_mem_raw(render3d_st: mut Render3dState, t: int, size: int, host: bool, out_map: []pointer) -> long {
let zero: long = 0 let zero: long = 0
let mai = bytes(VkMemoryAllocateInfo_sizeof) let mai = gvk_tmp(render3d_st, VkMemoryAllocateInfo_sizeof)
Vk.zero(mai, VkMemoryAllocateInfo_sizeof) Vk.zero(mai, VkMemoryAllocateInfo_sizeof)
Vk.put_i32(mai, VkMemoryAllocateInfo_sType, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO) Vk.put_i32(mai, VkMemoryAllocateInfo_sType, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO)
let size_l: long = size let size_l: long = size
Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, size_l) Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, size_l)
Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t) Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t)
let out = bytes(8) let out = gvk_tmp(render3d_st, 8)
render3d_st.gvk_mk_mem += 1
let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out) let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out)
if r != VK_SUCCESS { gvk_note(render3d_st, `r3d: vulkan: vkAllocateMemory of {size} bytes failed (VkResult {r}, {render3d_st.gvk_n_allocs} allocations live)`); return zero } if r != VK_SUCCESS { gvk_note(render3d_st, `r3d: vulkan: vkAllocateMemory of {size} bytes failed (VkResult {r}, {render3d_st.gvk_n_allocs} allocations live)`); return zero }
render3d_st.gvk_n_allocs += 1 render3d_st.gvk_n_allocs += 1
let mem = gvk_handle(out) let mem = gvk_handle(out)
out_map[0] = null out_map[0] = null
if host { if host {
let pp = bytes(8) let pp = gvk_tmp(render3d_st, 8)
if Vk.map_memory(render3d_st.gvk_dev, mem, zero, size_l, 0, pp) != VK_SUCCESS { gvk_note(render3d_st, "r3d: vulkan: vkMapMemory failed"); return zero } if Vk.map_memory(render3d_st.gvk_dev, mem, zero, size_l, 0, pp) != VK_SUCCESS { gvk_note(render3d_st, "r3d: vulkan: vkMapMemory failed"); return zero }
out_map[0] = Vk.get_ptr(pp, 0) out_map[0] = Vk.get_ptr(pp, 0)
} }
@ -447,6 +449,7 @@ function gvk_mem_free(render3d_st: mut Render3dState, a: int) -> void {
let zero: long = 0 let zero: long = 0
if b < 0 { if b < 0 {
if render3d_st.gvk_al_map[a] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a]) } if render3d_st.gvk_al_map[a] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a]) }
render3d_st.gvk_mk_x_mem += 1
Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a], null) Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a], null)
render3d_st.gvk_n_allocs -= 1 render3d_st.gvk_n_allocs -= 1
} else { } else {
@ -466,6 +469,7 @@ function gvk_mem_free(render3d_st: mut Render3dState, a: int) -> void {
if off == 0 and n == render3d_st.gvk_blk_size[b] { if off == 0 and n == render3d_st.gvk_blk_size[b] {
# the block is empty again: give it back, or a world swapped out keeps its memory for good # the block is empty again: give it back, or a world swapped out keeps its memory for good
if render3d_st.gvk_blk_map[b] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b]) } if render3d_st.gvk_blk_map[b] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b]) }
render3d_st.gvk_mk_x_mem += 1
Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b], null) Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b], null)
render3d_st.gvk_n_allocs -= 1 render3d_st.gvk_n_allocs -= 1
render3d_st.gvk_blk_mem[b] = zero; render3d_st.gvk_blk_map[b] = null; render3d_st.gvk_blk_kind[b] = -1; render3d_st.gvk_blk_size[b] = 0 render3d_st.gvk_blk_mem[b] = zero; render3d_st.gvk_blk_map[b] = null; render3d_st.gvk_blk_kind[b] = -1; render3d_st.gvk_blk_size[b] = 0
@ -503,17 +507,35 @@ function gvk_cmd_init(render3d_st: mut Render3dState) -> bool {
return true return true
} }
# a command buffer, begun # a command buffer, begun
function gvk_once_begin(render3d_st: Render3dState) -> pointer { # ---- scratch -------------------------------------------------------------------------------
let cbai = bytes(VkCommandBufferAllocateInfo_sizeof) # The structs a Vulkan call reads are copied before it returns, so the ones built for a draw, a
# pass, a barrier or a submit need not outlive it. They were each a bytes() - a malloc nothing
# freed, several per draw: about 0.5 MB a frame, and one windowed run grew past 150 GB. They
# come from this ring instead: one block, handed out in order and wrapped when it is used up.
const GVK_TMP_BYTES: int = 1048576
function gvk_tmp(render3d_st: mut Render3dState, n: int) -> pointer {
if render3d_st.gvk_tmp_buf == null { render3d_st.gvk_tmp_buf = bytes(GVK_TMP_BYTES) }
let sz = (n + 15) / 16 * 16
if sz > GVK_TMP_BYTES { print(`r3d: vulkan: {n} bytes of scratch asked for at once`); return null }
if render3d_st.gvk_tmp_off + sz > GVK_TMP_BYTES { render3d_st.gvk_tmp_off = 0 }
let p = mem_off(render3d_st.gvk_tmp_buf, render3d_st.gvk_tmp_off)
render3d_st.gvk_tmp_off += sz
return p
}
function gvk_once_begin(render3d_st: mut Render3dState) -> pointer {
let cbai = gvk_tmp(render3d_st, VkCommandBufferAllocateInfo_sizeof)
Vk.zero(cbai, VkCommandBufferAllocateInfo_sizeof) Vk.zero(cbai, VkCommandBufferAllocateInfo_sizeof)
Vk.put_i32(cbai, VkCommandBufferAllocateInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO) Vk.put_i32(cbai, VkCommandBufferAllocateInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO)
Vk.put_i64(cbai, VkCommandBufferAllocateInfo_commandPool, render3d_st.gvk_pool) Vk.put_i64(cbai, VkCommandBufferAllocateInfo_commandPool, render3d_st.gvk_pool)
Vk.put_i32(cbai, VkCommandBufferAllocateInfo_level, VK_COMMAND_BUFFER_LEVEL_PRIMARY) Vk.put_i32(cbai, VkCommandBufferAllocateInfo_level, VK_COMMAND_BUFFER_LEVEL_PRIMARY)
Vk.put_i32(cbai, VkCommandBufferAllocateInfo_commandBufferCount, 1) Vk.put_i32(cbai, VkCommandBufferAllocateInfo_commandBufferCount, 1)
let cbs = bytes(8) let cbs = gvk_tmp(render3d_st, 8)
render3d_st.gvk_mk_cmd += 1
if Vk.allocate_command_buffers(render3d_st.gvk_dev, cbai, cbs) != VK_SUCCESS { return null } if Vk.allocate_command_buffers(render3d_st.gvk_dev, cbai, cbs) != VK_SUCCESS { return null }
let cb = Vk.get_ptr(cbs, 0) let cb = Vk.get_ptr(cbs, 0)
let cbbi = bytes(VkCommandBufferBeginInfo_sizeof) let cbbi = gvk_tmp(render3d_st, VkCommandBufferBeginInfo_sizeof)
Vk.zero(cbbi, VkCommandBufferBeginInfo_sizeof) Vk.zero(cbbi, VkCommandBufferBeginInfo_sizeof)
Vk.put_i32(cbbi, VkCommandBufferBeginInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) Vk.put_i32(cbbi, VkCommandBufferBeginInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO)
Vk.put_i32(cbbi, VkCommandBufferBeginInfo_flags, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) Vk.put_i32(cbbi, VkCommandBufferBeginInfo_flags, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT)
@ -525,10 +547,10 @@ function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool {
gvk_frame_wait(render3d_st) gvk_frame_wait(render3d_st)
var r = Vk.end_command_buffer(cb) var r = Vk.end_command_buffer(cb)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
let cbs = bytes(8) let cbs = gvk_tmp(render3d_st, 8)
Vk.put_ptr(cbs, 0, cb) Vk.put_ptr(cbs, 0, cb)
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_fence) Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_fence)
let si = bytes(VkSubmitInfo_sizeof) let si = gvk_tmp(render3d_st, VkSubmitInfo_sizeof)
Vk.zero(si, VkSubmitInfo_sizeof) Vk.zero(si, VkSubmitInfo_sizeof)
Vk.put_i32(si, VkSubmitInfo_sType, VK_STRUCTURE_TYPE_SUBMIT_INFO) Vk.put_i32(si, VkSubmitInfo_sType, VK_STRUCTURE_TYPE_SUBMIT_INFO)
Vk.put_i32(si, VkSubmitInfo_commandBufferCount, 1) Vk.put_i32(si, VkSubmitInfo_commandBufferCount, 1)
@ -556,6 +578,7 @@ function gvk_inflight_on(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_inflight < 0 { if render3d_st.gvk_inflight < 0 {
render3d_st.gvk_inflight = 0 render3d_st.gvk_inflight = 0
if Os.platform() == "macos" { render3d_st.gvk_inflight = 1 } if Os.platform() == "macos" { render3d_st.gvk_inflight = 1 }
render3d_st.gvk_nopool = r3d_env_has(render3d_st, "R3D_VK_NOPOOL")
if r3d_env_has(render3d_st, "R3D_VK_INFLIGHT") { render3d_st.gvk_inflight = Text.to_int(r3d_env(render3d_st, "R3D_VK_INFLIGHT")) } if r3d_env_has(render3d_st, "R3D_VK_INFLIGHT") { render3d_st.gvk_inflight = Text.to_int(r3d_env(render3d_st, "R3D_VK_INFLIGHT")) }
} }
return render3d_st.gvk_inflight == 1 return render3d_st.gvk_inflight == 1
@ -565,10 +588,10 @@ function gvk_frame_submit(render3d_st: mut Render3dState, cb: pointer) -> bool {
gvk_frame_wait(render3d_st) gvk_frame_wait(render3d_st)
var r = Vk.end_command_buffer(cb) var r = Vk.end_command_buffer(cb)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
let cbs = bytes(8) let cbs = gvk_tmp(render3d_st, 8)
Vk.put_ptr(cbs, 0, cb) Vk.put_ptr(cbs, 0, cb)
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence) Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence)
let si = bytes(VkSubmitInfo_sizeof) let si = gvk_tmp(render3d_st, VkSubmitInfo_sizeof)
Vk.zero(si, VkSubmitInfo_sizeof) Vk.zero(si, VkSubmitInfo_sizeof)
Vk.put_i32(si, VkSubmitInfo_sType, VK_STRUCTURE_TYPE_SUBMIT_INFO) Vk.put_i32(si, VkSubmitInfo_sType, VK_STRUCTURE_TYPE_SUBMIT_INFO)
Vk.put_i32(si, VkSubmitInfo_commandBufferCount, 1) Vk.put_i32(si, VkSubmitInfo_commandBufferCount, 1)
@ -585,7 +608,7 @@ function gvk_frame_wait(render3d_st: mut Render3dState) -> void {
if not render3d_st.gvk_frame_pending { return } if not render3d_st.gvk_frame_pending { return }
let forever: long = -1 let forever: long = -1
Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence, 1, forever) Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence, 1, forever)
let cbs = bytes(8) let cbs = gvk_tmp(render3d_st, 8)
Vk.put_ptr(cbs, 0, render3d_st.gvk_frame_pending_cb) Vk.put_ptr(cbs, 0, render3d_st.gvk_frame_pending_cb)
Vk.free_command_buffers(render3d_st.gvk_dev, render3d_st.gvk_pool, 1, cbs) Vk.free_command_buffers(render3d_st.gvk_dev, render3d_st.gvk_pool, 1, cbs)
render3d_st.gvk_frame_pending = false render3d_st.gvk_frame_pending = false
@ -615,12 +638,12 @@ function gvk_shutdown(render3d_st: mut Render3dState) -> void {
function gvk_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> void { function gvk_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> void {
for i in 0 .. n { ids[i] = i } for i in 0 .. n { ids[i] = i }
if not render3d_st.gvk_has_hqr or render3d_st.gvk_dev == null { return } if not render3d_st.gvk_has_hqr or render3d_st.gvk_dev == null { return }
let qci = bytes(VkQueryPoolCreateInfo_sizeof) let qci = gvk_tmp(render3d_st, VkQueryPoolCreateInfo_sizeof)
Vk.zero(qci, VkQueryPoolCreateInfo_sizeof) Vk.zero(qci, VkQueryPoolCreateInfo_sizeof)
Vk.put_i32(qci, VkQueryPoolCreateInfo_sType, VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO) Vk.put_i32(qci, VkQueryPoolCreateInfo_sType, VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO)
Vk.put_i32(qci, VkQueryPoolCreateInfo_queryType, VK_QUERY_TYPE_TIMESTAMP) Vk.put_i32(qci, VkQueryPoolCreateInfo_queryType, VK_QUERY_TYPE_TIMESTAMP)
Vk.put_i32(qci, VkQueryPoolCreateInfo_queryCount, n * 2) Vk.put_i32(qci, VkQueryPoolCreateInfo_queryCount, n * 2)
let out = bytes(8) let out = gvk_tmp(render3d_st, 8)
if Vk.create_query_pool(render3d_st.gvk_dev, qci, null, out) != VK_SUCCESS { return } if Vk.create_query_pool(render3d_st.gvk_dev, qci, null, out) != VK_SUCCESS { return }
render3d_st.gvk_qpool = gvk_handle(out) render3d_st.gvk_qpool = gvk_handle(out)
Vk.reset_query_pool(render3d_st.gvk_dev, render3d_st.gvk_qpool, 0, n * 2) Vk.reset_query_pool(render3d_st.gvk_dev, render3d_st.gvk_qpool, 0, n * 2)
@ -636,9 +659,9 @@ function gvk_query_end(render3d_st: mut Render3dState) -> void {
Vk.cmd_write_timestamp(gvk_frame_cb(render3d_st), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, render3d_st.gvk_qpool, render3d_st.gvk_q_active * 2 + 1) Vk.cmd_write_timestamp(gvk_frame_cb(render3d_st), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, render3d_st.gvk_qpool, render3d_st.gvk_q_active * 2 + 1)
render3d_st.gvk_q_active = -1 render3d_st.gvk_q_active = -1
} }
function gvk_query_result(render3d_st: Render3dState, id: int, out: words) -> bool { function gvk_query_result(render3d_st: mut Render3dState, id: int, out: words) -> bool {
if render3d_st.gvk_qpool == 0 { return false } if render3d_st.gvk_qpool == 0 { return false }
let data = bytes(16) let data = gvk_tmp(render3d_st, 16)
let size: long = 16 let size: long = 16
let stride: long = 8 let stride: long = 8
if Vk.get_query_pool_results(render3d_st.gvk_dev, render3d_st.gvk_qpool, id * 2, 2, size, data, stride, VK_QUERY_RESULT_64_BIT) != VK_SUCCESS { return false } if Vk.get_query_pool_results(render3d_st.gvk_dev, render3d_st.gvk_qpool, id * 2, 2, size, data, stride, VK_QUERY_RESULT_64_BIT) != VK_SUCCESS { return false }
@ -653,7 +676,7 @@ function gvk_label_begin(render3d_st: mut Render3dState, name: pointer) -> void
# only inside a frame: a pass the load profiles comes before the frame's pools exist # only inside a frame: a pass the load profiles comes before the frame's pools exist
if render3d_st.gpu_kind != GPU_VK or render3d_st.gvk_cb == null { return } if render3d_st.gpu_kind != GPU_VK or render3d_st.gvk_cb == null { return }
let cb = render3d_st.gvk_cb let cb = render3d_st.gvk_cb
let li = bytes(VkDebugUtilsLabelEXT_sizeof) let li = gvk_tmp(render3d_st, VkDebugUtilsLabelEXT_sizeof)
Vk.zero(li, VkDebugUtilsLabelEXT_sizeof) Vk.zero(li, VkDebugUtilsLabelEXT_sizeof)
Vk.put_i32(li, VkDebugUtilsLabelEXT_sType, VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT) Vk.put_i32(li, VkDebugUtilsLabelEXT_sType, VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT)
Vk.put_ptr(li, VkDebugUtilsLabelEXT_pLabelName, name) Vk.put_ptr(li, VkDebugUtilsLabelEXT_pLabelName, name)

View file

@ -195,15 +195,16 @@ function gvk_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointe
let nb = render3d_st.gvk_cp_nbuf[c] let nb = render3d_st.gvk_cp_nbuf[c]
var nt = 0 var nt = 0
if render3d_st.gvk_cp_ntex != null and c < len(render3d_st.gvk_cp_ntex) { nt = render3d_st.gvk_cp_ntex[c] } if render3d_st.gvk_cp_ntex != null and c < len(render3d_st.gvk_cp_ntex) { nt = render3d_st.gvk_cp_ntex[c] }
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof) let dsai = gvk_tmp(render3d_st, VkDescriptorSetAllocateInfo_sizeof)
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof) Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO) Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_dpool) Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_dpool)
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1) Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
let layouts = bytes(8) let layouts = gvk_tmp(render3d_st, 8)
Vk.put_i64(layouts, 0, render3d_st.gvk_cp_dsl[c]) Vk.put_i64(layouts, 0, render3d_st.gvk_cp_dsl[c])
Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts) Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts)
let sets = bytes(8) let sets = gvk_tmp(render3d_st, 8)
render3d_st.gvk_mk_set += 1
let r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets) let r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets)
if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (compute)", r); return } if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (compute)", r); return }
let set = Vk.get_i64(sets, 0) let set = Vk.get_i64(sets, 0)
@ -212,11 +213,11 @@ function gvk_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointe
let ww = VkWriteDescriptorSet_sizeof let ww = VkWriteDescriptorSet_sizeof
let bw = VkDescriptorBufferInfo_sizeof let bw = VkDescriptorBufferInfo_sizeof
let iw = VkDescriptorImageInfo_sizeof let iw = VkDescriptorImageInfo_sizeof
let writes = bytes(ww * (nb + 1 + nt)) let writes = gvk_tmp(render3d_st, ww * (nb + 1 + nt))
Vk.zero(writes, ww * (nb + 1 + nt)) Vk.zero(writes, ww * (nb + 1 + nt))
let infos = bytes(bw * (nb + 1)) let infos = gvk_tmp(render3d_st, bw * (nb + 1))
Vk.zero(infos, bw * (nb + 1)) Vk.zero(infos, bw * (nb + 1))
let iis = bytes(iw * (nt + 1)) let iis = gvk_tmp(render3d_st, iw * (nt + 1))
Vk.zero(iis, iw * (nt + 1)) Vk.zero(iis, iw * (nt + 1))
for k in 0 .. nb + 1 { for k in 0 .. nb + 1 {
var buf = render3d_st.gvk_ring_buf var buf = render3d_st.gvk_ring_buf
@ -254,7 +255,7 @@ function gvk_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointe
Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_pipe[c]) Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_pipe[c])
Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_layout[c], 0, 1, sets, 0, null) Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_layout[c], 0, 1, sets, 0, null)
Vk.cmd_dispatch(cb, gx, gy, gz) Vk.cmd_dispatch(cb, gx, gy, gz)
let mb = bytes(VkMemoryBarrier_sizeof) let mb = gvk_tmp(render3d_st, VkMemoryBarrier_sizeof)
Vk.zero(mb, VkMemoryBarrier_sizeof) Vk.zero(mb, VkMemoryBarrier_sizeof)
Vk.put_i32(mb, VkMemoryBarrier_sType, VK_STRUCTURE_TYPE_MEMORY_BARRIER) Vk.put_i32(mb, VkMemoryBarrier_sType, VK_STRUCTURE_TYPE_MEMORY_BARRIER)
Vk.put_i32(mb, VkMemoryBarrier_srcAccessMask, VK_ACCESS_SHADER_WRITE_BIT) Vk.put_i32(mb, VkMemoryBarrier_srcAccessMask, VK_ACCESS_SHADER_WRITE_BIT)
@ -704,6 +705,7 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool {
let out = bytes(8) let out = bytes(8)
render3d_st.gvk_dpools = new []long render3d_st.gvk_dpools = new []long
for k in 0 .. 2 { for k in 0 .. 2 {
render3d_st.gvk_mk_dpool += 1
let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out) let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) }
push(render3d_st.gvk_dpools, gvk_handle(out)) push(render3d_st.gvk_dpools, gvk_handle(out))
@ -717,6 +719,8 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool {
} }
function gvk_frame_reset(render3d_st: mut Render3dState) -> void { function gvk_frame_reset(render3d_st: mut Render3dState) -> void {
# what MoltenVK autoreleased last frame goes back here (a no-op off macOS); R3D_VK_NOPOOL=1 for an A/B
if not render3d_st.gvk_nopool { Vk.frame_pool() }
let slot = render3d_st.gvk_frame_no & 1 let slot = render3d_st.gvk_frame_no & 1
render3d_st.gvk_ring_off = slot * GVK_RING_BYTES render3d_st.gvk_ring_off = slot * GVK_RING_BYTES
render3d_st.gvk_ring_end = (slot + 1) * GVK_RING_BYTES render3d_st.gvk_ring_end = (slot + 1) * GVK_RING_BYTES
@ -756,6 +760,7 @@ function gvk_kpool_make(render3d_st: mut Render3dState) -> bool {
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 2) Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 2)
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes) Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
let out = bytes(8) let out = bytes(8)
render3d_st.gvk_mk_dpool += 1
let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out) let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool (kept sets)", r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool (kept sets)", r) }
render3d_st.gvk_kpool = gvk_handle(out) render3d_st.gvk_kpool = gvk_handle(out)
@ -764,8 +769,13 @@ function gvk_kpool_make(render3d_st: mut Render3dState) -> bool {
} }
function gvk_sc_clear(render3d_st: mut Render3dState) -> void { function gvk_sc_clear(render3d_st: mut Render3dState) -> void {
# emptied, not replaced: five fresh lists each time the pool filled were never given back
if render3d_st.gvk_sc_prog == null {
render3d_st.gvk_sc_prog = new []int; render3d_st.gvk_sc_koff = new []int; render3d_st.gvk_sc_set = new []long; render3d_st.gvk_sc_next = new []int render3d_st.gvk_sc_prog = new []int; render3d_st.gvk_sc_koff = new []int; render3d_st.gvk_sc_set = new []long; render3d_st.gvk_sc_next = new []int
render3d_st.gvk_sc_keys = new []long render3d_st.gvk_sc_keys = new []long
}
List.clear(render3d_st.gvk_sc_prog); List.clear(render3d_st.gvk_sc_koff); List.clear(render3d_st.gvk_sc_set); List.clear(render3d_st.gvk_sc_next)
List.clear(render3d_st.gvk_sc_keys)
if render3d_st.gvk_sc_head == null { render3d_st.gvk_sc_head = words(4096) } if render3d_st.gvk_sc_head == null { render3d_st.gvk_sc_head = words(4096) }
for i in 0 .. 4096 { render3d_st.gvk_sc_head[i] = -1 } for i in 0 .. 4096 { render3d_st.gvk_sc_head[i] = -1 }
} }
@ -851,9 +861,9 @@ function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: i
function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long { function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
let zero: long = 0 let zero: long = 0
let v = render3d_st.gvk_prog_var[p] let v = render3d_st.gvk_prog_var[p]
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof) let dsai = gvk_tmp(render3d_st, VkDescriptorSetAllocateInfo_sizeof)
let layouts = bytes(8) let layouts = gvk_tmp(render3d_st, 8)
let sets = bytes(8) let sets = gvk_tmp(render3d_st, 8)
var r = 0 var r = 0
var tries = 0 var tries = 0
while tries < 2 { while tries < 2 {
@ -863,16 +873,17 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1) Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
Vk.put_i64(layouts, 0, render3d_st.gvk_prog_dsl[p]) Vk.put_i64(layouts, 0, render3d_st.gvk_prog_dsl[p])
Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts) Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts)
render3d_st.gvk_mk_set += 1
r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets) r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets)
if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(render3d_st); tries += 1 } if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(render3d_st); tries += 1 }
} }
if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (kept sets)", r); return zero } if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (kept sets)", r); return zero }
let set = Vk.get_i64(sets, 0) let set = Vk.get_i64(sets, 0)
let ww = VkWriteDescriptorSet_sizeof let ww = VkWriteDescriptorSet_sizeof
let writes = bytes(ww * (nt + 3)) let writes = gvk_tmp(render3d_st, ww * (nt + 3))
Vk.zero(writes, ww * (nt + 3)) Vk.zero(writes, ww * (nt + 3))
let bis = bytes(VkDescriptorBufferInfo_sizeof * 2) let bis = gvk_tmp(render3d_st, VkDescriptorBufferInfo_sizeof * 2)
let iis = bytes(VkDescriptorImageInfo_sizeof * (nt + 1)) let iis = gvk_tmp(render3d_st, VkDescriptorImageInfo_sizeof * (nt + 1))
var nw = 0 var nw = 0
var bi = 0 var bi = 0
for stage in 0 .. 2 { for stage in 0 .. 2 {
@ -1011,9 +1022,10 @@ function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer {
# on its own. Keyed by the image's generation, so a replaced image never reuses a stale view. # on its own. Keyed by the image's generation, so a replaced image never reuses a stale view.
function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> long { function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> long {
if layer1 == 0 and render3d_st.gvk_tex_levels[tex] <= 1 and render3d_st.gvk_tex_layers[tex] <= 1 { return render3d_st.gvk_tex_view[tex] } if layer1 == 0 and render3d_st.gvk_tex_levels[tex] <= 1 and render3d_st.gvk_tex_layers[tex] <= 1 { return render3d_st.gvk_tex_view[tex] }
let key = `{tex}:{render3d_st.gvk_tex_gen[tex]}:{layer1}` # keyed by numbers, not a string built on every call: the texture, its generation and the layer
if render3d_st.gvk_layer_views == null { render3d_st.gvk_layer_views = new []string; render3d_st.gvk_layer_view = new []long } let key = render3d_st.gvk_tex_gen[tex] * 4096 + layer1
for i in 0 .. len(render3d_st.gvk_layer_views) { if render3d_st.gvk_layer_views[i] == key { return render3d_st.gvk_layer_view[i] } } if render3d_st.gvk_layer_views == null { render3d_st.gvk_layer_views = new []int; render3d_st.gvk_layer_view = new []long; render3d_st.gvk_layer_view_tex = new []int }
for i in 0 .. len(render3d_st.gvk_layer_views) { if render3d_st.gvk_layer_view_tex[i] == tex and render3d_st.gvk_layer_views[i] == key { return render3d_st.gvk_layer_view[i] } }
let depth = render3d_st.gvk_tex_vkfmt[tex] == VK_FORMAT_D32_SFLOAT let depth = render3d_st.gvk_tex_vkfmt[tex] == VK_FORMAT_D32_SFLOAT
let vci = bytes(VkImageViewCreateInfo_sizeof) let vci = bytes(VkImageViewCreateInfo_sizeof)
Vk.zero(vci, VkImageViewCreateInfo_sizeof) Vk.zero(vci, VkImageViewCreateInfo_sizeof)
@ -1028,16 +1040,18 @@ function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> l
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, 1) Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, 1)
let out = bytes(8) let out = bytes(8)
let zero: long = 0 let zero: long = 0
render3d_st.gvk_mk_view += 1
if Vk.create_image_view(render3d_st.gvk_dev, vci, null, out) != VK_SUCCESS { return zero } if Vk.create_image_view(render3d_st.gvk_dev, vci, null, out) != VK_SUCCESS { return zero }
push(render3d_st.gvk_layer_views, key) push(render3d_st.gvk_layer_views, key)
push(render3d_st.gvk_layer_view, gvk_handle(out)) push(render3d_st.gvk_layer_view, gvk_handle(out))
push(render3d_st.gvk_layer_view_tex, tex)
return gvk_handle(out) return gvk_handle(out)
} }
# the layer range a barrier for an attachment covers: the whole image unless one layer is drawn # the layer range a barrier for an attachment covers: the whole image unless one layer is drawn
function gvk_att_barrier(render3d_st: Render3dState, cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void { function gvk_att_barrier(render3d_st: mut Render3dState, cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void {
# an attachment whose image was never made (no memory for it) has nothing to transition # an attachment whose image was never made (no memory for it) has nothing to transition
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { return } if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { return }
let b = bytes(VkImageMemoryBarrier_sizeof) let b = gvk_tmp(render3d_st, VkImageMemoryBarrier_sizeof)
Vk.zero(b, VkImageMemoryBarrier_sizeof) Vk.zero(b, VkImageMemoryBarrier_sizeof)
Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER) Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER)
Vk.put_i32(b, VkImageMemoryBarrier_srcAccessMask, gvk_layout_access(old_layout)) Vk.put_i32(b, VkImageMemoryBarrier_srcAccessMask, gvk_layout_access(old_layout))
@ -1085,7 +1099,7 @@ function gvk_pass_begin(render3d_st: mut Render3dState, rec: words, rec_o: int)
let cb = gvk_frame_cb(render3d_st) let cb = gvk_frame_cb(render3d_st)
gvk_pass_collect(render3d_st, render3d_st.gvk_fb_cur, rec, rec_o) gvk_pass_collect(render3d_st, render3d_st.gvk_fb_cur, rec, rec_o)
let aw = VkRenderingAttachmentInfo_sizeof let aw = VkRenderingAttachmentInfo_sizeof
let catt = bytes(aw * 3) let catt = gvk_tmp(render3d_st, aw * 3)
Vk.zero(catt, aw * 3) Vk.zero(catt, aw * 3)
render3d_st.gvk_pass_w = 0 render3d_st.gvk_pass_w = 0
render3d_st.gvk_pass_h = 0 render3d_st.gvk_pass_h = 0
@ -1108,7 +1122,7 @@ function gvk_pass_begin(render3d_st: mut Render3dState, rec: words, rec_o: int)
render3d_st.gvk_pass_samples = gvk_tex_samples_of(render3d_st, tex) render3d_st.gvk_pass_samples = gvk_tex_samples_of(render3d_st, tex)
if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, tex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, tex) } if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, tex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, tex) }
} }
let datt = bytes(aw) let datt = gvk_tmp(render3d_st, aw)
Vk.zero(datt, aw) Vk.zero(datt, aw)
let dtex = render3d_st.gvk_pass_dep[0] let dtex = render3d_st.gvk_pass_dep[0]
if dtex > 0 { if dtex > 0 {
@ -1126,7 +1140,7 @@ function gvk_pass_begin(render3d_st: mut Render3dState, rec: words, rec_o: int)
if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, dtex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, dtex) } if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, dtex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, dtex) }
} }
render3d_st.gvk_clear_bits = 0 render3d_st.gvk_clear_bits = 0
let ri = bytes(VkRenderingInfo_sizeof) let ri = gvk_tmp(render3d_st, VkRenderingInfo_sizeof)
Vk.zero(ri, VkRenderingInfo_sizeof) Vk.zero(ri, VkRenderingInfo_sizeof)
Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO) Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO)
Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w) Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
@ -1157,7 +1171,7 @@ function gvk_clear(render3d_st: mut Render3dState, mask: int, rec: words, rec_o:
if not render3d_st.gvk_in_pass { render3d_st.gvk_clear_bits = render3d_st.gvk_clear_bits | mask; return } if not render3d_st.gvk_in_pass { render3d_st.gvk_clear_bits = render3d_st.gvk_clear_bits | mask; return }
let cb = render3d_st.gvk_cb let cb = render3d_st.gvk_cb
let caw = VkClearAttachment_sizeof let caw = VkClearAttachment_sizeof
let atts = bytes(caw * 4) let atts = gvk_tmp(render3d_st, caw * 4)
Vk.zero(atts, caw * 4) Vk.zero(atts, caw * 4)
var n = 0 var n = 0
if (mask & GL_COLOR_BUFFER_BIT) != 0 { if (mask & GL_COLOR_BUFFER_BIT) != 0 {
@ -1174,7 +1188,7 @@ function gvk_clear(render3d_st: mut Render3dState, mask: int, rec: words, rec_o:
n += 1 n += 1
} }
if n == 0 { return } if n == 0 { return }
let rect = bytes(VkClearRect_sizeof) let rect = gvk_tmp(render3d_st, VkClearRect_sizeof)
Vk.zero(rect, VkClearRect_sizeof) Vk.zero(rect, VkClearRect_sizeof)
Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w) Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h) Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
@ -1187,8 +1201,8 @@ function gvk_tex_h(render3d_st: Render3dState, tex: int) -> int { return render3
# viewport and scissor for the draw; OpenGL's rows count from the bottom and so do a Vulkan # viewport and scissor for the draw; OpenGL's rows count from the bottom and so do a Vulkan
# target's here (no y flip), so both pass straight through # target's here (no y flip), so both pass straight through
function gvk_set_view(render3d_st: Render3dState, cb: pointer) -> void { function gvk_set_view(render3d_st: mut Render3dState, cb: pointer) -> void {
let vp = bytes(VkViewport_sizeof) let vp = gvk_tmp(render3d_st, VkViewport_sizeof)
Vk.zero(vp, VkViewport_sizeof) Vk.zero(vp, VkViewport_sizeof)
Vk.put_i32(vp, VkViewport_x, float_bits(float(render3d_st.gvk_vp[0]))) Vk.put_i32(vp, VkViewport_x, float_bits(float(render3d_st.gvk_vp[0])))
Vk.put_i32(vp, VkViewport_y, float_bits(float(render3d_st.gvk_vp[1]))) Vk.put_i32(vp, VkViewport_y, float_bits(float(render3d_st.gvk_vp[1])))
@ -1196,7 +1210,7 @@ function gvk_set_view(render3d_st: Render3dState, cb: pointer) -> void {
Vk.put_i32(vp, VkViewport_height, float_bits(float(render3d_st.gvk_vp[3]))) Vk.put_i32(vp, VkViewport_height, float_bits(float(render3d_st.gvk_vp[3])))
Vk.put_i32(vp, VkViewport_maxDepth, 0x3F800000) Vk.put_i32(vp, VkViewport_maxDepth, 0x3F800000)
Vk.cmd_set_viewport(cb, 0, 1, vp) Vk.cmd_set_viewport(cb, 0, 1, vp)
let sc = bytes(VkRect2D_sizeof) let sc = gvk_tmp(render3d_st, VkRect2D_sizeof)
Vk.zero(sc, VkRect2D_sizeof) Vk.zero(sc, VkRect2D_sizeof)
if render3d_st.gvk_sc[0] == 1 { if render3d_st.gvk_sc[0] == 1 {
Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_x, render3d_st.gvk_sc[1]) Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_x, render3d_st.gvk_sc[1])
@ -1238,16 +1252,17 @@ function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState,
let set = gvk_draw_set(render3d_st, p, tx, tx_w, tx_cap) let set = gvk_draw_set(render3d_st, p, tx, tx_w, tx_cap)
if set == 0 { return } if set == 0 { return }
if prof { render3d_st.gvk_us_set = render3d_st.gvk_us_set + (gl_now_us() - t1) } if prof { render3d_st.gvk_us_set = render3d_st.gvk_us_set + (gl_now_us() - t1) }
let sets = bytes(8) let sets = gvk_tmp(render3d_st, 8)
Vk.put_i64(sets, 0, set) Vk.put_i64(sets, 0, set)
Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, render3d_st.gvk_prog_layout[p], 0, 1, sets, render3d_st.gvk_set_ndyn, render3d_st.gvk_set_offs) Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, render3d_st.gvk_prog_layout[p], 0, 1, sets, render3d_st.gvk_set_ndyn, render3d_st.gvk_set_offs)
let v = render3d_st.gvk_prog_var[p] let v = render3d_st.gvk_prog_var[p]
if m != null and m.attrs != null { if m != null and m.attrs != null {
# the same bindings, in the same order, as gvk_pipeline gave the layout # the same bindings, in the same order, as gvk_pipeline gave the layout
let bufs = bytes(8 * (GPU_MAX_VBUFS + 1)) let bufs = gvk_tmp(render3d_st, 8 * (GPU_MAX_VBUFS + 1))
let offs = bytes(8 * (GPU_MAX_VBUFS + 1)) let offs = gvk_tmp(render3d_st, 8 * (GPU_MAX_VBUFS + 1))
Vk.zero(offs, 8 * (GPU_MAX_VBUFS + 1)) Vk.zero(offs, 8 * (GPU_MAX_VBUFS + 1))
let seen = words(GPU_MAX_VBUFS) if render3d_st.gvk_seen == null { render3d_st.gvk_seen = words(GPU_MAX_VBUFS) }
let seen = render3d_st.gvk_seen
var nbd = 0 var nbd = 0
for i in 0 .. m.n_attrs { for i in 0 .. m.n_attrs {
let o = i * GPU_ATTR_W let o = i * GPU_ATTR_W
@ -1510,7 +1525,7 @@ function gvk_resolve(render3d_st: mut Render3dState, cb: pointer, src: int, dst:
gvk_att_barrier(render3d_st, cb, src, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout) gvk_att_barrier(render3d_st, cb, src, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
gvk_att_barrier(render3d_st, cb, dst, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout) gvk_att_barrier(render3d_st, cb, dst, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
let aw = VkRenderingAttachmentInfo_sizeof let aw = VkRenderingAttachmentInfo_sizeof
let att = bytes(aw) let att = gvk_tmp(render3d_st, aw)
Vk.zero(att, aw) Vk.zero(att, aw)
Vk.put_i32(att, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO) Vk.put_i32(att, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
Vk.put_i64(att, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, src, 0)) Vk.put_i64(att, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, src, 0))
@ -1520,7 +1535,7 @@ function gvk_resolve(render3d_st: mut Render3dState, cb: pointer, src: int, dst:
Vk.put_i32(att, VkRenderingAttachmentInfo_resolveImageLayout, layout) Vk.put_i32(att, VkRenderingAttachmentInfo_resolveImageLayout, layout)
Vk.put_i32(att, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) Vk.put_i32(att, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD)
Vk.put_i32(att, VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE) Vk.put_i32(att, VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
let ri = bytes(VkRenderingInfo_sizeof) let ri = gvk_tmp(render3d_st, VkRenderingInfo_sizeof)
Vk.zero(ri, VkRenderingInfo_sizeof) Vk.zero(ri, VkRenderingInfo_sizeof)
Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO) Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO)
Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, w) Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, w)
@ -1536,9 +1551,9 @@ function gvk_resolve(render3d_st: mut Render3dState, cb: pointer, src: int, dst:
function gvk_copy(render3d_st: mut Render3dState, cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void { function gvk_copy(render3d_st: mut Render3dState, cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
if src <= 0 or dst <= 0 or render3d_st.gvk_tex_image[src] == 0 or render3d_st.gvk_tex_image[dst] == 0 { return } if src <= 0 or dst <= 0 or render3d_st.gvk_tex_image[src] == 0 or render3d_st.gvk_tex_image[dst] == 0 { return }
if gvk_tex_samples_of(render3d_st, src) > 1 and gvk_tex_samples_of(render3d_st, dst) == 1 { gvk_resolve(render3d_st, cb, src, dst, depth, w, h); return } if gvk_tex_samples_of(render3d_st, src) > 1 and gvk_tex_samples_of(render3d_st, dst) == 1 { gvk_resolve(render3d_st, cb, src, dst, depth, w, h); return }
gvk_barrier(cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
gvk_barrier(cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let ic = bytes(VkImageCopy_sizeof) let ic = gvk_tmp(render3d_st, VkImageCopy_sizeof)
Vk.zero(ic, VkImageCopy_sizeof) Vk.zero(ic, VkImageCopy_sizeof)
var aspect = VK_IMAGE_ASPECT_COLOR_BIT var aspect = VK_IMAGE_ASPECT_COLOR_BIT
if depth { aspect = VK_IMAGE_ASPECT_DEPTH_BIT } if depth { aspect = VK_IMAGE_ASPECT_DEPTH_BIT }
@ -1550,8 +1565,8 @@ function gvk_copy(render3d_st: mut Render3dState, cb: pointer, src: int, dst: in
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h) Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1) Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1)
Vk.cmd_copy_image(cb, render3d_st.gvk_tex_image[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic) Vk.cmd_copy_image(cb, render3d_st.gvk_tex_image[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
gvk_barrier(cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
gvk_barrier(cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
} }
function gvk_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> void { function gvk_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> void {
gvk_pass_end(render3d_st) gvk_pass_end(render3d_st)
@ -1725,20 +1740,20 @@ function gvk_present_window(render3d_st: mut Render3dState) -> void {
Time.sleep_us(16000) Time.sleep_us(16000)
return return
} }
if render3d_st.gvk_swap_stale { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return } if render3d_st.gvk_swap_stale { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; print("r3d: vulkan: swapchain remade at acquire"); gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return }
let idx = bytes(4) let idx = gvk_tmp(render3d_st, 4)
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence) Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence)
let forever: long = -1 let forever: long = -1
let zero: long = 0 let zero: long = 0
var r = Vk.acquire_next_image_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, forever, zero, Vk.get_i64(render3d_st.gvk_acq_fence, 0), idx) var r = Vk.acquire_next_image_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, forever, zero, Vk.get_i64(render3d_st.gvk_acq_fence, 0), idx)
if r == VK_ERROR_OUT_OF_DATE_KHR { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return } if r == VK_ERROR_OUT_OF_DATE_KHR { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; print("r3d: vulkan: swapchain remade at acquire"); gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return }
Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence, 1, forever) Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence, 1, forever)
let i = Vk.get_i32(idx, 0) let i = Vk.get_i32(idx, 0)
let dst = Vk.get_i64(render3d_st.gvk_swap_images, i * 8) let dst = Vk.get_i64(render3d_st.gvk_swap_images, i * 8)
let src = render3d_st.gvk_tex_image[render3d_st.gvk_screen_color] let src = render3d_st.gvk_tex_image[render3d_st.gvk_screen_color]
gvk_barrier(cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(render3d_st, cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
gvk_barrier(cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) gvk_barrier(render3d_st, cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let blit = bytes(VkImageBlit_sizeof) let blit = gvk_tmp(render3d_st, VkImageBlit_sizeof)
Vk.zero(blit, VkImageBlit_sizeof) Vk.zero(blit, VkImageBlit_sizeof)
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_layerCount, 1) Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_layerCount, 1)
@ -1752,14 +1767,14 @@ function gvk_present_window(render3d_st: mut Render3dState) -> void {
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, render3d_st.gvk_swap_h) Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, render3d_st.gvk_swap_h)
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1) Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
Vk.cmd_blit_image(cb, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR) Vk.cmd_blit_image(cb, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR)
gvk_barrier(cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
gvk_barrier(cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) gvk_barrier(render3d_st, cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR)
if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) } if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) }
render3d_st.gvk_cb = null render3d_st.gvk_cb = null
let pi = bytes(VkPresentInfoKHR_sizeof) let pi = gvk_tmp(render3d_st, VkPresentInfoKHR_sizeof)
Vk.zero(pi, VkPresentInfoKHR_sizeof) Vk.zero(pi, VkPresentInfoKHR_sizeof)
Vk.put_i32(pi, VkPresentInfoKHR_sType, VK_STRUCTURE_TYPE_PRESENT_INFO_KHR) Vk.put_i32(pi, VkPresentInfoKHR_sType, VK_STRUCTURE_TYPE_PRESENT_INFO_KHR)
let chains = bytes(8) let chains = gvk_tmp(render3d_st, 8)
Vk.put_i64(chains, 0, render3d_st.gvk_swap) Vk.put_i64(chains, 0, render3d_st.gvk_swap)
Vk.put_i32(pi, VkPresentInfoKHR_swapchainCount, 1) Vk.put_i32(pi, VkPresentInfoKHR_swapchainCount, 1)
Vk.put_ptr(pi, VkPresentInfoKHR_pSwapchains, chains) Vk.put_ptr(pi, VkPresentInfoKHR_pSwapchains, chains)
@ -1779,6 +1794,8 @@ function gvk_resize_check(render3d_st: mut Render3dState) -> bool {
let h = render3d_st.gvk_size_buf[1] let h = render3d_st.gvk_size_buf[1]
if w <= 0 or h <= 0 { return false } if w <= 0 or h <= 0 { return false }
if w == gl_width() and h == gl_height() and not render3d_st.gvk_swap_stale { return false } if w == gl_width() and h == gl_height() and not render3d_st.gvk_swap_stale { return false }
# a remake is rare (a resize); said, so one on every frame shows in the log
print(`r3d: vulkan: swapchain remade {w}x{h} (was {gl_width()}x{gl_height()}, stale {render3d_st.gvk_swap_stale})`)
gvk_flush(render3d_st) gvk_flush(render3d_st)
gl_set_drawable(w, h) gl_set_drawable(w, h)
gvk_screen_make(render3d_st, w, h) gvk_screen_make(render3d_st, w, h)
@ -1816,11 +1833,26 @@ function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
if render3d_st.gvk_n_asked != render3d_st.gvk_n_draws { if render3d_st.gvk_n_asked != render3d_st.gvk_n_draws {
print(`r3d: vulkan: {(render3d_st.gvk_n_asked - render3d_st.gvk_n_draws) / f} draws a frame were asked for and not made ({render3d_st.gvk_n_asked / f} asked, {render3d_st.gvk_n_draws / f} made)`) print(`r3d: vulkan: {(render3d_st.gvk_n_asked - render3d_st.gvk_n_draws) / f} draws a frame were asked for and not made ({render3d_st.gvk_n_asked / f} asked, {render3d_st.gvk_n_draws / f} made)`)
} }
gvk_prof_made(render3d_st)
let zero: long = 0 let zero: long = 0
render3d_st.gvk_us_pipe = zero; render3d_st.gvk_us_set = zero; render3d_st.gvk_us_draw = zero render3d_st.gvk_us_pipe = zero; render3d_st.gvk_us_set = zero; render3d_st.gvk_us_draw = zero
render3d_st.gvk_n_draws = 0; render3d_st.gvk_n_asked = 0; render3d_st.gvk_n_flush = 0; render3d_st.gvk_prof_frames = 0; render3d_st.gvk_n_pipe_new = 0 render3d_st.gvk_n_draws = 0; render3d_st.gvk_n_asked = 0; render3d_st.gvk_n_flush = 0; render3d_st.gvk_prof_frames = 0; render3d_st.gvk_n_pipe_new = 0
} }
# what was made and destroyed over those frames, and how long the caches are: a kind made every
# frame and never destroyed, or a cache that only grows, is a leak
function gvk_prof_made(render3d_st: mut Render3dState) -> void {
let r = render3d_st
print(`r3d: vulkan made/destroyed: images {r.gvk_mk_img}/{r.gvk_mk_x_img}, views {r.gvk_mk_view}/{r.gvk_mk_x_view}, buffers {r.gvk_mk_buf}/{r.gvk_mk_x_buf}, memory {r.gvk_mk_mem}/{r.gvk_mk_x_mem}, samplers {r.gvk_mk_smp}, sets {r.gvk_mk_set}, pools {r.gvk_mk_dpool}, cmds {r.gvk_mk_cmd}`)
print(`r3d: vulkan caches: layer views {gvk_len_i(r.gvk_layer_views)}, pipelines {gvk_len_s(r.gvk_pipe_keys)}, layouts {gvk_len_s(r.gvk_layout_keys)}, pipe cache {gvk_len_i(r.gvk_pc_prog)}, retired {gvk_len_l(r.gvk_retired_buf)}, buffers {gvk_len_l(r.gvk_buf)}, allocs {r.gvk_n_allocs}`)
render3d_st.gvk_mk_img = 0; render3d_st.gvk_mk_x_img = 0; render3d_st.gvk_mk_view = 0; render3d_st.gvk_mk_x_view = 0
render3d_st.gvk_mk_buf = 0; render3d_st.gvk_mk_x_buf = 0; render3d_st.gvk_mk_mem = 0; render3d_st.gvk_mk_x_mem = 0
render3d_st.gvk_mk_smp = 0; render3d_st.gvk_mk_set = 0; render3d_st.gvk_mk_dpool = 0; render3d_st.gvk_mk_cmd = 0
}
function gvk_len_s(xs: []string) -> int { if xs == null { return 0 }; return len(xs) }
function gvk_len_i(xs: []int) -> int { if xs == null { return 0 }; return len(xs) }
function gvk_len_l(xs: []long) -> int { if xs == null { return 0 }; return len(xs) }
# ---- the pipeline cache, by integers ----------------------------------------------------------- # ---- the pipeline cache, by integers -----------------------------------------------------------
# gvk_pipeline builds and keys pipelines by a string; a draw only needs to find one it already has. # gvk_pipeline builds and keys pipelines by a string; a draw only needs to find one it already has.
# A mesh carries an interned layout id (its recorded layout, buffers named by order), the render # A mesh carries an interned layout id (its recorded layout, buffers named by order), the render

View file

@ -88,10 +88,10 @@ function gvk_layout_access(layout: int) -> int {
return 0 return 0
} }
# levels [base, base + n) of every layer of an image, from one layout to another # levels [base, base + n) of every layer of an image, from one layout to another
function gvk_barrier(cb: pointer, image: long, depth: bool, base: int, n: int, layers: int, old_layout: int, new_layout: int) -> void { function gvk_barrier(render3d_st: mut Render3dState, cb: pointer, image: long, depth: bool, base: int, n: int, layers: int, old_layout: int, new_layout: int) -> void {
# an image that was never made (its memory could not be had) has nothing to transition # an image that was never made (its memory could not be had) has nothing to transition
if image == 0 { return } if image == 0 { return }
let b = bytes(VkImageMemoryBarrier_sizeof) let b = gvk_tmp(render3d_st, VkImageMemoryBarrier_sizeof)
Vk.zero(b, VkImageMemoryBarrier_sizeof) Vk.zero(b, VkImageMemoryBarrier_sizeof)
Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER) Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER)
Vk.put_i32(b, VkImageMemoryBarrier_srcAccessMask, gvk_layout_access(old_layout)) Vk.put_i32(b, VkImageMemoryBarrier_srcAccessMask, gvk_layout_access(old_layout))
@ -115,28 +115,31 @@ function gvk_barrier(cb: pointer, image: long, depth: bool, base: int, n: int, l
# a host-visible buffer of n bytes, mapped; gvk_staging_free unmaps and frees it # a host-visible buffer of n bytes, mapped; gvk_staging_free unmaps and frees it
function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> pointer { function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> pointer {
let size: long = n let size: long = n
let bci = bytes(VkBufferCreateInfo_sizeof) let bci = gvk_tmp(render3d_st, VkBufferCreateInfo_sizeof)
Vk.zero(bci, VkBufferCreateInfo_sizeof) Vk.zero(bci, VkBufferCreateInfo_sizeof)
Vk.put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO) Vk.put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO)
Vk.put_i64(bci, VkBufferCreateInfo_size, size) Vk.put_i64(bci, VkBufferCreateInfo_size, size)
Vk.put_i32(bci, VkBufferCreateInfo_usage, usage) Vk.put_i32(bci, VkBufferCreateInfo_usage, usage)
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
let out = bytes(8) let out = gvk_tmp(render3d_st, 8)
render3d_st.gvk_mk_buf += 1
if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return null } if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return null }
render3d_st.gvk_st_buf = gvk_handle(out) render3d_st.gvk_st_buf = gvk_handle(out)
let req = bytes(VkMemoryRequirements_sizeof) let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof)
Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, render3d_st.gvk_st_buf, req) Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, render3d_st.gvk_st_buf, req)
render3d_st.gvk_st_mem = gvk_alloc(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) render3d_st.gvk_st_mem = gvk_alloc(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
let zero: long = 0 let zero: long = 0
Vk.bind_buffer_memory(render3d_st.gvk_dev, render3d_st.gvk_st_buf, render3d_st.gvk_st_mem, zero) Vk.bind_buffer_memory(render3d_st.gvk_dev, render3d_st.gvk_st_buf, render3d_st.gvk_st_mem, zero)
let pp = bytes(8) let pp = gvk_tmp(render3d_st, 8)
if Vk.map_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null } if Vk.map_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null }
return Vk.get_ptr(pp, 0) return Vk.get_ptr(pp, 0)
} }
function gvk_staging_free(render3d_st: mut Render3dState) -> void { function gvk_staging_free(render3d_st: mut Render3dState) -> void {
gvk_frame_wait(render3d_st) # the frame in flight may still read it gvk_frame_wait(render3d_st) # the frame in flight may still read it
Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem) Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem)
render3d_st.gvk_mk_x_buf += 1
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, null) Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, null)
render3d_st.gvk_mk_x_mem += 1
Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, null) Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, null)
render3d_st.gvk_n_allocs -= 1 render3d_st.gvk_n_allocs -= 1
} }
@ -175,6 +178,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool,
Vk.put_i32(ici, VkImageCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) Vk.put_i32(ici, VkImageCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
Vk.put_i32(ici, VkImageCreateInfo_initialLayout, VK_IMAGE_LAYOUT_UNDEFINED) Vk.put_i32(ici, VkImageCreateInfo_initialLayout, VK_IMAGE_LAYOUT_UNDEFINED)
let out = bytes(8) let out = bytes(8)
render3d_st.gvk_mk_img += 1
var r = Vk.create_image(render3d_st.gvk_dev, ici, null, out) var r = Vk.create_image(render3d_st.gvk_dev, ici, null, out)
if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateImage {w}x{h}x{layers} format {vkfmt}`, r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateImage {w}x{h}x{layers} format {vkfmt}`, r) }
let image = gvk_handle(out) let image = gvk_handle(out)
@ -186,6 +190,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool,
# no memory for it (one allocation per resource meets the driver's allocation limit long before # no memory for it (one allocation per resource meets the driver's allocation limit long before
# the card is full): say so instead of binding a null allocation, which the driver may accept # the card is full): say so instead of binding a null allocation, which the driver may accept
if mem == 0 { if mem == 0 {
render3d_st.gvk_mk_x_img += 1
Vk.destroy_image(render3d_st.gvk_dev, image, null) Vk.destroy_image(render3d_st.gvk_dev, image, null)
return gvk_fail(render3d_st, `no device memory for a {w}x{h}x{layers} image ({render3d_st.gvk_n_allocs} allocations live)`, VK_ERROR_OUT_OF_DEVICE_MEMORY) return gvk_fail(render3d_st, `no device memory for a {w}x{h}x{layers} image ({render3d_st.gvk_n_allocs} allocations live)`, VK_ERROR_OUT_OF_DEVICE_MEMORY)
} }
@ -201,6 +206,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool,
if depth { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) } if depth { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, levels) Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, levels)
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, layers) Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, layers)
render3d_st.gvk_mk_view += 1
r = Vk.create_image_view(render3d_st.gvk_dev, vci, null, out) r = Vk.create_image_view(render3d_st.gvk_dev, vci, null, out)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateImageView", r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateImageView", r) }
render3d_st.gvk_tex_image[tex] = image; render3d_st.gvk_tex_view[tex] = gvk_handle(out); render3d_st.gvk_tex_mem[tex] = mem render3d_st.gvk_tex_image[tex] = image; render3d_st.gvk_tex_view[tex] = gvk_handle(out); render3d_st.gvk_tex_mem[tex] = mem
@ -211,7 +217,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool,
if array { render3d_st.gvk_tex_array[tex] = 1 } else { render3d_st.gvk_tex_array[tex] = 0 } if array { render3d_st.gvk_tex_array[tex] = 1 } else { render3d_st.gvk_tex_array[tex] = 0 }
# a target starts cleared-to-nothing but readable: every level in the layout samplers expect # a target starts cleared-to-nothing but readable: every level in the layout samplers expect
let cb = gvk_once_begin(render3d_st) let cb = gvk_once_begin(render3d_st)
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
return gvk_once_end(render3d_st, cb) return gvk_once_end(render3d_st, cb)
} }
@ -248,7 +254,8 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
let n = texels * cout * bout let n = texels * cout * bout
let dst = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT) let dst = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT)
if dst == null { print("r3d: vulkan: no staging buffer for an upload"); return false } if dst == null { print("r3d: vulkan: no staging buffer for an upload"); return false }
let buf = bytes(n) # written straight into the mapped staging memory: a copy of its own was never freed
let buf: pointer = dst
if cin == cout and bin == bout and not (bin == 2 and render3d_st.gvk_unpack_swap) { if cin == cout and bin == bout and not (bin == 2 and render3d_st.gvk_unpack_swap) {
mem_copy(buf, data, n) mem_copy(buf, data, n)
} else { } else {
@ -281,13 +288,12 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
} }
} }
} }
mem_copy(dst, buf, n)
let image = render3d_st.gvk_tex_image[tex] let image = render3d_st.gvk_tex_image[tex]
let depth = gvk_is_depth(ifmt) let depth = gvk_is_depth(ifmt)
let levels = render3d_st.gvk_tex_levels[tex] let levels = render3d_st.gvk_tex_levels[tex]
let cb = gvk_once_begin(render3d_st) let cb = gvk_once_begin(render3d_st)
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) gvk_barrier(render3d_st, cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let bic = bytes(VkBufferImageCopy_sizeof) let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
Vk.zero(bic, VkBufferImageCopy_sizeof) Vk.zero(bic, VkBufferImageCopy_sizeof)
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) } else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
@ -296,7 +302,7 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h) Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1) Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
Vk.cmd_copy_buffer_to_image(cb, render3d_st.gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic) Vk.cmd_copy_buffer_to_image(cb, render3d_st.gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic)
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
let ok = gvk_once_end(render3d_st, cb) let ok = gvk_once_end(render3d_st, cb)
gvk_staging_free(render3d_st) gvk_staging_free(render3d_st)
return ok return ok
@ -315,8 +321,8 @@ function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h:
render3d_st.gvk_tex_image[tex] = zero render3d_st.gvk_tex_image[tex] = zero
if not gvk_tex_storage(render3d_st, tex, render3d_st.gvk_tex_array[tex] == 1, render3d_st.gvk_tex_glfmt[tex], w, h, layers, true) { return false } if not gvk_tex_storage(render3d_st, tex, render3d_st.gvk_tex_array[tex] == 1, render3d_st.gvk_tex_glfmt[tex], w, h, layers, true) { return false }
let cb = gvk_once_begin(render3d_st) let cb = gvk_once_begin(render3d_st)
gvk_barrier(cb, old_image, false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(render3d_st, cb, old_image, false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
gvk_barrier(cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let ic = bytes(VkImageCopy_sizeof) let ic = bytes(VkImageCopy_sizeof)
Vk.zero(ic, VkImageCopy_sizeof) Vk.zero(ic, VkImageCopy_sizeof)
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
@ -327,9 +333,11 @@ function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h:
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h) Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1) Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1)
Vk.cmd_copy_image(cb, old_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[tex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic) Vk.cmd_copy_image(cb, old_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[tex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
gvk_barrier(cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
let ok = gvk_once_end(render3d_st, cb) let ok = gvk_once_end(render3d_st, cb)
render3d_st.gvk_mk_x_view += 1
Vk.destroy_image_view(render3d_st.gvk_dev, old_view, null) Vk.destroy_image_view(render3d_st.gvk_dev, old_view, null)
render3d_st.gvk_mk_x_img += 1
Vk.destroy_image(render3d_st.gvk_dev, old_image, null) Vk.destroy_image(render3d_st.gvk_dev, old_image, null)
gvk_mem_free(render3d_st, gvk_mem_id(old_mem)) gvk_mem_free(render3d_st, gvk_mem_id(old_mem))
return ok return ok
@ -346,7 +354,7 @@ function gvk_tex_mips(render3d_st: mut Render3dState, tex: int, w: int, h: int)
return gvk_once_end(render3d_st, cb) return gvk_once_end(render3d_st, cb)
} }
# the chain's blits and barriers recorded into cb (the frame's own, when one is open) # the chain's blits and barriers recorded into cb (the frame's own, when one is open)
function gvk_tex_mips_into(render3d_st: Render3dState, cb: pointer, tex: int, w: int, h: int) -> void { function gvk_tex_mips_into(render3d_st: mut Render3dState, cb: pointer, tex: int, w: int, h: int) -> void {
let levels = render3d_st.gvk_tex_levels[tex] let levels = render3d_st.gvk_tex_levels[tex]
if levels <= 1 { return } if levels <= 1 { return }
let image = render3d_st.gvk_tex_image[tex] let image = render3d_st.gvk_tex_image[tex]
@ -358,8 +366,8 @@ function gvk_tex_mips_into(render3d_st: Render3dState, cb: pointer, tex: int, w:
if dw < 1 { dw = 1 } if dw < 1 { dw = 1 }
var dh = sh / 2 var dh = sh / 2
if dh < 1 { dh = 1 } if dh < 1 { dh = 1 }
gvk_barrier(cb, image, false, lv - 1, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(render3d_st, cb, image, false, lv - 1, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
gvk_barrier(cb, image, false, lv, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) gvk_barrier(render3d_st, cb, image, false, lv, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let blit = bytes(VkImageBlit_sizeof) let blit = bytes(VkImageBlit_sizeof)
Vk.zero(blit, VkImageBlit_sizeof) Vk.zero(blit, VkImageBlit_sizeof)
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
@ -375,8 +383,8 @@ function gvk_tex_mips_into(render3d_st: Render3dState, cb: pointer, tex: int, w:
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, dh) Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, dh)
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1) Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
Vk.cmd_blit_image(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR) Vk.cmd_blit_image(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR)
gvk_barrier(cb, image, false, lv - 1, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, image, false, lv - 1, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
gvk_barrier(cb, image, false, lv, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, image, false, lv, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
sw = dw sw = dw
sh = dh sh = dh
} }
@ -400,7 +408,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
let image = render3d_st.gvk_tex_image[tex] let image = render3d_st.gvk_tex_image[tex]
let depth = gvk_is_depth(ifmt) let depth = gvk_is_depth(ifmt)
let cb = gvk_once_begin(render3d_st) let cb = gvk_once_begin(render3d_st)
gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
let bic = bytes(VkBufferImageCopy_sizeof) let bic = bytes(VkBufferImageCopy_sizeof)
Vk.zero(bic, VkBufferImageCopy_sizeof) Vk.zero(bic, VkBufferImageCopy_sizeof)
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
@ -410,7 +418,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h) Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1) Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_st_buf, 1, bic) Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_st_buf, 1, bic)
gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
let ok = gvk_once_end(render3d_st, cb) let ok = gvk_once_end(render3d_st, cb)
if ok and cwant == cout { mem_copy(out, src, n) } if ok and cwant == cout { mem_copy(out, src, n) }
if ok and cwant < cout { if ok and cwant < cout {
@ -426,14 +434,39 @@ function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void {
gvk_frame_wait(render3d_st) # the frame in flight may still read it gvk_frame_wait(render3d_st) # the frame in flight may still read it
if render3d_st.gvk_tex_image[tex] == 0 { return } if render3d_st.gvk_tex_image[tex] == 0 { return }
let zero: long = 0 let zero: long = 0
render3d_st.gvk_mk_x_view += 1
Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], null) Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], null)
render3d_st.gvk_mk_x_img += 1
Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], null) Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], null)
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_tex_mem[tex])) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_tex_mem[tex]))
gvk_layer_views_drop(render3d_st, tex)
render3d_st.gvk_tex_image[tex] = zero; render3d_st.gvk_tex_view[tex] = zero; render3d_st.gvk_tex_mem[tex] = zero render3d_st.gvk_tex_image[tex] = zero; render3d_st.gvk_tex_view[tex] = zero; render3d_st.gvk_tex_mem[tex] = zero
render3d_st.gvk_tex_levels[tex] = 0; render3d_st.gvk_tex_layers[tex] = 0; render3d_st.gvk_tex_vkfmt[tex] = 0 render3d_st.gvk_tex_levels[tex] = 0; render3d_st.gvk_tex_layers[tex] = 0; render3d_st.gvk_tex_vkfmt[tex] = 0
render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1 render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1
} }
# the per-level and per-layer views gvk_view_of made of a texture go with it: kept, each held its
# old Metal texture alive, so every rebuild of the screen targets left the last set's memory behind
function gvk_layer_views_drop(render3d_st: mut Render3dState, tex: int) -> void {
if render3d_st.gvk_layer_views == null { return }
let keys = render3d_st.gvk_layer_views
let views = render3d_st.gvk_layer_view
let texs = render3d_st.gvk_layer_view_tex
var w = 0
for i in 0 .. len(keys) {
if texs[i] == tex {
render3d_st.gvk_mk_x_view += 1
Vk.destroy_image_view(render3d_st.gvk_dev, views[i], null)
} else {
keys[w] = keys[i]; views[w] = views[i]; texs[w] = texs[i]
w += 1
}
}
while len(keys) > w { List.pop(keys) }
while len(views) > w { List.pop(views) }
while len(texs) > w { List.pop(texs) }
}
# ---- samplers ----------------------------------------------------------------------------- # ---- samplers -----------------------------------------------------------------------------
# One VkSampler per distinct way of reading a texture, made the first time it is asked for. # One VkSampler per distinct way of reading a texture, made the first time it is asked for.
# The arguments are what the renderer set through gpu_tex_param (0 where it set nothing, which # The arguments are what the renderer set through gpu_tex_param (0 where it set nothing, which
@ -526,6 +559,7 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra
Vk.put_i32(sci, VkSamplerCreateInfo_borderColor, VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE) Vk.put_i32(sci, VkSamplerCreateInfo_borderColor, VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE)
let out = bytes(8) let out = bytes(8)
let zero: long = 0 let zero: long = 0
render3d_st.gvk_mk_smp += 1
let r = Vk.create_sampler(render3d_st.gvk_dev, sci, null, out) let r = Vk.create_sampler(render3d_st.gvk_dev, sci, null, out)
if r != VK_SUCCESS { gvk_fail(render3d_st, "vkCreateSampler", r); return zero } if r != VK_SUCCESS { gvk_fail(render3d_st, "vkCreateSampler", r); return zero }
let s = gvk_handle(out) let s = gvk_handle(out)
@ -564,6 +598,7 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) } while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) }
push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b]) push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b])
} else { } else {
render3d_st.gvk_mk_x_buf += 1
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null) Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null)
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b])) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b]))
} }
@ -582,22 +617,27 @@ function gvk_retire_flush(render3d_st: mut Render3dState) -> void { gvk_retire_u
# the frame being recorded read stays # the frame being recorded read stays
function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void { function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void {
if render3d_st.gvk_retired_buf == null { return } if render3d_st.gvk_retired_buf == null { return }
let keep_buf = new []long if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int }
let keep_mem = new []long while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) { push(render3d_st.gvk_retired_frame, 0) }
let keep_frame = new []int # compacted in place: three fresh lists a frame were never given back
var w = 0
for i in 0 .. len(render3d_st.gvk_retired_buf) { for i in 0 .. len(render3d_st.gvk_retired_buf) {
var f = 0 let f = render3d_st.gvk_retired_frame[i]
if render3d_st.gvk_retired_frame != null and i < len(render3d_st.gvk_retired_frame) { f = render3d_st.gvk_retired_frame[i] }
if f > done { if f > done {
push(keep_buf, render3d_st.gvk_retired_buf[i]); push(keep_mem, render3d_st.gvk_retired_mem[i]); push(keep_frame, f) render3d_st.gvk_retired_buf[w] = render3d_st.gvk_retired_buf[i]; render3d_st.gvk_retired_mem[w] = render3d_st.gvk_retired_mem[i]; render3d_st.gvk_retired_frame[w] = f
w += 1
} else { } else {
render3d_st.gvk_mk_x_buf += 1
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null) Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null)
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i])) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i]))
} }
} }
render3d_st.gvk_retired_buf = keep_buf let rb = render3d_st.gvk_retired_buf
render3d_st.gvk_retired_mem = keep_mem let rm = render3d_st.gvk_retired_mem
render3d_st.gvk_retired_frame = keep_frame let rf = render3d_st.gvk_retired_frame
while len(rb) > w { List.pop(rb) }
while len(rm) > w { List.pop(rm) }
while len(rf) > w { List.pop(rf) }
} }
# A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there, # A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there,
@ -619,17 +659,18 @@ function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool
var size = n var size = n
if size < 64 { size = 64 } if size < 64 { size = 64 }
let size_l: long = size let size_l: long = size
let bci = bytes(VkBufferCreateInfo_sizeof) let bci = gvk_tmp(render3d_st, VkBufferCreateInfo_sizeof)
Vk.zero(bci, VkBufferCreateInfo_sizeof) Vk.zero(bci, VkBufferCreateInfo_sizeof)
Vk.put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO) Vk.put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO)
Vk.put_i64(bci, VkBufferCreateInfo_size, size_l) Vk.put_i64(bci, VkBufferCreateInfo_size, size_l)
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT) Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
let out = bytes(8) let out = gvk_tmp(render3d_st, 8)
render3d_st.gvk_mk_buf += 1
var r = Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) var r = Vk.create_buffer(render3d_st.gvk_dev, bci, null, out)
if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateBuffer ({size} bytes)`, r) } if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateBuffer ({size} bytes)`, r) }
let buf = gvk_handle(out) let buf = gvk_handle(out)
let req = bytes(VkMemoryRequirements_sizeof) let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof)
Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req) Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req)
let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false) let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false)
let zero: long = 0 let zero: long = 0

View file

@ -102,9 +102,14 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void {
gg_tiles(render3d_st, 4, 0.0, 16.0) gg_tiles(render3d_st, 4, 0.0, 16.0)
gg_tiles(render3d_st, 8, 16.0, 40.0) gg_tiles(render3d_st, 8, 16.0, 40.0)
gg_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius) gg_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius)
let rb = words(4) # made once: a words() a frame is a calloc nothing frees
if render3d_st.gg_rb == null {
render3d_st.gg_rb = words(4); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(3)
render3d_st.gg_texs = words(2); render3d_st.gg_pr = words(48)
}
let rb = render3d_st.gg_rb
rb[0] = GG_BANDS; rb[1] = 0; rb[2] = 0; rb[3] = 0 rb[0] = GG_BANDS; rb[1] = 0; rb[2] = 0; rb[3] = 0
let cb = words(1) let cb = render3d_st.gg_cb
cb[0] = render3d_st.gg_cmds cb[0] = render3d_st.gg_cmds
gpu_dispatch(render3d_st, render3d_st.gg_reset, data_of(rb), 16, cb, 1) gpu_dispatch(render3d_st, render3d_st.gg_reset, data_of(rb), 16, cb, 1)
if render3d_st.gg_n == 0 { return } if render3d_st.gg_n == 0 { return }
@ -112,16 +117,16 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void {
let tb = render3d_st.gg_tiles_buf[render3d_st.r3d_test_frame % 2] let tb = render3d_st.gg_tiles_buf[render3d_st.r3d_test_frame % 2]
gpu_buffer_upload(render3d_st, tb, render3d_st.gg_n * 16, data_of(render3d_st.gg_tv), GPU_DYNAMIC) gpu_buffer_upload(render3d_st, tb, render3d_st.gg_n * 16, data_of(render3d_st.gg_tv), GPU_DYNAMIC)
let pr = gg_params(render3d_st) let pr = gg_params(render3d_st)
let bufs = words(3) let bufs = render3d_st.gg_bufs
bufs[0] = tb; bufs[1] = render3d_st.gg_out; bufs[2] = render3d_st.gg_cmds bufs[0] = tb; bufs[1] = render3d_st.gg_out; bufs[2] = render3d_st.gg_cmds
let texs = words(2) let texs = render3d_st.gg_texs
texs[0] = render3d_st.ter_height_tex; texs[1] = render3d_st.ter_ortho_tex texs[0] = render3d_st.ter_height_tex; texs[1] = render3d_st.ter_ortho_tex
gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 192, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n) gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 192, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n)
} }
# grass_cull.comp's Params, std140: 12 vec4s # grass_cull.comp's Params, std140: 12 vec4s, into the block made once in gg_cull_frame
function gg_params(render3d_st: Render3dState) -> words { function gg_params(render3d_st: mut Render3dState) -> words {
let pr = words(48) let pr = render3d_st.gg_pr
for i in 0 .. 48 { pr[i] = 0 } for i in 0 .. 48 { pr[i] = 0 }
if render3d_st.cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(render3d_st.cam_planes[i]) } } if render3d_st.cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(render3d_st.cam_planes[i]) } }
pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(render3d_st.grass_radius) pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(render3d_st.grass_radius)

View file

@ -0,0 +1,34 @@
# chunks.ludic - the Things of one chunk of the world, counted and let go together: what a streamed
# world asks when a chunk is loaded or dropped. A chunk is chunk_of(x, z, TH_CHUNK_M).
export function things_chunk(x: float, z: float) -> int { return chunk_of(x, z, TH_CHUNK_M) }
export function things_chunk_count(things_st: ThingsState, chunk: int) -> int { return ix_count(things_st.th.chunks, chunk) }
# every Thing standing in the chunk removed for good, the port and the facts told of each as
# thing_remove tells them; how many went
export function things_drop_chunk(things_st: mut ThingsState, chunk: int) -> int {
var n = 0
let rows = ix_rows(things_st.th.chunks, chunk)
while len(rows) > 0 {
thing_remove(things_st, things_st.th.tab.rec[rows[len(rows) - 1]])
n += 1
}
return n
}
# change detection: the tick now, the table moving on to the next - a consumer keeps what this
# returned and asks things_changed(since: it) for what was written after
export function things_now(things_st: mut ThingsState) -> int {
let t = tb_tick(things_st.th.tab)
tb_advance(things_st.th.tab)
return t
}
# every Thing a verb wrote after `since`, into the caller's list (untouched 64-row blocks skipped)
export function things_changed(things_st: ThingsState, since: int, rows: words, out: []Thing) -> int {
List.clear(out)
let tb = things_st.th.tab
let n = tb_changed_since(tb, since, rows)
for k in 0 .. n { push(out, tb.rec[rows[k]]) }
return n
}

View file

@ -45,14 +45,22 @@ export function thing_is_personal(t: Thing) -> bool { return thing_kind_ok(t.kin
# set down by someone rather than grown by the world: the kind says, and the port has the last word # set down by someone rather than grown by the world: the kind says, and the port has the last word
export function thing_is_put_down(t: Thing) -> bool { return ThingsWorld.put_down(t) } export function thing_is_put_down(t: Thing) -> bool { return ThingsWorld.put_down(t) }
# every active Thing whose kind ticks, `hours` of the world's clock on # every active Thing whose kind ticks, `hours` of the world's clock on: only the kinds that tick
export function things_tick(things_st: ThingsState, hours: float) -> void { # are visited, through the kind index, from a list of handles taken first - a tick may hide one
let all = things_all(things_st) export function things_tick(things_st: mut ThingsState, hours: float) -> void {
for i in 0 .. len(all) { let hs = things_st.th_hs
let t = all[i] List.clear(hs)
if not t.active or not thing_kind_ok(t.kind) { continue } let tb = things_st.th.tab
let k = ThingKinds[t.kind] for k in 0 .. TH_COUNT {
if k.tick != null { k.tick(t, hours) } if ThingKinds[k].tick == null { continue }
let rows = ix_rows(things_st.th.kinds, k)
for i in 0 .. len(rows) { push(hs, tb.ent[rows[i]]) }
}
for i in 0 .. len(hs) {
let r = tb_row(tb, hs[i])
if r < 0 { continue }
let t = tb.rec[r]
if t.active { ThingKinds[t.kind].tick(t, hours) }
} }
} }

View file

@ -6,6 +6,10 @@ import "ludic.base"
import "kinds.ludic" import "kinds.ludic"
import "port.ludic" import "port.ludic"
import "store.ludic" import "store.ludic"
import "verbs.ludic"
import "moves.ludic"
import "queries.ludic" import "queries.ludic"
import "near.ludic"
import "chunks.ludic"
import "dispatch.ludic" import "dispatch.ludic"
import "system.ludic" import "system.ludic"

View file

@ -19,6 +19,8 @@ export property Thing {
sz: float = 0.0 sz: float = 0.0
syaw: float = 0.0 syaw: float = 0.0
shared: bool = false # the game copies it to others (a party) shared: bool = false # the game copies it to others (a party)
ent: int = -1 # its handle in the table, -1 once removed
tab: Table<Thing> = null # the table holding it, so a verb on the Thing alone keeps the indexes
} }
export property ThingKind { export property ThingKind {

View file

@ -0,0 +1,55 @@
# moves.ludic - a Thing moved, shown or hidden: the record and its row together, so the grid, the
# kind indexes and the chunk index never answer for where it was or what it was
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
thing_place_at(t, x, y, z, yaw)
ThingsWorld.moved(t)
}
# moved without telling the port: a stand-in the game draws itself, or nothing draws
export function thing_place_at(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
let r = th_row(t)
if r >= 0 { th_moved(t.tab, r, x, z) }
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
thing_set_on(t, on)
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }
# active or not without telling the port
export function thing_set_on(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
let r = th_row(t)
if r < 0 { return }
var v = 0
if on { v = 1 }
tb_set_i(t.tab, TH_ON, r, v)
}
# where it stands on the ground plane, without telling the port (its height is the caller's)
export function thing_set_xz(t: Thing, x: float, z: float) -> void {
if t == null { return }
t.x = x
t.z = z
let r = th_row(t)
if r >= 0 { th_moved(t.tab, r, x, z) }
}
# where a row now stands: its columns, its cell in the grid and its chunk
function th_moved(tb: Table<Thing>, r: int, x: float, z: float) -> void {
tb_set_xz(tb, TH_X, TH_Z, r, x, z)
let c = chunk_of(x, z, TH_CHUNK_M)
if tb.i[TH_CHUNK][r] != c { tb_set_i(tb, TH_CHUNK, r, c) }
}

View file

@ -0,0 +1,35 @@
# near.ludic - the neighbourhood questions a frame asks many times: what is around a point, of any
# kind or of one, into a list the CALLER keeps (in its own state, reused every frame), from the grid
# or the kind's own list - never a walk of every Thing, and nothing allocated or written here
export function things_near(things_st: ThingsState, x: float, z: float, r: float, out: []Thing) -> int {
return tb_within_recs(things_st.th.tab, things_st.th.grid, x, z, r, -1, 0, out)
}
export function things_near_of(things_st: ThingsState, kind: int, x: float, z: float, r: float, out: []Thing) -> int {
return tb_within_of_recs(things_st.th.tab, things_st.th.grid, things_st.th.kinds, x, z, r, kind, out)
}
# the nearest active Thing within maxr (0: any distance) that `keep` says yes to, or null
export function thing_nearest_where(things_st: ThingsState, x: float, z: float, maxr: float, keep: fn(Thing) -> bool) -> Thing {
let r = tb_nearest_where(things_st.th.tab, things_st.th.grid, x, z, maxr, keep)
if r < 0 { return null }
return things_st.th.tab.rec[r]
}
# every one of a kind, active or not, counted and walked without a list
export function things_count_all_of(things_st: ThingsState, kind: int) -> int { return ix_count(things_st.th.every, kind) }
export function thing_of_kind_all(things_st: ThingsState, kind: int, i: int) -> Thing {
let rows = ix_rows(things_st.th.every, kind)
if i < 0 or i >= len(rows) { return null }
return things_st.th.tab.rec[rows[i]]
}
# the nearest active one of a kind (below 0: any) strictly within maxr of (x, z), or null
export function thing_nearest_within(things_st: ThingsState, kind: int, x: float, z: float, maxr: float) -> Thing {
let tb = things_st.th.tab
var r = -1
if kind < 0 { r = tb_nearest(tb, things_st.th.grid, x, z, maxr, -1, 0) } else { r = tb_nearest_of(tb, things_st.th.grid, things_st.th.kinds, x, z, maxr, kind) }
if r < 0 { return null }
return tb.rec[r]
}

View file

@ -1,38 +1,43 @@
# queries.ludic - where the Things are. Only active Things answer, except thing_by_uid and # queries.ludic - where the Things are, answered by the table's indexes rather than a scan. Only
# things_each_all, which ask about everything that exists. A kind below 0 is any kind. # active Things answer, except thing_by_uid and things_each_all, which ask about everything that
# exists. A kind below 0 is any kind.
function th_of(t: Thing, kind: int) -> bool { return kind < 0 or t.kind == kind } function th_of(t: Thing, kind: int) -> bool { return kind < 0 or t.kind == kind }
# the first active one of a kind (and of that id, unless id < 0), in the order placed # the first-placed active one of a kind (and of that id, unless id < 0)
export function thing_find(things_st: ThingsState, kind: int, id: int) -> Thing { export function thing_find(things_st: ThingsState, kind: int, id: int) -> Thing {
let all = things_all(things_st) let tb = things_st.th.tab
for i in 0 .. len(all) { var best: Thing = null
let t = all[i] if kind < 0 {
if t.active and th_of(t, kind) and (id < 0 or t.id == id) { return t } for r in 0 .. tb_len(tb) { best = th_earlier(best, tb.rec[r], id) }
return best
} }
return null let rows = ix_rows(things_st.th.kinds, kind)
for k in 0 .. len(rows) { best = th_earlier(best, tb.rec[rows[k]], id) }
return best
}
# the one of the two placed first that is active and has the id asked for
function th_earlier(best: Thing, t: Thing, id: int) -> Thing {
if not t.active or (id >= 0 and t.id != id) { return best }
if best == null or t.uid < best.uid { return t }
return best
} }
export function thing_by_uid(things_st: ThingsState, uid: int) -> Thing { export function thing_by_uid(things_st: ThingsState, uid: int) -> Thing {
let all = things_all(things_st) let r = tb_row(things_st.th.tab, imap_get(things_st.th_uids, uid, -1))
for i in 0 .. len(all) { if all[i].uid == uid { return all[i] } } if r < 0 { return null }
return null return things_st.th.tab.rec[r]
} }
# the nearest active one of a kind to (x, z), or null # the nearest active one of a kind to (x, z), or null
export function thing_nearest(things_st: ThingsState, kind: int, x: float, z: float) -> Thing { export function thing_nearest(things_st: ThingsState, kind: int, x: float, z: float) -> Thing {
var best: Thing = null let tb = things_st.th.tab
var bd = 0.0 var r = -1
let all = things_all(things_st) if kind < 0 { r = tb_nearest(tb, things_st.th.grid, x, z, 0.0, -1, 0) } else {
for i in 0 .. len(all) { r = tb_nearest_of(tb, things_st.th.grid, things_st.th.kinds, x, z, 0.0, kind)
let t = all[i]
if not t.active or not th_of(t, kind) { continue }
let d = thing_dist2(t, x, z)
if best == null or d < bd {
best = t
bd = d
} }
} if r < 0 { return null }
return best return tb.rec[r]
} }
# the squared ground distance from a Thing to (x, z) # the squared ground distance from a Thing to (x, z)
@ -42,14 +47,13 @@ export function thing_dist2(t: Thing, x: float, z: float) -> float {
return dx * dx + dz * dz return dx * dx + dz * dz
} }
# every active one within r of (x, z), nearest first # every active one within r of (x, z), nearest first, in a new list: for a question asked now and
# then - a frame's question is things_near, into a list the caller keeps
export function things_within(things_st: ThingsState, x: float, z: float, r: float) -> []Thing { export function things_within(things_st: ThingsState, x: float, z: float, r: float) -> []Thing {
let found = new []Thing
things_near(things_st, x, z, r, found)
let out = new []Thing let out = new []Thing
let all = things_all(things_st) for k in 0 .. len(found) { th_insert(out, found[k], x, z) }
for i in 0 .. len(all) {
let t = all[i]
if t.active and thing_dist2(t, x, z) <= r * r { th_insert(out, t, x, z) }
}
return out return out
} }
@ -63,18 +67,44 @@ function th_insert(out: []Thing, t: Thing, x: float, z: float) -> void {
out[j] = t out[j] = t
} }
# every active one of a kind, in the order placed # every active one of a kind, first placed first
export function things_each(things_st: ThingsState, kind: int) -> []Thing { export function things_each(things_st: ThingsState, kind: int) -> []Thing {
let out = new []Thing return th_listed(things_st, ix_rows(things_st.th.kinds, kind))
let all = things_all(things_st)
for i in 0 .. len(all) { if all[i].active and th_of(all[i], kind) { push(out, all[i]) } }
return out
} }
# every one of a kind, active or not # every one of a kind, active or not
export function things_each_all(things_st: ThingsState, kind: int) -> []Thing { export function things_each_all(things_st: ThingsState, kind: int) -> []Thing {
let out = new []Thing return th_listed(things_st, ix_rows(things_st.th.every, kind))
let all = things_all(things_st) }
for i in 0 .. len(all) { if th_of(all[i], kind) { push(out, all[i]) } }
return out # how many active ones of a kind there are, without a list
export function things_count_of(things_st: ThingsState, kind: int) -> int {
return ix_count(things_st.th.kinds, kind)
}
function th_listed(things_st: ThingsState, rows: words) -> []Thing {
let out = new []Thing
for k in 0 .. len(rows) { push(out, things_st.th.tab.rec[rows[k]]) }
return th_by_uid_order(out)
}
function th_by_uid_order(xs: []Thing) -> []Thing {
for i in 1 .. len(xs) {
let t = xs[i]
var j = i
while j > 0 and xs[j - 1].uid > t.uid {
xs[j] = xs[j - 1]
j -= 1
}
xs[j] = t
}
return xs
}
# the i-th active one of a kind, 0 .. things_count_of(kind) - 1, in the index's order (which a
# removal or a hide changes: walk it within a frame, never keep an i)
export function thing_of_kind(things_st: ThingsState, kind: int, i: int) -> Thing {
let rows = ix_rows(things_st.th.kinds, kind)
if i < 0 or i >= len(rows) { return null }
return things_st.th.tab.rec[rows[i]]
} }

View file

@ -1,10 +1,42 @@
# store.ludic - the Things, and the only verbs that make, remove, move, show and hide one; each # store.ludic - the Things, rows of a ludic.base Table: where each stands and whether it is active
# placing and removing is a fact, and the port is told at once so the game can draw it # are columns a grid and two kind indexes are kept over, and only the verbs (verbs.ludic) make,
# move, show, hide and remove one; each placing and removing is a fact the port hears at once
export const TH_X: int = 0 # float columns
export const TH_Z: int = 1
export const TH_KIND: int = 0 # int columns
export const TH_ON: int = 1 # 1 while active
export const TH_CHUNK: int = 2 # the chunk of the world it stands in (chunk_of, TH_CHUNK_M)
export const TH_CHUNK_M: float = 256.0
const TH_CELL: float = 16.0
# the table and the indexes kept over it, made together
export property ThingsTable {
tab: Table<Thing> = null
grid: Grid = null
kinds: IntIndex = null # active Things by kind
every: IntIndex = null # every Thing by kind, active or not
chunks: IntIndex = null # every Thing by the chunk it stands in: loaded and let go together
}
export state ThingsState { export state ThingsState {
th_list: []Thing = new []Thing th: ThingsTable = th__new()
th_uids: IntMap = imap_new()
th_uid: int = 0 th_uid: int = 0
th_facts: Queue<ThingFact> = th_facts__new() th_facts: Queue<ThingFact> = th_facts__new()
th_marked: int = -1 th_hs: words = words(0) # things_tick's handles, taken before any tick runs
}
function th_facts__new() -> Queue<ThingFact> { return queue_new("things.facts") }
function th__new() -> ThingsTable {
let th = new ThingsTable
let tb: Table<Thing> = table_new(2, 3)
th.tab = tb
th.grid = tb_grid(tb, TH_X, TH_Z, TH_ON, TH_CELL)
th.kinds = tb_index(tb, TH_KIND, TH_ON)
th.every = tb_index(tb, TH_KIND, -1)
th.chunks = tb_index(tb, TH_CHUNK, -1)
return th
} }
export function things_facts(things_st: ThingsState) -> Queue<ThingFact> { export function things_facts(things_st: ThingsState) -> Queue<ThingFact> {
@ -24,88 +56,37 @@ function th_fact(things_st: ThingsState, what: int, t: Thing) -> void {
} }
# whether a world has been set out (things_clear) since the program began # whether a world has been set out (things_clear) since the program began
export function things_ready(things_st: ThingsState) -> bool { return things_st.th_list != null } export function things_ready(things_st: ThingsState) -> bool { return things_st.th.tab != null }
# every Thing, in the order placed; the list is the package's, never to be pushed to # every Thing, one per row - the table's own list: read it, never push to it. A removal moves the
export function things_all(things_st: ThingsState) -> []Thing { # last Thing into the removed one's place, so the order is the table's, not the order placed.
return things_st.th_list export function things_all(things_st: ThingsState) -> []Thing { return things_st.th.tab.rec }
}
export function things_count(things_st: ThingsState) -> int { return len(things_all(things_st)) } export function things_count(things_st: ThingsState) -> int { return tb_len(things_st.th.tab) }
# a new world: every Thing forgotten, quietly (the game has already dropped its drawings) # a new world: every Thing forgotten, quietly (the game has already dropped its drawings)
export function things_clear(things_st: mut ThingsState) -> void { things_st.th_list = new []Thing } export function things_clear(things_st: mut ThingsState) -> void {
let tb = things_st.th.tab
export function thing_spawn(things_st: mut ThingsState, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing { for r in 0 .. tb_len(tb) { tb.rec[r].ent = -1 }
return th_place(things_st, new Thing, kind, look, x, y, z, yaw, id) tb_clear(tb)
imap_clear(things_st.th_uids)
} }
function th_place(things_st: mut ThingsState, t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing { # the row a Thing stands in, or -1 when it is not in the table any more
t.kind = kind function th_row(t: Thing) -> int {
t.look = look if t == null or t.tab == null { return -1 }
t.x = x return tb_row(t.tab, t.ent)
t.y = y
t.z = z
t.yaw = yaw
t.id = id
things_st.th_uid += 1
t.uid = things_st.th_uid
push(things_all(things_st), t)
ThingsWorld.placed(t)
th_fact(things_st, THING_PLACED, t)
return t
} }
# a Thing on the ground (the port's), set into it by `sink`, used from `reach` # the columns say what the record says: a Thing's fields written around the verbs are caught here
export function thing_put(things_st: mut ThingsState, kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing { export function things_verify(things_st: ThingsState) -> int {
let t = new Thing let tb = things_st.th.tab
t.reach = reach var bad = 0
return th_place(things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id) for r in 0 .. tb_len(tb) {
} let t = tb.rec[r]
var on = 0
# gone for good if t.active { on = 1 }
export function thing_remove(things_st: mut ThingsState, t: Thing) -> void { if tb.f[TH_X][r] != t.x or tb.f[TH_Z][r] != t.z or tb.i[TH_KIND][r] != t.kind or tb.i[TH_ON][r] != on or tb.i[TH_CHUNK][r] != chunk_of(t.x, t.z, TH_CHUNK_M) or t.ent != tb.ent[r] { bad += 1 }
if t == null { return }
things_st.th_marked = t.uid
things_drop(things_st, fn th_is_marked)
things_st.th_marked = -1
}
function th_is_marked(things_st: ThingsState, t: Thing) -> bool { return t.uid == things_st.th_marked }
# every Thing `gone` says so is removed for good
export function things_drop(things_st: mut ThingsState, gone: fn(Thing) -> bool) -> void {
let keep = new []Thing
let all = things_all(things_st)
for i in 0 .. len(all) {
let t = all[i]
if gone(t) {
ThingsWorld.removed(t)
th_fact(things_st, THING_REMOVED, t)
} else {
push(keep, t)
} }
} return bad
things_st.th_list = keep
} }
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
ThingsWorld.moved(t)
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }
function th_facts__new() -> Queue<ThingFact> { return queue_new("things.facts") }

View file

@ -155,4 +155,64 @@ program ThingsTest {
expect_eq(t.owner, 0) expect_eq(t.owner, 0)
expect_eq(things_test_st.back, 1) expect_eq(things_test_st.back, 1)
} }
test "removals keep the indexes true: nearest, find, uid and the columns agree" (things_st: mut ThingsState) {
things_clear(things_st)
var keep: Thing = null
for i in 0 .. 400 {
let t = put(things_st, TH_BRANCH + (i % 2), float(i) * 3.0 - 600.0, float(i % 17) * 5.0)
if i == 250 { keep = t }
}
let gone = thing_find(things_st, TH_BRANCH, -1)
let uid = gone.uid
thing_remove(things_st, gone)
expect(thing_by_uid(things_st, uid) == null)
expect(thing_by_uid(things_st, keep.uid) == keep)
expect_eq(things_count(things_st), 399)
expect_eq(things_count_of(things_st, TH_BRANCH), 199)
thing_move(keep, 5000.0, 5000.0, 0.0)
expect(thing_nearest(things_st, -1, 4990.0, 4990.0) == keep)
thing_hide(keep)
expect(thing_nearest(things_st, -1, 4990.0, 4990.0) != keep)
expect_eq(things_verify(things_st), 0)
keep.x = 1.0
expect_eq(things_verify(things_st), 1)
}
test "a chunk's Things are counted and let go together; a move carries a Thing to its new chunk" (things_st: mut ThingsState, things_test_st: ThingsTestState) {
things_clear(things_st)
put(things_st, TH_BRANCH, 10.0, 10.0)
put(things_st, TH_BRANCH, 20.0, 30.0)
let far = put(things_st, TH_BRANCH, 600.0, 10.0)
let here = things_chunk(10.0, 10.0)
expect_eq(things_chunk_count(things_st, here), 2)
expect_eq(things_chunk_count(things_st, things_chunk(600.0, 10.0)), 1)
thing_move(far, 15.0, 15.0, 0.0)
expect_eq(things_chunk_count(things_st, here), 3)
facts(things_st)
let gone = things_test_st.gone
expect_eq(things_drop_chunk(things_st, here), 3)
expect_eq(things_count(things_st), 0)
expect_eq(things_test_st.gone, gone + 3)
expect_eq(len(facts(things_st)), 3)
expect_eq(things_verify(things_st), 0)
}
test "change detection names what a verb wrote since the tick a consumer kept" (things_st: mut ThingsState) {
things_clear(things_st)
let a = put(things_st, TH_BRANCH, 1.0, 0.0)
let b = put(things_st, TH_BRANCH, 2.0, 0.0)
let seen = things_now(things_st)
let rows = words(0)
let out = new []Thing
expect_eq(things_changed(things_st, seen, rows, out), 0)
thing_hide(b)
expect_eq(things_changed(things_st, seen, rows, out), 1)
expect(out[0] == b)
let later = things_now(things_st)
thing_move(a, 9.0, 9.0, 0.0)
expect_eq(things_changed(things_st, later, rows, out), 1)
expect(out[0] == a)
expect_eq(things_changed(things_st, seen, rows, out), 2)
}
} }

View file

@ -0,0 +1,69 @@
# verbs.ludic - the only ways a Thing is made, removed, moved, shown or hidden: each writes the
# record and the table's columns together, so the grid and the kind indexes never fall behind
export function thing_spawn(things_st: mut ThingsState, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
return th_place(things_st, new Thing, kind, look, x, y, z, yaw, id)
}
function th_place(things_st: mut ThingsState, t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
t.kind = kind
t.look = look
t.x = x
t.y = y
t.z = z
t.yaw = yaw
t.id = id
things_st.th_uid += 1
t.uid = things_st.th_uid
let tb = things_st.th.tab
t.tab = tb
t.ent = tb_add(tb, t)
th_columns(t)
imap_put(things_st.th_uids, t.uid, t.ent)
ThingsWorld.placed(t)
th_fact(things_st, THING_PLACED, t)
return t
}
# the record's x, z, kind and active into its row, refiled once
function th_columns(t: Thing) -> void {
let r = th_row(t)
if r < 0 { return }
let tb = t.tab
tb.f[TH_X][r] = t.x
tb.f[TH_Z][r] = t.z
tb.i[TH_KIND][r] = t.kind
var on = 0
if t.active { on = 1 }
tb.i[TH_ON][r] = on
tb.i[TH_CHUNK][r] = chunk_of(t.x, t.z, TH_CHUNK_M)
tb_refile(tb, r)
}
# a Thing on the ground (the port's), set into it by `sink`, used from `reach`
export function thing_put(things_st: mut ThingsState, kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing {
let t = new Thing
t.reach = reach
return th_place(things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id)
}
# gone for good
export function thing_remove(things_st: mut ThingsState, t: Thing) -> void {
if th_row(t) < 0 { return }
ThingsWorld.removed(t)
th_fact(things_st, THING_REMOVED, t)
imap_del(things_st.th_uids, t.uid)
tb_remove(t.tab, t.ent)
t.ent = -1
}
# every Thing `gone` says so is removed for good; from the last row down, so the row a removal
# moves into place has already been asked
export function things_drop(things_st: mut ThingsState, gone: fn(Thing) -> bool) -> void {
let tb = things_st.th.tab
var r = tb_len(tb) - 1
while r >= 0 {
let t = tb.rec[r]
if gone(t) { thing_remove(things_st, t) }
r -= 1
}
}

View file

@ -1,10 +1,11 @@
# call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there # call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there
# after; gone when its player leaves # after; gone when its player leaves
function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw: float) -> Vehicle { function ve_add(vehicles_st: VehiclesState, kind: int, owner: int, x: float, z: float, yaw: float) -> Vehicle {
if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null } if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null }
let v = new Vehicle let v = new Vehicle
v.nid = VehicleWorld.next_id() v.nid = VehicleWorld.next_id()
v.kind = kind v.kind = kind
v.owner = owner
v.yaw = yaw v.yaw = yaw
if kind == VEHICLE_HORSE { if kind == VEHICLE_HORSE {
v.seat_h = 1.35 v.seat_h = 1.35
@ -14,7 +15,7 @@ function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw:
v.seat_h = 0.30 v.seat_h = 0.30
} }
ve_put(v, x, z) ve_put(v, x, z)
push(ve_all(vehicles_st), v) ve_file(vehicles_st.ve, v)
ve_settle(v) ve_settle(v)
VehicleWorld.made(v) VehicleWorld.made(v)
return v return v
@ -51,9 +52,8 @@ export function vehicle_call_for(vehicles_st: VehiclesState, pid: int, kind: int
var yaw = vehicles_st.ve_dock_yaw var yaw = vehicles_st.ve_dock_yaw
if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw } if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw }
if v == null { if v == null {
let made = ve_add(vehicles_st, kind, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw) let made = ve_add(vehicles_st, kind, pid, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw)
if made == null { return false } if made == null { return false }
made.owner = pid
if pid == 0 { ve_apply_kept(vehicles_st, made) } if pid == 0 { ve_apply_kept(vehicles_st, made) }
return true return true
} }
@ -68,10 +68,10 @@ export function vehicle_call_for(vehicles_st: VehiclesState, pid: int, kind: int
# a player left: their boat and horse go with them, and their places are free # a player left: their boat and horse go with them, and their places are free
export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void { export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void {
if pid == 0 { return } if pid == 0 { return }
let keep = new []Vehicle let rows = ix_rows(vehicles_st.ve.owners, pid)
let l = ve_all(vehicles_st) while len(rows) > 0 {
for i in 0 .. len(l) { let v = ve_all(vehicles_st)[rows[len(rows) - 1]]
if l[i].owner != pid { push(keep, l[i]) } else { ve_gone(l[i]) } ve_gone(v)
ve_unfile(vehicles_st.ve, v)
} }
vehicles_st.ve_list = keep
} }

View file

@ -5,6 +5,7 @@ module ludic_vehicles uses ludic_base
numbers float numbers float
import "ludic.base" import "ludic.base"
import "kinds.ludic" import "kinds.ludic"
import "table.ludic"
import "ports.ludic" import "ports.ludic"
import "state.ludic" import "state.ludic"
import "places.ludic" import "places.ludic"

View file

@ -19,6 +19,8 @@ export property Vehicle {
rider: int = -1 # the player on it, -1 nobody rider: int = -1 # the player on it, -1 nobody
home_x: float = 0.0 # a horse is tied where its home is home_x: float = 0.0 # a horse is tied where its home is
home_z: float = 0.0 home_z: float = 0.0
ent: int = -1 # its handle in the table
tab: Table<Vehicle> = null
} }
export const VEHICLE_MOUNTED: int = 0 export const VEHICLE_MOUNTED: int = 0

View file

@ -45,7 +45,7 @@ export state VehiclesState {
ve_ids: int = 0 ve_ids: int = 0
ve_sx: float = 0.0 ve_sx: float = 0.0
ve_sz: float = 0.0 ve_sz: float = 0.0
ve_list: []Vehicle = new []Vehicle ve: VehicleTable = ve__new()
ve_cur: Vehicle = null ve_cur: Vehicle = null
ve_dock_x: float = 0.0 ve_dock_x: float = 0.0
ve_dock_z: float = 0.0 ve_dock_z: float = 0.0

View file

@ -18,7 +18,7 @@ function ve_say(vehicles_st: VehiclesState, what: int, v: Vehicle, n: float) ->
q_push(vehicle_facts(vehicles_st), f) q_push(vehicle_facts(vehicles_st), f)
} }
function ve_all(vehicles_st: VehiclesState) -> []Vehicle { return vehicles_st.ve_list } function ve_all(vehicles_st: VehiclesState) -> []Vehicle { return vehicles_st.ve.tab.rec }
# every vehicle in the valley, to draw and to aim at (change one only through the verbs) # every vehicle in the valley, to draw and to aim at (change one only through the verbs)
export function vehicles_all(vehicles_st: VehiclesState) -> []Vehicle { return ve_all(vehicles_st) } export function vehicles_all(vehicles_st: VehiclesState) -> []Vehicle { return ve_all(vehicles_st) }
@ -32,8 +32,9 @@ export function vehicle_ride_kind(vehicles_st: VehiclesState) -> int {
export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur } export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur }
export function vehicle_of(vehicles_st: VehiclesState, pid: int, kind: int) -> Vehicle { export function vehicle_of(vehicles_st: VehiclesState, pid: int, kind: int) -> Vehicle {
let rows = ix_rows(vehicles_st.ve.owners, pid)
let l = ve_all(vehicles_st) let l = ve_all(vehicles_st)
for i in 0 .. len(l) { if l[i].kind == kind and l[i].owner == pid { return l[i] } } for k in 0 .. len(rows) { if l[rows[k]].kind == kind { return l[rows[k]] } }
return null return null
} }

View file

@ -2,13 +2,14 @@
# and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are # and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are
# read back and pushed by the wind in vehicles_tick, which the game calls where the world is run # read back and pushed by the wind in vehicles_tick, which the game calls where the world is run
export function vehicles_reset(vehicles_st: mut VehiclesState) -> void { export function vehicles_reset(vehicles_st: mut VehiclesState) -> void {
if vehicles_st.ve_list != null { let l = ve_all(vehicles_st)
for i in 0 .. len(vehicles_st.ve_list) { for i in 0 .. len(l) {
let v = vehicles_st.ve_list[i] let v = l[i]
if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) } if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) }
v.ent = -1
} }
} tb_clear(vehicles_st.ve.tab)
vehicles_st.ve_list = new []Vehicle imap_clear(vehicles_st.ve.nids)
vehicles_st.ve_cur = null vehicles_st.ve_cur = null
vehicles_st.ve_have_dock = false vehicles_st.ve_have_dock = false
vehicles_st.ve_swell_said = 0.0 vehicles_st.ve_swell_said = 0.0

View file

@ -0,0 +1,45 @@
# table.ludic - the vehicles, rows of a ludic.base Table: kind and owner as columns, each player's
# own by index, and a nid to its handle. Where one is stays on its record: a boat moves every frame
# and there are never more than two a player to search.
export const VE_KIND: int = 0 # int columns
export const VE_OWNER: int = 1
export property VehicleTable {
tab: Table<Vehicle> = null
owners: IntIndex = null # every vehicle by the player it belongs to
nids: IntMap = null
}
function ve__new() -> VehicleTable {
let w = new VehicleTable
let tb: Table<Vehicle> = table_new(0, 2)
w.tab = tb
w.owners = tb_index(tb, VE_OWNER, -1)
w.nids = imap_new()
return w
}
# a new row for v, its kind and owner from its record
function ve_file(w: VehicleTable, v: Vehicle) -> void {
v.tab = w.tab
v.ent = tb_add(w.tab, v)
let r = tb_row(w.tab, v.ent)
w.tab.i[VE_KIND][r] = v.kind
w.tab.i[VE_OWNER][r] = v.owner
tb_refile(w.tab, r)
imap_put(w.nids, v.nid, v.ent)
}
function ve_unfile(w: VehicleTable, v: Vehicle) -> void {
imap_del(w.nids, v.nid)
tb_remove(w.tab, v.ent)
v.ent = -1
}
# the vehicle a message names, or null
export function vehicle_by_nid(vehicles_st: VehiclesState, nid: int) -> Vehicle {
let tb = vehicles_st.ve.tab
let r = tb_row(tb, imap_get(vehicles_st.ve.nids, nid, -1))
if r < 0 or tb.rec[r].nid != nid { return null }
return tb.rec[r]
}

View file

@ -354,4 +354,23 @@ program VehiclesTest {
expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01) expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01)
expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned) expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned)
} }
test "the table: a nid finds its vehicle, each player's own by the owner index, and a release takes both" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
fresh(vehicles_st, vehicles_test_st)
vehicle_call_for(vehicles_st, 5, VEHICLE_HORSE, 0)
vehicle_call_for(vehicles_st, 5, VEHICLE_BOAT, 0)
vehicle_call_for(vehicles_st, 6, VEHICLE_BOAT, 1)
let b = vehicle_of(vehicles_st, 6, VEHICLE_BOAT)
expect(b != null)
expect(vehicle_by_nid(vehicles_st, b.nid) == b)
expect(vehicle_of(vehicles_st, 6, VEHICLE_HORSE) == null)
let h = vehicle_of(vehicles_st, 5, VEHICLE_HORSE)
vehicles_release(vehicles_st, 5)
expect_eq(len(vehicles_all(vehicles_st)), 1)
expect(vehicle_by_nid(vehicles_st, h.nid) == null)
expect(vehicle_of(vehicles_st, 6, VEHICLE_BOAT) == b)
vehicles_reset(vehicles_st)
expect_eq(len(vehicles_all(vehicles_st)), 0)
expect(vehicle_by_nid(vehicles_st, b.nid) == null)
}
} }

View file

@ -64,4 +64,6 @@ export property WildAnimal {
sus_z: float = 0.0 sus_z: float = 0.0
spook: float = 0.0 # game minutes of extra wariness after being flushed spook: float = 0.0 # game minutes of extra wariness after being flushed
seen: float = 0.0 # the alert it has already reacted to seen: float = 0.0 # the alert it has already reacted to
ent: int = -1 # its handle in the table
tab: Table<WildAnimal> = null # the table holding it, so a verb on the animal alone keeps the indexes
} }

View file

@ -3,13 +3,13 @@
export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void { export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void {
if wildlife_st.wl_own != null { return } if wildlife_st.wl_own != null { return }
wildlife_st.wl_own = wildlife_all(wildlife_st) wildlife_st.wl_own = wildlife_st.wl
wildlife_st.wl_list = new []WildAnimal wildlife_st.wl = wl__new()
} }
export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void { export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void {
if wildlife_st.wl_own == null { return } if wildlife_st.wl_own == null { return }
wildlife_st.wl_list = wildlife_st.wl_own wildlife_st.wl = wildlife_st.wl_own
wildlife_st.wl_own = null wildlife_st.wl_own = null
} }

View file

@ -8,7 +8,9 @@ import "animal.ludic"
import "places.ludic" import "places.ludic"
import "port.ludic" import "port.ludic"
import "facts.ludic" import "facts.ludic"
import "table.ludic"
import "store.ludic" import "store.ludic"
import "near.ludic"
import "guest.ludic" import "guest.ludic"
import "record.ludic" import "record.ludic"
import "habitat.ludic" import "habitat.ludic"

View file

@ -0,0 +1,15 @@
# near.ludic - the animals around a point, living ones only, into a list the CALLER keeps (in its own
# state, reused every frame): from the grid, or a species' own list when it is rare. Nothing is
# allocated or written here.
export function wildlife_near(wildlife_st: WildlifeState, x: float, z: float, r: float, out: []WildAnimal) -> int {
let w = wildlife_st.wl
return tb_within_recs(w.tab, w.grid, x, z, r, -1, 0, out)
}
export function wildlife_near_of(wildlife_st: WildlifeState, sp: int, x: float, z: float, r: float, out: []WildAnimal) -> int {
let w = wildlife_st.wl
return tb_within_of_recs(w.tab, w.grid, w.species, x, z, r, sp, out)
}
# how many of a species are alive, legends included, without a walk
export function wildlife_alive_of(wildlife_st: WildlifeState, sp: int) -> int { return ix_count(wildlife_st.wl.species, sp) }

View file

@ -4,6 +4,7 @@ export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: flo
wl_seed_scan(wildlife_st, dt) wl_seed_scan(wildlife_st, dt)
let all = wildlife_all(wildlife_st) let all = wildlife_all(wildlife_st)
for i in 0 .. len(all) { wl_tick_one(wildlife_st, all[i], dt, gh) } for i in 0 .. len(all) { wl_tick_one(wildlife_st, all[i], dt, gh) }
wildlife_refile(wildlife_st)
} }
function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void { function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void {

View file

@ -34,7 +34,7 @@ export property WildSpecies {
export state WildlifeState { export state WildlifeState {
wl_facts: Queue<WildFact> = wl_facts__new() wl_facts: Queue<WildFact> = wl_facts__new()
wl_own: []WildAnimal = null wl_own: WildTable = null # a guest's own animals, put away whole while it draws the host's
wl_px: float = 0.0 wl_px: float = 0.0
wl_pz: float = 0.0 wl_pz: float = 0.0
wl_ask: WildLure = new WildLure wl_ask: WildLure = new WildLure
@ -53,7 +53,7 @@ export state WildlifeState {
wl_fed_any: bool = false wl_fed_any: bool = false
wl_near_i: int = 0 wl_near_i: int = 0
wl_species: []WildSpecies = new []WildSpecies wl_species: []WildSpecies = new []WildSpecies
wl_list: []WildAnimal = new []WildAnimal wl: WildTable = wl__new()
wl_keys: int = 0 wl_keys: int = 0
wl_dice: Rng = rng_new(97) wl_dice: Rng = rng_new(97)
wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed

View file

@ -1,14 +1,20 @@
# store.ludic - the animals, their own dice, and the verbs that make and take one away # store.ludic - the animals, their own dice, and the verbs that make and take one away
const WL_TAU: float = 6.2831853 const WL_TAU: float = 6.2831853
# every animal, one per row - the table's own list: read it, never push to it
export function wildlife_all(wildlife_st: WildlifeState) -> []WildAnimal { export function wildlife_all(wildlife_st: WildlifeState) -> []WildAnimal {
return wildlife_st.wl_list return wildlife_st.wl.tab.rec
} }
export function wildlife_count_all(wildlife_st: WildlifeState) -> int { return len(wildlife_all(wildlife_st)) } export function wildlife_count_all(wildlife_st: WildlifeState) -> int { return tb_len(wildlife_st.wl.tab) }
# a new valley: every animal forgotten, quietly (the game has already let its drawings go) # a new valley: every animal forgotten, quietly (the game has already let its drawings go)
export function wildlife_clear(wildlife_st: mut WildlifeState) -> void { wildlife_st.wl_list = new []WildAnimal } export function wildlife_clear(wildlife_st: mut WildlifeState) -> void {
let tb = wildlife_st.wl.tab
for r in 0 .. tb_len(tb) { tb.rec[r].ent = -1 }
tb_clear(tb)
imap_clear(wildlife_st.wl.nids)
}
function wl_rng(wildlife_st: WildlifeState) -> Rng { function wl_rng(wildlife_st: WildlifeState) -> Rng {
return wildlife_st.wl_dice return wildlife_st.wl_dice
@ -40,7 +46,7 @@ export function wildlife_make(wildlife_st: mut WildlifeState, sp: int, x: float,
a.scale = scale a.scale = scale
a.legend = legend a.legend = legend
a.state = WILD_IDLE a.state = WILD_IDLE
push(wildlife_all(wildlife_st), a) wl_file(wildlife_st.wl, a)
WildlifeWorld.born(a) WildlifeWorld.born(a)
return a return a
} }
@ -67,7 +73,7 @@ export function wildlife_new(wildlife_st: mut WildlifeState, sp: int, x: float,
# gone for good: not drawn, not ticked # gone for good: not drawn, not ticked
export function wildlife_remove(a: WildAnimal) -> void { export function wildlife_remove(a: WildAnimal) -> void {
if a == null { return } if a == null { return }
a.alive = false wildlife_set_alive(a, false)
a.shown = false a.shown = false
} }
@ -75,15 +81,16 @@ export function wildlife_remove(a: WildAnimal) -> void {
export function wildlife_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on } export function wildlife_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on }
export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal { export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal {
let all = wildlife_all(wildlife_st) let tb = wildlife_st.wl.tab
for i in 0 .. len(all) { if all[i].nid == nid { return all[i] } } let r = tb_row(tb, imap_get(wildlife_st.wl.nids, nid, -1))
return null if r < 0 or tb.rec[r].nid != nid { return null }
return tb.rec[r]
} }
# how many of a species are alive, legends apart # how many of a species are alive, legends apart
export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int { export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int {
var n = 0 var n = 0
let all = wildlife_all(wildlife_st) let rows = ix_rows(wildlife_st.wl.species, sp)
for i in 0 .. len(all) { if all[i].alive and all[i].sp == sp and not all[i].legend { n += 1 } } for k in 0 .. len(rows) { if not wildlife_st.wl.tab.rec[rows[k]].legend { n += 1 } }
return n return n
} }

View file

@ -0,0 +1,91 @@
# table.ludic - the animals, rows of a ludic.base Table: where each stands, its species and whether
# it lives are columns, with a grid over the living, the living by species, and a nid to its handle.
# The AI moves an animal by its record; wildlife_refile files every move once, after the tick.
export const WA_X: int = 0 # float columns
export const WA_Z: int = 1
export const WA_SP: int = 0 # int columns
export const WA_ALIVE: int = 1 # 1 while alive
const WA_CELL: float = 32.0
export property WildTable {
tab: Table<WildAnimal> = null
grid: Grid = null
species: IntIndex = null # the living, by species
nids: IntMap = null
}
function wl__new() -> WildTable {
let w = new WildTable
let tb: Table<WildAnimal> = table_new(2, 2)
w.tab = tb
w.grid = tb_grid(tb, WA_X, WA_Z, WA_ALIVE, WA_CELL)
w.species = tb_index(tb, WA_SP, WA_ALIVE)
w.nids = imap_new()
return w
}
function wl_row(a: WildAnimal) -> int {
if a == null or a.tab == null { return -1 }
return tb_row(a.tab, a.ent)
}
# a new row for `a`, its columns from its record
function wl_file(w: WildTable, a: WildAnimal) -> void {
a.tab = w.tab
a.ent = tb_add(w.tab, a)
let r = wl_row(a)
let tb = w.tab
tb.f[WA_X][r] = a.x
tb.f[WA_Z][r] = a.z
tb.i[WA_SP][r] = a.sp
var on = 0
if a.alive { on = 1 }
tb.i[WA_ALIVE][r] = on
tb_refile(tb, r)
if a.nid != 0 { imap_put(w.nids, a.nid, a.ent) }
}
# alive or not, the record and the row together (a copy the game is handed says so too)
export function wildlife_set_alive(a: WildAnimal, on: bool) -> void {
if a == null { return }
a.alive = on
let r = wl_row(a)
if r < 0 { return }
var v = 0
if on { v = 1 }
tb_set_i(a.tab, WA_ALIVE, r, v)
}
# every row whose record moved since it was filed, refiled: once after a tick that moves them all,
# and by the game after it moves the copies a guest is handed
export function wildlife_refile(wildlife_st: mut WildlifeState) -> void {
let tb = wildlife_st.wl.tab
let xs = tb.f[WA_X]
let zs = tb.f[WA_Z]
for r in 0 .. tb_len(tb) {
let a = tb.rec[r]
if xs[r] != a.x or zs[r] != a.z { tb_set_xz(tb, WA_X, WA_Z, r, a.x, a.z) }
}
}
# after a refile, the columns say what the records say; the count of rows that do not
export function wildlife_verify(wildlife_st: WildlifeState) -> int {
let tb = wildlife_st.wl.tab
var bad = 0
for r in 0 .. tb_len(tb) {
let a = tb.rec[r]
var on = 0
if a.alive { on = 1 }
if tb.f[WA_X][r] != a.x or tb.f[WA_Z][r] != a.z or tb.i[WA_SP][r] != a.sp or tb.i[WA_ALIVE][r] != on or a.ent != tb.ent[r] { bad += 1 }
}
return bad
}
# the nearest living animal of a species (below 0: any) within maxr (0: any distance), or null
export function wildlife_nearest(wildlife_st: WildlifeState, sp: int, x: float, z: float, maxr: float) -> WildAnimal {
let w = wildlife_st.wl
var r = -1
if sp < 0 { r = tb_nearest(w.tab, w.grid, x, z, maxr, -1, 0) } else { r = tb_nearest_of(w.tab, w.grid, w.species, x, z, maxr, sp) }
if r < 0 { return null }
return w.tab.rec[r]
}

View file

@ -230,4 +230,33 @@ program WildlifeTest {
expect_near(r.x, r.wx, 0.001) expect_near(r.x, r.wx, 0.001)
expect(r.x != x0 or r.wx == r.sx) expect(r.x != x0 or r.wx == r.sx)
} }
test "the table: a nid finds its animal, the living count by species, the nearest, and a refile" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) {
fk_setup(wildlife_st, wildlife_tests_fake_st)
wildlife_clear(wildlife_st)
let a = wildlife_make(wildlife_st, 0, 10.0, 0.0, false, 0.0, 0, 1.0, 41)
let b = wildlife_make(wildlife_st, 0, 90.0, 0.0, false, 0.0, 0, 1.0, 42)
let c = wildlife_make(wildlife_st, 1, 30.0, 0.0, true, 0.0, 0, 1.0, 43)
expect(wildlife_by_nid(wildlife_st, 42) == b)
expect(wildlife_by_nid(wildlife_st, 7) == null)
expect_eq(wildlife_count(wildlife_st, 0), 2)
expect_eq(wildlife_count(wildlife_st, 1), 0)
expect(wildlife_nearest(wildlife_st, -1, 0.0, 0.0, 0.0) == a)
expect(wildlife_nearest(wildlife_st, 1, 0.0, 0.0, 0.0) == c)
let near = new []WildAnimal
expect_eq(wildlife_near(wildlife_st, 0.0, 0.0, 40.0, near), 2)
expect_eq(wildlife_near_of(wildlife_st, 0, 0.0, 0.0, 200.0, near), 2)
expect_eq(wildlife_alive_of(wildlife_st, 1), 1)
wildlife_remove(a)
expect_eq(wildlife_count(wildlife_st, 0), 1)
expect(wildlife_nearest(wildlife_st, 0, 0.0, 0.0, 0.0) == b)
b.x = -5.0
wildlife_refile(wildlife_st)
expect(wildlife_nearest(wildlife_st, -1, 0.0, 0.0, 20.0) == b)
expect_eq(wildlife_verify(wildlife_st), 0)
expect_eq(wildlife_count_all(wildlife_st), 3)
wildlife_clear(wildlife_st)
expect_eq(wildlife_count_all(wildlife_st), 0)
expect(wildlife_by_nid(wildlife_st, 42) == null)
}
} }

View file

@ -25,6 +25,9 @@ import "vk_api.ludic"
# Vk.sl_active() says whether it was. Each Vk.sl_* is the sl* export of the same name and returns # Vk.sl_active() says whether it was. Each Vk.sl_* is the sl* export of the same name and returns
# its sl::Result (0 is eOk); -1 means there is no Streamline (macOS, or the DLL missing). The # its sl::Result (0 is eOk); -1 means there is no Streamline (macOS, or the DLL missing). The
# structs are plain memory, laid out as the SDK headers (include/sl_*.h, SDK 2.14.1) declare them. # structs are plain memory, laid out as the SDK headers (include/sl_*.h, SDK 2.14.1) declare them.
# Once a frame: on macOS drains what MoltenVK and the layer autoreleased during the last one (a
# program pumping its own events has no other pool); nothing elsewhere.
extern function vk_frame_pool() = "lvk_frame_pool"
extern function vk_sl_prefer(on: int) = "lvk_sl_prefer" extern function vk_sl_prefer(on: int) = "lvk_sl_prefer"
extern function vk_sl_active() -> int = "lvk_sl_active" extern function vk_sl_active() -> int = "lvk_sl_active"
extern function vk_sl_init(pref: pointer, sdk_version: long) -> int = "lsl_slInit" extern function vk_sl_init(pref: pointer, sdk_version: long) -> int = "lsl_slInit"

View file

@ -225,3 +225,26 @@ entry:
%r = call i64 %fn(ptr %a0, ptr %a1) %r = call i64 %fn(ptr %a0, ptr %a1)
ret i64 %r ret i64 %r
} }
; ---- the frame's autorelease pool --------------------------------------------------------------
; MoltenVK and CAMetalLayer autorelease objects on every frame (the drawable, each render pass's
; descriptor), and a program that pumps its own events never drains a pool: without this they are
; kept for the life of the process - 160 MB/s of small allocations in a windowed valley.
; lvk_frame_pool() pops the pool the last frame pushed and pushes the next one.
declare ptr @objc_autoreleasePoolPush()
declare void @objc_autoreleasePoolPop(ptr)
@lvk_pool = internal global ptr null
define void @lvk_frame_pool() {
entry:
%old = load ptr, ptr @lvk_pool
%have = icmp ne ptr %old, null
br i1 %have, label %pop, label %push
pop:
call void @objc_autoreleasePoolPop(ptr %old)
br label %push
push:
%p = call ptr @objc_autoreleasePoolPush()
store ptr %p, ptr @lvk_pool
ret void
}

View file

@ -363,3 +363,9 @@ entry:
%r = call i64 %fn(ptr %a0, ptr %a1) %r = call i64 %fn(ptr %a0, ptr %a1)
ret i64 %r ret i64 %r
} }
; the frame's autorelease pool is a macOS matter (vk_mac.ll); Windows has nothing to drain
define void @lvk_frame_pool() {
entry:
ret void
}

View file

@ -136,9 +136,11 @@ function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
let lt = emit_bind(`icmp slt i32 {i0}, {ec}`) let lt = emit_bind(`icmp slt i32 {i0}, {ec}`)
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n") emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
emit(body); emit(":\n") emit(body); emit(":\n")
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {i0}`) let eb1 = ecs_base("L_alive")
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb1}, i32 {i0}`)
let al = emit_bind(`load i32, ptr {ap}`) let al = emit_bind(`load i32, ptr {ap}`)
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, i32 {i0}`) let eb2 = ecs_base(`H_{prop}`)
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb2}, i32 {i0}`)
let hv = emit_bind(`load i8, ptr {hp}`) let hv = emit_bind(`load i8, ptr {hp}`)
let hz = emit_bind(`zext i8 {hv} to i32`) let hz = emit_bind(`zext i8 {hv} to i32`)
let both = emit_bind(`and i32 {al}, {hz}`) let both = emit_bind(`and i32 {al}, {hz}`)
@ -154,15 +156,19 @@ function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) } if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) }
let ev = emit_expr(e.kids[0]) let ev = emit_expr(e.kids[0])
if (meth == "of") { if (meth == "of") {
let slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_{prop}], ptr @S_{prop}, i32 0, i32 {ev.code}`) let eb3 = ecs_base(`S_{prop}`)
let slot = emit_bind(`getelementptr inbounds %Cmp_{prop}, ptr {eb3}, i32 {ev.code}`)
return val(slot, prop) return val(slot, prop)
} }
let in_range = emit_bind(`icmp ult i32 {ev.code}, {me}`) let cap = emit_bind("load i32, ptr @L_cap")
let in_range = emit_bind(`icmp ult i32 {ev.code}, {cap}`)
let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`) let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`)
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {idx}`) let eb4 = ecs_base("L_alive")
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb4}, i32 {idx}`)
let alive = emit_bind(`load i32, ptr {ap}`) let alive = emit_bind(`load i32, ptr {ap}`)
let is_alive = emit_bind(`icmp ne i32 {alive}, 0`) let is_alive = emit_bind(`icmp ne i32 {alive}, 0`)
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, i32 {idx}`) let eb5 = ecs_base(`H_{prop}`)
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb5}, i32 {idx}`)
let hv = emit_bind(`load i8, ptr {hp}`) let hv = emit_bind(`load i8, ptr {hp}`)
let carries = emit_bind(`icmp ne i8 {hv}, 0`) let carries = emit_bind(`icmp ne i8 {hv}, 0`)
let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`) let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`)
@ -372,7 +378,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
return emit_alias_or_fail("App", meth, e) return emit_alias_or_fail("App", meth, e)
} }
if (ns == "Pool") { if (ns == "Pool") {
if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities if (meth == "capacity") { return val(emit_bind("load i32, ptr @L_cap"), "int") } # the stores' size now: they grow
if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water) if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water)
if (meth == "free") { return val(emit_bind("load i32, ptr @L_freen"), "int") } # recycled slots ready for reuse if (meth == "free") { return val(emit_bind("load i32, ptr @L_freen"), "int") } # recycled slots ready for reuse
if (meth == "live") { # currently alive = reserved - free if (meth == "live") { # currently alive = reserved - free
@ -407,7 +413,8 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
let at = emit_expr(e.kids[1]) let at = emit_expr(e.kids[1])
let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`) let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_Position], ptr @S_Position, i32 0, i32 {ent}`) let eb6 = ecs_base(`S_Position`)
let slot = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {eb6}, i32 {ent}`)
let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`) let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`)
let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`) let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`)
let ax = vec_x(at.code) let ax = vec_x(at.code)

View file

@ -61,6 +61,7 @@ function emit_main(d: Node) -> void {
# its framebuffer allocated. Headless it only allocates — no window, no output — # its framebuffer allocated. Headless it only allocates — no window, no output —
# so a non-rendering entry game is unchanged. # so a non-rendering entry game is unchanged.
emit(" call void @L_init_runtime()\n") emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") } if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") }
emit(" call void @L_init_globals()\n") emit(" call void @L_init_globals()\n")
emit_block(d.a) emit_block(d.a)
@ -149,6 +150,7 @@ function emit_test_runner() -> void {
emit(" ret i32 0\n") emit(" ret i32 0\n")
emit(`{lrun0}:\n`) emit(`{lrun0}:\n`)
emit(" call void @L_init_runtime()\n") emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if has_ecs() { emit(" call void @rt_init()\n") } if has_ecs() { emit(" call void @rt_init()\n") }
emit(" call void @L_init_globals()\n") emit(" call void @L_init_globals()\n")
i = 0 i = 0

View file

@ -185,4 +185,11 @@ function check_worker_ref(e: Node) -> void {
var ok = len(d.kids) == s + 2 and llty(d.ty) == "void" var ok = len(d.kids) == s + 2 and llty(d.ty) == "void"
if ok { ok = llty(d.kids[s].ty) == "i32" and llty(d.kids[s + 1].ty) == "ptr" } if ok { ok = llty(d.kids[s].ty) == "i32" and llty(d.kids[s + 1].ty) == "ptr" }
if not ok { perr(`fn {e.s}: a worker function takes (i: int, ctx: pointer) and returns nothing`) } if not ok { perr(`fn {e.s}: a worker function takes (i: int, ctx: pointer) and returns nothing`) }
# every thread runs it at once: a state it is handed is read by all of them, so none may change one
var k = 0
while k < s {
let p = d.kids[k]
if p.uns == 1 { perr(`fn {e.s}: a worker runs on every core at once, so it may read {p.ty} but not change it ({p.s}: mut {p.ty}); write results into ctx, shared counts through Sync.add`) }
k += 1
}
} }

View file

@ -56,9 +56,12 @@ function emit_ecs_storage() -> void {
if len(g_scenes) > 0 { emith("@L_scene = internal global i32 0\n") } # active base scene id if len(g_scenes) > 0 { emith("@L_scene = internal global i32 0\n") } # active base scene id
emith("@L_entc = internal global i32 0\n") emith("@L_entc = internal global i32 0\n")
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emith(`@L_alive = internal global [{me} x i32] zeroinitializer\n`) # the per-entity stores are heap blocks L_grow makes and doubles: @L_cap slots each, from MAX_ENT
emith(`@L_kind = internal global [{me} x i32] zeroinitializer\n`) emith("@L_cap = internal global i32 0\n")
emith(`@L_freelist = internal global [{me} x i32] zeroinitializer\n`) emith("@L_snapcap = internal global i32 0\n") # a snapshot's slot count, read before its stores
emith("@L_alive = internal global ptr null\n")
emith("@L_kind = internal global ptr null\n")
emith("@L_freelist = internal global ptr null\n")
emith("@L_freen = internal global i32 0\n") emith("@L_freen = internal global i32 0\n")
# NETWORKING role registers (N3/N5): a runtime sets these; offline they hold the # NETWORKING role registers (N3/N5): a runtime sets these; offline they hold the
# single-player default — @L_role=1 (this peer is the authority), local id 0. # single-player default — @L_role=1 (this peer is the authority), local id 0.
@ -68,7 +71,7 @@ function emit_ecs_storage() -> void {
emith("@L_role = internal global i32 1\n") # 1 = server/authority (offline default) emith("@L_role = internal global i32 1\n") # 1 = server/authority (offline default)
emith("@L_localid = internal global i32 0\n") # this peer's id emith("@L_localid = internal global i32 0\n") # this peer's id
} }
if net_has_owned() { emith(`@L_owner_arr = internal global [{me} x i32] zeroinitializer\n`) } if net_has_owned() { emith("@L_owner_arr = internal global ptr null\n") }
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let c = prog[i] let c = prog[i]
@ -76,8 +79,8 @@ function emit_ecs_storage() -> void {
# header). Every property in an ECS program is a component today; a property # header). Every property in an ECS program is a component today; a property
# used only via `new` would not need these, but no such program mixes the two. # used only via `new` would not need these, but no such program mixes the two.
if c.kind == N_COMP { if c.kind == N_COMP {
emith(`@S_{c.s} = internal global [{me} x %Cmp_{c.s}] zeroinitializer\n`) emith(`@S_{c.s} = internal global ptr null\n`)
emith(`@H_{c.s} = internal global [{me} x i8] zeroinitializer\n`) emith(`@H_{c.s} = internal global ptr null\n`)
} }
# one enabled-flag global per model and per handler (default enabled) # one enabled-flag global per model and per handler (default enabled)
if c.kind == N_ARCH { emith(`@ME_{c.s} = internal global i32 1\n`) } if c.kind == N_ARCH { emith(`@ME_{c.s} = internal global i32 1\n`) }
@ -92,21 +95,25 @@ function emit_ecs_storage() -> void {
# L_reset(e): clear every has-flag and the archetype kind for entity e # L_reset(e): clear every has-flag and the archetype kind for entity e
function emit_ecs_allocator() -> void { function emit_ecs_allocator() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit_ecs_grow()
emit("define void @L_reset(i32 %e) {\nentry:\n") emit("define void @L_reset(i32 %e) {\nentry:\n")
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_COMP { if prog[i].kind == N_COMP {
let hn = `%h{itoa(i)}` let hn = `%h{itoa(i)}`
emit(" "); emit(hn); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(prog[i].s); emit(", i32 0, i32 %e\n") let eb1 = ecs_base(`H_{prog[i].s}`)
emit(" "); emit(hn); emit(" = getelementptr inbounds i8, ptr "); emit(eb1); emit(", i32 %e\n")
emit(" store i8 0, ptr "); emit(hn); emit("\n") emit(" store i8 0, ptr "); emit(hn); emit("\n")
} }
i += 1 i += 1
} }
emit(" %k = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") let eb2 = ecs_base("L_kind")
emit(" %k = getelementptr inbounds i32, ptr "); emit(eb2); emit(", i32 %e\n")
emit(" store i32 0, ptr %k\n") emit(" store i32 0, ptr %k\n")
# N3: reset an @Owned entity's network owner to -1 (unowned) on alloc/free # N3: reset an @Owned entity's network owner to -1 (unowned) on alloc/free
if net_has_owned() { if net_has_owned() {
emit(" %ow = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n") let eb3 = ecs_base("L_owner_arr")
emit(" %ow = getelementptr inbounds i32, ptr "); emit(eb3); emit(", i32 %e\n")
emit(" store i32 -1, ptr %ow\n") emit(" store i32 -1, ptr %ow\n")
} }
emit(" ret void\n}\n\n") emit(" ret void\n}\n\n")
@ -118,29 +125,100 @@ function emit_ecs_allocator() -> void {
emit("reuse:\n") emit("reuse:\n")
emit(" %fn1 = sub i32 %fn, 1\n") emit(" %fn1 = sub i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n") emit(" store i32 %fn1, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n") let eb4 = ecs_base("L_freelist")
emit(" %fp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %fn1\n")
emit(" %re = load i32, ptr %fp\n") emit(" %re = load i32, ptr %fp\n")
emit(" br label %done\n") emit(" br label %done\n")
emit("fresh:\n") emit("fresh:\n")
emit(" %ec = load i32, ptr @L_entc\n") emit(" %ec = load i32, ptr @L_entc\n")
emit(" %ec1 = add i32 %ec, 1\n") emit(" %ec1 = add i32 %ec, 1\n")
emit(" call void @L_grow(i32 %ec1)\n") # a slot past the stores doubles them first
emit(" store i32 %ec1, ptr @L_entc\n") emit(" store i32 %ec1, ptr @L_entc\n")
emit(" br label %done\n") emit(" br label %done\n")
emit("done:\n") emit("done:\n")
emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n") emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n")
emit(" call void @L_reset(i32 %e)\n") emit(" call void @L_reset(i32 %e)\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %e\n")
emit(" store i32 1, ptr %ap\n") emit(" store i32 1, ptr %ap\n")
emit(" ret i32 %e\n}\n\n") emit(" ret i32 %e\n}\n\n")
emit("define void @L_free_entity(i32 %e) {\nentry:\n") emit("define void @L_free_entity(i32 %e) {\nentry:\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") let eb6 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb6); emit(", i32 %e\n")
emit(" store i32 0, ptr %ap\n") emit(" store i32 0, ptr %ap\n")
emit(" call void @L_reset(i32 %e)\n") emit(" call void @L_reset(i32 %e)\n")
emit(" %fn = load i32, ptr @L_freen\n") emit(" %fn = load i32, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n") let eb7 = ecs_base("L_freelist")
emit(" %fp = getelementptr inbounds i32, ptr "); emit(eb7); emit(", i32 %fn\n")
emit(" store i32 %e, ptr %fp\n") emit(" store i32 %e, ptr %fp\n")
emit(" %fn1 = add i32 %fn, 1\n") emit(" %fn1 = add i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n") emit(" store i32 %fn1, ptr @L_freen\n")
emit(" ret void\n}\n\n") emit(" ret void\n}\n\n")
} }
# the base of a per-entity store, loaded where it is indexed (a store moves when L_grow doubles it)
function ecs_base(store: pointer) -> pointer {
let r = `%ecsb{itoa(ll_t)}` # its own prefix: raw functions name blocks t0, t1, ...
ll_t += 1
emit(` {r} = load ptr, ptr @{store}\n`)
return r
}
# L_grow(need): every per-entity store holds at least `need` slots - doubled from MAX_ENT as the
# entities outgrow them, the new slots zero, the mods' registered stores with them. Nothing is
# ever moved back down: an entity's slot is its handle for its whole life.
function emit_ecs_grow() -> void {
emit("define void @L_grow(i32 %need) {\nentry:\n %cap = load i32, ptr @L_cap\n")
emit(" %ok = icmp sle i32 %need, %cap\n br i1 %ok, label %done, label %grow\ngrow:\n")
emit(` %c0 = icmp eq i32 %cap, 0\n %start = select i1 %c0, i32 {itoa(MAX_ENT)}, i32 %cap\n br label %dbl\n`)
emit("dbl:\n %nc = phi i32 [ %start, %grow ], [ %nc2, %dbl2 ]\n %enough = icmp sge i32 %nc, %need\n")
emit(" br i1 %enough, label %apply, label %dbl2\ndbl2:\n %nc2 = mul i32 %nc, 2\n br label %dbl\n")
emit("apply:\n %cap64 = sext i32 %cap to i64\n %nc64 = sext i32 %nc to i64\n")
emit_grow_store("L_alive", "4", 0)
emit_grow_store("L_kind", "4", 1)
emit_grow_store("L_freelist", "4", 2)
if net_has_owned() { emit_grow_store("L_owner_arr", "4", 3) }
var k = 4
var i = 0
while i < len(prog) {
if prog[i].kind == N_COMP {
let sz = `%gs{itoa(k)}`
emit(` {sz} = ptrtoint ptr getelementptr (%Cmp_{prog[i].s}, ptr null, i32 1) to i64\n`)
emit_grow_store(`S_{prog[i].s}`, sz, k)
emit_grow_store(`H_{prog[i].s}`, "1", k + 1)
k += 2
}
i += 1
}
emit_grow_dyn()
emit("fin:\n store i32 %nc, ptr @L_cap\n br label %done\ndone:\n ret void\n}\n\n")
}
# one store to nc slots of `esz` bytes: reallocated (8 bytes spare, so a field-less tag is never
# a zero-byte block), its new tail zeroed
function emit_grow_store(store: pointer, esz: pointer, k: int) -> void {
let n = itoa(k)
emit(` %gp{n} = load ptr, ptr @{store}\n %gb{n} = mul i64 %nc64, {esz}\n %gr{n} = add i64 %gb{n}, 8\n`)
emit(` %gn{n} = call ptr @realloc(ptr %gp{n}, i64 %gr{n})\n %go{n} = mul i64 %cap64, {esz}\n`)
emit(` %gt{n} = getelementptr i8, ptr %gn{n}, i64 %go{n}\n %gz{n} = sub i64 %gr{n}, %go{n}\n`)
emit(` %gm{n} = call ptr @memset(ptr %gt{n}, i32 0, i64 %gz{n})\n store ptr %gn{n}, ptr @{store}\n`)
}
# the stores a mod registered at run time (ludic_register_prop), grown the same way
function emit_grow_dyn() -> void {
emit(" br label %dynh\ndynh:\n %di = phi i32 [ 0, %apply ], [ %di1, %dynb ]\n %dcn = load i32, ptr @dyn_count\n")
emit(" %dmore = icmp slt i32 %di, %dcn\n br i1 %dmore, label %dynb, label %fin\ndynb:\n")
emit(" %dfp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %di\n %dnf = load i32, ptr %dfp\n")
emit(" %dnf4 = mul i32 %dnf, 4\n %desz = sext i32 %dnf4 to i64\n")
emit_grow_slot("dynS", "%desz", "s")
emit_grow_slot("dynH", "1", "h")
emit(" %di1 = add i32 %di, 1\n br label %dynh\n")
}
function emit_grow_slot(table: pointer, esz: pointer, t: pointer) -> void {
emit(` %d{t}p = getelementptr inbounds [32 x ptr], ptr @{table}, i32 0, i32 %di\n %d{t}o = load ptr, ptr %d{t}p\n`)
emit(` %d{t}b = mul i64 %nc64, {esz}\n %d{t}r = add i64 %d{t}b, 8\n %d{t}n = call ptr @realloc(ptr %d{t}o, i64 %d{t}r)\n`)
emit(` %d{t}f = mul i64 %cap64, {esz}\n %d{t}t = getelementptr i8, ptr %d{t}n, i64 %d{t}f\n %d{t}z = sub i64 %d{t}r, %d{t}f\n`)
emit(` %d{t}m = call ptr @memset(ptr %d{t}t, i32 0, i64 %d{t}z)\n store ptr %d{t}n, ptr %d{t}p\n`)
}

View file

@ -93,7 +93,8 @@ function emit_countdown_system() -> void {
emit(" %n = load i32, ptr @L_entc\n br label %loop\nloop:\n") emit(" %n = load i32, ptr @L_entc\n br label %loop\nloop:\n")
emit(" %i = phi i32 [ 0, %entry ], [ %i1, %next ]\n") emit(" %i = phi i32 [ 0, %entry ], [ %i1, %next ]\n")
emit(" %go = icmp slt i32 %i, %n\n br i1 %go, label %body, label %done\nbody:\n") emit(" %go = icmp slt i32 %i, %n\n br i1 %go, label %body, label %done\nbody:\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n") let eb1 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb1); emit(", i32 %i\n")
emit(" %al = load i32, ptr %ap\n %alive = icmp ne i32 %al, 0\n br i1 %alive, label %c0, label %next\n") emit(" %al = load i32, ptr %ap\n %alive = icmp ne i32 %al, 0\n br i1 %alive, label %c0, label %next\n")
var k = 0 var k = 0
var i = 0 var i = 0
@ -106,12 +107,14 @@ function emit_countdown_system() -> void {
if any { if any {
let sk = itoa(k); let nk = itoa(k + 1) let sk = itoa(k); let nk = itoa(k + 1)
emit("c"); emit(sk); emit(":\n") emit("c"); emit(sk); emit(":\n")
emit(" %h"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(d.s); emit(", i32 0, i32 %i\n") let eb2 = ecs_base(`H_{d.s}`)
emit(" %h"); emit(sk); emit(" = getelementptr inbounds i8, ptr "); emit(eb2); emit(", i32 %i\n")
emit(" %hv"); emit(sk); emit(" = load i8, ptr %h"); emit(sk); emit("\n") emit(" %hv"); emit(sk); emit(" = load i8, ptr %h"); emit(sk); emit("\n")
emit(" %has"); emit(sk); emit(" = icmp ne i8 %hv"); emit(sk); emit(", 0\n") emit(" %has"); emit(sk); emit(" = icmp ne i8 %hv"); emit(sk); emit(", 0\n")
emit(" br i1 %has"); emit(sk); emit(", label %t"); emit(sk); emit(", label %c"); emit(nk); emit("\n") emit(" br i1 %has"); emit(sk); emit(", label %t"); emit(sk); emit(", label %c"); emit(nk); emit("\n")
emit("t"); emit(sk); emit(":\n") emit("t"); emit(sk); emit(":\n")
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(d.s); emit("], ptr @S_"); emit(d.s); emit(", i32 0, i32 %i\n") let eb3 = ecs_base(`S_{d.s}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(d.s); emit(", ptr "); emit(eb3); emit(", i32 %i\n")
f = 0 f = 0
while f < len(d.kids) { while f < len(d.kids) {
if (d.kids[f].ty == "countdown") { if (d.kids[f].ty == "countdown") {
@ -474,7 +477,8 @@ function emit_world_despawn_fn() -> void {
emit("define void @fn_world_despawn(i32 %e) {\nentry:\n") emit("define void @fn_world_despawn(i32 %e) {\nentry:\n")
if len(g_ondespawn) > 0 { if len(g_ondespawn) > 0 {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") let eb4 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %e\n")
emit(" %k = load i32, ptr %kp\n") emit(" %k = load i32, ptr %kp\n")
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
@ -508,10 +512,14 @@ function emit_despawn_all_fn() -> void {
emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n") emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n") emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n")
emit(" br i1 %go, label %body, label %fin\n") emit(" br i1 %go, label %body, label %fin\n")
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n") emit("body:\n")
let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %i\n")
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n") emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n")
emit(" br i1 %isa, label %do, label %cont\n") emit(" br i1 %isa, label %do, label %cont\n")
emit("do:\n %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %i\n") emit("do:\n")
let eb6 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb6); emit(", i32 %i\n")
emit(" %k = load i32, ptr %kp\n") emit(" %k = load i32, ptr %kp\n")
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
@ -608,7 +616,8 @@ function emit_event_fns() -> void {
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n") emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %lo = icmp sge i32 %tok, 0\n %hi = icmp slt i32 %tok, %n\n %ok = and i1 %lo, %hi\n") emit(" %lo = icmp sge i32 %tok, 0\n %hi = icmp slt i32 %tok, %n\n %ok = and i1 %lo, %hi\n")
emit(" br i1 %ok, label %do, label %skip\n") emit(" br i1 %ok, label %do, label %skip\n")
emit("do:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %tok\n") emit("do:\n")
emit(" %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %tok\n")
emit(" store ptr null, ptr %slot\n br label %skip\n") emit(" store ptr null, ptr %slot\n br label %skip\n")
emit("skip:\n ret void\n}\n\n") emit("skip:\n ret void\n}\n\n")

View file

@ -25,7 +25,8 @@ function emit_query(st: Node) -> void {
emit(body); emit(":\n"); g_term = false emit(body); emit(":\n"); g_term = false
let i1 = emit_bind(`load i32, ptr {ip}`) let i1 = emit_bind(`load i32, ptr {ip}`)
# alive? # alive?
let ap = nreg(); emit(" "); emit(ap); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 "); emit(i1); emit("\n") let eb1 = ecs_base("L_alive")
let ap = nreg(); emit(" "); emit(ap); emit(" = getelementptr inbounds i32, ptr "); emit(eb1); emit(", i32 "); emit(i1); emit("\n")
let al = emit_bind(`load i32, ptr {ap}`) let al = emit_bind(`load i32, ptr {ap}`)
let alc = emit_bind(`icmp ne i32 {al}, 0`) let alc = emit_bind(`icmp ne i32 {al}, 0`)
let ka = lbl("qa") let ka = lbl("qa")
@ -40,14 +41,16 @@ function emit_query(st: Node) -> void {
let ak = find_arch_id(tm.s) let ak = find_arch_id(tm.s)
var ok: pointer = "0" var ok: pointer = "0"
if ak > 0 { if ak > 0 {
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n") let eb2 = ecs_base("L_kind")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds i32, ptr "); emit(eb2); emit(", i32 "); emit(i1); emit("\n")
let kv = emit_bind(`load i32, ptr {kp}`) let kv = emit_bind(`load i32, ptr {kp}`)
let kok = emit_bind(`icmp eq i32 {kv}, {itoa(ak)}`) let kok = emit_bind(`icmp eq i32 {kv}, {itoa(ak)}`)
let mev = emit_bind(`load i32, ptr @ME_{tm.s}`) # model-enabled flag let mev = emit_bind(`load i32, ptr @ME_{tm.s}`) # model-enabled flag
let meok = emit_bind(`icmp ne i32 {mev}, 0`) let meok = emit_bind(`icmp ne i32 {mev}, 0`)
ok = emit_bind(`and i1 {kok}, {meok}`) ok = emit_bind(`and i1 {kok}, {meok}`)
} else { } else {
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n") let eb3 = ecs_base(`H_{tm.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb3); emit(", i32 "); emit(i1); emit("\n")
let hv = emit_bind(`load i8, ptr {hp}`) let hv = emit_bind(`load i8, ptr {hp}`)
ok = emit_bind(`icmp ne i8 {hv}, 0`) ok = emit_bind(`icmp ne i8 {hv}, 0`)
} }
@ -66,7 +69,8 @@ function emit_query(st: Node) -> void {
if tm.ival == 0 and find_arch_id(tm.s) == 0 { if tm.ival == 0 and find_arch_id(tm.s) == 0 {
if vi < len(st.kids) { if vi < len(st.kids) {
let slot = nreg() let slot = nreg()
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(tm.s); emit("], ptr @S_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n") let eb4 = ecs_base(`S_{tm.s}`)
emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(tm.s); emit(", ptr "); emit(eb4); emit(", i32 "); emit(i1); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.kids[vi].s, vslot, tm.s) loc_push(st.kids[vi].s, vslot, tm.s)

View file

@ -46,16 +46,23 @@ function emit_io(fn2: pointer, p: pointer, bytes: pointer) -> void {
function emit_snapshot_blocks(fn2: pointer) -> void { function emit_snapshot_blocks(fn2: pointer) -> void {
g_iok = 0 g_iok = 0
g_off = "0" g_off = "0"
let me = itoa(MAX_ENT) # the stores grow, so a snapshot says how many slots it holds first; a load grows to it before
# reading them back
let reading = (fn2 == "fread") or (g_snap_mode == "load")
if not reading { emit(" %sncap0 = load i32, ptr @L_cap\n store i32 %sncap0, ptr @L_snapcap\n") }
emit_io(fn2, "@L_snapcap", "4")
emit(" %sncap = load i32, ptr @L_snapcap\n")
if reading { emit(" call void @L_grow(i32 %sncap)\n") }
emit(" %sncap64 = sext i32 %sncap to i64\n")
emit_io(fn2, "@L_entc", "4") emit_io(fn2, "@L_entc", "4")
emit_io(fn2, "@L_freen", "4") emit_io(fn2, "@L_freen", "4")
emit(" %nalive = mul i64 "); emit(me); emit(", 4\n") emit(" %nalive = mul i64 %sncap64, 4\n")
emit_io(fn2, "@L_alive", "%nalive") emit_io(fn2, snap_base("L_alive", 0), "%nalive")
emit_io(fn2, "@L_freelist", "%nalive") emit_io(fn2, snap_base("L_freelist", 1), "%nalive")
emit_io(fn2, "@L_kind", "%nalive") emit_io(fn2, snap_base("L_kind", 2), "%nalive")
# N3: an @Owned world snapshots its per-entity owners too, so rollback/replication # N3: an @Owned world snapshots its per-entity owners too, so rollback/replication
# round-trips ownership (like @L_kind). Gated, so non-@Owned snapshots are unchanged. # round-trips ownership (like @L_kind). Gated, so non-@Owned snapshots are unchanged.
if net_has_owned() { emit_io(fn2, "@L_owner_arr", "%nalive") } if net_has_owned() { emit_io(fn2, snap_base("L_owner_arr", 3), "%nalive") }
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, `@g_{prog[i].s}`, "4") }; i += 1 } while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, `@g_{prog[i].s}`, "4") }; i += 1 }
var ci = 0 var ci = 0
@ -63,16 +70,25 @@ function emit_snapshot_blocks(fn2: pointer) -> void {
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_COMP { if prog[i].kind == N_COMP {
let c = prog[i].s let c = prog[i].s
let csz1 = `%csz1_{itoa(ci)}`
let csz = `%csz{itoa(ci)}` let csz = `%csz{itoa(ci)}`
emit(" "); emit(csz); emit(" = ptrtoint ptr getelementptr (%Cmp_"); emit(c); emit(", ptr null, i32 "); emit(me); emit(") to i64\n") emit(" "); emit(csz1); emit(" = ptrtoint ptr getelementptr (%Cmp_"); emit(c); emit(", ptr null, i32 1) to i64\n")
emit_io(fn2, `@S_{c}`, csz) emit(" "); emit(csz); emit(" = mul i64 %sncap64, "); emit(csz1); emit("\n")
emit_io(fn2, `@H_{c}`, me) emit_io(fn2, snap_base(`S_{c}`, 4 + ci * 2), csz)
emit_io(fn2, snap_base(`H_{c}`, 5 + ci * 2), "%sncap64")
ci += 1 ci += 1
} }
i += 1 i += 1
} }
} }
# a store's base, loaded after any grow the snapshot made
function snap_base(store: pointer, k: int) -> pointer {
let r = `%snb{itoa(k)}`
emit(` {r} = load ptr, ptr @{store}\n`)
return r
}
function emit_snapshot() -> void { function emit_snapshot() -> void {
g_snap_mode = "file" # seed (module ptr inits are null) g_snap_mode = "file" # seed (module ptr inits are null)
emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n") emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n")

View file

@ -15,10 +15,12 @@ function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let c = find_comp(comp) let c = find_comp(comp)
if (c == null) { perr(`spawn: unknown property {comp}`) } if (c == null) { perr(`spawn: unknown property {comp}`) }
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n") let eb1 = ecs_base(`H_{comp}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb1); emit(", i32 "); emit(e); emit("\n")
emit(" store i8 1, ptr "); emit(hp); emit("\n") emit(" store i8 1, ptr "); emit(hp); emit("\n")
let slot = nreg() let slot = nreg()
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(comp); emit("], ptr @S_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n") let eb2 = ecs_base(`S_{comp}`)
emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(eb2); emit(", i32 "); emit(e); emit("\n")
# defaults # defaults
var f = 0 var f = 0
while f < len(c.kids) { while f < len(c.kids) {
@ -71,7 +73,8 @@ function emit_bind_props(model: Node, e: pointer) -> void {
while c < len(model.kids) { while c < len(model.kids) {
let pname = model.kids[c].s let pname = model.kids[c].s
let slot = nreg() let slot = nreg()
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(pname); emit("], ptr @S_"); emit(pname); emit(", i32 0, i32 "); emit(e); emit("\n") let eb3 = ecs_base(`S_{pname}`)
emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(pname); emit(", ptr "); emit(eb3); emit(", i32 "); emit(e); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(pname, vslot, pname) loc_push(pname, vslot, pname)
@ -146,7 +149,8 @@ function emit_spawn(st: Node) -> pointer {
if (find_prefab(st.s) != null) and (ak == 0) { perr(`prefab {st.s}: unknown model {model}`) } if (find_prefab(st.s) != null) and (ak == 0) { perr(`prefab {st.s}: unknown model {model}`) }
if ak > 0 { if ak > 0 {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(e); emit("\n") let eb4 = ecs_base("L_kind")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 "); emit(e); emit("\n")
emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n") emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n")
let arch = find_arch(model) let arch = find_arch(model)
var c = 0 var c = 0
@ -179,7 +183,8 @@ function emit_despawn(st: Node) -> void {
# @OnDespawn: dispatch on the entity's kind and run the matching model's hook # @OnDespawn: dispatch on the entity's kind and run the matching model's hook
if len(g_ondespawn) > 0 { if len(g_ondespawn) > 0 {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(v.code); emit("\n") let eb5 = ecs_base("L_kind")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 "); emit(v.code); emit("\n")
let kind = emit_bind(`load i32, ptr {kp}`) let kind = emit_bind(`load i32, ptr {kp}`)
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
@ -205,7 +210,8 @@ function emit_despawn(st: Node) -> void {
function emit_attach(st: Node) -> void { function emit_attach(st: Node) -> void {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb6 = ecs_base(`H_{st.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb6); emit(", i32 "); emit(ev.code); emit("\n")
let cur = emit_bind(`load i8, ptr {hp}`) let cur = emit_bind(`load i8, ptr {hp}`)
let isnew = emit_bind(`icmp eq i8 {cur}, 0`) let isnew = emit_bind(`icmp eq i8 {cur}, 0`)
let doit = lbl("attach"); let done = lbl("attdone") let doit = lbl("attach"); let done = lbl("attdone")
@ -223,7 +229,8 @@ function emit_attach(st: Node) -> void {
function emit_detach(st: Node) -> void { function emit_detach(st: Node) -> void {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb7 = ecs_base(`H_{st.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb7); emit(", i32 "); emit(ev.code); emit("\n")
let cur = emit_bind(`load i8, ptr {hp}`) let cur = emit_bind(`load i8, ptr {hp}`)
let here = emit_bind(`icmp ne i8 {cur}, 0`) let here = emit_bind(`icmp ne i8 {cur}, 0`)
let doit = lbl("detach"); let done = lbl("detdone") let doit = lbl("detach"); let done = lbl("detdone")
@ -233,7 +240,8 @@ function emit_detach(st: Node) -> void {
let hb = ondetach_body(st.s) # @OnDetach reads the outgoing value let hb = ondetach_body(st.s) # @OnDetach reads the outgoing value
if (hb != null) { if (hb != null) {
let save = nloc let save = nloc
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb8 = ecs_base(`S_{st.s}`)
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(st.s); emit(", ptr "); emit(eb8); emit(", i32 "); emit(ev.code); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.s, vslot, st.s) loc_push(st.s, vslot, st.s)
@ -266,13 +274,15 @@ function emit_toggle(st: Node) -> void {
if (st.a != null) { if (st.a != null) {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb9 = ecs_base(`H_{st.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb9); emit(", i32 "); emit(ev.code); emit("\n")
emit(" store i8 "); emit(val); emit(", ptr "); emit(hp); emit("\n") emit(" store i8 "); emit(val); emit(", ptr "); emit(hp); emit("\n")
var hb: Node = null var hb: Node = null
if st.ival == 1 { hb = onenable_body(st.s) } else { hb = ondisable_body(st.s) } if st.ival == 1 { hb = onenable_body(st.s) } else { hb = ondisable_body(st.s) }
if (hb != null) { if (hb != null) {
let save = nloc let save = nloc
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb10 = ecs_base(`S_{st.s}`)
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(st.s); emit(", ptr "); emit(eb10); emit(", i32 "); emit(ev.code); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.s, vslot, st.s) loc_push(st.s, vslot, st.s)

View file

@ -96,7 +96,8 @@ function emit_world_table() -> void {
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n") emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n") emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
emit("g"); emit(sk); emit(":\n") emit("g"); emit(sk); emit(":\n")
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb1 = ecs_base(`S_{cn}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb1); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}` let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
@ -139,7 +140,8 @@ function emit_world_table() -> void {
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n") emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n") emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
emit("g"); emit(sk); emit(":\n") emit("g"); emit(sk); emit(":\n")
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb2 = ecs_base(`S_{cn}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb2); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}` let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
@ -182,7 +184,8 @@ function emit_world_table() -> void {
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n") emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n") emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
emit("g"); emit(sk); emit(":\n") emit("g"); emit(sk); emit(":\n")
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(cn); emit(", i32 0, i32 %e\n") let eb3 = ecs_base(`H_{cn}`)
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds i8, ptr "); emit(eb3); emit(", i32 %e\n")
emit(" %hv"); emit(sk); emit(" = load i8, ptr %hp"); emit(sk); emit("\n") emit(" %hv"); emit(sk); emit(" = load i8, ptr %hp"); emit(sk); emit("\n")
emit(" %hr"); emit(sk); emit(" = zext i8 %hv"); emit(sk); emit(" to i32\n ret i32 %hr"); emit(sk); emit("\n") emit(" %hr"); emit(sk); emit(" = zext i8 %hv"); emit(sk); emit(" to i32\n ret i32 %hr"); emit(sk); emit("\n")
emit("gn"); emit(sk); emit(":\n") emit("gn"); emit(sk); emit(":\n")
@ -201,10 +204,10 @@ function emit_world_table() -> void {
# zeroed. The returned id works with get/set/has/attach exactly like a built-in. # zeroed. The returned id works with get/set/has/attach exactly like a built-in.
emit("define i32 @ludic_register_prop(ptr %name, i32 %nfields) {\nentry:\n") emit("define i32 @ludic_register_prop(ptr %name, i32 %nfields) {\nentry:\n")
emit(" %dc = load i32, ptr @dyn_count\n %full = icmp slt i32 %dc, 32\n br i1 %full, label %do, label %rej\n") emit(" %dc = load i32, ptr @dyn_count\n %full = icmp slt i32 %dc, 32\n br i1 %full, label %do, label %rej\n")
emit("do:\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, "); emit(me); emit("\n %szl = sext i32 %sz to i64\n") emit("do:\n %rcap = load i32, ptr @L_cap\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, %rcap\n %szl = sext i32 %sz to i64\n")
emit(" %buf = call ptr @malloc(i64 %szl)\n call ptr @memset(ptr %buf, i32 0, i64 %szl)\n") emit(" %buf = call ptr @malloc(i64 %szl)\n call ptr @memset(ptr %buf, i32 0, i64 %szl)\n")
emit(" %sp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %dc\n store ptr %buf, ptr %sp\n") emit(" %sp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %dc\n store ptr %buf, ptr %sp\n")
emit(" %hbuf = call ptr @malloc(i64 "); emit(me); emit(")\n call ptr @memset(ptr %hbuf, i32 0, i64 "); emit(me); emit(")\n") emit(" %hcap = sext i32 %rcap to i64\n %hbuf = call ptr @malloc(i64 %hcap)\n call ptr @memset(ptr %hbuf, i32 0, i64 %hcap)\n")
emit(" %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dc\n store ptr %hbuf, ptr %hp\n") emit(" %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dc\n store ptr %hbuf, ptr %hp\n")
emit(" %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dc\n store i32 %nfields, ptr %fp\n") emit(" %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dc\n store i32 %nfields, ptr %fp\n")
emit(" %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dc\n store ptr %name, ptr %np\n") emit(" %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dc\n store ptr %name, ptr %np\n")
@ -222,7 +225,8 @@ function emit_world_table() -> void {
# identifies each entity's model, then reads/writes it with get/set/has above. # identifies each entity's model, then reads/writes it with get/set/has above.
emit("define i32 @ludic_entity_count() {\nentry:\n %n = load i32, ptr @L_entc\n ret i32 %n\n}\n\n") emit("define i32 @ludic_entity_count() {\nentry:\n %n = load i32, ptr @L_entc\n ret i32 %n\n}\n\n")
emit("define i32 @ludic_kind(i32 %e) {\nentry:\n") emit("define i32 @ludic_kind(i32 %e) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") let eb4 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %e\n")
emit(" %k = load i32, ptr %kp\n ret i32 %k\n}\n\n") emit(" %k = load i32, ptr %kp\n ret i32 %k\n}\n\n")
emit("define i32 @ludic_model_id(ptr %name) {\nentry:\n") emit("define i32 @ludic_model_id(ptr %name) {\nentry:\n")
k = 0; i = 0 k = 0; i = 0
@ -287,7 +291,9 @@ function emit_world_table() -> void {
emit(" %n = load i32, ptr @L_entc\n br label %loop\n") emit(" %n = load i32, ptr @L_entc\n br label %loop\n")
emit("loop:\n %e = phi i32 [ %from, %entry ], [ %e1, %cont ]\n") emit("loop:\n %e = phi i32 [ %from, %entry ], [ %e1, %cont ]\n")
emit(" %go = icmp slt i32 %e, %n\n br i1 %go, label %body, label %none\n") emit(" %go = icmp slt i32 %e, %n\n br i1 %go, label %body, label %none\n")
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") emit("body:\n")
let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %e\n")
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n br i1 %isa, label %chk, label %cont\n") emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n br i1 %isa, label %chk, label %cont\n")
emit("chk:\n %h = call i32 @ludic_has(i32 %e, i32 %p)\n %hit = icmp ne i32 %h, 0\n br i1 %hit, label %hitb, label %cont\n") emit("chk:\n %h = call i32 @ludic_has(i32 %e, i32 %p)\n %hit = icmp ne i32 %h, 0\n br i1 %hit, label %hitb, label %cont\n")
emit("hitb:\n ret i32 %e\n") emit("hitb:\n ret i32 %e\n")
@ -446,6 +452,7 @@ function emit_game_main() -> void {
emit(" store i32 %argc, ptr @L_argc\n") emit(" store i32 %argc, ptr @L_argc\n")
emit(" store ptr %argv, ptr @L_argv\n") emit(" store ptr %argv, ptr @L_argv\n")
emit(" call void @L_init_runtime()\n") emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") } if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") }
emit(" call void @L_init_globals()\n") emit(" call void @L_init_globals()\n")
emit_calls_for_phase("Start") emit_calls_for_phase("Start")

View file

@ -121,7 +121,8 @@ function emit_net_serialize(m: Node) -> void {
let cn = m.kids[ci].s let cn = m.kids[ci].s
let c = find_comp(cn) let c = find_comp(cn)
if (c != null) { if (c != null) {
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb1 = ecs_base(`S_{cn}`)
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb1); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
if c.kids[fj].ival == 1 { if c.kids[fj].ival == 1 {
@ -154,7 +155,8 @@ function emit_net_apply(m: Node) -> void {
let cn = m.kids[ci].s let cn = m.kids[ci].s
let c = find_comp(cn) let c = find_comp(cn)
if (c != null) { if (c != null) {
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb2 = ecs_base(`S_{cn}`)
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb2); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
if c.kids[fj].ival == 1 { if c.kids[fj].ival == 1 {
@ -181,7 +183,8 @@ function emit_net_dispatch() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n") emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n") let eb3 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb3); emit(", i32 %e\n %k = load i32, ptr %kp\n")
var k = 0 var k = 0
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
@ -198,7 +201,8 @@ function emit_net_dispatch() -> void {
emit(" ret i32 0\n}\n\n") emit(" ret i32 0\n}\n\n")
emit("define void @ludic_apply(i32 %e, ptr %buf, i32 %len) {\nentry:\n") emit("define void @ludic_apply(i32 %e, ptr %buf, i32 %len) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n") let eb4 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %e\n %k = load i32, ptr %kp\n")
k = 0; i = 0 k = 0; i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) { if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
@ -216,7 +220,8 @@ function emit_net_dispatch() -> void {
# ludic_sync_size(e): the replicated byte count for the entity's model — a # ludic_sync_size(e): the replicated byte count for the entity's model — a
# constant per kind, so a runtime can size a buffer before serialize. # constant per kind, so a runtime can size a buffer before serialize.
emit("define i32 @ludic_sync_size(i32 %e) {\nentry:\n") emit("define i32 @ludic_sync_size(i32 %e) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n") let eb5 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %e\n %k = load i32, ptr %kp\n")
k = 0; i = 0 k = 0; i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) { if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
@ -239,12 +244,15 @@ function emit_net_dispatch() -> void {
function emit_net_owner() -> void { function emit_net_owner() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @L_owner(i32 %e) {\nentry:\n") emit("define i32 @L_owner(i32 %e) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n") let eb6 = ecs_base("L_owner_arr")
emit(" %p = getelementptr inbounds i32, ptr "); emit(eb6); emit(", i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n")
emit("define void @L_set_owner(i32 %e, i32 %id) {\nentry:\n") emit("define void @L_set_owner(i32 %e, i32 %id) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n store i32 %id, ptr %p\n ret void\n}\n\n") let eb7 = ecs_base("L_owner_arr")
emit(" %p = getelementptr inbounds i32, ptr "); emit(eb7); emit(", i32 %e\n store i32 %id, ptr %p\n ret void\n}\n\n")
# is_owner(e): does the local peer own e? owner(e) == local_id(). # is_owner(e): does the local peer own e? owner(e) == local_id().
emit("define i32 @L_is_owner(i32 %e) {\nentry:\n") emit("define i32 @L_is_owner(i32 %e) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %o = load i32, ptr %p\n") let eb8 = ecs_base("L_owner_arr")
emit(" %p = getelementptr inbounds i32, ptr "); emit(eb8); emit(", i32 %e\n %o = load i32, ptr %p\n")
emit(" %lid = load i32, ptr @L_localid\n %eq = icmp eq i32 %o, %lid\n %r = zext i1 %eq to i32\n ret i32 %r\n}\n\n") emit(" %lid = load i32, ptr @L_localid\n %eq = icmp eq i32 %o, %lid\n %r = zext i1 %eq to i32\n ret i32 %r\n}\n\n")
} }

View file

@ -147,7 +147,7 @@ function red_check(i: int) -> void {
function act_src() -> pointer { function act_src() -> pointer {
var src = "export state LudicActions {\n kinds: []int = new []int\n vals: []pointer = new []pointer\n head: int = 0\n depth: int = 0\n}\n" var src = "export state LudicActions {\n kinds: []int = new []int\n vals: []pointer = new []pointer\n head: int = 0\n depth: int = 0\n}\n"
src = src + "export function ludic_act_push(q: mut LudicActions, k: int, v: pointer) -> void {\n push(q.kinds, k)\n push(q.vals, v)\n}\n" src = src + "export function ludic_act_push(q: mut LudicActions, k: int, v: pointer) -> void {\n push(q.kinds, k)\n push(q.vals, v)\n}\n"
src = src + "export function drain_actions(q: mut LudicActions) -> void {\n if q.depth > 0 { return }\n q.depth = 1\n var pass = 0\n var round_end = len(q.kinds)\n" src = src + "export function drain_actions(q: mut LudicActions) -> void {\n if q.depth > 0 or len(q.kinds) == 0 { return }\n q.depth = 1\n var pass = 0\n var round_end = len(q.kinds)\n"
src = src + " while q.head < len(q.kinds) {\n if q.head == round_end {\n pass += 1\n round_end = len(q.kinds)\n" src = src + " while q.head < len(q.kinds) {\n if q.head == round_end {\n pass += 1\n round_end = len(q.kinds)\n"
src = src + ` if pass >= {itoa(ACTION_PASSES)} {{ ludic_act_runaway(q.kinds[q.head]) }}\n }}\n` src = src + ` if pass >= {itoa(ACTION_PASSES)} {{ ludic_act_runaway(q.kinds[q.head]) }}\n }}\n`
src = src + " let k = q.kinds[q.head]\n let v = q.vals[q.head]\n q.head += 1\n" src = src + " let k = q.kinds[q.head]\n let v = q.vals[q.head]\n q.head += 1\n"
@ -165,7 +165,7 @@ function act_src() -> pointer {
} }
a += 1 a += 1
} }
src = src + " }\n q.kinds = new []int\n q.vals = new []pointer\n q.head = 0\n q.depth = 0\n}\n" src = src + " }\n List.clear(q.kinds)\n List.clear(q.vals)\n q.head = 0\n q.depth = 0\n}\n" # in place: a drain a phase, seven a frame, allocates nothing
src = src + "export function ludic_act_runaway(k: int) -> void {\n var name = \"?\"\n" src = src + "export function ludic_act_runaway(k: int) -> void {\n var name = \"?\"\n"
a = 0 a = 0
while a < len(g_act_names) { while a < len(g_act_names) {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff