merge lang/foundations into lang/native-jolt (seeds regenerated)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 03:20:44 +03:00
commit b247603b7e
62 changed files with 41302 additions and 40864 deletions

View file

@ -0,0 +1,4 @@
bump: patch
type: fix
**The foliage's near-fade programs have SPIR-V.** The three `NEAR_FADE` variants were missing
from `variants.list`, so on Vulkan the trees' and cards' programs failed to build.

View file

@ -0,0 +1,10 @@
bump: minor
type: feature
**render3d's Vulkan renderer runs in a macOS window, through MoltenVK.** `R3D_GFX=vk` (or
`gpu_request("vulkan")`) now presents to a `CAMetalLayer` on the window (`VK_EXT_metal_surface`,
the runtime's `win_metal_layer()`), where it used to fall back to OpenGL. The loader also opens
MoltenVK on its own - `Contents/Frameworks/libMoltenVK.dylib` in a bundle, beside the executable,
or `$VULKAN_SDK/lib` - so a game needs no Vulkan SDK to ship it. A covered window is not drawn to
(its layer would hold every frame for a second), and on macOS one frame stays in flight so the CPU
runs the next tick while the GPU draws. OpenGL stays the default: on an M4 Pro the Vulkan frame is
still about a third slower, most of it in the alpha-tested foliage.

View file

@ -0,0 +1,7 @@
bump: patch
type: fix
**A windowed program's input no longer stops it on the first frame.** Since slices carry their
length, the window's built-ins (`win_held`, `win_mouse`, `win_pad`, `win_touch`, `win_text`,
`win_present`) were handed the slice's header instead of its elements, so the platform wrote the
mouse position over the length and the next read failed its bounds check. They take the elements,
as an `extern` call already did.

View file

@ -23,10 +23,14 @@ event Died { e: int = 0, src: int = 0 }
event Healed { e: int = 0, amount: int = 0 }
event Crit { target: int = 0, src: int = 0 } # the attacker's crit_pct fired
# the faction table: FAC_MAX ids square, every pair unset (faction.ludic)
const FAC_MAX: int = 32
function fac_table() -> words { return words(FAC_MAX * FAC_MAX) }
state GameplayState {
combat_reflecting: bool = false # thorns never reflect thorns
combat_pending: int = 0
fac_tbl: words = words(FAC_MAX * FAC_MAX)
fac_tbl: words = fac_table()
}
# the mutable channel for lever 6: listeners rewrite the pending damage here.

View file

@ -9,20 +9,14 @@
property Faction { id: int = 0 }
const FAC_MAX: int = 32
const REL_FRIEND: int = 0
const REL_NEUTRAL: int = 1
const REL_HOSTILE: int = 2
# relationships stored as (rel + 1); a zero slot means "unset -> use the default".
# A global var initialiser must be a literal, so it is allocated lazily.
function fac_ensure(gameplay_st: mut GameplayState) -> void {
}
# relationships stored as (rel + 1); a zero slot means "unset -> use the default"
# Faction.set(a, b, rel) — record a symmetric relationship between two factions.
@Namespace(Faction) function faction_set(gameplay_st: mut GameplayState, a: int, b: int, rel: int) -> void {
fac_ensure(gameplay_st)
if (a < 0) or (a >= FAC_MAX) { return }
if (b < 0) or (b >= FAC_MAX) { return }
gameplay_st.fac_tbl[a * FAC_MAX + b] = rel + 1
@ -31,7 +25,6 @@ function fac_ensure(gameplay_st: mut GameplayState) -> void {
# Faction.rel(a, b) — the relationship code (0 friend, 1 neutral, 2 hostile).
@Namespace(Faction) function faction_rel(gameplay_st: mut GameplayState, a: int, b: int) -> int {
fac_ensure(gameplay_st)
if (a >= 0) and (a < FAC_MAX) and (b >= 0) and (b < FAC_MAX) {
let v = gameplay_st.fac_tbl[a * FAC_MAX + b]
if v != 0 { return v - 1 }

View file

@ -1,27 +1,18 @@
# mute.ludic - the cards a player has said they know, kept BY KEY: a card added, removed or moved in
# the registry leaves every other mute where it was, and a key this build does not know is carried
function hn_mute_ensure(hints_st: mut HintsState) -> void {
if hints_st.hn_mute != null { return }
hints_st.hn_mute = new []bool
for i in 0 .. HINT_COUNT { push(hints_st.hn_mute, false) }
hints_st.hn_foreign = new []string
}
export function hints_muted(hints_st: mut HintsState, id: int) -> bool {
hn_mute_ensure(hints_st)
export function hints_muted(hints_st: HintsState, id: int) -> bool {
if id < 0 or id >= HINT_COUNT { return false }
return hints_st.hn_mute[id]
}
export function hints_mute(hints_st: mut HintsState, id: int, on: bool) -> void {
hn_mute_ensure(hints_st)
if id < 0 or id >= HINT_COUNT { return }
hints_st.hn_mute[id] = on
}
export function hints_mute_count(hints_st: mut HintsState) -> int {
hn_mute_ensure(hints_st)
export function hints_mute_count(hints_st: HintsState) -> int {
var n = 0
for i in 0 .. HINT_COUNT { if hints_st.hn_mute[i] { n += 1 } }
return n
@ -29,7 +20,6 @@ export function hints_mute_count(hints_st: mut HintsState) -> int {
# every muted card given back, from the next pass
export function hints_unmute_all(hints_st: mut HintsState) -> void {
hn_mute_ensure(hints_st)
for i in 0 .. HINT_COUNT { hints_st.hn_mute[i] = false }
hints_st.hn_foreign = new []string
hints_st.hn_t = 0.0
@ -37,8 +27,7 @@ export function hints_unmute_all(hints_st: mut HintsState) -> void {
}
# the mutes as a list of keys, for wherever the game keeps the player's own choices
export function hints_mutes_save(hints_st: mut HintsState) -> Val {
hn_mute_ensure(hints_st)
export function hints_mutes_save(hints_st: HintsState) -> Val {
let l = Value.list()
for i in 0 .. HINT_COUNT { if hints_st.hn_mute[i] { Value.add(l, Value.str(Hints[i].key)) } }
for i in 0 .. len(hints_st.hn_foreign) { Value.add(l, Value.str(hints_st.hn_foreign[i])) }
@ -47,8 +36,7 @@ export function hints_mutes_save(hints_st: mut HintsState) -> Val {
# a list of keys read back; anything that is not a list is no mutes
export function hints_mutes_load(hints_st: mut HintsState, l: Val) -> void {
hints_st.hn_mute = null
hn_mute_ensure(hints_st)
hints_st.hn_mute = hn_mutes()
if l == null or Value.kind(l) != 5 { return }
for i in 0 .. Value.count(l) {
let e = Value.at(l, i)
@ -58,3 +46,8 @@ export function hints_mutes_load(hints_st: mut HintsState, l: Val) -> void {
if id >= 0 { hints_st.hn_mute[id] = true } else { push(hints_st.hn_foreign, k) }
}
}
function hn_mutes() -> []bool {
let out = new []bool
for i in 0 .. HINT_COUNT { push(out, false) }
return out
}

View file

@ -1,11 +1,11 @@
# rail.ludic - the rail: once a pass (a second by default) the most urgent true cards, up to a few,
# muted ones left out; in a tie the registry's order decides
export state HintsState {
hn_mute: []bool = null
hn_foreign: []string = null # muted keys with no card in this build, written back as read
hn_mute: []bool = hn_mutes()
hn_foreign: []string = new []string # muted keys with no card in this build, written back as read
hn_shown: int = 3 # how many the rail holds
hn_every: float = 1.0 # seconds between passes
hn_top: []int = null # the cards on the rail this pass
hn_top: []int = hn_slots(3) # the cards on the rail this pass
hn_n: int = 0
hn_t: float = 0.0 # seconds to the next pass
hn_pick: int = 0 # which chip the key would open
@ -28,22 +28,19 @@ function hn_fact(hints_st: HintsState, what: int, card: int) -> void {
export function hints_config(hints_st: mut HintsState, shown: int, every: float) -> void {
hints_st.hn_shown = shown
hints_st.hn_every = every
hints_st.hn_top = null
hints_st.hn_top = hn_slots(shown)
}
function hn_ensure(hints_st: mut HintsState) -> void {
if hints_st.hn_top == null or len(hints_st.hn_top) != hints_st.hn_shown {
hints_st.hn_top = new []int
for i in 0 .. hints_st.hn_shown { push(hints_st.hn_top, -1) }
}
hn_mute_ensure(hints_st)
# a rail of n empty places
function hn_slots(n: int) -> []int {
let out = new []int
for i in 0 .. n { push(out, -1) }
return out
}
# the next pass comes on the next tick rather than in a second
export function hints_refresh(hints_st: mut HintsState) -> void { hints_st.hn_t = 0.0 }
export function hints_tick(hints_st: mut HintsState, dt: float) -> void {
hn_ensure(hints_st)
hints_st.hn_t = hints_st.hn_t - dt
if hints_st.hn_t > 0.0 { return }
hints_st.hn_t = hints_st.hn_every
@ -53,7 +50,6 @@ export function hints_tick(hints_st: mut HintsState, dt: float) -> void {
# one pass now: the rail re-sorted, the pick kept in range, and the card following its subject
# along the rail - or put down when what it was about stopped being true
export function hints_sort(hints_st: mut HintsState) -> void {
hn_ensure(hints_st)
hints_st.hn_n = 0
let on = HintsWorld.showing()
for u in 0 .. 4 {
@ -76,8 +72,7 @@ export function hints_sort(hints_st: mut HintsState) -> void {
}
# the cards on the rail, most urgent first (empty while the rail is not showing)
export function hints_rail(hints_st: mut HintsState) -> []int {
hn_ensure(hints_st)
export function hints_rail(hints_st: HintsState) -> []int {
let out = new []int
if not HintsWorld.showing() { return out }
for k in 0 .. hints_st.hn_n { push(out, hints_st.hn_top[k]) }
@ -96,7 +91,7 @@ export function hints_pick(hints_st: HintsState) -> int { return hints_st.hn_pic
export function hints_hot(hints_st: HintsState, k: int) -> bool { return k == hints_st.hn_pick }
export function hints_reset(hints_st: mut HintsState) -> void {
hints_st.hn_top = null
hints_st.hn_top = hn_slots(hints_st.hn_shown)
hints_st.hn_n = 0
hints_st.hn_t = 0.0
hints_st.hn_pick = 0

View file

@ -2,14 +2,12 @@
# the jobs' own dice seeded by the day, so a board never moves anyone else's rolls
export function jobs_board_ok(b: int) -> bool { return b >= 0 and b < BOARD_COUNT }
function jobs__at(jobs_st: mut JobsState, b: int, s: int) -> int {
jobs__ensure(jobs_st)
function jobs__at(jobs_st: JobsState, b: int, s: int) -> int {
return jobs_st.jobs__off[b] + s
}
# a whole number in [lo, hi] from the jobs' dice: what JobsWorld.roll draws with
export function jobs_roll(jobs_st: mut JobsState, lo: int, hi: int) -> int {
jobs__ensure(jobs_st)
export function jobs_roll(jobs_st: JobsState, lo: int, hi: int) -> int {
return rng_between(jobs_st.jobs__dice, lo, hi)
}
@ -51,7 +49,6 @@ function jobs__repost(jobs_st: mut JobsState, b: int, s: int) -> void {
# a new day, on the boards this machine keeps: an empty or stale post is replaced, and one
# finished on an earlier day
export function jobs_morning(jobs_st: mut JobsState) -> void {
jobs__ensure(jobs_st)
let day = JobsWorld.day()
for b in 0 .. BOARD_COUNT {
let bd = JobBoards[b]

View file

@ -17,7 +17,6 @@ function jobs__takes(only: int, scope: int) -> bool {
}
function jobs__count(jobs_st: mut JobsState, only: int, kind: int, param: int, n: int) -> void {
jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT {
let d = Jobs[i]
if jobs_st.jobs__st[i] != JOBS_ACTIVE or not jobs__takes(only, d.scope) { continue }
@ -49,7 +48,6 @@ function jobs__recheck_one(jobs_st: mut JobsState, i: int) -> void {
# every job under way against what the state already proves: work done before it was taken counts
# for a written job; a post counts only what came after it went up
export function jobs_recheck(jobs_st: mut JobsState) -> void {
jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT { jobs__recheck_one(jobs_st, i) }
for b in 0 .. BOARD_COUNT {
if not JobsWorld.owns(JobBoards[b].scope) { continue }
@ -64,7 +62,6 @@ export function jobs_recheck(jobs_st: mut JobsState) -> void {
# offers newly open are said once; the first look after a reset says none of them
function jobs__offers(jobs_st: mut JobsState) -> void {
jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT {
let now = jobs_offered(jobs_st, i)
if now and not jobs_st.jobs__seen[i] and jobs_st.jobs__primed { jobs__fact(jobs_st, JOBS_F_OFFERED, i, -1, -1, 0, Jobs[i].need) }

View file

@ -1,26 +1,24 @@
# jobs.ludic - the written jobs: offered by standing and story, taken, readied, handed in and paid
export function jobs_ok(i: int) -> bool { return i >= 0 and i < JOB_COUNT }
export function jobs_state(jobs_st: mut JobsState, i: int) -> int {
jobs__ensure(jobs_st)
export function jobs_state(jobs_st: JobsState, i: int) -> int {
return jobs_st.jobs__st[i]
}
export function jobs_have(jobs_st: mut JobsState, i: int) -> int {
jobs__ensure(jobs_st)
export function jobs_have(jobs_st: JobsState, i: int) -> int {
return jobs_st.jobs__hv[i]
}
export function jobs_ready_now(jobs_st: mut JobsState, i: int) -> bool { return jobs_state(jobs_st, i) == JOBS_READY }
export function jobs_ready_now(jobs_st: JobsState, i: int) -> bool { return jobs_state(jobs_st, i) == JOBS_READY }
# taken and not yet paid
export function jobs_under_way(jobs_st: mut JobsState, i: int) -> bool {
export function jobs_under_way(jobs_st: JobsState, i: int) -> bool {
let s = jobs_state(jobs_st, i)
return s == JOBS_ACTIVE or s == JOBS_READY
}
# on offer: not taken, the scope's standing and the story far enough, and the game allowing it
export function jobs_offered(jobs_st: mut JobsState, i: int) -> bool {
export function jobs_offered(jobs_st: JobsState, i: int) -> bool {
let d = Jobs[i]
if jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i)
@ -79,18 +77,15 @@ export function jobs_complete(jobs_st: mut JobsState, i: int) -> bool {
# the state outright: the host's word for a party job, a test
export function jobs_set(jobs_st: mut JobsState, i: int, state: int, have: int) -> void {
jobs__ensure(jobs_st)
jobs_st.jobs__st[i] = state
jobs_st.jobs__hv[i] = have
}
export function jobs_done(jobs_st: mut JobsState, group: int) -> int {
jobs__ensure(jobs_st)
export function jobs_done(jobs_st: JobsState, group: int) -> int {
if group < 0 or group >= JOBS_GROUPS { return 0 }
return jobs_st.jobs__done_n[group]
}
export function jobs_set_done(jobs_st: mut JobsState, group: int, n: int) -> void {
jobs__ensure(jobs_st)
if group >= 0 and group < JOBS_GROUPS { jobs_st.jobs__done_n[group] = n }
}

View file

@ -1,6 +1,6 @@
# lists.ludic - the written jobs as a screen lists them: a vendor's, the taken, the ones to show
# a vendor's offers and the ones it can take back, in a group (-1 every vendor)
export function jobs_at(jobs_st: mut JobsState, group: int, vendor: int) -> []int {
export function jobs_at(jobs_st: JobsState, group: int, vendor: int) -> []int {
let out = new []int
for i in 0 .. JOB_COUNT {
let d = Jobs[i]
@ -13,14 +13,14 @@ export function jobs_at(jobs_st: mut JobsState, group: int, vendor: int) -> []in
}
# every job of a group taken at some point, done ones too
export function jobs_taken(jobs_st: mut JobsState, group: int) -> []int {
export function jobs_taken(jobs_st: JobsState, group: int) -> []int {
let out = new []int
for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(jobs_st, i) != JOBS_OFFERED { push(out, i) } }
return out
}
# the first `most` of a group under way
export function jobs_shown(jobs_st: mut JobsState, group: int, most: int) -> []int {
export function jobs_shown(jobs_st: JobsState, group: int, most: int) -> []int {
let out = new []int
for i in 0 .. JOB_COUNT {
if len(out) >= most { break }
@ -30,7 +30,7 @@ export function jobs_shown(jobs_st: mut JobsState, group: int, most: int) -> []i
}
# the first under way and not yet met, or -1
export function jobs_first_open(jobs_st: mut JobsState, group: int) -> int {
export function jobs_first_open(jobs_st: JobsState, group: int) -> int {
for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(jobs_st, i) == JOBS_ACTIVE { return i } }
return -1
}

View file

@ -1,12 +1,12 @@
# posts.ludic - what is on a board, slot by slot, and a board as the host says it
export function jobs_post_kind(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pk[jobs__at(jobs_st, b, s)] }
export function jobs_post_param(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pp[jobs__at(jobs_st, b, s)] }
export function jobs_post_need(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pn[jobs__at(jobs_st, b, s)] }
export function jobs_post_have(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__ph[jobs__at(jobs_st, b, s)] }
export function jobs_post_money(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pm[jobs__at(jobs_st, b, s)] }
export function jobs_post_rep(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pr[jobs__at(jobs_st, b, s)] }
export function jobs_post_state(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__ps[jobs__at(jobs_st, b, s)] }
export function jobs_post_day(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pd[jobs__at(jobs_st, b, s)] }
export function jobs_post_kind(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__pk[jobs__at(jobs_st, b, s)] }
export function jobs_post_param(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__pp[jobs__at(jobs_st, b, s)] }
export function jobs_post_need(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__pn[jobs__at(jobs_st, b, s)] }
export function jobs_post_have(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__ph[jobs__at(jobs_st, b, s)] }
export function jobs_post_money(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__pm[jobs__at(jobs_st, b, s)] }
export function jobs_post_rep(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__pr[jobs__at(jobs_st, b, s)] }
export function jobs_post_state(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__ps[jobs__at(jobs_st, b, s)] }
export function jobs_post_day(jobs_st: JobsState, b: int, s: int) -> int { return jobs_st.jobs__pd[jobs__at(jobs_st, b, s)] }
export function jobs_slots(b: int) -> int {
if not jobs_board_ok(b) { return 0 }
@ -14,21 +14,20 @@ export function jobs_slots(b: int) -> int {
}
# the slots with something posted, in order
export function jobs_posted(jobs_st: mut JobsState, b: int) -> []int {
export function jobs_posted(jobs_st: JobsState, b: int) -> []int {
let out = new []int
for s in 0 .. jobs_slots(b) { if jobs_post_kind(jobs_st, b, s) >= 0 { push(out, s) } }
return out
}
# how many of a board's posts are paid
export function jobs_posts_done(jobs_st: mut JobsState, b: int) -> int {
export function jobs_posts_done(jobs_st: JobsState, b: int) -> int {
var n = 0
for s in 0 .. jobs_slots(b) { if jobs_post_kind(jobs_st, b, s) >= 0 and jobs_post_state(jobs_st, b, s) == JOBS_DONE { n += 1 } }
return n
}
export function jobs_total(jobs_st: mut JobsState, b: int) -> int {
jobs__ensure(jobs_st)
export function jobs_total(jobs_st: JobsState, b: int) -> int {
return jobs_st.jobs__total[b]
}
@ -46,6 +45,5 @@ export function jobs_post_set(jobs_st: mut JobsState, b: int, s: int, kind: int,
}
export function jobs_set_total(jobs_st: mut JobsState, b: int, n: int) -> void {
jobs__ensure(jobs_st)
jobs_st.jobs__total[b] = n
}

View file

@ -1,7 +1,6 @@
# save.ludic - a scope's jobs and boards in a section of their own, by key: a job the game has since
# removed is dropped, a new one starts offered
function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void {
jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT {
if Jobs[i].scope != sc { continue }
jobs_st.jobs__st[i] = JOBS_OFFERED

View file

@ -1,25 +1,25 @@
# state.ludic - each job's state and count, the posts on the boards, the done counts, the dice and
# the facts. Made on first use, so the registries are complete by then.
# the facts, each sized from the registries when the state is made.
export state JobsState {
jobs__st: []int = null
jobs__hv: []int = null
jobs__seen: []bool = null # an offer already said
jobs__st: []int = jobs__ints(JOB_COUNT, JOBS_OFFERED)
jobs__hv: []int = jobs__ints(JOB_COUNT, 0)
jobs__seen: []bool = jobs__bools(JOB_COUNT) # an offer already said
jobs__primed: bool = false # the first tick after a reset learns the offers quietly
jobs__done_n: []int = null # handed in, per group
jobs__off: []int = null
jobs__pk: []int = null # kind, -1 empty
jobs__pp: []int = null
jobs__pn: []int = null
jobs__ph: []int = null
jobs__pb: []int = null # the evidence when it went up
jobs__pm: []int = null
jobs__pr: []int = null
jobs__ps: []int = null
jobs__pd: []int = null # the day it went up
jobs__total: []int = null # handed in, per board
jobs__serial: []int = null # posts made, per board: each roll its own seed
jobs__done_n: []int = jobs__ints(JOBS_GROUPS, 0) # handed in, per group
jobs__off: []int = jobs__offsets()
jobs__pk: []int = jobs__ints(jobs__slots(), -1) # kind, -1 empty
jobs__pp: []int = jobs__ints(jobs__slots(), -1)
jobs__pn: []int = jobs__ints(jobs__slots(), 0)
jobs__ph: []int = jobs__ints(jobs__slots(), 0)
jobs__pb: []int = jobs__ints(jobs__slots(), 0) # the evidence when it went up
jobs__pm: []int = jobs__ints(jobs__slots(), 0)
jobs__pr: []int = jobs__ints(jobs__slots(), 0)
jobs__ps: []int = jobs__ints(jobs__slots(), JOBS_OFFERED)
jobs__pd: []int = jobs__ints(jobs__slots(), 0) # the day it went up
jobs__total: []int = jobs__ints(BOARD_COUNT, 0) # handed in, per board
jobs__serial: []int = jobs__ints(BOARD_COUNT, 0) # posts made, per board: each roll its own seed
jobs__seed: int = 1000
jobs__dice: Rng = null
jobs__dice: Rng = rng_new(1000)
jobs__q: Queue<JobsFact> = jobs__q__new()
}
@ -32,31 +32,25 @@ function jobs__ints(n: int, v: int) -> []int {
return out
}
function jobs__ensure(jobs_st: mut JobsState) -> void {
if jobs_st.jobs__st != null { return }
jobs_st.jobs__st = jobs__ints(JOB_COUNT, JOBS_OFFERED)
jobs_st.jobs__hv = jobs__ints(JOB_COUNT, 0)
jobs_st.jobs__seen = new []bool
for i in 0 .. JOB_COUNT { push(jobs_st.jobs__seen, false) }
jobs_st.jobs__done_n = jobs__ints(JOBS_GROUPS, 0)
jobs_st.jobs__off = new []int
function jobs__bools(n: int) -> []bool {
let out = new []bool
for i in 0 .. n { push(out, false) }
return out
}
# every board's slots end to end: where each board's start, and how many in all
function jobs__offsets() -> []int {
let out = new []int
var n = 0
for b in 0 .. BOARD_COUNT {
push(jobs_st.jobs__off, n)
push(out, n)
n += JobBoards[b].slots
}
jobs_st.jobs__pk = jobs__ints(n, -1)
jobs_st.jobs__pp = jobs__ints(n, -1)
jobs_st.jobs__pn = jobs__ints(n, 0)
jobs_st.jobs__ph = jobs__ints(n, 0)
jobs_st.jobs__pb = jobs__ints(n, 0)
jobs_st.jobs__pm = jobs__ints(n, 0)
jobs_st.jobs__pr = jobs__ints(n, 0)
jobs_st.jobs__ps = jobs__ints(n, JOBS_OFFERED)
jobs_st.jobs__pd = jobs__ints(n, 0)
jobs_st.jobs__total = jobs__ints(BOARD_COUNT, 0)
jobs_st.jobs__serial = jobs__ints(BOARD_COUNT, 0)
jobs_st.jobs__dice = rng_new(jobs_st.jobs__seed)
return out
}
function jobs__slots() -> int {
var n = 0
for b in 0 .. BOARD_COUNT { n += JobBoards[b].slots }
return n
}
export function jobs_facts(jobs_st: JobsState) -> Queue<JobsFact> {

View file

@ -8,8 +8,7 @@ export function jobs_reset(jobs_st: mut JobsState) -> void {
export function jobs_party_reset(jobs_st: mut JobsState) -> void { jobs__reset_scope(jobs_st, JOBS_PARTY) }
export function jobs_save(jobs_st: mut JobsState) -> Val {
jobs__ensure(jobs_st)
export function jobs_save(jobs_st: JobsState) -> Val {
let v = jobs__save_scope(jobs_st, JOBS_SELF)
sv_put_ints(v, "done", jobs_st.jobs__done_n)
return v
@ -24,8 +23,7 @@ export function jobs_load(jobs_st: mut JobsState, v: Val, version: int) -> void
}
}
export function jobs_party_save(jobs_st: mut JobsState) -> Val {
jobs__ensure(jobs_st)
export function jobs_party_save(jobs_st: JobsState) -> Val {
return jobs__save_scope(jobs_st, JOBS_PARTY)
}

View file

@ -2,21 +2,21 @@
# compressed (a PNG anything opens, with no zlib here): the shot is the PPM's pixels, filter 0
# on every row, in deflate's stored blocks, with its CRCs and its Adler-32.
function lp_table_init(lab_st: mut LabState) -> void {
if lab_st.lp_table != null { return }
lab_st.lp_table = new []long
# the CRC-32 table, made once with the state
function lp_crc_table() -> []long {
let t = new []long
let poly: long = 3988292384 # 0xEDB88320: eight hex digits would be a 32-bit pattern
for n in 0 .. 256 {
var c = long(n)
for k in 0 .. 8 {
if (c & long(1)) != long(0) { c = (c >> long(1)) ^ poly } else { c = c >> long(1) }
}
push(lab_st.lp_table, c)
push(t, c)
}
return t
}
# CRC-32 of out[from .. from + n)
function lp_crc(lab_st: mut LabState, out: []byte, from: int, n: int) -> long {
lp_table_init(lab_st)
let mask: long = 4294967295
var c = mask
for i in from .. from + n {

View file

@ -2,7 +2,7 @@
export state LabState {
lb_plate: Actor = null
lb_plate_dir: string = ""
lp_table: []long = null
lp_table: []long = lp_crc_table()
lp_w: int = 0
lp_h: int = 0
lb_scene: string = ""

View file

@ -22,7 +22,6 @@ export function nb_copy(dst: []byte, d: int, src: []byte, s: int, n: int) -> voi
# the writer: past the MTU it counts on and keeps nothing, and np_queue cuts the message there
export function nw_begin(net_st: mut NetState) -> void {
net__init(net_st)
net_st.net__wl = 0
}
@ -82,7 +81,6 @@ export function nr_bad(net_st: NetState) -> bool { return net_st.net__rbad }
# a message that came in, made the one nr_* reads
export function net_read(net_st: mut NetState, m: NetMessage) -> void {
net__init(net_st)
nb_copy(net_st.net__rb, 0, m.data, 0, m.len)
net_st.net__rl = m.len
net_st.net__rp = 0
@ -90,8 +88,7 @@ export function net_read(net_st: mut NetState, m: NetMessage) -> void {
}
# the message just written, as if it had come in (a loopback: a test, a host telling itself)
export function net_written(net_st: mut NetState) -> NetMessage {
net__init(net_st)
export function net_written(net_st: NetState) -> NetMessage {
let m = new NetMessage
m.from = net_st.net__my_pid
m.len = min(net_st.net__wl, NET_MTU)

View file

@ -29,7 +29,6 @@ export function net_room_join(net_st: mut NetState, code: string) -> bool {
net_st.net__why = NET_WHY_NO_MATCHMAKER
return false
}
net__init(net_st)
if not net__socket(net_st) { return false }
net_st.net__r_role = 2
net_st.net__room = Text.upper(code)

View file

@ -3,7 +3,6 @@
# open a socket on `port` and take guests; false (net_why) when the port is taken
export function net_host(net_st: mut NetState, port: int, token: int) -> bool {
net__init(net_st)
if net_live(net_st) { return true }
net_st.net__sock = NetSocket.open(port)
if net_st.net__sock <= 0 {
@ -20,7 +19,6 @@ export function net_host(net_st: mut NetState, port: int, token: int) -> bool {
# dial a host at an address; the hello (net_hello_set) goes until a welcome or a refusal
export function net_join(net_st: mut NetState, ip: int, port: int) -> bool {
net__init(net_st)
if net_live(net_st) { return false }
if not net__socket(net_st) { return false }
for i in 0 .. NET_PEERS { np_close(net_st, i) }
@ -42,7 +40,6 @@ function net__socket(net_st: mut NetState) -> bool {
# the message just written is the hello: what a host is asked to let this machine in with
export function net_hello_set(net_st: mut NetState) -> void {
net__init(net_st)
nb_copy(net_st.net__hello_b, 0, net_st.net__wb, 0, net_st.net__wl)
net_st.net__hello_l = net_st.net__wl
if net_st.net__hello_l > NET_MTU - 40 { net_st.net__hello_l = NET_MTU - 40 }
@ -50,7 +47,6 @@ export function net_hello_set(net_st: mut NetState) -> void {
# (again) to that address, with a fresh connection and the hello sent anew
export function net_dial(net_st: mut NetState, ip: int, port: int) -> void {
net__init(net_st)
np_close(net_st, 0)
net_st.net__join_ip = ip
net_st.net__join_port = port
@ -70,7 +66,6 @@ export function net_follow(net_st: mut NetState, ip: int, port: int) -> void {
export function net_stand_alone(net_st: mut NetState) -> void { net_st.net__state = NET_IDLE }
export function net_become_host(net_st: mut NetState, token: int) -> void {
net__init(net_st)
net_st.net__state = NET_HOSTING
net_st.net__my_pid = 1
net_st.net__next_pid = 2

View file

@ -79,17 +79,17 @@ export state NetState {
net__token: int = 0
net__my_pid: int = 0
net__next_pid: int = 2
net__peers: []NetPeer = null
net__in: []byte = null
net__out: []byte = null
net__wb: []byte = null # the message being written
net__peers: []NetPeer = net__peers_new()
net__in: []byte = buffer(NET_MTU + 64)
net__out: []byte = buffer(NET_MTU + 64)
net__wb: []byte = buffer(NET_MTU) # the message being written
net__wl: int = 0
net__rb: []byte = null # the message being read
net__rb: []byte = buffer(NET_MTU) # the message being read
net__rl: int = 0
net__rp: int = 0
net__rbad: bool = false
net__tb: []byte = buffer(NET_MTU) # text read out of a message, through one scratch buffer
net__hello_b: []byte = null # the game's hello, sent again on every dial
net__hello_b: []byte = buffer(NET_MTU) # the game's hello, sent again on every dial
net__hello_l: int = 0
net__hello_t: float = 0.0 # seconds since it went; -1 not yet
net__wait_more: int = 0 # extra seconds a slow host is given (a map switch, a matchmaker)
@ -111,19 +111,15 @@ export function net_inbox(net_st: NetState) -> Queue<NetMessage> {
return net_st.net__inbox
}
function net__init(net_st: mut NetState) -> void {
if net_st.net__peers != null { return }
net_st.net__peers = new []NetPeer
function net__facts__new() -> Queue<NetFact> { return queue_new("net.facts") }
function net__inbox__new() -> Queue<NetMessage> { return queue_new("net.inbox") }
# every peer slot, each with its own queue
function net__peers_new() -> []NetPeer {
let out = new []NetPeer
for i in 0 .. NET_PEERS {
let p = new NetPeer
p.queue = new []NetMsg
push(net_st.net__peers, p)
push(out, p)
}
net_st.net__in = buffer(NET_MTU + 64)
net_st.net__out = buffer(NET_MTU + 64)
net_st.net__wb = buffer(NET_MTU)
net_st.net__rb = buffer(NET_MTU)
net_st.net__hello_b = buffer(NET_MTU)
return out
}
function net__facts__new() -> Queue<NetFact> { return queue_new("net.facts") }
function net__inbox__new() -> Queue<NetMessage> { return queue_new("net.inbox") }

View file

@ -1,7 +1,6 @@
# census.ludic - making one (a slot reused before a new one, a look, a name, a start on its own
# ground out of the players' sight where that can be had), a post's fixed person, and leaving
export function npc_spawn(npc_st: mut NpcState, kind: int) -> NpcPerson {
np_ensure()
if npc_st.np_n >= npc_st.np_max or kind < 0 or kind >= NPCK_COUNT { return null }
var p: NpcPerson = null
for i in 0 .. len(npc_st.np_walkers) {
@ -94,7 +93,6 @@ export function npc_set_at(p: NpcPerson, x: float, z: float) -> void {
# somebody fixed at a post (a vendor behind the counter): never walks, never counted, never leaves,
# turns to a player within its kind's notice and back to `yaw` when they go
export function npc_place(npc_st: mut NpcState, kind: int, x: float, z: float, yaw: float, name: string, look: int) -> NpcPerson {
np_ensure()
let p = new NpcPerson
p.slot = len(npc_st.np_posts)
np_fresh(p, kind, look)

View file

@ -3,7 +3,6 @@
# on: this machine's own walkers leave, and only the host's are drawn until it is turned off
export function npc_mirror(npc_st: mut NpcState, on: bool) -> void {
np_ensure()
if npc_st.np_mirror == on { return }
npc_st.np_mirror = on
for i in 0 .. len(npc_st.np_walkers) { npc_retire(npc_st, npc_st.np_walkers[i]) }
@ -14,7 +13,6 @@ export function npc_mirroring(npc_st: NpcState) -> bool { return npc_st.np_mirro
# the host's slot: somebody new in it (born), or nobody (gone)
export function npc_put(npc_st: mut NpcState, slot: int, id: int, kind: int, look: int, seed: float, name: string, hx: float, hz: float) -> NpcPerson {
np_ensure()
if slot < 0 or slot >= npc_st.np_max or kind < 0 or kind >= NPCK_COUNT { return null }
while len(npc_st.np_walkers) <= slot {
let q = new NpcPerson
@ -60,7 +58,6 @@ export function npc_heard(npc_st: NpcState, slot: int, x: float, y: float, z: fl
export function npc_heard_lately(npc_st: NpcState, p: NpcPerson) -> bool { return p.heard and npc_st.np_clock - p.heard_t <= 6.0 }
export function npc_mirror_tick(npc_st: mut NpcState, dt: float) -> void {
np_ensure()
npc_st.np_clock = npc_st.np_clock + dt
let k = Math.min(1.0, dt * 8.0)
for i in 0 .. len(npc_st.np_walkers) {

View file

@ -41,37 +41,30 @@ function np_fact(npc_st: NpcState, what: int, p: NpcPerson, player: int) -> NpcF
return f
}
function np_ensure() -> void {
}
# how many walkers the place can hold at once
export function npc_config(npc_st: mut NpcState, max: int) -> void { npc_st.np_max = max }
export function npc_seed(npc_st: NpcState, n: int) -> void {
np_ensure()
rng_seed(npc_st.np_dice, n)
}
# 0 .. 1, and whole seconds lo .. hi, off the package's dice
export function npc_rnd(npc_st: NpcState) -> float {
np_ensure()
return rng_float(npc_st.np_dice)
}
export function npc_between(npc_st: NpcState, lo: int, hi: int) -> int {
np_ensure()
return rng_between(npc_st.np_dice, lo, hi)
}
export function npc_secs(npc_st: NpcState, lo: int, hi: int) -> float { return float(npc_between(npc_st, lo, np_hi(lo, hi))) }
export function npc_walkers(npc_st: NpcState) -> []NpcPerson {
np_ensure()
return npc_st.np_walkers
}
export function npc_posts(npc_st: NpcState) -> []NpcPerson {
np_ensure()
return npc_st.np_posts
}
@ -81,19 +74,16 @@ export function npc_max(npc_st: NpcState) -> int { return npc_st.np_max }
# a walker's slot or a post, and anyone by id
export function npc_at(npc_st: NpcState, slot: int) -> NpcPerson {
np_ensure()
if slot < 0 or slot >= len(npc_st.np_walkers) { return null }
return npc_st.np_walkers[slot]
}
export function npc_post(npc_st: NpcState, i: int) -> NpcPerson {
np_ensure()
if i < 0 or i >= len(npc_st.np_posts) { return null }
return npc_st.np_posts[i]
}
export function npc_by_id(npc_st: NpcState, id: int) -> NpcPerson {
np_ensure()
for i in 0 .. len(npc_st.np_walkers) { if npc_st.np_walkers[i].active and npc_st.np_walkers[i].id == id { return npc_st.np_walkers[i] } }
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active and npc_st.np_posts[i].id == id { return npc_st.np_posts[i] } }
return null

View file

@ -1,7 +1,6 @@
# tick.ludic - a step of everyone, on the machine that runs the place: the census on the half hour,
# the day's end (only ever out of every player's sight), the walk or the act, and who noticed whom
export function npc_tick(npc_st: mut NpcState, dt: float) -> void {
np_ensure()
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(npc_st, npc_st.np_posts[i], dt) } }
if npc_st.np_hold or npc_st.np_mirror { return }
np_census(npc_st)
@ -15,7 +14,6 @@ export function npc_tick(npc_st: mut NpcState, dt: float) -> void {
# the posts alone: a machine that does not run the place still turns its vendors to whoever comes
export function npc_posts_tick(npc_st: mut NpcState, dt: float) -> void {
np_ensure()
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(npc_st, npc_st.np_posts[i], dt) } }
}
@ -82,7 +80,6 @@ export function npc_hold(npc_st: mut NpcState, on: bool) -> void { npc_st.np_hol
# everyone gone, as a new trip starts; the posts are the place's and stay
export function npc_reset(npc_st: mut NpcState) -> void {
np_ensure()
for i in 0 .. len(npc_st.np_walkers) { npc_retire(npc_st, npc_st.np_walkers[i]) }
npc_st.np_n = 0
npc_st.np_next_h = 0.0

View file

@ -11,18 +11,12 @@ const PREFS_MAX: int = 64
state PrefsState {
prefs_path: pointer = null
prefs_names: pointers = null
prefs_vals: words = null
prefs_names: pointers = pointers(PREFS_MAX)
prefs_vals: words = words(PREFS_MAX)
prefs_n: int = 0
}
namespace Prefs {
internal function init(prefs_st: mut PrefsState) -> void {
if prefs_st.prefs_names != null { return }
prefs_st.prefs_names = pointers(PREFS_MAX)
prefs_st.prefs_vals = words(PREFS_MAX)
prefs_st.prefs_n = 0
}
internal function find(prefs_st: PrefsState, key: pointer) -> int {
var i = 0
@ -31,7 +25,6 @@ namespace Prefs {
}
export function get(prefs_st: mut PrefsState, key: pointer, fallback: int) -> int {
init(prefs_st)
let i = find(prefs_st, key)
if i < 0 { return fallback }
return prefs_st.prefs_vals[i]
@ -45,7 +38,6 @@ namespace Prefs {
}
export function set(prefs_st: mut PrefsState, key: pointer, value: int) -> void {
init(prefs_st)
var i = find(prefs_st, key)
if i < 0 {
if prefs_st.prefs_n >= PREFS_MAX { return }
@ -89,7 +81,6 @@ namespace Prefs {
}
export function load(prefs_st: mut PrefsState, path: pointer) -> void {
init(prefs_st)
prefs_st.prefs_path = path
let buf = read_all(path)
if buf == null { return }
@ -109,7 +100,6 @@ namespace Prefs {
}
export function save(prefs_st: mut PrefsState) -> void {
init(prefs_st)
if prefs_st.prefs_path == null { return }
let f = file_open(prefs_st.prefs_path, "wb")
if f == null { return }

View file

@ -188,6 +188,10 @@ export state Render3dState {
gvk_al_spare: []int = null # ids to reuse
gvk_pool: long = 0
gvk_fence: bytes = null
gvk_frame_fence: bytes = null # the presented frame still on the GPU (one frame in flight)
gvk_frame_pending: bool = false
gvk_frame_pending_cb: pointer = null
gvk_inflight: int = -1 # -1 until asked: 1 leaves the presented frame in flight
gvk_has_hqr: bool = false
gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
gvk_qpool: long = 0

View file

@ -49,9 +49,10 @@ function gpu_select(render3d_st: mut Render3dState) -> int {
if render3d_st.gpu_wanted == 0 { render3d_st.gpu_wanted = GPU_GL }
render3d_st.gpu_kind = GPU_GL
if render3d_st.gpu_wanted == GPU_VK {
# a window needs a surface, and only the Win32 one is built
# a window needs a surface: Win32 on Windows, a CAMetalLayer through MoltenVK on macOS
render3d_st.gvk_want_surface = is_windowed()
if is_windowed() and Os.platform() != "windows" { render3d_st.gpu_fallback_reason = "the Vulkan window is built for Windows only" }
let os = Os.platform()
if is_windowed() and os != "windows" and os != "macos" { render3d_st.gpu_fallback_reason = `no Vulkan window is built for {os}` }
else if not gvk_init(render3d_st) { render3d_st.gpu_fallback_reason = render3d_st.gvk_why }
else if not gvk_manifest(render3d_st) { render3d_st.gpu_fallback_reason = "the renderer's SPIR-V manifest is missing" }
else { render3d_st.gpu_kind = GPU_VK }

View file

@ -43,6 +43,11 @@ function gvk_ext_in(props: bytes, n: int, want: string) -> bool {
# device with the Tier 1 floor switched on. False, with gvk_why set, if any of it is missing:
# the caller stays on OpenGL.
function gvk_surface_ext() -> string {
if Os.platform() == "macos" { return VK_EXT_METAL_SURFACE_EXTENSION_NAME }
return VK_KHR_WIN32_SURFACE_EXTENSION_NAME
}
function gvk_init(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_ready { return true }
gsl_boot(render3d_st)
@ -72,12 +77,13 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); n_iext += 1
Vk.put_i32(ici, VkInstanceCreateInfo_flags, VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR)
}
# a window's surface: only the Win32 one is built, so a window elsewhere stays on OpenGL
render3d_st.gvk_has_surface = render3d_st.gvk_want_surface and gvk_ext_in(iexts, nie, VK_KHR_SURFACE_EXTENSION_NAME) and gvk_ext_in(iexts, nie, VK_KHR_WIN32_SURFACE_EXTENSION_NAME)
if render3d_st.gvk_want_surface and not render3d_st.gvk_has_surface { render3d_st.gvk_why = "no Win32 surface in this Vulkan instance"; return false }
# a window's surface: Win32 on Windows, Metal (MoltenVK) on macOS
let surf_ext = gvk_surface_ext()
render3d_st.gvk_has_surface = render3d_st.gvk_want_surface and gvk_ext_in(iexts, nie, VK_KHR_SURFACE_EXTENSION_NAME) and gvk_ext_in(iexts, nie, surf_ext)
if render3d_st.gvk_want_surface and not render3d_st.gvk_has_surface { render3d_st.gvk_why = `no {surf_ext} in this Vulkan instance`; return false }
if render3d_st.gvk_has_surface {
Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_SURFACE_EXTENSION_NAME); n_iext += 1
Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_WIN32_SURFACE_EXTENSION_NAME); n_iext += 1
Vk.put_ptr(iext_names, n_iext * 8, surf_ext); n_iext += 1
}
# HDR output: an instance only lists the HDR colour spaces when it asks for them
render3d_st.gvk_has_colorspace = render3d_st.gvk_has_surface and gvk_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)
@ -488,6 +494,9 @@ function gvk_cmd_init(render3d_st: mut Render3dState) -> bool {
render3d_st.gvk_fence = bytes(8)
r = Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_fence)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateFence", r) }
render3d_st.gvk_frame_fence = bytes(8)
r = Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_frame_fence)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateFence", r) }
return true
}
# a command buffer, begun
@ -510,6 +519,7 @@ function gvk_once_begin(render3d_st: Render3dState) -> pointer {
}
# end it, submit it, wait for it, free it
function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool {
gvk_frame_wait(render3d_st)
var r = Vk.end_command_buffer(cb)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
let cbs = bytes(8)
@ -529,12 +539,61 @@ function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool {
return true
}
# ---- one frame in flight -------------------------------------------------------------------
# A window's frame is submitted and presented without waiting, so the game's next tick runs on the
# CPU while the GPU draws: waiting at the present serialised the two, which cost MoltenVK a third
# of its frame rate against OpenGL. Anything that would reuse what that frame reads - the next
# frame's command buffer and ring, a one-off submit, a descriptor pool reset, a swapchain rebuild -
# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). On by default
# on macOS; R3D_VK_INFLIGHT=0/1 says otherwise.
function gvk_inflight_on(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_inflight < 0 {
render3d_st.gvk_inflight = 0
if Os.platform() == "macos" { render3d_st.gvk_inflight = 1 }
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
}
function gvk_frame_submit(render3d_st: mut Render3dState, cb: pointer) -> bool {
gvk_frame_wait(render3d_st)
var r = Vk.end_command_buffer(cb)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
let cbs = bytes(8)
Vk.put_ptr(cbs, 0, cb)
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence)
let si = bytes(VkSubmitInfo_sizeof)
Vk.zero(si, VkSubmitInfo_sizeof)
Vk.put_i32(si, VkSubmitInfo_sType, VK_STRUCTURE_TYPE_SUBMIT_INFO)
Vk.put_i32(si, VkSubmitInfo_commandBufferCount, 1)
Vk.put_ptr(si, VkSubmitInfo_pCommandBuffers, cbs)
r = Vk.queue_submit(render3d_st.gvk_queue, 1, si, Vk.get_i64(render3d_st.gvk_frame_fence, 0))
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkQueueSubmit", r) }
render3d_st.gvk_frame_pending = true
render3d_st.gvk_frame_pending_cb = cb
return true
}
function gvk_frame_wait(render3d_st: mut Render3dState) -> void {
if not render3d_st.gvk_frame_pending { return }
let forever: long = -1
Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence, 1, forever)
let cbs = bytes(8)
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)
render3d_st.gvk_frame_pending = false
render3d_st.gvk_frame_pending_cb = null
gvk_retire_flush(render3d_st)
}
# ---- teardown -----------------------------------------------------------------------------
function gvk_shutdown(render3d_st: mut Render3dState) -> void {
if not render3d_st.gvk_ready { return }
gvk_frame_wait(render3d_st)
Vk.device_wait_idle(render3d_st.gvk_dev)
gsl_shutdown(render3d_st)
Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_fence, 0), null)
Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_frame_fence, 0), null)
Vk.destroy_command_pool(render3d_st.gvk_dev, render3d_st.gvk_pool, null)
Vk.destroy_device(render3d_st.gvk_dev, null)
Vk.destroy_instance(render3d_st.gvk_inst, null)

View file

@ -729,6 +729,7 @@ function gvk_sc_clear(render3d_st: mut Render3dState) -> void {
# the pool is full: finish the frame recorded so far (it may use sets from it), then start again
function gvk_kpool_reset(render3d_st: mut Render3dState) -> void {
gvk_flush(render3d_st)
gvk_frame_wait(render3d_st)
Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_kpool, 0)
gvk_sc_clear(render3d_st)
}
@ -953,6 +954,7 @@ function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool
function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer {
if render3d_st.gvk_cb == null {
gvk_frame_wait(render3d_st)
gvk_frame_reset(render3d_st)
render3d_st.gvk_cb = gvk_once_begin(render3d_st)
}
@ -1279,7 +1281,13 @@ function gvk_flush(render3d_st: mut Render3dState) -> void {
# - and that image is presented.
function gvk_present(render3d_st: mut Render3dState) -> void {
gvk_prof_frame(render3d_st)
if render3d_st.gvk_swap != 0 { gvk_present_window(render3d_st); render3d_st.gvk_frame_no += 1; gvk_retire_flush(render3d_st); return }
if render3d_st.gvk_swap != 0 {
gvk_present_window(render3d_st)
render3d_st.gvk_frame_no += 1
# what the frame in flight retired goes when it is done (gvk_frame_wait)
if not render3d_st.gvk_frame_pending { gvk_retire_flush(render3d_st) }
return
}
if render3d_st.gvk_cb == null { return }
gvk_pass_end(render3d_st)
gvk_once_end(render3d_st, render3d_st.gvk_cb)
@ -1332,6 +1340,8 @@ function gvk_open(render3d_st: mut Render3dState, w: int, h: int, title: string)
if is_windowed() {
win_open(w, h, 1, title) # the window exists before main: this retitles it
win_gl_resize(w, h)
# macOS: the Metal layer sets the backing scale, so it exists before the drawable is measured
if Os.platform() == "macos" and win_metal_layer() == null { render3d_st.gvk_why = "no Metal layer on the window"; return false }
if render3d_st.gvk_size_buf == null { render3d_st.gvk_size_buf = words(4) }
win_gl_drawable(render3d_st.gvk_size_buf)
if render3d_st.gvk_size_buf[0] > 0 and render3d_st.gvk_size_buf[1] > 0 { gl_set_drawable(render3d_st.gvk_size_buf[0], render3d_st.gvk_size_buf[1]) }
@ -1519,13 +1529,24 @@ function gvk_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: po
}
# ---- the window: surface and swapchain ----------------------------------------------------------
# Windows only for now (the Win32 surface); gpu_select keeps a window elsewhere on OpenGL.
# The Win32 surface on Windows; on macOS a CAMetalLayer the runtime hangs off the view (MoltenVK).
extern function win_native_window() -> pointer = "win_native_window"
extern function win_native_instance() -> pointer = "win_native_instance"
extern function win_metal_layer() -> pointer = "win_metal_layer"
extern function win_visible() -> int = "win_visible"
function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_surface != 0 { return true }
function gvk_surface_os(render3d_st: mut Render3dState, out: pointer) -> bool {
if Os.platform() == "macos" {
let layer = win_metal_layer()
if layer == null { render3d_st.gvk_why = "no Metal layer on the window"; return false }
let mci = bytes(VkMetalSurfaceCreateInfoEXT_sizeof)
Vk.zero(mci, VkMetalSurfaceCreateInfoEXT_sizeof)
Vk.put_i32(mci, VkMetalSurfaceCreateInfoEXT_sType, VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT)
Vk.put_ptr(mci, VkMetalSurfaceCreateInfoEXT_pLayer, layer)
let rm = Vk.create_metal_surface_ext(render3d_st.gvk_inst, mci, null, out)
if rm != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateMetalSurfaceEXT", rm) }
return true
}
let hwnd = win_native_window()
if hwnd == null { render3d_st.gvk_why = "no window to present to"; return false }
let sci = bytes(VkWin32SurfaceCreateInfoKHR_sizeof)
@ -1533,9 +1554,15 @@ function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
Vk.put_i32(sci, VkWin32SurfaceCreateInfoKHR_sType, VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR)
Vk.put_ptr(sci, VkWin32SurfaceCreateInfoKHR_hinstance, win_native_instance())
Vk.put_ptr(sci, VkWin32SurfaceCreateInfoKHR_hwnd, hwnd)
let out = bytes(8)
let r = Vk.create_win32_surface_khr(render3d_st.gvk_inst, sci, null, out)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateWin32SurfaceKHR", r) }
return true
}
function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_surface != 0 { return true }
let out = bytes(8)
if not gvk_surface_os(render3d_st, out) { return false }
render3d_st.gvk_surface = gvk_handle(out)
let ok = bytes(4)
Vk.put_i32(ok, 0, 0)
@ -1551,6 +1578,7 @@ function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
# (Re)make the swapchain for a w x h client area. The old one is handed over and then destroyed.
function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
if not gvk_surface_make(render3d_st) { return false }
gvk_frame_wait(render3d_st)
Vk.device_wait_idle(render3d_st.gvk_dev)
let caps = bytes(VkSurfaceCapabilitiesKHR_sizeof)
Vk.get_physical_device_surface_capabilities_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, caps)
@ -1641,6 +1669,14 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
function gvk_present_window(render3d_st: mut Render3dState) -> void {
let cb = gvk_frame_cb(render3d_st)
gvk_pass_end(render3d_st)
# a covered window is not drawn to: its layer would hold the frame for a drawable the compositor
# hands back once a second. The frame still runs, paced at about 60 Hz, so the game keeps time.
if win_visible() == 0 {
if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) }
render3d_st.gvk_cb = null
Time.sleep_us(16000)
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 }
let idx = bytes(4)
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence)
@ -1670,7 +1706,7 @@ function gvk_present_window(render3d_st: mut Render3dState) -> void {
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(cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR)
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
let pi = bytes(VkPresentInfoKHR_sizeof)
Vk.zero(pi, VkPresentInfoKHR_sizeof)

View file

@ -134,6 +134,7 @@ function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> poin
return Vk.get_ptr(pp, 0)
}
function gvk_staging_free(render3d_st: mut Render3dState) -> void {
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.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, null)
Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, null)
@ -142,6 +143,7 @@ function gvk_staging_free(render3d_st: mut Render3dState) -> void {
# (Re)make the image behind a handle: glTexImage2D on a texture that already has one replaces it.
function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, ifmt: int, w: int, h: int, layers: int, with_mips: bool) -> bool {
gvk_frame_wait(render3d_st) # the frame in flight may still read it
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) { return false }
let vkfmt = gvk_format(ifmt)
if vkfmt == VK_FORMAT_UNDEFINED { print(`r3d: vulkan: no image format for GL format {ifmt}`); return false }
@ -304,6 +306,7 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
# A target made without pixels has one level; the renderer asking it for mipmaps (the exposure
# measure reads the HDR scene's smallest level every frame) grows it a full chain, level 0 kept.
function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> bool {
gvk_frame_wait(render3d_st) # the frame in flight may still read it
let old_image = render3d_st.gvk_tex_image[tex]
let old_view = render3d_st.gvk_tex_view[tex]
let old_mem = render3d_st.gvk_tex_mem[tex]
@ -420,6 +423,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
}
function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void {
gvk_frame_wait(render3d_st) # the frame in flight may still read it
if render3d_st.gvk_tex_image[tex] == 0 { return }
let zero: long = 0
Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], null)
@ -553,8 +557,8 @@ function gvk_buf_new(render3d_st: mut Render3dState) -> int {
function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
if render3d_st.gvk_buf[b] == 0 { return }
let zero: long = 0
if render3d_st.gvk_buf_used[b] == render3d_st.gvk_frame_no {
# a draw recorded this frame still reads it: destroy it once the frame has been submitted
if gvk_buf_busy(render3d_st, b) {
# a draw recorded this frame (or the frame in flight) still reads it: destroy it once the frame has been submitted
push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b])
} else {
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null)
@ -562,6 +566,12 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
}
render3d_st.gvk_buf[b] = zero; render3d_st.gvk_buf_mem[b] = zero; render3d_st.gvk_buf_size[b] = 0; render3d_st.gvk_buf_map[b] = null; render3d_st.gvk_buf_used[b] = 0
}
# read by a draw recorded this frame, or by the presented frame still on the GPU
function gvk_buf_busy(render3d_st: Render3dState, b: int) -> bool {
let u = render3d_st.gvk_buf_used[b]
return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_no - 1)
}
# the frame's submitted work is done: storage retired during it can go
function gvk_retire_flush(render3d_st: mut Render3dState) -> void {
if render3d_st.gvk_retired_buf == null { return }
@ -587,7 +597,7 @@ function gvk_buf_is_gpu(render3d_st: Render3dState, b: int) -> bool { return ren
function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool {
if b <= 0 or b >= len(render3d_st.gvk_buf) { return false }
# already big enough, and no draw this frame reads what is there: fill it in place
if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n and (render3d_st.gvk_buf_used[b] != render3d_st.gvk_frame_no or gvk_buf_is_gpu(render3d_st, b)) { return true }
if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n and (not gvk_buf_busy(render3d_st, b) or gvk_buf_is_gpu(render3d_st, b)) { return true }
gvk_buf_release(render3d_st, b)
var size = n
if size < 64 { size = 64 }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -141,6 +141,56 @@ T c57b4f14 u_shadow 5
T c57b4f14 u_src 6
T c57b4f14 u_tershadow 7
T c57b4f14 u_ts_height 8
P 5161e6ba fullscreen.vert dof.frag
B 5161e6ba frag 1 916
U 5161e6ba frag u_cascade_vp 0 mat4 5 64
U 5161e6ba frag u_cascade_split 320 float 5 16
U 5161e6ba frag u_cascade_range 400 float 5 16
U 5161e6ba frag u_cascade_texel 480 float 5 16
U 5161e6ba frag u_sun_dir 560 vec3 1 0
U 5161e6ba frag u_sun_color 576 vec3 1 0
U 5161e6ba frag u_cam_pos 592 vec3 1 0
U 5161e6ba frag u_prefilter_levels 604 float 1 0
U 5161e6ba frag u_fog_density 608 float 1 0
U 5161e6ba frag u_fog_height_falloff 612 float 1 0
U 5161e6ba frag u_fog_base 616 float 1 0
U 5161e6ba frag u_clip_y 620 float 1 0
U 5161e6ba frag u_spec_scale 624 float 1 0
U 5161e6ba frag u_sky_rot 632 vec2 1 0
U 5161e6ba frag u_ground_alb 640 vec3 1 0
U 5161e6ba frag u_ground_alb_hi 656 vec3 1 0
U 5161e6ba frag u_ground_hi_y 668 float 1 0
U 5161e6ba frag u_ground_hi_w 672 float 1 0
U 5161e6ba frag u_ibl_scale 688 vec3 1 0
U 5161e6ba frag u_daylight 700 float 1 0
U 5161e6ba frag u_fire_pos 704 vec3 1 0
U 5161e6ba frag u_fire_color 720 vec3 1 0
U 5161e6ba frag u_hand_pos 736 vec3 1 0
U 5161e6ba frag u_hand_color 752 vec3 1 0
U 5161e6ba frag u_hand_dir 768 vec3 1 0
U 5161e6ba frag u_hand_cone 780 float 1 0
U 5161e6ba frag u_hand_reach 784 float 1 0
U 5161e6ba frag u_ts_origin 792 vec2 1 0
U 5161e6ba frag u_ts_half 800 float 1 0
U 5161e6ba frag u_ts_on 804 float 1 0
U 5161e6ba frag u_force_cascade 808 int 1 0
U 5161e6ba frag u_cloud_shadow 812 float 1 0
U 5161e6ba frag u_time 816 float 1 0
U 5161e6ba frag u_fog_inscatter 820 float 1 0
U 5161e6ba frag u_fog_desat 824 float 1 0
U 5161e6ba frag u_inv_proj 832 mat4 1 0
U 5161e6ba frag u_texel 896 vec2 1 0
U 5161e6ba frag u_focus 904 float 1 0
U 5161e6ba frag u_aperture 908 float 1 0
U 5161e6ba frag u_max_coc 912 float 1 0
T 5161e6ba u_brdf 2
T 5161e6ba u_depth 3
T 5161e6ba u_irradiance 4
T 5161e6ba u_prefilter 5
T 5161e6ba u_shadow 6
T 5161e6ba u_src 7
T 5161e6ba u_tershadow 8
T 5161e6ba u_ts_height 9
P 4be7acd9 fullscreen.vert heightgen.frag #define DEM;
B 4be7acd9 frag 1 920
U 4be7acd9 frag u_cascade_vp 0 mat4 5 64
@ -740,6 +790,58 @@ T a4683767 u_prefilter 8
T a4683767 u_shadow 9
T a4683767 u_tershadow 10
T a4683767 u_ts_height 11
P 74258189 fullscreen.vert volumetric.frag
B 74258189 frag 1 924
U 74258189 frag u_cascade_vp 0 mat4 5 64
U 74258189 frag u_cascade_split 320 float 5 16
U 74258189 frag u_cascade_range 400 float 5 16
U 74258189 frag u_cascade_texel 480 float 5 16
U 74258189 frag u_sun_dir 560 vec3 1 0
U 74258189 frag u_sun_color 576 vec3 1 0
U 74258189 frag u_cam_pos 592 vec3 1 0
U 74258189 frag u_prefilter_levels 604 float 1 0
U 74258189 frag u_fog_density 608 float 1 0
U 74258189 frag u_fog_height_falloff 612 float 1 0
U 74258189 frag u_fog_base 616 float 1 0
U 74258189 frag u_clip_y 620 float 1 0
U 74258189 frag u_spec_scale 624 float 1 0
U 74258189 frag u_sky_rot 632 vec2 1 0
U 74258189 frag u_ground_alb 640 vec3 1 0
U 74258189 frag u_ground_alb_hi 656 vec3 1 0
U 74258189 frag u_ground_hi_y 668 float 1 0
U 74258189 frag u_ground_hi_w 672 float 1 0
U 74258189 frag u_ibl_scale 688 vec3 1 0
U 74258189 frag u_daylight 700 float 1 0
U 74258189 frag u_fire_pos 704 vec3 1 0
U 74258189 frag u_fire_color 720 vec3 1 0
U 74258189 frag u_hand_pos 736 vec3 1 0
U 74258189 frag u_hand_color 752 vec3 1 0
U 74258189 frag u_hand_dir 768 vec3 1 0
U 74258189 frag u_hand_cone 780 float 1 0
U 74258189 frag u_hand_reach 784 float 1 0
U 74258189 frag u_ts_origin 792 vec2 1 0
U 74258189 frag u_ts_half 800 float 1 0
U 74258189 frag u_ts_on 804 float 1 0
U 74258189 frag u_force_cascade 808 int 1 0
U 74258189 frag u_cloud_shadow 812 float 1 0
U 74258189 frag u_time 816 float 1 0
U 74258189 frag u_fog_inscatter 820 float 1 0
U 74258189 frag u_fog_desat 824 float 1 0
U 74258189 frag u_inv_vp 832 mat4 1 0
U 74258189 frag u_vol_steps 896 float 1 0
U 74258189 frag u_vol_density 900 float 1 0
U 74258189 frag u_vol_falloff 904 float 1 0
U 74258189 frag u_vol_far 908 float 1 0
U 74258189 frag u_vol_g 912 float 1 0
U 74258189 frag u_vol_mist 916 float 1 0
U 74258189 frag u_vol_mist_h 920 float 1 0
T 74258189 u_brdf 2
T 74258189 u_depth 3
T 74258189 u_irradiance 4
T 74258189 u_prefilter 5
T 74258189 u_shadow 6
T 74258189 u_tershadow 7
T 74258189 u_ts_height 8
P d48d6a48 grass.mesh model.frag #define FOLIAGE;#define BLADE;#define MESH;
B d48d6a48 vert 0 4384
U d48d6a48 vert u_view 0 mat4 1 0
@ -1355,6 +1457,68 @@ T bfa15301 u_prefilter 5
T bfa15301 u_shadow 6
T bfa15301 u_tershadow 7
T bfa15301 u_ts_height 8
P 266d2401 model.vert depth.frag #define FOLIAGE;#define WIND;#define ALPHA_TEST;#define NEAR_FADE;
B 266d2401 vert 0 236
U 266d2401 vert u_view 0 mat4 1 0
U 266d2401 vert u_proj 64 mat4 1 0
U 266d2401 vert u_light_vp 128 mat4 1 0
U 266d2401 vert u_time 192 float 1 0
U 266d2401 vert u_wind 196 float 1 0
U 266d2401 vert u_card_w 200 float 1 0
U 266d2401 vert u_card_h 204 float 1 0
U 266d2401 vert u_ground 208 float 1 0
U 266d2401 vert u_ts_origin 216 vec2 1 0
U 266d2401 vert u_ts_half 224 float 1 0
U 266d2401 vert u_model_h 228 float 1 0
U 266d2401 vert u_flutter 232 float 1 0
I 266d2401 i_rot 4 vec4
I 266d2401 a_pos 0 vec3
I 266d2401 i_pos 3 vec4
I 266d2401 a_nrm 1 vec3
I 266d2401 a_uv 2 vec2
B 266d2401 frag 1 828
U 266d2401 frag u_cascade_vp 0 mat4 5 64
U 266d2401 frag u_cascade_split 320 float 5 16
U 266d2401 frag u_cascade_range 400 float 5 16
U 266d2401 frag u_cascade_texel 480 float 5 16
U 266d2401 frag u_sun_dir 560 vec3 1 0
U 266d2401 frag u_sun_color 576 vec3 1 0
U 266d2401 frag u_cam_pos 592 vec3 1 0
U 266d2401 frag u_prefilter_levels 604 float 1 0
U 266d2401 frag u_fog_density 608 float 1 0
U 266d2401 frag u_fog_height_falloff 612 float 1 0
U 266d2401 frag u_fog_base 616 float 1 0
U 266d2401 frag u_clip_y 620 float 1 0
U 266d2401 frag u_spec_scale 624 float 1 0
U 266d2401 frag u_sky_rot 632 vec2 1 0
U 266d2401 frag u_ground_alb 640 vec3 1 0
U 266d2401 frag u_ground_alb_hi 656 vec3 1 0
U 266d2401 frag u_ground_hi_y 668 float 1 0
U 266d2401 frag u_ground_hi_w 672 float 1 0
U 266d2401 frag u_ibl_scale 688 vec3 1 0
U 266d2401 frag u_daylight 700 float 1 0
U 266d2401 frag u_fire_pos 704 vec3 1 0
U 266d2401 frag u_fire_color 720 vec3 1 0
U 266d2401 frag u_hand_pos 736 vec3 1 0
U 266d2401 frag u_hand_color 752 vec3 1 0
U 266d2401 frag u_hand_dir 768 vec3 1 0
U 266d2401 frag u_hand_cone 780 float 1 0
U 266d2401 frag u_hand_reach 784 float 1 0
U 266d2401 frag u_ts_origin 792 vec2 1 0
U 266d2401 frag u_ts_half 800 float 1 0
U 266d2401 frag u_ts_on 804 float 1 0
U 266d2401 frag u_force_cascade 808 int 1 0
U 266d2401 frag u_cloud_shadow 812 float 1 0
U 266d2401 frag u_time 816 float 1 0
U 266d2401 frag u_fog_inscatter 820 float 1 0
U 266d2401 frag u_fog_desat 824 float 1 0
T 266d2401 u_brdf 2
T 266d2401 u_diff 3
T 266d2401 u_irradiance 4
T 266d2401 u_prefilter 5
T 266d2401 u_shadow 6
T 266d2401 u_tershadow 7
T 266d2401 u_ts_height 8
P 01089da8 model.vert model.frag
B 01089da8 vert 0 236
U 01089da8 vert u_view 0 mat4 1 0
@ -1566,6 +1730,76 @@ T 1ce5c5d1 u_prefilter 8
T 1ce5c5d1 u_shadow 9
T 1ce5c5d1 u_tershadow 10
T 1ce5c5d1 u_ts_height 11
P 5e70d731 model.vert model.frag #define FOLIAGE;#define WIND;#define ALPHA_TEST;#define NEAR_FADE;
B 5e70d731 vert 0 236
U 5e70d731 vert u_view 0 mat4 1 0
U 5e70d731 vert u_proj 64 mat4 1 0
U 5e70d731 vert u_light_vp 128 mat4 1 0
U 5e70d731 vert u_time 192 float 1 0
U 5e70d731 vert u_wind 196 float 1 0
U 5e70d731 vert u_card_w 200 float 1 0
U 5e70d731 vert u_card_h 204 float 1 0
U 5e70d731 vert u_ground 208 float 1 0
U 5e70d731 vert u_ts_origin 216 vec2 1 0
U 5e70d731 vert u_ts_half 224 float 1 0
U 5e70d731 vert u_model_h 228 float 1 0
U 5e70d731 vert u_flutter 232 float 1 0
I 5e70d731 i_rot 4 vec4
I 5e70d731 a_pos 0 vec3
I 5e70d731 i_pos 3 vec4
I 5e70d731 a_nrm 1 vec3
I 5e70d731 a_uv 2 vec2
B 5e70d731 frag 1 916
U 5e70d731 frag u_cascade_vp 0 mat4 5 64
U 5e70d731 frag u_cascade_split 320 float 5 16
U 5e70d731 frag u_cascade_range 400 float 5 16
U 5e70d731 frag u_cascade_texel 480 float 5 16
U 5e70d731 frag u_sun_dir 560 vec3 1 0
U 5e70d731 frag u_sun_color 576 vec3 1 0
U 5e70d731 frag u_cam_pos 592 vec3 1 0
U 5e70d731 frag u_prefilter_levels 604 float 1 0
U 5e70d731 frag u_fog_density 608 float 1 0
U 5e70d731 frag u_fog_height_falloff 612 float 1 0
U 5e70d731 frag u_fog_base 616 float 1 0
U 5e70d731 frag u_clip_y 620 float 1 0
U 5e70d731 frag u_spec_scale 624 float 1 0
U 5e70d731 frag u_sky_rot 632 vec2 1 0
U 5e70d731 frag u_ground_alb 640 vec3 1 0
U 5e70d731 frag u_ground_alb_hi 656 vec3 1 0
U 5e70d731 frag u_ground_hi_y 668 float 1 0
U 5e70d731 frag u_ground_hi_w 672 float 1 0
U 5e70d731 frag u_ibl_scale 688 vec3 1 0
U 5e70d731 frag u_daylight 700 float 1 0
U 5e70d731 frag u_fire_pos 704 vec3 1 0
U 5e70d731 frag u_fire_color 720 vec3 1 0
U 5e70d731 frag u_hand_pos 736 vec3 1 0
U 5e70d731 frag u_hand_color 752 vec3 1 0
U 5e70d731 frag u_hand_dir 768 vec3 1 0
U 5e70d731 frag u_hand_cone 780 float 1 0
U 5e70d731 frag u_hand_reach 784 float 1 0
U 5e70d731 frag u_ts_origin 792 vec2 1 0
U 5e70d731 frag u_ts_half 800 float 1 0
U 5e70d731 frag u_ts_on 804 float 1 0
U 5e70d731 frag u_force_cascade 808 int 1 0
U 5e70d731 frag u_cloud_shadow 812 float 1 0
U 5e70d731 frag u_time 816 float 1 0
U 5e70d731 frag u_fog_inscatter 820 float 1 0
U 5e70d731 frag u_fog_desat 824 float 1 0
U 5e70d731 frag u_model_h 828 float 1 0
U 5e70d731 frag u_view 832 mat4 1 0
U 5e70d731 frag u_tint 896 vec3 1 0
U 5e70d731 frag u_rough_scale 908 float 1 0
U 5e70d731 frag u_emissive 912 float 1 0
T 5e70d731 u_arm 2
T 5e70d731 u_blade_tex 3
T 5e70d731 u_brdf 4
T 5e70d731 u_diff 5
T 5e70d731 u_irradiance 6
T 5e70d731 u_nrm 7
T 5e70d731 u_prefilter 8
T 5e70d731 u_shadow 9
T 5e70d731 u_tershadow 10
T 5e70d731 u_ts_height 11
P f4a2b586 model.vert model.frag #define FOLIAGE;#define WIND;#define BLADE;
B f4a2b586 vert 0 252
U f4a2b586 vert u_view 0 mat4 1 0
@ -1783,6 +2017,76 @@ T 4dda406a u_prefilter 8
T 4dda406a u_shadow 9
T 4dda406a u_tershadow 10
T 4dda406a u_ts_height 11
P b603a056 model.vert model.frag #define FOLIAGE;#define WIND;#define EQ_PASS;#define NEAR_FADE;
B b603a056 vert 0 236
U b603a056 vert u_view 0 mat4 1 0
U b603a056 vert u_proj 64 mat4 1 0
U b603a056 vert u_light_vp 128 mat4 1 0
U b603a056 vert u_time 192 float 1 0
U b603a056 vert u_wind 196 float 1 0
U b603a056 vert u_card_w 200 float 1 0
U b603a056 vert u_card_h 204 float 1 0
U b603a056 vert u_ground 208 float 1 0
U b603a056 vert u_ts_origin 216 vec2 1 0
U b603a056 vert u_ts_half 224 float 1 0
U b603a056 vert u_model_h 228 float 1 0
U b603a056 vert u_flutter 232 float 1 0
I b603a056 i_rot 4 vec4
I b603a056 a_pos 0 vec3
I b603a056 i_pos 3 vec4
I b603a056 a_nrm 1 vec3
I b603a056 a_uv 2 vec2
B b603a056 frag 1 916
U b603a056 frag u_cascade_vp 0 mat4 5 64
U b603a056 frag u_cascade_split 320 float 5 16
U b603a056 frag u_cascade_range 400 float 5 16
U b603a056 frag u_cascade_texel 480 float 5 16
U b603a056 frag u_sun_dir 560 vec3 1 0
U b603a056 frag u_sun_color 576 vec3 1 0
U b603a056 frag u_cam_pos 592 vec3 1 0
U b603a056 frag u_prefilter_levels 604 float 1 0
U b603a056 frag u_fog_density 608 float 1 0
U b603a056 frag u_fog_height_falloff 612 float 1 0
U b603a056 frag u_fog_base 616 float 1 0
U b603a056 frag u_clip_y 620 float 1 0
U b603a056 frag u_spec_scale 624 float 1 0
U b603a056 frag u_sky_rot 632 vec2 1 0
U b603a056 frag u_ground_alb 640 vec3 1 0
U b603a056 frag u_ground_alb_hi 656 vec3 1 0
U b603a056 frag u_ground_hi_y 668 float 1 0
U b603a056 frag u_ground_hi_w 672 float 1 0
U b603a056 frag u_ibl_scale 688 vec3 1 0
U b603a056 frag u_daylight 700 float 1 0
U b603a056 frag u_fire_pos 704 vec3 1 0
U b603a056 frag u_fire_color 720 vec3 1 0
U b603a056 frag u_hand_pos 736 vec3 1 0
U b603a056 frag u_hand_color 752 vec3 1 0
U b603a056 frag u_hand_dir 768 vec3 1 0
U b603a056 frag u_hand_cone 780 float 1 0
U b603a056 frag u_hand_reach 784 float 1 0
U b603a056 frag u_ts_origin 792 vec2 1 0
U b603a056 frag u_ts_half 800 float 1 0
U b603a056 frag u_ts_on 804 float 1 0
U b603a056 frag u_force_cascade 808 int 1 0
U b603a056 frag u_cloud_shadow 812 float 1 0
U b603a056 frag u_time 816 float 1 0
U b603a056 frag u_fog_inscatter 820 float 1 0
U b603a056 frag u_fog_desat 824 float 1 0
U b603a056 frag u_model_h 828 float 1 0
U b603a056 frag u_view 832 mat4 1 0
U b603a056 frag u_tint 896 vec3 1 0
U b603a056 frag u_rough_scale 908 float 1 0
U b603a056 frag u_emissive 912 float 1 0
T b603a056 u_arm 2
T b603a056 u_blade_tex 3
T b603a056 u_brdf 4
T b603a056 u_diff 5
T b603a056 u_irradiance 6
T b603a056 u_nrm 7
T b603a056 u_prefilter 8
T b603a056 u_shadow 9
T b603a056 u_tershadow 10
T b603a056 u_ts_height 11
P 3b183d8f model.vert model.frag #define FOLIAGE;#define WIND;#define FLOWER;
B 3b183d8f vert 0 236
U 3b183d8f vert u_view 0 mat4 1 0
@ -3218,105 +3522,3 @@ T 46e2325c u_scene 7
T 46e2325c u_shadow 8
T 46e2325c u_tershadow 9
T 46e2325c u_ts_height 10
P 74258189 fullscreen.vert volumetric.frag
B 74258189 frag 1 924
U 74258189 frag u_cascade_vp 0 mat4 5 64
U 74258189 frag u_cascade_split 320 float 5 16
U 74258189 frag u_cascade_range 400 float 5 16
U 74258189 frag u_cascade_texel 480 float 5 16
U 74258189 frag u_sun_dir 560 vec3 1 0
U 74258189 frag u_sun_color 576 vec3 1 0
U 74258189 frag u_cam_pos 592 vec3 1 0
U 74258189 frag u_prefilter_levels 604 float 1 0
U 74258189 frag u_fog_density 608 float 1 0
U 74258189 frag u_fog_height_falloff 612 float 1 0
U 74258189 frag u_fog_base 616 float 1 0
U 74258189 frag u_clip_y 620 float 1 0
U 74258189 frag u_spec_scale 624 float 1 0
U 74258189 frag u_sky_rot 632 vec2 1 0
U 74258189 frag u_ground_alb 640 vec3 1 0
U 74258189 frag u_ground_alb_hi 656 vec3 1 0
U 74258189 frag u_ground_hi_y 668 float 1 0
U 74258189 frag u_ground_hi_w 672 float 1 0
U 74258189 frag u_ibl_scale 688 vec3 1 0
U 74258189 frag u_daylight 700 float 1 0
U 74258189 frag u_fire_pos 704 vec3 1 0
U 74258189 frag u_fire_color 720 vec3 1 0
U 74258189 frag u_hand_pos 736 vec3 1 0
U 74258189 frag u_hand_color 752 vec3 1 0
U 74258189 frag u_hand_dir 768 vec3 1 0
U 74258189 frag u_hand_cone 780 float 1 0
U 74258189 frag u_hand_reach 784 float 1 0
U 74258189 frag u_ts_origin 792 vec2 1 0
U 74258189 frag u_ts_half 800 float 1 0
U 74258189 frag u_ts_on 804 float 1 0
U 74258189 frag u_force_cascade 808 int 1 0
U 74258189 frag u_cloud_shadow 812 float 1 0
U 74258189 frag u_time 816 float 1 0
U 74258189 frag u_fog_inscatter 820 float 1 0
U 74258189 frag u_fog_desat 824 float 1 0
U 74258189 frag u_inv_vp 832 mat4 1 0
U 74258189 frag u_vol_steps 896 float 1 0
U 74258189 frag u_vol_density 900 float 1 0
U 74258189 frag u_vol_falloff 904 float 1 0
U 74258189 frag u_vol_far 908 float 1 0
U 74258189 frag u_vol_g 912 float 1 0
U 74258189 frag u_vol_mist 916 float 1 0
U 74258189 frag u_vol_mist_h 920 float 1 0
T 74258189 u_brdf 2
T 74258189 u_depth 3
T 74258189 u_irradiance 4
T 74258189 u_prefilter 5
T 74258189 u_shadow 6
T 74258189 u_tershadow 7
T 74258189 u_ts_height 8
P 5161e6ba fullscreen.vert dof.frag
B 5161e6ba frag 1 916
U 5161e6ba frag u_cascade_vp 0 mat4 5 64
U 5161e6ba frag u_cascade_split 320 float 5 16
U 5161e6ba frag u_cascade_range 400 float 5 16
U 5161e6ba frag u_cascade_texel 480 float 5 16
U 5161e6ba frag u_sun_dir 560 vec3 1 0
U 5161e6ba frag u_sun_color 576 vec3 1 0
U 5161e6ba frag u_cam_pos 592 vec3 1 0
U 5161e6ba frag u_prefilter_levels 604 float 1 0
U 5161e6ba frag u_fog_density 608 float 1 0
U 5161e6ba frag u_fog_height_falloff 612 float 1 0
U 5161e6ba frag u_fog_base 616 float 1 0
U 5161e6ba frag u_clip_y 620 float 1 0
U 5161e6ba frag u_spec_scale 624 float 1 0
U 5161e6ba frag u_sky_rot 632 vec2 1 0
U 5161e6ba frag u_ground_alb 640 vec3 1 0
U 5161e6ba frag u_ground_alb_hi 656 vec3 1 0
U 5161e6ba frag u_ground_hi_y 668 float 1 0
U 5161e6ba frag u_ground_hi_w 672 float 1 0
U 5161e6ba frag u_ibl_scale 688 vec3 1 0
U 5161e6ba frag u_daylight 700 float 1 0
U 5161e6ba frag u_fire_pos 704 vec3 1 0
U 5161e6ba frag u_fire_color 720 vec3 1 0
U 5161e6ba frag u_hand_pos 736 vec3 1 0
U 5161e6ba frag u_hand_color 752 vec3 1 0
U 5161e6ba frag u_hand_dir 768 vec3 1 0
U 5161e6ba frag u_hand_cone 780 float 1 0
U 5161e6ba frag u_hand_reach 784 float 1 0
U 5161e6ba frag u_ts_origin 792 vec2 1 0
U 5161e6ba frag u_ts_half 800 float 1 0
U 5161e6ba frag u_ts_on 804 float 1 0
U 5161e6ba frag u_force_cascade 808 int 1 0
U 5161e6ba frag u_cloud_shadow 812 float 1 0
U 5161e6ba frag u_time 816 float 1 0
U 5161e6ba frag u_fog_inscatter 820 float 1 0
U 5161e6ba frag u_fog_desat 824 float 1 0
U 5161e6ba frag u_inv_proj 832 mat4 1 0
U 5161e6ba frag u_texel 896 vec2 1 0
U 5161e6ba frag u_focus 904 float 1 0
U 5161e6ba frag u_aperture 908 float 1 0
U 5161e6ba frag u_max_coc 912 float 1 0
T 5161e6ba u_brdf 2
T 5161e6ba u_depth 3
T 5161e6ba u_irradiance 4
T 5161e6ba u_prefilter 5
T 5161e6ba u_shadow 6
T 5161e6ba u_src 7
T 5161e6ba u_tershadow 8
T 5161e6ba u_ts_height 9

View file

@ -5,6 +5,7 @@
fullscreen.vert|adapt.frag|
fullscreen.vert|bloom_down.frag|
fullscreen.vert|bloom_up.frag|
fullscreen.vert|dof.frag|
fullscreen.vert|heightgen.frag|#define DEM;
fullscreen.vert|ibl_brdf.frag|
fullscreen.vert|ibl_irradiance.frag|
@ -17,6 +18,7 @@ fullscreen.vert|ternormal.frag|
fullscreen.vert|tershadow.frag|#define NOISE_ONLY;
fullscreen.vert|tonemap.frag|
fullscreen.vert|tonemap.frag|#define HDR10;
fullscreen.vert|volumetric.frag|
grass.mesh|model.frag|#define FOLIAGE;#define BLADE;#define MESH;
grass.vert|model.frag|#define FOLIAGE;#define BLADE;
grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define TILES;
@ -26,12 +28,15 @@ model.vert|bake.frag|
model.vert|bake.frag|#define CARD;
model.vert|bake.frag|#define FLOWER;
model.vert|depth.frag|#define FOLIAGE;#define WIND;#define ALPHA_TEST;
model.vert|depth.frag|#define FOLIAGE;#define WIND;#define ALPHA_TEST;#define NEAR_FADE;
model.vert|model.frag|
model.vert|model.frag|#define CARD;#define CHEAP;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define ALPHA_TEST;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define ALPHA_TEST;#define NEAR_FADE;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define BLADE;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define CARD;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define EQ_PASS;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define EQ_PASS;#define NEAR_FADE;
model.vert|model.frag|#define FOLIAGE;#define WIND;#define FLOWER;
model.vert|model.frag|#define SHADOW_PASS;#define WIND;#define CARD;
model.vert|model.frag|#define WIND;
@ -54,5 +59,3 @@ terrain.vert|terrain.frag|#define SUN_INLINE;#define FAR_ONLY;
terrain.vert|terrain.frag|#define SUN_INLINE;#define NEAR_ONLY;
terrain.vert|tersun.frag|
water.vert|water.frag|
fullscreen.vert|volumetric.frag|
fullscreen.vert|dof.frag|

View file

@ -10,20 +10,12 @@ const CRAFT_MAX: int = 128
const CRAFT_ING_MAX: int = 512
function craft_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.recipe_reg == null {
rpg_st.recipe_reg = Dict.new()
rpg_st.recipe_out = pointers(CRAFT_MAX); rpg_st.recipe_outqty = words(CRAFT_MAX)
rpg_st.ing_rid = words(CRAFT_ING_MAX); rpg_st.ing_name = pointers(CRAFT_ING_MAX); rpg_st.ing_qty = words(CRAFT_ING_MAX)
}
}
event cancellable CraftRequested { e: int = 0, recipe: int = 0 }
event CraftCompleted { e: int = 0, recipe: int = 0 }
# Craft.recipe(name, out_item, out_qty) — declare a recipe; returns its id.
@Namespace(Craft) function craft_recipe(rpg_st: mut RpgState, name: pointer, out_item: pointer, out_qty: int) -> int {
craft_ensure(rpg_st)
if rpg_st.recipe_n >= CRAFT_MAX { return -1 }
let id = rpg_st.recipe_n
rpg_st.recipe_out[id] = out_item; rpg_st.recipe_outqty[id] = out_qty
@ -31,10 +23,9 @@ event CraftCompleted { e: int = 0, recipe: int = 0 }
Dict.set(rpg_st.recipe_reg, name, id)
return id
}
@Namespace(Craft) function craft_id(rpg_st: mut RpgState, name: pointer) -> int { craft_ensure(rpg_st); return Dict.get_or(rpg_st.recipe_reg, name, -1) }
@Namespace(Craft) function craft_id(rpg_st: mut RpgState, name: pointer) -> int { return Dict.get_or(rpg_st.recipe_reg, name, -1) }
# Craft.ingredient(recipe_name, item, qty) — add an ingredient requirement.
@Namespace(Craft) function craft_ingredient(rpg_st: mut RpgState, recipe_name: pointer, item: pointer, qty: int) -> void {
craft_ensure(rpg_st)
let rid = craft_id(rpg_st, recipe_name)
if rid < 0 { return }
if rpg_st.ing_n >= CRAFT_ING_MAX { return }
@ -44,7 +35,6 @@ event CraftCompleted { e: int = 0, recipe: int = 0 }
# Craft.can(e, recipe_name) — does the owner hold every ingredient?
@Namespace(Craft) function craft_can(rpg_st: mut RpgState, e: int, recipe_name: pointer) -> int {
craft_ensure(rpg_st)
let rid = craft_id(rpg_st, recipe_name)
if rid < 0 { return 0 }
var i = 0
@ -59,7 +49,6 @@ event CraftCompleted { e: int = 0, recipe: int = 0 }
# Craft.make(e, recipe_name) — consume ingredients + produce output; 1 on success.
@Namespace(Craft) function craft_make(rpg_st: mut RpgState, e: int, recipe_name: pointer) -> int {
craft_ensure(rpg_st)
let rid = craft_id(rpg_st, recipe_name)
if rid < 0 { return 0 }
if craft_can(rpg_st, e, recipe_name) == 0 { return 0 }

View file

@ -9,14 +9,6 @@ const DLG_NODE_MAX: int = 512
const DLG_CHOICE_MAX: int = 1024
function dlg_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.dlg_reg == null {
rpg_st.dlg_reg = Dict.new()
rpg_st.dlg_tree_start = words(DLG_TREE_MAX)
rpg_st.dlg_node_tree = words(DLG_NODE_MAX); rpg_st.dlg_node_text = pointers(DLG_NODE_MAX)
rpg_st.dlg_choice_node = words(DLG_CHOICE_MAX); rpg_st.dlg_choice_label = pointers(DLG_CHOICE_MAX); rpg_st.dlg_choice_next = words(DLG_CHOICE_MAX)
}
}
property Dialog { tree: int = 0, node: int = 0, active: int = 0 }
@ -27,7 +19,6 @@ event DialogEnded { e: int = 0 }
# Dialog.tree(name) — register (or fetch) a tree id.
@Namespace(Dialog) function dialog_tree(rpg_st: mut RpgState, name: pointer) -> int {
dlg_ensure(rpg_st)
let ex = Dict.get_or(rpg_st.dlg_reg, name, -1)
if ex >= 0 { return ex }
if rpg_st.dlg_tree_n >= DLG_TREE_MAX { return -1 }
@ -40,7 +31,6 @@ event DialogEnded { e: int = 0 }
# Dialog.node(tree_name, text) — add a line node; the first node added to a tree is
# its entry point. Returns the node id.
@Namespace(Dialog) function dialog_node(rpg_st: mut RpgState, tree_name: pointer, text: pointer) -> int {
dlg_ensure(rpg_st)
let tid = dialog_tree(rpg_st, tree_name)
if tid < 0 { return -1 }
if rpg_st.dlg_node_n >= DLG_NODE_MAX { return -1 }
@ -52,7 +42,6 @@ event DialogEnded { e: int = 0 }
}
# Dialog.choice(node, label, next_node) — add a branch (next_node -1 ends).
@Namespace(Dialog) function dialog_choice(rpg_st: mut RpgState, node: int, label: pointer, next_node: int) -> int {
dlg_ensure(rpg_st)
if rpg_st.dlg_choice_n >= DLG_CHOICE_MAX { return -1 }
let id = rpg_st.dlg_choice_n
rpg_st.dlg_choice_node[id] = node; rpg_st.dlg_choice_label[id] = label; rpg_st.dlg_choice_next[id] = next_node
@ -71,7 +60,6 @@ function dlg_set(e: int, name: pointer, v: int) -> void {
# Dialog.start(e, tree_name) — open the tree on a speaker (DialogStarted + Line).
@Namespace(Dialog) function dialog_start(rpg_st: mut RpgState, e: int, tree_name: pointer) -> void {
dlg_ensure(rpg_st)
let tid = dialog_tree(rpg_st, tree_name)
if tid < 0 { return }
dlg_set(e, "tree", tid)
@ -87,7 +75,6 @@ function dlg_set(e: int, name: pointer, v: int) -> void {
}
# Dialog.count(e) — number of choices at the current node.
@Namespace(Dialog) function dialog_count(rpg_st: mut RpgState, e: int) -> int {
dlg_ensure(rpg_st)
if dlg_get(e, "active") == 0 { return 0 }
let node = dlg_get(e, "node")
var n = 0; var i = 0
@ -96,7 +83,6 @@ function dlg_set(e: int, name: pointer, v: int) -> void {
}
# Dialog.choose(e, i) — take the i-th choice (veto-able); advance or end.
@Namespace(Dialog) function dialog_choose(rpg_st: mut RpgState, e: int, idx: int) -> int {
dlg_ensure(rpg_st)
if dlg_get(e, "active") == 0 { return 0 }
let node = dlg_get(e, "node")
var seen = 0; var i = 0; var pick = -1

View file

@ -11,52 +11,45 @@ const INV_MAX: int = 256
# ---- item registry --------------------------------------------------------
state RpgState {
recipe_reg: Dict = null # recipe name -> id
recipe_reg: Dict = Dict.new() # recipe name -> id
recipe_n: int = 0
recipe_out: pointers = null # output item name
recipe_outqty: words = null
recipe_out: pointers = pointers(CRAFT_MAX) # output item name
recipe_outqty: words = words(CRAFT_MAX)
ing_n: int = 0
ing_rid: words = null # owning recipe id
ing_name: pointers = null # ingredient item name
ing_qty: words = null
dlg_reg: Dict = null # tree name -> tree id
ing_rid: words = words(CRAFT_ING_MAX) # owning recipe id
ing_name: pointers = pointers(CRAFT_ING_MAX) # ingredient item name
ing_qty: words = words(CRAFT_ING_MAX)
dlg_reg: Dict = Dict.new() # tree name -> tree id
dlg_tree_n: int = 0
dlg_tree_start: words = null # first node id per tree
dlg_tree_start: words = words(DLG_TREE_MAX) # first node id per tree
dlg_node_n: int = 0
dlg_node_tree: words = null
dlg_node_text: pointers = null
dlg_node_tree: words = words(DLG_NODE_MAX)
dlg_node_text: pointers = pointers(DLG_NODE_MAX)
dlg_choice_n: int = 0
dlg_choice_node: words = null # owning node
dlg_choice_label: pointers = null
dlg_choice_next: words = null # next node id, or -1 to end
item_reg: Dict = null
dlg_choice_node: words = words(DLG_CHOICE_MAX) # owning node
dlg_choice_label: pointers = pointers(DLG_CHOICE_MAX)
dlg_choice_next: words = words(DLG_CHOICE_MAX) # next node id, or -1 to end
item_reg: Dict = Dict.new()
item_n: int = 0
item_stackmax: words = null
item_weight: words = null
item_value: words = null
inv_counts: Dict = null
quest_reg: Dict = null # quest name -> id
item_stackmax: words = words(INV_MAX)
item_weight: words = words(INV_MAX)
item_value: words = words(INV_MAX)
inv_counts: Dict = Dict.new()
quest_reg: Dict = Dict.new() # quest name -> id
quest_n: int = 0
quest_state: words = null
quest_state: words = words(QUEST_MAX)
obj_n: int = 0
obj_qid: words = null # owning quest id
obj_kind: words = null
obj_target: words = null # a target id/tag (kind-specific; 0 = any)
obj_need: words = null
obj_have: words = null
quest_flags: Dict = null # global flag store
obj_qid: words = words(OBJ_MAX) # owning quest id
obj_kind: words = words(OBJ_MAX)
obj_target: words = words(OBJ_MAX) # a target id/tag (kind-specific; 0 = any)
obj_need: words = words(OBJ_MAX)
obj_have: words = words(OBJ_MAX)
quest_flags: Dict = Dict.new() # global flag store
}
function item_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.item_reg == null {
rpg_st.item_reg = Dict.new()
rpg_st.item_stackmax = words(INV_MAX); rpg_st.item_weight = words(INV_MAX); rpg_st.item_value = words(INV_MAX)
}
}
# Item.def(name, stack_max, weight, value) — register an item template. Returns id.
@Namespace(Item) function item_def(rpg_st: mut RpgState, name: pointer, stack_max: int, weight: int, value: int) -> int {
item_ensure(rpg_st)
if rpg_st.item_n >= INV_MAX { return -1 }
let id = rpg_st.item_n
rpg_st.item_stackmax[id] = stack_max; rpg_st.item_weight[id] = weight; rpg_st.item_value[id] = value
@ -64,7 +57,7 @@ function item_ensure(rpg_st: mut RpgState) -> void {
Dict.set(rpg_st.item_reg, name, id)
return id
}
@Namespace(Item) function item_id(rpg_st: mut RpgState, name: pointer) -> int { item_ensure(rpg_st); return Dict.get_or(rpg_st.item_reg, name, -1) }
@Namespace(Item) function item_id(rpg_st: mut RpgState, name: pointer) -> int { return Dict.get_or(rpg_st.item_reg, name, -1) }
@Namespace(Item) function item_value_of(rpg_st: mut RpgState, name: pointer) -> int {
let id = item_id(rpg_st, name); if id < 0 { return 0 }
return rpg_st.item_value[id]
@ -73,7 +66,6 @@ function item_ensure(rpg_st: mut RpgState) -> void {
# ---- per-owner counts -----------------------------------------------------
property Inventory { gold: int = 0, weight_max: int = 0, slots: int = 0 }
function inv_ensure(rpg_st: mut RpgState) -> void { if rpg_st.inv_counts == null { rpg_st.inv_counts = Dict.new() } }
function inv_key(e: int, name: pointer) -> pointer { return `{e}/{name}` }
event ItemAdded { e: int = 0, item: int = 0, qty: int = 0 } # item = registry id
@ -82,14 +74,12 @@ event cancellable ItemUse { e: int = 0, item: int = 0 } # veto a use (
# Inventory.add(e, name, n) — add n of an item; emits ItemAdded.
@Namespace(Inventory) function inventory_add(rpg_st: mut RpgState, e: int, name: pointer, n: int) -> void {
inv_ensure(rpg_st)
let k = inv_key(e, name)
Dict.set(rpg_st.inv_counts, k, Dict.get_or(rpg_st.inv_counts, k, 0) + n)
emit ItemAdded(e: e, item: item_id(rpg_st, name), qty: n)
}
# Inventory.count(e, name) — how many the owner holds.
@Namespace(Inventory) function inventory_count(rpg_st: mut RpgState, e: int, name: pointer) -> int {
inv_ensure(rpg_st)
return Dict.get_or(rpg_st.inv_counts, inv_key(e, name), 0)
}
@Namespace(Inventory) function inventory_has(rpg_st: mut RpgState, e: int, name: pointer) -> int {
@ -98,7 +88,6 @@ event cancellable ItemUse { e: int = 0, item: int = 0 } # veto a use (
}
# Inventory.remove(e, name, n) — remove up to n; returns 1 if it had enough.
@Namespace(Inventory) function inventory_remove(rpg_st: mut RpgState, e: int, name: pointer, n: int) -> int {
inv_ensure(rpg_st)
let k = inv_key(e, name)
let have = Dict.get_or(rpg_st.inv_counts, k, 0)
if have < n { return 0 }

View file

@ -15,14 +15,6 @@ const QUEST_MAX: int = 128
const OBJ_MAX: int = 512
function quest_ensure(rpg_st: mut RpgState) -> void {
if rpg_st.quest_reg == null {
rpg_st.quest_reg = Dict.new(); rpg_st.quest_flags = Dict.new()
rpg_st.quest_state = words(QUEST_MAX)
rpg_st.obj_qid = words(OBJ_MAX); rpg_st.obj_kind = words(OBJ_MAX); rpg_st.obj_target = words(OBJ_MAX)
rpg_st.obj_need = words(OBJ_MAX); rpg_st.obj_have = words(OBJ_MAX)
}
}
event QuestStarted { quest: int = 0 }
event QuestCompleted { quest: int = 0 }
@ -30,7 +22,6 @@ event ObjectiveProgressed { quest: int = 0, objective: int = 0, have: int = 0, n
# Quest.define(name) — register a quest (state hidden). Returns id.
@Namespace(Quest) function quest_define(rpg_st: mut RpgState, name: pointer) -> int {
quest_ensure(rpg_st)
if rpg_st.quest_n >= QUEST_MAX { return -1 }
let id = rpg_st.quest_n
rpg_st.quest_state[id] = 0
@ -38,10 +29,9 @@ event ObjectiveProgressed { quest: int = 0, objective: int = 0, have: int = 0, n
Dict.set(rpg_st.quest_reg, name, id)
return id
}
@Namespace(Quest) function quest_id(rpg_st: mut RpgState, name: pointer) -> int { quest_ensure(rpg_st); return Dict.get_or(rpg_st.quest_reg, name, -1) }
@Namespace(Quest) function quest_id(rpg_st: mut RpgState, name: pointer) -> int { return Dict.get_or(rpg_st.quest_reg, name, -1) }
# Quest.objective(quest_name, kind, target, need) — add an objective.
@Namespace(Quest) function quest_objective(rpg_st: mut RpgState, quest_name: pointer, kind: int, target: int, need: int) -> int {
quest_ensure(rpg_st)
let qid = quest_id(rpg_st, quest_name)
if qid < 0 { return -1 }
if rpg_st.obj_n >= OBJ_MAX { return -1 }
@ -53,14 +43,12 @@ event ObjectiveProgressed { quest: int = 0, objective: int = 0, have: int = 0, n
}
# Quest.start(name) — activate a quest (emits QuestStarted).
@Namespace(Quest) function quest_start(rpg_st: mut RpgState, name: pointer) -> void {
quest_ensure(rpg_st)
let qid = quest_id(rpg_st, name)
if qid < 0 { return }
rpg_st.quest_state[qid] = 2
emit QuestStarted(quest: qid)
}
@Namespace(Quest) function quest_state_of(rpg_st: mut RpgState, name: pointer) -> int {
quest_ensure(rpg_st)
let qid = quest_id(rpg_st, name)
if qid < 0 { return -1 }
return rpg_st.quest_state[qid]
@ -80,7 +68,6 @@ function quest_all_done(rpg_st: RpgState, qid: int) -> int {
# quest's matching objectives tick up (target 0 on the objective = "any"); a quest
# whose objectives are all met auto-completes.
@Namespace(Quest) function quest_notify(rpg_st: mut RpgState, kind: int, target: int, amount: int) -> void {
quest_ensure(rpg_st)
var i = 0
while i < rpg_st.obj_n {
let qid = rpg_st.obj_qid[i]
@ -104,5 +91,5 @@ function quest_all_done(rpg_st: RpgState, qid: int) -> int {
}
# ---- global flag store (branching / world state) --------------------------
@Namespace(Quest) function quest_flag(rpg_st: mut RpgState, name: pointer, value: int) -> void { quest_ensure(rpg_st); Dict.set(rpg_st.quest_flags, name, value) }
@Namespace(Quest) function quest_flag_get(rpg_st: mut RpgState, name: pointer) -> int { quest_ensure(rpg_st); return Dict.get_or(rpg_st.quest_flags, name, 0) }
@Namespace(Quest) function quest_flag(rpg_st: mut RpgState, name: pointer, value: int) -> void { Dict.set(rpg_st.quest_flags, name, value) }
@Namespace(Quest) function quest_flag_get(rpg_st: mut RpgState, name: pointer) -> int { return Dict.get_or(rpg_st.quest_flags, name, 0) }

View file

@ -34,7 +34,7 @@ down ends the night). The transport says what this machine became (`ses_set_role
| | |
| --- | --- |
| `ply_init()`, `ply_join(pid) -> slot`, `ply_leave(p)`, `ply_leave_all()` | the roster (-1: full, or pid 0) |
| `ply_join(pid) -> slot`, `ply_leave(p)`, `ply_leave_all()` | the roster (-1: full, or pid 0) |
| `ply_report(p, x, y, z, yaw, speed)`, `ply_report_rest(p, rest, sleeping)`, `ply_report_name(p, name)` | what a remote machine said |
| `ply_on`, `ply_is_local`, `ply_slot_of(pid)`, `ply_count`, `ply_pid`, `ply_name`, `ply_x/y/z/yaw/speed/rest(p)`, `ply_sleeping(p)` | who and where |
| `ply_near(x, z)`, `ply_near_d2`, `ply_near_i()`, `ply_dist(p, x, z)` | the nearest player |

View file

@ -1,6 +1,5 @@
# roster.ludic - who is here. Slot 0 is this machine's player and reads the SessionLocal port
# live; slots 1.. are remote players, filled by what their machines report.
export function ply_init(session_st: mut SessionState) -> void { session__init(session_st) }
export function ply_on(session_st: SessionState, p: int) -> bool {
if p == 0 { return true }
@ -26,7 +25,6 @@ export function ply_count(session_st: SessionState) -> int {
# a remote player comes in, standing where this one stands until they say: the first free
# slot, or -1 when the party is full
export function ply_join(session_st: mut SessionState, pid: int) -> int {
session__init(session_st)
if pid <= 0 { return -1 } # 0 is this machine's player
for p in 1 .. PLY_MAX {
if session_st.session__slot[p].on { continue }

View file

@ -61,7 +61,7 @@ function session__nameless() -> string { return "" }
export state SessionState {
session__me_sleeping: bool = false
session__slot: []PartyMember = null
session__slot: []PartyMember = session__slots()
session__role: int = 0
session__max: int = 4
session__open: bool = true
@ -92,10 +92,11 @@ function session__fact(session_st: SessionState, what: int, slot: int, pid: int)
return f
}
function session__init(session_st: mut SessionState) -> void {
if session_st.session__slot != null { return }
session_st.session__slot = new []PartyMember
for i in 0 .. PLY_MAX { push(session_st.session__slot, new PartyMember) }
session_st.session__slot[0].on = true
}
function session__facts__new() -> Queue<SessionFact> { return queue_new("session.facts") }
# the roster's slots, slot 0 this machine's own and on
function session__slots() -> []PartyMember {
let out = new []PartyMember
for i in 0 .. PLY_MAX { push(out, new PartyMember) }
out[0].on = true
return out
}

View file

@ -30,14 +30,12 @@ program SessionTest {
# a host with n guests, pids 2.., facts drained
function party(session_st: mut SessionState, n: int) -> void {
ply_init(session_st)
ses_set_role(session_st, SES_HOST)
for i in 0 .. n { ply_join(session_st, 2 + i) }
q_clear(ses_facts(session_st))
}
test "slot 0 is the local player, read live; others join at its feet and leave" (session_st: mut SessionState, session_test_st: mut SessionTestState) {
ply_init(session_st)
session_test_st.me_x = 5.0
expect(ply_on(session_st, 0))
expect_near(ply_x(session_st, 0), 5.0, 0.001)
@ -58,14 +56,12 @@ program SessionTest {
}
test "the roster fills at twelve" (session_st: mut SessionState) {
ply_init(session_st)
for i in 0 .. PLY_MAX - 1 { expect(ply_join(session_st, 10 + i) > 0) }
expect_eq(ply_join(session_st, 99), -1)
expect_eq(ply_count(session_st), PLY_MAX)
}
test "the nearest player, whoever it is" (session_st: mut SessionState, session_test_st: mut SessionTestState) {
ply_init(session_st)
session_test_st.me_x = 100.0
let p = ply_join(session_st, 2)
ply_report(session_st, p, 3.0, 0.0, 4.0, 0.0, 1.0)
@ -75,7 +71,6 @@ program SessionTest {
}
test "roles: alone owns the world, a guest does not, and a change is a fact" (session_st: mut SessionState) {
ply_init(session_st)
expect(ses_owns_world(session_st))
expect(not ses_live(session_st))
expect(ses_night_passes(session_st))
@ -115,7 +110,6 @@ program SessionTest {
}
test "a guest's turning in is a fact for the host to hear; alone a bed just sleeps" (session_st: mut SessionState) {
ply_init(session_st)
expect(not ses_sleep(session_st, true))
ses_set_role(session_st, SES_GUEST)
q_clear(ses_facts(session_st))
@ -153,7 +147,6 @@ program SessionTest {
}
test "alone it may go; a guest may not, and its vote goes to the host" (session_st: mut SessionState) {
ply_init(session_st)
expect_eq(ses_may(session_st, 1, 0, "x"), SES_MAY_GO)
ses_set_role(session_st, SES_GUEST)
expect_eq(ses_may(session_st, 1, 0, "x"), SES_MAY_HOSTS)

View file

@ -1,8 +1,7 @@
# ludic.settings/file.ludic - the store as a JSON object, by key: a switch is 0 or 1, a whole number
# is itself, a float is thousandths (sv_put_float), text is a string. A key the file lacks keeps
# what the store holds, and a key the store does not know is ignored.
export function settings_to_json(settings_st: mut SettingsState) -> Val {
sg_init(settings_st)
export function settings_to_json(settings_st: SettingsState) -> Val {
let v = Value.object()
for i in 0 .. len(settings_st.sg_defs) { settings_put(settings_st, v, i) }
return v
@ -23,7 +22,6 @@ export function settings_put(settings_st: SettingsState, v: Val, id: int) -> voi
# every key `v` names, held to its range; quiet, since a load is applied whole by its caller
export function settings_from_json(settings_st: mut SettingsState, v: Val) -> void {
sg_init(settings_st)
if v == null { return }
for i in 0 .. len(settings_st.sg_defs) { sg_read(settings_st, v, i) }
}

View file

@ -1,7 +1,6 @@
# ludic.settings/rules.ludic - the whole store at once: every setting to its default, every one
# held to its range, and the rescue set. None of these is news: whoever calls them applies it all
export function settings_reset_all(settings_st: mut SettingsState) -> void {
sg_init(settings_st)
for i in 0 .. len(settings_st.sg_defs) {
settings_st.sg_num[i] = settings_st.sg_defs[i].num
settings_st.sg_text[i] = settings_st.sg_defs[i].text
@ -11,14 +10,12 @@ export function settings_reset_all(settings_st: mut SettingsState) -> void {
# a hand-edited file, or one from a build with more choices, never hands the game a value it has
# no case for
export function settings_clamp_all(settings_st: mut SettingsState) -> void {
sg_init(settings_st)
for i in 0 .. len(settings_st.sg_defs) { settings_st.sg_num[i] = settings_held(settings_st, i, settings_st.sg_num[i]) }
}
# the values every machine can run: each setting marked `rescue` back to its default, and the
# rest (keys, language, volume) left the player's
export function settings_rescue(settings_st: mut SettingsState) -> void {
sg_init(settings_st)
for i in 0 .. len(settings_st.sg_defs) {
if settings_st.sg_defs[i].rescue {
settings_st.sg_num[i] = settings_st.sg_defs[i].num

View file

@ -1,18 +1,12 @@
# ludic.settings/store.ludic - the values, by id (a setting's place in the order it was added),
# and the verbs: nothing else holds a setting, and every set that changes one is a fact
export state SettingsState {
sg_defs: []SettingDef = null
sg_num: []float = null
sg_text: []string = null
sg_defs: []SettingDef = new []SettingDef
sg_num: []float = new []float
sg_text: []string = new []string
sg_changes: Queue<SettingChanged> = sg_changes__new()
}
function sg_init(settings_st: mut SettingsState) -> void {
if settings_st.sg_defs != null { return }
settings_st.sg_defs = new []SettingDef
settings_st.sg_num = new []float
settings_st.sg_text = new []string
}
export function settings_changes(settings_st: SettingsState) -> Queue<SettingChanged> {
return settings_st.sg_changes
@ -28,23 +22,20 @@ export function settings_clear(settings_st: mut SettingsState) -> void {
# a setting, at its default; its id is its place in the order added
export function settings_add(settings_st: mut SettingsState, d: SettingDef) -> int {
sg_init(settings_st)
push(settings_st.sg_defs, d)
push(settings_st.sg_num, d.num)
push(settings_st.sg_text, d.text)
return len(settings_st.sg_defs) - 1
}
export function settings_count(settings_st: mut SettingsState) -> int {
sg_init(settings_st)
export function settings_count(settings_st: SettingsState) -> int {
return len(settings_st.sg_defs)
}
export function settings_def(settings_st: SettingsState, id: int) -> SettingDef { return settings_st.sg_defs[id] }
# the id of `key`, or -1
export function settings_id_of(settings_st: mut SettingsState, key: string) -> int {
sg_init(settings_st)
export function settings_id_of(settings_st: SettingsState, key: string) -> int {
for i in 0 .. len(settings_st.sg_defs) { if settings_st.sg_defs[i].key == key { return i } }
return -1
}

View file

@ -70,36 +70,26 @@ event WeaponFired { owner: int = 0, def_id: int = 0 }
# Weapon registry (name-keyed, mod-friendly — add a gun with no code)
# ---------------------------------------------------------------------------
state ShooterState {
wpn_reg: Dict = null
wpn_reg: Dict = Dict.new()
wpn_n: int = 0
wpn_rate: words = null
wpn_dmg: words = null
wpn_speed: words = null
wpn_spread: words = null
wpn_pellets: words = null
wpn_pattern: words = null
wpn_ttl: words = null
wpn_pierce: words = null
wpn_homing: words = null
wpn_color: words = null
wpn_rate_base: words = null
wpn_rate: words = words(WPN_MAX)
wpn_dmg: words = words(WPN_MAX)
wpn_speed: words = words(WPN_MAX)
wpn_spread: words = words(WPN_MAX)
wpn_pellets: words = words(WPN_MAX)
wpn_pattern: words = words(WPN_MAX)
wpn_ttl: words = words(WPN_MAX)
wpn_pierce: words = words(WPN_MAX)
wpn_homing: words = words(WPN_MAX)
wpn_color: words = words(WPN_MAX)
wpn_rate_base: words = words(WPN_MAX)
wpn_fire_color: int = 0xffffff # the colour of the weapon being fired (sh_fire)
}
function wpn_ensure(shooter_st: mut ShooterState) -> void {
if shooter_st.wpn_reg == null {
shooter_st.wpn_reg = Dict.new()
shooter_st.wpn_rate = words(WPN_MAX); shooter_st.wpn_dmg = words(WPN_MAX); shooter_st.wpn_speed = words(WPN_MAX)
shooter_st.wpn_spread = words(WPN_MAX); shooter_st.wpn_pellets = words(WPN_MAX); shooter_st.wpn_pattern = words(WPN_MAX)
shooter_st.wpn_ttl = words(WPN_MAX); shooter_st.wpn_pierce = words(WPN_MAX); shooter_st.wpn_homing = words(WPN_MAX)
shooter_st.wpn_color = words(WPN_MAX); shooter_st.wpn_rate_base = words(WPN_MAX)
}
}
# Weapon.def(name, fire_rate, damage, speed, spread, pellets, pattern) — register a
# gun. pattern: 0 single/aimed, 1 spread cone, 2 ring, 3 spiral. Returns the def id.
@Namespace(Weapon) function weapon_def(shooter_st: mut ShooterState, name: pointer, fire_rate: int, damage: int, speed: int, spread: int, pellets: int, pattern: int) -> int {
wpn_ensure(shooter_st)
if shooter_st.wpn_n >= WPN_MAX { return -1 }
let id = shooter_st.wpn_n
shooter_st.wpn_rate[id] = fire_rate; shooter_st.wpn_dmg[id] = damage; shooter_st.wpn_speed[id] = speed
@ -112,36 +102,35 @@ function wpn_ensure(shooter_st: mut ShooterState) -> void {
return id
}
@Namespace(Weapon) function weapon_id(shooter_st: mut ShooterState, name: pointer) -> int {
wpn_ensure(shooter_st)
return Dict.get_or(shooter_st.wpn_reg, name, -1)
}
# Weapon.set_rate(id, frames) — frames between shots (a fire-rate upgrade changes it in place).
@Namespace(Weapon) function weapon_set_rate(shooter_st: mut ShooterState, id: int, frames: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_rate[id] = frames }
if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_rate[id] = frames }
}
@Namespace(Weapon) function weapon_rate(shooter_st: mut ShooterState, id: int) -> int {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { return shooter_st.wpn_rate[id] }; return 0
if (id >= 0) and (id < WPN_MAX) { return shooter_st.wpn_rate[id] }; return 0
}
# Weapon.reset(id) — back to the definition: no pierce, no homing, the defined fire rate.
@Namespace(Weapon) function weapon_reset(shooter_st: mut ShooterState, id: int) -> void {
wpn_ensure(shooter_st); if (id < 0) or (id >= WPN_MAX) { return }
if (id < 0) or (id >= WPN_MAX) { return }
shooter_st.wpn_pierce[id] = 0; shooter_st.wpn_homing[id] = 0; shooter_st.wpn_rate[id] = shooter_st.wpn_rate_base[id]
}
# Weapon.set_color(id, color) — the colour the engine draws this weapon's shots in.
@Namespace(Weapon) function weapon_set_color(shooter_st: mut ShooterState, id: int, color: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_color[id] = color }
if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_color[id] = color }
}
# Weapon.set_ttl(id, frames) — projectile lifetime for a weapon (range control).
@Namespace(Weapon) function weapon_set_ttl(shooter_st: mut ShooterState, id: int, frames: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_ttl[id] = frames }
if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_ttl[id] = frames }
}
# Weapon.set_pierce(id, n) — a shot passes through n targets before it dies.
@Namespace(Weapon) function weapon_set_pierce(shooter_st: mut ShooterState, id: int, n: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_pierce[id] = n }
if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_pierce[id] = n }
}
# Weapon.set_homing(id, deg) — max degrees/frame a shot turns toward an enemy.
@Namespace(Weapon) function weapon_set_homing(shooter_st: mut ShooterState, id: int, deg: int) -> void {
wpn_ensure(shooter_st); if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_homing[id] = deg }
if (id >= 0) and (id < WPN_MAX) { shooter_st.wpn_homing[id] = deg }
}
# ---------------------------------------------------------------------------
@ -534,7 +523,6 @@ function sh_spawn_bullet(shooter_st: ShooterState, owner: int, fac: int, px: int
@EngineSystem(Weapon, Update) function esys_weapon(shooter_st: mut ShooterState) -> void {
let P = World.prop_id("Weapon")
if P < 0 { return }
wpn_ensure(shooter_st)
var e = World.query_next(P, 0)
while e >= 0 {
let cd = World.get(e, P, World.field_id(P, "cooldown"))

View file

@ -1607,6 +1607,14 @@ go:
%scale = select i1 %ison, i32 %bsv, i32 1
store i32 %scale, ptr @W_glscale
call void @win_gl_update()
; a Metal layer (Vulkan) draws at the same scale
%ml = load ptr, ptr @W_mtl
%noml = icmp eq ptr %ml, null
br i1 %noml, label %out, label %mtl
mtl:
%scd = sitofp i32 %scale to double
%sel_csc = call ptr @sel_registerName(ptr @.s_setcsc)
%rm = call ptr (ptr, ptr, double) @objc_msgSend(ptr %ml, ptr %sel_csc, double %scd)
br label %out
out:
ret void
@ -2035,3 +2043,81 @@ define ptr @win_native_instance() {
entry:
ret ptr null
}
; ---- a Metal layer on the view, for Vulkan through MoltenVK -----------------------------
; VK_EXT_metal_surface presents to a CAMetalLayer, so the view is made layer-hosting with one
; (the layer set before wantsLayer, which is what makes the view host it rather than back it).
; Its contentsScale is the window's backing scale, or the swapchain's pixels are stretched over
; a Retina window's points; @W_glscale follows it, as win_gl_attach sets it for OpenGL. The class
; is looked up by name: the program links QuartzCore when it uses Vk.* (null without it).
; win_metal_layer() -> CAMetalLayer* | null (no window, or no QuartzCore)
@.c_mtllayer = private unnamed_addr constant [13 x i8] c"CAMetalLayer\00"
@.s_layer = private unnamed_addr constant [6 x i8] c"layer\00"
@.s_retain = private unnamed_addr constant [7 x i8] c"retain\00"
@.s_setlayer = private unnamed_addr constant [10 x i8] c"setLayer:\00"
@.s_wantsl = private unnamed_addr constant [15 x i8] c"setWantsLayer:\00"
@.s_setcsc = private unnamed_addr constant [18 x i8] c"setContentsScale:\00"
@W_mtl = internal global ptr null
define ptr @win_metal_layer() {
entry:
%have = load ptr, ptr @W_mtl
%got = icmp ne ptr %have, null
br i1 %got, label %done, label %make
make:
%view = load ptr, ptr @W_view
%win = load ptr, ptr @W_win
%noview = icmp eq ptr %view, null
%nowin = icmp eq ptr %win, null
%nothing = or i1 %noview, %nowin
br i1 %nothing, label %none, label %cls
cls:
%c = call ptr @objc_getClass(ptr @.c_mtllayer)
%noc = icmp eq ptr %c, null
br i1 %noc, label %none, label %go
go:
%sel_l = call ptr @sel_registerName(ptr @.s_layer)
%l0 = call ptr @objc_msgSend(ptr %c, ptr %sel_l)
%sel_r = call ptr @sel_registerName(ptr @.s_retain)
%l = call ptr @objc_msgSend(ptr %l0, ptr %sel_r)
%sel_bs = call ptr @sel_registerName(ptr @.s_bscale)
%bs = call double (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_bs)
%bsok = fcmp ogt double %bs, 0.0
%scale = select i1 %bsok, double %bs, double 1.0
%sel_cs = call ptr @sel_registerName(ptr @.s_setcsc)
%r0 = call ptr (ptr, ptr, double) @objc_msgSend(ptr %l, ptr %sel_cs, double %scale)
%sel_sl = call ptr @sel_registerName(ptr @.s_setlayer)
%r1 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %view, ptr %sel_sl, ptr %l)
%sel_wl = call ptr @sel_registerName(ptr @.s_wantsl)
%r2 = call ptr (ptr, ptr, i8) @objc_msgSend(ptr %view, ptr %sel_wl, i8 1)
%si = fptosi double %scale to i32
store i32 %si, ptr @W_glscale
store ptr %l, ptr @W_mtl
ret ptr %l
done:
ret ptr %have
none:
ret ptr null
}
; ---- is the window on screen ----------------------------------------------------------------------
; A covered or minimised window's CAMetalLayer hands out drawables at the compositor's pace (a
; second's timeout each), so a Vulkan frame waiting for one stalled the whole game at 3 fps behind
; another window. NSWindowOcclusionStateVisible is bit 1 of -occlusionState.
; win_visible() -> 1 | 0 (1 with no window)
@.s_occl = private unnamed_addr constant [15 x i8] c"occlusionState\00"
define i32 @win_visible() {
entry:
%win = load ptr, ptr @W_win
%nowin = icmp eq ptr %win, null
br i1 %nowin, label %yes, label %ask
ask:
%sel = call ptr @sel_registerName(ptr @.s_occl)
%st = call i64 (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel)
%bit = and i64 %st, 2
%vis = icmp ne i64 %bit, 0
%r = zext i1 %vis to i32
ret i32 %r
yes:
ret i32 1
}

View file

@ -398,6 +398,15 @@ define weak ptr @win_native_instance() {
entry:
ret ptr null
}
define weak ptr @win_metal_layer() {
entry:
ret ptr null
}
; the window is always drawn to here (cocoa.ll asks macOS whether it is covered)
define weak i32 @win_visible() {
entry:
ret i32 1
}
; the drawable's size, for a graphics API that follows the window without a GL context (render3d's
; Vulkan resize check); headless there is no window, so 0 x 0. cocoa.ll's real one wins when linked.
; win_set_title: cocoa.ll's in a windowed build; nothing to retitle headless

View file

@ -52,6 +52,16 @@ define ptr @win_native_instance() {
entry:
ret ptr null
}
; macOS only (cocoa.ll): the CAMetalLayer MoltenVK presents to
define ptr @win_metal_layer() {
entry:
ret ptr null
}
; the window is always drawn to here (cocoa.ll asks macOS whether it is covered)
define i32 @win_visible() {
entry:
ret i32 1
}
define void @win_set_title(ptr %title) {
entry:

View file

@ -4,8 +4,8 @@
; macOS has no Vulkan of its own: the LunarG loader and MoltenVK (Vulkan over
; Metal) come with the Vulkan SDK or with a game that bundles them. So the loader
; is looked for at run time - beside the executable, in the usual library
; directories, then under $VULKAN_SDK - and a Mac without it gets 0 from vk_open
; and stays on OpenGL. The contract is vk_win.ll's:
; directories, then MoltenVK on its own (a bundle's Contents/Frameworks), then under
; $VULKAN_SDK - and a Mac without any gets 0 from vk_open and stays on OpenGL. The contract is vk_win.ll's:
;
; lvk_open() -> 1 | 0 lvk_sym(name) -> ptr lvk_has(name) -> 1 | 0
;
@ -25,6 +25,10 @@ declare i32 @lvk_bind()
@.lvk_c3 = private unnamed_addr constant [36 x i8] c"/opt/homebrew/lib/libvulkan.1.dylib\00"
@.lvk_env = private unnamed_addr constant [11 x i8] c"VULKAN_SDK\00"
@.lvk_fmt = private unnamed_addr constant [25 x i8] c"%s/lib/libvulkan.1.dylib\00"
@.lvk_fmtm = private unnamed_addr constant [25 x i8] c"%s/lib/libMoltenVK.dylib\00"
@.lvk_m0 = private unnamed_addr constant [49 x i8] c"@executable_path/../Frameworks/libMoltenVK.dylib\00"
@.lvk_m1 = private unnamed_addr constant [35 x i8] c"@executable_path/libMoltenVK.dylib\00"
@.lvk_m2 = private unnamed_addr constant [18 x i8] c"libMoltenVK.dylib\00"
@lvk_lib = internal global ptr null
; RTLD_NOW (2) | RTLD_LOCAL (4)
@ -34,52 +38,51 @@ entry:
ret ptr %h
}
; the candidates in order: a Vulkan loader first (the SDK's, so the validation layer can be
; stacked in development), then MoltenVK itself, which exports every vk* entry point and is what
; a bundle ships in Contents/Frameworks - no loader, no ICD json.
@lvk_paths = internal constant [7 x ptr] [ptr @.lvk_c0, ptr @.lvk_c1, ptr @.lvk_c2, ptr @.lvk_c3, ptr @.lvk_m0, ptr @.lvk_m1, ptr @.lvk_m2]
define i32 @lvk_open() {
entry:
%have = load ptr, ptr @lvk_lib
%open = icmp ne ptr %have, null
br i1 %open, label %ok, label %t0
t0:
%h0 = call ptr @lvk_try(ptr @.lvk_c0)
%n0 = icmp eq ptr %h0, null
br i1 %n0, label %t1, label %got0
got0:
store ptr %h0, ptr @lvk_lib
br i1 %open, label %ok, label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %i1, %next ]
%slot = getelementptr [7 x ptr], ptr @lvk_paths, i32 0, i32 %i
%path = load ptr, ptr %slot
%h = call ptr @lvk_try(ptr %path)
%miss = icmp eq ptr %h, null
br i1 %miss, label %next, label %got
next:
%i1 = add i32 %i, 1
%more = icmp slt i32 %i1, 7
br i1 %more, label %loop, label %sdk
got:
store ptr %h, ptr @lvk_lib
br label %bind
t1:
%h1 = call ptr @lvk_try(ptr @.lvk_c1)
%n1 = icmp eq ptr %h1, null
br i1 %n1, label %t2, label %got1
got1:
store ptr %h1, ptr @lvk_lib
br label %bind
t2:
%h2 = call ptr @lvk_try(ptr @.lvk_c2)
%n2 = icmp eq ptr %h2, null
br i1 %n2, label %t3, label %got2
got2:
store ptr %h2, ptr @lvk_lib
br label %bind
t3:
%h3 = call ptr @lvk_try(ptr @.lvk_c3)
%n3 = icmp eq ptr %h3, null
br i1 %n3, label %t4, label %got3
got3:
store ptr %h3, ptr @lvk_lib
br label %bind
t4:
%sdk = call ptr @getenv(ptr @.lvk_env)
%nosdk = icmp eq ptr %sdk, null
br i1 %nosdk, label %fail, label %t4b
t4b:
sdk:
%sdkp = call ptr @getenv(ptr @.lvk_env)
%nosdk = icmp eq ptr %sdkp, null
br i1 %nosdk, label %fail, label %s1
s1:
%buf = alloca [1024 x i8]
%w = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmt, ptr %sdk)
%w = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmt, ptr %sdkp)
%h4 = call ptr @lvk_try(ptr %buf)
%n4 = icmp eq ptr %h4, null
br i1 %n4, label %fail, label %got4
br i1 %n4, label %s2, label %got4
s2:
%w2 = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmtm, ptr %sdkp)
%h5 = call ptr @lvk_try(ptr %buf)
%n5 = icmp eq ptr %h5, null
br i1 %n5, label %fail, label %got5
got4:
store ptr %h4, ptr @lvk_lib
br label %bind
got5:
store ptr %h5, ptr @lvk_lib
br label %bind
bind:
%n = call i32 @lvk_bind()
br label %ok

View file

@ -547,6 +547,16 @@ entry:
%inst = call ptr @GetModuleHandleA(ptr null)
ret ptr %inst
}
; macOS only (cocoa.ll): the CAMetalLayer MoltenVK presents to
define ptr @win_metal_layer() {
entry:
ret ptr null
}
; the window is always drawn to here (cocoa.ll asks macOS whether it is covered)
define i32 @win_visible() {
entry:
ret i32 1
}
; set (%on != 0) or clear a key's bit in the 256-bit held set
define void @win_held_bit(i32 %k, i32 %on) {

View file

@ -20,6 +20,12 @@ function is_intrinsic(name: pointer) -> bool {
function emit_bind(rest: pointer) -> pointer { let r = nreg(); emit(" "); emit(r); emit(" = "); emit(rest); emit("\n"); return r }
function arg_code(e: Node, i: int) -> pointer { let v = emit_expr(e.kids[i]); return v.code }
# a buffer handed to the platform layer: a slice's elements, never its header (as an extern's are)
function arg_buf(e: Node, i: int) -> pointer {
let v = emit_expr(e.kids[i])
if is_slice_ty(v.ty) { return emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`) }
return v.code
}
function emit_intrinsic(name: pointer, e: Node) -> Val {
g_intrin_ok = true

View file

@ -70,7 +70,7 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
return val("0", "void")
}
if (name == "win_present") {
let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2)
let a = arg_buf(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2)
emit(" call void @win_present(ptr "); emit(a); emit(", i32 "); emit(b); emit(", i32 "); emit(c); emit(")\n")
return val("0", "void")
}
@ -78,19 +78,19 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
# #50 — fill a caller buffer with the platform device state (windowed only,
# DCE'd headless). win_held: an 8-word held-key bitset; win_mouse: [x, y,
# button-mask, wheel-delta].
if (name == "win_held") { let a = arg_code(e, 0); emit(" call void @win_held(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_mouse") { let a = arg_code(e, 0); emit(" call void @win_mouse(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_held") { let a = arg_buf(e, 0); emit(" call void @win_held(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_mouse") { let a = arg_buf(e, 0); emit(" call void @win_mouse(ptr "); emit(a); emit(")\n"); return val("0", "void") }
# what the active keyboard layout types on a held-set key position (windowed only,
# DCE'd headless): a code point, 0 for none.
if (name == "win_key_char") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @win_key_char(i32 {a})`), "int") }
# what the player TYPED since the last call - UTF-16 code units into the caller's buffer, one per
# word, returning how many. The layout, the modifiers and any dead key are already applied, which
# is why a name field must read this rather than build text out of key codes.
if (name == "win_text") { let a = arg_code(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call i32 @win_text(ptr {a}, i32 {b})`), "int") }
if (name == "win_text") { let a = arg_buf(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call i32 @win_text(ptr {a}, i32 {b})`), "int") }
# #51 — win_pad: 6 words/pad [connected, mask, lx, ly, rx, ry]; win_touch: 3
# words/point [active, x, y]. Both windowed-only, DCE'd headless.
if (name == "win_pad") { let a = arg_code(e, 0); emit(" call void @win_pad(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_touch") { let a = arg_code(e, 0); emit(" call void @win_touch(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_pad") { let a = arg_buf(e, 0); emit(" call void @win_pad(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_touch") { let a = arg_buf(e, 0); emit(" call void @win_touch(ptr "); emit(a); emit(")\n"); return val("0", "void") }
# #89 — set the OS cursor capture mode (windowed only; DCE'd headless).
if (name == "win_cursor_mode") { let a = arg_code(e, 0); emit(" call void @win_cursor_mode(i32 "); emit(a); emit(")\n"); return val("0", "void") }
# #22 audio backend — AVAudioPlayer via audio.ll; windowed-only, DCE'd headless.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -360,6 +360,8 @@ entry {
var vkl = join_path(home, "runtime/native/vk_mac.ll")
if g_target_win { vkl = join_path(home, "runtime/native/vk_win.ll") }
cmd = `{cmd} {vkt} {vkl}`
# the CAMetalLayer a macOS window presents through, found by name in cocoa.ll
if not g_target_win { cmd = `{cmd} -Wl,-needed_framework,QuartzCore` }
}
# A shipped Windows game is a GUI-subsystem program: started from Explorer, a
# console-subsystem one opens a console window behind the game. mainCRTStartup keeps

View file

@ -106,7 +106,8 @@ function gl_link_flags(ll: pointer) -> pointer {
function vk_link_flags(ll: pointer) -> pointer {
if not shq(`grep -q "@lvk_" {ll}`) { return "" }
let home = ludic_home()
return ` {home}runtime/native/vk_thunks.ll {home}runtime/native/vk_mac.ll`
# QuartzCore: the CAMetalLayer a window presents through (cocoa.ll finds the class by name)
return ` {home}runtime/native/vk_thunks.ll {home}runtime/native/vk_mac.ll -Wl,-needed_framework,QuartzCore`
}
# A package that carries a native library (phase 15) is written into the IR by the compiler as