diff --git a/examples/events/answer.ludic b/examples/events/answer.ludic
index ef5597e3..255ccb3b 100644
--- a/examples/events/answer.ludic
+++ b/examples/events/answer.ludic
@@ -5,22 +5,20 @@
# Running it prints: 10 20 0
program Answer {
event cancellable Ask { kind: int }
- state AnswerState {
- answer: int = 0
- }
+ var answer: int = 0
- @On(Ask) handler One(answer_st: mut AnswerState) { if kind != 1 { return }; answer_st.answer = 10; cancel }
- @On(Ask) handler Two(answer_st: mut AnswerState) { if kind != 2 { return }; answer_st.answer = 20; cancel }
+ @On(Ask) handler One { if kind != 1 { return }; answer = 10; cancel }
+ @On(Ask) handler Two { if kind != 2 { return }; answer = 20; cancel }
- function ask(answer_st: mut AnswerState, k: int) -> int {
- answer_st.answer = 0
- if emit Ask(kind: k) != 0 { return answer_st.answer }
+ function ask(k: int) -> int {
+ answer = 0
+ if emit Ask(kind: k) != 0 { return answer }
return 0
}
- entry (answer_st: mut AnswerState) {
- print(ask(answer_st, 1))
- print(ask(answer_st, 2))
- print(ask(answer_st, 3))
+ entry {
+ print(ask(1))
+ print(ask(2))
+ print(ask(3))
}
}
diff --git a/examples/events/layer_events.ludic b/examples/events/layer_events.ludic
index ac6c12d0..ab99630c 100644
--- a/examples/events/layer_events.ludic
+++ b/examples/events/layer_events.ludic
@@ -6,9 +6,7 @@
#
# Running it prints: 50 2 1 50
program LayerEvents {
- state LayereventsState {
- step: int = 0
- }
+ var step: int = 0
@On(layer_Hud_show) handler Shown { print(1) } # a mod reacts when the HUD returns
@On(layer_Hud_hide) handler Hidden { print(2) } # ...and when it's hidden
@@ -18,11 +16,11 @@ program LayerEvents {
handler Draw phase Update { print(50) } # only runs while Hud is enabled
}
layer Ctrl {
- handler Drive(layerevents_st: mut LayereventsState) phase LateUpdate {
- layerevents_st.step += 1
- if layerevents_st.step == 1 { disable layer Hud } # frame 1: hide -> Hidden 2, Draw stops
- if layerevents_st.step == 2 { enable layer Hud } # frame 2: show -> Shown 1, Draw resumes
- if layerevents_st.step == 3 { quit() } # frame 3: after Draw ran again
+ handler Drive phase LateUpdate {
+ step += 1
+ if step == 1 { disable layer Hud } # frame 1: hide -> Hidden 2, Draw stops
+ if step == 2 { enable layer Hud } # frame 2: show -> Shown 1, Draw resumes
+ if step == 3 { quit() } # frame 3: after Draw ran again
}
}
}
diff --git a/examples/events/mod_events.ludic b/examples/events/mod_events.ludic
index 1cf5d3da..f1b3dc23 100644
--- a/examples/events/mod_events.ludic
+++ b/examples/events/mod_events.ludic
@@ -3,16 +3,14 @@
# `Damage` has two @On listeners: one prints the amount, one accumulates a total.
# Emitting it twice runs both listeners each time. Prints 10 / 32 / 42.
program ModEvents {
- state ModeventsState {
- total: int = 0
- }
+ var total: int = 0
event Damage { amount: int = 0 }
@On(Damage) handler Native { print(amount) } # prints each hit
- @On(Damage) handler Accum(modevents_st: mut ModeventsState) { modevents_st.total += amount } # sums them
+ @On(Damage) handler Accum { total += amount } # sums them
- entry (modevents_st: ModeventsState) {
+ entry {
emit Damage(amount: 10) # Native prints 10
emit Damage(amount: 32) # Native prints 32
- print(modevents_st.total) # 42
+ print(total) # 42
}
}
diff --git a/examples/events/recurse.ludic b/examples/events/recurse.ludic
index fdb4288a..e1f8be69 100644
--- a/examples/events/recurse.ludic
+++ b/examples/events/recurse.ludic
@@ -6,18 +6,16 @@
#
# Running it prints: 16 (the cycle bottoms out at the depth cap, deterministically)
program Recurse {
- state RecurseState {
- n: int = 0
- }
+ var n: int = 0
event Ping { }
event Pong { }
- @On(Ping) handler A(recurse_st: mut RecurseState) { recurse_st.n += 1; emit Pong() } # each Ping deepens by one Pong
+ @On(Ping) handler A { n += 1; emit Pong() } # each Ping deepens by one Pong
@On(Pong) handler B { emit Ping() } # ...and each Pong by one Ping
- entry (recurse_st: RecurseState) {
+ entry {
emit Ping()
- print(recurse_st.n)
+ print(n)
}
}
diff --git a/examples/events/scoped.ludic b/examples/events/scoped.ludic
index 559a9a3a..6d54bfe5 100644
--- a/examples/events/scoped.ludic
+++ b/examples/events/scoped.ludic
@@ -6,18 +6,16 @@
program Scoped {
property Health { hp: int = 0 }
model Unit { Health }
- state ScopedState {
- fires: int = 0
- }
+ var fires: int = 0
event Tick { }
- @On(Tick) handler OnTick(scoped_st: mut ScopedState) { for (Health) in query [Health, {Unit}] { scoped_st.fires += 1 } }
+ @On(Tick) handler OnTick { for (Health) in query [Health, {Unit}] { fires += 1 } }
- entry (scoped_st: ScopedState) {
+ entry {
spawn Unit { Health { hp: 1 } }
emit Tick() # 1 live Unit -> fires = 1
emit Tick() # fires = 2
for (Health) in query [Health, {Unit}] { despawn self() } # kill the Unit
emit Tick() # no live Units -> fires stays 2
- print(scoped_st.fires) # 2
+ print(fires) # 2
}
}
diff --git a/examples/functions/values.ludic b/examples/functions/values.ludic
index f2e87943..658bbd16 100644
--- a/examples/functions/values.ludic
+++ b/examples/functions/values.ludic
@@ -9,14 +9,12 @@ program FunctionValues {
function double(x: int) -> int { return x * 2 }
function square(x: int) -> int { return x * x }
function half(x: float) -> float { return x / 2.0 }
- state FunctionvaluesState {
- op: fn(int) -> int = null
- }
+ var op: fn(int) -> int = null
function run_with(f: fn(int) -> int, v: int) -> int { return f(v) }
function pass_on(f: fn(float) -> float) -> fn(float) -> float { return f }
- entry (functionvalues_st: mut FunctionvaluesState) {
- functionvalues_st.op = fn double
- print(functionvalues_st.op(21))
+ entry {
+ op = fn double
+ print(op(21))
let ops = new []fn(int) -> int
push(ops, fn double)
push(ops, fn square)
@@ -28,6 +26,6 @@ program FunctionValues {
print(run_with(fn square, 9))
let h = pass_on(fn half)
print(h(3.0))
- if functionvalues_st.op != null { print(1) }
+ if op != null { print(1) }
}
}
diff --git a/examples/games/chronorift/combat.ludic b/examples/games/chronorift/combat.ludic
index 381c24a5..b5bb0e4e 100644
--- a/examples/games/chronorift/combat.ludic
+++ b/examples/games/chronorift/combat.ludic
@@ -8,43 +8,43 @@
enum KnightAct { Attack, Guard, Item, Flee }
enum MageAct { Attack, Heal, Guard }
-handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
- if games_chronorift_st.game_mode == 1 {
+handler Battle phase Update {
+ if game_mode == 1 {
let k = Input.key()
for (e) in query [Stats, {Enemy}] {
- machine games_chronorift_st.turn_phase {
+ machine turn_phase {
state KnightMenu {
for (st, pt) in query [Stats, Party] {
if pt.slot == 0 {
if st.hp <= 0 {
become MageMenu
- games_chronorift_st.menu_cursor = 0
+ menu_cursor = 0
} else {
- if k == 'w' { games_chronorift_st.menu_cursor = max(0, games_chronorift_st.menu_cursor - 1) }
- if k == 's' { games_chronorift_st.menu_cursor = min(3, games_chronorift_st.menu_cursor + 1) }
+ if k == 'w' { menu_cursor = max(0, menu_cursor - 1) }
+ if k == 's' { menu_cursor = min(3, menu_cursor + 1) }
if is_confirm(k) {
- match games_chronorift_st.menu_cursor {
+ match menu_cursor {
KnightAct.Attack => {
let d = max(1, st.atk - e.def + Random.range(0, 4))
e.hp -= d
- games_chronorift_st.player_hit = d
+ player_hit = d
}
- KnightAct.Guard => { st.guard = 1; games_chronorift_st.player_hit = 0 }
+ KnightAct.Guard => { st.guard = 1; player_hit = 0 }
KnightAct.Item => {
- if games_chronorift_st.potions > 0 {
- games_chronorift_st.potions -= 1
+ if potions > 0 {
+ potions -= 1
st.hp = min(st.maxhp, st.hp + 24)
- games_chronorift_st.player_hit = 24
- } else { games_chronorift_st.player_hit = 0 }
+ player_hit = 24
+ } else { player_hit = 0 }
}
KnightAct.Flee => {
- if Random.chance(50) { e.hp = 0; games_chronorift_st.game_mode = 0 }
- else { games_chronorift_st.player_hit = 0 }
+ if Random.chance(50) { e.hp = 0; game_mode = 0 }
+ else { player_hit = 0 }
}
}
become KnightResolve
- games_chronorift_st.menu_cursor = 0
+ menu_cursor = 0
}
}
}
@@ -55,13 +55,13 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if k != 0 {
if e.hp <= 0 {
grant_xp(e.xp)
- games_chronorift_st.gold += e.xp
- if games_chronorift_st.enemy_type == 2 { games_chronorift_st.boss_down = 1; games_chronorift_st.game_mode = 3; games_chronorift_st.ack = 0 }
- else { games_chronorift_st.game_mode = 0 }
+ gold += e.xp
+ if enemy_type == 2 { boss_down = 1; game_mode = 3; ack = 0 }
+ else { game_mode = 0 }
become KnightMenu
} else {
become MageMenu
- games_chronorift_st.menu_cursor = 0
+ menu_cursor = 0
}
}
}
@@ -72,14 +72,14 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if st.hp <= 0 {
become EnemyTurn
} else {
- if k == 'i' { games_chronorift_st.menu_cursor = max(0, games_chronorift_st.menu_cursor - 1) }
- if k == 'k' { games_chronorift_st.menu_cursor = min(2, games_chronorift_st.menu_cursor + 1) }
+ if k == 'i' { menu_cursor = max(0, menu_cursor - 1) }
+ if k == 'k' { menu_cursor = min(2, menu_cursor + 1) }
if k == 'j' {
- match games_chronorift_st.menu_cursor {
+ match menu_cursor {
MageAct.Attack => {
let d = max(1, st.atk + 5 - e.def + Random.range(0, 5))
e.hp -= d
- games_chronorift_st.player_hit = d
+ player_hit = d
}
MageAct.Heal => {
if st.mp >= 4 {
@@ -87,13 +87,13 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
for (t2, p2) in query [Stats, Party] {
if p2.slot == 0 { t2.hp = min(t2.maxhp, t2.hp + 16) }
}
- games_chronorift_st.player_hit = 16
- } else { games_chronorift_st.player_hit = 0 }
+ player_hit = 16
+ } else { player_hit = 0 }
}
- MageAct.Guard => { st.guard = 1; games_chronorift_st.player_hit = 0 }
+ MageAct.Guard => { st.guard = 1; player_hit = 0 }
}
become MageResolve
- games_chronorift_st.menu_cursor = 0
+ menu_cursor = 0
}
}
}
@@ -104,9 +104,9 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if k != 0 {
if e.hp <= 0 {
grant_xp(e.xp)
- games_chronorift_st.gold += e.xp
- if games_chronorift_st.enemy_type == 2 { games_chronorift_st.boss_down = 1; games_chronorift_st.game_mode = 3; games_chronorift_st.ack = 0 }
- else { games_chronorift_st.game_mode = 0 }
+ gold += e.xp
+ if enemy_type == 2 { boss_down = 1; game_mode = 3; ack = 0 }
+ else { game_mode = 0 }
become KnightMenu
} else {
become EnemyTurn
@@ -123,7 +123,7 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
var ed = max(1, e.atk - t3.def + Random.range(0, 4))
if t3.guard == 1 { ed /= 2; t3.guard = 0 }
t3.hp -= ed
- games_chronorift_st.enemy_hit = ed
+ enemy_hit = ed
done = 1
}
}
@@ -132,10 +132,10 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
for (t4) in query [Stats, {Party}] {
if t4.hp > 0 { alive += 1 }
}
- if alive == 0 { games_chronorift_st.game_mode = 2; games_chronorift_st.ack = 0 }
+ if alive == 0 { game_mode = 2; ack = 0 }
else {
become KnightMenu
- games_chronorift_st.menu_cursor = 0
+ menu_cursor = 0
}
}
}
@@ -144,29 +144,29 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
}
}
-handler CleanBattle(games_chronorift_st: GamesChronoriftState) phase LateUpdate {
- if games_chronorift_st.game_mode != 1 {
+handler CleanBattle phase LateUpdate {
+ if game_mode != 1 {
for (e) in query [Stats, {Enemy}] {
despawn self()
}
}
}
-handler Meta(games_chronorift_st: mut GamesChronoriftState) phase Update {
+handler Meta phase Update {
let k = Input.key()
- if games_chronorift_st.game_mode == 2 {
- if k == 0 { games_chronorift_st.ack = 1 }
+ if game_mode == 2 {
+ if k == 0 { ack = 1 }
if k != 0 {
- if games_chronorift_st.ack == 1 {
+ if ack == 1 {
for (h) in query [Stats, {Party}] { h.hp = h.maxhp }
- games_chronorift_st.game_mode = 0
+ game_mode = 0
}
}
}
- if games_chronorift_st.game_mode == 3 {
- if k == 0 { games_chronorift_st.ack = 1 }
+ if game_mode == 3 {
+ if k == 0 { ack = 1 }
if k != 0 {
- if games_chronorift_st.ack == 1 { games_chronorift_st.game_mode = 0 }
+ if ack == 1 { game_mode = 0 }
}
}
}
diff --git a/examples/games/chronorift/draw.ludic b/examples/games/chronorift/draw.ludic
index c65ea164..c10eee0b 100644
--- a/examples/games/chronorift/draw.ludic
+++ b/examples/games/chronorift/draw.ludic
@@ -1,7 +1,7 @@
# draw.ludic — the Render system (overworld, battle, and overlay screens).
-handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
- let m = games_chronorift_st.game_mode
+handler Draw phase Render {
+ let m = game_mode
Screen.clear(0x101018)
if m == 0 {
@@ -31,7 +31,7 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
if c == 'D' { Screen.sprite(SPR_DOOR, sx, sy) }
if c == 'S' { Screen.sprite(SPR_DOOR, sx, sy) }
if c == 'B' {
- if games_chronorift_st.boss_down == 0 { Screen.sprite(SPR_BOSS, sx, sy) }
+ if boss_down == 0 { Screen.sprite(SPR_BOSS, sx, sy) }
}
}
}
@@ -47,9 +47,9 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.draw_text(8, 18, "HP", 0x88ff88, 1)
Screen.draw_number(24, 18, h.hp, 0xffffff, 1)
Screen.draw_text(8, 28, "G", 0xffd24a, 1)
- Screen.draw_number(18, 28, games_chronorift_st.gold, 0xffffff, 1)
+ Screen.draw_number(18, 28, gold, 0xffffff, 1)
Screen.draw_text(54, 28, "PO", 0x88ffcc, 1)
- Screen.draw_number(72, 28, games_chronorift_st.potions, 0xffffff, 1)
+ Screen.draw_number(72, 28, potions, 0xffffff, 1)
}
}
}
@@ -58,13 +58,13 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.fill_rectangle(0, 0, 320, 176, 0x241a2e)
for (e, a) in query [Stats, Actor, {Enemy}] {
var bs = 5
- if games_chronorift_st.enemy_type == 2 { bs = 7 }
+ if enemy_type == 2 { bs = 7 }
Screen.sprite_scaled(a.kind, 160 - bs * 8, 44, bs)
- Screen.draw_text(100, 20, enemy_name(games_chronorift_st.enemy_type), 0xffffff, 1)
+ Screen.draw_text(100, 20, enemy_name(enemy_type), 0xffffff, 1)
Screen.fill_rectangle(100, 30, 120, 8, 0x401018)
Screen.fill_rectangle(100, 30, e.hp * 120 / e.maxhp, 8, 0xe04040)
}
- let ph = games_chronorift_st.turn_phase
+ let ph = turn_phase
var act = 0
if ph >= 2 { act = 1 }
if ph >= 4 { act = 2 }
@@ -83,14 +83,14 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.draw_text(hx + 36, 207, "MP", 0x88aaff, 1)
Screen.draw_number(hx + 56, 207, st.mp, 0xffffff, 1)
}
- let cur = games_chronorift_st.menu_cursor
+ let cur = menu_cursor
if ph == 0 or ph == 1 {
Screen.draw_text(206, 181, "P1 KNIGHT", 0xffe08a, 1)
Screen.draw_text(214, 192, "ATTACK", 0xffffff, 1)
Screen.draw_text(214, 203, "DEFEND", 0xffffff, 1)
Screen.draw_text(214, 214, "ITEM", 0xffffff, 1)
Screen.draw_text(256, 214, "x", 0x777777, 1)
- Screen.draw_number(264, 214, games_chronorift_st.potions, 0xffd24a, 1)
+ Screen.draw_number(264, 214, potions, 0xffd24a, 1)
Screen.draw_text(214, 225, "RUN", 0xffffff, 1)
if ph == 0 { Screen.draw_text(204, 192 + cur * 11, ">", 0xffff00, 1) }
}
@@ -104,11 +104,11 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
if ph == 4 { Screen.draw_text(206, 200, "ENEMY TURN", 0xff8888, 1) }
if ph == 1 or ph == 3 {
Screen.draw_text(6, 232, "DMG", 0xffff88, 1)
- Screen.draw_number(34, 232, games_chronorift_st.player_hit, 0xffffff, 1)
+ Screen.draw_number(34, 232, player_hit, 0xffffff, 1)
}
if ph == 4 {
Screen.draw_text(6, 232, "FOE HITS", 0xff8888, 1)
- Screen.draw_number(66, 232, games_chronorift_st.enemy_hit, 0xffffff, 1)
+ Screen.draw_number(66, 232, enemy_hit, 0xffffff, 1)
}
}
@@ -128,11 +128,11 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.draw_text(80, 104, "BUY POTION", 0xffffff, 1)
Screen.draw_text(180, 104, "10 G", 0xffd24a, 1)
Screen.draw_text(80, 122, "LEAVE", 0xffffff, 1)
- Screen.draw_text(70, 104 + games_chronorift_st.menu_cursor * 18, ">", 0xffff00, 1)
+ Screen.draw_text(70, 104 + menu_cursor * 18, ">", 0xffff00, 1)
Screen.draw_text(80, 150, "GOLD", 0xffd24a, 1)
- Screen.draw_number(120, 150, games_chronorift_st.gold, 0xffffff, 1)
+ Screen.draw_number(120, 150, gold, 0xffffff, 1)
Screen.draw_text(160, 150, "POTIONS", 0x88ffcc, 1)
- Screen.draw_number(226, 150, games_chronorift_st.potions, 0xffffff, 1)
+ Screen.draw_number(226, 150, potions, 0xffffff, 1)
}
Screen.show()
}
diff --git a/examples/games/chronorift/overworld.ludic b/examples/games/chronorift/overworld.ludic
index 9c37e5a8..87cb851b 100644
--- a/examples/games/chronorift/overworld.ludic
+++ b/examples/games/chronorift/overworld.ludic
@@ -1,6 +1,6 @@
# overworld.ludic — boot, movement/encounter input, and the shop system.
-handler Boot(games_chronorift_st: mut GamesChronoriftState) phase Start {
+handler Boot phase Start {
png_load("assets/kenney/tiny-town/Tiles/tile_0000.png") # 0 grass
png_load("assets/kenney/tiny-town/Tiles/tile_0004.png") # 1 tree
png_load("assets/kenney/tiny-town/Tiles/tile_0025.png") # 2 path
@@ -25,16 +25,16 @@ handler Boot(games_chronorift_st: mut GamesChronoriftState) phase Start {
Party { slot: 1 }
Stats { hp: 26, maxhp: 26, mp: 16, maxmp: 16, atk: 8, def: 4, lvl: 1, xp: 0, guard: 0 }
}
- games_chronorift_st.game_mode = 0
- games_chronorift_st.encounter = 4
- games_chronorift_st.world_map = 0
- games_chronorift_st.boss_down = 0
- games_chronorift_st.gold = 20
- games_chronorift_st.potions = 2
+ game_mode = 0
+ encounter = 4
+ world_map = 0
+ boss_down = 0
+ gold = 20
+ potions = 2
}
-handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
- if games_chronorift_st.game_mode == 0 {
+handler Control phase Input {
+ if game_mode == 0 {
for (p) in query [Pos, {Player}] {
let k = Input.key()
var nx = p.x
@@ -57,43 +57,43 @@ handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
build_dungeon(0)
p.x = 3
p.y = 2
- games_chronorift_st.world_map = 1
+ world_map = 1
}
if tc == 'S' {
build_field(0)
p.x = 36
p.y = 3
- games_chronorift_st.world_map = 0
+ world_map = 0
}
if tc == 'B' {
- if games_chronorift_st.boss_down == 0 {
- games_chronorift_st.enemy_type = 2
+ if boss_down == 0 {
+ enemy_type = 2
spawn Enemy {
Actor { kind: 8 }
Stats { hp: 120, maxhp: 120, mp: 0, maxmp: 0, atk: 18, def: 10, lvl: 5, xp: 300, guard: 0 }
}
- games_chronorift_st.game_mode = 1
- games_chronorift_st.turn_phase = 0
- games_chronorift_st.menu_cursor = 0
+ game_mode = 1
+ turn_phase = 0
+ menu_cursor = 0
}
}
if tc == 'h' {
- games_chronorift_st.game_mode = 4
- games_chronorift_st.menu_cursor = 0
+ game_mode = 4
+ menu_cursor = 0
}
if tc != 'D' {
if tc != 'S' {
if tc != 'B' {
if tc != 'h' {
- games_chronorift_st.encounter -= 1
- if games_chronorift_st.encounter <= 0 {
- games_chronorift_st.encounter = Random.range(4, 9)
+ encounter -= 1
+ if encounter <= 0 {
+ encounter = Random.range(4, 9)
var et = 0
- if games_chronorift_st.world_map == 1 { et = 1 }
- if games_chronorift_st.world_map == 0 {
+ if world_map == 1 { et = 1 }
+ if world_map == 0 {
if Random.chance(40) { et = 1 }
}
- games_chronorift_st.enemy_type = et
+ enemy_type = et
if et == 0 {
spawn Enemy {
Actor { kind: 6 }
@@ -106,9 +106,9 @@ handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
Stats { hp: 30, maxhp: 30, mp: 0, maxmp: 0, atk: 11, def: 5, lvl: 1, xp: 15, guard: 0 }
}
}
- games_chronorift_st.game_mode = 1
- games_chronorift_st.turn_phase = 0
- games_chronorift_st.menu_cursor = 0
+ game_mode = 1
+ turn_phase = 0
+ menu_cursor = 0
}
}
}
@@ -122,19 +122,19 @@ handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
}
# shop (at the house 'h' tile): buy potions with gold
-handler Shop(games_chronorift_st: mut GamesChronoriftState) phase Update {
- if games_chronorift_st.game_mode == 4 {
+handler Shop phase Update {
+ if game_mode == 4 {
let k = Input.key()
- if k == 'w' { games_chronorift_st.menu_cursor = max(0, games_chronorift_st.menu_cursor - 1) }
- if k == 's' { games_chronorift_st.menu_cursor = min(1, games_chronorift_st.menu_cursor + 1) }
+ if k == 'w' { menu_cursor = max(0, menu_cursor - 1) }
+ if k == 's' { menu_cursor = min(1, menu_cursor + 1) }
if is_confirm(k) {
- if games_chronorift_st.menu_cursor == 0 {
- if games_chronorift_st.gold >= 10 {
- games_chronorift_st.gold -= 10
- games_chronorift_st.potions += 1
+ if menu_cursor == 0 {
+ if gold >= 10 {
+ gold -= 10
+ potions += 1
}
}
- if games_chronorift_st.menu_cursor == 1 { games_chronorift_st.game_mode = 0 }
+ if menu_cursor == 1 { game_mode = 0 }
}
}
}
diff --git a/examples/games/chronorift/world.ludic b/examples/games/chronorift/world.ludic
index 7eca4fb4..023c9a08 100644
--- a/examples/games/chronorift/world.ludic
+++ b/examples/games/chronorift/world.ludic
@@ -29,17 +29,15 @@ const SPR_MAGE: int = 9
const SPR_FLOOR: int = 10
const SPR_WALL: int = 11
-export state GamesChronoriftState {
- game_mode: int = 0 # 0 field, 1 battle, 2 gameover, 3 victory
- turn_phase: int = 0
- menu_cursor: int = 0
- encounter: int = 0
- enemy_type: int = 0 # 0 slime, 1 skeleton, 2 boss
- player_hit: int = 0
- enemy_hit: int = 0
- gold: int = 0
- world_map: int = 0 # 0 overworld, 1 dungeon
- boss_down: int = 0 # boss defeated flag
- ack: int = 0 # debounce for end screens
- potions: int = 0 # potions in inventory
-}
+var game_mode: int = 0 # 0 field, 1 battle, 2 gameover, 3 victory
+var turn_phase: int = 0
+var menu_cursor: int = 0
+var encounter: int = 0
+var enemy_type: int = 0 # 0 slime, 1 skeleton, 2 boss
+var player_hit: int = 0
+var enemy_hit: int = 0
+var gold: int = 0
+var world_map: int = 0 # 0 overworld, 1 dungeon
+var boss_down: int = 0 # boss defeated flag
+var ack: int = 0 # debounce for end screens
+var potions: int = 0 # potions in inventory
diff --git a/examples/games/menu.ludic b/examples/games/menu.ludic
index 5e256c6e..8a841778 100644
--- a/examples/games/menu.ludic
+++ b/examples/games/menu.ludic
@@ -9,24 +9,22 @@ program Menu {
const FONT_PATH: string = "/System/Library/Fonts/Supplemental/Arial.ttf"
const BACKDROP: int = 0x0e0e16
- state MenuState {
- title_font: int = 0
- }
+ var title_font: int = 0
ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c border: 0x4a4a72 skin: "assets/ui/panel.png" inset: 10 align: center {
- label text: "CHRONO RIFT" font: MenuState.title_font size: 26 fg: 0xffe060 align: center
- label text: "a Ludic retained-UI demo" font: MenuState.title_font size: 14 fg: 0x9aa0c8 align: center
+ label text: "CHRONO RIFT" font: title_font size: 26 fg: 0xffe060 align: center
+ label text: "a Ludic retained-UI demo" font: title_font size: 14 fg: 0x9aa0c8 align: center
spacer h: 4
- button id: NewGame text: "New Game" font: MenuState.title_font size: 16 w: 236
- button id: Continue text: "Continue" font: MenuState.title_font size: 16 w: 236
- button id: Options text: "Options" font: MenuState.title_font size: 16 w: 236
- button id: Quit text: "Quit" font: MenuState.title_font size: 16 w: 236
+ button id: NewGame text: "New Game" font: title_font size: 16 w: 236
+ button id: Continue text: "Continue" font: title_font size: 16 w: 236
+ button id: Options text: "Options" font: title_font size: 16 w: 236
+ button id: Quit text: "Quit" font: title_font size: 16 w: 236
}
}
- handler Boot(menu_st: mut MenuState) phase Start {
- menu_st.title_font = Font.load(FONT_PATH)
+ handler Boot phase Start {
+ title_font = Font.load(FONT_PATH)
Ui.build()
Ui.open(UI_MainMenu)
}
diff --git a/examples/games/npcai_demo.ludic b/examples/games/npcai_demo.ludic
index 481347b8..5fc48687 100644
--- a/examples/games/npcai_demo.ludic
+++ b/examples/games/npcai_demo.ludic
@@ -33,10 +33,8 @@ program NpcAiDemo {
@OnSpawn(Ally) handler AInit { }
@OnSpawn(Dummy) handler DInit { }
- state NpcaidemoState {
- spotted: int = 0
- }
- @On(TargetSpotted) handler OnSpot(npcaidemo_st: mut NpcaidemoState) { npcaidemo_st.spotted += 1 }
+ var spotted: int = 0
+ @On(TargetSpotted) handler OnSpot { spotted += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@@ -57,7 +55,7 @@ program NpcAiDemo {
return -1
}
- entry (npcaidemo_st: NpcaidemoState) {
+ entry {
let PBr = World.prop_id("Brain")
Weapon.def("blaster", 4, 10, 10, 0, 1, 0)
@@ -74,7 +72,7 @@ program NpcAiDemo {
# ---- A. perception: the enemy spots the hostile dummy -------------------
tick_n(4)
- print(bi(npcaidemo_st.spotted >= 1)) # 1 — TargetSpotted fired
+ print(bi(spotted >= 1)) # 1 — TargetSpotted fired
print(bi(Ai.target(enemy) == dummy)) # 1 — remembers the right target
# ---- B. FSM attack: in range -> auto-aim + fire -> dummy takes damage ---
diff --git a/examples/games/platformer_demo.ludic b/examples/games/platformer_demo.ludic
index eb529b40..d6768a47 100644
--- a/examples/games/platformer_demo.ludic
+++ b/examples/games/platformer_demo.ludic
@@ -32,15 +32,13 @@ program PlatformerDemo {
@OnSpawn(Floor) handler FloorInit { }
@OnSpawn(Wall) handler WallInit { }
- state PlatformerdemoState {
- jumps: int = 0
- have_power: int = 0
- vetoed_air: int = 0
- }
- @On(JumpPerformed) handler CountJumps(platformerdemo_st: mut PlatformerdemoState) { platformerdemo_st.jumps += 1 }
+ var jumps: int = 0
+ @On(JumpPerformed) handler CountJumps { jumps += 1 }
# a game rule: forbid the air (double) jump unless a "power" flag is set — pure
# lever-4 veto, no controller edit.
- @On(JumpRequested) handler GateAir(platformerdemo_st: mut PlatformerdemoState) { if (kind == 2) and (platformerdemo_st.have_power == 0) { platformerdemo_st.vetoed_air = 1; cancel } }
+ var have_power: int = 0
+ var vetoed_air: int = 0
+ @On(JumpRequested) handler GateAir { if (kind == 2) and (have_power == 0) { vetoed_air = 1; cancel } }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@@ -49,7 +47,7 @@ program PlatformerDemo {
return World.get(e, P, World.field_id(P, "coyote_t"))
}
- entry (platformerdemo_st: mut PlatformerdemoState) {
+ entry {
let PB = World.prop_id("Body")
let PP = World.prop_id("Position")
let PL = World.prop_id("Platformer")
@@ -91,16 +89,16 @@ program PlatformerDemo {
print(bi(pf_coyote(h) == 6)) # 1
# ---- D. double jump gated by the veto hook -----------------------------
- platformerdemo_st.have_power = 0
+ have_power = 0
Platformer.jump(h); tick_n(3) # ground jump -> airborne
- let after_ground = platformerdemo_st.jumps
+ let after_ground = jumps
Platformer.jump(h); tick_n(2) # air jump attempt -> vetoed
- print(bi((platformerdemo_st.jumps == after_ground) and (platformerdemo_st.vetoed_air == 1))) # 1
+ print(bi((jumps == after_ground) and (vetoed_air == 1))) # 1
- platformerdemo_st.have_power = 1
- let before_power = platformerdemo_st.jumps
+ have_power = 1
+ let before_power = jumps
Platformer.jump(h); tick_n(2) # air jump now allowed
- print(bi(platformerdemo_st.jumps == before_power + 1)) # 1
+ print(bi(jumps == before_power + 1)) # 1
tick_n(60) # settle on the floor
# ---- E. horizontal run into the wall (collision reuse) -----------------
diff --git a/examples/games/platformer_scaffolding.ludic b/examples/games/platformer_scaffolding.ludic
index 54be3989..5f1b9743 100644
--- a/examples/games/platformer_scaffolding.ludic
+++ b/examples/games/platformer_scaffolding.ludic
@@ -33,19 +33,17 @@ program PlatformerScaffolding {
@OnSpawn(Bouncer) handler BI { }
@OnSpawn(Spike) handler SI { }
- state PlatformerscaffoldingState {
- picked: int = 0
- sprang: int = 0
- hurt: int = 0
- }
- @On(ItemPickedUp) handler OnPick(platformerscaffolding_st: mut PlatformerscaffoldingState) { platformerscaffolding_st.picked += 1 }
- @On(Sprang) handler OnSpring(platformerscaffolding_st: mut PlatformerscaffoldingState) { platformerscaffolding_st.sprang += 1 }
- @On(Hurt) handler OnHurt(platformerscaffolding_st: mut PlatformerscaffoldingState) { platformerscaffolding_st.hurt += 1 }
+ var picked: int = 0
+ @On(ItemPickedUp) handler OnPick { picked += 1 }
+ var sprang: int = 0
+ @On(Sprang) handler OnSpring { sprang += 1 }
+ var hurt: int = 0
+ @On(Hurt) handler OnHurt { hurt += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
- entry (platformerscaffolding_st: PlatformerscaffoldingState) {
+ entry {
let PP = World.prop_id("Position")
let PB = World.prop_id("Body")
let PL = World.prop_id("Platformer")
@@ -71,7 +69,7 @@ program PlatformerScaffolding {
spawn Coin { Position { x: World.get(h, PP, fx), y: World.get(h, PP, fy) },
Collider { w: 10, h: 10, is_trigger: 1 }, Pickup { kind: 0, value: 5 } }
tick_n(2)
- print(bi(platformerscaffolding_st.picked == 1)) # 1 — ItemPickedUp fired
+ print(bi(picked == 1)) # 1 — ItemPickedUp fired
print(bi(Score.get(h) == 5)) # 1 — value scored
# ---- spring launches the hero upward -----------------------------------
@@ -81,13 +79,13 @@ program PlatformerScaffolding {
let vy_before = World.get(h, PB, World.field_id(PB, "vy"))
tick_n(2)
let vy_after = World.get(h, PB, World.field_id(PB, "vy"))
- print(bi((platformerscaffolding_st.sprang >= 1) and (vy_after < 0))) # 1 — launched (vy negative = up)
+ print(bi((sprang >= 1) and (vy_after < 0))) # 1 — launched (vy negative = up)
# ---- hazard damages a Life --------------------------------------------
spawn Spike { Position { x: World.get(h, PP, fx), y: World.get(h, PP, fy) },
Collider { w: 12, h: 12, is_trigger: 1 }, Hazard { dmg: 1, knockback: 6 } }
tick_n(2)
- print(bi(platformerscaffolding_st.hurt >= 1)) # 1 — Hurt fired
+ print(bi(hurt >= 1)) # 1 — Hurt fired
let PLife = World.prop_id("Life")
print(bi(World.get(h, PLife, World.field_id(PLife, "hp")) < 3)) # 1 — hp dropped
diff --git a/examples/games/rpg_demo.ludic b/examples/games/rpg_demo.ludic
index 5fc94fe4..45c9a5ab 100644
--- a/examples/games/rpg_demo.ludic
+++ b/examples/games/rpg_demo.ludic
@@ -38,16 +38,14 @@ program RpgDemo {
@OnSpawn(Cfg) handler CI { }
# a locked-door rule: veto a rightward grid step while `locked` is set (lever 4).
- state RpgdemoState {
- locked: int = 0
- entered: int = 0
- talked: int = 0
- quest_done: int = 0
- }
- @On(MoveRequested) handler LockDoor(rpgdemo_st: RpgdemoState) { if (rpgdemo_st.locked == 1) and (dx == 1) { cancel } }
- @On(TileEntered) handler CountEnter(rpgdemo_st: mut RpgdemoState) { rpgdemo_st.entered += 1 }
- @On(Interacted) handler OnTalk(rpgdemo_st: mut RpgdemoState) { if target >= 0 { rpgdemo_st.talked += 1 } }
- @On(QuestCompleted) handler OnQuestDone(rpgdemo_st: mut RpgdemoState) { rpgdemo_st.quest_done += 1 }
+ var locked: int = 0
+ @On(MoveRequested) handler LockDoor { if (locked == 1) and (dx == 1) { cancel } }
+ var entered: int = 0
+ @On(TileEntered) handler CountEnter { entered += 1 }
+ var talked: int = 0
+ @On(Interacted) handler OnTalk { if target >= 0 { talked += 1 } }
+ var quest_done: int = 0
+ @On(QuestCompleted) handler OnQuestDone { quest_done += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@@ -55,7 +53,7 @@ program RpgDemo {
function py(e: int) -> int { let P = World.prop_id("Position"); return World.get(e, P, World.field_id(P, "y")) }
function hp(e: int) -> int { let P = World.prop_id("Stats"); return World.get(e, P, World.field_id(P, "hp")) }
- entry (rpgdemo_st: mut RpgdemoState) {
+ entry {
let PM = World.prop_id("Mover")
# a 6x3 tile world; a wall glyph at cell (3,1)
@@ -73,7 +71,7 @@ program RpgDemo {
# move down into the open cell (16,32)
Mover.move(h, 0, 1); tick_n(1); Mover.move(h, 0, 0); tick_n(1)
print(bi((px(h) == 16) and (py(h) == 32))) # 1 — stepped one cell down
- print(bi(rpgdemo_st.entered >= 1)) # 1 — TileEntered fired
+ print(bi(entered >= 1)) # 1 — TileEntered fired
# try to walk into the wall at cell (3,1)=(48,16): from (32,16) moving right blocked
World.set(h, World.prop_id("Position"), World.field_id(World.prop_id("Position"), "x"), 32)
@@ -83,17 +81,17 @@ program RpgDemo {
# MoveRequested veto: a locked door forbids the rightward step (open cell now)
World.set(h, World.prop_id("Position"), World.field_id(World.prop_id("Position"), "y"), 32) # row 2 open
- rpgdemo_st.locked = 1
+ locked = 1
Mover.move(h, 1, 0); tick_n(1); Mover.move(h, 0, 0)
print(bi(px(h) == 32)) # 1 — vetoed, no move
- rpgdemo_st.locked = 0
+ locked = 0
Mover.move(h, 1, 0); tick_n(1); Mover.move(h, 0, 0)
print(bi(px(h) == 48)) # 1 — now allowed
# interact: an NPC one cell ahead (facing right after the last move)
spawn Npc { Position { x: 64, y: 32 } }
print(bi(Mover.interact(h) >= 0)) # 1 — found the NPC ahead
- print(bi(rpgdemo_st.talked == 1)) # 1 — Interacted fired
+ print(bi(talked == 1)) # 1 — Interacted fired
# ================= Module B: inventory + equipment =================
Item.def("herb", 99, 1, 2)
@@ -122,7 +120,7 @@ program RpgDemo {
Quest.start("hunt")
Quest.notify(0, 0, 1); Quest.notify(0, 0, 1); Quest.notify(0, 0, 1)
print(bi(Quest.state_of("hunt") == 3)) # 1 — auto-completed
- print(bi(rpgdemo_st.quest_done == 1)) # 1 — QuestCompleted fired
+ print(bi(quest_done == 1)) # 1 — QuestCompleted fired
# ================= Module E: dialog =================
let dn0 = Dialog.node("elder", "hello")
diff --git a/examples/games/shooter_demo.ludic b/examples/games/shooter_demo.ludic
index a4838c43..297133a6 100644
--- a/examples/games/shooter_demo.ludic
+++ b/examples/games/shooter_demo.ludic
@@ -30,21 +30,19 @@ program ShooterDemo {
@OnSpawn(Mob) handler MInit { }
@OnSpawn(Nest) handler NInit { }
- state ShooterdemoState {
- spawn_reqs: int = 0
- waves_cleared: int = 0
- spawned: int = 0
- hits: int = 0
- deaths: int = 0
- }
- @On(SpawnRequested) handler OnSpawnReq(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.spawn_reqs += 1 }
- @On(WaveCleared) handler OnWaveClear(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.waves_cleared += 1 }
+ var spawn_reqs: int = 0
+ @On(SpawnRequested) handler OnSpawnReq { spawn_reqs += 1 }
+ var waves_cleared: int = 0
+ @On(WaveCleared) handler OnWaveClear { waves_cleared += 1 }
- @On(ProjectileSpawned) handler CountSpawn(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.spawned += 1 }
- @On(ProjectileHit) handler CountHit(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.hits += 1 }
+ var spawned: int = 0
+ @On(ProjectileSpawned) handler CountSpawn { spawned += 1 }
+ var hits: int = 0
+ @On(ProjectileHit) handler CountHit { hits += 1 }
# reap the dead (the realistic policy: a Died body leaves the world) so stale
# corpses do not sit in the faction/targeting scans.
- @On(Died) handler Reap(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.deaths += 1; despawn e }
+ var deaths: int = 0
+ @On(Died) handler Reap { deaths += 1; despawn e }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@@ -74,7 +72,7 @@ program ShooterDemo {
return -1
}
- entry (shooterdemo_st: ShooterdemoState) {
+ entry {
let PT = World.prop_id("TopDown")
let PB = World.prop_id("Body")
let PP = World.prop_id("Position")
@@ -112,7 +110,7 @@ program ShooterDemo {
TopDown.fire(g, 0)
let hp_before = hp_of(mob)
tick_n(40) # bullet flies ~120px and hits
- print(bi(shooterdemo_st.spawned >= 1)) # 1 — a projectile spawned
+ print(bi(spawned >= 1)) # 1 — a projectile spawned
print(bi(hp_of(mob) < hp_before)) # 1 — enemy took damage
# ---- C. friendly fire is ignored (same faction) ------------------------
@@ -127,29 +125,29 @@ program ShooterDemo {
print(bi(hp_of(ally) == ally_hp)) # 1 — ally unharmed (faction)
# ---- D. shotgun spread emits multiple pellets in one shot ---------------
- let s0 = shooterdemo_st.spawned
+ let s0 = spawned
Weapon.equip(g, "shotgun")
TopDown.aim(g, 0)
TopDown.fire(g, 1); tick_n(1); TopDown.fire(g, 0)
- print(bi(shooterdemo_st.spawned - s0 == 5)) # 1 — 5 pellets
+ print(bi(spawned - s0 == 5)) # 1 — 5 pellets
# ---- E. a fresh enemy is eventually killed (Combat -> Died -> reaped) --
- let d0 = shooterdemo_st.deaths
+ let d0 = deaths
Weapon.equip(g, "rifle")
TopDown.aim(g, 0)
var f = 0
- while (shooterdemo_st.deaths == d0) and (f < 400) {
+ while (deaths == d0) and (f < 400) {
TopDown.fire(g, 1); tick_fixed(); TopDown.fire(g, 0); tick_fixed()
f += 1
}
- print(bi(shooterdemo_st.deaths == d0 + 1)) # 1 — enemy destroyed (Died fired)
+ print(bi(deaths == d0 + 1)) # 1 — enemy destroyed (Died fired)
# ---- F. ring pattern (bullet-hell) emits N bullets around 360 ----------
- let r0 = shooterdemo_st.spawned
+ let r0 = spawned
let ring = Weapon.def("nova", 30, 3, 6, 0, 8, 2) # pattern 2 = ring, 8 pellets
Weapon.equip(g, "nova")
TopDown.fire(g, 1); tick_n(1); TopDown.fire(g, 0)
- print(bi(shooterdemo_st.spawned - r0 == 8)) # 1 — 8-way ring
+ print(bi(spawned - r0 == 8)) # 1 — 8-way ring
# ---- G. homing shot curves onto an off-axis enemy ----------------------
let seek = Weapon.def("seeker", 30, 40, 6, 0, 1, 0)
@@ -168,8 +166,8 @@ program ShooterDemo {
# ---- H. wave spawner: budgeted, throttled SpawnRequested + WaveCleared --
spawn Nest { Position { x: 300, y: 300 }, Spawner { rate: 3, budget: 4 } }
tick_n(20) # 4 spawns at 3-frame spacing, then cleared
- print(bi(shooterdemo_st.spawn_reqs == 4)) # 1 — exactly budget requests
- print(bi(shooterdemo_st.waves_cleared == 1)) # 1 — wave cleared once
+ print(bi(spawn_reqs == 4)) # 1 — exactly budget requests
+ print(bi(waves_cleared == 1)) # 1 — wave cleared once
quit()
}
diff --git a/examples/games/snake.ludic b/examples/games/snake.ludic
index 47b18fb4..2cc24601 100644
--- a/examples/games/snake.ludic
+++ b/examples/games/snake.ludic
@@ -24,19 +24,17 @@ program Snake {
const MODE_OVER: int = 1
# ---- the game's whole state, named ----------------------------------------
- state SnakeState {
- direction: int = DIR_RIGHT
- length: int = 3
- tick: int = 0
- food_x: int = 14
- food_y: int = 7
- mode: int = MODE_PLAY
- score: int = 0
- ate: int = 0 # did the head reach the food this step?
- tail_x: int = 0 # where the tail sat before it moved (grow here)
- tail_y: int = 0
- acked: int = 0 # game-over key must be released before restart
- }
+ var direction: int = DIR_RIGHT
+ var length: int = 3
+ var tick: int = 0
+ var food_x: int = 14
+ var food_y: int = 7
+ var mode: int = MODE_PLAY
+ var score: int = 0
+ var ate: int = 0 # did the head reach the food this step?
+ var tail_x: int = 0 # where the tail sat before it moved (grow here)
+ var tail_y: int = 0
+ var acked: int = 0 # game-over key must be released before restart
# ---- custom shades this game uses (values the named palette doesn't carry) -
const SHADE_BG: int = 0x0d1020 # backdrop behind the board
@@ -48,48 +46,48 @@ program Snake {
const SHADE_SCORE: int = 0xffe060
const SHADE_OVER: int = 0xff5555
- function reset(snake_st: mut SnakeState, unused: int) -> int {
+ function reset(unused: int) -> int {
for (p, s) in query [Pos, Seg] { despawn self() }
spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } }
spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } }
spawn S2 { Seg { order: 2 }; Pos { x: 8, y: 7 } }
- snake_st.direction = DIR_RIGHT
- snake_st.length = 3
- snake_st.tick = 0
- snake_st.mode = MODE_PLAY
- snake_st.score = 0
- snake_st.food_x = 14
- snake_st.food_y = 7
+ direction = DIR_RIGHT
+ length = 3
+ tick = 0
+ mode = MODE_PLAY
+ score = 0
+ food_x = 14
+ food_y = 7
return 0
}
- handler Boot(snake_st: mut SnakeState) phase Start { reset(snake_st, 0) }
+ handler Boot phase Start { reset(0) }
- handler Input(snake_st: mut SnakeState) phase Input {
+ handler Input phase Input {
let k = Input.key()
- if snake_st.mode == MODE_PLAY {
- if k == 'w' { if snake_st.direction != DIR_DOWN { snake_st.direction = DIR_UP } }
- if k == 's' { if snake_st.direction != DIR_UP { snake_st.direction = DIR_DOWN } }
- if k == 'a' { if snake_st.direction != DIR_RIGHT { snake_st.direction = DIR_LEFT } }
- if k == 'd' { if snake_st.direction != DIR_LEFT { snake_st.direction = DIR_RIGHT } }
+ if mode == MODE_PLAY {
+ if k == 'w' { if direction != DIR_DOWN { direction = DIR_UP } }
+ if k == 's' { if direction != DIR_UP { direction = DIR_DOWN } }
+ if k == 'a' { if direction != DIR_RIGHT { direction = DIR_LEFT } }
+ if k == 'd' { if direction != DIR_LEFT { direction = DIR_RIGHT } }
}
- if snake_st.mode == MODE_OVER {
- if k == 0 { snake_st.acked = 1 }
- if k != 0 { if snake_st.acked == 1 { reset(snake_st, 0) } }
+ if mode == MODE_OVER {
+ if k == 0 { acked = 1 }
+ if k != 0 { if acked == 1 { reset(0) } }
}
}
- handler Move(snake_st: mut SnakeState) phase Update {
- if snake_st.mode == MODE_PLAY {
- snake_st.tick += 1
- if snake_st.tick >= 6 {
- snake_st.tick = 0
+ handler Move phase Update {
+ if mode == MODE_PLAY {
+ tick += 1
+ if tick >= 6 {
+ tick = 0
# remember the tail cell, then drag every segment onto the one ahead of it
for (pt, stg) in query [Pos, Seg] {
- if stg.order == snake_st.length - 1 { snake_st.tail_x = pt.x; snake_st.tail_y = pt.y }
+ if stg.order == length - 1 { tail_x = pt.x; tail_y = pt.y }
}
- for step in 0 .. snake_st.length {
- let i = snake_st.length - 1 - step
+ for step in 0 .. length {
+ let i = length - 1 - step
if i >= 1 {
for (pa, sa) in query [Pos, Seg] {
if sa.order == i {
@@ -101,18 +99,18 @@ program Snake {
}
}
# move the head, check the walls, check the food
- snake_st.ate = 0
+ ate = 0
for (ph, sh) in query [Pos, Seg] {
if sh.order == 0 {
- if snake_st.direction == DIR_UP { ph.y -= 1 }
- if snake_st.direction == DIR_DOWN { ph.y += 1 }
- if snake_st.direction == DIR_LEFT { ph.x -= 1 }
- if snake_st.direction == DIR_RIGHT { ph.x += 1 }
- if ph.x < 0 { snake_st.mode = MODE_OVER }
- if ph.x >= GRID_W { snake_st.mode = MODE_OVER }
- if ph.y < 0 { snake_st.mode = MODE_OVER }
- if ph.y >= GRID_H { snake_st.mode = MODE_OVER }
- if ph.x == snake_st.food_x { if ph.y == snake_st.food_y { snake_st.ate = 1 } }
+ if direction == DIR_UP { ph.y -= 1 }
+ if direction == DIR_DOWN { ph.y += 1 }
+ if direction == DIR_LEFT { ph.x -= 1 }
+ if direction == DIR_RIGHT { ph.x += 1 }
+ if ph.x < 0 { mode = MODE_OVER }
+ if ph.x >= GRID_W { mode = MODE_OVER }
+ if ph.y < 0 { mode = MODE_OVER }
+ if ph.y >= GRID_H { mode = MODE_OVER }
+ if ph.x == food_x { if ph.y == food_y { ate = 1 } }
}
}
# self-collision: head onto any body segment
@@ -120,23 +118,23 @@ program Snake {
if sh.order == 0 {
for (pb, sb) in query [Pos, Seg] {
if sb.order > 0 {
- if pb.x == ph.x { if pb.y == ph.y { snake_st.mode = MODE_OVER } }
+ if pb.x == ph.x { if pb.y == ph.y { mode = MODE_OVER } }
}
}
}
}
- if snake_st.ate == 1 {
- snake_st.score += 1
- spawn Body { Seg { order: snake_st.length }; Pos { x: snake_st.tail_x, y: snake_st.tail_y } }
- snake_st.length += 1
- snake_st.food_x = Random.range(low: 0, high: GRID_W - 1)
- snake_st.food_y = Random.range(low: 0, high: GRID_H - 1)
+ if ate == 1 {
+ score += 1
+ spawn Body { Seg { order: length }; Pos { x: tail_x, y: tail_y } }
+ length += 1
+ food_x = Random.range(low: 0, high: GRID_W - 1)
+ food_y = Random.range(low: 0, high: GRID_H - 1)
}
}
}
}
- handler Draw(snake_st: SnakeState) phase Render {
+ handler Draw phase Render {
Screen.clear(SHADE_BG)
# checkerboard field
for gy in 0 .. GRID_H {
@@ -147,7 +145,7 @@ program Snake {
}
}
# food, inset a few pixels inside its tile
- Screen.fill_rectangle(x: snake_st.food_x * TILE + 3, y: snake_st.food_y * TILE + 3, width: TILE - 6, height: TILE - 6, color: SHADE_FOOD)
+ Screen.fill_rectangle(x: food_x * TILE + 3, y: food_y * TILE + 3, width: TILE - 6, height: TILE - 6, color: SHADE_FOOD)
# every snake segment, straight from the ECS
for (p, s) in query [Pos, Seg] {
var seg_color = SHADE_BODY
@@ -155,8 +153,8 @@ program Snake {
Screen.fill_rectangle(x: p.x * TILE + 1, y: p.y * TILE + 1, width: TILE - 2, height: TILE - 2, color: seg_color)
}
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1)
- Screen.draw_number(x: 52, y: 4, value: snake_st.score, color: SHADE_SCORE, scale: 1)
- if snake_st.mode == MODE_OVER {
+ Screen.draw_number(x: 52, y: 4, value: score, color: SHADE_SCORE, scale: 1)
+ if mode == MODE_OVER {
Screen.draw_text(x: 112, y: 104, text: "GAME OVER", color: SHADE_OVER, scale: 2)
Screen.draw_text(x: 84, y: 140, text: "PRESS ANY KEY", color: Color.White, scale: 1)
}
diff --git a/examples/lang/floats.ludic b/examples/lang/floats.ludic
index 67f6a006..04a6c86c 100644
--- a/examples/lang/floats.ludic
+++ b/examples/lang/floats.ludic
@@ -3,15 +3,13 @@
# Running it prints: 1 2 3 4 5 6 7 8 9 10 11 12
program Floats {
const HALF: float = 0.5
- state FloatsState {
- g_gravity: float = -9.81
- }
+ var g_gravity: float = -9.81
property Body { x: float = 1.25, vy: float, mass: double = 70.5 }
function lerp(a: float, b: float, t: float) -> float { return a + (b - a) * t }
- entry (floats_st: FloatsState) {
+ entry {
let a: float = 0.1
if a * 3 > 0.29 and a * 3 < 0.31 { print(1) } # the int 3 promotes
if lerp(0, 10, 0.25) == 2.5 { print(2) } # ints and literals as floats
@@ -20,7 +18,7 @@ program Floats {
if string(a + 0.2) == "0.3" { print(4) } # shortest text that reads back
let b = new Body
- b.vy = b.vy + floats_st.g_gravity * HALF
+ b.vy = b.vy + g_gravity * HALF
if b.vy < -4.9 and b.vy > -4.91 and b.mass == 70.5 { print(5) }
let v = floats(3)
diff --git a/examples/lang/long_literals.ludic b/examples/lang/long_literals.ludic
index cfe4285c..73e106fe 100644
--- a/examples/lang/long_literals.ludic
+++ b/examples/lang/long_literals.ludic
@@ -5,14 +5,12 @@
# Running it prints: 4294967295 3988292384 -4294967296 1099511627776 -3750763034362895579 -1 1
program LongLiterals {
const MASK: long = 4294967295
- state LongliteralsState {
- poly: long = 3988292384
- }
+ var poly: long = 3988292384
function neg() -> long { return -4294967296 }
- entry (longliterals_st: LongliteralsState) {
+ entry {
let big = 0x10000000000
let fnv = 0xcbf29ce484222325
let small = 0xFFFFFFFF
- print(`{MASK} {longliterals_st.poly} {neg()} {big} {fnv} {small} {MASK > long(2147483647)}`)
+ print(`{MASK} {poly} {neg()} {big} {fnv} {small} {MASK > long(2147483647)}`)
}
}
diff --git a/examples/lang/scenes.ludic b/examples/lang/scenes.ludic
index 9089fc3f..c23c7073 100644
--- a/examples/lang/scenes.ludic
+++ b/examples/lang/scenes.ludic
@@ -15,32 +15,30 @@
# bin/ludic-dev game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes
# printf 'aaaa' | /tmp/scenes
program SceneDemo {
- state ScenedemoState {
- counter: int = 0
- }
+ var counter: int = 0
- handler Boot(scenedemo_st: mut ScenedemoState) phase Start { scenedemo_st.counter = 0; print(1000) }
+ handler Boot phase Start { counter = 0; print(1000) }
scene Title start {
on enter { print(1) }
on exit { print(2) }
layer Main {
- handler Tick(scenedemo_st: mut ScenedemoState) phase Update {
- scenedemo_st.counter += 1
- print(100 + scenedemo_st.counter)
- if scenedemo_st.counter >= 2 { become Play }
+ handler Tick phase Update {
+ counter += 1
+ print(100 + counter)
+ if counter >= 2 { become Play }
}
}
}
scene Play {
- on enter (scenedemo_st: mut ScenedemoState) { print(3); scenedemo_st.counter = 0 }
+ on enter { print(3); counter = 0 }
on exit { print(4) }
layer World {
- handler Step(scenedemo_st: mut ScenedemoState) phase Update {
- scenedemo_st.counter += 1
- print(200 + scenedemo_st.counter)
- if scenedemo_st.counter >= 2 { quit() }
+ handler Step phase Update {
+ counter += 1
+ print(200 + counter)
+ if counter >= 2 { quit() }
}
}
layer Hud {
diff --git a/examples/library/combat_kit.ludic b/examples/library/combat_kit.ludic
index 030a4764..4afce2ce 100644
--- a/examples/library/combat_kit.ludic
+++ b/examples/library/combat_kit.ludic
@@ -12,15 +12,13 @@ program CombatKit {
model Hero { Position, Body, Collider, TopDown, Weapon, Faction, Stats, Dash, Melee }
model Creature { Position, Body, Collider, Faction, Stats, Foe }
model WorldRules { Solids }
- state CombatkitState {
- frames: int = 0
- hero: int = -1
- }
+ var frames: int = 0
+ var hero: int = -1
@On(MeleeHit) handler H { print(5000 + target) }
@On(Crit) handler C { print(6000) }
- handler Tick(combatkit_st: mut CombatkitState) phase Update {
- combatkit_st.frames += 1
- if combatkit_st.frames == 1 {
+ handler Tick phase Update {
+ frames += 1
+ if frames == 1 {
Map.size(20, 15)
Dungeon.arena(Dungeon.random_style(), '#', '.')
Dungeon.set_exit(Side.North, 'D')
@@ -30,19 +28,19 @@ program CombatKit {
print(e.x * 1000 + e.y) # 153196
print(Dungeon.opposite(Side.West) * 10 + Dungeon.at_edge(IVec2.make(0, 3))) # 31
Weapon.def(name: "p", fire_rate: 10, damage: 10, speed: 8, spread: 0, pellets: 1, pattern: 0)
- combatkit_st.hero = spawn Hero { Position { x: 100, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, TopDown { move_speed: 4, aim_mode: AimMode.Manual, aim_angle: 0 }, Faction { id: 1 },
+ hero = spawn Hero { Position { x: 100, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, TopDown { move_speed: 4, aim_mode: AimMode.Manual, aim_angle: 0 }, Faction { id: 1 },
Stats { hp: 100, max_hp: 100 }, Dash { frames: 3, speed: 9, cooldown_frames: 5 }, Melee { range: 30, damage: 7, knockback: 4 } }
- Stats.add(combatkit_st.hero, StatKind.CritPct, 100)
- Stats.add(combatkit_st.hero, StatKind.DamagePct, 50)
- let c = Collider.center(combatkit_st.hero); print(c.x * 1000 + c.y) # 107110
+ Stats.add(hero, StatKind.CritPct, 100)
+ Stats.add(hero, StatKind.DamagePct, 50)
+ let c = Collider.center(hero); print(c.x * 1000 + c.y) # 107110
spawn Creature { Position { x: 120, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, Faction { id: 2 }, Stats { hp: 100, max_hp: 100, thorns: 3 }, Foe { n: 1 } }
Faction.set(1, 2, 2)
- print(Melee.swing(combatkit_st.hero)) # 1 hit (after 6000 and 5002)
+ print(Melee.swing(hero)) # 1 hit (after 6000 and 5002)
for (s, f) in query [Stats, Foe] { print(s.hp) } # 80: 7 * 150% doubled by the crit
- print(Stats.of(combatkit_st.hero).hp) # 97: thorns
- print(Dash.start(combatkit_st.hero, 0, 0) * 10 + Dash.active(combatkit_st.hero)) # 11
+ print(Stats.of(hero).hp) # 97: thorns
+ print(Dash.start(hero, 0, 0) * 10 + Dash.active(hero)) # 11
print(Prefs.max("best", 5) * 10 + Prefs.max("best", 3)) # 10
}
- if combatkit_st.frames == 7 { print(700 + Dash.active(combatkit_st.hero)); print(Position.of(combatkit_st.hero).x); quit() } # 700 110
+ if frames == 7 { print(700 + Dash.active(hero)); print(Position.of(hero).x); quit() } # 700 110
}
}
diff --git a/examples/library/component_access.ludic b/examples/library/component_access.ludic
index 15498449..e07a302d 100644
--- a/examples/library/component_access.ludic
+++ b/examples/library/component_access.ludic
@@ -10,32 +10,30 @@ program ComponentAccess {
model Player { Position, Hero }
model Thing { Position, Tag }
enum HeroState { Idle, Rolling, Swinging }
- state ComponentaccessState {
- hero_state: HeroState = HeroState.Rolling
- player: int = -1
- frames: int = 0
- }
- function step(componentaccess_st: mut ComponentaccessState) -> void {
- machine componentaccess_st.hero_state {
+ var hero_state: HeroState = HeroState.Rolling
+ var player: int = -1
+ var frames: int = 0
+ function step() -> void {
+ machine hero_state {
state Idle { print(100); become Swinging }
state Rolling { print(101); become Idle }
state Swinging { print(102); become Rolling }
}
}
- handler Tick(componentaccess_st: mut ComponentaccessState) phase Update {
- componentaccess_st.frames += 1
- if componentaccess_st.frames == 1 {
- componentaccess_st.player = spawn Player { Position { x: 3, y: 4 }, Hero { roll_frames: 9, flash: 3 } }
+ handler Tick phase Update {
+ frames += 1
+ if frames == 1 {
+ player = spawn Player { Position { x: 3, y: 4 }, Hero { roll_frames: 9, flash: 3 } }
spawn Thing { Position { x: 30, y: 40 }, Tag { n: 7 } }
- print(Hero.of(componentaccess_st.player).roll_frames) # 9
- Hero.of(componentaccess_st.player).iframes = 20
- Hero.of(componentaccess_st.player).iframes -= 5
- print(Hero.of(componentaccess_st.player).iframes) # 15
- let hero = Hero.of(componentaccess_st.player)
+ print(Hero.of(player).roll_frames) # 9
+ Hero.of(player).iframes = 20
+ Hero.of(player).iframes -= 5
+ print(Hero.of(player).iframes) # 15
+ let hero = Hero.of(player)
hero.roll_frames += 1
- print(Hero.of(componentaccess_st.player).roll_frames) # 10
- print(Position.of(componentaccess_st.player).x + Position.of(1).x) # 33
- print(Hero.has(componentaccess_st.player)) # 1
+ print(Hero.of(player).roll_frames) # 10
+ print(Position.of(player).x + Position.of(1).x) # 33
+ print(Hero.has(player)) # 1
print(Hero.has(1)) # 0
print(Hero.has(-1)) # 0
print(Tag.has(1)) # 1
@@ -44,13 +42,13 @@ program ComponentAccess {
print(Position.count()) # 1: the player
spawn Thing { Position { x: 1 } }
}
- if componentaccess_st.frames == 2 {
- step(componentaccess_st); step(componentaccess_st); step(componentaccess_st) # 101 100 102
- print(componentaccess_st.hero_state) # 1
+ if frames == 2 {
+ step(); step(); step() # 101 100 102
+ print(hero_state) # 1
print(Position.count()) # 2: the player and the thing spawned after
}
- if componentaccess_st.frames == 3 {
- print(Hero.of(componentaccess_st.player).flash) # ticked twice: 1
+ if frames == 3 {
+ print(Hero.of(player).flash) # ticked twice: 1
Position.despawn_all()
print(Position.count()) # 0
quit()
diff --git a/examples/library/gameplay_foundation.ludic b/examples/library/gameplay_foundation.ludic
index 1e3b32c8..3d840b19 100644
--- a/examples/library/gameplay_foundation.ludic
+++ b/examples/library/gameplay_foundation.ludic
@@ -17,21 +17,19 @@ program GameplayFoundation {
# a game-supplied policy: while "enraged" is set, halve incoming damage by
# rewriting the pending amount (lever 6, the mutable hook), and hard-veto any
# hit over 100 (lever 4, cancel). Both are just listeners — no engine fork.
- state GameplayfoundationState {
- enraged: int = 0
- deaths: int = 0
- }
- @On(DamageAboutToApply) handler Ward(gameplayfoundation_st: GameplayfoundationState) {
+ var enraged: int = 0
+ @On(DamageAboutToApply) handler Ward {
if amount > 100 { cancel }
- if gameplayfoundation_st.enraged == 1 { Combat.set_amount(amount / 2) }
+ if enraged == 1 { Combat.set_amount(amount / 2) }
}
- @On(Died) handler Tally(gameplayfoundation_st: mut GameplayfoundationState) { gameplayfoundation_st.deaths += 1 }
+ var deaths: int = 0
+ @On(Died) handler Tally { deaths += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
- entry (gameplayfoundation_st: mut GameplayfoundationState) {
+ entry {
let PS = World.prop_id("Stats")
let fhp = World.field_id(PS, "hp")
let fatk = World.field_id(PS, "atk")
@@ -67,9 +65,9 @@ program GameplayFoundation {
print(World.get(t, PS, fhp)) # 85
# mutable hook: enrage halves the next hit ((20 - 5) -> 7)
- gameplayfoundation_st.enraged = 1
+ enraged = 1
print(Combat.damage(t, a, 20)) # 7
- gameplayfoundation_st.enraged = 0
+ enraged = 0
# veto hook: an over-100 hit is cancelled (0 applied)
print(bi(Combat.damage(t, a, 200) == 0)) # 1
@@ -77,7 +75,7 @@ program GameplayFoundation {
# finish it off with sub-100 hits so the veto never blocks the kill
var guard = 0
while (World.get(t, PS, fhp) > 0) and (guard < 100) { Combat.damage(t, a, 90); guard += 1 }
- print(gameplayfoundation_st.deaths) # 1
+ print(deaths) # 1
quit()
}
diff --git a/examples/library/input_auto.ludic b/examples/library/input_auto.ludic
index 5c1ad727..74bcaca2 100644
--- a/examples/library/input_auto.ludic
+++ b/examples/library/input_auto.ludic
@@ -12,11 +12,9 @@ program InputAuto {
property Tag { n: int = 0 }
model Player { Tag }
- state InputautoState {
- hits: int = 0
- }
+ var hits: int = 0
@OnStart handler Boot { Input.action("go", 'l') } # 'l' == 108
- handler Sense(inputauto_st: mut InputautoState) phase Update { if Input.active("go") { inputauto_st.hits += 1 } }
- @OnQuit handler Report(inputauto_st: InputautoState) { print(inputauto_st.hits) }
+ handler Sense phase Update { if Input.active("go") { hits += 1 } }
+ @OnQuit handler Report { print(hits) }
}
diff --git a/examples/library/managers.ludic b/examples/library/managers.ludic
index 7df9f023..5012d49e 100644
--- a/examples/library/managers.ludic
+++ b/examples/library/managers.ludic
@@ -8,12 +8,10 @@ program Managers {
property Tag { n: int = 0 }
model Thing { Position, Body, Sprite, Tag, TopDown }
model WorldRules { Solids }
- state ManagersState {
- frames: int = 0
- }
- handler Tick(managers_st: mut ManagersState) phase Update {
- managers_st.frames += 1
- if managers_st.frames == 1 {
+ var frames: int = 0
+ handler Tick phase Update {
+ frames += 1
+ if frames == 1 {
Map.size(10, 8)
Map.fill(glyph: '.')
Map.border(glyph: '#')
@@ -38,7 +36,7 @@ program Managers {
Fx.number(x: 20, y: 20, value: 42, color: 0xffff00)
spawn Thing { Position { x: 20, y: 20 }, Sprite { id: 5, move_id: 9, face: 1, flash: 3, blink: 6 }, TopDown { want_x: -1 } }
}
- if managers_st.frames == 3 { print(Map.is_solid(x: 0, y: 0) * 10 + Map.is_solid_at(x: 40, y: 40)) } # 10
- if managers_st.frames == 5 { for (s, t) in query [Sprite, Tag] { print(s.flip * 100 + s.flash * 10 + s.blink) }; quit() } # 102
+ if frames == 3 { print(Map.is_solid(x: 0, y: 0) * 10 + Map.is_solid_at(x: 40, y: 40)) } # 10
+ if frames == 5 { for (s, t) in query [Sprite, Tag] { print(s.flip * 100 + s.flash * 10 + s.blink) }; quit() } # 102
}
}
diff --git a/examples/library/prefabs.ludic b/examples/library/prefabs.ludic
index 6729a1eb..3b518b06 100644
--- a/examples/library/prefabs.ludic
+++ b/examples/library/prefabs.ludic
@@ -8,11 +8,9 @@ program Prefabs {
model Creature { Position, Enemy }
prefab Foe: Creature { Enemy { team: 2, hp: 1 }, Position { y: 9 } }
prefab Grunt: Foe { Enemy { kind: 1, hp: 30 } }
- state PrefabsState {
- last: int = -1
- }
- @OnSpawn(Creature) handler Remember(prefabs_st: mut PrefabsState) { prefabs_st.last = self() }
- entry (prefabs_st: PrefabsState) {
+ var last: int = -1
+ @OnSpawn(Creature) handler Remember { last = self() }
+ entry {
let g = spawn Grunt { Position { x: 5 } } # Foe's presets, Grunt's, then this
print(Enemy.of(g).team * 100000 + Enemy.of(g).hp * 1000 + Enemy.of(g).kind * 100 + Position.of(g).x * 10 + Position.of(g).y)
let n = Prefab.spawn(name: "Grunt")
@@ -22,6 +20,6 @@ program Prefabs {
print(Position.of(placed).x)
print(Position.of(placed).y)
print(Prefab.spawn(name: "Nope")) # no such prefab
- print(prefabs_st.last) # self() in the hook: the last spawn
+ print(last) # self() in the hook: the last spawn
}
}
diff --git a/examples/library/scene_menus.ludic b/examples/library/scene_menus.ludic
index 94c3a635..c3978392 100644
--- a/examples/library/scene_menus.ludic
+++ b/examples/library/scene_menus.ludic
@@ -6,20 +6,18 @@ program SceneMenus {
import "ludic.core/components.ludic"
property Pos { x: int = 0 }
model Thing { Pos }
- state ScenemenusState {
- menu_font: int = 0
- frames: int = 0
- }
+ var menu_font: int = 0
+ var frames: int = 0
ui Menu {
panel id: Root w: 100 pad: 4 bg: Color.Gunmetal border: Color.SlateGray align: center {
- button id: Go text: "Go" font: ScenemenusState.menu_font size: 16 w: 80 goto: Play
+ button id: Go text: "Go" font: menu_font size: 16 w: 80 goto: Play
}
}
scene Splash start lasts 1 then Title { on enter { print(100) } }
scene Title shows Menu {
on enter { print(200) }
on exit { print(202) }
- layer L { handler Tick(scenemenus_st: mut ScenemenusState) phase Update { scenemenus_st.frames += 1; if scenemenus_st.frames == 1 { print(201); emit UiClicked(id: UI_Go) } } }
+ layer L { handler Tick phase Update { frames += 1; if frames == 1 { print(201); emit UiClicked(id: UI_Go) } } }
}
@On(UiClicked) handler Report { print(id) } # UI_Go is widget 1 (the root panel is 0)
scene Play {
diff --git a/examples/library/sprite_atlas.ludic b/examples/library/sprite_atlas.ludic
index 57b6b792..1af47be2 100644
--- a/examples/library/sprite_atlas.ludic
+++ b/examples/library/sprite_atlas.ludic
@@ -10,25 +10,23 @@
program SpriteAtlas {
import "ludic.core/components.ludic"
- state SpriteatlasState {
- TALL: int = 0
- }
+ var TALL: int = 0
function bi(b: bool) -> int { if b { return 1 }; return 0 }
- @OnStart handler Boot(spriteatlas_st: mut SpriteatlasState) {
+ @OnStart handler Boot {
let sheet = Sprite.sheet("assets/kenney/tiny-dungeon/Tilemap/tilemap_packed.png", 16, 16)
- spriteatlas_st.TALL = Sprite.cell_span(sheet, 0, 0, 1, 2) # a 16x32 sprite: two cells tall
- spawn Tall { Position { x: 40, y: 40 }, Sprite { id: spriteatlas_st.TALL, atlas: 1 } }
- spawn Tinted { Position { x: 100, y: 40 }, Sprite { id: spriteatlas_st.TALL, atlas: 1, tint: 0xff0000 } }
- spawn Table { Position { x: 160, y: 40 }, Sprite { id: spriteatlas_st.TALL } } # atlas: 0 -> the (empty) 16x16 table
+ TALL = Sprite.cell_span(sheet, 0, 0, 1, 2) # a 16x32 sprite: two cells tall
+ spawn Tall { Position { x: 40, y: 40 }, Sprite { id: TALL, atlas: 1 } }
+ spawn Tinted { Position { x: 100, y: 40 }, Sprite { id: TALL, atlas: 1, tint: 0xff0000 } }
+ spawn Table { Position { x: 160, y: 40 }, Sprite { id: TALL } } # atlas: 0 -> the (empty) 16x16 table
}
# An empty Render handler: the engine sprite system draws the Sprite entities.
handler Draw phase Render { }
- @OnQuit handler Report(spriteatlas_st: SpriteatlasState) {
- print(Sprite.height(spriteatlas_st.TALL)) # 32 — the span is two cells tall
+ @OnQuit handler Report {
+ print(Sprite.height(TALL)) # 32 — the span is two cells tall
var top = 0
for yy in 40 .. 56 { for xx in 40 .. 56 { if Screen.pixel(xx, yy) != 0 { top = 1 } } }
diff --git a/examples/library/sprite_render.ludic b/examples/library/sprite_render.ludic
index bac6a39e..48139610 100644
--- a/examples/library/sprite_render.ludic
+++ b/examples/library/sprite_render.ludic
@@ -9,14 +9,12 @@
program SpriteRender {
import "ludic.core/components.ludic"
- state SpriterenderState {
- HERO: int = 0
- }
+ var HERO: int = 0
- @OnStart handler Boot(spriterender_st: mut SpriterenderState) {
- spriterender_st.HERO = png_load("assets/kenney/tiny-dungeon/Tiles/tile_0096.png")
- spawn Shown { Position { x: 40, y: 40 }, Sprite { id: spriterender_st.HERO } }
- spawn Hidden { Position { x: 100, y: 40 }, Sprite { id: spriterender_st.HERO, hidden: 1 } }
+ @OnStart handler Boot {
+ HERO = png_load("assets/kenney/tiny-dungeon/Tiles/tile_0096.png")
+ spawn Shown { Position { x: 40, y: 40 }, Sprite { id: HERO } }
+ spawn Hidden { Position { x: 100, y: 40 }, Sprite { id: HERO, hidden: 1 } }
}
# An empty Render handler: the engine sprite system draws the Sprite entities.
diff --git a/examples/library/threads.ludic b/examples/library/threads.ludic
index 24327230..91ad7175 100644
--- a/examples/library/threads.ludic
+++ b/examples/library/threads.ludic
@@ -6,27 +6,25 @@
program Threads {
const N: int = 20000
- state ThreadsState {
- calls: int = 0 # an atomic handle, made on the main thread before any work
- lock: int = 0 # a mutex handle
- total: long = 0 # guarded by `lock`
- }
+ var calls: int = 0 # an atomic handle, made on the main thread before any work
+ var lock: int = 0 # a mutex handle
+ var total: long = 0 # guarded by `lock`
# out[i] = i * i: every index written exactly once, by whichever thread claimed it
function square(i: int, out: words) -> void { out[i] = i * i }
# count the call atomically, and add i to a shared total under the mutex
- function tally(threads_st: mut ThreadsState, i: int, ctx: pointer) -> void {
- Sync.add(threads_st.calls, 1)
- Sync.lock(threads_st.lock)
- threads_st.total = threads_st.total + i
- Sync.unlock(threads_st.lock)
+ function tally(i: int, ctx: pointer) -> void {
+ Sync.add(calls, 1)
+ Sync.lock(lock)
+ total = total + i
+ Sync.unlock(lock)
}
# remember which calls ran on a pool thread
function placed(i: int, out: words) -> void { if Job.is_worker() { out[i] = 1 } else { out[i] = 0 } }
- entry (threads_st: mut ThreadsState) {
+ entry {
if Sync.cpu_count() >= 1 { print(1) }
if not Job.is_worker() { print(2) }
@@ -36,12 +34,12 @@ program Threads {
for i in 0 .. N { if sq[i] != i * i { right = false } }
if right { print(3) }
- threads_st.calls = Sync.atomic()
- threads_st.lock = Sync.mutex()
+ calls = Sync.atomic()
+ lock = Sync.mutex()
Job.parallel_for(N, fn tally, null)
- if Sync.get(threads_st.calls) == N { print(4) }
+ if Sync.get(calls) == N { print(4) }
let want: long = N * (N - 1) / 2
- if threads_st.total == want { print(5) }
+ if total == want { print(5) }
let ran = words(N)
Job.parallel_for(N, fn placed, ran)
diff --git a/examples/library/ui_anim.ludic b/examples/library/ui_anim.ludic
index 3a8d0bed..64207c23 100644
--- a/examples/library/ui_anim.ludic
+++ b/examples/library/ui_anim.ludic
@@ -4,26 +4,24 @@
import "ludic.ui"
program UiAnim {
numbers float
- state UianimState {
- dim: bool = false
- }
- view Anim (uianim_st: UianimState) {
- dim = uianim_st.dim
+ var dim: bool = false
+ view Anim {
+ dim = dim
}
const PAGE: string = " a b c title moved big wide narrow after fallback 1 2 3 4 a sentence long enough that it has to wrap onto several lines
\n h
a
b
c
" - function clock(uiease_st: UieaseState) -> float { return uiease_st.now } - function nodes(ui_st: mut UiState, name: string) -> UiNode { - ui_show(ui_st, name, view_ease(), 0.0, 0.0, 200.0, 200.0) - return ui_nodes(ui_st, name, view_ease()) + function clock() -> float { return now } + function nodes(name: string) -> UiNode { + ui_show(name, view_ease(), 0.0, 0.0, 200.0, 200.0) + return ui_nodes(name, view_ease()) } # one rate, on its own screen: the slide from its first frame, the fade from half a second in - function at_rate(ui_st: mut UiState, uiease_st: mut UieaseState, hz: int) -> string { + function at_rate(hz: int) -> string { let name = `r{hz}` - uiease_st.dim = false + dim = false var mid = "" var slid = false var landed = false var over = false for k in 0 .. hz + 1 { - uiease_st.now = float(k) / float(hz) - if k == hz / 2 { uiease_st.dim = true } - let r = nodes(ui_st, name) + now = float(k) / float(hz) + if k == hz / 2 { dim = true } + let r = nodes(name) let go = r.children[0] let soft = r.children[1] let want = r.children[2].alpha @@ -41,17 +39,17 @@ program UiEase { } return `{hz}: {mid} slid {slid} landed {landed} left {over}` } - entry (ui_st: mut UiState, uiease_st: mut UieaseState) { - ui_backend(ui_st, new UiBackend) - ui_clock(ui_st, fn clock) + entry { + ui_backend(new UiBackend) + ui_clock(fn clock) var page = "

inside
carefuloutside
\nmid
\n \nnever
\nHello
Hello
Hello
Hello
a
b
c
a
b
.po), and the atlas a
# language with no X-Font draws in
-export function i18n_config(i18n_st: mut I18nState, dir: string, default_font: string) -> void {
- i18n_st.lang_dir = dir
- i18n_st.font_default = default_font
- i18n_st.codes = null
+export function i18n_config(dir: string, default_font: string) -> void {
+ lang_dir = dir
+ font_default = default_font
+ codes = null
}
diff --git a/packages/ludic.i18n/state.ludic b/packages/ludic.i18n/state.ludic
index b59e3318..8fce2317 100644
--- a/packages/ludic.i18n/state.ludic
+++ b/packages/ludic.i18n/state.ludic
@@ -1,18 +1,47 @@
# state.ludic - the languages found, the one in use, and the loaded file's lookups
+var on: bool = false # a language other than English is drawing
+var cur: string = "en"
+var codes: []string = null # every language found, "en" first
+var names: []string = null
+var paths: []string = null
+var fonts: []string = null # a font atlas directory, "" for the game's own
+var font_now: string = ""
+var exact: Dict = null # msgid -> index into out
+var out: []string = null
+var lits: []string = null # every pattern's literal pieces, one run per pattern
+var pat_out: []string = null
+var pat_l0: words = null # where a pattern's pieces start in lits
+var pat_nl: words = null # how many pieces (holes + 1)
+var pat_sc: words = null # bytes of English it pins down: more is a better match
+var pat_ph: words = null # by piece: the hole number that follows it
+var pat_n: int = 0
+var pfx: Dict = null # a pattern's first four bytes -> its chain head + 1
+var sfx: Dict = null # else its last four
+var gen_head: int = 0 # else the short list, tried on short strings only
+var nx_p: words = null
+var nx_s: words = null
+var nx_g: words = null
+var cache: Dict = null # English as drawn -> index into cache_out
+var cache_out: []string = null
+var caps: []string = null
+var hit: bool = false
+var plural: Dict = null # a plural msgid (the singular) -> index into forms / 3
+var forms: []string = null # three forms per entry, "" where the file has fewer
+var plural_rule: int = 0 # PL_*
const CACHE_MAX: int = 4000
const CAPS: int = 16
-export function i18n_on(i18n_st: I18nState) -> bool { return i18n_st.on }
-export function i18n_current(i18n_st: I18nState) -> string { return i18n_st.cur }
-export function i18n_font_now(i18n_st: I18nState) -> string { return i18n_st.font_now }
-export function i18n_exact_count(i18n_st: I18nState) -> int {
- if i18n_st.out == null { return 0 }
- return len(i18n_st.out)
+export function i18n_on() -> bool { return on }
+export function i18n_current() -> string { return cur }
+export function i18n_font_now() -> string { return font_now }
+export function i18n_exact_count() -> int {
+ if out == null { return 0 }
+ return len(out)
}
-export function i18n_pattern_count(i18n_st: I18nState) -> int { return i18n_st.pat_n }
+export function i18n_pattern_count() -> int { return pat_n }
# the overlay's own font, as the overlay was opened with it
-export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path }
+export function i18n_font_base(path: string) -> void { font_now = path }
diff --git a/packages/ludic.i18n/tests/i18n_test.ludic b/packages/ludic.i18n/tests/i18n_test.ludic
index 61e101f8..dd525380 100644
--- a/packages/ludic.i18n/tests/i18n_test.ludic
+++ b/packages/ludic.i18n/tests/i18n_test.ludic
@@ -4,20 +4,18 @@
import "ludic.i18n"
program I18nTest {
numbers float
- state I18ntestState {
- forced_code: string = ""
- fonts_set: []string = null
- font_ok: bool = true
- }
+ var forced_code: string = ""
+ var fonts_set: []string = null
+ var font_ok: bool = true
function dir() -> string { return Os.temp_dir() + "/ludic-i18n-test" }
function mods() -> string { return dir() + "/mods" }
function t_mods() -> string { return mods() }
- function t_forced(i18ntest_st: I18ntestState) -> string { return i18ntest_st.forced_code }
+ function t_forced() -> string { return forced_code }
function t_ready() -> bool { return true }
- function t_font(i18ntest_st: mut I18ntestState, d: string) -> bool {
- push(i18ntest_st.fonts_set, d)
- return i18ntest_st.font_ok
+ function t_font(d: string) -> bool {
+ push(fonts_set, d)
+ return font_ok
}
bind I18nWorld { mod_dir: fn t_mods, forced: fn t_forced, font_ready: fn t_ready, set_font: fn t_font }
@@ -38,116 +36,116 @@ program I18nTest {
return s
}
- function fresh(i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) -> void {
- i18ntest_st.forced_code = ""
- i18ntest_st.fonts_set = new []string
- i18ntest_st.font_ok = true
+ function fresh() -> void {
+ forced_code = ""
+ fonts_set = new []string
+ font_ok = true
Fs.mkdir(dir())
Fs.mkdir(mods())
Fs.write_text(dir() + "/languages.txt", "# shipped\ntt\nnope\n")
Fs.write_text(dir() + "/tt.po", tt())
Fs.remove(mods() + "/zz.po")
- i18n_config(i18n_st, dir(), "font_default")
- i18n_init(i18n_st)
+ i18n_config(dir(), "font_default")
+ i18n_init()
}
- test "the shipped list and English are found, a missing file is not" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
- expect_eq(i18n_count(i18n_st), 2)
- expect(i18n_code_at(i18n_st, 0) == "en")
- expect_eq(i18n_index(i18n_st, "tt"), 1)
- expect_eq(i18n_index(i18n_st, "nope"), 0)
- expect(i18n_name_at(i18n_st, 1) == "Test Tongue")
- expect(i18n_font_at(i18n_st, 1) == dir() + "/font_tt")
- expect(L(i18n_st, "Settings") == "Settings")
+ test "the shipped list and English are found, a missing file is not" {
+ fresh()
+ expect_eq(i18n_count(), 2)
+ expect(i18n_code_at(0) == "en")
+ expect_eq(i18n_index("tt"), 1)
+ expect_eq(i18n_index("nope"), 0)
+ expect(i18n_name_at(1) == "Test Tongue")
+ expect(i18n_font_at(1) == dir() + "/font_tt")
+ expect(L("Settings") == "Settings")
}
- test "exact lines, patterns with translated holes, and English where unknown" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
- i18n_use(i18n_st, "tt")
- expect(i18n_on(i18n_st))
- expect(i18n_current(i18n_st) == "tt")
- expect(L(i18n_st, "Settings") == "Zettings")
- expect(L(i18n_st, "Day 12") == "12. Gun")
- expect(L(i18n_st, "+2 Plant fiber") == "Lif +2")
- expect(L(i18n_st, "You saw a deer.") == "geyik gordun.")
- expect(L(i18n_st, "zzqx words nobody wrote") == "zzqx words nobody wrote")
- expect(L(i18n_st, "Multiline") == "Coksatir\n")
- expect_eq(i18n_exact_count(i18n_st), 6)
- expect_eq(i18n_pattern_count(i18n_st), 3)
+ test "exact lines, patterns with translated holes, and English where unknown" {
+ fresh()
+ i18n_use("tt")
+ expect(i18n_on())
+ expect(i18n_current() == "tt")
+ expect(L("Settings") == "Zettings")
+ expect(L("Day 12") == "12. Gun")
+ expect(L("+2 Plant fiber") == "Lif +2")
+ expect(L("You saw a deer.") == "geyik gordun.")
+ expect(L("zzqx words nobody wrote") == "zzqx words nobody wrote")
+ expect(L("Multiline") == "Coksatir\n")
+ expect_eq(i18n_exact_count(), 6)
+ expect_eq(i18n_pattern_count(), 3)
}
- test "a paragraph a sentence at a time, and a layout's padding kept" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
- i18n_use(i18n_st, "tt")
- expect(L(i18n_st, "You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
- expect(L(i18n_st, "zzqx. zzqy.") == "zzqx. zzqy.")
- expect(L(i18n_st, " owned") == " senin")
- expect(L(i18n_st, " zzqx") == " zzqx")
+ test "a paragraph a sentence at a time, and a layout's padding kept" {
+ fresh()
+ i18n_use("tt")
+ expect(L("You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
+ expect(L("zzqx. zzqy.") == "zzqx. zzqy.")
+ expect(L(" owned") == " senin")
+ expect(L(" zzqx") == " zzqx")
}
- test "plurals by the file's rule, and English by one and many" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
- expect(Ln(i18n_st, "{1} trout", "{1} trouts", 1) == "1 trout")
- expect(Ln(i18n_st, "{1} trout", "{1} trouts", 3) == "3 trouts")
- i18n_use(i18n_st, "tt")
- expect_eq(i18n_plural_rule(i18n_st), PL_SLAVIC)
- expect(Ln(i18n_st, "{1} trout", "{1} trouts", 21) == "21 alabalik")
- expect(Ln(i18n_st, "{1} trout", "{1} trouts", 5) == "5 alabaliklar")
- expect(Ln(i18n_st, "{1} fish", "{1} fishes", 2) == "2 fishes")
+ test "plurals by the file's rule, and English by one and many" {
+ fresh()
+ expect(Ln("{1} trout", "{1} trouts", 1) == "1 trout")
+ expect(Ln("{1} trout", "{1} trouts", 3) == "3 trouts")
+ i18n_use("tt")
+ expect_eq(i18n_plural_rule(), PL_SLAVIC)
+ expect(Ln("{1} trout", "{1} trouts", 21) == "21 alabalik")
+ expect(Ln("{1} trout", "{1} trouts", 5) == "5 alabaliklar")
+ expect(Ln("{1} fish", "{1} fishes", 2) == "2 fishes")
expect_eq(i18n_plural_form(PL_NOT_ONE, 0), 1)
expect_eq(i18n_plural_form(PL_ABOVE_ONE, 0), 0)
expect_eq(i18n_plural_form(PL_SLAVIC, 3), 1)
expect_eq(i18n_plural_form(PL_ONE_FORM, 7), 0)
}
- test "the font follows the language, and falls back when it will not load" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
- i18n_font_base(i18n_st, "font_default")
- i18n_use(i18n_st, "tt")
- expect_eq(len(i18ntest_st.fonts_set), 1)
- expect(i18n_font_now(i18n_st) == dir() + "/font_tt")
- i18n_use(i18n_st, "en")
- expect(i18n_font_now(i18n_st) == "font_default")
- i18ntest_st.font_ok = false
- i18n_use(i18n_st, "tt")
- expect(i18n_font_now(i18n_st) == "font_default")
+ test "the font follows the language, and falls back when it will not load" {
+ fresh()
+ i18n_font_base("font_default")
+ i18n_use("tt")
+ expect_eq(len(fonts_set), 1)
+ expect(i18n_font_now() == dir() + "/font_tt")
+ i18n_use("en")
+ expect(i18n_font_now() == "font_default")
+ font_ok = false
+ i18n_use("tt")
+ expect(i18n_font_now() == "font_default")
}
- test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
+ test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" {
+ fresh()
Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("Settings", "Mettings"))
Fs.write_text(mods() + "/tt.po", header("Tongue Two", "") + po_line("Settings", "Z2"))
- i18n_init(i18n_st)
- expect_eq(i18n_count(i18n_st), 3)
- expect(i18n_name_at(i18n_st, i18n_index(i18n_st, "zz")) == "Mod Tongue")
- i18n_use(i18n_st, "zz")
- expect(L(i18n_st, "Settings") == "Mettings")
- expect(L(i18n_st, "Day 3") == "Day 3")
- i18n_use(i18n_st, "tt")
- expect(L(i18n_st, "Settings") == "Z2")
+ i18n_init()
+ expect_eq(i18n_count(), 3)
+ expect(i18n_name_at(i18n_index("zz")) == "Mod Tongue")
+ i18n_use("zz")
+ expect(L("Settings") == "Mettings")
+ expect(L("Day 3") == "Day 3")
+ i18n_use("tt")
+ expect(L("Settings") == "Z2")
Fs.remove(mods() + "/zz.po")
Fs.remove(mods() + "/tt.po")
- i18n_init(i18n_st)
- expect_eq(i18n_index(i18n_st, "zz"), 0)
- i18n_use(i18n_st, "en")
- expect(not i18n_on(i18n_st))
- expect(L(i18n_st, "Settings") == "Settings")
+ i18n_init()
+ expect_eq(i18n_index("zz"), 0)
+ i18n_use("en")
+ expect(not i18n_on())
+ expect(L("Settings") == "Settings")
}
- test "a forced code wins over the one asked for" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
- fresh(i18n_st, i18ntest_st)
- i18ntest_st.forced_code = "tt"
- i18n_use(i18n_st, "en")
- expect(i18n_current(i18n_st) == "tt")
+ test "a forced code wins over the one asked for" {
+ fresh()
+ forced_code = "tt"
+ i18n_use("en")
+ expect(i18n_current() == "tt")
}
- test "a header field, and a .po loaded from text" (i18n_st: mut I18nState) {
+ test "a header field, and a .po loaded from text" {
expect(i18n_header(tt(), "X-Language-Name") == "Test Tongue")
expect(i18n_header(tt(), "X-Nothing") == "")
- i18n_load_text(i18n_st, po_line("Quit", "Cik"))
- expect(L(i18n_st, "Quit") == "Cik")
- i18n_off(i18n_st)
- expect(L(i18n_st, "Quit") == "Quit")
+ i18n_load_text(po_line("Quit", "Cik"))
+ expect(L("Quit") == "Cik")
+ i18n_off()
+ expect(L("Quit") == "Quit")
}
}
diff --git a/packages/ludic.i18n/translate.ludic b/packages/ludic.i18n/translate.ludic
index 2365b234..b8032a77 100644
--- a/packages/ludic.i18n/translate.ludic
+++ b/packages/ludic.i18n/translate.ludic
@@ -1,26 +1,26 @@
# translate.ludic - what the game draws, in the language in use: exact, else a pattern, else the
# padding a layout put round it taken off, else a paragraph a sentence at a time; English otherwise
-export function L(i18n_st: mut I18nState, s: string) -> string {
- if not i18n_st.on { return s }
- let ci = Dict.get_or(i18n_st.cache, s, -1)
- if ci >= 0 { return i18n_st.cache_out[ci] }
- let r = whole(i18n_st, s)
- if Dict.size(i18n_st.cache) >= CACHE_MAX {
- Dict.clear(i18n_st.cache)
- i18n_st.cache_out = new []string
+export function L(s: string) -> string {
+ if not on { return s }
+ let ci = Dict.get_or(cache, s, -1)
+ if ci >= 0 { return cache_out[ci] }
+ let r = whole(s)
+ if Dict.size(cache) >= CACHE_MAX {
+ Dict.clear(cache)
+ cache_out = new []string
}
- Dict.set(i18n_st.cache, s, len(i18n_st.cache_out))
- push(i18n_st.cache_out, r)
+ Dict.set(cache, s, len(cache_out))
+ push(cache_out, r)
return r
}
-function whole(i18n_st: mut I18nState, s: string) -> string {
+function whole(s: string) -> string {
let n = len(s)
if n == 0 { return s }
- let e = Dict.get_or(i18n_st.exact, s, -1)
- if e >= 0 { return i18n_st.out[e] }
- let r = pattern(i18n_st, s, 0)
- if i18n_st.hit { return r }
+ let e = Dict.get_or(exact, s, -1)
+ if e >= 0 { return out[e] }
+ let r = pattern(s, 0)
+ if hit { return r }
# the padding a layout put round a word: " owned"
var a = 0
var b = n
@@ -29,15 +29,15 @@ function whole(i18n_st: mut I18nState, s: string) -> string {
if a > 0 or b < n {
if b <= a { return s }
let core: string = s[a..b]
- let t = whole(i18n_st, core)
+ let t = whole(core)
if t == core { return s }
return s[0..a] + t + s[b..n]
}
- return sentences(i18n_st, s)
+ return sentences(s)
}
# a paragraph built from sentences, one at a time; English stays where a sentence is unknown
-function sentences(i18n_st: mut I18nState, s: string) -> string {
+function sentences(s: string) -> string {
let n = len(s)
var r = ""
var start = 0
@@ -47,8 +47,8 @@ function sentences(i18n_st: mut I18nState, s: string) -> string {
while i < n - 1 {
let c = s[i]
if (c == 46 or c == 33 or c == 63) and s[i + 1] == 32 {
- let t = sentence(i18n_st, s[start..i + 1])
- if i18n_st.hit { any = true }
+ let t = sentence(s[start..i + 1])
+ if hit { any = true }
r = r + t + " "
start = i + 2
pieces += 1
@@ -58,18 +58,18 @@ function sentences(i18n_st: mut I18nState, s: string) -> string {
i += 1
}
if pieces == 0 { return s }
- let t = sentence(i18n_st, s[start..n])
- if i18n_st.hit { any = true }
+ let t = sentence(s[start..n])
+ if hit { any = true }
if not any { return s }
return r + t
}
-function sentence(i18n_st: mut I18nState, p: string) -> string {
- i18n_st.hit = false
- let e = Dict.get_or(i18n_st.exact, p, -1)
+function sentence(p: string) -> string {
+ hit = false
+ let e = Dict.get_or(exact, p, -1)
if e >= 0 {
- i18n_st.hit = true
- return i18n_st.out[e]
+ hit = true
+ return out[e]
}
- return pattern(i18n_st, p, 0)
+ return pattern(p, 0)
}
diff --git a/packages/ludic.i18n/use.ludic b/packages/ludic.i18n/use.ludic
index e4da8d39..09fa44b7 100644
--- a/packages/ludic.i18n/use.ludic
+++ b/packages/ludic.i18n/use.ludic
@@ -1,43 +1,43 @@
# use.ludic - choosing a language: its file loaded into the lookups, and the font atlas it names
-export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
- if i18n_st.codes == null { i18n_init(i18n_st) }
- i18n_st.on = false
- i18n_st.cur = "en"
- var k = i18n_index(i18n_st, code)
+export function i18n_use(code: string) -> void {
+ if codes == null { i18n_init() }
+ on = false
+ cur = "en"
+ var k = i18n_index(code)
let forced = I18nWorld.forced()
- if len(forced) > 0 { k = i18n_index(i18n_st, forced) }
+ if len(forced) > 0 { k = i18n_index(forced) }
var font = ""
if k > 0 {
- let text = Fs.read_text(i18n_st.paths[k])
+ let text = Fs.read_text(paths[k])
if text != null {
- i18n_load_text(i18n_st, text)
- i18n_st.on = true
- i18n_st.cur = i18n_st.codes[k]
- font = i18n_st.fonts[k]
+ i18n_load_text(text)
+ on = true
+ cur = codes[k]
+ font = fonts[k]
}
}
# the atlas follows the language, and is only reloaded when it actually changes
- if len(font) == 0 { font = i18n_st.font_default }
- if I18nWorld.font_ready() and not (font == i18n_st.font_now) {
- if I18nWorld.set_font(font) { i18n_st.font_now = font } else if not (i18n_st.font_now == i18n_st.font_default) {
- I18nWorld.set_font(i18n_st.font_default)
- i18n_st.font_now = i18n_st.font_default
+ if len(font) == 0 { font = font_default }
+ if I18nWorld.font_ready() and not (font == font_now) {
+ if I18nWorld.set_font(font) { font_now = font } else if not (font_now == font_default) {
+ I18nWorld.set_font(font_default)
+ font_now = font_default
}
}
}
# a .po's text as the lookups, whatever the language is called (a test, a file from elsewhere)
-export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void {
+export function i18n_load_text(text: string) -> void {
let es = i18n_parse(text)
- i18n_st.exact = Dict.new()
- i18n_st.out = new []string
- i18n_st.cache = Dict.new()
- i18n_st.cache_out = new []string
- i18n_st.caps = new []string
- for c in 0 .. CAPS { push(i18n_st.caps, "") }
- i18n_st.plural = Dict.new()
- i18n_st.forms = new []string
- i18n_st.plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
+ exact = Dict.new()
+ out = new []string
+ cache = Dict.new()
+ cache_out = new []string
+ caps = new []string
+ for c in 0 .. CAPS { push(caps, "") }
+ plural = Dict.new()
+ forms = new []string
+ plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
var np = 0
var nlit = 0
for i in 0 .. len(es) {
@@ -48,33 +48,33 @@ export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void {
nlit += h + 1
}
}
- i18n_st.pat_out = new []string
- i18n_st.lits = new []string
- i18n_st.pat_l0 = words(np + 1)
- i18n_st.pat_nl = words(np + 1)
- i18n_st.pat_sc = words(np + 1)
- i18n_st.pat_ph = words(nlit + 1)
- i18n_st.nx_p = words(np + 1)
- i18n_st.nx_s = words(np + 1)
- i18n_st.nx_g = words(np + 1)
- i18n_st.pfx = Dict.new()
- i18n_st.sfx = Dict.new()
- i18n_st.gen_head = 0
- i18n_st.pat_n = 0
+ pat_out = new []string
+ lits = new []string
+ pat_l0 = words(np + 1)
+ pat_nl = words(np + 1)
+ pat_sc = words(np + 1)
+ pat_ph = words(nlit + 1)
+ nx_p = words(np + 1)
+ nx_s = words(np + 1)
+ nx_g = words(np + 1)
+ pfx = Dict.new()
+ sfx = Dict.new()
+ gen_head = 0
+ pat_n = 0
for i in 0 .. len(es) {
let e = es[i]
- if len(e.plural) > 0 { plural_add(i18n_st, e) } else if not (blank(e.id) or blank(e.str)) {
- if holes(e.id) > 0 { add_pattern(i18n_st, e.id, e.str) } else {
- Dict.set(i18n_st.exact, e.id, len(i18n_st.out))
- push(i18n_st.out, e.str)
+ if len(e.plural) > 0 { plural_add(e) } else if not (blank(e.id) or blank(e.str)) {
+ if holes(e.id) > 0 { add_pattern(e.id, e.str) } else {
+ Dict.set(exact, e.id, len(out))
+ push(out, e.str)
}
}
}
- i18n_st.on = true
+ on = true
}
# English again: nothing loaded, nothing translated
-export function i18n_off(i18n_st: mut I18nState) -> void {
- i18n_st.on = false
- i18n_st.cur = "en"
+export function i18n_off() -> void {
+ on = false
+ cur = "en"
}
diff --git a/packages/ludic.inventory/state.ludic b/packages/ludic.inventory/state.ludic
index e1d12a9f..e6315562 100644
--- a/packages/ludic.inventory/state.ludic
+++ b/packages/ludic.inventory/state.ludic
@@ -12,33 +12,31 @@ export property ItemChanged {
after: int = 0
}
-export state InventoryState {
- iv_keys: []string = null
- iv_counts: []int = null
- iv_changes: Queue = null
-}
+var iv_keys: []string = null
+var iv_counts: []int = null
+var iv_changes: Queue = null
# the kinds, by the name each is saved under; the pack starts empty
-export function inv_config(inventory_st: mut InventoryState, keys: []string) -> void {
- inventory_st.iv_keys = keys
- inv_clear(inventory_st)
+export function inv_config(keys: []string) -> void {
+ iv_keys = keys
+ inv_clear()
}
-export function inv_changes(base_st: mut BaseState, inventory_st: mut InventoryState) -> Queue {
- if inventory_st.iv_changes == null { inventory_st.iv_changes = queue_new(base_st, "inventory.changes") }
- return inventory_st.iv_changes
+export function inv_changes() -> Queue {
+ if iv_changes == null { iv_changes = queue_new("inventory.changes") }
+ return iv_changes
}
-function iv_valid(inventory_st: InventoryState, it: int) -> bool { return inventory_st.iv_counts != null and it >= 0 and it < len(inventory_st.iv_counts) }
+function iv_valid(it: int) -> bool { return iv_counts != null and it >= 0 and it < len(iv_counts) }
-export function inv_kinds(inventory_st: InventoryState) -> int {
- if inventory_st.iv_keys == null { return 0 }
- return len(inventory_st.iv_keys)
+export function inv_kinds() -> int {
+ if iv_keys == null { return 0 }
+ return len(iv_keys)
}
-export function inv_count(inventory_st: InventoryState, it: int) -> int {
- if not iv_valid(inventory_st, it) { return 0 }
- return inventory_st.iv_counts[it]
+export function inv_count(it: int) -> int {
+ if not iv_valid(it) { return 0 }
+ return iv_counts[it]
}
export function inv_limit(it: int) -> int {
@@ -46,16 +44,16 @@ export function inv_limit(it: int) -> int {
}
# how many more fit
-export function inv_room(inventory_st: InventoryState, it: int) -> int {
- if not iv_valid(inventory_st, it) { return 0 }
- return Math.max(inv_limit(it) - inventory_st.iv_counts[it], 0)
+export function inv_room(it: int) -> int {
+ if not iv_valid(it) { return 0 }
+ return Math.max(inv_limit(it) - iv_counts[it], 0)
}
-export function inv_has(inventory_st: InventoryState, it: int, n: int) -> bool { return inv_count(inventory_st, it) >= n }
+export function inv_has(it: int, n: int) -> bool { return inv_count(it) >= n }
-export function inv_total(inventory_st: InventoryState) -> int {
+export function inv_total() -> int {
var t = 0
- if inventory_st.iv_counts == null { return 0 }
- for i in 0 .. len(inventory_st.iv_counts) { t += inventory_st.iv_counts[i] }
+ if iv_counts == null { return 0 }
+ for i in 0 .. len(iv_counts) { t += iv_counts[i] }
return t
}
diff --git a/packages/ludic.inventory/system.ludic b/packages/ludic.inventory/system.ludic
index e68108df..b1d932b0 100644
--- a/packages/ludic.inventory/system.ludic
+++ b/packages/ludic.inventory/system.ludic
@@ -1,14 +1,14 @@
# system.ludic - the pack as a system: reset and its own save section, each count under its
# kind's name, so kinds can be added or reordered without a migration
-export function inv_reset(base_st: mut BaseState, inventory_st: mut InventoryState) -> void {
- inv_clear(inventory_st)
- q_clear(base_st, inv_changes(base_st, inventory_st))
+export function inv_reset() -> void {
+ inv_clear()
+ q_clear(inv_changes())
}
-export function inv_save(inventory_st: InventoryState) -> Val {
+export function inv_save() -> Val {
let counts = Value.object()
- for i in 0 .. inv_kinds(inventory_st) {
- if inventory_st.iv_counts[i] != 0 { sv_put_int(counts, inventory_st.iv_keys[i], inventory_st.iv_counts[i]) }
+ for i in 0 .. inv_kinds() {
+ if iv_counts[i] != 0 { sv_put_int(counts, iv_keys[i], iv_counts[i]) }
}
let v = Value.object()
Value.put(v, "counts", counts)
@@ -16,11 +16,11 @@ export function inv_save(inventory_st: InventoryState) -> Val {
}
# a name this build does not know is dropped
-export function inv_load(inventory_st: mut InventoryState, v: Val, version: int) -> void {
- inv_clear(inventory_st)
+export function inv_load(v: Val, version: int) -> void {
+ inv_clear()
if Value.has(v, "counts") == 0 { return }
let counts = Value.get(v, "counts")
- for i in 0 .. inv_kinds(inventory_st) { inventory_st.iv_counts[i] = Math.max(sv_int(counts, inventory_st.iv_keys[i], 0), 0) }
+ for i in 0 .. inv_kinds() { iv_counts[i] = Math.max(sv_int(counts, iv_keys[i], 0), 0) }
}
export function inv_system() -> System {
diff --git a/packages/ludic.inventory/tests/inventory_test.ludic b/packages/ludic.inventory/tests/inventory_test.ludic
index 408e3952..6511cc36 100644
--- a/packages/ludic.inventory/tests/inventory_test.ludic
+++ b/packages/ludic.inventory/tests/inventory_test.ludic
@@ -23,74 +23,74 @@ program InventoryTest {
bind PackRules { stack_limit: fn limit }
- function fresh(base_st: mut BaseState, inventory_st: mut InventoryState) -> void {
- inv_config(inventory_st, names("wood", "rod", "fish"))
- inv_reset(base_st, inventory_st)
+ function fresh() -> void {
+ inv_config(names("wood", "rod", "fish"))
+ inv_reset()
}
- function changes(base_st: mut BaseState, inventory_st: mut InventoryState) -> []ItemChanged { return q_drain(base_st, inv_changes(base_st, inventory_st)) }
- function waiting(base_st: mut BaseState, inventory_st: mut InventoryState) -> int { return q_len(inv_changes(base_st, inventory_st)) }
+ function changes() -> []ItemChanged { return q_drain(inv_changes()) }
+ function waiting() -> int { return q_len(inv_changes()) }
- test "add puts in what fits and says how many" (base_st: mut BaseState, inventory_st: mut InventoryState) {
- fresh(base_st, inventory_st)
- expect_eq(inv_add(base_st, inventory_st, WOOD, 7), 7)
- expect_eq(inv_add(base_st, inventory_st, WOOD, 7), 3)
- expect_eq(inv_count(inventory_st, WOOD), 10)
- expect_eq(inv_room(inventory_st, WOOD), 0)
- expect_eq(inv_add(base_st, inventory_st, ROD, 2), 1)
- expect_eq(inv_total(inventory_st), 11)
+ test "add puts in what fits and says how many" {
+ fresh()
+ expect_eq(inv_add(WOOD, 7), 7)
+ expect_eq(inv_add(WOOD, 7), 3)
+ expect_eq(inv_count(WOOD), 10)
+ expect_eq(inv_room(WOOD), 0)
+ expect_eq(inv_add(ROD, 2), 1)
+ expect_eq(inv_total(), 11)
}
- test "take is all or none" (base_st: mut BaseState, inventory_st: mut InventoryState) {
- fresh(base_st, inventory_st)
- inv_add(base_st, inventory_st, FISH, 3)
- expect(not inv_take(base_st, inventory_st, FISH, 4))
- expect_eq(inv_count(inventory_st, FISH), 3)
- expect(inv_take(base_st, inventory_st, FISH, 3))
- expect(not inv_has(inventory_st, FISH, 1))
+ test "take is all or none" {
+ fresh()
+ inv_add(FISH, 3)
+ expect(not inv_take(FISH, 4))
+ expect_eq(inv_count(FISH), 3)
+ expect(inv_take(FISH, 3))
+ expect(not inv_has(FISH, 1))
}
- test "set goes past the room, never below zero" (base_st: mut BaseState, inventory_st: mut InventoryState) {
- fresh(base_st, inventory_st)
- inv_set(base_st, inventory_st, ROD, 3)
- expect_eq(inv_count(inventory_st, ROD), 3)
- expect_eq(inv_room(inventory_st, ROD), 0)
- inv_set(base_st, inventory_st, ROD, -2)
- expect_eq(inv_count(inventory_st, ROD), 0)
+ test "set goes past the room, never below zero" {
+ fresh()
+ inv_set(ROD, 3)
+ expect_eq(inv_count(ROD), 3)
+ expect_eq(inv_room(ROD), 0)
+ inv_set(ROD, -2)
+ expect_eq(inv_count(ROD), 0)
}
- test "every change is a fact, and no change is not" (base_st: mut BaseState, inventory_st: mut InventoryState) {
- fresh(base_st, inventory_st)
- inv_add(base_st, inventory_st, WOOD, 2)
- inv_take(base_st, inventory_st, WOOD, 1)
- inv_set(base_st, inventory_st, WOOD, 1)
- let cs = changes(base_st, inventory_st)
+ test "every change is a fact, and no change is not" {
+ fresh()
+ inv_add(WOOD, 2)
+ inv_take(WOOD, 1)
+ inv_set(WOOD, 1)
+ let cs = changes()
expect_eq(len(cs), 2)
expect_eq(cs[0].after, 2)
expect_eq(cs[1].before, 2)
expect_eq(cs[1].after, 1)
- expect_eq(waiting(base_st, inventory_st), 0)
+ expect_eq(waiting(), 0)
}
- test "an item out of range is nothing" (base_st: mut BaseState, inventory_st: mut InventoryState) {
- fresh(base_st, inventory_st)
- expect_eq(inv_add(base_st, inventory_st, 9, 1), 0)
- expect_eq(inv_count(inventory_st, -1), 0)
- expect(not inv_take(base_st, inventory_st, 9, 0))
+ test "an item out of range is nothing" {
+ fresh()
+ expect_eq(inv_add(9, 1), 0)
+ expect_eq(inv_count(-1), 0)
+ expect(not inv_take(9, 0))
}
- test "the save is by name, so a reordered table still reads it" (base_st: mut BaseState, inventory_st: mut InventoryState) {
- fresh(base_st, inventory_st)
- inv_add(base_st, inventory_st, WOOD, 4)
- inv_add(base_st, inventory_st, FISH, 2)
+ test "the save is by name, so a reordered table still reads it" {
+ fresh()
+ inv_add(WOOD, 4)
+ inv_add(FISH, 2)
let root = save_tree()
- save_section(root, "inventory", 1, inv_save(inventory_st))
+ save_section(root, "inventory", 1, inv_save())
let text = save_encode(root)
- inv_config(inventory_st, names("fish", "bait", "wood"))
+ inv_config(names("fish", "bait", "wood"))
let n = load_section(save_decode(text), "inventory")
- inv_load(inventory_st, n.data, n.version)
- expect_eq(inv_count(inventory_st, 0), 2)
- expect_eq(inv_count(inventory_st, 1), 0)
- expect_eq(inv_count(inventory_st, 2), 4)
+ inv_load(n.data, n.version)
+ expect_eq(inv_count(0), 2)
+ expect_eq(inv_count(1), 0)
+ expect_eq(inv_count(2), 4)
}
}
diff --git a/packages/ludic.inventory/verbs.ludic b/packages/ludic.inventory/verbs.ludic
index 0fabbd6f..017a2eac 100644
--- a/packages/ludic.inventory/verbs.ludic
+++ b/packages/ludic.inventory/verbs.ludic
@@ -1,38 +1,38 @@
# verbs.ludic - the only way a count changes, and each change is said once
-function iv_change(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, after: int) -> void {
- let before = inventory_st.iv_counts[it]
+function iv_change(it: int, after: int) -> void {
+ let before = iv_counts[it]
if before == after { return }
- inventory_st.iv_counts[it] = after
+ iv_counts[it] = after
let c = new ItemChanged
c.item = it
c.before = before
c.after = after
- q_push(base_st, inv_changes(base_st, inventory_st), c)
+ q_push(inv_changes(), c)
}
# as many of n as there is room for; returns how many went in
-export function inv_add(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> int {
- if not iv_valid(inventory_st, it) or n <= 0 { return 0 }
- let k = Math.min(n, inv_room(inventory_st, it))
- if k > 0 { iv_change(base_st, inventory_st, it, inventory_st.iv_counts[it] + k) }
+export function inv_add(it: int, n: int) -> int {
+ if not iv_valid(it) or n <= 0 { return 0 }
+ let k = Math.min(n, inv_room(it))
+ if k > 0 { iv_change(it, iv_counts[it] + k) }
return k
}
# all n or none
-export function inv_take(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> bool {
- if not iv_valid(inventory_st, it) or inventory_st.iv_counts[it] < n { return false }
- if n > 0 { iv_change(base_st, inventory_st, it, inventory_st.iv_counts[it] - n) }
+export function inv_take(it: int, n: int) -> bool {
+ if not iv_valid(it) or iv_counts[it] < n { return false }
+ if n > 0 { iv_change(it, iv_counts[it] - n) }
return true
}
# a count outright, room or not (a starting kit, a correction, the network's word); never below 0
-export function inv_set(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> void {
- if not iv_valid(inventory_st, it) { return }
- iv_change(base_st, inventory_st, it, Math.max(n, 0))
+export function inv_set(it: int, n: int) -> void {
+ if not iv_valid(it) { return }
+ iv_change(it, Math.max(n, 0))
}
# everything gone, silently: a new trip, or before a load
-export function inv_clear(inventory_st: mut InventoryState) -> void {
- inventory_st.iv_counts = new []int
- for i in 0 .. inv_kinds(inventory_st) { push(inventory_st.iv_counts, 0) }
+export function inv_clear() -> void {
+ iv_counts = new []int
+ for i in 0 .. inv_kinds() { push(iv_counts, 0) }
}
diff --git a/packages/ludic.jobs/boards.ludic b/packages/ludic.jobs/boards.ludic
index 3d02acab..310f0400 100644
--- a/packages/ludic.jobs/boards.ludic
+++ b/packages/ludic.jobs/boards.ludic
@@ -2,88 +2,88 @@
# 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)
- return jobs_st.jobs__off[b] + s
+function jobs__at(b: int, s: int) -> int {
+ jobs__ensure()
+ return 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)
- return rng_between(jobs_st.jobs__dice, lo, hi)
+export function jobs_roll(lo: int, hi: int) -> int {
+ jobs__ensure()
+ return rng_between(jobs__dice, lo, hi)
}
# inside JobsWorld.roll: what goes up in the slot being filled
-export function jobs_post(jobs_st: mut JobsState, b: int, s: int, kind: int, param: int, need: int, money: int, rep: int) -> void {
- let k = jobs__at(jobs_st, b, s)
- jobs_st.jobs__pk[k] = kind
- jobs_st.jobs__pp[k] = param
- jobs_st.jobs__pn[k] = need
- jobs_st.jobs__pm[k] = money
- jobs_st.jobs__pr[k] = rep
- jobs_st.jobs__ph[k] = 0
- jobs_st.jobs__ps[k] = JOBS_ACTIVE
- jobs_st.jobs__pd[k] = JobsWorld.day()
+export function jobs_post(b: int, s: int, kind: int, param: int, need: int, money: int, rep: int) -> void {
+ let k = jobs__at(b, s)
+ jobs__pk[k] = kind
+ jobs__pp[k] = param
+ jobs__pn[k] = need
+ jobs__pm[k] = money
+ jobs__pr[k] = rep
+ jobs__ph[k] = 0
+ jobs__ps[k] = JOBS_ACTIVE
+ jobs__pd[k] = JobsWorld.day()
let e = JobsWorld.evidence(kind, param)
- jobs_st.jobs__pb[k] = 0
- if e > 0 { jobs_st.jobs__pb[k] = e }
+ jobs__pb[k] = 0
+ if e > 0 { jobs__pb[k] = e }
}
-function jobs__clear(jobs_st: mut JobsState, k: int) -> void {
- jobs_st.jobs__pk[k] = -1
- jobs_st.jobs__pp[k] = -1
- jobs_st.jobs__pn[k] = 0
- jobs_st.jobs__ph[k] = 0
- jobs_st.jobs__ps[k] = JOBS_OFFERED
+function jobs__clear(k: int) -> void {
+ jobs__pk[k] = -1
+ jobs__pp[k] = -1
+ jobs__pn[k] = 0
+ jobs__ph[k] = 0
+ jobs__ps[k] = JOBS_OFFERED
}
# take a slot down and ask the game for what replaces it
-function jobs__repost(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int) -> void {
- let k = jobs__at(jobs_st, b, s)
+function jobs__repost(b: int, s: int) -> void {
+ let k = jobs__at(b, s)
let day = JobsWorld.day()
- rng_seed(jobs_st.jobs__dice, jobs_st.jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs_st.jobs__serial[b] * 31)
- jobs_st.jobs__serial[b] += 1
- jobs__clear(jobs_st, k)
+ rng_seed(jobs__dice, jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs__serial[b] * 31)
+ jobs__serial[b] += 1
+ jobs__clear(k)
JobsWorld.roll(b, s)
- if jobs_st.jobs__pk[k] >= 0 { jobs__fact(base_st, jobs_st, JOBS_F_OFFERED, -1, b, s, 0, jobs_st.jobs__pn[k]) }
+ if jobs__pk[k] >= 0 { jobs__fact(JOBS_F_OFFERED, -1, b, s, 0, jobs__pn[k]) }
}
# 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(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
- jobs__ensure(jobs_st)
+export function jobs_morning() -> void {
+ jobs__ensure()
let day = JobsWorld.day()
for b in 0 .. BOARD_COUNT {
let bd = JobBoards[b]
if not JobsWorld.owns(bd.scope) { continue }
for s in 0 .. bd.slots {
- let k = jobs_st.jobs__off[b] + s
- let finished = jobs_st.jobs__ps[k] == JOBS_DONE and day > jobs_st.jobs__pd[k]
- if jobs_st.jobs__pk[k] < 0 or finished or day - jobs_st.jobs__pd[k] >= bd.keep { jobs__repost(base_st, jobs_st, b, s) }
+ let k = jobs__off[b] + s
+ let finished = jobs__ps[k] == JOBS_DONE and day > jobs__pd[k]
+ if jobs__pk[k] < 0 or finished or day - jobs__pd[k] >= bd.keep { jobs__repost(b, s) }
}
}
}
-function jobs__post_moved(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int, have: int) -> void {
- let k = jobs_st.jobs__off[b] + s
- let was = jobs_st.jobs__ph[k]
- jobs_st.jobs__ph[k] = have
- if jobs_st.jobs__ph[k] > jobs_st.jobs__pn[k] { jobs_st.jobs__ph[k] = jobs_st.jobs__pn[k] }
- if jobs_st.jobs__ph[k] > was { jobs__fact(base_st, jobs_st, JOBS_F_PROGRESSED, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k]) }
- if jobs_st.jobs__ps[k] != JOBS_ACTIVE or jobs_st.jobs__ph[k] < jobs_st.jobs__pn[k] { return }
- jobs_st.jobs__ps[k] = JOBS_READY
- jobs__fact(base_st, jobs_st, JOBS_F_COMPLETED, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k])
- if JobBoards[b].auto { jobs_post_hand_in(base_st, jobs_st, b, s) }
+function jobs__post_moved(b: int, s: int, have: int) -> void {
+ let k = jobs__off[b] + s
+ let was = jobs__ph[k]
+ jobs__ph[k] = have
+ if jobs__ph[k] > jobs__pn[k] { jobs__ph[k] = jobs__pn[k] }
+ if jobs__ph[k] > was { jobs__fact(JOBS_F_PROGRESSED, -1, b, s, jobs__ph[k], jobs__pn[k]) }
+ if jobs__ps[k] != JOBS_ACTIVE or jobs__ph[k] < jobs__pn[k] { return }
+ jobs__ps[k] = JOBS_READY
+ jobs__fact(JOBS_F_COMPLETED, -1, b, s, jobs__ph[k], jobs__pn[k])
+ if JobBoards[b].auto { jobs_post_hand_in(b, s) }
}
-export function jobs_post_hand_in(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int) -> bool {
+export function jobs_post_hand_in(b: int, s: int) -> bool {
if not jobs_board_ok(b) or s < 0 or s >= JobBoards[b].slots { return false }
- let k = jobs__at(jobs_st, b, s)
- if jobs_st.jobs__ps[k] != JOBS_READY { return false }
- jobs_st.jobs__ps[k] = JOBS_DONE
- jobs_st.jobs__total[b] += 1
- JobsPay.reward(JobBoards[b].scope, jobs_st.jobs__pm[k], jobs_st.jobs__pr[k])
- jobs__fact(base_st, jobs_st, JOBS_F_HANDED_IN, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k])
- if JobBoards[b].refill { jobs__repost(base_st, jobs_st, b, s) }
+ let k = jobs__at(b, s)
+ if jobs__ps[k] != JOBS_READY { return false }
+ jobs__ps[k] = JOBS_DONE
+ jobs__total[b] += 1
+ JobsPay.reward(JobBoards[b].scope, jobs__pm[k], jobs__pr[k])
+ jobs__fact(JOBS_F_HANDED_IN, -1, b, s, jobs__ph[k], jobs__pn[k])
+ if JobBoards[b].refill { jobs__repost(b, s) }
return true
}
diff --git a/packages/ludic.jobs/count.ludic b/packages/ludic.jobs/count.ludic
index 5463eaf4..743c3486 100644
--- a/packages/ludic.jobs/count.ludic
+++ b/packages/ludic.jobs/count.ludic
@@ -6,69 +6,69 @@ function jobs__matches(want_kind: int, want_param: int, kind: int, param: int) -
}
# `n` of (kind, param) happened here: every job and post of a scope this machine keeps
-export function jobs_count(base_st: mut BaseState, jobs_st: mut JobsState, kind: int, param: int, n: int) -> void { jobs__count(base_st, jobs_st, -1, kind, param, n) }
+export function jobs_count(kind: int, param: int, n: int) -> void { jobs__count(-1, kind, param, n) }
# the same for one scope alone, kept or not: another member's deed arriving at the host
-export function jobs_count_in(base_st: mut BaseState, jobs_st: mut JobsState, scope: int, kind: int, param: int, n: int) -> void { jobs__count(base_st, jobs_st, scope, kind, param, n) }
+export function jobs_count_in(scope: int, kind: int, param: int, n: int) -> void { jobs__count(scope, kind, param, n) }
function jobs__takes(only: int, scope: int) -> bool {
if only >= 0 { return scope == only }
return JobsWorld.owns(scope)
}
-function jobs__count(base_st: mut BaseState, jobs_st: mut JobsState, only: int, kind: int, param: int, n: int) -> void {
- jobs__ensure(jobs_st)
+function jobs__count(only: int, kind: int, param: int, n: int) -> void {
+ jobs__ensure()
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 }
+ if jobs__st[i] != JOBS_ACTIVE or not jobs__takes(only, d.scope) { continue }
if not jobs__matches(d.kind, d.param, kind, param) { continue }
- let was = jobs_st.jobs__hv[i]
- jobs_st.jobs__hv[i] = was + n
- jobs__moved(base_st, jobs_st, i, was)
+ let was = jobs__hv[i]
+ jobs__hv[i] = was + n
+ jobs__moved(i, was)
}
for b in 0 .. BOARD_COUNT {
if not jobs__takes(only, JobBoards[b].scope) { continue }
for s in 0 .. JobBoards[b].slots {
- let k = jobs_st.jobs__off[b] + s
- if jobs_st.jobs__ps[k] != JOBS_ACTIVE or not jobs__matches(jobs_st.jobs__pk[k], jobs_st.jobs__pp[k], kind, param) { continue }
- jobs__post_moved(base_st, jobs_st, b, s, jobs_st.jobs__ph[k] + n)
+ let k = jobs__off[b] + s
+ if jobs__ps[k] != JOBS_ACTIVE or not jobs__matches(jobs__pk[k], jobs__pp[k], kind, param) { continue }
+ jobs__post_moved(b, s, jobs__ph[k] + n)
}
}
}
-function jobs__recheck_one(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> void {
+function jobs__recheck_one(i: int) -> void {
let d = Jobs[i]
- if jobs_st.jobs__st[i] != JOBS_ACTIVE or d.kind < 0 or not JobsWorld.owns(d.scope) { return }
+ if jobs__st[i] != JOBS_ACTIVE or d.kind < 0 or not JobsWorld.owns(d.scope) { return }
let e = JobsWorld.evidence(d.kind, d.param)
- if e <= jobs_st.jobs__hv[i] { return }
- let was = jobs_st.jobs__hv[i]
- jobs_st.jobs__hv[i] = e
- jobs__moved(base_st, jobs_st, i, was)
+ if e <= jobs__hv[i] { return }
+ let was = jobs__hv[i]
+ jobs__hv[i] = e
+ jobs__moved(i, was)
}
# 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(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
- jobs__ensure(jobs_st)
- for i in 0 .. JOB_COUNT { jobs__recheck_one(base_st, jobs_st, i) }
+export function jobs_recheck() -> void {
+ jobs__ensure()
+ for i in 0 .. JOB_COUNT { jobs__recheck_one(i) }
for b in 0 .. BOARD_COUNT {
if not JobsWorld.owns(JobBoards[b].scope) { continue }
for s in 0 .. JobBoards[b].slots {
- let k = jobs_st.jobs__off[b] + s
- if jobs_st.jobs__ps[k] != JOBS_ACTIVE or jobs_st.jobs__pk[k] < 0 { continue }
- let e = JobsWorld.evidence(jobs_st.jobs__pk[k], jobs_st.jobs__pp[k])
- if e >= 0 and e - jobs_st.jobs__pb[k] > jobs_st.jobs__ph[k] { jobs__post_moved(base_st, jobs_st, b, s, e - jobs_st.jobs__pb[k]) }
+ let k = jobs__off[b] + s
+ if jobs__ps[k] != JOBS_ACTIVE or jobs__pk[k] < 0 { continue }
+ let e = JobsWorld.evidence(jobs__pk[k], jobs__pp[k])
+ if e >= 0 and e - jobs__pb[k] > jobs__ph[k] { jobs__post_moved(b, s, e - jobs__pb[k]) }
}
}
}
# offers newly open are said once; the first look after a reset says none of them
-function jobs__offers(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
- jobs__ensure(jobs_st)
+function jobs__offers() -> void {
+ jobs__ensure()
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(base_st, jobs_st, JOBS_F_OFFERED, i, -1, -1, 0, Jobs[i].need) }
- jobs_st.jobs__seen[i] = now
+ let now = jobs_offered(i)
+ if now and not jobs__seen[i] and jobs__primed { jobs__fact(JOBS_F_OFFERED, i, -1, -1, 0, Jobs[i].need) }
+ jobs__seen[i] = now
}
- jobs_st.jobs__primed = true
+ jobs__primed = true
}
diff --git a/packages/ludic.jobs/jobs.ludic b/packages/ludic.jobs/jobs.ludic
index f54ae3ff..b0e389ba 100644
--- a/packages/ludic.jobs/jobs.ludic
+++ b/packages/ludic.jobs/jobs.ludic
@@ -1,96 +1,96 @@
# 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)
- return jobs_st.jobs__st[i]
+export function jobs_state(i: int) -> int {
+ jobs__ensure()
+ return jobs__st[i]
}
-export function jobs_have(jobs_st: mut JobsState, i: int) -> int {
- jobs__ensure(jobs_st)
- return jobs_st.jobs__hv[i]
+export function jobs_have(i: int) -> int {
+ jobs__ensure()
+ return 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(i: int) -> bool { return jobs_state(i) == JOBS_READY }
# taken and not yet paid
-export function jobs_under_way(jobs_st: mut JobsState, i: int) -> bool {
- let s = jobs_state(jobs_st, i)
+export function jobs_under_way(i: int) -> bool {
+ let s = jobs_state(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(i: int) -> bool {
let d = Jobs[i]
- if jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
+ if jobs_state(i) != JOBS_OFFERED { return false }
return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i)
}
-export function jobs_take(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
- if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
- jobs_st.jobs__st[i] = JOBS_ACTIVE
- jobs_st.jobs__hv[i] = 0
- jobs__recheck_one(base_st, jobs_st, i)
+export function jobs_take(i: int) -> bool {
+ if not jobs_ok(i) or jobs_state(i) != JOBS_OFFERED { return false }
+ jobs__st[i] = JOBS_ACTIVE
+ jobs__hv[i] = 0
+ jobs__recheck_one(i)
return true
}
# a count has moved: a job with a need is ready when it is met
-function jobs__moved(base_st: mut BaseState, jobs_st: mut JobsState, i: int, was: int) -> void {
+function jobs__moved(i: int, was: int) -> void {
let d = Jobs[i]
- if d.need > 0 and jobs_st.jobs__hv[i] > d.need { jobs_st.jobs__hv[i] = d.need }
- if jobs_st.jobs__hv[i] > was { jobs__fact(base_st, jobs_st, JOBS_F_PROGRESSED, i, -1, -1, jobs_st.jobs__hv[i], d.need) }
- if jobs_st.jobs__st[i] == JOBS_ACTIVE and d.need > 0 and jobs_st.jobs__hv[i] >= d.need { jobs_ready(base_st, jobs_st, i) }
+ if d.need > 0 and jobs__hv[i] > d.need { jobs__hv[i] = d.need }
+ if jobs__hv[i] > was { jobs__fact(JOBS_F_PROGRESSED, i, -1, -1, jobs__hv[i], d.need) }
+ if jobs__st[i] == JOBS_ACTIVE and d.need > 0 and jobs__hv[i] >= d.need { jobs_ready(i) }
}
# the game's own play: a step counter, a mask of targets met
-export function jobs_set_have(base_st: mut BaseState, jobs_st: mut JobsState, i: int, v: int) -> void {
- let was = jobs_have(jobs_st, i)
- jobs_st.jobs__hv[i] = v
- jobs__moved(base_st, jobs_st, i, was)
+export function jobs_set_have(i: int, v: int) -> void {
+ let was = jobs_have(i)
+ jobs__hv[i] = v
+ jobs__moved(i, was)
}
-export function jobs_add_have(base_st: mut BaseState, jobs_st: mut JobsState, i: int, n: int) -> void { jobs_set_have(base_st, jobs_st, i, jobs_have(jobs_st, i) + n) }
+export function jobs_add_have(i: int, n: int) -> void { jobs_set_have(i, jobs_have(i) + n) }
-export function jobs_mark(base_st: mut BaseState, jobs_st: mut JobsState, i: int, bit: int) -> void { jobs_set_have(base_st, jobs_st, i, jobs_have(jobs_st, i) | (1 << bit)) }
+export function jobs_mark(i: int, bit: int) -> void { jobs_set_have(i, jobs_have(i) | (1 << bit)) }
# everything in hand
-export function jobs_ready(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> void {
- if jobs_state(jobs_st, i) != JOBS_ACTIVE { return }
- jobs_st.jobs__st[i] = JOBS_READY
- jobs__fact(base_st, jobs_st, JOBS_F_COMPLETED, i, -1, -1, jobs_st.jobs__hv[i], Jobs[i].need)
+export function jobs_ready(i: int) -> void {
+ if jobs_state(i) != JOBS_ACTIVE { return }
+ jobs__st[i] = JOBS_READY
+ jobs__fact(JOBS_F_COMPLETED, i, -1, -1, jobs__hv[i], Jobs[i].need)
}
-export function jobs_hand_in(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
- if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_READY { return false }
+export function jobs_hand_in(i: int) -> bool {
+ if not jobs_ok(i) or jobs_state(i) != JOBS_READY { return false }
let d = Jobs[i]
- jobs_st.jobs__st[i] = JOBS_DONE
- if d.group >= 0 and d.group < JOBS_GROUPS { jobs_st.jobs__done_n[d.group] += 1 }
+ jobs__st[i] = JOBS_DONE
+ if d.group >= 0 and d.group < JOBS_GROUPS { jobs__done_n[d.group] += 1 }
JobsPay.reward(d.scope, d.money, d.rep)
- jobs__fact(base_st, jobs_st, JOBS_F_HANDED_IN, i, -1, -1, jobs_st.jobs__hv[i], d.need)
+ jobs__fact(JOBS_F_HANDED_IN, i, -1, -1, jobs__hv[i], d.need)
return true
}
# finished and paid in one go, whatever the count says
-export function jobs_complete(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
- if not jobs_ok(i) or not jobs_under_way(jobs_st, i) { return false }
- jobs_ready(base_st, jobs_st, i)
- return jobs_hand_in(base_st, jobs_st, i)
+export function jobs_complete(i: int) -> bool {
+ if not jobs_ok(i) or not jobs_under_way(i) { return false }
+ jobs_ready(i)
+ return jobs_hand_in(i)
}
# 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_set(i: int, state: int, have: int) -> void {
+ jobs__ensure()
+ jobs__st[i] = state
+ jobs__hv[i] = have
}
-export function jobs_done(jobs_st: mut JobsState, group: int) -> int {
- jobs__ensure(jobs_st)
+export function jobs_done(group: int) -> int {
+ jobs__ensure()
if group < 0 or group >= JOBS_GROUPS { return 0 }
- return jobs_st.jobs__done_n[group]
+ return 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 }
+export function jobs_set_done(group: int, n: int) -> void {
+ jobs__ensure()
+ if group >= 0 and group < JOBS_GROUPS { jobs__done_n[group] = n }
}
diff --git a/packages/ludic.jobs/lists.ludic b/packages/ludic.jobs/lists.ludic
index 6e827343..cf24faf5 100644
--- a/packages/ludic.jobs/lists.ludic
+++ b/packages/ludic.jobs/lists.ludic
@@ -1,36 +1,36 @@
# 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(group: int, vendor: int) -> []int {
let out = new []int
for i in 0 .. JOB_COUNT {
let d = Jobs[i]
if d.group != group or (vendor >= 0 and d.vendor != vendor) { continue }
- let s = jobs_state(jobs_st, i)
- if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(jobs_st, i)) { continue }
+ let s = jobs_state(i)
+ if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(i)) { continue }
push(out, i)
}
return out
}
# 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(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) } }
+ for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(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(group: int, most: int) -> []int {
let out = new []int
for i in 0 .. JOB_COUNT {
if len(out) >= most { break }
- if Jobs[i].group == group and jobs_under_way(jobs_st, i) { push(out, i) }
+ if Jobs[i].group == group and jobs_under_way(i) { push(out, i) }
}
return out
}
# the first under way and not yet met, or -1
-export function jobs_first_open(jobs_st: mut 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 } }
+export function jobs_first_open(group: int) -> int {
+ for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(i) == JOBS_ACTIVE { return i } }
return -1
}
diff --git a/packages/ludic.jobs/posts.ludic b/packages/ludic.jobs/posts.ludic
index e264ec2a..f7b44541 100644
--- a/packages/ludic.jobs/posts.ludic
+++ b/packages/ludic.jobs/posts.ludic
@@ -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(b: int, s: int) -> int { return jobs__pk[jobs__at(b, s)] }
+export function jobs_post_param(b: int, s: int) -> int { return jobs__pp[jobs__at(b, s)] }
+export function jobs_post_need(b: int, s: int) -> int { return jobs__pn[jobs__at(b, s)] }
+export function jobs_post_have(b: int, s: int) -> int { return jobs__ph[jobs__at(b, s)] }
+export function jobs_post_money(b: int, s: int) -> int { return jobs__pm[jobs__at(b, s)] }
+export function jobs_post_rep(b: int, s: int) -> int { return jobs__pr[jobs__at(b, s)] }
+export function jobs_post_state(b: int, s: int) -> int { return jobs__ps[jobs__at(b, s)] }
+export function jobs_post_day(b: int, s: int) -> int { return jobs__pd[jobs__at(b, s)] }
export function jobs_slots(b: int) -> int {
if not jobs_board_ok(b) { return 0 }
@@ -14,38 +14,38 @@ 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(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) } }
+ for s in 0 .. jobs_slots(b) { if jobs_post_kind(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(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 } }
+ for s in 0 .. jobs_slots(b) { if jobs_post_kind(b, s) >= 0 and jobs_post_state(b, s) == JOBS_DONE { n += 1 } }
return n
}
-export function jobs_total(jobs_st: mut JobsState, b: int) -> int {
- jobs__ensure(jobs_st)
- return jobs_st.jobs__total[b]
+export function jobs_total(b: int) -> int {
+ jobs__ensure()
+ return jobs__total[b]
}
# a slot outright: the host's board on a guest, a test
-export function jobs_post_set(jobs_st: mut JobsState, b: int, s: int, kind: int, param: int, need: int, have: int, money: int, rep: int, state: int, day: int) -> void {
- let k = jobs__at(jobs_st, b, s)
- jobs_st.jobs__pk[k] = kind
- jobs_st.jobs__pp[k] = param
- jobs_st.jobs__pn[k] = need
- jobs_st.jobs__ph[k] = have
- jobs_st.jobs__pm[k] = money
- jobs_st.jobs__pr[k] = rep
- jobs_st.jobs__ps[k] = state
- jobs_st.jobs__pd[k] = day
+export function jobs_post_set(b: int, s: int, kind: int, param: int, need: int, have: int, money: int, rep: int, state: int, day: int) -> void {
+ let k = jobs__at(b, s)
+ jobs__pk[k] = kind
+ jobs__pp[k] = param
+ jobs__pn[k] = need
+ jobs__ph[k] = have
+ jobs__pm[k] = money
+ jobs__pr[k] = rep
+ jobs__ps[k] = state
+ jobs__pd[k] = day
}
-export function jobs_set_total(jobs_st: mut JobsState, b: int, n: int) -> void {
- jobs__ensure(jobs_st)
- jobs_st.jobs__total[b] = n
+export function jobs_set_total(b: int, n: int) -> void {
+ jobs__ensure()
+ jobs__total[b] = n
}
diff --git a/packages/ludic.jobs/save.ludic b/packages/ludic.jobs/save.ludic
index 375823cc..aabe027b 100644
--- a/packages/ludic.jobs/save.ludic
+++ b/packages/ludic.jobs/save.ludic
@@ -1,39 +1,39 @@
# 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)
+function jobs__reset_scope(sc: int) -> void {
+ jobs__ensure()
for i in 0 .. JOB_COUNT {
if Jobs[i].scope != sc { continue }
- jobs_st.jobs__st[i] = JOBS_OFFERED
- jobs_st.jobs__hv[i] = 0
+ jobs__st[i] = JOBS_OFFERED
+ jobs__hv[i] = 0
}
for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc { continue }
- for s in 0 .. JobBoards[b].slots { jobs__clear(jobs_st, jobs_st.jobs__off[b] + s) }
- jobs_st.jobs__total[b] = 0
- jobs_st.jobs__serial[b] = 0
+ for s in 0 .. JobBoards[b].slots { jobs__clear(jobs__off[b] + s) }
+ jobs__total[b] = 0
+ jobs__serial[b] = 0
}
- jobs_st.jobs__primed = false
+ jobs__primed = false
}
-function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val {
+function jobs__save_scope(sc: int) -> Val {
let v = Value.object()
let js = Value.object()
for i in 0 .. JOB_COUNT {
- if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue }
- sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i]])
+ if Jobs[i].scope != sc or jobs__st[i] == JOBS_OFFERED { continue }
+ sv_put_ints(js, Jobs[i].key, [jobs__st[i], jobs__hv[i]])
}
Value.put(v, "jobs", js)
let bs = Value.object()
for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc { continue }
let o = Value.object()
- sv_put_int(o, "total", jobs_st.jobs__total[b])
- sv_put_int(o, "serial", jobs_st.jobs__serial[b])
+ sv_put_int(o, "total", jobs__total[b])
+ sv_put_int(o, "serial", jobs__serial[b])
let slots = new []int
for s in 0 .. JobBoards[b].slots {
- let k = jobs_st.jobs__off[b] + s
- let row = [jobs_st.jobs__pk[k], jobs_st.jobs__pp[k], jobs_st.jobs__pn[k], jobs_st.jobs__ph[k], jobs_st.jobs__pb[k], jobs_st.jobs__pm[k], jobs_st.jobs__pr[k], jobs_st.jobs__ps[k], jobs_st.jobs__pd[k]]
+ let k = jobs__off[b] + s
+ let row = [jobs__pk[k], jobs__pp[k], jobs__pn[k], jobs__ph[k], jobs__pb[k], jobs__pm[k], jobs__pr[k], jobs__ps[k], jobs__pd[k]]
for f in 0 .. len(row) { push(slots, row[f]) }
}
sv_put_ints(o, "slots", slots)
@@ -43,13 +43,13 @@ function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val {
return v
}
-function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void {
- jobs__reset_scope(jobs_st, sc)
+function jobs__load_scope(sc: int, v: Val) -> void {
+ jobs__reset_scope(sc)
if Value.has(v, "jobs") != 0 {
let js = Value.get(v, "jobs")
for i in 0 .. JOB_COUNT {
let p = sv_ints(js, Jobs[i].key)
- if Jobs[i].scope == sc and len(p) == 2 { jobs_set(jobs_st, i, p[0], p[1]) }
+ if Jobs[i].scope == sc and len(p) == 2 { jobs_set(i, p[0], p[1]) }
}
}
if Value.has(v, "boards") == 0 { return }
@@ -57,15 +57,15 @@ function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void {
for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc or Value.has(bs, JobBoards[b].key) == 0 { continue }
let o = Value.get(bs, JobBoards[b].key)
- jobs_st.jobs__total[b] = sv_int(o, "total", 0)
- jobs_st.jobs__serial[b] = sv_int(o, "serial", 0)
+ jobs__total[b] = sv_int(o, "total", 0)
+ jobs__serial[b] = sv_int(o, "serial", 0)
let r = sv_ints(o, "slots")
for s in 0 .. JobBoards[b].slots {
if s * 9 + 8 >= len(r) { break }
- let k = jobs_st.jobs__off[b] + s
+ let k = jobs__off[b] + s
let q = s * 9
- jobs_post_set(jobs_st, b, s, r[q], r[q + 1], r[q + 2], r[q + 3], r[q + 5], r[q + 6], r[q + 7], r[q + 8])
- jobs_st.jobs__pb[k] = r[q + 4]
+ jobs_post_set(b, s, r[q], r[q + 1], r[q + 2], r[q + 3], r[q + 5], r[q + 6], r[q + 7], r[q + 8])
+ jobs__pb[k] = r[q + 4]
}
}
}
diff --git a/packages/ludic.jobs/state.ludic b/packages/ludic.jobs/state.ludic
index 91072185..b5eae680 100644
--- a/packages/ludic.jobs/state.ludic
+++ b/packages/ludic.jobs/state.ludic
@@ -1,30 +1,28 @@
# 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.
-export state JobsState {
- jobs__st: []int = null
- jobs__hv: []int = null
- jobs__seen: []bool = null # 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__seed: int = 1000
- jobs__dice: Rng = null
- jobs__q: Queue = null
-}
+var jobs__st: []int = null
+var jobs__hv: []int = null
+var jobs__seen: []bool = null # an offer already said
+var jobs__primed: bool = false # the first tick after a reset learns the offers quietly
+var jobs__done_n: []int = null # handed in, per group
# the posts, every board's slots end to end from jobs__off[b]
+var jobs__off: []int = null
+var jobs__pk: []int = null # kind, -1 empty
+var jobs__pp: []int = null
+var jobs__pn: []int = null
+var jobs__ph: []int = null
+var jobs__pb: []int = null # the evidence when it went up
+var jobs__pm: []int = null
+var jobs__pr: []int = null
+var jobs__ps: []int = null
+var jobs__pd: []int = null # the day it went up
+var jobs__total: []int = null # handed in, per board
+var jobs__serial: []int = null # posts made, per board: each roll its own seed
+var jobs__seed: int = 1000
+var jobs__dice: Rng = null
+var jobs__q: Queue = null
function jobs__ints(n: int, v: int) -> []int {
let out = new []int
@@ -32,39 +30,39 @@ 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__ensure() -> void {
+ if jobs__st != null { return }
+ jobs__st = jobs__ints(JOB_COUNT, JOBS_OFFERED)
+ jobs__hv = jobs__ints(JOB_COUNT, 0)
+ jobs__seen = new []bool
+ for i in 0 .. JOB_COUNT { push(jobs__seen, false) }
+ jobs__done_n = jobs__ints(JOBS_GROUPS, 0)
+ jobs__off = new []int
var n = 0
for b in 0 .. BOARD_COUNT {
- push(jobs_st.jobs__off, n)
+ push(jobs__off, 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)
+ jobs__pk = jobs__ints(n, -1)
+ jobs__pp = jobs__ints(n, -1)
+ jobs__pn = jobs__ints(n, 0)
+ jobs__ph = jobs__ints(n, 0)
+ jobs__pb = jobs__ints(n, 0)
+ jobs__pm = jobs__ints(n, 0)
+ jobs__pr = jobs__ints(n, 0)
+ jobs__ps = jobs__ints(n, JOBS_OFFERED)
+ jobs__pd = jobs__ints(n, 0)
+ jobs__total = jobs__ints(BOARD_COUNT, 0)
+ jobs__serial = jobs__ints(BOARD_COUNT, 0)
+ jobs__dice = rng_new(jobs__seed)
}
-export function jobs_facts(base_st: mut BaseState, jobs_st: mut JobsState) -> Queue {
- if jobs_st.jobs__q == null { jobs_st.jobs__q = queue_new(base_st, "jobs.facts") }
- return jobs_st.jobs__q
+export function jobs_facts() -> Queue {
+ if jobs__q == null { jobs__q = queue_new("jobs.facts") }
+ return jobs__q
}
-function jobs__fact(base_st: mut BaseState, jobs_st: mut JobsState, what: int, job: int, b: int, s: int, have: int, need: int) -> void {
+function jobs__fact(what: int, job: int, b: int, s: int, have: int, need: int) -> void {
let f = new JobsFact
f.what = what
f.job = job
@@ -79,17 +77,17 @@ function jobs__fact(base_st: mut BaseState, jobs_st: mut JobsState, what: int, j
f.rep = Jobs[job].rep
f.scope = Jobs[job].scope
} else {
- f.kind = jobs_st.jobs__pk[jobs_st.jobs__off[b] + s]
- f.param = jobs_st.jobs__pp[jobs_st.jobs__off[b] + s]
- f.money = jobs_st.jobs__pm[jobs_st.jobs__off[b] + s]
- f.rep = jobs_st.jobs__pr[jobs_st.jobs__off[b] + s]
+ f.kind = jobs__pk[jobs__off[b] + s]
+ f.param = jobs__pp[jobs__off[b] + s]
+ f.money = jobs__pm[jobs__off[b] + s]
+ f.rep = jobs__pr[jobs__off[b] + s]
f.scope = JobBoards[b].scope
}
- q_push(base_st, jobs_facts(base_st, jobs_st), f)
+ q_push(jobs_facts(), f)
}
# the base of every roll: a post's seed is this, the day, the board, the slot and how many before
-export function jobs_config_seed(jobs_st: mut JobsState, seed: int) -> void {
- jobs_st.jobs__seed = seed
- jobs_st.jobs__dice = rng_new(seed)
+export function jobs_config_seed(seed: int) -> void {
+ jobs__seed = seed
+ jobs__dice = rng_new(seed)
}
diff --git a/packages/ludic.jobs/system.ludic b/packages/ludic.jobs/system.ludic
index 6e2bfb42..51701e01 100644
--- a/packages/ludic.jobs/system.ludic
+++ b/packages/ludic.jobs/system.ludic
@@ -1,39 +1,39 @@
# system.ludic - two systems, one per owner: "jobs" is this player's (personal jobs and boards, the
# done counts, the tick), "jobs.party" the party's, saved with the world
-export function jobs_reset(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
- jobs__reset_scope(jobs_st, JOBS_SELF)
- for g in 0 .. JOBS_GROUPS { jobs_st.jobs__done_n[g] = 0 }
- q_clear(base_st, jobs_facts(base_st, jobs_st))
+export function jobs_reset() -> void {
+ jobs__reset_scope(JOBS_SELF)
+ for g in 0 .. JOBS_GROUPS { jobs__done_n[g] = 0 }
+ q_clear(jobs_facts())
}
-export function jobs_party_reset(jobs_st: mut JobsState) -> void { jobs__reset_scope(jobs_st, JOBS_PARTY) }
+export function jobs_party_reset() -> void { jobs__reset_scope(JOBS_PARTY) }
-export function jobs_save(jobs_st: mut JobsState) -> Val {
- jobs__ensure(jobs_st)
- let v = jobs__save_scope(jobs_st, JOBS_SELF)
- sv_put_ints(v, "done", jobs_st.jobs__done_n)
+export function jobs_save() -> Val {
+ jobs__ensure()
+ let v = jobs__save_scope(JOBS_SELF)
+ sv_put_ints(v, "done", jobs__done_n)
return v
}
-export function jobs_load(jobs_st: mut JobsState, v: Val, version: int) -> void {
- jobs__load_scope(jobs_st, JOBS_SELF, v)
+export function jobs_load(v: Val, version: int) -> void {
+ jobs__load_scope(JOBS_SELF, v)
let d = sv_ints(v, "done")
for g in 0 .. JOBS_GROUPS {
- jobs_st.jobs__done_n[g] = 0
- if g < len(d) { jobs_st.jobs__done_n[g] = d[g] }
+ jobs__done_n[g] = 0
+ if g < len(d) { jobs__done_n[g] = d[g] }
}
}
-export function jobs_party_save(jobs_st: mut JobsState) -> Val {
- jobs__ensure(jobs_st)
- return jobs__save_scope(jobs_st, JOBS_PARTY)
+export function jobs_party_save() -> Val {
+ jobs__ensure()
+ return jobs__save_scope(JOBS_PARTY)
}
-export function jobs_party_load(jobs_st: mut JobsState, v: Val, version: int) -> void { jobs__load_scope(jobs_st, JOBS_PARTY, v) }
+export function jobs_party_load(v: Val, version: int) -> void { jobs__load_scope(JOBS_PARTY, v) }
-function jobs__tick(base_st: mut BaseState, jobs_st: mut JobsState, t: Tick) -> void {
- jobs__offers(base_st, jobs_st)
- jobs_recheck(base_st, jobs_st)
+function jobs__tick(t: Tick) -> void {
+ jobs__offers()
+ jobs_recheck()
}
export function jobs_system() -> System {
diff --git a/packages/ludic.jobs/tests/jobs_test.ludic b/packages/ludic.jobs/tests/jobs_test.ludic
index abab98d0..41fc110d 100644
--- a/packages/ludic.jobs/tests/jobs_test.ludic
+++ b/packages/ludic.jobs/tests/jobs_test.ludic
@@ -15,52 +15,50 @@ program JobsTest {
def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false }
def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY }
- state JobstestState {
- rep: int = 0
- stage: int = 0
- day: int = 1
- guest: bool = false
- fish: int = 0 # the state's count of fish
- paid: int = 0
- paid_rep: int = 0
- paid_scope: int = -1
- }
+ var rep: int = 0
+ var stage: int = 0
+ var day: int = 1
+ var guest: bool = false
+ var fish: int = 0 # the state's count of fish
+ var paid: int = 0
+ var paid_rep: int = 0
+ var paid_scope: int = -1
- function world_rep(jobstest_st: JobstestState, sc: int) -> int { return jobstest_st.rep }
- function world_stage(jobstest_st: JobstestState) -> int { return jobstest_st.stage }
- function world_day(jobstest_st: JobstestState) -> int { return jobstest_st.day }
- function world_owns(jobstest_st: JobstestState, sc: int) -> bool { return sc == JOBS_SELF or not jobstest_st.guest }
- function world_evidence(jobstest_st: JobstestState, kind: int, param: int) -> int {
- if kind == K_FISH { return jobstest_st.fish }
+ function world_rep(sc: int) -> int { return rep }
+ function world_stage() -> int { return stage }
+ function world_day() -> int { return day }
+ function world_owns(sc: int) -> bool { return sc == JOBS_SELF or not guest }
+ function world_evidence(kind: int, param: int) -> int {
+ if kind == K_FISH { return fish }
return -1
}
- function world_roll(jobs_st: mut JobsState, b: int, s: int) -> void {
- let need = jobs_roll(jobs_st, 2, 4)
- if b == BOARD_DAILY { jobs_post(jobs_st, b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(jobs_st, b, s, K_FISH, -1, need, 9 * need, 3) }
+ function world_roll(b: int, s: int) -> void {
+ let need = jobs_roll(2, 4)
+ if b == BOARD_DAILY { jobs_post(b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(b, s, K_FISH, -1, need, 9 * need, 3) }
}
- function pay(jobstest_st: mut JobstestState, sc: int, money: int, r: int) -> void {
- jobstest_st.paid += money
- jobstest_st.paid_rep += r
- jobstest_st.paid_scope = sc
+ function pay(sc: int, money: int, r: int) -> void {
+ paid += money
+ paid_rep += r
+ paid_scope = sc
}
bind JobsWorld { rep: fn world_rep, stage: fn world_stage, day: fn world_day, owns: fn world_owns, evidence: fn world_evidence, roll: fn world_roll }
bind JobsPay { reward: fn pay }
- function fresh(base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) -> void {
- jobstest_st.rep = 0
- jobstest_st.stage = 0
- jobstest_st.day = 1
- jobstest_st.guest = false
- jobstest_st.fish = 0
- jobstest_st.paid = 0
- jobstest_st.paid_rep = 0
- jobs_reset(base_st, jobs_st)
- jobs_party_reset(jobs_st)
+ function fresh() -> void {
+ rep = 0
+ stage = 0
+ day = 1
+ guest = false
+ fish = 0
+ paid = 0
+ paid_rep = 0
+ jobs_reset()
+ jobs_party_reset()
}
- function count_of(base_st: mut BaseState, jobs_st: mut JobsState, what: int) -> int {
- let fs = q_drain(base_st, jobs_facts(base_st, jobs_st))
+ function count_of(what: int) -> int {
+ let fs = q_drain(jobs_facts())
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
@@ -72,166 +70,166 @@ program JobsTest {
expect_eq(BOARD_COUNT, 2)
}
- test "a gate of standing and story" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- expect(jobs_offered(jobs_st, JOB_CATCH))
- expect(not jobs_offered(jobs_st, JOB_DEER))
- jobstest_st.rep = 10
- expect(jobs_offered(jobs_st, JOB_DEER))
- expect(not jobs_offered(jobs_st, JOB_STILL))
- jobstest_st.stage = 2
- expect(jobs_offered(jobs_st, JOB_STILL))
- expect_eq(len(jobs_at(jobs_st, 0, -1)), 3)
+ test "a gate of standing and story" {
+ fresh()
+ expect(jobs_offered(JOB_CATCH))
+ expect(not jobs_offered(JOB_DEER))
+ rep = 10
+ expect(jobs_offered(JOB_DEER))
+ expect(not jobs_offered(JOB_STILL))
+ stage = 2
+ expect(jobs_offered(JOB_STILL))
+ expect_eq(len(jobs_at(0, -1)), 3)
}
- test "counted by events, readied, handed in and paid" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobs_count(base_st, jobs_st, K_FISH, 0, 1)
- expect_eq(jobs_have(jobs_st, JOB_CATCH), 0)
- expect(jobs_take(base_st, jobs_st, JOB_CATCH))
- jobs_count(base_st, jobs_st, K_FISH, 0, 2)
- expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_ACTIVE)
- jobs_count(base_st, jobs_st, K_FISH, 0, 5)
- expect_eq(jobs_have(jobs_st, JOB_CATCH), 3)
- expect(jobs_ready_now(jobs_st, JOB_CATCH))
- expect_eq(count_of(base_st, jobs_st, JOBS_F_COMPLETED), 1)
- expect(jobs_hand_in(base_st, jobs_st, JOB_CATCH))
- expect(not jobs_hand_in(base_st, jobs_st, JOB_CATCH))
- expect_eq(jobstest_st.paid, 30)
- expect_eq(jobstest_st.paid_rep, 5)
- expect_eq(jobs_done(jobs_st, 0), 1)
- expect_eq(count_of(base_st, jobs_st, JOBS_F_HANDED_IN), 1)
+ test "counted by events, readied, handed in and paid" {
+ fresh()
+ jobs_count(K_FISH, 0, 1)
+ expect_eq(jobs_have(JOB_CATCH), 0)
+ expect(jobs_take(JOB_CATCH))
+ jobs_count(K_FISH, 0, 2)
+ expect_eq(jobs_state(JOB_CATCH), JOBS_ACTIVE)
+ jobs_count(K_FISH, 0, 5)
+ expect_eq(jobs_have(JOB_CATCH), 3)
+ expect(jobs_ready_now(JOB_CATCH))
+ expect_eq(count_of(JOBS_F_COMPLETED), 1)
+ expect(jobs_hand_in(JOB_CATCH))
+ expect(not jobs_hand_in(JOB_CATCH))
+ expect_eq(paid, 30)
+ expect_eq(paid_rep, 5)
+ expect_eq(jobs_done(0), 1)
+ expect_eq(count_of(JOBS_F_HANDED_IN), 1)
}
- test "the state already proves the work" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.fish = 2
- jobs_take(base_st, jobs_st, JOB_CATCH)
- expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
- jobstest_st.fish = 3
- jobs_recheck(base_st, jobs_st)
- expect(jobs_ready_now(jobs_st, JOB_CATCH))
+ test "the state already proves the work" {
+ fresh()
+ fish = 2
+ jobs_take(JOB_CATCH)
+ expect_eq(jobs_have(JOB_CATCH), 2)
+ fish = 3
+ jobs_recheck()
+ expect(jobs_ready_now(JOB_CATCH))
}
- test "a param narrows the count" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.rep = 10
- jobs_take(base_st, jobs_st, JOB_DEER)
- jobs_count(base_st, jobs_st, K_PHOTO, 3, 1)
- expect_eq(jobs_have(jobs_st, JOB_DEER), 0)
- jobs_count(base_st, jobs_st, K_PHOTO, 4, 1)
- expect(jobs_ready_now(jobs_st, JOB_DEER))
+ test "a param narrows the count" {
+ fresh()
+ rep = 10
+ jobs_take(JOB_DEER)
+ jobs_count(K_PHOTO, 3, 1)
+ expect_eq(jobs_have(JOB_DEER), 0)
+ jobs_count(K_PHOTO, 4, 1)
+ expect(jobs_ready_now(JOB_DEER))
}
- test "the game's own play readies a job with no need" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobs_take(base_st, jobs_st, JOB_STILL)
- jobs_mark(base_st, jobs_st, JOB_STILL, 2)
- expect_eq(jobs_have(jobs_st, JOB_STILL), 4)
- expect_eq(jobs_state(jobs_st, JOB_STILL), JOBS_ACTIVE)
- expect(jobs_complete(base_st, jobs_st, JOB_STILL))
- expect_eq(jobstest_st.paid, 60)
- expect_eq(jobs_done(jobs_st, 1), 1)
+ test "the game's own play readies a job with no need" {
+ fresh()
+ jobs_take(JOB_STILL)
+ jobs_mark(JOB_STILL, 2)
+ expect_eq(jobs_have(JOB_STILL), 4)
+ expect_eq(jobs_state(JOB_STILL), JOBS_ACTIVE)
+ expect(jobs_complete(JOB_STILL))
+ expect_eq(paid, 60)
+ expect_eq(jobs_done(1), 1)
}
- test "a guest counts its own and leaves the party's to the host" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.guest = true
- jobs_take(base_st, jobs_st, JOB_TEN)
- jobs_take(base_st, jobs_st, JOB_CATCH)
- jobs_count(base_st, jobs_st, K_FISH, 0, 2)
- expect_eq(jobs_have(jobs_st, JOB_TEN), 0)
- expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
- jobs_count_in(base_st, jobs_st, JOBS_PARTY, K_FISH, 0, 4)
- expect_eq(jobs_have(jobs_st, JOB_TEN), 4)
- expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
+ test "a guest counts its own and leaves the party's to the host" {
+ fresh()
+ guest = true
+ jobs_take(JOB_TEN)
+ jobs_take(JOB_CATCH)
+ jobs_count(K_FISH, 0, 2)
+ expect_eq(jobs_have(JOB_TEN), 0)
+ expect_eq(jobs_have(JOB_CATCH), 2)
+ jobs_count_in(JOBS_PARTY, K_FISH, 0, 4)
+ expect_eq(jobs_have(JOB_TEN), 4)
+ expect_eq(jobs_have(JOB_CATCH), 2)
}
- test "an offer is said once it opens, never for what was open at the reset" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
+ test "an offer is said once it opens, never for what was open at the reset" {
+ fresh()
jobs_system().tick(tick_new(0.1, 1, 0.0))
- expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 0)
- jobstest_st.rep = 10
+ expect_eq(count_of(JOBS_F_OFFERED), 0)
+ rep = 10
jobs_system().tick(tick_new(0.1, 2, 0.0))
- expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 1)
+ expect_eq(count_of(JOBS_F_OFFERED), 1)
}
- test "a board rolled by the day, the same every time" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.day = 3
- jobs_morning(base_st, jobs_st)
- expect_eq(len(jobs_posted(jobs_st, BOARD_DAILY)), 3)
- expect_eq(len(jobs_posted(jobs_st, BOARD_ADA)), 2)
- let a = jobs_post_need(jobs_st, BOARD_DAILY, 0) * 100 + jobs_post_need(jobs_st, BOARD_DAILY, 1) * 10 + jobs_post_need(jobs_st, BOARD_DAILY, 2)
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.day = 3
- jobs_morning(base_st, jobs_st)
- let b = jobs_post_need(jobs_st, BOARD_DAILY, 0) * 100 + jobs_post_need(jobs_st, BOARD_DAILY, 1) * 10 + jobs_post_need(jobs_st, BOARD_DAILY, 2)
+ test "a board rolled by the day, the same every time" {
+ fresh()
+ day = 3
+ jobs_morning()
+ expect_eq(len(jobs_posted(BOARD_DAILY)), 3)
+ expect_eq(len(jobs_posted(BOARD_ADA)), 2)
+ let a = jobs_post_need(BOARD_DAILY, 0) * 100 + jobs_post_need(BOARD_DAILY, 1) * 10 + jobs_post_need(BOARD_DAILY, 2)
+ fresh()
+ day = 3
+ jobs_morning()
+ let b = jobs_post_need(BOARD_DAILY, 0) * 100 + jobs_post_need(BOARD_DAILY, 1) * 10 + jobs_post_need(BOARD_DAILY, 2)
expect_eq(a, b)
- expect(jobs_post_need(jobs_st, BOARD_DAILY, 0) >= 2 and jobs_post_need(jobs_st, BOARD_DAILY, 0) <= 4)
+ expect(jobs_post_need(BOARD_DAILY, 0) >= 2 and jobs_post_need(BOARD_DAILY, 0) <= 4)
}
- test "a daily pays the moment it is met and waits for the morning" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.day = 2
- jobs_morning(base_st, jobs_st)
- let need = jobs_post_need(jobs_st, BOARD_DAILY, 1)
- jobs_count(base_st, jobs_st, K_WOOD, -1, need)
- expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_DONE)
- expect(jobs_posts_done(jobs_st, BOARD_DAILY) >= 1)
- expect_eq(jobstest_st.paid, 7 * jobs_posts_done(jobs_st, BOARD_DAILY))
- jobs_morning(base_st, jobs_st)
- expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_DONE)
- jobstest_st.day = 3
- jobs_morning(base_st, jobs_st)
- expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_ACTIVE)
+ test "a daily pays the moment it is met and waits for the morning" {
+ fresh()
+ day = 2
+ jobs_morning()
+ let need = jobs_post_need(BOARD_DAILY, 1)
+ jobs_count(K_WOOD, -1, need)
+ expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_DONE)
+ expect(jobs_posts_done(BOARD_DAILY) >= 1)
+ expect_eq(paid, 7 * jobs_posts_done(BOARD_DAILY))
+ jobs_morning()
+ expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_DONE)
+ day = 3
+ jobs_morning()
+ expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_ACTIVE)
}
- test "a post counts only what came after it went up, and is replaced when handed in" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobstest_st.fish = 5
- jobs_morning(base_st, jobs_st)
- let need = jobs_post_need(jobs_st, BOARD_ADA, 0)
- jobs_recheck(base_st, jobs_st)
- expect_eq(jobs_post_have(jobs_st, BOARD_ADA, 0), 0)
- jobstest_st.fish = 5 + need
- jobs_recheck(base_st, jobs_st)
- expect_eq(jobs_post_state(jobs_st, BOARD_ADA, 0), JOBS_READY)
- expect(jobs_post_hand_in(base_st, jobs_st, BOARD_ADA, 0))
- expect_eq(jobstest_st.paid_scope, JOBS_PARTY)
- expect_eq(jobs_total(jobs_st, BOARD_ADA), 1)
- expect_eq(jobs_post_state(jobs_st, BOARD_ADA, 0), JOBS_ACTIVE)
+ test "a post counts only what came after it went up, and is replaced when handed in" {
+ fresh()
+ fish = 5
+ jobs_morning()
+ let need = jobs_post_need(BOARD_ADA, 0)
+ jobs_recheck()
+ expect_eq(jobs_post_have(BOARD_ADA, 0), 0)
+ fish = 5 + need
+ jobs_recheck()
+ expect_eq(jobs_post_state(BOARD_ADA, 0), JOBS_READY)
+ expect(jobs_post_hand_in(BOARD_ADA, 0))
+ expect_eq(paid_scope, JOBS_PARTY)
+ expect_eq(jobs_total(BOARD_ADA), 1)
+ expect_eq(jobs_post_state(BOARD_ADA, 0), JOBS_ACTIVE)
}
- test "a stale post comes down after its days" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobs_morning(base_st, jobs_st)
- jobstest_st.day = 7
- jobs_morning(base_st, jobs_st)
- expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 1)
- jobstest_st.day = 8
- jobs_morning(base_st, jobs_st)
- expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 8)
+ test "a stale post comes down after its days" {
+ fresh()
+ jobs_morning()
+ day = 7
+ jobs_morning()
+ expect_eq(jobs_post_day(BOARD_ADA, 0), 1)
+ day = 8
+ jobs_morning()
+ expect_eq(jobs_post_day(BOARD_ADA, 0), 8)
}
- test "each scope saves its own, by key" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
- fresh(base_st, jobs_st, jobstest_st)
- jobs_take(base_st, jobs_st, JOB_CATCH)
- jobs_count(base_st, jobs_st, K_FISH, 0, 2)
- jobs_take(base_st, jobs_st, JOB_TEN)
- jobs_count(base_st, jobs_st, K_FISH, 0, 1)
- jobstest_st.day = 2
- jobs_morning(base_st, jobs_st)
- let body = jobs_save(jobs_st)
- let world = jobs_party_save(jobs_st)
- let need = jobs_post_need(jobs_st, BOARD_ADA, 1)
- fresh(base_st, jobs_st, jobstest_st)
- jobs_load(jobs_st, body, 1)
- expect_eq(jobs_have(jobs_st, JOB_CATCH), 3)
- expect_eq(jobs_state(jobs_st, JOB_TEN), JOBS_OFFERED)
- jobs_party_load(jobs_st, world, 1)
- expect_eq(jobs_have(jobs_st, JOB_TEN), 1)
- expect_eq(jobs_post_need(jobs_st, BOARD_ADA, 1), need)
- expect_eq(jobs_post_day(jobs_st, BOARD_DAILY, 0), 2)
+ test "each scope saves its own, by key" {
+ fresh()
+ jobs_take(JOB_CATCH)
+ jobs_count(K_FISH, 0, 2)
+ jobs_take(JOB_TEN)
+ jobs_count(K_FISH, 0, 1)
+ day = 2
+ jobs_morning()
+ let body = jobs_save()
+ let world = jobs_party_save()
+ let need = jobs_post_need(BOARD_ADA, 1)
+ fresh()
+ jobs_load(body, 1)
+ expect_eq(jobs_have(JOB_CATCH), 3)
+ expect_eq(jobs_state(JOB_TEN), JOBS_OFFERED)
+ jobs_party_load(world, 1)
+ expect_eq(jobs_have(JOB_TEN), 1)
+ expect_eq(jobs_post_need(BOARD_ADA, 1), need)
+ expect_eq(jobs_post_day(BOARD_DAILY, 0), 2)
}
}
diff --git a/packages/ludic.lab/boot.ludic b/packages/ludic.lab/boot.ludic
index be5ddd71..c6e97b79 100644
--- a/packages/ludic.lab/boot.ludic
+++ b/packages/ludic.lab/boot.ludic
@@ -1,56 +1,56 @@
# boot.ludic - the window, the renderer on a plate (no terrain, no grass, no water), the plate and
# its sky, then the scene named by R3D_SCENE (else the first one added)
-function lab_draw(lab_st: LabState, render3d_st: mut Render3dState) -> void {
- actor_draw(render3d_st)
- scatter_draw(render3d_st)
- if lab_st.lb_draw != null { lab_st.lb_draw() }
+function lab_draw() -> void {
+ actor_draw()
+ scatter_draw()
+ if lb_draw != null { lb_draw() }
}
-function lab_casters(render3d_st: mut Render3dState, light_vp: floats) -> void {
- actor_draw_casters(render3d_st, light_vp)
- scatter_draw_casters(render3d_st, light_vp)
+function lab_casters(light_vp: floats) -> void {
+ actor_draw_casters(light_vp)
+ scatter_draw_casters(light_vp)
}
-function lab_pick_scene(lab_st: mut LabState) -> int {
+function lab_pick_scene() -> int {
if LAB_COUNT == 0 { return -1 }
- lab_st.lb_scene = LabScenes[0].key
- if Os.has_env("R3D_SCENE") { lab_st.lb_scene = Os.env("R3D_SCENE") }
- return labscenes_find(lab_st.lb_scene)
+ lb_scene = LabScenes[0].key
+ if Os.has_env("R3D_SCENE") { lb_scene = Os.env("R3D_SCENE") }
+ return labscenes_find(lb_scene)
}
# once, from the program's Start handler
-export function lab_boot(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
- lab_st.lb_windowed = is_windowed()
- let k = lab_pick_scene(lab_st)
+export function lab_boot() -> void {
+ lb_windowed = is_windowed()
+ let k = lab_pick_scene()
if k < 0 {
- print(`lab: no scene called '{lab_st.lb_scene}' - {lab_scene_names()}`)
+ print(`lab: no scene called '{lb_scene}' - {lab_scene_names()}`)
quit()
return
}
var ww = 1920
var wh = 1080
- if lab_st.lb_windowed {
+ if lb_windowed {
ww = 960
wh = 540
}
- r3d_plate_mode(render3d_st, true)
- render3d_st.r3d_sky_path = `{lab_plate_dir(lab_st)}/sky.hdr`
- if not r3d_init(render3d_st, ww, wh, lab_st.lb_title) {
+ r3d_plate_mode(true)
+ r3d_sky_path = `{lab_plate_dir()}/sky.hdr`
+ if not r3d_init(ww, wh, lb_title) {
quit()
return
}
- if lab_st.lb_font != "" { overlay_init(render3d_st, lab_st.lb_font) }
- r3d_on_draw(render3d_st, fn lab_draw)
- r3d_on_casters(render3d_st, fn lab_casters)
- lab_plate_load(lab_st, render3d_st)
- lab_shots_reset(lab_st)
- lab_st.lb_out = `build/lab/{lab_st.lb_scene}`
+ if lb_font != "" { overlay_init(lb_font) }
+ r3d_on_draw(fn lab_draw)
+ r3d_on_casters(fn lab_casters)
+ lab_plate_load()
+ lab_shots_reset()
+ lb_out = `build/lab/{lb_scene}`
let sc = LabScenes[k]
- lab_st.lb_draw = sc.draw
+ lb_draw = sc.draw
if sc.setup != null { sc.setup() }
- if lab_shot_count(lab_st) == 0 { lab_shot_at(lab_st, "plate", 0.0, 1.0, 0.0, 8.0, 0.0, 15.0) }
- lab_shot_apply(lab_st, render3d_st, 0)
- if not lab_st.lb_windowed {
+ if lab_shot_count() == 0 { lab_shot_at("plate", 0.0, 1.0, 0.0, 8.0, 0.0, 15.0) }
+ lab_shot_apply(0)
+ if not lb_windowed {
Fs.mkdir("build")
Fs.mkdir("build/lab")
- Fs.mkdir(lab_st.lb_out)
+ Fs.mkdir(lb_out)
}
- print(`lab: scene {lab_st.lb_scene}, {lab_shot_count(lab_st)} shots`)
+ print(`lab: scene {lb_scene}, {lab_shot_count()} shots`)
}
diff --git a/packages/ludic.lab/example/plate.ludic b/packages/ludic.lab/example/plate.ludic
index da90f718..7c406e59 100644
--- a/packages/ludic.lab/example/plate.ludic
+++ b/packages/ludic.lab/example/plate.ludic
@@ -6,15 +6,15 @@
import "ludic.lab"
program LabPlate {
numbers float
- function probe_setup(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
- lab_model(render3d_st, lab_plate_dir(lab_st), "probe.gltf", "probe", 0.0, 0.0, 0.5)
- lab_note(lab_st, "a box and a ball, in the plate's light")
- lab_shot_at(lab_st, "front", 0.0, 0.7, 0.0, 4.0, 0.0, 12.0)
- lab_shot_at(lab_st, "side", 0.0, 0.7, 0.0, 4.0, 90.0, 12.0)
- lab_shot_at(lab_st, "above", 0.0, 0.5, 0.0, 7.0, 30.0, 55.0)
+ function probe_setup() -> void {
+ lab_model(lab_plate_dir(), "probe.gltf", "probe", 0.0, 0.0, 0.5)
+ lab_note("a box and a ball, in the plate's light")
+ lab_shot_at("front", 0.0, 0.7, 0.0, 4.0, 0.0, 12.0)
+ lab_shot_at("side", 0.0, 0.7, 0.0, 4.0, 90.0, 12.0)
+ lab_shot_at("above", 0.0, 0.5, 0.0, 7.0, 30.0, 55.0)
}
def LabScenes probe { setup: fn probe_setup }
- handler Boot(lab_st: mut LabState, render3d_st: mut Render3dState) phase Start { lab_boot(lab_st, render3d_st) }
- handler Look(lab_st: mut LabState, render3d_st: mut Render3dState) phase Input { lab_input(lab_st, render3d_st) }
- handler Draw(lab_st: mut LabState, render3d_st: mut Render3dState) phase Render { lab_render(lab_st, render3d_st) }
+ handler Boot phase Start { lab_boot() }
+ handler Look phase Input { lab_input() }
+ handler Draw phase Render { lab_render() }
}
diff --git a/packages/ludic.lab/loop.ludic b/packages/ludic.lab/loop.ludic
index 20cfed87..919f7d6b 100644
--- a/packages/ludic.lab/loop.ludic
+++ b/packages/ludic.lab/loop.ludic
@@ -1,50 +1,50 @@
# loop.ludic - each frame: the scene's tick by a fixed step, the shot's camera, its caption, and -
# headless - the picture once the shot has settled, then the next shot, then quit. In a window
# N and P step through the shots.
-export function lab_input(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
- if not lab_st.lb_windowed { return }
+export function lab_input() -> void {
+ if not lb_windowed { return }
Input.poll()
- let n = lab_shot_count(lab_st)
+ let n = lab_shot_count()
if n > 0 and Input.key_pressed(key: 'n') {
- lab_st.lb_shot = (lab_st.lb_shot + 1) % n
- lab_shot_apply(lab_st, render3d_st, lab_st.lb_shot)
+ lb_shot = (lb_shot + 1) % n
+ lab_shot_apply(lb_shot)
}
if n > 0 and Input.key_pressed(key: 'p') {
- lab_st.lb_shot = (lab_st.lb_shot + n - 1) % n
- lab_shot_apply(lab_st, render3d_st, lab_st.lb_shot)
+ lb_shot = (lb_shot + n - 1) % n
+ lab_shot_apply(lb_shot)
}
}
-export function lab_render(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
- lab_st.lb_frame += 1
- let k = labscenes_find(lab_st.lb_scene)
+export function lab_render() -> void {
+ lb_frame += 1
+ let k = labscenes_find(lb_scene)
if k >= 0 and LabScenes[k].tick != null { LabScenes[k].tick(1.0 / 60.0) }
- r3d_frame(render3d_st, lab_time(lab_st))
- lab_caption(lab_st, render3d_st)
- if not lab_st.lb_windowed { lab_take(lab_st, render3d_st) }
- r3d_present(render3d_st)
+ r3d_frame(lab_time())
+ lab_caption()
+ if not lb_windowed { lab_take() }
+ r3d_present()
}
-function lab_caption(lab_st: LabState, render3d_st: mut Render3dState) -> void {
- if lab_st.lb_font == "" { return }
- ov_begin(render3d_st)
+function lab_caption() -> void {
+ if lb_font == "" { return }
+ ov_begin()
var name = ""
- if lab_st.lb_shot < lab_shot_count(lab_st) { name = lab_st.lb_shot_names[lab_st.lb_shot] }
- ov_rect(render3d_st, 0, 0, 900, 64, 0.0, 0.0, 0.0, 0.55)
- ov_text(render3d_st, 20, 10, 30, `{lab_st.lb_scene} / {name} ({lab_st.lb_shot + 1} of {lab_shot_count(lab_st)})`, 1.0, 1.0, 1.0, 1.0)
- if len(lab_st.lb_note) > 0 { ov_text(render3d_st, 20, 42, 18, lab_st.lb_note, 0.8, 0.8, 0.8, 1.0) }
- ov_end(render3d_st)
+ if lb_shot < lab_shot_count() { name = lb_shot_names[lb_shot] }
+ ov_rect(0, 0, 900, 64, 0.0, 0.0, 0.0, 0.55)
+ ov_text(20, 10, 30, `{lb_scene} / {name} ({lb_shot + 1} of {lab_shot_count()})`, 1.0, 1.0, 1.0, 1.0)
+ if len(lb_note) > 0 { ov_text(20, 42, 18, lb_note, 0.8, 0.8, 0.8, 1.0) }
+ ov_end()
}
-function lab_take(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
- lab_st.lb_wait += 1
- if lab_st.lb_wait < lab_st.lb_settle { return }
- let base = `{lab_st.lb_out}/{lab_st.lb_shot_names[lab_st.lb_shot]}`
- r3d_screenshot(render3d_st, `{base}.ppm`)
- if lab_ppm_to_png(lab_st, `{base}.ppm`, `{base}.png`) { print(`lab: shot {base}.png`) } else { print(`lab: shot {base}.ppm`) }
- lab_st.lb_shots_taken += 1
- lab_st.lb_wait = 0
- lab_st.lb_shot += 1
- if lab_st.lb_shot >= lab_shot_count(lab_st) {
+function lab_take() -> void {
+ lb_wait += 1
+ if lb_wait < lb_settle { return }
+ let base = `{lb_out}/{lb_shot_names[lb_shot]}`
+ r3d_screenshot(`{base}.ppm`)
+ if lab_ppm_to_png(`{base}.ppm`, `{base}.png`) { print(`lab: shot {base}.png`) } else { print(`lab: shot {base}.ppm`) }
+ lb_shots_taken += 1
+ lb_wait = 0
+ lb_shot += 1
+ if lb_shot >= lab_shot_count() {
quit()
return
}
- lab_shot_apply(lab_st, render3d_st, lab_st.lb_shot)
+ lab_shot_apply(lb_shot)
}
diff --git a/packages/ludic.lab/plate.ludic b/packages/ludic.lab/plate.ludic
index 95f5cc51..59651c72 100644
--- a/packages/ludic.lab/plate.ludic
+++ b/packages/ludic.lab/plate.ludic
@@ -1,21 +1,23 @@
# plate.ludic - the stage: a flat disc 40 m across at y = 0 with a line every metre, lit by the
# package's own small sky (a sun 40 degrees up), and nothing else. Its files are in plate/.
+var lb_plate: Actor = null
+var lb_plate_dir: string = ""
# the package's plate/ directory: beside the project (a checkout of the toolchain) or installed
-export function lab_plate_dir(lab_st: mut LabState) -> string {
- if lab_st.lb_plate_dir != "" { return lab_st.lb_plate_dir }
- lab_st.lb_plate_dir = "packages/ludic.lab/plate"
- if not Fs.exists(`{lab_st.lb_plate_dir}/plate.gltf`) { lab_st.lb_plate_dir = `{r3d_home()}/packages/ludic.lab/plate` }
- return lab_st.lb_plate_dir
+export function lab_plate_dir() -> string {
+ if lb_plate_dir != "" { return lb_plate_dir }
+ lb_plate_dir = "packages/ludic.lab/plate"
+ if not Fs.exists(`{lb_plate_dir}/plate.gltf`) { lb_plate_dir = `{r3d_home()}/packages/ludic.lab/plate` }
+ return lb_plate_dir
}
-function lab_plate_load(lab_st: mut LabState, render3d_st: mut Render3dState) -> void {
- let m = gltf_load(render3d_st, lab_plate_dir(lab_st), "plate.gltf", "plate")
+function lab_plate_load() -> void {
+ let m = gltf_load(lab_plate_dir(), "plate.gltf", "plate")
if m == null { return }
- lab_st.lb_plate = actor_new(render3d_st, m)
- lab_st.lb_plate.casts = false
- lab_st.lb_plate.cull = 0.0
- lab_st.lb_plate.radius = 0.0
- actor_place(lab_st.lb_plate, 0.0, 0.0, 0.0, 0.0)
+ lb_plate = actor_new(m)
+ lb_plate.casts = false
+ lb_plate.cull = 0.0
+ lb_plate.radius = 0.0
+ actor_place(lb_plate, 0.0, 0.0, 0.0, 0.0)
}
# the ground under (x, z): the plate is flat at 0
export function lab_ground_y(x: float, z: float) -> float { return 0.0 }
@@ -25,10 +27,10 @@ export function lab_stand(a: Actor, x: float, z: float, yaw: float) -> void {
actor_place(a, x, lab_ground_y(x, z), z, yaw)
}
# a model from a glTF file stood on the plate: the one call a scene usually needs
-export function lab_model(render3d_st: mut Render3dState, dir: string, file: string, node: string, x: float, z: float, yaw: float) -> Actor {
- let m = gltf_load(render3d_st, dir, file, node)
+export function lab_model(dir: string, file: string, node: string, x: float, z: float, yaw: float) -> Actor {
+ let m = gltf_load(dir, file, node)
if m == null { return null }
- let a = actor_new(render3d_st, m)
+ let a = actor_new(m)
lab_stand(a, x, z, yaw)
return a
}
diff --git a/packages/ludic.lab/png.ludic b/packages/ludic.lab/png.ludic
index bd5ed1f6..2e4c9534 100644
--- a/packages/ludic.lab/png.ludic
+++ b/packages/ludic.lab/png.ludic
@@ -1,27 +1,28 @@
# png.ludic - the renderer writes a frame as a PPM; the lab keeps it as a PNG, stored rather than
# 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.
+var lp_table: []long = null
-function lp_table_init(lab_st: mut LabState) -> void {
- if lab_st.lp_table != null { return }
- lab_st.lp_table = new []long
+function lp_table_init() -> void {
+ if lp_table != null { return }
+ lp_table = 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(lp_table, c)
}
}
# 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)
+function lp_crc(out: []byte, from: int, n: int) -> long {
+ lp_table_init()
let mask: long = 4294967295
var c = mask
for i in from .. from + n {
let k = int((c ^ long(out[i])) & long(255))
- c = lab_st.lp_table[k] ^ (c >> long(8))
+ c = lp_table[k] ^ (c >> long(8))
}
return c ^ mask
}
@@ -35,7 +36,9 @@ function lp_tag(out: []byte, at: int, tag: string) -> void {
for i in 0 .. 4 { out[at + i] = tag[i] }
}
# a PPM's header: "P6", width, height, maxval, each after white space; returns the pixel offset
-function lp_ppm_header(lab_st: mut LabState, d: []byte) -> int {
+var lp_w: int = 0
+var lp_h: int = 0
+function lp_ppm_header(d: []byte) -> int {
var i = 2
var got = 0
var vals = new []int
@@ -49,17 +52,17 @@ function lp_ppm_header(lab_st: mut LabState, d: []byte) -> int {
push(vals, v)
got += 1
}
- lab_st.lp_w = vals[0]
- lab_st.lp_h = vals[1]
+ lp_w = vals[0]
+ lp_h = vals[1]
return i + 1
}
# the PPM at `ppm` as a PNG at `png`; the PPM is removed
-export function lab_ppm_to_png(lab_st: mut LabState, ppm: string, png: string) -> bool {
+export function lab_ppm_to_png(ppm: string, png: string) -> bool {
let d = Fs.read_bytes(ppm)
if d == null or len(d) < 8 { return false }
- let px = lp_ppm_header(lab_st, d)
- let w = lab_st.lp_w
- let h = lab_st.lp_h
+ let px = lp_ppm_header(d)
+ let w = lp_w
+ let h = lp_h
let row = w * 3 + 1
let raw = row * h
let blocks = (raw + 65534) / 65535
@@ -78,7 +81,7 @@ export function lab_ppm_to_png(lab_st: mut LabState, ppm: string, png: string) -
out[at + 18] = 0
out[at + 19] = 0
out[at + 20] = 0
- lp_put32(out, at + 21, lp_crc(lab_st, out, at + 4, 17))
+ lp_put32(out, at + 21, lp_crc(out, at + 4, 17))
at += 25
lp_put32(out, at, long(idat))
lp_tag(out, at + 4, "IDAT")
@@ -115,11 +118,11 @@ export function lab_ppm_to_png(lab_st: mut LabState, ppm: string, png: string) -
}
lp_put32(out, o, long(b) * long(65536) + long(a))
o += 4
- lp_put32(out, o, lp_crc(lab_st, out, at + 4, o - at - 4))
+ lp_put32(out, o, lp_crc(out, at + 4, o - at - 4))
o += 4
lp_put32(out, o, long(0))
lp_tag(out, o + 4, "IEND")
- lp_put32(out, o + 8, lp_crc(lab_st, out, o + 4, 4))
+ lp_put32(out, o + 8, lp_crc(out, o + 4, 4))
o += 12
if not Fs.write_bytes(png, out, o) { return false }
Fs.remove(ppm)
diff --git a/packages/ludic.lab/shots.ludic b/packages/ludic.lab/shots.ludic
index 4025a9c9..adfd6f55 100644
--- a/packages/ludic.lab/shots.ludic
+++ b/packages/ludic.lab/shots.ludic
@@ -1,20 +1,20 @@
# shots.ludic - a scene names its cameras; the loop takes them in order
-function lab_shots_reset(lab_st: mut LabState) -> void {
- lab_st.lb_shot_names = new []string
- lab_st.lb_shot_pose = new []float
+function lab_shots_reset() -> void {
+ lb_shot_names = new []string
+ lb_shot_pose = new []float
}
# a camera at (x, y, z) looking along yaw / pitch, both in DEGREES (cam_set's units)
-export function lab_shot(lab_st: mut LabState, name: string, x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
- push(lab_st.lb_shot_names, name)
- push(lab_st.lb_shot_pose, x)
- push(lab_st.lb_shot_pose, y)
- push(lab_st.lb_shot_pose, z)
- push(lab_st.lb_shot_pose, yaw_deg)
- push(lab_st.lb_shot_pose, pitch_deg)
+export function lab_shot(name: string, x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
+ push(lb_shot_names, name)
+ push(lb_shot_pose, x)
+ push(lb_shot_pose, y)
+ push(lb_shot_pose, z)
+ push(lb_shot_pose, yaw_deg)
+ push(lb_shot_pose, pitch_deg)
}
# a camera `dist` metres from the point (tx, ty, tz), from bearing `from_deg` (0 = from +z),
# `up_deg` above the horizontal, looking at the point
-export function lab_shot_at(lab_st: mut LabState, name: string, tx: float, ty: float, tz: float, dist: float, from_deg: float, up_deg: float) -> void {
+export function lab_shot_at(name: string, tx: float, ty: float, tz: float, dist: float, from_deg: float, up_deg: float) -> void {
let b = Math.deg_to_rad(from_deg)
let e = Math.deg_to_rad(up_deg)
let hd = dist * Math.cos(e)
@@ -22,14 +22,14 @@ export function lab_shot_at(lab_st: mut LabState, name: string, tx: float, ty: f
let cz = tz + Math.cos(b) * hd
let cy = ty + dist * Math.sin(e)
let yaw = Math.atan2(cx - tx, cz - tz) * (180.0 / PI)
- lab_shot(lab_st, name, cx, cy, cz, yaw, -up_deg)
+ lab_shot(name, cx, cy, cz, yaw, -up_deg)
}
-export function lab_shot_count(lab_st: LabState) -> int {
- if lab_st.lb_shot_names == null { return 0 }
- return len(lab_st.lb_shot_names)
+export function lab_shot_count() -> int {
+ if lb_shot_names == null { return 0 }
+ return len(lb_shot_names)
}
-function lab_shot_apply(lab_st: LabState, render3d_st: mut Render3dState, i: int) -> void {
- if i < 0 or i >= lab_shot_count(lab_st) { return }
+function lab_shot_apply(i: int) -> void {
+ if i < 0 or i >= lab_shot_count() { return }
let o = i * 5
- cam_set(render3d_st, lab_st.lb_shot_pose[o], lab_st.lb_shot_pose[o + 1], lab_st.lb_shot_pose[o + 2], lab_st.lb_shot_pose[o + 3], lab_st.lb_shot_pose[o + 4])
+ cam_set(lb_shot_pose[o], lb_shot_pose[o + 1], lb_shot_pose[o + 2], lb_shot_pose[o + 3], lb_shot_pose[o + 4])
}
diff --git a/packages/ludic.lab/state.ludic b/packages/ludic.lab/state.ludic
index affac690..b0b667b8 100644
--- a/packages/ludic.lab/state.ludic
+++ b/packages/ludic.lab/state.ludic
@@ -1,34 +1,27 @@
# state.ludic - which scene, its shots, and where the loop is in them
-export state LabState {
- lb_plate: Actor = null
- lb_plate_dir: string = ""
- lp_table: []long = null
- lp_w: int = 0
- lp_h: int = 0
- lb_scene: string = ""
- lb_frame: int = 0
- lb_shot_names: []string = null
- lb_shot_pose: []float = null # 5 per shot: x, y, z, yaw and pitch in degrees
- lb_shot: int = 0 # the shot being taken or looked at
- lb_wait: int = 0 # frames held on it so far
- lb_settle: int = 45 # frames a shot is held before it is taken
- lb_out: string = "" # build/lab/
- lb_windowed: bool = false
- lb_note: string = "" # one line a scene may show under its title
- lb_font: string = "" # an overlay font atlas directory; "" draws no captions
- lb_title: string = "Ludic Lab"
- lb_draw: fn() = null # the scene's own drawing, beside the actors
- lb_shots_taken: int = 0
-}
+var lb_scene: string = ""
+var lb_frame: int = 0
+var lb_shot_names: []string = null
+var lb_shot_pose: []float = null # 5 per shot: x, y, z, yaw and pitch in degrees
+var lb_shot: int = 0 # the shot being taken or looked at
+var lb_wait: int = 0 # frames held on it so far
+var lb_settle: int = 45 # frames a shot is held before it is taken
+var lb_out: string = "" # build/lab/
+var lb_windowed: bool = false
+var lb_note: string = "" # one line a scene may show under its title
+var lb_font: string = "" # an overlay font atlas directory; "" draws no captions
+var lb_title: string = "Ludic Lab"
+var lb_draw: fn() = null # the scene's own drawing, beside the actors
+var lb_shots_taken: int = 0
# frames a shot settles before it is taken (streaming, auto-exposure); 45 by default
-export function lab_settle(lab_st: mut LabState, frames: int) -> void { lab_st.lb_settle = Math.max(1, frames) }
+export function lab_settle(frames: int) -> void { lb_settle = Math.max(1, frames) }
# the overlay's font atlas (a directory with font.json and font.png) for the captions
-export function lab_font(lab_st: mut LabState, dir: string) -> void { lab_st.lb_font = dir }
-export function lab_title(lab_st: mut LabState, title: string) -> void { lab_st.lb_title = title }
+export function lab_font(dir: string) -> void { lb_font = dir }
+export function lab_title(title: string) -> void { lb_title = title }
# one line under the scene's title in a window
-export function lab_note(lab_st: mut LabState, s: string) -> void { lab_st.lb_note = s }
+export function lab_note(s: string) -> void { lb_note = s }
# the scene being shown, and the frame the clock is on (60 a second, whatever the machine)
-export function lab_scene(lab_st: LabState) -> string { return lab_st.lb_scene }
-export function lab_frame(lab_st: LabState) -> int { return lab_st.lb_frame }
-export function lab_time(lab_st: LabState) -> float { return float(lab_st.lb_frame) / 60.0 }
+export function lab_scene() -> string { return lb_scene }
+export function lab_frame() -> int { return lb_frame }
+export function lab_time() -> float { return float(lb_frame) / 60.0 }
diff --git a/packages/ludic.lab/tests/lab_test.ludic b/packages/ludic.lab/tests/lab_test.ludic
index 48f9b4fa..05e0af6f 100644
--- a/packages/ludic.lab/tests/lab_test.ludic
+++ b/packages/ludic.lab/tests/lab_test.ludic
@@ -16,10 +16,10 @@ program LabTest {
Fs.write_bytes(path, b, len(hdr) + 12)
}
- test "a PPM becomes a PNG with its signature, its size and a valid IEND" (lab_st: mut LabState) {
+ test "a PPM becomes a PNG with its signature, its size and a valid IEND" {
let dir = Os.temp_dir()
ppm(`{dir}/lab_test.ppm`)
- expect(lab_ppm_to_png(lab_st, `{dir}/lab_test.ppm`, `{dir}/lab_test.png`))
+ expect(lab_ppm_to_png(`{dir}/lab_test.ppm`, `{dir}/lab_test.png`))
let p = Fs.read_bytes(`{dir}/lab_test.png`)
expect(p != null)
expect_eq(p[0], 137)
@@ -35,15 +35,15 @@ program LabTest {
expect(not Fs.exists(`{dir}/lab_test.ppm`))
}
- test "a camera at a point looks back at it" (lab_st: mut LabState) {
- lab_shots_reset(lab_st)
- lab_shot_at(lab_st, "front", 0.0, 1.0, 0.0, 10.0, 0.0, 0.0)
- lab_shot_at(lab_st, "side", 0.0, 1.0, 0.0, 10.0, 90.0, 30.0)
- expect_eq(lab_shot_count(lab_st), 2)
- expect_near(lab_st.lb_shot_pose[2], 10.0, 0.001) # front: 10 m out along +z
- expect_near(lab_st.lb_shot_pose[5 + 0], 8.660, 0.01) # side: along +x, 30 degrees up
- expect_near(lab_st.lb_shot_pose[5 + 1], 6.0, 0.01)
- expect_near(lab_st.lb_shot_pose[5 + 4], -30.0, 0.001)
+ test "a camera at a point looks back at it" {
+ lab_shots_reset()
+ lab_shot_at("front", 0.0, 1.0, 0.0, 10.0, 0.0, 0.0)
+ lab_shot_at("side", 0.0, 1.0, 0.0, 10.0, 90.0, 30.0)
+ expect_eq(lab_shot_count(), 2)
+ expect_near(lb_shot_pose[2], 10.0, 0.001) # front: 10 m out along +z
+ expect_near(lb_shot_pose[5 + 0], 8.660, 0.01) # side: along +x, 30 degrees up
+ expect_near(lb_shot_pose[5 + 1], 6.0, 0.01)
+ expect_near(lb_shot_pose[5 + 4], -30.0, 0.001)
}
test "scenes are what the program added, in its order" {
diff --git a/packages/ludic.minimap/explored.ludic b/packages/ludic.minimap/explored.ludic
index e0445382..75cdc591 100644
--- a/packages/ludic.minimap/explored.ludic
+++ b/packages/ludic.minimap/explored.ludic
@@ -1,97 +1,100 @@
# explored.ludic - walking reveals the cells round you; "explored" is the share of the core's LAND
# that has been seen, since open water has nothing on it to miss
+var mm_land_n: int = 0 # land cells in the core, counted once per origin
+var mm_land_ox: float = 0.0
+var mm_land_oz: float = 0.0
# every cell any part of which is within r of (x, z)
-export function minimap_reveal(minimap_st: mut MinimapState, x: float, z: float, r: float) -> void {
- mm_grid(minimap_st)
- let c = minimap_cell(minimap_st, x, z)
- let reach = int(r / minimap_st.mm_cell_m) + 1
- let x0 = minimap_grid_x0(minimap_st)
- let z0 = minimap_grid_z0(minimap_st)
- let ci = int(Math.floor((x - x0) / minimap_st.mm_cell_m))
- let cj = int(Math.floor((z - z0) / minimap_st.mm_cell_m))
+export function minimap_reveal(x: float, z: float, r: float) -> void {
+ mm_grid()
+ let c = minimap_cell(x, z)
+ let reach = int(r / mm_cell_m) + 1
+ let x0 = minimap_grid_x0()
+ let z0 = minimap_grid_z0()
+ let ci = int(Math.floor((x - x0) / mm_cell_m))
+ let cj = int(Math.floor((z - z0) / mm_cell_m))
for j in cj - reach .. cj + reach + 1 {
for i in ci - reach .. ci + reach + 1 {
- if i < 0 or j < 0 or i >= minimap_st.mm_cells or j >= minimap_st.mm_cells { continue }
- let nx = Math.clamp(x, x0 + float(i) * minimap_st.mm_cell_m, x0 + float(i + 1) * minimap_st.mm_cell_m)
- let nz = Math.clamp(z, z0 + float(j) * minimap_st.mm_cell_m, z0 + float(j + 1) * minimap_st.mm_cell_m)
- if (nx - x) * (nx - x) + (nz - z) * (nz - z) <= r * r { minimap_reveal_cell(minimap_st, j * minimap_st.mm_cells + i) }
+ if i < 0 or j < 0 or i >= mm_cells or j >= mm_cells { continue }
+ let nx = Math.clamp(x, x0 + float(i) * mm_cell_m, x0 + float(i + 1) * mm_cell_m)
+ let nz = Math.clamp(z, z0 + float(j) * mm_cell_m, z0 + float(j + 1) * mm_cell_m)
+ if (nx - x) * (nx - x) + (nz - z) * (nz - z) <= r * r { minimap_reveal_cell(j * mm_cells + i) }
}
}
- if c >= 0 { minimap_reveal_cell(minimap_st, c) }
+ if c >= 0 { minimap_reveal_cell(c) }
}
-export function minimap_reveal_cell(minimap_st: mut MinimapState, c: int) -> void {
- mm_grid(minimap_st)
- if c < 0 or c >= minimap_st.mm_cells * minimap_st.mm_cells or minimap_st.mm_seen[c] != 0 { return }
- minimap_st.mm_seen[c] = 1
- minimap_st.mm_version += 1
+export function minimap_reveal_cell(c: int) -> void {
+ mm_grid()
+ if c < 0 or c >= mm_cells * mm_cells or mm_seen[c] != 0 { return }
+ mm_seen[c] = 1
+ mm_version += 1
}
-export function minimap_seen(minimap_st: MinimapState, c: int) -> bool {
- if minimap_st.mm_seen == null or c < 0 or c >= minimap_st.mm_cells * minimap_st.mm_cells { return false }
- return minimap_st.mm_seen[c] != 0
+export function minimap_seen(c: int) -> bool {
+ if mm_seen == null or c < 0 or c >= mm_cells * mm_cells { return false }
+ return mm_seen[c] != 0
}
-export function minimap_seen_at(minimap_st: MinimapState, x: float, z: float) -> bool { return minimap_seen(minimap_st, minimap_cell(minimap_st, x, z)) }
+export function minimap_seen_at(x: float, z: float) -> bool { return minimap_seen(minimap_cell(x, z)) }
-export function minimap_seen_count(minimap_st: MinimapState) -> int {
+export function minimap_seen_count() -> int {
var n = 0
- if minimap_st.mm_seen == null { return 0 }
- for c in 0 .. len(minimap_st.mm_seen) { if minimap_st.mm_seen[c] != 0 { n += 1 } }
+ if mm_seen == null { return 0 }
+ for c in 0 .. len(mm_seen) { if mm_seen[c] != 0 { n += 1 } }
return n
}
# goes up whenever what has been seen changes: a picture of the fog is baked again when it does
-export function minimap_version(minimap_st: MinimapState) -> int { return minimap_st.mm_version }
+export function minimap_version() -> int { return mm_version }
-export function minimap_explored_pct(minimap_st: mut MinimapState) -> int {
- mm_land_count(minimap_st)
+export function minimap_explored_pct() -> int {
+ mm_land_count()
var n = 0
- for j in minimap_st.mm_core0 .. minimap_st.mm_core0 + minimap_st.mm_core_n {
- for i in minimap_st.mm_core0 .. minimap_st.mm_core0 + minimap_st.mm_core_n {
- if minimap_seen(minimap_st, j * minimap_st.mm_cells + i) and mm_land(minimap_st, i, j) { n += 1 }
+ for j in mm_core0 .. mm_core0 + mm_core_n {
+ for i in mm_core0 .. mm_core0 + mm_core_n {
+ if minimap_seen(j * mm_cells + i) and mm_land(i, j) { n += 1 }
}
}
- return Math.min(n * 100 / minimap_st.mm_land_n, 100)
+ return Math.min(n * 100 / mm_land_n, 100)
}
-function mm_land(minimap_st: MinimapState, i: int, j: int) -> bool { return MinimapWorld.land(minimap_cell_x(minimap_st, i), minimap_cell_z(minimap_st, j)) }
+function mm_land(i: int, j: int) -> bool { return MinimapWorld.land(minimap_cell_x(i), minimap_cell_z(j)) }
-function mm_land_count(minimap_st: mut MinimapState) -> void {
+function mm_land_count() -> void {
let ox = MinimapWorld.origin_x()
let oz = MinimapWorld.origin_z()
- if minimap_st.mm_land_n > 0 and ox == minimap_st.mm_land_ox and oz == minimap_st.mm_land_oz { return }
- minimap_st.mm_land_ox = ox
- minimap_st.mm_land_oz = oz
- minimap_st.mm_land_n = 0
- for j in minimap_st.mm_core0 .. minimap_st.mm_core0 + minimap_st.mm_core_n {
- for i in minimap_st.mm_core0 .. minimap_st.mm_core0 + minimap_st.mm_core_n { if mm_land(minimap_st, i, j) { minimap_st.mm_land_n += 1 } }
+ if mm_land_n > 0 and ox == mm_land_ox and oz == mm_land_oz { return }
+ mm_land_ox = ox
+ mm_land_oz = oz
+ mm_land_n = 0
+ for j in mm_core0 .. mm_core0 + mm_core_n {
+ for i in mm_core0 .. mm_core0 + mm_core_n { if mm_land(i, j) { mm_land_n += 1 } }
}
- minimap_st.mm_land_n = Math.max(minimap_st.mm_land_n, 1)
+ mm_land_n = Math.max(mm_land_n, 1)
}
# the ground under the grid is not what it was (a new world): count its land again
-export function minimap_land_changed(minimap_st: mut MinimapState) -> void { minimap_st.mm_land_n = 0 }
+export function minimap_land_changed() -> void { mm_land_n = 0 }
# the grid as one string of 0 / 1, row by row, and back (a shorter string leaves the rest unseen)
-export function minimap_seen_text(minimap_st: mut MinimapState) -> string {
- mm_grid(minimap_st)
+export function minimap_seen_text() -> string {
+ mm_grid()
var s = ""
- for j in 0 .. minimap_st.mm_cells {
+ for j in 0 .. mm_cells {
var row = ""
- for i in 0 .. minimap_st.mm_cells { if minimap_st.mm_seen[j * minimap_st.mm_cells + i] != 0 { row = row + "1" } else { row = row + "0" } }
+ for i in 0 .. mm_cells { if mm_seen[j * mm_cells + i] != 0 { row = row + "1" } else { row = row + "0" } }
s = s + row
}
return s
}
-export function minimap_seen_from_text(minimap_st: mut MinimapState, s: string) -> void {
- mm_grid(minimap_st)
+export function minimap_seen_from_text(s: string) -> void {
+ mm_grid()
let n = len(s)
- for c in 0 .. minimap_st.mm_cells * minimap_st.mm_cells {
- minimap_st.mm_seen[c] = 0
- if c < n and s[c] == 49 { minimap_st.mm_seen[c] = 1 }
+ for c in 0 .. mm_cells * mm_cells {
+ mm_seen[c] = 0
+ if c < n and s[c] == 49 { mm_seen[c] = 1 }
}
- minimap_st.mm_version += 1
+ mm_version += 1
}
diff --git a/packages/ludic.minimap/grid.ludic b/packages/ludic.minimap/grid.ludic
index fb0689a0..5483dd60 100644
--- a/packages/ludic.minimap/grid.ludic
+++ b/packages/ludic.minimap/grid.ludic
@@ -1,76 +1,47 @@
# grid.ludic - the explored grid: square cells round the origin, and the part of it "explored" is
# measured over (a core, so a bigger grid does not change what a share of it means)
-export state MinimapState {
- mm_land_n: int = 0 # land cells in the core, counted once per origin
- mm_land_ox: float = 0.0
- mm_land_oz: float = 0.0
- mm_cells: int = 128 # per side
- mm_cell_m: float = 100.0 # metres per cell
- mm_core0: int = 0 # the measured square: from this cell, this many per side
- mm_core_n: int = 128
- mm_seen: []int = null # 0 / 1 per cell, row by row
- mm_version: int = 0 # goes up whenever a cell is revealed or the grid is replaced
- mm_pins: []MinimapPin = null
- mm_pin_max: int = 32
- mm_symbols: int = 8
- mm_colours: int = 5
- mm_open: int = -1
- mm_followed: int = -1
- mm_last: int = 0
- mm_placing: bool = false
- mm_x0: float = 0.0
- mm_y0: float = 0.0
- mm_size: float = 512.0
- mm_span: float = 6400.0
- mm_zoom_min: float = 0.5
- mm_zoom_max: float = 8.0
- mm_zoom_focus: float = 4.0
- mm_zoom: float = 1.0
- mm_vx: float = 0.0
- mm_vz: float = 0.0
- mm_have_view: bool = false
- mm_panning: bool = false
- mm_drag_rx: float = 0.0
- mm_drag_ry: float = 0.0
- mm_drag_vx: float = 0.0
- mm_drag_vz: float = 0.0
-}
+var mm_cells: int = 128 # per side
+var mm_cell_m: float = 100.0 # metres per cell
+var mm_core0: int = 0 # the measured square: from this cell, this many per side
+var mm_core_n: int = 128
+var mm_seen: []int = null # 0 / 1 per cell, row by row
+var mm_version: int = 0 # goes up whenever a cell is revealed or the grid is replaced
# the grid: `cells` per side of `cell_m` metres, centred on MinimapWorld's origin; the whole of it
# is measured until minimap_config_core says otherwise
-export function minimap_config(minimap_st: mut MinimapState, cells: int, cell_m: float) -> void {
- minimap_st.mm_cells = Math.max(cells, 1)
- minimap_st.mm_cell_m = Math.max(cell_m, 0.001)
- minimap_st.mm_core0 = 0
- minimap_st.mm_core_n = minimap_st.mm_cells
- minimap_st.mm_seen = new []int
- for i in 0 .. minimap_st.mm_cells * minimap_st.mm_cells { push(minimap_st.mm_seen, 0) }
- minimap_st.mm_land_n = 0
- minimap_st.mm_version += 1
+export function minimap_config(cells: int, cell_m: float) -> void {
+ mm_cells = Math.max(cells, 1)
+ mm_cell_m = Math.max(cell_m, 0.001)
+ mm_core0 = 0
+ mm_core_n = mm_cells
+ mm_seen = new []int
+ for i in 0 .. mm_cells * mm_cells { push(mm_seen, 0) }
+ mm_land_n = 0
+ mm_version += 1
}
-export function minimap_config_core(minimap_st: mut MinimapState, from: int, n: int) -> void {
- minimap_st.mm_core0 = Math.clamp(from, 0, minimap_st.mm_cells - 1)
- minimap_st.mm_core_n = Math.clamp(n, 1, minimap_st.mm_cells - minimap_st.mm_core0)
- minimap_st.mm_land_n = 0
+export function minimap_config_core(from: int, n: int) -> void {
+ mm_core0 = Math.clamp(from, 0, mm_cells - 1)
+ mm_core_n = Math.clamp(n, 1, mm_cells - mm_core0)
+ mm_land_n = 0
}
-export function minimap_cells(minimap_st: MinimapState) -> int { return minimap_st.mm_cells }
-export function minimap_cell_m(minimap_st: MinimapState) -> float { return minimap_st.mm_cell_m }
-export function minimap_grid_half(minimap_st: MinimapState) -> float { return float(minimap_st.mm_cells) * minimap_st.mm_cell_m * 0.5 }
+export function minimap_cells() -> int { return mm_cells }
+export function minimap_cell_m() -> float { return mm_cell_m }
+export function minimap_grid_half() -> float { return float(mm_cells) * mm_cell_m * 0.5 }
# the grid's west and north edges, and a column's or a row's
-export function minimap_grid_x0(minimap_st: MinimapState) -> float { return MinimapWorld.origin_x() - minimap_grid_half(minimap_st) }
-export function minimap_grid_z0(minimap_st: MinimapState) -> float { return MinimapWorld.origin_z() - minimap_grid_half(minimap_st) }
-export function minimap_cell_x(minimap_st: MinimapState, i: int) -> float { return minimap_grid_x0(minimap_st) + float(i) * minimap_st.mm_cell_m }
-export function minimap_cell_z(minimap_st: MinimapState, j: int) -> float { return minimap_grid_z0(minimap_st) + float(j) * minimap_st.mm_cell_m }
+export function minimap_grid_x0() -> float { return MinimapWorld.origin_x() - minimap_grid_half() }
+export function minimap_grid_z0() -> float { return MinimapWorld.origin_z() - minimap_grid_half() }
+export function minimap_cell_x(i: int) -> float { return minimap_grid_x0() + float(i) * mm_cell_m }
+export function minimap_cell_z(j: int) -> float { return minimap_grid_z0() + float(j) * mm_cell_m }
# the cell a point is in, -1 off the grid
-export function minimap_cell(minimap_st: MinimapState, x: float, z: float) -> int {
- let i = int(Math.floor((x - minimap_grid_x0(minimap_st)) / minimap_st.mm_cell_m))
- let j = int(Math.floor((z - minimap_grid_z0(minimap_st)) / minimap_st.mm_cell_m))
- if i < 0 or j < 0 or i >= minimap_st.mm_cells or j >= minimap_st.mm_cells { return -1 }
- return j * minimap_st.mm_cells + i
+export function minimap_cell(x: float, z: float) -> int {
+ let i = int(Math.floor((x - minimap_grid_x0()) / mm_cell_m))
+ let j = int(Math.floor((z - minimap_grid_z0()) / mm_cell_m))
+ if i < 0 or j < 0 or i >= mm_cells or j >= mm_cells { return -1 }
+ return j * mm_cells + i
}
-function mm_grid(minimap_st: mut MinimapState) -> void { if minimap_st.mm_seen == null { minimap_config(minimap_st, minimap_st.mm_cells, minimap_st.mm_cell_m) } }
+function mm_grid() -> void { if mm_seen == null { minimap_config(mm_cells, mm_cell_m) } }
diff --git a/packages/ludic.minimap/pin_edit.ludic b/packages/ludic.minimap/pin_edit.ludic
index e012faeb..12f4c715 100644
--- a/packages/ludic.minimap/pin_edit.ludic
+++ b/packages/ludic.minimap/pin_edit.ludic
@@ -1,68 +1,68 @@
# pin_edit.ludic - changing a mark: its name, symbol and colour, rubbing it out, which one is open
# and which followed, and whether the map is waiting for a place to drop the next
-export function minimap_pin_remove(minimap_st: mut MinimapState, i: int) -> void {
- if i < 0 or i >= minimap_pin_count(minimap_st) { return }
+export function minimap_pin_remove(i: int) -> void {
+ if i < 0 or i >= minimap_pin_count() { return }
let rest = new []MinimapPin
- for k in 0 .. len(minimap_st.mm_pins) { if k != i { push(rest, minimap_st.mm_pins[k]) } }
- minimap_st.mm_pins = rest
- if minimap_st.mm_open == i { minimap_st.mm_open = -1 } else if minimap_st.mm_open > i { minimap_st.mm_open -= 1 }
- if minimap_st.mm_followed == i { minimap_st.mm_followed = -1 } else if minimap_st.mm_followed > i { minimap_st.mm_followed -= 1 }
+ for k in 0 .. len(mm_pins) { if k != i { push(rest, mm_pins[k]) } }
+ mm_pins = rest
+ if mm_open == i { mm_open = -1 } else if mm_open > i { mm_open -= 1 }
+ if mm_followed == i { mm_followed = -1 } else if mm_followed > i { mm_followed -= 1 }
}
-export function minimap_pin_set_label(minimap_st: mut MinimapState, i: int, s: string) -> void { if i >= 0 and i < minimap_pin_count(minimap_st) { minimap_st.mm_pins[i].label = s } }
+export function minimap_pin_set_label(i: int, s: string) -> void { if i >= 0 and i < minimap_pin_count() { mm_pins[i].label = s } }
# the symbol chosen is the one the next mark takes
-export function minimap_pin_set_icon(minimap_st: mut MinimapState, i: int, k: int) -> void {
- if i < 0 or i >= minimap_pin_count(minimap_st) { return }
- minimap_st.mm_pins[i].icon = Math.clamp(k, 0, minimap_st.mm_symbols - 1)
- minimap_st.mm_last = minimap_st.mm_pins[i].icon
+export function minimap_pin_set_icon(i: int, k: int) -> void {
+ if i < 0 or i >= minimap_pin_count() { return }
+ mm_pins[i].icon = Math.clamp(k, 0, mm_symbols - 1)
+ mm_last = mm_pins[i].icon
}
-export function minimap_pin_set_colour(minimap_st: mut MinimapState, i: int, k: int) -> void { if i >= 0 and i < minimap_pin_count(minimap_st) { minimap_st.mm_pins[i].colour = Math.clamp(k, 0, minimap_st.mm_colours - 1) } }
+export function minimap_pin_set_colour(i: int, k: int) -> void { if i >= 0 and i < minimap_pin_count() { mm_pins[i].colour = Math.clamp(k, 0, mm_colours - 1) } }
-export function minimap_pin_last_icon(minimap_st: MinimapState) -> int { return minimap_st.mm_last }
+export function minimap_pin_last_icon() -> int { return mm_last }
# the mark the map's legend has open, -1 none
-export function minimap_pin_open(minimap_st: mut MinimapState) -> int {
- if minimap_st.mm_open >= minimap_pin_count(minimap_st) { minimap_st.mm_open = -1 }
- return minimap_st.mm_open
+export function minimap_pin_open() -> int {
+ if mm_open >= minimap_pin_count() { mm_open = -1 }
+ return mm_open
}
-export function minimap_pin_select(minimap_st: mut MinimapState, i: int) -> void {
- minimap_st.mm_open = -1
- if i >= 0 and i < minimap_pin_count(minimap_st) { minimap_st.mm_open = i }
+export function minimap_pin_select(i: int) -> void {
+ mm_open = -1
+ if i >= 0 and i < minimap_pin_count() { mm_open = i }
}
# the mark the compass carries, -1 none
-export function minimap_pin_followed(minimap_st: mut MinimapState) -> int {
- if minimap_st.mm_followed >= minimap_pin_count(minimap_st) { minimap_st.mm_followed = -1 }
- return minimap_st.mm_followed
+export function minimap_pin_followed() -> int {
+ if mm_followed >= minimap_pin_count() { mm_followed = -1 }
+ return mm_followed
}
-export function minimap_pin_follow(minimap_st: mut MinimapState, i: int) -> void {
- minimap_st.mm_followed = -1
- if i >= 0 and i < minimap_pin_count(minimap_st) { minimap_st.mm_followed = i }
+export function minimap_pin_follow(i: int) -> void {
+ mm_followed = -1
+ if i >= 0 and i < minimap_pin_count() { mm_followed = i }
}
# follow the open mark, or stop following it
-export function minimap_pin_follow_toggle(minimap_st: mut MinimapState) -> void {
- let i = minimap_pin_open(minimap_st)
+export function minimap_pin_follow_toggle() -> void {
+ let i = minimap_pin_open()
if i < 0 { return }
- if minimap_st.mm_followed == i { minimap_st.mm_followed = -1 } else { minimap_st.mm_followed = i }
+ if mm_followed == i { mm_followed = -1 } else { mm_followed = i }
}
-export function minimap_placing(minimap_st: MinimapState) -> bool { return minimap_st.mm_placing }
+export function minimap_placing() -> bool { return mm_placing }
# the next press on the map drops a mark; asked again, it does not
-export function minimap_place_toggle(minimap_st: mut MinimapState) -> void {
- minimap_st.mm_placing = not minimap_st.mm_placing
- minimap_st.mm_open = -1
+export function minimap_place_toggle() -> void {
+ mm_placing = not mm_placing
+ mm_open = -1
}
# the press that drops it, at a point on the window: the new mark is open, -1 if there was no room
-export function minimap_place(minimap_st: mut MinimapState, rx: float, ry: float, label: string) -> int {
- minimap_st.mm_placing = false
- let i = minimap_pin_add(minimap_st, minimap_wx(minimap_st, rx), minimap_wz(minimap_st, ry), minimap_st.mm_last, label)
- if i >= 0 { minimap_st.mm_open = i }
+export function minimap_place(rx: float, ry: float, label: string) -> int {
+ mm_placing = false
+ let i = minimap_pin_add(minimap_wx(rx), minimap_wz(ry), mm_last, label)
+ if i >= 0 { mm_open = i }
return i
}
diff --git a/packages/ludic.minimap/pins.ludic b/packages/ludic.minimap/pins.ludic
index c6994dea..f187da77 100644
--- a/packages/ludic.minimap/pins.ludic
+++ b/packages/ludic.minimap/pins.ludic
@@ -10,75 +10,83 @@ export property MinimapPin {
label: string = ""
}
+var mm_pins: []MinimapPin = null
+var mm_pin_max: int = 32
+var mm_symbols: int = 8
+var mm_colours: int = 5
+var mm_open: int = -1
+var mm_followed: int = -1
+var mm_last: int = 0
+var mm_placing: bool = false
-export function minimap_config_pins(minimap_st: mut MinimapState, max: int, symbols: int, colours: int) -> void {
- minimap_st.mm_pin_max = Math.max(max, 1)
- minimap_st.mm_symbols = Math.max(symbols, 1)
- minimap_st.mm_colours = Math.max(colours, 1)
- minimap_pins_clear(minimap_st)
+export function minimap_config_pins(max: int, symbols: int, colours: int) -> void {
+ mm_pin_max = Math.max(max, 1)
+ mm_symbols = Math.max(symbols, 1)
+ mm_colours = Math.max(colours, 1)
+ minimap_pins_clear()
}
-export function minimap_pins_clear(minimap_st: mut MinimapState) -> void {
- minimap_st.mm_pins = new []MinimapPin
- minimap_st.mm_open = -1
- minimap_st.mm_followed = -1
- minimap_st.mm_last = 0
- minimap_st.mm_placing = false
+export function minimap_pins_clear() -> void {
+ mm_pins = new []MinimapPin
+ mm_open = -1
+ mm_followed = -1
+ mm_last = 0
+ mm_placing = false
}
-export function minimap_pin_count(minimap_st: MinimapState) -> int {
- if minimap_st.mm_pins == null { return 0 }
- return len(minimap_st.mm_pins)
+export function minimap_pin_count() -> int {
+ if mm_pins == null { return 0 }
+ return len(mm_pins)
}
-export function minimap_pin_max(minimap_st: MinimapState) -> int { return minimap_st.mm_pin_max }
-export function minimap_pin_full(minimap_st: MinimapState) -> bool { return minimap_pin_count(minimap_st) >= minimap_st.mm_pin_max }
+export function minimap_pin_max() -> int { return mm_pin_max }
+export function minimap_pin_full() -> bool { return minimap_pin_count() >= mm_pin_max }
# a mark at (x, z) made today, with the colour 0; -1 when the map holds all it will
-export function minimap_pin_add(minimap_st: mut MinimapState, x: float, z: float, icon: int, label: string) -> int {
- if minimap_st.mm_pins == null { minimap_pins_clear(minimap_st) }
- if minimap_pin_full(minimap_st) { return -1 }
+export function minimap_pin_add(x: float, z: float, icon: int, label: string) -> int {
+ if mm_pins == null { minimap_pins_clear() }
+ if minimap_pin_full() { return -1 }
let p = new MinimapPin
p.x = x
p.z = z
- p.icon = Math.clamp(icon, 0, minimap_st.mm_symbols - 1)
+ p.icon = Math.clamp(icon, 0, mm_symbols - 1)
p.day = MinimapWorld.day()
p.label = label
- push(minimap_st.mm_pins, p)
- minimap_st.mm_last = p.icon
- return len(minimap_st.mm_pins) - 1
+ push(mm_pins, p)
+ mm_last = p.icon
+ return len(mm_pins) - 1
}
# as saved: everything given, the symbol last chosen left alone
-export function minimap_pin_put(minimap_st: mut MinimapState, x: float, z: float, icon: int, colour: int, day: int, label: string) -> int {
- if minimap_st.mm_pins == null { minimap_pins_clear(minimap_st) }
- if minimap_pin_full(minimap_st) { return -1 }
+export function minimap_pin_put(x: float, z: float, icon: int, colour: int, day: int, label: string) -> int {
+ if mm_pins == null { minimap_pins_clear() }
+ if minimap_pin_full() { return -1 }
let p = new MinimapPin
p.x = x
p.z = z
- p.icon = Math.clamp(icon, 0, minimap_st.mm_symbols - 1)
- p.colour = Math.clamp(colour, 0, minimap_st.mm_colours - 1)
+ p.icon = Math.clamp(icon, 0, mm_symbols - 1)
+ p.colour = Math.clamp(colour, 0, mm_colours - 1)
p.day = day
p.label = label
- push(minimap_st.mm_pins, p)
- return len(minimap_st.mm_pins) - 1
+ push(mm_pins, p)
+ return len(mm_pins) - 1
}
# the i-th mark, or an empty one past the end
-export function minimap_pin(minimap_st: MinimapState, i: int) -> MinimapPin {
- if i < 0 or i >= minimap_pin_count(minimap_st) { return new MinimapPin }
- return minimap_st.mm_pins[i]
+export function minimap_pin(i: int) -> MinimapPin {
+ if i < 0 or i >= minimap_pin_count() { return new MinimapPin }
+ return mm_pins[i]
}
# the nearest n marks within `within` of (x, z), nearest first, leaving out the followed one
-export function minimap_pins_near(minimap_st: MinimapState, x: float, z: float, n: int, within: float) -> []int {
+export function minimap_pins_near(x: float, z: float, n: int, within: float) -> []int {
let out = new []int
for k in 0 .. n {
var best = -1
var bd = within * within
- for i in 0 .. minimap_pin_count(minimap_st) {
- if i == minimap_st.mm_followed or mm_has(out, i) { continue }
- let d = (minimap_st.mm_pins[i].x - x) * (minimap_st.mm_pins[i].x - x) + (minimap_st.mm_pins[i].z - z) * (minimap_st.mm_pins[i].z - z)
+ for i in 0 .. minimap_pin_count() {
+ if i == mm_followed or mm_has(out, i) { continue }
+ let d = (mm_pins[i].x - x) * (mm_pins[i].x - x) + (mm_pins[i].z - z) * (mm_pins[i].z - z)
if d < bd {
bd = d
best = i
diff --git a/packages/ludic.minimap/system.ludic b/packages/ludic.minimap/system.ludic
index 4c542a3e..ff87652b 100644
--- a/packages/ludic.minimap/system.ludic
+++ b/packages/ludic.minimap/system.ludic
@@ -1,20 +1,20 @@
# system.ludic - the map as a system: what has been seen and the marks, in its own save section by
# key. The view is where the player left it this session and is not saved.
-export function minimap_reset(minimap_st: mut MinimapState) -> void {
- mm_grid(minimap_st)
- for c in 0 .. len(minimap_st.mm_seen) { minimap_st.mm_seen[c] = 0 }
- minimap_st.mm_version += 1
- minimap_pins_clear(minimap_st)
- minimap_st.mm_have_view = false
- minimap_st.mm_panning = false
+export function minimap_reset() -> void {
+ mm_grid()
+ for c in 0 .. len(mm_seen) { mm_seen[c] = 0 }
+ mm_version += 1
+ minimap_pins_clear()
+ mm_have_view = false
+ mm_panning = false
}
-export function minimap_save(minimap_st: mut MinimapState) -> Val {
+export function minimap_save() -> Val {
let v = Value.object()
- sv_put_str(v, "seen", minimap_seen_text(minimap_st))
+ sv_put_str(v, "seen", minimap_seen_text())
let l = Value.list()
- for i in 0 .. minimap_pin_count(minimap_st) {
- let p = minimap_st.mm_pins[i]
+ for i in 0 .. minimap_pin_count() {
+ let p = mm_pins[i]
let o = Value.object()
sv_put_float(o, "x", p.x)
sv_put_float(o, "z", p.z)
@@ -25,23 +25,23 @@ export function minimap_save(minimap_st: mut MinimapState) -> Val {
Value.add(l, o)
}
Value.put(v, "pins", l)
- sv_put_int(v, "followed", minimap_pin_followed(minimap_st))
- sv_put_int(v, "last", minimap_st.mm_last)
+ sv_put_int(v, "followed", minimap_pin_followed())
+ sv_put_int(v, "last", mm_last)
return v
}
-export function minimap_load(minimap_st: mut MinimapState, v: Val, version: int) -> void {
- minimap_reset(minimap_st)
- minimap_seen_from_text(minimap_st, sv_str(v, "seen", ""))
+export function minimap_load(v: Val, version: int) -> void {
+ minimap_reset()
+ minimap_seen_from_text(sv_str(v, "seen", ""))
if Value.has(v, "pins") != 0 {
let l = Value.get(v, "pins")
for i in 0 .. Value.count(l) {
let o = Value.at(l, i)
- minimap_pin_put(minimap_st, sv_float(o, "x", 0.0), sv_float(o, "z", 0.0), sv_int(o, "icon", 0), sv_int(o, "colour", 0), sv_int(o, "day", 1), sv_str(o, "label", ""))
+ minimap_pin_put(sv_float(o, "x", 0.0), sv_float(o, "z", 0.0), sv_int(o, "icon", 0), sv_int(o, "colour", 0), sv_int(o, "day", 1), sv_str(o, "label", ""))
}
}
- minimap_pin_follow(minimap_st, sv_int(v, "followed", -1))
- minimap_st.mm_last = Math.clamp(sv_int(v, "last", 0), 0, minimap_st.mm_symbols - 1)
+ minimap_pin_follow(sv_int(v, "followed", -1))
+ mm_last = Math.clamp(sv_int(v, "last", 0), 0, mm_symbols - 1)
}
export function minimap_system() -> System {
diff --git a/packages/ludic.minimap/tests/minimap_test.ludic b/packages/ludic.minimap/tests/minimap_test.ludic
index f54ba6ba..9040ccba 100644
--- a/packages/ludic.minimap/tests/minimap_test.ludic
+++ b/packages/ludic.minimap/tests/minimap_test.ludic
@@ -6,189 +6,187 @@ import "ludic.base"
program MinimapTest {
numbers float
- state MinimaptestState {
- you_x: float = 100.0
- you_z: float = 100.0
- day: int = 3
- }
+ var you_x: float = 100.0
+ var you_z: float = 100.0
+ var day: int = 3
function fk_ox() -> float { return 100.0 }
function fk_land(x: float, z: float) -> bool { return x >= 50.0 }
- function fk_you_x(minimaptest_st: MinimaptestState) -> float { return minimaptest_st.you_x }
- function fk_you_z(minimaptest_st: MinimaptestState) -> float { return minimaptest_st.you_z }
+ function fk_you_x() -> float { return you_x }
+ function fk_you_z() -> float { return you_z }
function fk_camp() -> float { return 120.0 }
- function fk_day(minimaptest_st: MinimaptestState) -> int { return minimaptest_st.day }
+ function fk_day() -> int { return day }
function fk_lo() -> float { return 0.0 }
function fk_hi() -> float { return 200.0 }
bind MinimapWorld { origin_x: fn fk_ox, origin_z: fn fk_ox, land: fn fk_land, you_x: fn fk_you_x, you_z: fn fk_you_z, camp_x: fn fk_camp, camp_z: fn fk_camp, day: fn fk_day, lo_x: fn fk_lo, lo_z: fn fk_lo, hi_x: fn fk_hi, hi_z: fn fk_hi }
- function fresh(minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) -> void {
- minimap_config(minimap_st, 20, 10.0)
- minimap_config_pins(minimap_st, 3, 8, 5)
- minimap_config_view(minimap_st, 200.0, 0.5, 8.0, 4.0)
- minimap_window(minimap_st, 0.0, 0.0, 400.0)
- minimap_reset(minimap_st)
- minimaptest_st.you_x = 100.0
- minimaptest_st.you_z = 100.0
+ function fresh() -> void {
+ minimap_config(20, 10.0)
+ minimap_config_pins(3, 8, 5)
+ minimap_config_view(200.0, 0.5, 8.0, 4.0)
+ minimap_window(0.0, 0.0, 400.0)
+ minimap_reset()
+ you_x = 100.0
+ you_z = 100.0
}
- test "the grid sits round its origin, and a point has its cell" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- expect_near(minimap_grid_x0(minimap_st), 0.0, 0.001)
- expect_eq(minimap_cell(minimap_st, 5.0, 5.0), 0)
- expect_eq(minimap_cell(minimap_st, 15.0, 25.0), 41)
- expect_eq(minimap_cell(minimap_st, -1.0, 5.0), -1)
- expect_eq(minimap_cell(minimap_st, 200.0, 5.0), -1)
+ test "the grid sits round its origin, and a point has its cell" {
+ fresh()
+ expect_near(minimap_grid_x0(), 0.0, 0.001)
+ expect_eq(minimap_cell(5.0, 5.0), 0)
+ expect_eq(minimap_cell(15.0, 25.0), 41)
+ expect_eq(minimap_cell(-1.0, 5.0), -1)
+ expect_eq(minimap_cell(200.0, 5.0), -1)
}
- test "walking reveals the cells within reach, and explored counts the land only" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- let v0 = minimap_version(minimap_st)
- minimap_reveal(minimap_st, 105.0, 105.0, 10.0)
- expect(minimap_seen_at(minimap_st, 105.0, 105.0))
- expect(minimap_seen_at(minimap_st, 95.0, 105.0))
- expect(not minimap_seen_at(minimap_st, 125.0, 105.0))
- expect_eq(minimap_seen_count(minimap_st), 9)
- expect(minimap_version(minimap_st) > v0)
- let v1 = minimap_version(minimap_st)
- minimap_reveal(minimap_st, 105.0, 105.0, 10.0)
- expect_eq(minimap_version(minimap_st), v1)
- expect_eq(minimap_explored_pct(minimap_st), 3)
- minimap_reveal(minimap_st, 5.0, 5.0, 0.0)
- expect_eq(minimap_explored_pct(minimap_st), 3)
- minimap_config_core(minimap_st, 5, 10)
- minimap_land_changed(minimap_st)
- expect_eq(minimap_explored_pct(minimap_st), 9)
+ test "walking reveals the cells within reach, and explored counts the land only" {
+ fresh()
+ let v0 = minimap_version()
+ minimap_reveal(105.0, 105.0, 10.0)
+ expect(minimap_seen_at(105.0, 105.0))
+ expect(minimap_seen_at(95.0, 105.0))
+ expect(not minimap_seen_at(125.0, 105.0))
+ expect_eq(minimap_seen_count(), 9)
+ expect(minimap_version() > v0)
+ let v1 = minimap_version()
+ minimap_reveal(105.0, 105.0, 10.0)
+ expect_eq(minimap_version(), v1)
+ expect_eq(minimap_explored_pct(), 3)
+ minimap_reveal(5.0, 5.0, 0.0)
+ expect_eq(minimap_explored_pct(), 3)
+ minimap_config_core(5, 10)
+ minimap_land_changed()
+ expect_eq(minimap_explored_pct(), 9)
}
- test "a mark is made today with the last symbol, and the map holds only so many" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- expect_eq(minimap_pin_add(minimap_st, 10.0, 20.0, 2, "pool"), 0)
- expect_eq(minimap_pin(minimap_st, 0).day, 3)
- expect_eq(minimap_pin_last_icon(minimap_st), 2)
- expect_eq(minimap_pin_add(minimap_st, 30.0, 20.0, 99, "rock"), 1)
- expect_eq(minimap_pin(minimap_st, 1).icon, 7)
- expect_eq(minimap_pin_add(minimap_st, 50.0, 20.0, 0, "bend"), 2)
- expect(minimap_pin_full(minimap_st))
- expect_eq(minimap_pin_add(minimap_st, 60.0, 20.0, 0, "one more"), -1)
- expect_eq(minimap_pin_count(minimap_st), 3)
+ test "a mark is made today with the last symbol, and the map holds only so many" {
+ fresh()
+ expect_eq(minimap_pin_add(10.0, 20.0, 2, "pool"), 0)
+ expect_eq(minimap_pin(0).day, 3)
+ expect_eq(minimap_pin_last_icon(), 2)
+ expect_eq(minimap_pin_add(30.0, 20.0, 99, "rock"), 1)
+ expect_eq(minimap_pin(1).icon, 7)
+ expect_eq(minimap_pin_add(50.0, 20.0, 0, "bend"), 2)
+ expect(minimap_pin_full())
+ expect_eq(minimap_pin_add(60.0, 20.0, 0, "one more"), -1)
+ expect_eq(minimap_pin_count(), 3)
}
- test "the open mark is named, coloured and followed, and rubbing one out moves the others up" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- minimap_pin_add(minimap_st, 10.0, 20.0, 0, "a")
- minimap_pin_add(minimap_st, 30.0, 20.0, 0, "b")
- minimap_pin_add(minimap_st, 50.0, 20.0, 0, "c")
- minimap_pin_select(minimap_st, 2)
- minimap_pin_follow_toggle(minimap_st)
- expect_eq(minimap_pin_followed(minimap_st), 2)
- minimap_pin_set_label(minimap_st, 2, "deer pool")
- minimap_pin_set_colour(minimap_st, 2, 9)
- minimap_pin_set_icon(minimap_st, 2, 3)
- expect(minimap_pin(minimap_st, 2).label == "deer pool")
- expect_eq(minimap_pin(minimap_st, 2).colour, 4)
- expect_eq(minimap_pin_last_icon(minimap_st), 3)
- minimap_pin_remove(minimap_st, 0)
- expect_eq(minimap_pin_count(minimap_st), 2)
- expect_eq(minimap_pin_open(minimap_st), 1)
- expect_eq(minimap_pin_followed(minimap_st), 1)
- minimap_pin_remove(minimap_st, 1)
- expect_eq(minimap_pin_open(minimap_st), -1)
- expect_eq(minimap_pin_followed(minimap_st), -1)
+ test "the open mark is named, coloured and followed, and rubbing one out moves the others up" {
+ fresh()
+ minimap_pin_add(10.0, 20.0, 0, "a")
+ minimap_pin_add(30.0, 20.0, 0, "b")
+ minimap_pin_add(50.0, 20.0, 0, "c")
+ minimap_pin_select(2)
+ minimap_pin_follow_toggle()
+ expect_eq(minimap_pin_followed(), 2)
+ minimap_pin_set_label(2, "deer pool")
+ minimap_pin_set_colour(2, 9)
+ minimap_pin_set_icon(2, 3)
+ expect(minimap_pin(2).label == "deer pool")
+ expect_eq(minimap_pin(2).colour, 4)
+ expect_eq(minimap_pin_last_icon(), 3)
+ minimap_pin_remove(0)
+ expect_eq(minimap_pin_count(), 2)
+ expect_eq(minimap_pin_open(), 1)
+ expect_eq(minimap_pin_followed(), 1)
+ minimap_pin_remove(1)
+ expect_eq(minimap_pin_open(), -1)
+ expect_eq(minimap_pin_followed(), -1)
}
- test "the nearest marks for a compass leave out the followed one and anything too far" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- minimap_config_pins(minimap_st, 8, 8, 5)
- minimap_pin_add(minimap_st, 110.0, 100.0, 0, "near")
- minimap_pin_add(minimap_st, 130.0, 100.0, 0, "next")
- minimap_pin_add(minimap_st, 105.0, 100.0, 0, "followed")
- minimap_pin_add(minimap_st, 900.0, 100.0, 0, "far")
- minimap_pin_follow(minimap_st, 2)
- let near = minimap_pins_near(minimap_st, 100.0, 100.0, 3, 400.0)
+ test "the nearest marks for a compass leave out the followed one and anything too far" {
+ fresh()
+ minimap_config_pins(8, 8, 5)
+ minimap_pin_add(110.0, 100.0, 0, "near")
+ minimap_pin_add(130.0, 100.0, 0, "next")
+ minimap_pin_add(105.0, 100.0, 0, "followed")
+ minimap_pin_add(900.0, 100.0, 0, "far")
+ minimap_pin_follow(2)
+ let near = minimap_pins_near(100.0, 100.0, 3, 400.0)
expect_eq(len(near), 2)
expect_eq(near[0], 0)
expect_eq(near[1], 1)
}
- test "the view: the world on the window and back, zoom about a point, a pan, focus and the clamp" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- minimap_centre(minimap_st, 100.0, 100.0)
- expect_near(minimap_px(minimap_st, 100.0), 200.0, 0.01)
- expect_near(minimap_px(minimap_st, 150.0), 300.0, 0.01)
- expect_near(minimap_wx(minimap_st, 300.0), 150.0, 0.01)
- minimap_zoom_about(minimap_st, 300.0, 200.0, 2.0)
- expect_near(minimap_zoom(minimap_st), 2.0, 0.001)
- expect_near(minimap_wx(minimap_st, 300.0), 150.0, 0.01)
- expect_near(minimap_span(minimap_st), 100.0, 0.01)
- minimap_pan_begin(minimap_st, 200.0, 200.0)
- minimap_pan_to(minimap_st, 160.0, 200.0)
- expect_near(minimap_view_x(minimap_st), 135.0, 0.01)
- minimap_pan_end(minimap_st)
- minimap_pan_to(minimap_st, 0.0, 0.0)
- expect_near(minimap_view_x(minimap_st), 135.0, 0.01)
- minimap_centre(minimap_st, 900.0, -5.0)
- expect_near(minimap_view_x(minimap_st), 200.0, 0.01)
- expect_near(minimap_view_z(minimap_st), 0.0, 0.01)
- minimap_focus(minimap_st, false)
- expect_near(minimap_view_x(minimap_st), 120.0, 0.01)
- expect_near(minimap_zoom(minimap_st), 4.0, 0.001)
- minimap_zoom_set(minimap_st, 6.0)
- minimaptest_st.you_x = 80.0
- minimap_focus(minimap_st, true)
- expect_near(minimap_view_x(minimap_st), 80.0, 0.01)
- expect_near(minimap_zoom(minimap_st), 6.0, 0.001)
- minimap_zoom_step(minimap_st, 1)
- expect_near(minimap_zoom(minimap_st), 7.8, 0.001)
- minimap_zoom_step(minimap_st, 1)
- expect_near(minimap_zoom(minimap_st), 8.0, 0.001)
+ test "the view: the world on the window and back, zoom about a point, a pan, focus and the clamp" {
+ fresh()
+ minimap_centre(100.0, 100.0)
+ expect_near(minimap_px(100.0), 200.0, 0.01)
+ expect_near(minimap_px(150.0), 300.0, 0.01)
+ expect_near(minimap_wx(300.0), 150.0, 0.01)
+ minimap_zoom_about(300.0, 200.0, 2.0)
+ expect_near(minimap_zoom(), 2.0, 0.001)
+ expect_near(minimap_wx(300.0), 150.0, 0.01)
+ expect_near(minimap_span(), 100.0, 0.01)
+ minimap_pan_begin(200.0, 200.0)
+ minimap_pan_to(160.0, 200.0)
+ expect_near(minimap_view_x(), 135.0, 0.01)
+ minimap_pan_end()
+ minimap_pan_to(0.0, 0.0)
+ expect_near(minimap_view_x(), 135.0, 0.01)
+ minimap_centre(900.0, -5.0)
+ expect_near(minimap_view_x(), 200.0, 0.01)
+ expect_near(minimap_view_z(), 0.0, 0.01)
+ minimap_focus(false)
+ expect_near(minimap_view_x(), 120.0, 0.01)
+ expect_near(minimap_zoom(), 4.0, 0.001)
+ minimap_zoom_set(6.0)
+ you_x = 80.0
+ minimap_focus(true)
+ expect_near(minimap_view_x(), 80.0, 0.01)
+ expect_near(minimap_zoom(), 6.0, 0.001)
+ minimap_zoom_step(1)
+ expect_near(minimap_zoom(), 7.8, 0.001)
+ minimap_zoom_step(1)
+ expect_near(minimap_zoom(), 8.0, 0.001)
}
- test "a press picks the mark under it, and placing drops one where the press lands" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- minimap_centre(minimap_st, 100.0, 100.0)
- minimap_pin_add(minimap_st, 150.0, 100.0, 4, "here")
- expect_eq(minimap_pick(minimap_st, 301.0, 199.0, 18.0), 0)
- expect_eq(minimap_pin_open(minimap_st), 0)
- expect_eq(minimap_pick(minimap_st, 10.0, 10.0, 18.0), -1)
- minimap_place_toggle(minimap_st)
- expect(minimap_placing(minimap_st))
- let i = minimap_place(minimap_st, 100.0, 200.0, "Day 3")
+ test "a press picks the mark under it, and placing drops one where the press lands" {
+ fresh()
+ minimap_centre(100.0, 100.0)
+ minimap_pin_add(150.0, 100.0, 4, "here")
+ expect_eq(minimap_pick(301.0, 199.0, 18.0), 0)
+ expect_eq(minimap_pin_open(), 0)
+ expect_eq(minimap_pick(10.0, 10.0, 18.0), -1)
+ minimap_place_toggle()
+ expect(minimap_placing())
+ let i = minimap_place(100.0, 200.0, "Day 3")
expect_eq(i, 1)
- expect(not minimap_placing(minimap_st))
- expect_near(minimap_pin(minimap_st, 1).x, 50.0, 0.01)
- expect_eq(minimap_pin(minimap_st, 1).icon, 4)
- expect_eq(minimap_pin_open(minimap_st), 1)
+ expect(not minimap_placing())
+ expect_near(minimap_pin(1).x, 50.0, 0.01)
+ expect_eq(minimap_pin(1).icon, 4)
+ expect_eq(minimap_pin_open(), 1)
}
- test "a scale bar is a round distance no wider than a quarter of the window" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- expect_eq(minimap_scale_metres(minimap_st), 50)
- minimap_zoom_set(minimap_st, 8.0)
- expect_eq(minimap_scale_metres(minimap_st), 25)
+ test "a scale bar is a round distance no wider than a quarter of the window" {
+ fresh()
+ expect_eq(minimap_scale_metres(), 50)
+ minimap_zoom_set(8.0)
+ expect_eq(minimap_scale_metres(), 25)
}
- test "what was seen and the marks come back from the save, and a missing section is a reset" (minimap_st: mut MinimapState, minimaptest_st: mut MinimaptestState) {
- fresh(minimap_st, minimaptest_st)
- minimap_reveal(minimap_st, 105.0, 105.0, 10.0)
- minimap_pin_add(minimap_st, 10.0, 20.0, 2, "pool")
- minimap_pin_add(minimap_st, 30.5, 20.25, 5, "rock")
- minimap_pin_set_colour(minimap_st, 1, 3)
- minimap_pin_follow(minimap_st, 1)
- let v = minimap_save(minimap_st)
- minimap_reset(minimap_st)
- expect_eq(minimap_seen_count(minimap_st), 0)
- minimap_load(minimap_st, v, 1)
- expect_eq(minimap_seen_count(minimap_st), 9)
- expect_eq(minimap_pin_count(minimap_st), 2)
- expect(minimap_pin(minimap_st, 1).label == "rock")
- expect_near(minimap_pin(minimap_st, 1).x, 30.5, 0.001)
- expect_eq(minimap_pin(minimap_st, 1).colour, 3)
- expect_eq(minimap_pin_followed(minimap_st), 1)
- expect_eq(minimap_pin_last_icon(minimap_st), 5)
- minimap_seen_from_text(minimap_st, "0011")
- expect_eq(minimap_seen_count(minimap_st), 2)
- let t = minimap_seen_text(minimap_st)
+ test "what was seen and the marks come back from the save, and a missing section is a reset" {
+ fresh()
+ minimap_reveal(105.0, 105.0, 10.0)
+ minimap_pin_add(10.0, 20.0, 2, "pool")
+ minimap_pin_add(30.5, 20.25, 5, "rock")
+ minimap_pin_set_colour(1, 3)
+ minimap_pin_follow(1)
+ let v = minimap_save()
+ minimap_reset()
+ expect_eq(minimap_seen_count(), 0)
+ minimap_load(v, 1)
+ expect_eq(minimap_seen_count(), 9)
+ expect_eq(minimap_pin_count(), 2)
+ expect(minimap_pin(1).label == "rock")
+ expect_near(minimap_pin(1).x, 30.5, 0.001)
+ expect_eq(minimap_pin(1).colour, 3)
+ expect_eq(minimap_pin_followed(), 1)
+ expect_eq(minimap_pin_last_icon(), 5)
+ minimap_seen_from_text("0011")
+ expect_eq(minimap_seen_count(), 2)
+ let t = minimap_seen_text()
expect_eq(len(t), 400)
expect(t[0..4] == "0011")
}
diff --git a/packages/ludic.minimap/view.ludic b/packages/ludic.minimap/view.ludic
index 7651a401..69918483 100644
--- a/packages/ludic.minimap/view.ludic
+++ b/packages/ludic.minimap/view.ludic
@@ -1,70 +1,81 @@
# view.ludic - the window onto the map: a square of `size` pixels at (x0, y0) showing `span` metres
# across at zoom 1, centred on a world point. World to window and back, and a scale bar's length.
+var mm_x0: float = 0.0
+var mm_y0: float = 0.0
+var mm_size: float = 512.0
+var mm_span: float = 6400.0
+var mm_zoom_min: float = 0.5
+var mm_zoom_max: float = 8.0
+var mm_zoom_focus: float = 4.0
+var mm_zoom: float = 1.0
+var mm_vx: float = 0.0
+var mm_vz: float = 0.0
+var mm_have_view: bool = false
# metres across the window at zoom 1, the zoom's range, and how close a focus goes at least
-export function minimap_config_view(minimap_st: mut MinimapState, span: float, zoom_min: float, zoom_max: float, focus: float) -> void {
- minimap_st.mm_span = Math.max(span, 1.0)
- minimap_st.mm_zoom_min = zoom_min
- minimap_st.mm_zoom_max = Math.max(zoom_max, zoom_min)
- minimap_st.mm_zoom_focus = focus
- minimap_st.mm_zoom = Math.clamp(1.0, minimap_st.mm_zoom_min, minimap_st.mm_zoom_max)
- minimap_st.mm_have_view = false
+export function minimap_config_view(span: float, zoom_min: float, zoom_max: float, focus: float) -> void {
+ mm_span = Math.max(span, 1.0)
+ mm_zoom_min = zoom_min
+ mm_zoom_max = Math.max(zoom_max, zoom_min)
+ mm_zoom_focus = focus
+ mm_zoom = Math.clamp(1.0, mm_zoom_min, mm_zoom_max)
+ mm_have_view = false
}
# where the window is on the screen this frame
-export function minimap_window(minimap_st: mut MinimapState, x0: float, y0: float, size: float) -> void {
- minimap_st.mm_x0 = x0
- minimap_st.mm_y0 = y0
- minimap_st.mm_size = Math.max(size, 1.0)
+export function minimap_window(x0: float, y0: float, size: float) -> void {
+ mm_x0 = x0
+ mm_y0 = y0
+ mm_size = Math.max(size, 1.0)
}
-export function minimap_size(minimap_st: MinimapState) -> float { return minimap_st.mm_size }
-export function minimap_zoom(minimap_st: MinimapState) -> float { return minimap_st.mm_zoom }
-export function minimap_view_x(minimap_st: MinimapState) -> float { return minimap_st.mm_vx }
-export function minimap_view_z(minimap_st: MinimapState) -> float { return minimap_st.mm_vz }
-export function minimap_have_view(minimap_st: MinimapState) -> bool { return minimap_st.mm_have_view }
+export function minimap_size() -> float { return mm_size }
+export function minimap_zoom() -> float { return mm_zoom }
+export function minimap_view_x() -> float { return mm_vx }
+export function minimap_view_z() -> float { return mm_vz }
+export function minimap_have_view() -> bool { return mm_have_view }
# the metres the window shows across at the zoom
-export function minimap_span(minimap_st: MinimapState) -> float { return minimap_st.mm_span / minimap_st.mm_zoom }
+export function minimap_span() -> float { return mm_span / mm_zoom }
# pixels per metre
-export function minimap_scale(minimap_st: MinimapState) -> float { return minimap_st.mm_size / minimap_span(minimap_st) }
+export function minimap_scale() -> float { return mm_size / minimap_span() }
# a world point on the screen
-export function minimap_px(minimap_st: MinimapState, x: float) -> float { return minimap_st.mm_x0 + minimap_st.mm_size * 0.5 + (x - minimap_st.mm_vx) * minimap_scale(minimap_st) }
-export function minimap_py(minimap_st: MinimapState, z: float) -> float { return minimap_st.mm_y0 + minimap_st.mm_size * 0.5 + (z - minimap_st.mm_vz) * minimap_scale(minimap_st) }
+export function minimap_px(x: float) -> float { return mm_x0 + mm_size * 0.5 + (x - mm_vx) * minimap_scale() }
+export function minimap_py(z: float) -> float { return mm_y0 + mm_size * 0.5 + (z - mm_vz) * minimap_scale() }
# the world under a point of the window (pixels from its corner)
-export function minimap_wx(minimap_st: MinimapState, rx: float) -> float { return minimap_st.mm_vx + (rx - minimap_st.mm_size * 0.5) / minimap_scale(minimap_st) }
-export function minimap_wz(minimap_st: MinimapState, ry: float) -> float { return minimap_st.mm_vz + (ry - minimap_st.mm_size * 0.5) / minimap_scale(minimap_st) }
+export function minimap_wx(rx: float) -> float { return mm_vx + (rx - mm_size * 0.5) / minimap_scale() }
+export function minimap_wz(ry: float) -> float { return mm_vz + (ry - mm_size * 0.5) / minimap_scale() }
# a round distance whose bar is no wider than a quarter of the window
-export function minimap_scale_metres(minimap_st: MinimapState) -> int {
+export function minimap_scale_metres() -> int {
let steps = [2000, 1000, 500, 250, 100, 50, 25]
- for k in 0 .. len(steps) { if float(steps[k]) * minimap_scale(minimap_st) <= minimap_st.mm_size / 4.0 { return steps[k] } }
+ for k in 0 .. len(steps) { if float(steps[k]) * minimap_scale() <= mm_size / 4.0 { return steps[k] } }
return 25
}
# the view's centre is kept where MinimapWorld allows, so the land can reach the edge but not leave
-export function minimap_clamp(minimap_st: mut MinimapState) -> void {
- minimap_st.mm_vx = Math.clamp(minimap_st.mm_vx, MinimapWorld.lo_x(), MinimapWorld.hi_x())
- minimap_st.mm_vz = Math.clamp(minimap_st.mm_vz, MinimapWorld.lo_z(), MinimapWorld.hi_z())
+export function minimap_clamp() -> void {
+ mm_vx = Math.clamp(mm_vx, MinimapWorld.lo_x(), MinimapWorld.hi_x())
+ mm_vz = Math.clamp(mm_vz, MinimapWorld.lo_z(), MinimapWorld.hi_z())
}
-export function minimap_centre(minimap_st: mut MinimapState, x: float, z: float) -> void {
- minimap_st.mm_vx = x
- minimap_st.mm_vz = z
- minimap_st.mm_have_view = true
- minimap_clamp(minimap_st)
+export function minimap_centre(x: float, z: float) -> void {
+ mm_vx = x
+ mm_vz = z
+ mm_have_view = true
+ minimap_clamp()
}
# the view on the camp, or on you
-export function minimap_recentre(minimap_st: mut MinimapState, on_you: bool) -> void {
- if on_you { minimap_centre(minimap_st, MinimapWorld.you_x(), MinimapWorld.you_z()) } else { minimap_centre(minimap_st, MinimapWorld.camp_x(), MinimapWorld.camp_z()) }
+export function minimap_recentre(on_you: bool) -> void {
+ if on_you { minimap_centre(MinimapWorld.you_x(), MinimapWorld.you_z()) } else { minimap_centre(MinimapWorld.camp_x(), MinimapWorld.camp_z()) }
}
# go there AND close in; never out, since a player already closer asked for that
-export function minimap_focus(minimap_st: mut MinimapState, on_you: bool) -> void {
- minimap_recentre(minimap_st, on_you)
- if minimap_st.mm_zoom < minimap_st.mm_zoom_focus { minimap_st.mm_zoom = Math.clamp(minimap_st.mm_zoom_focus, minimap_st.mm_zoom_min, minimap_st.mm_zoom_max) }
+export function minimap_focus(on_you: bool) -> void {
+ minimap_recentre(on_you)
+ if mm_zoom < mm_zoom_focus { mm_zoom = Math.clamp(mm_zoom_focus, mm_zoom_min, mm_zoom_max) }
}
diff --git a/packages/ludic.minimap/view_moves.ludic b/packages/ludic.minimap/view_moves.ludic
index a9eb4e20..8f654bb7 100644
--- a/packages/ludic.minimap/view_moves.ludic
+++ b/packages/ludic.minimap/view_moves.ludic
@@ -1,56 +1,61 @@
# view_moves.ludic - moving the view: zoom in steps or about a point (so what is under the pointer
# stays under it), a drag that pans, and a press that picks the mark under it
+var mm_panning: bool = false
+var mm_drag_rx: float = 0.0
+var mm_drag_ry: float = 0.0
+var mm_drag_vx: float = 0.0
+var mm_drag_vz: float = 0.0
-export function minimap_zoom_set(minimap_st: mut MinimapState, z: float) -> void {
- minimap_st.mm_zoom = Math.clamp(z, minimap_st.mm_zoom_min, minimap_st.mm_zoom_max)
- minimap_clamp(minimap_st)
+export function minimap_zoom_set(z: float) -> void {
+ mm_zoom = Math.clamp(z, mm_zoom_min, mm_zoom_max)
+ minimap_clamp()
}
# a step in (dir > 0) or out, a third at a time
-export function minimap_zoom_step(minimap_st: mut MinimapState, dir: int) -> void {
- if dir > 0 { minimap_zoom_set(minimap_st, minimap_st.mm_zoom * 1.3) } else { minimap_zoom_set(minimap_st, minimap_st.mm_zoom / 1.3) }
+export function minimap_zoom_step(dir: int) -> void {
+ if dir > 0 { minimap_zoom_set(mm_zoom * 1.3) } else { minimap_zoom_set(mm_zoom / 1.3) }
}
# zoom by `factor` about a point of the window
-export function minimap_zoom_about(minimap_st: mut MinimapState, rx: float, ry: float, factor: float) -> void {
- let wx = minimap_wx(minimap_st, rx)
- let wz = minimap_wz(minimap_st, ry)
- minimap_st.mm_zoom = Math.clamp(minimap_st.mm_zoom * factor, minimap_st.mm_zoom_min, minimap_st.mm_zoom_max)
- minimap_st.mm_vx = minimap_st.mm_vx + wx - minimap_wx(minimap_st, rx)
- minimap_st.mm_vz = minimap_st.mm_vz + wz - minimap_wz(minimap_st, ry)
- minimap_clamp(minimap_st)
+export function minimap_zoom_about(rx: float, ry: float, factor: float) -> void {
+ let wx = minimap_wx(rx)
+ let wz = minimap_wz(ry)
+ mm_zoom = Math.clamp(mm_zoom * factor, mm_zoom_min, mm_zoom_max)
+ mm_vx = mm_vx + wx - minimap_wx(rx)
+ mm_vz = mm_vz + wz - minimap_wz(ry)
+ minimap_clamp()
}
-export function minimap_pan_begin(minimap_st: mut MinimapState, rx: float, ry: float) -> void {
- minimap_st.mm_panning = true
- minimap_st.mm_drag_rx = rx
- minimap_st.mm_drag_ry = ry
- minimap_st.mm_drag_vx = minimap_st.mm_vx
- minimap_st.mm_drag_vz = minimap_st.mm_vz
+export function minimap_pan_begin(rx: float, ry: float) -> void {
+ mm_panning = true
+ mm_drag_rx = rx
+ mm_drag_ry = ry
+ mm_drag_vx = mm_vx
+ mm_drag_vz = mm_vz
}
# the point pressed follows the pointer
-export function minimap_pan_to(minimap_st: mut MinimapState, rx: float, ry: float) -> void {
- if not minimap_st.mm_panning { return }
- minimap_st.mm_vx = minimap_st.mm_drag_vx - (rx - minimap_st.mm_drag_rx) / minimap_scale(minimap_st)
- minimap_st.mm_vz = minimap_st.mm_drag_vz - (ry - minimap_st.mm_drag_ry) / minimap_scale(minimap_st)
- minimap_clamp(minimap_st)
+export function minimap_pan_to(rx: float, ry: float) -> void {
+ if not mm_panning { return }
+ mm_vx = mm_drag_vx - (rx - mm_drag_rx) / minimap_scale()
+ mm_vz = mm_drag_vz - (ry - mm_drag_ry) / minimap_scale()
+ minimap_clamp()
}
-export function minimap_pan_end(minimap_st: mut MinimapState) -> void { minimap_st.mm_panning = false }
-export function minimap_panning(minimap_st: MinimapState) -> bool { return minimap_st.mm_panning }
+export function minimap_pan_end() -> void { mm_panning = false }
+export function minimap_panning() -> bool { return mm_panning }
# the mark within r pixels of a point of the window is opened (the last drawn, so the top one);
# -1 and nothing changes when there is none
-export function minimap_pick(minimap_st: mut MinimapState, rx: float, ry: float, r: float) -> int {
- let sx = minimap_st.mm_x0 + rx
- let sy = minimap_st.mm_y0 + ry
+export function minimap_pick(rx: float, ry: float, r: float) -> int {
+ let sx = mm_x0 + rx
+ let sy = mm_y0 + ry
var hit = -1
- for i in 0 .. minimap_pin_count(minimap_st) {
- let px = minimap_px(minimap_st, minimap_st.mm_pins[i].x)
- let py = minimap_py(minimap_st, minimap_st.mm_pins[i].z)
+ for i in 0 .. minimap_pin_count() {
+ let px = minimap_px(mm_pins[i].x)
+ let py = minimap_py(mm_pins[i].z)
if sx >= px - r and sx < px + r and sy >= py - r and sy < py + r { hit = i }
}
- if hit >= 0 { minimap_st.mm_open = hit }
+ if hit >= 0 { mm_open = hit }
return hit
}
diff --git a/packages/ludic.needs/queries.ludic b/packages/ludic.needs/queries.ludic
index 771b7cef..25761ccc 100644
--- a/packages/ludic.needs/queries.ludic
+++ b/packages/ludic.needs/queries.ludic
@@ -1,36 +1,36 @@
# queries.ludic - what the world asks the body
-export function needs_level(needs_st: mut NeedsState, k: int) -> float {
+export function needs_level(k: int) -> float {
if not nd_ok(k) { return 0.0 }
- return nd_lv(needs_st)[k]
+ return nd_lv()[k]
}
-export function needs_warmth(needs_st: mut NeedsState) -> float { return nd_lv(needs_st)[NEED_WARMTH] }
-export function needs_food(needs_st: mut NeedsState) -> float { return nd_lv(needs_st)[NEED_FOOD] }
-export function needs_water(needs_st: mut NeedsState) -> float { return nd_lv(needs_st)[NEED_WATER] }
-export function needs_energy(needs_st: mut NeedsState) -> float { return nd_lv(needs_st)[NEED_ENERGY] }
+export function needs_warmth() -> float { return nd_lv()[NEED_WARMTH] }
+export function needs_food() -> float { return nd_lv()[NEED_FOOD] }
+export function needs_water() -> float { return nd_lv()[NEED_WATER] }
+export function needs_energy() -> float { return nd_lv()[NEED_ENERGY] }
# seconds left before a collapse (0: no countdown), and the need it is for
-export function needs_last_legs(needs_st: NeedsState) -> float { return needs_st.nd_legs }
-export function needs_last_need(needs_st: NeedsState) -> int { return needs_st.nd_last }
-export function needs_collapses(needs_st: NeedsState) -> int { return needs_st.nd_collapses }
+export function needs_last_legs() -> float { return nd_legs }
+export function needs_last_need() -> int { return nd_last }
+export function needs_collapses() -> int { return nd_collapses }
# the walk's pace, times: the ramp of each need, and 0.6 more on last legs
-export function needs_pace(needs_st: mut NeedsState) -> float {
- let lv = nd_lv(needs_st)
+export function needs_pace() -> float {
+ let lv = nd_lv()
var m = nd_pace(lv[NEED_WARMTH]) * nd_pace(lv[NEED_FOOD])
if NeedsWorld.thirst() { m = m * nd_pace(lv[NEED_WATER]) }
- if needs_st.nd_legs > 0.0 { m = m * 0.6 }
+ if nd_legs > 0.0 { m = m * 0.6 }
return m
}
# running needs legs, calories and a body that is not already in trouble
-export function needs_can_run(needs_st: mut NeedsState) -> bool {
- let lv = nd_lv(needs_st)
+export function needs_can_run() -> bool {
+ let lv = nd_lv()
return lv[NEED_ENERGY] > 8.0 and lv[NEED_FOOD] > 20.0 and lv[NEED_WARMTH] > 20.0 and lv[NEED_WATER] > 20.0
}
# the lowest of the four
-export function needs_lowest(needs_st: mut NeedsState) -> float {
- let lv = nd_lv(needs_st)
+export function needs_lowest() -> float {
+ let lv = nd_lv()
return Math.min(Math.min(lv[NEED_WARMTH], lv[NEED_FOOD]), Math.min(lv[NEED_WATER], lv[NEED_ENERGY]))
}
diff --git a/packages/ludic.needs/run.ludic b/packages/ludic.needs/run.ludic
index c6293a90..5585eff6 100644
--- a/packages/ludic.needs/run.ludic
+++ b/packages/ludic.needs/run.ludic
@@ -1,15 +1,15 @@
# run.ludic - `gh` game hours of whatever the body is doing, taken out of the four bars
-function nd_add(needs_st: mut NeedsState, k: int, d: float) -> void {
- let lv = nd_lv(needs_st)
+function nd_add(k: int, d: float) -> void {
+ let lv = nd_lv()
lv[k] = Math.clamp(lv[k] + d, 0.0, 100.0)
}
-export function needs_run(needs_st: mut NeedsState, gh: float) -> void {
+export function needs_run(gh: float) -> void {
let a = NeedsWorld.activity()
- nd_add(needs_st, NEED_FOOD, (NeedsWorld.bonus(NEED_FOOD) - nd_food_rate(a) * NeedsWorld.drain(NEED_FOOD)) * gh)
+ nd_add(NEED_FOOD, (NeedsWorld.bonus(NEED_FOOD) - nd_food_rate(a) * NeedsWorld.drain(NEED_FOOD)) * gh)
if NeedsWorld.thirst() {
- nd_add(needs_st, NEED_WATER, (NeedsWorld.bonus(NEED_WATER) - nd_water_rate(a) * NeedsWorld.drain(NEED_WATER)) * gh)
- } else { nd_lv(needs_st)[NEED_WATER] = 100.0 }
- nd_add(needs_st, NEED_ENERGY, (nd_energy_rate(a) + NeedsWorld.bonus(NEED_ENERGY)) * gh)
- nd_add(needs_st, NEED_WARMTH, nd_warmth_rate(a, nd_lv(needs_st)[NEED_WATER]) * gh)
+ nd_add(NEED_WATER, (NeedsWorld.bonus(NEED_WATER) - nd_water_rate(a) * NeedsWorld.drain(NEED_WATER)) * gh)
+ } else { nd_lv()[NEED_WATER] = 100.0 }
+ nd_add(NEED_ENERGY, (nd_energy_rate(a) + NeedsWorld.bonus(NEED_ENERGY)) * gh)
+ nd_add(NEED_WARMTH, nd_warmth_rate(a, nd_lv()[NEED_WATER]) * gh)
}
diff --git a/packages/ludic.needs/state.ludic b/packages/ludic.needs/state.ludic
index e3230d55..33d88d6c 100644
--- a/packages/ludic.needs/state.ludic
+++ b/packages/ludic.needs/state.ludic
@@ -51,40 +51,38 @@ export property NeedFact {
need: int = 0
}
-export state NeedsState {
- nd_level: []float = null
- nd_legs: float = 0.0 # seconds left before a collapse, 0 = none
- nd_last: int = 0 # the need the countdown is for
- nd_collapses: int = 0
- nd_facts: Queue = null
-}
+var nd_level: []float = null
+var nd_legs: float = 0.0 # seconds left before a collapse, 0 = none
+var nd_last: int = 0 # the need the countdown is for
+var nd_collapses: int = 0
+var nd_facts: Queue = null
-function nd_lv(needs_st: mut NeedsState) -> []float {
- if needs_st.nd_level == null {
- needs_st.nd_level = floats(NEED_COUNT)
- nd_fill(needs_st)
+function nd_lv() -> []float {
+ if nd_level == null {
+ nd_level = floats(NEED_COUNT)
+ nd_fill()
}
- return needs_st.nd_level
+ return nd_level
}
-function nd_fill(needs_st: mut NeedsState) -> void {
- needs_st.nd_level[NEED_WARMTH] = 100.0
- needs_st.nd_level[NEED_FOOD] = 70.0
- needs_st.nd_level[NEED_WATER] = 85.0
- needs_st.nd_level[NEED_ENERGY] = 100.0
+function nd_fill() -> void {
+ nd_level[NEED_WARMTH] = 100.0
+ nd_level[NEED_FOOD] = 70.0
+ nd_level[NEED_WATER] = 85.0
+ nd_level[NEED_ENERGY] = 100.0
}
# the facts, oldest first; the game drains them into its notices and its collapse
-export function needs_facts(base_st: mut BaseState, needs_st: mut NeedsState) -> Queue {
- if needs_st.nd_facts == null { needs_st.nd_facts = queue_new(base_st, "needs.facts") }
- return needs_st.nd_facts
+export function needs_facts() -> Queue {
+ if nd_facts == null { nd_facts = queue_new("needs.facts") }
+ return nd_facts
}
-function nd_fact(base_st: mut BaseState, needs_st: mut NeedsState, what: int, need: int) -> void {
+function nd_fact(what: int, need: int) -> void {
let f = new NeedFact
f.what = what
f.need = need
- q_push(base_st, needs_facts(base_st, needs_st), f)
+ q_push(needs_facts(), f)
}
function nd_ok(k: int) -> bool { return k >= 0 and k < NEED_COUNT }
diff --git a/packages/ludic.needs/system.ludic b/packages/ludic.needs/system.ludic
index 6d1beb82..365282d1 100644
--- a/packages/ludic.needs/system.ludic
+++ b/packages/ludic.needs/system.ludic
@@ -1,27 +1,27 @@
# system.ludic - the needs as a system: PH_SIMULATE, the drain then the watch, and its own save
# section (the countdown is not saved: a loaded body starts it again if a need is still empty)
-function needs_tick(base_st: mut BaseState, needs_st: mut NeedsState, t: Tick) -> void {
- needs_run(needs_st, t.hours)
- needs_watch(base_st, needs_st, t.dt)
+function needs_tick(t: Tick) -> void {
+ needs_run(t.hours)
+ needs_watch(t.dt)
}
-export function needs_save(needs_st: mut NeedsState) -> Val {
+export function needs_save() -> Val {
let v = Value.object()
- sv_put_float(v, "warmth", needs_warmth(needs_st))
- sv_put_float(v, "food", needs_food(needs_st))
- sv_put_float(v, "water", needs_water(needs_st))
- sv_put_float(v, "energy", needs_energy(needs_st))
- sv_put_int(v, "collapses", needs_st.nd_collapses)
+ sv_put_float(v, "warmth", needs_warmth())
+ sv_put_float(v, "food", needs_food())
+ sv_put_float(v, "water", needs_water())
+ sv_put_float(v, "energy", needs_energy())
+ sv_put_int(v, "collapses", nd_collapses)
return v
}
-export function needs_load(base_st: mut BaseState, needs_st: mut NeedsState, v: Val, version: int) -> void {
- needs_reset(base_st, needs_st)
- needs_set(needs_st, NEED_WARMTH, sv_float(v, "warmth", 100.0))
- needs_set(needs_st, NEED_FOOD, sv_float(v, "food", 70.0))
- needs_set(needs_st, NEED_WATER, sv_float(v, "water", 85.0))
- needs_set(needs_st, NEED_ENERGY, sv_float(v, "energy", 100.0))
- needs_st.nd_collapses = sv_int(v, "collapses", 0)
+export function needs_load(v: Val, version: int) -> void {
+ needs_reset()
+ needs_set(NEED_WARMTH, sv_float(v, "warmth", 100.0))
+ needs_set(NEED_FOOD, sv_float(v, "food", 70.0))
+ needs_set(NEED_WATER, sv_float(v, "water", 85.0))
+ needs_set(NEED_ENERGY, sv_float(v, "energy", 100.0))
+ nd_collapses = sv_int(v, "collapses", 0)
}
export function needs_system() -> System {
diff --git a/packages/ludic.needs/tests/needs_test.ludic b/packages/ludic.needs/tests/needs_test.ludic
index 2eb87f7a..334728b2 100644
--- a/packages/ludic.needs/tests/needs_test.ludic
+++ b/packages/ludic.needs/tests/needs_test.ludic
@@ -4,31 +4,29 @@ import "ludic.needs"
import "ludic.base"
program NeedsTest {
numbers float
- state NeedstestState {
- act: int = 0
- air: float = -2.0
- keep: float = 0.0
- hot: bool = false
- thirst: bool = true
- gentle: bool = false
- held: bool = false
- food_mult: float = 1.0
- warm_bonus: float = 0.0
- }
+ var act: int = 0
+ var air: float = -2.0
+ var keep: float = 0.0
+ var hot: bool = false
+ var thirst: bool = true
+ var gentle: bool = false
+ var held: bool = false
+ var food_mult: float = 1.0
+ var warm_bonus: float = 0.0
- function fake_act(needstest_st: NeedstestState) -> int { return needstest_st.act }
- function fake_air(needstest_st: NeedstestState) -> float { return needstest_st.air }
- function fake_keep(needstest_st: NeedstestState) -> float { return needstest_st.keep }
- function fake_hot(needstest_st: NeedstestState) -> bool { return needstest_st.hot }
- function fake_thirst(needstest_st: NeedstestState) -> bool { return needstest_st.thirst }
- function fake_gentle(needstest_st: NeedstestState) -> bool { return needstest_st.gentle }
- function fake_held(needstest_st: NeedstestState) -> bool { return needstest_st.held }
- function fake_drain(needstest_st: NeedstestState, k: int) -> float {
- if k == NEED_FOOD { return needstest_st.food_mult }
+ function fake_act() -> int { return act }
+ function fake_air() -> float { return air }
+ function fake_keep() -> float { return keep }
+ function fake_hot() -> bool { return hot }
+ function fake_thirst() -> bool { return thirst }
+ function fake_gentle() -> bool { return gentle }
+ function fake_held() -> bool { return held }
+ function fake_drain(k: int) -> float {
+ if k == NEED_FOOD { return food_mult }
return 1.0
}
- function fake_bonus(needstest_st: NeedstestState, k: int) -> float {
- if k == NEED_WARMTH { return needstest_st.warm_bonus }
+ function fake_bonus(k: int) -> float {
+ if k == NEED_WARMTH { return warm_bonus }
return 0.0
}
@@ -44,169 +42,169 @@ program NeedsTest {
bonus: fn fake_bonus
}
- function fresh(base_st: mut BaseState, needs_st: mut NeedsState) -> void { needs_reset(base_st, needs_st) }
- function hours(needs_st: mut NeedsState, h: float) -> void { for i in 0 .. int(h * 10.0) { needs_run(needs_st, 0.1) } }
- function facts(base_st: mut BaseState, needs_st: mut NeedsState) -> []NeedFact { return q_drain(base_st, needs_facts(base_st, needs_st)) }
+ function fresh() -> void { needs_reset() }
+ function hours(h: float) -> void { for i in 0 .. int(h * 10.0) { needs_run(0.1) } }
+ function facts() -> []NeedFact { return q_drain(needs_facts()) }
- test "a new body is warm, rested, fed and watered" (base_st: mut BaseState, needs_st: mut NeedsState) {
- fresh(base_st, needs_st)
- expect(needs_warmth(needs_st) == 100.0)
- expect(needs_energy(needs_st) == 100.0)
- expect(needs_food(needs_st) == 70.0)
- expect(needs_water(needs_st) == 85.0)
- expect(needs_pace(needs_st) == 1.0)
- expect(needs_can_run(needs_st))
+ test "a new body is warm, rested, fed and watered" {
+ fresh()
+ expect(needs_warmth() == 100.0)
+ expect(needs_energy() == 100.0)
+ expect(needs_food() == 70.0)
+ expect(needs_water() == 85.0)
+ expect(needs_pace() == 1.0)
+ expect(needs_can_run())
}
- test "a walking day empties a full bar of food in about a day, a run much sooner" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_FOOD, 100.0)
- needstest_st.act = NEEDS_WALK
- hours(needs_st, 12.0)
- expect_near(needs_food(needs_st), 52.0, 0.5)
- needs_set(needs_st, NEED_FOOD, 100.0)
- needstest_st.act = NEEDS_RUN
- hours(needs_st, 12.0)
- expect(needs_food(needs_st) < 20.0)
+ test "a walking day empties a full bar of food in about a day, a run much sooner" {
+ fresh()
+ needs_set(NEED_FOOD, 100.0)
+ act = NEEDS_WALK
+ hours(12.0)
+ expect_near(needs_food(), 52.0, 0.5)
+ needs_set(NEED_FOOD, 100.0)
+ act = NEEDS_RUN
+ hours(12.0)
+ expect(needs_food() < 20.0)
}
- test "the sun and the lake make thirst faster, but never past the cap" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_WATER, 100.0)
- needstest_st.act = NEEDS_SWIM_HARD
- needstest_st.hot = true
- hours(needs_st, 1.0)
- expect_near(needs_water(needs_st), 92.5, 0.01)
+ test "the sun and the lake make thirst faster, but never past the cap" {
+ fresh()
+ needs_set(NEED_WATER, 100.0)
+ act = NEEDS_SWIM_HARD
+ hot = true
+ hours(1.0)
+ expect_near(needs_water(), 92.5, 0.01)
}
- test "thirst off holds water full" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needstest_st.thirst = false
- needs_set(needs_st, NEED_WATER, 10.0)
- hours(needs_st, 1.0)
- expect(needs_water(needs_st) == 100.0)
+ test "thirst off holds water full" {
+ fresh()
+ thirst = false
+ needs_set(NEED_WATER, 10.0)
+ hours(1.0)
+ expect(needs_water() == 100.0)
}
- test "the port's drain and bonus are what the bars get" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needstest_st.food_mult = 2.0
- hours(needs_st, 1.0)
- expect_near(needs_food(needs_st), 62.0, 0.01)
- needstest_st.warm_bonus = 10.0
- needstest_st.air = 0.0
- needs_set(needs_st, NEED_WARMTH, 50.0)
- hours(needs_st, 1.0)
- expect_near(needs_warmth(needs_st), 60.0, 0.01)
+ test "the port's drain and bonus are what the bars get" {
+ fresh()
+ food_mult = 2.0
+ hours(1.0)
+ expect_near(needs_food(), 62.0, 0.01)
+ warm_bonus = 10.0
+ air = 0.0
+ needs_set(NEED_WARMTH, 50.0)
+ hours(1.0)
+ expect_near(needs_warmth(), 60.0, 0.01)
}
- test "clothing keeps what is being lost, and a dry body loses more" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needstest_st.air = -20.0
- needstest_st.keep = 0.5
- hours(needs_st, 1.0)
- expect_near(needs_warmth(needs_st), 90.0, 0.01)
- needs_set(needs_st, NEED_WARMTH, 100.0)
- needs_set(needs_st, NEED_WATER, 10.0)
- needstest_st.keep = 0.0
- hours(needs_st, 1.0)
- expect_near(needs_warmth(needs_st), 75.0, 0.01)
+ test "clothing keeps what is being lost, and a dry body loses more" {
+ fresh()
+ air = -20.0
+ keep = 0.5
+ hours(1.0)
+ expect_near(needs_warmth(), 90.0, 0.01)
+ needs_set(NEED_WARMTH, 100.0)
+ needs_set(NEED_WATER, 10.0)
+ keep = 0.0
+ hours(1.0)
+ expect_near(needs_warmth(), 75.0, 0.01)
}
- test "sitting gives energy back, swimming takes it" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_ENERGY, 40.0)
- needstest_st.act = NEEDS_SIT
- hours(needs_st, 1.0)
- expect_near(needs_energy(needs_st), 80.0, 0.01)
- needstest_st.act = NEEDS_SWIM
- hours(needs_st, 1.0)
- expect_near(needs_energy(needs_st), 52.0, 0.01)
+ test "sitting gives energy back, swimming takes it" {
+ fresh()
+ needs_set(NEED_ENERGY, 40.0)
+ act = NEEDS_SIT
+ hours(1.0)
+ expect_near(needs_energy(), 80.0, 0.01)
+ act = NEEDS_SWIM
+ hours(1.0)
+ expect_near(needs_energy(), 52.0, 0.01)
}
- test "meals fill, and every bar is held to 0..100" (base_st: mut BaseState, needs_st: mut NeedsState) {
- fresh(base_st, needs_st)
- needs_meal(needs_st, 50.0, 50.0, 10.0)
- expect(needs_food(needs_st) == 100.0)
- expect(needs_water(needs_st) == 100.0)
- needs_feed(needs_st, NEED_ENERGY, -150.0)
- expect(needs_energy(needs_st) == 0.0)
- needs_floor(needs_st, NEED_ENERGY, 30.0)
- expect(needs_energy(needs_st) == 30.0)
+ test "meals fill, and every bar is held to 0..100" {
+ fresh()
+ needs_meal(50.0, 50.0, 10.0)
+ expect(needs_food() == 100.0)
+ expect(needs_water() == 100.0)
+ needs_feed(NEED_ENERGY, -150.0)
+ expect(needs_energy() == 0.0)
+ needs_floor(NEED_ENERGY, 30.0)
+ expect(needs_energy() == 30.0)
}
- test "a need at zero is a countdown, then a collapse, as facts" (base_st: mut BaseState, needs_st: mut NeedsState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_WATER, 0.0)
- needs_watch(base_st, needs_st, 0.1)
- let a = facts(base_st, needs_st)
+ test "a need at zero is a countdown, then a collapse, as facts" {
+ fresh()
+ needs_set(NEED_WATER, 0.0)
+ needs_watch(0.1)
+ let a = facts()
expect_eq(len(a), 1)
expect_eq(a[0].what, NEEDS_CRITICAL)
expect_eq(a[0].need, NEED_WATER)
- expect(needs_last_legs(needs_st) > 0.0)
- expect(needs_pace(needs_st) < 0.6)
+ expect(needs_last_legs() > 0.0)
+ expect(needs_pace() < 0.6)
var i = 0
- while q_len(needs_facts(base_st, needs_st)) == 0 and i < 200 {
- needs_watch(base_st, needs_st, 0.1)
+ while q_len(needs_facts()) == 0 and i < 200 {
+ needs_watch(0.1)
i += 1
}
- let b = facts(base_st, needs_st)
+ let b = facts()
expect_eq(len(b), 1)
expect_eq(b[0].what, NEEDS_COLLAPSED)
expect_eq(b[0].need, NEED_WATER)
expect(i >= 99 and i <= 101)
- expect_eq(needs_collapses(needs_st), 0)
- needs_went_down(needs_st)
- expect_eq(needs_collapses(needs_st), 1)
+ expect_eq(needs_collapses(), 0)
+ needs_went_down()
+ expect_eq(needs_collapses(), 1)
}
- test "a drink in time calls the countdown off" (base_st: mut BaseState, needs_st: mut NeedsState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_FOOD, 0.0)
- needs_watch(base_st, needs_st, 0.1)
- needs_feed(needs_st, NEED_FOOD, 30.0)
- needs_watch(base_st, needs_st, 0.1)
- let f = facts(base_st, needs_st)
+ test "a drink in time calls the countdown off" {
+ fresh()
+ needs_set(NEED_FOOD, 0.0)
+ needs_watch(0.1)
+ needs_feed(NEED_FOOD, 30.0)
+ needs_watch(0.1)
+ let f = facts()
expect_eq(len(f), 2)
expect_eq(f[1].what, NEEDS_RESTORED)
- expect(needs_last_legs(needs_st) == 0.0)
+ expect(needs_last_legs() == 0.0)
}
- test "gentle never counts down, and a body already down waits" (base_st: mut BaseState, needs_st: mut NeedsState, needstest_st: mut NeedstestState) {
- fresh(base_st, needs_st)
- needstest_st.gentle = true
- needs_set(needs_st, NEED_WARMTH, 0.0)
- needs_watch(base_st, needs_st, 0.1)
- expect(needs_warmth(needs_st) == 22.0)
- expect_eq(q_len(needs_facts(base_st, needs_st)), 0)
- needstest_st.gentle = false
- needstest_st.held = true
- needs_set(needs_st, NEED_WARMTH, 0.0)
- needs_watch(base_st, needs_st, 0.1)
- expect_eq(q_len(needs_facts(base_st, needs_st)), 0)
+ test "gentle never counts down, and a body already down waits" {
+ fresh()
+ gentle = true
+ needs_set(NEED_WARMTH, 0.0)
+ needs_watch(0.1)
+ expect(needs_warmth() == 22.0)
+ expect_eq(q_len(needs_facts()), 0)
+ gentle = false
+ held = true
+ needs_set(NEED_WARMTH, 0.0)
+ needs_watch(0.1)
+ expect_eq(q_len(needs_facts()), 0)
}
- test "a hand up costs the body but leaves nothing below twenty" (base_st: mut BaseState, needs_st: mut NeedsState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_FOOD, 25.0)
- needs_helped_up(needs_st)
- expect(needs_food(needs_st) == 20.0)
- expect(needs_warmth(needs_st) == 85.0)
- expect(needs_energy(needs_st) == 75.0)
+ test "a hand up costs the body but leaves nothing below twenty" {
+ fresh()
+ needs_set(NEED_FOOD, 25.0)
+ needs_helped_up()
+ expect(needs_food() == 20.0)
+ expect(needs_warmth() == 85.0)
+ expect(needs_energy() == 75.0)
}
- test "a save section reads back the same body" (base_st: mut BaseState, needs_st: mut NeedsState) {
- fresh(base_st, needs_st)
- needs_set(needs_st, NEED_FOOD, 33.5)
- needs_set(needs_st, NEED_WARMTH, 12.25)
- needs_went_down(needs_st)
+ test "a save section reads back the same body" {
+ fresh()
+ needs_set(NEED_FOOD, 33.5)
+ needs_set(NEED_WARMTH, 12.25)
+ needs_went_down()
let root = save_tree()
- save_section(root, "needs", 1, needs_save(needs_st))
+ save_section(root, "needs", 1, needs_save())
let text = save_encode(root)
- needs_reset(base_st, needs_st)
+ needs_reset()
let n = load_section(save_decode(text), "needs")
- needs_load(base_st, needs_st, n.data, n.version)
- expect(needs_food(needs_st) == 33.5)
- expect(needs_warmth(needs_st) == 12.25)
- expect_eq(needs_collapses(needs_st), 1)
+ needs_load(n.data, n.version)
+ expect(needs_food() == 33.5)
+ expect(needs_warmth() == 12.25)
+ expect_eq(needs_collapses(), 1)
}
}
diff --git a/packages/ludic.needs/verbs.ludic b/packages/ludic.needs/verbs.ludic
index cefbdf5a..d0632389 100644
--- a/packages/ludic.needs/verbs.ludic
+++ b/packages/ludic.needs/verbs.ludic
@@ -1,58 +1,58 @@
# verbs.ludic - the only way the needs change from outside; each is held to 0..100
# `n` more of a need (less, when negative): a meal, a drink, a shove from a bear
-export function needs_feed(needs_st: mut NeedsState, k: int, n: float) -> void {
- if nd_ok(k) { nd_add(needs_st, k, n) }
+export function needs_feed(k: int, n: float) -> void {
+ if nd_ok(k) { nd_add(k, n) }
}
# a meal: food, water and energy at once
-export function needs_meal(needs_st: mut NeedsState, food: float, water: float, energy: float) -> void {
- nd_add(needs_st, NEED_FOOD, food)
- nd_add(needs_st, NEED_WATER, water)
- nd_add(needs_st, NEED_ENERGY, energy)
+export function needs_meal(food: float, water: float, energy: float) -> void {
+ nd_add(NEED_FOOD, food)
+ nd_add(NEED_WATER, water)
+ nd_add(NEED_ENERGY, energy)
}
-export function needs_set(needs_st: mut NeedsState, k: int, v: float) -> void {
- if nd_ok(k) { nd_lv(needs_st)[k] = Math.clamp(v, 0.0, 100.0) }
+export function needs_set(k: int, v: float) -> void {
+ if nd_ok(k) { nd_lv()[k] = Math.clamp(v, 0.0, 100.0) }
}
# at least `v`
-export function needs_floor(needs_st: mut NeedsState, k: int, v: float) -> void {
- if nd_ok(k) and nd_lv(needs_st)[k] < v { needs_set(needs_st, k, v) }
+export function needs_floor(k: int, v: float) -> void {
+ if nd_ok(k) and nd_lv()[k] < v { needs_set(k, v) }
}
# a night: rested to what it was worth, hungrier and thirstier for it, but never empty
-export function needs_after_night(needs_st: mut NeedsState, rested: float, food_lost: float, water_lost: float) -> void {
- let lv = nd_lv(needs_st)
+export function needs_after_night(rested: float, food_lost: float, water_lost: float) -> void {
+ let lv = nd_lv()
lv[NEED_ENERGY] = Math.clamp(rested, 0.0, 100.0)
lv[NEED_FOOD] = Math.max(lv[NEED_FOOD] - food_lost, 6.0)
if water_lost > 0.0 { lv[NEED_WATER] = Math.max(lv[NEED_WATER] - water_lost, 5.0) }
}
# the countdown called off, and nothing said (a wake, a load, a test)
-export function needs_calm(needs_st: mut NeedsState) -> void { needs_st.nd_legs = 0.0 }
+export function needs_calm() -> void { nd_legs = 0.0 }
# the body went down, however: one more collapse, and no countdown running
-export function needs_went_down(needs_st: mut NeedsState) -> void {
- needs_st.nd_collapses += 1
- needs_st.nd_legs = 0.0
+export function needs_went_down() -> void {
+ nd_collapses += 1
+ nd_legs = 0.0
}
# a hand up: the cost is in the body, and nothing goes below twenty
-export function needs_helped_up(needs_st: mut NeedsState) -> void {
- let lv = nd_lv(needs_st)
+export function needs_helped_up() -> void {
+ let lv = nd_lv()
lv[NEED_WARMTH] = Math.max(20.0, lv[NEED_WARMTH] - 15.0)
lv[NEED_FOOD] = Math.max(20.0, lv[NEED_FOOD] - 15.0)
lv[NEED_WATER] = Math.max(20.0, lv[NEED_WATER] - 15.0)
lv[NEED_ENERGY] = Math.max(20.0, lv[NEED_ENERGY] - 25.0)
- needs_st.nd_legs = 0.0
+ nd_legs = 0.0
}
# a new trip: fed, watered, rested and warm, and never walked out
-export function needs_reset(base_st: mut BaseState, needs_st: mut NeedsState) -> void {
- nd_lv(needs_st)
- nd_fill(needs_st)
- needs_st.nd_legs = 0.0
- needs_st.nd_last = 0
- needs_st.nd_collapses = 0
- q_clear(base_st, needs_facts(base_st, needs_st))
+export function needs_reset() -> void {
+ nd_lv()
+ nd_fill()
+ nd_legs = 0.0
+ nd_last = 0
+ nd_collapses = 0
+ q_clear(needs_facts())
}
diff --git a/packages/ludic.needs/watch.ludic b/packages/ludic.needs/watch.ludic
index d32b12b4..22f86f57 100644
--- a/packages/ludic.needs/watch.ludic
+++ b/packages/ludic.needs/watch.ludic
@@ -3,51 +3,51 @@
const LAST_LEGS: float = 10.0
const GENTLE_FLOOR: float = 22.0
-function nd_empty(needs_st: mut NeedsState) -> int {
- let lv = nd_lv(needs_st)
+function nd_empty() -> int {
+ let lv = nd_lv()
if lv[NEED_WARMTH] == 0.0 { return NEED_WARMTH }
if lv[NEED_FOOD] == 0.0 { return NEED_FOOD }
if lv[NEED_WATER] == 0.0 { return NEED_WATER }
return -1
}
-function nd_clear(needs_st: mut NeedsState) -> bool {
- let lv = nd_lv(needs_st)
+function nd_clear() -> bool {
+ let lv = nd_lv()
return lv[NEED_WARMTH] > 5.0 and lv[NEED_FOOD] > 5.0 and lv[NEED_WATER] > 5.0
}
# Gentle: a need that runs down stops just above where the legs start to go
-function nd_gentle(needs_st: mut NeedsState) -> void {
- let lv = nd_lv(needs_st)
+function nd_gentle() -> void {
+ let lv = nd_lv()
lv[NEED_WARMTH] = Math.max(lv[NEED_WARMTH], GENTLE_FLOOR)
lv[NEED_FOOD] = Math.max(lv[NEED_FOOD], GENTLE_FLOOR)
lv[NEED_WATER] = Math.max(lv[NEED_WATER], GENTLE_FLOOR)
- needs_st.nd_legs = 0.0
+ nd_legs = 0.0
}
# `dt` real seconds; run it after everything this frame that could fill a need
-export function needs_watch(base_st: mut BaseState, needs_st: mut NeedsState, dt: float) -> void {
+export function needs_watch(dt: float) -> void {
if NeedsWorld.held() { return }
if NeedsWorld.gentle() {
- nd_gentle(needs_st)
+ nd_gentle()
return
}
- let need = nd_empty(needs_st)
+ let need = nd_empty()
if need < 0 {
- if needs_st.nd_legs > 0.0 and nd_clear(needs_st) {
- needs_st.nd_legs = 0.0
- nd_fact(base_st, needs_st, NEEDS_RESTORED, needs_st.nd_last)
+ if nd_legs > 0.0 and nd_clear() {
+ nd_legs = 0.0
+ nd_fact(NEEDS_RESTORED, nd_last)
}
return
}
- if needs_st.nd_legs == 0.0 {
- needs_st.nd_legs = LAST_LEGS
- needs_st.nd_last = need
- nd_fact(base_st, needs_st, NEEDS_CRITICAL, need)
+ if nd_legs == 0.0 {
+ nd_legs = LAST_LEGS
+ nd_last = need
+ nd_fact(NEEDS_CRITICAL, need)
return
}
- needs_st.nd_legs = needs_st.nd_legs - dt
- if needs_st.nd_legs > 0.0 { return }
- needs_st.nd_legs = 0.0
- nd_fact(base_st, needs_st, NEEDS_COLLAPSED, needs_st.nd_last)
+ nd_legs = nd_legs - dt
+ if nd_legs > 0.0 { return }
+ nd_legs = 0.0
+ nd_fact(NEEDS_COLLAPSED, nd_last)
}
diff --git a/packages/ludic.net/codec.ludic b/packages/ludic.net/codec.ludic
index 67aae75d..f280ab83 100644
--- a/packages/ludic.net/codec.ludic
+++ b/packages/ludic.net/codec.ludic
@@ -21,89 +21,89 @@ 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
+export function nw_begin() -> void {
+ net__init()
+ net__wl = 0
}
-export function nw_i(net_st: mut NetState, v: int) -> void {
- if net_st.net__wl + 4 <= NET_MTU { nb_put32(net_st.net__wb, net_st.net__wl, v) }
- net_st.net__wl += 4
+export function nw_i(v: int) -> void {
+ if net__wl + 4 <= NET_MTU { nb_put32(net__wb, net__wl, v) }
+ net__wl += 4
}
-export function nw_b(net_st: mut NetState, v: bool) -> void { if v { nw_i(net_st, 1) } else { nw_i(net_st, 0) } }
+export function nw_b(v: bool) -> void { if v { nw_i(1) } else { nw_i(0) } }
-export function nw_f(net_st: mut NetState, v: float) -> void { nw_i(net_st, float_bits(v)) }
+export function nw_f(v: float) -> void { nw_i(float_bits(v)) }
# a string of at most 200 bytes; nw_text takes a longer one up to its own cap
-export function nw_s(net_st: mut NetState, s: string) -> void { nw_text(net_st, s, 200) }
+export function nw_s(s: string) -> void { nw_text(s, 200) }
-export function nw_text(net_st: mut NetState, s: string, cap: int) -> void {
+export function nw_text(s: string, cap: int) -> void {
var n = len(s)
if n > cap { n = cap }
- nw_i(net_st, n)
+ nw_i(n)
for i in 0 .. n {
- if net_st.net__wl < NET_MTU { net_st.net__wb[net_st.net__wl] = s[i] }
- net_st.net__wl += 1
+ if net__wl < NET_MTU { net__wb[net__wl] = s[i] }
+ net__wl += 1
}
}
-export function nw_len(net_st: NetState) -> int { return net_st.net__wl }
+export function nw_len() -> int { return net__wl }
# the reader: a short or bad message reads as zeros and sets nr_bad()
-export function nr_i(net_st: mut NetState) -> int {
- if net_st.net__rp + 4 > net_st.net__rl {
- net_st.net__rbad = true
+export function nr_i() -> int {
+ if net__rp + 4 > net__rl {
+ net__rbad = true
return 0
}
- let v = nb_get32(net_st.net__rb, net_st.net__rp)
- net_st.net__rp += 4
+ let v = nb_get32(net__rb, net__rp)
+ net__rp += 4
return v
}
-export function nr_b(net_st: mut NetState) -> bool { return nr_i(net_st) != 0 }
+export function nr_b() -> bool { return nr_i() != 0 }
-export function nr_f(net_st: mut NetState) -> float { return float_from_bits(nr_i(net_st)) }
+export function nr_f() -> float { return float_from_bits(nr_i()) }
-export function nr_s(net_st: mut NetState) -> string { return nr_text(net_st, 200) }
+export function nr_s() -> string { return nr_text(200) }
-export function nr_text(net_st: mut NetState, cap: int) -> string {
- let n = nr_i(net_st)
- if n < 0 or n > cap or net_st.net__rp + n > net_st.net__rl {
- net_st.net__rbad = true
+export function nr_text(cap: int) -> string {
+ let n = nr_i()
+ if n < 0 or n > cap or net__rp + n > net__rl {
+ net__rbad = true
return ""
}
- if net_st.net__tb == null { net_st.net__tb = buffer(NET_MTU) }
- nb_copy(net_st.net__tb, 0, net_st.net__rb, net_st.net__rp, n)
- net_st.net__rp += n
- return text_of(net_st.net__tb, n)
+ if net__tb == null { net__tb = buffer(NET_MTU) }
+ nb_copy(net__tb, 0, net__rb, net__rp, n)
+ net__rp += n
+ return text_of(net__tb, n)
}
-export function nr_bad(net_st: NetState) -> bool { return net_st.net__rbad }
+export function nr_bad() -> bool { return 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
- net_st.net__rbad = false
+export function net_read(m: NetMessage) -> void {
+ net__init()
+ nb_copy(net__rb, 0, m.data, 0, m.len)
+ net__rl = m.len
+ net__rp = 0
+ net__rbad = false
}
# 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() -> NetMessage {
+ net__init()
let m = new NetMessage
- m.from = net_st.net__my_pid
- m.len = min(net_st.net__wl, NET_MTU)
+ m.from = net__my_pid
+ m.len = min(net__wl, NET_MTU)
m.data = buffer(m.len)
- nb_copy(m.data, 0, net_st.net__wb, 0, m.len)
+ nb_copy(m.data, 0, net__wb, 0, m.len)
return m
}
# the message being read, copied out as the next one to write (a host passing it on)
-export function net_copy_read(net_st: mut NetState) -> void {
- nw_begin(net_st)
- nb_copy(net_st.net__wb, 0, net_st.net__rb, 0, net_st.net__rl)
- net_st.net__wl = net_st.net__rl
+export function net_copy_read() -> void {
+ nw_begin()
+ nb_copy(net__wb, 0, net__rb, 0, net__rl)
+ net__wl = net__rl
}
diff --git a/packages/ludic.net/codes.ludic b/packages/ludic.net/codes.ludic
index 2f9e2813..0e2aed8d 100644
--- a/packages/ludic.net/codes.ludic
+++ b/packages/ludic.net/codes.ludic
@@ -28,10 +28,10 @@ export function net_code_of(ip: int, port: int) -> string {
}
# this machine's code: its address on the network and the socket's port
-export function net_join_code(net_st: NetState) -> string {
+export function net_join_code() -> string {
var ip = NetSocket.local_ip()
if ip == 0 { ip = Udp.ip("127.0.0.1") }
- return net_code_of(ip, net_port(net_st))
+ return net_code_of(ip, net_port())
}
function net__letter(c: int) -> int {
@@ -45,7 +45,7 @@ function net__letter(c: int) -> int {
}
# a code or an address: true when net_code_ip() / net_code_port() say where
-export function net_parse_code(net_st: mut NetState, text: string) -> bool {
+export function net_parse_code(text: string) -> bool {
let p = text
var colon = -1
var dots = 0
@@ -53,7 +53,7 @@ export function net_parse_code(net_st: mut NetState, text: string) -> bool {
if p[i] == ':' { colon = i }
if p[i] == '.' { dots += 1 }
}
- if dots == 3 { return net__parse_addr(net_st, text, colon) }
+ if dots == 3 { return net__parse_addr(text, colon) }
var hi = 0
var lo = 0
var n = 0
@@ -72,24 +72,24 @@ export function net_parse_code(net_st: mut NetState, text: string) -> bool {
n += 1
}
if n != 10 { return false }
- net_st.net__code_ip = lo
- net_st.net__code_port = hi
- return net_st.net__code_ip != 0 and net_st.net__code_port > 0
+ net__code_ip = lo
+ net__code_port = hi
+ return net__code_ip != 0 and net__code_port > 0
}
-function net__parse_addr(net_st: mut NetState, text: string, colon: int) -> bool {
+function net__parse_addr(text: string, colon: int) -> bool {
var host = text
- net_st.net__code_port = net__conf(net_st).port
+ net__code_port = net__conf().port
if colon > 0 {
host = text[0..colon]
- net_st.net__code_port = Text.to_int(text[colon + 1..len(text)])
+ net__code_port = Text.to_int(text[colon + 1..len(text)])
}
- net_st.net__code_ip = Udp.ip(host)
- return net_st.net__code_ip != 0 and net_st.net__code_port > 0 and net_st.net__code_port < 65536
+ net__code_ip = Udp.ip(host)
+ return net__code_ip != 0 and net__code_port > 0 and net__code_port < 65536
}
-export function net_code_ip(net_st: NetState) -> int { return net_st.net__code_ip }
-export function net_code_port(net_st: NetState) -> int { return net_st.net__code_port }
+export function net_code_ip() -> int { return net__code_ip }
+export function net_code_port() -> int { return net__code_port }
# six of the code's letters: a room at the matchmaker
export function net_is_room(text: string) -> bool {
diff --git a/packages/ludic.net/peers.ludic b/packages/ludic.net/peers.ludic
index 96d61833..8706d345 100644
--- a/packages/ludic.net/peers.ludic
+++ b/packages/ludic.net/peers.ludic
@@ -1,29 +1,29 @@
# peers.ludic - who this machine talks to, and the written message queued for them
-function net__find(net_st: NetState, ip: int, port: int) -> int {
+function net__find(ip: int, port: int) -> int {
for i in 0 .. NET_PEERS {
- let p = net_st.net__peers[i]
+ let p = net__peers[i]
if p.on and p.ip == ip and p.port == port { return i }
}
return -1
}
# the slot talking to network player `pid`, or -1
-export function np_of_pid(net_st: NetState, pid: int) -> int {
- if net_st.net__peers == null { return -1 }
+export function np_of_pid(pid: int) -> int {
+ if net__peers == null { return -1 }
for i in 0 .. NET_PEERS {
- let p = net_st.net__peers[i]
+ let p = net__peers[i]
if p.on and p.pid == pid { return i }
}
return -1
}
-function net__open(net_st: mut NetState, slot: int, ip: int, port: int, pid: int) -> void {
- let p = net_st.net__peers[slot]
+function net__open(slot: int, ip: int, port: int, pid: int) -> void {
+ let p = net__peers[slot]
p.on = true
p.ip = ip
p.port = port
p.pid = pid
- p.heard = net_st.net__time
+ p.heard = net__time
p.next_id = 1
p.recv_id = 0
p.acked = 0
@@ -32,29 +32,29 @@ function net__open(net_st: mut NetState, slot: int, ip: int, port: int, pid: int
p.closing = false
}
-export function np_close(net_st: mut NetState, slot: int) -> void {
- if net_st.net__peers == null or slot < 0 or slot >= NET_PEERS { return }
- let p = net_st.net__peers[slot]
+export function np_close(slot: int) -> void {
+ if net__peers == null or slot < 0 or slot >= NET_PEERS { return }
+ let p = net__peers[slot]
p.on = false
p.closing = false
p.queue = new []NetMsg
}
# the written message, for one peer (a reliable one is kept until it is taken)
-export function np_queue(net_st: mut NetState, slot: int, kind: int, rel: bool) -> void {
- if net_st.net__peers == null or slot < 0 or slot >= NET_PEERS { return }
- let p = net_st.net__peers[slot]
+export function np_queue(slot: int, kind: int, rel: bool) -> void {
+ if net__peers == null or slot < 0 or slot >= NET_PEERS { return }
+ let p = net__peers[slot]
if not p.on { return }
let m = new NetMsg
m.kind = kind
m.rel = rel
m.sent = -1.0
- m.made = net_st.net__time
- var n = net_st.net__wl
+ m.made = net__time
+ var n = net__wl
if n > NET_MTU - 40 { n = NET_MTU - 40 }
m.len = n
m.data = buffer(n)
- nb_copy(m.data, 0, net_st.net__wb, 0, n)
+ nb_copy(m.data, 0, net__wb, 0, n)
if rel {
m.id = p.next_id
p.next_id += 1
@@ -63,48 +63,48 @@ export function np_queue(net_st: mut NetState, slot: int, kind: int, rel: bool)
}
# to everyone this machine talks to (a host: every guest but `except`; a guest: the host)
-export function np_queue_all(net_st: mut NetState, kind: int, rel: bool, except: int) -> void {
- if net_st.net__peers == null { return }
- for i in 0 .. NET_PEERS { if net_st.net__peers[i].on and i != except { np_queue(net_st, i, kind, rel) } }
+export function np_queue_all(kind: int, rel: bool, except: int) -> void {
+ if net__peers == null { return }
+ for i in 0 .. NET_PEERS { if net__peers[i].on and i != except { np_queue(i, kind, rel) } }
}
-export function net_peer_on(net_st: NetState, slot: int) -> bool {
- if net_st.net__peers == null or slot < 0 or slot >= NET_PEERS { return false }
- return net_st.net__peers[slot].on
+export function net_peer_on(slot: int) -> bool {
+ if net__peers == null or slot < 0 or slot >= NET_PEERS { return false }
+ return net__peers[slot].on
}
-export function net_peer_pid(net_st: NetState, slot: int) -> int {
- if not net_peer_on(net_st, slot) { return 0 }
- return net_st.net__peers[slot].pid
+export function net_peer_pid(slot: int) -> int {
+ if not net_peer_on(slot) { return 0 }
+ return net__peers[slot].pid
}
-export function net_peer_ip(net_st: NetState, slot: int) -> int {
- if not net_peer_on(net_st, slot) { return 0 }
- return net_st.net__peers[slot].ip
+export function net_peer_ip(slot: int) -> int {
+ if not net_peer_on(slot) { return 0 }
+ return net__peers[slot].ip
}
-export function net_peer_port(net_st: NetState, slot: int) -> int {
- if not net_peer_on(net_st, slot) { return 0 }
- return net_st.net__peers[slot].port
+export function net_peer_port(slot: int) -> int {
+ if not net_peer_on(slot) { return 0 }
+ return net__peers[slot].port
}
-export function net_peer_heard(net_st: NetState, slot: int) -> float {
- if not net_peer_on(net_st, slot) { return 0.0 }
- return net_st.net__peers[slot].heard
+export function net_peer_heard(slot: int) -> float {
+ if not net_peer_on(slot) { return 0.0 }
+ return net__peers[slot].heard
}
# let the slot go once it has taken everything queued for it so far, or after `secs`
-export function net_close_when_sent(net_st: mut NetState, slot: int, secs: float) -> void {
- if not net_peer_on(net_st, slot) { return }
- let p = net_st.net__peers[slot]
+export function net_close_when_sent(slot: int, secs: float) -> void {
+ if not net_peer_on(slot) { return }
+ let p = net__peers[slot]
p.closing = true
p.close_id = p.next_id - 1
- p.close_t = net_st.net__time + secs
+ p.close_t = net__time + secs
}
# the slots in use (a host's count of guests, a welcome not yet sent included)
-export function net_peer_count(net_st: NetState) -> int {
+export function net_peer_count() -> int {
var n = 0
- for i in 0 .. NET_PEERS { if net_peer_on(net_st, i) { n += 1 } }
+ for i in 0 .. NET_PEERS { if net_peer_on(i) { n += 1 } }
return n
}
diff --git a/packages/ludic.net/ports.ludic b/packages/ludic.net/ports.ludic
index 82bd1f90..742522fd 100644
--- a/packages/ludic.net/ports.ludic
+++ b/packages/ludic.net/ports.ludic
@@ -58,27 +58,29 @@ export port NetSocket {
local_ip: fn() -> int = fn net__udp_local_ip
}
+var net__cfg: NetConfig = null
+var net__facts: Queue = null
-export function net_config(net_st: mut NetState, c: NetConfig) -> void { net_st.net__cfg = c }
+export function net_config(c: NetConfig) -> void { net__cfg = c }
-function net__conf(net_st: mut NetState) -> NetConfig {
- if net_st.net__cfg == null { net_st.net__cfg = new NetConfig }
- return net_st.net__cfg
+function net__conf() -> NetConfig {
+ if net__cfg == null { net__cfg = new NetConfig }
+ return net__cfg
}
-export function net_facts(base_st: mut BaseState, net_st: mut NetState) -> Queue {
- if net_st.net__facts == null { net_st.net__facts = queue_new(base_st, "net.facts") }
- return net_st.net__facts
+export function net_facts() -> Queue {
+ if net__facts == null { net__facts = queue_new("net.facts") }
+ return net__facts
}
-function net__fact(base_st: mut BaseState, net_st: mut NetState, what: int, slot: int, pid: int, why: int, text: string) -> void {
+function net__fact(what: int, slot: int, pid: int, why: int, text: string) -> void {
let f = new NetFact
f.what = what
f.slot = slot
f.pid = pid
f.why = why
f.text = text
- q_push(base_st, net_facts(base_st, net_st), f)
+ q_push(net_facts(), f)
}
function net__no_log(s: string) -> void { }
diff --git a/packages/ludic.net/punch.ludic b/packages/ludic.net/punch.ludic
index 4e95e742..91811b69 100644
--- a/packages/ludic.net/punch.ludic
+++ b/packages/ludic.net/punch.ludic
@@ -1,51 +1,57 @@
# punch.ludic - the host sends a few packets to each new guest's outside address, so its router
# expects the guest, while the guest's hello goes the other way: both routers have seen traffic
# go out, so the replies come in
+var net__seen: []string = null # guests already punched for
+var net__punch_ip: words = null
+var net__punch_port: words = null
+var net__punch_left: words = null
+var net__punch_t: float = 0.0
+var net__punch_b: []byte = null # the punch itself, "MP" and two zeros, made once
-function net__new_guest(net_st: mut NetState, pub: string, local: string) -> void {
+function net__new_guest(pub: string, local: string) -> void {
let key = `{pub}|{local}`
- if net_st.net__seen == null { net_st.net__seen = new []string }
- for i in 0 .. len(net_st.net__seen) { if net_st.net__seen[i] == key { return } }
- push(net_st.net__seen, key)
- net__log(`room {net_st.net__room}: a guest at {pub} / {local}`)
- if len(pub) > 0 and net_parse_code(net_st, pub) { net__punch_add(net_st, net_st.net__code_ip, net_st.net__code_port) }
- if net_parse_code(net_st, local) { net__punch_add(net_st, net_st.net__code_ip, net_st.net__code_port) }
+ if net__seen == null { net__seen = new []string }
+ for i in 0 .. len(net__seen) { if net__seen[i] == key { return } }
+ push(net__seen, key)
+ net__log(`room {net__room}: a guest at {pub} / {local}`)
+ if len(pub) > 0 and net_parse_code(pub) { net__punch_add(net__code_ip, net__code_port) }
+ if net_parse_code(local) { net__punch_add(net__code_ip, net__code_port) }
}
-function net__punch_add(net_st: mut NetState, ip: int, port: int) -> void {
- if net_st.net__punch_ip == null {
- net_st.net__punch_ip = words(16)
- net_st.net__punch_port = words(16)
- net_st.net__punch_left = words(16)
- for k in 0 .. 16 { net_st.net__punch_left[k] = 0 }
+function net__punch_add(ip: int, port: int) -> void {
+ if net__punch_ip == null {
+ net__punch_ip = words(16)
+ net__punch_port = words(16)
+ net__punch_left = words(16)
+ for k in 0 .. 16 { net__punch_left[k] = 0 }
}
for k in 0 .. 16 {
- if net_st.net__punch_left[k] > 0 { continue }
- net_st.net__punch_ip[k] = ip
- net_st.net__punch_port[k] = port
- net_st.net__punch_left[k] = 10
+ if net__punch_left[k] > 0 { continue }
+ net__punch_ip[k] = ip
+ net__punch_port[k] = port
+ net__punch_left[k] = 10
return
}
}
# twice a second while a guest is new: a packet the guest's side ignores, which is the point
-function net__punch_tick(net_st: mut NetState, dt: float) -> void {
- if net_st.net__punch_ip == null { return }
- net_st.net__punch_t = net_st.net__punch_t - dt
- if net_st.net__punch_t > 0.0 { return }
- net_st.net__punch_t = 0.5
- if net_st.net__punch_b == null {
- net_st.net__punch_b = buffer(4)
- nb_put8(net_st.net__punch_b, 0, 'M')
- nb_put8(net_st.net__punch_b, 1, 'P')
+function net__punch_tick(dt: float) -> void {
+ if net__punch_ip == null { return }
+ net__punch_t = net__punch_t - dt
+ if net__punch_t > 0.0 { return }
+ net__punch_t = 0.5
+ if net__punch_b == null {
+ net__punch_b = buffer(4)
+ nb_put8(net__punch_b, 0, 'M')
+ nb_put8(net__punch_b, 1, 'P')
}
for k in 0 .. 16 {
- if net_st.net__punch_left[k] <= 0 { continue }
- NetSocket.send(net_st.net__sock, net_st.net__punch_ip[k], net_st.net__punch_port[k], net_st.net__punch_b, 4)
- net_st.net__punch_left[k] -= 1
+ if net__punch_left[k] <= 0 { continue }
+ NetSocket.send(net__sock, net__punch_ip[k], net__punch_port[k], net__punch_b, 4)
+ net__punch_left[k] -= 1
}
}
-function net__punch_clear(net_st: mut NetState) -> void {
- if net_st.net__punch_left != null { for k in 0 .. 16 { net_st.net__punch_left[k] = 0 } }
+function net__punch_clear() -> void {
+ if net__punch_left != null { for k in 0 .. 16 { net__punch_left[k] = 0 } }
}
diff --git a/packages/ludic.net/queries.ludic b/packages/ludic.net/queries.ludic
index 6e258224..d075114d 100644
--- a/packages/ludic.net/queries.ludic
+++ b/packages/ludic.net/queries.ludic
@@ -1,17 +1,17 @@
# queries.ludic - what the game asks of the session
-export function net_state(net_st: NetState) -> int { return net_st.net__state }
-export function net_live(net_st: NetState) -> bool { return net_st.net__state == NET_HOSTING or net_st.net__state == NET_JOINED }
-export function net_active(net_st: NetState) -> bool { return net_st.net__state != NET_IDLE and net_st.net__state != NET_LOST }
-export function net_hosting(net_st: NetState) -> bool { return net_st.net__state == NET_HOSTING }
-export function net_joined(net_st: NetState) -> bool { return net_st.net__state == NET_JOINED }
-export function net_joining(net_st: NetState) -> bool { return net_st.net__state == NET_JOINING }
-export function net_my_pid(net_st: NetState) -> int { return net_st.net__my_pid }
-export function net_token(net_st: NetState) -> int { return net_st.net__token }
-export function net_time(net_st: NetState) -> float { return net_st.net__time }
-export function net_why(net_st: NetState) -> int { return net_st.net__why }
-export function net_join_ip(net_st: NetState) -> int { return net_st.net__join_ip }
-export function net_join_port(net_st: NetState) -> int { return net_st.net__join_port }
-export function net_port(net_st: NetState) -> int {
- if net_st.net__sock <= 0 { return 0 }
- return NetSocket.port(net_st.net__sock)
+export function net_state() -> int { return net__state }
+export function net_live() -> bool { return net__state == NET_HOSTING or net__state == NET_JOINED }
+export function net_active() -> bool { return net__state != NET_IDLE and net__state != NET_LOST }
+export function net_hosting() -> bool { return net__state == NET_HOSTING }
+export function net_joined() -> bool { return net__state == NET_JOINED }
+export function net_joining() -> bool { return net__state == NET_JOINING }
+export function net_my_pid() -> int { return net__my_pid }
+export function net_token() -> int { return net__token }
+export function net_time() -> float { return net__time }
+export function net_why() -> int { return net__why }
+export function net_join_ip() -> int { return net__join_ip }
+export function net_join_port() -> int { return net__join_port }
+export function net_port() -> int {
+ if net__sock <= 0 { return 0 }
+ return NetSocket.port(net__sock)
}
diff --git a/packages/ludic.net/receive.ludic b/packages/ludic.net/receive.ludic
index d1e5b104..3d4c16d9 100644
--- a/packages/ludic.net/receive.ludic
+++ b/packages/ludic.net/receive.ludic
@@ -1,69 +1,69 @@
# receive.ludic - what arrived, unwrapped from the relay, checked, acknowledged and put in order
# on the inbox. A reliable message is taken strictly in order; one that arrives early is dropped
# and comes again.
-function net__pump(base_st: mut BaseState, net_st: mut NetState) -> void {
- if net_st.net__sock <= 0 { return }
+function net__pump() -> void {
+ if net__sock <= 0 { return }
for guard in 0 .. 200 {
- let n = NetSocket.recv(net_st.net__sock, net_st.net__in, NET_MTU + 64)
+ let n = NetSocket.recv(net__sock, net__in, NET_MTU + 64)
if n <= 0 { return }
- let n2 = net__unwrap(net_st, n, NetSocket.from_ip(net_st.net__sock), NetSocket.from_port(net_st.net__sock))
- if n2 > 0 { net__packet(base_st, net_st, n2, net_st.net__from_ip, net_st.net__from_port) }
+ let n2 = net__unwrap(n, NetSocket.from_ip(net__sock), NetSocket.from_port(net__sock))
+ if n2 > 0 { net__packet(n2, net__from_ip, net__from_port) }
}
}
-function net__packet(base_st: mut BaseState, net_st: mut NetState, n: int, ip: int, port: int) -> void {
+function net__packet(n: int, ip: int, port: int) -> void {
if NetWorld.trace() { net__log(`in {n} bytes from {Udp.ip_text(ip)}:{port}`) }
if n < 20 { return }
- if net__stun_packet(net_st, n) { return } # what the router looks like from outside
- if nb_get8(net_st.net__in, 0) != 'M' or nb_get8(net_st.net__in, 1) != 'L' { return } # a punch ("MP") or noise
- if nb_get8(net_st.net__in, 2) != net__conf(net_st).proto { return }
- let slot = net__slot_for(net_st, n, ip, port)
+ if net__stun_packet(n) { return } # what the router looks like from outside
+ if nb_get8(net__in, 0) != 'M' or nb_get8(net__in, 1) != 'L' { return } # a punch ("MP") or noise
+ if nb_get8(net__in, 2) != net__conf().proto { return }
+ let slot = net__slot_for(n, ip, port)
if slot < 0 { return }
- let p = net_st.net__peers[slot]
- p.heard = net_st.net__time
+ let p = net__peers[slot]
+ p.heard = net__time
# only what this connection has sent can be acknowledged: a packet from the one before it
# (a re-join) carries the old numbers and would drop the new hello as already taken
- let ack = nb_get32(net_st.net__in, 12)
+ let ack = nb_get32(net__in, 12)
if ack > p.acked and ack < p.next_id { p.acked = ack }
- net__messages(base_st, net_st, slot, n)
+ net__messages(slot, n)
}
# the known peer at that address, a stranger's new slot (a host, and only with a hello), or -1
-function net__slot_for(net_st: mut NetState, n: int, ip: int, port: int) -> int {
- let token = nb_get32(net_st.net__in, 4)
- var slot = net__find(net_st, ip, port)
+function net__slot_for(n: int, ip: int, port: int) -> int {
+ let token = nb_get32(net__in, 4)
+ var slot = net__find(ip, port)
if slot >= 0 {
- if token != net_st.net__token and net_st.net__state != NET_JOINING and net_st.net__peers[slot].pid != 0 { return -1 }
+ if token != net__token and net__state != NET_JOINING and net__peers[slot].pid != 0 { return -1 }
return slot
}
- if net_st.net__state != NET_HOSTING { return -1 }
- if n < 28 or nb_get8(net_st.net__in, 20) != NET_HELLO { return -1 }
+ if net__state != NET_HOSTING { return -1 }
+ if n < 28 or nb_get8(net__in, 20) != NET_HELLO { return -1 }
for i in 1 .. NET_PEERS {
- if not net_st.net__peers[i].on {
- net__open(net_st, i, ip, port, 0)
+ if not net__peers[i].on {
+ net__open(i, ip, port, 0)
return i
}
}
return -1
}
-function net__messages(base_st: mut BaseState, net_st: mut NetState, slot: int, n: int) -> void {
- let p = net_st.net__peers[slot]
- let from = nb_get32(net_st.net__in, 8)
- let count = nb_get32(net_st.net__in, 16)
+function net__messages(slot: int, n: int) -> void {
+ let p = net__peers[slot]
+ let from = nb_get32(net__in, 8)
+ let count = nb_get32(net__in, 16)
var o = 20
for c in 0 .. count {
if o + 4 > n { return }
- let kind = nb_get8(net_st.net__in, o)
- let rel = nb_get8(net_st.net__in, o + 1) == 1
+ let kind = nb_get8(net__in, o)
+ let rel = nb_get8(net__in, o + 1) == 1
o += 2
var id = 0
if rel {
if o + 4 > n { return }
- id = nb_get32(net_st.net__in, o)
+ id = nb_get32(net__in, o)
o += 4
}
- let ml = nb_get8(net_st.net__in, o) | (nb_get8(net_st.net__in, o + 1) << 8)
+ let ml = nb_get8(net__in, o) | (nb_get8(net__in, o + 1) << 8)
o += 2
if ml < 0 or o + ml > n { return }
var take = true
@@ -71,21 +71,21 @@ function net__messages(base_st: mut BaseState, net_st: mut NetState, slot: int,
p.ack_due = true
if id != p.recv_id + 1 { take = false } else { p.recv_id = id }
}
- if take { net__deliver(base_st, net_st, slot, from, kind, o, ml) }
+ if take { net__deliver(slot, from, kind, o, ml) }
o += ml
}
}
# onto the inbox - except a heartbeat, and on a host anything but a hello before its welcome
-function net__deliver(base_st: mut BaseState, net_st: mut NetState, slot: int, from: int, kind: int, o: int, ml: int) -> void {
+function net__deliver(slot: int, from: int, kind: int, o: int, ml: int) -> void {
if kind == NET_BEAT { return }
- if net_st.net__state == NET_HOSTING and net_st.net__peers[slot].pid < 2 and kind != NET_HELLO { return }
+ if net__state == NET_HOSTING and net__peers[slot].pid < 2 and kind != NET_HELLO { return }
let m = new NetMessage
m.slot = slot
m.from = from
m.kind = kind
m.len = ml
m.data = buffer(ml)
- nb_copy(m.data, 0, net_st.net__in, o, ml)
- q_push(base_st, net_inbox(base_st, net_st), m)
+ nb_copy(m.data, 0, net__in, o, ml)
+ q_push(net_inbox(), m)
}
diff --git a/packages/ludic.net/relay.ludic b/packages/ludic.net/relay.ludic
index cccef615..9cceee6d 100644
--- a/packages/ludic.net/relay.ludic
+++ b/packages/ludic.net/relay.ludic
@@ -2,17 +2,22 @@
# the host does not answer goes through the party server: "MLRG" + room + packet out, "MLRS" +
# session + packet back. The host keeps its address there fresh ("MLRH" + room) and sees each
# relayed guest as the relay's address with a port of relay_port0 + its session.
+var net__relay_ip: int = 0
+var net__relay_port: int = 0
+var net__via_relay: bool = false # a guest talking to its host through the relay
+var net__from_ip: int = 0
+var net__from_port: int = 0
-function net__take_relay(net_st: mut NetState, v: Val) -> void {
+function net__take_relay(v: Val) -> void {
let r = net__json_str(v, "relay")
- if len(r) > 0 and net_parse_code(net_st, r) {
- net_st.net__relay_ip = net_st.net__code_ip
- net_st.net__relay_port = net_st.net__code_port
+ if len(r) > 0 and net_parse_code(r) {
+ net__relay_ip = net__code_ip
+ net__relay_port = net__code_port
}
}
-function net__code_bytes(net_st: NetState, b: []byte, o: int) -> void {
- let cp = net_st.net__room
+function net__code_bytes(b: []byte, o: int) -> void {
+ let cp = net__room
for k in 0 .. 6 {
var c = 0
if k < len(cp) { c = cp[k] }
@@ -20,65 +25,65 @@ function net__code_bytes(net_st: NetState, b: []byte, o: int) -> void {
}
}
-function net__relay_hello(net_st: NetState) -> void {
+function net__relay_hello() -> void {
let b = buffer(10)
nb_put8(b, 0, 'M')
nb_put8(b, 1, 'L')
nb_put8(b, 2, 'R')
nb_put8(b, 3, 'H')
- net__code_bytes(net_st, b, 4)
- NetSocket.send(net_st.net__sock, net_st.net__relay_ip, net_st.net__relay_port, b, 10)
+ net__code_bytes(b, 4)
+ NetSocket.send(net__sock, net__relay_ip, net__relay_port, b, 10)
}
-function net__use_relay(net_st: mut NetState) -> void {
- net_st.net__via_relay = true
- net__log(`room {net_st.net__room}: no answer directly, through the relay`)
- net_dial(net_st, net_st.net__relay_ip, net_st.net__relay_port)
+function net__use_relay() -> void {
+ net__via_relay = true
+ net__log(`room {net__room}: no answer directly, through the relay`)
+ net_dial(net__relay_ip, net__relay_port)
}
-export function net_via_relay(net_st: NetState) -> bool { return net_st.net__via_relay }
+export function net_via_relay() -> bool { return net__via_relay }
-function net__relayed(net_st: mut NetState, p: NetPeer) -> bool {
- return net_st.net__relay_ip != 0 and p.ip == net_st.net__relay_ip and (p.port >= net__conf(net_st).relay_port0 or net_st.net__via_relay)
+function net__relayed(p: NetPeer) -> bool {
+ return net__relay_ip != 0 and p.ip == net__relay_ip and (p.port >= net__conf().relay_port0 or net__via_relay)
}
# a packet of `o` bytes in net__out, to peer p: straight, or wrapped for the relay
-function net__relay_send(net_st: mut NetState, p: NetPeer, o: int) -> int {
- if not net__relayed(net_st, p) { return NetSocket.send(net_st.net__sock, p.ip, p.port, net_st.net__out, o) }
+function net__relay_send(p: NetPeer, o: int) -> int {
+ if not net__relayed(p) { return NetSocket.send(net__sock, p.ip, p.port, net__out, o) }
var hdr = 10
- if not net_st.net__via_relay { hdr = 6 }
+ if not net__via_relay { hdr = 6 }
var k = o - 1
while k >= 0 {
- net_st.net__out[k + hdr] = net_st.net__out[k]
+ net__out[k + hdr] = net__out[k]
k -= 1
}
- nb_put8(net_st.net__out, 0, 'M')
- nb_put8(net_st.net__out, 1, 'L')
- nb_put8(net_st.net__out, 2, 'R')
- if net_st.net__via_relay {
- nb_put8(net_st.net__out, 3, 'G')
- net__code_bytes(net_st, net_st.net__out, 4)
+ nb_put8(net__out, 0, 'M')
+ nb_put8(net__out, 1, 'L')
+ nb_put8(net__out, 2, 'R')
+ if net__via_relay {
+ nb_put8(net__out, 3, 'G')
+ net__code_bytes(net__out, 4)
} else {
- let sid = p.port - net__conf(net_st).relay_port0
- nb_put8(net_st.net__out, 3, 'S')
- nb_put8(net_st.net__out, 4, (sid >> 8) & 255)
- nb_put8(net_st.net__out, 5, sid & 255)
+ let sid = p.port - net__conf().relay_port0
+ nb_put8(net__out, 3, 'S')
+ nb_put8(net__out, 4, (sid >> 8) & 255)
+ nb_put8(net__out, 5, sid & 255)
}
- let sent = NetSocket.send(net_st.net__sock, net_st.net__relay_ip, net_st.net__relay_port, net_st.net__out, o + hdr)
- nb_copy(net_st.net__out, 0, net_st.net__out, hdr, o) # the packet back where it was
+ let sent = NetSocket.send(net__sock, net__relay_ip, net__relay_port, net__out, o + hdr)
+ nb_copy(net__out, 0, net__out, hdr, o) # the packet back where it was
return sent
}
# a relayed packet is unwrapped in place and given the sender it stands for; the game packet's
# length comes back, 0 when there is nothing for the game
-function net__unwrap(net_st: mut NetState, n: int, ip: int, port: int) -> int {
- net_st.net__from_ip = ip
- net_st.net__from_port = port
- if n < 6 or net_st.net__relay_ip == 0 or ip != net_st.net__relay_ip or port != net_st.net__relay_port { return n }
- if nb_get8(net_st.net__in, 0) != 'M' or nb_get8(net_st.net__in, 1) != 'L' or nb_get8(net_st.net__in, 2) != 'R' or nb_get8(net_st.net__in, 3) != 'S' { return n }
- let sid = (nb_get8(net_st.net__in, 4) << 8) | nb_get8(net_st.net__in, 5)
- nb_copy(net_st.net__in, 0, net_st.net__in, 6, n - 6)
+function net__unwrap(n: int, ip: int, port: int) -> int {
+ net__from_ip = ip
+ net__from_port = port
+ if n < 6 or net__relay_ip == 0 or ip != net__relay_ip or port != net__relay_port { return n }
+ if nb_get8(net__in, 0) != 'M' or nb_get8(net__in, 1) != 'L' or nb_get8(net__in, 2) != 'R' or nb_get8(net__in, 3) != 'S' { return n }
+ let sid = (nb_get8(net__in, 4) << 8) | nb_get8(net__in, 5)
+ nb_copy(net__in, 0, net__in, 6, n - 6)
# the host: this guest's session; a guest: the relay itself is its host
- if sid > 0 { net_st.net__from_port = net__conf(net_st).relay_port0 + sid }
+ if sid > 0 { net__from_port = net__conf().relay_port0 + sid }
return n - 6
}
diff --git a/packages/ludic.net/room.ludic b/packages/ludic.net/room.ludic
index 89602371..31fb2099 100644
--- a/packages/ludic.net/room.ludic
+++ b/packages/ludic.net/room.ludic
@@ -8,54 +8,65 @@ const NET_R_ROOM: int = 3 # the host: registered, watching for guests
const NET_R_ASK: int = 4 # a guest: asking the room for the host
const NET_R_DONE: int = 5 # a guest: the hello is on its way
+var net__r_state: int = 0
+var net__r_role: int = 0 # 1 host, 2 guest
+var net__r_t: float = 0.0
+var net__r_tries: int = 0
+var net__pub_ip: int = 0
+var net__pub_port: int = 0
+var net__room: string = "" # the host's room code, or the one a guest asked for
+var net__http: int = 0
+var net__poll_t: float = 0.0
+var net__relay_t: float = 0.0
+var net__dial_heard: float = 0.0
# room codes are on when the game named a matchmaker
-export function net_room_on(net_st: mut NetState) -> bool { return len(net__conf(net_st).signal_url) > 0 }
-export function net_room(net_st: NetState) -> string { return net_st.net__room }
-export function net_room_asking(net_st: NetState) -> bool { return net_st.net__r_state == NET_R_STUN or net_st.net__r_state == NET_R_ASK }
+export function net_room_on() -> bool { return len(net__conf().signal_url) > 0 }
+export function net_room() -> string { return net__room }
+export function net_room_asking() -> bool { return net__r_state == NET_R_STUN or net__r_state == NET_R_ASK }
# the host (after net_host or net_become_host): a room at the matchmaker, if there is one
-export function net_room_host(net_st: mut NetState) -> void {
- if not net_room_on(net_st) { return }
- net_st.net__r_role = 1
- net_st.net__room = ""
- net_st.net__seen = new []string
- net__stun_begin(net_st)
+export function net_room_host() -> void {
+ if not net_room_on() { return }
+ net__r_role = 1
+ net__room = ""
+ net__seen = new []string
+ net__stun_begin()
}
# a guest by room code: the matchmaker's answer brings the host's address (the hello is set first)
-export function net_room_join(net_st: mut NetState, code: string) -> bool {
- if not net_room_on(net_st) {
- net_st.net__why = NET_WHY_NO_MATCHMAKER
+export function net_room_join(code: string) -> bool {
+ if not net_room_on() {
+ 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)
- net_st.net__wait_more = 20 # the matchmaker answers before the host does
- net__wait_for_host(net_st)
- net__stun_begin(net_st)
+ net__init()
+ if not net__socket() { return false }
+ net__r_role = 2
+ net__room = Text.upper(code)
+ net__wait_more = 20 # the matchmaker answers before the host does
+ net__wait_for_host()
+ net__stun_begin()
return true
}
-function net__stun_begin(net_st: mut NetState) -> void {
- net_st.net__r_state = NET_R_STUN
- net_st.net__r_t = 0.0
- net_st.net__r_tries = 0
- net_st.net__pub_ip = 0
- net_st.net__pub_port = 0
- if net__conf(net_st).no_stun {
- net_st.net__r_tries = 3
+function net__stun_begin() -> void {
+ net__r_state = NET_R_STUN
+ net__r_t = 0.0
+ net__r_tries = 0
+ net__pub_ip = 0
+ net__pub_port = 0
+ if net__conf().no_stun {
+ net__r_tries = 3
return
}
- net__stun_send(net_st)
+ net__stun_send()
}
-function net__room_body(net_st: NetState, role_key: string) -> string {
+function net__room_body(role_key: string) -> string {
var pub = ""
- if net_st.net__pub_ip != 0 { pub = net__addr_text(net_st.net__pub_ip, net_st.net__pub_port) }
- return "{\"" + role_key + "\": \"" + pub + "\", \"local\": \"" + net__local_text(net_st) + "\"}"
+ if net__pub_ip != 0 { pub = net__addr_text(net__pub_ip, net__pub_port) }
+ return "{\"" + role_key + "\": \"" + pub + "\", \"local\": \"" + net__local_text() + "\"}"
}
function net__json_str(v: Val, key: string) -> string {
@@ -65,8 +76,8 @@ function net__json_str(v: Val, key: string) -> string {
function net__addr_text(ip: int, port: int) -> string { return `{Udp.ip_text(ip)}:{port}` }
-function net__local_text(net_st: NetState) -> string {
+function net__local_text() -> string {
var ip = NetSocket.local_ip()
if ip == 0 { ip = Udp.ip("127.0.0.1") }
- return net__addr_text(ip, net_port(net_st))
+ return net__addr_text(ip, net_port())
}
diff --git a/packages/ludic.net/room_connect.ludic b/packages/ludic.net/room_connect.ludic
index 722621ee..1be28dc9 100644
--- a/packages/ludic.net/room_connect.ludic
+++ b/packages/ludic.net/room_connect.ludic
@@ -1,47 +1,47 @@
# room_connect.ludic - the matchmaker's answer: a guest dials the host it named (or is out),
# and leaving lets the room go
# the matchmaker let this machine down: a guest is out, a host keeps its code-less party
-function net__room_fail(base_st: mut BaseState, net_st: mut NetState, why: int) -> void {
+function net__room_fail(why: int) -> void {
net__log(`matchmaker: {why}`)
- let was_guest = net_st.net__r_role == 2
- let room = net_st.net__room
- net_st.net__r_state = NET_R_IDLE
- net_st.net__r_role = 0
- if was_guest { net__lose_with(base_st, net_st, why, room) } else { net__fact(base_st, net_st, NET_ROOM_FAILED, -1, 0, why, room) }
+ let was_guest = net__r_role == 2
+ let room = net__room
+ net__r_state = NET_R_IDLE
+ net__r_role = 0
+ if was_guest { net__lose_with(why, room) } else { net__fact(NET_ROOM_FAILED, -1, 0, why, room) }
}
# the guest: to the host's outside address, or its inside one behind the same router
-function net__room_connect(base_st: mut BaseState, net_st: mut NetState, host_pub: string, host_local: string) -> void {
+function net__room_connect(host_pub: string, host_local: string) -> void {
var target = host_pub
var same_router = false
- if len(host_pub) > 0 and net_st.net__pub_ip != 0 and net_parse_code(net_st, host_pub) and net_st.net__code_ip == net_st.net__pub_ip { same_router = true }
+ if len(host_pub) > 0 and net__pub_ip != 0 and net_parse_code(host_pub) and net__code_ip == net__pub_ip { same_router = true }
if len(host_pub) == 0 or same_router { target = host_local }
- if not net_parse_code(net_st, target) {
- net__room_fail(base_st, net_st, NET_WHY_BAD_ADDRESS)
+ if not net_parse_code(target) {
+ net__room_fail(NET_WHY_BAD_ADDRESS)
return
}
- net_st.net__r_state = NET_R_DONE
- net_st.net__r_t = 0.0
- net_st.net__state = NET_IDLE # net_join wants a fresh start
- net_join(net_st, net_st.net__code_ip, net_st.net__code_port)
- net_st.net__dial_heard = net_st.net__peers[0].heard
- if net__conf(net_st).force_relay and net_st.net__relay_ip != 0 { # a test of the relay alone
- net__use_relay(net_st)
+ net__r_state = NET_R_DONE
+ net__r_t = 0.0
+ net__state = NET_IDLE # net_join wants a fresh start
+ net_join(net__code_ip, net__code_port)
+ net__dial_heard = net__peers[0].heard
+ if net__conf().force_relay and net__relay_ip != 0 { # a test of the relay alone
+ net__use_relay()
return
}
- net__log(`room {net_st.net__room}: dialling {target}`)
+ net__log(`room {net__room}: dialling {target}`)
}
-function net__room_reset(net_st: mut NetState) -> void {
- if net_st.net__http != 0 {
- Http.free(net_st.net__http)
- net_st.net__http = 0
+function net__room_reset() -> void {
+ if net__http != 0 {
+ Http.free(net__http)
+ net__http = 0
}
- net_st.net__r_state = NET_R_IDLE
- net_st.net__r_role = 0
- net_st.net__room = ""
- net_st.net__via_relay = false
- net_st.net__relay_ip = 0
- net_st.net__relay_port = 0
- net__punch_clear(net_st)
+ net__r_state = NET_R_IDLE
+ net__r_role = 0
+ net__room = ""
+ net__via_relay = false
+ net__relay_ip = 0
+ net__relay_port = 0
+ net__punch_clear()
}
diff --git a/packages/ludic.net/room_tick.ludic b/packages/ludic.net/room_tick.ludic
index 0622a8ea..1e5708e2 100644
--- a/packages/ludic.net/room_tick.ludic
+++ b/packages/ludic.net/room_tick.ludic
@@ -1,82 +1,82 @@
# room_tick.ludic - the matchmaker's side of a frame: STUN asked three times, the room registered
# or asked for, the host's look at who has asked, and a guest's switch to the relay
-function net__room_tick(base_st: mut BaseState, net_st: mut NetState, dt: float) -> void {
- if net_st.net__r_state == NET_R_IDLE or net_st.net__r_role == 0 { return }
- net_st.net__r_t = net_st.net__r_t + dt
- if net_st.net__r_state == NET_R_STUN {
- net__room_stun_tick(net_st)
+function net__room_tick(dt: float) -> void {
+ if net__r_state == NET_R_IDLE or net__r_role == 0 { return }
+ net__r_t = net__r_t + dt
+ if net__r_state == NET_R_STUN {
+ net__room_stun_tick()
return
}
- if net_st.net__r_state == NET_R_REGISTER or net_st.net__r_state == NET_R_ASK or (net_st.net__r_state == NET_R_ROOM and net_st.net__http != 0) {
- net__room_answer(base_st, net_st)
+ if net__r_state == NET_R_REGISTER or net__r_state == NET_R_ASK or (net__r_state == NET_R_ROOM and net__http != 0) {
+ net__room_answer()
return
}
- if net_st.net__r_state == NET_R_DONE {
+ if net__r_state == NET_R_DONE {
# four seconds and not one packet back from the host: through the relay instead (a slow
# welcome is not a closed router - anything heard means the direct path works)
- let silent = net_st.net__peers[0].heard == net_st.net__dial_heard
- let wait = net__conf(net_st).force_relay or (silent and net_st.net__r_t > 4.0)
- if not net_st.net__via_relay and net_st.net__state == NET_JOINING and net_st.net__relay_ip != 0 and wait { net__use_relay(net_st) }
+ let silent = net__peers[0].heard == net__dial_heard
+ let wait = net__conf().force_relay or (silent and net__r_t > 4.0)
+ if not net__via_relay and net__state == NET_JOINING and net__relay_ip != 0 and wait { net__use_relay() }
return
}
- if net_st.net__r_state == NET_R_ROOM { net__room_watch(net_st, dt) }
+ if net__r_state == NET_R_ROOM { net__room_watch(dt) }
}
# a second and a half for an answer, asked three times; without one, inside addresses only
-function net__room_stun_tick(net_st: mut NetState) -> void {
- if net_st.net__pub_ip == 0 and net_st.net__r_t > 0.5 and net_st.net__r_tries < 3 {
- net_st.net__r_tries += 1
- net_st.net__r_t = 0.0
- net__stun_send(net_st)
+function net__room_stun_tick() -> void {
+ if net__pub_ip == 0 and net__r_t > 0.5 and net__r_tries < 3 {
+ net__r_tries += 1
+ net__r_t = 0.0
+ net__stun_send()
return
}
- if net_st.net__pub_ip == 0 and net_st.net__r_tries < 3 { return }
- let base = net__conf(net_st).signal_url
- if net_st.net__r_role == 1 {
- net_st.net__http = Http.post(`{base}/rooms`, net__room_body(net_st, "host"))
- net_st.net__r_state = NET_R_REGISTER
+ if net__pub_ip == 0 and net__r_tries < 3 { return }
+ let base = net__conf().signal_url
+ if net__r_role == 1 {
+ net__http = Http.post(`{base}/rooms`, net__room_body("host"))
+ net__r_state = NET_R_REGISTER
} else {
- net_st.net__http = Http.post(`{base}/rooms/{net_st.net__room}/join`, net__room_body(net_st, "pub"))
- net_st.net__r_state = NET_R_ASK
+ net__http = Http.post(`{base}/rooms/{net__room}/join`, net__room_body("pub"))
+ net__r_state = NET_R_ASK
}
- net_st.net__r_t = 0.0
+ net__r_t = 0.0
}
-function net__room_answer(base_st: mut BaseState, net_st: mut NetState) -> void {
- let st = Http.poll(net_st.net__http)
+function net__room_answer() -> void {
+ let st = Http.poll(net__http)
if st < 0 {
- if net_st.net__r_t > 12.0 {
- Http.free(net_st.net__http)
- net_st.net__http = 0
- net__room_fail(base_st, net_st, NET_WHY_MATCHMAKER_SILENT)
+ if net__r_t > 12.0 {
+ Http.free(net__http)
+ net__http = 0
+ net__room_fail(NET_WHY_MATCHMAKER_SILENT)
}
return
}
var v: Val = null
- if st > 0 and Http.ok(net_st.net__http) { v = Json.parse(Http.text(net_st.net__http)) }
- Http.free(net_st.net__http)
- net_st.net__http = 0
- if net_st.net__r_state == NET_R_REGISTER {
+ if st > 0 and Http.ok(net__http) { v = Json.parse(Http.text(net__http)) }
+ Http.free(net__http)
+ net__http = 0
+ if net__r_state == NET_R_REGISTER {
let code = net__json_str(v, "code")
if len(code) == 0 {
- net__room_fail(base_st, net_st, NET_WHY_NO_ROOM_GIVEN)
+ net__room_fail(NET_WHY_NO_ROOM_GIVEN)
return
}
- net_st.net__room = code
- net__take_relay(net_st, v)
- net_st.net__r_state = NET_R_ROOM
- net_st.net__poll_t = 0.0
+ net__room = code
+ net__take_relay(v)
+ net__r_state = NET_R_ROOM
+ net__poll_t = 0.0
net__log(`room {code}`)
- net__fact(base_st, net_st, NET_ROOM, -1, 0, 0, code)
+ net__fact(NET_ROOM, -1, 0, 0, code)
return
}
- if net_st.net__r_state == NET_R_ASK {
+ if net__r_state == NET_R_ASK {
if v == null {
- net__room_fail(base_st, net_st, NET_WHY_NO_ROOM)
+ net__room_fail(NET_WHY_NO_ROOM)
return
}
- net__take_relay(net_st, v)
- net__room_connect(base_st, net_st, net__json_str(v, "host"), net__json_str(v, "local"))
+ net__take_relay(v)
+ net__room_connect(net__json_str(v, "host"), net__json_str(v, "local"))
return
}
# the host's look at the room
@@ -84,23 +84,23 @@ function net__room_answer(base_st: mut BaseState, net_st: mut NetState) -> void
let l = value_get(v, "guests")
for i in 0 .. value_count(l) {
let g = value_at(l, i)
- net__new_guest(net_st, net__json_str(g, "pub"), net__json_str(g, "local"))
+ net__new_guest(net__json_str(g, "pub"), net__json_str(g, "local"))
}
}
}
# the host, registered: its relay address kept fresh, new guests punched for, the room looked at
-function net__room_watch(net_st: mut NetState, dt: float) -> void {
- net_st.net__relay_t = net_st.net__relay_t - dt
- if net_st.net__relay_t < 0.0 and net_st.net__relay_ip != 0 {
- net_st.net__relay_t = 3.0
- net__relay_hello(net_st)
+function net__room_watch(dt: float) -> void {
+ net__relay_t = net__relay_t - dt
+ if net__relay_t < 0.0 and net__relay_ip != 0 {
+ net__relay_t = 3.0
+ net__relay_hello()
}
- net__punch_tick(net_st, dt)
- net_st.net__poll_t = net_st.net__poll_t - dt
- if net_st.net__poll_t < 0.0 {
- net_st.net__poll_t = 1.0
- net_st.net__http = Http.get(`{net__conf(net_st).signal_url}/rooms/{net_st.net__room}`)
- net_st.net__r_t = 0.0
+ net__punch_tick(dt)
+ net__poll_t = net__poll_t - dt
+ if net__poll_t < 0.0 {
+ net__poll_t = 1.0
+ net__http = Http.get(`{net__conf().signal_url}/rooms/{net__room}`)
+ net__r_t = 0.0
}
}
diff --git a/packages/ludic.net/send.ludic b/packages/ludic.net/send.ludic
index 0be5d23d..336b1817 100644
--- a/packages/ludic.net/send.ludic
+++ b/packages/ludic.net/send.ludic
@@ -3,31 +3,31 @@
# highest reliable id taken in order, a message count - then each message: kind, flags, a
# reliable id when it is reliable, a 16-bit length, the payload. A reliable message goes again
# every quarter second until acknowledged; an unreliable one half a second old is dropped.
-export function np_flush(net_st: mut NetState, slot: int) -> void {
- if not net_peer_on(net_st, slot) { return }
- let p = net_st.net__peers[slot]
+export function np_flush(slot: int) -> void {
+ if not net_peer_on(slot) { return }
+ let p = net__peers[slot]
for pk in 0 .. 8 {
- if not net__flush_one(net_st, p) { return }
+ if not net__flush_one(p) { return }
}
}
-function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
+function net__flush_one(p: NetPeer) -> bool {
var o = 20
var count = 0
let keep = new []NetMsg
for i in 0 .. len(p.queue) {
let m = p.queue[i]
if m.rel and m.id <= p.acked { continue } # taken: drop it
- if not m.rel and net_st.net__time - m.made > 0.5 { continue } # stale
+ if not m.rel and net__time - m.made > 0.5 { continue } # stale
var due = true
- if m.rel and not (m.sent < 0.0) and net_st.net__time - m.sent < 0.25 { due = false }
+ if m.rel and not (m.sent < 0.0) and net__time - m.sent < 0.25 { due = false }
var size = 4 + m.len
if m.rel { size += 4 }
if due and o + size <= NET_MTU and count < 250 {
- o = net__put_msg(net_st, o, m)
+ o = net__put_msg(o, m)
count += 1
- m.sent = net_st.net__time
- if net_st.net__kind_out != null and m.kind < 64 { net_st.net__kind_out[m.kind] += size }
+ m.sent = net__time
+ if net__kind_out != null and m.kind < 64 { net__kind_out[m.kind] += size }
if m.rel { push(keep, m) } # kept until acknowledged
} else {
push(keep, m) # not due, or waits for the next packet
@@ -35,47 +35,47 @@ function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
}
p.queue = keep
if count == 0 and not p.ack_due { return false }
- nb_put8(net_st.net__out, 0, 'M')
- nb_put8(net_st.net__out, 1, 'L')
- nb_put8(net_st.net__out, 2, net__conf(net_st).proto)
- nb_put8(net_st.net__out, 3, 1)
- nb_put32(net_st.net__out, 4, net_st.net__token)
- nb_put32(net_st.net__out, 8, net_st.net__my_pid)
- nb_put32(net_st.net__out, 12, p.recv_id)
- nb_put32(net_st.net__out, 16, count)
- let sent = net__relay_send(net_st, p, o)
- if sent > 0 { net_st.net__bytes_out += sent + 28 } # with the IP and UDP headers: what the line carries
+ nb_put8(net__out, 0, 'M')
+ nb_put8(net__out, 1, 'L')
+ nb_put8(net__out, 2, net__conf().proto)
+ nb_put8(net__out, 3, 1)
+ nb_put32(net__out, 4, net__token)
+ nb_put32(net__out, 8, net__my_pid)
+ nb_put32(net__out, 12, p.recv_id)
+ nb_put32(net__out, 16, count)
+ let sent = net__relay_send(p, o)
+ if sent > 0 { net__bytes_out += sent + 28 } # with the IP and UDP headers: what the line carries
if NetWorld.trace() { net__log(`out {o} bytes to {Udp.ip_text(p.ip)}:{p.port}: {sent}`) }
p.ack_due = false
return count > 0
}
-function net__put_msg(net_st: NetState, at: int, m: NetMsg) -> int {
+function net__put_msg(at: int, m: NetMsg) -> int {
var o = at
- nb_put8(net_st.net__out, o, m.kind)
- nb_put8(net_st.net__out, o + 1, 0)
- if m.rel { nb_put8(net_st.net__out, o + 1, 1) }
+ nb_put8(net__out, o, m.kind)
+ nb_put8(net__out, o + 1, 0)
+ if m.rel { nb_put8(net__out, o + 1, 1) }
o += 2
if m.rel {
- nb_put32(net_st.net__out, o, m.id)
+ nb_put32(net__out, o, m.id)
o += 4
}
- nb_put8(net_st.net__out, o, m.len & 255)
- nb_put8(net_st.net__out, o + 1, (m.len >> 8) & 255)
+ nb_put8(net__out, o, m.len & 255)
+ nb_put8(net__out, o + 1, (m.len >> 8) & 255)
o += 2
- nb_copy(net_st.net__out, o, m.data, 0, m.len)
+ nb_copy(net__out, o, m.data, 0, m.len)
return o + m.len
}
# the per-kind byte count starts again (a game measuring what a party costs its upload)
-export function net_kind_out_clear(net_st: mut NetState) -> void {
- net_st.net__kind_out = words(64)
- for k in 0 .. 64 { net_st.net__kind_out[k] = 0 }
+export function net_kind_out_clear() -> void {
+ net__kind_out = words(64)
+ for k in 0 .. 64 { net__kind_out[k] = 0 }
}
-export function net_kind_bytes(net_st: NetState, kind: int) -> int {
- if net_st.net__kind_out == null or kind < 0 or kind >= 64 { return 0 }
- return net_st.net__kind_out[kind]
+export function net_kind_bytes(kind: int) -> int {
+ if net__kind_out == null or kind < 0 or kind >= 64 { return 0 }
+ return net__kind_out[kind]
}
-export function net_bytes_out(net_st: NetState) -> int { return net_st.net__bytes_out }
+export function net_bytes_out() -> int { return net__bytes_out }
diff --git a/packages/ludic.net/session.ludic b/packages/ludic.net/session.ludic
index 81641d10..d0ba1996 100644
--- a/packages/ludic.net/session.ludic
+++ b/packages/ludic.net/session.ludic
@@ -1,105 +1,106 @@
# session.ludic - hosting, joining and leaving. The host is pid 1 and hands out 2 upwards; this
# machine's pid is what its packets say. A token picked by the host names the session.
+var net__why: int = 0
# 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 {
- net_st.net__sock = 0
- net_st.net__why = NET_WHY_PORT_TAKEN
+export function net_host(port: int, token: int) -> bool {
+ net__init()
+ if net_live() { return true }
+ net__sock = NetSocket.open(port)
+ if net__sock <= 0 {
+ net__sock = 0
+ net__why = NET_WHY_PORT_TAKEN
return false
}
- for i in 0 .. NET_PEERS { np_close(net_st, i) }
- net_become_host(net_st, token)
- net__log(`hosting on port {NetSocket.port(net_st.net__sock)}`)
- net_room_host(net_st)
+ for i in 0 .. NET_PEERS { np_close(i) }
+ net_become_host(token)
+ net__log(`hosting on port {NetSocket.port(net__sock)}`)
+ net_room_host()
return true
}
# 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) }
- net_st.net__token = 0
- net_st.net__my_pid = 0
- net_st.net__state = NET_JOINING
- net_dial(net_st, ip, port)
+export function net_join(ip: int, port: int) -> bool {
+ net__init()
+ if net_live() { return false }
+ if not net__socket() { return false }
+ for i in 0 .. NET_PEERS { np_close(i) }
+ net__token = 0
+ net__my_pid = 0
+ net__state = NET_JOINING
+ net_dial(ip, port)
net__log(`joining {Udp.ip_text(ip)}:{port}`)
return true
}
-function net__socket(net_st: mut NetState) -> bool {
- if net_st.net__sock <= 0 { net_st.net__sock = NetSocket.open(0) }
- if net_st.net__sock > 0 { return true }
- net_st.net__sock = 0
- net_st.net__why = NET_WHY_NO_SOCKET
+function net__socket() -> bool {
+ if net__sock <= 0 { net__sock = NetSocket.open(0) }
+ if net__sock > 0 { return true }
+ net__sock = 0
+ net__why = NET_WHY_NO_SOCKET
return false
}
# 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 }
+export function net_hello_set() -> void {
+ net__init()
+ nb_copy(net__hello_b, 0, net__wb, 0, net__wl)
+ net__hello_l = net__wl
+ if net__hello_l > NET_MTU - 40 { net__hello_l = NET_MTU - 40 }
}
# (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
- net__open(net_st, 0, ip, port, 1)
- net_st.net__hello_t = -1.0
+export function net_dial(ip: int, port: int) -> void {
+ net__init()
+ np_close(0)
+ net__join_ip = ip
+ net__join_port = port
+ net__open(0, ip, port, 1)
+ net__hello_t = -1.0
}
# a host that set out on a new trip, or a new host: the same socket asks to be let in there
-export function net_follow(net_st: mut NetState, ip: int, port: int) -> void {
- net_dial(net_st, ip, port)
- net_st.net__token = 0
- net_st.net__my_pid = 0
- net_st.net__state = NET_JOINING
+export function net_follow(ip: int, port: int) -> void {
+ net_dial(ip, port)
+ net__token = 0
+ net__my_pid = 0
+ net__state = NET_JOINING
}
# the old host is gone and this machine keeps the socket, alone (it may become the host)
-export function net_stand_alone(net_st: mut NetState) -> void { net_st.net__state = NET_IDLE }
+export function net_stand_alone() -> void { 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
- net_st.net__token = token
+export function net_become_host(token: int) -> void {
+ net__init()
+ net__state = NET_HOSTING
+ net__my_pid = 1
+ net__next_pid = 2
+ net__token = token
}
# a welcome: this machine is `pid` in the session `token`
-export function net_joined_as(net_st: mut NetState, token: int, pid: int) -> void {
- net_st.net__token = token
- net_st.net__my_pid = pid
- net_st.net__state = NET_JOINED
- net_st.net__wait_more = 0
+export function net_joined_as(token: int, pid: int) -> void {
+ net__token = token
+ net__my_pid = pid
+ net__state = NET_JOINED
+ net__wait_more = 0
}
# the host lets the stranger in `slot` in: its pid, from 2 upwards
-export function net_accept(net_st: mut NetState, slot: int) -> int {
- if net_st.net__state != NET_HOSTING or not net_peer_on(net_st, slot) { return 0 }
- let p = net_st.net__peers[slot]
+export function net_accept(slot: int) -> int {
+ if net__state != NET_HOSTING or not net_peer_on(slot) { return 0 }
+ let p = net__peers[slot]
if p.pid >= 2 { return p.pid }
- p.pid = net_st.net__next_pid
- net_st.net__next_pid += 1
+ p.pid = net__next_pid
+ net__next_pid += 1
return p.pid
}
# a slow host is given this many more seconds to answer (a map switch, a matchmaker)
-export function net_hello_wait(net_st: mut NetState, secs: int) -> void { net_st.net__wait_more = secs }
+export function net_hello_wait(secs: int) -> void { net__wait_more = secs }
# a room code: joining, with no address yet - the matchmaker's answer brings one
-function net__wait_for_host(net_st: mut NetState) -> void {
- net_st.net__state = NET_JOINING
- net_st.net__hello_t = 0.0
+function net__wait_for_host() -> void {
+ net__state = NET_JOINING
+ net__hello_t = 0.0
}
diff --git a/packages/ludic.net/state.ludic b/packages/ludic.net/state.ludic
index b5bcfcc1..c03228be 100644
--- a/packages/ludic.net/state.ludic
+++ b/packages/ludic.net/state.ludic
@@ -47,82 +47,51 @@ export property NetMessage {
data: []byte
}
-export state NetState {
- net__cfg: NetConfig = null
- net__facts: Queue = null
- net__seen: []string = null # guests already punched for
- net__punch_ip: words = null
- net__punch_port: words = null
- net__punch_left: words = null
- net__punch_t: float = 0.0
- net__punch_b: []byte = null # the punch itself, "MP" and two zeros, made once
- net__relay_ip: int = 0
- net__relay_port: int = 0
- net__via_relay: bool = false # a guest talking to its host through the relay
- net__from_ip: int = 0
- net__from_port: int = 0
- net__r_state: int = 0
- net__r_role: int = 0 # 1 host, 2 guest
- net__r_t: float = 0.0
- net__r_tries: int = 0
- net__pub_ip: int = 0
- net__pub_port: int = 0
- net__room: string = "" # the host's room code, or the one a guest asked for
- net__http: int = 0
- net__poll_t: float = 0.0
- net__relay_t: float = 0.0
- net__dial_heard: float = 0.0
- net__why: int = 0
- net__state: int = 0
- net__sock: int = 0
- net__time: float = 0.0
- 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__wl: int = 0
- net__rb: []byte = null # the message being read
- net__rl: int = 0
- net__rp: int = 0
- net__rbad: bool = false
- net__tb: []byte = null # 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_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)
- net__beat_t: float = 0.0
- net__join_ip: int = 0
- net__join_port: int = 0
- net__code_ip: int = 0
- net__code_port: int = 0
- net__bytes_out: int = 0
- net__kind_out: words = null
- net__inbox: Queue = null
- net__stun_ip: int = 0
- net__stun_port: int = 0
- net__txn: words = null
- net__dice: Rng = null
+var net__state: int = 0
+var net__sock: int = 0
+var net__time: float = 0.0
+var net__token: int = 0
+var net__my_pid: int = 0
+var net__next_pid: int = 2
+var net__peers: []NetPeer = null
+var net__in: []byte = null
+var net__out: []byte = null
+var net__wb: []byte = null # the message being written
+var net__wl: int = 0
+var net__rb: []byte = null # the message being read
+var net__rl: int = 0
+var net__rp: int = 0
+var net__rbad: bool = false
+var net__tb: []byte = null # text read out of a message, through one scratch buffer
+var net__hello_b: []byte = null # the game's hello, sent again on every dial
+var net__hello_l: int = 0
+var net__hello_t: float = 0.0 # seconds since it went; -1 not yet
+var net__wait_more: int = 0 # extra seconds a slow host is given (a map switch, a matchmaker)
+var net__beat_t: float = 0.0
+var net__join_ip: int = 0
+var net__join_port: int = 0
+var net__code_ip: int = 0
+var net__code_port: int = 0
+var net__bytes_out: int = 0
+var net__kind_out: words = null
+var net__inbox: Queue = null
+
+export function net_inbox() -> Queue {
+ if net__inbox == null { net__inbox = queue_new("net.inbox") }
+ return net__inbox
}
-export function net_inbox(base_st: mut BaseState, net_st: mut NetState) -> Queue {
- if net_st.net__inbox == null { net_st.net__inbox = queue_new(base_st, "net.inbox") }
- 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__init() -> void {
+ if net__peers != null { return }
+ net__peers = new []NetPeer
for i in 0 .. NET_PEERS {
let p = new NetPeer
p.queue = new []NetMsg
- push(net_st.net__peers, p)
+ push(net__peers, 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)
+ net__in = buffer(NET_MTU + 64)
+ net__out = buffer(NET_MTU + 64)
+ net__wb = buffer(NET_MTU)
+ net__rb = buffer(NET_MTU)
+ net__hello_b = buffer(NET_MTU)
}
diff --git a/packages/ludic.net/stun.ludic b/packages/ludic.net/stun.ludic
index 5ad750b0..870376de 100644
--- a/packages/ludic.net/stun.ludic
+++ b/packages/ludic.net/stun.ludic
@@ -1,20 +1,24 @@
# stun.ludic - from the party's own socket, ask a public STUN server what this machine looks like
# from outside: the router's address and the port it mapped
+var net__stun_ip: int = 0
+var net__stun_port: int = 0
+var net__txn: words = null
+var net__dice: Rng = null
-function net__stun_send(net_st: mut NetState) -> void {
- if net_st.net__txn == null { net_st.net__txn = words(3) }
- if net_st.net__dice == null { net_st.net__dice = rng_new(net__conf(net_st).seed) }
- if net_st.net__stun_ip == 0 {
- let c = net__conf(net_st)
- if len(c.stun_at) > 0 and net_parse_code(net_st, c.stun_at) {
- net_st.net__stun_ip = net_st.net__code_ip
- net_st.net__stun_port = net_st.net__code_port
+function net__stun_send() -> void {
+ if net__txn == null { net__txn = words(3) }
+ if net__dice == null { net__dice = rng_new(net__conf().seed) }
+ if net__stun_ip == 0 {
+ let c = net__conf()
+ if len(c.stun_at) > 0 and net_parse_code(c.stun_at) {
+ net__stun_ip = net__code_ip
+ net__stun_port = net__code_port
} else if len(c.stun_host) > 0 {
let host: pointer = c.stun_host
- net_st.net__stun_ip = Udp.resolve(host)
- net_st.net__stun_port = c.stun_port
+ net__stun_ip = Udp.resolve(host)
+ net__stun_port = c.stun_port
}
- if net_st.net__stun_ip == 0 { return }
+ if net__stun_ip == 0 { return }
}
let b = buffer(20)
nb_put8(b, 0, 0) # a binding request, no attributes
@@ -25,9 +29,9 @@ function net__stun_send(net_st: mut NetState) -> void {
nb_put8(b, 5, 0x12)
nb_put8(b, 6, 0xA4)
nb_put8(b, 7, 0x42)
- for k in 0 .. 3 { net_st.net__txn[k] = rng_between(net_st.net__dice, 0, 2000000000) }
- for k in 0 .. 12 { nb_put8(b, 8 + k, (net_st.net__txn[k / 4] >> ((k % 4) * 8)) & 255) }
- NetSocket.send(net_st.net__sock, net_st.net__stun_ip, net_st.net__stun_port, b, 20)
+ for k in 0 .. 3 { net__txn[k] = rng_between(net__dice, 0, 2000000000) }
+ for k in 0 .. 12 { nb_put8(b, 8 + k, (net__txn[k / 4] >> ((k % 4) * 8)) & 255) }
+ NetSocket.send(net__sock, net__stun_ip, net__stun_port, b, 20)
}
function net__be16(p: []byte, o: int) -> int { return (nb_get8(p, o) << 8) | nb_get8(p, o + 1) }
@@ -35,23 +39,23 @@ function net__be16(p: []byte, o: int) -> int { return (nb_get8(p, o) << 8) | nb_
function net__be32(p: []byte, o: int) -> int { return (net__be16(p, o) << 16) | net__be16(p, o + 2) }
# a STUN reply: true when it was one (whatever it said)
-function net__stun_packet(net_st: mut NetState, n: int) -> bool {
- if n < 20 or nb_get8(net_st.net__in, 0) != 1 or nb_get8(net_st.net__in, 1) != 1 { return false }
- if net__be32(net_st.net__in, 4) != 0x2112A442 { return false }
- let len = net__be16(net_st.net__in, 2)
+function net__stun_packet(n: int) -> bool {
+ if n < 20 or nb_get8(net__in, 0) != 1 or nb_get8(net__in, 1) != 1 { return false }
+ if net__be32(net__in, 4) != 0x2112A442 { return false }
+ let len = net__be16(net__in, 2)
var o = 20
while o + 4 <= 20 + len and o + 4 <= n {
- let t = net__be16(net_st.net__in, o)
- let l = net__be16(net_st.net__in, o + 2)
- if (t == 0x0020 or t == 0x0001) and l >= 8 and o + 12 <= n and nb_get8(net_st.net__in, o + 5) == 1 {
- var port = net__be16(net_st.net__in, o + 6)
- var ip = net__be32(net_st.net__in, o + 8)
+ let t = net__be16(net__in, o)
+ let l = net__be16(net__in, o + 2)
+ if (t == 0x0020 or t == 0x0001) and l >= 8 and o + 12 <= n and nb_get8(net__in, o + 5) == 1 {
+ var port = net__be16(net__in, o + 6)
+ var ip = net__be32(net__in, o + 8)
if t == 0x0020 {
port = port ^ 0x2112
ip = ip ^ 0x2112A442
}
- net_st.net__pub_ip = ip
- net_st.net__pub_port = port
+ net__pub_ip = ip
+ net__pub_port = port
net__log(`outside we are {net__addr_text(ip, port)}`)
}
o += 4 + ((l + 3) / 4) * 4
@@ -60,5 +64,5 @@ function net__stun_packet(net_st: mut NetState, n: int) -> bool {
}
# what this machine looks like from outside, once STUN has said (0 until then)
-export function net_outside_ip(net_st: NetState) -> int { return net_st.net__pub_ip }
-export function net_outside_port(net_st: NetState) -> int { return net_st.net__pub_port }
+export function net_outside_ip() -> int { return net__pub_ip }
+export function net_outside_port() -> int { return net__pub_port }
diff --git a/packages/ludic.net/tests/net_test.ludic b/packages/ludic.net/tests/net_test.ludic
index 4793b2d2..9bd804bc 100644
--- a/packages/ludic.net/tests/net_test.ludic
+++ b/packages/ludic.net/tests/net_test.ludic
@@ -5,52 +5,50 @@ import "ludic.base"
program NetTest {
numbers float
property Pkt { ip: int = 0, port: int = 0, len: int = 0, data: []byte }
- state NettestState {
- sent: []Pkt = null
- inbound: []Pkt = null
- from_ip: int = 0
- from_port: int = 0
- }
+ var sent: []Pkt = null
+ var inbound: []Pkt = null
+ var from_ip: int = 0
+ var from_port: int = 0
function fake_open(port: int) -> int { return 5 }
function fake_close(h: int) -> void { }
function fake_port(h: int) -> int { return 40000 }
function fake_local() -> int { return Udp.ip("10.0.0.5") }
- function fake_from_ip(nettest_st: NettestState, h: int) -> int { return nettest_st.from_ip }
- function fake_from_port(nettest_st: NettestState, h: int) -> int { return nettest_st.from_port }
- function fake_send(nettest_st: mut NettestState, h: int, ip: int, port: int, b: []byte, n: int) -> int {
+ function fake_from_ip(h: int) -> int { return from_ip }
+ function fake_from_port(h: int) -> int { return from_port }
+ function fake_send(h: int, ip: int, port: int, b: []byte, n: int) -> int {
let p = new Pkt
p.ip = ip
p.port = port
p.len = n
p.data = buffer(n)
nb_copy(p.data, 0, b, 0, n)
- push(nettest_st.sent, p)
+ push(sent, p)
return n
}
- function fake_recv(nettest_st: mut NettestState, h: int, b: []byte, cap: int) -> int {
- if len(nettest_st.inbound) == 0 { return 0 }
- let p = nettest_st.inbound[0]
+ function fake_recv(h: int, b: []byte, cap: int) -> int {
+ if len(inbound) == 0 { return 0 }
+ let p = inbound[0]
let rest = new []Pkt
- for i in 1 .. len(nettest_st.inbound) { push(rest, nettest_st.inbound[i]) }
- nettest_st.inbound = rest
+ for i in 1 .. len(inbound) { push(rest, inbound[i]) }
+ inbound = rest
nb_copy(b, 0, p.data, 0, p.len)
- nettest_st.from_ip = p.ip
- nettest_st.from_port = p.port
+ from_ip = p.ip
+ from_port = p.port
return p.len
}
bind NetSocket { open: fn fake_open, close: fn fake_close, send: fn fake_send, recv: fn fake_recv, from_ip: fn fake_from_ip, from_port: fn fake_from_port, port: fn fake_port, local_ip: fn fake_local }
- function fresh(base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) -> void {
- nettest_st.sent = new []Pkt
- nettest_st.inbound = new []Pkt
- net_reset(net_st)
- q_clear(base_st, net_inbox(base_st, net_st))
- q_clear(base_st, net_facts(base_st, net_st))
+ function fresh() -> void {
+ sent = new []Pkt
+ inbound = new []Pkt
+ net_reset()
+ q_clear(net_inbox())
+ q_clear(net_facts())
}
# a packet from ip:port: token, pid, ack, then one message of `kind` (reliable with `id` > 0)
- function arrive(nettest_st: mut NettestState, ip: int, port: int, token: int, pid: int, ack: int, kind: int, id: int, payload: int) -> void {
+ function arrive(ip: int, port: int, token: int, pid: int, ack: int, kind: int, id: int, payload: int) -> void {
let b = buffer(40)
nb_put8(b, 0, 'M')
nb_put8(b, 1, 'L')
@@ -77,201 +75,201 @@ program NetTest {
p.port = port
p.len = o + 6
p.data = b
- push(nettest_st.inbound, p)
+ push(inbound, p)
}
- function frame(base_st: mut BaseState, net_st: mut NetState, dt: float) -> void {
- net_begin(base_st, net_st, dt)
- net_update(base_st, net_st, dt)
- net_end(base_st, net_st)
+ function frame(dt: float) -> void {
+ net_begin(dt)
+ net_update(dt)
+ net_end()
}
# the host with a guest let in at 10.0.0.9:5000 as pid 2
- function host_with_guest(base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) -> int {
- fresh(base_st, net_st, nettest_st)
- expect(net_host(net_st, 27415, 77))
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 0, 0, 0, NET_HELLO, 1, 42)
- net_begin(base_st, net_st, 0.01)
- let got = q_drain(base_st, net_inbox(base_st, net_st))
+ function host_with_guest() -> int {
+ fresh()
+ expect(net_host(27415, 77))
+ arrive(Udp.ip("10.0.0.9"), 5000, 0, 0, 0, NET_HELLO, 1, 42)
+ net_begin(0.01)
+ let got = q_drain(net_inbox())
expect_eq(len(got), 1)
- return net_accept(net_st, got[0].slot)
+ return net_accept(got[0].slot)
}
- function facts_of(base_st: mut BaseState, net_st: mut NetState, what: int) -> []NetFact {
- let all = q_drain(base_st, net_facts(base_st, net_st))
+ function facts_of(what: int) -> []NetFact {
+ let all = q_drain(net_facts())
let out = new []NetFact
for i in 0 .. len(all) { if all[i].what == what { push(out, all[i]) } }
return out
}
- test "the codec carries ints, bools, floats and text, and a short message reads bad" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- fresh(base_st, net_st, nettest_st)
- nw_begin(net_st)
- nw_i(net_st, -7)
- nw_b(net_st, true)
- nw_f(net_st, 2.5)
- nw_s(net_st, "maroon")
- net_read(net_st, net_written(net_st))
- expect_eq(nr_i(net_st), -7)
- expect(nr_b(net_st))
- expect_near(nr_f(net_st), 2.5, 0.0001)
- expect(nr_s(net_st) == "maroon")
- expect(not nr_bad(net_st))
- expect_eq(nr_i(net_st), 0)
- expect(nr_bad(net_st))
+ test "the codec carries ints, bools, floats and text, and a short message reads bad" {
+ fresh()
+ nw_begin()
+ nw_i(-7)
+ nw_b(true)
+ nw_f(2.5)
+ nw_s("maroon")
+ net_read(net_written())
+ expect_eq(nr_i(), -7)
+ expect(nr_b())
+ expect_near(nr_f(), 2.5, 0.0001)
+ expect(nr_s() == "maroon")
+ expect(not nr_bad())
+ expect_eq(nr_i(), 0)
+ expect(nr_bad())
}
- test "a join code decodes to its address and port; an address and a room code are taken too" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- fresh(base_st, net_st, nettest_st)
+ test "a join code decodes to its address and port; an address and a room code are taken too" {
+ fresh()
let ip = Udp.ip("192.168.1.20")
let code = net_code_of(ip, 27415)
expect_eq(len(code), 11)
- expect(net_parse_code(net_st, code))
- expect_eq(net_code_ip(net_st), ip)
- expect_eq(net_code_port(net_st), 27415)
- expect(net_parse_code(net_st, Text.lower(code)))
- expect(net_parse_code(net_st, "10.1.2.3:4000"))
- expect_eq(net_code_port(net_st), 4000)
- expect(net_parse_code(net_st, "10.1.2.3"))
- expect_eq(net_code_port(net_st), 27415)
- expect(not net_parse_code(net_st, "HELLO"))
+ expect(net_parse_code(code))
+ expect_eq(net_code_ip(), ip)
+ expect_eq(net_code_port(), 27415)
+ expect(net_parse_code(Text.lower(code)))
+ expect(net_parse_code("10.1.2.3:4000"))
+ expect_eq(net_code_port(), 4000)
+ expect(net_parse_code("10.1.2.3"))
+ expect_eq(net_code_port(), 27415)
+ expect(not net_parse_code("HELLO"))
expect(net_is_room("abcdef"))
expect(not net_is_room("ABCDE1"))
}
- test "a host takes a stranger only with a hello, and hands out pids from 2" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- let pid = host_with_guest(base_st, net_st, nettest_st)
+ test "a host takes a stranger only with a hello, and hands out pids from 2" {
+ let pid = host_with_guest()
expect_eq(pid, 2)
- expect(net_hosting(net_st))
- expect_eq(net_my_pid(net_st), 1)
- arrive(nettest_st, Udp.ip("10.0.0.10"), 6000, 0, 0, 0, 9, 1, 1)
- net_begin(base_st, net_st, 0.01)
- expect_eq(q_len(net_inbox(base_st, net_st)), 0)
- arrive(nettest_st, Udp.ip("10.0.0.11"), 6000, 0, 0, 0, NET_HELLO, 1, 1)
- net_begin(base_st, net_st, 0.01)
- let got = q_drain(base_st, net_inbox(base_st, net_st))
- expect_eq(net_accept(net_st, got[0].slot), 3)
+ expect(net_hosting())
+ expect_eq(net_my_pid(), 1)
+ arrive(Udp.ip("10.0.0.10"), 6000, 0, 0, 0, 9, 1, 1)
+ net_begin(0.01)
+ expect_eq(q_len(net_inbox()), 0)
+ arrive(Udp.ip("10.0.0.11"), 6000, 0, 0, 0, NET_HELLO, 1, 1)
+ net_begin(0.01)
+ let got = q_drain(net_inbox())
+ expect_eq(net_accept(got[0].slot), 3)
}
- test "a message's payload reads back as it was written" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- host_with_guest(base_st, net_st, nettest_st)
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 1234)
- net_begin(base_st, net_st, 0.01)
- let got = q_drain(base_st, net_inbox(base_st, net_st))
+ test "a message's payload reads back as it was written" {
+ host_with_guest()
+ arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 1234)
+ net_begin(0.01)
+ let got = q_drain(net_inbox())
expect_eq(len(got), 1)
expect_eq(got[0].kind, 9)
expect_eq(got[0].from, 2)
- net_read(net_st, got[0])
- expect_eq(nr_i(net_st), 1234)
- expect(not nr_bad(net_st))
- nr_i(net_st)
- expect(nr_bad(net_st))
+ net_read(got[0])
+ expect_eq(nr_i(), 1234)
+ expect(not nr_bad())
+ nr_i()
+ expect(nr_bad())
}
- test "a reliable message that arrives early is dropped and taken in order" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- let slot = np_of_pid(net_st, host_with_guest(base_st, net_st, nettest_st))
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
- net_begin(base_st, net_st, 0.01)
- expect_eq(q_len(net_inbox(base_st, net_st)), 0)
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 20)
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
- net_begin(base_st, net_st, 0.01)
- let got = q_drain(base_st, net_inbox(base_st, net_st))
+ test "a reliable message that arrives early is dropped and taken in order" {
+ let slot = np_of_pid(host_with_guest())
+ arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
+ net_begin(0.01)
+ expect_eq(q_len(net_inbox()), 0)
+ arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 20)
+ arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
+ net_begin(0.01)
+ let got = q_drain(net_inbox())
expect_eq(len(got), 2)
- net_read(net_st, got[1])
- expect_eq(nr_i(net_st), 30)
+ net_read(got[1])
+ expect_eq(nr_i(), 30)
expect_eq(got[1].slot, slot)
}
- test "a reliable message goes again until it is taken" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- let slot = np_of_pid(net_st, host_with_guest(base_st, net_st, nettest_st))
- net_end(base_st, net_st)
- nettest_st.sent = new []Pkt
- nw_begin(net_st)
- nw_i(net_st, 5)
- np_queue(net_st, slot, 9, true)
- frame(base_st, net_st, 0.01)
- expect_eq(len(nettest_st.sent), 1)
- frame(base_st, net_st, 0.1)
- expect_eq(len(nettest_st.sent), 1)
- frame(base_st, net_st, 0.2)
- expect_eq(len(nettest_st.sent), 2)
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
- frame(base_st, net_st, 0.3)
- let n = len(nettest_st.sent)
- frame(base_st, net_st, 0.3)
- expect_eq(len(nettest_st.sent), n)
+ test "a reliable message goes again until it is taken" {
+ let slot = np_of_pid(host_with_guest())
+ net_end()
+ sent = new []Pkt
+ nw_begin()
+ nw_i(5)
+ np_queue(slot, 9, true)
+ frame(0.01)
+ expect_eq(len(sent), 1)
+ frame(0.1)
+ expect_eq(len(sent), 1)
+ frame(0.2)
+ expect_eq(len(sent), 2)
+ arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
+ frame(0.3)
+ let n = len(sent)
+ frame(0.3)
+ expect_eq(len(sent), n)
}
- test "an unreliable message half a second old is dropped" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- let slot = np_of_pid(net_st, host_with_guest(base_st, net_st, nettest_st))
- net_end(base_st, net_st)
- nettest_st.sent = new []Pkt
- nw_begin(net_st)
- np_queue(net_st, slot, 9, false)
- net_begin(base_st, net_st, 0.6)
- net_end(base_st, net_st)
- expect_eq(len(nettest_st.sent), 0)
+ test "an unreliable message half a second old is dropped" {
+ let slot = np_of_pid(host_with_guest())
+ net_end()
+ sent = new []Pkt
+ nw_begin()
+ np_queue(slot, 9, false)
+ net_begin(0.6)
+ net_end()
+ expect_eq(len(sent), 0)
}
- test "a guest sends its hello until nobody has answered for too long" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- fresh(base_st, net_st, nettest_st)
- nw_begin(net_st)
- nw_s(net_st, "v1")
- net_hello_set(net_st)
- expect(net_join(net_st, Udp.ip("10.0.0.1"), 27415))
- expect(net_joining(net_st))
- frame(base_st, net_st, 0.1)
- expect_eq(len(nettest_st.sent), 1)
- expect_eq(nb_get8(nettest_st.sent[0].data, 20), NET_HELLO)
- for i in 0 .. 110 { frame(base_st, net_st, 0.1) }
- expect_eq(net_state(net_st), NET_LOST)
- let down = facts_of(base_st, net_st, NET_DOWN)
+ test "a guest sends its hello until nobody has answered for too long" {
+ fresh()
+ nw_begin()
+ nw_s("v1")
+ net_hello_set()
+ expect(net_join(Udp.ip("10.0.0.1"), 27415))
+ expect(net_joining())
+ frame(0.1)
+ expect_eq(len(sent), 1)
+ expect_eq(nb_get8(sent[0].data, 20), NET_HELLO)
+ for i in 0 .. 110 { frame(0.1) }
+ expect_eq(net_state(), NET_LOST)
+ let down = facts_of(NET_DOWN)
expect_eq(len(down), 1)
expect_eq(down[0].why, NET_WHY_NO_ANSWER)
- net_fresh(net_st)
- expect_eq(net_state(net_st), NET_IDLE)
+ net_fresh()
+ expect_eq(net_state(), NET_IDLE)
}
- test "a welcome makes the guest that pid in that session" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- fresh(base_st, net_st, nettest_st)
- nw_begin(net_st)
- net_hello_set(net_st)
- net_join(net_st, Udp.ip("10.0.0.1"), 27415)
- net_joined_as(net_st, 99, 4)
- expect(net_live(net_st))
- expect_eq(net_my_pid(net_st), 4)
- expect_eq(net_token(net_st), 99)
+ test "a welcome makes the guest that pid in that session" {
+ fresh()
+ nw_begin()
+ net_hello_set()
+ net_join(Udp.ip("10.0.0.1"), 27415)
+ net_joined_as(99, 4)
+ expect(net_live())
+ expect_eq(net_my_pid(), 4)
+ expect_eq(net_token(), 99)
}
- test "a guest silent for the timeout is gone" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- let pid = host_with_guest(base_st, net_st, nettest_st)
- let slot = np_of_pid(net_st, pid)
- for i in 0 .. 105 { frame(base_st, net_st, 0.1) }
- let gone = facts_of(base_st, net_st, NET_GONE)
+ test "a guest silent for the timeout is gone" {
+ let pid = host_with_guest()
+ let slot = np_of_pid(pid)
+ for i in 0 .. 105 { frame(0.1) }
+ let gone = facts_of(NET_GONE)
expect_eq(len(gone), 1)
expect_eq(gone[0].pid, pid)
expect_eq(gone[0].slot, slot)
- expect(not net_peer_on(net_st, slot))
+ expect(not net_peer_on(slot))
}
- test "a slot is let go once it has taken its last message" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- let slot = np_of_pid(net_st, host_with_guest(base_st, net_st, nettest_st))
- nw_begin(net_st)
- np_queue(net_st, slot, 9, true)
- net_close_when_sent(net_st, slot, 20.0)
- frame(base_st, net_st, 0.01)
- expect(net_peer_on(net_st, slot))
- arrive(nettest_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
- frame(base_st, net_st, 0.01)
- expect(not net_peer_on(net_st, slot))
- expect_eq(len(facts_of(base_st, net_st, NET_CLOSED)), 1)
+ test "a slot is let go once it has taken its last message" {
+ let slot = np_of_pid(host_with_guest())
+ nw_begin()
+ np_queue(slot, 9, true)
+ net_close_when_sent(slot, 20.0)
+ frame(0.01)
+ expect(net_peer_on(slot))
+ arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
+ frame(0.01)
+ expect(not net_peer_on(slot))
+ expect_eq(len(facts_of(NET_CLOSED)), 1)
}
- test "with no matchmaker, a room code is refused" (base_st: mut BaseState, net_st: mut NetState, nettest_st: mut NettestState) {
- fresh(base_st, net_st, nettest_st)
- expect(not net_room_on(net_st))
- expect(not net_room_join(net_st, "ABCDEF"))
- expect_eq(net_why(net_st), NET_WHY_NO_MATCHMAKER)
+ test "with no matchmaker, a room code is refused" {
+ fresh()
+ expect(not net_room_on())
+ expect(not net_room_join("ABCDEF"))
+ expect_eq(net_why(), NET_WHY_NO_MATCHMAKER)
}
}
diff --git a/packages/ludic.net/tick.ludic b/packages/ludic.net/tick.ludic
index 96ebc981..99431fa7 100644
--- a/packages/ludic.net/tick.ludic
+++ b/packages/ludic.net/tick.ludic
@@ -1,111 +1,111 @@
# tick.ludic - a frame of the session, in three calls with the game's between them: net_begin
# (time, and what came in onto the inbox), net_update (the matchmaker, the hello, silence) and,
# once the game has queued its own, net_end (heartbeats, sending, letting slots go)
-export function net_begin(base_st: mut BaseState, net_st: mut NetState, dt: float) -> void {
- if not net_active(net_st) { return }
- net_st.net__time = net_st.net__time + dt
- net__pump(base_st, net_st)
+export function net_begin(dt: float) -> void {
+ if not net_active() { return }
+ net__time = net__time + dt
+ net__pump()
}
-export function net_update(base_st: mut BaseState, net_st: mut NetState, dt: float) -> void {
- if not net_active(net_st) { return }
- net__room_tick(base_st, net_st, dt)
- if net_st.net__state == NET_JOINING {
- net__hello_tick(base_st, net_st, dt)
+export function net_update(dt: float) -> void {
+ if not net_active() { return }
+ net__room_tick(dt)
+ if net__state == NET_JOINING {
+ net__hello_tick(dt)
return
}
for i in 0 .. NET_PEERS {
- let p = net_st.net__peers[i]
- if not p.on or net_st.net__time - p.heard <= float(NET_TIMEOUT) { continue }
- if net_st.net__state == NET_JOINED {
- net_lose(base_st, net_st, NET_WHY_HOST_SILENT)
+ let p = net__peers[i]
+ if not p.on or net__time - p.heard <= float(NET_TIMEOUT) { continue }
+ if net__state == NET_JOINED {
+ net_lose(NET_WHY_HOST_SILENT)
return
}
let gone = p.pid
- np_close(net_st, i)
- net__fact(base_st, net_st, NET_GONE, i, gone, 0, "")
+ np_close(i)
+ net__fact(NET_GONE, i, gone, 0, "")
}
}
-function net__hello_tick(base_st: mut BaseState, net_st: mut NetState, dt: float) -> void {
- if net_st.net__hello_t < 0.0 {
- nb_copy(net_st.net__wb, 0, net_st.net__hello_b, 0, net_st.net__hello_l)
- net_st.net__wl = net_st.net__hello_l
- np_queue(net_st, 0, NET_HELLO, true)
- net_st.net__hello_t = 0.0
+function net__hello_tick(dt: float) -> void {
+ if net__hello_t < 0.0 {
+ nb_copy(net__wb, 0, net__hello_b, 0, net__hello_l)
+ net__wl = net__hello_l
+ np_queue(0, NET_HELLO, true)
+ net__hello_t = 0.0
}
- net_st.net__hello_t = net_st.net__hello_t + dt
- if net_st.net__hello_t > float(NET_TIMEOUT + net_st.net__wait_more) {
- net_lose(base_st, net_st, NET_WHY_NO_ANSWER)
+ net__hello_t = net__hello_t + dt
+ if net__hello_t > float(NET_TIMEOUT + net__wait_more) {
+ net_lose(NET_WHY_NO_ANSWER)
return
}
- np_flush(net_st, 0)
+ np_flush(0)
}
-export function net_end(base_st: mut BaseState, net_st: mut NetState) -> void {
- if not net_live(net_st) { return }
- let beat = net_st.net__time - net_st.net__beat_t >= 1.0
- if beat { net_st.net__beat_t = net_st.net__time }
+export function net_end() -> void {
+ if not net_live() { return }
+ let beat = net__time - net__beat_t >= 1.0
+ if beat { net__beat_t = net__time }
for i in 0 .. NET_PEERS {
- let p = net_st.net__peers[i]
+ let p = net__peers[i]
if not p.on { continue }
if beat {
- nw_begin(net_st)
- np_queue(net_st, i, NET_BEAT, false)
+ nw_begin()
+ np_queue(i, NET_BEAT, false)
}
- np_flush(net_st, i)
- if p.closing and (p.acked >= p.close_id or net_st.net__time > p.close_t) {
+ np_flush(i)
+ if p.closing and (p.acked >= p.close_id or net__time > p.close_t) {
let pid = p.pid
- np_close(net_st, i)
- net__fact(base_st, net_st, NET_CLOSED, i, pid, 0, "")
+ np_close(i)
+ net__fact(NET_CLOSED, i, pid, 0, "")
}
}
}
# while a load holds the frame: keep answering, so nobody times out on this machine
-export function net_keepalive(base_st: mut BaseState, net_st: mut NetState) -> void {
- if not net_live(net_st) { return }
- net__pump(base_st, net_st)
+export function net_keepalive() -> void {
+ if not net_live() { return }
+ net__pump()
for i in 0 .. NET_PEERS {
- let p = net_st.net__peers[i]
+ let p = net__peers[i]
if p.on {
- p.heard = net_st.net__time
- nw_begin(net_st)
- np_queue(net_st, i, NET_BEAT, false)
- np_flush(net_st, i)
+ p.heard = net__time
+ nw_begin()
+ np_queue(i, NET_BEAT, false)
+ np_flush(i)
}
}
}
# every queue to its peer now, three times over (a machine about to let go)
-export function net_flush_all(net_st: mut NetState) -> void {
- for t in 0 .. 3 { for i in 0 .. NET_PEERS { np_flush(net_st, i) } }
+export function net_flush_all() -> void {
+ for t in 0 .. 3 { for i in 0 .. NET_PEERS { np_flush(i) } }
}
# the session is over for this machine: every slot, the socket and the matchmaker let go
-export function net_reset(net_st: mut NetState) -> void {
- if net_st.net__peers != null { for i in 0 .. NET_PEERS { np_close(net_st, i) } }
- if net_st.net__sock > 0 {
- NetSocket.close(net_st.net__sock)
- net_st.net__sock = 0
+export function net_reset() -> void {
+ if net__peers != null { for i in 0 .. NET_PEERS { np_close(i) } }
+ if net__sock > 0 {
+ NetSocket.close(net__sock)
+ net__sock = 0
}
- net_st.net__state = NET_IDLE
- net_st.net__my_pid = 0
- net_st.net__wait_more = 0
- net__room_reset(net_st)
+ net__state = NET_IDLE
+ net__my_pid = 0
+ net__wait_more = 0
+ net__room_reset()
}
# out of the session, and why: the game hears NET_DOWN and says so in its own words
-export function net_lose(base_st: mut BaseState, net_st: mut NetState, why: int) -> void { net__lose_with(base_st, net_st, why, "") }
+export function net_lose(why: int) -> void { net__lose_with(why, "") }
# ... with what the reason is about (the room code nobody had)
-function net__lose_with(base_st: mut BaseState, net_st: mut NetState, why: int, text: string) -> void {
- net_reset(net_st)
- net_st.net__state = NET_LOST
- net_st.net__why = why
+function net__lose_with(why: int, text: string) -> void {
+ net_reset()
+ net__state = NET_LOST
+ net__why = why
net__log(`lost ({why})`)
- net__fact(base_st, net_st, NET_DOWN, -1, 0, why, text)
+ net__fact(NET_DOWN, -1, 0, why, text)
}
# a lost session read and done with: idle again
-export function net_fresh(net_st: mut NetState) -> void { if net_st.net__state == NET_LOST { net_st.net__state = NET_IDLE } }
+export function net_fresh() -> void { if net__state == NET_LOST { net__state = NET_IDLE } }
diff --git a/packages/ludic.npc/act.ludic b/packages/ludic.npc/act.ludic
index a2ab0580..ec73b214 100644
--- a/packages/ludic.npc/act.ludic
+++ b/packages/ludic.npc/act.ludic
@@ -1,6 +1,6 @@
# act.ludic - doing the act: turned toward what it is about - or toward whoever came over to talk,
# within the kind's notice - the game's own tick for the act, and a new plan when its time is up
-export function npc_act(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
+export function npc_act(p: NpcPerson, dt: float) -> void {
p.act_t = p.act_t - dt
var wx = p.fx
var wz = p.fz
@@ -8,8 +8,8 @@ export function npc_act(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
wx = p.x - Math.sin(p.stance) * 10.0
wz = p.z - Math.cos(p.stance) * 10.0
}
- if not p.stalk and np_turns(p.act) and npc_near(npc_st, p.x, p.z) < NpcKinds[p.kind].notice {
- let i = npc_near_i(npc_st)
+ if not p.stalk and np_turns(p.act) and npc_near(p.x, p.z) < NpcKinds[p.kind].notice {
+ let i = npc_near_i()
wx = NpcWorld.player_x(i)
wz = NpcWorld.player_z(i)
}
@@ -21,7 +21,7 @@ export function npc_act(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
let tick = NpcActs[p.act].tick
if tick(p, dt) { return }
}
- if p.act_t < 0.0 { npc_plan(npc_st, p) }
+ if p.act_t < 0.0 { npc_plan(p) }
}
function np_turns(act: int) -> bool {
@@ -30,7 +30,7 @@ function np_turns(act: int) -> bool {
}
# a player who came within notice since the last look is greeted - once, until they walk off
-export function npc_notice(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson) -> void {
+export function npc_notice(p: NpcPerson) -> void {
let reach = NpcKinds[p.kind].notice
for i in 0 .. NpcWorld.players() {
if i >= 30 { break }
@@ -39,7 +39,7 @@ export function npc_notice(base_st: mut BaseState, npc_st: mut NpcState, p: NpcP
if NpcWorld.player_on(i) { d = npc_d(p.x, p.z, NpcWorld.player_x(i), NpcWorld.player_z(i)) }
if d < reach and (p.near & bit) == 0 {
p.near = p.near | bit
- np_fact(base_st, npc_st, NPC_F_GREETED, p, i)
+ np_fact(NPC_F_GREETED, p, i)
}
if d > reach * 1.5 and (p.near & bit) != 0 { p.near = p.near - bit }
}
diff --git a/packages/ludic.npc/census.ludic b/packages/ludic.npc/census.ludic
index bff76be3..aada41e2 100644
--- a/packages/ludic.npc/census.ludic
+++ b/packages/ludic.npc/census.ludic
@@ -1,36 +1,36 @@
# 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(npc_st)
- if npc_st.np_n >= npc_st.np_max or kind < 0 or kind >= NPCK_COUNT { return null }
+export function npc_spawn(kind: int) -> NpcPerson {
+ np_ensure()
+ if np_n >= np_max or kind < 0 or kind >= NPCK_COUNT { return null }
var p: NpcPerson = null
- for i in 0 .. len(npc_st.np_walkers) {
- if not npc_st.np_walkers[i].active {
- p = npc_st.np_walkers[i]
+ for i in 0 .. len(np_walkers) {
+ if not np_walkers[i].active {
+ p = np_walkers[i]
break
}
}
if p == null {
- if len(npc_st.np_walkers) >= npc_st.np_max { return null }
+ if len(np_walkers) >= np_max { return null }
p = new NpcPerson
- p.slot = len(npc_st.np_walkers)
- push(npc_st.np_walkers, p)
+ p.slot = len(np_walkers)
+ push(np_walkers, p)
}
- np_fresh(p, kind, npc_between(npc_st, 1, 99999))
- p.name = npc_pick_name(npc_st, p.body)
+ np_fresh(p, kind, npc_between(1, 99999))
+ p.name = npc_pick_name(p.body)
let k = NpcKinds[kind]
- p.leg = npc_between(npc_st, 0, 3)
- p.seed = npc_rnd(npc_st) * 40.0
- p.out_h = k.leave_lo + npc_rnd(npc_st) * (k.leave_hi - k.leave_lo)
- p.yaw = npc_rnd(npc_st) * NP_TAU
+ p.leg = npc_between(0, 3)
+ p.seed = npc_rnd() * 40.0
+ p.out_h = k.leave_lo + npc_rnd() * (k.leave_hi - k.leave_lo)
+ p.yaw = npc_rnd() * NP_TAU
if k.start != null {
let start = k.start
start(p)
- } else { npc_start_near(npc_st, p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
+ } else { npc_start_near(p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
p.active = true
- npc_st.np_n += 1
+ np_n += 1
NpcWorld.born(p)
- npc_plan(npc_st, p)
+ npc_plan(p)
return p
}
@@ -62,20 +62,20 @@ function np_fresh(p: NpcPerson, kind: int, look: int) -> void {
# its own ground: a spot r0 .. r1 from (cx, cz), tried a few times for one no player is within 60 m
# of, else near the place's origin; that is home, where it stands and faces
-export function npc_start_near(npc_st: mut NpcState, p: NpcPerson, cx: float, cz: float, r0: float, r1: float) -> void {
+export function npc_start_near(p: NpcPerson, cx: float, cz: float, r0: float, r1: float) -> void {
var x = NpcWorld.origin_x()
var z = NpcWorld.origin_z()
var ok = false
for t in 0 .. 8 {
- if not npc_spot_near(npc_st, cx, cz, r0, r1) { continue }
- x = npc_st.np_px
- z = npc_st.np_pz
+ if not npc_spot_near(cx, cz, r0, r1) { continue }
+ x = np_px
+ z = np_pz
ok = true
- if npc_near(npc_st, x, z) > 60.0 { break }
+ if npc_near(x, z) > 60.0 { break }
}
- if not ok and npc_spot_near(npc_st, x, z, 10.0, 45.0) {
- x = npc_st.np_px
- z = npc_st.np_pz
+ if not ok and npc_spot_near(x, z, 10.0, 45.0) {
+ x = np_px
+ z = np_pz
}
npc_set_at(p, x, z)
}
@@ -93,10 +93,10 @@ 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(npc_st)
+export function npc_place(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)
+ p.slot = len(np_posts)
np_fresh(p, kind, look)
p.fixed = true
p.name = name
@@ -105,16 +105,16 @@ export function npc_place(npc_st: mut NpcState, kind: int, x: float, z: float, y
p.yaw = yaw
npc_set_at(p, x, z)
p.active = true
- push(npc_st.np_posts, p)
+ push(np_posts, p)
NpcWorld.born(p)
- npc_do(npc_st, p, NPCA_IDLE, 3600.0)
+ npc_do(p, NPCA_IDLE, 3600.0)
return p
}
-export function npc_retire(npc_st: mut NpcState, p: NpcPerson) -> void {
+export function npc_retire(p: NpcPerson) -> void {
if not p.active { return }
p.active = false
- if not p.fixed and npc_st.np_n > 0 { npc_st.np_n -= 1 }
- np_left(npc_st, p.name)
+ if not p.fixed and np_n > 0 { np_n -= 1 }
+ np_left(p.name)
NpcWorld.gone(p)
}
diff --git a/packages/ludic.npc/ground.ludic b/packages/ludic.npc/ground.ludic
index 5ee6b103..e5f0a13e 100644
--- a/packages/ludic.npc/ground.ludic
+++ b/packages/ludic.npc/ground.ludic
@@ -1,11 +1,15 @@
# ground.ludic - where a person can stand and walk: dry, gentle, allowed; a straight walk with no
# water and no scramble on it; a spot in a ring, a spot toward something, and the nearest shore.
# A finder that succeeds leaves its answer in npc_px / npc_pz (and the water in npc_wx / npc_wz).
+var np_px: float = 0.0
+var np_pz: float = 0.0
+var np_wx: float = 0.0
+var np_wz: float = 0.0
-export function npc_px(npc_st: NpcState) -> float { return npc_st.np_px }
-export function npc_pz(npc_st: NpcState) -> float { return npc_st.np_pz }
-export function npc_wx(npc_st: NpcState) -> float { return npc_st.np_wx }
-export function npc_wz(npc_st: NpcState) -> float { return npc_st.np_wz }
+export function npc_px() -> float { return np_px }
+export function npc_pz() -> float { return np_pz }
+export function npc_wx() -> float { return np_wx }
+export function npc_wz() -> float { return np_wz }
export function npc_walkable(x: float, z: float) -> bool {
if not NpcWorld.allowed(x, z) { return false }
@@ -30,41 +34,41 @@ export function npc_line_ok(ax: float, az: float, bx: float, bz: float) -> bool
return true
}
-function np_found(npc_st: mut NpcState, x: float, z: float) -> bool {
- npc_st.np_px = x
- npc_st.np_pz = z
+function np_found(x: float, z: float) -> bool {
+ np_px = x
+ np_pz = z
return true
}
# somewhere to stand d0 .. d1 from a point
-export function npc_spot_near(npc_st: mut NpcState, x0: float, z0: float, d0: float, d1: float) -> bool {
+export function npc_spot_near(x0: float, z0: float, d0: float, d1: float) -> bool {
for t in 0 .. 40 {
- let ang = npc_rnd(npc_st) * NP_TAU
- let d = d0 + npc_rnd(npc_st) * (d1 - d0)
+ let ang = npc_rnd() * NP_TAU
+ let d = d0 + npc_rnd() * (d1 - d0)
let x = x0 + Math.cos(ang) * d
let z = z0 + Math.sin(ang) * d
- if npc_walkable(x, z) { return np_found(npc_st, x, z) }
+ if npc_walkable(x, z) { return np_found(x, z) }
}
return false
}
# a place to stand d from (ax, az), on the side (bx, bz) is on
-export function npc_toward(npc_st: mut NpcState, ax: float, az: float, bx: float, bz: float, d: float) -> bool {
+export function npc_toward(ax: float, az: float, bx: float, bz: float, d: float) -> bool {
let dd = Math.max(npc_d(ax, az, bx, bz), 0.5)
let x = ax + (bx - ax) / dd * d
let z = az + (bz - az) / dd * d
- if npc_walkable(x, z) { return np_found(npc_st, x, z) }
+ if npc_walkable(x, z) { return np_found(x, z) }
for k in 0 .. 12 {
let ang = Math.atan2(bz - az, bx - ax) + float((k + 1) / 2 * (1 - 2 * (k % 2))) * 0.25
let x2 = ax + Math.cos(ang) * d
let z2 = az + Math.sin(ang) * d
- if npc_walkable(x2, z2) { return np_found(npc_st, x2, z2) }
+ if npc_walkable(x2, z2) { return np_found(x2, z2) }
}
- return npc_spot_near(npc_st, x, z, 0.0, 14.0)
+ return npc_spot_near(x, z, 0.0, 14.0)
}
# the nearest bit of shore within rmax: where to stand, and the water in front of it
-export function npc_shore(npc_st: mut NpcState, x0: float, z0: float, rmax: float) -> bool {
+export function npc_shore(x0: float, z0: float, rmax: float) -> bool {
var found = false
var bd = rmax
for k in 0 .. 16 {
@@ -79,9 +83,9 @@ export function npc_shore(npc_st: mut NpcState, x0: float, z0: float, rmax: floa
if NpcWorld.ground(x, z) < NpcWorld.water(x, z) {
if ld > 0.0 and d - ld < 12.0 {
bd = d
- found = np_found(npc_st, lx, lz)
- npc_st.np_wx = x
- npc_st.np_wz = z
+ found = np_found(lx, lz)
+ np_wx = x
+ np_wz = z
}
break
}
diff --git a/packages/ludic.npc/mirror.ludic b/packages/ludic.npc/mirror.ludic
index d69530d7..a7bf613c 100644
--- a/packages/ludic.npc/mirror.ludic
+++ b/packages/ludic.npc/mirror.ludic
@@ -1,43 +1,44 @@
# mirror.ludic - a guest draws the host's people: no routine and no census here, a slot filled or
# emptied as the host says, and each one eased toward where it was last heard to be
+var np_clock: float = 0.0
# 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(npc_st)
- 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]) }
- if not on { npc_census_now(npc_st) }
+export function npc_mirror(on: bool) -> void {
+ np_ensure()
+ if np_mirror == on { return }
+ np_mirror = on
+ for i in 0 .. len(np_walkers) { npc_retire(np_walkers[i]) }
+ if not on { npc_census_now() }
}
-export function npc_mirroring(npc_st: NpcState) -> bool { return npc_st.np_mirror }
+export function npc_mirroring() -> bool { return np_mirror }
# 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(npc_st)
- 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 {
+export function npc_put(slot: int, id: int, kind: int, look: int, seed: float, name: string, hx: float, hz: float) -> NpcPerson {
+ np_ensure()
+ if slot < 0 or slot >= np_max or kind < 0 or kind >= NPCK_COUNT { return null }
+ while len(np_walkers) <= slot {
let q = new NpcPerson
- q.slot = len(npc_st.np_walkers)
- push(npc_st.np_walkers, q)
+ q.slot = len(np_walkers)
+ push(np_walkers, q)
}
- let p = npc_st.np_walkers[slot]
+ let p = np_walkers[slot]
if p.active and p.id == id { return p }
- if p.active { npc_retire(npc_st, p) }
+ if p.active { npc_retire(p) }
np_fresh(p, kind, look)
p.id = id
p.seed = seed
p.name = name
npc_set_at(p, hx, hz)
p.active = true
- npc_st.np_n += 1
+ np_n += 1
NpcWorld.born(p)
return p
}
# where the host says it is now, and what it is doing
-export function npc_heard(npc_st: mut NpcState, slot: int, x: float, y: float, z: float, yaw: float, speed: float, act: int, stalk: bool) -> void {
- let p = npc_at(npc_st, slot)
+export function npc_heard(slot: int, x: float, y: float, z: float, yaw: float, speed: float, act: int, stalk: bool) -> void {
+ let p = npc_at(slot)
if p == null or not p.active { return }
if not p.heard {
p.x = x
@@ -46,7 +47,7 @@ export function npc_heard(npc_st: mut NpcState, slot: int, x: float, y: float, z
p.yaw = yaw
}
p.heard = true
- p.heard_t = npc_st.np_clock
+ p.heard_t = np_clock
p.nx = x
p.ny = y
p.nz = z
@@ -57,15 +58,15 @@ export function npc_heard(npc_st: mut NpcState, slot: int, x: float, y: float, z
}
# heard from in the last six seconds: drawn; not, and it is not drawn until it is again
-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_heard_lately(p: NpcPerson) -> bool { return p.heard and np_clock - p.heard_t <= 6.0 }
-export function npc_mirror_tick(npc_st: mut NpcState, dt: float) -> void {
- np_ensure(npc_st)
- npc_st.np_clock = npc_st.np_clock + dt
+export function npc_mirror_tick(dt: float) -> void {
+ np_ensure()
+ np_clock = np_clock + dt
let k = Math.min(1.0, dt * 8.0)
- for i in 0 .. len(npc_st.np_walkers) {
- let p = npc_st.np_walkers[i]
- if not p.active or not npc_heard_lately(npc_st, p) { continue }
+ for i in 0 .. len(np_walkers) {
+ let p = np_walkers[i]
+ if not p.active or not npc_heard_lately(p) { continue }
let ox = p.x
let oz = p.z
p.x = p.x + (p.nx - p.x) * k
diff --git a/packages/ludic.npc/names.ludic b/packages/ludic.npc/names.ludic
index 777c02f3..30059694 100644
--- a/packages/ludic.npc/names.ludic
+++ b/packages/ludic.npc/names.ludic
@@ -1,43 +1,44 @@
# names.ludic - a name nobody else in the place is using: not another person, not one the place
# keeps for itself (NpcWorld.taken: a vendor), and not somebody who left a little while ago
+var np_recent: []string = null # the last few names that left
-export function npc_name_used(npc_st: mut NpcState, n: string) -> bool {
+export function npc_name_used(n: string) -> bool {
if NpcWorld.taken(n) { return true }
- let all = npc_walkers(npc_st)
+ let all = npc_walkers()
for i in 0 .. len(all) { if all[i].active and all[i].name == n { return true } }
- let posts = npc_posts(npc_st)
+ let posts = npc_posts()
for i in 0 .. len(posts) { if posts[i].active and posts[i].name == n { return true } }
- if npc_st.np_recent != null { for i in 0 .. len(npc_st.np_recent) { if npc_st.np_recent[i] == n { return true } } }
+ if np_recent != null { for i in 0 .. len(np_recent) { if np_recent[i] == n { return true } } }
return false
}
# a free name from the body's pool, from a place in it the dice choose ("" when the pool is empty)
-export function npc_pick_name(npc_st: mut NpcState, body: int) -> string {
+export function npc_pick_name(body: int) -> string {
let pool = new []string
for i in 0 .. NPCN_COUNT { if NpcNames[i].body == body { push(pool, NpcNames[i].name) } }
let n = len(pool)
if n == 0 { return "" }
- let start = npc_between(npc_st, 0, n - 1)
+ let start = npc_between(0, n - 1)
for k in 0 .. n {
let name = pool[(start + k) % n]
- if not npc_name_used(npc_st, name) { return name }
+ if not npc_name_used(name) { return name }
}
return pool[start]
}
-function np_left(npc_st: mut NpcState, name: string) -> void {
- if npc_st.np_recent == null { npc_st.np_recent = new []string }
- push(npc_st.np_recent, name)
- if len(npc_st.np_recent) <= 8 { return }
+function np_left(name: string) -> void {
+ if np_recent == null { np_recent = new []string }
+ push(np_recent, name)
+ if len(np_recent) <= 8 { return }
let keep = new []string
- for i in len(npc_st.np_recent) - 8 .. len(npc_st.np_recent) { push(keep, npc_st.np_recent[i]) }
- npc_st.np_recent = keep
+ for i in len(np_recent) - 8 .. len(np_recent) { push(keep, np_recent[i]) }
+ np_recent = keep
}
# how many pairs share a name, the place's own names included
-export function npc_name_clashes(npc_st: mut NpcState) -> int {
+export function npc_name_clashes() -> int {
var dup = 0
- let all = npc_walkers(npc_st)
+ let all = npc_walkers()
for i in 0 .. len(all) {
let p = all[i]
if not p.active { continue }
diff --git a/packages/ludic.npc/person.ludic b/packages/ludic.npc/person.ludic
index 102a4b4e..43e5c1a2 100644
--- a/packages/ludic.npc/person.ludic
+++ b/packages/ludic.npc/person.ludic
@@ -1,34 +1,23 @@
# person.ludic - the people: walkers in slots the census fills and empties, and the fixed ones at
# their posts; the package's own dice and its facts
-export state NpcState {
- np_px: float = 0.0
- np_pz: float = 0.0
- np_wx: float = 0.0
- np_wz: float = 0.0
- np_clock: float = 0.0
- np_recent: []string = null # the last few names that left
- np_walkers: []NpcPerson = null
- np_posts: []NpcPerson = null
- np_max: int = 10 # walkers at most
- np_n: int = 0 # walkers about
- np_dice: Rng = null
- np_facts: Queue = null
- np_next_h: float = 0.0 # the hour the census is next looked at
- np_hold: bool = false # held where they stand (a shot of their poses)
- np_mirror: bool = false # a guest drawing a host's: no routine runs here
- nw_ids: int = 0
- np_near_i: int = -1
- np_said: string = ""
-}
+var np_walkers: []NpcPerson = null
+var np_posts: []NpcPerson = null
+var np_max: int = 10 # walkers at most
+var np_n: int = 0 # walkers about
+var np_dice: Rng = null
+var np_facts: Queue = null
+var np_next_h: float = 0.0 # the hour the census is next looked at
+var np_hold: bool = false # held where they stand (a shot of their poses)
+var np_mirror: bool = false # a guest drawing a host's: no routine runs here
const NP_PI: float = 3.1415927
const NP_TAU: float = 6.2831853
-export function npc_facts(base_st: mut BaseState, npc_st: mut NpcState) -> Queue {
- if npc_st.np_facts == null { npc_st.np_facts = queue_new(base_st, "npc.facts") }
- return npc_st.np_facts
+export function npc_facts() -> Queue {
+ if np_facts == null { np_facts = queue_new("npc.facts") }
+ return np_facts
}
-function np_fact(base_st: mut BaseState, npc_st: mut NpcState, what: int, p: NpcPerson, player: int) -> NpcFact {
+function np_fact(what: int, p: NpcPerson, player: int) -> NpcFact {
let f = new NpcFact
f.what = what
f.person = p.id
@@ -36,68 +25,68 @@ function np_fact(base_st: mut BaseState, npc_st: mut NpcState, what: int, p: Npc
f.fixed = p.fixed
f.player = player
f.act = p.act
- q_push(base_st, npc_facts(base_st, npc_st), f)
+ q_push(npc_facts(), f)
return f
}
-function np_ensure(npc_st: mut NpcState) -> void {
- if npc_st.np_walkers == null { npc_st.np_walkers = new []NpcPerson }
- if npc_st.np_posts == null { npc_st.np_posts = new []NpcPerson }
- if npc_st.np_dice == null { npc_st.np_dice = rng_new(20260925) }
+function np_ensure() -> void {
+ if np_walkers == null { np_walkers = new []NpcPerson }
+ if np_posts == null { np_posts = new []NpcPerson }
+ if np_dice == null { np_dice = rng_new(20260925) }
}
# 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_config(max: int) -> void { np_max = max }
-export function npc_seed(npc_st: mut NpcState, n: int) -> void {
- np_ensure(npc_st)
- rng_seed(npc_st.np_dice, n)
+export function npc_seed(n: int) -> void {
+ np_ensure()
+ rng_seed(np_dice, n)
}
# 0 .. 1, and whole seconds lo .. hi, off the package's dice
-export function npc_rnd(npc_st: mut NpcState) -> float {
- np_ensure(npc_st)
- return rng_float(npc_st.np_dice)
+export function npc_rnd() -> float {
+ np_ensure()
+ return rng_float(np_dice)
}
-export function npc_between(npc_st: mut NpcState, lo: int, hi: int) -> int {
- np_ensure(npc_st)
- return rng_between(npc_st.np_dice, lo, hi)
+export function npc_between(lo: int, hi: int) -> int {
+ np_ensure()
+ return rng_between(np_dice, lo, hi)
}
-export function npc_secs(npc_st: mut NpcState, lo: int, hi: int) -> float { return float(npc_between(npc_st, lo, np_hi(lo, hi))) }
+export function npc_secs(lo: int, hi: int) -> float { return float(npc_between(lo, np_hi(lo, hi))) }
-export function npc_walkers(npc_st: mut NpcState) -> []NpcPerson {
- np_ensure(npc_st)
- return npc_st.np_walkers
+export function npc_walkers() -> []NpcPerson {
+ np_ensure()
+ return np_walkers
}
-export function npc_posts(npc_st: mut NpcState) -> []NpcPerson {
- np_ensure(npc_st)
- return npc_st.np_posts
+export function npc_posts() -> []NpcPerson {
+ np_ensure()
+ return np_posts
}
-export function npc_count(npc_st: NpcState) -> int { return npc_st.np_n }
+export function npc_count() -> int { return np_n }
-export function npc_max(npc_st: NpcState) -> int { return npc_st.np_max }
+export function npc_max() -> int { return np_max }
# a walker's slot or a post, and anyone by id
-export function npc_at(npc_st: mut NpcState, slot: int) -> NpcPerson {
- np_ensure(npc_st)
- if slot < 0 or slot >= len(npc_st.np_walkers) { return null }
- return npc_st.np_walkers[slot]
+export function npc_at(slot: int) -> NpcPerson {
+ np_ensure()
+ if slot < 0 or slot >= len(np_walkers) { return null }
+ return np_walkers[slot]
}
-export function npc_post(npc_st: mut NpcState, i: int) -> NpcPerson {
- np_ensure(npc_st)
- if i < 0 or i >= len(npc_st.np_posts) { return null }
- return npc_st.np_posts[i]
+export function npc_post(i: int) -> NpcPerson {
+ np_ensure()
+ if i < 0 or i >= len(np_posts) { return null }
+ return np_posts[i]
}
-export function npc_by_id(npc_st: mut NpcState, id: int) -> NpcPerson {
- np_ensure(npc_st)
- 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] } }
+export function npc_by_id(id: int) -> NpcPerson {
+ np_ensure()
+ for i in 0 .. len(np_walkers) { if np_walkers[i].active and np_walkers[i].id == id { return np_walkers[i] } }
+ for i in 0 .. len(np_posts) { if np_posts[i].active and np_posts[i].id == id { return np_posts[i] } }
return null
}
diff --git a/packages/ludic.npc/port.ludic b/packages/ludic.npc/port.ludic
index ca23bdce..e9a582ea 100644
--- a/packages/ludic.npc/port.ludic
+++ b/packages/ludic.npc/port.ludic
@@ -45,25 +45,27 @@ function nw_free(n: string) -> bool { return false }
function nw_nothing(p: NpcPerson) -> void { }
function nw_silent(p: NpcPerson, line: int) -> string { return "" }
function nw_hello(p: NpcPerson) -> string { return "" }
-function nw_next_id(npc_st: mut NpcState) -> int {
- npc_st.nw_ids += 1
- return npc_st.nw_ids
+var nw_ids: int = 0
+function nw_next_id() -> int {
+ nw_ids += 1
+ return nw_ids
}
# the nearest player on and their distance (npc_near_i: -1 when nobody is on)
+var np_near_i: int = -1
-export function npc_near(npc_st: mut NpcState, x: float, z: float) -> float {
+export function npc_near(x: float, z: float) -> float {
var bd = 1000000.0
- npc_st.np_near_i = -1
+ np_near_i = -1
for i in 0 .. NpcWorld.players() {
if not NpcWorld.player_on(i) { continue }
let d = npc_d(x, z, NpcWorld.player_x(i), NpcWorld.player_z(i))
if d < bd {
bd = d
- npc_st.np_near_i = i
+ np_near_i = i
}
}
return bd
}
-export function npc_near_i(npc_st: NpcState) -> int { return npc_st.np_near_i }
+export function npc_near_i() -> int { return np_near_i }
diff --git a/packages/ludic.npc/routine.ludic b/packages/ludic.npc/routine.ludic
index 9192880b..9a983037 100644
--- a/packages/ludic.npc/routine.ludic
+++ b/packages/ludic.npc/routine.ludic
@@ -1,7 +1,7 @@
# routine.ludic - going somewhere to do something, doing it here, and deciding what is next: the
# kind's own plan first, then a weighted draw among its steps for this hour, falling through to the
# next step whose spot can be found, and standing a while when none can
-export function npc_go(npc_st: mut NpcState, p: NpcPerson, x: float, z: float, act: int, dur: float) -> void {
+export function npc_go(p: NpcPerson, x: float, z: float, act: int, dur: float) -> void {
p.tx = x
p.tz = z
p.next_act = act
@@ -13,16 +13,16 @@ export function npc_go(npc_st: mut NpcState, p: NpcPerson, x: float, z: float, a
p.best = npc_d(p.x, p.z, x, z)
p.fx = x + (x - p.x)
p.fz = z + (z - p.z)
- p.speed = 1.1 + npc_rnd(npc_st) * 0.4
+ p.speed = 1.1 + npc_rnd() * 0.4
}
# whatever it is, it is not done where the place says nobody stops: walk out of there first
-export function npc_do(npc_st: mut NpcState, p: NpcPerson, act: int, dur: float) -> void {
+export function npc_do(p: NpcPerson, act: int, dur: float) -> void {
if act != NPCA_WALK and not p.fixed and not NpcWorld.allowed(p.x, p.z) {
- if npc_spot_near(npc_st, p.x, p.z, 20.0, 60.0) {
+ if npc_spot_near(p.x, p.z, 20.0, 60.0) {
let fx = p.fx
let fz = p.fz
- npc_go(npc_st, p, npc_st.np_px, npc_st.np_pz, act, dur)
+ npc_go(p, np_px, np_pz, act, dur)
if act != NPCA_IDLE {
p.fx = fx
p.fz = fz
@@ -58,10 +58,10 @@ function np_in_hours(s: NpcStep, h: float) -> bool {
}
# what next: the kind's plan, then its steps for this hour
-export function npc_plan(npc_st: mut NpcState, p: NpcPerson) -> void {
+export function npc_plan(p: NpcPerson) -> void {
p.stalk = false
if p.fixed {
- npc_do(npc_st, p, NPCA_IDLE, 3600.0)
+ npc_do(p, NPCA_IDLE, 3600.0)
return
}
let k = NpcKinds[p.kind]
@@ -69,19 +69,19 @@ export function npc_plan(npc_st: mut NpcState, p: NpcPerson) -> void {
let plan = k.plan
if plan(p) { return }
}
- if k.steps != null and npc_steps(npc_st, p, k.steps) { return }
+ if k.steps != null and npc_steps(p, k.steps) { return }
npc_face_ahead(p)
- npc_do(npc_st, p, NPCA_IDLE, 10.0)
+ npc_do(p, NPCA_IDLE, 10.0)
}
# a weighted draw among the steps whose hours hold now; a step whose spot cannot be found
# gives way to the next one in the table, round to the start
-export function npc_steps(npc_st: mut NpcState, p: NpcPerson, steps: []NpcStep) -> bool {
+export function npc_steps(p: NpcPerson, steps: []NpcStep) -> bool {
let h = NpcWorld.hour()
var total = 0
for i in 0 .. len(steps) { if np_in_hours(steps[i], h) { total += np_pos(steps[i].weight) } }
if total <= 0 { return false }
- var r = npc_between(npc_st, 0, total - 1)
+ var r = npc_between(0, total - 1)
var first = -1
for i in 0 .. len(steps) {
if not np_in_hours(steps[i], h) { continue }
diff --git a/packages/ludic.npc/spots.ludic b/packages/ludic.npc/spots.ludic
index c6951ed2..424de009 100644
--- a/packages/ludic.npc/spots.ludic
+++ b/packages/ludic.npc/spots.ludic
@@ -10,71 +10,71 @@ def NpcSpots shore_here { find: fn np_spot_shore_here }
def NpcSpots wander { find: fn np_spot_wander }
# the step's act for the step's time, at (x, z)
-export function npc_step_go(npc_st: mut NpcState, p: NpcPerson, s: NpcStep, x: float, z: float) -> void {
- npc_go(npc_st, p, x, z, s.act, npc_secs(npc_st, s.lo, s.hi))
+export function npc_step_go(p: NpcPerson, s: NpcStep, x: float, z: float) -> void {
+ npc_go(p, x, z, s.act, npc_secs(s.lo, s.hi))
}
-function np_spot_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
+function np_spot_here(p: NpcPerson, s: NpcStep) -> bool {
npc_face_ahead(p)
- npc_do(npc_st, p, s.act, npc_secs(npc_st, s.lo, s.hi))
+ npc_do(p, s.act, npc_secs(s.lo, s.hi))
return true
}
-function np_spot_home(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
+function np_spot_home(p: NpcPerson, s: NpcStep) -> bool {
var fx = p.hx - Math.sin(p.yaw) * 5.0
var fz = p.hz - Math.cos(p.yaw) * 5.0
if p.home_face {
fx = p.hfx
fz = p.hfz
}
- if npc_d(p.x, p.z, p.hx, p.hz) < 2.0 { npc_do(npc_st, p, s.act, npc_secs(npc_st, s.lo, s.hi)) } else { npc_step_go(npc_st, p, s, p.hx, p.hz) }
+ if npc_d(p.x, p.z, p.hx, p.hz) < 2.0 { npc_do(p, s.act, npc_secs(s.lo, s.hi)) } else { npc_step_go(p, s, p.hx, p.hz) }
npc_face(p, fx, fz)
return true
}
-function np_spot_near_home(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
- if not npc_spot_near(npc_st, p.hx, p.hz, s.near, s.far) { return false }
- npc_step_go(npc_st, p, s, npc_st.np_px, npc_st.np_pz)
+function np_spot_near_home(p: NpcPerson, s: NpcStep) -> bool {
+ if not npc_spot_near(p.hx, p.hz, s.near, s.far) { return false }
+ npc_step_go(p, s, np_px, np_pz)
return true
}
-function np_spot_near_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
- if not npc_spot_near(npc_st, p.x, p.z, s.near, s.far) { return false }
- npc_step_go(npc_st, p, s, npc_st.np_px, npc_st.np_pz)
+function np_spot_near_here(p: NpcPerson, s: NpcStep) -> bool {
+ if not npc_spot_near(p.x, p.z, s.near, s.far) { return false }
+ npc_step_go(p, s, np_px, np_pz)
return true
}
# at the water's edge, facing out over it
-function np_spot_shore(npc_st: mut NpcState, p: NpcPerson, s: NpcStep, x: float, z: float) -> bool {
- if not npc_shore(npc_st, x, z, s.far) { return false }
- let wx = npc_st.np_wx
- let wz = npc_st.np_wz
- npc_step_go(npc_st, p, s, npc_st.np_px, npc_st.np_pz)
+function np_spot_shore(p: NpcPerson, s: NpcStep, x: float, z: float) -> bool {
+ if not npc_shore(x, z, s.far) { return false }
+ let wx = np_wx
+ let wz = np_wz
+ npc_step_go(p, s, np_px, np_pz)
npc_face(p, wx, wz)
return true
}
-function np_spot_shore_home(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(npc_st, p, s, p.hx, p.hz) }
+function np_spot_shore_home(p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(p, s, p.hx, p.hz) }
-function np_spot_shore_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(npc_st, p, s, p.x, p.z) }
+function np_spot_shore_here(p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(p, s, p.x, p.z) }
-function np_spot_wander(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
- npc_wander(npc_st, p)
+function np_spot_wander(p: NpcPerson, s: NpcStep) -> bool {
+ npc_wander(p)
return true
}
# a few steps somewhere that can actually be walked to, after being stuck
-export function npc_wander(npc_st: mut NpcState, p: NpcPerson) -> void {
+export function npc_wander(p: NpcPerson) -> void {
for t in 0 .. 16 {
- let ang = npc_rnd(npc_st) * NP_TAU
- let d = 8.0 + npc_rnd(npc_st) * 18.0
+ let ang = npc_rnd() * NP_TAU
+ let d = 8.0 + npc_rnd() * 18.0
let x = p.x + Math.cos(ang) * d
let z = p.z + Math.sin(ang) * d
if npc_walkable(x, z) and npc_line_ok(p.x, p.z, x, z) {
- npc_go(npc_st, p, x, z, NPCA_IDLE, npc_secs(npc_st, 5, 12))
+ npc_go(p, x, z, NPCA_IDLE, npc_secs(5, 12))
return
}
}
npc_face_ahead(p)
- npc_do(npc_st, p, NPCA_IDLE, 8.0)
+ npc_do(p, NPCA_IDLE, 8.0)
}
diff --git a/packages/ludic.npc/talk.ludic b/packages/ludic.npc/talk.ludic
index 947fb3ac..a977a357 100644
--- a/packages/ludic.npc/talk.ludic
+++ b/packages/ludic.npc/talk.ludic
@@ -20,6 +20,7 @@ export property NpcLine {
export open registry NpcLines of NpcLine as NPCL
+var np_said: string = ""
function np_line_fits(p: NpcPerson, l: NpcLine) -> bool {
return (l.kind < 0 or l.kind == p.kind) and (l.act < 0 or l.act == p.act)
@@ -33,8 +34,8 @@ export function npc_words(p: NpcPerson, line: int) -> string {
}
# the line it would say now (-1: none), its words kept for npc_said()
-export function npc_line_for(npc_st: mut NpcState, p: NpcPerson) -> int {
- npc_st.np_said = ""
+export function npc_line_for(p: NpcPerson) -> int {
+ np_said = ""
var top = -1000000
for i in 0 .. NPCL_COUNT { if NpcLines[i].prio > top { top = NpcLines[i].prio } }
var prio = top
@@ -44,10 +45,10 @@ export function npc_line_for(npc_st: mut NpcState, p: NpcPerson) -> int {
let l = NpcLines[i]
if l.prio < prio and l.prio > lower { lower = l.prio }
if l.prio != prio or not np_line_fits(p, l) { continue }
- if l.chance < 100 and npc_between(npc_st, 0, 99) >= l.chance { continue }
+ if l.chance < 100 and npc_between(0, 99) >= l.chance { continue }
let w = npc_words(p, i)
if len(w) > 0 {
- npc_st.np_said = w
+ np_said = w
return i
}
}
@@ -56,13 +57,13 @@ export function npc_line_for(npc_st: mut NpcState, p: NpcPerson) -> int {
return -1
}
-export function npc_said(npc_st: NpcState) -> string { return npc_st.np_said }
+export function npc_said() -> string { return np_said }
# a player talks to them: the line (-1: nothing to say), its words in npc_said(), a Talked fact,
# and `said` marked for the day
-export function npc_talk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, player: int) -> int {
- let line = npc_line_for(npc_st, p)
- let f = np_fact(base_st, npc_st, NPC_F_TALKED, p, player)
+export function npc_talk(p: NpcPerson, player: int) -> int {
+ let line = npc_line_for(p)
+ let f = np_fact(NPC_F_TALKED, p, player)
f.line = line
p.said = NpcWorld.day()
return line
@@ -70,16 +71,16 @@ export function npc_talk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPer
# the answer to choice c of a line: the line it leads to (-1: the talk is over), its words in
# npc_said(), and a Talked fact carrying the choice's code
-export function npc_choose(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, line: int, c: int, player: int) -> int {
- npc_st.np_said = ""
+export function npc_choose(p: NpcPerson, line: int, c: int, player: int) -> int {
+ np_said = ""
if line < 0 or line >= NPCL_COUNT { return -1 }
let cs = NpcLines[line].choices
if cs == null or c < 0 or c >= len(cs) { return -1 }
let next = npc_lines_find(cs[c].line)
- let f = np_fact(base_st, npc_st, NPC_F_TALKED, p, player)
+ let f = np_fact(NPC_F_TALKED, p, player)
f.line = next
f.choice = cs[c].code
- if next >= 0 { npc_st.np_said = npc_words(p, next) }
+ if next >= 0 { np_said = npc_words(p, next) }
return next
}
diff --git a/packages/ludic.npc/tests/fake/index.ludic b/packages/ludic.npc/tests/fake/index.ludic
index c13ffd91..c335a945 100644
--- a/packages/ludic.npc/tests/fake/index.ludic
+++ b/packages/ludic.npc/tests/fake/index.ludic
@@ -1,32 +1,30 @@
# fake - a place for the people: dry flat ground east of x = -50 and a lake west of it, a camp at
# (40, 0) nobody may stop in, two players, and what the port was asked to draw
numbers float
-export state NpcTestsFakeState {
- fk_hour: float = 12.0
- fk_day: int = 1
- fk_want: int = 0
- fk_extra: int = -1
- fk_px: []float = null
- fk_pz: []float = null
- fk_on: []bool = null
- fk_born: int = 0
- fk_gone: int = 0
-}
+var fk_hour: float = 12.0
+var fk_day: int = 1
+var fk_want: int = 0
+var fk_extra: int = -1
+var fk_px: []float = null
+var fk_pz: []float = null
+var fk_on: []bool = null
+var fk_born: int = 0
+var fk_gone: int = 0
-function fk_setup(base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) -> void {
- npc_tests_fake_st.fk_hour = 12.0
- npc_tests_fake_st.fk_day = 1
- npc_tests_fake_st.fk_want = 0
- npc_tests_fake_st.fk_extra = -1
- npc_tests_fake_st.fk_px = [500.0, 900.0]
- npc_tests_fake_st.fk_pz = [500.0, 900.0]
- npc_tests_fake_st.fk_on = [true, false]
- npc_tests_fake_st.fk_born = 0
- npc_tests_fake_st.fk_gone = 0
- npc_clear(npc_st)
- npc_reset(base_st, npc_st)
- npc_seed(npc_st, 7)
- npc_config(npc_st, 10)
+function fk_setup() -> void {
+ fk_hour = 12.0
+ fk_day = 1
+ fk_want = 0
+ fk_extra = -1
+ fk_px = [500.0, 900.0]
+ fk_pz = [500.0, 900.0]
+ fk_on = [true, false]
+ fk_born = 0
+ fk_gone = 0
+ npc_clear()
+ npc_reset()
+ npc_seed(7)
+ npc_config(10)
}
function fk_ground(x: float, z: float) -> float { return 0.0 }
@@ -35,28 +33,28 @@ function fk_water(x: float, z: float) -> float {
return -100.0
}
function fk_allowed(x: float, z: float) -> bool { return npc_d(x, z, 40.0, 0.0) > 15.0 }
-function fk_hour_now(npc_tests_fake_st: NpcTestsFakeState) -> float { return npc_tests_fake_st.fk_hour }
-function fk_day_now(npc_tests_fake_st: NpcTestsFakeState) -> int { return npc_tests_fake_st.fk_day }
+function fk_hour_now() -> float { return fk_hour }
+function fk_day_now() -> int { return fk_day }
function fk_players() -> int { return 2 }
-function fk_player_on(npc_tests_fake_st: NpcTestsFakeState, i: int) -> bool { return npc_tests_fake_st.fk_on[i] }
-function fk_x(npc_tests_fake_st: NpcTestsFakeState, i: int) -> float { return npc_tests_fake_st.fk_px[i] }
-function fk_z(npc_tests_fake_st: NpcTestsFakeState, i: int) -> float { return npc_tests_fake_st.fk_pz[i] }
-function fk_wanted(npc_tests_fake_st: NpcTestsFakeState) -> int { return npc_tests_fake_st.fk_want }
-function fk_extra_kind(npc_tests_fake_st: NpcTestsFakeState) -> int { return npc_tests_fake_st.fk_extra }
+function fk_player_on(i: int) -> bool { return fk_on[i] }
+function fk_x(i: int) -> float { return fk_px[i] }
+function fk_z(i: int) -> float { return fk_pz[i] }
+function fk_wanted() -> int { return fk_want }
+function fk_extra_kind() -> int { return fk_extra }
function fk_taken(n: string) -> bool { return n == "Ada" }
-function fk_on_born(npc_tests_fake_st: mut NpcTestsFakeState, p: NpcPerson) -> void { npc_tests_fake_st.fk_born += 1 }
-function fk_on_gone(npc_tests_fake_st: mut NpcTestsFakeState, p: NpcPerson) -> void { npc_tests_fake_st.fk_gone += 1 }
+function fk_on_born(p: NpcPerson) -> void { fk_born += 1 }
+function fk_on_gone(p: NpcPerson) -> void { fk_gone += 1 }
-function fk_player_at(npc_tests_fake_st: mut NpcTestsFakeState, x: float, z: float) -> void {
- npc_tests_fake_st.fk_px[0] = x
- npc_tests_fake_st.fk_pz[0] = z
+function fk_player_at(x: float, z: float) -> void {
+ fk_px[0] = x
+ fk_pz[0] = z
}
# seconds of the place, in twentieths
-function fk_run(base_st: mut BaseState, npc_st: mut NpcState, secs: float) -> void { for k in 0 .. int(secs * 20.0) { npc_tick(base_st, npc_st, 0.05) } }
+function fk_run(secs: float) -> void { for k in 0 .. int(secs * 20.0) { npc_tick(0.05) } }
-function fk_count(base_st: mut BaseState, npc_st: mut NpcState, what: int) -> int {
- let fs = q_drain(base_st, npc_facts(base_st, npc_st))
+function fk_count(what: int) -> int {
+ let fs = q_drain(npc_facts())
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
diff --git a/packages/ludic.npc/tests/npc_test.ludic b/packages/ludic.npc/tests/npc_test.ludic
index 19fdc93f..760619d4 100644
--- a/packages/ludic.npc/tests/npc_test.ludic
+++ b/packages/ludic.npc/tests/npc_test.ludic
@@ -30,20 +30,18 @@ program NpcTest {
def NpcNames from "names.lres"
def NpcLines from "lines.lres"
- state NpctestState {
- news_on: bool = false
- }
- function tk_say(npctest_st: NpctestState, p: NpcPerson, line: int) -> string {
- if npctest_st.news_on { return `{p.name} saw a bear.` }
+ var news_on: bool = false
+ function tk_say(p: NpcPerson, line: int) -> string {
+ if news_on { return `{p.name} saw a bear.` }
return ""
}
function tk_greeting(p: NpcPerson) -> string { return "What'll it be?" }
- function tk_start(npc_st: mut NpcState, p: NpcPerson) -> void { npc_start_near(npc_st, p, 0.0, 0.0, 0.0, 10.0) }
+ function tk_start(p: NpcPerson) -> void { npc_start_near(p, 0.0, 0.0, 0.0, 10.0) }
- function one(base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, kind: int) -> NpcPerson {
- fk_setup(base_st, npc_st, npc_tests_fake_st)
- npc_tests_fake_st.fk_want = 1
- let p = npc_spawn(npc_st, kind)
+ function one(kind: int) -> NpcPerson {
+ fk_setup()
+ fk_want = 1
+ let p = npc_spawn(kind)
npc_set_at(p, 0.0, 0.0)
return p
}
@@ -58,175 +56,175 @@ program NpcTest {
expect_eq(NPCN_COUNT, 14)
}
- test "the census fills to what the place wants, by weight, and after dark only who stays out" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- fk_setup(base_st, npc_st, npc_tests_fake_st)
- npc_tests_fake_st.fk_want = 4
- npc_tick(base_st, npc_st, 0.05)
- expect_eq(npc_count(npc_st), 4)
- expect_eq(npc_tests_fake_st.fk_born, 4)
- expect_eq(npc_name_clashes(npc_st), 0)
- for i in 0 .. len(npc_walkers(npc_st)) { expect(npc_walkers(npc_st)[i].kind != NPCK_VENDOR) }
- fk_setup(base_st, npc_st, npc_tests_fake_st)
- npc_tests_fake_st.fk_want = 3
- npc_tests_fake_st.fk_hour = 23.0
- npc_tick(base_st, npc_st, 0.05)
- expect_eq(npc_count(npc_st), 3)
- for i in 0 .. len(npc_walkers(npc_st)) { expect_eq(npc_walkers(npc_st)[i].kind, NPCK_CAMPER) }
+ test "the census fills to what the place wants, by weight, and after dark only who stays out" {
+ fk_setup()
+ fk_want = 4
+ npc_tick(0.05)
+ expect_eq(npc_count(), 4)
+ expect_eq(fk_born, 4)
+ expect_eq(npc_name_clashes(), 0)
+ for i in 0 .. len(npc_walkers()) { expect(npc_walkers()[i].kind != NPCK_VENDOR) }
+ fk_setup()
+ fk_want = 3
+ fk_hour = 23.0
+ npc_tick(0.05)
+ expect_eq(npc_count(), 3)
+ for i in 0 .. len(npc_walkers()) { expect_eq(npc_walkers()[i].kind, NPCK_CAMPER) }
}
- test "no name twice, and never one the place keeps" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- fk_setup(base_st, npc_st, npc_tests_fake_st)
- npc_tests_fake_st.fk_want = 6
- npc_tick(base_st, npc_st, 0.05)
- expect_eq(npc_count(npc_st), 6)
- expect_eq(npc_name_clashes(npc_st), 0)
- for i in 0 .. 6 { expect(npc_walkers(npc_st)[i].name != "Ada") }
+ test "no name twice, and never one the place keeps" {
+ fk_setup()
+ fk_want = 6
+ npc_tick(0.05)
+ expect_eq(npc_count(), 6)
+ expect_eq(npc_name_clashes(), 0)
+ for i in 0 .. 6 { expect(npc_walkers()[i].name != "Ada") }
}
- test "it walks to where it is going and begins there, facing what it came for" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
- npc_go(npc_st, p, 20.0, 0.0, NPCA_SIT, 30.0)
+ test "it walks to where it is going and begins there, facing what it came for" {
+ let p = one(NPCK_HIKER)
+ npc_go(p, 20.0, 0.0, NPCA_SIT, 30.0)
npc_face(p, 30.0, 0.0)
- fk_run(base_st, npc_st, 30.0)
+ fk_run(30.0)
expect_eq(p.act, NPCA_SIT)
expect(npc_d(p.x, p.z, 20.0, 0.0) < 1.3)
expect(p.phase > 20.0)
- expect_eq(fk_count(base_st, npc_st, NPC_F_ARRIVED), 1)
+ expect_eq(fk_count(NPC_F_ARRIVED), 1)
expect_near(p.fx, 30.0, 0.01)
}
- test "nobody walks into the lake" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
- npc_go(npc_st, p, -80.0, 0.0, NPCA_SIT, 30.0)
+ test "nobody walks into the lake" {
+ let p = one(NPCK_HIKER)
+ npc_go(p, -80.0, 0.0, NPCA_SIT, 30.0)
var west = 0.0
for k in 0 .. 1200 {
- npc_tick(base_st, npc_st, 0.05)
+ npc_tick(0.05)
west = Math.min(west, p.x)
}
expect(west > -50.5)
}
- test "a step's spot is where its hours say: the shore by day, home by night" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_CAMPER)
+ test "a step's spot is where its hours say: the shore by day, home by night" {
+ let p = one(NPCK_CAMPER)
npc_set_at(p, -20.0, 0.0)
- npc_tests_fake_st.fk_hour = 12.0
- npc_plan(npc_st, p)
+ fk_hour = 12.0
+ npc_plan(p)
expect_eq(p.act, NPCA_WALK)
expect_eq(p.next_act, NPCA_FISH)
expect(p.fx < -50.0)
- npc_tests_fake_st.fk_hour = 23.0
- npc_plan(npc_st, p)
+ fk_hour = 23.0
+ npc_plan(p)
expect_eq(p.act, NPCA_SIT)
}
- test "an act is not begun where the place says nobody stops" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
+ test "an act is not begun where the place says nobody stops" {
+ let p = one(NPCK_HIKER)
npc_set_at(p, 40.0, 0.0)
- npc_do(npc_st, p, NPCA_SIT, 30.0)
+ npc_do(p, NPCA_SIT, 30.0)
expect_eq(p.act, NPCA_WALK)
expect(fk_allowed(p.tx, p.tz))
}
- test "it turns to a player who comes near, greets them once, and again after they have gone" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
- npc_do(npc_st, p, NPCA_IDLE, 100.0)
+ test "it turns to a player who comes near, greets them once, and again after they have gone" {
+ let p = one(NPCK_HIKER)
+ npc_do(p, NPCA_IDLE, 100.0)
p.yaw = 0.0
- fk_player_at(npc_tests_fake_st, 0.0, 0.0 - 5.0)
- fk_run(base_st, npc_st, 2.0)
+ fk_player_at(0.0, 0.0 - 5.0)
+ fk_run(2.0)
expect(Math.abs(p.yaw) < 0.05)
- fk_player_at(npc_tests_fake_st, 5.0, 0.0)
- fk_run(base_st, npc_st, 3.0)
+ fk_player_at(5.0, 0.0)
+ fk_run(3.0)
expect(Math.abs(npc_wrap(p.yaw + 1.5707963)) < 0.05)
- fk_run(base_st, npc_st, 1.0)
- expect_eq(fk_count(base_st, npc_st, NPC_F_GREETED), 1)
- fk_player_at(npc_tests_fake_st, 40.0, 0.0)
- fk_run(base_st, npc_st, 0.5)
- fk_player_at(npc_tests_fake_st, 5.0, 0.0)
- fk_run(base_st, npc_st, 0.5)
- expect_eq(fk_count(base_st, npc_st, NPC_F_GREETED), 1)
+ fk_run(1.0)
+ expect_eq(fk_count(NPC_F_GREETED), 1)
+ fk_player_at(40.0, 0.0)
+ fk_run(0.5)
+ fk_player_at(5.0, 0.0)
+ fk_run(0.5)
+ expect_eq(fk_count(NPC_F_GREETED), 1)
}
- test "a fisher keeps its eyes on the water, whoever is near" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
- npc_do(npc_st, p, NPCA_FISH, 100.0)
+ test "a fisher keeps its eyes on the water, whoever is near" {
+ let p = one(NPCK_HIKER)
+ npc_do(p, NPCA_FISH, 100.0)
p.yaw = 0.0
npc_face(p, 0.0, -10.0)
- fk_player_at(npc_tests_fake_st, 5.0, 0.0)
- fk_run(base_st, npc_st, 2.0)
+ fk_player_at(5.0, 0.0)
+ fk_run(2.0)
expect(Math.abs(p.yaw) < 0.05)
}
- test "the day ends out of sight, and not in it" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
- npc_do(npc_st, p, NPCA_IDLE, 1000.0)
- npc_tests_fake_st.fk_hour = 22.5
- fk_player_at(npc_tests_fake_st, 20.0, 0.0)
- fk_run(base_st, npc_st, 0.5)
+ test "the day ends out of sight, and not in it" {
+ let p = one(NPCK_HIKER)
+ npc_do(p, NPCA_IDLE, 1000.0)
+ fk_hour = 22.5
+ fk_player_at(20.0, 0.0)
+ fk_run(0.5)
expect(p.active)
- fk_player_at(npc_tests_fake_st, 500.0, 0.0)
- fk_run(base_st, npc_st, 0.5)
+ fk_player_at(500.0, 0.0)
+ fk_run(0.5)
expect(not p.active)
- expect_eq(npc_tests_fake_st.fk_gone, 1)
- let c = one(base_st, npc_st, npc_tests_fake_st, NPCK_CAMPER)
- npc_do(npc_st, c, NPCA_IDLE, 1000.0)
- npc_tests_fake_st.fk_hour = 22.5
- fk_run(base_st, npc_st, 0.5)
+ expect_eq(fk_gone, 1)
+ let c = one(NPCK_CAMPER)
+ npc_do(c, NPCA_IDLE, 1000.0)
+ fk_hour = 22.5
+ fk_run(0.5)
expect(c.active)
}
- test "lines: the highest that has something to say, the story's words, and choices" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, npctest_st: mut NpctestState) {
- let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_CAMPER)
- npc_do(npc_st, p, NPCA_FISH, 100.0)
- expect_eq(npc_talk(base_st, npc_st, p, 0), NPCL_HUSH)
- expect(npc_said(npc_st) == "Shh.")
+ test "lines: the highest that has something to say, the story's words, and choices" {
+ let p = one(NPCK_CAMPER)
+ npc_do(p, NPCA_FISH, 100.0)
+ expect_eq(npc_talk(p, 0), NPCL_HUSH)
+ expect(npc_said() == "Shh.")
expect(npc_talked_today(p))
- npc_do(npc_st, p, NPCA_IDLE, 100.0)
- expect_eq(npc_line_for(npc_st, p), NPCL_HELLO)
- npctest_st.news_on = true
- expect_eq(npc_line_for(npc_st, p), NPCL_NEWS)
- expect(npc_said(npc_st) == `{p.name} saw a bear.`)
- npctest_st.news_on = false
- expect_eq(npc_choose(base_st, npc_st, p, NPCL_HELLO, 0, 0), NPCL_FISH_NO)
- expect(npc_said(npc_st) == "Not today.")
- expect_eq(npc_choose(base_st, npc_st, p, NPCL_HELLO, 1, 0), -1)
- let fs = q_drain(base_st, npc_facts(base_st, npc_st))
+ npc_do(p, NPCA_IDLE, 100.0)
+ expect_eq(npc_line_for(p), NPCL_HELLO)
+ news_on = true
+ expect_eq(npc_line_for(p), NPCL_NEWS)
+ expect(npc_said() == `{p.name} saw a bear.`)
+ news_on = false
+ expect_eq(npc_choose(p, NPCL_HELLO, 0, 0), NPCL_FISH_NO)
+ expect(npc_said() == "Not today.")
+ expect_eq(npc_choose(p, NPCL_HELLO, 1, 0), -1)
+ let fs = q_drain(npc_facts())
expect_eq(len(fs), 3)
expect_eq(fs[1].choice, 7)
expect(npc_greeting(p) == "What'll it be?")
}
- test "a post stands where it was put, is not counted, and turns back when nobody is near" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- fk_setup(base_st, npc_st, npc_tests_fake_st)
- let v = npc_place(npc_st, NPCK_VENDOR, 0.0, 0.0, 0.0, "Mateo", 5)
+ test "a post stands where it was put, is not counted, and turns back when nobody is near" {
+ fk_setup()
+ let v = npc_place(NPCK_VENDOR, 0.0, 0.0, 0.0, "Mateo", 5)
expect(v.fixed)
- expect_eq(npc_count(npc_st), 0)
- fk_player_at(npc_tests_fake_st, 8.0, 0.0)
- fk_run(base_st, npc_st, 3.0)
+ expect_eq(npc_count(), 0)
+ fk_player_at(8.0, 0.0)
+ fk_run(3.0)
expect(Math.abs(npc_wrap(v.yaw + 1.5707963)) < 0.05)
- fk_player_at(npc_tests_fake_st, 300.0, 0.0)
- fk_run(base_st, npc_st, 3.0)
+ fk_player_at(300.0, 0.0)
+ fk_run(3.0)
expect(Math.abs(v.yaw) < 0.05)
expect(npc_d(v.x, v.z, 0.0, 0.0) < 0.001)
- expect(npc_by_id(npc_st, v.id).name == "Mateo")
+ expect(npc_by_id(v.id).name == "Mateo")
}
- test "a guest's copy: the host's slot put, heard and eased toward, and not drawn once it goes quiet" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
- fk_setup(base_st, npc_st, npc_tests_fake_st)
- npc_tests_fake_st.fk_want = 2
- npc_tick(base_st, npc_st, 0.05)
- npc_mirror(npc_st, true)
- expect_eq(npc_count(npc_st), 0)
- let p = npc_put(npc_st, 3, 99, NPCK_HIKER, 12, 1.5, "Wren", 10.0, 10.0)
+ test "a guest's copy: the host's slot put, heard and eased toward, and not drawn once it goes quiet" {
+ fk_setup()
+ fk_want = 2
+ npc_tick(0.05)
+ npc_mirror(true)
+ expect_eq(npc_count(), 0)
+ let p = npc_put(3, 99, NPCK_HIKER, 12, 1.5, "Wren", 10.0, 10.0)
expect(p.active)
- expect_eq(npc_count(npc_st), 1)
- npc_heard(npc_st, 3, 10.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
- npc_heard(npc_st, 3, 12.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
- for k in 0 .. 40 { npc_mirror_tick(npc_st, 0.05) }
+ expect_eq(npc_count(), 1)
+ npc_heard(3, 10.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
+ npc_heard(3, 12.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
+ for k in 0 .. 40 { npc_mirror_tick(0.05) }
expect_near(p.x, 12.0, 0.01)
expect(p.phase > 0.0)
- npc_tick(base_st, npc_st, 0.05)
+ npc_tick(0.05)
expect_near(p.x, 12.0, 0.01)
- for k in 0 .. 140 { npc_mirror_tick(npc_st, 0.05) }
- expect(not npc_heard_lately(npc_st, p))
+ for k in 0 .. 140 { npc_mirror_tick(0.05) }
+ expect(not npc_heard_lately(p))
}
}
diff --git a/packages/ludic.npc/tick.ludic b/packages/ludic.npc/tick.ludic
index 246e284a..d04b4fa4 100644
--- a/packages/ludic.npc/tick.ludic
+++ b/packages/ludic.npc/tick.ludic
@@ -1,53 +1,53 @@
# 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(base_st: mut BaseState, npc_st: mut NpcState, dt: float) -> void {
- np_ensure(npc_st)
- for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(base_st, npc_st, npc_st.np_posts[i], dt) } }
- if npc_st.np_hold or npc_st.np_mirror { return }
- np_census(npc_st)
- for i in 0 .. len(npc_st.np_walkers) {
- let p = npc_st.np_walkers[i]
- if not p.active or np_leaves(npc_st, p) { continue }
- if p.act == NPCA_WALK { npc_walk(base_st, npc_st, p, dt) } else { npc_act(npc_st, p, dt) }
- npc_notice(base_st, npc_st, p)
+export function npc_tick(dt: float) -> void {
+ np_ensure()
+ for i in 0 .. len(np_posts) { if np_posts[i].active { np_step_post(np_posts[i], dt) } }
+ if np_hold or np_mirror { return }
+ np_census()
+ for i in 0 .. len(np_walkers) {
+ let p = np_walkers[i]
+ if not p.active or np_leaves(p) { continue }
+ if p.act == NPCA_WALK { npc_walk(p, dt) } else { npc_act(p, dt) }
+ npc_notice(p)
}
}
# the posts alone: a machine that does not run the place still turns its vendors to whoever comes
-export function npc_posts_tick(base_st: mut BaseState, npc_st: mut NpcState, dt: float) -> void {
- np_ensure(npc_st)
- for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(base_st, npc_st, npc_st.np_posts[i], dt) } }
+export function npc_posts_tick(dt: float) -> void {
+ np_ensure()
+ for i in 0 .. len(np_posts) { if np_posts[i].active { np_step_post(np_posts[i], dt) } }
}
-function np_step_post(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
- npc_act(npc_st, p, dt)
- npc_notice(base_st, npc_st, p)
+function np_step_post(p: NpcPerson, dt: float) -> void {
+ npc_act(p, dt)
+ npc_notice(p)
}
# counted, not tallied, so it cannot drift; the census fills to what the place wants from the kinds
# by weight (after dark, only the kinds that stay out at night), and brings out an extra when asked
-function np_census(npc_st: mut NpcState) -> void {
+function np_census() -> void {
let h = NpcWorld.hour()
- if Math.abs(h - npc_st.np_next_h) <= 0.5 { return }
- npc_st.np_next_h = h
- npc_st.np_n = 0
- for i in 0 .. len(npc_st.np_walkers) { if npc_st.np_walkers[i].active { npc_st.np_n += 1 } }
- let want = Math.min(NpcWorld.want(), npc_st.np_max)
- while npc_st.np_n < want {
- let k = np_draw_kind(npc_st, np_is_night(h))
- if k < 0 or npc_spawn(npc_st, k) == null { break }
+ if Math.abs(h - np_next_h) <= 0.5 { return }
+ np_next_h = h
+ np_n = 0
+ for i in 0 .. len(np_walkers) { if np_walkers[i].active { np_n += 1 } }
+ let want = Math.min(NpcWorld.want(), np_max)
+ while np_n < want {
+ let k = np_draw_kind(np_is_night(h))
+ if k < 0 or npc_spawn(k) == null { break }
}
let extra = NpcWorld.extra()
- if extra >= 0 and npc_st.np_n < npc_st.np_max { npc_spawn(npc_st, extra) }
+ if extra >= 0 and np_n < np_max { npc_spawn(extra) }
}
function np_is_night(h: float) -> bool { return h >= 21.0 or h < 5.0 }
-function np_draw_kind(npc_st: mut NpcState, night: bool) -> int {
+function np_draw_kind(night: bool) -> int {
var total = 0
for k in 0 .. NPCK_COUNT { if np_drawn(k, night) { total += NpcKinds[k].weight } }
if total <= 0 { return -1 }
- var r = npc_between(npc_st, 0, total - 1)
+ var r = npc_between(0, total - 1)
for k in 0 .. NPCK_COUNT {
if not np_drawn(k, night) { continue }
r -= NpcKinds[k].weight
@@ -61,40 +61,40 @@ function np_drawn(k: int, night: bool) -> bool { return NpcKinds[k].weight > 0 a
# Its day ends: somebody who does not stay walks out after its hour, and by ten at night nobody is
# out but those who stay; a crowd over what the place wants thins. Only ever where no player sees
# it go, and never while it is out for something (`goal`).
-function np_leaves(npc_st: mut NpcState, p: NpcPerson) -> bool {
+function np_leaves(p: NpcPerson) -> bool {
if p.goal >= 0 { return false }
- let away = npc_near(npc_st, p.x, p.z)
+ let away = npc_near(p.x, p.z)
let h = NpcWorld.hour()
let k = NpcKinds[p.kind]
var go = false
if not k.stays and h > p.out_h and away > 120.0 { go = true }
if not k.stays and (h > 22.0 or h < 4.0) and away > 70.0 { go = true }
- if npc_st.np_n > NpcWorld.want() and away > 150.0 { go = true }
- if go { npc_retire(npc_st, p) }
+ if np_n > NpcWorld.want() and away > 150.0 { go = true }
+ if go { npc_retire(p) }
return go
}
# the census looks again at the next tick
-export function npc_census_now(npc_st: mut NpcState) -> void { npc_st.np_next_h = -10.0 }
+export function npc_census_now() -> void { np_next_h = -10.0 }
# held where they stand: no routine and no census (a shot of the poses)
-export function npc_hold(npc_st: mut NpcState, on: bool) -> void { npc_st.np_hold = on }
+export function npc_hold(on: bool) -> void { np_hold = on }
# everyone gone, as a new trip starts; the posts are the place's and stay
-export function npc_reset(base_st: mut BaseState, npc_st: mut NpcState) -> void {
- np_ensure(npc_st)
- 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
- npc_st.np_hold = false
- npc_st.np_recent = new []string
- q_clear(base_st, npc_facts(base_st, npc_st))
+export function npc_reset() -> void {
+ np_ensure()
+ for i in 0 .. len(np_walkers) { npc_retire(np_walkers[i]) }
+ np_n = 0
+ np_next_h = 0.0
+ np_hold = false
+ np_recent = new []string
+ q_clear(npc_facts())
}
# forgotten altogether, walkers and posts (the place itself was replaced)
-export function npc_clear(npc_st: mut NpcState) -> void {
- npc_st.np_walkers = new []NpcPerson
- npc_st.np_posts = new []NpcPerson
- npc_st.np_n = 0
- npc_st.np_next_h = 0.0
+export function npc_clear() -> void {
+ np_walkers = new []NpcPerson
+ np_posts = new []NpcPerson
+ np_n = 0
+ np_next_h = 0.0
}
diff --git a/packages/ludic.npc/walk.ludic b/packages/ludic.npc/walk.ludic
index 87d6c54f..5cd2ad4e 100644
--- a/packages/ludic.npc/walk.ludic
+++ b/packages/ludic.npc/walk.ludic
@@ -1,6 +1,6 @@
# walk.ludic - walking to the target: turn toward it, step, never into the water and never up a
# scramble - step round it instead - and give up on a target that gets no closer for twenty seconds
-export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
+export function npc_walk(p: NpcPerson, dt: float) -> void {
var gx = p.tx
var gz = p.tz
if p.via_on {
@@ -10,7 +10,7 @@ export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPer
let dx = gx - p.x
let dz = gz - p.z
if Math.sqrt(dx * dx + dz * dz) < 1.2 {
- np_arrive(base_st, npc_st, p)
+ np_arrive(p)
return
}
let dg = npc_d(p.x, p.z, p.tx, p.tz)
@@ -19,7 +19,7 @@ export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPer
p.prog_t = 0.0
} else { p.prog_t = p.prog_t + dt }
if p.prog_t > 20.0 {
- np_stuck(npc_st, p)
+ np_stuck(p)
return
}
var sp = p.speed
@@ -39,7 +39,7 @@ export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPer
p.detours += 1
return
}
- np_stuck(npc_st, p)
+ np_stuck(p)
return
}
p.x = nx
@@ -50,18 +50,18 @@ export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPer
}
# there: a step round something ends, or the errand begins, facing what it came for
-function np_arrive(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson) -> void {
+function np_arrive(p: NpcPerson) -> void {
if p.via_on {
p.via_on = false
return
}
let fx = p.fx
let fz = p.fz
- npc_do(npc_st, p, p.next_act, p.next_t)
+ npc_do(p, p.next_act, p.next_t)
p.fx = fx
p.fz = fz
p.stall = 0.0
- if p.act != NPCA_WALK { np_fact(base_st, npc_st, NPC_F_ARRIVED, p, -1) }
+ if p.act != NPCA_WALK { np_fact(NPC_F_ARRIVED, p, -1) }
}
function np_detour(p: NpcPerson, want: float) -> bool {
@@ -80,11 +80,11 @@ function np_detour(p: NpcPerson, want: float) -> bool {
return false
}
-function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
+function np_stuck(p: NpcPerson) -> void {
p.fails += 1
p.via_on = false
if p.fails >= 3 {
p.fails = 0
- npc_wander(npc_st, p)
- } else { npc_plan(npc_st, p) }
+ npc_wander(p)
+ } else { npc_plan(p) }
}
diff --git a/packages/ludic.photo/frame.ludic b/packages/ludic.photo/frame.ludic
index c9a3118f..cab65c94 100644
--- a/packages/ludic.photo/frame.ludic
+++ b/packages/ludic.photo/frame.ludic
@@ -1,37 +1,37 @@
# frame.ludic - what the camera sees right now: the game offers each subject and tag, and the frame
# takes the best subject's grade - which is what a photographer would say about it
-export function photo_frame_begin(photo_st: mut PhotoState) -> void {
- photo_st.pht_pending_tags = ""
- photo_st.pht_pending_mask = 0
- photo_st.pht_pending_legend = 0
- photo_st.pht_pending_grade = 0
- photo_st.pht_pending_why = PHOTO_WHY_NONE
- photo_st.pht_pending_sign = -1
- photo_st.pht_pending_stars = false
+export function photo_frame_begin() -> void {
+ pht_pending_tags = ""
+ pht_pending_mask = 0
+ pht_pending_legend = 0
+ pht_pending_grade = 0
+ pht_pending_why = PHOTO_WHY_NONE
+ pht_pending_sign = -1
+ pht_pending_stars = false
}
# A subject `sp` (0..31) named `name`, h metres tall, standing at (x, y, z) and doing something worth
# `moment`. True when the lens sees it within its range: it is tagged (once a kind, every legend) and
# scored, and the best score is the frame's.
-export function photo_frame_subject(photo_st: mut PhotoState, sp: int, name: string, x: float, y: float, z: float, h: float, legend: bool, moment: int) -> bool {
+export function photo_frame_subject(sp: int, name: string, x: float, y: float, z: float, h: float, legend: bool, moment: int) -> bool {
if not photo_in_view(x, y + 0.8, z, PhotoLens.range()) { return false }
- if (photo_st.pht_pending_mask & (1 << sp)) == 0 or legend { photo_frame_tag(photo_st, name) }
- photo_st.pht_pending_mask = photo_st.pht_pending_mask | (1 << sp)
- if legend { photo_st.pht_pending_legend = photo_st.pht_pending_legend | (1 << sp) }
- let g = photo_score(photo_st, x, y, z, h, moment)
- if g > photo_st.pht_pending_grade {
- photo_st.pht_pending_grade = g
- photo_st.pht_pending_why = photo_why_of_comp(photo_st)
+ if (pht_pending_mask & (1 << sp)) == 0 or legend { photo_frame_tag(name) }
+ pht_pending_mask = pht_pending_mask | (1 << sp)
+ if legend { pht_pending_legend = pht_pending_legend | (1 << sp) }
+ let g = photo_score(x, y, z, h, moment)
+ if g > pht_pending_grade {
+ pht_pending_grade = g
+ pht_pending_why = photo_why_of_comp()
}
return true
}
# anything else in the picture worth naming: a landmark, the water, a sign
-export function photo_frame_tag(photo_st: mut PhotoState, text: string) -> void {
+export function photo_frame_tag(text: string) -> void {
if len(text) == 0 { return }
- if len(photo_st.pht_pending_tags) > 0 { photo_st.pht_pending_tags = photo_st.pht_pending_tags + ", " }
- photo_st.pht_pending_tags = photo_st.pht_pending_tags + text
+ if len(pht_pending_tags) > 0 { pht_pending_tags = pht_pending_tags + ", " }
+ pht_pending_tags = pht_pending_tags + text
}
-export function photo_frame_sign(photo_st: mut PhotoState, sign: int) -> void { photo_st.pht_pending_sign = sign }
-export function photo_frame_stars(photo_st: mut PhotoState) -> void { photo_st.pht_pending_stars = true }
+export function photo_frame_sign(sign: int) -> void { pht_pending_sign = sign }
+export function photo_frame_stars() -> void { pht_pending_stars = true }
diff --git a/packages/ludic.photo/grade.ludic b/packages/ludic.photo/grade.ludic
index 50963b51..f28dc590 100644
--- a/packages/ludic.photo/grade.ludic
+++ b/packages/ludic.photo/grade.ludic
@@ -37,39 +37,39 @@ export function photo_score_frame(sx: float, sy: float, frac: float) -> int {
# The whole score for a subject `h` metres tall standing at (x, y, z), doing something worth
# `moment` (0..20). Composition, light and behaviour are qualities OF a subject, so they are
# weighted by how much of one there is: a deer three pixels wide in perfect light is not a photograph.
-export function photo_score(photo_st: mut PhotoState, x: float, y: float, z: float, h: float, moment: int) -> int {
+export function photo_score(x: float, y: float, z: float, h: float, moment: int) -> int {
let dx = x - PhotoLens.x()
let dy = y + 0.6 - PhotoLens.y()
let dz = z - PhotoLens.z()
let d = Math.max(Math.sqrt(dx * dx + dy * dy + dz * dz), 0.01)
let frac = pht_size_frac(h, d)
- if photo_st.pht_comp == null { pht_blank(photo_st) }
+ if pht_comp == null { pht_blank() }
var sx = 0.5
var sy = 0.5
if PhotoLens.project(x, y + h * 0.5, z) {
sx = PhotoLens.sx()
sy = PhotoLens.sy()
}
- photo_st.pht_comp[0] = float(photo_score_size(frac))
- photo_st.pht_comp[1] = float(photo_score_frame(sx, sy, frac))
- if PhotoLens.steady() { photo_st.pht_comp[1] = Math.min(photo_st.pht_comp[1] + 5.0, 20.0) }
- photo_st.pht_comp[2] = float(photo_score_light(x, z))
- photo_st.pht_comp[3] = float(Math.clamp(moment, 0, 20))
- photo_st.pht_comp_have = true
- let presence = Math.clamp(photo_st.pht_comp[0] / 40.0 * 1.4, 0.0, 1.0)
+ pht_comp[0] = float(photo_score_size(frac))
+ pht_comp[1] = float(photo_score_frame(sx, sy, frac))
+ if PhotoLens.steady() { pht_comp[1] = Math.min(pht_comp[1] + 5.0, 20.0) }
+ pht_comp[2] = float(photo_score_light(x, z))
+ pht_comp[3] = float(Math.clamp(moment, 0, 20))
+ pht_comp_have = true
+ let presence = Math.clamp(pht_comp[0] / 40.0 * 1.4, 0.0, 1.0)
if PhotoRules.behaviour() {
- photo_st.pht_comp[0] = photo_st.pht_comp[0] * 0.5
- photo_st.pht_comp[3] = photo_st.pht_comp[3] * 2.0
+ pht_comp[0] = pht_comp[0] * 0.5
+ pht_comp[3] = pht_comp[3] * 2.0
}
- return int(photo_st.pht_comp[0] + (photo_st.pht_comp[1] + photo_st.pht_comp[2] + photo_st.pht_comp[3]) * presence)
+ return int(pht_comp[0] + (pht_comp[1] + pht_comp[2] + pht_comp[3]) * presence)
}
# the one thing to do differently next time: the worst component, or nothing to fault
-export function photo_why_of_comp(photo_st: PhotoState) -> int {
+export function photo_why_of_comp() -> int {
var worst = PHOTO_WHY_SIZE
- var least = photo_st.pht_comp[0] / 40.0
+ var least = pht_comp[0] / 40.0
for k in 1 .. 4 {
- let f = photo_st.pht_comp[k] / 20.0
+ let f = pht_comp[k] / 20.0
if f < least {
least = f
worst = k
diff --git a/packages/ludic.photo/keep.ludic b/packages/ludic.photo/keep.ludic
index cd3fabb5..b272af4c 100644
--- a/packages/ludic.photo/keep.ludic
+++ b/packages/ludic.photo/keep.ludic
@@ -1,81 +1,81 @@
# keep.ludic - the frame onto the roll, and a frame off it
# The composed frame, kept as `file` on `day` at `hour`: the index, or -1 when the card is full.
# Every subject in it counts as photographed, and its best grade may rise.
-export function photo_keep(base_st: mut BaseState, photo_st: mut PhotoState, file: string, day: int, hour: float) -> int {
- if photo_st.pht_n >= photo_st.pht_max {
- pht_fact(base_st, photo_st, PHOTO_FULL, -1)
+export function photo_keep(file: string, day: int, hour: float) -> int {
+ if pht_n >= pht_max {
+ pht_fact(PHOTO_FULL, -1)
return -1
}
var firsts = 0
- for sp in 0 .. 32 { if (photo_st.pht_pending_mask & (1 << sp)) != 0 and photo_st.pht_best[sp] == 0 { firsts = firsts | (1 << sp) } }
- let i = photo_add(photo_st, day, hour, photo_st.pht_pending_mask, photo_st.pht_pending_legend, false, photo_st.pht_pending_tags, file, photo_st.pht_pending_grade, photo_st.pht_pending_why)
- photo_st.pht_legend_shot = photo_st.pht_legend_shot | photo_st.pht_pending_legend
- let f = pht_fact(base_st, photo_st, PHOTO_KEPT, i)
- f.mask = photo_st.pht_pending_mask
- f.legend = photo_st.pht_pending_legend
+ for sp in 0 .. 32 { if (pht_pending_mask & (1 << sp)) != 0 and pht_best[sp] == 0 { firsts = firsts | (1 << sp) } }
+ let i = photo_add(day, hour, pht_pending_mask, pht_pending_legend, false, pht_pending_tags, file, pht_pending_grade, pht_pending_why)
+ pht_legend_shot = pht_legend_shot | pht_pending_legend
+ let f = pht_fact(PHOTO_KEPT, i)
+ f.mask = pht_pending_mask
+ f.legend = pht_pending_legend
f.firsts = firsts
- f.grade = photo_st.pht_pending_grade
- f.sign = photo_st.pht_pending_sign
- f.stars = photo_st.pht_pending_stars
+ f.grade = pht_pending_grade
+ f.sign = pht_pending_sign
+ f.stars = pht_pending_stars
f.file = file
- f.tags = photo_st.pht_pending_tags
+ f.tags = pht_pending_tags
return i
}
# a frame onto the roll as it stands (a save, an older trip): the index, or -1 when full
-export function photo_add(photo_st: mut PhotoState, day: int, hour: float, mask: int, legend: int, sold: bool, tags: string, file: string, grade: int, why: int) -> int {
- if photo_st.pht_day == null { pht_blank(photo_st) }
- if photo_st.pht_n >= photo_st.pht_max { return -1 }
- let i = photo_st.pht_n
- photo_st.pht_day[i] = day
- photo_st.pht_hour[i] = hour
- photo_st.pht_mask[i] = mask
- photo_st.pht_legend[i] = legend
- photo_st.pht_sold[i] = 0
- if sold { photo_st.pht_sold[i] = 1 }
- photo_st.pht_grade[i] = grade
- photo_st.pht_why[i] = why
- push(photo_st.pht_tags, tags)
- push(photo_st.pht_file, file)
- photo_st.pht_n += 1
- photo_st.pht_species_shot = photo_st.pht_species_shot | mask
- for sp in 0 .. 32 { if (mask & (1 << sp)) != 0 and grade > photo_st.pht_best[sp] { photo_st.pht_best[sp] = grade } }
+export function photo_add(day: int, hour: float, mask: int, legend: int, sold: bool, tags: string, file: string, grade: int, why: int) -> int {
+ if pht_day == null { pht_blank() }
+ if pht_n >= pht_max { return -1 }
+ let i = pht_n
+ pht_day[i] = day
+ pht_hour[i] = hour
+ pht_mask[i] = mask
+ pht_legend[i] = legend
+ pht_sold[i] = 0
+ if sold { pht_sold[i] = 1 }
+ pht_grade[i] = grade
+ pht_why[i] = why
+ push(pht_tags, tags)
+ push(pht_file, file)
+ pht_n += 1
+ pht_species_shot = pht_species_shot | mask
+ for sp in 0 .. 32 { if (mask & (1 << sp)) != 0 and grade > pht_best[sp] { pht_best[sp] = grade } }
return i
}
# forgetting one: the record goes, the ones after it close up, and the game is told the file
-export function photo_remove(base_st: mut BaseState, photo_st: mut PhotoState, i: int) -> void {
- if i < 0 or i >= photo_st.pht_n { return }
- let f = pht_fact(base_st, photo_st, PHOTO_DELETED, i)
- f.file = photo_st.pht_file[i]
+export function photo_remove(i: int) -> void {
+ if i < 0 or i >= pht_n { return }
+ let f = pht_fact(PHOTO_DELETED, i)
+ f.file = pht_file[i]
let tags = new []string
let files = new []string
- for k in 0 .. photo_st.pht_n {
+ for k in 0 .. pht_n {
if k == i { continue }
- push(tags, photo_st.pht_tags[k])
- push(files, photo_st.pht_file[k])
+ push(tags, pht_tags[k])
+ push(files, pht_file[k])
}
- for k in i .. photo_st.pht_n - 1 {
- photo_st.pht_day[k] = photo_st.pht_day[k + 1]
- photo_st.pht_hour[k] = photo_st.pht_hour[k + 1]
- photo_st.pht_mask[k] = photo_st.pht_mask[k + 1]
- photo_st.pht_legend[k] = photo_st.pht_legend[k + 1]
- photo_st.pht_sold[k] = photo_st.pht_sold[k + 1]
- photo_st.pht_grade[k] = photo_st.pht_grade[k + 1]
- photo_st.pht_why[k] = photo_st.pht_why[k + 1]
+ for k in i .. pht_n - 1 {
+ pht_day[k] = pht_day[k + 1]
+ pht_hour[k] = pht_hour[k + 1]
+ pht_mask[k] = pht_mask[k + 1]
+ pht_legend[k] = pht_legend[k + 1]
+ pht_sold[k] = pht_sold[k + 1]
+ pht_grade[k] = pht_grade[k + 1]
+ pht_why[k] = pht_why[k + 1]
}
- photo_st.pht_tags = tags
- photo_st.pht_file = files
- photo_st.pht_n -= 1
- if photo_st.pht_cover == i { photo_st.pht_cover = -1 } else if photo_st.pht_cover > i { photo_st.pht_cover -= 1 }
+ pht_tags = tags
+ pht_file = files
+ pht_n -= 1
+ if pht_cover == i { pht_cover = -1 } else if pht_cover > i { pht_cover -= 1 }
}
-export function photo_cover_set(photo_st: mut PhotoState, i: int) -> void { photo_st.pht_cover = i }
+export function photo_cover_set(i: int) -> void { pht_cover = i }
# what has been photographed outright (a network's word, a staged trip)
-export function photo_record_set(photo_st: mut PhotoState, species: int, legends: int) -> void {
- photo_st.pht_species_shot = species
- photo_st.pht_legend_shot = legends
+export function photo_record_set(species: int, legends: int) -> void {
+ pht_species_shot = species
+ pht_legend_shot = legends
}
-export function photo_best_set(photo_st: mut PhotoState, sp: int, grade: int) -> void { if sp >= 0 and sp < 32 { photo_st.pht_best[sp] = grade } }
+export function photo_best_set(sp: int, grade: int) -> void { if sp >= 0 and sp < 32 { pht_best[sp] = grade } }
diff --git a/packages/ludic.photo/order.ludic b/packages/ludic.photo/order.ludic
index d9637797..677db200 100644
--- a/packages/ludic.photo/order.ludic
+++ b/packages/ludic.photo/order.ludic
@@ -5,47 +5,50 @@ export const PHOTO_BEST: int = 1
export const PHOTO_BY_SUBJECT: int = 2
export const PHOTO_PAGE: int = 8
+var pht_sort: int = 0
+var pht_page: int = 0
+var pht_order: []int = null
-export function photo_sort(photo_st: PhotoState) -> int { return photo_st.pht_sort }
-export function photo_sort_next(photo_st: mut PhotoState) -> void { photo_st.pht_sort = (photo_st.pht_sort + 1) % 3 }
-export function photo_sort_set(photo_st: mut PhotoState, s: int) -> void { photo_st.pht_sort = Math.clamp(s, 0, 2) }
+export function photo_sort() -> int { return pht_sort }
+export function photo_sort_next() -> void { pht_sort = (pht_sort + 1) % 3 }
+export function photo_sort_set(s: int) -> void { pht_sort = Math.clamp(s, 0, 2) }
# the roll in the chosen order: newest first, best first, or by subject
-export function photo_order(photo_st: mut PhotoState) -> []int {
- photo_st.pht_order = new []int
- for i in 0 .. photo_st.pht_n { push(photo_st.pht_order, i) }
- if photo_st.pht_sort == PHOTO_NEWEST {
- for i in 0 .. photo_st.pht_n { photo_st.pht_order[i] = photo_st.pht_n - 1 - i }
- return photo_st.pht_order
+export function photo_order() -> []int {
+ pht_order = new []int
+ for i in 0 .. pht_n { push(pht_order, i) }
+ if pht_sort == PHOTO_NEWEST {
+ for i in 0 .. pht_n { pht_order[i] = pht_n - 1 - i }
+ return pht_order
}
- for i in 0 .. photo_st.pht_n {
+ for i in 0 .. pht_n {
var best = i
- for k in i + 1 .. photo_st.pht_n { if pht_before(photo_st, photo_st.pht_order[k], photo_st.pht_order[best]) { best = k } }
- let t = photo_st.pht_order[i]
- photo_st.pht_order[i] = photo_st.pht_order[best]
- photo_st.pht_order[best] = t
+ for k in i + 1 .. pht_n { if pht_before(pht_order[k], pht_order[best]) { best = k } }
+ let t = pht_order[i]
+ pht_order[i] = pht_order[best]
+ pht_order[best] = t
}
- return photo_st.pht_order
+ return pht_order
}
-function pht_before(photo_st: PhotoState, a: int, b: int) -> bool {
- if photo_st.pht_sort == PHOTO_BEST { return photo_st.pht_grade[a] > photo_st.pht_grade[b] }
- return photo_st.pht_mask[a] < photo_st.pht_mask[b]
+function pht_before(a: int, b: int) -> bool {
+ if pht_sort == PHOTO_BEST { return pht_grade[a] > pht_grade[b] }
+ return pht_mask[a] < pht_mask[b]
}
-export function photo_pages(photo_st: PhotoState) -> int { return (photo_st.pht_n + PHOTO_PAGE - 1) / PHOTO_PAGE }
-export function photo_page_now(photo_st: PhotoState) -> int { return photo_st.pht_page }
-export function photo_page_step(photo_st: mut PhotoState, d: int) -> void { photo_st.pht_page = Math.clamp(photo_st.pht_page + d, 0, Math.max(photo_pages(photo_st) - 1, 0)) }
+export function photo_pages() -> int { return (pht_n + PHOTO_PAGE - 1) / PHOTO_PAGE }
+export function photo_page_now() -> int { return pht_page }
+export function photo_page_step(d: int) -> void { pht_page = Math.clamp(pht_page + d, 0, Math.max(photo_pages() - 1, 0)) }
# a page of the roll, in its order; the page is held inside the roll
-export function photo_page_list(photo_st: mut PhotoState) -> []int {
+export function photo_page_list() -> []int {
let out = new []int
- if photo_st.pht_n == 0 { return out }
- let order = photo_order(photo_st)
- if photo_st.pht_page >= photo_pages(photo_st) { photo_st.pht_page = photo_pages(photo_st) - 1 }
+ if pht_n == 0 { return out }
+ let order = photo_order()
+ if pht_page >= photo_pages() { pht_page = photo_pages() - 1 }
for k in 0 .. PHOTO_PAGE {
- let oi = photo_st.pht_page * PHOTO_PAGE + k
- if oi < photo_st.pht_n { push(out, order[oi]) }
+ let oi = pht_page * PHOTO_PAGE + k
+ if oi < pht_n { push(out, order[oi]) }
}
return out
}
diff --git a/packages/ludic.photo/queries.ludic b/packages/ludic.photo/queries.ludic
index 259ac1cb..14080265 100644
--- a/packages/ludic.photo/queries.ludic
+++ b/packages/ludic.photo/queries.ludic
@@ -1,38 +1,38 @@
# queries.ludic - what a game may ask of the roll and of the frame being composed
-export function photo_count(photo_st: PhotoState) -> int { return photo_st.pht_n }
-export function photo_max(photo_st: PhotoState) -> int { return photo_st.pht_max }
-export function photo_full(photo_st: PhotoState) -> bool { return photo_st.pht_n >= photo_st.pht_max }
-export function photo_day(photo_st: PhotoState, i: int) -> int { return photo_st.pht_day[i] }
-export function photo_hour(photo_st: PhotoState, i: int) -> float { return photo_st.pht_hour[i] }
-export function photo_mask(photo_st: PhotoState, i: int) -> int { return photo_st.pht_mask[i] }
-export function photo_legend_mask(photo_st: PhotoState, i: int) -> int { return photo_st.pht_legend[i] }
-export function photo_sold(photo_st: PhotoState, i: int) -> bool { return photo_st.pht_sold[i] != 0 }
-export function photo_grade(photo_st: PhotoState, i: int) -> int { return photo_st.pht_grade[i] }
-export function photo_why(photo_st: PhotoState, i: int) -> int { return photo_st.pht_why[i] }
-export function photo_tags(photo_st: PhotoState, i: int) -> string { return photo_st.pht_tags[i] }
-export function photo_file(photo_st: PhotoState, i: int) -> string { return photo_st.pht_file[i] }
+export function photo_count() -> int { return pht_n }
+export function photo_max() -> int { return pht_max }
+export function photo_full() -> bool { return pht_n >= pht_max }
+export function photo_day(i: int) -> int { return pht_day[i] }
+export function photo_hour(i: int) -> float { return pht_hour[i] }
+export function photo_mask(i: int) -> int { return pht_mask[i] }
+export function photo_legend_mask(i: int) -> int { return pht_legend[i] }
+export function photo_sold(i: int) -> bool { return pht_sold[i] != 0 }
+export function photo_grade(i: int) -> int { return pht_grade[i] }
+export function photo_why(i: int) -> int { return pht_why[i] }
+export function photo_tags(i: int) -> string { return pht_tags[i] }
+export function photo_file(i: int) -> string { return pht_file[i] }
# a frame with a subject in it was graded; a view is not
-export function photo_graded(photo_st: PhotoState, i: int) -> bool { return photo_st.pht_mask[i] != 0 }
-export function photo_cover(photo_st: PhotoState) -> int { return photo_st.pht_cover }
+export function photo_graded(i: int) -> bool { return pht_mask[i] != 0 }
+export function photo_cover() -> int { return pht_cover }
-export function photo_species_shot(photo_st: PhotoState) -> int { return photo_st.pht_species_shot }
-export function photo_legend_shot(photo_st: PhotoState) -> int { return photo_st.pht_legend_shot }
-export function photo_best(photo_st: PhotoState, sp: int) -> int {
- if sp < 0 or sp >= len(photo_st.pht_best) { return 0 }
- return photo_st.pht_best[sp]
+export function photo_species_shot() -> int { return pht_species_shot }
+export function photo_legend_shot() -> int { return pht_legend_shot }
+export function photo_best(sp: int) -> int {
+ if sp < 0 or sp >= len(pht_best) { return 0 }
+ return pht_best[sp]
}
-export function photo_pending_tags(photo_st: PhotoState) -> string { return photo_st.pht_pending_tags }
-export function photo_pending_mask(photo_st: PhotoState) -> int { return photo_st.pht_pending_mask }
-export function photo_pending_legend(photo_st: PhotoState) -> int { return photo_st.pht_pending_legend }
-export function photo_pending_grade(photo_st: PhotoState) -> int { return photo_st.pht_pending_grade }
-export function photo_pending_why(photo_st: PhotoState) -> int { return photo_st.pht_pending_why }
-export function photo_pending_sign(photo_st: PhotoState) -> int { return photo_st.pht_pending_sign }
-export function photo_pending_stars(photo_st: PhotoState) -> bool { return photo_st.pht_pending_stars }
+export function photo_pending_tags() -> string { return pht_pending_tags }
+export function photo_pending_mask() -> int { return pht_pending_mask }
+export function photo_pending_legend() -> int { return pht_pending_legend }
+export function photo_pending_grade() -> int { return pht_pending_grade }
+export function photo_pending_why() -> int { return pht_pending_why }
+export function photo_pending_sign() -> int { return pht_pending_sign }
+export function photo_pending_stars() -> bool { return pht_pending_stars }
# the last subject's four components (size of 40, framing, light and the moment of 20 each), for a meter
-export function photo_comp(photo_st: PhotoState, k: int) -> float { return photo_st.pht_comp[k] }
-export function photo_comp_have(photo_st: PhotoState) -> bool { return photo_st.pht_comp_have }
+export function photo_comp(k: int) -> float { return pht_comp[k] }
+export function photo_comp_have() -> bool { return pht_comp_have }
# the band a grade falls in: 0 a record shot, 1 fair, 2 good, 3 fine, 4 one for the wall
export function photo_band(g: int) -> int {
diff --git a/packages/ludic.photo/roll.ludic b/packages/ludic.photo/roll.ludic
index fda7bbec..f2e65c6f 100644
--- a/packages/ludic.photo/roll.ludic
+++ b/packages/ludic.photo/roll.ludic
@@ -26,49 +26,44 @@ export property PhotoFact {
v: int = 0
}
-export state PhotoState {
- pht_sort: int = 0
- pht_page: int = 0
- pht_order: []int = null
- pht_max: int = 40
- pht_n: int = 0
- pht_day: []int = null
- pht_hour: []float = null
- pht_mask: []int = null
- pht_legend: []int = null
- pht_sold: []int = null
- pht_grade: []int = null
- pht_why: []int = null
- pht_tags: []string = null
- pht_file: []string = null
- pht_best: []int = null # the best grade yet, per subject
- pht_species_shot: int = 0 # every subject ever photographed, sold or deleted
- pht_legend_shot: int = 0
- pht_cover: int = -1 # the one frame a game shows for the whole roll
- pht_pending_tags: string = ""
- pht_pending_mask: int = 0
- pht_pending_legend: int = 0
- pht_pending_grade: int = 0
- pht_pending_why: int = -1
- pht_pending_sign: int = -1
- pht_pending_stars: bool = false
- pht_comp: []float = null # size, framing, light, moment of the last subject scored
- pht_comp_have: bool = false
- pht_fact_q: Queue = null
+var pht_max: int = 40
+var pht_n: int = 0
+var pht_day: []int = null
+var pht_hour: []float = null
+var pht_mask: []int = null
+var pht_legend: []int = null
+var pht_sold: []int = null
+var pht_grade: []int = null
+var pht_why: []int = null
+var pht_tags: []string = null
+var pht_file: []string = null
+var pht_best: []int = null # the best grade yet, per subject
+var pht_species_shot: int = 0 # every subject ever photographed, sold or deleted
+var pht_legend_shot: int = 0
+var pht_cover: int = -1 # the one frame a game shows for the whole roll
+
+var pht_pending_tags: string = ""
+var pht_pending_mask: int = 0
+var pht_pending_legend: int = 0
+var pht_pending_grade: int = 0
+var pht_pending_why: int = -1
+var pht_pending_sign: int = -1
+var pht_pending_stars: bool = false
+var pht_comp: []float = null # size, framing, light, moment of the last subject scored
+var pht_comp_have: bool = false
+
+var pht_fact_q: Queue = null
+
+export function photo_facts() -> Queue {
+ if pht_fact_q == null { pht_fact_q = queue_new("photo.facts") }
+ return pht_fact_q
}
-
-
-export function photo_facts(base_st: mut BaseState, photo_st: mut PhotoState) -> Queue {
- if photo_st.pht_fact_q == null { photo_st.pht_fact_q = queue_new(base_st, "photo.facts") }
- return photo_st.pht_fact_q
-}
-
-function pht_fact(base_st: mut BaseState, photo_st: mut PhotoState, what: int, i: int) -> PhotoFact {
+function pht_fact(what: int, i: int) -> PhotoFact {
let f = new PhotoFact
f.what = what
f.index = i
- q_push(base_st, photo_facts(base_st, photo_st), f)
+ q_push(photo_facts(), f)
return f
}
@@ -85,18 +80,18 @@ function pht_floats(n: int) -> []float {
}
# an empty roll of `max` frames
-function pht_blank(photo_st: mut PhotoState) -> void {
- photo_st.pht_n = 0
- photo_st.pht_day = pht_ints(photo_st.pht_max)
- photo_st.pht_hour = pht_floats(photo_st.pht_max)
- photo_st.pht_mask = pht_ints(photo_st.pht_max)
- photo_st.pht_legend = pht_ints(photo_st.pht_max)
- photo_st.pht_sold = pht_ints(photo_st.pht_max)
- photo_st.pht_grade = pht_ints(photo_st.pht_max)
- photo_st.pht_why = pht_ints(photo_st.pht_max)
- photo_st.pht_tags = new []string
- photo_st.pht_file = new []string
- photo_st.pht_best = pht_ints(32)
- photo_st.pht_comp = pht_floats(4)
- photo_st.pht_comp_have = false
+function pht_blank() -> void {
+ pht_n = 0
+ pht_day = pht_ints(pht_max)
+ pht_hour = pht_floats(pht_max)
+ pht_mask = pht_ints(pht_max)
+ pht_legend = pht_ints(pht_max)
+ pht_sold = pht_ints(pht_max)
+ pht_grade = pht_ints(pht_max)
+ pht_why = pht_ints(pht_max)
+ pht_tags = new []string
+ pht_file = new []string
+ pht_best = pht_ints(32)
+ pht_comp = pht_floats(4)
+ pht_comp_have = false
}
diff --git a/packages/ludic.photo/system.ludic b/packages/ludic.photo/system.ludic
index d1860f49..d9f777d6 100644
--- a/packages/ludic.photo/system.ludic
+++ b/packages/ludic.photo/system.ludic
@@ -1,56 +1,56 @@
# system.ludic - the camera as a system: the roll and what has been photographed in its own save
# section, by key. The best per subject follows from the frames, as a buyer would judge them.
-export function photo_config(base_st: mut BaseState, photo_st: mut PhotoState, max: int) -> void {
- photo_st.pht_max = Math.max(max, 1)
- photo_reset(base_st, photo_st)
+export function photo_config(max: int) -> void {
+ pht_max = Math.max(max, 1)
+ photo_reset()
}
-export function photo_reset(base_st: mut BaseState, photo_st: mut PhotoState) -> void {
- pht_blank(photo_st)
- photo_st.pht_species_shot = 0
- photo_st.pht_legend_shot = 0
- photo_st.pht_cover = -1
- photo_st.pht_page = 0
- photo_frame_begin(photo_st)
- q_clear(base_st, photo_facts(base_st, photo_st))
+export function photo_reset() -> void {
+ pht_blank()
+ pht_species_shot = 0
+ pht_legend_shot = 0
+ pht_cover = -1
+ pht_page = 0
+ photo_frame_begin()
+ q_clear(photo_facts())
}
-export function photo_save(photo_st: PhotoState) -> Val {
+export function photo_save() -> Val {
let v = Value.object()
let l = Value.list()
- for i in 0 .. photo_st.pht_n {
+ for i in 0 .. pht_n {
let o = Value.object()
- sv_put_int(o, "day", photo_st.pht_day[i])
- sv_put_float(o, "hour", photo_st.pht_hour[i])
- sv_put_int(o, "mask", photo_st.pht_mask[i])
- sv_put_int(o, "legend", photo_st.pht_legend[i])
- sv_put_bool(o, "sold", photo_st.pht_sold[i] != 0)
- sv_put_str(o, "tags", photo_st.pht_tags[i])
- sv_put_str(o, "file", photo_st.pht_file[i])
- sv_put_int(o, "grade", photo_st.pht_grade[i])
- sv_put_int(o, "why", photo_st.pht_why[i])
+ sv_put_int(o, "day", pht_day[i])
+ sv_put_float(o, "hour", pht_hour[i])
+ sv_put_int(o, "mask", pht_mask[i])
+ sv_put_int(o, "legend", pht_legend[i])
+ sv_put_bool(o, "sold", pht_sold[i] != 0)
+ sv_put_str(o, "tags", pht_tags[i])
+ sv_put_str(o, "file", pht_file[i])
+ sv_put_int(o, "grade", pht_grade[i])
+ sv_put_int(o, "why", pht_why[i])
Value.add(l, o)
}
Value.put(v, "frames", l)
- sv_put_int(v, "shot", photo_st.pht_species_shot)
- sv_put_int(v, "legend_shot", photo_st.pht_legend_shot)
- sv_put_int(v, "cover", photo_st.pht_cover)
- sv_put_int(v, "sort", photo_st.pht_sort)
+ sv_put_int(v, "shot", pht_species_shot)
+ sv_put_int(v, "legend_shot", pht_legend_shot)
+ sv_put_int(v, "cover", pht_cover)
+ sv_put_int(v, "sort", pht_sort)
return v
}
-export function photo_load(base_st: mut BaseState, photo_st: mut PhotoState, v: Val, version: int) -> void {
- photo_reset(base_st, photo_st)
+export function photo_load(v: Val, version: int) -> void {
+ photo_reset()
if Value.has(v, "frames") != 0 {
let l = Value.get(v, "frames")
for i in 0 .. Value.count(l) {
let o = Value.at(l, i)
- photo_add(photo_st, sv_int(o, "day", 1), sv_float(o, "hour", 0.0), sv_int(o, "mask", 0), sv_int(o, "legend", 0), sv_bool(o, "sold", false), sv_str(o, "tags", ""), sv_str(o, "file", ""), sv_int(o, "grade", 40), sv_int(o, "why", PHOTO_WHY_NONE))
+ photo_add(sv_int(o, "day", 1), sv_float(o, "hour", 0.0), sv_int(o, "mask", 0), sv_int(o, "legend", 0), sv_bool(o, "sold", false), sv_str(o, "tags", ""), sv_str(o, "file", ""), sv_int(o, "grade", 40), sv_int(o, "why", PHOTO_WHY_NONE))
}
}
- photo_record_set(photo_st, photo_st.pht_species_shot | sv_int(v, "shot", 0), sv_int(v, "legend_shot", 0))
- photo_st.pht_cover = sv_int(v, "cover", -1)
- photo_sort_set(photo_st, sv_int(v, "sort", PHOTO_NEWEST))
+ photo_record_set(pht_species_shot | sv_int(v, "shot", 0), sv_int(v, "legend_shot", 0))
+ pht_cover = sv_int(v, "cover", -1)
+ photo_sort_set(sv_int(v, "sort", PHOTO_NEWEST))
}
export function photo_system() -> System {
diff --git a/packages/ludic.photo/tests/photo_test.ludic b/packages/ludic.photo/tests/photo_test.ludic
index 468089da..a6bd8867 100644
--- a/packages/ludic.photo/tests/photo_test.ludic
+++ b/packages/ludic.photo/tests/photo_test.ludic
@@ -7,78 +7,76 @@ import "ludic.base"
program PhotoTest {
numbers float
- state PhototestState {
- hour: float = 12.0
- night: bool = false
- moon: float = 0.0
- dim: int = 0
- sun_z: float = 0.0
- behaviour: bool = false
- sx: float = 0.5
- sy: float = 0.5
- }
+ var hour: float = 12.0
+ var night: bool = false
+ var moon: float = 0.0
+ var dim: int = 0
+ var sun_z: float = 0.0
+ var behaviour: bool = false
+ var sx: float = 0.5
+ var sy: float = 0.5
function fk_y() -> float { return 1.5 }
function fk_fz() -> float { return -1.0 }
function fk_fov() -> float { return 0.8 }
# a pinhole at the origin: across by x over the distance, down by the height over it
- function fk_project(phototest_st: mut PhototestState, x: float, y: float, z: float) -> bool {
+ function fk_project(x: float, y: float, z: float) -> bool {
let d = Math.max(-z, 0.5)
- phototest_st.sx = 0.5 + x / d
- phototest_st.sy = 0.5 - (y - 1.5) / d
+ sx = 0.5 + x / d
+ sy = 0.5 - (y - 1.5) / d
return z < 0.0
}
- function fk_sx(phototest_st: PhototestState) -> float { return phototest_st.sx }
- function fk_sy(phototest_st: PhototestState) -> float { return phototest_st.sy }
+ function fk_sx() -> float { return sx }
+ function fk_sy() -> float { return sy }
function fk_ground(x: float, z: float) -> float {
if x < 0.0 and z < -45.0 and z > -50.0 { return 10.0 }
return 0.0
}
function fk_trunk(x: float, z: float, r: float) -> bool { return Math.abs(x - 3.75) < 1.6 and Math.abs(z + 30.0) < 1.6 }
function fk_range() -> float { return 80.0 }
- function fk_hour(phototest_st: PhototestState) -> float { return phototest_st.hour }
- function fk_night(phototest_st: PhototestState) -> bool { return phototest_st.night }
- function fk_moon(phototest_st: PhototestState) -> float { return phototest_st.moon }
- function fk_dim(phototest_st: PhototestState) -> int { return phototest_st.dim }
- function fk_sun_z(phototest_st: PhototestState) -> float { return phototest_st.sun_z }
- function fk_behaviour(phototest_st: PhototestState) -> bool { return phototest_st.behaviour }
+ function fk_hour() -> float { return hour }
+ function fk_night() -> bool { return night }
+ function fk_moon() -> float { return moon }
+ function fk_dim() -> int { return dim }
+ function fk_sun_z() -> float { return sun_z }
+ function fk_behaviour() -> bool { return behaviour }
function fk_value(sp: int) -> int { return 10 + sp }
bind PhotoLens { y: fn fk_y, fz: fn fk_fz, fov: fn fk_fov, project: fn fk_project, sx: fn fk_sx, sy: fn fk_sy, ground: fn fk_ground, trunk: fn fk_trunk, range: fn fk_range }
bind PhotoLight { hour: fn fk_hour, night: fn fk_night, moon: fn fk_moon, dim: fn fk_dim, sun_z: fn fk_sun_z }
bind PhotoRules { behaviour: fn fk_behaviour, subject_value: fn fk_value }
- function fresh(base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) -> void {
- photo_config(base_st, photo_st, 4)
- phototest_st.hour = 12.0
- phototest_st.night = false
- phototest_st.dim = 0
- phototest_st.sun_z = 0.0
- phototest_st.behaviour = false
+ function fresh() -> void {
+ photo_config(4)
+ hour = 12.0
+ night = false
+ dim = 0
+ sun_z = 0.0
+ behaviour = false
}
- function facts(base_st: mut BaseState, photo_st: mut PhotoState) -> []PhotoFact { return q_drain(base_st, photo_facts(base_st, photo_st)) }
+ function facts() -> []PhotoFact { return q_drain(photo_facts()) }
function count(fs: []PhotoFact, what: int) -> int {
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
}
# a deer-sized subject straight ahead at `d` metres, doing something worth `moment`
- function shoot(photo_st: mut PhotoState, sp: int, x: float, d: float, h: float, moment: int) -> bool {
- photo_frame_begin(photo_st)
- return photo_frame_subject(photo_st, sp, "deer", x, 0.0, -d, h, false, moment)
+ function shoot(sp: int, x: float, d: float, h: float, moment: int) -> bool {
+ photo_frame_begin()
+ return photo_frame_subject(sp, "deer", x, 0.0, -d, h, false, moment)
}
- test "a subject ahead and in reach is in the frame; behind, too far, over a ridge or a trunk it is not" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- expect(shoot(photo_st, 1, 0.0, 20.0, 1.1, 10))
- expect_eq(photo_pending_mask(photo_st), 2)
- expect(photo_pending_tags(photo_st) == "deer")
- expect(photo_pending_grade(photo_st) > 0)
- expect(not shoot(photo_st, 1, 0.0, -20.0, 1.1, 10))
- expect(not shoot(photo_st, 1, 0.0, 90.0, 1.1, 10))
- expect(not shoot(photo_st, 1, -2.0, 60.0, 1.1, 10))
- expect(not shoot(photo_st, 1, 5.0, 40.0, 1.1, 10))
- expect_eq(photo_pending_mask(photo_st), 0)
+ test "a subject ahead and in reach is in the frame; behind, too far, over a ridge or a trunk it is not" {
+ fresh()
+ expect(shoot(1, 0.0, 20.0, 1.1, 10))
+ expect_eq(photo_pending_mask(), 2)
+ expect(photo_pending_tags() == "deer")
+ expect(photo_pending_grade() > 0)
+ expect(not shoot(1, 0.0, -20.0, 1.1, 10))
+ expect(not shoot(1, 0.0, 90.0, 1.1, 10))
+ expect(not shoot(1, -2.0, 60.0, 1.1, 10))
+ expect(not shoot(1, 5.0, 40.0, 1.1, 10))
+ expect_eq(photo_pending_mask(), 0)
}
test "size: a record shot is a record shot, and the band that pays is a quarter to two thirds" {
@@ -90,123 +88,123 @@ program PhotoTest {
expect_eq(photo_score_frame(1.4, 0.4, 0.1), 0)
}
- test "the light: gold after dawn, flat at noon, the moon at night, the weather, and backlit" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- phototest_st.hour = 6.5
+ test "the light: gold after dawn, flat at noon, the moon at night, the weather, and backlit" {
+ fresh()
+ hour = 6.5
expect_eq(photo_score_light(0.0, -20.0), 20)
- phototest_st.hour = 12.0
+ hour = 12.0
expect_eq(photo_score_light(0.0, -20.0), 9)
- phototest_st.dim = 4
+ dim = 4
expect_eq(photo_score_light(0.0, -20.0), 5)
- phototest_st.dim = 0
- phototest_st.sun_z = -1.0
+ dim = 0
+ sun_z = -1.0
expect_eq(photo_score_light(0.0, -20.0), 2)
- phototest_st.sun_z = 0.0
- phototest_st.night = true
- phototest_st.moon = 1.0
+ sun_z = 0.0
+ night = true
+ moon = 1.0
expect_eq(photo_score_light(0.0, -20.0), 9)
}
- test "the moment counts by how much subject there is, and a judge of behaviour doubles it" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- shoot(photo_st, 1, 0.0, 70.0, 0.3, 20)
- let tiny = photo_pending_grade(photo_st)
- shoot(photo_st, 1, 0.0, 6.0, 1.1, 6)
- let big = photo_pending_grade(photo_st)
+ test "the moment counts by how much subject there is, and a judge of behaviour doubles it" {
+ fresh()
+ shoot(1, 0.0, 70.0, 0.3, 20)
+ let tiny = photo_pending_grade()
+ shoot(1, 0.0, 6.0, 1.1, 6)
+ let big = photo_pending_grade()
expect(big > tiny)
expect(tiny < 20)
- shoot(photo_st, 1, 0.0, 6.0, 1.1, 20)
- let busy = photo_pending_grade(photo_st)
- phototest_st.behaviour = true
- shoot(photo_st, 1, 0.0, 6.0, 1.1, 20)
- expect(photo_pending_grade(photo_st) > busy - 21)
- expect_near(photo_comp(photo_st, 3), 40.0, 0.01)
+ shoot(1, 0.0, 6.0, 1.1, 20)
+ let busy = photo_pending_grade()
+ behaviour = true
+ shoot(1, 0.0, 6.0, 1.1, 20)
+ expect(photo_pending_grade() > busy - 21)
+ expect_near(photo_comp(3), 40.0, 0.01)
}
- test "the frame takes its best subject, tags each kind once and every legend" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- photo_frame_begin(photo_st)
- photo_frame_subject(photo_st, 2, "elk", 0.0, 0.0, -60.0, 1.9, false, 6)
- let far = photo_pending_grade(photo_st)
- photo_frame_subject(photo_st, 2, "elk", 1.0, 0.0, -8.0, 1.9, false, 17)
- photo_frame_subject(photo_st, 4, "the Ghost Elk", -1.0, 0.0, -9.0, 2.2, true, 20)
- photo_frame_tag(photo_st, "Maroon Lake")
- expect(photo_pending_grade(photo_st) > far)
- expect(photo_pending_tags(photo_st) == "elk, the Ghost Elk, Maroon Lake")
- expect_eq(photo_pending_mask(photo_st), 4 + 16)
- expect_eq(photo_pending_legend(photo_st), 16)
- expect(photo_pending_why(photo_st) >= PHOTO_WHY_SIZE)
+ test "the frame takes its best subject, tags each kind once and every legend" {
+ fresh()
+ photo_frame_begin()
+ photo_frame_subject(2, "elk", 0.0, 0.0, -60.0, 1.9, false, 6)
+ let far = photo_pending_grade()
+ photo_frame_subject(2, "elk", 1.0, 0.0, -8.0, 1.9, false, 17)
+ photo_frame_subject(4, "the Ghost Elk", -1.0, 0.0, -9.0, 2.2, true, 20)
+ photo_frame_tag("Maroon Lake")
+ expect(photo_pending_grade() > far)
+ expect(photo_pending_tags() == "elk, the Ghost Elk, Maroon Lake")
+ expect_eq(photo_pending_mask(), 4 + 16)
+ expect_eq(photo_pending_legend(), 16)
+ expect(photo_pending_why() >= PHOTO_WHY_SIZE)
}
- test "a kept frame is on the roll, its subjects are photographed for good, and a full card says so" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- shoot(photo_st, 3, 0.0, 10.0, 1.1, 15)
- let g = photo_pending_grade(photo_st)
- expect_eq(photo_keep(base_st, photo_st, "a.png", 2, 7.5), 0)
- let fs = facts(base_st, photo_st)
+ test "a kept frame is on the roll, its subjects are photographed for good, and a full card says so" {
+ fresh()
+ shoot(3, 0.0, 10.0, 1.1, 15)
+ let g = photo_pending_grade()
+ expect_eq(photo_keep("a.png", 2, 7.5), 0)
+ let fs = facts()
expect_eq(count(fs, PHOTO_KEPT), 1)
expect_eq(fs[0].firsts, 8)
- expect_eq(photo_species_shot(photo_st), 8)
- expect_eq(photo_best(photo_st, 3), g)
- expect_eq(photo_day(photo_st, 0), 2)
- shoot(photo_st, 3, 0.0, 10.0, 1.1, 15)
- photo_keep(base_st, photo_st, "b.png", 2, 8.0)
- expect_eq(facts(base_st, photo_st)[0].firsts, 0)
- photo_keep(base_st, photo_st, "c.png", 2, 8.0)
- photo_keep(base_st, photo_st, "d.png", 2, 8.0)
- expect(photo_full(photo_st))
- expect_eq(photo_keep(base_st, photo_st, "e.png", 2, 8.0), -1)
- expect_eq(count(facts(base_st, photo_st), PHOTO_FULL), 1)
+ expect_eq(photo_species_shot(), 8)
+ expect_eq(photo_best(3), g)
+ expect_eq(photo_day(0), 2)
+ shoot(3, 0.0, 10.0, 1.1, 15)
+ photo_keep("b.png", 2, 8.0)
+ expect_eq(facts()[0].firsts, 0)
+ photo_keep("c.png", 2, 8.0)
+ photo_keep("d.png", 2, 8.0)
+ expect(photo_full())
+ expect_eq(photo_keep("e.png", 2, 8.0), -1)
+ expect_eq(count(facts(), PHOTO_FULL), 1)
}
- test "the first frame of a subject is worth its value on the grade's curve; sold is sold" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- photo_add(photo_st, 1, 9.0, 2, 0, false, "fox", "f.png", 60, PHOTO_WHY_NONE)
- photo_add(photo_st, 1, 9.5, 2, 2, false, "fox", "g.png", 95, PHOTO_WHY_FINE)
- photo_add(photo_st, 1, 9.5, 0, 0, false, "the valley", "h.png", 0, PHOTO_WHY_NONE)
- expect_eq(photo_value(photo_st, 0), int(11.0 * 1.5))
- expect_eq(photo_value(photo_st, 1), int(float(11 + 120) * 4.0))
- expect_eq(photo_value(photo_st, 2), 0)
- let total = photo_sell_all(base_st, photo_st)
+ test "the first frame of a subject is worth its value on the grade's curve; sold is sold" {
+ fresh()
+ photo_add(1, 9.0, 2, 0, false, "fox", "f.png", 60, PHOTO_WHY_NONE)
+ photo_add(1, 9.5, 2, 2, false, "fox", "g.png", 95, PHOTO_WHY_FINE)
+ photo_add(1, 9.5, 0, 0, false, "the valley", "h.png", 0, PHOTO_WHY_NONE)
+ expect_eq(photo_value(0), int(11.0 * 1.5))
+ expect_eq(photo_value(1), int(float(11 + 120) * 4.0))
+ expect_eq(photo_value(2), 0)
+ let total = photo_sell_all()
expect_eq(total, int(11.0 * 1.5) + int(120.0 * 4.0))
- expect_eq(count(facts(base_st, photo_st), PHOTO_SOLD), 2)
- expect(photo_sold(photo_st, 0))
- expect_eq(photo_value(photo_st, 0), 0)
- photo_add(photo_st, 1, 10.0, 2, 0, false, "fox", "i.png", 60, PHOTO_WHY_NONE)
- expect_eq(photo_value(photo_st, 3), 0)
+ expect_eq(count(facts(), PHOTO_SOLD), 2)
+ expect(photo_sold(0))
+ expect_eq(photo_value(0), 0)
+ photo_add(1, 10.0, 2, 0, false, "fox", "i.png", 60, PHOTO_WHY_NONE)
+ expect_eq(photo_value(3), 0)
}
- test "forgetting a frame closes the roll up, moves the cover, and names the file" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- photo_add(photo_st, 1, 9.0, 2, 0, false, "a", "a.png", 10, 0)
- photo_add(photo_st, 1, 9.0, 4, 0, false, "b", "b.png", 20, 0)
- photo_add(photo_st, 1, 9.0, 8, 0, false, "c", "c.png", 30, 0)
- photo_cover_set(photo_st, 2)
- photo_remove(base_st, photo_st, 0)
- expect_eq(photo_count(photo_st), 2)
- expect(photo_tags(photo_st, 0) == "b")
- expect_eq(photo_grade(photo_st, 1), 30)
- expect_eq(photo_cover(photo_st), 1)
- let fs = facts(base_st, photo_st)
+ test "forgetting a frame closes the roll up, moves the cover, and names the file" {
+ fresh()
+ photo_add(1, 9.0, 2, 0, false, "a", "a.png", 10, 0)
+ photo_add(1, 9.0, 4, 0, false, "b", "b.png", 20, 0)
+ photo_add(1, 9.0, 8, 0, false, "c", "c.png", 30, 0)
+ photo_cover_set(2)
+ photo_remove(0)
+ expect_eq(photo_count(), 2)
+ expect(photo_tags(0) == "b")
+ expect_eq(photo_grade(1), 30)
+ expect_eq(photo_cover(), 1)
+ let fs = facts()
expect(fs[0].file == "a.png")
- expect_eq(photo_species_shot(photo_st), 14)
+ expect_eq(photo_species_shot(), 14)
}
- test "the roll in order: newest first, best first, by subject, and a page at a time" (base_st: mut BaseState, photo_st: mut PhotoState) {
- photo_config(base_st, photo_st, 20)
- for i in 0 .. 10 { photo_add(photo_st, 1, 9.0, 1 << (i % 3), 0, false, "x", "x.png", (i * 37) % 100, 0) }
- expect_eq(photo_order(photo_st)[0], 9)
- photo_sort_next(photo_st)
- let best = photo_order(photo_st)
- expect(photo_grade(photo_st, best[0]) >= photo_grade(photo_st, best[1]))
- expect(photo_grade(photo_st, best[1]) >= photo_grade(photo_st, best[9]))
- photo_sort_next(photo_st)
- expect(photo_mask(photo_st, photo_order(photo_st)[0]) <= photo_mask(photo_st, photo_order(photo_st)[9]))
- expect_eq(photo_pages(photo_st), 2)
- expect_eq(len(photo_page_list(photo_st)), 8)
- photo_page_step(photo_st, 5)
- expect_eq(photo_page_now(photo_st), 1)
- expect_eq(len(photo_page_list(photo_st)), 2)
+ test "the roll in order: newest first, best first, by subject, and a page at a time" {
+ photo_config(20)
+ for i in 0 .. 10 { photo_add(1, 9.0, 1 << (i % 3), 0, false, "x", "x.png", (i * 37) % 100, 0) }
+ expect_eq(photo_order()[0], 9)
+ photo_sort_next()
+ let best = photo_order()
+ expect(photo_grade(best[0]) >= photo_grade(best[1]))
+ expect(photo_grade(best[1]) >= photo_grade(best[9]))
+ photo_sort_next()
+ expect(photo_mask(photo_order()[0]) <= photo_mask(photo_order()[9]))
+ expect_eq(photo_pages(), 2)
+ expect_eq(len(photo_page_list()), 8)
+ photo_page_step(5)
+ expect_eq(photo_page_now(), 1)
+ expect_eq(len(photo_page_list()), 2)
}
test "a file name from the tags" {
@@ -215,22 +213,22 @@ program PhotoTest {
expect(photo_slug("a very long list of names past twenty") == "a-very-long-list-of-name")
}
- test "the roll and what was ever photographed come back from the save" (base_st: mut BaseState, photo_st: mut PhotoState, phototest_st: mut PhototestState) {
- fresh(base_st, photo_st, phototest_st)
- photo_add(photo_st, 3, 18.25, 2, 2, true, "fox", "f.png", 80, PHOTO_WHY_LIGHT)
- photo_record_set(photo_st, 2 | 64, 2)
- photo_cover_set(photo_st, 0)
- let v = photo_save(photo_st)
- photo_reset(base_st, photo_st)
- expect_eq(photo_count(photo_st), 0)
- photo_load(base_st, photo_st, v, 1)
- expect_eq(photo_count(photo_st), 1)
- expect_near(photo_hour(photo_st, 0), 18.25, 0.01)
- expect(photo_sold(photo_st, 0))
- expect_eq(photo_why(photo_st, 0), PHOTO_WHY_LIGHT)
- expect_eq(photo_species_shot(photo_st), 66)
- expect_eq(photo_legend_shot(photo_st), 2)
- expect_eq(photo_best(photo_st, 1), 80)
- expect_eq(photo_cover(photo_st), 0)
+ test "the roll and what was ever photographed come back from the save" {
+ fresh()
+ photo_add(3, 18.25, 2, 2, true, "fox", "f.png", 80, PHOTO_WHY_LIGHT)
+ photo_record_set(2 | 64, 2)
+ photo_cover_set(0)
+ let v = photo_save()
+ photo_reset()
+ expect_eq(photo_count(), 0)
+ photo_load(v, 1)
+ expect_eq(photo_count(), 1)
+ expect_near(photo_hour(0), 18.25, 0.01)
+ expect(photo_sold(0))
+ expect_eq(photo_why(0), PHOTO_WHY_LIGHT)
+ expect_eq(photo_species_shot(), 66)
+ expect_eq(photo_legend_shot(), 2)
+ expect_eq(photo_best(1), 80)
+ expect_eq(photo_cover(), 0)
}
}
diff --git a/packages/ludic.photo/worth.ludic b/packages/ludic.photo/worth.ludic
index 6ac3d605..098ae917 100644
--- a/packages/ludic.photo/worth.ludic
+++ b/packages/ludic.photo/worth.ludic
@@ -1,38 +1,38 @@
# worth.ludic - what a buyer pays for a frame: the first frame of each subject and every legend,
# on the grade's curve and the lens's worth; sold is sold
-export function photo_value(photo_st: PhotoState, i: int) -> int {
- if i < 0 or i >= photo_st.pht_n or photo_st.pht_sold[i] != 0 { return 0 }
+export function photo_value(i: int) -> int {
+ if i < 0 or i >= pht_n or pht_sold[i] != 0 { return 0 }
var v = 0
for sp in 0 .. 32 {
- if (photo_st.pht_mask[i] & (1 << sp)) == 0 { continue }
+ if (pht_mask[i] & (1 << sp)) == 0 { continue }
var first = true
- for k in 0 .. photo_st.pht_n { if k != i and photo_st.pht_sold[k] != 0 and (photo_st.pht_mask[k] & (1 << sp)) != 0 { first = false } }
+ for k in 0 .. pht_n { if k != i and pht_sold[k] != 0 and (pht_mask[k] & (1 << sp)) != 0 { first = false } }
if first { v += PhotoRules.subject_value(sp) }
- if (photo_st.pht_legend[i] & (1 << sp)) != 0 { v += PhotoRules.legend_value(sp) }
+ if (pht_legend[i] & (1 << sp)) != 0 { v += PhotoRules.legend_value(sp) }
}
- if v > 0 { v = int(float(v) * (PhotoRules.mult() * photo_grade_mult(photo_st.pht_grade[i]))) }
+ if v > 0 { v = int(float(v) * (PhotoRules.mult() * photo_grade_mult(pht_grade[i]))) }
return v
}
# every frame with a price, sold: the total, and a fact a frame
-export function photo_sell_all(base_st: mut BaseState, photo_st: mut PhotoState) -> int {
+export function photo_sell_all() -> int {
var total = 0
- for i in 0 .. photo_st.pht_n {
- let v = photo_value(photo_st, i)
+ for i in 0 .. pht_n {
+ let v = photo_value(i)
if v > 0 {
total += v
- photo_st.pht_sold[i] = 1
- let f = pht_fact(base_st, photo_st, PHOTO_SOLD, i)
+ pht_sold[i] = 1
+ let f = pht_fact(PHOTO_SOLD, i)
f.v = v
- f.mask = photo_st.pht_mask[i]
+ f.mask = pht_mask[i]
}
}
return total
}
# what the roll would fetch now
-export function photo_worth(photo_st: PhotoState) -> int {
+export function photo_worth() -> int {
var total = 0
- for i in 0 .. photo_st.pht_n { total += photo_value(photo_st, i) }
+ for i in 0 .. pht_n { total += photo_value(i) }
return total
}
diff --git a/packages/ludic.prefs/prefs.ludic b/packages/ludic.prefs/prefs.ludic
index 6b1ddcf9..099e0a3a 100644
--- a/packages/ludic.prefs/prefs.ludic
+++ b/packages/ludic.prefs/prefs.ludic
@@ -9,51 +9,49 @@
const PREFS_MAX: int = 64
-state PrefsState {
- prefs_path: pointer = null
- prefs_names: pointers = null
- prefs_vals: words = null
- prefs_n: int = 0
-}
+var prefs_path: pointer = null
+var prefs_names: pointers = null
+var prefs_vals: words = null
+var 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 init() -> void {
+ if prefs_names != null { return }
+ prefs_names = pointers(PREFS_MAX)
+ prefs_vals = words(PREFS_MAX)
+ prefs_n = 0
}
- internal function find(prefs_st: PrefsState, key: pointer) -> int {
+ internal function find(key: pointer) -> int {
var i = 0
- while i < prefs_st.prefs_n { if prefs_st.prefs_names[i] == key { return i }; i += 1 }
+ while i < prefs_n { if prefs_names[i] == key { return i }; i += 1 }
return -1
}
- export function get(prefs_st: mut PrefsState, key: pointer, fallback: int) -> int {
- init(prefs_st)
- let i = find(prefs_st, key)
+ export function get(key: pointer, fallback: int) -> int {
+ init()
+ let i = find(key)
if i < 0 { return fallback }
- return prefs_st.prefs_vals[i]
+ return prefs_vals[i]
}
# keep the larger of the stored value and `value`; true when `value` is a new record
- export function max(prefs_st: mut PrefsState, key: pointer, value: int) -> bool {
- if value <= get(prefs_st, key, 0) { return false }
- set(prefs_st, key, value)
+ export function max(key: pointer, value: int) -> bool {
+ if value <= get(key, 0) { return false }
+ set(key, value)
return true
}
- export function set(prefs_st: mut PrefsState, key: pointer, value: int) -> void {
- init(prefs_st)
- var i = find(prefs_st, key)
+ export function set(key: pointer, value: int) -> void {
+ init()
+ var i = find(key)
if i < 0 {
- if prefs_st.prefs_n >= PREFS_MAX { return }
- i = prefs_st.prefs_n
- prefs_st.prefs_names[i] = key
- prefs_st.prefs_n += 1
+ if prefs_n >= PREFS_MAX { return }
+ i = prefs_n
+ prefs_names[i] = key
+ prefs_n += 1
}
- prefs_st.prefs_vals[i] = value
+ prefs_vals[i] = value
}
# the text of a whole file, or null when it does not exist
@@ -71,7 +69,7 @@ namespace Prefs {
}
# digits after `=` on one line -> an int (a leading '-' negates)
- internal function parse_line(prefs_st: mut PrefsState, buf: pointer, from: int, to: int) -> void {
+ internal function parse_line(buf: pointer, from: int, to: int) -> void {
var eq = from
while (eq < to) and (buf[eq] != '=') { eq += 1 } # '='
if eq >= to { return }
@@ -85,21 +83,21 @@ namespace Prefs {
if (p < to) and (buf[p] == '-') { neg = true; p += 1 } # '-'
while (p < to) and (buf[p] >= '0') and (buf[p] <= '9') { v = v * 10 + (buf[p] - 48); p += 1 }
if neg { v = -v }
- set(prefs_st, key, v)
+ set(key, v)
}
- export function load(prefs_st: mut PrefsState, path: pointer) -> void {
- init(prefs_st)
- prefs_st.prefs_path = path
+ export function load(path: pointer) -> void {
+ init()
+ prefs_path = path
let buf = read_all(path)
if buf == null { return }
var start = 0
var i = 0
while buf[i] != 0 {
- if buf[i] == '\n' { parse_line(prefs_st, buf, start, i); start = i + 1 } # newline
+ if buf[i] == '\n' { parse_line(buf, start, i); start = i + 1 } # newline
i += 1
}
- if i > start { parse_line(prefs_st, buf, start, i) }
+ if i > start { parse_line(buf, start, i) }
}
internal function write_text(f: pointer, s: pointer) -> void {
@@ -108,14 +106,14 @@ namespace Prefs {
file_write(f, s, n)
}
- 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")
+ export function save() -> void {
+ init()
+ if prefs_path == null { return }
+ let f = file_open(prefs_path, "wb")
if f == null { return }
var i = 0
- while i < prefs_st.prefs_n {
- write_text(f, `{prefs_st.prefs_names[i]}={string(prefs_st.prefs_vals[i])}\n`)
+ while i < prefs_n {
+ write_text(f, `{prefs_names[i]}={string(prefs_vals[i])}\n`)
i += 1
}
file_close(f)
diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic
index 96128dc1..cb6e1dfd 100644
--- a/packages/ludic.render3d/actor.ludic
+++ b/packages/ludic.render3d/actor.ludic
@@ -58,35 +58,44 @@ property AcProg {
scene_frame: int = -1 # the frame its scene uniforms were bound
}
+var ac_lit: AcProg = null
+var ac_lit_cut: AcProg = null
+var ac_sh: AcProg = null
+var ac_sh_cut: AcProg = null
+var ac_out: AcProg = null
+var ac_out_cut: AcProg = null
+var ac_actors: []Actor = null
+var ac_next_id: int = 0
+var ac_frame: int = 0
-function ac_prog_new(render3d_st: mut Render3dState, vs: string, fs: string, defs: string) -> AcProg {
+function ac_prog_new(vs: string, fs: string, defs: string) -> AcProg {
let a = new AcProg
- a.prog = r3d_program(render3d_st, vs, fs, defs)
+ a.prog = r3d_program(vs, fs, defs)
let p = a.prog
- a.l_model = gpu_uniform(render3d_st, p, "u_model"); a.l_skin = gpu_uniform(render3d_st, p, "u_skinned"); a.l_lvp = gpu_uniform(render3d_st, p, "u_light_vp")
- a.l_bones = gpu_uniform(render3d_st, p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gpu_uniform(render3d_st, p, "u_bones") }
- a.l_tint = gpu_uniform(render3d_st, p, "u_tint"); a.l_rough = gpu_uniform(render3d_st, p, "u_rough_scale"); a.l_emis = gpu_uniform(render3d_st, p, "u_emissive")
- a.l_view = gpu_uniform(render3d_st, p, "u_view"); a.l_proj = gpu_uniform(render3d_st, p, "u_proj"); a.l_mh = gpu_uniform(render3d_st, p, "u_model_h")
- a.l_out = gpu_uniform(render3d_st, p, "u_outline"); a.l_ocol = gpu_uniform(render3d_st, p, "u_outline_col")
+ a.l_model = gpu_uniform(p, "u_model"); a.l_skin = gpu_uniform(p, "u_skinned"); a.l_lvp = gpu_uniform(p, "u_light_vp")
+ a.l_bones = gpu_uniform(p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gpu_uniform(p, "u_bones") }
+ a.l_tint = gpu_uniform(p, "u_tint"); a.l_rough = gpu_uniform(p, "u_rough_scale"); a.l_emis = gpu_uniform(p, "u_emissive")
+ a.l_view = gpu_uniform(p, "u_view"); a.l_proj = gpu_uniform(p, "u_proj"); a.l_mh = gpu_uniform(p, "u_model_h")
+ a.l_out = gpu_uniform(p, "u_outline"); a.l_ocol = gpu_uniform(p, "u_outline_col")
# the samplers never move: units 0, 1, 2
- gpu_use_program(render3d_st, p)
- u_i(render3d_st, gpu_uniform(render3d_st, p, "u_diff"), 0); u_i(render3d_st, gpu_uniform(render3d_st, p, "u_nrm"), 1); u_i(render3d_st, gpu_uniform(render3d_st, p, "u_arm"), 2)
+ gpu_use_program(p)
+ u_i(gpu_uniform(p, "u_diff"), 0); u_i(gpu_uniform(p, "u_nrm"), 1); u_i(gpu_uniform(p, "u_arm"), 2)
return a
}
-function actor_init(render3d_st: mut Render3dState) -> void {
- render3d_st.ac_lit = ac_prog_new(render3d_st, "skin.vert", "model.frag", "")
- render3d_st.ac_lit_cut = ac_prog_new(render3d_st, "skin.vert", "model.frag", "#define ALPHA_TEST\n")
- render3d_st.ac_sh = ac_prog_new(render3d_st, "skin.vert", "shadow.frag", "#define SHADOW_PASS\n")
- render3d_st.ac_sh_cut = ac_prog_new(render3d_st, "skin.vert", "shadow.frag", "#define SHADOW_PASS\n#define ALPHA_TEST\n")
- render3d_st.ac_out = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n")
- render3d_st.ac_out_cut = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n")
- render3d_st.ac_actors = new []Actor
+function actor_init() -> void {
+ ac_lit = ac_prog_new("skin.vert", "model.frag", "")
+ ac_lit_cut = ac_prog_new("skin.vert", "model.frag", "#define ALPHA_TEST\n")
+ ac_sh = ac_prog_new("skin.vert", "shadow.frag", "#define SHADOW_PASS\n")
+ ac_sh_cut = ac_prog_new("skin.vert", "shadow.frag", "#define SHADOW_PASS\n#define ALPHA_TEST\n")
+ ac_out = ac_prog_new("skin.vert", "outline.frag", "#define OUTLINE\n")
+ ac_out_cut = ac_prog_new("skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n")
+ ac_actors = new []Actor
}
-function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void {
- if render3d_st.ac_actors == null { return }
+function actor_remove(a: Actor) -> void {
+ if ac_actors == null { return }
let keep = new []Actor
- for i in 0 .. len(render3d_st.ac_actors) { if render3d_st.ac_actors[i].id != a.id { push(keep, render3d_st.ac_actors[i]) } }
- render3d_st.ac_actors = keep
+ for i in 0 .. len(ac_actors) { if ac_actors[i].id != a.id { push(keep, ac_actors[i]) } }
+ ac_actors = keep
}
# colour one named part of the model (a material name from the file)
function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void {
@@ -104,18 +113,18 @@ function actor_hide_part(a: Actor, name: string, hidden: bool) -> void {
for i in 0 .. n { if a.model.prims[i].name == name { a.hide[i] = v } }
}
-function actor_new(render3d_st: mut Render3dState, model: Model) -> Actor {
+function actor_new(model: Model) -> Actor {
let a = new Actor
a.model = model
a.scale = 1.0
a.tint = v3_new(1.0, 1.0, 1.0)
a.rough = 1.0
a.mat = m4_new()
- render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id
+ ac_next_id += 1; a.id = ac_next_id
if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 }
a.cull = 450.0
- if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor }
- push(render3d_st.ac_actors, a)
+ if ac_actors == null { ac_actors = new []Actor }
+ push(ac_actors, a)
return a
}
@@ -125,24 +134,24 @@ function actor_place(a: Actor, x: float, y: float, z: float, yaw: float) -> void
}
# the scene's lighting for a lit program, once per frame
-function ac_bind_scene(render3d_st: mut Render3dState, ap: AcProg) -> void {
- if ap.scene_frame == render3d_st.ac_frame { return }
- ap.scene_frame = render3d_st.ac_frame
+function ac_bind_scene(ap: AcProg) -> void {
+ if ap.scene_frame == ac_frame { return }
+ ap.scene_frame = ac_frame
let p = ap.prog
- gpu_use_program(render3d_st, p)
- u_mat4(render3d_st, ap.l_view, render3d_st.cam_view)
- u_mat4(render3d_st, ap.l_proj, render3d_st.cam_proj)
- u_f(render3d_st, ap.l_mh, 0.0)
- sky_bind_lighting(render3d_st, p)
- shadow_bind(render3d_st, p)
- fog_bind(render3d_st, p)
+ gpu_use_program(p)
+ u_mat4(ap.l_view, cam_view)
+ u_mat4(ap.l_proj, cam_proj)
+ u_f(ap.l_mh, 0.0)
+ sky_bind_lighting(p)
+ shadow_bind(p)
+ fog_bind(p)
}
-function ac_visible(render3d_st: Render3dState, a: Actor, shadow: bool) -> bool {
+function ac_visible(a: Actor, shadow: bool) -> bool {
if a.model == null { return false }
if not a.visible and not (shadow and a.cast_hidden) { return false }
if shadow and not a.casts { return false }
if a.cull != 0.0 {
- let dx = a.x - render3d_st.cam_pos[0]; let dz = a.z - render3d_st.cam_pos[2]
+ let dx = a.x - cam_pos[0]; let dz = a.z - cam_pos[2]
let d2 = dx * dx + dz * dz
var c = a.cull
if shadow { c = Math.min(c, 300.0) }
@@ -155,38 +164,38 @@ function ac_visible(render3d_st: Render3dState, a: Actor, shadow: bool) -> bool
# keeps the main camera's frustum planes, so the image is what has to be tested against them: the
# actor itself was, and the town's people - far above the lake, their images far below the frame -
# all drew again into the reflection.
- if render3d_st.ter_reflect { cy = 2.0 * render3d_st.water_level - cy }
- if not cam_sphere_visible(render3d_st, a.x, cy, a.z, r * 1.5) { return false }
+ if ter_reflect { cy = 2.0 * water_level - cy }
+ if not cam_sphere_visible(a.x, cy, a.z, r * 1.5) { return false }
}
return true
}
-function actor_draw_one(render3d_st: mut Render3dState, a: Actor, ap: AcProg, shadow: bool) -> void {
+function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
let p = ap.prog
- gpu_use_program(render3d_st, p)
- u_mat4(render3d_st, ap.l_model, a.mat)
+ gpu_use_program(p)
+ u_mat4(ap.l_model, a.mat)
var skinned = 0.0
- if a.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.skin.n_joints, a.skin.bones) }
- else if a.model.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
- u_f(render3d_st, ap.l_skin, skinned)
+ if a.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
+ else if a.model.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
+ u_f(ap.l_skin, skinned)
if not shadow {
- u_f(render3d_st, ap.l_emis, a.emissive)
- u_f(render3d_st, ap.l_rough, a.rough)
+ u_f(ap.l_emis, a.emissive)
+ u_f(ap.l_rough, a.rough)
}
- gpu_cull(render3d_st, false)
+ gpu_cull(false)
let model = a.model
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
if a.hide != null and a.hide[i] != 0 { continue }
if not shadow {
- if a.ptint != null and a.ptint[i * 4] != 0 { u_f3(render3d_st, ap.l_tint, float_from_bits(a.ptint[i * 4 + 1]), float_from_bits(a.ptint[i * 4 + 2]), float_from_bits(a.ptint[i * 4 + 3])) }
- else { u_v3(render3d_st, ap.l_tint, a.tint) }
+ if a.ptint != null and a.ptint[i * 4] != 0 { u_f3(ap.l_tint, float_from_bits(a.ptint[i * 4 + 1]), float_from_bits(a.ptint[i * 4 + 2]), float_from_bits(a.ptint[i * 4 + 3])) }
+ else { u_v3(ap.l_tint, a.tint) }
}
- gpu_tex_unit(render3d_st, 0); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.diff)
+ gpu_tex_unit(0); gpu_tex_bind(GPU_TEX2D, pr.diff)
if not shadow {
- gpu_tex_unit(render3d_st, 1); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.nrm)
- gpu_tex_unit(render3d_st, 2); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.arm)
+ gpu_tex_unit(1); gpu_tex_bind(GPU_TEX2D, pr.nrm)
+ gpu_tex_unit(2); gpu_tex_bind(GPU_TEX2D, pr.arm)
}
- mesh_draw(render3d_st, pr.mesh)
+ mesh_draw(pr.mesh)
}
}
@@ -215,38 +224,41 @@ property OutlineReq {
g: float = 0.0,
b: float = 0.0
}
+var ac_oq: []OutlineReq = null
+var ac_oq_n: int = 0 # live entries; the array is kept and reused
# A frame is drawn by more than one pass - a shadow map, a water reflection, the scene -
# and actor_draw runs in each of them. An Actor survives that because its rim is a field
# on the actor; a queue does not, if the first pass empties it. So a flush CLOSES the
# batch rather than clearing it, and the next add opens a new one: every pass in a frame
# sees the same requests, and the caller needs no frame hook.
-function outline_model(render3d_st: mut Render3dState, m: Model, mat: floats, width: float, r: float, g: float, b: float) -> void {
+var ac_oq_closed: bool = true
+function outline_model(m: Model, mat: floats, width: float, r: float, g: float, b: float) -> void {
if m == null or mat == null or width == 0.0 { return }
- if render3d_st.ac_oq_closed { render3d_st.ac_oq_n = 0; render3d_st.ac_oq_closed = false }
- if render3d_st.ac_oq == null { render3d_st.ac_oq = new []OutlineReq }
+ if ac_oq_closed { ac_oq_n = 0; ac_oq_closed = false }
+ if ac_oq == null { ac_oq = new []OutlineReq }
var q: OutlineReq = null
- if render3d_st.ac_oq_n < len(render3d_st.ac_oq) { q = render3d_st.ac_oq[render3d_st.ac_oq_n] } else { q = new OutlineReq; q.mat = m4_new(); push(render3d_st.ac_oq, q) }
+ if ac_oq_n < len(ac_oq) { q = ac_oq[ac_oq_n] } else { q = new OutlineReq; q.mat = m4_new(); push(ac_oq, q) }
q.model = m; q.width = width; q.r = r; q.g = g; q.b = b
m4_copy(q.mat, mat)
- render3d_st.ac_oq_n += 1
+ ac_oq_n += 1
}
-function outline_clear(render3d_st: mut Render3dState) -> void { render3d_st.ac_oq_n = 0; render3d_st.ac_oq_closed = true }
+function outline_clear() -> void { ac_oq_n = 0; ac_oq_closed = true }
# Called at the top of every frame. A batch the last frame closed and nobody has reopened
# since is stale: the caller stopped asking. Without this it was drawn for ever - flush
# closes a batch but only the NEXT outline_model call empties it, so looking away from a
# highlighted tree left its rim on until something else was highlighted.
-function outline_frame(render3d_st: mut Render3dState) -> void { if render3d_st.ac_oq_closed { render3d_st.ac_oq_n = 0 } }
+function outline_frame() -> void { if ac_oq_closed { ac_oq_n = 0 } }
# R3D_ACTOR_CENSUS=: at that frame, the lit pass's actors grouped by model - skinned or rigid,
# how many share it, the primitives each draws - so instancing is aimed at what repeats
-function actor_census(render3d_st: Render3dState) -> void {
+function actor_census() -> void {
let keys = new []Model
let cnt = new []int
let prims = new []int
let rigid = new []int
- for i in 0 .. len(render3d_st.ac_actors) {
- let a = render3d_st.ac_actors[i]
- if not ac_visible(render3d_st, a, false) { continue }
+ for i in 0 .. len(ac_actors) {
+ let a = ac_actors[i]
+ if not ac_visible(a, false) { continue }
var k = -1
for j in 0 .. len(keys) { if keys[j] == a.model { k = j } }
if k < 0 { push(keys, a.model); push(cnt, 0); push(prims, 0); var r = 1; if a.model.skin != null { r = 0 }; push(rigid, r); k = len(keys) - 1 }
@@ -265,70 +277,71 @@ function actor_census(render3d_st: Render3dState) -> void {
}
print(`actor census: {len(keys)} models, {total} lit draws`)
}
-function actor_draw(render3d_st: mut Render3dState) -> void {
- if render3d_st.ac_actors == null { return }
- render3d_st.ac_frame += 1
- if render3d_st.ac_census_at == -2 { render3d_st.ac_census_at = -1; if r3d_env_has(render3d_st, "R3D_ACTOR_CENSUS") { render3d_st.ac_census_at = Text.to_int(r3d_env(render3d_st, "R3D_ACTOR_CENSUS")) } }
- if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at { actor_census(render3d_st) }
- for i in 0 .. len(render3d_st.ac_actors) {
- let a = render3d_st.ac_actors[i]
- if not ac_visible(render3d_st, a, false) { continue }
- var ap = render3d_st.ac_lit
- if a.cutout { ap = render3d_st.ac_lit_cut }
- ac_bind_scene(render3d_st, ap)
- actor_draw_one(render3d_st, a, ap, false)
+var ac_census_at: int = -2
+function actor_draw() -> void {
+ if ac_actors == null { return }
+ ac_frame += 1
+ if ac_census_at == -2 { ac_census_at = -1; if r3d_env_has("R3D_ACTOR_CENSUS") { ac_census_at = Text.to_int(r3d_env("R3D_ACTOR_CENSUS")) } }
+ if ac_census_at > 0 and ac_frame == ac_census_at { actor_census() }
+ for i in 0 .. len(ac_actors) {
+ let a = ac_actors[i]
+ if not ac_visible(a, false) { continue }
+ var ap = ac_lit
+ if a.cutout { ap = ac_lit_cut }
+ ac_bind_scene(ap)
+ actor_draw_one(a, ap, false)
}
- actor_draw_outlines(render3d_st)
- gpu_cull(render3d_st, true)
+ actor_draw_outlines()
+ gpu_cull(true)
}
# The rim around a highlighted actor: the model again, its vertices pushed out along their
# normals (skin.vert, OUTLINE) and its front faces culled, so only the far side of the
# swollen shell survives — a silhouette exactly `outline` metres wide. Depth-tested
# against the scene, so anything in front of the actor hides its rim too.
-function actor_draw_outlines(render3d_st: mut Render3dState) -> void {
- var any = render3d_st.ac_oq_n > 0
- for i in 0 .. len(render3d_st.ac_actors) { if render3d_st.ac_actors[i].outline != 0.0 and ac_visible(render3d_st, render3d_st.ac_actors[i], false) { any = true; break } }
+function actor_draw_outlines() -> void {
+ var any = ac_oq_n > 0
+ for i in 0 .. len(ac_actors) { if ac_actors[i].outline != 0.0 and ac_visible(ac_actors[i], false) { any = true; break } }
if not any { return }
- gpu_cull(render3d_st, true)
- gpu_cull_face(render3d_st, GL_FRONT)
- for i in 0 .. len(render3d_st.ac_actors) {
- let a = render3d_st.ac_actors[i]
- if a.outline == 0.0 or not ac_visible(render3d_st, a, false) { continue }
- var ap = render3d_st.ac_out
- if a.cutout { ap = render3d_st.ac_out_cut }
- gpu_use_program(render3d_st, ap.prog)
- u_mat4(render3d_st, ap.l_view, render3d_st.cam_view); u_mat4(render3d_st, ap.l_proj, render3d_st.cam_proj)
- u_f(render3d_st, ap.l_out, a.outline * a.scale)
- if a.ocol != null { u_v3(render3d_st, ap.l_ocol, a.ocol) } else { u_f3(render3d_st, ap.l_ocol, 1.0, 1.0, 1.0) }
- actor_draw_outline_one(render3d_st, a, ap)
+ gpu_cull(true)
+ gpu_cull_face(GL_FRONT)
+ for i in 0 .. len(ac_actors) {
+ let a = ac_actors[i]
+ if a.outline == 0.0 or not ac_visible(a, false) { continue }
+ var ap = ac_out
+ if a.cutout { ap = ac_out_cut }
+ gpu_use_program(ap.prog)
+ u_mat4(ap.l_view, cam_view); u_mat4(ap.l_proj, cam_proj)
+ u_f(ap.l_out, a.outline * a.scale)
+ if a.ocol != null { u_v3(ap.l_ocol, a.ocol) } else { u_f3(ap.l_ocol, 1.0, 1.0, 1.0) }
+ actor_draw_outline_one(a, ap)
}
# and whatever asked for a rim without being an actor
- for i in 0 .. render3d_st.ac_oq_n {
- let q = render3d_st.ac_oq[i]
- let ap = render3d_st.ac_out
- gpu_use_program(render3d_st, ap.prog)
- u_mat4(render3d_st, ap.l_view, render3d_st.cam_view); u_mat4(render3d_st, ap.l_proj, render3d_st.cam_proj)
- u_f(render3d_st, ap.l_out, q.width)
- u_f3(render3d_st, ap.l_ocol, q.r, q.g, q.b)
- u_mat4(render3d_st, ap.l_model, q.mat)
- u_f(render3d_st, ap.l_skin, 0.0)
- for k in 0 .. len(q.model.prims) { mesh_draw(render3d_st, q.model.prims[k].mesh) }
+ for i in 0 .. ac_oq_n {
+ let q = ac_oq[i]
+ let ap = ac_out
+ gpu_use_program(ap.prog)
+ u_mat4(ap.l_view, cam_view); u_mat4(ap.l_proj, cam_proj)
+ u_f(ap.l_out, q.width)
+ u_f3(ap.l_ocol, q.r, q.g, q.b)
+ u_mat4(ap.l_model, q.mat)
+ u_f(ap.l_skin, 0.0)
+ for k in 0 .. len(q.model.prims) { mesh_draw(q.model.prims[k].mesh) }
}
- render3d_st.ac_oq_closed = true
- gpu_cull_face(render3d_st, GL_BACK)
+ ac_oq_closed = true
+ gpu_cull_face(GL_BACK)
}
-function actor_draw_outline_one(render3d_st: mut Render3dState, a: Actor, ap: AcProg) -> void {
- u_mat4(render3d_st, ap.l_model, a.mat)
+function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {
+ u_mat4(ap.l_model, a.mat)
var skinned = 0.0
- if a.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.skin.n_joints, a.skin.bones) }
- else if a.model.skin != null { skinned = 1.0; u_mat4n(render3d_st, ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
- u_f(render3d_st, ap.l_skin, skinned)
+ if a.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
+ else if a.model.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
+ u_f(ap.l_skin, skinned)
let model = a.model
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
if a.hide != null and a.hide[i] != 0 { continue }
- if a.cutout { gpu_tex_unit(render3d_st, 0); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.diff) }
- mesh_draw(render3d_st, pr.mesh)
+ if a.cutout { gpu_tex_unit(0); gpu_tex_bind(GPU_TEX2D, pr.diff) }
+ mesh_draw(pr.mesh)
}
}
# Is a sphere inside this cascade's light box? The projection is orthographic, so a point's
@@ -336,66 +349,74 @@ function actor_draw_outline_one(render3d_st: mut Render3dState, a: Actor, ap: Ac
# three axis offsets. An actor outside the box casts nothing into that layer: skipping it changes no
# depth, only the draw. (Each actor used to draw into every cascade out to 300 m - in town that was
# about 1500 skinned shadow draws a frame against 69 in the lit pass.)
-function ac_in_light(render3d_st: mut Render3dState, vp: floats, x: float, y: float, z: float, r: float) -> bool {
- if render3d_st.ac_lp0 == null { render3d_st.ac_lp0 = floats(3); render3d_st.ac_lpx = floats(3); render3d_st.ac_lpy = floats(3); render3d_st.ac_lpz = floats(3) }
- m4_xform_point(render3d_st.ac_lp0, vp, x, y, z)
- m4_xform_point(render3d_st.ac_lpx, vp, x + r, y, z)
- m4_xform_point(render3d_st.ac_lpy, vp, x, y + r, z)
- m4_xform_point(render3d_st.ac_lpz, vp, x, y, z + r)
+var ac_lp0: floats = null
+var ac_lpx: floats = null
+var ac_lpy: floats = null
+var ac_lpz: floats = null
+function ac_in_light(vp: floats, x: float, y: float, z: float, r: float) -> bool {
+ if ac_lp0 == null { ac_lp0 = floats(3); ac_lpx = floats(3); ac_lpy = floats(3); ac_lpz = floats(3) }
+ m4_xform_point(ac_lp0, vp, x, y, z)
+ m4_xform_point(ac_lpx, vp, x + r, y, z)
+ m4_xform_point(ac_lpy, vp, x, y + r, z)
+ m4_xform_point(ac_lpz, vp, x, y, z + r)
for k in 0 .. 2 {
- let c = render3d_st.ac_lp0[k]
- let e = ac_abs(render3d_st.ac_lpx[k] - c) + ac_abs(render3d_st.ac_lpy[k] - c) + ac_abs(render3d_st.ac_lpz[k] - c)
+ let c = ac_lp0[k]
+ let e = ac_abs(ac_lpx[k] - c) + ac_abs(ac_lpy[k] - c) + ac_abs(ac_lpz[k] - c)
if ac_abs(c) - e > 1.0 { return false }
}
return true
}
function ac_abs(v: float) -> float { return Math.max(v, -v) }
-function actor_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void {
- if render3d_st.ac_actors == null { return }
- gpu_use_program(render3d_st, render3d_st.ac_sh.prog); u_mat4(render3d_st, render3d_st.ac_sh.l_lvp, light_vp)
- gpu_use_program(render3d_st, render3d_st.ac_sh_cut.prog); u_mat4(render3d_st, render3d_st.ac_sh_cut.l_lvp, light_vp)
- for i in 0 .. len(render3d_st.ac_actors) {
- let a = render3d_st.ac_actors[i]
- if not ac_visible(render3d_st, a, true) { continue }
+function actor_draw_casters(light_vp: floats) -> void {
+ if ac_actors == null { return }
+ gpu_use_program(ac_sh.prog); u_mat4(ac_sh.l_lvp, light_vp)
+ gpu_use_program(ac_sh_cut.prog); u_mat4(ac_sh_cut.l_lvp, light_vp)
+ for i in 0 .. len(ac_actors) {
+ let a = ac_actors[i]
+ if not ac_visible(a, true) { continue }
if a.radius != 0.0 {
# the radius is the horizontal extent: a person is far taller than wide, so the sphere is
# centred at half the model's height and reaches the larger of the two, with a margin for
# the pose (a first version centred it at the radius and dropped a head at a cascade's edge)
let hh = a.model.height * 0.5 * a.scale
let r = Math.max(a.radius * a.scale, hh)
- if not ac_in_light(render3d_st, light_vp, a.x, a.y + hh, a.z, r * 1.5) { continue }
+ if not ac_in_light(light_vp, a.x, a.y + hh, a.z, r * 1.5) { continue }
}
# Inside the light box is not the same as able to shade anything this cascade covers: every actor
# within 300 m sits inside the far cascades' boxes, whose receivers start 250 and 1100 m out - at
# the camp 160 actors cast 300 draws into each of those, against 41 into the nearest.
- let dxa = a.x - render3d_st.cam_pos[0]; let dza = a.z - render3d_st.cam_pos[2]
+ let dxa = a.x - cam_pos[0]; let dza = a.z - cam_pos[2]
let da = Math.sqrt(dxa * dxa + dza * dza)
let ra = a.radius * a.scale
- if not cast_band_reaches(render3d_st, Math.max(da - ra, 0.0), da + ra, a.model.height * a.scale) { continue }
- var ap = render3d_st.ac_sh
- if a.cutout { ap = render3d_st.ac_sh_cut }
- if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at { ac_caster_count(render3d_st, a) }
- actor_draw_one(render3d_st, a, ap, true)
+ if not cast_band_reaches(Math.max(da - ra, 0.0), da + ra, a.model.height * a.scale) { continue }
+ var ap = ac_sh
+ if a.cutout { ap = ac_sh_cut }
+ if ac_census_at > 0 and ac_frame == ac_census_at { ac_caster_count(a) }
+ actor_draw_one(a, ap, true)
}
- if render3d_st.ac_census_at > 0 and render3d_st.ac_frame == render3d_st.ac_census_at {
- print(`actor census, cascade {render3d_st.sh_cascade}: {render3d_st.ac_cc_actors} actors cast {render3d_st.ac_cc_draws} draws ({render3d_st.ac_cc_skinned} of them skinned, {render3d_st.ac_cc_cut} cutout)`)
- render3d_st.ac_cc_actors = 0; render3d_st.ac_cc_draws = 0; render3d_st.ac_cc_skinned = 0; render3d_st.ac_cc_cut = 0
+ if ac_census_at > 0 and ac_frame == ac_census_at {
+ print(`actor census, cascade {sh_cascade}: {ac_cc_actors} actors cast {ac_cc_draws} draws ({ac_cc_skinned} of them skinned, {ac_cc_cut} cutout)`)
+ ac_cc_actors = 0; ac_cc_draws = 0; ac_cc_skinned = 0; ac_cc_cut = 0
}
- prof_cpu_mark(render3d_st, "shadow actors")
+ prof_cpu_mark("shadow actors")
}
-function ac_caster_count(render3d_st: mut Render3dState, a: Actor) -> void {
+var ac_cc_actors: int = 0
+var ac_cc_draws: int = 0
+var ac_cc_skinned: int = 0
+var ac_cc_cut: int = 0
+function ac_caster_count(a: Actor) -> void {
var shown = 0
for p in 0 .. len(a.model.prims) { if a.hide == null or a.hide[p] == 0 { shown += 1 } }
- render3d_st.ac_cc_actors += 1
- render3d_st.ac_cc_draws += shown
- if a.skin != null or a.model.skin != null { render3d_st.ac_cc_skinned += shown }
- if a.cutout { render3d_st.ac_cc_cut += shown }
+ ac_cc_actors += 1
+ ac_cc_draws += shown
+ if a.skin != null or a.model.skin != null { ac_cc_skinned += shown }
+ if a.cutout { ac_cc_cut += shown }
}
# every actor off the stage at once, for a world being replaced. Actors own no GL objects;
# their models belong to whoever loaded them.
-function actor_clear_all(render3d_st: mut Render3dState) -> void {
- render3d_st.ac_actors = new []Actor
- outline_clear(render3d_st)
+function actor_clear_all() -> void {
+ ac_actors = new []Actor
+ outline_clear()
}
diff --git a/packages/ludic.render3d/camera.ludic b/packages/ludic.render3d/camera.ludic
index 1adc76b0..001ee61f 100644
--- a/packages/ludic.render3d/camera.ludic
+++ b/packages/ludic.render3d/camera.ludic
@@ -3,58 +3,75 @@
# projection matrices. Float bits throughout; see fmath.ludic.
# ============================================================================
+var cam_pos: floats = null # x, y, z
+var cam_yaw: float = 0.0 # radians, 0 = looking down -z
+var cam_pitch: float = 0.0
+var cam_fov: float = 0.0 # vertical, radians
+var cam_near: float = 0.0
+var cam_far: float = 0.0
+var cam_aspect: float = 0.0
+var cam_view: floats = null
+var cam_proj: floats = null
+var cam_vp: floats = null
+var cam_inv_vp: floats = null
+var cam_inv_proj: floats = null
+var cam_vp_clean: floats = null # view-projection, kept for depth reconstruction
+var cam_inv_vp_clean: floats = null
+var cam_fwd: floats = null
+var cam_right: floats = null
# the view frustum's four side planes (a, b, c, d), float bits: left, right, bottom, top;
# from the clean (unjittered) view-projection, column-major m[col * 4 + row]
+var cam_planes: floats = null
-function cam_init(render3d_st: mut Render3dState, aspect: float) -> void {
- render3d_st.cam_pos = v3_new(0.0, 2.0, 0.0)
- render3d_st.cam_view = m4_new(); render3d_st.cam_proj = m4_new(); render3d_st.cam_vp = m4_new(); render3d_st.cam_inv_vp = m4_new(); render3d_st.cam_inv_proj = m4_new()
- render3d_st.cam_vp_clean = m4_new(); render3d_st.cam_inv_vp_clean = m4_new()
- render3d_st.cam_fwd = v3_new(0.0, 0.0, -1.0)
- render3d_st.cam_right = v3_new(1.0, 0.0, 0.0)
- render3d_st.cam_fov = Math.deg_to_rad(42.0)
- render3d_st.cam_near = 0.3
- render3d_st.cam_far = 14000.0
- render3d_st.cam_aspect = aspect
- cam_update(render3d_st)
+function cam_init(aspect: float) -> void {
+ cam_pos = v3_new(0.0, 2.0, 0.0)
+ cam_view = m4_new(); cam_proj = m4_new(); cam_vp = m4_new(); cam_inv_vp = m4_new(); cam_inv_proj = m4_new()
+ cam_vp_clean = m4_new(); cam_inv_vp_clean = m4_new()
+ cam_fwd = v3_new(0.0, 0.0, -1.0)
+ cam_right = v3_new(1.0, 0.0, 0.0)
+ cam_fov = Math.deg_to_rad(42.0)
+ cam_near = 0.3
+ cam_far = 14000.0
+ cam_aspect = aspect
+ cam_update()
}
-function cam_begin_frame(render3d_st: mut Render3dState, n: int, w: int, h: int) -> void {
- gsl_jitter_frame(render3d_st)
- cam_update(render3d_st)
+function cam_begin_frame(n: int, w: int, h: int) -> void {
+ gsl_jitter_frame()
+ cam_update()
}
-function cam_set(render3d_st: mut Render3dState, x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
- v3_set(render3d_st.cam_pos, x, y, z)
- render3d_st.cam_yaw = Math.deg_to_rad(yaw_deg)
- render3d_st.cam_pitch = Math.deg_to_rad(pitch_deg)
- cam_update(render3d_st)
+function cam_set(x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
+ v3_set(cam_pos, x, y, z)
+ cam_yaw = Math.deg_to_rad(yaw_deg)
+ cam_pitch = Math.deg_to_rad(pitch_deg)
+ cam_update()
}
-function cam_update(render3d_st: mut Render3dState) -> void {
- let cy = Math.cos(render3d_st.cam_yaw); let sy = Math.sin(render3d_st.cam_yaw)
- let cp = Math.cos(render3d_st.cam_pitch); let sp = Math.sin(render3d_st.cam_pitch)
- v3_set(render3d_st.cam_fwd, -(sy * cp), sp, -(cy * cp))
- v3_set(render3d_st.cam_right, cy, 0.0, -sy)
+function cam_update() -> void {
+ let cy = Math.cos(cam_yaw); let sy = Math.sin(cam_yaw)
+ let cp = Math.cos(cam_pitch); let sp = Math.sin(cam_pitch)
+ v3_set(cam_fwd, -(sy * cp), sp, -(cy * cp))
+ v3_set(cam_right, cy, 0.0, -sy)
let at = floats(3)
- v3_add(at, render3d_st.cam_pos, render3d_st.cam_fwd)
+ v3_add(at, cam_pos, cam_fwd)
let up = v3_new(0.0, 1.0, 0.0)
- m4_look_at(render3d_st.cam_view, render3d_st.cam_pos, at, up)
- m4_perspective(render3d_st.cam_proj, render3d_st.cam_fov, render3d_st.cam_aspect, render3d_st.cam_near, render3d_st.cam_far)
- m4_mul(render3d_st.cam_vp_clean, render3d_st.cam_proj, render3d_st.cam_view)
- m4_inverse(render3d_st.cam_inv_vp_clean, render3d_st.cam_vp_clean)
+ m4_look_at(cam_view, cam_pos, at, up)
+ m4_perspective(cam_proj, cam_fov, cam_aspect, cam_near, cam_far)
+ m4_mul(cam_vp_clean, cam_proj, cam_view)
+ m4_inverse(cam_inv_vp_clean, cam_vp_clean)
# DLSS's sub-pixel jitter (streamline.ludic), in the projection the scene draws with only:
# culling, depth reconstruction and last frame's matrix keep the clean one
- if render3d_st.gsl_jitter_x != 0.0 or render3d_st.gsl_jitter_y != 0.0 {
- render3d_st.cam_proj[8] = render3d_st.cam_proj[8] - render3d_st.gsl_jitter_x
- render3d_st.cam_proj[9] = render3d_st.cam_proj[9] - render3d_st.gsl_jitter_y
+ if gsl_jitter_x != 0.0 or gsl_jitter_y != 0.0 {
+ cam_proj[8] = cam_proj[8] - gsl_jitter_x
+ cam_proj[9] = cam_proj[9] - gsl_jitter_y
}
- m4_mul(render3d_st.cam_vp, render3d_st.cam_proj, render3d_st.cam_view)
- m4_inverse(render3d_st.cam_inv_vp, render3d_st.cam_vp)
- m4_inverse(render3d_st.cam_inv_proj, render3d_st.cam_proj)
+ m4_mul(cam_vp, cam_proj, cam_view)
+ m4_inverse(cam_inv_vp, cam_vp)
+ m4_inverse(cam_inv_proj, cam_proj)
free(at); free(up)
- cam_planes_update(render3d_st)
+ cam_planes_update()
}
-function cam_planes_update(render3d_st: mut Render3dState) -> void {
- if render3d_st.cam_planes == null { render3d_st.cam_planes = floats(16) }
- let m = render3d_st.cam_vp_clean
+function cam_planes_update() -> void {
+ if cam_planes == null { cam_planes = floats(16) }
+ let m = cam_vp_clean
for p in 0 .. 4 {
var r = 0
if p >= 2 { r = 1 }
@@ -65,27 +82,27 @@ function cam_planes_update(render3d_st: mut Render3dState) -> void {
var c = m[11] + sg * m[8 + r]
var d = m[15] + sg * m[12 + r]
let inv = 1.0 / Math.sqrt(a * a + b * b + c * c)
- render3d_st.cam_planes[p * 4] = a * inv; render3d_st.cam_planes[p * 4 + 1] = b * inv
- render3d_st.cam_planes[p * 4 + 2] = c * inv; render3d_st.cam_planes[p * 4 + 3] = d * inv
+ cam_planes[p * 4] = a * inv; cam_planes[p * 4 + 1] = b * inv
+ cam_planes[p * 4 + 2] = c * inv; cam_planes[p * 4 + 3] = d * inv
}
}
# is a sphere (float bits) at least partly inside the side planes of the view?
-function cam_sphere_visible(render3d_st: Render3dState, x: float, y: float, z: float, r: float) -> bool {
- if render3d_st.cam_planes == null { return true }
+function cam_sphere_visible(x: float, y: float, z: float, r: float) -> bool {
+ if cam_planes == null { return true }
let nr = -r
for p in 0 .. 4 {
let o = p * 4
- let dist = render3d_st.cam_planes[o] * x + render3d_st.cam_planes[o + 1] * y + render3d_st.cam_planes[o + 2] * z + render3d_st.cam_planes[o + 3]
+ let dist = cam_planes[o] * x + cam_planes[o + 1] * y + cam_planes[o + 2] * z + cam_planes[o + 3]
if dist < nr { return false }
}
return true
}
# fly: forward/strafe in metres, turn in radians
-function cam_move(render3d_st: mut Render3dState, fwd: float, strafe: float, up: float, dyaw: float, dpitch: float) -> void {
- render3d_st.cam_yaw = render3d_st.cam_yaw + dyaw
- render3d_st.cam_pitch = Math.clamp(render3d_st.cam_pitch + dpitch, -1.5, 1.5)
- v3_madd(render3d_st.cam_pos, render3d_st.cam_pos, render3d_st.cam_fwd, fwd)
- v3_madd(render3d_st.cam_pos, render3d_st.cam_pos, render3d_st.cam_right, strafe)
- render3d_st.cam_pos[1] = render3d_st.cam_pos[1] + up
- cam_update(render3d_st)
+function cam_move(fwd: float, strafe: float, up: float, dyaw: float, dpitch: float) -> void {
+ cam_yaw = cam_yaw + dyaw
+ cam_pitch = Math.clamp(cam_pitch + dpitch, -1.5, 1.5)
+ v3_madd(cam_pos, cam_pos, cam_fwd, fwd)
+ v3_madd(cam_pos, cam_pos, cam_right, strafe)
+ cam_pos[1] = cam_pos[1] + up
+ cam_update()
}
diff --git a/packages/ludic.render3d/collide.ludic b/packages/ludic.render3d/collide.ludic
index da28144e..53cffd0b 100644
--- a/packages/ludic.render3d/collide.ludic
+++ b/packages/ludic.render3d/collide.ludic
@@ -9,78 +9,89 @@
const COL_CELL: int = 16
const COL_CAP: int = 120000
+var col_x: floats = null
+var col_z: floats = null
+var col_r: floats = null
# What a collider occupies VERTICALLY: y0 its base, y1 its top, metres, float bits. A circle used
# to be an infinite pillar - you could not climb a boulder, and a knee-high rock stopped you dead,
# because there was no height to compare against. col_add keeps that shape (a span from far below
# to far above) so every existing caller behaves exactly as it did; col_add_h gives a real one.
+var col_y0: floats = null
+var col_y1: floats = null
+var col_n: int = 0
+var col_side: int = 0 # cells per side
+var col_start: words = null # per cell: first index into col_sorted (side*side + 1)
+var col_sorted: words = null
+var col_built: bool = false
+var col_out: floats = null # the resolved position (x, z)
const COL_LOW: float = -16777216.0 # -16777216.0: below any ground
const COL_HIGH: float = 16777216.0 # 16777216.0: above any sky
-function col_add(render3d_st: mut Render3dState, x: float, z: float, r: float) -> void { col_add_h(render3d_st, x, z, r, COL_LOW, COL_HIGH) }
+function col_add(x: float, z: float, r: float) -> void { col_add_h(x, z, r, COL_LOW, COL_HIGH) }
# a collider that occupies only y0 .. y1: a body above its top walks over it, a body below its base
# passes under, and col_top_at reports it as something to stand on
-function col_add_h(render3d_st: mut Render3dState, x: float, z: float, r: float, y0: float, y1: float) -> void {
- if render3d_st.col_x == null {
- render3d_st.col_x = floats(COL_CAP); render3d_st.col_z = floats(COL_CAP); render3d_st.col_r = floats(COL_CAP)
- render3d_st.col_y0 = floats(COL_CAP); render3d_st.col_y1 = floats(COL_CAP); render3d_st.col_out = floats(2)
+function col_add_h(x: float, z: float, r: float, y0: float, y1: float) -> void {
+ if col_x == null {
+ col_x = floats(COL_CAP); col_z = floats(COL_CAP); col_r = floats(COL_CAP)
+ col_y0 = floats(COL_CAP); col_y1 = floats(COL_CAP); col_out = floats(2)
}
- if render3d_st.col_n >= COL_CAP { return }
- render3d_st.col_x[render3d_st.col_n] = x; render3d_st.col_z[render3d_st.col_n] = z; render3d_st.col_r[render3d_st.col_n] = r
- render3d_st.col_y0[render3d_st.col_n] = y0; render3d_st.col_y1[render3d_st.col_n] = y1
- render3d_st.col_n += 1
- render3d_st.col_built = false
+ if col_n >= COL_CAP { return }
+ col_x[col_n] = x; col_z[col_n] = z; col_r[col_n] = r
+ col_y0[col_n] = y0; col_y1[col_n] = y1
+ col_n += 1
+ col_built = false
}
-function col_cell_of(render3d_st: Render3dState, v: float, origin: float) -> int {
- var c = int(Math.floor((v - origin + float(render3d_st.TERRAIN_HALF)) / float(COL_CELL)))
+function col_cell_of(v: float, origin: float) -> int {
+ var c = int(Math.floor((v - origin + float(TERRAIN_HALF)) / float(COL_CELL)))
if c < 0 { c = 0 }
- if c > render3d_st.col_side - 1 { c = render3d_st.col_side - 1 }
+ if c > col_side - 1 { c = col_side - 1 }
return c
}
-function col_build(render3d_st: mut Render3dState) -> void {
- render3d_st.col_side = (render3d_st.TERRAIN_HALF * 2) / COL_CELL
- let ncell = render3d_st.col_side * render3d_st.col_side
- if render3d_st.col_start == null { render3d_st.col_start = words(ncell + 1); render3d_st.col_sorted = words(COL_CAP) }
- for i in 0 .. ncell + 1 { render3d_st.col_start[i] = 0 }
- for i in 0 .. render3d_st.col_n { render3d_st.col_start[col_cell_of(render3d_st, render3d_st.col_z[i], render3d_st.ter_oz) * render3d_st.col_side + col_cell_of(render3d_st, render3d_st.col_x[i], render3d_st.ter_ox) + 1] += 1 }
- for c in 0 .. ncell { render3d_st.col_start[c + 1] += render3d_st.col_start[c] }
+function col_build() -> void {
+ col_side = (TERRAIN_HALF * 2) / COL_CELL
+ let ncell = col_side * col_side
+ if col_start == null { col_start = words(ncell + 1); col_sorted = words(COL_CAP) }
+ for i in 0 .. ncell + 1 { col_start[i] = 0 }
+ for i in 0 .. col_n { col_start[col_cell_of(col_z[i], ter_oz) * col_side + col_cell_of(col_x[i], ter_ox) + 1] += 1 }
+ for c in 0 .. ncell { col_start[c + 1] += col_start[c] }
let fill = words(ncell)
- for c in 0 .. ncell { fill[c] = render3d_st.col_start[c] }
- for i in 0 .. render3d_st.col_n {
- let c = col_cell_of(render3d_st, render3d_st.col_z[i], render3d_st.ter_oz) * render3d_st.col_side + col_cell_of(render3d_st, render3d_st.col_x[i], render3d_st.ter_ox)
- render3d_st.col_sorted[fill[c]] = i
+ for c in 0 .. ncell { fill[c] = col_start[c] }
+ for i in 0 .. col_n {
+ let c = col_cell_of(col_z[i], ter_oz) * col_side + col_cell_of(col_x[i], ter_ox)
+ col_sorted[fill[c]] = i
fill[c] += 1
}
free(fill)
- render3d_st.col_built = true
- print(`colliders: {render3d_st.col_n}`)
+ col_built = true
+ print(`colliders: {col_n}`)
}
# push (px, pz) with radius pr out of every circle it overlaps; the result is in col_out
-function col_resolve(render3d_st: mut Render3dState, px: float, pz: float, pr: float) -> bool { return col_resolve_at(render3d_st, px, pz, pr, COL_LOW, COL_HIGH) }
+function col_resolve(px: float, pz: float, pr: float) -> bool { return col_resolve_at(px, pz, pr, COL_LOW, COL_HIGH) }
# the same, for a body that occupies feet .. head: a collider whose span misses that is not in the
# way at all. This is what lets a hiker stand on top of a boulder rather than inside it.
-function col_resolve_at(render3d_st: mut Render3dState, px: float, pz: float, pr: float, feet: float, head: float) -> bool {
- render3d_st.col_out[0] = px; render3d_st.col_out[1] = pz
- if not render3d_st.col_built or render3d_st.col_n == 0 { return false }
+function col_resolve_at(px: float, pz: float, pr: float, feet: float, head: float) -> bool {
+ col_out[0] = px; col_out[1] = pz
+ if not col_built or col_n == 0 { return false }
var x = px; var z = pz
var moved = false
- let cx = col_cell_of(render3d_st, px, render3d_st.ter_ox); let cz = col_cell_of(render3d_st, pz, render3d_st.ter_oz)
+ let cx = col_cell_of(px, ter_ox); let cz = col_cell_of(pz, ter_oz)
for pass in 0 .. 2 {
for dz in 0 .. 3 {
let zc = cz + dz - 1
- if zc < 0 or zc >= render3d_st.col_side { continue }
+ if zc < 0 or zc >= col_side { continue }
for dx in 0 .. 3 {
let xc = cx + dx - 1
- if xc < 0 or xc >= render3d_st.col_side { continue }
- let c = zc * render3d_st.col_side + xc
- for k in render3d_st.col_start[c] .. render3d_st.col_start[c + 1] {
- let i = render3d_st.col_sorted[k]
- let ex = x - render3d_st.col_x[i]; let ez = z - render3d_st.col_z[i]
+ if xc < 0 or xc >= col_side { continue }
+ let c = zc * col_side + xc
+ for k in col_start[c] .. col_start[c + 1] {
+ let i = col_sorted[k]
+ let ex = x - col_x[i]; let ez = z - col_z[i]
let d2 = ex * ex + ez * ez
- let rr = render3d_st.col_r[i] + pr
+ let rr = col_r[i] + pr
# nothing to push out of if the body is wholly above its top or below its base
- if not (feet < render3d_st.col_y1[i]) { continue }
- if not (head > render3d_st.col_y0[i]) { continue }
+ if not (feet < col_y1[i]) { continue }
+ if not (head > col_y0[i]) { continue }
if d2 < rr * rr {
let d = Math.sqrt(d2)
# The UNIT normal out of this circle. (ex, ez) / d is always unit for d > 0,
@@ -105,31 +116,31 @@ function col_resolve_at(render3d_st: mut Render3dState, px: float, pz: float, pr
}
}
}
- render3d_st.col_out[0] = x; render3d_st.col_out[1] = z
+ col_out[0] = x; col_out[1] = z
return moved
}
# The highest collider top under (px, pz) that a body at `feet` could be standing on or step up to:
# tops above `reach` are a wall, not a step. F_ZERO-safe: returns `floor` when there is nothing, so
# a caller can pass the terrain height and use the answer directly as the ground.
-function col_top_at(render3d_st: Render3dState, px: float, pz: float, pr: float, feet: float, reach: float, floor: float) -> float {
+function col_top_at(px: float, pz: float, pr: float, feet: float, reach: float, floor: float) -> float {
var top = floor
- if not render3d_st.col_built or render3d_st.col_n == 0 { return top }
- let cx = col_cell_of(render3d_st, px, render3d_st.ter_ox); let cz = col_cell_of(render3d_st, pz, render3d_st.ter_oz)
+ if not col_built or col_n == 0 { return top }
+ let cx = col_cell_of(px, ter_ox); let cz = col_cell_of(pz, ter_oz)
let limit = feet + reach
for dz in 0 .. 3 {
let zc = cz + dz - 1
- if zc < 0 or zc >= render3d_st.col_side { continue }
+ if zc < 0 or zc >= col_side { continue }
for dx in 0 .. 3 {
let xc = cx + dx - 1
- if xc < 0 or xc >= render3d_st.col_side { continue }
- let c = zc * render3d_st.col_side + xc
- for k in render3d_st.col_start[c] .. render3d_st.col_start[c + 1] {
- let i = render3d_st.col_sorted[k]
- let ex = px - render3d_st.col_x[i]; let ez = pz - render3d_st.col_z[i]
+ if xc < 0 or xc >= col_side { continue }
+ let c = zc * col_side + xc
+ for k in col_start[c] .. col_start[c + 1] {
+ let i = col_sorted[k]
+ let ex = px - col_x[i]; let ez = pz - col_z[i]
let d2 = ex * ex + ez * ez
- let rr = render3d_st.col_r[i] + pr
+ let rr = col_r[i] + pr
if not (d2 < rr * rr) { continue }
- let t = render3d_st.col_y1[i]
+ let t = col_y1[i]
if t > limit { continue } # too tall to step onto: it is a wall
if t > top { top = t }
}
@@ -138,21 +149,21 @@ function col_top_at(render3d_st: Render3dState, px: float, pz: float, pr: float,
return top
}
# is the segment from (x0,z0) to (x1,z1) clear of every circle (a camera line of sight)?
-function col_clear(render3d_st: mut Render3dState, x0: float, z0: float, x1: float, z1: float, r: float) -> bool {
+function col_clear(x0: float, z0: float, x1: float, z1: float, r: float) -> bool {
let steps = 6
for s in 0 .. steps + 1 {
let t = float(s) / float(steps)
let x = Math.lerp(x0, x1, t); let z = Math.lerp(z0, z1, t)
- if col_resolve(render3d_st, x, z, r) { return false }
+ if col_resolve(x, z, r) { return false }
}
return true
}
# No colliders, ready for another map's. The cell index is sized from TERRAIN_HALF on its
# first build and kept, so it is released too: a larger map would overrun the old one.
-function col_reset(render3d_st: mut Render3dState) -> void {
- render3d_st.col_n = 0
- render3d_st.col_built = false
- if render3d_st.col_start != null { free(render3d_st.col_start); render3d_st.col_start = null }
- if render3d_st.col_sorted != null { free(render3d_st.col_sorted); render3d_st.col_sorted = null }
+function col_reset() -> void {
+ col_n = 0
+ col_built = false
+ if col_start != null { free(col_start); col_start = null }
+ if col_sorted != null { free(col_sorted); col_sorted = null }
}
diff --git a/packages/ludic.render3d/daylight.ludic b/packages/ludic.render3d/daylight.ludic
index 206bba2d..59056842 100644
--- a/packages/ludic.render3d/daylight.ludic
+++ b/packages/ludic.render3d/daylight.ludic
@@ -9,35 +9,65 @@
# angle they were baked at; the height-field shadow rebakes as the sun moves.
# ============================================================================
+var day_on: bool = false
+var day_hours: float = 0.0 # float bits, 0 .. 24
+var day_light: float = 0.0 # 0 night .. 1 full day (float bits)
+var day_ibl: floats = null # rgb scale on the sky's light
+var day_sun_base: floats = null # the HDRI's sun radiance, kept
+var day_az0: float = 0.0 # the sun's azimuth at the reference hour (radians, yaw convention)
+var day_yaw0: float = 0.0 # the sky yaw the scene was tuned at
+var day_hour0: float = 0.0 # the hour the photograph was taken (10.5)
+var day_dir: float = 0.0 # +1 / -1: which way the sun travels in yaw
+var day_az: float = 0.0
+var day_el: float = 0.0
+var day_gen: int = 0 # bumps when the light moved enough to rebake the terrain shadow
+var day_baked_az: float = 0.0
+var day_baked_el: float = 0.0
+var day_sky_baked: float = 0.0 # the sky yaw the convolutions were baked at
+var fire_pos: floats = null
+var fire_color: floats = null
+var day_moon: bool = false
# The moon's place in its month, 0 new .. 0.5 full .. 1 new again. It decides the disc's
# terminator, how much light reaches the ground, and where in the sky it rides: a full
# moon is opposite the sun and rises at sunset, a new one travels with it.
+var day_moon_phase: float = 0.5 # 0.5: full, which is where the game used to be
+var day_moon_illum: float = 1.0 # the lit fraction, derived from the phase
+var day_moon_dir: floats = null # toward the moon, whether or not it is up
+var hand_pos: floats = null
+var hand_color: floats = null
+var hand_dir: floats = null
+var hand_cone: float = 0.0
+var hand_reach: float = 0.0 # metres the hand light reaches (float bits)
+var day_overcast: float = 0.0 # 0 clear .. 1 a low grey sky (float bits): dims the sun and the sky's light
+var day_flash: float = 0.0 # a lightning flash this frame (0..1): the sun brightens for it
+var day_fog_mul: float = 1.0 # multiplies the base fog density (rain and snow thicken the air)
+var day_fog_base: float = 0.0
-function daylight_init(render3d_st: mut Render3dState) -> void {
- render3d_st.day_ibl = v3_new(1.0, 1.0, 1.0)
- render3d_st.day_sun_base = v3_new(render3d_st.sun_color[0], render3d_st.sun_color[1], render3d_st.sun_color[2])
- render3d_st.fire_pos = v3_new(0.0, -1000.0, 0.0)
- render3d_st.fire_color = v3_new(0.0, 0.0, 0.0)
- render3d_st.day_moon_dir = v3_new(0.0, 1.0, 0.0)
- render3d_st.hand_pos = v3_new(0.0, -1000.0, 0.0); render3d_st.hand_color = v3_new(0.0, 0.0, 0.0); render3d_st.hand_dir = v3_new(0.0, 0.0, -1.0); render3d_st.hand_cone = -2.0; render3d_st.hand_reach = 12.0
- render3d_st.day_light = 1.0
+function daylight_init() -> void {
+ day_ibl = v3_new(1.0, 1.0, 1.0)
+ day_sun_base = v3_new(sun_color[0], sun_color[1], sun_color[2])
+ fire_pos = v3_new(0.0, -1000.0, 0.0)
+ fire_color = v3_new(0.0, 0.0, 0.0)
+ day_moon_dir = v3_new(0.0, 1.0, 0.0)
+ hand_pos = v3_new(0.0, -1000.0, 0.0); hand_color = v3_new(0.0, 0.0, 0.0); hand_dir = v3_new(0.0, 0.0, -1.0); hand_cone = -2.0; hand_reach = 12.0
+ day_light = 1.0
}
# Start the clock: the scene as tuned (sky yaw `yaw0`) is the photograph's hour `hour0`;
# the sun rises and sets toward `set_yaw` (the direction it should be in at 19:00).
-function daylight_start(render3d_st: mut Render3dState, yaw0: float, hour0: float, set_yaw: float) -> void {
- render3d_st.day_yaw0 = yaw0; render3d_st.day_hour0 = hour0
- render3d_st.day_az0 = Math.atan2(-render3d_st.sun_dir[0], -render3d_st.sun_dir[2])
- render3d_st.day_sky_baked = yaw0
+function daylight_start(yaw0: float, hour0: float, set_yaw: float) -> void {
+ day_yaw0 = yaw0; day_hour0 = hour0
+ day_az0 = Math.atan2(-sun_dir[0], -sun_dir[2])
+ day_sky_baked = yaw0
# which way does the sun travel? the way that puts it nearest `set_yaw` at 19:00
let step = (19.0 - hour0) * Math.deg_to_rad(15.0)
- let da = Math.abs(day_wrap(render3d_st.day_az0 + step - set_yaw))
- let db = Math.abs(day_wrap(render3d_st.day_az0 - step - set_yaw))
- render3d_st.day_dir = 1.0
- if db < da { render3d_st.day_dir = -1.0 }
- render3d_st.day_on = true
- render3d_st.day_baked_az = 1000.0
- daylight_set(render3d_st, hour0)
+ let da = Math.abs(day_wrap(day_az0 + step - set_yaw))
+ let db = Math.abs(day_wrap(day_az0 - step - set_yaw))
+ day_dir = 1.0
+ if db < da { day_dir = -1.0 }
+ day_on = true
+ day_baked_az = 1000.0
+ daylight_set(hour0)
}
function day_wrap(a: float) -> float {
let two_pi = 2.0 * PI
@@ -51,68 +81,68 @@ function smoothf(a: float, b: float, x: float) -> float {
return t * t * (3.0 - 2.0 * t)
}
-function daylight_set(render3d_st: mut Render3dState, hours: float) -> void {
+function daylight_set(hours: float) -> void {
var h = hours % 24.0
if h < 0.0 { h = h + 24.0 }
- render3d_st.day_hours = h
+ day_hours = h
# the arc: up at 5:30, highest (about 57 degrees) at 12:45, down at 20:00
let t = (h - 5.5) / 14.5 * PI
let el = Math.deg_to_rad(-6.0 + 63.0 * Math.sin(t))
- let az = render3d_st.day_az0 + (h - render3d_st.day_hour0) * Math.deg_to_rad(15.0) * render3d_st.day_dir
- render3d_st.day_az = az; render3d_st.day_el = el
+ let az = day_az0 + (h - day_hour0) * Math.deg_to_rad(15.0) * day_dir
+ day_az = az; day_el = el
let d = smoothf(Math.deg_to_rad(-8.0), Math.deg_to_rad(12.0), el)
- render3d_st.day_light = d
+ day_light = d
# the sun, warm and dim near the horizon; past dusk, the moon from across the sky
var laz = az; var lel = Math.max(el, Math.deg_to_rad(3.0))
var warm_r = 1.0; var warm_g = 1.0; var warm_b = 1.0
let low = smoothf(0.0, Math.deg_to_rad(24.0), el)
warm_g = Math.lerp(0.55, 1.0, low); warm_b = Math.lerp(0.28, 1.0, low)
# an overcast sky: the sun goes diffuse and grey, a lightning flash brings it back white
- let oc = Math.clamp(render3d_st.day_overcast, 0.0, 1.0)
- let sunk = 1.0 - 0.92 * oc + 2.5 * render3d_st.day_flash
- var sr = render3d_st.day_sun_base[0] * (d * warm_r * sunk)
- var sg = render3d_st.day_sun_base[1] * (d * warm_g * sunk)
- var sb = render3d_st.day_sun_base[2] * (d * warm_b * sunk)
+ let oc = Math.clamp(day_overcast, 0.0, 1.0)
+ let sunk = 1.0 - 0.92 * oc + 2.5 * day_flash
+ var sr = day_sun_base[0] * (d * warm_r * sunk)
+ var sg = day_sun_base[1] * (d * warm_g * sunk)
+ var sb = day_sun_base[2] * (d * warm_b * sunk)
# The moon rides a lag behind the sun that is its phase: full is opposite (half a turn),
# new is alongside. Its elevation follows the same arc, offset by the same amount, so a
# full moon rises as the sun sets and a new moon is up all day and invisible.
# The moon is where the sun was `lag` of a day ago: at full that is half a day, so it
# rises as the sun sets; at new it is alongside the sun and up all day, invisible. The
# sign matters — a waxing crescent has to set AFTER the sun, not before it.
- let moon_lag = 2.0 * PI * render3d_st.day_moon_phase
- let maz = az - moon_lag * render3d_st.day_dir
+ let moon_lag = 2.0 * PI * day_moon_phase
+ let maz = az - moon_lag * day_dir
let mel = Math.deg_to_rad(-6.0 + 63.0 * Math.sin(t - moon_lag))
let mce = Math.cos(mel)
- v3_set(render3d_st.day_moon_dir, -(Math.sin(maz) * mce), Math.sin(mel), -(Math.cos(maz) * mce))
- render3d_st.day_moon = false
+ v3_set(day_moon_dir, -(Math.sin(maz) * mce), Math.sin(mel), -(Math.cos(maz) * mce))
+ day_moon = false
if el < Math.deg_to_rad(-7.0) {
- render3d_st.day_moon = true
+ day_moon = true
laz = maz
lel = Math.clamp(mel, Math.deg_to_rad(6.0), Math.deg_to_rad(70.0))
let m = smoothf(Math.deg_to_rad(-7.0), Math.deg_to_rad(-16.0), el)
# what the moon is worth on the ground, by how much of it is lit. A new moon is a
# properly dark night, which is what makes a torch and a lantern matter.
let up = smoothf(Math.deg_to_rad(-4.0), Math.deg_to_rad(8.0), mel)
- let lit = m * up * (0.06 + 0.94 * render3d_st.day_moon_illum)
- sr = render3d_st.day_sun_base[0] * (0.0130 * lit)
- sg = render3d_st.day_sun_base[1] * (0.0165 * lit)
- sb = render3d_st.day_sun_base[2] * (0.0250 * lit)
+ let lit = m * up * (0.06 + 0.94 * day_moon_illum)
+ sr = day_sun_base[0] * (0.0130 * lit)
+ sg = day_sun_base[1] * (0.0165 * lit)
+ sb = day_sun_base[2] * (0.0250 * lit)
}
let ce = Math.cos(lel)
- v3_set(render3d_st.sun_dir, -(Math.sin(laz) * ce), Math.sin(lel), -(Math.cos(laz) * ce))
- v3_set(render3d_st.sun_color, sr, sg, sb)
+ v3_set(sun_dir, -(Math.sin(laz) * ce), Math.sin(lel), -(Math.cos(laz) * ce))
+ v3_set(sun_color, sr, sg, sb)
# the sky's light: full by day, a deep blue by night, amber through the dusk
let dusk = smoothf(Math.deg_to_rad(-10.0), Math.deg_to_rad(2.0), el) * (1.0 - smoothf(Math.deg_to_rad(2.0), Math.deg_to_rad(18.0), el))
# a full moon lifts the night's own ambient nearly threefold; a new moon leaves it alone
var moonlit = 1.0
- if render3d_st.day_moon { moonlit = 1.0 + 1.8 * render3d_st.day_moon_illum }
- v3_set(render3d_st.day_ibl, Math.lerp(0.020 * moonlit, 1.0, d), Math.lerp(0.026 * moonlit, 1.0, d), Math.lerp(0.045 * moonlit, 1.0, d))
- render3d_st.day_ibl[0] = render3d_st.day_ibl[0] * (1.0 + 0.35 * dusk)
- render3d_st.day_ibl[2] = render3d_st.day_ibl[2] * (1.0 - 0.25 * dusk)
+ if day_moon { moonlit = 1.0 + 1.8 * day_moon_illum }
+ v3_set(day_ibl, Math.lerp(0.020 * moonlit, 1.0, d), Math.lerp(0.026 * moonlit, 1.0, d), Math.lerp(0.045 * moonlit, 1.0, d))
+ day_ibl[0] = day_ibl[0] * (1.0 + 0.35 * dusk)
+ day_ibl[2] = day_ibl[2] * (1.0 - 0.25 * dusk)
# clouds: less light, and greyer (the blue and the warmth both fade)
- let grey = (render3d_st.day_ibl[0] + (render3d_st.day_ibl[1] + render3d_st.day_ibl[2])) * 0.3333 + 0.6 * render3d_st.day_flash
+ let grey = (day_ibl[0] + (day_ibl[1] + day_ibl[2])) * 0.3333 + 0.6 * day_flash
let dim = 1.0 - 0.55 * oc
- for i in 0 .. 3 { render3d_st.day_ibl[i] = Math.lerp(render3d_st.day_ibl[i], grey, 0.7 * oc) * dim }
+ for i in 0 .. 3 { day_ibl[i] = Math.lerp(day_ibl[i], grey, 0.7 * oc) * dim }
# ---- the air ---------------------------------------------------------------------------
# Aerial perspective is a CURVE, not the one constant it was. A low sun is shining through
# far more air than a high one, and it is shining ALONG the ground rather than down onto it,
@@ -122,25 +152,25 @@ function daylight_set(render3d_st: mut Render3dState, hours: float) -> void {
# gets a dawn's haze with no dawn to justify it. Overcast thickens the air and flattens the
# scatter, because a grey sky has no disc to scatter from. The player's slider still
# multiplies the result, so nobody loses the setting they chose.
- if render3d_st.day_fog_base == 0.0 { render3d_st.day_fog_base = render3d_st.r3d_fog_density }
+ if day_fog_base == 0.0 { day_fog_base = r3d_fog_density }
let fog_rise = smoothf(Math.deg_to_rad(-12.0), Math.deg_to_rad(1.0), el)
let fog_high = smoothf(Math.deg_to_rad(2.0), Math.deg_to_rad(26.0), el)
let lowsun = fog_rise * (1.0 - fog_high)
- var dens = render3d_st.day_fog_base * (1.0 + 1.5 * lowsun)
+ var dens = day_fog_base * (1.0 + 1.5 * lowsun)
dens = dens * (1.0 + 1.2 * oc)
- render3d_st.r3d_fog_density = dens * render3d_st.day_fog_mul * render3d_st.r3d_fog_scale
+ r3d_fog_density = dens * day_fog_mul * r3d_fog_scale
# how fast the haze thins with height: a settled morning's lies IN the valley, a noon sky is
# thin all the way up, so the falloff rises as the sun drops
- render3d_st.r3d_fog_falloff = 0.002 * (1.0 + 1.6 * lowsun)
+ r3d_fog_falloff = 0.002 * (1.0 + 1.6 * lowsun)
# the glow a ridge is silhouetted against when you look into a low sun
- render3d_st.r3d_fog_inscatter = (0.012 + 0.16 * lowsun) * (1.0 - 0.7 * oc)
+ r3d_fog_inscatter = (0.012 + 0.16 * lowsun) * (1.0 - 0.7 * oc)
# and distance takes colour away at every hour, harder under cloud
# 1.9 was tuned on a valley whose rock was grey anyway. Maroon's air is thin, dry and at
# 2900 m, and the Bells are only a couple of kilometres from the lake - in the photograph
# everybody knows, they are still plainly RED at that distance. Desaturating them to a pale
# grey-pink is physically defensible and loses the one thing the range is named for, so the
# basin's own air gets a gentler figure and overcast still takes colour away faster.
- render3d_st.r3d_fog_desat = 1.15 + 0.7 * oc
+ r3d_fog_desat = 1.15 + 0.7 * oc
# ---- the grade -------------------------------------------------------------------------
# The look of an hour is not only how much air is in front of the mountain; it is what colour
# the light is and what the shadows are filled with. Noon is the case worth naming: direct
@@ -153,29 +183,29 @@ function daylight_set(render3d_st: mut Render3dState, hours: float) -> void {
# half-cooled. It only begins under the horizon.
let night = 1.0 - smoothf(Math.deg_to_rad(-14.0), Math.deg_to_rad(-4.0), el)
let hi = fog_high
- render3d_st.post_wb_r = 1.02 + (0.10 * lowsun - (0.03 * hi + 0.06 * night))
- render3d_st.post_wb_g = 1.0 + 0.012 * lowsun
- render3d_st.post_wb_b = 0.97 + (0.055 * hi + 0.13 * night - 0.09 * lowsun)
+ post_wb_r = 1.02 + (0.10 * lowsun - (0.03 * hi + 0.06 * night))
+ post_wb_g = 1.0 + 0.012 * lowsun
+ post_wb_b = 0.97 + (0.055 * hi + 0.13 * night - 0.09 * lowsun)
# the shadows' floor: warm and open into a low sun, blue at noon, cold and crushed at night
- render3d_st.post_lift_r = 0.004 + 0.013 * lowsun - 0.002 * (hi + night)
- render3d_st.post_lift_g = 0.004 + 0.008 * lowsun - 0.001 * night
- render3d_st.post_lift_b = 0.012 + (0.004 * lowsun + 0.013 * hi)
+ post_lift_r = 0.004 + 0.013 * lowsun - 0.002 * (hi + night)
+ post_lift_g = 0.004 + 0.008 * lowsun - 0.001 * night
+ post_lift_b = 0.012 + (0.004 * lowsun + 0.013 * hi)
# Gain is left alone on purpose. It looks like a highlight control and is not: the grade is
# `c * gain + lift * (1 - c)`, so warming the gain warms the WHOLE frame, and warming it at
# noon undid the cool white balance above and turned one o'clock yellower than seven in the
# morning - the opposite of the thing this grade exists to do. Midday's punch comes from
# contrast, and midday's colour from a cool balance over a blue shadow.
- render3d_st.post_gain_r = 0.99
- render3d_st.post_gain_g = 0.995
- render3d_st.post_gain_b = 1.0
- render3d_st.post_contrast = 1.12 + 0.11 * hi - (0.17 * oc + 0.06 * night)
- render3d_st.post_saturation = 1.04 + 0.12 * lowsun - (0.20 * oc + 0.12 * night)
+ post_gain_r = 0.99
+ post_gain_g = 0.995
+ post_gain_b = 1.0
+ post_contrast = 1.12 + 0.11 * hi - (0.17 * oc + 0.06 * night)
+ post_saturation = 1.04 + 0.12 * lowsun - (0.20 * oc + 0.12 * night)
# The visible sky is relit by the hour (sky.frag). It fades out under the horizon, where the
# night's own tint, stars and moon take over, and it eases off under heavy cloud - a relit
# overcast is still overcast, and driving a clear-sky model hard through one paints a blue
# zenith onto a grey day.
- render3d_st.r3d_sky_relight = smoothf(Math.deg_to_rad(-10.0), Math.deg_to_rad(1.0), el) * (1.0 - 0.75 * oc)
+ r3d_sky_relight = smoothf(Math.deg_to_rad(-10.0), Math.deg_to_rad(1.0), el) * (1.0 - 0.75 * oc)
# ---- what is IN the air -------------------------------------------------------------
# The volumetric march (post.ludic) needs the same story the analytic fog tells, or the
@@ -184,64 +214,64 @@ function daylight_set(render3d_st: mut Render3dState, hours: float) -> void {
# the cold at either end of the day, lies ON the ground rather than filling the basin, and
# burns off by mid-morning. Cloud thickens the air and kills the shafts, because a shaft
# needs a disc to come from.
- render3d_st.post_vol_density = (0.00030 + 0.00070 * lowsun) * (1.0 - 0.55 * oc)
- render3d_st.post_vol_mist = 0.40 * lowsun * (1.0 - 0.4 * oc)
- render3d_st.post_vol_falloff = 0.006 + 0.004 * lowsun
+ post_vol_density = (0.00030 + 0.00070 * lowsun) * (1.0 - 0.55 * oc)
+ post_vol_mist = 0.40 * lowsun * (1.0 - 0.4 * oc)
+ post_vol_falloff = 0.006 + 0.004 * lowsun
# R3D_NOAIR=1: the air as it was before any of the above - one density, one falloff, the
# inscatter the shader used to hard-code, and no distance desaturation at all. It is here
# so a before-and-after can be shot from ONE binary at one hour, which is the only kind of
# comparison worth looking at, and it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI.
- if r3d_env_has(render3d_st, "R3D_NOAIR") {
- render3d_st.r3d_fog_density = render3d_st.day_fog_base * render3d_st.day_fog_mul * render3d_st.r3d_fog_scale
- render3d_st.r3d_fog_falloff = 0.002
- render3d_st.r3d_fog_inscatter = 0.02
- render3d_st.r3d_fog_desat = 0.0
- render3d_st.post_wb_r = 1.02; render3d_st.post_wb_g = 1.0; render3d_st.post_wb_b = 0.97
- render3d_st.post_lift_r = 0.004; render3d_st.post_lift_g = 0.004; render3d_st.post_lift_b = 0.012
- render3d_st.post_gain_r = 0.99; render3d_st.post_gain_g = 0.995; render3d_st.post_gain_b = 1.0
- render3d_st.post_contrast = 1.12; render3d_st.post_saturation = 1.04
- render3d_st.r3d_sky_relight = 0.0
- render3d_st.post_vol_density = 0.0; render3d_st.post_vol_mist = 0.0
+ if r3d_env_has("R3D_NOAIR") {
+ r3d_fog_density = day_fog_base * day_fog_mul * r3d_fog_scale
+ r3d_fog_falloff = 0.002
+ r3d_fog_inscatter = 0.02
+ r3d_fog_desat = 0.0
+ post_wb_r = 1.02; post_wb_g = 1.0; post_wb_b = 0.97
+ post_lift_r = 0.004; post_lift_g = 0.004; post_lift_b = 0.012
+ post_gain_r = 0.99; post_gain_g = 0.995; post_gain_b = 1.0
+ post_contrast = 1.12; post_saturation = 1.04
+ r3d_sky_relight = 0.0
+ post_vol_density = 0.0; post_vol_mist = 0.0
}
# exposure: auto-exposure must not turn the night into day
# the ceiling has to move with the moon or auto-exposure eats the difference between a
# full-moon night and a new-moon one
var night_max = 4.5
- if render3d_st.day_moon { night_max = 4.5 + 3.5 * render3d_st.day_moon_illum }
- render3d_st.post_exposure_max = Math.lerp(night_max, 20.0, d)
+ if day_moon { night_max = 4.5 + 3.5 * day_moon_illum }
+ post_exposure_max = Math.lerp(night_max, 20.0, d)
# the visible sky turns with the sun (cheap); its convolutions rebake when far off
- let sky_yaw_now = render3d_st.day_yaw0 + (h - render3d_st.day_hour0) * Math.deg_to_rad(15.0) * render3d_st.day_dir
- sky_set_rot(render3d_st, sky_yaw_now)
- if Math.abs(day_wrap(sky_yaw_now - render3d_st.day_sky_baked)) > Math.deg_to_rad(35.0) and d > 0.05 {
- render3d_st.day_sky_baked = sky_yaw_now
- sky_precompute(render3d_st)
+ let sky_yaw_now = day_yaw0 + (h - day_hour0) * Math.deg_to_rad(15.0) * day_dir
+ sky_set_rot(sky_yaw_now)
+ if Math.abs(day_wrap(sky_yaw_now - day_sky_baked)) > Math.deg_to_rad(35.0) and d > 0.05 {
+ day_sky_baked = sky_yaw_now
+ sky_precompute()
}
# the terrain's baked shadow follows the light in steps
- if Math.abs(day_wrap(laz - render3d_st.day_baked_az)) > Math.deg_to_rad(4.0) or Math.abs(lel - render3d_st.day_baked_el) > Math.deg_to_rad(3.0) {
- render3d_st.day_baked_az = laz; render3d_st.day_baked_el = lel
- render3d_st.day_gen += 1
+ if Math.abs(day_wrap(laz - day_baked_az)) > Math.deg_to_rad(4.0) or Math.abs(lel - day_baked_el) > Math.deg_to_rad(3.0) {
+ day_baked_az = laz; day_baked_el = lel
+ day_gen += 1
}
}
# where the moon is in its month; the game advances this each morning
-function daylight_moon(render3d_st: mut Render3dState, phase: float) -> void {
+function daylight_moon(phase: float) -> void {
var p = phase % 1.0
if p < 0.0 { p = p + 1.0 }
- render3d_st.day_moon_phase = p
+ day_moon_phase = p
# illuminated fraction: (1 - cos(2 pi p)) / 2, which is 0 at new and 1 at full
- render3d_st.day_moon_illum = (1.0 - Math.cos(2.0 * PI * p)) * 0.5
- daylight_set(render3d_st, render3d_st.day_hours)
+ day_moon_illum = (1.0 - Math.cos(2.0 * PI * p)) * 0.5
+ daylight_set(day_hours)
}
# the weather over the valley: overcast 0..1, a fog multiplier, a lightning flash 0..1
-function daylight_weather(render3d_st: mut Render3dState, overcast: float, fog_mul: float, flash: float) -> void {
- render3d_st.day_overcast = overcast; render3d_st.day_fog_mul = fog_mul; render3d_st.day_flash = flash
+function daylight_weather(overcast: float, fog_mul: float, flash: float) -> void {
+ day_overcast = overcast; day_fog_mul = fog_mul; day_flash = flash
}
# the campfire: a point light at (x, y, z) of `strength` (0 = out)
-function daylight_fire(render3d_st: Render3dState, x: float, y: float, z: float, strength: float) -> void {
- v3_set(render3d_st.fire_pos, x, y, z)
- v3_set(render3d_st.fire_color, 9.0 * strength, 4.6 * strength, 1.4 * strength)
+function daylight_fire(x: float, y: float, z: float, strength: float) -> void {
+ v3_set(fire_pos, x, y, z)
+ v3_set(fire_color, 9.0 * strength, 4.6 * strength, 1.4 * strength)
}
# the light in the hand: a point light (cone < -1) or a cone along dir (cone = cos half-angle)
@@ -250,23 +280,23 @@ function daylight_fire(render3d_st: Render3dState, x: float, y: float, z: float,
# is what a pool of firelight looks like. It used to be a windowed inverse square with the
# window and the scale both hard-coded, so every hand light in every game had the reach of
# a candle whatever it was meant to be.
-function daylight_hand(render3d_st: mut Render3dState, x: float, y: float, z: float, dx: float, dy: float, dz: float, cone: float, reach: float, r: float, g: float, b: float) -> void {
- v3_set(render3d_st.hand_pos, x, y, z); v3_set(render3d_st.hand_dir, dx, dy, dz); render3d_st.hand_cone = cone
- v3_set(render3d_st.hand_color, r, g, b)
- render3d_st.hand_reach = reach
+function daylight_hand(x: float, y: float, z: float, dx: float, dy: float, dz: float, cone: float, reach: float, r: float, g: float, b: float) -> void {
+ v3_set(hand_pos, x, y, z); v3_set(hand_dir, dx, dy, dz); hand_cone = cone
+ v3_set(hand_color, r, g, b)
+ hand_reach = reach
}
-function daylight_bind(render3d_st: mut Render3dState, prog: int) -> void {
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_hand_pos"), render3d_st.hand_pos)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_hand_color"), render3d_st.hand_color)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_hand_dir"), render3d_st.hand_dir)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hand_cone"), render3d_st.hand_cone)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hand_reach"), render3d_st.hand_reach)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_ibl_scale"), render3d_st.day_ibl)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_moon_dir"), render3d_st.day_moon_dir)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_moon_phase"), render3d_st.day_moon_phase)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_moon_illum"), render3d_st.day_moon_illum)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_moon_haze"), render3d_st.day_overcast)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_daylight"), render3d_st.day_light)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_fire_pos"), render3d_st.fire_pos)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_fire_color"), render3d_st.fire_color)
+function daylight_bind(prog: int) -> void {
+ u_v3(gpu_uniform(prog, "u_hand_pos"), hand_pos)
+ u_v3(gpu_uniform(prog, "u_hand_color"), hand_color)
+ u_v3(gpu_uniform(prog, "u_hand_dir"), hand_dir)
+ u_f(gpu_uniform(prog, "u_hand_cone"), hand_cone)
+ u_f(gpu_uniform(prog, "u_hand_reach"), hand_reach)
+ u_v3(gpu_uniform(prog, "u_ibl_scale"), day_ibl)
+ u_v3(gpu_uniform(prog, "u_moon_dir"), day_moon_dir)
+ u_f(gpu_uniform(prog, "u_moon_phase"), day_moon_phase)
+ u_f(gpu_uniform(prog, "u_moon_illum"), day_moon_illum)
+ u_f(gpu_uniform(prog, "u_moon_haze"), day_overcast)
+ u_f(gpu_uniform(prog, "u_daylight"), day_light)
+ u_v3(gpu_uniform(prog, "u_fire_pos"), fire_pos)
+ u_v3(gpu_uniform(prog, "u_fire_color"), fire_color)
}
diff --git a/packages/ludic.render3d/drawstats.ludic b/packages/ludic.render3d/drawstats.ludic
index 82f7e612..e971b96b 100644
--- a/packages/ludic.render3d/drawstats.ludic
+++ b/packages/ludic.render3d/drawstats.ludic
@@ -26,86 +26,103 @@ const DS_RANGE: int = 3
const DS_INDIRECT: int = 4
const DS_KINDS: int = 5
+var ds_on: int = -1 # -1: not read yet
+var ds_counting: bool = false
+var ds_from: int = 90
+var ds_frames: int = 60
+var ds_frame_no: int = 0
+var ds_outside: pointer = "(outside passes)"
# passes, in the order the frame first opens them
+var ds_pass_names: []pointer = null
+var ds_pass_prog: []long = null # program changes inside the pass
+var ds_pass_tex: []long = null # texture binds inside the pass
+var ds_pass_i: int = 0
# rows: (pass, program, kind)
+var ds_row_pass: []int = null
+var ds_row_prog: []int = null
+var ds_row_kind: []int = null
+var ds_row_draws: []long = null
+var ds_row_inst: []long = null
+var ds_row_idx: []long = null
+var ds_last: int = -1
-function ds_enabled(render3d_st: mut Render3dState) -> bool {
- if render3d_st.ds_on < 0 {
- render3d_st.ds_on = 0
- if r3d_env_has(render3d_st, "R3D_DRAWSTATS") {
- render3d_st.ds_on = 1
- render3d_st.ds_frames = Text.to_int(r3d_env(render3d_st, "R3D_DRAWSTATS"))
- if render3d_st.ds_frames < 2 { render3d_st.ds_frames = 60 }
- if r3d_env_has(render3d_st, "R3D_DRAWSTATS_FROM") { render3d_st.ds_from = Text.to_int(r3d_env(render3d_st, "R3D_DRAWSTATS_FROM")) }
- render3d_st.ds_pass_names = new []pointer; render3d_st.ds_pass_prog = new []long; render3d_st.ds_pass_tex = new []long
- render3d_st.ds_row_pass = new []int; render3d_st.ds_row_prog = new []int; render3d_st.ds_row_kind = new []int
- render3d_st.ds_row_draws = new []long; render3d_st.ds_row_inst = new []long; render3d_st.ds_row_idx = new []long
- ds_set_pass(render3d_st, render3d_st.ds_outside)
+function ds_enabled() -> bool {
+ if ds_on < 0 {
+ ds_on = 0
+ if r3d_env_has("R3D_DRAWSTATS") {
+ ds_on = 1
+ ds_frames = Text.to_int(r3d_env("R3D_DRAWSTATS"))
+ if ds_frames < 2 { ds_frames = 60 }
+ if r3d_env_has("R3D_DRAWSTATS_FROM") { ds_from = Text.to_int(r3d_env("R3D_DRAWSTATS_FROM")) }
+ ds_pass_names = new []pointer; ds_pass_prog = new []long; ds_pass_tex = new []long
+ ds_row_pass = new []int; ds_row_prog = new []int; ds_row_kind = new []int
+ ds_row_draws = new []long; ds_row_inst = new []long; ds_row_idx = new []long
+ ds_set_pass(ds_outside)
}
}
- return render3d_st.ds_on == 1
+ return ds_on == 1
}
# the profiler's pass boundaries (prof_begin / prof_end), whether or not R3D_PROF is on
-function ds_set_pass(render3d_st: mut Render3dState, name: pointer) -> void {
- if render3d_st.ds_on != 1 { return }
- render3d_st.ds_last = -1
+function ds_set_pass(name: pointer) -> void {
+ if ds_on != 1 { return }
+ ds_last = -1
var i = 0
- while i < len(render3d_st.ds_pass_names) {
- if render3d_st.ds_pass_names[i] == name { render3d_st.ds_pass_i = i; return }
+ while i < len(ds_pass_names) {
+ if ds_pass_names[i] == name { ds_pass_i = i; return }
i += 1
}
- push(render3d_st.ds_pass_names, name); push(render3d_st.ds_pass_prog, 0); push(render3d_st.ds_pass_tex, 0)
- render3d_st.ds_pass_i = len(render3d_st.ds_pass_names) - 1
+ push(ds_pass_names, name); push(ds_pass_prog, 0); push(ds_pass_tex, 0)
+ ds_pass_i = len(ds_pass_names) - 1
}
# once per render frame, before any of its work (prof_gen_frame)
-function ds_frame(render3d_st: mut Render3dState) -> void {
- if not ds_enabled(render3d_st) { return }
- render3d_st.ds_frame_no += 1
- ds_set_pass(render3d_st, render3d_st.ds_outside)
- if render3d_st.ds_frame_no == render3d_st.ds_from {
- render3d_st.ds_counting = true
- print(`r3d: drawstats counting frames {render3d_st.ds_from}..{render3d_st.ds_from + render3d_st.ds_frames - 1}`)
+function ds_frame() -> void {
+ if not ds_enabled() { return }
+ ds_frame_no += 1
+ ds_set_pass(ds_outside)
+ if ds_frame_no == ds_from {
+ ds_counting = true
+ print(`r3d: drawstats counting frames {ds_from}..{ds_from + ds_frames - 1}`)
}
- if render3d_st.ds_frame_no == render3d_st.ds_from + render3d_st.ds_frames {
- render3d_st.ds_counting = false
- ds_report(render3d_st)
+ if ds_frame_no == ds_from + ds_frames {
+ ds_counting = false
+ ds_report()
}
}
-function ds_draw(render3d_st: mut Render3dState, kind: int, instances: int, indices: int) -> void {
- if render3d_st.ds_on != 1 or not render3d_st.ds_counting { return }
- let p = render3d_st.gpu_prog_cur
- var r = render3d_st.ds_last
- if r < 0 or render3d_st.ds_row_prog[r] != p or render3d_st.ds_row_kind[r] != kind or render3d_st.ds_row_pass[r] != render3d_st.ds_pass_i {
+function ds_draw(kind: int, instances: int, indices: int) -> void {
+ if ds_on != 1 or not ds_counting { return }
+ let p = gpu_prog_cur
+ var r = ds_last
+ if r < 0 or ds_row_prog[r] != p or ds_row_kind[r] != kind or ds_row_pass[r] != ds_pass_i {
r = -1
var i = 0
- while r < 0 and i < len(render3d_st.ds_row_pass) {
- if render3d_st.ds_row_pass[i] == render3d_st.ds_pass_i and render3d_st.ds_row_prog[i] == p and render3d_st.ds_row_kind[i] == kind { r = i }
+ while r < 0 and i < len(ds_row_pass) {
+ if ds_row_pass[i] == ds_pass_i and ds_row_prog[i] == p and ds_row_kind[i] == kind { r = i }
i += 1
}
if r < 0 {
- push(render3d_st.ds_row_pass, render3d_st.ds_pass_i); push(render3d_st.ds_row_prog, p); push(render3d_st.ds_row_kind, kind)
- push(render3d_st.ds_row_draws, 0); push(render3d_st.ds_row_inst, 0); push(render3d_st.ds_row_idx, 0)
- r = len(render3d_st.ds_row_pass) - 1
+ push(ds_row_pass, ds_pass_i); push(ds_row_prog, p); push(ds_row_kind, kind)
+ push(ds_row_draws, 0); push(ds_row_inst, 0); push(ds_row_idx, 0)
+ r = len(ds_row_pass) - 1
}
- render3d_st.ds_last = r
+ ds_last = r
}
- render3d_st.ds_row_draws[r] = render3d_st.ds_row_draws[r] + 1
- render3d_st.ds_row_inst[r] = render3d_st.ds_row_inst[r] + instances
- render3d_st.ds_row_idx[r] = render3d_st.ds_row_idx[r] + indices
+ ds_row_draws[r] = ds_row_draws[r] + 1
+ ds_row_inst[r] = ds_row_inst[r] + instances
+ ds_row_idx[r] = ds_row_idx[r] + indices
}
-function ds_program_change(render3d_st: mut Render3dState, from: int, to: int) -> void {
- if render3d_st.ds_on != 1 or not render3d_st.ds_counting or from == to { return }
- render3d_st.ds_pass_prog[render3d_st.ds_pass_i] = render3d_st.ds_pass_prog[render3d_st.ds_pass_i] + 1
+function ds_program_change(from: int, to: int) -> void {
+ if ds_on != 1 or not ds_counting or from == to { return }
+ ds_pass_prog[ds_pass_i] = ds_pass_prog[ds_pass_i] + 1
}
-function ds_tex_bind(render3d_st: mut Render3dState) -> void {
- if render3d_st.ds_on != 1 or not render3d_st.ds_counting { return }
- render3d_st.ds_pass_tex[render3d_st.ds_pass_i] = render3d_st.ds_pass_tex[render3d_st.ds_pass_i] + 1
+function ds_tex_bind() -> void {
+ if ds_on != 1 or not ds_counting { return }
+ ds_pass_tex[ds_pass_i] = ds_pass_tex[ds_pass_i] + 1
}
function ds_kind_name(k: int) -> pointer {
@@ -116,18 +133,18 @@ function ds_kind_name(k: int) -> pointer {
return "indirect"
}
# a per-frame mean with one decimal
-function ds_per(render3d_st: Render3dState, v: long) -> string {
- let x = (v * 10) / render3d_st.ds_frames
+function ds_per(v: long) -> string {
+ let x = (v * 10) / ds_frames
return `{string(x / 10)}.{string(x % 10)}`
}
-function ds_backend(render3d_st: Render3dState) -> pointer {
- if render3d_st.gpu_kind == GPU_VK { return "vulkan" }
+function ds_backend() -> pointer {
+ if gpu_kind == GPU_VK { return "vulkan" }
return "opengl"
}
-function ds_report(render3d_st: mut Render3dState) -> void {
- let n_rows = len(render3d_st.ds_row_pass)
- let n_pass = len(render3d_st.ds_pass_names)
+function ds_report() -> void {
+ let n_rows = len(ds_row_pass)
+ let n_pass = len(ds_pass_names)
var total: long = 0
let by_kind = new []long
for k in 0 .. DS_KINDS { push(by_kind, 0) }
@@ -135,20 +152,20 @@ function ds_report(render3d_st: mut Render3dState) -> void {
let pass_inst = new []long
for s in 0 .. n_pass { push(pass_draws, 0); push(pass_inst, 0) }
for r in 0 .. n_rows {
- total = total + render3d_st.ds_row_draws[r]
- by_kind[render3d_st.ds_row_kind[r]] = by_kind[render3d_st.ds_row_kind[r]] + render3d_st.ds_row_draws[r]
- pass_draws[render3d_st.ds_row_pass[r]] = pass_draws[render3d_st.ds_row_pass[r]] + render3d_st.ds_row_draws[r]
- pass_inst[render3d_st.ds_row_pass[r]] = pass_inst[render3d_st.ds_row_pass[r]] + render3d_st.ds_row_inst[r]
+ total = total + ds_row_draws[r]
+ by_kind[ds_row_kind[r]] = by_kind[ds_row_kind[r]] + ds_row_draws[r]
+ pass_draws[ds_row_pass[r]] = pass_draws[ds_row_pass[r]] + ds_row_draws[r]
+ pass_inst[ds_row_pass[r]] = pass_inst[ds_row_pass[r]] + ds_row_inst[r]
}
print("")
- print(`draw calls per frame ({ds_backend(render3d_st)}, {gl_w}x{gl_h}, mean of {render3d_st.ds_frames} frames from {render3d_st.ds_from}): {ds_per(render3d_st, total)}`)
+ print(`draw calls per frame ({ds_backend()}, {gl_w}x{gl_h}, mean of {ds_frames} frames from {ds_from}): {ds_per(total)}`)
var kinds = ""
- for k in 0 .. DS_KINDS { kinds = kinds + ` {ds_kind_name(k)} {ds_per(render3d_st, by_kind[k])}` }
+ for k in 0 .. DS_KINDS { kinds = kinds + ` {ds_kind_name(k)} {ds_per(by_kind[k])}` }
print(` by kind:{kinds}`)
print("")
print(" pass draws instances program changes texture binds")
for s in 0 .. n_pass {
- print(` {Text.pad_right(render3d_st.ds_pass_names[s], 24)} {Text.pad_left(ds_per(render3d_st, pass_draws[s]), 9)} {Text.pad_left(ds_per(render3d_st, pass_inst[s]), 11)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_pass_prog[s]), 17)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_pass_tex[s]), 15)}`)
+ print(` {Text.pad_right(ds_pass_names[s], 24)} {Text.pad_left(ds_per(pass_draws[s]), 9)} {Text.pad_left(ds_per(pass_inst[s]), 11)} {Text.pad_left(ds_per(ds_pass_prog[s]), 17)} {Text.pad_left(ds_per(ds_pass_tex[s]), 15)}`)
}
# rows by draws, most first
let order = new []int
@@ -157,7 +174,7 @@ function ds_report(render3d_st: mut Render3dState) -> void {
while a < n_rows {
let v = order[a]
var b = a - 1
- while b >= 0 and render3d_st.ds_row_draws[order[b]] < render3d_st.ds_row_draws[v] { order[b + 1] = order[b]; b -= 1 }
+ while b >= 0 and ds_row_draws[order[b]] < ds_row_draws[v] { order[b + 1] = order[b]; b -= 1 }
order[b + 1] = v
a += 1
}
@@ -167,19 +184,19 @@ function ds_report(render3d_st: mut Render3dState) -> void {
var shown = 0
while shown < 40 and shown < n_rows {
let r = order[shown]
- print(` {Text.pad_left(ds_per(render3d_st, render3d_st.ds_row_draws[r]), 9)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_row_inst[r]), 11)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_row_idx[r]), 12)} {Text.pad_right(render3d_st.ds_pass_names[render3d_st.ds_row_pass[r]], 24)} {Text.pad_right(ds_kind_name(render3d_st.ds_row_kind[r]), 14)} {gpu_program_key(render3d_st, render3d_st.ds_row_prog[r])}`)
+ print(` {Text.pad_left(ds_per(ds_row_draws[r]), 9)} {Text.pad_left(ds_per(ds_row_inst[r]), 11)} {Text.pad_left(ds_per(ds_row_idx[r]), 12)} {Text.pad_right(ds_pass_names[ds_row_pass[r]], 24)} {Text.pad_right(ds_kind_name(ds_row_kind[r]), 14)} {gpu_program_key(ds_row_prog[r])}`)
shown += 1
}
var path = "build/drawstats.csv"
- if r3d_env_has(render3d_st, "R3D_DRAWSTATS_CSV") { path = r3d_env(render3d_st, "R3D_DRAWSTATS_CSV") }
+ if r3d_env_has("R3D_DRAWSTATS_CSV") { path = r3d_env("R3D_DRAWSTATS_CSV") }
let f = file_open(path, "wb")
if f == null { print(`r3d: drawstats could not write {path}`); return }
let head = "backend,width,height,frames,pass,kind,program,draws_per_frame,instances_per_frame,indices_per_frame,pass_program_changes_per_frame,pass_texture_binds_per_frame\n"
file_write(f, head, len(head))
for i in 0 .. n_rows {
let r = order[i]
- let s = render3d_st.ds_row_pass[r]
- let line = `{ds_backend(render3d_st)},{gl_w},{gl_h},{render3d_st.ds_frames},"{render3d_st.ds_pass_names[s]}",{ds_kind_name(render3d_st.ds_row_kind[r])},"{gpu_program_key(render3d_st, render3d_st.ds_row_prog[r])}",{ds_per(render3d_st, render3d_st.ds_row_draws[r])},{ds_per(render3d_st, render3d_st.ds_row_inst[r])},{ds_per(render3d_st, render3d_st.ds_row_idx[r])},{ds_per(render3d_st, render3d_st.ds_pass_prog[s])},{ds_per(render3d_st, render3d_st.ds_pass_tex[s])}\n`
+ let s = ds_row_pass[r]
+ let line = `{ds_backend()},{gl_w},{gl_h},{ds_frames},"{ds_pass_names[s]}",{ds_kind_name(ds_row_kind[r])},"{gpu_program_key(ds_row_prog[r])}",{ds_per(ds_row_draws[r])},{ds_per(ds_row_inst[r])},{ds_per(ds_row_idx[r])},{ds_per(ds_pass_prog[s])},{ds_per(ds_pass_tex[s])}\n`
file_write(f, line, len(line))
}
file_close(f)
diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic
index 6879f6c8..71fbefde 100644
--- a/packages/ludic.render3d/env.ludic
+++ b/packages/ludic.render3d/env.ludic
@@ -4,862 +4,16 @@
# windowed build - which is what a player runs - only when R3D_DEV is set to
# anything but "0". Read every R3D_* through r3d_env_has / r3d_env, never
# Os.has_env / Os.env.
-export state Render3dState {
- ac_lit: AcProg = null
- ac_lit_cut: AcProg = null
- ac_sh: AcProg = null
- ac_sh_cut: AcProg = null
- ac_out: AcProg = null
- ac_out_cut: AcProg = null
- ac_actors: []Actor = null
- ac_next_id: int = 0
- ac_frame: int = 0
- ac_oq: []OutlineReq = null
- ac_oq_n: int = 0 # live entries; the array is kept and reused
- ac_oq_closed: bool = true
- ac_census_at: int = -2
- ac_lp0: floats = null
- ac_lpx: floats = null
- ac_lpy: floats = null
- ac_lpz: floats = null
- ac_cc_actors: int = 0
- ac_cc_draws: int = 0
- ac_cc_skinned: int = 0
- ac_cc_cut: int = 0
- cam_pos: floats = null # x, y, z
- cam_yaw: float = 0.0 # radians, 0 = looking down -z
- cam_pitch: float = 0.0
- cam_fov: float = 0.0 # vertical, radians
- cam_near: float = 0.0
- cam_far: float = 0.0
- cam_aspect: float = 0.0
- cam_view: floats = null
- cam_proj: floats = null
- cam_vp: floats = null
- cam_inv_vp: floats = null
- cam_inv_proj: floats = null
- cam_vp_clean: floats = null # view-projection, kept for depth reconstruction
- cam_inv_vp_clean: floats = null
- cam_fwd: floats = null
- cam_right: floats = null
- cam_planes: floats = null
- col_x: floats = null
- col_z: floats = null
- col_r: floats = null
- col_y0: floats = null
- col_y1: floats = null
- col_n: int = 0
- col_side: int = 0 # cells per side
- col_start: words = null # per cell: first index into col_sorted (side*side + 1)
- col_sorted: words = null
- col_built: bool = false
- col_out: floats = null # the resolved position (x, z)
- day_on: bool = false
- day_hours: float = 0.0 # float bits, 0 .. 24
- day_light: float = 0.0 # 0 night .. 1 full day (float bits)
- day_ibl: floats = null # rgb scale on the sky's light
- day_sun_base: floats = null # the HDRI's sun radiance, kept
- day_az0: float = 0.0 # the sun's azimuth at the reference hour (radians, yaw convention)
- day_yaw0: float = 0.0 # the sky yaw the scene was tuned at
- day_hour0: float = 0.0 # the hour the photograph was taken (10.5)
- day_dir: float = 0.0 # +1 / -1: which way the sun travels in yaw
- day_az: float = 0.0
- day_el: float = 0.0
- day_gen: int = 0 # bumps when the light moved enough to rebake the terrain shadow
- day_baked_az: float = 0.0
- day_baked_el: float = 0.0
- day_sky_baked: float = 0.0 # the sky yaw the convolutions were baked at
- fire_pos: floats = null
- fire_color: floats = null
- day_moon: bool = false
- day_moon_phase: float = 0.5 # 0.5: full, which is where the game used to be
- day_moon_illum: float = 1.0 # the lit fraction, derived from the phase
- day_moon_dir: floats = null # toward the moon, whether or not it is up
- hand_pos: floats = null
- hand_color: floats = null
- hand_dir: floats = null
- hand_cone: float = 0.0
- hand_reach: float = 0.0 # metres the hand light reaches (float bits)
- day_overcast: float = 0.0 # 0 clear .. 1 a low grey sky (float bits): dims the sun and the sky's light
- day_flash: float = 0.0 # a lightning flash this frame (0..1): the sun brightens for it
- day_fog_mul: float = 1.0 # multiplies the base fog density (rain and snow thicken the air)
- day_fog_base: float = 0.0
- ds_on: int = -1 # -1: not read yet
- ds_counting: bool = false
- ds_from: int = 90
- ds_frames: int = 60
- ds_frame_no: int = 0
- ds_outside: pointer = "(outside passes)"
- ds_pass_names: []pointer = null
- ds_pass_prog: []long = null # program changes inside the pass
- ds_pass_tex: []long = null # texture binds inside the pass
- ds_pass_i: int = 0
- ds_row_pass: []int = null
- ds_row_prog: []int = null
- ds_row_kind: []int = null
- ds_row_draws: []long = null
- ds_row_inst: []long = null
- ds_row_idx: []long = null
- ds_last: int = -1
- r3d_dev_v: int = -1
- gltf_dir: string = null
- gltf_bin: pointer = null
- gltf_doc: Val = null
- gltf_count: int = 0
- gltf_ctype: int = 0
- gltf_comps: int = 0
- gltf_tex_paths: []pointer = null
- gltf_tex_ids: words = null
- gltf_tex_n: int = 0
- gltf_white: int = 0
- gltf_flat: int = 0
- gltf_mat_names: []string = null
- gltf_mat_diff: words = null
- gltf_mat_nrm: words = null
- gltf_mat_arm: words = null
- gltf_mat_n: int = 0
- gltf_cutout: bool = false # the material being loaded is alpha-blended (a cut-out atlas)
- gpu_kind: int = 0 # the backend running; 0 until gpu_select
- gpu_wanted: int = 0 # what was asked for (setting or R3D_GFX)
- gpu_fallback_reason: string = null # why the wanted backend is not the one running
- gpu_s_depth_test: int = -1
- gpu_s_depth_func: int = -1
- gpu_s_depth_write: int = -1
- gpu_s_blend: int = -1
- gpu_s_blend_src: int = -1
- gpu_s_blend_dst: int = -1
- gpu_s_cull: int = -1
- gpu_s_cull_face: int = -1
- gpu_s_color_write: int = -1
- gpu_s_a2c: int = -1
- gpu_s_bias: int = -1
- gpu_s_bias_f: float = 0.0 # float bits, for a backend that bakes the bias into a pipeline
- gpu_s_bias_u: float = 0.0
- gpu_s_scissor: int = -1
- gpu_prog_ids: []int = null
- gpu_prog_keys: []string = null
- gpu_prog_cur: int = 0
- gpu_u_tmp: words = null
- gpu_cap_probed: bool = false
- gpu_cap_windows: bool = false # the advanced features exist on this platform at all
- gpu_cap_vulkan: bool = false # a Vulkan loader, an instance and a device
- gpu_cap_floor: bool = false # Vulkan 1.3 with everything the renderer's floor needs
- gpu_cap_rt: bool = false # ray query + acceleration structures
- gpu_cap_mesh: bool = false # VK_EXT_mesh_shader
- gpu_cap_nvidia: bool = false
- gpu_cap_rtx: int = 0 # the RTX generation (20, 30, 40, 50); 0 = not RTX
- gpu_cap_reflex: bool = false # VK_NV_low_latency2
- gpu_cap_hdr: bool = false # the instance offers HDR colour spaces
- gpu_cap_device: string = ""
- gpu_tx: words = null
- gpu_tx_cap: int = 0
- gpu_bound_2d: int = 0
- gpu_bound_array: int = 0
- gpu_unit_2d: words = null
- gpu_unit_cur: int = 0
- gpu_fb: words = null
- gpu_fb_cap: int = 0
- gpu_fb_cur: int = 0
- gpu_glcheck: int = -1
- gpu_rb_samples: int = 0
- gpu_drawbufs: words = null
- gpu_glcheck_seen: []string = null
- gpu_variants: []GpuVariant = null
- gvk_ready: bool = false
- gvk_inst: pointer = null
- gvk_pd: pointer = null
- gvk_dev: pointer = null
- gvk_queue: pointer = null
- gvk_family: int = -1
- gvk_mp: bytes = null # VkPhysicalDeviceMemoryProperties
- gvk_device_name: string = ""
- gvk_why: string = "" # why the device did not come up, for the fallback notice
- gvk_max_aniso: int = 0x3F800000 # the device's anisotropy limit as float bits; 1.0 when it has none
- gvk_want_surface: bool = false # set before gvk_init when the renderer will present to a window
- gvk_has_surface: bool = false # the instance and device can make a surface and a swapchain
- gvk_errlog: string = null
- gvk_errlog_read: bool = false
- gvk_has_mdi: bool = false # multiDrawIndirect + drawIndirectFirstInstance
- gvk_has_dic: bool = false # drawIndirectCount
- gvk_has_mesh: bool = false # VK_EXT_mesh_shader with its meshShader feature on
- gvk_mesh_fn: pointer = null # vkCmdDrawMeshTasksEXT: the device's own, the interposer exports none
- gvk_n_allocs: int = 0
- gvk_blk_mem: []long = null
- gvk_blk_kind: []int = null # memory type * 2, + 1 for images
- gvk_blk_size: []int = null
- gvk_blk_map: []pointer = null
- gvk_fr_blk: []int = null # free ranges: block, offset, length (0 = an unused slot)
- gvk_fr_off: []int = null
- gvk_fr_len: []int = null
- gvk_al_blk: []int = null # per allocation id: its block, or -1 for its own memory
- gvk_al_mem: []long = null # its own memory when it has one
- gvk_al_off: []int = null
- gvk_al_len: []int = null
- gvk_al_map: []pointer = null
- gvk_al_spare: []int = null # ids to reuse
- gvk_pool: long = 0
- gvk_fence: bytes = null
- gvk_has_hqr: bool = false
- gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
- gvk_qpool: long = 0
- gvk_q_active: int = -1
- gvk_prog_var: []GpuVariant = null
- gvk_prog_vs: []long = null
- gvk_prog_fs: []long = null
- gvk_prog_dsl: []long = null
- gvk_prog_layout: []long = null
- gvk_spv_dir: string = ""
- gvk_spv_len: int = 0
- gvk_prog_mesh: []int = null
- gvk_cp_pipe: []long = null
- gvk_cp_layout: []long = null
- gvk_cp_dsl: []long = null
- gvk_cp_nbuf: []int = null
- gvk_pipe_keys: []string = null
- gvk_pipe: []long = null
- gvk_zero_vbuf: int = 0
- gvk_skip_said: words = null # per program: its skipped draws have been reported
- gvk_ublk_v: []bytes = null # per program: the vertex stage's block as last set
- gvk_ublk_f: []bytes = null
- gvk_prog_tex: []words = null # per program: the texture bound to each manifest sampler
- gvk_prog_unit: []words = null # per program: the unit + 1 each sampler was pointed at with u_i
- gvk_prog_dirty: []int = null # per program: its blocks or textures changed since its last set
- gvk_set_last: []long = null # per program: the descriptor set its last draw used
- gvk_set_frame: []int = null # per program: the frame that set belongs to
- gvk_set_tex: []words = null # per program: the textures that set was filled with
- gvk_ring_buf: int = 0 # a gvk_buf handle
- gvk_ring_off: int = 0
- gvk_ring_align: int = 256
- gvk_dpool: long = 0
- gvk_white: int = 0 # a 1x1 white texture for a sampler nothing was bound to
- gvk_msaa_max: int = 0 # samples the scene may use (gvk_frame_init reads the device's limits)
- gvk_kpool: long = 0
- gvk_sc_prog: []int = null # per cached set: its program
- gvk_sc_koff: []int = null # per cached set: where its key starts in gvk_sc_keys
- gvk_sc_set: []long = null
- gvk_sc_next: []int = null # the program's next cached set, -1 at the end
- gvk_sc_keys: []long = null # per texture of a key: handle * 65536 + generation, sampler
- gvk_sc_head: words = null # per program (< 4096): its most recently made set, -1 if none
- gvk_sc_tmp: []long = null # this draw's key while it is looked up
- gvk_set_offs: bytes = null # this draw's dynamic offsets, in binding order
- gvk_set_ndyn: int = 0
- gvk_ub_frame: []int = null # per program: the frame its blocks were last copied into the ring
- gvk_ub_offv: []int = null
- gvk_ub_offf: []int = null
- gvk_sc_made: int = 0 # sets made since start (R3D_VK_PROF)
- gvk_default_smp: long = 0 # linear, clamped: for a texture slot nothing was bound to
- gvk_cb: pointer = null
- gvk_in_pass: bool = false
- gvk_fb_cur: int = 0
- gvk_fb_read: int = 0
- gvk_fb_draw: int = 0
- gvk_pass_w: int = 0
- gvk_pass_h: int = 0
- gvk_pass_ncolor: int = 0
- gvk_pass_cfmt: int = 0
- gvk_pass_dfmt: int = 0
- gvk_pass_samples: int = 1 # the attachments' sample count, which every pipeline in the pass must match
- gvk_pass_col: words = null # the pass's colour attachments: texture, layer + 1
- gvk_pass_dep: words = null # its depth attachment: texture, layer + 1
- gvk_clear_bits: int = 0 # GL_COLOR_BUFFER_BIT / GL_DEPTH_BUFFER_BIT waiting for the pass
- gvk_clear_rgba: floats = null # float bits
- gvk_vp: words = null # x, y, w, h
- gvk_sc: words = null # scissor on, x, y (OpenGL rows), w, h
- gvk_screen_color: int = 0
- gvk_screen_depth: int = 0
- gvk_screen_w: int = 0
- gvk_screen_h: int = 0
- gvk_layer_views: []string = null # "tex:layer" -> index into gvk_layer_view
- gvk_layer_view: []long = null
- gvk_fb_ncolor: words = null # per framebuffer: colour slots drawn (draw buffers; 0 = none)
- gvk_hdr_want: bool = false # r3d_hdr: the setting
- gvk_hdr_on: bool = false # the swapchain is HDR10 now
- gvk_has_colorspace: bool = false # the instance took VK_EXT_swapchain_colorspace
- gvk_has_hdr_meta: bool = false # the device took VK_EXT_hdr_metadata
- r3d_hdr_peak: float = 0.0 # 0 until r3d_hdr_calibrate: 1000
- r3d_hdr_paper: float = 0.0 # 200
- r3d_hdr_black: float = 0.0 # 0
- gvk_fb_counter: int = 0
- gvk_prog_counter: int = 0
- gvk_wireframe: int = 0
- gvk_state: GvkState = null
- gvk_ind_buf: int = 0
- gvk_ind_off: int = 0
- gvk_ind_n: int = 0
- gvk_ind_cbuf: int = 0
- gvk_ind_coff: int = 0
- gvk_mesh_x: int = 0
- gvk_mesh_y: int = 0
- gvk_mesh_z: int = 0
- gvk_surface: long = 0
- gvk_swap: long = 0
- gvk_swap_n: int = 0
- gvk_swap_images: bytes = null
- gvk_swap_fmt: int = 0
- gvk_swap_w: int = 0
- gvk_swap_h: int = 0
- gvk_vsync: bool = true
- gvk_acq_fence: bytes = null
- gvk_swap_stale: bool = false
- gvk_size_buf: words = null
- gvk_prof_state: int = -1
- gvk_us_pipe: long = 0
- gvk_us_set: long = 0
- gvk_us_draw: long = 0
- gvk_n_draws: int = 0
- gvk_n_asked: int = 0 # draws that reached gvk_draw_now: every one the renderer asked for
- gvk_n_flush: int = 0
- gvk_prof_frames: int = 0
- gvk_n_pipe_new: int = 0 # pipelines made this profile window
- gvk_layout_keys: []string = null
- gvk_pc_prog: []int = null
- gvk_pc_layout: []int = null
- gvk_pc_state: []int = null
- gvk_pc_bias: []int = null
- gvk_pc_pass: []int = null
- gvk_pc_pipe: []long = null
- gvk_pc_last: words = null
- gvk_tex_image: []long = null
- gvk_tex_view: []long = null
- gvk_tex_mem: []long = null
- gvk_tex_levels: []int = null
- gvk_tex_layers: []int = null
- gvk_tex_vkfmt: []int = null
- gvk_tex_dims_w: []int = null
- gvk_tex_dims_h: []int = null
- gvk_tex_glfmt: []int = null # the format the renderer asked for (gpu.ludic's vocabulary)
- gvk_tex_array: []int = null # 1 for an array image
- gvk_tex_gen: []int = null # bumped whenever the image behind a handle is replaced
- gvk_tex_smp_sig: []int = null # the sampling parameters the cached sampler was made for
- gvk_tex_smp: []long = null
- gvk_tex_samples: []int = null # 1, or the sample count of a multisampled renderbuffer
- gvk_storage_samples: int = 1 # what the next gvk_tex_storage makes: gpu_rb_storage sets it around its call
- gvk_unpack_swap: bool = false # GL_UNPACK_SWAP_BYTES: 16-bit PNG samples arrive big-endian
- gvk_st_buf: long = 0
- gvk_st_mem: long = 0
- gvk_smp_keys: []string = null
- gvk_smp: []long = null
- gvk_buf: []long = null
- gvk_buf_mem: []long = null
- gvk_buf_size: []int = null
- gvk_buf_map: []pointer = null # host-visible buffers stay mapped for their whole life
- gvk_buf_used: []int = null # the frame (gvk_frame_no) a draw last bound the buffer in
- gvk_frame_no: int = 1
- gvk_retired_buf: []long = null
- gvk_retired_mem: []long = null
- gvk_buf_gpu: []int = null
- grass_prog: int = 0
- grass_mesh: Mesh = null
- grass_on: bool = true
- grass_wind: float = 0.0
- grass_blade_tex: int = 0
- grass_blade_cols: int = 8
- grass_push_x: float = 0.0
- grass_push_z: float = 0.0
- grass_push_r: float = 0.0
- grass_s0: float = 0.0 # float bits: blade spacing at the camera (m)
- grass_d0: float = 0.0 # the distance at which the spacing has doubled (m)
- grass_radius: float = 0.0 # no blades past this (m)
- grass_draws: int = 0
- grass_dbg: int = 0
- grass_merge: bool = false
- grass_rec: words = null # VkDrawIndexedIndirectCommand records, 5 words each
- grass_tv: floats = null # per record: corner x, corner z, indices per cell, 0 (float bits)
- grass_chunk_tv: floats = null
- grass_n: int = 0
- grass_cmds: int = 0
- grass_band_start: words = null # per band: its first record
- grass_band_cells: words = null # ... and its 16 m cells per tile side
- grass_band_n: int = 0
- grass_mesh_on: bool = false
- grass_mesh_prog: int = 0
- r3d_draw_cb: fn() = null
- r3d_casters_cb: fn(floats) = null
- r3d_fill_cb: fn(Stream, int, int, int) = null
- ov_prog: int = 0
- ov_mesh: Mesh = null
- ov_vbo: int = 0
- ov_buf: pointer = null
- ov_n: int = 0
- ov_tex: int = 0
- ov_mode: int = 2 # what the next quads read: 0 the image texture, 1 the font, 2 a flat colour
- ov_voff: float = 0.0 # float bits: 4 x ov_mode, added to v (overlay.frag)
- ov_white: int = 0
- ov_font: int = 0
- ov_font_adv: floats = null # float bits, em units, one per glyph in the atlas
- ov_font_n: int = 0 # glyphs in the atlas
- ov_font_map: words = null
- ov_font_hi_cp: words = null # code points >= OV_MAP_N, ascending
- ov_font_hi_g: words = null
- ov_font_hi_n: int = 0
- ov_font_space: int = 0 # the glyph for ' ', and for what has none: '?'
- ov_font_qmark: int = 0
- ov_font_cols: int = 16
- ov_font_rows: int = 6
- ov_font_cell: int = 128 # px per cell in the atlas
- ov_font_em: int = 100 # px per em in the atlas
- ov_pad_x: float = 0.0 # float bits, em
- ov_base_y: float = 0.0
- ov_ready: bool = false
- ov_open: bool = false
- ov_dbg: bool = false
- ov_ranges: words = null
- ov_nr: int = 0
- ov_range_start: int = 0
- ov_clip_x: int = 0 # the current clip rectangle in screen pixels (top-left origin)
- ov_clip_y: int = 0
- ov_clip_w: int = 0
- ov_clip_h: int = 0
- ov_prog_sdr: int = 0
- ov_prog_hdr: int = 0
- ov_hdr_scale: float = 0.0 # float bits; 0 until ov_begin sets 1
- ov_u8_len: int = 1
- post_hdr: Target = null
- post_ms_fbo: int = 0 # 4x multisampled scene target, resolved into post_hdr
- post_ms_samples: int = 1 # temporal AA carries the edges; R3D_MSAA=n to compare
- post_bloom: []Target = null
- post_p_down: int = 0
- post_p_up: int = 0
- post_p_tone: int = 0
- post_fs: Mesh = null
- post_exposure: float = 0.0
- post_bloom_strength: int = 0
- post_vignette: float = 0.0
- post_saturation: float = 0.0
- post_contrast: float = 0.0
- post_w: int = 0
- post_h: int = 0
- post_auto: bool = true
- post_key: float = 0.0 # target mean luminance after exposure (float bits)
- post_lum: words = null
- post_mips: int = 0
- post_adapt: float = 0.0 # smoothed exposure (float bits)
- post_exposure_max: float = 20.0 # 20: the ceiling auto-exposure may reach (night lowers it)
- post_wb_r: float = 1.02 # 1.02
- post_wb_g: float = 1.0 # 1.0
- post_wb_b: float = 0.97 # 0.97
- post_lift_r: float = 0.004 # 0.004
- post_lift_g: float = 0.004 # 0.004
- post_lift_b: float = 0.012 # 0.012
- post_gain_r: float = 0.99 # 0.99
- post_gain_g: float = 0.995 # 0.995
- post_gain_b: float = 1.0 # 1.0
- post_ao: Target = null
- post_ao_blur: Target = null
- post_p_ao: int = 0
- post_p_ao_blur: int = 0
- post_ao_radius: float = 0.0
- post_contact: float = 1.25 # 1.25: how hard a thing is darkened where it meets the ground
- post_ao_intensity: float = 0.0
- post_ao_strength: float = 0.0
- post_gi_strength: float = 0.4 # 0.4
- post_no_gi: bool = false
- post_ldr: Target = null
- post_depth_copy: Target = null
- post_vol: Target = null
- post_p_vol: int = 0
- post_vol_steps: float = 24.0 # 24
- post_vol_density: float = 0.002 # 0.002
- post_vol_falloff: float = 0.005 # 0.005
- post_vol_far: float = 800.0 # 800 m
- post_vol_g: float = 0.6 # 0.6: air throws light forward
- post_vol_mist: float = 0.0 # the day sets these two
- post_vol_mist_h: float = 40.0 # 40 m
- post_prev: Target = null # last frame's scene colour, for the SSGI bounce only
- post_scene: Target = null # this frame's scene colour before the water, for refraction
- post_frame: int = 0
- post_color_w: int = 0 # its size: the display's when DLSS upscaled it
- post_color_h: int = 0
- post_color: int = 0 # the HDR colour the rest of post reads # the resolved depth, copied so passes can read it while drawing into the frame
- post_p_sharp: int = 0
- post_fxaa: float = 1.0
- post_p_dof: int = 0
- post_dof: Target = null
- post_dof_focus: float = 10.0 # 10 m
- post_dof_aperture: float = 0.0 # 0 = no lens, and no pass
- post_dof_max: float = 12.0 # 12 px
- post_p_tone_hdr: int = 0 # the tonemap's HDR10 variant, made the first time HDR is on
- post_ldr_hdr: bool = false # post_ldr was made for HDR10 output (10-bit)
- post_sharpen: float = 0.0
- post_grain: int = 0
- post_adapt_t: []Target = null
- post_adapt_i: int = 0
- post_p_adapt: int = 0
- post_adapt_reset: bool = true
- prof_on: bool = false
- prof_names: []pointer = null
- prof_ids: words = null # PROF_SLOTS * PROF_RING query objects
- prof_ns: []long = null # accumulated nanoseconds per slot
- prof_hits: []long = null # samples accumulated per slot
- prof_n: int = 0 # slots in use
- prof_frame: int = 0
- prof_active: int = -1 # slot whose query is currently open
- prof_scratch: words = null
- prof_gen: []int = null
- prof_gen_cur: int = 0
- prof_ft: []long = null # real frame times, microseconds
- prof_last_us: long = 0
- prof_chunk_us: []long = null
- prof_chunk_kind: []int = null
- prof_chunk_band: []int = null
- prof_chunk_n: []int = null
- prof_gen_us_cur: long = 0
- prof_gen_us: []long = null # generation microseconds per frame
- prof_layer_us_cur: long = 0 # rebuilding and uploading instance buffers
- prof_bake_us_cur: long = 0 # the height-field shadow rebake
- prof_layer_us: []long = null
- prof_bake_us: []long = null
- prof_dt: []long = null # frame time, aligned with the arrays above
- prof_up_cur: long = 0 # instance bytes uploaded this frame
- prof_up: []long = null
- prof_pre_swap: long = 0
- prof_cpu_us: []long = null
- prof_sum_dt: long = 0 # sums over the frames past the first eight, for the mean split
- prof_sum_cpu: long = 0
- prof_sum_game: long = 0
- prof_sum_n: int = 0
- prof_mark_last: long = 0
- prof_mark_best: long = 0
- prof_mark_name: pointer = null
- prof_mark_us: []long = null
- prof_mark_who: []pointer = null
- prof_mk_names: []pointer = null
- prof_mk_us: []long = null
- prof_mk_n: int = 0
- prof_game_us_cur: long = 0
- prof_game_us: []long = null
- r3d_root: string = "packages/ludic.render3d" # where shaders/ lives
- r3d_assets: string = "assets/polyhaven" # where the CC0 assets live
- r3d_root_found: bool = false
- r3d_global_defs: string = ""
- r3d_noise_src: string = null
- r3d_lighting_src: string = null
- r3d_wind_src: string = null
- r3d_prog_log: int = -1
- q_scratch: floats = null
- q_sy: floats = null # views of q_scratch's second, third and fourth
- q_sz: floats = null
- q_st: floats = null
- r3d_sky_prog: int = 0
- r3d_fog_density: float = 0.0
- r3d_fog_scale: float = 1.0 # float bits: a setting's multiplier over the density the day sets
- r3d_fog_falloff: float = 0.0
- r3d_fog_base: float = 0.0 # the height fog is measured from (float bits); 0 = y = 0
- r3d_fog_inscatter: float = 0.02 # 0.02, what the shader used to hard-code
- r3d_fog_desat: float = 1.35 # 1.35
- r3d_sky_relight: float = 0.0
- r3d_ground_alb_r: float = 0.3
- r3d_ground_alb_g: float = 0.34
- r3d_ground_alb_b: float = 0.14
- r3d_ground_hi_r: float = 0.289
- r3d_ground_hi_g: float = 0.28
- r3d_ground_hi_b: float = 0.26
- r3d_ground_hi_y: float = 480.0 # 480
- r3d_ground_hi_w: float = 150.0 # 150
- r3d_time: float = 0.0
- r3d_ready: bool = false
- r3d_dem_path: string = null
- r3d_dem_min: float = 0.0
- r3d_dem_max: float = 0.0
- r3d_dem_base: float = 0.0
- r3d_dem_ox: float = 0.0
- r3d_dem_oz: float = 0.0
- r3d_ortho_path: string = null
- r3d_plate: bool = false
- r3d_sky_path: string = null
- r3d_debug: bool = false
- r3d_debug_shadow: bool = false
- r3d_debug_max: bool = false
- r3d_cloud_shadow: float = 0.5 # 0.5
- r3d_clip_y: float = -2147483600.0 # -2^31: no clipping
- r3d_prepass_env: int = -1
- r3d_test_frame: int = 0
- r3d_test_resize: int = 0
- r3d_no_shadow: bool = false
- r3d_no_trees: bool = false
- r3d_no_refl: bool = false
- r3d_cam_log: int = -1
- sc_prog: int = 0
- sc_prog_fol: int = 0
- sc_prog_wind: int = 0
- sc_prog_blade: int = 0
- sc_prog_flower: int = 0
- sc_prog_card: int = 0
- sc_prog_card_shadow: int = 0
- sc_prog_card_cheap: int = 0
- sc_blade_base: floats = null
- sc_blade_tip: floats = null
- sc_blade_tint: floats = null
- sc_prog_shadow: int = 0
- sc_prog_shadow_wind: int = 0
- sc_prog_shadow_fol: int = 0 # foliage meshes: alpha-tested casters
- sc_prog_fol_depth: int = 0
- sc_prog_fol_eq: int = 0
- sc_prepass: bool = false # the main pass is drawing over what the prepass laid down
- sc_imp_prog: int = 0
- sc_imp_prog_shadow: int = 0
- sc_bake_prog: int = 0
- sc_bake_card_prog: int = 0
- sc_bake_flower_prog: int = 0
- sc_card: Mesh = null
- sc_ident_buf: int = 0
- sc_layers: []Layer = null
- sc_debug_dump: bool = false
- sc_printed: bool = false
- sc_a2c: bool = true
- sc_bake_flower: bool = false
- sc_dump_n: int = 0
- sc_view_gen: int = 1
- sc_view_pos: floats = null
- sc_view_fwd: floats = null
- sc_cull_prog: int = 0
- sc_cull_tried: bool = false
- sc_ind_base: int = -1 # >= 0: layer_draw_model / layer_draw_depth draw records from here
- sc_ind_n: int = 1 # records each of those draws covers: one per level of a material
- sc_ind_stride: int = 4 # records between one material's first and the next's (3 for the shadow LOD)
- sc_freeze: bool = false # a secondary pass (reflection) reuses the partition
- sc_dbg_blade: int = 0
- sc_dbg_level: int = -1
- sc_dbg_lod: bool = false
- sc_dbg_tint: floats = null
- sc_cast_all: int = -1
- sc_cast_gen: int = -1
- sc_cast_dy: float = 0.0
- sc_cast_k: float = 0.0
- sc_skip_blade: bool = false
- sc_skip_flower: bool = false
- sc_skip_card: bool = false
- cb_state: int = 12345
- shadow_res: int = 2048
- shadow_refused: int = 0 # the last size the graphics card had no memory for (0: none)
- sh_tex: int = 0
- sh_fbo: int = 0
- sh_vp: floats = null # the cascades' view-projections, 16 floats each
- sh_vp_v: [][]float = null # a view of each
- sh_split: floats = null # view-space far distance of each cascade
- sh_range: floats = null # 4 light-frustum depth extents (metres)
- sh_texel: floats = null # 4 shadow texel sizes (metres)
- sh_tmp_proj: floats = null
- sh_tmp_vp: floats = null
- sh_tmp_inv: floats = null
- sh_tmp_view: floats = null
- sh_corner: floats = null
- sh_cascade: int = 0 # the cascade being rendered (for casters that skip far ones)
- sh_printed2: bool = false
- sh_printed3: bool = false
- sh_enabled: bool = true
- sh_force: int = -1 # R3D_FORCE= pins every pixel to cascade c (debug)
- sh_skip_terrain: bool = false
- sh_probe_x: float = 0.0
- sh_probe_y: float = 0.0
- sh_probe_z: float = 0.0
- sh_printed: bool = false
- sky_tex: int = 0
- sky_w: int = 0
- sky_h: int = 0
- sky_irradiance: int = 0
- sky_prefilter: int = 0 # GL_TEXTURE_2D_ARRAY
- sky_brdf: int = 0
- sun_dir: floats = null # toward the sun (float bits)
- sun_color: floats = null # radiance (float bits)
- sky_fullscreen: Mesh = null
- sky_yaw: float = 0.0 # radians: the HDRI is turned by this about y
- sky_sun_boost: float = 2.3 # 2.3: the photograph's thin cloud dims its sun; a crisper day wants more
- sky_rot_s: float = 0.0
- sky_rot_c: float = 0.0
- sun_hdri: floats = null # the sun direction as found in the file
- sky_start_yaw: float = 0.0
- sky_baked: bool = false # the IBL set exists, baked at sky_baked_yaw
- sky_baked_yaw: float = 0.0
- sky_p_irr: int = 0
- sky_p_pre: int = 0
- sky_p_brdf: int = 0
- sky_prefilter_w: int = 512
- STREAM_MAX_CHUNKS: int = 4096
- stream_all: []Stream = null
- stream_cap_read: bool = false
- stream_no_evict: bool = false # R3D_NOEVICT: the old behaviour, for comparison
- stream_walk_no: int = 0 # counts ring walks; a chunk's age is measured in these
- stream_evictions: int = 0
- stream_us_gen: long = 0 # generating new chunks (stream_fill)
- stream_us_gather: long = 0 # copying cached chunks into the layer buffer
- stream_us_walk: long = 0 # the ring walk itself
- stream_walks: int = 0 # streams that walked their whole ring this frame
- stream_debug_n: int = 0
- stream_scratch: floats = null
- STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame; a setting may move it
- stream_deadline: long = 0
- stream_budget_left: int = 0
- gsl_on: bool = false # slInit succeeded: the interposer is the loader, the plugins are in
- gsl_dlss_ok: bool = false # what this adapter can run, from slIsFeatureSupported
- gsl_rr_ok: bool = false
- gsl_fg_ok: bool = false
- gsl_reflex_ok: bool = false
- gsl_pcl_ok: bool = false
- gsl_feats: bytes = null # kept alive: Preferences points at it
- gsl_logdir: bytes = null # and at this
- gsl_vp: bytes = null # sl::ViewportHandle 0, the one view
- gsl_tok_buf: bytes = null
- gsl_idx_buf: bytes = null
- gsl_token: pointer = null # this frame's sl::FrameToken, owned by Streamline
- gsl_frame_n: int = 0
- gsl_said: bool = false # one failure message, not one a frame
- gsl_fresh: bool = false # a token was taken since the last frame started (a present took it)
- gsl_reflex_mode: int = 0 # 0 off, 1 low latency, 2 low latency + boost
- gsl_reflex_applied: int = -1
- gsl_f_sleep: pointer = null
- gsl_f_marker: pointer = null
- gsl_dlss_mode: int = 0 # 0 off, 1 DLAA, 2 quality, 3 balanced, 4 performance
- gsl_opts: bytes = null
- gsl_opt_mode: int = -1
- gsl_opt_w: int = 0
- gsl_opt_h: int = 0
- gsl_rw: int = 0 # the render size DLSS asked for
- gsl_rh: int = 0
- gsl_set_mode: int = -1
- gsl_set_w: int = 0
- gsl_set_h: int = 0
- gsl_mv: Target = null
- gsl_out: Target = null
- gsl_consts: bytes = null
- gsl_tags: bytes = null
- gsl_res: bytes = null
- gsl_inputs: bytes = null
- gsl_prev_vp: floats = null
- gsl_reset: bool = true
- gsl_jitter_x: float = 0.0 # float bits, NDC offsets the projection carries this frame
- gsl_jitter_y: float = 0.0
- gsl_jpx: float = 0.0 # float bits, the same in pixels
- gsl_jpy: float = 0.0
- gsl_eval_ok: bool = true # the last evaluate worked: only then is the next frame jittered
- TERRAIN_HALF: int = 4096 # world half-size in metres (8 km square)
- cd_mesh: Mesh = null
- cd_range: floats = null # float bits: how far each level is drawn
- cd_min: []floats = null # per level: min height of each patch (float bits)
- cd_max: []floats = null
- cd_draws: int = 0
- cd_far_draws: int = 0
- cd_near_draws: int = 0
- ter_force_far: bool = false
- ter_no_split: bool = false
- ter_force_near: bool = false
- ter_skip: bool = false
- ter_height_tex: int = 0
- ter_heights: floats = null # CPU copy, float bits, TERRAIN_RES^2
- ter_reflect: bool = false # drawing the reflection: the mid mesh is plenty
- ter_prog: int = 0
- ter_prog_far: int = 0
- ter_prog_near: int = 0 # the detailed tier alone (NEAR_ONLY)
- ter_sun_prog: int = 0 # tersun.frag: sun visibility into a screen buffer
- ter_sun_inline: bool = false # the ground's shader reads the cascades itself (Vulkan); no sun pass
- ter_sun_tgt: Target = null # that buffer, at the frame's size
- ter_sun_refl: Target = null # and at the reflection's, which is smaller
- ter_sun_dumped: bool = false
- ter_sun_checked: bool = false
- ter_sun_done: bool = false # the caller already ran the pass (so it can time it)
- ter_sun_pass: bool = false # selection is drawing the visibility pass
- ter_sun_tex: int = 0
- ter_prog_cur: int = 0 # the program currently bound during selection
- ter_smooth: bool = false # generate the analytic test ground instead of a survey
- ter_far_split: float = 0.0 # metres: beyond this the terrain takes its cheap far path (R3D_TFAR)
- ter_far_band: float = 0.0 # half-width of the near/far blend (R3D_TBAND)
- ter_snow_line: float = 0.0
- ter_wet: float = 0.0
- ter_tex: words = null # 11 material textures (see terrain_bind)
- ter_ox: float = 0.0 # world x/z of the terrain centre (float bits)
- ter_oz: float = 0.0
- ter_dem_tex: int = 0 # a real height map (16-bit), or 0 for the procedural valley
- ter_dem_blur: float = 0.0 # gaussian texels applied to the survey (0 for lidar; ~3 for 30 m data)
- ter_dem_min: float = 0.0
- ter_dem_max: float = 0.0
- ter_dem_base: float = 0.0
- ter_ortho_tex: int = 0 # a photograph of the same window, draped with distance
- ter_carpet: int = 0 # the distant-grass carpet (carpet_bake), 0 = none
- ter_shadow_tex: int = 0 # height-field sun shadow: RG32F (lowest lit height, occluder distance)
- ter_shadow_yaw: float = 1000000000.0 # the sky yaw it was baked for
- ter_shadow_gen: int = -1 # the daylight generation it was baked for
- ter_shadow_prog: int = 0
- ter_lake_level: float = 0.0 # a lake carved into the height map (float bits; ex = 0 → none)
- ter_lake_cx: float = 0.0
- ter_lake_cz: float = 0.0
- ter_lake_ex: float = 0.0
- ter_lake_ez: float = 0.0
- ter_lake_carve: bool = true
- ter_sea_level: float = 0.0
- ter_sea_set: bool = false
- ter_isle_cx: float = 0.0
- ter_isle_cz: float = 0.0
- ter_isle_r: float = 0.0
- ter_isle_fall: float = 0.0
- ter_isle_mode: int = 0
- ter_ortho_px: pointer = null # the photograph on the CPU (RGB8, TERRAIN_RES^2) for placement rules
- ter_ortho_w: int = 0
- ter_ortho_c: int = 3
- ter_h_scale: float = 0.0
- ter_o_scale: float = 0.0
- ter_bw: floats = null
- ter_wire: bool = false
- ter_printed: bool = false
- ter_scr: floats = null
- tex_w: int = 0 # the last decoded image
- tex_h: int = 0
- tex_channels: int = 0
- tex_depth: int = 0 # bits per sample (8 or 16)
- tex_file_len: int = 0
- tex_anisotropy: fixed = 16.0
- tex_size_ids: []int = new []int
- tex_size_ws: []int = new []int
- tex_size_hs: []int = new []int
- hdr_max_lum: float = 0.0 # float bits of the brightest texel (sun finding)
- hdr_max_x: int = 0
- hdr_max_y: int = 0
- hdr_sun_r: float = 0.0 # irradiance (float bits) of everything above the IBL clip: the sun
- hdr_sun_g: float = 0.0
- hdr_sun_b: float = 0.0
- hdr_clip: float = 0.0 # float bits; texels above this (per channel) feed the sun, not the IBL
- tex_dump_alpha: bool = false
- water_mesh: Mesh = null
- water_prog: int = 0
- water_level: float = 0.0
- water_cx: float = 0.0
- water_cz: float = 0.0
- water_ex: float = 0.0
- water_ez: float = 0.0
- water_on: bool = false
- wt_wade_x: float = 0.0
- wt_wade_z: float = 0.0
- wt_wade_s: float = 0.0
- water_refl: Target = null # the world mirrored in the surface, half resolution
- water_refl_div: int = 2 # R3D_REFLDIV overrides: 2 = half res, 4 = quarter
- water_saved: floats = null # the real camera's matrices, restored after the pass
- water_saved_vp: floats = null # views of its second and third matrices
- water_saved_ivp: floats = null
- water_dumped: bool = false
- wb_n: int = 0
- wb_level: floats = null
- wb_cx: floats = null
- wb_cz: floats = null
- wb_ex: floats = null
- wb_ez: floats = null
- wb_reflect: words = null
- wb_primary: int = -1 # the body the reflection pass mirrors, or -1
- wb_cells: floats = null # x0, z0, x1, z1 of each rectangle with water showing
- wb_ncells: int = -1 # -1: not built for the current mirrored body
- wb_blocks: words = null # x0, z0, x1, z1, first cell, cells past the last - of each block
- wb_nblocks: int = 0
- wb_refl_always: int = -1
- wb_dbg: int = -1
- wb_build_us: long = 0 # how long the last water_cells_build took (R3D_REFL_DBG prints it)
+var r3d_dev_v: int = -1
+function r3d_dev() -> bool {
+ if r3d_dev_v < 0 { if Os.has_env("R3D_DEV") and Os.env("R3D_DEV") != "0" { r3d_dev_v = 1 } else { r3d_dev_v = 0 } }
+ return r3d_dev_v == 1
}
-function r3d_dev(render3d_st: mut Render3dState) -> bool {
- if render3d_st.r3d_dev_v < 0 { if Os.has_env("R3D_DEV") and Os.env("R3D_DEV") != "0" { render3d_st.r3d_dev_v = 1 } else { render3d_st.r3d_dev_v = 0 } }
- return render3d_st.r3d_dev_v == 1
-}
-function r3d_env_has(render3d_st: mut Render3dState, name: string) -> bool {
- if is_windowed() and not r3d_dev(render3d_st) { return false }
+function r3d_env_has(name: string) -> bool {
+ if is_windowed() and not r3d_dev() { return false }
return Os.has_env(name)
}
-function r3d_env(render3d_st: mut Render3dState, name: string) -> string {
- if is_windowed() and not r3d_dev(render3d_st) { return "" }
+function r3d_env(name: string) -> string {
+ if is_windowed() and not r3d_dev() { return "" }
return Os.env(name)
}
diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic
index c991972a..92aa0af5 100644
--- a/packages/ludic.render3d/gltf.ludic
+++ b/packages/ludic.render3d/gltf.ludic
@@ -22,6 +22,17 @@ property Model {
skin: Skin # the skeleton, for a skinned node (skin.ludic); null for a rigid model
}
+var gltf_dir: string = null
+var gltf_bin: pointer = null
+var gltf_doc: Val = null
+var gltf_count: int = 0
+var gltf_ctype: int = 0
+var gltf_comps: int = 0
+var gltf_tex_paths: []pointer = null
+var gltf_tex_ids: words = null
+var gltf_tex_n: int = 0
+var gltf_white: int = 0
+var gltf_flat: int = 0
# a JSON number as float bits (ints and fixed-point decimals both)
function jnum(v: Val) -> float {
@@ -37,35 +48,41 @@ function jint(v: Val, key: pointer, fallback: int) -> int {
# lod_export.py) carries the material names but no images — the blend links textures
# that are not downloaded with it — so its primitives take the textures the LOD0
# download's material of the same name loaded.
-function gltf_mat_find(render3d_st: Render3dState, name: string) -> int {
- if render3d_st.gltf_mat_names == null { return -1 }
- for i in 0 .. render3d_st.gltf_mat_n { if render3d_st.gltf_mat_names[i] == name { return i } }
+var gltf_mat_names: []string = null
+var gltf_mat_diff: words = null
+var gltf_mat_nrm: words = null
+var gltf_mat_arm: words = null
+var gltf_mat_n: int = 0
+function gltf_mat_find(name: string) -> int {
+ if gltf_mat_names == null { return -1 }
+ for i in 0 .. gltf_mat_n { if gltf_mat_names[i] == name { return i } }
return -1
}
-function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: int, nrm: int, arm: int) -> void {
- if render3d_st.gltf_mat_names == null { render3d_st.gltf_mat_names = new []string; render3d_st.gltf_mat_diff = words(256); render3d_st.gltf_mat_nrm = words(256); render3d_st.gltf_mat_arm = words(256) }
- if gltf_mat_find(render3d_st, name) >= 0 or render3d_st.gltf_mat_n >= 256 { return }
- push(render3d_st.gltf_mat_names, name); render3d_st.gltf_mat_diff[render3d_st.gltf_mat_n] = diff; render3d_st.gltf_mat_nrm[render3d_st.gltf_mat_n] = nrm; render3d_st.gltf_mat_arm[render3d_st.gltf_mat_n] = arm
- render3d_st.gltf_mat_n += 1
+function gltf_mat_remember(name: string, diff: int, nrm: int, arm: int) -> void {
+ if gltf_mat_names == null { gltf_mat_names = new []string; gltf_mat_diff = words(256); gltf_mat_nrm = words(256); gltf_mat_arm = words(256) }
+ if gltf_mat_find(name) >= 0 or gltf_mat_n >= 256 { return }
+ push(gltf_mat_names, name); gltf_mat_diff[gltf_mat_n] = diff; gltf_mat_nrm[gltf_mat_n] = nrm; gltf_mat_arm[gltf_mat_n] = arm
+ gltf_mat_n += 1
}
-function gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -> int {
- if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256) }
+var gltf_cutout: bool = false # the material being loaded is alpha-blended (a cut-out atlas)
+function gltf_texture(uri: string, srgb: bool) -> int {
+ if gltf_tex_paths == null { gltf_tex_paths = new []pointer; gltf_tex_ids = words(256) }
var png: string = uri
let n = len(uri)
if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png" }
- let path = render3d_st.gltf_dir + "/" + png
+ let path = gltf_dir + "/" + png
var i = 0
- while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { return render3d_st.gltf_tex_ids[i] }; i += 1 }
+ while i < gltf_tex_n { if gltf_tex_paths[i] == path { return gltf_tex_ids[i] }; i += 1 }
var dil = 0
- if render3d_st.gltf_cutout { dil = 24 }
- let id = tex_load_ex(render3d_st, path, srgb, dil)
- if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_n += 1 }
+ if gltf_cutout { dil = 24 }
+ let id = tex_load_ex(path, srgb, dil)
+ if gltf_tex_n < 256 { push(gltf_tex_paths, path); gltf_tex_ids[gltf_tex_n] = id; gltf_tex_n += 1 }
return id
}
# the image uri behind materials[m]..index, or null
-function gltf_mat_uri(render3d_st: Render3dState, mat: Val, slot: pointer) -> string {
+function gltf_mat_uri(mat: Val, slot: pointer) -> string {
var holder = mat
if slot == "baseColorTexture" or slot == "metallicRoughnessTexture" {
if value_has(mat, "pbrMetallicRoughness") == 0 { return null }
@@ -73,84 +90,84 @@ function gltf_mat_uri(render3d_st: Render3dState, mat: Val, slot: pointer) -> st
}
if value_has(holder, slot) == 0 { return null }
let ti = value_as_int(value_get(value_get(holder, slot), "index"))
- let tex = value_at(value_get(render3d_st.gltf_doc, "textures"), ti)
+ let tex = value_at(value_get(gltf_doc, "textures"), ti)
let src = value_as_int(value_get(tex, "source"))
- let img = value_at(value_get(render3d_st.gltf_doc, "images"), src)
+ let img = value_at(value_get(gltf_doc, "images"), src)
return value_as_str(value_get(img, "uri"))
}
# read one accessor's bytes from the .bin; sets gltf_count / gltf_ctype / gltf_comps
-function gltf_accessor(render3d_st: mut Render3dState, idx: int) -> pointer {
- let acc = value_at(value_get(render3d_st.gltf_doc, "accessors"), idx)
- let bv = value_at(value_get(render3d_st.gltf_doc, "bufferViews"), jint(acc, "bufferView", 0))
+function gltf_accessor(idx: int) -> pointer {
+ let acc = value_at(value_get(gltf_doc, "accessors"), idx)
+ let bv = value_at(value_get(gltf_doc, "bufferViews"), jint(acc, "bufferView", 0))
let off = jint(bv, "byteOffset", 0) + jint(acc, "byteOffset", 0)
- render3d_st.gltf_count = jint(acc, "count", 0)
- render3d_st.gltf_ctype = jint(acc, "componentType", 5126)
+ gltf_count = jint(acc, "count", 0)
+ gltf_ctype = jint(acc, "componentType", 5126)
let ty = value_as_str(value_get(acc, "type"))
- render3d_st.gltf_comps = 1
- if ty == "VEC2" { render3d_st.gltf_comps = 2 }
- if ty == "VEC3" { render3d_st.gltf_comps = 3 }
- if ty == "VEC4" { render3d_st.gltf_comps = 4 }
- if ty == "MAT2" { render3d_st.gltf_comps = 4 }
- if ty == "MAT3" { render3d_st.gltf_comps = 9 }
- if ty == "MAT4" { render3d_st.gltf_comps = 16 } # a skin's inverse bind matrices
+ gltf_comps = 1
+ if ty == "VEC2" { gltf_comps = 2 }
+ if ty == "VEC3" { gltf_comps = 3 }
+ if ty == "VEC4" { gltf_comps = 4 }
+ if ty == "MAT2" { gltf_comps = 4 }
+ if ty == "MAT3" { gltf_comps = 9 }
+ if ty == "MAT4" { gltf_comps = 16 } # a skin's inverse bind matrices
var csz = 4
- if render3d_st.gltf_ctype == 5123 or render3d_st.gltf_ctype == 5122 { csz = 2 }
- if render3d_st.gltf_ctype == 5121 or render3d_st.gltf_ctype == 5120 { csz = 1 }
- let n = render3d_st.gltf_count * render3d_st.gltf_comps * csz
+ if gltf_ctype == 5123 or gltf_ctype == 5122 { csz = 2 }
+ if gltf_ctype == 5121 or gltf_ctype == 5120 { csz = 1 }
+ let n = gltf_count * gltf_comps * csz
let buf = bytes(n + 8)
- file_seek(render3d_st.gltf_bin, off, 0)
- file_read(render3d_st.gltf_bin, buf, n)
+ file_seek(gltf_bin, off, 0)
+ file_read(gltf_bin, buf, n)
return buf
}
-function gltf_attrib(render3d_st: mut Render3dState, m: Mesh, attrs: Val, name: pointer, loc: int) -> bool {
+function gltf_attrib(m: Mesh, attrs: Val, name: pointer, loc: int) -> bool {
if value_has(attrs, name) == 0 { return false }
- let data = gltf_accessor(render3d_st, value_as_int(value_get(attrs, name)))
- gpu_mesh_vertices(render3d_st, m, data, render3d_st.gltf_count * render3d_st.gltf_comps * 4, GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, loc, render3d_st.gltf_comps, GPU_F32, 0, 0, false)
+ let data = gltf_accessor(value_as_int(value_get(attrs, name)))
+ gpu_mesh_vertices(m, data, gltf_count * gltf_comps * 4, GPU_STATIC)
+ gpu_mesh_attr(m, loc, gltf_comps, GPU_F32, 0, 0, false)
free(data)
return true
}
-function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
+function gltf_prim(p: Val) -> Prim {
let pr = new Prim
- let m = gpu_mesh_new(render3d_st)
+ let m = gpu_mesh_new()
let attrs = value_get(p, "attributes")
- gltf_attrib(render3d_st, m, attrs, "POSITION", 0)
- let nverts = render3d_st.gltf_count
- gltf_attrib(render3d_st, m, attrs, "NORMAL", 1)
- gltf_attrib(render3d_st, m, attrs, "TEXCOORD_0", 2)
- skin_attribs(render3d_st, m, attrs) # JOINTS_0 / WEIGHTS_0 onto 5 / 6, when the mesh has them
- let idx = gltf_accessor(render3d_st, value_as_int(value_get(p, "indices")))
+ gltf_attrib(m, attrs, "POSITION", 0)
+ let nverts = gltf_count
+ gltf_attrib(m, attrs, "NORMAL", 1)
+ gltf_attrib(m, attrs, "TEXCOORD_0", 2)
+ skin_attribs(m, attrs) # JOINTS_0 / WEIGHTS_0 onto 5 / 6, when the mesh has them
+ let idx = gltf_accessor(value_as_int(value_get(p, "indices")))
var isz = 4
- if render3d_st.gltf_ctype == 5123 { isz = 2 }
- gpu_mesh_indices(render3d_st, m, idx, render3d_st.gltf_count * isz, isz)
+ if gltf_ctype == 5123 { isz = 2 }
+ gpu_mesh_indices(m, idx, gltf_count * isz, isz)
free(idx)
- m.count = render3d_st.gltf_count
- gpu_mesh_done(render3d_st, m)
+ m.count = gltf_count
+ gpu_mesh_done(m)
pr.mesh = m
pr.verts = nverts
# material textures
- if render3d_st.gltf_white == 0 { render3d_st.gltf_white = tex_solid(render3d_st, 200, 200, 200, 255); render3d_st.gltf_flat = tex_solid(render3d_st, 128, 128, 255, 255) }
- pr.diff = render3d_st.gltf_white; pr.nrm = render3d_st.gltf_flat; pr.arm = render3d_st.gltf_white
+ if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
+ pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
if value_has(p, "material") != 0 {
- let mat = value_at(value_get(render3d_st.gltf_doc, "materials"), value_as_int(value_get(p, "material")))
- render3d_st.gltf_cutout = value_has(mat, "alphaMode") != 0
- let ud = gltf_mat_uri(render3d_st, mat, "baseColorTexture")
- let un = gltf_mat_uri(render3d_st, mat, "normalTexture")
- let ua = gltf_mat_uri(render3d_st, mat, "metallicRoughnessTexture")
- if ud != null { pr.diff = gltf_texture(render3d_st, ud, true) }
- if un != null { pr.nrm = gltf_texture(render3d_st, un, false) }
- if ua != null { pr.arm = gltf_texture(render3d_st, ua, false) }
+ let mat = value_at(value_get(gltf_doc, "materials"), value_as_int(value_get(p, "material")))
+ gltf_cutout = value_has(mat, "alphaMode") != 0
+ let ud = gltf_mat_uri(mat, "baseColorTexture")
+ let un = gltf_mat_uri(mat, "normalTexture")
+ let ua = gltf_mat_uri(mat, "metallicRoughnessTexture")
+ if ud != null { pr.diff = gltf_texture(ud, true) }
+ if un != null { pr.nrm = gltf_texture(un, false) }
+ if ua != null { pr.arm = gltf_texture(ua, false) }
var mname: string = null
if value_has(mat, "name") != 0 { mname = value_as_str(value_get(mat, "name")) }
pr.name = mname
if mname != null {
- if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) }
+ if ud != null { gltf_mat_remember(mname, pr.diff, pr.nrm, pr.arm) }
else {
- let k = gltf_mat_find(render3d_st, mname)
- if k >= 0 { pr.diff = render3d_st.gltf_mat_diff[k]; pr.nrm = render3d_st.gltf_mat_nrm[k]; pr.arm = render3d_st.gltf_mat_arm[k] }
+ let k = gltf_mat_find(mname)
+ if k >= 0 { pr.diff = gltf_mat_diff[k]; pr.nrm = gltf_mat_nrm[k]; pr.arm = gltf_mat_arm[k] }
else { print(`gltf: material {mname} has no textures and none were loaded before it`) }
}
}
@@ -159,34 +176,34 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
}
# Load the mesh of the node called `node_name` from dir/file.
-function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
- render3d_st.gltf_dir = dir
+function gltf_load(dir: string, file: string, node_name: string) -> Model {
+ gltf_dir = dir
let text = Fs.read_text(dir + "/" + file)
if text == null { print(`gltf: cannot read {dir}/{file}`); return null }
- render3d_st.gltf_doc = Json.parse(text)
- let buffers = value_get(render3d_st.gltf_doc, "buffers")
+ gltf_doc = Json.parse(text)
+ let buffers = value_get(gltf_doc, "buffers")
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
- render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")
- if render3d_st.gltf_bin == null { print(`gltf: cannot open {bin_uri}`); return null }
- let nodes = value_get(render3d_st.gltf_doc, "nodes")
+ gltf_bin = file_open(dir + "/" + bin_uri, "rb")
+ if gltf_bin == null { print(`gltf: cannot open {bin_uri}`); return null }
+ let nodes = value_get(gltf_doc, "nodes")
var mesh_idx = -1
var skin_idx = -1
for i in 0 .. value_count(nodes) {
let nd = value_at(nodes, i)
if value_as_str(value_get(nd, "name")) == node_name { mesh_idx = jint(nd, "mesh", -1); skin_idx = jint(nd, "skin", -1) }
}
- if mesh_idx < 0 { print(`gltf: no node {node_name} in {file}`); file_close(render3d_st.gltf_bin); return null }
+ if mesh_idx < 0 { print(`gltf: no node {node_name} in {file}`); file_close(gltf_bin); return null }
let model = new Model
model.prims = new []Prim
- let mesh = value_at(value_get(render3d_st.gltf_doc, "meshes"), mesh_idx)
+ let mesh = value_at(value_get(gltf_doc, "meshes"), mesh_idx)
let prims = value_get(mesh, "primitives")
var r2 = 0.0; var ymin = 1000.0; var ymax = -1000.0
for i in 0 .. value_count(prims) {
let p = value_at(prims, i)
- push(model.prims, gltf_prim(render3d_st, p))
- model.tris += render3d_st.gltf_count / 3
+ push(model.prims, gltf_prim(p))
+ model.tris += gltf_count / 3
# bounds from the accessor min/max
- let acc = value_at(value_get(render3d_st.gltf_doc, "accessors"), value_as_int(value_get(value_get(p, "attributes"), "POSITION")))
+ let acc = value_at(value_get(gltf_doc, "accessors"), value_as_int(value_get(value_get(p, "attributes"), "POSITION")))
let mn = value_get(acc, "min"); let mx = value_get(acc, "max")
let x0 = Math.abs(jnum(value_at(mn, 0))); let x1 = Math.abs(jnum(value_at(mx, 0)))
let z0 = Math.abs(jnum(value_at(mn, 2))); let z1 = Math.abs(jnum(value_at(mx, 2)))
@@ -197,8 +214,8 @@ function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, no
if y0 < ymin { ymin = y0 }
if y1 > ymax { ymax = y1 }
}
- if skin_idx >= 0 { model.skin = skin_load(render3d_st, skin_idx) }
- file_close(render3d_st.gltf_bin)
+ if skin_idx >= 0 { model.skin = skin_load(skin_idx) }
+ file_close(gltf_bin)
model.radius = Math.sqrt(r2)
model.ymin = ymin
model.height = ymax - ymin
diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic
index 9cdafa01..c1ef8134 100644
--- a/packages/ludic.render3d/gpu.ludic
+++ b/packages/ludic.render3d/gpu.ludic
@@ -26,10 +26,13 @@
const GPU_GL: int = 1
const GPU_VK: int = 2
+var gpu_kind: int = 0 # the backend running; 0 until gpu_select
+var gpu_wanted: int = 0 # what was asked for (setting or R3D_GFX)
+var gpu_fallback_reason: string = null # why the wanted backend is not the one running
# Ask for a backend before r3d_init ("gl", "opengl", "vk", "vulkan"). The environment
# (R3D_GFX) wins over it, so a test or a take can force one whatever the setting says.
-function gpu_request(render3d_st: mut Render3dState, name: string) -> void { render3d_st.gpu_wanted = gpu_kind_of(name) }
+function gpu_request(name: string) -> void { gpu_wanted = gpu_kind_of(name) }
function gpu_kind_of(name: string) -> int {
if name == "vk" or name == "vulkan" { return GPU_VK }
@@ -43,120 +46,134 @@ function gpu_name(kind: int) -> string {
# Settle the backend. Anything that cannot run lands on OpenGL with a reason a
# caller can show the player (gpu_fallback_reason).
-function gpu_select(render3d_st: mut Render3dState) -> int {
- if render3d_st.gpu_kind != 0 { return render3d_st.gpu_kind }
- if r3d_env_has(render3d_st, "R3D_GFX") { render3d_st.gpu_wanted = gpu_kind_of(r3d_env(render3d_st, "R3D_GFX")) }
- 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 {
+function gpu_select() -> int {
+ if gpu_kind != 0 { return gpu_kind }
+ if r3d_env_has("R3D_GFX") { gpu_wanted = gpu_kind_of(r3d_env("R3D_GFX")) }
+ if gpu_wanted == 0 { gpu_wanted = GPU_GL }
+ gpu_kind = GPU_GL
+ if gpu_wanted == GPU_VK {
# a window needs a surface, and only the Win32 one is built
- 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" }
- 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 }
- if render3d_st.gpu_kind == GPU_GL { print(`r3d: vulkan requested: {render3d_st.gpu_fallback_reason}; using opengl`) }
+ gvk_want_surface = is_windowed()
+ if is_windowed() and Os.platform() != "windows" { gpu_fallback_reason = "the Vulkan window is built for Windows only" }
+ else if not gvk_init() { gpu_fallback_reason = gvk_why }
+ else if not gvk_manifest() { gpu_fallback_reason = "the renderer's SPIR-V manifest is missing" }
+ else { gpu_kind = GPU_VK }
+ if gpu_kind == GPU_GL { print(`r3d: vulkan requested: {gpu_fallback_reason}; using opengl`) }
}
- return render3d_st.gpu_kind
+ return gpu_kind
}
-function gpu_backend(render3d_st: Render3dState) -> string { return gpu_name(render3d_st.gpu_kind) }
-function gpu_is_gl(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind != GPU_VK }
+function gpu_backend() -> string { return gpu_name(gpu_kind) }
+function gpu_is_gl() -> bool { return gpu_kind != GPU_VK }
# ---- render state ----------------------------------------------------------------
# -1 = not known yet: the first set always reaches the driver, so the cache never
# assumes a default the context might not have.
+var gpu_s_depth_test: int = -1
+var gpu_s_depth_func: int = -1
+var gpu_s_depth_write: int = -1
+var gpu_s_blend: int = -1
+var gpu_s_blend_src: int = -1
+var gpu_s_blend_dst: int = -1
+var gpu_s_cull: int = -1
+var gpu_s_cull_face: int = -1
+var gpu_s_color_write: int = -1
+var gpu_s_a2c: int = -1
function gpu_b(on: bool) -> int { if on { return 1 }; return 0 }
# Forget the cache: after anything outside the renderer may have changed GL state
# (a context rebuilt, a foreign library drawing into the frame).
-function gpu_state_forget(render3d_st: mut Render3dState) -> void {
- render3d_st.gpu_s_depth_test = -1; render3d_st.gpu_s_depth_func = -1; render3d_st.gpu_s_depth_write = -1
- render3d_st.gpu_s_blend = -1; render3d_st.gpu_s_blend_src = -1; render3d_st.gpu_s_blend_dst = -1
- render3d_st.gpu_s_cull = -1; render3d_st.gpu_s_cull_face = -1; render3d_st.gpu_s_color_write = -1; render3d_st.gpu_s_a2c = -1
+function gpu_state_forget() -> void {
+ gpu_s_depth_test = -1; gpu_s_depth_func = -1; gpu_s_depth_write = -1
+ gpu_s_blend = -1; gpu_s_blend_src = -1; gpu_s_blend_dst = -1
+ gpu_s_cull = -1; gpu_s_cull_face = -1; gpu_s_color_write = -1; gpu_s_a2c = -1
}
-function gpu_gl_cap(render3d_st: Render3dState, cap: int, on: int) -> void { if render3d_st.gpu_kind == GPU_VK { return }; if on == 1 { gl_enable(cap) } else { gl_disable(cap) } }
+function gpu_gl_cap(cap: int, on: int) -> void { if gpu_kind == GPU_VK { return }; if on == 1 { gl_enable(cap) } else { gl_disable(cap) } }
-function gpu_depth_test(render3d_st: mut Render3dState, on: bool) -> void {
+function gpu_depth_test(on: bool) -> void {
let v = gpu_b(on)
- if v == render3d_st.gpu_s_depth_test { return }
- render3d_st.gpu_s_depth_test = v
- gpu_gl_cap(render3d_st, GL_DEPTH_TEST, v)
+ if v == gpu_s_depth_test { return }
+ gpu_s_depth_test = v
+ gpu_gl_cap(GL_DEPTH_TEST, v)
}
# GL_LESS, GL_LEQUAL, GL_EQUAL, GL_ALWAYS, ... (the comparison names are the same in every API)
-function gpu_depth_func(render3d_st: mut Render3dState, f: int) -> void {
- if f == render3d_st.gpu_s_depth_func { return }
- render3d_st.gpu_s_depth_func = f
- if render3d_st.gpu_kind != GPU_VK { gl_depth_func(f) }
+function gpu_depth_func(f: int) -> void {
+ if f == gpu_s_depth_func { return }
+ gpu_s_depth_func = f
+ if gpu_kind != GPU_VK { gl_depth_func(f) }
}
-function gpu_depth_write(render3d_st: mut Render3dState, on: bool) -> void {
+function gpu_depth_write(on: bool) -> void {
let v = gpu_b(on)
- if v == render3d_st.gpu_s_depth_write { return }
- render3d_st.gpu_s_depth_write = v
- if render3d_st.gpu_kind != GPU_VK { gl_depth_mask(v) }
+ if v == gpu_s_depth_write { return }
+ gpu_s_depth_write = v
+ if gpu_kind != GPU_VK { gl_depth_mask(v) }
}
-function gpu_blend(render3d_st: mut Render3dState, on: bool) -> void {
+function gpu_blend(on: bool) -> void {
let v = gpu_b(on)
- if v == render3d_st.gpu_s_blend { return }
- render3d_st.gpu_s_blend = v
- gpu_gl_cap(render3d_st, GL_BLEND, v)
+ if v == gpu_s_blend { return }
+ gpu_s_blend = v
+ gpu_gl_cap(GL_BLEND, v)
}
-function gpu_blend_func(render3d_st: mut Render3dState, src: int, dst: int) -> void {
- if src == render3d_st.gpu_s_blend_src and dst == render3d_st.gpu_s_blend_dst { return }
- render3d_st.gpu_s_blend_src = src; render3d_st.gpu_s_blend_dst = dst
- if render3d_st.gpu_kind != GPU_VK { gl_blend_func(src, dst) }
+function gpu_blend_func(src: int, dst: int) -> void {
+ if src == gpu_s_blend_src and dst == gpu_s_blend_dst { return }
+ gpu_s_blend_src = src; gpu_s_blend_dst = dst
+ if gpu_kind != GPU_VK { gl_blend_func(src, dst) }
}
-function gpu_cull(render3d_st: mut Render3dState, on: bool) -> void {
+function gpu_cull(on: bool) -> void {
let v = gpu_b(on)
- if v == render3d_st.gpu_s_cull { return }
- render3d_st.gpu_s_cull = v
- gpu_gl_cap(render3d_st, GL_CULL_FACE, v)
+ if v == gpu_s_cull { return }
+ gpu_s_cull = v
+ gpu_gl_cap(GL_CULL_FACE, v)
}
# GL_BACK or GL_FRONT
-function gpu_cull_face(render3d_st: mut Render3dState, face: int) -> void {
- if face == render3d_st.gpu_s_cull_face { return }
- render3d_st.gpu_s_cull_face = face
- if render3d_st.gpu_kind != GPU_VK { gl_cull_face(face) }
+function gpu_cull_face(face: int) -> void {
+ if face == gpu_s_cull_face { return }
+ gpu_s_cull_face = face
+ if gpu_kind != GPU_VK { gl_cull_face(face) }
}
# all four channels together: nothing in the renderer writes a partial mask
-function gpu_color_write(render3d_st: mut Render3dState, on: bool) -> void {
+function gpu_color_write(on: bool) -> void {
let v = gpu_b(on)
- if v == render3d_st.gpu_s_color_write { return }
- render3d_st.gpu_s_color_write = v
- if render3d_st.gpu_kind != GPU_VK { gl_color_mask(v, v, v, v) }
+ if v == gpu_s_color_write { return }
+ gpu_s_color_write = v
+ if gpu_kind != GPU_VK { gl_color_mask(v, v, v, v) }
}
-function gpu_alpha_to_coverage(render3d_st: mut Render3dState, on: bool) -> void {
+function gpu_alpha_to_coverage(on: bool) -> void {
let v = gpu_b(on)
- if v == render3d_st.gpu_s_a2c { return }
- render3d_st.gpu_s_a2c = v
- gpu_gl_cap(render3d_st, GL_SAMPLE_ALPHA_TO_COVERAGE, v)
+ if v == gpu_s_a2c { return }
+ gpu_s_a2c = v
+ gpu_gl_cap(GL_SAMPLE_ALPHA_TO_COVERAGE, v)
}
# Depth bias for the shadow casters; (0, 0) turns it off. factor/units are fixed, as
# gl_polygon_offset takes them. A pipeline API bakes the bias into the pipeline.
-function gpu_depth_bias(render3d_st: mut Render3dState, factor: fixed, units: fixed) -> void {
+var gpu_s_bias: int = -1
+var gpu_s_bias_f: float = 0.0 # float bits, for a backend that bakes the bias into a pipeline
+var gpu_s_bias_u: float = 0.0
+function gpu_depth_bias(factor: fixed, units: fixed) -> void {
var v = 1
if factor == 0.0 and units == 0.0 { v = 0 }
- if v != render3d_st.gpu_s_bias { render3d_st.gpu_s_bias = v; gpu_gl_cap(render3d_st, GL_POLYGON_OFFSET_FILL, v) }
- render3d_st.gpu_s_bias_f = float(factor); render3d_st.gpu_s_bias_u = float(units)
- if v == 1 and render3d_st.gpu_kind != GPU_VK { gl_polygon_offset(factor, units) }
+ if v != gpu_s_bias { gpu_s_bias = v; gpu_gl_cap(GL_POLYGON_OFFSET_FILL, v) }
+ gpu_s_bias_f = float(factor); gpu_s_bias_u = float(units)
+ if v == 1 and gpu_kind != GPU_VK { gl_polygon_offset(factor, units) }
}
# A scissor rectangle in top-down pixels (x, y from the top-left of the drawable), or
# off. Every API but OpenGL counts rows from the top; the GL backend flips it.
-function gpu_scissor(render3d_st: mut Render3dState, x: int, y_top: int, w: int, h: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { render3d_st.gpu_s_scissor = 1; gvk_scissor(render3d_st, x, gl_h - y_top - h, w, h); return }
- if render3d_st.gpu_s_scissor != 1 { render3d_st.gpu_s_scissor = 1; gl_enable(GL_SCISSOR_TEST) }
+var gpu_s_scissor: int = -1
+function gpu_scissor(x: int, y_top: int, w: int, h: int) -> void {
+ if gpu_kind == GPU_VK { gpu_s_scissor = 1; gvk_scissor(x, gl_h - y_top - h, w, h); return }
+ if gpu_s_scissor != 1 { gpu_s_scissor = 1; gl_enable(GL_SCISSOR_TEST) }
gl_scissor(x, gl_h - y_top - h, w, h)
- gpu_glcheck_after(render3d_st, "scissor")
+ gpu_glcheck_after("scissor")
}
-function gpu_scissor_off(render3d_st: mut Render3dState) -> void {
- if render3d_st.gpu_kind == GPU_VK { render3d_st.gpu_s_scissor = 0; gvk_scissor_off(render3d_st); return }
- if render3d_st.gpu_s_scissor == 0 { return }
- render3d_st.gpu_s_scissor = 0
+function gpu_scissor_off() -> void {
+ if gpu_kind == GPU_VK { gpu_s_scissor = 0; gvk_scissor_off(); return }
+ if gpu_s_scissor == 0 { return }
+ gpu_s_scissor = 0
gl_disable(GL_SCISSOR_TEST)
- gpu_glcheck_after(render3d_st, "scissor off")
+ gpu_glcheck_after("scissor off")
}
# ---- uniforms --------------------------------------------------------------------
@@ -164,44 +181,47 @@ function gpu_scissor_off(render3d_st: mut Render3dState) -> void {
# the handle is the location. On a pipeline API it will name a slot in the program's
# uniform block, so the u_* setters below are the only code that knows which. Arrays
# are looked up by their first element ("u_bones[0]"), as every GL driver accepts.
-function gpu_uniform(render3d_st: Render3dState, prog: int, name: string) -> int { if render3d_st.gpu_kind == GPU_VK { return gvk_uniform(render3d_st, prog, name) }; return gl_get_uniform_location(prog, name) }
+function gpu_uniform(prog: int, name: string) -> int { if gpu_kind == GPU_VK { return gvk_uniform(prog, name) }; return gl_get_uniform_location(prog, name) }
# ---- programs ----------------------------------------------------------------------
# A program remembers the variant it was built from - vertex file, fragment file and the
# defines on one line, the key the SPIR-V manifest uses - which is how a backend that cannot
# compile shaders at run time finds its pipeline for the same handle.
-function gpu_program(render3d_st: mut Render3dState, vs_src: string, fs_src: string, vs: string, fs: string, defines: string) -> int {
- if render3d_st.gpu_kind == GPU_VK { return gvk_program_new(render3d_st, vs, fs, defines) }
+var gpu_prog_ids: []int = null
+var gpu_prog_keys: []string = null
+var gpu_prog_cur: int = 0
+function gpu_program(vs_src: string, fs_src: string, vs: string, fs: string, defines: string) -> int {
+ if gpu_kind == GPU_VK { return gvk_program_new(vs, fs, defines) }
let p = gl_program(vs_src, fs_src)
if p == 0 { return 0 }
- if render3d_st.gpu_prog_ids == null { render3d_st.gpu_prog_ids = new []int; render3d_st.gpu_prog_keys = new []string }
- push(render3d_st.gpu_prog_ids, p)
- push(render3d_st.gpu_prog_keys, `{vs}|{fs}|{Text.replace(defines, "\n", ";")}`)
+ if gpu_prog_ids == null { gpu_prog_ids = new []int; gpu_prog_keys = new []string }
+ push(gpu_prog_ids, p)
+ push(gpu_prog_keys, `{vs}|{fs}|{Text.replace(defines, "\n", ";")}`)
return p
}
# the manifest key a program was built from; "" for one this layer did not build
-function gpu_program_key(render3d_st: Render3dState, p: int) -> string {
- if render3d_st.gpu_prog_ids == null { return "" }
- for i in 0 .. len(render3d_st.gpu_prog_ids) { if render3d_st.gpu_prog_ids[i] == p { return render3d_st.gpu_prog_keys[i] } }
+function gpu_program_key(p: int) -> string {
+ if gpu_prog_ids == null { return "" }
+ for i in 0 .. len(gpu_prog_ids) { if gpu_prog_ids[i] == p { return gpu_prog_keys[i] } }
return ""
}
-function gpu_use_program(render3d_st: mut Render3dState, p: int) -> void { ds_program_change(render3d_st, render3d_st.gpu_prog_cur, p); if render3d_st.gpu_kind == GPU_VK { render3d_st.gpu_prog_cur = p; return }; gl_use_program(p); render3d_st.gpu_prog_cur = p; gpu_glcheck_after(render3d_st, "use program") }
-function gpu_program_free(render3d_st: mut Render3dState, p: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { return }
+function gpu_use_program(p: int) -> void { ds_program_change(gpu_prog_cur, p); if gpu_kind == GPU_VK { gpu_prog_cur = p; return }; gl_use_program(p); gpu_prog_cur = p; gpu_glcheck_after("use program") }
+function gpu_program_free(p: int) -> void {
+ if gpu_kind == GPU_VK { return }
if p == 0 { return }
gl_delete_program(p)
- if render3d_st.gpu_prog_cur == p { render3d_st.gpu_prog_cur = 0 }
- if render3d_st.gpu_prog_ids != null { for i in 0 .. len(render3d_st.gpu_prog_ids) { if render3d_st.gpu_prog_ids[i] == p { render3d_st.gpu_prog_ids[i] = 0; render3d_st.gpu_prog_keys[i] = "" } } }
- gpu_glcheck_after(render3d_st, "program free")
+ if gpu_prog_cur == p { gpu_prog_cur = 0 }
+ if gpu_prog_ids != null { for i in 0 .. len(gpu_prog_ids) { if gpu_prog_ids[i] == p { gpu_prog_ids[i] = 0; gpu_prog_keys[i] = "" } } }
+ gpu_glcheck_after("program free")
}
# ---- GPU timers (R3D_PROF) ----------------------------------------------------------
-function gpu_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_new(render3d_st, n, ids); return }; gl_gen_queries(n, ids) }
-function gpu_query_begin(render3d_st: mut Render3dState, id: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_begin(render3d_st, id); return }; gl_begin_query(GL_TIME_ELAPSED, id) }
-function gpu_query_end(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_query_end(render3d_st); return }; gl_end_query(GL_TIME_ELAPSED) }
+function gpu_query_new(n: int, ids: words) -> void { if gpu_kind == GPU_VK { gvk_query_new(n, ids); return }; gl_gen_queries(n, ids) }
+function gpu_query_begin(id: int) -> void { if gpu_kind == GPU_VK { gvk_query_begin(id); return }; gl_begin_query(GL_TIME_ELAPSED, id) }
+function gpu_query_end() -> void { if gpu_kind == GPU_VK { gvk_query_end(); return }; gl_end_query(GL_TIME_ELAPSED) }
# true once the query has its result; the nanoseconds (low 32 bits) are then in out[0]
-function gpu_query_result(render3d_st: Render3dState, id: int, out: words) -> bool {
- if render3d_st.gpu_kind == GPU_VK { return gvk_query_result(render3d_st, id, out) }
+function gpu_query_result(id: int, out: words) -> bool {
+ if gpu_kind == GPU_VK { return gvk_query_result(id, out) }
gl_get_query_objectiv(id, GL_QUERY_RESULT_AVAILABLE, out)
if out[0] == 0 { return false }
gl_get_query_objectui64v(id, GL_QUERY_RESULT, out)
@@ -209,30 +229,31 @@ function gpu_query_result(render3d_st: Render3dState, id: int, out: words) -> bo
}
# ---- the context --------------------------------------------------------------------
-function gpu_open(render3d_st: mut Render3dState, w: int, h: int, title: string) -> bool { if render3d_st.gpu_kind == GPU_VK { return gvk_open(render3d_st, w, h, title) }; return gl_open(w, h, title) }
-function gpu_vsync(render3d_st: mut Render3dState, on: int) -> void { if render3d_st.gpu_kind == GPU_VK { render3d_st.gvk_vsync = on != 0; if render3d_st.gvk_swap != 0 { render3d_st.gvk_swap_stale = true }; return }; gl_vsync(on) }
-function gpu_renderer_name(render3d_st: Render3dState) -> string { if render3d_st.gpu_kind == GPU_VK { return `{render3d_st.gvk_device_name} (Vulkan)` }; return gl_get_string(GL_RENDERER) }
-function gpu_resize_check(render3d_st: mut Render3dState) -> bool { if render3d_st.gpu_kind == GPU_VK { return gvk_resize_check(render3d_st) }; let r = gl_resize_check(); gpu_glcheck_after(render3d_st, "the resize check"); return r }
+function gpu_open(w: int, h: int, title: string) -> bool { if gpu_kind == GPU_VK { return gvk_open(w, h, title) }; return gl_open(w, h, title) }
+function gpu_vsync(on: int) -> void { if gpu_kind == GPU_VK { gvk_vsync = on != 0; if gvk_swap != 0 { gvk_swap_stale = true }; return }; gl_vsync(on) }
+function gpu_renderer_name() -> string { if gpu_kind == GPU_VK { return `{gvk_device_name} (Vulkan)` }; return gl_get_string(GL_RENDERER) }
+function gpu_resize_check() -> bool { if gpu_kind == GPU_VK { return gvk_resize_check() }; let r = gl_resize_check(); gpu_glcheck_after("the resize check"); return r }
# the finished frame: presented to the window, or (headless) the GPU's work finished
-function gpu_present(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_present(render3d_st); return }; Gl.swap() }
+function gpu_present() -> void { if gpu_kind == GPU_VK { gvk_present(); return }; Gl.swap() }
# the frame as it will be presented, to a binary PPM with the top row first; before gpu_present
-function gpu_screenshot(render3d_st: mut Render3dState, path: string) -> bool { if render3d_st.gpu_kind == GPU_VK { return gvk_screenshot(render3d_st, path) }; return Gl.screenshot(path: path) }
+function gpu_screenshot(path: string) -> bool { if gpu_kind == GPU_VK { return gvk_screenshot(path) }; return Gl.screenshot(path: path) }
-function gpu_tmp(render3d_st: mut Render3dState) -> words { if render3d_st.gpu_u_tmp == null { render3d_st.gpu_u_tmp = words(4) }; return render3d_st.gpu_u_tmp }
+var gpu_u_tmp: words = null
+function gpu_tmp() -> words { if gpu_u_tmp == null { gpu_u_tmp = words(4) }; return gpu_u_tmp }
# float bits (IEEE singles in an int), like every other number in the renderer
-function u_f(render3d_st: mut Render3dState, loc: int, v: float) -> void { if render3d_st.gpu_kind == GPU_VK { let t = gpu_tmp(render3d_st); t[0] = float_bits(v); gvk_u_set(render3d_st, loc, data_of(t), 4, 1); return }; let t = gpu_tmp(render3d_st); t[0] = float_bits(v); gl_uniform1fv(loc, 1, t) }
-function u_f2(render3d_st: mut Render3dState, loc: int, x: float, y: float) -> void { if render3d_st.gpu_kind == GPU_VK { let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); gvk_u_set(render3d_st, loc, data_of(t), 8, 1); return }; let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); gl_uniform2fv(loc, 1, t) }
-function u_f3(render3d_st: mut Render3dState, loc: int, x: float, y: float, z: float) -> void { if render3d_st.gpu_kind == GPU_VK { let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gvk_u_set(render3d_st, loc, data_of(t), 12, 1); return }; let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gl_uniform3fv(loc, 1, t) }
-function u_f4(render3d_st: mut Render3dState, loc: int, x: float, y: float, z: float, w: float) -> void { if render3d_st.gpu_kind == GPU_VK { let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gvk_u_set(render3d_st, loc, data_of(t), 16, 1); return }; let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gl_uniform4fv(loc, 1, t) }
-function u_v3(render3d_st: mut Render3dState, loc: int, v: floats) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_u_set(render3d_st, loc, data_of(v), 12, 1); return }; gl_uniform3fv(loc, 1, v) }
-function u_fv(render3d_st: mut Render3dState, loc: int, n: int, v: floats) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_u_set(render3d_st, loc, data_of(v), 4, n); return }; gl_uniform1fv(loc, n, v) }
+function u_f(loc: int, v: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(v); gvk_u_set(loc, data_of(t), 4, 1); return }; let t = gpu_tmp(); t[0] = float_bits(v); gl_uniform1fv(loc, 1, t) }
+function u_f2(loc: int, x: float, y: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gvk_u_set(loc, data_of(t), 8, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gl_uniform2fv(loc, 1, t) }
+function u_f3(loc: int, x: float, y: float, z: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gvk_u_set(loc, data_of(t), 12, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gl_uniform3fv(loc, 1, t) }
+function u_f4(loc: int, x: float, y: float, z: float, w: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gvk_u_set(loc, data_of(t), 16, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gl_uniform4fv(loc, 1, t) }
+function u_v3(loc: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(v), 12, 1); return }; gl_uniform3fv(loc, 1, v) }
+function u_fv(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(v), 4, n); return }; gl_uniform1fv(loc, n, v) }
# n vec4s from 4n float bits. Not u_fv with 4n: on Vulkan an array element is copied at the size
# given and placed at the array's stride, so a vec4 array fed floats got one float per element -
# which drew the chunked grass with every tile at a nonsense corner and zero blades a cell.
-function u_f4v(render3d_st: mut Render3dState, loc: int, n: int, v: floats) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_u_set(render3d_st, loc, data_of(v), 16, n); return }; gl_uniform4fv(loc, n, v) }
-function u_mat4(render3d_st: mut Render3dState, loc: int, m: floats) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_u_set(render3d_st, loc, data_of(m), 64, 1); return }; gl_uniform_matrix4fv(loc, 1, 0, m) }
-function u_mat4n(render3d_st: mut Render3dState, loc: int, n: int, m: floats) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_u_set(render3d_st, loc, data_of(m), 64, n); return }; gl_uniform_matrix4fv(loc, n, 0, m) }
-function u_i(render3d_st: mut Render3dState, loc: int, v: int) -> void { if render3d_st.gpu_kind == GPU_VK { let t = gpu_tmp(render3d_st); t[0] = v; gvk_u_set(render3d_st, loc, data_of(t), 4, 1); return }; gl_uniform1i(loc, v) }
+function u_f4v(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(v), 16, n); return }; gl_uniform4fv(loc, n, v) }
+function u_mat4(loc: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(m), 64, 1); return }; gl_uniform_matrix4fv(loc, 1, 0, m) }
+function u_mat4n(loc: int, n: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, data_of(m), 64, n); return }; gl_uniform_matrix4fv(loc, n, 0, m) }
+function u_i(loc: int, v: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = v; gvk_u_set(loc, data_of(t), 4, 1); return }; gl_uniform1i(loc, v) }
# ---- what this machine can do ----------------------------------------------------
# The advanced graphics features are Windows features: the Vulkan renderer, ray tracing,
@@ -253,6 +274,17 @@ const GF_HDR: int = 9
const GF_MESH_GRASS: int = 10
const GF_COUNT: int = 11
+var gpu_cap_probed: bool = false
+var gpu_cap_windows: bool = false # the advanced features exist on this platform at all
+var gpu_cap_vulkan: bool = false # a Vulkan loader, an instance and a device
+var gpu_cap_floor: bool = false # Vulkan 1.3 with everything the renderer's floor needs
+var gpu_cap_rt: bool = false # ray query + acceleration structures
+var gpu_cap_mesh: bool = false # VK_EXT_mesh_shader
+var gpu_cap_nvidia: bool = false
+var gpu_cap_rtx: int = 0 # the RTX generation (20, 30, 40, 50); 0 = not RTX
+var gpu_cap_reflex: bool = false # VK_NV_low_latency2
+var gpu_cap_hdr: bool = false # the instance offers HDR colour spaces
+var gpu_cap_device: string = ""
# 20 for "NVIDIA GeForce RTX 2080", 50 for "RTX 5090"; 30 for a workstation "RTX A4000"
function gpu_rtx_generation(name: string) -> int {
@@ -273,22 +305,22 @@ function gpu_ext_in(props: bytes, n: int, want: string) -> bool {
# R3D_CAPS=rtx50|rtx40|rtx30|amd|intel|none pretends to be a Windows machine with that GPU,
# so the settings screen can be shot and tested anywhere
-function gpu_caps_fake(render3d_st: mut Render3dState, kind: string) -> void {
- render3d_st.gpu_cap_windows = true
+function gpu_caps_fake(kind: string) -> void {
+ gpu_cap_windows = true
if kind == "none" { return }
- render3d_st.gpu_cap_vulkan = true; render3d_st.gpu_cap_floor = true; render3d_st.gpu_cap_hdr = true
- if kind == "amd" or kind == "intel" { render3d_st.gpu_cap_rt = true; render3d_st.gpu_cap_mesh = true; render3d_st.gpu_cap_device = `test {kind} GPU`; return }
- render3d_st.gpu_cap_nvidia = true; render3d_st.gpu_cap_rt = true; render3d_st.gpu_cap_mesh = true; render3d_st.gpu_cap_reflex = true
- render3d_st.gpu_cap_rtx = gpu_rtx_generation(`RTX {kind[3 .. 5]}`)
- render3d_st.gpu_cap_device = `test NVIDIA GeForce RTX {kind[3 .. 5]}`
+ gpu_cap_vulkan = true; gpu_cap_floor = true; gpu_cap_hdr = true
+ if kind == "amd" or kind == "intel" { gpu_cap_rt = true; gpu_cap_mesh = true; gpu_cap_device = `test {kind} GPU`; return }
+ gpu_cap_nvidia = true; gpu_cap_rt = true; gpu_cap_mesh = true; gpu_cap_reflex = true
+ gpu_cap_rtx = gpu_rtx_generation(`RTX {kind[3 .. 5]}`)
+ gpu_cap_device = `test NVIDIA GeForce RTX {kind[3 .. 5]}`
}
-function gpu_caps_probe(render3d_st: mut Render3dState) -> void {
- if render3d_st.gpu_cap_probed { return }
- render3d_st.gpu_cap_probed = true
- if r3d_env_has(render3d_st, "R3D_CAPS") { gpu_caps_fake(render3d_st, r3d_env(render3d_st, "R3D_CAPS")); return }
- render3d_st.gpu_cap_windows = Os.platform() == "windows"
- if not render3d_st.gpu_cap_windows { return }
+function gpu_caps_probe() -> void {
+ if gpu_cap_probed { return }
+ gpu_cap_probed = true
+ if r3d_env_has("R3D_CAPS") { gpu_caps_fake(r3d_env("R3D_CAPS")); return }
+ gpu_cap_windows = Os.platform() == "windows"
+ if not gpu_cap_windows { return }
if Vk.open() == 0 { return }
let cnt = bytes(4)
Vk.put_i32(cnt, 0, 0)
@@ -296,7 +328,7 @@ function gpu_caps_probe(render3d_st: mut Render3dState) -> void {
let nie = Vk.get_i32(cnt, 0)
let iexts = bytes(nie * VkExtensionProperties_sizeof + 8)
Vk.enumerate_instance_extension_properties(null, cnt, iexts)
- render3d_st.gpu_cap_hdr = gpu_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)
+ gpu_cap_hdr = gpu_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)
let app = bytes(VkApplicationInfo_sizeof)
Vk.zero(app, VkApplicationInfo_sizeof)
Vk.put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO)
@@ -309,8 +341,8 @@ function gpu_caps_probe(render3d_st: mut Render3dState) -> void {
# interposer, and a second instance made and destroyed under it is what this avoids.
var inst: pointer = null
var own = false
- if render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_ready {
- inst = render3d_st.gvk_inst
+ if gpu_kind == GPU_VK and gvk_ready {
+ inst = gvk_inst
} else {
let out = bytes(8)
if Vk.create_instance(ici, null, out) != VK_SUCCESS { return }
@@ -332,11 +364,11 @@ function gpu_caps_probe(render3d_st: mut Render3dState) -> void {
if pick < 0 and nd > 0 { pick = 0 }
if pick >= 0 {
let pd = Vk.get_ptr(devs, pick * 8)
- render3d_st.gpu_cap_vulkan = true
+ gpu_cap_vulkan = true
Vk.get_physical_device_properties(pd, props)
- render3d_st.gpu_cap_device = string(Vk.at(props, VkPhysicalDeviceProperties_deviceName))
- render3d_st.gpu_cap_nvidia = Vk.get_i32(props, VkPhysicalDeviceProperties_vendorID) == 4318
- if render3d_st.gpu_cap_nvidia { render3d_st.gpu_cap_rtx = gpu_rtx_generation(render3d_st.gpu_cap_device) }
+ gpu_cap_device = string(Vk.at(props, VkPhysicalDeviceProperties_deviceName))
+ gpu_cap_nvidia = Vk.get_i32(props, VkPhysicalDeviceProperties_vendorID) == 4318
+ if gpu_cap_nvidia { gpu_cap_rtx = gpu_rtx_generation(gpu_cap_device) }
let api = Vk.get_i32(props, VkPhysicalDeviceProperties_apiVersion)
let f13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof)
Vk.zero(f13, VkPhysicalDeviceVulkan13Features_sizeof)
@@ -352,18 +384,18 @@ function gpu_caps_probe(render3d_st: mut Render3dState) -> void {
Vk.get_physical_device_features2(pd, f2)
let major = (api >> 22) & 127
let minor = (api >> 12) & 1023
- render3d_st.gpu_cap_floor = (major > 1 or (major == 1 and minor >= 3)) and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_dynamicRendering) == 1 and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_synchronization2) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_bufferDeviceAddress) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) == 1
+ gpu_cap_floor = (major > 1 or (major == 1 and minor >= 3)) and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_dynamicRendering) == 1 and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_synchronization2) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_bufferDeviceAddress) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) == 1
Vk.put_i32(cnt, 0, 0)
Vk.enumerate_device_extension_properties(pd, null, cnt, null)
let ne = Vk.get_i32(cnt, 0)
let dexts = bytes(ne * VkExtensionProperties_sizeof + 8)
Vk.enumerate_device_extension_properties(pd, null, cnt, dexts)
- render3d_st.gpu_cap_rt = gpu_ext_in(dexts, ne, VK_KHR_RAY_QUERY_EXTENSION_NAME) and gpu_ext_in(dexts, ne, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME)
- render3d_st.gpu_cap_mesh = gpu_ext_in(dexts, ne, VK_EXT_MESH_SHADER_EXTENSION_NAME)
- render3d_st.gpu_cap_reflex = gpu_ext_in(dexts, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME)
+ gpu_cap_rt = gpu_ext_in(dexts, ne, VK_KHR_RAY_QUERY_EXTENSION_NAME) and gpu_ext_in(dexts, ne, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME)
+ gpu_cap_mesh = gpu_ext_in(dexts, ne, VK_EXT_MESH_SHADER_EXTENSION_NAME)
+ gpu_cap_reflex = gpu_ext_in(dexts, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME)
}
if own { Vk.destroy_instance(inst, null) }
- print(`r3d: gpu caps: {render3d_st.gpu_cap_device} vulkan {render3d_st.gpu_cap_vulkan} floor {render3d_st.gpu_cap_floor} rt {render3d_st.gpu_cap_rt} mesh {render3d_st.gpu_cap_mesh} rtx {render3d_st.gpu_cap_rtx} reflex {render3d_st.gpu_cap_reflex} hdr {render3d_st.gpu_cap_hdr}`)
+ print(`r3d: gpu caps: {gpu_cap_device} vulkan {gpu_cap_vulkan} floor {gpu_cap_floor} rt {gpu_cap_rt} mesh {gpu_cap_mesh} rtx {gpu_cap_rtx} reflex {gpu_cap_reflex} hdr {gpu_cap_hdr}`)
}
# Whether the renderer actually draws a feature yet. Until a feature lands, choosing it is saved and
@@ -406,18 +438,18 @@ function gpu_gl_usage(u: int) -> int {
}
# a new mesh, its vertex array bound: the vertex, attribute and index calls below describe it
-function gpu_mesh_new(render3d_st: Render3dState) -> Mesh {
+function gpu_mesh_new() -> Mesh {
let m = new Mesh
m.attrs = words(GPU_MAX_ATTRS * GPU_ATTR_W)
for i in 0 .. GPU_MAX_ATTRS * GPU_ATTR_W { m.attrs[i] = 0 }
m.vbufs = words(GPU_MAX_VBUFS)
- if render3d_st.gpu_kind != GPU_VK { m.vao = gl_vao() }
+ if gpu_kind != GPU_VK { m.vao = gl_vao() }
return m
}
# a vertex buffer for the mesh being built (data may be null: storage only); returns it
-function gpu_mesh_vertices(render3d_st: mut Render3dState, m: Mesh, data: pointer, nbytes: int, usage: int) -> int {
+function gpu_mesh_vertices(m: Mesh, data: pointer, nbytes: int, usage: int) -> int {
var b = 0
- if render3d_st.gpu_kind == GPU_VK { b = gvk_buf_new(render3d_st); gvk_buf_upload(render3d_st, b, nbytes, data) }
+ if gpu_kind == GPU_VK { b = gvk_buf_new(); gvk_buf_upload(b, nbytes, data) }
else {
b = gl_buffer()
gl_bind_buffer(GL_ARRAY_BUFFER, b)
@@ -440,38 +472,38 @@ function gpu_mesh_record(m: Mesh, index: int, comps: int, type: int, stride: int
if index + 1 > m.n_attrs { m.n_attrs = index + 1 }
}
# attribute `index` read from the last vertex buffer (stride 0: tightly packed)
-function gpu_mesh_attr(render3d_st: Render3dState, m: Mesh, index: int, comps: int, type: int, stride: int, offset: int, normalized: bool) -> void {
- if render3d_st.gpu_kind != GPU_VK {
+function gpu_mesh_attr(m: Mesh, index: int, comps: int, type: int, stride: int, offset: int, normalized: bool) -> void {
+ if gpu_kind != GPU_VK {
gl_enable_vertex_attrib_array(index)
gl_vertex_attrib_pointer(index, comps, gpu_gl_type(type), gpu_b(normalized), stride, gl_ptr(null, offset))
}
gpu_mesh_record(m, index, comps, type, stride, offset, normalized, false)
}
# the index buffer: 4-byte or 2-byte indices
-function gpu_mesh_indices(render3d_st: mut Render3dState, m: Mesh, data: pointer, nbytes: int, index_bytes: int) -> void {
+function gpu_mesh_indices(m: Mesh, data: pointer, nbytes: int, index_bytes: int) -> void {
m.itype = GL_UNSIGNED_INT
if index_bytes == 2 { m.itype = GL_UNSIGNED_SHORT }
- if render3d_st.gpu_kind == GPU_VK { m.ebo = gvk_buf_new(render3d_st); gvk_buf_upload(render3d_st, m.ebo, nbytes, data); return }
+ if gpu_kind == GPU_VK { m.ebo = gvk_buf_new(); gvk_buf_upload(m.ebo, nbytes, data); return }
m.ebo = gl_buffer()
gl_bind_buffer(GL_ELEMENT_ARRAY_BUFFER, m.ebo)
gl_buffer_data(GL_ELEMENT_ARRAY_BUFFER, nbytes, data, GL_STATIC_DRAW)
}
# finished describing: nothing else is bound to it by accident
-function gpu_mesh_done(render3d_st: Render3dState, m: Mesh) -> void { if render3d_st.gpu_kind == GPU_VK { return }; gl_bind_vertex_array(0) }
+function gpu_mesh_done(m: Mesh) -> void { if gpu_kind == GPU_VK { return }; gl_bind_vertex_array(0) }
# Per-instance data: `buf` feeds the attributes named next, one element per instance. A mesh
# drawn from different instance buffers (the scatter layers' LOD buckets) is re-pointed here
# before each draw; on Vulkan that is a vertex-buffer binding, not a change of layout.
-function gpu_mesh_bind_instances(render3d_st: Render3dState, m: Mesh, buf: int) -> void {
- if render3d_st.gpu_kind != GPU_VK {
+function gpu_mesh_bind_instances(m: Mesh, buf: int) -> void {
+ if gpu_kind != GPU_VK {
gl_bind_vertex_array(m.vao)
gl_bind_buffer(GL_ARRAY_BUFFER, buf)
}
m.cur_buf = buf
m.ibuf = buf
}
-function gpu_mesh_attr_inst(render3d_st: Render3dState, m: Mesh, index: int, comps: int, type: int, stride: int, offset: int) -> void {
- if render3d_st.gpu_kind != GPU_VK {
+function gpu_mesh_attr_inst(m: Mesh, index: int, comps: int, type: int, stride: int, offset: int) -> void {
+ if gpu_kind != GPU_VK {
gl_enable_vertex_attrib_array(index)
gl_vertex_attrib_pointer(index, comps, gpu_gl_type(type), 0, stride, gl_ptr(null, offset))
gl_vertex_attrib_divisor(index, 1)
@@ -480,16 +512,16 @@ function gpu_mesh_attr_inst(render3d_st: Render3dState, m: Mesh, index: int, com
}
# a buffer on its own (instances, a stream): made, filled whole, freed
-function gpu_buffer_new(render3d_st: mut Render3dState) -> int { if render3d_st.gpu_kind == GPU_VK { return gvk_buf_new(render3d_st) }; return gl_buffer() }
-function gpu_buffer_upload(render3d_st: mut Render3dState, buf: int, nbytes: int, data: pointer, usage: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_buf_upload(render3d_st, buf, nbytes, data); return }
+function gpu_buffer_new() -> int { if gpu_kind == GPU_VK { return gvk_buf_new() }; return gl_buffer() }
+function gpu_buffer_upload(buf: int, nbytes: int, data: pointer, usage: int) -> void {
+ if gpu_kind == GPU_VK { gvk_buf_upload(buf, nbytes, data); return }
gl_bind_buffer(GL_ARRAY_BUFFER, buf)
gl_buffer_data(GL_ARRAY_BUFFER, nbytes, data, gpu_gl_usage(usage))
}
-function gpu_buffer_free(render3d_st: mut Render3dState, buf: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { if buf > 0 { gvk_buf_release(render3d_st, buf) }; return }
+function gpu_buffer_free(buf: int) -> void {
+ if gpu_kind == GPU_VK { if buf > 0 { gvk_buf_release(buf) }; return }
if buf == 0 { return }
- let ids = gpu_tmp(render3d_st)
+ let ids = gpu_tmp()
ids[0] = buf
gl_delete_buffers(1, ids)
}
@@ -499,77 +531,77 @@ function gpu_buffer_free(render3d_st: mut Render3dState, buf: int) -> void {
# the draws then read. OpenGL here is 4.1 (macOS) with no compute, so gpu_compute is 0 there and
# a caller keeps its CPU path. A compute program's binding 0 is its parameter block (params,
# copied at the dispatch); bindings 1.. are `bufs`, gpu buffers.
-function gpu_has_compute(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK }
+function gpu_has_compute() -> bool { return gpu_kind == GPU_VK }
# several records in one indirect draw, each with its own firstInstance
-function gpu_has_mdi(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_has_mdi }
+function gpu_has_mdi() -> bool { return gpu_kind == GPU_VK and gvk_has_mdi }
# mesh shaders (VK_EXT_mesh_shader): a *.mesh program drawn with gpu_draw_mesh_tasks
-function gpu_has_mesh(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_has_mesh }
-function gpu_draw_mesh_tasks(render3d_st: mut Render3dState, x: int, y: int, z: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_draw_mesh_tasks_now(render3d_st, x, y, z) } }
-function gpu_compute(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int {
- if render3d_st.gpu_kind != GPU_VK { return 0 }
- return gvk_compute_new(render3d_st, name, n_bufs)
+function gpu_has_mesh() -> bool { return gpu_kind == GPU_VK and gvk_has_mesh }
+function gpu_draw_mesh_tasks(x: int, y: int, z: int) -> void { if gpu_kind == GPU_VK { gvk_draw_mesh_tasks_now(x, y, z) } }
+function gpu_compute(name: string, n_bufs: int) -> int {
+ if gpu_kind != GPU_VK { return 0 }
+ return gvk_compute_new(name, n_bufs)
}
-function gpu_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, groups: int) -> void {
- if render3d_st.gpu_kind == GPU_VK and c > 0 { gvk_dispatch(render3d_st, c, params, n_params, bufs, groups, 1, 1) }
+function gpu_dispatch(c: int, params: pointer, n_params: int, bufs: words, groups: int) -> void {
+ if gpu_kind == GPU_VK and c > 0 { gvk_dispatch(c, params, n_params, bufs, groups, 1, 1) }
}
# a buffer a compute pass writes (never reallocated under a draw that reads it)
-function gpu_buffer_gpu_owned(render3d_st: mut Render3dState, buf: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_buf_gpu_owned(render3d_st, buf) } }
+function gpu_buffer_gpu_owned(buf: int) -> void { if gpu_kind == GPU_VK { gvk_buf_gpu_owned(buf) } }
# the host-visible contents of a buffer, for a readback after gpu_finish; null on OpenGL
-function gpu_buffer_map(render3d_st: Render3dState, buf: int) -> pointer {
- if render3d_st.gpu_kind != GPU_VK or buf <= 0 { return null }
- return render3d_st.gvk_buf_map[buf]
+function gpu_buffer_map(buf: int) -> pointer {
+ if gpu_kind != GPU_VK or buf <= 0 { return null }
+ return gvk_buf_map[buf]
}
-function gpu_finish(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_flush(render3d_st) } }
+function gpu_finish() -> void { if gpu_kind == GPU_VK { gvk_flush() } }
# n indexed draws of mesh m from VkDrawIndexedIndirectCommand records in buffer cmds at offset
# (bytes); each record's firstInstance selects its instances out of the bound instance buffer.
# With count_buf > 0 the GPU's own count (a uint at count_off) is used, up to n.
-function gpu_draw_mesh_indirect(render3d_st: mut Render3dState, m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void {
- if m != null { ds_draw(render3d_st, DS_INDIRECT, n, m.count) }
- if render3d_st.gpu_kind == GPU_VK { gvk_draw_indirect_now(render3d_st, m, cmds, offset, n, count_buf, count_off) }
+function gpu_draw_mesh_indirect(m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void {
+ if m != null { ds_draw(DS_INDIRECT, n, m.count) }
+ if gpu_kind == GPU_VK { gvk_draw_indirect_now(m, cmds, offset, n, count_buf, count_off) }
}
# drawing
-function gpu_mesh_bind(render3d_st: Render3dState, m: Mesh) -> void { if render3d_st.gpu_kind == GPU_VK { return }; gl_bind_vertex_array(m.vao) }
-function gpu_mesh_unbind(render3d_st: Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { return }; gl_bind_vertex_array(0) }
-function gpu_draw_mesh(render3d_st: mut Render3dState, m: Mesh) -> void {
- ds_draw(render3d_st, DS_MESH, 1, m.count)
- if render3d_st.gpu_kind == GPU_VK { gvk_draw_now(render3d_st, m, 0, 0, 1); return }
- gpu_glcheck_before(render3d_st, "a draw")
+function gpu_mesh_bind(m: Mesh) -> void { if gpu_kind == GPU_VK { return }; gl_bind_vertex_array(m.vao) }
+function gpu_mesh_unbind() -> void { if gpu_kind == GPU_VK { return }; gl_bind_vertex_array(0) }
+function gpu_draw_mesh(m: Mesh) -> void {
+ ds_draw(DS_MESH, 1, m.count)
+ if gpu_kind == GPU_VK { gvk_draw_now(m, 0, 0, 1); return }
+ gpu_glcheck_before("a draw")
gl_bind_vertex_array(m.vao)
if m.ebo != 0 { gl_draw_elements(m.mode, m.count, m.itype, null) }
else { gl_draw_arrays(m.mode, 0, m.count) }
- gpu_glcheck_after(render3d_st, "a draw")
+ gpu_glcheck_after("a draw")
}
-function gpu_draw_mesh_instanced(render3d_st: mut Render3dState, m: Mesh, n: int) -> void {
- ds_draw(render3d_st, DS_INSTANCED, n, m.count * n)
- if render3d_st.gpu_kind == GPU_VK { gvk_draw_now(render3d_st, m, 0, 0, n); return }
- gpu_glcheck_before(render3d_st, "an instanced draw")
+function gpu_draw_mesh_instanced(m: Mesh, n: int) -> void {
+ ds_draw(DS_INSTANCED, n, m.count * n)
+ if gpu_kind == GPU_VK { gvk_draw_now(m, 0, 0, n); return }
+ gpu_glcheck_before("an instanced draw")
gl_bind_vertex_array(m.vao)
if m.ebo != 0 { gl_draw_elements_instanced(m.mode, m.count, m.itype, null, n) }
else { gl_draw_arrays_instanced(m.mode, 0, m.count, n) }
- gpu_glcheck_after(render3d_st, "an instanced draw")
+ gpu_glcheck_after("an instanced draw")
}
# the bound mesh's indices again (a patch mesh drawn once per terrain node)
-function gpu_draw_bound_elements(render3d_st: mut Render3dState, m: Mesh) -> void {
- ds_draw(render3d_st, DS_PATCH, 1, m.count)
- if render3d_st.gpu_kind == GPU_VK { gvk_draw_now(render3d_st, m, 0, 0, 1); return }
- gpu_glcheck_before(render3d_st, "a terrain patch")
+function gpu_draw_bound_elements(m: Mesh) -> void {
+ ds_draw(DS_PATCH, 1, m.count)
+ if gpu_kind == GPU_VK { gvk_draw_now(m, 0, 0, 1); return }
+ gpu_glcheck_before("a terrain patch")
gl_draw_elements(m.mode, m.count, m.itype, null)
- gpu_glcheck_after(render3d_st, "a terrain patch")
+ gpu_glcheck_after("a terrain patch")
}
# vertices [first, first + count) of the bound mesh, as triangles (the overlay's ranges)
-function gpu_draw_range(render3d_st: mut Render3dState, m: Mesh, first: int, count: int) -> void {
- ds_draw(render3d_st, DS_RANGE, 1, count)
- if render3d_st.gpu_kind == GPU_VK { gvk_draw_now(render3d_st, m, first, count, 1); return }
- gpu_glcheck_before(render3d_st, "an overlay draw")
+function gpu_draw_range(m: Mesh, first: int, count: int) -> void {
+ ds_draw(DS_RANGE, 1, count)
+ if gpu_kind == GPU_VK { gvk_draw_now(m, first, count, 1); return }
+ gpu_glcheck_before("an overlay draw")
gl_draw_arrays(GL_TRIANGLES, first, count)
- gpu_glcheck_after(render3d_st, "an overlay draw")
+ gpu_glcheck_after("an overlay draw")
}
-function gpu_mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_mesh_free(render3d_st, m); return }
+function gpu_mesh_free(m: Mesh) -> void {
+ if gpu_kind == GPU_VK { gvk_mesh_free(m); return }
if m == null { return }
- let ids = gpu_tmp(render3d_st)
+ let ids = gpu_tmp()
if m.vbufs != null {
for i in 0 .. m.n_vbufs { ids[0] = m.vbufs[i]; gl_delete_buffers(1, ids) }
m.n_vbufs = 0
@@ -589,137 +621,143 @@ function gpu_mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void {
const GPU_TEX2D: int = 1
const GPU_TEX2D_ARRAY: int = 2
const GPU_TX_W: int = 12 # per handle: kind, w, h, layers, ifmt, min, mag, wrap s, wrap t, compare, mips, aniso
+var gpu_tx: words = null
+var gpu_tx_cap: int = 0
+var gpu_bound_2d: int = 0
+var gpu_bound_array: int = 0
function gpu_gl_target(kind: int) -> int { if kind == GPU_TEX2D_ARRAY { return GL_TEXTURE_2D_ARRAY }; return GL_TEXTURE_2D }
# the record for a handle, growing the table when a new name is larger than it
-function gpu_tx_at(render3d_st: mut Render3dState, tex: int) -> int {
+function gpu_tx_at(tex: int) -> int {
if tex <= 0 { return -1 }
- if tex >= render3d_st.gpu_tx_cap {
- var cap = render3d_st.gpu_tx_cap * 2
+ if tex >= gpu_tx_cap {
+ var cap = gpu_tx_cap * 2
if cap < 256 { cap = 256 }
while cap <= tex { cap = cap * 2 }
let t = words(cap * GPU_TX_W)
for i in 0 .. cap * GPU_TX_W { t[i] = 0 }
- if render3d_st.gpu_tx != null { mem_copy(t, render3d_st.gpu_tx, render3d_st.gpu_tx_cap * GPU_TX_W * 4); free(render3d_st.gpu_tx) }
- render3d_st.gpu_tx = t
- render3d_st.gpu_tx_cap = cap
+ if gpu_tx != null { mem_copy(t, gpu_tx, gpu_tx_cap * GPU_TX_W * 4); free(gpu_tx) }
+ gpu_tx = t
+ gpu_tx_cap = cap
}
return tex * GPU_TX_W
}
-function gpu_bound(render3d_st: Render3dState, kind: int) -> int { if kind == GPU_TEX2D_ARRAY { return render3d_st.gpu_bound_array }; return render3d_st.gpu_bound_2d }
+function gpu_bound(kind: int) -> int { if kind == GPU_TEX2D_ARRAY { return gpu_bound_array }; return gpu_bound_2d }
-function gpu_tex_new(render3d_st: mut Render3dState) -> int { if render3d_st.gpu_kind == GPU_VK { return gvk_tex_new(render3d_st) }; return gl_texture() }
+function gpu_tex_new() -> int { if gpu_kind == GPU_VK { return gvk_tex_new() }; return gl_texture() }
# does texture `tex` have an image behind it? Always on OpenGL; on Vulkan an image whose memory could
# not be had is never made, and a caller that can fall back (a smaller shadow map) asks here.
-function gpu_tex_ok(render3d_st: Render3dState, tex: int) -> bool {
- if render3d_st.gpu_kind != GPU_VK { return tex > 0 }
- return tex > 0 and tex < len(render3d_st.gvk_tex_image) and render3d_st.gvk_tex_image[tex] != 0
+function gpu_tex_ok(tex: int) -> bool {
+ if gpu_kind != GPU_VK { return tex > 0 }
+ return tex > 0 and tex < len(gvk_tex_image) and gvk_tex_image[tex] != 0
}
# what GL has on each unit's 2D target, for R3D_GLCHECK: deleting a texture unbinds it everywhere
-function gpu_tex_unit(render3d_st: mut Render3dState, unit: int) -> void { if render3d_st.gpu_kind == GPU_VK { render3d_st.gpu_unit_cur = unit; return }; gl_active_texture(GL_TEXTURE0 + unit); render3d_st.gpu_unit_cur = unit }
-function gpu_tex_bind(render3d_st: mut Render3dState, kind: int, tex: int) -> void {
- ds_tex_bind(render3d_st)
- if render3d_st.gpu_kind != GPU_VK { gl_bind_texture(gpu_gl_target(kind), tex) }
- if kind == GPU_TEX2D_ARRAY { render3d_st.gpu_bound_array = tex } else { render3d_st.gpu_bound_2d = tex }
- if kind != GPU_TEX2D_ARRAY and render3d_st.gpu_unit_cur < 32 {
- if render3d_st.gpu_unit_2d == null { render3d_st.gpu_unit_2d = words(32); for i in 0 .. 32 { render3d_st.gpu_unit_2d[i] = -1 } }
- render3d_st.gpu_unit_2d[render3d_st.gpu_unit_cur] = tex
+var gpu_unit_2d: words = null
+var gpu_unit_cur: int = 0
+function gpu_tex_unit(unit: int) -> void { if gpu_kind == GPU_VK { gpu_unit_cur = unit; return }; gl_active_texture(GL_TEXTURE0 + unit); gpu_unit_cur = unit }
+function gpu_tex_bind(kind: int, tex: int) -> void {
+ ds_tex_bind()
+ if gpu_kind != GPU_VK { gl_bind_texture(gpu_gl_target(kind), tex) }
+ if kind == GPU_TEX2D_ARRAY { gpu_bound_array = tex } else { gpu_bound_2d = tex }
+ if kind != GPU_TEX2D_ARRAY and gpu_unit_cur < 32 {
+ if gpu_unit_2d == null { gpu_unit_2d = words(32); for i in 0 .. 32 { gpu_unit_2d[i] = -1 } }
+ gpu_unit_2d[gpu_unit_cur] = tex
}
}
# pixel transfer packing (alignment, byte swap) for the uploads and read-backs that follow
-function gpu_pixel_store(render3d_st: mut Render3dState, pname: int, value: int) -> void { if render3d_st.gpu_kind == GPU_VK { if pname == GL_UNPACK_SWAP_BYTES { render3d_st.gvk_unpack_swap = value == 1 }; return }; gl_pixel_storei(pname, value); gpu_glcheck_after(render3d_st, "pixel store") }
-function gpu_tex_image2d(render3d_st: mut Render3dState, ifmt: int, w: int, h: int, fmt: int, ty: int, data: pointer) -> void {
- if render3d_st.gpu_kind == GPU_VK {
- gvk_flush(render3d_st)
- gvk_tex_storage(render3d_st, render3d_st.gpu_bound_2d, false, ifmt, w, h, 1, data != null)
- if data != null { gvk_tex_upload(render3d_st, render3d_st.gpu_bound_2d, ifmt, w, h, 1, fmt, ty, data) }
+function gpu_pixel_store(pname: int, value: int) -> void { if gpu_kind == GPU_VK { if pname == GL_UNPACK_SWAP_BYTES { gvk_unpack_swap = value == 1 }; return }; gl_pixel_storei(pname, value); gpu_glcheck_after("pixel store") }
+function gpu_tex_image2d(ifmt: int, w: int, h: int, fmt: int, ty: int, data: pointer) -> void {
+ if gpu_kind == GPU_VK {
+ gvk_flush()
+ gvk_tex_storage(gpu_bound_2d, false, ifmt, w, h, 1, data != null)
+ if data != null { gvk_tex_upload(gpu_bound_2d, ifmt, w, h, 1, fmt, ty, data) }
} else { gl_tex_image2d(GL_TEXTURE_2D, 0, ifmt, w, h, 0, fmt, ty, data) }
- gpu_glcheck_after(render3d_st, `a {w}x{h} texture upload (format {ifmt})`)
- let o = gpu_tx_at(render3d_st, render3d_st.gpu_bound_2d)
- if o >= 0 { render3d_st.gpu_tx[o] = GPU_TEX2D; render3d_st.gpu_tx[o + 1] = w; render3d_st.gpu_tx[o + 2] = h; render3d_st.gpu_tx[o + 3] = 1; render3d_st.gpu_tx[o + 4] = ifmt }
+ gpu_glcheck_after(`a {w}x{h} texture upload (format {ifmt})`)
+ let o = gpu_tx_at(gpu_bound_2d)
+ if o >= 0 { gpu_tx[o] = GPU_TEX2D; gpu_tx[o + 1] = w; gpu_tx[o + 2] = h; gpu_tx[o + 3] = 1; gpu_tx[o + 4] = ifmt }
}
-function gpu_tex_image3d(render3d_st: mut Render3dState, ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> void {
- if render3d_st.gpu_kind == GPU_VK {
- gvk_flush(render3d_st)
- gvk_tex_storage(render3d_st, render3d_st.gpu_bound_array, true, ifmt, w, h, layers, data != null)
- if data != null { gvk_tex_upload(render3d_st, render3d_st.gpu_bound_array, ifmt, w, h, layers, fmt, ty, data) }
+function gpu_tex_image3d(ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> void {
+ if gpu_kind == GPU_VK {
+ gvk_flush()
+ gvk_tex_storage(gpu_bound_array, true, ifmt, w, h, layers, data != null)
+ if data != null { gvk_tex_upload(gpu_bound_array, ifmt, w, h, layers, fmt, ty, data) }
} else { gl_tex_image3d(GL_TEXTURE_2D_ARRAY, 0, ifmt, w, h, layers, 0, fmt, ty, data) }
- gpu_glcheck_after(render3d_st, `a {w}x{h}x{layers} array upload (format {ifmt})`)
- let o = gpu_tx_at(render3d_st, render3d_st.gpu_bound_array)
- if o >= 0 { render3d_st.gpu_tx[o] = GPU_TEX2D_ARRAY; render3d_st.gpu_tx[o + 1] = w; render3d_st.gpu_tx[o + 2] = h; render3d_st.gpu_tx[o + 3] = layers; render3d_st.gpu_tx[o + 4] = ifmt }
+ gpu_glcheck_after(`a {w}x{h}x{layers} array upload (format {ifmt})`)
+ let o = gpu_tx_at(gpu_bound_array)
+ if o >= 0 { gpu_tx[o] = GPU_TEX2D_ARRAY; gpu_tx[o + 1] = w; gpu_tx[o + 2] = h; gpu_tx[o + 3] = layers; gpu_tx[o + 4] = ifmt }
}
-function gpu_tex_param(render3d_st: mut Render3dState, kind: int, pname: int, value: int) -> void {
- if render3d_st.gpu_kind != GPU_VK { gl_tex_parameteri(gpu_gl_target(kind), pname, value) }
- let o = gpu_tx_at(render3d_st, gpu_bound(render3d_st, kind))
+function gpu_tex_param(kind: int, pname: int, value: int) -> void {
+ if gpu_kind != GPU_VK { gl_tex_parameteri(gpu_gl_target(kind), pname, value) }
+ let o = gpu_tx_at(gpu_bound(kind))
if o < 0 { return }
- if pname == GL_TEXTURE_MIN_FILTER { render3d_st.gpu_tx[o + 5] = value }
- if pname == GL_TEXTURE_MAG_FILTER { render3d_st.gpu_tx[o + 6] = value }
- if pname == GL_TEXTURE_WRAP_S { render3d_st.gpu_tx[o + 7] = value }
- if pname == GL_TEXTURE_WRAP_T { render3d_st.gpu_tx[o + 8] = value }
- if pname == GL_TEXTURE_COMPARE_MODE { if value == GL_NONE { render3d_st.gpu_tx[o + 9] = 0 } }
- if pname == GL_TEXTURE_COMPARE_FUNC { render3d_st.gpu_tx[o + 9] = value }
- gpu_glcheck_after(render3d_st, "tex param")
+ if pname == GL_TEXTURE_MIN_FILTER { gpu_tx[o + 5] = value }
+ if pname == GL_TEXTURE_MAG_FILTER { gpu_tx[o + 6] = value }
+ if pname == GL_TEXTURE_WRAP_S { gpu_tx[o + 7] = value }
+ if pname == GL_TEXTURE_WRAP_T { gpu_tx[o + 8] = value }
+ if pname == GL_TEXTURE_COMPARE_MODE { if value == GL_NONE { gpu_tx[o + 9] = 0 } }
+ if pname == GL_TEXTURE_COMPARE_FUNC { gpu_tx[o + 9] = value }
+ gpu_glcheck_after("tex param")
}
# a float parameter (fixed, as Gl.* takes it): anisotropy is the one the renderer sets
-function gpu_tex_paramf(render3d_st: mut Render3dState, kind: int, pname: int, value: fixed) -> void {
- if render3d_st.gpu_kind != GPU_VK { gl_tex_parameterf(gpu_gl_target(kind), pname, value) }
- let o = gpu_tx_at(render3d_st, gpu_bound(render3d_st, kind))
- if o >= 0 and pname == 0x84FE { render3d_st.gpu_tx[o + 11] = float_bits(float(value)) }
- gpu_glcheck_after(render3d_st, "tex paramf")
+function gpu_tex_paramf(kind: int, pname: int, value: fixed) -> void {
+ if gpu_kind != GPU_VK { gl_tex_parameterf(gpu_gl_target(kind), pname, value) }
+ let o = gpu_tx_at(gpu_bound(kind))
+ if o >= 0 and pname == 0x84FE { gpu_tx[o + 11] = float_bits(float(value)) }
+ gpu_glcheck_after("tex paramf")
}
# the border colour clamp-to-border reads (four fixed values in `rgba`)
-function gpu_tex_border(render3d_st: Render3dState, kind: int, rgba: pointer) -> void { if render3d_st.gpu_kind == GPU_VK { return }; gl_tex_parameterfv(gpu_gl_target(kind), GL_TEXTURE_BORDER_COLOR, rgba) }
-function gpu_tex_mips(render3d_st: mut Render3dState, kind: int) -> void {
- if render3d_st.gpu_kind == GPU_VK {
- let mt = gpu_bound(render3d_st, kind)
- let mo = gpu_tx_at(render3d_st, mt)
- if mo >= 0 { gvk_mips_now(render3d_st, mt, render3d_st.gpu_tx[mo + 1], render3d_st.gpu_tx[mo + 2]) }
+function gpu_tex_border(kind: int, rgba: pointer) -> void { if gpu_kind == GPU_VK { return }; gl_tex_parameterfv(gpu_gl_target(kind), GL_TEXTURE_BORDER_COLOR, rgba) }
+function gpu_tex_mips(kind: int) -> void {
+ if gpu_kind == GPU_VK {
+ let mt = gpu_bound(kind)
+ let mo = gpu_tx_at(mt)
+ if mo >= 0 { gvk_mips_now(mt, gpu_tx[mo + 1], gpu_tx[mo + 2]) }
} else { gl_generate_mipmap(gpu_gl_target(kind)) }
- gpu_glcheck_after(render3d_st, `mipmaps for texture {gpu_bound(render3d_st, kind)}`)
- let o = gpu_tx_at(render3d_st, gpu_bound(render3d_st, kind))
- if o >= 0 { render3d_st.gpu_tx[o + 10] = 1 }
+ gpu_glcheck_after(`mipmaps for texture {gpu_bound(kind)}`)
+ let o = gpu_tx_at(gpu_bound(kind))
+ if o >= 0 { gpu_tx[o + 10] = 1 }
}
# level 0 of the bound texture into `out`
-function gpu_tex_read(render3d_st: mut Render3dState, kind: int, fmt: int, ty: int, out: pointer) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_flush(render3d_st); let rt = gpu_bound(render3d_st, kind); let ro = gpu_tx_at(render3d_st, rt); if ro >= 0 { gvk_tex_read(render3d_st, rt, render3d_st.gpu_tx[ro + 4], render3d_st.gpu_tx[ro + 1], render3d_st.gpu_tx[ro + 2], fmt, ty, out) }; return }
+function gpu_tex_read(kind: int, fmt: int, ty: int, out: pointer) -> void {
+ if gpu_kind == GPU_VK { gvk_flush(); let rt = gpu_bound(kind); let ro = gpu_tx_at(rt); if ro >= 0 { gvk_tex_read(rt, gpu_tx[ro + 4], gpu_tx[ro + 1], gpu_tx[ro + 2], fmt, ty, out) }; return }
gl_get_tex_image(gpu_gl_target(kind), 0, fmt, ty, out)
- gpu_glcheck_after(render3d_st, `a read-back of texture {gpu_bound(render3d_st, kind)}`)
+ gpu_glcheck_after(`a read-back of texture {gpu_bound(kind)}`)
}
-function gpu_tex_free(render3d_st: mut Render3dState, tex: int) -> void {
+function gpu_tex_free(tex: int) -> void {
if tex == 0 { return }
- let ids = gpu_tmp(render3d_st)
+ let ids = gpu_tmp()
ids[0] = tex
- if render3d_st.gpu_kind == GPU_VK { gvk_flush(render3d_st); if tex < len(render3d_st.gvk_tex_image) { gvk_tex_release(render3d_st, tex) } } else { gl_delete_textures(1, ids) }
- if render3d_st.gpu_unit_2d != null { for i in 0 .. 32 { if render3d_st.gpu_unit_2d[i] == tex { render3d_st.gpu_unit_2d[i] = 0; if gpu_glcheck_on(render3d_st) { gpu_glcheck_say(render3d_st, `gpu: texture {tex} freed while bound on unit {i}`) } } } }
- if render3d_st.gpu_bound_2d == tex { render3d_st.gpu_bound_2d = 0 }
- let o = gpu_tx_at(render3d_st, tex)
- if o >= 0 { for i in 0 .. GPU_TX_W { render3d_st.gpu_tx[o + i] = 0 } }
+ if gpu_kind == GPU_VK { gvk_flush(); if tex < len(gvk_tex_image) { gvk_tex_release(tex) } } else { gl_delete_textures(1, ids) }
+ if gpu_unit_2d != null { for i in 0 .. 32 { if gpu_unit_2d[i] == tex { gpu_unit_2d[i] = 0; if gpu_glcheck_on() { gpu_glcheck_say(`gpu: texture {tex} freed while bound on unit {i}`) } } } }
+ if gpu_bound_2d == tex { gpu_bound_2d = 0 }
+ let o = gpu_tx_at(tex)
+ if o >= 0 { for i in 0 .. GPU_TX_W { gpu_tx[o + i] = 0 } }
}
# a texture on a unit for a program's sampler, by the sampler's name
-function gpu_bind_sampler(render3d_st: mut Render3dState, prog: int, name: string, unit: int, kind: int, tex: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_bind_texture(render3d_st, prog, name, tex); return }
- gpu_tex_unit(render3d_st, unit)
- gpu_tex_bind(render3d_st, kind, tex)
- u_i(render3d_st, gpu_uniform(render3d_st, prog, name), unit)
- if gpu_glcheck_on(render3d_st) {
+function gpu_bind_sampler(prog: int, name: string, unit: int, kind: int, tex: int) -> void {
+ if gpu_kind == GPU_VK { gvk_bind_texture(prog, name, tex); return }
+ gpu_tex_unit(unit)
+ gpu_tex_bind(kind, tex)
+ u_i(gpu_uniform(prog, name), unit)
+ if gpu_glcheck_on() {
# a sampler reading a texture nobody made (0 or freed) is the "unloadable" the driver warns of
- let o = gpu_tx_at(render3d_st, tex)
- if tex == 0 or o < 0 or render3d_st.gpu_tx[o] == 0 { gpu_glcheck_say(render3d_st, `gpu: {name} on unit {unit} of program {prog} samples texture {tex}, which has no image`) }
+ let o = gpu_tx_at(tex)
+ if tex == 0 or o < 0 or gpu_tx[o] == 0 { gpu_glcheck_say(`gpu: {name} on unit {unit} of program {prog} samples texture {tex}, which has no image`) }
else {
# incomplete: a min filter that reads mipmaps (GL's default does, when none was set) on a
# texture that never had them generated - the driver samples zero ("unloadable")
- let mn = render3d_st.gpu_tx[o + 5]
+ let mn = gpu_tx[o + 5]
let wants_mips = mn == 0 or mn == 0x2700 or mn == 0x2701 or mn == 0x2702 or mn == 0x2703
- if wants_mips and render3d_st.gpu_tx[o + 10] == 0 {
+ if wants_mips and gpu_tx[o + 10] == 0 {
var why = "a mipmap filter"
if mn == 0 { why = "no min filter set (GL's default reads mipmaps)" }
- gpu_glcheck_say(render3d_st, `gpu: {name} on unit {unit} of program {prog} samples texture {tex} ({render3d_st.gpu_tx[o + 1]}x{render3d_st.gpu_tx[o + 2]}, format {render3d_st.gpu_tx[o + 4]}) with {why} but no mipmaps`)
+ gpu_glcheck_say(`gpu: {name} on unit {unit} of program {prog} samples texture {tex} ({gpu_tx[o + 1]}x{gpu_tx[o + 2]}, format {gpu_tx[o + 4]}) with {why} but no mipmaps`)
}
# compare mode on: only a shadow sampler may read it; a plain one reads zero ("unloadable")
- if render3d_st.gpu_tx[o + 9] != 0 and not Text.contains(name, "shadow") { gpu_glcheck_say(render3d_st, `gpu: {name} on unit {unit} of program {prog} samples texture {tex} ({render3d_st.gpu_tx[o + 1]}x{render3d_st.gpu_tx[o + 2]}, format {render3d_st.gpu_tx[o + 4]}) with depth compare on`) }
+ if gpu_tx[o + 9] != 0 and not Text.contains(name, "shadow") { gpu_glcheck_say(`gpu: {name} on unit {unit} of program {prog} samples texture {tex} ({gpu_tx[o + 1]}x{gpu_tx[o + 2]}, format {gpu_tx[o + 4]}) with depth compare on`) }
}
- gpu_glcheck_after(render3d_st, `binding {name} (texture {tex}) on unit {unit}`)
+ gpu_glcheck_after(`binding {name} (texture {tex}) on unit {unit}`)
}
}
@@ -733,196 +771,203 @@ function gpu_bind_sampler(render3d_st: mut Render3dState, prog: int, name: strin
# completeness check of the bound framebuffer whenever a viewport is set, naming the handle, so
# a pass drawing into an incomplete target says which one; unset, it calls nothing.
const GPU_FB_W: int = 8 # per handle: colour 0, colour 1, depth texture, depth layer + 1, colour rb, depth rb, samples, colour layer + 1
+var gpu_fb: words = null
+var gpu_fb_cap: int = 0
+var gpu_fb_cur: int = 0
+var gpu_glcheck: int = -1
+var gpu_rb_samples: int = 0
+var gpu_drawbufs: words = null
-function gpu_fb_at(render3d_st: mut Render3dState, fb: int) -> int {
+function gpu_fb_at(fb: int) -> int {
if fb <= 0 { return -1 }
- if fb >= render3d_st.gpu_fb_cap {
- var cap = render3d_st.gpu_fb_cap * 2
+ if fb >= gpu_fb_cap {
+ var cap = gpu_fb_cap * 2
if cap < 64 { cap = 64 }
while cap <= fb { cap = cap * 2 }
let t = words(cap * GPU_FB_W)
for i in 0 .. cap * GPU_FB_W { t[i] = 0 }
- if render3d_st.gpu_fb != null { mem_copy(t, render3d_st.gpu_fb, render3d_st.gpu_fb_cap * GPU_FB_W * 4); free(render3d_st.gpu_fb) }
- render3d_st.gpu_fb = t
- render3d_st.gpu_fb_cap = cap
+ if gpu_fb != null { mem_copy(t, gpu_fb, gpu_fb_cap * GPU_FB_W * 4); free(gpu_fb) }
+ gpu_fb = t
+ gpu_fb_cap = cap
}
return fb * GPU_FB_W
}
-function gpu_glcheck_on(render3d_st: mut Render3dState) -> bool {
- if render3d_st.gpu_glcheck < 0 { render3d_st.gpu_glcheck = 0; if r3d_env_has(render3d_st, "R3D_GLCHECK") { render3d_st.gpu_glcheck = 1 } }
- return render3d_st.gpu_glcheck == 1
+function gpu_glcheck_on() -> bool {
+ if gpu_glcheck < 0 { gpu_glcheck = 0; if r3d_env_has("R3D_GLCHECK") { gpu_glcheck = 1 } }
+ return gpu_glcheck == 1
}
-function gpu_check(render3d_st: Render3dState, tag: string) -> int { if render3d_st.gpu_kind == GPU_VK { return 0 }; return gl_check(tag) }
+function gpu_check(tag: string) -> int { if gpu_kind == GPU_VK { return 0 }; return gl_check(tag) }
# a named checkpoint that costs nothing unless R3D_GLCHECK is set
-function gpu_debug_check(render3d_st: mut Render3dState, tag: string) -> void { if render3d_st.gpu_kind == GPU_VK { return }; if gpu_glcheck_on(render3d_st) { gl_check(tag) } }
+function gpu_debug_check(tag: string) -> void { if gpu_kind == GPU_VK { return }; if gpu_glcheck_on() { gl_check(tag) } }
-function gpu_fb_new(render3d_st: mut Render3dState) -> int { if render3d_st.gpu_kind == GPU_VK { render3d_st.gvk_fb_counter += 1; return render3d_st.gvk_fb_counter }; return gl_framebuffer() }
-function gpu_fb_bind(render3d_st: mut Render3dState, fb: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_rebind(render3d_st, fb) } else { gl_bind_framebuffer(GL_FRAMEBUFFER, fb) }
- render3d_st.gpu_fb_cur = fb
- gpu_glcheck_after(render3d_st, "fb bind")
+function gpu_fb_new() -> int { if gpu_kind == GPU_VK { gvk_fb_counter += 1; return gvk_fb_counter }; return gl_framebuffer() }
+function gpu_fb_bind(fb: int) -> void {
+ if gpu_kind == GPU_VK { gvk_rebind(fb) } else { gl_bind_framebuffer(GL_FRAMEBUFFER, fb) }
+ gpu_fb_cur = fb
+ gpu_glcheck_after("fb bind")
}
-function gpu_fb_bind_read(render3d_st: mut Render3dState, fb: int) -> void { if render3d_st.gpu_kind == GPU_VK { render3d_st.gvk_fb_read = fb; return }; gl_bind_framebuffer(GL_READ_FRAMEBUFFER, fb); gpu_glcheck_after(render3d_st, "fb bind read") }
-function gpu_fb_bind_draw(render3d_st: mut Render3dState, fb: int) -> void { if render3d_st.gpu_kind == GPU_VK { render3d_st.gvk_fb_draw = fb; return }; gl_bind_framebuffer(GL_DRAW_FRAMEBUFFER, fb); gpu_glcheck_after(render3d_st, "fb bind draw") }
-function gpu_fb_color(render3d_st: mut Render3dState, slot: int, tex: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_pass_end(render3d_st) } else { gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, GL_TEXTURE_2D, tex, 0) }
- let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur)
- if o >= 0 and slot < 2 { render3d_st.gpu_fb[o + slot] = tex; if slot == 0 { render3d_st.gpu_fb[o + 7] = 0 } }
- gpu_glcheck_after(render3d_st, "attaching to {gpu_fb_describe(gpu_fb_cur)}")
+function gpu_fb_bind_read(fb: int) -> void { if gpu_kind == GPU_VK { gvk_fb_read = fb; return }; gl_bind_framebuffer(GL_READ_FRAMEBUFFER, fb); gpu_glcheck_after("fb bind read") }
+function gpu_fb_bind_draw(fb: int) -> void { if gpu_kind == GPU_VK { gvk_fb_draw = fb; return }; gl_bind_framebuffer(GL_DRAW_FRAMEBUFFER, fb); gpu_glcheck_after("fb bind draw") }
+function gpu_fb_color(slot: int, tex: int) -> void {
+ if gpu_kind == GPU_VK { gvk_pass_end() } else { gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, GL_TEXTURE_2D, tex, 0) }
+ let o = gpu_fb_at(gpu_fb_cur)
+ if o >= 0 and slot < 2 { gpu_fb[o + slot] = tex; if slot == 0 { gpu_fb[o + 7] = 0 } }
+ gpu_glcheck_after("attaching to {gpu_fb_describe(gpu_fb_cur)}")
}
-function gpu_fb_color_layer(render3d_st: mut Render3dState, slot: int, tex: int, layer: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_pass_end(render3d_st) } else { gl_framebuffer_texture_layer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, tex, 0, layer) }
- let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur)
- if o >= 0 and slot < 2 { render3d_st.gpu_fb[o + slot] = tex; if slot == 0 { render3d_st.gpu_fb[o + 7] = layer + 1 } }
- gpu_glcheck_after(render3d_st, "attaching to {gpu_fb_describe(gpu_fb_cur)}")
+function gpu_fb_color_layer(slot: int, tex: int, layer: int) -> void {
+ if gpu_kind == GPU_VK { gvk_pass_end() } else { gl_framebuffer_texture_layer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, tex, 0, layer) }
+ let o = gpu_fb_at(gpu_fb_cur)
+ if o >= 0 and slot < 2 { gpu_fb[o + slot] = tex; if slot == 0 { gpu_fb[o + 7] = layer + 1 } }
+ gpu_glcheck_after("attaching to {gpu_fb_describe(gpu_fb_cur)}")
}
-function gpu_fb_depth(render3d_st: mut Render3dState, tex: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_pass_end(render3d_st) } else { gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, tex, 0) }
- let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur)
- if o >= 0 { render3d_st.gpu_fb[o + 2] = tex; render3d_st.gpu_fb[o + 3] = 0 }
- gpu_glcheck_after(render3d_st, "attaching to {gpu_fb_describe(gpu_fb_cur)}")
+function gpu_fb_depth(tex: int) -> void {
+ if gpu_kind == GPU_VK { gvk_pass_end() } else { gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, tex, 0) }
+ let o = gpu_fb_at(gpu_fb_cur)
+ if o >= 0 { gpu_fb[o + 2] = tex; gpu_fb[o + 3] = 0 }
+ gpu_glcheck_after("attaching to {gpu_fb_describe(gpu_fb_cur)}")
}
-function gpu_fb_depth_layer(render3d_st: mut Render3dState, tex: int, layer: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_pass_end(render3d_st) } else { gl_framebuffer_texture_layer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, tex, 0, layer) }
- let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur)
- if o >= 0 { render3d_st.gpu_fb[o + 2] = tex; render3d_st.gpu_fb[o + 3] = layer + 1 }
- gpu_glcheck_after(render3d_st, "attaching to {gpu_fb_describe(gpu_fb_cur)}")
+function gpu_fb_depth_layer(tex: int, layer: int) -> void {
+ if gpu_kind == GPU_VK { gvk_pass_end() } else { gl_framebuffer_texture_layer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, tex, 0, layer) }
+ let o = gpu_fb_at(gpu_fb_cur)
+ if o >= 0 { gpu_fb[o + 2] = tex; gpu_fb[o + 3] = layer + 1 }
+ gpu_glcheck_after("attaching to {gpu_fb_describe(gpu_fb_cur)}")
}
-function gpu_rb_new(render3d_st: mut Render3dState) -> int {
- if render3d_st.gpu_kind == GPU_VK { return gvk_tex_new(render3d_st) }
- let ids = gpu_tmp(render3d_st)
+function gpu_rb_new() -> int {
+ if gpu_kind == GPU_VK { return gvk_tex_new() }
+ let ids = gpu_tmp()
gl_gen_renderbuffers(1, ids)
return ids[0]
}
# storage for a renderbuffer: samples > 0 makes it multisampled
-function gpu_rb_storage(render3d_st: mut Render3dState, rb: int, ifmt: int, w: int, h: int, samples: int) -> void {
- if render3d_st.gpu_kind == GPU_VK {
- gvk_flush(render3d_st); render3d_st.gpu_rb_samples = samples
- render3d_st.gvk_storage_samples = samples
- gvk_tex_storage(render3d_st, rb, false, ifmt, w, h, 1, false)
- render3d_st.gvk_storage_samples = 1
+function gpu_rb_storage(rb: int, ifmt: int, w: int, h: int, samples: int) -> void {
+ if gpu_kind == GPU_VK {
+ gvk_flush(); gpu_rb_samples = samples
+ gvk_storage_samples = samples
+ gvk_tex_storage(rb, false, ifmt, w, h, 1, false)
+ gvk_storage_samples = 1
return
}
gl_bind_renderbuffer(GL_RENDERBUFFER, rb)
if samples > 0 { gl_renderbuffer_storage_multisample(GL_RENDERBUFFER, samples, ifmt, w, h) }
else { gl_renderbuffer_storage(GL_RENDERBUFFER, ifmt, w, h) }
- render3d_st.gpu_rb_samples = samples
- gpu_glcheck_after(render3d_st, "rb storage")
+ gpu_rb_samples = samples
+ gpu_glcheck_after("rb storage")
}
-function gpu_fb_color_rb(render3d_st: mut Render3dState, slot: int, rb: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_pass_end(render3d_st); let co = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur); if co >= 0 and slot < 2 { render3d_st.gpu_fb[co + slot] = rb }; return }
+function gpu_fb_color_rb(slot: int, rb: int) -> void {
+ if gpu_kind == GPU_VK { gvk_pass_end(); let co = gpu_fb_at(gpu_fb_cur); if co >= 0 and slot < 2 { gpu_fb[co + slot] = rb }; return }
gl_framebuffer_renderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, GL_RENDERBUFFER, rb)
- let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur)
- if o >= 0 { render3d_st.gpu_fb[o + 4] = rb; render3d_st.gpu_fb[o + 6] = render3d_st.gpu_rb_samples }
+ let o = gpu_fb_at(gpu_fb_cur)
+ if o >= 0 { gpu_fb[o + 4] = rb; gpu_fb[o + 6] = gpu_rb_samples }
}
-function gpu_fb_depth_rb(render3d_st: mut Render3dState, rb: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_pass_end(render3d_st); let dop = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur); if dop >= 0 { render3d_st.gpu_fb[dop + 2] = rb; render3d_st.gpu_fb[dop + 3] = 0 }; return }
+function gpu_fb_depth_rb(rb: int) -> void {
+ if gpu_kind == GPU_VK { gvk_pass_end(); let dop = gpu_fb_at(gpu_fb_cur); if dop >= 0 { gpu_fb[dop + 2] = rb; gpu_fb[dop + 3] = 0 }; return }
gl_framebuffer_renderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb)
- let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur)
- if o >= 0 { render3d_st.gpu_fb[o + 5] = rb; render3d_st.gpu_fb[o + 6] = render3d_st.gpu_rb_samples }
+ let o = gpu_fb_at(gpu_fb_cur)
+ if o >= 0 { gpu_fb[o + 5] = rb; gpu_fb[o + 6] = gpu_rb_samples }
}
# colour slots 0 .. n-1 are drawn into (several: an MRT bake)
-function gpu_fb_draw_buffers(render3d_st: mut Render3dState, n: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_fb_colors(render3d_st, render3d_st.gpu_fb_cur, n); return }
- if render3d_st.gpu_drawbufs == null { render3d_st.gpu_drawbufs = words(8) }
- for i in 0 .. n { render3d_st.gpu_drawbufs[i] = GL_COLOR_ATTACHMENT0 + i }
- gl_draw_buffers(n, render3d_st.gpu_drawbufs)
- gpu_glcheck_after(render3d_st, "fb draw buffers")
+function gpu_fb_draw_buffers(n: int) -> void {
+ if gpu_kind == GPU_VK { gvk_fb_colors(gpu_fb_cur, n); return }
+ if gpu_drawbufs == null { gpu_drawbufs = words(8) }
+ for i in 0 .. n { gpu_drawbufs[i] = GL_COLOR_ATTACHMENT0 + i }
+ gl_draw_buffers(n, gpu_drawbufs)
+ gpu_glcheck_after("fb draw buffers")
}
# a depth-only target: no colour is drawn or read
-function gpu_fb_no_color(render3d_st: mut Render3dState) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_fb_colors(render3d_st, render3d_st.gpu_fb_cur, 0); return }
+function gpu_fb_no_color() -> void {
+ if gpu_kind == GPU_VK { gvk_fb_colors(gpu_fb_cur, 0); return }
gl_draw_buffer(GL_NONE)
gl_read_buffer(GL_NONE)
- gpu_glcheck_after(render3d_st, "fb no color")
+ gpu_glcheck_after("fb no color")
}
-function gpu_fb_status(render3d_st: Render3dState) -> int { if render3d_st.gpu_kind == GPU_VK { return GL_FRAMEBUFFER_COMPLETE }; return gl_check_framebuffer_status(GL_FRAMEBUFFER) }
-function gpu_fb_free(render3d_st: mut Render3dState, fb: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_fb_forget(render3d_st, fb); let fo = gpu_fb_at(render3d_st, fb); if fo >= 0 { for i in 0 .. GPU_FB_W { render3d_st.gpu_fb[fo + i] = 0 } }; return }
+function gpu_fb_status() -> int { if gpu_kind == GPU_VK { return GL_FRAMEBUFFER_COMPLETE }; return gl_check_framebuffer_status(GL_FRAMEBUFFER) }
+function gpu_fb_free(fb: int) -> void {
+ if gpu_kind == GPU_VK { gvk_fb_forget(fb); let fo = gpu_fb_at(fb); if fo >= 0 { for i in 0 .. GPU_FB_W { gpu_fb[fo + i] = 0 } }; return }
if fb == 0 { return }
- let ids = gpu_tmp(render3d_st)
+ let ids = gpu_tmp()
ids[0] = fb
gl_delete_framebuffers(1, ids)
- let o = gpu_fb_at(render3d_st, fb)
- if o >= 0 { for i in 0 .. GPU_FB_W { render3d_st.gpu_fb[o + i] = 0 } }
- gpu_glcheck_after(render3d_st, "fb free")
+ let o = gpu_fb_at(fb)
+ if o >= 0 { for i in 0 .. GPU_FB_W { gpu_fb[o + i] = 0 } }
+ gpu_glcheck_after("fb free")
}
-function gpu_rb_free(render3d_st: mut Render3dState, rb: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_flush(render3d_st); if rb > 0 and rb < len(render3d_st.gvk_tex_image) { gvk_tex_release(render3d_st, rb) }; return }
+function gpu_rb_free(rb: int) -> void {
+ if gpu_kind == GPU_VK { gvk_flush(); if rb > 0 and rb < len(gvk_tex_image) { gvk_tex_release(rb) }; return }
if rb == 0 { return }
- let ids = gpu_tmp(render3d_st)
+ let ids = gpu_tmp()
ids[0] = rb
gl_delete_renderbuffers(1, ids)
- gpu_glcheck_after(render3d_st, "rb free")
+ gpu_glcheck_after("rb free")
}
-function gpu_viewport(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_viewport(render3d_st, x, y, w, h); return }; gl_viewport(x, y, w, h); gpu_glcheck_after(render3d_st, "viewport") }
+function gpu_viewport(x: int, y: int, w: int, h: int) -> void { if gpu_kind == GPU_VK { gvk_viewport(x, y, w, h); return }; gl_viewport(x, y, w, h); gpu_glcheck_after("viewport") }
# R3D_GLCHECK: before a draw or a clear, the bound framebuffer must be complete; after it, no
# error may be pending. Each report names the framebuffer and what was being done, once per
# distinct message, so the first bad pass is found without a flood. (Checking when a viewport
# is set reported the shadow pass, which sets its viewport before attaching a cascade.)
-function gpu_glcheck_say(render3d_st: mut Render3dState, msg: string) -> void {
- if render3d_st.gpu_glcheck_seen == null { render3d_st.gpu_glcheck_seen = new []string }
- for i in 0 .. len(render3d_st.gpu_glcheck_seen) { if render3d_st.gpu_glcheck_seen[i] == msg { return } }
- push(render3d_st.gpu_glcheck_seen, msg)
+var gpu_glcheck_seen: []string = null
+function gpu_glcheck_say(msg: string) -> void {
+ if gpu_glcheck_seen == null { gpu_glcheck_seen = new []string }
+ for i in 0 .. len(gpu_glcheck_seen) { if gpu_glcheck_seen[i] == msg { return } }
+ push(gpu_glcheck_seen, msg)
print(msg)
}
# a framebuffer as a person reads it: its handle and its colour (or depth) attachment's size and format
-function gpu_fb_describe(render3d_st: mut Render3dState, fb: int) -> string {
+function gpu_fb_describe(fb: int) -> string {
if fb == 0 { return "the default framebuffer" }
- let o = gpu_fb_at(render3d_st, fb)
+ let o = gpu_fb_at(fb)
if o < 0 { return `framebuffer {fb}` }
- var tex = render3d_st.gpu_fb[o]
+ var tex = gpu_fb[o]
var what = "colour"
- if tex == 0 { tex = render3d_st.gpu_fb[o + 2]; what = "depth" }
- let t = gpu_tx_at(render3d_st, tex)
+ if tex == 0 { tex = gpu_fb[o + 2]; what = "depth" }
+ let t = gpu_tx_at(tex)
if tex == 0 or t < 0 { return `framebuffer {fb} (nothing recorded attached)` }
- return `framebuffer {fb} ({what} texture {tex}, {render3d_st.gpu_tx[t + 1]}x{render3d_st.gpu_tx[t + 2]}, format {render3d_st.gpu_tx[t + 4]})`
+ return `framebuffer {fb} ({what} texture {tex}, {gpu_tx[t + 1]}x{gpu_tx[t + 2]}, format {gpu_tx[t + 4]})`
}
-function gpu_glcheck_before(render3d_st: mut Render3dState, what: string) -> void {
- if render3d_st.gpu_kind == GPU_VK { return }
- if not gpu_glcheck_on(render3d_st) { return }
+function gpu_glcheck_before(what: string) -> void {
+ if gpu_kind == GPU_VK { return }
+ if not gpu_glcheck_on() { return }
let pending = gl_get_error()
- if pending != 0 { gpu_glcheck_say(render3d_st, `gpu: error {pending} pending before {what} into {gpu_fb_describe(render3d_st, render3d_st.gpu_fb_cur)}`) }
- if render3d_st.gpu_unit_2d != null and render3d_st.gpu_unit_2d[0] == 0 { gpu_glcheck_say(render3d_st, `gpu: {what} into {gpu_fb_describe(render3d_st, render3d_st.gpu_fb_cur)} with unit 0's 2D texture deleted`) }
+ if pending != 0 { gpu_glcheck_say(`gpu: error {pending} pending before {what} into {gpu_fb_describe(gpu_fb_cur)}`) }
+ if gpu_unit_2d != null and gpu_unit_2d[0] == 0 { gpu_glcheck_say(`gpu: {what} into {gpu_fb_describe(gpu_fb_cur)} with unit 0's 2D texture deleted`) }
let st = gl_check_framebuffer_status(GL_FRAMEBUFFER)
- if st != GL_FRAMEBUFFER_COMPLETE { gpu_glcheck_say(render3d_st, `gpu: {gpu_fb_describe(render3d_st, render3d_st.gpu_fb_cur)} incomplete ({st}) at {what}`) }
+ if st != GL_FRAMEBUFFER_COMPLETE { gpu_glcheck_say(`gpu: {gpu_fb_describe(gpu_fb_cur)} incomplete ({st}) at {what}`) }
}
-function gpu_glcheck_after(render3d_st: mut Render3dState, what: string) -> void {
- if render3d_st.gpu_kind == GPU_VK { return }
- if not gpu_glcheck_on(render3d_st) { return }
+function gpu_glcheck_after(what: string) -> void {
+ if gpu_kind == GPU_VK { return }
+ if not gpu_glcheck_on() { return }
let e = gl_get_error()
- if e != 0 { gpu_glcheck_say(render3d_st, `gpu: error {e} from {what} into {gpu_fb_describe(render3d_st, render3d_st.gpu_fb_cur)}`) }
+ if e != 0 { gpu_glcheck_say(`gpu: error {e} from {what} into {gpu_fb_describe(gpu_fb_cur)}`) }
}
-function gpu_clear_color(render3d_st: mut Render3dState, r: fixed, g: fixed, b: fixed, a: fixed) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_clear_color(render3d_st, float(r), float(g), float(b), float(a)); return }; gl_clear_color(r, g, b, a) }
-function gpu_clear(render3d_st: mut Render3dState, mask: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_clear(render3d_st, mask, render3d_st.gpu_fb, gpu_fb_at(render3d_st, render3d_st.gvk_fb_cur)); return }
- gpu_glcheck_before(render3d_st, "a clear")
+function gpu_clear_color(r: fixed, g: fixed, b: fixed, a: fixed) -> void { if gpu_kind == GPU_VK { gvk_clear_color(float(r), float(g), float(b), float(a)); return }; gl_clear_color(r, g, b, a) }
+function gpu_clear(mask: int) -> void {
+ if gpu_kind == GPU_VK { gvk_clear(mask, gpu_fb, gpu_fb_at(gvk_fb_cur)); return }
+ gpu_glcheck_before("a clear")
gl_clear(mask)
- gpu_glcheck_after(render3d_st, "a clear")
+ gpu_glcheck_after("a clear")
}
# the bound read framebuffer's [0, w) x [0, h) into the bound draw framebuffer's, unscaled
-function gpu_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_blit(render3d_st, w, h, mask); return }
+function gpu_blit(w: int, h: int, mask: int) -> void {
+ if gpu_kind == GPU_VK { gvk_blit(w, h, mask); return }
gl_blit_framebuffer(0, 0, w, h, 0, 0, w, h, mask, GL_NEAREST)
- gpu_glcheck_after(render3d_st, "a blit")
+ gpu_glcheck_after("a blit")
}
# the framebuffer the finished frame is presented from (an offscreen one, headless)
-function gpu_screen_fb(render3d_st: Render3dState) -> int { if render3d_st.gpu_kind == GPU_VK { return 0 }; return gl_screen }
-function gpu_multisample(render3d_st: mut Render3dState, on: bool) -> void { if render3d_st.gpu_kind == GPU_VK { return }; gpu_gl_cap(render3d_st, GL_MULTISAMPLE, gpu_b(on)); gpu_glcheck_after(render3d_st, "multisample") }
+function gpu_screen_fb() -> int { if gpu_kind == GPU_VK { return 0 }; return gl_screen }
+function gpu_multisample(on: bool) -> void { if gpu_kind == GPU_VK { return }; gpu_gl_cap(GL_MULTISAMPLE, gpu_b(on)); gpu_glcheck_after("multisample") }
# the most samples the scene may be drawn with: the device's colour-and-depth limit on Vulkan (0 before
# it is open), 4 on OpenGL, which has always asked for up to that
-function gpu_msaa_max(render3d_st: Render3dState) -> int { if render3d_st.gpu_kind == GPU_VK { return render3d_st.gvk_msaa_max }; return 4 }
-function gpu_wireframe(render3d_st: mut Render3dState, on: bool) -> void {
- if render3d_st.gpu_kind == GPU_VK { render3d_st.gvk_wireframe = gpu_b(on); return }
+function gpu_msaa_max() -> int { if gpu_kind == GPU_VK { return gvk_msaa_max }; return 4 }
+function gpu_wireframe(on: bool) -> void {
+ if gpu_kind == GPU_VK { gvk_wireframe = gpu_b(on); return }
if on { gl_polygon_mode(GL_FRONT_AND_BACK, GL_LINE) } else { gl_polygon_mode(GL_FRONT_AND_BACK, GL_FILL) }
- gpu_glcheck_after(render3d_st, "wireframe")
+ gpu_glcheck_after("wireframe")
}
# the presented frame as RGB8, bottom row first (a photograph)
-function gpu_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: pointer) -> void {
- if render3d_st.gpu_kind == GPU_VK { gvk_read_screen(render3d_st, w, h, out); return }
+function gpu_read_screen(w: int, h: int, out: pointer) -> void {
+ if gpu_kind == GPU_VK { gvk_read_screen(w, h, out); return }
gl_bind_framebuffer(GL_READ_FRAMEBUFFER, gl_screen)
gl_pixel_storei(GL_PACK_ALIGNMENT, 1)
gl_read_pixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, out)
- gpu_glcheck_after(render3d_st, `a {w}x{h} read of the screen`)
+ gpu_glcheck_after(`a {w}x{h} read of the screen`)
}
diff --git a/packages/ludic.render3d/gpu_manifest.ludic b/packages/ludic.render3d/gpu_manifest.ludic
index 498ae78d..885c3b60 100644
--- a/packages/ludic.render3d/gpu_manifest.ludic
+++ b/packages/ludic.render3d/gpu_manifest.ludic
@@ -34,6 +34,7 @@ property GpuVariant {
i_type: []string
}
+var gpu_variants: []GpuVariant = null
function gpu_variant_new(id: string, vs: string, fs: string, defs: string) -> GpuVariant {
let v = new GpuVariant
@@ -46,8 +47,8 @@ function gpu_variant_new(id: string, vs: string, fs: string, defs: string) -> Gp
}
# Read a manifest; returns how many programs it holds (0: none, or no file).
-function gpu_manifest_load(render3d_st: mut Render3dState, path: string) -> int {
- render3d_st.gpu_variants = new []GpuVariant
+function gpu_manifest_load(path: string) -> int {
+ gpu_variants = new []GpuVariant
let text = Fs.read_text(path)
if text == null { return 0 }
let lines = Text.split(text, "\n")
@@ -67,7 +68,7 @@ function gpu_manifest_load(render3d_st: mut Render3dState, path: string) -> int
k += 1
}
cur = gpu_variant_new(f[1], f[2], f[3], defs)
- push(render3d_st.gpu_variants, cur)
+ push(gpu_variants, cur)
continue
}
if cur == null { continue }
@@ -84,18 +85,18 @@ function gpu_manifest_load(render3d_st: mut Render3dState, path: string) -> int
push(cur.i_name, f[2]); push(cur.i_loc, Text.to_int(f[3])); push(cur.i_type, f[4])
}
}
- return len(render3d_st.gpu_variants)
+ return len(gpu_variants)
}
# the variant for a program as r3d_program names it (defines with real newlines)
-function gpu_variant_find(render3d_st: Render3dState, vs: string, fs: string, defines: string) -> GpuVariant {
- return gpu_variant_find_key(render3d_st, vs, fs, Text.replace(defines, "\n", ";"))
+function gpu_variant_find(vs: string, fs: string, defines: string) -> GpuVariant {
+ return gpu_variant_find_key(vs, fs, Text.replace(defines, "\n", ";"))
}
# ... or as variants.list writes it (';' for newlines)
-function gpu_variant_find_key(render3d_st: Render3dState, vs: string, fs: string, defs: string) -> GpuVariant {
- if render3d_st.gpu_variants == null { return null }
- for i in 0 .. len(render3d_st.gpu_variants) {
- let v = render3d_st.gpu_variants[i]
+function gpu_variant_find_key(vs: string, fs: string, defs: string) -> GpuVariant {
+ if gpu_variants == null { return null }
+ for i in 0 .. len(gpu_variants) {
+ let v = gpu_variants[i]
if v.vs == vs and v.fs == fs and v.defs == defs { return v }
}
return null
diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic
index 69743591..d9971016 100644
--- a/packages/ludic.render3d/gpu_vk.ludic
+++ b/packages/ludic.render3d/gpu_vk.ludic
@@ -7,24 +7,38 @@
# here. Nothing in this file runs unless the Vulkan backend was chosen and came up.
# ---- the device ---------------------------------------------------------------------------
+var gvk_ready: bool = false
+var gvk_inst: pointer = null
+var gvk_pd: pointer = null
+var gvk_dev: pointer = null
+var gvk_queue: pointer = null
+var gvk_family: int = -1
+var gvk_mp: bytes = null # VkPhysicalDeviceMemoryProperties
+var gvk_device_name: string = ""
+var gvk_why: string = "" # why the device did not come up, for the fallback notice
+var gvk_max_aniso: int = 0x3F800000 # the device's anisotropy limit as float bits; 1.0 when it has none
+var gvk_want_surface: bool = false # set before gvk_init when the renderer will present to a window
+var gvk_has_surface: bool = false # the instance and device can make a surface and a swapchain
-function gvk_fail(render3d_st: mut Render3dState, what: string, r: int) -> bool {
- render3d_st.gvk_why = `{what} failed (VkResult {r})`
- gvk_note(render3d_st, `r3d: vulkan: {render3d_st.gvk_why}`)
+function gvk_fail(what: string, r: int) -> bool {
+ gvk_why = `{what} failed (VkResult {r})`
+ gvk_note(`r3d: vulkan: {gvk_why}`)
return false
}
# A Vulkan failure is printed, and with R3D_VK_ERRLOG= also appended to that file, opened and
# closed for each line: stdout is buffered, and a crash straight after a failure (an image that was
# never made, then used) used to take the one line that explained it with it.
-function gvk_note(render3d_st: mut Render3dState, msg: string) -> void {
+var gvk_errlog: string = null
+var gvk_errlog_read: bool = false
+function gvk_note(msg: string) -> void {
print(msg)
- if not render3d_st.gvk_errlog_read {
- render3d_st.gvk_errlog_read = true
- if r3d_env_has(render3d_st, "R3D_VK_ERRLOG") { render3d_st.gvk_errlog = r3d_env(render3d_st, "R3D_VK_ERRLOG") }
+ if not gvk_errlog_read {
+ gvk_errlog_read = true
+ if r3d_env_has("R3D_VK_ERRLOG") { gvk_errlog = r3d_env("R3D_VK_ERRLOG") }
}
- if render3d_st.gvk_errlog == null { return }
- let f = file_open(render3d_st.gvk_errlog, "ab")
+ if gvk_errlog == null { return }
+ let f = file_open(gvk_errlog, "ab")
if f == null { return }
let line = msg + "\n"
file_write(f, line, len(line))
@@ -42,12 +56,16 @@ function gvk_ext_in(props: bytes, n: int, want: string) -> bool {
# Instance, the first discrete GPU (else the first listed), its first graphics queue, and a
# device with the Tier 1 floor switched on. False, with gvk_why set, if any of it is missing:
# the caller stays on OpenGL.
+var gvk_has_mdi: bool = false # multiDrawIndirect + drawIndirectFirstInstance
+var gvk_has_dic: bool = false # drawIndirectCount
+var gvk_has_mesh: bool = false # VK_EXT_mesh_shader with its meshShader feature on
+var gvk_mesh_fn: pointer = null # vkCmdDrawMeshTasksEXT: the device's own, the interposer exports none
-function gvk_init(render3d_st: mut Render3dState) -> bool {
- if render3d_st.gvk_ready { return true }
- gsl_boot(render3d_st)
- if Vk.open() == 0 { render3d_st.gvk_why = "no Vulkan loader"; return false }
- gsl_init(render3d_st)
+function gvk_init() -> bool {
+ if gvk_ready { return true }
+ gsl_boot()
+ if Vk.open() == 0 { gvk_why = "no Vulkan loader"; return false }
+ gsl_init()
let cnt = bytes(4)
Vk.put_i32(cnt, 0, 0)
@@ -73,50 +91,50 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
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 }
- if render3d_st.gvk_has_surface {
+ gvk_has_surface = 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 gvk_want_surface and not gvk_has_surface { gvk_why = "no Win32 surface in this Vulkan instance"; return false }
+ if 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
}
# 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)
- if render3d_st.gvk_has_colorspace { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); n_iext += 1 }
+ gvk_has_colorspace = gvk_has_surface and gvk_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)
+ if gvk_has_colorspace { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); n_iext += 1 }
if n_iext > 0 {
Vk.put_i32(ici, VkInstanceCreateInfo_enabledExtensionCount, n_iext)
Vk.put_ptr(ici, VkInstanceCreateInfo_ppEnabledExtensionNames, iext_names)
}
let out = bytes(8)
var r = Vk.create_instance(ici, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateInstance", r) }
- render3d_st.gvk_inst = Vk.get_ptr(out, 0)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateInstance", r) }
+ gvk_inst = Vk.get_ptr(out, 0)
Vk.put_i32(cnt, 0, 0)
- Vk.enumerate_physical_devices(render3d_st.gvk_inst, cnt, null)
+ Vk.enumerate_physical_devices(gvk_inst, cnt, null)
let nd = Vk.get_i32(cnt, 0)
- if nd == 0 { render3d_st.gvk_why = "no Vulkan device"; return false }
+ if nd == 0 { gvk_why = "no Vulkan device"; return false }
let devs = bytes(nd * 8 + 8)
- Vk.enumerate_physical_devices(render3d_st.gvk_inst, cnt, devs)
+ Vk.enumerate_physical_devices(gvk_inst, cnt, devs)
let props = bytes(VkPhysicalDeviceProperties_sizeof)
var pick = 0
for d in 0 .. nd {
Vk.get_physical_device_properties(Vk.get_ptr(devs, d * 8), props)
if Vk.get_i32(props, VkPhysicalDeviceProperties_deviceType) == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU { pick = d; break }
}
- render3d_st.gvk_pd = Vk.get_ptr(devs, pick * 8)
- Vk.get_physical_device_properties(render3d_st.gvk_pd, props)
- render3d_st.gvk_device_name = string(Vk.at(props, VkPhysicalDeviceProperties_deviceName))
+ gvk_pd = Vk.get_ptr(devs, pick * 8)
+ Vk.get_physical_device_properties(gvk_pd, props)
+ gvk_device_name = string(Vk.at(props, VkPhysicalDeviceProperties_deviceName))
Vk.put_i32(cnt, 0, 0)
- Vk.get_physical_device_queue_family_properties(render3d_st.gvk_pd, cnt, null)
+ Vk.get_physical_device_queue_family_properties(gvk_pd, cnt, null)
let nq = Vk.get_i32(cnt, 0)
let qprops = bytes(nq * VkQueueFamilyProperties_sizeof + 8)
- Vk.get_physical_device_queue_family_properties(render3d_st.gvk_pd, cnt, qprops)
+ Vk.get_physical_device_queue_family_properties(gvk_pd, cnt, qprops)
for q in 0 .. nq {
let flags = Vk.get_i32(qprops, q * VkQueueFamilyProperties_sizeof + VkQueueFamilyProperties_queueFlags)
- if render3d_st.gvk_family < 0 and (flags & VK_QUEUE_GRAPHICS_BIT) != 0 { render3d_st.gvk_family = q }
+ if gvk_family < 0 and (flags & VK_QUEUE_GRAPHICS_BIT) != 0 { gvk_family = q }
}
- if render3d_st.gvk_family < 0 { render3d_st.gvk_why = `{render3d_st.gvk_device_name} has no graphics queue`; return false }
+ if gvk_family < 0 { gvk_why = `{gvk_device_name} has no graphics queue`; return false }
# what the device offers, then the same structs handed back asking for the floor
let f13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof)
@@ -130,16 +148,16 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
Vk.zero(f2, VkPhysicalDeviceFeatures2_sizeof)
Vk.put_i32(f2, VkPhysicalDeviceFeatures2_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)
Vk.put_ptr(f2, VkPhysicalDeviceFeatures2_pNext, f12)
- Vk.get_physical_device_features2(render3d_st.gvk_pd, f2)
+ Vk.get_physical_device_features2(gvk_pd, f2)
var missing = ""
if Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_dynamicRendering) != 1 { missing = missing + " dynamicRendering" }
if Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_synchronization2) != 1 { missing = missing + " synchronization2" }
if Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) != 1 { missing = missing + " descriptorIndexing" }
if Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) != 1 { missing = missing + " timelineSemaphore" }
- if len(missing) > 0 { render3d_st.gvk_why = `{render3d_st.gvk_device_name} lacks{missing}`; return false }
+ if len(missing) > 0 { gvk_why = `{gvk_device_name} lacks{missing}`; return false }
# anisotropic filtering is a 1.0 feature every desktop GPU has; ask for it when it is there
let aniso = Vk.get_i32(f2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_samplerAnisotropy) == 1
- if aniso { render3d_st.gvk_max_aniso = Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_maxSamplerAnisotropy) }
+ if aniso { gvk_max_aniso = Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_maxSamplerAnisotropy) }
let want13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof)
Vk.zero(want13, VkPhysicalDeviceVulkan13Features_sizeof)
Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES)
@@ -149,7 +167,7 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
if Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_maintenance4) == 1 { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_maintenance4, 1) }
# Streamline's hooks keep their own data on our objects (private data slots), and ask the device
# to have the feature rather than turning it on themselves
- if render3d_st.gsl_on and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_privateData) == 1 { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_privateData, 1) }
+ if gsl_on and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_privateData) == 1 { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_privateData, 1) }
let want12 = bytes(VkPhysicalDeviceVulkan12Features_sizeof)
Vk.zero(want12, VkPhysicalDeviceVulkan12Features_sizeof)
Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES)
@@ -164,30 +182,30 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
# GPU-driven drawing: one indirect buffer holds a layer's draws, and a compute pass may write
# how many of them there are. Asked for where the device has them; the renderer checks
# gvk_has_mdi / gvk_has_dic before it takes that path.
- render3d_st.gvk_has_mdi = Vk.get_i32(f2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_multiDrawIndirect) == 1 and Vk.get_i32(f2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_drawIndirectFirstInstance) == 1
- if render3d_st.gvk_has_mdi {
+ gvk_has_mdi = Vk.get_i32(f2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_multiDrawIndirect) == 1 and Vk.get_i32(f2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_drawIndirectFirstInstance) == 1
+ if gvk_has_mdi {
Vk.put_i32(want2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_multiDrawIndirect, 1)
Vk.put_i32(want2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_drawIndirectFirstInstance, 1)
}
- render3d_st.gvk_has_dic = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_drawIndirectCount) == 1
- if render3d_st.gvk_has_dic { Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_drawIndirectCount, 1) }
+ gvk_has_dic = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_drawIndirectCount) == 1
+ if gvk_has_dic { Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_drawIndirectCount, 1) }
# R3D_PROF: per-pass GPU time from timestamp queries (gvk_query_*). The slots are reused every few
# frames, so they are reset from the host - which is a feature to ask for.
- render3d_st.gvk_has_hqr = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_hostQueryReset) == 1 and Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampComputeAndGraphics) == 1
- if render3d_st.gvk_has_hqr {
+ gvk_has_hqr = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_hostQueryReset) == 1 and Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampComputeAndGraphics) == 1
+ if gvk_has_hqr {
Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_hostQueryReset, 1)
- render3d_st.gvk_ts_period = float_from_bits(Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampPeriod))
+ gvk_ts_period = float_from_bits(Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampPeriod))
}
Vk.put_i32(cnt, 0, 0)
- Vk.enumerate_device_extension_properties(render3d_st.gvk_pd, null, cnt, null)
+ Vk.enumerate_device_extension_properties(gvk_pd, null, cnt, null)
let nde = Vk.get_i32(cnt, 0)
let dexts = bytes(nde * VkExtensionProperties_sizeof + 8)
- Vk.enumerate_device_extension_properties(render3d_st.gvk_pd, null, cnt, dexts)
+ Vk.enumerate_device_extension_properties(gvk_pd, null, cnt, dexts)
# mesh-shader grass: the extension, and its meshShader feature asked for where the device has it.
# R3D_NO_MESH=1 leaves it off.
- render3d_st.gvk_has_mesh = false
- if gvk_ext_in(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME) and not r3d_env_has(render3d_st, "R3D_NO_MESH") {
+ gvk_has_mesh = false
+ if gvk_ext_in(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME) and not r3d_env_has("R3D_NO_MESH") {
let fm = bytes(VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof)
Vk.zero(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof)
Vk.put_i32(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT)
@@ -195,14 +213,14 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
Vk.zero(qm, VkPhysicalDeviceFeatures2_sizeof)
Vk.put_i32(qm, VkPhysicalDeviceFeatures2_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)
Vk.put_ptr(qm, VkPhysicalDeviceFeatures2_pNext, fm)
- Vk.get_physical_device_features2(render3d_st.gvk_pd, qm)
+ Vk.get_physical_device_features2(gvk_pd, qm)
if Vk.get_i32(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_meshShader) == 1 {
let wm = bytes(VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof)
Vk.zero(wm, VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof)
Vk.put_i32(wm, VkPhysicalDeviceMeshShaderFeaturesEXT_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT)
Vk.put_i32(wm, VkPhysicalDeviceMeshShaderFeaturesEXT_meshShader, 1)
Vk.put_ptr(want13, VkPhysicalDeviceVulkan13Features_pNext, wm)
- render3d_st.gvk_has_mesh = true
+ gvk_has_mesh = true
}
}
let prio = bytes(4)
@@ -210,7 +228,7 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
let qci = bytes(VkDeviceQueueCreateInfo_sizeof)
Vk.zero(qci, VkDeviceQueueCreateInfo_sizeof)
Vk.put_i32(qci, VkDeviceQueueCreateInfo_sType, VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO)
- Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueFamilyIndex, render3d_st.gvk_family)
+ Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueFamilyIndex, gvk_family)
Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueCount, 1)
Vk.put_ptr(qci, VkDeviceQueueCreateInfo_pQueuePriorities, prio)
let dext_names = bytes(48)
@@ -222,38 +240,38 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
Vk.put_i32(dci, VkDeviceCreateInfo_queueCreateInfoCount, 1)
Vk.put_ptr(dci, VkDeviceCreateInfo_pQueueCreateInfos, qci)
if gvk_ext_in(dexts, nde, "VK_KHR_portability_subset") { Vk.put_ptr(dext_names, n_dext * 8, "VK_KHR_portability_subset"); n_dext += 1 }
- if render3d_st.gvk_has_surface {
- if not gvk_ext_in(dexts, nde, VK_KHR_SWAPCHAIN_EXTENSION_NAME) { render3d_st.gvk_why = `{render3d_st.gvk_device_name} has no swapchain`; return false }
+ if gvk_has_surface {
+ if not gvk_ext_in(dexts, nde, VK_KHR_SWAPCHAIN_EXTENSION_NAME) { gvk_why = `{gvk_device_name} has no swapchain`; return false }
Vk.put_ptr(dext_names, n_dext * 8, VK_KHR_SWAPCHAIN_EXTENSION_NAME); n_dext += 1
# Reflex: Streamline adds VK_NV_low_latency2 to this device, which needs present ids it does not add
- if render3d_st.gsl_on and gvk_ext_in(dexts, nde, "VK_KHR_present_id") { Vk.put_ptr(dext_names, n_dext * 8, "VK_KHR_present_id"); n_dext += 1 }
+ if gsl_on and gvk_ext_in(dexts, nde, "VK_KHR_present_id") { Vk.put_ptr(dext_names, n_dext * 8, "VK_KHR_present_id"); n_dext += 1 }
# HDR output tells the display what the picture holds
# and only where the loader has the command: Streamline's interposer exports no vkSetHdrMetadataEXT,
# and calling its thunk there crashed the game the moment the swapchain came up HDR10
- render3d_st.gvk_has_hdr_meta = gvk_ext_in(dexts, nde, VK_EXT_HDR_METADATA_EXTENSION_NAME) and Vk.has("vkSetHdrMetadataEXT") == 1
- if render3d_st.gvk_has_hdr_meta { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_HDR_METADATA_EXTENSION_NAME); n_dext += 1 }
+ gvk_has_hdr_meta = gvk_ext_in(dexts, nde, VK_EXT_HDR_METADATA_EXTENSION_NAME) and Vk.has("vkSetHdrMetadataEXT") == 1
+ if gvk_has_hdr_meta { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_HDR_METADATA_EXTENSION_NAME); n_dext += 1 }
}
- if render3d_st.gvk_has_mesh { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_MESH_SHADER_EXTENSION_NAME); n_dext += 1 }
+ if gvk_has_mesh { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_MESH_SHADER_EXTENSION_NAME); n_dext += 1 }
if n_dext > 0 {
Vk.put_i32(dci, VkDeviceCreateInfo_enabledExtensionCount, n_dext)
Vk.put_ptr(dci, VkDeviceCreateInfo_ppEnabledExtensionNames, dext_names)
}
- r = Vk.create_device(render3d_st.gvk_pd, dci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDevice", r) }
- render3d_st.gvk_dev = Vk.get_ptr(out, 0)
- Vk.get_device_queue(render3d_st.gvk_dev, render3d_st.gvk_family, 0, out)
- render3d_st.gvk_queue = Vk.get_ptr(out, 0)
- gsl_probe_device(render3d_st, render3d_st.gvk_pd)
- if render3d_st.gvk_has_mesh {
- render3d_st.gvk_mesh_fn = Vk.get_device_proc_addr(render3d_st.gvk_dev, "vkCmdDrawMeshTasksEXT")
- if render3d_st.gvk_mesh_fn == null { render3d_st.gvk_has_mesh = false }
+ r = Vk.create_device(gvk_pd, dci, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateDevice", r) }
+ gvk_dev = Vk.get_ptr(out, 0)
+ Vk.get_device_queue(gvk_dev, gvk_family, 0, out)
+ gvk_queue = Vk.get_ptr(out, 0)
+ gsl_probe_device(gvk_pd)
+ if gvk_has_mesh {
+ gvk_mesh_fn = Vk.get_device_proc_addr(gvk_dev, "vkCmdDrawMeshTasksEXT")
+ if gvk_mesh_fn == null { gvk_has_mesh = false }
}
- render3d_st.gvk_mp = bytes(VkPhysicalDeviceMemoryProperties_sizeof)
- Vk.get_physical_device_memory_properties(render3d_st.gvk_pd, render3d_st.gvk_mp)
+ gvk_mp = bytes(VkPhysicalDeviceMemoryProperties_sizeof)
+ Vk.get_physical_device_memory_properties(gvk_pd, gvk_mp)
- if not gvk_cmd_init(render3d_st) { return false }
- render3d_st.gvk_ready = true
- print(`r3d: vulkan on {render3d_st.gvk_device_name}, anisotropy up to {gvk_aniso_x(render3d_st.gvk_max_aniso)}x, multi-draw indirect {render3d_st.gvk_has_mdi}, indirect count {render3d_st.gvk_has_dic}, mesh shaders {render3d_st.gvk_has_mesh}`)
+ if not gvk_cmd_init() { return false }
+ gvk_ready = true
+ print(`r3d: vulkan on {gvk_device_name}, anisotropy up to {gvk_aniso_x(gvk_max_aniso)}x, multi-draw indirect {gvk_has_mdi}, indirect count {gvk_has_dic}, mesh shaders {gvk_has_mesh}`)
return true
}
@@ -266,9 +284,9 @@ function gvk_aniso_x(bits: int) -> int {
# ---- memory -------------------------------------------------------------------------------
# The first memory type a resource allows with every property wanted; -1 if there is none.
-function gvk_mem_type(render3d_st: Render3dState, allowed: int, want: int) -> int {
- for t in 0 .. Vk.get_i32(render3d_st.gvk_mp, VkPhysicalDeviceMemoryProperties_memoryTypeCount) {
- let pf = Vk.get_i32(render3d_st.gvk_mp, VkPhysicalDeviceMemoryProperties_memoryTypes + t * VkMemoryType_sizeof + VkMemoryType_propertyFlags)
+function gvk_mem_type(allowed: int, want: int) -> int {
+ for t in 0 .. Vk.get_i32(gvk_mp, VkPhysicalDeviceMemoryProperties_memoryTypeCount) {
+ let pf = Vk.get_i32(gvk_mp, VkPhysicalDeviceMemoryProperties_memoryTypes + t * VkMemoryType_sizeof + VkMemoryType_propertyFlags)
if ((allowed >> t) & 1) == 1 and (pf & want) == want { return t }
}
return -1
@@ -276,11 +294,12 @@ function gvk_mem_type(render3d_st: Render3dState, allowed: int, want: int) -> in
# Memory for one resource, from its requirements (a VkMemoryRequirements). One allocation per
# resource while the backend comes up; the block allocator with sub-allocation replaces it
# before the forest and the streams are on this backend, which allocate thousands.
-function gvk_alloc(render3d_st: mut Render3dState, req: bytes, want: int) -> long {
+var gvk_n_allocs: int = 0
+function gvk_alloc(req: bytes, want: int) -> long {
let allowed = Vk.get_i32(req, VkMemoryRequirements_memoryTypeBits)
- var t = gvk_mem_type(render3d_st, allowed, want)
+ var t = gvk_mem_type(allowed, want)
# device-local is a preference; host-visible is a need
- if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(render3d_st, allowed, 0) }
+ if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(allowed, 0) }
let zero: long = 0
if t < 0 { print(`r3d: vulkan: no memory type for properties {want}`); return zero }
let mai = bytes(VkMemoryAllocateInfo_sizeof)
@@ -289,9 +308,9 @@ function gvk_alloc(render3d_st: mut Render3dState, req: bytes, want: int) -> lon
Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, Vk.get_i64(req, VkMemoryRequirements_size))
Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t)
let out = bytes(8)
- let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out)
+ let r = Vk.allocate_memory(gvk_dev, mai, null, out)
if r != VK_SUCCESS { print(`r3d: vulkan: vkAllocateMemory failed (VkResult {r})`); return zero }
- render3d_st.gvk_n_allocs += 1
+ gvk_n_allocs += 1
return gvk_handle(out)
}
@@ -308,19 +327,32 @@ function gvk_alloc(render3d_st: mut Render3dState, req: bytes, want: int) -> lon
const GVK_BLOCK_DEVICE: int = 67108864
const GVK_BLOCK_HOST: int = 67108864
const GVK_OWN_OVER: int = 16777216
+var gvk_blk_mem: []long = null
+var gvk_blk_kind: []int = null # memory type * 2, + 1 for images
+var gvk_blk_size: []int = null
+var gvk_blk_map: []pointer = null
+var gvk_fr_blk: []int = null # free ranges: block, offset, length (0 = an unused slot)
+var gvk_fr_off: []int = null
+var gvk_fr_len: []int = null
+var gvk_al_blk: []int = null # per allocation id: its block, or -1 for its own memory
+var gvk_al_mem: []long = null # its own memory when it has one
+var gvk_al_off: []int = null
+var gvk_al_len: []int = null
+var gvk_al_map: []pointer = null
+var gvk_al_spare: []int = null # ids to reuse
-function gvk_mem_init(render3d_st: mut Render3dState) -> void {
- if render3d_st.gvk_al_blk != null { return }
+function gvk_mem_init() -> void {
+ if gvk_al_blk != null { return }
let zero: long = 0
- render3d_st.gvk_blk_mem = new []long; render3d_st.gvk_blk_kind = new []int; render3d_st.gvk_blk_size = new []int; render3d_st.gvk_blk_map = new []pointer
- render3d_st.gvk_fr_blk = new []int; render3d_st.gvk_fr_off = new []int; render3d_st.gvk_fr_len = new []int
- render3d_st.gvk_al_blk = new []int; render3d_st.gvk_al_mem = new []long; render3d_st.gvk_al_off = new []int; render3d_st.gvk_al_len = new []int
- render3d_st.gvk_al_map = new []pointer; render3d_st.gvk_al_spare = new []int
- push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null)
+ gvk_blk_mem = new []long; gvk_blk_kind = new []int; gvk_blk_size = new []int; gvk_blk_map = new []pointer
+ gvk_fr_blk = new []int; gvk_fr_off = new []int; gvk_fr_len = new []int
+ gvk_al_blk = new []int; gvk_al_mem = new []long; gvk_al_off = new []int; gvk_al_len = new []int
+ gvk_al_map = new []pointer; gvk_al_spare = new []int
+ push(gvk_al_blk, -1); push(gvk_al_mem, zero); push(gvk_al_off, 0); push(gvk_al_len, 0); push(gvk_al_map, null)
}
# one vkAllocateMemory of `size` bytes of memory type t, mapped when host-visible; 0 on failure
-function gvk_mem_raw(render3d_st: mut Render3dState, t: int, size: int, host: bool, out_map: []pointer) -> long {
+function gvk_mem_raw(t: int, size: int, host: bool, out_map: []pointer) -> long {
let zero: long = 0
let mai = bytes(VkMemoryAllocateInfo_sizeof)
Vk.zero(mai, VkMemoryAllocateInfo_sizeof)
@@ -329,32 +361,32 @@ function gvk_mem_raw(render3d_st: mut Render3dState, t: int, size: int, host: bo
Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, size_l)
Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t)
let out = bytes(8)
- let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out)
- if r != VK_SUCCESS { gvk_note(render3d_st, `r3d: vulkan: vkAllocateMemory of {size} bytes failed (VkResult {r}, {render3d_st.gvk_n_allocs} allocations live)`); return zero }
- render3d_st.gvk_n_allocs += 1
+ let r = Vk.allocate_memory(gvk_dev, mai, null, out)
+ if r != VK_SUCCESS { gvk_note(`r3d: vulkan: vkAllocateMemory of {size} bytes failed (VkResult {r}, {gvk_n_allocs} allocations live)`); return zero }
+ gvk_n_allocs += 1
let mem = gvk_handle(out)
out_map[0] = null
if host {
let pp = bytes(8)
- if Vk.map_memory(render3d_st.gvk_dev, mem, zero, size_l, 0, pp) != VK_SUCCESS { gvk_note(render3d_st, "r3d: vulkan: vkMapMemory failed"); return zero }
+ if Vk.map_memory(gvk_dev, mem, zero, size_l, 0, pp) != VK_SUCCESS { gvk_note("r3d: vulkan: vkMapMemory failed"); return zero }
out_map[0] = Vk.get_ptr(pp, 0)
}
return mem
}
-function gvk_fr_put(render3d_st: mut Render3dState, b: int, off: int, len_: int) -> void {
+function gvk_fr_put(b: int, off: int, len_: int) -> void {
if len_ <= 0 { return }
- for i in 0 .. len(render3d_st.gvk_fr_blk) { if render3d_st.gvk_fr_len[i] == 0 { render3d_st.gvk_fr_blk[i] = b; render3d_st.gvk_fr_off[i] = off; render3d_st.gvk_fr_len[i] = len_; return } }
- push(render3d_st.gvk_fr_blk, b); push(render3d_st.gvk_fr_off, off); push(render3d_st.gvk_fr_len, len_)
+ for i in 0 .. len(gvk_fr_blk) { if gvk_fr_len[i] == 0 { gvk_fr_blk[i] = b; gvk_fr_off[i] = off; gvk_fr_len[i] = len_; return } }
+ push(gvk_fr_blk, b); push(gvk_fr_off, off); push(gvk_fr_len, len_)
}
# Memory for a resource from its requirements (a VkMemoryRequirements): an allocation id, 0 if none.
-function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, image: bool) -> int {
- gvk_mem_init(render3d_st)
+function gvk_mem_new(req: bytes, want: int, image: bool) -> int {
+ gvk_mem_init()
let allowed = Vk.get_i32(req, VkMemoryRequirements_memoryTypeBits)
- var t = gvk_mem_type(render3d_st, allowed, want)
- if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(render3d_st, allowed, 0) }
- if t < 0 { gvk_note(render3d_st, `r3d: vulkan: no memory type for properties {want}`); return 0 }
+ var t = gvk_mem_type(allowed, want)
+ if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(allowed, 0) }
+ if t < 0 { gvk_note(`r3d: vulkan: no memory type for properties {want}`); return 0 }
let host = (want & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0
let size = Text.to_int(string(Vk.get_i64(req, VkMemoryRequirements_size)))
var align = Text.to_int(string(Vk.get_i64(req, VkMemoryRequirements_alignment)))
@@ -368,17 +400,17 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
var kind = t * 2
if image { kind += 1 }
var i = 0
- while i < len(render3d_st.gvk_fr_blk) and blk < 0 {
- let n = render3d_st.gvk_fr_len[i]
- if n > 0 and render3d_st.gvk_blk_kind[render3d_st.gvk_fr_blk[i]] == kind {
- let start = (render3d_st.gvk_fr_off[i] + align - 1) / align * align
- let end = render3d_st.gvk_fr_off[i] + n
+ while i < len(gvk_fr_blk) and blk < 0 {
+ let n = gvk_fr_len[i]
+ if n > 0 and gvk_blk_kind[gvk_fr_blk[i]] == kind {
+ let start = (gvk_fr_off[i] + align - 1) / align * align
+ let end = gvk_fr_off[i] + n
if start + size <= end {
- blk = render3d_st.gvk_fr_blk[i]; off = start
- let front = start - render3d_st.gvk_fr_off[i]
+ blk = gvk_fr_blk[i]; off = start
+ let front = start - gvk_fr_off[i]
let back = end - (start + size)
- if front > 0 { render3d_st.gvk_fr_len[i] = front } else { render3d_st.gvk_fr_len[i] = 0 }
- gvk_fr_put(render3d_st, blk, start + size, back)
+ if front > 0 { gvk_fr_len[i] = front } else { gvk_fr_len[i] = 0 }
+ gvk_fr_put(blk, start + size, back)
}
}
i += 1
@@ -386,32 +418,32 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
if blk < 0 {
var bsize = GVK_BLOCK_DEVICE
if host { bsize = GVK_BLOCK_HOST }
- let mem = gvk_mem_raw(render3d_st, t, bsize, host, mp)
+ let mem = gvk_mem_raw(t, bsize, host, mp)
if mem != 0 {
- push(render3d_st.gvk_blk_mem, mem); push(render3d_st.gvk_blk_kind, kind); push(render3d_st.gvk_blk_size, bsize); push(render3d_st.gvk_blk_map, mp[0])
- blk = len(render3d_st.gvk_blk_mem) - 1; off = 0
- gvk_fr_put(render3d_st, blk, size, bsize - size)
+ push(gvk_blk_mem, mem); push(gvk_blk_kind, kind); push(gvk_blk_size, bsize); push(gvk_blk_map, mp[0])
+ blk = len(gvk_blk_mem) - 1; off = 0
+ gvk_fr_put(blk, size, bsize - size)
}
}
}
var own: long = 0
var map: pointer = null
if blk < 0 {
- own = gvk_mem_raw(render3d_st, t, size, host, mp)
+ own = gvk_mem_raw(t, size, host, mp)
if own == 0 { return 0 }
map = mp[0]
- } else if render3d_st.gvk_blk_map[blk] != null {
- map = mem_off(render3d_st.gvk_blk_map[blk], off)
+ } else if gvk_blk_map[blk] != null {
+ map = mem_off(gvk_blk_map[blk], off)
}
var a = 0
- if len(render3d_st.gvk_al_spare) > 0 {
- a = render3d_st.gvk_al_spare[len(render3d_st.gvk_al_spare) - 1]
- render3d_st.gvk_al_spare = gvk_list_drop_last(render3d_st.gvk_al_spare)
+ if len(gvk_al_spare) > 0 {
+ a = gvk_al_spare[len(gvk_al_spare) - 1]
+ gvk_al_spare = gvk_list_drop_last(gvk_al_spare)
} else {
- push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null)
- a = len(render3d_st.gvk_al_blk) - 1
+ push(gvk_al_blk, -1); push(gvk_al_mem, zero); push(gvk_al_off, 0); push(gvk_al_len, 0); push(gvk_al_map, null)
+ a = len(gvk_al_blk) - 1
}
- render3d_st.gvk_al_blk[a] = blk; render3d_st.gvk_al_mem[a] = own; render3d_st.gvk_al_off[a] = off; render3d_st.gvk_al_len[a] = size; render3d_st.gvk_al_map[a] = map
+ gvk_al_blk[a] = blk; gvk_al_mem[a] = own; gvk_al_off[a] = off; gvk_al_len[a] = size; gvk_al_map[a] = map
return a
}
@@ -421,85 +453,87 @@ function gvk_list_drop_last(l: []int) -> []int {
return out
}
-function gvk_mem_handle(render3d_st: Render3dState, a: int) -> long {
- if render3d_st.gvk_al_blk[a] < 0 { return render3d_st.gvk_al_mem[a] }
- return render3d_st.gvk_blk_mem[render3d_st.gvk_al_blk[a]]
+function gvk_mem_handle(a: int) -> long {
+ if gvk_al_blk[a] < 0 { return gvk_al_mem[a] }
+ return gvk_blk_mem[gvk_al_blk[a]]
}
-function gvk_mem_offset(render3d_st: Render3dState, a: int) -> long {
- let o: long = render3d_st.gvk_al_off[a]
+function gvk_mem_offset(a: int) -> long {
+ let o: long = gvk_al_off[a]
return o
}
-function gvk_mem_ptr(render3d_st: Render3dState, a: int) -> pointer { return render3d_st.gvk_al_map[a] }
+function gvk_mem_ptr(a: int) -> pointer { return gvk_al_map[a] }
# give an allocation back: its own memory is freed, a range returns to its block and merges
-function gvk_mem_free(render3d_st: mut Render3dState, a: int) -> void {
- if render3d_st.gvk_al_blk == null or a <= 0 or a >= len(render3d_st.gvk_al_blk) or render3d_st.gvk_al_len[a] == 0 { return }
- let b = render3d_st.gvk_al_blk[a]
+function gvk_mem_free(a: int) -> void {
+ if gvk_al_blk == null or a <= 0 or a >= len(gvk_al_blk) or gvk_al_len[a] == 0 { return }
+ let b = gvk_al_blk[a]
let zero: long = 0
if b < 0 {
- if render3d_st.gvk_al_map[a] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a]) }
- Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a], null)
- render3d_st.gvk_n_allocs -= 1
+ if gvk_al_map[a] != null { Vk.unmap_memory(gvk_dev, gvk_al_mem[a]) }
+ Vk.free_memory(gvk_dev, gvk_al_mem[a], null)
+ gvk_n_allocs -= 1
} else {
- var off = render3d_st.gvk_al_off[a]
- var n = render3d_st.gvk_al_len[a]
+ var off = gvk_al_off[a]
+ var n = gvk_al_len[a]
# merge with a free range that ends where this starts, and one that starts where this ends
- for i in 0 .. len(render3d_st.gvk_fr_blk) {
- if render3d_st.gvk_fr_len[i] > 0 and render3d_st.gvk_fr_blk[i] == b and render3d_st.gvk_fr_off[i] + render3d_st.gvk_fr_len[i] == off {
- off = render3d_st.gvk_fr_off[i]; n += render3d_st.gvk_fr_len[i]; render3d_st.gvk_fr_len[i] = 0
+ for i in 0 .. len(gvk_fr_blk) {
+ if gvk_fr_len[i] > 0 and gvk_fr_blk[i] == b and gvk_fr_off[i] + gvk_fr_len[i] == off {
+ off = gvk_fr_off[i]; n += gvk_fr_len[i]; gvk_fr_len[i] = 0
}
}
- for i in 0 .. len(render3d_st.gvk_fr_blk) {
- if render3d_st.gvk_fr_len[i] > 0 and render3d_st.gvk_fr_blk[i] == b and render3d_st.gvk_fr_off[i] == off + n {
- n += render3d_st.gvk_fr_len[i]; render3d_st.gvk_fr_len[i] = 0
+ for i in 0 .. len(gvk_fr_blk) {
+ if gvk_fr_len[i] > 0 and gvk_fr_blk[i] == b and gvk_fr_off[i] == off + n {
+ n += gvk_fr_len[i]; gvk_fr_len[i] = 0
}
}
- if off == 0 and n == render3d_st.gvk_blk_size[b] {
+ if off == 0 and n == gvk_blk_size[b] {
# the block is empty again: give it back, or a world swapped out keeps its memory for good
- if render3d_st.gvk_blk_map[b] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b]) }
- Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b], null)
- render3d_st.gvk_n_allocs -= 1
- render3d_st.gvk_blk_mem[b] = zero; render3d_st.gvk_blk_map[b] = null; render3d_st.gvk_blk_kind[b] = -1; render3d_st.gvk_blk_size[b] = 0
+ if gvk_blk_map[b] != null { Vk.unmap_memory(gvk_dev, gvk_blk_mem[b]) }
+ Vk.free_memory(gvk_dev, gvk_blk_mem[b], null)
+ gvk_n_allocs -= 1
+ gvk_blk_mem[b] = zero; gvk_blk_map[b] = null; gvk_blk_kind[b] = -1; gvk_blk_size[b] = 0
} else {
- gvk_fr_put(render3d_st, b, off, n)
+ gvk_fr_put(b, off, n)
}
}
- render3d_st.gvk_al_len[a] = 0; render3d_st.gvk_al_mem[a] = zero; render3d_st.gvk_al_map[a] = null; render3d_st.gvk_al_blk[a] = -1
- push(render3d_st.gvk_al_spare, a)
+ gvk_al_len[a] = 0; gvk_al_mem[a] = zero; gvk_al_map[a] = null; gvk_al_blk[a] = -1
+ push(gvk_al_spare, a)
}
function gvk_mem_id(x: long) -> int { return Text.to_int(string(x)) }
# ---- one-shot commands --------------------------------------------------------------------
# Uploads, bakes and read-backs record into a command buffer, submit it and wait. The frame
# itself does not go through here.
-function gvk_cmd_init(render3d_st: mut Render3dState) -> bool {
+var gvk_pool: long = 0
+var gvk_fence: bytes = null
+function gvk_cmd_init() -> bool {
let cpi = bytes(VkCommandPoolCreateInfo_sizeof)
Vk.zero(cpi, VkCommandPoolCreateInfo_sizeof)
Vk.put_i32(cpi, VkCommandPoolCreateInfo_sType, VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO)
Vk.put_i32(cpi, VkCommandPoolCreateInfo_flags, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT)
- Vk.put_i32(cpi, VkCommandPoolCreateInfo_queueFamilyIndex, render3d_st.gvk_family)
+ Vk.put_i32(cpi, VkCommandPoolCreateInfo_queueFamilyIndex, gvk_family)
let out = bytes(8)
- var r = Vk.create_command_pool(render3d_st.gvk_dev, cpi, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateCommandPool", r) }
- render3d_st.gvk_pool = gvk_handle(out)
+ var r = Vk.create_command_pool(gvk_dev, cpi, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateCommandPool", r) }
+ gvk_pool = gvk_handle(out)
let fci = bytes(VkFenceCreateInfo_sizeof)
Vk.zero(fci, VkFenceCreateInfo_sizeof)
Vk.put_i32(fci, VkFenceCreateInfo_sType, VK_STRUCTURE_TYPE_FENCE_CREATE_INFO)
- 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) }
+ gvk_fence = bytes(8)
+ r = Vk.create_fence(gvk_dev, fci, null, gvk_fence)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateFence", r) }
return true
}
# a command buffer, begun
-function gvk_once_begin(render3d_st: Render3dState) -> pointer {
+function gvk_once_begin() -> pointer {
let cbai = bytes(VkCommandBufferAllocateInfo_sizeof)
Vk.zero(cbai, VkCommandBufferAllocateInfo_sizeof)
Vk.put_i32(cbai, VkCommandBufferAllocateInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO)
- Vk.put_i64(cbai, VkCommandBufferAllocateInfo_commandPool, render3d_st.gvk_pool)
+ Vk.put_i64(cbai, VkCommandBufferAllocateInfo_commandPool, gvk_pool)
Vk.put_i32(cbai, VkCommandBufferAllocateInfo_level, VK_COMMAND_BUFFER_LEVEL_PRIMARY)
Vk.put_i32(cbai, VkCommandBufferAllocateInfo_commandBufferCount, 1)
let cbs = bytes(8)
- if Vk.allocate_command_buffers(render3d_st.gvk_dev, cbai, cbs) != VK_SUCCESS { return null }
+ if Vk.allocate_command_buffers(gvk_dev, cbai, cbs) != VK_SUCCESS { return null }
let cb = Vk.get_ptr(cbs, 0)
let cbbi = bytes(VkCommandBufferBeginInfo_sizeof)
Vk.zero(cbbi, VkCommandBufferBeginInfo_sizeof)
@@ -509,36 +543,36 @@ function gvk_once_begin(render3d_st: Render3dState) -> pointer {
return cb
}
# end it, submit it, wait for it, free it
-function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool {
+function gvk_once_end(cb: pointer) -> bool {
var r = Vk.end_command_buffer(cb)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
+ if r != VK_SUCCESS { return gvk_fail("vkEndCommandBuffer", r) }
let cbs = bytes(8)
Vk.put_ptr(cbs, 0, cb)
- Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_fence)
+ Vk.reset_fences(gvk_dev, 1, gvk_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_fence, 0))
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkQueueSubmit", r) }
+ r = Vk.queue_submit(gvk_queue, 1, si, Vk.get_i64(gvk_fence, 0))
+ if r != VK_SUCCESS { return gvk_fail("vkQueueSubmit", r) }
let forever: long = -1
- r = Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_fence, 1, forever)
- Vk.free_command_buffers(render3d_st.gvk_dev, render3d_st.gvk_pool, 1, cbs)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkWaitForFences", r) }
+ r = Vk.wait_for_fences(gvk_dev, 1, gvk_fence, 1, forever)
+ Vk.free_command_buffers(gvk_dev, gvk_pool, 1, cbs)
+ if r != VK_SUCCESS { return gvk_fail("vkWaitForFences", r) }
return true
}
# ---- teardown -----------------------------------------------------------------------------
-function gvk_shutdown(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gvk_ready { return }
- 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_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)
- render3d_st.gvk_ready = false
+function gvk_shutdown() -> void {
+ if not gvk_ready { return }
+ Vk.device_wait_idle(gvk_dev)
+ gsl_shutdown()
+ Vk.destroy_fence(gvk_dev, Vk.get_i64(gvk_fence, 0), null)
+ Vk.destroy_command_pool(gvk_dev, gvk_pool, null)
+ Vk.destroy_device(gvk_dev, null)
+ Vk.destroy_instance(gvk_inst, null)
+ gvk_ready = false
}
# ---- GPU timestamps (R3D_PROF) ------------------------------------------------------------
@@ -546,38 +580,42 @@ function gvk_shutdown(render3d_st: mut Render3dState) -> void {
# ends. A slot is read back PROF_RING frames after it was written and reset just before it is written
# again. On a device without host query reset or timestamps, every read says "not yet" and the report
# stays empty.
-function gvk_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> void {
+var gvk_has_hqr: bool = false
+var gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
+var gvk_qpool: long = 0
+var gvk_q_active: int = -1
+function gvk_query_new(n: int, ids: words) -> void {
for i in 0 .. n { ids[i] = i }
- if not render3d_st.gvk_has_hqr or render3d_st.gvk_dev == null { return }
+ if not gvk_has_hqr or gvk_dev == null { return }
let qci = bytes(VkQueryPoolCreateInfo_sizeof)
Vk.zero(qci, VkQueryPoolCreateInfo_sizeof)
Vk.put_i32(qci, VkQueryPoolCreateInfo_sType, VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO)
Vk.put_i32(qci, VkQueryPoolCreateInfo_queryType, VK_QUERY_TYPE_TIMESTAMP)
Vk.put_i32(qci, VkQueryPoolCreateInfo_queryCount, n * 2)
let out = bytes(8)
- if Vk.create_query_pool(render3d_st.gvk_dev, qci, null, out) != VK_SUCCESS { return }
- render3d_st.gvk_qpool = gvk_handle(out)
- Vk.reset_query_pool(render3d_st.gvk_dev, render3d_st.gvk_qpool, 0, n * 2)
+ if Vk.create_query_pool(gvk_dev, qci, null, out) != VK_SUCCESS { return }
+ gvk_qpool = gvk_handle(out)
+ Vk.reset_query_pool(gvk_dev, gvk_qpool, 0, n * 2)
}
-function gvk_query_begin(render3d_st: mut Render3dState, id: int) -> void {
- if render3d_st.gvk_qpool == 0 { return }
- Vk.reset_query_pool(render3d_st.gvk_dev, render3d_st.gvk_qpool, id * 2, 2)
- Vk.cmd_write_timestamp(gvk_frame_cb(render3d_st), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, render3d_st.gvk_qpool, id * 2)
- render3d_st.gvk_q_active = id
+function gvk_query_begin(id: int) -> void {
+ if gvk_qpool == 0 { return }
+ Vk.reset_query_pool(gvk_dev, gvk_qpool, id * 2, 2)
+ Vk.cmd_write_timestamp(gvk_frame_cb(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, gvk_qpool, id * 2)
+ gvk_q_active = id
}
-function gvk_query_end(render3d_st: mut Render3dState) -> void {
- if render3d_st.gvk_qpool == 0 or render3d_st.gvk_q_active < 0 { return }
- Vk.cmd_write_timestamp(gvk_frame_cb(render3d_st), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, render3d_st.gvk_qpool, render3d_st.gvk_q_active * 2 + 1)
- render3d_st.gvk_q_active = -1
+function gvk_query_end() -> void {
+ if gvk_qpool == 0 or gvk_q_active < 0 { return }
+ Vk.cmd_write_timestamp(gvk_frame_cb(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, gvk_qpool, gvk_q_active * 2 + 1)
+ gvk_q_active = -1
}
-function gvk_query_result(render3d_st: Render3dState, id: int, out: words) -> bool {
- if render3d_st.gvk_qpool == 0 { return false }
+function gvk_query_result(id: int, out: words) -> bool {
+ if gvk_qpool == 0 { return false }
let data = bytes(16)
let size: long = 16
let stride: long = 8
- if Vk.get_query_pool_results(render3d_st.gvk_dev, render3d_st.gvk_qpool, id * 2, 2, size, data, stride, VK_QUERY_RESULT_64_BIT) != VK_SUCCESS { return false }
+ if Vk.get_query_pool_results(gvk_dev, gvk_qpool, id * 2, 2, size, data, stride, VK_QUERY_RESULT_64_BIT) != VK_SUCCESS { return false }
let ticks = Text.to_int(string(Vk.get_i64(data, 8) - Vk.get_i64(data, 0)))
if ticks < 0 { return false }
- out[0] = int(float(ticks) * render3d_st.gvk_ts_period)
+ out[0] = int(float(ticks) * gvk_ts_period)
return true
}
diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic
index 00805c58..93010847 100644
--- a/packages/ludic.render3d/gpu_vk_draw.ludic
+++ b/packages/ludic.render3d/gpu_vk_draw.ludic
@@ -11,8 +11,14 @@
# layout (binding 0 the vertex stage's uniform block, 1 the fragment stage's, the samplers at
# the manifest's bindings from 2) and the pipeline layout over it. Programs are never compiled
# here; one whose variant is not in the manifest cannot draw, and says so once.
+var gvk_prog_var: []GpuVariant = null
+var gvk_prog_vs: []long = null
+var gvk_prog_fs: []long = null
+var gvk_prog_dsl: []long = null
+var gvk_prog_layout: []long = null
+var gvk_spv_dir: string = ""
-function gvk_read_spv(render3d_st: mut Render3dState, path: string) -> bytes {
+function gvk_read_spv(path: string) -> bytes {
let f = file_open(path, "rb")
if f == null { return null }
file_seek(f, 0, 2)
@@ -21,23 +27,24 @@ function gvk_read_spv(render3d_st: mut Render3dState, path: string) -> bytes {
let b = bytes(n + 4)
file_read(f, b, n)
file_close(f)
- render3d_st.gvk_spv_len = n
+ gvk_spv_len = n
return b
}
+var gvk_spv_len: int = 0
-function gvk_module(render3d_st: mut Render3dState, path: string) -> long {
+function gvk_module(path: string) -> long {
let zero: long = 0
- let spv = gvk_read_spv(render3d_st, path)
+ let spv = gvk_read_spv(path)
if spv == null { print(`r3d: vulkan: no SPIR-V at {path}`); return zero }
let smci = bytes(VkShaderModuleCreateInfo_sizeof)
Vk.zero(smci, VkShaderModuleCreateInfo_sizeof)
Vk.put_i32(smci, VkShaderModuleCreateInfo_sType, VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO)
- let code_size: long = render3d_st.gvk_spv_len
+ let code_size: long = gvk_spv_len
Vk.put_i64(smci, VkShaderModuleCreateInfo_codeSize, code_size)
Vk.put_ptr(smci, VkShaderModuleCreateInfo_pCode, spv)
let out = bytes(8)
- let r = Vk.create_shader_module(render3d_st.gvk_dev, smci, null, out)
- if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateShaderModule {path}`, r); return zero }
+ let r = Vk.create_shader_module(gvk_dev, smci, null, out)
+ if r != VK_SUCCESS { gvk_fail(`vkCreateShaderModule {path}`, r); return zero }
return gvk_handle(out)
}
@@ -45,27 +52,28 @@ function gvk_module(render3d_st: mut Render3dState, path: string) -> long {
# manifest key finds the variant. Returns false when there is no such variant.
# a program whose first stage is a mesh shader (its first file is *.mesh): its pipeline has no vertex
# input, and its first stage's bindings are the mesh stage's
-function gvk_stage_first(render3d_st: Render3dState, p: int) -> int {
- if render3d_st.gvk_prog_mesh != null and p < len(render3d_st.gvk_prog_mesh) and render3d_st.gvk_prog_mesh[p] == 1 { return VK_SHADER_STAGE_MESH_BIT_EXT }
+var gvk_prog_mesh: []int = null
+function gvk_stage_first(p: int) -> int {
+ if gvk_prog_mesh != null and p < len(gvk_prog_mesh) and gvk_prog_mesh[p] == 1 { return VK_SHADER_STAGE_MESH_BIT_EXT }
return VK_SHADER_STAGE_VERTEX_BIT
}
-function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_dir: string) -> bool {
+function gvk_program(p: int, key: string, spv_dir: string) -> bool {
let zero: long = 0
- if render3d_st.gvk_prog_var == null {
- render3d_st.gvk_prog_var = new []GpuVariant; render3d_st.gvk_prog_vs = new []long; render3d_st.gvk_prog_fs = new []long
- render3d_st.gvk_prog_dsl = new []long; render3d_st.gvk_prog_layout = new []long; render3d_st.gvk_prog_mesh = new []int
+ if gvk_prog_var == null {
+ gvk_prog_var = new []GpuVariant; gvk_prog_vs = new []long; gvk_prog_fs = new []long
+ gvk_prog_dsl = new []long; gvk_prog_layout = new []long; gvk_prog_mesh = new []int
}
- while len(render3d_st.gvk_prog_var) <= p {
- push(render3d_st.gvk_prog_var, null); push(render3d_st.gvk_prog_vs, zero); push(render3d_st.gvk_prog_fs, zero); push(render3d_st.gvk_prog_dsl, zero); push(render3d_st.gvk_prog_layout, zero); push(render3d_st.gvk_prog_mesh, 0)
+ while len(gvk_prog_var) <= p {
+ push(gvk_prog_var, null); push(gvk_prog_vs, zero); push(gvk_prog_fs, zero); push(gvk_prog_dsl, zero); push(gvk_prog_layout, zero); push(gvk_prog_mesh, 0)
}
let parts = Text.split(key, "|")
var defs = ""
if len(parts) > 2 { defs = parts[2] }
- let v = gpu_variant_find_key(render3d_st, parts[0], parts[1], defs)
+ let v = gpu_variant_find_key(parts[0], parts[1], defs)
if v == null { print(`r3d: vulkan: no SPIR-V variant for {key}`); return false }
- let vs = gvk_module(render3d_st, `{spv_dir}/{v.id}.vert.spv`)
- if Text.ends_with(v.vs, ".mesh") { render3d_st.gvk_prog_mesh[p] = 1 } else { render3d_st.gvk_prog_mesh[p] = 0 }
- let fs = gvk_module(render3d_st, `{spv_dir}/{v.id}.frag.spv`)
+ let vs = gvk_module(`{spv_dir}/{v.id}.vert.spv`)
+ if Text.ends_with(v.vs, ".mesh") { gvk_prog_mesh[p] = 1 } else { gvk_prog_mesh[p] = 0 }
+ let fs = gvk_module(`{spv_dir}/{v.id}.frag.spv`)
if vs == 0 or fs == 0 { return false }
let nt = len(v.t_name)
@@ -80,7 +88,7 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, 0)
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
- Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(render3d_st, p))
+ Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(p))
k += 1
}
if v.fblock >= 0 {
@@ -94,7 +102,7 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, v.t_bind[t])
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
- Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(render3d_st, p) | VK_SHADER_STAGE_FRAGMENT_BIT)
+ Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(p) | VK_SHADER_STAGE_FRAGMENT_BIT)
k += 1
}
let dslci = bytes(VkDescriptorSetLayoutCreateInfo_sizeof)
@@ -103,18 +111,18 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di
Vk.put_i32(dslci, VkDescriptorSetLayoutCreateInfo_bindingCount, nb)
Vk.put_ptr(dslci, VkDescriptorSetLayoutCreateInfo_pBindings, binds)
let dsl = bytes(8)
- var r = Vk.create_descriptor_set_layout(render3d_st.gvk_dev, dslci, null, dsl)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateDescriptorSetLayout for {key}`, r) }
+ var r = Vk.create_descriptor_set_layout(gvk_dev, dslci, null, dsl)
+ if r != VK_SUCCESS { return gvk_fail(`vkCreateDescriptorSetLayout for {key}`, r) }
let plci = bytes(VkPipelineLayoutCreateInfo_sizeof)
Vk.zero(plci, VkPipelineLayoutCreateInfo_sizeof)
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO)
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_setLayoutCount, 1)
Vk.put_ptr(plci, VkPipelineLayoutCreateInfo_pSetLayouts, dsl)
let out = bytes(8)
- r = Vk.create_pipeline_layout(render3d_st.gvk_dev, plci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreatePipelineLayout for {key}`, r) }
- render3d_st.gvk_prog_var[p] = v; render3d_st.gvk_prog_vs[p] = vs; render3d_st.gvk_prog_fs[p] = fs
- render3d_st.gvk_prog_dsl[p] = Vk.get_i64(dsl, 0); render3d_st.gvk_prog_layout[p] = gvk_handle(out)
+ r = Vk.create_pipeline_layout(gvk_dev, plci, null, out)
+ if r != VK_SUCCESS { return gvk_fail(`vkCreatePipelineLayout for {key}`, r) }
+ gvk_prog_var[p] = v; gvk_prog_vs[p] = vs; gvk_prog_fs[p] = fs
+ gvk_prog_dsl[p] = Vk.get_i64(dsl, 0); gvk_prog_layout[p] = gvk_handle(out)
return true
}
@@ -122,14 +130,18 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di
# A compute program is .comp.spv beside the manifest, with bindings fixed by convention:
# 0 the parameter block (a uniform buffer, copied into the frame's ring at the dispatch) and
# 1 .. n storage buffers. Handles start at 1.
+var gvk_cp_pipe: []long = null
+var gvk_cp_layout: []long = null
+var gvk_cp_dsl: []long = null
+var gvk_cp_nbuf: []int = null
-function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int {
+function gvk_compute_new(name: string, n_bufs: int) -> int {
let zero: long = 0
- if render3d_st.gvk_cp_pipe == null {
- render3d_st.gvk_cp_pipe = new []long; render3d_st.gvk_cp_layout = new []long; render3d_st.gvk_cp_dsl = new []long; render3d_st.gvk_cp_nbuf = new []int
- push(render3d_st.gvk_cp_pipe, zero); push(render3d_st.gvk_cp_layout, zero); push(render3d_st.gvk_cp_dsl, zero); push(render3d_st.gvk_cp_nbuf, 0)
+ if gvk_cp_pipe == null {
+ gvk_cp_pipe = new []long; gvk_cp_layout = new []long; gvk_cp_dsl = new []long; gvk_cp_nbuf = new []int
+ push(gvk_cp_pipe, zero); push(gvk_cp_layout, zero); push(gvk_cp_dsl, zero); push(gvk_cp_nbuf, 0)
}
- let module = gvk_module(render3d_st, `{render3d_st.gvk_spv_dir}/{name}.comp.spv`)
+ let module = gvk_module(`{gvk_spv_dir}/{name}.comp.spv`)
if module == 0 { return 0 }
let nb = n_bufs + 1
let bw = VkDescriptorSetLayoutBinding_sizeof
@@ -149,16 +161,16 @@ function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: i
Vk.put_i32(dslci, VkDescriptorSetLayoutCreateInfo_bindingCount, nb)
Vk.put_ptr(dslci, VkDescriptorSetLayoutCreateInfo_pBindings, binds)
let dsl = bytes(8)
- var r = Vk.create_descriptor_set_layout(render3d_st.gvk_dev, dslci, null, dsl)
- if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateDescriptorSetLayout for compute {name}`, r); return 0 }
+ var r = Vk.create_descriptor_set_layout(gvk_dev, dslci, null, dsl)
+ if r != VK_SUCCESS { gvk_fail(`vkCreateDescriptorSetLayout for compute {name}`, r); return 0 }
let plci = bytes(VkPipelineLayoutCreateInfo_sizeof)
Vk.zero(plci, VkPipelineLayoutCreateInfo_sizeof)
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO)
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_setLayoutCount, 1)
Vk.put_ptr(plci, VkPipelineLayoutCreateInfo_pSetLayouts, dsl)
let out = bytes(8)
- r = Vk.create_pipeline_layout(render3d_st.gvk_dev, plci, null, out)
- if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreatePipelineLayout for compute {name}`, r); return 0 }
+ r = Vk.create_pipeline_layout(gvk_dev, plci, null, out)
+ if r != VK_SUCCESS { gvk_fail(`vkCreatePipelineLayout for compute {name}`, r); return 0 }
let layout = gvk_handle(out)
let cpci = bytes(VkComputePipelineCreateInfo_sizeof)
Vk.zero(cpci, VkComputePipelineCreateInfo_sizeof)
@@ -169,33 +181,33 @@ function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: i
Vk.put_i64(cpci, so + VkPipelineShaderStageCreateInfo_module, module)
Vk.put_ptr(cpci, so + VkPipelineShaderStageCreateInfo_pName, "main")
Vk.put_i64(cpci, VkComputePipelineCreateInfo_layout, layout)
- r = Vk.create_compute_pipelines(render3d_st.gvk_dev, zero, 1, cpci, null, out)
- if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateComputePipelines {name}`, r); return 0 }
- push(render3d_st.gvk_cp_pipe, gvk_handle(out)); push(render3d_st.gvk_cp_layout, layout); push(render3d_st.gvk_cp_dsl, Vk.get_i64(dsl, 0)); push(render3d_st.gvk_cp_nbuf, n_bufs)
- return len(render3d_st.gvk_cp_pipe) - 1
+ r = Vk.create_compute_pipelines(gvk_dev, zero, 1, cpci, null, out)
+ if r != VK_SUCCESS { gvk_fail(`vkCreateComputePipelines {name}`, r); return 0 }
+ push(gvk_cp_pipe, gvk_handle(out)); push(gvk_cp_layout, layout); push(gvk_cp_dsl, Vk.get_i64(dsl, 0)); push(gvk_cp_nbuf, n_bufs)
+ return len(gvk_cp_pipe) - 1
}
# Record a dispatch of compute program c into the frame, between passes: params (n_params
# bytes, std140) as binding 0 and bufs[0 .. n) as bindings 1 .. n.
-function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void {
+function gvk_dispatch(c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void {
let zero: long = 0
- if render3d_st.gvk_cp_pipe == null or c <= 0 or c >= len(render3d_st.gvk_cp_pipe) { return }
- gvk_pass_end(render3d_st)
- let cb = gvk_frame_cb(render3d_st)
- let nb = render3d_st.gvk_cp_nbuf[c]
+ if gvk_cp_pipe == null or c <= 0 or c >= len(gvk_cp_pipe) { return }
+ gvk_pass_end()
+ let cb = gvk_frame_cb()
+ let nb = gvk_cp_nbuf[c]
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof)
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
- Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_dpool)
+ Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, gvk_dpool)
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
let layouts = bytes(8)
- Vk.put_i64(layouts, 0, render3d_st.gvk_cp_dsl[c])
+ Vk.put_i64(layouts, 0, gvk_cp_dsl[c])
Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts)
let sets = bytes(8)
- let r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets)
- if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (compute)", r); return }
+ let r = Vk.allocate_descriptor_sets(gvk_dev, dsai, sets)
+ if r != VK_SUCCESS { gvk_fail("vkAllocateDescriptorSets (compute)", r); return }
let set = Vk.get_i64(sets, 0)
- let at = gvk_ring_put(render3d_st, params, n_params)
+ let at = gvk_ring_put(params, n_params)
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return }
let ww = VkWriteDescriptorSet_sizeof
let bw = VkDescriptorBufferInfo_sizeof
@@ -204,13 +216,13 @@ function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n
let infos = bytes(bw * (nb + 1))
Vk.zero(infos, bw * (nb + 1))
for k in 0 .. nb + 1 {
- var buf = render3d_st.gvk_ring_buf
+ var buf = gvk_ring_buf
var off: long = 0
var range: long = 0
var kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
if k == 0 { off = at; range = n_params; kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER }
- else { buf = bufs[k - 1]; range = render3d_st.gvk_buf_size[buf]; render3d_st.gvk_buf_used[buf] = render3d_st.gvk_frame_no }
- Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_buffer, render3d_st.gvk_buf[buf])
+ else { buf = bufs[k - 1]; range = gvk_buf_size[buf]; gvk_buf_used[buf] = gvk_frame_no }
+ Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_buffer, gvk_buf[buf])
Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_offset, off)
Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_range, range)
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
@@ -220,31 +232,31 @@ function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorType, kind)
Vk.put_ptr(writes, k * ww + VkWriteDescriptorSet_pBufferInfo, mem_off(infos, k * bw))
}
- Vk.update_descriptor_sets(render3d_st.gvk_dev, nb + 1, writes, 0, null)
- Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_pipe[c])
- Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_layout[c], 0, 1, sets, 0, null)
+ Vk.update_descriptor_sets(gvk_dev, nb + 1, writes, 0, null)
+ Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, gvk_cp_pipe[c])
+ Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, gvk_cp_layout[c], 0, 1, sets, 0, null)
Vk.cmd_dispatch(cb, gx, gy, gz)
}
# R3D_VK_PROBE=1: one dispatch over a GPU-owned buffer, read back - the compute path end to end
-function gvk_compute_probe(render3d_st: mut Render3dState) -> void {
- let c = gvk_compute_new(render3d_st, "probe", 1)
+function gvk_compute_probe() -> void {
+ let c = gvk_compute_new("probe", 1)
if c == 0 { print("r3d: vulkan compute probe: FAILED (no program)"); return }
let n = 1000
- let b = gvk_buf_new(render3d_st)
+ let b = gvk_buf_new()
let data = bytes(n * 4)
for i in 0 .. n { Vk.put_i32(data, i * 4, float_bits(float(i))) }
- gvk_buf_upload(render3d_st, b, n * 4, data)
- gvk_buf_gpu_owned(render3d_st, b)
+ gvk_buf_upload(b, n * 4, data)
+ gvk_buf_gpu_owned(b)
let pr = bytes(8)
Vk.put_i32(pr, 0, n)
Vk.put_i32(pr, 4, float_bits(3.0))
let bufs = words(1)
bufs[0] = b
- gvk_dispatch(render3d_st, c, pr, 8, bufs, (n + 63) / 64, 1, 1)
- gvk_flush(render3d_st)
+ gvk_dispatch(c, pr, 8, bufs, (n + 63) / 64, 1, 1)
+ gvk_flush()
var bad = 0
- let mp = render3d_st.gvk_buf_map[b]
+ let mp = gvk_buf_map[b]
for i in 0 .. n { if float_from_bits(Vk.get_i32(mp, i * 4)) != float(i * 3) { bad += 1 } }
if bad == 0 { print(`r3d: vulkan compute probe OK ({n} values)`) } else { print(`r3d: vulkan compute probe: FAILED ({bad} of {n} wrong)`) }
}
@@ -269,6 +281,8 @@ property GvkState {
bias_units: int = 0, # float bits
wireframe: int = 0
}
+var gvk_pipe_keys: []string = null
+var gvk_pipe: []long = null
# does the mesh leave shader input `loc` unfed (no attribute recorded there)?
function gvk_input_unfed(m: Mesh, loc: int) -> bool {
@@ -283,14 +297,16 @@ function gvk_input_format(t: string) -> int {
return VK_FORMAT_R32G32B32A32_SFLOAT
}
# 64 KB of zeros, the buffer every unfed input reads (one vec4 per instance, up to 4096)
-function gvk_zero_vbuf_get(render3d_st: mut Render3dState) -> int {
- if render3d_st.gvk_zero_vbuf == 0 {
- render3d_st.gvk_zero_vbuf = gvk_buf_new(render3d_st)
+var gvk_zero_vbuf: int = 0
+var gvk_skip_said: words = null # per program: its skipped draws have been reported
+function gvk_zero_vbuf_get() -> int {
+ if gvk_zero_vbuf == 0 {
+ gvk_zero_vbuf = gvk_buf_new()
let z = bytes(65536)
Vk.zero(z, 65536)
- gvk_buf_upload(render3d_st, render3d_st.gvk_zero_vbuf, 65536, z)
+ gvk_buf_upload(gvk_zero_vbuf, 65536, z)
}
- return render3d_st.gvk_zero_vbuf
+ return gvk_zero_vbuf
}
function gvk_attr_format(comps: int, type: int, normalized: int) -> int {
@@ -351,24 +367,24 @@ function gvk_layout_key(m: Mesh) -> string {
# The pipeline for program p drawing mesh m (null for a draw without vertex input, like the
# full-screen triangle) with state st into a pass of n_color colour attachments of format
# color_fmt, a depth attachment of depth_fmt (VK_FORMAT_UNDEFINED for none) at `samples`.
-function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
+function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
let zero: long = 0
- let v = render3d_st.gvk_prog_var[p]
+ let v = gvk_prog_var[p]
if v == null { return zero }
let key = `{p}|{gvk_layout_key(m)}|{st.depth_test},{st.depth_write},{st.depth_func},{st.blend},{st.blend_src},{st.blend_dst},{st.cull},{st.cull_face},{st.color_write},{st.a2c},{st.bias},{st.bias_factor},{st.bias_units},{st.wireframe}|{n_color},{color_fmt},{depth_fmt},{samples}`
- if render3d_st.gvk_pipe_keys == null { render3d_st.gvk_pipe_keys = new []string; render3d_st.gvk_pipe = new []long }
- for i in 0 .. len(render3d_st.gvk_pipe_keys) { if render3d_st.gvk_pipe_keys[i] == key { return render3d_st.gvk_pipe[i] } }
+ if gvk_pipe_keys == null { gvk_pipe_keys = new []string; gvk_pipe = new []long }
+ for i in 0 .. len(gvk_pipe_keys) { if gvk_pipe_keys[i] == key { return gvk_pipe[i] } }
let ss = VkPipelineShaderStageCreateInfo_sizeof
let stages = bytes(ss * 2)
Vk.zero(stages, ss * 2)
Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
- Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_stage, gvk_stage_first(render3d_st, p))
- Vk.put_i64(stages, VkPipelineShaderStageCreateInfo_module, render3d_st.gvk_prog_vs[p])
+ Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_stage, gvk_stage_first(p))
+ Vk.put_i64(stages, VkPipelineShaderStageCreateInfo_module, gvk_prog_vs[p])
Vk.put_ptr(stages, VkPipelineShaderStageCreateInfo_pName, "main")
Vk.put_i32(stages, ss + VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
Vk.put_i32(stages, ss + VkPipelineShaderStageCreateInfo_stage, VK_SHADER_STAGE_FRAGMENT_BIT)
- Vk.put_i64(stages, ss + VkPipelineShaderStageCreateInfo_module, render3d_st.gvk_prog_fs[p])
+ Vk.put_i64(stages, ss + VkPipelineShaderStageCreateInfo_module, gvk_prog_fs[p])
Vk.put_ptr(stages, ss + VkPipelineShaderStageCreateInfo_pName, "main")
# vertex input: one binding per distinct buffer the mesh's attributes read, in the order met;
@@ -517,7 +533,7 @@ function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkSt
Vk.put_i32(gpci, VkGraphicsPipelineCreateInfo_stageCount, 2)
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pStages, stages)
# a mesh-shader pipeline has neither: the mesh stage makes its own vertices
- if gvk_stage_first(render3d_st, p) == VK_SHADER_STAGE_VERTEX_BIT {
+ if gvk_stage_first(p) == VK_SHADER_STAGE_VERTEX_BIT {
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pVertexInputState, vin)
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pInputAssemblyState, ias)
}
@@ -527,15 +543,15 @@ function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkSt
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pDepthStencilState, ds)
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pColorBlendState, cbs)
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pDynamicState, dys)
- Vk.put_i64(gpci, VkGraphicsPipelineCreateInfo_layout, render3d_st.gvk_prog_layout[p])
+ Vk.put_i64(gpci, VkGraphicsPipelineCreateInfo_layout, gvk_prog_layout[p])
let out = bytes(8)
- let r = Vk.create_graphics_pipelines(render3d_st.gvk_dev, zero, 1, gpci, null, out)
- if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateGraphicsPipelines for {v.vs} + {v.fs}`, r); return zero }
+ let r = Vk.create_graphics_pipelines(gvk_dev, zero, 1, gpci, null, out)
+ if r != VK_SUCCESS { gvk_fail(`vkCreateGraphicsPipelines for {v.vs} + {v.fs}`, r); return zero }
let pipe = gvk_handle(out)
# R3D_VK_PROF names each pipeline as it is made: one made during play is a stall a warm-up missed
- if gvk_prof(render3d_st) { render3d_st.gvk_n_pipe_new += 1; print(`r3d: vulkan pipeline {len(render3d_st.gvk_pipe) + 1}: {v.vs} + {v.fs}, {n_color} colour format {color_fmt}, depth {depth_fmt}, {samples}x`) }
- push(render3d_st.gvk_pipe_keys, key)
- push(render3d_st.gvk_pipe, pipe)
+ if gvk_prof() { gvk_n_pipe_new += 1; print(`r3d: vulkan pipeline {len(gvk_pipe) + 1}: {v.vs} + {v.fs}, {n_color} colour format {color_fmt}, depth {depth_fmt}, {samples}x`) }
+ push(gvk_pipe_keys, key)
+ push(gvk_pipe, pipe)
return pipe
}
@@ -545,6 +561,14 @@ function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkSt
# A uniform "location" is program * 4096 + the index of its first manifest entry, so -1 still
# means "not in this program". Writing one writes every stage that declares it; the blocks go to
# the GPU at the draw.
+var gvk_ublk_v: []bytes = null # per program: the vertex stage's block as last set
+var gvk_ublk_f: []bytes = null
+var gvk_prog_tex: []words = null # per program: the texture bound to each manifest sampler
+var gvk_prog_unit: []words = null # per program: the unit + 1 each sampler was pointed at with u_i
+var gvk_prog_dirty: []int = null # per program: its blocks or textures changed since its last set
+var gvk_set_last: []long = null # per program: the descriptor set its last draw used
+var gvk_set_frame: []int = null # per program: the frame that set belongs to
+var gvk_set_tex: []words = null # per program: the textures that set was filled with
function gvk_same(a: pointer, b: pointer, n: int) -> bool {
var i = 0
@@ -552,34 +576,34 @@ function gvk_same(a: pointer, b: pointer, n: int) -> bool {
return true
}
-function gvk_uniform_blocks(render3d_st: mut Render3dState, p: int) -> void {
- if render3d_st.gvk_ublk_v == null {
- render3d_st.gvk_ublk_v = new []bytes; render3d_st.gvk_ublk_f = new []bytes; render3d_st.gvk_prog_tex = new []words; render3d_st.gvk_prog_unit = new []words
- render3d_st.gvk_prog_dirty = new []int; render3d_st.gvk_set_last = new []long; render3d_st.gvk_set_frame = new []int; render3d_st.gvk_set_tex = new []words
+function gvk_uniform_blocks(p: int) -> void {
+ if gvk_ublk_v == null {
+ gvk_ublk_v = new []bytes; gvk_ublk_f = new []bytes; gvk_prog_tex = new []words; gvk_prog_unit = new []words
+ gvk_prog_dirty = new []int; gvk_set_last = new []long; gvk_set_frame = new []int; gvk_set_tex = new []words
}
let zero: long = 0
- while len(render3d_st.gvk_ublk_v) <= p {
- push(render3d_st.gvk_ublk_v, null); push(render3d_st.gvk_ublk_f, null); push(render3d_st.gvk_prog_tex, null); push(render3d_st.gvk_prog_unit, null)
- push(render3d_st.gvk_prog_dirty, 1); push(render3d_st.gvk_set_last, zero); push(render3d_st.gvk_set_frame, 0); push(render3d_st.gvk_set_tex, null)
+ while len(gvk_ublk_v) <= p {
+ push(gvk_ublk_v, null); push(gvk_ublk_f, null); push(gvk_prog_tex, null); push(gvk_prog_unit, null)
+ push(gvk_prog_dirty, 1); push(gvk_set_last, zero); push(gvk_set_frame, 0); push(gvk_set_tex, null)
}
- let v = render3d_st.gvk_prog_var[p]
- if v == null or render3d_st.gvk_ublk_v[p] != null or render3d_st.gvk_prog_tex[p] != null { return }
- if v.vblock > 0 { let b = bytes(v.vblock); Vk.zero(b, v.vblock); render3d_st.gvk_ublk_v[p] = b }
- if v.fblock > 0 { let b = bytes(v.fblock); Vk.zero(b, v.fblock); render3d_st.gvk_ublk_f[p] = b }
+ let v = gvk_prog_var[p]
+ if v == null or gvk_ublk_v[p] != null or gvk_prog_tex[p] != null { return }
+ if v.vblock > 0 { let b = bytes(v.vblock); Vk.zero(b, v.vblock); gvk_ublk_v[p] = b }
+ if v.fblock > 0 { let b = bytes(v.fblock); Vk.zero(b, v.fblock); gvk_ublk_f[p] = b }
let nt = len(v.t_name) + 1
let t = words(nt)
let u = words(nt)
for i in 0 .. nt { t[i] = 0; u[i] = 0 }
- render3d_st.gvk_prog_tex[p] = t
- render3d_st.gvk_prog_unit[p] = u
+ gvk_prog_tex[p] = t
+ gvk_prog_unit[p] = u
let st = words(nt)
for i in 0 .. nt { st[i] = -1 }
- render3d_st.gvk_set_tex[p] = st
+ gvk_set_tex[p] = st
}
-function gvk_uniform(render3d_st: Render3dState, p: int, name: string) -> int {
- if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) { return -1 }
- let v = render3d_st.gvk_prog_var[p]
+function gvk_uniform(p: int, name: string) -> int {
+ if gvk_prog_var == null or p <= 0 or p >= len(gvk_prog_var) { return -1 }
+ let v = gvk_prog_var[p]
if v == null { return -1 }
for i in 0 .. len(v.u_name) { if v.u_name[i] == name { return p * 4096 + i } }
# a sampler: OpenGL code points it at a texture unit once with u_i and then only binds units
@@ -589,23 +613,23 @@ function gvk_uniform(render3d_st: Render3dState, p: int, name: string) -> int {
}
# n elements of `size` bytes each from src into every block that declares the uniform at loc
-function gvk_u_set(render3d_st: mut Render3dState, loc: int, src: pointer, size: int, n: int) -> void {
+function gvk_u_set(loc: int, src: pointer, size: int, n: int) -> void {
if loc < 0 { return }
let p = loc / 4096
- let v = render3d_st.gvk_prog_var[p]
+ let v = gvk_prog_var[p]
if v == null { return }
- gvk_uniform_blocks(render3d_st, p)
+ gvk_uniform_blocks(p)
if loc % 4096 >= 2048 {
let si = loc % 4096 - 2048
- if si < len(v.t_name) and render3d_st.gvk_prog_unit[p][si] != Vk.get_i32(src, 0) + 1 { render3d_st.gvk_prog_unit[p][si] = Vk.get_i32(src, 0) + 1; render3d_st.gvk_prog_dirty[p] = 1 }
+ if si < len(v.t_name) and gvk_prog_unit[p][si] != Vk.get_i32(src, 0) + 1 { gvk_prog_unit[p][si] = Vk.get_i32(src, 0) + 1; gvk_prog_dirty[p] = 1 }
return
}
let name = v.u_name[loc % 4096]
for j in 0 .. len(v.u_name) {
if v.u_name[j] != name { continue }
- var blk = render3d_st.gvk_ublk_v[p]
+ var blk = gvk_ublk_v[p]
var cap = v.vblock
- if v.u_stage[j] == 1 { blk = render3d_st.gvk_ublk_f[p]; cap = v.fblock }
+ if v.u_stage[j] == 1 { blk = gvk_ublk_f[p]; cap = v.fblock }
if blk == null { continue }
var stride = v.u_stride[j]
if stride == 0 { stride = size }
@@ -616,19 +640,19 @@ function gvk_u_set(render3d_st: mut Render3dState, loc: int, src: pointer, size:
# the same value again (most per-draw uniforms repeat) leaves the program's set reusable
if at + size <= cap and not gvk_same(mem_off(blk, at), mem_off(src, k * size), size) {
mem_copy(mem_off(blk, at), mem_off(src, k * size), size)
- render3d_st.gvk_prog_dirty[p] = 1
+ gvk_prog_dirty[p] = 1
}
}
}
}
# a sampler uniform: the texture for the manifest binding with that name
-function gvk_bind_texture(render3d_st: mut Render3dState, p: int, name: string, tex: int) -> void {
- if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) { return }
- let v = render3d_st.gvk_prog_var[p]
+function gvk_bind_texture(p: int, name: string, tex: int) -> void {
+ if gvk_prog_var == null or p <= 0 or p >= len(gvk_prog_var) { return }
+ let v = gvk_prog_var[p]
if v == null { return }
- gvk_uniform_blocks(render3d_st, p)
- for i in 0 .. len(v.t_name) { if v.t_name[i] == name and render3d_st.gvk_prog_tex[p][i] != tex { render3d_st.gvk_prog_tex[p][i] = tex; render3d_st.gvk_prog_dirty[p] = 1 } }
+ gvk_uniform_blocks(p)
+ for i in 0 .. len(v.t_name) { if v.t_name[i] == name and gvk_prog_tex[p][i] != tex { gvk_prog_tex[p][i] = tex; gvk_prog_dirty[p] = 1 } }
}
# ---- per-frame uniform ring and descriptor sets -------------------------------------------
@@ -636,21 +660,27 @@ function gvk_bind_texture(render3d_st: mut Render3dState, p: int, name: string,
# its descriptor set comes from a pool that is reset with the frame. Both are rewound at
# gvk_frame_reset.
const GVK_RING_BYTES: int = 64 * 1024 * 1024
+var gvk_ring_buf: int = 0 # a gvk_buf handle
+var gvk_ring_off: int = 0
+var gvk_ring_align: int = 256
+var gvk_dpool: long = 0
+var gvk_white: int = 0 # a 1x1 white texture for a sampler nothing was bound to
+var gvk_msaa_max: int = 0 # samples the scene may use (gvk_frame_init reads the device's limits)
-function gvk_frame_init(render3d_st: mut Render3dState) -> bool {
+function gvk_frame_init() -> bool {
let props = bytes(VkPhysicalDeviceProperties_sizeof)
- Vk.get_physical_device_properties(render3d_st.gvk_pd, props)
+ Vk.get_physical_device_properties(gvk_pd, props)
let al = Vk.get_i64(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_minUniformBufferOffsetAlignment)
- render3d_st.gvk_ring_align = Text.to_int(string(al))
- if render3d_st.gvk_ring_align < 16 { render3d_st.gvk_ring_align = 16 }
+ gvk_ring_align = Text.to_int(string(al))
+ if gvk_ring_align < 16 { gvk_ring_align = 16 }
# MSAA: the most samples (up to the 4 the scene asks for) both a colour and a depth target can take
let lim = VkPhysicalDeviceProperties_limits
let counts = Vk.get_i32(props, lim + VkPhysicalDeviceLimits_framebufferColorSampleCounts) & Vk.get_i32(props, lim + VkPhysicalDeviceLimits_framebufferDepthSampleCounts)
- render3d_st.gvk_msaa_max = 1
- if (counts & VK_SAMPLE_COUNT_2_BIT) != 0 { render3d_st.gvk_msaa_max = 2 }
- if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { render3d_st.gvk_msaa_max = 4 }
- render3d_st.gvk_ring_buf = gvk_buf_new(render3d_st)
- if not gvk_buf_reserve(render3d_st, render3d_st.gvk_ring_buf, GVK_RING_BYTES) { return false }
+ gvk_msaa_max = 1
+ if (counts & VK_SAMPLE_COUNT_2_BIT) != 0 { gvk_msaa_max = 2 }
+ if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { gvk_msaa_max = 4 }
+ gvk_ring_buf = gvk_buf_new()
+ if not gvk_buf_reserve(gvk_ring_buf, GVK_RING_BYTES) { return false }
let sizes = bytes(VkDescriptorPoolSize_sizeof * 3)
Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, 32768)
@@ -665,27 +695,27 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool {
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 3)
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
let out = bytes(8)
- let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) }
- render3d_st.gvk_dpool = gvk_handle(out)
- if not gvk_kpool_make(render3d_st) { return false }
- render3d_st.gvk_white = gvk_tex_new(render3d_st)
+ let r = Vk.create_descriptor_pool(gvk_dev, dpci, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateDescriptorPool", r) }
+ gvk_dpool = gvk_handle(out)
+ if not gvk_kpool_make() { return false }
+ gvk_white = gvk_tex_new()
let px = bytes(4)
px[0] = 255; px[1] = 255; px[2] = 255; px[3] = 255
- return gvk_tex_storage(render3d_st, render3d_st.gvk_white, false, GL_RGBA8, 1, 1, 1, false) and gvk_tex_upload(render3d_st, render3d_st.gvk_white, GL_RGBA8, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px)
+ return gvk_tex_storage(gvk_white, false, GL_RGBA8, 1, 1, 1, false) and gvk_tex_upload(gvk_white, GL_RGBA8, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px)
}
-function gvk_frame_reset(render3d_st: mut Render3dState) -> void {
- render3d_st.gvk_ring_off = 0
- Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_dpool, 0)
+function gvk_frame_reset() -> void {
+ gvk_ring_off = 0
+ Vk.reset_descriptor_pool(gvk_dev, gvk_dpool, 0)
}
# a block into the ring; its offset, or -1 when the frame has used the whole ring
-function gvk_ring_put(render3d_st: mut Render3dState, blk: pointer, n: int) -> int {
- let at = (render3d_st.gvk_ring_off + render3d_st.gvk_ring_align - 1) / render3d_st.gvk_ring_align * render3d_st.gvk_ring_align
+function gvk_ring_put(blk: pointer, n: int) -> int {
+ let at = (gvk_ring_off + gvk_ring_align - 1) / gvk_ring_align * gvk_ring_align
if at + n > GVK_RING_BYTES { return -1 }
- mem_copy(mem_off(render3d_st.gvk_buf_map[render3d_st.gvk_ring_buf], at), blk, n)
- render3d_st.gvk_ring_off = at + n
+ mem_copy(mem_off(gvk_buf_map[gvk_ring_buf], at), blk, n)
+ gvk_ring_off = at + n
return at
}
@@ -698,8 +728,23 @@ function gvk_ring_put(render3d_st: mut Render3dState, blk: pointer, n: int) -> i
# texture's handle and generation (bumped when the image behind a handle is replaced) and its
# sampler, so a set is never matched against a texture it no longer describes.
const GVK_KPOOL_SETS: int = 8192
+var gvk_kpool: long = 0
+var gvk_sc_prog: []int = null # per cached set: its program
+var gvk_sc_koff: []int = null # per cached set: where its key starts in gvk_sc_keys
+var gvk_sc_set: []long = null
+var gvk_sc_next: []int = null # the program's next cached set, -1 at the end
+var gvk_sc_keys: []long = null # per texture of a key: handle * 65536 + generation, sampler
+var gvk_sc_head: words = null # per program (< 4096): its most recently made set, -1 if none
+var gvk_sc_tmp: []long = null # this draw's key while it is looked up
+var gvk_set_offs: bytes = null # this draw's dynamic offsets, in binding order
+var gvk_set_ndyn: int = 0
+var gvk_ub_frame: []int = null # per program: the frame its blocks were last copied into the ring
+var gvk_ub_offv: []int = null
+var gvk_ub_offf: []int = null
+var gvk_sc_made: int = 0 # sets made since start (R3D_VK_PROF)
+var gvk_default_smp: long = 0 # linear, clamped: for a texture slot nothing was bound to
-function gvk_kpool_make(render3d_st: mut Render3dState) -> bool {
+function gvk_kpool_make() -> bool {
let sizes = bytes(VkDescriptorPoolSize_sizeof * 2)
Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)
Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, GVK_KPOOL_SETS * 2)
@@ -712,100 +757,100 @@ function gvk_kpool_make(render3d_st: mut Render3dState) -> bool {
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 2)
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
let out = bytes(8)
- let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool (kept sets)", r) }
- render3d_st.gvk_kpool = gvk_handle(out)
- gvk_sc_clear(render3d_st)
+ let r = Vk.create_descriptor_pool(gvk_dev, dpci, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateDescriptorPool (kept sets)", r) }
+ gvk_kpool = gvk_handle(out)
+ gvk_sc_clear()
return true
}
-function gvk_sc_clear(render3d_st: mut Render3dState) -> void {
- render3d_st.gvk_sc_prog = new []int; render3d_st.gvk_sc_koff = new []int; render3d_st.gvk_sc_set = new []long; render3d_st.gvk_sc_next = new []int
- render3d_st.gvk_sc_keys = new []long
- if render3d_st.gvk_sc_head == null { render3d_st.gvk_sc_head = words(4096) }
- for i in 0 .. 4096 { render3d_st.gvk_sc_head[i] = -1 }
+function gvk_sc_clear() -> void {
+ gvk_sc_prog = new []int; gvk_sc_koff = new []int; gvk_sc_set = new []long; gvk_sc_next = new []int
+ gvk_sc_keys = new []long
+ if gvk_sc_head == null { gvk_sc_head = words(4096) }
+ for i in 0 .. 4096 { gvk_sc_head[i] = -1 }
}
# 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)
- Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_kpool, 0)
- gvk_sc_clear(render3d_st)
+function gvk_kpool_reset() -> void {
+ gvk_flush()
+ Vk.reset_descriptor_pool(gvk_dev, gvk_kpool, 0)
+ gvk_sc_clear()
}
-function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: int, tx_cap: int) -> long {
+function gvk_draw_set(p: int, tx: words, tx_w: int, tx_cap: int) -> long {
let zero: long = 0
- let v = render3d_st.gvk_prog_var[p]
- gvk_uniform_blocks(render3d_st, p)
- if render3d_st.gvk_sc_tmp == null {
- render3d_st.gvk_sc_tmp = new []long
- render3d_st.gvk_set_offs = bytes(16)
- render3d_st.gvk_ub_frame = new []int; render3d_st.gvk_ub_offv = new []int; render3d_st.gvk_ub_offf = new []int
+ let v = gvk_prog_var[p]
+ gvk_uniform_blocks(p)
+ if gvk_sc_tmp == null {
+ gvk_sc_tmp = new []long
+ gvk_set_offs = bytes(16)
+ gvk_ub_frame = new []int; gvk_ub_offv = new []int; gvk_ub_offf = new []int
}
- while len(render3d_st.gvk_ub_frame) <= p { push(render3d_st.gvk_ub_frame, 0); push(render3d_st.gvk_ub_offv, 0); push(render3d_st.gvk_ub_offf, 0) }
+ while len(gvk_ub_frame) <= p { push(gvk_ub_frame, 0); push(gvk_ub_offv, 0); push(gvk_ub_offf, 0) }
let nt = len(v.t_name)
# the key: every texture as this draw resolves it, and its sampler
- while len(render3d_st.gvk_sc_tmp) < nt * 2 + 2 { push(render3d_st.gvk_sc_tmp, zero) }
+ while len(gvk_sc_tmp) < nt * 2 + 2 { push(gvk_sc_tmp, zero) }
for t in 0 .. nt {
- var tex = render3d_st.gvk_prog_tex[p][t]
+ var tex = gvk_prog_tex[p][t]
# nothing bound by name: the texture on the unit the sampler was pointed at, as OpenGL reads it
- let unit1 = render3d_st.gvk_prog_unit[p][t]
- if tex <= 0 and unit1 > 0 and unit1 <= 32 and render3d_st.gpu_unit_2d != null { tex = render3d_st.gpu_unit_2d[unit1 - 1] }
- if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { tex = render3d_st.gvk_white }
+ let unit1 = gvk_prog_unit[p][t]
+ if tex <= 0 and unit1 > 0 and unit1 <= 32 and gpu_unit_2d != null { tex = gpu_unit_2d[unit1 - 1] }
+ if tex <= 0 or tex >= len(gvk_tex_image) or gvk_tex_image[tex] == 0 { tex = gvk_white }
var smp: long = 0
let o = tex * tx_w
- if tex != render3d_st.gvk_white and tex < tx_cap {
- smp = gvk_tex_sampler(render3d_st, tex, tx[o + 5], tx[o + 6], tx[o + 7], tx[o + 8], tx[o + 9], tx[o + 11])
+ if tex != gvk_white and tex < tx_cap {
+ smp = gvk_tex_sampler(tex, tx[o + 5], tx[o + 6], tx[o + 7], tx[o + 8], tx[o + 9], tx[o + 11])
} else {
# the sampler for a slot nothing was bound to, made once: looking it up by its string key on
# every draw was the costly part of the key for the shadow and depth variants
- if render3d_st.gvk_default_smp == 0 { render3d_st.gvk_default_smp = gvk_sampler(render3d_st, GL_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 0) }
- smp = render3d_st.gvk_default_smp
+ if gvk_default_smp == 0 { gvk_default_smp = gvk_sampler(GL_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 0) }
+ smp = gvk_default_smp
}
- let tg: long = tex * 65536 + render3d_st.gvk_tex_gen[tex]
- render3d_st.gvk_sc_tmp[t * 2] = tg
- render3d_st.gvk_sc_tmp[t * 2 + 1] = smp
+ let tg: long = tex * 65536 + gvk_tex_gen[tex]
+ gvk_sc_tmp[t * 2] = tg
+ gvk_sc_tmp[t * 2 + 1] = smp
}
var set: long = 0
var e = -1
- if p < 4096 { e = render3d_st.gvk_sc_head[p] }
+ if p < 4096 { e = gvk_sc_head[p] }
while e >= 0 and set == 0 {
- let ko = render3d_st.gvk_sc_koff[e]
+ let ko = gvk_sc_koff[e]
var same = true
var t = 0
- while same and t < nt * 2 { if render3d_st.gvk_sc_keys[ko + t] != render3d_st.gvk_sc_tmp[t] { same = false }; t += 1 }
- if same { set = render3d_st.gvk_sc_set[e] } else { e = render3d_st.gvk_sc_next[e] }
+ while same and t < nt * 2 { if gvk_sc_keys[ko + t] != gvk_sc_tmp[t] { same = false }; t += 1 }
+ if same { set = gvk_sc_set[e] } else { e = gvk_sc_next[e] }
}
if set == 0 {
- set = gvk_sc_make(render3d_st, p, nt)
+ set = gvk_sc_make(p, nt)
if set == 0 { return zero }
}
# the uniform blocks: copied into the ring once per frame, and again only when a value changed
- if render3d_st.gvk_prog_dirty[p] == 1 or render3d_st.gvk_ub_frame[p] != render3d_st.gvk_frame_no {
- if render3d_st.gvk_ublk_v[p] != null and v.vblock > 0 {
- let at = gvk_ring_put(render3d_st, render3d_st.gvk_ublk_v[p], v.vblock)
+ if gvk_prog_dirty[p] == 1 or gvk_ub_frame[p] != gvk_frame_no {
+ if gvk_ublk_v[p] != null and v.vblock > 0 {
+ let at = gvk_ring_put(gvk_ublk_v[p], v.vblock)
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return zero }
- render3d_st.gvk_ub_offv[p] = at
+ gvk_ub_offv[p] = at
}
- if render3d_st.gvk_ublk_f[p] != null and v.fblock > 0 {
- let at = gvk_ring_put(render3d_st, render3d_st.gvk_ublk_f[p], v.fblock)
+ if gvk_ublk_f[p] != null and v.fblock > 0 {
+ let at = gvk_ring_put(gvk_ublk_f[p], v.fblock)
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return zero }
- render3d_st.gvk_ub_offf[p] = at
+ gvk_ub_offf[p] = at
}
- render3d_st.gvk_ub_frame[p] = render3d_st.gvk_frame_no
- render3d_st.gvk_prog_dirty[p] = 0
+ gvk_ub_frame[p] = gvk_frame_no
+ gvk_prog_dirty[p] = 0
}
- render3d_st.gvk_set_ndyn = 0
- if v.vblock >= 0 { Vk.put_i32(render3d_st.gvk_set_offs, render3d_st.gvk_set_ndyn * 4, render3d_st.gvk_ub_offv[p]); render3d_st.gvk_set_ndyn += 1 }
- if v.fblock >= 0 { Vk.put_i32(render3d_st.gvk_set_offs, render3d_st.gvk_set_ndyn * 4, render3d_st.gvk_ub_offf[p]); render3d_st.gvk_set_ndyn += 1 }
- render3d_st.gvk_buf_used[render3d_st.gvk_ring_buf] = render3d_st.gvk_frame_no
+ gvk_set_ndyn = 0
+ if v.vblock >= 0 { Vk.put_i32(gvk_set_offs, gvk_set_ndyn * 4, gvk_ub_offv[p]); gvk_set_ndyn += 1 }
+ if v.fblock >= 0 { Vk.put_i32(gvk_set_offs, gvk_set_ndyn * 4, gvk_ub_offf[p]); gvk_set_ndyn += 1 }
+ gvk_buf_used[gvk_ring_buf] = gvk_frame_no
return set
}
# a new kept set for program p with the textures in gvk_sc_tmp, written and cached
-function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
+function gvk_sc_make(p: int, nt: int) -> long {
let zero: long = 0
- let v = render3d_st.gvk_prog_var[p]
+ let v = gvk_prog_var[p]
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof)
let layouts = bytes(8)
let sets = bytes(8)
@@ -814,14 +859,14 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
while tries < 2 {
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
- Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_kpool)
+ Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, gvk_kpool)
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
- Vk.put_i64(layouts, 0, render3d_st.gvk_prog_dsl[p])
+ Vk.put_i64(layouts, 0, gvk_prog_dsl[p])
Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts)
- r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets)
- if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(render3d_st); tries += 1 }
+ r = Vk.allocate_descriptor_sets(gvk_dev, dsai, sets)
+ if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(); tries += 1 }
}
- if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (kept sets)", r); return zero }
+ if r != VK_SUCCESS { gvk_fail("vkAllocateDescriptorSets (kept sets)", r); return zero }
let set = Vk.get_i64(sets, 0)
let ww = VkWriteDescriptorSet_sizeof
let writes = bytes(ww * (nt + 3))
@@ -838,7 +883,7 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
let off0: long = 0
var range: long = size
if size == 0 { range = 16 }
- Vk.put_i64(bis, at + VkDescriptorBufferInfo_buffer, render3d_st.gvk_buf[render3d_st.gvk_ring_buf])
+ Vk.put_i64(bis, at + VkDescriptorBufferInfo_buffer, gvk_buf[gvk_ring_buf])
Vk.put_i64(bis, at + VkDescriptorBufferInfo_offset, off0)
Vk.put_i64(bis, at + VkDescriptorBufferInfo_range, range)
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
@@ -852,9 +897,9 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
}
let iw = VkDescriptorImageInfo_sizeof
for t in 0 .. nt {
- let tex = Text.to_int(string(render3d_st.gvk_sc_tmp[t * 2] / 65536))
- Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_sampler, render3d_st.gvk_sc_tmp[t * 2 + 1])
- Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_imageView, render3d_st.gvk_tex_view[tex])
+ let tex = Text.to_int(string(gvk_sc_tmp[t * 2] / 65536))
+ Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_sampler, gvk_sc_tmp[t * 2 + 1])
+ Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_imageView, gvk_tex_view[tex])
Vk.put_i32(iis, t * iw + VkDescriptorImageInfo_imageLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
Vk.put_i64(writes, nw * ww + VkWriteDescriptorSet_dstSet, set)
@@ -864,14 +909,14 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
Vk.put_ptr(writes, nw * ww + VkWriteDescriptorSet_pImageInfo, mem_off(iis, t * iw))
nw += 1
}
- if nw > 0 { Vk.update_descriptor_sets(render3d_st.gvk_dev, nw, writes, 0, null) }
- let e = len(render3d_st.gvk_sc_set)
- push(render3d_st.gvk_sc_prog, p); push(render3d_st.gvk_sc_koff, len(render3d_st.gvk_sc_keys)); push(render3d_st.gvk_sc_set, set)
- for t in 0 .. nt * 2 { push(render3d_st.gvk_sc_keys, render3d_st.gvk_sc_tmp[t]) }
+ if nw > 0 { Vk.update_descriptor_sets(gvk_dev, nw, writes, 0, null) }
+ let e = len(gvk_sc_set)
+ push(gvk_sc_prog, p); push(gvk_sc_koff, len(gvk_sc_keys)); push(gvk_sc_set, set)
+ for t in 0 .. nt * 2 { push(gvk_sc_keys, gvk_sc_tmp[t]) }
var head = -1
- if p < 4096 { head = render3d_st.gvk_sc_head[p]; render3d_st.gvk_sc_head[p] = e }
- push(render3d_st.gvk_sc_next, head)
- render3d_st.gvk_sc_made += 1
+ if p < 4096 { head = gvk_sc_head[p]; gvk_sc_head[p] = e }
+ push(gvk_sc_next, head)
+ gvk_sc_made += 1
return set
}
@@ -884,42 +929,73 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
#
# Framebuffer 0 is the screen: a colour and a depth image made at gvk_screen_make. Headless that
# is all the screen there is; with a window the present copies it to the swapchain.
+var gvk_cb: pointer = null
+var gvk_in_pass: bool = false
+var gvk_fb_cur: int = 0
+var gvk_fb_read: int = 0
+var gvk_fb_draw: int = 0
+var gvk_pass_w: int = 0
+var gvk_pass_h: int = 0
+var gvk_pass_ncolor: int = 0
+var gvk_pass_cfmt: int = 0
+var gvk_pass_dfmt: int = 0
+var gvk_pass_samples: int = 1 # the attachments' sample count, which every pipeline in the pass must match
+var gvk_pass_col: words = null # the pass's colour attachments: texture, layer + 1
+var gvk_pass_dep: words = null # its depth attachment: texture, layer + 1
+var gvk_clear_bits: int = 0 # GL_COLOR_BUFFER_BIT / GL_DEPTH_BUFFER_BIT waiting for the pass
+var gvk_clear_rgba: floats = null # float bits
+var gvk_vp: words = null # x, y, w, h
+var gvk_sc: words = null # scissor on, x, y (OpenGL rows), w, h
+var gvk_screen_color: int = 0
+var gvk_screen_depth: int = 0
+var gvk_screen_w: int = 0
+var gvk_screen_h: int = 0
+var gvk_layer_views: []string = null # "tex:layer" -> index into gvk_layer_view
+var gvk_layer_view: []long = null
+var gvk_fb_ncolor: words = null # per framebuffer: colour slots drawn (draw buffers; 0 = none)
# ---- HDR output ------------------------------------------------------------------------------
# HDR10 (ST 2084 over BT.2020) when the player asks for it and the surface offers it. The screen
# image and the tonemap's LDR image go 10-bit with it; the tonemap and the overlay switch to their
# HDR10 variants (gpu_hdr_active). Headless there is no surface, so never.
+var gvk_hdr_want: bool = false # r3d_hdr: the setting
+var gvk_hdr_on: bool = false # the swapchain is HDR10 now
+var gvk_has_colorspace: bool = false # the instance took VK_EXT_swapchain_colorspace
+var gvk_has_hdr_meta: bool = false # the device took VK_EXT_hdr_metadata
# R3D_HDR=1 / 0 overrides the setting, for a desktop test
-function r3d_hdr(render3d_st: mut Render3dState, on: bool) -> void {
+function r3d_hdr(on: bool) -> void {
var want = on
- if r3d_env_has(render3d_st, "R3D_HDR") { want = Text.to_int(r3d_env(render3d_st, "R3D_HDR")) != 0 }
- if want == render3d_st.gvk_hdr_want { return }
- render3d_st.gvk_hdr_want = want
- if render3d_st.gvk_swap != 0 { render3d_st.gvk_swap_stale = true }
+ if r3d_env_has("R3D_HDR") { want = Text.to_int(r3d_env("R3D_HDR")) != 0 }
+ if want == gvk_hdr_want { return }
+ gvk_hdr_want = want
+ if gvk_swap != 0 { gvk_swap_stale = true }
}
-function gpu_hdr_active(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_hdr_on }
-function gvk_screen_fmt(render3d_st: Render3dState) -> int { if render3d_st.gvk_hdr_on { return GL_RGB10_A2 }; return GL_RGBA8 }
+function gpu_hdr_active() -> bool { return gpu_kind == GPU_VK and gvk_hdr_on }
+function gvk_screen_fmt() -> int { if gvk_hdr_on { return GL_RGB10_A2 }; return GL_RGBA8 }
# The player's calibration of their display, in nits (float bits): the brightest it shows, where the
# picture's and the interface's white sit, and how far the darkest shade is lifted. Displays differ by
# an order of magnitude - a 400-nit monitor and a 2000-nit television - and one fixed curve either
# clips the first's highlights flat or leaves the second dim.
-function r3d_hdr_peak_nits(render3d_st: Render3dState) -> float { if render3d_st.r3d_hdr_peak == 0.0 { return 1000.0 }; return render3d_st.r3d_hdr_peak }
-function r3d_hdr_paper_nits(render3d_st: Render3dState) -> float { if render3d_st.r3d_hdr_paper == 0.0 { return 200.0 }; return render3d_st.r3d_hdr_paper }
-function r3d_hdr_black_nits(render3d_st: Render3dState) -> float { return render3d_st.r3d_hdr_black }
-function r3d_hdr_calibrate(render3d_st: mut Render3dState, peak: float, paper: float, black: float) -> void {
+var r3d_hdr_peak: float = 0.0 # 0 until r3d_hdr_calibrate: 1000
+var r3d_hdr_paper: float = 0.0 # 200
+var r3d_hdr_black: float = 0.0 # 0
+function r3d_hdr_peak_nits() -> float { if r3d_hdr_peak == 0.0 { return 1000.0 }; return r3d_hdr_peak }
+function r3d_hdr_paper_nits() -> float { if r3d_hdr_paper == 0.0 { return 200.0 }; return r3d_hdr_paper }
+function r3d_hdr_black_nits() -> float { return r3d_hdr_black }
+function r3d_hdr_calibrate(peak: float, paper: float, black: float) -> void {
var pk = Math.clamp(peak, 100.0, 10000.0)
let pp = Math.clamp(paper, 80.0, 1000.0)
if pk < pp { pk = pp }
let bl = Math.clamp(black, 0.0, 5.0)
- if pk == render3d_st.r3d_hdr_peak and pp == render3d_st.r3d_hdr_paper and bl == render3d_st.r3d_hdr_black { return }
- render3d_st.r3d_hdr_peak = pk; render3d_st.r3d_hdr_paper = pp; render3d_st.r3d_hdr_black = bl
+ if pk == r3d_hdr_peak and pp == r3d_hdr_paper and bl == r3d_hdr_black { return }
+ r3d_hdr_peak = pk; r3d_hdr_paper = pp; r3d_hdr_black = bl
# the display is told what the picture now reaches
- if render3d_st.gvk_hdr_on and render3d_st.gvk_has_hdr_meta and render3d_st.gvk_swap != 0 { gvk_hdr_metadata(render3d_st) }
+ if gvk_hdr_on and gvk_has_hdr_meta and gvk_swap != 0 { gvk_hdr_metadata() }
}
# what the picture is: graded in BT.709 around D65, highlights to the calibrated peak, paper white average
-function gvk_hdr_metadata(render3d_st: Render3dState) -> void {
+function gvk_hdr_metadata() -> void {
let md = bytes(VkHdrMetadataEXT_sizeof)
Vk.zero(md, VkHdrMetadataEXT_sizeof)
Vk.put_i32(md, VkHdrMetadataEXT_sType, VK_STRUCTURE_TYPE_HDR_METADATA_EXT)
@@ -927,53 +1003,53 @@ function gvk_hdr_metadata(render3d_st: Render3dState) -> void {
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_x, float_bits(0.30)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_y, float_bits(0.60))
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_x, float_bits(0.15)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_y, float_bits(0.06))
Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_x, float_bits(0.3127)); Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_y, float_bits(0.3290))
- Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, float_bits(r3d_hdr_peak_nits(render3d_st)))
+ Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, float_bits(r3d_hdr_peak_nits()))
Vk.put_i32(md, VkHdrMetadataEXT_minLuminance, float_bits(0.001))
- Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, float_bits(r3d_hdr_peak_nits(render3d_st)))
- Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, float_bits(r3d_hdr_paper_nits(render3d_st)))
+ Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, float_bits(r3d_hdr_peak_nits()))
+ Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, float_bits(r3d_hdr_paper_nits()))
let chains = bytes(8)
- Vk.put_i64(chains, 0, render3d_st.gvk_swap)
- Vk.set_hdr_metadata_ext(render3d_st.gvk_dev, 1, chains, md)
+ Vk.put_i64(chains, 0, gvk_swap)
+ Vk.set_hdr_metadata_ext(gvk_dev, 1, chains, md)
}
-function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
- if render3d_st.gvk_vp == null {
- render3d_st.gvk_vp = words(4); render3d_st.gvk_sc = words(5); render3d_st.gvk_clear_rgba = floats(4)
- render3d_st.gvk_pass_col = words(4); render3d_st.gvk_pass_dep = words(2)
- render3d_st.gvk_fb_ncolor = words(4096)
- for i in 0 .. 4096 { render3d_st.gvk_fb_ncolor[i] = 1 }
- for i in 0 .. 5 { render3d_st.gvk_sc[i] = 0 }
+function gvk_screen_make(w: int, h: int) -> bool {
+ if gvk_vp == null {
+ gvk_vp = words(4); gvk_sc = words(5); gvk_clear_rgba = floats(4)
+ gvk_pass_col = words(4); gvk_pass_dep = words(2)
+ gvk_fb_ncolor = words(4096)
+ for i in 0 .. 4096 { gvk_fb_ncolor[i] = 1 }
+ for i in 0 .. 5 { gvk_sc[i] = 0 }
}
- render3d_st.gvk_screen_w = w
- render3d_st.gvk_screen_h = h
- if render3d_st.gvk_screen_color == 0 { render3d_st.gvk_screen_color = gvk_tex_new(render3d_st); render3d_st.gvk_screen_depth = gvk_tex_new(render3d_st) }
- render3d_st.gvk_vp[0] = 0; render3d_st.gvk_vp[1] = 0; render3d_st.gvk_vp[2] = w; render3d_st.gvk_vp[3] = h
- return gvk_tex_storage(render3d_st, render3d_st.gvk_screen_color, false, gvk_screen_fmt(render3d_st), w, h, 1, false) and gvk_tex_storage(render3d_st, render3d_st.gvk_screen_depth, false, GL_DEPTH_COMPONENT32F, w, h, 1, false)
+ gvk_screen_w = w
+ gvk_screen_h = h
+ if gvk_screen_color == 0 { gvk_screen_color = gvk_tex_new(); gvk_screen_depth = gvk_tex_new() }
+ gvk_vp[0] = 0; gvk_vp[1] = 0; gvk_vp[2] = w; gvk_vp[3] = h
+ return gvk_tex_storage(gvk_screen_color, false, gvk_screen_fmt(), w, h, 1, false) and gvk_tex_storage(gvk_screen_depth, false, GL_DEPTH_COMPONENT32F, w, h, 1, false)
}
-function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer {
- if render3d_st.gvk_cb == null {
- gvk_frame_reset(render3d_st)
- render3d_st.gvk_cb = gvk_once_begin(render3d_st)
+function gvk_frame_cb() -> pointer {
+ if gvk_cb == null {
+ gvk_frame_reset()
+ gvk_cb = gvk_once_begin()
}
- return render3d_st.gvk_cb
+ return gvk_cb
}
# The view a pass draws into: level 0 only (an attachment view has exactly one level, and a target
# the exposure measure mipmaps has several), and one layer of an array image for a cascade drawn
# on its own. Keyed by the image's generation, so a replaced image never reuses a stale view.
-function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> long {
- if layer1 == 0 and render3d_st.gvk_tex_levels[tex] <= 1 and render3d_st.gvk_tex_layers[tex] <= 1 { return render3d_st.gvk_tex_view[tex] }
- let key = `{tex}:{render3d_st.gvk_tex_gen[tex]}:{layer1}`
- if render3d_st.gvk_layer_views == null { render3d_st.gvk_layer_views = new []string; render3d_st.gvk_layer_view = new []long }
- for i in 0 .. len(render3d_st.gvk_layer_views) { if render3d_st.gvk_layer_views[i] == key { return render3d_st.gvk_layer_view[i] } }
- let depth = render3d_st.gvk_tex_vkfmt[tex] == VK_FORMAT_D32_SFLOAT
+function gvk_view_of(tex: int, layer1: int) -> long {
+ if layer1 == 0 and gvk_tex_levels[tex] <= 1 and gvk_tex_layers[tex] <= 1 { return gvk_tex_view[tex] }
+ let key = `{tex}:{gvk_tex_gen[tex]}:{layer1}`
+ if gvk_layer_views == null { gvk_layer_views = new []string; gvk_layer_view = new []long }
+ for i in 0 .. len(gvk_layer_views) { if gvk_layer_views[i] == key { return gvk_layer_view[i] } }
+ let depth = gvk_tex_vkfmt[tex] == VK_FORMAT_D32_SFLOAT
let vci = bytes(VkImageViewCreateInfo_sizeof)
Vk.zero(vci, VkImageViewCreateInfo_sizeof)
Vk.put_i32(vci, VkImageViewCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO)
- Vk.put_i64(vci, VkImageViewCreateInfo_image, render3d_st.gvk_tex_image[tex])
+ Vk.put_i64(vci, VkImageViewCreateInfo_image, gvk_tex_image[tex])
Vk.put_i32(vci, VkImageViewCreateInfo_viewType, VK_IMAGE_VIEW_TYPE_2D)
- Vk.put_i32(vci, VkImageViewCreateInfo_format, render3d_st.gvk_tex_vkfmt[tex])
+ Vk.put_i32(vci, VkImageViewCreateInfo_format, gvk_tex_vkfmt[tex])
let sr = VkImageViewCreateInfo_subresourceRange
if depth { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, 1)
@@ -981,15 +1057,15 @@ function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> l
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, 1)
let out = bytes(8)
let zero: long = 0
- if Vk.create_image_view(render3d_st.gvk_dev, vci, null, out) != VK_SUCCESS { return zero }
- push(render3d_st.gvk_layer_views, key)
- push(render3d_st.gvk_layer_view, gvk_handle(out))
+ if Vk.create_image_view(gvk_dev, vci, null, out) != VK_SUCCESS { return zero }
+ push(gvk_layer_views, key)
+ push(gvk_layer_view, gvk_handle(out))
return gvk_handle(out)
}
# the layer range a barrier for an attachment covers: the whole image unless one layer is drawn
-function gvk_att_barrier(render3d_st: Render3dState, cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void {
+function gvk_att_barrier(cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void {
# an attachment whose image was never made (no memory for it) has nothing to transition
- if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { return }
+ if tex <= 0 or tex >= len(gvk_tex_image) or gvk_tex_image[tex] == 0 { return }
let b = bytes(VkImageMemoryBarrier_sizeof)
Vk.zero(b, VkImageMemoryBarrier_sizeof)
Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER)
@@ -999,129 +1075,129 @@ function gvk_att_barrier(render3d_st: Render3dState, cb: pointer, tex: int, laye
Vk.put_i32(b, VkImageMemoryBarrier_newLayout, new_layout)
Vk.put_i32(b, VkImageMemoryBarrier_srcQueueFamilyIndex, VK_QUEUE_FAMILY_IGNORED)
Vk.put_i32(b, VkImageMemoryBarrier_dstQueueFamilyIndex, VK_QUEUE_FAMILY_IGNORED)
- Vk.put_i64(b, VkImageMemoryBarrier_image, render3d_st.gvk_tex_image[tex])
+ Vk.put_i64(b, VkImageMemoryBarrier_image, gvk_tex_image[tex])
let r = VkImageMemoryBarrier_subresourceRange
if depth { Vk.put_i32(b, r + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(b, r + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
Vk.put_i32(b, r + VkImageSubresourceRange_levelCount, 1)
- if layer1 == 0 { Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, render3d_st.gvk_tex_layers[tex]) }
+ if layer1 == 0 { Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, gvk_tex_layers[tex]) }
else { Vk.put_i32(b, r + VkImageSubresourceRange_baseArrayLayer, layer1 - 1); Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, 1) }
Vk.cmd_pipeline_barrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, null, 0, null, 1, b)
}
# fb's attachments from gpu.ludic's record (colour 0, colour 1, depth texture, depth layer + 1,
# colour rb, depth rb, samples, colour layer + 1), framebuffer 0 being the screen
-function gvk_pass_collect(render3d_st: mut Render3dState, fb: int, rec: words, rec_o: int) -> void {
- for i in 0 .. 4 { render3d_st.gvk_pass_col[i] = 0 }
- render3d_st.gvk_pass_dep[0] = 0; render3d_st.gvk_pass_dep[1] = 0
- render3d_st.gvk_pass_ncolor = 0
+function gvk_pass_collect(fb: int, rec: words, rec_o: int) -> void {
+ for i in 0 .. 4 { gvk_pass_col[i] = 0 }
+ gvk_pass_dep[0] = 0; gvk_pass_dep[1] = 0
+ gvk_pass_ncolor = 0
if fb == 0 {
- render3d_st.gvk_pass_col[0] = render3d_st.gvk_screen_color; render3d_st.gvk_pass_ncolor = 1
- render3d_st.gvk_pass_dep[0] = render3d_st.gvk_screen_depth
+ gvk_pass_col[0] = gvk_screen_color; gvk_pass_ncolor = 1
+ gvk_pass_dep[0] = gvk_screen_depth
return
}
if rec_o < 0 { return }
var want = 1
- if fb < 4096 { want = render3d_st.gvk_fb_ncolor[fb] }
+ if fb < 4096 { want = gvk_fb_ncolor[fb] }
for slot in 0 .. 2 {
if slot < want and rec[rec_o + slot] > 0 {
- render3d_st.gvk_pass_col[render3d_st.gvk_pass_ncolor * 2] = rec[rec_o + slot]
- if slot == 0 { render3d_st.gvk_pass_col[render3d_st.gvk_pass_ncolor * 2 + 1] = rec[rec_o + 7] }
- render3d_st.gvk_pass_ncolor += 1
+ gvk_pass_col[gvk_pass_ncolor * 2] = rec[rec_o + slot]
+ if slot == 0 { gvk_pass_col[gvk_pass_ncolor * 2 + 1] = rec[rec_o + 7] }
+ gvk_pass_ncolor += 1
}
}
- render3d_st.gvk_pass_dep[0] = rec[rec_o + 2]
- render3d_st.gvk_pass_dep[1] = rec[rec_o + 3]
+ gvk_pass_dep[0] = rec[rec_o + 2]
+ gvk_pass_dep[1] = rec[rec_o + 3]
}
-function gvk_pass_begin(render3d_st: mut Render3dState, rec: words, rec_o: int) -> void {
- if render3d_st.gvk_in_pass { return }
- let cb = gvk_frame_cb(render3d_st)
- gvk_pass_collect(render3d_st, render3d_st.gvk_fb_cur, rec, rec_o)
+function gvk_pass_begin(rec: words, rec_o: int) -> void {
+ if gvk_in_pass { return }
+ let cb = gvk_frame_cb()
+ gvk_pass_collect(gvk_fb_cur, rec, rec_o)
let aw = VkRenderingAttachmentInfo_sizeof
let catt = bytes(aw * 3)
Vk.zero(catt, aw * 3)
- render3d_st.gvk_pass_w = 0
- render3d_st.gvk_pass_h = 0
- render3d_st.gvk_pass_cfmt = VK_FORMAT_UNDEFINED
- render3d_st.gvk_pass_dfmt = VK_FORMAT_UNDEFINED
- render3d_st.gvk_pass_samples = 1
- for c in 0 .. render3d_st.gvk_pass_ncolor {
- let tex = render3d_st.gvk_pass_col[c * 2]
- let layer1 = render3d_st.gvk_pass_col[c * 2 + 1]
- gvk_att_barrier(render3d_st, cb, tex, layer1, false, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
+ gvk_pass_w = 0
+ gvk_pass_h = 0
+ gvk_pass_cfmt = VK_FORMAT_UNDEFINED
+ gvk_pass_dfmt = VK_FORMAT_UNDEFINED
+ gvk_pass_samples = 1
+ for c in 0 .. gvk_pass_ncolor {
+ let tex = gvk_pass_col[c * 2]
+ let layer1 = gvk_pass_col[c * 2 + 1]
+ gvk_att_barrier(cb, tex, layer1, false, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
- Vk.put_i64(catt, c * aw + VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, tex, layer1))
+ Vk.put_i64(catt, c * aw + VkRenderingAttachmentInfo_imageView, gvk_view_of(tex, layer1))
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_imageLayout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
- if (render3d_st.gvk_clear_bits & GL_COLOR_BUFFER_BIT) != 0 {
+ if (gvk_clear_bits & GL_COLOR_BUFFER_BIT) != 0 {
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_CLEAR)
- for k in 0 .. 4 { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_clearValue + k * 4, float_bits(render3d_st.gvk_clear_rgba[k])) }
+ for k in 0 .. 4 { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_clearValue + k * 4, float_bits(gvk_clear_rgba[k])) }
} else { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) }
- render3d_st.gvk_pass_cfmt = render3d_st.gvk_tex_vkfmt[tex]
- render3d_st.gvk_pass_samples = gvk_tex_samples_of(render3d_st, tex)
- if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, tex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, tex) }
+ gvk_pass_cfmt = gvk_tex_vkfmt[tex]
+ gvk_pass_samples = gvk_tex_samples_of(tex)
+ if gvk_pass_w == 0 { gvk_pass_w = gvk_tex_w(tex); gvk_pass_h = gvk_tex_h(tex) }
}
let datt = bytes(aw)
Vk.zero(datt, aw)
- let dtex = render3d_st.gvk_pass_dep[0]
+ let dtex = gvk_pass_dep[0]
if dtex > 0 {
- gvk_att_barrier(render3d_st, cb, dtex, render3d_st.gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL)
+ gvk_att_barrier(cb, dtex, gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL)
Vk.put_i32(datt, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
- Vk.put_i64(datt, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, dtex, render3d_st.gvk_pass_dep[1]))
+ Vk.put_i64(datt, VkRenderingAttachmentInfo_imageView, gvk_view_of(dtex, gvk_pass_dep[1]))
Vk.put_i32(datt, VkRenderingAttachmentInfo_imageLayout, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL)
Vk.put_i32(datt, VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
- if (render3d_st.gvk_clear_bits & GL_DEPTH_BUFFER_BIT) != 0 {
+ if (gvk_clear_bits & GL_DEPTH_BUFFER_BIT) != 0 {
Vk.put_i32(datt, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_CLEAR)
Vk.put_i32(datt, VkRenderingAttachmentInfo_clearValue, 0x3F800000)
} else { Vk.put_i32(datt, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) }
- render3d_st.gvk_pass_dfmt = VK_FORMAT_D32_SFLOAT
- render3d_st.gvk_pass_samples = gvk_tex_samples_of(render3d_st, dtex)
- if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, dtex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, dtex) }
+ gvk_pass_dfmt = VK_FORMAT_D32_SFLOAT
+ gvk_pass_samples = gvk_tex_samples_of(dtex)
+ if gvk_pass_w == 0 { gvk_pass_w = gvk_tex_w(dtex); gvk_pass_h = gvk_tex_h(dtex) }
}
- render3d_st.gvk_clear_bits = 0
+ gvk_clear_bits = 0
let ri = bytes(VkRenderingInfo_sizeof)
Vk.zero(ri, VkRenderingInfo_sizeof)
Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO)
- Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
- Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
+ Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, gvk_pass_w)
+ Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_height, gvk_pass_h)
Vk.put_i32(ri, VkRenderingInfo_layerCount, 1)
- Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, render3d_st.gvk_pass_ncolor)
+ Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, gvk_pass_ncolor)
Vk.put_ptr(ri, VkRenderingInfo_pColorAttachments, catt)
if dtex > 0 { Vk.put_ptr(ri, VkRenderingInfo_pDepthAttachment, datt) }
Vk.cmd_begin_rendering(cb, ri)
- render3d_st.gvk_in_pass = true
+ gvk_in_pass = true
}
-function gvk_pass_end(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gvk_in_pass { return }
- let cb = render3d_st.gvk_cb
+function gvk_pass_end() -> void {
+ if not gvk_in_pass { return }
+ let cb = gvk_cb
Vk.cmd_end_rendering(cb)
- for c in 0 .. render3d_st.gvk_pass_ncolor {
- gvk_att_barrier(render3d_st, cb, render3d_st.gvk_pass_col[c * 2], render3d_st.gvk_pass_col[c * 2 + 1], false, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ for c in 0 .. gvk_pass_ncolor {
+ gvk_att_barrier(cb, gvk_pass_col[c * 2], gvk_pass_col[c * 2 + 1], false, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
}
- if render3d_st.gvk_pass_dep[0] > 0 {
- gvk_att_barrier(render3d_st, cb, render3d_st.gvk_pass_dep[0], render3d_st.gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ if gvk_pass_dep[0] > 0 {
+ gvk_att_barrier(cb, gvk_pass_dep[0], gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
}
- render3d_st.gvk_in_pass = false
+ gvk_in_pass = false
}
# a clear: the load op of a pass that has not begun, an explicit clear inside one that has
-function gvk_clear(render3d_st: mut Render3dState, mask: int, rec: words, rec_o: int) -> void {
- if not render3d_st.gvk_in_pass { render3d_st.gvk_clear_bits = render3d_st.gvk_clear_bits | mask; return }
- let cb = render3d_st.gvk_cb
+function gvk_clear(mask: int, rec: words, rec_o: int) -> void {
+ if not gvk_in_pass { gvk_clear_bits = gvk_clear_bits | mask; return }
+ let cb = gvk_cb
let caw = VkClearAttachment_sizeof
let atts = bytes(caw * 4)
Vk.zero(atts, caw * 4)
var n = 0
if (mask & GL_COLOR_BUFFER_BIT) != 0 {
- for c in 0 .. render3d_st.gvk_pass_ncolor {
+ for c in 0 .. gvk_pass_ncolor {
Vk.put_i32(atts, n * caw + VkClearAttachment_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(atts, n * caw + VkClearAttachment_colorAttachment, c)
- for k in 0 .. 4 { Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue + k * 4, float_bits(render3d_st.gvk_clear_rgba[k])) }
+ for k in 0 .. 4 { Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue + k * 4, float_bits(gvk_clear_rgba[k])) }
n += 1
}
}
- if (mask & GL_DEPTH_BUFFER_BIT) != 0 and render3d_st.gvk_pass_dep[0] > 0 {
+ if (mask & GL_DEPTH_BUFFER_BIT) != 0 and gvk_pass_dep[0] > 0 {
Vk.put_i32(atts, n * caw + VkClearAttachment_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT)
Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue, 0x3F800000)
n += 1
@@ -1129,72 +1205,72 @@ function gvk_clear(render3d_st: mut Render3dState, mask: int, rec: words, rec_o:
if n == 0 { return }
let rect = bytes(VkClearRect_sizeof)
Vk.zero(rect, VkClearRect_sizeof)
- Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
- Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
+ Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_width, gvk_pass_w)
+ Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_height, gvk_pass_h)
Vk.put_i32(rect, VkClearRect_layerCount, 1)
Vk.cmd_clear_attachments(cb, n, atts, 1, rect)
}
-function gvk_tex_w(render3d_st: Render3dState, tex: int) -> int { return render3d_st.gvk_tex_dims_w[tex] }
-function gvk_tex_h(render3d_st: Render3dState, tex: int) -> int { return render3d_st.gvk_tex_dims_h[tex] }
+function gvk_tex_w(tex: int) -> int { return gvk_tex_dims_w[tex] }
+function gvk_tex_h(tex: int) -> int { return gvk_tex_dims_h[tex] }
# viewport and scissor for the draw; OpenGL's rows count from the bottom and so do a Vulkan
# target's here (no y flip), so both pass straight through
-function gvk_set_view(render3d_st: Render3dState, cb: pointer) -> void {
+function gvk_set_view(cb: pointer) -> void {
let vp = bytes(VkViewport_sizeof)
Vk.zero(vp, VkViewport_sizeof)
- Vk.put_i32(vp, VkViewport_x, float_bits(float(render3d_st.gvk_vp[0])))
- Vk.put_i32(vp, VkViewport_y, float_bits(float(render3d_st.gvk_vp[1])))
- Vk.put_i32(vp, VkViewport_width, float_bits(float(render3d_st.gvk_vp[2])))
- Vk.put_i32(vp, VkViewport_height, float_bits(float(render3d_st.gvk_vp[3])))
+ Vk.put_i32(vp, VkViewport_x, float_bits(float(gvk_vp[0])))
+ Vk.put_i32(vp, VkViewport_y, float_bits(float(gvk_vp[1])))
+ Vk.put_i32(vp, VkViewport_width, float_bits(float(gvk_vp[2])))
+ Vk.put_i32(vp, VkViewport_height, float_bits(float(gvk_vp[3])))
Vk.put_i32(vp, VkViewport_maxDepth, 0x3F800000)
Vk.cmd_set_viewport(cb, 0, 1, vp)
let sc = bytes(VkRect2D_sizeof)
Vk.zero(sc, VkRect2D_sizeof)
- if render3d_st.gvk_sc[0] == 1 {
- Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_x, render3d_st.gvk_sc[1])
- Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_y, render3d_st.gvk_sc[2])
- Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_sc[3])
- Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_sc[4])
+ if gvk_sc[0] == 1 {
+ Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_x, gvk_sc[1])
+ Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_y, gvk_sc[2])
+ Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, gvk_sc[3])
+ Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, gvk_sc[4])
} else {
- Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
- Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
+ Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, gvk_pass_w)
+ Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, gvk_pass_h)
}
Vk.cmd_set_scissor(cb, 0, 1, sc)
}
# A draw of mesh m with program p and state st: the pipeline, the view, the set, the buffers.
# first / count select vertices or indices; count 0 means the mesh's own count. instances >= 1.
-function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, first: int, count: int, instances: int, tx: words, tx_w: int, tx_cap: int, rec: words, rec_o: int) -> void {
- if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) or render3d_st.gvk_prog_var[p] == null { return }
- let prof = gvk_prof(render3d_st)
+function gvk_draw(p: int, m: Mesh, st: GvkState, first: int, count: int, instances: int, tx: words, tx_w: int, tx_cap: int, rec: words, rec_o: int) -> void {
+ if gvk_prog_var == null or p <= 0 or p >= len(gvk_prog_var) or gvk_prog_var[p] == null { return }
+ let prof = gvk_prof()
var t0: long = 0
if prof { t0 = gl_now_us() }
- gvk_pass_begin(render3d_st, rec, rec_o)
- let cb = render3d_st.gvk_cb
- let samples = render3d_st.gvk_pass_samples
- let pipe = gvk_pipeline_fast(render3d_st, p, m, st, render3d_st.gvk_pass_ncolor, render3d_st.gvk_pass_cfmt, render3d_st.gvk_pass_dfmt, samples)
+ gvk_pass_begin(rec, rec_o)
+ let cb = gvk_cb
+ let samples = gvk_pass_samples
+ let pipe = gvk_pipeline_fast(p, m, st, gvk_pass_ncolor, gvk_pass_cfmt, gvk_pass_dfmt, samples)
if pipe == 0 {
# Say so, once per program: a driver that refuses a pipeline other drivers accept (MoltenVK
# rejected every skinned one) otherwise leaves a whole layer missing from the frame in silence.
- if render3d_st.gvk_skip_said == null { render3d_st.gvk_skip_said = words(4096); for i in 0 .. 4096 { render3d_st.gvk_skip_said[i] = 0 } }
- if p < 4096 and render3d_st.gvk_skip_said[p] == 0 {
- render3d_st.gvk_skip_said[p] = 1
- print(`r3d: vulkan: no pipeline for {gpu_program_key(render3d_st, p)}; its draws are skipped`)
+ if gvk_skip_said == null { gvk_skip_said = words(4096); for i in 0 .. 4096 { gvk_skip_said[i] = 0 } }
+ if p < 4096 and gvk_skip_said[p] == 0 {
+ gvk_skip_said[p] = 1
+ print(`r3d: vulkan: no pipeline for {gpu_program_key(p)}; its draws are skipped`)
}
return
}
var t1: long = 0
- if prof { t1 = gl_now_us(); render3d_st.gvk_us_pipe = render3d_st.gvk_us_pipe + (t1 - t0) }
+ if prof { t1 = gl_now_us(); gvk_us_pipe = gvk_us_pipe + (t1 - t0) }
Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe)
- gvk_set_view(render3d_st, cb)
- let set = gvk_draw_set(render3d_st, p, tx, tx_w, tx_cap)
+ gvk_set_view(cb)
+ let set = gvk_draw_set(p, tx, tx_w, tx_cap)
if set == 0 { return }
- if prof { render3d_st.gvk_us_set = render3d_st.gvk_us_set + (gl_now_us() - t1) }
+ if prof { gvk_us_set = gvk_us_set + (gl_now_us() - t1) }
let sets = bytes(8)
Vk.put_i64(sets, 0, set)
- Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, render3d_st.gvk_prog_layout[p], 0, 1, sets, render3d_st.gvk_set_ndyn, render3d_st.gvk_set_offs)
- let v = render3d_st.gvk_prog_var[p]
+ Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, gvk_prog_layout[p], 0, 1, sets, gvk_set_ndyn, gvk_set_offs)
+ let v = gvk_prog_var[p]
if m != null and m.attrs != null {
# the same bindings, in the same order, as gvk_pipeline gave the layout
let bufs = bytes(8 * (GPU_MAX_VBUFS + 1))
@@ -1212,45 +1288,45 @@ function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState,
for q in 0 .. nbd { if seen[q] == m.attrs[o] { known = true } }
if not known and nbd < GPU_MAX_VBUFS {
seen[nbd] = m.attrs[o]
- Vk.put_i64(bufs, nbd * 8, render3d_st.gvk_buf[m.attrs[o]])
- render3d_st.gvk_buf_used[m.attrs[o]] = render3d_st.gvk_frame_no
+ Vk.put_i64(bufs, nbd * 8, gvk_buf[m.attrs[o]])
+ gvk_buf_used[m.attrs[o]] = gvk_frame_no
nbd += 1
}
}
var unfed = false
for q in 0 .. len(v.i_loc) { if gvk_input_unfed(m, v.i_loc[q]) { unfed = true } }
if unfed and nbd <= GPU_MAX_VBUFS {
- let zb = gvk_zero_vbuf_get(render3d_st)
- Vk.put_i64(bufs, nbd * 8, render3d_st.gvk_buf[zb])
- render3d_st.gvk_buf_used[zb] = render3d_st.gvk_frame_no
+ let zb = gvk_zero_vbuf_get()
+ Vk.put_i64(bufs, nbd * 8, gvk_buf[zb])
+ gvk_buf_used[zb] = gvk_frame_no
nbd += 1
}
if nbd > 0 { Vk.cmd_bind_vertex_buffers(cb, 0, nbd, bufs, offs) }
}
var n = count
if n == 0 and m != null { n = m.count }
- if render3d_st.gvk_mesh_x > 0 {
+ if gvk_mesh_x > 0 {
# a mesh-shader dispatch (gpu_draw_mesh_tasks): the device's command, through a pointer
- Vk.sl_call_piii(render3d_st.gvk_mesh_fn, cb, render3d_st.gvk_mesh_x, render3d_st.gvk_mesh_y, render3d_st.gvk_mesh_z)
- if prof { render3d_st.gvk_n_draws += 1; render3d_st.gvk_us_draw = render3d_st.gvk_us_draw + (gl_now_us() - t0) }
+ Vk.sl_call_piii(gvk_mesh_fn, cb, gvk_mesh_x, gvk_mesh_y, gvk_mesh_z)
+ if prof { gvk_n_draws += 1; gvk_us_draw = gvk_us_draw + (gl_now_us() - t0) }
return
}
if m != null and m.ebo != 0 {
let zero: long = 0
var itype = VK_INDEX_TYPE_UINT32
if m.itype == GL_UNSIGNED_SHORT { itype = VK_INDEX_TYPE_UINT16 }
- Vk.cmd_bind_index_buffer(cb, render3d_st.gvk_buf[m.ebo], zero, itype)
- render3d_st.gvk_buf_used[m.ebo] = render3d_st.gvk_frame_no
- if render3d_st.gvk_ind_buf > 0 {
+ Vk.cmd_bind_index_buffer(cb, gvk_buf[m.ebo], zero, itype)
+ gvk_buf_used[m.ebo] = gvk_frame_no
+ if gvk_ind_buf > 0 {
# the draws are records in a buffer (gvk_draw_indirect_now); the GPU may have written them
- let ioff: long = render3d_st.gvk_ind_off
- render3d_st.gvk_buf_used[render3d_st.gvk_ind_buf] = render3d_st.gvk_frame_no
- if render3d_st.gvk_ind_cbuf > 0 and render3d_st.gvk_has_dic {
- let coff: long = render3d_st.gvk_ind_coff
- render3d_st.gvk_buf_used[render3d_st.gvk_ind_cbuf] = render3d_st.gvk_frame_no
- Vk.cmd_draw_indexed_indirect_count(cb, render3d_st.gvk_buf[render3d_st.gvk_ind_buf], ioff, render3d_st.gvk_buf[render3d_st.gvk_ind_cbuf], coff, render3d_st.gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof)
+ let ioff: long = gvk_ind_off
+ gvk_buf_used[gvk_ind_buf] = gvk_frame_no
+ if gvk_ind_cbuf > 0 and gvk_has_dic {
+ let coff: long = gvk_ind_coff
+ gvk_buf_used[gvk_ind_cbuf] = gvk_frame_no
+ Vk.cmd_draw_indexed_indirect_count(cb, gvk_buf[gvk_ind_buf], ioff, gvk_buf[gvk_ind_cbuf], coff, gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof)
} else {
- Vk.cmd_draw_indexed_indirect(cb, render3d_st.gvk_buf[render3d_st.gvk_ind_buf], ioff, render3d_st.gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof)
+ Vk.cmd_draw_indexed_indirect(cb, gvk_buf[gvk_ind_buf], ioff, gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof)
}
} else {
Vk.cmd_draw_indexed(cb, n, instances, first, 0, 0)
@@ -1258,46 +1334,46 @@ function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState,
} else {
Vk.cmd_draw(cb, n, instances, first, 0)
}
- if prof { render3d_st.gvk_n_draws += 1; render3d_st.gvk_us_draw = render3d_st.gvk_us_draw + (gl_now_us() - t0) }
+ if prof { gvk_n_draws += 1; gvk_us_draw = gvk_us_draw + (gl_now_us() - t0) }
}
# The frame so far, submitted and waited for, so work that submits on its own - an upload, a
# read-back, a new or freed image or buffer - happens after the draws recorded before it, in the
# order OpenGL would have done them. Costs a submit per such call while the backend comes up.
-function gvk_flush(render3d_st: mut Render3dState) -> void {
- if render3d_st.gvk_cb == null { return }
- render3d_st.gvk_n_flush += 1
- gvk_pass_end(render3d_st)
- gvk_once_end(render3d_st, render3d_st.gvk_cb)
- render3d_st.gvk_cb = null
- render3d_st.gvk_frame_no += 1
- gvk_retire_flush(render3d_st)
+function gvk_flush() -> void {
+ if gvk_cb == null { return }
+ gvk_n_flush += 1
+ gvk_pass_end()
+ gvk_once_end(gvk_cb)
+ gvk_cb = null
+ gvk_frame_no += 1
+ gvk_retire_flush()
}
# The finished frame: submitted and waited for. With a window, the screen image is blitted into
# the swapchain's next image first - flipped, since the screen image keeps OpenGL's bottom-up rows
# - 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_cb == null { return }
- gvk_pass_end(render3d_st)
- gvk_once_end(render3d_st, render3d_st.gvk_cb)
- render3d_st.gvk_cb = null
- render3d_st.gvk_frame_no += 1
- gvk_retire_flush(render3d_st)
+function gvk_present() -> void {
+ gvk_prof_frame()
+ if gvk_swap != 0 { gvk_present_window(); gvk_frame_no += 1; gvk_retire_flush(); return }
+ if gvk_cb == null { return }
+ gvk_pass_end()
+ gvk_once_end(gvk_cb)
+ gvk_cb = null
+ gvk_frame_no += 1
+ gvk_retire_flush()
}
# The screen as a binary PPM, top row first. The frame so far is finished first, then read back;
# row 0 of the image is OpenGL's bottom row, so rows are written last to first, as gl_screenshot does.
-function gvk_screenshot(render3d_st: mut Render3dState, path: string) -> bool {
+function gvk_screenshot(path: string) -> bool {
# the screen image is PQ-encoded 10-bit while HDR is on, which an 8-bit PPM would misread
- if render3d_st.gvk_hdr_on { print("r3d: vulkan: screenshots are SDR only - turn HDR output off to take one"); return false }
- gvk_present(render3d_st)
- let w = render3d_st.gvk_screen_w
- let h = render3d_st.gvk_screen_h
+ if gvk_hdr_on { print("r3d: vulkan: screenshots are SDR only - turn HDR output off to take one"); return false }
+ gvk_present()
+ let w = gvk_screen_w
+ let h = gvk_screen_h
let px = bytes(w * h * 4)
- if not gvk_tex_read(render3d_st, render3d_st.gvk_screen_color, GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px) { return false }
+ if not gvk_tex_read(gvk_screen_color, GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px) { return false }
let f = file_open(path, "wb")
if f == null { return false }
let hdr = `P6\n{w} {h}\n255\n`
@@ -1317,150 +1393,162 @@ function gvk_screenshot(render3d_st: mut Render3dState, path: string) -> bool {
}
# ---- what gpu.ludic's Vulkan branches call -----------------------------------------------------
+var gvk_fb_counter: int = 0
+var gvk_prog_counter: int = 0
+var gvk_wireframe: int = 0
+var gvk_state: GvkState = null
# the manifest the programs are looked up in, from the renderer's own shader directory
-function gvk_manifest(render3d_st: mut Render3dState) -> bool {
- r3d_find_root(render3d_st)
- render3d_st.gvk_spv_dir = `{render3d_st.r3d_root}/shaders/spv`
- return gpu_manifest_load(render3d_st, `{render3d_st.gvk_spv_dir}/manifest.txt`) > 0 and len(render3d_st.gpu_variants) > 0
+function gvk_manifest() -> bool {
+ r3d_find_root()
+ gvk_spv_dir = `{r3d_root}/shaders/spv`
+ return gpu_manifest_load(`{gvk_spv_dir}/manifest.txt`) > 0 and len(gpu_variants) > 0
}
# The screen: a colour and a depth image. Headless they are the asked-for size; with a window
# they are its client area in pixels, and the swapchain is made on it.
-function gvk_open(render3d_st: mut Render3dState, w: int, h: int, title: string) -> bool {
+function gvk_open(w: int, h: int, title: string) -> bool {
gl_w = w
gl_h = h
if is_windowed() {
win_open(w, h, 1, title) # the window exists before main: this retitles it
win_gl_resize(w, h)
- 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_w = render3d_st.gvk_size_buf[0]; gl_h = render3d_st.gvk_size_buf[1] }
+ if gvk_size_buf == null { gvk_size_buf = words(4) }
+ win_gl_drawable(gvk_size_buf)
+ if gvk_size_buf[0] > 0 and gvk_size_buf[1] > 0 { gl_w = gvk_size_buf[0]; gl_h = gvk_size_buf[1] }
gl_scale = win_gl_scale()
}
- if not gvk_frame_init(render3d_st) { return false }
- if not gvk_screen_make(render3d_st, gl_w, gl_h) { return false }
- if r3d_env_has(render3d_st, "R3D_VK_PROBE") { gvk_compute_probe(render3d_st) }
- if is_windowed() { return gvk_swap_make(render3d_st, gl_w, gl_h) }
+ if not gvk_frame_init() { return false }
+ if not gvk_screen_make(gl_w, gl_h) { return false }
+ if r3d_env_has("R3D_VK_PROBE") { gvk_compute_probe() }
+ if is_windowed() { return gvk_swap_make(gl_w, gl_h) }
return true
}
# a program handle for a variant; the key is gpu_program's, so gpu_program_key works on both
-function gvk_program_new(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int {
- render3d_st.gvk_prog_counter += 1
- let p = render3d_st.gvk_prog_counter
+function gvk_program_new(vs: string, fs: string, defines: string) -> int {
+ gvk_prog_counter += 1
+ let p = gvk_prog_counter
let key = `{vs}|{fs}|{Text.replace(defines, "\n", ";")}`
- if render3d_st.gpu_prog_ids == null { render3d_st.gpu_prog_ids = new []int; render3d_st.gpu_prog_keys = new []string }
- push(render3d_st.gpu_prog_ids, p)
- push(render3d_st.gpu_prog_keys, key)
- if not gvk_program(render3d_st, p, key, render3d_st.gvk_spv_dir) { return 0 }
+ if gpu_prog_ids == null { gpu_prog_ids = new []int; gpu_prog_keys = new []string }
+ push(gpu_prog_ids, p)
+ push(gpu_prog_keys, key)
+ if not gvk_program(p, key, gvk_spv_dir) { return 0 }
return p
}
-function gvk_mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void {
+function gvk_mesh_free(m: Mesh) -> void {
if m == null { return }
- if m.vbufs != null { for i in 0 .. m.n_vbufs { if m.vbufs[i] > 0 { gvk_buf_release(render3d_st, m.vbufs[i]) } } }
+ if m.vbufs != null { for i in 0 .. m.n_vbufs { if m.vbufs[i] > 0 { gvk_buf_release(m.vbufs[i]) } } }
m.n_vbufs = 0
m.vbo = 0
- if m.ebo > 0 { gvk_buf_release(render3d_st, m.ebo); m.ebo = 0 }
+ if m.ebo > 0 { gvk_buf_release(m.ebo); m.ebo = 0 }
}
-function gvk_scissor(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void {
- if render3d_st.gvk_sc == null { return }
- render3d_st.gvk_sc[0] = 1; render3d_st.gvk_sc[1] = x; render3d_st.gvk_sc[2] = y; render3d_st.gvk_sc[3] = w; render3d_st.gvk_sc[4] = h
+function gvk_scissor(x: int, y: int, w: int, h: int) -> void {
+ if gvk_sc == null { return }
+ gvk_sc[0] = 1; gvk_sc[1] = x; gvk_sc[2] = y; gvk_sc[3] = w; gvk_sc[4] = h
}
-function gvk_scissor_off(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_sc != null { render3d_st.gvk_sc[0] = 0 } }
-function gvk_viewport(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void {
- if render3d_st.gvk_vp == null { return }
- render3d_st.gvk_vp[0] = x; render3d_st.gvk_vp[1] = y; render3d_st.gvk_vp[2] = w; render3d_st.gvk_vp[3] = h
+function gvk_scissor_off() -> void { if gvk_sc != null { gvk_sc[0] = 0 } }
+function gvk_viewport(x: int, y: int, w: int, h: int) -> void {
+ if gvk_vp == null { return }
+ gvk_vp[0] = x; gvk_vp[1] = y; gvk_vp[2] = w; gvk_vp[3] = h
}
-function gvk_clear_color(render3d_st: mut Render3dState, r: float, g: float, b: float, a: float) -> void {
- if render3d_st.gvk_clear_rgba == null { return }
- render3d_st.gvk_clear_rgba[0] = r; render3d_st.gvk_clear_rgba[1] = g; render3d_st.gvk_clear_rgba[2] = b; render3d_st.gvk_clear_rgba[3] = a
+function gvk_clear_color(r: float, g: float, b: float, a: float) -> void {
+ if gvk_clear_rgba == null { return }
+ gvk_clear_rgba[0] = r; gvk_clear_rgba[1] = g; gvk_clear_rgba[2] = b; gvk_clear_rgba[3] = a
}
-function gvk_fb_colors(render3d_st: mut Render3dState, fb: int, n: int) -> void { if render3d_st.gvk_fb_ncolor != null and fb >= 0 and fb < 4096 { render3d_st.gvk_fb_ncolor[fb] = n } }
+function gvk_fb_colors(fb: int, n: int) -> void { if gvk_fb_ncolor != null and fb >= 0 and fb < 4096 { gvk_fb_ncolor[fb] = n } }
# The framebuffer changes: a clear still waiting for this one's pass runs now, on this target,
# rather than becoming the load op of whichever pass begins next.
-function gvk_rebind(render3d_st: mut Render3dState, fb: int) -> void {
- if fb == render3d_st.gvk_fb_cur { return }
- if not render3d_st.gvk_in_pass and render3d_st.gvk_clear_bits != 0 { gvk_pass_begin(render3d_st, render3d_st.gpu_fb, gpu_fb_at(render3d_st, render3d_st.gvk_fb_cur)) }
- gvk_pass_end(render3d_st)
- render3d_st.gvk_fb_cur = fb
+function gvk_rebind(fb: int) -> void {
+ if fb == gvk_fb_cur { return }
+ if not gvk_in_pass and gvk_clear_bits != 0 { gvk_pass_begin(gpu_fb, gpu_fb_at(gvk_fb_cur)) }
+ gvk_pass_end()
+ gvk_fb_cur = fb
}
-function gvk_fb_forget(render3d_st: mut Render3dState, fb: int) -> void {
- if fb == render3d_st.gvk_fb_cur { gvk_pass_end(render3d_st) }
- gvk_fb_colors(render3d_st, fb, 1)
+function gvk_fb_forget(fb: int) -> void {
+ if fb == gvk_fb_cur { gvk_pass_end() }
+ gvk_fb_colors(fb, 1)
}
# the render state gpu.ludic has cached, with OpenGL's defaults where nothing was set yet
-function gvk_state_now(render3d_st: mut Render3dState) -> GvkState {
- if render3d_st.gvk_state == null { render3d_st.gvk_state = new GvkState }
- let st = render3d_st.gvk_state
- st.depth_test = 0; if render3d_st.gpu_s_depth_test == 1 { st.depth_test = 1 }
- st.depth_write = 1; if render3d_st.gpu_s_depth_write == 0 { st.depth_write = 0 }
- st.depth_func = GL_LESS; if render3d_st.gpu_s_depth_func > 0 { st.depth_func = render3d_st.gpu_s_depth_func }
- st.blend = 0; if render3d_st.gpu_s_blend == 1 { st.blend = 1 }
- st.blend_src = GL_ONE; if render3d_st.gpu_s_blend_src >= 0 { st.blend_src = render3d_st.gpu_s_blend_src }
- st.blend_dst = GL_ZERO; if render3d_st.gpu_s_blend_dst >= 0 { st.blend_dst = render3d_st.gpu_s_blend_dst }
- st.cull = 0; if render3d_st.gpu_s_cull == 1 { st.cull = 1 }
- st.cull_face = GL_BACK; if render3d_st.gpu_s_cull_face > 0 { st.cull_face = render3d_st.gpu_s_cull_face }
- st.color_write = 1; if render3d_st.gpu_s_color_write == 0 { st.color_write = 0 }
- st.a2c = 0; if render3d_st.gpu_s_a2c == 1 { st.a2c = 1 }
- st.bias = 0; if render3d_st.gpu_s_bias == 1 { st.bias = 1 }
- st.bias_factor = float_bits(render3d_st.gpu_s_bias_f)
- st.bias_units = float_bits(render3d_st.gpu_s_bias_u)
- st.wireframe = render3d_st.gvk_wireframe
+function gvk_state_now() -> GvkState {
+ if gvk_state == null { gvk_state = new GvkState }
+ let st = gvk_state
+ st.depth_test = 0; if gpu_s_depth_test == 1 { st.depth_test = 1 }
+ st.depth_write = 1; if gpu_s_depth_write == 0 { st.depth_write = 0 }
+ st.depth_func = GL_LESS; if gpu_s_depth_func > 0 { st.depth_func = gpu_s_depth_func }
+ st.blend = 0; if gpu_s_blend == 1 { st.blend = 1 }
+ st.blend_src = GL_ONE; if gpu_s_blend_src >= 0 { st.blend_src = gpu_s_blend_src }
+ st.blend_dst = GL_ZERO; if gpu_s_blend_dst >= 0 { st.blend_dst = gpu_s_blend_dst }
+ st.cull = 0; if gpu_s_cull == 1 { st.cull = 1 }
+ st.cull_face = GL_BACK; if gpu_s_cull_face > 0 { st.cull_face = gpu_s_cull_face }
+ st.color_write = 1; if gpu_s_color_write == 0 { st.color_write = 0 }
+ st.a2c = 0; if gpu_s_a2c == 1 { st.a2c = 1 }
+ st.bias = 0; if gpu_s_bias == 1 { st.bias = 1 }
+ st.bias_factor = float_bits(gpu_s_bias_f)
+ st.bias_units = float_bits(gpu_s_bias_u)
+ st.wireframe = gvk_wireframe
return st
}
# An indirect draw goes through gvk_draw like any other - the same pipeline, set and buffers -
# and only its last call differs, so the record is handed over in these for that one draw.
+var gvk_ind_buf: int = 0
+var gvk_ind_off: int = 0
+var gvk_ind_n: int = 0
+var gvk_ind_cbuf: int = 0
+var gvk_ind_coff: int = 0
# x * y * z mesh-shader invocations with the current program (a *.mesh one) and state
-function gvk_draw_mesh_tasks_now(render3d_st: mut Render3dState, x: int, y: int, z: int) -> void {
- if not render3d_st.gvk_has_mesh or render3d_st.gvk_mesh_fn == null or x <= 0 or y <= 0 or z <= 0 { return }
- render3d_st.gvk_mesh_x = x; render3d_st.gvk_mesh_y = y; render3d_st.gvk_mesh_z = z
- gvk_draw_now(render3d_st, null, 0, 0, 1)
- render3d_st.gvk_mesh_x = 0
+var gvk_mesh_x: int = 0
+var gvk_mesh_y: int = 0
+var gvk_mesh_z: int = 0
+function gvk_draw_mesh_tasks_now(x: int, y: int, z: int) -> void {
+ if not gvk_has_mesh or gvk_mesh_fn == null or x <= 0 or y <= 0 or z <= 0 { return }
+ gvk_mesh_x = x; gvk_mesh_y = y; gvk_mesh_z = z
+ gvk_draw_now(null, 0, 0, 1)
+ gvk_mesh_x = 0
}
-function gvk_draw_indirect_now(render3d_st: mut Render3dState, m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void {
+function gvk_draw_indirect_now(m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void {
if m == null or m.ebo == 0 or cmds <= 0 or n <= 0 { return }
- render3d_st.gvk_ind_buf = cmds; render3d_st.gvk_ind_off = offset; render3d_st.gvk_ind_n = n; render3d_st.gvk_ind_cbuf = count_buf; render3d_st.gvk_ind_coff = count_off
- gvk_draw_now(render3d_st, m, 0, 0, 1)
- render3d_st.gvk_ind_buf = 0; render3d_st.gvk_ind_cbuf = 0
+ gvk_ind_buf = cmds; gvk_ind_off = offset; gvk_ind_n = n; gvk_ind_cbuf = count_buf; gvk_ind_coff = count_off
+ gvk_draw_now(m, 0, 0, 1)
+ gvk_ind_buf = 0; gvk_ind_cbuf = 0
}
-function gvk_draw_now(render3d_st: mut Render3dState, m: Mesh, first: int, count: int, instances: int) -> void {
- if gvk_prof(render3d_st) { render3d_st.gvk_n_asked += 1 }
- gvk_draw(render3d_st, render3d_st.gpu_prog_cur, m, gvk_state_now(render3d_st), first, count, instances, render3d_st.gpu_tx, GPU_TX_W, render3d_st.gpu_tx_cap, render3d_st.gpu_fb, gpu_fb_at(render3d_st, render3d_st.gvk_fb_cur))
+function gvk_draw_now(m: Mesh, first: int, count: int, instances: int) -> void {
+ if gvk_prof() { gvk_n_asked += 1 }
+ gvk_draw(gpu_prog_cur, m, gvk_state_now(), first, count, instances, gpu_tx, GPU_TX_W, gpu_tx_cap, gpu_fb, gpu_fb_at(gvk_fb_cur))
}
# the colour (and / or depth) of the read framebuffer into the draw framebuffer, same size
-function gvk_fb_att(render3d_st: mut Render3dState, fb: int, depth: bool) -> int {
- if fb == 0 { if depth { return render3d_st.gvk_screen_depth }; return render3d_st.gvk_screen_color }
- let o = gpu_fb_at(render3d_st, fb)
+function gvk_fb_att(fb: int, depth: bool) -> int {
+ if fb == 0 { if depth { return gvk_screen_depth }; return gvk_screen_color }
+ let o = gpu_fb_at(fb)
if o < 0 { return 0 }
- if depth { return render3d_st.gpu_fb[o + 2] }
- return render3d_st.gpu_fb[o]
+ if depth { return gpu_fb[o + 2] }
+ return gpu_fb[o]
}
-function gvk_tex_samples_of(render3d_st: Render3dState, tex: int) -> int {
- if tex <= 0 or render3d_st.gvk_tex_samples == null or tex >= len(render3d_st.gvk_tex_samples) or render3d_st.gvk_tex_samples[tex] < 1 { return 1 }
- return render3d_st.gvk_tex_samples[tex]
+function gvk_tex_samples_of(tex: int) -> int {
+ if tex <= 0 or gvk_tex_samples == null or tex >= len(gvk_tex_samples) or gvk_tex_samples[tex] < 1 { return 1 }
+ return gvk_tex_samples[tex]
}
# A multisampled image into a single-sampled one: a pass that draws nothing and resolves on its end -
# colour averaged, depth from sample zero. vkCmdResolveImage cannot resolve depth; a pass can.
-function gvk_resolve(render3d_st: mut Render3dState, cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
+function gvk_resolve(cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
var layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
var mode = VK_RESOLVE_MODE_AVERAGE_BIT
if depth { layout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; mode = VK_RESOLVE_MODE_SAMPLE_ZERO_BIT }
- gvk_att_barrier(render3d_st, cb, src, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
- gvk_att_barrier(render3d_st, cb, dst, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
+ gvk_att_barrier(cb, src, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
+ gvk_att_barrier(cb, dst, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
let aw = VkRenderingAttachmentInfo_sizeof
let att = bytes(aw)
Vk.zero(att, aw)
Vk.put_i32(att, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
- Vk.put_i64(att, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, src, 0))
+ Vk.put_i64(att, VkRenderingAttachmentInfo_imageView, gvk_view_of(src, 0))
Vk.put_i32(att, VkRenderingAttachmentInfo_imageLayout, layout)
Vk.put_i32(att, VkRenderingAttachmentInfo_resolveMode, mode)
- Vk.put_i64(att, VkRenderingAttachmentInfo_resolveImageView, gvk_view_of(render3d_st, dst, 0))
+ Vk.put_i64(att, VkRenderingAttachmentInfo_resolveImageView, gvk_view_of(dst, 0))
Vk.put_i32(att, VkRenderingAttachmentInfo_resolveImageLayout, layout)
Vk.put_i32(att, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD)
Vk.put_i32(att, VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
@@ -1474,14 +1562,14 @@ function gvk_resolve(render3d_st: mut Render3dState, cb: pointer, src: int, dst:
else { Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, 1); Vk.put_ptr(ri, VkRenderingInfo_pColorAttachments, att) }
Vk.cmd_begin_rendering(cb, ri)
Vk.cmd_end_rendering(cb)
- gvk_att_barrier(render3d_st, cb, src, 0, depth, layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
- gvk_att_barrier(render3d_st, cb, dst, 0, depth, layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ gvk_att_barrier(cb, src, 0, depth, layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ gvk_att_barrier(cb, dst, 0, depth, layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
}
-function gvk_copy(render3d_st: mut Render3dState, cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
- if src <= 0 or dst <= 0 or render3d_st.gvk_tex_image[src] == 0 or render3d_st.gvk_tex_image[dst] == 0 { return }
- if gvk_tex_samples_of(render3d_st, src) > 1 and gvk_tex_samples_of(render3d_st, dst) == 1 { gvk_resolve(render3d_st, cb, src, dst, depth, w, h); return }
- gvk_barrier(cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
- gvk_barrier(cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
+function gvk_copy(cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
+ if src <= 0 or dst <= 0 or gvk_tex_image[src] == 0 or gvk_tex_image[dst] == 0 { return }
+ if gvk_tex_samples_of(src) > 1 and gvk_tex_samples_of(dst) == 1 { gvk_resolve(cb, src, dst, depth, w, h); return }
+ gvk_barrier(cb, gvk_tex_image[src], depth, 0, 1, gvk_tex_layers[src], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
+ gvk_barrier(cb, gvk_tex_image[dst], depth, 0, 1, gvk_tex_layers[dst], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let ic = bytes(VkImageCopy_sizeof)
Vk.zero(ic, VkImageCopy_sizeof)
var aspect = VK_IMAGE_ASPECT_COLOR_BIT
@@ -1493,26 +1581,26 @@ function gvk_copy(render3d_st: mut Render3dState, cb: pointer, src: int, dst: in
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_width, w)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1)
- Vk.cmd_copy_image(cb, render3d_st.gvk_tex_image[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
- gvk_barrier(cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
- gvk_barrier(cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ Vk.cmd_copy_image(cb, gvk_tex_image[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, gvk_tex_image[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
+ gvk_barrier(cb, gvk_tex_image[src], depth, 0, 1, gvk_tex_layers[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ gvk_barrier(cb, gvk_tex_image[dst], depth, 0, 1, gvk_tex_layers[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
}
-function gvk_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> void {
- gvk_pass_end(render3d_st)
- let cb = gvk_frame_cb(render3d_st)
- if (mask & GL_COLOR_BUFFER_BIT) != 0 { gvk_copy(render3d_st, cb, gvk_fb_att(render3d_st, render3d_st.gvk_fb_read, false), gvk_fb_att(render3d_st, render3d_st.gvk_fb_draw, false), false, w, h) }
- if (mask & GL_DEPTH_BUFFER_BIT) != 0 { gvk_copy(render3d_st, cb, gvk_fb_att(render3d_st, render3d_st.gvk_fb_read, true), gvk_fb_att(render3d_st, render3d_st.gvk_fb_draw, true), true, w, h) }
+function gvk_blit(w: int, h: int, mask: int) -> void {
+ gvk_pass_end()
+ let cb = gvk_frame_cb()
+ if (mask & GL_COLOR_BUFFER_BIT) != 0 { gvk_copy(cb, gvk_fb_att(gvk_fb_read, false), gvk_fb_att(gvk_fb_draw, false), false, w, h) }
+ if (mask & GL_DEPTH_BUFFER_BIT) != 0 { gvk_copy(cb, gvk_fb_att(gvk_fb_read, true), gvk_fb_att(gvk_fb_draw, true), true, w, h) }
}
# the screen as RGB8, bottom row first, as glReadPixels hands it back (a photograph)
-function gvk_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: pointer) -> void {
- gvk_present(render3d_st)
- let px = bytes(render3d_st.gvk_screen_w * render3d_st.gvk_screen_h * 4)
- if not gvk_tex_read(render3d_st, render3d_st.gvk_screen_color, GL_RGBA8, render3d_st.gvk_screen_w, render3d_st.gvk_screen_h, GL_RGBA, GL_UNSIGNED_BYTE, px) { return }
+function gvk_read_screen(w: int, h: int, out: pointer) -> void {
+ gvk_present()
+ let px = bytes(gvk_screen_w * gvk_screen_h * 4)
+ if not gvk_tex_read(gvk_screen_color, GL_RGBA8, gvk_screen_w, gvk_screen_h, GL_RGBA, GL_UNSIGNED_BYTE, px) { return }
let dst: pointer = out
for y in 0 .. h {
for x in 0 .. w {
- let o = (y * render3d_st.gvk_screen_w + x) * 4
+ let o = (y * gvk_screen_w + x) * 4
let q = (y * w + x) * 3
dst[q] = px[o]; dst[q + 1] = px[o + 1]; dst[q + 2] = px[o + 2]
}
@@ -1524,47 +1612,57 @@ function gvk_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: po
extern function win_native_window() -> pointer = "win_native_window"
extern function win_native_instance() -> pointer = "win_native_instance"
+var gvk_surface: long = 0
+var gvk_swap: long = 0
+var gvk_swap_n: int = 0
+var gvk_swap_images: bytes = null
+var gvk_swap_fmt: int = 0
+var gvk_swap_w: int = 0
+var gvk_swap_h: int = 0
+var gvk_vsync: bool = true
+var gvk_acq_fence: bytes = null
+var gvk_swap_stale: bool = false
-function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
- if render3d_st.gvk_surface != 0 { return true }
+function gvk_surface_make() -> bool {
+ if gvk_surface != 0 { return true }
let hwnd = win_native_window()
- if hwnd == null { render3d_st.gvk_why = "no window to present to"; return false }
+ if hwnd == null { gvk_why = "no window to present to"; return false }
let sci = bytes(VkWin32SurfaceCreateInfoKHR_sizeof)
Vk.zero(sci, VkWin32SurfaceCreateInfoKHR_sizeof)
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) }
- render3d_st.gvk_surface = gvk_handle(out)
+ let r = Vk.create_win32_surface_khr(gvk_inst, sci, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateWin32SurfaceKHR", r) }
+ gvk_surface = gvk_handle(out)
let ok = bytes(4)
Vk.put_i32(ok, 0, 0)
- Vk.get_physical_device_surface_support_khr(render3d_st.gvk_pd, render3d_st.gvk_family, render3d_st.gvk_surface, ok)
- if Vk.get_i32(ok, 0) != 1 { render3d_st.gvk_why = "the graphics queue cannot present to the window"; return false }
+ Vk.get_physical_device_surface_support_khr(gvk_pd, gvk_family, gvk_surface, ok)
+ if Vk.get_i32(ok, 0) != 1 { gvk_why = "the graphics queue cannot present to the window"; return false }
let fci = bytes(VkFenceCreateInfo_sizeof)
Vk.zero(fci, VkFenceCreateInfo_sizeof)
Vk.put_i32(fci, VkFenceCreateInfo_sType, VK_STRUCTURE_TYPE_FENCE_CREATE_INFO)
- render3d_st.gvk_acq_fence = bytes(8)
- return Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_acq_fence) == VK_SUCCESS
+ gvk_acq_fence = bytes(8)
+ return Vk.create_fence(gvk_dev, fci, null, gvk_acq_fence) == VK_SUCCESS
}
# (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 }
- Vk.device_wait_idle(render3d_st.gvk_dev)
+function gvk_swap_make(w: int, h: int) -> bool {
+ if not gvk_surface_make() { return false }
+ Vk.device_wait_idle(gvk_dev)
let caps = bytes(VkSurfaceCapabilitiesKHR_sizeof)
- Vk.get_physical_device_surface_capabilities_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, caps)
+ Vk.get_physical_device_surface_capabilities_khr(gvk_pd, gvk_surface, caps)
var ew = Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_currentExtent + VkExtent2D_width)
var eh = Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_currentExtent + VkExtent2D_height)
if ew == -1 or ew <= 0 { ew = w; eh = h }
if ew <= 0 or eh <= 0 { return false } # minimised: keep the old chain until it has a size
let cnt = bytes(4)
Vk.put_i32(cnt, 0, 0)
- Vk.get_physical_device_surface_formats_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, null)
+ Vk.get_physical_device_surface_formats_khr(gvk_pd, gvk_surface, cnt, null)
let nf = Vk.get_i32(cnt, 0)
let fmts = bytes(nf * VkSurfaceFormatKHR_sizeof + 8)
- Vk.get_physical_device_surface_formats_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, fmts)
+ Vk.get_physical_device_surface_formats_khr(gvk_pd, gvk_surface, cnt, fmts)
# the screen image holds display-ready 8-bit colour, so the swapchain takes it unconverted
var fmt = Vk.get_i32(fmts, VkSurfaceFormatKHR_format)
var cs = Vk.get_i32(fmts, VkSurfaceFormatKHR_colorSpace)
@@ -1574,7 +1672,7 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
if (f == VK_FORMAT_B8G8R8A8_UNORM or f == VK_FORMAT_R8G8B8A8_UNORM) and c == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR { fmt = f; cs = c }
}
var hdr = false
- if render3d_st.gvk_hdr_want and render3d_st.gvk_has_colorspace {
+ if gvk_hdr_want and gvk_has_colorspace {
for i in 0 .. nf {
let f = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_format)
let c = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_colorSpace)
@@ -1583,15 +1681,15 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
if not hdr { print("r3d: vulkan: HDR output asked for, but this display offers no HDR10 swapchain") }
}
# the screen image carries the swapchain's depth of colour: remade when HDR comes or goes
- if hdr != render3d_st.gvk_hdr_on { render3d_st.gvk_hdr_on = hdr; gvk_screen_make(render3d_st, render3d_st.gvk_screen_w, render3d_st.gvk_screen_h) }
+ if hdr != gvk_hdr_on { gvk_hdr_on = hdr; gvk_screen_make(gvk_screen_w, gvk_screen_h) }
Vk.put_i32(cnt, 0, 0)
- Vk.get_physical_device_surface_present_modes_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, null)
+ Vk.get_physical_device_surface_present_modes_khr(gvk_pd, gvk_surface, cnt, null)
let nm = Vk.get_i32(cnt, 0)
let modes = bytes(nm * 4 + 8)
- Vk.get_physical_device_surface_present_modes_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, modes)
+ Vk.get_physical_device_surface_present_modes_khr(gvk_pd, gvk_surface, cnt, modes)
# vsync: FIFO, which every device has. Off: mailbox where offered (no tearing), else immediate.
var mode = VK_PRESENT_MODE_FIFO_KHR
- if not render3d_st.gvk_vsync {
+ if not gvk_vsync {
for i in 0 .. nm { if Vk.get_i32(modes, i * 4) == VK_PRESENT_MODE_IMMEDIATE_KHR { mode = VK_PRESENT_MODE_IMMEDIATE_KHR } }
for i in 0 .. nm { if Vk.get_i32(modes, i * 4) == VK_PRESENT_MODE_MAILBOX_KHR { mode = VK_PRESENT_MODE_MAILBOX_KHR } }
}
@@ -1601,7 +1699,7 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
let sci = bytes(VkSwapchainCreateInfoKHR_sizeof)
Vk.zero(sci, VkSwapchainCreateInfoKHR_sizeof)
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_sType, VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR)
- Vk.put_i64(sci, VkSwapchainCreateInfoKHR_surface, render3d_st.gvk_surface)
+ Vk.put_i64(sci, VkSwapchainCreateInfoKHR_surface, gvk_surface)
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_minImageCount, n)
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageFormat, fmt)
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageColorSpace, cs)
@@ -1614,45 +1712,45 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_compositeAlpha, VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_presentMode, mode)
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_clipped, 1)
- Vk.put_i64(sci, VkSwapchainCreateInfoKHR_oldSwapchain, render3d_st.gvk_swap)
+ Vk.put_i64(sci, VkSwapchainCreateInfoKHR_oldSwapchain, gvk_swap)
let out = bytes(8)
- let r = Vk.create_swapchain_khr(render3d_st.gvk_dev, sci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateSwapchainKHR", r) }
- if render3d_st.gvk_swap != 0 { Vk.destroy_swapchain_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, null) }
- render3d_st.gvk_swap = gvk_handle(out)
- if render3d_st.gvk_hdr_on and render3d_st.gvk_has_hdr_meta { gvk_hdr_metadata(render3d_st) }
+ let r = Vk.create_swapchain_khr(gvk_dev, sci, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateSwapchainKHR", r) }
+ if gvk_swap != 0 { Vk.destroy_swapchain_khr(gvk_dev, gvk_swap, null) }
+ gvk_swap = gvk_handle(out)
+ if gvk_hdr_on and gvk_has_hdr_meta { gvk_hdr_metadata() }
Vk.put_i32(cnt, 0, 0)
- Vk.get_swapchain_images_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, cnt, null)
- render3d_st.gvk_swap_n = Vk.get_i32(cnt, 0)
- render3d_st.gvk_swap_images = bytes(render3d_st.gvk_swap_n * 8 + 8)
- Vk.get_swapchain_images_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, cnt, render3d_st.gvk_swap_images)
- render3d_st.gvk_swap_fmt = fmt
- render3d_st.gvk_swap_w = ew
- render3d_st.gvk_swap_h = eh
- render3d_st.gvk_swap_stale = false
+ Vk.get_swapchain_images_khr(gvk_dev, gvk_swap, cnt, null)
+ gvk_swap_n = Vk.get_i32(cnt, 0)
+ gvk_swap_images = bytes(gvk_swap_n * 8 + 8)
+ Vk.get_swapchain_images_khr(gvk_dev, gvk_swap, cnt, gvk_swap_images)
+ gvk_swap_fmt = fmt
+ gvk_swap_w = ew
+ gvk_swap_h = eh
+ gvk_swap_stale = false
var mname = "fifo"
if mode == VK_PRESENT_MODE_MAILBOX_KHR { mname = "mailbox" }
if mode == VK_PRESENT_MODE_IMMEDIATE_KHR { mname = "immediate" }
var cname = "SDR"
- if render3d_st.gvk_hdr_on { cname = "HDR10" }
- print(`r3d: vulkan swapchain {ew}x{eh}, {render3d_st.gvk_swap_n} images, {mname}, {cname}`)
+ if gvk_hdr_on { cname = "HDR10" }
+ print(`r3d: vulkan swapchain {ew}x{eh}, {gvk_swap_n} images, {mname}, {cname}`)
return true
}
-function gvk_present_window(render3d_st: mut Render3dState) -> void {
- let cb = gvk_frame_cb(render3d_st)
- gvk_pass_end(render3d_st)
- 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 }
+function gvk_present_window() -> void {
+ let cb = gvk_frame_cb()
+ gvk_pass_end()
+ if gvk_swap_stale { gvk_once_end(cb); gvk_cb = null; gvk_swap_make(gvk_swap_w, gvk_swap_h); return }
let idx = bytes(4)
- Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence)
+ Vk.reset_fences(gvk_dev, 1, gvk_acq_fence)
let forever: long = -1
let zero: long = 0
- var r = Vk.acquire_next_image_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, forever, zero, Vk.get_i64(render3d_st.gvk_acq_fence, 0), idx)
- if r == VK_ERROR_OUT_OF_DATE_KHR { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return }
- Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence, 1, forever)
+ var r = Vk.acquire_next_image_khr(gvk_dev, gvk_swap, forever, zero, Vk.get_i64(gvk_acq_fence, 0), idx)
+ if r == VK_ERROR_OUT_OF_DATE_KHR { gvk_once_end(cb); gvk_cb = null; gvk_swap_make(gvk_swap_w, gvk_swap_h); return }
+ Vk.wait_for_fences(gvk_dev, 1, gvk_acq_fence, 1, forever)
let i = Vk.get_i32(idx, 0)
- let dst = Vk.get_i64(render3d_st.gvk_swap_images, i * 8)
- let src = render3d_st.gvk_tex_image[render3d_st.gvk_screen_color]
+ let dst = Vk.get_i64(gvk_swap_images, i * 8)
+ let src = gvk_tex_image[gvk_screen_color]
gvk_barrier(cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
gvk_barrier(cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let blit = bytes(VkImageBlit_sizeof)
@@ -1660,83 +1758,93 @@ function gvk_present_window(render3d_st: mut Render3dState) -> void {
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_layerCount, 1)
# the screen image's row 0 is the bottom of the picture: read it from the top edge down
- Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_y, render3d_st.gvk_screen_h)
- Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_x, render3d_st.gvk_screen_w)
+ Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_y, gvk_screen_h)
+ Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_x, gvk_screen_w)
Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_layerCount, 1)
- Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_x, render3d_st.gvk_swap_w)
- Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, render3d_st.gvk_swap_h)
+ Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_x, gvk_swap_w)
+ Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, gvk_swap_h)
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
Vk.cmd_blit_image(cb, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR)
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)
- render3d_st.gvk_cb = null
+ gvk_once_end(cb)
+ gvk_cb = null
let pi = bytes(VkPresentInfoKHR_sizeof)
Vk.zero(pi, VkPresentInfoKHR_sizeof)
Vk.put_i32(pi, VkPresentInfoKHR_sType, VK_STRUCTURE_TYPE_PRESENT_INFO_KHR)
let chains = bytes(8)
- Vk.put_i64(chains, 0, render3d_st.gvk_swap)
+ Vk.put_i64(chains, 0, gvk_swap)
Vk.put_i32(pi, VkPresentInfoKHR_swapchainCount, 1)
Vk.put_ptr(pi, VkPresentInfoKHR_pSwapchains, chains)
Vk.put_ptr(pi, VkPresentInfoKHR_pImageIndices, idx)
- gsl_before_present(render3d_st)
- r = Vk.queue_present_khr(render3d_st.gvk_queue, pi)
- if r == VK_ERROR_OUT_OF_DATE_KHR or r == VK_SUBOPTIMAL_KHR { render3d_st.gvk_swap_stale = true }
- gsl_after_present(render3d_st)
+ gsl_before_present()
+ r = Vk.queue_present_khr(gvk_queue, pi)
+ if r == VK_ERROR_OUT_OF_DATE_KHR or r == VK_SUBOPTIMAL_KHR { gvk_swap_stale = true }
+ gsl_after_present()
}
# a window's client area changed: the screen images and the swapchain follow it
-function gvk_resize_check(render3d_st: mut Render3dState) -> bool {
- if render3d_st.gvk_swap == 0 { return false }
- if render3d_st.gvk_size_buf == null { render3d_st.gvk_size_buf = words(4) }
- win_gl_drawable(render3d_st.gvk_size_buf)
- let w = render3d_st.gvk_size_buf[0]
- let h = render3d_st.gvk_size_buf[1]
+var gvk_size_buf: words = null
+function gvk_resize_check() -> bool {
+ if gvk_swap == 0 { return false }
+ if gvk_size_buf == null { gvk_size_buf = words(4) }
+ win_gl_drawable(gvk_size_buf)
+ let w = gvk_size_buf[0]
+ let h = gvk_size_buf[1]
if w <= 0 or h <= 0 { return false }
- if w == gl_w and h == gl_h and not render3d_st.gvk_swap_stale { return false }
- gvk_flush(render3d_st)
+ if w == gl_w and h == gl_h and not gvk_swap_stale { return false }
+ gvk_flush()
gl_w = w
gl_h = h
- gvk_screen_make(render3d_st, w, h)
- gvk_swap_make(render3d_st, w, h)
+ gvk_screen_make(w, h)
+ gvk_swap_make(w, h)
return true
}
# Mipmaps for a texture the renderer asks for mid-frame (the exposure measure reads the HDR scene's
# smallest level every frame): recorded into the open frame after its pass, so they cost no submit.
# A chain that has to grow first still goes through its one-shot path.
-function gvk_mips_now(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> void {
- if render3d_st.gvk_cb == null or render3d_st.gvk_tex_levels[tex] <= 1 { gvk_flush(render3d_st); gvk_tex_mips(render3d_st, tex, w, h); return }
- gvk_pass_end(render3d_st)
- gvk_tex_mips_into(render3d_st, render3d_st.gvk_cb, tex, w, h)
+function gvk_mips_now(tex: int, w: int, h: int) -> void {
+ if gvk_cb == null or gvk_tex_levels[tex] <= 1 { gvk_flush(); gvk_tex_mips(tex, w, h); return }
+ gvk_pass_end()
+ gvk_tex_mips_into(gvk_cb, tex, w, h)
}
# ---- R3D_VK_PROF: where a Vulkan frame's CPU time goes ---------------------------------------------
# Every 120 frames: draws and flushes per frame, and milliseconds per frame spent finding pipelines,
# filling descriptor sets, and inside draws altogether. Off, it costs one flag test a draw.
-function gvk_prof(render3d_st: mut Render3dState) -> bool {
- if render3d_st.gvk_prof_state < 0 { render3d_st.gvk_prof_state = 0; if r3d_env_has(render3d_st, "R3D_VK_PROF") { render3d_st.gvk_prof_state = 1 } }
- return render3d_st.gvk_prof_state == 1
+var gvk_prof_state: int = -1
+var gvk_us_pipe: long = 0
+var gvk_us_set: long = 0
+var gvk_us_draw: long = 0
+var gvk_n_draws: int = 0
+var gvk_n_asked: int = 0 # draws that reached gvk_draw_now: every one the renderer asked for
+var gvk_n_flush: int = 0
+var gvk_prof_frames: int = 0
+var gvk_n_pipe_new: int = 0 # pipelines made this profile window
+function gvk_prof() -> bool {
+ if gvk_prof_state < 0 { gvk_prof_state = 0; if r3d_env_has("R3D_VK_PROF") { gvk_prof_state = 1 } }
+ return gvk_prof_state == 1
}
-function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
- if not gvk_prof(render3d_st) { return }
- render3d_st.gvk_prof_frames += 1
- if render3d_st.gvk_prof_frames < 120 { return }
- let f = render3d_st.gvk_prof_frames
- let pipe = Text.to_int(string(render3d_st.gvk_us_pipe)) / f
- let set = Text.to_int(string(render3d_st.gvk_us_set)) / f
- let draw = Text.to_int(string(render3d_st.gvk_us_draw)) / f
- print(`r3d: vulkan per frame: {render3d_st.gvk_n_draws / f} draws, {render3d_st.gvk_n_flush / f} flushes; pipelines {pipe / 1000}.{(pipe / 100) % 10} ms, sets {set / 1000}.{(set / 100) % 10} ms, inside draws {draw / 1000}.{(draw / 100) % 10} ms; {render3d_st.gvk_n_pipe_new} pipelines made`)
+function gvk_prof_frame() -> void {
+ if not gvk_prof() { return }
+ gvk_prof_frames += 1
+ if gvk_prof_frames < 120 { return }
+ let f = gvk_prof_frames
+ let pipe = Text.to_int(string(gvk_us_pipe)) / f
+ let set = Text.to_int(string(gvk_us_set)) / f
+ let draw = Text.to_int(string(gvk_us_draw)) / f
+ print(`r3d: vulkan per frame: {gvk_n_draws / f} draws, {gvk_n_flush / f} flushes; pipelines {pipe / 1000}.{(pipe / 100) % 10} ms, sets {set / 1000}.{(set / 100) % 10} ms, inside draws {draw / 1000}.{(draw / 100) % 10} ms; {gvk_n_pipe_new} pipelines made`)
# every draw asked for should have been made: a gap is a layer missing from the frame, which is how
# MoltenVK's refused skinned pipelines went unseen (the drawstats session found it by counting)
- if render3d_st.gvk_n_asked != render3d_st.gvk_n_draws {
- print(`r3d: vulkan: {(render3d_st.gvk_n_asked - render3d_st.gvk_n_draws) / f} draws a frame were asked for and not made ({render3d_st.gvk_n_asked / f} asked, {render3d_st.gvk_n_draws / f} made)`)
+ if gvk_n_asked != gvk_n_draws {
+ print(`r3d: vulkan: {(gvk_n_asked - gvk_n_draws) / f} draws a frame were asked for and not made ({gvk_n_asked / f} asked, {gvk_n_draws / f} made)`)
}
let zero: long = 0
- render3d_st.gvk_us_pipe = zero; render3d_st.gvk_us_set = zero; render3d_st.gvk_us_draw = zero
- render3d_st.gvk_n_draws = 0; render3d_st.gvk_n_asked = 0; render3d_st.gvk_n_flush = 0; render3d_st.gvk_prof_frames = 0; render3d_st.gvk_n_pipe_new = 0
+ gvk_us_pipe = zero; gvk_us_set = zero; gvk_us_draw = zero
+ gvk_n_draws = 0; gvk_n_asked = 0; gvk_n_flush = 0; gvk_prof_frames = 0; gvk_n_pipe_new = 0
}
# ---- the pipeline cache, by integers -----------------------------------------------------------
@@ -1744,15 +1852,23 @@ function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
# A mesh carries an interned layout id (its recorded layout, buffers named by order), the render
# state packs into one int, and the pass's formats into another; the program's last hit is tried
# first, then the entries it owns.
+var gvk_layout_keys: []string = null
+var gvk_pc_prog: []int = null
+var gvk_pc_layout: []int = null
+var gvk_pc_state: []int = null
+var gvk_pc_bias: []int = null
+var gvk_pc_pass: []int = null
+var gvk_pc_pipe: []long = null
+var gvk_pc_last: words = null
-function gvk_layout_id(render3d_st: mut Render3dState, m: Mesh) -> int {
+function gvk_layout_id(m: Mesh) -> int {
if m == null or m.attrs == null { return 1 }
if m.vk_layout > 0 { return m.vk_layout }
let key = gvk_layout_key(m)
- if render3d_st.gvk_layout_keys == null { render3d_st.gvk_layout_keys = new []string }
+ if gvk_layout_keys == null { gvk_layout_keys = new []string }
var id = 0
- for i in 0 .. len(render3d_st.gvk_layout_keys) { if id == 0 and render3d_st.gvk_layout_keys[i] == key { id = i + 2 } }
- if id == 0 { push(render3d_st.gvk_layout_keys, key); id = len(render3d_st.gvk_layout_keys) + 1 }
+ for i in 0 .. len(gvk_layout_keys) { if id == 0 and gvk_layout_keys[i] == key { id = i + 2 } }
+ if id == 0 { push(gvk_layout_keys, key); id = len(gvk_layout_keys) + 1 }
m.vk_layout = id
return id
}
@@ -1767,32 +1883,32 @@ function gvk_blend_index(f: int) -> int {
if f == GL_ONE_MINUS_SRC_COLOR { return 7 }
return 8
}
-function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
- let layout = gvk_layout_id(render3d_st, m)
+function gvk_pipeline_fast(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
+ let layout = gvk_layout_id(m)
var state = st.depth_test | (st.depth_write << 1) | (st.blend << 2) | (st.cull << 3) | (st.color_write << 4) | (st.a2c << 5) | (st.bias << 6) | (st.wireframe << 7)
state = state | ((st.depth_func & 15) << 8) | ((st.cull_face & 15) << 12) | (gvk_blend_index(st.blend_src) << 16) | (gvk_blend_index(st.blend_dst) << 20)
let bias = st.bias_factor * 31 + st.bias_units
let pass = ((n_color * 1000 + color_fmt) * 1000 + depth_fmt) * 64 + samples
- if render3d_st.gvk_pc_prog == null {
- render3d_st.gvk_pc_prog = new []int; render3d_st.gvk_pc_layout = new []int; render3d_st.gvk_pc_state = new []int; render3d_st.gvk_pc_bias = new []int
- render3d_st.gvk_pc_pass = new []int; render3d_st.gvk_pc_pipe = new []long
- render3d_st.gvk_pc_last = words(4096)
- for i in 0 .. 4096 { render3d_st.gvk_pc_last[i] = -1 }
+ if gvk_pc_prog == null {
+ gvk_pc_prog = new []int; gvk_pc_layout = new []int; gvk_pc_state = new []int; gvk_pc_bias = new []int
+ gvk_pc_pass = new []int; gvk_pc_pipe = new []long
+ gvk_pc_last = words(4096)
+ for i in 0 .. 4096 { gvk_pc_last[i] = -1 }
}
if p < 4096 {
- let k = render3d_st.gvk_pc_last[p]
- if k >= 0 and render3d_st.gvk_pc_layout[k] == layout and render3d_st.gvk_pc_state[k] == state and render3d_st.gvk_pc_pass[k] == pass and render3d_st.gvk_pc_bias[k] == bias { return render3d_st.gvk_pc_pipe[k] }
+ let k = gvk_pc_last[p]
+ if k >= 0 and gvk_pc_layout[k] == layout and gvk_pc_state[k] == state and gvk_pc_pass[k] == pass and gvk_pc_bias[k] == bias { return gvk_pc_pipe[k] }
}
- for k in 0 .. len(render3d_st.gvk_pc_prog) {
- if render3d_st.gvk_pc_prog[k] == p and render3d_st.gvk_pc_layout[k] == layout and render3d_st.gvk_pc_state[k] == state and render3d_st.gvk_pc_pass[k] == pass and render3d_st.gvk_pc_bias[k] == bias {
- if p < 4096 { render3d_st.gvk_pc_last[p] = k }
- return render3d_st.gvk_pc_pipe[k]
+ for k in 0 .. len(gvk_pc_prog) {
+ if gvk_pc_prog[k] == p and gvk_pc_layout[k] == layout and gvk_pc_state[k] == state and gvk_pc_pass[k] == pass and gvk_pc_bias[k] == bias {
+ if p < 4096 { gvk_pc_last[p] = k }
+ return gvk_pc_pipe[k]
}
}
- let pipe = gvk_pipeline(render3d_st, p, m, st, n_color, color_fmt, depth_fmt, samples)
+ let pipe = gvk_pipeline(p, m, st, n_color, color_fmt, depth_fmt, samples)
if pipe == 0 { return pipe }
- push(render3d_st.gvk_pc_prog, p); push(render3d_st.gvk_pc_layout, layout); push(render3d_st.gvk_pc_state, state)
- push(render3d_st.gvk_pc_bias, bias); push(render3d_st.gvk_pc_pass, pass); push(render3d_st.gvk_pc_pipe, pipe)
- if p < 4096 { render3d_st.gvk_pc_last[p] = len(render3d_st.gvk_pc_prog) - 1 }
+ push(gvk_pc_prog, p); push(gvk_pc_layout, layout); push(gvk_pc_state, state)
+ push(gvk_pc_bias, bias); push(gvk_pc_pass, pass); push(gvk_pc_pipe, pipe)
+ if p < 4096 { gvk_pc_last[p] = len(gvk_pc_prog) - 1 }
return pipe
}
diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic
index 902b7be3..42e22560 100644
--- a/packages/ludic.render3d/gpu_vk_res.ludic
+++ b/packages/ludic.render3d/gpu_vk_res.ludic
@@ -45,30 +45,46 @@ function gvk_channel_bytes(ifmt: int) -> int {
# objects. Pixels uploaded with the image get a full mip chain, allocated up front, because the
# renderer asks for mipmaps after the upload; an image made without pixels is a target and gets
# one level. Every level sits in SHADER_READ_ONLY_OPTIMAL between uses.
+var gvk_tex_image: []long = null
+var gvk_tex_view: []long = null
+var gvk_tex_mem: []long = null
+var gvk_tex_levels: []int = null
+var gvk_tex_layers: []int = null
+var gvk_tex_vkfmt: []int = null
+var gvk_tex_dims_w: []int = null
+var gvk_tex_dims_h: []int = null
+var gvk_tex_glfmt: []int = null # the format the renderer asked for (gpu.ludic's vocabulary)
+var gvk_tex_array: []int = null # 1 for an array image
+var gvk_tex_gen: []int = null # bumped whenever the image behind a handle is replaced
+var gvk_tex_smp_sig: []int = null # the sampling parameters the cached sampler was made for
+var gvk_tex_smp: []long = null
+var gvk_tex_samples: []int = null # 1, or the sample count of a multisampled renderbuffer
+var gvk_storage_samples: int = 1 # what the next gvk_tex_storage makes: gpu_rb_storage sets it around its call
+var gvk_unpack_swap: bool = false # GL_UNPACK_SWAP_BYTES: 16-bit PNG samples arrive big-endian
-function gvk_tex_new(render3d_st: mut Render3dState) -> int {
+function gvk_tex_new() -> int {
let zero: long = 0
- if render3d_st.gvk_tex_image == null {
- render3d_st.gvk_tex_image = new []long; render3d_st.gvk_tex_view = new []long; render3d_st.gvk_tex_mem = new []long
- render3d_st.gvk_tex_levels = new []int; render3d_st.gvk_tex_layers = new []int; render3d_st.gvk_tex_vkfmt = new []int
- render3d_st.gvk_tex_dims_w = new []int; render3d_st.gvk_tex_dims_h = new []int
- render3d_st.gvk_tex_glfmt = new []int; render3d_st.gvk_tex_array = new []int; render3d_st.gvk_tex_gen = new []int
- render3d_st.gvk_tex_smp_sig = new []int; render3d_st.gvk_tex_smp = new []long
- render3d_st.gvk_tex_samples = new []int; push(render3d_st.gvk_tex_samples, 0)
- push(render3d_st.gvk_tex_dims_w, 0); push(render3d_st.gvk_tex_dims_h, 0)
- push(render3d_st.gvk_tex_glfmt, 0); push(render3d_st.gvk_tex_array, 0); push(render3d_st.gvk_tex_gen, 0)
- push(render3d_st.gvk_tex_smp_sig, 0); push(render3d_st.gvk_tex_smp, zero)
+ if gvk_tex_image == null {
+ gvk_tex_image = new []long; gvk_tex_view = new []long; gvk_tex_mem = new []long
+ gvk_tex_levels = new []int; gvk_tex_layers = new []int; gvk_tex_vkfmt = new []int
+ gvk_tex_dims_w = new []int; gvk_tex_dims_h = new []int
+ gvk_tex_glfmt = new []int; gvk_tex_array = new []int; gvk_tex_gen = new []int
+ gvk_tex_smp_sig = new []int; gvk_tex_smp = new []long
+ gvk_tex_samples = new []int; push(gvk_tex_samples, 0)
+ push(gvk_tex_dims_w, 0); push(gvk_tex_dims_h, 0)
+ push(gvk_tex_glfmt, 0); push(gvk_tex_array, 0); push(gvk_tex_gen, 0)
+ push(gvk_tex_smp_sig, 0); push(gvk_tex_smp, zero)
# handle 0 is "no texture", as it is on OpenGL
- push(render3d_st.gvk_tex_image, zero); push(render3d_st.gvk_tex_view, zero); push(render3d_st.gvk_tex_mem, zero)
- push(render3d_st.gvk_tex_levels, 0); push(render3d_st.gvk_tex_layers, 0); push(render3d_st.gvk_tex_vkfmt, 0)
+ push(gvk_tex_image, zero); push(gvk_tex_view, zero); push(gvk_tex_mem, zero)
+ push(gvk_tex_levels, 0); push(gvk_tex_layers, 0); push(gvk_tex_vkfmt, 0)
}
- push(render3d_st.gvk_tex_image, zero); push(render3d_st.gvk_tex_view, zero); push(render3d_st.gvk_tex_mem, zero)
- push(render3d_st.gvk_tex_levels, 0); push(render3d_st.gvk_tex_layers, 0); push(render3d_st.gvk_tex_vkfmt, 0)
- push(render3d_st.gvk_tex_dims_w, 0); push(render3d_st.gvk_tex_dims_h, 0)
- push(render3d_st.gvk_tex_glfmt, 0); push(render3d_st.gvk_tex_array, 0); push(render3d_st.gvk_tex_gen, 0)
- push(render3d_st.gvk_tex_smp_sig, 0); push(render3d_st.gvk_tex_smp, zero)
- push(render3d_st.gvk_tex_samples, 0)
- return len(render3d_st.gvk_tex_image) - 1
+ push(gvk_tex_image, zero); push(gvk_tex_view, zero); push(gvk_tex_mem, zero)
+ push(gvk_tex_levels, 0); push(gvk_tex_layers, 0); push(gvk_tex_vkfmt, 0)
+ push(gvk_tex_dims_w, 0); push(gvk_tex_dims_h, 0)
+ push(gvk_tex_glfmt, 0); push(gvk_tex_array, 0); push(gvk_tex_gen, 0)
+ push(gvk_tex_smp_sig, 0); push(gvk_tex_smp, zero)
+ push(gvk_tex_samples, 0)
+ return len(gvk_tex_image) - 1
}
function gvk_mip_levels(w: int, h: int) -> int {
@@ -113,7 +129,9 @@ function gvk_barrier(cb: pointer, image: long, depth: bool, base: int, n: int, l
}
# a host-visible buffer of n bytes, mapped; gvk_staging_free unmaps and frees it
-function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> pointer {
+var gvk_st_buf: long = 0
+var gvk_st_mem: long = 0
+function gvk_staging(n: int, usage: int) -> pointer {
let size: long = n
let bci = bytes(VkBufferCreateInfo_sizeof)
Vk.zero(bci, VkBufferCreateInfo_sizeof)
@@ -122,41 +140,41 @@ function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> poin
Vk.put_i32(bci, VkBufferCreateInfo_usage, usage)
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
let out = bytes(8)
- if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return null }
- render3d_st.gvk_st_buf = gvk_handle(out)
+ if Vk.create_buffer(gvk_dev, bci, null, out) != VK_SUCCESS { return null }
+ gvk_st_buf = gvk_handle(out)
let req = bytes(VkMemoryRequirements_sizeof)
- Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, render3d_st.gvk_st_buf, req)
- render3d_st.gvk_st_mem = gvk_alloc(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
+ Vk.get_buffer_memory_requirements(gvk_dev, gvk_st_buf, req)
+ gvk_st_mem = gvk_alloc(req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
let zero: long = 0
- Vk.bind_buffer_memory(render3d_st.gvk_dev, render3d_st.gvk_st_buf, render3d_st.gvk_st_mem, zero)
+ Vk.bind_buffer_memory(gvk_dev, gvk_st_buf, gvk_st_mem, zero)
let pp = bytes(8)
- if Vk.map_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null }
+ if Vk.map_memory(gvk_dev, gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null }
return Vk.get_ptr(pp, 0)
}
-function gvk_staging_free(render3d_st: mut Render3dState) -> void {
- 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)
- render3d_st.gvk_n_allocs -= 1
+function gvk_staging_free() -> void {
+ Vk.unmap_memory(gvk_dev, gvk_st_mem)
+ Vk.destroy_buffer(gvk_dev, gvk_st_buf, null)
+ Vk.free_memory(gvk_dev, gvk_st_mem, null)
+ gvk_n_allocs -= 1
}
# (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 {
- if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) { return false }
+function gvk_tex_storage(tex: int, array: bool, ifmt: int, w: int, h: int, layers: int, with_mips: bool) -> bool {
+ if tex <= 0 or tex >= len(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 }
- gvk_tex_release(render3d_st, tex)
+ gvk_tex_release(tex)
let depth = gvk_is_depth(ifmt)
var levels = 1
if with_mips and not depth { levels = gvk_mip_levels(w, h) }
- var samples = render3d_st.gvk_storage_samples
+ var samples = gvk_storage_samples
if samples < 1 { samples = 1 }
if samples > 1 { levels = 1 }
var usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT
if depth { usage = usage | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT } else { usage = usage | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT }
# DLSS reads and writes the frame from compute: a float colour target is storage too while
# Streamline is running (8-bit sRGB formats cannot be, so only the float ones)
- if render3d_st.gsl_on and samples == 1 and (vkfmt == VK_FORMAT_R16G16B16A16_SFLOAT or vkfmt == VK_FORMAT_R32_SFLOAT) { usage = usage | VK_IMAGE_USAGE_STORAGE_BIT }
+ if gsl_on and samples == 1 and (vkfmt == VK_FORMAT_R16G16B16A16_SFLOAT or vkfmt == VK_FORMAT_R32_SFLOAT) { usage = usage | VK_IMAGE_USAGE_STORAGE_BIT }
let ici = bytes(VkImageCreateInfo_sizeof)
Vk.zero(ici, VkImageCreateInfo_sizeof)
Vk.put_i32(ici, VkImageCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO)
@@ -173,22 +191,22 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool,
Vk.put_i32(ici, VkImageCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
Vk.put_i32(ici, VkImageCreateInfo_initialLayout, VK_IMAGE_LAYOUT_UNDEFINED)
let out = bytes(8)
- var r = Vk.create_image(render3d_st.gvk_dev, ici, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateImage {w}x{h}x{layers} format {vkfmt}`, r) }
+ var r = Vk.create_image(gvk_dev, ici, null, out)
+ if r != VK_SUCCESS { return gvk_fail(`vkCreateImage {w}x{h}x{layers} format {vkfmt}`, r) }
let image = gvk_handle(out)
let req = bytes(VkMemoryRequirements_sizeof)
- Vk.get_image_memory_requirements(render3d_st.gvk_dev, image, req)
- let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, true)
+ Vk.get_image_memory_requirements(gvk_dev, image, req)
+ let ma = gvk_mem_new(req, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, true)
let mem: long = ma # the allocation id (gvk_mem_new), kept where the memory was
let zero: long = 0
# no memory for it (one allocation per resource meets the driver's allocation limit long before
# the card is full): say so instead of binding a null allocation, which the driver may accept
if mem == 0 {
- Vk.destroy_image(render3d_st.gvk_dev, image, null)
- return gvk_fail(render3d_st, `no device memory for a {w}x{h}x{layers} image ({render3d_st.gvk_n_allocs} allocations live)`, VK_ERROR_OUT_OF_DEVICE_MEMORY)
+ Vk.destroy_image(gvk_dev, image, null)
+ return gvk_fail(`no device memory for a {w}x{h}x{layers} image ({gvk_n_allocs} allocations live)`, VK_ERROR_OUT_OF_DEVICE_MEMORY)
}
- r = Vk.bind_image_memory(render3d_st.gvk_dev, image, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma))
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkBindImageMemory", r) }
+ r = Vk.bind_image_memory(gvk_dev, image, gvk_mem_handle(ma), gvk_mem_offset(ma))
+ if r != VK_SUCCESS { return gvk_fail("vkBindImageMemory", r) }
let vci = bytes(VkImageViewCreateInfo_sizeof)
Vk.zero(vci, VkImageViewCreateInfo_sizeof)
Vk.put_i32(vci, VkImageViewCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO)
@@ -199,18 +217,18 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool,
if depth { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, levels)
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, layers)
- r = Vk.create_image_view(render3d_st.gvk_dev, vci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateImageView", r) }
- render3d_st.gvk_tex_image[tex] = image; render3d_st.gvk_tex_view[tex] = gvk_handle(out); render3d_st.gvk_tex_mem[tex] = mem
- render3d_st.gvk_tex_levels[tex] = levels; render3d_st.gvk_tex_layers[tex] = layers; render3d_st.gvk_tex_vkfmt[tex] = vkfmt
- render3d_st.gvk_tex_dims_w[tex] = w; render3d_st.gvk_tex_dims_h[tex] = h
- render3d_st.gvk_tex_glfmt[tex] = ifmt; render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1
- render3d_st.gvk_tex_samples[tex] = samples
- if array { render3d_st.gvk_tex_array[tex] = 1 } else { render3d_st.gvk_tex_array[tex] = 0 }
+ r = Vk.create_image_view(gvk_dev, vci, null, out)
+ if r != VK_SUCCESS { return gvk_fail("vkCreateImageView", r) }
+ gvk_tex_image[tex] = image; gvk_tex_view[tex] = gvk_handle(out); gvk_tex_mem[tex] = mem
+ gvk_tex_levels[tex] = levels; gvk_tex_layers[tex] = layers; gvk_tex_vkfmt[tex] = vkfmt
+ gvk_tex_dims_w[tex] = w; gvk_tex_dims_h[tex] = h
+ gvk_tex_glfmt[tex] = ifmt; gvk_tex_gen[tex] = gvk_tex_gen[tex] + 1
+ gvk_tex_samples[tex] = samples
+ if array { gvk_tex_array[tex] = 1 } else { gvk_tex_array[tex] = 0 }
# a target starts cleared-to-nothing but readable: every level in the layout samplers expect
- let cb = gvk_once_begin(render3d_st)
+ let cb = gvk_once_begin()
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
- return gvk_once_end(render3d_st, cb)
+ return gvk_once_end(cb)
}
# IEEE single (as its bits) to IEEE half: out-of-range values saturate to infinity, tiny ones to zero
@@ -237,17 +255,17 @@ function gvk_gl_type_bytes(ty: int) -> int {
# Level 0 of every layer from pixels laid out the OpenGL way (fmt channels of type ty), converted
# to what the image stores: a missing alpha filled opaque, 16-bit samples byte-swapped when the
# unpack state says so, 32-bit floats into a half-float image halved.
-function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> bool {
+function gvk_tex_upload(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> bool {
let cin = gvk_gl_channels(fmt)
let bin = gvk_gl_type_bytes(ty)
let cout = gvk_channels(ifmt)
let bout = gvk_channel_bytes(ifmt)
let texels = w * h * layers
let n = texels * cout * bout
- let dst = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT)
+ let dst = gvk_staging(n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT)
if dst == null { print("r3d: vulkan: no staging buffer for an upload"); return false }
let buf = bytes(n)
- if cin == cout and bin == bout and not (bin == 2 and render3d_st.gvk_unpack_swap) {
+ if cin == cout and bin == bout and not (bin == 2 and gvk_unpack_swap) {
mem_copy(buf, data, n)
} else {
let src: pointer = data
@@ -264,7 +282,7 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
if bin == bout {
if bin == 1 { buf[o] = src[i] }
if bin == 2 {
- if render3d_st.gvk_unpack_swap { buf[o] = src[i + 1]; buf[o + 1] = src[i] } else { buf[o] = src[i]; buf[o + 1] = src[i + 1] }
+ if gvk_unpack_swap { buf[o] = src[i + 1]; buf[o + 1] = src[i] } else { buf[o] = src[i]; buf[o + 1] = src[i + 1] }
}
if bin == 4 { Vk.put_i32(buf, o, Vk.get_i32(src, i)) }
} else if bin == 4 and bout == 2 {
@@ -272,7 +290,7 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
buf[o] = hv & 255; buf[o + 1] = (hv >> 8) & 255
} else {
print(`r3d: vulkan: no conversion from {bin}-byte to {bout}-byte samples`)
- gvk_staging_free(render3d_st)
+ gvk_staging_free()
return false
}
}
@@ -280,10 +298,10 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
}
}
mem_copy(dst, buf, n)
- let image = render3d_st.gvk_tex_image[tex]
+ let image = gvk_tex_image[tex]
let depth = gvk_is_depth(ifmt)
- let levels = render3d_st.gvk_tex_levels[tex]
- let cb = gvk_once_begin(render3d_st)
+ let levels = gvk_tex_levels[tex]
+ let cb = gvk_once_begin()
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let bic = bytes(VkBufferImageCopy_sizeof)
Vk.zero(bic, VkBufferImageCopy_sizeof)
@@ -293,27 +311,27 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w:
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_width, w)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
- Vk.cmd_copy_buffer_to_image(cb, render3d_st.gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic)
+ Vk.cmd_copy_buffer_to_image(cb, gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic)
gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
- let ok = gvk_once_end(render3d_st, cb)
- gvk_staging_free(render3d_st)
+ let ok = gvk_once_end(cb)
+ gvk_staging_free()
return ok
}
# The mip chain from level 0, each level blitted down from the one above it
# 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 {
- 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]
- let layers = render3d_st.gvk_tex_layers[tex]
+function gvk_tex_grow_mips(tex: int, w: int, h: int) -> bool {
+ let old_image = gvk_tex_image[tex]
+ let old_view = gvk_tex_view[tex]
+ let old_mem = gvk_tex_mem[tex]
+ let layers = gvk_tex_layers[tex]
let zero: long = 0
- render3d_st.gvk_tex_image[tex] = zero
- if not gvk_tex_storage(render3d_st, tex, render3d_st.gvk_tex_array[tex] == 1, render3d_st.gvk_tex_glfmt[tex], w, h, layers, true) { return false }
- let cb = gvk_once_begin(render3d_st)
+ gvk_tex_image[tex] = zero
+ if not gvk_tex_storage(tex, gvk_tex_array[tex] == 1, gvk_tex_glfmt[tex], w, h, layers, true) { return false }
+ let cb = gvk_once_begin()
gvk_barrier(cb, old_image, false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
- gvk_barrier(cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
+ gvk_barrier(cb, gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
let ic = bytes(VkImageCopy_sizeof)
Vk.zero(ic, VkImageCopy_sizeof)
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
@@ -323,31 +341,31 @@ function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h:
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_width, w)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h)
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1)
- Vk.cmd_copy_image(cb, old_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[tex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
- gvk_barrier(cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
- let ok = gvk_once_end(render3d_st, cb)
- Vk.destroy_image_view(render3d_st.gvk_dev, old_view, null)
- Vk.destroy_image(render3d_st.gvk_dev, old_image, null)
- gvk_mem_free(render3d_st, gvk_mem_id(old_mem))
+ Vk.cmd_copy_image(cb, old_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, gvk_tex_image[tex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
+ gvk_barrier(cb, gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+ let ok = gvk_once_end(cb)
+ Vk.destroy_image_view(gvk_dev, old_view, null)
+ Vk.destroy_image(gvk_dev, old_image, null)
+ gvk_mem_free(gvk_mem_id(old_mem))
return ok
}
-function gvk_tex_mips(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> bool {
- if render3d_st.gvk_tex_levels[tex] <= 1 and (w > 1 or h > 1) and not gvk_is_depth(render3d_st.gvk_tex_glfmt[tex]) {
- if not gvk_tex_grow_mips(render3d_st, tex, w, h) { return false }
+function gvk_tex_mips(tex: int, w: int, h: int) -> bool {
+ if gvk_tex_levels[tex] <= 1 and (w > 1 or h > 1) and not gvk_is_depth(gvk_tex_glfmt[tex]) {
+ if not gvk_tex_grow_mips(tex, w, h) { return false }
}
- let levels = render3d_st.gvk_tex_levels[tex]
+ let levels = gvk_tex_levels[tex]
if levels <= 1 { return true }
- let cb = gvk_once_begin(render3d_st)
- gvk_tex_mips_into(render3d_st, cb, tex, w, h)
- return gvk_once_end(render3d_st, cb)
+ let cb = gvk_once_begin()
+ gvk_tex_mips_into(cb, tex, w, h)
+ return gvk_once_end(cb)
}
# the chain's blits and barriers recorded into cb (the frame's own, when one is open)
-function gvk_tex_mips_into(render3d_st: Render3dState, cb: pointer, tex: int, w: int, h: int) -> void {
- let levels = render3d_st.gvk_tex_levels[tex]
+function gvk_tex_mips_into(cb: pointer, tex: int, w: int, h: int) -> void {
+ let levels = gvk_tex_levels[tex]
if levels <= 1 { return }
- let image = render3d_st.gvk_tex_image[tex]
- let layers = render3d_st.gvk_tex_layers[tex]
+ let image = gvk_tex_image[tex]
+ let layers = gvk_tex_layers[tex]
var sw = w
var sh = h
for lv in 1 .. levels {
@@ -381,7 +399,7 @@ function gvk_tex_mips_into(render3d_st: Render3dState, cb: pointer, tex: int, w:
# Level 0 of layer 0 back to the CPU, laid out the OpenGL way when the layouts agree (the terrain
# reads its height field back as R32F into GL_RED / GL_FLOAT)
-function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, out: pointer) -> bool {
+function gvk_tex_read(tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, out: pointer) -> bool {
let cout = gvk_channels(ifmt)
let bout = gvk_channel_bytes(ifmt)
let cwant = gvk_gl_channels(fmt)
@@ -392,11 +410,11 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
return false
}
let n = w * h * cout * bout
- let src = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_DST_BIT)
+ let src = gvk_staging(n, VK_BUFFER_USAGE_TRANSFER_DST_BIT)
if src == null { return false }
- let image = render3d_st.gvk_tex_image[tex]
+ let image = gvk_tex_image[tex]
let depth = gvk_is_depth(ifmt)
- let cb = gvk_once_begin(render3d_st)
+ let cb = gvk_once_begin()
gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
let bic = bytes(VkBufferImageCopy_sizeof)
Vk.zero(bic, VkBufferImageCopy_sizeof)
@@ -406,34 +424,36 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_width, w)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
- Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_st_buf, 1, bic)
+ Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, gvk_st_buf, 1, bic)
gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
- let ok = gvk_once_end(render3d_st, cb)
+ let ok = gvk_once_end(cb)
if ok and cwant == cout { mem_copy(out, src, n) }
if ok and cwant < cout {
let texel = cout * bout
let keep = cwant * bout
for t in 0 .. w * h { mem_copy(mem_off(out, t * keep), mem_off(src, t * texel), keep) }
}
- gvk_staging_free(render3d_st)
+ gvk_staging_free()
return ok
}
-function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void {
- if render3d_st.gvk_tex_image[tex] == 0 { return }
+function gvk_tex_release(tex: int) -> void {
+ if 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)
- Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], null)
- gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_tex_mem[tex]))
- render3d_st.gvk_tex_image[tex] = zero; render3d_st.gvk_tex_view[tex] = zero; render3d_st.gvk_tex_mem[tex] = zero
- render3d_st.gvk_tex_levels[tex] = 0; render3d_st.gvk_tex_layers[tex] = 0; render3d_st.gvk_tex_vkfmt[tex] = 0
- render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1
+ Vk.destroy_image_view(gvk_dev, gvk_tex_view[tex], null)
+ Vk.destroy_image(gvk_dev, gvk_tex_image[tex], null)
+ gvk_mem_free(gvk_mem_id(gvk_tex_mem[tex]))
+ gvk_tex_image[tex] = zero; gvk_tex_view[tex] = zero; gvk_tex_mem[tex] = zero
+ gvk_tex_levels[tex] = 0; gvk_tex_layers[tex] = 0; gvk_tex_vkfmt[tex] = 0
+ gvk_tex_gen[tex] = gvk_tex_gen[tex] + 1
}
# ---- samplers -----------------------------------------------------------------------------
# One VkSampler per distinct way of reading a texture, made the first time it is asked for.
# The arguments are what the renderer set through gpu_tex_param (0 where it set nothing, which
# means GL's own default).
+var gvk_smp_keys: []string = null
+var gvk_smp: []long = null
function gvk_filter(f: int) -> int {
if f == GL_NEAREST or f == GL_NEAREST_MIPMAP_NEAREST or f == GL_NEAREST_MIPMAP_LINEAR { return VK_FILTER_NEAREST }
@@ -460,19 +480,19 @@ function gvk_compare_op(f: int) -> int {
# which is -2.0 at Performance and about -1.6 at Quality. It is applied ONLY while DLSS is live: a
# plain spatial upscale has no temporal accumulation to hide the aliasing a negative bias brings,
# so biasing there would trade blur for shimmer.
-function gvk_mip_bias(render3d_st: mut Render3dState) -> float {
+function gvk_mip_bias() -> float {
# R3D_NO_MIPBIAS=1 puts it back the way it was, so one build can be compared against itself
- if r3d_env_has(render3d_st, "R3D_NO_MIPBIAS") { return 0.0 }
- if not r3d_dlss_live(render3d_st) { return 0.0 }
- let rw = r3d_dlss_render_w(render3d_st)
+ if r3d_env_has("R3D_NO_MIPBIAS") { return 0.0 }
+ if not r3d_dlss_live() { return 0.0 }
+ let rw = r3d_dlss_render_w()
if rw <= 0 or gl_w <= 0 or rw >= gl_w { return 0.0 }
# log2 from the natural log the runtime has: log2(x) = ln(x) * 1/ln(2)
return Math.log(float(rw) / float(gl_w)) * 1.4426950408889634 - 1.0
}
# the sampler for texture tex's parameters, from the texture's own one-entry cache when they have
# not changed since it last asked - a draw asks for every texture it binds
-function gvk_tex_sampler(render3d_st: mut Render3dState, tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
- let bias = float_bits(gvk_mip_bias(render3d_st))
+function gvk_tex_sampler(tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
+ let bias = float_bits(gvk_mip_bias())
var sig = min_f * 31 + mag_f
sig = sig * 31 + wrap_s
sig = sig * 31 + wrap_t
@@ -481,16 +501,16 @@ function gvk_tex_sampler(render3d_st: mut Render3dState, tex: int, min_f: int, m
# the bias is part of what the sampler IS, so it has to invalidate this cache too - otherwise
# turning DLSS on mid-session keeps every sampler already made at the old bias
sig = (sig * 31 + bias) | 1
- if tex > 0 and tex < len(render3d_st.gvk_tex_smp_sig) and render3d_st.gvk_tex_smp_sig[tex] == sig { return render3d_st.gvk_tex_smp[tex] }
- let s = gvk_sampler(render3d_st, min_f, mag_f, wrap_s, wrap_t, compare, aniso)
- if tex > 0 and tex < len(render3d_st.gvk_tex_smp_sig) { render3d_st.gvk_tex_smp_sig[tex] = sig; render3d_st.gvk_tex_smp[tex] = s }
+ if tex > 0 and tex < len(gvk_tex_smp_sig) and gvk_tex_smp_sig[tex] == sig { return gvk_tex_smp[tex] }
+ let s = gvk_sampler(min_f, mag_f, wrap_s, wrap_t, compare, aniso)
+ if tex > 0 and tex < len(gvk_tex_smp_sig) { gvk_tex_smp_sig[tex] = sig; gvk_tex_smp[tex] = s }
return s
}
-function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
- let bias = float_bits(gvk_mip_bias(render3d_st))
+function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
+ let bias = float_bits(gvk_mip_bias())
let key = `{min_f}/{mag_f}/{wrap_s}/{wrap_t}/{compare}/{aniso}/{bias}`
- if render3d_st.gvk_smp_keys == null { render3d_st.gvk_smp_keys = new []string; render3d_st.gvk_smp = new []long }
- for i in 0 .. len(render3d_st.gvk_smp_keys) { if render3d_st.gvk_smp_keys[i] == key { return render3d_st.gvk_smp[i] } }
+ if gvk_smp_keys == null { gvk_smp_keys = new []string; gvk_smp = new []long }
+ for i in 0 .. len(gvk_smp_keys) { if gvk_smp_keys[i] == key { return gvk_smp[i] } }
var mn = min_f
if mn == 0 { mn = GL_NEAREST_MIPMAP_LINEAR }
var mg = mag_f
@@ -511,7 +531,7 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra
if mipmapped { Vk.put_i32(sci, VkSamplerCreateInfo_mipLodBias, bias) }
if aniso > 0x3F800000 and mipmapped {
var a = aniso
- if a > render3d_st.gvk_max_aniso { a = render3d_st.gvk_max_aniso }
+ if a > gvk_max_aniso { a = gvk_max_aniso }
Vk.put_i32(sci, VkSamplerCreateInfo_anisotropyEnable, 1)
Vk.put_i32(sci, VkSamplerCreateInfo_maxAnisotropy, a)
}
@@ -522,11 +542,11 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra
Vk.put_i32(sci, VkSamplerCreateInfo_borderColor, VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE)
let out = bytes(8)
let zero: long = 0
- let r = Vk.create_sampler(render3d_st.gvk_dev, sci, null, out)
- if r != VK_SUCCESS { gvk_fail(render3d_st, "vkCreateSampler", r); return zero }
+ let r = Vk.create_sampler(gvk_dev, sci, null, out)
+ if r != VK_SUCCESS { gvk_fail("vkCreateSampler", r); return zero }
let s = gvk_handle(out)
- push(render3d_st.gvk_smp_keys, key)
- push(render3d_st.gvk_smp, s)
+ push(gvk_smp_keys, key)
+ push(gvk_smp, s)
return s
}
@@ -535,60 +555,69 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra
# indexes these lists. Every buffer can be read as vertices or as indices, so one handle serves
# whichever the renderer binds it as. While the backend comes up every buffer is host-visible and
# an upload maps and copies; staged device-local buffers arrive with the block allocator.
+var gvk_buf: []long = null
+var gvk_buf_mem: []long = null
+var gvk_buf_size: []int = null
+var gvk_buf_map: []pointer = null # host-visible buffers stay mapped for their whole life
+var gvk_buf_used: []int = null # the frame (gvk_frame_no) a draw last bound the buffer in
+var gvk_frame_no: int = 1
# Storage a buffer was moved off, or freed, while the frame that used it has not been submitted:
# destroyed at gvk_retire_flush, after the frame's work is done.
+var gvk_retired_buf: []long = null
+var gvk_retired_mem: []long = null
-function gvk_buf_new(render3d_st: mut Render3dState) -> int {
+function gvk_buf_new() -> int {
let zero: long = 0
- if render3d_st.gvk_buf == null {
- render3d_st.gvk_buf = new []long; render3d_st.gvk_buf_mem = new []long; render3d_st.gvk_buf_size = new []int; render3d_st.gvk_buf_map = new []pointer
- render3d_st.gvk_buf_used = new []int; render3d_st.gvk_retired_buf = new []long; render3d_st.gvk_retired_mem = new []long
+ if gvk_buf == null {
+ gvk_buf = new []long; gvk_buf_mem = new []long; gvk_buf_size = new []int; gvk_buf_map = new []pointer
+ gvk_buf_used = new []int; gvk_retired_buf = new []long; gvk_retired_mem = new []long
# handle 0 is "no buffer", as it is on OpenGL
- push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
+ push(gvk_buf, zero); push(gvk_buf_mem, zero); push(gvk_buf_size, 0); push(gvk_buf_map, null); push(gvk_buf_used, 0)
}
- push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
- return len(render3d_st.gvk_buf) - 1
+ push(gvk_buf, zero); push(gvk_buf_mem, zero); push(gvk_buf_size, 0); push(gvk_buf_map, null); push(gvk_buf_used, 0)
+ return len(gvk_buf) - 1
}
-function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
- if render3d_st.gvk_buf[b] == 0 { return }
+function gvk_buf_release(b: int) -> void {
+ if gvk_buf[b] == 0 { return }
let zero: long = 0
- if render3d_st.gvk_buf_used[b] == render3d_st.gvk_frame_no {
+ if gvk_buf_used[b] == gvk_frame_no {
# a draw recorded this frame 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])
+ push(gvk_retired_buf, gvk_buf[b]); push(gvk_retired_mem, gvk_buf_mem[b])
} else {
- Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null)
- gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b]))
+ Vk.destroy_buffer(gvk_dev, gvk_buf[b], null)
+ gvk_mem_free(gvk_mem_id(gvk_buf_mem[b]))
}
- 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
+ gvk_buf[b] = zero; gvk_buf_mem[b] = zero; gvk_buf_size[b] = 0; gvk_buf_map[b] = null; gvk_buf_used[b] = 0
}
# 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 }
- for i in 0 .. len(render3d_st.gvk_retired_buf) {
- Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null)
- gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i]))
+function gvk_retire_flush() -> void {
+ if gvk_retired_buf == null { return }
+ for i in 0 .. len(gvk_retired_buf) {
+ Vk.destroy_buffer(gvk_dev, gvk_retired_buf[i], null)
+ gvk_mem_free(gvk_mem_id(gvk_retired_mem[i]))
}
- render3d_st.gvk_retired_buf = new []long
- render3d_st.gvk_retired_mem = new []long
+ gvk_retired_buf = new []long
+ gvk_retired_mem = new []long
}
# A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there,
# so it is never swapped for fresh storage because it was used this frame (gvk_buf_reserve).
-function gvk_buf_gpu_owned(render3d_st: mut Render3dState, b: int) -> void {
- if render3d_st.gvk_buf_gpu == null { render3d_st.gvk_buf_gpu = new []int }
- while len(render3d_st.gvk_buf_gpu) <= b { push(render3d_st.gvk_buf_gpu, 0) }
- render3d_st.gvk_buf_gpu[b] = 1
+var gvk_buf_gpu: []int = null
+function gvk_buf_gpu_owned(b: int) -> void {
+ if gvk_buf_gpu == null { gvk_buf_gpu = new []int }
+ while len(gvk_buf_gpu) <= b { push(gvk_buf_gpu, 0) }
+ gvk_buf_gpu[b] = 1
}
-function gvk_buf_is_gpu(render3d_st: Render3dState, b: int) -> bool { return render3d_st.gvk_buf_gpu != null and b < len(render3d_st.gvk_buf_gpu) and render3d_st.gvk_buf_gpu[b] == 1 }
+function gvk_buf_is_gpu(b: int) -> bool { return gvk_buf_gpu != null and b < len(gvk_buf_gpu) and gvk_buf_gpu[b] == 1 }
# Room for at least n bytes behind handle b; a buffer that is already big enough is kept, so a
# stream re-filled every frame allocates once.
-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 }
+function gvk_buf_reserve(b: int, n: int) -> bool {
+ if b <= 0 or b >= len(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 }
- gvk_buf_release(render3d_st, b)
+ if gvk_buf[b] != 0 and gvk_buf_size[b] >= n and (gvk_buf_used[b] != gvk_frame_no or gvk_buf_is_gpu(b)) { return true }
+ gvk_buf_release(b)
var size = n
if size < 64 { size = 64 }
let size_l: long = size
@@ -599,24 +628,24 @@ function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
let out = bytes(8)
- var r = Vk.create_buffer(render3d_st.gvk_dev, bci, null, out)
- if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateBuffer ({size} bytes)`, r) }
+ var r = Vk.create_buffer(gvk_dev, bci, null, out)
+ if r != VK_SUCCESS { return gvk_fail(`vkCreateBuffer ({size} bytes)`, r) }
let buf = gvk_handle(out)
let req = bytes(VkMemoryRequirements_sizeof)
- Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req)
- let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false)
+ Vk.get_buffer_memory_requirements(gvk_dev, buf, req)
+ let ma = gvk_mem_new(req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false)
let zero: long = 0
- if ma == 0 { Vk.destroy_buffer(render3d_st.gvk_dev, buf, null); return false }
+ if ma == 0 { Vk.destroy_buffer(gvk_dev, buf, null); return false }
let mem: long = ma
- r = Vk.bind_buffer_memory(render3d_st.gvk_dev, buf, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma))
- if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkBindBufferMemory", r) }
- render3d_st.gvk_buf[b] = buf; render3d_st.gvk_buf_mem[b] = mem; render3d_st.gvk_buf_size[b] = size; render3d_st.gvk_buf_map[b] = gvk_mem_ptr(render3d_st, ma)
+ r = Vk.bind_buffer_memory(gvk_dev, buf, gvk_mem_handle(ma), gvk_mem_offset(ma))
+ if r != VK_SUCCESS { return gvk_fail("vkBindBufferMemory", r) }
+ gvk_buf[b] = buf; gvk_buf_mem[b] = mem; gvk_buf_size[b] = size; gvk_buf_map[b] = gvk_mem_ptr(ma)
return true
}
# glBufferData: the whole buffer, from data (or storage only when data is null)
-function gvk_buf_upload(render3d_st: mut Render3dState, b: int, n: int, data: pointer) -> bool {
- if not gvk_buf_reserve(render3d_st, b, n) { return false }
- if data != null and n > 0 { mem_copy(render3d_st.gvk_buf_map[b], data, n) }
+function gvk_buf_upload(b: int, n: int, data: pointer) -> bool {
+ if not gvk_buf_reserve(b, n) { return false }
+ if data != null and n > 0 { mem_copy(gvk_buf_map[b], data, n) }
return true
}
diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic
index b1e2d321..d04bd717 100644
--- a/packages/ludic.render3d/grass.ludic
+++ b/packages/ludic.render3d/grass.ludic
@@ -11,35 +11,60 @@
# ============================================================================
const GRASS_CELL: int = 16
+var grass_prog: int = 0
+var grass_mesh: Mesh = null
+var grass_on: bool = true
+var grass_wind: float = 0.0
# A photographed blade, as an atlas of straightened blades side by side (the game sets
# this; the renderer does not name a game asset). 0 = the procedural gradient, which is
# what this was for a year: a two-tone ramp with a hard edge, and every blade in the
# valley the same blade. A real blade has a midrib, a colour that runs olive to straw,
# browning where it has dried and a tip that is its own shape - none of which can be
# written down, only photographed.
+var grass_blade_tex: int = 0
+var grass_blade_cols: int = 8
# Where a body is standing, and how wide it pushes. The grass has never known the player was
# in it: you walked through a meadow and every blade ignored you, which is the single most
# noticeable thing missing from every step the game asks you to take. The game sets this each
# frame; radius 0 means nobody is there.
+var grass_push_x: float = 0.0
+var grass_push_z: float = 0.0
+var grass_push_r: float = 0.0
+var grass_s0: float = 0.0 # float bits: blade spacing at the camera (m)
+var grass_d0: float = 0.0 # the distance at which the spacing has doubled (m)
+var grass_radius: float = 0.0 # no blades past this (m)
+var grass_draws: int = 0
+var grass_dbg: int = 0
# Vulkan with multi-draw indirect: every visible tile is a record in one buffer, uploaded once a
# frame, and each band draws its records GRASS_CHUNK at a time - one draw for up to 256 tiles. A
# record's firstInstance is its place in its chunk times 65536; grass.vert's TILES variant reads that
# place's corner and indices per cell from u_tiles. R3D_GRASS_TILES=1 keeps a draw per tile.
const GRASS_CHUNK: int = 256
const GRASS_RECS: int = 8192
+var grass_merge: bool = false
+var grass_rec: words = null # VkDrawIndexedIndirectCommand records, 5 words each
+var grass_tv: floats = null # per record: corner x, corner z, indices per cell, 0 (float bits)
+var grass_chunk_tv: floats = null
+var grass_n: int = 0
+var grass_cmds: int = 0
+var grass_band_start: words = null # per band: its first record
+var grass_band_cells: words = null # ... and its 16 m cells per tile side
+var grass_band_n: int = 0
# Mesh-shader grass (Settings, Video, Advanced): the chunked path's records, but each chunk is one
# mesh dispatch - work group y a tile, x a batch of GRASS_MESH_BLADES of its blades - so a blade the
# tests reject emits nothing instead of eight degenerate vertices. R3D_MESH_GRASS=1 / 0 overrides.
const GRASS_MESH_BLADES: int = 16
-function r3d_mesh_grass(render3d_st: mut Render3dState, on: bool) -> void {
- render3d_st.grass_mesh_on = on
- if r3d_env_has(render3d_st, "R3D_MESH_GRASS") { render3d_st.grass_mesh_on = Text.to_int(r3d_env(render3d_st, "R3D_MESH_GRASS")) != 0 }
+var grass_mesh_on: bool = false
+var grass_mesh_prog: int = 0
+function r3d_mesh_grass(on: bool) -> void {
+ grass_mesh_on = on
+ if r3d_env_has("R3D_MESH_GRASS") { grass_mesh_on = Text.to_int(r3d_env("R3D_MESH_GRASS")) != 0 }
}
-function grass_mesh_live(render3d_st: Render3dState) -> bool { return render3d_st.grass_mesh_on and render3d_st.grass_merge and render3d_st.grass_mesh_prog != 0 and gpu_has_mesh(render3d_st) }
+function grass_mesh_live() -> bool { return grass_mesh_on and grass_merge and grass_mesh_prog != 0 and gpu_has_mesh() }
# a blade: `rows` rows of 2 vertices (x across, y along, z bend), attribute 2 = uv
-function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh {
- let m = gpu_mesh_new(render3d_st)
+function grass_blade_mesh(rows: int) -> Mesh {
+ let m = gpu_mesh_new()
let v = gl_floats(rows * 2 * 5)
var k = 0
# The blade's PROFILE, and it is the whole difference between grass and a green spike.
@@ -69,9 +94,9 @@ function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh {
k += 5
}
}
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(rows * 2 * 5), GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, 0, 3, GPU_F32, 20, 0, false)
- gpu_mesh_attr(render3d_st, m, 2, 2, GPU_F32, 20, 12, false)
+ gpu_mesh_vertices(m, v, gl_bytes_of(rows * 2 * 5), GPU_STATIC)
+ gpu_mesh_attr(m, 0, 3, GPU_F32, 20, 0, false)
+ gpu_mesh_attr(m, 2, 2, GPU_F32, 20, 12, false)
free(v)
let nq = rows - 1
let idx = words(nq * 6)
@@ -80,65 +105,65 @@ function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh {
idx[q * 6] = b; idx[q * 6 + 1] = b + 1; idx[q * 6 + 2] = b + 2
idx[q * 6 + 3] = b + 1; idx[q * 6 + 4] = b + 3; idx[q * 6 + 5] = b + 2
}
- gpu_mesh_indices(render3d_st, m, data_of(idx), nq * 6 * 4, 4)
+ gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4)
free(idx)
m.count = nq * 6
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
return m
}
-function grass_init(render3d_st: mut Render3dState) -> void {
- render3d_st.grass_merge = gpu_has_mdi(render3d_st) and not r3d_env_has(render3d_st, "R3D_GRASS_TILES")
+function grass_init() -> void {
+ grass_merge = gpu_has_mdi() and not r3d_env_has("R3D_GRASS_TILES")
var defs = "#define FOLIAGE\n#define BLADE\n"
- if render3d_st.grass_merge {
+ if grass_merge {
defs = defs + "#define TILES\n"
- render3d_st.grass_rec = words(GRASS_RECS * 5); render3d_st.grass_tv = floats(GRASS_RECS * 4); render3d_st.grass_chunk_tv = floats(GRASS_CHUNK * 4)
- render3d_st.grass_band_start = words(4); render3d_st.grass_band_cells = words(4)
- render3d_st.grass_cmds = gpu_buffer_new(render3d_st)
+ grass_rec = words(GRASS_RECS * 5); grass_tv = floats(GRASS_RECS * 4); grass_chunk_tv = floats(GRASS_CHUNK * 4)
+ grass_band_start = words(4); grass_band_cells = words(4)
+ grass_cmds = gpu_buffer_new()
}
- render3d_st.grass_prog = r3d_program(render3d_st, "grass.vert", "model.frag", defs)
- if render3d_st.grass_merge and gpu_has_mesh(render3d_st) { render3d_st.grass_mesh_prog = r3d_program(render3d_st, "grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") }
+ grass_prog = r3d_program("grass.vert", "model.frag", defs)
+ if grass_merge and gpu_has_mesh() { grass_mesh_prog = r3d_program("grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") }
# five rows, four quads: the arch above needs somewhere to bend, and at four rows a
# blade that leans over is three straight segments and shows every join
- render3d_st.grass_mesh = grass_blade_mesh(render3d_st, 5)
- render3d_st.grass_wind = 2.4
+ grass_mesh = grass_blade_mesh(5)
+ grass_wind = 2.4
# Matched to the blade's width: a 1 cm blade at 0.11 m spacing covers a third of what a
# 2.8 cm blade did, and the meadow goes bare. The game's graphics settings override this
# (gfx_grass_spacing), but only once game_init has run - a plain headless render never
# gets there, so the two have to agree or a shot shows something no player will see.
# That is exactly how the last change measured as "no effect": the render was identical
# because this line, not the settings, was deciding.
- render3d_st.grass_s0 = 0.066
- render3d_st.grass_d0 = 45.0
- render3d_st.grass_radius = 1600.0
- if r3d_env_has(render3d_st, "R3D_NOBLADES") { render3d_st.grass_on = false }
- if r3d_env_has(render3d_st, "R3D_GRASS_R") { render3d_st.grass_radius = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_R"))) }
- if r3d_env_has(render3d_st, "R3D_GRASS_DBG") { render3d_st.grass_dbg = Text.to_int(r3d_env(render3d_st, "R3D_GRASS_DBG")) }
+ grass_s0 = 0.066
+ grass_d0 = 45.0
+ grass_radius = 1600.0
+ if r3d_env_has("R3D_NOBLADES") { grass_on = false }
+ if r3d_env_has("R3D_GRASS_R") { grass_radius = float(Text.to_int(r3d_env("R3D_GRASS_R"))) }
+ if r3d_env_has("R3D_GRASS_DBG") { grass_dbg = Text.to_int(r3d_env("R3D_GRASS_DBG")) }
}
# indices per 16 m cell that could exist at distance d (the count the shader computes)
-function grass_count_at(render3d_st: Render3dState, d: float) -> int {
- let spacing = render3d_st.grass_s0 * (1.0 + d / render3d_st.grass_d0)
+function grass_count_at(d: float) -> int {
+ let spacing = grass_s0 * (1.0 + d / grass_d0)
let n = float(GRASS_CELL * GRASS_CELL) / (spacing * spacing)
return int(n) + 1
}
# one tile size over one distance band
-function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float) -> void {
- let p = render3d_st.grass_prog
+function grass_tiles(size: int, d_min: float, d_max: float) -> void {
+ let p = grass_prog
let cells = size / GRASS_CELL
- if render3d_st.grass_merge {
- render3d_st.grass_band_start[render3d_st.grass_band_n] = render3d_st.grass_n; render3d_st.grass_band_cells[render3d_st.grass_band_n] = cells; render3d_st.grass_band_n += 1
+ if grass_merge {
+ grass_band_start[grass_band_n] = grass_n; grass_band_cells[grass_band_n] = cells; grass_band_n += 1
} else {
- u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), cells)
+ u_i(gpu_uniform(p, "u_tile_cells"), cells)
}
let sz = float(size)
let half = sz * 0.5
let reach = d_max + half * 1.5
- let tx0 = int(Math.floor((render3d_st.cam_pos[0] - reach) / sz))
- let tx1 = int(Math.floor((render3d_st.cam_pos[0] + reach) / sz))
- let tz0 = int(Math.floor((render3d_st.cam_pos[2] - reach) / sz))
- let tz1 = int(Math.floor((render3d_st.cam_pos[2] + reach) / sz))
+ let tx0 = int(Math.floor((cam_pos[0] - reach) / sz))
+ let tx1 = int(Math.floor((cam_pos[0] + reach) / sz))
+ let tz0 = int(Math.floor((cam_pos[2] - reach) / sz))
+ let tz1 = int(Math.floor((cam_pos[2] + reach) / sz))
let corner_r = half * 1.42
var tz = tz0
while tz <= tz1 {
@@ -146,30 +171,30 @@ function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_
while tx <= tx1 {
let ox = float(tx) * sz; let oz = float(tz) * sz
let cx = ox + half; let cz = oz + half
- let dx = cx - render3d_st.cam_pos[0]; let dz = cz - render3d_st.cam_pos[2]
+ let dx = cx - cam_pos[0]; let dz = cz - cam_pos[2]
let dc = Math.sqrt(dx * dx + dz * dz)
# the tile's nearest and farthest points decide which band it belongs to
let dnear = Math.max(dc - corner_r, 0.0)
if dc < d_min or not (dnear < d_max) { tx += 1; continue }
- let cy = terrain_height(render3d_st, cx, cz)
- if cam_sphere_visible(render3d_st, cx, cy, cz, corner_r + 6.0) {
- let per = grass_count_at(render3d_st, dnear)
- if per > 0 and render3d_st.grass_merge {
- if render3d_st.grass_n < GRASS_RECS {
+ let cy = terrain_height(cx, cz)
+ if cam_sphere_visible(cx, cy, cz, corner_r + 6.0) {
+ let per = grass_count_at(dnear)
+ if per > 0 and grass_merge {
+ if grass_n < GRASS_RECS {
var inst = per * cells * cells
if inst > 65535 { inst = 65535 }
- let r = render3d_st.grass_n * 5
- render3d_st.grass_rec[r] = render3d_st.grass_mesh.count; render3d_st.grass_rec[r + 1] = inst; render3d_st.grass_rec[r + 2] = 0; render3d_st.grass_rec[r + 3] = 0
- render3d_st.grass_rec[r + 4] = ((render3d_st.grass_n - render3d_st.grass_band_start[render3d_st.grass_band_n - 1]) % GRASS_CHUNK) * 65536
- let t = render3d_st.grass_n * 4
- render3d_st.grass_tv[t] = ox; render3d_st.grass_tv[t + 1] = oz; render3d_st.grass_tv[t + 2] = float(per); render3d_st.grass_tv[t + 3] = 0.0
- render3d_st.grass_n += 1
+ let r = grass_n * 5
+ grass_rec[r] = grass_mesh.count; grass_rec[r + 1] = inst; grass_rec[r + 2] = 0; grass_rec[r + 3] = 0
+ grass_rec[r + 4] = ((grass_n - grass_band_start[grass_band_n - 1]) % GRASS_CHUNK) * 65536
+ let t = grass_n * 4
+ grass_tv[t] = ox; grass_tv[t + 1] = oz; grass_tv[t + 2] = float(per); grass_tv[t + 3] = 0.0
+ grass_n += 1
}
} else if per > 0 {
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_tile"), ox, oz)
- u_i(render3d_st, gpu_uniform(render3d_st, p, "u_per_cell"), per)
- mesh_draw_instanced(render3d_st, render3d_st.grass_mesh, per * cells * cells)
- render3d_st.grass_draws += 1
+ u_f2(gpu_uniform(p, "u_tile"), ox, oz)
+ u_i(gpu_uniform(p, "u_per_cell"), per)
+ mesh_draw_instanced(grass_mesh, per * cells * cells)
+ grass_draws += 1
}
}
tx += 1
@@ -178,104 +203,104 @@ function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_
}
}
-function grass_draw(render3d_st: mut Render3dState) -> void {
- if not render3d_st.grass_on or render3d_st.ter_reflect or render3d_st.grass_prog == 0 { return }
- var p = render3d_st.grass_prog
- if grass_mesh_live(render3d_st) { p = render3d_st.grass_mesh_prog }
- gpu_use_program(render3d_st, p)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_vp"), render3d_st.cam_vp_clean)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wind"), render3d_st.grass_wind)
+function grass_draw() -> void {
+ if not grass_on or ter_reflect or grass_prog == 0 { return }
+ var p = grass_prog
+ if grass_mesh_live() { p = grass_mesh_prog }
+ gpu_use_program(p)
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_mat4(gpu_uniform(p, "u_vp"), cam_vp_clean)
+ u_f(gpu_uniform(p, "u_wind"), grass_wind)
# copied into a local first: a global reaching a uniform call is the codegen fault
# CLAUDE.md records against u_wade and u_flutter, and it costs a day every time
- let btex = render3d_st.grass_blade_tex
- let bcols = render3d_st.grass_blade_cols
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_blade_cols"), float(bcols))
+ let btex = grass_blade_tex
+ let bcols = grass_blade_cols
+ u_f(gpu_uniform(p, "u_blade_cols"), float(bcols))
var bon = 0.0
- if btex != 0 { bon = 1.0; r3d_bind_2d(render3d_st, p, "u_blade_tex", 12, btex) }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tex_on"), bon)
- u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_push"), render3d_st.grass_push_x, render3d_st.grass_push_z, render3d_st.grass_push_r)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_rough_scale"), 1.0)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), render3d_st.sc_blade_tint)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_base"), render3d_st.sc_blade_base)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tip"), render3d_st.sc_blade_tip)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_cull"), render3d_st.grass_radius)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_model_h"), 0.0)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_s0"), render3d_st.grass_s0)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_d0"), render3d_st.grass_d0)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), render3d_st.grass_radius)
- u_i(render3d_st, gpu_uniform(render3d_st, p, "u_dbg"), render3d_st.grass_dbg)
- var orthotex = render3d_st.ter_ortho_tex
+ if btex != 0 { bon = 1.0; r3d_bind_2d(p, "u_blade_tex", 12, btex) }
+ u_f(gpu_uniform(p, "u_blade_tex_on"), bon)
+ u_f3(gpu_uniform(p, "u_push"), grass_push_x, grass_push_z, grass_push_r)
+ u_f(gpu_uniform(p, "u_rough_scale"), 1.0)
+ u_v3(gpu_uniform(p, "u_tint"), sc_blade_tint)
+ u_v3(gpu_uniform(p, "u_blade_base"), sc_blade_base)
+ u_v3(gpu_uniform(p, "u_blade_tip"), sc_blade_tip)
+ u_f(gpu_uniform(p, "u_cull"), grass_radius)
+ u_f(gpu_uniform(p, "u_model_h"), 0.0)
+ u_f(gpu_uniform(p, "u_s0"), grass_s0)
+ u_f(gpu_uniform(p, "u_d0"), grass_d0)
+ u_f(gpu_uniform(p, "u_radius"), grass_radius)
+ u_i(gpu_uniform(p, "u_dbg"), grass_dbg)
+ var orthotex = ter_ortho_tex
var oon = 1.0
- if orthotex == 0 { orthotex = render3d_st.ter_height_tex; oon = 0.0 }
- r3d_bind_2d(render3d_st, p, "u_ortho", 4, orthotex)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ortho_on"), oon)
+ if orthotex == 0 { orthotex = ter_height_tex; oon = 0.0 }
+ r3d_bind_2d(p, "u_ortho", 4, orthotex)
+ u_f(gpu_uniform(p, "u_ortho_on"), oon)
var lake = -100000.0
- if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), lake)
+ if ter_lake_ex != 0.0 { lake = ter_lake_level }
+ u_f(gpu_uniform(p, "u_lake_level"), lake)
var sea = lake
- if render3d_st.ter_sea_set { sea = render3d_st.ter_sea_level }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), sea)
- u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_lake"), render3d_st.ter_lake_cx, render3d_st.ter_lake_cz, render3d_st.ter_lake_ex, render3d_st.ter_lake_ez)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_snow_line"), render3d_st.ter_snow_line)
- sky_bind_lighting(render3d_st, p)
- shadow_bind(render3d_st, p)
- fog_bind(render3d_st, p)
+ if ter_sea_set { sea = ter_sea_level }
+ u_f(gpu_uniform(p, "u_sea_level"), sea)
+ u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
+ u_f(gpu_uniform(p, "u_snow_line"), ter_snow_line)
+ sky_bind_lighting(p)
+ shadow_bind(p)
+ fog_bind(p)
# 0.15 was enough to put a hard white highlight down the length of a blade whenever it
# caught the sun, and a white blade of grass is the one thing grass is never. Measured:
# at 0.15, 0.70% of a near-ground frame was over 210 of 255; at 0.0 it is 0.05%. A blade
# does have a faint sheen, so this is small rather than nothing - the foliage layers have
# used 0.05 all along and never showed the fault.
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.008)
- gpu_cull(render3d_st, false)
- render3d_st.grass_draws = 0
- render3d_st.grass_n = 0
- render3d_st.grass_band_n = 0
- gpu_mesh_bind(render3d_st, render3d_st.grass_mesh)
- grass_tiles(render3d_st, 16, 0.0, 300.0)
- grass_tiles(render3d_st, 64, 300.0, 1200.0)
- grass_tiles(render3d_st, 256, 1200.0, render3d_st.grass_radius)
- if render3d_st.grass_merge { grass_flush(render3d_st) }
- gpu_cull(render3d_st, true)
+ u_f(gpu_uniform(p, "u_spec_scale"), 0.008)
+ gpu_cull(false)
+ grass_draws = 0
+ grass_n = 0
+ grass_band_n = 0
+ gpu_mesh_bind(grass_mesh)
+ grass_tiles(16, 0.0, 300.0)
+ grass_tiles(64, 300.0, 1200.0)
+ grass_tiles(256, 1200.0, grass_radius)
+ if grass_merge { grass_flush() }
+ gpu_cull(true)
}
# the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw
-function grass_flush(render3d_st: mut Render3dState) -> void {
- if render3d_st.grass_n == 0 { return }
- var p = render3d_st.grass_prog
- let mesh = grass_mesh_live(render3d_st)
- if mesh { p = render3d_st.grass_mesh_prog } else { gpu_buffer_upload(render3d_st, render3d_st.grass_cmds, render3d_st.grass_n * 20, data_of(render3d_st.grass_rec), GPU_DYNAMIC) }
- for b in 0 .. render3d_st.grass_band_n {
- let s = render3d_st.grass_band_start[b]
- var e = render3d_st.grass_n
- if b + 1 < render3d_st.grass_band_n { e = render3d_st.grass_band_start[b + 1] }
- if e > s { u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), render3d_st.grass_band_cells[b]) }
+function grass_flush() -> void {
+ if grass_n == 0 { return }
+ var p = grass_prog
+ let mesh = grass_mesh_live()
+ if mesh { p = grass_mesh_prog } else { gpu_buffer_upload(grass_cmds, grass_n * 20, data_of(grass_rec), GPU_DYNAMIC) }
+ for b in 0 .. grass_band_n {
+ let s = grass_band_start[b]
+ var e = grass_n
+ if b + 1 < grass_band_n { e = grass_band_start[b + 1] }
+ if e > s { u_i(gpu_uniform(p, "u_tile_cells"), grass_band_cells[b]) }
var k = s
while k < e {
var m = e - k
if m > GRASS_CHUNK { m = GRASS_CHUNK }
- for q in 0 .. m * 4 { render3d_st.grass_chunk_tv[q] = render3d_st.grass_tv[k * 4 + q] }
+ for q in 0 .. m * 4 { grass_chunk_tv[q] = grass_tv[k * 4 + q] }
if mesh {
# one dispatch per tile, as many blade batches as that tile has: sized for a chunk's largest
# tile, the far tiles beside a near one ran thousands of empty invocations (9.8 ms against the
# chunked path's 2 at 4K on an RTX 3070 Ti)
- let cells = render3d_st.grass_band_cells[b]
+ let cells = grass_band_cells[b]
for q in 0 .. m {
- var total = int(render3d_st.grass_tv[(k + q) * 4 + 2]) * cells * cells
+ var total = int(grass_tv[(k + q) * 4 + 2]) * cells * cells
if total > 65535 { total = 65535 }
if total > 0 {
- for c in 0 .. 4 { render3d_st.grass_chunk_tv[c] = render3d_st.grass_tv[(k + q) * 4 + c] }
- u_f4v(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), 1, render3d_st.grass_chunk_tv)
- gpu_draw_mesh_tasks(render3d_st, (total + GRASS_MESH_BLADES - 1) / GRASS_MESH_BLADES, 1, 1)
- render3d_st.grass_draws += 1
+ for c in 0 .. 4 { grass_chunk_tv[c] = grass_tv[(k + q) * 4 + c] }
+ u_f4v(gpu_uniform(p, "u_tiles"), 1, grass_chunk_tv)
+ gpu_draw_mesh_tasks((total + GRASS_MESH_BLADES - 1) / GRASS_MESH_BLADES, 1, 1)
+ grass_draws += 1
}
}
} else {
- u_f4v(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), m, render3d_st.grass_chunk_tv)
- gpu_draw_mesh_indirect(render3d_st, render3d_st.grass_mesh, render3d_st.grass_cmds, k * 20, m, 0, 0)
+ u_f4v(gpu_uniform(p, "u_tiles"), m, grass_chunk_tv)
+ gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0)
}
- if not mesh { render3d_st.grass_draws += 1 }
+ if not mesh { grass_draws += 1 }
k += m
}
}
diff --git a/packages/ludic.render3d/hooks.ludic b/packages/ludic.render3d/hooks.ludic
index 059fa96e..49b27dd2 100644
--- a/packages/ludic.render3d/hooks.ludic
+++ b/packages/ludic.render3d/hooks.ludic
@@ -1,14 +1,17 @@
# hooks.ludic - the scene a frame draws, as callbacks the program registers. They used to be
# functions every program had to define by name (scene_draw, scene_draw_casters, stream_fill),
# a contract only the linker enforced and that allowed one scene per program.
+var r3d_draw_cb: fn() = null
+var r3d_casters_cb: fn(floats) = null
+var r3d_fill_cb: fn(Stream, int, int, int) = null
# what a frame draws, for every pass that draws the scene (the view, the reflection)
-function r3d_on_draw(render3d_st: mut Render3dState, f: fn()) -> void { render3d_st.r3d_draw_cb = f }
+function r3d_on_draw(f: fn()) -> void { r3d_draw_cb = f }
# what casts a shadow, drawn into the light's view `light_vp`
-function r3d_on_casters(render3d_st: mut Render3dState, f: fn(floats)) -> void { render3d_st.r3d_casters_cb = f }
+function r3d_on_casters(f: fn(floats)) -> void { r3d_casters_cb = f }
# a streamed layer's chunk (cx, cz) at detail `band`, filled with its instances
-function r3d_on_stream_fill(render3d_st: mut Render3dState, f: fn(Stream, int, int, int)) -> void { render3d_st.r3d_fill_cb = f }
+function r3d_on_stream_fill(f: fn(Stream, int, int, int)) -> void { r3d_fill_cb = f }
-function r3d_scene_draw(render3d_st: Render3dState) -> void { if render3d_st.r3d_draw_cb != null { render3d_st.r3d_draw_cb() } }
-function r3d_scene_casters(render3d_st: Render3dState, light_vp: floats) -> void { if render3d_st.r3d_casters_cb != null { render3d_st.r3d_casters_cb(light_vp) } }
-function r3d_stream_fill(render3d_st: Render3dState, s: Stream, cx: int, cz: int, band: int) -> void { if render3d_st.r3d_fill_cb != null { render3d_st.r3d_fill_cb(s, cx, cz, band) } }
+function r3d_scene_draw() -> void { if r3d_draw_cb != null { r3d_draw_cb() } }
+function r3d_scene_casters(light_vp: floats) -> void { if r3d_casters_cb != null { r3d_casters_cb(light_vp) } }
+function r3d_stream_fill(s: Stream, cx: int, cz: int, band: int) -> void { if r3d_fill_cb != null { r3d_fill_cb(s, cx, cz, band) } }
diff --git a/packages/ludic.render3d/mesh.ludic b/packages/ludic.render3d/mesh.ludic
index 058289f4..56ace605 100644
--- a/packages/ludic.render3d/mesh.ludic
+++ b/packages/ludic.render3d/mesh.ludic
@@ -20,13 +20,13 @@ property Mesh {
vk_layout: int = 0 # the Vulkan backend's interned id for the recorded layout (0: not yet)
}
-function mesh_draw(render3d_st: mut Render3dState, m: Mesh) -> void { gpu_draw_mesh(render3d_st, m) }
-function mesh_draw_instanced(render3d_st: mut Render3dState, m: Mesh, n: int) -> void { gpu_draw_mesh_instanced(render3d_st, m, n) }
+function mesh_draw(m: Mesh) -> void { gpu_draw_mesh(m) }
+function mesh_draw_instanced(m: Mesh, n: int) -> void { gpu_draw_mesh_instanced(m, n) }
# A flat n x n vertex grid over [-half, half]^2 in x/z, y = 0. Attribute 0 = (x, z).
# The terrain vertex shader lifts it with the height map.
-function mesh_grid(render3d_st: mut Render3dState, n: int, half: float) -> Mesh {
- let m = gpu_mesh_new(render3d_st)
+function mesh_grid(n: int, half: float) -> Mesh {
+ let m = gpu_mesh_new()
let nv = n * n
let v = gl_floats(nv * 2)
var k = 0
@@ -38,8 +38,8 @@ function mesh_grid(render3d_st: mut Render3dState, n: int, half: float) -> Mesh
k += 2
}
}
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(nv * 2), GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, 0, 2, GPU_F32, 8, 0, false)
+ gpu_mesh_vertices(m, v, gl_bytes_of(nv * 2), GPU_STATIC)
+ gpu_mesh_attr(m, 0, 2, GPU_F32, 8, 0, false)
free(v)
let ni = (n - 1) * (n - 1) * 6
let idx = words(ni)
@@ -52,41 +52,41 @@ function mesh_grid(render3d_st: mut Render3dState, n: int, half: float) -> Mesh
k += 6
}
}
- gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4)
+ gpu_mesh_indices(m, data_of(idx), ni * 4, 4)
free(idx)
m.count = ni
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
return m
}
# A full-screen triangle with no attributes (the vertex shader uses gl_VertexID).
-function mesh_fullscreen(render3d_st: Render3dState) -> Mesh {
- let m = gpu_mesh_new(render3d_st)
- gpu_mesh_done(render3d_st, m)
+function mesh_fullscreen() -> Mesh {
+ let m = gpu_mesh_new()
+ gpu_mesh_done(m)
m.count = 3
return m
}
# A unit quad in x/y ([-0.5, 0.5] x [0, 1]) with uv, attribute 0 = xy, 1 = uv.
-function mesh_card(render3d_st: mut Render3dState) -> Mesh {
- let m = gpu_mesh_new(render3d_st)
+function mesh_card() -> Mesh {
+ let m = gpu_mesh_new()
let v = gl_floats(16)
gl_put(v, 0, -0.5); gl_put(v, 1, 0.0); gl_put(v, 2, 0.0); gl_put(v, 3, 0.0)
gl_put(v, 4, 0.5); gl_put(v, 5, 0.0); gl_put(v, 6, 1.0); gl_put(v, 7, 0.0)
gl_put(v, 8, 0.5); gl_put(v, 9, 1.0); gl_put(v, 10, 1.0); gl_put(v, 11, 1.0)
gl_put(v, 12, -0.5); gl_put(v, 13, 1.0); gl_put(v, 14, 0.0); gl_put(v, 15, 1.0)
- gpu_mesh_vertices(render3d_st, m, v, 64, GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, 0, 2, GPU_F32, 16, 0, false)
- gpu_mesh_attr(render3d_st, m, 1, 2, GPU_F32, 16, 8, false)
+ gpu_mesh_vertices(m, v, 64, GPU_STATIC)
+ gpu_mesh_attr(m, 0, 2, GPU_F32, 16, 0, false)
+ gpu_mesh_attr(m, 1, 2, GPU_F32, 16, 8, false)
free(v)
let idx = words(6)
idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3
- gpu_mesh_indices(render3d_st, m, data_of(idx), 24, 4)
+ gpu_mesh_indices(m, data_of(idx), 24, 4)
free(idx)
m.count = 6
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
return m
}
# release a mesh's GPU objects (a mesh this package built for something being thrown away)
-function mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void { gpu_mesh_free(render3d_st, m) }
+function mesh_free(m: Mesh) -> void { gpu_mesh_free(m) }
diff --git a/packages/ludic.render3d/overlay.ludic b/packages/ludic.render3d/overlay.ludic
index 8eebd898..bfd6939f 100644
--- a/packages/ludic.render3d/overlay.ludic
+++ b/packages/ludic.render3d/overlay.ludic
@@ -11,322 +11,360 @@
const OV_MAX_QUADS: int = 6000
const OV_FLOATS: int = 8 # x, y, u, v, r, g, b, a
+var ov_prog: int = 0
+var ov_mesh: Mesh = null
+var ov_vbo: int = 0
+var ov_buf: pointer = null
+var ov_n: int = 0
+var ov_tex: int = 0
+var ov_mode: int = 2 # what the next quads read: 0 the image texture, 1 the font, 2 a flat colour
+var ov_voff: float = 0.0 # float bits: 4 x ov_mode, added to v (overlay.frag)
+var ov_white: int = 0
+var ov_font: int = 0
+var ov_font_adv: floats = null # float bits, em units, one per glyph in the atlas
+var ov_font_n: int = 0 # glyphs in the atlas
# Text is UTF-8, and the atlas says which code points it holds. font.json's "codes" lists
# them in atlas order; an older atlas without it is ASCII from "first" (32) on, which is
# what every font built before this was. ov_font_map[cp] is glyph + 1 for a code point
# under OV_MAP_N (0: not in the atlas); anything above is looked up in the sorted tail.
const OV_MAP_N: int = 8192
+var ov_font_map: words = null
+var ov_font_hi_cp: words = null # code points >= OV_MAP_N, ascending
+var ov_font_hi_g: words = null
+var ov_font_hi_n: int = 0
+var ov_font_space: int = 0 # the glyph for ' ', and for what has none: '?'
+var ov_font_qmark: int = 0
+var ov_font_cols: int = 16
+var ov_font_rows: int = 6
+var ov_font_cell: int = 128 # px per cell in the atlas
+var ov_font_em: int = 100 # px per em in the atlas
+var ov_pad_x: float = 0.0 # float bits, em
+var ov_base_y: float = 0.0
+var ov_ready: bool = false
+var ov_open: bool = false
+var ov_dbg: bool = false
const OV_MAX_RANGES: int = 512
const OV_RANGE_W: int = 7 # texture, first quad, quad count, clip x, y, w, h (w = 0: none)
+var ov_ranges: words = null
+var ov_nr: int = 0
+var ov_range_start: int = 0
+var ov_clip_x: int = 0 # the current clip rectangle in screen pixels (top-left origin)
+var ov_clip_y: int = 0
+var ov_clip_w: int = 0
+var ov_clip_h: int = 0
# The interface is sRGB. While the output is HDR10 the overlay draws with its HDR10 variant, which puts
# the interface at the picture's paper white; the SDR program there sent sRGB values as PQ, and the
# menu's yellow read as red on an HDR display.
+var ov_prog_sdr: int = 0
+var ov_prog_hdr: int = 0
# How bright the interface's white is while HDR10 is on, as a multiple of paper white. It is 1 for the
# interface; a calibration screen draws its test patches at a number of nits with ov_hdr_nits, which
# closes the batch so far - the multiple is one uniform per flush. On an SDR frame it does nothing.
-function ov_hdr_nits(render3d_st: mut Render3dState, nits: float) -> void {
+var ov_hdr_scale: float = 0.0 # float bits; 0 until ov_begin sets 1
+function ov_hdr_nits(nits: float) -> void {
var s = 1.0
- if nits != 0.0 { s = nits / r3d_hdr_paper_nits(render3d_st) }
- if s == render3d_st.ov_hdr_scale { return }
- if render3d_st.ov_open { ov_flush(render3d_st) }
- render3d_st.ov_hdr_scale = s
+ if nits != 0.0 { s = nits / r3d_hdr_paper_nits() }
+ if s == ov_hdr_scale { return }
+ if ov_open { ov_flush() }
+ ov_hdr_scale = s
}
# back to the interface's own white
-function ov_hdr_paper(render3d_st: mut Render3dState) -> void { ov_hdr_nits(render3d_st, 0.0) }
-function ov_pick_prog(render3d_st: mut Render3dState) -> void {
- if gpu_hdr_active(render3d_st) {
- if render3d_st.ov_prog_hdr == 0 {
- r3d_program_log(render3d_st, "overlay.vert", "overlay.frag", "#define HDR10\n")
- render3d_st.ov_prog_hdr = gpu_program(render3d_st, "#version 410 core\n#define HDR10\n" + r3d_shader_file(render3d_st, "overlay.vert"), "#version 410 core\n#define HDR10\n" + r3d_shader_file(render3d_st, "overlay.frag"), "overlay.vert", "overlay.frag", "#define HDR10\n")
+function ov_hdr_paper() -> void { ov_hdr_nits(0.0) }
+function ov_pick_prog() -> void {
+ if gpu_hdr_active() {
+ if ov_prog_hdr == 0 {
+ r3d_program_log("overlay.vert", "overlay.frag", "#define HDR10\n")
+ ov_prog_hdr = gpu_program("#version 410 core\n#define HDR10\n" + r3d_shader_file("overlay.vert"), "#version 410 core\n#define HDR10\n" + r3d_shader_file("overlay.frag"), "overlay.vert", "overlay.frag", "#define HDR10\n")
}
- if render3d_st.ov_prog_hdr != 0 { render3d_st.ov_prog = render3d_st.ov_prog_hdr; return }
+ if ov_prog_hdr != 0 { ov_prog = ov_prog_hdr; return }
}
- render3d_st.ov_prog = render3d_st.ov_prog_sdr
+ ov_prog = ov_prog_sdr
}
-function overlay_init(render3d_st: mut Render3dState, font_dir: string) -> bool {
- r3d_program_log(render3d_st, "overlay.vert", "overlay.frag", "")
- render3d_st.ov_prog = gpu_program(render3d_st, "#version 410 core\n" + r3d_shader_file(render3d_st, "overlay.vert"), "#version 410 core\n" + r3d_shader_file(render3d_st, "overlay.frag"), "overlay.vert", "overlay.frag", "")
- if render3d_st.ov_prog == 0 { print("overlay: program failed"); return false }
- render3d_st.ov_prog_sdr = render3d_st.ov_prog
- render3d_st.ov_mesh = gpu_mesh_new(render3d_st)
- render3d_st.ov_vbo = gpu_mesh_vertices(render3d_st, render3d_st.ov_mesh, null, gl_bytes_of(OV_MAX_QUADS * 6 * OV_FLOATS), GPU_DYNAMIC)
- gpu_mesh_attr(render3d_st, render3d_st.ov_mesh, 0, 2, GPU_F32, OV_FLOATS * 4, 0, false)
- gpu_mesh_attr(render3d_st, render3d_st.ov_mesh, 1, 2, GPU_F32, OV_FLOATS * 4, 8, false)
- gpu_mesh_attr(render3d_st, render3d_st.ov_mesh, 2, 4, GPU_F32, OV_FLOATS * 4, 16, false)
- gpu_mesh_done(render3d_st, render3d_st.ov_mesh)
- render3d_st.ov_buf = gl_floats(OV_MAX_QUADS * 6 * OV_FLOATS)
- render3d_st.ov_ranges = words(OV_MAX_RANGES * OV_RANGE_W)
- render3d_st.ov_white = tex_solid(render3d_st, 255, 255, 255, 255)
- overlay_font(render3d_st, font_dir)
- if render3d_st.ov_font == 0 { print("overlay: no font atlas, text disabled") }
- render3d_st.ov_dbg = r3d_env_has(render3d_st, "R3D_FONTDBG")
- render3d_st.ov_ready = true
+function overlay_init(font_dir: string) -> bool {
+ r3d_program_log("overlay.vert", "overlay.frag", "")
+ ov_prog = gpu_program("#version 410 core\n" + r3d_shader_file("overlay.vert"), "#version 410 core\n" + r3d_shader_file("overlay.frag"), "overlay.vert", "overlay.frag", "")
+ if ov_prog == 0 { print("overlay: program failed"); return false }
+ ov_prog_sdr = ov_prog
+ ov_mesh = gpu_mesh_new()
+ ov_vbo = gpu_mesh_vertices(ov_mesh, null, gl_bytes_of(OV_MAX_QUADS * 6 * OV_FLOATS), GPU_DYNAMIC)
+ gpu_mesh_attr(ov_mesh, 0, 2, GPU_F32, OV_FLOATS * 4, 0, false)
+ gpu_mesh_attr(ov_mesh, 1, 2, GPU_F32, OV_FLOATS * 4, 8, false)
+ gpu_mesh_attr(ov_mesh, 2, 4, GPU_F32, OV_FLOATS * 4, 16, false)
+ gpu_mesh_done(ov_mesh)
+ ov_buf = gl_floats(OV_MAX_QUADS * 6 * OV_FLOATS)
+ ov_ranges = words(OV_MAX_RANGES * OV_RANGE_W)
+ ov_white = tex_solid(255, 255, 255, 255)
+ overlay_font(font_dir)
+ if ov_font == 0 { print("overlay: no font atlas, text disabled") }
+ ov_dbg = r3d_env_has("R3D_FONTDBG")
+ ov_ready = true
return true
}
# Load (or swap to) the font atlas in `font_dir`: font.png and font.json. A game calls it
# again to change fonts at run time - a language whose script the default atlas does not
# carry brings its own. Returns false, and keeps the font it had, if there is none there.
-function overlay_font(render3d_st: mut Render3dState, font_dir: string) -> bool {
+function overlay_font(font_dir: string) -> bool {
let meta = Fs.read_text(font_dir + "/font.json")
if meta == null { return false }
- let tex = tex_load_ex(render3d_st, font_dir + "/font.png", false, 0)
+ let tex = tex_load_ex(font_dir + "/font.png", false, 0)
if tex == 0 { return false }
let j = Json.parse(meta)
- render3d_st.ov_font_cols = jint(j, "cols", 16); render3d_st.ov_font_rows = jint(j, "rows", 6)
- render3d_st.ov_font_cell = int(jnum(value_get(j, "cell"))); render3d_st.ov_font_em = int(jnum(value_get(j, "em")))
- render3d_st.ov_pad_x = 0.14; render3d_st.ov_base_y = 0.30
- if value_has(j, "pad_x") != 0 { render3d_st.ov_pad_x = jnum(value_get(j, "pad_x")) }
- if value_has(j, "base_y") != 0 { render3d_st.ov_base_y = jnum(value_get(j, "base_y")) }
+ ov_font_cols = jint(j, "cols", 16); ov_font_rows = jint(j, "rows", 6)
+ ov_font_cell = int(jnum(value_get(j, "cell"))); ov_font_em = int(jnum(value_get(j, "em")))
+ ov_pad_x = 0.14; ov_base_y = 0.30
+ if value_has(j, "pad_x") != 0 { ov_pad_x = jnum(value_get(j, "pad_x")) }
+ if value_has(j, "base_y") != 0 { ov_base_y = jnum(value_get(j, "base_y")) }
let adv = value_get(j, "adv")
let n = value_count(adv)
let first = jint(j, "first", 32)
var codes: Val = null
if value_has(j, "codes") != 0 { codes = value_get(j, "codes") }
- render3d_st.ov_font_n = n
- render3d_st.ov_font_adv = floats(n + 1)
- for i in 0 .. n { render3d_st.ov_font_adv[i] = jnum(value_at(adv, i)) }
- render3d_st.ov_font_map = words(OV_MAP_N)
- for c in 0 .. OV_MAP_N { render3d_st.ov_font_map[c] = 0 }
- render3d_st.ov_font_hi_cp = words(n + 1); render3d_st.ov_font_hi_g = words(n + 1); render3d_st.ov_font_hi_n = 0
+ ov_font_n = n
+ ov_font_adv = floats(n + 1)
+ for i in 0 .. n { ov_font_adv[i] = jnum(value_at(adv, i)) }
+ ov_font_map = words(OV_MAP_N)
+ for c in 0 .. OV_MAP_N { ov_font_map[c] = 0 }
+ ov_font_hi_cp = words(n + 1); ov_font_hi_g = words(n + 1); ov_font_hi_n = 0
for i in 0 .. n {
var cp = first + i
if codes != null and i < value_count(codes) { cp = int(jnum(value_at(codes, i))) }
- if cp >= 0 and cp < OV_MAP_N { render3d_st.ov_font_map[cp] = i + 1 }
+ if cp >= 0 and cp < OV_MAP_N { ov_font_map[cp] = i + 1 }
else if cp >= OV_MAP_N {
# kept ascending: the builder writes code points in order, so this is an append
- render3d_st.ov_font_hi_cp[render3d_st.ov_font_hi_n] = cp; render3d_st.ov_font_hi_g[render3d_st.ov_font_hi_n] = i; render3d_st.ov_font_hi_n += 1
+ ov_font_hi_cp[ov_font_hi_n] = cp; ov_font_hi_g[ov_font_hi_n] = i; ov_font_hi_n += 1
}
}
- render3d_st.ov_font_space = 0
- if render3d_st.ov_font_map[32] > 0 { render3d_st.ov_font_space = render3d_st.ov_font_map[32] - 1 }
- render3d_st.ov_font_qmark = render3d_st.ov_font_space
- if render3d_st.ov_font_map[63] > 0 { render3d_st.ov_font_qmark = render3d_st.ov_font_map[63] - 1 }
- render3d_st.ov_font = tex
+ ov_font_space = 0
+ if ov_font_map[32] > 0 { ov_font_space = ov_font_map[32] - 1 }
+ ov_font_qmark = ov_font_space
+ if ov_font_map[63] > 0 { ov_font_qmark = ov_font_map[63] - 1 }
+ ov_font = tex
return true
}
# The code point starting at sp[i], with the bytes it took in ov_u8_len. A malformed or
# cut-off sequence is one byte of '?', so a string sliced mid-character still draws.
-function ov_u8(render3d_st: mut Render3dState, sp: pointer, i: int, n: int) -> int {
+var ov_u8_len: int = 1
+function ov_u8(sp: pointer, i: int, n: int) -> int {
let b0 = sp[i] & 255
- render3d_st.ov_u8_len = 1
+ ov_u8_len = 1
if b0 < 128 { return b0 }
if b0 >= 240 and b0 < 248 and i + 3 < n {
- render3d_st.ov_u8_len = 4
+ ov_u8_len = 4
return ((b0 & 7) << 18) | ((sp[i + 1] & 63) << 12) | ((sp[i + 2] & 63) << 6) | (sp[i + 3] & 63)
}
if b0 >= 224 and b0 < 240 and i + 2 < n {
- render3d_st.ov_u8_len = 3
+ ov_u8_len = 3
return ((b0 & 15) << 12) | ((sp[i + 1] & 63) << 6) | (sp[i + 2] & 63)
}
if b0 >= 192 and b0 < 224 and i + 1 < n {
- render3d_st.ov_u8_len = 2
+ ov_u8_len = 2
return ((b0 & 31) << 6) | (sp[i + 1] & 63)
}
return 63
}
# the atlas glyph for a code point: a control character is a space, a missing one is '?'
-function ov_glyph(render3d_st: Render3dState, cp: int) -> int {
- if cp < 32 { return render3d_st.ov_font_space }
+function ov_glyph(cp: int) -> int {
+ if cp < 32 { return ov_font_space }
if cp < OV_MAP_N {
- let g = render3d_st.ov_font_map[cp]
+ let g = ov_font_map[cp]
if g > 0 { return g - 1 }
- return render3d_st.ov_font_qmark
+ return ov_font_qmark
}
var lo = 0
- var hi = render3d_st.ov_font_hi_n - 1
+ var hi = ov_font_hi_n - 1
while lo <= hi {
let mid = (lo + hi) / 2
- if render3d_st.ov_font_hi_cp[mid] == cp { return render3d_st.ov_font_hi_g[mid] }
- if render3d_st.ov_font_hi_cp[mid] < cp { lo = mid + 1 } else { hi = mid - 1 }
+ if ov_font_hi_cp[mid] == cp { return ov_font_hi_g[mid] }
+ if ov_font_hi_cp[mid] < cp { lo = mid + 1 } else { hi = mid - 1 }
}
- return render3d_st.ov_font_qmark
+ return ov_font_qmark
}
# start drawing onto the screen: blending on, depth off
-function ov_begin(render3d_st: mut Render3dState) -> void {
- if not render3d_st.ov_ready { return }
- gpu_fb_bind(render3d_st, gpu_screen_fb(render3d_st))
- gpu_viewport(render3d_st, 0, 0, gl_w, gl_h)
- gpu_depth_test(render3d_st, false)
- gpu_cull(render3d_st, false)
- gpu_blend(render3d_st, true)
- gpu_blend_func(render3d_st, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
- ov_pick_prog(render3d_st)
- gpu_use_program(render3d_st, render3d_st.ov_prog)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.ov_prog, "u_screen"), float(gl_w), float(gl_h))
- render3d_st.ov_n = 0; render3d_st.ov_nr = 0; render3d_st.ov_range_start = 0
- render3d_st.ov_tex = render3d_st.ov_white
- render3d_st.ov_mode = 2; render3d_st.ov_voff = 8.0
- render3d_st.ov_clip_w = 0
- render3d_st.ov_hdr_scale = 1.0
- render3d_st.ov_open = true
+function ov_begin() -> void {
+ if not ov_ready { return }
+ gpu_fb_bind(gpu_screen_fb())
+ gpu_viewport(0, 0, gl_w, gl_h)
+ gpu_depth_test(false)
+ gpu_cull(false)
+ gpu_blend(true)
+ gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ ov_pick_prog()
+ gpu_use_program(ov_prog)
+ u_f2(gpu_uniform(ov_prog, "u_screen"), float(gl_w), float(gl_h))
+ ov_n = 0; ov_nr = 0; ov_range_start = 0
+ ov_tex = ov_white
+ ov_mode = 2; ov_voff = 8.0
+ ov_clip_w = 0
+ ov_hdr_scale = 1.0
+ ov_open = true
}
# everything drawn until ov_unclip stays inside this rectangle (a scrolling list)
-function ov_clip(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void {
- ov_close_range(render3d_st)
- render3d_st.ov_clip_x = x; render3d_st.ov_clip_y = y; render3d_st.ov_clip_w = w; render3d_st.ov_clip_h = h
+function ov_clip(x: int, y: int, w: int, h: int) -> void {
+ ov_close_range()
+ ov_clip_x = x; ov_clip_y = y; ov_clip_w = w; ov_clip_h = h
}
-function ov_unclip(render3d_st: mut Render3dState) -> void { ov_close_range(render3d_st); render3d_st.ov_clip_w = 0 }
+function ov_unclip() -> void { ov_close_range(); ov_clip_w = 0 }
# close the current draw range (quads since its start, with the current texture)
-function ov_close_range(render3d_st: mut Render3dState) -> void {
- let n = render3d_st.ov_n - render3d_st.ov_range_start
+function ov_close_range() -> void {
+ let n = ov_n - ov_range_start
if n <= 0 { return }
- if render3d_st.ov_nr >= OV_MAX_RANGES { ov_flush(render3d_st); return }
- let o = render3d_st.ov_nr * OV_RANGE_W
- render3d_st.ov_ranges[o] = render3d_st.ov_tex; render3d_st.ov_ranges[o + 1] = render3d_st.ov_range_start; render3d_st.ov_ranges[o + 2] = n
- render3d_st.ov_ranges[o + 3] = render3d_st.ov_clip_x; render3d_st.ov_ranges[o + 4] = render3d_st.ov_clip_y; render3d_st.ov_ranges[o + 5] = render3d_st.ov_clip_w; render3d_st.ov_ranges[o + 6] = render3d_st.ov_clip_h
- render3d_st.ov_nr += 1
- render3d_st.ov_range_start = render3d_st.ov_n
+ if ov_nr >= OV_MAX_RANGES { ov_flush(); return }
+ let o = ov_nr * OV_RANGE_W
+ ov_ranges[o] = ov_tex; ov_ranges[o + 1] = ov_range_start; ov_ranges[o + 2] = n
+ ov_ranges[o + 3] = ov_clip_x; ov_ranges[o + 4] = ov_clip_y; ov_ranges[o + 5] = ov_clip_w; ov_ranges[o + 6] = ov_clip_h
+ ov_nr += 1
+ ov_range_start = ov_n
}
# one upload of everything batched so far, then a draw per range
-function ov_flush(render3d_st: mut Render3dState) -> void {
- ov_close_range(render3d_st)
- if render3d_st.ov_n == 0 { render3d_st.ov_nr = 0; render3d_st.ov_range_start = 0; return }
+function ov_flush() -> void {
+ ov_close_range()
+ if ov_n == 0 { ov_nr = 0; ov_range_start = 0; return }
# The overlay draws onto the screen, whatever was bound since ov_begin: a render-scale change
# rebuilds the scene targets mid-frame and leaves framebuffer 0 bound, and on a headless run
# (where the screen is an offscreen framebuffer) every overlay draw after it was an invalid
# framebuffer operation. Saying the target at each flush is what a render pass says anyway.
- gpu_fb_bind(render3d_st, gpu_screen_fb(render3d_st))
- gpu_viewport(render3d_st, 0, 0, gl_w, gl_h)
- ov_pick_prog(render3d_st)
- gpu_use_program(render3d_st, render3d_st.ov_prog)
- gpu_mesh_bind(render3d_st, render3d_st.ov_mesh)
- gpu_buffer_upload(render3d_st, render3d_st.ov_vbo, gl_bytes_of(render3d_st.ov_n * 6 * OV_FLOATS), render3d_st.ov_buf, GPU_STREAM)
- var font = render3d_st.ov_font
- if font == 0 { font = render3d_st.ov_white }
- r3d_bind_2d(render3d_st, render3d_st.ov_prog, "u_font", 1, font)
- var hs = render3d_st.ov_hdr_scale
+ gpu_fb_bind(gpu_screen_fb())
+ gpu_viewport(0, 0, gl_w, gl_h)
+ ov_pick_prog()
+ gpu_use_program(ov_prog)
+ gpu_mesh_bind(ov_mesh)
+ gpu_buffer_upload(ov_vbo, gl_bytes_of(ov_n * 6 * OV_FLOATS), ov_buf, GPU_STREAM)
+ var font = ov_font
+ if font == 0 { font = ov_white }
+ r3d_bind_2d(ov_prog, "u_font", 1, font)
+ var hs = ov_hdr_scale
if hs == 0.0 { hs = 1.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.ov_prog, "u_hdr_paper"), r3d_hdr_paper_nits(render3d_st))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.ov_prog, "u_hdr_scale"), hs)
+ u_f(gpu_uniform(ov_prog, "u_hdr_paper"), r3d_hdr_paper_nits())
+ u_f(gpu_uniform(ov_prog, "u_hdr_scale"), hs)
var last = -1
var clipped = false
- for i in 0 .. render3d_st.ov_nr {
+ for i in 0 .. ov_nr {
let o = i * OV_RANGE_W
- let t = render3d_st.ov_ranges[o]
- if t != last { r3d_bind_2d(render3d_st, render3d_st.ov_prog, "u_tex", 0, t); last = t }
- if render3d_st.ov_ranges[o + 5] > 0 {
+ let t = ov_ranges[o]
+ if t != last { r3d_bind_2d(ov_prog, "u_tex", 0, t); last = t }
+ if ov_ranges[o + 5] > 0 {
clipped = true
- gpu_scissor(render3d_st, render3d_st.ov_ranges[o + 3], render3d_st.ov_ranges[o + 4], render3d_st.ov_ranges[o + 5], render3d_st.ov_ranges[o + 6])
- } else if clipped { gpu_scissor_off(render3d_st); clipped = false }
- gpu_draw_range(render3d_st, render3d_st.ov_mesh, render3d_st.ov_ranges[o + 1] * 6, render3d_st.ov_ranges[o + 2] * 6)
+ gpu_scissor(ov_ranges[o + 3], ov_ranges[o + 4], ov_ranges[o + 5], ov_ranges[o + 6])
+ } else if clipped { gpu_scissor_off(); clipped = false }
+ gpu_draw_range(ov_mesh, ov_ranges[o + 1] * 6, ov_ranges[o + 2] * 6)
}
- if clipped { gpu_scissor_off(render3d_st) }
- gpu_mesh_unbind(render3d_st)
- render3d_st.ov_n = 0; render3d_st.ov_nr = 0; render3d_st.ov_range_start = 0
+ if clipped { gpu_scissor_off() }
+ gpu_mesh_unbind()
+ ov_n = 0; ov_nr = 0; ov_range_start = 0
}
-function ov_end(render3d_st: mut Render3dState) -> void {
- if not render3d_st.ov_open { return }
- ov_flush(render3d_st)
- gpu_blend(render3d_st, false)
- gpu_depth_test(render3d_st, true)
- render3d_st.ov_open = false
+function ov_end() -> void {
+ if not ov_open { return }
+ ov_flush()
+ gpu_blend(false)
+ gpu_depth_test(true)
+ ov_open = false
}
# Text and flat panels never end a draw: the font atlas stays bound beside the image texture and each
# vertex says which it reads (ov_voff). Only a different image texture closes the range. A panel and
# its label used to alternate the white and font textures, and the HUD was 77-91 draws a frame.
-function ov_use_tex(render3d_st: mut Render3dState, t: int) -> void {
- if t != 0 and t == render3d_st.ov_font { render3d_st.ov_mode = 1; render3d_st.ov_voff = 4.0; return }
- if t == render3d_st.ov_white { render3d_st.ov_mode = 2; render3d_st.ov_voff = 8.0; return }
- render3d_st.ov_mode = 0; render3d_st.ov_voff = 0.0
- if t != render3d_st.ov_tex { ov_close_range(render3d_st); render3d_st.ov_tex = t }
+function ov_use_tex(t: int) -> void {
+ if t != 0 and t == ov_font { ov_mode = 1; ov_voff = 4.0; return }
+ if t == ov_white { ov_mode = 2; ov_voff = 8.0; return }
+ ov_mode = 0; ov_voff = 0.0
+ if t != ov_tex { ov_close_range(); ov_tex = t }
}
# one vertex into the batch
-function ov_vert(render3d_st: Render3dState, k: int, x: float, y: float, u: float, v: float, r: float, g: float, b: float, a: float) -> void {
+function ov_vert(k: int, x: float, y: float, u: float, v: float, r: float, g: float, b: float, a: float) -> void {
let o = k * OV_FLOATS
- gl_put_bits(render3d_st.ov_buf, o, float_bits(x)); gl_put_bits(render3d_st.ov_buf, o + 1, float_bits(y))
- gl_put_bits(render3d_st.ov_buf, o + 2, float_bits(u)); gl_put_bits(render3d_st.ov_buf, o + 3, float_bits(v + render3d_st.ov_voff))
- gl_put_bits(render3d_st.ov_buf, o + 4, float_bits(r)); gl_put_bits(render3d_st.ov_buf, o + 5, float_bits(g)); gl_put_bits(render3d_st.ov_buf, o + 6, float_bits(b)); gl_put_bits(render3d_st.ov_buf, o + 7, float_bits(a))
+ gl_put_bits(ov_buf, o, float_bits(x)); gl_put_bits(ov_buf, o + 1, float_bits(y))
+ gl_put_bits(ov_buf, o + 2, float_bits(u)); gl_put_bits(ov_buf, o + 3, float_bits(v + ov_voff))
+ gl_put_bits(ov_buf, o + 4, float_bits(r)); gl_put_bits(ov_buf, o + 5, float_bits(g)); gl_put_bits(ov_buf, o + 6, float_bits(b)); gl_put_bits(ov_buf, o + 7, float_bits(a))
}
# a textured quad, float-bit pixel corners and uvs
-function ov_quad(render3d_st: mut Render3dState, x0: float, y0: float, x1: float, y1: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
- if render3d_st.ov_n >= OV_MAX_QUADS { ov_flush(render3d_st) }
- let k = render3d_st.ov_n * 6
- ov_vert(render3d_st, k, x0, y0, u0, v0, r, g, b, a)
- ov_vert(render3d_st, k + 1, x1, y0, u1, v0, r, g, b, a)
- ov_vert(render3d_st, k + 2, x1, y1, u1, v1, r, g, b, a)
- ov_vert(render3d_st, k + 3, x0, y0, u0, v0, r, g, b, a)
- ov_vert(render3d_st, k + 4, x1, y1, u1, v1, r, g, b, a)
- ov_vert(render3d_st, k + 5, x0, y1, u0, v1, r, g, b, a)
- render3d_st.ov_n += 1
+function ov_quad(x0: float, y0: float, x1: float, y1: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
+ if ov_n >= OV_MAX_QUADS { ov_flush() }
+ let k = ov_n * 6
+ ov_vert(k, x0, y0, u0, v0, r, g, b, a)
+ ov_vert(k + 1, x1, y0, u1, v0, r, g, b, a)
+ ov_vert(k + 2, x1, y1, u1, v1, r, g, b, a)
+ ov_vert(k + 3, x0, y0, u0, v0, r, g, b, a)
+ ov_vert(k + 4, x1, y1, u1, v1, r, g, b, a)
+ ov_vert(k + 5, x0, y1, u0, v1, r, g, b, a)
+ ov_n += 1
}
# a filled rectangle at integer pixels; colour as float bits 0..1
-function ov_rect(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int, r: float, g: float, b: float, a: float) -> void {
- ov_use_tex(render3d_st, render3d_st.ov_white)
- ov_quad(render3d_st, float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, r, g, b, a)
+function ov_rect(x: int, y: int, w: int, h: int, r: float, g: float, b: float, a: float) -> void {
+ ov_use_tex(ov_white)
+ ov_quad(float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, r, g, b, a)
}
-function ov_frame(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int, t: int, r: float, g: float, b: float, a: float) -> void {
- ov_rect(render3d_st, x, y, w, t, r, g, b, a)
- ov_rect(render3d_st, x, y + h - t, w, t, r, g, b, a)
- ov_rect(render3d_st, x, y, t, h, r, g, b, a)
- ov_rect(render3d_st, x + w - t, y, t, h, r, g, b, a)
+function ov_frame(x: int, y: int, w: int, h: int, t: int, r: float, g: float, b: float, a: float) -> void {
+ ov_rect(x, y, w, t, r, g, b, a)
+ ov_rect(x, y + h - t, w, t, r, g, b, a)
+ ov_rect(x, y, t, h, r, g, b, a)
+ ov_rect(x + w - t, y, t, h, r, g, b, a)
}
# a whole texture at integer pixels
-function ov_image(render3d_st: mut Render3dState, tex: int, x: int, y: int, w: int, h: int, a: float) -> void {
- ov_use_tex(render3d_st, tex)
- ov_quad(render3d_st, float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, a)
+function ov_image(tex: int, x: int, y: int, w: int, h: int, a: float) -> void {
+ ov_use_tex(tex)
+ ov_quad(float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, a)
}
# the width in pixels of `s` at `size` pixels per em
-function ov_text_w(render3d_st: mut Render3dState, size: int, s: string) -> int {
- if render3d_st.ov_font_adv == null { return 0 }
+function ov_text_w(size: int, s: string) -> int {
+ if ov_font_adv == null { return 0 }
var w = 0.0
let sp: pointer = s # UTF-8 bytes, not one-character strings
let n = len(sp)
var i = 0
while i < n {
- let g = ov_glyph(render3d_st, ov_u8(render3d_st, sp, i, n))
- i += render3d_st.ov_u8_len
- w = w + render3d_st.ov_font_adv[g] * float(size)
+ let g = ov_glyph(ov_u8(sp, i, n))
+ i += ov_u8_len
+ w = w + ov_font_adv[g] * float(size)
}
return int(w)
}
# text with its top-left at (x, y); returns the pen x after it
-function ov_text(render3d_st: mut Render3dState, x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int {
- if render3d_st.ov_font == 0 { return x }
- ov_use_tex(render3d_st, render3d_st.ov_font)
- let k = float(size) / float(render3d_st.ov_font_em) # atlas px -> screen px
- let cell = float(render3d_st.ov_font_cell) * k
+function ov_text(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int {
+ if ov_font == 0 { return x }
+ ov_use_tex(ov_font)
+ let k = float(size) / float(ov_font_em) # atlas px -> screen px
+ let cell = float(ov_font_cell) * k
var pen = float(x)
let base = float(y) + float(size) * 0.80
- let px = render3d_st.ov_pad_x * float(render3d_st.ov_font_em) * k
- let py = render3d_st.ov_base_y * float(render3d_st.ov_font_em) * k
+ let px = ov_pad_x * float(ov_font_em) * k
+ let py = ov_base_y * float(ov_font_em) * k
let sp: pointer = s
let n = len(sp)
var i = 0
while i < n {
- let c = ov_glyph(render3d_st, ov_u8(render3d_st, sp, i, n))
- i += render3d_st.ov_u8_len
- if c != render3d_st.ov_font_space {
- let cx = c - (c / render3d_st.ov_font_cols) * render3d_st.ov_font_cols
- let cy = c / render3d_st.ov_font_cols
- let u0 = float(cx) / float(render3d_st.ov_font_cols); let u1 = float(cx + 1) / float(render3d_st.ov_font_cols)
- let v0 = float(cy) / float(render3d_st.ov_font_rows); let v1 = float(cy + 1) / float(render3d_st.ov_font_rows)
+ let c = ov_glyph(ov_u8(sp, i, n))
+ i += ov_u8_len
+ if c != ov_font_space {
+ let cx = c - (c / ov_font_cols) * ov_font_cols
+ let cy = c / ov_font_cols
+ let u0 = float(cx) / float(ov_font_cols); let u1 = float(cx + 1) / float(ov_font_cols)
+ let v0 = float(cy) / float(ov_font_rows); let v1 = float(cy + 1) / float(ov_font_rows)
let x0 = pen - px; let y1 = base + py
- ov_quad(render3d_st, x0, y1 - cell, x0 + cell, y1, u0, v0, u1, v1, r, g, b, a)
+ ov_quad(x0, y1 - cell, x0 + cell, y1, u0, v0, u1, v1, r, g, b, a)
}
- pen = pen + render3d_st.ov_font_adv[c] * float(size)
+ pen = pen + ov_font_adv[c] * float(size)
}
return int(pen)
}
# text with a soft dark shadow under it (HUD over a bright meadow)
-function ov_text_sh(render3d_st: mut Render3dState, x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int {
+function ov_text_sh(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int {
let d = size / 18 + 1
- ov_text(render3d_st, x + d, y + d, size, s, 0.0, 0.0, 0.0, a * 0.7)
- return ov_text(render3d_st, x, y, size, s, r, g, b, a)
+ ov_text(x + d, y + d, size, s, 0.0, 0.0, 0.0, a * 0.7)
+ return ov_text(x, y, size, s, r, g, b, a)
}
-function ov_text_center(render3d_st: mut Render3dState, cx: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> void {
- ov_text(render3d_st, cx - ov_text_w(render3d_st, size, s) / 2, y, size, s, r, g, b, a)
+function ov_text_center(cx: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> void {
+ ov_text(cx - ov_text_w(size, s) / 2, y, size, s, r, g, b, a)
}
# text wrapped at `maxw` pixels on spaces; returns the y after the last line
-function ov_text_wrap(render3d_st: mut Render3dState, x: int, y: int, size: int, maxw: int, s: string, r: float, g: float, b: float, a: float) -> int {
+function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: float, g: float, b: float, a: float) -> int {
let sp: pointer = s
let n = len(sp)
var first = 0
@@ -339,11 +377,11 @@ function ov_text_wrap(render3d_st: mut Render3dState, x: int, y: int, size: int,
if sp[i] == 10 { stop = i; break }
if sp[i] == 32 { last_space = i }
let piece: string = s[first .. i + 1]
- if ov_text_w(render3d_st, size, piece) > maxw and last_space > first { stop = last_space; break }
+ if ov_text_w(size, piece) > maxw and last_space > first { stop = last_space; break }
i += 1
}
let line: string = s[first .. stop]
- ov_text(render3d_st, x, ly, size, line, r, g, b, a)
+ ov_text(x, ly, size, line, r, g, b, a)
ly += size * 13 / 10
first = stop
while first < n and (sp[first] == 32 or sp[first] == 10) { first += 1 }
@@ -353,14 +391,14 @@ function ov_text_wrap(render3d_st: mut Render3dState, x: int, y: int, size: int,
# ---- more shapes for a game's interface -------------------------------------------------
# a sub-rectangle of a texture (uv corners as float bits) tinted, at integer pixels
-function ov_sub(render3d_st: mut Render3dState, tex: int, x: int, y: int, w: int, h: int, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
- ov_use_tex(render3d_st, tex)
- ov_quad(render3d_st, float(x), float(y), float(x + w), float(y + h), u0, v0, u1, v1, r, g, b, a)
+function ov_sub(tex: int, x: int, y: int, w: int, h: int, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
+ ov_use_tex(tex)
+ ov_quad(float(x), float(y), float(x + w), float(y + h), u0, v0, u1, v1, r, g, b, a)
}
# a whole texture stretched by nine slices: corners `src` texels wide in a `tw` px square
# texture, drawn `dst` pixels wide, so rounded corners keep their shape at any size
-function ov_nine(render3d_st: mut Render3dState, tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, dst: int, r: float, g: float, b: float, a: float) -> void {
- ov_use_tex(render3d_st, tex)
+function ov_nine(tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, dst: int, r: float, g: float, b: float, a: float) -> void {
+ ov_use_tex(tex)
let s = float(src) / float(tw)
let xs = floats(4); let ys = floats(4); let us = floats(4); let vs = floats(4)
xs[0] = float(x); xs[1] = float(x + dst); xs[2] = float(x + w - dst); xs[3] = float(x + w)
@@ -368,33 +406,33 @@ function ov_nine(render3d_st: mut Render3dState, tex: int, tw: int, src: int, x:
us[0] = 0.0; us[1] = s; us[2] = 1.0 - s; us[3] = 1.0
vs[0] = 0.0; vs[1] = s; vs[2] = 1.0 - s; vs[3] = 1.0
for j in 0 .. 3 {
- for i in 0 .. 3 { ov_quad(render3d_st, xs[i], ys[j], xs[i + 1], ys[j + 1], us[i], vs[j], us[i + 1], vs[j + 1], r, g, b, a) }
+ for i in 0 .. 3 { ov_quad(xs[i], ys[j], xs[i + 1], ys[j + 1], us[i], vs[j], us[i + 1], vs[j + 1], r, g, b, a) }
}
free(xs); free(ys); free(us); free(vs)
}
# an arbitrary quad (float-bit pixel corners, clockwise from top-left) of a texture
-function ov_quad4(render3d_st: mut Render3dState, x0: float, y0: float, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
- if render3d_st.ov_n >= OV_MAX_QUADS { ov_flush(render3d_st) }
- let k = render3d_st.ov_n * 6
- ov_vert(render3d_st, k, x0, y0, u0, v0, r, g, b, a)
- ov_vert(render3d_st, k + 1, x1, y1, u1, v0, r, g, b, a)
- ov_vert(render3d_st, k + 2, x2, y2, u1, v1, r, g, b, a)
- ov_vert(render3d_st, k + 3, x0, y0, u0, v0, r, g, b, a)
- ov_vert(render3d_st, k + 4, x2, y2, u1, v1, r, g, b, a)
- ov_vert(render3d_st, k + 5, x3, y3, u0, v1, r, g, b, a)
- render3d_st.ov_n += 1
+function ov_quad4(x0: float, y0: float, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
+ if ov_n >= OV_MAX_QUADS { ov_flush() }
+ let k = ov_n * 6
+ ov_vert(k, x0, y0, u0, v0, r, g, b, a)
+ ov_vert(k + 1, x1, y1, u1, v0, r, g, b, a)
+ ov_vert(k + 2, x2, y2, u1, v1, r, g, b, a)
+ ov_vert(k + 3, x0, y0, u0, v0, r, g, b, a)
+ ov_vert(k + 4, x2, y2, u1, v1, r, g, b, a)
+ ov_vert(k + 5, x3, y3, u0, v1, r, g, b, a)
+ ov_n += 1
}
# a line of thickness `t` pixels between two points (float-bit pixels)
-function ov_line(render3d_st: mut Render3dState, x0: float, y0: float, x1: float, y1: float, t: float, r: float, g: float, b: float, a: float) -> void {
- ov_use_tex(render3d_st, render3d_st.ov_white)
+function ov_line(x0: float, y0: float, x1: float, y1: float, t: float, r: float, g: float, b: float, a: float) -> void {
+ ov_use_tex(ov_white)
let dx = x1 - x0; let dy = y1 - y0
let l = Math.max(Math.sqrt(dx * dx + dy * dy), 0.001)
let nx = -dy / l * (t * 0.5); let ny = dx / l * (t * 0.5)
- ov_quad4(render3d_st, x0 + nx, y0 + ny, x1 + nx, y1 + ny, x1 - nx, y1 - ny, x0 - nx, y0 - ny, 0.0, 0.0, 1.0, 1.0, r, g, b, a)
+ ov_quad4(x0 + nx, y0 + ny, x1 + nx, y1 + ny, x1 - nx, y1 - ny, x0 - nx, y0 - ny, 0.0, 0.0, 1.0, 1.0, r, g, b, a)
}
# a sub-rectangle of a texture rotated by `ang` radians about its centre (cx, cy), `w` x `h` pixels
-function ov_sub_rot(render3d_st: mut Render3dState, tex: int, cx: int, cy: int, w: int, h: int, ang: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
- ov_use_tex(render3d_st, tex)
+function ov_sub_rot(tex: int, cx: int, cy: int, w: int, h: int, ang: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
+ ov_use_tex(tex)
let c = Math.cos(ang); let s = Math.sin(ang)
let hw = float(w) * 0.5; let hh = float(h) * 0.5
let fx = float(cx); let fy = float(cy)
@@ -403,24 +441,24 @@ function ov_sub_rot(render3d_st: mut Render3dState, tex: int, cx: int, cy: int,
let x1 = fx + (hw * c - -hh * s); let y1 = fy + (hw * s + -hh * c)
let x2 = fx + (hw * c - hh * s); let y2 = fy + (hw * s + hh * c)
let x3 = fx + (-hw * c - hh * s); let y3 = fy + (-hw * s + hh * c)
- ov_quad4(render3d_st, x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, a)
+ ov_quad4(x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, a)
}
# a filled circle approximated by `n` wedges (float-bit centre and radius)
-function ov_disc(render3d_st: mut Render3dState, cx: float, cy: float, rad: float, n: int, r: float, g: float, b: float, a: float) -> void {
- ov_use_tex(render3d_st, render3d_st.ov_white)
+function ov_disc(cx: float, cy: float, rad: float, n: int, r: float, g: float, b: float, a: float) -> void {
+ ov_use_tex(ov_white)
let step = 2.0 * PI / float(n)
for i in 0 .. n {
let a0 = float(i) * step; let a1 = a0 + step
let ax = cx + Math.cos(a0) * rad; let ay = cy + Math.sin(a0) * rad
let bx = cx + Math.cos(a1) * rad; let by = cy + Math.sin(a1) * rad
- ov_quad4(render3d_st, cx, cy, ax, ay, bx, by, cx, cy, 0.0, 0.0, 1.0, 1.0, r, g, b, a)
+ ov_quad4(cx, cy, ax, ay, bx, by, cx, cy, 0.0, 0.0, 1.0, 1.0, r, g, b, a)
}
}
# a ring: `n` segments of thickness `t`, from angle a0 for `span` radians (float bits)
-function ov_arc(render3d_st: mut Render3dState, cx: float, cy: float, rad: float, t: float, a0: float, span: float, n: int, r: float, g: float, b: float, a: float) -> void {
+function ov_arc(cx: float, cy: float, rad: float, t: float, a0: float, span: float, n: int, r: float, g: float, b: float, a: float) -> void {
let step = span / float(n)
for i in 0 .. n {
let b0 = a0 + float(i) * step; let b1 = b0 + step
- ov_line(render3d_st, cx + Math.cos(b0) * rad, cy + Math.sin(b0) * rad, cx + Math.cos(b1) * rad, cy + Math.sin(b1) * rad, t, r, g, b, a)
+ ov_line(cx + Math.cos(b0) * rad, cy + Math.sin(b0) * rad, cx + Math.cos(b1) * rad, cy + Math.sin(b1) * rad, t, r, g, b, a)
}
}
diff --git a/packages/ludic.render3d/post.ludic b/packages/ludic.render3d/post.ludic
index bf53ca1f..14d482bd 100644
--- a/packages/ludic.render3d/post.ludic
+++ b/packages/ludic.render3d/post.ludic
@@ -6,6 +6,27 @@
const BLOOM_LEVELS: int = 6
+var post_hdr: Target = null
+var post_ms_fbo: int = 0 # 4x multisampled scene target, resolved into post_hdr
+var post_ms_samples: int = 1 # temporal AA carries the edges; R3D_MSAA=n to compare
+var post_bloom: []Target = null
+var post_p_down: int = 0
+var post_p_up: int = 0
+var post_p_tone: int = 0
+var post_fs: Mesh = null
+var post_exposure: float = 0.0
+var post_bloom_strength: int = 0
+var post_vignette: float = 0.0
+var post_saturation: float = 0.0
+var post_contrast: float = 0.0
+var post_w: int = 0
+var post_h: int = 0
+var post_auto: bool = true
+var post_key: float = 0.0 # target mean luminance after exposure (float bits)
+var post_lum: words = null
+var post_mips: int = 0
+var post_adapt: float = 0.0 # smoothed exposure (float bits)
+var post_exposure_max: float = 20.0 # 20: the ceiling auto-exposure may reach (night lowers it)
# ---- the grade ------------------------------------------------------------------------------
# White balance, the shadows' floor and the highlights' gain. These were nine literals bound at
# the draw, so the game had the same colour at seven in the morning as at one in the afternoon -
@@ -14,72 +35,119 @@ const BLOOM_LEVELS: int = 6
# program that never starts a day looks exactly as it did.
# They are plain ints rather than a v3 on purpose: a grade is written by daylight_set, which can
# run before post_init has allocated anything, and nine ints cannot be null.
+var post_wb_r: float = 1.02 # 1.02
+var post_wb_g: float = 1.0 # 1.0
+var post_wb_b: float = 0.97 # 0.97
+var post_lift_r: float = 0.004 # 0.004
+var post_lift_g: float = 0.004 # 0.004
+var post_lift_b: float = 0.012 # 0.012
+var post_gain_r: float = 0.99 # 0.99
+var post_gain_g: float = 0.995 # 0.995
+var post_gain_b: float = 1.0 # 1.0
+var post_ao: Target = null
+var post_ao_blur: Target = null
+var post_p_ao: int = 0
+var post_p_ao_blur: int = 0
+var post_ao_radius: float = 0.0
+var post_contact: float = 1.25 # 1.25: how hard a thing is darkened where it meets the ground
+var post_ao_intensity: float = 0.0
+var post_ao_strength: float = 0.0
+var post_gi_strength: float = 0.4 # 0.4
+var post_no_gi: bool = false
+var post_ldr: Target = null
+var post_depth_copy: Target = null
# ---- volumetric light ----------------------------------------------------------------------
# Half resolution on purpose: in-scattered light is smooth, a shaft has no sharp edge, and the
# march is the whole cost of the pass. post_vol_steps is the quality dial; 0 switches it off
# and the pass is skipped entirely rather than run at one step.
+var post_vol: Target = null
+var post_p_vol: int = 0
+var post_vol_steps: float = 24.0 # 24
+var post_vol_density: float = 0.002 # 0.002
+var post_vol_falloff: float = 0.005 # 0.005
+var post_vol_far: float = 800.0 # 800 m
+var post_vol_g: float = 0.6 # 0.6: air throws light forward
+var post_vol_mist: float = 0.0 # the day sets these two
+var post_vol_mist_h: float = 40.0 # 40 m
+var post_prev: Target = null # last frame's scene colour, for the SSGI bounce only
+var post_scene: Target = null # this frame's scene colour before the water, for refraction
+var post_frame: int = 0
+var post_color_w: int = 0 # its size: the display's when DLSS upscaled it
+var post_color_h: int = 0
+var post_color: int = 0 # the HDR colour the rest of post reads # the resolved depth, copied so passes can read it while drawing into the frame
+var post_p_sharp: int = 0
# Spatial anti-aliasing, in the sharpen pass because that pass already reads this pixel's
# neighbourhood and runs last on the LDR image. 1 on, 0 off; the game's setting drives it.
+var post_fxaa: float = 1.0
# ---- depth of field ------------------------------------------------------------------------
# Off in ordinary play - the pass is skipped whole, not run at zero radius. The game turns it on
# behind the viewfinder and says what to focus on.
+var post_p_dof: int = 0
+var post_dof: Target = null
+var post_dof_focus: float = 10.0 # 10 m
+var post_dof_aperture: float = 0.0 # 0 = no lens, and no pass
+var post_dof_max: float = 12.0 # 12 px
+var post_p_tone_hdr: int = 0 # the tonemap's HDR10 variant, made the first time HDR is on
+var post_ldr_hdr: bool = false # post_ldr was made for HDR10 output (10-bit)
# the LDR image is 10-bit while the output is HDR10: PQ in 8 bits bands
-function post_ldr_fmt(render3d_st: Render3dState) -> int { if gpu_hdr_active(render3d_st) { return GL_RGB10_A2 }; return GL_RGBA8 }
+function post_ldr_fmt() -> int { if gpu_hdr_active() { return GL_RGB10_A2 }; return GL_RGBA8 }
+var post_sharpen: float = 0.0
+var post_grain: int = 0
# the screen-sized targets go away before post_init makes them at a new size
-function post_free(render3d_st: mut Render3dState) -> void {
- if render3d_st.post_hdr == null { return }
- if render3d_st.post_ms_fbo != 0 { gpu_fb_free(render3d_st, render3d_st.post_ms_fbo); render3d_st.post_ms_fbo = 0 }
- target_free(render3d_st, render3d_st.post_hdr); target_free(render3d_st, render3d_st.post_ao); target_free(render3d_st, render3d_st.post_ao_blur); target_free(render3d_st, render3d_st.post_ldr)
- target_free(render3d_st, render3d_st.post_depth_copy); target_free(render3d_st, render3d_st.post_prev); target_free(render3d_st, render3d_st.post_scene); target_free(render3d_st, render3d_st.post_vol); target_free(render3d_st, render3d_st.post_dof)
- for i in 0 .. len(render3d_st.post_bloom) { target_free(render3d_st, render3d_st.post_bloom[i]) }
- render3d_st.post_hdr = null
+function post_free() -> void {
+ if post_hdr == null { return }
+ if post_ms_fbo != 0 { gpu_fb_free(post_ms_fbo); post_ms_fbo = 0 }
+ target_free(post_hdr); target_free(post_ao); target_free(post_ao_blur); target_free(post_ldr)
+ target_free(post_depth_copy); target_free(post_prev); target_free(post_scene); target_free(post_vol); target_free(post_dof)
+ for i in 0 .. len(post_bloom) { target_free(post_bloom[i]) }
+ post_hdr = null
}
# Multisampled scene: 1 (temporal AA alone), 2 or 4, remade at once. Vulkan draws it into
# multisampled renderbuffers and resolves them in a pass; a device that cannot take the count asked
# for gets the most it can (gpu_msaa_max).
-function post_msaa_live(render3d_st: Render3dState) -> bool { return gpu_is_gl(render3d_st) or gpu_msaa_max(render3d_st) > 1 }
-function post_set_msaa(render3d_st: mut Render3dState, n: int) -> void {
+function post_msaa_live() -> bool { return gpu_is_gl() or gpu_msaa_max() > 1 }
+function post_set_msaa(n: int) -> void {
var want = n
if want < 1 { want = 1 }
- if not post_msaa_live(render3d_st) { want = 1 }
- if want > gpu_msaa_max(render3d_st) and gpu_msaa_max(render3d_st) >= 1 { want = gpu_msaa_max(render3d_st) }
- if want == render3d_st.post_ms_samples { return }
- render3d_st.post_ms_samples = want
- if render3d_st.post_hdr != null {
- let w = render3d_st.post_w; let h = render3d_st.post_h
- post_free(render3d_st)
- post_init(render3d_st, w, h)
+ if not post_msaa_live() { want = 1 }
+ if want > gpu_msaa_max() and gpu_msaa_max() >= 1 { want = gpu_msaa_max() }
+ if want == post_ms_samples { return }
+ post_ms_samples = want
+ if post_hdr != null {
+ let w = post_w; let h = post_h
+ post_free()
+ post_init(w, h)
}
}
-function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void {
- render3d_st.post_w = w; render3d_st.post_h = h
- render3d_st.post_hdr = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
- if render3d_st.post_ms_samples > 1 {
- render3d_st.post_ms_fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, render3d_st.post_ms_fbo)
- let rbc = gpu_rb_new(render3d_st)
- gpu_rb_storage(render3d_st, rbc, GL_RGBA16F, w, h, render3d_st.post_ms_samples)
- gpu_fb_color_rb(render3d_st, 0, rbc)
- let rbd = gpu_rb_new(render3d_st)
- gpu_rb_storage(render3d_st, rbd, GL_DEPTH_COMPONENT32F, w, h, render3d_st.post_ms_samples)
- gpu_fb_depth_rb(render3d_st, rbd)
- let st = gpu_fb_status(render3d_st)
- if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: msaa framebuffer incomplete {st}`); render3d_st.post_ms_fbo = 0 }
- gpu_fb_bind(render3d_st, 0)
+function post_init(w: int, h: int) -> void {
+ post_w = w; post_h = h
+ post_hdr = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
+ if post_ms_samples > 1 {
+ post_ms_fbo = gpu_fb_new()
+ gpu_fb_bind(post_ms_fbo)
+ let rbc = gpu_rb_new()
+ gpu_rb_storage(rbc, GL_RGBA16F, w, h, post_ms_samples)
+ gpu_fb_color_rb(0, rbc)
+ let rbd = gpu_rb_new()
+ gpu_rb_storage(rbd, GL_DEPTH_COMPONENT32F, w, h, post_ms_samples)
+ gpu_fb_depth_rb(rbd)
+ let st = gpu_fb_status()
+ if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: msaa framebuffer incomplete {st}`); post_ms_fbo = 0 }
+ gpu_fb_bind(0)
}
- render3d_st.post_bloom = new []Target
+ post_bloom = new []Target
var bw = w / 2; var bh = h / 2
for i in 0 .. BLOOM_LEVELS {
- push(render3d_st.post_bloom, target_new(render3d_st, max(bw, 1), max(bh, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR))
+ push(post_bloom, target_new(max(bw, 1), max(bh, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR))
bw = bw / 2; bh = bh / 2
}
- if render3d_st.post_p_down == 0 {
- render3d_st.post_p_down = r3d_program(render3d_st, "fullscreen.vert", "bloom_down.frag", "")
- render3d_st.post_p_up = r3d_program(render3d_st, "fullscreen.vert", "bloom_up.frag", "")
- render3d_st.post_p_tone = r3d_program(render3d_st, "fullscreen.vert", "tonemap.frag", "")
+ if post_p_down == 0 {
+ post_p_down = r3d_program("fullscreen.vert", "bloom_down.frag", "")
+ post_p_up = r3d_program("fullscreen.vert", "bloom_up.frag", "")
+ post_p_tone = r3d_program("fullscreen.vert", "tonemap.frag", "")
}
# Full resolution, not half. The occlusion is reconstructed from depth differences,
# so on a surface seen at a grazing angle its gradient is steep in screen space; at
@@ -87,36 +155,36 @@ function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void {
# upsample in the tonemapper then stretched over the whole ground. They read as thin
# transparent black bars, appear only where there is depth (never on the sky), and
# are nothing to do with the shadow map or the reflection.
- render3d_st.post_ao = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- render3d_st.post_ao_blur = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- if render3d_st.post_p_ao == 0 { render3d_st.post_p_ao = r3d_program(render3d_st, "fullscreen.vert", "ssgi.frag", ""); render3d_st.post_p_ao_blur = r3d_program(render3d_st, "fullscreen.vert", "ssao_blur.frag", "") }
- render3d_st.post_ldr = target_new(render3d_st, w, h, post_ldr_fmt(render3d_st), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
- render3d_st.post_ldr_hdr = gpu_hdr_active(render3d_st)
- render3d_st.post_depth_copy = target_new(render3d_st, w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, true, GL_NEAREST)
- render3d_st.post_dof = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- if render3d_st.post_p_dof == 0 { render3d_st.post_p_dof = r3d_program(render3d_st, "fullscreen.vert", "dof.frag", "") }
- render3d_st.post_vol = target_new(render3d_st, max(w / 2, 1), max(h / 2, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- if render3d_st.post_p_vol == 0 { render3d_st.post_p_vol = r3d_program(render3d_st, "fullscreen.vert", "volumetric.frag", "") }
- render3d_st.post_prev = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- render3d_st.post_scene = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- if render3d_st.post_p_sharp == 0 { render3d_st.post_p_sharp = r3d_program(render3d_st, "fullscreen.vert", "sharpen.frag", "") }
- render3d_st.post_sharpen = 1.2
- render3d_st.post_grain = float_bits(0.025)
- render3d_st.post_ao_radius = 0.7
- render3d_st.post_ao_intensity = 1.4
- render3d_st.post_ao_strength = 0.8
- render3d_st.post_fs = mesh_fullscreen(render3d_st)
- render3d_st.post_exposure = 0.36
- render3d_st.post_bloom_strength = float_bits(0.06)
- render3d_st.post_vignette = 0.35
- render3d_st.post_saturation = 1.04
- render3d_st.post_contrast = 1.12
- render3d_st.post_key = 0.19
- render3d_st.post_lum = words(4)
+ post_ao = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ post_ao_blur = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ if post_p_ao == 0 { post_p_ao = r3d_program("fullscreen.vert", "ssgi.frag", ""); post_p_ao_blur = r3d_program("fullscreen.vert", "ssao_blur.frag", "") }
+ post_ldr = target_new(w, h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
+ post_ldr_hdr = gpu_hdr_active()
+ post_depth_copy = target_new(w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, true, GL_NEAREST)
+ post_dof = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ if post_p_dof == 0 { post_p_dof = r3d_program("fullscreen.vert", "dof.frag", "") }
+ post_vol = target_new(max(w / 2, 1), max(h / 2, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ if post_p_vol == 0 { post_p_vol = r3d_program("fullscreen.vert", "volumetric.frag", "") }
+ post_prev = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ post_scene = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ if post_p_sharp == 0 { post_p_sharp = r3d_program("fullscreen.vert", "sharpen.frag", "") }
+ post_sharpen = 1.2
+ post_grain = float_bits(0.025)
+ post_ao_radius = 0.7
+ post_ao_intensity = 1.4
+ post_ao_strength = 0.8
+ post_fs = mesh_fullscreen()
+ post_exposure = 0.36
+ post_bloom_strength = float_bits(0.06)
+ post_vignette = 0.35
+ post_saturation = 1.04
+ post_contrast = 1.12
+ post_key = 0.19
+ post_lum = words(4)
var m = 1; var sz = max(w, h)
while sz > 1 { sz = sz / 2; m += 1 }
- render3d_st.post_mips = m
- render3d_st.post_adapt = 0.0
+ post_mips = m
+ post_adapt = 0.0
}
# Mean scene luminance from the HDR mip chain -> exposure = key / mean, eased over
@@ -129,59 +197,63 @@ function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void {
# buffer still synchronised the texture: 50% of the CPU's frame waiting, sampled), so
# the adaptation now stays on the GPU: a 1x1 pass (adapt.frag) eases last frame's value
# toward key / mean and the tonemapper samples it. The CPU never waits for the picture.
-function post_measure(render3d_st: mut Render3dState) -> void {
- gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.post_hdr.color)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
- if render3d_st.post_adapt_t == null {
- render3d_st.post_adapt_t = new []Target
- for k in 0 .. 2 { push(render3d_st.post_adapt_t, target_new(render3d_st, 1, 1, GL_R32F, GL_RED, GL_FLOAT, false, GL_NEAREST)) }
- render3d_st.post_adapt_reset = true
+var post_adapt_t: []Target = null
+var post_adapt_i: int = 0
+var post_p_adapt: int = 0
+var post_adapt_reset: bool = true
+function post_measure() -> void {
+ gpu_tex_bind(GPU_TEX2D, post_hdr.color)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_mips(GPU_TEX2D)
+ if post_adapt_t == null {
+ post_adapt_t = new []Target
+ for k in 0 .. 2 { push(post_adapt_t, target_new(1, 1, GL_R32F, GL_RED, GL_FLOAT, false, GL_NEAREST)) }
+ post_adapt_reset = true
}
- if render3d_st.post_p_adapt == 0 { render3d_st.post_p_adapt = r3d_program(render3d_st, "fullscreen.vert", "adapt.frag", "") }
- let next = 1 - render3d_st.post_adapt_i
- target_bind(render3d_st, render3d_st.post_adapt_t[next])
- gpu_depth_test(render3d_st, false)
- gpu_use_program(render3d_st, render3d_st.post_p_adapt)
- r3d_bind_2d(render3d_st, render3d_st.post_p_adapt, "u_scene", 0, render3d_st.post_hdr.color)
- r3d_bind_2d(render3d_st, render3d_st.post_p_adapt, "u_prev", 1, render3d_st.post_adapt_t[render3d_st.post_adapt_i].color)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_lod"), float(render3d_st.post_mips - 1))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_key"), render3d_st.post_key)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_max"), render3d_st.post_exposure_max)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_rate"), 0.08)
+ if post_p_adapt == 0 { post_p_adapt = r3d_program("fullscreen.vert", "adapt.frag", "") }
+ let next = 1 - post_adapt_i
+ target_bind(post_adapt_t[next])
+ gpu_depth_test(false)
+ gpu_use_program(post_p_adapt)
+ r3d_bind_2d(post_p_adapt, "u_scene", 0, post_hdr.color)
+ r3d_bind_2d(post_p_adapt, "u_prev", 1, post_adapt_t[post_adapt_i].color)
+ u_f(gpu_uniform(post_p_adapt, "u_lod"), float(post_mips - 1))
+ u_f(gpu_uniform(post_p_adapt, "u_key"), post_key)
+ u_f(gpu_uniform(post_p_adapt, "u_max"), post_exposure_max)
+ u_f(gpu_uniform(post_p_adapt, "u_rate"), 0.08)
var reset = 0.0
- if render3d_st.post_adapt_reset { reset = 1.0; render3d_st.post_adapt_reset = false }
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_reset"), reset)
- mesh_draw(render3d_st, render3d_st.post_fs)
- render3d_st.post_adapt_i = next
- gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.post_hdr.color)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ if post_adapt_reset { reset = 1.0; post_adapt_reset = false }
+ u_f(gpu_uniform(post_p_adapt, "u_reset"), reset)
+ mesh_draw(post_fs)
+ post_adapt_i = next
+ gpu_tex_bind(GPU_TEX2D, post_hdr.color)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
}
-function post_begin_scene(render3d_st: mut Render3dState) -> void {
- target_bind(render3d_st, render3d_st.post_hdr)
- if render3d_st.post_ms_fbo != 0 { gpu_fb_bind(render3d_st, render3d_st.post_ms_fbo); gpu_multisample(render3d_st, true) }
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- gpu_depth_write(render3d_st, true)
- gpu_cull(render3d_st, true)
- gpu_cull_face(render3d_st, GL_BACK)
- gpu_clear_color(render3d_st, 0.0, 0.0, 0.0, 1.0)
- gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+function post_begin_scene() -> void {
+ target_bind(post_hdr)
+ if post_ms_fbo != 0 { gpu_fb_bind(post_ms_fbo); gpu_multisample(true) }
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ gpu_depth_write(true)
+ gpu_cull(true)
+ gpu_cull_face(GL_BACK)
+ gpu_clear_color(0.0, 0.0, 0.0, 1.0)
+ gpu_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
}
# resolve the multisampled scene into the plain HDR target (colour + depth)
-function post_resolve(render3d_st: mut Render3dState) -> void {
- if render3d_st.post_ms_fbo != 0 {
- gpu_fb_bind_read(render3d_st, render3d_st.post_ms_fbo)
- gpu_fb_bind_draw(render3d_st, render3d_st.post_hdr.fbo)
- gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+function post_resolve() -> void {
+ if post_ms_fbo != 0 {
+ gpu_fb_bind_read(post_ms_fbo)
+ gpu_fb_bind_draw(post_hdr.fbo)
+ gpu_blit(post_w, post_h, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
}
# the depth copy every pass after this may read while the frame is still being drawn into
- gpu_fb_bind_read(render3d_st, render3d_st.post_hdr.fbo)
- gpu_fb_bind_draw(render3d_st, render3d_st.post_depth_copy.fbo)
- gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_DEPTH_BUFFER_BIT)
- gpu_fb_bind(render3d_st, 0)
+ gpu_fb_bind_read(post_hdr.fbo)
+ gpu_fb_bind_draw(post_depth_copy.fbo)
+ gpu_blit(post_w, post_h, GL_DEPTH_BUFFER_BIT)
+ gpu_fb_bind(0)
}
# There is no temporal anti-aliasing. It was reprojecting every pixel through the
@@ -196,194 +268,194 @@ function post_resolve(render3d_st: mut Render3dState) -> void {
# then absorb it, which is what makes the surface read as a body of water rather than a
# sheet laid over the ground: the bottom is seen THROUGH the water, tinted and dimmed by
# how far the light travelled, instead of being the dry terrain showing through an alpha.
-function post_capture_scene(render3d_st: mut Render3dState) -> void {
- gpu_fb_bind_read(render3d_st, render3d_st.post_hdr.fbo)
- gpu_fb_bind_draw(render3d_st, render3d_st.post_scene.fbo)
- gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_COLOR_BUFFER_BIT)
- gpu_fb_bind(render3d_st, render3d_st.post_hdr.fbo)
- gpu_viewport(render3d_st, 0, 0, render3d_st.post_w, render3d_st.post_h)
+function post_capture_scene() -> void {
+ gpu_fb_bind_read(post_hdr.fbo)
+ gpu_fb_bind_draw(post_scene.fbo)
+ gpu_blit(post_w, post_h, GL_COLOR_BUFFER_BIT)
+ gpu_fb_bind(post_hdr.fbo)
+ gpu_viewport(0, 0, post_w, post_h)
}
# Keep a copy of the finished scene colour: the SSGI bounce reads last frame's colour.
-function post_capture_prev(render3d_st: mut Render3dState) -> void {
- gpu_fb_bind_read(render3d_st, render3d_st.post_hdr.fbo)
- gpu_fb_bind_draw(render3d_st, render3d_st.post_prev.fbo)
- gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_COLOR_BUFFER_BIT)
- gpu_fb_bind(render3d_st, 0)
- render3d_st.post_frame += 1
+function post_capture_prev() -> void {
+ gpu_fb_bind_read(post_hdr.fbo)
+ gpu_fb_bind_draw(post_prev.fbo)
+ gpu_blit(post_w, post_h, GL_COLOR_BUFFER_BIT)
+ gpu_fb_bind(0)
+ post_frame += 1
}
-function post_ssao_pass(render3d_st: mut Render3dState) -> void {
- gpu_depth_test(render3d_st, false)
- gpu_blend(render3d_st, false)
- target_bind(render3d_st, render3d_st.post_ao)
- gpu_use_program(render3d_st, render3d_st.post_p_ao)
- r3d_bind_2d(render3d_st, render3d_st.post_p_ao, "u_depth", 0, render3d_st.post_hdr.depth)
- r3d_bind_2d(render3d_st, render3d_st.post_p_ao, "u_prev_color", 1, render3d_st.post_prev.color)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_frame"), float(render3d_st.post_frame % 64))
- u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_inv_proj"), render3d_st.cam_inv_proj)
- u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_proj"), render3d_st.cam_proj)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_texel"), 1.0 / float(render3d_st.post_w), 1.0 / float(render3d_st.post_h))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_radius"), render3d_st.post_ao_radius)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_intensity"), render3d_st.post_ao_intensity)
- var contact = render3d_st.post_contact
- if r3d_env_has(render3d_st, "R3D_NOCONTACT") { contact = 0.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_contact"), contact)
- mesh_draw(render3d_st, render3d_st.post_fs)
- target_bind(render3d_st, render3d_st.post_ao_blur)
- gpu_use_program(render3d_st, render3d_st.post_p_ao_blur)
- r3d_bind_2d(render3d_st, render3d_st.post_p_ao_blur, "u_ao", 0, render3d_st.post_ao.color)
- r3d_bind_2d(render3d_st, render3d_st.post_p_ao_blur, "u_depth", 1, render3d_st.post_hdr.depth)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao_blur, "u_texel"), 1.0 / float(render3d_st.post_ao.w), 1.0 / float(render3d_st.post_ao.h))
- mesh_draw(render3d_st, render3d_st.post_fs)
+function post_ssao_pass() -> void {
+ gpu_depth_test(false)
+ gpu_blend(false)
+ target_bind(post_ao)
+ gpu_use_program(post_p_ao)
+ r3d_bind_2d(post_p_ao, "u_depth", 0, post_hdr.depth)
+ r3d_bind_2d(post_p_ao, "u_prev_color", 1, post_prev.color)
+ u_f(gpu_uniform(post_p_ao, "u_frame"), float(post_frame % 64))
+ u_mat4(gpu_uniform(post_p_ao, "u_inv_proj"), cam_inv_proj)
+ u_mat4(gpu_uniform(post_p_ao, "u_proj"), cam_proj)
+ u_f2(gpu_uniform(post_p_ao, "u_texel"), 1.0 / float(post_w), 1.0 / float(post_h))
+ u_f(gpu_uniform(post_p_ao, "u_radius"), post_ao_radius)
+ u_f(gpu_uniform(post_p_ao, "u_intensity"), post_ao_intensity)
+ var contact = post_contact
+ if r3d_env_has("R3D_NOCONTACT") { contact = 0.0 }
+ u_f(gpu_uniform(post_p_ao, "u_contact"), contact)
+ mesh_draw(post_fs)
+ target_bind(post_ao_blur)
+ gpu_use_program(post_p_ao_blur)
+ r3d_bind_2d(post_p_ao_blur, "u_ao", 0, post_ao.color)
+ r3d_bind_2d(post_p_ao_blur, "u_depth", 1, post_hdr.depth)
+ u_f2(gpu_uniform(post_p_ao_blur, "u_texel"), 1.0 / float(post_ao.w), 1.0 / float(post_ao.h))
+ mesh_draw(post_fs)
}
# The march, then the composite. It is added to the scene BEFORE bloom on purpose: a shaft of
# light is a bright thing in the air and should bloom like one, and compositing it after the
# bloom pyramid would give hard-edged rays with no glow at all.
-function post_volumetric_pass(render3d_st: mut Render3dState) -> void {
- if render3d_st.post_vol_steps <= 0.0 { return }
- if r3d_env_has(render3d_st, "R3D_NOVOL") { return }
- gpu_depth_test(render3d_st, false)
- gpu_blend(render3d_st, false)
- target_bind(render3d_st, render3d_st.post_vol)
- gpu_use_program(render3d_st, render3d_st.post_p_vol)
- r3d_bind_2d(render3d_st, render3d_st.post_p_vol, "u_depth", 0, render3d_st.post_hdr.depth)
- shadow_bind(render3d_st, render3d_st.post_p_vol)
- sky_bind_lighting(render3d_st, render3d_st.post_p_vol)
- sky_bind_rot(render3d_st, render3d_st.post_p_vol)
- fog_bind(render3d_st, render3d_st.post_p_vol)
- u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_inv_vp"), render3d_st.cam_inv_vp)
- u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_cam_pos"), render3d_st.cam_pos)
- u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_sun_dir"), render3d_st.sun_dir)
- u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_sun_color"), render3d_st.sun_color)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_steps"), render3d_st.post_vol_steps)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_density"), render3d_st.post_vol_density)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_falloff"), render3d_st.post_vol_falloff)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_far"), render3d_st.post_vol_far)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_g"), render3d_st.post_vol_g)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_mist"), render3d_st.post_vol_mist)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_mist_h"), render3d_st.post_vol_mist_h)
- mesh_draw(render3d_st, render3d_st.post_fs)
+function post_volumetric_pass() -> void {
+ if post_vol_steps <= 0.0 { return }
+ if r3d_env_has("R3D_NOVOL") { return }
+ gpu_depth_test(false)
+ gpu_blend(false)
+ target_bind(post_vol)
+ gpu_use_program(post_p_vol)
+ r3d_bind_2d(post_p_vol, "u_depth", 0, post_hdr.depth)
+ shadow_bind(post_p_vol)
+ sky_bind_lighting(post_p_vol)
+ sky_bind_rot(post_p_vol)
+ fog_bind(post_p_vol)
+ u_mat4(gpu_uniform(post_p_vol, "u_inv_vp"), cam_inv_vp)
+ u_v3(gpu_uniform(post_p_vol, "u_cam_pos"), cam_pos)
+ u_v3(gpu_uniform(post_p_vol, "u_sun_dir"), sun_dir)
+ u_v3(gpu_uniform(post_p_vol, "u_sun_color"), sun_color)
+ u_f(gpu_uniform(post_p_vol, "u_vol_steps"), post_vol_steps)
+ u_f(gpu_uniform(post_p_vol, "u_vol_density"), post_vol_density)
+ u_f(gpu_uniform(post_p_vol, "u_vol_falloff"), post_vol_falloff)
+ u_f(gpu_uniform(post_p_vol, "u_vol_far"), post_vol_far)
+ u_f(gpu_uniform(post_p_vol, "u_vol_g"), post_vol_g)
+ u_f(gpu_uniform(post_p_vol, "u_vol_mist"), post_vol_mist)
+ u_f(gpu_uniform(post_p_vol, "u_vol_mist_h"), post_vol_mist_h)
+ mesh_draw(post_fs)
# Composite into the HDR scene with the bloom pyramid's own upsample - a 3x3 tent under
# ONE/ONE blending, which is exactly what is wanted here and already exists, rather than a
# blit shader written for one caller. Into post_hdr, not post_scene: post_scene is the copy
# the water refracts, so adding shafts there would put them UNDER the lake.
- target_bind(render3d_st, render3d_st.post_hdr)
- gpu_blend(render3d_st, true)
- gpu_blend_func(render3d_st, GL_ONE, GL_ONE)
- gpu_use_program(render3d_st, render3d_st.post_p_up)
- r3d_bind_2d(render3d_st, render3d_st.post_p_up, "u_src", 0, render3d_st.post_vol.color)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_texel"), 1.0 / float(render3d_st.post_vol.w), 1.0 / float(render3d_st.post_vol.h))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_radius"), 1.0)
- mesh_draw(render3d_st, render3d_st.post_fs)
- gpu_blend(render3d_st, false)
+ target_bind(post_hdr)
+ gpu_blend(true)
+ gpu_blend_func(GL_ONE, GL_ONE)
+ gpu_use_program(post_p_up)
+ r3d_bind_2d(post_p_up, "u_src", 0, post_vol.color)
+ u_f2(gpu_uniform(post_p_up, "u_texel"), 1.0 / float(post_vol.w), 1.0 / float(post_vol.h))
+ u_f(gpu_uniform(post_p_up, "u_radius"), 1.0)
+ mesh_draw(post_fs)
+ gpu_blend(false)
}
# The lens, between the scene and the bloom: a blurred highlight should still bloom, and a
# bloom smeared by the lens afterwards would be a halo round nothing.
-function post_dof_pass(render3d_st: mut Render3dState) -> void {
- if render3d_st.post_dof_aperture == 0.0 { return }
- gpu_depth_test(render3d_st, false)
- gpu_blend(render3d_st, false)
- target_bind(render3d_st, render3d_st.post_dof)
- gpu_use_program(render3d_st, render3d_st.post_p_dof)
- r3d_bind_2d(render3d_st, render3d_st.post_p_dof, "u_src", 0, render3d_st.post_color)
- r3d_bind_2d(render3d_st, render3d_st.post_p_dof, "u_depth", 1, render3d_st.post_hdr.depth)
- u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_inv_proj"), render3d_st.cam_inv_proj)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_texel"), 1.0 / float(render3d_st.post_w), 1.0 / float(render3d_st.post_h))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_focus"), render3d_st.post_dof_focus)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_aperture"), render3d_st.post_dof_aperture)
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_max_coc"), render3d_st.post_dof_max)
- mesh_draw(render3d_st, render3d_st.post_fs)
- render3d_st.post_color = render3d_st.post_dof.color
+function post_dof_pass() -> void {
+ if post_dof_aperture == 0.0 { return }
+ gpu_depth_test(false)
+ gpu_blend(false)
+ target_bind(post_dof)
+ gpu_use_program(post_p_dof)
+ r3d_bind_2d(post_p_dof, "u_src", 0, post_color)
+ r3d_bind_2d(post_p_dof, "u_depth", 1, post_hdr.depth)
+ u_mat4(gpu_uniform(post_p_dof, "u_inv_proj"), cam_inv_proj)
+ u_f2(gpu_uniform(post_p_dof, "u_texel"), 1.0 / float(post_w), 1.0 / float(post_h))
+ u_f(gpu_uniform(post_p_dof, "u_focus"), post_dof_focus)
+ u_f(gpu_uniform(post_p_dof, "u_aperture"), post_dof_aperture)
+ u_f(gpu_uniform(post_p_dof, "u_max_coc"), post_dof_max)
+ mesh_draw(post_fs)
+ post_color = post_dof.color
}
-function post_bloom_pass(render3d_st: mut Render3dState) -> void {
- gpu_depth_test(render3d_st, false)
- gpu_blend(render3d_st, false)
- var src = render3d_st.post_color
- var sw = render3d_st.post_color_w; var sh = render3d_st.post_color_h
- gpu_use_program(render3d_st, render3d_st.post_p_down)
+function post_bloom_pass() -> void {
+ gpu_depth_test(false)
+ gpu_blend(false)
+ var src = post_color
+ var sw = post_color_w; var sh = post_color_h
+ gpu_use_program(post_p_down)
for i in 0 .. BLOOM_LEVELS {
- let t = render3d_st.post_bloom[i]
- target_bind(render3d_st, t)
- r3d_bind_2d(render3d_st, render3d_st.post_p_down, "u_src", 0, src)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_down, "u_texel"), 1.0 / float(sw), 1.0 / float(sh))
+ let t = post_bloom[i]
+ target_bind(t)
+ r3d_bind_2d(post_p_down, "u_src", 0, src)
+ u_f2(gpu_uniform(post_p_down, "u_texel"), 1.0 / float(sw), 1.0 / float(sh))
var th = -1.0
if i == 0 { th = 1.2 }
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_down, "u_threshold"), th)
- mesh_draw(render3d_st, render3d_st.post_fs)
+ u_f(gpu_uniform(post_p_down, "u_threshold"), th)
+ mesh_draw(post_fs)
src = t.color; sw = t.w; sh = t.h
}
- gpu_use_program(render3d_st, render3d_st.post_p_up)
- gpu_blend(render3d_st, true)
- gpu_blend_func(render3d_st, GL_ONE, GL_ONE)
+ gpu_use_program(post_p_up)
+ gpu_blend(true)
+ gpu_blend_func(GL_ONE, GL_ONE)
var i = BLOOM_LEVELS - 1
while i > 0 {
- let from = render3d_st.post_bloom[i]
- let to = render3d_st.post_bloom[i - 1]
- target_bind(render3d_st, to)
- r3d_bind_2d(render3d_st, render3d_st.post_p_up, "u_src", 0, from.color)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_texel"), 1.0 / float(from.w), 1.0 / float(from.h))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_radius"), 1.0)
- mesh_draw(render3d_st, render3d_st.post_fs)
+ let from = post_bloom[i]
+ let to = post_bloom[i - 1]
+ target_bind(to)
+ r3d_bind_2d(post_p_up, "u_src", 0, from.color)
+ u_f2(gpu_uniform(post_p_up, "u_texel"), 1.0 / float(from.w), 1.0 / float(from.h))
+ u_f(gpu_uniform(post_p_up, "u_radius"), 1.0)
+ mesh_draw(post_fs)
i -= 1
}
- gpu_blend(render3d_st, false)
+ gpu_blend(false)
}
-function post_tonemap(render3d_st: mut Render3dState, color_tex: int) -> void {
- var prog = render3d_st.post_p_tone
- if gpu_hdr_active(render3d_st) {
- if render3d_st.post_p_tone_hdr == 0 { render3d_st.post_p_tone_hdr = r3d_program(render3d_st, "fullscreen.vert", "tonemap.frag", "#define HDR10\n") }
- prog = render3d_st.post_p_tone_hdr
+function post_tonemap(color_tex: int) -> void {
+ var prog = post_p_tone
+ if gpu_hdr_active() {
+ if post_p_tone_hdr == 0 { post_p_tone_hdr = r3d_program("fullscreen.vert", "tonemap.frag", "#define HDR10\n") }
+ prog = post_p_tone_hdr
}
- if render3d_st.post_ldr_hdr != gpu_hdr_active(render3d_st) {
- let w = render3d_st.post_ldr.w; let h = render3d_st.post_ldr.h
- target_free(render3d_st, render3d_st.post_ldr)
- render3d_st.post_ldr = target_new(render3d_st, w, h, post_ldr_fmt(render3d_st), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
- render3d_st.post_ldr_hdr = gpu_hdr_active(render3d_st)
+ if post_ldr_hdr != gpu_hdr_active() {
+ let w = post_ldr.w; let h = post_ldr.h
+ target_free(post_ldr)
+ post_ldr = target_new(w, h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
+ post_ldr_hdr = gpu_hdr_active()
}
- if render3d_st.post_auto { post_measure(render3d_st) }
- target_bind(render3d_st, render3d_st.post_ldr)
- gpu_depth_test(render3d_st, false)
- gpu_use_program(render3d_st, prog)
- r3d_bind_2d(render3d_st, prog, "u_hdr", 0, color_tex)
- r3d_bind_2d(render3d_st, prog, "u_bloom", 1, render3d_st.post_bloom[0].color)
- r3d_bind_2d(render3d_st, prog, "u_ao", 2, render3d_st.post_ao_blur.color)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ao_strength"), render3d_st.post_ao_strength)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_gi_strength"), render3d_st.post_gi_strength)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_exposure"), render3d_st.post_exposure)
+ if post_auto { post_measure() }
+ target_bind(post_ldr)
+ gpu_depth_test(false)
+ gpu_use_program(prog)
+ r3d_bind_2d(prog, "u_hdr", 0, color_tex)
+ r3d_bind_2d(prog, "u_bloom", 1, post_bloom[0].color)
+ r3d_bind_2d(prog, "u_ao", 2, post_ao_blur.color)
+ u_f(gpu_uniform(prog, "u_ao_strength"), post_ao_strength)
+ u_f(gpu_uniform(prog, "u_gi_strength"), post_gi_strength)
+ u_f(gpu_uniform(prog, "u_exposure"), post_exposure)
var auto = 0.0
- if render3d_st.post_auto and render3d_st.post_adapt_t != null { auto = 1.0; r3d_bind_2d(render3d_st, prog, "u_adapt", 3, render3d_st.post_adapt_t[render3d_st.post_adapt_i].color) }
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_auto"), auto)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_bloom_strength"), float_from_bits(render3d_st.post_bloom_strength))
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_vignette"), render3d_st.post_vignette)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_saturation"), render3d_st.post_saturation)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_contrast"), render3d_st.post_contrast)
- u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_wb"), render3d_st.post_wb_r, render3d_st.post_wb_g, render3d_st.post_wb_b)
- u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_lift"), render3d_st.post_lift_r, render3d_st.post_lift_g, render3d_st.post_lift_b)
- u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_gain"), render3d_st.post_gain_r, render3d_st.post_gain_g, render3d_st.post_gain_b)
+ if post_auto and post_adapt_t != null { auto = 1.0; r3d_bind_2d(prog, "u_adapt", 3, post_adapt_t[post_adapt_i].color) }
+ u_f(gpu_uniform(prog, "u_auto"), auto)
+ u_f(gpu_uniform(prog, "u_bloom_strength"), float_from_bits(post_bloom_strength))
+ u_f(gpu_uniform(prog, "u_vignette"), post_vignette)
+ u_f(gpu_uniform(prog, "u_saturation"), post_saturation)
+ u_f(gpu_uniform(prog, "u_contrast"), post_contrast)
+ u_f3(gpu_uniform(prog, "u_wb"), post_wb_r, post_wb_g, post_wb_b)
+ u_f3(gpu_uniform(prog, "u_lift"), post_lift_r, post_lift_g, post_lift_b)
+ u_f3(gpu_uniform(prog, "u_gain"), post_gain_r, post_gain_g, post_gain_b)
# the HDR10 variant's display calibration (the SDR program has none of these, and -1 sets nothing)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hdr_peak"), r3d_hdr_peak_nits(render3d_st))
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hdr_paper"), r3d_hdr_paper_nits(render3d_st))
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hdr_black"), r3d_hdr_black_nits(render3d_st))
- mesh_draw(render3d_st, render3d_st.post_fs)
+ u_f(gpu_uniform(prog, "u_hdr_peak"), r3d_hdr_peak_nits())
+ u_f(gpu_uniform(prog, "u_hdr_paper"), r3d_hdr_paper_nits())
+ u_f(gpu_uniform(prog, "u_hdr_black"), r3d_hdr_black_nits())
+ mesh_draw(post_fs)
# sharpen + grain onto the screen
- gpu_fb_bind(render3d_st, gpu_screen_fb(render3d_st))
- gpu_viewport(render3d_st, 0, 0, gl_w, gl_h)
- gpu_use_program(render3d_st, render3d_st.post_p_sharp)
- r3d_bind_2d(render3d_st, render3d_st.post_p_sharp, "u_src", 0, render3d_st.post_ldr.color)
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_texel"), 1.0 / float(render3d_st.post_ldr.w), 1.0 / float(render3d_st.post_ldr.h))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_amount"), render3d_st.post_sharpen)
- var fx = render3d_st.post_fxaa
- if r3d_env_has(render3d_st, "R3D_NOFXAA") { fx = 0.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_fxaa"), fx)
+ gpu_fb_bind(gpu_screen_fb())
+ gpu_viewport(0, 0, gl_w, gl_h)
+ gpu_use_program(post_p_sharp)
+ r3d_bind_2d(post_p_sharp, "u_src", 0, post_ldr.color)
+ u_f2(gpu_uniform(post_p_sharp, "u_texel"), 1.0 / float(post_ldr.w), 1.0 / float(post_ldr.h))
+ u_f(gpu_uniform(post_p_sharp, "u_amount"), post_sharpen)
+ var fx = post_fxaa
+ if r3d_env_has("R3D_NOFXAA") { fx = 0.0 }
+ u_f(gpu_uniform(post_p_sharp, "u_fxaa"), fx)
# R3D_NOGRAIN=1: no film grain, so two frames of a still camera can be compared for what else moves
- var grain = render3d_st.post_grain
- if r3d_env_has(render3d_st, "R3D_NOGRAIN") { grain = float_bits(0.0) }
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_grain"), float_from_bits(grain))
- u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_time"), render3d_st.r3d_time)
- mesh_draw(render3d_st, render3d_st.post_fs)
+ var grain = post_grain
+ if r3d_env_has("R3D_NOGRAIN") { grain = float_bits(0.0) }
+ u_f(gpu_uniform(post_p_sharp, "u_grain"), float_from_bits(grain))
+ u_f(gpu_uniform(post_p_sharp, "u_time"), r3d_time)
+ mesh_draw(post_fs)
}
diff --git a/packages/ludic.render3d/prof.ludic b/packages/ludic.render3d/prof.ludic
index 32bd1391..e3fa3739 100644
--- a/packages/ludic.render3d/prof.ludic
+++ b/packages/ludic.render3d/prof.ludic
@@ -14,89 +14,98 @@
const PROF_SLOTS: int = 16
const PROF_RING: int = 4 # frames of latency before a result is read
+var prof_on: bool = false
+var prof_names: []pointer = null
+var prof_ids: words = null # PROF_SLOTS * PROF_RING query objects
+var prof_ns: []long = null # accumulated nanoseconds per slot
+var prof_hits: []long = null # samples accumulated per slot
+var prof_n: int = 0 # slots in use
+var prof_frame: int = 0
+var prof_active: int = -1 # slot whose query is currently open
+var prof_scratch: words = null
-function prof_init(render3d_st: mut Render3dState) -> void {
- render3d_st.prof_on = r3d_env_has(render3d_st, "R3D_PROF")
- if not render3d_st.prof_on { return }
- render3d_st.prof_names = new []pointer
- render3d_st.prof_ns = new []long
- render3d_st.prof_hits = new []long
- render3d_st.prof_ids = words(PROF_SLOTS * PROF_RING)
- render3d_st.prof_scratch = words(4)
- gpu_query_new(render3d_st, PROF_SLOTS * PROF_RING, render3d_st.prof_ids)
- render3d_st.prof_n = 0
- render3d_st.prof_frame = 0
- render3d_st.prof_active = -1
+function prof_init() -> void {
+ prof_on = r3d_env_has("R3D_PROF")
+ if not prof_on { return }
+ prof_names = new []pointer
+ prof_ns = new []long
+ prof_hits = new []long
+ prof_ids = words(PROF_SLOTS * PROF_RING)
+ prof_scratch = words(4)
+ gpu_query_new(PROF_SLOTS * PROF_RING, prof_ids)
+ prof_n = 0
+ prof_frame = 0
+ prof_active = -1
}
# the slot index for `name`, registering it on first sight (order = frame order)
-function prof_slot(render3d_st: mut Render3dState, name: pointer) -> int {
+function prof_slot(name: pointer) -> int {
var i = 0
- while i < render3d_st.prof_n {
- if render3d_st.prof_names[i] == name { return i }
+ while i < prof_n {
+ if prof_names[i] == name { return i }
i += 1
}
- if render3d_st.prof_n >= PROF_SLOTS { return -1 }
- push(render3d_st.prof_names, name)
- push(render3d_st.prof_ns, 0)
- push(render3d_st.prof_hits, 0)
- render3d_st.prof_n += 1
- return render3d_st.prof_n - 1
+ if prof_n >= PROF_SLOTS { return -1 }
+ push(prof_names, name)
+ push(prof_ns, 0)
+ push(prof_hits, 0)
+ prof_n += 1
+ return prof_n - 1
}
-function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void {
- ds_set_pass(render3d_st, name)
- if not render3d_st.prof_on { return }
- if render3d_st.prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest
- let s = prof_slot(render3d_st, name)
+function prof_begin(name: pointer) -> void {
+ ds_set_pass(name)
+ if not prof_on { return }
+ if prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest
+ let s = prof_slot(name)
if s < 0 { return }
- render3d_st.prof_active = s
- gpu_query_begin(render3d_st, render3d_st.prof_ids[s * PROF_RING + (render3d_st.prof_frame % PROF_RING)])
+ prof_active = s
+ gpu_query_begin(prof_ids[s * PROF_RING + (prof_frame % PROF_RING)])
}
-function prof_end(render3d_st: mut Render3dState) -> void {
- ds_set_pass(render3d_st, render3d_st.ds_outside)
- if not render3d_st.prof_on { return }
- if render3d_st.prof_active < 0 { return }
- gpu_query_end(render3d_st)
- render3d_st.prof_active = -1
+function prof_end() -> void {
+ ds_set_pass(ds_outside)
+ if not prof_on { return }
+ if prof_active < 0 { return }
+ gpu_query_end()
+ prof_active = -1
}
# Collect the queries issued PROF_RING-1 frames ago — long finished, so no stall.
-function prof_collect(render3d_st: mut Render3dState) -> void {
- if not render3d_st.prof_on { return }
- render3d_st.prof_frame += 1
- if render3d_st.prof_frame < PROF_RING { return }
- let slot_frame = (render3d_st.prof_frame + 1) % PROF_RING
+function prof_collect() -> void {
+ if not prof_on { return }
+ prof_frame += 1
+ if prof_frame < PROF_RING { return }
+ let slot_frame = (prof_frame + 1) % PROF_RING
var s = 0
- while s < render3d_st.prof_n {
- let q = render3d_st.prof_ids[s * PROF_RING + slot_frame]
- if gpu_query_result(render3d_st, q, render3d_st.prof_scratch) {
+ while s < prof_n {
+ let q = prof_ids[s * PROF_RING + slot_frame]
+ if gpu_query_result(q, prof_scratch) {
# the low 32 bits are ample: a pass is far below 4 seconds
- render3d_st.prof_ns[s] = render3d_st.prof_ns[s] + render3d_st.prof_scratch[0]
- render3d_st.prof_hits[s] = render3d_st.prof_hits[s] + 1
+ prof_ns[s] = prof_ns[s] + prof_scratch[0]
+ prof_hits[s] = prof_hits[s] + 1
}
s += 1
}
}
-function prof_report(render3d_st: Render3dState) -> void {
- if not render3d_st.prof_on { return }
+function prof_report() -> void {
+ if not prof_on { return }
print("")
print("GPU time per pass (mean over the run):")
var total = 0
var s = 0
- while s < render3d_st.prof_n {
- if render3d_st.prof_hits[s] > 0 { total = total + render3d_st.prof_ns[s] / render3d_st.prof_hits[s] }
+ while s < prof_n {
+ if prof_hits[s] > 0 { total = total + prof_ns[s] / prof_hits[s] }
s += 1
}
s = 0
- while s < render3d_st.prof_n {
- if render3d_st.prof_hits[s] > 0 {
- let us = render3d_st.prof_ns[s] / render3d_st.prof_hits[s] / 1000
+ while s < prof_n {
+ if prof_hits[s] > 0 {
+ let us = prof_ns[s] / prof_hits[s] / 1000
var pct = 0
- if total > 0 { pct = (render3d_st.prof_ns[s] / render3d_st.prof_hits[s]) * 100 / total }
- print(` {Text.pad_right(render3d_st.prof_names[s], 22)} {Text.pad_left(string(us), 7)} us {string(pct)}%`)
+ if total > 0 { pct = (prof_ns[s] / prof_hits[s]) * 100 / total }
+ print(` {Text.pad_right(prof_names[s], 22)} {Text.pad_left(string(us), 7)} us {string(pct)}%`)
}
s += 1
}
@@ -109,66 +118,99 @@ function prof_report(render3d_st: Render3dState) -> void {
# in fifty does all the work. `Time.delta` here is a fixed 60 Hz timestep and there is
# no finer wall clock in the runtime, so measure the WORK instead — instances generated
# per frame is exactly what the hitch is made of, and it needs no clock at all.
+var prof_gen: []int = null
+var prof_gen_cur: int = 0
+var prof_ft: []long = null # real frame times, microseconds
+var prof_last_us: long = 0
-function prof_gen_add(render3d_st: mut Render3dState, n: int) -> void {
- if not render3d_st.prof_on { return }
- render3d_st.prof_gen_cur += n
+function prof_gen_add(n: int) -> void {
+ if not prof_on { return }
+ prof_gen_cur += n
}
# Per-chunk generation, so a hitch can be pinned on a stream and a band rather than on
# "streaming". One chunk is generated atomically, so the worst chunk is the worst frame.
+var prof_chunk_us: []long = null
+var prof_chunk_kind: []int = null
+var prof_chunk_band: []int = null
+var prof_chunk_n: []int = null
+var prof_gen_us_cur: long = 0
+var prof_gen_us: []long = null # generation microseconds per frame
# CPU work that only happens on some frames — which is what a hitch is made of.
+var prof_layer_us_cur: long = 0 # rebuilding and uploading instance buffers
+var prof_bake_us_cur: long = 0 # the height-field shadow rebake
+var prof_layer_us: []long = null
+var prof_bake_us: []long = null
+var prof_dt: []long = null # frame time, aligned with the arrays above
+var prof_up_cur: long = 0 # instance bytes uploaded this frame
+var prof_up: []long = null
# Where a frame's time went at the coarsest useful split: work this process did, and
# time spent waiting for the GPU to finish it. Headless drains the GPU inside swap, so
# the two are cleanly separable there.
-function prof_before_swap(render3d_st: mut Render3dState) -> void { if render3d_st.prof_on { render3d_st.prof_pre_swap = gl_now_us() } }
+var prof_pre_swap: long = 0
+var prof_cpu_us: []long = null
+var prof_sum_dt: long = 0 # sums over the frames past the first eight, for the mean split
+var prof_sum_cpu: long = 0
+var prof_sum_game: long = 0
+var prof_sum_n: int = 0
+function prof_before_swap() -> void { if prof_on { prof_pre_swap = gl_now_us() } }
# The single most expensive CPU phase of each frame, and what it was. A hitch is one
# phase running long on one frame, so recording the worst one per frame is enough to
# name it without keeping a timeline.
-function prof_mark_start(render3d_st: mut Render3dState) -> void { if render3d_st.prof_on { render3d_st.prof_mark_last = gl_now_us(); render3d_st.prof_mark_best = 0; render3d_st.prof_mark_name = null } }
+var prof_mark_last: long = 0
+var prof_mark_best: long = 0
+var prof_mark_name: pointer = null
+var prof_mark_us: []long = null
+var prof_mark_who: []pointer = null
+function prof_mark_start() -> void { if prof_on { prof_mark_last = gl_now_us(); prof_mark_best = 0; prof_mark_name = null } }
# ... and every phase's share of the average frame, past the first eight frames (loading, first fills)
-function prof_cpu_mark(render3d_st: mut Render3dState, name: pointer) -> void {
- if not render3d_st.prof_on { return }
+var prof_mk_names: []pointer = null
+var prof_mk_us: []long = null
+var prof_mk_n: int = 0
+function prof_cpu_mark(name: pointer) -> void {
+ if not prof_on { return }
let now = gl_now_us()
- let d = now - render3d_st.prof_mark_last
- render3d_st.prof_mark_last = now
- if d > render3d_st.prof_mark_best { render3d_st.prof_mark_best = d; render3d_st.prof_mark_name = name }
+ let d = now - prof_mark_last
+ prof_mark_last = now
+ if d > prof_mark_best { prof_mark_best = d; prof_mark_name = name }
var k = -1
var i = 0
- while i < render3d_st.prof_mk_n { if render3d_st.prof_mk_names[i] == name { k = i }; i += 1 }
+ while i < prof_mk_n { if prof_mk_names[i] == name { k = i }; i += 1 }
if k < 0 {
- if render3d_st.prof_mk_names == null { render3d_st.prof_mk_names = new []pointer; render3d_st.prof_mk_us = new []long }
- push(render3d_st.prof_mk_names, name); push(render3d_st.prof_mk_us, 0)
- render3d_st.prof_mk_n += 1
- k = render3d_st.prof_mk_n - 1
+ if prof_mk_names == null { prof_mk_names = new []pointer; prof_mk_us = new []long }
+ push(prof_mk_names, name); push(prof_mk_us, 0)
+ prof_mk_n += 1
+ k = prof_mk_n - 1
}
- if render3d_st.prof_gen != null and len(render3d_st.prof_gen) > 8 { render3d_st.prof_mk_us[k] = render3d_st.prof_mk_us[k] + d }
+ if prof_gen != null and len(prof_gen) > 8 { prof_mk_us[k] = prof_mk_us[k] + d }
}
# the game's own per-frame work, kept apart from the renderer's
-function prof_game_add(render3d_st: mut Render3dState, us: long) -> void { if render3d_st.prof_on { render3d_st.prof_game_us_cur = render3d_st.prof_game_us_cur + us } }
+var prof_game_us_cur: long = 0
+var prof_game_us: []long = null
+function prof_game_add(us: long) -> void { if prof_on { prof_game_us_cur = prof_game_us_cur + us } }
-function prof_layer_add(render3d_st: mut Render3dState, us: long, bytes: long) -> void {
- if not render3d_st.prof_on { return }
- render3d_st.prof_layer_us_cur = render3d_st.prof_layer_us_cur + us
- render3d_st.prof_up_cur = render3d_st.prof_up_cur + bytes
+function prof_layer_add(us: long, bytes: long) -> void {
+ if not prof_on { return }
+ prof_layer_us_cur = prof_layer_us_cur + us
+ prof_up_cur = prof_up_cur + bytes
}
-function prof_bake_add(render3d_st: mut Render3dState, us: long) -> void { if render3d_st.prof_on { render3d_st.prof_bake_us_cur = render3d_st.prof_bake_us_cur + us } }
+function prof_bake_add(us: long) -> void { if prof_on { prof_bake_us_cur = prof_bake_us_cur + us } }
-function prof_chunk(render3d_st: mut Render3dState, kind: int, band: int, count: int, us: long) -> void {
- if not render3d_st.prof_on { return }
- if render3d_st.prof_chunk_us == null {
- render3d_st.prof_chunk_us = new []long; render3d_st.prof_chunk_kind = new []int
- render3d_st.prof_chunk_band = new []int; render3d_st.prof_chunk_n = new []int
+function prof_chunk(kind: int, band: int, count: int, us: long) -> void {
+ if not prof_on { return }
+ if prof_chunk_us == null {
+ prof_chunk_us = new []long; prof_chunk_kind = new []int
+ prof_chunk_band = new []int; prof_chunk_n = new []int
}
- push(render3d_st.prof_chunk_us, us); push(render3d_st.prof_chunk_kind, kind)
- push(render3d_st.prof_chunk_band, band); push(render3d_st.prof_chunk_n, count)
- render3d_st.prof_gen_us_cur += us
+ push(prof_chunk_us, us); push(prof_chunk_kind, kind)
+ push(prof_chunk_band, band); push(prof_chunk_n, count)
+ prof_gen_us_cur += us
}
-function prof_chunk_report(render3d_st: Render3dState) -> void {
- if not render3d_st.prof_on or render3d_st.prof_chunk_us == null { return }
+function prof_chunk_report() -> void {
+ if not prof_on or prof_chunk_us == null { return }
print("")
print("chunk generation (one chunk is atomic, so the worst chunk is the worst frame):")
# totals per (kind, band)
@@ -178,18 +220,18 @@ function prof_chunk_report(render3d_st: Render3dState) -> void {
var cnt = new []int
var mx = new []long
var i = 0
- while i < len(render3d_st.prof_chunk_us) {
+ while i < len(prof_chunk_us) {
var f = -1
var j = 0
- while j < len(kinds) { if kinds[j] == render3d_st.prof_chunk_kind[i] and bands[j] == render3d_st.prof_chunk_band[i] { f = j }; j += 1 }
+ while j < len(kinds) { if kinds[j] == prof_chunk_kind[i] and bands[j] == prof_chunk_band[i] { f = j }; j += 1 }
if f < 0 {
- push(kinds, render3d_st.prof_chunk_kind[i]); push(bands, render3d_st.prof_chunk_band[i])
+ push(kinds, prof_chunk_kind[i]); push(bands, prof_chunk_band[i])
push(tot, 0); push(cnt, 0); push(mx, 0)
f = len(kinds) - 1
}
- tot[f] = tot[f] + render3d_st.prof_chunk_us[i]
+ tot[f] = tot[f] + prof_chunk_us[i]
cnt[f] = cnt[f] + 1
- if render3d_st.prof_chunk_us[i] > mx[f] { mx[f] = render3d_st.prof_chunk_us[i] }
+ if prof_chunk_us[i] > mx[f] { mx[f] = prof_chunk_us[i] }
i += 1
}
var k = 0
@@ -198,10 +240,10 @@ function prof_chunk_report(render3d_st: Render3dState) -> void {
k += 1
}
# the per-frame distribution of generation time: this is the hitch itself
- if render3d_st.prof_gen_us == null or len(render3d_st.prof_gen_us) < 16 { return }
+ if prof_gen_us == null or len(prof_gen_us) < 16 { return }
let sorted = new []long
var a = 8
- while a < len(render3d_st.prof_gen_us) { push(sorted, render3d_st.prof_gen_us[a]); a += 1 }
+ while a < len(prof_gen_us) { push(sorted, prof_gen_us[a]); a += 1 }
var x = 1
while x < len(sorted) {
let v = sorted[x]
@@ -218,48 +260,48 @@ function prof_chunk_report(render3d_st: Render3dState) -> void {
print(` generation per frame (us): p95 {string(sorted[(n * 95) / 100])} p99 {string(sorted[(n * 99) / 100])} worst {string(sorted[n - 1])} frames that generated: {string(busy)} of {string(n)} total {string(t / 1000)} ms`)
}
-function prof_gen_frame(render3d_st: mut Render3dState) -> void {
- ds_frame(render3d_st)
- if not render3d_st.prof_on { return }
- if render3d_st.prof_gen == null {
- render3d_st.prof_gen = new []int; render3d_st.prof_ft = new []long
- render3d_st.prof_gen_us = new []long; render3d_st.prof_layer_us = new []long
- render3d_st.prof_bake_us = new []long; render3d_st.prof_dt = new []long; render3d_st.prof_up = new []long
- render3d_st.prof_cpu_us = new []long; render3d_st.prof_game_us = new []long
- render3d_st.prof_mark_us = new []long; render3d_st.prof_mark_who = new []pointer
+function prof_gen_frame() -> void {
+ ds_frame()
+ if not prof_on { return }
+ if prof_gen == null {
+ prof_gen = new []int; prof_ft = new []long
+ prof_gen_us = new []long; prof_layer_us = new []long
+ prof_bake_us = new []long; prof_dt = new []long; prof_up = new []long
+ prof_cpu_us = new []long; prof_game_us = new []long
+ prof_mark_us = new []long; prof_mark_who = new []pointer
}
- push(render3d_st.prof_gen, render3d_st.prof_gen_cur)
- render3d_st.prof_gen_cur = 0
+ push(prof_gen, prof_gen_cur)
+ prof_gen_cur = 0
let now = gl_now_us()
var dtf: long = 0
- if render3d_st.prof_last_us != 0 { push(render3d_st.prof_ft, now - render3d_st.prof_last_us); dtf = now - render3d_st.prof_last_us }
- render3d_st.prof_last_us = now
+ if prof_last_us != 0 { push(prof_ft, now - prof_last_us); dtf = now - prof_last_us }
+ prof_last_us = now
# everything the frame just ended spent on work it only does sometimes
- push(render3d_st.prof_dt, dtf)
- push(render3d_st.prof_gen_us, render3d_st.prof_gen_us_cur)
- push(render3d_st.prof_layer_us, render3d_st.prof_layer_us_cur)
- push(render3d_st.prof_bake_us, render3d_st.prof_bake_us_cur)
- push(render3d_st.prof_up, render3d_st.prof_up_cur)
+ push(prof_dt, dtf)
+ push(prof_gen_us, prof_gen_us_cur)
+ push(prof_layer_us, prof_layer_us_cur)
+ push(prof_bake_us, prof_bake_us_cur)
+ push(prof_up, prof_up_cur)
var cpu: long = 0
- if render3d_st.prof_pre_swap != 0 and dtf != 0 { cpu = render3d_st.prof_pre_swap - (now - dtf) }
- push(render3d_st.prof_cpu_us, cpu)
- push(render3d_st.prof_game_us, render3d_st.prof_game_us_cur)
- if len(render3d_st.prof_gen) > 9 and dtf != 0 {
- render3d_st.prof_sum_dt = render3d_st.prof_sum_dt + dtf; render3d_st.prof_sum_cpu = render3d_st.prof_sum_cpu + cpu; render3d_st.prof_sum_game = render3d_st.prof_sum_game + render3d_st.prof_game_us_cur
- render3d_st.prof_sum_n += 1
+ if prof_pre_swap != 0 and dtf != 0 { cpu = prof_pre_swap - (now - dtf) }
+ push(prof_cpu_us, cpu)
+ push(prof_game_us, prof_game_us_cur)
+ if len(prof_gen) > 9 and dtf != 0 {
+ prof_sum_dt = prof_sum_dt + dtf; prof_sum_cpu = prof_sum_cpu + cpu; prof_sum_game = prof_sum_game + prof_game_us_cur
+ prof_sum_n += 1
}
- push(render3d_st.prof_mark_us, render3d_st.prof_mark_best)
- if render3d_st.prof_mark_name == null { push(render3d_st.prof_mark_who, "-") } else { push(render3d_st.prof_mark_who, render3d_st.prof_mark_name) }
- render3d_st.prof_gen_us_cur = 0; render3d_st.prof_layer_us_cur = 0; render3d_st.prof_bake_us_cur = 0; render3d_st.prof_up_cur = 0; render3d_st.prof_game_us_cur = 0
+ push(prof_mark_us, prof_mark_best)
+ if prof_mark_name == null { push(prof_mark_who, "-") } else { push(prof_mark_who, prof_mark_name) }
+ prof_gen_us_cur = 0; prof_layer_us_cur = 0; prof_bake_us_cur = 0; prof_up_cur = 0; prof_game_us_cur = 0
}
# the distribution of REAL frame times: stutter lives in the tail, not the mean
-function prof_ft_report(render3d_st: Render3dState) -> void {
- if not render3d_st.prof_on { return }
- if render3d_st.prof_ft == null or len(render3d_st.prof_ft) < 16 { return }
+function prof_ft_report() -> void {
+ if not prof_on { return }
+ if prof_ft == null or len(prof_ft) < 16 { return }
let sorted = new []long
var i = 8
- while i < len(render3d_st.prof_ft) { push(sorted, render3d_st.prof_ft[i]); i += 1 }
+ while i < len(prof_ft) { push(sorted, prof_ft[i]); i += 1 }
var a = 1
while a < len(sorted) {
let v = sorted[a]
@@ -298,19 +340,19 @@ function prof_ft_report(render3d_st: Render3dState) -> void {
}
print(` fps at median {string(1000000 / med)} frames over 1.3x median: {string(o13)}, over 1.5x: {string(o15)}, over 2x: {string(over)} — of {string(n)}`)
print(` stutter: {string(excess / 1000)} ms of frame time beyond 1.2x median over the run`)
- print(` streaming totals over the run: walk {string(render3d_st.stream_us_walk / 1000)} ms (generate {string(render3d_st.stream_us_gen / 1000)} ms, gather {string(render3d_st.stream_us_gather / 1000)} ms), {string(render3d_st.stream_walks)} stream-walks`)
+ print(` streaming totals over the run: walk {string(stream_us_walk / 1000)} ms (generate {string(stream_us_gen / 1000)} ms, gather {string(stream_us_gather / 1000)} ms), {string(stream_walks)} stream-walks`)
# the average frame, split: what the process did before the swap (the game's share of it apart),
# the rest waiting on the GPU and the swap, and the renderer's CPU phases in frame order
- if render3d_st.prof_sum_n > 0 {
- let dt = render3d_st.prof_sum_dt / render3d_st.prof_sum_n
- let cpu = render3d_st.prof_sum_cpu / render3d_st.prof_sum_n
- let game = render3d_st.prof_sum_game / render3d_st.prof_sum_n
+ if prof_sum_n > 0 {
+ let dt = prof_sum_dt / prof_sum_n
+ let cpu = prof_sum_cpu / prof_sum_n
+ let game = prof_sum_game / prof_sum_n
print("")
- print(`MEAN frame {string(dt)} us over {string(render3d_st.prof_sum_n)} frames: CPU before the swap {string(cpu)} us (the game's own {string(game)} us), GPU and swap {string(dt - cpu)} us`)
+ print(`MEAN frame {string(dt)} us over {string(prof_sum_n)} frames: CPU before the swap {string(cpu)} us (the game's own {string(game)} us), GPU and swap {string(dt - cpu)} us`)
print(" renderer CPU by phase, mean per frame:")
var i = 0
- while i < render3d_st.prof_mk_n {
- print(` {Text.pad_right(render3d_st.prof_mk_names[i], 20)} {Text.pad_left(string(render3d_st.prof_mk_us[i] / render3d_st.prof_sum_n), 7)} us`)
+ while i < prof_mk_n {
+ print(` {Text.pad_right(prof_mk_names[i], 20)} {Text.pad_left(string(prof_mk_us[i] / prof_sum_n), 7)} us`)
i += 1
}
}
@@ -318,13 +360,13 @@ function prof_ft_report(render3d_st: Render3dState) -> void {
# The slowest frames of the run, with the once-in-a-while CPU work that landed in them.
# An average never shows a hitch; this is the list of the frames you actually felt.
-function prof_hitch_report(render3d_st: Render3dState) -> void {
- if not render3d_st.prof_on or render3d_st.prof_dt == null or len(render3d_st.prof_dt) < 32 { return }
- let n = len(render3d_st.prof_dt)
+function prof_hitch_report() -> void {
+ if not prof_on or prof_dt == null or len(prof_dt) < 32 { return }
+ let n = len(prof_dt)
# median, for a sense of what "slow" means here
let sorted = new []long
var i = 8
- while i < n { push(sorted, render3d_st.prof_dt[i]); i += 1 }
+ while i < n { push(sorted, prof_dt[i]); i += 1 }
var a = 1
while a < len(sorted) {
let v = sorted[a]
@@ -345,22 +387,22 @@ function prof_hitch_report(render3d_st: Render3dState) -> void {
var bestv: long = -1
var k = 8
while k < n {
- if render3d_st.prof_dt[k] <= cut and render3d_st.prof_dt[k] > bestv { bestv = render3d_st.prof_dt[k]; best = k }
+ if prof_dt[k] <= cut and prof_dt[k] > bestv { bestv = prof_dt[k]; best = k }
k += 1
}
if best < 0 { return }
- print(` {Text.pad_left(string(best), 6)} {Text.pad_left(string(render3d_st.prof_dt[best]), 6)}us {Text.pad_left(string(render3d_st.prof_cpu_us[best]), 7)}us {Text.pad_left(string(render3d_st.prof_dt[best] - render3d_st.prof_cpu_us[best]), 8)}us {Text.pad_left(string(render3d_st.prof_gen_us[best]), 7)}us {Text.pad_left(string(render3d_st.prof_game_us[best]), 7)}us {Text.pad_left(string(render3d_st.prof_up[best] / 1024), 7)}KB {Text.pad_right(render3d_st.prof_mark_who[best], 18)} {Text.pad_left(string(render3d_st.prof_mark_us[best]), 7)}us`)
+ print(` {Text.pad_left(string(best), 6)} {Text.pad_left(string(prof_dt[best]), 6)}us {Text.pad_left(string(prof_cpu_us[best]), 7)}us {Text.pad_left(string(prof_dt[best] - prof_cpu_us[best]), 8)}us {Text.pad_left(string(prof_gen_us[best]), 7)}us {Text.pad_left(string(prof_game_us[best]), 7)}us {Text.pad_left(string(prof_up[best] / 1024), 7)}KB {Text.pad_right(prof_mark_who[best], 18)} {Text.pad_left(string(prof_mark_us[best]), 7)}us`)
cut = bestv - 1
shown += 1
}
}
-function prof_gen_report(render3d_st: Render3dState) -> void {
- if not render3d_st.prof_on { return }
- if render3d_st.prof_gen == null or len(render3d_st.prof_gen) < 8 { return }
+function prof_gen_report() -> void {
+ if not prof_on { return }
+ if prof_gen == null or len(prof_gen) < 8 { return }
let sorted = new []int
var i = 8 # skip the first frames: one-off initial fill
- while i < len(render3d_st.prof_gen) { push(sorted, render3d_st.prof_gen[i]); i += 1 }
+ while i < len(prof_gen) { push(sorted, prof_gen[i]); i += 1 }
var a = 1
while a < len(sorted) {
let v = sorted[a]
diff --git a/packages/ludic.render3d/programs.ludic b/packages/ludic.render3d/programs.ludic
index c3140758..665c7104 100644
--- a/packages/ludic.render3d/programs.ludic
+++ b/packages/ludic.render3d/programs.ludic
@@ -10,9 +10,16 @@
# copy them in. The scanned CC0 materials are the other half: too large to ship with a
# toolchain and redistributable from their origin, so they are fetched into the project
# (`ludic assets`) and read from there.
+var r3d_root: string = "packages/ludic.render3d" # where shaders/ lives
+var r3d_assets: string = "assets/polyhaven" # where the CC0 assets live
+var r3d_root_found: bool = false
# defines prepended to EVERY program (set before any is built): renderer-wide switches
+var r3d_global_defs: string = ""
+var r3d_noise_src: string = null
+var r3d_lighting_src: string = null
+var r3d_wind_src: string = null
-function r3d_set_paths(render3d_st: mut Render3dState, root: string, assets: string) -> void { render3d_st.r3d_root = root; render3d_st.r3d_assets = assets }
+function r3d_set_paths(root: string, assets: string) -> void { r3d_root = root; r3d_assets = assets }
# The install root, as the compiler computes it: $LUDIC_HOME, else the directory of the
# `ludic` on PATH. A game running from its own tree finds this package there.
@@ -24,60 +31,61 @@ function r3d_home() -> string {
# Settle r3d_root on first use: the package as checked out beside the project, else the
# copy that ships with the toolchain.
-function r3d_find_root(render3d_st: mut Render3dState) -> void {
- if render3d_st.r3d_root_found { return }
- render3d_st.r3d_root_found = true
- if Fs.exists(`{render3d_st.r3d_root}/shaders/lighting.glsl`) { return }
+function r3d_find_root() -> void {
+ if r3d_root_found { return }
+ r3d_root_found = true
+ if Fs.exists(`{r3d_root}/shaders/lighting.glsl`) { return }
let home = r3d_home()
let alt = `{home}/packages/ludic.render3d`
- if Fs.exists(`{alt}/shaders/lighting.glsl`) { render3d_st.r3d_root = alt; return }
- print(`r3d: cannot find the renderer's shaders (looked in {render3d_st.r3d_root}/shaders and {alt}/shaders)`)
+ if Fs.exists(`{alt}/shaders/lighting.glsl`) { r3d_root = alt; return }
+ print(`r3d: cannot find the renderer's shaders (looked in {r3d_root}/shaders and {alt}/shaders)`)
}
-function r3d_shader_file(render3d_st: mut Render3dState, name: string) -> string {
- r3d_find_root(render3d_st)
- let path = `{render3d_st.r3d_root}/shaders/{name}`
+function r3d_shader_file(name: string) -> string {
+ r3d_find_root()
+ let path = `{r3d_root}/shaders/{name}`
let s = Fs.read_text(path)
if s == null { print(`r3d: missing shader {path}`); return "" }
return s
}
-function r3d_shader_src(render3d_st: mut Render3dState, name: string, defines: string, is_frag: bool) -> string {
- if render3d_st.r3d_noise_src == null { render3d_st.r3d_noise_src = r3d_shader_file(render3d_st, "noise.glsl") }
- if render3d_st.r3d_lighting_src == null { render3d_st.r3d_lighting_src = r3d_shader_file(render3d_st, "lighting.glsl") }
+function r3d_shader_src(name: string, defines: string, is_frag: bool) -> string {
+ if r3d_noise_src == null { r3d_noise_src = r3d_shader_file("noise.glsl") }
+ if r3d_lighting_src == null { r3d_lighting_src = r3d_shader_file("lighting.glsl") }
# the wind goes to BOTH stages: the grass and the crowns move in the vertex shader, the water
# ripples in the fragment one, and they have to be reading the same field or the meadow and
# the lake disagree about the weather
- if render3d_st.r3d_wind_src == null { render3d_st.r3d_wind_src = r3d_shader_file(render3d_st, "wind.glsl") }
- var s = "#version 410 core\n" + render3d_st.r3d_global_defs + defines + render3d_st.r3d_wind_src
- if is_frag { s = s + render3d_st.r3d_noise_src + render3d_st.r3d_lighting_src }
- return s + r3d_shader_file(render3d_st, name)
+ if r3d_wind_src == null { r3d_wind_src = r3d_shader_file("wind.glsl") }
+ var s = "#version 410 core\n" + r3d_global_defs + defines + r3d_wind_src
+ if is_frag { s = s + r3d_noise_src + r3d_lighting_src }
+ return s + r3d_shader_file(name)
}
# Build a program from a vertex + fragment file pair (defines apply to both).
# R3D_PROGRAMS_LOG=: append every program built (vertex, fragment, defines) - the list a
# backend that cannot compile shaders at run time (Vulkan: SPIR-V is built ahead) must cover
-function r3d_program_log(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> void {
- if render3d_st.r3d_prog_log < 0 { render3d_st.r3d_prog_log = 0; if r3d_env_has(render3d_st, "R3D_PROGRAMS_LOG") { render3d_st.r3d_prog_log = 1 } }
- if render3d_st.r3d_prog_log == 0 { return }
- let f = file_open(r3d_env(render3d_st, "R3D_PROGRAMS_LOG"), "ab")
+var r3d_prog_log: int = -1
+function r3d_program_log(vs: string, fs: string, defines: string) -> void {
+ if r3d_prog_log < 0 { r3d_prog_log = 0; if r3d_env_has("R3D_PROGRAMS_LOG") { r3d_prog_log = 1 } }
+ if r3d_prog_log == 0 { return }
+ let f = file_open(r3d_env("R3D_PROGRAMS_LOG"), "ab")
if f == null { return }
# one line per program: the defines' newlines become ';' so a variant is one line
- let defs = Text.replace(`{render3d_st.r3d_global_defs}{defines}`, "\n", ";")
+ let defs = Text.replace(`{r3d_global_defs}{defines}`, "\n", ";")
let line = `{vs}|{fs}|{defs}\n`
file_write(f, line, len(line))
file_close(f)
}
-function r3d_program(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int {
- r3d_program_log(render3d_st, vs, fs, defines)
- let p = gpu_program(render3d_st, r3d_shader_src(render3d_st, vs, defines, false), r3d_shader_src(render3d_st, fs, defines, true), vs, fs, `{render3d_st.r3d_global_defs}{defines}`)
+function r3d_program(vs: string, fs: string, defines: string) -> int {
+ r3d_program_log(vs, fs, defines)
+ let p = gpu_program(r3d_shader_src(vs, defines, false), r3d_shader_src(fs, defines, true), vs, fs, `{r3d_global_defs}{defines}`)
if p == 0 { print(`r3d: program failed: {vs} + {fs}`) }
return p
}
# Bind a texture to a unit and point a sampler uniform at it.
-function r3d_bind_tex(render3d_st: mut Render3dState, prog: int, name: string, unit: int, kind: int, tex: int) -> void { gpu_bind_sampler(render3d_st, prog, name, unit, kind, tex) }
-function r3d_bind_2d(render3d_st: mut Render3dState, prog: int, name: string, unit: int, tex: int) -> void { gpu_bind_sampler(render3d_st, prog, name, unit, GPU_TEX2D, tex) }
+function r3d_bind_tex(prog: int, name: string, unit: int, kind: int, tex: int) -> void { gpu_bind_sampler(prog, name, unit, kind, tex) }
+function r3d_bind_2d(prog: int, name: string, unit: int, tex: int) -> void { gpu_bind_sampler(prog, name, unit, GPU_TEX2D, tex) }
# A framebuffer with one colour texture (and optionally a depth texture).
property Target {
@@ -87,29 +95,29 @@ property Target {
w: int = 0,
h: int = 0
}
-function target_new(render3d_st: mut Render3dState, w: int, h: int, ifmt: int, fmt: int, ty: int, with_depth: bool, filter: int) -> Target {
+function target_new(w: int, h: int, ifmt: int, fmt: int, ty: int, with_depth: bool, filter: int) -> Target {
let t = new Target
t.w = w; t.h = h
- t.fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, t.fbo)
- t.color = tex_target(render3d_st, w, h, ifmt, fmt, ty, filter)
- gpu_fb_color(render3d_st, 0, t.color)
+ t.fbo = gpu_fb_new()
+ gpu_fb_bind(t.fbo)
+ t.color = tex_target(w, h, ifmt, fmt, ty, filter)
+ gpu_fb_color(0, t.color)
if with_depth {
- t.depth = tex_target(render3d_st, w, h, GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, GL_NEAREST)
- gpu_fb_depth(render3d_st, t.depth)
+ t.depth = tex_target(w, h, GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, GL_NEAREST)
+ gpu_fb_depth(t.depth)
}
- let st = gpu_fb_status(render3d_st)
+ let st = gpu_fb_status()
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: framebuffer incomplete {st} ({w}x{h})`) }
- gpu_fb_bind(render3d_st, 0)
+ gpu_fb_bind(0)
return t
}
-function target_free(render3d_st: mut Render3dState, t: Target) -> void {
+function target_free(t: Target) -> void {
if t == null { return }
- gpu_fb_free(render3d_st, t.fbo)
- gpu_tex_free(render3d_st, t.color)
- if t.depth != 0 { gpu_tex_free(render3d_st, t.depth) }
+ gpu_fb_free(t.fbo)
+ gpu_tex_free(t.color)
+ if t.depth != 0 { gpu_tex_free(t.depth) }
}
-function target_bind(render3d_st: mut Render3dState, t: Target) -> void {
- gpu_fb_bind(render3d_st, t.fbo)
- gpu_viewport(render3d_st, 0, 0, t.w, t.h)
+function target_bind(t: Target) -> void {
+ gpu_fb_bind(t.fbo)
+ gpu_viewport(0, 0, t.w, t.h)
}
diff --git a/packages/ludic.render3d/quat.ludic b/packages/ludic.render3d/quat.ludic
index 6c8cc534..b95a5018 100644
--- a/packages/ludic.render3d/quat.ludic
+++ b/packages/ludic.render3d/quat.ludic
@@ -57,14 +57,18 @@ function q_rotate(o: floats, q: floats, v: floats) -> void {
o[0] = x; o[1] = y; o[2] = z
}
# pitch about X, yaw about Y, roll about Z, composed as yaw * pitch * roll
-function q_euler(render3d_st: mut Render3dState, o: floats, pitch: float, yaw: float, roll: float) -> void {
- if render3d_st.q_scratch == null {
- render3d_st.q_scratch = floats(16)
- render3d_st.q_sy = view(render3d_st.q_scratch, 4, 4)
- render3d_st.q_sz = view(render3d_st.q_scratch, 8, 4)
- render3d_st.q_st = view(render3d_st.q_scratch, 12, 4)
+var q_scratch: floats = null
+var q_sy: floats = null # views of q_scratch's second, third and fourth
+var q_sz: floats = null
+var q_st: floats = null
+function q_euler(o: floats, pitch: float, yaw: float, roll: float) -> void {
+ if q_scratch == null {
+ q_scratch = floats(16)
+ q_sy = view(q_scratch, 4, 4)
+ q_sz = view(q_scratch, 8, 4)
+ q_st = view(q_scratch, 12, 4)
}
- let qx = render3d_st.q_scratch; let qy = render3d_st.q_sy; let qz = render3d_st.q_sz; let t = render3d_st.q_st
+ let qx = q_scratch; let qy = q_sy; let qz = q_sz; let t = q_st
q_axis_angle(qx, 1.0, 0.0, 0.0, pitch)
q_axis_angle(qy, 0.0, 1.0, 0.0, yaw)
q_axis_angle(qz, 0.0, 0.0, 1.0, roll)
diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic
index ae280a93..b23fb345 100644
--- a/packages/ludic.render3d/render.ludic
+++ b/packages/ludic.render3d/render.ludic
@@ -5,86 +5,120 @@
# scene_draw_casters, which the demo defines.
# ============================================================================
+var r3d_sky_prog: int = 0
+var r3d_fog_density: float = 0.0
+var r3d_fog_scale: float = 1.0 # float bits: a setting's multiplier over the density the day sets
+var r3d_fog_falloff: float = 0.0
+var r3d_fog_base: float = 0.0 # the height fog is measured from (float bits); 0 = y = 0
# Both of these are the DAY's, set by daylight_set from the sun's own elevation. The inscatter
# is how hard the air scatters the sun forward - nearly nothing at noon, and the glow a ridge
# is silhouetted against at dusk. The desaturation is how fast distance takes a surface's own
# colour away, which is the term that was missing entirely and is most of why a midday frame
# had no depth in it at all.
+var r3d_fog_inscatter: float = 0.02 # 0.02, what the shader used to hard-code
+var r3d_fog_desat: float = 1.35 # 1.35
# How much of the visible sky's colour comes from the analytic model rather than from the
# photograph (sky.frag). The day fades it in: at night the sky has its own tint, a star field
# and a moon, and relighting on top of those would only wash them out.
+var r3d_sky_relight: float = 0.0
# What the ground reflects back up, low and high, and the height they cross at. The defaults
# are Maroon Lake's - a green basin under a grey-rock treeline at about 480 m over the datum.
+var r3d_ground_alb_r: float = 0.3; var r3d_ground_alb_g: float = 0.34; var r3d_ground_alb_b: float = 0.14
+var r3d_ground_hi_r: float = 0.289; var r3d_ground_hi_g: float = 0.28; var r3d_ground_hi_b: float = 0.26
+var r3d_ground_hi_y: float = 480.0 # 480
+var r3d_ground_hi_w: float = 150.0 # 150
+var r3d_time: float = 0.0
+var r3d_ready: bool = false
# set before r3d_init to build the landscape from a real height map
+var r3d_dem_path: string = null
+var r3d_dem_min: float = 0.0
+var r3d_dem_max: float = 0.0
+var r3d_dem_base: float = 0.0
+var r3d_dem_ox: float = 0.0
+var r3d_dem_oz: float = 0.0
+var r3d_ortho_path: string = null
# A plate: no terrain, no grass, no water - the sky, the sun, the shadows and whatever the scene
# draws (a lab's stage, ludic.lab). Set before the load; nothing of the landscape is made, so
# its hundreds of megabytes of height field, materials and grass never exist.
-function r3d_plate_mode(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.r3d_plate = on }
+var r3d_plate: bool = false
+function r3d_plate_mode(on: bool) -> void { r3d_plate = on }
# the sky's HDR image, when it is not the default photograph under r3d_assets
+var r3d_sky_path: string = null
+var r3d_debug: bool = false
+var r3d_debug_shadow: bool = false
+var r3d_debug_max: bool = false
+var r3d_cloud_shadow: float = 0.5 # 0.5
+var r3d_clip_y: float = -2147483600.0 # -2^31: no clipping
# R3D_NOPREPASS=1: light the foliage the old way, every card behind the front one included
-function r3d_prepass_off(render3d_st: mut Render3dState) -> bool {
- if render3d_st.r3d_prepass_env < 0 { render3d_st.r3d_prepass_env = 0; if r3d_env_has(render3d_st, "R3D_NOPREPASS") { render3d_st.r3d_prepass_env = 1 } }
- return render3d_st.r3d_prepass_env == 1
+var r3d_prepass_env: int = -1
+function r3d_prepass_off() -> bool {
+ if r3d_prepass_env < 0 { r3d_prepass_env = 0; if r3d_env_has("R3D_NOPREPASS") { r3d_prepass_env = 1 } }
+ return r3d_prepass_env == 1
}
-function fog_bind(render3d_st: mut Render3dState, prog: int) -> void {
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_clip_y"), render3d_st.r3d_clip_y)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_spec_scale"), 1.0)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_density"), render3d_st.r3d_fog_density)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_height_falloff"), render3d_st.r3d_fog_falloff)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_base"), render3d_st.r3d_fog_base)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_inscatter"), render3d_st.r3d_fog_inscatter)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_desat"), render3d_st.r3d_fog_desat)
- u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_alb"), render3d_st.r3d_ground_alb_r, render3d_st.r3d_ground_alb_g, render3d_st.r3d_ground_alb_b)
- u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_alb_hi"), render3d_st.r3d_ground_hi_r, render3d_st.r3d_ground_hi_g, render3d_st.r3d_ground_hi_b)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_hi_y"), render3d_st.r3d_ground_hi_y)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_hi_w"), render3d_st.r3d_ground_hi_w)
- var cs = render3d_st.r3d_cloud_shadow
- if r3d_env_has(render3d_st, "R3D_NOCLOUD") { cs = 0.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_cloud_shadow"), cs)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_time"), render3d_st.r3d_time)
+function fog_bind(prog: int) -> void {
+ u_f(gpu_uniform(prog, "u_clip_y"), r3d_clip_y)
+ u_f(gpu_uniform(prog, "u_spec_scale"), 1.0)
+ u_f(gpu_uniform(prog, "u_fog_density"), r3d_fog_density)
+ u_f(gpu_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff)
+ u_f(gpu_uniform(prog, "u_fog_base"), r3d_fog_base)
+ u_f(gpu_uniform(prog, "u_fog_inscatter"), r3d_fog_inscatter)
+ u_f(gpu_uniform(prog, "u_fog_desat"), r3d_fog_desat)
+ u_f3(gpu_uniform(prog, "u_ground_alb"), r3d_ground_alb_r, r3d_ground_alb_g, r3d_ground_alb_b)
+ u_f3(gpu_uniform(prog, "u_ground_alb_hi"), r3d_ground_hi_r, r3d_ground_hi_g, r3d_ground_hi_b)
+ u_f(gpu_uniform(prog, "u_ground_hi_y"), r3d_ground_hi_y)
+ u_f(gpu_uniform(prog, "u_ground_hi_w"), r3d_ground_hi_w)
+ var cs = r3d_cloud_shadow
+ if r3d_env_has("R3D_NOCLOUD") { cs = 0.0 }
+ u_f(gpu_uniform(prog, "u_cloud_shadow"), cs)
+ u_f(gpu_uniform(prog, "u_time"), r3d_time)
}
# profiling switches (environment): R3D_NOSHADOW R3D_NOGI R3D_MSAA=n R3D_NOBLADES R3D_NOCARDS R3D_NOTREES R3D_NEAR=m
-function r3d_env_flags(render3d_st: mut Render3dState) -> void {
- render3d_st.r3d_no_shadow = r3d_env_has(render3d_st, "R3D_NOSHADOW")
- render3d_st.r3d_no_trees = r3d_env_has(render3d_st, "R3D_NOTREES")
- render3d_st.r3d_no_refl = r3d_env_has(render3d_st, "R3D_NOREFL")
- if r3d_env_has(render3d_st, "R3D_DEBUG") { render3d_st.r3d_debug = true }
- if r3d_env_has(render3d_st, "R3D_DBGSHADOW") { render3d_st.r3d_debug_shadow = true }
- if r3d_env_has(render3d_st, "R3D_NOGI") { render3d_st.post_gi_strength = 0.0; render3d_st.post_ao_strength = 0.0; render3d_st.post_no_gi = true }
- if r3d_env_has(render3d_st, "R3D_MSAA") { render3d_st.post_ms_samples = Text.to_int(r3d_env(render3d_st, "R3D_MSAA")) }
- render3d_st.sc_skip_blade = r3d_env_has(render3d_st, "R3D_NOBLADES")
- render3d_st.sc_skip_card = r3d_env_has(render3d_st, "R3D_NOCARDS")
- render3d_st.sc_dbg_lod = r3d_env_has(render3d_st, "R3D_LODDBG")
- if r3d_env_has(render3d_st, "R3D_ANISO") {
- let a = Text.to_int(r3d_env(render3d_st, "R3D_ANISO"))
- render3d_st.tex_anisotropy = 1.0
- if a >= 2 { render3d_st.tex_anisotropy = 2.0 }
- if a >= 4 { render3d_st.tex_anisotropy = 4.0 }
- if a >= 8 { render3d_st.tex_anisotropy = 8.0 }
- if a >= 16 { render3d_st.tex_anisotropy = 16.0 }
+var r3d_test_frame: int = 0
+var r3d_test_resize: int = 0
+var r3d_no_shadow: bool = false
+var r3d_no_trees: bool = false
+var r3d_no_refl: bool = false
+function r3d_env_flags() -> void {
+ r3d_no_shadow = r3d_env_has("R3D_NOSHADOW")
+ r3d_no_trees = r3d_env_has("R3D_NOTREES")
+ r3d_no_refl = r3d_env_has("R3D_NOREFL")
+ if r3d_env_has("R3D_DEBUG") { r3d_debug = true }
+ if r3d_env_has("R3D_DBGSHADOW") { r3d_debug_shadow = true }
+ if r3d_env_has("R3D_NOGI") { post_gi_strength = 0.0; post_ao_strength = 0.0; post_no_gi = true }
+ if r3d_env_has("R3D_MSAA") { post_ms_samples = Text.to_int(r3d_env("R3D_MSAA")) }
+ sc_skip_blade = r3d_env_has("R3D_NOBLADES")
+ sc_skip_card = r3d_env_has("R3D_NOCARDS")
+ sc_dbg_lod = r3d_env_has("R3D_LODDBG")
+ if r3d_env_has("R3D_ANISO") {
+ let a = Text.to_int(r3d_env("R3D_ANISO"))
+ tex_anisotropy = 1.0
+ if a >= 2 { tex_anisotropy = 2.0 }
+ if a >= 4 { tex_anisotropy = 4.0 }
+ if a >= 8 { tex_anisotropy = 8.0 }
+ if a >= 16 { tex_anisotropy = 16.0 }
}
}
# Start the renderer in one call: the window, then every load step in order. A game that shows
# a loading screen calls r3d_open, draws its screen, and runs r3d_load_step itself between frames.
-function r3d_init(render3d_st: mut Render3dState, w: int, h: int, title: string) -> bool {
- if not r3d_open(render3d_st, w, h, title) { return false }
- for i in 0 .. r3d_load_count() { if not r3d_load_step(render3d_st, i) { return false } }
+function r3d_init(w: int, h: int, title: string) -> bool {
+ if not r3d_open(w, h, title) { return false }
+ for i in 0 .. r3d_load_count() { if not r3d_load_step(i) { return false } }
return true
}
# the window and the graphics backend; nothing is baked yet, but a frame can be presented
-function r3d_open(render3d_st: mut Render3dState, w: int, h: int, title: string) -> bool {
- r3d_env_flags(render3d_st)
- gpu_select(render3d_st)
- if not gpu_open(render3d_st, w, h, title) { print("r3d: no OpenGL context"); return false }
- if r3d_env_has(render3d_st, "R3D_NOVSYNC") { gpu_vsync(render3d_st, 0) }
- var renderer: string = gpu_renderer_name(render3d_st)
+function r3d_open(w: int, h: int, title: string) -> bool {
+ r3d_env_flags()
+ gpu_select()
+ if not gpu_open(w, h, title) { print("r3d: no OpenGL context"); return false }
+ if r3d_env_has("R3D_NOVSYNC") { gpu_vsync(0) }
+ var renderer: string = gpu_renderer_name()
print(`r3d: {gl_w}x{gl_h} on {renderer}`)
- prof_init(render3d_st)
- cam_init(render3d_st, float(gl_w) / float(gl_h))
+ prof_init()
+ cam_init(float(gl_w) / float(gl_h))
return true
}
@@ -93,191 +127,192 @@ function r3d_open(render3d_st: mut Render3dState, w: int, h: int, title: string)
# programs), 5 the shadow maps and the screen targets, 6 the scattered cover and the actors, 7 the
# grass and the sky pass. The order is r3d_init's.
function r3d_load_count() -> int { return 4 + TERRAIN_INIT_STEPS }
-function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool {
+function r3d_load_step(i: int) -> bool {
let t = i - 1
if i == 0 {
- var sky = render3d_st.r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr"
- if render3d_st.r3d_sky_path != null { sky = render3d_st.r3d_sky_path }
- if not sky_load(render3d_st, sky) { return false }
- daylight_init(render3d_st)
+ var sky = r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr"
+ if r3d_sky_path != null { sky = r3d_sky_path }
+ if not sky_load(sky) { return false }
+ daylight_init()
} else if t >= 0 and t < TERRAIN_INIT_STEPS {
- if render3d_st.r3d_plate { return true }
+ if r3d_plate { return true }
if t == 0 {
- if render3d_st.r3d_dem_path != null { terrain_use_dem(render3d_st, render3d_st.r3d_dem_path, render3d_st.r3d_dem_min, render3d_st.r3d_dem_max, render3d_st.r3d_dem_base, render3d_st.r3d_dem_ox, render3d_st.r3d_dem_oz) }
- if render3d_st.r3d_ortho_path != null { terrain_use_ortho(render3d_st, render3d_st.r3d_ortho_path) }
+ if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) }
+ if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) }
}
- terrain_init_step(render3d_st, t)
+ terrain_init_step(t)
} else if i == 1 + TERRAIN_INIT_STEPS {
- shadow_init(render3d_st)
- post_init(render3d_st, gl_w, gl_h)
+ shadow_init()
+ post_init(gl_w, gl_h)
} else if i == 2 + TERRAIN_INIT_STEPS {
- scatter_init(render3d_st)
- actor_init(render3d_st)
+ scatter_init()
+ actor_init()
} else if i == 3 + TERRAIN_INIT_STEPS {
- if not render3d_st.r3d_plate { grass_init(render3d_st) }
- render3d_st.r3d_sky_prog = r3d_program(render3d_st, "fullscreen.vert", "sky.frag", "")
+ if not r3d_plate { grass_init() }
+ r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
# The base is NOON's air, and it was tuned when nothing desaturated with distance - so it
# was doing its whole job through colour and could not be raised without the valley going
# blue. With the desaturation term carrying the depth, the air can be as thick as real air
# at 2900 m and the far ridge recedes instead of tinting.
- render3d_st.r3d_fog_density = 0.00024
- render3d_st.r3d_fog_falloff = 0.002
- render3d_st.r3d_ready = true
- gpu_check(render3d_st, "r3d init")
+ r3d_fog_density = 0.00024
+ r3d_fog_falloff = 0.002
+ r3d_ready = true
+ gpu_check("r3d init")
}
return true
}
-function r3d_draw_sky(render3d_st: mut Render3dState) -> void {
- gpu_depth_func(render3d_st, GL_LEQUAL)
- gpu_depth_write(render3d_st, false)
- gpu_cull(render3d_st, false)
- let p = render3d_st.r3d_sky_prog
- gpu_use_program(render3d_st, p)
- r3d_bind_2d(render3d_st, p, "u_sky", 0, render3d_st.sky_tex)
- sky_bind_rot(render3d_st, p)
- sky_bind_lighting(render3d_st, p)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_inv_vp"), render3d_st.cam_inv_vp)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sky_gain"), 0.95)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sky_relight"), render3d_st.r3d_sky_relight)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sky_sat"), 1.35)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_time"), render3d_st.r3d_time)
- mesh_draw(render3d_st, render3d_st.sky_fullscreen)
- gpu_depth_write(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
+function r3d_draw_sky() -> void {
+ gpu_depth_func(GL_LEQUAL)
+ gpu_depth_write(false)
+ gpu_cull(false)
+ let p = r3d_sky_prog
+ gpu_use_program(p)
+ r3d_bind_2d(p, "u_sky", 0, sky_tex)
+ sky_bind_rot(p)
+ sky_bind_lighting(p)
+ u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
+ u_f(gpu_uniform(p, "u_sky_gain"), 0.95)
+ u_f(gpu_uniform(p, "u_sky_relight"), r3d_sky_relight)
+ u_f(gpu_uniform(p, "u_sky_sat"), 1.35)
+ u_f(gpu_uniform(p, "u_time"), r3d_time)
+ mesh_draw(sky_fullscreen)
+ gpu_depth_write(true)
+ gpu_depth_func(GL_LESS)
}
# The end of the game's frame, whichever backend draws it: a screenshot first if one is wanted,
# then the present. A game calls these rather than Gl.swap / Gl.screenshot.
-function r3d_screenshot(render3d_st: mut Render3dState, path: string) -> bool { return gpu_screenshot(render3d_st, path) }
-function r3d_present(render3d_st: mut Render3dState) -> void { gpu_present(render3d_st) }
+function r3d_screenshot(path: string) -> bool { return gpu_screenshot(path) }
+function r3d_present() -> void { gpu_present() }
# the drawable changed size: the camera's aspect and every screen-sized target follow
-function r3d_resize(render3d_st: mut Render3dState) -> void {
- render3d_st.cam_aspect = float(gl_w) / float(gl_h)
- cam_update(render3d_st)
- post_free(render3d_st)
- post_init(render3d_st, gl_w, gl_h)
- if render3d_st.water_refl != null { target_free(render3d_st, render3d_st.water_refl); render3d_st.water_refl = null }
+function r3d_resize() -> void {
+ cam_aspect = float(gl_w) / float(gl_h)
+ cam_update()
+ post_free()
+ post_init(gl_w, gl_h)
+ if water_refl != null { target_free(water_refl); water_refl = null }
print(`r3d: resized to {gl_w}x{gl_h}`)
}
-function r3d_frame(render3d_st: mut Render3dState, time: float) -> void {
- gpu_glcheck_after(render3d_st, "the time between frames")
- outline_frame(render3d_st)
- if not render3d_st.r3d_ready { return }
- if gpu_resize_check(render3d_st) { r3d_resize(render3d_st) }
+var r3d_cam_log: int = -1
+function r3d_frame(time: float) -> void {
+ gpu_glcheck_after("the time between frames")
+ outline_frame()
+ if not r3d_ready { return }
+ if gpu_resize_check() { r3d_resize() }
# R3D_RESIZE_AT=: rebuild every screen-sized buffer mid-run, as a window resize
# or a fullscreen change does. Headless has no window to resize, and this path is where
# a stale attachment or a texture freed twice shows up.
- render3d_st.r3d_test_frame += 1
- if render3d_st.r3d_test_resize == 0 and r3d_env_has(render3d_st, "R3D_RESIZE_AT") { render3d_st.r3d_test_resize = Text.to_int(r3d_env(render3d_st, "R3D_RESIZE_AT")) }
+ r3d_test_frame += 1
+ if r3d_test_resize == 0 and r3d_env_has("R3D_RESIZE_AT") { r3d_test_resize = Text.to_int(r3d_env("R3D_RESIZE_AT")) }
# R3D_RESIZE_AT=: from frame n on, rebuild every screen-sized buffer every few
# frames at a different size, as dragging a window edge or entering fullscreen does.
- if render3d_st.r3d_test_resize > 0 and render3d_st.r3d_test_frame >= render3d_st.r3d_test_resize and render3d_st.r3d_test_frame % 4 == 0 {
- let step = (render3d_st.r3d_test_frame / 4) % 4
+ if r3d_test_resize > 0 and r3d_test_frame >= r3d_test_resize and r3d_test_frame % 4 == 0 {
+ let step = (r3d_test_frame / 4) % 4
var w = 1920; var h = 1080
if step == 1 { w = 1440; h = 810 }
if step == 2 { w = 2560; h = 1440 }
if step == 3 { w = 1281; h = 721 }
gl_w = w; gl_h = h
- r3d_resize(render3d_st)
+ r3d_resize()
}
- render3d_st.r3d_time = time
- gsl_frame_start(render3d_st)
+ r3d_time = time
+ gsl_frame_start()
# the frame's counters close here, before any of its own work: the window each of them
# covers is exactly one frame, from this point to the same point next time
- prof_gen_frame(render3d_st)
- prof_mark_start(render3d_st)
- cam_begin_frame(render3d_st, render3d_st.post_frame, gl_w, gl_h)
+ prof_gen_frame()
+ prof_mark_start()
+ cam_begin_frame(post_frame, gl_w, gl_h)
# R3D_CAM_LOG=1: the camera each frame, in millimetres and thousandths of a radian - to tell a
# camera that moves while the hiker stands still from a picture that shakes on its own
- if render3d_st.r3d_cam_log < 0 { render3d_st.r3d_cam_log = 0; if r3d_env_has(render3d_st, "R3D_CAM_LOG") { render3d_st.r3d_cam_log = 1 } }
- if render3d_st.r3d_cam_log == 1 {
- print(`cam {render3d_st.r3d_test_frame} pos {int(render3d_st.cam_pos[0] * 1000.0)} {int(render3d_st.cam_pos[1] * 1000.0)} {int(render3d_st.cam_pos[2] * 1000.0)} yaw {int(render3d_st.cam_yaw * 1000.0)} pitch {int(render3d_st.cam_pitch * 1000.0)} jitter {int(render3d_st.gsl_jitter_x * 1000000.0)} {int(render3d_st.gsl_jitter_y * 1000000.0)} render {render3d_st.post_w}x{render3d_st.post_h} reset {render3d_st.gsl_reset} evalok {render3d_st.gsl_eval_ok} fresh {render3d_st.gsl_fresh}`)
+ if r3d_cam_log < 0 { r3d_cam_log = 0; if r3d_env_has("R3D_CAM_LOG") { r3d_cam_log = 1 } }
+ if r3d_cam_log == 1 {
+ print(`cam {r3d_test_frame} pos {int(cam_pos[0] * 1000.0)} {int(cam_pos[1] * 1000.0)} {int(cam_pos[2] * 1000.0)} yaw {int(cam_yaw * 1000.0)} pitch {int(cam_pitch * 1000.0)} jitter {int(gsl_jitter_x * 1000000.0)} {int(gsl_jitter_y * 1000000.0)} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`)
}
# the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock
- if not render3d_st.r3d_plate and ((not render3d_st.day_on and render3d_st.ter_shadow_yaw != render3d_st.sky_yaw) or render3d_st.ter_shadow_gen != render3d_st.day_gen) {
- render3d_st.ter_shadow_gen = render3d_st.day_gen
+ if not r3d_plate and ((not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen) {
+ ter_shadow_gen = day_gen
let t_bk = gl_now_us()
- terrain_bake_shadow(render3d_st)
- prof_bake_add(render3d_st, gl_now_us() - t_bk)
+ terrain_bake_shadow()
+ prof_bake_add(gl_now_us() - t_bk)
}
- scatter_begin_frame(render3d_st)
- prof_cpu_mark(render3d_st, "shadow rebake")
- stream_update_all(render3d_st)
- prof_cpu_mark(render3d_st, "streaming")
- if not render3d_st.r3d_no_shadow { prof_begin(render3d_st, "shadow"); shadow_pass(render3d_st); prof_end(render3d_st) }
- prof_cpu_mark(render3d_st, "shadow pass")
- if render3d_st.water_on and not render3d_st.r3d_no_refl and water_reflect_visible(render3d_st) { prof_begin(render3d_st, "water reflection"); water_reflection_pass(render3d_st); prof_end(render3d_st) }
- prof_cpu_mark(render3d_st, "reflection")
- post_begin_scene(render3d_st)
+ scatter_begin_frame()
+ prof_cpu_mark("shadow rebake")
+ stream_update_all()
+ prof_cpu_mark("streaming")
+ if not r3d_no_shadow { prof_begin("shadow"); shadow_pass(); prof_end() }
+ prof_cpu_mark("shadow pass")
+ if water_on and not r3d_no_refl and water_reflect_visible() { prof_begin("water reflection"); water_reflection_pass(); prof_end() }
+ prof_cpu_mark("reflection")
+ post_begin_scene()
# the near tree foliage lays its depth down before anything is shaded, so the terrain
# under the stands and the cards behind the front ones are rejected before lighting.
# It has to come straight after post_begin_scene: terrain_sun_prepare binds its own
# target, and depth drawn after it lands in the sun buffer, not the scene's.
- if not r3d_prepass_off(render3d_st) {
- prof_begin(render3d_st, "foliage prepass")
- gpu_color_write(render3d_st, false)
- scatter_draw_depth(render3d_st)
- gpu_color_write(render3d_st, true)
- prof_end(render3d_st)
- render3d_st.sc_prepass = true
+ if not r3d_prepass_off() {
+ prof_begin("foliage prepass")
+ gpu_color_write(false)
+ scatter_draw_depth()
+ gpu_color_write(true)
+ prof_end()
+ sc_prepass = true
}
- prof_cpu_mark(render3d_st, "foliage prepass")
- if not render3d_st.r3d_plate {
- prof_begin(render3d_st, "terrain sun")
- terrain_sun_prepare(render3d_st)
- prof_end(render3d_st)
- prof_begin(render3d_st, "terrain")
- terrain_draw(render3d_st)
- prof_end(render3d_st)
+ prof_cpu_mark("foliage prepass")
+ if not r3d_plate {
+ prof_begin("terrain sun")
+ terrain_sun_prepare()
+ prof_end()
+ prof_begin("terrain")
+ terrain_draw()
+ prof_end()
}
- prof_cpu_mark(render3d_st, "terrain")
- prof_begin(render3d_st, "scene (vegetation)")
- r3d_scene_draw(render3d_st)
- prof_end(render3d_st)
- render3d_st.sc_prepass = false
- prof_cpu_mark(render3d_st, "vegetation")
- prof_begin(render3d_st, "grass")
- grass_draw(render3d_st)
- prof_end(render3d_st)
- prof_cpu_mark(render3d_st, "grass")
- prof_begin(render3d_st, "sky")
- r3d_draw_sky(render3d_st)
- prof_end(render3d_st)
- prof_begin(render3d_st, "resolve MSAA")
- post_resolve(render3d_st)
- prof_end(render3d_st)
+ prof_cpu_mark("terrain")
+ prof_begin("scene (vegetation)")
+ r3d_scene_draw()
+ prof_end()
+ sc_prepass = false
+ prof_cpu_mark("vegetation")
+ prof_begin("grass")
+ grass_draw()
+ prof_end()
+ prof_cpu_mark("grass")
+ prof_begin("sky")
+ r3d_draw_sky()
+ prof_end()
+ prof_begin("resolve MSAA")
+ post_resolve()
+ prof_end()
# transparent water over the resolved frame: it tests against the frame's own
# depth and reads a copy of it for the depth tint and soft shores
- if render3d_st.water_on {
- prof_begin(render3d_st, "water surface")
- post_capture_scene(render3d_st)
- target_bind(render3d_st, render3d_st.post_hdr)
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- water_draw(render3d_st, render3d_st.post_depth_copy.depth)
- prof_end(render3d_st)
+ if water_on {
+ prof_begin("water surface")
+ post_capture_scene()
+ target_bind(post_hdr)
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ water_draw(post_depth_copy.depth)
+ prof_end()
}
- render3d_st.post_color = render3d_st.post_hdr.color; render3d_st.post_color_w = render3d_st.post_w; render3d_st.post_color_h = render3d_st.post_h
+ post_color = post_hdr.color; post_color_w = post_w; post_color_h = post_h
# DLSS super resolution: the lit frame up to the display's size, before anything reads it
- if gsl_dlss_live(render3d_st) { prof_begin(render3d_st, "DLSS"); render3d_st.post_color = gsl_dlss_eval(render3d_st); prof_end(render3d_st) }
+ if gsl_dlss_live() { prof_begin("DLSS"); post_color = gsl_dlss_eval(); prof_end() }
# Before bloom and before the tonemap: a shaft of light is a bright thing in the air and
# should bloom like one.
- prof_begin(render3d_st, "volumetric"); post_volumetric_pass(render3d_st); prof_end(render3d_st)
- prof_begin(render3d_st, "dof"); post_dof_pass(render3d_st); prof_end(render3d_st)
- if not render3d_st.post_no_gi { prof_begin(render3d_st, "SSAO/GI"); post_ssao_pass(render3d_st); prof_end(render3d_st) }
- if render3d_st.r3d_debug_max { tex_max(render3d_st, render3d_st.post_hdr.color, render3d_st.post_hdr.w, render3d_st.post_hdr.h, "hdr") }
- prof_begin(render3d_st, "bloom")
- post_bloom_pass(render3d_st)
- prof_end(render3d_st)
- prof_begin(render3d_st, "tonemap+exposure")
- post_tonemap(render3d_st, render3d_st.post_color)
- prof_end(render3d_st)
- prof_cpu_mark(render3d_st, "post")
- prof_begin(render3d_st, "prev-colour copy")
- post_capture_prev(render3d_st)
- prof_end(render3d_st)
- prof_collect(render3d_st)
- gpu_check(render3d_st, "frame")
+ prof_begin("volumetric"); post_volumetric_pass(); prof_end()
+ prof_begin("dof"); post_dof_pass(); prof_end()
+ if not post_no_gi { prof_begin("SSAO/GI"); post_ssao_pass(); prof_end() }
+ if r3d_debug_max { tex_max(post_hdr.color, post_hdr.w, post_hdr.h, "hdr") }
+ prof_begin("bloom")
+ post_bloom_pass()
+ prof_end()
+ prof_begin("tonemap+exposure")
+ post_tonemap(post_color)
+ prof_end()
+ prof_cpu_mark("post")
+ prof_begin("prev-colour copy")
+ post_capture_prev()
+ prof_end()
+ prof_collect()
+ gpu_check("frame")
}
diff --git a/packages/ludic.render3d/safe_api.ludic b/packages/ludic.render3d/safe_api.ludic
index 3db24a92..16f22b05 100644
--- a/packages/ludic.render3d/safe_api.ludic
+++ b/packages/ludic.render3d/safe_api.ludic
@@ -4,31 +4,31 @@
# at the boundary, so nothing raw crosses it.
# an accessor's float components (gltf_count elements of gltf_comps floats), as floats
-function gltf_accessor_floats(render3d_st: mut Render3dState, idx: int) -> floats {
- let p = gltf_accessor(render3d_st, idx)
- let n = render3d_st.gltf_count * render3d_st.gltf_comps
+function gltf_accessor_floats(idx: int) -> floats {
+ let p = gltf_accessor(idx)
+ let n = gltf_count * gltf_comps
let out = floats(n)
for i in 0 .. n { out[i] = float_from_bits(mem_get_f32_bits(p, i)) }
free(p)
return out
}
# how many elements an accessor holds, without reading them
-function gltf_accessor_count(render3d_st: Render3dState, idx: int) -> int {
- let acc = value_at(value_get(render3d_st.gltf_doc, "accessors"), idx)
+function gltf_accessor_count(idx: int) -> int {
+ let acc = value_at(value_get(gltf_doc, "accessors"), idx)
return jint(acc, "count", 0)
}
# the presented frame as RGB8, bottom row first, into `out` (at least w * h * 3 bytes)
-function gpu_read_screen_bytes(render3d_st: mut Render3dState, w: int, h: int, out: []byte) -> bool {
+function gpu_read_screen_bytes(w: int, h: int, out: []byte) -> bool {
if out == null or len(out) < w * h * 3 { return false }
- gpu_read_screen(render3d_st, w, h, data_of(out))
+ gpu_read_screen(w, h, data_of(out))
return true
}
# a PNG's samples (tex_w x tex_h x tex_channels, 8 or 16 bits): into `reuse` when it is large
# enough - a map swap decodes the same size again - else into a new buffer. null if unreadable.
-function png_decode_bytes(render3d_st: mut Render3dState, path: string, reuse: []byte) -> []byte {
- let p = png_decode(render3d_st, path)
+function png_decode_bytes(path: string, reuse: []byte) -> []byte {
+ let p = png_decode(path)
if p == null { return null }
- let n = render3d_st.tex_w * render3d_st.tex_h * render3d_st.tex_channels * (render3d_st.tex_depth / 8)
+ let n = tex_w * tex_h * tex_channels * (tex_depth / 8)
var out = reuse
if out == null or len(out) < n { out = buffer(n) }
for i in 0 .. n { out[i] = p[i] }
@@ -36,6 +36,6 @@ function png_decode_bytes(render3d_st: mut Render3dState, path: string, reuse: [
return out
}
# upload samples laid out as the last decode left them (tex_w, tex_h, tex_channels, tex_depth)
-function tex_upload_bytes(render3d_st: mut Render3dState, px: []byte, srgb: bool, mips: bool) -> int {
- return tex_upload(render3d_st, data_of(px), srgb, mips)
+function tex_upload_bytes(px: []byte, srgb: bool, mips: bool) -> int {
+ return tex_upload(data_of(px), srgb, mips)
}
diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic
index 6a4edf88..029d0e90 100644
--- a/packages/ludic.render3d/scatter.ludic
+++ b/packages/ludic.render3d/scatter.ludic
@@ -84,19 +84,19 @@ property Layer {
}
# the procedural models' one layout: position, normal, uv, interleaved at 32 bytes
-function sc_model_layout(render3d_st: Render3dState, m: Mesh) -> void {
- gpu_mesh_attr(render3d_st, m, 0, 3, GPU_F32, 32, 0, false)
- gpu_mesh_attr(render3d_st, m, 1, 3, GPU_F32, 32, 12, false)
- gpu_mesh_attr(render3d_st, m, 2, 2, GPU_F32, 32, 24, false)
+function sc_model_layout(m: Mesh) -> void {
+ gpu_mesh_attr(m, 0, 3, GPU_F32, 32, 0, false)
+ gpu_mesh_attr(m, 1, 3, GPU_F32, 32, 12, false)
+ gpu_mesh_attr(m, 2, 2, GPU_F32, 32, 24, false)
}
# Two crossed unit quads (x in [-0.5, 0.5], y in [0, 1]), attribute 0 = pos,
# 1 = the quad's facing normal, 2 = uv. Scaled per layer to the atlas card size.
-function model_cross_card(render3d_st: mut Render3dState) -> Model {
+function model_cross_card() -> Model {
let model = new Model
model.prims = new []Prim
let pr = new Prim
- let m = gpu_mesh_new(render3d_st)
+ let m = gpu_mesh_new()
let v = gl_floats(8 * 8)
var k = 0
for q in 0 .. 2 {
@@ -110,19 +110,19 @@ function model_cross_card(render3d_st: mut Render3dState) -> Model {
k += 8
}
}
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(64), GPU_STATIC)
- sc_model_layout(render3d_st, m)
+ gpu_mesh_vertices(m, v, gl_bytes_of(64), GPU_STATIC)
+ sc_model_layout(m)
free(v)
let idx = words(12)
idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3
idx[6] = 4; idx[7] = 5; idx[8] = 6; idx[9] = 4; idx[10] = 6; idx[11] = 7
- gpu_mesh_indices(render3d_st, m, data_of(idx), 48, 4)
+ gpu_mesh_indices(m, data_of(idx), 48, 4)
free(idx)
m.count = 12
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
pr.mesh = m
- if render3d_st.gltf_white == 0 { render3d_st.gltf_white = tex_solid(render3d_st, 200, 200, 200, 255); render3d_st.gltf_flat = tex_solid(render3d_st, 128, 128, 255, 255) }
- pr.diff = render3d_st.gltf_white; pr.nrm = render3d_st.gltf_flat; pr.arm = render3d_st.gltf_white
+ if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
+ pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = 0.5; model.height = 1.0; model.tris = 4
return model
@@ -134,20 +134,20 @@ function model_cross_card(render3d_st: mut Render3dState) -> Model {
# leaves it at zero. It is per layer rather than per instance because a species quakes or
# it does not.
function layer_flutter(l: Layer, v: float) -> void { l.flutter = v }
-function layer_cards(render3d_st: mut Render3dState, scan: Model, cap: int, wind: float, cull: float) -> Layer {
- let l = layer_new(render3d_st, model_cross_card(render3d_st), cap, true, wind, 0.0, cull)
+function layer_cards(scan: Model, cap: int, wind: float, cull: float) -> Layer {
+ let l = layer_new(model_cross_card(), cap, true, wind, 0.0, cull)
l.card = true
- l.atlas = impostor_bake(render3d_st, scan, 1, 512, 512)
+ l.atlas = impostor_bake(scan, 1, 512, 512)
return l
}
# A lupine spike (1 m tall): a stem of two crossed quads (uv.x in [0,1]) and
# seven tiers of crossed floret quads (uv.x in [1,2]), coloured in the shader.
-function model_lupine(render3d_st: mut Render3dState) -> Model {
+function model_lupine() -> Model {
let model = new Model
model.prims = new []Prim
let pr = new Prim
- let m = gpu_mesh_new(render3d_st)
+ let m = gpu_mesh_new()
# quads: stem x2 + tiers 12 x 2 + 3 leaves = 29 quads
let nq = 29
let v = gl_floats(nq * 4 * 8)
@@ -191,16 +191,16 @@ function model_lupine(render3d_st: mut Render3dState) -> Model {
idx[qi] = b; idx[qi + 1] = b + 1; idx[qi + 2] = b + 2; idx[qi + 3] = b; idx[qi + 4] = b + 2; idx[qi + 5] = b + 3
qi += 6
}
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC)
- sc_model_layout(render3d_st, m)
+ gpu_mesh_vertices(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC)
+ sc_model_layout(m)
free(v)
- gpu_mesh_indices(render3d_st, m, data_of(idx), nq * 6 * 4, 4)
+ gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4)
free(idx)
m.count = nq * 6
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
pr.mesh = m
- if render3d_st.gltf_white == 0 { render3d_st.gltf_white = tex_solid(render3d_st, 200, 200, 200, 255); render3d_st.gltf_flat = tex_solid(render3d_st, 128, 128, 255, 255) }
- pr.diff = render3d_st.gltf_white; pr.nrm = render3d_st.gltf_flat; pr.arm = render3d_st.gltf_white
+ if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
+ pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = 0.08; model.height = 0.65; model.tris = nq * 2
return model
@@ -208,11 +208,11 @@ function model_lupine(render3d_st: mut Render3dState) -> Model {
# A dense lupine for baking into a card: a stem, ~220 small floret quads in a
# tapering spiral (uv.x in [1,2]) and five leaves (uv.x in [2,3]).
-function model_lupine_dense(render3d_st: mut Render3dState) -> Model {
+function model_lupine_dense() -> Model {
let model = new Model
model.prims = new []Prim
let pr = new Prim
- let m = gpu_mesh_new(render3d_st)
+ let m = gpu_mesh_new()
let nfl = 220
let nq = 2 + nfl + 5
let v = gl_floats(nq * 4 * 8)
@@ -264,27 +264,27 @@ function model_lupine_dense(render3d_st: mut Render3dState) -> Model {
idx[qi] = b; idx[qi + 1] = b + 1; idx[qi + 2] = b + 2; idx[qi + 3] = b; idx[qi + 4] = b + 2; idx[qi + 5] = b + 3
qi += 6
}
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC)
- sc_model_layout(render3d_st, m)
+ gpu_mesh_vertices(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC)
+ sc_model_layout(m)
free(v)
- gpu_mesh_indices(render3d_st, m, data_of(idx), nq * 6 * 4, 4)
+ gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4)
free(idx)
m.count = nq * 6
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
pr.mesh = m
- if render3d_st.gltf_white == 0 { render3d_st.gltf_white = tex_solid(render3d_st, 200, 200, 200, 255); render3d_st.gltf_flat = tex_solid(render3d_st, 128, 128, 255, 255) }
- pr.diff = render3d_st.gltf_white; pr.nrm = render3d_st.gltf_flat; pr.arm = render3d_st.gltf_white
+ if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
+ pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = 0.11; model.height = 0.68; model.tris = nq * 2
return model
}
# A procedural grass blade (1 m tall, 5 cm wide, curved): 5 rows of 2 vertices.
-function model_blade(render3d_st: mut Render3dState) -> Model {
+function model_blade() -> Model {
let model = new Model
model.prims = new []Prim
let pr = new Prim
- let m = gpu_mesh_new(render3d_st)
+ let m = gpu_mesh_new()
let rows = 5
let v = gl_floats(rows * 2 * 8)
var k = 0
@@ -303,8 +303,8 @@ function model_blade(render3d_st: mut Render3dState) -> Model {
k += 8
}
}
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(rows * 2 * 8), GPU_STATIC)
- sc_model_layout(render3d_st, m)
+ gpu_mesh_vertices(m, v, gl_bytes_of(rows * 2 * 8), GPU_STATIC)
+ sc_model_layout(m)
free(v)
let ni = (rows - 1) * 6
let idx = words(ni)
@@ -315,68 +315,96 @@ function model_blade(render3d_st: mut Render3dState) -> Model {
idx[k + 3] = a + 1; idx[k + 4] = a + 3; idx[k + 5] = a + 2
k += 6
}
- gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4)
+ gpu_mesh_indices(m, data_of(idx), ni * 4, 4)
free(idx)
m.count = ni
- gpu_mesh_done(render3d_st, m)
+ gpu_mesh_done(m)
pr.mesh = m
- if render3d_st.gltf_white == 0 { render3d_st.gltf_white = tex_solid(render3d_st, 200, 200, 200, 255); render3d_st.gltf_flat = tex_solid(render3d_st, 128, 128, 255, 255) }
- pr.diff = render3d_st.gltf_white; pr.nrm = render3d_st.gltf_flat; pr.arm = render3d_st.gltf_white
+ if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
+ pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = 0.05; model.height = 1.0; model.tris = ni / 3
return model
}
+var sc_prog: int = 0
+var sc_prog_fol: int = 0
+var sc_prog_wind: int = 0
+var sc_prog_blade: int = 0
+var sc_prog_flower: int = 0
+var sc_prog_card: int = 0
+var sc_prog_card_shadow: int = 0
+var sc_prog_card_cheap: int = 0
+var sc_blade_base: floats = null
+var sc_blade_tip: floats = null
+var sc_blade_tint: floats = null
+var sc_prog_shadow: int = 0
+var sc_prog_shadow_wind: int = 0
+var sc_prog_shadow_fol: int = 0 # foliage meshes: alpha-tested casters
# The foliage depth prepass (render.ludic): the near tree LODs write depth first with a
# shader that only runs the alpha test, then the lit pass shades them with no discard and
# an equal depth test, so a pixel of needles is lit once rather than once for every card
# stacked behind it. In a dense stand at 4K that overdraw was the largest pass in the frame.
+var sc_prog_fol_depth: int = 0
+var sc_prog_fol_eq: int = 0
+var sc_prepass: bool = false # the main pass is drawing over what the prepass laid down
+var sc_imp_prog: int = 0
+var sc_imp_prog_shadow: int = 0
+var sc_bake_prog: int = 0
+var sc_bake_card_prog: int = 0
+var sc_bake_flower_prog: int = 0
+var sc_card: Mesh = null
+var sc_ident_buf: int = 0
+var sc_layers: []Layer = null
+var sc_debug_dump: bool = false
+var sc_printed: bool = false
+var sc_a2c: bool = true
-function scatter_init(render3d_st: mut Render3dState) -> void {
+function scatter_init() -> void {
# R3D_DUMP_ATLAS: every impostor and card atlas the run bakes, to build/atlas__{color,alpha}.ppm
- render3d_st.sc_debug_dump = r3d_env_has(render3d_st, "R3D_DUMP_ATLAS")
- render3d_st.sc_prog = r3d_program(render3d_st, "model.vert", "model.frag", "")
- render3d_st.sc_prog_fol = r3d_program(render3d_st, "model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n#define NEAR_FADE\n")
- render3d_st.sc_prog_fol_depth = r3d_program(render3d_st, "model.vert", "depth.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n#define NEAR_FADE\n")
- render3d_st.sc_prog_fol_eq = r3d_program(render3d_st, "model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define EQ_PASS\n#define NEAR_FADE\n")
- render3d_st.sc_prog_wind = r3d_program(render3d_st, "model.vert", "model.frag", "#define WIND\n")
- render3d_st.sc_prog_blade = r3d_program(render3d_st, "model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define BLADE\n")
- render3d_st.sc_prog_flower = r3d_program(render3d_st, "model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define FLOWER\n")
- render3d_st.sc_prog_card = r3d_program(render3d_st, "model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define CARD\n")
- render3d_st.sc_prog_card_shadow = r3d_program(render3d_st, "model.vert", "model.frag", "#define SHADOW_PASS\n#define WIND\n#define CARD\n")
- render3d_st.sc_prog_card_cheap = r3d_program(render3d_st, "model.vert", "model.frag", "#define CARD\n#define CHEAP\n")
+ sc_debug_dump = r3d_env_has("R3D_DUMP_ATLAS")
+ sc_prog = r3d_program("model.vert", "model.frag", "")
+ sc_prog_fol = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n#define NEAR_FADE\n")
+ sc_prog_fol_depth = r3d_program("model.vert", "depth.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n#define NEAR_FADE\n")
+ sc_prog_fol_eq = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define EQ_PASS\n#define NEAR_FADE\n")
+ sc_prog_wind = r3d_program("model.vert", "model.frag", "#define WIND\n")
+ sc_prog_blade = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define BLADE\n")
+ sc_prog_flower = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define FLOWER\n")
+ sc_prog_card = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define CARD\n")
+ sc_prog_card_shadow = r3d_program("model.vert", "model.frag", "#define SHADOW_PASS\n#define WIND\n#define CARD\n")
+ sc_prog_card_cheap = r3d_program("model.vert", "model.frag", "#define CARD\n#define CHEAP\n")
# a dry alpine meadow: brown-olive roots, straw with a little green at the tips
- render3d_st.sc_blade_base = v3_new(0.045, 0.06, 0.025)
- render3d_st.sc_blade_tip = v3_new(0.22, 0.27, 0.13)
- render3d_st.sc_blade_tint = v3_new(1.0, 1.0, 1.0)
- render3d_st.sc_prog_shadow = r3d_program(render3d_st, "model.vert", "shadow.frag", "#define SHADOW_PASS\n")
- render3d_st.sc_prog_shadow_wind = r3d_program(render3d_st, "model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n")
- render3d_st.sc_prog_shadow_fol = r3d_program(render3d_st, "model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n#define ALPHA_TEST\n")
- render3d_st.sc_imp_prog = r3d_program(render3d_st, "impostor.vert", "impostor.frag", "")
- render3d_st.sc_imp_prog_shadow = r3d_program(render3d_st, "impostor.vert", "impostor.frag", "#define SHADOW_PASS\n")
- render3d_st.sc_bake_prog = r3d_program(render3d_st, "model.vert", "bake.frag", "")
- render3d_st.sc_bake_flower_prog = r3d_program(render3d_st, "model.vert", "bake.frag", "#define FLOWER\n")
- render3d_st.sc_bake_card_prog = r3d_program(render3d_st, "model.vert", "bake.frag", "#define CARD\n")
- render3d_st.sc_card = mesh_card(render3d_st)
+ sc_blade_base = v3_new(0.045, 0.06, 0.025)
+ sc_blade_tip = v3_new(0.22, 0.27, 0.13)
+ sc_blade_tint = v3_new(1.0, 1.0, 1.0)
+ sc_prog_shadow = r3d_program("model.vert", "shadow.frag", "#define SHADOW_PASS\n")
+ sc_prog_shadow_wind = r3d_program("model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n")
+ sc_prog_shadow_fol = r3d_program("model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n#define ALPHA_TEST\n")
+ sc_imp_prog = r3d_program("impostor.vert", "impostor.frag", "")
+ sc_imp_prog_shadow = r3d_program("impostor.vert", "impostor.frag", "#define SHADOW_PASS\n")
+ sc_bake_prog = r3d_program("model.vert", "bake.frag", "")
+ sc_bake_flower_prog = r3d_program("model.vert", "bake.frag", "#define FLOWER\n")
+ sc_bake_card_prog = r3d_program("model.vert", "bake.frag", "#define CARD\n")
+ sc_card = mesh_card()
# a single identity instance, for baking
let one = gl_floats(INST_FLOATS)
for i in 0 .. INST_FLOATS { gl_put_bits(one, i, float_bits(0.0)) }
gl_put_bits(one, 3, float_bits(1.0)); gl_put_bits(one, 5, float_bits(1.0))
- render3d_st.sc_ident_buf = gpu_buffer_new(render3d_st)
- gpu_buffer_upload(render3d_st, render3d_st.sc_ident_buf, INST_FLOATS * 4, one, GPU_STATIC)
+ sc_ident_buf = gpu_buffer_new()
+ gpu_buffer_upload(sc_ident_buf, INST_FLOATS * 4, one, GPU_STATIC)
free(one)
- render3d_st.sc_layers = new []Layer
+ sc_layers = new []Layer
}
# feed a mesh its instances from `buf`: attribute 3 = position + scale, 4 = sin, cos, seed, wind
-function scatter_attach(render3d_st: Render3dState, m: Mesh, buf: int) -> void {
- gpu_mesh_bind_instances(render3d_st, m, buf)
- gpu_mesh_attr_inst(render3d_st, m, 3, 4, GPU_F32, INST_FLOATS * 4, 0)
- gpu_mesh_attr_inst(render3d_st, m, 4, 4, GPU_F32, INST_FLOATS * 4, 16)
- gpu_mesh_done(render3d_st, m)
+function scatter_attach(m: Mesh, buf: int) -> void {
+ gpu_mesh_bind_instances(m, buf)
+ gpu_mesh_attr_inst(m, 3, 4, GPU_F32, INST_FLOATS * 4, 0)
+ gpu_mesh_attr_inst(m, 4, 4, GPU_F32, INST_FLOATS * 4, 16)
+ gpu_mesh_done(m)
}
-function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer {
+function layer_new(model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer {
let l = new Layer
l.model = model
l.cap = cap
@@ -388,12 +416,12 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
l.inst = floats(cap * INST_FLOATS)
l.scratch = floats(cap * INST_FLOATS)
l.last_cam = v3_new(100000.0, 0.0, 0.0)
- l.buf = gpu_buffer_new(render3d_st)
- l.imp_buf = gpu_buffer_new(render3d_st)
- l.sh_buf = gpu_buffer_new(render3d_st)
+ l.buf = gpu_buffer_new()
+ l.imp_buf = gpu_buffer_new()
+ l.sh_buf = gpu_buffer_new()
l.rough = 1.0
- for i in 0 .. len(model.prims) { scatter_attach(render3d_st, model.prims[i].mesh, l.buf) }
- push(render3d_st.sc_layers, l)
+ for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh, l.buf) }
+ push(sc_layers, l)
return l
}
@@ -406,39 +434,41 @@ function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: fl
}
# ---- impostors ---------------------------------------------------------------------
-function impostor_bake(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int) -> Impostor {
+var sc_bake_flower: bool = false
+var sc_dump_n: int = 0
+function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor {
let im = new Impostor
im.tiles = tiles
im.radius = model.radius * 1.02
im.height = model.height
let aw = tiles * tw
- im.albedo = tex_target(render3d_st, aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
- im.normal = tex_target(render3d_st, aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
- let fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, fbo)
- gpu_fb_color(render3d_st, 0, im.albedo)
- gpu_fb_color(render3d_st, 1, im.normal)
- let rb = gpu_rb_new(render3d_st)
- gpu_rb_storage(render3d_st, rb, GL_DEPTH_COMPONENT24, aw, th, 0)
- gpu_fb_depth_rb(render3d_st, rb)
- gpu_fb_draw_buffers(render3d_st, 2)
- gpu_viewport(render3d_st, 0, 0, aw, th)
- gpu_clear_color(render3d_st, 0.0, 0.0, 0.0, 0.0)
- gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- gpu_cull(render3d_st, false)
- gpu_blend(render3d_st, false)
+ im.albedo = tex_target(aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
+ im.normal = tex_target(aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
+ let fbo = gpu_fb_new()
+ gpu_fb_bind(fbo)
+ gpu_fb_color(0, im.albedo)
+ gpu_fb_color(1, im.normal)
+ let rb = gpu_rb_new()
+ gpu_rb_storage(rb, GL_DEPTH_COMPONENT24, aw, th, 0)
+ gpu_fb_depth_rb(rb)
+ gpu_fb_draw_buffers(2)
+ gpu_viewport(0, 0, aw, th)
+ gpu_clear_color(0.0, 0.0, 0.0, 0.0)
+ gpu_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ gpu_cull(false)
+ gpu_blend(false)
# the model's prims temporarily take the identity instance
- for i in 0 .. len(model.prims) { scatter_attach(render3d_st, model.prims[i].mesh, render3d_st.sc_ident_buf) }
+ for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh, sc_ident_buf) }
let view = m4_new(); let proj = m4_new()
let eye = floats(3); let at = floats(3); let up = v3_new(0.0, 1.0, 0.0)
let cy = model.ymin + model.height * 0.5
let r = im.radius
let hh = model.height * 0.5
- var bake = render3d_st.sc_bake_prog
- if render3d_st.sc_bake_flower { bake = render3d_st.sc_bake_flower_prog }
- gpu_use_program(render3d_st, bake)
+ var bake = sc_bake_prog
+ if sc_bake_flower { bake = sc_bake_flower_prog }
+ gpu_use_program(bake)
for t in 0 .. tiles {
let a = 2.0 * PI * (float(t) / float(tiles))
v3_set(at, 0.0, cy, 0.0)
@@ -446,49 +476,49 @@ function impostor_bake(render3d_st: mut Render3dState, model: Model, tiles: int,
v3_set(eye, Math.sin(a) * (r * 4.0), cy + r * 0.5, -(Math.cos(a) * (r * 4.0)))
m4_look_at(view, eye, at, up)
m4_ortho(proj, -r, r, -hh, hh, 0.1, r * 9.0)
- u_mat4(render3d_st, gpu_uniform(render3d_st, bake, "u_view"), view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, bake, "u_proj"), proj)
- u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_wind"), 0.0)
- u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_flutter"), 0.0)
- gpu_viewport(render3d_st, t * tw, 0, tw, th)
+ u_mat4(gpu_uniform(bake, "u_view"), view)
+ u_mat4(gpu_uniform(bake, "u_proj"), proj)
+ u_f(gpu_uniform(bake, "u_wind"), 0.0)
+ u_f(gpu_uniform(bake, "u_flutter"), 0.0)
+ gpu_viewport(t * tw, 0, tw, th)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
- r3d_bind_2d(render3d_st, bake, "u_diff", 0, pr.diff)
- r3d_bind_2d(render3d_st, bake, "u_arm", 2, pr.arm)
- mesh_draw_instanced(render3d_st, pr.mesh, 1)
+ r3d_bind_2d(bake, "u_diff", 0, pr.diff)
+ r3d_bind_2d(bake, "u_arm", 2, pr.arm)
+ mesh_draw_instanced(pr.mesh, 1)
}
}
free(view); free(proj); free(eye); free(at); free(up)
- gpu_fb_bind(render3d_st, 0)
- gpu_fb_free(render3d_st, fbo)
- gpu_rb_free(render3d_st, rb)
- gpu_tex_bind(render3d_st, GPU_TEX2D, im.albedo)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
- gpu_tex_bind(render3d_st, GPU_TEX2D, im.normal)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
- gpu_check(render3d_st, "impostor bake")
+ gpu_fb_bind(0)
+ gpu_fb_free(fbo)
+ gpu_rb_free(rb)
+ gpu_tex_bind(GPU_TEX2D, im.albedo)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_mips(GPU_TEX2D)
+ gpu_tex_bind(GPU_TEX2D, im.normal)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_mips(GPU_TEX2D)
+ gpu_check("impostor bake")
# numbered, so every bake of a run survives to be compared (a card layer per species bakes one)
- if render3d_st.sc_debug_dump {
- render3d_st.sc_dump_n += 1
- render3d_st.tex_dump_alpha = true; tex_dump(render3d_st, im.albedo, aw, th, `build/atlas_{render3d_st.sc_dump_n}_alpha.ppm`); render3d_st.tex_dump_alpha = false
- tex_dump(render3d_st, im.albedo, aw, th, `build/atlas_{render3d_st.sc_dump_n}_color.ppm`)
+ if sc_debug_dump {
+ sc_dump_n += 1
+ tex_dump_alpha = true; tex_dump(im.albedo, aw, th, `build/atlas_{sc_dump_n}_alpha.ppm`); tex_dump_alpha = false
+ tex_dump(im.albedo, aw, th, `build/atlas_{sc_dump_n}_color.ppm`)
}
return im
}
# Give a layer a LOD chain. `dists` (float bits) are the outer distances of each level;
# the last one becomes the layer's `near` so the impostor (if any) starts there.
-function layer_set_lods(render3d_st: mut Render3dState, l: Layer, models: []Model, dists: floats) -> void {
+function layer_set_lods(l: Layer, models: []Model, dists: floats) -> void {
l.lods = models
l.n_lods = len(models)
l.lod_dist = floats(l.n_lods); l.lod_card = words(l.n_lods); l.lod_buf = words(l.n_lods); l.n_lod = words(l.n_lods)
for k in 0 .. l.n_lods {
l.lod_dist[k] = dists[k]; l.lod_card[k] = 0; l.n_lod[k] = 0
- l.lod_buf[k] = gpu_buffer_new(render3d_st)
+ l.lod_buf[k] = gpu_buffer_new()
let m = models[k]
- for i in 0 .. len(m.prims) { scatter_attach(render3d_st, m.prims[i].mesh, l.lod_buf[k]) }
+ for i in 0 .. len(m.prims) { scatter_attach(m.prims[i].mesh, l.lod_buf[k]) }
}
l.model = models[0]
l.near = dists[l.n_lods - 1]
@@ -497,25 +527,28 @@ function layer_set_lods(render3d_st: mut Render3dState, l: Layer, models: []Mode
# mark level k as the layer's crossed card (drawn with the card program and its atlas)
function layer_lod_card(l: Layer, k: int) -> void { l.lod_card[k] = 1 }
-function layer_set_impostor(render3d_st: Render3dState, l: Layer, im: Impostor) -> void {
+function layer_set_impostor(l: Layer, im: Impostor) -> void {
l.imp = im
- scatter_attach(render3d_st, render3d_st.sc_card, l.imp_buf)
+ scatter_attach(sc_card, l.imp_buf)
}
# ---- per frame -----------------------------------------------------------------------
# Partitions and gathers are redone only when the view changed enough to matter: the
# camera moved 1.5 m or turned about 2.5 degrees. Everything culled by the frustum keys
# off this one counter, so a turn re-gathers the streams and the grids together.
-function scatter_begin_frame(render3d_st: mut Render3dState) -> void {
- if render3d_st.sc_view_pos == null { render3d_st.sc_view_pos = v3_new(100000.0, 0.0, 0.0); render3d_st.sc_view_fwd = v3_new(0.0, 0.0, -1.0) }
- if v3_dist(render3d_st.sc_view_pos, render3d_st.cam_pos) > 1.5 or v3_dot(render3d_st.sc_view_fwd, render3d_st.cam_fwd) < 0.999 {
- render3d_st.sc_view_gen += 1
- v3_copy(render3d_st.sc_view_pos, render3d_st.cam_pos)
- v3_copy(render3d_st.sc_view_fwd, render3d_st.cam_fwd)
+var sc_view_gen: int = 1
+var sc_view_pos: floats = null
+var sc_view_fwd: floats = null
+function scatter_begin_frame() -> void {
+ if sc_view_pos == null { sc_view_pos = v3_new(100000.0, 0.0, 0.0); sc_view_fwd = v3_new(0.0, 0.0, -1.0) }
+ if v3_dist(sc_view_pos, cam_pos) > 1.5 or v3_dot(sc_view_fwd, cam_fwd) < 0.999 {
+ sc_view_gen += 1
+ v3_copy(sc_view_pos, cam_pos)
+ v3_copy(sc_view_fwd, cam_fwd)
}
# GPU-culled layers dispatch before the first pass of the frame, so no pass is split for it
- if render3d_st.sc_layers != null {
- for i in 0 .. len(render3d_st.sc_layers) { if render3d_st.sc_layers[i].n_lods > 1 and render3d_st.sc_layers[i].imp != null { layer_update(render3d_st, render3d_st.sc_layers[i]) } }
+ if sc_layers != null {
+ for i in 0 .. len(sc_layers) { if sc_layers[i].n_lods > 1 and sc_layers[i].imp != null { layer_update(sc_layers[i]) } }
}
}
@@ -527,10 +560,15 @@ function scatter_begin_frame(render3d_st: mut Render3dState) -> void {
# when the view moves. PC camp benchmark: 2791 -> 2657 draws, 4.3 -> 4.1 s for 400 frames.
const SC_REC_W: int = 20 # a VkDrawIndexedIndirectCommand
const SC_RECS: int = 29 # 16 level x prim, 1 impostor, 12 shadow LOD (scatter_cull.comp)
+var sc_cull_prog: int = 0
+var sc_cull_tried: bool = false
+var sc_ind_base: int = -1 # >= 0: layer_draw_model / layer_draw_depth draw records from here
+var sc_ind_n: int = 1 # records each of those draws covers: one per level of a material
+var sc_ind_stride: int = 4 # records between one material's first and the next's (3 for the shadow LOD)
-function layer_gpu_eligible(render3d_st: Render3dState, l: Layer) -> bool {
+function layer_gpu_eligible(l: Layer) -> bool {
if l.n_lods < 2 or l.n_lods > 4 or l.imp == null or l.streamed or l.flower or l.blade or l.count == 0 { return false }
- return layer_arena_ok(render3d_st, l)
+ return layer_arena_ok(l)
}
# The merged meshes. Every level of a kit tree or rock carries the same materials in the same order
@@ -539,7 +577,7 @@ function layer_gpu_eligible(render3d_st: Render3dState, l: Layer) -> bool {
# level's index and vertex range and its bucket's instances. A conifer's lit pass goes from eight
# draws to two. Anything that does not fit - a card level, a level with other materials, other
# attributes or 32-bit indices - keeps the CPU path.
-function layer_arena_ok(render3d_st: Render3dState, l: Layer) -> bool {
+function layer_arena_ok(l: Layer) -> bool {
let n_mat = len(l.lods[0].prims)
if n_mat == 0 or n_mat > 4 { return false }
for k in 0 .. l.n_lods {
@@ -550,18 +588,18 @@ function layer_arena_ok(render3d_st: Render3dState, l: Layer) -> bool {
let pm = m.prims[j].mesh
let p0 = l.lods[0].prims[j]
if m.prims[j].diff != p0.diff or m.prims[j].verts == 0 or pm.ebo == 0 or pm.itype != GL_UNSIGNED_SHORT { return false }
- if gpu_buffer_map(render3d_st, pm.ebo) == null { return false }
+ if gpu_buffer_map(pm.ebo) == null { return false }
for a in 0 .. 3 {
let o = a * GPU_ATTR_W
if p0.mesh.attrs[o + 1] == 0 or pm.attrs[o + 1] != p0.mesh.attrs[o + 1] or pm.attrs[o + 3] != pm.attrs[o + 1] * 4 { return false }
- if gpu_buffer_map(render3d_st, pm.attrs[o]) == null { return false }
+ if gpu_buffer_map(pm.attrs[o]) == null { return false }
}
}
}
return true
}
-function layer_arena_build(render3d_st: mut Render3dState, l: Layer) -> void {
+function layer_arena_build(l: Layer) -> void {
let n_mat = len(l.lods[0].prims)
l.g_arena = new []Prim
l.g_first = words(16); l.g_base = words(16)
@@ -575,28 +613,28 @@ function layer_arena_build(render3d_st: mut Render3dState, l: Layer) -> void {
nv += pr.verts; ni += pr.mesh.count
}
let p0 = l.lods[0].prims[j]
- let m = gpu_mesh_new(render3d_st)
+ let m = gpu_mesh_new()
for a in 0 .. 3 {
let comps = p0.mesh.attrs[a * GPU_ATTR_W + 1]
let vb = bytes(nv * comps * 4 + 8)
for k in 0 .. l.n_lods {
let pr = l.lods[k].prims[j]
- mem_copy(mem_off(vb, l.g_base[j * 4 + k] * comps * 4), gpu_buffer_map(render3d_st, pr.mesh.attrs[a * GPU_ATTR_W]), pr.verts * comps * 4)
+ mem_copy(mem_off(vb, l.g_base[j * 4 + k] * comps * 4), gpu_buffer_map(pr.mesh.attrs[a * GPU_ATTR_W]), pr.verts * comps * 4)
}
- gpu_mesh_vertices(render3d_st, m, vb, nv * comps * 4, GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, a, comps, GPU_F32, 0, 0, false)
+ gpu_mesh_vertices(m, vb, nv * comps * 4, GPU_STATIC)
+ gpu_mesh_attr(m, a, comps, GPU_F32, 0, 0, false)
free(vb)
}
let ib = bytes(ni * 2 + 8)
for k in 0 .. l.n_lods {
let pm = l.lods[k].prims[j].mesh
- mem_copy(mem_off(ib, l.g_first[j * 4 + k] * 2), gpu_buffer_map(render3d_st, pm.ebo), pm.count * 2)
+ mem_copy(mem_off(ib, l.g_first[j * 4 + k] * 2), gpu_buffer_map(pm.ebo), pm.count * 2)
}
- gpu_mesh_indices(render3d_st, m, ib, ni * 2, 2)
+ gpu_mesh_indices(m, ib, ni * 2, 2)
free(ib)
m.count = ni
- gpu_mesh_done(render3d_st, m)
- scatter_attach(render3d_st, m, l.g_dst)
+ gpu_mesh_done(m)
+ scatter_attach(m, l.g_dst)
let ap = new Prim
ap.mesh = m; ap.diff = p0.diff; ap.nrm = p0.nrm; ap.arm = p0.arm; ap.verts = nv; ap.name = p0.name
push(l.g_arena, ap)
@@ -609,29 +647,29 @@ function layer_arena_build(render3d_st: mut Render3dState, l: Layer) -> void {
l.g_model_sh.prims = l.g_arena; l.g_model_sh.height = l.lods[sh].height; l.g_model_sh.radius = l.lods[sh].radius; l.g_model_sh.ymin = l.lods[sh].ymin
}
-function layer_gpu_prepare(render3d_st: mut Render3dState, l: Layer) -> bool {
- if not render3d_st.sc_cull_tried {
- render3d_st.sc_cull_tried = true
+function layer_gpu_prepare(l: Layer) -> bool {
+ if not sc_cull_tried {
+ sc_cull_tried = true
# Os.env is null when the variable is unset, and a compare reads through it: ask first
# on by default wherever there is compute; R3D_GPU_CULL=0 keeps the CPU partition (for comparing)
var off = false
- if r3d_env_has(render3d_st, "R3D_GPU_CULL") { off = r3d_env(render3d_st, "R3D_GPU_CULL") == "0" }
- if gpu_has_compute(render3d_st) and gpu_has_mdi(render3d_st) and not off {
- render3d_st.sc_cull_prog = gpu_compute(render3d_st, "scatter_cull", 4)
- if render3d_st.sc_cull_prog > 0 { print("r3d: scatter: tree layers are culled on the GPU") }
+ if r3d_env_has("R3D_GPU_CULL") { off = r3d_env("R3D_GPU_CULL") == "0" }
+ if gpu_has_compute() and gpu_has_mdi() and not off {
+ sc_cull_prog = gpu_compute("scatter_cull", 4)
+ if sc_cull_prog > 0 { print("r3d: scatter: tree layers are culled on the GPU") }
}
}
- if render3d_st.sc_cull_prog == 0 or not layer_gpu_eligible(render3d_st, l) { return false }
+ if sc_cull_prog == 0 or not layer_gpu_eligible(l) { return false }
if l.g_on and l.g_n == l.count { return true }
if l.g_src == 0 {
- l.g_src = gpu_buffer_new(render3d_st); l.g_dst = gpu_buffer_new(render3d_st); l.g_cmds = gpu_buffer_new(render3d_st); l.g_counts = gpu_buffer_new(render3d_st)
- gpu_buffer_gpu_owned(render3d_st, l.g_dst); gpu_buffer_gpu_owned(render3d_st, l.g_cmds); gpu_buffer_gpu_owned(render3d_st, l.g_counts)
+ l.g_src = gpu_buffer_new(); l.g_dst = gpu_buffer_new(); l.g_cmds = gpu_buffer_new(); l.g_counts = gpu_buffer_new()
+ gpu_buffer_gpu_owned(l.g_dst); gpu_buffer_gpu_owned(l.g_cmds); gpu_buffer_gpu_owned(l.g_counts)
}
let n = l.n_lods
let cap = l.count
- gpu_buffer_upload(render3d_st, l.g_src, cap * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
- gpu_buffer_upload(render3d_st, l.g_dst, (n + 1) * cap * INST_FLOATS * 4, null, GPU_DYNAMIC)
- if l.g_arena == null { layer_arena_build(render3d_st, l) }
+ gpu_buffer_upload(l.g_src, cap * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
+ gpu_buffer_upload(l.g_dst, (n + 1) * cap * INST_FLOATS * 4, null, GPU_DYNAMIC)
+ if l.g_arena == null { layer_arena_build(l) }
let n_mat = len(l.g_arena)
let rec = words(SC_RECS * 5)
for i in 0 .. SC_RECS * 5 { rec[i] = 0 }
@@ -642,7 +680,7 @@ function layer_gpu_prepare(render3d_st: mut Render3dState, l: Layer) -> bool {
rec[r] = l.lods[k].prims[j].mesh.count; rec[r + 2] = l.g_first[j * 4 + k]; rec[r + 3] = l.g_base[j * 4 + k]; rec[r + 4] = k * cap
}
}
- rec[16 * 5] = render3d_st.sc_card.count; rec[16 * 5 + 4] = n * cap
+ rec[16 * 5] = sc_card.count; rec[16 * 5 + 4] = n * cap
# the shadow LOD: level 2's range, from buckets 0 .. 2
if n > 2 {
for j in 0 .. n_mat {
@@ -652,39 +690,39 @@ function layer_gpu_prepare(render3d_st: mut Render3dState, l: Layer) -> bool {
}
}
}
- gpu_buffer_upload(render3d_st, l.g_cmds, SC_RECS * SC_REC_W, data_of(rec), GPU_DYNAMIC)
+ gpu_buffer_upload(l.g_cmds, SC_RECS * SC_REC_W, data_of(rec), GPU_DYNAMIC)
let zeros = words(5)
for i in 0 .. 5 { zeros[i] = 0 }
- gpu_buffer_upload(render3d_st, l.g_counts, 20, data_of(zeros), GPU_DYNAMIC)
+ gpu_buffer_upload(l.g_counts, 20, data_of(zeros), GPU_DYNAMIC)
free(rec); free(zeros)
# the card casts every instance, as on the CPU path (layer_grid_build uploads this there)
l.n_sh = l.count
- gpu_buffer_upload(render3d_st, l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
+ gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
l.g_n = l.count
l.g_on = true
return true
}
# the dispatch for the view as it stands: frustum, camera, distances, the layer's shape
-function layer_gpu_cull(render3d_st: mut Render3dState, l: Layer) -> void {
+function layer_gpu_cull(l: Layer) -> void {
let pr = words(36)
for i in 0 .. 36 { pr[i] = 0 }
- if render3d_st.cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(render3d_st.cam_planes[i]) } }
- pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(l.cull)
+ if cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(cam_planes[i]) } }
+ pr[16] = float_bits(cam_pos[0]); pr[17] = float_bits(cam_pos[1]); pr[18] = float_bits(cam_pos[2]); pr[19] = float_bits(l.cull)
for k in 0 .. l.n_lods { pr[20 + k] = float_bits(l.lod_dist[k]); pr[24 + k] = len(l.lods[k].prims) }
pr[28] = l.count; pr[29] = l.count; pr[30] = l.n_lods; pr[31] = 1
# as layer_grid_gather pads a cell: the tallest instance, plus a margin
pr[32] = float_bits(l.lods[0].height * 2.0); pr[33] = float_bits(4.0)
let bufs = words(4)
bufs[0] = l.g_src; bufs[1] = l.g_dst; bufs[2] = l.g_cmds; bufs[3] = l.g_counts
- gpu_dispatch(render3d_st, render3d_st.sc_cull_prog, data_of(pr), 144, bufs, 1)
+ gpu_dispatch(sc_cull_prog, data_of(pr), 144, bufs, 1)
free(pr); free(bufs)
}
# Sort a static layer's instances into square cells (call once, after placement; a
# large layer that was never gridded gets a 96 m grid on its first update). The
# shadow buffer is uploaded here once — casters are never culled by the view.
-function layer_grid_build(render3d_st: mut Render3dState, l: Layer, cell: float) -> void {
+function layer_grid_build(l: Layer, cell: float) -> void {
if l.count == 0 { return }
var minx = l.inst[0]; var maxx = minx; var minz = l.inst[2]; var maxz = minz
for i in 0 .. l.count {
@@ -724,11 +762,11 @@ function layer_grid_build(render3d_st: mut Render3dState, l: Layer, cell: float)
free(cellof); free(fill)
if l.vis == null { l.vis = floats(l.cap * INST_FLOATS) }
l.n_sh = l.count
- gpu_buffer_upload(render3d_st, l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
+ gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC)
}
# gather the instances of the cells the camera can see (and that are within cull)
-function layer_grid_gather(render3d_st: Render3dState, l: Layer) -> void {
+function layer_grid_gather(l: Layer) -> void {
let cell = l.gcell
let half = cell * 0.5
let reach = l.cull + cell * 0.71
@@ -741,14 +779,14 @@ function layer_grid_gather(render3d_st: Render3dState, l: Layer) -> void {
if cnt == 0 { continue }
let wx = l.gx0 + float(ix) * cell + half
if l.cull != 0.0 {
- let dx = wx - render3d_st.cam_pos[0]; let dz = wz - render3d_st.cam_pos[2]
+ let dx = wx - cam_pos[0]; let dz = wz - cam_pos[2]
if Math.sqrt(dx * dx + dz * dz) > reach { continue }
}
let hy = (l.gymax[c] - l.gymin[c]) * 0.5
let cy = l.gymin[c] + hy
# pad by the tallest instance (scale 2 of the model's height) so crowns at the frame's edge stay
let r = Math.sqrt(half * half * 2.0 + hy * hy) + (l.model.height * 2.0 + 4.0)
- if not cam_sphere_visible(render3d_st, wx, cy, wz, r) { continue }
+ if not cam_sphere_visible(wx, cy, wz, r) { continue }
mem_copy(mem_off(l.vis, n * INST_FLOATS * 4), mem_off(l.gsorted, l.gstart[c] * INST_FLOATS * 4), cnt * INST_FLOATS * 4)
n += cnt
}
@@ -758,7 +796,7 @@ function layer_grid_gather(render3d_st: Render3dState, l: Layer) -> void {
# Sort the gathered instances into their LOD levels (counting sort into the scratch),
# the impostor bucket last, and upload one buffer per level.
-function layer_partition_lods(render3d_st: mut Render3dState, l: Layer, src: floats, total: int) -> void {
+function layer_partition_lods(l: Layer, src: floats, total: int) -> void {
let n = l.n_lods
let counts = words(n + 2)
for k in 0 .. n + 2 { counts[k] = 0 }
@@ -766,7 +804,7 @@ function layer_partition_lods(render3d_st: mut Render3dState, l: Layer, src: flo
let open = l.lod_dist[n - 1] == 0.0 # the last level runs out to the cull distance
for i in 0 .. total {
let o = i * INST_FLOATS
- let dx = src[o] - render3d_st.cam_pos[0]; let dz = src[o + 2] - render3d_st.cam_pos[2]
+ let dx = src[o] - cam_pos[0]; let dz = src[o + 2] - cam_pos[2]
let d2 = dx * dx + dz * dz
var lv = n + 1 # n + 1 = dropped
if l.cull == 0.0 or not (d2 > cull2) {
@@ -798,43 +836,44 @@ function layer_partition_lods(render3d_st: mut Render3dState, l: Layer, src: flo
for k in 0 .. n {
l.n_lod[k] = counts[k]
if counts[k] > 0 {
- gpu_buffer_upload(render3d_st, l.lod_buf[k], counts[k] * INST_FLOATS * 4, mem_off(tmp, start[k] * INST_FLOATS * 4), GPU_DYNAMIC)
+ gpu_buffer_upload(l.lod_buf[k], counts[k] * INST_FLOATS * 4, mem_off(tmp, start[k] * INST_FLOATS * 4), GPU_DYNAMIC)
}
}
l.n_near = counts[0]
l.n_far = counts[n]
- if render3d_st.sc_dbg_lod and total > 1000 { print(`lod partition: total {total} dropped {counts[n + 1]} far {counts[n]} l0 {counts[0]} l1 {counts[1]} l2 {counts[2]} l3 {counts[3]} dist0 {fixed(l.lod_dist[0])} dist3 {fixed(l.lod_dist[n - 1])} cull {fixed(l.cull)} cam {fixed(render3d_st.cam_pos[0])} {fixed(render3d_st.cam_pos[2])} first {fixed(src[0])} {fixed(src[2])}`) }
+ if sc_dbg_lod and total > 1000 { print(`lod partition: total {total} dropped {counts[n + 1]} far {counts[n]} l0 {counts[0]} l1 {counts[1]} l2 {counts[2]} l3 {counts[3]} dist0 {fixed(l.lod_dist[0])} dist3 {fixed(l.lod_dist[n - 1])} cull {fixed(l.cull)} cam {fixed(cam_pos[0])} {fixed(cam_pos[2])} first {fixed(src[0])} {fixed(src[2])}`) }
if l.n_far > 0 {
- gpu_buffer_upload(render3d_st, l.imp_buf, l.n_far * INST_FLOATS * 4, mem_off(tmp, start[n] * INST_FLOATS * 4), GPU_DYNAMIC)
+ gpu_buffer_upload(l.imp_buf, l.n_far * INST_FLOATS * 4, mem_off(tmp, start[n] * INST_FLOATS * 4), GPU_DYNAMIC)
}
# casters: the whole (gathered) set from the shadow buffer, unless the impostor casts
if l.gcell == 0.0 {
l.n_sh = total
- if total > 0 { gpu_buffer_upload(render3d_st, l.sh_buf, total * INST_FLOATS * 4, data_of(src), GPU_DYNAMIC) }
+ if total > 0 { gpu_buffer_upload(l.sh_buf, total * INST_FLOATS * 4, data_of(src), GPU_DYNAMIC) }
}
free(counts); free(start); free(fill)
}
# split the instances by distance to the camera (only when the view changed)
-function layer_update(render3d_st: mut Render3dState, l: Layer) -> void {
- if render3d_st.sc_freeze { return }
- if l.view_gen == render3d_st.sc_view_gen { return }
- l.view_gen = render3d_st.sc_view_gen
- if layer_gpu_prepare(render3d_st, l) { layer_gpu_cull(render3d_st, l); return }
+var sc_freeze: bool = false # a secondary pass (reflection) reuses the partition
+function layer_update(l: Layer) -> void {
+ if sc_freeze { return }
+ if l.view_gen == sc_view_gen { return }
+ l.view_gen = sc_view_gen
+ if layer_gpu_prepare(l) { layer_gpu_cull(l); return }
let t_lu = gl_now_us()
let n_lu = l.count
# A streamed layer's instances were already gathered per visible chunk: no split, no
# per-instance loop — one upload, and the same buffer casts its shadows.
if l.streamed and l.imp == null and l.near == 0.0 and l.n_lods <= 1 {
l.n_near = l.count; l.n_far = 0; l.n_sh = l.count
- gpu_buffer_upload(render3d_st, l.buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC)
- prof_layer_add(render3d_st, gl_now_us() - t_lu, l.count * INST_FLOATS * 4)
+ gpu_buffer_upload(l.buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC)
+ prof_layer_add(gl_now_us() - t_lu, l.count * INST_FLOATS * 4)
return
}
- if l.gcell == 0.0 and not l.streamed and l.count > 2000 { layer_grid_build(render3d_st, l, 96.0) }
+ if l.gcell == 0.0 and not l.streamed and l.count > 2000 { layer_grid_build(l, 96.0) }
var src = l.inst
var total = l.count
- if l.gcell != 0.0 { layer_grid_gather(render3d_st, l); src = l.vis; total = l.n_vis }
+ if l.gcell != 0.0 { layer_grid_gather(l); src = l.vis; total = l.n_vis }
let near2 = l.near * l.near
let cull2 = l.cull * l.cull
var nn = 0
@@ -842,15 +881,15 @@ function layer_update(render3d_st: mut Render3dState, l: Layer) -> void {
let far_off = l.cap * INST_FLOATS # far instances fill the scratch from its end backwards
let tmp = l.scratch
if l.n_lods > 1 {
- layer_partition_lods(render3d_st, l, src, total)
- prof_layer_add(render3d_st, gl_now_us() - t_lu, total * INST_FLOATS * 4)
+ layer_partition_lods(l, src, total)
+ prof_layer_add(gl_now_us() - t_lu, total * INST_FLOATS * 4)
return
}
var i = 0
while i < total {
let o = i * INST_FLOATS
- let dx = src[o] - render3d_st.cam_pos[0]
- let dz = src[o + 2] - render3d_st.cam_pos[2]
+ let dx = src[o] - cam_pos[0]
+ let dz = src[o + 2] - cam_pos[2]
let d2 = dx * dx + dz * dz
if l.cull != 0.0 and d2 > cull2 { i += 1; continue }
if l.near == 0.0 or d2 < near2 {
@@ -866,8 +905,8 @@ function layer_update(render3d_st: mut Render3dState, l: Layer) -> void {
}
l.n_near = nn
l.n_far = nf
- if l.imp != null and nn > 0 and render3d_st.sc_debug_dump { print(`near full-mesh instances: {nn} (first at {fixed(tmp[0])} {fixed(tmp[1])} {fixed(tmp[2])})`) }
- if render3d_st.sc_debug_dump and l.imp != null {
+ if l.imp != null and nn > 0 and sc_debug_dump { print(`near full-mesh instances: {nn} (first at {fixed(tmp[0])} {fixed(tmp[1])} {fixed(tmp[2])})`) }
+ if sc_debug_dump and l.imp != null {
print(`layer: near {nn} far {nf}`)
for k in 0 .. nn { let q = k * INST_FLOATS; print(` near {fixed(tmp[q])} {fixed(tmp[q + 1])} {fixed(tmp[q + 2])} s {fixed(tmp[q + 3])}`) }
}
@@ -877,38 +916,42 @@ function layer_update(render3d_st: mut Render3dState, l: Layer) -> void {
if l.gcell == 0.0 {
l.n_sh = l.count
if l.count > 0 {
- gpu_buffer_upload(render3d_st, l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC)
+ gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC)
}
}
- gpu_buffer_upload(render3d_st, l.buf, nn * INST_FLOATS * 4, data_of(tmp), GPU_DYNAMIC)
+ gpu_buffer_upload(l.buf, nn * INST_FLOATS * 4, data_of(tmp), GPU_DYNAMIC)
if nf > 0 {
- gpu_buffer_upload(render3d_st, l.imp_buf, nf * INST_FLOATS * 4, mem_off(tmp, (far_off - nf * INST_FLOATS) * 4), GPU_DYNAMIC)
+ gpu_buffer_upload(l.imp_buf, nf * INST_FLOATS * 4, mem_off(tmp, (far_off - nf * INST_FLOATS) * 4), GPU_DYNAMIC)
}
- prof_layer_add(render3d_st, gl_now_us() - t_lu, (nn + nf + l.n_sh) * INST_FLOATS * 4)
+ prof_layer_add(gl_now_us() - t_lu, (nn + nf + l.n_sh) * INST_FLOATS * 4)
}
-function layer_program(render3d_st: Render3dState, l: Layer, shadow: bool, card: bool) -> int {
+function layer_program(l: Layer, shadow: bool, card: bool) -> int {
if card {
- if shadow { return render3d_st.sc_prog_card_shadow }
- if l.cheap { return render3d_st.sc_prog_card_cheap }
- return render3d_st.sc_prog_card
+ if shadow { return sc_prog_card_shadow }
+ if l.cheap { return sc_prog_card_cheap }
+ return sc_prog_card
}
if shadow {
- if l.foliage and not l.blade and not l.flower { return render3d_st.sc_prog_shadow_fol }
- if l.wind != 0.0 { return render3d_st.sc_prog_shadow_wind }
- return render3d_st.sc_prog_shadow
+ if l.foliage and not l.blade and not l.flower { return sc_prog_shadow_fol }
+ if l.wind != 0.0 { return sc_prog_shadow_wind }
+ return sc_prog_shadow
}
- if l.blade { return render3d_st.sc_prog_blade }
- if l.flower { return render3d_st.sc_prog_flower }
+ if l.blade { return sc_prog_blade }
+ if l.flower { return sc_prog_flower }
if l.foliage {
- if render3d_st.sc_prepass and render3d_st.sc_prog_fol_eq != 0 { return render3d_st.sc_prog_fol_eq }
- return render3d_st.sc_prog_fol
+ if sc_prepass and sc_prog_fol_eq != 0 { return sc_prog_fol_eq }
+ return sc_prog_fol
}
- if l.wind != 0.0 { return render3d_st.sc_prog_wind }
- return render3d_st.sc_prog
+ if l.wind != 0.0 { return sc_prog_wind }
+ return sc_prog
}
+var sc_dbg_blade: int = 0
# R3D_LODDBG=1 tints each LOD level (red, green, blue, yellow) and impostors magenta
+var sc_dbg_level: int = -1
+var sc_dbg_lod: bool = false
+var sc_dbg_tint: floats = null
# Can level k's casters (its instances lie between the previous level's distance and its own) put a
# shadow on anything the cascade being rendered covers? A receiver in that slice of view depth
# [near, far] stands between near - dy and far * K metres away on the ground: dy is the camera's height
@@ -918,35 +961,39 @@ function layer_program(render3d_st: Render3dState, l: Layer, shadow: bool, card:
# at fixed distances, which dropped casters that did cast (see scatter_draw_casters). The flowers'
# mesh levels (6 - 30 m) stop being drawn into the three outer cascades. R3D_CAST_ALL=1 draws every
# level into every cascade, for comparing.
-function layer_level_casts_here(render3d_st: mut Render3dState, l: Layer, k: int) -> bool {
+var sc_cast_all: int = -1
+var sc_cast_gen: int = -1
+var sc_cast_dy: float = 0.0
+var sc_cast_k: float = 0.0
+function layer_level_casts_here(l: Layer, k: int) -> bool {
if l.lod_dist == null { return true }
var dmin = 0.0
if k > 0 { dmin = l.lod_dist[k - 1] }
- return cast_band_reaches(render3d_st, dmin, l.lod_dist[k], l.lods[k].height)
+ return cast_band_reaches(dmin, l.lod_dist[k], l.lods[k].height)
}
# Can something standing between dmin and dmax metres from the camera (dmax 0: no outer limit), this
# tall, put a shadow on anything the cascade being rendered covers? The flowers' levels ask it
# (layer_level_casts_here), and so does every actor (actor_draw_casters).
-function cast_band_reaches(render3d_st: mut Render3dState, dmin: float, dmax: float, height: float) -> bool {
- if render3d_st.sc_cast_all < 0 { render3d_st.sc_cast_all = 0; if r3d_env_has(render3d_st, "R3D_CAST_ALL") { render3d_st.sc_cast_all = 1 } }
- if render3d_st.sc_cast_all == 1 or render3d_st.sh_split == null { return true }
- if render3d_st.sc_cast_gen != render3d_st.sc_view_gen {
- render3d_st.sc_cast_gen = render3d_st.sc_view_gen
- render3d_st.sc_cast_dy = Math.abs(render3d_st.cam_pos[1] - terrain_height(render3d_st, render3d_st.cam_pos[0], render3d_st.cam_pos[2])) + 5.0
- let half = render3d_st.cam_fov * 0.5
+function cast_band_reaches(dmin: float, dmax: float, height: float) -> bool {
+ if sc_cast_all < 0 { sc_cast_all = 0; if r3d_env_has("R3D_CAST_ALL") { sc_cast_all = 1 } }
+ if sc_cast_all == 1 or sh_split == null { return true }
+ if sc_cast_gen != sc_view_gen {
+ sc_cast_gen = sc_view_gen
+ sc_cast_dy = Math.abs(cam_pos[1] - terrain_height(cam_pos[0], cam_pos[2])) + 5.0
+ let half = cam_fov * 0.5
let t = Math.sin(half) / Math.cos(half)
- let ta = t * render3d_st.cam_aspect
- render3d_st.sc_cast_k = Math.sqrt(1.0 + (t * t + ta * ta)) * 1.1
+ let ta = t * cam_aspect
+ sc_cast_k = Math.sqrt(1.0 + (t * t + ta * ta)) * 1.1
}
- let c = render3d_st.sh_cascade
- var near = render3d_st.cam_near
+ let c = sh_cascade
+ var near = cam_near
# sunShadow cross-fades into this cascade from 0.85 of the previous one's split (lighting.glsl), so
# its receivers start there, not at the split: starting at the split changed 19 pixels in town
- if c > 0 { near = render3d_st.sh_split[c - 1] * 0.85 }
- let far = render3d_st.sh_split[c]
+ if c > 0 { near = sh_split[c - 1] * 0.85 }
+ let far = sh_split[c]
let reach = Math.max(Math.min(height * 6.0, 40.0), 8.0)
- if dmax != 0.0 and dmax + reach + render3d_st.sc_cast_dy < near { return false }
- if dmin - reach > far * render3d_st.sc_cast_k { return false }
+ if dmax != 0.0 and dmax + reach + sc_cast_dy < near { return false }
+ if dmin - reach > far * sc_cast_k { return false }
return true
}
@@ -954,244 +1001,247 @@ function cast_band_reaches(render3d_st: mut Render3dState, dmin: float, dmax: fl
# partition out of l.buf. A layer with no impostor (the tree crowns' branch cards) has
# no cheap stand-in to cast from, so without this its shadow simply began at the near
# distance — which is the crown shadow that appeared as you walked up to a tree.
-function layer_draw_near(render3d_st: mut Render3dState, l: Layer, shadow: bool, light_vp: floats, full: bool) -> void {
+function layer_draw_near(l: Layer, shadow: bool, light_vp: floats, full: bool) -> void {
if l.n_lods > 1 and l.g_on and not shadow {
# one draw per material covering all of its levels (the merged meshes)
- render3d_st.sc_ind_base = 0; render3d_st.sc_ind_n = l.n_lods
- layer_draw_model(render3d_st, l, l.g_model, l.g_dst, 1, false, shadow, light_vp)
- render3d_st.sc_ind_base = -1; render3d_st.sc_ind_n = 1
+ sc_ind_base = 0; sc_ind_n = l.n_lods
+ layer_draw_model(l, l.g_model, l.g_dst, 1, false, shadow, light_vp)
+ sc_ind_base = -1; sc_ind_n = 1
return
}
if l.n_lods > 1 {
# a LOD chain: every level from its own bucket (casters are what is drawn), and a caster level
# only into the cascades it can put a shadow in
for k in 0 .. l.n_lods {
- if shadow and not layer_level_casts_here(render3d_st, l, k) { continue }
- render3d_st.sc_dbg_level = k; layer_draw_model(render3d_st, l, l.lods[k], l.lod_buf[k], l.n_lod[k], l.lod_card[k] == 1, shadow, light_vp)
+ if shadow and not layer_level_casts_here(l, k) { continue }
+ sc_dbg_level = k; layer_draw_model(l, l.lods[k], l.lod_buf[k], l.n_lod[k], l.lod_card[k] == 1, shadow, light_vp)
}
- render3d_st.sc_dbg_level = -1
+ sc_dbg_level = -1
return
}
var vb = l.buf
var cnt = l.n_near
if full and not l.streamed { vb = l.sh_buf; cnt = l.n_sh }
- layer_draw_model(render3d_st, l, l.model, vb, cnt, l.card, shadow, light_vp)
+ layer_draw_model(l, l.model, vb, cnt, l.card, shadow, light_vp)
}
# draw `cnt` instances of `model` out of instance buffer `vb`, as a mesh or as the layer's card
-function layer_draw_model(render3d_st: mut Render3dState, l: Layer, model: Model, vb: int, cnt: int, card: bool, shadow: bool, light_vp: floats) -> void {
+function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool, shadow: bool, light_vp: floats) -> void {
if cnt == 0 { return }
- let p = layer_program(render3d_st, l, shadow, card)
- gpu_use_program(render3d_st, p)
+ let p = layer_program(l, shadow, card)
+ gpu_use_program(p)
# over the prepass: only the fragment the prepass kept, at exactly its depth (a texel
# it cut would otherwise pass LEQUAL over the terrain behind and draw the quad solid)
- if p == render3d_st.sc_prog_fol_eq { gpu_depth_func(render3d_st, GL_EQUAL) }
+ if p == sc_prog_fol_eq { gpu_depth_func(GL_EQUAL) }
var ground = 0.0
if l.grounded { ground = 1.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ground"), ground)
- if l.grounded { terrain_bind_height(render3d_st, p) }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_time"), render3d_st.r3d_time)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wind"), l.wind)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_flutter"), l.flutter)
+ u_f(gpu_uniform(p, "u_ground"), ground)
+ if l.grounded { terrain_bind_height(p) }
+ u_f(gpu_uniform(p, "u_time"), r3d_time)
+ u_f(gpu_uniform(p, "u_wind"), l.wind)
+ u_f(gpu_uniform(p, "u_flutter"), l.flutter)
var mh = 0.0
if not card and l.foliage { mh = model.height }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_model_h"), mh)
- if not card and l.foliage and not shadow and render3d_st.sc_a2c { gpu_alpha_to_coverage(render3d_st, true) }
+ u_f(gpu_uniform(p, "u_model_h"), mh)
+ if not card and l.foliage and not shadow and sc_a2c { gpu_alpha_to_coverage(true) }
if card {
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_card_w"), l.atlas.radius * 2.0); u_f(render3d_st, gpu_uniform(render3d_st, p, "u_card_h"), l.atlas.height)
- r3d_bind_2d(render3d_st, p, "u_diff", 0, l.atlas.albedo)
- r3d_bind_2d(render3d_st, p, "u_nrm", 1, l.atlas.normal)
- gpu_alpha_to_coverage(render3d_st, true)
+ u_f(gpu_uniform(p, "u_card_w"), l.atlas.radius * 2.0); u_f(gpu_uniform(p, "u_card_h"), l.atlas.height)
+ r3d_bind_2d(p, "u_diff", 0, l.atlas.albedo)
+ r3d_bind_2d(p, "u_nrm", 1, l.atlas.normal)
+ gpu_alpha_to_coverage(true)
}
- if shadow { u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_light_vp"), light_vp) }
+ if shadow { u_mat4(gpu_uniform(p, "u_light_vp"), light_vp) }
else {
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), l.tint)
- if render3d_st.sc_dbg_lod and render3d_st.sc_dbg_level >= 0 {
- if render3d_st.sc_dbg_tint == null { render3d_st.sc_dbg_tint = v3_new(1.0, 1.0, 1.0) }
- let k = render3d_st.sc_dbg_level
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_v3(gpu_uniform(p, "u_tint"), l.tint)
+ if sc_dbg_lod and sc_dbg_level >= 0 {
+ if sc_dbg_tint == null { sc_dbg_tint = v3_new(1.0, 1.0, 1.0) }
+ let k = sc_dbg_level
var r = 0.0; var g = 0.0; var b = 0.0
if k == 0 { r = 3.0 } else if k == 1 { g = 3.0 } else if k == 2 { b = 3.0 } else { r = 3.0; g = 3.0 }
- v3_set(render3d_st.sc_dbg_tint, r, g, b)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), render3d_st.sc_dbg_tint)
+ v3_set(sc_dbg_tint, r, g, b)
+ u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint)
}
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_rough_scale"), l.rough)
- if l.blade { u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_base"), render3d_st.sc_blade_base); u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tip"), render3d_st.sc_blade_tip) }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_cull"), l.cull)
- sky_bind_lighting(render3d_st, p)
- shadow_bind(render3d_st, p)
- fog_bind(render3d_st, p)
- if l.foliage { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.05) }
+ u_f(gpu_uniform(p, "u_rough_scale"), l.rough)
+ if l.blade { u_v3(gpu_uniform(p, "u_blade_base"), sc_blade_base); u_v3(gpu_uniform(p, "u_blade_tip"), sc_blade_tip) }
+ u_f(gpu_uniform(p, "u_cull"), l.cull)
+ sky_bind_lighting(p)
+ shadow_bind(p)
+ fog_bind(p)
+ if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), 0.05) }
}
- gpu_cull(render3d_st, false)
+ gpu_cull(false)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
- scatter_attach(render3d_st, pr.mesh, vb)
+ scatter_attach(pr.mesh, vb)
if not card {
- r3d_bind_2d(render3d_st, p, "u_diff", 0, pr.diff)
- if not shadow { r3d_bind_2d(render3d_st, p, "u_nrm", 1, pr.nrm); r3d_bind_2d(render3d_st, p, "u_arm", 2, pr.arm) }
+ r3d_bind_2d(p, "u_diff", 0, pr.diff)
+ if not shadow { r3d_bind_2d(p, "u_nrm", 1, pr.nrm); r3d_bind_2d(p, "u_arm", 2, pr.arm) }
}
- if render3d_st.sc_ind_base >= 0 { gpu_draw_mesh_indirect(render3d_st, pr.mesh, l.g_cmds, (render3d_st.sc_ind_base + i * render3d_st.sc_ind_stride) * SC_REC_W, render3d_st.sc_ind_n, 0, 0) }
- else { mesh_draw_instanced(render3d_st, pr.mesh, cnt) }
+ if sc_ind_base >= 0 { gpu_draw_mesh_indirect(pr.mesh, l.g_cmds, (sc_ind_base + i * sc_ind_stride) * SC_REC_W, sc_ind_n, 0, 0) }
+ else { mesh_draw_instanced(pr.mesh, cnt) }
}
- gpu_alpha_to_coverage(render3d_st, false)
- if p == render3d_st.sc_prog_fol_eq { gpu_depth_func(render3d_st, GL_LESS) }
+ gpu_alpha_to_coverage(false)
+ if p == sc_prog_fol_eq { gpu_depth_func(GL_LESS) }
}
-function layer_draw_far(render3d_st: mut Render3dState, l: Layer, shadow: bool, light_vp: floats) -> void {
+function layer_draw_far(l: Layer, shadow: bool, light_vp: floats) -> void {
if l.imp == null or (l.n_far == 0 and not l.g_on) { return }
- var p = render3d_st.sc_imp_prog
- if shadow { p = render3d_st.sc_imp_prog_shadow }
- gpu_use_program(render3d_st, p)
+ var p = sc_imp_prog
+ if shadow { p = sc_imp_prog_shadow }
+ gpu_use_program(p)
let im = l.imp
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), im.radius)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_height"), im.height)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), float(im.tiles))
- r3d_bind_2d(render3d_st, p, "u_atlas_albedo", 0, im.albedo)
+ u_f(gpu_uniform(p, "u_radius"), im.radius)
+ u_f(gpu_uniform(p, "u_height"), im.height)
+ u_f(gpu_uniform(p, "u_tiles"), float(im.tiles))
+ r3d_bind_2d(p, "u_atlas_albedo", 0, im.albedo)
if shadow {
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_light_vp"), light_vp)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_face_dir"), render3d_st.sun_dir)
- if render3d_st.r3d_debug_shadow and not render3d_st.sc_printed { render3d_st.sc_printed = true; print(`imp shadow prog {p} face_dir loc {gpu_uniform(render3d_st, p, "u_face_dir")} sun {fixed(render3d_st.sun_dir[0])} {fixed(render3d_st.sun_dir[1])} {fixed(render3d_st.sun_dir[2])} cam {fixed(render3d_st.cam_pos[0])} {fixed(render3d_st.cam_pos[1])} {fixed(render3d_st.cam_pos[2])} n_far {l.n_far}`) }
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos)
+ u_mat4(gpu_uniform(p, "u_light_vp"), light_vp)
+ u_v3(gpu_uniform(p, "u_face_dir"), sun_dir)
+ if r3d_debug_shadow and not sc_printed { sc_printed = true; print(`imp shadow prog {p} face_dir loc {gpu_uniform(p, "u_face_dir")} sun {fixed(sun_dir[0])} {fixed(sun_dir[1])} {fixed(sun_dir[2])} cam {fixed(cam_pos[0])} {fixed(cam_pos[1])} {fixed(cam_pos[2])} n_far {l.n_far}`) }
+ u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
} else {
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), l.tint)
- if render3d_st.sc_dbg_lod { if render3d_st.sc_dbg_tint == null { render3d_st.sc_dbg_tint = v3_new(1.0, 1.0, 1.0) }; v3_set(render3d_st.sc_dbg_tint, 3.0, 0.0, 3.0); u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), render3d_st.sc_dbg_tint) }
- r3d_bind_2d(render3d_st, p, "u_atlas_normal", 1, im.normal)
- sky_bind_lighting(render3d_st, p)
- shadow_bind(render3d_st, p)
- fog_bind(render3d_st, p)
- if l.foliage { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.05) }
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_v3(gpu_uniform(p, "u_tint"), l.tint)
+ if sc_dbg_lod { if sc_dbg_tint == null { sc_dbg_tint = v3_new(1.0, 1.0, 1.0) }; v3_set(sc_dbg_tint, 3.0, 0.0, 3.0); u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint) }
+ r3d_bind_2d(p, "u_atlas_normal", 1, im.normal)
+ sky_bind_lighting(p)
+ shadow_bind(p)
+ fog_bind(p)
+ if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), 0.05) }
}
- gpu_cull(render3d_st, false)
- if not shadow and render3d_st.sc_a2c { gpu_alpha_to_coverage(render3d_st, true) }
+ gpu_cull(false)
+ if not shadow and sc_a2c { gpu_alpha_to_coverage(true) }
if l.g_on {
- scatter_attach(render3d_st, render3d_st.sc_card, l.g_dst)
- gpu_draw_mesh_indirect(render3d_st, render3d_st.sc_card, l.g_cmds, 16 * SC_REC_W, 1, 0, 0)
+ scatter_attach(sc_card, l.g_dst)
+ gpu_draw_mesh_indirect(sc_card, l.g_cmds, 16 * SC_REC_W, 1, 0, 0)
} else {
- scatter_attach(render3d_st, render3d_st.sc_card, l.imp_buf)
- mesh_draw_instanced(render3d_st, render3d_st.sc_card, l.n_far)
+ scatter_attach(sc_card, l.imp_buf)
+ mesh_draw_instanced(sc_card, l.n_far)
}
- gpu_alpha_to_coverage(render3d_st, false)
+ gpu_alpha_to_coverage(false)
}
# Cast from the impostor card, always, for every instance in the layer. The lit pass may
# swap a scanned mesh in up close; the shadow must not, or a tree's shadow changes shape
# as you approach it. The card is also far the cheaper of the two, which is what pays for
# casting the whole set into all five cascades.
-function layer_draw_shadow(render3d_st: mut Render3dState, l: Layer, light_vp: floats) -> void {
+function layer_draw_shadow(l: Layer, light_vp: floats) -> void {
if l.n_sh == 0 or l.imp == null { return }
- let p = render3d_st.sc_imp_prog_shadow
- gpu_use_program(render3d_st, p)
+ let p = sc_imp_prog_shadow
+ gpu_use_program(p)
let im = l.imp
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), im.radius)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_height"), im.height)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), float(im.tiles))
- r3d_bind_2d(render3d_st, p, "u_atlas_albedo", 0, im.albedo)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_light_vp"), light_vp)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_face_dir"), render3d_st.sun_dir)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos)
- gpu_cull(render3d_st, false)
- scatter_attach(render3d_st, render3d_st.sc_card, l.sh_buf)
- mesh_draw_instanced(render3d_st, render3d_st.sc_card, l.n_sh)
+ u_f(gpu_uniform(p, "u_radius"), im.radius)
+ u_f(gpu_uniform(p, "u_height"), im.height)
+ u_f(gpu_uniform(p, "u_tiles"), float(im.tiles))
+ r3d_bind_2d(p, "u_atlas_albedo", 0, im.albedo)
+ u_mat4(gpu_uniform(p, "u_light_vp"), light_vp)
+ u_v3(gpu_uniform(p, "u_face_dir"), sun_dir)
+ u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
+ gpu_cull(false)
+ scatter_attach(sc_card, l.sh_buf)
+ mesh_draw_instanced(sc_card, l.n_sh)
}
+var sc_skip_blade: bool = false
+var sc_skip_flower: bool = false
+var sc_skip_card: bool = false
# ---- the foliage depth prepass -----------------------------------------------------------
# Exactly the instances and positions layer_draw_model will light (the same LOD buckets,
# the same vertex shader, the same ground and wind), into depth only.
-function layer_draw_depth(render3d_st: mut Render3dState, l: Layer, model: Model, vb: int, cnt: int) -> void {
+function layer_draw_depth(l: Layer, model: Model, vb: int, cnt: int) -> void {
if cnt == 0 or model == null or vb == 0 { return }
- let p = render3d_st.sc_prog_fol_depth
- gpu_use_program(render3d_st, p)
+ let p = sc_prog_fol_depth
+ gpu_use_program(p)
var ground = 0.0
if l.grounded { ground = 1.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ground"), ground)
- if l.grounded { terrain_bind_height(render3d_st, p) }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_time"), render3d_st.r3d_time)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wind"), l.wind)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_flutter"), l.flutter)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_model_h"), model.height)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_clip_y"), render3d_st.r3d_clip_y)
- gpu_cull(render3d_st, false)
+ u_f(gpu_uniform(p, "u_ground"), ground)
+ if l.grounded { terrain_bind_height(p) }
+ u_f(gpu_uniform(p, "u_time"), r3d_time)
+ u_f(gpu_uniform(p, "u_wind"), l.wind)
+ u_f(gpu_uniform(p, "u_flutter"), l.flutter)
+ u_f(gpu_uniform(p, "u_model_h"), model.height)
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_f(gpu_uniform(p, "u_clip_y"), r3d_clip_y)
+ gpu_cull(false)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
- scatter_attach(render3d_st, pr.mesh, vb)
- r3d_bind_2d(render3d_st, p, "u_diff", 0, pr.diff)
- if render3d_st.sc_ind_base >= 0 { gpu_draw_mesh_indirect(render3d_st, pr.mesh, l.g_cmds, (render3d_st.sc_ind_base + i * render3d_st.sc_ind_stride) * SC_REC_W, render3d_st.sc_ind_n, 0, 0) }
- else { mesh_draw_instanced(render3d_st, pr.mesh, cnt) }
+ scatter_attach(pr.mesh, vb)
+ r3d_bind_2d(p, "u_diff", 0, pr.diff)
+ if sc_ind_base >= 0 { gpu_draw_mesh_indirect(pr.mesh, l.g_cmds, (sc_ind_base + i * sc_ind_stride) * SC_REC_W, sc_ind_n, 0, 0) }
+ else { mesh_draw_instanced(pr.mesh, cnt) }
}
}
# every foliage mesh draw the lit pass will make with sc_prog_fol_eq: not blades, not
# flowers, not card levels (those keep their own alpha and draw as before)
-function scatter_draw_depth(render3d_st: mut Render3dState) -> void {
- if render3d_st.sc_prog_fol_depth == 0 { return }
- for i in 0 .. len(render3d_st.sc_layers) {
- let l = render3d_st.sc_layers[i]
+function scatter_draw_depth() -> void {
+ if sc_prog_fol_depth == 0 { return }
+ for i in 0 .. len(sc_layers) {
+ let l = sc_layers[i]
if not l.foliage or l.blade or l.flower { continue }
- if render3d_st.r3d_no_trees and l.imp != null { continue }
- layer_update(render3d_st, l)
+ if r3d_no_trees and l.imp != null { continue }
+ layer_update(l)
if l.n_lods > 1 and l.g_on {
- render3d_st.sc_ind_base = 0; render3d_st.sc_ind_n = l.n_lods
- layer_draw_depth(render3d_st, l, l.g_model, l.g_dst, 1)
- render3d_st.sc_ind_base = -1; render3d_st.sc_ind_n = 1
+ sc_ind_base = 0; sc_ind_n = l.n_lods
+ layer_draw_depth(l, l.g_model, l.g_dst, 1)
+ sc_ind_base = -1; sc_ind_n = 1
} else if l.n_lods > 1 {
# (a tree layer is flagged `card` for its distant level; its mesh levels still count)
- for k in 0 .. l.n_lods { if l.lod_card[k] != 1 { layer_draw_depth(render3d_st, l, l.lods[k], l.lod_buf[k], l.n_lod[k]) } }
+ for k in 0 .. l.n_lods { if l.lod_card[k] != 1 { layer_draw_depth(l, l.lods[k], l.lod_buf[k], l.n_lod[k]) } }
} else if not l.card {
- layer_draw_depth(render3d_st, l, l.model, l.buf, l.n_near)
+ layer_draw_depth(l, l.model, l.buf, l.n_near)
}
}
- gpu_cull(render3d_st, true)
+ gpu_cull(true)
}
-function scatter_draw(render3d_st: mut Render3dState) -> void {
- for i in 0 .. len(render3d_st.sc_layers) {
- let l = render3d_st.sc_layers[i]
- if render3d_st.sc_skip_blade and l.blade { continue }
- if render3d_st.sc_skip_flower and l.flower { continue }
- if render3d_st.sc_skip_card and l.card { continue }
- if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue }
- layer_update(render3d_st, l)
- layer_draw_near(render3d_st, l, false, null, false)
- layer_draw_far(render3d_st, l, false, null)
+function scatter_draw() -> void {
+ for i in 0 .. len(sc_layers) {
+ let l = sc_layers[i]
+ if sc_skip_blade and l.blade { continue }
+ if sc_skip_flower and l.flower { continue }
+ if sc_skip_card and l.card { continue }
+ if r3d_no_trees and l.imp != null and not l.card { continue }
+ layer_update(l)
+ layer_draw_near(l, false, null, false)
+ layer_draw_far(l, false, null)
}
- gpu_cull(render3d_st, true)
+ gpu_cull(true)
}
# Nothing here is keyed off the cascade. Every skip that used to be — ground cover past
# the 250 m cascade, blades past the nearest, the scanned mesh past the second — made a
# whole class of caster vanish at a fixed distance, which is exactly the popping. A layer
# with an impostor now casts its entire instance list from the card in every cascade;
# only layers that have no impostor at all fall back to the mesh.
-function scatter_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void {
- for i in 0 .. len(render3d_st.sc_layers) {
- let l = render3d_st.sc_layers[i]
- if render3d_st.sc_skip_blade and l.blade { continue }
- if render3d_st.sc_skip_card and l.card { continue }
- if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue }
- layer_update(render3d_st, l)
+function scatter_draw_casters(light_vp: floats) -> void {
+ for i in 0 .. len(sc_layers) {
+ let l = sc_layers[i]
+ if sc_skip_blade and l.blade { continue }
+ if sc_skip_card and l.card { continue }
+ if r3d_no_trees and l.imp != null and not l.card { continue }
+ layer_update(l)
if l.imp != null {
- layer_draw_shadow(render3d_st, l, light_vp)
+ layer_draw_shadow(l, light_vp)
# Shadow LOD (the practice in every production engine: a caster uses a low mesh LOD,
# the billboard only far away). The card alone is a side-view silhouette and a
# crown of drooping needle cards is mostly slivers from the side, so the sun, which
# sees the crown from above, cast a trunk line with a few blobs. The near levels
# now also cast their LOD2 mesh, alpha-tested, on top of the card.
if l.n_lods > 2 and l.g_on {
- render3d_st.sc_ind_base = 17; render3d_st.sc_ind_n = 3; render3d_st.sc_ind_stride = 3
- layer_draw_model(render3d_st, l, l.g_model_sh, l.g_dst, 1, false, true, light_vp)
- render3d_st.sc_ind_base = -1; render3d_st.sc_ind_n = 1; render3d_st.sc_ind_stride = 4
+ sc_ind_base = 17; sc_ind_n = 3; sc_ind_stride = 3
+ layer_draw_model(l, l.g_model_sh, l.g_dst, 1, false, true, light_vp)
+ sc_ind_base = -1; sc_ind_n = 1; sc_ind_stride = 4
} else if l.n_lods > 2 {
- for k in 0 .. 3 { layer_draw_model(render3d_st, l, l.lods[2], l.lod_buf[k], l.n_lod[k], false, true, light_vp) }
+ for k in 0 .. 3 { layer_draw_model(l, l.lods[2], l.lod_buf[k], l.n_lod[k], false, true, light_vp) }
}
}
- else { layer_draw_near(render3d_st, l, true, light_vp, true) }
+ else { layer_draw_near(l, true, light_vp, true) }
}
- prof_cpu_mark(render3d_st, "shadow scatter")
+ prof_cpu_mark("shadow scatter")
}
@@ -1201,38 +1251,39 @@ function scatter_draw_casters(render3d_st: mut Render3dState, light_vp: floats)
# gaps as the near cover instead of a lawn scan (the far-field trick the big open
# worlds use: geometry up close, an authored ground texture that matches it beyond).
# Returns an RGBA8 texture (alpha = coverage), repeat-wrapped and mipmapped.
-function cb_rnd(render3d_st: mut Render3dState) -> float {
- render3d_st.cb_state = (render3d_st.cb_state * 1103515245 + 12345) & 0x7FFFFFFF
- return float((render3d_st.cb_state >> 8) & 0xFFFF) / 65536.0
+var cb_state: int = 12345
+function cb_rnd() -> float {
+ cb_state = (cb_state * 1103515245 + 12345) & 0x7FFFFFFF
+ return float((cb_state >> 8) & 0xFFFF) / 65536.0
}
-function carpet_bake(render3d_st: mut Render3dState, layers: []Layer, count: int, tile: float, res: int) -> int {
- let tex = tex_target(render3d_st, res, res, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
- let fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, fbo)
- gpu_fb_color(render3d_st, 0, tex)
- let rb = gpu_rb_new(render3d_st)
- gpu_rb_storage(render3d_st, rb, GL_DEPTH_COMPONENT24, res, res, 0)
- gpu_fb_depth_rb(render3d_st, rb)
- gpu_fb_draw_buffers(render3d_st, 1)
- gpu_viewport(render3d_st, 0, 0, res, res)
- gpu_clear_color(render3d_st, 0.0, 0.0, 0.0, 0.0)
- gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- gpu_cull(render3d_st, false)
- gpu_blend(render3d_st, false)
+function carpet_bake(layers: []Layer, count: int, tile: float, res: int) -> int {
+ let tex = tex_target(res, res, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
+ let fbo = gpu_fb_new()
+ gpu_fb_bind(fbo)
+ gpu_fb_color(0, tex)
+ let rb = gpu_rb_new()
+ gpu_rb_storage(rb, GL_DEPTH_COMPONENT24, res, res, 0)
+ gpu_fb_depth_rb(rb)
+ gpu_fb_draw_buffers(1)
+ gpu_viewport(0, 0, res, res)
+ gpu_clear_color(0.0, 0.0, 0.0, 0.0)
+ gpu_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ gpu_cull(false)
+ gpu_blend(false)
# the clumps, and eight wrapped copies so the tile's edges continue
let half = tile * 0.5
let n9 = count * 9
let inst = gl_floats(n9 * INST_FLOATS)
- render3d_st.cb_state = 977
+ cb_state = 977
var k = 0
for i in 0 .. count {
- let x = cb_rnd(render3d_st) * tile - half
- let z = cb_rnd(render3d_st) * tile - half
- let sc = 1.5 + cb_rnd(render3d_st)
- let yaw = cb_rnd(render3d_st) * (2.0 * PI)
- let sd = cb_rnd(render3d_st)
+ let x = cb_rnd() * tile - half
+ let z = cb_rnd() * tile - half
+ let sc = 1.5 + cb_rnd()
+ let yaw = cb_rnd() * (2.0 * PI)
+ let sd = cb_rnd()
for oz in 0 .. 3 {
for ox in 0 .. 3 {
let px = x + float(ox - 1) * tile
@@ -1243,45 +1294,45 @@ function carpet_bake(render3d_st: mut Render3dState, layers: []Layer, count: int
}
}
}
- let buf = gpu_buffer_new(render3d_st)
- gpu_buffer_upload(render3d_st, buf, gl_bytes_of(n9 * INST_FLOATS), inst, GPU_STATIC)
+ let buf = gpu_buffer_new()
+ gpu_buffer_upload(buf, gl_bytes_of(n9 * INST_FLOATS), inst, GPU_STATIC)
free(inst)
# straight down: the window is exactly one tile
let view = m4_new(); let proj = m4_new()
let eye = v3_new(0.0, 6.0, 0.0); let at = v3_new(0.0, 0.0, 0.0); let up = v3_new(0.0, 0.0, -1.0)
m4_look_at(view, eye, at, up)
m4_ortho(proj, -half, half, -half, half, 0.1, 12.0)
- let bake = render3d_st.sc_bake_card_prog
- gpu_use_program(render3d_st, bake)
- u_mat4(render3d_st, gpu_uniform(render3d_st, bake, "u_view"), view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, bake, "u_proj"), proj)
- u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_wind"), 0.0)
- u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_flutter"), 0.0)
- u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_time"), 0.0)
+ let bake = sc_bake_card_prog
+ gpu_use_program(bake)
+ u_mat4(gpu_uniform(bake, "u_view"), view)
+ u_mat4(gpu_uniform(bake, "u_proj"), proj)
+ u_f(gpu_uniform(bake, "u_wind"), 0.0)
+ u_f(gpu_uniform(bake, "u_flutter"), 0.0)
+ u_f(gpu_uniform(bake, "u_time"), 0.0)
for li in 0 .. len(layers) {
let l = layers[li]
if l.atlas == null { continue }
- u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_card_w"), l.atlas.radius * 2.0); u_f(render3d_st, gpu_uniform(render3d_st, bake, "u_card_h"), l.atlas.height)
- r3d_bind_2d(render3d_st, bake, "u_diff", 0, l.atlas.albedo)
- r3d_bind_2d(render3d_st, bake, "u_arm", 2, l.atlas.normal)
+ u_f(gpu_uniform(bake, "u_card_w"), l.atlas.radius * 2.0); u_f(gpu_uniform(bake, "u_card_h"), l.atlas.height)
+ r3d_bind_2d(bake, "u_diff", 0, l.atlas.albedo)
+ r3d_bind_2d(bake, "u_arm", 2, l.atlas.normal)
for i in 0 .. len(l.model.prims) {
let pr = l.model.prims[i]
- scatter_attach(render3d_st, pr.mesh, buf)
- mesh_draw_instanced(render3d_st, pr.mesh, n9)
+ scatter_attach(pr.mesh, buf)
+ mesh_draw_instanced(pr.mesh, n9)
}
}
free(view); free(proj); free(eye); free(at); free(up)
- gpu_fb_bind(render3d_st, 0)
- gpu_fb_free(render3d_st, fbo)
- gpu_rb_free(render3d_st, rb)
- gpu_buffer_free(render3d_st, buf)
- gpu_tex_bind(render3d_st, GPU_TEX2D, tex)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
- gpu_check(render3d_st, "carpet bake")
+ gpu_fb_bind(0)
+ gpu_fb_free(fbo)
+ gpu_rb_free(rb)
+ gpu_buffer_free(buf)
+ gpu_tex_bind(GPU_TEX2D, tex)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_paramf(GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, tex_anisotropy)
+ gpu_tex_mips(GPU_TEX2D)
+ gpu_check("carpet bake")
return tex
}
@@ -1290,15 +1341,15 @@ function carpet_bake(render3d_st: mut Render3dState, layers: []Layer, count: int
# crossed-card mesh and baked atlas - and every stream feeding them. The models a layer drew
# belong to whoever loaded them and are kept, and so are the programs. A game rebuilding its
# world calls this, then places the new map's layers exactly as it did at boot.
-function scatter_clear_all(render3d_st: mut Render3dState) -> void {
- stream_clear_all(render3d_st)
- if render3d_st.sc_layers == null { return }
- for i in 0 .. len(render3d_st.sc_layers) {
- let l = render3d_st.sc_layers[i]
- if l.buf != 0 { gpu_buffer_free(render3d_st, l.buf) }
- if l.imp_buf != 0 { gpu_buffer_free(render3d_st, l.imp_buf) }
- if l.sh_buf != 0 { gpu_buffer_free(render3d_st, l.sh_buf) }
- if l.lod_buf != null { for k in 0 .. l.n_lods { gpu_buffer_free(render3d_st, l.lod_buf[k]) } }
+function scatter_clear_all() -> void {
+ stream_clear_all()
+ if sc_layers == null { return }
+ for i in 0 .. len(sc_layers) {
+ let l = sc_layers[i]
+ if l.buf != 0 { gpu_buffer_free(l.buf) }
+ if l.imp_buf != 0 { gpu_buffer_free(l.imp_buf) }
+ if l.sh_buf != 0 { gpu_buffer_free(l.sh_buf) }
+ if l.lod_buf != null { for k in 0 .. l.n_lods { gpu_buffer_free(l.lod_buf[k]) } }
if l.inst != null { free(l.inst) }
if l.scratch != null { free(l.scratch) }
if l.tint != null { free(l.tint) }
@@ -1310,14 +1361,14 @@ function scatter_clear_all(render3d_st: mut Render3dState) -> void {
if l.vis != null { free(l.vis) }
if l.lod_dist != null { free(l.lod_dist); free(l.lod_card); free(l.lod_buf); free(l.n_lod) }
if l.lvl != null { free(l.lvl) }
- if l.g_arena != null { for k in 0 .. len(l.g_arena) { mesh_free(render3d_st, l.g_arena[k].mesh) } }
+ if l.g_arena != null { for k in 0 .. len(l.g_arena) { mesh_free(l.g_arena[k].mesh) } }
# layer_cards built this layer's crossed card and its atlas itself
- if l.card and l.n_lods == 0 and l.model != null { for k in 0 .. len(l.model.prims) { mesh_free(render3d_st, l.model.prims[k].mesh) } }
+ if l.card and l.n_lods == 0 and l.model != null { for k in 0 .. len(l.model.prims) { mesh_free(l.model.prims[k].mesh) } }
if l.card and l.atlas != null {
- gpu_tex_free(render3d_st, l.atlas.albedo)
- gpu_tex_free(render3d_st, l.atlas.normal)
+ gpu_tex_free(l.atlas.albedo)
+ gpu_tex_free(l.atlas.normal)
}
}
- render3d_st.sc_layers = new []Layer
- render3d_st.sc_view_gen += 1
+ sc_layers = new []Layer
+ sc_view_gen += 1
}
diff --git a/packages/ludic.render3d/shadow.ludic b/packages/ludic.render3d/shadow.ludic
index d9b60358..63659800 100644
--- a/packages/ludic.render3d/shadow.ludic
+++ b/packages/ludic.render3d/shadow.ludic
@@ -5,80 +5,95 @@
# ============================================================================
# the size of each cascade's depth layer; shadow_set_res changes it at run time
+var shadow_res: int = 2048
+var shadow_refused: int = 0 # the last size the graphics card had no memory for (0: none)
const SHADOW_CASCADES: int = 5
+var sh_tex: int = 0
+var sh_fbo: int = 0
+var sh_vp: floats = null # the cascades' view-projections, 16 floats each
+var sh_vp_v: [][]float = null # a view of each
+var sh_split: floats = null # view-space far distance of each cascade
+var sh_range: floats = null # 4 light-frustum depth extents (metres)
+var sh_texel: floats = null # 4 shadow texel sizes (metres)
+var sh_tmp_proj: floats = null
+var sh_tmp_vp: floats = null
+var sh_tmp_inv: floats = null
+var sh_tmp_view: floats = null
+var sh_corner: floats = null
+var sh_cascade: int = 0 # the cascade being rendered (for casters that skip far ones)
-function shadow_init(render3d_st: mut Render3dState) -> void {
- shadow_make_tex(render3d_st)
- render3d_st.sh_fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, render3d_st.sh_fbo)
- gpu_fb_no_color(render3d_st)
- gpu_fb_bind(render3d_st, 0)
- render3d_st.sh_vp = floats(16 * SHADOW_CASCADES)
- render3d_st.sh_vp_v = m4_views(render3d_st.sh_vp, SHADOW_CASCADES)
- render3d_st.sh_split = floats(SHADOW_CASCADES)
- render3d_st.sh_range = floats(SHADOW_CASCADES)
- render3d_st.sh_texel = floats(SHADOW_CASCADES)
+function shadow_init() -> void {
+ shadow_make_tex()
+ sh_fbo = gpu_fb_new()
+ gpu_fb_bind(sh_fbo)
+ gpu_fb_no_color()
+ gpu_fb_bind(0)
+ sh_vp = floats(16 * SHADOW_CASCADES)
+ sh_vp_v = m4_views(sh_vp, SHADOW_CASCADES)
+ sh_split = floats(SHADOW_CASCADES)
+ sh_range = floats(SHADOW_CASCADES)
+ sh_texel = floats(SHADOW_CASCADES)
# the fourth slice keeps a tree-sized texel out to a kilometre; only the massif uses the last
- render3d_st.sh_split[0] = 16.0; render3d_st.sh_split[1] = 60.0; render3d_st.sh_split[2] = 250.0; render3d_st.sh_split[3] = 1100.0; render3d_st.sh_split[4] = 6000.0
- render3d_st.sh_tmp_proj = m4_new(); render3d_st.sh_tmp_vp = m4_new(); render3d_st.sh_tmp_inv = m4_new(); render3d_st.sh_tmp_view = m4_new()
- render3d_st.sh_corner = floats(3)
+ sh_split[0] = 16.0; sh_split[1] = 60.0; sh_split[2] = 250.0; sh_split[3] = 1100.0; sh_split[4] = 6000.0
+ sh_tmp_proj = m4_new(); sh_tmp_vp = m4_new(); sh_tmp_inv = m4_new(); sh_tmp_view = m4_new()
+ sh_corner = floats(3)
}
# A shadow resolution setting: 1024, 2048 or 4096 per cascade. The depth layers are made again at
# the new size; the pass attaches a layer per cascade every frame, and the lighting reads the
# texel size from the map itself, so nothing else has to follow.
-function shadow_set_res(render3d_st: mut Render3dState, r: int) -> void {
- if r < 256 or r == render3d_st.shadow_res { return }
- let was = render3d_st.shadow_res
- render3d_st.shadow_res = r
- if render3d_st.sh_tex == 0 { return }
- gpu_tex_free(render3d_st, render3d_st.sh_tex)
- shadow_make_tex(render3d_st)
+function shadow_set_res(r: int) -> void {
+ if r < 256 or r == shadow_res { return }
+ let was = shadow_res
+ shadow_res = r
+ if sh_tex == 0 { return }
+ gpu_tex_free(sh_tex)
+ shadow_make_tex()
# not enough video memory for that size: go back to the one that worked, and smaller again if even
# that is refused now, rather than ending with no shadow map at all
- if not gpu_tex_ok(render3d_st, render3d_st.sh_tex) {
- render3d_st.shadow_refused = r
+ if not gpu_tex_ok(sh_tex) {
+ shadow_refused = r
print(`r3d: shadows: no memory for {r} x {r} cascades; keeping {was}`)
var size = was
- render3d_st.shadow_res = size
- gpu_tex_free(render3d_st, render3d_st.sh_tex)
- shadow_make_tex(render3d_st)
- while not gpu_tex_ok(render3d_st, render3d_st.sh_tex) and size > 512 {
+ shadow_res = size
+ gpu_tex_free(sh_tex)
+ shadow_make_tex()
+ while not gpu_tex_ok(sh_tex) and size > 512 {
size = size / 2
- render3d_st.shadow_res = size
- gpu_tex_free(render3d_st, render3d_st.sh_tex)
- shadow_make_tex(render3d_st)
+ shadow_res = size
+ gpu_tex_free(sh_tex)
+ shadow_make_tex()
}
}
}
-function shadow_make_tex(render3d_st: mut Render3dState) -> void {
- render3d_st.sh_tex = gpu_tex_new(render3d_st)
- gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sh_tex)
- gpu_tex_image3d(render3d_st, GL_DEPTH_COMPONENT32F, render3d_st.shadow_res, render3d_st.shadow_res, SHADOW_CASCADES, GL_DEPTH_COMPONENT, GL_FLOAT, null)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL)
+function shadow_make_tex() -> void {
+ sh_tex = gpu_tex_new()
+ gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex)
+ gpu_tex_image3d(GL_DEPTH_COMPONENT32F, shadow_res, shadow_res, SHADOW_CASCADES, GL_DEPTH_COMPONENT, GL_FLOAT, null)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL)
let border = gl_floats(4)
gl_put(border, 0, 1.0); gl_put(border, 1, 1.0); gl_put(border, 2, 1.0); gl_put(border, 3, 1.0)
- gpu_tex_border(render3d_st, GPU_TEX2D_ARRAY, border)
+ gpu_tex_border(GPU_TEX2D_ARRAY, border)
free(border)
}
# light view-projection for the camera-frustum slice [near, far]
-function shadow_fit(render3d_st: mut Render3dState, c: int, near: float, far: float) -> void {
+function shadow_fit(c: int, near: float, far: float) -> void {
# Fit the slice in VIEW space, not world space. The bounding sphere of a frustum
# slice depends only on near/far/fov/aspect — never on where the camera is pointing —
# so computing it here makes the radius a constant per cascade. Doing it in world
# space (as this did) let the radius wobble as the camera turned, which changed the
# texel size, which moved the grid the projection is snapped to, so the whole shadow
# map resampled every frame: that is the crawl and flicker seen while moving.
- m4_perspective(render3d_st.sh_tmp_proj, render3d_st.cam_fov, render3d_st.cam_aspect, near, far)
- m4_inverse(render3d_st.sh_tmp_inv, render3d_st.sh_tmp_proj) # NDC -> view space
+ m4_perspective(sh_tmp_proj, cam_fov, cam_aspect, near, far)
+ m4_inverse(sh_tmp_inv, sh_tmp_proj) # NDC -> view space
let cview = v3_new(0.0, 0.0, 0.0)
let corners = floats(24)
for i in 0 .. 8 {
@@ -86,123 +101,131 @@ function shadow_fit(render3d_st: mut Render3dState, c: int, near: float, far: fl
if (i & 1) != 0 { x = 1.0 }
if (i & 2) != 0 { y = 1.0 }
if (i & 4) != 0 { z = 1.0 }
- let w = m4_xform_point(render3d_st.sh_corner, render3d_st.sh_tmp_inv, x, y, z)
+ let w = m4_xform_point(sh_corner, sh_tmp_inv, x, y, z)
let iw = 1.0 / w
- corners[i * 3] = render3d_st.sh_corner[0] * iw; corners[i * 3 + 1] = render3d_st.sh_corner[1] * iw; corners[i * 3 + 2] = render3d_st.sh_corner[2] * iw
+ corners[i * 3] = sh_corner[0] * iw; corners[i * 3 + 1] = sh_corner[1] * iw; corners[i * 3 + 2] = sh_corner[2] * iw
cview[0] = cview[0] + corners[i * 3]; cview[1] = cview[1] + corners[i * 3 + 1]; cview[2] = cview[2] + corners[i * 3 + 2]
}
v3_scale(cview, cview, 1.0 / 8.0)
var radius = 0.0
for i in 0 .. 8 {
- v3_set(render3d_st.sh_corner, corners[i * 3], corners[i * 3 + 1], corners[i * 3 + 2])
- let d = v3_dist(render3d_st.sh_corner, cview)
+ v3_set(sh_corner, corners[i * 3], corners[i * 3 + 1], corners[i * 3 + 2])
+ let d = v3_dist(sh_corner, cview)
if d > radius { radius = d }
}
radius = radius * 1.05
# the slice centre back into world space
- m4_inverse(render3d_st.sh_tmp_vp, render3d_st.cam_view)
+ m4_inverse(sh_tmp_vp, cam_view)
let center = floats(3)
- m4_xform_point(center, render3d_st.sh_tmp_vp, cview[0], cview[1], cview[2])
+ m4_xform_point(center, sh_tmp_vp, cview[0], cview[1], cview[2])
free(cview)
# light view: from far along the sun direction, looking at the centre
let eye = floats(3)
# casters up to ~900 m toward the sun (a mountain across the valley), and the
# slice itself behind the centre: a tight depth range keeps the bias small
let back = radius + 900.0
- v3_madd(eye, center, render3d_st.sun_dir, back)
+ v3_madd(eye, center, sun_dir, back)
let up = v3_new(0.0, 1.0, 0.0)
- m4_look_at(render3d_st.sh_tmp_view, eye, center, up)
+ m4_look_at(sh_tmp_view, eye, center, up)
# snap the ortho window to the shadow texel grid
- let texel = radius * 2.0 / float(render3d_st.shadow_res)
- m4_xform_point(render3d_st.sh_corner, render3d_st.sh_tmp_view, center[0], center[1], center[2])
- let ox = Math.floor(render3d_st.sh_corner[0] / texel) * texel - render3d_st.sh_corner[0]
- let oy = Math.floor(render3d_st.sh_corner[1] / texel) * texel - render3d_st.sh_corner[1]
+ let texel = radius * 2.0 / float(shadow_res)
+ m4_xform_point(sh_corner, sh_tmp_view, center[0], center[1], center[2])
+ let ox = Math.floor(sh_corner[0] / texel) * texel - sh_corner[0]
+ let oy = Math.floor(sh_corner[1] / texel) * texel - sh_corner[1]
let nr = -radius
let zfar = back + radius + 100.0
- m4_ortho(render3d_st.sh_tmp_proj, nr + ox, radius + ox, nr + oy, radius + oy, 1.0, zfar)
- render3d_st.sh_range[c] = zfar - 1.0
- render3d_st.sh_texel[c] = texel
+ m4_ortho(sh_tmp_proj, nr + ox, radius + ox, nr + oy, radius + oy, 1.0, zfar)
+ sh_range[c] = zfar - 1.0
+ sh_texel[c] = texel
let out = floats(16)
- m4_mul(out, render3d_st.sh_tmp_proj, render3d_st.sh_tmp_view)
- for i in 0 .. 16 { render3d_st.sh_vp[c * 16 + i] = out[i] }
+ m4_mul(out, sh_tmp_proj, sh_tmp_view)
+ for i in 0 .. 16 { sh_vp[c * 16 + i] = out[i] }
free(out); free(eye); free(up); free(center); free(corners)
}
-function shadow_cascade_vp(render3d_st: Render3dState, c: int) -> floats { return render3d_st.sh_vp_v[c] }
+function shadow_cascade_vp(c: int) -> floats { return sh_vp_v[c] }
# render every cascade; `draw` happens through terrain_draw_shadow + the scene's casters
-function shadow_pass(render3d_st: mut Render3dState) -> void {
- var near = render3d_st.cam_near
- gpu_fb_bind(render3d_st, render3d_st.sh_fbo)
- gpu_viewport(render3d_st, 0, 0, render3d_st.shadow_res, render3d_st.shadow_res)
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- gpu_depth_bias(render3d_st, 2.0, 4.0)
- gpu_cull(render3d_st, false)
+function shadow_pass() -> void {
+ var near = cam_near
+ gpu_fb_bind(sh_fbo)
+ gpu_viewport(0, 0, shadow_res, shadow_res)
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ gpu_depth_bias(2.0, 4.0)
+ gpu_cull(false)
for c in 0 .. SHADOW_CASCADES {
- render3d_st.sh_cascade = c
- shadow_fit(render3d_st, c, near, render3d_st.sh_split[c])
- prof_cpu_mark(render3d_st, "shadow fit")
- near = render3d_st.sh_split[c]
- gpu_fb_depth_layer(render3d_st, render3d_st.sh_tex, c)
- if render3d_st.r3d_debug and c == 0 { let st = gpu_fb_status(render3d_st); print(`shadow fbo status {st}`) }
- gpu_clear(render3d_st, GL_DEPTH_BUFFER_BIT)
- let vp = shadow_cascade_vp(render3d_st, c)
+ sh_cascade = c
+ shadow_fit(c, near, sh_split[c])
+ prof_cpu_mark("shadow fit")
+ near = sh_split[c]
+ gpu_fb_depth_layer(sh_tex, c)
+ if r3d_debug and c == 0 { let st = gpu_fb_status(); print(`shadow fbo status {st}`) }
+ gpu_clear(GL_DEPTH_BUFFER_BIT)
+ let vp = shadow_cascade_vp(c)
# shadows off (a video setting): the cascades stay cleared, so everything reads lit
- if render3d_st.sh_enabled {
- if not render3d_st.sh_skip_terrain { terrain_draw_shadow(vp) }
- r3d_scene_casters(render3d_st, vp)
+ if sh_enabled {
+ if not sh_skip_terrain { terrain_draw_shadow(vp) }
+ r3d_scene_casters(vp)
}
}
- gpu_depth_bias(render3d_st, 0.0, 0.0)
- gpu_fb_bind(render3d_st, 0)
- if render3d_st.r3d_debug_shadow { shadow_dump(render3d_st) }
- if render3d_st.r3d_debug_shadow and not render3d_st.sh_printed2 {
- render3d_st.sh_printed2 = true
+ gpu_depth_bias(0.0, 0.0)
+ gpu_fb_bind(0)
+ if r3d_debug_shadow { shadow_dump() }
+ if r3d_debug_shadow and not sh_printed2 {
+ sh_printed2 = true
let q = floats(3)
- if render3d_st.sh_probe_x != 0.0 {
- let vp = shadow_cascade_vp(render3d_st, 2)
- m4_xform_point(q, vp, render3d_st.sh_probe_x, render3d_st.sh_probe_y, render3d_st.sh_probe_z)
+ if sh_probe_x != 0.0 {
+ let vp = shadow_cascade_vp(2)
+ m4_xform_point(q, vp, sh_probe_x, sh_probe_y, sh_probe_z)
print(`probe base ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])}`)
- m4_xform_point(q, vp, render3d_st.sh_probe_x, render3d_st.sh_probe_y + 15.0, render3d_st.sh_probe_z)
- print(`probe top ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} -> map texel {int((q[0] * 0.5 + 0.5) * float(render3d_st.shadow_res))} {int((q[1] * 0.5 + 0.5) * float(render3d_st.shadow_res))}`)
+ m4_xform_point(q, vp, sh_probe_x, sh_probe_y + 15.0, sh_probe_z)
+ print(`probe top ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} -> map texel {int((q[0] * 0.5 + 0.5) * float(shadow_res))} {int((q[1] * 0.5 + 0.5) * float(shadow_res))}`)
# where the top's shadow lands on the ground: walk down the sun ray
- let gx = render3d_st.sh_probe_x + 0.0 - render3d_st.sun_dir[0] * (15.0 / render3d_st.sun_dir[1])
- let gz = render3d_st.sh_probe_z - render3d_st.sun_dir[2] * (15.0 / render3d_st.sun_dir[1])
- m4_xform_point(q, vp, gx, terrain_height(render3d_st, gx, gz), gz)
+ let gx = sh_probe_x + 0.0 - sun_dir[0] * (15.0 / sun_dir[1])
+ let gz = sh_probe_z - sun_dir[2] * (15.0 / sun_dir[1])
+ m4_xform_point(q, vp, gx, terrain_height(gx, gz), gz)
print(`shadow-of-top ground ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} at {fixed(gx)} {fixed(gz)}`)
}
for c in 0 .. SHADOW_CASCADES {
- let vp = shadow_cascade_vp(render3d_st, c)
+ let vp = shadow_cascade_vp(c)
# a point 5 m ahead of the camera on the ground
- let px = render3d_st.cam_pos[0] + render3d_st.cam_fwd[0] * 5.0; let pz = render3d_st.cam_pos[2] + render3d_st.cam_fwd[2] * 5.0
- let w = m4_xform_point(q, vp, px, terrain_height(render3d_st, px, pz), pz)
+ let px = cam_pos[0] + cam_fwd[0] * 5.0; let pz = cam_pos[2] + cam_fwd[2] * 5.0
+ let w = m4_xform_point(q, vp, px, terrain_height(px, pz), pz)
print(`cascade {c}: ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} w {fixed(w)} m0 {fixed(vp[0])} m5 {fixed(vp[5])} m14 {fixed(vp[14])}`)
}
free(q)
}
}
+var sh_printed2: bool = false
+var sh_printed3: bool = false
+var sh_enabled: bool = true
+var sh_force: int = -1 # R3D_FORCE= pins every pixel to cascade c (debug)
+var sh_skip_terrain: bool = false
+var sh_probe_x: float = 0.0
+var sh_probe_y: float = 0.0
+var sh_probe_z: float = 0.0
# Debug: cascade depths as grey PPMs (build/dbg_shadow_.ppm)
-function shadow_dump(render3d_st: mut Render3dState) -> void {
- let n = render3d_st.shadow_res * render3d_st.shadow_res
+function shadow_dump() -> void {
+ let n = shadow_res * shadow_res
let buf = floats(n * SHADOW_CASCADES)
- gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sh_tex)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE)
- gpu_tex_read(render3d_st, GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, data_of(buf))
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE)
- if render3d_st.sh_probe_x != 0.0 {
- let vp = shadow_cascade_vp(render3d_st, 2)
+ gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE)
+ gpu_tex_read(GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, data_of(buf))
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE)
+ if sh_probe_x != 0.0 {
+ let vp = shadow_cascade_vp(2)
let q = floats(3)
- m4_xform_point(q, vp, render3d_st.sh_probe_x, render3d_st.sh_probe_y + 12.0, render3d_st.sh_probe_z)
- let tx = int((q[0] * 0.5 + 0.5) * float(render3d_st.shadow_res))
- let ty = int((q[1] * 0.5 + 0.5) * float(render3d_st.shadow_res))
+ m4_xform_point(q, vp, sh_probe_x, sh_probe_y + 12.0, sh_probe_z)
+ let tx = int((q[0] * 0.5 + 0.5) * float(shadow_res))
+ let ty = int((q[1] * 0.5 + 0.5) * float(shadow_res))
let want = q[2] * 0.5 + 0.5
print(`probe (12 m up) texel {tx} {ty} card depth {fixed(want * 1000.0)}/1000`)
for dy in 0 .. 5 {
let yy = ty - 40 + dy * 20
- print(` row {yy}: {fixed(buf[2 * n + yy * render3d_st.shadow_res + tx - 20] * 1000.0)} {fixed(buf[2 * n + yy * render3d_st.shadow_res + tx] * 1000.0)} {fixed(buf[2 * n + yy * render3d_st.shadow_res + tx + 20] * 1000.0)} /1000`)
+ print(` row {yy}: {fixed(buf[2 * n + yy * shadow_res + tx - 20] * 1000.0)} {fixed(buf[2 * n + yy * shadow_res + tx] * 1000.0)} {fixed(buf[2 * n + yy * shadow_res + tx + 20] * 1000.0)} /1000`)
}
free(q)
}
@@ -217,10 +240,10 @@ function shadow_dump(render3d_st: mut Render3dState) -> void {
let f = file_open(`build/dbg_shadow_{c}.ppm`, "wb")
let hdr = `P6\n{sm} {sm}\n255\n`
file_write(f, hdr, len(hdr))
- let st = render3d_st.shadow_res / sm
+ let st = shadow_res / sm
for y in 0 .. sm {
for x in 0 .. sm {
- let d = buf[c * n + (y * st) * render3d_st.shadow_res + x * st]
+ let d = buf[c * n + (y * st) * shadow_res + x * st]
let g = int(Math.clamp((d - lo) / Math.max(hi - lo, 0.0001), 0.0, 1.0) * 255.0)
row[x * 3] = g; row[x * 3 + 1] = g; row[x * 3 + 2] = g
}
@@ -231,31 +254,32 @@ function shadow_dump(render3d_st: mut Render3dState) -> void {
free(buf); free(row)
}
+var sh_printed: bool = false
# a uniform array's location: some drivers only answer to the "[0]" spelling
-function sh_loc(render3d_st: Render3dState, prog: int, name: string) -> int {
- var loc = gpu_uniform(render3d_st, prog, name + "[0]")
- if loc < 0 { loc = gpu_uniform(render3d_st, prog, name) }
+function sh_loc(prog: int, name: string) -> int {
+ var loc = gpu_uniform(prog, name + "[0]")
+ if loc < 0 { loc = gpu_uniform(prog, name) }
return loc
}
-function shadow_bind(render3d_st: mut Render3dState, prog: int) -> void {
- r3d_bind_tex(render3d_st, prog, "u_shadow", 15, GPU_TEX2D_ARRAY, render3d_st.sh_tex)
+function shadow_bind(prog: int) -> void {
+ r3d_bind_tex(prog, "u_shadow", 15, GPU_TEX2D_ARRAY, sh_tex)
# the height-field shadow (terrain.ludic); a stand-in texture keeps the unit valid before the bake
- var ts = render3d_st.ter_shadow_tex
+ var ts = ter_shadow_tex
var ts_on = 1.0
- if ts == 0 { ts = render3d_st.ter_height_tex; ts_on = 0.0 }
- r3d_bind_2d(render3d_st, prog, "u_tershadow", 6, ts)
- terrain_bind_height(render3d_st, prog)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ts_on"), ts_on)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ts_half"), float(render3d_st.TERRAIN_HALF))
- u_f2(render3d_st, gpu_uniform(render3d_st, prog, "u_ts_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
- var loc = gpu_uniform(render3d_st, prog, "u_cascade_vp[0]")
- if loc < 0 { loc = gpu_uniform(render3d_st, prog, "u_cascade_vp") }
- if render3d_st.r3d_debug_shadow and not render3d_st.sh_printed { render3d_st.sh_printed = true; print(`cascade vp loc {loc} / {gpu_uniform(render3d_st, prog, "u_cascade_vp")} split loc {gpu_uniform(render3d_st, prog, "u_cascade_split")} shadow loc {gpu_uniform(render3d_st, prog, "u_shadow")}`) }
- u_mat4n(render3d_st, loc, SHADOW_CASCADES, render3d_st.sh_vp)
- u_fv(render3d_st, sh_loc(render3d_st, prog, "u_cascade_split"), SHADOW_CASCADES, render3d_st.sh_split)
- if render3d_st.r3d_debug_shadow and not render3d_st.sh_printed3 { render3d_st.sh_printed3 = true; print(`range {fixed(render3d_st.sh_range[0])} {fixed(render3d_st.sh_range[1])} {fixed(render3d_st.sh_range[2])} {fixed(render3d_st.sh_range[3])} texel*1000 {fixed(render3d_st.sh_texel[0] * 1000.0)} {fixed(render3d_st.sh_texel[1] * 1000.0)} {fixed(render3d_st.sh_texel[2] * 1000.0)} {fixed(render3d_st.sh_texel[3] * 1000.0)} locs {gpu_uniform(render3d_st, prog, "u_cascade_range")} {gpu_uniform(render3d_st, prog, "u_cascade_texel")}`) }
- u_fv(render3d_st, sh_loc(render3d_st, prog, "u_cascade_range"), SHADOW_CASCADES, render3d_st.sh_range)
- if r3d_env_has(render3d_st, "R3D_FORCE") { render3d_st.sh_force = Text.to_int(r3d_env(render3d_st, "R3D_FORCE")) }
- u_i(render3d_st, gpu_uniform(render3d_st, prog, "u_force_cascade"), render3d_st.sh_force)
- u_fv(render3d_st, sh_loc(render3d_st, prog, "u_cascade_texel"), SHADOW_CASCADES, render3d_st.sh_texel)
+ if ts == 0 { ts = ter_height_tex; ts_on = 0.0 }
+ r3d_bind_2d(prog, "u_tershadow", 6, ts)
+ terrain_bind_height(prog)
+ u_f(gpu_uniform(prog, "u_ts_on"), ts_on)
+ u_f(gpu_uniform(prog, "u_ts_half"), float(TERRAIN_HALF))
+ u_f2(gpu_uniform(prog, "u_ts_origin"), ter_ox, ter_oz)
+ var loc = gpu_uniform(prog, "u_cascade_vp[0]")
+ if loc < 0 { loc = gpu_uniform(prog, "u_cascade_vp") }
+ if r3d_debug_shadow and not sh_printed { sh_printed = true; print(`cascade vp loc {loc} / {gpu_uniform(prog, "u_cascade_vp")} split loc {gpu_uniform(prog, "u_cascade_split")} shadow loc {gpu_uniform(prog, "u_shadow")}`) }
+ u_mat4n(loc, SHADOW_CASCADES, sh_vp)
+ u_fv(sh_loc(prog, "u_cascade_split"), SHADOW_CASCADES, sh_split)
+ if r3d_debug_shadow and not sh_printed3 { sh_printed3 = true; print(`range {fixed(sh_range[0])} {fixed(sh_range[1])} {fixed(sh_range[2])} {fixed(sh_range[3])} texel*1000 {fixed(sh_texel[0] * 1000.0)} {fixed(sh_texel[1] * 1000.0)} {fixed(sh_texel[2] * 1000.0)} {fixed(sh_texel[3] * 1000.0)} locs {gpu_uniform(prog, "u_cascade_range")} {gpu_uniform(prog, "u_cascade_texel")}`) }
+ u_fv(sh_loc(prog, "u_cascade_range"), SHADOW_CASCADES, sh_range)
+ if r3d_env_has("R3D_FORCE") { sh_force = Text.to_int(r3d_env("R3D_FORCE")) }
+ u_i(gpu_uniform(prog, "u_force_cascade"), sh_force)
+ u_fv(sh_loc(prog, "u_cascade_texel"), SHADOW_CASCADES, sh_texel)
}
diff --git a/packages/ludic.render3d/skin.ludic b/packages/ludic.render3d/skin.ludic
index 2c9ecf11..20f69019 100644
--- a/packages/ludic.render3d/skin.ludic
+++ b/packages/ludic.render3d/skin.ludic
@@ -50,31 +50,31 @@ function skin_jv3(o: floats, at: int, nd: Val, key: pointer, dx: float, dy: floa
}
# JOINTS_0 / WEIGHTS_0 onto attributes 5 and 6 of the VAO being built (gltf_prim)
-function skin_attribs(render3d_st: mut Render3dState, m: Mesh, attrs: Val) -> bool {
+function skin_attribs(m: Mesh, attrs: Val) -> bool {
if value_has(attrs, "JOINTS_0") == 0 or value_has(attrs, "WEIGHTS_0") == 0 { return false }
- let jd = gltf_accessor(render3d_st, value_as_int(value_get(attrs, "JOINTS_0")))
+ let jd = gltf_accessor(value_as_int(value_get(attrs, "JOINTS_0")))
var jsz = 1
var jtype = GPU_U8
- if render3d_st.gltf_ctype == 5123 { jsz = 2; jtype = GPU_U16 }
- gpu_mesh_vertices(render3d_st, m, jd, render3d_st.gltf_count * render3d_st.gltf_comps * jsz, GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, 5, render3d_st.gltf_comps, jtype, 0, 0, false) # integers, read as floats
+ if gltf_ctype == 5123 { jsz = 2; jtype = GPU_U16 }
+ gpu_mesh_vertices(m, jd, gltf_count * gltf_comps * jsz, GPU_STATIC)
+ gpu_mesh_attr(m, 5, gltf_comps, jtype, 0, 0, false) # integers, read as floats
free(jd)
- let wd = gltf_accessor(render3d_st, value_as_int(value_get(attrs, "WEIGHTS_0")))
+ let wd = gltf_accessor(value_as_int(value_get(attrs, "WEIGHTS_0")))
var wsz = 4
var wtype = GPU_F32
var norm = false
- if render3d_st.gltf_ctype == 5123 { wsz = 2; wtype = GPU_U16; norm = true }
- if render3d_st.gltf_ctype == 5121 { wsz = 1; wtype = GPU_U8; norm = true }
- gpu_mesh_vertices(render3d_st, m, wd, render3d_st.gltf_count * render3d_st.gltf_comps * wsz, GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, 6, render3d_st.gltf_comps, wtype, 0, 0, norm)
+ if gltf_ctype == 5123 { wsz = 2; wtype = GPU_U16; norm = true }
+ if gltf_ctype == 5121 { wsz = 1; wtype = GPU_U8; norm = true }
+ gpu_mesh_vertices(m, wd, gltf_count * gltf_comps * wsz, GPU_STATIC)
+ gpu_mesh_attr(m, 6, gltf_comps, wtype, 0, 0, norm)
free(wd)
return true
}
# the skin `idx` of the document being loaded (gltf_load holds gltf_doc / gltf_bin open)
-function skin_load(render3d_st: mut Render3dState, idx: int) -> Skin {
+function skin_load(idx: int) -> Skin {
let sk = new Skin
- let nodes = value_get(render3d_st.gltf_doc, "nodes")
+ let nodes = value_get(gltf_doc, "nodes")
let n = value_count(nodes)
sk.n_nodes = n
sk.par = words(n); sk.walk = words(n)
@@ -121,7 +121,7 @@ function skin_load(render3d_st: mut Render3dState, idx: int) -> Skin {
else { q_store(sk.rest_g, i, sk.tmp_a) }
}
# the skin: joints and inverse bind matrices
- let skv = value_at(value_get(render3d_st.gltf_doc, "skins"), idx)
+ let skv = value_at(value_get(gltf_doc, "skins"), idx)
let jl = value_get(skv, "joints")
var nj = value_count(jl)
if nj > SKIN_MAX_JOINTS { print(`skin: {nj} joints, only the first {SKIN_MAX_JOINTS} are used`); nj = SKIN_MAX_JOINTS }
@@ -133,7 +133,7 @@ function skin_load(render3d_st: mut Render3dState, idx: int) -> Skin {
sk.bones_v = m4_views(sk.bones, nj)
for j in 0 .. nj { sk.joints[j] = value_as_int(value_at(jl, j)) }
if value_has(skv, "inverseBindMatrices") != 0 {
- let ib = gltf_accessor(render3d_st, value_as_int(value_get(skv, "inverseBindMatrices")))
+ let ib = gltf_accessor(value_as_int(value_get(skv, "inverseBindMatrices")))
for i in 0 .. nj * 16 { sk.inv_bind[i] = float_from_bits(mem_get_f32_bits(ib, i)) }
free(ib)
} else {
@@ -160,9 +160,9 @@ function skin_reset(sk: Skin) -> void {
}
}
# a node's pose rotation in the model frame: pitch about X, yaw about Y, roll about Z (radians)
-function skin_set_rot(render3d_st: mut Render3dState, sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void {
+function skin_set_rot(sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void {
if node < 0 { return }
- q_euler(render3d_st, sk.tmp_q, pitch, yaw, roll)
+ q_euler(sk.tmp_q, pitch, yaw, roll)
q_store(sk.pose_r, node, sk.tmp_q)
}
function skin_set_quat(sk: Skin, node: int, q: floats) -> void { if node >= 0 { q_store(sk.pose_r, node, q) } }
@@ -203,10 +203,10 @@ function skin_pose(sk: Skin) -> void {
}
# the joint matrices onto a program's u_bones[]
-function skin_bind(render3d_st: mut Render3dState, sk: Skin, prog: int) -> void {
- var loc = gpu_uniform(render3d_st, prog, "u_bones[0]")
- if loc < 0 { loc = gpu_uniform(render3d_st, prog, "u_bones") }
- u_mat4n(render3d_st, loc, sk.n_joints, sk.bones)
+function skin_bind(sk: Skin, prog: int) -> void {
+ var loc = gpu_uniform(prog, "u_bones[0]")
+ if loc < 0 { loc = gpu_uniform(prog, "u_bones") }
+ u_mat4n(loc, sk.n_joints, sk.bones)
}
# the same skeleton posed on its own: shares the rest data, owns the pose and the matrices
diff --git a/packages/ludic.render3d/sky.ludic b/packages/ludic.render3d/sky.ludic
index fc9c5296..4dbc7e8e 100644
--- a/packages/ludic.render3d/sky.ludic
+++ b/packages/ludic.render3d/sky.ludic
@@ -7,27 +7,47 @@
const SKY_PREFILTER_LEVELS: int = 6
+var sky_tex: int = 0
+var sky_w: int = 0
+var sky_h: int = 0
+var sky_irradiance: int = 0
+var sky_prefilter: int = 0 # GL_TEXTURE_2D_ARRAY
+var sky_brdf: int = 0
+var sun_dir: floats = null # toward the sun (float bits)
+var sun_color: floats = null # radiance (float bits)
+var sky_fullscreen: Mesh = null
+var sky_yaw: float = 0.0 # radians: the HDRI is turned by this about y
+var sky_sun_boost: float = 2.3 # 2.3: the photograph's thin cloud dims its sun; a crisper day wants more
+var sky_rot_s: float = 0.0
+var sky_rot_c: float = 0.0
+var sun_hdri: floats = null # the sun direction as found in the file
# The yaw to bake the light at the first time (radians, float bits). A game that turns the sky at
# start sets it before r3d_init, so the image-based light is baked once at that yaw rather than at
# 0 and then again - which is what sky_set_yaw at boot used to cost.
+var sky_start_yaw: float = 0.0
+var sky_baked: bool = false # the IBL set exists, baked at sky_baked_yaw
+var sky_baked_yaw: float = 0.0
+var sky_p_irr: int = 0
+var sky_p_pre: int = 0
+var sky_p_brdf: int = 0
# turn the HDRI so its sun sits at world azimuth `yaw` (radians, 0 = toward -z)
-function sky_set_yaw(render3d_st: mut Render3dState, yaw: float) -> void {
- sky_set_rot(render3d_st, yaw)
+function sky_set_yaw(yaw: float) -> void {
+ sky_set_rot(yaw)
# world sun = rotY(sun_hdri, -yaw): the lookup rotates a world direction by +yaw
- let s = render3d_st.sun_hdri
- v3_set(render3d_st.sun_dir, render3d_st.sky_rot_c * s[0] - render3d_st.sky_rot_s * s[2], s[1], render3d_st.sky_rot_s * s[0] + render3d_st.sky_rot_c * s[2])
+ let s = sun_hdri
+ v3_set(sun_dir, sky_rot_c * s[0] - sky_rot_s * s[2], s[1], sky_rot_s * s[0] + sky_rot_c * s[2])
# the light is already baked at this yaw: turning to it again bakes nothing
- if render3d_st.sky_baked and yaw == render3d_st.sky_baked_yaw { return }
- sky_precompute(render3d_st)
+ if sky_baked and yaw == sky_baked_yaw { return }
+ sky_precompute()
}
# turn only the visible sky image (cheap, per frame): the light and the convolved
# maps stay where they are — daylight.ludic moves those on its own terms
-function sky_set_rot(render3d_st: mut Render3dState, yaw: float) -> void {
- render3d_st.sky_yaw = yaw
- render3d_st.sky_rot_s = Math.sin(yaw); render3d_st.sky_rot_c = Math.cos(yaw)
+function sky_set_rot(yaw: float) -> void {
+ sky_yaw = yaw
+ sky_rot_s = Math.sin(yaw); sky_rot_c = Math.cos(yaw)
}
-function sky_bind_rot(render3d_st: mut Render3dState, prog: int) -> void { u_f2(render3d_st, gpu_uniform(render3d_st, prog, "u_sky_rot"), render3d_st.sky_rot_s, render3d_st.sky_rot_c) }
+function sky_bind_rot(prog: int) -> void { u_f2(gpu_uniform(prog, "u_sky_rot"), sky_rot_s, sky_rot_c) }
# direction for an equirect uv (matches equirectUV in lighting.glsl)
function sky_dir_from_uv(o: floats, u: float, v: float) -> void {
@@ -37,93 +57,94 @@ function sky_dir_from_uv(o: floats, u: float, v: float) -> void {
v3_set(o, st * Math.sin(phi), Math.cos(theta), -(st * Math.cos(phi)))
}
-function sky_load(render3d_st: mut Render3dState, path: string) -> bool {
- render3d_st.sky_tex = tex_load_hdr(render3d_st, path)
- if render3d_st.sky_tex == 0 { return false }
- render3d_st.sky_w = render3d_st.tex_w; render3d_st.sky_h = render3d_st.tex_h
- render3d_st.sun_dir = floats(3)
- render3d_st.sun_hdri = floats(3)
- sky_dir_from_uv(render3d_st.sun_hdri, float(render3d_st.hdr_max_x * 2 + 1) / float(render3d_st.sky_w * 2), float(render3d_st.hdr_max_y * 2 + 1) / float(render3d_st.sky_h * 2))
- v3_copy(render3d_st.sun_dir, render3d_st.sun_hdri)
- render3d_st.sky_rot_c = 1.0
+function sky_load(path: string) -> bool {
+ sky_tex = tex_load_hdr(path)
+ if sky_tex == 0 { return false }
+ sky_w = tex_w; sky_h = tex_h
+ sun_dir = floats(3)
+ sun_hdri = floats(3)
+ sky_dir_from_uv(sun_hdri, float(hdr_max_x * 2 + 1) / float(sky_w * 2), float(hdr_max_y * 2 + 1) / float(sky_h * 2))
+ v3_copy(sun_dir, sun_hdri)
+ sky_rot_c = 1.0
# the sun's irradiance is what the IBL clip leaves out of the map; lighting it
# directly with that keeps sun and sky in the photograph's own proportion
- render3d_st.sun_color = v3_new(render3d_st.hdr_sun_r * render3d_st.sky_sun_boost, render3d_st.hdr_sun_g * render3d_st.sky_sun_boost, render3d_st.hdr_sun_b * render3d_st.sky_sun_boost)
- print(`sun irradiance: {fixed(render3d_st.hdr_sun_r)} {fixed(render3d_st.hdr_sun_g)} {fixed(render3d_st.hdr_sun_b)} (Q16.16), clip {fixed(render3d_st.hdr_clip)}`)
- print(`sky: {render3d_st.sky_w}x{render3d_st.sky_h}, sun at texel {render3d_st.hdr_max_x},{render3d_st.hdr_max_y}`)
- render3d_st.sky_fullscreen = mesh_fullscreen(render3d_st)
- if render3d_st.sky_start_yaw != 0.0 { sky_set_yaw(render3d_st, render3d_st.sky_start_yaw) } else { sky_precompute(render3d_st) }
+ sun_color = v3_new(hdr_sun_r * sky_sun_boost, hdr_sun_g * sky_sun_boost, hdr_sun_b * sky_sun_boost)
+ print(`sun irradiance: {fixed(hdr_sun_r)} {fixed(hdr_sun_g)} {fixed(hdr_sun_b)} (Q16.16), clip {fixed(hdr_clip)}`)
+ print(`sky: {sky_w}x{sky_h}, sun at texel {hdr_max_x},{hdr_max_y}`)
+ sky_fullscreen = mesh_fullscreen()
+ if sky_start_yaw != 0.0 { sky_set_yaw(sky_start_yaw) } else { sky_precompute() }
return true
}
-function sky_convolve(render3d_st: mut Render3dState, prog: int, target_tex: int, layer: int, w: int, h: int, rough: float) -> void {
- let fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, fbo)
- if layer < 0 { gpu_fb_color(render3d_st, 0, target_tex) }
- else { gpu_fb_color_layer(render3d_st, 0, target_tex, layer) }
- gpu_viewport(render3d_st, 0, 0, w, h)
- gpu_use_program(render3d_st, prog)
- r3d_bind_2d(render3d_st, prog, "u_sky", 0, render3d_st.sky_tex)
- sky_bind_rot(render3d_st, prog)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_sun_clip"), render3d_st.hdr_clip)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_rough"), rough)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_sky_w"), float(render3d_st.sky_w))
- mesh_draw(render3d_st, render3d_st.sky_fullscreen)
- gpu_fb_bind(render3d_st, 0)
- gpu_fb_free(render3d_st, fbo)
+function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, rough: float) -> void {
+ let fbo = gpu_fb_new()
+ gpu_fb_bind(fbo)
+ if layer < 0 { gpu_fb_color(0, target_tex) }
+ else { gpu_fb_color_layer(0, target_tex, layer) }
+ gpu_viewport(0, 0, w, h)
+ gpu_use_program(prog)
+ r3d_bind_2d(prog, "u_sky", 0, sky_tex)
+ sky_bind_rot(prog)
+ u_f(gpu_uniform(prog, "u_sun_clip"), hdr_clip)
+ u_f(gpu_uniform(prog, "u_rough"), rough)
+ u_f(gpu_uniform(prog, "u_sky_w"), float(sky_w))
+ mesh_draw(sky_fullscreen)
+ gpu_fb_bind(0)
+ gpu_fb_free(fbo)
}
# the prefiltered specular's width per level (its height is half): 256, 512 or 1024. The image
# the reflections and the rough sheen are lit from; sky_set_quality bakes it again at a new size.
-function sky_set_quality(render3d_st: mut Render3dState, w: int) -> void {
- if w < 64 or w == render3d_st.sky_prefilter_w { return }
- render3d_st.sky_prefilter_w = w
- if render3d_st.sky_irradiance != 0 { sky_precompute(render3d_st) }
+var sky_prefilter_w: int = 512
+function sky_set_quality(w: int) -> void {
+ if w < 64 or w == sky_prefilter_w { return }
+ sky_prefilter_w = w
+ if sky_irradiance != 0 { sky_precompute() }
}
-function sky_precompute(render3d_st: mut Render3dState) -> void {
- gpu_depth_test(render3d_st, false)
- if render3d_st.sky_irradiance != 0 {
- gpu_tex_free(render3d_st, render3d_st.sky_irradiance)
- gpu_tex_free(render3d_st, render3d_st.sky_prefilter)
- gpu_tex_free(render3d_st, render3d_st.sky_brdf)
+function sky_precompute() -> void {
+ gpu_depth_test(false)
+ if sky_irradiance != 0 {
+ gpu_tex_free(sky_irradiance)
+ gpu_tex_free(sky_prefilter)
+ gpu_tex_free(sky_brdf)
}
# irradiance: 128 x 64 equirect
- if render3d_st.sky_p_irr == 0 { render3d_st.sky_p_irr = r3d_program(render3d_st, "fullscreen.vert", "ibl_irradiance.frag", ""); render3d_st.sky_p_pre = r3d_program(render3d_st, "fullscreen.vert", "ibl_prefilter.frag", ""); render3d_st.sky_p_brdf = r3d_program(render3d_st, "fullscreen.vert", "ibl_brdf.frag", "") }
- let p_irr = render3d_st.sky_p_irr
- render3d_st.sky_irradiance = tex_target(render3d_st, 128, 64, GL_RGB16F, GL_RGB, GL_FLOAT, GL_LINEAR)
- gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.sky_irradiance)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
- sky_convolve(render3d_st, p_irr, render3d_st.sky_irradiance, -1, 128, 64, 0.0)
+ if sky_p_irr == 0 { sky_p_irr = r3d_program("fullscreen.vert", "ibl_irradiance.frag", ""); sky_p_pre = r3d_program("fullscreen.vert", "ibl_prefilter.frag", ""); sky_p_brdf = r3d_program("fullscreen.vert", "ibl_brdf.frag", "") }
+ let p_irr = sky_p_irr
+ sky_irradiance = tex_target(128, 64, GL_RGB16F, GL_RGB, GL_FLOAT, GL_LINEAR)
+ gpu_tex_bind(GPU_TEX2D, sky_irradiance)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
+ sky_convolve(p_irr, sky_irradiance, -1, 128, 64, 0.0)
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
- let p_pre = render3d_st.sky_p_pre
- render3d_st.sky_prefilter = gpu_tex_new(render3d_st)
- gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sky_prefilter)
- gpu_tex_image3d(render3d_st, GL_RGB16F, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
- gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ let p_pre = sky_p_pre
+ sky_prefilter = gpu_tex_new()
+ gpu_tex_bind(GPU_TEX2D_ARRAY, sky_prefilter)
+ gpu_tex_image3d(GL_RGB16F, sky_prefilter_w, sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
for l in 0 .. SKY_PREFILTER_LEVELS {
- sky_convolve(render3d_st, p_pre, render3d_st.sky_prefilter, l, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1))
+ sky_convolve(p_pre, sky_prefilter, l, sky_prefilter_w, sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1))
}
# BRDF LUT
- let p_brdf = render3d_st.sky_p_brdf
- render3d_st.sky_brdf = tex_target(render3d_st, 256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
- sky_convolve(render3d_st, p_brdf, render3d_st.sky_brdf, -1, 256, 256, 0.0)
- render3d_st.sky_baked = true
- render3d_st.sky_baked_yaw = render3d_st.sky_yaw
- gpu_check(render3d_st, "sky precompute")
+ let p_brdf = sky_p_brdf
+ sky_brdf = tex_target(256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
+ sky_convolve(p_brdf, sky_brdf, -1, 256, 256, 0.0)
+ sky_baked = true
+ sky_baked_yaw = sky_yaw
+ gpu_check("sky precompute")
}
# bind the IBL set + sun for a lit program (units 12..14)
-function sky_bind_lighting(render3d_st: mut Render3dState, prog: int) -> void {
- r3d_bind_2d(render3d_st, prog, "u_irradiance", 12, render3d_st.sky_irradiance)
- r3d_bind_tex(render3d_st, prog, "u_prefilter", 13, GPU_TEX2D_ARRAY, render3d_st.sky_prefilter)
- r3d_bind_2d(render3d_st, prog, "u_brdf", 14, render3d_st.sky_brdf)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_sun_dir"), render3d_st.sun_dir)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_sun_color"), render3d_st.sun_color)
- u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_cam_pos"), render3d_st.cam_pos)
- u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_prefilter_levels"), float(SKY_PREFILTER_LEVELS))
- daylight_bind(render3d_st, prog)
+function sky_bind_lighting(prog: int) -> void {
+ r3d_bind_2d(prog, "u_irradiance", 12, sky_irradiance)
+ r3d_bind_tex(prog, "u_prefilter", 13, GPU_TEX2D_ARRAY, sky_prefilter)
+ r3d_bind_2d(prog, "u_brdf", 14, sky_brdf)
+ u_v3(gpu_uniform(prog, "u_sun_dir"), sun_dir)
+ u_v3(gpu_uniform(prog, "u_sun_color"), sun_color)
+ u_v3(gpu_uniform(prog, "u_cam_pos"), cam_pos)
+ u_f(gpu_uniform(prog, "u_prefilter_levels"), float(SKY_PREFILTER_LEVELS))
+ daylight_bind(prog)
}
diff --git a/packages/ludic.render3d/stream.ludic b/packages/ludic.render3d/stream.ludic
index d5bde33a..1c9797c8 100644
--- a/packages/ludic.render3d/stream.ludic
+++ b/packages/ludic.render3d/stream.ludic
@@ -12,6 +12,7 @@
# stream_emit(...) per instance. One Stream drives one Layer.
# ============================================================================
+var STREAM_MAX_CHUNKS: int = 4096
property Chunk {
key: int = 0, # packed (cx, cz, band)
@@ -39,13 +40,22 @@ property Stream {
htab: words # open-addressed key -> chunk index + 1 (0 = empty)
}
+var stream_all: []Stream = null
+var stream_cap_read: bool = false
+var stream_no_evict: bool = false # R3D_NOEVICT: the old behaviour, for comparison
+var stream_walk_no: int = 0 # counts ring walks; a chunk's age is measured in these
+var stream_evictions: int = 0
# microseconds spent per frame, split so the hitch can be attributed (R3D_PROF=1)
+var stream_us_gen: long = 0 # generating new chunks (stream_fill)
+var stream_us_gather: long = 0 # copying cached chunks into the layer buffer
+var stream_us_walk: long = 0 # the ring walk itself
+var stream_walks: int = 0 # streams that walked their whole ring this frame
-function stream_new(render3d_st: mut Render3dState, layer: Layer, size: float, reach: float, b0: float, b1: float, b2: float, b3: float) -> Stream {
- if not render3d_st.stream_cap_read {
- render3d_st.stream_cap_read = true
- if r3d_env_has(render3d_st, "R3D_STREAM_CAP") { render3d_st.STREAM_MAX_CHUNKS = Text.to_int(r3d_env(render3d_st, "R3D_STREAM_CAP")) }
- render3d_st.stream_no_evict = r3d_env_has(render3d_st, "R3D_NOEVICT")
+function stream_new(layer: Layer, size: float, reach: float, b0: float, b1: float, b2: float, b3: float) -> Stream {
+ if not stream_cap_read {
+ stream_cap_read = true
+ if r3d_env_has("R3D_STREAM_CAP") { STREAM_MAX_CHUNKS = Text.to_int(r3d_env("R3D_STREAM_CAP")) }
+ stream_no_evict = r3d_env_has("R3D_NOEVICT")
}
let s = new Stream
s.layer = layer; s.size = size; s.reach = reach
@@ -54,11 +64,11 @@ function stream_new(render3d_st: mut Render3dState, layer: Layer, size: float, r
s.bands = floats(4)
s.bands[0] = b0; s.bands[1] = b1; s.bands[2] = b2; s.bands[3] = b3
s.chunks = new []Chunk
- s.keys = words(render3d_st.STREAM_MAX_CHUNKS)
+ s.keys = words(STREAM_MAX_CHUNKS)
s.htab = words(STREAM_HASH)
for i in 0 .. STREAM_HASH { s.htab[i] = 0 }
- if render3d_st.stream_all == null { render3d_st.stream_all = new []Stream }
- push(render3d_st.stream_all, s)
+ if stream_all == null { stream_all = new []Stream }
+ push(stream_all, s)
return s
}
@@ -97,14 +107,16 @@ function stream_remember(s: Stream, key: int, idx: int) -> void {
# the generator adds instances to the chunk being filled (into a shared scratch; the
# chunk gets an exactly-sized copy when the fill ends)
const STREAM_CHUNK_MAX: int = 262144
-function stream_emit(render3d_st: mut Render3dState, s: Stream, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
+var stream_debug_n: int = 0
+var stream_scratch: floats = null
+function stream_emit(s: Stream, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
let c = s.cur
if c.count >= STREAM_CHUNK_MAX { return }
- if render3d_st.stream_scratch == null { render3d_st.stream_scratch = floats(STREAM_CHUNK_MAX * INST_FLOATS) }
+ if stream_scratch == null { stream_scratch = floats(STREAM_CHUNK_MAX * INST_FLOATS) }
if c.count == 0 { c.ymin = y; c.ymax = y } else { c.ymin = Math.min(c.ymin, y); c.ymax = Math.max(c.ymax, y) }
let o = c.count * INST_FLOATS
- render3d_st.stream_scratch[o] = x; render3d_st.stream_scratch[o + 1] = y; render3d_st.stream_scratch[o + 2] = z; render3d_st.stream_scratch[o + 3] = scale
- render3d_st.stream_scratch[o + 4] = Math.sin(yaw); render3d_st.stream_scratch[o + 5] = Math.cos(yaw); render3d_st.stream_scratch[o + 6] = seed; render3d_st.stream_scratch[o + 7] = wind
+ stream_scratch[o] = x; stream_scratch[o + 1] = y; stream_scratch[o + 2] = z; stream_scratch[o + 3] = scale
+ stream_scratch[o + 4] = Math.sin(yaw); stream_scratch[o + 5] = Math.cos(yaw); stream_scratch[o + 6] = seed; stream_scratch[o + 7] = wind
c.count += 1
}
@@ -131,11 +143,14 @@ function stream_band(s: Stream, d: float) -> int {
# by band and kind. With a real microsecond clock the budget can just be the thing we
# actually care about — how long this frame is allowed to spend growing ground cover.
# Overshoot is bounded by one chunk, so keep chunks small on the dense near streams.
+var STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame; a setting may move it
+var stream_deadline: long = 0
const STREAM_BUDGET: int = 8000 # kept for the work counter only
# The worst frame is now bounded by one chunk, not by the budget: stream_fill emits a
# whole chunk in one call, and the densest band-0 chunk is ~114k instances. Splitting a
# chunk's generation across frames would need a resumable generator contract; that is
# the next step if the residual hitch ever matters.
+var stream_budget_left: int = 0
# Drop the half of the cache nobody has asked for in the longest time, and rebuild the
# index over what is left.
@@ -145,10 +160,10 @@ const STREAM_BUDGET: int = 8000 # kept for the work counter only
# — it is a cliff. Past it every frame pays the whole generation budget and the ground
# visibly re-grows as you turn, and it arrives after enough of the map has been walked,
# which is exactly when a player is least likely to connect it to anything.
-function stream_evict(render3d_st: mut Render3dState, s: Stream) -> void {
+function stream_evict(s: Stream) -> void {
# the age threshold that keeps about half, found by bisection on the count (no sort)
var lo = 0
- var hi = render3d_st.stream_walk_no
+ var hi = stream_walk_no
var keep = s.n / 2
var t = 0
var it = 0
@@ -166,7 +181,7 @@ function stream_evict(render3d_st: mut Render3dState, s: Stream) -> void {
var i = 0
while i < s.n {
let c = s.chunks[i]
- if c.used >= t or c.used == render3d_st.stream_walk_no { push(kept, c) }
+ if c.used >= t or c.used == stream_walk_no { push(kept, c) }
else { if c.data != null { free(c.data) } }
i += 1
}
@@ -175,21 +190,21 @@ function stream_evict(render3d_st: mut Render3dState, s: Stream) -> void {
for h in 0 .. STREAM_HASH { s.htab[h] = 0 }
i = 0
while i < s.n { s.keys[i] = s.chunks[i].key; stream_remember(s, s.chunks[i].key, i); i += 1 }
- render3d_st.stream_evictions += 1
+ stream_evictions += 1
}
-function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float, cam_z: float) -> void {
+function stream_update(s: Stream, cam_x: float, cam_z: float) -> void {
let ccx = int(Math.floor(cam_x / s.size))
let ccz = int(Math.floor(cam_z / s.size))
- if ccx == s.last_cx and ccz == s.last_cz and not s.pending and s.view_gen == render3d_st.sc_view_gen { return }
+ if ccx == s.last_cx and ccz == s.last_cz and not s.pending and s.view_gen == sc_view_gen { return }
let first = s.last_cx == 999999
- s.view_gen = render3d_st.sc_view_gen
+ s.view_gen = sc_view_gen
s.last_cx = ccx; s.last_cz = ccz
let l = s.layer
l.count = 0
var missing = false
- render3d_st.stream_walks += 1
- render3d_st.stream_walk_no += 1
+ stream_walks += 1
+ stream_walk_no += 1
let tw = gl_now_us()
let r = int(s.reach / s.size) + 1
# rings outward from the camera's cell: the nearest chunks are generated first
@@ -209,31 +224,31 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
if band < 4 and band >= s.min_band and d < s.reach + s.size {
let key = stream_key(cx, cz, band)
var c = stream_find(s, key)
- if c != null { c.used = render3d_st.stream_walk_no }
+ if c != null { c.used = stream_walk_no }
# The cell underfoot and its neighbours are never deferred: they are what you
# are looking at, and a hole there is the grass vanishing as you walk into it.
let urgent = band == 0 and ring <= 1
- if c == null and (first or urgent or gl_now_us() < render3d_st.stream_deadline) {
+ if c == null and (first or urgent or gl_now_us() < stream_deadline) {
c = new Chunk
c.key = key
s.cur = c
let t0 = gl_now_us()
- r3d_stream_fill(render3d_st, s, cx, cz, band)
+ r3d_stream_fill(s, cx, cz, band)
let dt = gl_now_us() - t0
- render3d_st.stream_us_gen = render3d_st.stream_us_gen + dt
- prof_chunk(render3d_st, s.kind, band, c.count, dt)
- if render3d_st.r3d_debug and band == 0 and render3d_st.stream_debug_n < 40 { render3d_st.stream_debug_n += 1; print(`stream kind {s.kind} band {band} chunk {cx},{cz}: {c.count} instances`) }
- if c.count > 0 { c.data = words(c.count * INST_FLOATS); mem_copy(c.data, render3d_st.stream_scratch, c.count * INST_FLOATS * 4) }
- if s.n >= render3d_st.STREAM_MAX_CHUNKS and not render3d_st.stream_no_evict { stream_evict(render3d_st, s) }
+ stream_us_gen = stream_us_gen + dt
+ prof_chunk(s.kind, band, c.count, dt)
+ if r3d_debug and band == 0 and stream_debug_n < 40 { stream_debug_n += 1; print(`stream kind {s.kind} band {band} chunk {cx},{cz}: {c.count} instances`) }
+ if c.count > 0 { c.data = words(c.count * INST_FLOATS); mem_copy(c.data, stream_scratch, c.count * INST_FLOATS * 4) }
+ if s.n >= STREAM_MAX_CHUNKS and not stream_no_evict { stream_evict(s) }
# If the walk in progress wants more chunks than the cache can hold, there
# is nothing to evict and this one is used and dropped, as every chunk used
# to be. The cap has to exceed one walk's ring for the cache to work at all.
- if s.n < render3d_st.STREAM_MAX_CHUNKS {
+ if s.n < STREAM_MAX_CHUNKS {
push(s.chunks, c); s.keys[s.n] = key; stream_remember(s, key, s.n); s.n += 1
- c.used = render3d_st.stream_walk_no
+ c.used = stream_walk_no
}
- prof_gen_add(render3d_st, c.count + 512)
+ prof_gen_add(c.count + 512)
}
if c == null {
missing = true
@@ -242,13 +257,13 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
# few frames instead of disappearing.
var b2 = band + 1
while c == null and b2 < 4 { c = stream_find(s, stream_key(cx, cz, b2)); b2 += 1 }
- if c != null { c.used = render3d_st.stream_walk_no }
+ if c != null { c.used = stream_walk_no }
}
- if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(render3d_st, s, cx, cz, c) {
+ if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(s, cx, cz, c) {
let tg = gl_now_us()
mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), data_of(c.data), c.count * INST_FLOATS * 4)
l.count += c.count
- render3d_st.stream_us_gather = render3d_st.stream_us_gather + (gl_now_us() - tg)
+ stream_us_gather = stream_us_gather + (gl_now_us() - tg)
}
}
}
@@ -259,7 +274,7 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
ring += 1
}
s.pending = missing
- render3d_st.stream_us_walk = render3d_st.stream_us_walk + (gl_now_us() - tw)
+ stream_us_walk = stream_us_walk + (gl_now_us() - tw)
# force the layer to re-partition its (new) instances
l.view_gen = -1
}
@@ -267,24 +282,24 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
# Only chunks that can be seen are gathered: a sphere around the chunk's footprint and
# height range, padded for the tallest cover and for casters just outside the frame
# whose short shadows still fall inside it.
-function stream_chunk_visible(render3d_st: Render3dState, s: Stream, cx: int, cz: int, c: Chunk) -> bool {
+function stream_chunk_visible(s: Stream, cx: int, cz: int, c: Chunk) -> bool {
let half = s.size * 0.5
let wx = float(cx) * s.size + half
let wz = float(cz) * s.size + half
let hy = (c.ymax - c.ymin) * 0.5
let cy = c.ymin + hy
let r = Math.sqrt(half * half * 2.0 + hy * hy) + 8.0
- return cam_sphere_visible(render3d_st, wx, cy, wz, r)
+ return cam_sphere_visible(wx, cy, wz, r)
}
# what the caches hold, and whether they are being churned (R3D_PROF)
-function stream_census(render3d_st: Render3dState) -> void {
- if render3d_st.stream_all == null { return }
+function stream_census() -> void {
+ if stream_all == null { return }
print("")
- print(`ground-cover chunk caches (cap {string(render3d_st.STREAM_MAX_CHUNKS)} each, {string(render3d_st.stream_evictions)} evictions over the run):`)
+ print(`ground-cover chunk caches (cap {string(STREAM_MAX_CHUNKS)} each, {string(stream_evictions)} evictions over the run):`)
var inst = 0
- for i in 0 .. len(render3d_st.stream_all) {
- let s = render3d_st.stream_all[i]
+ for i in 0 .. len(stream_all) {
+ let s = stream_all[i]
var n = 0
for k in 0 .. s.n { n += s.chunks[k].count }
inst += n
@@ -293,21 +308,21 @@ function stream_census(render3d_st: Render3dState) -> void {
print(` {string(inst)} instances held, {string(inst * INST_FLOATS * 4 / 1024)} KB`)
}
-function stream_update_all(render3d_st: mut Render3dState) -> void {
- if render3d_st.stream_all == null { return }
- render3d_st.stream_deadline = gl_now_us() + render3d_st.STREAM_BUDGET_US
- for i in 0 .. len(render3d_st.stream_all) { stream_update(render3d_st, render3d_st.stream_all[i], render3d_st.cam_pos[0], render3d_st.cam_pos[2]) }
+function stream_update_all() -> void {
+ if stream_all == null { return }
+ stream_deadline = gl_now_us() + STREAM_BUDGET_US
+ for i in 0 .. len(stream_all) { stream_update(stream_all[i], cam_pos[0], cam_pos[2]) }
}
# every stream and its cached chunks, for a world being replaced (scatter_clear_all)
-function stream_clear_all(render3d_st: mut Render3dState) -> void {
- if render3d_st.stream_all == null { return }
- for i in 0 .. len(render3d_st.stream_all) {
- let s = render3d_st.stream_all[i]
+function stream_clear_all() -> void {
+ if stream_all == null { return }
+ for i in 0 .. len(stream_all) {
+ let s = stream_all[i]
if s.chunks != null { for c in 0 .. len(s.chunks) { if s.chunks[c].data != null { free(s.chunks[c].data) } } }
if s.keys != null { free(s.keys) }
if s.htab != null { free(s.htab) }
if s.bands != null { free(s.bands) }
}
- render3d_st.stream_all = null
+ stream_all = null
}
diff --git a/packages/ludic.render3d/streamline.ludic b/packages/ludic.render3d/streamline.ludic
index 970532c3..460d4168 100644
--- a/packages/ludic.render3d/streamline.ludic
+++ b/packages/ludic.render3d/streamline.ludic
@@ -30,7 +30,22 @@ const GSL_PRESENT_END: int = 5
const GSL_LAYOUT_READ: int = 5 # VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
+var gsl_on: bool = false # slInit succeeded: the interposer is the loader, the plugins are in
+var gsl_dlss_ok: bool = false # what this adapter can run, from slIsFeatureSupported
+var gsl_rr_ok: bool = false
+var gsl_fg_ok: bool = false
+var gsl_reflex_ok: bool = false
+var gsl_pcl_ok: bool = false
+var gsl_feats: bytes = null # kept alive: Preferences points at it
+var gsl_logdir: bytes = null # and at this
+var gsl_vp: bytes = null # sl::ViewportHandle 0, the one view
+var gsl_tok_buf: bytes = null
+var gsl_idx_buf: bytes = null
+var gsl_token: pointer = null # this frame's sl::FrameToken, owned by Streamline
+var gsl_frame_n: int = 0
+var gsl_said: bool = false # one failure message, not one a frame
+var gsl_fresh: bool = false # a token was taken since the last frame started (a present took it)
# ---- structure headers ------------------------------------------------------------------
function gsl_le16(v: int) -> int { return ((v >> 8) & 255) | ((v & 255) << 8) }
@@ -59,36 +74,36 @@ function gsl_fn(feature: int, name: string) -> pointer {
# ---- start and stop ---------------------------------------------------------------------
# Before Vk.open(): ask the loader for the interposer. R3D_NO_STREAMLINE=1 keeps plain Vulkan.
-function gsl_boot(render3d_st: mut Render3dState) -> void {
+function gsl_boot() -> void {
if Os.platform() != "windows" { return }
- if r3d_env_has(render3d_st, "R3D_NO_STREAMLINE") { return }
+ if r3d_env_has("R3D_NO_STREAMLINE") { return }
Vk.sl_prefer(1)
}
# After Vk.open(), before vkCreateInstance.
-function gsl_init(render3d_st: mut Render3dState) -> void {
- if render3d_st.gsl_on or Vk.sl_active() == 0 { return }
- render3d_st.gsl_feats = gsl_struct(20)
- Vk.put_i32(render3d_st.gsl_feats, 0, GSL_DLSS); Vk.put_i32(render3d_st.gsl_feats, 4, GSL_REFLEX); Vk.put_i32(render3d_st.gsl_feats, 8, GSL_PCL)
- Vk.put_i32(render3d_st.gsl_feats, 12, GSL_DLSS_RR); Vk.put_i32(render3d_st.gsl_feats, 16, GSL_DLSS_G)
+function gsl_init() -> void {
+ if gsl_on or Vk.sl_active() == 0 { return }
+ gsl_feats = gsl_struct(20)
+ Vk.put_i32(gsl_feats, 0, GSL_DLSS); Vk.put_i32(gsl_feats, 4, GSL_REFLEX); Vk.put_i32(gsl_feats, 8, GSL_PCL)
+ Vk.put_i32(gsl_feats, 12, GSL_DLSS_RR); Vk.put_i32(gsl_feats, 16, GSL_DLSS_G)
# sl::Preferences
let pref = gsl_struct(144)
gsl_header(pref, 0x1ca1, 0x0965, 0xbf8e, 0x432b, 0x8da1, 0x6716, 0xd879, 0xfb14, 1)
# logLevel eOff; R3D_SL_LOG= writes Streamline's own verbose log (sl.log) there
var level = 0
- if r3d_env_has(render3d_st, "R3D_SL_LOG") {
+ if r3d_env_has("R3D_SL_LOG") {
level = 2
- let dir: pointer = r3d_env(render3d_st, "R3D_SL_LOG")
- let n = Text.length(r3d_env(render3d_st, "R3D_SL_LOG"))
- render3d_st.gsl_logdir = gsl_struct(n * 2 + 8) # pathToLogsAndData is a wide string
- for i in 0 .. n { Vk.put_i32(render3d_st.gsl_logdir, i * 2, dir[i] & 255) }
- Vk.put_ptr(pref, 56, render3d_st.gsl_logdir)
+ let dir: pointer = r3d_env("R3D_SL_LOG")
+ let n = Text.length(r3d_env("R3D_SL_LOG"))
+ gsl_logdir = gsl_struct(n * 2 + 8) # pathToLogsAndData is a wide string
+ for i in 0 .. n { Vk.put_i32(gsl_logdir, i * 2, dir[i] & 255) }
+ Vk.put_ptr(pref, 56, gsl_logdir)
}
Vk.put_i32(pref, 36, level)
# eDisableCLStateTracking | eAllowOTA | eLoadDownloadedPlugins | eUseFrameBasedResourceTagging
let flags: long = 1 | 8 | 64 | 128
Vk.put_i64(pref, 88, flags)
- Vk.put_ptr(pref, 96, render3d_st.gsl_feats)
+ Vk.put_ptr(pref, 96, gsl_feats)
Vk.put_i32(pref, 104, 5)
Vk.put_i32(pref, 112, 0) # engine eCustom
Vk.put_ptr(pref, 120, "ludic render3d")
@@ -96,11 +111,11 @@ function gsl_init(render3d_st: mut Render3dState) -> void {
Vk.put_i32(pref, 136, 2) # renderAPI eVulkan
let r = Vk.sl_init(pref, gsl_sdk_version())
if r != 0 { print(`r3d: streamline: slInit failed ({r}); DLSS and Reflex are off`); return }
- render3d_st.gsl_on = true
- render3d_st.gsl_vp = gsl_struct(40)
- gsl_header(render3d_st.gsl_vp, 0x171b, 0x6435, 0x9b3c, 0x4fc8, 0x9994, 0xfbe5, 0x2569, 0xaaa4, 1)
- render3d_st.gsl_tok_buf = bytes(8)
- render3d_st.gsl_idx_buf = bytes(4)
+ gsl_on = true
+ gsl_vp = gsl_struct(40)
+ gsl_header(gsl_vp, 0x171b, 0x6435, 0x9b3c, 0x4fc8, 0x9994, 0xfbe5, 0x2569, 0xaaa4, 1)
+ gsl_tok_buf = bytes(8)
+ gsl_idx_buf = bytes(4)
}
# kSDKVersion, built from longs: (2 << 48) on ints is computed in 32 bits and arrives as garbage,
@@ -114,86 +129,90 @@ function gsl_sdk_version() -> long {
}
# After vkCreateDevice: what this adapter supports.
-function gsl_probe_device(render3d_st: mut Render3dState, pd: pointer) -> void {
- if not render3d_st.gsl_on { return }
+function gsl_probe_device(pd: pointer) -> void {
+ if not gsl_on { return }
let ai = gsl_struct(56) # sl::AdapterInfo
gsl_header(ai, 0x0677, 0x315f, 0xa746, 0x4492, 0x9f42, 0xcb61, 0x42c9, 0xc3d4, 1)
Vk.put_ptr(ai, 48, pd)
- render3d_st.gsl_dlss_ok = Vk.sl_is_feature_supported(GSL_DLSS, ai) == 0
- render3d_st.gsl_rr_ok = Vk.sl_is_feature_supported(GSL_DLSS_RR, ai) == 0
- render3d_st.gsl_fg_ok = Vk.sl_is_feature_supported(GSL_DLSS_G, ai) == 0
- render3d_st.gsl_reflex_ok = Vk.sl_is_feature_supported(GSL_REFLEX, ai) == 0
- render3d_st.gsl_pcl_ok = Vk.sl_is_feature_supported(GSL_PCL, ai) == 0
- print(`r3d: streamline: DLSS {render3d_st.gsl_dlss_ok}, ray reconstruction {render3d_st.gsl_rr_ok}, frame generation {render3d_st.gsl_fg_ok}, Reflex {render3d_st.gsl_reflex_ok}`)
+ gsl_dlss_ok = Vk.sl_is_feature_supported(GSL_DLSS, ai) == 0
+ gsl_rr_ok = Vk.sl_is_feature_supported(GSL_DLSS_RR, ai) == 0
+ gsl_fg_ok = Vk.sl_is_feature_supported(GSL_DLSS_G, ai) == 0
+ gsl_reflex_ok = Vk.sl_is_feature_supported(GSL_REFLEX, ai) == 0
+ gsl_pcl_ok = Vk.sl_is_feature_supported(GSL_PCL, ai) == 0
+ print(`r3d: streamline: DLSS {gsl_dlss_ok}, ray reconstruction {gsl_rr_ok}, frame generation {gsl_fg_ok}, Reflex {gsl_reflex_ok}`)
}
# Before the device goes.
-function gsl_shutdown(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gsl_on { return }
+function gsl_shutdown() -> void {
+ if not gsl_on { return }
Vk.sl_shutdown()
- render3d_st.gsl_on = false
+ gsl_on = false
}
# ---- frames, Reflex and the latency markers ---------------------------------------------
# A frame's token is taken straight after the previous present, where Reflex sleeps: the
# wait lands before the game reads input and simulates, which is the latency it removes.
+var gsl_reflex_mode: int = 0 # 0 off, 1 low latency, 2 low latency + boost
+var gsl_reflex_applied: int = -1
+var gsl_f_sleep: pointer = null
+var gsl_f_marker: pointer = null
-function r3d_reflex(render3d_st: mut Render3dState, mode: int) -> void {
- render3d_st.gsl_reflex_mode = mode
- if r3d_env_has(render3d_st, "R3D_REFLEX") { render3d_st.gsl_reflex_mode = Text.to_int(r3d_env(render3d_st, "R3D_REFLEX")) }
+function r3d_reflex(mode: int) -> void {
+ gsl_reflex_mode = mode
+ if r3d_env_has("R3D_REFLEX") { gsl_reflex_mode = Text.to_int(r3d_env("R3D_REFLEX")) }
}
-function r3d_reflex_live(render3d_st: Render3dState) -> bool { return render3d_st.gsl_on and render3d_st.gsl_reflex_ok and render3d_st.gsl_reflex_mode > 0 }
+function r3d_reflex_live() -> bool { return gsl_on and gsl_reflex_ok and gsl_reflex_mode > 0 }
-function gsl_marker(render3d_st: mut Render3dState, m: int) -> void {
- if not render3d_st.gsl_pcl_ok or render3d_st.gsl_token == null { return }
- if render3d_st.gsl_f_marker == null { render3d_st.gsl_f_marker = gsl_fn(GSL_PCL, "slPCLSetMarker") }
- if render3d_st.gsl_f_marker != null { Vk.sl_call_ip(render3d_st.gsl_f_marker, m, render3d_st.gsl_token) }
+function gsl_marker(m: int) -> void {
+ if not gsl_pcl_ok or gsl_token == null { return }
+ if gsl_f_marker == null { gsl_f_marker = gsl_fn(GSL_PCL, "slPCLSetMarker") }
+ if gsl_f_marker != null { Vk.sl_call_ip(gsl_f_marker, m, gsl_token) }
}
-function gsl_reflex_apply(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gsl_reflex_ok or render3d_st.gsl_reflex_applied == render3d_st.gsl_reflex_mode { return }
+function gsl_reflex_apply() -> void {
+ if not gsl_reflex_ok or gsl_reflex_applied == gsl_reflex_mode { return }
let f = gsl_fn(GSL_REFLEX, "slReflexSetOptions")
if f == null { return }
let o = gsl_struct(48) # sl::ReflexOptions
gsl_header(o, 0xf03a, 0xf81a, 0x6d0b, 0x4902, 0xa651, 0xc496, 0x5e21, 0x5434, 1)
- Vk.put_i32(o, 32, render3d_st.gsl_reflex_mode)
- if render3d_st.gsl_pcl_ok { Vk.put_i32(o, 40, 1) } # useMarkersToOptimize
- if Vk.sl_call_p(f, o) == 0 { render3d_st.gsl_reflex_applied = render3d_st.gsl_reflex_mode }
+ Vk.put_i32(o, 32, gsl_reflex_mode)
+ if gsl_pcl_ok { Vk.put_i32(o, 40, 1) } # useMarkersToOptimize
+ if Vk.sl_call_p(f, o) == 0 { gsl_reflex_applied = gsl_reflex_mode }
}
-function gsl_new_frame(render3d_st: mut Render3dState) -> void {
- Vk.put_i32(render3d_st.gsl_idx_buf, 0, render3d_st.gsl_frame_n)
- render3d_st.gsl_frame_n += 1
- render3d_st.gsl_token = null
- render3d_st.gsl_fresh = true
- if Vk.sl_get_new_frame_token(render3d_st.gsl_tok_buf, render3d_st.gsl_idx_buf) == 0 { render3d_st.gsl_token = Vk.get_ptr(render3d_st.gsl_tok_buf, 0) }
- if render3d_st.gsl_token == null { return }
- gsl_reflex_apply(render3d_st)
- if r3d_reflex_live(render3d_st) {
- if render3d_st.gsl_f_sleep == null { render3d_st.gsl_f_sleep = gsl_fn(GSL_REFLEX, "slReflexSleep") }
- if render3d_st.gsl_f_sleep != null { Vk.sl_call_p(render3d_st.gsl_f_sleep, render3d_st.gsl_token) }
+function gsl_new_frame() -> void {
+ Vk.put_i32(gsl_idx_buf, 0, gsl_frame_n)
+ gsl_frame_n += 1
+ gsl_token = null
+ gsl_fresh = true
+ if Vk.sl_get_new_frame_token(gsl_tok_buf, gsl_idx_buf) == 0 { gsl_token = Vk.get_ptr(gsl_tok_buf, 0) }
+ if gsl_token == null { return }
+ gsl_reflex_apply()
+ if r3d_reflex_live() {
+ if gsl_f_sleep == null { gsl_f_sleep = gsl_fn(GSL_REFLEX, "slReflexSleep") }
+ if gsl_f_sleep != null { Vk.sl_call_p(gsl_f_sleep, gsl_token) }
}
- gsl_marker(render3d_st, GSL_SIM_START)
+ gsl_marker(GSL_SIM_START)
}
# r3d_frame's first line: the game has simulated, the renderer starts recording
-function gsl_frame_start(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gsl_on or render3d_st.gpu_kind != GPU_VK { return }
+function gsl_frame_start() -> void {
+ if not gsl_on or gpu_kind != GPU_VK { return }
# a headless run never presents: each frame takes its own token here
- if not render3d_st.gsl_fresh { gsl_new_frame(render3d_st) }
- render3d_st.gsl_fresh = false
- gsl_marker(render3d_st, GSL_SIM_END)
- gsl_marker(render3d_st, GSL_SUBMIT_START)
+ if not gsl_fresh { gsl_new_frame() }
+ gsl_fresh = false
+ gsl_marker(GSL_SIM_END)
+ gsl_marker(GSL_SUBMIT_START)
}
-function gsl_before_present(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gsl_on { return }
- gsl_marker(render3d_st, GSL_SUBMIT_END)
- gsl_marker(render3d_st, GSL_PRESENT_START)
+function gsl_before_present() -> void {
+ if not gsl_on { return }
+ gsl_marker(GSL_SUBMIT_END)
+ gsl_marker(GSL_PRESENT_START)
}
-function gsl_after_present(render3d_st: mut Render3dState) -> void {
- if not render3d_st.gsl_on { return }
- gsl_marker(render3d_st, GSL_PRESENT_END)
- gsl_new_frame(render3d_st)
+function gsl_after_present() -> void {
+ if not gsl_on { return }
+ gsl_marker(GSL_PRESENT_END)
+ gsl_new_frame()
}
# ---- DLSS super resolution ----------------------------------------------------------------
@@ -202,16 +221,39 @@ function gsl_after_present(render3d_st: mut Render3dState) -> void {
# sharpen pass change size. The projection is jittered on a Halton (2, 3) cycle while DLSS is on.
# There are no per-object motion vectors yet: a zero target is tagged and Streamline adds the
# camera's own motion from depth and clipToPrevClip.
+var gsl_dlss_mode: int = 0 # 0 off, 1 DLAA, 2 quality, 3 balanced, 4 performance
+var gsl_opts: bytes = null
+var gsl_opt_mode: int = -1
+var gsl_opt_w: int = 0
+var gsl_opt_h: int = 0
+var gsl_rw: int = 0 # the render size DLSS asked for
+var gsl_rh: int = 0
+var gsl_set_mode: int = -1
+var gsl_set_w: int = 0
+var gsl_set_h: int = 0
+var gsl_mv: Target = null
+var gsl_out: Target = null
+var gsl_consts: bytes = null
+var gsl_tags: bytes = null
+var gsl_res: bytes = null
+var gsl_inputs: bytes = null
+var gsl_prev_vp: floats = null
+var gsl_reset: bool = true
+var gsl_jitter_x: float = 0.0 # float bits, NDC offsets the projection carries this frame
+var gsl_jitter_y: float = 0.0
+var gsl_jpx: float = 0.0 # float bits, the same in pixels
+var gsl_jpy: float = 0.0
+var gsl_eval_ok: bool = true # the last evaluate worked: only then is the next frame jittered
# R3D_DLSS=0..4 overrides the setting, for a headless take
-function r3d_dlss(render3d_st: mut Render3dState, mode: int) -> void {
+function r3d_dlss(mode: int) -> void {
var m = mode
- if r3d_env_has(render3d_st, "R3D_DLSS") { m = Text.to_int(r3d_env(render3d_st, "R3D_DLSS")) }
- if m != render3d_st.gsl_dlss_mode { render3d_st.gsl_reset = true }
- render3d_st.gsl_dlss_mode = m
+ if r3d_env_has("R3D_DLSS") { m = Text.to_int(r3d_env("R3D_DLSS")) }
+ if m != gsl_dlss_mode { gsl_reset = true }
+ gsl_dlss_mode = m
}
-function gsl_dlss_live(render3d_st: Render3dState) -> bool { return render3d_st.gsl_on and render3d_st.gsl_dlss_ok and render3d_st.gsl_dlss_mode > 0 and render3d_st.gpu_kind == GPU_VK and render3d_st.gsl_token != null }
-function r3d_dlss_live(render3d_st: Render3dState) -> bool { return render3d_st.gsl_on and render3d_st.gsl_dlss_ok and render3d_st.gsl_dlss_mode > 0 and render3d_st.gpu_kind == GPU_VK }
+function gsl_dlss_live() -> bool { return gsl_on and gsl_dlss_ok and gsl_dlss_mode > 0 and gpu_kind == GPU_VK and gsl_token != null }
+function r3d_dlss_live() -> bool { return gsl_on and gsl_dlss_ok and gsl_dlss_mode > 0 and gpu_kind == GPU_VK }
# sl::DLSSMode from the setting
function gsl_sl_mode(m: int) -> int {
@@ -222,43 +264,43 @@ function gsl_sl_mode(m: int) -> int {
return 0
}
-function gsl_fill_options(render3d_st: mut Render3dState, w: int, h: int) -> void {
- if render3d_st.gsl_opts == null { render3d_st.gsl_opts = gsl_struct(88) }
- Vk.zero(render3d_st.gsl_opts, 88)
- gsl_header(render3d_st.gsl_opts, 0x6ac8, 0x26e4, 0x4c61, 0x4101, 0xa92d, 0x638d, 0x4210, 0x57b8, 3)
- Vk.put_i32(render3d_st.gsl_opts, 32, gsl_sl_mode(render3d_st.gsl_dlss_mode))
- Vk.put_i32(render3d_st.gsl_opts, 36, w)
- Vk.put_i32(render3d_st.gsl_opts, 40, h)
- Vk.put_i32(render3d_st.gsl_opts, 48, float_bits(1.0)) # preExposure
- Vk.put_i32(render3d_st.gsl_opts, 52, float_bits(1.0)) # exposureScale
- Vk.put_i32(render3d_st.gsl_opts, 56, 1) # colorBuffersHDR eTrue
+function gsl_fill_options(w: int, h: int) -> void {
+ if gsl_opts == null { gsl_opts = gsl_struct(88) }
+ Vk.zero(gsl_opts, 88)
+ gsl_header(gsl_opts, 0x6ac8, 0x26e4, 0x4c61, 0x4101, 0xa92d, 0x638d, 0x4210, 0x57b8, 3)
+ Vk.put_i32(gsl_opts, 32, gsl_sl_mode(gsl_dlss_mode))
+ Vk.put_i32(gsl_opts, 36, w)
+ Vk.put_i32(gsl_opts, 40, h)
+ Vk.put_i32(gsl_opts, 48, float_bits(1.0)) # preExposure
+ Vk.put_i32(gsl_opts, 52, float_bits(1.0)) # exposureScale
+ Vk.put_i32(gsl_opts, 56, 1) # colorBuffersHDR eTrue
# The model: preset K (the transformer NVIDIA calls its best image quality) in every mode. The
# defaults put Performance on preset M, which on an RTX 3070 Ti at 4K evaluated in 18 ms against
# K's 2.8 - slower than no DLSS at all (33 fps against 41; with K, 60). R3D_DLSS_PRESET=
# (sl::DLSSPreset: 11 K, 12 L, 13 M) sets every mode's, to measure them against each other.
var preset = 11
- if r3d_env_has(render3d_st, "R3D_DLSS_PRESET") { preset = Text.to_int(r3d_env(render3d_st, "R3D_DLSS_PRESET")) }
- if preset > 0 { for f in 0 .. 6 { Vk.put_i32(render3d_st.gsl_opts, 60 + f * 4, preset) } } # dlaa, quality, balanced, performance, ultra performance, ultra quality
+ if r3d_env_has("R3D_DLSS_PRESET") { preset = Text.to_int(r3d_env("R3D_DLSS_PRESET")) }
+ if preset > 0 { for f in 0 .. 6 { Vk.put_i32(gsl_opts, 60 + f * 4, preset) } } # dlaa, quality, balanced, performance, ultra performance, ultra quality
}
# the render size for the display's size and the mode, asked once per change
-function gsl_optimal(render3d_st: mut Render3dState) -> void {
- if render3d_st.gsl_opt_mode == render3d_st.gsl_dlss_mode and render3d_st.gsl_opt_w == gl_w and render3d_st.gsl_opt_h == gl_h { return }
- render3d_st.gsl_opt_mode = render3d_st.gsl_dlss_mode; render3d_st.gsl_opt_w = gl_w; render3d_st.gsl_opt_h = gl_h
- render3d_st.gsl_rw = gl_w; render3d_st.gsl_rh = gl_h
+function gsl_optimal() -> void {
+ if gsl_opt_mode == gsl_dlss_mode and gsl_opt_w == gl_w and gsl_opt_h == gl_h { return }
+ gsl_opt_mode = gsl_dlss_mode; gsl_opt_w = gl_w; gsl_opt_h = gl_h
+ gsl_rw = gl_w; gsl_rh = gl_h
let f = gsl_fn(GSL_DLSS, "slDLSSGetOptimalSettings")
if f == null { return }
- gsl_fill_options(render3d_st, gl_w, gl_h)
+ gsl_fill_options(gl_w, gl_h)
let os = gsl_struct(64) # sl::DLSSOptimalSettings
gsl_header(os, 0xef1d, 0x0957, 0xfd58, 0x4df7, 0xb504, 0x8b69, 0xd8aa, 0x6b76, 1)
- if Vk.sl_call_pp(f, render3d_st.gsl_opts, os) == 0 {
+ if Vk.sl_call_pp(f, gsl_opts, os) == 0 {
let w = Vk.get_i32(os, 32)
let h = Vk.get_i32(os, 36)
- if w > 0 and h > 0 { render3d_st.gsl_rw = w; render3d_st.gsl_rh = h }
+ if w > 0 and h > 0 { gsl_rw = w; gsl_rh = h }
}
}
-function r3d_dlss_render_w(render3d_st: mut Render3dState) -> int { if not r3d_dlss_live(render3d_st) { return gl_w }; gsl_optimal(render3d_st); return render3d_st.gsl_rw }
-function r3d_dlss_render_h(render3d_st: mut Render3dState) -> int { if not r3d_dlss_live(render3d_st) { return gl_h }; gsl_optimal(render3d_st); return render3d_st.gsl_rh }
+function r3d_dlss_render_w() -> int { if not r3d_dlss_live() { return gl_w }; gsl_optimal(); return gsl_rw }
+function r3d_dlss_render_h() -> int { if not r3d_dlss_live() { return gl_h }; gsl_optimal(); return gsl_rh }
# a radical-inverse sample in [0, 1), float bits
function gsl_halton(i: int, b: int) -> float {
@@ -275,52 +317,52 @@ function gsl_halton(i: int, b: int) -> float {
}
# cam_begin_frame: this frame's sub-pixel offset, before the camera builds its matrices
-function gsl_jitter_frame(render3d_st: mut Render3dState) -> void {
- render3d_st.gsl_jitter_x = 0.0; render3d_st.gsl_jitter_y = 0.0; render3d_st.gsl_jpx = 0.0; render3d_st.gsl_jpy = 0.0
+function gsl_jitter_frame() -> void {
+ gsl_jitter_x = 0.0; gsl_jitter_y = 0.0; gsl_jpx = 0.0; gsl_jpy = 0.0
# a jittered frame nobody resolves shakes on screen however still the camera is: jitter only while
# this frame holds a DLSS token and the last evaluate worked. (Not gsl_fresh: gsl_frame_start has
# already taken the token and cleared it by the time the camera asks, which turned jitter off.)
- if not r3d_dlss_live(render3d_st) or not render3d_st.gsl_eval_ok or render3d_st.gsl_token == null or render3d_st.post_w <= 0 or render3d_st.post_h <= 0 { return }
+ if not r3d_dlss_live() or not gsl_eval_ok or gsl_token == null or post_w <= 0 or post_h <= 0 { return }
# DLSS wants at least 8 x (display / render)^2 phases; 32 covers performance mode
- let i = (render3d_st.gsl_frame_n % 32) + 1
- render3d_st.gsl_jpx = gsl_halton(i, 2) - 0.5
- render3d_st.gsl_jpy = gsl_halton(i, 3) - 0.5
- render3d_st.gsl_jitter_x = 2.0 * render3d_st.gsl_jpx / float(render3d_st.post_w)
- render3d_st.gsl_jitter_y = 2.0 * render3d_st.gsl_jpy / float(render3d_st.post_h)
+ let i = (gsl_frame_n % 32) + 1
+ gsl_jpx = gsl_halton(i, 2) - 0.5
+ gsl_jpy = gsl_halton(i, 3) - 0.5
+ gsl_jitter_x = 2.0 * gsl_jpx / float(post_w)
+ gsl_jitter_y = 2.0 * gsl_jpy / float(post_h)
}
# sl::Resource for one of the renderer's textures, in the layout every pass leaves them in
-function gsl_resource(render3d_st: Render3dState, at: int, tex: int) -> void {
- let p = Vk.at(render3d_st.gsl_res, at)
+function gsl_resource(at: int, tex: int) -> void {
+ let p = Vk.at(gsl_res, at)
Vk.zero(p, 112)
gsl_header(p, 0x3a9d, 0x70cf, 0x2418, 0x4b72, 0x8391, 0x13f8, 0x721c, 0x7261, 1)
- Vk.put_i64(p, 40, render3d_st.gvk_tex_image[tex])
- Vk.put_i64(p, 48, gvk_mem_handle(render3d_st, gvk_mem_id(render3d_st.gvk_tex_mem[tex])))
- Vk.put_i64(p, 56, render3d_st.gvk_tex_view[tex])
+ Vk.put_i64(p, 40, gvk_tex_image[tex])
+ Vk.put_i64(p, 48, gvk_mem_handle(gvk_mem_id(gvk_tex_mem[tex])))
+ Vk.put_i64(p, 56, gvk_tex_view[tex])
Vk.put_i32(p, 64, GSL_LAYOUT_READ)
- Vk.put_i32(p, 68, render3d_st.gvk_tex_dims_w[tex])
- Vk.put_i32(p, 72, render3d_st.gvk_tex_dims_h[tex])
- Vk.put_i32(p, 76, render3d_st.gvk_tex_vkfmt[tex])
- Vk.put_i32(p, 80, render3d_st.gvk_tex_levels[tex])
- Vk.put_i32(p, 84, render3d_st.gvk_tex_layers[tex])
- Vk.put_i32(p, 100, gvk_tex_usage(render3d_st, tex))
+ Vk.put_i32(p, 68, gvk_tex_dims_w[tex])
+ Vk.put_i32(p, 72, gvk_tex_dims_h[tex])
+ Vk.put_i32(p, 76, gvk_tex_vkfmt[tex])
+ Vk.put_i32(p, 80, gvk_tex_levels[tex])
+ Vk.put_i32(p, 84, gvk_tex_layers[tex])
+ Vk.put_i32(p, 100, gvk_tex_usage(tex))
}
# the usage gvk_tex_storage gave the image
-function gvk_tex_usage(render3d_st: Render3dState, tex: int) -> int {
- let fmt = render3d_st.gvk_tex_vkfmt[tex]
+function gvk_tex_usage(tex: int) -> int {
+ let fmt = gvk_tex_vkfmt[tex]
var usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT
if fmt == VK_FORMAT_D32_SFLOAT { return usage | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT }
usage = usage | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
- if render3d_st.gvk_tex_samples[tex] <= 1 and (fmt == VK_FORMAT_R16G16B16A16_SFLOAT or fmt == VK_FORMAT_R32_SFLOAT) { usage = usage | VK_IMAGE_USAGE_STORAGE_BIT }
+ if gvk_tex_samples[tex] <= 1 and (fmt == VK_FORMAT_R16G16B16A16_SFLOAT or fmt == VK_FORMAT_R32_SFLOAT) { usage = usage | VK_IMAGE_USAGE_STORAGE_BIT }
return usage
}
# sl::ResourceTag i, pointing at resource i
-function gsl_tag(render3d_st: Render3dState, i: int, buffer: int, w: int, h: int) -> void {
- let p = Vk.at(render3d_st.gsl_tags, i * 64)
+function gsl_tag(i: int, buffer: int, w: int, h: int) -> void {
+ let p = Vk.at(gsl_tags, i * 64)
Vk.zero(p, 64)
gsl_header(p, 0x4c6a, 0x5aad, 0xb445, 0x496c, 0x87ff, 0x1af3, 0x845b, 0xe653, 1)
- Vk.put_ptr(p, 32, Vk.at(render3d_st.gsl_res, i * 112))
+ Vk.put_ptr(p, 32, Vk.at(gsl_res, i * 112))
Vk.put_i32(p, 40, buffer)
Vk.put_i32(p, 44, 2) # eValidUntilEvaluate
Vk.put_i32(p, 56, w)
@@ -345,15 +387,15 @@ function gsl_clip_fix(m: floats) -> void {
m[14] = 0.5
}
-function gsl_constants(render3d_st: mut Render3dState) -> void {
- if render3d_st.gsl_consts == null { render3d_st.gsl_consts = gsl_struct(456) }
- let k = render3d_st.gsl_consts
+function gsl_constants() -> void {
+ if gsl_consts == null { gsl_consts = gsl_struct(456) }
+ let k = gsl_consts
Vk.zero(k, 456)
gsl_header(k, 0xdcd3, 0x5ad7, 0x4e4a, 0x4bad, 0xa90c, 0xe0c4, 0x9eb2, 0x3afe, 2)
let fix = m4_new(); let proj = m4_new(); let v2c = m4_new(); let c2v = m4_new()
let cur = m4_new(); let prev = m4_new(); let inv_cur = m4_new(); let c2p = m4_new(); let p2c = m4_new()
gsl_clip_fix(fix)
- m4_perspective(proj, render3d_st.cam_fov, render3d_st.cam_aspect, render3d_st.cam_near, render3d_st.cam_far)
+ m4_perspective(proj, cam_fov, cam_aspect, cam_near, cam_far)
m4_mul(v2c, fix, proj)
m4_inverse(c2v, v2c)
gsl_put_m4(k, 32, v2c) # cameraViewToClip (no jitter)
@@ -361,9 +403,9 @@ function gsl_constants(render3d_st: mut Render3dState) -> void {
let ident = m4_new()
m4_identity(ident)
gsl_put_m4(k, 160, ident) # clipToLensClip
- if render3d_st.gsl_prev_vp == null { render3d_st.gsl_prev_vp = m4_new(); for i in 0 .. 16 { render3d_st.gsl_prev_vp[i] = render3d_st.cam_vp_clean[i] } }
- m4_mul(cur, fix, render3d_st.cam_vp_clean)
- m4_mul(prev, fix, render3d_st.gsl_prev_vp)
+ if gsl_prev_vp == null { gsl_prev_vp = m4_new(); for i in 0 .. 16 { gsl_prev_vp[i] = cam_vp_clean[i] } }
+ m4_mul(cur, fix, cam_vp_clean)
+ m4_mul(prev, fix, gsl_prev_vp)
m4_inverse(inv_cur, cur)
m4_mul(c2p, prev, inv_cur)
m4_inverse(p2c, c2p)
@@ -372,87 +414,87 @@ function gsl_constants(render3d_st: mut Render3dState) -> void {
# the sample's offset from the pixel centre, in the image Streamline sees: row 0 at the top, so the
# vertical offset flips with it. With jy unflipped DLSS resolved the ground into concentric
# rings; with jx flipped too, thin stems doubled sideways (PC shots, 2026-09-15).
- var jx = -render3d_st.gsl_jpx
- var jy = -render3d_st.gsl_jpy
+ var jx = -gsl_jpx
+ var jy = -gsl_jpy
# R3D_DLSS_JX / R3D_DLSS_JY = -1 flip a sign, to check the convention against the picture
- if r3d_env_has(render3d_st, "R3D_DLSS_JX") and Text.to_int(r3d_env(render3d_st, "R3D_DLSS_JX")) < 0 { jx = -jx }
- if r3d_env_has(render3d_st, "R3D_DLSS_JY") and Text.to_int(r3d_env(render3d_st, "R3D_DLSS_JY")) < 0 { jy = -jy }
+ if r3d_env_has("R3D_DLSS_JX") and Text.to_int(r3d_env("R3D_DLSS_JX")) < 0 { jx = -jx }
+ if r3d_env_has("R3D_DLSS_JY") and Text.to_int(r3d_env("R3D_DLSS_JY")) < 0 { jy = -jy }
Vk.put_i32(k, 352, float_bits(jx))
Vk.put_i32(k, 356, float_bits(jy))
Vk.put_i32(k, 360, float_bits(1.0)) # mvecScale
Vk.put_i32(k, 364, float_bits(1.0))
- gsl_put_v3(k, 376, render3d_st.cam_pos)
+ gsl_put_v3(k, 376, cam_pos)
let up = floats(3)
- v3_cross(up, render3d_st.cam_right, render3d_st.cam_fwd)
+ v3_cross(up, cam_right, cam_fwd)
gsl_put_v3(k, 388, up)
- gsl_put_v3(k, 400, render3d_st.cam_right)
- gsl_put_v3(k, 412, render3d_st.cam_fwd)
- Vk.put_i32(k, 424, float_bits(render3d_st.cam_near))
- Vk.put_i32(k, 428, float_bits(render3d_st.cam_far))
- Vk.put_i32(k, 432, float_bits(render3d_st.cam_fov))
- Vk.put_i32(k, 436, float_bits(render3d_st.cam_aspect))
+ gsl_put_v3(k, 400, cam_right)
+ gsl_put_v3(k, 412, cam_fwd)
+ Vk.put_i32(k, 424, float_bits(cam_near))
+ Vk.put_i32(k, 428, float_bits(cam_far))
+ Vk.put_i32(k, 432, float_bits(cam_fov))
+ Vk.put_i32(k, 436, float_bits(cam_aspect))
Vk.put_i32(k, 440, float_bits(0.0)) # motionVectorsInvalidValue
# depthInverted, cameraMotionIncluded, motionVectors3D false; reset on a cut; not orthographic,
# not dilated, not jittered
- if render3d_st.gsl_reset { Vk.put_i32(k, 444, 256 * 256 * 256) }
+ if gsl_reset { Vk.put_i32(k, 444, 256 * 256 * 256) }
Vk.put_i32(k, 452, float_bits(40.0)) # minRelativeLinearDepthObjectSeparation
free(fix); free(proj); free(v2c); free(c2v); free(cur); free(prev); free(inv_cur); free(c2p); free(p2c); free(ident); free(up)
}
# make the DLSS targets at this frame's sizes
-function gsl_targets(render3d_st: mut Render3dState) -> void {
- if render3d_st.gsl_mv == null or render3d_st.gsl_mv.w != render3d_st.post_w or render3d_st.gsl_mv.h != render3d_st.post_h {
- if render3d_st.gsl_mv != null { target_free(render3d_st, render3d_st.gsl_mv) }
- render3d_st.gsl_mv = target_new(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_RG16F, GL_RG, GL_HALF_FLOAT, false, GL_NEAREST)
- render3d_st.gsl_reset = true
+function gsl_targets() -> void {
+ if gsl_mv == null or gsl_mv.w != post_w or gsl_mv.h != post_h {
+ if gsl_mv != null { target_free(gsl_mv) }
+ gsl_mv = target_new(post_w, post_h, GL_RG16F, GL_RG, GL_HALF_FLOAT, false, GL_NEAREST)
+ gsl_reset = true
}
- if render3d_st.gsl_out == null or render3d_st.gsl_out.w != gl_w or render3d_st.gsl_out.h != gl_h {
- if render3d_st.gsl_out != null { target_free(render3d_st, render3d_st.gsl_out) }
- render3d_st.gsl_out = target_new(render3d_st, gl_w, gl_h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
- render3d_st.gsl_reset = true
+ if gsl_out == null or gsl_out.w != gl_w or gsl_out.h != gl_h {
+ if gsl_out != null { target_free(gsl_out) }
+ gsl_out = target_new(gl_w, gl_h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
+ gsl_reset = true
}
# the LDR image the tonemap writes follows the upscaled size
- if render3d_st.post_ldr.w != gl_w or render3d_st.post_ldr.h != gl_h {
- target_free(render3d_st, render3d_st.post_ldr)
- render3d_st.post_ldr = target_new(render3d_st, gl_w, gl_h, post_ldr_fmt(render3d_st), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
- render3d_st.post_ldr_hdr = gpu_hdr_active(render3d_st)
+ if post_ldr.w != gl_w or post_ldr.h != gl_h {
+ target_free(post_ldr)
+ post_ldr = target_new(gl_w, gl_h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
+ post_ldr_hdr = gpu_hdr_active()
}
- if render3d_st.gsl_res == null { render3d_st.gsl_res = gsl_struct(4 * 112); render3d_st.gsl_tags = gsl_struct(4 * 64); render3d_st.gsl_inputs = bytes(8) }
+ if gsl_res == null { gsl_res = gsl_struct(4 * 112); gsl_tags = gsl_struct(4 * 64); gsl_inputs = bytes(8) }
}
# Upscale post_hdr into gsl_out; the colour the rest of post reads (post_hdr's own if it failed).
-function gsl_dlss_eval(render3d_st: mut Render3dState) -> int {
- gsl_targets(render3d_st)
+function gsl_dlss_eval() -> int {
+ gsl_targets()
# no motion of its own: the camera's comes from depth
- target_bind(render3d_st, render3d_st.gsl_mv)
- gpu_clear_color(render3d_st, 0.0, 0.0, 0.0, 0.0)
- gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT)
- gvk_pass_end(render3d_st)
- let cb = gvk_frame_cb(render3d_st)
- if render3d_st.gsl_set_mode != render3d_st.gsl_dlss_mode or render3d_st.gsl_set_w != gl_w or render3d_st.gsl_set_h != gl_h {
+ target_bind(gsl_mv)
+ gpu_clear_color(0.0, 0.0, 0.0, 0.0)
+ gpu_clear(GL_COLOR_BUFFER_BIT)
+ gvk_pass_end()
+ let cb = gvk_frame_cb()
+ if gsl_set_mode != gsl_dlss_mode or gsl_set_w != gl_w or gsl_set_h != gl_h {
let f = gsl_fn(GSL_DLSS, "slDLSSSetOptions")
- gsl_fill_options(render3d_st, gl_w, gl_h)
- if f != null and Vk.sl_call_pp(f, render3d_st.gsl_vp, render3d_st.gsl_opts) == 0 { render3d_st.gsl_set_mode = render3d_st.gsl_dlss_mode; render3d_st.gsl_set_w = gl_w; render3d_st.gsl_set_h = gl_h }
+ gsl_fill_options(gl_w, gl_h)
+ if f != null and Vk.sl_call_pp(f, gsl_vp, gsl_opts) == 0 { gsl_set_mode = gsl_dlss_mode; gsl_set_w = gl_w; gsl_set_h = gl_h }
}
- gsl_constants(render3d_st)
- Vk.sl_set_constants(render3d_st.gsl_consts, render3d_st.gsl_token, render3d_st.gsl_vp)
- gsl_resource(render3d_st, 0, render3d_st.post_hdr.depth); gsl_tag(render3d_st, 0, 0, render3d_st.post_w, render3d_st.post_h) # kBufferTypeDepth
- gsl_resource(render3d_st, 112, render3d_st.gsl_mv.color); gsl_tag(render3d_st, 1, 1, render3d_st.post_w, render3d_st.post_h) # kBufferTypeMotionVectors
- gsl_resource(render3d_st, 224, render3d_st.post_hdr.color); gsl_tag(render3d_st, 2, 3, render3d_st.post_w, render3d_st.post_h) # kBufferTypeScalingInputColor
- gsl_resource(render3d_st, 336, render3d_st.gsl_out.color); gsl_tag(render3d_st, 3, 4, gl_w, gl_h) # kBufferTypeScalingOutputColor
- Vk.sl_set_tag_for_frame(render3d_st.gsl_token, render3d_st.gsl_vp, render3d_st.gsl_tags, 4, cb)
- Vk.put_ptr(render3d_st.gsl_inputs, 0, render3d_st.gsl_vp)
- let r = Vk.sl_evaluate_feature(GSL_DLSS, render3d_st.gsl_token, render3d_st.gsl_inputs, 1, cb)
- for i in 0 .. 16 { render3d_st.gsl_prev_vp[i] = render3d_st.cam_vp_clean[i] }
- render3d_st.gsl_reset = false
+ gsl_constants()
+ Vk.sl_set_constants(gsl_consts, gsl_token, gsl_vp)
+ gsl_resource(0, post_hdr.depth); gsl_tag(0, 0, post_w, post_h) # kBufferTypeDepth
+ gsl_resource(112, gsl_mv.color); gsl_tag(1, 1, post_w, post_h) # kBufferTypeMotionVectors
+ gsl_resource(224, post_hdr.color); gsl_tag(2, 3, post_w, post_h) # kBufferTypeScalingInputColor
+ gsl_resource(336, gsl_out.color); gsl_tag(3, 4, gl_w, gl_h) # kBufferTypeScalingOutputColor
+ Vk.sl_set_tag_for_frame(gsl_token, gsl_vp, gsl_tags, 4, cb)
+ Vk.put_ptr(gsl_inputs, 0, gsl_vp)
+ let r = Vk.sl_evaluate_feature(GSL_DLSS, gsl_token, gsl_inputs, 1, cb)
+ for i in 0 .. 16 { gsl_prev_vp[i] = cam_vp_clean[i] }
+ gsl_reset = false
# Streamline records its own pipeline and descriptors into the command buffer; nothing needs
# forgetting, because every gvk_draw binds its pipeline, view and set afresh
- render3d_st.gsl_eval_ok = r == 0
+ gsl_eval_ok = r == 0
if r != 0 {
- if not render3d_st.gsl_said { print(`r3d: streamline: DLSS evaluate failed ({r}); drawing without it`); render3d_st.gsl_said = true }
- render3d_st.post_color_w = render3d_st.post_w; render3d_st.post_color_h = render3d_st.post_h
- return render3d_st.post_hdr.color
+ if not gsl_said { print(`r3d: streamline: DLSS evaluate failed ({r}); drawing without it`); gsl_said = true }
+ post_color_w = post_w; post_color_h = post_h
+ return post_hdr.color
}
- render3d_st.post_color_w = gl_w; render3d_st.post_color_h = gl_h
- return render3d_st.gsl_out.color
+ post_color_w = gl_w; post_color_h = gl_h
+ return gsl_out.color
}
diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic
index ecf7866c..806f1261 100644
--- a/packages/ludic.render3d/terrain.ludic
+++ b/packages/ludic.render3d/terrain.ludic
@@ -4,33 +4,88 @@
# PBR materials blended by slope, altitude and the track mask.
# ============================================================================
+var TERRAIN_HALF: int = 4096 # world half-size in metres (8 km square)
const TERRAIN_RES: int = 4096 # height-map texels per side (2 m over 8 km)
const TERRAIN_SHADOW_RES: int = 2048 # the baked height-field shadow / cloud mask
# CDLOD: the map is a quadtree of 32x32-cell patches; the leaf patch is 32 m (1 m cells)
const CD_G: int = 32 # cells per patch side
const CD_LEVELS: int = 9 # 32 m leaves .. 8192 m root
const CD_LEAVES: int = 256 # leaf patches per side (8192 / 32)
+var cd_mesh: Mesh = null
+var cd_range: floats = null # float bits: how far each level is drawn
+var cd_min: []floats = null # per level: min height of each patch (float bits)
+var cd_max: []floats = null
+var cd_draws: int = 0
+var cd_far_draws: int = 0
+var cd_near_draws: int = 0
+var ter_force_far: bool = false
+var ter_no_split: bool = false
+var ter_force_near: bool = false
+var ter_skip: bool = false
+var ter_height_tex: int = 0
+var ter_heights: floats = null # CPU copy, float bits, TERRAIN_RES^2
+var ter_reflect: bool = false # drawing the reflection: the mid mesh is plenty
+var ter_prog: int = 0
# The far tier compiled on its own (FAR_ONLY). A patch that lies entirely beyond the
# near/far split is drawn with it: same pixels, a shader small enough to run wide.
+var ter_prog_far: int = 0
+var ter_prog_near: int = 0 # the detailed tier alone (NEAR_ONLY)
+var ter_sun_prog: int = 0 # tersun.frag: sun visibility into a screen buffer
+var ter_sun_inline: bool = false # the ground's shader reads the cascades itself (Vulkan); no sun pass
+var ter_sun_tgt: Target = null # that buffer, at the frame's size
+var ter_sun_refl: Target = null # and at the reflection's, which is smaller
+var ter_sun_dumped: bool = false
+var ter_sun_checked: bool = false
+var ter_sun_done: bool = false # the caller already ran the pass (so it can time it)
+var ter_sun_pass: bool = false # selection is drawing the visibility pass
+var ter_sun_tex: int = 0
+var ter_prog_cur: int = 0 # the program currently bound during selection
+var ter_smooth: bool = false # generate the analytic test ground instead of a survey
+var ter_far_split: float = 0.0 # metres: beyond this the terrain takes its cheap far path (R3D_TFAR)
+var ter_far_band: float = 0.0 # half-width of the near/far blend (R3D_TBAND)
+var ter_snow_line: float = 0.0
# 0 dry .. 1 soaked. The game sets it from the weather and lets it dry out.
-function terrain_set_carpet(render3d_st: mut Render3dState, tex: int) -> void { render3d_st.ter_carpet = tex }
+var ter_wet: float = 0.0
+var ter_tex: words = null # 11 material textures (see terrain_bind)
+var ter_ox: float = 0.0 # world x/z of the terrain centre (float bits)
+var ter_oz: float = 0.0
+var ter_dem_tex: int = 0 # a real height map (16-bit), or 0 for the procedural valley
+var ter_dem_blur: float = 0.0 # gaussian texels applied to the survey (0 for lidar; ~3 for 30 m data)
+var ter_dem_min: float = 0.0
+var ter_dem_max: float = 0.0
+var ter_dem_base: float = 0.0
+var ter_ortho_tex: int = 0 # a photograph of the same window, draped with distance
+var ter_carpet: int = 0 # the distant-grass carpet (carpet_bake), 0 = none
+var ter_shadow_tex: int = 0 # height-field sun shadow: RG32F (lowest lit height, occluder distance)
+var ter_shadow_yaw: float = 1000000000.0 # the sky yaw it was baked for
+var ter_shadow_gen: int = -1 # the daylight generation it was baked for
+var ter_shadow_prog: int = 0
+function terrain_set_carpet(tex: int) -> void { ter_carpet = tex }
+var ter_lake_level: float = 0.0 # a lake carved into the height map (float bits; ex = 0 → none)
+var ter_lake_cx: float = 0.0
+var ter_lake_cz: float = 0.0
+var ter_lake_ex: float = 0.0
+var ter_lake_ez: float = 0.0
# Carve a lake bed below `level` inside the ellipse (cx, cz) ± (ex, ez); call before r3d_init.
-function terrain_lake(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float) -> void {
- render3d_st.ter_lake_level = level; render3d_st.ter_lake_cx = cx; render3d_st.ter_lake_cz = cz; render3d_st.ter_lake_ex = ex; render3d_st.ter_lake_ez = ez
+function terrain_lake(level: float, cx: float, cz: float, ex: float, ez: float) -> void {
+ ter_lake_level = level; ter_lake_cx = cx; ter_lake_cz = cz; ter_lake_ex = ex; ter_lake_ez = ez
}
# Whether the generator carves that bed. A height map that already carries a shaped bed
# turns it off and keeps the lake for everything else (its line, its outline, its shore):
# the carve drops the bed metres within the survey's last 0.9 m of shore, which reads as a
# cut edge on a lake whose own bank was shaped to come down gently.
-function terrain_lake_carve(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.ter_lake_carve = on }
+var ter_lake_carve: bool = true
+function terrain_lake_carve(on: bool) -> void { ter_lake_carve = on }
# The SEA's level, for the coast, the strand and the shoreline shading, when it is not the
# carved lake's. They were one number, which holds only while the lake is at sea level: a
# lake eighty metres up the valley drowned the whole coast toward its own surface. Unset,
# the sea is the lake exactly as before, so a map with one water line needs no call.
-function terrain_sea(render3d_st: mut Render3dState, level: float) -> void { render3d_st.ter_sea_level = level; render3d_st.ter_sea_set = true }
+var ter_sea_level: float = 0.0
+var ter_sea_set: bool = false
+function terrain_sea(level: float) -> void { ter_sea_level = level; ter_sea_set = true }
# the level the generator scales the coast toward (it always read the lake's, set or not)
-function ter_sea_gen(render3d_st: Render3dState) -> float { if render3d_st.ter_sea_set { return render3d_st.ter_sea_level }; return render3d_st.ter_lake_level }
+function ter_sea_gen() -> float { if ter_sea_set { return ter_sea_level }; return ter_lake_level }
# An island: land out to `r` from (cx, cz), then the terrain scaled down into the water
# over `fall` metres and on down to a shelf. Scaling rather than blending to a fixed bed is
# what makes the coastline come out of the terrain that is already there — low ground turns
@@ -38,10 +93,15 @@ function ter_sea_gen(render3d_st: Render3dState) -> float { if render3d_st.ter_s
const TER_ISLE_NONE: int = 0
const TER_ISLE_RADIAL: int = 1
const TER_ISLE_COAST: int = 2
-function terrain_island(render3d_st: mut Render3dState, cx: float, cz: float, r: float, fall: float) -> void {
- render3d_st.ter_isle_cx = cx; render3d_st.ter_isle_cz = cz; render3d_st.ter_isle_r = r; render3d_st.ter_isle_fall = fall
- render3d_st.ter_isle_mode = TER_ISLE_RADIAL
- if r == 0.0 { render3d_st.ter_isle_mode = TER_ISLE_NONE }
+var ter_isle_cx: float = 0.0
+var ter_isle_cz: float = 0.0
+var ter_isle_r: float = 0.0
+var ter_isle_fall: float = 0.0
+var ter_isle_mode: int = 0
+function terrain_island(cx: float, cz: float, r: float, fall: float) -> void {
+ ter_isle_cx = cx; ter_isle_cz = cz; ter_isle_r = r; ter_isle_fall = fall
+ ter_isle_mode = TER_ISLE_RADIAL
+ if r == 0.0 { ter_isle_mode = TER_ISLE_NONE }
}
# The other way to make an island, and the one a real survey usually wants: put the sea
# around the survey's OWN EDGE rather than cutting a circle out of the middle of it.
@@ -56,26 +116,29 @@ function terrain_island(render3d_st: mut Render3dState, cx: float, cz: float, r:
#
# The band is wobbled by low-frequency noise so the coastline is headlands and bays rather
# than the square the data arrived in. `margin = 0` leaves the survey alone.
-function terrain_coast(render3d_st: mut Render3dState, cx: float, cz: float, margin: float, fall: float) -> void {
- render3d_st.ter_isle_cx = cx; render3d_st.ter_isle_cz = cz; render3d_st.ter_isle_r = margin; render3d_st.ter_isle_fall = fall
- render3d_st.ter_isle_mode = TER_ISLE_COAST
- if margin == 0.0 { render3d_st.ter_isle_mode = TER_ISLE_NONE }
+function terrain_coast(cx: float, cz: float, margin: float, fall: float) -> void {
+ ter_isle_cx = cx; ter_isle_cz = cz; ter_isle_r = margin; ter_isle_fall = fall
+ ter_isle_mode = TER_ISLE_COAST
+ if margin == 0.0 { ter_isle_mode = TER_ISLE_NONE }
}
# Use a real place: a 16-bit PNG height map plus its elevation range (metres). The
# elevation `base` becomes y = 0; `ox`/`oz` put the map's centre in the world.
+var ter_ortho_px: pointer = null # the photograph on the CPU (RGB8, TERRAIN_RES^2) for placement rules
+var ter_ortho_w: int = 0
+var ter_ortho_c: int = 3
# the photograph's colour at world (x, z): packed 0xRRGGBB (0 outside the map)
-function terrain_ortho(render3d_st: mut Render3dState, x: float, z: float) -> int {
- if render3d_st.ter_ortho_px == null { return 0 }
- if render3d_st.ter_o_scale == 0.0 { render3d_st.ter_o_scale = float(render3d_st.ter_ortho_w) / float(render3d_st.TERRAIN_HALF * 2) }
- let scale = render3d_st.ter_o_scale
- var ix = int(Math.floor((x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale))
- var iz = int(Math.floor((z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale))
+function terrain_ortho(x: float, z: float) -> int {
+ if ter_ortho_px == null { return 0 }
+ if ter_o_scale == 0.0 { ter_o_scale = float(ter_ortho_w) / float(TERRAIN_HALF * 2) }
+ let scale = ter_o_scale
+ var ix = int(Math.floor((x - ter_ox + float(TERRAIN_HALF)) * scale))
+ var iz = int(Math.floor((z - ter_oz + float(TERRAIN_HALF)) * scale))
if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 }
- if ix > render3d_st.ter_ortho_w - 1 { ix = render3d_st.ter_ortho_w - 1 }; if iz > render3d_st.ter_ortho_w - 1 { iz = render3d_st.ter_ortho_w - 1 }
- let o = (iz * render3d_st.ter_ortho_w + ix) * render3d_st.ter_ortho_c
- return (render3d_st.ter_ortho_px[o] << 16) | (render3d_st.ter_ortho_px[o + 1] << 8) | render3d_st.ter_ortho_px[o + 2]
+ if ix > ter_ortho_w - 1 { ix = ter_ortho_w - 1 }; if iz > ter_ortho_w - 1 { iz = ter_ortho_w - 1 }
+ let o = (iz * ter_ortho_w + ix) * ter_ortho_c
+ return (ter_ortho_px[o] << 16) | (ter_ortho_px[o + 1] << 8) | ter_ortho_px[o + 2]
}
# The three classifiers below all read the same pixel. A caller that wants more than
# one should fetch the colour once with terrain_ortho() and use the *_of forms — the
@@ -105,8 +168,8 @@ function ortho_forest_of(c: int) -> float {
}
# how green the ground is in the photograph (0..1 float bits): meadow / forest vs rock, scree, water
-function terrain_ortho_green(render3d_st: mut Render3dState, x: float, z: float) -> float {
- let c = terrain_ortho(render3d_st, x, z)
+function terrain_ortho_green(x: float, z: float) -> float {
+ let c = terrain_ortho(x, z)
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
var v = g - max(r, b) # green excess
if v < 0 { v = 0 }
@@ -115,14 +178,14 @@ function terrain_ortho_green(render3d_st: mut Render3dState, x: float, z: float)
# how grey and mid-bright (scree / pebbles / bare rock) the photograph is there (0..1)
# Bare ground only where most of a 50 m neighbourhood is bare: a single 10 m trail pixel
# must not place a boulder or bar a tree.
-function terrain_ortho_scree(render3d_st: mut Render3dState, x: float, z: float) -> float {
+function terrain_ortho_scree(x: float, z: float) -> float {
var votes = 0
- for j in 0 .. 5 { for i in 0 .. 5 { if ortho_scree_of(terrain_ortho(render3d_st, x + float((i - 2) * 10), z + float((j - 2) * 10))) != 0.0 { votes += 1 } } }
+ for j in 0 .. 5 { for i in 0 .. 5 { if ortho_scree_of(terrain_ortho(x + float((i - 2) * 10), z + float((j - 2) * 10))) != 0.0 { votes += 1 } } }
if votes >= 15 { return 1.0 }
return 0.0
}
-function terrain_ortho_scree_pixel(render3d_st: mut Render3dState, x: float, z: float) -> float {
- let c = terrain_ortho(render3d_st, x, z)
+function terrain_ortho_scree_pixel(x: float, z: float) -> float {
+ let c = terrain_ortho(x, z)
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
@@ -131,8 +194,8 @@ function terrain_ortho_scree_pixel(render3d_st: mut Render3dState, x: float, z:
return 1.0
}
# dense conifer forest in the photograph: green-dominant and dark (the meadows are brighter)
-function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float) -> float {
- let c = terrain_ortho(render3d_st, x, z)
+function terrain_ortho_forest(x: float, z: float) -> float {
+ let c = terrain_ortho(x, z)
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
@@ -142,107 +205,107 @@ function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float
return 0.0
}
-function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void {
- let px = png_decode(render3d_st, path)
+function terrain_use_ortho(path: string) -> void {
+ let px = png_decode(path)
if px == null { return }
- render3d_st.ter_ortho_px = px
- render3d_st.ter_ortho_w = render3d_st.tex_w
- render3d_st.ter_ortho_c = render3d_st.tex_channels
- render3d_st.ter_ortho_tex = tex_upload(render3d_st, px, true, true)
- gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_ortho_tex)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+ ter_ortho_px = px
+ ter_ortho_w = tex_w
+ ter_ortho_c = tex_channels
+ ter_ortho_tex = tex_upload(px, true, true)
+ gpu_tex_bind(GPU_TEX2D, ter_ortho_tex)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
}
-function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
- render3d_st.ter_dem_tex = tex_load(render3d_st, path, false)
- render3d_st.ter_dem_min = emin; render3d_st.ter_dem_max = emax; render3d_st.ter_dem_base = base
- render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
- gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_dem_tex)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+function terrain_use_dem(path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
+ ter_dem_tex = tex_load(path, false)
+ ter_dem_min = emin; ter_dem_max = emax; ter_dem_base = base
+ ter_ox = ox; ter_oz = oz
+ gpu_tex_bind(GPU_TEX2D, ter_dem_tex)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
}
-function terrain_generate(render3d_st: mut Render3dState) -> void {
+function terrain_generate() -> void {
var defs = ""
- if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" }
- if render3d_st.ter_smooth { defs = "#define SMOOTH\n" }
- let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", defs)
- render3d_st.ter_height_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
- let fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, fbo)
- gpu_fb_color(render3d_st, 0, render3d_st.ter_height_tex)
- gpu_viewport(render3d_st, 0, 0, TERRAIN_RES, TERRAIN_RES)
- gpu_depth_test(render3d_st, false)
- gpu_use_program(render3d_st, p)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
- if render3d_st.ter_dem_tex != 0 {
- r3d_bind_2d(render3d_st, p, "u_dem", 0, render3d_st.ter_dem_tex)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_min"), render3d_st.ter_dem_min)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_max"), render3d_st.ter_dem_max)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_base"), render3d_st.ter_dem_base)
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
- u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_lake"), render3d_st.ter_lake_cx, render3d_st.ter_lake_cz, render3d_st.ter_lake_ex, render3d_st.ter_lake_ez)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), render3d_st.ter_lake_level)
+ if ter_dem_tex != 0 { defs = "#define DEM\n" }
+ if ter_smooth { defs = "#define SMOOTH\n" }
+ let p = r3d_program("fullscreen.vert", "heightgen.frag", defs)
+ ter_height_tex = tex_target(TERRAIN_RES, TERRAIN_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
+ let fbo = gpu_fb_new()
+ gpu_fb_bind(fbo)
+ gpu_fb_color(0, ter_height_tex)
+ gpu_viewport(0, 0, TERRAIN_RES, TERRAIN_RES)
+ gpu_depth_test(false)
+ gpu_use_program(p)
+ u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
+ if ter_dem_tex != 0 {
+ r3d_bind_2d(p, "u_dem", 0, ter_dem_tex)
+ u_f(gpu_uniform(p, "u_dem_min"), ter_dem_min)
+ u_f(gpu_uniform(p, "u_dem_max"), ter_dem_max)
+ u_f(gpu_uniform(p, "u_dem_base"), ter_dem_base)
+ u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
+ u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
+ u_f(gpu_uniform(p, "u_lake_level"), ter_lake_level)
var carve = 1.0
- if not render3d_st.ter_lake_carve { carve = 0.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_carve"), carve)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), ter_sea_gen(render3d_st))
- u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_isle"), render3d_st.ter_isle_cx, render3d_st.ter_isle_cz, render3d_st.ter_isle_r, render3d_st.ter_isle_fall)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_isle_mode"), float(render3d_st.ter_isle_mode))
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_blur"), float(render3d_st.ter_dem_blur))
+ if not ter_lake_carve { carve = 0.0 }
+ u_f(gpu_uniform(p, "u_lake_carve"), carve)
+ u_f(gpu_uniform(p, "u_sea_level"), ter_sea_gen())
+ u_f4(gpu_uniform(p, "u_isle"), ter_isle_cx, ter_isle_cz, ter_isle_r, ter_isle_fall)
+ u_f(gpu_uniform(p, "u_isle_mode"), float(ter_isle_mode))
+ u_f(gpu_uniform(p, "u_dem_blur"), float(ter_dem_blur))
}
- mesh_draw(render3d_st, render3d_st.sky_fullscreen)
+ mesh_draw(sky_fullscreen)
# second pass: R = height, GBA = the smooth surface normal, baked once (ternormal.frag)
- let raw = render3d_st.ter_height_tex
- render3d_st.ter_height_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_RGBA32F, GL_RGBA, GL_FLOAT, GL_LINEAR)
- gpu_fb_color(render3d_st, 0, render3d_st.ter_height_tex)
- let pn = r3d_program(render3d_st, "fullscreen.vert", "ternormal.frag", "")
- gpu_use_program(render3d_st, pn)
- r3d_bind_2d(render3d_st, pn, "u_src", 0, raw)
- u_f(render3d_st, gpu_uniform(render3d_st, pn, "u_half"), float(render3d_st.TERRAIN_HALF))
- mesh_draw(render3d_st, render3d_st.sky_fullscreen)
- gpu_program_free(render3d_st, pn)
- gpu_tex_free(render3d_st, raw)
+ let raw = ter_height_tex
+ ter_height_tex = tex_target(TERRAIN_RES, TERRAIN_RES, GL_RGBA32F, GL_RGBA, GL_FLOAT, GL_LINEAR)
+ gpu_fb_color(0, ter_height_tex)
+ let pn = r3d_program("fullscreen.vert", "ternormal.frag", "")
+ gpu_use_program(pn)
+ r3d_bind_2d(pn, "u_src", 0, raw)
+ u_f(gpu_uniform(pn, "u_half"), float(TERRAIN_HALF))
+ mesh_draw(sky_fullscreen)
+ gpu_program_free(pn)
+ gpu_tex_free(raw)
# read the heights back for placement
- render3d_st.ter_heights = floats(TERRAIN_RES * TERRAIN_RES)
- gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_height_tex)
- gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 4)
- gpu_tex_read(render3d_st, GPU_TEX2D, GL_RED, GL_FLOAT, data_of(render3d_st.ter_heights))
- gpu_fb_bind(render3d_st, 0)
- gpu_fb_free(render3d_st, fbo)
- gpu_program_free(render3d_st, p)
- gpu_check(render3d_st, "terrain generate")
+ ter_heights = floats(TERRAIN_RES * TERRAIN_RES)
+ gpu_tex_bind(GPU_TEX2D, ter_height_tex)
+ gpu_pixel_store(GL_PACK_ALIGNMENT, 4)
+ gpu_tex_read(GPU_TEX2D, GL_RED, GL_FLOAT, data_of(ter_heights))
+ gpu_fb_bind(0)
+ gpu_fb_free(fbo)
+ gpu_program_free(p)
+ gpu_check("terrain generate")
}
# The height field for shaders that place things on the ground (model.vert's u_ground)
-function terrain_bind_height(render3d_st: mut Render3dState, p: int) -> void {
- r3d_bind_2d(render3d_st, p, "u_ts_height", 5, render3d_st.ter_height_tex)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ts_half"), float(render3d_st.TERRAIN_HALF))
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_ts_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
+function terrain_bind_height(p: int) -> void {
+ r3d_bind_2d(p, "u_ts_height", 5, ter_height_tex)
+ u_f(gpu_uniform(p, "u_ts_half"), float(TERRAIN_HALF))
+ u_f2(gpu_uniform(p, "u_ts_origin"), ter_ox, ter_oz)
}
# Bake the height-field sun shadow (see tershadow.frag). Cheap enough to redo whenever
# the sun moves; r3d_frame calls it again when sky_set_yaw has changed the yaw.
-function terrain_bake_shadow(render3d_st: mut Render3dState) -> void {
- if render3d_st.sun_dir == null { return }
- if render3d_st.ter_shadow_tex == 0 { render3d_st.ter_shadow_tex = tex_target(render3d_st, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES, GL_RGBA32F, GL_RGBA, GL_FLOAT, GL_LINEAR) }
- if render3d_st.ter_shadow_prog == 0 { render3d_st.ter_shadow_prog = r3d_program(render3d_st, "fullscreen.vert", "tershadow.frag", "#define NOISE_ONLY\n") }
- let p = render3d_st.ter_shadow_prog
- let fbo = gpu_fb_new(render3d_st)
- gpu_fb_bind(render3d_st, fbo)
- gpu_fb_color(render3d_st, 0, render3d_st.ter_shadow_tex)
- gpu_viewport(render3d_st, 0, 0, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES)
- gpu_depth_test(render3d_st, false)
- gpu_blend(render3d_st, false)
- gpu_use_program(render3d_st, p)
- r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_sun"), render3d_st.sun_dir)
- mesh_draw(render3d_st, render3d_st.sky_fullscreen)
- gpu_fb_bind(render3d_st, 0)
- gpu_fb_free(render3d_st, fbo)
- render3d_st.ter_shadow_yaw = render3d_st.sky_yaw
- gpu_check(render3d_st, "terrain shadow bake")
+function terrain_bake_shadow() -> void {
+ if sun_dir == null { return }
+ if ter_shadow_tex == 0 { ter_shadow_tex = tex_target(TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES, GL_RGBA32F, GL_RGBA, GL_FLOAT, GL_LINEAR) }
+ if ter_shadow_prog == 0 { ter_shadow_prog = r3d_program("fullscreen.vert", "tershadow.frag", "#define NOISE_ONLY\n") }
+ let p = ter_shadow_prog
+ let fbo = gpu_fb_new()
+ gpu_fb_bind(fbo)
+ gpu_fb_color(0, ter_shadow_tex)
+ gpu_viewport(0, 0, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES)
+ gpu_depth_test(false)
+ gpu_blend(false)
+ gpu_use_program(p)
+ r3d_bind_2d(p, "u_height", 0, ter_height_tex)
+ u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
+ u_v3(gpu_uniform(p, "u_sun"), sun_dir)
+ mesh_draw(sky_fullscreen)
+ gpu_fb_bind(0)
+ gpu_fb_free(fbo)
+ ter_shadow_yaw = sky_yaw
+ gpu_check("terrain shadow bake")
}
# height at world (x, z) — float bits, bilinear over the CPU copy
@@ -250,39 +313,42 @@ function terrain_bake_shadow(render3d_st: mut Render3dState) -> void {
# a fixed-point divide — on every call, and the cover generator calls terrain_height
# five times per candidate (once directly, four more inside slope_at) across hundreds
# of thousands of candidates per chunk. Hoisted, they cost nothing.
+var ter_h_scale: float = 0.0
+var ter_o_scale: float = 0.0
-function terrain_height(render3d_st: mut Render3dState, x: float, z: float) -> float {
- if render3d_st.ter_heights == null { return 0.0 } # a plate (r3d_plate_mode): flat at y = 0
- if render3d_st.ter_h_scale == 0.0 { render3d_st.ter_h_scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) }
- let scale = render3d_st.ter_h_scale
- let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale
- let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale
+function terrain_height(x: float, z: float) -> float {
+ if ter_heights == null { return 0.0 } # a plate (r3d_plate_mode): flat at y = 0
+ if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
+ let scale = ter_h_scale
+ let fx = (x - ter_ox + float(TERRAIN_HALF)) * scale
+ let fz = (z - ter_oz + float(TERRAIN_HALF)) * scale
var ix = int(Math.floor(fx)); var iz = int(Math.floor(fz))
if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 }
if ix > TERRAIN_RES - 2 { ix = TERRAIN_RES - 2 }; if iz > TERRAIN_RES - 2 { iz = TERRAIN_RES - 2 }
let tx = Math.clamp(fx - float(ix), 0.0, 1.0)
let tz = Math.clamp(fz - float(iz), 0.0, 1.0)
- let h00 = render3d_st.ter_heights[iz * TERRAIN_RES + ix]
- let h10 = render3d_st.ter_heights[iz * TERRAIN_RES + ix + 1]
- let h01 = render3d_st.ter_heights[(iz + 1) * TERRAIN_RES + ix]
- let h11 = render3d_st.ter_heights[(iz + 1) * TERRAIN_RES + ix + 1]
+ let h00 = ter_heights[iz * TERRAIN_RES + ix]
+ let h10 = ter_heights[iz * TERRAIN_RES + ix + 1]
+ let h01 = ter_heights[(iz + 1) * TERRAIN_RES + ix]
+ let h11 = ter_heights[(iz + 1) * TERRAIN_RES + ix + 1]
return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz)
}
# the same for Q16.16 callers
-function terrain_height_fx(render3d_st: mut Render3dState, x: fixed, z: fixed) -> fixed { return fixed(terrain_height(render3d_st, float(x), float(z))) }
+function terrain_height_fx(x: fixed, z: fixed) -> fixed { return fixed(terrain_height(float(x), float(z))) }
# The height the terrain is DRAWN at: the cubic B-spline of the texels (heightSmooth in
# terrain.vert), not the bilinear read above. The two differ by up to half a metre on
# rough ground, which is the difference between a character standing on the meadow
# and one buried to the knee in it. Sixteen taps; for things that move, not for the
# thousands of placement queries a chunk makes.
-function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: float) -> float {
- if render3d_st.ter_heights == null { return 0.0 }
- if render3d_st.ter_h_scale == 0.0 { render3d_st.ter_h_scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) }
- if render3d_st.ter_bw == null { render3d_st.ter_bw = floats(8) }
- let scale = render3d_st.ter_h_scale
- let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale - 0.5
- let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale - 0.5
+var ter_bw: floats = null
+function terrain_height_smooth(x: float, z: float) -> float {
+ if ter_heights == null { return 0.0 }
+ if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
+ if ter_bw == null { ter_bw = floats(8) }
+ let scale = ter_h_scale
+ let fx = (x - ter_ox + float(TERRAIN_HALF)) * scale - 0.5
+ let fz = (z - ter_oz + float(TERRAIN_HALF)) * scale - 0.5
let ix = int(Math.floor(fx)); let iz = int(Math.floor(fz))
let tx = Math.clamp(fx - float(ix), 0.0, 1.0)
let tz = Math.clamp(fz - float(iz), 0.0, 1.0)
@@ -296,7 +362,7 @@ function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: floa
let w1 = (4.0 - 6.0 * t2 + 3.0 * t3) / 6.0
let w3 = t3 / 6.0
let w2 = 1.0 - w0 - w1 - w3
- render3d_st.ter_bw[a * 4] = w0; render3d_st.ter_bw[a * 4 + 1] = w1; render3d_st.ter_bw[a * 4 + 2] = w2; render3d_st.ter_bw[a * 4 + 3] = w3
+ ter_bw[a * 4] = w0; ter_bw[a * 4 + 1] = w1; ter_bw[a * 4 + 2] = w2; ter_bw[a * 4 + 3] = w3
}
var h = 0.0
for j in 0 .. 4 {
@@ -306,62 +372,62 @@ function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: floa
for i in 0 .. 4 {
var rx = ix - 1 + i
if rx < 0 { rx = 0 }; if rx > TERRAIN_RES - 1 { rx = TERRAIN_RES - 1 }
- row = row + render3d_st.ter_bw[i] * render3d_st.ter_heights[rz * TERRAIN_RES + rx]
+ row = row + ter_bw[i] * ter_heights[rz * TERRAIN_RES + rx]
}
- h = h + render3d_st.ter_bw[4 + j] * row
+ h = h + ter_bw[4 + j] * row
}
return h
}
-function terrain_load_textures(render3d_st: mut Render3dState) -> void {
- render3d_st.ter_tex = words(15)
- let a = render3d_st.r3d_assets + "/textures/"
- render3d_st.ter_tex[0] = tex_load(render3d_st, a + "aerial_grass_rock_diff_2k.png", true)
- render3d_st.ter_tex[1] = tex_load(render3d_st, a + "aerial_grass_rock_nor_gl_2k.png", false)
- render3d_st.ter_tex[2] = tex_load(render3d_st, a + "aerial_grass_rock_arm_2k.png", false)
- render3d_st.ter_tex[3] = tex_load(render3d_st, a + "grass_path_2_diff_2k.png", true)
- render3d_st.ter_tex[4] = tex_load(render3d_st, a + "grass_path_2_nor_gl_2k.png", false)
- render3d_st.ter_tex[5] = tex_load(render3d_st, a + "grass_path_2_arm_2k.png", false)
- render3d_st.ter_tex[6] = tex_load(render3d_st, a + "gray_rocks_diff_2k.png", true)
- render3d_st.ter_tex[7] = tex_load(render3d_st, a + "gray_rocks_nor_gl_2k.png", false)
- render3d_st.ter_tex[8] = tex_load(render3d_st, a + "gray_rocks_arm_2k.png", false)
- render3d_st.ter_tex[9] = tex_load(render3d_st, a + "snow_02_diff_2k.png", true)
- render3d_st.ter_tex[10] = tex_load(render3d_st, a + "snow_02_nor_gl_2k.png", false)
- render3d_st.ter_tex[11] = tex_load(render3d_st, a + "snow_02_arm_2k.png", false)
- render3d_st.ter_tex[12] = tex_load(render3d_st, a + "aerial_grass_rock_disp_2k.png", false)
- render3d_st.ter_tex[13] = tex_load(render3d_st, a + "cliff_side_diff_2k.png", true)
- render3d_st.ter_tex[14] = tex_load(render3d_st, a + "cliff_side_nor_gl_2k.png", false)
- if r3d_env_has(render3d_st, "R3D_TEXDBG") { for i in 0 .. 15 { print(`ter_tex[{string(i)}] = {string(render3d_st.ter_tex[i])}`) } }
+function terrain_load_textures() -> void {
+ ter_tex = words(15)
+ let a = r3d_assets + "/textures/"
+ ter_tex[0] = tex_load(a + "aerial_grass_rock_diff_2k.png", true)
+ ter_tex[1] = tex_load(a + "aerial_grass_rock_nor_gl_2k.png", false)
+ ter_tex[2] = tex_load(a + "aerial_grass_rock_arm_2k.png", false)
+ ter_tex[3] = tex_load(a + "grass_path_2_diff_2k.png", true)
+ ter_tex[4] = tex_load(a + "grass_path_2_nor_gl_2k.png", false)
+ ter_tex[5] = tex_load(a + "grass_path_2_arm_2k.png", false)
+ ter_tex[6] = tex_load(a + "gray_rocks_diff_2k.png", true)
+ ter_tex[7] = tex_load(a + "gray_rocks_nor_gl_2k.png", false)
+ ter_tex[8] = tex_load(a + "gray_rocks_arm_2k.png", false)
+ ter_tex[9] = tex_load(a + "snow_02_diff_2k.png", true)
+ ter_tex[10] = tex_load(a + "snow_02_nor_gl_2k.png", false)
+ ter_tex[11] = tex_load(a + "snow_02_arm_2k.png", false)
+ ter_tex[12] = tex_load(a + "aerial_grass_rock_disp_2k.png", false)
+ ter_tex[13] = tex_load(a + "cliff_side_diff_2k.png", true)
+ ter_tex[14] = tex_load(a + "cliff_side_nor_gl_2k.png", false)
+ if r3d_env_has("R3D_TEXDBG") { for i in 0 .. 15 { print(`ter_tex[{string(i)}] = {string(ter_tex[i])}`) } }
}
-function terrain_init(render3d_st: mut Render3dState) -> void {
- for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(render3d_st, i) }
+function terrain_init() -> void {
+ for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(i) }
}
# The terrain's start-up as steps a loading screen can show between, in terrain_init's order:
# 0 the height field and its normals (and the read-back for placement), 1 the sun's height-field
# shadow, 2 the ground's materials, 3 the patch tree and the terrain programs.
const TERRAIN_INIT_STEPS: int = 4
-function terrain_init_step(render3d_st: mut Render3dState, i: int) -> void {
- if i == 0 { terrain_generate(render3d_st); return }
- if i == 1 { terrain_bake_shadow(render3d_st); return }
- if i == 2 { terrain_load_textures(render3d_st); return }
- terrain_init_finish(render3d_st)
+function terrain_init_step(i: int) -> void {
+ if i == 0 { terrain_generate(); return }
+ if i == 1 { terrain_bake_shadow(); return }
+ if i == 2 { terrain_load_textures(); return }
+ terrain_init_finish()
}
-function terrain_init_finish(render3d_st: mut Render3dState) -> void {
- cdlod_init(render3d_st)
- render3d_st.ter_wire = r3d_env_has(render3d_st, "R3D_WIRE")
- render3d_st.ter_force_far = r3d_env_has(render3d_st, "R3D_TFARONLY")
- render3d_st.ter_no_split = r3d_env_has(render3d_st, "R3D_NOSPLIT")
- render3d_st.ter_force_near = r3d_env_has(render3d_st, "R3D_TNEARONLY")
- render3d_st.ter_skip = r3d_env_has(render3d_st, "R3D_NOTERRAIN")
+function terrain_init_finish() -> void {
+ cdlod_init()
+ ter_wire = r3d_env_has("R3D_WIRE")
+ ter_force_far = r3d_env_has("R3D_TFARONLY")
+ ter_no_split = r3d_env_has("R3D_NOSPLIT")
+ ter_force_near = r3d_env_has("R3D_TNEARONLY")
+ ter_skip = r3d_env_has("R3D_NOTERRAIN")
var defs = ""
- if render3d_st.r3d_debug_shadow { defs = "#define DEBUG_SHADOW\n" }
- if r3d_env_has(render3d_st, "R3D_DEBUG_MAT") { defs = "#define DEBUG_MAT\n" }
- if r3d_env_has(render3d_st, "R3D_DEBUG_WIND") { defs = "#define DEBUG_WIND\n" }
- if r3d_env_has(render3d_st, "R3D_DEBUG_NRM") { defs = "#define DEBUG_NRM\n" }
- if r3d_env_has(render3d_st, "R3D_DEBUG_ALB") { defs = "#define DEBUG_ALB\n" }
+ if r3d_debug_shadow { defs = "#define DEBUG_SHADOW\n" }
+ if r3d_env_has("R3D_DEBUG_MAT") { defs = "#define DEBUG_MAT\n" }
+ if r3d_env_has("R3D_DEBUG_WIND") { defs = "#define DEBUG_WIND\n" }
+ if r3d_env_has("R3D_DEBUG_NRM") { defs = "#define DEBUG_NRM\n" }
+ if r3d_env_has("R3D_DEBUG_ALB") { defs = "#define DEBUG_ALB\n" }
# Elimination profiling. Measure these by FRAME TIME (prof_ft_report), not by the
# per-pass GPU timers: this driver's timer queries attribute a pass's fragment work
# almost arbitrarily, and will happily report a pass at a tenth of its cost.
@@ -371,22 +437,22 @@ function terrain_init_finish(render3d_st: mut Render3dState) -> void {
# R3D_NOTERRAIN skips the ground entirely (what it costs); R3D_TNEARONLY / R3D_TFARONLY
# draw every patch with one tier's program (what each tier costs over a whole frame);
# R3D_NOSPLIT goes back to the single program that holds both tiers.
- if r3d_env_has(render3d_st, "R3D_TFAST") { defs = defs + "#define TFAST_" + r3d_env(render3d_st, "R3D_TFAST") + "\n" }
+ if r3d_env_has("R3D_TFAST") { defs = defs + "#define TFAST_" + r3d_env("R3D_TFAST") + "\n" }
# Vulkan reads the cascades inside the ground's own shader and draws no separate sun pass
# (terrain.frag, SUN_INLINE): one rasterisation of the patches instead of two, in the frame and in
# the reflection. R3D_SUN_PASS=1 keeps the pass there too, for comparing.
- render3d_st.ter_sun_inline = render3d_st.gpu_kind == GPU_VK and not r3d_env_has(render3d_st, "R3D_SUN_PASS")
- if render3d_st.ter_sun_inline { defs = defs + "#define SUN_INLINE\n" }
- render3d_st.ter_prog = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs)
- render3d_st.ter_prog_far = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs + "#define FAR_ONLY\n")
- render3d_st.ter_prog_near = r3d_program(render3d_st, "terrain.vert", "terrain.frag", defs + "#define NEAR_ONLY\n")
- render3d_st.ter_sun_prog = r3d_program(render3d_st, "terrain.vert", "tersun.frag", "")
- render3d_st.ter_far_split = 200.0
- if r3d_env_has(render3d_st, "R3D_TFAR") { render3d_st.ter_far_split = float(Text.to_int(r3d_env(render3d_st, "R3D_TFAR"))) }
- render3d_st.ter_far_band = 60.0
- if r3d_env_has(render3d_st, "R3D_TBAND") { render3d_st.ter_far_band = float(Text.to_int(r3d_env(render3d_st, "R3D_TBAND"))) }
- render3d_st.ter_snow_line = 880.0
- gpu_check(render3d_st, "terrain init")
+ ter_sun_inline = gpu_kind == GPU_VK and not r3d_env_has("R3D_SUN_PASS")
+ if ter_sun_inline { defs = defs + "#define SUN_INLINE\n" }
+ ter_prog = r3d_program("terrain.vert", "terrain.frag", defs)
+ ter_prog_far = r3d_program("terrain.vert", "terrain.frag", defs + "#define FAR_ONLY\n")
+ ter_prog_near = r3d_program("terrain.vert", "terrain.frag", defs + "#define NEAR_ONLY\n")
+ ter_sun_prog = r3d_program("terrain.vert", "tersun.frag", "")
+ ter_far_split = 200.0
+ if r3d_env_has("R3D_TFAR") { ter_far_split = float(Text.to_int(r3d_env("R3D_TFAR"))) }
+ ter_far_band = 60.0
+ if r3d_env_has("R3D_TBAND") { ter_far_band = float(Text.to_int(r3d_env("R3D_TBAND"))) }
+ ter_snow_line = 880.0
+ gpu_check("terrain init")
}
# draw into the current cascade with the given light view-projection
@@ -398,46 +464,46 @@ function terrain_draw_shadow(light_vp: floats) -> void {
# Every per-frame uniform of one terrain program. Both tiers are bound up front so
# selection can switch between them per patch without re-binding anything but the node.
-function terrain_bind_prog(render3d_st: mut Render3dState, p: int) -> void {
- gpu_use_program(render3d_st, p)
- r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex)
- r3d_bind_2d(render3d_st, p, "u_grass_d", 1, render3d_st.ter_tex[0]); r3d_bind_2d(render3d_st, p, "u_grass_n", 2, render3d_st.ter_tex[1]); r3d_bind_2d(render3d_st, p, "u_grass_a", 3, render3d_st.ter_tex[2])
+function terrain_bind_prog(p: int) -> void {
+ gpu_use_program(p)
+ r3d_bind_2d(p, "u_height", 0, ter_height_tex)
+ r3d_bind_2d(p, "u_grass_d", 1, ter_tex[0]); r3d_bind_2d(p, "u_grass_n", 2, ter_tex[1]); r3d_bind_2d(p, "u_grass_a", 3, ter_tex[2])
# The cliff maps went with the dead cliff sample. Binding textures for uniforms the
# shader no longer declares leaves those units pointing at nothing, which the driver
# reports as an unloadable sampler and resolves as a zero texture.
# A scene with no photograph still has to bind something valid here: sampler unit
# pointed at texture 0 is an incomplete texture, which the driver reports as
# unloadable and which poisons sampling for the rest of the unit's stage.
- var orthotex = render3d_st.ter_ortho_tex
- if orthotex == 0 { orthotex = render3d_st.ter_tex[0] }
- r3d_bind_2d(render3d_st, p, "u_ortho", 4, orthotex)
+ var orthotex = ter_ortho_tex
+ if orthotex == 0 { orthotex = ter_tex[0] }
+ r3d_bind_2d(p, "u_ortho", 4, orthotex)
var oon = 0.0
- if render3d_st.ter_ortho_tex != 0 { oon = 1.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ortho_on"), oon)
- r3d_bind_2d(render3d_st, p, "u_rock_d", 7, render3d_st.ter_tex[6]); r3d_bind_2d(render3d_st, p, "u_rock_n", 8, render3d_st.ter_tex[7]); r3d_bind_2d(render3d_st, p, "u_rock_a", 9, render3d_st.ter_tex[8])
- r3d_bind_2d(render3d_st, p, "u_snow_d", 10, render3d_st.ter_tex[9])
- if render3d_st.ter_carpet != 0 { r3d_bind_2d(render3d_st, p, "u_carpet", 11, render3d_st.ter_carpet); u_f(render3d_st, gpu_uniform(render3d_st, p, "u_carpet_on"), 1.0) }
- else { r3d_bind_2d(render3d_st, p, "u_carpet", 11, render3d_st.ter_tex[0]); u_f(render3d_st, gpu_uniform(render3d_st, p, "u_carpet_on"), 0.0) }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_texel"), 1.0 / float(TERRAIN_RES))
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_snow_line"), render3d_st.ter_snow_line)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wet"), render3d_st.ter_wet)
+ if ter_ortho_tex != 0 { oon = 1.0 }
+ u_f(gpu_uniform(p, "u_ortho_on"), oon)
+ r3d_bind_2d(p, "u_rock_d", 7, ter_tex[6]); r3d_bind_2d(p, "u_rock_n", 8, ter_tex[7]); r3d_bind_2d(p, "u_rock_a", 9, ter_tex[8])
+ r3d_bind_2d(p, "u_snow_d", 10, ter_tex[9])
+ if ter_carpet != 0 { r3d_bind_2d(p, "u_carpet", 11, ter_carpet); u_f(gpu_uniform(p, "u_carpet_on"), 1.0) }
+ else { r3d_bind_2d(p, "u_carpet", 11, ter_tex[0]); u_f(gpu_uniform(p, "u_carpet_on"), 0.0) }
+ u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
+ u_f(gpu_uniform(p, "u_texel"), 1.0 / float(TERRAIN_RES))
+ u_f(gpu_uniform(p, "u_snow_line"), ter_snow_line)
+ u_f(gpu_uniform(p, "u_wet"), ter_wet)
var lake = -100000.0
- if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), lake)
+ if ter_lake_ex != 0.0 { lake = ter_lake_level }
+ u_f(gpu_uniform(p, "u_lake_level"), lake)
# the shoreline, forest and scree gates read the sea; unset it is what they always read
var sea = lake
- if render3d_st.ter_sea_set { sea = render3d_st.ter_sea_level }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), sea)
- u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_lake"), render3d_st.ter_lake_cx, render3d_st.ter_lake_cz, render3d_st.ter_lake_ex, render3d_st.ter_lake_ez)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_split"), render3d_st.ter_far_split)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_band"), render3d_st.ter_far_band)
- sky_bind_lighting(render3d_st, p)
- shadow_bind(render3d_st, p)
- fog_bind(render3d_st, p)
+ if ter_sea_set { sea = ter_sea_level }
+ u_f(gpu_uniform(p, "u_sea_level"), sea)
+ u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
+ u_f(gpu_uniform(p, "u_far_split"), ter_far_split)
+ u_f(gpu_uniform(p, "u_far_band"), ter_far_band)
+ sky_bind_lighting(p)
+ shadow_bind(p)
+ fog_bind(p)
# GROUND IS NOT A MIRROR, and this has to come AFTER fog_bind, which hands every
# program u_spec_scale = 1. That is right for water and for a varnished prop and wrong
# for a hillside: the image-based specular lays a broad reflection of a bright sky over
@@ -448,17 +514,17 @@ function terrain_bind_prog(render3d_st: mut Render3dState, p: int) -> void {
# Set before fog_bind it measured as EXACTLY zero pixels changed, which is the same
# shape of mistake as setting r3d_fog_scale before gfx_apply: the value was right and
# something downstream put it back.
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.22)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_grid"), float(CD_G))
+ u_f(gpu_uniform(p, "u_spec_scale"), 0.22)
+ u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
+ u_f(gpu_uniform(p, "u_grid"), float(CD_G))
# The ground reads its sun visibility out of the buffer tersun.frag filled, and has no
# use for the cascade array shadow_bind just put on this unit; leaving both bound under
# one unit is undefined ground, so the array comes off first.
# (With the read inline, the array shadow_bind put there is exactly what the ground wants.)
- if not render3d_st.ter_sun_inline {
- gpu_tex_unit(render3d_st, 15)
- gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, 0)
- r3d_bind_2d(render3d_st, p, "u_sunshadow", 15, render3d_st.ter_sun_tex)
+ if not ter_sun_inline {
+ gpu_tex_unit(15)
+ gpu_tex_bind(GPU_TEX2D_ARRAY, 0)
+ r3d_bind_2d(p, "u_sunshadow", 15, ter_sun_tex)
}
}
@@ -474,117 +540,119 @@ function terrain_bind_prog(render3d_st: mut Render3dState, p: int) -> void {
# depth attachment, the sky's fullscreen quad had nothing left to fail against, and it
# painted over the whole valley — with "gl error 1286" every frame from the passes whose
# attachment now named a deleted texture.
-function terrain_sun_target(render3d_st: mut Render3dState, w: int, h: int) -> Target {
- var t = render3d_st.ter_sun_tgt
- if render3d_st.ter_reflect { t = render3d_st.ter_sun_refl }
+function terrain_sun_target(w: int, h: int) -> Target {
+ var t = ter_sun_tgt
+ if ter_reflect { t = ter_sun_refl }
if t == null or t.w != w or t.h != h {
- target_free(render3d_st, t) # owns its colour, and nothing else
- t = target_new(render3d_st, w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, false, GL_NEAREST)
- if render3d_st.ter_reflect { render3d_st.ter_sun_refl = t } else { render3d_st.ter_sun_tgt = t }
+ target_free(t) # owns its colour, and nothing else
+ t = target_new(w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, false, GL_NEAREST)
+ if ter_reflect { ter_sun_refl = t } else { ter_sun_tgt = t }
}
return t
}
# Rasterise the patches once with the small shader that reads the cascades (tersun.frag).
-function terrain_sun_pass(render3d_st: mut Render3dState, w: int, h: int, depth: int) -> Target {
- let t = terrain_sun_target(render3d_st, w, h)
+function terrain_sun_pass(w: int, h: int, depth: int) -> Target {
+ let t = terrain_sun_target(w, h)
# Attach the frame's depth afresh every pass. It is a different texture every time the
# screen-sized buffers are rebuilt — a resize, a fullscreen change — and an attachment
# naming a texture that has been deleted leaves this framebuffer incomplete, which is
# an error per draw and a pass that silently does nothing. One call a pass is cheaper
# than any scheme for noticing.
- target_bind(render3d_st, t)
- gpu_fb_depth(render3d_st, depth)
- if not render3d_st.ter_sun_checked {
- render3d_st.ter_sun_checked = true
- let st = gpu_fb_status(render3d_st)
+ target_bind(t)
+ gpu_fb_depth(depth)
+ if not ter_sun_checked {
+ ter_sun_checked = true
+ let st = gpu_fb_status()
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: sun-visibility framebuffer incomplete {st}`) }
}
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- gpu_depth_write(render3d_st, true)
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ gpu_depth_write(true)
# the depth is the frame's own and was cleared with it; only the visibility is cleared
- gpu_clear_color(render3d_st, 1.0, 1.0, 1.0, 1.0) # unshadowed where nothing is drawn
- gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT)
- let p = render3d_st.ter_sun_prog
- gpu_use_program(render3d_st, p)
- r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF))
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_origin"), render3d_st.ter_ox, render3d_st.ter_oz)
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_cam_pos"), render3d_st.cam_pos)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_grid"), float(CD_G))
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_split"), render3d_st.ter_far_split)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_far_band"), render3d_st.ter_far_band)
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_clip_y"), render3d_st.r3d_clip_y)
- shadow_bind(render3d_st, p)
- sky_bind_lighting(render3d_st, p)
- render3d_st.ter_sun_pass = true
- gpu_mesh_bind(render3d_st, render3d_st.cd_mesh)
- cdlod_select(render3d_st, CD_LEVELS - 1, 0, 0)
- render3d_st.ter_sun_pass = false
- if r3d_env_has(render3d_st, "R3D_DUMP_SUN") and not render3d_st.ter_sun_dumped and not render3d_st.ter_reflect { render3d_st.ter_sun_dumped = true; tex_dump(render3d_st, t.color, w, h, "build/dbg_sun.ppm") }
+ gpu_clear_color(1.0, 1.0, 1.0, 1.0) # unshadowed where nothing is drawn
+ gpu_clear(GL_COLOR_BUFFER_BIT)
+ let p = ter_sun_prog
+ gpu_use_program(p)
+ r3d_bind_2d(p, "u_height", 0, ter_height_tex)
+ u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
+ u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
+ u_f(gpu_uniform(p, "u_grid"), float(CD_G))
+ u_f(gpu_uniform(p, "u_far_split"), ter_far_split)
+ u_f(gpu_uniform(p, "u_far_band"), ter_far_band)
+ u_f(gpu_uniform(p, "u_clip_y"), r3d_clip_y)
+ shadow_bind(p)
+ sky_bind_lighting(p)
+ ter_sun_pass = true
+ gpu_mesh_bind(cd_mesh)
+ cdlod_select(CD_LEVELS - 1, 0, 0)
+ ter_sun_pass = false
+ if r3d_env_has("R3D_DUMP_SUN") and not ter_sun_dumped and not ter_reflect { ter_sun_dumped = true; tex_dump(t.color, w, h, "build/dbg_sun.ppm") }
return t
}
-function terrain_sun_prepare(render3d_st: mut Render3dState) -> void {
- if render3d_st.ter_sun_inline { return }
- render3d_st.ter_sun_tex = terrain_sun_pass(render3d_st, render3d_st.post_w, render3d_st.post_h, render3d_st.post_hdr.depth).color
- render3d_st.ter_sun_done = true
+function terrain_sun_prepare() -> void {
+ if ter_sun_inline { return }
+ ter_sun_tex = terrain_sun_pass(post_w, post_h, post_hdr.depth).color
+ ter_sun_done = true
}
-function terrain_draw(render3d_st: mut Render3dState) -> void {
- if render3d_st.ter_skip { return }
+function terrain_draw() -> void {
+ if ter_skip { return }
# The sun visibility first, into its own buffer; the shading pass looks it up per pixel.
# The pass binds its own framebuffer, so the caller's target is restored afterwards —
# the reflection's, or the scene's, without disturbing what is already drawn in it.
- var vw = render3d_st.post_w
- var vh = render3d_st.post_h
- if render3d_st.ter_reflect { vw = render3d_st.water_refl.w; vh = render3d_st.water_refl.h }
- if not render3d_st.ter_sun_done and not render3d_st.ter_sun_inline {
- var dep = render3d_st.post_hdr.depth
- if render3d_st.ter_reflect { dep = render3d_st.water_refl.depth }
- render3d_st.ter_sun_tex = terrain_sun_pass(render3d_st, vw, vh, dep).color
+ var vw = post_w
+ var vh = post_h
+ if ter_reflect { vw = water_refl.w; vh = water_refl.h }
+ if not ter_sun_done and not ter_sun_inline {
+ var dep = post_hdr.depth
+ if ter_reflect { dep = water_refl.depth }
+ ter_sun_tex = terrain_sun_pass(vw, vh, dep).color
}
- render3d_st.ter_sun_done = false
- if render3d_st.ter_reflect { target_bind(render3d_st, render3d_st.water_refl) }
+ ter_sun_done = false
+ if ter_reflect { target_bind(water_refl) }
else {
- target_bind(render3d_st, render3d_st.post_hdr)
- if render3d_st.post_ms_fbo != 0 { gpu_fb_bind(render3d_st, render3d_st.post_ms_fbo) }
+ target_bind(post_hdr)
+ if post_ms_fbo != 0 { gpu_fb_bind(post_ms_fbo) }
}
- gpu_depth_test(render3d_st, true)
+ gpu_depth_test(true)
# the prepass already laid this geometry's depth down: only the frontmost fragment of
# each pixel has anything to shade, and it meets that depth exactly
- gpu_depth_func(render3d_st, GL_LEQUAL)
- gpu_depth_write(render3d_st, true)
- terrain_bind_prog(render3d_st, render3d_st.ter_prog)
- terrain_bind_prog(render3d_st, render3d_st.ter_prog_far)
- terrain_bind_prog(render3d_st, render3d_st.ter_prog_near)
- render3d_st.ter_prog_cur = 0
- render3d_st.cd_draws = 0
- render3d_st.cd_far_draws = 0
- render3d_st.cd_near_draws = 0
- gpu_mesh_bind(render3d_st, render3d_st.cd_mesh)
- if render3d_st.ter_wire { gpu_wireframe(render3d_st, true) }
- cdlod_select(render3d_st, CD_LEVELS - 1, 0, 0)
- if render3d_st.ter_wire { gpu_wireframe(render3d_st, false) }
- gpu_depth_func(render3d_st, GL_LESS)
- if render3d_st.r3d_debug and not render3d_st.ter_printed { render3d_st.ter_printed = true; print(`cdlod patches drawn: {render3d_st.cd_draws} (far {render3d_st.cd_far_draws}, near {render3d_st.cd_near_draws}, band {render3d_st.cd_draws - render3d_st.cd_far_draws - render3d_st.cd_near_draws})`) }
+ gpu_depth_func(GL_LEQUAL)
+ gpu_depth_write(true)
+ terrain_bind_prog(ter_prog)
+ terrain_bind_prog(ter_prog_far)
+ terrain_bind_prog(ter_prog_near)
+ ter_prog_cur = 0
+ cd_draws = 0
+ cd_far_draws = 0
+ cd_near_draws = 0
+ gpu_mesh_bind(cd_mesh)
+ if ter_wire { gpu_wireframe(true) }
+ cdlod_select(CD_LEVELS - 1, 0, 0)
+ if ter_wire { gpu_wireframe(false) }
+ gpu_depth_func(GL_LESS)
+ if r3d_debug and not ter_printed { ter_printed = true; print(`cdlod patches drawn: {cd_draws} (far {cd_far_draws}, near {cd_near_draws}, band {cd_draws - cd_far_draws - cd_near_draws})`) }
}
+var ter_wire: bool = false
+var ter_printed: bool = false
# ---- CDLOD --------------------------------------------------------------------------
# One 32x32 patch mesh (a_xz in 0..1) drawn once per selected quadtree node; the vertex
# shader places, scales and morphs it. Levels are drawn out to cd_range[k] = 48 * 2^k m,
# so cells are 1 m within 48 m, 2 m to 96 m, 4 m to 192 m ... 256 m at the root.
-function cdlod_init(render3d_st: mut Render3dState) -> void {
- let m = gpu_mesh_new(render3d_st)
+function cdlod_init() -> void {
+ let m = gpu_mesh_new()
let n = CD_G + 1
let v = gl_floats(n * n * 2)
var k = 0
for j in 0 .. n { for i in 0 .. n { gl_put_bits(v, k, float_bits(float(i) / float(CD_G))); gl_put_bits(v, k + 1, float_bits(float(j) / float(CD_G))); k += 2 } }
- gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(n * n * 2), GPU_STATIC)
- gpu_mesh_attr(render3d_st, m, 0, 2, GPU_F32, 8, 0, false)
+ gpu_mesh_vertices(m, v, gl_bytes_of(n * n * 2), GPU_STATIC)
+ gpu_mesh_attr(m, 0, 2, GPU_F32, 8, 0, false)
free(v)
let ni = CD_G * CD_G * 6
let idx = words(ni)
@@ -597,21 +665,21 @@ function cdlod_init(render3d_st: mut Render3dState) -> void {
k += 6
}
}
- gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4)
+ gpu_mesh_indices(m, data_of(idx), ni * 4, 4)
free(idx)
m.count = ni
- gpu_mesh_done(render3d_st, m)
- render3d_st.cd_mesh = m
- render3d_st.cd_range = floats(CD_LEVELS)
+ gpu_mesh_done(m)
+ cd_mesh = m
+ cd_range = floats(CD_LEVELS)
var r = 48.0
- if r3d_env_has(render3d_st, "R3D_CD_R0") { r = float(Text.to_int(r3d_env(render3d_st, "R3D_CD_R0"))) }
- for l in 0 .. CD_LEVELS { render3d_st.cd_range[l] = r; r = r * 2.0 }
- cdlod_bounds(render3d_st)
+ if r3d_env_has("R3D_CD_R0") { r = float(Text.to_int(r3d_env("R3D_CD_R0"))) }
+ for l in 0 .. CD_LEVELS { cd_range[l] = r; r = r * 2.0 }
+ cdlod_bounds()
}
# min/max height per patch at every level, from the CPU copy of the height field
-function cdlod_bounds(render3d_st: mut Render3dState) -> void {
- render3d_st.cd_min = new []floats; render3d_st.cd_max = new []floats
+function cdlod_bounds() -> void {
+ cd_min = new []floats; cd_max = new []floats
let t = TERRAIN_RES / CD_LEAVES # texels per leaf patch side
var n = CD_LEAVES
var lo = floats(n * n); var hi = floats(n * n)
@@ -622,14 +690,14 @@ function cdlod_bounds(render3d_st: mut Render3dState) -> void {
let ty = min(j * t + y, TERRAIN_RES - 1)
for x in 0 .. t + 1 {
let tx = min(i * t + x, TERRAIN_RES - 1)
- let h = render3d_st.ter_heights[ty * TERRAIN_RES + tx]
+ let h = ter_heights[ty * TERRAIN_RES + tx]
mn = Math.min(mn, h); mx = Math.max(mx, h)
}
}
lo[j * n + i] = mn; hi[j * n + i] = mx
}
}
- push(render3d_st.cd_min, lo); push(render3d_st.cd_max, hi)
+ push(cd_min, lo); push(cd_max, hi)
while n > 1 {
let m = n / 2
let plo = floats(m * m); let phi = floats(m * m)
@@ -640,7 +708,7 @@ function cdlod_bounds(render3d_st: mut Render3dState) -> void {
phi[j * m + i] = Math.max(Math.max(hi[a], hi[a + 1]), Math.max(hi[a + n], hi[a + n + 1]))
}
}
- push(render3d_st.cd_min, plo); push(render3d_st.cd_max, phi)
+ push(cd_min, plo); push(cd_max, phi)
lo = plo; hi = phi; n = m
}
}
@@ -649,86 +717,86 @@ function cdlod_bounds(render3d_st: mut Render3dState) -> void {
# map, so a leaf is (2 * TERRAIN_HALF) / CD_LEAVES - 32 m only on the 8192 m map the numbers
# were chosen for. Placing patches at a fixed 32 m put every bound in the wrong place on any
# other TERRAIN_HALF.
-function cd_size(render3d_st: Render3dState, level: int) -> float { return float(32 << level) * (float(render3d_st.TERRAIN_HALF * 2) / 8192.0) }
+function cd_size(level: int) -> float { return float(32 << level) * (float(TERRAIN_HALF * 2) / 8192.0) }
# does the patch's box come within r of the camera?
-function cd_within(render3d_st: Render3dState, x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
- let dx = Math.max(Math.max(x0 - render3d_st.cam_pos[0], render3d_st.cam_pos[0] - (x0 + size)), 0.0)
- let dz = Math.max(Math.max(z0 - render3d_st.cam_pos[2], render3d_st.cam_pos[2] - (z0 + size)), 0.0)
- let dy = Math.max(Math.max(ymin - render3d_st.cam_pos[1], render3d_st.cam_pos[1] - ymax), 0.0)
+function cd_within(x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
+ let dx = Math.max(Math.max(x0 - cam_pos[0], cam_pos[0] - (x0 + size)), 0.0)
+ let dz = Math.max(Math.max(z0 - cam_pos[2], cam_pos[2] - (z0 + size)), 0.0)
+ let dy = Math.max(Math.max(ymin - cam_pos[1], cam_pos[1] - ymax), 0.0)
return dx * dx + dz * dz + dy * dy < r * r
}
# is the patch's box entirely inside r of the camera? (its farthest corner is within r)
-function cd_inside(render3d_st: Render3dState, x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
+function cd_inside(x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
let x1 = x0 + size
let z1 = z0 + size
- let dx = Math.max(Math.abs(render3d_st.cam_pos[0] - x0), Math.abs(render3d_st.cam_pos[0] - x1))
- let dz = Math.max(Math.abs(render3d_st.cam_pos[2] - z0), Math.abs(render3d_st.cam_pos[2] - z1))
- let dy = Math.max(Math.abs(render3d_st.cam_pos[1] - ymin), Math.abs(render3d_st.cam_pos[1] - ymax))
+ let dx = Math.max(Math.abs(cam_pos[0] - x0), Math.abs(cam_pos[0] - x1))
+ let dz = Math.max(Math.abs(cam_pos[2] - z0), Math.abs(cam_pos[2] - z1))
+ let dy = Math.max(Math.abs(cam_pos[1] - ymin), Math.abs(cam_pos[1] - ymax))
return dx * dx + dz * dz + dy * dy < r * r
}
-function cdlod_draw(render3d_st: mut Render3dState, level: int, ix: int, iz: int) -> void {
- let size = cd_size(render3d_st, level)
- let x0 = render3d_st.ter_ox - float(render3d_st.TERRAIN_HALF) + float(ix) * size
- let z0 = render3d_st.ter_oz - float(render3d_st.TERRAIN_HALF) + float(iz) * size
+function cdlod_draw(level: int, ix: int, iz: int) -> void {
+ let size = cd_size(level)
+ let x0 = ter_ox - float(TERRAIN_HALF) + float(ix) * size
+ let z0 = ter_oz - float(TERRAIN_HALF) + float(iz) * size
# Which tier can run inside this patch. A patch that never comes within the split takes
# the cheap tier at every pixel; one that lies wholly inside it takes the detailed tier
# at every pixel. Only a patch that straddles the band needs the program that holds both
# and cross-fades between them — and there are few of those, one ring of them.
- if render3d_st.ter_sun_pass {
- let t = ter_scratch(render3d_st)
+ if ter_sun_pass {
+ let t = ter_scratch()
t[0] = x0; t[1] = z0; t[2] = size
- u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.ter_sun_prog, "u_node"), t)
+ u_v3(gpu_uniform(ter_sun_prog, "u_node"), t)
var st0 = 0.0
- if level > 0 { st0 = render3d_st.cd_range[level - 1] }
- u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.ter_sun_prog, "u_morph"), Math.lerp(st0, render3d_st.cd_range[level], 0.7), render3d_st.cd_range[level])
- gpu_draw_bound_elements(render3d_st, render3d_st.cd_mesh)
+ if level > 0 { st0 = cd_range[level - 1] }
+ u_f2(gpu_uniform(ter_sun_prog, "u_morph"), Math.lerp(st0, cd_range[level], 0.7), cd_range[level])
+ gpu_draw_bound_elements(cd_mesh)
return
}
let n = CD_LEAVES >> level
- let ymin = render3d_st.cd_min[level][iz * n + ix]
- let ymax = render3d_st.cd_max[level][iz * n + ix]
- var p = render3d_st.ter_prog
- if not cd_within(render3d_st, x0, z0, size, ymin, ymax, render3d_st.ter_far_split + render3d_st.ter_far_band) { p = render3d_st.ter_prog_far }
- else if cd_inside(render3d_st, x0, z0, size, ymin, ymax, render3d_st.ter_far_split - render3d_st.ter_far_band) { p = render3d_st.ter_prog_near }
- if render3d_st.ter_force_far { p = render3d_st.ter_prog_far }
- if render3d_st.ter_no_split { p = render3d_st.ter_prog }
- if render3d_st.ter_force_near { p = render3d_st.ter_prog_near }
- if p == render3d_st.ter_prog_far { render3d_st.cd_far_draws += 1 }
- if p == render3d_st.ter_prog_near { render3d_st.cd_near_draws += 1 }
- if p != render3d_st.ter_prog_cur { gpu_use_program(render3d_st, p); render3d_st.ter_prog_cur = p }
- let t = ter_scratch(render3d_st)
+ let ymin = cd_min[level][iz * n + ix]
+ let ymax = cd_max[level][iz * n + ix]
+ var p = ter_prog
+ if not cd_within(x0, z0, size, ymin, ymax, ter_far_split + ter_far_band) { p = ter_prog_far }
+ else if cd_inside(x0, z0, size, ymin, ymax, ter_far_split - ter_far_band) { p = ter_prog_near }
+ if ter_force_far { p = ter_prog_far }
+ if ter_no_split { p = ter_prog }
+ if ter_force_near { p = ter_prog_near }
+ if p == ter_prog_far { cd_far_draws += 1 }
+ if p == ter_prog_near { cd_near_draws += 1 }
+ if p != ter_prog_cur { gpu_use_program(p); ter_prog_cur = p }
+ let t = ter_scratch()
t[0] = x0; t[1] = z0; t[2] = size
- u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_node"), t)
+ u_v3(gpu_uniform(p, "u_node"), t)
var start = 0.0
- if level > 0 { start = render3d_st.cd_range[level - 1] }
- start = Math.lerp(start, render3d_st.cd_range[level], 0.7)
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_morph"), start, render3d_st.cd_range[level])
- gpu_draw_bound_elements(render3d_st, render3d_st.cd_mesh)
- render3d_st.cd_draws += 1
+ if level > 0 { start = cd_range[level - 1] }
+ start = Math.lerp(start, cd_range[level], 0.7)
+ u_f2(gpu_uniform(p, "u_morph"), start, cd_range[level])
+ gpu_draw_bound_elements(cd_mesh)
+ cd_draws += 1
}
# Strugar's selection: a node is drawn at its own level unless it is close enough to need
# its children, in which case each child either selects itself or is drawn at this level.
-function cdlod_select(render3d_st: mut Render3dState, level: int, ix: int, iz: int) -> bool {
+function cdlod_select(level: int, ix: int, iz: int) -> bool {
let n = CD_LEAVES >> level
- let size = cd_size(render3d_st, level)
- let x0 = render3d_st.ter_ox - float(render3d_st.TERRAIN_HALF) + float(ix) * size
- let z0 = render3d_st.ter_oz - float(render3d_st.TERRAIN_HALF) + float(iz) * size
- let ymin = render3d_st.cd_min[level][iz * n + ix]
- let ymax = render3d_st.cd_max[level][iz * n + ix]
- if not cd_within(render3d_st, x0, z0, size, ymin, ymax, render3d_st.cd_range[level]) { return false }
+ let size = cd_size(level)
+ let x0 = ter_ox - float(TERRAIN_HALF) + float(ix) * size
+ let z0 = ter_oz - float(TERRAIN_HALF) + float(iz) * size
+ let ymin = cd_min[level][iz * n + ix]
+ let ymax = cd_max[level][iz * n + ix]
+ if not cd_within(x0, z0, size, ymin, ymax, cd_range[level]) { return false }
let half = size * 0.5
let cy = (ymin + ymax) * 0.5
let rad = Math.sqrt(half * half * 2.0 + (ymax - cy) * (ymax - cy) * 1.0)
- if not cam_sphere_visible(render3d_st, x0 + half, cy, z0 + half, rad + 2.0) { return true }
- if level == 0 { cdlod_draw(render3d_st, 0, ix, iz); return true }
- if not cd_within(render3d_st, x0, z0, size, ymin, ymax, render3d_st.cd_range[level - 1]) { cdlod_draw(render3d_st, level, ix, iz); return true }
+ if not cam_sphere_visible(x0 + half, cy, z0 + half, rad + 2.0) { return true }
+ if level == 0 { cdlod_draw(0, ix, iz); return true }
+ if not cd_within(x0, z0, size, ymin, ymax, cd_range[level - 1]) { cdlod_draw(level, ix, iz); return true }
for c in 0 .. 4 {
let cx = ix * 2 + (c & 1); let cz = iz * 2 + (c >> 1)
- if not cdlod_select(render3d_st, level - 1, cx, cz) { cdlod_draw(render3d_st, level - 1, cx, cz) }
+ if not cdlod_select(level - 1, cx, cz) { cdlod_draw(level - 1, cx, cz) }
}
return true
}
@@ -739,39 +807,40 @@ function cdlod_select(render3d_st: mut Render3dState, level: int, ix: int, iz: i
# so a different survey can be generated in its place without restarting the process. What
# belongs to the process stays: the patch mesh, the material textures, the programs, the
# frame-sized targets and the shadow texture, whose size does not depend on the map.
-function terrain_unload(render3d_st: mut Render3dState) -> void {
- if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex); render3d_st.ter_height_tex = 0 }
- if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex); render3d_st.ter_dem_tex = 0 }
- if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex); render3d_st.ter_ortho_tex = 0 }
- if render3d_st.ter_heights != null { free(render3d_st.ter_heights); render3d_st.ter_heights = null }
- if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null }
- render3d_st.ter_ortho_w = 0
- if render3d_st.cd_min != null {
- for i in 0 .. len(render3d_st.cd_min) { free(render3d_st.cd_min[i]); free(render3d_st.cd_max[i]) }
- render3d_st.cd_min = null; render3d_st.cd_max = null
+function terrain_unload() -> void {
+ if ter_height_tex != 0 { gpu_tex_free(ter_height_tex); ter_height_tex = 0 }
+ if ter_dem_tex != 0 { gpu_tex_free(ter_dem_tex); ter_dem_tex = 0 }
+ if ter_ortho_tex != 0 { gpu_tex_free(ter_ortho_tex); ter_ortho_tex = 0 }
+ if ter_heights != null { free(ter_heights); ter_heights = null }
+ if ter_ortho_px != null { free(ter_ortho_px); ter_ortho_px = null }
+ ter_ortho_w = 0
+ if cd_min != null {
+ for i in 0 .. len(cd_min) { free(cd_min[i]); free(cd_max[i]) }
+ cd_min = null; cd_max = null
}
# derived from TERRAIN_HALF and cached on first use: stale ones would keep the old size
- render3d_st.ter_h_scale = 0.0; render3d_st.ter_o_scale = 0.0
- render3d_st.ter_carpet = 0
- render3d_st.ter_shadow_gen = -1; render3d_st.ter_shadow_yaw = 1000000000.0
+ ter_h_scale = 0.0; ter_o_scale = 0.0
+ ter_carpet = 0
+ ter_shadow_gen = -1; ter_shadow_yaw = 1000000000.0
}
# Generate another map in place. Call terrain_lake / terrain_coast / terrain_island for it
# first - the generator reads them - then this. `half` is the new TERRAIN_HALF in metres.
# An empty `dem` generates the analytic ground (with ter_smooth) and an empty `ortho` drapes
# no photograph. The shadow is rebaked and the patch bounds rebuilt before it returns.
-function terrain_reload(render3d_st: mut Render3dState, dem: string, emin: float, emax: float, base: float, ox: float, oz: float, ortho: string, half: int) -> void {
- terrain_unload(render3d_st)
- render3d_st.TERRAIN_HALF = half
- render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
- if len(dem) > 0 { terrain_use_dem(render3d_st, dem, emin, emax, base, ox, oz) }
- if len(ortho) > 0 { terrain_use_ortho(render3d_st, ortho) }
- terrain_generate(render3d_st)
- terrain_bake_shadow(render3d_st)
- cdlod_bounds(render3d_st)
- gpu_check(render3d_st, "terrain reload")
+function terrain_reload(dem: string, emin: float, emax: float, base: float, ox: float, oz: float, ortho: string, half: int) -> void {
+ terrain_unload()
+ TERRAIN_HALF = half
+ ter_ox = ox; ter_oz = oz
+ if len(dem) > 0 { terrain_use_dem(dem, emin, emax, base, ox, oz) }
+ if len(ortho) > 0 { terrain_use_ortho(ortho) }
+ terrain_generate()
+ terrain_bake_shadow()
+ cdlod_bounds()
+ gpu_check("terrain reload")
}
-function ter_scratch(render3d_st: mut Render3dState) -> floats {
- if render3d_st.ter_scr == null { render3d_st.ter_scr = floats(16) }
- return render3d_st.ter_scr
+var ter_scr: floats = null
+function ter_scratch() -> floats {
+ if ter_scr == null { ter_scr = floats(16) }
+ return ter_scr
}
diff --git a/packages/ludic.render3d/texture.ludic b/packages/ludic.render3d/texture.ludic
index 8e6211ec..50b18e26 100644
--- a/packages/ludic.render3d/texture.ludic
+++ b/packages/ludic.render3d/texture.ludic
@@ -11,32 +11,38 @@
const GL_TEXTURE_MAX_ANISOTROPY_EXT: int = 0x84FE
+var tex_w: int = 0 # the last decoded image
+var tex_h: int = 0
+var tex_channels: int = 0
+var tex_depth: int = 0 # bits per sample (8 or 16)
+var tex_file_len: int = 0
+var tex_anisotropy: fixed = 16.0
# The anisotropic filtering level for every mipmapped texture, loaded or not: 1 (off), 2, 4, 8
# or 16. Textures uploaded before a change are updated in place - on OpenGL by setting the
# parameter again, on Vulkan by rewriting the record the sampler cache reads - so a settings
# screen can offer it live instead of on the next start.
-function r3d_set_anisotropy(render3d_st: mut Render3dState, level: int) -> void {
+function r3d_set_anisotropy(level: int) -> void {
var a: fixed = 1.0
if level >= 2 { a = 2.0 }
if level >= 4 { a = 4.0 }
if level >= 8 { a = 8.0 }
if level >= 16 { a = 16.0 }
- if a == render3d_st.tex_anisotropy { return }
- render3d_st.tex_anisotropy = a
- if render3d_st.gpu_tx == null { return }
- let keep = render3d_st.gpu_bound_2d
- for t in 1 .. render3d_st.gpu_tx_cap {
+ if a == tex_anisotropy { return }
+ tex_anisotropy = a
+ if gpu_tx == null { return }
+ let keep = gpu_bound_2d
+ for t in 1 .. gpu_tx_cap {
let o = t * GPU_TX_W
# a 2D texture with mipmaps that was given a level when it was uploaded
- if render3d_st.gpu_tx[o] != GPU_TEX2D or render3d_st.gpu_tx[o + 10] != 1 or render3d_st.gpu_tx[o + 11] == 0 { continue }
- gpu_tex_bind(render3d_st, GPU_TEX2D, t)
- gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, a)
+ if gpu_tx[o] != GPU_TEX2D or gpu_tx[o + 10] != 1 or gpu_tx[o + 11] == 0 { continue }
+ gpu_tex_bind(GPU_TEX2D, t)
+ gpu_tex_paramf(GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, a)
}
- if keep > 0 { gpu_tex_bind(render3d_st, GPU_TEX2D, keep) }
+ if keep > 0 { gpu_tex_bind(GPU_TEX2D, keep) }
}
-function r3d_read_file(render3d_st: mut Render3dState, path: pointer) -> pointer {
+function r3d_read_file(path: pointer) -> pointer {
let f = file_open(path, "rb")
if f == null { return null }
file_seek(f, 0, 2)
@@ -46,7 +52,7 @@ function r3d_read_file(render3d_st: mut Render3dState, path: pointer) -> pointer
let buf = bytes(n + 8)
file_read(f, buf, n)
file_close(f)
- render3d_st.tex_file_len = n
+ tex_file_len = n
return buf
}
@@ -112,10 +118,10 @@ function png_unfilter(raw: pointer, cur: int, prev: int, stride: int, fbpp: int,
}
}
-function png_decode(render3d_st: mut Render3dState, path: pointer) -> pointer {
- let d = r3d_read_file(render3d_st, path)
+function png_decode(path: pointer) -> pointer {
+ let d = r3d_read_file(path)
if d == null { print(`png: cannot read {path}`); return null }
- let size = render3d_st.tex_file_len
+ let size = tex_file_len
if size < 8 or d[0] != 137 or d[1] != 80 { free(d); print(`png: not a png: {path}`); return null }
var w = 0; var h = 0; var bd = 0; var ct = 0
let plte = bytes(768)
@@ -182,16 +188,16 @@ function png_decode(render3d_st: mut Render3dState, path: pointer) -> pointer {
free(raw)
}
free(plte)
- render3d_st.tex_w = w; render3d_st.tex_h = h; render3d_st.tex_channels = channels; render3d_st.tex_depth = bd
+ tex_w = w; tex_h = h; tex_channels = channels; tex_depth = bd
return out
}
# Edge padding for cut-out atlases: pixels darker than `thresh` (the unused
# background) take the mean of their lit neighbours, repeated `passes` times, so
# mipmaps and bilinear taps never pull black into the blades. 8-bit RGB/RGBA only.
-function tex_dilate(render3d_st: Render3dState, px: pointer, thresh: int, passes: int) -> void {
- if render3d_st.tex_depth != 8 or render3d_st.tex_channels < 3 { return }
- let w = render3d_st.tex_w; let h = render3d_st.tex_h; let c = render3d_st.tex_channels
+function tex_dilate(px: pointer, thresh: int, passes: int) -> void {
+ if tex_depth != 8 or tex_channels < 3 { return }
+ let w = tex_w; let h = tex_h; let c = tex_channels
let mask = bytes(w * h)
var i = 0
while i < w * h { let o = i * c; if px[o] + px[o + 1] + px[o + 2] < thresh { mask[i] = 1 } else { mask[i] = 0 }; i += 1 }
@@ -221,91 +227,101 @@ function tex_dilate(render3d_st: Render3dState, px: pointer, thresh: int, passes
}
# Upload the last-decoded samples as a 2D texture. srgb: colour data (8-bit only).
-function tex_upload(render3d_st: mut Render3dState, px: pointer, srgb: bool, mips: bool) -> int {
- let id = gpu_tex_new(render3d_st)
- gpu_tex_bind(render3d_st, GPU_TEX2D, id)
+function tex_upload(px: pointer, srgb: bool, mips: bool) -> int {
+ let id = gpu_tex_new()
+ gpu_tex_bind(GPU_TEX2D, id)
var fmt = GL_RED
- if render3d_st.tex_channels == 2 { fmt = GL_RG }
- if render3d_st.tex_channels == 3 { fmt = GL_RGB }
- if render3d_st.tex_channels == 4 { fmt = GL_RGBA }
+ if tex_channels == 2 { fmt = GL_RG }
+ if tex_channels == 3 { fmt = GL_RGB }
+ if tex_channels == 4 { fmt = GL_RGBA }
var ifmt = GL_R8
var ty = GL_UNSIGNED_BYTE
- if render3d_st.tex_depth == 16 {
+ if tex_depth == 16 {
ty = GL_UNSIGNED_SHORT
ifmt = GL_R16
- if render3d_st.tex_channels == 2 { ifmt = GL_RG16 }
- if render3d_st.tex_channels == 3 { ifmt = GL_RGB16 }
- if render3d_st.tex_channels == 4 { ifmt = GL_RGBA16 }
- gpu_pixel_store(render3d_st, GL_UNPACK_SWAP_BYTES, 1)
+ if tex_channels == 2 { ifmt = GL_RG16 }
+ if tex_channels == 3 { ifmt = GL_RGB16 }
+ if tex_channels == 4 { ifmt = GL_RGBA16 }
+ gpu_pixel_store(GL_UNPACK_SWAP_BYTES, 1)
} else {
- if render3d_st.tex_channels == 2 { ifmt = GL_RG8 }
- if render3d_st.tex_channels == 3 { ifmt = GL_RGB8; if srgb { ifmt = GL_SRGB8 } }
- if render3d_st.tex_channels == 4 { ifmt = GL_RGBA8; if srgb { ifmt = GL_SRGB8_ALPHA8 } }
- gpu_pixel_store(render3d_st, GL_UNPACK_SWAP_BYTES, 0)
+ if tex_channels == 2 { ifmt = GL_RG8 }
+ if tex_channels == 3 { ifmt = GL_RGB8; if srgb { ifmt = GL_SRGB8 } }
+ if tex_channels == 4 { ifmt = GL_RGBA8; if srgb { ifmt = GL_SRGB8_ALPHA8 } }
+ gpu_pixel_store(GL_UNPACK_SWAP_BYTES, 0)
}
- gpu_pixel_store(render3d_st, GL_UNPACK_ALIGNMENT, 1)
- gpu_tex_image2d(render3d_st, ifmt, render3d_st.tex_w, render3d_st.tex_h, fmt, ty, px)
- gpu_pixel_store(render3d_st, GL_UNPACK_SWAP_BYTES, 0)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ gpu_pixel_store(GL_UNPACK_ALIGNMENT, 1)
+ gpu_tex_image2d(ifmt, tex_w, tex_h, fmt, ty, px)
+ gpu_pixel_store(GL_UNPACK_SWAP_BYTES, 0)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
if mips {
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
- gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_mips(GPU_TEX2D)
+ gpu_tex_paramf(GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, tex_anisotropy)
} else {
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
}
return id
}
# Load a PNG as a mipmapped, anisotropic texture (0 on failure). srgb for albedo.
-function tex_load(render3d_st: mut Render3dState, path: pointer, srgb: bool) -> int { return tex_load_ex(render3d_st, path, srgb, 0) }
+function tex_load(path: pointer, srgb: bool) -> int { return tex_load_ex(path, srgb, 0) }
# ... with `dilate` passes of edge padding for a cut-out atlas (0 = none)
-function tex_load_ex(render3d_st: mut Render3dState, path: pointer, srgb: bool, dilate: int) -> int {
- let px = png_decode(render3d_st, path)
+function tex_load_ex(path: pointer, srgb: bool, dilate: int) -> int {
+ let px = png_decode(path)
if px == null { return 0 }
- if dilate > 0 { tex_dilate(render3d_st, px, 60, dilate) }
- let id = tex_upload(render3d_st, px, srgb, true)
+ if dilate > 0 { tex_dilate(px, 60, dilate) }
+ let id = tex_upload(px, srgb, true)
free(px)
- tex_note_size(render3d_st, id)
+ tex_note_size(id)
return id
}
# the size each loaded texture was, by id: a nine-slice or a UI image asks
-function tex_note_size(render3d_st: mut Render3dState, id: int) -> void {
- push(render3d_st.tex_size_ids, id)
- push(render3d_st.tex_size_ws, render3d_st.tex_w)
- push(render3d_st.tex_size_hs, render3d_st.tex_h)
+var tex_size_ids: []int = new []int
+var tex_size_ws: []int = new []int
+var tex_size_hs: []int = new []int
+function tex_note_size(id: int) -> void {
+ push(tex_size_ids, id)
+ push(tex_size_ws, tex_w)
+ push(tex_size_hs, tex_h)
}
-function tex_width(render3d_st: Render3dState, id: int) -> int {
- for i in 0 .. len(render3d_st.tex_size_ids) {
- if render3d_st.tex_size_ids[i] == id { return render3d_st.tex_size_ws[i] }
+function tex_width(id: int) -> int {
+ for i in 0 .. len(tex_size_ids) {
+ if tex_size_ids[i] == id { return tex_size_ws[i] }
}
return 0
}
-function tex_height(render3d_st: Render3dState, id: int) -> int {
- for i in 0 .. len(render3d_st.tex_size_ids) {
- if render3d_st.tex_size_ids[i] == id { return render3d_st.tex_size_hs[i] }
+function tex_height(id: int) -> int {
+ for i in 0 .. len(tex_size_ids) {
+ if tex_size_ids[i] == id { return tex_size_hs[i] }
}
return 0
}
# A small solid-colour fallback texture (linear rgb 0..255), for missing maps.
-function tex_solid(render3d_st: mut Render3dState, r: int, g: int, b: int, a: int) -> int {
+function tex_solid(r: int, g: int, b: int, a: int) -> int {
let px = bytes(16)
for i in 0 .. 4 { px[i * 4] = r; px[i * 4 + 1] = g; px[i * 4 + 2] = b; px[i * 4 + 3] = a }
- render3d_st.tex_w = 2; render3d_st.tex_h = 2; render3d_st.tex_channels = 4; render3d_st.tex_depth = 8
- let id = tex_upload(render3d_st, px, false, false)
+ tex_w = 2; tex_h = 2; tex_channels = 4; tex_depth = 8
+ let id = tex_upload(px, false, false)
free(px)
return id
}
# ---- Radiance .hdr (RGBE, new-style RLE) -> RGB float bits -------------------------
+var hdr_max_lum: float = 0.0 # float bits of the brightest texel (sun finding)
+var hdr_max_x: int = 0
+var hdr_max_y: int = 0
+var hdr_sun_r: float = 0.0 # irradiance (float bits) of everything above the IBL clip: the sun
+var hdr_sun_g: float = 0.0
+var hdr_sun_b: float = 0.0
+var hdr_clip: float = 0.0 # float bits; texels above this (per channel) feed the sun, not the IBL
-function hdr_decode(render3d_st: mut Render3dState, path: pointer) -> floats {
- let d = r3d_read_file(render3d_st, path)
+function hdr_decode(path: pointer) -> floats {
+ let d = r3d_read_file(path)
if d == null { print(`hdr: cannot read {path}`); return null }
- let size = render3d_st.tex_file_len
+ let size = tex_file_len
# header: lines until an empty line, then "-Y h +X w"
var i = 0
var blank = false
@@ -324,7 +340,7 @@ function hdr_decode(render3d_st: mut Render3dState, path: pointer) -> floats {
let out = floats(w * h * 3)
let line = bytes(w * 4)
var maxl = 0.0
- if render3d_st.hdr_clip == 0.0 { render3d_st.hdr_clip = 20.0 }
+ if hdr_clip == 0.0 { hdr_clip = 20.0 }
var sr = 0.0; var sg = 0.0; var sb = 0.0
var skye = 0.0 # sky irradiance on an upward face (clipped part only)
let dphi = 2.0 * PI / float(w)
@@ -369,60 +385,61 @@ function hdr_decode(render3d_st: mut Render3dState, path: pointer) -> floats {
out[o + 1] = Math.min(vg, 60000.0)
out[o + 2] = Math.min(vb, 60000.0)
let lum = vr + vg + vb
- if maxl < lum { maxl = lum; render3d_st.hdr_max_x = x; render3d_st.hdr_max_y = y }
- if y < h / 2 { skye = skye + Math.min(vg, render3d_st.hdr_clip) * Math.cos((float(y) + 0.5) * dth) * domega }
- if render3d_st.hdr_clip < vg or render3d_st.hdr_clip < vr {
- sr = sr + Math.max(vr - render3d_st.hdr_clip, 0.0) * domega
- sg = sg + Math.max(vg - render3d_st.hdr_clip, 0.0) * domega
- sb = sb + Math.max(vb - render3d_st.hdr_clip, 0.0) * domega
+ if maxl < lum { maxl = lum; hdr_max_x = x; hdr_max_y = y }
+ if y < h / 2 { skye = skye + Math.min(vg, hdr_clip) * Math.cos((float(y) + 0.5) * dth) * domega }
+ if hdr_clip < vg or hdr_clip < vr {
+ sr = sr + Math.max(vr - hdr_clip, 0.0) * domega
+ sg = sg + Math.max(vg - hdr_clip, 0.0) * domega
+ sb = sb + Math.max(vb - hdr_clip, 0.0) * domega
}
}
}
y += 1
}
free(line); free(d)
- render3d_st.tex_w = w; render3d_st.tex_h = h; render3d_st.tex_channels = 3; render3d_st.tex_depth = 32
- render3d_st.hdr_max_lum = maxl
- render3d_st.hdr_sun_r = sr; render3d_st.hdr_sun_g = sg; render3d_st.hdr_sun_b = sb
+ tex_w = w; tex_h = h; tex_channels = 3; tex_depth = 32
+ hdr_max_lum = maxl
+ hdr_sun_r = sr; hdr_sun_g = sg; hdr_sun_b = sb
print(`hdr: peak/1000 {fixed(maxl / 1000.0)} sky irradiance(up) {fixed(skye)} sun irradiance {fixed(sg)} (Q16.16 = /65536)`)
return out
}
# Load an equirectangular .hdr as an RGB16F texture with mips (clamped in v).
-function tex_load_hdr(render3d_st: mut Render3dState, path: pointer) -> int {
- let px = hdr_decode(render3d_st, path)
+function tex_load_hdr(path: pointer) -> int {
+ let px = hdr_decode(path)
if px == null { return 0 }
- let id = gpu_tex_new(render3d_st)
- gpu_tex_bind(render3d_st, GPU_TEX2D, id)
- gpu_pixel_store(render3d_st, GL_UNPACK_ALIGNMENT, 4)
- gpu_tex_image2d(render3d_st, GL_RGB16F, render3d_st.tex_w, render3d_st.tex_h, GL_RGB, GL_FLOAT, data_of(px))
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
+ let id = gpu_tex_new()
+ gpu_tex_bind(GPU_TEX2D, id)
+ gpu_pixel_store(GL_UNPACK_ALIGNMENT, 4)
+ gpu_tex_image2d(GL_RGB16F, tex_w, tex_h, GL_RGB, GL_FLOAT, data_of(px))
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_mips(GPU_TEX2D)
free(px)
return id
}
# An empty render-target texture of the given internal format (no mips, clamped).
-function tex_target(render3d_st: mut Render3dState, w: int, h: int, ifmt: int, fmt: int, ty: int, filter: int) -> int {
- let id = gpu_tex_new(render3d_st)
- gpu_tex_bind(render3d_st, GPU_TEX2D, id)
- gpu_tex_image2d(render3d_st, ifmt, w, h, fmt, ty, null)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MAG_FILTER, filter)
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, filter)
+function tex_target(w: int, h: int, ifmt: int, fmt: int, ty: int, filter: int) -> int {
+ let id = gpu_tex_new()
+ gpu_tex_bind(GPU_TEX2D, id)
+ gpu_tex_image2d(ifmt, w, h, fmt, ty, null)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MAG_FILTER, filter)
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, filter)
return id
}
+var tex_dump_alpha: bool = false
# Debug: the brightest texel of an RGBA float texture and where it is.
-function tex_max(render3d_st: mut Render3dState, tex: int, w: int, h: int, tag: pointer) -> void {
+function tex_max(tex: int, w: int, h: int, tag: pointer) -> void {
let buf = floats(w * h * 4)
- gpu_tex_bind(render3d_st, GPU_TEX2D, tex)
- gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 4)
- gpu_tex_read(render3d_st, GPU_TEX2D, GL_RGBA, GL_FLOAT, data_of(buf))
+ gpu_tex_bind(GPU_TEX2D, tex)
+ gpu_pixel_store(GL_PACK_ALIGNMENT, 4)
+ gpu_tex_read(GPU_TEX2D, GL_RGBA, GL_FLOAT, data_of(buf))
var best = 0.0; var bx = 0; var by = 0
var i = 0
while i < w * h {
@@ -438,20 +455,20 @@ function tex_max(render3d_st: mut Render3dState, tex: int, w: int, h: int, tag:
free(buf)
}
# Debug: write a 2D texture's level 0 (RGBA8, alpha dropped) as a binary PPM.
-function tex_dump(render3d_st: mut Render3dState, tex: int, w: int, h: int, path: pointer) -> void {
+function tex_dump(tex: int, w: int, h: int, path: pointer) -> void {
let f = file_open(path, "wb")
if f == null { return }
let buf = bytes(w * h * 4)
- gpu_tex_bind(render3d_st, GPU_TEX2D, tex)
- gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 1)
- gpu_tex_read(render3d_st, GPU_TEX2D, GL_RGBA, GL_UNSIGNED_BYTE, buf)
+ gpu_tex_bind(GPU_TEX2D, tex)
+ gpu_pixel_store(GL_PACK_ALIGNMENT, 1)
+ gpu_tex_read(GPU_TEX2D, GL_RGBA, GL_UNSIGNED_BYTE, buf)
let hdr = `P6\n{w} {h}\n255\n`
file_write(f, hdr, len(hdr))
let row = bytes(w * 3)
for y in 0 .. h {
for x in 0 .. w {
row[x * 3] = buf[(y * w + x) * 4]; row[x * 3 + 1] = buf[(y * w + x) * 4 + 1]; row[x * 3 + 2] = buf[(y * w + x) * 4 + 2]
- if render3d_st.tex_dump_alpha { let a = buf[(y * w + x) * 4 + 3]; row[x * 3] = a; row[x * 3 + 1] = a; row[x * 3 + 2] = a }
+ if tex_dump_alpha { let a = buf[(y * w + x) * 4 + 3]; row[x * 3] = a; row[x * 3 + 1] = a; row[x * 3 + 2] = a }
}
file_write(f, row, w * 3)
}
@@ -461,10 +478,10 @@ function tex_dump(render3d_st: mut Render3dState, tex: int, w: int, h: int, path
# A binary PPM (P6, what Gl.screenshot writes) as an RGB8 texture, box-filtered down by
# `shrink` (a photo thumbnail); 0 when the file is missing.
-function tex_load_ppm(render3d_st: mut Render3dState, path: pointer, shrink: int) -> int {
- let d = r3d_read_file(render3d_st, path)
+function tex_load_ppm(path: pointer, shrink: int) -> int {
+ let d = r3d_read_file(path)
if d == null { return 0 }
- let size = render3d_st.tex_file_len
+ let size = tex_file_len
var i = 2
var w = 0; var h = 0; var mx = 0
var field = 0
@@ -494,8 +511,8 @@ function tex_load_ppm(render3d_st: mut Render3dState, path: pointer, shrink: int
}
}
free(d)
- render3d_st.tex_w = ow; render3d_st.tex_h = oh; render3d_st.tex_channels = 3; render3d_st.tex_depth = 8
- let id = tex_upload(render3d_st, px, true, false)
+ tex_w = ow; tex_h = oh; tex_channels = 3; tex_depth = 8
+ let id = tex_upload(px, true, false)
free(px)
return id
}
diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic
index e85b01d0..126a7bfd 100644
--- a/packages/ludic.render3d/water.ludic
+++ b/packages/ludic.render3d/water.ludic
@@ -5,83 +5,108 @@
# tinted body read from the scene depth, and soft shores.
# ============================================================================
+var water_mesh: Mesh = null
+var water_prog: int = 0
+var water_level: float = 0.0
+var water_cx: float = 0.0
+var water_cz: float = 0.0
+var water_ex: float = 0.0
+var water_ez: float = 0.0
+var water_on: bool = false
# Where a body is standing in the water and how hard it is disturbing it. The game sets it;
# strength 0 means nobody is in the water and the whole term is skipped.
+var wt_wade_x: float = 0.0
+var wt_wade_z: float = 0.0
+var wt_wade_s: float = 0.0
+var water_refl: Target = null # the world mirrored in the surface, half resolution
+var water_refl_div: int = 2 # R3D_REFLDIV overrides: 2 = half res, 4 = quarter
+var water_saved: floats = null # the real camera's matrices, restored after the pass
+var water_saved_vp: floats = null # views of its second and third matrices
+var water_saved_ivp: floats = null
+var water_dumped: bool = false
# Several still-water planes, each at its own level over its own bounds: the sea round an
# island and a lake a hundred metres above it cannot be one surface. Each draws the same way;
# only one - the first added with `reflect` - gets the planar reflection pass, because every
# mirrored plane is another full scene pass. water_level / water_cx ... mirror that one, so
# code written against a single plane still reads the surface that reflects.
const WATER_MAX: int = 8
+var wb_n: int = 0
+var wb_level: floats = null
+var wb_cx: floats = null
+var wb_cz: floats = null
+var wb_ex: floats = null
+var wb_ez: floats = null
+var wb_reflect: words = null
+var wb_primary: int = -1 # the body the reflection pass mirrors, or -1
# Render the scene through a camera mirrored in the water plane into water_refl,
# clipping everything below the surface; terrain, scattered layers and sky.
-function water_reflection_pass(render3d_st: mut Render3dState) -> void {
- if render3d_st.wb_primary < 0 { return } # no body reflects: nothing to mirror
- if render3d_st.water_refl == null {
- if r3d_env_has(render3d_st, "R3D_REFLDIV") { render3d_st.water_refl_div = Text.to_int(r3d_env(render3d_st, "R3D_REFLDIV")) }
- if render3d_st.water_refl != null { target_free(render3d_st, render3d_st.water_refl) }
+function water_reflection_pass() -> void {
+ if wb_primary < 0 { return } # no body reflects: nothing to mirror
+ if water_refl == null {
+ if r3d_env_has("R3D_REFLDIV") { water_refl_div = Text.to_int(r3d_env("R3D_REFLDIV")) }
+ if water_refl != null { target_free(water_refl) }
# sized from the scene target, not the window: with a render scale below 1 the frame
# this reflection is composited into is smaller than the drawable, and a reflection
# rendered at the window's size would be paying for pixels the water never samples
- render3d_st.water_refl = target_new(render3d_st, render3d_st.post_w / render3d_st.water_refl_div, render3d_st.post_h / render3d_st.water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
- render3d_st.water_saved = floats(16 * 4 + 3)
- render3d_st.water_saved_vp = view(render3d_st.water_saved, 16, 16)
- render3d_st.water_saved_ivp = view(render3d_st.water_saved, 32, 16)
+ water_refl = target_new(post_w / water_refl_div, post_h / water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
+ water_saved = floats(16 * 4 + 3)
+ water_saved_vp = view(water_saved, 16, 16)
+ water_saved_ivp = view(water_saved, 32, 16)
}
# save the camera
- m4_copy(render3d_st.water_saved, render3d_st.cam_view)
- m4_copy(render3d_st.water_saved_vp, render3d_st.cam_vp)
- m4_copy(render3d_st.water_saved_ivp, render3d_st.cam_inv_vp)
- let sx = render3d_st.cam_pos[0]; let sy = render3d_st.cam_pos[1]; let sz = render3d_st.cam_pos[2]
+ m4_copy(water_saved, cam_view)
+ m4_copy(water_saved_vp, cam_vp)
+ m4_copy(water_saved_ivp, cam_inv_vp)
+ let sx = cam_pos[0]; let sy = cam_pos[1]; let sz = cam_pos[2]
# the mirrored camera: view' = view * R, R reflecting y about the surface (y' = 2L - y).
# R has determinant -1, so the winding flips (front faces culled below) and the image
# lands exactly where the main camera's pixels expect the reflection.
- let eye = v3_new(sx, 2.0 * render3d_st.water_level - sy, sz)
+ let eye = v3_new(sx, 2.0 * water_level - sy, sz)
let refl = m4_new()
refl[5] = -1.0
- refl[13] = 2.0 * render3d_st.water_level
+ refl[13] = 2.0 * water_level
let mv = floats(16)
- m4_mul(mv, render3d_st.water_saved, refl)
- m4_copy(render3d_st.cam_view, mv)
+ m4_mul(mv, water_saved, refl)
+ m4_copy(cam_view, mv)
free(mv); free(refl)
let fwd = words(3); let up = words(3); let at = words(3)
- m4_mul(render3d_st.cam_vp, render3d_st.cam_proj, render3d_st.cam_view)
- m4_inverse(render3d_st.cam_inv_vp, render3d_st.cam_vp)
- v3_copy(render3d_st.cam_pos, eye)
- render3d_st.r3d_clip_y = render3d_st.water_level - 0.05
- target_bind(render3d_st, render3d_st.water_refl)
- gpu_depth_test(render3d_st, true)
- gpu_depth_func(render3d_st, GL_LESS)
- gpu_depth_write(render3d_st, true)
- gpu_cull(render3d_st, true)
- gpu_cull_face(render3d_st, GL_FRONT) # the mirror flips the winding
- gpu_clear_color(render3d_st, 0.0, 0.0, 0.0, 1.0)
- gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
- render3d_st.sc_freeze = true
- let sb = render3d_st.sc_skip_blade
- render3d_st.sc_skip_blade = true # blades are invisible at this scale in a reflection
- render3d_st.ter_reflect = true
- prof_cpu_mark(render3d_st, "reflection setup")
- terrain_draw(render3d_st)
- prof_cpu_mark(render3d_st, "reflection terrain")
- r3d_scene_draw(render3d_st)
- prof_cpu_mark(render3d_st, "reflection scene")
- r3d_draw_sky(render3d_st)
- prof_cpu_mark(render3d_st, "reflection sky")
- render3d_st.ter_reflect = false
- render3d_st.sc_skip_blade = sb
- render3d_st.sc_freeze = false
- gpu_cull_face(render3d_st, GL_BACK)
- if r3d_env_has(render3d_st, "R3D_DUMP_REFL") and not render3d_st.water_dumped { render3d_st.water_dumped = true; tex_dump(render3d_st, render3d_st.water_refl.color, render3d_st.water_refl.w, render3d_st.water_refl.h, "build/dbg_refl.ppm") }
+ m4_mul(cam_vp, cam_proj, cam_view)
+ m4_inverse(cam_inv_vp, cam_vp)
+ v3_copy(cam_pos, eye)
+ r3d_clip_y = water_level - 0.05
+ target_bind(water_refl)
+ gpu_depth_test(true)
+ gpu_depth_func(GL_LESS)
+ gpu_depth_write(true)
+ gpu_cull(true)
+ gpu_cull_face(GL_FRONT) # the mirror flips the winding
+ gpu_clear_color(0.0, 0.0, 0.0, 1.0)
+ gpu_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ sc_freeze = true
+ let sb = sc_skip_blade
+ sc_skip_blade = true # blades are invisible at this scale in a reflection
+ ter_reflect = true
+ prof_cpu_mark("reflection setup")
+ terrain_draw()
+ prof_cpu_mark("reflection terrain")
+ r3d_scene_draw()
+ prof_cpu_mark("reflection scene")
+ r3d_draw_sky()
+ prof_cpu_mark("reflection sky")
+ ter_reflect = false
+ sc_skip_blade = sb
+ sc_freeze = false
+ gpu_cull_face(GL_BACK)
+ if r3d_env_has("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, water_refl.w, water_refl.h, "build/dbg_refl.ppm") }
# restore
- render3d_st.r3d_clip_y = -2147483600.0
- m4_copy(render3d_st.cam_view, render3d_st.water_saved)
- m4_copy(render3d_st.cam_vp, render3d_st.water_saved_vp)
- m4_copy(render3d_st.cam_inv_vp, render3d_st.water_saved_ivp)
- v3_set(render3d_st.cam_pos, sx, sy, sz)
+ r3d_clip_y = -2147483600.0
+ m4_copy(cam_view, water_saved)
+ m4_copy(cam_vp, water_saved_vp)
+ m4_copy(cam_inv_vp, water_saved_ivp)
+ v3_set(cam_pos, sx, sy, sz)
free(eye); free(fwd); free(up); free(at)
- gpu_fb_bind(render3d_st, 0)
+ gpu_fb_bind(0)
}
# Is any of the mirrored water in view? The reflection pass is a second copy of the terrain, the
@@ -93,32 +118,39 @@ function water_reflection_pass(render3d_st: mut Render3dState) -> void {
# asking, so it errs on the side of running. R3D_REFL_ALWAYS=1 runs it on every frame, for comparing.
# Mac, the five shot viewpoints: looking down at the meadow (c) 225 reflection draws -> none; every view
# with the lake in it unchanged, byte-identical frames.
-function water_reflect_visible(render3d_st: mut Render3dState) -> bool {
- if render3d_st.wb_primary < 0 { return false }
- if render3d_st.wb_refl_always < 0 { render3d_st.wb_refl_always = 0; if r3d_env_has(render3d_st, "R3D_REFL_ALWAYS") { render3d_st.wb_refl_always = 1 } }
- if render3d_st.wb_refl_always == 1 or render3d_st.ter_heights == null { return true }
- if render3d_st.wb_ncells < 0 { let t0 = gl_now_us(); water_cells_build(render3d_st); render3d_st.wb_build_us = gl_now_us() - t0 }
- if render3d_st.wb_dbg < 0 { render3d_st.wb_dbg = 0; if r3d_env_has(render3d_st, "R3D_REFL_DBG") { render3d_st.wb_dbg = 1 } }
- if render3d_st.wb_dbg == 1 {
+var wb_cells: floats = null # x0, z0, x1, z1 of each rectangle with water showing
+var wb_ncells: int = -1 # -1: not built for the current mirrored body
+var wb_blocks: words = null # x0, z0, x1, z1, first cell, cells past the last - of each block
+var wb_nblocks: int = 0
+var wb_refl_always: int = -1
+var wb_dbg: int = -1
+var wb_build_us: long = 0 # how long the last water_cells_build took (R3D_REFL_DBG prints it)
+function water_reflect_visible() -> bool {
+ if wb_primary < 0 { return false }
+ if wb_refl_always < 0 { wb_refl_always = 0; if r3d_env_has("R3D_REFL_ALWAYS") { wb_refl_always = 1 } }
+ if wb_refl_always == 1 or ter_heights == null { return true }
+ if wb_ncells < 0 { let t0 = gl_now_us(); water_cells_build(); wb_build_us = gl_now_us() - t0 }
+ if wb_dbg < 0 { wb_dbg = 0; if r3d_env_has("R3D_REFL_DBG") { wb_dbg = 1 } }
+ if wb_dbg == 1 {
# R3D_REFL_DBG: once, what the test is made of and how much of it is in view
- render3d_st.wb_dbg = 2
+ wb_dbg = 2
var seen = 0
var hidden = 0
- for i in 0 .. render3d_st.wb_ncells { if wb_cell_visible(render3d_st, i) { seen += 1; if wb_cell_occluded(render3d_st, i) { hidden += 1 } } }
- print(`water reflection test: {render3d_st.wb_ncells} wet rectangles in {render3d_st.wb_nblocks} blocks (built in {render3d_st.wb_build_us} us), {seen} in view, {hidden} of them behind the ground`)
+ for i in 0 .. wb_ncells { if wb_cell_visible(i) { seen += 1; if wb_cell_occluded(i) { hidden += 1 } } }
+ print(`water reflection test: {wb_ncells} wet rectangles in {wb_nblocks} blocks (built in {wb_build_us} us), {seen} in view, {hidden} of them behind the ground`)
}
# A rectangle in the frustum can still be behind the ground or dry where it shows: standing on a shore
# above the lake and looking down at your feet puts water inside the view and none of it on screen.
# The first 16 in view are checked against the height field; past that the pass simply runs.
var checked = 0
- for b in 0 .. render3d_st.wb_nblocks {
+ for b in 0 .. wb_nblocks {
let o = b * 6
- if not wb_rect_visible(render3d_st, float_from_bits(render3d_st.wb_blocks[o]), float_from_bits(render3d_st.wb_blocks[o + 1]), float_from_bits(render3d_st.wb_blocks[o + 2]), float_from_bits(render3d_st.wb_blocks[o + 3])) { continue }
- for i in render3d_st.wb_blocks[o + 4] .. render3d_st.wb_blocks[o + 5] {
- if not wb_cell_visible(render3d_st, i) { continue }
+ if not wb_rect_visible(float_from_bits(wb_blocks[o]), float_from_bits(wb_blocks[o + 1]), float_from_bits(wb_blocks[o + 2]), float_from_bits(wb_blocks[o + 3])) { continue }
+ for i in wb_blocks[o + 4] .. wb_blocks[o + 5] {
+ if not wb_cell_visible(i) { continue }
if checked >= 16 { return true }
checked += 1
- if not wb_cell_occluded(render3d_st, i) { return true }
+ if not wb_cell_occluded(i) { return true }
}
}
return false
@@ -126,82 +158,82 @@ function water_reflect_visible(render3d_st: mut Render3dState) -> bool {
# A flat rectangle at the water's level against the view's side planes: hidden when all four corners
# lie outside one plane. (A bounding sphere was the first version: a 156 m cell's sphere reached into
# the view from a lake the camera had its back to, and the pass never skipped.)
-function wb_rect_visible(render3d_st: Render3dState, x0: float, z0: float, x1: float, z1: float) -> bool {
- if render3d_st.cam_planes == null { return true }
+function wb_rect_visible(x0: float, z0: float, x1: float, z1: float) -> bool {
+ if cam_planes == null { return true }
for p in 0 .. 4 {
let q = p * 4
- let a = render3d_st.cam_planes[q]; let c = render3d_st.cam_planes[q + 2]
- let by = render3d_st.cam_planes[q + 1] * render3d_st.water_level + render3d_st.cam_planes[q + 3]
+ let a = cam_planes[q]; let c = cam_planes[q + 2]
+ let by = cam_planes[q + 1] * water_level + cam_planes[q + 3]
let ax0 = a * x0; let ax1 = a * x1; let cz0 = c * z0; let cz1 = c * z1
if ax0 + cz0 + by < 0.0 and ax1 + cz0 + by < 0.0 and ax0 + cz1 + by < 0.0 and ax1 + cz1 + by < 0.0 { return false }
}
return true
}
-function wb_cell_visible(render3d_st: Render3dState, i: int) -> bool {
+function wb_cell_visible(i: int) -> bool {
let o = i * 4
- return wb_rect_visible(render3d_st, render3d_st.wb_cells[o], render3d_st.wb_cells[o + 1], render3d_st.wb_cells[o + 2], render3d_st.wb_cells[o + 3])
+ return wb_rect_visible(wb_cells[o], wb_cells[o + 1], wb_cells[o + 2], wb_cells[o + 3])
}
# Is the water at (x, z) out of sight - dry ground there, outside the frame, or behind the ground? The
# sight line is sampled at seven points short of both ends, with half a metre (and a little more with
# distance) of margin, so a far ridge the drawn terrain rounds off never hides real water. A camera at
# or under the surface hides nothing behind the ground.
-function wb_point_hidden(render3d_st: mut Render3dState, x: float, z: float) -> bool {
- if terrain_height(render3d_st, x, z) > render3d_st.water_level + 0.05 { return true }
- if not cam_sphere_visible(render3d_st, x, render3d_st.water_level, z, 0.5) { return true }
- let ey = render3d_st.cam_pos[1]
- if not (ey > render3d_st.water_level) { return false }
- let dx = x - render3d_st.cam_pos[0]; let dz = z - render3d_st.cam_pos[2]
+function wb_point_hidden(x: float, z: float) -> bool {
+ if terrain_height(x, z) > water_level + 0.05 { return true }
+ if not cam_sphere_visible(x, water_level, z, 0.5) { return true }
+ let ey = cam_pos[1]
+ if not (ey > water_level) { return false }
+ let dx = x - cam_pos[0]; let dz = z - cam_pos[2]
let margin = 0.5 + Math.sqrt(dx * dx + dz * dz) * 0.004
for k in 1 .. 8 {
let t = float(k) / 8.0
- let sy = ey + (render3d_st.water_level - ey) * t
- if terrain_height(render3d_st, render3d_st.cam_pos[0] + dx * t, render3d_st.cam_pos[2] + dz * t) > sy + margin { return true }
+ let sy = ey + (water_level - ey) * t
+ if terrain_height(cam_pos[0] + dx * t, cam_pos[2] + dz * t) > sy + margin { return true }
}
return false
}
# every point the wet test sampled (the centre and four near the corners) is out of sight
-function wb_cell_occluded(render3d_st: mut Render3dState, i: int) -> bool {
+function wb_cell_occluded(i: int) -> bool {
let o = i * 4
- let x0 = render3d_st.wb_cells[o]; let z0 = render3d_st.wb_cells[o + 1]; let x1 = render3d_st.wb_cells[o + 2]; let z1 = render3d_st.wb_cells[o + 3]
+ let x0 = wb_cells[o]; let z0 = wb_cells[o + 1]; let x1 = wb_cells[o + 2]; let z1 = wb_cells[o + 3]
let cx = (x0 + x1) * 0.5; let cz = (z0 + z1) * 0.5
let ox = (x1 - x0) * 0.45; let oz = (z1 - z0) * 0.45
- if not wb_point_hidden(render3d_st, cx, cz) { return false }
- if not wb_point_hidden(render3d_st, cx - ox, cz - oz) { return false }
- if not wb_point_hidden(render3d_st, cx + ox, cz - oz) { return false }
- if not wb_point_hidden(render3d_st, cx - ox, cz + oz) { return false }
- return wb_point_hidden(render3d_st, cx + ox, cz + oz)
+ if not wb_point_hidden(cx, cz) { return false }
+ if not wb_point_hidden(cx - ox, cz - oz) { return false }
+ if not wb_point_hidden(cx + ox, cz - oz) { return false }
+ if not wb_point_hidden(cx - ox, cz + oz) { return false }
+ return wb_point_hidden(cx + ox, cz + oz)
}
-function wb_cell_add(render3d_st: mut Render3dState, x0: float, z0: float, x1: float, z1: float) -> void {
- let o = render3d_st.wb_ncells * 4
- render3d_st.wb_cells[o] = x0; render3d_st.wb_cells[o + 1] = z0; render3d_st.wb_cells[o + 2] = x1; render3d_st.wb_cells[o + 3] = z1
- render3d_st.wb_ncells += 1
+function wb_cell_add(x0: float, z0: float, x1: float, z1: float) -> void {
+ let o = wb_ncells * 4
+ wb_cells[o] = x0; wb_cells[o + 1] = z0; wb_cells[o + 2] = x1; wb_cells[o + 3] = z1
+ wb_ncells += 1
}
-function wb_block_add(render3d_st: mut Render3dState, x0: float, z0: float, x1: float, z1: float, first: int) -> void {
- if render3d_st.wb_ncells <= first { return }
- let o = render3d_st.wb_nblocks * 6
- render3d_st.wb_blocks[o] = float_bits(x0); render3d_st.wb_blocks[o + 1] = float_bits(z0); render3d_st.wb_blocks[o + 2] = float_bits(x1); render3d_st.wb_blocks[o + 3] = float_bits(z1)
- render3d_st.wb_blocks[o + 4] = first; render3d_st.wb_blocks[o + 5] = render3d_st.wb_ncells
- render3d_st.wb_nblocks += 1
+function wb_block_add(x0: float, z0: float, x1: float, z1: float, first: int) -> void {
+ if wb_ncells <= first { return }
+ let o = wb_nblocks * 6
+ wb_blocks[o] = float_bits(x0); wb_blocks[o + 1] = float_bits(z0); wb_blocks[o + 2] = float_bits(x1); wb_blocks[o + 3] = float_bits(z1)
+ wb_blocks[o + 4] = first; wb_blocks[o + 5] = wb_ncells
+ wb_nblocks += 1
}
-function wb_wet(render3d_st: mut Render3dState, x: float, z: float, off: float) -> bool {
- if terrain_height(render3d_st, x, z) < render3d_st.water_level { return true }
+function wb_wet(x: float, z: float, off: float) -> bool {
+ if terrain_height(x, z) < water_level { return true }
if off == 0.0 { return false }
- if terrain_height(render3d_st, x - off, z - off) < render3d_st.water_level { return true }
- if terrain_height(render3d_st, x + off, z - off) < render3d_st.water_level { return true }
- if terrain_height(render3d_st, x - off, z + off) < render3d_st.water_level { return true }
- return terrain_height(render3d_st, x + off, z + off) < render3d_st.water_level
+ if terrain_height(x - off, z - off) < water_level { return true }
+ if terrain_height(x + off, z - off) < water_level { return true }
+ if terrain_height(x - off, z + off) < water_level { return true }
+ return terrain_height(x + off, z + off) < water_level
}
# Over the height map, 32 m rectangles (at most 512 a side) tested at five points and kept in blocks of
# 8 x 8, so a block out of view skips its rectangles in one test - looking away from the water was a
# walk of every rectangle, every frame. The body beyond the map (a sea runs far past it) is 64 x 64
# coarse rectangles tested at their centre, in one last block, which only ever matter kilometres away.
const WB_BLOCK: int = 8
-function water_cells_build(render3d_st: mut Render3dState) -> void {
- let bx0 = render3d_st.water_cx - render3d_st.water_ex; let bx1 = render3d_st.water_cx + render3d_st.water_ex
- let bz0 = render3d_st.water_cz - render3d_st.water_ez; let bz1 = render3d_st.water_cz + render3d_st.water_ez
- let th = float(render3d_st.TERRAIN_HALF)
- let tx0 = Math.max(bx0, render3d_st.ter_ox - th); let tx1 = Math.min(bx1, render3d_st.ter_ox + th)
- let tz0 = Math.max(bz0, render3d_st.ter_oz - th); let tz1 = Math.min(bz1, render3d_st.ter_oz + th)
+function water_cells_build() -> void {
+ let bx0 = water_cx - water_ex; let bx1 = water_cx + water_ex
+ let bz0 = water_cz - water_ez; let bz1 = water_cz + water_ez
+ let th = float(TERRAIN_HALF)
+ let tx0 = Math.max(bx0, ter_ox - th); let tx1 = Math.min(bx1, ter_ox + th)
+ let tz0 = Math.max(bz0, ter_oz - th); let tz1 = Math.min(bz1, ter_oz + th)
let inside = tx1 > tx0 and tz1 > tz0
var cell = 32.0
var nx = 0
@@ -212,35 +244,35 @@ function water_cells_build(render3d_st: mut Render3dState) -> void {
nx = int((tx1 - tx0) / cell) + 1
nz = int((tz1 - tz0) / cell) + 1
}
- if render3d_st.wb_cells != null { free(render3d_st.wb_cells) }
- if render3d_st.wb_blocks != null { free(render3d_st.wb_blocks) }
- render3d_st.wb_cells = floats((nx * nz + 64 * 64) * 4)
+ if wb_cells != null { free(wb_cells) }
+ if wb_blocks != null { free(wb_blocks) }
+ wb_cells = floats((nx * nz + 64 * 64) * 4)
let nbx = (nx + WB_BLOCK - 1) / WB_BLOCK
let nbz = (nz + WB_BLOCK - 1) / WB_BLOCK
- render3d_st.wb_blocks = words((nbx * nbz + 1) * 6)
- render3d_st.wb_ncells = 0
- render3d_st.wb_nblocks = 0
+ wb_blocks = words((nbx * nbz + 1) * 6)
+ wb_ncells = 0
+ wb_nblocks = 0
let off = cell * 0.45
let half = cell * 0.5
for bz in 0 .. nbz {
for bx in 0 .. nbx {
- let first = render3d_st.wb_ncells
+ let first = wb_ncells
var iz = bz * WB_BLOCK
while iz < (bz + 1) * WB_BLOCK and iz < nz {
let z0 = tz0 + float(iz) * cell
var ix = bx * WB_BLOCK
while ix < (bx + 1) * WB_BLOCK and ix < nx {
let x0 = tx0 + float(ix) * cell
- if wb_wet(render3d_st, x0 + half, z0 + half, off) { wb_cell_add(render3d_st, x0, z0, x0 + cell, z0 + cell) }
+ if wb_wet(x0 + half, z0 + half, off) { wb_cell_add(x0, z0, x0 + cell, z0 + cell) }
ix += 1
}
iz += 1
}
let x0 = tx0 + float(bx * WB_BLOCK) * cell; let z0 = tz0 + float(bz * WB_BLOCK) * cell
- wb_block_add(render3d_st, x0, z0, x0 + float(WB_BLOCK) * cell, z0 + float(WB_BLOCK) * cell, first)
+ wb_block_add(x0, z0, x0 + float(WB_BLOCK) * cell, z0 + float(WB_BLOCK) * cell, first)
}
}
- let first = render3d_st.wb_ncells
+ let first = wb_ncells
let cw = (bx1 - bx0) / 64.0
let ch = (bz1 - bz0) / 64.0
for iz in 0 .. 64 {
@@ -249,64 +281,64 @@ function water_cells_build(render3d_st: mut Render3dState) -> void {
let x0 = bx0 + float(ix) * cw; let x1 = x0 + cw
# inside the height map's rectangle: the fine cells above cover it
if inside and not (x0 < tx0) and not (x1 > tx1) and not (z0 < tz0) and not (z1 > tz1) { continue }
- if wb_wet(render3d_st, x0 + cw * 0.5, z0 + ch * 0.5, 0.0) { wb_cell_add(render3d_st, x0, z0, x1, z1) }
+ if wb_wet(x0 + cw * 0.5, z0 + ch * 0.5, 0.0) { wb_cell_add(x0, z0, x1, z1) }
}
}
- wb_block_add(render3d_st, bx0, bz0, bx1, bz1, first)
+ wb_block_add(bx0, bz0, bx1, bz1, first)
}
# the program and the plane are the process's, built once; the bodies are the map's
-function water_setup(render3d_st: mut Render3dState) -> void {
- if render3d_st.water_prog != 0 { return }
- render3d_st.water_mesh = mesh_grid(render3d_st, 2, 0.5)
- render3d_st.water_prog = r3d_program(render3d_st, "water.vert", "water.frag", "")
- render3d_st.wb_level = floats(WATER_MAX); render3d_st.wb_cx = floats(WATER_MAX); render3d_st.wb_cz = floats(WATER_MAX)
- render3d_st.wb_ex = floats(WATER_MAX); render3d_st.wb_ez = floats(WATER_MAX); render3d_st.wb_reflect = words(WATER_MAX)
+function water_setup() -> void {
+ if water_prog != 0 { return }
+ water_mesh = mesh_grid(2, 0.5)
+ water_prog = r3d_program("water.vert", "water.frag", "")
+ wb_level = floats(WATER_MAX); wb_cx = floats(WATER_MAX); wb_cz = floats(WATER_MAX)
+ wb_ex = floats(WATER_MAX); wb_ez = floats(WATER_MAX); wb_reflect = words(WATER_MAX)
}
-function water_bodies_clear(render3d_st: mut Render3dState) -> void {
- render3d_st.wb_n = 0
- render3d_st.wb_primary = -1
- render3d_st.wb_ncells = -1
- render3d_st.water_on = false
+function water_bodies_clear() -> void {
+ wb_n = 0
+ wb_primary = -1
+ wb_ncells = -1
+ water_on = false
}
# Add a plane at `level` over (cx, cz) +- (ex, ez); true `reflect` makes it the mirrored one
# if none is yet. Returns its index, or -1 once WATER_MAX are in use.
-function water_body_add(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float, reflect: bool) -> int {
- water_setup(render3d_st)
- if render3d_st.wb_n >= WATER_MAX { return -1 }
- let i = render3d_st.wb_n
- render3d_st.wb_level[i] = level; render3d_st.wb_cx[i] = cx; render3d_st.wb_cz[i] = cz; render3d_st.wb_ex[i] = ex; render3d_st.wb_ez[i] = ez
- render3d_st.wb_reflect[i] = 0
- if reflect { render3d_st.wb_reflect[i] = 1 }
- render3d_st.wb_n += 1
- render3d_st.water_on = true
- if reflect and render3d_st.wb_primary < 0 {
- render3d_st.wb_primary = i
- render3d_st.wb_ncells = -1
- render3d_st.water_level = level; render3d_st.water_cx = cx; render3d_st.water_cz = cz; render3d_st.water_ex = ex; render3d_st.water_ez = ez
+function water_body_add(level: float, cx: float, cz: float, ex: float, ez: float, reflect: bool) -> int {
+ water_setup()
+ if wb_n >= WATER_MAX { return -1 }
+ let i = wb_n
+ wb_level[i] = level; wb_cx[i] = cx; wb_cz[i] = cz; wb_ex[i] = ex; wb_ez[i] = ez
+ wb_reflect[i] = 0
+ if reflect { wb_reflect[i] = 1 }
+ wb_n += 1
+ water_on = true
+ if reflect and wb_primary < 0 {
+ wb_primary = i
+ wb_ncells = -1
+ water_level = level; water_cx = cx; water_cz = cz; water_ex = ex; water_ez = ez
}
return i
}
# one reflecting plane: what this function always meant, without a new mesh and program
# every time it is called
-function water_init(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float) -> void {
- water_bodies_clear(render3d_st)
- water_body_add(render3d_st, level, cx, cz, ex, ez, true)
+function water_init(level: float, cx: float, cz: float, ex: float, ez: float) -> void {
+ water_bodies_clear()
+ water_body_add(level, cx, cz, ex, ez, true)
}
# call after the opaque pass, before the sky: blends over the resolved depth
-function water_draw(render3d_st: mut Render3dState, depth_tex: int) -> void {
- if not render3d_st.water_on or render3d_st.wb_n == 0 { return }
- let p = render3d_st.water_prog
- gpu_use_program(render3d_st, p)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
- u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_inv_vp"), render3d_st.cam_inv_vp)
+function water_draw(depth_tex: int) -> void {
+ if not water_on or wb_n == 0 { return }
+ let p = water_prog
+ gpu_use_program(p)
+ u_mat4(gpu_uniform(p, "u_view"), cam_view)
+ u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
+ u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
# gl_FragCoord here runs over the scene target, which is post_w x post_h — not the
# window. They are the same size only at a render scale of 1; at anything less, taking
# the window's size sent the refraction and depth reads into the wrong corner of the
# frame, and the lake showed a squashed copy of it instead of its own bed.
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_screen"), float(render3d_st.post_w), float(render3d_st.post_h))
+ u_f2(gpu_uniform(p, "u_screen"), float(post_w), float(post_h))
# Read into locals first. Passing these three globals straight into the call gives the
# shader wrong values - the whole lake churns instead of a patch of it - and a single dead
# `let junk = wt_wade_x` above the same unchanged call is enough to make it correct again.
@@ -315,49 +347,49 @@ function water_draw(render3d_st: mut Render3dState, depth_tex: int) -> void {
# right after gpu_use_program) nor the nested gpu_uniform call (hoisting that changes
# nothing). Verified by picture, both backends. Read a global into a local before handing it
# to a uniform call.
- let wx = render3d_st.wt_wade_x
- let wz = render3d_st.wt_wade_z
- let ws = render3d_st.wt_wade_s
- u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_wade"), wx, wz, ws)
+ let wx = wt_wade_x
+ let wz = wt_wade_z
+ let ws = wt_wade_s
+ u_f3(gpu_uniform(p, "u_wade"), wx, wz, ws)
var ron = 0.0
- if render3d_st.water_refl != null and render3d_st.wb_primary >= 0 {
+ if water_refl != null and wb_primary >= 0 {
# bind on its own unit first: generating the mip chain re-binds the texture on the active unit,
# and it must not displace the depth texture the shader reads for the shore
- r3d_bind_2d(render3d_st, p, "u_refl", 1, render3d_st.water_refl.color); ron = 1.0
- gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
- gpu_tex_mips(render3d_st, GPU_TEX2D)
+ r3d_bind_2d(p, "u_refl", 1, water_refl.color); ron = 1.0
+ gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
+ gpu_tex_mips(GPU_TEX2D)
}
- r3d_bind_2d(render3d_st, p, "u_depth", 0, depth_tex)
- r3d_bind_2d(render3d_st, p, "u_scene", 2, render3d_st.post_scene.color)
- sky_bind_lighting(render3d_st, p)
- shadow_bind(render3d_st, p)
- fog_bind(render3d_st, p)
+ r3d_bind_2d(p, "u_depth", 0, depth_tex)
+ r3d_bind_2d(p, "u_scene", 2, post_scene.color)
+ sky_bind_lighting(p)
+ shadow_bind(p)
+ fog_bind(p)
# Opaque. The surface composites the refracted bed itself, so there is nothing for
# hardware blending to do — and an alpha was what left see-through gaps in the foam
# and a clear band at the shore wide enough to give the plane away.
- gpu_blend(render3d_st, false)
- gpu_blend_func(render3d_st, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ gpu_blend(false)
+ gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# the surface writes depth: the ambient-occlusion and temporal passes read the frame's depth,
# and the bed 9 m below the shore would otherwise darken a band along the water line
- gpu_depth_write(render3d_st, true)
- gpu_cull(render3d_st, false)
+ gpu_depth_write(true)
+ gpu_cull(false)
# every body is the same plane at its own level and bounds; only the mirrored one samples
# the reflection - another body reading it would show the wrong world upside down
- for i in 0 .. render3d_st.wb_n {
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_level"), render3d_st.wb_level[i])
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_center"), render3d_st.wb_cx[i], render3d_st.wb_cz[i])
- u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_extent"), render3d_st.wb_ex[i], render3d_st.wb_ez[i])
+ for i in 0 .. wb_n {
+ u_f(gpu_uniform(p, "u_level"), wb_level[i])
+ u_f2(gpu_uniform(p, "u_center"), wb_cx[i], wb_cz[i])
+ u_f2(gpu_uniform(p, "u_extent"), wb_ex[i], wb_ez[i])
var r = 0.0
- if i == render3d_st.wb_primary { r = ron }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_refl_on"), r)
+ if i == wb_primary { r = ron }
+ u_f(gpu_uniform(p, "u_refl_on"), r)
# Every body is clipped to the ellipse inside its bounds but an unbounded mirrored sea,
# whose rectangle is the point. A mirrored LAKE is clipped like any other: a map whose
# reflection belongs to its lake (the Bells in Maroon Lake) would otherwise draw that
# lake's level over every hollow in the survey.
var clip = 1.0
- if i == render3d_st.wb_primary and Math.max(render3d_st.wb_ex[i], render3d_st.wb_ez[i]) > 10000.0 { clip = 0.0 }
- u_f(render3d_st, gpu_uniform(render3d_st, p, "u_clip_ellipse"), clip)
- mesh_draw(render3d_st, render3d_st.water_mesh)
+ if i == wb_primary and Math.max(wb_ex[i], wb_ez[i]) > 10000.0 { clip = 0.0 }
+ u_f(gpu_uniform(p, "u_clip_ellipse"), clip)
+ mesh_draw(water_mesh)
}
- gpu_blend(render3d_st, false)
+ gpu_blend(false)
}
diff --git a/packages/ludic.rpg/crafting.ludic b/packages/ludic.rpg/crafting.ludic
index d54c2861..7f058a87 100644
--- a/packages/ludic.rpg/crafting.ludic
+++ b/packages/ludic.rpg/crafting.ludic
@@ -9,12 +9,20 @@ import "ludic.rpg/inventory.ludic"
const CRAFT_MAX: int = 128
const CRAFT_ING_MAX: int = 512
+var recipe_reg: Dict = null # recipe name -> id
+var recipe_n: int = 0
+var recipe_out: pointers = null # output item name
+var recipe_outqty: words = null
+var ing_n: int = 0
+var ing_rid: words = null # owning recipe id
+var ing_name: pointers = null # ingredient item name
+var ing_qty: words = null
-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)
+function craft_ensure() -> void {
+ if recipe_reg == null {
+ recipe_reg = Dict.new()
+ recipe_out = pointers(CRAFT_MAX); recipe_outqty = words(CRAFT_MAX)
+ ing_rid = words(CRAFT_ING_MAX); ing_name = pointers(CRAFT_ING_MAX); ing_qty = words(CRAFT_ING_MAX)
}
}
@@ -22,35 +30,35 @@ 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
- rpg_st.recipe_n += 1
- Dict.set(rpg_st.recipe_reg, name, id)
+@Namespace(Craft) function craft_recipe(name: pointer, out_item: pointer, out_qty: int) -> int {
+ craft_ensure()
+ if recipe_n >= CRAFT_MAX { return -1 }
+ let id = recipe_n
+ recipe_out[id] = out_item; recipe_outqty[id] = out_qty
+ recipe_n += 1
+ Dict.set(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(name: pointer) -> int { craft_ensure(); return Dict.get_or(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)
+@Namespace(Craft) function craft_ingredient(recipe_name: pointer, item: pointer, qty: int) -> void {
+ craft_ensure()
+ let rid = craft_id(recipe_name)
if rid < 0 { return }
- if rpg_st.ing_n >= CRAFT_ING_MAX { return }
- rpg_st.ing_rid[rpg_st.ing_n] = rid; rpg_st.ing_name[rpg_st.ing_n] = item; rpg_st.ing_qty[rpg_st.ing_n] = qty
- rpg_st.ing_n += 1
+ if ing_n >= CRAFT_ING_MAX { return }
+ ing_rid[ing_n] = rid; ing_name[ing_n] = item; ing_qty[ing_n] = qty
+ ing_n += 1
}
# 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)
+@Namespace(Craft) function craft_can(e: int, recipe_name: pointer) -> int {
+ craft_ensure()
+ let rid = craft_id(recipe_name)
if rid < 0 { return 0 }
var i = 0
- while i < rpg_st.ing_n {
- if rpg_st.ing_rid[i] == rid {
- if Inventory.count(e, rpg_st.ing_name[i]) < rpg_st.ing_qty[i] { return 0 }
+ while i < ing_n {
+ if ing_rid[i] == rid {
+ if Inventory.count(e, ing_name[i]) < ing_qty[i] { return 0 }
}
i += 1
}
@@ -58,18 +66,18 @@ 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)
+@Namespace(Craft) function craft_make(e: int, recipe_name: pointer) -> int {
+ craft_ensure()
+ let rid = craft_id(recipe_name)
if rid < 0 { return 0 }
- if craft_can(rpg_st, e, recipe_name) == 0 { return 0 }
+ if craft_can(e, recipe_name) == 0 { return 0 }
if emit CraftRequested(e: e, recipe: rid) != 0 { return 0 }
var i = 0
- while i < rpg_st.ing_n {
- if rpg_st.ing_rid[i] == rid { Inventory.remove(e, rpg_st.ing_name[i], rpg_st.ing_qty[i]) }
+ while i < ing_n {
+ if ing_rid[i] == rid { Inventory.remove(e, ing_name[i], ing_qty[i]) }
i += 1
}
- Inventory.add(e, rpg_st.recipe_out[rid], rpg_st.recipe_outqty[rid])
+ Inventory.add(e, recipe_out[rid], recipe_outqty[rid])
emit CraftCompleted(e: e, recipe: rid)
return 1
}
diff --git a/packages/ludic.rpg/dialog.ludic b/packages/ludic.rpg/dialog.ludic
index 91bd8e41..41103b8d 100644
--- a/packages/ludic.rpg/dialog.ludic
+++ b/packages/ludic.rpg/dialog.ludic
@@ -8,13 +8,23 @@ const DLG_TREE_MAX: int = 64
const DLG_NODE_MAX: int = 512
const DLG_CHOICE_MAX: int = 1024
+var dlg_reg: Dict = null # tree name -> tree id
+var dlg_tree_n: int = 0
+var dlg_tree_start: words = null # first node id per tree
+var dlg_node_n: int = 0
+var dlg_node_tree: words = null
+var dlg_node_text: pointers = null
+var dlg_choice_n: int = 0
+var dlg_choice_node: words = null # owning node
+var dlg_choice_label: pointers = null
+var dlg_choice_next: words = null # next node id, or -1 to end
-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)
+function dlg_ensure() -> void {
+ if dlg_reg == null {
+ dlg_reg = Dict.new()
+ dlg_tree_start = words(DLG_TREE_MAX)
+ dlg_node_tree = words(DLG_NODE_MAX); dlg_node_text = pointers(DLG_NODE_MAX)
+ dlg_choice_node = words(DLG_CHOICE_MAX); dlg_choice_label = pointers(DLG_CHOICE_MAX); dlg_choice_next = words(DLG_CHOICE_MAX)
}
}
@@ -26,37 +36,37 @@ event cancellable DialogChoice { e: int = 0, node: int = 0, choice: int = 0 }
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)
+@Namespace(Dialog) function dialog_tree(name: pointer) -> int {
+ dlg_ensure()
+ let ex = Dict.get_or(dlg_reg, name, -1)
if ex >= 0 { return ex }
- if rpg_st.dlg_tree_n >= DLG_TREE_MAX { return -1 }
- let id = rpg_st.dlg_tree_n
- rpg_st.dlg_tree_start[id] = -1
- rpg_st.dlg_tree_n += 1
- Dict.set(rpg_st.dlg_reg, name, id)
+ if dlg_tree_n >= DLG_TREE_MAX { return -1 }
+ let id = dlg_tree_n
+ dlg_tree_start[id] = -1
+ dlg_tree_n += 1
+ Dict.set(dlg_reg, name, id)
return id
}
# 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)
+@Namespace(Dialog) function dialog_node(tree_name: pointer, text: pointer) -> int {
+ dlg_ensure()
+ let tid = dialog_tree(tree_name)
if tid < 0 { return -1 }
- if rpg_st.dlg_node_n >= DLG_NODE_MAX { return -1 }
- let id = rpg_st.dlg_node_n
- rpg_st.dlg_node_tree[id] = tid; rpg_st.dlg_node_text[id] = text
- rpg_st.dlg_node_n += 1
- if rpg_st.dlg_tree_start[tid] < 0 { rpg_st.dlg_tree_start[tid] = id }
+ if dlg_node_n >= DLG_NODE_MAX { return -1 }
+ let id = dlg_node_n
+ dlg_node_tree[id] = tid; dlg_node_text[id] = text
+ dlg_node_n += 1
+ if dlg_tree_start[tid] < 0 { dlg_tree_start[tid] = id }
return id
}
# 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
- rpg_st.dlg_choice_n += 1
+@Namespace(Dialog) function dialog_choice(node: int, label: pointer, next_node: int) -> int {
+ dlg_ensure()
+ if dlg_choice_n >= DLG_CHOICE_MAX { return -1 }
+ let id = dlg_choice_n
+ dlg_choice_node[id] = node; dlg_choice_label[id] = label; dlg_choice_next[id] = next_node
+ dlg_choice_n += 1
return id
}
@@ -70,45 +80,45 @@ 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)
+@Namespace(Dialog) function dialog_start(e: int, tree_name: pointer) -> void {
+ dlg_ensure()
+ let tid = dialog_tree(tree_name)
if tid < 0 { return }
dlg_set(e, "tree", tid)
- dlg_set(e, "node", rpg_st.dlg_tree_start[tid])
+ dlg_set(e, "node", dlg_tree_start[tid])
dlg_set(e, "active", 1)
emit DialogStarted(e: e, tree: tid)
- emit DialogLine(e: e, node: rpg_st.dlg_tree_start[tid])
+ emit DialogLine(e: e, node: dlg_tree_start[tid])
}
# Dialog.line(e) — text of the current node (empty when inactive).
-@Namespace(Dialog) function dialog_line(rpg_st: RpgState, e: int) -> pointer {
+@Namespace(Dialog) function dialog_line(e: int) -> pointer {
if dlg_get(e, "active") == 0 { return "" }
- return rpg_st.dlg_node_text[dlg_get(e, "node")]
+ return dlg_node_text[dlg_get(e, "node")]
}
# 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)
+@Namespace(Dialog) function dialog_count(e: int) -> int {
+ dlg_ensure()
if dlg_get(e, "active") == 0 { return 0 }
let node = dlg_get(e, "node")
var n = 0; var i = 0
- while i < rpg_st.dlg_choice_n { if rpg_st.dlg_choice_node[i] == node { n += 1 }; i += 1 }
+ while i < dlg_choice_n { if dlg_choice_node[i] == node { n += 1 }; i += 1 }
return n
}
# 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)
+@Namespace(Dialog) function dialog_choose(e: int, idx: int) -> int {
+ dlg_ensure()
if dlg_get(e, "active") == 0 { return 0 }
let node = dlg_get(e, "node")
var seen = 0; var i = 0; var pick = -1
- while i < rpg_st.dlg_choice_n {
- if rpg_st.dlg_choice_node[i] == node {
- if seen == idx { pick = i; i = rpg_st.dlg_choice_n } else { seen += 1 }
+ while i < dlg_choice_n {
+ if dlg_choice_node[i] == node {
+ if seen == idx { pick = i; i = dlg_choice_n } else { seen += 1 }
}
i += 1
}
if pick < 0 { return 0 }
if emit DialogChoice(e: e, node: node, choice: idx) != 0 { return 0 }
- let nxt = rpg_st.dlg_choice_next[pick]
+ let nxt = dlg_choice_next[pick]
if nxt < 0 {
dlg_set(e, "active", 0)
emit DialogEnded(e: e)
diff --git a/packages/ludic.rpg/inventory.ludic b/packages/ludic.rpg/inventory.ludic
index cf188a28..fbb193f2 100644
--- a/packages/ludic.rpg/inventory.ludic
+++ b/packages/ludic.rpg/inventory.ludic
@@ -10,70 +10,40 @@ import "ludic.gameplay/stats.ludic"
const INV_MAX: int = 256
# ---- item registry --------------------------------------------------------
-state RpgState {
- recipe_reg: Dict = null # recipe name -> id
- recipe_n: int = 0
- recipe_out: pointers = null # output item name
- recipe_outqty: words = null
- 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
- dlg_tree_n: int = 0
- dlg_tree_start: words = null # first node id per tree
- dlg_node_n: int = 0
- dlg_node_tree: words = null
- dlg_node_text: pointers = null
- 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
- 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
- quest_n: int = 0
- quest_state: words = null
- 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
-}
+var item_reg: Dict = null
+var item_n: int = 0
+var item_stackmax: words = null
+var item_weight: words = null
+var item_value: words = null
-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)
+function item_ensure() -> void {
+ if item_reg == null {
+ item_reg = Dict.new()
+ item_stackmax = words(INV_MAX); item_weight = words(INV_MAX); 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
- rpg_st.item_n += 1
- Dict.set(rpg_st.item_reg, name, id)
+@Namespace(Item) function item_def(name: pointer, stack_max: int, weight: int, value: int) -> int {
+ item_ensure()
+ if item_n >= INV_MAX { return -1 }
+ let id = item_n
+ item_stackmax[id] = stack_max; item_weight[id] = weight; item_value[id] = value
+ item_n += 1
+ Dict.set(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_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]
+@Namespace(Item) function item_id(name: pointer) -> int { item_ensure(); return Dict.get_or(item_reg, name, -1) }
+@Namespace(Item) function item_value_of(name: pointer) -> int {
+ let id = item_id(name); if id < 0 { return 0 }
+ return item_value[id]
}
# ---- 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() } }
+var inv_counts: Dict = null
+function inv_ensure() -> void { if inv_counts == null { 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
@@ -81,36 +51,36 @@ event ItemRemoved { e: int = 0, item: int = 0, qty: int = 0 }
event cancellable ItemUse { e: int = 0, item: int = 0 } # veto a use (e.g. cursed / requirement)
# 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)
+@Namespace(Inventory) function inventory_add(e: int, name: pointer, n: int) -> void {
+ inv_ensure()
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)
+ Dict.set(inv_counts, k, Dict.get_or(inv_counts, k, 0) + n)
+ emit ItemAdded(e: e, item: item_id(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_count(e: int, name: pointer) -> int {
+ inv_ensure()
+ return Dict.get_or(inv_counts, inv_key(e, name), 0)
}
-@Namespace(Inventory) function inventory_has(rpg_st: mut RpgState, e: int, name: pointer) -> int {
- if inventory_count(rpg_st, e, name) > 0 { return 1 }
+@Namespace(Inventory) function inventory_has(e: int, name: pointer) -> int {
+ if inventory_count(e, name) > 0 { return 1 }
return 0
}
# 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)
+@Namespace(Inventory) function inventory_remove(e: int, name: pointer, n: int) -> int {
+ inv_ensure()
let k = inv_key(e, name)
- let have = Dict.get_or(rpg_st.inv_counts, k, 0)
+ let have = Dict.get_or(inv_counts, k, 0)
if have < n { return 0 }
- Dict.set(rpg_st.inv_counts, k, have - n)
- emit ItemRemoved(e: e, item: item_id(rpg_st, name), qty: n)
+ Dict.set(inv_counts, k, have - n)
+ emit ItemRemoved(e: e, item: item_id(name), qty: n)
return 1
}
# Inventory.use(e, name) — consume one item unless a listener vetoes ItemUse.
-@Namespace(Inventory) function inventory_use(rpg_st: mut RpgState, e: int, name: pointer) -> int {
- if inventory_count(rpg_st, e, name) <= 0 { return 0 }
- if emit ItemUse(e: e, item: item_id(rpg_st, name)) != 0 { return 0 }
- return inventory_remove(rpg_st, e, name, 1)
+@Namespace(Inventory) function inventory_use(e: int, name: pointer) -> int {
+ if inventory_count(e, name) <= 0 { return 0 }
+ if emit ItemUse(e: e, item: item_id(name)) != 0 { return 0 }
+ return inventory_remove(e, name, 1)
}
# Inventory.gold / add_gold / spend_gold
@Namespace(Inventory) function inventory_gold(e: int) -> int {
diff --git a/packages/ludic.rpg/quests.ludic b/packages/ludic.rpg/quests.ludic
index 277ec1cb..3e6105e7 100644
--- a/packages/ludic.rpg/quests.ludic
+++ b/packages/ludic.rpg/quests.ludic
@@ -14,13 +14,23 @@
const QUEST_MAX: int = 128
const OBJ_MAX: int = 512
+var quest_reg: Dict = null # quest name -> id
+var quest_n: int = 0
+var quest_state: words = null
+var obj_n: int = 0
+var obj_qid: words = null # owning quest id
+var obj_kind: words = null
+var obj_target: words = null # a target id/tag (kind-specific; 0 = any)
+var obj_need: words = null
+var obj_have: words = null
+var quest_flags: Dict = null # global flag store
-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)
+function quest_ensure() -> void {
+ if quest_reg == null {
+ quest_reg = Dict.new(); quest_flags = Dict.new()
+ quest_state = words(QUEST_MAX)
+ obj_qid = words(OBJ_MAX); obj_kind = words(OBJ_MAX); obj_target = words(OBJ_MAX)
+ obj_need = words(OBJ_MAX); obj_have = words(OBJ_MAX)
}
}
@@ -29,48 +39,48 @@ event QuestCompleted { quest: int = 0 }
event ObjectiveProgressed { quest: int = 0, objective: int = 0, have: int = 0, need: int = 0 }
# 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
- rpg_st.quest_n += 1
- Dict.set(rpg_st.quest_reg, name, id)
+@Namespace(Quest) function quest_define(name: pointer) -> int {
+ quest_ensure()
+ if quest_n >= QUEST_MAX { return -1 }
+ let id = quest_n
+ quest_state[id] = 0
+ quest_n += 1
+ Dict.set(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(name: pointer) -> int { quest_ensure(); return Dict.get_or(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)
+@Namespace(Quest) function quest_objective(quest_name: pointer, kind: int, target: int, need: int) -> int {
+ quest_ensure()
+ let qid = quest_id(quest_name)
if qid < 0 { return -1 }
- if rpg_st.obj_n >= OBJ_MAX { return -1 }
- let id = rpg_st.obj_n
- rpg_st.obj_qid[id] = qid; rpg_st.obj_kind[id] = kind; rpg_st.obj_target[id] = target
- rpg_st.obj_need[id] = need; rpg_st.obj_have[id] = 0
- rpg_st.obj_n += 1
+ if obj_n >= OBJ_MAX { return -1 }
+ let id = obj_n
+ obj_qid[id] = qid; obj_kind[id] = kind; obj_target[id] = target
+ obj_need[id] = need; obj_have[id] = 0
+ obj_n += 1
return id
}
# 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)
+@Namespace(Quest) function quest_start(name: pointer) -> void {
+ quest_ensure()
+ let qid = quest_id(name)
if qid < 0 { return }
- rpg_st.quest_state[qid] = 2
+ 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)
+@Namespace(Quest) function quest_state_of(name: pointer) -> int {
+ quest_ensure()
+ let qid = quest_id(name)
if qid < 0 { return -1 }
- return rpg_st.quest_state[qid]
+ return quest_state[qid]
}
# is every objective of a quest satisfied?
-function quest_all_done(rpg_st: RpgState, qid: int) -> int {
+function quest_all_done(qid: int) -> int {
var i = 0
- while i < rpg_st.obj_n {
- if rpg_st.obj_qid[i] == qid { if rpg_st.obj_have[i] < rpg_st.obj_need[i] { return 0 } }
+ while i < obj_n {
+ if obj_qid[i] == qid { if obj_have[i] < obj_need[i] { return 0 } }
i += 1
}
return 1
@@ -79,20 +89,20 @@ function quest_all_done(rpg_st: RpgState, qid: int) -> int {
# Quest.notify(kind, target, amount) — report a gameplay signal. Every ACTIVE
# 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)
+@Namespace(Quest) function quest_notify(kind: int, target: int, amount: int) -> void {
+ quest_ensure()
var i = 0
- while i < rpg_st.obj_n {
- let qid = rpg_st.obj_qid[i]
- if rpg_st.quest_state[qid] == 2 {
- if rpg_st.obj_kind[i] == kind {
- if (rpg_st.obj_target[i] == 0) or (rpg_st.obj_target[i] == target) {
- if rpg_st.obj_have[i] < rpg_st.obj_need[i] {
- rpg_st.obj_have[i] += amount
- if rpg_st.obj_have[i] > rpg_st.obj_need[i] { rpg_st.obj_have[i] = rpg_st.obj_need[i] }
- emit ObjectiveProgressed(quest: qid, objective: i, have: rpg_st.obj_have[i], need: rpg_st.obj_need[i])
- if quest_all_done(rpg_st, qid) == 1 {
- rpg_st.quest_state[qid] = 3
+ while i < obj_n {
+ let qid = obj_qid[i]
+ if quest_state[qid] == 2 {
+ if obj_kind[i] == kind {
+ if (obj_target[i] == 0) or (obj_target[i] == target) {
+ if obj_have[i] < obj_need[i] {
+ obj_have[i] += amount
+ if obj_have[i] > obj_need[i] { obj_have[i] = obj_need[i] }
+ emit ObjectiveProgressed(quest: qid, objective: i, have: obj_have[i], need: obj_need[i])
+ if quest_all_done(qid) == 1 {
+ quest_state[qid] = 3
emit QuestCompleted(quest: qid)
}
}
@@ -104,5 +114,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(name: pointer, value: int) -> void { quest_ensure(); Dict.set(quest_flags, name, value) }
+@Namespace(Quest) function quest_flag_get(name: pointer) -> int { quest_ensure(); return Dict.get_or(quest_flags, name, 0) }
diff --git a/packages/ludic.session/near.ludic b/packages/ludic.session/near.ludic
index af272e6b..056cd203 100644
--- a/packages/ludic.session/near.ludic
+++ b/packages/ludic.session/near.ludic
@@ -1,17 +1,17 @@
# near.ludic - the world asks who is nearest, never "the player": an animal notices whoever is
# closest, a camper keeps clear of everyone
-export function ply_near_d2(session_st: mut SessionState, x: float, z: float) -> float {
- session_st.session__near_i = 0
+export function ply_near_d2(x: float, z: float) -> float {
+ session__near_i = 0
var best = 0.0
var first = true
for p in 0 .. PLY_MAX {
- if not ply_on(session_st, p) { continue }
- let dx = x - ply_x(session_st, p)
- let dz = z - ply_z(session_st, p)
+ if not ply_on(p) { continue }
+ let dx = x - ply_x(p)
+ let dz = z - ply_z(p)
let d2 = dx * dx + dz * dz
if first or d2 < best {
best = d2
- session_st.session__near_i = p
+ session__near_i = p
first = false
}
}
@@ -19,12 +19,12 @@ export function ply_near_d2(session_st: mut SessionState, x: float, z: float) ->
}
# the distance to the nearest player, in metres; ply_near_i() says which
-export function ply_near(session_st: mut SessionState, x: float, z: float) -> float { return Math.sqrt(ply_near_d2(session_st, x, z)) }
+export function ply_near(x: float, z: float) -> float { return Math.sqrt(ply_near_d2(x, z)) }
-export function ply_near_i(session_st: SessionState) -> int { return session_st.session__near_i }
+export function ply_near_i() -> int { return session__near_i }
-export function ply_dist(session_st: SessionState, p: int, x: float, z: float) -> float {
- let dx = x - ply_x(session_st, p)
- let dz = z - ply_z(session_st, p)
+export function ply_dist(p: int, x: float, z: float) -> float {
+ let dx = x - ply_x(p)
+ let dz = z - ply_z(p)
return Math.sqrt(dx * dx + dz * dz)
}
diff --git a/packages/ludic.session/night.ludic b/packages/ludic.session/night.ludic
index 92e2197c..d13862c8 100644
--- a/packages/ludic.session/night.ludic
+++ b/packages/ludic.session/night.ludic
@@ -1,42 +1,43 @@
# night.ludic - with company the night passes when everyone has turned in. Lying down marks this
# player ready and getting up takes it back; the host counts, and when the last one is in it hears
# SES_NIGHT and passes the night for everyone.
+var session__me_sleeping: bool = false
-export function ses_sleeping(session_st: SessionState) -> bool { return session_st.session__me_sleeping }
+export function ses_sleeping() -> bool { return session__me_sleeping }
# this player turns in (or gets up): false alone, where a bed simply sleeps
-export function ses_sleep(base_st: mut BaseState, session_st: mut SessionState, on: bool) -> bool {
- if not ses_live(session_st) { return false }
- if on == session_st.session__me_sleeping { return on }
- session_st.session__me_sleeping = on
- let f = session__fact(base_st, session_st, SES_SLEEP, 0, 0)
+export function ses_sleep(on: bool) -> bool {
+ if not ses_live() { return false }
+ if on == session__me_sleeping { return on }
+ session__me_sleeping = on
+ let f = session__fact(SES_SLEEP, 0, 0)
f.yes = on
- if on { ses_night_check(base_st, session_st) }
+ if on { ses_night_check() }
return true
}
# the host: a guest said it turned in, or got up
-export function ses_sleep_ready(base_st: mut BaseState, session_st: mut SessionState, p: int, on: bool) -> void {
- if p <= 0 or not ply_on(session_st, p) { return }
- session_st.session__slot[p].ready = on
- ses_night_check(base_st, session_st)
+export function ses_sleep_ready(p: int, on: bool) -> void {
+ if p <= 0 or not ply_on(p) { return }
+ session__slot[p].ready = on
+ ses_night_check()
}
# the host: is everyone in? Then the night is due, and everyone's readiness starts again
-export function ses_night_check(base_st: mut BaseState, session_st: mut SessionState) -> void {
- if session_st.session__role != SES_HOST or not session_st.session__me_sleeping { return }
- for p in 1 .. PLY_MAX { if ply_on(session_st, p) and not session_st.session__slot[p].ready { return } }
- for p in 1 .. PLY_MAX { session_st.session__slot[p].ready = false }
- session__fact(base_st, session_st, SES_NIGHT, -1, 0)
+export function ses_night_check() -> void {
+ if session__role != SES_HOST or not session__me_sleeping { return }
+ for p in 1 .. PLY_MAX { if ply_on(p) and not session__slot[p].ready { return } }
+ for p in 1 .. PLY_MAX { session__slot[p].ready = false }
+ session__fact(SES_NIGHT, -1, 0)
}
# the night went by on this machine: awake again
-export function ses_night_passed(session_st: mut SessionState) -> void { session_st.session__me_sleeping = false }
+export function ses_night_passed() -> void { session__me_sleeping = false }
# how many are still up, this player included
-export function ses_sleep_waiting(session_st: SessionState) -> int {
+export function ses_sleep_waiting() -> int {
var n = 0
- for p in 1 .. PLY_MAX { if ply_on(session_st, p) and not session_st.session__slot[p].sleeping { n += 1 } }
- if not session_st.session__me_sleeping { n += 1 }
+ for p in 1 .. PLY_MAX { if ply_on(p) and not session__slot[p].sleeping { n += 1 } }
+ if not session__me_sleeping { n += 1 }
return n
}
diff --git a/packages/ludic.session/roles.ludic b/packages/ludic.session/roles.ludic
index 48268a34..d5b42f19 100644
--- a/packages/ludic.session/roles.ludic
+++ b/packages/ludic.session/roles.ludic
@@ -1,46 +1,46 @@
# roles.ludic - who owns the world. Alone, this machine does; a host does and has company; a
# guest owns only its own player. The transport says what this machine became.
-export function ses_role(session_st: SessionState) -> int { return session_st.session__role }
+export function ses_role() -> int { return session__role }
-export function ses_set_role(base_st: mut BaseState, session_st: mut SessionState, r: int) -> void {
- if r == session_st.session__role { return }
- session_st.session__role = r
- session__fact(base_st, session_st, SES_ROLE, -1, 0)
+export function ses_set_role(r: int) -> void {
+ if r == session__role { return }
+ session__role = r
+ session__fact(SES_ROLE, -1, 0)
}
# this machine runs the world: the clock, the weather, the animals, the people
-export function ses_owns_world(session_st: SessionState) -> bool { return session_st.session__role != SES_GUEST }
+export function ses_owns_world() -> bool { return session__role != SES_GUEST }
# somebody else is in this world, so nothing this player opens may stop it
-export function ses_live(session_st: SessionState) -> bool { return session_st.session__role != SES_SOLO }
+export function ses_live() -> bool { return session__role != SES_SOLO }
-export function ses_hosting(session_st: SessionState) -> bool { return session_st.session__role == SES_HOST }
+export function ses_hosting() -> bool { return session__role == SES_HOST }
-export function ses_guest(session_st: SessionState) -> bool { return session_st.session__role == SES_GUEST }
+export function ses_guest() -> bool { return session__role == SES_GUEST }
# whether this player lying down ends the night; alone, it always does
-export function ses_night_passes(session_st: SessionState) -> bool { return ses_owns_world(session_st) and not ses_live(session_st) }
+export function ses_night_passes() -> bool { return ses_owns_world() and not ses_live() }
# the party a host allows (2..PLY_MAX) and whether it takes new players
-export function ses_set_party(session_st: mut SessionState, max: int, open: bool) -> void {
- session_st.session__max = max
- if session_st.session__max < 2 { session_st.session__max = 2 }
- if session_st.session__max > PLY_MAX { session_st.session__max = PLY_MAX }
- session_st.session__open = open
+export function ses_set_party(max: int, open: bool) -> void {
+ session__max = max
+ if session__max < 2 { session__max = 2 }
+ if session__max > PLY_MAX { session__max = PLY_MAX }
+ session__open = open
}
-export function ses_set_open(session_st: mut SessionState, open: bool) -> void { session_st.session__open = open }
-export function ses_max(session_st: SessionState) -> int { return session_st.session__max }
-export function ses_open(session_st: SessionState) -> bool { return session_st.session__open }
+export function ses_set_open(open: bool) -> void { session__open = open }
+export function ses_max() -> int { return session__max }
+export function ses_open() -> bool { return session__open }
# may one more join: open, and not full
-export function ses_room_for_one(session_st: SessionState) -> bool { return session_st.session__open and ply_count(session_st) < session_st.session__max }
+export function ses_room_for_one() -> bool { return session__open and ply_count() < session__max }
# the session is over: everyone else gone, alone again, nothing asked and nobody asleep
-export function ses_reset(base_st: mut BaseState, session_st: mut SessionState) -> void {
- ply_leave_all(base_st, session_st)
- ses_set_role(base_st, session_st, SES_SOLO)
- session_st.session__me_sleeping = false
- session_st.session__vote_t = 0.0
- session_st.session__vote_mine = -1
+export function ses_reset() -> void {
+ ply_leave_all()
+ ses_set_role(SES_SOLO)
+ session__me_sleeping = false
+ session__vote_t = 0.0
+ session__vote_mine = -1
}
diff --git a/packages/ludic.session/roster.ludic b/packages/ludic.session/roster.ludic
index 3bf26e21..f0865d11 100644
--- a/packages/ludic.session/roster.ludic
+++ b/packages/ludic.session/roster.ludic
@@ -1,62 +1,62 @@
# 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_init() -> void { session__init() }
-export function ply_on(session_st: SessionState, p: int) -> bool {
+export function ply_on(p: int) -> bool {
if p == 0 { return true }
- if session_st.session__slot == null or p < 0 or p >= PLY_MAX { return false }
- return session_st.session__slot[p].on
+ if session__slot == null or p < 0 or p >= PLY_MAX { return false }
+ return session__slot[p].on
}
export function ply_is_local(p: int) -> bool { return p == 0 }
# the roster slot holding network player `pid`, or -1
-export function ply_slot_of(session_st: SessionState, pid: int) -> int {
- if session_st.session__slot == null or pid <= 0 { return -1 }
- for p in 1 .. PLY_MAX { if session_st.session__slot[p].on and session_st.session__slot[p].pid == pid { return p } }
+export function ply_slot_of(pid: int) -> int {
+ if session__slot == null or pid <= 0 { return -1 }
+ for p in 1 .. PLY_MAX { if session__slot[p].on and session__slot[p].pid == pid { return p } }
return -1
}
-export function ply_count(session_st: SessionState) -> int {
+export function ply_count() -> int {
var n = 1
- for p in 1 .. PLY_MAX { if ply_on(session_st, p) { n += 1 } }
+ for p in 1 .. PLY_MAX { if ply_on(p) { n += 1 } }
return n
}
# 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(base_st: mut BaseState, session_st: mut SessionState, pid: int) -> int {
- session__init(session_st)
+export function ply_join(pid: int) -> int {
+ session__init()
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 }
+ if session__slot[p].on { continue }
let r = new PartyMember
r.on = true
r.pid = pid
r.x = SessionLocal.x()
r.y = SessionLocal.y()
r.z = SessionLocal.z()
- session_st.session__slot[p] = r
- session__fact(base_st, session_st, SES_JOINED, p, pid)
+ session__slot[p] = r
+ session__fact(SES_JOINED, p, pid)
return p
}
return -1
}
-export function ply_leave(base_st: mut BaseState, session_st: mut SessionState, p: int) -> void {
- if p <= 0 or not ply_on(session_st, p) { return }
- let r = session_st.session__slot[p]
+export function ply_leave(p: int) -> void {
+ if p <= 0 or not ply_on(p) { return }
+ let r = session__slot[p]
r.on = false
r.ready = false
- session__fact(base_st, session_st, SES_LEFT, p, r.pid)
+ session__fact(SES_LEFT, p, r.pid)
}
-export function ply_leave_all(base_st: mut BaseState, session_st: mut SessionState) -> void { for p in 1 .. PLY_MAX { ply_leave(base_st, session_st, p) } }
+export function ply_leave_all() -> void { for p in 1 .. PLY_MAX { ply_leave(p) } }
# what a remote player's machine reported this tick
-export function ply_report(session_st: mut SessionState, p: int, x: float, y: float, z: float, yaw: float, speed: float) -> void {
- if p <= 0 or not ply_on(session_st, p) { return }
- let r = session_st.session__slot[p]
+export function ply_report(p: int, x: float, y: float, z: float, yaw: float, speed: float) -> void {
+ if p <= 0 or not ply_on(p) { return }
+ let r = session__slot[p]
r.x = x
r.y = y
r.z = z
@@ -64,13 +64,13 @@ export function ply_report(session_st: mut SessionState, p: int, x: float, y: fl
r.speed = speed
}
-export function ply_report_rest(session_st: mut SessionState, p: int, rest: float, sleeping: bool) -> void {
- if p <= 0 or not ply_on(session_st, p) { return }
- session_st.session__slot[p].rest = rest
- session_st.session__slot[p].sleeping = sleeping
+export function ply_report_rest(p: int, rest: float, sleeping: bool) -> void {
+ if p <= 0 or not ply_on(p) { return }
+ session__slot[p].rest = rest
+ session__slot[p].sleeping = sleeping
}
-export function ply_report_name(session_st: mut SessionState, p: int, name: string) -> void {
- if p <= 0 or not ply_on(session_st, p) { return }
- session_st.session__slot[p].name = name
+export function ply_report_name(p: int, name: string) -> void {
+ if p <= 0 or not ply_on(p) { return }
+ session__slot[p].name = name
}
diff --git a/packages/ludic.session/state.ludic b/packages/ludic.session/state.ludic
index 982ce6af..35445521 100644
--- a/packages/ludic.session/state.ludic
+++ b/packages/ludic.session/state.ludic
@@ -59,43 +59,31 @@ function session__zero() -> float { return 0.0 }
function session__one() -> float { return 1.0 }
function session__nameless() -> string { return "" }
-export state SessionState {
- session__me_sleeping: bool = false
- session__slot: []PartyMember = null
- session__role: int = 0
- session__max: int = 4
- session__open: bool = true
- session__near_i: int = 0
- session__facts: Queue = null
- session__vote_id: int = 0
- session__vote_kind: int = 0
- session__vote_param: int = 0
- session__vote_text: string = ""
- session__vote_t: float = 0.0
- session__vote_yes: int = 0
- session__vote_no: int = 0
- session__vote_mine: int = -1 # -1 not voted, 0 no, 1 yes
- session__vote_passed: int = -1 # the last vote: -1 none, 0 failed, 1 passed
+var session__slot: []PartyMember = null
+var session__role: int = 0
+var session__max: int = 4
+var session__open: bool = true
+var session__near_i: int = 0
+var session__facts: Queue = null
+
+export function ses_facts() -> Queue {
+ if session__facts == null { session__facts = queue_new("session.facts") }
+ return session__facts
}
-export function ses_facts(base_st: mut BaseState, session_st: mut SessionState) -> Queue {
- if session_st.session__facts == null { session_st.session__facts = queue_new(base_st, "session.facts") }
- return session_st.session__facts
-}
-
-function session__fact(base_st: mut BaseState, session_st: mut SessionState, what: int, slot: int, pid: int) -> SessionFact {
+function session__fact(what: int, slot: int, pid: int) -> SessionFact {
let f = new SessionFact
f.what = what
f.slot = slot
f.pid = pid
- f.role = session_st.session__role
- q_push(base_st, ses_facts(base_st, session_st), f)
+ f.role = session__role
+ q_push(ses_facts(), f)
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__init() -> void {
+ if session__slot != null { return }
+ session__slot = new []PartyMember
+ for i in 0 .. PLY_MAX { push(session__slot, new PartyMember) }
+ session__slot[0].on = true
}
diff --git a/packages/ludic.session/tests/session_test.ludic b/packages/ludic.session/tests/session_test.ludic
index 4ecdf407..954efe43 100644
--- a/packages/ludic.session/tests/session_test.ludic
+++ b/packages/ludic.session/tests/session_test.ludic
@@ -4,176 +4,174 @@ import "ludic.session"
import "ludic.base"
program SessionTest {
numbers float
- state SessiontestState {
- me_x: float = 0.0
- me_z: float = 0.0
- }
- function fx(sessiontest_st: SessiontestState) -> float { return sessiontest_st.me_x }
+ var me_x: float = 0.0
+ var me_z: float = 0.0
+ function fx() -> float { return me_x }
function fy() -> float { return 0.0 }
- function fz(sessiontest_st: SessiontestState) -> float { return sessiontest_st.me_z }
+ function fz() -> float { return me_z }
function fname() -> string { return "me" }
bind SessionLocal { x: fn fx, y: fn fy, z: fn fz, name: fn fname }
- function whats(base_st: mut BaseState, session_st: mut SessionState) -> []int {
- let fs = q_drain(base_st, ses_facts(base_st, session_st))
+ function whats() -> []int {
+ let fs = q_drain(ses_facts())
let out = new []int
for i in 0 .. len(fs) { push(out, fs[i].what) }
return out
}
- function last_of(base_st: mut BaseState, session_st: mut SessionState, what: int) -> SessionFact {
- let fs = q_drain(base_st, ses_facts(base_st, session_st))
+ function last_of(what: int) -> SessionFact {
+ let fs = q_drain(ses_facts())
var f: SessionFact = null
for i in 0 .. len(fs) { if fs[i].what == what { f = fs[i] } }
return f
}
# a host with n guests, pids 2.., facts drained
- function party(base_st: mut BaseState, session_st: mut SessionState, n: int) -> void {
- ply_init(session_st)
- ses_set_role(base_st, session_st, SES_HOST)
- for i in 0 .. n { ply_join(base_st, session_st, 2 + i) }
- q_clear(base_st, ses_facts(base_st, session_st))
+ function party(n: int) -> void {
+ ply_init()
+ ses_set_role(SES_HOST)
+ for i in 0 .. n { ply_join(2 + i) }
+ q_clear(ses_facts())
}
- test "slot 0 is the local player, read live; others join at its feet and leave" (base_st: mut BaseState, session_st: mut SessionState, sessiontest_st: mut SessiontestState) {
- ply_init(session_st)
- sessiontest_st.me_x = 5.0
- expect(ply_on(session_st, 0))
- expect_near(ply_x(session_st, 0), 5.0, 0.001)
- expect(ply_name(session_st, 0) == "me")
- let p = ply_join(base_st, session_st, 7)
+ test "slot 0 is the local player, read live; others join at its feet and leave" {
+ ply_init()
+ me_x = 5.0
+ expect(ply_on(0))
+ expect_near(ply_x(0), 5.0, 0.001)
+ expect(ply_name(0) == "me")
+ let p = ply_join(7)
expect_eq(p, 1)
- expect_near(ply_x(session_st, p), 5.0, 0.001)
- expect_eq(ply_slot_of(session_st, 7), 1)
- expect_eq(ply_count(session_st), 2)
- expect_eq(ply_join(base_st, session_st, 0), -1)
- ply_leave(base_st, session_st, p)
- expect_eq(ply_slot_of(session_st, 7), -1)
- expect_eq(ply_count(session_st), 1)
- let w = whats(base_st, session_st)
+ expect_near(ply_x(p), 5.0, 0.001)
+ expect_eq(ply_slot_of(7), 1)
+ expect_eq(ply_count(), 2)
+ expect_eq(ply_join(0), -1)
+ ply_leave(p)
+ expect_eq(ply_slot_of(7), -1)
+ expect_eq(ply_count(), 1)
+ let w = whats()
expect_eq(len(w), 2)
expect_eq(w[0], SES_JOINED)
expect_eq(w[1], SES_LEFT)
}
- test "the roster fills at twelve" (base_st: mut BaseState, session_st: mut SessionState) {
- ply_init(session_st)
- for i in 0 .. PLY_MAX - 1 { expect(ply_join(base_st, session_st, 10 + i) > 0) }
- expect_eq(ply_join(base_st, session_st, 99), -1)
- expect_eq(ply_count(session_st), PLY_MAX)
+ test "the roster fills at twelve" {
+ ply_init()
+ for i in 0 .. PLY_MAX - 1 { expect(ply_join(10 + i) > 0) }
+ expect_eq(ply_join(99), -1)
+ expect_eq(ply_count(), PLY_MAX)
}
- test "the nearest player, whoever it is" (base_st: mut BaseState, session_st: mut SessionState, sessiontest_st: mut SessiontestState) {
- ply_init(session_st)
- sessiontest_st.me_x = 100.0
- let p = ply_join(base_st, session_st, 2)
- ply_report(session_st, p, 3.0, 0.0, 4.0, 0.0, 1.0)
- expect_near(ply_near(session_st, 0.0, 0.0), 5.0, 0.001)
- expect_eq(ply_near_i(session_st), p)
- expect_near(ply_dist(session_st, 0, 100.0, 0.0), 0.0, 0.001)
+ test "the nearest player, whoever it is" {
+ ply_init()
+ me_x = 100.0
+ let p = ply_join(2)
+ ply_report(p, 3.0, 0.0, 4.0, 0.0, 1.0)
+ expect_near(ply_near(0.0, 0.0), 5.0, 0.001)
+ expect_eq(ply_near_i(), p)
+ expect_near(ply_dist(0, 100.0, 0.0), 0.0, 0.001)
}
- test "roles: alone owns the world, a guest does not, and a change is a fact" (base_st: mut BaseState, 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))
- ses_set_role(base_st, session_st, SES_GUEST)
- expect(not ses_owns_world(session_st))
- expect(ses_live(session_st))
- expect(not ses_night_passes(session_st))
- let f = last_of(base_st, session_st, SES_ROLE)
+ test "roles: alone owns the world, a guest does not, and a change is a fact" {
+ ply_init()
+ expect(ses_owns_world())
+ expect(not ses_live())
+ expect(ses_night_passes())
+ ses_set_role(SES_GUEST)
+ expect(not ses_owns_world())
+ expect(ses_live())
+ expect(not ses_night_passes())
+ let f = last_of(SES_ROLE)
expect_eq(f.role, SES_GUEST)
- ses_set_role(base_st, session_st, SES_HOST)
- expect(ses_owns_world(session_st))
- expect(not ses_night_passes(session_st))
+ ses_set_role(SES_HOST)
+ expect(ses_owns_world())
+ expect(not ses_night_passes())
}
- test "the party's size and door" (base_st: mut BaseState, session_st: mut SessionState) {
- ses_set_party(session_st, 20, true)
- expect_eq(ses_max(session_st), PLY_MAX)
- ses_set_party(session_st, 3, true)
- party(base_st, session_st, 1)
- expect(ses_room_for_one(session_st))
- ply_join(base_st, session_st, 9)
- expect(not ses_room_for_one(session_st))
- ses_set_party(session_st, 12, false)
- expect(not ses_room_for_one(session_st))
+ test "the party's size and door" {
+ ses_set_party(20, true)
+ expect_eq(ses_max(), PLY_MAX)
+ ses_set_party(3, true)
+ party(1)
+ expect(ses_room_for_one())
+ ply_join(9)
+ expect(not ses_room_for_one())
+ ses_set_party(12, false)
+ expect(not ses_room_for_one())
}
- test "the night passes when the host and every guest have turned in" (base_st: mut BaseState, session_st: mut SessionState) {
- party(base_st, session_st, 2)
- expect(ses_sleep(base_st, session_st, true))
- expect_eq(ses_sleep_waiting(session_st), 2)
- ses_sleep_ready(base_st, session_st, 1, true)
- expect(last_of(base_st, session_st, SES_NIGHT) == null)
- ses_sleep_ready(base_st, session_st, 2, true)
- expect(last_of(base_st, session_st, SES_NIGHT) != null)
- ses_night_passed(session_st)
- expect(not ses_sleeping(session_st))
+ test "the night passes when the host and every guest have turned in" {
+ party(2)
+ expect(ses_sleep(true))
+ expect_eq(ses_sleep_waiting(), 2)
+ ses_sleep_ready(1, true)
+ expect(last_of(SES_NIGHT) == null)
+ ses_sleep_ready(2, true)
+ expect(last_of(SES_NIGHT) != null)
+ ses_night_passed()
+ expect(not ses_sleeping())
}
- test "a guest's turning in is a fact for the host to hear; alone a bed just sleeps" (base_st: mut BaseState, session_st: mut SessionState) {
- ply_init(session_st)
- expect(not ses_sleep(base_st, session_st, true))
- ses_set_role(base_st, session_st, SES_GUEST)
- q_clear(base_st, ses_facts(base_st, session_st))
- expect(ses_sleep(base_st, session_st, true))
- let f = last_of(base_st, session_st, SES_SLEEP)
+ test "a guest's turning in is a fact for the host to hear; alone a bed just sleeps" {
+ ply_init()
+ expect(not ses_sleep(true))
+ ses_set_role(SES_GUEST)
+ q_clear(ses_facts())
+ expect(ses_sleep(true))
+ let f = last_of(SES_SLEEP)
expect(f.yes)
- ses_sleep(base_st, session_st, false)
- expect(not ses_sleeping(session_st))
+ ses_sleep(false)
+ expect(not ses_sleeping())
}
- test "a vote passes on a majority, the host's yes counted" (base_st: mut BaseState, session_st: mut SessionState) {
- party(base_st, session_st, 2)
- expect_eq(ses_may(base_st, session_st, 4, 9, "set out"), SES_MAY_ASKED)
- expect(ses_vote_live(session_st))
- expect_eq(ses_may(base_st, session_st, 4, 9, "set out"), SES_MAY_BUSY)
- ses_vote_heard(base_st, session_st, ses_vote_id(session_st), true)
- let f = last_of(base_st, session_st, SES_VOTE_RESOLVED)
+ test "a vote passes on a majority, the host's yes counted" {
+ party(2)
+ expect_eq(ses_may(4, 9, "set out"), SES_MAY_ASKED)
+ expect(ses_vote_live())
+ expect_eq(ses_may(4, 9, "set out"), SES_MAY_BUSY)
+ ses_vote_heard(ses_vote_id(), true)
+ let f = last_of(SES_VOTE_RESOLVED)
expect(f.yes)
expect_eq(f.param, 9)
expect(f.text == "set out")
- expect(not ses_vote_live(session_st))
+ expect(not ses_vote_live())
}
- test "a vote fails on half saying no, or when the time runs out" (base_st: mut BaseState, session_st: mut SessionState) {
- party(base_st, session_st, 1)
- ses_vote_open(base_st, session_st, 1, 0, "a")
- ses_vote_heard(base_st, session_st, ses_vote_id(session_st), false)
- expect(not last_of(base_st, session_st, SES_VOTE_RESOLVED).yes)
- party(base_st, session_st, 3)
- ses_vote_open(base_st, session_st, 1, 0, "b")
- ses_vote_tick(base_st, session_st, 31.0)
- let f = last_of(base_st, session_st, SES_VOTE_RESOLVED)
+ test "a vote fails on half saying no, or when the time runs out" {
+ party(1)
+ ses_vote_open(1, 0, "a")
+ ses_vote_heard(ses_vote_id(), false)
+ expect(not last_of(SES_VOTE_RESOLVED).yes)
+ party(3)
+ ses_vote_open(1, 0, "b")
+ ses_vote_tick(31.0)
+ let f = last_of(SES_VOTE_RESOLVED)
expect(f != null)
expect(not f.yes)
}
- test "alone it may go; a guest may not, and its vote goes to the host" (base_st: mut BaseState, session_st: mut SessionState) {
- ply_init(session_st)
- expect_eq(ses_may(base_st, session_st, 1, 0, "x"), SES_MAY_GO)
- ses_set_role(base_st, session_st, SES_GUEST)
- expect_eq(ses_may(base_st, session_st, 1, 0, "x"), SES_MAY_HOSTS)
- ses_vote_told(session_st, 5, 1, "x")
- expect(ses_vote_live(session_st))
- q_clear(base_st, ses_facts(base_st, session_st))
- ses_vote_cast(base_st, session_st, true)
- let f = last_of(base_st, session_st, SES_VOTE_CAST)
+ test "alone it may go; a guest may not, and its vote goes to the host" {
+ ply_init()
+ expect_eq(ses_may(1, 0, "x"), SES_MAY_GO)
+ ses_set_role(SES_GUEST)
+ expect_eq(ses_may(1, 0, "x"), SES_MAY_HOSTS)
+ ses_vote_told(5, 1, "x")
+ expect(ses_vote_live())
+ q_clear(ses_facts())
+ ses_vote_cast(true)
+ let f = last_of(SES_VOTE_CAST)
expect_eq(f.id, 5)
expect(f.yes)
- ses_vote_ended(session_st, 5, true)
- expect_eq(ses_vote_passed(session_st), 1)
+ ses_vote_ended(5, true)
+ expect_eq(ses_vote_passed(), 1)
}
- test "a reset leaves everyone and stands alone" (base_st: mut BaseState, session_st: mut SessionState) {
- party(base_st, session_st, 2)
- ses_sleep(base_st, session_st, true)
- ses_reset(base_st, session_st)
- expect_eq(ply_count(session_st), 1)
- expect_eq(ses_role(session_st), SES_SOLO)
- expect(not ses_sleeping(session_st))
+ test "a reset leaves everyone and stands alone" {
+ party(2)
+ ses_sleep(true)
+ ses_reset()
+ expect_eq(ply_count(), 1)
+ expect_eq(ses_role(), SES_SOLO)
+ expect(not ses_sleeping())
}
}
diff --git a/packages/ludic.session/vote_state.ludic b/packages/ludic.session/vote_state.ludic
index e2bbc240..2c9c6b64 100644
--- a/packages/ludic.session/vote_state.ludic
+++ b/packages/ludic.session/vote_state.ludic
@@ -1,10 +1,10 @@
# vote_state.ludic - the vote as it stands
-export function ses_vote_live(session_st: SessionState) -> bool { return session_st.session__vote_t > 0.0 }
-export function ses_vote_id(session_st: SessionState) -> int { return session_st.session__vote_id }
-export function ses_vote_kind(session_st: SessionState) -> int { return session_st.session__vote_kind }
-export function ses_vote_text(session_st: SessionState) -> string { return session_st.session__vote_text }
-export function ses_vote_left(session_st: SessionState) -> float { return session_st.session__vote_t }
-export function ses_vote_yes(session_st: SessionState) -> int { return session_st.session__vote_yes }
-export function ses_vote_no(session_st: SessionState) -> int { return session_st.session__vote_no }
-export function ses_vote_mine(session_st: SessionState) -> int { return session_st.session__vote_mine }
-export function ses_vote_passed(session_st: SessionState) -> int { return session_st.session__vote_passed }
+export function ses_vote_live() -> bool { return session__vote_t > 0.0 }
+export function ses_vote_id() -> int { return session__vote_id }
+export function ses_vote_kind() -> int { return session__vote_kind }
+export function ses_vote_text() -> string { return session__vote_text }
+export function ses_vote_left() -> float { return session__vote_t }
+export function ses_vote_yes() -> int { return session__vote_yes }
+export function ses_vote_no() -> int { return session__vote_no }
+export function ses_vote_mine() -> int { return session__vote_mine }
+export function ses_vote_passed() -> int { return session__vote_passed }
diff --git a/packages/ludic.session/votes.ludic b/packages/ludic.session/votes.ludic
index 9ed35195..85c67170 100644
--- a/packages/ludic.session/votes.ludic
+++ b/packages/ludic.session/votes.ludic
@@ -8,95 +8,104 @@ export const SES_MAY_HOSTS: int = 3 # a guest: it is for the host to ask
const SESSION_VOTE_SECS: float = 30.0
+var session__vote_id: int = 0
+var session__vote_kind: int = 0
+var session__vote_param: int = 0
+var session__vote_text: string = ""
+var session__vote_t: float = 0.0
+var session__vote_yes: int = 0
+var session__vote_no: int = 0
+var session__vote_mine: int = -1 # -1 not voted, 0 no, 1 yes
+var session__vote_passed: int = -1 # the last vote: -1 none, 0 failed, 1 passed
# a world-changing act: may it go ahead now, or does the party have to be asked first?
-export function ses_may(base_st: mut BaseState, session_st: mut SessionState, kind: int, param: int, text: string) -> int {
- if not ses_live(session_st) { return SES_MAY_GO }
- if session_st.session__role == SES_GUEST { return SES_MAY_HOSTS }
- if ply_count(session_st) <= 1 { return SES_MAY_GO }
- if not ses_vote_open(base_st, session_st, kind, param, text) { return SES_MAY_BUSY }
+export function ses_may(kind: int, param: int, text: string) -> int {
+ if not ses_live() { return SES_MAY_GO }
+ if session__role == SES_GUEST { return SES_MAY_HOSTS }
+ if ply_count() <= 1 { return SES_MAY_GO }
+ if not ses_vote_open(kind, param, text) { return SES_MAY_BUSY }
return SES_MAY_ASKED
}
# the host puts it to the party, voting yes itself
-export function ses_vote_open(base_st: mut BaseState, session_st: mut SessionState, kind: int, param: int, text: string) -> bool {
- if session_st.session__role != SES_HOST or ses_vote_live(session_st) { return false }
- session_st.session__vote_id += 1
- session_st.session__vote_kind = kind
- session_st.session__vote_param = param
- session_st.session__vote_text = text
- session_st.session__vote_t = SESSION_VOTE_SECS
- session_st.session__vote_yes = 1
- session_st.session__vote_no = 0
- session_st.session__vote_mine = 1
- session_st.session__vote_passed = -1
- let f = session__vote_fact(base_st, session_st, SES_VOTE_OPENED)
+export function ses_vote_open(kind: int, param: int, text: string) -> bool {
+ if session__role != SES_HOST or ses_vote_live() { return false }
+ session__vote_id += 1
+ session__vote_kind = kind
+ session__vote_param = param
+ session__vote_text = text
+ session__vote_t = SESSION_VOTE_SECS
+ session__vote_yes = 1
+ session__vote_no = 0
+ session__vote_mine = 1
+ session__vote_passed = -1
+ let f = session__vote_fact(SES_VOTE_OPENED)
f.yes = true
- session__vote_check(base_st, session_st)
+ session__vote_check()
return true
}
# this player's answer; a guest's goes to the host as SES_VOTE_CAST
-export function ses_vote_cast(base_st: mut BaseState, session_st: mut SessionState, yes: bool) -> void {
- if not ses_vote_live(session_st) or session_st.session__vote_mine >= 0 { return }
- if yes { session_st.session__vote_mine = 1 } else { session_st.session__vote_mine = 0 }
- if session_st.session__role == SES_GUEST {
- let f = session__vote_fact(base_st, session_st, SES_VOTE_CAST)
+export function ses_vote_cast(yes: bool) -> void {
+ if not ses_vote_live() or session__vote_mine >= 0 { return }
+ if yes { session__vote_mine = 1 } else { session__vote_mine = 0 }
+ if session__role == SES_GUEST {
+ let f = session__vote_fact(SES_VOTE_CAST)
f.yes = yes
return
}
- ses_vote_heard(base_st, session_st, session_st.session__vote_id, yes)
+ ses_vote_heard(session__vote_id, yes)
}
# the host: a player's answer to vote `id`
-export function ses_vote_heard(base_st: mut BaseState, session_st: mut SessionState, id: int, yes: bool) -> void {
- if session_st.session__role != SES_HOST or id != session_st.session__vote_id or not ses_vote_live(session_st) { return }
- if yes { session_st.session__vote_yes += 1 } else { session_st.session__vote_no += 1 }
- session__vote_check(base_st, session_st)
+export function ses_vote_heard(id: int, yes: bool) -> void {
+ if session__role != SES_HOST or id != session__vote_id or not ses_vote_live() { return }
+ if yes { session__vote_yes += 1 } else { session__vote_no += 1 }
+ session__vote_check()
}
# a guest: the host opened vote `id`, or ended it
-export function ses_vote_told(session_st: mut SessionState, id: int, kind: int, text: string) -> void {
- session_st.session__vote_id = id
- session_st.session__vote_kind = kind
- session_st.session__vote_text = text
- session_st.session__vote_t = SESSION_VOTE_SECS
- session_st.session__vote_mine = -1
+export function ses_vote_told(id: int, kind: int, text: string) -> void {
+ session__vote_id = id
+ session__vote_kind = kind
+ session__vote_text = text
+ session__vote_t = SESSION_VOTE_SECS
+ session__vote_mine = -1
}
-export function ses_vote_ended(session_st: mut SessionState, id: int, passed: bool) -> void {
- if id != session_st.session__vote_id { return }
- session_st.session__vote_t = 0.0
- if passed { session_st.session__vote_passed = 1 } else { session_st.session__vote_passed = 0 }
+export function ses_vote_ended(id: int, passed: bool) -> void {
+ if id != session__vote_id { return }
+ session__vote_t = 0.0
+ if passed { session__vote_passed = 1 } else { session__vote_passed = 0 }
}
# the vote's thirty seconds; on the host, running out decides it
-export function ses_vote_tick(base_st: mut BaseState, session_st: mut SessionState, dt: float) -> void {
- if not ses_vote_live(session_st) { return }
- session_st.session__vote_t = session_st.session__vote_t - dt
- if session_st.session__role == SES_HOST and not ses_vote_live(session_st) { session__vote_finish(base_st, session_st) }
+export function ses_vote_tick(dt: float) -> void {
+ if not ses_vote_live() { return }
+ session__vote_t = session__vote_t - dt
+ if session__role == SES_HOST and not ses_vote_live() { session__vote_finish() }
}
-function session__vote_check(base_st: mut BaseState, session_st: mut SessionState) -> void {
- let n = ply_count(session_st)
- let yes = session_st.session__vote_yes
- let no = session_st.session__vote_no
- if yes * 2 > n or no * 2 >= n or yes + no >= n { session__vote_finish(base_st, session_st) }
+function session__vote_check() -> void {
+ let n = ply_count()
+ let yes = session__vote_yes
+ let no = session__vote_no
+ if yes * 2 > n or no * 2 >= n or yes + no >= n { session__vote_finish() }
}
-function session__vote_finish(base_st: mut BaseState, session_st: mut SessionState) -> void {
- let passed = session_st.session__vote_yes * 2 > ply_count(session_st)
- session_st.session__vote_t = 0.0
- if passed { session_st.session__vote_passed = 1 } else { session_st.session__vote_passed = 0 }
- let f = session__vote_fact(base_st, session_st, SES_VOTE_RESOLVED)
+function session__vote_finish() -> void {
+ let passed = session__vote_yes * 2 > ply_count()
+ session__vote_t = 0.0
+ if passed { session__vote_passed = 1 } else { session__vote_passed = 0 }
+ let f = session__vote_fact(SES_VOTE_RESOLVED)
f.yes = passed
}
-function session__vote_fact(base_st: mut BaseState, session_st: mut SessionState, what: int) -> SessionFact {
- let f = session__fact(base_st, session_st, what, -1, 0)
- f.id = session_st.session__vote_id
- f.kind = session_st.session__vote_kind
- f.param = session_st.session__vote_param
- f.text = session_st.session__vote_text
+function session__vote_fact(what: int) -> SessionFact {
+ let f = session__fact(what, -1, 0)
+ f.id = session__vote_id
+ f.kind = session__vote_kind
+ f.param = session__vote_param
+ f.text = session__vote_text
return f
}
diff --git a/packages/ludic.session/where.ludic b/packages/ludic.session/where.ludic
index d0d82a4c..142782ee 100644
--- a/packages/ludic.session/where.ludic
+++ b/packages/ludic.session/where.ludic
@@ -1,47 +1,47 @@
# where.ludic - who and where each player is; slot 0 answers through SessionLocal, live
-export function ply_pid(session_st: SessionState, p: int) -> int {
- if p <= 0 or not ply_on(session_st, p) { return 0 }
- return session_st.session__slot[p].pid
+export function ply_pid(p: int) -> int {
+ if p <= 0 or not ply_on(p) { return 0 }
+ return session__slot[p].pid
}
-export function ply_name(session_st: SessionState, p: int) -> string {
+export function ply_name(p: int) -> string {
if p == 0 { return SessionLocal.name() }
- if not ply_on(session_st, p) { return "" }
- return session_st.session__slot[p].name
+ if not ply_on(p) { return "" }
+ return session__slot[p].name
}
-export function ply_x(session_st: SessionState, p: int) -> float {
+export function ply_x(p: int) -> float {
if p == 0 { return SessionLocal.x() }
- return session_st.session__slot[p].x
+ return session__slot[p].x
}
-export function ply_y(session_st: SessionState, p: int) -> float {
+export function ply_y(p: int) -> float {
if p == 0 { return SessionLocal.y() }
- return session_st.session__slot[p].y
+ return session__slot[p].y
}
-export function ply_z(session_st: SessionState, p: int) -> float {
+export function ply_z(p: int) -> float {
if p == 0 { return SessionLocal.z() }
- return session_st.session__slot[p].z
+ return session__slot[p].z
}
-export function ply_yaw(session_st: SessionState, p: int) -> float {
+export function ply_yaw(p: int) -> float {
if p == 0 { return SessionLocal.yaw() }
- return session_st.session__slot[p].yaw
+ return session__slot[p].yaw
}
-export function ply_speed(session_st: SessionState, p: int) -> float {
+export function ply_speed(p: int) -> float {
if p == 0 { return SessionLocal.speed() }
- return session_st.session__slot[p].speed
+ return session__slot[p].speed
}
# how much a player's rest speeds the clock: their seat's or the ground's, 1 on their feet
-export function ply_rest(session_st: SessionState, p: int) -> float {
+export function ply_rest(p: int) -> float {
if p == 0 { return SessionLocal.rest() }
- return session_st.session__slot[p].rest
+ return session__slot[p].rest
}
-export function ply_sleeping(session_st: SessionState, p: int) -> bool {
- if p == 0 { return session_st.session__me_sleeping }
- return ply_on(session_st, p) and session_st.session__slot[p].sleeping
+export function ply_sleeping(p: int) -> bool {
+ if p == 0 { return session__me_sleeping }
+ return ply_on(p) and session__slot[p].sleeping
}
diff --git a/packages/ludic.settings/file.ludic b/packages/ludic.settings/file.ludic
index 2fa98e7a..556de859 100644
--- a/packages/ludic.settings/file.ludic
+++ b/packages/ludic.settings/file.ludic
@@ -1,54 +1,54 @@
# 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() -> Val {
+ sg_init()
let v = Value.object()
- for i in 0 .. len(settings_st.sg_defs) { settings_put(settings_st, v, i) }
+ for i in 0 .. len(sg_defs) { settings_put(v, i) }
return v
}
# one setting into `v` under its key (a game building its own file adds its extras beside them)
-export function settings_put(settings_st: SettingsState, v: Val, id: int) -> void {
- let d = settings_st.sg_defs[id]
+export function settings_put(v: Val, id: int) -> void {
+ let d = sg_defs[id]
if not d.saved { return }
if d.kind == SETTING_TEXT {
- sv_put_str(v, d.key, settings_st.sg_text[id])
+ sv_put_str(v, d.key, sg_text[id])
} else if d.kind == SETTING_FLOAT {
- sv_put_float(v, d.key, settings_st.sg_num[id])
+ sv_put_float(v, d.key, sg_num[id])
} else {
- sv_put_int(v, d.key, int(settings_st.sg_num[id]))
+ sv_put_int(v, d.key, int(sg_num[id]))
}
}
# 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)
+export function settings_from_json(v: Val) -> void {
+ sg_init()
if v == null { return }
- for i in 0 .. len(settings_st.sg_defs) { sg_read(settings_st, v, i) }
+ for i in 0 .. len(sg_defs) { sg_read(v, i) }
}
-function sg_read(settings_st: mut SettingsState, v: Val, id: int) -> void {
- let d = settings_st.sg_defs[id]
+function sg_read(v: Val, id: int) -> void {
+ let d = sg_defs[id]
if not d.saved or Value.kind(v) != 6 or Value.has(v, d.key) == 0 { return }
if d.kind == SETTING_TEXT {
- settings_st.sg_text[id] = sv_str(v, d.key, settings_st.sg_text[id])
+ sg_text[id] = sv_str(v, d.key, sg_text[id])
} else if d.kind == SETTING_FLOAT {
- settings_st.sg_num[id] = settings_held(settings_st, id, sv_float(v, d.key, settings_st.sg_num[id]))
+ sg_num[id] = settings_held(id, sv_float(v, d.key, sg_num[id]))
} else {
- settings_st.sg_num[id] = settings_held(settings_st, id, float(sv_int(v, d.key, int(settings_st.sg_num[id]))))
+ sg_num[id] = settings_held(id, float(sv_int(v, d.key, int(sg_num[id]))))
}
}
# one setting from a JSON value in the file's own form (a row in a settings screen hands these):
# a change is a fact
-export function settings_set_value(base_st: mut BaseState, settings_st: mut SettingsState, id: int, x: Val) -> void {
- let d = settings_st.sg_defs[id]
+export function settings_set_value(id: int, x: Val) -> void {
+ let d = sg_defs[id]
if d.kind == SETTING_TEXT {
- if Value.kind(x) == 4 { settings_set_text(base_st, settings_st, id, Value.as_str(x)) }
+ if Value.kind(x) == 4 { settings_set_text(id, Value.as_str(x)) }
} else if d.kind == SETTING_FLOAT {
- settings_set_float(base_st, settings_st, id, float(Value.as_int(x)) / 1000.0)
+ settings_set_float(id, float(Value.as_int(x)) / 1000.0)
} else {
- settings_set_int(base_st, settings_st, id, Value.as_int(x))
+ settings_set_int(id, Value.as_int(x))
}
}
diff --git a/packages/ludic.settings/rules.ludic b/packages/ludic.settings/rules.ludic
index 48102475..e5c42278 100644
--- a/packages/ludic.settings/rules.ludic
+++ b/packages/ludic.settings/rules.ludic
@@ -1,35 +1,35 @@
# 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
+export function settings_reset_all() -> void {
+ sg_init()
+ for i in 0 .. len(sg_defs) {
+ sg_num[i] = sg_defs[i].num
+ sg_text[i] = sg_defs[i].text
}
}
# 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]) }
+export function settings_clamp_all() -> void {
+ sg_init()
+ for i in 0 .. len(sg_defs) { sg_num[i] = settings_held(i, 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
- settings_st.sg_text[i] = settings_st.sg_defs[i].text
+export function settings_rescue() -> void {
+ sg_init()
+ for i in 0 .. len(sg_defs) {
+ if sg_defs[i].rescue {
+ sg_num[i] = sg_defs[i].num
+ sg_text[i] = sg_defs[i].text
}
}
}
# whether a setting is on the rescue list at its safe value
-export function settings_is_safe(settings_st: SettingsState, id: int) -> bool {
- let d = settings_st.sg_defs[id]
+export function settings_is_safe(id: int) -> bool {
+ let d = sg_defs[id]
if not d.rescue { return true }
- return settings_st.sg_num[id] == d.num and settings_st.sg_text[id] == d.text
+ return sg_num[id] == d.num and sg_text[id] == d.text
}
diff --git a/packages/ludic.settings/store.ludic b/packages/ludic.settings/store.ludic
index 34c930bf..7b2ef165 100644
--- a/packages/ludic.settings/store.ludic
+++ b/packages/ludic.settings/store.ludic
@@ -1,90 +1,88 @@
# 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_changes: Queue = null
+var sg_defs: []SettingDef = null
+var sg_num: []float = null
+var sg_text: []string = null
+var sg_changes: Queue = null
+
+function sg_init() -> void {
+ if sg_defs != null { return }
+ sg_defs = new []SettingDef
+ sg_num = new []float
+ sg_text = new []string
}
-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(base_st: mut BaseState, settings_st: mut SettingsState) -> Queue {
- if settings_st.sg_changes == null { settings_st.sg_changes = queue_new(base_st, "settings.changes") }
- return settings_st.sg_changes
+export function settings_changes() -> Queue {
+ if sg_changes == null { sg_changes = queue_new("settings.changes") }
+ return sg_changes
}
# forget every setting (a test, or a game that declares its table again)
-export function settings_clear(base_st: mut BaseState, settings_st: mut SettingsState) -> void {
- settings_st.sg_defs = new []SettingDef
- settings_st.sg_num = new []float
- settings_st.sg_text = new []string
- q_clear(base_st, settings_changes(base_st, settings_st))
+export function settings_clear() -> void {
+ sg_defs = new []SettingDef
+ sg_num = new []float
+ sg_text = new []string
+ q_clear(settings_changes())
}
# 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_add(d: SettingDef) -> int {
+ sg_init()
+ push(sg_defs, d)
+ push(sg_num, d.num)
+ push(sg_text, d.text)
+ return len(sg_defs) - 1
}
-export function settings_count(settings_st: mut SettingsState) -> int {
- sg_init(settings_st)
- return len(settings_st.sg_defs)
+export function settings_count() -> int {
+ sg_init()
+ return len(sg_defs)
}
-export function settings_def(settings_st: SettingsState, id: int) -> SettingDef { return settings_st.sg_defs[id] }
+export function settings_def(id: int) -> SettingDef { return 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)
- for i in 0 .. len(settings_st.sg_defs) { if settings_st.sg_defs[i].key == key { return i } }
+export function settings_id_of(key: string) -> int {
+ sg_init()
+ for i in 0 .. len(sg_defs) { if sg_defs[i].key == key { return i } }
return -1
}
-export function settings_get_float(settings_st: SettingsState, id: int) -> float { return settings_st.sg_num[id] }
-export function settings_get_int(settings_st: SettingsState, id: int) -> int { return int(settings_st.sg_num[id]) }
-export function settings_get_bool(settings_st: SettingsState, id: int) -> bool { return settings_st.sg_num[id] != 0.0 }
-export function settings_get_text(settings_st: SettingsState, id: int) -> string { return settings_st.sg_text[id] }
+export function settings_get_float(id: int) -> float { return sg_num[id] }
+export function settings_get_int(id: int) -> int { return int(sg_num[id]) }
+export function settings_get_bool(id: int) -> bool { return sg_num[id] != 0.0 }
+export function settings_get_text(id: int) -> string { return sg_text[id] }
-function sg_changed(base_st: mut BaseState, settings_st: mut SettingsState, id: int) -> void {
+function sg_changed(id: int) -> void {
let c = new SettingChanged
c.id = id
- c.key = settings_st.sg_defs[id].key
- q_push(base_st, settings_changes(base_st, settings_st), c)
+ c.key = sg_defs[id].key
+ q_push(settings_changes(), c)
}
# a value outside the setting's range is its default: a damaged file never reaches the game
-export function settings_set_float(base_st: mut BaseState, settings_st: mut SettingsState, id: int, v: float) -> void {
- let h = settings_held(settings_st, id, v)
- if h == settings_st.sg_num[id] { return }
- settings_st.sg_num[id] = h
- sg_changed(base_st, settings_st, id)
+export function settings_set_float(id: int, v: float) -> void {
+ let h = settings_held(id, v)
+ if h == sg_num[id] { return }
+ sg_num[id] = h
+ sg_changed(id)
}
-export function settings_set_int(base_st: mut BaseState, settings_st: mut SettingsState, id: int, v: int) -> void { settings_set_float(base_st, settings_st, id, float(v)) }
+export function settings_set_int(id: int, v: int) -> void { settings_set_float(id, float(v)) }
-export function settings_set_bool(base_st: mut BaseState, settings_st: mut SettingsState, id: int, v: bool) -> void {
- if v { settings_set_float(base_st, settings_st, id, 1.0) } else { settings_set_float(base_st, settings_st, id, 0.0) }
+export function settings_set_bool(id: int, v: bool) -> void {
+ if v { settings_set_float(id, 1.0) } else { settings_set_float(id, 0.0) }
}
-export function settings_set_text(base_st: mut BaseState, settings_st: mut SettingsState, id: int, v: string) -> void {
- if v == settings_st.sg_text[id] { return }
- settings_st.sg_text[id] = v
- sg_changed(base_st, settings_st, id)
+export function settings_set_text(id: int, v: string) -> void {
+ if v == sg_text[id] { return }
+ sg_text[id] = v
+ sg_changed(id)
}
# `v` held to the setting's range: outside it, the default
-export function settings_held(settings_st: SettingsState, id: int, v: float) -> float {
- let d = settings_st.sg_defs[id]
+export function settings_held(id: int, v: float) -> float {
+ let d = sg_defs[id]
var x = v
if d.kind == SETTING_BOOL {
if x != 0.0 { x = 1.0 }
diff --git a/packages/ludic.settings/tests/settings_test.ludic b/packages/ludic.settings/tests/settings_test.ludic
index 20a47e67..c8df2d3c 100644
--- a/packages/ludic.settings/tests/settings_test.ludic
+++ b/packages/ludic.settings/tests/settings_test.ludic
@@ -6,70 +6,70 @@ program SettingsTest {
numbers float
registry Toy of SettingDef as TOY from "packages/ludic.settings/tests/toy.lres"
- function declare(base_st: mut BaseState, settings_st: mut SettingsState) -> void {
- settings_clear(base_st, settings_st)
- for i in 0 .. TOY_COUNT { settings_add(settings_st, Toy[i]) }
+ function declare() -> void {
+ settings_clear()
+ for i in 0 .. TOY_COUNT { settings_add(Toy[i]) }
}
- function facts(base_st: mut BaseState, settings_st: mut SettingsState) -> []SettingChanged { return q_drain(base_st, settings_changes(base_st, settings_st)) }
+ function facts() -> []SettingChanged { return q_drain(settings_changes()) }
- test "a registry declares the store, each at its default" (base_st: mut BaseState, settings_st: mut SettingsState) {
- declare(base_st, settings_st)
- expect_eq(settings_count(settings_st), 5)
- expect(settings_get_bool(settings_st, TOY_BLOOM))
- expect_eq(settings_get_int(settings_st, TOY_SHADOW_Q), 2)
- expect(settings_get_float(settings_st, TOY_RSCALE) > 0.77)
- expect(settings_get_text(settings_st, TOY_LANG) == "en")
- expect_eq(settings_id_of(settings_st, "rscale"), TOY_RSCALE)
- expect_eq(settings_id_of(settings_st, "nothing"), -1)
+ test "a registry declares the store, each at its default" {
+ declare()
+ expect_eq(settings_count(), 5)
+ expect(settings_get_bool(TOY_BLOOM))
+ expect_eq(settings_get_int(TOY_SHADOW_Q), 2)
+ expect(settings_get_float(TOY_RSCALE) > 0.77)
+ expect(settings_get_text(TOY_LANG) == "en")
+ expect_eq(settings_id_of("rscale"), TOY_RSCALE)
+ expect_eq(settings_id_of("nothing"), -1)
}
- test "a change is a fact, and setting what is there is none" (base_st: mut BaseState, settings_st: mut SettingsState) {
- declare(base_st, settings_st)
- settings_set_int(base_st, settings_st, TOY_SHADOW_Q, 3)
- settings_set_int(base_st, settings_st, TOY_SHADOW_Q, 3)
- settings_set_text(base_st, settings_st, TOY_LANG, "tr")
- let fs = facts(base_st, settings_st)
+ test "a change is a fact, and setting what is there is none" {
+ declare()
+ settings_set_int(TOY_SHADOW_Q, 3)
+ settings_set_int(TOY_SHADOW_Q, 3)
+ settings_set_text(TOY_LANG, "tr")
+ let fs = facts()
expect_eq(len(fs), 2)
expect(fs[0].key == "shadow_q")
expect_eq(fs[1].id, TOY_LANG)
}
- test "a value out of range is the default" (base_st: mut BaseState, settings_st: mut SettingsState) {
- declare(base_st, settings_st)
- settings_set_int(base_st, settings_st, TOY_SHADOW_Q, 9)
- expect_eq(settings_get_int(settings_st, TOY_SHADOW_Q), 2)
- settings_set_float(base_st, settings_st, TOY_RSCALE, 0.2)
- expect(settings_get_float(settings_st, TOY_RSCALE) > 0.77)
+ test "a value out of range is the default" {
+ declare()
+ settings_set_int(TOY_SHADOW_Q, 9)
+ expect_eq(settings_get_int(TOY_SHADOW_Q), 2)
+ settings_set_float(TOY_RSCALE, 0.2)
+ expect(settings_get_float(TOY_RSCALE) > 0.77)
}
- test "a rescue puts back only what it lists" (base_st: mut BaseState, settings_st: mut SettingsState) {
- declare(base_st, settings_st)
- settings_set_int(base_st, settings_st, TOY_SHADOW_Q, 0)
- settings_set_bool(base_st, settings_st, TOY_BLOOM, false)
- expect(not settings_is_safe(settings_st, TOY_SHADOW_Q))
- settings_rescue(settings_st)
- expect_eq(settings_get_int(settings_st, TOY_SHADOW_Q), 2)
- expect(not settings_get_bool(settings_st, TOY_BLOOM))
+ test "a rescue puts back only what it lists" {
+ declare()
+ settings_set_int(TOY_SHADOW_Q, 0)
+ settings_set_bool(TOY_BLOOM, false)
+ expect(not settings_is_safe(TOY_SHADOW_Q))
+ settings_rescue()
+ expect_eq(settings_get_int(TOY_SHADOW_Q), 2)
+ expect(not settings_get_bool(TOY_BLOOM))
}
- test "the file is by key, and what it lacks or cannot hold keeps the store's" (base_st: mut BaseState, settings_st: mut SettingsState) {
- declare(base_st, settings_st)
- settings_set_float(base_st, settings_st, TOY_RSCALE, 0.9)
- settings_set_text(base_st, settings_st, TOY_LANG, "tr")
- settings_set_bool(base_st, settings_st, TOY_HELD, true)
- let text = Json.encode(settings_to_json(settings_st))
- declare(base_st, settings_st)
- settings_set_bool(base_st, settings_st, TOY_BLOOM, false)
+ test "the file is by key, and what it lacks or cannot hold keeps the store's" {
+ declare()
+ settings_set_float(TOY_RSCALE, 0.9)
+ settings_set_text(TOY_LANG, "tr")
+ settings_set_bool(TOY_HELD, true)
+ let text = Json.encode(settings_to_json())
+ declare()
+ settings_set_bool(TOY_BLOOM, false)
let v = Json.parse(text)
Value.put(v, "shadow_q", Value.int(7))
Value.put(v, "stranger", Value.int(1))
- settings_from_json(settings_st, v)
- expect(settings_get_float(settings_st, TOY_RSCALE) > 0.89)
- expect(settings_get_text(settings_st, TOY_LANG) == "tr")
- expect_eq(settings_get_int(settings_st, TOY_SHADOW_Q), 2)
- expect(settings_get_bool(settings_st, TOY_BLOOM))
- expect(not settings_get_bool(settings_st, TOY_HELD))
- expect(Value.has(settings_to_json(settings_st), "held") == 0)
+ settings_from_json(v)
+ expect(settings_get_float(TOY_RSCALE) > 0.89)
+ expect(settings_get_text(TOY_LANG) == "tr")
+ expect_eq(settings_get_int(TOY_SHADOW_Q), 2)
+ expect(settings_get_bool(TOY_BLOOM))
+ expect(not settings_get_bool(TOY_HELD))
+ expect(Value.has(settings_to_json(), "held") == 0)
}
}
diff --git a/packages/ludic.shooter/shooter.ludic b/packages/ludic.shooter/shooter.ludic
index ca38bbba..7170f36d 100644
--- a/packages/ludic.shooter/shooter.ludic
+++ b/packages/ludic.shooter/shooter.ludic
@@ -69,79 +69,76 @@ 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_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_fire_color: int = 0xffffff # the colour of the weapon being fired (sh_fire)
-}
+var wpn_reg: Dict = null
+var wpn_n: int = 0
+var wpn_rate: words = null
+var wpn_dmg: words = null
+var wpn_speed: words = null
+var wpn_spread: words = null
+var wpn_pellets: words = null
+var wpn_pattern: words = null
+var wpn_ttl: words = null
+var wpn_pierce: words = null
+var wpn_homing: words = null
+var wpn_color: words = null
-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)
+function wpn_ensure() -> void {
+ if wpn_reg == null {
+ wpn_reg = Dict.new()
+ wpn_rate = words(WPN_MAX); wpn_dmg = words(WPN_MAX); wpn_speed = words(WPN_MAX)
+ wpn_spread = words(WPN_MAX); wpn_pellets = words(WPN_MAX); wpn_pattern = words(WPN_MAX)
+ wpn_ttl = words(WPN_MAX); wpn_pierce = words(WPN_MAX); wpn_homing = words(WPN_MAX)
+ wpn_color = words(WPN_MAX); 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
- shooter_st.wpn_spread[id] = spread; shooter_st.wpn_pellets[id] = pellets; shooter_st.wpn_pattern[id] = pattern
- shooter_st.wpn_ttl[id] = 120
- shooter_st.wpn_color[id] = 0xffffff
- shooter_st.wpn_rate_base[id] = fire_rate
- shooter_st.wpn_n += 1
- Dict.set(shooter_st.wpn_reg, name, id)
+@Namespace(Weapon) function weapon_def(name: pointer, fire_rate: int, damage: int, speed: int, spread: int, pellets: int, pattern: int) -> int {
+ wpn_ensure()
+ if wpn_n >= WPN_MAX { return -1 }
+ let id = wpn_n
+ wpn_rate[id] = fire_rate; wpn_dmg[id] = damage; wpn_speed[id] = speed
+ wpn_spread[id] = spread; wpn_pellets[id] = pellets; wpn_pattern[id] = pattern
+ wpn_ttl[id] = 120
+ wpn_color[id] = 0xffffff
+ wpn_rate_base[id] = fire_rate
+ wpn_n += 1
+ Dict.set(wpn_reg, name, id)
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)
+@Namespace(Weapon) function weapon_id(name: pointer) -> int {
+ wpn_ensure()
+ return Dict.get_or(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 }
+@Namespace(Weapon) function weapon_set_rate(id: int, frames: int) -> void {
+ wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { 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
+@Namespace(Weapon) function weapon_rate(id: int) -> int {
+ wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { return 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 }
- shooter_st.wpn_pierce[id] = 0; shooter_st.wpn_homing[id] = 0; shooter_st.wpn_rate[id] = shooter_st.wpn_rate_base[id]
+var wpn_rate_base: words = null
+@Namespace(Weapon) function weapon_reset(id: int) -> void {
+ wpn_ensure(); if (id < 0) or (id >= WPN_MAX) { return }
+ wpn_pierce[id] = 0; wpn_homing[id] = 0; wpn_rate[id] = 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 }
+@Namespace(Weapon) function weapon_set_color(id: int, color: int) -> void {
+ wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { 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 }
+@Namespace(Weapon) function weapon_set_ttl(id: int, frames: int) -> void {
+ wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { 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 }
+@Namespace(Weapon) function weapon_set_pierce(id: int, n: int) -> void {
+ wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { 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 }
+@Namespace(Weapon) function weapon_set_homing(id: int, deg: int) -> void {
+ wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_homing[id] = deg }
}
# ---------------------------------------------------------------------------
@@ -496,7 +493,7 @@ function sh_angle_diff(from: int, to: int) -> int {
}
# spawn one projectile travelling at `deg` degrees from (px,py).
-function sh_spawn_bullet(shooter_st: ShooterState, owner: int, fac: int, px: int, py: int, deg: int, speed: int, dmg: int, ttl: int, pierce: int, homing: int) -> int {
+function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed: int, dmg: int, ttl: int, pierce: int, homing: int) -> int {
let m = World.model_id("Bullet")
if m < 0 { return -1 }
let b = World.spawn(m)
@@ -521,20 +518,21 @@ function sh_spawn_bullet(shooter_st: ShooterState, owner: int, fac: int, px: int
World.set(b, PC, World.field_id(PC, "h"), 4)
}
# announce it: a listener may veto (cancel -> despawn) or tweak the Projectile.
- pr_set(b, "color", shooter_st.wpn_fire_color)
+ pr_set(b, "color", wpn_fire_color)
if emit ProjectileSpawned(proj: b, owner: owner, damage: dmg) != 0 { despawn b; return -1 }
return b
}
+var wpn_fire_color: int = 0xffffff # the colour of the weapon being fired (sh_fire)
# ===========================================================================
# esys_weapon (Update) — fire-rate gating + pattern emission. Reads the owner's
# TopDown.aim_angle + Weapon.want-fire-equivalent (TopDown.want_fire); one impl
# serves player and NPC.
# ===========================================================================
-@EngineSystem(Weapon, Update) function esys_weapon(shooter_st: mut ShooterState) -> void {
+@EngineSystem(Weapon, Update) function esys_weapon() -> void {
let P = World.prop_id("Weapon")
if P < 0 { return }
- wpn_ensure(shooter_st)
+ wpn_ensure()
var e = World.query_next(P, 0)
while e >= 0 {
let cd = World.get(e, P, World.field_id(P, "cooldown"))
@@ -542,34 +540,34 @@ function sh_spawn_bullet(shooter_st: ShooterState, owner: int, fac: int, px: int
let fire = td_get(e, "want_fire")
if (fire == 1) and (cd <= 0) {
let id = World.get(e, P, World.field_id(P, "def_id"))
- if (id >= 0) and (id < shooter_st.wpn_n) {
- sh_fire(shooter_st, e, id)
- World.set(e, P, World.field_id(P, "cooldown"), shooter_st.wpn_rate[id] * stats_field(e, "fire_rate_pct", 100) / 100)
+ if (id >= 0) and (id < wpn_n) {
+ sh_fire(e, id)
+ World.set(e, P, World.field_id(P, "cooldown"), wpn_rate[id] * stats_field(e, "fire_rate_pct", 100) / 100)
}
}
e = World.query_next(P, e + 1)
}
}
-function sh_fire(shooter_st: mut ShooterState, owner: int, id: int) -> void {
+function sh_fire(owner: int, id: int) -> void {
let px = sh_cx(owner)
let py = sh_cy(owner)
let base = td_get(owner, "aim_angle")
let fac = Faction.id_of(owner)
- let speed = shooter_st.wpn_speed[id]
- let dmg = shooter_st.wpn_dmg[id]
- let spread = shooter_st.wpn_spread[id]
- let pellets0 = shooter_st.wpn_pellets[id]
- let pattern = shooter_st.wpn_pattern[id]
- let ttl = shooter_st.wpn_ttl[id]
- let pierce = shooter_st.wpn_pierce[id]
- let homing = shooter_st.wpn_homing[id]
- shooter_st.wpn_fire_color = shooter_st.wpn_color[id]
+ let speed = wpn_speed[id]
+ let dmg = wpn_dmg[id]
+ let spread = wpn_spread[id]
+ let pellets0 = wpn_pellets[id]
+ let pattern = wpn_pattern[id]
+ let ttl = wpn_ttl[id]
+ let pierce = wpn_pierce[id]
+ let homing = wpn_homing[id]
+ wpn_fire_color = wpn_color[id]
var pellets = pellets0; if pellets < 1 { pellets = 1 }
emit WeaponFired(owner: owner, def_id: id)
if pattern == WeaponPattern.Ring { # pellets around 360
var i = 0
- while i < pellets { sh_spawn_bullet(shooter_st, owner, fac, px, py, sh_wrap_deg(base + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
+ while i < pellets { sh_spawn_bullet(owner, fac, px, py, sh_wrap_deg(base + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
return
}
if pattern == WeaponPattern.Spiral { # ring + a rotating phase
@@ -577,18 +575,18 @@ function sh_fire(shooter_st: mut ShooterState, owner: int, id: int) -> void {
let fph = World.field_id(PW, "phase")
var ph = 0; if fph >= 0 { ph = World.get(owner, PW, fph) }
var i = 0
- while i < pellets { sh_spawn_bullet(shooter_st, owner, fac, px, py, sh_wrap_deg(base + ph + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
+ while i < pellets { sh_spawn_bullet(owner, fac, px, py, sh_wrap_deg(base + ph + i * 360 / pellets), speed, dmg, ttl, pierce, homing); i += 1 }
if fph >= 0 { World.set(owner, PW, fph, sh_wrap_deg(ph + 15)) }
return
}
# single / spread cone centred on the aim
if pellets == 1 {
- sh_spawn_bullet(shooter_st, owner, fac, px, py, base, speed, dmg, ttl, pierce, homing)
+ sh_spawn_bullet(owner, fac, px, py, base, speed, dmg, ttl, pierce, homing)
} else {
var i = 0
while i < pellets {
let off = -(spread / 2) + i * spread / (pellets - 1)
- sh_spawn_bullet(shooter_st, owner, fac, px, py, sh_wrap_deg(base + off), speed, dmg, ttl, pierce, homing)
+ sh_spawn_bullet(owner, fac, px, py, sh_wrap_deg(base + off), speed, dmg, ttl, pierce, homing)
i += 1
}
}
@@ -729,8 +727,8 @@ event WaveCleared { spawner: int = 0, wave: int = 0 }
@Namespace(TopDown) function topdown_aim(e: int, deg: int) -> void { td_set(e, "aim_angle", deg) }
@Namespace(TopDown) function topdown_fire(e: int, on: int) -> void { td_set(e, "want_fire", on) }
# equip a weapon by registry name.
-@Namespace(Weapon) function weapon_equip(shooter_st: mut ShooterState, e: int, name: pointer) -> void {
+@Namespace(Weapon) function weapon_equip(e: int, name: pointer) -> void {
let P = World.prop_id("Weapon")
if P < 0 { return }
- World.set(e, P, World.field_id(P, "def_id"), weapon_id(shooter_st, name))
+ World.set(e, P, World.field_id(P, "def_id"), weapon_id(name))
}
diff --git a/packages/ludic.shop/demand.ludic b/packages/ludic.shop/demand.ludic
index c3a98bd7..7cf99bda 100644
--- a/packages/ludic.shop/demand.ludic
+++ b/packages/ludic.shop/demand.ludic
@@ -1,22 +1,31 @@
# demand.ludic - what each vendor is short of this week, and the one thing it is full of: rolled
# from the shop's own dice, seeded by the week, so it never moves anyone else's rolls
+var shop_wants_n: int = 3
+var shop_want_mult: float = 1.5
+var shop_glut_mult: float = 0.6
+var shop_week_days: int = 7
+var shop_seed: int = 1013
+var shop_week_rolled: int = -1
+var shop_want_list: []int = null # shop_wants_n per vendor, -1 empty
+var shop_full_list: []int = null # one per vendor, -1 none
+var shop_dice: Rng = null
# how many things a vendor wants a week, what that pays and what a glut pays, the week, the seed
-export function shop_config_demand(shop_st: mut ShopState, wants: int, want_mult: float, glut_mult: float, days: int, seed: int) -> void {
- shop_st.shop_wants_n = wants
- shop_st.shop_want_mult = want_mult
- shop_st.shop_glut_mult = glut_mult
- shop_st.shop_week_days = days
- shop_st.shop_seed = seed
- shop_st.shop_week_rolled = -1
+export function shop_config_demand(wants: int, want_mult: float, glut_mult: float, days: int, seed: int) -> void {
+ shop_wants_n = wants
+ shop_want_mult = want_mult
+ shop_glut_mult = glut_mult
+ shop_week_days = days
+ shop_seed = seed
+ shop_week_rolled = -1
}
-export function shop_week(shop_st: ShopState) -> int { return (ShopWorld.day() - 1) / shop_st.shop_week_days }
+export function shop_week() -> int { return (ShopWorld.day() - 1) / shop_week_days }
-function shop_pick_one(shop_st: ShopState, v: int, taken: []int) -> int {
+function shop_pick_one(v: int, taken: []int) -> int {
for t in 0 .. 200 {
- let it = rng_between(shop_st.shop_dice, 0, shop_items(shop_st) - 1)
- if not shop_buys(shop_st, v, it) { continue }
+ let it = rng_between(shop_dice, 0, shop_items() - 1)
+ if not shop_buys(v, it) { continue }
var dup = false
for k in 0 .. len(taken) { if taken[k] == it { dup = true } }
if not dup { return it }
@@ -24,72 +33,72 @@ function shop_pick_one(shop_st: ShopState, v: int, taken: []int) -> int {
return -1
}
-function shop_roll_vendor(shop_st: mut ShopState, v: int) -> void {
+function shop_roll_vendor(v: int) -> void {
let taken = new []int
- for k in 0 .. shop_st.shop_wants_n {
- let it = shop_pick_one(shop_st, v, taken)
- shop_st.shop_want_list[v * shop_st.shop_wants_n + k] = it
+ for k in 0 .. shop_wants_n {
+ let it = shop_pick_one(v, taken)
+ shop_want_list[v * shop_wants_n + k] = it
if it >= 0 { push(taken, it) }
}
- shop_st.shop_full_list[v] = shop_pick_one(shop_st, v, taken)
+ shop_full_list[v] = shop_pick_one(v, taken)
}
# the week's lists, rolled again only when the week turns
-function shop_roll(shop_st: mut ShopState) -> void {
- let week = shop_week(shop_st)
- if week == shop_st.shop_week_rolled and shop_st.shop_want_list != null { return }
- shop_st.shop_week_rolled = week
- shop_st.shop_dice = rng_new(shop_st.shop_seed + week * 37)
- shop_st.shop_want_list = new []int
- shop_st.shop_full_list = new []int
- for i in 0 .. shop_vendors(shop_st) * shop_st.shop_wants_n { push(shop_st.shop_want_list, -1) }
- for v in 0 .. shop_vendors(shop_st) { push(shop_st.shop_full_list, -1) }
- if shop_items(shop_st) == 0 { return }
- for v in 0 .. shop_vendors(shop_st) { shop_roll_vendor(shop_st, v) }
+function shop_roll() -> void {
+ let week = shop_week()
+ if week == shop_week_rolled and shop_want_list != null { return }
+ shop_week_rolled = week
+ shop_dice = rng_new(shop_seed + week * 37)
+ shop_want_list = new []int
+ shop_full_list = new []int
+ for i in 0 .. shop_vendors() * shop_wants_n { push(shop_want_list, -1) }
+ for v in 0 .. shop_vendors() { push(shop_full_list, -1) }
+ if shop_items() == 0 { return }
+ for v in 0 .. shop_vendors() { shop_roll_vendor(v) }
}
-export function shop_wants(shop_st: mut ShopState, v: int, it: int) -> bool {
- if not shop_vendor_ok(shop_st, v) { return false }
- shop_roll(shop_st)
- for k in 0 .. shop_st.shop_wants_n { if shop_st.shop_want_list[v * shop_st.shop_wants_n + k] == it { return true } }
+export function shop_wants(v: int, it: int) -> bool {
+ if not shop_vendor_ok(v) { return false }
+ shop_roll()
+ for k in 0 .. shop_wants_n { if shop_want_list[v * shop_wants_n + k] == it { return true } }
return false
}
-export function shop_glutted(shop_st: mut ShopState, v: int, it: int) -> bool {
- if not shop_vendor_ok(shop_st, v) { return false }
- shop_roll(shop_st)
- return shop_st.shop_full_list[v] == it
+export function shop_glutted(v: int, it: int) -> bool {
+ if not shop_vendor_ok(v) { return false }
+ shop_roll()
+ return shop_full_list[v] == it
}
# what a vendor wants this week, in its order
-export function shop_wanted(shop_st: mut ShopState, v: int) -> []int {
+export function shop_wanted(v: int) -> []int {
let out = new []int
- if not shop_vendor_ok(shop_st, v) { return out }
- shop_roll(shop_st)
- for k in 0 .. shop_st.shop_wants_n {
- let it = shop_st.shop_want_list[v * shop_st.shop_wants_n + k]
+ if not shop_vendor_ok(v) { return out }
+ shop_roll()
+ for k in 0 .. shop_wants_n {
+ let it = shop_want_list[v * shop_wants_n + k]
if it >= 0 { push(out, it) }
}
return out
}
# the week's multiplier on what `v` pays for `it` (1 with no vendor)
-export function shop_demand_mult(shop_st: mut ShopState, v: int, it: int) -> float {
- if shop_wants(shop_st, v, it) { return shop_st.shop_want_mult }
- if shop_glutted(shop_st, v, it) { return shop_st.shop_glut_mult }
+export function shop_demand_mult(v: int, it: int) -> float {
+ if shop_wants(v, it) { return shop_want_mult }
+ if shop_glutted(v, it) { return shop_glut_mult }
return 1.0
}
# this week, `v` wants `it` (a test, staging, a board that says so)
-export function shop_set_want(shop_st: mut ShopState, v: int, it: int) -> void {
- if not shop_vendor_ok(shop_st, v) { return }
- shop_roll(shop_st)
- shop_st.shop_want_list[v * shop_st.shop_wants_n] = it
- if shop_st.shop_full_list[v] == it { shop_st.shop_full_list[v] = -1 }
+export function shop_set_want(v: int, it: int) -> void {
+ if not shop_vendor_ok(v) { return }
+ shop_roll()
+ shop_want_list[v * shop_wants_n] = it
+ if shop_full_list[v] == it { shop_full_list[v] = -1 }
}
-function shop_demand_reset(shop_st: mut ShopState) -> void {
- shop_st.shop_week_rolled = -1
- shop_st.shop_want_list = null
- shop_st.shop_full_list = null
+function shop_demand_reset() -> void {
+ shop_week_rolled = -1
+ shop_want_list = null
+ shop_full_list = null
}
diff --git a/packages/ludic.shop/prices.ludic b/packages/ludic.shop/prices.ludic
index 10a6c5a6..cabac49d 100644
--- a/packages/ludic.shop/prices.ludic
+++ b/packages/ludic.shop/prices.ludic
@@ -3,40 +3,40 @@
function shop_rep01() -> float { return Math.clamp(float(ShopWorld.rep()), 0.0, 100.0) / 100.0 }
# the fraction of base a buyer pays, and a seller is paid before the item's own fraction
-export function shop_buy_factor(shop_st: ShopState) -> float { return shop_st.shop_buy_new + (shop_st.shop_buy_best - shop_st.shop_buy_new) * shop_rep01() }
-export function shop_sell_factor(shop_st: ShopState) -> float { return shop_st.shop_sell_new + (shop_st.shop_sell_best - shop_st.shop_sell_new) * shop_rep01() }
+export function shop_buy_factor() -> float { return shop_buy_new + (shop_buy_best - shop_buy_new) * shop_rep01() }
+export function shop_sell_factor() -> float { return shop_sell_new + (shop_sell_best - shop_sell_new) * shop_rep01() }
-export function shop_price_buy(shop_st: ShopState, it: int) -> int {
- if not shop_item_ok(shop_st, it) { return 0 }
- let p = int(float(shop_st.shop_base[it]) * shop_buy_factor(shop_st) * ShopWorld.buy_mult() + 0.5)
+export function shop_price_buy(it: int) -> int {
+ if not shop_item_ok(it) { return 0 }
+ let p = int(float(shop_base[it]) * shop_buy_factor() * ShopWorld.buy_mult() + 0.5)
return Math.max(p, 1)
}
# what vendor `v` pays (-1: no vendor, no demand). ROUNDED, so cheap raw material keeps its
# order; and a vendor that stocks the thing never pays what it charges, not even a floor of $1
-export function shop_price_sell_at(shop_st: mut ShopState, v: int, it: int) -> int {
- if not shop_item_ok(shop_st, it) { return 0 }
- let f = shop_sell_factor(shop_st) * shop_st.shop_sell_frac[it] * shop_demand_mult(shop_st, v, it)
- var p = Math.max(int(float(shop_st.shop_base[it]) * f + 0.5), 1)
- if shop_stocks(shop_st, v, it) { p = Math.max(Math.min(p, shop_price_buy(shop_st, it) - 1), 0) }
+export function shop_price_sell_at(v: int, it: int) -> int {
+ if not shop_item_ok(it) { return 0 }
+ let f = shop_sell_factor() * shop_sell_frac[it] * shop_demand_mult(v, it)
+ var p = Math.max(int(float(shop_base[it]) * f + 0.5), 1)
+ if shop_stocks(v, it) { p = Math.max(Math.min(p, shop_price_buy(it) - 1), 0) }
return p
}
# what the vendor at the counter pays
-export function shop_price_sell(shop_st: mut ShopState, it: int) -> int { return shop_price_sell_at(shop_st, shop_st.shop_at, it) }
+export function shop_price_sell(it: int) -> int { return shop_price_sell_at(shop_at, it) }
-export function shop_stocks(shop_st: ShopState, v: int, it: int) -> bool {
- if not shop_vendor_ok(shop_st, v) or shop_st.shop_vendor_list[v].stock == null { return false }
- let st = shop_st.shop_vendor_list[v].stock
+export function shop_stocks(v: int, it: int) -> bool {
+ if not shop_vendor_ok(v) or shop_vendor_list[v].stock == null { return false }
+ let st = shop_vendor_list[v].stock
for k in 0 .. len(st) { if st[k] == it { return true } }
return false
}
# what `v` takes: its list and whatever the world says, and never the one thing it refuses
-export function shop_buys(shop_st: ShopState, v: int, it: int) -> bool {
- if not shop_vendor_ok(shop_st, v) or it == shop_st.shop_vendor_list[v].refuses { return false }
+export function shop_buys(v: int, it: int) -> bool {
+ if not shop_vendor_ok(v) or it == shop_vendor_list[v].refuses { return false }
if ShopWorld.also_buys(v, it) { return true }
- let b = shop_st.shop_vendor_list[v].buys
+ let b = shop_vendor_list[v].buys
if b == null { return false }
for k in 0 .. len(b) { if b[k] == it { return true } }
return false
diff --git a/packages/ludic.shop/state.ludic b/packages/ludic.shop/state.ludic
index eb3f4441..31122e1c 100644
--- a/packages/ludic.shop/state.ludic
+++ b/packages/ludic.shop/state.ludic
@@ -31,67 +31,55 @@ export property ShopVendor {
refuses: int = -1
}
-export state ShopState {
- shop_wants_n: int = 3
- shop_want_mult: float = 1.5
- shop_glut_mult: float = 0.6
- shop_week_days: int = 7
- shop_seed: int = 1013
- shop_week_rolled: int = -1
- shop_want_list: []int = null # shop_wants_n per vendor, -1 empty
- shop_full_list: []int = null # one per vendor, -1 none
- shop_dice: Rng = null
- shop_base: []int = null # an item's base price
- shop_sell_frac: []float = null # what fraction of the curve a vendor pays for it
- shop_vendor_list: []ShopVendor = null
- shop_at: int = -1 # the vendor at the counter, -1 none
- shop_buy_new: float = 1.15
- shop_buy_best: float = 0.92
- shop_sell_new: float = 0.62
- shop_sell_best: float = 0.85
- shop_trade_q: Queue = null
-}
+var shop_base: []int = null # an item's base price
+var shop_sell_frac: []float = null # what fraction of the curve a vendor pays for it
+var shop_vendor_list: []ShopVendor = null
+var shop_at: int = -1 # the vendor at the counter, -1 none
# the curve: buying from `buy_new` of base at rep 0 down to `buy_best` at 100; selling the same way
+var shop_buy_new: float = 1.15
+var shop_buy_best: float = 0.92
+var shop_sell_new: float = 0.62
+var shop_sell_best: float = 0.85
# the items: a base price and a sell fraction each (raw material pays less than something made)
-export function shop_config_items(shop_st: mut ShopState, prices: []int, sell: []float) -> void {
- shop_st.shop_base = prices
- shop_st.shop_sell_frac = sell
+export function shop_config_items(prices: []int, sell: []float) -> void {
+ shop_base = prices
+ shop_sell_frac = sell
}
-export function shop_config_curve(shop_st: mut ShopState, buy_new: float, buy_best: float, sell_new: float, sell_best: float) -> void {
- shop_st.shop_buy_new = buy_new
- shop_st.shop_buy_best = buy_best
- shop_st.shop_sell_new = sell_new
- shop_st.shop_sell_best = sell_best
+export function shop_config_curve(buy_new: float, buy_best: float, sell_new: float, sell_best: float) -> void {
+ shop_buy_new = buy_new
+ shop_buy_best = buy_best
+ shop_sell_new = sell_new
+ shop_sell_best = sell_best
}
-export function shop_clear_vendors(shop_st: mut ShopState) -> void { shop_st.shop_vendor_list = new []ShopVendor }
+export function shop_clear_vendors() -> void { shop_vendor_list = new []ShopVendor }
# a vendor; its number is the order added
-export function shop_add_vendor(shop_st: mut ShopState, stock: []int, buys: []int, refuses: int) -> int {
- if shop_st.shop_vendor_list == null { shop_clear_vendors(shop_st) }
+export function shop_add_vendor(stock: []int, buys: []int, refuses: int) -> int {
+ if shop_vendor_list == null { shop_clear_vendors() }
let v = new ShopVendor
v.stock = stock
v.buys = buys
v.refuses = refuses
- push(shop_st.shop_vendor_list, v)
- return len(shop_st.shop_vendor_list) - 1
+ push(shop_vendor_list, v)
+ return len(shop_vendor_list) - 1
}
-export function shop_vendors(shop_st: ShopState) -> int {
- if shop_st.shop_vendor_list == null { return 0 }
- return len(shop_st.shop_vendor_list)
+export function shop_vendors() -> int {
+ if shop_vendor_list == null { return 0 }
+ return len(shop_vendor_list)
}
-export function shop_items(shop_st: ShopState) -> int {
- if shop_st.shop_base == null { return 0 }
- return len(shop_st.shop_base)
+export function shop_items() -> int {
+ if shop_base == null { return 0 }
+ return len(shop_base)
}
-export function shop_vendor_ok(shop_st: ShopState, v: int) -> bool { return v >= 0 and v < shop_vendors(shop_st) }
-export function shop_item_ok(shop_st: ShopState, it: int) -> bool { return it >= 0 and it < shop_items(shop_st) }
+export function shop_vendor_ok(v: int) -> bool { return v >= 0 and v < shop_vendors() }
+export function shop_item_ok(it: int) -> bool { return it >= 0 and it < shop_items() }
export const SHOP_BOUGHT: int = 0
export const SHOP_SOLD: int = 1
@@ -105,8 +93,9 @@ export property ShopTrade {
price: int = 0
}
+var shop_trade_q: Queue = null
-export function shop_trades(base_st: mut BaseState, shop_st: mut ShopState) -> Queue {
- if shop_st.shop_trade_q == null { shop_st.shop_trade_q = queue_new(base_st, "shop.trades") }
- return shop_st.shop_trade_q
+export function shop_trades() -> Queue {
+ if shop_trade_q == null { shop_trade_q = queue_new("shop.trades") }
+ return shop_trade_q
}
diff --git a/packages/ludic.shop/system.ludic b/packages/ludic.shop/system.ludic
index b5e5103d..04468ae4 100644
--- a/packages/ludic.shop/system.ludic
+++ b/packages/ludic.shop/system.ludic
@@ -1,9 +1,9 @@
# system.ludic - the shop as a system: a reset and nothing saved (the week's demand follows from
# the day, and nobody is at a counter across a save)
-export function shop_reset(base_st: mut BaseState, shop_st: mut ShopState) -> void {
- shop_st.shop_at = -1
- shop_demand_reset(shop_st)
- q_clear(base_st, shop_trades(base_st, shop_st))
+export function shop_reset() -> void {
+ shop_at = -1
+ shop_demand_reset()
+ q_clear(shop_trades())
}
export function shop_system() -> System {
diff --git a/packages/ludic.shop/tests/shop_test.ludic b/packages/ludic.shop/tests/shop_test.ludic
index 8afe4679..e5ebc600 100644
--- a/packages/ludic.shop/tests/shop_test.ludic
+++ b/packages/ludic.shop/tests/shop_test.ludic
@@ -19,36 +19,34 @@ program ShopTest {
const MADE: float = 1.15
const BOUGHT: float = 0.55
- state ShoptestState {
- rep: int = 0
- day: int = 1
- money: int = 0
- pack: []int = null
- food_too: bool = false
- }
+ var rep: int = 0
+ var day: int = 1
+ var money: int = 0
+ var pack: []int = null
+ var food_too: bool = false
- function fake_rep(shoptest_st: ShoptestState) -> int { return shoptest_st.rep }
- function fake_day(shoptest_st: ShoptestState) -> int { return shoptest_st.day }
- function fake_food(shoptest_st: ShoptestState, v: int, it: int) -> bool { return shoptest_st.food_too and v == 1 and (it == TROUT or it == COOKED or it == BURNT) }
- function fake_money(shoptest_st: ShoptestState) -> int { return shoptest_st.money }
- function fake_spend(shoptest_st: mut ShoptestState, n: int) -> bool {
- if n > shoptest_st.money { return false }
- shoptest_st.money -= n
+ function fake_rep() -> int { return rep }
+ function fake_day() -> int { return day }
+ function fake_food(v: int, it: int) -> bool { return food_too and v == 1 and (it == TROUT or it == COOKED or it == BURNT) }
+ function fake_money() -> int { return money }
+ function fake_spend(n: int) -> bool {
+ if n > money { return false }
+ money -= n
return true
}
- function fake_earn(shoptest_st: mut ShoptestState, n: int) -> void { shoptest_st.money += n }
- function fake_count(shoptest_st: ShoptestState, it: int) -> int { return shoptest_st.pack[it] }
- function fake_take(shoptest_st: mut ShoptestState, it: int, n: int) -> bool {
- if shoptest_st.pack[it] < n { return false }
- shoptest_st.pack[it] = shoptest_st.pack[it] - n
+ function fake_earn(n: int) -> void { money += n }
+ function fake_count(it: int) -> int { return pack[it] }
+ function fake_take(it: int, n: int) -> bool {
+ if pack[it] < n { return false }
+ pack[it] = pack[it] - n
return true
}
- function fake_add(shoptest_st: mut ShoptestState, it: int, n: int) -> int {
- shoptest_st.pack[it] = shoptest_st.pack[it] + n
+ function fake_add(it: int, n: int) -> int {
+ pack[it] = pack[it] + n
return n
}
- function fake_room(shoptest_st: ShoptestState, it: int) -> int {
- if it == JACKET { return 1 - shoptest_st.pack[it] }
+ function fake_room(it: int) -> int {
+ if it == JACKET { return 1 - pack[it] }
return 99
}
@@ -66,7 +64,7 @@ program ShopTest {
}
# the valley's own numbers: base prices and the sell classes
- function fresh(base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) -> void {
+ function fresh() -> void {
let prices = new []int
let sell = new []float
let p = ints(3, 6, 8, 9)
@@ -85,104 +83,104 @@ program ShopTest {
push(sell, BOUGHT)
push(sell, RAW)
push(sell, RAW)
- shop_config_items(shop_st, prices, sell)
- shop_clear_vendors(shop_st)
- shop_add_vendor(shop_st, ints(ROPE, JACKET, -1, -1), ints(WOOD, CHANTERELLE, -1, -1), -1)
- shop_add_vendor(shop_st, ints(BAIT, COOKED, -1, -1), ints(WOOD, PUFFBALL, BAIT, -1), BURNT)
- shop_add_vendor(shop_st, ints(BAIT, COOKED, GRUB, -1), ints(TROUT, COOKED, BAIT, GRUB), -1)
- shoptest_st.pack = new []int
- for i in 0 .. 10 { push(shoptest_st.pack, 0) }
- shoptest_st.rep = 0
- shoptest_st.day = 1
- shoptest_st.money = 0
- shop_reset(base_st, shop_st)
+ shop_config_items(prices, sell)
+ shop_clear_vendors()
+ shop_add_vendor(ints(ROPE, JACKET, -1, -1), ints(WOOD, CHANTERELLE, -1, -1), -1)
+ shop_add_vendor(ints(BAIT, COOKED, -1, -1), ints(WOOD, PUFFBALL, BAIT, -1), BURNT)
+ shop_add_vendor(ints(BAIT, COOKED, GRUB, -1), ints(TROUT, COOKED, BAIT, GRUB), -1)
+ pack = new []int
+ for i in 0 .. 10 { push(pack, 0) }
+ rep = 0
+ day = 1
+ money = 0
+ shop_reset()
}
- function trades(base_st: mut BaseState, shop_st: mut ShopState) -> []ShopTrade { return q_drain(base_st, shop_trades(base_st, shop_st)) }
+ function trades() -> []ShopTrade { return q_drain(shop_trades()) }
- test "buying takes the price and puts one in the pack, and says so" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- shoptest_st.money = 100
- shop_visit(shop_st, 0)
- let pr = shop_price_buy(shop_st, JACKET)
- expect(shop_buy(base_st, shop_st, JACKET))
- expect_eq(shoptest_st.money, 100 - pr)
- expect_eq(shoptest_st.pack[JACKET], 1)
- expect(not shop_buy(base_st, shop_st, JACKET))
- let ts = trades(base_st, shop_st)
+ test "buying takes the price and puts one in the pack, and says so" {
+ fresh()
+ money = 100
+ shop_visit(0)
+ let pr = shop_price_buy(JACKET)
+ expect(shop_buy(JACKET))
+ expect_eq(money, 100 - pr)
+ expect_eq(pack[JACKET], 1)
+ expect(not shop_buy(JACKET))
+ let ts = trades()
expect_eq(len(ts), 1)
expect_eq(ts[0].what, SHOP_BOUGHT)
expect_eq(ts[0].vendor, 0)
expect_eq(ts[0].price, pr)
}
- test "no money, nothing bought and nothing spent" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- shoptest_st.money = 3
- expect(not shop_can_buy(shop_st, JACKET))
- expect(not shop_buy(base_st, shop_st, JACKET))
- expect_eq(shoptest_st.money, 3)
- expect_eq(len(trades(base_st, shop_st)), 0)
+ test "no money, nothing bought and nothing spent" {
+ fresh()
+ money = 3
+ expect(not shop_can_buy(JACKET))
+ expect(not shop_buy(JACKET))
+ expect_eq(money, 3)
+ expect_eq(len(trades()), 0)
}
- test "selling pays what the counter's vendor pays, only what the pack has" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- shoptest_st.pack[TROUT] = 3
- shop_visit(shop_st, 2)
- let pr = shop_price_sell(shop_st, TROUT)
- expect_eq(shop_sell(base_st, shop_st, TROUT, 5), 3)
- expect_eq(shoptest_st.money, pr * 3)
- expect_eq(shoptest_st.pack[TROUT], 0)
- expect_eq(trades(base_st, shop_st)[0].n, 3)
+ test "selling pays what the counter's vendor pays, only what the pack has" {
+ fresh()
+ pack[TROUT] = 3
+ shop_visit(2)
+ let pr = shop_price_sell(TROUT)
+ expect_eq(shop_sell(TROUT, 5), 3)
+ expect_eq(money, pr * 3)
+ expect_eq(pack[TROUT], 0)
+ expect_eq(trades()[0].n, 3)
}
- test "a vendor buys its list and what the world adds, never what it refuses" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- expect(shop_buys(shop_st, 1, PUFFBALL))
- expect(not shop_buys(shop_st, 1, TROUT))
- shoptest_st.food_too = true
- expect(shop_buys(shop_st, 1, TROUT))
- expect(not shop_buys(shop_st, 1, BURNT))
- shoptest_st.pack[BURNT] = 1
- shoptest_st.pack[TROUT] = 1
- let s = shop_sellable(shop_st, 1)
+ test "a vendor buys its list and what the world adds, never what it refuses" {
+ fresh()
+ expect(shop_buys(1, PUFFBALL))
+ expect(not shop_buys(1, TROUT))
+ food_too = true
+ expect(shop_buys(1, TROUT))
+ expect(not shop_buys(1, BURNT))
+ pack[BURNT] = 1
+ pack[TROUT] = 1
+ let s = shop_sellable(1)
expect_eq(len(s), 1)
expect_eq(s[0], TROUT)
- shop_visit(shop_st, 1)
- expect_eq(shop_sell(base_st, shop_st, BURNT, 1), 0)
+ shop_visit(1)
+ expect_eq(shop_sell(BURNT, 1), 0)
}
- test "the week's demand is the shop's own dice, and turns with the week" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- let a = shop_wanted(shop_st, 0)
+ test "the week's demand is the shop's own dice, and turns with the week" {
+ fresh()
+ let a = shop_wanted(0)
expect(len(a) > 0)
- for k in 0 .. len(a) { expect(shop_buys(shop_st, 0, a[k])) }
- let again = shop_wanted(shop_st, 0)
+ for k in 0 .. len(a) { expect(shop_buys(0, a[k])) }
+ let again = shop_wanted(0)
expect_eq(again[0], a[0])
- shop_visit(shop_st, 0)
+ shop_visit(0)
let it = a[0]
- let plain = shop_price_sell_at(shop_st, -1, it)
- expect(shop_demand_mult(shop_st, 0, it) == 1.5)
- expect(shop_price_sell(shop_st, it) >= plain)
+ let plain = shop_price_sell_at(-1, it)
+ expect(shop_demand_mult(0, it) == 1.5)
+ expect(shop_price_sell(it) >= plain)
var moved = false
for w in 1 .. 12 {
- shoptest_st.day = 1 + w * 7
- let b = shop_wanted(shop_st, 0)
+ day = 1 + w * 7
+ let b = shop_wanted(0)
if len(b) != len(a) or b[0] != a[0] { moved = true }
}
expect(moved)
}
- test "a shop is not a fountain: nothing it stocks sells back for what it costs" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- for v in 0 .. shop_vendors(shop_st) {
- let st = shop_stock(shop_st, v)
+ test "a shop is not a fountain: nothing it stocks sells back for what it costs" {
+ fresh()
+ for v in 0 .. shop_vendors() {
+ let st = shop_stock(v)
for k in 0 .. len(st) {
for r in 0 .. 11 {
- shoptest_st.rep = r * 10
- shop_set_want(shop_st, v, st[k])
- let buy = shop_price_buy(shop_st, st[k])
- let sell = shop_price_sell_at(shop_st, v, st[k])
+ rep = r * 10
+ shop_set_want(v, st[k])
+ let buy = shop_price_buy(st[k])
+ let sell = shop_price_sell_at(v, st[k])
expect(sell < buy)
expect(sell >= 0)
}
@@ -190,49 +188,49 @@ program ShopTest {
}
}
- test "not even at a dollar: a penny item a vendor stocks fetches nothing" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
- shoptest_st.rep = 100
- shop_set_want(shop_st, 2, GRUB)
- expect_eq(shop_price_buy(shop_st, GRUB), 1)
- expect_eq(shop_price_sell_at(shop_st, 2, GRUB), 0)
- expect(shop_price_sell_at(shop_st, -1, GRUB) >= 1)
+ test "not even at a dollar: a penny item a vendor stocks fetches nothing" {
+ fresh()
+ rep = 100
+ shop_set_want(2, GRUB)
+ expect_eq(shop_price_buy(GRUB), 1)
+ expect_eq(shop_price_sell_at(2, GRUB), 0)
+ expect(shop_price_sell_at(-1, GRUB) >= 1)
}
- test "friction falls with standing and never inverts" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
+ test "friction falls with standing and never inverts" {
+ fresh()
var last_buy = 1000000
var last_sell = -1
for r in 0 .. 11 {
- shoptest_st.rep = r * 10
- expect(shop_buy_factor(shop_st) > shop_sell_factor(shop_st))
- let b = shop_price_buy(shop_st, JACKET)
- let s = shop_price_sell_at(shop_st, -1, COOKED)
+ rep = r * 10
+ expect(shop_buy_factor() > shop_sell_factor())
+ let b = shop_price_buy(JACKET)
+ let s = shop_price_sell_at(-1, COOKED)
expect(b <= last_buy)
expect(s >= last_sell)
last_buy = b
last_sell = s
}
- shoptest_st.rep = 0
- let b0 = shop_price_buy(shop_st, JACKET)
- shoptest_st.rep = 100
- expect(shop_price_buy(shop_st, JACKET) < b0)
- shoptest_st.rep = 0
+ rep = 0
+ let b0 = shop_price_buy(JACKET)
+ rep = 100
+ expect(shop_price_buy(JACKET) < b0)
+ rep = 0
expect(float(b0) / 60.0 <= 1.25)
}
- test "raw material keeps its order, and is not an income" (base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) {
- fresh(base_st, shop_st, shoptest_st)
+ test "raw material keeps its order, and is not an income" {
+ fresh()
for r in 0 .. 11 {
- shoptest_st.rep = r * 10
- let w = shop_price_sell_at(shop_st, -1, WOOD)
- let ch = shop_price_sell_at(shop_st, -1, CHANTERELLE)
- let pf = shop_price_sell_at(shop_st, -1, PUFFBALL)
- let tr = shop_price_sell_at(shop_st, -1, TROUT)
+ rep = r * 10
+ let w = shop_price_sell_at(-1, WOOD)
+ let ch = shop_price_sell_at(-1, CHANTERELLE)
+ let pf = shop_price_sell_at(-1, PUFFBALL)
+ let tr = shop_price_sell_at(-1, TROUT)
expect(w <= ch and ch <= pf and pf <= tr)
expect(pf <= 8 / 2)
}
- shoptest_st.rep = 0
- expect(shop_price_sell_at(shop_st, -1, WOOD) < shop_price_sell_at(shop_st, -1, TROUT))
+ rep = 0
+ expect(shop_price_sell_at(-1, WOOD) < shop_price_sell_at(-1, TROUT))
}
}
diff --git a/packages/ludic.shop/trade.ludic b/packages/ludic.shop/trade.ludic
index 8d50bb1d..a7d49626 100644
--- a/packages/ludic.shop/trade.ludic
+++ b/packages/ludic.shop/trade.ludic
@@ -1,63 +1,63 @@
# trade.ludic - standing at a counter, and the only ways anything changes hands
-export function shop_visit(shop_st: mut ShopState, v: int) -> void {
- if shop_vendor_ok(shop_st, v) { shop_st.shop_at = v } else { shop_st.shop_at = -1 }
+export function shop_visit(v: int) -> void {
+ if shop_vendor_ok(v) { shop_at = v } else { shop_at = -1 }
}
-export function shop_away(shop_st: mut ShopState) -> void { shop_st.shop_at = -1 }
+export function shop_away() -> void { shop_at = -1 }
# the vendor at the counter, -1 none
-export function shop_vendor(shop_st: ShopState) -> int { return shop_st.shop_at }
+export function shop_vendor() -> int { return shop_at }
-export function shop_at_counter(shop_st: ShopState) -> bool { return shop_st.shop_at >= 0 }
+export function shop_at_counter() -> bool { return shop_at >= 0 }
# what `v` has on its shelves, in shelf order
-export function shop_stock(shop_st: ShopState, v: int) -> []int {
+export function shop_stock(v: int) -> []int {
let out = new []int
- if not shop_vendor_ok(shop_st, v) or shop_st.shop_vendor_list[v].stock == null { return out }
- let st = shop_st.shop_vendor_list[v].stock
+ if not shop_vendor_ok(v) or shop_vendor_list[v].stock == null { return out }
+ let st = shop_vendor_list[v].stock
for k in 0 .. len(st) { push(out, st[k]) }
return out
}
# what in the pack `v` would buy
-export function shop_sellable(shop_st: ShopState, v: int) -> []int {
+export function shop_sellable(v: int) -> []int {
let out = new []int
- for it in 0 .. shop_items(shop_st) { if ShopPack.count(it) > 0 and shop_buys(shop_st, v, it) { push(out, it) } }
+ for it in 0 .. shop_items() { if ShopPack.count(it) > 0 and shop_buys(v, it) { push(out, it) } }
return out
}
-function shop_trade_fact(base_st: mut BaseState, shop_st: mut ShopState, what: int, it: int, n: int, price: int) -> void {
+function shop_trade_fact(what: int, it: int, n: int, price: int) -> void {
let t = new ShopTrade
t.what = what
- t.vendor = shop_st.shop_at
+ t.vendor = shop_at
t.item = it
t.n = n
t.price = price
- q_push(base_st, shop_trades(base_st, shop_st), t)
+ q_push(shop_trades(), t)
}
-export function shop_can_buy(shop_st: ShopState, it: int) -> bool {
- return shop_item_ok(shop_st, it) and ShopPurse.money() >= shop_price_buy(shop_st, it) and ShopPack.room(it) > 0
+export function shop_can_buy(it: int) -> bool {
+ return shop_item_ok(it) and ShopPurse.money() >= shop_price_buy(it) and ShopPack.room(it) > 0
}
# one, paid for and in the pack; false and nothing spent when it cannot be
-export function shop_buy(base_st: mut BaseState, shop_st: mut ShopState, it: int) -> bool {
- if not shop_can_buy(shop_st, it) { return false }
- let pr = shop_price_buy(shop_st, it)
+export function shop_buy(it: int) -> bool {
+ if not shop_can_buy(it) { return false }
+ let pr = shop_price_buy(it)
if not ShopPurse.spend(pr) { return false }
ShopPack.add(it, 1)
- shop_trade_fact(base_st, shop_st, SHOP_BOUGHT, it, 1, pr)
+ shop_trade_fact(SHOP_BOUGHT, it, 1, pr)
return true
}
# `n` to the vendor at the counter (as many as the pack has); how many went
-export function shop_sell(base_st: mut BaseState, shop_st: mut ShopState, it: int, n: int) -> int {
- if not shop_buys(shop_st, shop_st.shop_at, it) { return 0 }
+export function shop_sell(it: int, n: int) -> int {
+ if not shop_buys(shop_at, it) { return 0 }
let k = Math.min(n, ShopPack.count(it))
if k <= 0 { return 0 }
- let pr = shop_price_sell(shop_st, it)
+ let pr = shop_price_sell(it)
if not ShopPack.take(it, k) { return 0 }
ShopPurse.earn(pr * k)
- shop_trade_fact(base_st, shop_st, SHOP_SOLD, it, k, pr)
+ shop_trade_fact(SHOP_SOLD, it, k, pr)
return k
}
diff --git a/packages/ludic.steps/arc.ludic b/packages/ludic.steps/arc.ludic
index a5789e2f..df4517c7 100644
--- a/packages/ludic.steps/arc.ludic
+++ b/packages/ludic.steps/arc.ludic
@@ -1,78 +1,78 @@
# arc.ludic - an arc is a table of chapters, each a table of up to STEPS_PER_CHAPTER steps. The
# game builds one with steps_chapter_add / steps_add; what a chapter is called is the game's.
# empty the tables for another arc (the progress goes back to the first chapter)
-export function steps_arc_clear(steps_st: mut StepsState) -> void {
- steps_st.steps__n = new []int
- steps_st.steps__kind = new []int
- steps_st.steps__param = new []int
- steps_st.steps__need = new []int
- steps_st.steps__cur = 0
- steps_st.steps__done = 0
+export function steps_arc_clear() -> void {
+ steps__n = new []int
+ steps__kind = new []int
+ steps__param = new []int
+ steps__need = new []int
+ steps__cur = 0
+ steps__done = 0
}
-function steps__ready(steps_st: mut StepsState) -> void { if steps_st.steps__n == null { steps_arc_clear(steps_st) } }
+function steps__ready() -> void { if steps__n == null { steps_arc_clear() } }
# open the next chapter; its number
-export function steps_chapter_add(steps_st: mut StepsState) -> int {
- steps__ready(steps_st)
- push(steps_st.steps__n, 0)
+export function steps_chapter_add() -> int {
+ steps__ready()
+ push(steps__n, 0)
for i in 0 .. STEPS_PER_CHAPTER {
- push(steps_st.steps__kind, -1)
- push(steps_st.steps__param, 0)
- push(steps_st.steps__need, 0)
+ push(steps__kind, -1)
+ push(steps__param, 0)
+ push(steps__need, 0)
}
- return len(steps_st.steps__n) - 1
+ return len(steps__n) - 1
}
# a step in the chapter just opened; false when there is no chapter or it is full
-export function steps_add(steps_st: mut StepsState, kind: int, param: int, need: int) -> bool {
- steps__ready(steps_st)
- let c = len(steps_st.steps__n) - 1
- if c < 0 or steps_st.steps__n[c] >= STEPS_PER_CHAPTER { return false }
- let k = c * STEPS_PER_CHAPTER + steps_st.steps__n[c]
- steps_st.steps__kind[k] = kind
- steps_st.steps__param[k] = param
- steps_st.steps__need[k] = need
- steps_st.steps__n[c] += 1
+export function steps_add(kind: int, param: int, need: int) -> bool {
+ steps__ready()
+ let c = len(steps__n) - 1
+ if c < 0 or steps__n[c] >= STEPS_PER_CHAPTER { return false }
+ let k = c * STEPS_PER_CHAPTER + steps__n[c]
+ steps__kind[k] = kind
+ steps__param[k] = param
+ steps__need[k] = need
+ steps__n[c] += 1
return true
}
-export function steps_count(steps_st: StepsState) -> int {
- if steps_st.steps__n == null { return 0 }
- return len(steps_st.steps__n)
+export function steps_count() -> int {
+ if steps__n == null { return 0 }
+ return len(steps__n)
}
-export function steps_in(steps_st: StepsState, ch: int) -> int {
- if ch < 0 or ch >= steps_count(steps_st) { return 0 }
- return steps_st.steps__n[ch]
+export function steps_in(ch: int) -> int {
+ if ch < 0 or ch >= steps_count() { return 0 }
+ return steps__n[ch]
}
# a step by its flat index, ch * STEPS_PER_CHAPTER + i (-1, 0, 0 past the table)
-export function steps_kind_at(steps_st: StepsState, k: int) -> int {
- if steps_st.steps__kind == null or k < 0 or k >= len(steps_st.steps__kind) { return -1 }
- return steps_st.steps__kind[k]
+export function steps_kind_at(k: int) -> int {
+ if steps__kind == null or k < 0 or k >= len(steps__kind) { return -1 }
+ return steps__kind[k]
}
-export function steps_param_at(steps_st: StepsState, k: int) -> int {
- if steps_st.steps__param == null or k < 0 or k >= len(steps_st.steps__param) { return 0 }
- return steps_st.steps__param[k]
+export function steps_param_at(k: int) -> int {
+ if steps__param == null or k < 0 or k >= len(steps__param) { return 0 }
+ return steps__param[k]
}
-export function steps_need_at(steps_st: StepsState, k: int) -> int {
- if steps_st.steps__need == null or k < 0 or k >= len(steps_st.steps__need) { return 0 }
- return steps_st.steps__need[k]
+export function steps_need_at(k: int) -> int {
+ if steps__need == null or k < 0 or k >= len(steps__need) { return 0 }
+ return steps__need[k]
}
-export function steps_kind(steps_st: StepsState, ch: int, i: int) -> int { return steps_kind_at(steps_st, ch * STEPS_PER_CHAPTER + i) }
-export function steps_param(steps_st: StepsState, ch: int, i: int) -> int { return steps_param_at(steps_st, ch * STEPS_PER_CHAPTER + i) }
-export function steps_need(steps_st: StepsState, ch: int, i: int) -> int { return steps_need_at(steps_st, ch * STEPS_PER_CHAPTER + i) }
+export function steps_kind(ch: int, i: int) -> int { return steps_kind_at(ch * STEPS_PER_CHAPTER + i) }
+export function steps_param(ch: int, i: int) -> int { return steps_param_at(ch * STEPS_PER_CHAPTER + i) }
+export function steps_need(ch: int, i: int) -> int { return steps_need_at(ch * STEPS_PER_CHAPTER + i) }
# the chapter being played, and which of its steps are done (sticky)
-export function steps_current(steps_st: StepsState) -> int { return steps_st.steps__cur }
-export function steps_done_mask(steps_st: StepsState) -> int { return steps_st.steps__done }
-export function steps_done(steps_st: StepsState, i: int) -> bool { return (steps_st.steps__done & (1 << i)) != 0 }
-export function steps_finished(steps_st: StepsState) -> bool { return steps_st.steps__cur >= steps_count(steps_st) }
+export function steps_current() -> int { return steps__cur }
+export function steps_done_mask() -> int { return steps__done }
+export function steps_done(i: int) -> bool { return (steps__done & (1 << i)) != 0 }
+export function steps_finished() -> bool { return steps__cur >= steps_count() }
# the progress outright: a load, the host's word, a test
-export function steps_set_current(steps_st: mut StepsState, ch: int) -> void { steps_st.steps__cur = ch }
-export function steps_set_done(steps_st: mut StepsState, mask: int) -> void { steps_st.steps__done = mask }
+export function steps_set_current(ch: int) -> void { steps__cur = ch }
+export function steps_set_done(mask: int) -> void { steps__done = mask }
diff --git a/packages/ludic.steps/check.ludic b/packages/ludic.steps/check.ludic
index 831d82cc..bff9e432 100644
--- a/packages/ludic.steps/check.ludic
+++ b/packages/ludic.steps/check.ludic
@@ -1,43 +1,43 @@
# check.ludic - once a frame on the machine that owns the story: every open step of the current
# chapter that is met now sticks (STEPS_MET), and a chapter with all of them done moves the arc on
# (STEPS_CHAPTER). A step asks about STATE, so one met before its chapter opened ticks at once.
-export function steps_check(base_st: mut BaseState, steps_st: mut StepsState) -> void {
- if steps_finished(steps_st) { return }
- let ch = steps_st.steps__cur
- let n = steps_in(steps_st, ch)
+export function steps_check() -> void {
+ if steps_finished() { return }
+ let ch = steps__cur
+ let n = steps_in(ch)
var all = true
for i in 0 .. n {
- if not steps_done(steps_st, i) and steps_met(steps_st, i) {
- steps_st.steps__done = steps_st.steps__done | (1 << i)
- steps__fact(base_st, steps_st, STEPS_MET, ch, i)
+ if not steps_done(i) and steps_met(i) {
+ steps__done = steps__done | (1 << i)
+ steps__fact(STEPS_MET, ch, i)
}
- if not steps_done(steps_st, i) { all = false }
+ if not steps_done(i) { all = false }
}
if not all { return }
- steps_st.steps__cur = ch + 1
- steps_st.steps__done = 0
- steps__fact(base_st, steps_st, STEPS_CHAPTER, ch, n)
+ steps__cur = ch + 1
+ steps__done = 0
+ steps__fact(STEPS_CHAPTER, ch, n)
}
# is step i of chapter ch met by this machine's own state? (a chapter not being played, a panel)
-export function steps_check_at(steps_st: StepsState, ch: int, i: int) -> bool {
- if ch < 0 or ch >= steps_count(steps_st) or i < 0 or i >= steps_in(steps_st, ch) { return false }
- return steps_eval(steps_st, steps_kind(steps_st, ch, i), steps_param(steps_st, ch, i), steps_need(steps_st, ch, i))
+export function steps_check_at(ch: int, i: int) -> bool {
+ if ch < 0 or ch >= steps_count() or i < 0 or i >= steps_in(ch) { return false }
+ return steps_eval(steps_kind(ch, i), steps_param(ch, i), steps_need(ch, i))
}
# is an open step of the current chapter asking for this kind (and this param, unless < 0)?
-export function steps_wants(steps_st: StepsState, kind: int, param: int) -> bool {
- if steps_finished(steps_st) { return false }
- for i in 0 .. steps_in(steps_st, steps_st.steps__cur) {
- if steps_done(steps_st, i) or steps_kind(steps_st, steps_st.steps__cur, i) != kind { continue }
- if param < 0 or steps_param(steps_st, steps_st.steps__cur, i) == param { return true }
+export function steps_wants(kind: int, param: int) -> bool {
+ if steps_finished() { return false }
+ for i in 0 .. steps_in(steps__cur) {
+ if steps_done(i) or steps_kind(steps__cur, i) != kind { continue }
+ if param < 0 or steps_param(steps__cur, i) == param { return true }
}
return false
}
# back to no arc and no party (tests)
-export function steps_reset(base_st: mut BaseState, steps_st: mut StepsState) -> void {
- steps_arc_clear(steps_st)
- steps__party_clear(steps_st)
- q_clear(base_st, steps_facts(base_st, steps_st))
+export function steps_reset() -> void {
+ steps_arc_clear()
+ steps__party_clear()
+ q_clear(steps_facts())
}
diff --git a/packages/ludic.steps/each.ludic b/packages/ludic.steps/each.ludic
index 98c58bc1..6f1d00c5 100644
--- a/packages/ludic.steps/each.ludic
+++ b/packages/ludic.steps/each.ludic
@@ -1,27 +1,27 @@
# each.ludic - a step every player must meet in their own pack: how many seats are taken, how
# many of them carry it, and which do not
-export function steps_players(steps_st: StepsState) -> int {
+export function steps_players() -> int {
var n = 0
- for p in 0 .. steps_st.steps__players { if StepsWorld.present(p) { n += 1 } }
+ for p in 0 .. steps__players { if StepsWorld.present(p) { n += 1 } }
return n
}
# how many seats taken have step i's thing (seat 0 counted live)
-export function steps_each(steps_st: mut StepsState, i: int) -> int {
- steps__cols(steps_st)
+export function steps_each(i: int) -> int {
+ steps__cols()
var n = 0
- for p in 0 .. steps_st.steps__players {
- if StepsWorld.present(p) and steps_st.steps__col[p * STEPS_PER_CHAPTER + i] != 0 { n += 1 }
+ for p in 0 .. steps__players {
+ if StepsWorld.present(p) and steps__col[p * STEPS_PER_CHAPTER + i] != 0 { n += 1 }
}
return n
}
# the seats that do not have it yet
-export function steps_lacking(steps_st: mut StepsState, i: int) -> []int {
- steps__cols(steps_st)
+export function steps_lacking(i: int) -> []int {
+ steps__cols()
let out = new []int
- for p in 0 .. steps_st.steps__players {
- if StepsWorld.present(p) and steps_st.steps__col[p * STEPS_PER_CHAPTER + i] == 0 { push(out, p) }
+ for p in 0 .. steps__players {
+ if StepsWorld.present(p) and steps__col[p * STEPS_PER_CHAPTER + i] == 0 { push(out, p) }
}
return out
}
diff --git a/packages/ludic.steps/kinds.ludic b/packages/ludic.steps/kinds.ludic
index a92f2a68..f2db6590 100644
--- a/packages/ludic.steps/kinds.ludic
+++ b/packages/ludic.steps/kinds.ludic
@@ -1,34 +1,34 @@
# kinds.ludic - the kinds the game hands in, and a step answered from its kind's shape: met or
# not, its count, what it wants. A kind past the list is a plain yes, the world's.
-export function steps_kinds(steps_st: mut StepsState, ks: []StepsKind) -> void { steps_st.steps__kinds = ks }
+export function steps_kinds(ks: []StepsKind) -> void { steps__kinds = ks }
-function steps__known(steps_st: StepsState, kind: int) -> bool { return steps_st.steps__kinds != null and kind >= 0 and kind < len(steps_st.steps__kinds) }
+function steps__known(kind: int) -> bool { return steps__kinds != null and kind >= 0 and kind < len(steps__kinds) }
-export function steps_shape(steps_st: StepsState, kind: int) -> int {
- if not steps__known(steps_st, kind) { return STEPS_SHAPE_YES }
- return steps_st.steps__kinds[kind].shape
+export function steps_shape(kind: int) -> int {
+ if not steps__known(kind) { return STEPS_SHAPE_YES }
+ return steps__kinds[kind].shape
}
-export function steps_pool(steps_st: StepsState, kind: int) -> int {
- if not steps__known(steps_st, kind) { return STEPS_POOL_WORLD }
- return steps_st.steps__kinds[kind].pool
+export function steps_pool(kind: int) -> int {
+ if not steps__known(kind) { return STEPS_POOL_WORLD }
+ return steps__kinds[kind].pool
}
-export function steps_shows(steps_st: StepsState, kind: int) -> bool {
- if not steps__known(steps_st, kind) { return false }
- return steps_st.steps__kinds[kind].shows
+export function steps_shows(kind: int) -> bool {
+ if not steps__known(kind) { return false }
+ return steps__kinds[kind].shows
}
function steps__value(kind: int, param: int) -> int { return StepsWorld.value(kind, param, 0) }
# is (kind, param, need) met by this machine's own state?
-export function steps_eval(steps_st: StepsState, kind: int, param: int, need: int) -> bool {
+export function steps_eval(kind: int, param: int, need: int) -> bool {
if kind < 0 { return false }
- return steps__met_by(steps_st, kind, param, need, steps__value(kind, param))
+ return steps__met_by(kind, param, need, steps__value(kind, param))
}
-function steps__met_by(steps_st: StepsState, kind: int, param: int, need: int, v: int) -> bool {
- let sh = steps_shape(steps_st, kind)
+function steps__met_by(kind: int, param: int, need: int, v: int) -> bool {
+ let sh = steps_shape(kind)
if sh == STEPS_SHAPE_COUNT { return v >= need }
if sh == STEPS_SHAPE_MASK { return steps_popcount(v) >= need }
if sh == STEPS_SHAPE_MASK_ALL { return (v & param) == param }
@@ -36,30 +36,30 @@ function steps__met_by(steps_st: StepsState, kind: int, param: int, need: int, v
}
# how far a counted step has got, -1 for a kind a panel shows no count for
-export function steps_got(steps_st: StepsState, kind: int, param: int) -> int {
- if kind < 0 or not steps_shows(steps_st, kind) { return -1 }
- return steps__shaped(steps_st, kind, param, steps__value(kind, param))
+export function steps_got(kind: int, param: int) -> int {
+ if kind < 0 or not steps_shows(kind) { return -1 }
+ return steps__shaped(kind, param, steps__value(kind, param))
}
-function steps__shaped(steps_st: StepsState, kind: int, param: int, v: int) -> int {
- let sh = steps_shape(steps_st, kind)
+function steps__shaped(kind: int, param: int, v: int) -> int {
+ let sh = steps_shape(kind)
if sh == STEPS_SHAPE_MASK { return steps_popcount(v) }
if sh == STEPS_SHAPE_MASK_ALL { return steps_popcount(v & param) }
return v
}
-export function steps_want(steps_st: StepsState, kind: int, param: int, need: int) -> int {
- if steps_shape(steps_st, kind) == STEPS_SHAPE_MASK_ALL { return steps_popcount(param) }
+export function steps_want(kind: int, param: int, need: int) -> int {
+ if steps_shape(kind) == STEPS_SHAPE_MASK_ALL { return steps_popcount(param) }
return need
}
# THIS machine's share: a mask for OR, a count for SUM and MAX, 1 or 0 for ANY and EACH, and
# nothing for a question about the world
-export function steps_mine(steps_st: StepsState, kind: int, param: int, need: int) -> int {
- let how = steps_pool(steps_st, kind)
+export function steps_mine(kind: int, param: int, need: int) -> int {
+ let how = steps_pool(kind)
if how == STEPS_POOL_WORLD { return 0 }
if how == STEPS_POOL_ANY or how == STEPS_POOL_EACH {
- if steps_eval(steps_st, kind, param, need) { return 1 }
+ if steps_eval(kind, param, need) { return 1 }
return 0
}
return steps__value(kind, param)
@@ -67,22 +67,22 @@ export function steps_mine(steps_st: StepsState, kind: int, param: int, need: in
# the party's verdict from what it has between it: `v` combined as the pool says; for EACH,
# how many are in the party and how many carry it
-export function steps_from(steps_st: StepsState, kind: int, param: int, need: int, v: int, players: int, have: int) -> bool {
- let how = steps_pool(steps_st, kind)
- if how == STEPS_POOL_WORLD { return steps_eval(steps_st, kind, param, need) }
+export function steps_from(kind: int, param: int, need: int, v: int, players: int, have: int) -> bool {
+ let how = steps_pool(kind)
+ if how == STEPS_POOL_WORLD { return steps_eval(kind, param, need) }
if how == STEPS_POOL_ANY { return v != 0 }
if how == STEPS_POOL_EACH { return players > 0 and have >= players }
if how == STEPS_POOL_OR {
- if steps_shape(steps_st, kind) == STEPS_SHAPE_MASK_ALL { return (v & param) == param }
+ if steps_shape(kind) == STEPS_SHAPE_MASK_ALL { return (v & param) == param }
return steps_popcount(v) >= need
}
return v >= need
}
# the count from a share already combined across the party
-export function steps_got_from(steps_st: StepsState, kind: int, param: int, v: int) -> int {
- let how = steps_pool(steps_st, kind)
- if not steps_shows(steps_st, kind) { return -1 }
- if how == STEPS_POOL_OR or how == STEPS_POOL_SUM or how == STEPS_POOL_MAX { return steps__shaped(steps_st, kind, param, v) }
- return steps_got(steps_st, kind, param)
+export function steps_got_from(kind: int, param: int, v: int) -> int {
+ let how = steps_pool(kind)
+ if not steps_shows(kind) { return -1 }
+ if how == STEPS_POOL_OR or how == STEPS_POOL_SUM or how == STEPS_POOL_MAX { return steps__shaped(kind, param, v) }
+ return steps_got(kind, param)
}
diff --git a/packages/ludic.steps/party.ludic b/packages/ludic.steps/party.ludic
index d5bb5371..9c688577 100644
--- a/packages/ludic.steps/party.ludic
+++ b/packages/ludic.steps/party.ludic
@@ -1,63 +1,66 @@
# party.ludic - with company every player reports what THEY have toward each step of the current
# chapter (steps_mine) and the owner of the story answers the step from the lot. Held per seat,
# so a player who leaves takes their column with them; seat 0 is this machine, read live.
+var steps__players: int = 12
+var steps__col: []int = null # players * STEPS_PER_CHAPTER shares
+var steps__col_ch: int = -1 # the chapter they are about
# how many seats a party has
-export function steps_config(steps_st: mut StepsState, players: int) -> void {
- steps_st.steps__players = players
- steps_st.steps__col = null
+export function steps_config(players: int) -> void {
+ steps__players = players
+ steps__col = null
}
-function steps__party_clear(steps_st: mut StepsState) -> void {
- steps_st.steps__col = new []int
- for i in 0 .. steps_st.steps__players * STEPS_PER_CHAPTER { push(steps_st.steps__col, 0) }
- steps_st.steps__col_ch = steps_st.steps__cur
+function steps__party_clear() -> void {
+ steps__col = new []int
+ for i in 0 .. steps__players * STEPS_PER_CHAPTER { push(steps__col, 0) }
+ steps__col_ch = steps__cur
}
# a party over (or a new one): every column forgotten
-export function steps_party_reset(steps_st: mut StepsState) -> void { steps_st.steps__col = null }
+export function steps_party_reset() -> void { steps__col = null }
# the columns, emptied when the chapter they were about is not this one; seat 0 refilled
-function steps__cols(steps_st: mut StepsState) -> void {
- if steps_st.steps__col == null or steps_st.steps__col_ch != steps_st.steps__cur { steps__party_clear(steps_st) }
- if steps_finished(steps_st) { return }
- for i in 0 .. steps_in(steps_st, steps_st.steps__cur) {
- steps_st.steps__col[i] = steps_mine(steps_st, steps_kind(steps_st, steps_st.steps__cur, i), steps_param(steps_st, steps_st.steps__cur, i), steps_need(steps_st, steps_st.steps__cur, i))
+function steps__cols() -> void {
+ if steps__col == null or steps__col_ch != steps__cur { steps__party_clear() }
+ if steps_finished() { return }
+ for i in 0 .. steps_in(steps__cur) {
+ steps__col[i] = steps_mine(steps_kind(steps__cur, i), steps_param(steps__cur, i), steps_need(steps__cur, i))
}
}
# this machine's shares for the current chapter, to send to whoever owns the story
-export function steps_my_shares(steps_st: mut StepsState) -> []int {
- steps__cols(steps_st)
+export function steps_my_shares() -> []int {
+ steps__cols()
let out = new []int
- for i in 0 .. STEPS_PER_CHAPTER { push(out, steps_st.steps__col[i]) }
+ for i in 0 .. STEPS_PER_CHAPTER { push(out, steps__col[i]) }
return out
}
# a teammate's shares, as they sent them; ignored when they were about another chapter
-export function steps_share(steps_st: mut StepsState, p: int, ch: int, shares: []int) -> void {
- if p <= 0 or p >= steps_st.steps__players or ch != steps_st.steps__cur { return }
- steps__cols(steps_st)
- for i in 0 .. min(len(shares), STEPS_PER_CHAPTER) { steps_st.steps__col[p * STEPS_PER_CHAPTER + i] = shares[i] }
+export function steps_share(p: int, ch: int, shares: []int) -> void {
+ if p <= 0 or p >= steps__players or ch != steps__cur { return }
+ steps__cols()
+ for i in 0 .. min(len(shares), STEPS_PER_CHAPTER) { steps__col[p * STEPS_PER_CHAPTER + i] = shares[i] }
}
-export function steps_share_of(steps_st: mut StepsState, p: int, i: int) -> int {
- steps__cols(steps_st)
- return steps_st.steps__col[p * STEPS_PER_CHAPTER + i]
+export function steps_share_of(p: int, i: int) -> int {
+ steps__cols()
+ return steps__col[p * STEPS_PER_CHAPTER + i]
}
# a seat emptied: what it had goes with it
-export function steps_leave(steps_st: mut StepsState, p: int) -> void {
- if steps_st.steps__col == null or p <= 0 or p >= steps_st.steps__players { return }
- for i in 0 .. STEPS_PER_CHAPTER { steps_st.steps__col[p * STEPS_PER_CHAPTER + i] = 0 }
+export function steps_leave(p: int) -> void {
+ if steps__col == null or p <= 0 or p >= steps__players { return }
+ for i in 0 .. STEPS_PER_CHAPTER { steps__col[p * STEPS_PER_CHAPTER + i] = 0 }
}
# the shares of every seat taken, combined as the pool says (seat 0 is the start)
-function steps__combined(steps_st: StepsState, i: int, how: int) -> int {
- var v = steps_st.steps__col[i]
- for p in 1 .. steps_st.steps__players {
+function steps__combined(i: int, how: int) -> int {
+ var v = steps__col[i]
+ for p in 1 .. steps__players {
if not StepsWorld.present(p) { continue }
- let c = steps_st.steps__col[p * STEPS_PER_CHAPTER + i]
+ let c = steps__col[p * STEPS_PER_CHAPTER + i]
if how == STEPS_POOL_ANY or how == STEPS_POOL_OR { v = v | c }
else if how == STEPS_POOL_SUM { v += c }
else if how == STEPS_POOL_MAX { if c > v { v = c } }
@@ -66,24 +69,24 @@ function steps__combined(steps_st: StepsState, i: int, how: int) -> int {
}
# the answer to step i of the current chapter: solo this is steps_eval and nothing more
-export function steps_met(steps_st: mut StepsState, i: int) -> bool {
- let ch = steps_st.steps__cur
- let kind = steps_kind(steps_st, ch, i)
- if not StepsWorld.party() { return steps_eval(steps_st, kind, steps_param(steps_st, ch, i), steps_need(steps_st, ch, i)) }
- let how = steps_pool(steps_st, kind)
- if how == STEPS_POOL_WORLD { return steps_eval(steps_st, kind, steps_param(steps_st, ch, i), steps_need(steps_st, ch, i)) }
- steps__cols(steps_st)
- return steps_from(steps_st, kind, steps_param(steps_st, ch, i), steps_need(steps_st, ch, i), steps__combined(steps_st, i, how), steps_players(steps_st), steps_each(steps_st, i))
+export function steps_met(i: int) -> bool {
+ let ch = steps__cur
+ let kind = steps_kind(ch, i)
+ if not StepsWorld.party() { return steps_eval(kind, steps_param(ch, i), steps_need(ch, i)) }
+ let how = steps_pool(kind)
+ if how == STEPS_POOL_WORLD { return steps_eval(kind, steps_param(ch, i), steps_need(ch, i)) }
+ steps__cols()
+ return steps_from(kind, steps_param(ch, i), steps_need(ch, i), steps__combined(i, how), steps_players(), steps_each(i))
}
# the party's count for step i of the current chapter, -1 for one that is not a tally
-export function steps_party_got(steps_st: mut StepsState, i: int) -> int {
- let ch = steps_st.steps__cur
- let kind = steps_kind(steps_st, ch, i)
- let param = steps_param(steps_st, ch, i)
- if steps_got(steps_st, kind, param) < 0 { return -1 }
- let how = steps_pool(steps_st, kind)
- if not StepsWorld.party() or how == STEPS_POOL_WORLD or how == STEPS_POOL_ANY or how == STEPS_POOL_EACH { return steps_got(steps_st, kind, param) }
- steps__cols(steps_st)
- return steps_got_from(steps_st, kind, param, steps__combined(steps_st, i, how))
+export function steps_party_got(i: int) -> int {
+ let ch = steps__cur
+ let kind = steps_kind(ch, i)
+ let param = steps_param(ch, i)
+ if steps_got(kind, param) < 0 { return -1 }
+ let how = steps_pool(kind)
+ if not StepsWorld.party() or how == STEPS_POOL_WORLD or how == STEPS_POOL_ANY or how == STEPS_POOL_EACH { return steps_got(kind, param) }
+ steps__cols()
+ return steps_got_from(kind, param, steps__combined(i, how))
}
diff --git a/packages/ludic.steps/state.ludic b/packages/ludic.steps/state.ludic
index 56eb76b4..1d56b9cf 100644
--- a/packages/ludic.steps/state.ludic
+++ b/packages/ludic.steps/state.ludic
@@ -45,31 +45,26 @@ export property StepsFact {
step: int = 0
}
-export state StepsState {
- steps__players: int = 12
- steps__col: []int = null # players * STEPS_PER_CHAPTER shares
- steps__col_ch: int = -1 # the chapter they are about
- steps__kinds: []StepsKind = null
- steps__n: []int = null # steps per chapter
- steps__kind: []int = null # STEPS_PER_CHAPTER per chapter
- steps__param: []int = null
- steps__need: []int = null
- steps__cur: int = 0
- steps__done: int = 0
- steps__facts: Queue = null
+var steps__kinds: []StepsKind = null
+var steps__n: []int = null # steps per chapter
+var steps__kind: []int = null # STEPS_PER_CHAPTER per chapter
+var steps__param: []int = null
+var steps__need: []int = null
+var steps__cur: int = 0
+var steps__done: int = 0
+var steps__facts: Queue = null
+
+export function steps_facts() -> Queue {
+ if steps__facts == null { steps__facts = queue_new("steps.facts") }
+ return steps__facts
}
-export function steps_facts(base_st: mut BaseState, steps_st: mut StepsState) -> Queue {
- if steps_st.steps__facts == null { steps_st.steps__facts = queue_new(base_st, "steps.facts") }
- return steps_st.steps__facts
-}
-
-function steps__fact(base_st: mut BaseState, steps_st: mut StepsState, what: int, ch: int, i: int) -> void {
+function steps__fact(what: int, ch: int, i: int) -> void {
let f = new StepsFact
f.what = what
f.chapter = ch
f.step = i
- q_push(base_st, steps_facts(base_st, steps_st), f)
+ q_push(steps_facts(), f)
}
export function steps_popcount(m: int) -> int {
diff --git a/packages/ludic.steps/tests/steps_test.ludic b/packages/ludic.steps/tests/steps_test.ludic
index 5d4c9f32..bd4655bc 100644
--- a/packages/ludic.steps/tests/steps_test.ludic
+++ b/packages/ludic.steps/tests/steps_test.ludic
@@ -4,18 +4,16 @@ import "ludic.steps"
import "ludic.base"
program StepsTest {
numbers float
- state StepstestState {
- vals: []int = null # value of kind k for this machine
- party: bool = false
- seats: int = 1 # seats 0 .. seats-1 are taken
- }
+ var vals: []int = null # value of kind k for this machine
+ var party: bool = false
+ var seats: int = 1 # seats 0 .. seats-1 are taken
- function fake_value(stepstest_st: StepstestState, kind: int, param: int, player: int) -> int {
- if player != 0 or kind < 0 or kind >= len(stepstest_st.vals) { return 0 }
- return stepstest_st.vals[kind]
+ function fake_value(kind: int, param: int, player: int) -> int {
+ if player != 0 or kind < 0 or kind >= len(vals) { return 0 }
+ return vals[kind]
}
- function fake_party(stepstest_st: StepstestState) -> bool { return stepstest_st.party }
- function fake_present(stepstest_st: StepstestState, p: int) -> bool { return p < stepstest_st.seats }
+ function fake_party() -> bool { return party }
+ function fake_present(p: int) -> bool { return p < seats }
bind StepsWorld { value: fn fake_value, party: fn fake_party, present: fn fake_present }
@@ -35,13 +33,13 @@ program StepsTest {
return k
}
- function fresh(base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) -> void {
- steps_reset(base_st, steps_st)
- steps_config(steps_st, 4)
- stepstest_st.party = false
- stepstest_st.seats = 1
- stepstest_st.vals = new []int
- for i in 0 .. 7 { push(stepstest_st.vals, 0) }
+ function fresh() -> void {
+ steps_reset()
+ steps_config(4)
+ party = false
+ seats = 1
+ vals = new []int
+ for i in 0 .. 7 { push(vals, 0) }
let ks = new []StepsKind
push(ks, kind(STEPS_SHAPE_COUNT, STEPS_POOL_SUM, true))
push(ks, kind(STEPS_SHAPE_MASK, STEPS_POOL_OR, true))
@@ -50,21 +48,21 @@ program StepsTest {
push(ks, kind(STEPS_SHAPE_COUNT, STEPS_POOL_MAX, true))
push(ks, kind(STEPS_SHAPE_YES, STEPS_POOL_EACH, false))
push(ks, kind(STEPS_SHAPE_YES, STEPS_POOL_WORLD, false))
- steps_kinds(steps_st, ks)
+ steps_kinds(ks)
}
# chapter 0: 5 wood, a fire; chapter 1: two signs, the compass
- function two_chapters(steps_st: mut StepsState) -> void {
- steps_chapter_add(steps_st)
- steps_add(steps_st, K_WOOD, 0, 5)
- steps_add(steps_st, K_FIRE, 0, 1)
- steps_chapter_add(steps_st)
- steps_add(steps_st, K_SIGNS, 0, 2)
- steps_add(steps_st, K_COMPASS, 0, 1)
+ function two_chapters() -> void {
+ steps_chapter_add()
+ steps_add(K_WOOD, 0, 5)
+ steps_add(K_FIRE, 0, 1)
+ steps_chapter_add()
+ steps_add(K_SIGNS, 0, 2)
+ steps_add(K_COMPASS, 0, 1)
}
- function facts_of(base_st: mut BaseState, steps_st: mut StepsState, what: int) -> int {
- let fs = q_drain(base_st, steps_facts(base_st, steps_st))
+ function facts_of(what: int) -> int {
+ let fs = q_drain(steps_facts())
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
@@ -77,150 +75,150 @@ program StepsTest {
return s
}
- test "a step is answered by its kind's shape" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- stepstest_st.vals[K_WOOD] = 4
- expect(not steps_eval(steps_st, K_WOOD, 0, 5))
- stepstest_st.vals[K_WOOD] = 5
- expect(steps_eval(steps_st, K_WOOD, 0, 5))
- stepstest_st.vals[K_SIGNS] = 5
- expect(steps_eval(steps_st, K_SIGNS, 0, 2))
- expect_eq(steps_got(steps_st, K_SIGNS, 0), 2)
- stepstest_st.vals[K_ALL] = 3
- expect(not steps_eval(steps_st, K_ALL, 7, 0))
- expect_eq(steps_got(steps_st, K_ALL, 7), 2)
- expect_eq(steps_want(steps_st, K_ALL, 7, 0), 3)
- expect_eq(steps_got(steps_st, K_FIRE, 0), -1)
+ test "a step is answered by its kind's shape" {
+ fresh()
+ vals[K_WOOD] = 4
+ expect(not steps_eval(K_WOOD, 0, 5))
+ vals[K_WOOD] = 5
+ expect(steps_eval(K_WOOD, 0, 5))
+ vals[K_SIGNS] = 5
+ expect(steps_eval(K_SIGNS, 0, 2))
+ expect_eq(steps_got(K_SIGNS, 0), 2)
+ vals[K_ALL] = 3
+ expect(not steps_eval(K_ALL, 7, 0))
+ expect_eq(steps_got(K_ALL, 7), 2)
+ expect_eq(steps_want(K_ALL, 7, 0), 3)
+ expect_eq(steps_got(K_FIRE, 0), -1)
}
- test "an arc's steps stick as they are met, and a full chapter opens the next" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- expect_eq(steps_count(steps_st), 2)
- expect_eq(steps_in(steps_st, 1), 2)
- stepstest_st.vals[K_FIRE] = 1
- steps_check(base_st, steps_st)
- expect(steps_done(steps_st, 1))
- expect(not steps_done(steps_st, 0))
- expect_eq(facts_of(base_st, steps_st, STEPS_MET), 1)
- stepstest_st.vals[K_FIRE] = 0
- steps_check(base_st, steps_st)
- expect(steps_done(steps_st, 1))
- stepstest_st.vals[K_WOOD] = 9
- steps_check(base_st, steps_st)
- expect_eq(steps_current(steps_st), 1)
- expect_eq(steps_done_mask(steps_st), 0)
- let fs = q_drain(base_st, steps_facts(base_st, steps_st))
+ test "an arc's steps stick as they are met, and a full chapter opens the next" {
+ fresh()
+ two_chapters()
+ expect_eq(steps_count(), 2)
+ expect_eq(steps_in(1), 2)
+ vals[K_FIRE] = 1
+ steps_check()
+ expect(steps_done(1))
+ expect(not steps_done(0))
+ expect_eq(facts_of(STEPS_MET), 1)
+ vals[K_FIRE] = 0
+ steps_check()
+ expect(steps_done(1))
+ vals[K_WOOD] = 9
+ steps_check()
+ expect_eq(steps_current(), 1)
+ expect_eq(steps_done_mask(), 0)
+ let fs = q_drain(steps_facts())
expect_eq(len(fs), 2)
expect_eq(fs[1].what, STEPS_CHAPTER)
expect_eq(fs[1].chapter, 0)
}
- test "a step met before its chapter opens ticks the moment it is looked at" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- stepstest_st.vals[K_SIGNS] = 3
- stepstest_st.vals[K_COMPASS] = 1
- stepstest_st.vals[K_WOOD] = 5
- stepstest_st.vals[K_FIRE] = 1
- steps_check(base_st, steps_st)
- expect_eq(steps_current(steps_st), 1)
- steps_check(base_st, steps_st)
- expect(steps_finished(steps_st))
+ test "a step met before its chapter opens ticks the moment it is looked at" {
+ fresh()
+ two_chapters()
+ vals[K_SIGNS] = 3
+ vals[K_COMPASS] = 1
+ vals[K_WOOD] = 5
+ vals[K_FIRE] = 1
+ steps_check()
+ expect_eq(steps_current(), 1)
+ steps_check()
+ expect(steps_finished())
}
- test "a chapter holds at most six steps" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- expect(not steps_add(steps_st, K_WOOD, 0, 1))
- steps_chapter_add(steps_st)
- for i in 0 .. STEPS_PER_CHAPTER { expect(steps_add(steps_st, K_WOOD, 0, i)) }
- expect(not steps_add(steps_st, K_WOOD, 0, 9))
+ test "a chapter holds at most six steps" {
+ fresh()
+ expect(not steps_add(K_WOOD, 0, 1))
+ steps_chapter_add()
+ for i in 0 .. STEPS_PER_CHAPTER { expect(steps_add(K_WOOD, 0, i)) }
+ expect(not steps_add(K_WOOD, 0, 9))
}
- test "the steps asked for right now" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- expect(steps_wants(steps_st, K_FIRE, -1))
- expect(not steps_wants(steps_st, K_SIGNS, -1))
+ test "the steps asked for right now" {
+ fresh()
+ two_chapters()
+ expect(steps_wants(K_FIRE, -1))
+ expect(not steps_wants(K_SIGNS, -1))
}
- test "a tally adds up across the party, and a mask unions" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- stepstest_st.party = true
- stepstest_st.seats = 2
- stepstest_st.vals[K_WOOD] = 3
- expect(not steps_met(steps_st, 0))
- steps_share(steps_st, 1, 0, shares(2, 0))
- expect(steps_met(steps_st, 0))
- expect_eq(steps_party_got(steps_st, 0), 5)
- steps_set_current(steps_st, 1)
- stepstest_st.vals[K_SIGNS] = 1
- steps_share(steps_st, 1, 1, shares(2, 0))
- expect(steps_met(steps_st, 0))
+ test "a tally adds up across the party, and a mask unions" {
+ fresh()
+ two_chapters()
+ party = true
+ seats = 2
+ vals[K_WOOD] = 3
+ expect(not steps_met(0))
+ steps_share(1, 0, shares(2, 0))
+ expect(steps_met(0))
+ expect_eq(steps_party_got(0), 5)
+ steps_set_current(1)
+ vals[K_SIGNS] = 1
+ steps_share(1, 1, shares(2, 0))
+ expect(steps_met(0))
}
- test "anyone having done it is the party having done it" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- stepstest_st.party = true
- stepstest_st.seats = 3
- steps_share(steps_st, 2, 0, shares(0, 1))
- expect(steps_met(steps_st, 1))
+ test "anyone having done it is the party having done it" {
+ fresh()
+ two_chapters()
+ party = true
+ seats = 3
+ steps_share(2, 0, shares(0, 1))
+ expect(steps_met(1))
}
- test "a thing in the pack is met when every player has one, and says who has not" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- steps_set_current(steps_st, 1)
- stepstest_st.party = true
- stepstest_st.seats = 3
- stepstest_st.vals[K_COMPASS] = 1
- steps_share(steps_st, 1, 1, shares(0, 1))
- expect(not steps_met(steps_st, 1))
- expect_eq(steps_each(steps_st, 1), 2)
- let lack = steps_lacking(steps_st, 1)
+ test "a thing in the pack is met when every player has one, and says who has not" {
+ fresh()
+ two_chapters()
+ steps_set_current(1)
+ party = true
+ seats = 3
+ vals[K_COMPASS] = 1
+ steps_share(1, 1, shares(0, 1))
+ expect(not steps_met(1))
+ expect_eq(steps_each(1), 2)
+ let lack = steps_lacking(1)
expect_eq(len(lack), 1)
expect_eq(lack[0], 2)
- steps_share(steps_st, 2, 1, shares(0, 1))
- expect(steps_met(steps_st, 1))
+ steps_share(2, 1, shares(0, 1))
+ expect(steps_met(1))
}
- test "a player who leaves takes their share with them" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- two_chapters(steps_st)
- stepstest_st.party = true
- stepstest_st.seats = 2
- steps_share(steps_st, 1, 0, shares(5, 0))
- expect(steps_met(steps_st, 0))
- steps_leave(steps_st, 1)
- expect(not steps_met(steps_st, 0))
+ test "a player who leaves takes their share with them" {
+ fresh()
+ two_chapters()
+ party = true
+ seats = 2
+ steps_share(1, 0, shares(5, 0))
+ expect(steps_met(0))
+ steps_leave(1)
+ expect(not steps_met(0))
}
- test "a share about another chapter is ignored, and the best of a tally is the best" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- steps_chapter_add(steps_st)
- steps_add(steps_st, K_REP, 0, 25)
- stepstest_st.party = true
- stepstest_st.seats = 2
- steps_share(steps_st, 1, 3, shares(99, 0))
- expect(not steps_met(steps_st, 0))
- stepstest_st.vals[K_REP] = 10
- steps_share(steps_st, 1, 0, shares(30, 0))
- expect(steps_met(steps_st, 0))
- expect_eq(steps_party_got(steps_st, 0), 30)
+ test "a share about another chapter is ignored, and the best of a tally is the best" {
+ fresh()
+ steps_chapter_add()
+ steps_add(K_REP, 0, 25)
+ party = true
+ seats = 2
+ steps_share(1, 3, shares(99, 0))
+ expect(not steps_met(0))
+ vals[K_REP] = 10
+ steps_share(1, 0, shares(30, 0))
+ expect(steps_met(0))
+ expect_eq(steps_party_got(0), 30)
}
- test "the world's steps are asked of this machine alone" (base_st: mut BaseState, steps_st: mut StepsState, stepstest_st: mut StepstestState) {
- fresh(base_st, steps_st, stepstest_st)
- steps_chapter_add(steps_st)
- steps_add(steps_st, K_HOUR, 0, 1)
- stepstest_st.party = true
- stepstest_st.seats = 2
- steps_share(steps_st, 1, 0, shares(1, 0))
- expect(not steps_met(steps_st, 0))
- stepstest_st.vals[K_HOUR] = 1
- expect(steps_met(steps_st, 0))
- expect_eq(steps_mine(steps_st, K_HOUR, 0, 1), 0)
+ test "the world's steps are asked of this machine alone" {
+ fresh()
+ steps_chapter_add()
+ steps_add(K_HOUR, 0, 1)
+ party = true
+ seats = 2
+ steps_share(1, 0, shares(1, 0))
+ expect(not steps_met(0))
+ vals[K_HOUR] = 1
+ expect(steps_met(0))
+ expect_eq(steps_mine(K_HOUR, 0, 1), 0)
}
}
diff --git a/packages/ludic.telemetry/ident.ludic b/packages/ludic.telemetry/ident.ludic
index ed936a49..d5ae9164 100644
--- a/packages/ludic.telemetry/ident.ludic
+++ b/packages/ludic.telemetry/ident.ludic
@@ -1,59 +1,63 @@
# ident.ludic - one player id per machine: the operating system's own id (Windows MachineGuid,
# macOS IOPlatformUUID, Linux /etc/machine-id) hashed with the game's salt, else random
+var telemetry__proc: int = -1
+var telemetry__proc_out: string = ""
+var telemetry__proc_t0: int = 0
+var telemetry__rand_n: int = 0
# n random bytes as hex. Crypto.random_hex reads /dev/urandom, which Windows lacks (zeros there),
# so it is mixed with the clock and the process and hashed: distinct per start everywhere
-export function telemetry_random_hex(telemetry_st: mut TelemetryState, n: int) -> string {
- telemetry_st.telemetry__rand_n += 1
- let seed = Crypto.random_hex(n) + "|" + `{Time.now()}|{Crypto.random_u32()}|{Os.pid()}|{telemetry_st.telemetry__rand_n}`
+export function telemetry_random_hex(n: int) -> string {
+ telemetry__rand_n += 1
+ let seed = Crypto.random_hex(n) + "|" + `{Time.now()}|{Crypto.random_u32()}|{Os.pid()}|{telemetry__rand_n}`
return Crypto.sha256(seed)[0..n * 2]
}
# the id outright (a game with accounts of its own, a test); written to the id file
-export function telemetry_set_id(base_st: mut BaseState, telemetry_st: mut TelemetryState, id: string) -> void {
- telemetry_st.telemetry__id = id
- let path = telemetry__conf(telemetry_st).id_file
+export function telemetry_set_id(id: string) -> void {
+ telemetry__id = id
+ let path = telemetry__conf().id_file
if len(path) > 0 {
var v = telemetry__read_obj(path)
if v == null { v = Value.object() }
Value.put(v, "id", Value.str(id))
Fs.write_text(path, Json.encode(v))
}
- telemetry__fact(base_st, telemetry_st, TELEMETRY_ID, 0, 0, 0)
+ telemetry__fact(TELEMETRY_ID, 0, 0, 0)
}
-function telemetry__random_id(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> void { telemetry_set_id(base_st, telemetry_st, "r-" + telemetry_random_hex(telemetry_st, 16)) }
+function telemetry__random_id() -> void { telemetry_set_id("r-" + telemetry_random_hex(16)) }
-function telemetry__from_machine(base_st: mut BaseState, telemetry_st: mut TelemetryState, raw: string) -> void {
- let salt = telemetry__conf(telemetry_st).id_salt
- telemetry_set_id(base_st, telemetry_st, "m-" + Crypto.sha256(salt + ":" + Text.lower(raw))[0..32])
+function telemetry__from_machine(raw: string) -> void {
+ let salt = telemetry__conf().id_salt
+ telemetry_set_id("m-" + Crypto.sha256(salt + ":" + Text.lower(raw))[0..32])
}
-function telemetry__id_begin(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> void {
- if len(telemetry__conf(telemetry_st).id_salt) == 0 {
- telemetry__random_id(base_st, telemetry_st)
+function telemetry__id_begin() -> void {
+ if len(telemetry__conf().id_salt) == 0 {
+ telemetry__random_id()
return
}
let plat = Os.platform()
if plat == "linux" {
let m = Fs.read_text("/etc/machine-id")
- if m != null and len(Text.trim(m)) > 0 { telemetry__from_machine(base_st, telemetry_st, Text.trim(m)) } else { telemetry__random_id(base_st, telemetry_st) }
+ if m != null and len(Text.trim(m)) > 0 { telemetry__from_machine(Text.trim(m)) } else { telemetry__random_id() }
return
}
- telemetry_st.telemetry__proc_out = telemetry__conf(telemetry_st).id_scratch
- if len(telemetry_st.telemetry__proc_out) == 0 { telemetry_st.telemetry__proc_out = Os.temp_dir() + "/ludic-telemetry-machine.txt" }
- if Fs.exists(telemetry_st.telemetry__proc_out) { Fs.remove(telemetry_st.telemetry__proc_out) }
- telemetry_st.telemetry__proc = telemetry__spawn_machine(telemetry_st, plat)
- telemetry_st.telemetry__proc_t0 = Time.now()
- if telemetry_st.telemetry__proc < 0 { telemetry__random_id(base_st, telemetry_st) }
+ telemetry__proc_out = telemetry__conf().id_scratch
+ if len(telemetry__proc_out) == 0 { telemetry__proc_out = Os.temp_dir() + "/ludic-telemetry-machine.txt" }
+ if Fs.exists(telemetry__proc_out) { Fs.remove(telemetry__proc_out) }
+ telemetry__proc = telemetry__spawn_machine(plat)
+ telemetry__proc_t0 = Time.now()
+ if telemetry__proc < 0 { telemetry__random_id() }
}
-function telemetry__spawn_machine(telemetry_st: TelemetryState, plat: string) -> int {
+function telemetry__spawn_machine(plat: string) -> int {
let args = new []string
if plat == "windows" {
var root = "C:\\Windows"
if Os.has_env("SystemRoot") and len(Os.env("SystemRoot")) > 0 { root = Os.env("SystemRoot") }
- let out = "'" + Text.replace(telemetry_st.telemetry__proc_out, "'", "''") + "'"
+ let out = "'" + Text.replace(telemetry__proc_out, "'", "''") + "'"
push(args, "-NoProfile")
push(args, "-NonInteractive")
push(args, "-Command")
@@ -61,28 +65,28 @@ function telemetry__spawn_machine(telemetry_st: TelemetryState, plat: string) ->
return Process.spawn(root + "\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", args)
}
push(args, "-c")
- push(args, "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}' > '" + telemetry_st.telemetry__proc_out + "'")
+ push(args, "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}' > '" + telemetry__proc_out + "'")
return Process.spawn("/bin/sh", args)
}
# the machine id, once the child has written it (ten seconds at most)
-function telemetry__id_tick(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> void {
- if telemetry_st.telemetry__proc < 0 { return }
- let code = Process.poll(telemetry_st.telemetry__proc)
+function telemetry__id_tick() -> void {
+ if telemetry__proc < 0 { return }
+ let code = Process.poll(telemetry__proc)
if code < 0 {
- if Time.now() - telemetry_st.telemetry__proc_t0 > 10 {
- Process.kill(telemetry_st.telemetry__proc)
- Process.free(telemetry_st.telemetry__proc)
- telemetry_st.telemetry__proc = -1
- telemetry__random_id(base_st, telemetry_st)
+ if Time.now() - telemetry__proc_t0 > 10 {
+ Process.kill(telemetry__proc)
+ Process.free(telemetry__proc)
+ telemetry__proc = -1
+ telemetry__random_id()
}
return
}
- Process.free(telemetry_st.telemetry__proc)
- telemetry_st.telemetry__proc = -1
+ Process.free(telemetry__proc)
+ telemetry__proc = -1
var raw = ""
- let s = Fs.read_text(telemetry_st.telemetry__proc_out)
+ let s = Fs.read_text(telemetry__proc_out)
if s != null { raw = Text.trim(s) }
- if Fs.exists(telemetry_st.telemetry__proc_out) { Fs.remove(telemetry_st.telemetry__proc_out) }
- if code == 0 and len(raw) >= 8 { telemetry__from_machine(base_st, telemetry_st, raw) } else { telemetry__random_id(base_st, telemetry_st) }
+ if Fs.exists(telemetry__proc_out) { Fs.remove(telemetry__proc_out) }
+ if code == 0 and len(raw) >= 8 { telemetry__from_machine(raw) } else { telemetry__random_id() }
}
diff --git a/packages/ludic.telemetry/ports.ludic b/packages/ludic.telemetry/ports.ludic
index 7e74c73c..1f1f20d1 100644
--- a/packages/ludic.telemetry/ports.ludic
+++ b/packages/ludic.telemetry/ports.ludic
@@ -1,17 +1,17 @@
# ports.ludic - the config, and what the ports answer when the game leaves them unbound
-export function telemetry_config(telemetry_st: mut TelemetryState, c: TelemetryConfig) -> void { telemetry_st.telemetry__cfg = c }
+export function telemetry_config(c: TelemetryConfig) -> void { telemetry__cfg = c }
-function telemetry__conf(telemetry_st: mut TelemetryState) -> TelemetryConfig {
- if telemetry_st.telemetry__cfg == null { telemetry_st.telemetry__cfg = new TelemetryConfig }
- return telemetry_st.telemetry__cfg
+function telemetry__conf() -> TelemetryConfig {
+ if telemetry__cfg == null { telemetry__cfg = new TelemetryConfig }
+ return telemetry__cfg
}
function telemetry__yes() -> bool { return true }
-function telemetry__wall_ms(telemetry_st: TelemetryState) -> int { return (Time.now() - telemetry_st.telemetry__t0) * 1000 }
+function telemetry__wall_ms() -> int { return (Time.now() - telemetry__t0) * 1000 }
function telemetry__wall_stamp() -> string { return DateTime.format(Time.now(), "YYYY-MM-DDTHH:mm:ssZ") }
-function telemetry__can_send(telemetry_st: mut TelemetryState) -> bool {
- let c = telemetry__conf(telemetry_st)
+function telemetry__can_send() -> bool {
+ let c = telemetry__conf()
if len(c.host) == 0 or len(c.key) == 0 { return false }
return TelemetryWorld.can_send()
}
@@ -40,12 +40,12 @@ function telemetry__http_poll(h: int) -> int {
function telemetry__http_drop(h: int) -> void { Http.free(h) }
-function telemetry__send(telemetry_st: mut TelemetryState, body: string) -> int { return TelemetryTransport.send(telemetry__conf(telemetry_st).host + telemetry__conf(telemetry_st).path, body) }
+function telemetry__send(body: string) -> int { return TelemetryTransport.send(telemetry__conf().host + telemetry__conf().path, body) }
function telemetry__poll(h: int) -> int { return TelemetryTransport.poll(h) }
# a request abandoned (the player said no)
-function telemetry__drop_request(telemetry_st: mut TelemetryState) -> void {
- if telemetry_st.telemetry__h < 0 { return }
- TelemetryTransport.drop(telemetry_st.telemetry__h)
- telemetry_st.telemetry__h = -1
+function telemetry__drop_request() -> void {
+ if telemetry__h < 0 { return }
+ TelemetryTransport.drop(telemetry__h)
+ telemetry__h = -1
}
diff --git a/packages/ludic.telemetry/queries.ludic b/packages/ludic.telemetry/queries.ludic
index f3ba14c8..55667da1 100644
--- a/packages/ludic.telemetry/queries.ludic
+++ b/packages/ludic.telemetry/queries.ludic
@@ -1,33 +1,33 @@
# queries.ludic - what the game (a Settings row, a test) asks, and a reset back to before start
-export function telemetry_id(telemetry_st: TelemetryState) -> string { return telemetry_st.telemetry__id }
-export function telemetry_session(telemetry_st: TelemetryState) -> string { return telemetry_st.telemetry__session }
-export function telemetry_ready(telemetry_st: TelemetryState) -> bool { return telemetry_st.telemetry__ready }
-export function telemetry_get_config(telemetry_st: mut TelemetryState) -> TelemetryConfig { return telemetry__conf(telemetry_st) }
-export function telemetry_fails(telemetry_st: TelemetryState) -> int { return telemetry_st.telemetry__fails }
-export function telemetry_next_ms(telemetry_st: TelemetryState) -> int { return telemetry_st.telemetry__next_ms }
-export function telemetry_in_flight(telemetry_st: TelemetryState) -> bool { return telemetry_st.telemetry__h >= 0 }
+export function telemetry_id() -> string { return telemetry__id }
+export function telemetry_session() -> string { return telemetry__session }
+export function telemetry_ready() -> bool { return telemetry__ready }
+export function telemetry_get_config() -> TelemetryConfig { return telemetry__conf() }
+export function telemetry_fails() -> int { return telemetry__fails }
+export function telemetry_next_ms() -> int { return telemetry__next_ms }
+export function telemetry_in_flight() -> bool { return telemetry__h >= 0 }
-export function telemetry_queued(telemetry_st: TelemetryState) -> int {
- if telemetry_st.telemetry__q == null { return 0 }
- return len(telemetry_st.telemetry__q)
+export function telemetry_queued() -> int {
+ if telemetry__q == null { return 0 }
+ return len(telemetry__q)
}
# a queued line as it will be sent, player mark and all
-export function telemetry_line(telemetry_st: TelemetryState, i: int) -> string { return telemetry_st.telemetry__q[i] }
+export function telemetry_line(i: int) -> string { return telemetry__q[i] }
# back to before telemetry_start, ports and config kept (tests; a start after a failed boot)
-export function telemetry_reset(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> void {
- telemetry__drop_request(telemetry_st)
- telemetry_st.telemetry__ready = false
- telemetry_st.telemetry__id = ""
- telemetry_st.telemetry__session = ""
- telemetry_st.telemetry__q = null
- telemetry_st.telemetry__dirty = false
- telemetry_st.telemetry__sent_n = 0
- telemetry_st.telemetry__next_ms = 0
- telemetry_st.telemetry__saved_ms = 0
- telemetry_st.telemetry__fails = 0
- telemetry_st.telemetry__clock_on = false
- telemetry_st.telemetry__proc = -1
- q_clear(base_st, telemetry_facts(base_st, telemetry_st))
+export function telemetry_reset() -> void {
+ telemetry__drop_request()
+ telemetry__ready = false
+ telemetry__id = ""
+ telemetry__session = ""
+ telemetry__q = null
+ telemetry__dirty = false
+ telemetry__sent_n = 0
+ telemetry__next_ms = 0
+ telemetry__saved_ms = 0
+ telemetry__fails = 0
+ telemetry__clock_on = false
+ telemetry__proc = -1
+ q_clear(telemetry_facts())
}
diff --git a/packages/ludic.telemetry/queue.ludic b/packages/ludic.telemetry/queue.ludic
index 55191f74..c92408c6 100644
--- a/packages/ludic.telemetry/queue.ludic
+++ b/packages/ludic.telemetry/queue.ludic
@@ -1,90 +1,90 @@
# queue.ludic - a start, an event encoded once into a line, the queue on disk, and forgetting
# once, at boot: who this is (from the id file, or begun from the machine), a new session, and
# whatever an earlier start left unsent - or nothing, if the player has it off
-export function telemetry_start(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> void {
- if telemetry_st.telemetry__ready { return }
- telemetry_st.telemetry__ready = true
- telemetry_st.telemetry__t0 = Time.now()
- telemetry_st.telemetry__session = telemetry_random_hex(telemetry_st, 16)
- telemetry_st.telemetry__q = new []string
- telemetry_st.telemetry__id = sv_str(telemetry__read_obj(telemetry__conf(telemetry_st).id_file), "id", "")
- if len(telemetry_st.telemetry__id) == 0 { telemetry__id_begin(base_st, telemetry_st) }
- if telemetry__enabled() { telemetry__queue_load(telemetry_st) } else { telemetry_forget(telemetry_st) }
+export function telemetry_start() -> void {
+ if telemetry__ready { return }
+ telemetry__ready = true
+ telemetry__t0 = Time.now()
+ telemetry__session = telemetry_random_hex(16)
+ telemetry__q = new []string
+ telemetry__id = sv_str(telemetry__read_obj(telemetry__conf().id_file), "id", "")
+ if len(telemetry__id) == 0 { telemetry__id_begin() }
+ if telemetry__enabled() { telemetry__queue_load() } else { telemetry_forget() }
}
# may an event be kept right now?
-export function telemetry_on(telemetry_st: mut TelemetryState) -> bool { return telemetry_st.telemetry__ready and telemetry__enabled() and telemetry__can_send(telemetry_st) }
+export function telemetry_on() -> bool { return telemetry__ready and telemetry__enabled() and telemetry__can_send() }
# an event: the properties (null for none) with the session and the library added, and the player
# as a mark put in when the batch goes, so an event queued before the id is known is not misfiled
-export function telemetry_event(telemetry_st: mut TelemetryState, name: string, props: Val) -> void {
- if not telemetry_on(telemetry_st) { return }
+export function telemetry_event(name: string, props: Val) -> void {
+ if not telemetry_on() { return }
var p = props
if p == null { p = Value.object() }
- Value.put(p, "$session_id", Value.str(telemetry_st.telemetry__session))
- if len(telemetry__conf(telemetry_st).lib) > 0 { Value.put(p, "$lib", Value.str(telemetry__conf(telemetry_st).lib)) }
+ Value.put(p, "$session_id", Value.str(telemetry__session))
+ if len(telemetry__conf().lib) > 0 { Value.put(p, "$lib", Value.str(telemetry__conf().lib)) }
let e = Value.object()
Value.put(e, "event", Value.str(name))
Value.put(e, "distinct_id", Value.str(TELEMETRY_MARK))
Value.put(e, "properties", p)
Value.put(e, "timestamp", Value.str(telemetry__stamp()))
- push(telemetry_st.telemetry__q, Json.encode(e))
- telemetry_st.telemetry__dirty = true
- let c = telemetry__conf(telemetry_st)
- if len(telemetry_st.telemetry__q) > c.queue_max + c.batch { telemetry__queue_drop(telemetry_st, len(telemetry_st.telemetry__q) - c.queue_max) }
+ push(telemetry__q, Json.encode(e))
+ telemetry__dirty = true
+ let c = telemetry__conf()
+ if len(telemetry__q) > c.queue_max + c.batch { telemetry__queue_drop(len(telemetry__q) - c.queue_max) }
}
# the queue on disk now, if it changed (a quit, a crash about to happen, a child starting)
-export function telemetry_save(telemetry_st: mut TelemetryState) -> void {
- if not telemetry_st.telemetry__dirty or telemetry_st.telemetry__q == null { return }
- telemetry_st.telemetry__dirty = false
- let f = telemetry__conf(telemetry_st).queue_file
+export function telemetry_save() -> void {
+ if not telemetry__dirty or telemetry__q == null { return }
+ telemetry__dirty = false
+ let f = telemetry__conf().queue_file
if len(f) == 0 { return }
- if len(telemetry_st.telemetry__q) == 0 {
+ if len(telemetry__q) == 0 {
if Fs.exists(f) { Fs.remove(f) }
return
}
- Fs.write_text(f, Text.join(telemetry_st.telemetry__q, "\n") + "\n")
+ Fs.write_text(f, Text.join(telemetry__q, "\n") + "\n")
}
# the player turned it off: nothing more is sent, and nothing queued is kept, here or on disk
-export function telemetry_forget(telemetry_st: mut TelemetryState) -> void {
- telemetry__drop_request(telemetry_st)
- telemetry_st.telemetry__q = new []string
- telemetry_st.telemetry__dirty = false
- let f = telemetry__conf(telemetry_st).queue_file
+export function telemetry_forget() -> void {
+ telemetry__drop_request()
+ telemetry__q = new []string
+ telemetry__dirty = false
+ let f = telemetry__conf().queue_file
if len(f) > 0 and Fs.exists(f) { Fs.remove(f) }
}
-function telemetry__queue_load(telemetry_st: mut TelemetryState) -> void {
- telemetry_st.telemetry__q = new []string
- let f = telemetry__conf(telemetry_st).queue_file
+function telemetry__queue_load() -> void {
+ telemetry__q = new []string
+ let f = telemetry__conf().queue_file
if len(f) == 0 { return }
let s = Fs.read_text(f)
if s == null { return }
let parts = Text.split(s, "\n")
- for i in 0 .. len(parts) { if len(parts[i]) > 2 { push(telemetry_st.telemetry__q, parts[i]) } }
- let max = telemetry__conf(telemetry_st).queue_max
- if len(telemetry_st.telemetry__q) > max { telemetry__queue_drop(telemetry_st, len(telemetry_st.telemetry__q) - max) }
+ for i in 0 .. len(parts) { if len(parts[i]) > 2 { push(telemetry__q, parts[i]) } }
+ let max = telemetry__conf().queue_max
+ if len(telemetry__q) > max { telemetry__queue_drop(len(telemetry__q) - max) }
}
# drop the first n lines (sent, or the oldest past the cap)
-function telemetry__queue_drop(telemetry_st: mut TelemetryState, n: int) -> void {
+function telemetry__queue_drop(n: int) -> void {
let rest = new []string
- for i in n .. len(telemetry_st.telemetry__q) { push(rest, telemetry_st.telemetry__q[i]) }
- telemetry_st.telemetry__q = rest
- telemetry_st.telemetry__dirty = true
+ for i in n .. len(telemetry__q) { push(rest, telemetry__q[i]) }
+ telemetry__q = rest
+ telemetry__dirty = true
}
# the batch: the first n lines joined, with the player id put in - no parsing
-export function telemetry_batch_body(telemetry_st: mut TelemetryState, n: int) -> string {
+export function telemetry_batch_body(n: int) -> string {
let part = new []string
- for i in 0 .. n { push(part, telemetry_st.telemetry__q[i]) }
+ for i in 0 .. n { push(part, telemetry__q[i]) }
let head = Value.object()
- Value.put(head, "api_key", Value.str(telemetry__conf(telemetry_st).key))
+ Value.put(head, "api_key", Value.str(telemetry__conf().key))
let h: string = Json.encode(head)
let body = h[0..len(h) - 1] + ",\"batch\":[" + Text.join(part, ",") + "]}"
- return Text.replace(body, TELEMETRY_MARK, telemetry_st.telemetry__id)
+ return Text.replace(body, TELEMETRY_MARK, telemetry__id)
}
function telemetry__read_obj(path: string) -> Val {
diff --git a/packages/ludic.telemetry/send.ludic b/packages/ludic.telemetry/send.ludic
index 18e4717f..777f20f0 100644
--- a/packages/ludic.telemetry/send.ludic
+++ b/packages/ludic.telemetry/send.ludic
@@ -1,69 +1,70 @@
# send.ludic - every frame: a batch when one is due, its answer when it lands, the wait doubled
# when it failed, the queue on disk every few seconds. One request at a time; nothing waits on it.
+var telemetry__clock_on: bool = false
-export function telemetry_tick(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> void {
- if not telemetry_st.telemetry__ready { return }
- telemetry__id_tick(base_st, telemetry_st)
+export function telemetry_tick() -> void {
+ if not telemetry__ready { return }
+ telemetry__id_tick()
if not telemetry__enabled() {
- if len(telemetry_st.telemetry__q) > 0 or telemetry_st.telemetry__h >= 0 { telemetry_forget(telemetry_st) }
+ if len(telemetry__q) > 0 or telemetry__h >= 0 { telemetry_forget() }
return
}
- if not telemetry__can_send(telemetry_st) { return }
+ if not telemetry__can_send() { return }
let now = telemetry__now()
- let c = telemetry__conf(telemetry_st)
- if not telemetry_st.telemetry__clock_on {
- telemetry_st.telemetry__clock_on = true
- telemetry_st.telemetry__next_ms = now + c.flush_ms
- telemetry_st.telemetry__saved_ms = now
+ let c = telemetry__conf()
+ if not telemetry__clock_on {
+ telemetry__clock_on = true
+ telemetry__next_ms = now + c.flush_ms
+ telemetry__saved_ms = now
}
- if now - telemetry_st.telemetry__saved_ms >= c.save_ms {
- telemetry_save(telemetry_st)
- telemetry_st.telemetry__saved_ms = now
+ if now - telemetry__saved_ms >= c.save_ms {
+ telemetry_save()
+ telemetry__saved_ms = now
}
- if telemetry_st.telemetry__h >= 0 {
- telemetry__answer(base_st, telemetry_st, now)
+ if telemetry__h >= 0 {
+ telemetry__answer(now)
return
}
- if len(telemetry_st.telemetry__q) == 0 { return }
- let early = telemetry_st.telemetry__fails == 0 and len(telemetry_st.telemetry__q) >= c.flush_at
- if early or now >= telemetry_st.telemetry__next_ms {
- telemetry_save(telemetry_st)
- telemetry__flush(base_st, telemetry_st, now)
- if telemetry_st.telemetry__fails == 0 { telemetry_st.telemetry__next_ms = now + c.flush_ms }
+ if len(telemetry__q) == 0 { return }
+ let early = telemetry__fails == 0 and len(telemetry__q) >= c.flush_at
+ if early or now >= telemetry__next_ms {
+ telemetry_save()
+ telemetry__flush(now)
+ if telemetry__fails == 0 { telemetry__next_ms = now + c.flush_ms }
}
}
-function telemetry__answer(base_st: mut BaseState, telemetry_st: mut TelemetryState, now: int) -> void {
- let st = telemetry__poll(telemetry_st.telemetry__h)
+function telemetry__answer(now: int) -> void {
+ let st = telemetry__poll(telemetry__h)
if st < 0 { return }
- telemetry_st.telemetry__h = -1
+ telemetry__h = -1
if st > 0 {
- telemetry__queue_drop(telemetry_st, telemetry_st.telemetry__sent_n)
- telemetry_st.telemetry__fails = 0
- telemetry_st.telemetry__next_ms = now + telemetry__conf(telemetry_st).flush_ms
- telemetry__fact(base_st, telemetry_st, TELEMETRY_SENT, telemetry_st.telemetry__sent_n, st, 0)
+ telemetry__queue_drop(telemetry__sent_n)
+ telemetry__fails = 0
+ telemetry__next_ms = now + telemetry__conf().flush_ms
+ telemetry__fact(TELEMETRY_SENT, telemetry__sent_n, st, 0)
} else {
- telemetry__backoff(telemetry_st, now)
- telemetry__fact(base_st, telemetry_st, TELEMETRY_FAILED, telemetry_st.telemetry__sent_n, st, telemetry_st.telemetry__next_ms - now)
+ telemetry__backoff(now)
+ telemetry__fact(TELEMETRY_FAILED, telemetry__sent_n, st, telemetry__next_ms - now)
}
- telemetry_save(telemetry_st)
+ telemetry_save()
}
-function telemetry__flush(base_st: mut BaseState, telemetry_st: mut TelemetryState, now: int) -> void {
- if telemetry_st.telemetry__h >= 0 or len(telemetry_st.telemetry__id) == 0 or len(telemetry_st.telemetry__q) == 0 { return }
- let n = min(telemetry__conf(telemetry_st).batch, len(telemetry_st.telemetry__q))
- let h = telemetry__send(telemetry_st, telemetry_batch_body(telemetry_st, n))
+function telemetry__flush(now: int) -> void {
+ if telemetry__h >= 0 or len(telemetry__id) == 0 or len(telemetry__q) == 0 { return }
+ let n = min(telemetry__conf().batch, len(telemetry__q))
+ let h = telemetry__send(telemetry_batch_body(n))
if h < 0 {
- telemetry__backoff(telemetry_st, now)
- telemetry__fact(base_st, telemetry_st, TELEMETRY_FAILED, n, 0, telemetry_st.telemetry__next_ms - now)
+ telemetry__backoff(now)
+ telemetry__fact(TELEMETRY_FAILED, n, 0, telemetry__next_ms - now)
return
}
- telemetry_st.telemetry__h = h
- telemetry_st.telemetry__sent_n = n
+ telemetry__h = h
+ telemetry__sent_n = n
}
# a failed send: wait twice as long next time, up to backoff_max doublings
-function telemetry__backoff(telemetry_st: mut TelemetryState, now: int) -> void {
- if telemetry_st.telemetry__fails < telemetry__conf(telemetry_st).backoff_max { telemetry_st.telemetry__fails += 1 }
- telemetry_st.telemetry__next_ms = now + (telemetry__conf(telemetry_st).flush_ms << telemetry_st.telemetry__fails)
+function telemetry__backoff(now: int) -> void {
+ if telemetry__fails < telemetry__conf().backoff_max { telemetry__fails += 1 }
+ telemetry__next_ms = now + (telemetry__conf().flush_ms << telemetry__fails)
}
diff --git a/packages/ludic.telemetry/state.ludic b/packages/ludic.telemetry/state.ludic
index 5aafa9cc..5310bce2 100644
--- a/packages/ludic.telemetry/state.ludic
+++ b/packages/ludic.telemetry/state.ludic
@@ -46,37 +46,30 @@ export property TelemetryFact {
const TELEMETRY_MARK: string = "@@TELEMETRY_PLAYER@@"
-export state TelemetryState {
- telemetry__proc: int = -1
- telemetry__proc_out: string = ""
- telemetry__proc_t0: int = 0
- telemetry__rand_n: int = 0
- telemetry__clock_on: bool = false
- telemetry__cfg: TelemetryConfig = null
- telemetry__ready: bool = false
- telemetry__id: string = ""
- telemetry__session: string = ""
- telemetry__q: []string = null # the queued lines, oldest first
- telemetry__dirty: bool = false # telemetry__q differs from the file
- telemetry__h: int = -1 # the batch in flight
- telemetry__sent_n: int = 0 # how many lines it carries
- telemetry__next_ms: int = 0 # when the next send may go
- telemetry__saved_ms: int = 0 # when the queue was last put on disk
- telemetry__fails: int = 0 # sends failed in a row
- telemetry__t0: int = 0 # the default clock's zero, wall seconds
- telemetry__facts: Queue = null
+var telemetry__cfg: TelemetryConfig = null
+var telemetry__ready: bool = false
+var telemetry__id: string = ""
+var telemetry__session: string = ""
+var telemetry__q: []string = null # the queued lines, oldest first
+var telemetry__dirty: bool = false # telemetry__q differs from the file
+var telemetry__h: int = -1 # the batch in flight
+var telemetry__sent_n: int = 0 # how many lines it carries
+var telemetry__next_ms: int = 0 # when the next send may go
+var telemetry__saved_ms: int = 0 # when the queue was last put on disk
+var telemetry__fails: int = 0 # sends failed in a row
+var telemetry__t0: int = 0 # the default clock's zero, wall seconds
+var telemetry__facts: Queue = null
+
+export function telemetry_facts() -> Queue {
+ if telemetry__facts == null { telemetry__facts = queue_new("telemetry.facts") }
+ return telemetry__facts
}
-export function telemetry_facts(base_st: mut BaseState, telemetry_st: mut TelemetryState) -> Queue {
- if telemetry_st.telemetry__facts == null { telemetry_st.telemetry__facts = queue_new(base_st, "telemetry.facts") }
- return telemetry_st.telemetry__facts
-}
-
-function telemetry__fact(base_st: mut BaseState, telemetry_st: mut TelemetryState, what: int, count: int, status: int, retry: int) -> void {
+function telemetry__fact(what: int, count: int, status: int, retry: int) -> void {
let f = new TelemetryFact
f.what = what
f.count = count
f.status = status
f.retry_ms = retry
- q_push(base_st, telemetry_facts(base_st, telemetry_st), f)
+ q_push(telemetry_facts(), f)
}
diff --git a/packages/ludic.telemetry/tests/telemetry_test.ludic b/packages/ludic.telemetry/tests/telemetry_test.ludic
index 071d28f7..20084f62 100644
--- a/packages/ludic.telemetry/tests/telemetry_test.ludic
+++ b/packages/ludic.telemetry/tests/telemetry_test.ludic
@@ -4,29 +4,27 @@ import "ludic.telemetry"
import "ludic.base"
program TelemetryTest {
numbers float
- state TelemetrytestState {
- clock: int = 0
- on: bool = true
- allowed: bool = true
- answer: int = -1
- refuse: bool = false
- sends: int = 0
- last_url: string = ""
- last_body: string = ""
- }
+ var clock: int = 0
+ var on: bool = true
+ var allowed: bool = true
+ var answer: int = -1
+ var refuse: bool = false
+ var sends: int = 0
+ var last_url: string = ""
+ var last_body: string = ""
- function fake_now(telemetrytest_st: TelemetrytestState) -> int { return telemetrytest_st.clock }
- function fake_enabled(telemetrytest_st: TelemetrytestState) -> bool { return telemetrytest_st.on }
- function fake_can(telemetrytest_st: TelemetrytestState) -> bool { return telemetrytest_st.allowed }
+ function fake_now() -> int { return clock }
+ function fake_enabled() -> bool { return on }
+ function fake_can() -> bool { return allowed }
function fake_stamp() -> string { return "2026-09-25T12:00:00Z" }
- function fake_send(telemetrytest_st: mut TelemetrytestState, url: string, body: string) -> int {
- if telemetrytest_st.refuse { return -1 }
- telemetrytest_st.sends += 1
- telemetrytest_st.last_url = url
- telemetrytest_st.last_body = body
+ function fake_send(url: string, body: string) -> int {
+ if refuse { return -1 }
+ sends += 1
+ last_url = url
+ last_body = body
return 7
}
- function fake_poll(telemetrytest_st: TelemetrytestState, h: int) -> int { return telemetrytest_st.answer }
+ function fake_poll(h: int) -> int { return answer }
function fake_drop(h: int) -> void { }
bind TelemetryWorld { can_send: fn fake_can, enabled: fn fake_enabled, now_ms: fn fake_now, stamp: fn fake_stamp }
@@ -35,16 +33,16 @@ program TelemetryTest {
function qfile() -> string { return Os.temp_dir() + "/ludic-telemetry-test-queue.txt" }
function idfile() -> string { return Os.temp_dir() + "/ludic-telemetry-test-id.json" }
- function fresh(base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) -> void {
- telemetrytest_st.clock = 0
- telemetrytest_st.on = true
- telemetrytest_st.allowed = true
- telemetrytest_st.answer = -1
- telemetrytest_st.refuse = false
- telemetrytest_st.sends = 0
- telemetrytest_st.last_url = ""
- telemetrytest_st.last_body = ""
- telemetry_reset(base_st, telemetry_st)
+ function fresh() -> void {
+ clock = 0
+ on = true
+ allowed = true
+ answer = -1
+ refuse = false
+ sends = 0
+ last_url = ""
+ last_body = ""
+ telemetry_reset()
if Fs.exists(qfile()) { Fs.remove(qfile()) }
Fs.write_text(idfile(), "{\"id\":\"p-1\",\"notices\":2}")
let c = new TelemetryConfig
@@ -59,144 +57,144 @@ program TelemetryTest {
c.queue_max = 8
c.backoff_max = 2
c.save_ms = 100000
- telemetry_config(telemetry_st, c)
- telemetry_start(base_st, telemetry_st)
- telemetry_tick(base_st, telemetry_st)
+ telemetry_config(c)
+ telemetry_start()
+ telemetry_tick()
}
- function events(telemetry_st: mut TelemetryState, n: int) -> void {
- for i in 0 .. n { telemetry_event(telemetry_st, `e{i}`, null) }
+ function events(n: int) -> void {
+ for i in 0 .. n { telemetry_event(`e{i}`, null) }
}
- function at(base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState, ms: int) -> void {
- telemetrytest_st.clock = ms
- telemetry_tick(base_st, telemetry_st)
+ function at(ms: int) -> void {
+ clock = ms
+ telemetry_tick()
}
- function facts_of(base_st: mut BaseState, telemetry_st: mut TelemetryState, what: int) -> int {
- let fs = q_drain(base_st, telemetry_facts(base_st, telemetry_st))
+ function facts_of(what: int) -> int {
+ let fs = q_drain(telemetry_facts())
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
}
- test "an event is one line with the player as a mark, and the batch puts the id in" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- expect(telemetry_id(telemetry_st) == "p-1")
- telemetry_event(telemetry_st, "hello", null)
- expect_eq(telemetry_queued(telemetry_st), 1)
- let line = telemetry_line(telemetry_st, 0)
+ test "an event is one line with the player as a mark, and the batch puts the id in" {
+ fresh()
+ expect(telemetry_id() == "p-1")
+ telemetry_event("hello", null)
+ expect_eq(telemetry_queued(), 1)
+ let line = telemetry_line(0)
expect(Text.contains(line, "\"event\":\"hello\""))
expect(Text.contains(line, "test-game"))
expect(not Text.contains(line, "p-1"))
- let body = telemetry_batch_body(telemetry_st, 1)
+ let body = telemetry_batch_body(1)
expect(Text.contains(body, "\"api_key\":\"k-1\""))
expect(Text.contains(body, "\"distinct_id\":\"p-1\""))
}
- test "a batch goes when due, and only what the server took leaves the queue" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- events(telemetry_st, 4)
- at(base_st, telemetry_st, telemetrytest_st, 500)
- expect_eq(telemetrytest_st.sends, 0)
- at(base_st, telemetry_st, telemetrytest_st, 1000)
- expect_eq(telemetrytest_st.sends, 1)
- expect(telemetrytest_st.last_url == "http://fake/batch/")
- expect(telemetry_in_flight(telemetry_st))
- events(telemetry_st, 1)
- telemetrytest_st.answer = 1
- at(base_st, telemetry_st, telemetrytest_st, 1100)
- expect_eq(telemetry_queued(telemetry_st), 2)
- expect_eq(facts_of(base_st, telemetry_st, TELEMETRY_SENT), 1)
+ test "a batch goes when due, and only what the server took leaves the queue" {
+ fresh()
+ events(4)
+ at(500)
+ expect_eq(sends, 0)
+ at(1000)
+ expect_eq(sends, 1)
+ expect(last_url == "http://fake/batch/")
+ expect(telemetry_in_flight())
+ events(1)
+ answer = 1
+ at(1100)
+ expect_eq(telemetry_queued(), 2)
+ expect_eq(facts_of(TELEMETRY_SENT), 1)
}
- test "enough events go early" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- events(telemetry_st, 5)
- at(base_st, telemetry_st, telemetrytest_st, 10)
- expect_eq(telemetrytest_st.sends, 1)
+ test "enough events go early" {
+ fresh()
+ events(5)
+ at(10)
+ expect_eq(sends, 1)
}
- test "a failure keeps the lines and waits twice as long, up to the cap" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- events(telemetry_st, 2)
- telemetrytest_st.answer = 0
- at(base_st, telemetry_st, telemetrytest_st, 1000)
- at(base_st, telemetry_st, telemetrytest_st, 1001)
- expect_eq(telemetry_queued(telemetry_st), 2)
- expect_eq(telemetry_fails(telemetry_st), 1)
- expect_eq(telemetry_next_ms(telemetry_st), 1001 + 2000)
- expect_eq(facts_of(base_st, telemetry_st, TELEMETRY_FAILED), 1)
- at(base_st, telemetry_st, telemetrytest_st, 3001)
- at(base_st, telemetry_st, telemetrytest_st, 3002)
- at(base_st, telemetry_st, telemetrytest_st, 7002)
- at(base_st, telemetry_st, telemetrytest_st, 7003)
- expect_eq(telemetry_fails(telemetry_st), 2)
- expect_eq(telemetry_next_ms(telemetry_st), 7003 + 4000)
+ test "a failure keeps the lines and waits twice as long, up to the cap" {
+ fresh()
+ events(2)
+ answer = 0
+ at(1000)
+ at(1001)
+ expect_eq(telemetry_queued(), 2)
+ expect_eq(telemetry_fails(), 1)
+ expect_eq(telemetry_next_ms(), 1001 + 2000)
+ expect_eq(facts_of(TELEMETRY_FAILED), 1)
+ at(3001)
+ at(3002)
+ at(7002)
+ at(7003)
+ expect_eq(telemetry_fails(), 2)
+ expect_eq(telemetry_next_ms(), 7003 + 4000)
}
- test "a request that cannot start backs off too" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- telemetrytest_st.refuse = true
- events(telemetry_st, 1)
- at(base_st, telemetry_st, telemetrytest_st, 1000)
- expect_eq(telemetry_fails(telemetry_st), 1)
- expect(not telemetry_in_flight(telemetry_st))
+ test "a request that cannot start backs off too" {
+ fresh()
+ refuse = true
+ events(1)
+ at(1000)
+ expect_eq(telemetry_fails(), 1)
+ expect(not telemetry_in_flight())
}
- test "off keeps nothing and deletes the file" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- events(telemetry_st, 3)
- telemetry_save(telemetry_st)
+ test "off keeps nothing and deletes the file" {
+ fresh()
+ events(3)
+ telemetry_save()
expect(Fs.exists(qfile()))
- telemetrytest_st.on = false
- at(base_st, telemetry_st, telemetrytest_st, 10)
- expect_eq(telemetry_queued(telemetry_st), 0)
+ on = false
+ at(10)
+ expect_eq(telemetry_queued(), 0)
expect(not Fs.exists(qfile()))
- events(telemetry_st, 3)
- expect_eq(telemetry_queued(telemetry_st), 0)
+ events(3)
+ expect_eq(telemetry_queued(), 0)
}
- test "a run that may not send queues nothing and sends nothing" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- telemetrytest_st.allowed = false
- events(telemetry_st, 9)
- at(base_st, telemetry_st, telemetrytest_st, 100000)
- expect_eq(telemetry_queued(telemetry_st), 0)
- expect_eq(telemetrytest_st.sends, 0)
+ test "a run that may not send queues nothing and sends nothing" {
+ fresh()
+ allowed = false
+ events(9)
+ at(100000)
+ expect_eq(telemetry_queued(), 0)
+ expect_eq(sends, 0)
}
- test "the queue is capped at its newest events" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- telemetrytest_st.allowed = true
- events(telemetry_st, 12)
- expect(telemetry_queued(telemetry_st) <= 11)
- expect(Text.contains(telemetry_line(telemetry_st, telemetry_queued(telemetry_st) - 1), "e11"))
+ test "the queue is capped at its newest events" {
+ fresh()
+ allowed = true
+ events(12)
+ expect(telemetry_queued() <= 11)
+ expect(Text.contains(telemetry_line(telemetry_queued() - 1), "e11"))
}
- test "what was not sent is there at the next start" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- events(telemetry_st, 3)
- telemetry_save(telemetry_st)
- telemetry_reset(base_st, telemetry_st)
- telemetry_start(base_st, telemetry_st)
- expect_eq(telemetry_queued(telemetry_st), 3)
+ test "what was not sent is there at the next start" {
+ fresh()
+ events(3)
+ telemetry_save()
+ telemetry_reset()
+ telemetry_start()
+ expect_eq(telemetry_queued(), 3)
}
- test "an id set outright is written beside the file's other keys" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- telemetry_set_id(base_st, telemetry_st, "p-2")
+ test "an id set outright is written beside the file's other keys" {
+ fresh()
+ telemetry_set_id("p-2")
let v = Json.parse(Fs.read_text(idfile()))
expect(sv_str(v, "id", "") == "p-2")
expect_eq(sv_int(v, "notices", 0), 2)
}
- test "no salt and no id file makes a random id" (base_st: mut BaseState, telemetry_st: mut TelemetryState, telemetrytest_st: mut TelemetrytestState) {
- fresh(base_st, telemetry_st, telemetrytest_st)
- telemetry_reset(base_st, telemetry_st)
+ test "no salt and no id file makes a random id" {
+ fresh()
+ telemetry_reset()
Fs.remove(idfile())
- telemetry_start(base_st, telemetry_st)
- expect(Text.starts_with(telemetry_id(telemetry_st), "r-"))
- expect_eq(len(telemetry_session(telemetry_st)), 32)
+ telemetry_start()
+ expect(Text.starts_with(telemetry_id(), "r-"))
+ expect_eq(len(telemetry_session()), 32)
}
}
diff --git a/packages/ludic.things/dispatch.ludic b/packages/ludic.things/dispatch.ludic
index 5086144c..ebc9bd74 100644
--- a/packages/ludic.things/dispatch.ludic
+++ b/packages/ludic.things/dispatch.ludic
@@ -21,10 +21,10 @@ export function thing_prompt(t: Thing) -> string {
}
# the kind does what using it does; the use is a fact whatever the kind made of it
-export function thing_use(base_st: mut BaseState, things_st: mut ThingsState, t: Thing) -> void {
+export function thing_use(t: Thing) -> void {
if t == null or not thing_kind_ok(t.kind) { return }
let k = ThingKinds[t.kind]
- th_fact(base_st, things_st, THING_USED, t)
+ th_fact(THING_USED, t)
if k.use != null { k.use(t) }
}
@@ -46,8 +46,8 @@ export function thing_is_personal(t: Thing) -> bool { return thing_kind_ok(t.kin
export function thing_is_put_down(t: Thing) -> bool { return ThingsWorld.put_down(t) }
# every active Thing whose kind ticks, `hours` of the world's clock on
-export function things_tick(things_st: mut ThingsState, hours: float) -> void {
- let all = things_all(things_st)
+export function things_tick(hours: float) -> void {
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if not t.active or not thing_kind_ok(t.kind) { continue }
@@ -58,8 +58,8 @@ export function things_tick(things_st: mut ThingsState, hours: float) -> void {
# a restock: every Thing the world grew is back, and loses its `used` unless its kind keeps it;
# what was put down is left as it is
-export function things_restock(things_st: mut ThingsState) -> void {
- let all = things_all(things_st)
+export function things_restock() -> void {
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if thing_is_put_down(t) { continue }
diff --git a/packages/ludic.things/queries.ludic b/packages/ludic.things/queries.ludic
index a38a6d86..22d17e0a 100644
--- a/packages/ludic.things/queries.ludic
+++ b/packages/ludic.things/queries.ludic
@@ -3,8 +3,8 @@
function th_of(t: Thing, kind: int) -> bool { return kind < 0 or t.kind == kind }
# the first active one of a kind (and of that id, unless id < 0), in the order placed
-export function thing_find(things_st: mut ThingsState, kind: int, id: int) -> Thing {
- let all = things_all(things_st)
+export function thing_find(kind: int, id: int) -> Thing {
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if t.active and th_of(t, kind) and (id < 0 or t.id == id) { return t }
@@ -12,17 +12,17 @@ export function thing_find(things_st: mut ThingsState, kind: int, id: int) -> Th
return null
}
-export function thing_by_uid(things_st: mut ThingsState, uid: int) -> Thing {
- let all = things_all(things_st)
+export function thing_by_uid(uid: int) -> Thing {
+ let all = things_all()
for i in 0 .. len(all) { if all[i].uid == uid { return all[i] } }
return null
}
# the nearest active one of a kind to (x, z), or null
-export function thing_nearest(things_st: mut ThingsState, kind: int, x: float, z: float) -> Thing {
+export function thing_nearest(kind: int, x: float, z: float) -> Thing {
var best: Thing = null
var bd = 0.0
- let all = things_all(things_st)
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if not t.active or not th_of(t, kind) { continue }
@@ -43,9 +43,9 @@ export function thing_dist2(t: Thing, x: float, z: float) -> float {
}
# every active one within r of (x, z), nearest first
-export function things_within(things_st: mut ThingsState, x: float, z: float, r: float) -> []Thing {
+export function things_within(x: float, z: float, r: float) -> []Thing {
let out = new []Thing
- let all = things_all(things_st)
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if t.active and thing_dist2(t, x, z) <= r * r { th_insert(out, t, x, z) }
@@ -64,17 +64,17 @@ function th_insert(out: []Thing, t: Thing, x: float, z: float) -> void {
}
# every active one of a kind, in the order placed
-export function things_each(things_st: mut ThingsState, kind: int) -> []Thing {
+export function things_each(kind: int) -> []Thing {
let out = new []Thing
- let all = things_all(things_st)
+ let all = things_all()
for i in 0 .. len(all) { if all[i].active and th_of(all[i], kind) { push(out, all[i]) } }
return out
}
# every one of a kind, active or not
-export function things_each_all(things_st: mut ThingsState, kind: int) -> []Thing {
+export function things_each_all(kind: int) -> []Thing {
let out = new []Thing
- let all = things_all(things_st)
+ let all = things_all()
for i in 0 .. len(all) { if th_of(all[i], kind) { push(out, all[i]) } }
return out
}
diff --git a/packages/ludic.things/store.ludic b/packages/ludic.things/store.ludic
index 171d9a58..4c5351ad 100644
--- a/packages/ludic.things/store.ludic
+++ b/packages/ludic.things/store.ludic
@@ -1,18 +1,15 @@
# store.ludic - the Things, and the only verbs that make, remove, move, show and hide one; each
# placing and removing is a fact, and the port is told at once so the game can draw it
-export state ThingsState {
- th_list: []Thing = null
- th_uid: int = 0
- th_facts: Queue = null
- th_marked: int = -1
+var th_list: []Thing = null
+var th_uid: int = 0
+var th_facts: Queue = null
+
+export function things_facts() -> Queue {
+ if th_facts == null { th_facts = queue_new("things.facts") }
+ return th_facts
}
-export function things_facts(base_st: mut BaseState, things_st: mut ThingsState) -> Queue {
- if things_st.th_facts == null { things_st.th_facts = queue_new(base_st, "things.facts") }
- return things_st.th_facts
-}
-
-function th_fact(base_st: mut BaseState, things_st: mut ThingsState, what: int, t: Thing) -> void {
+function th_fact(what: int, t: Thing) -> void {
let f = new ThingFact
f.what = what
f.uid = t.uid
@@ -21,28 +18,28 @@ function th_fact(base_st: mut BaseState, things_st: mut ThingsState, what: int,
f.owner = t.owner
f.x = t.x
f.z = t.z
- q_push(base_st, things_facts(base_st, things_st), f)
+ q_push(things_facts(), f)
}
# whether a world has been set out (things_clear) since the program began
-export function things_ready(things_st: ThingsState) -> bool { return things_st.th_list != null }
+export function things_ready() -> bool { return th_list != null }
# every Thing, in the order placed; the list is the package's, never to be pushed to
-export function things_all(things_st: mut ThingsState) -> []Thing {
- if things_st.th_list == null { things_st.th_list = new []Thing }
- return things_st.th_list
+export function things_all() -> []Thing {
+ if th_list == null { th_list = new []Thing }
+ return th_list
}
-export function things_count(things_st: mut ThingsState) -> int { return len(things_all(things_st)) }
+export function things_count() -> int { return len(things_all()) }
# a new world: every Thing forgotten, quietly (the game has already dropped its drawings)
-export function things_clear(things_st: mut ThingsState) -> void { things_st.th_list = new []Thing }
+export function things_clear() -> void { th_list = new []Thing }
-export function thing_spawn(base_st: mut BaseState, things_st: mut ThingsState, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
- return th_place(base_st, things_st, new Thing, kind, look, x, y, z, yaw, id)
+export function thing_spawn(kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
+ return th_place(new Thing, kind, look, x, y, z, yaw, id)
}
-function th_place(base_st: mut BaseState, things_st: mut ThingsState, t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
+function th_place(t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
t.kind = kind
t.look = look
t.x = x
@@ -50,45 +47,46 @@ function th_place(base_st: mut BaseState, things_st: mut ThingsState, t: Thing,
t.z = z
t.yaw = yaw
t.id = id
- things_st.th_uid += 1
- t.uid = things_st.th_uid
- push(things_all(things_st), t)
+ th_uid += 1
+ t.uid = th_uid
+ push(things_all(), t)
ThingsWorld.placed(t)
- th_fact(base_st, things_st, THING_PLACED, t)
+ th_fact(THING_PLACED, t)
return t
}
# a Thing on the ground (the port's), set into it by `sink`, used from `reach`
-export function thing_put(base_st: mut BaseState, things_st: mut ThingsState, kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing {
+export function thing_put(kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing {
let t = new Thing
t.reach = reach
- return th_place(base_st, things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id)
+ return th_place(t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id)
}
# gone for good
-export function thing_remove(base_st: mut BaseState, things_st: mut ThingsState, t: Thing) -> void {
+export function thing_remove(t: Thing) -> void {
if t == null { return }
- things_st.th_marked = t.uid
- things_drop(base_st, things_st, fn th_is_marked)
- things_st.th_marked = -1
+ th_marked = t.uid
+ things_drop(fn th_is_marked)
+ th_marked = -1
}
-function th_is_marked(things_st: ThingsState, t: Thing) -> bool { return t.uid == things_st.th_marked }
+var th_marked: int = -1
+function th_is_marked(t: Thing) -> bool { return t.uid == th_marked }
# every Thing `gone` says so is removed for good
-export function things_drop(base_st: mut BaseState, things_st: mut ThingsState, gone: fn(Thing) -> bool) -> void {
+export function things_drop(gone: fn(Thing) -> bool) -> void {
let keep = new []Thing
- let all = things_all(things_st)
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if gone(t) {
ThingsWorld.removed(t)
- th_fact(base_st, things_st, THING_REMOVED, t)
+ th_fact(THING_REMOVED, t)
} else {
push(keep, t)
}
}
- things_st.th_list = keep
+ th_list = keep
}
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
diff --git a/packages/ludic.things/system.ludic b/packages/ludic.things/system.ludic
index 742c4447..290ab3ca 100644
--- a/packages/ludic.things/system.ludic
+++ b/packages/ludic.things/system.ludic
@@ -1,8 +1,8 @@
# system.ludic - the things as a system: its save section carries every active Thing the port
# keeps, by its kind's KEY (so the game may reorder its kinds), and a load puts each back
-export function things_save(things_st: mut ThingsState) -> Val {
+export function things_save() -> Val {
let l = Value.list()
- let all = things_all(things_st)
+ let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if t.active and thing_kind_ok(t.kind) and ThingsWorld.keeps(t) { Value.add(l, th_to_json(t)) }
@@ -33,18 +33,18 @@ function th_to_json(t: Thing) -> Val {
}
# every kept Thing removed for good: what a load or a new trip is about to put back
-export function things_reset(base_st: mut BaseState, things_st: mut ThingsState) -> void { things_drop(base_st, things_st, fn th_kept) }
+export function things_reset() -> void { things_drop(fn th_kept) }
function th_kept(t: Thing) -> bool { return ThingsWorld.keeps(t) }
# a kind this build does not know is dropped
-export function things_load(base_st: mut BaseState, things_st: mut ThingsState, v: Val, version: int) -> void {
- things_reset(base_st, things_st)
+export function things_load(v: Val, version: int) -> void {
+ things_reset()
if v == null or Value.kind(v) != 6 or Value.has(v, "placed") == 0 { return }
let l = Value.get(v, "placed")
- for i in 0 .. Value.count(l) { th_from_json(base_st, things_st, Value.at(l, i)) }
+ for i in 0 .. Value.count(l) { th_from_json(Value.at(l, i)) }
}
-function th_from_json(base_st: mut BaseState, things_st: mut ThingsState, o: Val) -> void {
+function th_from_json(o: Val) -> void {
let k = thing_kind_by_key(sv_str(o, "kind", ""))
if k < 0 { return }
let t = new Thing
@@ -59,7 +59,7 @@ function th_from_json(base_st: mut BaseState, things_st: mut ThingsState, o: Val
let x = sv_float(o, "x", 0.0)
let z = sv_float(o, "z", 0.0)
let y = sv_float(o, "y", ThingsWorld.ground(x, z))
- th_place(base_st, things_st, t, k, sv_str(o, "look", ""), x, y, z, sv_float(o, "yaw", 0.0), sv_int(o, "id", 0))
+ th_place(t, k, sv_str(o, "look", ""), x, y, z, sv_float(o, "yaw", 0.0), sv_int(o, "id", 0))
ThingsWorld.restored(t)
}
diff --git a/packages/ludic.things/tests/things_test.ludic b/packages/ludic.things/tests/things_test.ludic
index a051057b..65616462 100644
--- a/packages/ludic.things/tests/things_test.ludic
+++ b/packages/ludic.things/tests/things_test.ludic
@@ -4,13 +4,11 @@ import "ludic.things"
import "ludic.base"
program ThingsTest {
numbers float
- state ThingstestState {
- drawn: int = 0
- gone: int = 0
- shows: int = 0
- back: int = 0
- wood: int = 0
- }
+ var drawn: int = 0
+ var gone: int = 0
+ var shows: int = 0
+ var back: int = 0
+ var wood: int = 0
function lit_prompt(t: Thing) -> string {
if t.used != 0 { return "Add wood" }
@@ -21,8 +19,8 @@ program ThingsTest {
if t.owner == 0 { return "Your tent" }
return ""
}
- function branch_use(thingstest_st: mut ThingstestState, t: Thing) -> void {
- thingstest_st.wood += 1
+ function branch_use(t: Thing) -> void {
+ wood += 1
thing_hide(t)
}
function lure_tick(t: Thing, hours: float) -> void {
@@ -38,121 +36,121 @@ program ThingsTest {
def ThingKinds sign { name: "A sign", reach_of: fn far_reach }
function hill(x: float, z: float) -> float { return x * 0.5 }
- function on_placed(thingstest_st: mut ThingstestState, t: Thing) -> void { thingstest_st.drawn += 1 }
- function on_removed(thingstest_st: mut ThingstestState, t: Thing) -> void { thingstest_st.gone += 1 }
- function on_shown(thingstest_st: mut ThingstestState, t: Thing) -> void { thingstest_st.shows += 1 }
- function on_restored(thingstest_st: mut ThingstestState, t: Thing) -> void { thingstest_st.back += 1 }
+ function on_placed(t: Thing) -> void { drawn += 1 }
+ function on_removed(t: Thing) -> void { gone += 1 }
+ function on_shown(t: Thing) -> void { shows += 1 }
+ function on_restored(t: Thing) -> void { back += 1 }
bind ThingsWorld { ground: fn hill, placed: fn on_placed, removed: fn on_removed, shown: fn on_shown, restored: fn on_restored }
- function put(base_st: mut BaseState, things_st: mut ThingsState, kind: int, x: float, z: float) -> Thing { return thing_spawn(base_st, things_st, kind, "m", x, 0.0, z, 0.0, 0) }
- function facts(base_st: mut BaseState, things_st: mut ThingsState) -> []ThingFact { return q_drain(base_st, things_facts(base_st, things_st)) }
+ function put(kind: int, x: float, z: float) -> Thing { return thing_spawn(kind, "m", x, 0.0, z, 0.0, 0) }
+ function facts() -> []ThingFact { return q_drain(things_facts()) }
- test "a spawn is drawn, numbered and said" (base_st: mut BaseState, things_st: mut ThingsState, thingstest_st: ThingstestState) {
- things_clear(things_st)
- let a = put(base_st, things_st, TH_TENT, 1.0, 1.0)
- let b = put(base_st, things_st, TH_FIRE, 2.0, 2.0)
- expect_eq(thingstest_st.drawn, 2)
+ test "a spawn is drawn, numbered and said" {
+ things_clear()
+ let a = put(TH_TENT, 1.0, 1.0)
+ let b = put(TH_FIRE, 2.0, 2.0)
+ expect_eq(drawn, 2)
expect(a.uid != b.uid)
- expect_eq(things_count(things_st), 2)
- let fs = facts(base_st, things_st)
+ expect_eq(things_count(), 2)
+ let fs = facts()
expect_eq(len(fs), 2)
expect_eq(fs[1].what, THING_PLACED)
expect_eq(fs[1].kind, TH_FIRE)
- expect(thing_by_uid(things_st, b.uid) == b)
+ expect(thing_by_uid(b.uid) == b)
}
- test "the spatial questions see only what is active" (base_st: mut BaseState, things_st: mut ThingsState, thingstest_st: ThingstestState) {
- things_clear(things_st)
- let near = put(base_st, things_st, TH_BRANCH, 1.0, 0.0)
- let far = put(base_st, things_st, TH_BRANCH, 5.0, 0.0)
- put(base_st, things_st, TH_FIRE, 3.0, 0.0)
- expect(thing_nearest(things_st, TH_BRANCH, 0.0, 0.0) == near)
- expect(thing_nearest(things_st, -1, 3.2, 0.0).kind == TH_FIRE)
- expect_eq(len(things_within(things_st, 0.0, 0.0, 3.5)), 2)
- expect(things_within(things_st, 6.0, 0.0, 10.0)[0] == far)
+ test "the spatial questions see only what is active" {
+ things_clear()
+ let near = put(TH_BRANCH, 1.0, 0.0)
+ let far = put(TH_BRANCH, 5.0, 0.0)
+ put(TH_FIRE, 3.0, 0.0)
+ expect(thing_nearest(TH_BRANCH, 0.0, 0.0) == near)
+ expect(thing_nearest(-1, 3.2, 0.0).kind == TH_FIRE)
+ expect_eq(len(things_within(0.0, 0.0, 3.5)), 2)
+ expect(things_within(6.0, 0.0, 10.0)[0] == far)
thing_hide(near)
- expect(thing_nearest(things_st, TH_BRANCH, 0.0, 0.0) == far)
- expect_eq(len(things_each(things_st, TH_BRANCH)), 1)
- expect_eq(len(things_each_all(things_st, TH_BRANCH)), 2)
- expect(thing_find(things_st, TH_BRANCH, -1) == far)
- expect(thing_find(things_st, TH_TENT, -1) == null)
- expect_eq(thingstest_st.shows, 1)
+ expect(thing_nearest(TH_BRANCH, 0.0, 0.0) == far)
+ expect_eq(len(things_each(TH_BRANCH)), 1)
+ expect_eq(len(things_each_all(TH_BRANCH)), 2)
+ expect(thing_find(TH_BRANCH, -1) == far)
+ expect(thing_find(TH_TENT, -1) == null)
+ expect_eq(shows, 1)
}
- test "remove and move tell the port" (base_st: mut BaseState, things_st: mut ThingsState, thingstest_st: ThingstestState) {
- things_clear(things_st)
- let a = put(base_st, things_st, TH_TENT, 1.0, 1.0)
- put(base_st, things_st, TH_TENT, 2.0, 2.0)
- facts(base_st, things_st)
- thing_remove(base_st, things_st, a)
- expect_eq(thingstest_st.gone, 1)
- expect_eq(things_count(things_st), 1)
- expect_eq(facts(base_st, things_st)[0].what, THING_REMOVED)
- let b = thing_find(things_st, TH_TENT, -1)
+ test "remove and move tell the port" {
+ things_clear()
+ let a = put(TH_TENT, 1.0, 1.0)
+ put(TH_TENT, 2.0, 2.0)
+ facts()
+ thing_remove(a)
+ expect_eq(gone, 1)
+ expect_eq(things_count(), 1)
+ expect_eq(facts()[0].what, THING_REMOVED)
+ let b = thing_find(TH_TENT, -1)
thing_move(b, 4.0, 0.0, 1.0)
expect_near(b.y, 2.0, 0.001)
- let c = thing_put(base_st, things_st, TH_BRANCH, "stick", 2.0, 0.0, 0.0, 0.25, 1.9, 7)
+ let c = thing_put(TH_BRANCH, "stick", 2.0, 0.0, 0.0, 0.25, 1.9, 7)
expect_near(c.y, 0.75, 0.001)
expect_near(thing_reach(c), 1.9, 0.001)
expect_eq(c.id, 7)
}
- test "the kind answers: name, prompt, use, reach" (base_st: mut BaseState, things_st: mut ThingsState) {
- things_clear(things_st)
- let t = put(base_st, things_st, TH_TENT, 0.0, 0.0)
+ test "the kind answers: name, prompt, use, reach" {
+ things_clear()
+ let t = put(TH_TENT, 0.0, 0.0)
expect(thing_name(t) == "A tent")
t.owner = 0
expect(thing_name(t) == "Your tent")
- let f = put(base_st, things_st, TH_FIRE, 0.0, 0.0)
+ let f = put(TH_FIRE, 0.0, 0.0)
expect(thing_prompt(f) == "Light it")
- thing_use(base_st, things_st, f)
+ thing_use(f)
expect(thing_prompt(f) == "Add wood")
expect(thing_prompt(t) == "")
expect(not thing_can_use(t))
expect_near(thing_reach(f), 2.2, 0.001)
- expect_near(thing_reach(put(base_st, things_st, TH_BRANCH, 0.0, 0.0)), 1.5, 0.001)
- expect_near(thing_reach(put(base_st, things_st, TH_SIGN, 0.0, 0.0)), 9.0, 0.001)
- let fs = facts(base_st, things_st)
+ expect_near(thing_reach(put(TH_BRANCH, 0.0, 0.0)), 1.5, 0.001)
+ expect_near(thing_reach(put(TH_SIGN, 0.0, 0.0)), 9.0, 0.001)
+ let fs = facts()
expect_eq(fs[len(fs) - 3].what, THING_USED)
}
- test "ticks and a restock" (base_st: mut BaseState, things_st: mut ThingsState, thingstest_st: ThingstestState) {
- things_clear(things_st)
- let l = put(base_st, things_st, TH_LURE, 0.0, 0.0)
+ test "ticks and a restock" {
+ things_clear()
+ let l = put(TH_LURE, 0.0, 0.0)
l.timer = 1.0
- let b = put(base_st, things_st, TH_BRANCH, 0.0, 0.0)
- let f = put(base_st, things_st, TH_FIRE, 0.0, 0.0)
- thing_use(base_st, things_st, b)
- thing_use(base_st, things_st, f)
- expect_eq(thingstest_st.wood, 1)
- things_tick(things_st, 0.6)
+ let b = put(TH_BRANCH, 0.0, 0.0)
+ let f = put(TH_FIRE, 0.0, 0.0)
+ thing_use(b)
+ thing_use(f)
+ expect_eq(wood, 1)
+ things_tick(0.6)
expect(l.active)
- things_tick(things_st, 0.6)
+ things_tick(0.6)
expect(not l.active)
- things_restock(things_st)
+ things_restock()
expect(b.active)
expect(not l.active)
expect_eq(f.used, 1)
}
- test "the save keeps what the port keeps, by the kind's key" (base_st: mut BaseState, things_st: mut ThingsState, thingstest_st: ThingstestState) {
- things_clear(things_st)
- let l = put(base_st, things_st, TH_LURE, 3.0, 4.0)
+ test "the save keeps what the port keeps, by the kind's key" {
+ things_clear()
+ let l = put(TH_LURE, 3.0, 4.0)
l.owner = 0
l.timer = 2.5
- put(base_st, things_st, TH_TENT, 0.0, 0.0)
+ put(TH_TENT, 0.0, 0.0)
let root = save_tree()
- save_section(root, "things", 1, things_save(things_st))
+ save_section(root, "things", 1, things_save())
let text = save_encode(root)
- things_clear(things_st)
- put(base_st, things_st, TH_LURE, 9.0, 9.0)
+ things_clear()
+ put(TH_LURE, 9.0, 9.0)
let n = load_section(save_decode(text), "things")
- things_load(base_st, things_st, n.data, n.version)
- expect_eq(things_count(things_st), 1)
- let t = thing_find(things_st, TH_LURE, -1)
+ things_load(n.data, n.version)
+ expect_eq(things_count(), 1)
+ let t = thing_find(TH_LURE, -1)
expect_near(t.x, 3.0, 0.001)
expect_near(t.timer, 2.5, 0.001)
expect_eq(t.owner, 0)
- expect_eq(thingstest_st.back, 1)
+ expect_eq(back, 1)
}
}
diff --git a/packages/ludic.tracks/config.ludic b/packages/ludic.tracks/config.ludic
index 4c850ccf..7b4c3df4 100644
--- a/packages/ludic.tracks/config.ludic
+++ b/packages/ludic.tracks/config.ludic
@@ -1,32 +1,32 @@
# config.ludic - how many kinds of print, how many of each the ground remembers, how the reader
# reads, and the port's defaults
-export function tracks_config(tracks_st: mut TracksState, kinds: int, cap: int) -> void {
- tracks_st.tk_kinds = kinds
- tracks_st.tk_cap = cap
- tracks_st.tk_ring = new []Track
- tracks_st.tk_count = new []int
- tracks_st.tk_next = new []int
- for i in 0 .. kinds * cap { push(tracks_st.tk_ring, new Track) }
+export function tracks_config(kinds: int, cap: int) -> void {
+ tk_kinds = kinds
+ tk_cap = cap
+ tk_ring = new []Track
+ tk_count = new []int
+ tk_next = new []int
+ for i in 0 .. kinds * cap { push(tk_ring, new Track) }
for k in 0 .. kinds {
- push(tracks_st.tk_count, 0)
- push(tracks_st.tk_next, 0)
+ push(tk_count, 0)
+ push(tk_next, 0)
}
}
# a reader at `name_at` or above can tell the animal; a print is fresh under `fresh` hours old,
# recent under `recent`, and old after
-export function tracks_config_reading(tracks_st: mut TracksState, name_at: int, fresh: float, recent: float) -> void {
- tracks_st.tk_name_at = name_at
- tracks_st.tk_fresh_h = fresh
- tracks_st.tk_recent_h = recent
+export function tracks_config_reading(name_at: int, fresh: float, recent: float) -> void {
+ tk_name_at = name_at
+ tk_fresh_h = fresh
+ tk_recent_h = recent
}
function tk_now() -> float {
return TracksWorld.now()
}
-function tk_named(tracks_st: TracksState) -> bool {
- return TracksWorld.skill() >= tracks_st.tk_name_at
+function tk_named() -> bool {
+ return TracksWorld.skill() >= tk_name_at
}
function tk_bearing(yaw: float) -> string {
diff --git a/packages/ludic.tracks/leave.ludic b/packages/ludic.tracks/leave.ludic
index d7b95414..f24fe7a4 100644
--- a/packages/ludic.tracks/leave.ludic
+++ b/packages/ludic.tracks/leave.ludic
@@ -1,17 +1,17 @@
# leave.ludic - a print put down. Each kind is a ring: past its cap the oldest is written over, and
# a slot written over is a new print, unread
-function tk_ok(tracks_st: TracksState, kind: int) -> bool { return tracks_st.tk_ring != null and kind >= 0 and kind < tracks_st.tk_kinds }
+function tk_ok(kind: int) -> bool { return tk_ring != null and kind >= 0 and kind < tk_kinds }
# a print made at `made` game hours (another machine's, arriving here); the ring index, or -1
-export function tracks_leave_at(tracks_st: mut TracksState, kind: int, x: float, z: float, yaw: float, sp: int, legend: bool, made: float) -> int {
- if not tk_ok(tracks_st, kind) { return -1 }
- var i = tracks_st.tk_next[kind]
- if tracks_st.tk_count[kind] < tracks_st.tk_cap {
- i = tracks_st.tk_count[kind]
- tracks_st.tk_count[kind] = tracks_st.tk_count[kind] + 1
+export function tracks_leave_at(kind: int, x: float, z: float, yaw: float, sp: int, legend: bool, made: float) -> int {
+ if not tk_ok(kind) { return -1 }
+ var i = tk_next[kind]
+ if tk_count[kind] < tk_cap {
+ i = tk_count[kind]
+ tk_count[kind] = tk_count[kind] + 1
}
- tracks_st.tk_next[kind] = (i + 1) % tracks_st.tk_cap
- let t = tracks_st.tk_ring[kind * tracks_st.tk_cap + i]
+ tk_next[kind] = (i + 1) % tk_cap
+ let t = tk_ring[kind * tk_cap + i]
t.x = x
t.z = z
t.yaw = yaw
@@ -23,15 +23,15 @@ export function tracks_leave_at(tracks_st: mut TracksState, kind: int, x: float,
return i
}
-export function tracks_leave(tracks_st: mut TracksState, kind: int, x: float, z: float, yaw: float, sp: int, legend: bool) -> int {
- return tracks_leave_at(tracks_st, kind, x, z, yaw, sp, legend, tk_now())
+export function tracks_leave(kind: int, x: float, z: float, yaw: float, sp: int, legend: bool) -> int {
+ return tracks_leave_at(kind, x, z, yaw, sp, legend, tk_now())
}
# the ground forgets every print (a new world)
-export function tracks_clear(tracks_st: mut TracksState) -> void {
- if tracks_st.tk_ring == null { return }
- for k in 0 .. tracks_st.tk_kinds {
- tracks_st.tk_count[k] = 0
- tracks_st.tk_next[k] = 0
+export function tracks_clear() -> void {
+ if tk_ring == null { return }
+ for k in 0 .. tk_kinds {
+ tk_count[k] = 0
+ tk_next[k] = 0
}
}
diff --git a/packages/ludic.tracks/queries.ludic b/packages/ludic.tracks/queries.ludic
index cbe91287..8749e468 100644
--- a/packages/ludic.tracks/queries.ludic
+++ b/packages/ludic.tracks/queries.ludic
@@ -1,38 +1,38 @@
# queries.ludic - what the world asks the ground. A slot is kind * cap + ring index.
-export function tracks_kinds(tracks_st: TracksState) -> int { return tracks_st.tk_kinds }
-export function tracks_cap(tracks_st: TracksState) -> int { return tracks_st.tk_cap }
+export function tracks_kinds() -> int { return tk_kinds }
+export function tracks_cap() -> int { return tk_cap }
-export function tracks_count(tracks_st: TracksState, kind: int) -> int {
- if not tk_ok(tracks_st, kind) { return 0 }
- return tracks_st.tk_count[kind]
+export function tracks_count(kind: int) -> int {
+ if not tk_ok(kind) { return 0 }
+ return tk_count[kind]
}
-export function tracks_slot(tracks_st: TracksState, kind: int, i: int) -> int { return kind * tracks_st.tk_cap + i }
+export function tracks_slot(kind: int, i: int) -> int { return kind * tk_cap + i }
-export function tracks_x(tracks_st: TracksState, slot: int) -> float { return tracks_st.tk_ring[slot].x }
-export function tracks_z(tracks_st: TracksState, slot: int) -> float { return tracks_st.tk_ring[slot].z }
-export function tracks_yaw(tracks_st: TracksState, slot: int) -> float { return tracks_st.tk_ring[slot].yaw }
-export function tracks_sp(tracks_st: TracksState, slot: int) -> int { return tracks_st.tk_ring[slot].sp }
-export function tracks_legend(tracks_st: TracksState, slot: int) -> bool { return tracks_st.tk_ring[slot].legend }
-export function tracks_made(tracks_st: TracksState, slot: int) -> float { return tracks_st.tk_ring[slot].made }
-export function tracks_was_read(tracks_st: TracksState, slot: int) -> bool { return tracks_st.tk_ring[slot].read }
+export function tracks_x(slot: int) -> float { return tk_ring[slot].x }
+export function tracks_z(slot: int) -> float { return tk_ring[slot].z }
+export function tracks_yaw(slot: int) -> float { return tk_ring[slot].yaw }
+export function tracks_sp(slot: int) -> int { return tk_ring[slot].sp }
+export function tracks_legend(slot: int) -> bool { return tk_ring[slot].legend }
+export function tracks_made(slot: int) -> float { return tk_ring[slot].made }
+export function tracks_was_read(slot: int) -> bool { return tk_ring[slot].read }
# game hours since it was made; never below 0
-export function tracks_age(tracks_st: TracksState, slot: int) -> float { return Math.max(0.0, tk_now() - tracks_st.tk_ring[slot].made) }
+export function tracks_age(slot: int) -> float { return Math.max(0.0, tk_now() - tk_ring[slot].made) }
# the nearest print within `r` of (x, z), as a slot; -1 when there is none
-export function tracks_nearest(tracks_st: TracksState, x: float, z: float, r: float) -> int {
+export function tracks_nearest(x: float, z: float, r: float) -> int {
var best = r * r
var at = -1
- for k in 0 .. tracks_st.tk_kinds {
- for i in 0 .. tracks_st.tk_count[k] {
- let t = tracks_st.tk_ring[k * tracks_st.tk_cap + i]
+ for k in 0 .. tk_kinds {
+ for i in 0 .. tk_count[k] {
+ let t = tk_ring[k * tk_cap + i]
let dx = t.x - x
let dz = t.z - z
let d2 = dx * dx + dz * dz
if d2 < best {
best = d2
- at = k * tracks_st.tk_cap + i
+ at = k * tk_cap + i
}
}
}
@@ -40,8 +40,8 @@ export function tracks_nearest(tracks_st: TracksState, x: float, z: float, r: fl
}
# signs read for the first time, of every source
-export function tracks_read_count(tracks_st: TracksState) -> int { return tracks_st.tk_read_n }
+export function tracks_read_count() -> int { return tk_read_n }
# the heading of the last sign read, and whether there has been one
-export function tracks_last_yaw(tracks_st: TracksState) -> float { return tracks_st.tk_yaw }
-export function tracks_has_bearing(tracks_st: TracksState) -> bool { return tracks_st.tk_has_yaw }
+export function tracks_last_yaw() -> float { return tk_yaw }
+export function tracks_has_bearing() -> bool { return tk_has_yaw }
diff --git a/packages/ludic.tracks/read.ludic b/packages/ludic.tracks/read.ludic
index 6fa4326e..645f413d 100644
--- a/packages/ludic.tracks/read.ludic
+++ b/packages/ludic.tracks/read.ludic
@@ -1,54 +1,54 @@
# read.ludic - reading the ground. A print is read once: reading it again says the same thing
# and counts nothing, so pressing a key over one print is not as good as walking a trail.
-export function tracks_fresh_of(tracks_st: TracksState, age: float) -> int {
- if age > tracks_st.tk_recent_h { return TRACK_OLD }
- if age > tracks_st.tk_fresh_h { return TRACK_RECENT }
+export function tracks_fresh_of(age: float) -> int {
+ if age > tk_recent_h { return TRACK_OLD }
+ if age > tk_fresh_h { return TRACK_RECENT }
return TRACK_FRESH
}
-function tk_push(base_st: mut BaseState, tracks_st: mut TracksState, f: TrackRead) -> void {
- if f.first { tracks_st.tk_read_n += 1 }
- q_push(base_st, tracks_reads(base_st, tracks_st), f)
+function tk_push(f: TrackRead) -> void {
+ if f.first { tk_read_n += 1 }
+ q_push(tracks_reads(), f)
}
# read the print at a slot (tracks_nearest's answer): a fact every time, a count the first time
-export function tracks_read(base_st: mut BaseState, tracks_st: mut TracksState, slot: int) -> void {
- if slot < 0 or tracks_st.tk_ring == null or slot >= len(tracks_st.tk_ring) { return }
- let t = tracks_st.tk_ring[slot]
+export function tracks_read(slot: int) -> void {
+ if slot < 0 or tk_ring == null or slot >= len(tk_ring) { return }
+ let t = tk_ring[slot]
let f = new TrackRead
f.sp = t.sp
f.source = TRACK_PRINT
- f.kind = slot / tracks_st.tk_cap
+ f.kind = slot / tk_cap
f.legend = t.legend
- f.age = tracks_age(tracks_st, slot)
- f.fresh = tracks_fresh_of(tracks_st, f.age)
+ f.age = tracks_age(slot)
+ f.fresh = tracks_fresh_of(f.age)
f.yaw = t.yaw
- f.named = tk_named(tracks_st)
+ f.named = tk_named()
f.bearing = tk_bearing(t.yaw)
f.first = not t.read
t.read = true
- tracks_note_bearing(tracks_st, t.yaw)
- tk_push(base_st, tracks_st, f)
+ tracks_note_bearing(t.yaw)
+ tk_push(f)
}
# a sign that is not a print (droppings, a legend's mark): the caller knows whether it is the
# first reading of it
-export function tracks_note(base_st: mut BaseState, tracks_st: mut TracksState, sp: int, source: int, legend: bool, yaw: float, first: bool) -> void {
+export function tracks_note(sp: int, source: int, legend: bool, yaw: float, first: bool) -> void {
let f = new TrackRead
f.sp = sp
f.source = source
f.legend = legend
f.yaw = yaw
- f.named = tk_named(tracks_st)
+ f.named = tk_named()
f.bearing = tk_bearing(yaw)
f.first = first
- tk_push(base_st, tracks_st, f)
+ tk_push(f)
}
# the way the last thing read went (a bed, droppings, a print)
-export function tracks_note_bearing(tracks_st: mut TracksState, yaw: float) -> void {
- tracks_st.tk_yaw = yaw
- tracks_st.tk_has_yaw = true
+export function tracks_note_bearing(yaw: float) -> void {
+ tk_yaw = yaw
+ tk_has_yaw = true
}
-export function tracks_set_read_count(tracks_st: mut TracksState, n: int) -> void { tracks_st.tk_read_n = n }
+export function tracks_set_read_count(n: int) -> void { tk_read_n = n }
diff --git a/packages/ludic.tracks/state.ludic b/packages/ludic.tracks/state.ludic
index 3a000d32..01c71eda 100644
--- a/packages/ludic.tracks/state.ludic
+++ b/packages/ludic.tracks/state.ludic
@@ -47,24 +47,22 @@ export property TrackRead {
first: bool = false
}
-export state TracksState {
- tk_kinds: int = 0
- tk_cap: int = 0
- tk_ring: []Track = null # kinds x cap, kind-major
- tk_count: []int = null # per kind: how many of the ring are in use
- tk_next: []int = null # per kind: where the next print goes
- tk_name_at: int = 0 # the skill that tells the animal from its print
- tk_fresh_h: float = 0.5
- tk_recent_h: float = 2.5
- tk_read_n: int = 0 # signs read for the first time, of every source
- tk_yaw: float = 0.0 # the heading of the last sign read
- tk_has_yaw: bool = false
- tk_reads: Queue = null
-}
+var tk_kinds: int = 0
+var tk_cap: int = 0
+var tk_ring: []Track = null # kinds x cap, kind-major
+var tk_count: []int = null # per kind: how many of the ring are in use
+var tk_next: []int = null # per kind: where the next print goes
+var tk_name_at: int = 0 # the skill that tells the animal from its print
+var tk_fresh_h: float = 0.5
+var tk_recent_h: float = 2.5
+var tk_read_n: int = 0 # signs read for the first time, of every source
+var tk_yaw: float = 0.0 # the heading of the last sign read
+var tk_has_yaw: bool = false
+var tk_reads: Queue = null
# the facts: every sign read, oldest first
-export function tracks_reads(base_st: mut BaseState, tracks_st: mut TracksState) -> Queue {
- if tracks_st.tk_reads == null { tracks_st.tk_reads = queue_new(base_st, "tracks.reads") }
- return tracks_st.tk_reads
+export function tracks_reads() -> Queue {
+ if tk_reads == null { tk_reads = queue_new("tracks.reads") }
+ return tk_reads
}
diff --git a/packages/ludic.tracks/system.ludic b/packages/ludic.tracks/system.ludic
index e672824f..2ce6f91f 100644
--- a/packages/ludic.tracks/system.ludic
+++ b/packages/ludic.tracks/system.ludic
@@ -1,25 +1,25 @@
# system.ludic - the reader as a system: what has been read and the last bearing are saved; the
# prints themselves are the ground's and are not (the animals leave new ones)
-export function tracks_reset(base_st: mut BaseState, tracks_st: mut TracksState) -> void {
- tracks_st.tk_read_n = 0
- tracks_st.tk_yaw = 0.0
- tracks_st.tk_has_yaw = false
- q_clear(base_st, tracks_reads(base_st, tracks_st))
+export function tracks_reset() -> void {
+ tk_read_n = 0
+ tk_yaw = 0.0
+ tk_has_yaw = false
+ q_clear(tracks_reads())
}
-export function tracks_save(tracks_st: TracksState) -> Val {
+export function tracks_save() -> Val {
let v = Value.object()
- sv_put_int(v, "read", tracks_st.tk_read_n)
- sv_put_bool(v, "has_yaw", tracks_st.tk_has_yaw)
- sv_put_float(v, "yaw", tracks_st.tk_yaw)
+ sv_put_int(v, "read", tk_read_n)
+ sv_put_bool(v, "has_yaw", tk_has_yaw)
+ sv_put_float(v, "yaw", tk_yaw)
return v
}
-export function tracks_load(base_st: mut BaseState, tracks_st: mut TracksState, v: Val, version: int) -> void {
- tracks_reset(base_st, tracks_st)
- tracks_st.tk_read_n = sv_int(v, "read", 0)
- tracks_st.tk_has_yaw = sv_bool(v, "has_yaw", false)
- tracks_st.tk_yaw = sv_float(v, "yaw", 0.0)
+export function tracks_load(v: Val, version: int) -> void {
+ tracks_reset()
+ tk_read_n = sv_int(v, "read", 0)
+ tk_has_yaw = sv_bool(v, "has_yaw", false)
+ tk_yaw = sv_float(v, "yaw", 0.0)
}
export function tracks_system() -> System {
diff --git a/packages/ludic.tracks/tests/tracks_test.ludic b/packages/ludic.tracks/tests/tracks_test.ludic
index b027ef78..1521b519 100644
--- a/packages/ludic.tracks/tests/tracks_test.ludic
+++ b/packages/ludic.tracks/tests/tracks_test.ludic
@@ -5,140 +5,138 @@ import "ludic.tracks"
import "ludic.base"
program TracksTest {
numbers float
- state TrackstestState {
- now: float = 0.0
- skill: int = 1
- placed_n: int = 0
- placed_last: int = -1
- }
+ var now: float = 0.0
+ var skill: int = 1
+ var placed_n: int = 0
+ var placed_last: int = -1
- function fake_now(trackstest_st: TrackstestState) -> float { return trackstest_st.now }
- function fake_skill(trackstest_st: TrackstestState) -> int { return trackstest_st.skill }
+ function fake_now() -> float { return now }
+ function fake_skill() -> int { return skill }
function fake_bearing(yaw: float) -> string {
if yaw > 0.0 { return "east" }
return "west"
}
- function fake_placed(trackstest_st: mut TrackstestState, kind: int, i: int) -> void {
- trackstest_st.placed_n += 1
- trackstest_st.placed_last = i
+ function fake_placed(kind: int, i: int) -> void {
+ placed_n += 1
+ placed_last = i
}
bind TracksWorld { now: fn fake_now, skill: fn fake_skill, bearing: fn fake_bearing, placed: fn fake_placed }
- function fresh(base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) -> void {
- trackstest_st.now = 10.0
- trackstest_st.skill = 1
- trackstest_st.placed_n = 0
- trackstest_st.placed_last = -1
- tracks_config(tracks_st, 3, 4)
- tracks_config_reading(tracks_st, 1, 0.5, 2.5)
- tracks_reset(base_st, tracks_st)
+ function fresh() -> void {
+ now = 10.0
+ skill = 1
+ placed_n = 0
+ placed_last = -1
+ tracks_config(3, 4)
+ tracks_config_reading(1, 0.5, 2.5)
+ tracks_reset()
}
- function reads(base_st: mut BaseState, tracks_st: mut TracksState) -> []TrackRead { return q_drain(base_st, tracks_reads(base_st, tracks_st)) }
+ function reads() -> []TrackRead { return q_drain(tracks_reads()) }
- test "a print goes into its kind's ring and is drawn" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- let i = tracks_leave(tracks_st, 1, 5.0, 6.0, 1.0, 7, false)
+ test "a print goes into its kind's ring and is drawn" {
+ fresh()
+ let i = tracks_leave(1, 5.0, 6.0, 1.0, 7, false)
expect_eq(i, 0)
- expect_eq(tracks_count(tracks_st, 1), 1)
- expect_eq(tracks_count(tracks_st, 0), 0)
- expect_eq(trackstest_st.placed_n, 1)
- let s = tracks_slot(tracks_st, 1, 0)
- expect(tracks_x(tracks_st, s) == 5.0 and tracks_z(tracks_st, s) == 6.0)
- expect_eq(tracks_sp(tracks_st, s), 7)
+ expect_eq(tracks_count(1), 1)
+ expect_eq(tracks_count(0), 0)
+ expect_eq(placed_n, 1)
+ let s = tracks_slot(1, 0)
+ expect(tracks_x(s) == 5.0 and tracks_z(s) == 6.0)
+ expect_eq(tracks_sp(s), 7)
}
- test "a full ring writes over its oldest print" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- for n in 0 .. 5 { tracks_leave(tracks_st, 2, float(n), 0.0, 0.0, n, false) }
- expect_eq(tracks_count(tracks_st, 2), 4)
- expect_eq(trackstest_st.placed_last, 0)
- expect_eq(tracks_sp(tracks_st, tracks_slot(tracks_st, 2, 0)), 4)
+ test "a full ring writes over its oldest print" {
+ fresh()
+ for n in 0 .. 5 { tracks_leave(2, float(n), 0.0, 0.0, n, false) }
+ expect_eq(tracks_count(2), 4)
+ expect_eq(placed_last, 0)
+ expect_eq(tracks_sp(tracks_slot(2, 0)), 4)
}
- test "a print ages on the world's clock" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_leave(tracks_st, 1, 0.0, 0.0, 0.0, 1, false)
- trackstest_st.now = 13.0
- let s = tracks_slot(tracks_st, 1, 0)
- expect(tracks_age(tracks_st, s) == 3.0)
- expect_eq(tracks_fresh_of(tracks_st, tracks_age(tracks_st, s)), TRACK_OLD)
- expect_eq(tracks_fresh_of(tracks_st, 0.2), TRACK_FRESH)
- expect_eq(tracks_fresh_of(tracks_st, 1.0), TRACK_RECENT)
+ test "a print ages on the world's clock" {
+ fresh()
+ tracks_leave(1, 0.0, 0.0, 0.0, 1, false)
+ now = 13.0
+ let s = tracks_slot(1, 0)
+ expect(tracks_age(s) == 3.0)
+ expect_eq(tracks_fresh_of(tracks_age(s)), TRACK_OLD)
+ expect_eq(tracks_fresh_of(0.2), TRACK_FRESH)
+ expect_eq(tracks_fresh_of(1.0), TRACK_RECENT)
}
- test "the nearest print within reach, or none" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_leave(tracks_st, 1, 10.0, 0.0, 0.0, 1, false)
- tracks_leave(tracks_st, 2, 1.0, 1.0, 0.0, 2, false)
- expect_eq(tracks_nearest(tracks_st, 0.0, 0.0, 2.0), tracks_slot(tracks_st, 2, 0))
- expect_eq(tracks_nearest(tracks_st, 50.0, 50.0, 2.0), -1)
+ test "the nearest print within reach, or none" {
+ fresh()
+ tracks_leave(1, 10.0, 0.0, 0.0, 1, false)
+ tracks_leave(2, 1.0, 1.0, 0.0, 2, false)
+ expect_eq(tracks_nearest(0.0, 0.0, 2.0), tracks_slot(2, 0))
+ expect_eq(tracks_nearest(50.0, 50.0, 2.0), -1)
}
- test "a print read twice is two facts and one count" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_leave(tracks_st, 1, 0.0, 0.0, 1.0, 3, true)
- let s = tracks_nearest(tracks_st, 0.0, 0.0, 2.0)
- tracks_read(base_st, tracks_st, s)
- tracks_read(base_st, tracks_st, s)
- let rs = reads(base_st, tracks_st)
+ test "a print read twice is two facts and one count" {
+ fresh()
+ tracks_leave(1, 0.0, 0.0, 1.0, 3, true)
+ let s = tracks_nearest(0.0, 0.0, 2.0)
+ tracks_read(s)
+ tracks_read(s)
+ let rs = reads()
expect_eq(len(rs), 2)
expect(rs[0].first and not rs[1].first)
expect_eq(rs[0].source, TRACK_PRINT)
expect_eq(rs[0].kind, 1)
expect(rs[0].legend)
expect(rs[0].bearing == "east")
- expect_eq(tracks_read_count(tracks_st), 1)
- expect(tracks_has_bearing(tracks_st) and tracks_last_yaw(tracks_st) == 1.0)
+ expect_eq(tracks_read_count(), 1)
+ expect(tracks_has_bearing() and tracks_last_yaw() == 1.0)
}
- test "a reader without the skill cannot name the animal" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- trackstest_st.skill = 0
- tracks_leave(tracks_st, 1, 0.0, 0.0, 0.0, 3, false)
- tracks_read(base_st, tracks_st, tracks_slot(tracks_st, 1, 0))
- expect(not reads(base_st, tracks_st)[0].named)
+ test "a reader without the skill cannot name the animal" {
+ fresh()
+ skill = 0
+ tracks_leave(1, 0.0, 0.0, 0.0, 3, false)
+ tracks_read(tracks_slot(1, 0))
+ expect(not reads()[0].named)
}
- test "a sign that is not a print is told, and counts when it is the first" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_note(base_st, tracks_st, 4, TRACK_SCAT, false, -1.0, true)
- tracks_note(base_st, tracks_st, 4, TRACK_SCAT, false, -1.0, false)
- let rs = reads(base_st, tracks_st)
+ test "a sign that is not a print is told, and counts when it is the first" {
+ fresh()
+ tracks_note(4, TRACK_SCAT, false, -1.0, true)
+ tracks_note(4, TRACK_SCAT, false, -1.0, false)
+ let rs = reads()
expect_eq(len(rs), 2)
expect_eq(rs[0].source, TRACK_SCAT)
- expect_eq(tracks_read_count(tracks_st), 1)
+ expect_eq(tracks_read_count(), 1)
}
- test "a written-over slot is a new, unread print" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_leave(tracks_st, 0, 0.0, 0.0, 0.0, 1, false)
- tracks_read(base_st, tracks_st, tracks_slot(tracks_st, 0, 0))
- for n in 0 .. 4 { tracks_leave(tracks_st, 0, 0.0, 0.0, 0.0, 2, false) }
- expect(not tracks_was_read(tracks_st, tracks_slot(tracks_st, 0, 0)))
+ test "a written-over slot is a new, unread print" {
+ fresh()
+ tracks_leave(0, 0.0, 0.0, 0.0, 1, false)
+ tracks_read(tracks_slot(0, 0))
+ for n in 0 .. 4 { tracks_leave(0, 0.0, 0.0, 0.0, 2, false) }
+ expect(not tracks_was_read(tracks_slot(0, 0)))
}
- test "clearing the ground forgets the prints, not the reader" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_leave(tracks_st, 1, 0.0, 0.0, 0.0, 1, false)
- tracks_read(base_st, tracks_st, tracks_slot(tracks_st, 1, 0))
- tracks_clear(tracks_st)
- expect_eq(tracks_count(tracks_st, 1), 0)
- expect_eq(tracks_read_count(tracks_st), 1)
+ test "clearing the ground forgets the prints, not the reader" {
+ fresh()
+ tracks_leave(1, 0.0, 0.0, 0.0, 1, false)
+ tracks_read(tracks_slot(1, 0))
+ tracks_clear()
+ expect_eq(tracks_count(1), 0)
+ expect_eq(tracks_read_count(), 1)
}
- test "a save section reads back the same reader" (base_st: mut BaseState, tracks_st: mut TracksState, trackstest_st: mut TrackstestState) {
- fresh(base_st, tracks_st, trackstest_st)
- tracks_note(base_st, tracks_st, 1, TRACK_SIGN, true, 0.0, true)
- tracks_note_bearing(tracks_st, 2.5)
+ test "a save section reads back the same reader" {
+ fresh()
+ tracks_note(1, TRACK_SIGN, true, 0.0, true)
+ tracks_note_bearing(2.5)
let root = save_tree()
- save_section(root, "tracks", 1, tracks_save(tracks_st))
+ save_section(root, "tracks", 1, tracks_save())
let text = save_encode(root)
- tracks_reset(base_st, tracks_st)
+ tracks_reset()
let n = load_section(save_decode(text), "tracks")
- tracks_load(base_st, tracks_st, n.data, n.version)
- expect_eq(tracks_read_count(tracks_st), 1)
- expect(tracks_has_bearing(tracks_st) and tracks_last_yaw(tracks_st) == 2.5)
+ tracks_load(n.data, n.version)
+ expect_eq(tracks_read_count(), 1)
+ expect(tracks_has_bearing() and tracks_last_yaw() == 2.5)
}
}
diff --git a/packages/ludic.ui/act.ludic b/packages/ludic.ui/act.ludic
index 38276e58..65aae0b3 100644
--- a/packages/ludic.ui/act.ludic
+++ b/packages/ludic.ui/act.ludic
@@ -51,20 +51,20 @@ function act_keyword(r: UiRd, word: string) -> bool {
}
return true
}
-function act_run(ui_st: mut UiState, a: UiAct, e: UiEnv) -> void {
+function act_run(a: UiAct, e: UiEnv) -> void {
for i in 0 .. len(a.kinds) {
- if a.kinds[i] == A_SET { env_set(ui_st, e, a.names[i], ui_eval(ui_st, a.exprs[i], e)) }
- if a.kinds[i] == A_CALL { ui_eval(ui_st, a.exprs[i], e) }
- if a.kinds[i] == A_EMIT { act_emit(ui_st, a.names[i], e) }
+ if a.kinds[i] == A_SET { env_set(e, a.names[i], ui_eval(a.exprs[i], e)) }
+ if a.kinds[i] == A_CALL { ui_eval(a.exprs[i], e) }
+ if a.kinds[i] == A_EMIT { act_emit(a.names[i], e) }
}
}
-function act_emit(ui_st: mut UiState, name: string, e: UiEnv) -> void {
+function act_emit(name: string, e: UiEnv) -> void {
var at = e
while at != null {
if at.on_names != null {
for i in 0 .. len(at.on_names) {
if at.on_names[i] == name {
- act_run(ui_st, at.on_acts[i], at.on_envs[i])
+ act_run(at.on_acts[i], at.on_envs[i])
return
}
}
diff --git a/packages/ludic.ui/anim.ludic b/packages/ludic.ui/anim.ludic
index 22932f41..99a448b0 100644
--- a/packages/ludic.ui/anim.ludic
+++ b/packages/ludic.ui/anim.ludic
@@ -6,13 +6,17 @@ export property UiKeyframes {
stops: []float = null
rules: []UiRule = null
}
+var an_frames: []UiKeyframes = new []UiKeyframes
+var an_keys: []string = new []string
+var an_start: []float = new []float
+var an_frame: int = 0
# the renderer's clock in seconds, or the frames shown at sixty a second (a test, a still picture)
-function an_now(ui_st: UiState) -> float {
- if ui_st.ui_be != null and ui_st.ui_be.now != null { return ui_st.ui_be.now() }
- return float(ui_st.an_frame) / 60.0
+function an_now() -> float {
+ if ui_be != null and ui_be.now != null { return ui_be.now() }
+ return float(an_frame) / 60.0
}
# @keyframes NAME { from {...} 50% {...} to {...} }
-function an_read(ui_st: mut UiState, name: string, body: string, err: []string) -> void {
+function an_read(name: string, body: string, err: []string) -> void {
let k = new UiKeyframes
k.name = tpl_words(name)
k.stops = new []float
@@ -30,40 +34,40 @@ function an_read(ui_st: mut UiState, name: string, body: string, err: []string)
push(k.stops, at)
push(k.rules, decls)
}
- push(ui_st.an_frames, k)
+ push(an_frames, k)
}
-function an_find(ui_st: UiState, name: string) -> UiKeyframes {
- for i in 0 .. len(ui_st.an_frames) {
- if ui_st.an_frames[i].name == name { return ui_st.an_frames[i] }
+function an_find(name: string) -> UiKeyframes {
+ for i in 0 .. len(an_frames) {
+ if an_frames[i].name == name { return an_frames[i] }
}
return null
}
# when this element's animation began: the first frame it was seen with it
-function an_began(ui_st: mut UiState, key: string) -> float {
- for i in 0 .. len(ui_st.an_keys) {
- if ui_st.an_keys[i] == key { return ui_st.an_start[i] }
+function an_began(key: string) -> float {
+ for i in 0 .. len(an_keys) {
+ if an_keys[i] == key { return an_start[i] }
}
- push(ui_st.an_keys, key)
- push(ui_st.an_start, an_now(ui_st))
- return an_now(ui_st)
+ push(an_keys, key)
+ push(an_start, an_now())
+ return an_now()
}
# the element's animation at this moment: each property between the stops around it
-function an_apply(ui_st: mut UiState, n: UiNode) -> void {
+function an_apply(n: UiNode) -> void {
if n.anim == "" or n.anim_dur <= 0.0 { return }
- let k = an_find(ui_st, n.anim)
+ let k = an_find(n.anim)
if k == null {
- ui_err(ui_st, `animation {n.anim}: there is no @keyframes of that name`)
+ ui_err(`animation {n.anim}: there is no @keyframes of that name`)
return
}
- var t = (an_now(ui_st) - an_began(ui_st, `{n.key}@{n.anim}`)) / n.anim_dur
+ var t = (an_now() - an_began(`{n.key}@{n.anim}`)) / n.anim_dur
if n.anim_loop {
let lap = int(t)
t = t - float(lap)
if n.anim_alt and lap % 2 == 1 { t = 1.0 - t }
} else { t = Math.clamp(t, 0.0, 1.0) }
- an_at(ui_st, n, k, t)
+ an_at(n, k, t)
}
-function an_at(ui_st: mut UiState, n: UiNode, k: UiKeyframes, t: float) -> void {
+function an_at(n: UiNode, k: UiKeyframes, t: float) -> void {
var lo = 0
var hi = len(k.stops) - 1
for i in 0 .. len(k.stops) {
@@ -75,11 +79,11 @@ function an_at(ui_st: mut UiState, n: UiNode, k: UiKeyframes, t: float) -> void
let a = k.rules[lo]
let b = k.rules[hi]
for i in 0 .. len(a.keys) {
- let va = el_num(vl_text(ui_eval(ui_st, a.vals[i], n.env)))
+ let va = el_num(vl_text(ui_eval(a.vals[i], n.env)))
var vb = va
for j in 0 .. len(b.keys) {
- if b.keys[j] == a.keys[i] { vb = el_num(vl_text(ui_eval(ui_st, b.vals[j], n.env))) }
+ if b.keys[j] == a.keys[i] { vb = el_num(vl_text(ui_eval(b.vals[j], n.env))) }
}
- el_apply(ui_st, n, a.keys[i], Value.float(va + (vb - va) * f))
+ el_apply(n, a.keys[i], Value.float(va + (vb - va) * f))
}
}
diff --git a/packages/ludic.ui/attr.ludic b/packages/ludic.ui/attr.ludic
index c7ca60cd..62510a58 100644
--- a/packages/ludic.ui/attr.ludic
+++ b/packages/ludic.ui/attr.ludic
@@ -3,21 +3,21 @@
const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-shrink=shrink flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign "
const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow cursor transition animation letter-spacing text-decoration "
# a stylesheet's property: one that means nothing is said
-function el_attr(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
- if el_apply(ui_st, n, key, v) { return }
- if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(ui_st, `{key}: this renderer does not draw it`) } else { ui_err(ui_st, `{key}: there is no such property`) }
+function el_attr(n: UiNode, key: string, v: Val) -> void {
+ if el_apply(n, key, v) { return }
+ if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(`{key}: this renderer does not draw it`) } else { ui_err(`{key}: there is no such property`) }
}
# an element's attribute: a property when it names one, otherwise only there for [selectors]
-function el_apply(ui_st: mut UiState, n: UiNode, key: string, v0: Val) -> bool {
+function el_apply(n: UiNode, key: string, v0: Val) -> bool {
let k = at_alias(key)
- ui_st.el_cur = n
- ui_st.el_key = k
+ el_cur = n
+ el_key = k
if len(k) > 2 and k[0..2] == "--" {
cs_var_set(n, k, vl_text(v0))
return true
}
- let v = cs_vars_in(ui_st, n, v0)
- return el_size(ui_st, n, k, v) or el_box(ui_st, n, k, v) or el_flex(n, k, v) or el_look(ui_st, n, k, v) or el_place_prop(ui_st, n, k, v) or el_edge(ui_st, n, k, v) or el_more(ui_st, n, k, v)
+ let v = cs_vars_in(n, v0)
+ return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v) or el_place_prop(n, k, v) or el_edge(n, k, v) or el_more(n, k, v)
}
function at_alias(key: string) -> string {
let pat = ` {key}=`
@@ -31,8 +31,8 @@ function at_alias(key: string) -> string {
}
return key
}
-function el_size(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
- if key == "w" or key == "h" { el_dim(ui_st, n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(ui_st, v) } else if key == "max-w" { n.max_w = el_len(ui_st, v) } else if key == "min-h" { n.min_h = el_len(ui_st, v) } else if key == "max-h" { n.max_h = el_len(ui_st, v) } else if key == "gap" { n.gap = el_len(ui_st, v) } else { return false }
+function el_size(n: UiNode, key: string, v: Val) -> bool {
+ if key == "w" or key == "h" { el_dim(n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false }
return true
}
function el_flex(n: UiNode, key: string, v: Val) -> bool {
@@ -43,33 +43,33 @@ function el_flex(n: UiNode, key: string, v: Val) -> bool {
} else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
return true
}
-function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
+function el_look(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
- if key == "size" { n.size = el_len(ui_st, v) } else if key == "color" {
- n.fg = ui_color(ui_st, s)
- n.fg_a = ui_st.cl_alpha
+ if key == "size" { n.size = el_len(v) } else if key == "color" {
+ n.fg = ui_color(s)
+ n.fg_a = cl_alpha
} else if key == "bg" {
- if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" {
+ if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(s) } else if len(s) > 4 and s[0..4] == "url(" { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" {
n.bg = 0
n.bg_a = 0.0
} else {
- n.bg = ui_color(ui_st, s)
- n.bg_a = ui_st.cl_alpha
+ n.bg = ui_color(s)
+ n.bg_a = cl_alpha
}
} else if key == "alpha" { n.alpha = el_num(s) } else if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" } else if key == "border-color" {
- n.border_c = ui_color(ui_st, s)
- n.border_a = ui_st.cl_alpha
- } else if key == "border-width" { el_sides_border(n, el_len(ui_st, v)) } else if key == "border" { el_border(ui_st, n, s) } else if key == "border-top" or key == "border-right" or key == "border-bottom" or key == "border-left" { el_border_side(ui_st, n, key, s) } else { return false }
+ n.border_c = ui_color(s)
+ n.border_a = cl_alpha
+ } else if key == "border-width" { el_sides_border(n, el_len(v)) } else if key == "border" { el_border(n, s) } else if key == "border-top" or key == "border-right" or key == "border-bottom" or key == "border-left" { el_border_side(n, key, s) } else { return false }
return true
}
# border: 1 #ffcc00, or 1px solid accent
-function el_border(ui_st: mut UiState, n: UiNode, s: string) -> void {
+function el_border(n: UiNode, s: string) -> void {
let parts = lss_split(s, 32)
for i in 0 .. len(parts) {
let p = parts[i]
- if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { el_sides_border(n, el_unit(ui_st, p)) } else if p == "none" { el_sides_border(n, 0.0) } else if len(p) > 0 and p != "solid" {
- n.border_c = ui_color(ui_st, p)
- n.border_a = ui_st.cl_alpha
+ if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { el_sides_border(n, el_unit(p)) } else if p == "none" { el_sides_border(n, 0.0) } else if len(p) > 0 and p != "solid" {
+ n.border_c = ui_color(p)
+ n.border_a = cl_alpha
}
}
if n.border_c == UI_NONE { n.border_c = n.rfg }
diff --git a/packages/ludic.ui/attr_border.ludic b/packages/ludic.ui/attr_border.ludic
index 462bde71..dec8b96c 100644
--- a/packages/ludic.ui/attr_border.ludic
+++ b/packages/ludic.ui/attr_border.ludic
@@ -8,12 +8,12 @@ function el_sides_border(n: UiNode, w: float) -> void {
n.bl = w
}
# border-bottom: 3px solid var(--accent) - one side's width, and the colour for the border
-function el_border_side(ui_st: mut UiState, n: UiNode, key: string, s: string) -> void {
+function el_border_side(n: UiNode, key: string, s: string) -> void {
let keep_t = n.bt
let keep_r = n.br
let keep_b = n.bb
let keep_l = n.bl
- el_border(ui_st, n, s)
+ el_border(n, s)
let w = n.bt
n.bt = keep_t
n.br = keep_r
diff --git a/packages/ludic.ui/attr_box.ludic b/packages/ludic.ui/attr_box.ludic
index 6b072ff5..06dca9a3 100644
--- a/packages/ludic.ui/attr_box.ludic
+++ b/packages/ludic.ui/attr_box.ludic
@@ -1,46 +1,50 @@
# attr_box.ludic - padding and margin (one to four lengths, as CSS writes them, or one side by
# name), lengths with px or %, and the words that place things
-function el_box(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
+function el_box(n: UiNode, key: string, v: Val) -> bool {
if key == "pad" {
- el_sides(ui_st, v)
- n.pt = ui_st.el_s0
- n.pr = ui_st.el_s1
- n.pb = ui_st.el_s2
- n.pl = ui_st.el_s3
+ el_sides(v)
+ n.pt = el_s0
+ n.pr = el_s1
+ n.pb = el_s2
+ n.pl = el_s3
} else if key == "margin" {
- el_sides(ui_st, v)
- n.mt = ui_st.el_s0
- n.mr = ui_st.el_s1
- n.mb = ui_st.el_s2
- n.ml = ui_st.el_s3
- } else if key == "padding-top" { n.pt = el_len(ui_st, v) } else if key == "padding-right" { n.pr = el_len(ui_st, v) } else if key == "padding-bottom" { n.pb = el_len(ui_st, v) } else if key == "padding-left" { n.pl = el_len(ui_st, v) } else if key == "margin-top" { n.mt = el_len(ui_st, v) } else if key == "margin-right" { n.mr = el_len(ui_st, v) } else if key == "margin-bottom" { n.mb = el_len(ui_st, v) } else if key == "margin-left" { n.ml = el_len(ui_st, v) } else { return false }
+ el_sides(v)
+ n.mt = el_s0
+ n.mr = el_s1
+ n.mb = el_s2
+ n.ml = el_s3
+ } else if key == "padding-top" { n.pt = el_len(v) } else if key == "padding-right" { n.pr = el_len(v) } else if key == "padding-bottom" { n.pb = el_len(v) } else if key == "padding-left" { n.pl = el_len(v) } else if key == "margin-top" { n.mt = el_len(v) } else if key == "margin-right" { n.mr = el_len(v) } else if key == "margin-bottom" { n.mb = el_len(v) } else if key == "margin-left" { n.ml = el_len(v) } else { return false }
return true
}
+var el_s0: float = 0.0
+var el_s1: float = 0.0
+var el_s2: float = 0.0
+var el_s3: float = 0.0
# top right bottom left out of 1 to 4 lengths
-function el_sides(ui_st: mut UiState, v: Val) -> void {
+function el_sides(v: Val) -> void {
let parts = new []float
- if vl_num(v) { push(parts, el_px(ui_st, Value.as_float(v))) } else {
+ if vl_num(v) { push(parts, el_px(Value.as_float(v))) } else {
let words = el_words(vl_text(v))
for i in 0 .. len(words) {
- if len(words[i]) > 0 { push(parts, el_word_len(ui_st, words[i])) }
+ if len(words[i]) > 0 { push(parts, el_word_len(words[i])) }
}
}
while len(parts) < 4 {
if len(parts) == 0 { push(parts, 0.0) } else if len(parts) == 1 { push(parts, parts[0]) } else if len(parts) == 2 { push(parts, parts[0]) } else { push(parts, parts[1]) }
}
- ui_st.el_s0 = parts[0]
- ui_st.el_s1 = parts[1]
- ui_st.el_s2 = parts[2]
- ui_st.el_s3 = parts[3]
+ el_s0 = parts[0]
+ el_s1 = parts[1]
+ el_s2 = parts[2]
+ el_s3 = parts[3]
}
# a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's)
-function el_len(ui_st: mut UiState, v: Val) -> float {
- if vl_num(v) { return el_px(ui_st, Value.as_float(v)) }
+function el_len(v: Val) -> float {
+ if vl_num(v) { return el_px(Value.as_float(v)) }
let s = vl_text(v)
if s == "fill" { return UI_FILL }
- if ca_is(s) { return el_calc_len(ui_st, s) }
+ if ca_is(s) { return el_calc_len(s) }
if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }
- return el_unit(ui_st, s)
+ return el_unit(s)
}
function el_pct(v: Val) -> float {
let s = vl_text(v)
diff --git a/packages/ludic.ui/attr_edge.ludic b/packages/ludic.ui/attr_edge.ludic
index 97adbd08..d4a324fb 100644
--- a/packages/ludic.ui/attr_edge.ludic
+++ b/packages/ludic.ui/attr_edge.ludic
@@ -1,14 +1,14 @@
# attr_edge.ludic - border-radius, outline (and its width, colour and offset), scrollbar-color
-function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
+function el_edge(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
- if key == "border-radius" { n.radius = el_len(ui_st, v) } else if key == "outline" { el_outline(ui_st, n, s) } else if key == "outline-width" { n.outline_w = el_len(ui_st, v) } else if key == "outline-offset" { n.outline_off = el_len(ui_st, v) } else if key == "outline-color" {
- n.outline_c = ui_color(ui_st, s)
- n.outline_a = ui_st.cl_alpha
+ if key == "border-radius" { n.radius = el_len(v) } else if key == "outline" { el_outline(n, s) } else if key == "outline-width" { n.outline_w = el_len(v) } else if key == "outline-offset" { n.outline_off = el_len(v) } else if key == "outline-color" {
+ n.outline_c = ui_color(s)
+ n.outline_a = cl_alpha
} else if key == "scrollbar-color" {
let parts = lss_split(s, 32)
- if len(parts) > 0 { n.bar_c = ui_color(ui_st, parts[0]) }
- if len(parts) > 1 { n.bar_track = ui_color(ui_st, parts[1]) }
- } else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(ui_st, n, s) } else if key == "box-shadow" { el_shadow(ui_st, n, s) } else if key == "animation" { el_animation(n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
+ if len(parts) > 0 { n.bar_c = ui_color(parts[0]) }
+ if len(parts) > 1 { n.bar_track = ui_color(parts[1]) }
+ } else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(n, s) } else if key == "box-shadow" { el_shadow(n, s) } else if key == "animation" { el_animation(n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
return true
}
# url(path) or url("path") -> path
@@ -20,17 +20,17 @@ function el_url(s: string) -> string {
return t
}
# border-image: url(chip.png) 14 / 12px - the source's corner size, then how wide they are drawn
-function el_border_image(ui_st: UiState, n: UiNode, s: string) -> void {
+function el_border_image(n: UiNode, s: string) -> void {
var close = 0
while close < len(s) and s[close] != 41 { close += 1 }
n.bi_src = el_url(s[0..Math.min(close + 1, len(s))])
let rest = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
n.bi_slice = el_num(tpl_words(rest[0]))
- n.bi_width = el_px(ui_st, n.bi_slice)
- if len(rest) > 1 { n.bi_width = el_unit(ui_st, tpl_words(rest[1])) }
+ n.bi_width = el_px(n.bi_slice)
+ if len(rest) > 1 { n.bi_width = el_unit(tpl_words(rest[1])) }
}
# box-shadow: 6px 10px 0 rgba(0, 0, 0, 0.45) - x, y, a blur this renderer draws sharp, the colour
-function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
+function el_shadow(n: UiNode, s: string) -> void {
if tpl_words(s) == "none" {
n.sh_c = -2
return
@@ -42,24 +42,24 @@ function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
let p = parts[i]
if len(p) == 0 { continue }
if col == "" and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) {
- if nums == 0 { n.sh_x = el_unit(ui_st, p) }
- if nums == 1 { n.sh_y = el_unit(ui_st, p) }
+ if nums == 0 { n.sh_x = el_unit(p) }
+ if nums == 1 { n.sh_y = el_unit(p) }
nums += 1
} else if col == "" { col = p } else { col = col + " " + p }
}
if col == "" { col = "rgba(0,0,0,0.5)" }
- n.sh_c = ui_color(ui_st, col)
- n.sh_a = ui_st.cl_alpha
+ n.sh_c = ui_color(col)
+ n.sh_a = cl_alpha
}
# outline: 2px solid #ffcc00, or none
-function el_outline(ui_st: mut UiState, n: UiNode, s: string) -> void {
+function el_outline(n: UiNode, s: string) -> void {
let parts = lss_split(s, 32)
n.outline_w = 0.0
for i in 0 .. len(parts) {
let p = parts[i]
- if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.outline_w = el_unit(ui_st, p) } else if len(p) > 0 and p != "solid" and p != "none" {
- n.outline_c = ui_color(ui_st, p)
- n.outline_a = ui_st.cl_alpha
+ if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.outline_w = el_unit(p) } else if len(p) > 0 and p != "solid" and p != "none" {
+ n.outline_c = ui_color(p)
+ n.outline_a = cl_alpha
}
}
}
diff --git a/packages/ludic.ui/attr_fit.ludic b/packages/ludic.ui/attr_fit.ludic
index fa083941..cde3db3d 100644
--- a/packages/ludic.ui/attr_fit.ludic
+++ b/packages/ludic.ui/attr_fit.ludic
@@ -1,6 +1,6 @@
# attr_fit.ludic - aspect-ratio (16 / 9, or 1.5), object-fit (fill, contain, cover, none, scale-down)
# for pictures and natives, zoom (zoom.ludic), and a list split at its top-level commas
-function el_fit_props(ui_st: mut UiState, n: UiNode, key: string, s: string) -> bool {
+function el_fit_props(n: UiNode, key: string, s: string) -> bool {
if key == "aspect-ratio" {
let parts = lss_split(s, 47)
var r = el_num(tpl_words(parts[0]))
@@ -12,7 +12,7 @@ function el_fit_props(ui_st: mut UiState, n: UiNode, key: string, s: string) ->
var k = 0
if w == "contain" { k = 1 } else if w == "cover" { k = 2 } else if w == "none" { k = 3 } else if w == "scale-down" { k = 4 }
nx(n).fit_obj = k
- } else if key == "zoom" { el_zoom(ui_st, n, s) } else { return false }
+ } else if key == "zoom" { el_zoom(n, s) } else { return false }
return true
}
# "a, rgba(1, 2, 3, 0.5) 40%, b" -> its three parts
diff --git a/packages/ludic.ui/attr_inset.ludic b/packages/ludic.ui/attr_inset.ludic
index b33af5a1..7ae1430b 100644
--- a/packages/ludic.ui/attr_inset.ludic
+++ b/packages/ludic.ui/attr_inset.ludic
@@ -1,21 +1,21 @@
# attr_inset.ludic - top, right, bottom and left as lengths, percentages or calc() of both: the
# percentage is kept apart and taken of the containing block's width (left, right) or height (top,
# bottom) when the element is placed
-function el_inset(ui_st: mut UiState, n: UiNode, side: int, v: Val) -> float {
+function el_inset(n: UiNode, side: int, v: Val) -> float {
let s = vl_text(v)
var pct = 0.0
var px = UI_AUTO
if s != "auto" {
if ca_is(s) {
px = 0.0
- if ca_eval(ui_st, s) {
- pct = ui_st.ca_pct
- px = ui_st.ca_px
+ if ca_eval(s) {
+ pct = ca_pct
+ px = ca_px
}
} else if len(s) > 1 and s[len(s) - 1] == 37 {
pct = el_num(s)
px = 0.0
- } else { px = el_len(ui_st, v) }
+ } else { px = el_len(v) }
}
if pct != 0.0 or n.ex != null {
let x = nx(n)
diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic
index 426b78b0..dbd4d887 100644
--- a/packages/ludic.ui/attr_more.ludic
+++ b/packages/ludic.ui/attr_more.ludic
@@ -1,26 +1,26 @@
# attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex
# shorthand, flex-shrink, order, text-shadow, pointer-events, text-fit and line-height (text_fit.ludic)
-function el_more(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
+function el_more(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
- if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(ui_st, n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(ui_st, n, s) } else if key == "line-height" { el_line_h(ui_st, n, s) } else { return el_fit_props(ui_st, n, key, s) }
+ if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(n, s) } else if key == "line-height" { el_line_h(n, s) } else { return el_fit_props(n, key, s) }
return true
}
# width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none)
-function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void {
+function el_dim(n: UiNode, wide: bool, v: Val) -> void {
let s = vl_text(v)
var fixed = UI_AUTO
var pct = 0.0
var off = 0.0
if ca_is(s) {
- if ca_eval(ui_st, s) {
- pct = ui_st.ca_pct
- off = ui_st.ca_px
- if pct <= 0.0 { fixed = Math.max(ui_st.ca_px, 0.0) }
+ if ca_eval(s) {
+ pct = ca_pct
+ off = ca_px
+ if pct <= 0.0 { fixed = Math.max(ca_px, 0.0) }
}
} else if s == "fill" { fixed = UI_FILL } else if len(s) > 1 and s[len(s) - 1] == 37 {
pct = el_num(s)
if pct <= 0.0 { fixed = 0.0 }
- } else if s != "auto" and s != "fit" and s != "" { fixed = el_len(ui_st, v) }
+ } else if s != "auto" and s != "fit" and s != "" { fixed = el_len(v) }
if pct <= 0.0 { off = 0.0 }
if n.ex != null or off != 0.0 {
if wide { nx(n).w_off = off } else { nx(n).h_off = off }
@@ -34,10 +34,10 @@ function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void {
}
}
# a length out of calc(); a percentage has nothing to be a share of here
-function el_calc_len(ui_st: mut UiState, s: string) -> float {
- if not ca_eval(ui_st, s) { return 0.0 }
- if ui_st.ca_pct != 0.0 { ui_err(ui_st, `{ui_st.el_key}: {s} - a percentage in calc() is for a width or a height`) }
- return ui_st.ca_px
+function el_calc_len(s: string) -> float {
+ if not ca_eval(s) { return 0.0 }
+ if ca_pct != 0.0 { ui_err(`{el_key}: {s} - a percentage in calc() is for a width or a height`) }
+ return ca_px
}
# flex: 1 | flex: 1 0 | flex: 2 1 auto | flex: none - the grow, then the shrink
function el_flex_short(n: UiNode, s: string) -> void {
@@ -67,7 +67,7 @@ function el_words(s: string) -> []string {
return out
}
# text-shadow: 1px 2px 3px rgba(0, 0, 0, 0.6) - x, y, a blur drawn sharp, the colour; or none
-function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
+function el_text_shadow(n: UiNode, s: string) -> void {
let x = nx(n)
x.tsh_set = true
x.tsh_c = -2
@@ -78,17 +78,17 @@ function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
for i in 0 .. len(parts) {
let p = parts[i]
if col == "" and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) {
- if nums == 0 { x.tsh_x = el_unit(ui_st, p) }
- if nums == 1 { x.tsh_y = el_unit(ui_st, p) }
+ if nums == 0 { x.tsh_x = el_unit(p) }
+ if nums == 1 { x.tsh_y = el_unit(p) }
nums += 1
} else if col == "" { col = p } else { col = col + " " + p }
}
if col == "" { col = "rgba(0,0,0,0.6)" }
- x.tsh_c = ui_color(ui_st, col)
- x.tsh_a = ui_st.cl_alpha
+ x.tsh_c = ui_color(col)
+ x.tsh_a = cl_alpha
}
# one word of a list of lengths (padding: calc(1em / 4) 2px)
-function el_word_len(ui_st: mut UiState, w: string) -> float {
- if ca_is(w) { return el_calc_len(ui_st, w) }
- return el_unit(ui_st, w)
+function el_word_len(w: string) -> float {
+ if ca_is(w) { return el_calc_len(w) }
+ return el_unit(w)
}
diff --git a/packages/ludic.ui/attr_place.ludic b/packages/ludic.ui/attr_place.ludic
index 31263c8e..16ee6d40 100644
--- a/packages/ludic.ui/attr_place.ludic
+++ b/packages/ludic.ui/attr_place.ludic
@@ -1,17 +1,17 @@
# attr_place.ludic - position and its insets, z-index, overflow, white-space and text-overflow
-function el_place_prop(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
+function el_place_prop(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "position" {
n.pos = 0
if s == "relative" { n.pos = 1 }
if s == "absolute" { n.pos = 2 }
if s == "fixed" { n.pos = 3 }
- } else if key == "top" { n.top = el_inset(ui_st, n, 0, v) } else if key == "right" { n.right = el_inset(ui_st, n, 1, v) } else if key == "bottom" { n.bottom = el_inset(ui_st, n, 2, v) } else if key == "left" { n.left = el_inset(ui_st, n, 3, v) } else if key == "inset" {
- el_sides(ui_st, v)
- n.top = ui_st.el_s0
- n.right = ui_st.el_s1
- n.bottom = ui_st.el_s2
- n.left = ui_st.el_s3
+ } else if key == "top" { n.top = el_inset(n, 0, v) } else if key == "right" { n.right = el_inset(n, 1, v) } else if key == "bottom" { n.bottom = el_inset(n, 2, v) } else if key == "left" { n.left = el_inset(n, 3, v) } else if key == "inset" {
+ el_sides(v)
+ n.top = el_s0
+ n.right = el_s1
+ n.bottom = el_s2
+ n.left = el_s3
} else if key == "z-index" { n.z = int(el_num(s)) } else if key == "overflow" or key == "overflow-y" {
if (s == "auto" or s == "scroll" or s == "hidden") and n.kind == UI_BOX { n.kind = UI_SCROLL }
} else if key == "white-space" { n.nowrap = s == "nowrap" or s == "pre" } else if key == "text-overflow" { n.ellipsis = s == "ellipsis" } else { return false }
diff --git a/packages/ludic.ui/backend.ludic b/packages/ludic.ui/backend.ludic
index 21b2f3e2..0ae11962 100644
--- a/packages/ludic.ui/backend.ludic
+++ b/packages/ludic.ui/backend.ludic
@@ -20,165 +20,30 @@ export property UiBackend {
now: fn() -> float = null # seconds, for animation (else frames at 60 a second)
input: bool = true # false: take no input from Input.* (a test drives it)
}
-# a renderer a program installs by importing it (render3d.ludic): the first one is the backend
-property UiRenderer {
- key: string = ""
- make: fn() -> UiBackend = null
-}
-registry UiRenderers of UiRenderer as UIR
-function ui_default_backend() -> UiBackend {
- if len(UiRenderers) == 0 { return null }
- return UiRenderers[0].make()
-}
-export state UiState {
- an_frames: []UiKeyframes = new []UiKeyframes
- an_keys: []string = new []string
- an_start: []float = new []float
- an_frame: int = 0
- el_s0: float = 0.0
- el_s1: float = 0.0
- el_s2: float = 0.0
- el_s3: float = 0.0
- ui_be: UiBackend = ui_default_backend()
- tr_no: bool = false # inside translate="no" while a frame is built
- ca_s: string = ""
- ca_i: int = 0
- ca_bad: string = ""
- ca_px: float = 0.0 # the result: its lengths, its percentage, and whether it had units
- ca_pct: float = 0.0
- ca_num: float = 0.0 # a plain number's own value, and whether it was one
- ca_bare: bool = false
- cs_pass: int = 0
- ua_sh: UiSheet = null
- uc_list: []UiClass = null # filled by the compiler's registrations, before the program's globals
- cl_alpha: float = 1.0
- ct_capture: string = ""
- ct_capture_at: int = -1 # the frame listening began, whose own key started it
- ct_capture_seen: int = -1 # the last frame that began listening, for ui_capturing
- ct_num_key: string = ""
- ct_num_text: string = ""
- dv_on: bool = false
- dv_frames: int = 0
- dv_dumped: bool = false
- fr_ga: float = 1.0 # the opacity of everything around this element, which it multiplies
- gr_a: float = 1.0
- fr_clips: []float = new []float
- ui_view: UiView = null
- ui_errs: []string = new []string
- ui_at: string = ""
- fc_key: string = ""
- fc_visible: bool = false # focus came from the keyboard: :focus-visible, and the ring
- fc_list: []UiNode = new []UiNode
- fb_roots: []UiNode = new []UiNode
- hd_key: string = ""
- hd_t0: float = 0.0
- hd_last: float = 0.0
- in_now: UiInput = new UiInput
- in_was_down: bool = false
- in_last: UiInput = new UiInput
- in_given: bool = false
- rp_next: []float = [-1.0, -1.0, -1.0, -1.0]
- rp_a: bool = false
- rp_b: bool = false
- rp_act: bool = false
- in_act_down: bool = false # A or Enter went down this frame, or came up
- in_act_up: bool = false
- in_list: []UiInst = new []UiInst
- in_build: int = 0
- ly_fg_a: float = 1.0
- ly_lead: float = 0.0
- ly_extra: float = 0.0
- lib_files: []UiFile = new []UiFile
- lib_screen_names: []string = new []string
- lib_screens: []UiTpl = new []UiTpl
- lc_keys: []string = new []string
- lc_mounts: []UiAct = new []UiAct
- lc_unmounts: []UiAct = new []UiAct
- lc_envs: []UiEnv = new []UiEnv
- lc_prev_keys: []string = new []string
- lc_prev_unmounts: []UiAct = new []UiAct
- lc_prev_envs: []UiEnv = new []UiEnv
- nt_list: []UiNative = new []UiNative
- nt_q_nodes: []UiNode = new []UiNode
- nt_q_names: []string = new []string
- nt_q_vals: []Val = new []Val
- fr_hover: []string = new []string
- fr_press_key: string = ""
- fr_down_now: bool = false
- fr_key_press: string = "" # the control Enter or the pad's A is holding down
- pe_key: string = "" # the element the pointer is captured by
- pe_btn: int = 0
- pop_list: []UiNode = new []UiNode
- pp_bx: float = 0.0
- pp_by: float = 0.0
- pp_bw: float = 0.0
- pp_bh: float = 0.0
- r3u_user: float = 1.0
- r3u_tex_paths: []string = new []string
- r3u_tex_ids: []int = new []int
- r3u_tex_sizes: []int = new []int
- r3u_tex_seen: []float = new []float
- r3u_atlas: []string = new []string
- r3u_atlas_tex: []int = new []int
- r3u_atlas_cols: []int = new []int
- r3u_atlas_rows: []int = new []int
- r3u_atlas_names: [][]string = new [][]string
- sc_keys: []string = new []string
- sc_offs: []float = new []float
- sc_drag: string = ""
- sc_scope: UiNode = null # the popover that has the pointer this frame, if one is up
- sb_x: float = 0.0
- sb_w: float = 0.0
- sb_ty: float = 0.0
- sb_th: float = 0.0
- sb_bar: UiNode = null
- sc_grab: float = 0.0 # how far below the thumb's top the pointer holds it
- sc_want_keys: []string = new []string
- sc_want_px: []float = new []float
- st_keys: []string = new []string
- st_vals: []Val = new []Val
- tf_size: float = 0.0 # the size tf_fit drew at
- tt_key: string = ""
- tt_since: float = 0.0
- tt_text: string = ""
- tt_x: float = 0.0
- tt_y: float = 0.0
- tt_by_keys: bool = false # the focus moved by the keyboard since the pointer last moved
- tt_focus_seen: string = ""
- tp_nl: []int = new []int
- tr_keys: []string = new []string
- tr_from: []float = new []float
- tr_to: []float = new []float
- tr_at: []float = new []float
- el_cur: UiNode = null
- el_key: string = ""
- ly_vp_x: float = 0.0
- ly_vp_y: float = 0.0
- ly_vp_w: float = 0.0
- ly_vp_h: float = 0.0
-}
-export function ui_backend(ui_st: mut UiState, b: UiBackend) -> void { ui_st.ui_be = b }
-function ui_px(ui_st: UiState, v: float) -> float {
- if ui_st.ui_be != null and ui_st.ui_be.scale != null { return v * ui_st.ui_be.scale() }
+var ui_be: UiBackend = null
+export function ui_backend(b: UiBackend) -> void { ui_be = b }
+function ui_px(v: float) -> float {
+ if ui_be != null and ui_be.scale != null { return v * ui_be.scale() }
return v
}
-function ui_tr(ui_st: UiState, s: string) -> string {
- if ui_st.tr_no { return s }
- if ui_st.ui_be != null and ui_st.ui_be.translate != null { return ui_st.ui_be.translate(s) }
+var tr_no: bool = false # inside translate="no" while a frame is built
+function ui_tr(s: string) -> string {
+ if tr_no { return s }
+ if ui_be != null and ui_be.translate != null { return ui_be.translate(s) }
return s
}
-function ui_sound(ui_st: UiState, name: string) -> void {
- if ui_st.ui_be != null and ui_st.ui_be.sound != null { ui_st.ui_be.sound(name) }
+function ui_sound(name: string) -> void {
+ if ui_be != null and ui_be.sound != null { ui_be.sound(name) }
}
# the program's language: every text a template shows goes through `f` before it is measured
-export function ui_translator(ui_st: mut UiState, f: fn(string) -> string) -> void {
- if ui_st.ui_be != null { ui_st.ui_be.translate = f }
+export function ui_translator(f: fn(string) -> string) -> void {
+ if ui_be != null { ui_be.translate = f }
}
# the program's sounds: "click" when a control is used, or the control's own sound="..."
-export function ui_sounds(ui_st: mut UiState, f: fn(string) -> void) -> void {
- if ui_st.ui_be != null { ui_st.ui_be.sound = f }
+export function ui_sounds(f: fn(string) -> void) -> void {
+ if ui_be != null { ui_be.sound = f }
}
# the program's clock, in seconds, for animation (a game's own time, which a headless run counts)
-export function ui_clock(ui_st: mut UiState, f: fn() -> float) -> void {
- if ui_st.ui_be != null { ui_st.ui_be.now = f }
+export function ui_clock(f: fn() -> float) -> void {
+ if ui_be != null { ui_be.now = f }
}
diff --git a/packages/ludic.ui/build.ludic b/packages/ludic.ui/build.ludic
index 74b12d07..425de853 100644
--- a/packages/ludic.ui/build.ludic
+++ b/packages/ludic.ui/build.ludic
@@ -2,47 +2,47 @@
# are the template's own; a tag its file linked to a component is that one,
# given its attributes as props (over the defaults its own tag gives: ), its on-* as what `emit` runs, and its content as .
-function bd_kids(ui_st: mut UiState, kids: []UiTpl, e0: UiEnv, key: string, out: []UiNode) -> void {
+function bd_kids(kids: []UiTpl, e0: UiEnv, key: string, out: []UiNode) -> void {
var last_if = true
var e = e0
for i in 0 .. len(kids) {
let k = kids[i]
- ui_st.ui_at = k.where
+ ui_at = k.where
let ck = `{key}/{i}`
if k.tag == "state" or k.tag == "style" { continue }
if k.tag == "let" {
- e = bd_let(ui_st, k, e)
+ e = bd_let(k, e)
continue
}
- if k.tag == "fragment" or k.tag == "#text" or (k.tag == "template" and tpl_attr(k, "slot") == null) { bd_group(ui_st, k, e, ck, out) } else if k.tag == "provide" { bd_provide(ui_st, k, e, ck, out) } else if k.tag == "template" { continue } else if k.tag == "if" {
- last_if = bd_test(ui_st, k, e)
- if last_if { bd_kids(ui_st, k.kids, e, ck, out) }
+ if k.tag == "fragment" or k.tag == "#text" or (k.tag == "template" and tpl_attr(k, "slot") == null) { bd_group(k, e, ck, out) } else if k.tag == "provide" { bd_provide(k, e, ck, out) } else if k.tag == "template" { continue } else if k.tag == "if" {
+ last_if = bd_test(k, e)
+ if last_if { bd_kids(k.kids, e, ck, out) }
} else if k.tag == "else" {
- if not last_if { bd_kids(ui_st, k.kids, e, ck, out) }
+ if not last_if { bd_kids(k.kids, e, ck, out) }
last_if = true
- } else if k.tag == "each" { bd_each(ui_st, k, e, ck, out) } else if k.tag == "slot" { bd_slot(ui_st, k, e, ck, out) } else if not bd_comp(ui_st, k, e, ck, out) { push(out, bd_elem(ui_st, k, e, ck)) }
+ } else if k.tag == "each" { bd_each(k, e, ck, out) } else if k.tag == "slot" { bd_slot(k, e, ck, out) } else if not bd_comp(k, e, ck, out) { push(out, bd_elem(k, e, ck)) }
}
}
-function bd_test(ui_st: mut UiState, k: UiTpl, e: UiEnv) -> bool {
+function bd_test(k: UiTpl, e: UiEnv) -> bool {
let t = tpl_attr(k, "test")
if t == null {
- ui_err(ui_st, " needs a test=\"{...}\"")
+ ui_err(" needs a test=\"{...}\"")
return false
}
- return ui_eval_bool(ui_st, t, e)
+ return ui_eval_bool(t, e)
}
function bd_name(k: UiTpl, attr: string, dflt: string) -> string {
let x = tpl_attr(k, attr)
if x == null or x.op != X_LIT { return dflt }
return Value.as_str(x.v)
}
-function bd_each(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
+function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let src = tpl_attr(k, "in")
if src == null {
- ui_err(ui_st, " needs an in=\"{...}\"")
+ ui_err(" needs an in=\"{...}\"")
return
}
- let list = ui_eval(ui_st, src, e)
+ let list = ui_eval(src, e)
let as_name = bd_name(k, "as", "item")
let at_name = bd_name(k, "index", "index")
let by = tpl_attr(k, "key")
@@ -51,28 +51,28 @@ function bd_each(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
env_bind(le, as_name, Value.at(list, j))
env_bind(le, at_name, Value.int(j))
var id = string(j)
- if by != null { id = ui_eval_text(ui_st, by, le) }
- bd_kids(ui_st, k.kids, le, `{key}#{id}`, out)
+ if by != null { id = ui_eval_text(by, le) }
+ bd_kids(k.kids, le, `{key}#{id}`, out)
}
}
# is what the component's user put inside it; their
-function bd_slot(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
+function bd_slot(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
var at = e
while at != null and at.slot == null { at = at.parent }
if at == null { return }
let name = bd_name(k, "name", "")
if name == "" {
- bd_kids(ui_st, at.slot.kids, at.slot_env, key, out)
+ bd_kids(at.slot.kids, at.slot_env, key, out)
return
}
for i in 0 .. len(at.slot.kids) {
let t = at.slot.kids[i]
- if t.tag == "template" and bd_name(t, "slot", "") == name { bd_group(ui_st, t, at.slot_env, key, out) }
+ if t.tag == "template" and bd_name(t, "slot", "") == name { bd_group(t, at.slot_env, key, out) }
}
}
-function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
+function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
if k.cls != null {
- bd_class(ui_st, k, e, key, out)
+ bd_class(k, e, key, out)
return true
}
if k.comp == null { return false }
@@ -81,9 +81,9 @@ function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
ce.model = e.model
ce.ctx = e.ctx
for i in 0 .. len(c.keys) {
- if c.keys[i] != "name" and c.keys[i] != "export" { env_bind(ce, c.keys[i], ui_eval(ui_st, c.vals[i], ce)) }
+ if c.keys[i] != "name" and c.keys[i] != "export" { env_bind(ce, c.keys[i], ui_eval(c.vals[i], ce)) }
}
- for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
+ for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(k.vals[i], e)) }
ce.on_names = k.on_keys
ce.on_acts = k.on_acts
ce.on_envs = new []UiEnv
@@ -91,8 +91,8 @@ function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
ce.slot = k
ce.slot_env = e
ce.ctx = e.ctx
- ce.state = st_for(ui_st, key, c, ce)
- lc_note(ui_st, key, k.on_keys, k.on_acts, e)
- bd_kids(ui_st, c.kids, ce, key, out)
+ ce.state = st_for(key, c, ce)
+ lc_note(key, k.on_keys, k.on_acts, e)
+ bd_kids(c.kids, ce, key, out)
return true
}
diff --git a/packages/ludic.ui/build_class.ludic b/packages/ludic.ui/build_class.ludic
index 62e11c71..c9ddec9f 100644
--- a/packages/ludic.ui/build_class.ludic
+++ b/packages/ludic.ui/build_class.ludic
@@ -1,14 +1,14 @@
# build_class.ludic - a Ludic component's instance built: the parent's attributes are its props (id,
# class and style go on its root element instead, styled by the parent's sheet as well as its own),
# its model is its props, state and fields, and its template reads nothing else
-function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
+function bd_class(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let cls = k.cls
- uc_load(ui_st, cls)
- let it = in_get(ui_st, key, cls)
+ uc_load(cls)
+ let it = in_get(key, cls)
let props = Value.object()
for i in 0 .. len(k.keys) {
let a = k.keys[i]
- if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(ui_st, k.vals[i], e)) }
+ if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(k.vals[i], e)) }
}
cls.props(it.ptr, props)
let ce = env_new(null)
@@ -21,25 +21,25 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
ce.on_acts = k.on_acts
ce.on_envs = new []UiEnv
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
- lc_note(ui_st, key, k.on_keys, k.on_acts, e)
+ lc_note(key, k.on_keys, k.on_acts, e)
let at = len(out)
let root = new []UiTpl
push(root, cls.root)
- bd_kids(ui_st, root, ce, key, out)
- if len(out) > at { bd_pass_down(ui_st, out[at], k, e) }
+ bd_kids(root, ce, key, out)
+ if len(out) > at { bd_pass_down(out[at], k, e) }
}
# puts those on the card's own root element; when that
# root is itself a component, each user in turn adds theirs (the outermost's id wins)
-function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void {
+function bd_pass_down(n: UiNode, k: UiTpl, e: UiEnv) -> void {
let c = tpl_attr(k, "class")
if c != null {
- let parts = lss_split(ui_eval_text(ui_st, c, e), 32)
+ let parts = lss_split(ui_eval_text(c, e), 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
}
}
let id = tpl_attr(k, "id")
- if id != null { n.id = ui_eval_text(ui_st, id, e) }
+ if id != null { n.id = ui_eval_text(id, e) }
let up = new UiPass
up.sheet = k.sheet
up.style = k.style
diff --git a/packages/ludic.ui/build_more.ludic b/packages/ludic.ui/build_more.ludic
index 0e58a44c..99141e88 100644
--- a/packages/ludic.ui/build_more.ludic
+++ b/packages/ludic.ui/build_more.ludic
@@ -1,27 +1,27 @@
# build_more.ludic - names a value for the siblings after it, and
# gives it to everything inside, components included (React's context)
-function bd_let(ui_st: mut UiState, k: UiTpl, e: UiEnv) -> UiEnv {
+function bd_let(k: UiTpl, e: UiEnv) -> UiEnv {
let le = env_new(e)
- for i in 0 .. len(k.keys) { env_bind(le, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
+ for i in 0 .. len(k.keys) { env_bind(le, k.keys[i], ui_eval(k.vals[i], e)) }
return le
}
-function bd_provide(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
+function bd_provide(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let cx = env_new(e.ctx)
- for i in 0 .. len(k.keys) { env_bind(cx, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
+ for i in 0 .. len(k.keys) { env_bind(cx, k.keys[i], ui_eval(k.vals[i], e)) }
let pe = env_new(e)
pe.ctx = cx
- bd_kids(ui_st, k.kids, pe, key, out)
+ bd_kids(k.kids, pe, key, out)
}
# a fragment's (or a template's) content: its elements, or its words as a text of their own
-function bd_group(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
+function bd_group(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
if k.text == null {
- bd_kids(ui_st, k.kids, e, key, out)
+ bd_kids(k.kids, e, key, out)
return
}
let t = new UiNode
t.kind = UI_TEXT
t.key = key
- t.text = ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))
+ t.text = ui_tr(ui_eval_text(k.text, e))
t.env = e
t.children = new []UiNode
push(out, t)
diff --git a/packages/ludic.ui/calc.ludic b/packages/ludic.ui/calc.ludic
index 4f92f448..10849836 100644
--- a/packages/ludic.ui/calc.ludic
+++ b/packages/ludic.ui/calc.ludic
@@ -1,24 +1,31 @@
# calc.ludic - CSS's calc(), min(), max() and clamp(): + - * / and brackets over lengths (px, em, rem,
# vw, vh, a bare number is px), percentages and plain numbers. A width, height or inset keeps its
# percentage apart, to be taken of the room it is given; elsewhere a percentage has nothing to share.
+var ca_s: string = ""
+var ca_i: int = 0
+var ca_bad: string = ""
+var ca_px: float = 0.0 # the result: its lengths, its percentage, and whether it had units
+var ca_pct: float = 0.0
+var ca_num: float = 0.0 # a plain number's own value, and whether it was one
+var ca_bare: bool = false
function ca_is(s: string) -> bool { return ca_fn_at(s, 0) != "" }
# calc(...) worked out into ca_px and ca_pct; false (with the reason said) when it cannot be
-function ca_eval(ui_st: mut UiState, s: string) -> bool {
- ui_st.ca_s = s
- ui_st.ca_i = 0
- ui_st.ca_bad = ""
- let v = ca_factor(ui_st)
- ca_ws(ui_st)
- if ui_st.ca_bad == "" and ui_st.ca_i < len(ui_st.ca_s) { ui_st.ca_bad = "nothing after its closing )" }
- if ui_st.ca_bad != "" {
- ui_err(ui_st, `{s}: calc() expected {ui_st.ca_bad}`)
+function ca_eval(s: string) -> bool {
+ ca_s = s
+ ca_i = 0
+ ca_bad = ""
+ let v = ca_factor()
+ ca_ws()
+ if ca_bad == "" and ca_i < len(ca_s) { ca_bad = "nothing after its closing )" }
+ if ca_bad != "" {
+ ui_err(`{s}: calc() expected {ca_bad}`)
return false
}
- ui_st.ca_px = v.px
- ui_st.ca_pct = v.pct
- ui_st.ca_num = v.n
- ui_st.ca_bare = v.bare
- if v.bare { ui_st.ca_px = el_px(ui_st, v.n) }
+ ca_px = v.px
+ ca_pct = v.pct
+ ca_num = v.n
+ ca_bare = v.bare
+ if v.bare { ca_px = el_px(v.n) }
return true
}
property CaVal {
@@ -27,22 +34,22 @@ property CaVal {
n: float = 0.0
bare: bool = true # a plain number, which may scale a length
}
-function ca_ws(ui_st: mut UiState) -> void {
- while ui_st.ca_i < len(ui_st.ca_s) and (ui_st.ca_s[ui_st.ca_i] == 32 or ui_st.ca_s[ui_st.ca_i] == 9 or ui_st.ca_s[ui_st.ca_i] == 10) { ui_st.ca_i += 1 }
+function ca_ws() -> void {
+ while ca_i < len(ca_s) and (ca_s[ca_i] == 32 or ca_s[ca_i] == 9 or ca_s[ca_i] == 10) { ca_i += 1 }
}
-function ca_sum(ui_st: mut UiState) -> CaVal {
- var a = ca_product(ui_st)
- while ui_st.ca_bad == "" {
- ca_ws(ui_st)
- if ui_st.ca_i >= len(ui_st.ca_s) or (ui_st.ca_s[ui_st.ca_i] != 43 and ui_st.ca_s[ui_st.ca_i] != 45) { return a }
- let minus = ui_st.ca_s[ui_st.ca_i] == 45
- ui_st.ca_i += 1
- let b = ca_product(ui_st)
- a = ca_add(ui_st, a, b, minus)
+function ca_sum() -> CaVal {
+ var a = ca_product()
+ while ca_bad == "" {
+ ca_ws()
+ if ca_i >= len(ca_s) or (ca_s[ca_i] != 43 and ca_s[ca_i] != 45) { return a }
+ let minus = ca_s[ca_i] == 45
+ ca_i += 1
+ let b = ca_product()
+ a = ca_add(a, b, minus)
}
return a
}
-function ca_add(ui_st: UiState, a: CaVal, b: CaVal, minus: bool) -> CaVal {
+function ca_add(a: CaVal, b: CaVal, minus: bool) -> CaVal {
var sign = 1.0
if minus { sign = -1.0 }
let r = new CaVal
@@ -51,15 +58,15 @@ function ca_add(ui_st: UiState, a: CaVal, b: CaVal, minus: bool) -> CaVal {
return r
}
r.bare = false
- r.px = ca_len(ui_st, a).px + sign * ca_len(ui_st, b).px
+ r.px = ca_len(a).px + sign * ca_len(b).px
r.pct = a.pct + sign * b.pct
return r
}
# a bare number beside a length is a length in px
-function ca_len(ui_st: UiState, a: CaVal) -> CaVal {
+function ca_len(a: CaVal) -> CaVal {
if not a.bare { return a }
let r = new CaVal
r.bare = false
- r.px = el_px(ui_st, a.n)
+ r.px = el_px(a.n)
return r
}
diff --git a/packages/ludic.ui/calc_fns.ludic b/packages/ludic.ui/calc_fns.ludic
index d3f17233..b9eb7b4a 100644
--- a/packages/ludic.ui/calc_fns.ludic
+++ b/packages/ludic.ui/calc_fns.ludic
@@ -9,40 +9,40 @@ function ca_fn_at(s: string, at: int) -> string {
return ""
}
function ca_word(s: string, at: int, w: string) -> bool { return at + len(w) <= len(s) and s[at..at + len(w)] == w }
-function ca_fn(ui_st: mut UiState, f: string) -> CaVal {
- ui_st.ca_i = ui_st.ca_i + len(f) + 1
+function ca_fn(f: string) -> CaVal {
+ ca_i = ca_i + len(f) + 1
let args = new []CaVal
- while ui_st.ca_bad == "" {
- push(args, ca_sum(ui_st))
- ca_ws(ui_st)
- if ui_st.ca_i < len(ui_st.ca_s) and ui_st.ca_s[ui_st.ca_i] == 44 { ui_st.ca_i += 1 } else { break }
+ while ca_bad == "" {
+ push(args, ca_sum())
+ ca_ws()
+ if ca_i < len(ca_s) and ca_s[ca_i] == 44 { ca_i += 1 } else { break }
}
- if ui_st.ca_bad == "" and (ui_st.ca_i >= len(ui_st.ca_s) or ui_st.ca_s[ui_st.ca_i] != 41) { ui_st.ca_bad = "a closing )" }
- ui_st.ca_i += 1
- if ui_st.ca_bad != "" { return new CaVal }
+ if ca_bad == "" and (ca_i >= len(ca_s) or ca_s[ca_i] != 41) { ca_bad = "a closing )" }
+ ca_i += 1
+ if ca_bad != "" { return new CaVal }
if f == "clamp" and len(args) != 3 {
- ui_st.ca_bad = "clamp(least, want, most)"
+ ca_bad = "clamp(least, want, most)"
return new CaVal
}
for i in 0 .. len(args) {
if args[i].pct != 0.0 {
- ui_st.ca_bad = `lengths in {f}(), not a percentage`
+ ca_bad = `lengths in {f}(), not a percentage`
return new CaVal
}
}
- if f == "clamp" { return ca_pick(ui_st, ca_pick(ui_st, args[1], args[0], false), args[2], true) }
+ if f == "clamp" { return ca_pick(ca_pick(args[1], args[0], false), args[2], true) }
var r = args[0]
- for i in 1 .. len(args) { r = ca_pick(ui_st, r, args[i], f == "min") }
+ for i in 1 .. len(args) { r = ca_pick(r, args[i], f == "min") }
return r
}
# the smaller (or the larger) of two, a bare number beside a length being px
-function ca_pick(ui_st: UiState, a: CaVal, b: CaVal, smaller: bool) -> CaVal {
+function ca_pick(a: CaVal, b: CaVal, smaller: bool) -> CaVal {
if a.bare and b.bare {
if (b.n < a.n) == smaller and b.n != a.n { return b }
return a
}
- let la = ca_len(ui_st, a)
- let lb = ca_len(ui_st, b)
+ let la = ca_len(a)
+ let lb = ca_len(b)
if (lb.px < la.px) == smaller and lb.px != la.px { return lb }
return la
}
diff --git a/packages/ludic.ui/calc_terms.ludic b/packages/ludic.ui/calc_terms.ludic
index dd3b01b0..d5c7cd5f 100644
--- a/packages/ludic.ui/calc_terms.ludic
+++ b/packages/ludic.ui/calc_terms.ludic
@@ -1,28 +1,28 @@
# calc_terms.ludic - calc()'s products and its terms: a number with its unit, a bracket, a nested
# calc(), a minus in front; a product needs a plain number on one side, and a quotient a plain number
# or a length on its right (a length over a length is a plain number, as CSS's typed division makes)
-function ca_product(ui_st: mut UiState) -> CaVal {
- var a = ca_factor(ui_st)
- while ui_st.ca_bad == "" {
- ca_ws(ui_st)
- if ui_st.ca_i >= len(ui_st.ca_s) or (ui_st.ca_s[ui_st.ca_i] != 42 and ui_st.ca_s[ui_st.ca_i] != 47) { return a }
- let div = ui_st.ca_s[ui_st.ca_i] == 47
- ui_st.ca_i += 1
- let b = ca_factor(ui_st)
+function ca_product() -> CaVal {
+ var a = ca_factor()
+ while ca_bad == "" {
+ ca_ws()
+ if ca_i >= len(ca_s) or (ca_s[ca_i] != 42 and ca_s[ca_i] != 47) { return a }
+ let div = ca_s[ca_i] == 47
+ ca_i += 1
+ let b = ca_factor()
if div and not b.bare and not a.bare and a.pct == 0.0 and b.pct == 0.0 and b.px != 0.0 {
a = ca_ratio(a.px / b.px)
continue
}
if div and not b.bare {
- ui_st.ca_bad = "a plain number, or a length under a length, after /"
+ ca_bad = "a plain number, or a length under a length, after /"
return a
}
if div and b.n == 0.0 {
- ui_st.ca_bad = "a number that is not 0 after /"
+ ca_bad = "a number that is not 0 after /"
return a
}
if div { a = ca_scale(a, 1.0 / b.n) } else if a.bare { a = ca_scale(b, a.n) } else if b.bare { a = ca_scale(a, b.n) } else {
- ui_st.ca_bad = "a plain number on one side of *"
+ ca_bad = "a plain number on one side of *"
return a
}
}
@@ -42,47 +42,47 @@ function ca_scale(a: CaVal, k: float) -> CaVal {
r.pct = a.pct * k
return r
}
-function ca_factor(ui_st: mut UiState) -> CaVal {
- ca_ws(ui_st)
- if ui_st.ca_i >= len(ui_st.ca_s) {
- ui_st.ca_bad = "a value"
+function ca_factor() -> CaVal {
+ ca_ws()
+ if ca_i >= len(ca_s) {
+ ca_bad = "a value"
return new CaVal
}
- let c = ui_st.ca_s[ui_st.ca_i]
+ let c = ca_s[ca_i]
if c == 45 {
- ui_st.ca_i += 1
- return ca_scale(ca_factor(ui_st), -1.0)
+ ca_i += 1
+ return ca_scale(ca_factor(), -1.0)
}
- let f = ca_fn_at(ui_st.ca_s, ui_st.ca_i)
- if f == "min" or f == "max" or f == "clamp" { return ca_fn(ui_st, f) }
+ let f = ca_fn_at(ca_s, ca_i)
+ if f == "min" or f == "max" or f == "clamp" { return ca_fn(f) }
if c == 40 or f == "calc" {
- while ui_st.ca_s[ui_st.ca_i] != 40 { ui_st.ca_i += 1 }
- ui_st.ca_i += 1
- let v = ca_sum(ui_st)
- ca_ws(ui_st)
- if ui_st.ca_i < len(ui_st.ca_s) and ui_st.ca_s[ui_st.ca_i] == 41 { ui_st.ca_i += 1 } else if ui_st.ca_bad == "" { ui_st.ca_bad = "a closing )" }
+ while ca_s[ca_i] != 40 { ca_i += 1 }
+ ca_i += 1
+ let v = ca_sum()
+ ca_ws()
+ if ca_i < len(ca_s) and ca_s[ca_i] == 41 { ca_i += 1 } else if ca_bad == "" { ca_bad = "a closing )" }
return v
}
- return ca_number(ui_st)
+ return ca_number()
}
# 12, 1.5em, 50%, 20vw
-function ca_number(ui_st: mut UiState) -> CaVal {
- let at = ui_st.ca_i
- while ui_st.ca_i < len(ui_st.ca_s) and ((ui_st.ca_s[ui_st.ca_i] >= 48 and ui_st.ca_s[ui_st.ca_i] <= 57) or ui_st.ca_s[ui_st.ca_i] == 46) { ui_st.ca_i += 1 }
- let num_end = ui_st.ca_i
- while ui_st.ca_i < len(ui_st.ca_s) and ((ui_st.ca_s[ui_st.ca_i] >= 97 and ui_st.ca_s[ui_st.ca_i] <= 122) or ui_st.ca_s[ui_st.ca_i] == 37) { ui_st.ca_i += 1 }
+function ca_number() -> CaVal {
+ let at = ca_i
+ while ca_i < len(ca_s) and ((ca_s[ca_i] >= 48 and ca_s[ca_i] <= 57) or ca_s[ca_i] == 46) { ca_i += 1 }
+ let num_end = ca_i
+ while ca_i < len(ca_s) and ((ca_s[ca_i] >= 97 and ca_s[ca_i] <= 122) or ca_s[ca_i] == 37) { ca_i += 1 }
let r = new CaVal
if num_end == at {
- ui_st.ca_bad = "a number"
+ ca_bad = "a number"
return r
}
- let word: string = ui_st.ca_s[at..ui_st.ca_i]
- let unit: string = ui_st.ca_s[num_end..ui_st.ca_i]
+ let word: string = ca_s[at..ca_i]
+ let unit: string = ca_s[num_end..ca_i]
if unit == "" {
r.n = el_num(word)
return r
}
r.bare = false
- if unit == "%" { r.pct = el_num(word) } else if unit == "px" or unit == "em" or unit == "rem" or unit == "vw" or unit == "vh" { r.px = el_unit(ui_st, word) } else { ui_st.ca_bad = `a unit calc() knows, not {unit}` }
+ if unit == "%" { r.pct = el_num(word) } else if unit == "px" or unit == "em" or unit == "rem" or unit == "vw" or unit == "vh" { r.px = el_unit(word) } else { ca_bad = `a unit calc() knows, not {unit}` }
return r
}
diff --git a/packages/ludic.ui/cascade.ludic b/packages/ludic.ui/cascade.ludic
index 57670225..b91a631d 100644
--- a/packages/ludic.ui/cascade.ludic
+++ b/packages/ludic.ui/cascade.ludic
@@ -1,14 +1,14 @@
# cascade.ludic - once the frame's tree is built, every element is styled as a browser would:
# the default sheet, then the file's rules from least to most specific, then its style="...", then
# its own attributes. Then what is display: none leaves the tree.
-function cs_tree(ui_st: mut UiState, n: UiNode) -> void {
+function cs_tree(n: UiNode) -> void {
for i in 0 .. len(n.children) {
let c = n.children[i]
c.parent = n
c.index = i
}
- if n.tag != "" { cs_node(ui_st, n) }
- for i in 0 .. len(n.children) { cs_tree(ui_st, n.children[i]) }
+ if n.tag != "" { cs_node(n) }
+ for i in 0 .. len(n.children) { cs_tree(n.children[i]) }
var shown = new []UiNode
for i in 0 .. len(n.children) {
if not n.children[i].hidden { push(shown, n.children[i]) }
@@ -18,52 +18,54 @@ function cs_tree(ui_st: mut UiState, n: UiNode) -> void {
}
# twice: the font size (and zoom) first, from every rule, so an em anywhere reads the size the
# element ends with (again when it zooms, so its own lengths are zoomed)
-function cs_node(ui_st: mut UiState, n: UiNode) -> void {
- ui_st.cs_pass = 1
- cs_styles(ui_st, n)
- if zm_own(n) != 1.0 { cs_styles(ui_st, n) }
- ui_st.cs_pass = 0
- cs_styles(ui_st, n)
- an_apply(ui_st, n)
- tr_apply(ui_st, n)
+function cs_node(n: UiNode) -> void {
+ cs_pass = 1
+ cs_styles(n)
+ if zm_own(n) != 1.0 { cs_styles(n) }
+ cs_pass = 0
+ cs_styles(n)
+ an_apply(n)
+ tr_apply(n)
}
-function cs_styles(ui_st: mut UiState, n: UiNode) -> void {
- cs_sheet(ui_st, n, ua_sheet(ui_st))
- if n.ups != null { cs_merged(ui_st, n) } else if n.sheet != null { cs_sheet(ui_st, n, n.sheet) }
- if n.style != null { cs_rule(ui_st, n, n.style) }
+function cs_styles(n: UiNode) -> void {
+ cs_sheet(n, ua_sheet())
+ if n.ups != null { cs_merged(n) } else if n.sheet != null { cs_sheet(n, n.sheet) }
+ if n.style != null { cs_rule(n, n.style) }
if n.ups != null {
for i in 0 .. len(n.ups) {
- if n.ups[i].style != null { cs_rule_in(ui_st, n, n.ups[i].style, n.ups[i].env) }
+ if n.ups[i].style != null { cs_rule_in(n, n.ups[i].style, n.ups[i].env) }
}
}
for i in 0 .. len(n.akeys) {
- if cs_pass_ok(ui_st, n.akeys[i]) { el_apply(ui_st, n, n.akeys[i], n.vals[i]) }
+ if cs_pass_ok(n.akeys[i]) { el_apply(n, n.akeys[i], n.vals[i]) }
}
}
-function cs_pass_ok(ui_st: UiState, key: string) -> bool { return ui_st.cs_pass == 0 or at_alias(key) == "size" or key == "zoom" }
-function cs_sheet(ui_st: mut UiState, n: UiNode, sh: UiSheet) -> void {
+var cs_pass: int = 0
+function cs_pass_ok(key: string) -> bool { return cs_pass == 0 or at_alias(key) == "size" or key == "zoom" }
+function cs_sheet(n: UiNode, sh: UiSheet) -> void {
for i in 0 .. len(sh.rules) {
let r = sh.rules[i]
- if md_ok(ui_st, r) and sel_match(ui_st, r.sel, n) { cs_rule(ui_st, n, r) }
+ if md_ok(r) and sel_match(r.sel, n) { cs_rule(n, r) }
}
}
-function cs_rule(ui_st: mut UiState, n: UiNode, r: UiRule) -> void { cs_rule_in(ui_st, n, r, n.env) }
+function cs_rule(n: UiNode, r: UiRule) -> void { cs_rule_in(n, r, n.env) }
# a rule's custom properties first, so its other declarations can read them; values read in `e`
-function cs_rule_in(ui_st: mut UiState, n: UiNode, r: UiRule, e: UiEnv) -> void {
+function cs_rule_in(n: UiNode, r: UiRule, e: UiEnv) -> void {
for j in 0 .. len(r.keys) {
- if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
+ if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) }
}
for j in 0 .. len(r.keys) {
- if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") and cs_pass_ok(ui_st, r.keys[j]) { el_attr(ui_st, n, r.keys[j], ui_eval(ui_st, r.vals[j], e)) }
+ if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") and cs_pass_ok(r.keys[j]) { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) }
}
}
-function ua_sheet(ui_st: mut UiState) -> UiSheet {
- if ui_st.ua_sh != null { return ui_st.ua_sh }
+var ua_sh: UiSheet = null
+function ua_sheet() -> UiSheet {
+ if ua_sh != null { return ua_sh }
let f = new UiFile
f.path = "the default sheet"
f.imp = new []UiFile
f.own = new []UiRule
- lss_text(ui_st, f, UA_LSS)
- ui_st.ua_sh = sheet_of(f)
- return ui_st.ua_sh
+ lss_text(f, UA_LSS)
+ ua_sh = sheet_of(f)
+ return ua_sh
}
diff --git a/packages/ludic.ui/cascade_merge.ludic b/packages/ludic.ui/cascade_merge.ludic
index 1210b945..0f5f6bea 100644
--- a/packages/ludic.ui/cascade_merge.ludic
+++ b/packages/ludic.ui/cascade_merge.ludic
@@ -2,25 +2,25 @@
# that passed a class down to it, all at once: the rules that match, from all of them, ordered by
# specificity as one sheet's are. On a tie the user's rule wins (the outermost user's last), as a
# later rule does. Then `order`, which rearranges a flex box's children without touching the tree.
-function cs_merged(ui_st: mut UiState, n: UiNode) -> void {
+function cs_merged(n: UiNode) -> void {
let rules = new []UiRule
let envs = new []UiEnv
let seen = new []UiSheet
- if n.sheet != null { cs_gather(ui_st, n, n.sheet, n.env, rules, envs, seen) }
+ if n.sheet != null { cs_gather(n, n.sheet, n.env, rules, envs, seen) }
for i in 0 .. len(n.ups) {
- if n.ups[i].sheet != null { cs_gather(ui_st, n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) }
+ if n.ups[i].sheet != null { cs_gather(n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) }
}
- for i in 0 .. len(rules) { cs_rule_in(ui_st, n, rules[i], envs[i]) }
+ for i in 0 .. len(rules) { cs_rule_in(n, rules[i], envs[i]) }
}
# a sheet's matching rules into the list, each after every rule at most as specific (stable)
-function cs_gather(ui_st: mut UiState, n: UiNode, sh: UiSheet, e: UiEnv, rules: []UiRule, envs: []UiEnv, seen: []UiSheet) -> void {
+function cs_gather(n: UiNode, sh: UiSheet, e: UiEnv, rules: []UiRule, envs: []UiEnv, seen: []UiSheet) -> void {
for i in 0 .. len(seen) {
if seen[i] == sh { return }
}
push(seen, sh)
for i in 0 .. len(sh.rules) {
let r = sh.rules[i]
- if md_ok(ui_st, r) and sel_match(ui_st, r.sel, n) {
+ if md_ok(r) and sel_match(r.sel, n) {
push(rules, r)
push(envs, e)
var j = len(rules) - 1
diff --git a/packages/ludic.ui/class.ludic b/packages/ludic.ui/class.ludic
index c95080ba..39ddd39b 100644
--- a/packages/ludic.ui/class.ludic
+++ b/packages/ludic.ui/class.ludic
@@ -16,42 +16,43 @@ export property UiClass {
seen_xml: string = "" # the files as a developer's build last found them on disk
seen_lss: string = ""
}
-export function ui_register(ui_st: mut UiState, k: UiClass) -> int {
- if ui_st.uc_list == null { ui_st.uc_list = new []UiClass }
- push(ui_st.uc_list, k)
- return len(ui_st.uc_list)
+var uc_list: []UiClass = null # filled by the compiler's registrations, before the program's globals
+export function ui_register(k: UiClass) -> int {
+ if uc_list == null { uc_list = new []UiClass }
+ push(uc_list, k)
+ return len(uc_list)
}
-function uc_find(ui_st: UiState, name: string) -> UiClass {
- if ui_st.uc_list == null { return null }
- for i in 0 .. len(ui_st.uc_list) {
- if ui_st.uc_list[i].name == name { return ui_st.uc_list[i] }
+function uc_find(name: string) -> UiClass {
+ if uc_list == null { return null }
+ for i in 0 .. len(uc_list) {
+ if uc_list[i].name == name { return uc_list[i] }
}
return null
}
# the class's template and styles, read the first time it is shown
-function uc_load(ui_st: mut UiState, k: UiClass) -> void {
+function uc_load(k: UiClass) -> void {
if k.root != null { return }
let f = lib_blank(k.xml_path)
f.src = k.xml
- if k.lss != "" { lss_text(ui_st, f, k.lss) }
+ if k.lss != "" { lss_text(f, k.lss) }
f.sheet = sheet_of(f)
- tp_lines(ui_st, k.xml)
- let root = tpl_read(ui_st, Xml.parse(k.xml), k.xml_path)
+ tp_lines(k.xml)
+ let root = tpl_read(Xml.parse(k.xml), k.xml_path)
k.sheet = f.sheet
- lib_link(ui_st, f, root)
+ lib_link(f, root)
k.root = root
}
# a developer's build reads a class's files again when they change on disk: the first look is what
# they are, and a later one that differs is taken (an @import in the styles is read from disk too)
-function uc_reload(ui_st: mut UiState) -> bool {
- if ui_st.uc_list == null { return false }
+function uc_reload() -> bool {
+ if uc_list == null { return false }
var changed = false
- for i in 0 .. len(ui_st.uc_list) {
- let k = ui_st.uc_list[i]
+ for i in 0 .. len(uc_list) {
+ let k = uc_list[i]
if k.xml_path != "" and Fs.exists(k.xml_path) {
- let x = lib_text(ui_st, k.xml_path)
+ let x = lib_text(k.xml_path)
var l = ""
- if k.lss_path != "" and Fs.exists(k.lss_path) { l = lib_text(ui_st, k.lss_path) }
+ if k.lss_path != "" and Fs.exists(k.lss_path) { l = lib_text(k.lss_path) }
if k.seen_xml != "" and (x != k.seen_xml or l != k.seen_lss) {
k.xml = x
k.lss = l
@@ -65,7 +66,7 @@ function uc_reload(ui_st: mut UiState) -> bool {
return changed
}
# every class reads its template and styles again, as when a stylesheet they import has changed
-function uc_invalidate(ui_st: mut UiState) -> void {
- if ui_st.uc_list == null { return }
- for i in 0 .. len(ui_st.uc_list) { ui_st.uc_list[i].root = null }
+function uc_invalidate() -> void {
+ if uc_list == null { return }
+ for i in 0 .. len(uc_list) { uc_list[i].root = null }
}
diff --git a/packages/ludic.ui/color.ludic b/packages/ludic.ui/color.ludic
index 88ae3493..1a238c5c 100644
--- a/packages/ludic.ui/color.ludic
+++ b/packages/ludic.ui/color.ludic
@@ -1,18 +1,19 @@
# color.ludic - CSS colours: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(r, g, b), rgba(r, g, b, a), the
# basic names, transparent and none. A colour is 0xRRGGBB; its alpha is left in cl_alpha.
+var cl_alpha: float = 1.0
const CL_NAMES: string = " black=000000 white=ffffff red=ff0000 green=008000 lime=00ff00 blue=0000ff yellow=ffff00 cyan=00ffff aqua=00ffff magenta=ff00ff fuchsia=ff00ff gray=808080 grey=808080 silver=c0c0c0 maroon=800000 olive=808000 navy=000080 purple=800080 teal=008080 orange=ffa500 "
-function ui_color(ui_st: mut UiState, name0: string) -> int {
+function ui_color(name0: string) -> int {
let name = tpl_words(name0)
- ui_st.cl_alpha = 1.0
+ cl_alpha = 1.0
if name == "none" or name == "" or name == "transparent" { return UI_NONE }
- if name[0] == 35 { return cl_hex(ui_st, name[1..len(name)]) }
- if len(name) > 4 and (name[0..4] == "rgb(" or name[0..5] == "rgba(") { return cl_rgb(ui_st, name) }
+ if name[0] == 35 { return cl_hex(name[1..len(name)]) }
+ if len(name) > 4 and (name[0..4] == "rgb(" or name[0..5] == "rgba(") { return cl_rgb(name) }
let pat = ` {name}=`
let n = len(pat)
for i in 0 .. len(CL_NAMES) - n + 1 {
- if CL_NAMES[i..i + n] == pat { return cl_hex(ui_st, CL_NAMES[i + n..i + n + 6]) }
+ if CL_NAMES[i..i + n] == pat { return cl_hex(CL_NAMES[i + n..i + n + 6]) }
}
- ui_err(ui_st, `{name}: not a colour - #rrggbb, rgb(), rgba(), a name, or var(--a-colour)`)
+ ui_err(`{name}: not a colour - #rrggbb, rgb(), rgba(), a name, or var(--a-colour)`)
return 15658734
}
function cl_digit(c: int) -> int {
@@ -20,26 +21,26 @@ function cl_digit(c: int) -> int {
if c >= 65 { return c - 55 }
return c - 48
}
-function cl_hex(ui_st: mut UiState, h: string) -> int {
+function cl_hex(h: string) -> int {
var c = 0
let n = len(h)
if n == 3 or n == 4 {
for i in 0 .. 3 { c = c * 256 + cl_digit(h[i]) * 17 }
- if n == 4 { ui_st.cl_alpha = float(cl_digit(h[3]) * 17) / 255.0 }
+ if n == 4 { cl_alpha = float(cl_digit(h[3]) * 17) / 255.0 }
return c
}
for i in 0 .. 3 { c = c * 256 + cl_digit(h[i * 2]) * 16 + cl_digit(h[i * 2 + 1]) }
- if n == 8 { ui_st.cl_alpha = float(cl_digit(h[6]) * 16 + cl_digit(h[7])) / 255.0 }
+ if n == 8 { cl_alpha = float(cl_digit(h[6]) * 16 + cl_digit(h[7])) / 255.0 }
return c
}
# rgb(217, 165, 74) or rgba(0, 0, 0, 0.55)
-function cl_rgb(ui_st: mut UiState, s: string) -> int {
+function cl_rgb(s: string) -> int {
var open = 0
while open < len(s) and s[open] != 40 { open += 1 }
let parts = lss_split(s[open + 1..len(s) - 1], 44)
var c = 0
for i in 0 .. Math.min(3, len(parts)) { c = c * 256 + Math.clamp(int(el_num(tpl_words(parts[i]))), 0, 255) }
- if len(parts) > 3 { ui_st.cl_alpha = el_num(tpl_words(parts[3])) }
+ if len(parts) > 3 { cl_alpha = el_num(tpl_words(parts[3])) }
return c
}
# a colour as #rrggbb, for a dump or a test
diff --git a/packages/ludic.ui/controls.ludic b/packages/ludic.ui/controls.ludic
index abb41f86..eefd6c5f 100644
--- a/packages/ludic.ui/controls.ludic
+++ b/packages/ludic.ui/controls.ludic
@@ -11,45 +11,45 @@ function ct_type(n: UiNode) -> string {
}
function ct_takes_arrows(n: UiNode) -> bool { return ct_type(n) == "range" or ct_type(n) == "select" or ct_type(n) == "number" }
function ct_takes_space(n: UiNode) -> bool { return ct_type(n) == "text" }
-function ct_change(ui_st: mut UiState, n: UiNode, v: Val) -> void {
- ui_sound(ui_st, ct_sound(n))
- ui_fire(ui_st, n, "change", v)
+function ct_change(n: UiNode, v: Val) -> void {
+ ui_sound(ct_sound(n))
+ ui_fire(n, "change", v)
}
function ct_sound(n: UiNode) -> string {
let s = ui_attr_text(n, "sound")
if s == "" { return "click" }
return s
}
-function ct_activate(ui_st: mut UiState, n: UiNode) -> void {
+function ct_activate(n: UiNode) -> void {
if not n.enabled { return }
let t = ct_type(n)
- if t == "checkbox" { ct_change(ui_st, n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(ui_st, n, ui_attr(n, "value")) } else if t == "select" { ct_step(ui_st, n, 1) } else if t == "key" { ct_key_start(ui_st, n) } else if t == "number" { ct_num_commit(ui_st, n) } else if t == "text" { ct_submit(ui_st, n) } else if t == "" {
- ui_sound(ui_st, ct_sound(n))
- ui_fire(ui_st, n, "press", Value.null())
+ if t == "checkbox" { ct_change(n, vl_bool(not ui_attr_on(n, "checked"))) } else if t == "radio" { ct_change(n, ui_attr(n, "value")) } else if t == "select" { ct_step(n, 1) } else if t == "key" { ct_key_start(n) } else if t == "number" { ct_num_commit(n) } else if t == "text" { ct_submit(n) } else if t == "" {
+ ui_sound(ct_sound(n))
+ ui_fire(n, "press", Value.null())
}
}
-function ct_step(ui_st: mut UiState, n: UiNode, d: int) -> void {
+function ct_step(n: UiNode, d: int) -> void {
let t = ct_type(n)
if t == "select" and n.opts != null and len(n.opts) > 0 {
let at = (ct_option_at(n, n.opts) + d + len(n.opts)) % len(n.opts)
let ov = ui_attr(n.opts[at], "value")
- if Value.kind(ov) == 0 { ct_change(ui_st, n, Value.int(at)) } else { ct_change(ui_st, n, ov) }
+ if Value.kind(ov) == 0 { ct_change(n, Value.int(at)) } else { ct_change(n, ov) }
}
- if t == "number" { ct_num_step(ui_st, n, d) }
+ if t == "number" { ct_num_step(n, d) }
if t == "range" {
var step = Value.as_float(ui_attr(n, "step"))
if step <= 0.0 { step = 1.0 }
- ct_range_set(ui_st, n, Value.as_float(ui_attr(n, "value")) + step * float(d))
+ ct_range_set(n, Value.as_float(ui_attr(n, "value")) + step * float(d))
}
}
-function ct_range_set(ui_st: mut UiState, n: UiNode, v0: float) -> void {
+function ct_range_set(n: UiNode, v0: float) -> void {
let lo = Value.as_float(ui_attr(n, "min"))
let hi = Value.as_float(ui_attr(n, "max"))
let v = Math.clamp(v0, lo, Math.max(hi, lo))
- if v != Value.as_float(ui_attr(n, "value")) { ui_fire(ui_st, n, "change", Value.float(v)) }
+ if v != Value.as_float(ui_attr(n, "value")) { ui_fire(n, "change", Value.float(v)) }
}
# a range's track pressed or dragged: the value where the pointer is
-function ct_press(ui_st: mut UiState, n: UiNode, x: float, y: float) -> void {
+function ct_press(n: UiNode, x: float, y: float) -> void {
if ct_type(n) != "range" { return }
for i in 0 .. len(n.children) {
let t = n.children[i]
@@ -59,26 +59,26 @@ function ct_press(ui_st: mut UiState, n: UiNode, x: float, y: float) -> void {
var step = Value.as_float(ui_attr(n, "step"))
if step <= 0.0 { step = 1.0 }
let raw = lo + Math.clamp((x - t.x) / t.cw, 0.0, 1.0) * (hi - lo)
- ct_range_set(ui_st, n, lo + float(int((raw - lo) / step + 0.5)) * step)
+ ct_range_set(n, lo + float(int((raw - lo) / step + 0.5)) * step)
}
}
}
-function ct_drag(ui_st: mut UiState, root: UiNode, key: string, x: float, y: float) -> void {
+function ct_drag(root: UiNode, key: string, x: float, y: float) -> void {
let n = fr_node(root, key)
- if n != null { ct_press(ui_st, n, x, y) }
+ if n != null { ct_press(n, x, y) }
}
# typing into a focused text field, or the key a key field is waiting for
-function ct_typed(ui_st: mut UiState, n: UiNode) -> void {
+function ct_typed(n: UiNode) -> void {
let t = ct_type(n)
- if t == "key" { ct_key_typed(ui_st, n) }
- if t == "number" { ct_num_typed(ui_st, n) }
- if t != "text" or (ui_st.in_now.typed == "" and not ui_st.in_now.backspace) { return }
+ if t == "key" { ct_key_typed(n) }
+ if t == "number" { ct_num_typed(n) }
+ if t != "text" or (in_now.typed == "" and not in_now.backspace) { return }
var s = ui_attr_text(n, "value")
- if ui_st.in_now.backspace and len(s) > 0 { s = s[0..ct_char_start(s)] }
+ if in_now.backspace and len(s) > 0 { s = s[0..ct_char_start(s)] }
var max = Value.as_int(ui_attr(n, "maxlength"))
if max <= 0 { max = 1000 }
- if len(ui_st.in_now.typed) > 0 and len(s) < max { s = s + ui_st.in_now.typed[0..Math.min(len(ui_st.in_now.typed), max - len(s))] }
- ui_fire(ui_st, n, "change", Value.str(s))
+ if len(in_now.typed) > 0 and len(s) < max { s = s + in_now.typed[0..Math.min(len(in_now.typed), max - len(s))] }
+ ui_fire(n, "change", Value.str(s))
}
# where the last UTF-8 character starts, so Backspace takes a whole letter
function ct_char_start(s: string) -> int {
diff --git a/packages/ludic.ui/controls_build.ludic b/packages/ludic.ui/controls_build.ludic
index 450d8350..b5177c68 100644
--- a/packages/ludic.ui/controls_build.ludic
+++ b/packages/ludic.ui/controls_build.ludic
@@ -32,7 +32,7 @@ function ct_build_bar(n: UiNode) -> void {
fill.w_pct = Math.clamp((Value.as_float(ui_attr(n, "value")) - lo) / (hi - lo), 0.0, 1.0) * 100.0
push(n.children, fill)
}
-function ct_build(ui_st: UiState, n: UiNode) -> void {
+function ct_build(n: UiNode) -> void {
if n.tag == "progress" or n.tag == "meter" {
ct_build_bar(n)
return
@@ -41,10 +41,10 @@ function ct_build(ui_st: UiState, n: UiNode) -> void {
n.focusable = true
let t = ui_attr_text(n, "type")
if n.tag == "select" {
- ct_build_select(ui_st, n)
+ ct_build_select(n)
return
}
- ct_label(ui_st, n)
+ ct_label(n)
if t == "checkbox" or t == "radio" {
let track = ct_part(n, "ui-track", "")
push(track.children, ct_part(n, "ui-knob", ""))
@@ -53,8 +53,8 @@ function ct_build(ui_st: UiState, n: UiNode) -> void {
ct_build_range(n)
} else {
let field = ct_part(n, "ui-field", "")
- push(field.children, ct_part(n, "ui-value", ct_field_text(ui_st, n)))
- if ui_st.fc_key == n.key and t != "key" { push(field.children, ct_part(n, "ui-caret", "")) }
+ push(field.children, ct_part(n, "ui-value", ct_field_text(n)))
+ if fc_key == n.key and t != "key" { push(field.children, ct_part(n, "ui-caret", "")) }
push(n.children, field)
}
}
@@ -71,10 +71,10 @@ function ct_build_range(n: UiNode) -> void {
push(n.children, track)
push(n.children, ct_part(n, "ui-value", ct_value_text(n)))
}
-function ct_build_select(ui_st: UiState, n: UiNode) -> void {
+function ct_build_select(n: UiNode) -> void {
let opts = n.children
n.children = new []UiNode
- ct_label(ui_st, n)
+ ct_label(n)
var shown = ""
let at = ct_option_at(n, opts)
if at >= 0 { shown = opts[at].text }
@@ -92,9 +92,9 @@ function ct_option_at(n: UiNode, opts: []UiNode) -> int {
}
return -1
}
-function ct_field_text(ui_st: UiState, n: UiNode) -> string {
+function ct_field_text(n: UiNode) -> string {
let t = ui_attr_text(n, "type")
- if t == "key" { return ct_key_text(ui_st, n) }
- if t == "number" { return ct_num_shown(ui_st, n) }
+ if t == "key" { return ct_key_text(n) }
+ if t == "number" { return ct_num_shown(n) }
return ui_attr_text(n, "value")
}
diff --git a/packages/ludic.ui/controls_key.ludic b/packages/ludic.ui/controls_key.ludic
index d8db1f02..837e8a51 100644
--- a/packages/ludic.ui/controls_key.ludic
+++ b/packages/ludic.ui/controls_key.ludic
@@ -2,51 +2,54 @@
# the next key pressed is its value - Tab, the arrows and Enter included, which move nothing while it
# listens. Esc stops listening and keeps the value; Backspace, when it is not listening, clears it (0).
# `shown` is what it says for its value (a host's own name for the key), else the keyboard's label.
-function ct_key_start(ui_st: mut UiState, n: UiNode) -> void {
- ui_st.ct_capture = n.key
- ui_st.ct_capture_at = ui_st.an_frame
- ui_st.ct_capture_seen = ui_st.an_frame
+var ct_capture: string = ""
+var ct_capture_at: int = -1 # the frame listening began, whose own key started it
+var ct_capture_seen: int = -1 # the last frame that began listening, for ui_capturing
+function ct_key_start(n: UiNode) -> void {
+ ct_capture = n.key
+ ct_capture_at = an_frame
+ ct_capture_seen = an_frame
}
# while it listens, the frame's keys are the field's: nothing else reads them (fc_keys)
-function ct_key_listening(ui_st: UiState) -> bool { return ui_st.ct_capture != "" and ui_st.ct_capture_at != ui_st.an_frame }
+function ct_key_listening() -> bool { return ct_capture != "" and ct_capture_at != an_frame }
# the field listening, given this frame's key; false when there is none to give it to
-function ct_key_capture(ui_st: mut UiState, list: []UiNode) -> bool {
- if ui_st.ct_capture == "" { return false }
- ui_st.ct_capture_seen = ui_st.an_frame
+function ct_key_capture(list: []UiNode) -> bool {
+ if ct_capture == "" { return false }
+ ct_capture_seen = an_frame
var n: UiNode = null
for i in 0 .. len(list) {
- if list[i].key == ui_st.ct_capture { n = list[i] }
+ if list[i].key == ct_capture { n = list[i] }
}
if n == null {
- ui_st.ct_capture = ""
+ ct_capture = ""
return false
}
- if ui_st.ct_capture_at == ui_st.an_frame { return true }
- if ui_st.in_now.escape {
- ui_st.ct_capture = ""
+ if ct_capture_at == an_frame { return true }
+ if in_now.escape {
+ ct_capture = ""
return true
}
- if ui_st.in_now.key != 0 {
- ui_st.ct_capture = ""
- ct_change(ui_st, n, Value.int(ui_st.in_now.key))
+ if in_now.key != 0 {
+ ct_capture = ""
+ ct_change(n, Value.int(in_now.key))
}
return true
}
# focused, not listening: Backspace unbinds it
-function ct_key_typed(ui_st: mut UiState, n: UiNode) -> void {
- if ui_st.ct_capture != n.key and ui_st.in_now.backspace { ct_change(ui_st, n, Value.int(0)) }
+function ct_key_typed(n: UiNode) -> void {
+ if ct_capture != n.key and in_now.backspace { ct_change(n, Value.int(0)) }
}
# a press anywhere but the field stops it listening
-function ct_key_pressed_away(ui_st: mut UiState, hit: UiNode) -> void {
- if ui_st.ct_capture != "" and (hit == null or hit.key != ui_st.ct_capture) { ui_st.ct_capture = "" }
+function ct_key_pressed_away(hit: UiNode) -> void {
+ if ct_capture != "" and (hit == null or hit.key != ct_capture) { ct_capture = "" }
}
-function ct_key_text(ui_st: UiState, n: UiNode) -> string {
- if ui_st.ct_capture == n.key { return ui_tr(ui_st, "press a key...") }
- if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_st, ui_attr_text(n, "shown")) }
+function ct_key_text(n: UiNode) -> string {
+ if ct_capture == n.key { return ui_tr("press a key...") }
+ if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_attr_text(n, "shown")) }
let v = Value.as_int(ui_attr(n, "value"))
- if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(ui_st, v) }
+ if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(v) }
return Input.key_label(v)
}
# a key field is listening: the host should leave the keys alone until it is done (it stays true
# for the frame in which Esc or a key ended it, so that key is not read twice)
-export function ui_capturing(ui_st: UiState) -> bool { return ui_st.ct_capture != "" or ui_st.ct_capture_seen == ui_st.an_frame }
+export function ui_capturing() -> bool { return ct_capture != "" or ct_capture_seen == an_frame }
diff --git a/packages/ludic.ui/controls_key_mouse.ludic b/packages/ludic.ui/controls_key_mouse.ludic
index 6c8fc59f..7c021afe 100644
--- a/packages/ludic.ui/controls_key_mouse.ludic
+++ b/packages/ludic.ui/controls_key_mouse.ludic
@@ -4,19 +4,19 @@
export const UI_MOUSE_LEFT: int = 256
export const UI_MOUSE_RIGHT: int = 257
export const UI_MOUSE_MIDDLE: int = 258
-function ct_key_mouse(ui_st: mut UiState, root: UiNode) -> bool {
- if ui_st.ct_capture == "" or ui_st.ct_capture_at == ui_st.an_frame { return false }
- let b = pe_pressed(ui_st.in_now, ui_st.in_last)
+function ct_key_mouse(root: UiNode) -> bool {
+ if ct_capture == "" or ct_capture_at == an_frame { return false }
+ let b = pe_pressed(in_now, in_last)
if b < 0 { return false }
- let n = fr_node(root, ui_st.ct_capture)
- ui_st.ct_capture = ""
- ui_st.ct_capture_seen = ui_st.an_frame
- ui_st.fr_press_key = ""
- if n != null { ct_change(ui_st, n, Value.int(UI_MOUSE_LEFT + b)) }
+ let n = fr_node(root, ct_capture)
+ ct_capture = ""
+ ct_capture_seen = an_frame
+ fr_press_key = ""
+ if n != null { ct_change(n, Value.int(UI_MOUSE_LEFT + b)) }
return true
}
-function ct_mouse_name(ui_st: UiState, v: int) -> string {
- if v == UI_MOUSE_RIGHT { return ui_tr(ui_st, "Right click") }
- if v == UI_MOUSE_MIDDLE { return ui_tr(ui_st, "Middle click") }
- return ui_tr(ui_st, "Left click")
+function ct_mouse_name(v: int) -> string {
+ if v == UI_MOUSE_RIGHT { return ui_tr("Right click") }
+ if v == UI_MOUSE_MIDDLE { return ui_tr("Middle click") }
+ return ui_tr("Left click")
}
diff --git a/packages/ludic.ui/controls_number.ludic b/packages/ludic.ui/controls_number.ludic
index 0c782d32..4b78f80b 100644
--- a/packages/ludic.ui/controls_number.ludic
+++ b/packages/ludic.ui/controls_number.ludic
@@ -3,6 +3,8 @@
# the field commits, held between min and max; Backspace takes a digit back (of the value, when it is
# the first key), and the left and right arrows step it. Each commit is one on-change with the
# number: an int when step and the value are whole.
+var ct_num_key: string = ""
+var ct_num_text: string = ""
function ct_num_bound(n: UiNode, key: string) -> bool { return Value.kind(ui_attr(n, key)) != 0 }
function ct_num_step_of(n: UiNode) -> float {
let s = Value.as_float(ui_attr(n, "step"))
@@ -15,57 +17,57 @@ function ct_num_clamp(n: UiNode, v0: float) -> float {
if ct_num_bound(n, "min") { v = Math.max(v, Value.as_float(ui_attr(n, "min"))) }
return v
}
-function ct_num_set(ui_st: mut UiState, n: UiNode, v0: float) -> void {
+function ct_num_set(n: UiNode, v0: float) -> void {
let v = ct_num_clamp(n, v0)
let step = ct_num_step_of(n)
let was = ui_attr(n, "value")
if Value.kind(was) != 0 and Value.as_float(was) == v { return }
- if step == float(int(step)) and v == float(int(v)) { ct_change(ui_st, n, Value.int(int(v))) } else { ct_change(ui_st, n, Value.float(v)) }
+ if step == float(int(step)) and v == float(int(v)) { ct_change(n, Value.int(int(v))) } else { ct_change(n, Value.float(v)) }
}
# Enter, or the focus leaving it: the draft becomes the value (a draft of nothing changes nothing)
-function ct_num_commit(ui_st: mut UiState, n: UiNode) -> void {
- if ui_st.ct_num_key != n.key { return }
- let s = ui_st.ct_num_text
- ui_st.ct_num_key = ""
+function ct_num_commit(n: UiNode) -> void {
+ if ct_num_key != n.key { return }
+ let s = ct_num_text
+ ct_num_key = ""
if s == "" or s == "-" or s == "." or s == "-." { return }
- ct_num_set(ui_st, n, el_num(s))
+ ct_num_set(n, el_num(s))
}
-function ct_num_step(ui_st: mut UiState, n: UiNode, d: int) -> void {
+function ct_num_step(n: UiNode, d: int) -> void {
var base = Value.as_float(ui_attr(n, "value"))
- if ui_st.ct_num_key == n.key and ui_st.ct_num_text != "" and ui_st.ct_num_text != "-" { base = el_num(ui_st.ct_num_text) }
- ui_st.ct_num_key = ""
- ct_num_set(ui_st, n, base + ct_num_step_of(n) * float(d))
+ if ct_num_key == n.key and ct_num_text != "" and ct_num_text != "-" { base = el_num(ct_num_text) }
+ ct_num_key = ""
+ ct_num_set(n, base + ct_num_step_of(n) * float(d))
}
# what was typed this frame, the characters a number can hold; Backspace takes one back
-function ct_num_typed(ui_st: mut UiState, n: UiNode) -> void {
- if ui_st.fc_key != n.key or (ui_st.in_now.typed == "" and not ui_st.in_now.backspace) { return }
- if ui_st.ct_num_key != n.key {
- ui_st.ct_num_key = n.key
- ui_st.ct_num_text = ""
- if ui_st.in_now.typed == "" { ui_st.ct_num_text = ui_attr_text(n, "value") }
+function ct_num_typed(n: UiNode) -> void {
+ if fc_key != n.key or (in_now.typed == "" and not in_now.backspace) { return }
+ if ct_num_key != n.key {
+ ct_num_key = n.key
+ ct_num_text = ""
+ if in_now.typed == "" { ct_num_text = ui_attr_text(n, "value") }
}
- if ui_st.in_now.backspace and len(ui_st.ct_num_text) > 0 { ui_st.ct_num_text = ui_st.ct_num_text[0..len(ui_st.ct_num_text) - 1] }
+ if in_now.backspace and len(ct_num_text) > 0 { ct_num_text = ct_num_text[0..len(ct_num_text) - 1] }
let step = ct_num_step_of(n)
let lo = Value.as_float(ui_attr(n, "min"))
- for i in 0 .. len(ui_st.in_now.typed) {
- let c = ui_st.in_now.typed[i]
+ for i in 0 .. len(in_now.typed) {
+ let c = in_now.typed[i]
let digit = c >= 48 and c <= 57
- let minus = c == 45 and ui_st.ct_num_text == "" and (not ct_num_bound(n, "min") or lo < 0.0)
- let point = c == 46 and step != float(int(step)) and not ui_has(ui_st.ct_num_text, ".")
- if (digit or minus or point) and len(ui_st.ct_num_text) < 24 { ui_st.ct_num_text = ui_st.ct_num_text + ui_st.in_now.typed[i..i + 1] }
+ let minus = c == 45 and ct_num_text == "" and (not ct_num_bound(n, "min") or lo < 0.0)
+ let point = c == 46 and step != float(int(step)) and not ui_has(ct_num_text, ".")
+ if (digit or minus or point) and len(ct_num_text) < 24 { ct_num_text = ct_num_text + in_now.typed[i..i + 1] }
}
}
# the focus has gone from the field being typed in: what was typed is kept
-function ct_num_blur(ui_st: mut UiState, root: UiNode) -> void {
- if ui_st.ct_num_key == "" or ui_st.ct_num_key == ui_st.fc_key { return }
- let n = fr_node(root, ui_st.ct_num_key)
+function ct_num_blur(root: UiNode) -> void {
+ if ct_num_key == "" or ct_num_key == fc_key { return }
+ let n = fr_node(root, ct_num_key)
if n == null {
- ui_st.ct_num_key = ""
+ ct_num_key = ""
return
}
- ct_num_commit(ui_st, n)
+ ct_num_commit(n)
}
-function ct_num_shown(ui_st: UiState, n: UiNode) -> string {
- if ui_st.ct_num_key == n.key { return ui_st.ct_num_text }
+function ct_num_shown(n: UiNode) -> string {
+ if ct_num_key == n.key { return ct_num_text }
return ct_value_text(n)
}
diff --git a/packages/ludic.ui/controls_parts.ludic b/packages/ludic.ui/controls_parts.ludic
index e9bd0d41..1205681d 100644
--- a/packages/ludic.ui/controls_parts.ludic
+++ b/packages/ludic.ui/controls_parts.ludic
@@ -1,16 +1,16 @@
# controls_parts.ludic - what a control shows besides its own parts: its label, with a `note` (a line
# of help under it) in .ui-labels > .ui-label + .ui-note, and a range's or a number's value as text:
# `decimals="1"` places, `format="percent"` its place between min and max, and a `unit` after it
-function ct_label(ui_st: UiState, n: UiNode) -> void {
+function ct_label(n: UiNode) -> void {
let label = ui_attr_text(n, "label")
let note = ui_attr_text(n, "note")
if note == "" {
- if label != "" { push(n.children, ct_part(n, "ui-label", ui_tr(ui_st, label))) }
+ if label != "" { push(n.children, ct_part(n, "ui-label", ui_tr(label))) }
return
}
let both = ct_part(n, "ui-labels", "")
- if label != "" { push(both.children, ct_part(n, "ui-label", ui_tr(ui_st, label))) }
- push(both.children, ct_part(n, "ui-note", ui_tr(ui_st, note)))
+ if label != "" { push(both.children, ct_part(n, "ui-label", ui_tr(label))) }
+ push(both.children, ct_part(n, "ui-note", ui_tr(note)))
push(n.children, both)
}
function ct_value_text(n: UiNode) -> string {
@@ -46,7 +46,7 @@ function ct_fixed(f: float, places: int) -> string {
}
# Enter (or A) on a text field: on-submit with what it holds; the focus stays, and the text too
# unless it says clear-on-submit
-function ct_submit(ui_st: mut UiState, n: UiNode) -> void {
- ui_fire(ui_st, n, "submit", ui_attr(n, "value"))
- if ui_attr_on(n, "clear-on-submit") { ui_fire(ui_st, n, "change", Value.str("")) }
+function ct_submit(n: UiNode) -> void {
+ ui_fire(n, "submit", ui_attr(n, "value"))
+ if ui_attr_on(n, "clear-on-submit") { ui_fire(n, "change", Value.str("")) }
}
diff --git a/packages/ludic.ui/dev.ludic b/packages/ludic.ui/dev.ludic
index 2c74c1a1..ae187996 100644
--- a/packages/ludic.ui/dev.ludic
+++ b/packages/ludic.ui/dev.ludic
@@ -1,21 +1,24 @@
# dev.ludic - a developer's build: templates and stylesheets read again when they change (once a
# second), template errors drawn over the screen, and LUDIC_UI_DUMP= printing that screen's
# tree once, with every element's box and computed style
-export function ui_dev(ui_st: mut UiState, on: bool) -> void { ui_st.dv_on = on }
-function dv_before(ui_st: mut UiState) -> void {
- if not ui_st.dv_on { return }
- ui_st.dv_frames += 1
- if ui_st.dv_frames % 60 == 0 { ui_reload(ui_st) }
+var dv_on: bool = false
+var dv_frames: int = 0
+var dv_dumped: bool = false
+export function ui_dev(on: bool) -> void { dv_on = on }
+function dv_before() -> void {
+ if not dv_on { return }
+ dv_frames += 1
+ if dv_frames % 60 == 0 { ui_reload() }
}
-function dv_after(ui_st: mut UiState, screen: string, root: UiNode) -> void {
- if not ui_st.dv_on { return }
- if not ui_st.dv_dumped and Os.has_env("LUDIC_UI_DUMP") and Os.env("LUDIC_UI_DUMP") == screen {
- ui_st.dv_dumped = true
+function dv_after(screen: string, root: UiNode) -> void {
+ if not dv_on { return }
+ if not dv_dumped and Os.has_env("LUDIC_UI_DUMP") and Os.env("LUDIC_UI_DUMP") == screen {
+ dv_dumped = true
print(ui_dump(root, 0))
}
- if len(ui_st.ui_errs) == 0 or ui_st.ui_be == null or ui_st.ui_be.rect == null { return }
- let n = Math.min(len(ui_st.ui_errs), 8)
- let line = ui_px(ui_st, 26.0)
- ui_st.ui_be.rect(0.0, 0.0, 4000.0, line * float(n) + ui_px(ui_st, 8.0), 4194304, 0.85)
- for i in 0 .. n { ui_st.ui_be.text(ui_px(ui_st, 12.0), ui_px(ui_st, 6.0) + line * float(i), ui_st.ui_errs[len(ui_st.ui_errs) - n + i], ui_px(ui_st, 18.0), 16767449, 1.0) }
+ if len(ui_errs) == 0 or ui_be == null or ui_be.rect == null { return }
+ let n = Math.min(len(ui_errs), 8)
+ let line = ui_px(26.0)
+ ui_be.rect(0.0, 0.0, 4000.0, line * float(n) + ui_px(8.0), 4194304, 0.85)
+ for i in 0 .. n { ui_be.text(ui_px(12.0), ui_px(6.0) + line * float(i), ui_errs[len(ui_errs) - n + i], ui_px(18.0), 16767449, 1.0) }
}
diff --git a/packages/ludic.ui/draw.ludic b/packages/ludic.ui/draw.ludic
index f136efd1..7a6cb55b 100644
--- a/packages/ludic.ui/draw.ludic
+++ b/packages/ludic.ui/draw.ludic
@@ -1,83 +1,84 @@
# draw.ludic - the laid-out nodes through the renderer: the background (rounded by border-radius),
# the border, the content, the children in painting order (a scroll box's clipped to it, with its
# bar), then the outline, which is where a focus ring is drawn
-function fr_draw(ui_st: mut UiState, n: UiNode) -> void {
- let outer = ui_st.fr_ga
- ui_st.fr_ga = outer * n.alpha
- fr_draw_in(ui_st, n)
- ui_st.fr_ga = outer
+var fr_ga: float = 1.0 # the opacity of everything around this element, which it multiplies
+function fr_draw(n: UiNode) -> void {
+ let outer = fr_ga
+ fr_ga = outer * n.alpha
+ fr_draw_in(n)
+ fr_ga = outer
}
-function fr_draw_in(ui_st: mut UiState, n: UiNode) -> void {
+function fr_draw_in(n: UiNode) -> void {
if n.kind == UI_NATIVE {
n.native.draw(n)
return
}
- fr_background(ui_st, n)
- fr_border(ui_st, n)
- if n.kind == UI_TEXT { fr_text(ui_st, n) }
- if n.kind == UI_IMAGE and ui_st.ui_be.image != null { fr_image(ui_st, n) }
- let clipped = n.kind == UI_SCROLL and ui_st.ui_be.clip != null
- if clipped { fr_clip_push(ui_st, n.x, n.y, n.cw, n.ch) }
- if n.children != null { fr_children(ui_st, n) }
- if clipped { fr_clip_pop(ui_st) }
- if n.kind == UI_SCROLL { sc_bar(ui_st, n) }
- if n.outline_w > 0.0 { fr_outline(ui_st, n) }
+ fr_background(n)
+ fr_border(n)
+ if n.kind == UI_TEXT { fr_text(n) }
+ if n.kind == UI_IMAGE and ui_be.image != null { fr_image(n) }
+ let clipped = n.kind == UI_SCROLL and ui_be.clip != null
+ if clipped { fr_clip_push(n.x, n.y, n.cw, n.ch) }
+ if n.children != null { fr_children(n) }
+ if clipped { fr_clip_pop() }
+ if n.kind == UI_SCROLL { sc_bar(n) }
+ if n.outline_w > 0.0 { fr_outline(n) }
}
# the shadow, then the background: the colour (rounded), or a border-image's nine-slice - drawn as
# painted, as CSS draws one, or tinted by a background colour when there is one (so `transparent`,
# or any colour with no alpha, leaves nothing to see) - then a picture over it
-function fr_background(ui_st: mut UiState, n: UiNode) -> void {
- if n.sh_c >= 0 { fr_fill(ui_st, n, n.x + n.sh_x, n.y + n.sh_y, n.sh_c, ui_st.fr_ga * n.sh_a) }
+function fr_background(n: UiNode) -> void {
+ if n.sh_c >= 0 { fr_fill(n, n.x + n.sh_x, n.y + n.sh_y, n.sh_c, fr_ga * n.sh_a) }
if n.bg >= 0 or n.bi_src != "" {
var c = n.bg
- var a = ui_st.fr_ga * n.bg_a
+ var a = fr_ga * n.bg_a
if c < 0 {
c = 16777215
- a = ui_st.fr_ga
+ a = fr_ga
}
- if a > 0.0 { fr_fill(ui_st, n, n.x, n.y, c, a) }
+ if a > 0.0 { fr_fill(n, n.x, n.y, c, a) }
}
- if n.ex != null and n.ex.grad != null { gr_draw(ui_st, n.ex.grad, n.x, n.y, n.cw, n.ch, ui_st.fr_ga) }
- if n.bg_img != "" and ui_st.ui_be.image != null { ui_st.ui_be.image(n.bg_img, n.x, n.y, n.cw, n.ch, 16777215, ui_st.fr_ga) }
+ if n.ex != null and n.ex.grad != null { gr_draw(n.ex.grad, n.x, n.y, n.cw, n.ch, fr_ga) }
+ if n.bg_img != "" and ui_be.image != null { ui_be.image(n.bg_img, n.x, n.y, n.cw, n.ch, 16777215, fr_ga) }
}
-function fr_fill(ui_st: UiState, n: UiNode, x: float, y: float, c: int, a: float) -> void {
- if n.bi_src != "" and ui_st.ui_be.nine != null {
- ui_st.ui_be.nine(n.bi_src, n.bi_slice, x, y, n.cw, n.ch, n.bi_width, c, a)
+function fr_fill(n: UiNode, x: float, y: float, c: int, a: float) -> void {
+ if n.bi_src != "" and ui_be.nine != null {
+ ui_be.nine(n.bi_src, n.bi_slice, x, y, n.cw, n.ch, n.bi_width, c, a)
return
}
- fr_box(ui_st, x, y, n.cw, n.ch, n.radius, c, a)
+ fr_box(x, y, n.cw, n.ch, n.radius, c, a)
}
-function fr_box(ui_st: UiState, x: float, y: float, w: float, h: float, r: float, c: int, a: float) -> void {
- if r > 0.0 and ui_st.ui_be.round != null { ui_st.ui_be.round(x, y, w, h, r, c, a) } else if ui_st.ui_be.rect != null { ui_st.ui_be.rect(x, y, w, h, c, a) }
+function fr_box(x: float, y: float, w: float, h: float, r: float, c: int, a: float) -> void {
+ if r > 0.0 and ui_be.round != null { ui_be.round(x, y, w, h, r, c, a) } else if ui_be.rect != null { ui_be.rect(x, y, w, h, c, a) }
}
-function fr_border(ui_st: UiState, n: UiNode) -> void {
- if n.bt + n.br + n.bb + n.bl <= 0.0 or ui_st.ui_be.rect == null { return }
+function fr_border(n: UiNode) -> void {
+ if n.bt + n.br + n.bb + n.bl <= 0.0 or ui_be.rect == null { return }
var c = n.border_c
- var ca = ui_st.fr_ga * n.border_a
+ var ca = fr_ga * n.border_a
if c < 0 {
c = n.rfg
- ca = ui_st.fr_ga * n.rfg_a
+ ca = fr_ga * n.rfg_a
}
- if n.bt > 0.0 { ui_st.ui_be.rect(n.x, n.y, n.cw, n.bt, c, ca) }
- if n.bb > 0.0 { ui_st.ui_be.rect(n.x, n.y + n.ch - n.bb, n.cw, n.bb, c, ca) }
- if n.bl > 0.0 { ui_st.ui_be.rect(n.x, n.y + n.bt, n.bl, n.ch - n.bt - n.bb, c, ca) }
- if n.br > 0.0 { ui_st.ui_be.rect(n.x + n.cw - n.br, n.y + n.bt, n.br, n.ch - n.bt - n.bb, c, ca) }
+ if n.bt > 0.0 { ui_be.rect(n.x, n.y, n.cw, n.bt, c, ca) }
+ if n.bb > 0.0 { ui_be.rect(n.x, n.y + n.ch - n.bb, n.cw, n.bb, c, ca) }
+ if n.bl > 0.0 { ui_be.rect(n.x, n.y + n.bt, n.bl, n.ch - n.bt - n.bb, c, ca) }
+ if n.br > 0.0 { ui_be.rect(n.x + n.cw - n.br, n.y + n.bt, n.br, n.ch - n.bt - n.bb, c, ca) }
}
-function fr_frame(ui_st: UiState, x: float, y: float, w: float, h: float, b: float, c: int, a: float) -> void {
- ui_st.ui_be.rect(x, y, w, b, c, a)
- ui_st.ui_be.rect(x, y + h - b, w, b, c, a)
- ui_st.ui_be.rect(x, y + b, b, h - b * 2.0, c, a)
- ui_st.ui_be.rect(x + w - b, y + b, b, h - b * 2.0, c, a)
+function fr_frame(x: float, y: float, w: float, h: float, b: float, c: int, a: float) -> void {
+ ui_be.rect(x, y, w, b, c, a)
+ ui_be.rect(x, y + h - b, w, b, c, a)
+ ui_be.rect(x, y + b, b, h - b * 2.0, c, a)
+ ui_be.rect(x + w - b, y + b, b, h - b * 2.0, c, a)
}
# an outline follows the box's corners, as CSS's does: rounded by border-radius, grown by its offset
-function fr_outline(ui_st: UiState, n: UiNode) -> void {
+function fr_outline(n: UiNode) -> void {
var c = n.outline_c
if c < 0 { c = n.rfg }
let o = n.outline_off + n.outline_w
- let a = ui_st.fr_ga * n.outline_a
- if n.radius > 0.0 and ui_st.ui_be.ring != null {
- ui_st.ui_be.ring(n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.radius + o, n.outline_w, c, a)
+ let a = fr_ga * n.outline_a
+ if n.radius > 0.0 and ui_be.ring != null {
+ ui_be.ring(n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.radius + o, n.outline_w, c, a)
return
}
- if ui_st.ui_be.rect != null { fr_frame(ui_st, n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.outline_w, c, a) }
+ if ui_be.rect != null { fr_frame(n.x - o, n.y - o, n.cw + o * 2.0, n.ch + o * 2.0, n.outline_w, c, a) }
}
diff --git a/packages/ludic.ui/draw_content.ludic b/packages/ludic.ui/draw_content.ludic
index 83130d83..a0defe3e 100644
--- a/packages/ludic.ui/draw_content.ludic
+++ b/packages/ludic.ui/draw_content.ludic
@@ -1,23 +1,23 @@
# draw_content.ludic - what is inside a box drawn: a text's lines, placed by text-align, each over its
# text-shadow, and a picture
# a text's lines (one, or the wrapped ones), each placed by text-align, the last cut by an ellipsis
-function fr_text(ui_st: mut UiState, n: UiNode) -> void {
+function fr_text(n: UiNode) -> void {
let lh = ly_line_h(n)
let y = n.y + n.pt + n.bt + (lh - n.rsize * 1.25) / 2.0
- let a = n.rfg_a * ui_st.fr_ga
+ let a = n.rfg_a * fr_ga
let sh = fr_text_shadow(n)
if n.lines == null {
- var s = tf_fit(ui_st, n, n.text)
- if n.ellipsis { s = ly_ellipsis(ui_st, s, ui_st.tf_size, n.cw - ly_hin(n)) }
- fr_line(ui_st, n, sh, fr_line_x(ui_st, n, s, ui_st.tf_size), y, s, a, ui_st.tf_size)
+ var s = tf_fit(n, n.text)
+ if n.ellipsis { s = ly_ellipsis(s, tf_size, n.cw - ly_hin(n)) }
+ fr_line(n, sh, fr_line_x(n, s, tf_size), y, s, a, tf_size)
return
}
- for i in 0 .. len(n.lines) { fr_line(ui_st, n, sh, fr_line_x(ui_st, n, n.lines[i], n.rsize), y + float(i) * lh, n.lines[i], a, n.rsize) }
+ for i in 0 .. len(n.lines) { fr_line(n, sh, fr_line_x(n, n.lines[i], n.rsize), y + float(i) * lh, n.lines[i], a, n.rsize) }
}
# one line, over its shadow when it has one
-function fr_line(ui_st: UiState, n: UiNode, sh: UiNodeX, x: float, y: float, s: string, a: float, size: float) -> void {
- if sh != null { ui_st.ui_be.text(x + sh.tsh_x, y + sh.tsh_y, s, size, sh.tsh_c, sh.tsh_a * ui_st.fr_ga) }
- ui_st.ui_be.text(x, y, s, size, n.rfg, a)
+function fr_line(n: UiNode, sh: UiNodeX, x: float, y: float, s: string, a: float, size: float) -> void {
+ if sh != null { ui_be.text(x + sh.tsh_x, y + sh.tsh_y, s, size, sh.tsh_c, sh.tsh_a * fr_ga) }
+ ui_be.text(x, y, s, size, n.rfg, a)
}
# text-shadow is inherited: the nearest element that says it, or null (none)
function fr_text_shadow(n: UiNode) -> UiNodeX {
@@ -32,35 +32,35 @@ function fr_text_shadow(n: UiNode) -> UiNodeX {
return null
}
# the opacity a native's draw is at: every group opacity around it, its own included
-export function ui_opacity(ui_st: UiState) -> float { return ui_st.fr_ga }
+export function ui_opacity() -> float { return fr_ga }
# where a line starts: text-align within the room the box gives it
-function fr_line_x(ui_st: UiState, n: UiNode, s: string, size: float) -> float {
+function fr_line_x(n: UiNode, s: string, size: float) -> float {
let x = n.x + n.pl + n.bl
if n.talign == UI_START { return x }
- let spare = n.cw - ly_hin(n) - ly_text_w(ui_st, s, size)
+ let spare = n.cw - ly_hin(n) - ly_text_w(s, size)
if n.talign == UI_CENTER { return x + spare / 2.0 }
return x + spare
}
# an
: a picture file drawn as it is, as a browser draws one (unless the img sets its own
# color); a cell of an atlas ("icon:tent") is a glyph, and takes the color around it as text does
-function fr_image(ui_st: mut UiState, n: UiNode) -> void {
+function fr_image(n: UiNode) -> void {
var c = 16777215
- var a = ui_st.fr_ga
+ var a = fr_ga
if n.fg != UI_INHERIT or fr_is_cell(n.src) {
c = n.rfg
- a = ui_st.fr_ga * n.rfg_a
+ a = fr_ga * n.rfg_a
}
var nat_w = 0.0
var nat_h = 0.0
- if ui_st.ui_be.image_w != null and ui_st.ui_be.image_h != null {
- nat_w = ui_st.ui_be.image_w(n.src)
- nat_h = ui_st.ui_be.image_h(n.src)
+ if ui_be.image_w != null and ui_be.image_h != null {
+ nat_w = ui_be.image_w(n.src)
+ nat_h = ui_be.image_h(n.src)
}
let r = ui_object_fit(n, nat_w, nat_h)
- let clipped = ui_st.ui_be.clip != null and (r[2] > n.cw - ly_hin(n) or r[3] > n.ch - ly_vin(n))
- if clipped { fr_clip_push(ui_st, n.x + n.pl + n.bl, n.y + n.pt + n.bt, n.cw - ly_hin(n), n.ch - ly_vin(n)) }
- ui_st.ui_be.image(n.src, r[0], r[1], r[2], r[3], c, a)
- if clipped { fr_clip_pop(ui_st) }
+ let clipped = ui_be.clip != null and (r[2] > n.cw - ly_hin(n) or r[3] > n.ch - ly_vin(n))
+ if clipped { fr_clip_push(n.x + n.pl + n.bl, n.y + n.pt + n.bt, n.cw - ly_hin(n), n.ch - ly_vin(n)) }
+ ui_be.image(n.src, r[0], r[1], r[2], r[3], c, a)
+ if clipped { fr_clip_pop() }
}
# prefix:name, and not a path (C:/... is a drive, not an atlas)
function fr_is_cell(src: string) -> bool {
diff --git a/packages/ludic.ui/draw_gradient.ludic b/packages/ludic.ui/draw_gradient.ludic
index 3edd7163..d98fde69 100644
--- a/packages/ludic.ui/draw_gradient.ludic
+++ b/packages/ludic.ui/draw_gradient.ludic
@@ -8,7 +8,7 @@ export property UiGradient {
at: []float = null # each stop's place, 0 to 1
}
function gr_is(s: string) -> bool { return len(s) > 16 and s[0..16] == "linear-gradient(" }
-function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
+function gr_parse(s0: string) -> UiGradient {
let s = tpl_words(s0)
let parts = el_commas(s[16..len(s) - 1])
let g = new UiGradient
@@ -26,8 +26,8 @@ function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
at = el_num(last) / 100.0
col = tpl_words(col[0..len(col) - len(last)])
}
- push(g.cols, ui_color(ui_st, col))
- push(g.alphas, ui_st.cl_alpha)
+ push(g.cols, ui_color(col))
+ push(g.alphas, cl_alpha)
push(g.at, at)
}
for i in 0 .. len(g.at) {
@@ -46,8 +46,8 @@ function gr_dir(g: UiGradient, w: string) -> bool {
return true
}
# the gradient over the box, a band every two pixels at most
-function gr_draw(ui_st: mut UiState, g: UiGradient, x: float, y: float, w: float, h: float, a: float) -> void {
- if ui_st.ui_be.rect == null or len(g.cols) == 0 { return }
+function gr_draw(g: UiGradient, x: float, y: float, w: float, h: float, a: float) -> void {
+ if ui_be.rect == null or len(g.cols) == 0 { return }
let across = g.dir >= 2
var span = h
if across { span = w }
@@ -57,19 +57,20 @@ function gr_draw(ui_st: mut UiState, g: UiGradient, x: float, y: float, w: float
let e1 = Math.floor(span * float(i + 1) / float(bands) + 0.5)
var t = (e0 + e1) / 2.0 / span
if g.dir == 1 or g.dir == 3 { t = 1.0 - t }
- let c = gr_at(ui_st, g, t)
- if across { ui_st.ui_be.rect(x + e0, y, e1 - e0, h, c, ui_st.gr_a * a) } else { ui_st.ui_be.rect(x, y + e0, w, e1 - e0, c, ui_st.gr_a * a) }
+ let c = gr_at(g, t)
+ if across { ui_be.rect(x + e0, y, e1 - e0, h, c, gr_a * a) } else { ui_be.rect(x, y + e0, w, e1 - e0, c, gr_a * a) }
}
}
+var gr_a: float = 1.0
# the colour at `t` between the stops around it, its alpha in gr_a
-function gr_at(ui_st: mut UiState, g: UiGradient, t: float) -> int {
+function gr_at(g: UiGradient, t: float) -> int {
var k = 0
while k + 1 < len(g.at) and g.at[k + 1] < t { k += 1 }
let j = Math.min(k + 1, len(g.at) - 1)
var f = 0.0
if g.at[j] > g.at[k] { f = Math.clamp((t - g.at[k]) / (g.at[j] - g.at[k]), 0.0, 1.0) }
if t <= g.at[0] { f = 0.0 }
- ui_st.gr_a = g.alphas[k] + (g.alphas[j] - g.alphas[k]) * f
+ gr_a = g.alphas[k] + (g.alphas[j] - g.alphas[k]) * f
return cl_mix(g.cols[k], g.cols[j], f)
}
function cl_mix(a: int, b: int, f: float) -> int {
diff --git a/packages/ludic.ui/draw_order.ludic b/packages/ludic.ui/draw_order.ludic
index eab01678..15a4d7b5 100644
--- a/packages/ludic.ui/draw_order.ludic
+++ b/packages/ludic.ui/draw_order.ludic
@@ -1,34 +1,35 @@
# draw_order.ludic - painting order, and clipping that nests
-function fr_clip_push(ui_st: mut UiState, x: float, y: float, w: float, h: float) -> void {
+var fr_clips: []float = new []float
+function fr_clip_push(x: float, y: float, w: float, h: float) -> void {
var x0 = x
var y0 = y
var x1 = x + w
var y1 = y + h
- let n = len(ui_st.fr_clips)
+ let n = len(fr_clips)
if n >= 4 {
- x0 = Math.max(x0, ui_st.fr_clips[n - 4])
- y0 = Math.max(y0, ui_st.fr_clips[n - 3])
- x1 = Math.min(x1, ui_st.fr_clips[n - 2])
- y1 = Math.min(y1, ui_st.fr_clips[n - 1])
+ x0 = Math.max(x0, fr_clips[n - 4])
+ y0 = Math.max(y0, fr_clips[n - 3])
+ x1 = Math.min(x1, fr_clips[n - 2])
+ y1 = Math.min(y1, fr_clips[n - 1])
}
- push(ui_st.fr_clips, x0)
- push(ui_st.fr_clips, y0)
- push(ui_st.fr_clips, x1)
- push(ui_st.fr_clips, y1)
- ui_st.ui_be.clip(x0, y0, Math.max(x1 - x0, 0.0), Math.max(y1 - y0, 0.0))
+ push(fr_clips, x0)
+ push(fr_clips, y0)
+ push(fr_clips, x1)
+ push(fr_clips, y1)
+ ui_be.clip(x0, y0, Math.max(x1 - x0, 0.0), Math.max(y1 - y0, 0.0))
}
-function fr_clip_pop(ui_st: mut UiState) -> void {
+function fr_clip_pop() -> void {
var keep = new []float
- for i in 0 .. len(ui_st.fr_clips) - 4 { push(keep, ui_st.fr_clips[i]) }
- ui_st.fr_clips = keep
- let n = len(ui_st.fr_clips)
- if n >= 4 { ui_st.ui_be.clip(ui_st.fr_clips[n - 4], ui_st.fr_clips[n - 3], ui_st.fr_clips[n - 2] - ui_st.fr_clips[n - 4], ui_st.fr_clips[n - 1] - ui_st.fr_clips[n - 3]) } else if ui_st.ui_be.unclip != null { ui_st.ui_be.unclip() }
+ for i in 0 .. len(fr_clips) - 4 { push(keep, fr_clips[i]) }
+ fr_clips = keep
+ let n = len(fr_clips)
+ if n >= 4 { ui_be.clip(fr_clips[n - 4], fr_clips[n - 3], fr_clips[n - 2] - fr_clips[n - 4], fr_clips[n - 1] - fr_clips[n - 3]) } else if ui_be.unclip != null { ui_be.unclip() }
}
# the children in painting order: those in the flow first, then positioned ones by z-index
-function fr_children(ui_st: mut UiState, n: UiNode) -> void {
+function fr_children(n: UiNode) -> void {
var positioned = 0
for i in 0 .. len(n.children) {
- if n.children[i].popover { push(ui_st.pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(ui_st, n.children[i]) } else { positioned += 1 }
+ if n.children[i].popover { push(pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 }
}
if positioned == 0 { return }
var lo = 1000000
@@ -41,7 +42,7 @@ function fr_children(ui_st: mut UiState, n: UiNode) -> void {
}
for z in lo .. hi + 1 {
for i in 0 .. len(n.children) {
- if n.children[i].pos != 0 and not n.children[i].popover and n.children[i].z == z { fr_draw(ui_st, n.children[i]) }
+ if n.children[i].pos != 0 and not n.children[i].popover and n.children[i].z == z { fr_draw(n.children[i]) }
}
}
}
diff --git a/packages/ludic.ui/elem.ludic b/packages/ludic.ui/elem.ludic
index 1a7d4e76..e11b1094 100644
--- a/packages/ludic.ui/elem.ludic
+++ b/packages/ludic.ui/elem.ludic
@@ -2,8 +2,8 @@
# and attributes (kept for selectors, applied after every rule), its text, and its children
const EL_BOXES: string = " col div progress meter section header footer nav main article aside ul ol li form box "
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a option "
-function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode {
- ui_st.ui_at = k.where
+function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
+ ui_at = k.where
let n = new UiNode
n.key = key
n.tag = k.tag
@@ -15,34 +15,34 @@ function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode
n.akeys = new []string
n.avals = new []string
n.vals = new []Val
- el_kind(ui_st, n, k.tag)
- for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
- n.hovered = fr_was_hovered(ui_st, key)
+ el_kind(n, k.tag)
+ for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(k.vals[i], e)) }
+ n.hovered = fr_was_hovered(key)
n.on_names = k.on_keys
n.on_acts = k.on_acts
for i in 0 .. len(k.on_keys) {
if k.on_keys[i] == "press" { n.act = k.on_acts[i] }
}
- lc_note(ui_st, key, k.on_keys, k.on_acts, e)
- let was_no = ui_st.tr_no
+ lc_note(key, k.on_keys, k.on_acts, e)
+ let was_no = tr_no
let say = ui_attr_text(n, "translate")
- if say == "no" { ui_st.tr_no = true } else if say == "yes" { ui_st.tr_no = false }
- bd_kids(ui_st, k.kids, e, key, n.children)
- if k.text != null { el_text(n, ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))) }
+ if say == "no" { tr_no = true } else if say == "yes" { tr_no = false }
+ bd_kids(k.kids, e, key, n.children)
+ if k.text != null { el_text(n, ui_tr(ui_eval_text(k.text, e))) }
el_inline(n)
- if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(ui_st, n) }
- ui_st.tr_no = was_no
+ if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(n) }
+ tr_no = was_no
return n
}
-function el_kind(ui_st: mut UiState, n: UiNode, tag: string) -> void {
- let nv = nt_find(ui_st, tag)
+function el_kind(n: UiNode, tag: string) -> void {
+ let nv = nt_find(tag)
if nv != null {
n.kind = UI_NATIVE
n.native = nv
} else if lk_in(EL_TEXTS, tag) { n.kind = UI_TEXT } else if tag == "row" { n.dir = UI_ROW } else if tag == "button" {
n.dir = UI_ROW
n.focusable = true
- } else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(ui_st, `<{tag}>: there is no such element`) }
+ } else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(`<{tag}>: there is no such element`) }
}
# a box with words in it holds them as a text of its own, as words does
function el_text(n: UiNode, s: string) -> void {
diff --git a/packages/ludic.ui/env.ludic b/packages/ludic.ui/env.ludic
index 79214a21..ea35bee9 100644
--- a/packages/ludic.ui/env.ludic
+++ b/packages/ludic.ui/env.ludic
@@ -15,6 +15,8 @@ export property UiEnv {
ctx: UiEnv = null # what a above gave, seen through components too
inst: UiInst = null # the Ludic component whose template this is
}
+var ui_view: UiView = null
+var ui_errs: []string = new []string
function env_new(parent: UiEnv) -> UiEnv {
let e = new UiEnv
@@ -31,7 +33,7 @@ function env_bind(e: UiEnv, name: string, v: Val) -> void {
push(e.names, name)
push(e.vals, v)
}
-function env_get(ui_st: mut UiState, e: UiEnv, name: string) -> Val {
+function env_get(e: UiEnv, name: string) -> Val {
var at = e
while at != null {
var i = len(at.names) - 1
@@ -50,11 +52,11 @@ function env_get(ui_st: mut UiState, e: UiEnv, name: string) -> Val {
cx = cx.parent
}
if e.model != null and Value.has(e.model, name) != 0 { return Value.get(e.model, name) }
- ui_err(ui_st, `no {name} here: not a loop's, a prop, a state or in the view`)
+ ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
return Value.null()
}
# `set name = value`: the nearest component's state of that name, or that holds it
-function env_set(ui_st: mut UiState, e: UiEnv, name: string, v: Val) -> void {
+function env_set(e: UiEnv, name: string, v: Val) -> void {
var at = e
while at != null {
if at.inst != null {
@@ -68,17 +70,18 @@ function env_set(ui_st: mut UiState, e: UiEnv, name: string, v: Val) -> void {
}
at = at.parent
}
- ui_err(ui_st, `set {name}: no state here holds it - declare it with `)
+ ui_err(`set {name}: no state here holds it - declare it with `)
}
# an error is said once, however many frames make it, and names the template line being built
+var ui_at: string = ""
# said by the runtime, or by a host about something a template asked for
-export function ui_err(ui_st: mut UiState, msg0: string) -> void {
+export function ui_err(msg0: string) -> void {
var msg = msg0
- if ui_st.ui_at != "" and not ui_has(msg, ".xml:") { msg = `{ui_st.ui_at}: {msg}` }
- for i in 0 .. len(ui_st.ui_errs) {
- if ui_st.ui_errs[i] == msg { return }
+ if ui_at != "" and not ui_has(msg, ".xml:") { msg = `{ui_at}: {msg}` }
+ for i in 0 .. len(ui_errs) {
+ if ui_errs[i] == msg { return }
}
- push(ui_st.ui_errs, msg)
+ push(ui_errs, msg)
print(`ui: {msg}`)
}
function ui_has(s: string, part: string) -> bool {
@@ -89,4 +92,4 @@ function ui_has(s: string, part: string) -> bool {
return false
}
# every error said so far - for a host to show them where a developer will see them
-export function ui_errors(ui_st: UiState) -> []string { return ui_st.ui_errs }
+export function ui_errors() -> []string { return ui_errs }
diff --git a/packages/ludic.ui/eval.ludic b/packages/ludic.ui/eval.ludic
index 9b88cf47..2c757675 100644
--- a/packages/ludic.ui/eval.ludic
+++ b/packages/ludic.ui/eval.ludic
@@ -1,41 +1,41 @@
# eval.ludic - an expression's value, here
-function ui_eval(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
+function ui_eval(x: UiExpr, e: UiEnv) -> Val {
if x.op == X_LIT { return x.v }
- if x.op == X_NAME { return env_get(ui_st, e, x.s) }
+ if x.op == X_NAME { return env_get(e, x.s) }
if x.op == X_MEMBER {
- let o = ui_eval(ui_st, x.a, e)
+ let o = ui_eval(x.a, e)
if Value.kind(o) == 6 and Value.has(o, x.s) != 0 { return Value.get(o, x.s) }
if Value.kind(o) == 5 and x.s == "length" { return Value.int(Value.count(o)) }
- ui_err(ui_st, `.{x.s}: there is no such field`)
+ ui_err(`.{x.s}: there is no such field`)
return Value.null()
}
- if x.op == X_INDEX { return Value.at(ui_eval(ui_st, x.a, e), Value.as_int(ui_eval(ui_st, x.b, e))) }
- if x.op == X_CALL { return ev_call(ui_st, x, e) }
- if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(ui_st, x.a, e))) }
- if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(ui_st, x.a, e)) }
+ if x.op == X_INDEX { return Value.at(ui_eval(x.a, e), Value.as_int(ui_eval(x.b, e))) }
+ if x.op == X_CALL { return ev_call(x, e) }
+ if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(x.a, e))) }
+ if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(x.a, e)) }
if x.op == X_AND {
- if not vl_truthy(ui_eval(ui_st, x.a, e)) { return vl_bool(false) }
- return vl_bool(vl_truthy(ui_eval(ui_st, x.b, e)))
+ if not vl_truthy(ui_eval(x.a, e)) { return vl_bool(false) }
+ return vl_bool(vl_truthy(ui_eval(x.b, e)))
}
if x.op == X_OR {
- if vl_truthy(ui_eval(ui_st, x.a, e)) { return vl_bool(true) }
- return vl_bool(vl_truthy(ui_eval(ui_st, x.b, e)))
+ if vl_truthy(ui_eval(x.a, e)) { return vl_bool(true) }
+ return vl_bool(vl_truthy(ui_eval(x.b, e)))
}
if x.op == X_COND {
- if vl_truthy(ui_eval(ui_st, x.a, e)) { return ui_eval(ui_st, x.b, e) }
- return ui_eval(ui_st, x.c, e)
+ if vl_truthy(ui_eval(x.a, e)) { return ui_eval(x.b, e) }
+ return ui_eval(x.c, e)
}
if x.op == X_TEXT {
var out = ""
- for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(ui_st, x.args[i], e)) }
+ for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(x.args[i], e)) }
return Value.str(out)
}
- return vl_binary(x.s, ui_eval(ui_st, x.a, e), ui_eval(ui_st, x.b, e))
+ return vl_binary(x.s, ui_eval(x.a, e), ui_eval(x.b, e))
}
# a call: len() and range() are the template's own, anything else is the view's
-function ev_call(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
+function ev_call(x: UiExpr, e: UiEnv) -> Val {
let args = Value.list()
- for i in 0 .. len(x.args) { Value.add(args, ui_eval(ui_st, x.args[i], e)) }
+ for i in 0 .. len(x.args) { Value.add(args, ui_eval(x.args[i], e)) }
if x.s == "len" and len(x.args) == 1 { return Value.int(Value.count(Value.at(args, 0))) }
if x.s == "range" and len(x.args) == 1 {
let out = Value.list()
@@ -50,11 +50,11 @@ function ev_call(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
}
at = at.parent
}
- if ui_st.ui_view == null or ui_st.ui_view.call == null {
- ui_err(ui_st, `{x.s}(): there is no function or event of that name here`)
+ if ui_view == null or ui_view.call == null {
+ ui_err(`{x.s}(): there is no function or event of that name here`)
return Value.null()
}
- return ui_st.ui_view.call(x.s, args)
+ return ui_view.call(x.s, args)
}
-function ui_eval_text(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(ui_st, x, e)) }
-function ui_eval_bool(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(ui_st, x, e)) }
+function ui_eval_text(x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(x, e)) }
+function ui_eval_bool(x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(x, e)) }
diff --git a/packages/ludic.ui/focus.ludic b/packages/ludic.ui/focus.ludic
index c73e6c25..31d06728 100644
--- a/packages/ludic.ui/focus.ludic
+++ b/packages/ludic.ui/focus.ludic
@@ -1,72 +1,75 @@
# focus.ludic - the keyboard's place: focus moves through the controls in document order (Tab and
# Shift+Tab, or the arrows), Enter and Space activate what has it, and a pointer press focuses what
# it presses; a key field that is listening has every key to itself. A key that is gone from the screen leaves nothing focused.
-function fc_collect(ui_st: mut UiState, n: UiNode) -> void {
- if n.focusable and n.enabled { push(ui_st.fc_list, n) }
- for i in 0 .. len(n.children) { fc_collect(ui_st, n.children[i]) }
+var fc_key: string = ""
+var fc_visible: bool = false # focus came from the keyboard: :focus-visible, and the ring
+var fc_list: []UiNode = new []UiNode
+function fc_collect(n: UiNode) -> void {
+ if n.focusable and n.enabled { push(fc_list, n) }
+ for i in 0 .. len(n.children) { fc_collect(n.children[i]) }
}
-function fc_find(ui_st: UiState) -> int {
- for i in 0 .. len(ui_st.fc_list) {
- if ui_st.fc_list[i].key == ui_st.fc_key { return i }
+function fc_find() -> int {
+ for i in 0 .. len(fc_list) {
+ if fc_list[i].key == fc_key { return i }
}
return -1
}
-function fc_move(ui_st: mut UiState, step: int) -> void {
- if len(ui_st.fc_list) == 0 { return }
- var at = fc_find(ui_st)
+function fc_move(step: int) -> void {
+ if len(fc_list) == 0 { return }
+ var at = fc_find()
if at < 0 {
at = 0
- if step < 0 { at = len(ui_st.fc_list) - 1 }
- } else { at = (at + step + len(ui_st.fc_list)) % len(ui_st.fc_list) }
- ui_st.fc_key = ui_st.fc_list[at].key
- ui_st.fc_visible = true
+ if step < 0 { at = len(fc_list) - 1 }
+ } else { at = (at + step + len(fc_list)) % len(fc_list) }
+ fc_key = fc_list[at].key
+ fc_visible = true
}
# the keys, after the tree is laid out: moving, then acting on what has the focus
-function fc_keys(ui_st: mut UiState, root: UiNode) -> void {
- ui_st.fc_list = new []UiNode
+function fc_keys(root: UiNode) -> void {
+ fc_list = new []UiNode
let top = pp_top(root)
- if top != null { fc_collect(ui_st, top) } else { fc_collect(ui_st, root) }
- if ct_key_capture(ui_st, ui_st.fc_list) { return }
- if top != null and ui_st.in_now.escape { ui_fire(ui_st, top, "close", Value.null()) }
- let i = ui_st.in_now
- if fc_find(ui_st) < 0 { fc_autofocus(ui_st) }
- let at = fc_find(ui_st)
+ if top != null { fc_collect(top) } else { fc_collect(root) }
+ if ct_key_capture(fc_list) { return }
+ if top != null and in_now.escape { ui_fire(top, "close", Value.null()) }
+ let i = in_now
+ if fc_find() < 0 { fc_autofocus() }
+ let at = fc_find()
var n: UiNode = null
- if at >= 0 { n = ui_st.fc_list[at] }
- if i.tab and i.shift { fc_move(ui_st, -1) } else if i.tab { fc_move(ui_st, 1) }
+ if at >= 0 { n = fc_list[at] }
+ if i.tab and i.shift { fc_move(-1) } else if i.tab { fc_move(1) }
let horizontal = n != null and ct_takes_arrows(n)
- if i.up or (i.left and not horizontal) { fc_move(ui_st, -1) }
- if i.down_key or (i.right and not horizontal) { fc_move(ui_st, 1) }
- ct_num_blur(ui_st, root)
+ if i.up or (i.left and not horizontal) { fc_move(-1) }
+ if i.down_key or (i.right and not horizontal) { fc_move(1) }
+ ct_num_blur(root)
if n == null { return }
- fc_hold(ui_st, root, n)
- if i.enter or (i.space and not ct_takes_space(n)) { ct_activate(ui_st, n) }
- if horizontal and i.left { ct_step(ui_st, n, -1) }
- if horizontal and i.right { ct_step(ui_st, n, 1) }
- ct_typed(ui_st, n)
+ fc_hold(root, n)
+ if i.enter or (i.space and not ct_takes_space(n)) { ct_activate(n) }
+ if horizontal and i.left { ct_step(n, -1) }
+ if horizontal and i.right { ct_step(n, 1) }
+ ct_typed(n)
}
# Enter or A held on a control: its on-down, and on-up when let go (or the focus moves away)
-function fc_hold(ui_st: mut UiState, root: UiNode, n: UiNode) -> void {
- if ui_st.fr_key_press != "" and (ui_st.in_act_up or n.key != ui_st.fr_key_press) {
- let was = fr_node(root, ui_st.fr_key_press)
- if was != null { ui_fire(ui_st, was, "up", Value.null()) }
- ui_st.fr_key_press = ""
+function fc_hold(root: UiNode, n: UiNode) -> void {
+ if fr_key_press != "" and (in_act_up or n.key != fr_key_press) {
+ let was = fr_node(root, fr_key_press)
+ if was != null { ui_fire(was, "up", Value.null()) }
+ fr_key_press = ""
}
- if ui_st.in_act_down and n.enabled {
- ui_st.fr_key_press = n.key
- ui_fire(ui_st, n, "down", Value.null())
+ if in_act_down and n.enabled {
+ fr_key_press = n.key
+ ui_fire(n, "down", Value.null())
}
}
-export function ui_focus(ui_st: mut UiState, key: string) -> void {
- ui_st.fc_key = key
- ui_st.fc_visible = false
+export function ui_focus(key: string) -> void {
+ fc_key = key
+ fc_visible = false
}
# nothing focused (a screen just opened): the control that says autofocus, as HTML's does
-function fc_autofocus(ui_st: mut UiState) -> void {
- for i in 0 .. len(ui_st.fc_list) {
- if ui_attr_on(ui_st.fc_list[i], "autofocus") {
- ui_st.fc_key = ui_st.fc_list[i].key
- ui_st.fc_visible = false
+function fc_autofocus() -> void {
+ for i in 0 .. len(fc_list) {
+ if ui_attr_on(fc_list[i], "autofocus") {
+ fc_key = fc_list[i].key
+ fc_visible = false
return
}
}
diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic
index 8e9c6ecd..bfe61f2f 100644
--- a/packages/ludic.ui/frame.ludic
+++ b/packages/ludic.ui/frame.ludic
@@ -1,46 +1,46 @@
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
# run once the frame is drawn, so nothing the view does can change a frame half way through it
-export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
- in_read(ui_st)
- dv_before(ui_st)
- ui_st.an_frame += 1
- ui_viewport(ui_st, w, h)
- let root = ui_nodes(ui_st, screen, view)
+export function ui_show(screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
+ in_read()
+ dv_before()
+ an_frame += 1
+ ui_viewport(w, h)
+ let root = ui_nodes(screen, view)
if root == null { return }
- ui_place(ui_st, root, x, y, w, h)
- nt_fired_clear(ui_st)
+ ui_place(root, x, y, w, h)
+ nt_fired_clear()
sc_undo(root)
- sc_update(ui_st, root)
- fr_pointer(ui_st, root)
- fc_keys(ui_st, root)
- hd_tick(ui_st, root)
- tt_track(ui_st, root, ui_st.in_now.x, ui_st.in_now.y)
- fb_keep(ui_st, screen, root)
- if ui_st.ui_be != null and ui_st.ui_be.text != null {
- fr_draw(ui_st, root)
- pp_draw_top(ui_st)
- tt_draw(ui_st, root)
+ sc_update(root)
+ fr_pointer(root)
+ fc_keys(root)
+ hd_tick(root)
+ tt_track(root, in_now.x, in_now.y)
+ fb_keep(screen, root)
+ if ui_be != null and ui_be.text != null {
+ fr_draw(root)
+ pp_draw_top()
+ tt_draw(root)
}
- dv_after(ui_st, screen, root)
- lc_run(ui_st, screen)
- nt_fired_run(ui_st)
+ dv_after(screen, root)
+ lc_run(screen)
+ nt_fired_run()
}
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
-export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> UiNode {
- let si = lib_find(ui_st.lib_screen_names, screen)
- let cls = uc_find(ui_st, screen)
- if si < 0 and cls != null { return fr_class_nodes(ui_st, cls) }
+export function ui_nodes(screen: string, view: UiView) -> UiNode {
+ let si = lib_find(lib_screen_names, screen)
+ let cls = uc_find(screen)
+ if si < 0 and cls != null { return fr_class_nodes(cls) }
if si < 0 {
- ui_err(ui_st, `there is no screen {screen} - ui_load the file that has it`)
+ ui_err(`there is no screen {screen} - ui_load the file that has it`)
return null
}
- let t = ui_st.lib_screens[si]
- lc_begin(ui_st)
- ui_st.ui_view = view
+ let t = lib_screens[si]
+ lc_begin()
+ ui_view = view
let e = env_new(null)
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
- e.state = st_for(ui_st, screen, t, e)
+ e.state = st_for(screen, t, e)
let root = new UiNode
root.key = screen
root.children = new []UiNode
@@ -52,32 +52,32 @@ export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> Ui
root.vals = new []Val
root.tag = "screen"
for i in 0 .. len(t.keys) {
- if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) }
+ if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(t.vals[i], e)) }
}
- bd_kids(ui_st, t.kids, e, screen, root.children)
- cs_tree(ui_st, root)
+ bd_kids(t.kids, e, screen, root.children)
+ cs_tree(root)
return root
}
# the room vw, vh and @media measure against; ui_show sets it to the screen's rectangle
-export function ui_viewport(ui_st: mut UiState, w: float, h: float) -> void {
- ui_st.ly_vp_w = w
- ui_st.ly_vp_h = h
+export function ui_viewport(w: float, h: float) -> void {
+ ly_vp_w = w
+ ly_vp_h = h
}
-export function ui_place(ui_st: mut UiState, root: UiNode, x: float, y: float, w: float, h: float) -> void {
- ui_st.ly_vp_x = x
- ui_st.ly_vp_y = y
+export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -> void {
+ ly_vp_x = x
+ ly_vp_y = y
root.w = UI_FILL
root.h = UI_FILL
let fg = 15658734
- ui_st.ly_fg_a = 1.0
- ly_measure(ui_st, root, w, ui_px(ui_st, 16.0), fg)
- ly_place(ui_st, root, x, y, w, h)
- sc_apply(ui_st, root)
- pp_anchor_all(ui_st, root)
+ ly_fg_a = 1.0
+ ly_measure(root, w, ui_px(16.0), fg)
+ ly_place(root, x, y, w, h)
+ sc_apply(root)
+ pp_anchor_all(root)
}
# a press, as if the pointer had made it
-export function ui_press(ui_st: mut UiState, n: UiNode) -> void {
- if n.act != null and n.enabled { act_run(ui_st, n.act, n.env) }
+export function ui_press(n: UiNode) -> void {
+ if n.act != null and n.enabled { act_run(n.act, n.env) }
}
# any event, as if the element had fired it - for a test
-export function ui_event(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void { nt_run(ui_st, n, name, value) }
+export function ui_event(n: UiNode, name: string, value: Val) -> void { nt_run(n, name, value) }
diff --git a/packages/ludic.ui/frame_box.ludic b/packages/ludic.ui/frame_box.ludic
index d44682f1..2b67eb92 100644
--- a/packages/ludic.ui/frame_box.ludic
+++ b/packages/ludic.ui/frame_box.ludic
@@ -6,21 +6,22 @@ export property UiBox {
w: float = 0.0
h: float = 0.0
}
+var fb_roots: []UiNode = new []UiNode
# the screen pixels per design pixel: the renderer's scale (render3d's includes ui_render3d_scale)
-export function ui_scale(ui_st: UiState) -> float { return ui_px(ui_st, 1.0) }
-function fb_keep(ui_st: mut UiState, screen: string, root: UiNode) -> void {
- for i in 0 .. len(ui_st.fb_roots) {
- if ui_st.fb_roots[i].key == screen {
- ui_st.fb_roots[i] = root
+export function ui_scale() -> float { return ui_px(1.0) }
+function fb_keep(screen: string, root: UiNode) -> void {
+ for i in 0 .. len(fb_roots) {
+ if fb_roots[i].key == screen {
+ fb_roots[i] = root
return
}
}
- push(ui_st.fb_roots, root)
+ push(fb_roots, root)
}
# the box of the element with that id (or key) on any screen shown, in screen pixels; null if none
-export function ui_box(ui_st: UiState, id: string) -> UiBox {
- for i in 0 .. len(ui_st.fb_roots) {
- let n = fb_find(ui_st.fb_roots[i], id)
+export function ui_box(id: string) -> UiBox {
+ for i in 0 .. len(fb_roots) {
+ let n = fb_find(fb_roots[i], id)
if n != null {
let b = new UiBox
b.x = n.x
diff --git a/packages/ludic.ui/hold.ludic b/packages/ludic.ui/hold.ludic
index cf2dad52..e1601db3 100644
--- a/packages/ludic.ui/hold.ludic
+++ b/packages/ludic.ui/hold.ludic
@@ -1,25 +1,28 @@
# hold.ludic - on-hold: every frame a control is held down - by the pointer (wherever it goes until it
# is let go), or by Enter or the pad's A on the focus - with event.dt, the seconds since the last
# frame, and event.t, how long it has been held, on the ui clock
-function hd_tick(ui_st: mut UiState, root: UiNode) -> void {
- var key = ui_st.fr_key_press
- if ui_st.fr_press_key != "" and ui_st.in_now.down { key = ui_st.fr_press_key }
+var hd_key: string = ""
+var hd_t0: float = 0.0
+var hd_last: float = 0.0
+function hd_tick(root: UiNode) -> void {
+ var key = fr_key_press
+ if fr_press_key != "" and in_now.down { key = fr_press_key }
if key == "" {
- ui_st.hd_key = ""
+ hd_key = ""
return
}
- let now = an_now(ui_st)
- if key != ui_st.hd_key {
- ui_st.hd_key = key
- ui_st.hd_t0 = now
- ui_st.hd_last = now
+ let now = an_now()
+ if key != hd_key {
+ hd_key = key
+ hd_t0 = now
+ hd_last = now
}
let n = fr_node(root, key)
if n != null {
let v = Value.object()
- Value.put(v, "dt", Value.float(now - ui_st.hd_last))
- Value.put(v, "t", Value.float(now - ui_st.hd_t0))
- ui_fire(ui_st, n, "hold", v)
+ Value.put(v, "dt", Value.float(now - hd_last))
+ Value.put(v, "t", Value.float(now - hd_t0))
+ ui_fire(n, "hold", v)
}
- ui_st.hd_last = now
+ hd_last = now
}
diff --git a/packages/ludic.ui/input.ludic b/packages/ludic.ui/input.ludic
index 7aef8c9f..5a10ed83 100644
--- a/packages/ludic.ui/input.ludic
+++ b/packages/ludic.ui/input.ludic
@@ -28,15 +28,19 @@ export property UiInput {
down_right: bool = false # the other mouse buttons held
down_middle: bool = false
}
-function in_read(ui_st: mut UiState) -> void {
- if ui_st.in_given {
- ui_st.in_given = false
+var in_now: UiInput = new UiInput
+var in_was_down: bool = false
+var in_last: UiInput = new UiInput
+var in_given: bool = false
+function in_read() -> void {
+ if in_given {
+ in_given = false
return
}
- ui_st.in_was_down = ui_st.in_now.down
- ui_st.in_last = ui_st.in_now
+ in_was_down = in_now.down
+ in_last = in_now
let i = new UiInput
- if ui_st.ui_be != null and ui_st.ui_be.input {
+ if ui_be != null and ui_be.input {
i.x = float(Input.mouse_x())
i.y = float(Input.mouse_y())
i.down = Input.mouse_down(0)
@@ -57,8 +61,8 @@ function in_read(ui_st: mut UiState) -> void {
i.key = in_any_key()
in_held(i)
}
- in_repeat(ui_st, i)
- ui_st.in_now = i
+ in_repeat(i)
+ in_now = i
}
function in_any_key() -> int {
for k in 8 .. 167 {
@@ -67,10 +71,10 @@ function in_any_key() -> int {
return 0
}
# this frame's input, given by a test or a host that reads its own
-export function ui_input(ui_st: mut UiState, i: UiInput) -> void {
- ui_st.in_was_down = ui_st.in_now.down
- ui_st.in_last = ui_st.in_now
- in_repeat(ui_st, i)
- ui_st.in_now = i
- ui_st.in_given = true
+export function ui_input(i: UiInput) -> void {
+ in_was_down = in_now.down
+ in_last = in_now
+ in_repeat(i)
+ in_now = i
+ in_given = true
}
diff --git a/packages/ludic.ui/input_pad.ludic b/packages/ludic.ui/input_pad.ludic
index e88b4a56..faeca0eb 100644
--- a/packages/ludic.ui/input_pad.ludic
+++ b/packages/ludic.ui/input_pad.ludic
@@ -4,6 +4,12 @@
const RP_FIRST: float = 0.42
const RP_EVERY: float = 0.11
const PAD_STICK: float = 0.5
+var rp_next: []float = [-1.0, -1.0, -1.0, -1.0]
+var rp_a: bool = false
+var rp_b: bool = false
+var rp_act: bool = false
+var in_act_down: bool = false # A or Enter went down this frame, or came up
+var in_act_up: bool = false
# what is held this frame: the arrow keys, the d-pad (SDL's order: 11 up, 12 down, 13 left, 14
# right) and the stick past half way along its larger axis
function in_held(i: UiInput) -> void {
@@ -24,32 +30,32 @@ function in_held(i: UiInput) -> void {
i.pad_b = Input.pad_button(0, 1)
}
# the held directions and buttons as this frame's presses
-function in_repeat(ui_st: mut UiState, i: UiInput) -> void {
- let now = an_now(ui_st)
- if rp_fire(ui_st, 0, i.held_up, now) { i.up = true }
- if rp_fire(ui_st, 1, i.held_down, now) { i.down_key = true }
- if rp_fire(ui_st, 2, i.held_left, now) { i.left = true }
- if rp_fire(ui_st, 3, i.held_right, now) { i.right = true }
- if i.pad_a and not ui_st.rp_a { i.enter = true }
- if i.pad_b and not ui_st.rp_b { i.escape = true }
- ui_st.rp_a = i.pad_a
- ui_st.rp_b = i.pad_b
+function in_repeat(i: UiInput) -> void {
+ let now = an_now()
+ if rp_fire(0, i.held_up, now) { i.up = true }
+ if rp_fire(1, i.held_down, now) { i.down_key = true }
+ if rp_fire(2, i.held_left, now) { i.left = true }
+ if rp_fire(3, i.held_right, now) { i.right = true }
+ if i.pad_a and not rp_a { i.enter = true }
+ if i.pad_b and not rp_b { i.escape = true }
+ rp_a = i.pad_a
+ rp_b = i.pad_b
let act = i.pad_a or i.held_enter
- ui_st.in_act_down = act and not ui_st.rp_act
- ui_st.in_act_up = ui_st.rp_act and not act
- ui_st.rp_act = act
+ in_act_down = act and not rp_act
+ in_act_up = rp_act and not act
+ rp_act = act
}
-function rp_fire(ui_st: mut UiState, k: int, held: bool, now: float) -> bool {
+function rp_fire(k: int, held: bool, now: float) -> bool {
if not held {
- ui_st.rp_next[k] = -1.0
+ rp_next[k] = -1.0
return false
}
- if ui_st.rp_next[k] < 0.0 {
- ui_st.rp_next[k] = now + RP_FIRST
+ if rp_next[k] < 0.0 {
+ rp_next[k] = now + RP_FIRST
return true
}
- if now + 0.000001 < ui_st.rp_next[k] { return false }
- ui_st.rp_next[k] = ui_st.rp_next[k] + RP_EVERY
- if ui_st.rp_next[k] <= now { ui_st.rp_next[k] = now + RP_EVERY }
+ if now + 0.000001 < rp_next[k] { return false }
+ rp_next[k] = rp_next[k] + RP_EVERY
+ if rp_next[k] <= now { rp_next[k] = now + RP_EVERY }
return true
}
diff --git a/packages/ludic.ui/instance.ludic b/packages/ludic.ui/instance.ludic
index 57bbec41..dc9b7acf 100644
--- a/packages/ludic.ui/instance.ludic
+++ b/packages/ludic.ui/instance.ludic
@@ -6,11 +6,13 @@ property UiInst {
ptr: pointer = null
seen: int = 0
}
-function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
- for i in 0 .. len(ui_st.in_list) {
- let it = ui_st.in_list[i]
+var in_list: []UiInst = new []UiInst
+var in_build: int = 0
+function in_get(key: string, k: UiClass) -> UiInst {
+ for i in 0 .. len(in_list) {
+ let it = in_list[i]
if it.key == key and it.cls.name == k.name {
- it.seen = ui_st.in_build
+ it.seen = in_build
return it
}
}
@@ -18,16 +20,16 @@ function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
it.key = key
it.cls = k
it.ptr = k.make()
- it.seen = ui_st.in_build
- push(ui_st.in_list, it)
+ it.seen = in_build
+ push(in_list, it)
return it
}
# after a build of `screen`: its instances the build did not reach are unmounted
-function in_sweep(ui_st: mut UiState, screen: string) -> void {
+function in_sweep(screen: string) -> void {
let keep = new []UiInst
- for i in 0 .. len(ui_st.in_list) {
- let it = ui_st.in_list[i]
- if it.seen == ui_st.in_build or not lc_mine(it.key, screen) { push(keep, it) }
+ for i in 0 .. len(in_list) {
+ let it = in_list[i]
+ if it.seen == in_build or not lc_mine(it.key, screen) { push(keep, it) }
}
- ui_st.in_list = keep
+ in_list = keep
}
diff --git a/packages/ludic.ui/layout.ludic b/packages/ludic.ui/layout.ludic
index e5a9f5ee..395fe941 100644
--- a/packages/ludic.ui/layout.ludic
+++ b/packages/ludic.ui/layout.ludic
@@ -1,19 +1,19 @@
# layout.ludic - flex layout on the CSS box model, top-down: a box is told how wide it may be,
# measures its content at that width (so a wrapping row knows how tall it is), then places each
# child. A size is the border box: padding and border inside it, margin outside it.
-function ly_measure(ui_st: mut UiState, n: UiNode, avail: float, size: float, fg: int) -> void {
+function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
n.rsize = size * zm_own(n)
if n.size > 0.0 { n.rsize = n.size }
n.rfg = fg
- n.rfg_a = ui_st.ly_fg_a
+ n.rfg_a = ly_fg_a
if n.fg != UI_INHERIT {
n.rfg = n.fg
n.rfg_a = n.fg_a
}
- ui_st.ly_fg_a = n.rfg_a
+ ly_fg_a = n.rfg_a
n.mw = ly_hin(n)
n.mh = ly_vin(n)
- if n.kind == UI_TEXT { ly_measure_text(ui_st, n, avail) }
+ if n.kind == UI_TEXT { ly_measure_text(n, avail) }
if n.kind == UI_NATIVE and n.native.measure != null {
let base_w = n.mw
let base_h = n.mh
@@ -33,7 +33,7 @@ function ly_measure(ui_st: mut UiState, n: UiNode, avail: float, size: float, fg
var inner = avail - n.ml - n.mr
if fixed_w >= 0.0 { inner = fixed_w }
inner = ly_clamp(inner, n.min_w, n.max_w) - ly_hin(n)
- if n.dir == UI_ROW and n.wrap { ly_measure_wrap(ui_st, n, inner) } else { ly_measure_box(ui_st, n, inner) }
+ if n.dir == UI_ROW and n.wrap { ly_measure_wrap(n, inner) } else { ly_measure_box(n, inner) }
}
n.content_h = n.mh
if fixed_w >= 0.0 { n.mw = fixed_w }
@@ -43,6 +43,7 @@ function ly_measure(ui_st: mut UiState, n: UiNode, avail: float, size: float, fg
n.mh = ly_clamp(n.mh, n.min_h, n.max_h)
ly_min_note(n)
}
+var ly_fg_a: float = 1.0
function ly_hin(n: UiNode) -> float { return n.pl + n.pr + n.bl + n.br }
function ly_vin(n: UiNode) -> float { return n.pt + n.pb + n.bt + n.bb }
function ly_clamp(v: float, lo: float, hi: float) -> float {
diff --git a/packages/ludic.ui/layout_box.ludic b/packages/ludic.ui/layout_box.ludic
index 687022ca..de68db28 100644
--- a/packages/ludic.ui/layout_box.ludic
+++ b/packages/ludic.ui/layout_box.ludic
@@ -1,11 +1,11 @@
# layout_box.ludic - a box's content measured: its children in a line along its direction (a
# row's text wrapped into the room its siblings leave), or a wrapping row's lines
-function ly_measure_box(ui_st: mut UiState, n: UiNode, inner: float) -> void {
+function ly_measure_box(n: UiNode, inner: float) -> void {
for i in 0 .. len(n.children) {
- ui_st.ly_fg_a = n.rfg_a
- ly_measure(ui_st, n.children[i], inner, n.rsize, n.rfg)
+ ly_fg_a = n.rfg_a
+ ly_measure(n.children[i], inner, n.rsize, n.rfg)
}
- if n.dir == UI_ROW and inner > 0.0 { ly_row_fit(ui_st, n, inner) }
+ if n.dir == UI_ROW and inner > 0.0 { ly_row_fit(n, inner) }
var main = 0.0
var cross = 0.0
var count = 0
@@ -28,7 +28,7 @@ function ly_measure_box(ui_st: mut UiState, n: UiNode, inner: float) -> void {
}
# a row wider than its room: its texts (and boxes of their own width) measured again in the room the
# rest leave them, each giving up a share of the excess as wide as it is, so a line wraps beside an icon
-function ly_row_fit(ui_st: mut UiState, n: UiNode, inner: float) -> void {
+function ly_row_fit(n: UiNode, inner: float) -> void {
var used = 0.0
var flex = 0.0
var count = 0
@@ -46,8 +46,8 @@ function ly_row_fit(ui_st: mut UiState, n: UiNode, inner: float) -> void {
let c = n.children[i]
let share = ly_main(c, UI_ROW) - over * ly_main(c, UI_ROW) / flex
if ly_flexes(c) and share > 0.0 {
- ui_st.ly_fg_a = n.rfg_a
- ly_measure(ui_st, c, share, n.rsize, n.rfg)
+ ly_fg_a = n.rfg_a
+ ly_measure(c, share, n.rsize, n.rfg)
}
}
}
@@ -57,7 +57,7 @@ function ly_flexes(c: UiNode) -> bool {
return c.kind == UI_BOX and c.children != null and len(c.children) > 0
}
# a wrapping row: children in lines no wider than `inner`, the lines stacked `gap` apart
-function ly_measure_wrap(ui_st: mut UiState, n: UiNode, inner: float) -> void {
+function ly_measure_wrap(n: UiNode, inner: float) -> void {
var line_w = 0.0
var line_h = 0.0
var widest = 0.0
@@ -65,8 +65,8 @@ function ly_measure_wrap(ui_st: mut UiState, n: UiNode, inner: float) -> void {
var count = 0
for i in 0 .. len(n.children) {
let c = n.children[i]
- ui_st.ly_fg_a = n.rfg_a
- ly_measure(ui_st, c, inner, n.rsize, n.rfg)
+ ly_fg_a = n.rfg_a
+ ly_measure(c, inner, n.rsize, n.rfg)
if c.pos >= 2 { continue }
let cw = ly_main(c, UI_ROW)
if count > 0 and line_w + n.gap + cw > inner {
diff --git a/packages/ludic.ui/layout_run.ludic b/packages/ludic.ui/layout_run.ludic
index 7bcc2b51..ff12d114 100644
--- a/packages/ludic.ui/layout_run.ludic
+++ b/packages/ludic.ui/layout_run.ludic
@@ -1,6 +1,8 @@
# layout_run.ludic - a box's children given their places: along its direction by size, grow and
# justify, across it by align (or each child's own `self`), a wrapping row one line at a time
-function ly_place(ui_st: mut UiState, n: UiNode, x: float, y: float, w: float, h: float) -> void {
+var ly_lead: float = 0.0
+var ly_extra: float = 0.0
+function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
n.x = x
n.y = y
n.cw = w
@@ -10,9 +12,9 @@ function ly_place(ui_st: mut UiState, n: UiNode, x: float, y: float, w: float, h
var inner_h = h - ly_vin(n)
if n.kind == UI_SCROLL { inner_h = Math.max(n.content_h, h) - ly_vin(n) }
let top = n.y + n.pt + n.bt
- if n.dir == UI_ROW and n.wrap { ly_place_wrap(ui_st, n, top, inner_w) } else if n.dir == UI_ROW { ly_place_line(ui_st, n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(ui_st, n, 0, len(n.children), top, inner_h, inner_w) }
+ if n.dir == UI_ROW and n.wrap { ly_place_wrap(n, top, inner_w) } else if n.dir == UI_ROW { ly_place_line(n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(n, 0, len(n.children), top, inner_h, inner_w) }
for i in 0 .. len(n.children) {
- if n.children[i].pos >= 2 { ly_place_abs(ui_st, n, n.children[i]) }
+ if n.children[i].pos >= 2 { ly_place_abs(n, n.children[i]) }
}
}
function ly_fills(c: UiNode, dir: int) -> bool {
@@ -37,7 +39,7 @@ function ly_stretches(n: UiNode, c: UiNode) -> bool {
return a == UI_STRETCH
}
# children from..to along one line starting at `top`: sizes, the spare room, then each place
-function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
+function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
var used = 0.0
var grows = 0.0
var count = 0
@@ -53,21 +55,21 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
used = used + n.gap * float(Math.max(count - 1, 0))
let free = main_room - used
if free < 0.0 { ly_shrink_line(n, from, sizes, 0.0 - free) }
- ly_justify(ui_st, n, free, count, grows)
- let extra = ui_st.ly_extra
- var at = ui_st.ly_lead
+ ly_justify(n, free, count, grows)
+ let extra = ly_extra
+ var at = ly_lead
for i in from .. to {
let c = n.children[i]
if c.pos >= 2 { continue }
var m = sizes[i - from]
if grows > 0.0 and free > 0.0 { m = m + free * ly_grow(c, n.dir) / grows }
- ly_place_child(ui_st, n, c, at, top, m, cross_room)
+ ly_place_child(n, c, at, top, m, cross_room)
if c.pos == 1 { ly_nudge(n, c) }
at = at + m + n.gap + extra
}
}
# one child: `m` along the run and its own size (or the whole room, stretched) across, less margins
-function ly_place_child(ui_st: mut UiState, n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
+function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
var cr = ly_cross(c, n.dir)
if n.dir == UI_ROW and c.h_pct > 0.0 { cr = ly_ph(c, cross_room) + c.mt + c.mb }
if n.dir == UI_COL and c.w_pct > 0.0 { cr = ly_pw(c, cross_room) + c.ml + c.mr }
@@ -80,9 +82,9 @@ function ly_place_child(ui_st: mut UiState, n: UiNode, c: UiNode, at: float, top
let left = n.x + n.pl + n.bl
if n.dir == UI_ROW {
let cw = ly_clamp(m - c.ml - c.mr, c.min_w, c.max_w)
- ly_place(ui_st, c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
+ ly_place(c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
} else {
let chh = ly_clamp(m - c.mt - c.mb, c.min_h, c.max_h)
- ly_place(ui_st, c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
+ ly_place(c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
}
}
diff --git a/packages/ludic.ui/layout_text.ludic b/packages/ludic.ui/layout_text.ludic
index f69f2552..a44f913d 100644
--- a/packages/ludic.ui/layout_text.ludic
+++ b/packages/ludic.ui/layout_text.ludic
@@ -1,9 +1,9 @@
# layout_text.ludic - a text's size: one line, or as many as it takes to fit its room (white-space
# normal), breaking between words; nowrap keeps one line, and text-overflow: ellipsis cuts it
-function ly_measure_text(ui_st: UiState, n: UiNode, avail: float) -> void {
+function ly_measure_text(n: UiNode, avail: float) -> void {
n.lines = null
let lh = ly_line_h(n)
- var one = ly_text_w(ui_st, n.text, n.rsize)
+ var one = ly_text_w(n.text, n.rsize)
var room = avail - n.ml - n.mr - ly_hin(n)
if n.w >= 0.0 { room = n.w - ly_hin(n) }
let fits = n.ex != null and n.ex.fit_min > 0.0
@@ -13,13 +13,13 @@ function ly_measure_text(ui_st: UiState, n: UiNode, avail: float) -> void {
n.mh = n.mh + lh
return
}
- n.lines = ly_wrap(ui_st, n.text, n.rsize, room)
+ n.lines = ly_wrap(n.text, n.rsize, room)
var widest = 0.0
- for i in 0 .. len(n.lines) { widest = Math.max(widest, ly_text_w(ui_st, n.lines[i], n.rsize)) }
+ for i in 0 .. len(n.lines) { widest = Math.max(widest, ly_text_w(n.lines[i], n.rsize)) }
n.mw = n.mw + widest
n.mh = n.mh + lh * float(len(n.lines))
}
-function ly_wrap(ui_st: UiState, s: string, size: float, room: float) -> []string {
+function ly_wrap(s: string, size: float, room: float) -> []string {
let words = lss_split(s, 32)
let out = new []string
var line = ""
@@ -27,7 +27,7 @@ function ly_wrap(ui_st: UiState, s: string, size: float, room: float) -> []strin
if len(words[i]) == 0 { continue }
var next = words[i]
if len(line) > 0 { next = line + " " + words[i] }
- if len(line) > 0 and ly_text_w(ui_st, next, size) > room {
+ if len(line) > 0 and ly_text_w(next, size) > room {
push(out, line)
line = words[i]
} else { line = next }
@@ -36,14 +36,14 @@ function ly_wrap(ui_st: UiState, s: string, size: float, room: float) -> []strin
return out
}
# a line cut to `room` with an ellipsis, when it does not fit
-function ly_ellipsis(ui_st: UiState, s: string, size: float, room: float) -> string {
- if ly_text_w(ui_st, s, size) <= room { return s }
+function ly_ellipsis(s: string, size: float, room: float) -> string {
+ if ly_text_w(s, size) <= room { return s }
var n = len(s)
- while n > 0 and ly_text_w(ui_st, s[0..n] + "...", size) > room { n -= 1 }
+ while n > 0 and ly_text_w(s[0..n] + "...", size) > room { n -= 1 }
return s[0..n] + "..."
}
-function ly_text_w(ui_st: UiState, s: string, size: float) -> float {
- if ui_st.ui_be != null and ui_st.ui_be.measure != null { return ui_st.ui_be.measure(s, size) }
+function ly_text_w(s: string, size: float) -> float {
+ if ui_be != null and ui_be.measure != null { return ui_be.measure(s, size) }
return float(len(s)) * size * 0.5
}
# a child's size along and across a direction, margins included
diff --git a/packages/ludic.ui/layout_wrap.ludic b/packages/ludic.ui/layout_wrap.ludic
index fa086a1b..39430496 100644
--- a/packages/ludic.ui/layout_wrap.ludic
+++ b/packages/ludic.ui/layout_wrap.ludic
@@ -1,5 +1,5 @@
# layout_wrap.ludic - a wrapping row, line by line: each line is as tall as its tallest child
-function ly_place_wrap(ui_st: mut UiState, n: UiNode, top0: float, inner_w: float) -> void {
+function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
var top = top0
var from = 0
while from < len(n.children) {
@@ -16,23 +16,23 @@ function ly_place_wrap(ui_st: mut UiState, n: UiNode, top0: float, inner_w: floa
line_h = Math.max(line_h, ly_cross(n.children[to], UI_ROW))
to += 1
}
- ly_place_line(ui_st, n, from, to, top, inner_w, line_h)
+ ly_place_line(n, from, to, top, inner_w, line_h)
top = top + line_h + n.gap
from = to
}
}
# position: absolute (in the parent's padding box) or fixed (in the screen): the insets say where,
# and two opposite ones say how big; relative is placed as usual and moved by its insets
-function ly_place_abs(ui_st: mut UiState, n: UiNode, c: UiNode) -> void {
+function ly_place_abs(n: UiNode, c: UiNode) -> void {
var bx = n.x + n.bl
var by = n.y + n.bt
var bw = n.cw - n.bl - n.br
var bh = n.ch - n.bt - n.bb
if c.pos == 3 {
- bx = ui_st.ly_vp_x
- by = ui_st.ly_vp_y
- bw = ui_st.ly_vp_w
- bh = ui_st.ly_vp_h
+ bx = ly_vp_x
+ by = ly_vp_y
+ bw = ly_vp_w
+ bh = ly_vp_h
}
var w = c.mw
var h = c.mh
@@ -48,7 +48,7 @@ function ly_place_abs(ui_st: mut UiState, n: UiNode, c: UiNode) -> void {
if l != UI_AUTO { x = bx + l + c.ml } else if r != UI_AUTO { x = bx + bw - r - c.mr - w }
var y = by + n.pt + c.mt
if t != UI_AUTO { y = by + t + c.mt } else if b != UI_AUTO { y = by + bh - b - c.mb - h }
- ly_place(ui_st, c, x, y, w, h)
+ ly_place(c, x, y, w, h)
}
function ly_or0(v: float) -> float {
if v == UI_AUTO { return 0.0 }
@@ -60,19 +60,19 @@ function ly_offset(n: UiNode, dx: float, dy: float) -> void {
for i in 0 .. len(n.children) { ly_offset(n.children[i], dx, dy) }
}
# where the first child starts and what goes between, when nothing grows into the spare room
-function ly_justify(ui_st: mut UiState, n: UiNode, free: float, count: int, grows: float) -> void {
- ui_st.ly_lead = 0.0
- ui_st.ly_extra = 0.0
+function ly_justify(n: UiNode, free: float, count: int, grows: float) -> void {
+ ly_lead = 0.0
+ ly_extra = 0.0
if grows > 0.0 or free <= 0.0 or count == 0 { return }
- if n.justify == UI_CENTER { ui_st.ly_lead = free / 2.0 }
- if n.justify == UI_END { ui_st.ly_lead = free }
- if n.justify == UI_BETWEEN and count > 1 { ui_st.ly_extra = free / float(count - 1) }
+ if n.justify == UI_CENTER { ly_lead = free / 2.0 }
+ if n.justify == UI_END { ly_lead = free }
+ if n.justify == UI_BETWEEN and count > 1 { ly_extra = free / float(count - 1) }
if n.justify == UI_AROUND {
- ui_st.ly_extra = free / float(count)
- ui_st.ly_lead = ui_st.ly_extra / 2.0
+ ly_extra = free / float(count)
+ ly_lead = ly_extra / 2.0
}
if n.justify == UI_EVENLY {
- ui_st.ly_extra = free / float(count + 1)
- ui_st.ly_lead = ui_st.ly_extra
+ ly_extra = free / float(count + 1)
+ ly_lead = ly_extra
}
}
diff --git a/packages/ludic.ui/lib.ludic b/packages/ludic.ui/lib.ludic
index caf71545..665e40c9 100644
--- a/packages/ludic.ui/lib.ludic
+++ b/packages/ludic.ui/lib.ludic
@@ -14,48 +14,51 @@ export property UiFile {
src: string = "" # the text it was read from
disk: bool = false # read from a file (ui_reload looks again), or handed over as text
}
+var lib_files: []UiFile = new []UiFile
+var lib_screen_names: []string = new []string
+var lib_screens: []UiTpl = new []UiTpl
# the file a path names, read once
-export function ui_load(ui_st: mut UiState, path: string) -> bool { return lib_file(ui_st, path) != null }
-function lib_file(ui_st: mut UiState, path: string) -> UiFile {
- for i in 0 .. len(ui_st.lib_files) {
- if ui_st.lib_files[i].path == path { return ui_st.lib_files[i] }
+export function ui_load(path: string) -> bool { return lib_file(path) != null }
+function lib_file(path: string) -> UiFile {
+ for i in 0 .. len(lib_files) {
+ if lib_files[i].path == path { return lib_files[i] }
}
- let src = lib_text(ui_st, path)
+ let src = lib_text(path)
if len(src) == 0 {
- ui_err(ui_st, `{path}: cannot read it`)
+ ui_err(`{path}: cannot read it`)
return null
}
var f: UiFile = null
- if lss_is(path) { f = lss_file(ui_st, src, path) } else { f = lib_read(ui_st, src, path) }
+ if lss_is(path) { f = lss_file(src, path) } else { f = lib_read(src, path) }
f.disk = true
return f
}
# a file's text, as if read from `path` (its imports are found beside it)
-export function ui_load_text(ui_st: mut UiState, src: string, path: string) -> void {
- if lss_is(path) { lss_file(ui_st, src, path) } else { lib_read(ui_st, src, path) }
+export function ui_load_text(src: string, path: string) -> void {
+ if lss_is(path) { lss_file(src, path) } else { lib_read(src, path) }
}
-function lib_read(ui_st: mut UiState, src: string, path: string) -> UiFile {
- let f = lib_new(ui_st, path)
+function lib_read(src: string, path: string) -> UiFile {
+ let f = lib_new(path)
f.src = src
- tp_lines(ui_st, src)
- let root = tpl_read(ui_st, Xml.parse(src), path)
+ tp_lines(src)
+ let root = tpl_read(Xml.parse(src), path)
var tops = root.kids
if root.tag != "ui" { tops = [root] }
- for i in 0 .. len(tops) { lib_top(ui_st, f, tops[i]) }
+ for i in 0 .. len(tops) { lib_top(f, tops[i]) }
f.sheet = sheet_of(f)
- for i in 0 .. len(f.comps) { lib_link(ui_st, f, f.comps[i]) }
+ for i in 0 .. len(f.comps) { lib_link(f, f.comps[i]) }
for i in 0 .. len(tops) {
- if tops[i].tag == "screen" { lib_link(ui_st, f, tops[i]) }
+ if tops[i].tag == "screen" { lib_link(f, tops[i]) }
}
return f
}
-function lib_top(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
+function lib_top(f: UiFile, t: UiTpl) -> void {
if t.tag == "style" {
- sheet_read(ui_st, f, t)
+ sheet_read(f, t)
return
}
if t.tag == "import" {
- let other = lib_file(ui_st, lib_beside(f.path, bd_name(t, "src", "")))
+ let other = lib_file(lib_beside(f.path, bd_name(t, "src", "")))
if other != null {
push(f.imp, other)
push(f.imp_as, bd_name(t, "as", ""))
@@ -64,14 +67,14 @@ function lib_top(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
}
let name = bd_name(t, "name", "")
if name == "" or (t.tag != "component" and t.tag != "screen") {
- ui_err(ui_st, `{f.path}: <{t.tag}> - a file holds s,