diff --git a/packages/ludic.aim/pick.ludic b/packages/ludic.aim/pick.ludic index da86bfac..4bd61251 100644 --- a/packages/ludic.aim/pick.ludic +++ b/packages/ludic.aim/pick.ludic @@ -6,50 +6,55 @@ export const AIM_REACH_MIN: float = 3.0 # about as far as a hand goes with a export const AIM_USE_MAX: float = 10.0 # past this the crosshair is for looking, not doing export const AIM_NEVER: float = -1.0 # a reach that is never in reach (an animal without a camera) -var am_kind: int = 0 -var am_id: int = -1 -var am_t: float = 10000.0 -var am_dist: float = 0.0 -var am_reach: bool = false -var am_off: float = 1.0 +export state AimState { + am_kind: int = 0 + am_id: int = -1 + am_t: float = 10000.0 + am_dist: float = 0.0 + am_reach: bool = false + am_off: float = 1.0 + am_seen: int = 0 + am_blocked: int = 0 + am_disagree: int = 0 +} # The crosshair decides WHICH; the distance decides only WHETHER. Every reach is lifted to a floor of # AIM_REACH_MIN, because a per-kind reach chosen against its model was never chosen against standing # between two props and looking at one. export function aim_reach_of(reach: float) -> float { return Math.max(reach, AIM_REACH_MIN) } -export function aim_begin() -> void { - am_kind = AIM_NONE - am_id = -1 - am_t = 10000.0 - am_dist = 0.0 - am_reach = false - am_off = 1.0 +export function aim_begin(aim_st: mut AimState) -> void { + aim_st.am_kind = AIM_NONE + aim_st.am_id = -1 + aim_st.am_t = 10000.0 + aim_st.am_dist = 0.0 + aim_st.am_reach = false + aim_st.am_off = 1.0 } # A candidate of `kind` (the game's own, not AIM_NONE) and `id`: an upright cylinder at (x, z) from y0 # to y1 of radius r, within `far` of the body, in reach within `reach` (floored; AIM_NEVER never). # True when it is now the pick. -export function aim_offer(kind: int, id: int, x: float, y0: float, y1: float, z: float, r: float, reach: float, far: float) -> bool { +export function aim_offer(aim_st: mut AimState, kind: int, id: int, x: float, y0: float, y1: float, z: float, r: float, reach: float, far: float) -> bool { let d = aim_range(x, z) if d > far { return false } let rr = r + aim_slack_m(d) let hit = aim_ray_cyl(x, y0, y1, z, rr) - if hit < 0.0 or not (hit < am_t) { return false } + if hit < 0.0 or not (hit < aim_st.am_t) { return false } if not aim_clear_to(x, (y0 + y1) * 0.5, z) { return false } - am_t = hit - am_off = aim_miss(x, z, rr) - am_kind = kind - am_id = id - am_dist = d - am_reach = reach >= 0.0 and d < aim_reach_of(reach) + aim_st.am_t = hit + aim_st.am_off = aim_miss(x, z, rr) + aim_st.am_kind = kind + aim_st.am_id = id + aim_st.am_dist = d + aim_st.am_reach = reach >= 0.0 and d < aim_reach_of(reach) return true } -export function aim_kind() -> int { return am_kind } -export function aim_id() -> int { return am_id } -export function aim_dist() -> float { return am_dist } -export function aim_in_reach() -> bool { return am_reach } -export function aim_t() -> float { return am_t } +export function aim_kind(aim_st: AimState) -> int { return aim_st.am_kind } +export function aim_id(aim_st: AimState) -> int { return aim_st.am_id } +export function aim_dist(aim_st: AimState) -> float { return aim_st.am_dist } +export function aim_in_reach(aim_st: AimState) -> bool { return aim_st.am_reach } +export function aim_t(aim_st: AimState) -> float { return aim_st.am_t } # how well centred the pick is: 0 dead centre, 1 at its (forgiven) edge -export function aim_off_centre() -> float { return am_off } +export function aim_off_centre(aim_st: AimState) -> float { return aim_st.am_off } diff --git a/packages/ludic.aim/probe.ludic b/packages/ludic.aim/probe.ludic index e3303580..ed8e68c7 100644 --- a/packages/ludic.aim/probe.ludic +++ b/packages/ludic.aim/probe.ludic @@ -3,19 +3,16 @@ # the ground under each line against aim_clear_to. Lines the ground merely grazes are left out: that # is the probe's sampling against the aim's, not a disagreement. Moves the camera (AimView's) through # `look`, which the game binds to put its camera at a point. -var am_seen: int = 0 -var am_blocked: int = 0 -var am_disagree: int = 0 -export function aim_probe_seen() -> int { return am_seen } -export function aim_probe_blocked() -> int { return am_blocked } -export function aim_probe_disagree() -> int { return am_disagree } +export function aim_probe_seen(aim_st: AimState) -> int { return aim_st.am_seen } +export function aim_probe_blocked(aim_st: AimState) -> int { return aim_st.am_blocked } +export function aim_probe_disagree(aim_st: AimState) -> int { return aim_st.am_disagree } # true when at least one line is hidden and the aim disagrees with the ground on at most one in fifty -export function aim_probe_ground(px: float, pz: float, look: fn(float, float, float) -> void) -> bool { - am_seen = 0 - am_blocked = 0 - am_disagree = 0 +export function aim_probe_ground(aim_st: mut AimState, px: float, pz: float, look: fn(float, float, float) -> void) -> bool { + aim_st.am_seen = 0 + aim_st.am_blocked = 0 + aim_st.am_disagree = 0 for sp in 0 .. 9 { var sx = px var sz = pz @@ -24,12 +21,12 @@ export function aim_probe_ground(px: float, pz: float, look: fn(float, float, fl sx = px + Math.sin(sa) * 250.0 sz = pz + Math.cos(sa) * 250.0 } - if AimView.dry(sx, sz) { am_probe_from(sx, AimView.ground(sx, sz) + 1.6, sz, look) } + if AimView.dry(sx, sz) { am_probe_from(aim_st, sx, AimView.ground(sx, sz) + 1.6, sz, look) } } - return am_blocked >= 1 and not (am_disagree > am_seen / 50) + return aim_st.am_blocked >= 1 and not (aim_st.am_disagree > aim_st.am_seen / 50) } -function am_probe_from(sx: float, sy: float, sz: float, look: fn(float, float, float) -> void) -> void { +function am_probe_from(aim_st: mut AimState, sx: float, sy: float, sz: float, look: fn(float, float, float) -> void) -> void { for di in 0 .. 3 { var dist = 60.0 if di == 1 { dist = 120.0 } @@ -42,10 +39,10 @@ function am_probe_from(sx: float, sy: float, sz: float, look: fn(float, float, f let truth = am_probe_gap(sx, sy, sz, ex, ey, ez, 0.5, 10000.0) if am_probe_gap(sx, sy, sz, ex, ey, ez, -0.5, 0.5) and not truth { continue } look(sx, sy, sz) - am_seen += 1 + aim_st.am_seen += 1 let clear = aim_clear_to(ex, ey, ez) - if not clear { am_blocked += 1 } - if clear == truth { am_disagree += 1 } + if not clear { aim_st.am_blocked += 1 } + if clear == truth { aim_st.am_disagree += 1 } } } } diff --git a/packages/ludic.aim/tests/aim_test.ludic b/packages/ludic.aim/tests/aim_test.ludic index f315deed..7171e9c7 100644 --- a/packages/ludic.aim/tests/aim_test.ludic +++ b/packages/ludic.aim/tests/aim_test.ludic @@ -6,34 +6,36 @@ import "ludic.base" program AimTest { numbers float - var cx: float = 0.0 - var cy: float = 1.5 - var cz: float = 0.0 - var fy: float = 0.0 - function fk_x() -> float { return cx } - function fk_y() -> float { return cy } - function fk_z() -> float { return cz } - function fk_fy() -> float { return fy } - function fk_fz() -> float { return -Math.sqrt(1.0 - fy * fy) } + state AimtestState { + cx: float = 0.0 + cy: float = 1.5 + cz: float = 0.0 + fy: float = 0.0 + } + function fk_x(aimtest_st: AimtestState) -> float { return aimtest_st.cx } + function fk_y(aimtest_st: AimtestState) -> float { return aimtest_st.cy } + function fk_z(aimtest_st: AimtestState) -> float { return aimtest_st.cz } + function fk_fy(aimtest_st: AimtestState) -> float { return aimtest_st.fy } + function fk_fz(aimtest_st: AimtestState) -> float { return -Math.sqrt(1.0 - aimtest_st.fy * aimtest_st.fy) } # a 6 m ridge along z = -42, thirty metres across, the whole width of the view function fk_ground(x: float, z: float) -> float { return 6.0 * Math.max(0.0, 1.0 - Math.abs(z + 42.0) / 15.0) } - function fk_look(x: float, y: float, z: float) -> void { - cx = x - cy = y - cz = z + function fk_look(aimtest_st: mut AimtestState, x: float, y: float, z: float) -> void { + aimtest_st.cx = x + aimtest_st.cy = y + aimtest_st.cz = z } bind AimView { x: fn fk_x, y: fn fk_y, z: fn fk_z, fy: fn fk_fy, fz: fn fk_fz, ground: fn fk_ground } - function fresh() -> void { - cx = 0.0 - cy = 1.5 - cz = 0.0 - fy = 0.0 - aim_begin() + function fresh(aim_st: mut AimState, aimtest_st: mut AimtestState) -> void { + aimtest_st.cx = 0.0 + aimtest_st.cy = 1.5 + aimtest_st.cz = 0.0 + aimtest_st.fy = 0.0 + aim_begin(aim_st) } - test "the ray enters a cylinder in front and misses one to the side or behind" { - fresh() + test "the ray enters a cylinder in front and misses one to the side or behind" (aim_st: mut AimState, aimtest_st: mut AimtestState) { + fresh(aim_st, aimtest_st) expect_near(aim_ray_cyl(0.0, 0.0, 2.0, -10.0, 0.5), 9.5, 0.01) expect(aim_ray_cyl(3.0, 0.0, 2.0, -10.0, 0.5) < 0.0) expect(aim_ray_cyl(0.0, 0.0, 2.0, 10.0, 0.5) < 0.0) @@ -41,61 +43,61 @@ program AimTest { expect_near(aim_ray_cyl(0.0, 0.0, 2.0, 0.0, 0.5), 0.0, 0.001) } - test "a thin thing far off is still aimable, by an angle and not a metre" { - fresh() + test "a thin thing far off is still aimable, by an angle and not a metre" (aim_st: mut AimState, aimtest_st: mut AimtestState) { + fresh(aim_st, aimtest_st) expect(aim_slack_m(20.0) > aim_slack_m(5.0) * 3.9) - expect(aim_offer(1, 7, 0.25, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0)) - expect(aim_off_centre() > 0.5) - fresh() - expect(not aim_offer(1, 7, 3.0, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0)) + expect(aim_offer(aim_st, 1, 7, 0.25, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0)) + expect(aim_off_centre(aim_st) > 0.5) + fresh(aim_st, aimtest_st) + expect(not aim_offer(aim_st, 1, 7, 3.0, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0)) } - test "the nearest thing the ray enters is the pick, whatever order they are offered in" { - fresh() - expect(aim_offer(1, 1, 0.0, 0.0, 2.0, -20.0, 0.5, 1.5, 30.0)) - expect(aim_offer(1, 2, 0.0, 0.0, 2.0, -8.0, 0.5, 1.5, 30.0)) - expect(not aim_offer(1, 3, 0.0, 0.0, 2.0, -15.0, 0.5, 1.5, 30.0)) - expect_eq(aim_id(), 2) - expect_near(aim_dist(), 8.0, 0.01) - expect(not aim_offer(2, 4, 0.0, 0.0, 2.0, -25.0, 0.5, 1.5, 30.0)) - expect(not aim_offer(2, 5, 0.0, 0.0, 2.0, -3.0, 0.5, 1.5, 2.0)) + test "the nearest thing the ray enters is the pick, whatever order they are offered in" (aim_st: mut AimState, aimtest_st: mut AimtestState) { + fresh(aim_st, aimtest_st) + expect(aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -20.0, 0.5, 1.5, 30.0)) + expect(aim_offer(aim_st, 1, 2, 0.0, 0.0, 2.0, -8.0, 0.5, 1.5, 30.0)) + expect(not aim_offer(aim_st, 1, 3, 0.0, 0.0, 2.0, -15.0, 0.5, 1.5, 30.0)) + expect_eq(aim_id(aim_st), 2) + expect_near(aim_dist(aim_st), 8.0, 0.01) + expect(not aim_offer(aim_st, 2, 4, 0.0, 0.0, 2.0, -25.0, 0.5, 1.5, 30.0)) + expect(not aim_offer(aim_st, 2, 5, 0.0, 0.0, 2.0, -3.0, 0.5, 1.5, 2.0)) } - test "the ground in front hides a thing, and nothing within arm's length is hidden" { - fresh() + test "the ground in front hides a thing, and nothing within arm's length is hidden" (aim_st: mut AimState, aimtest_st: mut AimtestState) { + fresh(aim_st, aimtest_st) expect(not aim_clear_to(0.0, 1.0, -60.0)) expect(aim_clear_to(0.0, 1.0, -30.0)) - expect(not aim_offer(1, 1, 0.0, 0.0, 2.0, -60.0, 2.0, 1.5, 80.0)) - cy = -5.0 + expect(not aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -60.0, 2.0, 1.5, 80.0)) + aimtest_st.cy = -5.0 expect(aim_clear_to(0.0, -5.0, -2.0)) } - test "the crosshair decides which, the distance whether: every reach is floored at three metres" { - fresh() + test "the crosshair decides which, the distance whether: every reach is floored at three metres" (aim_st: mut AimState, aimtest_st: mut AimtestState) { + fresh(aim_st, aimtest_st) expect_near(aim_reach_of(1.5), AIM_REACH_MIN, 0.001) expect_near(aim_reach_of(3.4), 3.4, 0.001) - aim_offer(1, 1, 0.0, 0.0, 2.0, -2.6, 0.3, 1.5, 30.0) - expect(aim_in_reach()) - expect_eq(aim_use(1), AIM_USE_THIS) - fresh() - aim_offer(1, 1, 0.0, 0.0, 2.0, -6.0, 0.3, 1.5, 30.0) - expect(not aim_in_reach()) - expect_eq(aim_use(1), AIM_USE_NOTHING) - expect_eq(aim_use(3), AIM_USE_NEAREST) - fresh() - aim_offer(1, 1, 0.0, 0.0, 3.0, -20.0, 0.3, 1.5, 30.0) - expect_eq(aim_use(1), AIM_USE_NEAREST) - fresh() - aim_offer(2, 9, 0.0, 0.0, 2.0, -2.0, 0.3, AIM_NEVER, 30.0) - expect(not aim_in_reach()) - expect_eq(aim_kind(), 2) + aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -2.6, 0.3, 1.5, 30.0) + expect(aim_in_reach(aim_st)) + expect_eq(aim_use(aim_st, 1), AIM_USE_THIS) + fresh(aim_st, aimtest_st) + aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -6.0, 0.3, 1.5, 30.0) + expect(not aim_in_reach(aim_st)) + expect_eq(aim_use(aim_st, 1), AIM_USE_NOTHING) + expect_eq(aim_use(aim_st, 3), AIM_USE_NEAREST) + fresh(aim_st, aimtest_st) + aim_offer(aim_st, 1, 1, 0.0, 0.0, 3.0, -20.0, 0.3, 1.5, 30.0) + expect_eq(aim_use(aim_st, 1), AIM_USE_NEAREST) + fresh(aim_st, aimtest_st) + aim_offer(aim_st, 2, 9, 0.0, 0.0, 2.0, -2.0, 0.3, AIM_NEVER, 30.0) + expect(not aim_in_reach(aim_st)) + expect_eq(aim_kind(aim_st), 2) } - test "the probe finds the bank and agrees with the ground about every line it hides" { - fresh() - expect(aim_probe_ground(0.0, 0.0, fn fk_look)) - expect(aim_probe_seen() > 100) - expect(aim_probe_blocked() > 0) - expect(not (aim_probe_disagree() > aim_probe_seen() / 50)) + test "the probe finds the bank and agrees with the ground about every line it hides" (aim_st: mut AimState, aimtest_st: mut AimtestState) { + fresh(aim_st, aimtest_st) + expect(aim_probe_ground(aim_st, 0.0, 0.0, fn fk_look)) + expect(aim_probe_seen(aim_st) > 100) + expect(aim_probe_blocked(aim_st) > 0) + expect(not (aim_probe_disagree(aim_st) > aim_probe_seen(aim_st) / 50)) } } diff --git a/packages/ludic.aim/use.ludic b/packages/ludic.aim/use.ludic index d886db3a..43e2c447 100644 --- a/packages/ludic.aim/use.ludic +++ b/packages/ludic.aim/use.ludic @@ -8,8 +8,8 @@ export const AIM_USE_THIS: int = 0 export const AIM_USE_NOTHING: int = 1 export const AIM_USE_NEAREST: int = 2 -export function aim_use(kind: int) -> int { - if am_kind == kind and am_reach { return AIM_USE_THIS } - if am_kind == kind and am_dist < AIM_USE_MAX { return AIM_USE_NOTHING } +export function aim_use(aim_st: AimState, kind: int) -> int { + if aim_st.am_kind == kind and aim_st.am_reach { return AIM_USE_THIS } + if aim_st.am_kind == kind and aim_st.am_dist < AIM_USE_MAX { return AIM_USE_NOTHING } return AIM_USE_NEAREST } diff --git a/packages/ludic.anim/clip.ludic b/packages/ludic.anim/clip.ludic index 27c37b98..73150678 100644 --- a/packages/ludic.anim/clip.ludic +++ b/packages/ludic.anim/clip.ludic @@ -16,37 +16,47 @@ export property Clip { vals: floats } -var clips: []Clip = null +export state AnimState { + clips: []Clip = null + positions: bool = false + q_a: floats = null + q_b: floats = null + q_cur: floats = null + parts: []floats = null + rot_s: floats = null + pos_s: floats = null + hit_s: words = null + cap: int = 0 +} # Keyed translations are off unless asked for: baking keys location on every bone, and a skeleton # does not translate its joints, so applying them turns rounding into a pose. -var positions: bool = false -function clips_init() -> void { - if clips == null { clips = new []Clip } +function clips_init(anim_st: mut AnimState) -> void { + if anim_st.clips == null { anim_st.clips = new []Clip } } # the clip of that name, or null; names are per file (per body, per species), never shared -export function anim_find(name: string) -> Clip { - clips_init() - for i in 0 .. len(clips) { if clips[i].name == name { return clips[i] } } +export function anim_find(anim_st: mut AnimState, name: string) -> Clip { + clips_init(anim_st) + for i in 0 .. len(anim_st.clips) { if anim_st.clips[i].name == name { return anim_st.clips[i] } } return null } -export function anim_count() -> int { - clips_init() - return len(clips) +export function anim_count(anim_st: mut AnimState) -> int { + clips_init(anim_st) + return len(anim_st.clips) } # a clip made outside a glTF (a test, a generated clip); a name already held is refused -export function anim_add(c: Clip) -> bool { - if c == null or anim_find(c.name) != null { return false } - push(clips, c) +export function anim_add(anim_st: mut AnimState, c: Clip) -> bool { + if c == null or anim_find(anim_st, c.name) != null { return false } + push(anim_st.clips, c) return true } -export function anim_clear() -> void { clips = new []Clip } -export function anim_set_positions(on: bool) -> void { positions = on } -export function anim_positions() -> bool { return positions } +export function anim_clear(anim_st: mut AnimState) -> void { anim_st.clips = new []Clip } +export function anim_set_positions(anim_st: mut AnimState, on: bool) -> void { anim_st.positions = on } +export function anim_positions(anim_st: AnimState) -> bool { return anim_st.positions } # Which key pair straddles time t, by halving rather than walking: a grazing clip is three # hundred keys long and a valley can have dozens of animals posed in a frame. diff --git a/packages/ludic.anim/mix.ludic b/packages/ludic.anim/mix.ludic index 825c12d7..829996ca 100644 --- a/packages/ludic.anim/mix.ludic +++ b/packages/ludic.anim/mix.ludic @@ -1,16 +1,13 @@ # mix.ludic - sample one clip at a time into per-node buffers, mixed in at a weight. `rot` is 4 per # node, `pos` 3 per node, and `hit` says which nodes have received a value (bit 0 a rotation, bit 1 # a translation), so a node the clips never mention stays at rest -var q_a: floats = null -var q_b: floats = null -var q_cur: floats = null -export function anim_mix(c: Clip, sk: Skin, t: float, w: float, rot: floats, pos: floats, hit: words) -> void { +export function anim_mix(anim_st: mut AnimState, c: Clip, sk: Skin, t: float, w: float, rot: floats, pos: floats, hit: words) -> void { if c == null or sk == null { return } - if q_a == null { - q_a = floats(4) - q_b = floats(4) - q_cur = floats(4) + if anim_st.q_a == null { + anim_st.q_a = floats(4) + anim_st.q_b = floats(4) + anim_st.q_cur = floats(4) } var tt = t if c.dur > 0.0 { tt = t % c.dur } @@ -27,27 +24,27 @@ export function anim_mix(c: Clip, sk: Skin, t: float, w: float, rot: floats, pos let span = c.times[o + i1] - c.times[o + i0] if span > 0.000001 { f = Math.clamp((tt - c.times[o + i0]) / span, 0.0, 1.0) } } - if c.cpath[ch] == AC_ROT { mix_rot(c, ch, node, i0, i1, f, w, rot, hit) } else { mix_pos(c, ch, node, i0, i1, f, w, pos, hit) } + if c.cpath[ch] == AC_ROT { mix_rot(anim_st, c, ch, node, i0, i1, f, w, rot, hit) } else { mix_pos(c, ch, node, i0, i1, f, w, pos, hit) } } } # Across clips the first contributor lands and the rest are nlerped onto it by their share, which # is what makes a cross-fade rather than a jump. Test the ROTATION bit, not the whole word: glTF # writes a node's translation before its rotation, and `hit == 0` then skipped every rotation. -function mix_rot(c: Clip, ch: int, node: int, i0: int, i1: int, f: float, w: float, rot: floats, hit: words) -> void { +function mix_rot(anim_st: mut AnimState, c: Clip, ch: int, node: int, i0: int, i1: int, f: float, w: float, rot: floats, hit: words) -> void { let vo = c.cvoff[ch] for e in 0 .. 4 { - q_a[e] = c.vals[vo + i0 * 4 + e] - q_b[e] = c.vals[vo + i1 * 4 + e] + anim_st.q_a[e] = c.vals[vo + i0 * 4 + e] + anim_st.q_b[e] = c.vals[vo + i1 * 4 + e] } - q_nlerp(q_a, q_a, q_b, f) + q_nlerp(anim_st.q_a, anim_st.q_a, anim_st.q_b, f) if (hit[node] & 1) == 0 { - q_store(rot, node, q_a) + q_store(rot, node, anim_st.q_a) hit[node] = hit[node] | 1 } else { - q_load(q_cur, rot, node) - q_nlerp(q_cur, q_cur, q_a, w) - q_store(rot, node, q_cur) + q_load(anim_st.q_cur, rot, node) + q_nlerp(anim_st.q_cur, anim_st.q_cur, anim_st.q_a, w) + q_store(rot, node, anim_st.q_cur) } } diff --git a/packages/ludic.anim/pose.ludic b/packages/ludic.anim/pose.ludic index a96f6c92..427eb8f8 100644 --- a/packages/ludic.anim/pose.ludic +++ b/packages/ludic.anim/pose.ludic @@ -1,28 +1,23 @@ # pose.ludic - the sampled LOCAL transforms turned into the model-frame deltas `skin_pose` wants. # skin_pose folds a delta D in as local = (G_p^-1 . D . G_p) . R_rest, and a channel gives the # absolute local L, so D = G_p . (L . R_rest^-1) . G_p^-1 (a root: D = L . R_rest^-1) -var parts: []floats = null -var rot_s: floats = null -var pos_s: floats = null -var hit_s: words = null -var cap: int = 0 -function part(i: int) -> floats { - if parts == null { - parts = new []floats - for k in 0 .. 8 { push(parts, floats(4)) } +function part(anim_st: mut AnimState, i: int) -> floats { + if anim_st.parts == null { + anim_st.parts = new []floats + for k in 0 .. 8 { push(anim_st.parts, floats(4)) } } - return parts[i] + return anim_st.parts[i] } -export function anim_to_pose(sk: Skin, rot: floats, pos: floats, hit: words) -> void { - let l = part(0) - let rinv = part(1) - let gp = part(2) - let gpi = part(3) - let tmp = part(4) - let d = part(5) - let v = part(6) +export function anim_to_pose(anim_st: mut AnimState, sk: Skin, rot: floats, pos: floats, hit: words) -> void { + let l = part(anim_st, 0) + let rinv = part(anim_st, 1) + let gp = part(anim_st, 2) + let gpi = part(anim_st, 3) + let tmp = part(anim_st, 4) + let d = part(anim_st, 5) + let v = part(anim_st, 6) for i in 0 .. sk.n_nodes { let p = sk.par[i] if (hit[i] & 1) != 0 { @@ -39,7 +34,7 @@ export function anim_to_pose(sk: Skin, rot: floats, pos: floats, hit: words) -> q_normalize(d) skin_set_quat(sk, i, d) } - if (hit[i] & 2) != 0 and positions { + if (hit[i] & 2) != 0 and anim_st.positions { let dx = pos[i * 3] - sk.rest_t[i * 3] let dy = pos[i * 3 + 1] - sk.rest_t[i * 3 + 1] let dz = pos[i * 3 + 2] - sk.rest_t[i * 3 + 2] @@ -54,25 +49,25 @@ export function anim_to_pose(sk: Skin, rot: floats, pos: floats, hit: words) -> } # The scratch is made ONCE and grown, never allocated per call: a call is per actor per frame. -function scratch(n: int) -> void { - if cap >= n { return } - rot_s = floats(n * 4) - pos_s = floats(n * 3) - hit_s = words(n) - cap = n +function scratch(anim_st: mut AnimState, n: int) -> void { + if anim_st.cap >= n { return } + anim_st.rot_s = floats(n * 4) + anim_st.pos_s = floats(n * 3) + anim_st.hit_s = words(n) + anim_st.cap = n } # Play `a` at time `ta`, cross-faded toward `b` at `tb` by `blend` (0 is all a, 1 is all b), and # fold it into the joint matrices. `b` may be null. False when there is no skin or no clip. -export function anim_play(sk: Skin, a: Clip, ta: float, b: Clip, tb: float, blend: float) -> bool { +export function anim_play(anim_st: mut AnimState, sk: Skin, a: Clip, ta: float, b: Clip, tb: float, blend: float) -> bool { if sk == null or a == null { return false } let n = sk.n_nodes - scratch(n) - for i in 0 .. n { hit_s[i] = 0 } + scratch(anim_st, n) + for i in 0 .. n { anim_st.hit_s[i] = 0 } skin_reset(sk) - anim_mix(a, sk, ta, 1.0, rot_s, pos_s, hit_s) - if b != null and blend > 0.001 { anim_mix(b, sk, tb, blend, rot_s, pos_s, hit_s) } - anim_to_pose(sk, rot_s, pos_s, hit_s) + anim_mix(anim_st, a, sk, ta, 1.0, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s) + if b != null and blend > 0.001 { anim_mix(anim_st, b, sk, tb, blend, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s) } + anim_to_pose(anim_st, sk, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s) skin_pose(sk) return true } diff --git a/packages/ludic.anim/read.ludic b/packages/ludic.anim/read.ludic index e2f3a518..63527775 100644 --- a/packages/ludic.anim/read.ludic +++ b/packages/ludic.anim/read.ludic @@ -3,31 +3,31 @@ # Call it straight after a load and before the next one: `gltf_doc` and `gltf_bin` point at # whichever file was read last, so a clip that is not taken now cannot be taken later. -export function anim_read_doc(dir: string) -> int { - clips_init() - if gltf_doc == null { return 0 } - if value_has(gltf_doc, "animations") == 0 { return 0 } +export function anim_read_doc(anim_st: mut AnimState, render3d_st: mut Render3dState, dir: string) -> int { + clips_init(anim_st) + if render3d_st.gltf_doc == null { return 0 } + if value_has(render3d_st.gltf_doc, "animations") == 0 { return 0 } # REOPEN THE .bin. `gltf_load` closes it before it returns, and an accessor read through a closed # handle returns a buffer of the right size full of zeros: right names, right counts, no motion. - let buffers = value_get(gltf_doc, "buffers") + let buffers = value_get(render3d_st.gltf_doc, "buffers") if value_count(buffers) == 0 { return 0 } let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri")) - gltf_bin = file_open(dir + "/" + bin_uri, "rb") - if gltf_bin == null { + render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb") + if render3d_st.gltf_bin == null { print(`anim: cannot reopen {dir}/{bin_uri}`) return 0 } - let arr = value_get(gltf_doc, "animations") + let arr = value_get(render3d_st.gltf_doc, "animations") var got = 0 for ai in 0 .. value_count(arr) { - let c = read_one(value_at(arr, ai)) + let c = read_one(anim_st, render3d_st, value_at(arr, ai)) if c != null { - push(clips, c) + push(anim_st.clips, c) got += 1 } } - file_close(gltf_bin) - gltf_bin = null + file_close(render3d_st.gltf_bin) + render3d_st.gltf_bin = null return got } @@ -39,10 +39,10 @@ function kept_path(ch: Val) -> string { return path } -function read_one(a: Val) -> Clip { +function read_one(anim_st: mut AnimState, render3d_st: mut Render3dState, a: Val) -> Clip { var nm = "" if value_has(a, "name") != 0 { nm = value_as_str(value_get(a, "name")) } - if anim_find(nm) != null { return null } # a second rig's file, same clip + if anim_find(anim_st, nm) != null { return null } # a second rig's file, same clip let chans = value_get(a, "channels") let samps = value_get(a, "samplers") let nc = value_count(chans) @@ -55,7 +55,7 @@ function read_one(a: Val) -> Clip { let path = kept_path(ch) if path == "" { continue } let sp = value_at(samps, value_as_int(value_get(ch, "sampler"))) - let cnt = gltf_accessor_count(value_as_int(value_get(sp, "input"))) + let cnt = gltf_accessor_count(render3d_st, value_as_int(value_get(sp, "input"))) n_keep += 1 n_t += cnt if path == "rotation" { n_v += cnt * 4 } else { n_v += cnt * 3 } @@ -81,14 +81,14 @@ function read_one(a: Val) -> Clip { let sp = value_at(samps, value_as_int(value_get(ch, "sampler"))) c.cnode[k] = value_as_int(value_get(value_get(ch, "target"), "node")) if path == "rotation" { c.cpath[k] = AC_ROT } else { c.cpath[k] = AC_POS } - let tb = gltf_accessor_floats(value_as_int(value_get(sp, "input"))) - let nk = gltf_count + let tb = gltf_accessor_floats(render3d_st, value_as_int(value_get(sp, "input"))) + let nk = render3d_st.gltf_count c.ckeys[k] = nk c.coff[k] = at for i in 0 .. nk { c.times[at + i] = tb[i] } if nk > 0 and c.times[at + nk - 1] > c.dur { c.dur = c.times[at + nk - 1] } at += nk - let vb = gltf_accessor_floats(value_as_int(value_get(sp, "output"))) + let vb = gltf_accessor_floats(render3d_st, value_as_int(value_get(sp, "output"))) var comps = 3 if c.cpath[k] == AC_ROT { comps = 4 } c.cvoff[k] = av diff --git a/packages/ludic.anim/tests/anim_test.ludic b/packages/ludic.anim/tests/anim_test.ludic index e3997137..87c78528 100644 --- a/packages/ludic.anim/tests/anim_test.ludic +++ b/packages/ludic.anim/tests/anim_test.ludic @@ -9,9 +9,9 @@ program AnimTest { function links_runtime() -> bool { return Input.key_pressed(0) } # a root and one child 1 m up, no inverse bind matrices, one skin of both - function skin() -> Skin { - gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}") - return skin_load(0) + function skin(render3d_st: mut Render3dState) -> Skin { + render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}") + return skin_load(render3d_st, 0) } # one channel on node 1: identity at 0, a quarter turn about Z at 1 s @@ -44,14 +44,14 @@ program AnimTest { return q[2] } - test "a clip is found by name, once" { - anim_clear() - expect(anim_add(turn("hiker_m_walk"))) - expect(not anim_add(turn("hiker_m_walk"))) - expect(anim_add(turn("hiker_f_walk"))) - expect_eq(anim_count(), 2) - expect(anim_find("hiker_f_walk") != null) - expect(anim_find("bear_walk") == null) + test "a clip is found by name, once" (anim_st: mut AnimState) { + anim_clear(anim_st) + expect(anim_add(anim_st, turn("hiker_m_walk"))) + expect(not anim_add(anim_st, turn("hiker_m_walk"))) + expect(anim_add(anim_st, turn("hiker_f_walk"))) + expect_eq(anim_count(anim_st), 2) + expect(anim_find(anim_st, "hiker_f_walk") != null) + expect(anim_find(anim_st, "bear_walk") == null) } test "a key pair is found by halving" { @@ -61,56 +61,56 @@ program AnimTest { expect_eq(anim_key_at(c, 0, -1.0), 0) } - test "playing samples between keys, and wraps on the duration" { - let sk = skin() + test "playing samples between keys, and wraps on the duration" (anim_st: mut AnimState, render3d_st: mut Render3dState) { + let sk = skin(render3d_st) let c = turn("t") - expect(anim_play(sk, c, 0.0, null, 0.0, 0.0)) + expect(anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0)) expect_near(pose_z(sk), 0.0, 0.001) - anim_play(sk, c, 1.0 - 0.0001, null, 0.0, 0.0) + anim_play(anim_st, sk, c, 1.0 - 0.0001, null, 0.0, 0.0) expect_near(pose_z(sk), HALF, 0.01) - anim_play(sk, c, 0.5, null, 0.0, 0.0) + anim_play(anim_st, sk, c, 0.5, null, 0.0, 0.0) let mid = pose_z(sk) - anim_play(sk, c, 1.5, null, 0.0, 0.0) + anim_play(anim_st, sk, c, 1.5, null, 0.0, 0.0) expect_near(pose_z(sk), mid, 0.001) expect(mid > 0.3 and mid < 0.45) } - test "a cross-fade lands between the two clips" { - let sk = skin() + test "a cross-fade lands between the two clips" (anim_st: mut AnimState, render3d_st: mut Render3dState) { + let sk = skin(render3d_st) let a = turn("a") let b = turn("b") - anim_play(sk, a, 0.0, b, 0.999, 0.5) + anim_play(anim_st, sk, a, 0.0, b, 0.999, 0.5) let z = pose_z(sk) expect(z > 0.3 and z < 0.45) - anim_play(sk, a, 0.0, b, 0.999, 0.0) + anim_play(anim_st, sk, a, 0.0, b, 0.999, 0.0) expect_near(pose_z(sk), 0.0, 0.001) } - test "a bone is found in the skin's own joints, not the file's first of that name" { - gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}") - let sk = skin_load(0) + test "a bone is found in the skin's own joints, not the file's first of that name" (render3d_st: mut Render3dState) { + render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}") + let sk = skin_load(render3d_st, 0) expect_eq(skin_joint(sk, "bone"), 2) expect_eq(skin_joint_quiet(sk, "wing_l"), -1) expect_eq(skin_joint_quiet(null, "bone"), -1) } - test "no skin or no clip plays nothing" { - expect(not anim_play(null, turn("t"), 0.0, null, 0.0, 0.0)) - expect(not anim_play(skin(), null, 0.0, null, 0.0, 0.0)) + test "no skin or no clip plays nothing" (anim_st: mut AnimState, render3d_st: mut Render3dState) { + expect(not anim_play(anim_st, null, turn("t"), 0.0, null, 0.0, 0.0)) + expect(not anim_play(anim_st, skin(render3d_st), null, 0.0, null, 0.0, 0.0)) } - test "keyed translations are ignored until asked for" { - let sk = skin() + test "keyed translations are ignored until asked for" (anim_st: mut AnimState, render3d_st: mut Render3dState) { + let sk = skin(render3d_st) let c = turn("t") c.cpath[0] = AC_POS c.vals[0] = 0.0 c.vals[1] = 3.0 c.vals[3] = 0.0 c.vals[4] = 3.0 - anim_play(sk, c, 0.0, null, 0.0, 0.0) + anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0) expect_near(sk.pose_t[4], 0.0, 0.0001) - anim_set_positions(true) - anim_play(sk, c, 0.0, null, 0.0, 0.0) + anim_set_positions(anim_st, true) + anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0) expect_near(sk.pose_t[4], 2.0, 0.0001) } @@ -122,8 +122,8 @@ program AnimTest { b[at + 3] = (v >> 24) & 255 } - test "clips are read out of a glTF's .bin, and a late first key is measured" { - anim_clear() + test "clips are read out of a glTF's .bin, and a late first key is measured" (anim_st: mut AnimState, render3d_st: mut Render3dState) { + anim_clear(anim_st) let dir = Os.temp_dir() + "/ludic-anim-test" Fs.mkdir(dir) let b = buffer(40) @@ -133,10 +133,10 @@ program AnimTest { put_f(b, 8 + 24, HALF) put_f(b, 8 + 28, HALF) expect(Fs.write_bytes(dir + "/t.bin", b, 40)) - gltf_doc = Json.parse("{\"buffers\": [{\"uri\": \"t.bin\"}], \"bufferViews\": [{\"byteOffset\": 0}, {\"byteOffset\": 8}], \"accessors\": [{\"bufferView\": 0, \"count\": 2, \"type\": \"SCALAR\"}, {\"bufferView\": 1, \"count\": 2, \"type\": \"VEC4\"}], \"animations\": [{\"name\": \"bear_walk\", \"samplers\": [{\"input\": 0, \"output\": 1}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"rotation\"}}, {\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"scale\"}}]}]}") - expect_eq(anim_read_doc(dir), 1) - expect_eq(anim_read_doc(dir), 0) - let c = anim_find("bear_walk") + render3d_st.gltf_doc = Json.parse("{\"buffers\": [{\"uri\": \"t.bin\"}], \"bufferViews\": [{\"byteOffset\": 0}, {\"byteOffset\": 8}], \"accessors\": [{\"bufferView\": 0, \"count\": 2, \"type\": \"SCALAR\"}, {\"bufferView\": 1, \"count\": 2, \"type\": \"VEC4\"}], \"animations\": [{\"name\": \"bear_walk\", \"samplers\": [{\"input\": 0, \"output\": 1}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"rotation\"}}, {\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"scale\"}}]}]}") + expect_eq(anim_read_doc(anim_st, render3d_st, dir), 1) + expect_eq(anim_read_doc(anim_st, render3d_st, dir), 0) + let c = anim_find(anim_st, "bear_walk") expect(c != null) expect_eq(c.n, 1) expect_near(c.dur, 1.0, 0.0001) diff --git a/packages/ludic.audio/emit.ludic b/packages/ludic.audio/emit.ludic index 8b3d9f8e..fcc456f8 100644 --- a/packages/ludic.audio/emit.ludic +++ b/packages/ludic.audio/emit.ludic @@ -1,33 +1,37 @@ # emit.ludic - the only calls into the runtime's playback. aud_emit is the ONE place a float # crosses into Audio.play_at's Q16.16: handed over unconverted, 0.94 as a pitch clamped to 4x and # -0.85 as a pan to hard left, and every sound in a valley played fast out of one speaker -var last_gain: float = 0.0 -var last_pitch: float = 0.0 -var last_pan: float = 0.0 -var emitted: int = 0 +export state AudioState { + last_gain: float = 0.0 + last_pitch: float = 0.0 + last_pan: float = 0.0 + emitted: int = 0 + tried: int = 0 + live: int = 0 +} -export function aud_emit(h: int, gain: float, pitch: float, pan: float) -> void { +export function aud_emit(audio_st: mut AudioState, h: int, gain: float, pitch: float, pan: float) -> void { if h == 0 { return } - last_gain = gain - last_pitch = pitch - last_pan = pan - emitted += 1 + audio_st.last_gain = gain + audio_st.last_pitch = pitch + audio_st.last_pan = pan + audio_st.emitted += 1 Audio.play_at(h, fixed(gain), fixed(pitch), fixed(pan)) } # a sound at a place in the world, heard within `range` metres: false (and nothing played) when the # listener is out of earshot -export function aud_emit_at(h: int, x: float, z: float, range: float) -> bool { +export function aud_emit_at(audio_st: mut AudioState, h: int, x: float, z: float, range: float) -> bool { let g = aud_gain_at(x, z, range) if g == 0.0 { return false } - aud_emit(h, g, aud_pitch_at(g), aud_pan_at(x, z)) + aud_emit(audio_st, h, g, aud_pitch_at(g), aud_pan_at(x, z)) return true } # a click, a chime, a shutter, a warning: in the player's head, flat -export function aud_ui(h: int) -> void { +export function aud_ui(audio_st: mut AudioState, h: int) -> void { if h == 0 { return } - emitted += 1 + audio_st.emitted += 1 Audio.play(h) } @@ -37,7 +41,7 @@ export function aud_master(v: float) -> void { Audio.volume(fixed(Math.clamp(v, # what the last emit handed the runtime, and how many shots so far: Audio.* is a no-op headless, so # the numbers are the only thing a test can hear -export function aud_last_gain() -> float { return last_gain } -export function aud_last_pitch() -> float { return last_pitch } -export function aud_last_pan() -> float { return last_pan } -export function aud_emitted() -> int { return emitted } +export function aud_last_gain(audio_st: AudioState) -> float { return audio_st.last_gain } +export function aud_last_pitch(audio_st: AudioState) -> float { return audio_st.last_pitch } +export function aud_last_pan(audio_st: AudioState) -> float { return audio_st.last_pan } +export function aud_emitted(audio_st: AudioState) -> int { return audio_st.emitted } diff --git a/packages/ludic.audio/load.ludic b/packages/ludic.audio/load.ludic index bdb162d4..934d13a0 100644 --- a/packages/ludic.audio/load.ludic +++ b/packages/ludic.audio/load.ludic @@ -2,25 +2,23 @@ # bottoms out in AVAudioPlayer), not the pack-aware file_open every other asset arrives through: in a # bundle every clip loads as 0 while Fs.exists, which IS pack-aware, says it is there. So try the # runtime's path, and on 0 lift the bytes out of the pack into the cache directory and load that. -var tried: int = 0 -var live: int = 0 -export function aud_load(path: string) -> int { - tried += 1 +export function aud_load(audio_st: mut AudioState, path: string) -> int { + audio_st.tried += 1 if not is_windowed() { return 0 } var h = Audio.load(path) if h == 0 { let out = aud_unpack(path, base_name(path)) if out != null { h = Audio.load(out) } } - if h != 0 { live += 1 } + if h != 0 { audio_st.live += 1 } return h } # how many clips were asked for and how many loaded: a silent game looks exactly like a working one # unless something says so -export function aud_tried() -> int { return tried } -export function aud_live() -> int { return live } +export function aud_tried(audio_st: AudioState) -> int { return audio_st.tried } +export function aud_live(audio_st: AudioState) -> int { return audio_st.live } # One packed file out to the cache, or null. A cached copy is trusted only if it is as long as the # packed one: a write interrupted halfway would otherwise play truncated for ever. diff --git a/packages/ludic.audio/tests/audio_test.ludic b/packages/ludic.audio/tests/audio_test.ludic index d8f5c4dd..862a506f 100644 --- a/packages/ludic.audio/tests/audio_test.ludic +++ b/packages/ludic.audio/tests/audio_test.ludic @@ -4,35 +4,37 @@ import "ludic.audio" program AudioTest { numbers float - var lx: float = 0.0 - var lz: float = 0.0 - var lyaw: float = 0.0 - function t_x() -> float { return lx } - function t_z() -> float { return lz } - function t_yaw() -> float { return lyaw } + state AudiotestState { + lx: float = 0.0 + lz: float = 0.0 + lyaw: float = 0.0 + } + function t_x(audiotest_st: AudiotestState) -> float { return audiotest_st.lx } + function t_z(audiotest_st: AudiotestState) -> float { return audiotest_st.lz } + function t_yaw(audiotest_st: AudiotestState) -> float { return audiotest_st.lyaw } function dir() -> string { return Os.temp_dir() + "/ludic-audio-test" } function cache() -> string { return dir() + "/cache" } bind AudioListener { x: fn t_x, z: fn t_z, yaw: fn t_yaw } bind AudioFiles { cache_dir: fn cache } - test "the gain falls off with distance and is gone past the range" { + test "the gain falls off with distance and is gone past the range" (audiotest_st: mut AudiotestState) { let near = aud_gain_at(0.0, -10.0, 200.0) let mid = aud_gain_at(0.0, -100.0, 200.0) let far = aud_gain_at(0.0, -150.0, 200.0) expect(near > mid and mid > far and far > 0.0) expect_eq(aud_gain_at(0.0, -400.0, 200.0), 0.0) expect_near(aud_gain_at(0.0, 0.0, 50.0), 1.0, 0.0001) - lx = 1000.0 + audiotest_st.lx = 1000.0 expect_near(aud_gain_at(1000.0, 0.0, 50.0), 1.0, 0.0001) } - test "the pan says which side, ahead and behind are centred, and it turns with the listener" { + test "the pan says which side, ahead and behind are centred, and it turns with the listener" (audiotest_st: mut AudiotestState) { expect(aud_pan_at(-60.0, 0.0) < -0.5) expect(aud_pan_at(60.0, 0.0) > 0.5) expect_near(aud_pan_at(0.0, -60.0), 0.0, 0.01) expect_near(aud_pan_at(0.0, 60.0), 0.0, 0.01) expect_near(aud_pan_at(0.5, 0.0), 0.0, 0.0001) - lyaw = 3.14159265 + audiotest_st.lyaw = 3.14159265 expect(aud_pan_at(-60.0, 0.0) > 0.5) } @@ -41,18 +43,18 @@ program AudioTest { expect(aud_pitch_at(0.0) > 0.9 and aud_pitch_at(1.0) < 1.1) } - test "emit hands the runtime what it was told, and a sound out of earshot is not played" { - aud_emit(7, 0.5, 0.97, -0.25) - expect_near(aud_last_gain(), 0.5, 0.0001) - expect_near(aud_last_pan(), -0.25, 0.0001) - expect_eq(aud_emitted(), 1) - aud_emit(0, 1.0, 1.0, 0.0) - expect_eq(aud_emitted(), 1) - expect(not aud_emit_at(7, 0.0, -500.0, 100.0)) - expect(aud_emit_at(7, 30.0, 0.0, 100.0)) - expect(aud_last_pan() > 0.5) - expect(aud_last_gain() > 0.4 and aud_last_gain() < 0.5) - expect(aud_last_pitch() < 1.0) + test "emit hands the runtime what it was told, and a sound out of earshot is not played" (audio_st: mut AudioState) { + aud_emit(audio_st, 7, 0.5, 0.97, -0.25) + expect_near(aud_last_gain(audio_st), 0.5, 0.0001) + expect_near(aud_last_pan(audio_st), -0.25, 0.0001) + expect_eq(aud_emitted(audio_st), 1) + aud_emit(audio_st, 0, 1.0, 1.0, 0.0) + expect_eq(aud_emitted(audio_st), 1) + expect(not aud_emit_at(audio_st, 7, 0.0, -500.0, 100.0)) + expect(aud_emit_at(audio_st, 7, 30.0, 0.0, 100.0)) + expect(aud_last_pan(audio_st) > 0.5) + expect(aud_last_gain(audio_st) > 0.4 and aud_last_gain(audio_st) < 0.5) + expect(aud_last_pitch(audio_st) < 1.0) } function wav(path: string, rate: int, frames: int) -> void { @@ -92,9 +94,9 @@ program AudioTest { expect(aud_unpack(dir() + "/nothing.wav", "nothing.wav") == null) } - test "nothing loads headless, and the count says so" { - expect_eq(aud_load(dir() + "/click.wav"), 0) - expect_eq(aud_tried(), 1) - expect_eq(aud_live(), 0) + test "nothing loads headless, and the count says so" (audio_st: mut AudioState) { + expect_eq(aud_load(audio_st, dir() + "/click.wav"), 0) + expect_eq(aud_tried(audio_st), 1) + expect_eq(aud_live(audio_st), 0) } } diff --git a/packages/ludic.base/queue.ludic b/packages/ludic.base/queue.ludic index 7c25d70b..ed88101f 100644 --- a/packages/ludic.base/queue.ludic +++ b/packages/ludic.base/queue.ludic @@ -6,53 +6,56 @@ export property Queue { } # every named queue's pending count, so a frame's end can say which facts nobody read -var qb_names: []string = null -var qb_pending: []int = null +export state BaseState { + qb_names: []string = null + qb_pending: []int = null + cs_list: []System = null +} -function qb_init() -> void { - if qb_names != null { return } - qb_names = new []string - qb_pending = new []int +function qb_init(base_st: mut BaseState) -> void { + if base_st.qb_names != null { return } + base_st.qb_names = new []string + base_st.qb_pending = new []int } # a queue; the name is only for core_undrained -export function queue_new(name: string) -> Queue { - qb_init() +export function queue_new(base_st: mut BaseState, name: string) -> Queue { + qb_init(base_st) let q = new Queue q.items = new []T - q.id = len(qb_names) - push(qb_names, name) - push(qb_pending, 0) + q.id = len(base_st.qb_names) + push(base_st.qb_names, name) + push(base_st.qb_pending, 0) return q } -export function q_push(q: Queue, v: T) -> void { +export function q_push(base_st: mut BaseState, q: Queue, v: T) -> void { push(q.items, v) - qb_pending[q.id] = len(q.items) + base_st.qb_pending[q.id] = len(q.items) } # the facts in the order they were pushed; the queue is empty afterwards -export function q_drain(q: Queue) -> []T { +export function q_drain(base_st: mut BaseState, q: Queue) -> []T { let out = q.items q.items = new []T - qb_pending[q.id] = 0 + base_st.qb_pending[q.id] = 0 return out } export function q_len(q: Queue) -> int { return len(q.items) } -export function q_clear(q: Queue) -> void { +export function q_clear(base_st: mut BaseState, q: Queue) -> void { q.items = new []T - qb_pending[q.id] = 0 + base_st.qb_pending[q.id] = 0 } # the names of the queues still holding facts; call it at the end of a frame, where a fact # left behind is a route nobody wrote -export function core_undrained() -> []string { - qb_init() +export function core_undrained(base_st: mut BaseState) -> []string { + qb_init(base_st) let out = new []string - for i in 0 .. len(qb_names) { - if qb_pending[i] > 0 { push(out, qb_names[i]) } + for i in 0 .. len(base_st.qb_names) { + if base_st.qb_pending[i] > 0 { push(out, base_st.qb_names[i]) } } return out } diff --git a/packages/ludic.base/system.ludic b/packages/ludic.base/system.ludic index 4786321b..9850b50d 100644 --- a/packages/ludic.base/system.ludic +++ b/packages/ludic.base/system.ludic @@ -16,14 +16,13 @@ export property System { # fishing_tick }` from its own module; the runner starts from these, then core_add appends export open registry Systems of System as SYS -var cs_list: []System = null -function cs_all() -> []System { - if cs_list == null { - cs_list = new []System - for i in 0 .. SYS_COUNT { push(cs_list, Systems[i]) } +function cs_all(base_st: mut BaseState) -> []System { + if base_st.cs_list == null { + base_st.cs_list = new []System + for i in 0 .. SYS_COUNT { push(base_st.cs_list, Systems[i]) } } - return cs_list + return base_st.cs_list } export function system_new(key: string, phase: int) -> System { @@ -34,8 +33,8 @@ export function system_new(key: string, phase: int) -> System { } # a key is a save section's name, so two systems may not share one -export function core_add(s: System) -> void { - let all = cs_all() +export function core_add(base_st: mut BaseState, s: System) -> void { + let all = cs_all(base_st) for i in 0 .. len(all) { if all[i].key == s.key { print(`ludic.base: two systems are keyed "{s.key}"`) @@ -45,21 +44,21 @@ export function core_add(s: System) -> void { push(all, s) } -export function core_clear() -> void { cs_list = new []System } -export function core_count() -> int { return len(cs_all()) } +export function core_clear(base_st: mut BaseState) -> void { base_st.cs_list = new []System } +export function core_count(base_st: mut BaseState) -> int { return len(cs_all(base_st)) } -export function core_init_all() -> void { - let all = cs_all() +export function core_init_all(base_st: mut BaseState) -> void { + let all = cs_all(base_st) for i in 0 .. len(all) { if all[i].init != null { all[i].init() } } } -export function core_reset_all() -> void { - let all = cs_all() +export function core_reset_all(base_st: mut BaseState) -> void { + let all = cs_all(base_st) for i in 0 .. len(all) { if all[i].reset != null { all[i].reset() } } } -export function core_tick_all(t: Tick) -> void { - let all = cs_all() +export function core_tick_all(base_st: mut BaseState, t: Tick) -> void { + let all = cs_all(base_st) for ph in 0 .. PH_COUNT { for i in 0 .. len(all) { if all[i].phase == ph and all[i].tick != null { all[i].tick(t) } @@ -68,9 +67,9 @@ export function core_tick_all(t: Tick) -> void { } # every system that saves, under its key with its version -export function core_save_all() -> Val { +export function core_save_all(base_st: mut BaseState) -> Val { let root = save_tree() - let all = cs_all() + let all = cs_all(base_st) for i in 0 .. len(all) { if all[i].save != null { save_section(root, all[i].key, all[i].version, all[i].save()) } } @@ -78,8 +77,8 @@ export function core_save_all() -> Val { } # each system its own section; one with no section (or no load) is reset instead -export function core_load_all(root: Val) -> void { - let all = cs_all() +export function core_load_all(base_st: mut BaseState, root: Val) -> void { + let all = cs_all(base_st) for i in 0 .. len(all) { let n = load_section(root, all[i].key) if n.found and all[i].load != null { all[i].load(n.data, n.version) } else if all[i].reset != null { all[i].reset() } diff --git a/packages/ludic.base/tests/queue_test.ludic b/packages/ludic.base/tests/queue_test.ludic index 35bc4423..e1ce6eb4 100644 --- a/packages/ludic.base/tests/queue_test.ludic +++ b/packages/ludic.base/tests/queue_test.ludic @@ -12,52 +12,52 @@ program QueueTest { return c } - function fifo_case() -> void { - let q: Queue = queue_new("ints") - q_push(q, 3) - q_push(q, 1) - q_push(q, 2) + function fifo_case(base_st: mut BaseState) -> void { + let q: Queue = queue_new(base_st, "ints") + q_push(base_st, q, 3) + q_push(base_st, q, 1) + q_push(base_st, q, 2) expect_eq(q_len(q), 3) - let xs = q_drain(q) + let xs = q_drain(base_st, q) expect_eq(len(xs), 3) expect_eq(xs[0], 3) expect_eq(xs[1], 1) expect_eq(xs[2], 2) expect_eq(q_len(q), 0) - expect_eq(len(q_drain(q)), 0) + expect_eq(len(q_drain(base_st, q)), 0) } - function record_case() -> void { - let q: Queue = queue_new("caught") - q_push(q, caught(2, 1.5)) - q_push(q, caught(4, 0.25)) - let cs = q_drain(q) + function record_case(base_st: mut BaseState) -> void { + let q: Queue = queue_new(base_st, "caught") + q_push(base_st, q, caught(2, 1.5)) + q_push(base_st, q, caught(4, 0.25)) + let cs = q_drain(base_st, q) expect_eq(cs[1].species, 4) expect(cs[0].weight == 1.5) } - function clear_case() -> void { - let q: Queue = queue_new("words") - q_push(q, "a") - q_clear(q) + function clear_case(base_st: mut BaseState) -> void { + let q: Queue = queue_new(base_st, "words") + q_push(base_st, q, "a") + q_clear(base_st, q) expect_eq(q_len(q), 0) } - function undrained_case() -> void { - let a: Queue = queue_new("left_behind") - let b: Queue = queue_new("read") - q_push(a, 1) - q_push(b, 2) - q_drain(b) - let names = core_undrained() + function undrained_case(base_st: mut BaseState) -> void { + let a: Queue = queue_new(base_st, "left_behind") + let b: Queue = queue_new(base_st, "read") + q_push(base_st, a, 1) + q_push(base_st, b, 2) + q_drain(base_st, b) + let names = core_undrained(base_st) expect_eq(len(names), 1) expect(names[0] == "left_behind") - q_drain(a) - expect_eq(len(core_undrained()), 0) + q_drain(base_st, a) + expect_eq(len(core_undrained(base_st)), 0) } - test "a queue drains in the order it was pushed" { fifo_case() } - test "a queue holds records" { record_case() } - test "clear drops what is waiting" { clear_case() } - test "the undrained queues are named, and a drain clears the report" { undrained_case() } + test "a queue drains in the order it was pushed" (base_st: mut BaseState) { fifo_case(base_st) } + test "a queue holds records" (base_st: mut BaseState) { record_case(base_st) } + test "clear drops what is waiting" (base_st: mut BaseState) { clear_case(base_st) } + test "the undrained queues are named, and a drain clears the report" (base_st: mut BaseState) { undrained_case(base_st) } } diff --git a/packages/ludic.base/tests/registry_test.ludic b/packages/ludic.base/tests/registry_test.ludic index 45775675..93bfc7d5 100644 --- a/packages/ludic.base/tests/registry_test.ludic +++ b/packages/ludic.base/tests/registry_test.ludic @@ -3,19 +3,21 @@ import "ludic.base" program RegistryTest { numbers float - var trail: string = "" - function late_tick(t: Tick) -> void { trail = trail + "l" } - function early_tick(t: Tick) -> void { trail = trail + "e" } - function added_tick(t: Tick) -> void { trail = trail + "a" } + state RegistrytestState { + trail: string = "" + } + function late_tick(registrytest_st: mut RegistrytestState, t: Tick) -> void { registrytest_st.trail = registrytest_st.trail + "l" } + function early_tick(registrytest_st: mut RegistrytestState, t: Tick) -> void { registrytest_st.trail = registrytest_st.trail + "e" } + function added_tick(registrytest_st: mut RegistrytestState, t: Tick) -> void { registrytest_st.trail = registrytest_st.trail + "a" } def Systems late { phase: PH_RESOLVE, tick: fn late_tick } def Systems early { phase: PH_INPUT, tick: fn early_tick } - test "the declared systems run, phase by phase" { - expect_eq(core_count(), 2) + test "the declared systems run, phase by phase" (base_st: mut BaseState, registrytest_st: RegistrytestState) { + expect_eq(core_count(base_st), 2) expect(Systems[SYS_LATE].key == "late") let s = system_new("added", PH_INPUT) s.tick = fn added_tick - core_add(s) - core_tick_all(tick_new(0.016, 1, 0.0)) - expect(trail == "eal") + core_add(base_st, s) + core_tick_all(base_st, tick_new(0.016, 1, 0.0)) + expect(registrytest_st.trail == "eal") } } diff --git a/packages/ludic.base/tests/route_test.ludic b/packages/ludic.base/tests/route_test.ludic index ab3b6717..02fa5782 100644 --- a/packages/ludic.base/tests/route_test.ludic +++ b/packages/ludic.base/tests/route_test.ludic @@ -10,57 +10,57 @@ program RouteTest { bind FishingWorld { is_water: fn water_everywhere_but_land } # the route: a landed fish goes into the pack - function route_fishing_pack(t: Tick) -> void { - let fish = q_drain(caught) - for i in 0 .. len(fish) { pack_add(fish[i].species, 1) } + function route_fishing_pack(base_st: mut BaseState, toy_fishing_st: ToyFishingState, toy_pack_st: mut ToyPackState, t: Tick) -> void { + let fish = q_drain(base_st, toy_fishing_st.caught) + for i in 0 .. len(fish) { pack_add(toy_pack_st, fish[i].species, 1) } } - function game_start() -> void { - core_clear() - core_add(fishing_system()) + function game_start(base_st: mut BaseState) -> void { + core_clear(base_st) + core_add(base_st, fishing_system()) let r = system_new("route.fishing_pack", PH_RESOLVE) r.tick = fn route_fishing_pack - core_add(r) - core_add(pack_system()) - core_reset_all() + core_add(base_st, r) + core_add(base_st, pack_system()) + core_reset_all(base_st) } - function pack_total() -> int { return pack_count(0) + pack_count(1) + pack_count(2) } + function pack_total(toy_pack_st: ToyPackState) -> int { return pack_count(toy_pack_st, 0) + pack_count(toy_pack_st, 1) + pack_count(toy_pack_st, 2) } - test "three casts on water land three fish in the pack, one on land lands nothing" { - game_start() - expect(fishing_cast(1.0, 2.0)) - expect(fishing_cast(3.0, 2.0)) - expect(fishing_cast(5.0, 2.0)) - expect(not fishing_cast(500.0, 2.0)) - core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) - expect_eq(pack_total(), 3) - expect_eq(len(core_undrained()), 0) + test "three casts on water land three fish in the pack, one on land lands nothing" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, toy_pack_st: ToyPackState) { + game_start(base_st) + expect(fishing_cast(toy_fishing_st, 1.0, 2.0)) + expect(fishing_cast(toy_fishing_st, 3.0, 2.0)) + expect(fishing_cast(toy_fishing_st, 5.0, 2.0)) + expect(not fishing_cast(toy_fishing_st, 500.0, 2.0)) + core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0)) + expect_eq(pack_total(toy_pack_st), 3) + expect_eq(len(core_undrained(base_st)), 0) } - test "the pack saves its own section and the fishing, which saves nothing, is reset" { - game_start() - fishing_cast(1.0, 1.0) - core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) - let text = save_encode(core_save_all()) + test "the pack saves its own section and the fishing, which saves nothing, is reset" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, toy_pack_st: ToyPackState) { + game_start(base_st) + fishing_cast(toy_fishing_st, 1.0, 1.0) + core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0)) + let text = save_encode(core_save_all(base_st)) let tree = save_decode(text) expect_eq(Value.count(tree), 1) expect_eq(load_section(tree, "pack").version, 1) expect(not load_section(tree, "fishing").found) - core_reset_all() - expect_eq(pack_total(), 0) - core_load_all(save_decode(text)) - expect_eq(pack_total(), 1) + core_reset_all(base_st) + expect_eq(pack_total(toy_pack_st), 0) + core_load_all(base_st, save_decode(text)) + expect_eq(pack_total(toy_pack_st), 1) } - test "the same seed lands the same fish" { - game_start() - fishing_cast(1.0, 1.0) - core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) - let first = save_encode(core_save_all()) - game_start() - fishing_cast(1.0, 1.0) - core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) - expect(save_encode(core_save_all()) == first) + test "the same seed lands the same fish" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) { + game_start(base_st) + fishing_cast(toy_fishing_st, 1.0, 1.0) + core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0)) + let first = save_encode(core_save_all(base_st)) + game_start(base_st) + fishing_cast(toy_fishing_st, 1.0, 1.0) + core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0)) + expect(save_encode(core_save_all(base_st)) == first) } } diff --git a/packages/ludic.base/tests/system_test.ludic b/packages/ludic.base/tests/system_test.ludic index 5ffeffd2..79d40e43 100644 --- a/packages/ludic.base/tests/system_test.ludic +++ b/packages/ludic.base/tests/system_test.ludic @@ -3,31 +3,33 @@ import "ludic.base" program SystemTest { numbers float - var trail: string = "" - var apples: int = 0 - var hours: float = 0.0 + state SystemtestState { + trail: string = "" + apples: int = 0 + hours: float = 0.0 + } - function draw_tick(t: Tick) -> void { trail = trail + "d" } - function think_tick(t: Tick) -> void { trail = trail + "t" } - function input_tick(t: Tick) -> void { trail = trail + "i" } - function commit_tick(t: Tick) -> void { trail = trail + "c" } - function apples_init() -> void { trail = trail + "A" } - function apples_reset() -> void { apples = 0 } - function apples_tick(t: Tick) -> void { apples += 1 } - function apples_save() -> Val { + function draw_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "d" } + function think_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "t" } + function input_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "i" } + function commit_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "c" } + function apples_init(systemtest_st: mut SystemtestState) -> void { systemtest_st.trail = systemtest_st.trail + "A" } + function apples_reset(systemtest_st: mut SystemtestState) -> void { systemtest_st.apples = 0 } + function apples_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.apples += 1 } + function apples_save(systemtest_st: SystemtestState) -> Val { let v = Value.object() - sv_put_int(v, "n", apples) + sv_put_int(v, "n", systemtest_st.apples) return v } - function apples_load(v: Val, version: int) -> void { apples = sv_int(v, "n", 0) * version } - function clock_reset() -> void { hours = 6.0 } - function clock_tick(t: Tick) -> void { hours = hours + t.hours } - function clock_save() -> Val { + function apples_load(systemtest_st: mut SystemtestState, v: Val, version: int) -> void { systemtest_st.apples = sv_int(v, "n", 0) * version } + function clock_reset(systemtest_st: mut SystemtestState) -> void { systemtest_st.hours = 6.0 } + function clock_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.hours = systemtest_st.hours + t.hours } + function clock_save(systemtest_st: SystemtestState) -> Val { let v = Value.object() - sv_put_float(v, "h", hours) + sv_put_float(v, "h", systemtest_st.hours) return v } - function clock_load(v: Val, version: int) -> void { hours = sv_float(v, "h", 6.0) } + function clock_load(systemtest_st: mut SystemtestState, v: Val, version: int) -> void { systemtest_st.hours = sv_float(v, "h", 6.0) } function sys(key: string, phase: int, tick: fn(Tick) -> void) -> System { let s = system_new(key, phase) @@ -35,71 +37,71 @@ program SystemTest { return s } - function setup() -> void { - core_clear() - trail = "" - core_add(sys("draw", PH_PRESENT, fn draw_tick)) - core_add(sys("think", PH_SIMULATE, fn think_tick)) + function setup(base_st: mut BaseState, systemtest_st: mut SystemtestState) -> void { + core_clear(base_st) + systemtest_st.trail = "" + core_add(base_st, sys("draw", PH_PRESENT, fn draw_tick)) + core_add(base_st, sys("think", PH_SIMULATE, fn think_tick)) let a = sys("apples", PH_SIMULATE, fn apples_tick) a.init = fn apples_init a.reset = fn apples_reset a.save = fn apples_save a.load = fn apples_load a.version = 2 - core_add(a) - core_add(sys("input", PH_INPUT, fn input_tick)) - core_add(sys("commit", PH_COMMIT, fn commit_tick)) + core_add(base_st, a) + core_add(base_st, sys("input", PH_INPUT, fn input_tick)) + core_add(base_st, sys("commit", PH_COMMIT, fn commit_tick)) let c = sys("clock", PH_SIMULATE, fn clock_tick) c.reset = fn clock_reset c.save = fn clock_save c.load = fn clock_load - core_add(c) + core_add(base_st, c) } - test "tick runs phase by phase, in the order added within a phase" { - setup() - core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) - expect(trail == "itcd") - expect_eq(apples, 1) + test "tick runs phase by phase, in the order added within a phase" (base_st: mut BaseState, systemtest_st: mut SystemtestState) { + setup(base_st, systemtest_st) + core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0)) + expect(systemtest_st.trail == "itcd") + expect_eq(systemtest_st.apples, 1) } - test "init and reset run in the order added and skip a null verb" { - setup() - core_init_all() - expect(trail == "A") - apples = 9 - core_reset_all() - expect_eq(apples, 0) - expect(hours == 6.0) - expect_eq(core_count(), 6) + test "init and reset run in the order added and skip a null verb" (base_st: mut BaseState, systemtest_st: mut SystemtestState) { + setup(base_st, systemtest_st) + core_init_all(base_st) + expect(systemtest_st.trail == "A") + systemtest_st.apples = 9 + core_reset_all(base_st) + expect_eq(systemtest_st.apples, 0) + expect(systemtest_st.hours == 6.0) + expect_eq(core_count(base_st), 6) } - test "save writes each system under its key with its version, and load reads it back" { - setup() - core_reset_all() - apples = 4 - core_tick_all(tick_new(0.016, 1, 0.5)) - let root = core_save_all() + test "save writes each system under its key with its version, and load reads it back" (base_st: mut BaseState, systemtest_st: mut SystemtestState) { + setup(base_st, systemtest_st) + core_reset_all(base_st) + systemtest_st.apples = 4 + core_tick_all(base_st, tick_new(0.016, 1, 0.5)) + let root = core_save_all(base_st) expect_eq(Value.count(root), 2) expect_eq(load_section(root, "apples").version, 2) expect_eq(load_section(root, "clock").version, 1) let text = save_encode(root) - core_reset_all() - core_load_all(save_decode(text)) - expect_eq(apples, 10) - expect(hours == 6.5) + core_reset_all(base_st) + core_load_all(base_st, save_decode(text)) + expect_eq(systemtest_st.apples, 10) + expect(systemtest_st.hours == 6.5) } - test "a system with no section in the save is reset" { - setup() - apples = 3 - hours = 11.0 + test "a system with no section in the save is reset" (base_st: mut BaseState, systemtest_st: mut SystemtestState) { + setup(base_st, systemtest_st) + systemtest_st.apples = 3 + systemtest_st.hours = 11.0 let root = save_tree() let v = Value.object() sv_put_float(v, "h", 20.25) save_section(root, "clock", 1, v) - core_load_all(root) - expect_eq(apples, 0) - expect(hours == 20.25) + core_load_all(base_st, root) + expect_eq(systemtest_st.apples, 0) + expect(systemtest_st.hours == 20.25) } } diff --git a/packages/ludic.base/tests/toys/fishing/index.ludic b/packages/ludic.base/tests/toys/fishing/index.ludic index 1bd7fe13..25f52030 100644 --- a/packages/ludic.base/tests/toys/fishing/index.ludic +++ b/packages/ludic.base/tests/toys/fishing/index.ludic @@ -4,26 +4,28 @@ numbers float import "ludic.base" export property Caught { species: int = 0, weight: float = 0.0 } export port FishingWorld { is_water: fn(float, float) -> bool } # its port -var dice: Rng = null -var casts: int = 0 -export var caught: Queue = null # its facts -export function fishing_cast(x: float, z: float) -> bool { # its verb +export state ToyFishingState { + dice: Rng = null + casts: int = 0 + caught: Queue = null # its facts +} +export function fishing_cast(toy_fishing_st: mut ToyFishingState, x: float, z: float) -> bool { # its verb if not FishingWorld.is_water(x, z) { return false } - casts += 1 + toy_fishing_st.casts += 1 return true } -function fishing_reset() -> void { - casts = 0 - dice = rng_new(7) - caught = queue_new("fishing.caught") +function fishing_reset(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) -> void { + toy_fishing_st.casts = 0 + toy_fishing_st.dice = rng_new(7) + toy_fishing_st.caught = queue_new(base_st, "fishing.caught") } -function fishing_tick(t: Tick) -> void { - while casts > 0 { +function fishing_tick(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, t: Tick) -> void { + while toy_fishing_st.casts > 0 { let c = new Caught - c.species = rng_between(dice, 0, 2) - c.weight = 0.5 + rng_float(dice) - q_push(caught, c) - casts -= 1 + c.species = rng_between(toy_fishing_st.dice, 0, 2) + c.weight = 0.5 + rng_float(toy_fishing_st.dice) + q_push(base_st, toy_fishing_st.caught, c) + toy_fishing_st.casts -= 1 } } export function fishing_system() -> System { diff --git a/packages/ludic.base/tests/toys/pack/index.ludic b/packages/ludic.base/tests/toys/pack/index.ludic index 7ec041e4..997810d5 100644 --- a/packages/ludic.base/tests/toys/pack/index.ludic +++ b/packages/ludic.base/tests/toys/pack/index.ludic @@ -2,22 +2,24 @@ module toy_pack uses ludic_base numbers float import "ludic.base" -var counts: []int = null -export function pack_add(kind: int, n: int) -> void { counts[kind] = counts[kind] + n } -export function pack_count(kind: int) -> int { return counts[kind] } -function pack_reset() -> void { - counts = new []int - for i in 0 .. 3 { push(counts, 0) } +export state ToyPackState { + counts: []int = null } -function pack_save() -> Val { +export function pack_add(toy_pack_st: mut ToyPackState, kind: int, n: int) -> void { toy_pack_st.counts[kind] = toy_pack_st.counts[kind] + n } +export function pack_count(toy_pack_st: ToyPackState, kind: int) -> int { return toy_pack_st.counts[kind] } +function pack_reset(toy_pack_st: mut ToyPackState) -> void { + toy_pack_st.counts = new []int + for i in 0 .. 3 { push(toy_pack_st.counts, 0) } +} +function pack_save(toy_pack_st: ToyPackState) -> Val { let v = Value.object() - sv_put_ints(v, "counts", counts) + sv_put_ints(v, "counts", toy_pack_st.counts) return v } -function pack_load(v: Val, version: int) -> void { - pack_reset() +function pack_load(toy_pack_st: mut ToyPackState, v: Val, version: int) -> void { + pack_reset(toy_pack_st) let xs = sv_ints(v, "counts") - for i in 0 .. len(xs) { if i < len(counts) { counts[i] = xs[i] } } + for i in 0 .. len(xs) { if i < len(toy_pack_st.counts) { toy_pack_st.counts[i] = xs[i] } } } export function pack_system() -> System { let s = system_new("pack", PH_COMMIT) diff --git a/packages/ludic.character/camera.ludic b/packages/ludic.character/camera.ludic index 78fb890b..e0074c1e 100644 --- a/packages/ludic.character/camera.ludic +++ b/packages/ludic.character/camera.ludic @@ -1,70 +1,70 @@ # camera.ludic - the orbit: a pivot at the chest easing after the body, the camera on its orbit # behind and a share of the distance to the right (so the body sits left of the crosshair), lifted # out of the ground and aimed back at the pivot. Or the eyes, with the gait's bob on them. -function chr_eyes_allowed() -> bool { return not CharacterBody.riding() and not chr_sit and CharacterBody.eyes_ok() } +function chr_eyes_allowed(character_st: CharacterState) -> bool { return not CharacterBody.riding() and not character_st.chr_sit and CharacterBody.eyes_ok() } -function chr_shoulder() -> float { return Math.clamp(chr_cam_dist * 0.30, 0.6, 1.9) } +function chr_shoulder(character_st: CharacterState) -> float { return Math.clamp(character_st.chr_cam_dist * 0.30, 0.6, 1.9) } -export function char_camera(dt: float) -> void { - if chr_fps and chr_eyes_allowed() { - chr_camera_eyes(dt) +export function char_camera(character_st: mut CharacterState, dt: float) -> void { + if character_st.chr_fps and chr_eyes_allowed(character_st) { + chr_camera_eyes(character_st, dt) return } - char_camera_third(dt) + char_camera_third(character_st, dt) } # over the shoulder, whatever the player plays in (a figure looked at from inside its head is not) -export function char_camera_third(dt: float) -> void { - chr_eyes_now = false +export function char_camera_third(character_st: mut CharacterState, dt: float) -> void { + character_st.chr_eyes_now = false let k = Math.min(1.0, 10.0 * dt) - var ty = chr_y + 1.45 - if chr_swim { ty = chr_y + 0.9 } - chr_piv_x = Math.lerp(chr_piv_x, chr_x, k) - chr_piv_y = Math.lerp(chr_piv_y, ty, k) - chr_piv_z = Math.lerp(chr_piv_z, chr_z, k) - let cyw = Math.cos(chr_cam_yaw) - let syw = Math.sin(chr_cam_yaw) - let cpt = Math.cos(chr_cam_pitch) + var ty = character_st.chr_y + 1.45 + if character_st.chr_swim { ty = character_st.chr_y + 0.9 } + character_st.chr_piv_x = Math.lerp(character_st.chr_piv_x, character_st.chr_x, k) + character_st.chr_piv_y = Math.lerp(character_st.chr_piv_y, ty, k) + character_st.chr_piv_z = Math.lerp(character_st.chr_piv_z, character_st.chr_z, k) + let cyw = Math.cos(character_st.chr_cam_yaw) + let syw = Math.sin(character_st.chr_cam_yaw) + let cpt = Math.cos(character_st.chr_cam_pitch) let fx = -(syw * cpt) - let fy = Math.sin(chr_cam_pitch) + let fy = Math.sin(character_st.chr_cam_pitch) let fz = -(cyw * cpt) - let ax = chr_piv_x + cyw * chr_shoulder() - let az = chr_piv_z - syw * chr_shoulder() - let cx = ax - fx * chr_cam_dist - var cyy = chr_piv_y - fy * chr_cam_dist - let cz = az - fz * chr_cam_dist + let ax = character_st.chr_piv_x + cyw * chr_shoulder(character_st) + let az = character_st.chr_piv_z - syw * chr_shoulder(character_st) + let cx = ax - fx * character_st.chr_cam_dist + var cyy = character_st.chr_piv_y - fy * character_st.chr_cam_dist + let cz = az - fz * character_st.chr_cam_dist cyy = Math.max(cyy, CharacterGround.bed(cx, cz) + 0.55) # at the top of the water the camera is held clear of it; under it, it follows the dive - if chr_swim and not chr_dive and cyy < chr_water + 0.35 { cyy = chr_water + 0.35 } + if character_st.chr_swim and not character_st.chr_dive and cyy < character_st.chr_water + 0.35 { cyy = character_st.chr_water + 0.35 } let mid_g = CharacterGround.bed((cx + ax) * 0.5, (cz + az) * 0.5) + 0.4 - let mid_y = (cyy + chr_piv_y) * 0.5 + let mid_y = (cyy + character_st.chr_piv_y) * 0.5 if mid_y < mid_g { cyy = cyy + (mid_g - mid_y) * 2.0 } - chr_eye_x = cx - chr_eye_y = cyy - chr_eye_z = cz + character_st.chr_eye_x = cx + character_st.chr_eye_y = cyy + character_st.chr_eye_z = cz let ddx = ax - cx let ddz = az - cz - chr_eye_yaw = Math.atan2(-ddx, -ddz) - chr_eye_pitch = Math.atan2(chr_piv_y - cyy, Math.sqrt(ddx * ddx + ddz * ddz)) + character_st.chr_eye_yaw = Math.atan2(-ddx, -ddz) + character_st.chr_eye_pitch = Math.atan2(character_st.chr_piv_y - cyy, Math.sqrt(ddx * ddx + ddz * ddz)) } # the eyes: the head's height, the walk's bob, and the look the player is already holding -function chr_camera_eyes(dt: float) -> void { - chr_eyes_now = true +function chr_camera_eyes(character_st: mut CharacterState, dt: float) -> void { + character_st.chr_eyes_now = true var eye = 1.62 - if chr_swim { eye = 0.42 } - if chr_dive { eye = 0.30 } - let moving = Math.clamp(chr_speed / 5.0, 0.0, 1.0) - let want = 0.035 * moving * (1.0 - Math.cos(chr_phase * 2.0)) - chr_fps_bob = Math.lerp(chr_fps_bob, want, Math.min(1.0, 12.0 * dt)) + if character_st.chr_swim { eye = 0.42 } + if character_st.chr_dive { eye = 0.30 } + let moving = Math.clamp(character_st.chr_speed / 5.0, 0.0, 1.0) + let want = 0.035 * moving * (1.0 - Math.cos(character_st.chr_phase * 2.0)) + character_st.chr_fps_bob = Math.lerp(character_st.chr_fps_bob, want, Math.min(1.0, 12.0 * dt)) # never inside the ground on a steep step - let ey = Math.max(chr_y + eye + chr_fps_bob, CharacterGround.bed(chr_x, chr_z) + 0.30) - chr_piv_x = chr_x - chr_piv_y = ey - chr_piv_z = chr_z - chr_eye_x = chr_x - chr_eye_y = ey - chr_eye_z = chr_z - chr_eye_yaw = chr_cam_yaw - chr_eye_pitch = chr_cam_pitch + let ey = Math.max(character_st.chr_y + eye + character_st.chr_fps_bob, CharacterGround.bed(character_st.chr_x, character_st.chr_z) + 0.30) + character_st.chr_piv_x = character_st.chr_x + character_st.chr_piv_y = ey + character_st.chr_piv_z = character_st.chr_z + character_st.chr_eye_x = character_st.chr_x + character_st.chr_eye_y = ey + character_st.chr_eye_z = character_st.chr_z + character_st.chr_eye_yaw = character_st.chr_cam_yaw + character_st.chr_eye_pitch = character_st.chr_cam_pitch } diff --git a/packages/ludic.character/ground.ludic b/packages/ludic.character/ground.ludic index b7907339..d178da10 100644 --- a/packages/ludic.character/ground.ludic +++ b/packages/ludic.character/ground.ludic @@ -10,25 +10,23 @@ export function char_stand_at(x: float, z: float, from_y: float) -> float { # A step up and a slope up are different refusals. Within CHAR_STEP of the feet is a step, taken # whatever the gradient into it (a 45 cm deck arrives in one frame); above that the boots decide, # measured over CHAR_SLOPE_PROBE, so the frame rate never changes the answer. -export function char_slope_blocks(nh: float, fx: float, fz: float) -> bool { - if nh - chr_y > CHAR_STEP { return true } - let sx = chr_x + fx * CHAR_SLOPE_PROBE - let sz = chr_z + fz * CHAR_SLOPE_PROBE - return CharacterGround.height(sx, sz) - chr_y > CHAR_SLOPE_PROBE * CharacterGround.slope_limit() +export function char_slope_blocks(character_st: CharacterState, nh: float, fx: float, fz: float) -> bool { + if nh - character_st.chr_y > CHAR_STEP { return true } + let sx = character_st.chr_x + fx * CHAR_SLOPE_PROBE + let sz = character_st.chr_z + fz * CHAR_SLOPE_PROBE + return CharacterGround.height(sx, sz) - character_st.chr_y > CHAR_SLOPE_PROBE * CharacterGround.slope_limit() } # a body of radius r at (x, z) from y0 to y1, out of every collider: the resolved point is chr_out_* -var chr_out_x: float = 0.0 -var chr_out_z: float = 0.0 -function chr_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { - chr_ask_x = x - chr_ask_z = z +function chr_push(character_st: mut CharacterState, x: float, z: float, r: float, y0: float, y1: float) -> bool { + character_st.chr_ask_x = x + character_st.chr_ask_z = z let hit = CharacterGround.push(x, z, r, y0, y1) - chr_out_x = x - chr_out_z = z + character_st.chr_out_x = x + character_st.chr_out_z = z if hit { - chr_out_x = CharacterGround.pushed_x() - chr_out_z = CharacterGround.pushed_z() + character_st.chr_out_x = CharacterGround.pushed_x() + character_st.chr_out_z = CharacterGround.pushed_z() } return hit } diff --git a/packages/ludic.character/ports.ludic b/packages/ludic.character/ports.ludic index 806c20c1..9c1eb472 100644 --- a/packages/ludic.character/ports.ludic +++ b/packages/ludic.character/ports.ludic @@ -39,14 +39,70 @@ export port CharacterInput { look_always: fn() -> bool = fn chr__no # the look is held: never settle behind } -var chr_ask_x: float = 0.0 -var chr_ask_z: float = 0.0 +export state CharacterState { + chr_out_x: float = 0.0 + chr_out_z: float = 0.0 + chr_ask_x: float = 0.0 + chr_ask_z: float = 0.0 + chr_x: float = 0.0 + chr_y: float = 0.0 + chr_z: float = 0.0 + chr_yaw: float = 0.0 # facing, radians; 0 faces -z + chr_speed: float = 0.0 # m/s along the facing + chr_phase: float = 0.0 # the gait: one cycle is two steps, advanced by ground covered + chr_move: float = 0.0 # 0 standing .. 1 moving + chr_run: float = 0.0 # 0 walking .. 1 running + chr_time: float = 0.0 + chr_water: float = -10000.0 # the water line under the body, every update + chr_walk_speed: float = 3.2 + chr_run_speed: float = 6.8 + chr_vy: float = 0.0 + chr_air: bool = false + chr_air_vx: float = 0.0 + chr_air_vz: float = 0.0 + chr_land: float = 0.0 # seconds of the landing's crouch left + chr_steep: float = 0.0 # seconds of "that was too steep" left to say + chr_can_run: bool = true + chr_step: float = 0.0 # metres covered this frame + chr_sit: bool = false + chr_sit_mode: int = 0 + chr_sit_t: float = 0.0 # 0 standing .. 1 sat, eased over a quarter second + chr_sit_x: float = 0.0 # the seat's point, walked to over the blend + chr_sit_z: float = 0.0 + chr_sit_yaw: float = 0.0 + chr_swim: bool = false + chr_dive: bool = false + chr_depth: float = 0.0 # metres below the surface + chr_breath: float = 0.0 + chr_float: float = 0.0 # 0 swimming .. 1 floating + chr_swim_phase: float = 0.0 + chr_edge_said: float = 0.0 + chr_holds: int = CHAR_HOLD_SCREEN + chr_cam_yaw: float = 0.0 + chr_cam_pitch: float = -0.157 + chr_cam_dist: float = 4.2 + chr_lock: bool = false # the orbit never settles behind on its own + chr_fps: bool = false + chr_fps_bob: float = 0.0 + chr_piv_x: float = 0.0 + chr_piv_y: float = 0.0 + chr_piv_z: float = 0.0 + chr_eye_x: float = 0.0 # where the camera is and where it looks, once computed + chr_eye_y: float = 0.0 + chr_eye_z: float = 0.0 + chr_eye_yaw: float = 0.0 + chr_eye_pitch: float = 0.0 + chr_eyes_now: bool = false # the last camera was the eyes: the figure is hidden + chr_safe_x: float = 0.0 + chr_safe_z: float = 0.0 + chr_fact_q: Queue = null +} function chr__top_plain(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float { return base } function chr__bed_plain(x: float, z: float) -> float { return CharacterGround.height(x, z) } function chr__dry(x: float, z: float) -> float { return -10000.0 } function chr__no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false } -function chr__asked_x() -> float { return chr_ask_x } -function chr__asked_z() -> float { return chr_ask_z } +function chr__asked_x(character_st: CharacterState) -> float { return character_st.chr_ask_x } +function chr__asked_z(character_st: CharacterState) -> float { return character_st.chr_ask_z } function chr__slope_plain(x: float, z: float) -> float { let e = 2.0 let dx = CharacterGround.height(x + e, z) - CharacterGround.height(x - e, z) diff --git a/packages/ludic.character/queries.ludic b/packages/ludic.character/queries.ludic index 9a43fcfb..a96321cf 100644 --- a/packages/ludic.character/queries.ludic +++ b/packages/ludic.character/queries.ludic @@ -1,48 +1,48 @@ # queries.ludic - what the rest of a game may ASK about the body: the only way out of its state -export function body_x() -> float { return chr_x } -export function body_y() -> float { return chr_y } -export function body_z() -> float { return chr_z } -export function body_yaw() -> float { return chr_yaw } -export function body_speed() -> float { return chr_speed } -export function body_moving() -> float { return chr_move } -export function body_run() -> float { return chr_run } -export function body_phase() -> float { return chr_phase } -export function body_time() -> float { return chr_time } -export function body_water() -> float { return chr_water } -export function body_airborne() -> bool { return chr_air } -export function body_vy() -> float { return chr_vy } -export function body_landing() -> float { return chr_land } -export function body_steep() -> float { return chr_steep } -export function body_step() -> float { return chr_step } -export function body_sitting() -> bool { return chr_sit } -export function body_sit_mode() -> int { return chr_sit_mode } -export function body_sit_t() -> float { return chr_sit_t } -export function body_swimming() -> bool { return chr_swim } -export function body_diving() -> bool { return chr_dive } -export function body_depth() -> float { return chr_depth } -export function body_breath() -> float { return chr_breath } -export function body_floating() -> float { return chr_float } -export function body_swim_phase() -> float { return chr_swim_phase } -export function body_can_run() -> bool { return chr_can_run } -export function body_walk_speed() -> float { return chr_walk_speed } -export function body_run_speed() -> float { return chr_run_speed } -export function body_held() -> bool { return chr_holds != 0 } -export function body_fps() -> bool { return chr_fps } -export function body_cam_yaw() -> float { return chr_cam_yaw } -export function body_cam_pitch() -> float { return chr_cam_pitch } -export function body_cam_dist() -> float { return chr_cam_dist } +export function body_x(character_st: CharacterState) -> float { return character_st.chr_x } +export function body_y(character_st: CharacterState) -> float { return character_st.chr_y } +export function body_z(character_st: CharacterState) -> float { return character_st.chr_z } +export function body_yaw(character_st: CharacterState) -> float { return character_st.chr_yaw } +export function body_speed(character_st: CharacterState) -> float { return character_st.chr_speed } +export function body_moving(character_st: CharacterState) -> float { return character_st.chr_move } +export function body_run(character_st: CharacterState) -> float { return character_st.chr_run } +export function body_phase(character_st: CharacterState) -> float { return character_st.chr_phase } +export function body_time(character_st: CharacterState) -> float { return character_st.chr_time } +export function body_water(character_st: CharacterState) -> float { return character_st.chr_water } +export function body_airborne(character_st: CharacterState) -> bool { return character_st.chr_air } +export function body_vy(character_st: CharacterState) -> float { return character_st.chr_vy } +export function body_landing(character_st: CharacterState) -> float { return character_st.chr_land } +export function body_steep(character_st: CharacterState) -> float { return character_st.chr_steep } +export function body_step(character_st: CharacterState) -> float { return character_st.chr_step } +export function body_sitting(character_st: CharacterState) -> bool { return character_st.chr_sit } +export function body_sit_mode(character_st: CharacterState) -> int { return character_st.chr_sit_mode } +export function body_sit_t(character_st: CharacterState) -> float { return character_st.chr_sit_t } +export function body_swimming(character_st: CharacterState) -> bool { return character_st.chr_swim } +export function body_diving(character_st: CharacterState) -> bool { return character_st.chr_dive } +export function body_depth(character_st: CharacterState) -> float { return character_st.chr_depth } +export function body_breath(character_st: CharacterState) -> float { return character_st.chr_breath } +export function body_floating(character_st: CharacterState) -> float { return character_st.chr_float } +export function body_swim_phase(character_st: CharacterState) -> float { return character_st.chr_swim_phase } +export function body_can_run(character_st: CharacterState) -> bool { return character_st.chr_can_run } +export function body_walk_speed(character_st: CharacterState) -> float { return character_st.chr_walk_speed } +export function body_run_speed(character_st: CharacterState) -> float { return character_st.chr_run_speed } +export function body_held(character_st: CharacterState) -> bool { return character_st.chr_holds != 0 } +export function body_fps(character_st: CharacterState) -> bool { return character_st.chr_fps } +export function body_cam_yaw(character_st: CharacterState) -> float { return character_st.chr_cam_yaw } +export function body_cam_pitch(character_st: CharacterState) -> float { return character_st.chr_cam_pitch } +export function body_cam_dist(character_st: CharacterState) -> float { return character_st.chr_cam_dist } # the camera the last char_camera worked out, for the game to hand its renderer -export function body_eye_x() -> float { return chr_eye_x } -export function body_eye_y() -> float { return chr_eye_y } -export function body_eye_z() -> float { return chr_eye_z } -export function body_eye_yaw() -> float { return chr_eye_yaw } -export function body_eye_pitch() -> float { return chr_eye_pitch } -export function body_eyes_now() -> bool { return chr_eyes_now } +export function body_eye_x(character_st: CharacterState) -> float { return character_st.chr_eye_x } +export function body_eye_y(character_st: CharacterState) -> float { return character_st.chr_eye_y } +export function body_eye_z(character_st: CharacterState) -> float { return character_st.chr_eye_z } +export function body_eye_yaw(character_st: CharacterState) -> float { return character_st.chr_eye_yaw } +export function body_eye_pitch(character_st: CharacterState) -> float { return character_st.chr_eye_pitch } +export function body_eyes_now(character_st: CharacterState) -> bool { return character_st.chr_eyes_now } # where char_safe_spot found room, after it answered true -export function body_safe_x() -> float { return chr_safe_x } -export function body_safe_z() -> float { return chr_safe_z } +export function body_safe_x(character_st: CharacterState) -> float { return character_st.chr_safe_x } +export function body_safe_z(character_st: CharacterState) -> float { return character_st.chr_safe_z } # the air a full breath holds, with the lungs the game gives export function body_breath_max() -> float { return CHAR_BREATH_MAX * CharacterBody.breath() } diff --git a/packages/ludic.character/safe.ludic b/packages/ludic.character/safe.ludic index a1540944..18b81edf 100644 --- a/packages/ludic.character/safe.ludic +++ b/packages/ludic.character/safe.ludic @@ -2,11 +2,11 @@ # every landing asks. The wanted spot if it will do, else the nearest ring out that is clear of every # collider, no deeper than max_depth, within CHAR_DROP of the height being left, and on ground # gentle enough to walk off - a spot you can stand on but cannot leave is not a safe spot. -function chr_safe_ok(x: float, z: float, from_y: float, max_depth: float) -> bool { +function chr_safe_ok(character_st: mut CharacterState, x: float, z: float, from_y: float, max_depth: float) -> bool { let g = CharacterGround.height(x, z) if Math.abs(g - from_y) > CHAR_DROP { return false } if CharacterGround.water(x, z) - g > max_depth { return false } - if chr_push(x, z, 0.4, g, g + CHAR_BODY) { return false } + if chr_push(character_st, x, z, 0.4, g, g + CHAR_BODY) { return false } var lo = g var hi = g for k in 0 .. 4 { @@ -18,19 +18,19 @@ function chr_safe_ok(x: float, z: float, from_y: float, max_depth: float) -> boo return not (hi - lo > CHAR_SLOPE_PROBE * 2.0) } -export function char_safe_spot(x: float, z: float, from_y: float, max_depth: float) -> bool { - chr_safe_x = x - chr_safe_z = z - if chr_safe_ok(x, z, from_y, max_depth) { return true } +export function char_safe_spot(character_st: mut CharacterState, x: float, z: float, from_y: float, max_depth: float) -> bool { + character_st.chr_safe_x = x + character_st.chr_safe_z = z + if chr_safe_ok(character_st, x, z, from_y, max_depth) { return true } for r in 0 .. 9 { let rad = 0.8 + float(r) * 1.1 for k in 0 .. 16 { let a = float(k) * (chr_pi / 8.0) let px = x + Math.sin(a) * rad let pz = z + Math.cos(a) * rad - if chr_safe_ok(px, pz, from_y, max_depth) { - chr_safe_x = px - chr_safe_z = pz + if chr_safe_ok(character_st, px, pz, from_y, max_depth) { + character_st.chr_safe_x = px + character_st.chr_safe_z = pz return true } } diff --git a/packages/ludic.character/sit.ludic b/packages/ludic.character/sit.ludic index 2f786111..f87b6cde 100644 --- a/packages/ludic.character/sit.ludic +++ b/packages/ludic.character/sit.ludic @@ -1,54 +1,54 @@ # sit.ludic - sitting: on a seat's point, or on the ground where the body stands. The pose either # side is a still one, so the sit's own weight (body_sit_t) eases 0 -> 1 over a quarter second and # the body walks the last step to the seat over the same time rather than jumping there. -export function char_sit(x: float, z: float, yaw: float, mode: int) -> void { - chr_sit = true - chr_sit_mode = mode - chr_speed = 0.0 - chr_move = 0.0 - chr_run = 0.0 - chr_sit_x = x - chr_sit_z = z - chr_sit_yaw = yaw - chr_water = CharacterGround.water(chr_x, chr_z) +export function char_sit(character_st: mut CharacterState, x: float, z: float, yaw: float, mode: int) -> void { + character_st.chr_sit = true + character_st.chr_sit_mode = mode + character_st.chr_speed = 0.0 + character_st.chr_move = 0.0 + character_st.chr_run = 0.0 + character_st.chr_sit_x = x + character_st.chr_sit_z = z + character_st.chr_sit_yaw = yaw + character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z) } -function chr_sit_tick(dt: float) -> void { +function chr_sit_tick(character_st: mut CharacterState, dt: float) -> void { var want = 0.0 - if chr_sit { want = 1.0 } - chr_sit_t = char_toward(chr_sit_t, want, 4.0 * dt) - if not chr_sit { return } + if character_st.chr_sit { want = 1.0 } + character_st.chr_sit_t = char_toward(character_st.chr_sit_t, want, 4.0 * dt) + if not character_st.chr_sit { return } let k = Math.min(1.0, 8.0 * dt) - chr_x = Math.lerp(chr_x, chr_sit_x, k) - chr_z = Math.lerp(chr_z, chr_sit_z, k) - chr_yaw = chr_yaw + char_wrap(chr_sit_yaw - chr_yaw) * k - chr_y = CharacterGround.height(chr_x, chr_z) - chr_water = CharacterGround.water(chr_x, chr_z) + character_st.chr_x = Math.lerp(character_st.chr_x, character_st.chr_sit_x, k) + character_st.chr_z = Math.lerp(character_st.chr_z, character_st.chr_sit_z, k) + character_st.chr_yaw = character_st.chr_yaw + char_wrap(character_st.chr_sit_yaw - character_st.chr_yaw) * k + character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z) + character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z) } # Sitting down where the body stands: not astride something, not in the air, not on a slope, # not in the water. Asked again while sitting, it stands up. The answer is a CHAR_* for the game # to word. -export function char_sit_here() -> int { - if chr_sit { - char_stand() +export function char_sit_here(character_st: mut CharacterState) -> int { + if character_st.chr_sit { + char_stand(character_st) return CHAR_STOOD } if CharacterBody.riding() { return CHAR_NOT_RIDING } - if chr_air { return CHAR_NOT_AIRBORNE } - if CharacterGround.slope(chr_x, chr_z) > 0.35 { return CHAR_NOT_STEEP } - if chr_y < chr_water + 0.9 { return CHAR_NOT_WET } - char_sit(chr_x, chr_z, chr_yaw, CHAR_SIT_GROUND) + if character_st.chr_air { return CHAR_NOT_AIRBORNE } + if CharacterGround.slope(character_st.chr_x, character_st.chr_z) > 0.35 { return CHAR_NOT_STEEP } + if character_st.chr_y < character_st.chr_water + 0.9 { return CHAR_NOT_WET } + char_sit(character_st, character_st.chr_x, character_st.chr_z, character_st.chr_yaw, CHAR_SIT_GROUND) return CHAR_SAT } # standing up out of a seat put down against a boulder or a bank must not leave the body inside it -export function char_stand() -> void { - chr_sit = false - if char_safe_spot(chr_x, chr_z, chr_y, 0.0) { - chr_x = chr_safe_x - chr_z = chr_safe_z - chr_y = CharacterGround.height(chr_x, chr_z) +export function char_stand(character_st: mut CharacterState) -> void { + character_st.chr_sit = false + if char_safe_spot(character_st, character_st.chr_x, character_st.chr_z, character_st.chr_y, 0.0) { + character_st.chr_x = character_st.chr_safe_x + character_st.chr_z = character_st.chr_safe_z + character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z) } } diff --git a/packages/ludic.character/state.ludic b/packages/ludic.character/state.ludic index f4b414b5..85afcfd0 100644 --- a/packages/ludic.character/state.ludic +++ b/packages/ludic.character/state.ludic @@ -1,77 +1,25 @@ # state.ludic - the body, its camera, and the queue of what happened to it const chr_pi: float = 3.14159265 -var chr_x: float = 0.0 -var chr_y: float = 0.0 -var chr_z: float = 0.0 -var chr_yaw: float = 0.0 # facing, radians; 0 faces -z -var chr_speed: float = 0.0 # m/s along the facing -var chr_phase: float = 0.0 # the gait: one cycle is two steps, advanced by ground covered -var chr_move: float = 0.0 # 0 standing .. 1 moving -var chr_run: float = 0.0 # 0 walking .. 1 running -var chr_time: float = 0.0 -var chr_water: float = -10000.0 # the water line under the body, every update -var chr_walk_speed: float = 3.2 -var chr_run_speed: float = 6.8 -var chr_vy: float = 0.0 -var chr_air: bool = false -var chr_air_vx: float = 0.0 -var chr_air_vz: float = 0.0 -var chr_land: float = 0.0 # seconds of the landing's crouch left -var chr_steep: float = 0.0 # seconds of "that was too steep" left to say -var chr_can_run: bool = true -var chr_step: float = 0.0 # metres covered this frame -var chr_sit: bool = false -var chr_sit_mode: int = 0 -var chr_sit_t: float = 0.0 # 0 standing .. 1 sat, eased over a quarter second -var chr_sit_x: float = 0.0 # the seat's point, walked to over the blend -var chr_sit_z: float = 0.0 -var chr_sit_yaw: float = 0.0 -var chr_swim: bool = false -var chr_dive: bool = false -var chr_depth: float = 0.0 # metres below the surface -var chr_breath: float = 0.0 -var chr_float: float = 0.0 # 0 swimming .. 1 floating -var chr_swim_phase: float = 0.0 -var chr_edge_said: float = 0.0 -var chr_holds: int = CHAR_HOLD_SCREEN # the camera: an orbit round a pivot at the chest, or the eyes -var chr_cam_yaw: float = 0.0 -var chr_cam_pitch: float = -0.157 -var chr_cam_dist: float = 4.2 -var chr_lock: bool = false # the orbit never settles behind on its own -var chr_fps: bool = false -var chr_fps_bob: float = 0.0 -var chr_piv_x: float = 0.0 -var chr_piv_y: float = 0.0 -var chr_piv_z: float = 0.0 -var chr_eye_x: float = 0.0 # where the camera is and where it looks, once computed -var chr_eye_y: float = 0.0 -var chr_eye_z: float = 0.0 -var chr_eye_yaw: float = 0.0 -var chr_eye_pitch: float = 0.0 -var chr_eyes_now: bool = false # the last camera was the eyes: the figure is hidden -var chr_safe_x: float = 0.0 -var chr_safe_z: float = 0.0 -var chr_fact_q: Queue = null -export function char_facts() -> Queue { - if chr_fact_q == null { chr_fact_q = queue_new("character.facts") } - return chr_fact_q +export function char_facts(base_st: mut BaseState, character_st: mut CharacterState) -> Queue { + if character_st.chr_fact_q == null { character_st.chr_fact_q = queue_new(base_st, "character.facts") } + return character_st.chr_fact_q } -function chr_say(what: int, v: float) -> void { +function chr_say(base_st: mut BaseState, character_st: mut CharacterState, what: int, v: float) -> void { let f = new CharacterFact f.what = what - f.x = chr_x - f.y = chr_y - f.z = chr_z + f.x = character_st.chr_x + f.y = character_st.chr_y + f.z = character_st.chr_z f.v = v - q_push(char_facts(), f) + q_push(base_st, char_facts(base_st, character_st), f) } diff --git a/packages/ludic.character/steer.ludic b/packages/ludic.character/steer.ludic index 9c9e160b..deeaa073 100644 --- a/packages/ludic.character/steer.ludic +++ b/packages/ludic.character/steer.ludic @@ -1,61 +1,61 @@ # steer.ludic - turning to the stick and easing to the speed it asks; then the gait and the orbit -function chr_turn_to(dx: float, dz: float, rate: float) -> float { - let diff = char_wrap(Math.atan2(-dx, -dz) - chr_yaw) - chr_yaw = chr_yaw + Math.clamp(diff, -rate, rate) +function chr_turn_to(character_st: mut CharacterState, dx: float, dz: float, rate: float) -> float { + let diff = char_wrap(Math.atan2(-dx, -dz) - character_st.chr_yaw) + character_st.chr_yaw = character_st.chr_yaw + Math.clamp(diff, -rate, rate) return diff } -function chr_steer(dx: float, dz: float, mag: float, run: bool, dt: float) -> void { +function chr_steer(character_st: mut CharacterState, dx: float, dz: float, mag: float, run: bool, dt: float) -> void { var target = 0.0 - if mag > 0.05 and not chr_air { - let diff = chr_turn_to(dx, dz, Math.lerp(10.0, 5.0, chr_run) * dt) + if mag > 0.05 and not character_st.chr_air { + let diff = chr_turn_to(character_st, dx, dz, Math.lerp(10.0, 5.0, character_st.chr_run) * dt) let lined = Math.clamp((Math.cos(diff) + 0.3) / 1.3, 0.0, 1.0) - var top = chr_walk_speed * CharacterBody.pace() - if run { top = chr_run_speed * CharacterBody.pace() } + var top = character_st.chr_walk_speed * CharacterBody.pace() + if run { top = character_st.chr_run_speed * CharacterBody.pace() } if CharacterBody.sneaking() { top = 1.3 } target = mag * top * lined - } else if CharacterBody.holding() and not chr_air { + } else if CharacterBody.holding() and not character_st.chr_air { # a hold turns the body to the thing being worked, so the work is seen being done - chr_turn_to(CharacterBody.hold_x() - chr_x, CharacterBody.hold_z() - chr_z, 6.0 * dt) + chr_turn_to(character_st, CharacterBody.hold_x() - character_st.chr_x, CharacterBody.hold_z() - character_st.chr_z, 6.0 * dt) } # in the water the top speed and the inertia are the water's: a swim is half a walk - if chr_swim { + if character_st.chr_swim { target = mag * 1.5 if run { target = mag * 2.4 } - if mag > 0.05 { chr_turn_to(dx, dz, 4.0 * dt) } + if mag > 0.05 { chr_turn_to(character_st, dx, dz, 4.0 * dt) } } - if not chr_air { + if not character_st.chr_air { var acc = 12.0 - if target > chr_speed { acc = 9.0 } - if chr_swim { acc = 4.5 } - chr_speed = char_toward(chr_speed, target, acc * dt) + if target > character_st.chr_speed { acc = 9.0 } + if character_st.chr_swim { acc = 4.5 } + character_st.chr_speed = char_toward(character_st.chr_speed, target, acc * dt) } } # The gait's phase is distance over the stride, so the feet never slide; a boot comes down as the # phase crosses 0 and again as it crosses pi, and that is when a footfall is said. -function chr_gait(dist: float, dyaw: float, dpitch: float, dt: float) -> void { - if chr_land > 0.0 { chr_land = chr_land - dt } - if chr_steep > 0.0 { chr_steep = chr_steep - dt } - if chr_swim { chr_swim_phase = (chr_swim_phase + dt * (1.6 + chr_speed * 0.9)) % (2.0 * chr_pi) } - let stride = Math.lerp(1.5, 2.7, chr_run) - let phase0 = chr_phase - chr_phase = (chr_phase + dist / stride * (2.0 * chr_pi)) % (2.0 * chr_pi) - if not chr_air and not chr_swim and not chr_sit and chr_speed > 0.4 { - if chr_cross(phase0, chr_phase, 0.0) or chr_cross(phase0, chr_phase, chr_pi) { chr_say(CHAR_FOOTFALL, chr_speed) } +function chr_gait(base_st: mut BaseState, character_st: mut CharacterState, dist: float, dyaw: float, dpitch: float, dt: float) -> void { + if character_st.chr_land > 0.0 { character_st.chr_land = character_st.chr_land - dt } + if character_st.chr_steep > 0.0 { character_st.chr_steep = character_st.chr_steep - dt } + if character_st.chr_swim { character_st.chr_swim_phase = (character_st.chr_swim_phase + dt * (1.6 + character_st.chr_speed * 0.9)) % (2.0 * chr_pi) } + let stride = Math.lerp(1.5, 2.7, character_st.chr_run) + let phase0 = character_st.chr_phase + character_st.chr_phase = (character_st.chr_phase + dist / stride * (2.0 * chr_pi)) % (2.0 * chr_pi) + if not character_st.chr_air and not character_st.chr_swim and not character_st.chr_sit and character_st.chr_speed > 0.4 { + if chr_cross(phase0, character_st.chr_phase, 0.0) or chr_cross(phase0, character_st.chr_phase, chr_pi) { chr_say(base_st, character_st, CHAR_FOOTFALL, character_st.chr_speed) } } var mv = 0.0 - if chr_speed > 0.12 { mv = 1.0 } - chr_move = char_toward(chr_move, mv, 5.0 * dt) - chr_run = char_toward(chr_run, Math.clamp((chr_speed - 3.6) / 2.8, 0.0, 1.0), 3.0 * dt) - chr_look(dyaw, dpitch) + if character_st.chr_speed > 0.12 { mv = 1.0 } + character_st.chr_move = char_toward(character_st.chr_move, mv, 5.0 * dt) + character_st.chr_run = char_toward(character_st.chr_run, Math.clamp((character_st.chr_speed - 3.6) / 2.8, 0.0, 1.0), 3.0 * dt) + chr_look(character_st, dyaw, dpitch) # the orbit settles slowly behind a walking body unless the player is holding the look - if dyaw == 0.0 and chr_speed > 0.4 and not chr_lock and not CharacterInput.look_always() { - chr_cam_yaw = chr_cam_yaw + char_wrap(chr_yaw - chr_cam_yaw) * Math.min(1.0, 1.1 * dt) + if dyaw == 0.0 and character_st.chr_speed > 0.4 and not character_st.chr_lock and not CharacterInput.look_always() { + character_st.chr_cam_yaw = character_st.chr_cam_yaw + char_wrap(character_st.chr_yaw - character_st.chr_cam_yaw) * Math.min(1.0, 1.1 * dt) } } -function chr_look(dyaw: float, dpitch: float) -> void { - chr_cam_yaw = chr_cam_yaw + dyaw - chr_cam_pitch = Math.clamp(chr_cam_pitch + dpitch, -0.96, 0.61) +function chr_look(character_st: mut CharacterState, dyaw: float, dpitch: float) -> void { + character_st.chr_cam_yaw = character_st.chr_cam_yaw + dyaw + character_st.chr_cam_pitch = Math.clamp(character_st.chr_cam_pitch + dpitch, -0.96, 0.61) } diff --git a/packages/ludic.character/swim.ludic b/packages/ludic.character/swim.ludic index 770341ef..958971ed 100644 --- a/packages/ludic.character/swim.ludic +++ b/packages/ludic.character/swim.ludic @@ -1,22 +1,22 @@ # swim.ludic - afloat: buoyancy, the descent, the breath, and the shore stopping the body -function chr_swim_tick(dx: float, dz: float, jump: bool, dt: float) -> void { +function chr_swim_tick(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, jump: bool, dt: float) -> void { # standing still out here is floating, not swimming - if Math.abs(dx) + Math.abs(dz) > 0.05 { chr_float = 0.0 } else { chr_float = Math.min(1.0, chr_float + dt / 1.5) } - chr_dive_tick(jump, dt) - chr_breath_tick(dt) - chr_current_tick(dt) - let step = chr_speed * dt + if Math.abs(dx) + Math.abs(dz) > 0.05 { character_st.chr_float = 0.0 } else { character_st.chr_float = Math.min(1.0, character_st.chr_float + dt / 1.5) } + chr_dive_tick(character_st, jump, dt) + chr_breath_tick(base_st, character_st, dt) + chr_current_tick(base_st, character_st, dt) + let step = character_st.chr_speed * dt if step > 0.0 { - chr_push(chr_x - Math.sin(chr_yaw) * step, chr_z - Math.cos(chr_yaw) * step, 0.35, chr_y, chr_y + CHAR_BODY) - let mx = chr_out_x - chr_x - let mz = chr_out_z - chr_z - chr_step = Math.sqrt(mx * mx + mz * mz) - chr_x = chr_out_x - chr_z = chr_out_z + chr_push(character_st, character_st.chr_x - Math.sin(character_st.chr_yaw) * step, character_st.chr_z - Math.cos(character_st.chr_yaw) * step, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY) + let mx = character_st.chr_out_x - character_st.chr_x + let mz = character_st.chr_out_z - character_st.chr_z + character_st.chr_step = Math.sqrt(mx * mx + mz * mz) + character_st.chr_x = character_st.chr_out_x + character_st.chr_z = character_st.chr_out_z } # the surface, with a slow bob so a floating body is not a statue on glass - let bob = 0.04 * Math.sin(chr_time * 3.8) - chr_y = chr_water - 0.30 + bob - chr_depth + let bob = 0.04 * Math.sin(character_st.chr_time * 3.8) + character_st.chr_y = character_st.chr_water - 0.30 + bob - character_st.chr_depth # shallow enough to stand up in, and at the top rather than under it - if chr_bed_depth(chr_x, chr_z) < CHAR_STAND_UP and chr_depth < 0.2 { chr_leave_water() } + if chr_bed_depth(character_st.chr_x, character_st.chr_z) < CHAR_STAND_UP and character_st.chr_depth < 0.2 { chr_leave_water(base_st, character_st) } } diff --git a/packages/ludic.character/system.ludic b/packages/ludic.character/system.ludic index 6d4c7476..d0d8983c 100644 --- a/packages/ludic.character/system.ludic +++ b/packages/ludic.character/system.ludic @@ -1,20 +1,20 @@ # system.ludic - the body as a system: reset, a tick from the input port, and nothing saved (where a # body stands is the game's to write, with the rest of the player) -export function char_reset() -> void { - char_teleport(0.0, 0.0, 0.0) - chr_time = 0.0 - chr_phase = 0.0 - chr_breath = CHAR_BREATH_MAX - chr_can_run = true - chr_sit_t = 0.0 - chr_steep = 0.0 - chr_land = 0.0 - chr_edge_said = 0.0 - chr_fps = false - q_clear(char_facts()) +export function char_reset(base_st: mut BaseState, character_st: mut CharacterState) -> void { + char_teleport(character_st, 0.0, 0.0, 0.0) + character_st.chr_time = 0.0 + character_st.chr_phase = 0.0 + character_st.chr_breath = CHAR_BREATH_MAX + character_st.chr_can_run = true + character_st.chr_sit_t = 0.0 + character_st.chr_steep = 0.0 + character_st.chr_land = 0.0 + character_st.chr_edge_said = 0.0 + character_st.chr_fps = false + q_clear(base_st, char_facts(base_st, character_st)) } -function chr_sys_tick(t: Tick) -> void { char_tick(t.dt) } +function chr_sys_tick(base_st: mut BaseState, character_st: mut CharacterState, t: Tick) -> void { char_tick(base_st, character_st, t.dt) } export function char_system() -> System { let s = system_new("character", PH_INPUT) diff --git a/packages/ludic.character/tests/character_test.ludic b/packages/ludic.character/tests/character_test.ludic index 6f2a530f..3b463863 100644 --- a/packages/ludic.character/tests/character_test.ludic +++ b/packages/ludic.character/tests/character_test.ludic @@ -20,24 +20,32 @@ program CharacterTest { return -10000.0 } # a boulder at (0, 6), a metre round and 40 cm high, and a pillar at (-6, 0) - var px: float = 0.0 - var pz: float = 0.0 - function fk_ring(x: float, z: float, cx: float, cz: float, r: float) -> bool { + state CharactertestState { + px: float = 0.0 + pz: float = 0.0 + riding: bool = false + rides: int = 0 + sneaking: bool = false + pace: float = 1.0 + eyes: bool = true + mz: float = 0.0 + } + function fk_ring(charactertest_st: mut CharactertestState, x: float, z: float, cx: float, cz: float, r: float) -> bool { let dx = x - cx let dz = z - cz let d = Math.sqrt(dx * dx + dz * dz) if d >= r { return false } let k = r / Math.max(d, 0.001) - px = cx + dx * k - pz = cz + dz * k + charactertest_st.px = cx + dx * k + charactertest_st.pz = cz + dz * k return true } - function fk_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { - if y0 < 0.4 and fk_ring(x, z, 0.0, 6.0, 1.0 + r) { return true } - return fk_ring(x, z, -6.0, 0.0, 0.5 + r) + function fk_push(charactertest_st: mut CharactertestState, x: float, z: float, r: float, y0: float, y1: float) -> bool { + if y0 < 0.4 and fk_ring(charactertest_st, x, z, 0.0, 6.0, 1.0 + r) { return true } + return fk_ring(charactertest_st, x, z, -6.0, 0.0, 0.5 + r) } - function fk_px() -> float { return px } - function fk_pz() -> float { return pz } + function fk_px(charactertest_st: CharactertestState) -> float { return charactertest_st.px } + function fk_pz(charactertest_st: CharactertestState) -> float { return charactertest_st.pz } function fk_top(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float { let dx = x let dz = z - 6.0 @@ -45,228 +53,222 @@ program CharacterTest { return base } - var riding: bool = false - var rides: int = 0 - var sneaking: bool = false - var pace: float = 1.0 - var eyes: bool = true - function fk_riding() -> bool { return riding } - function fk_ride(ix: float, iz: float, run: bool, dt: float) -> void { rides += 1 } - function fk_sneak() -> bool { return sneaking } - function fk_pace() -> float { return pace } - function fk_eyes() -> bool { return eyes } + function fk_riding(charactertest_st: CharactertestState) -> bool { return charactertest_st.riding } + function fk_ride(charactertest_st: mut CharactertestState, ix: float, iz: float, run: bool, dt: float) -> void { charactertest_st.rides += 1 } + function fk_sneak(charactertest_st: CharactertestState) -> bool { return charactertest_st.sneaking } + function fk_pace(charactertest_st: CharactertestState) -> float { return charactertest_st.pace } + function fk_eyes(charactertest_st: CharactertestState) -> bool { return charactertest_st.eyes } - var mz: float = 0.0 - function fk_move_z() -> float { return mz } + function fk_move_z(charactertest_st: CharactertestState) -> float { return charactertest_st.mz } bind CharacterGround { height: fn fk_height, water: fn fk_water, push: fn fk_push, pushed_x: fn fk_px, pushed_z: fn fk_pz, top_at: fn fk_top } bind CharacterBody { riding: fn fk_riding, ride: fn fk_ride, sneaking: fn fk_sneak, pace: fn fk_pace, eyes_ok: fn fk_eyes } bind CharacterInput { move_z: fn fk_move_z } - function fresh(x: float, z: float, yaw: float) -> void { - char_reset() - char_release(CHAR_HOLD_SCREEN) - char_teleport(x, z, yaw) + function fresh(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float, yaw: float) -> void { + char_reset(base_st, character_st) + char_release(character_st, CHAR_HOLD_SCREEN) + char_teleport(character_st, x, z, yaw) } - function facts() -> []CharacterFact { return q_drain(char_facts()) } + function facts(base_st: mut BaseState, character_st: mut CharacterState) -> []CharacterFact { return q_drain(base_st, char_facts(base_st, character_st)) } function count(fs: []CharacterFact, what: int) -> int { var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } # walk the stick straight ahead (the camera looks where the body faces) for `secs` - function walk(secs: float, hz: float, run: bool) -> void { + function walk(base_st: mut BaseState, character_st: mut CharacterState, secs: float, hz: float, run: bool) -> void { let n = int(secs * hz) - for i in 0 .. n { char_update(0.0, 1.0, run, false, 0.0, 0.0, 1.0 / hz) } + for i in 0 .. n { char_update(base_st, character_st, 0.0, 1.0, run, false, 0.0, 0.0, 1.0 / hz) } } # yaw 0 faces -z; the heading of a walk toward +x is -pi/2 const EAST: float = -1.5707963 const SOUTH: float = 3.14159265 - test "a 45 cm deck is a step at 60 Hz and at 240" { - fresh(1.0, 0.0, EAST) - walk(2.0, 60.0, false) - expect(body_x() > 5.0) - expect_near(body_y(), 0.45, 0.01) - fresh(1.0, 0.0, EAST) - walk(2.0, 240.0, false) - expect(body_x() > 5.0) - expect_near(body_y(), 0.45, 0.01) - expect_eq(count(facts(), CHAR_TOO_STEEP), 0) + test "a 45 cm deck is a step at 60 Hz and at 240" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 1.0, 0.0, EAST) + walk(base_st, character_st, 2.0, 60.0, false) + expect(body_x(character_st) > 5.0) + expect_near(body_y(character_st), 0.45, 0.01) + fresh(base_st, character_st, 1.0, 0.0, EAST) + walk(base_st, character_st, 2.0, 240.0, false) + expect(body_x(character_st) > 5.0) + expect_near(body_y(character_st), 0.45, 0.01) + expect_eq(count(facts(base_st, character_st), CHAR_TOO_STEEP), 0) } - test "a cliff is refused at the same place whatever the frame rate, and said once" { - fresh(0.0, -8.0, 0.0) - walk(3.0, 30.0, false) - let at30 = body_z() - expect(body_steep() > 0.0) - expect_eq(count(facts(), CHAR_TOO_STEEP), 1) - fresh(0.0, -8.0, 0.0) - walk(3.0, 240.0, false) - expect_near(body_z(), at30, 0.25) - expect(body_z() > -10.0) - expect(body_y() < 0.3) + test "a cliff is refused at the same place whatever the frame rate, and said once" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 0.0, -8.0, 0.0) + walk(base_st, character_st, 3.0, 30.0, false) + let at30 = body_z(character_st) + expect(body_steep(character_st) > 0.0) + expect_eq(count(facts(base_st, character_st), CHAR_TOO_STEEP), 1) + fresh(base_st, character_st, 0.0, -8.0, 0.0) + walk(base_st, character_st, 3.0, 240.0, false) + expect_near(body_z(character_st), at30, 0.25) + expect(body_z(character_st) > -10.0) + expect(body_y(character_st) < 0.3) } - test "a gentle rise is walked up" { - fresh(0.0, 8.0, SOUTH) - walk(3.0, 60.0, false) - expect(body_z() > 14.0) - expect(body_y() > 1.0) + test "a gentle rise is walked up" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 0.0, 8.0, SOUTH) + walk(base_st, character_st, 3.0, 60.0, false) + expect(body_z(character_st) > 14.0) + expect(body_y(character_st) > 1.0) } - test "the boots set the limit: a rise they cannot take is refused" { - fresh(0.0, 8.0, SOUTH) - walk(0.2, 60.0, false) - expect(not char_slope_blocks(body_y() + 0.1, 0.0, 1.0)) + test "the boots set the limit: a rise they cannot take is refused" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 0.0, 8.0, SOUTH) + walk(base_st, character_st, 0.2, 60.0, false) + expect(not char_slope_blocks(character_st, body_y(character_st) + 0.1, 0.0, 1.0)) } - test "the walk reaches the walk speed, the run the run speed, a sneak its own" { - fresh(0.0, 0.0, SOUTH) - walk(2.0, 60.0, false) - expect_near(body_speed(), 3.2, 0.05) - fresh(0.0, 0.0, SOUTH) - walk(2.0, 60.0, true) - expect_near(body_speed(), 6.8, 0.05) - pace = 0.5 - fresh(0.0, 0.0, SOUTH) - walk(2.0, 60.0, false) - expect_near(body_speed(), 1.6, 0.05) - pace = 1.0 - sneaking = true - fresh(0.0, 0.0, SOUTH) - walk(2.0, 60.0, true) - expect_near(body_speed(), 1.3, 0.05) + test "the walk reaches the walk speed, the run the run speed, a sneak its own" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { + fresh(base_st, character_st, 0.0, 0.0, SOUTH) + walk(base_st, character_st, 2.0, 60.0, false) + expect_near(body_speed(character_st), 3.2, 0.05) + fresh(base_st, character_st, 0.0, 0.0, SOUTH) + walk(base_st, character_st, 2.0, 60.0, true) + expect_near(body_speed(character_st), 6.8, 0.05) + charactertest_st.pace = 0.5 + fresh(base_st, character_st, 0.0, 0.0, SOUTH) + walk(base_st, character_st, 2.0, 60.0, false) + expect_near(body_speed(character_st), 1.6, 0.05) + charactertest_st.pace = 1.0 + charactertest_st.sneaking = true + fresh(base_st, character_st, 0.0, 0.0, SOUTH) + walk(base_st, character_st, 2.0, 60.0, true) + expect_near(body_speed(character_st), 1.3, 0.05) } - test "a boulder is a wall to a body below its step and a step onto its top" { - fresh(0.0, 3.0, SOUTH) - walk(2.0, 60.0, false) - expect(body_z() < 5.1) + test "a boulder is a wall to a body below its step and a step onto its top" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 0.0, 3.0, SOUTH) + walk(base_st, character_st, 2.0, 60.0, false) + expect(body_z(character_st) < 5.1) expect_near(char_stand_at(0.0, 6.0, 0.0), 0.4, 0.001) expect_near(char_stand_at(3.0, 6.0, 0.0), 0.0, 0.001) # and the world's own ground never includes it expect_near(fk_height(0.0, 6.0), 0.0, 0.001) } - test "wading shelves into a swim, and the shallows give the ground back" { - fresh(18.0, 0.0, EAST) - walk(3.0, 60.0, false) - let fs = facts() - expect(body_swimming()) + test "wading shelves into a swim, and the shallows give the ground back" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 18.0, 0.0, EAST) + walk(base_st, character_st, 3.0, 60.0, false) + let fs = facts(base_st, character_st) + expect(body_swimming(character_st)) expect_eq(count(fs, CHAR_STARTED_SWIMMING), 1) - expect_near(body_y(), -0.30, 0.06) - expect(body_breath() > 40.0) - char_face(-EAST) - char_look(-EAST, 0.0) - walk(8.0, 60.0, false) - expect(not body_swimming()) - expect_eq(count(facts(), CHAR_LEFT_WATER), 1) + expect_near(body_y(character_st), -0.30, 0.06) + expect(body_breath(character_st) > 40.0) + char_face(character_st, -EAST) + char_look(character_st, -EAST, 0.0) + walk(base_st, character_st, 8.0, 60.0, false) + expect(not body_swimming(character_st)) + expect_eq(count(facts(base_st, character_st), CHAR_LEFT_WATER), 1) } - test "the jump held dives, the air runs out, and the body surfaces" { - fresh(30.0, 0.0, 0.0) - char_resume_swim(false, 0.0, 2.0) - for i in 0 .. 600 { char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) } - let fs = facts() + test "the jump held dives, the air runs out, and the body surfaces" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 30.0, 0.0, 0.0) + char_resume_swim(character_st, false, 0.0, 2.0) + for i in 0 .. 600 { char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) } + let fs = facts(base_st, character_st) expect(count(fs, CHAR_OUT_OF_AIR) > 0) - for i in 0 .. 300 { char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) } - expect(body_depth() < 0.05) - expect(body_breath() > 10.0) + for i in 0 .. 300 { char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) } + expect(body_depth(character_st) < 0.05) + expect(body_breath(character_st) > 10.0) } - test "a jump leaves the ground, falls, lands, and says both" { - fresh(0.0, 0.0, 0.0) - char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) - expect(body_airborne()) + test "a jump leaves the ground, falls, lands, and says both" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 0.0, 0.0, 0.0) + char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) + expect(body_airborne(character_st)) var guard = 0 - while body_airborne() and guard < 200 { - char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) + while body_airborne(character_st) and guard < 200 { + char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) guard += 1 } - let fs = facts() - expect(not body_airborne()) + let fs = facts(base_st, character_st) + expect(not body_airborne(character_st)) expect_eq(count(fs, CHAR_JUMPED), 1) expect_eq(count(fs, CHAR_LANDED), 1) expect(guard > 30) - char_allow_run(false) - char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) - expect(not body_airborne()) + char_allow_run(character_st, false) + char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) + expect(not body_airborne(character_st)) } - test "the safe spot clears a collider, keeps out of deep water, and allows a wade when asked" { - expect(char_safe_spot(-6.0, 0.0, 0.0, 0.0)) - let dx = body_safe_x() + 6.0 - let dz = body_safe_z() + test "the safe spot clears a collider, keeps out of deep water, and allows a wade when asked" (character_st: mut CharacterState) { + expect(char_safe_spot(character_st, -6.0, 0.0, 0.0, 0.0)) + let dx = body_safe_x(character_st) + 6.0 + let dz = body_safe_z(character_st) expect(dx * dx + dz * dz > 0.81) - expect(not char_safe_spot(40.0, 0.0, 0.0, 0.0)) - expect(char_safe_spot(20.5, 0.0, 0.0, 1.25)) - expect(char_safe_spot(15.0, 0.0, 0.0, 0.0)) - expect_near(body_safe_x(), 15.0, 0.001) + expect(not char_safe_spot(character_st, 40.0, 0.0, 0.0, 0.0)) + expect(char_safe_spot(character_st, 20.5, 0.0, 0.0, 1.25)) + expect(char_safe_spot(character_st, 15.0, 0.0, 0.0, 0.0)) + expect_near(body_safe_x(character_st), 15.0, 0.001) } - test "sitting where you stand, and the refusals" { - fresh(0.0, 0.0, 0.0) - expect_eq(char_sit_here(), CHAR_SAT) - for i in 0 .. 30 { char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) } - expect(body_sitting()) - expect_near(body_sit_t(), 1.0, 0.01) - expect_eq(char_sit_here(), CHAR_STOOD) - expect(not body_sitting()) - fresh(0.0, -14.0, 0.0) - expect_eq(char_sit_here(), CHAR_NOT_STEEP) - fresh(24.0, 0.0, 0.0) - char_resume_swim(false, 0.0, 10.0) - expect_eq(char_sit_here(), CHAR_NOT_WET) - riding = true - fresh(0.0, 0.0, 0.0) - expect_eq(char_sit_here(), CHAR_NOT_RIDING) - riding = false + test "sitting where you stand, and the refusals" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { + fresh(base_st, character_st, 0.0, 0.0, 0.0) + expect_eq(char_sit_here(character_st), CHAR_SAT) + for i in 0 .. 30 { char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) } + expect(body_sitting(character_st)) + expect_near(body_sit_t(character_st), 1.0, 0.01) + expect_eq(char_sit_here(character_st), CHAR_STOOD) + expect(not body_sitting(character_st)) + fresh(base_st, character_st, 0.0, -14.0, 0.0) + expect_eq(char_sit_here(character_st), CHAR_NOT_STEEP) + fresh(base_st, character_st, 24.0, 0.0, 0.0) + char_resume_swim(character_st, false, 0.0, 10.0) + expect_eq(char_sit_here(character_st), CHAR_NOT_WET) + charactertest_st.riding = true + fresh(base_st, character_st, 0.0, 0.0, 0.0) + expect_eq(char_sit_here(character_st), CHAR_NOT_RIDING) + charactertest_st.riding = false } - test "a hold stops the stick, and only its own reason lets it go" { - fresh(0.0, 0.0, 0.0) - mz = 1.0 - char_hold(CHAR_HOLD_STAGE) - char_hold(CHAR_HOLD_SCREEN) - for i in 0 .. 60 { char_tick(1.0 / 60.0) } - expect_near(body_z(), 0.0, 0.001) - char_release(CHAR_HOLD_SCREEN) - char_release(CHAR_HOLD_SCREEN) - expect(body_held()) - char_release(CHAR_HOLD_STAGE) - for i in 0 .. 60 { char_tick(1.0 / 60.0) } - expect(body_z() < -1.0) - mz = 0.0 + test "a hold stops the stick, and only its own reason lets it go" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { + fresh(base_st, character_st, 0.0, 0.0, 0.0) + charactertest_st.mz = 1.0 + char_hold(character_st, CHAR_HOLD_STAGE) + char_hold(character_st, CHAR_HOLD_SCREEN) + for i in 0 .. 60 { char_tick(base_st, character_st, 1.0 / 60.0) } + expect_near(body_z(character_st), 0.0, 0.001) + char_release(character_st, CHAR_HOLD_SCREEN) + char_release(character_st, CHAR_HOLD_SCREEN) + expect(body_held(character_st)) + char_release(character_st, CHAR_HOLD_STAGE) + for i in 0 .. 60 { char_tick(base_st, character_st, 1.0 / 60.0) } + expect(body_z(character_st) < -1.0) + charactertest_st.mz = 0.0 } - test "riding hands the stick over and leaves the body where the ride puts it" { - fresh(0.0, 0.0, 0.0) - riding = true - rides = 0 - char_board(3.0, 3.0, 0.0) - for i in 0 .. 10 { char_update(0.0, 1.0, false, false, 0.0, 0.0, 1.0 / 60.0) } - expect_eq(rides, 10) - char_ride_at(4.0, 1.2, 3.0, 0.0, 2.0) - expect_near(body_x(), 4.0, 0.001) - expect_near(body_y(), 1.2, 0.001) - riding = false - char_step_off(5.0, 0.0) - expect_near(body_y(), 0.45, 0.001) + test "riding hands the stick over and leaves the body where the ride puts it" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) { + fresh(base_st, character_st, 0.0, 0.0, 0.0) + charactertest_st.riding = true + charactertest_st.rides = 0 + char_board(character_st, 3.0, 3.0, 0.0) + for i in 0 .. 10 { char_update(base_st, character_st, 0.0, 1.0, false, false, 0.0, 0.0, 1.0 / 60.0) } + expect_eq(charactertest_st.rides, 10) + char_ride_at(character_st, 4.0, 1.2, 3.0, 0.0, 2.0) + expect_near(body_x(character_st), 4.0, 0.001) + expect_near(body_y(character_st), 1.2, 0.001) + charactertest_st.riding = false + char_step_off(character_st, 5.0, 0.0) + expect_near(body_y(character_st), 0.45, 0.001) } - test "the orbit sits behind and above the ground; the eyes at head height, refused when sat" { - fresh(0.0, 0.0, 0.0) - char_camera(1.0) - expect(body_eye_z() > 1.0) - expect(body_eye_y() > 0.55) - expect(not body_eyes_now()) - expect_eq(char_fps_toggle(), CHAR_EYES_ON) - char_camera(1.0) - expect(body_eyes_now()) - expect_near(body_eye_y(), 1.62, 0.05) - expect_eq(char_fps_toggle(), CHAR_EYES_OFF) - char_sit(0.0, 0.0, 0.0, CHAR_SIT_SEAT) - expect_eq(char_fps_toggle(), CHAR_EYES_SITTING) + test "the orbit sits behind and above the ground; the eyes at head height, refused when sat" (base_st: mut BaseState, character_st: mut CharacterState) { + fresh(base_st, character_st, 0.0, 0.0, 0.0) + char_camera(character_st, 1.0) + expect(body_eye_z(character_st) > 1.0) + expect(body_eye_y(character_st) > 0.55) + expect(not body_eyes_now(character_st)) + expect_eq(char_fps_toggle(character_st), CHAR_EYES_ON) + char_camera(character_st, 1.0) + expect(body_eyes_now(character_st)) + expect_near(body_eye_y(character_st), 1.62, 0.05) + expect_eq(char_fps_toggle(character_st), CHAR_EYES_OFF) + char_sit(character_st, 0.0, 0.0, 0.0, CHAR_SIT_SEAT) + expect_eq(char_fps_toggle(character_st), CHAR_EYES_SITTING) } } diff --git a/packages/ludic.character/update.ludic b/packages/ludic.character/update.ludic index 861a1b0e..bd2f2cd7 100644 --- a/packages/ludic.character/update.ludic +++ b/packages/ludic.character/update.ludic @@ -1,28 +1,28 @@ # update.ludic - a frame of the body: the stick in the camera's frame, the step, the gait, the camera -export function char_update(ix0: float, iz0: float, run0: bool, jump0: bool, dyaw: float, dpitch: float, dt: float) -> void { - var run = run0 and chr_can_run +export function char_update(base_st: mut BaseState, character_st: mut CharacterState, ix0: float, iz0: float, run0: bool, jump0: bool, dyaw: float, dpitch: float, dt: float) -> void { + var run = run0 and character_st.chr_can_run var ix = ix0 var iz = iz0 var jump = jump0 - chr_step = 0.0 - chr_time = chr_time + dt - chr_water = CharacterGround.water(chr_x, chr_z) - chr_sit_tick(dt) + character_st.chr_step = 0.0 + character_st.chr_time = character_st.chr_time + dt + character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z) + chr_sit_tick(character_st, dt) if CharacterBody.riding() { - chr_ride_update(ix, iz, run, dyaw, dpitch, dt) + chr_ride_update(character_st, ix, iz, run, dyaw, dpitch, dt) return } if CharacterBody.sneaking() { run = false } # sitting: any step stands up, and nothing else moves - if chr_sit and (Math.abs(ix) > 0.3 or Math.abs(iz) > 0.3 or jump) { chr_sit = false } - if chr_sit { + if character_st.chr_sit and (Math.abs(ix) > 0.3 or Math.abs(iz) > 0.3 or jump) { character_st.chr_sit = false } + if character_st.chr_sit { ix = 0.0 iz = 0.0 jump = false } # the stick in the camera's frame: forward on the ground is (-sin yaw, -cos yaw) - let cy = Math.cos(chr_cam_yaw) - let sy = Math.sin(chr_cam_yaw) + let cy = Math.cos(character_st.chr_cam_yaw) + let sy = Math.sin(character_st.chr_cam_yaw) var dx = -sy * iz + cy * ix var dz = -cy * iz - sy * ix var mag = Math.sqrt(dx * dx + dz * dz) @@ -31,30 +31,30 @@ export function char_update(ix0: float, iz0: float, run0: bool, jump0: bool, dya dz = dz / mag mag = 1.0 } - chr_steer(dx, dz, mag, run, dt) - let dist = chr_travel(dx, dz, -Math.sin(chr_yaw), -Math.cos(chr_yaw), jump, dt) - chr_gait(dist, dyaw, dpitch, dt) - char_camera(dt) + chr_steer(character_st, dx, dz, mag, run, dt) + let dist = chr_travel(base_st, character_st, dx, dz, -Math.sin(character_st.chr_yaw), -Math.cos(character_st.chr_yaw), jump, dt) + chr_gait(base_st, character_st, dist, dyaw, dpitch, dt) + char_camera(character_st, dt) } # something else carries the body (CharacterBody.ride moves it with char_ride_at); the orbit # drifts behind whatever it is on -function chr_ride_update(ix: float, iz: float, run: bool, dyaw: float, dpitch: float, dt: float) -> void { +function chr_ride_update(character_st: mut CharacterState, ix: float, iz: float, run: bool, dyaw: float, dpitch: float, dt: float) -> void { CharacterBody.ride(ix, iz, run, dt) - chr_look(dyaw, dpitch) - if dyaw == 0.0 and not CharacterInput.look_always() { chr_cam_yaw = chr_cam_yaw + char_wrap(chr_yaw - chr_cam_yaw) * Math.min(1.0, 1.5 * dt) } - char_camera(dt) + chr_look(character_st, dyaw, dpitch) + if dyaw == 0.0 and not CharacterInput.look_always() { character_st.chr_cam_yaw = character_st.chr_cam_yaw + char_wrap(character_st.chr_yaw - character_st.chr_cam_yaw) * Math.min(1.0, 1.5 * dt) } + char_camera(character_st, dt) } # a frame from the input port: the game's devices, read once, stepping the body -export function char_tick(dt: float) -> void { +export function char_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void { var ix = 0.0 var iz = 0.0 var run = false var jump = false var dyaw = 0.0 var dpitch = 0.0 - if chr_holds == 0 { + if character_st.chr_holds == 0 { ix = CharacterInput.move_x() iz = CharacterInput.move_z() run = CharacterInput.run() @@ -62,7 +62,7 @@ export function char_tick(dt: float) -> void { dyaw = CharacterInput.look_yaw() dpitch = CharacterInput.look_pitch() let wheel = CharacterInput.zoom() - if wheel != 0.0 { chr_cam_dist = Math.clamp(chr_cam_dist - wheel * 0.35, 1.8, 9.0) } + if wheel != 0.0 { character_st.chr_cam_dist = Math.clamp(character_st.chr_cam_dist - wheel * 0.35, 1.8, 9.0) } } - char_update(ix, iz, run, jump, dyaw, dpitch, dt) + char_update(base_st, character_st, ix, iz, run, jump, dyaw, dpitch, dt) } diff --git a/packages/ludic.character/verbs.ludic b/packages/ludic.character/verbs.ludic index e560018f..8797dcb8 100644 --- a/packages/ludic.character/verbs.ludic +++ b/packages/ludic.character/verbs.ludic @@ -1,125 +1,125 @@ # verbs.ludic - every change the rest of a game may make to the body -export function char_config(walk: float, run: float) -> void { - chr_walk_speed = walk - chr_run_speed = run +export function char_config(character_st: mut CharacterState, walk: float, run: float) -> void { + character_st.chr_walk_speed = walk + character_st.chr_run_speed = run } # put the body somewhere, standing, the camera behind it -export function char_teleport(x: float, z: float, yaw: float) -> void { - chr_x = x - chr_z = z - chr_yaw = yaw - chr_y = CharacterGround.height(x, z) - char_halt() - chr_sit = false - chr_swim = false - chr_dive = false - chr_depth = 0.0 - chr_float = 0.0 +export function char_teleport(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void { + character_st.chr_x = x + character_st.chr_z = z + character_st.chr_yaw = yaw + character_st.chr_y = CharacterGround.height(x, z) + char_halt(character_st) + character_st.chr_sit = false + character_st.chr_swim = false + character_st.chr_dive = false + character_st.chr_depth = 0.0 + character_st.chr_float = 0.0 # the water where the body lands, not where it was: a trip reloaded mid-swim reads it next - chr_water = CharacterGround.water(x, z) - chr_cam_yaw = yaw - chr_piv_x = chr_x - chr_piv_y = chr_y + 1.45 - chr_piv_z = chr_z - char_camera(1.0) + character_st.chr_water = CharacterGround.water(x, z) + character_st.chr_cam_yaw = yaw + character_st.chr_piv_x = character_st.chr_x + character_st.chr_piv_y = character_st.chr_y + 1.45 + character_st.chr_piv_z = character_st.chr_z + char_camera(character_st, 1.0) } # a screen holds the body still by a reason, so two holders cannot release each other's hold -export function char_hold(reason: int) -> void { chr_holds = chr_holds | reason } -export function char_release(reason: int) -> void { if (chr_holds & reason) != 0 { chr_holds = chr_holds - reason } } +export function char_hold(character_st: mut CharacterState, reason: int) -> void { character_st.chr_holds = character_st.chr_holds | reason } +export function char_release(character_st: mut CharacterState, reason: int) -> void { if (character_st.chr_holds & reason) != 0 { character_st.chr_holds = character_st.chr_holds - reason } } # everything that carries the body stops: no fall, no stride, no run -export function char_halt() -> void { - chr_air = false - chr_vy = 0.0 - chr_speed = 0.0 - chr_move = 0.0 - chr_run = 0.0 +export function char_halt(character_st: mut CharacterState) -> void { + character_st.chr_air = false + character_st.chr_vy = 0.0 + character_st.chr_speed = 0.0 + character_st.chr_move = 0.0 + character_st.chr_run = 0.0 } # out of strength in the water: the stroke stops and the body floats -export function char_go_limp() -> void { - chr_speed = 0.0 - chr_float = 1.0 +export function char_go_limp(character_st: mut CharacterState) -> void { + character_st.chr_speed = 0.0 + character_st.chr_float = 1.0 } # set down somewhere without the teleport's reset (a swimmer turned back) -export function char_push_to(x: float, z: float) -> void { - chr_x = x - chr_z = z - chr_speed = 0.0 +export function char_push_to(character_st: mut CharacterState, x: float, z: float) -> void { + character_st.chr_x = x + character_st.chr_z = z + character_st.chr_speed = 0.0 } # stood on the ground at a point, facing a way -export function char_stand_on(x: float, z: float, yaw: float) -> void { - chr_x = x - chr_z = z - chr_yaw = yaw - chr_y = CharacterGround.height(x, z) +export function char_stand_on(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void { + character_st.chr_x = x + character_st.chr_z = z + character_st.chr_yaw = yaw + character_st.chr_y = CharacterGround.height(x, z) } # a trip saved in the middle of the lake reloads in the middle of the lake, not on the bed -export function char_resume_swim(dive: bool, depth: float, breath: float) -> void { - chr_water = CharacterGround.water(chr_x, chr_z) - chr_swim = true - chr_dive = dive - chr_depth = depth - chr_breath = breath - chr_y = chr_water - 0.30 - chr_depth +export function char_resume_swim(character_st: mut CharacterState, dive: bool, depth: float, breath: float) -> void { + character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z) + character_st.chr_swim = true + character_st.chr_dive = dive + character_st.chr_depth = depth + character_st.chr_breath = breath + character_st.chr_y = character_st.chr_water - 0.30 - character_st.chr_depth } # the map changed under the body: nothing about the old water may survive -export function char_map_changed() -> void { - chr_swim = false - chr_dive = false - chr_depth = 0.0 - chr_water = CharacterGround.water(chr_x, chr_z) +export function char_map_changed(character_st: mut CharacterState) -> void { + character_st.chr_swim = false + character_st.chr_dive = false + character_st.chr_depth = 0.0 + character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z) } -export function char_face(yaw: float) -> void { chr_yaw = yaw } -export function char_allow_run(on: bool) -> void { chr_can_run = on } -export function char_look(yaw: float, pitch: float) -> void { - chr_cam_yaw = yaw - chr_cam_pitch = pitch +export function char_face(character_st: mut CharacterState, yaw: float) -> void { character_st.chr_yaw = yaw } +export function char_allow_run(character_st: mut CharacterState, on: bool) -> void { character_st.chr_can_run = on } +export function char_look(character_st: mut CharacterState, yaw: float, pitch: float) -> void { + character_st.chr_cam_yaw = yaw + character_st.chr_cam_pitch = pitch } -export function char_look_pitch(pitch: float) -> void { chr_cam_pitch = pitch } -export function char_orbit(dist: float) -> void { chr_cam_dist = Math.clamp(dist, 1.8, 9.0) } -export function char_lock_orbit(on: bool) -> void { chr_lock = on } -export function char_set_fps(on: bool) -> void { chr_fps = on } +export function char_look_pitch(character_st: mut CharacterState, pitch: float) -> void { character_st.chr_cam_pitch = pitch } +export function char_orbit(character_st: mut CharacterState, dist: float) -> void { character_st.chr_cam_dist = Math.clamp(dist, 1.8, 9.0) } +export function char_lock_orbit(character_st: mut CharacterState, on: bool) -> void { character_st.chr_lock = on } +export function char_set_fps(character_st: mut CharacterState, on: bool) -> void { character_st.chr_fps = on } # the eyes or the shoulder; refused while sitting or carried, and the answer says which -export function char_fps_toggle() -> int { - if not chr_fps and not chr_eyes_allowed() { - if chr_sit { return CHAR_EYES_SITTING } +export function char_fps_toggle(character_st: mut CharacterState) -> int { + if not character_st.chr_fps and not chr_eyes_allowed(character_st) { + if character_st.chr_sit { return CHAR_EYES_SITTING } return CHAR_EYES_RIDING } - chr_fps = not chr_fps - if chr_fps { return CHAR_EYES_ON } + character_st.chr_fps = not character_st.chr_fps + if character_st.chr_fps { return CHAR_EYES_ON } return CHAR_EYES_OFF } # something to ride: getting on, being carried, and stepping off onto a spot char_safe_spot found -export function char_board(x: float, z: float, yaw: float) -> void { - chr_sit = false - chr_x = x - chr_z = z - chr_yaw = yaw - chr_speed = 0.0 - chr_move = 0.0 - chr_run = 0.0 +export function char_board(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void { + character_st.chr_sit = false + character_st.chr_x = x + character_st.chr_z = z + character_st.chr_yaw = yaw + character_st.chr_speed = 0.0 + character_st.chr_move = 0.0 + character_st.chr_run = 0.0 } -export function char_ride_at(x: float, y: float, z: float, yaw: float, speed: float) -> void { - chr_x = x - chr_y = y - chr_z = z - chr_yaw = yaw - chr_speed = speed +export function char_ride_at(character_st: mut CharacterState, x: float, y: float, z: float, yaw: float, speed: float) -> void { + character_st.chr_x = x + character_st.chr_y = y + character_st.chr_z = z + character_st.chr_yaw = yaw + character_st.chr_speed = speed } -export function char_step_off(x: float, z: float) -> void { - chr_x = x - chr_z = z - chr_y = CharacterGround.height(x, z) +export function char_step_off(character_st: mut CharacterState, x: float, z: float) -> void { + character_st.chr_x = x + character_st.chr_z = z + character_st.chr_y = CharacterGround.height(x, z) } diff --git a/packages/ludic.character/walk.ludic b/packages/ludic.character/walk.ludic index 39d33882..619ed046 100644 --- a/packages/ludic.character/walk.ludic +++ b/packages/ludic.character/walk.ludic @@ -1,69 +1,69 @@ # walk.ludic - the step itself: swimming, in the air, or on the ground; the distance, for the gait -function chr_travel(dx: float, dz: float, fx: float, fz: float, jump: bool, dt: float) -> float { - if chr_swim { - chr_swim_tick(dx, dz, jump, dt) - return chr_speed * dt +function chr_travel(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, fx: float, fz: float, jump: bool, dt: float) -> float { + if character_st.chr_swim { + chr_swim_tick(base_st, character_st, dx, dz, jump, dt) + return character_st.chr_speed * dt } - if chr_air { return chr_fly(dx, dz, dt) } - return chr_walk(fx, fz, jump, dt) + if character_st.chr_air { return chr_fly(base_st, character_st, dx, dz, dt) } + return chr_walk(base_st, character_st, fx, fz, jump, dt) } # ballistic: gravity, the takeoff's velocity carried, a little steering -function chr_fly(dx: float, dz: float, dt: float) -> float { - chr_vy = chr_vy - 14.0 * dt - chr_air_vx = chr_air_vx + dx * (4.0 * dt) - chr_air_vz = chr_air_vz + dz * (4.0 * dt) - chr_push(chr_x + chr_air_vx * dt, chr_z + chr_air_vz * dt, 0.35, chr_y, chr_y + CHAR_BODY) - chr_y = chr_y + chr_vy * dt - let ground = char_stand_at(chr_out_x, chr_out_z, chr_y) - chr_x = chr_out_x - chr_z = chr_out_z - if chr_y < ground and chr_vy < 0.0 { - chr_y = ground - chr_air = false - chr_land = 0.22 - chr_speed = Math.sqrt(chr_air_vx * chr_air_vx + chr_air_vz * chr_air_vz) - chr_say(CHAR_LANDED, chr_speed) +function chr_fly(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, dt: float) -> float { + character_st.chr_vy = character_st.chr_vy - 14.0 * dt + character_st.chr_air_vx = character_st.chr_air_vx + dx * (4.0 * dt) + character_st.chr_air_vz = character_st.chr_air_vz + dz * (4.0 * dt) + chr_push(character_st, character_st.chr_x + character_st.chr_air_vx * dt, character_st.chr_z + character_st.chr_air_vz * dt, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY) + character_st.chr_y = character_st.chr_y + character_st.chr_vy * dt + let ground = char_stand_at(character_st.chr_out_x, character_st.chr_out_z, character_st.chr_y) + character_st.chr_x = character_st.chr_out_x + character_st.chr_z = character_st.chr_out_z + if character_st.chr_y < ground and character_st.chr_vy < 0.0 { + character_st.chr_y = ground + character_st.chr_air = false + character_st.chr_land = 0.22 + character_st.chr_speed = Math.sqrt(character_st.chr_air_vx * character_st.chr_air_vx + character_st.chr_air_vz * character_st.chr_air_vz) + chr_say(base_st, character_st, CHAR_LANDED, character_st.chr_speed) } - return chr_speed * (dt * 0.4) + return character_st.chr_speed * (dt * 0.4) } # on the ground: a step, unless a slope refuses it or the water takes the body; then a jump -function chr_walk(fx: float, fz: float, jump: bool, dt: float) -> float { - var dist = chr_speed * dt - if dist > 0.0 { dist = chr_walk_step(fx, fz, dist) } else { chr_y = CharacterGround.height(chr_x, chr_z) } - if jump and chr_can_run and chr_land < 0.05 { - chr_air = true - chr_vy = 5.0 - chr_air_vx = fx * chr_speed - chr_air_vz = fz * chr_speed - chr_say(CHAR_JUMPED, 0.0) +function chr_walk(base_st: mut BaseState, character_st: mut CharacterState, fx: float, fz: float, jump: bool, dt: float) -> float { + var dist = character_st.chr_speed * dt + if dist > 0.0 { dist = chr_walk_step(base_st, character_st, fx, fz, dist) } else { character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z) } + if jump and character_st.chr_can_run and character_st.chr_land < 0.05 { + character_st.chr_air = true + character_st.chr_vy = 5.0 + character_st.chr_air_vx = fx * character_st.chr_speed + character_st.chr_air_vz = fz * character_st.chr_speed + chr_say(base_st, character_st, CHAR_JUMPED, 0.0) } return dist } -function chr_walk_step(fx: float, fz: float, dist: float) -> float { - chr_push(chr_x + fx * dist, chr_z + fz * dist, 0.35, chr_y, chr_y + CHAR_BODY) - let nx = chr_out_x - let nz = chr_out_z - let nh = char_stand_at(nx, nz, chr_y) - if char_slope_blocks(nh, fx, fz) { - if not (chr_steep > 0.0) { chr_say(CHAR_TOO_STEEP, nh - chr_y) } - chr_steep = 1.2 - chr_speed = 0.0 +function chr_walk_step(base_st: mut BaseState, character_st: mut CharacterState, fx: float, fz: float, dist: float) -> float { + chr_push(character_st, character_st.chr_x + fx * dist, character_st.chr_z + fz * dist, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY) + let nx = character_st.chr_out_x + let nz = character_st.chr_out_z + let nh = char_stand_at(nx, nz, character_st.chr_y) + if char_slope_blocks(character_st, nh, fx, fz) { + if not (character_st.chr_steep > 0.0) { chr_say(base_st, character_st, CHAR_TOO_STEEP, nh - character_st.chr_y) } + character_st.chr_steep = 1.2 + character_st.chr_speed = 0.0 return 0.0 } # the lake shelves: shin-deep is a wade, and past CHAR_WADE_MAX it is a swim. Entering owns # the position and the height this frame, or the body sits on the bed for one frame. if nh < CharacterGround.water(nx, nz) - CHAR_WADE_MAX { - chr_enter_water(nx, nz) + chr_enter_water(base_st, character_st, nx, nz) return dist } - let mx = nx - chr_x - let mz = nz - chr_z - chr_step = Math.sqrt(mx * mx + mz * mz) - chr_x = nx - chr_z = nz - chr_y = nh + let mx = nx - character_st.chr_x + let mz = nz - character_st.chr_z + character_st.chr_step = Math.sqrt(mx * mx + mz * mz) + character_st.chr_x = nx + character_st.chr_z = nz + character_st.chr_y = nh return dist } diff --git a/packages/ludic.character/water.ludic b/packages/ludic.character/water.ludic index e4a650d8..85acc47c 100644 --- a/packages/ludic.character/water.ludic +++ b/packages/ludic.character/water.ludic @@ -1,73 +1,73 @@ # water.ludic - one axis decides it: how far the bed is below the surface where the body stands. # Under CHAR_WADE_MAX the walk wades; past it the feet leave the bottom. Coming out wants a # shallower bed (CHAR_STAND_UP) than going in did, so a shelving shore does not flicker. -function chr_enter_water(x: float, z: float) -> void { - if chr_swim { return } - chr_swim = true - chr_dive = false - chr_depth = 0.0 - chr_breath = body_breath_max() - chr_float = 0.0 - chr_air = false - chr_vy = 0.0 - chr_sit = false - chr_x = x - chr_z = z - chr_water = CharacterGround.water(x, z) - chr_y = chr_water - 0.30 - chr_say(CHAR_STARTED_SWIMMING, 0.0) +function chr_enter_water(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float) -> void { + if character_st.chr_swim { return } + character_st.chr_swim = true + character_st.chr_dive = false + character_st.chr_depth = 0.0 + character_st.chr_breath = body_breath_max() + character_st.chr_float = 0.0 + character_st.chr_air = false + character_st.chr_vy = 0.0 + character_st.chr_sit = false + character_st.chr_x = x + character_st.chr_z = z + character_st.chr_water = CharacterGround.water(x, z) + character_st.chr_y = character_st.chr_water - 0.30 + chr_say(base_st, character_st, CHAR_STARTED_SWIMMING, 0.0) } -function chr_leave_water() -> void { - if not chr_swim { return } - chr_swim = false - chr_dive = false - chr_depth = 0.0 - chr_y = CharacterGround.height(chr_x, chr_z) - chr_speed = Math.min(chr_speed, 1.0) - chr_say(CHAR_LEFT_WATER, 0.0) +function chr_leave_water(base_st: mut BaseState, character_st: mut CharacterState) -> void { + if not character_st.chr_swim { return } + character_st.chr_swim = false + character_st.chr_dive = false + character_st.chr_depth = 0.0 + character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z) + character_st.chr_speed = Math.min(character_st.chr_speed, 1.0) + chr_say(base_st, character_st, CHAR_LEFT_WATER, 0.0) } # the descent: the jump held pushes the body under, letting go floats it back up -function chr_dive_tick(jump: bool, dt: float) -> void { - let bed = Math.max(chr_bed_depth(chr_x, chr_z), 0.0) +function chr_dive_tick(character_st: mut CharacterState, jump: bool, dt: float) -> void { + let bed = Math.max(chr_bed_depth(character_st.chr_x, character_st.chr_z), 0.0) let max_d = Math.min(6.0, Math.max(bed - 0.4, 0.0)) if jump and max_d > 0.2 { - chr_dive = true - chr_depth = Math.min(chr_depth + 1.2 * dt, max_d) + character_st.chr_dive = true + character_st.chr_depth = Math.min(character_st.chr_depth + 1.2 * dt, max_d) } else { - chr_depth = Math.max(chr_depth - 1.6 * dt, 0.0) - if chr_depth < 0.05 { chr_dive = false } + character_st.chr_depth = Math.max(character_st.chr_depth - 1.6 * dt, 0.0) + if character_st.chr_depth < 0.05 { character_st.chr_dive = false } } } # air is spent only under the surface, and comes back fast at the top -function chr_breath_tick(dt: float) -> void { - if not (chr_depth > 0.55) { - chr_breath = Math.min(chr_breath + dt * 12.0, body_breath_max()) +function chr_breath_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void { + if not (character_st.chr_depth > 0.55) { + character_st.chr_breath = Math.min(character_st.chr_breath + dt * 12.0, body_breath_max()) return } - chr_breath = chr_breath - dt - if chr_breath < 0.0 { - chr_breath = 0.0 - chr_dive = false - chr_depth = Math.max(chr_depth - 3.0 * dt, 0.0) - chr_say(CHAR_OUT_OF_AIR, dt) + character_st.chr_breath = character_st.chr_breath - dt + if character_st.chr_breath < 0.0 { + character_st.chr_breath = 0.0 + character_st.chr_dive = false + character_st.chr_depth = Math.max(character_st.chr_depth - 3.0 * dt, 0.0) + chr_say(base_st, character_st, CHAR_OUT_OF_AIR, dt) } } # The edge of the world is a current, not a wall: it takes the outward part of a stroke, so # turning round always works. Said once every twelve seconds while it is strong. -function chr_current_tick(dt: float) -> void { - let fx = -Math.sin(chr_yaw) - let fz = -Math.cos(chr_yaw) - let take = Math.clamp(CharacterGround.current(chr_x, chr_z, fx, fz), 0.0, 1.0) +function chr_current_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void { + let fx = -Math.sin(character_st.chr_yaw) + let fz = -Math.cos(character_st.chr_yaw) + let take = Math.clamp(CharacterGround.current(character_st.chr_x, character_st.chr_z, fx, fz), 0.0, 1.0) if take > 0.0 { - chr_speed = chr_speed * (1.0 - take) - if take > 0.55 and chr_edge_said < 0.0 { - chr_edge_said = 12.0 - chr_say(CHAR_CURRENT, take) + character_st.chr_speed = character_st.chr_speed * (1.0 - take) + if take > 0.55 and character_st.chr_edge_said < 0.0 { + character_st.chr_edge_said = 12.0 + chr_say(base_st, character_st, CHAR_CURRENT, take) } } - chr_edge_said = chr_edge_said - dt + character_st.chr_edge_said = character_st.chr_edge_said - dt } diff --git a/packages/ludic.clock/calendar.ludic b/packages/ludic.clock/calendar.ludic index 83aee2b4..2ce2acea 100644 --- a/packages/ludic.clock/calendar.ludic +++ b/packages/ludic.clock/calendar.ludic @@ -1,48 +1,45 @@ # calendar.ludic - where a day falls: its month, its year, and its season, a pure function of the # day. The game says how long a month is, how many make a year, and which season each month is. -var cal_month_days: int = 30 -var cal_year_months: int = 12 -var cal_season: []int = null -export function clock_calendar(month_days: int, season_of_month: []int) -> void { - cal_month_days = month_days - cal_year_months = len(season_of_month) - cal_season = season_of_month +export function clock_calendar(clock_st: mut ClockState, month_days: int, season_of_month: []int) -> void { + clock_st.cal_month_days = month_days + clock_st.cal_year_months = len(season_of_month) + clock_st.cal_season = season_of_month } -export function clock_month_days() -> int { return cal_month_days } -export function clock_year_months() -> int { return cal_year_months } +export function clock_month_days(clock_st: ClockState) -> int { return clock_st.cal_month_days } +export function clock_year_months(clock_st: ClockState) -> int { return clock_st.cal_year_months } # the month a day falls in, 0 .. months - 1 -export function clock_month_of(day: int) -> int { return ((day - 1) / cal_month_days) % cal_year_months } -export function clock_year_of(day: int) -> int { return (day - 1) / (cal_month_days * cal_year_months) + 1 } -export function clock_day_of_month_of(day: int) -> int { return (day - 1) % cal_month_days + 1 } +export function clock_month_of(clock_st: ClockState, day: int) -> int { return ((day - 1) / clock_st.cal_month_days) % clock_st.cal_year_months } +export function clock_year_of(clock_st: ClockState, day: int) -> int { return (day - 1) / (clock_st.cal_month_days * clock_st.cal_year_months) + 1 } +export function clock_day_of_month_of(clock_st: ClockState, day: int) -> int { return (day - 1) % clock_st.cal_month_days + 1 } -export function clock_season_of(day: int) -> int { - if cal_season == null { return 0 } - return cal_season[clock_month_of(day)] +export function clock_season_of(clock_st: ClockState, day: int) -> int { + if clock_st.cal_season == null { return 0 } + return clock_st.cal_season[clock_month_of(clock_st, day)] } -export function clock_month() -> int { return clock_month_of(ck_day) } -export function clock_year() -> int { return clock_year_of(ck_day) } -export function clock_day_of_month() -> int { return clock_day_of_month_of(ck_day) } -export function clock_season() -> int { return clock_season_of(ck_day) } +export function clock_month(clock_st: ClockState) -> int { return clock_month_of(clock_st, clock_st.ck_day) } +export function clock_year(clock_st: ClockState) -> int { return clock_year_of(clock_st, clock_st.ck_day) } +export function clock_day_of_month(clock_st: ClockState) -> int { return clock_day_of_month_of(clock_st, clock_st.ck_day) } +export function clock_season(clock_st: ClockState) -> int { return clock_season_of(clock_st, clock_st.ck_day) } # days from `day` until the season is another; 0 when a year has only one -export function clock_days_to_change_from(day: int) -> int { - let here = clock_season_of(day) - for d in 1 .. cal_month_days * cal_year_months { - if clock_season_of(day + d) != here { return d } +export function clock_days_to_change_from(clock_st: ClockState, day: int) -> int { + let here = clock_season_of(clock_st, day) + for d in 1 .. clock_st.cal_month_days * clock_st.cal_year_months { + if clock_season_of(clock_st, day + d) != here { return d } } return 0 } -export function clock_days_to_change() -> int { return clock_days_to_change_from(ck_day) } +export function clock_days_to_change(clock_st: ClockState) -> int { return clock_days_to_change_from(clock_st, clock_st.ck_day) } # the first day (counted from day one) that falls in a season; -1 when none does -export function clock_first_day_of(season: int) -> int { - for d in 1 .. cal_month_days * cal_year_months + 1 { - if clock_season_of(d) == season { return d } +export function clock_first_day_of(clock_st: ClockState, season: int) -> int { + for d in 1 .. clock_st.cal_month_days * clock_st.cal_year_months + 1 { + if clock_season_of(clock_st, d) == season { return d } } return -1 } diff --git a/packages/ludic.clock/queries.ludic b/packages/ludic.clock/queries.ludic index fa8b732d..de4c0b68 100644 --- a/packages/ludic.clock/queries.ludic +++ b/packages/ludic.clock/queries.ludic @@ -1,21 +1,21 @@ # queries.ludic - what the world asks the clock -export function clock_day() -> int { return ck_day } -export function clock_hours() -> float { return ck_hours } -export function clock_moon() -> float { return ck_moon } -export function clock_scale() -> float { return ck_scale } +export function clock_day(clock_st: ClockState) -> int { return clock_st.ck_day } +export function clock_hours(clock_st: ClockState) -> float { return clock_st.ck_hours } +export function clock_moon(clock_st: ClockState) -> float { return clock_st.ck_moon } +export function clock_scale(clock_st: ClockState) -> float { return clock_st.ck_scale } # game seconds per real second right now, the rest speed-up included -export function clock_rate() -> float { return ck_scale * ck_rest() } +export function clock_rate(clock_st: ClockState) -> float { return clock_st.ck_scale * ck_rest() } # the game hours `dt` real seconds are worth at the current rate -export function clock_hours_in(dt: float) -> float { return dt * clock_rate() / 3600.0 } +export function clock_hours_in(clock_st: ClockState, dt: float) -> float { return dt * clock_rate(clock_st) / 3600.0 } -export function clock_is_night() -> bool { return ck_hours > ck_night_from or ck_hours < ck_night_to } +export function clock_is_night(clock_st: ClockState) -> bool { return clock_st.ck_hours > clock_st.ck_night_from or clock_st.ck_hours < clock_st.ck_night_to } # "07:05" -export function clock_hhmm() -> string { - let h = int(Math.floor(ck_hours)) - let m = int(Math.floor((ck_hours - float(h)) * 60.0)) +export function clock_hhmm(clock_st: ClockState) -> string { + let h = int(Math.floor(clock_st.ck_hours)) + let m = int(Math.floor((clock_st.ck_hours - float(h)) * 60.0)) var hs = `{h}` var ms = `{m}` if h < 10 { hs = `0{h}` } diff --git a/packages/ludic.clock/state.ludic b/packages/ludic.clock/state.ludic index a91ec525..23f66928 100644 --- a/packages/ludic.clock/state.ludic +++ b/packages/ludic.clock/state.ludic @@ -20,23 +20,28 @@ export property DayTurned { how: int = 0 } -var ck_day: int = 1 -var ck_hours: float = 0.0 -var ck_moon: float = 0.0 -var ck_scale: float = 60.0 # game seconds per real second -var ck_start_hours: float = 8.0 -var ck_night_from: float = 20.0 -var ck_night_to: float = 6.0 -var ck_moon_days: float = 29.5 -var ck_moon_lo: float = 0.0 -var ck_moon_hi: float = 1.0 -var ck_dice: Rng = null -var ck_days: Queue = null +export state ClockState { + cal_month_days: int = 30 + cal_year_months: int = 12 + cal_season: []int = null + ck_day: int = 1 + ck_hours: float = 0.0 + ck_moon: float = 0.0 + ck_scale: float = 60.0 # game seconds per real second + ck_start_hours: float = 8.0 + ck_night_from: float = 20.0 + ck_night_to: float = 6.0 + ck_moon_days: float = 29.5 + ck_moon_lo: float = 0.0 + ck_moon_hi: float = 1.0 + ck_dice: Rng = null + ck_days: Queue = null +} # the facts: each day that turned, in order; the game drains it and runs its morning -export function clock_days() -> Queue { - if ck_days == null { ck_days = queue_new("clock.days") } - return ck_days +export function clock_days(base_st: mut BaseState, clock_st: mut ClockState) -> Queue { + if clock_st.ck_days == null { clock_st.ck_days = queue_new(base_st, "clock.days") } + return clock_st.ck_days } function ck_owns() -> bool { return ClockWorld.owns_world() } @@ -44,15 +49,15 @@ function ck_owns() -> bool { return ClockWorld.owns_world() } function ck_rest() -> float { return ClockWorld.rest_scale() } # when a trip starts, when the night is, and how long the moon's month is -export function clock_config(start_hours: float, night_from: float, night_to: float) -> void { - ck_start_hours = start_hours - ck_night_from = night_from - ck_night_to = night_to +export function clock_config(clock_st: mut ClockState, start_hours: float, night_from: float, night_to: float) -> void { + clock_st.ck_start_hours = start_hours + clock_st.ck_night_from = night_from + clock_st.ck_night_to = night_to } # a new trip's moon falls in [lo, hi) of its month; the month is `days` long -export function clock_config_moon(days: float, lo: float, hi: float) -> void { - ck_moon_days = days - ck_moon_lo = lo - ck_moon_hi = hi +export function clock_config_moon(clock_st: mut ClockState, days: float, lo: float, hi: float) -> void { + clock_st.ck_moon_days = days + clock_st.ck_moon_lo = lo + clock_st.ck_moon_hi = hi } diff --git a/packages/ludic.clock/system.ludic b/packages/ludic.clock/system.ludic index 4ee5124d..d62e2b00 100644 --- a/packages/ludic.clock/system.ludic +++ b/packages/ludic.clock/system.ludic @@ -1,20 +1,20 @@ # system.ludic - the clock as a system: it runs first (PH_INPUT) so every later system reads this # frame's hour, and it saves its own section -function clock_tick(t: Tick) -> void { clock_advance(t.hours) } +function clock_tick(base_st: mut BaseState, clock_st: mut ClockState, t: Tick) -> void { clock_advance(base_st, clock_st, t.hours) } -export function clock_save() -> Val { +export function clock_save(clock_st: ClockState) -> Val { let v = Value.object() - sv_put_int(v, "day", ck_day) - sv_put_float(v, "hours", ck_hours) - sv_put_float(v, "moon", ck_moon) + sv_put_int(v, "day", clock_st.ck_day) + sv_put_float(v, "hours", clock_st.ck_hours) + sv_put_float(v, "moon", clock_st.ck_moon) return v } -export function clock_load(v: Val, version: int) -> void { - ck_day = sv_int(v, "day", 1) - clock_set_hours(sv_float(v, "hours", ck_start_hours)) - ck_moon = sv_float(v, "moon", 0.5) - q_clear(clock_days()) +export function clock_load(base_st: mut BaseState, clock_st: mut ClockState, v: Val, version: int) -> void { + clock_st.ck_day = sv_int(v, "day", 1) + clock_set_hours(clock_st, sv_float(v, "hours", clock_st.ck_start_hours)) + clock_st.ck_moon = sv_float(v, "moon", 0.5) + q_clear(base_st, clock_days(base_st, clock_st)) } export function clock_system() -> System { diff --git a/packages/ludic.clock/tests/clock_test.ludic b/packages/ludic.clock/tests/clock_test.ludic index bbd81906..1a88bb76 100644 --- a/packages/ludic.clock/tests/clock_test.ludic +++ b/packages/ludic.clock/tests/clock_test.ludic @@ -4,123 +4,125 @@ import "ludic.clock" import "ludic.base" program ClockTest { numbers float - var owns: bool = true - var rest: float = 1.0 + state ClocktestState { + owns: bool = true + rest: float = 1.0 + } - function fake_owns() -> bool { return owns } - function fake_rest() -> float { return rest } + function fake_owns(clocktest_st: ClocktestState) -> bool { return clocktest_st.owns } + function fake_rest(clocktest_st: ClocktestState) -> float { return clocktest_st.rest } function fake_seed() -> int { return 42 } bind ClockWorld { owns_world: fn fake_owns, rest_scale: fn fake_rest, seed: fn fake_seed } - function fresh() -> void { - owns = true - rest = 1.0 - clock_config(8.0, 20.0, 6.0) - clock_config_moon(29.5, 0.2, 0.8) - clock_set_scale(60.0) + function fresh(base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) -> void { + clocktest_st.owns = true + clocktest_st.rest = 1.0 + clock_config(clock_st, 8.0, 20.0, 6.0) + clock_config_moon(clock_st, 29.5, 0.2, 0.8) + clock_set_scale(clock_st, 60.0) let seasons = new []int for m in 0 .. 12 { push(seasons, m / 3) } - clock_calendar(10, seasons) - clock_reset() + clock_calendar(clock_st, 10, seasons) + clock_reset(base_st, clock_st) } - function turned() -> []DayTurned { return q_drain(clock_days()) } - function waiting() -> int { return q_len(clock_days()) } + function turned(base_st: mut BaseState, clock_st: mut ClockState) -> []DayTurned { return q_drain(base_st, clock_days(base_st, clock_st)) } + function waiting(base_st: mut BaseState, clock_st: mut ClockState) -> int { return q_len(clock_days(base_st, clock_st)) } function run_hours(h: float) -> void { let s = clock_system() s.tick(tick_new(0.016, 0, h)) } - test "a trip starts on day one at the start hour, the moon inside its range" { - fresh() - expect_eq(clock_day(), 1) - expect(clock_hours() == 8.0) - expect(clock_moon() >= 0.2 and clock_moon() < 0.8) - expect(clock_hhmm() == "08:00") + test "a trip starts on day one at the start hour, the moon inside its range" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + expect_eq(clock_day(clock_st), 1) + expect(clock_hours(clock_st) == 8.0) + expect(clock_moon(clock_st) >= 0.2 and clock_moon(clock_st) < 0.8) + expect(clock_hhmm(clock_st) == "08:00") } - test "the same seed gives the same moon" { - fresh() - let a = clock_moon() - clock_reset() - expect(clock_moon() == a) + test "the same seed gives the same moon" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + let a = clock_moon(clock_st) + clock_reset(base_st, clock_st) + expect(clock_moon(clock_st) == a) } - test "a real minute is a game hour at scale 60, and resting speeds it" { - fresh() - expect(clock_hours_in(60.0) == 1.0) - rest = 4.0 - expect(clock_rate() == 240.0) + test "a real minute is a game hour at scale 60, and resting speeds it" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + expect(clock_hours_in(clock_st, 60.0) == 1.0) + clocktest_st.rest = 4.0 + expect(clock_rate(clock_st) == 240.0) } - test "past midnight the day turns, once, as a fact" { - fresh() + test "past midnight the day turns, once, as a fact" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) run_hours(15.0) - expect_eq(waiting(), 0) + expect_eq(waiting(base_st, clock_st), 0) run_hours(2.5) - expect_eq(clock_day(), 2) - expect(clock_hours() == 1.5) - let t = turned() + expect_eq(clock_day(clock_st), 2) + expect(clock_hours(clock_st) == 1.5) + let t = turned(base_st, clock_st) expect_eq(len(t), 1) expect_eq(t[0].day, 2) expect_eq(t[0].how, DAY_STAYED_UP) } - test "a machine that does not own the world does not move it" { - fresh() - owns = false + test "a machine that does not own the world does not move it" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + clocktest_st.owns = false run_hours(20.0) - expect(clock_hours() == 8.0) - expect_eq(clock_day(), 1) + expect(clock_hours(clock_st) == 8.0) + expect_eq(clock_day(clock_st), 1) } - test "a night slept turns the day and moves the moon a month's share" { - fresh() - let m0 = clock_moon() - clock_set_hours(6.5) - clock_new_day() - expect_eq(clock_day(), 2) - expect(clock_moon() > m0) - expect_eq(turned()[0].how, DAY_SLEPT) + test "a night slept turns the day and moves the moon a month's share" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + let m0 = clock_moon(clock_st) + clock_set_hours(clock_st, 6.5) + clock_new_day(base_st, clock_st) + expect_eq(clock_day(clock_st), 2) + expect(clock_moon(clock_st) > m0) + expect_eq(turned(base_st, clock_st)[0].how, DAY_SLEPT) } - test "night is outside the day's hours, and the hour wraps" { - fresh() - clock_set_hours(21.0) - expect(clock_is_night()) - clock_set_hours(12.0) - expect(not clock_is_night()) - clock_set_hours(25.0) - expect(clock_hours() == 1.0) + test "night is outside the day's hours, and the hour wraps" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + clock_set_hours(clock_st, 21.0) + expect(clock_is_night(clock_st)) + clock_set_hours(clock_st, 12.0) + expect(not clock_is_night(clock_st)) + clock_set_hours(clock_st, 25.0) + expect(clock_hours(clock_st) == 1.0) } - test "the calendar is a function of the day" { - fresh() - expect_eq(clock_season_of(1), 0) - expect_eq(clock_month_of(31), 3) - expect_eq(clock_season_of(31), 1) - expect_eq(clock_day_of_month_of(35), 5) - expect_eq(clock_year_of(121), 2) - expect_eq(clock_first_day_of(2), 61) - clock_set_day(28) - expect_eq(clock_days_to_change(), 3) - expect_eq(clock_season(), 0) + test "the calendar is a function of the day" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + expect_eq(clock_season_of(clock_st, 1), 0) + expect_eq(clock_month_of(clock_st, 31), 3) + expect_eq(clock_season_of(clock_st, 31), 1) + expect_eq(clock_day_of_month_of(clock_st, 35), 5) + expect_eq(clock_year_of(clock_st, 121), 2) + expect_eq(clock_first_day_of(clock_st, 2), 61) + clock_set_day(clock_st, 28) + expect_eq(clock_days_to_change(clock_st), 3) + expect_eq(clock_season(clock_st), 0) } - test "a save section reads back the same clock" { - fresh() - clock_set_day(17) - clock_set_hours(13.25) - clock_set_moon(0.375) + test "a save section reads back the same clock" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) { + fresh(base_st, clock_st, clocktest_st) + clock_set_day(clock_st, 17) + clock_set_hours(clock_st, 13.25) + clock_set_moon(clock_st, 0.375) let root = save_tree() - save_section(root, "clock", 1, clock_save()) + save_section(root, "clock", 1, clock_save(clock_st)) let text = save_encode(root) - clock_reset() + clock_reset(base_st, clock_st) let n = load_section(save_decode(text), "clock") - clock_load(n.data, n.version) - expect_eq(clock_day(), 17) - expect(clock_hours() == 13.25) - expect(clock_moon() == 0.375) + clock_load(base_st, clock_st, n.data, n.version) + expect_eq(clock_day(clock_st), 17) + expect(clock_hours(clock_st) == 13.25) + expect(clock_moon(clock_st) == 0.375) } } diff --git a/packages/ludic.clock/verbs.ludic b/packages/ludic.clock/verbs.ludic index b2a8b2dd..68f5baee 100644 --- a/packages/ludic.clock/verbs.ludic +++ b/packages/ludic.clock/verbs.ludic @@ -1,43 +1,43 @@ # verbs.ludic - the only way the clock changes -export function clock_set_scale(s: float) -> void { ck_scale = s } -export function clock_set_day(d: int) -> void { ck_day = d } -export function clock_set_moon(m: float) -> void { ck_moon = m } +export function clock_set_scale(clock_st: mut ClockState, s: float) -> void { clock_st.ck_scale = s } +export function clock_set_day(clock_st: mut ClockState, d: int) -> void { clock_st.ck_day = d } +export function clock_set_moon(clock_st: mut ClockState, m: float) -> void { clock_st.ck_moon = m } # the hour, wrapped into [0, 24); setting it never turns the day - that is clock_new_day's -export function clock_set_hours(h: float) -> void { +export function clock_set_hours(clock_st: mut ClockState, h: float) -> void { var x = h % 24.0 if x < 0.0 { x = x + 24.0 } - ck_hours = x + clock_st.ck_hours = x } -function ck_turn(how: int) -> void { - ck_day += 1 - ck_moon = (ck_moon + 1.0 / ck_moon_days) % 1.0 +function ck_turn(base_st: mut BaseState, clock_st: mut ClockState, how: int) -> void { + clock_st.ck_day += 1 + clock_st.ck_moon = (clock_st.ck_moon + 1.0 / clock_st.ck_moon_days) % 1.0 let d = new DayTurned - d.day = ck_day + d.day = clock_st.ck_day d.how = how - q_push(clock_days(), d) + q_push(base_st, clock_days(base_st, clock_st), d) } # a new day however it came (a night slept, a collapse); the caller sets the hour first -export function clock_new_day() -> void { ck_turn(DAY_SLEPT) } +export function clock_new_day(base_st: mut BaseState, clock_st: mut ClockState) -> void { ck_turn(base_st, clock_st, DAY_SLEPT) } # run the hours on by `gh` game hours; past midnight the day turns. Only the machine that owns # the world moves it - another machine is told the time. -export function clock_advance(gh: float) -> void { +export function clock_advance(base_st: mut BaseState, clock_st: mut ClockState, gh: float) -> void { if not ck_owns() { return } - ck_hours = ck_hours + gh - while not (ck_hours < 24.0) { - ck_hours = ck_hours - 24.0 - ck_turn(DAY_STAYED_UP) + clock_st.ck_hours = clock_st.ck_hours + gh + while not (clock_st.ck_hours < 24.0) { + clock_st.ck_hours = clock_st.ck_hours - 24.0 + ck_turn(base_st, clock_st, DAY_STAYED_UP) } } # a new trip: day one at the start hour, the moon somewhere in its month -export function clock_reset() -> void { - ck_dice = rng_new(ClockWorld.seed()) - ck_day = 1 - ck_hours = ck_start_hours - ck_moon = rng_span(ck_dice, ck_moon_lo, ck_moon_hi) % 1.0 - q_clear(clock_days()) +export function clock_reset(base_st: mut BaseState, clock_st: mut ClockState) -> void { + clock_st.ck_dice = rng_new(ClockWorld.seed()) + clock_st.ck_day = 1 + clock_st.ck_hours = clock_st.ck_start_hours + clock_st.ck_moon = rng_span(clock_st.ck_dice, clock_st.ck_moon_lo, clock_st.ck_moon_hi) % 1.0 + q_clear(base_st, clock_days(base_st, clock_st)) } diff --git a/packages/ludic.compass/bearing.ludic b/packages/ludic.compass/bearing.ludic index 9fd60caa..2b6a8f3f 100644 --- a/packages/ludic.compass/bearing.ludic +++ b/packages/ludic.compass/bearing.ludic @@ -1,15 +1,15 @@ # bearing.ludic - the frame's marks as the strip reads them: how many, each one, where it bears # from the viewer's facing and how far it is. A heading is the world's (0 toward -z, a quarter # turn toward -x), a bearing is the heading less the viewer's yaw, in -pi .. pi. -export function compass_count() -> int { - if cmp_list == null { return 0 } - return len(cmp_list) +export function compass_count(compass_st: CompassState) -> int { + if compass_st.cmp_list == null { return 0 } + return len(compass_st.cmp_list) } # the i-th mark, or an empty one past the end -export function compass_at(i: int) -> CompassMark { - if i < 0 or i >= compass_count() { return new CompassMark } - return cmp_list[i] +export function compass_at(compass_st: CompassState, i: int) -> CompassMark { + if i < 0 or i >= compass_count(compass_st) { return new CompassMark } + return compass_st.cmp_list[i] } export function compass_wrap(a: float) -> float { @@ -33,13 +33,13 @@ export function compass_dist_to(x: float, z: float) -> float { return Math.sqrt(dx * dx + dz * dz) } -export function compass_bearing(i: int) -> float { - let m = compass_at(i) +export function compass_bearing(compass_st: CompassState, i: int) -> float { + let m = compass_at(compass_st, i) return compass_bearing_to(m.x, m.z) } -export function compass_dist(i: int) -> float { - let m = compass_at(i) +export function compass_dist(compass_st: CompassState, i: int) -> float { + let m = compass_at(compass_st, i) return compass_dist_to(m.x, m.z) } @@ -47,8 +47,8 @@ export function compass_dist(i: int) -> float { export function compass_strip_t(bearing: float, half: float) -> float { return bearing / Math.max(half, 0.001) } # the objective: the first tracked mark of the frame, or -1 -export function compass_tracked() -> int { - for i in 0 .. compass_count() { if cmp_list[i].prio >= COMPASS_TRACKED { return i } } +export function compass_tracked(compass_st: CompassState) -> int { + for i in 0 .. compass_count(compass_st) { if compass_st.cmp_list[i].prio >= COMPASS_TRACKED { return i } } return -1 } diff --git a/packages/ludic.compass/gather.ludic b/packages/ludic.compass/gather.ludic index 041870ae..bec0f109 100644 --- a/packages/ludic.compass/gather.ludic +++ b/packages/ludic.compass/gather.ludic @@ -1,56 +1,56 @@ # gather.ludic - a frame's marks: begin, every provider marks, end sorts and culls. While a # capture is open the marks go to whoever is listening instead, whatever the tier. -export function compass_begin() -> void { cmp_list = new []CompassMark } +export function compass_begin(compass_st: mut CompassState) -> void { compass_st.cmp_list = new []CompassMark } # a provider's verb: a thing at (x, z), shown from compass `tier` up -export function compass_mark(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> void { - if cmp_list == null { compass_begin() } +export function compass_mark(compass_st: mut CompassState, x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> void { + if compass_st.cmp_list == null { compass_begin(compass_st) } let m = cmp_new(x, z, icon, colour, tier, prio, label) - if cmp_capturing { - push(cmp_caught, m) + if compass_st.cmp_capturing { + push(compass_st.cmp_caught, m) return } if CompassWorld.tier() < tier { return } - push(cmp_list, m) + push(compass_st.cmp_list, m) } # highest priority first, the providers' order kept within one; then the cap -export function compass_end() -> void { - if cmp_list == null { compass_begin() } +export function compass_end(compass_st: mut CompassState) -> void { + if compass_st.cmp_list == null { compass_begin(compass_st) } let out = new []CompassMark for p in 0 .. 3 { let want = COMPASS_TRACKED - p - for i in 0 .. len(cmp_list) { if cmp_band(cmp_list[i].prio) == want and len(out) < cmp_cap { push(out, cmp_list[i]) } } + for i in 0 .. len(compass_st.cmp_list) { if cmp_band(compass_st.cmp_list[i].prio) == want and len(out) < compass_st.cmp_cap { push(out, compass_st.cmp_list[i]) } } } - cmp_list = out + compass_st.cmp_list = out } function cmp_band(p: int) -> int { return Math.clamp(p, COMPASS_PLAIN, COMPASS_TRACKED) } # once a frame: ask every registered provider, in the registry's order -export function compass_gather() -> void { - compass_begin() +export function compass_gather(compass_st: mut CompassState) -> void { + compass_begin(compass_st) if CompassWorld.tier() <= 0 { return } for i in 0 .. COMPASS_FROM_COUNT { let f = CompassProviders[i].gather if f != null { f() } } - compass_end() + compass_end(compass_st) } # someone else wants to know where the providers point (a guide's arrow): until the end every # mark is theirs, whatever the tier, and the frame's list is left alone -export function compass_capture_begin() -> void { - cmp_capturing = true - cmp_caught = new []CompassMark +export function compass_capture_begin(compass_st: mut CompassState) -> void { + compass_st.cmp_capturing = true + compass_st.cmp_caught = new []CompassMark } -export function compass_capture_end() -> []CompassMark { - cmp_capturing = false - var got = cmp_caught - cmp_caught = new []CompassMark +export function compass_capture_end(compass_st: mut CompassState) -> []CompassMark { + compass_st.cmp_capturing = false + var got = compass_st.cmp_caught + compass_st.cmp_caught = new []CompassMark if got == null { got = new []CompassMark } return got } -export function compass_capturing() -> bool { return cmp_capturing } +export function compass_capturing(compass_st: CompassState) -> bool { return compass_st.cmp_capturing } diff --git a/packages/ludic.compass/marks.ludic b/packages/ludic.compass/marks.ludic index 855b2922..ec2a546d 100644 --- a/packages/ludic.compass/marks.ludic +++ b/packages/ludic.compass/marks.ludic @@ -22,15 +22,19 @@ export property CompassProvider { export open registry CompassProviders of CompassProvider as COMPASS_FROM -var cmp_cap: int = 64 -var cmp_list: []CompassMark = null # this frame's, sorted and culled -var cmp_capturing: bool = false -var cmp_caught: []CompassMark = null # what providers said while someone was listening +export state CompassState { + cmp_cap: int = 64 + cmp_list: []CompassMark = null # this frame's, sorted and culled + cmp_capturing: bool = false + cmp_caught: []CompassMark = null # what providers said while someone was listening + cmp_ranges: []float = null + cmp_radar_from: int = 2 # marks of at least this tier are blips; the tracked one always is +} # how many marks a frame keeps (the lowest priorities go first past it) -export function compass_config(cap: int) -> void { - cmp_cap = Math.max(cap, 1) - compass_begin() +export function compass_config(compass_st: mut CompassState, cap: int) -> void { + compass_st.cmp_cap = Math.max(cap, 1) + compass_begin(compass_st) } function cmp_new(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> CompassMark { diff --git a/packages/ludic.compass/radar.ludic b/packages/ludic.compass/radar.ludic index f830a8b9..b4a5145c 100644 --- a/packages/ludic.compass/radar.ludic +++ b/packages/ludic.compass/radar.ludic @@ -1,40 +1,38 @@ # radar.ludic - the radar's tiers: a range for each compass tier (0 is no radar), which marks it # shows, and where each falls on a unit disc with ahead at the top -var cmp_ranges: []float = null -var cmp_radar_from: int = 2 # marks of at least this tier are blips; the tracked one always is -export function compass_config_radar(ranges: []float, from_tier: int) -> void { - cmp_ranges = new []float - for i in 0 .. len(ranges) { push(cmp_ranges, ranges[i]) } - cmp_radar_from = from_tier +export function compass_config_radar(compass_st: mut CompassState, ranges: []float, from_tier: int) -> void { + compass_st.cmp_ranges = new []float + for i in 0 .. len(ranges) { push(compass_st.cmp_ranges, ranges[i]) } + compass_st.cmp_radar_from = from_tier } # how far the radar reaches with the viewer's compass, 0 without one; a tier past the table has # the last range -export function compass_radar_range() -> float { - if cmp_ranges == null or len(cmp_ranges) == 0 { return 0.0 } +export function compass_radar_range(compass_st: CompassState) -> float { + if compass_st.cmp_ranges == null or len(compass_st.cmp_ranges) == 0 { return 0.0 } let t = CompassWorld.tier() if t < 0 { return 0.0 } - if t >= len(cmp_ranges) { return cmp_ranges[len(cmp_ranges) - 1] } - return cmp_ranges[t] + if t >= len(compass_st.cmp_ranges) { return compass_st.cmp_ranges[len(compass_st.cmp_ranges) - 1] } + return compass_st.cmp_ranges[t] } -export function compass_radar_on() -> bool { return compass_radar_range() > 0.0 } +export function compass_radar_on(compass_st: CompassState) -> bool { return compass_radar_range(compass_st) > 0.0 } # the i-th mark is a blip: a radar is carried, it is shown there and it is in range -export function compass_radar_has(i: int) -> bool { - if i < 0 or i >= compass_count() or not compass_radar_on() { return false } - let m = cmp_list[i] - if m.tier < cmp_radar_from and m.prio < COMPASS_TRACKED { return false } - return compass_dist(i) <= compass_radar_range() +export function compass_radar_has(compass_st: CompassState, i: int) -> bool { + if i < 0 or i >= compass_count(compass_st) or not compass_radar_on(compass_st) { return false } + let m = compass_st.cmp_list[i] + if m.tier < compass_st.cmp_radar_from and m.prio < COMPASS_TRACKED { return false } + return compass_dist(compass_st, i) <= compass_radar_range(compass_st) } # where on a unit disc: x right, y down, so ahead is (0, -1) and the edge is the range -export function compass_radar_x(i: int) -> float { return -Math.sin(compass_bearing(i)) * cmp_radar_frac(i) } -export function compass_radar_y(i: int) -> float { return -Math.cos(compass_bearing(i)) * cmp_radar_frac(i) } +export function compass_radar_x(compass_st: CompassState, i: int) -> float { return -Math.sin(compass_bearing(compass_st, i)) * cmp_radar_frac(compass_st, i) } +export function compass_radar_y(compass_st: CompassState, i: int) -> float { return -Math.cos(compass_bearing(compass_st, i)) * cmp_radar_frac(compass_st, i) } -function cmp_radar_frac(i: int) -> float { - let r = compass_radar_range() +function cmp_radar_frac(compass_st: CompassState, i: int) -> float { + let r = compass_radar_range(compass_st) if r <= 0.0 { return 0.0 } - return Math.min(compass_dist(i) / r, 1.0) + return Math.min(compass_dist(compass_st, i) / r, 1.0) } diff --git a/packages/ludic.compass/tests/compass_test.ludic b/packages/ludic.compass/tests/compass_test.ludic index fbcda331..53685cfc 100644 --- a/packages/ludic.compass/tests/compass_test.ludic +++ b/packages/ludic.compass/tests/compass_test.ludic @@ -6,120 +6,122 @@ import "ludic.base" program CompassTest { numbers float - var vx: float = 0.0 - var vz: float = 0.0 - var yaw: float = 0.0 - var tier: int = 4 - var objective: bool = true + state CompasstestState { + vx: float = 0.0 + vz: float = 0.0 + yaw: float = 0.0 + tier: int = 4 + objective: bool = true + } - function fk_x() -> float { return vx } - function fk_z() -> float { return vz } - function fk_yaw() -> float { return yaw } - function fk_tier() -> int { return tier } + function fk_x(compasstest_st: CompasstestState) -> float { return compasstest_st.vx } + function fk_z(compasstest_st: CompasstestState) -> float { return compasstest_st.vz } + function fk_yaw(compasstest_st: CompasstestState) -> float { return compasstest_st.yaw } + function fk_tier(compasstest_st: CompasstestState) -> int { return compasstest_st.tier } bind CompassWorld { x: fn fk_x, z: fn fk_z, yaw: fn fk_yaw, tier: fn fk_tier } - function places() -> void { - compass_mark(0.0, -100.0, 1, 10, 1, COMPASS_PLAIN, "camp") - compass_mark(-50.0, 0.0, 2, 11, 2, COMPASS_PLAIN, "sign") - compass_mark(0.0, 300.0, 3, 12, 4, COMPASS_PLAIN, "legend") - compass_mark(10.0, 10.0, 4, 13, 1, COMPASS_NOTE, "my mark") + function places(compass_st: mut CompassState) -> void { + compass_mark(compass_st, 0.0, -100.0, 1, 10, 1, COMPASS_PLAIN, "camp") + compass_mark(compass_st, -50.0, 0.0, 2, 11, 2, COMPASS_PLAIN, "sign") + compass_mark(compass_st, 0.0, 300.0, 3, 12, 4, COMPASS_PLAIN, "legend") + compass_mark(compass_st, 10.0, 10.0, 4, 13, 1, COMPASS_NOTE, "my mark") } - function goal() -> void { if objective { compass_mark(80.0, 0.0, 5, 14, 2, COMPASS_TRACKED, "the dock") } } + function goal(compass_st: mut CompassState, compasstest_st: CompasstestState) -> void { if compasstest_st.objective { compass_mark(compass_st, 80.0, 0.0, 5, 14, 2, COMPASS_TRACKED, "the dock") } } def CompassProviders places { gather: fn places } def CompassProviders goal { gather: fn goal } - function fresh() -> void { - compass_config(64) - compass_config_radar([0.0, 0.0, 0.0, 120.0, 260.0], 2) - vx = 0.0 - vz = 0.0 - yaw = 0.0 - tier = 4 - objective = true + function fresh(compass_st: mut CompassState, compasstest_st: mut CompasstestState) -> void { + compass_config(compass_st, 64) + compass_config_radar(compass_st, [0.0, 0.0, 0.0, 120.0, 260.0], 2) + compasstest_st.vx = 0.0 + compasstest_st.vz = 0.0 + compasstest_st.yaw = 0.0 + compasstest_st.tier = 4 + compasstest_st.objective = true } - function labels() -> string { + function labels(compass_st: CompassState) -> string { var s = "" - for i in 0 .. compass_count() { s = s + compass_at(i).label + ";" } + for i in 0 .. compass_count(compass_st) { s = s + compass_at(compass_st, i).label + ";" } return s } - test "every provider is asked, the tracked mark first, then the player's, then the rest in order" { - fresh() - compass_gather() - expect_eq(compass_count(), 5) - expect(labels() == "the dock;my mark;camp;sign;legend;") - expect_eq(compass_tracked(), 0) + test "every provider is asked, the tracked mark first, then the player's, then the rest in order" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) { + fresh(compass_st, compasstest_st) + compass_gather(compass_st) + expect_eq(compass_count(compass_st), 5) + expect(labels(compass_st) == "the dock;my mark;camp;sign;legend;") + expect_eq(compass_tracked(compass_st), 0) } - test "a mark shows from its tier up, and without a compass nothing is gathered" { - fresh() - tier = 1 - compass_gather() - expect(labels() == "my mark;camp;") - expect_eq(compass_tracked(), -1) - tier = 0 - compass_gather() - expect_eq(compass_count(), 0) + test "a mark shows from its tier up, and without a compass nothing is gathered" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) { + fresh(compass_st, compasstest_st) + compasstest_st.tier = 1 + compass_gather(compass_st) + expect(labels(compass_st) == "my mark;camp;") + expect_eq(compass_tracked(compass_st), -1) + compasstest_st.tier = 0 + compass_gather(compass_st) + expect_eq(compass_count(compass_st), 0) } - test "past the cap the lowest priorities go" { - fresh() - compass_config(2) - compass_gather() - expect(labels() == "the dock;my mark;") + test "past the cap the lowest priorities go" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) { + fresh(compass_st, compasstest_st) + compass_config(compass_st, 2) + compass_gather(compass_st) + expect(labels(compass_st) == "the dock;my mark;") } - test "a bearing is from the viewer's facing, a distance from where it stands" { - fresh() - compass_gather() - expect_near(compass_bearing(2), 0.0, 0.001) - expect_near(compass_dist(2), 100.0, 0.01) - expect_near(compass_bearing(3), 1.5708, 0.001) - expect_near(compass_bearing(0), -1.5708, 0.001) - yaw = 1.5708 - expect_near(compass_bearing(3), 0.0, 0.001) - expect_near(compass_bearing(2), -1.5708, 0.001) - vx = -50.0 - vz = 100.0 - expect_near(compass_dist(3), 100.0, 0.01) + test "a bearing is from the viewer's facing, a distance from where it stands" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) { + fresh(compass_st, compasstest_st) + compass_gather(compass_st) + expect_near(compass_bearing(compass_st, 2), 0.0, 0.001) + expect_near(compass_dist(compass_st, 2), 100.0, 0.01) + expect_near(compass_bearing(compass_st, 3), 1.5708, 0.001) + expect_near(compass_bearing(compass_st, 0), -1.5708, 0.001) + compasstest_st.yaw = 1.5708 + expect_near(compass_bearing(compass_st, 3), 0.0, 0.001) + expect_near(compass_bearing(compass_st, 2), -1.5708, 0.001) + compasstest_st.vx = -50.0 + compasstest_st.vz = 100.0 + expect_near(compass_dist(compass_st, 3), 100.0, 0.01) expect_near(compass_wrap(7.0), 7.0 - 6.2832, 0.001) expect_near(compass_strip_t(0.5, 1.0), 0.5, 0.001) } - test "a capture hears every mark whatever the tier and leaves the frame's list alone" { - fresh() - compass_gather() - tier = 1 - compass_capture_begin() - expect(compass_capturing()) - goal() - places() - let got = compass_capture_end() - expect(not compass_capturing()) + test "a capture hears every mark whatever the tier and leaves the frame's list alone" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) { + fresh(compass_st, compasstest_st) + compass_gather(compass_st) + compasstest_st.tier = 1 + compass_capture_begin(compass_st) + expect(compass_capturing(compass_st)) + goal(compass_st, compasstest_st) + places(compass_st) + let got = compass_capture_end(compass_st) + expect(not compass_capturing(compass_st)) expect_eq(len(got), 5) expect(got[0].label == "the dock") - expect_eq(compass_count(), 5) + expect_eq(compass_count(compass_st), 5) } - test "the radar: its range by tier, the blips in it, and where each falls on the disc" { - fresh() - tier = 2 - compass_gather() - expect(not compass_radar_on()) - expect(not compass_radar_has(0)) - tier = 3 - compass_gather() - expect(compass_radar_on()) - expect_near(compass_radar_range(), 120.0, 0.01) - expect(compass_radar_has(0)) - expect(not compass_radar_has(1)) - expect(not compass_radar_has(2)) - expect(compass_radar_has(3)) - expect_near(compass_radar_x(3), -50.0 / 120.0, 0.001) - expect_near(compass_radar_y(3), 0.0, 0.001) - expect_near(compass_radar_x(0), 80.0 / 120.0, 0.001) - tier = 9 - expect_near(compass_radar_range(), 260.0, 0.01) + test "the radar: its range by tier, the blips in it, and where each falls on the disc" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) { + fresh(compass_st, compasstest_st) + compasstest_st.tier = 2 + compass_gather(compass_st) + expect(not compass_radar_on(compass_st)) + expect(not compass_radar_has(compass_st, 0)) + compasstest_st.tier = 3 + compass_gather(compass_st) + expect(compass_radar_on(compass_st)) + expect_near(compass_radar_range(compass_st), 120.0, 0.01) + expect(compass_radar_has(compass_st, 0)) + expect(not compass_radar_has(compass_st, 1)) + expect(not compass_radar_has(compass_st, 2)) + expect(compass_radar_has(compass_st, 3)) + expect_near(compass_radar_x(compass_st, 3), -50.0 / 120.0, 0.001) + expect_near(compass_radar_y(compass_st, 3), 0.0, 0.001) + expect_near(compass_radar_x(compass_st, 0), 80.0 / 120.0, 0.001) + compasstest_st.tier = 9 + expect_near(compass_radar_range(compass_st), 260.0, 0.01) } test "the eight points go round from the heading 0 toward -x" { diff --git a/packages/ludic.crafting/hold.ludic b/packages/ludic.crafting/hold.ludic index 2104dac6..5644ea2c 100644 --- a/packages/ludic.crafting/hold.ludic +++ b/packages/ludic.crafting/hold.ludic @@ -1,42 +1,40 @@ # hold.ludic - a recipe made by holding a button for its `hold` seconds: letting go wastes # nothing, and it stops by itself the moment it can no longer be made -var craft_hold_i: int = -1 -var craft_hold_t: float = 0.0 export function craft_holds(i: int) -> bool { return craft_ok(i) and CraftRecipes[i].hold > 0.0 } # the recipe being held, or -1 -export function craft_holding() -> int { return craft_hold_i } +export function craft_holding(crafting_st: CraftingState) -> int { return crafting_st.craft_hold_i } # starts holding one that is made by holding and can be made now -export function craft_hold_start(i: int) -> bool { +export function craft_hold_start(crafting_st: mut CraftingState, i: int) -> bool { if not craft_holds(i) or not craft_can(i) { return false } - craft_hold_i = i - craft_hold_t = 0.0 + crafting_st.craft_hold_i = i + crafting_st.craft_hold_t = 0.0 return true } -export function craft_hold_stop() -> void { - craft_hold_i = -1 - craft_hold_t = 0.0 +export function craft_hold_stop(crafting_st: mut CraftingState) -> void { + crafting_st.craft_hold_i = -1 + crafting_st.craft_hold_t = 0.0 } # how far through the hold on `i` is, 0 to 1 -export function craft_hold_frac(i: int) -> float { - if craft_hold_i != i or not craft_holds(i) { return 0.0 } - return Math.clamp(craft_hold_t / CraftRecipes[i].hold, 0.0, 1.0) +export function craft_hold_frac(crafting_st: CraftingState, i: int) -> float { + if crafting_st.craft_hold_i != i or not craft_holds(i) { return 0.0 } + return Math.clamp(crafting_st.craft_hold_t / CraftRecipes[i].hold, 0.0, 1.0) } # `dt` real seconds of holding; true on the frame it is made -export function craft_hold_run(dt: float) -> bool { - if craft_hold_i < 0 { return false } - if not craft_can(craft_hold_i) { - craft_hold_stop() +export function craft_hold_run(base_st: mut BaseState, crafting_st: mut CraftingState, dt: float) -> bool { + if crafting_st.craft_hold_i < 0 { return false } + if not craft_can(crafting_st.craft_hold_i) { + craft_hold_stop(crafting_st) return false } - craft_hold_t = craft_hold_t + dt - if craft_hold_t <= CraftRecipes[craft_hold_i].hold { return false } - let i = craft_hold_i - craft_hold_stop() - return craft_make(i) + crafting_st.craft_hold_t = crafting_st.craft_hold_t + dt + if crafting_st.craft_hold_t <= CraftRecipes[crafting_st.craft_hold_i].hold { return false } + let i = crafting_st.craft_hold_i + craft_hold_stop(crafting_st) + return craft_make(base_st, crafting_st, i) } diff --git a/packages/ludic.crafting/system.ludic b/packages/ludic.crafting/system.ludic index fb233a5c..100cfd8c 100644 --- a/packages/ludic.crafting/system.ludic +++ b/packages/ludic.crafting/system.ludic @@ -1,8 +1,8 @@ # system.ludic - crafting as a system: a reset, and nothing saved (a hold never outlives its # screen; what a station holds is the station's) -export function craft_reset() -> void { - craft_hold_stop() - q_clear(craft_facts()) +export function craft_reset(base_st: mut BaseState, crafting_st: mut CraftingState) -> void { + craft_hold_stop(crafting_st) + q_clear(base_st, craft_facts(base_st, crafting_st)) } export function craft_system() -> System { diff --git a/packages/ludic.crafting/tests/crafting_test.ludic b/packages/ludic.crafting/tests/crafting_test.ludic index c000b949..bb953817 100644 --- a/packages/ludic.crafting/tests/crafting_test.ludic +++ b/packages/ludic.crafting/tests/crafting_test.ludic @@ -21,49 +21,51 @@ program CraftingTest { def CraftRecipes chair { out: TORCH, ins: [WOOD], ns: [3], station: BENCH } def CraftRecipes secret { out: TORCH, ins: [WOOD], ns: [1], learned: false } - var pack: []int = null - var bench_near: bool = true - var pot_takes: bool = true - var pot_got: int = -1 + state CraftingtestState { + pack: []int = null + bench_near: bool = true + pot_takes: bool = true + pot_got: int = -1 + } - function count(it: int) -> int { return pack[it] } - function take(it: int, n: int) -> bool { - if pack[it] < n { return false } - pack[it] = pack[it] - n + function count(craftingtest_st: CraftingtestState, it: int) -> int { return craftingtest_st.pack[it] } + function take(craftingtest_st: mut CraftingtestState, it: int, n: int) -> bool { + if craftingtest_st.pack[it] < n { return false } + craftingtest_st.pack[it] = craftingtest_st.pack[it] - n return true } - function add(it: int, n: int) -> int { - pack[it] = pack[it] + n + function add(craftingtest_st: mut CraftingtestState, it: int, n: int) -> int { + craftingtest_st.pack[it] = craftingtest_st.pack[it] + n return n } - function room(it: int) -> int { - if it == TORCH { return 3 - pack[it] } + function room(craftingtest_st: CraftingtestState, it: int) -> int { + if it == TORCH { return 3 - craftingtest_st.pack[it] } return 99 } function leaves(it: int) -> int { if it == WATER { return BOTTLE } return -1 } - function ready(st: int) -> bool { - if st == BENCH { return bench_near } + function ready(craftingtest_st: CraftingtestState, st: int) -> bool { + if st == BENCH { return craftingtest_st.bench_near } return true } - function start(st: int, recipe: int) -> bool { - if not pot_takes { return false } - pot_got = recipe + function start(craftingtest_st: mut CraftingtestState, st: int, recipe: int) -> bool { + if not craftingtest_st.pot_takes { return false } + craftingtest_st.pot_got = recipe return true } bind CraftPack { count: fn count, take: fn take, add: fn add, room: fn room, leaves: fn leaves } bind CraftStations { ready: fn ready, start: fn start } - function fresh() -> void { - pack = new []int - for i in 0 .. 8 { push(pack, 0) } - craft_reset() + function fresh(base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) -> void { + craftingtest_st.pack = new []int + for i in 0 .. 8 { push(craftingtest_st.pack, 0) } + craft_reset(base_st, crafting_st) } - function facts() -> []Crafted { return q_drain(craft_facts()) } + function facts(base_st: mut BaseState, crafting_st: mut CraftingState) -> []Crafted { return q_drain(base_st, craft_facts(base_st, crafting_st)) } test "the registry is the game's, in the order written" { expect_eq(CRAFT_COUNT, 5) @@ -74,105 +76,105 @@ program CraftingTest { expect_eq(craft_find(99), -1) } - test "made at once from the makings, and said" { - fresh() + test "made at once from the makings, and said" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) expect(not craft_can(CRAFT_TORCH)) - pack[WOOD] = 2 - pack[CLOTH] = 1 - expect(craft_make(CRAFT_TORCH)) - expect_eq(pack[WOOD], 1) - expect_eq(pack[CLOTH], 0) - expect_eq(pack[TORCH], 1) - let fs = facts() + craftingtest_st.pack[WOOD] = 2 + craftingtest_st.pack[CLOTH] = 1 + expect(craft_make(base_st, crafting_st, CRAFT_TORCH)) + expect_eq(craftingtest_st.pack[WOOD], 1) + expect_eq(craftingtest_st.pack[CLOTH], 0) + expect_eq(craftingtest_st.pack[TORCH], 1) + let fs = facts(base_st, crafting_st) expect_eq(len(fs), 1) expect_eq(fs[0].item, TORCH) expect(not fs[0].queued) } - test "nothing is taken when it cannot be made" { - fresh() - pack[WOOD] = 1 - expect(not craft_make(CRAFT_TORCH)) - expect_eq(pack[WOOD], 1) - expect_eq(len(facts()), 0) + test "nothing is taken when it cannot be made" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[WOOD] = 1 + expect(not craft_make(base_st, crafting_st, CRAFT_TORCH)) + expect_eq(craftingtest_st.pack[WOOD], 1) + expect_eq(len(facts(base_st, crafting_st)), 0) } - test "the station decides, and so does the room for what it makes" { - fresh() - pack[WOOD] = 9 - bench_near = false + test "the station decides, and so does the room for what it makes" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[WOOD] = 9 + craftingtest_st.bench_near = false expect(craft_has(CRAFT_CHAIR)) expect(not craft_can(CRAFT_CHAIR)) - bench_near = true + craftingtest_st.bench_near = true expect(craft_can(CRAFT_CHAIR)) - pack[TORCH] = 3 + craftingtest_st.pack[TORCH] = 3 expect(not craft_can(CRAFT_CHAIR)) } - test "an unlearned recipe is never made" { - fresh() - pack[WOOD] = 5 + test "an unlearned recipe is never made" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[WOOD] = 5 expect(not craft_can(CRAFT_SECRET)) } - test "a timed recipe goes into its station, and water leaves its bottle" { - fresh() - pack[HERB] = 2 - pack[WATER] = 1 - expect(craft_make(CRAFT_TEA)) - expect_eq(pot_got, CRAFT_TEA) - expect_eq(pack[TEA], 0) - expect_eq(pack[WATER], 0) - expect_eq(pack[BOTTLE], 1) - let fs = facts() + test "a timed recipe goes into its station, and water leaves its bottle" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[HERB] = 2 + craftingtest_st.pack[WATER] = 1 + expect(craft_make(base_st, crafting_st, CRAFT_TEA)) + expect_eq(craftingtest_st.pot_got, CRAFT_TEA) + expect_eq(craftingtest_st.pack[TEA], 0) + expect_eq(craftingtest_st.pack[WATER], 0) + expect_eq(craftingtest_st.pack[BOTTLE], 1) + let fs = facts(base_st, crafting_st) expect(fs[0].queued) expect_eq(fs[0].station, POT) } - test "a station that says no gives everything back" { - fresh() - pot_takes = false - pack[HERB] = 2 - pack[WATER] = 1 - expect(not craft_make(CRAFT_TEA)) - expect_eq(pack[HERB], 2) - expect_eq(pack[WATER], 1) - expect_eq(pack[BOTTLE], 0) - expect_eq(len(facts()), 0) + test "a station that says no gives everything back" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pot_takes = false + craftingtest_st.pack[HERB] = 2 + craftingtest_st.pack[WATER] = 1 + expect(not craft_make(base_st, crafting_st, CRAFT_TEA)) + expect_eq(craftingtest_st.pack[HERB], 2) + expect_eq(craftingtest_st.pack[WATER], 1) + expect_eq(craftingtest_st.pack[BOTTLE], 0) + expect_eq(len(facts(base_st, crafting_st)), 0) } - test "a hold makes it after its seconds, and not before" { - fresh() - pack[HERB] = 4 + test "a hold makes it after its seconds, and not before" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[HERB] = 4 expect(craft_holds(CRAFT_ROPE)) expect(not craft_holds(CRAFT_TORCH)) - expect(craft_hold_start(CRAFT_ROPE)) - expect(not craft_hold_run(1.0)) - expect(craft_hold_frac(CRAFT_ROPE) == 0.5) - expect(not craft_hold_run(0.9)) - expect(craft_hold_run(0.2)) - expect_eq(pack[ROPE], 2) - expect_eq(craft_holding(), -1) + expect(craft_hold_start(crafting_st, CRAFT_ROPE)) + expect(not craft_hold_run(base_st, crafting_st, 1.0)) + expect(craft_hold_frac(crafting_st, CRAFT_ROPE) == 0.5) + expect(not craft_hold_run(base_st, crafting_st, 0.9)) + expect(craft_hold_run(base_st, crafting_st, 0.2)) + expect_eq(craftingtest_st.pack[ROPE], 2) + expect_eq(craft_holding(crafting_st), -1) } - test "letting go wastes nothing, and losing the makings stops the hold" { - fresh() - pack[HERB] = 4 - craft_hold_start(CRAFT_ROPE) - craft_hold_run(1.5) - craft_hold_stop() - expect_eq(pack[HERB], 4) - craft_hold_start(CRAFT_ROPE) - pack[HERB] = 3 - expect(not craft_hold_run(5.0)) - expect_eq(craft_holding(), -1) - expect_eq(pack[ROPE], 0) + test "letting go wastes nothing, and losing the makings stops the hold" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[HERB] = 4 + craft_hold_start(crafting_st, CRAFT_ROPE) + craft_hold_run(base_st, crafting_st, 1.5) + craft_hold_stop(crafting_st) + expect_eq(craftingtest_st.pack[HERB], 4) + craft_hold_start(crafting_st, CRAFT_ROPE) + craftingtest_st.pack[HERB] = 3 + expect(not craft_hold_run(base_st, crafting_st, 5.0)) + expect_eq(craft_holding(crafting_st), -1) + expect_eq(craftingtest_st.pack[ROPE], 0) } - test "a click recipe cannot be held" { - fresh() - pack[WOOD] = 1 - pack[CLOTH] = 1 - expect(not craft_hold_start(CRAFT_TORCH)) + test "a click recipe cannot be held" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) { + fresh(base_st, crafting_st, craftingtest_st) + craftingtest_st.pack[WOOD] = 1 + craftingtest_st.pack[CLOTH] = 1 + expect(not craft_hold_start(crafting_st, CRAFT_TORCH)) } } diff --git a/packages/ludic.crafting/verbs.ludic b/packages/ludic.crafting/verbs.ludic index f50a8624..2b3c08ae 100644 --- a/packages/ludic.crafting/verbs.ludic +++ b/packages/ludic.crafting/verbs.ludic @@ -1,9 +1,13 @@ # verbs.ludic - can it be made, taking and giving back its makings, and making it -var craft_fact_q: Queue = null +export state CraftingState { + craft_hold_i: int = -1 + craft_hold_t: float = 0.0 + craft_fact_q: Queue = null +} -export function craft_facts() -> Queue { - if craft_fact_q == null { craft_fact_q = queue_new("crafting.crafted") } - return craft_fact_q +export function craft_facts(base_st: mut BaseState, crafting_st: mut CraftingState) -> Queue { + if crafting_st.craft_fact_q == null { crafting_st.craft_fact_q = queue_new(base_st, "crafting.crafted") } + return crafting_st.craft_fact_q } # every making is in the pack @@ -46,7 +50,7 @@ export function craft_refund(i: int) -> void { } } -function craft_fact_push(i: int, queued: bool) -> void { +function craft_fact_push(base_st: mut BaseState, crafting_st: mut CraftingState, i: int, queued: bool) -> void { let r = CraftRecipes[i] let c = new Crafted c.recipe = i @@ -54,12 +58,12 @@ function craft_fact_push(i: int, queued: bool) -> void { c.n = r.n c.station = r.station c.queued = queued - q_push(craft_facts(), c) + q_push(base_st, craft_facts(base_st, crafting_st), c) } # make it: a timed recipe at a station goes into the station (given back if it will not take it), # anything else is handed over at once. False, and the pack untouched, when it cannot be made. -export function craft_make(i: int) -> bool { +export function craft_make(base_st: mut BaseState, crafting_st: mut CraftingState, i: int) -> bool { if not craft_can(i) { return false } let r = CraftRecipes[i] craft_take(i) @@ -68,10 +72,10 @@ export function craft_make(i: int) -> bool { craft_refund(i) return false } - craft_fact_push(i, true) + craft_fact_push(base_st, crafting_st, i, true) return true } CraftPack.add(r.out, r.n) - craft_fact_push(i, false) + craft_fact_push(base_st, crafting_st, i, false) return true } diff --git a/packages/ludic.effects/queries.ludic b/packages/ludic.effects/queries.ludic index 2533775b..8c0ea0fe 100644 --- a/packages/ludic.effects/queries.ludic +++ b/packages/ludic.effects/queries.ludic @@ -2,32 +2,32 @@ function ef_live(e: Effect) -> bool { return e.delay < 0.01 } # what the rules should add for a kind right now (a delayed one counts only once it begins) -export function effects_sum(kind: int) -> float { - let all = ef_all() +export function effects_sum(effects_st: mut EffectsState, kind: int) -> float { + let all = ef_all(effects_st) var t = 0.0 for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { t = t + all[i].mag } } return t } -export function effects_has(kind: int) -> bool { - let all = ef_all() +export function effects_has(effects_st: mut EffectsState, kind: int) -> bool { + let all = ef_all(effects_st) for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { return true } } return false } -export function effects_bonus_count() -> int { - let all = ef_all() +export function effects_bonus_count(effects_st: mut EffectsState) -> int { + let all = ef_all(effects_st) var n = 0 for i in 0 .. len(all) { if all[i].bonus { n += 1 } } return n } # everything held, delayed or not, in the order added -export function effects_count() -> int { return len(ef_all()) } +export function effects_count(effects_st: mut EffectsState) -> int { return len(ef_all(effects_st)) } # a copy, so a reader cannot change the ring -export function effects_at(i: int) -> Effect { - let src = ef_all()[i] +export function effects_at(effects_st: mut EffectsState, i: int) -> Effect { + let src = ef_all(effects_st)[i] let e = new Effect e.kind = src.kind e.mag = src.mag @@ -39,9 +39,9 @@ export function effects_at(i: int) -> Effect { } # the indices of the ones that have begun, for a read-out -export function effects_live() -> []int { +export function effects_live(effects_st: mut EffectsState) -> []int { let out = new []int - let all = ef_all() + let all = ef_all(effects_st) for i in 0 .. len(all) { if ef_live(all[i]) { push(out, i) } } return out } diff --git a/packages/ludic.effects/state.ludic b/packages/ludic.effects/state.ludic index 0974b486..8abea912 100644 --- a/packages/ludic.effects/state.ludic +++ b/packages/ludic.effects/state.ludic @@ -20,27 +20,29 @@ export property EffectEnded { bonus: bool = false } -var ef_list: []Effect = null -var ef_max: int = 8 -var ef_bonus_max: int = 3 -var ef_ended: Queue = null +export state EffectsState { + ef_list: []Effect = null + ef_max: int = 8 + ef_bonus_max: int = 3 + ef_ended: Queue = null +} -function ef_all() -> []Effect { - if ef_list == null { ef_list = new []Effect } - return ef_list +function ef_all(effects_st: mut EffectsState) -> []Effect { + if effects_st.ef_list == null { effects_st.ef_list = new []Effect } + return effects_st.ef_list } # the facts: what ended and why, oldest first; the game drains it -export function effects_ended() -> Queue { - if ef_ended == null { ef_ended = queue_new("effects.ended") } - return ef_ended +export function effects_ended(base_st: mut BaseState, effects_st: mut EffectsState) -> Queue { + if effects_st.ef_ended == null { effects_st.ef_ended = queue_new(base_st, "effects.ended") } + return effects_st.ef_ended } # how many may run at once, and how many of those may be bonuses -export function effects_config(max: int, bonus_max: int) -> void { - ef_max = max - ef_bonus_max = bonus_max +export function effects_config(effects_st: mut EffectsState, max: int, bonus_max: int) -> void { + effects_st.ef_max = max + effects_st.ef_bonus_max = bonus_max } -export function effects_capacity() -> int { return ef_max } -export function effects_bonus_capacity() -> int { return ef_bonus_max } +export function effects_capacity(effects_st: EffectsState) -> int { return effects_st.ef_max } +export function effects_bonus_capacity(effects_st: EffectsState) -> int { return effects_st.ef_bonus_max } diff --git a/packages/ludic.effects/system.ludic b/packages/ludic.effects/system.ludic index 38a74b0c..2ba17fb3 100644 --- a/packages/ludic.effects/system.ludic +++ b/packages/ludic.effects/system.ludic @@ -1,14 +1,14 @@ # system.ludic - the effects as a system: reset, a tick in game hours, and its own save section -export function effects_reset() -> void { - effects_clear() - q_clear(effects_ended()) +export function effects_reset(base_st: mut BaseState, effects_st: mut EffectsState) -> void { + effects_clear(effects_st) + q_clear(base_st, effects_ended(base_st, effects_st)) } -function effects_tick(t: Tick) -> void { effects_run(t.hours * 60.0) } +function effects_tick(base_st: mut BaseState, effects_st: mut EffectsState, t: Tick) -> void { effects_run(base_st, effects_st, t.hours * 60.0) } -export function effects_save() -> Val { +export function effects_save(effects_st: mut EffectsState) -> Val { let l = Value.list() - let all = ef_all() + let all = ef_all(effects_st) for i in 0 .. len(all) { let o = Value.object() sv_put_int(o, "k", all[i].kind) @@ -24,12 +24,12 @@ export function effects_save() -> Val { return v } -export function effects_load(v: Val, version: int) -> void { - effects_clear() +export function effects_load(effects_st: mut EffectsState, v: Val, version: int) -> void { + effects_clear(effects_st) if Value.has(v, "list") == 0 { return } let l = Value.get(v, "list") for i in 0 .. Value.count(l) { - if len(ef_all()) >= ef_max { break } + if len(ef_all(effects_st)) >= effects_st.ef_max { break } let o = Value.at(l, i) let e = new Effect e.kind = sv_int(o, "k", 0) @@ -38,7 +38,7 @@ export function effects_load(v: Val, version: int) -> void { e.delay = sv_float(o, "d", 0.0) e.bonus = sv_bool(o, "b", false) e.label = sv_str(o, "l", "") - push(ef_all(), e) + push(ef_all(effects_st), e) } } diff --git a/packages/ludic.effects/tests/effects_test.ludic b/packages/ludic.effects/tests/effects_test.ludic index d9589670..2ca7a3a0 100644 --- a/packages/ludic.effects/tests/effects_test.ludic +++ b/packages/ludic.effects/tests/effects_test.ludic @@ -8,120 +8,120 @@ program EffectsTest { const WARMTH: int = 1 const CARRY: int = 2 - function fresh() -> void { - effects_config(8, 3) - effects_reset() + function fresh(base_st: mut BaseState, effects_st: mut EffectsState) -> void { + effects_config(effects_st, 8, 3) + effects_reset(base_st, effects_st) } - function ended() -> []EffectEnded { return q_drain(effects_ended()) } - function waiting() -> int { return q_len(effects_ended()) } + function ended(base_st: mut BaseState, effects_st: mut EffectsState) -> []EffectEnded { return q_drain(base_st, effects_ended(base_st, effects_st)) } + function waiting(base_st: mut BaseState, effects_st: mut EffectsState) -> int { return q_len(effects_ended(base_st, effects_st)) } function tick_minutes(m: float) -> void { let s = effects_system() s.tick(tick_new(0.016, 0, m / 60.0)) } - test "the sum is per kind, and a kind with nothing running is zero" { - fresh() - effects_add(LEGS, 8.0, 20.0, false, "Fizz") - effects_add(LEGS, 6.0, 45.0, false, "Trail mix") - effects_add(WARMTH, 12.0, 50.0, false, "Stew") - expect(effects_sum(LEGS) == 14.0) - expect(effects_sum(WARMTH) == 12.0) - expect(effects_sum(CARRY) == 0.0) - expect(effects_has(WARMTH)) - expect(not effects_has(CARRY)) + test "the sum is per kind, and a kind with nothing running is zero" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz") + effects_add(base_st, effects_st, LEGS, 6.0, 45.0, false, "Trail mix") + effects_add(base_st, effects_st, WARMTH, 12.0, 50.0, false, "Stew") + expect(effects_sum(effects_st, LEGS) == 14.0) + expect(effects_sum(effects_st, WARMTH) == 12.0) + expect(effects_sum(effects_st, CARRY) == 0.0) + expect(effects_has(effects_st, WARMTH)) + expect(not effects_has(effects_st, CARRY)) } - test "the same kind from the same label replaces itself rather than stacking" { - fresh() - effects_add(LEGS, 8.0, 20.0, false, "Fizz") - effects_add(LEGS, 8.0, 20.0, false, "Fizz") - expect_eq(effects_count(), 1) - expect(effects_sum(LEGS) == 8.0) + test "the same kind from the same label replaces itself rather than stacking" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz") + effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz") + expect_eq(effects_count(effects_st), 1) + expect(effects_sum(effects_st, LEGS) == 8.0) } - test "a fourth bonus pushes the oldest out and says so" { - fresh() - effects_add(LEGS, 1.0, 10.0, true, "a") - effects_add(LEGS, 1.0, 30.0, true, "b") - effects_add(WARMTH, 1.0, 40.0, true, "c") - effects_add(CARRY, 1.0, 50.0, true, "d") - expect_eq(effects_bonus_count(), 3) - let gone = ended() + test "a fourth bonus pushes the oldest out and says so" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 1.0, 10.0, true, "a") + effects_add(base_st, effects_st, LEGS, 1.0, 30.0, true, "b") + effects_add(base_st, effects_st, WARMTH, 1.0, 40.0, true, "c") + effects_add(base_st, effects_st, CARRY, 1.0, 50.0, true, "d") + expect_eq(effects_bonus_count(effects_st), 3) + let gone = ended(base_st, effects_st) expect_eq(len(gone), 1) expect(gone[0].label == "a") expect_eq(gone[0].why, EFFECT_PUSHED_OUT) } - test "a full ring drops the one nearest its end" { - fresh() - effects_config(2, 3) - effects_add(LEGS, 1.0, 10.0, false, "short") - effects_add(WARMTH, 1.0, 90.0, false, "long") - effects_add(CARRY, 1.0, 50.0, false, "new") - expect_eq(effects_count(), 2) - expect(not effects_has(LEGS)) - expect_eq(ended()[0].why, EFFECT_CROWDED_OUT) + test "a full ring drops the one nearest its end" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_config(effects_st, 2, 3) + effects_add(base_st, effects_st, LEGS, 1.0, 10.0, false, "short") + effects_add(base_st, effects_st, WARMTH, 1.0, 90.0, false, "long") + effects_add(base_st, effects_st, CARRY, 1.0, 50.0, false, "new") + expect_eq(effects_count(effects_st), 2) + expect(not effects_has(effects_st, LEGS)) + expect_eq(ended(base_st, effects_st)[0].why, EFFECT_CROWDED_OUT) } - test "a delayed one counts only once it has begun" { - fresh() - effects_add(LEGS, 8.0, 20.0, false, "Fizz") - effects_add_after(20.0, LEGS, -5.0, 15.0, false, "Sugar crash") - expect(effects_sum(LEGS) == 8.0) - expect_eq(len(effects_live()), 1) + test "a delayed one counts only once it has begun" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz") + effects_add_after(base_st, effects_st, 20.0, LEGS, -5.0, 15.0, false, "Sugar crash") + expect(effects_sum(effects_st, LEGS) == 8.0) + expect_eq(len(effects_live(effects_st)), 1) tick_minutes(25.0) - expect(effects_sum(LEGS) == -5.0) + expect(effects_sum(effects_st, LEGS) == -5.0) tick_minutes(20.0) - expect_eq(effects_count(), 0) + expect_eq(effects_count(effects_st), 0) } - test "running down in game hours reports each ending" { - fresh() - effects_add(WARMTH, -10.0, 10.0, false, "Wet through") + test "running down in game hours reports each ending" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, WARMTH, -10.0, 10.0, false, "Wet through") tick_minutes(5.0) - expect_eq(waiting(), 0) + expect_eq(waiting(base_st, effects_st), 0) tick_minutes(6.0) - expect_eq(effects_count(), 0) - let gone = ended() + expect_eq(effects_count(effects_st), 0) + let gone = ended(base_st, effects_st) expect_eq(gone[0].why, EFFECT_RAN_OUT) expect(gone[0].label == "Wet through") } - test "clear stops everything and says nothing" { - fresh() - effects_add(LEGS, 1.0, 10.0, false, "a") - effects_clear() - expect_eq(effects_count(), 0) - expect_eq(waiting(), 0) + test "clear stops everything and says nothing" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 1.0, 10.0, false, "a") + effects_clear(effects_st) + expect_eq(effects_count(effects_st), 0) + expect_eq(waiting(base_st, effects_st), 0) } - test "a save section reads back the same ring" { - fresh() - effects_add(LEGS, 8.5, 20.0, true, "Fizz") - effects_add_after(20.0, LEGS, -5.0, 15.0, false, "Sugar crash") + test "a save section reads back the same ring" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 8.5, 20.0, true, "Fizz") + effects_add_after(base_st, effects_st, 20.0, LEGS, -5.0, 15.0, false, "Sugar crash") let root = save_tree() - save_section(root, "effects", 1, effects_save()) + save_section(root, "effects", 1, effects_save(effects_st)) let text = save_encode(root) - effects_clear() + effects_clear(effects_st) let n = load_section(save_decode(text), "effects") expect(n.found) - effects_load(n.data, n.version) - expect_eq(effects_count(), 2) - let a = effects_at(0) + effects_load(effects_st, n.data, n.version) + expect_eq(effects_count(effects_st), 2) + let a = effects_at(effects_st, 0) expect(a.mag == 8.5) expect(a.bonus) - let b = effects_at(1) + let b = effects_at(effects_st, 1) expect(b.delay == 20.0) expect(b.label == "Sugar crash") } - test "the copy a reader gets does not change the ring" { - fresh() - effects_add(LEGS, 3.0, 10.0, false, "a") - let e = effects_at(0) + test "the copy a reader gets does not change the ring" (base_st: mut BaseState, effects_st: mut EffectsState) { + fresh(base_st, effects_st) + effects_add(base_st, effects_st, LEGS, 3.0, 10.0, false, "a") + let e = effects_at(effects_st, 0) e.mag = 100.0 - expect(effects_sum(LEGS) == 3.0) + expect(effects_sum(effects_st, LEGS) == 3.0) } } diff --git a/packages/ludic.effects/verbs.ludic b/packages/ludic.effects/verbs.ludic index f1376329..7a1de872 100644 --- a/packages/ludic.effects/verbs.ludic +++ b/packages/ludic.effects/verbs.ludic @@ -1,26 +1,26 @@ # verbs.ludic - the only way the ring changes. The same kind from the same label replaces # itself; a bonus past the cap pushes out the oldest bonus; a full ring drops the nearest end. -function ef_end(i: int, why: int) -> void { - let all = ef_all() +function ef_end(base_st: mut BaseState, effects_st: mut EffectsState, i: int, why: int) -> void { + let all = ef_all(effects_st) let e = new EffectEnded e.kind = all[i].kind e.label = all[i].label e.why = why e.bonus = all[i].bonus - q_push(effects_ended(), e) - ef_drop(i) + q_push(base_st, effects_ended(base_st, effects_st), e) + ef_drop(effects_st, i) } -function ef_drop(i: int) -> void { - let all = ef_all() +function ef_drop(effects_st: mut EffectsState, i: int) -> void { + let all = ef_all(effects_st) let rest = new []Effect for k in 0 .. len(all) { if k != i { push(rest, all[k]) } } - ef_list = rest + effects_st.ef_list = rest } # the one with the least time left (of the bonuses only, when asked); -1 when there is none -function ef_nearest_end(bonus_only: bool) -> int { - let all = ef_all() +function ef_nearest_end(effects_st: mut EffectsState, bonus_only: bool) -> int { + let all = ef_all(effects_st) var at = -1 for i in 0 .. len(all) { if bonus_only and not all[i].bonus { continue } @@ -29,16 +29,16 @@ function ef_nearest_end(bonus_only: bool) -> int { return at } -function ef_make_room(bonus: bool) -> void { +function ef_make_room(base_st: mut BaseState, effects_st: mut EffectsState, bonus: bool) -> void { if bonus { - while effects_bonus_count() >= ef_bonus_max and ef_nearest_end(true) >= 0 { ef_end(ef_nearest_end(true), EFFECT_PUSHED_OUT) } + while effects_bonus_count(effects_st) >= effects_st.ef_bonus_max and ef_nearest_end(effects_st, true) >= 0 { ef_end(base_st, effects_st, ef_nearest_end(effects_st, true), EFFECT_PUSHED_OUT) } } - if len(ef_all()) >= ef_max and ef_nearest_end(false) >= 0 { ef_end(ef_nearest_end(false), EFFECT_CROWDED_OUT) } + if len(ef_all(effects_st)) >= effects_st.ef_max and ef_nearest_end(effects_st, false) >= 0 { ef_end(base_st, effects_st, ef_nearest_end(effects_st, false), EFFECT_CROWDED_OUT) } } # begin one `after` game minutes from now, lasting `minutes` once it has begun -export function effects_add_after(after: float, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void { - let all = ef_all() +export function effects_add_after(base_st: mut BaseState, effects_st: mut EffectsState, after: float, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void { + let all = ef_all(effects_st) for i in 0 .. len(all) { if all[i].kind == kind and all[i].label == label { all[i].mag = mag @@ -46,7 +46,7 @@ export function effects_add_after(after: float, kind: int, mag: float, minutes: return } } - ef_make_room(bonus) + ef_make_room(base_st, effects_st, bonus) let e = new Effect e.kind = kind e.mag = mag @@ -54,27 +54,27 @@ export function effects_add_after(after: float, kind: int, mag: float, minutes: e.delay = after e.bonus = bonus e.label = label - push(ef_all(), e) + push(ef_all(effects_st), e) } -export function effects_add(kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void { - effects_add_after(0.0, kind, mag, minutes, bonus, label) +export function effects_add(base_st: mut BaseState, effects_st: mut EffectsState, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void { + effects_add_after(base_st, effects_st, 0.0, kind, mag, minutes, bonus, label) } # everything stops at once, and says nothing: a new trip or a night's sleep -export function effects_clear() -> void { ef_list = new []Effect } +export function effects_clear(effects_st: mut EffectsState) -> void { effects_st.ef_list = new []Effect } # run them down by `minutes` of game time; a delayed one waits first -export function effects_run(minutes: float) -> void { +export function effects_run(base_st: mut BaseState, effects_st: mut EffectsState, minutes: float) -> void { var i = 0 - while i < len(ef_all()) { - let e = ef_all()[i] + while i < len(ef_all(effects_st)) { + let e = ef_all(effects_st)[i] if e.delay > 0.0 { e.delay = e.delay - minutes i += 1 continue } e.left = e.left - minutes - if e.left < 0.0 { ef_end(i, EFFECT_RAN_OUT) } else { i += 1 } + if e.left < 0.0 { ef_end(base_st, effects_st, i, EFFECT_RAN_OUT) } else { i += 1 } } } diff --git a/packages/ludic.fire/decide.ludic b/packages/ludic.fire/decide.ludic index 69250371..e620798f 100644 --- a/packages/ludic.fire/decide.ludic +++ b/packages/ludic.fire/decide.ludic @@ -1,31 +1,31 @@ # decide.ludic - lighting and feeding in three steps, so a machine that does not own the fire can # ask the one that does: what it would come to (fire_decide), what it costs in wood # (fire_cost), and doing it (fire_apply - only ever on the machine that owns the world) -export function fire_decide(wood: int) -> int { - if fr_lit { +export function fire_decide(fire_st: FireState, wood: int) -> int { + if fire_st.fr_lit { if wood < 1 { return FIRE_NO_LOG } return FIRE_FEED } if not FireWorld.allowed() { return FIRE_BANNED } - if wood < fr_light_wood { return FIRE_NO_WOOD } + if wood < fire_st.fr_light_wood { return FIRE_NO_WOOD } if FireWorld.is_wet() and not FireWorld.sheltered() { return FIRE_SOAKED } return FIRE_LIGHT } -export function fire_cost(how: int) -> int { - if how == FIRE_LIGHT { return fr_light_wood } +export function fire_cost(fire_st: FireState, how: int) -> int { + if how == FIRE_LIGHT { return fire_st.fr_light_wood } if how == FIRE_FEED { return 1 } return 0 } -export function fire_apply(how: int) -> void { - if how == FIRE_FEED { fr_fuel = fr_fuel + fr_log_minutes } +export function fire_apply(base_st: mut BaseState, fire_st: mut FireState, how: int) -> void { + if how == FIRE_FEED { fire_st.fr_fuel = fire_st.fr_fuel + fire_st.fr_log_minutes } if how == FIRE_LIGHT { - fr_lit = true - fr_fuel = fr_light_minutes - fr_fact(FIRE_LIT) + fire_st.fr_lit = true + fire_st.fr_fuel = fire_st.fr_light_minutes + fr_fact(base_st, fire_st, FIRE_LIT) } } # the logs it takes to light one -export function fire_light_wood() -> int { return fr_light_wood } +export function fire_light_wood(fire_st: FireState) -> int { return fire_st.fr_light_wood } diff --git a/packages/ludic.fire/queries.ludic b/packages/ludic.fire/queries.ludic index f9a7a4fc..162e26c7 100644 --- a/packages/ludic.fire/queries.ludic +++ b/packages/ludic.fire/queries.ludic @@ -1,16 +1,16 @@ # queries.ludic - what the world asks the fire -export function fire_lit() -> bool { return fr_lit } +export function fire_lit(fire_st: FireState) -> bool { return fire_st.fr_lit } # game minutes of fuel left -export function fire_fuel() -> float { return fr_fuel } +export function fire_fuel(fire_st: FireState) -> float { return fire_st.fr_fuel } -export function fire_low() -> bool { return fr_lit and fr_fuel < fr_low_minutes } +export function fire_low(fire_st: FireState) -> bool { return fire_st.fr_lit and fire_st.fr_fuel < fire_st.fr_low_minutes } # game hours today it has burned in the rain with something over it -export function fire_rain_hours() -> float { return fr_rain_h } +export function fire_rain_hours(fire_st: FireState) -> float { return fire_st.fr_rain_h } # does it warm a body `dist` metres from it? -export function fire_warms(dist: float) -> bool { return fr_lit and dist < fr_warm_radius } +export function fire_warms(fire_st: FireState, dist: float) -> bool { return fire_st.fr_lit and dist < fire_st.fr_warm_radius } # will it cook for someone `dist` metres from it? (a station that stands on it asks only fire_lit) -export function fire_cooks(dist: float) -> bool { return fr_lit and dist < fr_cook_radius } +export function fire_cooks(fire_st: FireState, dist: float) -> bool { return fire_st.fr_lit and dist < fire_st.fr_cook_radius } diff --git a/packages/ludic.fire/state.ludic b/packages/ludic.fire/state.ludic index d5184146..dba12309 100644 --- a/packages/ludic.fire/state.ludic +++ b/packages/ludic.fire/state.ludic @@ -10,12 +10,18 @@ export port FireWorld { function fr_yes() -> bool { return true } function fr_no() -> bool { return false } -var fr_light_wood: int = 3 # logs to light one -var fr_light_minutes: float = 120.0 # game minutes of fuel a new fire has -var fr_log_minutes: float = 60.0 # game minutes a log adds -var fr_low_minutes: float = 15.0 # under this, the fire is burning low -var fr_warm_radius: float = 3.6 # metres it warms a body -var fr_cook_radius: float = 3.5 # metres it cooks from +export state FireState { + fr_light_wood: int = 3 # logs to light one + fr_light_minutes: float = 120.0 # game minutes of fuel a new fire has + fr_log_minutes: float = 60.0 # game minutes a log adds + fr_low_minutes: float = 15.0 # under this, the fire is burning low + fr_warm_radius: float = 3.6 # metres it warms a body + fr_cook_radius: float = 3.5 # metres it cooks from + fr_lit: bool = false + fr_fuel: float = 0.0 # game minutes left + fr_rain_h: float = 0.0 # game hours today it burned in the rain under cover + fr_facts: Queue = null +} # what lighting or feeding would come to, as the world stands export const FIRE_NO_WOOD: int = 0 # not enough to light one @@ -35,34 +41,30 @@ export property FireFact { fuel: float = 0.0 } -var fr_lit: bool = false -var fr_fuel: float = 0.0 # game minutes left -var fr_rain_h: float = 0.0 # game hours today it burned in the rain under cover -var fr_facts: Queue = null # the facts, oldest first; the game drains them into its notices -export function fire_facts() -> Queue { - if fr_facts == null { fr_facts = queue_new("fire.facts") } - return fr_facts +export function fire_facts(base_st: mut BaseState, fire_st: mut FireState) -> Queue { + if fire_st.fr_facts == null { fire_st.fr_facts = queue_new(base_st, "fire.facts") } + return fire_st.fr_facts } -function fr_fact(what: int) -> void { +function fr_fact(base_st: mut BaseState, fire_st: mut FireState, what: int) -> void { let f = new FireFact f.what = what - f.fuel = fr_fuel - q_push(fire_facts(), f) + f.fuel = fire_st.fr_fuel + q_push(base_st, fire_facts(base_st, fire_st), f) } # a game's numbers: logs to light, minutes a new fire and a log are worth, when it is low -export function fire_config(light_wood: int, light_minutes: float, log_minutes: float, low_minutes: float) -> void { - fr_light_wood = light_wood - fr_light_minutes = light_minutes - fr_log_minutes = log_minutes - fr_low_minutes = low_minutes +export function fire_config(fire_st: mut FireState, light_wood: int, light_minutes: float, log_minutes: float, low_minutes: float) -> void { + fire_st.fr_light_wood = light_wood + fire_st.fr_light_minutes = light_minutes + fire_st.fr_log_minutes = log_minutes + fire_st.fr_low_minutes = low_minutes } # how far it warms a body and how far it cooks from, in metres -export function fire_config_reach(warm: float, cook: float) -> void { - fr_warm_radius = warm - fr_cook_radius = cook +export function fire_config_reach(fire_st: mut FireState, warm: float, cook: float) -> void { + fire_st.fr_warm_radius = warm + fire_st.fr_cook_radius = cook } diff --git a/packages/ludic.fire/system.ludic b/packages/ludic.fire/system.ludic index e6dd92e1..f699c4e2 100644 --- a/packages/ludic.fire/system.ludic +++ b/packages/ludic.fire/system.ludic @@ -1,19 +1,19 @@ # system.ludic - the fire as a system: PH_SIMULATE, and its own save section -function fire_tick(t: Tick) -> void { fire_run(t.hours) } +function fire_tick(base_st: mut BaseState, fire_st: mut FireState, t: Tick) -> void { fire_run(base_st, fire_st, t.hours) } -export function fire_save() -> Val { +export function fire_save(fire_st: FireState) -> Val { let v = Value.object() - sv_put_bool(v, "lit", fr_lit) - sv_put_float(v, "fuel", fr_fuel) - sv_put_float(v, "rain_h", fr_rain_h) + sv_put_bool(v, "lit", fire_st.fr_lit) + sv_put_float(v, "fuel", fire_st.fr_fuel) + sv_put_float(v, "rain_h", fire_st.fr_rain_h) return v } -export function fire_load(v: Val, version: int) -> void { - fire_reset() - fr_lit = sv_bool(v, "lit", false) - fr_fuel = sv_float(v, "fuel", 0.0) - fr_rain_h = sv_float(v, "rain_h", 0.0) +export function fire_load(base_st: mut BaseState, fire_st: mut FireState, v: Val, version: int) -> void { + fire_reset(base_st, fire_st) + fire_st.fr_lit = sv_bool(v, "lit", false) + fire_st.fr_fuel = sv_float(v, "fuel", 0.0) + fire_st.fr_rain_h = sv_float(v, "rain_h", 0.0) } export function fire_system() -> System { diff --git a/packages/ludic.fire/tests/fire_test.ludic b/packages/ludic.fire/tests/fire_test.ludic index dee2078d..363bfd6f 100644 --- a/packages/ludic.fire/tests/fire_test.ludic +++ b/packages/ludic.fire/tests/fire_test.ludic @@ -4,15 +4,17 @@ import "ludic.fire" import "ludic.base" program FireTest { numbers float - var owns: bool = true - var wet: bool = false - var tarp: bool = false - var ban: bool = false + state FiretestState { + owns: bool = true + wet: bool = false + tarp: bool = false + ban: bool = false + } - function fake_owns() -> bool { return owns } - function fake_wet() -> bool { return wet } - function fake_tarp() -> bool { return tarp } - function fake_allowed() -> bool { return not ban } + function fake_owns(firetest_st: FiretestState) -> bool { return firetest_st.owns } + function fake_wet(firetest_st: FiretestState) -> bool { return firetest_st.wet } + function fake_tarp(firetest_st: FiretestState) -> bool { return firetest_st.tarp } + function fake_allowed(firetest_st: FiretestState) -> bool { return not firetest_st.ban } bind FireWorld { owns_world: fn fake_owns @@ -21,125 +23,125 @@ program FireTest { allowed: fn fake_allowed } - function fresh() -> void { - fire_config(3, 120.0, 60.0, 15.0) - fire_config_reach(3.6, 3.5) - fire_reset() + function fresh(base_st: mut BaseState, fire_st: mut FireState) -> void { + fire_config(fire_st, 3, 120.0, 60.0, 15.0) + fire_config_reach(fire_st, 3.6, 3.5) + fire_reset(base_st, fire_st) } - function facts() -> []FireFact { return q_drain(fire_facts()) } + function facts(base_st: mut BaseState, fire_st: mut FireState) -> []FireFact { return q_drain(base_st, fire_facts(base_st, fire_st)) } function count(fs: []FireFact, what: int) -> int { var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } - function light() -> void { fire_apply(fire_decide(3)) } + function light(base_st: mut BaseState, fire_st: mut FireState) -> void { fire_apply(base_st, fire_st, fire_decide(fire_st, 3)) } - test "three logs light it, and that is a fact" { - fresh() - expect_eq(fire_decide(2), FIRE_NO_WOOD) - expect_eq(fire_decide(3), FIRE_LIGHT) - expect_eq(fire_cost(FIRE_LIGHT), 3) - light() - expect(fire_lit()) - expect(fire_fuel() == 120.0) - expect_eq(count(facts(), FIRE_LIT), 1) + test "three logs light it, and that is a fact" (base_st: mut BaseState, fire_st: mut FireState) { + fresh(base_st, fire_st) + expect_eq(fire_decide(fire_st, 2), FIRE_NO_WOOD) + expect_eq(fire_decide(fire_st, 3), FIRE_LIGHT) + expect_eq(fire_cost(fire_st, FIRE_LIGHT), 3) + light(base_st, fire_st) + expect(fire_lit(fire_st)) + expect(fire_fuel(fire_st) == 120.0) + expect_eq(count(facts(base_st, fire_st), FIRE_LIT), 1) } - test "a lit fire takes a log for an hour" { - fresh() - light() - expect_eq(fire_decide(0), FIRE_NO_LOG) - expect_eq(fire_decide(1), FIRE_FEED) - expect_eq(fire_cost(FIRE_FEED), 1) - fire_apply(FIRE_FEED) - expect(fire_fuel() == 180.0) + test "a lit fire takes a log for an hour" (base_st: mut BaseState, fire_st: mut FireState) { + fresh(base_st, fire_st) + light(base_st, fire_st) + expect_eq(fire_decide(fire_st, 0), FIRE_NO_LOG) + expect_eq(fire_decide(fire_st, 1), FIRE_FEED) + expect_eq(fire_cost(fire_st, FIRE_FEED), 1) + fire_apply(base_st, fire_st, FIRE_FEED) + expect(fire_fuel(fire_st) == 180.0) } - test "a ban and a soaked ring refuse, a tarp keeps it dry" { - fresh() - ban = true - expect_eq(fire_decide(5), FIRE_BANNED) - ban = false - wet = true - expect_eq(fire_decide(5), FIRE_SOAKED) - expect_eq(fire_cost(FIRE_SOAKED), 0) - tarp = true - expect_eq(fire_decide(5), FIRE_LIGHT) + test "a ban and a soaked ring refuse, a tarp keeps it dry" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) { + fresh(base_st, fire_st) + firetest_st.ban = true + expect_eq(fire_decide(fire_st, 5), FIRE_BANNED) + firetest_st.ban = false + firetest_st.wet = true + expect_eq(fire_decide(fire_st, 5), FIRE_SOAKED) + expect_eq(fire_cost(fire_st, FIRE_SOAKED), 0) + firetest_st.tarp = true + expect_eq(fire_decide(fire_st, 5), FIRE_LIGHT) } - test "it burns a minute a minute, runs low, and goes out as facts" { - fresh() - light() - facts() - fire_run(1.5) - expect(fire_fuel() == 30.0) - expect(not fire_low()) - fire_run(0.3) - expect(fire_low()) - expect_eq(count(facts(), FIRE_LOW_FUEL), 1) - fire_run(0.5) - expect(not fire_lit()) - expect(fire_fuel() == 0.0) - expect_eq(count(facts(), FIRE_WENT_OUT), 1) + test "it burns a minute a minute, runs low, and goes out as facts" (base_st: mut BaseState, fire_st: mut FireState) { + fresh(base_st, fire_st) + light(base_st, fire_st) + facts(base_st, fire_st) + fire_run(base_st, fire_st, 1.5) + expect(fire_fuel(fire_st) == 30.0) + expect(not fire_low(fire_st)) + fire_run(base_st, fire_st, 0.3) + expect(fire_low(fire_st)) + expect_eq(count(facts(base_st, fire_st), FIRE_LOW_FUEL), 1) + fire_run(base_st, fire_st, 0.5) + expect(not fire_lit(fire_st)) + expect(fire_fuel(fire_st) == 0.0) + expect_eq(count(facts(base_st, fire_st), FIRE_WENT_OUT), 1) } - test "the rain burns it twice as fast, and under a tarp it counts the hours" { - fresh() - light() - wet = true - fire_run(0.5) - expect(fire_fuel() == 60.0) - tarp = true - fire_run(0.5) - expect(fire_fuel() == 30.0) - expect(fire_rain_hours() == 0.5) - fire_morning() - expect(fire_rain_hours() == 0.0) + test "the rain burns it twice as fast, and under a tarp it counts the hours" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) { + fresh(base_st, fire_st) + light(base_st, fire_st) + firetest_st.wet = true + fire_run(base_st, fire_st, 0.5) + expect(fire_fuel(fire_st) == 60.0) + firetest_st.tarp = true + fire_run(base_st, fire_st, 0.5) + expect(fire_fuel(fire_st) == 30.0) + expect(fire_rain_hours(fire_st) == 0.5) + fire_morning(fire_st) + expect(fire_rain_hours(fire_st) == 0.0) } - test "a machine that does not own the world burns nothing, but is told" { - fresh() - light() - owns = false - fire_run(5.0) - expect(fire_fuel() == 120.0) - facts() - fire_told(false, 0.0, true) - expect(not fire_lit()) - expect_eq(count(facts(), FIRE_WENT_OUT), 1) - fire_told(true, 50.0, true) - fire_told(false, 0.0, false) - expect_eq(q_len(fire_facts()), 0) + test "a machine that does not own the world burns nothing, but is told" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) { + fresh(base_st, fire_st) + light(base_st, fire_st) + firetest_st.owns = false + fire_run(base_st, fire_st, 5.0) + expect(fire_fuel(fire_st) == 120.0) + facts(base_st, fire_st) + fire_told(base_st, fire_st, false, 0.0, true) + expect(not fire_lit(fire_st)) + expect_eq(count(facts(base_st, fire_st), FIRE_WENT_OUT), 1) + fire_told(base_st, fire_st, true, 50.0, true) + fire_told(base_st, fire_st, false, 0.0, false) + expect_eq(q_len(fire_facts(base_st, fire_st)), 0) } - test "it warms and cooks close, and only while lit" { - fresh() - expect(not fire_warms(1.0)) - light() - expect(fire_warms(3.5)) - expect(not fire_warms(3.7)) - expect(fire_cooks(3.4)) - expect(not fire_cooks(3.55)) - fire_out() - expect(not fire_cooks(0.0)) + test "it warms and cooks close, and only while lit" (base_st: mut BaseState, fire_st: mut FireState) { + fresh(base_st, fire_st) + expect(not fire_warms(fire_st, 1.0)) + light(base_st, fire_st) + expect(fire_warms(fire_st, 3.5)) + expect(not fire_warms(fire_st, 3.7)) + expect(fire_cooks(fire_st, 3.4)) + expect(not fire_cooks(fire_st, 3.55)) + fire_out(fire_st) + expect(not fire_cooks(fire_st, 0.0)) } - test "a save section reads back the same fire" { - fresh() - light() - wet = true - tarp = true - fire_run(0.25) + test "a save section reads back the same fire" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) { + fresh(base_st, fire_st) + light(base_st, fire_st) + firetest_st.wet = true + firetest_st.tarp = true + fire_run(base_st, fire_st, 0.25) let root = save_tree() - save_section(root, "fire", 1, fire_save()) + save_section(root, "fire", 1, fire_save(fire_st)) let text = save_encode(root) - fire_reset() + fire_reset(base_st, fire_st) let n = load_section(save_decode(text), "fire") - fire_load(n.data, n.version) - expect(fire_lit()) - expect(fire_fuel() == 105.0) - expect(fire_rain_hours() == 0.25) + fire_load(base_st, fire_st, n.data, n.version) + expect(fire_lit(fire_st)) + expect(fire_fuel(fire_st) == 105.0) + expect(fire_rain_hours(fire_st) == 0.25) } } diff --git a/packages/ludic.fire/verbs.ludic b/packages/ludic.fire/verbs.ludic index 22d48dfa..22424067 100644 --- a/packages/ludic.fire/verbs.ludic +++ b/packages/ludic.fire/verbs.ludic @@ -1,55 +1,55 @@ # verbs.ludic - the fire burning down, and the only other ways it changes -function fr_out() -> void { - fr_lit = false - fr_fuel = 0.0 - fr_fact(FIRE_WENT_OUT) +function fr_out(base_st: mut BaseState, fire_st: mut FireState) -> void { + fire_st.fr_lit = false + fire_st.fr_fuel = 0.0 + fr_fact(base_st, fire_st, FIRE_WENT_OUT) } # `gh` game hours of burning; in the rain it burns twice as fast with nothing over the ring, # and under cover it counts the hours it held. Only the machine that owns the world burns it. -export function fire_run(gh: float) -> void { - if not fr_lit or not FireWorld.owns_world() { return } - let was = fr_fuel - fr_fuel = fr_fuel - gh * 60.0 +export function fire_run(base_st: mut BaseState, fire_st: mut FireState, gh: float) -> void { + if not fire_st.fr_lit or not FireWorld.owns_world() { return } + let was = fire_st.fr_fuel + fire_st.fr_fuel = fire_st.fr_fuel - gh * 60.0 if FireWorld.is_wet() { - if FireWorld.sheltered() { fr_rain_h = fr_rain_h + gh } else { fr_fuel = fr_fuel - gh * 60.0 } + if FireWorld.sheltered() { fire_st.fr_rain_h = fire_st.fr_rain_h + gh } else { fire_st.fr_fuel = fire_st.fr_fuel - gh * 60.0 } } - if fr_fuel < 0.0 { - fr_out() + if fire_st.fr_fuel < 0.0 { + fr_out(base_st, fire_st) return } - if was >= fr_low_minutes and fr_fuel < fr_low_minutes { fr_fact(FIRE_LOW_FUEL) } + if was >= fire_st.fr_low_minutes and fire_st.fr_fuel < fire_st.fr_low_minutes { fr_fact(base_st, fire_st, FIRE_LOW_FUEL) } } # put out, and nothing said (the night burns it down) -export function fire_out() -> void { - fr_lit = false - fr_fuel = 0.0 +export function fire_out(fire_st: mut FireState) -> void { + fire_st.fr_lit = false + fire_st.fr_fuel = 0.0 } # burning with `fuel` minutes, and nothing said (staging, a test) -export function fire_set_burning(fuel: float) -> void { - fr_lit = true - fr_fuel = fuel +export function fire_set_burning(fire_st: mut FireState, fuel: float) -> void { + fire_st.fr_lit = true + fire_st.fr_fuel = fuel } # what the machine that owns the world says the fire is; going out is a fact when `say` -export function fire_told(lit: bool, fuel: float, say: bool) -> void { - let was = fr_lit - fr_lit = lit - fr_fuel = fuel - if was and not lit and say { fr_fact(FIRE_WENT_OUT) } +export function fire_told(base_st: mut BaseState, fire_st: mut FireState, lit: bool, fuel: float, say: bool) -> void { + let was = fire_st.fr_lit + fire_st.fr_lit = lit + fire_st.fr_fuel = fuel + if was and not lit and say { fr_fact(base_st, fire_st, FIRE_WENT_OUT) } } # the rain hours outright (staging, a test) -export function fire_set_rain_hours(h: float) -> void { fr_rain_h = h } +export function fire_set_rain_hours(fire_st: mut FireState, h: float) -> void { fire_st.fr_rain_h = h } # a new day: the rain hours start again -export function fire_morning() -> void { fr_rain_h = 0.0 } +export function fire_morning(fire_st: mut FireState) -> void { fire_st.fr_rain_h = 0.0 } -export function fire_reset() -> void { - fr_lit = false - fr_fuel = 0.0 - fr_rain_h = 0.0 - q_clear(fire_facts()) +export function fire_reset(base_st: mut BaseState, fire_st: mut FireState) -> void { + fire_st.fr_lit = false + fire_st.fr_fuel = 0.0 + fire_st.fr_rain_h = 0.0 + q_clear(base_st, fire_facts(base_st, fire_st)) } diff --git a/packages/ludic.fishing/fight.ludic b/packages/ludic.fishing/fight.ludic index 668a22a7..19fa0305 100644 --- a/packages/ludic.fishing/fight.ludic +++ b/packages/ludic.fishing/fight.ludic @@ -1,16 +1,16 @@ # fight.ludic - what takes, how big it is, and the green band the marker has to cross in # a legend on a lure, likelier in deep water and with skill; else an everyday one by weight -function fishing_pick() -> int { +function fishing_pick(fishing_st: mut FishingState) -> int { let leg = fishing_legend() - if leg >= 0 and fishing_lure_on { + if leg >= 0 and fishing_st.fishing_lure_on { let k = FishSpecies[leg] var p = k.chance - if FishingWorld.afloat() or FishingWorld.depth(fishing_bx, fishing_bz) >= fishing_deep { p = k.chance_deep } - if fishing_rand() < p + FishingWorld.skill() { return leg } + if FishingWorld.afloat() or FishingWorld.depth(fishing_st.fishing_bx, fishing_st.fishing_bz) >= fishing_st.fishing_deep { p = k.chance_deep } + if fishing_rand(fishing_st) < p + FishingWorld.skill() { return leg } } var total = 0.0 for s in 0 .. FISH_COUNT { if not FishSpecies[s].legend { total = total + FishSpecies[s].weight } } - var r = fishing_rand() * total + var r = fishing_rand(fishing_st) * total var last = -1 for s in 0 .. FISH_COUNT { if FishSpecies[s].legend { continue } @@ -21,37 +21,37 @@ function fishing_pick() -> int { return last } -function fishing_size(s: int) -> int { +function fishing_size(fishing_st: mut FishingState, s: int) -> int { let k = FishSpecies[s] - let spread = int(fishing_rand() * float(k.cm_spread + 1)) + let spread = int(fishing_rand(fishing_st) * float(k.cm_spread + 1)) return k.cm_min + spread + int(FishingWorld.skill() * k.skill_cm) } # the green's half width this crossing: a fish that closes it does so at every rung, and Hard # closes it for everything else -export function fishing_window() -> float { - if not fishing_kind_ok(fishing_sp) { return 0.12 } - let k = FishSpecies[fishing_sp] - let hw = FishingWorld.window(fishing_sp) - if k.closes > 0.0 { return Math.max(hw * (1.0 - k.closes * float(fishing_won)), hw * k.floor) } - if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_won)), hw * 0.55) } +export function fishing_window(fishing_st: FishingState) -> float { + if not fishing_kind_ok(fishing_st.fishing_sp) { return 0.12 } + let k = FishSpecies[fishing_st.fishing_sp] + let hw = FishingWorld.window(fishing_st.fishing_sp) + if k.closes > 0.0 { return Math.max(hw * (1.0 - k.closes * float(fishing_st.fishing_won)), hw * k.floor) } + if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_st.fishing_won)), hw * 0.55) } return hw } -export function fishing_window_lo() -> float { return Math.clamp(fishing_win_c - fishing_window(), 0.0, 1.0) } -export function fishing_window_hi() -> float { return Math.clamp(fishing_win_c + fishing_window(), 0.0, 1.0) } +export function fishing_window_lo(fishing_st: FishingState) -> float { return Math.clamp(fishing_st.fishing_win_c - fishing_window(fishing_st), 0.0, 1.0) } +export function fishing_window_hi(fishing_st: FishingState) -> float { return Math.clamp(fishing_st.fishing_win_c + fishing_window(fishing_st), 0.0, 1.0) } # where it sits: the middle on Relaxed, somewhere new each crossing above it -function fishing_new_window() -> void { +function fishing_new_window(fishing_st: mut FishingState) -> void { let lv = FishingWorld.level() - if lv == FISHING_RELAXED or fishing_gentle_on { - fishing_win_c = 0.5 + if lv == FISHING_RELAXED or fishing_st.fishing_gentle_on { + fishing_st.fishing_win_c = 0.5 return } - let hw = fishing_window() + 0.05 - fishing_win_c = hw + fishing_rand() * (1.0 - hw - hw) + let hw = fishing_window(fishing_st) + 0.05 + fishing_st.fishing_win_c = hw + fishing_rand(fishing_st) * (1.0 - hw - hw) } -function fishing_in_window() -> bool { - return fishing_mark > fishing_win_c - fishing_window() and fishing_mark < fishing_win_c + fishing_window() +function fishing_in_window(fishing_st: FishingState) -> bool { + return fishing_st.fishing_mark > fishing_st.fishing_win_c - fishing_window(fishing_st) and fishing_st.fishing_mark < fishing_st.fishing_win_c + fishing_window(fishing_st) } diff --git a/packages/ludic.fishing/run.ludic b/packages/ludic.fishing/run.ludic index 1325e024..64db8cd6 100644 --- a/packages/ludic.fishing/run.ludic +++ b/packages/ludic.fishing/run.ludic @@ -1,64 +1,64 @@ # run.ludic - `dt` real seconds of a line in the water: the wait, the bite, and the fight -function fishing_run_wait() -> void { - if fishing_t >= 0.0 { return } - fishing_st = FISHING_BITE_ON - fishing_t = 1.6 - fishing_sp = fishing_pick() - fishing_say(FISHING_BITE) +function fishing_run_wait(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + if fishing_st.fishing_t >= 0.0 { return } + fishing_st.fishing_st = FISHING_BITE_ON + fishing_st.fishing_t = 1.6 + fishing_st.fishing_sp = fishing_pick(fishing_st) + fishing_say(base_st, fishing_st, FISHING_BITE) } # a fish that runs: the marker is all over the bar and nothing done with it helps -function fishing_run_runs(dt: float) -> float { - let k = FishSpecies[fishing_sp] - if fishing_run_t > 0.0 { - fishing_run_t = fishing_run_t - dt - if fishing_run_t <= 0.0 { - fishing_run_t = 0.0 - fishing_say(FISHING_TIRED) +function fishing_run_runs(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> float { + let k = FishSpecies[fishing_st.fishing_sp] + if fishing_st.fishing_run_t > 0.0 { + fishing_st.fishing_run_t = fishing_st.fishing_run_t - dt + if fishing_st.fishing_run_t <= 0.0 { + fishing_st.fishing_run_t = 0.0 + fishing_say(base_st, fishing_st, FISHING_TIRED) } return 4.2 } - if fishing_run_left > 0 { - fishing_run_next = fishing_run_next - dt - if fishing_run_next <= 0.0 { - fishing_run_left -= 1 - fishing_run_t = 2.2 - fishing_run_next = 4.0 + fishing_rand() * 4.0 - fishing_say(FISHING_RUNS) + if fishing_st.fishing_run_left > 0 { + fishing_st.fishing_run_next = fishing_st.fishing_run_next - dt + if fishing_st.fishing_run_next <= 0.0 { + fishing_st.fishing_run_left -= 1 + fishing_st.fishing_run_t = 2.2 + fishing_st.fishing_run_next = 4.0 + fishing_rand(fishing_st) * 4.0 + fishing_say(base_st, fishing_st, FISHING_RUNS) } } - return k.speed + k.speed_per_hit * float(fishing_won) + return k.speed + k.speed_per_hit * float(fishing_st.fishing_won) } -function fishing_run_reel(dt: float) -> void { - let sp = fishing_run_runs(dt) - if fishing_gentle_on { - fishing_ring_t = fishing_ring_t - dt - if fishing_ring_t <= 0.0 { fishing_lost(FISHING_LINE_SNAPPED) } +function fishing_run_reel(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> void { + let sp = fishing_run_runs(base_st, fishing_st, dt) + if fishing_st.fishing_gentle_on { + fishing_st.fishing_ring_t = fishing_st.fishing_ring_t - dt + if fishing_st.fishing_ring_t <= 0.0 { fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED) } return } - fishing_mark = fishing_mark + sp * fishing_dir * dt - if fishing_mark > 1.0 { - fishing_mark = 1.0 - fishing_dir = -1.0 + fishing_st.fishing_mark = fishing_st.fishing_mark + sp * fishing_st.fishing_dir * dt + if fishing_st.fishing_mark > 1.0 { + fishing_st.fishing_mark = 1.0 + fishing_st.fishing_dir = -1.0 } - if fishing_mark < 0.0 { - fishing_mark = 0.0 - fishing_dir = 1.0 + if fishing_st.fishing_mark < 0.0 { + fishing_st.fishing_mark = 0.0 + fishing_st.fishing_dir = 1.0 } - if fishing_t < 0.0 { fishing_lost(FISHING_SLACK) } + if fishing_st.fishing_t < 0.0 { fishing_lost(base_st, fishing_st, FISHING_SLACK) } } -export function fishing_run(dt: float) -> void { - if fishing_st == FISHING_OFF { return } - fishing_t = fishing_t - dt - if fishing_st == FISHING_WAIT { - fishing_run_wait() +export function fishing_run(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> void { + if fishing_st.fishing_st == FISHING_OFF { return } + fishing_st.fishing_t = fishing_st.fishing_t - dt + if fishing_st.fishing_st == FISHING_WAIT { + fishing_run_wait(base_st, fishing_st) return } - if fishing_st == FISHING_BITE_ON { - if fishing_t < 0.0 { fishing_lost(FISHING_MISSED) } + if fishing_st.fishing_st == FISHING_BITE_ON { + if fishing_st.fishing_t < 0.0 { fishing_lost(base_st, fishing_st, FISHING_MISSED) } return } - fishing_run_reel(dt) + fishing_run_reel(base_st, fishing_st, dt) } diff --git a/packages/ludic.fishing/state.ludic b/packages/ludic.fishing/state.ludic index 0a6eac12..ce400488 100644 --- a/packages/ludic.fishing/state.ludic +++ b/packages/ludic.fishing/state.ludic @@ -4,86 +4,88 @@ export const FISHING_WAIT: int = 1 export const FISHING_BITE_ON: int = 2 export const FISHING_REEL: int = 3 -var fishing_st: int = 0 -var fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight -var fishing_bx: float = 0.0 # where the line is in the water -var fishing_bz: float = 0.0 -var fishing_sp: int = -1 # what is on it -var fishing_len: int = 0 # cm -var fishing_bait_on: bool = false -var fishing_lure_on: bool = false -var fishing_mark: float = 0.0 # 0..1 across the bar -var fishing_dir: float = 1.0 -var fishing_win_c: float = 0.5 # where the green sits this crossing -var fishing_ring_t: float = 0.0 # Gentle: seconds left to press -var fishing_gentle_on: bool = false -var fishing_won: int = 0 # crossings won -var fishing_need_n: int = 3 -var fishing_run_t: float = 0.0 # seconds left of a run -var fishing_run_left: int = 0 -var fishing_run_next: float = 0.0 -var fishing_dice: Rng = null -var fishing_fact_q: Queue = null -var fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed -var fishing_big: int = 0 -var fishing_leg: bool = false +export state FishingState { + fishing_st: int = 0 + fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight + fishing_bx: float = 0.0 # where the line is in the water + fishing_bz: float = 0.0 + fishing_sp: int = -1 # what is on it + fishing_len: int = 0 # cm + fishing_bait_on: bool = false + fishing_lure_on: bool = false + fishing_mark: float = 0.0 # 0..1 across the bar + fishing_dir: float = 1.0 + fishing_win_c: float = 0.5 # where the green sits this crossing + fishing_ring_t: float = 0.0 # Gentle: seconds left to press + fishing_gentle_on: bool = false + fishing_won: int = 0 # crossings won + fishing_need_n: int = 3 + fishing_run_t: float = 0.0 # seconds left of a run + fishing_run_left: int = 0 + fishing_run_next: float = 0.0 + fishing_dice: Rng = null + fishing_fact_q: Queue = null + fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed + fishing_big: int = 0 + fishing_leg: bool = false + fishing_min_depth: float = 0.15 # a cast needs this much water + fishing_deep: float = 2.5 # a legend's deep-water chance from here down + fishing_seed_n: int = 4242 +} -var fishing_min_depth: float = 0.15 # a cast needs this much water -var fishing_deep: float = 2.5 # a legend's deep-water chance from here down -var fishing_seed_n: int = 4242 # a cast needs `min_depth` metres; `deep` is where a legend is likelier; the line's own dice -export function fishing_config(min_depth: float, deep: float, seed: int) -> void { - fishing_min_depth = min_depth - fishing_deep = deep - fishing_seed_n = seed - fishing_dice = rng_new(seed) +export function fishing_config(fishing_st: mut FishingState, min_depth: float, deep: float, seed: int) -> void { + fishing_st.fishing_min_depth = min_depth + fishing_st.fishing_deep = deep + fishing_st.fishing_seed_n = seed + fishing_st.fishing_dice = rng_new(seed) } -export function fishing_facts() -> Queue { - if fishing_fact_q == null { fishing_fact_q = queue_new("fishing.facts") } - return fishing_fact_q +export function fishing_facts(base_st: mut BaseState, fishing_st: mut FishingState) -> Queue { + if fishing_st.fishing_fact_q == null { fishing_st.fishing_fact_q = queue_new(base_st, "fishing.facts") } + return fishing_st.fishing_fact_q } -function fishing_rand() -> float { - if fishing_dice == null { fishing_dice = rng_new(fishing_seed_n) } - return rng_float(fishing_dice) +function fishing_rand(fishing_st: mut FishingState) -> float { + if fishing_st.fishing_dice == null { fishing_st.fishing_dice = rng_new(fishing_st.fishing_seed_n) } + return rng_float(fishing_st.fishing_dice) } -function fishing_say(what: int) -> void { +function fishing_say(base_st: mut BaseState, fishing_st: mut FishingState, what: int) -> void { let f = new FishingFact f.what = what - f.species = fishing_sp - f.cm = fishing_len - f.kg = fishing_weight_of(fishing_sp, fishing_len) - f.hits = fishing_won - f.need = fishing_need_n - f.bait = fishing_bait_on - f.lure = fishing_lure_on - f.x = fishing_bx - f.z = fishing_bz - q_push(fishing_facts(), f) + f.species = fishing_st.fishing_sp + f.cm = fishing_st.fishing_len + f.kg = fishing_weight_of(fishing_st.fishing_sp, fishing_st.fishing_len) + f.hits = fishing_st.fishing_won + f.need = fishing_st.fishing_need_n + f.bait = fishing_st.fishing_bait_on + f.lure = fishing_st.fishing_lure_on + f.x = fishing_st.fishing_bx + f.z = fishing_st.fishing_bz + q_push(base_st, fishing_facts(base_st, fishing_st), f) } -export function fishing_state() -> int { return fishing_st } -export function fishing_active() -> bool { return fishing_st != FISHING_OFF } -export function fishing_species() -> int { return fishing_sp } -export function fishing_cm() -> int { return fishing_len } -export function fishing_line_x() -> float { return fishing_bx } -export function fishing_line_z() -> float { return fishing_bz } -export function fishing_marker() -> float { return fishing_mark } -export function fishing_hits() -> int { return fishing_won } -export function fishing_need() -> int { return fishing_need_n } -export function fishing_running() -> bool { return fishing_run_t > 0.0 } -export function fishing_gentle() -> bool { return fishing_gentle_on } -export function fishing_ring() -> float { return fishing_ring_t } -export function fishing_baited() -> bool { return fishing_bait_on } -export function fishing_lured() -> bool { return fishing_lure_on } -export function fishing_caught() -> int { return fishing_n } -export function fishing_biggest() -> int { return fishing_big } -export function fishing_legend_caught() -> bool { return fishing_leg } +export function fishing_state(fishing_st: FishingState) -> int { return fishing_st.fishing_st } +export function fishing_active(fishing_st: FishingState) -> bool { return fishing_st.fishing_st != FISHING_OFF } +export function fishing_species(fishing_st: FishingState) -> int { return fishing_st.fishing_sp } +export function fishing_cm(fishing_st: FishingState) -> int { return fishing_st.fishing_len } +export function fishing_line_x(fishing_st: FishingState) -> float { return fishing_st.fishing_bx } +export function fishing_line_z(fishing_st: FishingState) -> float { return fishing_st.fishing_bz } +export function fishing_marker(fishing_st: FishingState) -> float { return fishing_st.fishing_mark } +export function fishing_hits(fishing_st: FishingState) -> int { return fishing_st.fishing_won } +export function fishing_need(fishing_st: FishingState) -> int { return fishing_st.fishing_need_n } +export function fishing_running(fishing_st: FishingState) -> bool { return fishing_st.fishing_run_t > 0.0 } +export function fishing_gentle(fishing_st: FishingState) -> bool { return fishing_st.fishing_gentle_on } +export function fishing_ring(fishing_st: FishingState) -> float { return fishing_st.fishing_ring_t } +export function fishing_baited(fishing_st: FishingState) -> bool { return fishing_st.fishing_bait_on } +export function fishing_lured(fishing_st: FishingState) -> bool { return fishing_st.fishing_lure_on } +export function fishing_caught(fishing_st: FishingState) -> int { return fishing_st.fishing_n } +export function fishing_biggest(fishing_st: FishingState) -> int { return fishing_st.fishing_big } +export function fishing_legend_caught(fishing_st: FishingState) -> bool { return fishing_st.fishing_leg } # can a line land at (x, z)? -export function fishing_water_at(x: float, z: float) -> bool { - return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_min_depth +export function fishing_water_at(fishing_st: FishingState, x: float, z: float) -> bool { + return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_st.fishing_min_depth } diff --git a/packages/ludic.fishing/system.ludic b/packages/ludic.fishing/system.ludic index e8f48db9..1b575bf7 100644 --- a/packages/ludic.fishing/system.ludic +++ b/packages/ludic.fishing/system.ludic @@ -1,35 +1,35 @@ # system.ludic - fishing as a system: its tick, and the record in its own save section (a line in # the water is never saved) -export function fishing_reset() -> void { - fishing_stop() - fishing_n = 0 - fishing_big = 0 - fishing_leg = false - fishing_dice = rng_new(fishing_seed_n) - q_clear(fishing_facts()) +export function fishing_reset(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + fishing_stop(fishing_st) + fishing_st.fishing_n = 0 + fishing_st.fishing_big = 0 + fishing_st.fishing_leg = false + fishing_st.fishing_dice = rng_new(fishing_st.fishing_seed_n) + q_clear(base_st, fishing_facts(base_st, fishing_st)) } # the record outright (staging, a test, the network's word) -export function fishing_record_set(caught: int, biggest: int, legend: bool) -> void { - fishing_n = caught - fishing_big = biggest - fishing_leg = legend +export function fishing_record_set(fishing_st: mut FishingState, caught: int, biggest: int, legend: bool) -> void { + fishing_st.fishing_n = caught + fishing_st.fishing_big = biggest + fishing_st.fishing_leg = legend } -export function fishing_save() -> Val { +export function fishing_save(fishing_st: FishingState) -> Val { let v = Value.object() - sv_put_int(v, "caught", fishing_n) - sv_put_int(v, "biggest", fishing_big) - sv_put_bool(v, "legend", fishing_leg) + sv_put_int(v, "caught", fishing_st.fishing_n) + sv_put_int(v, "biggest", fishing_st.fishing_big) + sv_put_bool(v, "legend", fishing_st.fishing_leg) return v } -export function fishing_load(v: Val, version: int) -> void { - fishing_reset() - fishing_record_set(sv_int(v, "caught", 0), sv_int(v, "biggest", 0), sv_bool(v, "legend", false)) +export function fishing_load(base_st: mut BaseState, fishing_st: mut FishingState, v: Val, version: int) -> void { + fishing_reset(base_st, fishing_st) + fishing_record_set(fishing_st, sv_int(v, "caught", 0), sv_int(v, "biggest", 0), sv_bool(v, "legend", false)) } -function fishing_sys_tick(t: Tick) -> void { fishing_run(t.dt) } +function fishing_sys_tick(base_st: mut BaseState, fishing_st: mut FishingState, t: Tick) -> void { fishing_run(base_st, fishing_st, t.dt) } export function fishing_system() -> System { let s = system_new("fishing", PH_SIMULATE) diff --git a/packages/ludic.fishing/tests/fishing_test.ludic b/packages/ludic.fishing/tests/fishing_test.ludic index 49c59611..2451733b 100644 --- a/packages/ludic.fishing/tests/fishing_test.ludic +++ b/packages/ludic.fishing/tests/fishing_test.ludic @@ -10,246 +10,248 @@ program FishingTest { def FishSpecies brook { weight: 0.25, cm_min: 20, cm_spread: 10 } def FishSpecies old_maroon { legend: true, cm_min: 70, cm_spread: 25, skill_cm: 0.0, chance: 1.0, chance_deep: 1.0, need: 5, runs: 2, fight: 30.0, speed: 1.3, speed_per_hit: 0.22, closes: 0.12 } - var bait: bool = false - var lure: bool = false - var boat: bool = false - var level: int = 0 - var skill: float = 0.0 + state FishingtestState { + bait: bool = false + lure: bool = false + boat: bool = false + level: int = 0 + skill: float = 0.0 + } # water past x = 10, deepening a metre every ten function depth(x: float, z: float) -> float { return Math.max((x - 10.0) / 10.0, 0.0) } - function afloat() -> bool { return boat } - function has_bait() -> bool { return bait } - function has_lure() -> bool { return lure } - function fake_skill() -> float { return skill } - function fake_level() -> int { return level } + function afloat(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.boat } + function has_bait(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.bait } + function has_lure(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.lure } + function fake_skill(fishingtest_st: FishingtestState) -> float { return fishingtest_st.skill } + function fake_level(fishingtest_st: FishingtestState) -> int { return fishingtest_st.level } bind FishingWorld { depth: fn depth, afloat: fn afloat, has_bait: fn has_bait, has_lure: fn has_lure, skill: fn fake_skill, level: fn fake_level } - function fresh() -> void { - fishing_config(0.15, 2.5, 99) - fishing_reset() - bait = false - lure = false - boat = false - level = FISHING_RELAXED - skill = 0.0 + function fresh(base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) -> void { + fishing_config(fishing_st, 0.15, 2.5, 99) + fishing_reset(base_st, fishing_st) + fishingtest_st.bait = false + fishingtest_st.lure = false + fishingtest_st.boat = false + fishingtest_st.level = FISHING_RELAXED + fishingtest_st.skill = 0.0 } - function facts() -> []FishingFact { return q_drain(fishing_facts()) } + function facts(base_st: mut BaseState, fishing_st: mut FishingState) -> []FishingFact { return q_drain(base_st, fishing_facts(base_st, fishing_st)) } function saw(fs: []FishingFact, what: int) -> bool { for i in 0 .. len(fs) { if fs[i].what == what { return true } } return false } # run until it bites, then strike - function hook() -> void { - fishing_set_wait(0.0) - fishing_run(0.02) - fishing_press() + function hook(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + fishing_set_wait(fishing_st, 0.0) + fishing_run(base_st, fishing_st, 0.02) + fishing_press(base_st, fishing_st) } # hold the marker in the green and press, until it is landed or `n` presses are spent - function play(n: int) -> void { + function play(base_st: mut BaseState, fishing_st: mut FishingState, n: int) -> void { for i in 0 .. n { - if fishing_state() != FISHING_REEL { return } + if fishing_state(fishing_st) != FISHING_REEL { return } var guard = 0 - while (fishing_running() or fishing_marker() <= fishing_window_lo() or fishing_marker() >= fishing_window_hi()) and guard < 4000 { - fishing_run(0.005) + while (fishing_running(fishing_st) or fishing_marker(fishing_st) <= fishing_window_lo(fishing_st) or fishing_marker(fishing_st) >= fishing_window_hi(fishing_st)) and guard < 4000 { + fishing_run(base_st, fishing_st, 0.005) guard += 1 } - if fishing_state() == FISHING_REEL { fishing_press() } + if fishing_state(fishing_st) == FISHING_REEL { fishing_press(base_st, fishing_st) } } } - test "a cast lands only in water, and never twice" { - fresh() - expect_eq(fishing_cast(5.0, 0.0), FISHING_NOT_WATER) - expect(not fishing_active()) - expect_eq(fishing_cast(20.0, 0.0), FISHING_CAST_OK) - expect_eq(fishing_state(), FISHING_WAIT) - expect_eq(fishing_cast(20.0, 0.0), FISHING_BUSY) - boat = true - fishing_stop() - expect_eq(fishing_cast(0.0, 0.0), FISHING_CAST_OK) + test "a cast lands only in water, and never twice" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + expect_eq(fishing_cast(base_st, fishing_st, 5.0, 0.0), FISHING_NOT_WATER) + expect(not fishing_active(fishing_st)) + expect_eq(fishing_cast(base_st, fishing_st, 20.0, 0.0), FISHING_CAST_OK) + expect_eq(fishing_state(fishing_st), FISHING_WAIT) + expect_eq(fishing_cast(base_st, fishing_st, 20.0, 0.0), FISHING_BUSY) + fishingtest_st.boat = true + fishing_stop(fishing_st) + expect_eq(fishing_cast(base_st, fishing_st, 0.0, 0.0), FISHING_CAST_OK) } - test "bait is used and said, and halves the wait" { - fresh() - fishing_cast(20.0, 0.0) - expect(not saw(facts(), FISHING_BAIT_USED)) + test "bait is used and said, and halves the wait" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + expect(not saw(facts(base_st, fishing_st), FISHING_BAIT_USED)) var plain = 0 - while fishing_state() == FISHING_WAIT { - fishing_run(0.1) + while fishing_state(fishing_st) == FISHING_WAIT { + fishing_run(base_st, fishing_st, 0.1) plain += 1 } expect(plain <= 201) - fresh() - bait = true - fishing_cast(20.0, 0.0) - let fs = facts() + fresh(base_st, fishing_st, fishingtest_st) + fishingtest_st.bait = true + fishing_cast(base_st, fishing_st, 20.0, 0.0) + let fs = facts(base_st, fishing_st) expect(saw(fs, FISHING_BAIT_USED)) expect(fs[0].bait) var baited = 0 - while fishing_state() == FISHING_WAIT { - fishing_run(0.1) + while fishing_state(fishing_st) == FISHING_WAIT { + fishing_run(base_st, fishing_st, 0.1) baited += 1 } expect(baited <= 101) } - test "a bite missed is gone, a bite struck is hooked" { - fresh() - fishing_cast(20.0, 0.0) - fishing_set_wait(0.0) - fishing_run(0.02) - expect_eq(fishing_state(), FISHING_BITE_ON) - expect(fishing_species() >= 0) - fishing_run(2.0) - expect(not fishing_active()) - expect(saw(facts(), FISHING_MISSED)) - fishing_cast(20.0, 0.0) - hook() - expect_eq(fishing_state(), FISHING_REEL) - expect(saw(facts(), FISHING_HOOKED)) + test "a bite missed is gone, a bite struck is hooked" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + fishing_set_wait(fishing_st, 0.0) + fishing_run(base_st, fishing_st, 0.02) + expect_eq(fishing_state(fishing_st), FISHING_BITE_ON) + expect(fishing_species(fishing_st) >= 0) + fishing_run(base_st, fishing_st, 2.0) + expect(not fishing_active(fishing_st)) + expect(saw(facts(base_st, fishing_st), FISHING_MISSED)) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + expect_eq(fishing_state(fishing_st), FISHING_REEL) + expect(saw(facts(base_st, fishing_st), FISHING_HOOKED)) } - test "everyday fish only without a lure, sized and weighed from the data" { - fresh() + test "everyday fish only without a lure, sized and weighed from the data" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) for i in 0 .. 40 { - fishing_cast(20.0, 0.0) - hook() - let s = fishing_species() + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + let s = fishing_species(fishing_st) expect(s != FISH_OLD_MAROON) let k = FishSpecies[s] - expect(fishing_cm() >= k.cm_min and fishing_cm() <= k.cm_min + k.cm_spread) - fishing_stop() + expect(fishing_cm(fishing_st) >= k.cm_min and fishing_cm(fishing_st) <= k.cm_min + k.cm_spread) + fishing_stop(fishing_st) } expect(fishing_weight_of(FISH_CUTTHROAT, 50) > fishing_weight_of(FISH_CUTTHROAT, 30)) } - test "reeled in the green three times, it is landed and counted" { - fresh() - fishing_cast(20.0, 0.0) - hook() - facts() - play(3) - let fs = facts() + test "reeled in the green three times, it is landed and counted" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + facts(base_st, fishing_st) + play(base_st, fishing_st, 3) + let fs = facts(base_st, fishing_st) expect(saw(fs, FISHING_CAUGHT)) - expect_eq(fishing_caught(), 1) - expect(fishing_biggest() > 0) - expect(not fishing_active()) + expect_eq(fishing_caught(fishing_st), 1) + expect(fishing_biggest(fishing_st) > 0) + expect(not fishing_active(fishing_st)) let last = fs[len(fs) - 1] expect(last.kg > 0.0) expect(last.x == 20.0) } - test "a press outside the green throws the hook" { - fresh() - fishing_cast(20.0, 0.0) - hook() - while fishing_marker() < fishing_window_hi() + 0.01 { fishing_run(0.005) } - fishing_press() - expect(saw(facts(), FISHING_LINE_SNAPPED)) - expect(not fishing_active()) + test "a press outside the green throws the hook" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + while fishing_marker(fishing_st) < fishing_window_hi(fishing_st) + 0.01 { fishing_run(base_st, fishing_st, 0.005) } + fishing_press(base_st, fishing_st) + expect(saw(facts(base_st, fishing_st), FISHING_LINE_SNAPPED)) + expect(not fishing_active(fishing_st)) } - test "a fight left alone goes slack" { - fresh() - fishing_cast(20.0, 0.0) - hook() - for i in 0 .. 700 { fishing_run(0.02) } - expect(saw(facts(), FISHING_SLACK)) - expect_eq(fishing_caught(), 0) + test "a fight left alone goes slack" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + for i in 0 .. 700 { fishing_run(base_st, fishing_st, 0.02) } + expect(saw(facts(base_st, fishing_st), FISHING_SLACK)) + expect_eq(fishing_caught(fishing_st), 0) } - test "a legend takes the lure, runs, turns, and is landed at five" { - fresh() - lure = true - fishing_cast(20.0, 0.0) - hook() - expect_eq(fishing_species(), FISH_OLD_MAROON) - expect_eq(fishing_need(), 5) - let w0 = fishing_window() - play(2) - expect(fishing_window() < w0) + test "a legend takes the lure, runs, turns, and is landed at five" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishingtest_st.lure = true + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + expect_eq(fishing_species(fishing_st), FISH_OLD_MAROON) + expect_eq(fishing_need(fishing_st), 5) + let w0 = fishing_window(fishing_st) + play(base_st, fishing_st, 2) + expect(fishing_window(fishing_st) < w0) for i in 0 .. 400 { - if not fishing_running() { fishing_run(0.02) } + if not fishing_running(fishing_st) { fishing_run(base_st, fishing_st, 0.02) } } - let fs = facts() + let fs = facts(base_st, fishing_st) expect(saw(fs, FISHING_RUNS)) - if fishing_running() { - let before = fishing_hits() - fishing_press() - expect_eq(fishing_hits(), before - 1) + if fishing_running(fishing_st) { + let before = fishing_hits(fishing_st) + fishing_press(base_st, fishing_st) + expect_eq(fishing_hits(fishing_st), before - 1) } - play(40) - expect(fishing_legend_caught()) - let last = facts() + play(base_st, fishing_st, 40) + expect(fishing_legend_caught(fishing_st)) + let last = facts(base_st, fishing_st) expect(last[len(last) - 1].lure) } - test "gentle: one press inside the ring, and the ring closes" { - fresh() - level = FISHING_GENTLE - fishing_cast(20.0, 0.0) - hook() - expect(fishing_gentle()) - expect_eq(fishing_need(), 1) - fishing_press() - expect_eq(fishing_caught(), 1) - fishing_cast(20.0, 0.0) - hook() - fishing_run(2.1) - expect(saw(facts(), FISHING_LINE_SNAPPED)) + test "gentle: one press inside the ring, and the ring closes" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishingtest_st.level = FISHING_GENTLE + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + expect(fishing_gentle(fishing_st)) + expect_eq(fishing_need(fishing_st), 1) + fishing_press(base_st, fishing_st) + expect_eq(fishing_caught(fishing_st), 1) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + fishing_run(base_st, fishing_st, 2.1) + expect(saw(facts(base_st, fishing_st), FISHING_LINE_SNAPPED)) } - test "a legend fights on gentle too" { - fresh() - level = FISHING_GENTLE - lure = true - fishing_cast(20.0, 0.0) - hook() - expect(not fishing_gentle()) - expect_eq(fishing_need(), 5) + test "a legend fights on gentle too" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishingtest_st.level = FISHING_GENTLE + fishingtest_st.lure = true + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + expect(not fishing_gentle(fishing_st)) + expect_eq(fishing_need(fishing_st), 5) } - test "hard closes the window for everyday fish, relaxed keeps it centred" { - fresh() - fishing_cast(20.0, 0.0) - hook() - expect(fishing_window_lo() + fishing_window_hi() > 0.999 and fishing_window_lo() + fishing_window_hi() < 1.001) - fishing_stop() - level = FISHING_HARD - fishing_cast(20.0, 0.0) - hook() - let w0 = fishing_window() - play(1) - expect(fishing_window() < w0) + test "hard closes the window for everyday fish, relaxed keeps it centred" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + expect(fishing_window_lo(fishing_st) + fishing_window_hi(fishing_st) > 0.999 and fishing_window_lo(fishing_st) + fishing_window_hi(fishing_st) < 1.001) + fishing_stop(fishing_st) + fishingtest_st.level = FISHING_HARD + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + let w0 = fishing_window(fishing_st) + play(base_st, fishing_st, 1) + expect(fishing_window(fishing_st) < w0) } - test "skill makes a fish bigger" { - fresh() - skill = 1.0 - fishing_cast(20.0, 0.0) - hook() - let k = FishSpecies[fishing_species()] - expect(fishing_cm() >= k.cm_min + 60) + test "skill makes a fish bigger" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishingtest_st.skill = 1.0 + fishing_cast(base_st, fishing_st, 20.0, 0.0) + hook(base_st, fishing_st) + let k = FishSpecies[fishing_species(fishing_st)] + expect(fishing_cm(fishing_st) >= k.cm_min + 60) } - test "the record is saved, and a line in the water is not" { - fresh() - fishing_record_set(7, 52, true) - fishing_cast(20.0, 0.0) + test "the record is saved, and a line in the water is not" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) { + fresh(base_st, fishing_st, fishingtest_st) + fishing_record_set(fishing_st, 7, 52, true) + fishing_cast(base_st, fishing_st, 20.0, 0.0) let root = save_tree() - save_section(root, "fishing", 1, fishing_save()) + save_section(root, "fishing", 1, fishing_save(fishing_st)) let text = save_encode(root) - fishing_reset() + fishing_reset(base_st, fishing_st) let n = load_section(save_decode(text), "fishing") - fishing_load(n.data, n.version) - expect_eq(fishing_caught(), 7) - expect_eq(fishing_biggest(), 52) - expect(fishing_legend_caught()) - expect(not fishing_active()) + fishing_load(base_st, fishing_st, n.data, n.version) + expect_eq(fishing_caught(fishing_st), 7) + expect_eq(fishing_biggest(fishing_st), 52) + expect(fishing_legend_caught(fishing_st)) + expect(not fishing_active(fishing_st)) } } diff --git a/packages/ludic.fishing/verbs.ludic b/packages/ludic.fishing/verbs.ludic index 9f1767c6..37525e27 100644 --- a/packages/ludic.fishing/verbs.ludic +++ b/packages/ludic.fishing/verbs.ludic @@ -4,102 +4,102 @@ export const FISHING_NOT_WATER: int = 1 export const FISHING_BUSY: int = 2 # a line into (x, z): bait halves the wait (and is a fact, for the game to take from the pack) -export function fishing_cast(x: float, z: float) -> int { - if fishing_st != FISHING_OFF { return FISHING_BUSY } - fishing_bx = x - fishing_bz = z - if not fishing_water_at(x, z) { return FISHING_NOT_WATER } - fishing_st = FISHING_WAIT - fishing_sp = -1 - fishing_len = 0 - fishing_lure_on = FishingWorld.has_lure() - fishing_bait_on = FishingWorld.has_bait() - var wait = 6.0 + fishing_rand() * 14.0 - if fishing_bait_on { wait = wait * 0.5 } - fishing_t = wait * FishingWorld.bite_mult() - if fishing_bait_on { fishing_say(FISHING_BAIT_USED) } - fishing_say(FISHING_CAST) +export function fishing_cast(base_st: mut BaseState, fishing_st: mut FishingState, x: float, z: float) -> int { + if fishing_st.fishing_st != FISHING_OFF { return FISHING_BUSY } + fishing_st.fishing_bx = x + fishing_st.fishing_bz = z + if not fishing_water_at(fishing_st, x, z) { return FISHING_NOT_WATER } + fishing_st.fishing_st = FISHING_WAIT + fishing_st.fishing_sp = -1 + fishing_st.fishing_len = 0 + fishing_st.fishing_lure_on = FishingWorld.has_lure() + fishing_st.fishing_bait_on = FishingWorld.has_bait() + var wait = 6.0 + fishing_rand(fishing_st) * 14.0 + if fishing_st.fishing_bait_on { wait = wait * 0.5 } + fishing_st.fishing_t = wait * FishingWorld.bite_mult() + if fishing_st.fishing_bait_on { fishing_say(base_st, fishing_st, FISHING_BAIT_USED) } + fishing_say(base_st, fishing_st, FISHING_CAST) return FISHING_CAST_OK } # the bite comes exactly `secs` after now (staging, footage) -export function fishing_set_wait(secs: float) -> void { - if fishing_st == FISHING_WAIT { fishing_t = secs } +export function fishing_set_wait(fishing_st: mut FishingState, secs: float) -> void { + if fishing_st.fishing_st == FISHING_WAIT { fishing_st.fishing_t = secs } } -export function fishing_stop() -> void { - fishing_st = FISHING_OFF - fishing_run_t = 0.0 +export function fishing_stop(fishing_st: mut FishingState) -> void { + fishing_st.fishing_st = FISHING_OFF + fishing_st.fishing_run_t = 0.0 } -function fishing_strike() -> void { - let k = FishSpecies[fishing_sp] - fishing_st = FISHING_REEL - fishing_won = 0 - fishing_mark = 0.0 - fishing_dir = 1.0 - fishing_t = k.fight - fishing_need_n = k.need - fishing_run_t = 0.0 - fishing_run_left = k.runs - fishing_run_next = 3.0 + fishing_rand() * 3.0 - fishing_len = fishing_size(fishing_sp) - fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend - fishing_ring_t = 0.0 - if fishing_gentle_on { - fishing_need_n = 1 - fishing_ring_t = 2.0 +function fishing_strike(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + let k = FishSpecies[fishing_st.fishing_sp] + fishing_st.fishing_st = FISHING_REEL + fishing_st.fishing_won = 0 + fishing_st.fishing_mark = 0.0 + fishing_st.fishing_dir = 1.0 + fishing_st.fishing_t = k.fight + fishing_st.fishing_need_n = k.need + fishing_st.fishing_run_t = 0.0 + fishing_st.fishing_run_left = k.runs + fishing_st.fishing_run_next = 3.0 + fishing_rand(fishing_st) * 3.0 + fishing_st.fishing_len = fishing_size(fishing_st, fishing_st.fishing_sp) + fishing_st.fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend + fishing_st.fishing_ring_t = 0.0 + if fishing_st.fishing_gentle_on { + fishing_st.fishing_need_n = 1 + fishing_st.fishing_ring_t = 2.0 } - fishing_new_window() - fishing_say(FISHING_HOOKED) + fishing_new_window(fishing_st) + fishing_say(base_st, fishing_st, FISHING_HOOKED) } -function fishing_land() -> void { - fishing_n += 1 - if fishing_len > fishing_big { fishing_big = fishing_len } - if FishSpecies[fishing_sp].legend { fishing_leg = true } - fishing_say(FISHING_CAUGHT) - fishing_stop() +function fishing_land(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + fishing_st.fishing_n += 1 + if fishing_st.fishing_len > fishing_st.fishing_big { fishing_st.fishing_big = fishing_st.fishing_len } + if FishSpecies[fishing_st.fishing_sp].legend { fishing_st.fishing_leg = true } + fishing_say(base_st, fishing_st, FISHING_CAUGHT) + fishing_stop(fishing_st) } -function fishing_lost(what: int) -> void { - fishing_say(what) - fishing_stop() +function fishing_lost(base_st: mut BaseState, fishing_st: mut FishingState, what: int) -> void { + fishing_say(base_st, fishing_st, what) + fishing_stop(fishing_st) } -function fishing_reel_press() -> void { - if fishing_gentle_on { - if fishing_ring_t > 0.0 { fishing_land() } else { fishing_lost(FISHING_LINE_SNAPPED) } +function fishing_reel_press(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + if fishing_st.fishing_gentle_on { + if fishing_st.fishing_ring_t > 0.0 { fishing_land(base_st, fishing_st) } else { fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED) } return } - if fishing_run_t > 0.0 { - fishing_won = Math.max(fishing_won - 1, 0) - fishing_say(FISHING_CHECKED) + if fishing_st.fishing_run_t > 0.0 { + fishing_st.fishing_won = Math.max(fishing_st.fishing_won - 1, 0) + fishing_say(base_st, fishing_st, FISHING_CHECKED) return } - if fishing_in_window() { - fishing_won += 1 - fishing_new_window() - if fishing_won >= fishing_need_n { fishing_land() } else { fishing_say(FISHING_GOOD) } + if fishing_in_window(fishing_st) { + fishing_st.fishing_won += 1 + fishing_new_window(fishing_st) + if fishing_st.fishing_won >= fishing_st.fishing_need_n { fishing_land(base_st, fishing_st) } else { fishing_say(base_st, fishing_st, FISHING_GOOD) } return } - if FishSpecies[fishing_sp].legend and fishing_won > 0 { - fishing_won -= 1 - fishing_say(FISHING_TURNED) + if FishSpecies[fishing_st.fishing_sp].legend and fishing_st.fishing_won > 0 { + fishing_st.fishing_won -= 1 + fishing_say(base_st, fishing_st, FISHING_TURNED) return } - fishing_lost(FISHING_LINE_SNAPPED) + fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED) } # the button: waiting it reels in, on a bite it strikes, on a fish it reels -export function fishing_press() -> void { - if fishing_st == FISHING_WAIT { - fishing_lost(FISHING_REELED_IN) +export function fishing_press(base_st: mut BaseState, fishing_st: mut FishingState) -> void { + if fishing_st.fishing_st == FISHING_WAIT { + fishing_lost(base_st, fishing_st, FISHING_REELED_IN) return } - if fishing_st == FISHING_BITE_ON { - fishing_strike() + if fishing_st.fishing_st == FISHING_BITE_ON { + fishing_strike(base_st, fishing_st) return } - if fishing_st == FISHING_REEL { fishing_reel_press() } + if fishing_st.fishing_st == FISHING_REEL { fishing_reel_press(base_st, fishing_st) } } diff --git a/packages/ludic.gear/buy.ludic b/packages/ludic.gear/buy.ludic index de05c837..b4bf8bf6 100644 --- a/packages/ludic.gear/buy.ludic +++ b/packages/ludic.gear/buy.ludic @@ -1,25 +1,25 @@ # buy.ludic - a tier bought with money once the standing is there; the kind's item follows -export function gear_can_buy(k: int) -> bool { - if not gear_ok(k) or gear_best(k) or GearWorld.hidden(k) { return false } - return GearPurse.rep() >= gear_next_rep(k) and GearPurse.money() >= gear_next_cost(k) +export function gear_can_buy(gear_st: mut GearState, k: int) -> bool { + if not gear_ok(k) or gear_best(gear_st, k) or GearWorld.hidden(k) { return false } + return GearPurse.rep() >= gear_next_rep(gear_st, k) and GearPurse.money() >= gear_next_cost(gear_st, k) } -export function gear_buy(k: int) -> bool { - if not gear_can_buy(k) { return false } - let cost = gear_next_cost(k) +export function gear_buy(base_st: mut BaseState, gear_st: mut GearState, k: int) -> bool { + if not gear_can_buy(gear_st, k) { return false } + let cost = gear_next_cost(gear_st, k) if not GearPurse.spend(cost) { return false } - gear__lv[k] += 1 + gear_st.gear__lv[k] += 1 let it = GearKinds[k].item if it >= 0 and GearPack.count(it) == 0 { GearPack.add(it, 1) } - gear__fact(GEAR_BOUGHT, k, -1, cost) + gear__fact(base_st, gear_st, GEAR_BOUGHT, k, -1, cost) return true } # a kind whose item is already carried counts as its first tier -export function gear_sync() -> void { - gear__ensure() +export function gear_sync(gear_st: mut GearState) -> void { + gear__ensure(gear_st) for k in 0 .. GEAR_COUNT { let it = GearKinds[k].item - if it >= 0 and gear__lv[k] == 0 and GearPack.count(it) > 0 { gear__lv[k] = 1 } + if it >= 0 and gear_st.gear__lv[k] == 0 and GearPack.count(it) > 0 { gear_st.gear__lv[k] = 1 } } } diff --git a/packages/ludic.gear/hand.ludic b/packages/ludic.gear/hand.ludic index 739906c2..4eb0d1d5 100644 --- a/packages/ludic.gear/hand.ludic +++ b/packages/ludic.gear/hand.ludic @@ -1,12 +1,12 @@ # hand.ludic - what is held, and the charges: a held one runs only while it is in the hand, and # each burns down by the game hours while it runs -export function gear_hand() -> int { return gear__hand } +export function gear_hand(gear_st: GearState) -> int { return gear_st.gear__hand } # a new thing in the hand (-1 empty): whatever the old one was running goes out -export function gear_set_hand(it: int) -> void { - gear__ensure() - gear__hand = it - for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear__on[c] = false } } +export function gear_set_hand(gear_st: mut GearState, it: int) -> void { + gear__ensure(gear_st) + gear_st.gear__hand = it + for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear_st.gear__on[c] = false } } } # the charge an item carries, -1 none @@ -15,58 +15,58 @@ export function gear_charge_of(it: int) -> int { return -1 } -export function gear_charge(c: int) -> float { - gear__ensure() - return gear__left[c] +export function gear_charge(gear_st: mut GearState, c: int) -> float { + gear__ensure(gear_st) + return gear_st.gear__left[c] } -export function gear_charge_set(c: int, v: float) -> void { - gear__ensure() - gear__left[c] = Math.clamp(v, 0.0, GearCharges[c].full) +export function gear_charge_set(gear_st: mut GearState, c: int, v: float) -> void { + gear__ensure(gear_st) + gear_st.gear__left[c] = Math.clamp(v, 0.0, GearCharges[c].full) } -export function gear_charge_fill(c: int) -> void { gear_charge_set(c, GearCharges[c].full) } +export function gear_charge_fill(gear_st: mut GearState, c: int) -> void { gear_charge_set(gear_st, c, GearCharges[c].full) } -export function gear_running(c: int) -> bool { - gear__ensure() - return gear__on[c] +export function gear_running(gear_st: mut GearState, c: int) -> bool { + gear__ensure(gear_st) + return gear_st.gear__on[c] } # on or off; false when it has nothing left, or is held and not in the hand -export function gear_run(c: int, on: bool) -> bool { - gear__ensure() +export function gear_run(gear_st: mut GearState, c: int, on: bool) -> bool { + gear__ensure(gear_st) if not on { - gear__on[c] = false + gear_st.gear__on[c] = false return true } - if gear__left[c] < 1.0 { return false } - if GearCharges[c].hand and GearCharges[c].item != gear__hand { return false } - gear__on[c] = true + if gear_st.gear__left[c] < 1.0 { return false } + if GearCharges[c].hand and GearCharges[c].item != gear_st.gear__hand { return false } + gear_st.gear__on[c] = true return true } # the hand's own charge is running (a torch lit, a flashlight on) -export function gear_lit() -> bool { - if gear__hand < 0 { return false } - let c = gear_charge_of(gear__hand) - return c >= 0 and GearCharges[c].hand and gear_running(c) +export function gear_lit(gear_st: mut GearState) -> bool { + if gear_st.gear__hand < 0 { return false } + let c = gear_charge_of(gear_st.gear__hand) + return c >= 0 and GearCharges[c].hand and gear_running(gear_st, c) } -function gear__rate(c: int) -> float { +function gear__rate(gear_st: mut GearState, c: int) -> float { let d = GearCharges[c] - if d.rate >= 0 { return gear_value(d.rate) } + if d.rate >= 0 { return gear_value(gear_st, d.rate) } return d.per_hour } # the game hours of a tick burn every running charge; one that runs out stops and says so -function gear__burn(hours: float) -> void { - gear__ensure() +function gear__burn(base_st: mut BaseState, gear_st: mut GearState, hours: float) -> void { + gear__ensure(gear_st) for c in 0 .. CHARGE_COUNT { - if not gear__on[c] { continue } - gear__left[c] = gear__left[c] - hours * gear__rate(c) - if gear__left[c] > 0.0 { continue } - gear__left[c] = 0.0 - gear__on[c] = false - gear__fact(GEAR_RAN_OUT, -1, c, 0) + if not gear_st.gear__on[c] { continue } + gear_st.gear__left[c] = gear_st.gear__left[c] - hours * gear__rate(gear_st, c) + if gear_st.gear__left[c] > 0.0 { continue } + gear_st.gear__left[c] = 0.0 + gear_st.gear__on[c] = false + gear__fact(base_st, gear_st, GEAR_RAN_OUT, -1, c, 0) } } diff --git a/packages/ludic.gear/state.ludic b/packages/ludic.gear/state.ludic index e90e5cee..92ec709c 100644 --- a/packages/ludic.gear/state.ludic +++ b/packages/ludic.gear/state.ludic @@ -1,37 +1,39 @@ # state.ludic - the level of each kind, the hand, each charge's units and whether it runs, the facts -var gear__lv: []int = null -var gear__hand: int = -1 -var gear__left: []float = null -var gear__on: []bool = null -var gear__q: Queue = null +export state GearState { + gear__lv: []int = null + gear__hand: int = -1 + gear__left: []float = null + gear__on: []bool = null + gear__q: Queue = null +} -function gear__ensure() -> void { - if gear__lv != null { return } - gear__lv = new []int - for k in 0 .. GEAR_COUNT { push(gear__lv, GearKinds[k].start) } - gear__left = new []float - gear__on = new []bool +function gear__ensure(gear_st: mut GearState) -> void { + if gear_st.gear__lv != null { return } + gear_st.gear__lv = new []int + for k in 0 .. GEAR_COUNT { push(gear_st.gear__lv, GearKinds[k].start) } + gear_st.gear__left = new []float + gear_st.gear__on = new []bool for c in 0 .. CHARGE_COUNT { - push(gear__left, GearCharges[c].full) - push(gear__on, false) + push(gear_st.gear__left, GearCharges[c].full) + push(gear_st.gear__on, false) } } -export function gear_facts() -> Queue { - if gear__q == null { gear__q = queue_new("gear.facts") } - return gear__q +export function gear_facts(base_st: mut BaseState, gear_st: mut GearState) -> Queue { + if gear_st.gear__q == null { gear_st.gear__q = queue_new(base_st, "gear.facts") } + return gear_st.gear__q } -function gear__fact(what: int, k: int, c: int, cost: int) -> void { +function gear__fact(base_st: mut BaseState, gear_st: mut GearState, what: int, k: int, c: int, cost: int) -> void { let f = new GearFact f.what = what f.cost = cost f.kind = k f.charge = c if k >= 0 { - f.level = gear__lv[k] + f.level = gear_st.gear__lv[k] f.item = GearKinds[k].item } if c >= 0 { f.item = GearCharges[c].item } - q_push(gear_facts(), f) + q_push(base_st, gear_facts(base_st, gear_st), f) } diff --git a/packages/ludic.gear/system.ludic b/packages/ludic.gear/system.ludic index 0ffa2c4d..68f2bc42 100644 --- a/packages/ludic.gear/system.ludic +++ b/packages/ludic.gear/system.ludic @@ -1,43 +1,43 @@ # system.ludic - gear as a system: the burn each tick, and the levels and charges saved by key (the # hand is not saved: a trip opens with empty hands) -export function gear_reset() -> void { - gear__ensure() - for k in 0 .. GEAR_COUNT { gear__lv[k] = GearKinds[k].start } +export function gear_reset(base_st: mut BaseState, gear_st: mut GearState) -> void { + gear__ensure(gear_st) + for k in 0 .. GEAR_COUNT { gear_st.gear__lv[k] = GearKinds[k].start } for c in 0 .. CHARGE_COUNT { - gear__left[c] = GearCharges[c].full - gear__on[c] = false + gear_st.gear__left[c] = GearCharges[c].full + gear_st.gear__on[c] = false } - gear__hand = -1 - q_clear(gear_facts()) + gear_st.gear__hand = -1 + q_clear(base_st, gear_facts(base_st, gear_st)) } -export function gear_save() -> Val { - gear__ensure() +export function gear_save(gear_st: mut GearState) -> Val { + gear__ensure(gear_st) let v = Value.object() let lv = Value.object() - for k in 0 .. GEAR_COUNT { sv_put_int(lv, GearKinds[k].key, gear__lv[k]) } + for k in 0 .. GEAR_COUNT { sv_put_int(lv, GearKinds[k].key, gear_st.gear__lv[k]) } Value.put(v, "levels", lv) let ch = Value.object() - for c in 0 .. CHARGE_COUNT { sv_put_float(ch, GearCharges[c].key, gear__left[c]) } + for c in 0 .. CHARGE_COUNT { sv_put_float(ch, GearCharges[c].key, gear_st.gear__left[c]) } Value.put(v, "charges", ch) return v } # a kind the file does not name starts where a new trip does; then what the pack carries counts -export function gear_load(v: Val, version: int) -> void { - gear_reset() +export function gear_load(base_st: mut BaseState, gear_st: mut GearState, v: Val, version: int) -> void { + gear_reset(base_st, gear_st) if Value.has(v, "levels") != 0 { let lv = Value.get(v, "levels") - for k in 0 .. GEAR_COUNT { gear__lv[k] = sv_int(lv, GearKinds[k].key, GearKinds[k].start) } + for k in 0 .. GEAR_COUNT { gear_st.gear__lv[k] = sv_int(lv, GearKinds[k].key, GearKinds[k].start) } } if Value.has(v, "charges") != 0 { let ch = Value.get(v, "charges") - for c in 0 .. CHARGE_COUNT { gear_charge_set(c, sv_float(ch, GearCharges[c].key, GearCharges[c].full)) } + for c in 0 .. CHARGE_COUNT { gear_charge_set(gear_st, c, sv_float(ch, GearCharges[c].key, GearCharges[c].full)) } } - gear_sync() + gear_sync(gear_st) } -function gear__tick(t: Tick) -> void { gear__burn(t.hours) } +function gear__tick(base_st: mut BaseState, gear_st: mut GearState, t: Tick) -> void { gear__burn(base_st, gear_st, t.hours) } export function gear_system() -> System { let s = system_new("gear", PH_SIMULATE) diff --git a/packages/ludic.gear/tests/gear_test.ludic b/packages/ludic.gear/tests/gear_test.ludic index a8c07c45..14487798 100644 --- a/packages/ludic.gear/tests/gear_test.ludic +++ b/packages/ludic.gear/tests/gear_test.ludic @@ -17,147 +17,149 @@ program GearTest { def GearCharges lamp { item: IT_LAMP, full: 100.0, rate: GV_DRAIN } def GearCharges radio { item: 9, full: 100.0, per_hour: 5.0, hand: false } - var money: int = 0 - var rep: int = 0 - var pack: []int = null - var hide_pack: bool = false + state GeartestState { + money: int = 0 + rep: int = 0 + pack: []int = null + hide_pack: bool = false + } - function purse_money() -> int { return money } - function purse_spend(n: int) -> bool { - if n > money { return false } - money -= n + function purse_money(geartest_st: GeartestState) -> int { return geartest_st.money } + function purse_spend(geartest_st: mut GeartestState, n: int) -> bool { + if n > geartest_st.money { return false } + geartest_st.money -= n return true } - function purse_rep() -> int { return rep } - function pack_count(it: int) -> int { return pack[it] } - function pack_add(it: int, n: int) -> int { - pack[it] = pack[it] + n + function purse_rep(geartest_st: GeartestState) -> int { return geartest_st.rep } + function pack_count(geartest_st: GeartestState, it: int) -> int { return geartest_st.pack[it] } + function pack_add(geartest_st: mut GeartestState, it: int, n: int) -> int { + geartest_st.pack[it] = geartest_st.pack[it] + n return n } - function hidden(k: int) -> bool { return hide_pack and k == GEAR_PACK } + function hidden(geartest_st: GeartestState, k: int) -> bool { return geartest_st.hide_pack and k == GEAR_PACK } bind GearPurse { money: fn purse_money, spend: fn purse_spend, rep: fn purse_rep } bind GearPack { count: fn pack_count, add: fn pack_add } bind GearWorld { hidden: fn hidden } - function fresh() -> void { - money = 0 - rep = 0 - hide_pack = false - pack = new []int - for i in 0 .. 12 { push(pack, 0) } - gear_reset() + function fresh(base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) -> void { + geartest_st.money = 0 + geartest_st.rep = 0 + geartest_st.hide_pack = false + geartest_st.pack = new []int + for i in 0 .. 12 { push(geartest_st.pack, 0) } + gear_reset(base_st, gear_st) } - function ran_out() -> int { - let fs = q_drain(gear_facts()) + function ran_out(base_st: mut BaseState, gear_st: mut GearState) -> int { + let fs = q_drain(base_st, gear_facts(base_st, gear_st)) var n = -1 for i in 0 .. len(fs) { if fs[i].what == GEAR_RAN_OUT { n = fs[i].charge } } return n } - test "a new trip starts where the kinds say" { - fresh() - expect_eq(gear_level(GEAR_ROD), 0) - expect_eq(gear_level(GEAR_CAMERA), 1) - expect(gear_next_name(GEAR_CAMERA) == "Zoom") - expect_eq(gear_next_cost(GEAR_ROD), 30) + test "a new trip starts where the kinds say" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + expect_eq(gear_level(gear_st, GEAR_ROD), 0) + expect_eq(gear_level(gear_st, GEAR_CAMERA), 1) + expect(gear_next_name(gear_st, GEAR_CAMERA) == "Zoom") + expect_eq(gear_next_cost(gear_st, GEAR_ROD), 30) } - test "bought with money once the standing is there, and the item follows" { - fresh() - expect(not gear_buy(GEAR_ROD)) - money = 200 - expect(gear_buy(GEAR_ROD)) - expect_eq(money, 170) - expect_eq(pack[IT_ROD], 1) - expect(not gear_can_buy(GEAR_ROD)) - rep = 10 - expect(gear_buy(GEAR_ROD)) - expect_eq(pack[IT_ROD], 1) - let fs = q_drain(gear_facts()) + test "bought with money once the standing is there, and the item follows" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + expect(not gear_buy(base_st, gear_st, GEAR_ROD)) + geartest_st.money = 200 + expect(gear_buy(base_st, gear_st, GEAR_ROD)) + expect_eq(geartest_st.money, 170) + expect_eq(geartest_st.pack[IT_ROD], 1) + expect(not gear_can_buy(gear_st, GEAR_ROD)) + geartest_st.rep = 10 + expect(gear_buy(base_st, gear_st, GEAR_ROD)) + expect_eq(geartest_st.pack[IT_ROD], 1) + let fs = q_drain(base_st, gear_facts(base_st, gear_st)) expect_eq(len(fs), 2) expect_eq(fs[1].what, GEAR_BOUGHT) expect_eq(fs[1].level, 2) expect_eq(fs[1].cost, 100) } - test "the best tier is the end, and a hidden kind is not for sale" { - fresh() - money = 1000 - rep = 50 - for i in 0 .. 5 { gear_buy(GEAR_PACK) } - expect(gear_best(GEAR_PACK)) - expect_eq(gear_level(GEAR_PACK), 3) - fresh() - money = 100 - hide_pack = true - expect(not gear_buy(GEAR_PACK)) + test "the best tier is the end, and a hidden kind is not for sale" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + geartest_st.money = 1000 + geartest_st.rep = 50 + for i in 0 .. 5 { gear_buy(base_st, gear_st, GEAR_PACK) } + expect(gear_best(gear_st, GEAR_PACK)) + expect_eq(gear_level(gear_st, GEAR_PACK), 3) + fresh(base_st, gear_st, geartest_st) + geartest_st.money = 100 + geartest_st.hide_pack = true + expect(not gear_buy(base_st, gear_st, GEAR_PACK)) expect_eq(len(gear_kinds_at(-1)), 2) } - test "a tier decides a value and a mult, and the stack's room follows" { - fresh() - expect_eq(gear_value(GV_BITE), 1.0) - gear_set_level(GEAR_ROD, 3) - expect_eq(gear_value(GV_BITE), 0.55) - expect_eq(gear_mult(GEAR_PACK), 1.0) - gear_set_level(GEAR_PACK, 2) - expect_eq(gear_mult(GEAR_PACK), 1.7) - expect_eq(gear_stack_limit(10, gear_mult(GEAR_PACK)), 17) + test "a tier decides a value and a mult, and the stack's room follows" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + expect_eq(gear_value(gear_st, GV_BITE), 1.0) + gear_set_level(gear_st, GEAR_ROD, 3) + expect_eq(gear_value(gear_st, GV_BITE), 0.55) + expect_eq(gear_mult(gear_st, GEAR_PACK), 1.0) + gear_set_level(gear_st, GEAR_PACK, 2) + expect_eq(gear_mult(gear_st, GEAR_PACK), 1.7) + expect_eq(gear_stack_limit(10, gear_mult(gear_st, GEAR_PACK)), 17) expect_eq(gear_stack_limit(1, 3.0), 1) expect_eq(gear_stack_limit(4, 0.1), 1) - expect_eq(gear_mult(GEAR_ROD), 1.0) + expect_eq(gear_mult(gear_st, GEAR_ROD), 1.0) } - test "a carried item counts as the first tier" { - fresh() - pack[IT_ROD] = 1 - gear_sync() - expect_eq(gear_level(GEAR_ROD), 1) + test "a carried item counts as the first tier" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + geartest_st.pack[IT_ROD] = 1 + gear_sync(gear_st) + expect_eq(gear_level(gear_st, GEAR_ROD), 1) } - test "a held charge runs only in the hand, burns by the hour and runs out" { - fresh() + test "a held charge runs only in the hand, burns by the hour and runs out" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) let t = CHARGE_TORCH - expect(not gear_run(t, true)) - gear_set_hand(IT_TORCH) - expect(gear_run(t, true)) - expect(gear_lit()) + expect(not gear_run(gear_st, t, true)) + gear_set_hand(gear_st, IT_TORCH) + expect(gear_run(gear_st, t, true)) + expect(gear_lit(gear_st)) gear_system().tick(tick_new(0.1, 1, 0.5)) - expect_eq(gear_charge(t), 30.0) + expect_eq(gear_charge(gear_st, t), 30.0) gear_system().tick(tick_new(0.1, 2, 0.6)) - expect_eq(gear_charge(t), 0.0) - expect(not gear_lit()) - expect_eq(ran_out(), t) - expect(not gear_run(t, true)) + expect_eq(gear_charge(gear_st, t), 0.0) + expect(not gear_lit(gear_st)) + expect_eq(ran_out(base_st, gear_st), t) + expect(not gear_run(gear_st, t, true)) } - test "a charge's rate can be its tier's, and a new hand puts the light out" { - fresh() - gear_set_hand(IT_LAMP) - gear_run(CHARGE_LAMP, true) + test "a charge's rate can be its tier's, and a new hand puts the light out" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + gear_set_hand(gear_st, IT_LAMP) + gear_run(gear_st, CHARGE_LAMP, true) gear_system().tick(tick_new(0.1, 1, 1.0)) - expect_eq(gear_charge(CHARGE_LAMP), 86.0) - gear_set_level(GEAR_CAMERA, 2) + expect_eq(gear_charge(gear_st, CHARGE_LAMP), 86.0) + gear_set_level(gear_st, GEAR_CAMERA, 2) gear_system().tick(tick_new(0.1, 2, 1.0)) - expect_eq(gear_charge(CHARGE_LAMP), 78.0) - gear_run(CHARGE_RADIO, true) - gear_set_hand(-1) - expect(not gear_running(CHARGE_LAMP)) - expect(gear_running(CHARGE_RADIO)) + expect_eq(gear_charge(gear_st, CHARGE_LAMP), 78.0) + gear_run(gear_st, CHARGE_RADIO, true) + gear_set_hand(gear_st, -1) + expect(not gear_running(gear_st, CHARGE_LAMP)) + expect(gear_running(gear_st, CHARGE_RADIO)) } - test "levels and charges are saved by key" { - fresh() - gear_set_level(GEAR_ROD, 2) - gear_charge_set(CHARGE_TORCH, 12.5) - let v = gear_save() - fresh() - gear_load(v, 1) - expect_eq(gear_level(GEAR_ROD), 2) - expect_eq(gear_level(GEAR_CAMERA), 1) - expect_eq(gear_charge(CHARGE_TORCH), 12.5) - expect_eq(gear_charge(CHARGE_LAMP), 100.0) + test "levels and charges are saved by key" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) { + fresh(base_st, gear_st, geartest_st) + gear_set_level(gear_st, GEAR_ROD, 2) + gear_charge_set(gear_st, CHARGE_TORCH, 12.5) + let v = gear_save(gear_st) + fresh(base_st, gear_st, geartest_st) + gear_load(base_st, gear_st, v, 1) + expect_eq(gear_level(gear_st, GEAR_ROD), 2) + expect_eq(gear_level(gear_st, GEAR_CAMERA), 1) + expect_eq(gear_charge(gear_st, CHARGE_TORCH), 12.5) + expect_eq(gear_charge(gear_st, CHARGE_LAMP), 100.0) } } diff --git a/packages/ludic.gear/tiers.ludic b/packages/ludic.gear/tiers.ludic index 69a10adc..65d54861 100644 --- a/packages/ludic.gear/tiers.ludic +++ b/packages/ludic.gear/tiers.ludic @@ -1,20 +1,20 @@ # tiers.ludic - what is owned and what a tier is worth export function gear_ok(k: int) -> bool { return k >= 0 and k < GEAR_COUNT } -export function gear_level(k: int) -> int { - gear__ensure() - return gear__lv[k] +export function gear_level(gear_st: mut GearState, k: int) -> int { + gear__ensure(gear_st) + return gear_st.gear__lv[k] } # outright: staging, a test, a camera handed over with the trip -export function gear_set_level(k: int, lv: int) -> void { - gear__ensure() - gear__lv[k] = lv +export function gear_set_level(gear_st: mut GearState, k: int, lv: int) -> void { + gear__ensure(gear_st) + gear_st.gear__lv[k] = lv } -export function gear_owned(k: int) -> bool { return gear_level(k) > 0 } +export function gear_owned(gear_st: mut GearState, k: int) -> bool { return gear_level(gear_st, k) > 0 } -export function gear_best(k: int) -> bool { return gear_level(k) >= GearKinds[k].tiers } +export function gear_best(gear_st: mut GearState, k: int) -> bool { return gear_level(gear_st, k) >= GearKinds[k].tiers } # a tier's name, "" for none export function gear_level_name(k: int, lv: int) -> string { @@ -23,16 +23,16 @@ export function gear_level_name(k: int, lv: int) -> string { } # the tier on offer next: its name, cost and the standing it wants -export function gear_next_name(k: int) -> string { return gear_level_name(k, gear_level(k) + 1) } +export function gear_next_name(gear_st: mut GearState, k: int) -> string { return gear_level_name(k, gear_level(gear_st, k) + 1) } -export function gear_next_cost(k: int) -> int { - if gear_best(k) { return 0 } - return GearKinds[k].cost[gear_level(k)] +export function gear_next_cost(gear_st: mut GearState, k: int) -> int { + if gear_best(gear_st, k) { return 0 } + return GearKinds[k].cost[gear_level(gear_st, k)] } -export function gear_next_rep(k: int) -> int { - if gear_best(k) { return 0 } - return GearKinds[k].rep[gear_level(k)] +export function gear_next_rep(gear_st: mut GearState, k: int) -> int { + if gear_best(gear_st, k) { return 0 } + return GearKinds[k].rep[gear_level(gear_st, k)] } function gear__pick(xs: []float, lv: int, fallback: float) -> float { @@ -43,12 +43,12 @@ function gear__pick(xs: []float, lv: int, fallback: float) -> float { } # the kind's own number at its level (1.0 when it has none) -export function gear_mult(k: int) -> float { return gear__pick(GearKinds[k].mult, gear_level(k), 1.0) } +export function gear_mult(gear_st: mut GearState, k: int) -> float { return gear__pick(GearKinds[k].mult, gear_level(gear_st, k), 1.0) } # a value its kind's tier decides -export function gear_value(v: int) -> float { +export function gear_value(gear_st: mut GearState, v: int) -> float { let d = GearValues[v] - return gear__pick(d.at, gear_level(d.gear), 0.0) + return gear__pick(d.at, gear_level(gear_st, d.gear), 0.0) } # the room for a thing whose base stack is `base`, scaled: a single thing stays single, and a diff --git a/packages/ludic.hints/card.ludic b/packages/ludic.hints/card.ludic index 285729b0..2ae05535 100644 --- a/packages/ludic.hints/card.ludic +++ b/packages/ludic.hints/card.ludic @@ -1,45 +1,45 @@ # card.ludic - a card read off the rail: the key opens the picked chip and puts it down, the arrows # walk the rail with the card up, and "I know this" mutes the card being read -export function hints_card() -> int { return hn_card } +export function hints_card(hints_st: HintsState) -> int { return hints_st.hn_card } # the key: the picked card opened, or the open one put down -export function hints_key() -> void { - if hn_card >= 0 { - hints_close() +export function hints_key(base_st: mut BaseState, hints_st: mut HintsState) -> void { + if hints_st.hn_card >= 0 { + hints_close(base_st, hints_st) return } - hints_open(hn_pick) + hints_open(base_st, hints_st, hints_st.hn_pick) } # the k-th chip on the rail opened as a card -export function hints_open(k: int) -> void { - if k < 0 or k >= hn_n { return } - hn_pick = k - hn_card = hn_top[k] - hn_fact(HINTS_F_OPENED, hn_card) +export function hints_open(base_st: mut BaseState, hints_st: mut HintsState, k: int) -> void { + if k < 0 or k >= hints_st.hn_n { return } + hints_st.hn_pick = k + hints_st.hn_card = hints_st.hn_top[k] + hn_fact(base_st, hints_st, HINTS_F_OPENED, hints_st.hn_card) } -export function hints_close() -> void { - if hn_card < 0 { return } - let c = hn_card - hn_card = -1 - hn_fact(HINTS_F_CLOSED, c) +export function hints_close(base_st: mut BaseState, hints_st: mut HintsState) -> void { + if hints_st.hn_card < 0 { return } + let c = hints_st.hn_card + hints_st.hn_card = -1 + hn_fact(base_st, hints_st, HINTS_F_CLOSED, c) } # the arrows, with a card up: the next chip along (d = 1) or the one before, round the ends -export function hints_move(d: int) -> void { - if hn_card < 0 or hn_n <= 1 { return } - hints_open((hn_pick + hn_n + d) % hn_n) +export function hints_move(base_st: mut BaseState, hints_st: mut HintsState, d: int) -> void { + if hints_st.hn_card < 0 or hints_st.hn_n <= 1 { return } + hints_open(base_st, hints_st, (hints_st.hn_pick + hints_st.hn_n + d) % hints_st.hn_n) } # "I know this - do not show it again": the card off the rail until the mutes are given back -export function hints_mute_card() -> void { - if hn_card < 0 { return } - let c = hn_card - hints_mute(c, true) - hn_card = -1 - hn_t = 0.0 - hn_fact(HINTS_F_MUTED, c) +export function hints_mute_card(base_st: mut BaseState, hints_st: mut HintsState) -> void { + if hints_st.hn_card < 0 { return } + let c = hints_st.hn_card + hints_mute(hints_st, c, true) + hints_st.hn_card = -1 + hints_st.hn_t = 0.0 + hn_fact(base_st, hints_st, HINTS_F_MUTED, c) } # a step's words and its picture (n 1..3): the glyph's name, or the item (-1 when it is a glyph) diff --git a/packages/ludic.hints/mute.ludic b/packages/ludic.hints/mute.ludic index 7c221404..590c0722 100644 --- a/packages/ludic.hints/mute.ludic +++ b/packages/ludic.hints/mute.ludic @@ -1,62 +1,60 @@ # mute.ludic - the cards a player has said they know, kept BY KEY: a card added, removed or moved in # the registry leaves every other mute where it was, and a key this build does not know is carried -var hn_mute: []bool = null -var hn_foreign: []string = null # muted keys with no card in this build, written back as read -function hn_mute_ensure() -> void { - if hn_mute != null { return } - hn_mute = new []bool - for i in 0 .. HINT_COUNT { push(hn_mute, false) } - hn_foreign = new []string +function hn_mute_ensure(hints_st: mut HintsState) -> void { + if hints_st.hn_mute != null { return } + hints_st.hn_mute = new []bool + for i in 0 .. HINT_COUNT { push(hints_st.hn_mute, false) } + hints_st.hn_foreign = new []string } -export function hints_muted(id: int) -> bool { - hn_mute_ensure() +export function hints_muted(hints_st: mut HintsState, id: int) -> bool { + hn_mute_ensure(hints_st) if id < 0 or id >= HINT_COUNT { return false } - return hn_mute[id] + return hints_st.hn_mute[id] } -export function hints_mute(id: int, on: bool) -> void { - hn_mute_ensure() +export function hints_mute(hints_st: mut HintsState, id: int, on: bool) -> void { + hn_mute_ensure(hints_st) if id < 0 or id >= HINT_COUNT { return } - hn_mute[id] = on + hints_st.hn_mute[id] = on } -export function hints_mute_count() -> int { - hn_mute_ensure() +export function hints_mute_count(hints_st: mut HintsState) -> int { + hn_mute_ensure(hints_st) var n = 0 - for i in 0 .. HINT_COUNT { if hn_mute[i] { n += 1 } } + for i in 0 .. HINT_COUNT { if hints_st.hn_mute[i] { n += 1 } } return n } # every muted card given back, from the next pass -export function hints_unmute_all() -> void { - hn_mute_ensure() - for i in 0 .. HINT_COUNT { hn_mute[i] = false } - hn_foreign = new []string - hn_t = 0.0 - hn_fact(HINTS_F_UNMUTED, -1) +export function hints_unmute_all(base_st: mut BaseState, hints_st: mut HintsState) -> void { + hn_mute_ensure(hints_st) + for i in 0 .. HINT_COUNT { hints_st.hn_mute[i] = false } + hints_st.hn_foreign = new []string + hints_st.hn_t = 0.0 + hn_fact(base_st, hints_st, HINTS_F_UNMUTED, -1) } # the mutes as a list of keys, for wherever the game keeps the player's own choices -export function hints_mutes_save() -> Val { - hn_mute_ensure() +export function hints_mutes_save(hints_st: mut HintsState) -> Val { + hn_mute_ensure(hints_st) let l = Value.list() - for i in 0 .. HINT_COUNT { if hn_mute[i] { Value.add(l, Value.str(Hints[i].key)) } } - for i in 0 .. len(hn_foreign) { Value.add(l, Value.str(hn_foreign[i])) } + for i in 0 .. HINT_COUNT { if hints_st.hn_mute[i] { Value.add(l, Value.str(Hints[i].key)) } } + for i in 0 .. len(hints_st.hn_foreign) { Value.add(l, Value.str(hints_st.hn_foreign[i])) } return l } # a list of keys read back; anything that is not a list is no mutes -export function hints_mutes_load(l: Val) -> void { - hn_mute = null - hn_mute_ensure() +export function hints_mutes_load(hints_st: mut HintsState, l: Val) -> void { + hints_st.hn_mute = null + hn_mute_ensure(hints_st) if l == null or Value.kind(l) != 5 { return } for i in 0 .. Value.count(l) { let e = Value.at(l, i) if Value.kind(e) != 4 { continue } let k = Value.as_str(e) let id = hints_find(k) - if id >= 0 { hn_mute[id] = true } else { push(hn_foreign, k) } + if id >= 0 { hints_st.hn_mute[id] = true } else { push(hints_st.hn_foreign, k) } } } diff --git a/packages/ludic.hints/rail.ludic b/packages/ludic.hints/rail.ludic index 05460edb..4fef81c0 100644 --- a/packages/ludic.hints/rail.ludic +++ b/packages/ludic.hints/rail.ludic @@ -1,102 +1,106 @@ # rail.ludic - the rail: once a pass (a second by default) the most urgent true cards, up to a few, # muted ones left out; in a tie the registry's order decides -var hn_shown: int = 3 # how many the rail holds -var hn_every: float = 1.0 # seconds between passes -var hn_top: []int = null # the cards on the rail this pass -var hn_n: int = 0 -var hn_t: float = 0.0 # seconds to the next pass -var hn_pick: int = 0 # which chip the key would open -var hn_card: int = -1 # the card being read, or -1 -var hn_facts: Queue = null - -export function hints_facts() -> Queue { - if hn_facts == null { hn_facts = queue_new("hints.facts") } - return hn_facts +export state HintsState { + hn_mute: []bool = null + hn_foreign: []string = null # muted keys with no card in this build, written back as read + hn_shown: int = 3 # how many the rail holds + hn_every: float = 1.0 # seconds between passes + hn_top: []int = null # the cards on the rail this pass + hn_n: int = 0 + hn_t: float = 0.0 # seconds to the next pass + hn_pick: int = 0 # which chip the key would open + hn_card: int = -1 # the card being read, or -1 + hn_facts: Queue = null } -function hn_fact(what: int, card: int) -> void { +export function hints_facts(base_st: mut BaseState, hints_st: mut HintsState) -> Queue { + if hints_st.hn_facts == null { hints_st.hn_facts = queue_new(base_st, "hints.facts") } + return hints_st.hn_facts +} + +function hn_fact(base_st: mut BaseState, hints_st: mut HintsState, what: int, card: int) -> void { let f = new HintsFact f.what = what f.card = card - q_push(hints_facts(), f) + q_push(base_st, hints_facts(base_st, hints_st), f) } # how many the rail holds and how often it is sorted -export function hints_config(shown: int, every: float) -> void { - hn_shown = shown - hn_every = every - hn_top = null +export function hints_config(hints_st: mut HintsState, shown: int, every: float) -> void { + hints_st.hn_shown = shown + hints_st.hn_every = every + hints_st.hn_top = null } -function hn_ensure() -> void { - if hn_top == null or len(hn_top) != hn_shown { - hn_top = new []int - for i in 0 .. hn_shown { push(hn_top, -1) } +function hn_ensure(hints_st: mut HintsState) -> void { + if hints_st.hn_top == null or len(hints_st.hn_top) != hints_st.hn_shown { + hints_st.hn_top = new []int + for i in 0 .. hints_st.hn_shown { push(hints_st.hn_top, -1) } } - hn_mute_ensure() + hn_mute_ensure(hints_st) } # the next pass comes on the next tick rather than in a second -export function hints_refresh() -> void { hn_t = 0.0 } +export function hints_refresh(hints_st: mut HintsState) -> void { hints_st.hn_t = 0.0 } -export function hints_tick(dt: float) -> void { - hn_ensure() - hn_t = hn_t - dt - if hn_t > 0.0 { return } - hn_t = hn_every - hints_sort() +export function hints_tick(hints_st: mut HintsState, dt: float) -> void { + hn_ensure(hints_st) + hints_st.hn_t = hints_st.hn_t - dt + if hints_st.hn_t > 0.0 { return } + hints_st.hn_t = hints_st.hn_every + hints_sort(hints_st) } # one pass now: the rail re-sorted, the pick kept in range, and the card following its subject # along the rail - or put down when what it was about stopped being true -export function hints_sort() -> void { - hn_ensure() - hn_n = 0 +export function hints_sort(hints_st: mut HintsState) -> void { + hn_ensure(hints_st) + hints_st.hn_n = 0 let on = HintsWorld.showing() for u in 0 .. 4 { let want = HINTS_URGENT - u for id in 0 .. HINT_COUNT { - if not on or hn_n >= hn_shown { break } - if Hints[id].urgency != want or hints_muted(id) { continue } + if not on or hints_st.hn_n >= hints_st.hn_shown { break } + if Hints[id].urgency != want or hints_muted(hints_st, id) { continue } if not HintsWorld.true_now(id) { continue } - hn_top[hn_n] = id - hn_n += 1 + hints_st.hn_top[hints_st.hn_n] = id + hints_st.hn_n += 1 } } - if hn_pick >= hn_n { hn_pick = 0 } - if hn_card < 0 { return } - if not on or hints_muted(hn_card) or not HintsWorld.true_now(hn_card) { - hn_card = -1 + if hints_st.hn_pick >= hints_st.hn_n { hints_st.hn_pick = 0 } + if hints_st.hn_card < 0 { return } + if not on or hints_muted(hints_st, hints_st.hn_card) or not HintsWorld.true_now(hints_st.hn_card) { + hints_st.hn_card = -1 return } - for k in 0 .. hn_n { if hn_top[k] == hn_card { hn_pick = k } } + for k in 0 .. hints_st.hn_n { if hints_st.hn_top[k] == hints_st.hn_card { hints_st.hn_pick = k } } } # the cards on the rail, most urgent first (empty while the rail is not showing) -export function hints_rail() -> []int { - hn_ensure() +export function hints_rail(hints_st: mut HintsState) -> []int { + hn_ensure(hints_st) let out = new []int if not HintsWorld.showing() { return out } - for k in 0 .. hn_n { push(out, hn_top[k]) } + for k in 0 .. hints_st.hn_n { push(out, hints_st.hn_top[k]) } return out } -export function hints_count() -> int { return hn_n } +export function hints_count(hints_st: HintsState) -> int { return hints_st.hn_n } -export function hints_at(k: int) -> int { - if k < 0 or k >= hn_n { return -1 } - return hn_top[k] +export function hints_at(hints_st: HintsState, k: int) -> int { + if k < 0 or k >= hints_st.hn_n { return -1 } + return hints_st.hn_top[k] } -export function hints_pick() -> int { return hn_pick } +export function hints_pick(hints_st: HintsState) -> int { return hints_st.hn_pick } -export function hints_hot(k: int) -> bool { return k == hn_pick } +export function hints_hot(hints_st: HintsState, k: int) -> bool { return k == hints_st.hn_pick } -export function hints_reset() -> void { - hn_top = null - hn_n = 0 - hn_t = 0.0 - hn_pick = 0 - hn_card = -1 - q_clear(hints_facts()) +export function hints_reset(base_st: mut BaseState, hints_st: mut HintsState) -> void { + hints_st.hn_top = null + hints_st.hn_n = 0 + hints_st.hn_t = 0.0 + hints_st.hn_pick = 0 + hints_st.hn_card = -1 + q_clear(base_st, hints_facts(base_st, hints_st)) } diff --git a/packages/ludic.hints/system.ludic b/packages/ludic.hints/system.ludic index c054d713..e5175afb 100644 --- a/packages/ludic.hints/system.ludic +++ b/packages/ludic.hints/system.ludic @@ -1,6 +1,6 @@ # system.ludic - the rail as a system in PH_PRESENT: it only reads the world, and a trip saves # nothing of it (the mutes are the player's, kept wherever the game keeps its settings) -function hn_tick(t: Tick) -> void { hints_tick(t.dt) } +function hn_tick(hints_st: mut HintsState, t: Tick) -> void { hints_tick(hints_st, t.dt) } export function hints_system() -> System { let s = system_new("hints", PH_PRESENT) diff --git a/packages/ludic.hints/tests/hints_test.ludic b/packages/ludic.hints/tests/hints_test.ludic index ab95eded..74805f18 100644 --- a/packages/ludic.hints/tests/hints_test.ludic +++ b/packages/ludic.hints/tests/hints_test.ludic @@ -11,24 +11,26 @@ program HintsTest { def Hints night { urgency: HINTS_TIP, line: "Night soon" } def Hints from "cards.lres" - var on: []bool = null - var showing: bool = true - - function fake_true(i: int) -> bool { return on[i] } - function fake_showing() -> bool { return showing } - bind HintsWorld { true_now: fn fake_true, showing: fn fake_showing } - - function fresh() -> void { - on = new []bool - for i in 0 .. HINT_COUNT { push(on, false) } - showing = true - hints_config(3, 1.0) - hints_reset() - hints_mutes_load(Value.list()) + state HintstestState { + on: []bool = null + showing: bool = true } - function count(what: int) -> int { - let fs = q_drain(hints_facts()) + function fake_true(hintstest_st: HintstestState, i: int) -> bool { return hintstest_st.on[i] } + function fake_showing(hintstest_st: HintstestState) -> bool { return hintstest_st.showing } + bind HintsWorld { true_now: fn fake_true, showing: fn fake_showing } + + function fresh(base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) -> void { + hintstest_st.on = new []bool + for i in 0 .. HINT_COUNT { push(hintstest_st.on, false) } + hintstest_st.showing = true + hints_config(hints_st, 3, 1.0) + hints_reset(base_st, hints_st) + hints_mutes_load(hints_st, Value.list()) + } + + function count(base_st: mut BaseState, hints_st: mut HintsState, what: int) -> int { + let fs = q_drain(base_st, hints_facts(base_st, hints_st)) var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n @@ -44,119 +46,119 @@ program HintsTest { expect(hints_step_text(HINT_THIRSTY, 1) == "Drink at a shore") } - test "the most urgent true cards, capped, ties in registry order" { - fresh() - for i in 0 .. HINT_COUNT { on[i] = true } - hints_tick(1.0) - expect_eq(hints_count(), 3) - expect_eq(hints_at(0), HINT_PARCHED) - expect_eq(hints_at(1), HINT_BEAR) - expect_eq(hints_at(2), HINT_COLD) - on[HINT_PARCHED] = false - on[HINT_BEAR] = false - hints_tick(0.5) - expect_eq(hints_at(0), HINT_PARCHED) # not a second yet: the rail holds - hints_tick(0.6) - expect_eq(hints_at(0), HINT_COLD) - expect_eq(hints_at(1), HINT_THIRSTY) - expect_eq(hints_at(2), HINT_FIRE) + test "the most urgent true cards, capped, ties in registry order" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + for i in 0 .. HINT_COUNT { hintstest_st.on[i] = true } + hints_tick(hints_st, 1.0) + expect_eq(hints_count(hints_st), 3) + expect_eq(hints_at(hints_st, 0), HINT_PARCHED) + expect_eq(hints_at(hints_st, 1), HINT_BEAR) + expect_eq(hints_at(hints_st, 2), HINT_COLD) + hintstest_st.on[HINT_PARCHED] = false + hintstest_st.on[HINT_BEAR] = false + hints_tick(hints_st, 0.5) + expect_eq(hints_at(hints_st, 0), HINT_PARCHED) # not a second yet: the rail holds + hints_tick(hints_st, 0.6) + expect_eq(hints_at(hints_st, 0), HINT_COLD) + expect_eq(hints_at(hints_st, 1), HINT_THIRSTY) + expect_eq(hints_at(hints_st, 2), HINT_FIRE) } - test "nothing on the rail while it is not showing" { - fresh() - on[HINT_COLD] = true - showing = false - hints_sort() - expect_eq(len(hints_rail()), 0) - expect_eq(hints_count(), 0) - showing = true - hints_sort() - expect_eq(len(hints_rail()), 1) + test "nothing on the rail while it is not showing" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + hintstest_st.on[HINT_COLD] = true + hintstest_st.showing = false + hints_sort(hints_st) + expect_eq(len(hints_rail(hints_st)), 0) + expect_eq(hints_count(hints_st), 0) + hintstest_st.showing = true + hints_sort(hints_st) + expect_eq(len(hints_rail(hints_st)), 1) } - test "the key opens the picked chip, the arrows walk round, the key puts it down" { - fresh() - on[HINT_COLD] = true - on[HINT_THIRSTY] = true - on[HINT_RAIN] = true - hints_sort() - hints_key() - expect_eq(hints_card(), HINT_COLD) - hints_move(1) - expect_eq(hints_card(), HINT_THIRSTY) - expect(hints_hot(1)) - hints_move(-1) - hints_move(-1) - expect_eq(hints_card(), HINT_RAIN) - hints_key() - expect_eq(hints_card(), -1) - expect_eq(count(HINTS_F_OPENED), 4) + test "the key opens the picked chip, the arrows walk round, the key puts it down" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + hintstest_st.on[HINT_COLD] = true + hintstest_st.on[HINT_THIRSTY] = true + hintstest_st.on[HINT_RAIN] = true + hints_sort(hints_st) + hints_key(base_st, hints_st) + expect_eq(hints_card(hints_st), HINT_COLD) + hints_move(base_st, hints_st, 1) + expect_eq(hints_card(hints_st), HINT_THIRSTY) + expect(hints_hot(hints_st, 1)) + hints_move(base_st, hints_st, -1) + hints_move(base_st, hints_st, -1) + expect_eq(hints_card(hints_st), HINT_RAIN) + hints_key(base_st, hints_st) + expect_eq(hints_card(hints_st), -1) + expect_eq(count(base_st, hints_st, HINTS_F_OPENED), 4) } - test "the card follows its subject as the rail re-sorts, and goes when it is no longer true" { - fresh() - on[HINT_THIRSTY] = true - on[HINT_RAIN] = true - hints_sort() - hints_open(1) - expect_eq(hints_card(), HINT_RAIN) - on[HINT_COLD] = true - hints_sort() - expect_eq(hints_card(), HINT_RAIN) - expect_eq(hints_pick(), 2) - on[HINT_RAIN] = false - hints_sort() - expect_eq(hints_card(), -1) - expect_eq(count(HINTS_F_CLOSED), 0) # it went; nobody put it down + test "the card follows its subject as the rail re-sorts, and goes when it is no longer true" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + hintstest_st.on[HINT_THIRSTY] = true + hintstest_st.on[HINT_RAIN] = true + hints_sort(hints_st) + hints_open(base_st, hints_st, 1) + expect_eq(hints_card(hints_st), HINT_RAIN) + hintstest_st.on[HINT_COLD] = true + hints_sort(hints_st) + expect_eq(hints_card(hints_st), HINT_RAIN) + expect_eq(hints_pick(hints_st), 2) + hintstest_st.on[HINT_RAIN] = false + hints_sort(hints_st) + expect_eq(hints_card(hints_st), -1) + expect_eq(count(base_st, hints_st, HINTS_F_CLOSED), 0) # it went; nobody put it down } - test "I know this: muted off the rail, counted, given back" { - fresh() - on[HINT_COLD] = true - on[HINT_THIRSTY] = true - hints_sort() - hints_key() - hints_mute_card() - expect_eq(hints_card(), -1) - expect(hints_muted(HINT_COLD)) - expect_eq(hints_mute_count(), 1) - expect_eq(count(HINTS_F_MUTED), 1) - hints_tick(0.0) # the next pass comes at once - expect_eq(hints_at(0), HINT_THIRSTY) - expect_eq(hints_count(), 1) - hints_unmute_all() - hints_tick(0.0) - expect_eq(hints_at(0), HINT_COLD) - expect_eq(count(HINTS_F_UNMUTED), 1) + test "I know this: muted off the rail, counted, given back" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + hintstest_st.on[HINT_COLD] = true + hintstest_st.on[HINT_THIRSTY] = true + hints_sort(hints_st) + hints_key(base_st, hints_st) + hints_mute_card(base_st, hints_st) + expect_eq(hints_card(hints_st), -1) + expect(hints_muted(hints_st, HINT_COLD)) + expect_eq(hints_mute_count(hints_st), 1) + expect_eq(count(base_st, hints_st, HINTS_F_MUTED), 1) + hints_tick(hints_st, 0.0) # the next pass comes at once + expect_eq(hints_at(hints_st, 0), HINT_THIRSTY) + expect_eq(hints_count(hints_st), 1) + hints_unmute_all(base_st, hints_st) + hints_tick(hints_st, 0.0) + expect_eq(hints_at(hints_st, 0), HINT_COLD) + expect_eq(count(base_st, hints_st, HINTS_F_UNMUTED), 1) } - test "mutes are kept by key, and a key this build lacks is carried through" { - fresh() - hints_mute(HINT_NIGHT, true) - hints_mute(HINT_BEAR, true) - let l = hints_mutes_save() + test "mutes are kept by key, and a key this build lacks is carried through" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + hints_mute(hints_st, HINT_NIGHT, true) + hints_mute(hints_st, HINT_BEAR, true) + let l = hints_mutes_save(hints_st) expect_eq(Value.count(l), 2) Value.add(l, Value.str("gone_card")) Value.add(l, Value.int(3)) # not a key: ignored - hints_mutes_load(l) - expect(hints_muted(HINT_NIGHT)) - expect(hints_muted(HINT_BEAR)) - expect(not hints_muted(HINT_RAIN)) - expect_eq(hints_mute_count(), 2) - let back = hints_mutes_save() + hints_mutes_load(hints_st, l) + expect(hints_muted(hints_st, HINT_NIGHT)) + expect(hints_muted(hints_st, HINT_BEAR)) + expect(not hints_muted(hints_st, HINT_RAIN)) + expect_eq(hints_mute_count(hints_st), 2) + let back = hints_mutes_save(hints_st) expect_eq(Value.count(back), 3) expect(Value.as_str(Value.at(back, 2)) == "gone_card") - hints_mutes_load(null) - expect_eq(hints_mute_count(), 0) + hints_mutes_load(hints_st, null) + expect_eq(hints_mute_count(hints_st), 0) } - test "as a system it ticks the rail" { - fresh() - core_clear() - core_add(hints_system()) - core_reset_all() - on[HINT_FIRE] = true - core_tick_all(tick_new(1.0, 1, 0.0)) - expect_eq(hints_at(0), HINT_FIRE) + test "as a system it ticks the rail" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) { + fresh(base_st, hints_st, hintstest_st) + core_clear(base_st) + core_add(base_st, hints_system()) + core_reset_all(base_st) + hintstest_st.on[HINT_FIRE] = true + core_tick_all(base_st, tick_new(1.0, 1, 0.0)) + expect_eq(hints_at(hints_st, 0), HINT_FIRE) } } diff --git a/packages/ludic.i18n/langs.ludic b/packages/ludic.i18n/langs.ludic index c216ffc9..be7e6351 100644 --- a/packages/ludic.i18n/langs.ludic +++ b/packages/ludic.i18n/langs.ludic @@ -1,18 +1,18 @@ # langs.ludic - where languages come from, later ones replacing earlier ones with the same code: # /languages.txt (one code per line, # a comment) and every .po in the mod folder -export function i18n_init() -> void { - codes = new []string - names = new []string - paths = new []string - fonts = new []string - lang_add("en", "English", "", "") - let idx = Fs.read_text(lang_dir + "/languages.txt") +export function i18n_init(i18n_st: mut I18nState) -> void { + i18n_st.codes = new []string + i18n_st.names = new []string + i18n_st.paths = new []string + i18n_st.fonts = new []string + lang_add(i18n_st, "en", "English", "", "") + let idx = Fs.read_text(i18n_st.lang_dir + "/languages.txt") if idx != null { let ls = lines_of(idx) for i in 0 .. len(ls) { let ln = trim(ls[i]) if len(ln) == 0 or ln[0] == 35 { continue } - probe(ln, `{lang_dir}/{ln}.po`, lang_dir) + probe(i18n_st, ln, `{i18n_st.lang_dir}/{ln}.po`, i18n_st.lang_dir) } } let md = i18n_mod_dir() @@ -23,60 +23,60 @@ export function i18n_init() -> void { for i in 0 .. len(files) { let nm = files[i] let n = len(nm) - if n > 3 and i18n_ends(nm, ".po") { probe(nm[0..n - 3], md + "/" + nm, md) } + if n > 3 and i18n_ends(nm, ".po") { probe(i18n_st, nm[0..n - 3], md + "/" + nm, md) } } } # where a player's own .po files go; "" when the game has none export function i18n_mod_dir() -> string { return I18nWorld.mod_dir() } -function lang_add(code: string, name: string, path: string, font: string) -> void { - for i in 0 .. len(codes) { - if codes[i] == code { - names[i] = name - paths[i] = path - fonts[i] = font +function lang_add(i18n_st: mut I18nState, code: string, name: string, path: string, font: string) -> void { + for i in 0 .. len(i18n_st.codes) { + if i18n_st.codes[i] == code { + i18n_st.names[i] = name + i18n_st.paths[i] = path + i18n_st.fonts[i] = font return } } - push(codes, code) - push(names, name) - push(paths, path) - push(fonts, font) + push(i18n_st.codes, code) + push(i18n_st.names, name) + push(i18n_st.paths, path) + push(i18n_st.fonts, font) } -function probe(code: string, path: string, dir: string) -> void { +function probe(i18n_st: mut I18nState, code: string, path: string, dir: string) -> void { let text = Fs.read_text(path) if text == null { return } var name = i18n_header(text, "X-Language-Name") if len(name) == 0 { name = code } var font = i18n_header(text, "X-Font") if len(font) > 0 { font = dir + "/" + font } - lang_add(code, name, path, font) + lang_add(i18n_st, code, name, path, font) } -export function i18n_count() -> int { - if codes == null { return 1 } - return len(codes) +export function i18n_count(i18n_st: I18nState) -> int { + if i18n_st.codes == null { return 1 } + return len(i18n_st.codes) } -export function i18n_index(code: string) -> int { - if codes != null { for i in 0 .. len(codes) { if codes[i] == code { return i } } } +export function i18n_index(i18n_st: I18nState, code: string) -> int { + if i18n_st.codes != null { for i in 0 .. len(i18n_st.codes) { if i18n_st.codes[i] == code { return i } } } return 0 } -export function i18n_code_at(i: int) -> string { - if codes == null or i < 0 or i >= len(codes) { return "en" } - return codes[i] +export function i18n_code_at(i18n_st: I18nState, i: int) -> string { + if i18n_st.codes == null or i < 0 or i >= len(i18n_st.codes) { return "en" } + return i18n_st.codes[i] } -export function i18n_name_at(i: int) -> string { - if names == null or i < 0 or i >= len(names) { return "English" } - return names[i] +export function i18n_name_at(i18n_st: I18nState, i: int) -> string { + if i18n_st.names == null or i < 0 or i >= len(i18n_st.names) { return "English" } + return i18n_st.names[i] } # the font atlas a language names with X-Font, beside its .po; "" is the default -export function i18n_font_at(i: int) -> string { - if fonts == null or i < 0 or i >= len(fonts) { return "" } - return fonts[i] +export function i18n_font_at(i18n_st: I18nState, i: int) -> string { + if i18n_st.fonts == null or i < 0 or i >= len(i18n_st.fonts) { return "" } + return i18n_st.fonts[i] } diff --git a/packages/ludic.i18n/match.ludic b/packages/ludic.i18n/match.ludic index 08d4c7cb..d4fdd614 100644 --- a/packages/ludic.i18n/match.ludic +++ b/packages/ludic.i18n/match.ludic @@ -2,13 +2,13 @@ # English, and the text that filled each hole translated in turn # does pattern p produce s? The holes land in caps, in hole order -function try_pattern(p: int, s: string) -> bool { +function try_pattern(i18n_st: mut I18nState, p: int, s: string) -> bool { let n = len(s) - let l0 = pat_l0[p] - let nl = pat_nl[p] - let first = lits[l0] + let l0 = i18n_st.pat_l0[p] + let nl = i18n_st.pat_nl[p] + let first = i18n_st.lits[l0] let f_n = len(first) - let last = lits[l0 + nl - 1] + let last = i18n_st.lits[l0 + nl - 1] let l_n = len(last) if nl - 1 > CAPS { return false } if n < f_n + l_n + nl - 1 { return false } # every hole takes at least a byte @@ -20,76 +20,76 @@ function try_pattern(p: int, s: string) -> bool { var stop = lim var adv = 0 if g < nl - 2 { - let mid = lits[l0 + g + 1] + let mid = i18n_st.lits[l0 + g + 1] stop = i18n_find(s, lim, pos + 1, mid, len(mid)) if stop < 0 { return false } adv = len(mid) } if stop <= pos { return false } - caps[g] = s[pos..stop] + i18n_st.caps[g] = s[pos..stop] pos = stop + adv } return true } # the best of a chain that produces s, or `best` as it was -function best_of(head: int, next: words, s: string, best: int) -> int { +function best_of(i18n_st: mut I18nState, head: int, next: words, s: string, best: int) -> int { var b = best var c = head while c > 0 { let p = c - 1 var sc = -1 - if b >= 0 { sc = pat_sc[b] } - if pat_sc[p] > sc and try_pattern(p, s) { b = p } + if b >= 0 { sc = i18n_st.pat_sc[b] } + if i18n_st.pat_sc[p] > sc and try_pattern(i18n_st, p, s) { b = p } c = next[p] } return b } -function pattern(s: string, depth: int) -> string { - hit = false - if pat_n == 0 { return s } +function pattern(i18n_st: mut I18nState, s: string, depth: int) -> string { + i18n_st.hit = false + if i18n_st.pat_n == 0 { return s } let n = len(s) var best = -1 if n >= 4 { - best = best_of(Dict.get_or(pfx, s[0..4], 0), nx_p, s, best) - best = best_of(Dict.get_or(sfx, s[n - 4..n], 0), nx_s, s, best) + best = best_of(i18n_st, Dict.get_or(i18n_st.pfx, s[0..4], 0), i18n_st.nx_p, s, best) + best = best_of(i18n_st, Dict.get_or(i18n_st.sfx, s[n - 4..n], 0), i18n_st.nx_s, s, best) } - if n < 80 { best = best_of(gen_head, nx_g, s, best) } + if n < 80 { best = best_of(i18n_st, i18n_st.gen_head, i18n_st.nx_g, s, best) } if best < 0 { return s } - try_pattern(best, s) - let l0 = pat_l0[best] - let nl = pat_nl[best] + try_pattern(i18n_st, best, s) + let l0 = i18n_st.pat_l0[best] + let nl = i18n_st.pat_nl[best] # copied out first: translating a hole may match another pattern and reuse caps let raw = new []string - for g in 0 .. nl - 1 { push(raw, caps[g]) } + for g in 0 .. nl - 1 { push(raw, i18n_st.caps[g]) } let cs = new []string - for g in 0 .. nl - 1 { push(cs, piece(raw[g], depth + 1)) } - let r = fill_holes(pat_out[best], l0, nl, cs) + for g in 0 .. nl - 1 { push(cs, piece(i18n_st, raw[g], depth + 1)) } + let r = fill_holes(i18n_st, i18n_st.pat_out[best], l0, nl, cs) # a pattern that gives the English straight back has translated nothing, and must not stop # the sentence-by-sentence pass if r == s { return s } - hit = true + i18n_st.hit = true return r } # the text that filled a hole: known as it stands, known without the English article the game # glued on ("a deer", "the Dark King"), or itself a pattern -function piece(c: string, depth: int) -> string { - let e = Dict.get_or(exact, c, -1) - if e >= 0 { return out[e] } +function piece(i18n_st: mut I18nState, c: string, depth: int) -> string { + let e = Dict.get_or(i18n_st.exact, c, -1) + if e >= 0 { return i18n_st.out[e] } let n = len(c) var bare = -1 if n > 4 and (i18n_starts(c, "the ") or i18n_starts(c, "The ")) { bare = 4 } else if n > 3 and (i18n_starts(c, "an ") or i18n_starts(c, "An ")) { bare = 3 } else if n > 2 and (i18n_starts(c, "a ") or i18n_starts(c, "A ")) { bare = 2 } if bare > 0 { - let e2 = Dict.get_or(exact, c[bare..n], -1) - if e2 >= 0 { return out[e2] } + let e2 = Dict.get_or(i18n_st.exact, c[bare..n], -1) + if e2 >= 0 { return i18n_st.out[e2] } } if depth < 3 { - let r = pattern(c, depth) - if hit { return r } + let r = pattern(i18n_st, c, depth) + if i18n_st.hit { return r } } return c } diff --git a/packages/ludic.i18n/patterns.ludic b/packages/ludic.i18n/patterns.ludic index 2fb90f51..c12ebfd1 100644 --- a/packages/ludic.i18n/patterns.ludic +++ b/packages/ludic.i18n/patterns.ludic @@ -15,10 +15,10 @@ function holes(s: string) -> int { return h } -function add_pattern(id: string, tr: string) -> void { - let p = pat_n - pat_n += 1 - let l0 = len(lits) +function add_pattern(i18n_st: mut I18nState, id: string, tr: string) -> void { + let p = i18n_st.pat_n + i18n_st.pat_n += 1 + let l0 = len(i18n_st.lits) let n = len(id) var seg = 0 var i = 0 @@ -27,9 +27,9 @@ function add_pattern(id: string, tr: string) -> void { while i < n { let num = hole_at(id, i) if num >= 0 { - push(lits, id[seg..i]) + push(i18n_st.lits, id[seg..i]) score += i - seg - pat_ph[l0 + nl] = num + i18n_st.pat_ph[l0 + nl] = num nl += 1 i = hole_end(id, i) seg = i @@ -37,33 +37,33 @@ function add_pattern(id: string, tr: string) -> void { } i += 1 } - push(lits, id[seg..n]) + push(i18n_st.lits, id[seg..n]) score += n - seg nl += 1 - pat_l0[p] = l0 - pat_nl[p] = nl - pat_sc[p] = score - push(pat_out, tr) - chain(p, lits[l0], lits[l0 + nl - 1]) + i18n_st.pat_l0[p] = l0 + i18n_st.pat_nl[p] = nl + i18n_st.pat_sc[p] = score + push(i18n_st.pat_out, tr) + chain(i18n_st, p, i18n_st.lits[l0], i18n_st.lits[l0 + nl - 1]) } -function chain(p: int, first: string, last: string) -> void { +function chain(i18n_st: mut I18nState, p: int, first: string, last: string) -> void { if len(first) >= 4 { let key: string = first[0..4] - nx_p[p] = Dict.get_or(pfx, key, 0) - Dict.set(pfx, key, p + 1) + i18n_st.nx_p[p] = Dict.get_or(i18n_st.pfx, key, 0) + Dict.set(i18n_st.pfx, key, p + 1) } else if len(last) >= 4 { let key: string = last[len(last) - 4..len(last)] - nx_s[p] = Dict.get_or(sfx, key, 0) - Dict.set(sfx, key, p + 1) + i18n_st.nx_s[p] = Dict.get_or(i18n_st.sfx, key, 0) + Dict.set(i18n_st.sfx, key, p + 1) } else { - nx_g[p] = gen_head - gen_head = p + 1 + i18n_st.nx_g[p] = i18n_st.gen_head + i18n_st.gen_head = p + 1 } } # a translation with its holes filled from caps, by number: a translation may reorder or drop them -function fill_holes(t: string, l0: int, nl: int, cs: []string) -> string { +function fill_holes(i18n_st: I18nState, t: string, l0: int, nl: int, cs: []string) -> string { let n = len(t) var r = "" var seg = 0 @@ -72,7 +72,7 @@ function fill_holes(t: string, l0: int, nl: int, cs: []string) -> string { let num = hole_at(t, i) if num >= 0 { r = r + t[seg..i] - for g in 0 .. nl - 1 { if pat_ph[l0 + g] == num { r = r + cs[g] } } + for g in 0 .. nl - 1 { if i18n_st.pat_ph[l0 + g] == num { r = r + cs[g] } } i = hole_end(t, i) seg = i continue diff --git a/packages/ludic.i18n/plural.ludic b/packages/ludic.i18n/plural.ludic index 504b0f46..4c9021dc 100644 --- a/packages/ludic.i18n/plural.ludic +++ b/packages/ludic.i18n/plural.ludic @@ -15,7 +15,7 @@ function plural_rule_of(h: string) -> int { return PL_NOT_ONE } -export function i18n_plural_rule() -> int { return plural_rule } +export function i18n_plural_rule(i18n_st: I18nState) -> int { return i18n_st.plural_rule } # which msgstr[k] a count takes under a rule export function i18n_plural_form(rule: int, n: int) -> int { @@ -33,23 +33,23 @@ export function i18n_plural_form(rule: int, n: int) -> int { return 0 } -function plural_add(e: PoEntry) -> void { +function plural_add(i18n_st: mut I18nState, e: PoEntry) -> void { if len(e.forms) == 0 or blank(e.forms[0]) { return } - Dict.set(plural, e.id, len(forms) / 3) + Dict.set(i18n_st.plural, e.id, len(i18n_st.forms) / 3) for k in 0 .. 3 { - if k < len(e.forms) { push(forms, e.forms[k]) } else { push(forms, e.forms[len(e.forms) - 1]) } + if k < len(e.forms) { push(i18n_st.forms, e.forms[k]) } else { push(i18n_st.forms, e.forms[len(e.forms) - 1]) } } } # `one` or `many` by the count, in the language in use, with every {1} the count: Ln("{1} trout", # "{1} trout", 3). English (or a line the file lacks) is one for 1 and many otherwise -export function Ln(one: string, many: string, n: int) -> string { +export function Ln(i18n_st: I18nState, one: string, many: string, n: int) -> string { var t = many if n == 1 { t = one } - if on { - let k = Dict.get_or(plural, one, -1) + if i18n_st.on { + let k = Dict.get_or(i18n_st.plural, one, -1) if k >= 0 { - let f = forms[k * 3 + i18n_plural_form(plural_rule, n)] + let f = i18n_st.forms[k * 3 + i18n_plural_form(i18n_st.plural_rule, n)] if not blank(f) { t = f } } } diff --git a/packages/ludic.i18n/ports.ludic b/packages/ludic.i18n/ports.ludic index 9dece674..31bcb362 100644 --- a/packages/ludic.i18n/ports.ludic +++ b/packages/ludic.i18n/ports.ludic @@ -12,13 +12,44 @@ function i18n__no_code() -> string { return "" } function i18n__no() -> bool { return false } function i18n__no_font(dir: string) -> bool { return false } -var lang_dir: string = "assets/lang" -var font_default: string = "assets/kit/font" +export state I18nState { + lang_dir: string = "assets/lang" + font_default: string = "assets/kit/font" + on: bool = false # a language other than English is drawing + cur: string = "en" + codes: []string = null # every language found, "en" first + names: []string = null + paths: []string = null + fonts: []string = null # a font atlas directory, "" for the game's own + font_now: string = "" + exact: Dict = null # msgid -> index into out + out: []string = null + lits: []string = null # every pattern's literal pieces, one run per pattern + pat_out: []string = null + pat_l0: words = null # where a pattern's pieces start in lits + pat_nl: words = null # how many pieces (holes + 1) + pat_sc: words = null # bytes of English it pins down: more is a better match + pat_ph: words = null # by piece: the hole number that follows it + pat_n: int = 0 + pfx: Dict = null # a pattern's first four bytes -> its chain head + 1 + sfx: Dict = null # else its last four + gen_head: int = 0 # else the short list, tried on short strings only + nx_p: words = null + nx_s: words = null + nx_g: words = null + cache: Dict = null # English as drawn -> index into cache_out + cache_out: []string = null + caps: []string = null + hit: bool = false + plural: Dict = null # a plural msgid (the singular) -> index into forms / 3 + forms: []string = null # three forms per entry, "" where the file has fewer + plural_rule: int = 0 # PL_* +} # where the shipped languages are (a directory with languages.txt and .po), and the atlas a # language with no X-Font draws in -export function i18n_config(dir: string, default_font: string) -> void { - lang_dir = dir - font_default = default_font - codes = null +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 } diff --git a/packages/ludic.i18n/state.ludic b/packages/ludic.i18n/state.ludic index 8fce2317..b59e3318 100644 --- a/packages/ludic.i18n/state.ludic +++ b/packages/ludic.i18n/state.ludic @@ -1,47 +1,18 @@ # 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() -> 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_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_pattern_count() -> int { return pat_n } +export function i18n_pattern_count(i18n_st: I18nState) -> int { return i18n_st.pat_n } # the overlay's own font, as the overlay was opened with it -export function i18n_font_base(path: string) -> void { font_now = path } +export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path } diff --git a/packages/ludic.i18n/tests/i18n_test.ludic b/packages/ludic.i18n/tests/i18n_test.ludic index dd525380..61e101f8 100644 --- a/packages/ludic.i18n/tests/i18n_test.ludic +++ b/packages/ludic.i18n/tests/i18n_test.ludic @@ -4,18 +4,20 @@ import "ludic.i18n" program I18nTest { numbers float - var forced_code: string = "" - var fonts_set: []string = null - var font_ok: bool = true + state I18ntestState { + forced_code: string = "" + fonts_set: []string = null + 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() -> string { return forced_code } + function t_forced(i18ntest_st: I18ntestState) -> string { return i18ntest_st.forced_code } function t_ready() -> bool { return true } - function t_font(d: string) -> bool { - push(fonts_set, d) - return font_ok + function t_font(i18ntest_st: mut I18ntestState, d: string) -> bool { + push(i18ntest_st.fonts_set, d) + return i18ntest_st.font_ok } bind I18nWorld { mod_dir: fn t_mods, forced: fn t_forced, font_ready: fn t_ready, set_font: fn t_font } @@ -36,116 +38,116 @@ program I18nTest { return s } - function fresh() -> void { - forced_code = "" - fonts_set = new []string - font_ok = true + 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 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(dir(), "font_default") - i18n_init() + i18n_config(i18n_st, dir(), "font_default") + i18n_init(i18n_st) } - 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 "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 "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 "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 "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 "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 "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") + 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") 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" { - 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 "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 "a mod in the player's folder is found, replaces a shipped code, and goes when removed" { - fresh() + 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) 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() - 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") + 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") Fs.remove(mods() + "/zz.po") Fs.remove(mods() + "/tt.po") - i18n_init() - expect_eq(i18n_index("zz"), 0) - i18n_use("en") - expect(not i18n_on()) - expect(L("Settings") == "Settings") + 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") } - test "a forced code wins over the one asked for" { - fresh() - forced_code = "tt" - i18n_use("en") - expect(i18n_current() == "tt") + 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 header field, and a .po loaded from text" { + test "a header field, and a .po loaded from text" (i18n_st: mut I18nState) { expect(i18n_header(tt(), "X-Language-Name") == "Test Tongue") expect(i18n_header(tt(), "X-Nothing") == "") - i18n_load_text(po_line("Quit", "Cik")) - expect(L("Quit") == "Cik") - i18n_off() - expect(L("Quit") == "Quit") + 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") } } diff --git a/packages/ludic.i18n/translate.ludic b/packages/ludic.i18n/translate.ludic index b8032a77..2365b234 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(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 +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 } - Dict.set(cache, s, len(cache_out)) - push(cache_out, r) + Dict.set(i18n_st.cache, s, len(i18n_st.cache_out)) + push(i18n_st.cache_out, r) return r } -function whole(s: string) -> string { +function whole(i18n_st: mut I18nState, s: string) -> string { let n = len(s) if n == 0 { return s } - let e = Dict.get_or(exact, s, -1) - if e >= 0 { return out[e] } - let r = pattern(s, 0) - if hit { return r } + 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 } # the padding a layout put round a word: " owned" var a = 0 var b = n @@ -29,15 +29,15 @@ function whole(s: string) -> string { if a > 0 or b < n { if b <= a { return s } let core: string = s[a..b] - let t = whole(core) + let t = whole(i18n_st, core) if t == core { return s } return s[0..a] + t + s[b..n] } - return sentences(s) + return sentences(i18n_st, s) } # a paragraph built from sentences, one at a time; English stays where a sentence is unknown -function sentences(s: string) -> string { +function sentences(i18n_st: mut I18nState, s: string) -> string { let n = len(s) var r = "" var start = 0 @@ -47,8 +47,8 @@ function sentences(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(s[start..i + 1]) - if hit { any = true } + let t = sentence(i18n_st, s[start..i + 1]) + if i18n_st.hit { any = true } r = r + t + " " start = i + 2 pieces += 1 @@ -58,18 +58,18 @@ function sentences(s: string) -> string { i += 1 } if pieces == 0 { return s } - let t = sentence(s[start..n]) - if hit { any = true } + let t = sentence(i18n_st, s[start..n]) + if i18n_st.hit { any = true } if not any { return s } return r + t } -function sentence(p: string) -> string { - hit = false - let e = Dict.get_or(exact, p, -1) +function sentence(i18n_st: mut I18nState, p: string) -> string { + i18n_st.hit = false + let e = Dict.get_or(i18n_st.exact, p, -1) if e >= 0 { - hit = true - return out[e] + i18n_st.hit = true + return i18n_st.out[e] } - return pattern(p, 0) + return pattern(i18n_st, p, 0) } diff --git a/packages/ludic.i18n/use.ludic b/packages/ludic.i18n/use.ludic index 09fa44b7..e4da8d39 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(code: string) -> void { - if codes == null { i18n_init() } - on = false - cur = "en" - var k = i18n_index(code) +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) let forced = I18nWorld.forced() - if len(forced) > 0 { k = i18n_index(forced) } + if len(forced) > 0 { k = i18n_index(i18n_st, forced) } var font = "" if k > 0 { - let text = Fs.read_text(paths[k]) + let text = Fs.read_text(i18n_st.paths[k]) if text != null { - i18n_load_text(text) - on = true - cur = codes[k] - font = fonts[k] + i18n_load_text(i18n_st, text) + i18n_st.on = true + i18n_st.cur = i18n_st.codes[k] + font = i18n_st.fonts[k] } } # the atlas follows the language, and is only reloaded when it actually changes - 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 + 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 } } } # a .po's text as the lookups, whatever the language is called (a test, a file from elsewhere) -export function i18n_load_text(text: string) -> void { +export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void { let es = i18n_parse(text) - 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")) + 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")) var np = 0 var nlit = 0 for i in 0 .. len(es) { @@ -48,33 +48,33 @@ export function i18n_load_text(text: string) -> void { nlit += h + 1 } } - 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 + 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 for i in 0 .. len(es) { let e = es[i] - 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) + 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) } } } - on = true + i18n_st.on = true } # English again: nothing loaded, nothing translated -export function i18n_off() -> void { - on = false - cur = "en" +export function i18n_off(i18n_st: mut I18nState) -> void { + i18n_st.on = false + i18n_st.cur = "en" } diff --git a/packages/ludic.inventory/state.ludic b/packages/ludic.inventory/state.ludic index e6315562..e1d12a9f 100644 --- a/packages/ludic.inventory/state.ludic +++ b/packages/ludic.inventory/state.ludic @@ -12,31 +12,33 @@ export property ItemChanged { after: int = 0 } -var iv_keys: []string = null -var iv_counts: []int = null -var iv_changes: Queue = null +export state InventoryState { + iv_keys: []string = null + iv_counts: []int = null + iv_changes: Queue = null +} # the kinds, by the name each is saved under; the pack starts empty -export function inv_config(keys: []string) -> void { - iv_keys = keys - inv_clear() +export function inv_config(inventory_st: mut InventoryState, keys: []string) -> void { + inventory_st.iv_keys = keys + inv_clear(inventory_st) } -export function inv_changes() -> Queue { - if iv_changes == null { iv_changes = queue_new("inventory.changes") } - return iv_changes +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 } -function iv_valid(it: int) -> bool { return iv_counts != null and it >= 0 and it < len(iv_counts) } +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) } -export function inv_kinds() -> int { - if iv_keys == null { return 0 } - return len(iv_keys) +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_count(it: int) -> int { - if not iv_valid(it) { return 0 } - return iv_counts[it] +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_limit(it: int) -> int { @@ -44,16 +46,16 @@ export function inv_limit(it: int) -> int { } # how many more fit -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_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_has(it: int, n: int) -> bool { return inv_count(it) >= n } +export function inv_has(inventory_st: InventoryState, it: int, n: int) -> bool { return inv_count(inventory_st, it) >= n } -export function inv_total() -> int { +export function inv_total(inventory_st: InventoryState) -> int { var t = 0 - if iv_counts == null { return 0 } - for i in 0 .. len(iv_counts) { t += iv_counts[i] } + if inventory_st.iv_counts == null { return 0 } + for i in 0 .. len(inventory_st.iv_counts) { t += inventory_st.iv_counts[i] } return t } diff --git a/packages/ludic.inventory/system.ludic b/packages/ludic.inventory/system.ludic index b1d932b0..e68108df 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() -> void { - inv_clear() - q_clear(inv_changes()) +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_save() -> Val { +export function inv_save(inventory_st: InventoryState) -> Val { let counts = Value.object() - for i in 0 .. inv_kinds() { - if iv_counts[i] != 0 { sv_put_int(counts, iv_keys[i], iv_counts[i]) } + 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]) } } let v = Value.object() Value.put(v, "counts", counts) @@ -16,11 +16,11 @@ export function inv_save() -> Val { } # a name this build does not know is dropped -export function inv_load(v: Val, version: int) -> void { - inv_clear() +export function inv_load(inventory_st: mut InventoryState, v: Val, version: int) -> void { + inv_clear(inventory_st) if Value.has(v, "counts") == 0 { return } let counts = Value.get(v, "counts") - for i in 0 .. inv_kinds() { iv_counts[i] = Math.max(sv_int(counts, iv_keys[i], 0), 0) } + 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) } } export function inv_system() -> System { diff --git a/packages/ludic.inventory/tests/inventory_test.ludic b/packages/ludic.inventory/tests/inventory_test.ludic index 6511cc36..408e3952 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() -> void { - inv_config(names("wood", "rod", "fish")) - inv_reset() + 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 changes() -> []ItemChanged { return q_drain(inv_changes()) } - function waiting() -> int { return q_len(inv_changes()) } + 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)) } - 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 "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 "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 "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 "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 "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 "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() + 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) 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(), 0) + expect_eq(waiting(base_st, inventory_st), 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 "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 "the save is by name, so a reordered table still reads it" { - fresh() - inv_add(WOOD, 4) - inv_add(FISH, 2) + 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) let root = save_tree() - save_section(root, "inventory", 1, inv_save()) + save_section(root, "inventory", 1, inv_save(inventory_st)) let text = save_encode(root) - inv_config(names("fish", "bait", "wood")) + inv_config(inventory_st, names("fish", "bait", "wood")) let n = load_section(save_decode(text), "inventory") - inv_load(n.data, n.version) - expect_eq(inv_count(0), 2) - expect_eq(inv_count(1), 0) - expect_eq(inv_count(2), 4) + 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) } } diff --git a/packages/ludic.inventory/verbs.ludic b/packages/ludic.inventory/verbs.ludic index 017a2eac..0fabbd6f 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(it: int, after: int) -> void { - let before = iv_counts[it] +function iv_change(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, after: int) -> void { + let before = inventory_st.iv_counts[it] if before == after { return } - iv_counts[it] = after + inventory_st.iv_counts[it] = after let c = new ItemChanged c.item = it c.before = before c.after = after - q_push(inv_changes(), c) + q_push(base_st, inv_changes(base_st, inventory_st), c) } # as many of n as there is room for; returns how many went in -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) } +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) } return k } # all n or none -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) } +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) } return true } # a count outright, room or not (a starting kit, a correction, the network's word); never below 0 -export function inv_set(it: int, n: int) -> void { - if not iv_valid(it) { return } - iv_change(it, Math.max(n, 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)) } # everything gone, silently: a new trip, or before a load -export function inv_clear() -> void { - iv_counts = new []int - for i in 0 .. inv_kinds() { push(iv_counts, 0) } +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) } } diff --git a/packages/ludic.jobs/boards.ludic b/packages/ludic.jobs/boards.ludic index 310f0400..3d02acab 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(b: int, s: int) -> int { - jobs__ensure() - return jobs__off[b] + s +function jobs__at(jobs_st: mut JobsState, b: int, s: int) -> int { + jobs__ensure(jobs_st) + return jobs_st.jobs__off[b] + s } # a whole number in [lo, hi] from the jobs' dice: what JobsWorld.roll draws with -export function jobs_roll(lo: int, hi: int) -> int { - jobs__ensure() - return rng_between(jobs__dice, lo, hi) +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) } # inside JobsWorld.roll: what goes up in the slot being filled -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() +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() let e = JobsWorld.evidence(kind, param) - jobs__pb[k] = 0 - if e > 0 { jobs__pb[k] = e } + jobs_st.jobs__pb[k] = 0 + if e > 0 { jobs_st.jobs__pb[k] = e } } -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 +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 } # take a slot down and ask the game for what replaces it -function jobs__repost(b: int, s: int) -> void { - let k = jobs__at(b, s) +function jobs__repost(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int) -> void { + let k = jobs__at(jobs_st, b, s) let day = JobsWorld.day() - rng_seed(jobs__dice, jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs__serial[b] * 31) - jobs__serial[b] += 1 - jobs__clear(k) + 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) JobsWorld.roll(b, s) - if jobs__pk[k] >= 0 { jobs__fact(JOBS_F_OFFERED, -1, b, s, 0, jobs__pn[k]) } + 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]) } } # 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() -> void { - jobs__ensure() +export function jobs_morning(base_st: mut BaseState, jobs_st: mut JobsState) -> void { + jobs__ensure(jobs_st) 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__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) } + 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) } } } } -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) } +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) } } -export function jobs_post_hand_in(b: int, s: int) -> bool { +export function jobs_post_hand_in(base_st: mut BaseState, jobs_st: mut JobsState, 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(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) } + 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) } return true } diff --git a/packages/ludic.jobs/count.ludic b/packages/ludic.jobs/count.ludic index 743c3486..5463eaf4 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(kind: int, param: int, n: int) -> void { jobs__count(-1, kind, param, n) } +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) } # the same for one scope alone, kept or not: another member's deed arriving at the host -export function jobs_count_in(scope: int, kind: int, param: int, n: int) -> void { jobs__count(scope, kind, param, n) } +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) } function jobs__takes(only: int, scope: int) -> bool { if only >= 0 { return scope == only } return JobsWorld.owns(scope) } -function jobs__count(only: int, kind: int, param: int, n: int) -> void { - jobs__ensure() +function jobs__count(base_st: mut BaseState, jobs_st: mut JobsState, only: int, kind: int, param: int, n: int) -> void { + jobs__ensure(jobs_st) for i in 0 .. JOB_COUNT { let d = Jobs[i] - if jobs__st[i] != JOBS_ACTIVE or not jobs__takes(only, d.scope) { continue } + if jobs_st.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__hv[i] - jobs__hv[i] = was + n - jobs__moved(i, was) + let was = jobs_st.jobs__hv[i] + jobs_st.jobs__hv[i] = was + n + jobs__moved(base_st, jobs_st, 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__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) + 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) } } } -function jobs__recheck_one(i: int) -> void { +function jobs__recheck_one(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> void { let d = Jobs[i] - if jobs__st[i] != JOBS_ACTIVE or d.kind < 0 or not JobsWorld.owns(d.scope) { return } + if jobs_st.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__hv[i] { return } - let was = jobs__hv[i] - jobs__hv[i] = e - jobs__moved(i, was) + 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) } # 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() -> void { - jobs__ensure() - for i in 0 .. JOB_COUNT { jobs__recheck_one(i) } +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) } for b in 0 .. BOARD_COUNT { if not JobsWorld.owns(JobBoards[b].scope) { continue } for s in 0 .. JobBoards[b].slots { - 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]) } + 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]) } } } } # offers newly open are said once; the first look after a reset says none of them -function jobs__offers() -> void { - jobs__ensure() +function jobs__offers(base_st: mut BaseState, jobs_st: mut JobsState) -> void { + jobs__ensure(jobs_st) for i in 0 .. JOB_COUNT { - 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 + 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 } - jobs__primed = true + jobs_st.jobs__primed = true } diff --git a/packages/ludic.jobs/jobs.ludic b/packages/ludic.jobs/jobs.ludic index b0e389ba..f54ae3ff 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(i: int) -> int { - jobs__ensure() - return jobs__st[i] +export function jobs_state(jobs_st: mut JobsState, i: int) -> int { + jobs__ensure(jobs_st) + return jobs_st.jobs__st[i] } -export function jobs_have(i: int) -> int { - jobs__ensure() - return jobs__hv[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_ready_now(i: int) -> bool { return jobs_state(i) == JOBS_READY } +export function jobs_ready_now(jobs_st: mut JobsState, i: int) -> bool { return jobs_state(jobs_st, i) == JOBS_READY } # taken and not yet paid -export function jobs_under_way(i: int) -> bool { - let s = jobs_state(i) +export function jobs_under_way(jobs_st: mut JobsState, i: int) -> bool { + let s = jobs_state(jobs_st, i) return s == JOBS_ACTIVE or s == JOBS_READY } # on offer: not taken, the scope's standing and the story far enough, and the game allowing it -export function jobs_offered(i: int) -> bool { +export function jobs_offered(jobs_st: mut JobsState, i: int) -> bool { let d = Jobs[i] - if jobs_state(i) != JOBS_OFFERED { return false } + if jobs_state(jobs_st, i) != JOBS_OFFERED { return false } return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i) } -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) +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) return true } # a count has moved: a job with a need is ready when it is met -function jobs__moved(i: int, was: int) -> void { +function jobs__moved(base_st: mut BaseState, jobs_st: mut JobsState, i: int, was: int) -> void { let d = Jobs[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) } + 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) } } # the game's own play: a step counter, a mask of targets met -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_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_add_have(i: int, n: int) -> void { jobs_set_have(i, jobs_have(i) + n) } +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_mark(i: int, bit: int) -> void { jobs_set_have(i, jobs_have(i) | (1 << bit)) } +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)) } # everything in hand -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_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_hand_in(i: int) -> bool { - if not jobs_ok(i) or jobs_state(i) != JOBS_READY { return false } +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 } let d = Jobs[i] - jobs__st[i] = JOBS_DONE - if d.group >= 0 and d.group < JOBS_GROUPS { jobs__done_n[d.group] += 1 } + jobs_st.jobs__st[i] = JOBS_DONE + if d.group >= 0 and d.group < JOBS_GROUPS { jobs_st.jobs__done_n[d.group] += 1 } JobsPay.reward(d.scope, d.money, d.rep) - jobs__fact(JOBS_F_HANDED_IN, i, -1, -1, jobs__hv[i], d.need) + jobs__fact(base_st, jobs_st, JOBS_F_HANDED_IN, i, -1, -1, jobs_st.jobs__hv[i], d.need) return true } # finished and paid in one go, whatever the count says -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) +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) } # the state outright: the host's word for a party job, a test -export function jobs_set(i: int, state: int, have: int) -> void { - jobs__ensure() - jobs__st[i] = state - jobs__hv[i] = have +export function jobs_set(jobs_st: mut JobsState, i: int, state: int, have: int) -> void { + jobs__ensure(jobs_st) + jobs_st.jobs__st[i] = state + jobs_st.jobs__hv[i] = have } -export function jobs_done(group: int) -> int { - jobs__ensure() +export function jobs_done(jobs_st: mut JobsState, group: int) -> int { + jobs__ensure(jobs_st) if group < 0 or group >= JOBS_GROUPS { return 0 } - return jobs__done_n[group] + return jobs_st.jobs__done_n[group] } -export function jobs_set_done(group: int, n: int) -> void { - jobs__ensure() - if group >= 0 and group < JOBS_GROUPS { jobs__done_n[group] = n } +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 } } diff --git a/packages/ludic.jobs/lists.ludic b/packages/ludic.jobs/lists.ludic index cf24faf5..6e827343 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(group: int, vendor: int) -> []int { +export function jobs_at(jobs_st: mut JobsState, 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(i) - if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(i)) { continue } + let s = jobs_state(jobs_st, i) + if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(jobs_st, i)) { continue } push(out, i) } return out } # every job of a group taken at some point, done ones too -export function jobs_taken(group: int) -> []int { +export function jobs_taken(jobs_st: mut JobsState, group: int) -> []int { let out = new []int - for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(i) != JOBS_OFFERED { push(out, i) } } + for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(jobs_st, i) != JOBS_OFFERED { push(out, i) } } return out } # the first `most` of a group under way -export function jobs_shown(group: int, most: int) -> []int { +export function jobs_shown(jobs_st: mut JobsState, 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(i) { push(out, i) } + if Jobs[i].group == group and jobs_under_way(jobs_st, i) { push(out, i) } } return out } # the first under way and not yet met, or -1 -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 } } +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 } } return -1 } diff --git a/packages/ludic.jobs/posts.ludic b/packages/ludic.jobs/posts.ludic index f7b44541..e264ec2a 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(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_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_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(b: int) -> []int { +export function jobs_posted(jobs_st: mut JobsState, b: int) -> []int { let out = new []int - for s in 0 .. jobs_slots(b) { if jobs_post_kind(b, s) >= 0 { push(out, s) } } + for s in 0 .. jobs_slots(b) { if jobs_post_kind(jobs_st, b, s) >= 0 { push(out, s) } } return out } # how many of a board's posts are paid -export function jobs_posts_done(b: int) -> int { +export function jobs_posts_done(jobs_st: mut JobsState, b: int) -> int { var n = 0 - for s in 0 .. jobs_slots(b) { if jobs_post_kind(b, s) >= 0 and jobs_post_state(b, s) == JOBS_DONE { n += 1 } } + for s in 0 .. jobs_slots(b) { if jobs_post_kind(jobs_st, b, s) >= 0 and jobs_post_state(jobs_st, b, s) == JOBS_DONE { n += 1 } } return n } -export function jobs_total(b: int) -> int { - jobs__ensure() - return jobs__total[b] +export function jobs_total(jobs_st: mut JobsState, b: int) -> int { + jobs__ensure(jobs_st) + return jobs_st.jobs__total[b] } # a slot outright: the host's board on a guest, a test -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_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_set_total(b: int, n: int) -> void { - jobs__ensure() - jobs__total[b] = n +export function jobs_set_total(jobs_st: mut JobsState, b: int, n: int) -> void { + jobs__ensure(jobs_st) + jobs_st.jobs__total[b] = n } diff --git a/packages/ludic.jobs/save.ludic b/packages/ludic.jobs/save.ludic index aabe027b..375823cc 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(sc: int) -> void { - jobs__ensure() +function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void { + jobs__ensure(jobs_st) for i in 0 .. JOB_COUNT { if Jobs[i].scope != sc { continue } - jobs__st[i] = JOBS_OFFERED - jobs__hv[i] = 0 + jobs_st.jobs__st[i] = JOBS_OFFERED + jobs_st.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__off[b] + s) } - jobs__total[b] = 0 - jobs__serial[b] = 0 + 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 } - jobs__primed = false + jobs_st.jobs__primed = false } -function jobs__save_scope(sc: int) -> Val { +function jobs__save_scope(jobs_st: JobsState, 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[i] == JOBS_OFFERED { continue } - sv_put_ints(js, Jobs[i].key, [jobs__st[i], jobs__hv[i]]) + 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]]) } 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__total[b]) - sv_put_int(o, "serial", jobs__serial[b]) + sv_put_int(o, "total", jobs_st.jobs__total[b]) + sv_put_int(o, "serial", jobs_st.jobs__serial[b]) let slots = new []int for s in 0 .. JobBoards[b].slots { - 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]] + 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]] for f in 0 .. len(row) { push(slots, row[f]) } } sv_put_ints(o, "slots", slots) @@ -43,13 +43,13 @@ function jobs__save_scope(sc: int) -> Val { return v } -function jobs__load_scope(sc: int, v: Val) -> void { - jobs__reset_scope(sc) +function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void { + jobs__reset_scope(jobs_st, 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(i, p[0], p[1]) } + if Jobs[i].scope == sc and len(p) == 2 { jobs_set(jobs_st, i, p[0], p[1]) } } } if Value.has(v, "boards") == 0 { return } @@ -57,15 +57,15 @@ function jobs__load_scope(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__total[b] = sv_int(o, "total", 0) - jobs__serial[b] = sv_int(o, "serial", 0) + jobs_st.jobs__total[b] = sv_int(o, "total", 0) + jobs_st.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__off[b] + s + let k = jobs_st.jobs__off[b] + s let q = s * 9 - 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] + 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] } } } diff --git a/packages/ludic.jobs/state.ludic b/packages/ludic.jobs/state.ludic index b5eae680..91072185 100644 --- a/packages/ludic.jobs/state.ludic +++ b/packages/ludic.jobs/state.ludic @@ -1,28 +1,30 @@ # 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. -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 +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 +} # 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 @@ -30,39 +32,39 @@ function jobs__ints(n: int, v: int) -> []int { return out } -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 +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 var n = 0 for b in 0 .. BOARD_COUNT { - push(jobs__off, n) + push(jobs_st.jobs__off, n) n += JobBoards[b].slots } - 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) + 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) } -export function jobs_facts() -> Queue { - if jobs__q == null { jobs__q = queue_new("jobs.facts") } - return jobs__q +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 } -function jobs__fact(what: int, job: int, b: int, s: int, have: int, need: int) -> void { +function jobs__fact(base_st: mut BaseState, jobs_st: mut JobsState, what: int, job: int, b: int, s: int, have: int, need: int) -> void { let f = new JobsFact f.what = what f.job = job @@ -77,17 +79,17 @@ function jobs__fact(what: int, job: int, b: int, s: int, have: int, need: int) - f.rep = Jobs[job].rep f.scope = Jobs[job].scope } else { - 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.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.scope = JobBoards[b].scope } - q_push(jobs_facts(), f) + q_push(base_st, jobs_facts(base_st, jobs_st), 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(seed: int) -> void { - jobs__seed = seed - jobs__dice = rng_new(seed) +export function jobs_config_seed(jobs_st: mut JobsState, seed: int) -> void { + jobs_st.jobs__seed = seed + jobs_st.jobs__dice = rng_new(seed) } diff --git a/packages/ludic.jobs/system.ludic b/packages/ludic.jobs/system.ludic index 51701e01..6e2bfb42 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() -> void { - jobs__reset_scope(JOBS_SELF) - for g in 0 .. JOBS_GROUPS { jobs__done_n[g] = 0 } - q_clear(jobs_facts()) +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_party_reset() -> void { jobs__reset_scope(JOBS_PARTY) } +export function jobs_party_reset(jobs_st: mut JobsState) -> void { jobs__reset_scope(jobs_st, JOBS_PARTY) } -export function jobs_save() -> Val { - jobs__ensure() - let v = jobs__save_scope(JOBS_SELF) - sv_put_ints(v, "done", jobs__done_n) +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) return v } -export function jobs_load(v: Val, version: int) -> void { - jobs__load_scope(JOBS_SELF, v) +export function jobs_load(jobs_st: mut JobsState, v: Val, version: int) -> void { + jobs__load_scope(jobs_st, JOBS_SELF, v) let d = sv_ints(v, "done") for g in 0 .. JOBS_GROUPS { - jobs__done_n[g] = 0 - if g < len(d) { jobs__done_n[g] = d[g] } + jobs_st.jobs__done_n[g] = 0 + if g < len(d) { jobs_st.jobs__done_n[g] = d[g] } } } -export function jobs_party_save() -> Val { - jobs__ensure() - return jobs__save_scope(JOBS_PARTY) +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_load(v: Val, version: int) -> void { jobs__load_scope(JOBS_PARTY, v) } +export function jobs_party_load(jobs_st: mut JobsState, v: Val, version: int) -> void { jobs__load_scope(jobs_st, JOBS_PARTY, v) } -function jobs__tick(t: Tick) -> void { - jobs__offers() - jobs_recheck() +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) } export function jobs_system() -> System { diff --git a/packages/ludic.jobs/tests/jobs_test.ludic b/packages/ludic.jobs/tests/jobs_test.ludic index 41fc110d..abab98d0 100644 --- a/packages/ludic.jobs/tests/jobs_test.ludic +++ b/packages/ludic.jobs/tests/jobs_test.ludic @@ -15,50 +15,52 @@ program JobsTest { def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false } def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY } - 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 + 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 + } - 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 } + 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 } return -1 } - 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 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 pay(sc: int, money: int, r: int) -> void { - paid += money - paid_rep += r - paid_scope = sc + 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 } 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() -> void { - rep = 0 - stage = 0 - day = 1 - guest = false - fish = 0 - paid = 0 - paid_rep = 0 - jobs_reset() - jobs_party_reset() + 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 count_of(what: int) -> int { - let fs = q_drain(jobs_facts()) + 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)) var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n @@ -70,166 +72,166 @@ program JobsTest { expect_eq(BOARD_COUNT, 2) } - 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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "an offer is said once it opens, never for what was open at the reset" { - fresh() + 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) jobs_system().tick(tick_new(0.1, 1, 0.0)) - expect_eq(count_of(JOBS_F_OFFERED), 0) - rep = 10 + expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 0) + jobstest_st.rep = 10 jobs_system().tick(tick_new(0.1, 2, 0.0)) - expect_eq(count_of(JOBS_F_OFFERED), 1) + expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 1) } - 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) + 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) expect_eq(a, b) - expect(jobs_post_need(BOARD_DAILY, 0) >= 2 and jobs_post_need(BOARD_DAILY, 0) <= 4) + expect(jobs_post_need(jobs_st, BOARD_DAILY, 0) >= 2 and jobs_post_need(jobs_st, BOARD_DAILY, 0) <= 4) } - 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 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 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 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 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 "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 "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) + 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) } } diff --git a/packages/ludic.lab/boot.ludic b/packages/ludic.lab/boot.ludic index c6e97b79..be5ddd71 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() -> void { - actor_draw() - scatter_draw() - if lb_draw != null { lb_draw() } +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_casters(light_vp: floats) -> void { - actor_draw_casters(light_vp) - scatter_draw_casters(light_vp) +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_pick_scene() -> int { +function lab_pick_scene(lab_st: mut LabState) -> int { if LAB_COUNT == 0 { return -1 } - lb_scene = LabScenes[0].key - if Os.has_env("R3D_SCENE") { lb_scene = Os.env("R3D_SCENE") } - return labscenes_find(lb_scene) + 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) } # once, from the program's Start handler -export function lab_boot() -> void { - lb_windowed = is_windowed() - let k = lab_pick_scene() +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) if k < 0 { - print(`lab: no scene called '{lb_scene}' - {lab_scene_names()}`) + print(`lab: no scene called '{lab_st.lb_scene}' - {lab_scene_names()}`) quit() return } var ww = 1920 var wh = 1080 - if lb_windowed { + if lab_st.lb_windowed { ww = 960 wh = 540 } - r3d_plate_mode(true) - r3d_sky_path = `{lab_plate_dir()}/sky.hdr` - if not r3d_init(ww, wh, lb_title) { + 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) { quit() return } - 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}` + 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}` let sc = LabScenes[k] - lb_draw = sc.draw + lab_st.lb_draw = sc.draw if sc.setup != null { sc.setup() } - 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 { + 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 { Fs.mkdir("build") Fs.mkdir("build/lab") - Fs.mkdir(lb_out) + Fs.mkdir(lab_st.lb_out) } - print(`lab: scene {lb_scene}, {lab_shot_count()} shots`) + print(`lab: scene {lab_st.lb_scene}, {lab_shot_count(lab_st)} shots`) } diff --git a/packages/ludic.lab/loop.ludic b/packages/ludic.lab/loop.ludic index 919f7d6b..20cfed87 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() -> void { - if not lb_windowed { return } +export function lab_input(lab_st: mut LabState, render3d_st: mut Render3dState) -> void { + if not lab_st.lb_windowed { return } Input.poll() - let n = lab_shot_count() + let n = lab_shot_count(lab_st) if n > 0 and Input.key_pressed(key: 'n') { - lb_shot = (lb_shot + 1) % n - lab_shot_apply(lb_shot) + lab_st.lb_shot = (lab_st.lb_shot + 1) % n + lab_shot_apply(lab_st, render3d_st, lab_st.lb_shot) } if n > 0 and Input.key_pressed(key: 'p') { - lb_shot = (lb_shot + n - 1) % n - lab_shot_apply(lb_shot) + lab_st.lb_shot = (lab_st.lb_shot + n - 1) % n + lab_shot_apply(lab_st, render3d_st, lab_st.lb_shot) } } -export function lab_render() -> void { - lb_frame += 1 - let k = labscenes_find(lb_scene) +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) if k >= 0 and LabScenes[k].tick != null { LabScenes[k].tick(1.0 / 60.0) } - r3d_frame(lab_time()) - lab_caption() - if not lb_windowed { lab_take() } - r3d_present() + 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) } -function lab_caption() -> void { - if lb_font == "" { return } - ov_begin() +function lab_caption(lab_st: LabState, render3d_st: mut Render3dState) -> void { + if lab_st.lb_font == "" { return } + ov_begin(render3d_st) var name = "" - 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() + 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) } -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() { +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) { quit() return } - lab_shot_apply(lb_shot) + lab_shot_apply(lab_st, render3d_st, lab_st.lb_shot) } diff --git a/packages/ludic.lab/plate.ludic b/packages/ludic.lab/plate.ludic index 59651c72..95f5cc51 100644 --- a/packages/ludic.lab/plate.ludic +++ b/packages/ludic.lab/plate.ludic @@ -1,23 +1,21 @@ # 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() -> 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 +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 } -function lab_plate_load() -> void { - let m = gltf_load(lab_plate_dir(), "plate.gltf", "plate") +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") if m == null { return } - 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) + 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) } # the ground under (x, z): the plate is flat at 0 export function lab_ground_y(x: float, z: float) -> float { return 0.0 } @@ -27,10 +25,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(dir: string, file: string, node: string, x: float, z: float, yaw: float) -> Actor { - let m = gltf_load(dir, file, node) +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) if m == null { return null } - let a = actor_new(m) + let a = actor_new(render3d_st, m) lab_stand(a, x, z, yaw) return a } diff --git a/packages/ludic.lab/png.ludic b/packages/ludic.lab/png.ludic index 2e4c9534..bd5ed1f6 100644 --- a/packages/ludic.lab/png.ludic +++ b/packages/ludic.lab/png.ludic @@ -1,28 +1,27 @@ # 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() -> void { - if lp_table != null { return } - lp_table = new []long +function lp_table_init(lab_st: mut LabState) -> void { + if lab_st.lp_table != null { return } + lab_st.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(lp_table, c) + push(lab_st.lp_table, c) } } # CRC-32 of out[from .. from + n) -function lp_crc(out: []byte, from: int, n: int) -> long { - lp_table_init() +function lp_crc(lab_st: mut LabState, out: []byte, from: int, n: int) -> long { + lp_table_init(lab_st) let mask: long = 4294967295 var c = mask for i in from .. from + n { let k = int((c ^ long(out[i])) & long(255)) - c = lp_table[k] ^ (c >> long(8)) + c = lab_st.lp_table[k] ^ (c >> long(8)) } return c ^ mask } @@ -36,9 +35,7 @@ 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 -var lp_w: int = 0 -var lp_h: int = 0 -function lp_ppm_header(d: []byte) -> int { +function lp_ppm_header(lab_st: mut LabState, d: []byte) -> int { var i = 2 var got = 0 var vals = new []int @@ -52,17 +49,17 @@ function lp_ppm_header(d: []byte) -> int { push(vals, v) got += 1 } - lp_w = vals[0] - lp_h = vals[1] + lab_st.lp_w = vals[0] + lab_st.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(ppm: string, png: string) -> bool { +export function lab_ppm_to_png(lab_st: mut LabState, 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(d) - let w = lp_w - let h = lp_h + let px = lp_ppm_header(lab_st, d) + let w = lab_st.lp_w + let h = lab_st.lp_h let row = w * 3 + 1 let raw = row * h let blocks = (raw + 65534) / 65535 @@ -81,7 +78,7 @@ export function lab_ppm_to_png(ppm: string, png: string) -> bool { out[at + 18] = 0 out[at + 19] = 0 out[at + 20] = 0 - lp_put32(out, at + 21, lp_crc(out, at + 4, 17)) + lp_put32(out, at + 21, lp_crc(lab_st, out, at + 4, 17)) at += 25 lp_put32(out, at, long(idat)) lp_tag(out, at + 4, "IDAT") @@ -118,11 +115,11 @@ export function lab_ppm_to_png(ppm: string, png: string) -> bool { } lp_put32(out, o, long(b) * long(65536) + long(a)) o += 4 - lp_put32(out, o, lp_crc(out, at + 4, o - at - 4)) + lp_put32(out, o, lp_crc(lab_st, 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(out, o + 4, 4)) + lp_put32(out, o + 8, lp_crc(lab_st, 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 adfd6f55..4025a9c9 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() -> void { - lb_shot_names = new []string - lb_shot_pose = new []float +function lab_shots_reset(lab_st: mut LabState) -> void { + lab_st.lb_shot_names = new []string + lab_st.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(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) +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) } # 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(name: string, tx: float, ty: float, tz: float, dist: float, from_deg: float, up_deg: float) -> void { +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 { 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(name: string, tx: float, ty: float, tz: float, dist: 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(name, cx, cy, cz, yaw, -up_deg) + lab_shot(lab_st, name, cx, cy, cz, yaw, -up_deg) } -export function lab_shot_count() -> int { - if lb_shot_names == null { return 0 } - return len(lb_shot_names) +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) } -function lab_shot_apply(i: int) -> void { - if i < 0 or i >= lab_shot_count() { return } +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 } let o = i * 5 - 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]) + 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]) } diff --git a/packages/ludic.lab/state.ludic b/packages/ludic.lab/state.ludic index b0b667b8..affac690 100644 --- a/packages/ludic.lab/state.ludic +++ b/packages/ludic.lab/state.ludic @@ -1,27 +1,34 @@ # state.ludic - which scene, its shots, and where the loop is in them -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 +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 +} # frames a shot settles before it is taken (streaming, auto-exposure); 45 by default -export function lab_settle(frames: int) -> void { lb_settle = Math.max(1, frames) } +export function lab_settle(lab_st: mut LabState, frames: int) -> void { lab_st.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(dir: string) -> void { lb_font = dir } -export function lab_title(title: string) -> void { lb_title = title } +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 } # one line under the scene's title in a window -export function lab_note(s: string) -> void { lb_note = s } +export function lab_note(lab_st: mut LabState, s: string) -> void { lab_st.lb_note = s } # the scene being shown, and the frame the clock is on (60 a second, whatever the machine) -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 } +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 } diff --git a/packages/ludic.lab/tests/lab_test.ludic b/packages/ludic.lab/tests/lab_test.ludic index 05e0af6f..48f9b4fa 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" { + test "a PPM becomes a PNG with its signature, its size and a valid IEND" (lab_st: mut LabState) { let dir = Os.temp_dir() ppm(`{dir}/lab_test.ppm`) - expect(lab_ppm_to_png(`{dir}/lab_test.ppm`, `{dir}/lab_test.png`)) + expect(lab_ppm_to_png(lab_st, `{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_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 "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 "scenes are what the program added, in its order" { diff --git a/packages/ludic.minimap/explored.ludic b/packages/ludic.minimap/explored.ludic index 75cdc591..e0445382 100644 --- a/packages/ludic.minimap/explored.ludic +++ b/packages/ludic.minimap/explored.ludic @@ -1,100 +1,97 @@ # 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(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)) +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)) for j in cj - reach .. cj + reach + 1 { for i in ci - reach .. ci + reach + 1 { - 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 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 c >= 0 { minimap_reveal_cell(c) } + if c >= 0 { minimap_reveal_cell(minimap_st, c) } } -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_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_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(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_at(x: float, z: float) -> bool { return minimap_seen(minimap_cell(x, z)) } +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_count() -> int { +export function minimap_seen_count(minimap_st: MinimapState) -> int { var n = 0 - if mm_seen == null { return 0 } - for c in 0 .. len(mm_seen) { if mm_seen[c] != 0 { n += 1 } } + 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 } } 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() -> int { return mm_version } +export function minimap_version(minimap_st: MinimapState) -> int { return minimap_st.mm_version } -export function minimap_explored_pct() -> int { - mm_land_count() +export function minimap_explored_pct(minimap_st: mut MinimapState) -> int { + mm_land_count(minimap_st) var n = 0 - 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 } + 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 } } } - return Math.min(n * 100 / mm_land_n, 100) + return Math.min(n * 100 / minimap_st.mm_land_n, 100) } -function mm_land(i: int, j: int) -> bool { return MinimapWorld.land(minimap_cell_x(i), minimap_cell_z(j)) } +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_count() -> void { +function mm_land_count(minimap_st: mut MinimapState) -> void { let ox = MinimapWorld.origin_x() let oz = MinimapWorld.origin_z() - 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 } } + 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 } } } - mm_land_n = Math.max(mm_land_n, 1) + minimap_st.mm_land_n = Math.max(minimap_st.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() -> void { mm_land_n = 0 } +export function minimap_land_changed(minimap_st: mut MinimapState) -> void { minimap_st.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() -> string { - mm_grid() +export function minimap_seen_text(minimap_st: mut MinimapState) -> string { + mm_grid(minimap_st) var s = "" - for j in 0 .. mm_cells { + for j in 0 .. minimap_st.mm_cells { var row = "" - for i in 0 .. mm_cells { if mm_seen[j * mm_cells + i] != 0 { row = row + "1" } else { row = row + "0" } } + 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" } } s = s + row } return s } -export function minimap_seen_from_text(s: string) -> void { - mm_grid() +export function minimap_seen_from_text(minimap_st: mut MinimapState, s: string) -> void { + mm_grid(minimap_st) let n = len(s) - for c in 0 .. mm_cells * mm_cells { - mm_seen[c] = 0 - if c < n and s[c] == 49 { mm_seen[c] = 1 } + 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 } } - mm_version += 1 + minimap_st.mm_version += 1 } diff --git a/packages/ludic.minimap/grid.ludic b/packages/ludic.minimap/grid.ludic index 5483dd60..fb0689a0 100644 --- a/packages/ludic.minimap/grid.ludic +++ b/packages/ludic.minimap/grid.ludic @@ -1,47 +1,76 @@ # 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) -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 +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 +} # 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(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(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_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_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_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 } +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 } # the grid's west and north edges, and a column's or a row's -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 } +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 } # the cell a point is in, -1 off the grid -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 +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 } -function mm_grid() -> void { if mm_seen == null { minimap_config(mm_cells, mm_cell_m) } } +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) } } diff --git a/packages/ludic.minimap/pin_edit.ludic b/packages/ludic.minimap/pin_edit.ludic index 12f4c715..e012faeb 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(i: int) -> void { - if i < 0 or i >= minimap_pin_count() { return } +export function minimap_pin_remove(minimap_st: mut MinimapState, i: int) -> void { + if i < 0 or i >= minimap_pin_count(minimap_st) { return } let rest = new []MinimapPin - 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 } + 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 } } -export function minimap_pin_set_label(i: int, s: string) -> void { if i >= 0 and i < minimap_pin_count() { mm_pins[i].label = s } } +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 } } # the symbol chosen is the one the next mark takes -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_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_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_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_last_icon() -> int { return mm_last } +export function minimap_pin_last_icon(minimap_st: MinimapState) -> int { return minimap_st.mm_last } # the mark the map's legend has open, -1 none -export function minimap_pin_open() -> int { - if mm_open >= minimap_pin_count() { mm_open = -1 } - return mm_open +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_select(i: int) -> void { - mm_open = -1 - if i >= 0 and i < minimap_pin_count() { mm_open = i } +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 } } # the mark the compass carries, -1 none -export function minimap_pin_followed() -> int { - if mm_followed >= minimap_pin_count() { mm_followed = -1 } - return mm_followed +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_follow(i: int) -> void { - mm_followed = -1 - if i >= 0 and i < minimap_pin_count() { mm_followed = i } +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 } } # follow the open mark, or stop following it -export function minimap_pin_follow_toggle() -> void { - let i = minimap_pin_open() +export function minimap_pin_follow_toggle(minimap_st: mut MinimapState) -> void { + let i = minimap_pin_open(minimap_st) if i < 0 { return } - if mm_followed == i { mm_followed = -1 } else { mm_followed = i } + if minimap_st.mm_followed == i { minimap_st.mm_followed = -1 } else { minimap_st.mm_followed = i } } -export function minimap_placing() -> bool { return mm_placing } +export function minimap_placing(minimap_st: MinimapState) -> bool { return minimap_st.mm_placing } # the next press on the map drops a mark; asked again, it does not -export function minimap_place_toggle() -> void { - mm_placing = not mm_placing - mm_open = -1 +export function minimap_place_toggle(minimap_st: mut MinimapState) -> void { + minimap_st.mm_placing = not minimap_st.mm_placing + minimap_st.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(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 } +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 } return i } diff --git a/packages/ludic.minimap/pins.ludic b/packages/ludic.minimap/pins.ludic index f187da77..c6994dea 100644 --- a/packages/ludic.minimap/pins.ludic +++ b/packages/ludic.minimap/pins.ludic @@ -10,83 +10,75 @@ 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(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_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_pins_clear() -> void { - mm_pins = new []MinimapPin - mm_open = -1 - mm_followed = -1 - mm_last = 0 - mm_placing = false +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_pin_count() -> int { - if mm_pins == null { return 0 } - return len(mm_pins) +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_max() -> int { return mm_pin_max } -export function minimap_pin_full() -> bool { return minimap_pin_count() >= mm_pin_max } +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 } # a mark at (x, z) made today, with the colour 0; -1 when the map holds all it will -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 } +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 } let p = new MinimapPin p.x = x p.z = z - p.icon = Math.clamp(icon, 0, mm_symbols - 1) + p.icon = Math.clamp(icon, 0, minimap_st.mm_symbols - 1) p.day = MinimapWorld.day() p.label = label - push(mm_pins, p) - mm_last = p.icon - return len(mm_pins) - 1 + push(minimap_st.mm_pins, p) + minimap_st.mm_last = p.icon + return len(minimap_st.mm_pins) - 1 } # as saved: everything given, the symbol last chosen left alone -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 } +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 } let p = new MinimapPin p.x = x p.z = z - p.icon = Math.clamp(icon, 0, mm_symbols - 1) - p.colour = Math.clamp(colour, 0, mm_colours - 1) + p.icon = Math.clamp(icon, 0, minimap_st.mm_symbols - 1) + p.colour = Math.clamp(colour, 0, minimap_st.mm_colours - 1) p.day = day p.label = label - push(mm_pins, p) - return len(mm_pins) - 1 + push(minimap_st.mm_pins, p) + return len(minimap_st.mm_pins) - 1 } # the i-th mark, or an empty one past the end -export function minimap_pin(i: int) -> MinimapPin { - if i < 0 or i >= minimap_pin_count() { return new MinimapPin } - return mm_pins[i] +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] } # the nearest n marks within `within` of (x, z), nearest first, leaving out the followed one -export function minimap_pins_near(x: float, z: float, n: int, within: float) -> []int { +export function minimap_pins_near(minimap_st: MinimapState, 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() { - 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) + 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) if d < bd { bd = d best = i diff --git a/packages/ludic.minimap/system.ludic b/packages/ludic.minimap/system.ludic index ff87652b..4c542a3e 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() -> 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_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_save() -> Val { +export function minimap_save(minimap_st: mut MinimapState) -> Val { let v = Value.object() - sv_put_str(v, "seen", minimap_seen_text()) + sv_put_str(v, "seen", minimap_seen_text(minimap_st)) let l = Value.list() - for i in 0 .. minimap_pin_count() { - let p = mm_pins[i] + for i in 0 .. minimap_pin_count(minimap_st) { + let p = minimap_st.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() -> Val { Value.add(l, o) } Value.put(v, "pins", l) - sv_put_int(v, "followed", minimap_pin_followed()) - sv_put_int(v, "last", mm_last) + sv_put_int(v, "followed", minimap_pin_followed(minimap_st)) + sv_put_int(v, "last", minimap_st.mm_last) return v } -export function minimap_load(v: Val, version: int) -> void { - minimap_reset() - minimap_seen_from_text(sv_str(v, "seen", "")) +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", "")) 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(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(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_follow(sv_int(v, "followed", -1)) - mm_last = Math.clamp(sv_int(v, "last", 0), 0, mm_symbols - 1) + 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) } export function minimap_system() -> System { diff --git a/packages/ludic.minimap/tests/minimap_test.ludic b/packages/ludic.minimap/tests/minimap_test.ludic index 9040ccba..f54ba6ba 100644 --- a/packages/ludic.minimap/tests/minimap_test.ludic +++ b/packages/ludic.minimap/tests/minimap_test.ludic @@ -6,187 +6,189 @@ import "ludic.base" program MinimapTest { numbers float - var you_x: float = 100.0 - var you_z: float = 100.0 - var day: int = 3 + state MinimaptestState { + you_x: float = 100.0 + you_z: float = 100.0 + 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() -> float { return you_x } - function fk_you_z() -> float { return you_z } + 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_camp() -> float { return 120.0 } - function fk_day() -> int { return day } + function fk_day(minimaptest_st: MinimaptestState) -> int { return minimaptest_st.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() -> 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 + 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 } - 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 "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 "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 "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 "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 "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 "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 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 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) + 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) 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" { - 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 "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 "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") + 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") expect_eq(i, 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) + 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) } - 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 "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 "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() + 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) 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 69918483..7651a401 100644 --- a/packages/ludic.minimap/view.ludic +++ b/packages/ludic.minimap/view.ludic @@ -1,81 +1,70 @@ # 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(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 +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 } # where the window is on the screen this frame -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_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_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 } +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 } # the metres the window shows across at the zoom -export function minimap_span() -> float { return mm_span / mm_zoom } +export function minimap_span(minimap_st: MinimapState) -> float { return minimap_st.mm_span / minimap_st.mm_zoom } # pixels per metre -export function minimap_scale() -> float { return mm_size / minimap_span() } +export function minimap_scale(minimap_st: MinimapState) -> float { return minimap_st.mm_size / minimap_span(minimap_st) } # a world point on the screen -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() } +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) } # the world under a point of the window (pixels from its corner) -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() } +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) } # a round distance whose bar is no wider than a quarter of the window -export function minimap_scale_metres() -> int { +export function minimap_scale_metres(minimap_st: MinimapState) -> int { let steps = [2000, 1000, 500, 250, 100, 50, 25] - for k in 0 .. len(steps) { if float(steps[k]) * minimap_scale() <= mm_size / 4.0 { return steps[k] } } + for k in 0 .. len(steps) { if float(steps[k]) * minimap_scale(minimap_st) <= minimap_st.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() -> 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_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_centre(x: float, z: float) -> void { - mm_vx = x - mm_vz = z - mm_have_view = true - minimap_clamp() +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) } # the view on the camp, or on you -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()) } +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()) } } # go there AND close in; never out, since a player already closer asked for that -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) } +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) } } diff --git a/packages/ludic.minimap/view_moves.ludic b/packages/ludic.minimap/view_moves.ludic index 8f654bb7..a9eb4e20 100644 --- a/packages/ludic.minimap/view_moves.ludic +++ b/packages/ludic.minimap/view_moves.ludic @@ -1,61 +1,56 @@ # 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(z: float) -> void { - mm_zoom = Math.clamp(z, mm_zoom_min, mm_zoom_max) - minimap_clamp() +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) } # a step in (dir > 0) or out, a third at a time -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) } +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) } } # zoom by `factor` about a point of the window -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_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_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 +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 } # the point pressed follows the pointer -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_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_end() -> void { mm_panning = false } -export function minimap_panning() -> bool { return mm_panning } +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 } # 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(rx: float, ry: float, r: float) -> int { - let sx = mm_x0 + rx - let sy = mm_y0 + ry +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 var hit = -1 - for i in 0 .. minimap_pin_count() { - let px = minimap_px(mm_pins[i].x) - let py = minimap_py(mm_pins[i].z) + 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) if sx >= px - r and sx < px + r and sy >= py - r and sy < py + r { hit = i } } - if hit >= 0 { mm_open = hit } + if hit >= 0 { minimap_st.mm_open = hit } return hit } diff --git a/packages/ludic.needs/queries.ludic b/packages/ludic.needs/queries.ludic index 25761ccc..771b7cef 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(k: int) -> float { +export function needs_level(needs_st: mut NeedsState, k: int) -> float { if not nd_ok(k) { return 0.0 } - return nd_lv()[k] + return nd_lv(needs_st)[k] } -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] } +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] } # seconds left before a collapse (0: no countdown), and the need it is for -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 } +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 } # the walk's pace, times: the ramp of each need, and 0.6 more on last legs -export function needs_pace() -> float { - let lv = nd_lv() +export function needs_pace(needs_st: mut NeedsState) -> float { + let lv = nd_lv(needs_st) var m = nd_pace(lv[NEED_WARMTH]) * nd_pace(lv[NEED_FOOD]) if NeedsWorld.thirst() { m = m * nd_pace(lv[NEED_WATER]) } - if nd_legs > 0.0 { m = m * 0.6 } + if needs_st.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() -> bool { - let lv = nd_lv() +export function needs_can_run(needs_st: mut NeedsState) -> bool { + let lv = nd_lv(needs_st) 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() -> float { - let lv = nd_lv() +export function needs_lowest(needs_st: mut NeedsState) -> float { + let lv = nd_lv(needs_st) 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 5585eff6..c6293a90 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(k: int, d: float) -> void { - let lv = nd_lv() +function nd_add(needs_st: mut NeedsState, k: int, d: float) -> void { + let lv = nd_lv(needs_st) lv[k] = Math.clamp(lv[k] + d, 0.0, 100.0) } -export function needs_run(gh: float) -> void { +export function needs_run(needs_st: mut NeedsState, gh: float) -> void { let a = NeedsWorld.activity() - nd_add(NEED_FOOD, (NeedsWorld.bonus(NEED_FOOD) - nd_food_rate(a) * NeedsWorld.drain(NEED_FOOD)) * gh) + nd_add(needs_st, NEED_FOOD, (NeedsWorld.bonus(NEED_FOOD) - nd_food_rate(a) * NeedsWorld.drain(NEED_FOOD)) * gh) if NeedsWorld.thirst() { - 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) + 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) } diff --git a/packages/ludic.needs/state.ludic b/packages/ludic.needs/state.ludic index 33d88d6c..e3230d55 100644 --- a/packages/ludic.needs/state.ludic +++ b/packages/ludic.needs/state.ludic @@ -51,38 +51,40 @@ export property NeedFact { need: int = 0 } -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() -> []float { - if nd_level == null { - nd_level = floats(NEED_COUNT) - nd_fill() - } - return nd_level +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 } -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 +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) + } + return needs_st.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 } # the facts, oldest first; the game drains them into its notices and its collapse -export function needs_facts() -> Queue { - if nd_facts == null { nd_facts = queue_new("needs.facts") } - return nd_facts +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 } -function nd_fact(what: int, need: int) -> void { +function nd_fact(base_st: mut BaseState, needs_st: mut NeedsState, what: int, need: int) -> void { let f = new NeedFact f.what = what f.need = need - q_push(needs_facts(), f) + q_push(base_st, needs_facts(base_st, needs_st), 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 365282d1..6d1beb82 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(t: Tick) -> void { - needs_run(t.hours) - needs_watch(t.dt) +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) } -export function needs_save() -> Val { +export function needs_save(needs_st: mut NeedsState) -> Val { let v = Value.object() - 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) + 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) return v } -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_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_system() -> System { diff --git a/packages/ludic.needs/tests/needs_test.ludic b/packages/ludic.needs/tests/needs_test.ludic index 334728b2..2eb87f7a 100644 --- a/packages/ludic.needs/tests/needs_test.ludic +++ b/packages/ludic.needs/tests/needs_test.ludic @@ -4,29 +4,31 @@ import "ludic.needs" import "ludic.base" program NeedsTest { numbers float - 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 + 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 + } - 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 } + 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 } return 1.0 } - function fake_bonus(k: int) -> float { - if k == NEED_WARMTH { return warm_bonus } + function fake_bonus(needstest_st: NeedstestState, k: int) -> float { + if k == NEED_WARMTH { return needstest_st.warm_bonus } return 0.0 } @@ -42,169 +44,169 @@ program NeedsTest { bonus: fn fake_bonus } - 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()) } + 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)) } - 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 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 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 "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 "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 "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 "thirst off holds water full" { - fresh() - thirst = false - needs_set(NEED_WATER, 10.0) - hours(1.0) - expect(needs_water() == 100.0) + 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 "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 "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 "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 "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 "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 "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 "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 "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 "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() + 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) expect_eq(len(a), 1) expect_eq(a[0].what, NEEDS_CRITICAL) expect_eq(a[0].need, NEED_WATER) - expect(needs_last_legs() > 0.0) - expect(needs_pace() < 0.6) + expect(needs_last_legs(needs_st) > 0.0) + expect(needs_pace(needs_st) < 0.6) var i = 0 - while q_len(needs_facts()) == 0 and i < 200 { - needs_watch(0.1) + while q_len(needs_facts(base_st, needs_st)) == 0 and i < 200 { + needs_watch(base_st, needs_st, 0.1) i += 1 } - let b = facts() + let b = facts(base_st, needs_st) 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(), 0) - needs_went_down() - expect_eq(needs_collapses(), 1) + expect_eq(needs_collapses(needs_st), 0) + needs_went_down(needs_st) + expect_eq(needs_collapses(needs_st), 1) } - 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() + 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) expect_eq(len(f), 2) expect_eq(f[1].what, NEEDS_RESTORED) - expect(needs_last_legs() == 0.0) + expect(needs_last_legs(needs_st) == 0.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 "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 "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 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 save section reads back the same body" { - fresh() - needs_set(NEED_FOOD, 33.5) - needs_set(NEED_WARMTH, 12.25) - needs_went_down() + 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) let root = save_tree() - save_section(root, "needs", 1, needs_save()) + save_section(root, "needs", 1, needs_save(needs_st)) let text = save_encode(root) - needs_reset() + needs_reset(base_st, needs_st) let n = load_section(save_decode(text), "needs") - needs_load(n.data, n.version) - expect(needs_food() == 33.5) - expect(needs_warmth() == 12.25) - expect_eq(needs_collapses(), 1) + 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) } } diff --git a/packages/ludic.needs/verbs.ludic b/packages/ludic.needs/verbs.ludic index d0632389..cefbdf5a 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(k: int, n: float) -> void { - if nd_ok(k) { nd_add(k, n) } +export function needs_feed(needs_st: mut NeedsState, k: int, n: float) -> void { + if nd_ok(k) { nd_add(needs_st, k, n) } } # a meal: food, water and energy at once -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_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_set(k: int, v: float) -> void { - if nd_ok(k) { nd_lv()[k] = Math.clamp(v, 0.0, 100.0) } +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) } } # at least `v` -export function needs_floor(k: int, v: float) -> void { - if nd_ok(k) and nd_lv()[k] < v { needs_set(k, 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) } } # a night: rested to what it was worth, hungrier and thirstier for it, but never empty -export function needs_after_night(rested: float, food_lost: float, water_lost: float) -> void { - let lv = nd_lv() +export function needs_after_night(needs_st: mut NeedsState, rested: float, food_lost: float, water_lost: float) -> void { + let lv = nd_lv(needs_st) 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() -> void { nd_legs = 0.0 } +export function needs_calm(needs_st: mut NeedsState) -> void { needs_st.nd_legs = 0.0 } # the body went down, however: one more collapse, and no countdown running -export function needs_went_down() -> void { - nd_collapses += 1 - nd_legs = 0.0 +export function needs_went_down(needs_st: mut NeedsState) -> void { + needs_st.nd_collapses += 1 + needs_st.nd_legs = 0.0 } # a hand up: the cost is in the body, and nothing goes below twenty -export function needs_helped_up() -> void { - let lv = nd_lv() +export function needs_helped_up(needs_st: mut NeedsState) -> void { + let lv = nd_lv(needs_st) 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) - nd_legs = 0.0 + needs_st.nd_legs = 0.0 } # a new trip: fed, watered, rested and warm, and never walked out -export function needs_reset() -> void { - nd_lv() - nd_fill() - nd_legs = 0.0 - nd_last = 0 - nd_collapses = 0 - q_clear(needs_facts()) +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)) } diff --git a/packages/ludic.needs/watch.ludic b/packages/ludic.needs/watch.ludic index 22f86f57..d32b12b4 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() -> int { - let lv = nd_lv() +function nd_empty(needs_st: mut NeedsState) -> int { + let lv = nd_lv(needs_st) 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() -> bool { - let lv = nd_lv() +function nd_clear(needs_st: mut NeedsState) -> bool { + let lv = nd_lv(needs_st) 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() -> void { - let lv = nd_lv() +function nd_gentle(needs_st: mut NeedsState) -> void { + let lv = nd_lv(needs_st) 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) - nd_legs = 0.0 + needs_st.nd_legs = 0.0 } # `dt` real seconds; run it after everything this frame that could fill a need -export function needs_watch(dt: float) -> void { +export function needs_watch(base_st: mut BaseState, needs_st: mut NeedsState, dt: float) -> void { if NeedsWorld.held() { return } if NeedsWorld.gentle() { - nd_gentle() + nd_gentle(needs_st) return } - let need = nd_empty() + let need = nd_empty(needs_st) if need < 0 { - if nd_legs > 0.0 and nd_clear() { - nd_legs = 0.0 - nd_fact(NEEDS_RESTORED, nd_last) + 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) } return } - if nd_legs == 0.0 { - nd_legs = LAST_LEGS - nd_last = need - nd_fact(NEEDS_CRITICAL, need) + 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) return } - nd_legs = nd_legs - dt - if nd_legs > 0.0 { return } - nd_legs = 0.0 - nd_fact(NEEDS_COLLAPSED, nd_last) + 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) } diff --git a/packages/ludic.net/codec.ludic b/packages/ludic.net/codec.ludic index f280ab83..67aae75d 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() -> void { - net__init() - net__wl = 0 +export function nw_begin(net_st: mut NetState) -> void { + net__init(net_st) + net_st.net__wl = 0 } -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_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_b(v: bool) -> void { if v { nw_i(1) } else { nw_i(0) } } +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_f(v: float) -> void { nw_i(float_bits(v)) } +export function nw_f(net_st: mut NetState, v: float) -> void { nw_i(net_st, 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(s: string) -> void { nw_text(s, 200) } +export function nw_s(net_st: mut NetState, s: string) -> void { nw_text(net_st, s, 200) } -export function nw_text(s: string, cap: int) -> void { +export function nw_text(net_st: mut NetState, s: string, cap: int) -> void { var n = len(s) if n > cap { n = cap } - nw_i(n) + nw_i(net_st, n) for i in 0 .. n { - if net__wl < NET_MTU { net__wb[net__wl] = s[i] } - net__wl += 1 + if net_st.net__wl < NET_MTU { net_st.net__wb[net_st.net__wl] = s[i] } + net_st.net__wl += 1 } } -export function nw_len() -> int { return net__wl } +export function nw_len(net_st: NetState) -> int { return net_st.net__wl } # the reader: a short or bad message reads as zeros and sets nr_bad() -export function nr_i() -> int { - if net__rp + 4 > net__rl { - net__rbad = true +export function nr_i(net_st: mut NetState) -> int { + if net_st.net__rp + 4 > net_st.net__rl { + net_st.net__rbad = true return 0 } - let v = nb_get32(net__rb, net__rp) - net__rp += 4 + let v = nb_get32(net_st.net__rb, net_st.net__rp) + net_st.net__rp += 4 return v } -export function nr_b() -> bool { return nr_i() != 0 } +export function nr_b(net_st: mut NetState) -> bool { return nr_i(net_st) != 0 } -export function nr_f() -> float { return float_from_bits(nr_i()) } +export function nr_f(net_st: mut NetState) -> float { return float_from_bits(nr_i(net_st)) } -export function nr_s() -> string { return nr_text(200) } +export function nr_s(net_st: mut NetState) -> string { return nr_text(net_st, 200) } -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 +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 return "" } - 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) + 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) } -export function nr_bad() -> bool { return net__rbad } +export function nr_bad(net_st: NetState) -> bool { return net_st.net__rbad } # a message that came in, made the one nr_* reads -export function net_read(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 +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 } # the message just written, as if it had come in (a loopback: a test, a host telling itself) -export function net_written() -> NetMessage { - net__init() +export function net_written(net_st: mut NetState) -> NetMessage { + net__init(net_st) let m = new NetMessage - m.from = net__my_pid - m.len = min(net__wl, NET_MTU) + m.from = net_st.net__my_pid + m.len = min(net_st.net__wl, NET_MTU) m.data = buffer(m.len) - nb_copy(m.data, 0, net__wb, 0, m.len) + nb_copy(m.data, 0, net_st.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() -> void { - nw_begin() - nb_copy(net__wb, 0, net__rb, 0, net__rl) - net__wl = net__rl +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 } diff --git a/packages/ludic.net/codes.ludic b/packages/ludic.net/codes.ludic index 0e2aed8d..2f9e2813 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() -> string { +export function net_join_code(net_st: NetState) -> string { var ip = NetSocket.local_ip() if ip == 0 { ip = Udp.ip("127.0.0.1") } - return net_code_of(ip, net_port()) + return net_code_of(ip, net_port(net_st)) } 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(text: string) -> bool { +export function net_parse_code(net_st: mut NetState, text: string) -> bool { let p = text var colon = -1 var dots = 0 @@ -53,7 +53,7 @@ export function net_parse_code(text: string) -> bool { if p[i] == ':' { colon = i } if p[i] == '.' { dots += 1 } } - if dots == 3 { return net__parse_addr(text, colon) } + if dots == 3 { return net__parse_addr(net_st, text, colon) } var hi = 0 var lo = 0 var n = 0 @@ -72,24 +72,24 @@ export function net_parse_code(text: string) -> bool { n += 1 } if n != 10 { return false } - net__code_ip = lo - net__code_port = hi - return net__code_ip != 0 and net__code_port > 0 + 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 } -function net__parse_addr(text: string, colon: int) -> bool { +function net__parse_addr(net_st: mut NetState, text: string, colon: int) -> bool { var host = text - net__code_port = net__conf().port + net_st.net__code_port = net__conf(net_st).port if colon > 0 { host = text[0..colon] - net__code_port = Text.to_int(text[colon + 1..len(text)]) + net_st.net__code_port = Text.to_int(text[colon + 1..len(text)]) } - net__code_ip = Udp.ip(host) - return net__code_ip != 0 and net__code_port > 0 and net__code_port < 65536 + 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 } -export function net_code_ip() -> int { return net__code_ip } -export function net_code_port() -> int { return net__code_port } +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 } # 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 8706d345..86ccc9c7 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(ip: int, port: int) -> int { +function net__find(net_st: NetState, ip: int, port: int) -> int { for i in 0 .. NET_PEERS { - let p = net__peers[i] + let p = net_st.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(pid: int) -> int { - if net__peers == null { return -1 } +export function np_of_pid(net_st: NetState, pid: int) -> int { + if net_st.net__peers == null { return -1 } for i in 0 .. NET_PEERS { - let p = net__peers[i] + let p = net_st.net__peers[i] if p.on and p.pid == pid { return i } } return -1 } -function net__open(slot: int, ip: int, port: int, pid: int) -> void { - let p = net__peers[slot] +function net__open(net_st: NetState, slot: int, ip: int, port: int, pid: int) -> void { + let p = net_st.net__peers[slot] p.on = true p.ip = ip p.port = port p.pid = pid - p.heard = net__time + p.heard = net_st.net__time p.next_id = 1 p.recv_id = 0 p.acked = 0 @@ -32,29 +32,29 @@ function net__open(slot: int, ip: int, port: int, pid: int) -> void { p.closing = false } -export function np_close(slot: int) -> void { - if net__peers == null or slot < 0 or slot >= NET_PEERS { return } - let p = net__peers[slot] +export function np_close(net_st: 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] 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(slot: int, kind: int, rel: bool) -> void { - if net__peers == null or slot < 0 or slot >= NET_PEERS { return } - let p = net__peers[slot] +export function np_queue(net_st: 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] if not p.on { return } let m = new NetMsg m.kind = kind m.rel = rel m.sent = -1.0 - m.made = net__time - var n = net__wl + m.made = net_st.net__time + var n = net_st.net__wl if n > NET_MTU - 40 { n = NET_MTU - 40 } m.len = n m.data = buffer(n) - nb_copy(m.data, 0, net__wb, 0, n) + nb_copy(m.data, 0, net_st.net__wb, 0, n) if rel { m.id = p.next_id p.next_id += 1 @@ -63,48 +63,48 @@ export function np_queue(slot: int, kind: int, rel: bool) -> void { } # to everyone this machine talks to (a host: every guest but `except`; a guest: the host) -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 np_queue_all(net_st: 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 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_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_pid(slot: int) -> int { - if not net_peer_on(slot) { return 0 } - return net__peers[slot].pid +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_ip(slot: int) -> int { - if not net_peer_on(slot) { return 0 } - return net__peers[slot].ip +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_port(slot: int) -> int { - if not net_peer_on(slot) { return 0 } - return net__peers[slot].port +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_heard(slot: int) -> float { - if not net_peer_on(slot) { return 0.0 } - return net__peers[slot].heard +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 } # let the slot go once it has taken everything queued for it so far, or after `secs` -export function net_close_when_sent(slot: int, secs: float) -> void { - if not net_peer_on(slot) { return } - let p = net__peers[slot] +export function net_close_when_sent(net_st: NetState, slot: int, secs: float) -> void { + if not net_peer_on(net_st, slot) { return } + let p = net_st.net__peers[slot] p.closing = true p.close_id = p.next_id - 1 - p.close_t = net__time + secs + p.close_t = net_st.net__time + secs } # the slots in use (a host's count of guests, a welcome not yet sent included) -export function net_peer_count() -> int { +export function net_peer_count(net_st: NetState) -> int { var n = 0 - for i in 0 .. NET_PEERS { if net_peer_on(i) { n += 1 } } + for i in 0 .. NET_PEERS { if net_peer_on(net_st, i) { n += 1 } } return n } diff --git a/packages/ludic.net/ports.ludic b/packages/ludic.net/ports.ludic index 742522fd..82bd1f90 100644 --- a/packages/ludic.net/ports.ludic +++ b/packages/ludic.net/ports.ludic @@ -58,29 +58,27 @@ 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(c: NetConfig) -> void { net__cfg = c } +export function net_config(net_st: mut NetState, c: NetConfig) -> void { net_st.net__cfg = c } -function net__conf() -> NetConfig { - if net__cfg == null { net__cfg = new NetConfig } - return net__cfg +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 } -export function net_facts() -> Queue { - if net__facts == null { net__facts = queue_new("net.facts") } - return net__facts +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 } -function net__fact(what: int, slot: int, pid: int, why: int, text: string) -> void { +function net__fact(base_st: mut BaseState, net_st: mut NetState, 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(net_facts(), f) + q_push(base_st, net_facts(base_st, net_st), f) } function net__no_log(s: string) -> void { } diff --git a/packages/ludic.net/punch.ludic b/packages/ludic.net/punch.ludic index 91811b69..4e95e742 100644 --- a/packages/ludic.net/punch.ludic +++ b/packages/ludic.net/punch.ludic @@ -1,57 +1,51 @@ # 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(pub: string, local: string) -> void { +function net__new_guest(net_st: mut NetState, pub: string, local: string) -> void { let key = `{pub}|{local}` - 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) } + 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) } } -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 } +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 } } for k in 0 .. 16 { - if net__punch_left[k] > 0 { continue } - net__punch_ip[k] = ip - net__punch_port[k] = port - net__punch_left[k] = 10 + 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 return } } # twice a second while a guest is new: a packet the guest's side ignores, which is the point -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') +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') } for k in 0 .. 16 { - 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 + 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 } } -function net__punch_clear() -> void { - if net__punch_left != null { for k in 0 .. 16 { net__punch_left[k] = 0 } } +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 } } } diff --git a/packages/ludic.net/queries.ludic b/packages/ludic.net/queries.ludic index d075114d..6e258224 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() -> 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) +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) } diff --git a/packages/ludic.net/receive.ludic b/packages/ludic.net/receive.ludic index 3d4c16d9..035c33a8 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() -> void { - if net__sock <= 0 { return } +function net__pump(base_st: mut BaseState, net_st: mut NetState) -> void { + if net_st.net__sock <= 0 { return } for guard in 0 .. 200 { - let n = NetSocket.recv(net__sock, net__in, NET_MTU + 64) + let n = NetSocket.recv(net_st.net__sock, net_st.net__in, NET_MTU + 64) if n <= 0 { return } - 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) } + 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) } } } -function net__packet(n: int, ip: int, port: int) -> void { +function net__packet(base_st: mut BaseState, net_st: mut NetState, 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(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 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 slot < 0 { return } - let p = net__peers[slot] - p.heard = net__time + let p = net_st.net__peers[slot] + p.heard = net_st.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__in, 12) + let ack = nb_get32(net_st.net__in, 12) if ack > p.acked and ack < p.next_id { p.acked = ack } - net__messages(slot, n) + net__messages(base_st, net_st, 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(n: int, ip: int, port: int) -> int { - let token = nb_get32(net__in, 4) - var slot = net__find(ip, port) +function net__slot_for(net_st: 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) if slot >= 0 { - if token != net__token and net__state != NET_JOINING and net__peers[slot].pid != 0 { return -1 } + if token != net_st.net__token and net_st.net__state != NET_JOINING and net_st.net__peers[slot].pid != 0 { return -1 } return slot } - if net__state != NET_HOSTING { return -1 } - if n < 28 or nb_get8(net__in, 20) != NET_HELLO { return -1 } + if net_st.net__state != NET_HOSTING { return -1 } + if n < 28 or nb_get8(net_st.net__in, 20) != NET_HELLO { return -1 } for i in 1 .. NET_PEERS { - if not net__peers[i].on { - net__open(i, ip, port, 0) + if not net_st.net__peers[i].on { + net__open(net_st, i, ip, port, 0) return i } } return -1 } -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) +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) var o = 20 for c in 0 .. count { if o + 4 > n { return } - let kind = nb_get8(net__in, o) - let rel = nb_get8(net__in, o + 1) == 1 + let kind = nb_get8(net_st.net__in, o) + let rel = nb_get8(net_st.net__in, o + 1) == 1 o += 2 var id = 0 if rel { if o + 4 > n { return } - id = nb_get32(net__in, o) + id = nb_get32(net_st.net__in, o) o += 4 } - let ml = nb_get8(net__in, o) | (nb_get8(net__in, o + 1) << 8) + let ml = nb_get8(net_st.net__in, o) | (nb_get8(net_st.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(slot: int, n: int) -> void { p.ack_due = true if id != p.recv_id + 1 { take = false } else { p.recv_id = id } } - if take { net__deliver(slot, from, kind, o, ml) } + if take { net__deliver(base_st, net_st, 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(slot: int, from: int, kind: int, o: int, ml: int) -> void { +function net__deliver(base_st: mut BaseState, net_st: mut NetState, slot: int, from: int, kind: int, o: int, ml: int) -> void { if kind == NET_BEAT { return } - if net__state == NET_HOSTING and net__peers[slot].pid < 2 and kind != NET_HELLO { return } + if net_st.net__state == NET_HOSTING and net_st.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__in, o, ml) - q_push(net_inbox(), m) + nb_copy(m.data, 0, net_st.net__in, o, ml) + q_push(base_st, net_inbox(base_st, net_st), m) } diff --git a/packages/ludic.net/relay.ludic b/packages/ludic.net/relay.ludic index 9cceee6d..cccef615 100644 --- a/packages/ludic.net/relay.ludic +++ b/packages/ludic.net/relay.ludic @@ -2,22 +2,17 @@ # 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(v: Val) -> void { +function net__take_relay(net_st: mut NetState, v: Val) -> void { let r = net__json_str(v, "relay") - if len(r) > 0 and net_parse_code(r) { - net__relay_ip = net__code_ip - net__relay_port = net__code_port + 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 } } -function net__code_bytes(b: []byte, o: int) -> void { - let cp = net__room +function net__code_bytes(net_st: NetState, b: []byte, o: int) -> void { + let cp = net_st.net__room for k in 0 .. 6 { var c = 0 if k < len(cp) { c = cp[k] } @@ -25,65 +20,65 @@ function net__code_bytes(b: []byte, o: int) -> void { } } -function net__relay_hello() -> void { +function net__relay_hello(net_st: NetState) -> 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(b, 4) - NetSocket.send(net__sock, net__relay_ip, net__relay_port, b, 10) + 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) } -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) +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) } -export function net_via_relay() -> bool { return net__via_relay } +export function net_via_relay(net_st: NetState) -> bool { return 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) +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) } # a packet of `o` bytes in net__out, to peer p: straight, or wrapped for the relay -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) } +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) } var hdr = 10 - if not net__via_relay { hdr = 6 } + if not net_st.net__via_relay { hdr = 6 } var k = o - 1 while k >= 0 { - net__out[k + hdr] = net__out[k] + net_st.net__out[k + hdr] = net_st.net__out[k] k -= 1 } - 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) + 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) } else { - 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 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 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 + 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 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(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) +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) # the host: this guest's session; a guest: the relay itself is its host - if sid > 0 { net__from_port = net__conf().relay_port0 + sid } + if sid > 0 { net_st.net__from_port = net__conf(net_st).relay_port0 + sid } return n - 6 } diff --git a/packages/ludic.net/room.ludic b/packages/ludic.net/room.ludic index 31fb2099..89602371 100644 --- a/packages/ludic.net/room.ludic +++ b/packages/ludic.net/room.ludic @@ -8,65 +8,54 @@ 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() -> 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 } +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 } # the host (after net_host or net_become_host): a room at the matchmaker, if there is one -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() +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) } # a guest by room code: the matchmaker's answer brings the host's address (the hello is set first) -export function net_room_join(code: string) -> bool { - if not net_room_on() { - net__why = NET_WHY_NO_MATCHMAKER +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 return false } - 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() + 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) return true } -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 +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 return } - net__stun_send() + net__stun_send(net_st) } -function net__room_body(role_key: string) -> string { +function net__room_body(net_st: NetState, role_key: string) -> string { var pub = "" - if net__pub_ip != 0 { pub = net__addr_text(net__pub_ip, net__pub_port) } - return "{\"" + role_key + "\": \"" + pub + "\", \"local\": \"" + net__local_text() + "\"}" + 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) + "\"}" } function net__json_str(v: Val, key: string) -> string { @@ -76,8 +65,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() -> string { +function net__local_text(net_st: NetState) -> string { var ip = NetSocket.local_ip() if ip == 0 { ip = Udp.ip("127.0.0.1") } - return net__addr_text(ip, net_port()) + return net__addr_text(ip, net_port(net_st)) } diff --git a/packages/ludic.net/room_connect.ludic b/packages/ludic.net/room_connect.ludic index 1be28dc9..722621ee 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(why: int) -> void { +function net__room_fail(base_st: mut BaseState, net_st: mut NetState, why: int) -> void { net__log(`matchmaker: {why}`) - 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) } + 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) } } # the guest: to the host's outside address, or its inside one behind the same router -function net__room_connect(host_pub: string, host_local: string) -> void { +function net__room_connect(base_st: mut BaseState, net_st: mut NetState, host_pub: string, host_local: string) -> void { var target = host_pub var same_router = false - 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 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 or same_router { target = host_local } - if not net_parse_code(target) { - net__room_fail(NET_WHY_BAD_ADDRESS) + if not net_parse_code(net_st, target) { + net__room_fail(base_st, net_st, NET_WHY_BAD_ADDRESS) return } - 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() + 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) return } - net__log(`room {net__room}: dialling {target}`) + net__log(`room {net_st.net__room}: dialling {target}`) } -function net__room_reset() -> void { - if net__http != 0 { - Http.free(net__http) - net__http = 0 +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 } - 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() + 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) } diff --git a/packages/ludic.net/room_tick.ludic b/packages/ludic.net/room_tick.ludic index 1e5708e2..0622a8ea 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(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() +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) return } - 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() + 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) return } - if net__r_state == NET_R_DONE { + if net_st.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__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() } + 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) } return } - if net__r_state == NET_R_ROOM { net__room_watch(dt) } + if net_st.net__r_state == NET_R_ROOM { net__room_watch(net_st, dt) } } # a second and a half for an answer, asked three times; without one, inside addresses only -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() +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) return } - 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 + 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 } else { - net__http = Http.post(`{base}/rooms/{net__room}/join`, net__room_body("pub")) - net__r_state = NET_R_ASK + 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__r_t = 0.0 + net_st.net__r_t = 0.0 } -function net__room_answer() -> void { - let st = Http.poll(net__http) +function net__room_answer(base_st: mut BaseState, net_st: mut NetState) -> void { + let st = Http.poll(net_st.net__http) if st < 0 { - if net__r_t > 12.0 { - Http.free(net__http) - net__http = 0 - net__room_fail(NET_WHY_MATCHMAKER_SILENT) + 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) } return } var v: Val = null - 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 { + 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 { let code = net__json_str(v, "code") if len(code) == 0 { - net__room_fail(NET_WHY_NO_ROOM_GIVEN) + net__room_fail(base_st, net_st, NET_WHY_NO_ROOM_GIVEN) return } - net__room = code - net__take_relay(v) - net__r_state = NET_R_ROOM - net__poll_t = 0.0 + 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__log(`room {code}`) - net__fact(NET_ROOM, -1, 0, 0, code) + net__fact(base_st, net_st, NET_ROOM, -1, 0, 0, code) return } - if net__r_state == NET_R_ASK { + if net_st.net__r_state == NET_R_ASK { if v == null { - net__room_fail(NET_WHY_NO_ROOM) + net__room_fail(base_st, net_st, NET_WHY_NO_ROOM) return } - net__take_relay(v) - net__room_connect(net__json_str(v, "host"), net__json_str(v, "local")) + net__take_relay(net_st, v) + net__room_connect(base_st, net_st, 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() -> void { let l = value_get(v, "guests") for i in 0 .. value_count(l) { let g = value_at(l, i) - net__new_guest(net__json_str(g, "pub"), net__json_str(g, "local")) + net__new_guest(net_st, 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(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() +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) } - 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 + 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 } } diff --git a/packages/ludic.net/send.ludic b/packages/ludic.net/send.ludic index 336b1817..0be5d23d 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(slot: int) -> void { - if not net_peer_on(slot) { return } - let p = net__peers[slot] +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] for pk in 0 .. 8 { - if not net__flush_one(p) { return } + if not net__flush_one(net_st, p) { return } } } -function net__flush_one(p: NetPeer) -> bool { +function net__flush_one(net_st: mut NetState, 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__time - m.made > 0.5 { continue } # stale + if not m.rel and net_st.net__time - m.made > 0.5 { continue } # stale var due = true - if m.rel and not (m.sent < 0.0) and net__time - m.sent < 0.25 { due = false } + if m.rel and not (m.sent < 0.0) and net_st.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(o, m) + o = net__put_msg(net_st, o, m) count += 1 - m.sent = net__time - if net__kind_out != null and m.kind < 64 { net__kind_out[m.kind] += size } + 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 } 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(p: NetPeer) -> bool { } p.queue = keep if count == 0 and not p.ack_due { return false } - 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 + 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 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(at: int, m: NetMsg) -> int { +function net__put_msg(net_st: NetState, at: int, m: NetMsg) -> int { var o = at - nb_put8(net__out, o, m.kind) - nb_put8(net__out, o + 1, 0) - if m.rel { nb_put8(net__out, o + 1, 1) } + 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) } o += 2 if m.rel { - nb_put32(net__out, o, m.id) + nb_put32(net_st.net__out, o, m.id) o += 4 } - nb_put8(net__out, o, m.len & 255) - nb_put8(net__out, o + 1, (m.len >> 8) & 255) + nb_put8(net_st.net__out, o, m.len & 255) + nb_put8(net_st.net__out, o + 1, (m.len >> 8) & 255) o += 2 - nb_copy(net__out, o, m.data, 0, m.len) + nb_copy(net_st.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() -> void { - net__kind_out = words(64) - for k in 0 .. 64 { net__kind_out[k] = 0 } +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_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_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_bytes_out() -> int { return net__bytes_out } +export function net_bytes_out(net_st: NetState) -> int { return net_st.net__bytes_out } diff --git a/packages/ludic.net/session.ludic b/packages/ludic.net/session.ludic index d0ba1996..81641d10 100644 --- a/packages/ludic.net/session.ludic +++ b/packages/ludic.net/session.ludic @@ -1,106 +1,105 @@ # 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(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 +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 return false } - 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() + 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) return true } # dial a host at an address; the hello (net_hello_set) goes until a welcome or a refusal -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) +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) net__log(`joining {Udp.ip_text(ip)}:{port}`) return true } -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 +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 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() -> 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 } +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 } } # (again) to that address, with a fresh connection and the hello sent anew -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 +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 } # 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(ip: int, port: int) -> void { - net_dial(ip, port) - net__token = 0 - net__my_pid = 0 - net__state = NET_JOINING +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 } # the old host is gone and this machine keeps the socket, alone (it may become the host) -export function net_stand_alone() -> void { net__state = NET_IDLE } +export function net_stand_alone(net_st: mut NetState) -> void { net_st.net__state = NET_IDLE } -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 +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 } # a welcome: this machine is `pid` in the session `token` -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 +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 } # the host lets the stranger in `slot` in: its pid, from 2 upwards -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] +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] if p.pid >= 2 { return p.pid } - p.pid = net__next_pid - net__next_pid += 1 + p.pid = net_st.net__next_pid + net_st.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(secs: int) -> void { net__wait_more = secs } +export function net_hello_wait(net_st: mut NetState, secs: int) -> void { net_st.net__wait_more = secs } # a room code: joining, with no address yet - the matchmaker's answer brings one -function net__wait_for_host() -> void { - net__state = NET_JOINING - net__hello_t = 0.0 +function net__wait_for_host(net_st: mut NetState) -> void { + net_st.net__state = NET_JOINING + net_st.net__hello_t = 0.0 } diff --git a/packages/ludic.net/state.ludic b/packages/ludic.net/state.ludic index c03228be..b5bcfcc1 100644 --- a/packages/ludic.net/state.ludic +++ b/packages/ludic.net/state.ludic @@ -47,51 +47,82 @@ export property NetMessage { data: []byte } -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 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 } -function net__init() -> void { - if net__peers != null { return } - net__peers = new []NetPeer +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 for i in 0 .. NET_PEERS { let p = new NetPeer p.queue = new []NetMsg - push(net__peers, p) + push(net_st.net__peers, p) } - 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) + 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) } diff --git a/packages/ludic.net/stun.ludic b/packages/ludic.net/stun.ludic index 870376de..5ad750b0 100644 --- a/packages/ludic.net/stun.ludic +++ b/packages/ludic.net/stun.ludic @@ -1,24 +1,20 @@ # 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() -> 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 +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 } else if len(c.stun_host) > 0 { let host: pointer = c.stun_host - net__stun_ip = Udp.resolve(host) - net__stun_port = c.stun_port + net_st.net__stun_ip = Udp.resolve(host) + net_st.net__stun_port = c.stun_port } - if net__stun_ip == 0 { return } + if net_st.net__stun_ip == 0 { return } } let b = buffer(20) nb_put8(b, 0, 0) # a binding request, no attributes @@ -29,9 +25,9 @@ function net__stun_send() -> void { nb_put8(b, 5, 0x12) nb_put8(b, 6, 0xA4) nb_put8(b, 7, 0x42) - 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) + 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) } function net__be16(p: []byte, o: int) -> int { return (nb_get8(p, o) << 8) | nb_get8(p, o + 1) } @@ -39,23 +35,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(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) +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) var o = 20 while o + 4 <= 20 + len and o + 4 <= n { - 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) + 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) if t == 0x0020 { port = port ^ 0x2112 ip = ip ^ 0x2112A442 } - net__pub_ip = ip - net__pub_port = port + net_st.net__pub_ip = ip + net_st.net__pub_port = port net__log(`outside we are {net__addr_text(ip, port)}`) } o += 4 + ((l + 3) / 4) * 4 @@ -64,5 +60,5 @@ function net__stun_packet(n: int) -> bool { } # what this machine looks like from outside, once STUN has said (0 until then) -export function net_outside_ip() -> int { return net__pub_ip } -export function net_outside_port() -> int { return net__pub_port } +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 } diff --git a/packages/ludic.net/tests/net_test.ludic b/packages/ludic.net/tests/net_test.ludic index 9bd804bc..4793b2d2 100644 --- a/packages/ludic.net/tests/net_test.ludic +++ b/packages/ludic.net/tests/net_test.ludic @@ -5,50 +5,52 @@ import "ludic.base" program NetTest { numbers float property Pkt { ip: int = 0, port: int = 0, len: int = 0, data: []byte } - var sent: []Pkt = null - var inbound: []Pkt = null - var from_ip: int = 0 - var from_port: int = 0 + state NettestState { + sent: []Pkt = null + inbound: []Pkt = null + from_ip: int = 0 + 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(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 { + 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 { 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(sent, p) + push(nettest_st.sent, p) return n } - function fake_recv(h: int, b: []byte, cap: int) -> int { - if len(inbound) == 0 { return 0 } - let p = inbound[0] + 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] let rest = new []Pkt - for i in 1 .. len(inbound) { push(rest, inbound[i]) } - inbound = rest + for i in 1 .. len(nettest_st.inbound) { push(rest, nettest_st.inbound[i]) } + nettest_st.inbound = rest nb_copy(b, 0, p.data, 0, p.len) - from_ip = p.ip - from_port = p.port + nettest_st.from_ip = p.ip + nettest_st.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() -> void { - sent = new []Pkt - inbound = new []Pkt - net_reset() - q_clear(net_inbox()) - q_clear(net_facts()) + 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)) } # a packet from ip:port: token, pid, ack, then one message of `kind` (reliable with `id` > 0) - function arrive(ip: int, port: int, token: int, pid: int, ack: int, kind: int, id: int, payload: int) -> void { + function arrive(nettest_st: mut NettestState, 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') @@ -75,201 +77,201 @@ program NetTest { p.port = port p.len = o + 6 p.data = b - push(inbound, p) + push(nettest_st.inbound, p) } - function frame(dt: float) -> void { - net_begin(dt) - net_update(dt) - net_end() + 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) } # the host with a guest let in at 10.0.0.9:5000 as pid 2 - 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()) + 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)) expect_eq(len(got), 1) - return net_accept(got[0].slot) + return net_accept(net_st, got[0].slot) } - function facts_of(what: int) -> []NetFact { - let all = q_drain(net_facts()) + 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)) 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" { - 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 "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 "a join code decodes to its address and port; an address and a room code are taken too" { - fresh() + 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) let ip = Udp.ip("192.168.1.20") let code = net_code_of(ip, 27415) expect_eq(len(code), 11) - 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_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_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" { - let pid = host_with_guest() + 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) expect_eq(pid, 2) - 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) + 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) } - 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()) + 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)) expect_eq(len(got), 1) expect_eq(got[0].kind, 9) expect_eq(got[0].from, 2) - net_read(got[0]) - expect_eq(nr_i(), 1234) - expect(not nr_bad()) - nr_i() - expect(nr_bad()) + 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)) } - 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()) + 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)) expect_eq(len(got), 2) - net_read(got[1]) - expect_eq(nr_i(), 30) + net_read(net_st, got[1]) + expect_eq(nr_i(net_st), 30) expect_eq(got[1].slot, slot) } - 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 "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 "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 "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 "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) + 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) expect_eq(len(down), 1) expect_eq(down[0].why, NET_WHY_NO_ANSWER) - net_fresh() - expect_eq(net_state(), NET_IDLE) + net_fresh(net_st) + expect_eq(net_state(net_st), NET_IDLE) } - 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 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 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) + 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) expect_eq(len(gone), 1) expect_eq(gone[0].pid, pid) expect_eq(gone[0].slot, slot) - expect(not net_peer_on(slot)) + expect(not net_peer_on(net_st, slot)) } - 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 "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 "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) + 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) } } diff --git a/packages/ludic.net/tick.ludic b/packages/ludic.net/tick.ludic index 99431fa7..96ebc981 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(dt: float) -> void { - if not net_active() { return } - net__time = net__time + dt - net__pump() +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_update(dt: float) -> void { - if not net_active() { return } - net__room_tick(dt) - if net__state == NET_JOINING { - net__hello_tick(dt) +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) return } for i in 0 .. NET_PEERS { - 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) + 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) return } let gone = p.pid - np_close(i) - net__fact(NET_GONE, i, gone, 0, "") + np_close(net_st, i) + net__fact(base_st, net_st, NET_GONE, i, gone, 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 +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 } - net__hello_t = net__hello_t + dt - if net__hello_t > float(NET_TIMEOUT + net__wait_more) { - net_lose(NET_WHY_NO_ANSWER) + 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) return } - np_flush(0) + np_flush(net_st, 0) } -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 } +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 } for i in 0 .. NET_PEERS { - let p = net__peers[i] + let p = net_st.net__peers[i] if not p.on { continue } if beat { - nw_begin() - np_queue(i, NET_BEAT, false) + nw_begin(net_st) + np_queue(net_st, i, NET_BEAT, false) } - np_flush(i) - if p.closing and (p.acked >= p.close_id or net__time > p.close_t) { + np_flush(net_st, i) + if p.closing and (p.acked >= p.close_id or net_st.net__time > p.close_t) { let pid = p.pid - np_close(i) - net__fact(NET_CLOSED, i, pid, 0, "") + np_close(net_st, i) + net__fact(base_st, net_st, NET_CLOSED, i, pid, 0, "") } } } # while a load holds the frame: keep answering, so nobody times out on this machine -export function net_keepalive() -> void { - if not net_live() { return } - net__pump() +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) for i in 0 .. NET_PEERS { - let p = net__peers[i] + let p = net_st.net__peers[i] if p.on { - p.heard = net__time - nw_begin() - np_queue(i, NET_BEAT, false) - np_flush(i) + p.heard = net_st.net__time + nw_begin(net_st) + np_queue(net_st, i, NET_BEAT, false) + np_flush(net_st, i) } } } # every queue to its peer now, three times over (a machine about to let go) -export function net_flush_all() -> void { - for t in 0 .. 3 { for i in 0 .. NET_PEERS { np_flush(i) } } +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) } } } # the session is over for this machine: every slot, the socket and the matchmaker let go -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 +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 } - net__state = NET_IDLE - net__my_pid = 0 - net__wait_more = 0 - net__room_reset() + net_st.net__state = NET_IDLE + net_st.net__my_pid = 0 + net_st.net__wait_more = 0 + net__room_reset(net_st) } # out of the session, and why: the game hears NET_DOWN and says so in its own words -export function net_lose(why: int) -> void { net__lose_with(why, "") } +export function net_lose(base_st: mut BaseState, net_st: mut NetState, why: int) -> void { net__lose_with(base_st, net_st, why, "") } # ... with what the reason is about (the room code nobody had) -function net__lose_with(why: int, text: string) -> void { - net_reset() - net__state = NET_LOST - net__why = why +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 net__log(`lost ({why})`) - net__fact(NET_DOWN, -1, 0, why, text) + net__fact(base_st, net_st, NET_DOWN, -1, 0, why, text) } # a lost session read and done with: idle again -export function net_fresh() -> void { if net__state == NET_LOST { net__state = NET_IDLE } } +export function net_fresh(net_st: mut NetState) -> void { if net_st.net__state == NET_LOST { net_st.net__state = NET_IDLE } } diff --git a/packages/ludic.npc/act.ludic b/packages/ludic.npc/act.ludic index ec73b214..a2ab0580 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(p: NpcPerson, dt: float) -> void { +export function npc_act(npc_st: mut NpcState, 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(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(p.x, p.z) < NpcKinds[p.kind].notice { - let i = npc_near_i() + 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) wx = NpcWorld.player_x(i) wz = NpcWorld.player_z(i) } @@ -21,7 +21,7 @@ export function npc_act(p: NpcPerson, dt: float) -> void { let tick = NpcActs[p.act].tick if tick(p, dt) { return } } - if p.act_t < 0.0 { npc_plan(p) } + if p.act_t < 0.0 { npc_plan(npc_st, 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(p: NpcPerson) -> void { +export function npc_notice(base_st: mut BaseState, npc_st: mut NpcState, 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(p: NpcPerson) -> void { 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(NPC_F_GREETED, p, i) + np_fact(base_st, npc_st, 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 aada41e2..bff76be3 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(kind: int) -> NpcPerson { - np_ensure() - if np_n >= np_max or kind < 0 or kind >= NPCK_COUNT { return null } +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 } var p: NpcPerson = null - for i in 0 .. len(np_walkers) { - if not np_walkers[i].active { - p = np_walkers[i] + for i in 0 .. len(npc_st.np_walkers) { + if not npc_st.np_walkers[i].active { + p = npc_st.np_walkers[i] break } } if p == null { - if len(np_walkers) >= np_max { return null } + if len(npc_st.np_walkers) >= npc_st.np_max { return null } p = new NpcPerson - p.slot = len(np_walkers) - push(np_walkers, p) + p.slot = len(npc_st.np_walkers) + push(npc_st.np_walkers, p) } - np_fresh(p, kind, npc_between(1, 99999)) - p.name = npc_pick_name(p.body) + np_fresh(p, kind, npc_between(npc_st, 1, 99999)) + p.name = npc_pick_name(npc_st, p.body) let k = NpcKinds[kind] - 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 + 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 if k.start != null { let start = k.start start(p) - } else { npc_start_near(p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) } + } else { npc_start_near(npc_st, p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) } p.active = true - np_n += 1 + npc_st.np_n += 1 NpcWorld.born(p) - npc_plan(p) + npc_plan(npc_st, 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(p: NpcPerson, cx: float, cz: float, r0: float, r1: float) -> void { +export function npc_start_near(npc_st: mut NpcState, 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(cx, cz, r0, r1) { continue } - x = np_px - z = np_pz + if not npc_spot_near(npc_st, cx, cz, r0, r1) { continue } + x = npc_st.np_px + z = npc_st.np_pz ok = true - if npc_near(x, z) > 60.0 { break } + if npc_near(npc_st, x, z) > 60.0 { break } } - if not ok and npc_spot_near(x, z, 10.0, 45.0) { - x = np_px - z = np_pz + 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 } 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(kind: int, x: float, z: float, yaw: float, name: string, look: int) -> NpcPerson { - np_ensure() +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) let p = new NpcPerson - p.slot = len(np_posts) + p.slot = len(npc_st.np_posts) np_fresh(p, kind, look) p.fixed = true p.name = name @@ -105,16 +105,16 @@ export function npc_place(kind: int, x: float, z: float, yaw: float, name: strin p.yaw = yaw npc_set_at(p, x, z) p.active = true - push(np_posts, p) + push(npc_st.np_posts, p) NpcWorld.born(p) - npc_do(p, NPCA_IDLE, 3600.0) + npc_do(npc_st, p, NPCA_IDLE, 3600.0) return p } -export function npc_retire(p: NpcPerson) -> void { +export function npc_retire(npc_st: mut NpcState, p: NpcPerson) -> void { if not p.active { return } p.active = false - if not p.fixed and np_n > 0 { np_n -= 1 } - np_left(p.name) + if not p.fixed and npc_st.np_n > 0 { npc_st.np_n -= 1 } + np_left(npc_st, p.name) NpcWorld.gone(p) } diff --git a/packages/ludic.npc/ground.ludic b/packages/ludic.npc/ground.ludic index e5f0a13e..5ee6b103 100644 --- a/packages/ludic.npc/ground.ludic +++ b/packages/ludic.npc/ground.ludic @@ -1,15 +1,11 @@ # 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() -> 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_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_walkable(x: float, z: float) -> bool { if not NpcWorld.allowed(x, z) { return false } @@ -34,41 +30,41 @@ export function npc_line_ok(ax: float, az: float, bx: float, bz: float) -> bool return true } -function np_found(x: float, z: float) -> bool { - np_px = x - np_pz = z +function np_found(npc_st: mut NpcState, x: float, z: float) -> bool { + npc_st.np_px = x + npc_st.np_pz = z return true } # somewhere to stand d0 .. d1 from a point -export function npc_spot_near(x0: float, z0: float, d0: float, d1: float) -> bool { +export function npc_spot_near(npc_st: mut NpcState, x0: float, z0: float, d0: float, d1: float) -> bool { for t in 0 .. 40 { - let ang = npc_rnd() * NP_TAU - let d = d0 + npc_rnd() * (d1 - d0) + let ang = npc_rnd(npc_st) * NP_TAU + let d = d0 + npc_rnd(npc_st) * (d1 - d0) let x = x0 + Math.cos(ang) * d let z = z0 + Math.sin(ang) * d - if npc_walkable(x, z) { return np_found(x, z) } + if npc_walkable(x, z) { return np_found(npc_st, x, z) } } return false } # a place to stand d from (ax, az), on the side (bx, bz) is on -export function npc_toward(ax: float, az: float, bx: float, bz: float, d: float) -> bool { +export function npc_toward(npc_st: mut NpcState, 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(x, z) } + if npc_walkable(x, z) { return np_found(npc_st, 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(x2, z2) } + if npc_walkable(x2, z2) { return np_found(npc_st, x2, z2) } } - return npc_spot_near(x, z, 0.0, 14.0) + return npc_spot_near(npc_st, 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(x0: float, z0: float, rmax: float) -> bool { +export function npc_shore(npc_st: mut NpcState, x0: float, z0: float, rmax: float) -> bool { var found = false var bd = rmax for k in 0 .. 16 { @@ -83,9 +79,9 @@ export function npc_shore(x0: float, z0: float, rmax: float) -> bool { if NpcWorld.ground(x, z) < NpcWorld.water(x, z) { if ld > 0.0 and d - ld < 12.0 { bd = d - found = np_found(lx, lz) - np_wx = x - np_wz = z + found = np_found(npc_st, lx, lz) + npc_st.np_wx = x + npc_st.np_wz = z } break } diff --git a/packages/ludic.npc/mirror.ludic b/packages/ludic.npc/mirror.ludic index a7bf613c..d69530d7 100644 --- a/packages/ludic.npc/mirror.ludic +++ b/packages/ludic.npc/mirror.ludic @@ -1,44 +1,43 @@ # 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(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_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_mirroring() -> bool { return np_mirror } +export function npc_mirroring(npc_st: NpcState) -> bool { return npc_st.np_mirror } # the host's slot: somebody new in it (born), or nobody (gone) -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 { +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 { let q = new NpcPerson - q.slot = len(np_walkers) - push(np_walkers, q) + q.slot = len(npc_st.np_walkers) + push(npc_st.np_walkers, q) } - let p = np_walkers[slot] + let p = npc_st.np_walkers[slot] if p.active and p.id == id { return p } - if p.active { npc_retire(p) } + if p.active { npc_retire(npc_st, p) } np_fresh(p, kind, look) p.id = id p.seed = seed p.name = name npc_set_at(p, hx, hz) p.active = true - np_n += 1 + npc_st.np_n += 1 NpcWorld.born(p) return p } # where the host says it is now, and what it is doing -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) +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) if p == null or not p.active { return } if not p.heard { p.x = x @@ -47,7 +46,7 @@ export function npc_heard(slot: int, x: float, y: float, z: float, yaw: float, s p.yaw = yaw } p.heard = true - p.heard_t = np_clock + p.heard_t = npc_st.np_clock p.nx = x p.ny = y p.nz = z @@ -58,15 +57,15 @@ export function npc_heard(slot: int, x: float, y: float, z: float, yaw: float, s } # heard from in the last six seconds: drawn; not, and it is not drawn until it is again -export function npc_heard_lately(p: NpcPerson) -> bool { return p.heard and np_clock - p.heard_t <= 6.0 } +export function npc_heard_lately(npc_st: NpcState, p: NpcPerson) -> bool { return p.heard and npc_st.np_clock - p.heard_t <= 6.0 } -export function npc_mirror_tick(dt: float) -> void { - np_ensure() - np_clock = np_clock + dt +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 let k = Math.min(1.0, dt * 8.0) - for i in 0 .. len(np_walkers) { - let p = np_walkers[i] - if not p.active or not npc_heard_lately(p) { continue } + 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 } 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 30059694..777c02f3 100644 --- a/packages/ludic.npc/names.ludic +++ b/packages/ludic.npc/names.ludic @@ -1,44 +1,43 @@ # 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(n: string) -> bool { +export function npc_name_used(npc_st: mut NpcState, n: string) -> bool { if NpcWorld.taken(n) { return true } - let all = npc_walkers() + let all = npc_walkers(npc_st) for i in 0 .. len(all) { if all[i].active and all[i].name == n { return true } } - let posts = npc_posts() + let posts = npc_posts(npc_st) for i in 0 .. len(posts) { if posts[i].active and posts[i].name == n { return true } } - if np_recent != null { for i in 0 .. len(np_recent) { if np_recent[i] == 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 } } } 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(body: int) -> string { +export function npc_pick_name(npc_st: mut NpcState, 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(0, n - 1) + let start = npc_between(npc_st, 0, n - 1) for k in 0 .. n { let name = pool[(start + k) % n] - if not npc_name_used(name) { return name } + if not npc_name_used(npc_st, name) { return name } } return pool[start] } -function np_left(name: string) -> void { - if np_recent == null { np_recent = new []string } - push(np_recent, name) - if len(np_recent) <= 8 { return } +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 } let keep = new []string - for i in len(np_recent) - 8 .. len(np_recent) { push(keep, np_recent[i]) } - np_recent = keep + 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 } # how many pairs share a name, the place's own names included -export function npc_name_clashes() -> int { +export function npc_name_clashes(npc_st: mut NpcState) -> int { var dup = 0 - let all = npc_walkers() + let all = npc_walkers(npc_st) 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 43e5c1a2..102a4b4e 100644 --- a/packages/ludic.npc/person.ludic +++ b/packages/ludic.npc/person.ludic @@ -1,23 +1,34 @@ # 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 -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 +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 = "" +} const NP_PI: float = 3.1415927 const NP_TAU: float = 6.2831853 -export function npc_facts() -> Queue { - if np_facts == null { np_facts = queue_new("npc.facts") } - return np_facts +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 } -function np_fact(what: int, p: NpcPerson, player: int) -> NpcFact { +function np_fact(base_st: mut BaseState, npc_st: mut NpcState, what: int, p: NpcPerson, player: int) -> NpcFact { let f = new NpcFact f.what = what f.person = p.id @@ -25,68 +36,68 @@ function np_fact(what: int, p: NpcPerson, player: int) -> NpcFact { f.fixed = p.fixed f.player = player f.act = p.act - q_push(npc_facts(), f) + q_push(base_st, npc_facts(base_st, npc_st), f) return f } -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) } +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) } } # how many walkers the place can hold at once -export function npc_config(max: int) -> void { np_max = max } +export function npc_config(npc_st: mut NpcState, max: int) -> void { npc_st.np_max = max } -export function npc_seed(n: int) -> void { - np_ensure() - rng_seed(np_dice, n) +export function npc_seed(npc_st: mut NpcState, n: int) -> void { + np_ensure(npc_st) + rng_seed(npc_st.np_dice, n) } # 0 .. 1, and whole seconds lo .. hi, off the package's dice -export function npc_rnd() -> float { - np_ensure() - return rng_float(np_dice) +export function npc_rnd(npc_st: mut NpcState) -> float { + np_ensure(npc_st) + return rng_float(npc_st.np_dice) } -export function npc_between(lo: int, hi: int) -> int { - np_ensure() - return rng_between(np_dice, lo, hi) +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_secs(lo: int, hi: int) -> float { return float(npc_between(lo, np_hi(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_walkers() -> []NpcPerson { - np_ensure() - return np_walkers +export function npc_walkers(npc_st: mut NpcState) -> []NpcPerson { + np_ensure(npc_st) + return npc_st.np_walkers } -export function npc_posts() -> []NpcPerson { - np_ensure() - return np_posts +export function npc_posts(npc_st: mut NpcState) -> []NpcPerson { + np_ensure(npc_st) + return npc_st.np_posts } -export function npc_count() -> int { return np_n } +export function npc_count(npc_st: NpcState) -> int { return npc_st.np_n } -export function npc_max() -> int { return np_max } +export function npc_max(npc_st: NpcState) -> int { return npc_st.np_max } # a walker's slot or a post, and anyone by id -export function npc_at(slot: int) -> NpcPerson { - np_ensure() - if slot < 0 or slot >= len(np_walkers) { return null } - return np_walkers[slot] +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_post(i: int) -> NpcPerson { - np_ensure() - if i < 0 or i >= len(np_posts) { return null } - return np_posts[i] +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_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] } } +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] } } return null } diff --git a/packages/ludic.npc/port.ludic b/packages/ludic.npc/port.ludic index e9a582ea..ca23bdce 100644 --- a/packages/ludic.npc/port.ludic +++ b/packages/ludic.npc/port.ludic @@ -45,27 +45,25 @@ 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 "" } -var nw_ids: int = 0 -function nw_next_id() -> int { - nw_ids += 1 - return nw_ids +function nw_next_id(npc_st: mut NpcState) -> int { + npc_st.nw_ids += 1 + return npc_st.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(x: float, z: float) -> float { +export function npc_near(npc_st: mut NpcState, x: float, z: float) -> float { var bd = 1000000.0 - np_near_i = -1 + npc_st.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 - np_near_i = i + npc_st.np_near_i = i } } return bd } -export function npc_near_i() -> int { return np_near_i } +export function npc_near_i(npc_st: NpcState) -> int { return npc_st.np_near_i } diff --git a/packages/ludic.npc/routine.ludic b/packages/ludic.npc/routine.ludic index 9a983037..9192880b 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(p: NpcPerson, x: float, z: float, act: int, dur: float) -> void { +export function npc_go(npc_st: mut NpcState, 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(p: NpcPerson, x: float, z: float, act: int, dur: float) - 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() * 0.4 + p.speed = 1.1 + npc_rnd(npc_st) * 0.4 } # whatever it is, it is not done where the place says nobody stops: walk out of there first -export function npc_do(p: NpcPerson, act: int, dur: float) -> void { +export function npc_do(npc_st: mut NpcState, 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(p.x, p.z, 20.0, 60.0) { + if npc_spot_near(npc_st, p.x, p.z, 20.0, 60.0) { let fx = p.fx let fz = p.fz - npc_go(p, np_px, np_pz, act, dur) + npc_go(npc_st, p, npc_st.np_px, npc_st.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(p: NpcPerson) -> void { +export function npc_plan(npc_st: mut NpcState, p: NpcPerson) -> void { p.stalk = false if p.fixed { - npc_do(p, NPCA_IDLE, 3600.0) + npc_do(npc_st, p, NPCA_IDLE, 3600.0) return } let k = NpcKinds[p.kind] @@ -69,19 +69,19 @@ export function npc_plan(p: NpcPerson) -> void { let plan = k.plan if plan(p) { return } } - if k.steps != null and npc_steps(p, k.steps) { return } + if k.steps != null and npc_steps(npc_st, p, k.steps) { return } npc_face_ahead(p) - npc_do(p, NPCA_IDLE, 10.0) + npc_do(npc_st, 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(p: NpcPerson, steps: []NpcStep) -> bool { +export function npc_steps(npc_st: mut NpcState, 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(0, total - 1) + var r = npc_between(npc_st, 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 424de009..c6951ed2 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(p: NpcPerson, s: NpcStep, x: float, z: float) -> void { - npc_go(p, x, z, s.act, npc_secs(s.lo, s.hi)) +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)) } -function np_spot_here(p: NpcPerson, s: NpcStep) -> bool { +function np_spot_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool { npc_face_ahead(p) - npc_do(p, s.act, npc_secs(s.lo, s.hi)) + npc_do(npc_st, p, s.act, npc_secs(npc_st, s.lo, s.hi)) return true } -function np_spot_home(p: NpcPerson, s: NpcStep) -> bool { +function np_spot_home(npc_st: mut NpcState, 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(p, s.act, npc_secs(s.lo, s.hi)) } else { npc_step_go(p, s, p.hx, p.hz) } + 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) } npc_face(p, fx, fz) return true } -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) +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) return true } -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) +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) return true } # at the water's edge, facing out over it -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) +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) npc_face(p, wx, wz) return true } -function np_spot_shore_home(p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(p, s, p.hx, p.hz) } +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_here(p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(p, s, p.x, p.z) } +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_wander(p: NpcPerson, s: NpcStep) -> bool { - npc_wander(p) +function np_spot_wander(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool { + npc_wander(npc_st, p) return true } # a few steps somewhere that can actually be walked to, after being stuck -export function npc_wander(p: NpcPerson) -> void { +export function npc_wander(npc_st: mut NpcState, p: NpcPerson) -> void { for t in 0 .. 16 { - let ang = npc_rnd() * NP_TAU - let d = 8.0 + npc_rnd() * 18.0 + let ang = npc_rnd(npc_st) * NP_TAU + let d = 8.0 + npc_rnd(npc_st) * 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(p, x, z, NPCA_IDLE, npc_secs(5, 12)) + npc_go(npc_st, p, x, z, NPCA_IDLE, npc_secs(npc_st, 5, 12)) return } } npc_face_ahead(p) - npc_do(p, NPCA_IDLE, 8.0) + npc_do(npc_st, p, NPCA_IDLE, 8.0) } diff --git a/packages/ludic.npc/talk.ludic b/packages/ludic.npc/talk.ludic index a977a357..947fb3ac 100644 --- a/packages/ludic.npc/talk.ludic +++ b/packages/ludic.npc/talk.ludic @@ -20,7 +20,6 @@ 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) @@ -34,8 +33,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(p: NpcPerson) -> int { - np_said = "" +export function npc_line_for(npc_st: mut NpcState, p: NpcPerson) -> int { + npc_st.np_said = "" var top = -1000000 for i in 0 .. NPCL_COUNT { if NpcLines[i].prio > top { top = NpcLines[i].prio } } var prio = top @@ -45,10 +44,10 @@ export function npc_line_for(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(0, 99) >= l.chance { continue } + if l.chance < 100 and npc_between(npc_st, 0, 99) >= l.chance { continue } let w = npc_words(p, i) if len(w) > 0 { - np_said = w + npc_st.np_said = w return i } } @@ -57,13 +56,13 @@ export function npc_line_for(p: NpcPerson) -> int { return -1 } -export function npc_said() -> string { return np_said } +export function npc_said(npc_st: NpcState) -> string { return npc_st.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(p: NpcPerson, player: int) -> int { - let line = npc_line_for(p) - let f = np_fact(NPC_F_TALKED, p, player) +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) f.line = line p.said = NpcWorld.day() return line @@ -71,16 +70,16 @@ export function npc_talk(p: NpcPerson, player: int) -> int { # 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(p: NpcPerson, line: int, c: int, player: int) -> int { - np_said = "" +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 = "" 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(NPC_F_TALKED, p, player) + let f = np_fact(base_st, npc_st, NPC_F_TALKED, p, player) f.line = next f.choice = cs[c].code - if next >= 0 { np_said = npc_words(p, next) } + if next >= 0 { npc_st.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 c335a945..c13ffd91 100644 --- a/packages/ludic.npc/tests/fake/index.ludic +++ b/packages/ludic.npc/tests/fake/index.ludic @@ -1,30 +1,32 @@ # 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 -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 +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 +} -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_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_ground(x: float, z: float) -> float { return 0.0 } @@ -33,28 +35,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() -> float { return fk_hour } -function fk_day_now() -> int { return fk_day } +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_players() -> int { return 2 } -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_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_taken(n: string) -> bool { return n == "Ada" } -function fk_on_born(p: NpcPerson) -> void { fk_born += 1 } -function fk_on_gone(p: NpcPerson) -> void { fk_gone += 1 } +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_player_at(x: float, z: float) -> void { - fk_px[0] = x - fk_pz[0] = z +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 } # seconds of the place, in twentieths -function fk_run(secs: float) -> void { for k in 0 .. int(secs * 20.0) { npc_tick(0.05) } } +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_count(what: int) -> int { - let fs = q_drain(npc_facts()) +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)) 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 760619d4..19fdc93f 100644 --- a/packages/ludic.npc/tests/npc_test.ludic +++ b/packages/ludic.npc/tests/npc_test.ludic @@ -30,18 +30,20 @@ program NpcTest { def NpcNames from "names.lres" def NpcLines from "lines.lres" - var news_on: bool = false - function tk_say(p: NpcPerson, line: int) -> string { - if news_on { return `{p.name} saw a bear.` } + 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.` } return "" } function tk_greeting(p: NpcPerson) -> string { return "What'll it be?" } - function tk_start(p: NpcPerson) -> void { npc_start_near(p, 0.0, 0.0, 0.0, 10.0) } + 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 one(kind: int) -> NpcPerson { - fk_setup() - fk_want = 1 - let p = npc_spawn(kind) + 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) npc_set_at(p, 0.0, 0.0) return p } @@ -56,175 +58,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" { - 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 "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 "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 "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 "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) + 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) npc_face(p, 30.0, 0.0) - fk_run(30.0) + fk_run(base_st, npc_st, 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(NPC_F_ARRIVED), 1) + expect_eq(fk_count(base_st, npc_st, NPC_F_ARRIVED), 1) expect_near(p.fx, 30.0, 0.01) } - test "nobody walks into the lake" { - let p = one(NPCK_HIKER) - npc_go(p, -80.0, 0.0, NPCA_SIT, 30.0) + 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) var west = 0.0 for k in 0 .. 1200 { - npc_tick(0.05) + npc_tick(base_st, npc_st, 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" { - let p = one(NPCK_CAMPER) + 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) npc_set_at(p, -20.0, 0.0) - fk_hour = 12.0 - npc_plan(p) + npc_tests_fake_st.fk_hour = 12.0 + npc_plan(npc_st, p) expect_eq(p.act, NPCA_WALK) expect_eq(p.next_act, NPCA_FISH) expect(p.fx < -50.0) - fk_hour = 23.0 - npc_plan(p) + npc_tests_fake_st.fk_hour = 23.0 + npc_plan(npc_st, p) expect_eq(p.act, NPCA_SIT) } - test "an act is not begun where the place says nobody stops" { - let p = one(NPCK_HIKER) + 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) npc_set_at(p, 40.0, 0.0) - npc_do(p, NPCA_SIT, 30.0) + npc_do(npc_st, 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" { - let p = one(NPCK_HIKER) - npc_do(p, NPCA_IDLE, 100.0) + 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) p.yaw = 0.0 - fk_player_at(0.0, 0.0 - 5.0) - fk_run(2.0) + fk_player_at(npc_tests_fake_st, 0.0, 0.0 - 5.0) + fk_run(base_st, npc_st, 2.0) expect(Math.abs(p.yaw) < 0.05) - fk_player_at(5.0, 0.0) - fk_run(3.0) + fk_player_at(npc_tests_fake_st, 5.0, 0.0) + fk_run(base_st, npc_st, 3.0) expect(Math.abs(npc_wrap(p.yaw + 1.5707963)) < 0.05) - 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) + 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) } - test "a fisher keeps its eyes on the water, whoever is near" { - let p = one(NPCK_HIKER) - npc_do(p, NPCA_FISH, 100.0) + 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) p.yaw = 0.0 npc_face(p, 0.0, -10.0) - fk_player_at(5.0, 0.0) - fk_run(2.0) + fk_player_at(npc_tests_fake_st, 5.0, 0.0) + fk_run(base_st, npc_st, 2.0) expect(Math.abs(p.yaw) < 0.05) } - 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) + 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) expect(p.active) - fk_player_at(500.0, 0.0) - fk_run(0.5) + fk_player_at(npc_tests_fake_st, 500.0, 0.0) + fk_run(base_st, npc_st, 0.5) expect(not p.active) - 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_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(c.active) } - 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.") + 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.") expect(npc_talked_today(p)) - 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()) + 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)) 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" { - fk_setup() - let v = npc_place(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" (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) expect(v.fixed) - expect_eq(npc_count(), 0) - fk_player_at(8.0, 0.0) - fk_run(3.0) + 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(Math.abs(npc_wrap(v.yaw + 1.5707963)) < 0.05) - fk_player_at(300.0, 0.0) - fk_run(3.0) + fk_player_at(npc_tests_fake_st, 300.0, 0.0) + fk_run(base_st, npc_st, 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(v.id).name == "Mateo") + expect(npc_by_id(npc_st, 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" { - 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) + 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) expect(p.active) - 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_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_near(p.x, 12.0, 0.01) expect(p.phase > 0.0) - npc_tick(0.05) + npc_tick(base_st, npc_st, 0.05) expect_near(p.x, 12.0, 0.01) - for k in 0 .. 140 { npc_mirror_tick(0.05) } - expect(not npc_heard_lately(p)) + for k in 0 .. 140 { npc_mirror_tick(npc_st, 0.05) } + expect(not npc_heard_lately(npc_st, p)) } } diff --git a/packages/ludic.npc/tick.ludic b/packages/ludic.npc/tick.ludic index d04b4fa4..246e284a 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(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) +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) } } # the posts alone: a machine that does not run the place still turns its vendors to whoever comes -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) } } +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) } } } -function np_step_post(p: NpcPerson, dt: float) -> void { - npc_act(p, dt) - npc_notice(p) +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) } # 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() -> void { +function np_census(npc_st: mut NpcState) -> void { let h = NpcWorld.hour() - 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 } + 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 } } let extra = NpcWorld.extra() - if extra >= 0 and np_n < np_max { npc_spawn(extra) } + if extra >= 0 and npc_st.np_n < npc_st.np_max { npc_spawn(npc_st, extra) } } function np_is_night(h: float) -> bool { return h >= 21.0 or h < 5.0 } -function np_draw_kind(night: bool) -> int { +function np_draw_kind(npc_st: mut NpcState, 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(0, total - 1) + var r = npc_between(npc_st, 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(p: NpcPerson) -> bool { +function np_leaves(npc_st: mut NpcState, p: NpcPerson) -> bool { if p.goal >= 0 { return false } - let away = npc_near(p.x, p.z) + let away = npc_near(npc_st, 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 np_n > NpcWorld.want() and away > 150.0 { go = true } - if go { npc_retire(p) } + if npc_st.np_n > NpcWorld.want() and away > 150.0 { go = true } + if go { npc_retire(npc_st, p) } return go } # the census looks again at the next tick -export function npc_census_now() -> void { np_next_h = -10.0 } +export function npc_census_now(npc_st: mut NpcState) -> void { npc_st.np_next_h = -10.0 } # held where they stand: no routine and no census (a shot of the poses) -export function npc_hold(on: bool) -> void { np_hold = on } +export function npc_hold(npc_st: mut NpcState, on: bool) -> void { npc_st.np_hold = on } # everyone gone, as a new trip starts; the posts are the place's and stay -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()) +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)) } # forgotten altogether, walkers and posts (the place itself was replaced) -export function npc_clear() -> void { - np_walkers = new []NpcPerson - np_posts = new []NpcPerson - np_n = 0 - np_next_h = 0.0 +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 } diff --git a/packages/ludic.npc/walk.ludic b/packages/ludic.npc/walk.ludic index 5cd2ad4e..87d6c54f 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(p: NpcPerson, dt: float) -> void { +export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, 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(p: NpcPerson, dt: float) -> void { let dx = gx - p.x let dz = gz - p.z if Math.sqrt(dx * dx + dz * dz) < 1.2 { - np_arrive(p) + np_arrive(base_st, npc_st, p) return } let dg = npc_d(p.x, p.z, p.tx, p.tz) @@ -19,7 +19,7 @@ export function npc_walk(p: NpcPerson, dt: float) -> void { p.prog_t = 0.0 } else { p.prog_t = p.prog_t + dt } if p.prog_t > 20.0 { - np_stuck(p) + np_stuck(npc_st, p) return } var sp = p.speed @@ -39,7 +39,7 @@ export function npc_walk(p: NpcPerson, dt: float) -> void { p.detours += 1 return } - np_stuck(p) + np_stuck(npc_st, p) return } p.x = nx @@ -50,18 +50,18 @@ export function npc_walk(p: NpcPerson, dt: float) -> void { } # there: a step round something ends, or the errand begins, facing what it came for -function np_arrive(p: NpcPerson) -> void { +function np_arrive(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson) -> void { if p.via_on { p.via_on = false return } let fx = p.fx let fz = p.fz - npc_do(p, p.next_act, p.next_t) + npc_do(npc_st, p, p.next_act, p.next_t) p.fx = fx p.fz = fz p.stall = 0.0 - if p.act != NPCA_WALK { np_fact(NPC_F_ARRIVED, p, -1) } + if p.act != NPCA_WALK { np_fact(base_st, npc_st, 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(p: NpcPerson) -> void { +function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void { p.fails += 1 p.via_on = false if p.fails >= 3 { p.fails = 0 - npc_wander(p) - } else { npc_plan(p) } + npc_wander(npc_st, p) + } else { npc_plan(npc_st, p) } } diff --git a/packages/ludic.photo/frame.ludic b/packages/ludic.photo/frame.ludic index cab65c94..c9a3118f 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() -> 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 +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 } # 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(sp: int, name: string, x: float, y: float, z: float, h: float, legend: bool, moment: int) -> bool { +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 { if not photo_in_view(x, y + 0.8, z, PhotoLens.range()) { return false } - 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() + 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) } return true } # anything else in the picture worth naming: a landmark, the water, a sign -export function photo_frame_tag(text: string) -> void { +export function photo_frame_tag(photo_st: mut PhotoState, text: string) -> void { if len(text) == 0 { return } - if len(pht_pending_tags) > 0 { pht_pending_tags = pht_pending_tags + ", " } - pht_pending_tags = pht_pending_tags + text + 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 } -export function photo_frame_sign(sign: int) -> void { pht_pending_sign = sign } -export function photo_frame_stars() -> void { pht_pending_stars = true } +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 } diff --git a/packages/ludic.photo/grade.ludic b/packages/ludic.photo/grade.ludic index f28dc590..50963b51 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(x: float, y: float, z: float, h: float, moment: int) -> int { +export function photo_score(photo_st: mut PhotoState, 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 pht_comp == null { pht_blank() } + if photo_st.pht_comp == null { pht_blank(photo_st) } var sx = 0.5 var sy = 0.5 if PhotoLens.project(x, y + h * 0.5, z) { sx = PhotoLens.sx() sy = PhotoLens.sy() } - 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) + 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) if PhotoRules.behaviour() { - pht_comp[0] = pht_comp[0] * 0.5 - pht_comp[3] = pht_comp[3] * 2.0 + photo_st.pht_comp[0] = photo_st.pht_comp[0] * 0.5 + photo_st.pht_comp[3] = photo_st.pht_comp[3] * 2.0 } - return int(pht_comp[0] + (pht_comp[1] + pht_comp[2] + pht_comp[3]) * presence) + return int(photo_st.pht_comp[0] + (photo_st.pht_comp[1] + photo_st.pht_comp[2] + photo_st.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() -> int { +export function photo_why_of_comp(photo_st: PhotoState) -> int { var worst = PHOTO_WHY_SIZE - var least = pht_comp[0] / 40.0 + var least = photo_st.pht_comp[0] / 40.0 for k in 1 .. 4 { - let f = pht_comp[k] / 20.0 + let f = photo_st.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 b272af4c..cd3fabb5 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(file: string, day: int, hour: float) -> int { - if pht_n >= pht_max { - pht_fact(PHOTO_FULL, -1) +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) return -1 } var firsts = 0 - 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 + 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 f.firsts = firsts - f.grade = pht_pending_grade - f.sign = pht_pending_sign - f.stars = pht_pending_stars + f.grade = photo_st.pht_pending_grade + f.sign = photo_st.pht_pending_sign + f.stars = photo_st.pht_pending_stars f.file = file - f.tags = pht_pending_tags + f.tags = photo_st.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(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 } } +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 } } return i } # forgetting one: the record goes, the ones after it close up, and the game is told the file -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] +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] let tags = new []string let files = new []string - for k in 0 .. pht_n { + for k in 0 .. photo_st.pht_n { if k == i { continue } - push(tags, pht_tags[k]) - push(files, pht_file[k]) + push(tags, photo_st.pht_tags[k]) + push(files, photo_st.pht_file[k]) } - 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] + 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] } - pht_tags = tags - pht_file = files - pht_n -= 1 - if pht_cover == i { pht_cover = -1 } else if pht_cover > i { pht_cover -= 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 } } -export function photo_cover_set(i: int) -> void { pht_cover = i } +export function photo_cover_set(photo_st: mut PhotoState, i: int) -> void { photo_st.pht_cover = i } # what has been photographed outright (a network's word, a staged trip) -export function photo_record_set(species: int, legends: int) -> void { - pht_species_shot = species - pht_legend_shot = legends +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_best_set(sp: int, grade: int) -> void { if sp >= 0 and sp < 32 { pht_best[sp] = grade } } +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 } } diff --git a/packages/ludic.photo/order.ludic b/packages/ludic.photo/order.ludic index 677db200..d9637797 100644 --- a/packages/ludic.photo/order.ludic +++ b/packages/ludic.photo/order.ludic @@ -5,50 +5,47 @@ 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() -> 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) } +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) } # the roll in the chosen order: newest first, best first, or by subject -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 +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 } - for i in 0 .. pht_n { + for i in 0 .. photo_st.pht_n { var best = i - 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 + 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 } - return pht_order + return photo_st.pht_order } -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] +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] } -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)) } +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)) } # a page of the roll, in its order; the page is held inside the roll -export function photo_page_list() -> []int { +export function photo_page_list(photo_st: mut PhotoState) -> []int { let out = new []int - if pht_n == 0 { return out } - let order = photo_order() - if pht_page >= photo_pages() { pht_page = photo_pages() - 1 } + 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 } for k in 0 .. PHOTO_PAGE { - let oi = pht_page * PHOTO_PAGE + k - if oi < pht_n { push(out, order[oi]) } + let oi = photo_st.pht_page * PHOTO_PAGE + k + if oi < photo_st.pht_n { push(out, order[oi]) } } return out } diff --git a/packages/ludic.photo/queries.ludic b/packages/ludic.photo/queries.ludic index 14080265..259ac1cb 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() -> 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] } +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] } # a frame with a subject in it was graded; a view is not -export function photo_graded(i: int) -> bool { return pht_mask[i] != 0 } -export function photo_cover() -> int { return pht_cover } +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_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_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_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 } +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 } # the last subject's four components (size of 40, framing, light and the moment of 20 each), for a meter -export function photo_comp(k: int) -> float { return pht_comp[k] } -export function photo_comp_have() -> bool { return pht_comp_have } +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 } # 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 f2e65c6f..fda7bbec 100644 --- a/packages/ludic.photo/roll.ludic +++ b/packages/ludic.photo/roll.ludic @@ -26,44 +26,49 @@ export property PhotoFact { v: int = 0 } -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 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 } -function pht_fact(what: int, i: int) -> PhotoFact { + + +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 { let f = new PhotoFact f.what = what f.index = i - q_push(photo_facts(), f) + q_push(base_st, photo_facts(base_st, photo_st), f) return f } @@ -80,18 +85,18 @@ function pht_floats(n: int) -> []float { } # an empty roll of `max` frames -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 +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 } diff --git a/packages/ludic.photo/system.ludic b/packages/ludic.photo/system.ludic index d9f777d6..d1860f49 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(max: int) -> void { - pht_max = Math.max(max, 1) - photo_reset() +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_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_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_save() -> Val { +export function photo_save(photo_st: PhotoState) -> Val { let v = Value.object() let l = Value.list() - for i in 0 .. pht_n { + for i in 0 .. photo_st.pht_n { let o = Value.object() - 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]) + 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]) Value.add(l, o) } Value.put(v, "frames", l) - 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) + 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) return v } -export function photo_load(v: Val, version: int) -> void { - photo_reset() +export function photo_load(base_st: mut BaseState, photo_st: mut PhotoState, v: Val, version: int) -> void { + photo_reset(base_st, photo_st) 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(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(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_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)) + 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)) } export function photo_system() -> System { diff --git a/packages/ludic.photo/tests/photo_test.ludic b/packages/ludic.photo/tests/photo_test.ludic index a6bd8867..468089da 100644 --- a/packages/ludic.photo/tests/photo_test.ludic +++ b/packages/ludic.photo/tests/photo_test.ludic @@ -7,76 +7,78 @@ import "ludic.base" program PhotoTest { numbers float - 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 + 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 + } 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(x: float, y: float, z: float) -> bool { + function fk_project(phototest_st: mut PhototestState, x: float, y: float, z: float) -> bool { let d = Math.max(-z, 0.5) - sx = 0.5 + x / d - sy = 0.5 - (y - 1.5) / d + phototest_st.sx = 0.5 + x / d + phototest_st.sy = 0.5 - (y - 1.5) / d return z < 0.0 } - function fk_sx() -> float { return sx } - function fk_sy() -> float { return sy } + function fk_sx(phototest_st: PhototestState) -> float { return phototest_st.sx } + function fk_sy(phototest_st: PhototestState) -> float { return phototest_st.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() -> 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_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_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() -> void { - photo_config(4) - hour = 12.0 - night = false - dim = 0 - sun_z = 0.0 - behaviour = false + 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 facts() -> []PhotoFact { return q_drain(photo_facts()) } + function facts(base_st: mut BaseState, photo_st: mut PhotoState) -> []PhotoFact { return q_drain(base_st, photo_facts(base_st, photo_st)) } 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(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) + 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) } - 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 "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 "size: a record shot is a record shot, and the band that pays is a quarter to two thirds" { @@ -88,123 +90,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" { - fresh() - hour = 6.5 + 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 expect_eq(photo_score_light(0.0, -20.0), 20) - hour = 12.0 + phototest_st.hour = 12.0 expect_eq(photo_score_light(0.0, -20.0), 9) - dim = 4 + phototest_st.dim = 4 expect_eq(photo_score_light(0.0, -20.0), 5) - dim = 0 - sun_z = -1.0 + phototest_st.dim = 0 + phototest_st.sun_z = -1.0 expect_eq(photo_score_light(0.0, -20.0), 2) - sun_z = 0.0 - night = true - moon = 1.0 + phototest_st.sun_z = 0.0 + phototest_st.night = true + phototest_st.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" { - 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() + 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) expect(big > tiny) expect(tiny < 20) - 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) + 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) } - 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 "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 "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() + 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) expect_eq(count(fs, PHOTO_KEPT), 1) expect_eq(fs[0].firsts, 8) - 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) + 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) } - 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() + 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) expect_eq(total, int(11.0 * 1.5) + int(120.0 * 4.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) + 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) } - 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() + 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) expect(fs[0].file == "a.png") - expect_eq(photo_species_shot(), 14) + expect_eq(photo_species_shot(photo_st), 14) } - 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 "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 "a file name from the tags" { @@ -213,22 +215,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" { - 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) + 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) } } diff --git a/packages/ludic.photo/worth.ludic b/packages/ludic.photo/worth.ludic index 098ae917..6ac3d605 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(i: int) -> int { - if i < 0 or i >= pht_n or pht_sold[i] != 0 { return 0 } +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 } var v = 0 for sp in 0 .. 32 { - if (pht_mask[i] & (1 << sp)) == 0 { continue } + if (photo_st.pht_mask[i] & (1 << sp)) == 0 { continue } var first = true - for k in 0 .. pht_n { if k != i and pht_sold[k] != 0 and (pht_mask[k] & (1 << sp)) != 0 { first = false } } + 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 } } if first { v += PhotoRules.subject_value(sp) } - if (pht_legend[i] & (1 << sp)) != 0 { v += PhotoRules.legend_value(sp) } + if (photo_st.pht_legend[i] & (1 << sp)) != 0 { v += PhotoRules.legend_value(sp) } } - if v > 0 { v = int(float(v) * (PhotoRules.mult() * photo_grade_mult(pht_grade[i]))) } + if v > 0 { v = int(float(v) * (PhotoRules.mult() * photo_grade_mult(photo_st.pht_grade[i]))) } return v } # every frame with a price, sold: the total, and a fact a frame -export function photo_sell_all() -> int { +export function photo_sell_all(base_st: mut BaseState, photo_st: mut PhotoState) -> int { var total = 0 - for i in 0 .. pht_n { - let v = photo_value(i) + for i in 0 .. photo_st.pht_n { + let v = photo_value(photo_st, i) if v > 0 { total += v - pht_sold[i] = 1 - let f = pht_fact(PHOTO_SOLD, i) + photo_st.pht_sold[i] = 1 + let f = pht_fact(base_st, photo_st, PHOTO_SOLD, i) f.v = v - f.mask = pht_mask[i] + f.mask = photo_st.pht_mask[i] } } return total } # what the roll would fetch now -export function photo_worth() -> int { +export function photo_worth(photo_st: PhotoState) -> int { var total = 0 - for i in 0 .. pht_n { total += photo_value(i) } + for i in 0 .. photo_st.pht_n { total += photo_value(photo_st, i) } return total } diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index cb6e1dfd..96128dc1 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -58,44 +58,35 @@ 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(vs: string, fs: string, defs: string) -> AcProg { +function ac_prog_new(render3d_st: mut Render3dState, vs: string, fs: string, defs: string) -> AcProg { let a = new AcProg - a.prog = r3d_program(vs, fs, defs) + a.prog = r3d_program(render3d_st, vs, fs, defs) let p = a.prog - 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") + 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") # the samplers never move: units 0, 1, 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) + 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) return a } -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_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_remove(a: Actor) -> void { - if ac_actors == null { return } +function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void { + if render3d_st.ac_actors == null { return } let keep = new []Actor - for i in 0 .. len(ac_actors) { if ac_actors[i].id != a.id { push(keep, ac_actors[i]) } } - ac_actors = keep + 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 } # 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 { @@ -113,18 +104,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(model: Model) -> Actor { +function actor_new(render3d_st: mut Render3dState, 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() - ac_next_id += 1; a.id = ac_next_id + render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 } a.cull = 450.0 - if ac_actors == null { ac_actors = new []Actor } - push(ac_actors, a) + if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor } + push(render3d_st.ac_actors, a) return a } @@ -134,24 +125,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(ap: AcProg) -> void { - if ap.scene_frame == ac_frame { return } - ap.scene_frame = ac_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 let p = ap.prog - 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) + 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) } -function ac_visible(a: Actor, shadow: bool) -> bool { +function ac_visible(render3d_st: Render3dState, 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 - cam_pos[0]; let dz = a.z - cam_pos[2] + let dx = a.x - render3d_st.cam_pos[0]; let dz = a.z - render3d_st.cam_pos[2] let d2 = dx * dx + dz * dz var c = a.cull if shadow { c = Math.min(c, 300.0) } @@ -164,38 +155,38 @@ function ac_visible(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 ter_reflect { cy = 2.0 * water_level - cy } - if not cam_sphere_visible(a.x, cy, a.z, r * 1.5) { return false } + 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 } } return true } -function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void { +function actor_draw_one(render3d_st: mut Render3dState, a: Actor, ap: AcProg, shadow: bool) -> void { let p = ap.prog - gpu_use_program(p) - u_mat4(ap.l_model, a.mat) + gpu_use_program(render3d_st, p) + u_mat4(render3d_st, ap.l_model, a.mat) var skinned = 0.0 - 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 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 not shadow { - u_f(ap.l_emis, a.emissive) - u_f(ap.l_rough, a.rough) + u_f(render3d_st, ap.l_emis, a.emissive) + u_f(render3d_st, ap.l_rough, a.rough) } - gpu_cull(false) + gpu_cull(render3d_st, 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(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) } + 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) } } - gpu_tex_unit(0); gpu_tex_bind(GPU_TEX2D, pr.diff) + gpu_tex_unit(render3d_st, 0); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.diff) if not shadow { - gpu_tex_unit(1); gpu_tex_bind(GPU_TEX2D, pr.nrm) - gpu_tex_unit(2); gpu_tex_bind(GPU_TEX2D, pr.arm) + 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) } - mesh_draw(pr.mesh) + mesh_draw(render3d_st, pr.mesh) } } @@ -224,41 +215,38 @@ 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. -var ac_oq_closed: bool = true -function outline_model(m: Model, mat: floats, width: float, r: float, g: float, b: float) -> void { +function outline_model(render3d_st: mut Render3dState, 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 ac_oq_closed { ac_oq_n = 0; ac_oq_closed = false } - if ac_oq == null { ac_oq = new []OutlineReq } + 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 } var q: OutlineReq = null - 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) } + 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) } q.model = m; q.width = width; q.r = r; q.g = g; q.b = b m4_copy(q.mat, mat) - ac_oq_n += 1 + render3d_st.ac_oq_n += 1 } -function outline_clear() -> void { ac_oq_n = 0; ac_oq_closed = true } +function outline_clear(render3d_st: mut Render3dState) -> void { render3d_st.ac_oq_n = 0; render3d_st.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() -> void { if ac_oq_closed { ac_oq_n = 0 } } +function outline_frame(render3d_st: mut Render3dState) -> void { if render3d_st.ac_oq_closed { render3d_st.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() -> void { +function actor_census(render3d_st: Render3dState) -> void { let keys = new []Model let cnt = new []int let prims = new []int let rigid = new []int - for i in 0 .. len(ac_actors) { - let a = ac_actors[i] - if not ac_visible(a, false) { continue } + 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 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 } @@ -277,71 +265,70 @@ function actor_census() -> void { } print(`actor census: {len(keys)} models, {total} lit draws`) } -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) +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) } - actor_draw_outlines() - gpu_cull(true) + actor_draw_outlines(render3d_st) + gpu_cull(render3d_st, 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() -> 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 } } +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 } } if not any { return } - 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) + 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) } # and whatever asked for a rim without being an actor - 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) } + 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) } } - ac_oq_closed = true - gpu_cull_face(GL_BACK) + render3d_st.ac_oq_closed = true + gpu_cull_face(render3d_st, GL_BACK) } -function actor_draw_outline_one(a: Actor, ap: AcProg) -> void { - u_mat4(ap.l_model, a.mat) +function actor_draw_outline_one(render3d_st: mut Render3dState, a: Actor, ap: AcProg) -> void { + u_mat4(render3d_st, ap.l_model, a.mat) var skinned = 0.0 - 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 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) 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(0); gpu_tex_bind(GPU_TEX2D, pr.diff) } - mesh_draw(pr.mesh) + if a.cutout { gpu_tex_unit(render3d_st, 0); gpu_tex_bind(render3d_st, GPU_TEX2D, pr.diff) } + mesh_draw(render3d_st, pr.mesh) } } # Is a sphere inside this cascade's light box? The projection is orthographic, so a point's @@ -349,74 +336,66 @@ function actor_draw_outline_one(a: Actor, ap: AcProg) -> void { # 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.) -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) +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) for k in 0 .. 2 { - 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) + 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) 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(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 } +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 } 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(light_vp, a.x, a.y + hh, a.z, r * 1.5) { continue } + if not ac_in_light(render3d_st, 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 - cam_pos[0]; let dza = a.z - cam_pos[2] + let dxa = a.x - render3d_st.cam_pos[0]; let dza = a.z - render3d_st.cam_pos[2] let da = Math.sqrt(dxa * dxa + dza * dza) let ra = a.radius * a.scale - 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 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 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 + 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 } - prof_cpu_mark("shadow actors") + prof_cpu_mark(render3d_st, "shadow actors") } -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 { +function ac_caster_count(render3d_st: mut Render3dState, 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 } } - 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 } + 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 } } # 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() -> void { - ac_actors = new []Actor - outline_clear() +function actor_clear_all(render3d_st: mut Render3dState) -> void { + render3d_st.ac_actors = new []Actor + outline_clear(render3d_st) } diff --git a/packages/ludic.render3d/camera.ludic b/packages/ludic.render3d/camera.ludic index 001ee61f..1adc76b0 100644 --- a/packages/ludic.render3d/camera.ludic +++ b/packages/ludic.render3d/camera.ludic @@ -3,75 +3,58 @@ # 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(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_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_begin_frame(n: int, w: int, h: int) -> void { - gsl_jitter_frame() - 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_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_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_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) +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) let at = floats(3) - v3_add(at, cam_pos, cam_fwd) + v3_add(at, render3d_st.cam_pos, render3d_st.cam_fwd) let up = v3_new(0.0, 1.0, 0.0) - 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) + 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) # 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 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 + 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 } - m4_mul(cam_vp, cam_proj, cam_view) - m4_inverse(cam_inv_vp, cam_vp) - m4_inverse(cam_inv_proj, cam_proj) + 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) free(at); free(up) - cam_planes_update() + cam_planes_update(render3d_st) } -function cam_planes_update() -> void { - if cam_planes == null { cam_planes = floats(16) } - let m = cam_vp_clean +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 for p in 0 .. 4 { var r = 0 if p >= 2 { r = 1 } @@ -82,27 +65,27 @@ function cam_planes_update() -> 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) - 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 + 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 } } # is a sphere (float bits) at least partly inside the side planes of the view? -function cam_sphere_visible(x: float, y: float, z: float, r: float) -> bool { - if cam_planes == null { return true } +function cam_sphere_visible(render3d_st: Render3dState, x: float, y: float, z: float, r: float) -> bool { + if render3d_st.cam_planes == null { return true } let nr = -r for p in 0 .. 4 { let o = p * 4 - let dist = cam_planes[o] * x + cam_planes[o + 1] * y + cam_planes[o + 2] * z + cam_planes[o + 3] + 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] if dist < nr { return false } } return true } # fly: forward/strafe in metres, turn in radians -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() +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) } diff --git a/packages/ludic.render3d/collide.ludic b/packages/ludic.render3d/collide.ludic index 53cffd0b..da28144e 100644 --- a/packages/ludic.render3d/collide.ludic +++ b/packages/ludic.render3d/collide.ludic @@ -9,89 +9,78 @@ 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(x: float, z: float, r: float) -> void { col_add_h(x, z, r, COL_LOW, COL_HIGH) } +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) } # 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(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) +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) } - 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 + 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 } -function col_cell_of(v: float, origin: float) -> int { - var c = int(Math.floor((v - origin + float(TERRAIN_HALF)) / float(COL_CELL))) +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))) if c < 0 { c = 0 } - if c > col_side - 1 { c = col_side - 1 } + if c > render3d_st.col_side - 1 { c = render3d_st.col_side - 1 } return 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] } +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] } let fill = words(ncell) - 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 + 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 fill[c] += 1 } free(fill) - col_built = true - print(`colliders: {col_n}`) + render3d_st.col_built = true + print(`colliders: {render3d_st.col_n}`) } # push (px, pz) with radius pr out of every circle it overlaps; the result is in col_out -function col_resolve(px: float, pz: float, pr: float) -> bool { return col_resolve_at(px, pz, pr, COL_LOW, COL_HIGH) } +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) } # 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(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 } +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 } var x = px; var z = pz var moved = false - let cx = col_cell_of(px, ter_ox); let cz = col_cell_of(pz, ter_oz) + let cx = col_cell_of(render3d_st, px, render3d_st.ter_ox); let cz = col_cell_of(render3d_st, pz, render3d_st.ter_oz) for pass in 0 .. 2 { for dz in 0 .. 3 { let zc = cz + dz - 1 - if zc < 0 or zc >= col_side { continue } + if zc < 0 or zc >= render3d_st.col_side { continue } for dx in 0 .. 3 { let xc = cx + dx - 1 - 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] + 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] let d2 = ex * ex + ez * ez - let rr = col_r[i] + pr + let rr = render3d_st.col_r[i] + pr # nothing to push out of if the body is wholly above its top or below its base - if not (feet < col_y1[i]) { continue } - if not (head > col_y0[i]) { continue } + if not (feet < render3d_st.col_y1[i]) { continue } + if not (head > render3d_st.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, @@ -116,31 +105,31 @@ function col_resolve_at(px: float, pz: float, pr: float, feet: float, head: floa } } } - col_out[0] = x; col_out[1] = z + render3d_st.col_out[0] = x; render3d_st.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(px: float, pz: float, pr: float, feet: float, reach: float, floor: float) -> float { +function col_top_at(render3d_st: Render3dState, px: float, pz: float, pr: float, feet: float, reach: float, floor: float) -> float { var top = floor - 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) + 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) let limit = feet + reach for dz in 0 .. 3 { let zc = cz + dz - 1 - if zc < 0 or zc >= col_side { continue } + if zc < 0 or zc >= render3d_st.col_side { continue } for dx in 0 .. 3 { let xc = cx + dx - 1 - 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] + 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] let d2 = ex * ex + ez * ez - let rr = col_r[i] + pr + let rr = render3d_st.col_r[i] + pr if not (d2 < rr * rr) { continue } - let t = col_y1[i] + let t = render3d_st.col_y1[i] if t > limit { continue } # too tall to step onto: it is a wall if t > top { top = t } } @@ -149,21 +138,21 @@ function col_top_at(px: float, pz: float, pr: float, feet: float, reach: float, return top } # is the segment from (x0,z0) to (x1,z1) clear of every circle (a camera line of sight)? -function col_clear(x0: float, z0: float, x1: float, z1: float, r: float) -> bool { +function col_clear(render3d_st: mut Render3dState, 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(x, z, r) { return false } + if col_resolve(render3d_st, 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() -> 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 } +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 } } diff --git a/packages/ludic.render3d/daylight.ludic b/packages/ludic.render3d/daylight.ludic index 59056842..206bba2d 100644 --- a/packages/ludic.render3d/daylight.ludic +++ b/packages/ludic.render3d/daylight.ludic @@ -9,65 +9,35 @@ # 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() -> 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 +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 } # 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(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 +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 # 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(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) + 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) } function day_wrap(a: float) -> float { let two_pi = 2.0 * PI @@ -81,68 +51,68 @@ function smoothf(a: float, b: float, x: float) -> float { return t * t * (3.0 - 2.0 * t) } -function daylight_set(hours: float) -> void { +function daylight_set(render3d_st: mut Render3dState, hours: float) -> void { var h = hours % 24.0 if h < 0.0 { h = h + 24.0 } - day_hours = h + render3d_st.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 = day_az0 + (h - day_hour0) * Math.deg_to_rad(15.0) * day_dir - day_az = az; day_el = el + 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 d = smoothf(Math.deg_to_rad(-8.0), Math.deg_to_rad(12.0), el) - day_light = d + render3d_st.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(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) + 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) # 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 * day_moon_phase - let maz = az - moon_lag * day_dir + let moon_lag = 2.0 * PI * render3d_st.day_moon_phase + let maz = az - moon_lag * render3d_st.day_dir let mel = Math.deg_to_rad(-6.0 + 63.0 * Math.sin(t - moon_lag)) let mce = Math.cos(mel) - v3_set(day_moon_dir, -(Math.sin(maz) * mce), Math.sin(mel), -(Math.cos(maz) * mce)) - day_moon = false + v3_set(render3d_st.day_moon_dir, -(Math.sin(maz) * mce), Math.sin(mel), -(Math.cos(maz) * mce)) + render3d_st.day_moon = false if el < Math.deg_to_rad(-7.0) { - day_moon = true + render3d_st.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 * 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 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 ce = Math.cos(lel) - v3_set(sun_dir, -(Math.sin(laz) * ce), Math.sin(lel), -(Math.cos(laz) * ce)) - v3_set(sun_color, sr, sg, sb) + 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) # 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 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) + 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) # clouds: less light, and greyer (the blue and the warmth both fade) - let grey = (day_ibl[0] + (day_ibl[1] + day_ibl[2])) * 0.3333 + 0.6 * day_flash + 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 dim = 1.0 - 0.55 * oc - for i in 0 .. 3 { day_ibl[i] = Math.lerp(day_ibl[i], grey, 0.7 * oc) * dim } + for i in 0 .. 3 { render3d_st.day_ibl[i] = Math.lerp(render3d_st.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, @@ -152,25 +122,25 @@ function daylight_set(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 day_fog_base == 0.0 { day_fog_base = r3d_fog_density } + if render3d_st.day_fog_base == 0.0 { render3d_st.day_fog_base = render3d_st.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 = day_fog_base * (1.0 + 1.5 * lowsun) + var dens = render3d_st.day_fog_base * (1.0 + 1.5 * lowsun) dens = dens * (1.0 + 1.2 * oc) - r3d_fog_density = dens * day_fog_mul * r3d_fog_scale + render3d_st.r3d_fog_density = dens * render3d_st.day_fog_mul * render3d_st.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 - r3d_fog_falloff = 0.002 * (1.0 + 1.6 * lowsun) + render3d_st.r3d_fog_falloff = 0.002 * (1.0 + 1.6 * lowsun) # the glow a ridge is silhouetted against when you look into a low sun - r3d_fog_inscatter = (0.012 + 0.16 * lowsun) * (1.0 - 0.7 * oc) + render3d_st.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. - r3d_fog_desat = 1.15 + 0.7 * oc + render3d_st.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 @@ -183,29 +153,29 @@ function daylight_set(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 - 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) + 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) # the shadows' floor: warm and open into a low sun, blue at noon, cold and crushed at night - 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) + 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) # 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. - 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) + 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) # 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. - r3d_sky_relight = smoothf(Math.deg_to_rad(-10.0), Math.deg_to_rad(1.0), el) * (1.0 - 0.75 * oc) + render3d_st.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 @@ -214,64 +184,64 @@ function daylight_set(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. - 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 + 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 # 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("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 + 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 } # 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 day_moon { night_max = 4.5 + 3.5 * day_moon_illum } - post_exposure_max = Math.lerp(night_max, 20.0, d) + 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) # the visible sky turns with the sun (cheap); its convolutions rebake when far off - 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() + 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) } # the terrain's baked shadow follows the light in steps - 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 + 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 } } # where the moon is in its month; the game advances this each morning -function daylight_moon(phase: float) -> void { +function daylight_moon(render3d_st: mut Render3dState, phase: float) -> void { var p = phase % 1.0 if p < 0.0 { p = p + 1.0 } - day_moon_phase = p + render3d_st.day_moon_phase = p # illuminated fraction: (1 - cos(2 pi p)) / 2, which is 0 at new and 1 at full - day_moon_illum = (1.0 - Math.cos(2.0 * PI * p)) * 0.5 - daylight_set(day_hours) + render3d_st.day_moon_illum = (1.0 - Math.cos(2.0 * PI * p)) * 0.5 + daylight_set(render3d_st, render3d_st.day_hours) } # the weather over the valley: overcast 0..1, a fog multiplier, a lightning flash 0..1 -function daylight_weather(overcast: float, fog_mul: float, flash: float) -> void { - day_overcast = overcast; day_fog_mul = fog_mul; day_flash = flash +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 } # the campfire: a point light at (x, y, z) of `strength` (0 = out) -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) +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) } # the light in the hand: a point light (cone < -1) or a cone along dir (cone = cos half-angle) @@ -280,23 +250,23 @@ function daylight_fire(x: float, y: float, z: float, strength: float) -> void { # 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(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_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_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) +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) } diff --git a/packages/ludic.render3d/drawstats.ludic b/packages/ludic.render3d/drawstats.ludic index e971b96b..82f7e612 100644 --- a/packages/ludic.render3d/drawstats.ludic +++ b/packages/ludic.render3d/drawstats.ludic @@ -26,103 +26,86 @@ 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() -> 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) +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) } } - return ds_on == 1 + return render3d_st.ds_on == 1 } # the profiler's pass boundaries (prof_begin / prof_end), whether or not R3D_PROF is on -function ds_set_pass(name: pointer) -> void { - if ds_on != 1 { return } - ds_last = -1 +function ds_set_pass(render3d_st: mut Render3dState, name: pointer) -> void { + if render3d_st.ds_on != 1 { return } + render3d_st.ds_last = -1 var i = 0 - while i < len(ds_pass_names) { - if ds_pass_names[i] == name { ds_pass_i = i; return } + while i < len(render3d_st.ds_pass_names) { + if render3d_st.ds_pass_names[i] == name { render3d_st.ds_pass_i = i; return } i += 1 } - push(ds_pass_names, name); push(ds_pass_prog, 0); push(ds_pass_tex, 0) - ds_pass_i = len(ds_pass_names) - 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 } # once per render frame, before any of its work (prof_gen_frame) -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}`) +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}`) } - if ds_frame_no == ds_from + ds_frames { - ds_counting = false - ds_report() + if render3d_st.ds_frame_no == render3d_st.ds_from + render3d_st.ds_frames { + render3d_st.ds_counting = false + ds_report(render3d_st) } } -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 { +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 { r = -1 var i = 0 - 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 } + 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 } i += 1 } if r < 0 { - 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 + 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 } - ds_last = r + render3d_st.ds_last = r } - 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 + 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 } -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_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_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_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_kind_name(k: int) -> pointer { @@ -133,18 +116,18 @@ function ds_kind_name(k: int) -> pointer { return "indirect" } # a per-frame mean with one decimal -function ds_per(v: long) -> string { - let x = (v * 10) / ds_frames +function ds_per(render3d_st: Render3dState, v: long) -> string { + let x = (v * 10) / render3d_st.ds_frames return `{string(x / 10)}.{string(x % 10)}` } -function ds_backend() -> pointer { - if gpu_kind == GPU_VK { return "vulkan" } +function ds_backend(render3d_st: Render3dState) -> pointer { + if render3d_st.gpu_kind == GPU_VK { return "vulkan" } return "opengl" } -function ds_report() -> void { - let n_rows = len(ds_row_pass) - let n_pass = len(ds_pass_names) +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) var total: long = 0 let by_kind = new []long for k in 0 .. DS_KINDS { push(by_kind, 0) } @@ -152,20 +135,20 @@ function ds_report() -> 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 + 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] + 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] } print("") - print(`draw calls per frame ({ds_backend()}, {gl_w}x{gl_h}, mean of {ds_frames} frames from {ds_from}): {ds_per(total)}`) + 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)}`) var kinds = "" - for k in 0 .. DS_KINDS { kinds = kinds + ` {ds_kind_name(k)} {ds_per(by_kind[k])}` } + for k in 0 .. DS_KINDS { kinds = kinds + ` {ds_kind_name(k)} {ds_per(render3d_st, 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(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)}`) + 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)}`) } # rows by draws, most first let order = new []int @@ -174,7 +157,7 @@ function ds_report() -> void { while a < n_rows { let v = order[a] var b = a - 1 - while b >= 0 and ds_row_draws[order[b]] < ds_row_draws[v] { order[b + 1] = order[b]; b -= 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 } order[b + 1] = v a += 1 } @@ -184,19 +167,19 @@ function ds_report() -> void { var shown = 0 while shown < 40 and shown < n_rows { let r = order[shown] - 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])}`) + 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])}`) shown += 1 } var path = "build/drawstats.csv" - if r3d_env_has("R3D_DRAWSTATS_CSV") { path = r3d_env("R3D_DRAWSTATS_CSV") } + if r3d_env_has(render3d_st, "R3D_DRAWSTATS_CSV") { path = r3d_env(render3d_st, "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 = 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` + 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` file_write(f, line, len(line)) } file_close(f) diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 71fbefde..6879f6c8 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -4,16 +4,862 @@ # 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. -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 +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) } -function r3d_env_has(name: string) -> bool { - if is_windowed() and not r3d_dev() { return false } +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 } return Os.has_env(name) } -function r3d_env(name: string) -> string { - if is_windowed() and not r3d_dev() { return "" } +function r3d_env(render3d_st: mut Render3dState, name: string) -> string { + if is_windowed() and not r3d_dev(render3d_st) { return "" } return Os.env(name) } diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic index 92aa0af5..c991972a 100644 --- a/packages/ludic.render3d/gltf.ludic +++ b/packages/ludic.render3d/gltf.ludic @@ -22,17 +22,6 @@ 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 { @@ -48,41 +37,35 @@ 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. -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 } } +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 } } return -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_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 } -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) } +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 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 = gltf_dir + "/" + png + let path = render3d_st.gltf_dir + "/" + png var i = 0 - while i < gltf_tex_n { if gltf_tex_paths[i] == path { return gltf_tex_ids[i] }; i += 1 } + while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { return render3d_st.gltf_tex_ids[i] }; i += 1 } var dil = 0 - 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 } + 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 } return id } # the image uri behind materials[m]..index, or null -function gltf_mat_uri(mat: Val, slot: pointer) -> string { +function gltf_mat_uri(render3d_st: Render3dState, mat: Val, slot: pointer) -> string { var holder = mat if slot == "baseColorTexture" or slot == "metallicRoughnessTexture" { if value_has(mat, "pbrMetallicRoughness") == 0 { return null } @@ -90,84 +73,84 @@ function gltf_mat_uri(mat: Val, slot: pointer) -> string { } 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(gltf_doc, "textures"), ti) + let tex = value_at(value_get(render3d_st.gltf_doc, "textures"), ti) let src = value_as_int(value_get(tex, "source")) - let img = value_at(value_get(gltf_doc, "images"), src) + let img = value_at(value_get(render3d_st.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(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)) +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)) let off = jint(bv, "byteOffset", 0) + jint(acc, "byteOffset", 0) - gltf_count = jint(acc, "count", 0) - gltf_ctype = jint(acc, "componentType", 5126) + render3d_st.gltf_count = jint(acc, "count", 0) + render3d_st.gltf_ctype = jint(acc, "componentType", 5126) let ty = value_as_str(value_get(acc, "type")) - 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 + 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 var csz = 4 - 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 + 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 let buf = bytes(n + 8) - file_seek(gltf_bin, off, 0) - file_read(gltf_bin, buf, n) + file_seek(render3d_st.gltf_bin, off, 0) + file_read(render3d_st.gltf_bin, buf, n) return buf } -function gltf_attrib(m: Mesh, attrs: Val, name: pointer, loc: int) -> bool { +function gltf_attrib(render3d_st: mut Render3dState, m: Mesh, attrs: Val, name: pointer, loc: int) -> bool { if value_has(attrs, name) == 0 { return 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) + 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) free(data) return true } -function gltf_prim(p: Val) -> Prim { +function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim { let pr = new Prim - let m = gpu_mesh_new() + let m = gpu_mesh_new(render3d_st) let attrs = value_get(p, "attributes") - 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"))) + 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"))) var isz = 4 - if gltf_ctype == 5123 { isz = 2 } - gpu_mesh_indices(m, idx, gltf_count * isz, isz) + if render3d_st.gltf_ctype == 5123 { isz = 2 } + gpu_mesh_indices(render3d_st, m, idx, render3d_st.gltf_count * isz, isz) free(idx) - m.count = gltf_count - gpu_mesh_done(m) + m.count = render3d_st.gltf_count + gpu_mesh_done(render3d_st, m) pr.mesh = m pr.verts = nverts # material textures - 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 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 value_has(p, "material") != 0 { - 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) } + 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) } 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(mname, pr.diff, pr.nrm, pr.arm) } + if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) } else { - 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] } + 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] } else { print(`gltf: material {mname} has no textures and none were loaded before it`) } } } @@ -176,34 +159,34 @@ function gltf_prim(p: Val) -> Prim { } # Load the mesh of the node called `node_name` from dir/file. -function gltf_load(dir: string, file: string, node_name: string) -> Model { - gltf_dir = dir +function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model { + render3d_st.gltf_dir = dir let text = Fs.read_text(dir + "/" + file) if text == null { print(`gltf: cannot read {dir}/{file}`); return null } - gltf_doc = Json.parse(text) - let buffers = value_get(gltf_doc, "buffers") + render3d_st.gltf_doc = Json.parse(text) + let buffers = value_get(render3d_st.gltf_doc, "buffers") let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri")) - 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") + 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") 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(gltf_bin); return null } + if mesh_idx < 0 { print(`gltf: no node {node_name} in {file}`); file_close(render3d_st.gltf_bin); return null } let model = new Model model.prims = new []Prim - let mesh = value_at(value_get(gltf_doc, "meshes"), mesh_idx) + let mesh = value_at(value_get(render3d_st.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(p)) - model.tris += gltf_count / 3 + push(model.prims, gltf_prim(render3d_st, p)) + model.tris += render3d_st.gltf_count / 3 # bounds from the accessor min/max - let acc = value_at(value_get(gltf_doc, "accessors"), value_as_int(value_get(value_get(p, "attributes"), "POSITION"))) + let acc = value_at(value_get(render3d_st.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))) @@ -214,8 +197,8 @@ function gltf_load(dir: string, file: string, node_name: string) -> Model { if y0 < ymin { ymin = y0 } if y1 > ymax { ymax = y1 } } - if skin_idx >= 0 { model.skin = skin_load(skin_idx) } - file_close(gltf_bin) + if skin_idx >= 0 { model.skin = skin_load(render3d_st, skin_idx) } + file_close(render3d_st.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 c1ef8134..9cdafa01 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -26,13 +26,10 @@ 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(name: string) -> void { gpu_wanted = gpu_kind_of(name) } +function gpu_request(render3d_st: mut Render3dState, name: string) -> void { render3d_st.gpu_wanted = gpu_kind_of(name) } function gpu_kind_of(name: string) -> int { if name == "vk" or name == "vulkan" { return GPU_VK } @@ -46,134 +43,120 @@ 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() -> 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 { +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 { # a window needs a surface, and only the Win32 one is built - 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`) } + 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`) } } - return gpu_kind + return render3d_st.gpu_kind } -function gpu_backend() -> string { return gpu_name(gpu_kind) } -function gpu_is_gl() -> bool { return gpu_kind != GPU_VK } +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 } # ---- 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() -> 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_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_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_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_depth_test(on: bool) -> void { +function gpu_depth_test(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) - if v == gpu_s_depth_test { return } - gpu_s_depth_test = v - gpu_gl_cap(GL_DEPTH_TEST, v) + 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) } # GL_LESS, GL_LEQUAL, GL_EQUAL, GL_ALWAYS, ... (the comparison names are the same in every API) -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_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_write(on: bool) -> void { +function gpu_depth_write(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) - if v == gpu_s_depth_write { return } - gpu_s_depth_write = v - if gpu_kind != GPU_VK { gl_depth_mask(v) } + 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) } } -function gpu_blend(on: bool) -> void { +function gpu_blend(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) - if v == gpu_s_blend { return } - gpu_s_blend = v - gpu_gl_cap(GL_BLEND, v) + if v == render3d_st.gpu_s_blend { return } + render3d_st.gpu_s_blend = v + gpu_gl_cap(render3d_st, GL_BLEND, v) } -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_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_cull(on: bool) -> void { +function gpu_cull(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) - if v == gpu_s_cull { return } - gpu_s_cull = v - gpu_gl_cap(GL_CULL_FACE, v) + if v == render3d_st.gpu_s_cull { return } + render3d_st.gpu_s_cull = v + gpu_gl_cap(render3d_st, GL_CULL_FACE, v) } # GL_BACK or GL_FRONT -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) } +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) } } # all four channels together: nothing in the renderer writes a partial mask -function gpu_color_write(on: bool) -> void { +function gpu_color_write(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) - if v == gpu_s_color_write { return } - gpu_s_color_write = v - if gpu_kind != GPU_VK { gl_color_mask(v, v, v, v) } + 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) } } -function gpu_alpha_to_coverage(on: bool) -> void { +function gpu_alpha_to_coverage(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) - if v == gpu_s_a2c { return } - gpu_s_a2c = v - gpu_gl_cap(GL_SAMPLE_ALPHA_TO_COVERAGE, v) + 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) } # 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. -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 { +function gpu_depth_bias(render3d_st: mut Render3dState, factor: fixed, units: fixed) -> void { var v = 1 if factor == 0.0 and units == 0.0 { v = 0 } - 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) } + 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) } } # 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. -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) } +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) } gl_scissor(x, gl_h - y_top - h, w, h) - gpu_glcheck_after("scissor") + gpu_glcheck_after(render3d_st, "scissor") } -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 +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 gl_disable(GL_SCISSOR_TEST) - gpu_glcheck_after("scissor off") + gpu_glcheck_after(render3d_st, "scissor off") } # ---- uniforms -------------------------------------------------------------------- @@ -181,47 +164,44 @@ function gpu_scissor_off() -> 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(prog: int, name: string) -> int { if gpu_kind == GPU_VK { return gvk_uniform(prog, name) }; return gl_get_uniform_location(prog, name) } +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) } # ---- 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. -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) } +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) } let p = gl_program(vs_src, fs_src) if p == 0 { 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, `{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, `{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(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] } } +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] } } 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 } +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 } if p == 0 { return } gl_delete_program(p) - 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") + 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") } # ---- GPU timers (R3D_PROF) ---------------------------------------------------------- -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) } +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) } # true once the query has its result; the nanoseconds (low 32 bits) are then in out[0] -function gpu_query_result(id: int, out: words) -> bool { - if gpu_kind == GPU_VK { return gvk_query_result(id, out) } +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) } 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) @@ -229,31 +209,30 @@ function gpu_query_result(id: int, out: words) -> bool { } # ---- the context -------------------------------------------------------------------- -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 } +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 } # the finished frame: presented to the window, or (headless) the GPU's work finished -function gpu_present() -> void { if gpu_kind == GPU_VK { gvk_present(); return }; Gl.swap() } +function gpu_present(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_present(render3d_st); 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(path: string) -> bool { if gpu_kind == GPU_VK { return gvk_screenshot(path) }; return Gl.screenshot(path: path) } +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) } -var gpu_u_tmp: words = null -function gpu_tmp() -> words { if gpu_u_tmp == null { gpu_u_tmp = words(4) }; return gpu_u_tmp } +function gpu_tmp(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 } # float bits (IEEE singles in an int), like every other number in the renderer -function u_f(loc: int, v: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(v); gvk_u_set(loc, 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) } +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) } # 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(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) } +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) } # ---- what this machine can do ---------------------------------------------------- # The advanced graphics features are Windows features: the Vulkan renderer, ray tracing, @@ -274,17 +253,6 @@ 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 { @@ -305,22 +273,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(kind: string) -> void { - gpu_cap_windows = true +function gpu_caps_fake(render3d_st: mut Render3dState, kind: string) -> void { + render3d_st.gpu_cap_windows = true if kind == "none" { return } - 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]}` + 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]}` } -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 } +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 } if Vk.open() == 0 { return } let cnt = bytes(4) Vk.put_i32(cnt, 0, 0) @@ -328,7 +296,7 @@ function gpu_caps_probe() -> void { let nie = Vk.get_i32(cnt, 0) let iexts = bytes(nie * VkExtensionProperties_sizeof + 8) Vk.enumerate_instance_extension_properties(null, cnt, iexts) - gpu_cap_hdr = gpu_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) + render3d_st.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) @@ -341,8 +309,8 @@ function gpu_caps_probe() -> void { # interposer, and a second instance made and destroyed under it is what this avoids. var inst: pointer = null var own = false - if gpu_kind == GPU_VK and gvk_ready { - inst = gvk_inst + if render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_ready { + inst = render3d_st.gvk_inst } else { let out = bytes(8) if Vk.create_instance(ici, null, out) != VK_SUCCESS { return } @@ -364,11 +332,11 @@ function gpu_caps_probe() -> void { if pick < 0 and nd > 0 { pick = 0 } if pick >= 0 { let pd = Vk.get_ptr(devs, pick * 8) - gpu_cap_vulkan = true + render3d_st.gpu_cap_vulkan = true Vk.get_physical_device_properties(pd, props) - 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) } + 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) } let api = Vk.get_i32(props, VkPhysicalDeviceProperties_apiVersion) let f13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof) Vk.zero(f13, VkPhysicalDeviceVulkan13Features_sizeof) @@ -384,18 +352,18 @@ function gpu_caps_probe() -> void { Vk.get_physical_device_features2(pd, f2) let major = (api >> 22) & 127 let minor = (api >> 12) & 1023 - 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 + 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 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) - 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) + 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) } if own { Vk.destroy_instance(inst, null) } - 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}`) + 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}`) } # Whether the renderer actually draws a feature yet. Until a feature lands, choosing it is saved and @@ -438,18 +406,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() -> Mesh { +function gpu_mesh_new(render3d_st: Render3dState) -> 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 gpu_kind != GPU_VK { m.vao = gl_vao() } + if render3d_st.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(m: Mesh, data: pointer, nbytes: int, usage: int) -> int { +function gpu_mesh_vertices(render3d_st: mut Render3dState, m: Mesh, data: pointer, nbytes: int, usage: int) -> int { var b = 0 - if gpu_kind == GPU_VK { b = gvk_buf_new(); gvk_buf_upload(b, nbytes, data) } + if render3d_st.gpu_kind == GPU_VK { b = gvk_buf_new(render3d_st); gvk_buf_upload(render3d_st, b, nbytes, data) } else { b = gl_buffer() gl_bind_buffer(GL_ARRAY_BUFFER, b) @@ -472,38 +440,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(m: Mesh, index: int, comps: int, type: int, stride: int, offset: int, normalized: bool) -> void { - if gpu_kind != GPU_VK { +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 { 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(m: Mesh, data: pointer, nbytes: int, index_bytes: int) -> void { +function gpu_mesh_indices(render3d_st: mut Render3dState, 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 gpu_kind == GPU_VK { m.ebo = gvk_buf_new(); gvk_buf_upload(m.ebo, nbytes, data); return } + if render3d_st.gpu_kind == GPU_VK { m.ebo = gvk_buf_new(render3d_st); gvk_buf_upload(render3d_st, 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(m: Mesh) -> void { if gpu_kind == GPU_VK { return }; gl_bind_vertex_array(0) } +function gpu_mesh_done(render3d_st: Render3dState, m: Mesh) -> void { if render3d_st.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(m: Mesh, buf: int) -> void { - if gpu_kind != GPU_VK { +function gpu_mesh_bind_instances(render3d_st: Render3dState, m: Mesh, buf: int) -> void { + if render3d_st.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(m: Mesh, index: int, comps: int, type: int, stride: int, offset: int) -> void { - if gpu_kind != GPU_VK { +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 { 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) @@ -512,16 +480,16 @@ function gpu_mesh_attr_inst(m: Mesh, index: int, comps: int, type: int, stride: } # a buffer on its own (instances, a stream): made, filled whole, freed -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 } +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 } gl_bind_buffer(GL_ARRAY_BUFFER, buf) gl_buffer_data(GL_ARRAY_BUFFER, nbytes, data, gpu_gl_usage(usage)) } -function gpu_buffer_free(buf: int) -> void { - if gpu_kind == GPU_VK { if buf > 0 { gvk_buf_release(buf) }; return } +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 } if buf == 0 { return } - let ids = gpu_tmp() + let ids = gpu_tmp(render3d_st) ids[0] = buf gl_delete_buffers(1, ids) } @@ -531,77 +499,77 @@ function gpu_buffer_free(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() -> bool { return gpu_kind == GPU_VK } +function gpu_has_compute(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK } # several records in one indirect draw, each with its own firstInstance -function gpu_has_mdi() -> bool { return gpu_kind == GPU_VK and gvk_has_mdi } +function gpu_has_mdi(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_has_mdi } # mesh shaders (VK_EXT_mesh_shader): a *.mesh program drawn with gpu_draw_mesh_tasks -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_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_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) } +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) } } # a buffer a compute pass writes (never reallocated under a draw that reads it) -function gpu_buffer_gpu_owned(buf: int) -> void { if gpu_kind == GPU_VK { gvk_buf_gpu_owned(buf) } } +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) } } # the host-visible contents of a buffer, for a readback after gpu_finish; null on OpenGL -function gpu_buffer_map(buf: int) -> pointer { - if gpu_kind != GPU_VK or buf <= 0 { return null } - return gvk_buf_map[buf] +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_finish() -> void { if gpu_kind == GPU_VK { gvk_flush() } } +function gpu_finish(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_flush(render3d_st) } } # 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(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) } +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) } } # drawing -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") +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") 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("a draw") + gpu_glcheck_after(render3d_st, "a 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") +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") 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("an instanced draw") + gpu_glcheck_after(render3d_st, "an instanced draw") } # the bound mesh's indices again (a patch mesh drawn once per terrain node) -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") +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") gl_draw_elements(m.mode, m.count, m.itype, null) - gpu_glcheck_after("a terrain patch") + gpu_glcheck_after(render3d_st, "a terrain patch") } # vertices [first, first + count) of the bound mesh, as triangles (the overlay's ranges) -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") +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") gl_draw_arrays(GL_TRIANGLES, first, count) - gpu_glcheck_after("an overlay draw") + gpu_glcheck_after(render3d_st, "an overlay draw") } -function gpu_mesh_free(m: Mesh) -> void { - if gpu_kind == GPU_VK { gvk_mesh_free(m); return } +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 } if m == null { return } - let ids = gpu_tmp() + let ids = gpu_tmp(render3d_st) 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 @@ -621,143 +589,137 @@ function gpu_mesh_free(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(tex: int) -> int { +function gpu_tx_at(render3d_st: mut Render3dState, tex: int) -> int { if tex <= 0 { return -1 } - if tex >= gpu_tx_cap { - var cap = gpu_tx_cap * 2 + if tex >= render3d_st.gpu_tx_cap { + var cap = render3d_st.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 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 + 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 } return tex * GPU_TX_W } -function gpu_bound(kind: int) -> int { if kind == GPU_TEX2D_ARRAY { return gpu_bound_array }; return gpu_bound_2d } +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_tex_new() -> int { if gpu_kind == GPU_VK { return gvk_tex_new() }; return gl_texture() } +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() } # 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(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 +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 } # what GL has on each unit's 2D target, for R3D_GLCHECK: deleting a texture unbinds it everywhere -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 +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 } } # pixel transfer packing (alignment, byte swap) for the uploads and read-backs that follow -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) } +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) } } else { gl_tex_image2d(GL_TEXTURE_2D, 0, ifmt, w, h, 0, fmt, ty, data) } - 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 } + 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 } } -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) } +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) } } else { gl_tex_image3d(GL_TEXTURE_2D_ARRAY, 0, ifmt, w, h, layers, 0, fmt, ty, data) } - 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 } + 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 } } -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)) +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)) if o < 0 { return } - 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") + 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") } # a float parameter (fixed, as Gl.* takes it): anisotropy is the one the renderer sets -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") +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") } # the border colour clamp-to-border reads (four fixed values in `rgba`) -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]) } +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]) } } else { gl_generate_mipmap(gpu_gl_target(kind)) } - 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 } + 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 } } # level 0 of the bound texture into `out` -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 } +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 } gl_get_tex_image(gpu_gl_target(kind), 0, fmt, ty, out) - gpu_glcheck_after(`a read-back of texture {gpu_bound(kind)}`) + gpu_glcheck_after(render3d_st, `a read-back of texture {gpu_bound(render3d_st, kind)}`) } -function gpu_tex_free(tex: int) -> void { +function gpu_tex_free(render3d_st: mut Render3dState, tex: int) -> void { if tex == 0 { return } - let ids = gpu_tmp() + let ids = gpu_tmp(render3d_st) ids[0] = tex - 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 } } + 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 } } } # a texture on a unit for a program's sampler, by the sampler's name -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() { +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) { # a sampler reading a texture nobody made (0 or freed) is the "unloadable" the driver warns of - 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`) } + 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`) } 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 = gpu_tx[o + 5] + let mn = render3d_st.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 gpu_tx[o + 10] == 0 { + if wants_mips and render3d_st.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(`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`) + 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`) } # compare mode on: only a shadow sampler may read it; a plain one reads zero ("unloadable") - 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`) } + 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`) } } - gpu_glcheck_after(`binding {name} (texture {tex}) on unit {unit}`) + gpu_glcheck_after(render3d_st, `binding {name} (texture {tex}) on unit {unit}`) } } @@ -771,203 +733,196 @@ function gpu_bind_sampler(prog: int, name: string, unit: int, kind: int, tex: in # 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(fb: int) -> int { +function gpu_fb_at(render3d_st: mut Render3dState, fb: int) -> int { if fb <= 0 { return -1 } - if fb >= gpu_fb_cap { - var cap = gpu_fb_cap * 2 + if fb >= render3d_st.gpu_fb_cap { + var cap = render3d_st.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 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 + 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 } return fb * GPU_FB_W } -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_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_check(tag: string) -> int { if gpu_kind == GPU_VK { return 0 }; return gl_check(tag) } +function gpu_check(render3d_st: Render3dState, tag: string) -> int { if render3d_st.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(tag: string) -> void { if gpu_kind == GPU_VK { return }; if gpu_glcheck_on() { gl_check(tag) } } +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_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_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_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_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_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_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_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(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_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_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_rb_new() -> int { - if gpu_kind == GPU_VK { return gvk_tex_new() } - let ids = gpu_tmp() +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) gl_gen_renderbuffers(1, ids) return ids[0] } # storage for a renderbuffer: samples > 0 makes it multisampled -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 +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 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) } - gpu_rb_samples = samples - gpu_glcheck_after("rb storage") + render3d_st.gpu_rb_samples = samples + gpu_glcheck_after(render3d_st, "rb storage") } -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 } +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 } gl_framebuffer_renderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, GL_RENDERBUFFER, rb) - let o = gpu_fb_at(gpu_fb_cur) - if o >= 0 { gpu_fb[o + 4] = rb; gpu_fb[o + 6] = gpu_rb_samples } + 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 } } -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 } +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 } gl_framebuffer_renderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb) - let o = gpu_fb_at(gpu_fb_cur) - if o >= 0 { gpu_fb[o + 5] = rb; gpu_fb[o + 6] = gpu_rb_samples } + 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 } } # colour slots 0 .. n-1 are drawn into (several: an MRT bake) -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") +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") } # a depth-only target: no colour is drawn or read -function gpu_fb_no_color() -> void { - if gpu_kind == GPU_VK { gvk_fb_colors(gpu_fb_cur, 0); return } +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 } gl_draw_buffer(GL_NONE) gl_read_buffer(GL_NONE) - gpu_glcheck_after("fb no color") + gpu_glcheck_after(render3d_st, "fb no color") } -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 } +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 } if fb == 0 { return } - let ids = gpu_tmp() + let ids = gpu_tmp(render3d_st) ids[0] = fb gl_delete_framebuffers(1, ids) - 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") + 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") } -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 } +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 } if rb == 0 { return } - let ids = gpu_tmp() + let ids = gpu_tmp(render3d_st) ids[0] = rb gl_delete_renderbuffers(1, ids) - gpu_glcheck_after("rb free") + gpu_glcheck_after(render3d_st, "rb free") } -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") } +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") } # 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.) -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) +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) 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(fb: int) -> string { +function gpu_fb_describe(render3d_st: mut Render3dState, fb: int) -> string { if fb == 0 { return "the default framebuffer" } - let o = gpu_fb_at(fb) + let o = gpu_fb_at(render3d_st, fb) if o < 0 { return `framebuffer {fb}` } - var tex = gpu_fb[o] + var tex = render3d_st.gpu_fb[o] var what = "colour" - if tex == 0 { tex = gpu_fb[o + 2]; what = "depth" } - let t = gpu_tx_at(tex) + if tex == 0 { tex = render3d_st.gpu_fb[o + 2]; what = "depth" } + let t = gpu_tx_at(render3d_st, tex) if tex == 0 or t < 0 { return `framebuffer {fb} (nothing recorded attached)` } - return `framebuffer {fb} ({what} texture {tex}, {gpu_tx[t + 1]}x{gpu_tx[t + 2]}, format {gpu_tx[t + 4]})` + 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]})` } -function gpu_glcheck_before(what: string) -> void { - if gpu_kind == GPU_VK { return } - if not gpu_glcheck_on() { return } +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 } let pending = gl_get_error() - 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`) } + 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`) } let st = gl_check_framebuffer_status(GL_FRAMEBUFFER) - if st != GL_FRAMEBUFFER_COMPLETE { gpu_glcheck_say(`gpu: {gpu_fb_describe(gpu_fb_cur)} incomplete ({st}) at {what}`) } + 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}`) } } -function gpu_glcheck_after(what: string) -> void { - if gpu_kind == GPU_VK { return } - if not gpu_glcheck_on() { return } +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 } let e = gl_get_error() - if e != 0 { gpu_glcheck_say(`gpu: error {e} from {what} into {gpu_fb_describe(gpu_fb_cur)}`) } + if e != 0 { gpu_glcheck_say(render3d_st, `gpu: error {e} from {what} into {gpu_fb_describe(render3d_st, render3d_st.gpu_fb_cur)}`) } } -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") +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") gl_clear(mask) - gpu_glcheck_after("a clear") + gpu_glcheck_after(render3d_st, "a clear") } # the bound read framebuffer's [0, w) x [0, h) into the bound draw framebuffer's, unscaled -function gpu_blit(w: int, h: int, mask: int) -> void { - if gpu_kind == GPU_VK { gvk_blit(w, h, mask); return } +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 } gl_blit_framebuffer(0, 0, w, h, 0, 0, w, h, mask, GL_NEAREST) - gpu_glcheck_after("a blit") + gpu_glcheck_after(render3d_st, "a blit") } # the framebuffer the finished frame is presented from (an offscreen one, headless) -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") } +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") } # 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() -> 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 } +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 } if on { gl_polygon_mode(GL_FRONT_AND_BACK, GL_LINE) } else { gl_polygon_mode(GL_FRONT_AND_BACK, GL_FILL) } - gpu_glcheck_after("wireframe") + gpu_glcheck_after(render3d_st, "wireframe") } # the presented frame as RGB8, bottom row first (a photograph) -function gpu_read_screen(w: int, h: int, out: pointer) -> void { - if gpu_kind == GPU_VK { gvk_read_screen(w, h, out); return } +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 } 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(`a {w}x{h} read of the screen`) + gpu_glcheck_after(render3d_st, `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 885c3b60..498ae78d 100644 --- a/packages/ludic.render3d/gpu_manifest.ludic +++ b/packages/ludic.render3d/gpu_manifest.ludic @@ -34,7 +34,6 @@ 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 @@ -47,8 +46,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(path: string) -> int { - gpu_variants = new []GpuVariant +function gpu_manifest_load(render3d_st: mut Render3dState, path: string) -> int { + render3d_st.gpu_variants = new []GpuVariant let text = Fs.read_text(path) if text == null { return 0 } let lines = Text.split(text, "\n") @@ -68,7 +67,7 @@ function gpu_manifest_load(path: string) -> int { k += 1 } cur = gpu_variant_new(f[1], f[2], f[3], defs) - push(gpu_variants, cur) + push(render3d_st.gpu_variants, cur) continue } if cur == null { continue } @@ -85,18 +84,18 @@ function gpu_manifest_load(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(gpu_variants) + return len(render3d_st.gpu_variants) } # the variant for a program as r3d_program names it (defines with real newlines) -function gpu_variant_find(vs: string, fs: string, defines: string) -> GpuVariant { - return gpu_variant_find_key(vs, fs, Text.replace(defines, "\n", ";")) +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", ";")) } # ... or as variants.list writes it (';' for newlines) -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] +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] 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 d9971016..69743591 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -7,38 +7,24 @@ # 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(what: string, r: int) -> bool { - gvk_why = `{what} failed (VkResult {r})` - gvk_note(`r3d: vulkan: {gvk_why}`) +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}`) 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. -var gvk_errlog: string = null -var gvk_errlog_read: bool = false -function gvk_note(msg: string) -> void { +function gvk_note(render3d_st: mut Render3dState, msg: string) -> void { print(msg) - if not gvk_errlog_read { - gvk_errlog_read = true - if r3d_env_has("R3D_VK_ERRLOG") { gvk_errlog = r3d_env("R3D_VK_ERRLOG") } + 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 gvk_errlog == null { return } - let f = file_open(gvk_errlog, "ab") + if render3d_st.gvk_errlog == null { return } + let f = file_open(render3d_st.gvk_errlog, "ab") if f == null { return } let line = msg + "\n" file_write(f, line, len(line)) @@ -56,16 +42,12 @@ 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() -> bool { - if gvk_ready { return true } - gsl_boot() - if Vk.open() == 0 { gvk_why = "no Vulkan loader"; return false } - gsl_init() +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) let cnt = bytes(4) Vk.put_i32(cnt, 0, 0) @@ -91,50 +73,50 @@ function gvk_init() -> 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 - 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 { + 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 { 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 - 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 } + 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 } 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("vkCreateInstance", r) } - gvk_inst = Vk.get_ptr(out, 0) + if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateInstance", r) } + render3d_st.gvk_inst = Vk.get_ptr(out, 0) Vk.put_i32(cnt, 0, 0) - Vk.enumerate_physical_devices(gvk_inst, cnt, null) + Vk.enumerate_physical_devices(render3d_st.gvk_inst, cnt, null) let nd = Vk.get_i32(cnt, 0) - if nd == 0 { gvk_why = "no Vulkan device"; return false } + if nd == 0 { render3d_st.gvk_why = "no Vulkan device"; return false } let devs = bytes(nd * 8 + 8) - Vk.enumerate_physical_devices(gvk_inst, cnt, devs) + Vk.enumerate_physical_devices(render3d_st.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 } } - 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)) + 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)) Vk.put_i32(cnt, 0, 0) - Vk.get_physical_device_queue_family_properties(gvk_pd, cnt, null) + Vk.get_physical_device_queue_family_properties(render3d_st.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(gvk_pd, cnt, qprops) + Vk.get_physical_device_queue_family_properties(render3d_st.gvk_pd, cnt, qprops) for q in 0 .. nq { let flags = Vk.get_i32(qprops, q * VkQueueFamilyProperties_sizeof + VkQueueFamilyProperties_queueFlags) - if gvk_family < 0 and (flags & VK_QUEUE_GRAPHICS_BIT) != 0 { gvk_family = q } + if render3d_st.gvk_family < 0 and (flags & VK_QUEUE_GRAPHICS_BIT) != 0 { render3d_st.gvk_family = q } } - if gvk_family < 0 { gvk_why = `{gvk_device_name} has no graphics queue`; return false } + if render3d_st.gvk_family < 0 { render3d_st.gvk_why = `{render3d_st.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) @@ -148,16 +130,16 @@ function gvk_init() -> 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(gvk_pd, f2) + Vk.get_physical_device_features2(render3d_st.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 { gvk_why = `{gvk_device_name} lacks{missing}`; return false } + if len(missing) > 0 { render3d_st.gvk_why = `{render3d_st.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 { gvk_max_aniso = Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_maxSamplerAnisotropy) } + if aniso { render3d_st.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) @@ -167,7 +149,7 @@ function gvk_init() -> 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 gsl_on and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_privateData) == 1 { Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_privateData, 1) } + if render3d_st.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) @@ -182,30 +164,30 @@ function gvk_init() -> 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. - 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 { + 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 { Vk.put_i32(want2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_multiDrawIndirect, 1) Vk.put_i32(want2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_drawIndirectFirstInstance, 1) } - gvk_has_dic = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_drawIndirectCount) == 1 - if gvk_has_dic { Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_drawIndirectCount, 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) } # 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. - gvk_has_hqr = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_hostQueryReset) == 1 and Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampComputeAndGraphics) == 1 - if gvk_has_hqr { + 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 { Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_hostQueryReset, 1) - gvk_ts_period = float_from_bits(Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampPeriod)) + render3d_st.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(gvk_pd, null, cnt, null) + Vk.enumerate_device_extension_properties(render3d_st.gvk_pd, null, cnt, null) let nde = Vk.get_i32(cnt, 0) let dexts = bytes(nde * VkExtensionProperties_sizeof + 8) - Vk.enumerate_device_extension_properties(gvk_pd, null, cnt, dexts) + Vk.enumerate_device_extension_properties(render3d_st.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. - gvk_has_mesh = false - if gvk_ext_in(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME) and not r3d_env_has("R3D_NO_MESH") { + 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") { 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) @@ -213,14 +195,14 @@ function gvk_init() -> 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(gvk_pd, qm) + Vk.get_physical_device_features2(render3d_st.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) - gvk_has_mesh = true + render3d_st.gvk_has_mesh = true } } let prio = bytes(4) @@ -228,7 +210,7 @@ function gvk_init() -> 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, gvk_family) + Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueFamilyIndex, render3d_st.gvk_family) Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueCount, 1) Vk.put_ptr(qci, VkDeviceQueueCreateInfo_pQueuePriorities, prio) let dext_names = bytes(48) @@ -240,38 +222,38 @@ function gvk_init() -> 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 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 } + 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 } 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 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 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 } # 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 - 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 } + 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 } } - if gvk_has_mesh { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_MESH_SHADER_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 n_dext > 0 { Vk.put_i32(dci, VkDeviceCreateInfo_enabledExtensionCount, n_dext) Vk.put_ptr(dci, VkDeviceCreateInfo_ppEnabledExtensionNames, dext_names) } - 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 } + 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 } } - gvk_mp = bytes(VkPhysicalDeviceMemoryProperties_sizeof) - Vk.get_physical_device_memory_properties(gvk_pd, gvk_mp) + render3d_st.gvk_mp = bytes(VkPhysicalDeviceMemoryProperties_sizeof) + Vk.get_physical_device_memory_properties(render3d_st.gvk_pd, render3d_st.gvk_mp) - 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}`) + 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}`) return true } @@ -284,9 +266,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(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) +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) if ((allowed >> t) & 1) == 1 and (pf & want) == want { return t } } return -1 @@ -294,12 +276,11 @@ function gvk_mem_type(allowed: int, want: int) -> int { # 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. -var gvk_n_allocs: int = 0 -function gvk_alloc(req: bytes, want: int) -> long { +function gvk_alloc(render3d_st: mut Render3dState, req: bytes, want: int) -> long { let allowed = Vk.get_i32(req, VkMemoryRequirements_memoryTypeBits) - var t = gvk_mem_type(allowed, want) + var t = gvk_mem_type(render3d_st, 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(allowed, 0) } + if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(render3d_st, allowed, 0) } let zero: long = 0 if t < 0 { print(`r3d: vulkan: no memory type for properties {want}`); return zero } let mai = bytes(VkMemoryAllocateInfo_sizeof) @@ -308,9 +289,9 @@ function gvk_alloc(req: bytes, want: int) -> long { 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(gvk_dev, mai, null, out) + let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out) if r != VK_SUCCESS { print(`r3d: vulkan: vkAllocateMemory failed (VkResult {r})`); return zero } - gvk_n_allocs += 1 + render3d_st.gvk_n_allocs += 1 return gvk_handle(out) } @@ -327,32 +308,19 @@ function gvk_alloc(req: bytes, want: int) -> long { 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() -> void { - if gvk_al_blk != null { return } +function gvk_mem_init(render3d_st: mut Render3dState) -> void { + if render3d_st.gvk_al_blk != null { return } let zero: long = 0 - 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) + 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) } # one vkAllocateMemory of `size` bytes of memory type t, mapped when host-visible; 0 on failure -function gvk_mem_raw(t: int, size: int, host: bool, out_map: []pointer) -> long { +function gvk_mem_raw(render3d_st: mut Render3dState, t: int, size: int, host: bool, out_map: []pointer) -> long { let zero: long = 0 let mai = bytes(VkMemoryAllocateInfo_sizeof) Vk.zero(mai, VkMemoryAllocateInfo_sizeof) @@ -361,32 +329,32 @@ function gvk_mem_raw(t: int, size: int, host: bool, out_map: []pointer) -> long Vk.put_i64(mai, VkMemoryAllocateInfo_allocationSize, size_l) Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t) let out = bytes(8) - 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 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 mem = gvk_handle(out) out_map[0] = null if host { let pp = bytes(8) - if Vk.map_memory(gvk_dev, mem, zero, size_l, 0, pp) != VK_SUCCESS { gvk_note("r3d: vulkan: vkMapMemory failed"); return zero } + if Vk.map_memory(render3d_st.gvk_dev, mem, zero, size_l, 0, pp) != VK_SUCCESS { gvk_note(render3d_st, "r3d: vulkan: vkMapMemory failed"); return zero } out_map[0] = Vk.get_ptr(pp, 0) } return mem } -function gvk_fr_put(b: int, off: int, len_: int) -> void { +function gvk_fr_put(render3d_st: mut Render3dState, b: int, off: int, len_: int) -> void { if len_ <= 0 { return } - 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_) + 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_) } # Memory for a resource from its requirements (a VkMemoryRequirements): an allocation id, 0 if none. -function gvk_mem_new(req: bytes, want: int, image: bool) -> int { - gvk_mem_init() +function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, image: bool) -> int { + gvk_mem_init(render3d_st) let allowed = Vk.get_i32(req, VkMemoryRequirements_memoryTypeBits) - 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 } + 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 } 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))) @@ -400,17 +368,17 @@ function gvk_mem_new(req: bytes, want: int, image: bool) -> int { var kind = t * 2 if image { kind += 1 } var i = 0 - 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 + 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 if start + size <= end { - blk = gvk_fr_blk[i]; off = start - let front = start - gvk_fr_off[i] + blk = render3d_st.gvk_fr_blk[i]; off = start + let front = start - render3d_st.gvk_fr_off[i] let back = end - (start + size) - if front > 0 { gvk_fr_len[i] = front } else { gvk_fr_len[i] = 0 } - gvk_fr_put(blk, start + size, back) + 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) } } i += 1 @@ -418,32 +386,32 @@ function gvk_mem_new(req: bytes, want: int, image: bool) -> int { if blk < 0 { var bsize = GVK_BLOCK_DEVICE if host { bsize = GVK_BLOCK_HOST } - let mem = gvk_mem_raw(t, bsize, host, mp) + let mem = gvk_mem_raw(render3d_st, t, bsize, host, mp) if mem != 0 { - 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) + 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) } } } var own: long = 0 var map: pointer = null if blk < 0 { - own = gvk_mem_raw(t, size, host, mp) + own = gvk_mem_raw(render3d_st, t, size, host, mp) if own == 0 { return 0 } map = mp[0] - } else if gvk_blk_map[blk] != null { - map = mem_off(gvk_blk_map[blk], off) + } else if render3d_st.gvk_blk_map[blk] != null { + map = mem_off(render3d_st.gvk_blk_map[blk], off) } var a = 0 - 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) + 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) } else { - 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 + 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 } - 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 + 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 return a } @@ -453,87 +421,85 @@ function gvk_list_drop_last(l: []int) -> []int { return out } -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_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_offset(a: int) -> long { - let o: long = gvk_al_off[a] +function gvk_mem_offset(render3d_st: Render3dState, a: int) -> long { + let o: long = render3d_st.gvk_al_off[a] return o } -function gvk_mem_ptr(a: int) -> pointer { return gvk_al_map[a] } +function gvk_mem_ptr(render3d_st: Render3dState, a: int) -> pointer { return render3d_st.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(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] +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] let zero: long = 0 if b < 0 { - 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 + 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 } else { - var off = gvk_al_off[a] - var n = gvk_al_len[a] + var off = render3d_st.gvk_al_off[a] + var n = render3d_st.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(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] + 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] == off + n { - 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 } } - if off == 0 and n == gvk_blk_size[b] { + if off == 0 and n == render3d_st.gvk_blk_size[b] { # the block is empty again: give it back, or a world swapped out keeps its memory for good - 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 + 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 } else { - gvk_fr_put(b, off, n) + gvk_fr_put(render3d_st, b, off, n) } } - 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) + 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) } 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. -var gvk_pool: long = 0 -var gvk_fence: bytes = null -function gvk_cmd_init() -> bool { +function gvk_cmd_init(render3d_st: mut Render3dState) -> 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, gvk_family) + Vk.put_i32(cpi, VkCommandPoolCreateInfo_queueFamilyIndex, render3d_st.gvk_family) let out = bytes(8) - 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) + 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) let fci = bytes(VkFenceCreateInfo_sizeof) Vk.zero(fci, VkFenceCreateInfo_sizeof) Vk.put_i32(fci, VkFenceCreateInfo_sType, VK_STRUCTURE_TYPE_FENCE_CREATE_INFO) - gvk_fence = bytes(8) - r = Vk.create_fence(gvk_dev, fci, null, gvk_fence) - if r != VK_SUCCESS { return gvk_fail("vkCreateFence", r) } + 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) } return true } # a command buffer, begun -function gvk_once_begin() -> pointer { +function gvk_once_begin(render3d_st: Render3dState) -> 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, gvk_pool) + Vk.put_i64(cbai, VkCommandBufferAllocateInfo_commandPool, render3d_st.gvk_pool) Vk.put_i32(cbai, VkCommandBufferAllocateInfo_level, VK_COMMAND_BUFFER_LEVEL_PRIMARY) Vk.put_i32(cbai, VkCommandBufferAllocateInfo_commandBufferCount, 1) let cbs = bytes(8) - if Vk.allocate_command_buffers(gvk_dev, cbai, cbs) != VK_SUCCESS { return null } + if Vk.allocate_command_buffers(render3d_st.gvk_dev, cbai, cbs) != VK_SUCCESS { return null } let cb = Vk.get_ptr(cbs, 0) let cbbi = bytes(VkCommandBufferBeginInfo_sizeof) Vk.zero(cbbi, VkCommandBufferBeginInfo_sizeof) @@ -543,36 +509,36 @@ function gvk_once_begin() -> pointer { return cb } # end it, submit it, wait for it, free it -function gvk_once_end(cb: pointer) -> bool { +function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool { var r = Vk.end_command_buffer(cb) - if r != VK_SUCCESS { return gvk_fail("vkEndCommandBuffer", r) } + if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) } let cbs = bytes(8) Vk.put_ptr(cbs, 0, cb) - Vk.reset_fences(gvk_dev, 1, gvk_fence) + Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.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(gvk_queue, 1, si, Vk.get_i64(gvk_fence, 0)) - if r != VK_SUCCESS { return gvk_fail("vkQueueSubmit", r) } + 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) } let forever: long = -1 - 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) } + 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) } return true } # ---- teardown ----------------------------------------------------------------------------- -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 +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 } # ---- GPU timestamps (R3D_PROF) ------------------------------------------------------------ @@ -580,42 +546,38 @@ function gvk_shutdown() -> 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. -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 { +function gvk_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> void { for i in 0 .. n { ids[i] = i } - if not gvk_has_hqr or gvk_dev == null { return } + if not render3d_st.gvk_has_hqr or render3d_st.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(gvk_dev, qci, null, out) != VK_SUCCESS { return } - gvk_qpool = gvk_handle(out) - Vk.reset_query_pool(gvk_dev, gvk_qpool, 0, n * 2) + 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) } -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_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_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_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_result(id: int, out: words) -> bool { - if gvk_qpool == 0 { return false } +function gvk_query_result(render3d_st: Render3dState, id: int, out: words) -> bool { + if render3d_st.gvk_qpool == 0 { return false } let data = bytes(16) let size: long = 16 let stride: long = 8 - 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 } + 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 } 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) * gvk_ts_period) + out[0] = int(float(ticks) * render3d_st.gvk_ts_period) return true } diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index 93010847..00805c58 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -11,14 +11,8 @@ # 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(path: string) -> bytes { +function gvk_read_spv(render3d_st: mut Render3dState, path: string) -> bytes { let f = file_open(path, "rb") if f == null { return null } file_seek(f, 0, 2) @@ -27,24 +21,23 @@ function gvk_read_spv(path: string) -> bytes { let b = bytes(n + 4) file_read(f, b, n) file_close(f) - gvk_spv_len = n + render3d_st.gvk_spv_len = n return b } -var gvk_spv_len: int = 0 -function gvk_module(path: string) -> long { +function gvk_module(render3d_st: mut Render3dState, path: string) -> long { let zero: long = 0 - let spv = gvk_read_spv(path) + let spv = gvk_read_spv(render3d_st, 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 = gvk_spv_len + let code_size: long = render3d_st.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(gvk_dev, smci, null, out) - if r != VK_SUCCESS { gvk_fail(`vkCreateShaderModule {path}`, r); return zero } + 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 } return gvk_handle(out) } @@ -52,28 +45,27 @@ function gvk_module(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 -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 } +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 } return VK_SHADER_STAGE_VERTEX_BIT } -function gvk_program(p: int, key: string, spv_dir: string) -> bool { +function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_dir: string) -> bool { let zero: long = 0 - 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 + 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 } - 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) + 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) } let parts = Text.split(key, "|") var defs = "" if len(parts) > 2 { defs = parts[2] } - let v = gpu_variant_find_key(parts[0], parts[1], defs) + let v = gpu_variant_find_key(render3d_st, parts[0], parts[1], defs) if v == null { print(`r3d: vulkan: no SPIR-V variant for {key}`); return false } - 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`) + 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`) if vs == 0 or fs == 0 { return false } let nt = len(v.t_name) @@ -88,7 +80,7 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { 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(p)) + Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(render3d_st, p)) k += 1 } if v.fblock >= 0 { @@ -102,7 +94,7 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { 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(p) | VK_SHADER_STAGE_FRAGMENT_BIT) + Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(render3d_st, p) | VK_SHADER_STAGE_FRAGMENT_BIT) k += 1 } let dslci = bytes(VkDescriptorSetLayoutCreateInfo_sizeof) @@ -111,18 +103,18 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { 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(gvk_dev, dslci, null, dsl) - if r != VK_SUCCESS { return gvk_fail(`vkCreateDescriptorSetLayout for {key}`, r) } + 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) } 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(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) + 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) return true } @@ -130,18 +122,14 @@ function gvk_program(p: int, key: string, spv_dir: string) -> bool { # 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(name: string, n_bufs: int) -> int { +function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int { let zero: long = 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) + 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) } - let module = gvk_module(`{gvk_spv_dir}/{name}.comp.spv`) + let module = gvk_module(render3d_st, `{render3d_st.gvk_spv_dir}/{name}.comp.spv`) if module == 0 { return 0 } let nb = n_bufs + 1 let bw = VkDescriptorSetLayoutBinding_sizeof @@ -161,16 +149,16 @@ function gvk_compute_new(name: string, n_bufs: int) -> int { 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(gvk_dev, dslci, null, dsl) - if r != VK_SUCCESS { gvk_fail(`vkCreateDescriptorSetLayout for compute {name}`, r); return 0 } + 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 } 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(gvk_dev, plci, null, out) - if r != VK_SUCCESS { gvk_fail(`vkCreatePipelineLayout for compute {name}`, r); return 0 } + 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 } let layout = gvk_handle(out) let cpci = bytes(VkComputePipelineCreateInfo_sizeof) Vk.zero(cpci, VkComputePipelineCreateInfo_sizeof) @@ -181,33 +169,33 @@ function gvk_compute_new(name: string, n_bufs: int) -> int { 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(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 + 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 } # 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(c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void { +function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void { let zero: long = 0 - 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] + 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] 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, gvk_dpool) + Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_dpool) Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1) let layouts = bytes(8) - Vk.put_i64(layouts, 0, gvk_cp_dsl[c]) + Vk.put_i64(layouts, 0, render3d_st.gvk_cp_dsl[c]) Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts) let sets = bytes(8) - let r = Vk.allocate_descriptor_sets(gvk_dev, dsai, sets) - if r != VK_SUCCESS { gvk_fail("vkAllocateDescriptorSets (compute)", r); return } + 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 set = Vk.get_i64(sets, 0) - let at = gvk_ring_put(params, n_params) + let at = gvk_ring_put(render3d_st, 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 @@ -216,13 +204,13 @@ function gvk_dispatch(c: int, params: pointer, n_params: int, bufs: words, gx: i let infos = bytes(bw * (nb + 1)) Vk.zero(infos, bw * (nb + 1)) for k in 0 .. nb + 1 { - var buf = gvk_ring_buf + var buf = render3d_st.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 = gvk_buf_size[buf]; gvk_buf_used[buf] = gvk_frame_no } - Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_buffer, gvk_buf[buf]) + 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]) 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) @@ -232,31 +220,31 @@ function gvk_dispatch(c: int, params: pointer, n_params: int, bufs: words, gx: i 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(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.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.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() -> void { - let c = gvk_compute_new("probe", 1) +function gvk_compute_probe(render3d_st: mut Render3dState) -> void { + let c = gvk_compute_new(render3d_st, "probe", 1) if c == 0 { print("r3d: vulkan compute probe: FAILED (no program)"); return } let n = 1000 - let b = gvk_buf_new() + let b = gvk_buf_new(render3d_st) let data = bytes(n * 4) for i in 0 .. n { Vk.put_i32(data, i * 4, float_bits(float(i))) } - gvk_buf_upload(b, n * 4, data) - gvk_buf_gpu_owned(b) + gvk_buf_upload(render3d_st, b, n * 4, data) + gvk_buf_gpu_owned(render3d_st, 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(c, pr, 8, bufs, (n + 63) / 64, 1, 1) - gvk_flush() + gvk_dispatch(render3d_st, c, pr, 8, bufs, (n + 63) / 64, 1, 1) + gvk_flush(render3d_st) var bad = 0 - let mp = gvk_buf_map[b] + let mp = render3d_st.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)`) } } @@ -281,8 +269,6 @@ 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 { @@ -297,16 +283,14 @@ 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) -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() +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) let z = bytes(65536) Vk.zero(z, 65536) - gvk_buf_upload(gvk_zero_vbuf, 65536, z) + gvk_buf_upload(render3d_st, render3d_st.gvk_zero_vbuf, 65536, z) } - return gvk_zero_vbuf + return render3d_st.gvk_zero_vbuf } function gvk_attr_format(comps: int, type: int, normalized: int) -> int { @@ -367,24 +351,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(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long { +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 { let zero: long = 0 - let v = gvk_prog_var[p] + let v = render3d_st.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 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] } } + 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] } } 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(p)) - Vk.put_i64(stages, VkPipelineShaderStageCreateInfo_module, gvk_prog_vs[p]) + 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_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, gvk_prog_fs[p]) + Vk.put_i64(stages, ss + VkPipelineShaderStageCreateInfo_module, render3d_st.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; @@ -533,7 +517,7 @@ function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: in 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(p) == VK_SHADER_STAGE_VERTEX_BIT { + if gvk_stage_first(render3d_st, p) == VK_SHADER_STAGE_VERTEX_BIT { Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pVertexInputState, vin) Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pInputAssemblyState, ias) } @@ -543,15 +527,15 @@ function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: in 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, gvk_prog_layout[p]) + Vk.put_i64(gpci, VkGraphicsPipelineCreateInfo_layout, render3d_st.gvk_prog_layout[p]) let out = bytes(8) - 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 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 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() { 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) + 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) return pipe } @@ -561,14 +545,6 @@ function gvk_pipeline(p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: in # 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 @@ -576,34 +552,34 @@ function gvk_same(a: pointer, b: pointer, n: int) -> bool { return true } -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 +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 } let zero: long = 0 - 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) + 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) } - 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 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 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 } - gvk_prog_tex[p] = t - gvk_prog_unit[p] = u + render3d_st.gvk_prog_tex[p] = t + render3d_st.gvk_prog_unit[p] = u let st = words(nt) for i in 0 .. nt { st[i] = -1 } - gvk_set_tex[p] = st + render3d_st.gvk_set_tex[p] = st } -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] +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] 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 @@ -613,23 +589,23 @@ function gvk_uniform(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(loc: int, src: pointer, size: int, n: int) -> void { +function gvk_u_set(render3d_st: mut Render3dState, loc: int, src: pointer, size: int, n: int) -> void { if loc < 0 { return } let p = loc / 4096 - let v = gvk_prog_var[p] + let v = render3d_st.gvk_prog_var[p] if v == null { return } - gvk_uniform_blocks(p) + gvk_uniform_blocks(render3d_st, p) if loc % 4096 >= 2048 { let si = loc % 4096 - 2048 - 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 } + 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 } 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 = gvk_ublk_v[p] + var blk = render3d_st.gvk_ublk_v[p] var cap = v.vblock - if v.u_stage[j] == 1 { blk = gvk_ublk_f[p]; cap = v.fblock } + if v.u_stage[j] == 1 { blk = render3d_st.gvk_ublk_f[p]; cap = v.fblock } if blk == null { continue } var stride = v.u_stride[j] if stride == 0 { stride = size } @@ -640,19 +616,19 @@ function gvk_u_set(loc: int, src: pointer, size: int, n: int) -> void { # 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) - gvk_prog_dirty[p] = 1 + render3d_st.gvk_prog_dirty[p] = 1 } } } } # a sampler uniform: the texture for the manifest binding with that name -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] +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] if v == null { return } - 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 } } + 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 } } } # ---- per-frame uniform ring and descriptor sets ------------------------------------------- @@ -660,27 +636,21 @@ function gvk_bind_texture(p: int, name: string, tex: int) -> void { # 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() -> bool { +function gvk_frame_init(render3d_st: mut Render3dState) -> bool { let props = bytes(VkPhysicalDeviceProperties_sizeof) - Vk.get_physical_device_properties(gvk_pd, props) + Vk.get_physical_device_properties(render3d_st.gvk_pd, props) let al = Vk.get_i64(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_minUniformBufferOffsetAlignment) - gvk_ring_align = Text.to_int(string(al)) - if gvk_ring_align < 16 { gvk_ring_align = 16 } + render3d_st.gvk_ring_align = Text.to_int(string(al)) + if render3d_st.gvk_ring_align < 16 { render3d_st.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) - 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 } + 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 } let sizes = bytes(VkDescriptorPoolSize_sizeof * 3) Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, 32768) @@ -695,27 +665,27 @@ function gvk_frame_init() -> 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(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 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 px = bytes(4) px[0] = 255; px[1] = 255; px[2] = 255; px[3] = 255 - 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) + 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) } -function gvk_frame_reset() -> void { - gvk_ring_off = 0 - Vk.reset_descriptor_pool(gvk_dev, gvk_dpool, 0) +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) } # a block into the ring; its offset, or -1 when the frame has used the whole ring -function gvk_ring_put(blk: pointer, n: int) -> int { - let at = (gvk_ring_off + gvk_ring_align - 1) / gvk_ring_align * gvk_ring_align +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 if at + n > GVK_RING_BYTES { return -1 } - mem_copy(mem_off(gvk_buf_map[gvk_ring_buf], at), blk, n) - gvk_ring_off = at + n + mem_copy(mem_off(render3d_st.gvk_buf_map[render3d_st.gvk_ring_buf], at), blk, n) + render3d_st.gvk_ring_off = at + n return at } @@ -728,23 +698,8 @@ function gvk_ring_put(blk: pointer, n: int) -> int { # 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() -> bool { +function gvk_kpool_make(render3d_st: mut Render3dState) -> 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) @@ -757,100 +712,100 @@ function gvk_kpool_make() -> 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(gvk_dev, dpci, null, out) - if r != VK_SUCCESS { return gvk_fail("vkCreateDescriptorPool (kept sets)", r) } - gvk_kpool = gvk_handle(out) - gvk_sc_clear() + 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) return true } -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 } +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 } } # the pool is full: finish the frame recorded so far (it may use sets from it), then start again -function gvk_kpool_reset() -> void { - gvk_flush() - Vk.reset_descriptor_pool(gvk_dev, gvk_kpool, 0) - gvk_sc_clear() +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_draw_set(p: int, tx: words, tx_w: int, tx_cap: int) -> long { +function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: int, tx_cap: int) -> long { let zero: long = 0 - 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 + 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 } - while len(gvk_ub_frame) <= p { push(gvk_ub_frame, 0); push(gvk_ub_offv, 0); push(gvk_ub_offf, 0) } + 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) } let nt = len(v.t_name) # the key: every texture as this draw resolves it, and its sampler - while len(gvk_sc_tmp) < nt * 2 + 2 { push(gvk_sc_tmp, zero) } + while len(render3d_st.gvk_sc_tmp) < nt * 2 + 2 { push(render3d_st.gvk_sc_tmp, zero) } for t in 0 .. nt { - var tex = gvk_prog_tex[p][t] + var tex = render3d_st.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 = 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 } + 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 } var smp: long = 0 let o = tex * tx_w - 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]) + 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]) } 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 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 + 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 } - let tg: long = tex * 65536 + gvk_tex_gen[tex] - gvk_sc_tmp[t * 2] = tg - gvk_sc_tmp[t * 2 + 1] = 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 } var set: long = 0 var e = -1 - if p < 4096 { e = gvk_sc_head[p] } + if p < 4096 { e = render3d_st.gvk_sc_head[p] } while e >= 0 and set == 0 { - let ko = gvk_sc_koff[e] + let ko = render3d_st.gvk_sc_koff[e] var same = true var t = 0 - 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] } + 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] } } if set == 0 { - set = gvk_sc_make(p, nt) + set = gvk_sc_make(render3d_st, 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 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 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 at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return zero } - gvk_ub_offv[p] = at + render3d_st.gvk_ub_offv[p] = at } - if gvk_ublk_f[p] != null and v.fblock > 0 { - let at = gvk_ring_put(gvk_ublk_f[p], v.fblock) + 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 at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return zero } - gvk_ub_offf[p] = at + render3d_st.gvk_ub_offf[p] = at } - gvk_ub_frame[p] = gvk_frame_no - gvk_prog_dirty[p] = 0 + render3d_st.gvk_ub_frame[p] = render3d_st.gvk_frame_no + render3d_st.gvk_prog_dirty[p] = 0 } - 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 + 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 return set } # a new kept set for program p with the textures in gvk_sc_tmp, written and cached -function gvk_sc_make(p: int, nt: int) -> long { +function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long { let zero: long = 0 - let v = gvk_prog_var[p] + let v = render3d_st.gvk_prog_var[p] let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof) let layouts = bytes(8) let sets = bytes(8) @@ -859,14 +814,14 @@ function gvk_sc_make(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, gvk_kpool) + Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_kpool) Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1) - Vk.put_i64(layouts, 0, gvk_prog_dsl[p]) + Vk.put_i64(layouts, 0, render3d_st.gvk_prog_dsl[p]) Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts) - r = Vk.allocate_descriptor_sets(gvk_dev, dsai, sets) - if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(); tries += 1 } + r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets) + if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(render3d_st); tries += 1 } } - if r != VK_SUCCESS { gvk_fail("vkAllocateDescriptorSets (kept sets)", r); return zero } + if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (kept sets)", r); return zero } let set = Vk.get_i64(sets, 0) let ww = VkWriteDescriptorSet_sizeof let writes = bytes(ww * (nt + 3)) @@ -883,7 +838,7 @@ function gvk_sc_make(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, gvk_buf[gvk_ring_buf]) + Vk.put_i64(bis, at + VkDescriptorBufferInfo_buffer, render3d_st.gvk_buf[render3d_st.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) @@ -897,9 +852,9 @@ function gvk_sc_make(p: int, nt: int) -> long { } let iw = VkDescriptorImageInfo_sizeof for t in 0 .. nt { - 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]) + 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]) 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) @@ -909,14 +864,14 @@ function gvk_sc_make(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(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]) } + 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]) } var head = -1 - if p < 4096 { head = gvk_sc_head[p]; gvk_sc_head[p] = e } - push(gvk_sc_next, head) - gvk_sc_made += 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 return set } @@ -929,73 +884,42 @@ function gvk_sc_make(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(on: bool) -> void { +function r3d_hdr(render3d_st: mut Render3dState, on: bool) -> void { var want = on - 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 } + 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 } } -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 } +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 } # 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. -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 { +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 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 == 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 + 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 # the display is told what the picture now reaches - if gvk_hdr_on and gvk_has_hdr_meta and gvk_swap != 0 { gvk_hdr_metadata() } + if render3d_st.gvk_hdr_on and render3d_st.gvk_has_hdr_meta and render3d_st.gvk_swap != 0 { gvk_hdr_metadata(render3d_st) } } # what the picture is: graded in BT.709 around D65, highlights to the calibrated peak, paper white average -function gvk_hdr_metadata() -> void { +function gvk_hdr_metadata(render3d_st: Render3dState) -> void { let md = bytes(VkHdrMetadataEXT_sizeof) Vk.zero(md, VkHdrMetadataEXT_sizeof) Vk.put_i32(md, VkHdrMetadataEXT_sType, VK_STRUCTURE_TYPE_HDR_METADATA_EXT) @@ -1003,53 +927,53 @@ function gvk_hdr_metadata() -> 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())) + Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, float_bits(r3d_hdr_peak_nits(render3d_st))) Vk.put_i32(md, VkHdrMetadataEXT_minLuminance, float_bits(0.001)) - Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, float_bits(r3d_hdr_peak_nits())) - Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, float_bits(r3d_hdr_paper_nits())) + 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))) let chains = bytes(8) - Vk.put_i64(chains, 0, gvk_swap) - Vk.set_hdr_metadata_ext(gvk_dev, 1, chains, md) + Vk.put_i64(chains, 0, render3d_st.gvk_swap) + Vk.set_hdr_metadata_ext(render3d_st.gvk_dev, 1, chains, md) } -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 } +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 } } - 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) + 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) } -function gvk_frame_cb() -> pointer { - if gvk_cb == null { - gvk_frame_reset() - gvk_cb = gvk_once_begin() +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) } - return gvk_cb + return render3d_st.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(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 +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 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, gvk_tex_image[tex]) + Vk.put_i64(vci, VkImageViewCreateInfo_image, render3d_st.gvk_tex_image[tex]) Vk.put_i32(vci, VkImageViewCreateInfo_viewType, VK_IMAGE_VIEW_TYPE_2D) - Vk.put_i32(vci, VkImageViewCreateInfo_format, gvk_tex_vkfmt[tex]) + Vk.put_i32(vci, VkImageViewCreateInfo_format, render3d_st.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) @@ -1057,15 +981,15 @@ function gvk_view_of(tex: int, layer1: int) -> long { Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, 1) let out = bytes(8) let zero: long = 0 - 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)) + 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)) 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(cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void { +function gvk_att_barrier(render3d_st: Render3dState, cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void { # an attachment whose image was never made (no memory for it) has nothing to transition - if tex <= 0 or tex >= len(gvk_tex_image) or gvk_tex_image[tex] == 0 { return } + if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { return } let b = bytes(VkImageMemoryBarrier_sizeof) Vk.zero(b, VkImageMemoryBarrier_sizeof) Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER) @@ -1075,129 +999,129 @@ function gvk_att_barrier(cb: pointer, tex: int, layer1: int, depth: bool, old_la 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, gvk_tex_image[tex]) + Vk.put_i64(b, VkImageMemoryBarrier_image, render3d_st.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, gvk_tex_layers[tex]) } + if layer1 == 0 { Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, render3d_st.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(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 +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 if fb == 0 { - gvk_pass_col[0] = gvk_screen_color; gvk_pass_ncolor = 1 - gvk_pass_dep[0] = gvk_screen_depth + 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 return } if rec_o < 0 { return } var want = 1 - if fb < 4096 { want = gvk_fb_ncolor[fb] } + if fb < 4096 { want = render3d_st.gvk_fb_ncolor[fb] } for slot in 0 .. 2 { if slot < want and rec[rec_o + slot] > 0 { - 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_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_dep[0] = rec[rec_o + 2] - gvk_pass_dep[1] = rec[rec_o + 3] + render3d_st.gvk_pass_dep[0] = rec[rec_o + 2] + render3d_st.gvk_pass_dep[1] = rec[rec_o + 3] } -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) +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) let aw = VkRenderingAttachmentInfo_sizeof let catt = bytes(aw * 3) Vk.zero(catt, aw * 3) - 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) + 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) 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(tex, layer1)) + Vk.put_i64(catt, c * aw + VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, 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 (gvk_clear_bits & GL_COLOR_BUFFER_BIT) != 0 { + if (render3d_st.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(gvk_clear_rgba[k])) } + for k in 0 .. 4 { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_clearValue + k * 4, float_bits(render3d_st.gvk_clear_rgba[k])) } } else { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) } - 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) } + 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) } } let datt = bytes(aw) Vk.zero(datt, aw) - let dtex = gvk_pass_dep[0] + let dtex = render3d_st.gvk_pass_dep[0] if dtex > 0 { - gvk_att_barrier(cb, dtex, gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL) + 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) Vk.put_i32(datt, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO) - Vk.put_i64(datt, VkRenderingAttachmentInfo_imageView, gvk_view_of(dtex, gvk_pass_dep[1])) + Vk.put_i64(datt, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, dtex, render3d_st.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 (gvk_clear_bits & GL_DEPTH_BUFFER_BIT) != 0 { + if (render3d_st.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) } - 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_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_clear_bits = 0 + render3d_st.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, gvk_pass_w) - Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_height, gvk_pass_h) + 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_layerCount, 1) - Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, gvk_pass_ncolor) + Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, render3d_st.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) - gvk_in_pass = true + render3d_st.gvk_in_pass = true } -function gvk_pass_end() -> void { - if not gvk_in_pass { return } - let cb = gvk_cb +function gvk_pass_end(render3d_st: mut Render3dState) -> void { + if not render3d_st.gvk_in_pass { return } + let cb = render3d_st.gvk_cb Vk.cmd_end_rendering(cb) - 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) + 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) } - 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) + 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) } - gvk_in_pass = false + render3d_st.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(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 +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 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 .. gvk_pass_ncolor { + for c in 0 .. render3d_st.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(gvk_clear_rgba[k])) } + for k in 0 .. 4 { Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue + k * 4, float_bits(render3d_st.gvk_clear_rgba[k])) } n += 1 } } - if (mask & GL_DEPTH_BUFFER_BIT) != 0 and gvk_pass_dep[0] > 0 { + if (mask & GL_DEPTH_BUFFER_BIT) != 0 and render3d_st.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 @@ -1205,72 +1129,72 @@ function gvk_clear(mask: int, rec: words, rec_o: int) -> void { if n == 0 { return } let rect = bytes(VkClearRect_sizeof) Vk.zero(rect, VkClearRect_sizeof) - 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_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_layerCount, 1) Vk.cmd_clear_attachments(cb, n, atts, 1, rect) } -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] } +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] } # 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(cb: pointer) -> void { +function gvk_set_view(render3d_st: Render3dState, cb: pointer) -> void { let vp = bytes(VkViewport_sizeof) Vk.zero(vp, VkViewport_sizeof) - 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_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_maxDepth, 0x3F800000) Vk.cmd_set_viewport(cb, 0, 1, vp) let sc = bytes(VkRect2D_sizeof) Vk.zero(sc, VkRect2D_sizeof) - 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]) + 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]) } else { - Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, gvk_pass_w) - Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, gvk_pass_h) + 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.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(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() +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) var t0: long = 0 if prof { t0 = gl_now_us() } - 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) + 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) 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 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`) + 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`) } return } var t1: long = 0 - if prof { t1 = gl_now_us(); gvk_us_pipe = gvk_us_pipe + (t1 - t0) } + if prof { t1 = gl_now_us(); render3d_st.gvk_us_pipe = render3d_st.gvk_us_pipe + (t1 - t0) } Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe) - gvk_set_view(cb) - let set = gvk_draw_set(p, tx, tx_w, tx_cap) + gvk_set_view(render3d_st, cb) + let set = gvk_draw_set(render3d_st, p, tx, tx_w, tx_cap) if set == 0 { return } - if prof { gvk_us_set = gvk_us_set + (gl_now_us() - t1) } + if prof { render3d_st.gvk_us_set = render3d_st.gvk_us_set + (gl_now_us() - t1) } let sets = bytes(8) Vk.put_i64(sets, 0, set) - 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] + 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] 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)) @@ -1288,45 +1212,45 @@ function gvk_draw(p: int, m: Mesh, st: GvkState, first: int, count: int, instanc 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, gvk_buf[m.attrs[o]]) - gvk_buf_used[m.attrs[o]] = gvk_frame_no + 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 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() - Vk.put_i64(bufs, nbd * 8, gvk_buf[zb]) - gvk_buf_used[zb] = gvk_frame_no + 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 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 gvk_mesh_x > 0 { + if render3d_st.gvk_mesh_x > 0 { # a mesh-shader dispatch (gpu_draw_mesh_tasks): the device's command, through a pointer - 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) } + 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) } 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, gvk_buf[m.ebo], zero, itype) - gvk_buf_used[m.ebo] = gvk_frame_no - if gvk_ind_buf > 0 { + 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 { # the draws are records in a buffer (gvk_draw_indirect_now); the GPU may have written them - 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) + 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) } else { - Vk.cmd_draw_indexed_indirect(cb, gvk_buf[gvk_ind_buf], ioff, gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof) + Vk.cmd_draw_indexed_indirect(cb, render3d_st.gvk_buf[render3d_st.gvk_ind_buf], ioff, render3d_st.gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof) } } else { Vk.cmd_draw_indexed(cb, n, instances, first, 0, 0) @@ -1334,46 +1258,46 @@ function gvk_draw(p: int, m: Mesh, st: GvkState, first: int, count: int, instanc } else { Vk.cmd_draw(cb, n, instances, first, 0) } - if prof { gvk_n_draws += 1; gvk_us_draw = gvk_us_draw + (gl_now_us() - t0) } + if prof { render3d_st.gvk_n_draws += 1; render3d_st.gvk_us_draw = render3d_st.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() -> 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() +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) } # 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() -> 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() +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) } # 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(path: string) -> bool { +function gvk_screenshot(render3d_st: mut Render3dState, path: string) -> bool { # the screen image is PQ-encoded 10-bit while HDR is on, which an 8-bit PPM would misread - 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 + 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 let px = bytes(w * h * 4) - if not gvk_tex_read(gvk_screen_color, GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px) { return false } + if not gvk_tex_read(render3d_st, render3d_st.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` @@ -1393,162 +1317,150 @@ function gvk_screenshot(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() -> 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 +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 } # 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(w: int, h: int, title: string) -> bool { +function gvk_open(render3d_st: mut Render3dState, 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 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] } + 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] } gl_scale = win_gl_scale() } - 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) } + 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) } 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(vs: string, fs: string, defines: string) -> int { - gvk_prog_counter += 1 - let p = gvk_prog_counter +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 let key = `{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, key) - if not gvk_program(p, key, gvk_spv_dir) { 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, key) + if not gvk_program(render3d_st, p, key, render3d_st.gvk_spv_dir) { return 0 } return p } -function gvk_mesh_free(m: Mesh) -> void { +function gvk_mesh_free(render3d_st: mut Render3dState, 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(m.vbufs[i]) } } } + if m.vbufs != null { for i in 0 .. m.n_vbufs { if m.vbufs[i] > 0 { gvk_buf_release(render3d_st, m.vbufs[i]) } } } m.n_vbufs = 0 m.vbo = 0 - if m.ebo > 0 { gvk_buf_release(m.ebo); m.ebo = 0 } + if m.ebo > 0 { gvk_buf_release(render3d_st, m.ebo); m.ebo = 0 } } -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(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_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_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_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_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_fb_colors(fb: int, n: int) -> void { if gvk_fb_ncolor != null and fb >= 0 and fb < 4096 { gvk_fb_ncolor[fb] = n } } +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 } } # 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(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_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_fb_forget(fb: int) -> void { - if fb == gvk_fb_cur { gvk_pass_end() } - gvk_fb_colors(fb, 1) +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) } # the render state gpu.ludic has cached, with OpenGL's defaults where nothing was set yet -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 +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 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 -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_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 } -function gvk_draw_indirect_now(m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void { +function gvk_draw_indirect_now(render3d_st: mut Render3dState, 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 } - 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 + 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 } -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)) +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)) } # the colour (and / or depth) of the read framebuffer into the draw framebuffer, same size -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) +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) if o < 0 { return 0 } - if depth { return gpu_fb[o + 2] } - return gpu_fb[o] + if depth { return render3d_st.gpu_fb[o + 2] } + return render3d_st.gpu_fb[o] } -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] +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] } # 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(cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void { +function gvk_resolve(render3d_st: mut Render3dState, 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(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) + 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) 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(src, 0)) + Vk.put_i64(att, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, 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(dst, 0)) + Vk.put_i64(att, VkRenderingAttachmentInfo_resolveImageView, gvk_view_of(render3d_st, 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) @@ -1562,14 +1474,14 @@ function gvk_resolve(cb: pointer, src: int, dst: int, depth: bool, w: int, h: in 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(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) + 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) } -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) +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) let ic = bytes(VkImageCopy_sizeof) Vk.zero(ic, VkImageCopy_sizeof) var aspect = VK_IMAGE_ASPECT_COLOR_BIT @@ -1581,26 +1493,26 @@ function gvk_copy(cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) 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, 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) + 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) } -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) } +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) } } # the screen as RGB8, bottom row first, as glReadPixels hands it back (a photograph) -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 } +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 } let dst: pointer = out for y in 0 .. h { for x in 0 .. w { - let o = (y * gvk_screen_w + x) * 4 + let o = (y * render3d_st.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] } @@ -1612,57 +1524,47 @@ function gvk_read_screen(w: int, h: int, out: pointer) -> void { 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() -> bool { - if gvk_surface != 0 { return true } +function gvk_surface_make(render3d_st: mut Render3dState) -> bool { + if render3d_st.gvk_surface != 0 { return true } let hwnd = win_native_window() - if hwnd == null { gvk_why = "no window to present to"; return false } + if hwnd == null { render3d_st.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(gvk_inst, sci, null, out) - if r != VK_SUCCESS { return gvk_fail("vkCreateWin32SurfaceKHR", r) } - gvk_surface = gvk_handle(out) + 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 ok = bytes(4) Vk.put_i32(ok, 0, 0) - 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 } + 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 } let fci = bytes(VkFenceCreateInfo_sizeof) Vk.zero(fci, VkFenceCreateInfo_sizeof) Vk.put_i32(fci, VkFenceCreateInfo_sType, VK_STRUCTURE_TYPE_FENCE_CREATE_INFO) - gvk_acq_fence = bytes(8) - return Vk.create_fence(gvk_dev, fci, null, gvk_acq_fence) == VK_SUCCESS + render3d_st.gvk_acq_fence = bytes(8) + return Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.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(w: int, h: int) -> bool { - if not gvk_surface_make() { return false } - Vk.device_wait_idle(gvk_dev) +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) let caps = bytes(VkSurfaceCapabilitiesKHR_sizeof) - Vk.get_physical_device_surface_capabilities_khr(gvk_pd, gvk_surface, caps) + Vk.get_physical_device_surface_capabilities_khr(render3d_st.gvk_pd, render3d_st.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(gvk_pd, gvk_surface, cnt, null) + Vk.get_physical_device_surface_formats_khr(render3d_st.gvk_pd, render3d_st.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(gvk_pd, gvk_surface, cnt, fmts) + Vk.get_physical_device_surface_formats_khr(render3d_st.gvk_pd, render3d_st.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) @@ -1672,7 +1574,7 @@ function gvk_swap_make(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 gvk_hdr_want and gvk_has_colorspace { + if render3d_st.gvk_hdr_want and render3d_st.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) @@ -1681,15 +1583,15 @@ function gvk_swap_make(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 != gvk_hdr_on { gvk_hdr_on = hdr; gvk_screen_make(gvk_screen_w, gvk_screen_h) } + 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) } Vk.put_i32(cnt, 0, 0) - Vk.get_physical_device_surface_present_modes_khr(gvk_pd, gvk_surface, cnt, null) + Vk.get_physical_device_surface_present_modes_khr(render3d_st.gvk_pd, render3d_st.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(gvk_pd, gvk_surface, cnt, modes) + Vk.get_physical_device_surface_present_modes_khr(render3d_st.gvk_pd, render3d_st.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 gvk_vsync { + if not render3d_st.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 } } } @@ -1699,7 +1601,7 @@ function gvk_swap_make(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, gvk_surface) + Vk.put_i64(sci, VkSwapchainCreateInfoKHR_surface, render3d_st.gvk_surface) Vk.put_i32(sci, VkSwapchainCreateInfoKHR_minImageCount, n) Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageFormat, fmt) Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageColorSpace, cs) @@ -1712,45 +1614,45 @@ function gvk_swap_make(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, gvk_swap) + Vk.put_i64(sci, VkSwapchainCreateInfoKHR_oldSwapchain, render3d_st.gvk_swap) let out = bytes(8) - 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() } + 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) } Vk.put_i32(cnt, 0, 0) - 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 + 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 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 gvk_hdr_on { cname = "HDR10" } - print(`r3d: vulkan swapchain {ew}x{eh}, {gvk_swap_n} images, {mname}, {cname}`) + if render3d_st.gvk_hdr_on { cname = "HDR10" } + print(`r3d: vulkan swapchain {ew}x{eh}, {render3d_st.gvk_swap_n} images, {mname}, {cname}`) return true } -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 } +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 } let idx = bytes(4) - Vk.reset_fences(gvk_dev, 1, gvk_acq_fence) + Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence) let forever: long = -1 let zero: long = 0 - 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) + 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) let i = Vk.get_i32(idx, 0) - let dst = Vk.get_i64(gvk_swap_images, i * 8) - let src = gvk_tex_image[gvk_screen_color] + let dst = Vk.get_i64(render3d_st.gvk_swap_images, i * 8) + let src = render3d_st.gvk_tex_image[render3d_st.gvk_screen_color] gvk_barrier(cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) let blit = bytes(VkImageBlit_sizeof) @@ -1758,93 +1660,83 @@ function gvk_present_window() -> 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, gvk_screen_h) - Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_x, gvk_screen_w) + 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_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, 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_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_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(cb) - gvk_cb = null + gvk_once_end(render3d_st, cb) + render3d_st.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, gvk_swap) + Vk.put_i64(chains, 0, render3d_st.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() - 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() + 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) } # a window's client area changed: the screen images and the swapchain follow it -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] +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] if w <= 0 or h <= 0 { return false } - if w == gl_w and h == gl_h and not gvk_swap_stale { return false } - gvk_flush() + if w == gl_w and h == gl_h and not render3d_st.gvk_swap_stale { return false } + gvk_flush(render3d_st) gl_w = w gl_h = h - gvk_screen_make(w, h) - gvk_swap_make(w, h) + gvk_screen_make(render3d_st, w, h) + gvk_swap_make(render3d_st, 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(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) +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) } # ---- 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. -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(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 } -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`) +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`) # 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 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)`) + 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)`) } let zero: long = 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 + 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 } # ---- the pipeline cache, by integers ----------------------------------------------------------- @@ -1852,23 +1744,15 @@ function gvk_prof_frame() -> 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(m: Mesh) -> int { +function gvk_layout_id(render3d_st: mut Render3dState, 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 gvk_layout_keys == null { gvk_layout_keys = new []string } + if render3d_st.gvk_layout_keys == null { render3d_st.gvk_layout_keys = new []string } var id = 0 - 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 } + 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 } m.vk_layout = id return id } @@ -1883,32 +1767,32 @@ function gvk_blend_index(f: int) -> int { if f == GL_ONE_MINUS_SRC_COLOR { return 7 } return 8 } -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) +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) 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 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 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 p < 4096 { - 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] } + 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] } } - 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] + 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] } } - let pipe = gvk_pipeline(p, m, st, n_color, color_fmt, depth_fmt, samples) + let pipe = gvk_pipeline(render3d_st, p, m, st, n_color, color_fmt, depth_fmt, samples) if pipe == 0 { return pipe } - 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 } + 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 } return pipe } diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 42e22560..902b7be3 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -45,46 +45,30 @@ 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() -> int { +function gvk_tex_new(render3d_st: mut Render3dState) -> int { let zero: long = 0 - 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) + 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) # handle 0 is "no texture", as it is on OpenGL - 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(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 + 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 } function gvk_mip_levels(w: int, h: int) -> int { @@ -129,9 +113,7 @@ 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 -var gvk_st_buf: long = 0 -var gvk_st_mem: long = 0 -function gvk_staging(n: int, usage: int) -> pointer { +function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> pointer { let size: long = n let bci = bytes(VkBufferCreateInfo_sizeof) Vk.zero(bci, VkBufferCreateInfo_sizeof) @@ -140,41 +122,41 @@ function gvk_staging(n: int, usage: int) -> pointer { 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(gvk_dev, bci, null, out) != VK_SUCCESS { return null } - gvk_st_buf = gvk_handle(out) + if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return null } + render3d_st.gvk_st_buf = gvk_handle(out) let req = bytes(VkMemoryRequirements_sizeof) - 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) + 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) let zero: long = 0 - Vk.bind_buffer_memory(gvk_dev, gvk_st_buf, gvk_st_mem, zero) + Vk.bind_buffer_memory(render3d_st.gvk_dev, render3d_st.gvk_st_buf, render3d_st.gvk_st_mem, zero) let pp = bytes(8) - if Vk.map_memory(gvk_dev, gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null } + if Vk.map_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null } return Vk.get_ptr(pp, 0) } -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 +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 } # (Re)make the image behind a handle: glTexImage2D on a texture that already has one replaces it. -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 } +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 } 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(tex) + gvk_tex_release(render3d_st, tex) let depth = gvk_is_depth(ifmt) var levels = 1 if with_mips and not depth { levels = gvk_mip_levels(w, h) } - var samples = gvk_storage_samples + var samples = render3d_st.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 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 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 } let ici = bytes(VkImageCreateInfo_sizeof) Vk.zero(ici, VkImageCreateInfo_sizeof) Vk.put_i32(ici, VkImageCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO) @@ -191,22 +173,22 @@ function gvk_tex_storage(tex: int, array: bool, ifmt: int, w: int, h: int, layer 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(gvk_dev, ici, null, out) - if r != VK_SUCCESS { return gvk_fail(`vkCreateImage {w}x{h}x{layers} format {vkfmt}`, r) } + 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) } let image = gvk_handle(out) let req = bytes(VkMemoryRequirements_sizeof) - Vk.get_image_memory_requirements(gvk_dev, image, req) - let ma = gvk_mem_new(req, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, true) + 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) 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(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) + 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) } - 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) } + 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) } let vci = bytes(VkImageViewCreateInfo_sizeof) Vk.zero(vci, VkImageViewCreateInfo_sizeof) Vk.put_i32(vci, VkImageViewCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO) @@ -217,18 +199,18 @@ function gvk_tex_storage(tex: int, array: bool, ifmt: int, w: int, h: int, layer 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(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 } + 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 } # a target starts cleared-to-nothing but readable: every level in the layout samplers expect - let cb = gvk_once_begin() + let cb = gvk_once_begin(render3d_st) gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) - return gvk_once_end(cb) + return gvk_once_end(render3d_st, cb) } # IEEE single (as its bits) to IEEE half: out-of-range values saturate to infinity, tiny ones to zero @@ -255,17 +237,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(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> bool { +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 { 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(n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT) + let dst = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT) if dst == null { print("r3d: vulkan: no staging buffer for an upload"); return false } let buf = bytes(n) - if cin == cout and bin == bout and not (bin == 2 and gvk_unpack_swap) { + if cin == cout and bin == bout and not (bin == 2 and render3d_st.gvk_unpack_swap) { mem_copy(buf, data, n) } else { let src: pointer = data @@ -282,7 +264,7 @@ function gvk_tex_upload(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: i if bin == bout { if bin == 1 { buf[o] = src[i] } if bin == 2 { - 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 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 bin == 4 { Vk.put_i32(buf, o, Vk.get_i32(src, i)) } } else if bin == 4 and bout == 2 { @@ -290,7 +272,7 @@ function gvk_tex_upload(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: i 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() + gvk_staging_free(render3d_st) return false } } @@ -298,10 +280,10 @@ function gvk_tex_upload(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: i } } mem_copy(dst, buf, n) - let image = gvk_tex_image[tex] + let image = render3d_st.gvk_tex_image[tex] let depth = gvk_is_depth(ifmt) - let levels = gvk_tex_levels[tex] - let cb = gvk_once_begin() + let levels = render3d_st.gvk_tex_levels[tex] + let cb = gvk_once_begin(render3d_st) gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) let bic = bytes(VkBufferImageCopy_sizeof) Vk.zero(bic, VkBufferImageCopy_sizeof) @@ -311,27 +293,27 @@ function gvk_tex_upload(tex: int, ifmt: int, w: int, h: int, layers: int, fmt: i 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, gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic) + Vk.cmd_copy_buffer_to_image(cb, render3d_st.gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic) gvk_barrier(cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) - let ok = gvk_once_end(cb) - gvk_staging_free() + let ok = gvk_once_end(render3d_st, cb) + gvk_staging_free(render3d_st) 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(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] +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] let zero: long = 0 - 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() + 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_barrier(cb, old_image, false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_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) + 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) let ic = bytes(VkImageCopy_sizeof) Vk.zero(ic, VkImageCopy_sizeof) Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) @@ -341,31 +323,31 @@ function gvk_tex_grow_mips(tex: int, w: int, h: int) -> bool { 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, 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)) + 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)) return ok } -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 } +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 } } - let levels = gvk_tex_levels[tex] + let levels = render3d_st.gvk_tex_levels[tex] if levels <= 1 { return true } - let cb = gvk_once_begin() - gvk_tex_mips_into(cb, tex, w, h) - return gvk_once_end(cb) + let cb = gvk_once_begin(render3d_st) + gvk_tex_mips_into(render3d_st, cb, tex, w, h) + return gvk_once_end(render3d_st, cb) } # the chain's blits and barriers recorded into cb (the frame's own, when one is open) -function gvk_tex_mips_into(cb: pointer, tex: int, w: int, h: int) -> void { - let levels = gvk_tex_levels[tex] +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] if levels <= 1 { return } - let image = gvk_tex_image[tex] - let layers = gvk_tex_layers[tex] + let image = render3d_st.gvk_tex_image[tex] + let layers = render3d_st.gvk_tex_layers[tex] var sw = w var sh = h for lv in 1 .. levels { @@ -399,7 +381,7 @@ function gvk_tex_mips_into(cb: pointer, tex: int, w: int, h: int) -> void { # 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(tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, out: pointer) -> bool { +function gvk_tex_read(render3d_st: mut Render3dState, 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) @@ -410,11 +392,11 @@ function gvk_tex_read(tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, ou return false } let n = w * h * cout * bout - let src = gvk_staging(n, VK_BUFFER_USAGE_TRANSFER_DST_BIT) + let src = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_DST_BIT) if src == null { return false } - let image = gvk_tex_image[tex] + let image = render3d_st.gvk_tex_image[tex] let depth = gvk_is_depth(ifmt) - let cb = gvk_once_begin() + let cb = gvk_once_begin(render3d_st) gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) let bic = bytes(VkBufferImageCopy_sizeof) Vk.zero(bic, VkBufferImageCopy_sizeof) @@ -424,36 +406,34 @@ function gvk_tex_read(tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, ou 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, gvk_st_buf, 1, bic) + Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_st_buf, 1, bic) gvk_barrier(cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) - let ok = gvk_once_end(cb) + let ok = gvk_once_end(render3d_st, 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() + gvk_staging_free(render3d_st) return ok } -function gvk_tex_release(tex: int) -> void { - if gvk_tex_image[tex] == 0 { return } +function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void { + if render3d_st.gvk_tex_image[tex] == 0 { return } let zero: long = 0 - 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 + 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 } # ---- 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 } @@ -480,19 +460,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() -> float { +function gvk_mip_bias(render3d_st: mut Render3dState) -> float { # R3D_NO_MIPBIAS=1 puts it back the way it was, so one build can be compared against itself - 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 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 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(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()) +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)) var sig = min_f * 31 + mag_f sig = sig * 31 + wrap_s sig = sig * 31 + wrap_t @@ -501,16 +481,16 @@ function gvk_tex_sampler(tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: # 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(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 } + 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 } return s } -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()) +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)) let key = `{min_f}/{mag_f}/{wrap_s}/{wrap_t}/{compare}/{aniso}/{bias}` - 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] } } + 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] } } var mn = min_f if mn == 0 { mn = GL_NEAREST_MIPMAP_LINEAR } var mg = mag_f @@ -531,7 +511,7 @@ function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: if mipmapped { Vk.put_i32(sci, VkSamplerCreateInfo_mipLodBias, bias) } if aniso > 0x3F800000 and mipmapped { var a = aniso - if a > gvk_max_aniso { a = gvk_max_aniso } + if a > render3d_st.gvk_max_aniso { a = render3d_st.gvk_max_aniso } Vk.put_i32(sci, VkSamplerCreateInfo_anisotropyEnable, 1) Vk.put_i32(sci, VkSamplerCreateInfo_maxAnisotropy, a) } @@ -542,11 +522,11 @@ function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: 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(gvk_dev, sci, null, out) - if r != VK_SUCCESS { gvk_fail("vkCreateSampler", r); return zero } + 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 s = gvk_handle(out) - push(gvk_smp_keys, key) - push(gvk_smp, s) + push(render3d_st.gvk_smp_keys, key) + push(render3d_st.gvk_smp, s) return s } @@ -555,69 +535,60 @@ function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: # 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() -> int { +function gvk_buf_new(render3d_st: mut Render3dState) -> int { let zero: long = 0 - 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 + 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 # handle 0 is "no buffer", as it is on OpenGL - 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) } - 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 + 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 } -function gvk_buf_release(b: int) -> void { - if gvk_buf[b] == 0 { return } +function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { + if render3d_st.gvk_buf[b] == 0 { return } let zero: long = 0 - if gvk_buf_used[b] == gvk_frame_no { + if render3d_st.gvk_buf_used[b] == render3d_st.gvk_frame_no { # a draw recorded this frame still reads it: destroy it once the frame has been submitted - push(gvk_retired_buf, gvk_buf[b]); push(gvk_retired_mem, gvk_buf_mem[b]) + push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b]) } else { - Vk.destroy_buffer(gvk_dev, gvk_buf[b], null) - gvk_mem_free(gvk_mem_id(gvk_buf_mem[b])) + Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null) + gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b])) } - gvk_buf[b] = zero; gvk_buf_mem[b] = zero; gvk_buf_size[b] = 0; gvk_buf_map[b] = null; gvk_buf_used[b] = 0 + 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 } # the frame's submitted work is done: storage retired during it can go -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])) +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])) } - gvk_retired_buf = new []long - gvk_retired_mem = new []long + render3d_st.gvk_retired_buf = new []long + render3d_st.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). -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_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 } -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 } +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 } # 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(b: int, n: int) -> bool { - if b <= 0 or b >= len(gvk_buf) { return false } +function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool { + if b <= 0 or b >= len(render3d_st.gvk_buf) { return false } # already big enough, and no draw this frame reads what is there: fill it in place - if 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) + 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) var size = n if size < 64 { size = 64 } let size_l: long = size @@ -628,24 +599,24 @@ function gvk_buf_reserve(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(gvk_dev, bci, null, out) - if r != VK_SUCCESS { return gvk_fail(`vkCreateBuffer ({size} bytes)`, r) } + 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) } let buf = gvk_handle(out) let req = bytes(VkMemoryRequirements_sizeof) - 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) + Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req) + let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false) let zero: long = 0 - if ma == 0 { Vk.destroy_buffer(gvk_dev, buf, null); return false } + if ma == 0 { Vk.destroy_buffer(render3d_st.gvk_dev, buf, null); return false } let mem: long = 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) + 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) return true } # glBufferData: the whole buffer, from data (or storage only when data is null) -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) } +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) } return true } diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index d04bd717..b1e2d321 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -11,60 +11,35 @@ # ============================================================================ 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 -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 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 } } -function grass_mesh_live() -> bool { return grass_mesh_on and grass_merge and grass_mesh_prog != 0 and gpu_has_mesh() } +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) } # a blade: `rows` rows of 2 vertices (x across, y along, z bend), attribute 2 = uv -function grass_blade_mesh(rows: int) -> Mesh { - let m = gpu_mesh_new() +function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh { + let m = gpu_mesh_new(render3d_st) 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. @@ -94,9 +69,9 @@ function grass_blade_mesh(rows: int) -> Mesh { k += 5 } } - 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) + 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) free(v) let nq = rows - 1 let idx = words(nq * 6) @@ -105,65 +80,65 @@ function grass_blade_mesh(rows: int) -> Mesh { idx[q * 6] = b; idx[q * 6 + 1] = b + 1; idx[q * 6 + 2] = b + 2 idx[q * 6 + 3] = b + 1; idx[q * 6 + 4] = b + 3; idx[q * 6 + 5] = b + 2 } - gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), nq * 6 * 4, 4) free(idx) m.count = nq * 6 - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) return m } -function grass_init() -> void { - grass_merge = gpu_has_mdi() and not r3d_env_has("R3D_GRASS_TILES") +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") var defs = "#define FOLIAGE\n#define BLADE\n" - if grass_merge { + if render3d_st.grass_merge { defs = defs + "#define TILES\n" - 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_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_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") } + 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") } # 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 - grass_mesh = grass_blade_mesh(5) - grass_wind = 2.4 + render3d_st.grass_mesh = grass_blade_mesh(render3d_st, 5) + render3d_st.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. - 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")) } + 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")) } } # indices per 16 m cell that could exist at distance d (the count the shader computes) -function grass_count_at(d: float) -> int { - let spacing = grass_s0 * (1.0 + d / grass_d0) +function grass_count_at(render3d_st: Render3dState, d: float) -> int { + let spacing = render3d_st.grass_s0 * (1.0 + d / render3d_st.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(size: int, d_min: float, d_max: float) -> void { - let p = grass_prog +function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float) -> void { + let p = render3d_st.grass_prog let cells = size / GRASS_CELL - if grass_merge { - grass_band_start[grass_band_n] = grass_n; grass_band_cells[grass_band_n] = cells; grass_band_n += 1 + 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 } else { - u_i(gpu_uniform(p, "u_tile_cells"), cells) + u_i(render3d_st, gpu_uniform(render3d_st, 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((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 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 corner_r = half * 1.42 var tz = tz0 while tz <= tz1 { @@ -171,30 +146,30 @@ function grass_tiles(size: int, d_min: float, d_max: float) -> void { while tx <= tx1 { let ox = float(tx) * sz; let oz = float(tz) * sz let cx = ox + half; let cz = oz + half - let dx = cx - cam_pos[0]; let dz = cz - cam_pos[2] + let dx = cx - render3d_st.cam_pos[0]; let dz = cz - render3d_st.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(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 { + 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 { var inst = per * cells * cells if inst > 65535 { inst = 65535 } - 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 + 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 } } else if per > 0 { - 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 + 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 } } tx += 1 @@ -203,104 +178,104 @@ function grass_tiles(size: int, d_min: float, d_max: float) -> void { } } -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) +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) # 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 = grass_blade_tex - let bcols = grass_blade_cols - u_f(gpu_uniform(p, "u_blade_cols"), float(bcols)) + 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)) var bon = 0.0 - 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 + 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 var oon = 1.0 - 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) + 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) var lake = -100000.0 - if ter_lake_ex != 0.0 { lake = ter_lake_level } - u_f(gpu_uniform(p, "u_lake_level"), lake) + 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) var sea = lake - 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) + 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) # 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(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) + 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) } # the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw -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]) } +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]) } var k = s while k < e { var m = e - k if m > GRASS_CHUNK { m = GRASS_CHUNK } - for q in 0 .. m * 4 { grass_chunk_tv[q] = grass_tv[k * 4 + q] } + for q in 0 .. m * 4 { render3d_st.grass_chunk_tv[q] = render3d_st.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 = grass_band_cells[b] + let cells = render3d_st.grass_band_cells[b] for q in 0 .. m { - var total = int(grass_tv[(k + q) * 4 + 2]) * cells * cells + var total = int(render3d_st.grass_tv[(k + q) * 4 + 2]) * cells * cells if total > 65535 { total = 65535 } if total > 0 { - 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 + 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 } } } else { - u_f4v(gpu_uniform(p, "u_tiles"), m, grass_chunk_tv) - gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0) + 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) } - if not mesh { grass_draws += 1 } + if not mesh { render3d_st.grass_draws += 1 } k += m } } diff --git a/packages/ludic.render3d/hooks.ludic b/packages/ludic.render3d/hooks.ludic index 49b27dd2..059fa96e 100644 --- a/packages/ludic.render3d/hooks.ludic +++ b/packages/ludic.render3d/hooks.ludic @@ -1,17 +1,14 @@ # 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(f: fn()) -> void { r3d_draw_cb = f } +function r3d_on_draw(render3d_st: mut Render3dState, f: fn()) -> void { render3d_st.r3d_draw_cb = f } # what casts a shadow, drawn into the light's view `light_vp` -function r3d_on_casters(f: fn(floats)) -> void { r3d_casters_cb = f } +function r3d_on_casters(render3d_st: mut Render3dState, f: fn(floats)) -> void { render3d_st.r3d_casters_cb = f } # a streamed layer's chunk (cx, cz) at detail `band`, filled with its instances -function r3d_on_stream_fill(f: fn(Stream, int, int, int)) -> void { r3d_fill_cb = f } +function r3d_on_stream_fill(render3d_st: mut Render3dState, f: fn(Stream, int, int, int)) -> void { render3d_st.r3d_fill_cb = f } -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) } } +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) } } diff --git a/packages/ludic.render3d/mesh.ludic b/packages/ludic.render3d/mesh.ludic index 56ace605..058289f4 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(m: Mesh) -> void { gpu_draw_mesh(m) } -function mesh_draw_instanced(m: Mesh, n: int) -> void { gpu_draw_mesh_instanced(m, n) } +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) } # 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(n: int, half: float) -> Mesh { - let m = gpu_mesh_new() +function mesh_grid(render3d_st: mut Render3dState, n: int, half: float) -> Mesh { + let m = gpu_mesh_new(render3d_st) let nv = n * n let v = gl_floats(nv * 2) var k = 0 @@ -38,8 +38,8 @@ function mesh_grid(n: int, half: float) -> Mesh { k += 2 } } - gpu_mesh_vertices(m, v, gl_bytes_of(nv * 2), GPU_STATIC) - gpu_mesh_attr(m, 0, 2, GPU_F32, 8, 0, false) + 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) free(v) let ni = (n - 1) * (n - 1) * 6 let idx = words(ni) @@ -52,41 +52,41 @@ function mesh_grid(n: int, half: float) -> Mesh { k += 6 } } - gpu_mesh_indices(m, data_of(idx), ni * 4, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4) free(idx) m.count = ni - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) return m } # A full-screen triangle with no attributes (the vertex shader uses gl_VertexID). -function mesh_fullscreen() -> Mesh { - let m = gpu_mesh_new() - gpu_mesh_done(m) +function mesh_fullscreen(render3d_st: Render3dState) -> Mesh { + let m = gpu_mesh_new(render3d_st) + gpu_mesh_done(render3d_st, 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() -> Mesh { - let m = gpu_mesh_new() +function mesh_card(render3d_st: mut Render3dState) -> Mesh { + let m = gpu_mesh_new(render3d_st) 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(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) + 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) 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(m, data_of(idx), 24, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), 24, 4) free(idx) m.count = 6 - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) return m } # release a mesh's GPU objects (a mesh this package built for something being thrown away) -function mesh_free(m: Mesh) -> void { gpu_mesh_free(m) } +function mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void { gpu_mesh_free(render3d_st, m) } diff --git a/packages/ludic.render3d/overlay.ludic b/packages/ludic.render3d/overlay.ludic index bfd6939f..8eebd898 100644 --- a/packages/ludic.render3d/overlay.ludic +++ b/packages/ludic.render3d/overlay.ludic @@ -11,360 +11,322 @@ 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. -var ov_hdr_scale: float = 0.0 # float bits; 0 until ov_begin sets 1 -function ov_hdr_nits(nits: float) -> void { +function ov_hdr_nits(render3d_st: mut Render3dState, nits: float) -> void { var s = 1.0 - 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 + 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 } # back to the interface's own white -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") +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") } - if ov_prog_hdr != 0 { ov_prog = ov_prog_hdr; return } + if render3d_st.ov_prog_hdr != 0 { render3d_st.ov_prog = render3d_st.ov_prog_hdr; return } } - ov_prog = ov_prog_sdr + render3d_st.ov_prog = render3d_st.ov_prog_sdr } -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 +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 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(font_dir: string) -> bool { +function overlay_font(render3d_st: mut Render3dState, font_dir: string) -> bool { let meta = Fs.read_text(font_dir + "/font.json") if meta == null { return false } - let tex = tex_load_ex(font_dir + "/font.png", false, 0) + let tex = tex_load_ex(render3d_st, font_dir + "/font.png", false, 0) if tex == 0 { return false } let j = Json.parse(meta) - 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")) } + 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")) } 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") } - 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 + 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 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 { ov_font_map[cp] = i + 1 } + if cp >= 0 and cp < OV_MAP_N { render3d_st.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 - 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_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_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 + 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 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. -var ov_u8_len: int = 1 -function ov_u8(sp: pointer, i: int, n: int) -> int { +function ov_u8(render3d_st: mut Render3dState, sp: pointer, i: int, n: int) -> int { let b0 = sp[i] & 255 - ov_u8_len = 1 + render3d_st.ov_u8_len = 1 if b0 < 128 { return b0 } if b0 >= 240 and b0 < 248 and i + 3 < n { - ov_u8_len = 4 + render3d_st.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 { - ov_u8_len = 3 + render3d_st.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 { - ov_u8_len = 2 + render3d_st.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(cp: int) -> int { - if cp < 32 { return ov_font_space } +function ov_glyph(render3d_st: Render3dState, cp: int) -> int { + if cp < 32 { return render3d_st.ov_font_space } if cp < OV_MAP_N { - let g = ov_font_map[cp] + let g = render3d_st.ov_font_map[cp] if g > 0 { return g - 1 } - return ov_font_qmark + return render3d_st.ov_font_qmark } var lo = 0 - var hi = ov_font_hi_n - 1 + var hi = render3d_st.ov_font_hi_n - 1 while lo <= hi { let mid = (lo + hi) / 2 - 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 } + 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 } } - return ov_font_qmark + return render3d_st.ov_font_qmark } # start drawing onto the screen: blending on, depth off -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 +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 } # everything drawn until ov_unclip stays inside this rectangle (a scrolling list) -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_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_unclip() -> void { ov_close_range(); ov_clip_w = 0 } +function ov_unclip(render3d_st: mut Render3dState) -> void { ov_close_range(render3d_st); render3d_st.ov_clip_w = 0 } # close the current draw range (quads since its start, with the current texture) -function ov_close_range() -> void { - let n = ov_n - ov_range_start +function ov_close_range(render3d_st: mut Render3dState) -> void { + let n = render3d_st.ov_n - render3d_st.ov_range_start if n <= 0 { return } - 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 + 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 } # one upload of everything batched so far, then a draw per range -function ov_flush() -> void { - ov_close_range() - if ov_n == 0 { ov_nr = 0; ov_range_start = 0; return } +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 } # 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(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 + 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 if hs == 0.0 { hs = 1.0 } - u_f(gpu_uniform(ov_prog, "u_hdr_paper"), r3d_hdr_paper_nits()) - u_f(gpu_uniform(ov_prog, "u_hdr_scale"), hs) + 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) var last = -1 var clipped = false - for i in 0 .. ov_nr { + for i in 0 .. render3d_st.ov_nr { let o = i * OV_RANGE_W - 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 { + 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 { clipped = true - 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) + 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) } - if clipped { gpu_scissor_off() } - gpu_mesh_unbind() - ov_n = 0; ov_nr = 0; ov_range_start = 0 + 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 } -function ov_end() -> void { - if not ov_open { return } - ov_flush() - gpu_blend(false) - gpu_depth_test(true) - ov_open = false +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 } # 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(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 } +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 } } # one vertex into the batch -function ov_vert(k: int, x: float, y: float, u: float, v: float, r: float, g: float, b: float, a: float) -> void { +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 { let o = k * OV_FLOATS - 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)) + 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)) } # a textured quad, float-bit pixel corners and uvs -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 +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 } # a filled rectangle at integer pixels; colour as float bits 0..1 -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_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_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) +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) } # a whole texture at integer pixels -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) +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) } # the width in pixels of `s` at `size` pixels per em -function ov_text_w(size: int, s: string) -> int { - if ov_font_adv == null { return 0 } +function ov_text_w(render3d_st: mut Render3dState, size: int, s: string) -> int { + if render3d_st.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(ov_u8(sp, i, n)) - i += ov_u8_len - w = w + ov_font_adv[g] * float(size) + 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) } return int(w) } # text with its top-left at (x, y); returns the pen x after it -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 +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 var pen = float(x) let base = float(y) + float(size) * 0.80 - let px = ov_pad_x * float(ov_font_em) * k - let py = ov_base_y * float(ov_font_em) * k + 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 sp: pointer = s let n = len(sp) var i = 0 while i < n { - 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 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 x0 = pen - px; let y1 = base + py - ov_quad(x0, y1 - cell, x0 + cell, y1, u0, v0, u1, v1, r, g, b, a) + ov_quad(render3d_st, x0, y1 - cell, x0 + cell, y1, u0, v0, u1, v1, r, g, b, a) } - pen = pen + ov_font_adv[c] * float(size) + pen = pen + render3d_st.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(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int { +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 { let d = size / 18 + 1 - 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) + 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) } -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) +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) } # text wrapped at `maxw` pixels on spaces; returns the y after the last line -function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: float, g: float, b: float, a: float) -> int { +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 { let sp: pointer = s let n = len(sp) var first = 0 @@ -377,11 +339,11 @@ function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: float, 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(size, piece) > maxw and last_space > first { stop = last_space; break } + if ov_text_w(render3d_st, size, piece) > maxw and last_space > first { stop = last_space; break } i += 1 } let line: string = s[first .. stop] - ov_text(x, ly, size, line, r, g, b, a) + ov_text(render3d_st, 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 } @@ -391,14 +353,14 @@ function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: float, # ---- 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(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) +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) } # 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(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) +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) 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) @@ -406,33 +368,33 @@ function ov_nine(tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, ds 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(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(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) } } 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(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 +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 } # a line of thickness `t` pixels between two points (float-bit pixels) -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) +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) 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(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(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) } # a sub-rectangle of a texture rotated by `ang` radians about its centre (cx, cy), `w` x `h` pixels -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) +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) 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) @@ -441,24 +403,24 @@ function ov_sub_rot(tex: int, cx: int, cy: int, w: int, h: int, ang: float, u0: 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(x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, a) + ov_quad4(render3d_st, 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(cx: float, cy: float, rad: float, n: int, r: float, g: float, b: float, a: float) -> void { - ov_use_tex(ov_white) +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) 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(cx, cy, ax, ay, bx, by, cx, cy, 0.0, 0.0, 1.0, 1.0, r, g, b, a) + ov_quad4(render3d_st, 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(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(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 { let step = span / float(n) for i in 0 .. n { let b0 = a0 + float(i) * step; let b1 = b0 + step - 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) + 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) } } diff --git a/packages/ludic.render3d/post.ludic b/packages/ludic.render3d/post.ludic index 14d482bd..bf53ca1f 100644 --- a/packages/ludic.render3d/post.ludic +++ b/packages/ludic.render3d/post.ludic @@ -6,27 +6,6 @@ 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 - @@ -35,119 +14,72 @@ var post_exposure_max: float = 20.0 # 20: the ceiling auto-exposure may reach # 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() -> int { if gpu_hdr_active() { return GL_RGB10_A2 }; return GL_RGBA8 } -var post_sharpen: float = 0.0 -var post_grain: int = 0 +function post_ldr_fmt(render3d_st: Render3dState) -> int { if gpu_hdr_active(render3d_st) { return GL_RGB10_A2 }; return GL_RGBA8 } # the screen-sized targets go away before post_init makes them at a new size -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 +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 } # 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() -> bool { return gpu_is_gl() or gpu_msaa_max() > 1 } -function post_set_msaa(n: int) -> void { +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 { var want = n if want < 1 { want = 1 } - 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) + 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) } } -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) +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) } - post_bloom = new []Target + render3d_st.post_bloom = new []Target var bw = w / 2; var bh = h / 2 for i in 0 .. BLOOM_LEVELS { - push(post_bloom, target_new(max(bw, 1), max(bh, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)) + 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)) bw = bw / 2; bh = bh / 2 } - 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", "") + 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", "") } # 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 @@ -155,36 +87,36 @@ function post_init(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. - 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) + 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) var m = 1; var sz = max(w, h) while sz > 1 { sz = sz / 2; m += 1 } - post_mips = m - post_adapt = 0.0 + render3d_st.post_mips = m + render3d_st.post_adapt = 0.0 } # Mean scene luminance from the HDR mip chain -> exposure = key / mean, eased over @@ -197,63 +129,59 @@ function post_init(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. -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 +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 } - 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) + 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) var reset = 0.0 - 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) + 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) } -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) +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) } # resolve the multisampled scene into the plain HDR target (colour + depth) -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) +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) } # the depth copy every pass after this may read while the frame is still being drawn into - 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) + 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) } # There is no temporal anti-aliasing. It was reprojecting every pixel through the @@ -268,194 +196,194 @@ function post_resolve() -> 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() -> 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) +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) } # Keep a copy of the finished scene colour: the SSGI bounce reads last frame's colour. -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_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_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) +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) } # 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() -> 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) +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) # 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(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) + 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) } # 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() -> 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_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_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) +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) for i in 0 .. BLOOM_LEVELS { - 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)) + 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)) var th = -1.0 if i == 0 { th = 1.2 } - u_f(gpu_uniform(post_p_down, "u_threshold"), th) - mesh_draw(post_fs) + u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_down, "u_threshold"), th) + mesh_draw(render3d_st, render3d_st.post_fs) src = t.color; sw = t.w; sh = t.h } - gpu_use_program(post_p_up) - gpu_blend(true) - gpu_blend_func(GL_ONE, GL_ONE) + gpu_use_program(render3d_st, render3d_st.post_p_up) + gpu_blend(render3d_st, true) + gpu_blend_func(render3d_st, GL_ONE, GL_ONE) var i = BLOOM_LEVELS - 1 while i > 0 { - 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) + 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) i -= 1 } - gpu_blend(false) + gpu_blend(render3d_st, false) } -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 +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 } - 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_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_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) + 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) var auto = 0.0 - 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) + 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) # the HDR10 variant's display calibration (the SDR program has none of these, and -1 sets nothing) - 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) + 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) # sharpen + grain onto the screen - 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) + 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) # R3D_NOGRAIN=1: no film grain, so two frames of a still camera can be compared for what else moves - 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) + 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) } diff --git a/packages/ludic.render3d/prof.ludic b/packages/ludic.render3d/prof.ludic index e3fa3739..32bd1391 100644 --- a/packages/ludic.render3d/prof.ludic +++ b/packages/ludic.render3d/prof.ludic @@ -14,98 +14,89 @@ 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() -> 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 +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 } # the slot index for `name`, registering it on first sight (order = frame order) -function prof_slot(name: pointer) -> int { +function prof_slot(render3d_st: mut Render3dState, name: pointer) -> int { var i = 0 - while i < prof_n { - if prof_names[i] == name { return i } + while i < render3d_st.prof_n { + if render3d_st.prof_names[i] == name { return i } i += 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 + 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 } -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) +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) if s < 0 { return } - prof_active = s - gpu_query_begin(prof_ids[s * PROF_RING + (prof_frame % PROF_RING)]) + render3d_st.prof_active = s + gpu_query_begin(render3d_st, render3d_st.prof_ids[s * PROF_RING + (render3d_st.prof_frame % PROF_RING)]) } -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 +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 } # Collect the queries issued PROF_RING-1 frames ago — long finished, so no stall. -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 +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 var s = 0 - while s < prof_n { - let q = prof_ids[s * PROF_RING + slot_frame] - if gpu_query_result(q, prof_scratch) { + 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) { # the low 32 bits are ample: a pass is far below 4 seconds - prof_ns[s] = prof_ns[s] + prof_scratch[0] - prof_hits[s] = prof_hits[s] + 1 + 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 } s += 1 } } -function prof_report() -> void { - if not prof_on { return } +function prof_report(render3d_st: Render3dState) -> void { + if not render3d_st.prof_on { return } print("") print("GPU time per pass (mean over the run):") var total = 0 var s = 0 - while s < prof_n { - if prof_hits[s] > 0 { total = total + prof_ns[s] / prof_hits[s] } + 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] } s += 1 } s = 0 - while s < prof_n { - if prof_hits[s] > 0 { - let us = prof_ns[s] / prof_hits[s] / 1000 + 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 var pct = 0 - 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)}%`) + 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)}%`) } s += 1 } @@ -118,99 +109,66 @@ function prof_report() -> 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(n: int) -> void { - if not prof_on { return } - prof_gen_cur += n +function prof_gen_add(render3d_st: mut Render3dState, n: int) -> void { + if not render3d_st.prof_on { return } + render3d_st.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. -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() } } +function prof_before_swap(render3d_st: mut Render3dState) -> void { if render3d_st.prof_on { render3d_st.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. -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 } } +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 } } # ... and every phase's share of the average frame, past the first eight frames (loading, first fills) -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 } +function prof_cpu_mark(render3d_st: mut Render3dState, name: pointer) -> void { + if not render3d_st.prof_on { return } let now = gl_now_us() - let d = now - prof_mark_last - prof_mark_last = now - if d > prof_mark_best { prof_mark_best = d; prof_mark_name = name } + 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 } var k = -1 var i = 0 - while i < prof_mk_n { if prof_mk_names[i] == name { k = i }; i += 1 } + while i < render3d_st.prof_mk_n { if render3d_st.prof_mk_names[i] == name { k = i }; i += 1 } if k < 0 { - 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_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_gen != null and len(prof_gen) > 8 { prof_mk_us[k] = prof_mk_us[k] + d } + 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 } } # the game's own per-frame work, kept apart from the renderer's -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_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 } } -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_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_bake_add(us: long) -> void { if prof_on { prof_bake_us_cur = prof_bake_us_cur + us } } +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_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 +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 } - 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 + 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 } -function prof_chunk_report() -> void { - if not prof_on or prof_chunk_us == null { return } +function prof_chunk_report(render3d_st: Render3dState) -> void { + if not render3d_st.prof_on or render3d_st.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) @@ -220,18 +178,18 @@ function prof_chunk_report() -> void { var cnt = new []int var mx = new []long var i = 0 - while i < len(prof_chunk_us) { + while i < len(render3d_st.prof_chunk_us) { var f = -1 var j = 0 - while j < len(kinds) { if kinds[j] == prof_chunk_kind[i] and bands[j] == prof_chunk_band[i] { f = j }; j += 1 } + 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 } if f < 0 { - push(kinds, prof_chunk_kind[i]); push(bands, prof_chunk_band[i]) + push(kinds, render3d_st.prof_chunk_kind[i]); push(bands, render3d_st.prof_chunk_band[i]) push(tot, 0); push(cnt, 0); push(mx, 0) f = len(kinds) - 1 } - tot[f] = tot[f] + prof_chunk_us[i] + tot[f] = tot[f] + render3d_st.prof_chunk_us[i] cnt[f] = cnt[f] + 1 - if prof_chunk_us[i] > mx[f] { mx[f] = prof_chunk_us[i] } + if render3d_st.prof_chunk_us[i] > mx[f] { mx[f] = render3d_st.prof_chunk_us[i] } i += 1 } var k = 0 @@ -240,10 +198,10 @@ function prof_chunk_report() -> void { k += 1 } # the per-frame distribution of generation time: this is the hitch itself - if prof_gen_us == null or len(prof_gen_us) < 16 { return } + if render3d_st.prof_gen_us == null or len(render3d_st.prof_gen_us) < 16 { return } let sorted = new []long var a = 8 - while a < len(prof_gen_us) { push(sorted, prof_gen_us[a]); a += 1 } + while a < len(render3d_st.prof_gen_us) { push(sorted, render3d_st.prof_gen_us[a]); a += 1 } var x = 1 while x < len(sorted) { let v = sorted[x] @@ -260,48 +218,48 @@ function prof_chunk_report() -> 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() -> 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 +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 } - push(prof_gen, prof_gen_cur) - prof_gen_cur = 0 + push(render3d_st.prof_gen, render3d_st.prof_gen_cur) + render3d_st.prof_gen_cur = 0 let now = gl_now_us() var dtf: long = 0 - if prof_last_us != 0 { push(prof_ft, now - prof_last_us); dtf = now - prof_last_us } - prof_last_us = now + 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 # everything the frame just ended spent on work it only does sometimes - 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) + 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) var cpu: long = 0 - 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 + 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 } - 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 + 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 } # the distribution of REAL frame times: stutter lives in the tail, not the mean -function prof_ft_report() -> void { - if not prof_on { return } - if prof_ft == null or len(prof_ft) < 16 { return } +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 } let sorted = new []long var i = 8 - while i < len(prof_ft) { push(sorted, prof_ft[i]); i += 1 } + while i < len(render3d_st.prof_ft) { push(sorted, render3d_st.prof_ft[i]); i += 1 } var a = 1 while a < len(sorted) { let v = sorted[a] @@ -340,19 +298,19 @@ function prof_ft_report() -> 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(stream_us_walk / 1000)} ms (generate {string(stream_us_gen / 1000)} ms, gather {string(stream_us_gather / 1000)} ms), {string(stream_walks)} stream-walks`) + 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`) # 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 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 + 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 print("") - 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(`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(" renderer CPU by phase, mean per frame:") var i = 0 - 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`) + 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`) i += 1 } } @@ -360,13 +318,13 @@ function prof_ft_report() -> 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() -> void { - if not prof_on or prof_dt == null or len(prof_dt) < 32 { return } - let n = len(prof_dt) +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) # median, for a sense of what "slow" means here let sorted = new []long var i = 8 - while i < n { push(sorted, prof_dt[i]); i += 1 } + while i < n { push(sorted, render3d_st.prof_dt[i]); i += 1 } var a = 1 while a < len(sorted) { let v = sorted[a] @@ -387,22 +345,22 @@ function prof_hitch_report() -> void { var bestv: long = -1 var k = 8 while k < n { - if prof_dt[k] <= cut and prof_dt[k] > bestv { bestv = prof_dt[k]; best = k } + if render3d_st.prof_dt[k] <= cut and render3d_st.prof_dt[k] > bestv { bestv = render3d_st.prof_dt[k]; best = k } k += 1 } if best < 0 { return } - 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`) + 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`) cut = bestv - 1 shown += 1 } } -function prof_gen_report() -> void { - if not prof_on { return } - if prof_gen == null or len(prof_gen) < 8 { return } +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 } let sorted = new []int var i = 8 # skip the first frames: one-off initial fill - while i < len(prof_gen) { push(sorted, prof_gen[i]); i += 1 } + while i < len(render3d_st.prof_gen) { push(sorted, render3d_st.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 665c7104..c3140758 100644 --- a/packages/ludic.render3d/programs.ludic +++ b/packages/ludic.render3d/programs.ludic @@ -10,16 +10,9 @@ # 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(root: string, assets: string) -> void { r3d_root = root; r3d_assets = assets } +function r3d_set_paths(render3d_st: mut Render3dState, root: string, assets: string) -> void { render3d_st.r3d_root = root; render3d_st.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. @@ -31,61 +24,60 @@ 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() -> void { - if r3d_root_found { return } - r3d_root_found = true - if Fs.exists(`{r3d_root}/shaders/lighting.glsl`) { return } +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 } let home = r3d_home() let alt = `{home}/packages/ludic.render3d` - 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)`) + 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)`) } -function r3d_shader_file(name: string) -> string { - r3d_find_root() - let path = `{r3d_root}/shaders/{name}` +function r3d_shader_file(render3d_st: mut Render3dState, name: string) -> string { + r3d_find_root(render3d_st) + let path = `{render3d_st.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(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") } +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") } # 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 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) + 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) } # 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 -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") +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") if f == null { return } # one line per program: the defines' newlines become ';' so a variant is one line - let defs = Text.replace(`{r3d_global_defs}{defines}`, "\n", ";") + let defs = Text.replace(`{render3d_st.r3d_global_defs}{defines}`, "\n", ";") let line = `{vs}|{fs}|{defs}\n` file_write(f, line, len(line)) file_close(f) } -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}`) +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}`) 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(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) } +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) } # A framebuffer with one colour texture (and optionally a depth texture). property Target { @@ -95,29 +87,29 @@ property Target { w: int = 0, h: int = 0 } -function target_new(w: int, h: int, ifmt: int, fmt: int, ty: int, with_depth: bool, filter: int) -> Target { +function target_new(render3d_st: mut Render3dState, 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() - gpu_fb_bind(t.fbo) - t.color = tex_target(w, h, ifmt, fmt, ty, filter) - gpu_fb_color(0, t.color) + 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) if with_depth { - t.depth = tex_target(w, h, GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, GL_NEAREST) - gpu_fb_depth(t.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) } - let st = gpu_fb_status() + let st = gpu_fb_status(render3d_st) if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: framebuffer incomplete {st} ({w}x{h})`) } - gpu_fb_bind(0) + gpu_fb_bind(render3d_st, 0) return t } -function target_free(t: Target) -> void { +function target_free(render3d_st: mut Render3dState, t: Target) -> void { if t == null { return } - gpu_fb_free(t.fbo) - gpu_tex_free(t.color) - if t.depth != 0 { gpu_tex_free(t.depth) } + 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) } } -function target_bind(t: Target) -> void { - gpu_fb_bind(t.fbo) - gpu_viewport(0, 0, t.w, t.h) +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) } diff --git a/packages/ludic.render3d/quat.ludic b/packages/ludic.render3d/quat.ludic index b95a5018..6c8cc534 100644 --- a/packages/ludic.render3d/quat.ludic +++ b/packages/ludic.render3d/quat.ludic @@ -57,18 +57,14 @@ 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 -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) +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) } - let qx = q_scratch; let qy = q_sy; let qz = q_sz; let t = q_st + let qx = render3d_st.q_scratch; let qy = render3d_st.q_sy; let qz = render3d_st.q_sz; let t = render3d_st.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 b23fb345..ae280a93 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -5,120 +5,86 @@ # 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. -var r3d_plate: bool = false -function r3d_plate_mode(on: bool) -> void { r3d_plate = on } +function r3d_plate_mode(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.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 -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 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 } -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) +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) } # profiling switches (environment): R3D_NOSHADOW R3D_NOGI R3D_MSAA=n R3D_NOBLADES R3D_NOCARDS R3D_NOTREES R3D_NEAR=m -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 } +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 } } } # 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(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 } } +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 } } return true } # the window and the graphics backend; nothing is baked yet, but a frame can be presented -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() +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) print(`r3d: {gl_w}x{gl_h} on {renderer}`) - prof_init() - cam_init(float(gl_w) / float(gl_h)) + prof_init(render3d_st) + cam_init(render3d_st, float(gl_w) / float(gl_h)) return true } @@ -127,192 +93,191 @@ function r3d_open(w: int, h: int, title: string) -> bool { # 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(i: int) -> bool { +function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool { let t = i - 1 if i == 0 { - 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() + 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) } else if t >= 0 and t < TERRAIN_INIT_STEPS { - if r3d_plate { return true } + if render3d_st.r3d_plate { return true } if t == 0 { - 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) } + 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) } } - terrain_init_step(t) + terrain_init_step(render3d_st, t) } else if i == 1 + TERRAIN_INIT_STEPS { - shadow_init() - post_init(gl_w, gl_h) + shadow_init(render3d_st) + post_init(render3d_st, gl_w, gl_h) } else if i == 2 + TERRAIN_INIT_STEPS { - scatter_init() - actor_init() + scatter_init(render3d_st) + actor_init(render3d_st) } else if i == 3 + TERRAIN_INIT_STEPS { - if not r3d_plate { grass_init() } - r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "") + if not render3d_st.r3d_plate { grass_init(render3d_st) } + render3d_st.r3d_sky_prog = r3d_program(render3d_st, "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. - r3d_fog_density = 0.00024 - r3d_fog_falloff = 0.002 - r3d_ready = true - gpu_check("r3d init") + 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") } return true } -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) +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) } # 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(path: string) -> bool { return gpu_screenshot(path) } -function r3d_present() -> void { gpu_present() } +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) } # the drawable changed size: the camera's aspect and every screen-sized target follow -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 } +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 } print(`r3d: resized to {gl_w}x{gl_h}`) } -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() } +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) } # 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. - 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")) } + 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_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 r3d_test_resize > 0 and r3d_test_frame >= r3d_test_resize and r3d_test_frame % 4 == 0 { - let step = (r3d_test_frame / 4) % 4 + 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 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() + r3d_resize(render3d_st) } - r3d_time = time - gsl_frame_start() + render3d_st.r3d_time = time + gsl_frame_start(render3d_st) # 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() - prof_mark_start() - cam_begin_frame(post_frame, gl_w, gl_h) + prof_gen_frame(render3d_st) + prof_mark_start(render3d_st) + cam_begin_frame(render3d_st, render3d_st.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 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}`) + 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}`) } # the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock - 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 + 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 let t_bk = gl_now_us() - terrain_bake_shadow() - prof_bake_add(gl_now_us() - t_bk) + terrain_bake_shadow(render3d_st) + prof_bake_add(render3d_st, gl_now_us() - t_bk) } - 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() + 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) # 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() { - prof_begin("foliage prepass") - gpu_color_write(false) - scatter_draw_depth() - gpu_color_write(true) - prof_end() - sc_prepass = true + 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 } - 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, "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("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() + 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) # 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 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() + 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) } - post_color = post_hdr.color; post_color_w = post_w; post_color_h = post_h + 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 # DLSS super resolution: the lit frame up to the display's size, before anything reads it - if gsl_dlss_live() { prof_begin("DLSS"); post_color = gsl_dlss_eval(); prof_end() } + if gsl_dlss_live(render3d_st) { prof_begin(render3d_st, "DLSS"); render3d_st.post_color = gsl_dlss_eval(render3d_st); prof_end(render3d_st) } # Before bloom and before the tonemap: a shaft of light is a bright thing in the air and # should bloom like one. - 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") + 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") } diff --git a/packages/ludic.render3d/safe_api.ludic b/packages/ludic.render3d/safe_api.ludic index 16f22b05..3db24a92 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(idx: int) -> floats { - let p = gltf_accessor(idx) - let n = gltf_count * gltf_comps +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 let out = floats(n) for i in 0 .. n { out[i] = float_from_bits(mem_get_f32_bits(p, i)) } free(p) return out } # how many elements an accessor holds, without reading them -function gltf_accessor_count(idx: int) -> int { - let acc = value_at(value_get(gltf_doc, "accessors"), idx) +function gltf_accessor_count(render3d_st: Render3dState, idx: int) -> int { + let acc = value_at(value_get(render3d_st.gltf_doc, "accessors"), idx) return jint(acc, "count", 0) } # the presented frame as RGB8, bottom row first, into `out` (at least w * h * 3 bytes) -function gpu_read_screen_bytes(w: int, h: int, out: []byte) -> bool { +function gpu_read_screen_bytes(render3d_st: mut Render3dState, w: int, h: int, out: []byte) -> bool { if out == null or len(out) < w * h * 3 { return false } - gpu_read_screen(w, h, data_of(out)) + gpu_read_screen(render3d_st, w, h, data_of(out)) return true } # a PNG's samples (tex_w x tex_h x tex_channels, 8 or 16 bits): into `reuse` when it is large # enough - a map swap decodes the same size again - else into a new buffer. null if unreadable. -function png_decode_bytes(path: string, reuse: []byte) -> []byte { - let p = png_decode(path) +function png_decode_bytes(render3d_st: mut Render3dState, path: string, reuse: []byte) -> []byte { + let p = png_decode(render3d_st, path) if p == null { return null } - let n = tex_w * tex_h * tex_channels * (tex_depth / 8) + let n = render3d_st.tex_w * render3d_st.tex_h * render3d_st.tex_channels * (render3d_st.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(path: string, reuse: []byte) -> []byte { return out } # upload samples laid out as the last decode left them (tex_w, tex_h, tex_channels, tex_depth) -function tex_upload_bytes(px: []byte, srgb: bool, mips: bool) -> int { - return tex_upload(data_of(px), srgb, mips) +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) } diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 029d0e90..6a4edf88 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(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) +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) } # 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() -> Model { +function model_cross_card(render3d_st: mut Render3dState) -> Model { let model = new Model model.prims = new []Prim let pr = new Prim - let m = gpu_mesh_new() + let m = gpu_mesh_new(render3d_st) let v = gl_floats(8 * 8) var k = 0 for q in 0 .. 2 { @@ -110,19 +110,19 @@ function model_cross_card() -> Model { k += 8 } } - gpu_mesh_vertices(m, v, gl_bytes_of(64), GPU_STATIC) - sc_model_layout(m) + gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(64), GPU_STATIC) + sc_model_layout(render3d_st, 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(m, data_of(idx), 48, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), 48, 4) free(idx) m.count = 12 - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) pr.mesh = m - 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 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 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() -> 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(scan: Model, cap: int, wind: float, cull: float) -> Layer { - let l = layer_new(model_cross_card(), cap, true, wind, 0.0, cull) +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) l.card = true - l.atlas = impostor_bake(scan, 1, 512, 512) + l.atlas = impostor_bake(render3d_st, 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() -> Model { +function model_lupine(render3d_st: mut Render3dState) -> Model { let model = new Model model.prims = new []Prim let pr = new Prim - let m = gpu_mesh_new() + let m = gpu_mesh_new(render3d_st) # 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() -> 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(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC) - sc_model_layout(m) + gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC) + sc_model_layout(render3d_st, m) free(v) - gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), nq * 6 * 4, 4) free(idx) m.count = nq * 6 - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) pr.mesh = m - 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 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 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() -> 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() -> Model { +function model_lupine_dense(render3d_st: mut Render3dState) -> Model { let model = new Model model.prims = new []Prim let pr = new Prim - let m = gpu_mesh_new() + let m = gpu_mesh_new(render3d_st) let nfl = 220 let nq = 2 + nfl + 5 let v = gl_floats(nq * 4 * 8) @@ -264,27 +264,27 @@ function model_lupine_dense() -> 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(m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC) - sc_model_layout(m) + gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(nq * 4 * 8), GPU_STATIC) + sc_model_layout(render3d_st, m) free(v) - gpu_mesh_indices(m, data_of(idx), nq * 6 * 4, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), nq * 6 * 4, 4) free(idx) m.count = nq * 6 - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) pr.mesh = m - 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 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 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() -> Model { +function model_blade(render3d_st: mut Render3dState) -> Model { let model = new Model model.prims = new []Prim let pr = new Prim - let m = gpu_mesh_new() + let m = gpu_mesh_new(render3d_st) let rows = 5 let v = gl_floats(rows * 2 * 8) var k = 0 @@ -303,8 +303,8 @@ function model_blade() -> Model { k += 8 } } - gpu_mesh_vertices(m, v, gl_bytes_of(rows * 2 * 8), GPU_STATIC) - sc_model_layout(m) + gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(rows * 2 * 8), GPU_STATIC) + sc_model_layout(render3d_st, m) free(v) let ni = (rows - 1) * 6 let idx = words(ni) @@ -315,96 +315,68 @@ function model_blade() -> Model { idx[k + 3] = a + 1; idx[k + 4] = a + 3; idx[k + 5] = a + 2 k += 6 } - gpu_mesh_indices(m, data_of(idx), ni * 4, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4) free(idx) m.count = ni - gpu_mesh_done(m) + gpu_mesh_done(render3d_st, m) pr.mesh = m - 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 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 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() -> void { +function scatter_init(render3d_st: mut Render3dState) -> void { # R3D_DUMP_ATLAS: every impostor and card atlas the run bakes, to build/atlas__{color,alpha}.ppm - 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") + 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") # a dry alpine meadow: brown-olive roots, straw with a little green at the tips - 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() + 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) # 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)) - sc_ident_buf = gpu_buffer_new() - gpu_buffer_upload(sc_ident_buf, INST_FLOATS * 4, one, GPU_STATIC) + 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) free(one) - sc_layers = new []Layer + render3d_st.sc_layers = new []Layer } # feed a mesh its instances from `buf`: attribute 3 = position + scale, 4 = sin, cos, seed, wind -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 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 layer_new(model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer { +function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer { let l = new Layer l.model = model l.cap = cap @@ -416,12 +388,12 @@ function layer_new(model: Model, cap: int, foliage: bool, wind: float, near: flo 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() - l.imp_buf = gpu_buffer_new() - l.sh_buf = gpu_buffer_new() + l.buf = gpu_buffer_new(render3d_st) + l.imp_buf = gpu_buffer_new(render3d_st) + l.sh_buf = gpu_buffer_new(render3d_st) l.rough = 1.0 - for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh, l.buf) } - push(sc_layers, l) + for i in 0 .. len(model.prims) { scatter_attach(render3d_st, model.prims[i].mesh, l.buf) } + push(render3d_st.sc_layers, l) return l } @@ -434,41 +406,39 @@ function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: fl } # ---- impostors --------------------------------------------------------------------- -var sc_bake_flower: bool = false -var sc_dump_n: int = 0 -function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor { +function impostor_bake(render3d_st: mut Render3dState, 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(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) + 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) # the model's prims temporarily take the identity instance - for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh, sc_ident_buf) } + for i in 0 .. len(model.prims) { scatter_attach(render3d_st, model.prims[i].mesh, render3d_st.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 = sc_bake_prog - if sc_bake_flower { bake = sc_bake_flower_prog } - gpu_use_program(bake) + 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) for t in 0 .. tiles { let a = 2.0 * PI * (float(t) / float(tiles)) v3_set(at, 0.0, cy, 0.0) @@ -476,49 +446,49 @@ function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor { 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(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) + 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) for i in 0 .. len(model.prims) { let pr = model.prims[i] - r3d_bind_2d(bake, "u_diff", 0, pr.diff) - r3d_bind_2d(bake, "u_arm", 2, pr.arm) - mesh_draw_instanced(pr.mesh, 1) + 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) } } free(view); free(proj); free(eye); free(at); free(up) - 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") + 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") # numbered, so every bake of a run survives to be compared (a card layer per species bakes one) - 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`) + 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`) } 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(l: Layer, models: []Model, dists: floats) -> void { +function layer_set_lods(render3d_st: mut Render3dState, 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() + l.lod_buf[k] = gpu_buffer_new(render3d_st) let m = models[k] - for i in 0 .. len(m.prims) { scatter_attach(m.prims[i].mesh, l.lod_buf[k]) } + for i in 0 .. len(m.prims) { scatter_attach(render3d_st, m.prims[i].mesh, l.lod_buf[k]) } } l.model = models[0] l.near = dists[l.n_lods - 1] @@ -527,28 +497,25 @@ function layer_set_lods(l: Layer, models: []Model, dists: floats) -> void { # 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(l: Layer, im: Impostor) -> void { +function layer_set_impostor(render3d_st: Render3dState, l: Layer, im: Impostor) -> void { l.imp = im - scatter_attach(sc_card, l.imp_buf) + scatter_attach(render3d_st, render3d_st.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. -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) +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) } # GPU-culled layers dispatch before the first pass of the frame, so no pass is split for it - 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]) } } + 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]) } } } } @@ -560,15 +527,10 @@ function scatter_begin_frame() -> 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(l: Layer) -> bool { +function layer_gpu_eligible(render3d_st: Render3dState, 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(l) + return layer_arena_ok(render3d_st, l) } # The merged meshes. Every level of a kit tree or rock carries the same materials in the same order @@ -577,7 +539,7 @@ function layer_gpu_eligible(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(l: Layer) -> bool { +function layer_arena_ok(render3d_st: Render3dState, 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 { @@ -588,18 +550,18 @@ function layer_arena_ok(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(pm.ebo) == null { return false } + if gpu_buffer_map(render3d_st, 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(pm.attrs[o]) == null { return false } + if gpu_buffer_map(render3d_st, pm.attrs[o]) == null { return false } } } } return true } -function layer_arena_build(l: Layer) -> void { +function layer_arena_build(render3d_st: mut Render3dState, 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) @@ -613,28 +575,28 @@ function layer_arena_build(l: Layer) -> void { nv += pr.verts; ni += pr.mesh.count } let p0 = l.lods[0].prims[j] - let m = gpu_mesh_new() + let m = gpu_mesh_new(render3d_st) 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(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(render3d_st, pr.mesh.attrs[a * GPU_ATTR_W]), pr.verts * comps * 4) } - gpu_mesh_vertices(m, vb, nv * comps * 4, GPU_STATIC) - gpu_mesh_attr(m, a, comps, GPU_F32, 0, 0, false) + 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) 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(pm.ebo), pm.count * 2) + mem_copy(mem_off(ib, l.g_first[j * 4 + k] * 2), gpu_buffer_map(render3d_st, pm.ebo), pm.count * 2) } - gpu_mesh_indices(m, ib, ni * 2, 2) + gpu_mesh_indices(render3d_st, m, ib, ni * 2, 2) free(ib) m.count = ni - gpu_mesh_done(m) - scatter_attach(m, l.g_dst) + gpu_mesh_done(render3d_st, m) + scatter_attach(render3d_st, 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) @@ -647,29 +609,29 @@ function layer_arena_build(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(l: Layer) -> bool { - if not sc_cull_tried { - sc_cull_tried = true +function layer_gpu_prepare(render3d_st: mut Render3dState, l: Layer) -> bool { + if not render3d_st.sc_cull_tried { + render3d_st.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("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 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 sc_cull_prog == 0 or not layer_gpu_eligible(l) { return false } + if render3d_st.sc_cull_prog == 0 or not layer_gpu_eligible(render3d_st, 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(); 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) + 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) } let n = l.n_lods let cap = l.count - 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) } + 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) } let n_mat = len(l.g_arena) let rec = words(SC_RECS * 5) for i in 0 .. SC_RECS * 5 { rec[i] = 0 } @@ -680,7 +642,7 @@ function layer_gpu_prepare(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] = sc_card.count; rec[16 * 5 + 4] = n * cap + rec[16 * 5] = render3d_st.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 { @@ -690,39 +652,39 @@ function layer_gpu_prepare(l: Layer) -> bool { } } } - gpu_buffer_upload(l.g_cmds, SC_RECS * SC_REC_W, data_of(rec), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, 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(l.g_counts, 20, data_of(zeros), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, 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(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC) + gpu_buffer_upload(render3d_st, 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(l: Layer) -> void { +function layer_gpu_cull(render3d_st: mut Render3dState, l: Layer) -> void { let pr = words(36) for i in 0 .. 36 { pr[i] = 0 } - 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) + 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) 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(sc_cull_prog, data_of(pr), 144, bufs, 1) + gpu_dispatch(render3d_st, render3d_st.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(l: Layer, cell: float) -> void { +function layer_grid_build(render3d_st: mut Render3dState, 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 { @@ -762,11 +724,11 @@ function layer_grid_build(l: Layer, cell: float) -> void { free(cellof); free(fill) if l.vis == null { l.vis = floats(l.cap * INST_FLOATS) } l.n_sh = l.count - gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_STATIC) + gpu_buffer_upload(render3d_st, 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(l: Layer) -> void { +function layer_grid_gather(render3d_st: Render3dState, l: Layer) -> void { let cell = l.gcell let half = cell * 0.5 let reach = l.cull + cell * 0.71 @@ -779,14 +741,14 @@ function layer_grid_gather(l: Layer) -> void { if cnt == 0 { continue } let wx = l.gx0 + float(ix) * cell + half if l.cull != 0.0 { - let dx = wx - cam_pos[0]; let dz = wz - cam_pos[2] + let dx = wx - render3d_st.cam_pos[0]; let dz = wz - render3d_st.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(wx, cy, wz, r) { continue } + if not cam_sphere_visible(render3d_st, 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 } @@ -796,7 +758,7 @@ function layer_grid_gather(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(l: Layer, src: floats, total: int) -> void { +function layer_partition_lods(render3d_st: mut Render3dState, 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 } @@ -804,7 +766,7 @@ function layer_partition_lods(l: Layer, src: floats, total: int) -> void { 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] - cam_pos[0]; let dz = src[o + 2] - cam_pos[2] + let dx = src[o] - render3d_st.cam_pos[0]; let dz = src[o + 2] - render3d_st.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) { @@ -836,44 +798,43 @@ function layer_partition_lods(l: Layer, src: floats, total: int) -> void { for k in 0 .. n { l.n_lod[k] = counts[k] if counts[k] > 0 { - gpu_buffer_upload(l.lod_buf[k], counts[k] * INST_FLOATS * 4, mem_off(tmp, start[k] * INST_FLOATS * 4), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, 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 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 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 l.n_far > 0 { - gpu_buffer_upload(l.imp_buf, l.n_far * INST_FLOATS * 4, mem_off(tmp, start[n] * INST_FLOATS * 4), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, 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(l.sh_buf, total * INST_FLOATS * 4, data_of(src), GPU_DYNAMIC) } + if total > 0 { gpu_buffer_upload(render3d_st, 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) -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 } +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 } 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(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) + 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) return } - if l.gcell == 0.0 and not l.streamed and l.count > 2000 { layer_grid_build(l, 96.0) } + if l.gcell == 0.0 and not l.streamed and l.count > 2000 { layer_grid_build(render3d_st, l, 96.0) } var src = l.inst var total = l.count - if l.gcell != 0.0 { layer_grid_gather(l); src = l.vis; total = l.n_vis } + if l.gcell != 0.0 { layer_grid_gather(render3d_st, l); src = l.vis; total = l.n_vis } let near2 = l.near * l.near let cull2 = l.cull * l.cull var nn = 0 @@ -881,15 +842,15 @@ function layer_update(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(l, src, total) - prof_layer_add(gl_now_us() - t_lu, total * INST_FLOATS * 4) + layer_partition_lods(render3d_st, l, src, total) + prof_layer_add(render3d_st, 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] - cam_pos[0] - let dz = src[o + 2] - cam_pos[2] + let dx = src[o] - render3d_st.cam_pos[0] + let dz = src[o + 2] - render3d_st.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 { @@ -905,8 +866,8 @@ function layer_update(l: Layer) -> void { } l.n_near = nn l.n_far = nf - 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 { + 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 { 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])}`) } } @@ -916,42 +877,38 @@ function layer_update(l: Layer) -> void { if l.gcell == 0.0 { l.n_sh = l.count if l.count > 0 { - gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, l.sh_buf, l.count * INST_FLOATS * 4, data_of(l.inst), GPU_DYNAMIC) } } - gpu_buffer_upload(l.buf, nn * INST_FLOATS * 4, data_of(tmp), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, l.buf, nn * INST_FLOATS * 4, data_of(tmp), GPU_DYNAMIC) if nf > 0 { - gpu_buffer_upload(l.imp_buf, nf * INST_FLOATS * 4, mem_off(tmp, (far_off - nf * INST_FLOATS) * 4), GPU_DYNAMIC) + gpu_buffer_upload(render3d_st, l.imp_buf, nf * INST_FLOATS * 4, mem_off(tmp, (far_off - nf * INST_FLOATS) * 4), GPU_DYNAMIC) } - prof_layer_add(gl_now_us() - t_lu, (nn + nf + l.n_sh) * INST_FLOATS * 4) + prof_layer_add(render3d_st, gl_now_us() - t_lu, (nn + nf + l.n_sh) * INST_FLOATS * 4) } -function layer_program(l: Layer, shadow: bool, card: bool) -> int { +function layer_program(render3d_st: Render3dState, l: Layer, shadow: bool, card: bool) -> int { if card { - if shadow { return sc_prog_card_shadow } - if l.cheap { return sc_prog_card_cheap } - return sc_prog_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 { - 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.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.blade { return sc_prog_blade } - if l.flower { return sc_prog_flower } + if l.blade { return render3d_st.sc_prog_blade } + if l.flower { return render3d_st.sc_prog_flower } if l.foliage { - if sc_prepass and sc_prog_fol_eq != 0 { return sc_prog_fol_eq } - return sc_prog_fol + 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 l.wind != 0.0 { return sc_prog_wind } - return sc_prog + if l.wind != 0.0 { return render3d_st.sc_prog_wind } + return render3d_st.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 @@ -961,39 +918,35 @@ var sc_dbg_tint: floats = null # 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. -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 { +function layer_level_casts_here(render3d_st: mut Render3dState, 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(dmin, l.lod_dist[k], l.lods[k].height) + return cast_band_reaches(render3d_st, 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(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 +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 let t = Math.sin(half) / Math.cos(half) - let ta = t * cam_aspect - sc_cast_k = Math.sqrt(1.0 + (t * t + ta * ta)) * 1.1 + let ta = t * render3d_st.cam_aspect + render3d_st.sc_cast_k = Math.sqrt(1.0 + (t * t + ta * ta)) * 1.1 } - let c = sh_cascade - var near = cam_near + let c = render3d_st.sh_cascade + var near = render3d_st.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 = sh_split[c - 1] * 0.85 } - let far = sh_split[c] + if c > 0 { near = render3d_st.sh_split[c - 1] * 0.85 } + let far = render3d_st.sh_split[c] let reach = Math.max(Math.min(height * 6.0, 40.0), 8.0) - if dmax != 0.0 and dmax + reach + sc_cast_dy < near { return false } - if dmin - reach > far * sc_cast_k { return false } + 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 } return true } @@ -1001,247 +954,244 @@ function cast_band_reaches(dmin: float, dmax: float, height: float) -> bool { # 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(l: Layer, shadow: bool, light_vp: floats, full: bool) -> void { +function layer_draw_near(render3d_st: mut Render3dState, 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) - 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 + 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 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(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) + 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) } - sc_dbg_level = -1 + render3d_st.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(l, l.model, vb, cnt, l.card, shadow, light_vp) + layer_draw_model(render3d_st, 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(l: Layer, model: Model, vb: int, cnt: int, card: bool, shadow: bool, light_vp: floats) -> void { +function layer_draw_model(render3d_st: mut Render3dState, l: Layer, model: Model, vb: int, cnt: int, card: bool, shadow: bool, light_vp: floats) -> void { if cnt == 0 { return } - let p = layer_program(l, shadow, card) - gpu_use_program(p) + let p = layer_program(render3d_st, l, shadow, card) + gpu_use_program(render3d_st, 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 == sc_prog_fol_eq { gpu_depth_func(GL_EQUAL) } + if p == render3d_st.sc_prog_fol_eq { gpu_depth_func(render3d_st, GL_EQUAL) } var ground = 0.0 if l.grounded { ground = 1.0 } - 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(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) var mh = 0.0 if not card and l.foliage { mh = model.height } - 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) } + 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) } if card { - 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) + 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) } - if shadow { u_mat4(gpu_uniform(p, "u_light_vp"), light_vp) } + if shadow { u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_light_vp"), light_vp) } else { - 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 + 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 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(sc_dbg_tint, r, g, b) - u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint) + 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) } - 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) } + 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) } } - gpu_cull(false) + gpu_cull(render3d_st, false) for i in 0 .. len(model.prims) { let pr = model.prims[i] - scatter_attach(pr.mesh, vb) + scatter_attach(render3d_st, pr.mesh, vb) if not card { - 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) } + 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) } } - 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) } + 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) } } - gpu_alpha_to_coverage(false) - if p == sc_prog_fol_eq { gpu_depth_func(GL_LESS) } + gpu_alpha_to_coverage(render3d_st, false) + if p == render3d_st.sc_prog_fol_eq { gpu_depth_func(render3d_st, GL_LESS) } } -function layer_draw_far(l: Layer, shadow: bool, light_vp: floats) -> void { +function layer_draw_far(render3d_st: mut Render3dState, 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 = sc_imp_prog - if shadow { p = sc_imp_prog_shadow } - gpu_use_program(p) + var p = render3d_st.sc_imp_prog + if shadow { p = render3d_st.sc_imp_prog_shadow } + gpu_use_program(render3d_st, p) let im = l.imp - 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_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) if shadow { - 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) + 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) } else { - 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) } + 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) } } - gpu_cull(false) - if not shadow and sc_a2c { gpu_alpha_to_coverage(true) } + gpu_cull(render3d_st, false) + if not shadow and render3d_st.sc_a2c { gpu_alpha_to_coverage(render3d_st, true) } if l.g_on { - scatter_attach(sc_card, l.g_dst) - gpu_draw_mesh_indirect(sc_card, l.g_cmds, 16 * SC_REC_W, 1, 0, 0) + 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) } else { - scatter_attach(sc_card, l.imp_buf) - mesh_draw_instanced(sc_card, l.n_far) + scatter_attach(render3d_st, render3d_st.sc_card, l.imp_buf) + mesh_draw_instanced(render3d_st, render3d_st.sc_card, l.n_far) } - gpu_alpha_to_coverage(false) + gpu_alpha_to_coverage(render3d_st, 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(l: Layer, light_vp: floats) -> void { +function layer_draw_shadow(render3d_st: mut Render3dState, l: Layer, light_vp: floats) -> void { if l.n_sh == 0 or l.imp == null { return } - let p = sc_imp_prog_shadow - gpu_use_program(p) + let p = render3d_st.sc_imp_prog_shadow + gpu_use_program(render3d_st, p) let im = l.imp - 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) + 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) } -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(l: Layer, model: Model, vb: int, cnt: int) -> void { +function layer_draw_depth(render3d_st: mut Render3dState, l: Layer, model: Model, vb: int, cnt: int) -> void { if cnt == 0 or model == null or vb == 0 { return } - let p = sc_prog_fol_depth - gpu_use_program(p) + let p = render3d_st.sc_prog_fol_depth + gpu_use_program(render3d_st, p) var ground = 0.0 if l.grounded { ground = 1.0 } - 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) + 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) for i in 0 .. len(model.prims) { let pr = model.prims[i] - 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) } + 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) } } } # 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() -> void { - if sc_prog_fol_depth == 0 { return } - for i in 0 .. len(sc_layers) { - let l = sc_layers[i] +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] if not l.foliage or l.blade or l.flower { continue } - if r3d_no_trees and l.imp != null { continue } - layer_update(l) + if render3d_st.r3d_no_trees and l.imp != null { continue } + layer_update(render3d_st, l) if l.n_lods > 1 and l.g_on { - 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 + 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 } 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(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(render3d_st, l, l.lods[k], l.lod_buf[k], l.n_lod[k]) } } } else if not l.card { - layer_draw_depth(l, l.model, l.buf, l.n_near) + layer_draw_depth(render3d_st, l, l.model, l.buf, l.n_near) } } - gpu_cull(true) + gpu_cull(render3d_st, true) } -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) +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) } - gpu_cull(true) + gpu_cull(render3d_st, 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(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) +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) if l.imp != null { - layer_draw_shadow(l, light_vp) + layer_draw_shadow(render3d_st, 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 { - 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 + 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 } else if l.n_lods > 2 { - for k in 0 .. 3 { layer_draw_model(l, l.lods[2], l.lod_buf[k], l.n_lod[k], false, true, light_vp) } + 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) } } } - else { layer_draw_near(l, true, light_vp, true) } + else { layer_draw_near(render3d_st, l, true, light_vp, true) } } - prof_cpu_mark("shadow scatter") + prof_cpu_mark(render3d_st, "shadow scatter") } @@ -1251,39 +1201,38 @@ function scatter_draw_casters(light_vp: floats) -> void { # 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. -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 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 } -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) +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) # 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) - cb_state = 977 + render3d_st.cb_state = 977 var k = 0 for i in 0 .. count { - 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() + 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) for oz in 0 .. 3 { for ox in 0 .. 3 { let px = x + float(ox - 1) * tile @@ -1294,45 +1243,45 @@ function carpet_bake(layers: []Layer, count: int, tile: float, res: int) -> int } } } - let buf = gpu_buffer_new() - gpu_buffer_upload(buf, gl_bytes_of(n9 * INST_FLOATS), inst, GPU_STATIC) + let buf = gpu_buffer_new(render3d_st) + gpu_buffer_upload(render3d_st, 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 = 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) + 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) for li in 0 .. len(layers) { let l = layers[li] if l.atlas == null { continue } - 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) + 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) for i in 0 .. len(l.model.prims) { let pr = l.model.prims[i] - scatter_attach(pr.mesh, buf) - mesh_draw_instanced(pr.mesh, n9) + scatter_attach(render3d_st, pr.mesh, buf) + mesh_draw_instanced(render3d_st, pr.mesh, n9) } } free(view); free(proj); free(eye); free(at); free(up) - 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") + 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") return tex } @@ -1341,15 +1290,15 @@ function carpet_bake(layers: []Layer, count: int, tile: float, res: int) -> 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() -> 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]) } } +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]) } } if l.inst != null { free(l.inst) } if l.scratch != null { free(l.scratch) } if l.tint != null { free(l.tint) } @@ -1361,14 +1310,14 @@ function scatter_clear_all() -> 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(l.g_arena[k].mesh) } } + if l.g_arena != null { for k in 0 .. len(l.g_arena) { mesh_free(render3d_st, 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(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(render3d_st, l.model.prims[k].mesh) } } if l.card and l.atlas != null { - gpu_tex_free(l.atlas.albedo) - gpu_tex_free(l.atlas.normal) + gpu_tex_free(render3d_st, l.atlas.albedo) + gpu_tex_free(render3d_st, l.atlas.normal) } } - sc_layers = new []Layer - sc_view_gen += 1 + render3d_st.sc_layers = new []Layer + render3d_st.sc_view_gen += 1 } diff --git a/packages/ludic.render3d/shadow.ludic b/packages/ludic.render3d/shadow.ludic index 63659800..d9b60358 100644 --- a/packages/ludic.render3d/shadow.ludic +++ b/packages/ludic.render3d/shadow.ludic @@ -5,95 +5,80 @@ # ============================================================================ # 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() -> 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) +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) # the fourth slice keeps a tree-sized texel out to a kilometre; only the massif uses the last - 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) + 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) } # 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(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() +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) # 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(sh_tex) { - shadow_refused = r + if not gpu_tex_ok(render3d_st, render3d_st.sh_tex) { + render3d_st.shadow_refused = r print(`r3d: shadows: no memory for {r} x {r} cascades; keeping {was}`) var size = was - shadow_res = size - gpu_tex_free(sh_tex) - shadow_make_tex() - while not gpu_tex_ok(sh_tex) and size > 512 { + 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 { size = size / 2 - shadow_res = size - gpu_tex_free(sh_tex) - shadow_make_tex() + render3d_st.shadow_res = size + gpu_tex_free(render3d_st, render3d_st.sh_tex) + shadow_make_tex(render3d_st) } } } -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) +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) 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(GPU_TEX2D_ARRAY, border) + gpu_tex_border(render3d_st, GPU_TEX2D_ARRAY, border) free(border) } # light view-projection for the camera-frustum slice [near, far] -function shadow_fit(c: int, near: float, far: float) -> void { +function shadow_fit(render3d_st: mut Render3dState, 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(sh_tmp_proj, cam_fov, cam_aspect, near, far) - m4_inverse(sh_tmp_inv, sh_tmp_proj) # NDC -> view space + 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 let cview = v3_new(0.0, 0.0, 0.0) let corners = floats(24) for i in 0 .. 8 { @@ -101,131 +86,123 @@ function shadow_fit(c: int, near: float, far: float) -> void { 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(sh_corner, sh_tmp_inv, x, y, z) + let w = m4_xform_point(render3d_st.sh_corner, render3d_st.sh_tmp_inv, x, y, z) let iw = 1.0 / w - corners[i * 3] = sh_corner[0] * iw; corners[i * 3 + 1] = sh_corner[1] * iw; corners[i * 3 + 2] = sh_corner[2] * iw + 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 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(sh_corner, corners[i * 3], corners[i * 3 + 1], corners[i * 3 + 2]) - let d = v3_dist(sh_corner, cview) + 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) if d > radius { radius = d } } radius = radius * 1.05 # the slice centre back into world space - m4_inverse(sh_tmp_vp, cam_view) + m4_inverse(render3d_st.sh_tmp_vp, render3d_st.cam_view) let center = floats(3) - m4_xform_point(center, sh_tmp_vp, cview[0], cview[1], cview[2]) + m4_xform_point(center, render3d_st.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, sun_dir, back) + v3_madd(eye, center, render3d_st.sun_dir, back) let up = v3_new(0.0, 1.0, 0.0) - m4_look_at(sh_tmp_view, eye, center, up) + m4_look_at(render3d_st.sh_tmp_view, eye, center, up) # snap the ortho window to the shadow texel grid - 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 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 nr = -radius let zfar = back + radius + 100.0 - 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 + 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 let out = floats(16) - m4_mul(out, sh_tmp_proj, sh_tmp_view) - for i in 0 .. 16 { sh_vp[c * 16 + i] = out[i] } + 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] } free(out); free(eye); free(up); free(center); free(corners) } -function shadow_cascade_vp(c: int) -> floats { return sh_vp_v[c] } +function shadow_cascade_vp(render3d_st: Render3dState, c: int) -> floats { return render3d_st.sh_vp_v[c] } # render every cascade; `draw` happens through terrain_draw_shadow + the scene's casters -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) +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) for c in 0 .. SHADOW_CASCADES { - 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) + 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) # shadows off (a video setting): the cascades stay cleared, so everything reads lit - if sh_enabled { - if not sh_skip_terrain { terrain_draw_shadow(vp) } - r3d_scene_casters(vp) + if render3d_st.sh_enabled { + if not render3d_st.sh_skip_terrain { terrain_draw_shadow(vp) } + r3d_scene_casters(render3d_st, vp) } } - 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 + 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 let q = floats(3) - 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) + 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) print(`probe base ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])}`) - 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))}`) + 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))}`) # where the top's shadow lands on the ground: walk down the sun ray - 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) + 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) 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(c) + let vp = shadow_cascade_vp(render3d_st, c) # a point 5 m ahead of the camera on the ground - 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) + 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) 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() -> void { - let n = shadow_res * shadow_res +function shadow_dump(render3d_st: mut Render3dState) -> void { + let n = render3d_st.shadow_res * render3d_st.shadow_res let buf = floats(n * SHADOW_CASCADES) - 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) + 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) let q = floats(3) - 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)) + 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)) 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 * 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`) + 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`) } free(q) } @@ -240,10 +217,10 @@ function shadow_dump() -> 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 = shadow_res / sm + let st = render3d_st.shadow_res / sm for y in 0 .. sm { for x in 0 .. sm { - let d = buf[c * n + (y * st) * shadow_res + x * st] + let d = buf[c * n + (y * st) * render3d_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 } @@ -254,32 +231,31 @@ function shadow_dump() -> 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(prog: int, name: string) -> int { - var loc = gpu_uniform(prog, name + "[0]") - if loc < 0 { loc = gpu_uniform(prog, name) } +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) } return loc } -function shadow_bind(prog: int) -> void { - r3d_bind_tex(prog, "u_shadow", 15, GPU_TEX2D_ARRAY, sh_tex) +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) # the height-field shadow (terrain.ludic); a stand-in texture keeps the unit valid before the bake - var ts = ter_shadow_tex + var ts = render3d_st.ter_shadow_tex var ts_on = 1.0 - 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) + 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) } diff --git a/packages/ludic.render3d/skin.ludic b/packages/ludic.render3d/skin.ludic index 20f69019..2c9ecf11 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(m: Mesh, attrs: Val) -> bool { +function skin_attribs(render3d_st: mut Render3dState, 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(value_as_int(value_get(attrs, "JOINTS_0"))) + let jd = gltf_accessor(render3d_st, value_as_int(value_get(attrs, "JOINTS_0"))) var jsz = 1 var jtype = GPU_U8 - 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 + 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 free(jd) - let wd = gltf_accessor(value_as_int(value_get(attrs, "WEIGHTS_0"))) + let wd = gltf_accessor(render3d_st, value_as_int(value_get(attrs, "WEIGHTS_0"))) var wsz = 4 var wtype = GPU_F32 var norm = false - 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) + 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) free(wd) return true } # the skin `idx` of the document being loaded (gltf_load holds gltf_doc / gltf_bin open) -function skin_load(idx: int) -> Skin { +function skin_load(render3d_st: mut Render3dState, idx: int) -> Skin { let sk = new Skin - let nodes = value_get(gltf_doc, "nodes") + let nodes = value_get(render3d_st.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(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(gltf_doc, "skins"), idx) + let skv = value_at(value_get(render3d_st.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(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(value_as_int(value_get(skv, "inverseBindMatrices"))) + let ib = gltf_accessor(render3d_st, 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(sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void { +function skin_set_rot(render3d_st: mut Render3dState, sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void { if node < 0 { return } - q_euler(sk.tmp_q, pitch, yaw, roll) + q_euler(render3d_st, 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(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) +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) } # 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 4dbc7e8e..fc9c5296 100644 --- a/packages/ludic.render3d/sky.ludic +++ b/packages/ludic.render3d/sky.ludic @@ -7,47 +7,27 @@ 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(yaw: float) -> void { - sky_set_rot(yaw) +function sky_set_yaw(render3d_st: mut Render3dState, yaw: float) -> void { + sky_set_rot(render3d_st, yaw) # world sun = rotY(sun_hdri, -yaw): the lookup rotates a world direction by +yaw - 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]) + 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]) # the light is already baked at this yaw: turning to it again bakes nothing - if sky_baked and yaw == sky_baked_yaw { return } - sky_precompute() + if render3d_st.sky_baked and yaw == render3d_st.sky_baked_yaw { return } + sky_precompute(render3d_st) } # 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(yaw: float) -> void { - sky_yaw = yaw - sky_rot_s = Math.sin(yaw); sky_rot_c = Math.cos(yaw) +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_bind_rot(prog: int) -> void { u_f2(gpu_uniform(prog, "u_sky_rot"), sky_rot_s, sky_rot_c) } +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) } # direction for an equirect uv (matches equirectUV in lighting.glsl) function sky_dir_from_uv(o: floats, u: float, v: float) -> void { @@ -57,94 +37,93 @@ 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(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 +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 # 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 - 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() } + 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) } return true } -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) +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) } # 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. -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_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) } } -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) +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) } # irradiance: 128 x 64 equirect - 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) + 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) # prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array - 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) + 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) for l in 0 .. SKY_PREFILTER_LEVELS { - sky_convolve(p_pre, sky_prefilter, l, sky_prefilter_w, sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1)) + 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)) } # BRDF LUT - 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") + 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") } # bind the IBL set + sun for a lit program (units 12..14) -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) +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) } diff --git a/packages/ludic.render3d/stream.ludic b/packages/ludic.render3d/stream.ludic index 1c9797c8..d5bde33a 100644 --- a/packages/ludic.render3d/stream.ludic +++ b/packages/ludic.render3d/stream.ludic @@ -12,7 +12,6 @@ # stream_emit(...) per instance. One Stream drives one Layer. # ============================================================================ -var STREAM_MAX_CHUNKS: int = 4096 property Chunk { key: int = 0, # packed (cx, cz, band) @@ -40,22 +39,13 @@ 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(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") +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") } let s = new Stream s.layer = layer; s.size = size; s.reach = reach @@ -64,11 +54,11 @@ function stream_new(layer: Layer, size: float, reach: float, b0: float, b1: floa 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(STREAM_MAX_CHUNKS) + s.keys = words(render3d_st.STREAM_MAX_CHUNKS) s.htab = words(STREAM_HASH) for i in 0 .. STREAM_HASH { s.htab[i] = 0 } - if stream_all == null { stream_all = new []Stream } - push(stream_all, s) + if render3d_st.stream_all == null { render3d_st.stream_all = new []Stream } + push(render3d_st.stream_all, s) return s } @@ -107,16 +97,14 @@ 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 -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 { +function stream_emit(render3d_st: mut Render3dState, 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 stream_scratch == null { stream_scratch = floats(STREAM_CHUNK_MAX * INST_FLOATS) } + if render3d_st.stream_scratch == null { render3d_st.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 - 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 + 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 c.count += 1 } @@ -143,14 +131,11 @@ 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. @@ -160,10 +145,10 @@ var stream_budget_left: int = 0 # — 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(s: Stream) -> void { +function stream_evict(render3d_st: mut Render3dState, s: Stream) -> void { # the age threshold that keeps about half, found by bisection on the count (no sort) var lo = 0 - var hi = stream_walk_no + var hi = render3d_st.stream_walk_no var keep = s.n / 2 var t = 0 var it = 0 @@ -181,7 +166,7 @@ function stream_evict(s: Stream) -> void { var i = 0 while i < s.n { let c = s.chunks[i] - if c.used >= t or c.used == stream_walk_no { push(kept, c) } + if c.used >= t or c.used == render3d_st.stream_walk_no { push(kept, c) } else { if c.data != null { free(c.data) } } i += 1 } @@ -190,21 +175,21 @@ function stream_evict(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 } - stream_evictions += 1 + render3d_st.stream_evictions += 1 } -function stream_update(s: Stream, cam_x: float, cam_z: float) -> void { +function stream_update(render3d_st: mut Render3dState, 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 == sc_view_gen { return } + if ccx == s.last_cx and ccz == s.last_cz and not s.pending and s.view_gen == render3d_st.sc_view_gen { return } let first = s.last_cx == 999999 - s.view_gen = sc_view_gen + s.view_gen = render3d_st.sc_view_gen s.last_cx = ccx; s.last_cz = ccz let l = s.layer l.count = 0 var missing = false - stream_walks += 1 - stream_walk_no += 1 + render3d_st.stream_walks += 1 + render3d_st.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 @@ -224,31 +209,31 @@ function stream_update(s: Stream, cam_x: float, cam_z: float) -> void { 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 = stream_walk_no } + if c != null { c.used = render3d_st.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() < stream_deadline) { + if c == null and (first or urgent or gl_now_us() < render3d_st.stream_deadline) { c = new Chunk c.key = key s.cur = c let t0 = gl_now_us() - r3d_stream_fill(s, cx, cz, band) + r3d_stream_fill(render3d_st, s, cx, cz, band) let dt = gl_now_us() - t0 - 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) } + 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) } # 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 < STREAM_MAX_CHUNKS { + if s.n < render3d_st.STREAM_MAX_CHUNKS { push(s.chunks, c); s.keys[s.n] = key; stream_remember(s, key, s.n); s.n += 1 - c.used = stream_walk_no + c.used = render3d_st.stream_walk_no } - prof_gen_add(c.count + 512) + prof_gen_add(render3d_st, c.count + 512) } if c == null { missing = true @@ -257,13 +242,13 @@ function stream_update(s: Stream, cam_x: float, cam_z: float) -> void { # 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 = stream_walk_no } + if c != null { c.used = render3d_st.stream_walk_no } } - if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(s, cx, cz, c) { + if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(render3d_st, 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 - stream_us_gather = stream_us_gather + (gl_now_us() - tg) + render3d_st.stream_us_gather = render3d_st.stream_us_gather + (gl_now_us() - tg) } } } @@ -274,7 +259,7 @@ function stream_update(s: Stream, cam_x: float, cam_z: float) -> void { ring += 1 } s.pending = missing - stream_us_walk = stream_us_walk + (gl_now_us() - tw) + render3d_st.stream_us_walk = render3d_st.stream_us_walk + (gl_now_us() - tw) # force the layer to re-partition its (new) instances l.view_gen = -1 } @@ -282,24 +267,24 @@ function stream_update(s: Stream, cam_x: float, cam_z: float) -> void { # 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(s: Stream, cx: int, cz: int, c: Chunk) -> bool { +function stream_chunk_visible(render3d_st: Render3dState, 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(wx, cy, wz, r) + return cam_sphere_visible(render3d_st, wx, cy, wz, r) } # what the caches hold, and whether they are being churned (R3D_PROF) -function stream_census() -> void { - if stream_all == null { return } +function stream_census(render3d_st: Render3dState) -> void { + if render3d_st.stream_all == null { return } print("") - print(`ground-cover chunk caches (cap {string(STREAM_MAX_CHUNKS)} each, {string(stream_evictions)} evictions over the run):`) + print(`ground-cover chunk caches (cap {string(render3d_st.STREAM_MAX_CHUNKS)} each, {string(render3d_st.stream_evictions)} evictions over the run):`) var inst = 0 - for i in 0 .. len(stream_all) { - let s = stream_all[i] + for i in 0 .. len(render3d_st.stream_all) { + let s = render3d_st.stream_all[i] var n = 0 for k in 0 .. s.n { n += s.chunks[k].count } inst += n @@ -308,21 +293,21 @@ function stream_census() -> void { print(` {string(inst)} instances held, {string(inst * INST_FLOATS * 4 / 1024)} KB`) } -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]) } +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]) } } # every stream and its cached chunks, for a world being replaced (scatter_clear_all) -function stream_clear_all() -> void { - if stream_all == null { return } - for i in 0 .. len(stream_all) { - let s = stream_all[i] +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] 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) } } - stream_all = null + render3d_st.stream_all = null } diff --git a/packages/ludic.render3d/streamline.ludic b/packages/ludic.render3d/streamline.ludic index 460d4168..970532c3 100644 --- a/packages/ludic.render3d/streamline.ludic +++ b/packages/ludic.render3d/streamline.ludic @@ -30,22 +30,7 @@ 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) } @@ -74,36 +59,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() -> void { +function gsl_boot(render3d_st: mut Render3dState) -> void { if Os.platform() != "windows" { return } - if r3d_env_has("R3D_NO_STREAMLINE") { return } + if r3d_env_has(render3d_st, "R3D_NO_STREAMLINE") { return } Vk.sl_prefer(1) } # After Vk.open(), before vkCreateInstance. -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) +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) # 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("R3D_SL_LOG") { + if r3d_env_has(render3d_st, "R3D_SL_LOG") { level = 2 - 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) + 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) } 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, gsl_feats) + Vk.put_ptr(pref, 96, render3d_st.gsl_feats) Vk.put_i32(pref, 104, 5) Vk.put_i32(pref, 112, 0) # engine eCustom Vk.put_ptr(pref, 120, "ludic render3d") @@ -111,11 +96,11 @@ function gsl_init() -> 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 } - 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) + 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) } # kSDKVersion, built from longs: (2 << 48) on ints is computed in 32 bits and arrives as garbage, @@ -129,90 +114,86 @@ function gsl_sdk_version() -> long { } # After vkCreateDevice: what this adapter supports. -function gsl_probe_device(pd: pointer) -> void { - if not gsl_on { return } +function gsl_probe_device(render3d_st: mut Render3dState, pd: pointer) -> void { + if not render3d_st.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) - 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}`) + 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}`) } # Before the device goes. -function gsl_shutdown() -> void { - if not gsl_on { return } +function gsl_shutdown(render3d_st: mut Render3dState) -> void { + if not render3d_st.gsl_on { return } Vk.sl_shutdown() - gsl_on = false + render3d_st.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(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(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_live() -> bool { return gsl_on and gsl_reflex_ok and gsl_reflex_mode > 0 } +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 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_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_reflex_apply() -> void { - if not gsl_reflex_ok or gsl_reflex_applied == gsl_reflex_mode { return } +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 } 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, 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 } + 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 } } -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) } +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) } } - gsl_marker(GSL_SIM_START) + gsl_marker(render3d_st, GSL_SIM_START) } # r3d_frame's first line: the game has simulated, the renderer starts recording -function gsl_frame_start() -> void { - if not gsl_on or gpu_kind != GPU_VK { return } +function gsl_frame_start(render3d_st: mut Render3dState) -> void { + if not render3d_st.gsl_on or render3d_st.gpu_kind != GPU_VK { return } # a headless run never presents: each frame takes its own token here - if not gsl_fresh { gsl_new_frame() } - gsl_fresh = false - gsl_marker(GSL_SIM_END) - gsl_marker(GSL_SUBMIT_START) + 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) } -function gsl_before_present() -> void { - if not gsl_on { return } - gsl_marker(GSL_SUBMIT_END) - gsl_marker(GSL_PRESENT_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_after_present() -> void { - if not gsl_on { return } - gsl_marker(GSL_PRESENT_END) - gsl_new_frame() +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) } # ---- DLSS super resolution ---------------------------------------------------------------- @@ -221,39 +202,16 @@ function gsl_after_present() -> 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(mode: int) -> void { +function r3d_dlss(render3d_st: mut Render3dState, mode: int) -> void { var m = mode - 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 + 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 } -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 } +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 } # sl::DLSSMode from the setting function gsl_sl_mode(m: int) -> int { @@ -264,43 +222,43 @@ function gsl_sl_mode(m: int) -> int { return 0 } -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 +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 # 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("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 + 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 } # the render size for the display's size and the mode, asked once per change -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 +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 let f = gsl_fn(GSL_DLSS, "slDLSSGetOptimalSettings") if f == null { return } - gsl_fill_options(gl_w, gl_h) + gsl_fill_options(render3d_st, 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, gsl_opts, os) == 0 { + if Vk.sl_call_pp(f, render3d_st.gsl_opts, os) == 0 { let w = Vk.get_i32(os, 32) let h = Vk.get_i32(os, 36) - if w > 0 and h > 0 { gsl_rw = w; gsl_rh = h } + if w > 0 and h > 0 { render3d_st.gsl_rw = w; render3d_st.gsl_rh = h } } } -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 } +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 } # a radical-inverse sample in [0, 1), float bits function gsl_halton(i: int, b: int) -> float { @@ -317,52 +275,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() -> void { - gsl_jitter_x = 0.0; gsl_jitter_y = 0.0; gsl_jpx = 0.0; gsl_jpy = 0.0 +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 # 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() or not gsl_eval_ok or gsl_token == null or post_w <= 0 or post_h <= 0 { return } + 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 } # DLSS wants at least 8 x (display / render)^2 phases; 32 covers performance mode - 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) + 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) } # sl::Resource for one of the renderer's textures, in the layout every pass leaves them in -function gsl_resource(at: int, tex: int) -> void { - let p = Vk.at(gsl_res, at) +function gsl_resource(render3d_st: Render3dState, at: int, tex: int) -> void { + let p = Vk.at(render3d_st.gsl_res, at) Vk.zero(p, 112) gsl_header(p, 0x3a9d, 0x70cf, 0x2418, 0x4b72, 0x8391, 0x13f8, 0x721c, 0x7261, 1) - 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_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_i32(p, 64, GSL_LAYOUT_READ) - 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)) + 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)) } # the usage gvk_tex_storage gave the image -function gvk_tex_usage(tex: int) -> int { - let fmt = gvk_tex_vkfmt[tex] +function gvk_tex_usage(render3d_st: Render3dState, tex: int) -> int { + let fmt = render3d_st.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 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 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 } return usage } # sl::ResourceTag i, pointing at resource i -function gsl_tag(i: int, buffer: int, w: int, h: int) -> void { - let p = Vk.at(gsl_tags, i * 64) +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) Vk.zero(p, 64) gsl_header(p, 0x4c6a, 0x5aad, 0xb445, 0x496c, 0x87ff, 0x1af3, 0x845b, 0xe653, 1) - Vk.put_ptr(p, 32, Vk.at(gsl_res, i * 112)) + Vk.put_ptr(p, 32, Vk.at(render3d_st.gsl_res, i * 112)) Vk.put_i32(p, 40, buffer) Vk.put_i32(p, 44, 2) # eValidUntilEvaluate Vk.put_i32(p, 56, w) @@ -387,15 +345,15 @@ function gsl_clip_fix(m: floats) -> void { m[14] = 0.5 } -function gsl_constants() -> void { - if gsl_consts == null { gsl_consts = gsl_struct(456) } - let k = gsl_consts +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 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, cam_fov, cam_aspect, cam_near, cam_far) + m4_perspective(proj, render3d_st.cam_fov, render3d_st.cam_aspect, render3d_st.cam_near, render3d_st.cam_far) m4_mul(v2c, fix, proj) m4_inverse(c2v, v2c) gsl_put_m4(k, 32, v2c) # cameraViewToClip (no jitter) @@ -403,9 +361,9 @@ function gsl_constants() -> void { let ident = m4_new() m4_identity(ident) gsl_put_m4(k, 160, ident) # clipToLensClip - 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) + 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) m4_inverse(inv_cur, cur) m4_mul(c2p, prev, inv_cur) m4_inverse(p2c, c2p) @@ -414,87 +372,87 @@ function gsl_constants() -> 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 = -gsl_jpx - var jy = -gsl_jpy + var jx = -render3d_st.gsl_jpx + var jy = -render3d_st.gsl_jpy # R3D_DLSS_JX / R3D_DLSS_JY = -1 flip a sign, to check the convention against the picture - 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 } + 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 } 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, cam_pos) + gsl_put_v3(k, 376, render3d_st.cam_pos) let up = floats(3) - v3_cross(up, cam_right, cam_fwd) + v3_cross(up, render3d_st.cam_right, render3d_st.cam_fwd) gsl_put_v3(k, 388, up) - 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)) + 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)) 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 gsl_reset { Vk.put_i32(k, 444, 256 * 256 * 256) } + if render3d_st.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() -> 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 +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 } - 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 + 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 } # the LDR image the tonemap writes follows the upscaled size - 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.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 gsl_res == null { gsl_res = gsl_struct(4 * 112); gsl_tags = gsl_struct(4 * 64); gsl_inputs = bytes(8) } + 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) } } # Upscale post_hdr into gsl_out; the colour the rest of post reads (post_hdr's own if it failed). -function gsl_dlss_eval() -> int { - gsl_targets() +function gsl_dlss_eval(render3d_st: mut Render3dState) -> int { + gsl_targets(render3d_st) # no motion of its own: the camera's comes from depth - 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 { + 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 { let f = gsl_fn(GSL_DLSS, "slDLSSSetOptions") - 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_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_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 + 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 # 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 - gsl_eval_ok = r == 0 + render3d_st.gsl_eval_ok = r == 0 if r != 0 { - 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 + 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 } - post_color_w = gl_w; post_color_h = gl_h - return gsl_out.color + render3d_st.post_color_w = gl_w; render3d_st.post_color_h = gl_h + return render3d_st.gsl_out.color } diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index 806f1261..ecf7866c 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -4,88 +4,33 @@ # 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. -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 +function terrain_set_carpet(render3d_st: mut Render3dState, tex: int) -> void { render3d_st.ter_carpet = tex } # Carve a lake bed below `level` inside the ellipse (cx, cz) ± (ex, ez); call before r3d_init. -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 +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 } # 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. -var ter_lake_carve: bool = true -function terrain_lake_carve(on: bool) -> void { ter_lake_carve = on } +function terrain_lake_carve(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.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. -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 } +function terrain_sea(render3d_st: mut Render3dState, level: float) -> void { render3d_st.ter_sea_level = level; render3d_st.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() -> float { if ter_sea_set { return ter_sea_level }; return ter_lake_level } +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 } # 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 @@ -93,15 +38,10 @@ function ter_sea_gen() -> float { if ter_sea_set { return ter_sea_level }; retu const TER_ISLE_NONE: int = 0 const TER_ISLE_RADIAL: int = 1 const TER_ISLE_COAST: int = 2 -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 } +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 } } # 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. @@ -116,29 +56,26 @@ function terrain_island(cx: float, cz: float, r: float, fall: float) -> void { # # 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(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 } +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 } } # 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(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)) +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)) if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 } - 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] + 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] } # 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 @@ -168,8 +105,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(x: float, z: float) -> float { - let c = terrain_ortho(x, z) +function terrain_ortho_green(render3d_st: mut Render3dState, x: float, z: float) -> float { + let c = terrain_ortho(render3d_st, 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 } @@ -178,14 +115,14 @@ function terrain_ortho_green(x: float, z: float) -> 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(x: float, z: float) -> float { +function terrain_ortho_scree(render3d_st: mut Render3dState, x: float, z: float) -> float { var votes = 0 - 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 } } } + 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 } } } if votes >= 15 { return 1.0 } return 0.0 } -function terrain_ortho_scree_pixel(x: float, z: float) -> float { - let c = terrain_ortho(x, z) +function terrain_ortho_scree_pixel(render3d_st: mut Render3dState, x: float, z: float) -> float { + let c = terrain_ortho(render3d_st, 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)) @@ -194,8 +131,8 @@ function terrain_ortho_scree_pixel(x: float, z: float) -> float { return 1.0 } # dense conifer forest in the photograph: green-dominant and dark (the meadows are brighter) -function terrain_ortho_forest(x: float, z: float) -> float { - let c = terrain_ortho(x, z) +function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float) -> float { + let c = terrain_ortho(render3d_st, 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)) @@ -205,107 +142,107 @@ function terrain_ortho_forest(x: float, z: float) -> float { return 0.0 } -function terrain_use_ortho(path: string) -> void { - let px = png_decode(path) +function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void { + let px = png_decode(render3d_st, path) if px == null { return } - 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) + 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) } -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_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_generate() -> void { +function terrain_generate(render3d_st: mut Render3dState) -> void { var defs = "" - 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) + 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) var carve = 1.0 - 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)) + 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)) } - mesh_draw(sky_fullscreen) + mesh_draw(render3d_st, render3d_st.sky_fullscreen) # second pass: R = height, GBA = the smooth surface normal, baked once (ternormal.frag) - 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) + 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) # read the heights back for placement - 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") + 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") } # The height field for shaders that place things on the ground (model.vert's u_ground) -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) +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) } # 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() -> 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") +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") } # height at world (x, z) — float bits, bilinear over the CPU copy @@ -313,42 +250,39 @@ function terrain_bake_shadow() -> 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(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 +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 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 = 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] + 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] return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz) } # the same for Q16.16 callers -function terrain_height_fx(x: fixed, z: fixed) -> fixed { return fixed(terrain_height(float(x), float(z))) } +function terrain_height_fx(render3d_st: mut Render3dState, x: fixed, z: fixed) -> fixed { return fixed(terrain_height(render3d_st, 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. -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 +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 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) @@ -362,7 +296,7 @@ function terrain_height_smooth(x: float, z: float) -> float { let w1 = (4.0 - 6.0 * t2 + 3.0 * t3) / 6.0 let w3 = t3 / 6.0 let w2 = 1.0 - w0 - w1 - w3 - ter_bw[a * 4] = w0; ter_bw[a * 4 + 1] = w1; ter_bw[a * 4 + 2] = w2; ter_bw[a * 4 + 3] = 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 } var h = 0.0 for j in 0 .. 4 { @@ -372,62 +306,62 @@ function terrain_height_smooth(x: float, z: float) -> float { 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 + ter_bw[i] * ter_heights[rz * TERRAIN_RES + rx] + row = row + render3d_st.ter_bw[i] * render3d_st.ter_heights[rz * TERRAIN_RES + rx] } - h = h + ter_bw[4 + j] * row + h = h + render3d_st.ter_bw[4 + j] * row } return h } -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_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_init() -> void { - for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(i) } +function terrain_init(render3d_st: mut Render3dState) -> void { + for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(render3d_st, 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(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_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_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") +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") var defs = "" - 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" } + 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" } # 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. @@ -437,22 +371,22 @@ function terrain_init_finish() -> 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("R3D_TFAST") { defs = defs + "#define TFAST_" + r3d_env("R3D_TFAST") + "\n" } + if r3d_env_has(render3d_st, "R3D_TFAST") { defs = defs + "#define TFAST_" + r3d_env(render3d_st, "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. - 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") + 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") } # draw into the current cascade with the given light view-projection @@ -464,46 +398,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(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]) +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]) # 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 = ter_ortho_tex - if orthotex == 0 { orthotex = ter_tex[0] } - r3d_bind_2d(p, "u_ortho", 4, orthotex) + 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 oon = 0.0 - 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) + 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) var lake = -100000.0 - if ter_lake_ex != 0.0 { lake = ter_lake_level } - u_f(gpu_uniform(p, "u_lake_level"), lake) + 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) # the shoreline, forest and scree gates read the sea; unset it is what they always read var sea = lake - 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) + 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) # 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 @@ -514,17 +448,17 @@ function terrain_bind_prog(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(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)) + 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)) # 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 ter_sun_inline { - gpu_tex_unit(15) - gpu_tex_bind(GPU_TEX2D_ARRAY, 0) - r3d_bind_2d(p, "u_sunshadow", 15, ter_sun_tex) + 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) } } @@ -540,119 +474,117 @@ function terrain_bind_prog(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(w: int, h: int) -> Target { - var t = ter_sun_tgt - if ter_reflect { t = ter_sun_refl } +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 } if t == null or t.w != w or t.h != h { - 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 } + 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 } } return t } # Rasterise the patches once with the small shader that reads the cascades (tersun.frag). -function terrain_sun_pass(w: int, h: int, depth: int) -> Target { - let t = terrain_sun_target(w, h) +function terrain_sun_pass(render3d_st: mut Render3dState, w: int, h: int, depth: int) -> Target { + let t = terrain_sun_target(render3d_st, 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(t) - gpu_fb_depth(depth) - if not ter_sun_checked { - ter_sun_checked = true - let st = gpu_fb_status() + 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) if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: sun-visibility framebuffer incomplete {st}`) } } - gpu_depth_test(true) - gpu_depth_func(GL_LESS) - gpu_depth_write(true) + gpu_depth_test(render3d_st, true) + gpu_depth_func(render3d_st, GL_LESS) + gpu_depth_write(render3d_st, true) # the depth is the frame's own and was cleared with it; only the visibility is cleared - 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") } + 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") } return t } -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_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_draw() -> void { - if ter_skip { return } +function terrain_draw(render3d_st: mut Render3dState) -> void { + if render3d_st.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 = 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 + 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 } - ter_sun_done = false - if ter_reflect { target_bind(water_refl) } + render3d_st.ter_sun_done = false + if render3d_st.ter_reflect { target_bind(render3d_st, render3d_st.water_refl) } else { - target_bind(post_hdr) - if post_ms_fbo != 0 { gpu_fb_bind(post_ms_fbo) } + 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_depth_test(true) + gpu_depth_test(render3d_st, 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(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})`) } + 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})`) } } -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() -> void { - let m = gpu_mesh_new() +function cdlod_init(render3d_st: mut Render3dState) -> void { + let m = gpu_mesh_new(render3d_st) 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(m, v, gl_bytes_of(n * n * 2), GPU_STATIC) - gpu_mesh_attr(m, 0, 2, GPU_F32, 8, 0, false) + 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) free(v) let ni = CD_G * CD_G * 6 let idx = words(ni) @@ -665,21 +597,21 @@ function cdlod_init() -> void { k += 6 } } - gpu_mesh_indices(m, data_of(idx), ni * 4, 4) + gpu_mesh_indices(render3d_st, m, data_of(idx), ni * 4, 4) free(idx) m.count = ni - gpu_mesh_done(m) - cd_mesh = m - cd_range = floats(CD_LEVELS) + gpu_mesh_done(render3d_st, m) + render3d_st.cd_mesh = m + render3d_st.cd_range = floats(CD_LEVELS) var r = 48.0 - 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() + 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) } # min/max height per patch at every level, from the CPU copy of the height field -function cdlod_bounds() -> void { - cd_min = new []floats; cd_max = new []floats +function cdlod_bounds(render3d_st: mut Render3dState) -> void { + render3d_st.cd_min = new []floats; render3d_st.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) @@ -690,14 +622,14 @@ function cdlod_bounds() -> 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 = ter_heights[ty * TERRAIN_RES + tx] + let h = render3d_st.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(cd_min, lo); push(cd_max, hi) + push(render3d_st.cd_min, lo); push(render3d_st.cd_max, hi) while n > 1 { let m = n / 2 let plo = floats(m * m); let phi = floats(m * m) @@ -708,7 +640,7 @@ function cdlod_bounds() -> void { phi[j * m + i] = Math.max(Math.max(hi[a], hi[a + 1]), Math.max(hi[a + n], hi[a + n + 1])) } } - push(cd_min, plo); push(cd_max, phi) + push(render3d_st.cd_min, plo); push(render3d_st.cd_max, phi) lo = plo; hi = phi; n = m } } @@ -717,86 +649,86 @@ function cdlod_bounds() -> 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(level: int) -> float { return float(32 << level) * (float(TERRAIN_HALF * 2) / 8192.0) } +function cd_size(render3d_st: Render3dState, level: int) -> float { return float(32 << level) * (float(render3d_st.TERRAIN_HALF * 2) / 8192.0) } # does the patch's box come within r of the camera? -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) +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) 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(x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool { +function cd_inside(render3d_st: Render3dState, 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(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)) + 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)) return dx * dx + dz * dz + dy * dy < r * r } -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 +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 # 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 ter_sun_pass { - let t = ter_scratch() + if render3d_st.ter_sun_pass { + let t = ter_scratch(render3d_st) t[0] = x0; t[1] = z0; t[2] = size - u_v3(gpu_uniform(ter_sun_prog, "u_node"), t) + u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.ter_sun_prog, "u_node"), t) var st0 = 0.0 - 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) + 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) return } let n = CD_LEAVES >> level - 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() + 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) t[0] = x0; t[1] = z0; t[2] = size - u_v3(gpu_uniform(p, "u_node"), t) + u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_node"), t) var start = 0.0 - 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 + 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 } # 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(level: int, ix: int, iz: int) -> bool { +function cdlod_select(render3d_st: mut Render3dState, level: int, ix: int, iz: int) -> bool { let n = CD_LEAVES >> level - 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 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 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(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 } + 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 } for c in 0 .. 4 { let cx = ix * 2 + (c & 1); let cz = iz * 2 + (c >> 1) - if not cdlod_select(level - 1, cx, cz) { cdlod_draw(level - 1, cx, cz) } + if not cdlod_select(render3d_st, level - 1, cx, cz) { cdlod_draw(render3d_st, level - 1, cx, cz) } } return true } @@ -807,40 +739,39 @@ function cdlod_select(level: int, ix: int, iz: int) -> bool { # 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() -> 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 +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 } # derived from TERRAIN_HALF and cached on first use: stale ones would keep the old size - ter_h_scale = 0.0; ter_o_scale = 0.0 - ter_carpet = 0 - ter_shadow_gen = -1; ter_shadow_yaw = 1000000000.0 + 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 } # 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(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 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") } -var ter_scr: floats = null -function ter_scratch() -> floats { - if ter_scr == null { ter_scr = floats(16) } - return ter_scr +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 } diff --git a/packages/ludic.render3d/texture.ludic b/packages/ludic.render3d/texture.ludic index 50b18e26..8e6211ec 100644 --- a/packages/ludic.render3d/texture.ludic +++ b/packages/ludic.render3d/texture.ludic @@ -11,38 +11,32 @@ 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(level: int) -> void { +function r3d_set_anisotropy(render3d_st: mut Render3dState, 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 == tex_anisotropy { return } - tex_anisotropy = a - if gpu_tx == null { return } - let keep = gpu_bound_2d - for t in 1 .. gpu_tx_cap { + 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 { let o = t * GPU_TX_W # a 2D texture with mipmaps that was given a level when it was uploaded - 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 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 keep > 0 { gpu_tex_bind(GPU_TEX2D, keep) } + if keep > 0 { gpu_tex_bind(render3d_st, GPU_TEX2D, keep) } } -function r3d_read_file(path: pointer) -> pointer { +function r3d_read_file(render3d_st: mut Render3dState, path: pointer) -> pointer { let f = file_open(path, "rb") if f == null { return null } file_seek(f, 0, 2) @@ -52,7 +46,7 @@ function r3d_read_file(path: pointer) -> pointer { let buf = bytes(n + 8) file_read(f, buf, n) file_close(f) - tex_file_len = n + render3d_st.tex_file_len = n return buf } @@ -118,10 +112,10 @@ function png_unfilter(raw: pointer, cur: int, prev: int, stride: int, fbpp: int, } } -function png_decode(path: pointer) -> pointer { - let d = r3d_read_file(path) +function png_decode(render3d_st: mut Render3dState, path: pointer) -> pointer { + let d = r3d_read_file(render3d_st, path) if d == null { print(`png: cannot read {path}`); return null } - let size = tex_file_len + let size = render3d_st.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) @@ -188,16 +182,16 @@ function png_decode(path: pointer) -> pointer { free(raw) } free(plte) - tex_w = w; tex_h = h; tex_channels = channels; tex_depth = bd + render3d_st.tex_w = w; render3d_st.tex_h = h; render3d_st.tex_channels = channels; render3d_st.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(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 +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 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 } @@ -227,101 +221,91 @@ function tex_dilate(px: pointer, thresh: int, passes: int) -> void { } # Upload the last-decoded samples as a 2D texture. srgb: colour data (8-bit only). -function tex_upload(px: pointer, srgb: bool, mips: bool) -> int { - let id = gpu_tex_new() - gpu_tex_bind(GPU_TEX2D, id) +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) var fmt = GL_RED - if tex_channels == 2 { fmt = GL_RG } - if tex_channels == 3 { fmt = GL_RGB } - if tex_channels == 4 { fmt = GL_RGBA } + 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 } var ifmt = GL_R8 var ty = GL_UNSIGNED_BYTE - if tex_depth == 16 { + if render3d_st.tex_depth == 16 { ty = GL_UNSIGNED_SHORT ifmt = GL_R16 - 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) + 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) } else { - 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) + 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) } - 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) + 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) if mips { - 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) + 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) } else { - gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + gpu_tex_param(render3d_st, 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(path: pointer, srgb: bool) -> int { return tex_load_ex(path, srgb, 0) } +function tex_load(render3d_st: mut Render3dState, path: pointer, srgb: bool) -> int { return tex_load_ex(render3d_st, path, srgb, 0) } # ... with `dilate` passes of edge padding for a cut-out atlas (0 = none) -function tex_load_ex(path: pointer, srgb: bool, dilate: int) -> int { - let px = png_decode(path) +function tex_load_ex(render3d_st: mut Render3dState, path: pointer, srgb: bool, dilate: int) -> int { + let px = png_decode(render3d_st, path) if px == null { return 0 } - if dilate > 0 { tex_dilate(px, 60, dilate) } - let id = tex_upload(px, srgb, true) + if dilate > 0 { tex_dilate(render3d_st, px, 60, dilate) } + let id = tex_upload(render3d_st, px, srgb, true) free(px) - tex_note_size(id) + tex_note_size(render3d_st, id) return id } # the size each loaded texture was, by id: a nine-slice or a UI image asks -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_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) } -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] } +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] } } return 0 } -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] } +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] } } return 0 } # A small solid-colour fallback texture (linear rgb 0..255), for missing maps. -function tex_solid(r: int, g: int, b: int, a: int) -> int { +function tex_solid(render3d_st: mut Render3dState, 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 } - tex_w = 2; tex_h = 2; tex_channels = 4; tex_depth = 8 - let id = tex_upload(px, false, false) + 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) 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(path: pointer) -> floats { - let d = r3d_read_file(path) +function hdr_decode(render3d_st: mut Render3dState, path: pointer) -> floats { + let d = r3d_read_file(render3d_st, path) if d == null { print(`hdr: cannot read {path}`); return null } - let size = tex_file_len + let size = render3d_st.tex_file_len # header: lines until an empty line, then "-Y h +X w" var i = 0 var blank = false @@ -340,7 +324,7 @@ function hdr_decode(path: pointer) -> floats { let out = floats(w * h * 3) let line = bytes(w * 4) var maxl = 0.0 - if hdr_clip == 0.0 { hdr_clip = 20.0 } + if render3d_st.hdr_clip == 0.0 { render3d_st.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) @@ -385,61 +369,60 @@ function hdr_decode(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; 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 + 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 } } } y += 1 } free(line); free(d) - 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 + 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 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(path: pointer) -> int { - let px = hdr_decode(path) +function tex_load_hdr(render3d_st: mut Render3dState, path: pointer) -> int { + let px = hdr_decode(render3d_st, path) if px == null { return 0 } - 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) + 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) free(px) return id } # An empty render-target texture of the given internal format (no mips, clamped). -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) +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) return id } -var tex_dump_alpha: bool = false # Debug: the brightest texel of an RGBA float texture and where it is. -function tex_max(tex: int, w: int, h: int, tag: pointer) -> void { +function tex_max(render3d_st: mut Render3dState, tex: int, w: int, h: int, tag: pointer) -> void { let buf = floats(w * h * 4) - 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)) + 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)) var best = 0.0; var bx = 0; var by = 0 var i = 0 while i < w * h { @@ -455,20 +438,20 @@ function tex_max(tex: int, w: int, h: int, tag: pointer) -> void { free(buf) } # Debug: write a 2D texture's level 0 (RGBA8, alpha dropped) as a binary PPM. -function tex_dump(tex: int, w: int, h: int, path: pointer) -> void { +function tex_dump(render3d_st: mut Render3dState, 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(GPU_TEX2D, tex) - gpu_pixel_store(GL_PACK_ALIGNMENT, 1) - gpu_tex_read(GPU_TEX2D, GL_RGBA, GL_UNSIGNED_BYTE, buf) + 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) 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 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 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 } } file_write(f, row, w * 3) } @@ -478,10 +461,10 @@ function tex_dump(tex: int, w: int, h: int, path: pointer) -> void { # 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(path: pointer, shrink: int) -> int { - let d = r3d_read_file(path) +function tex_load_ppm(render3d_st: mut Render3dState, path: pointer, shrink: int) -> int { + let d = r3d_read_file(render3d_st, path) if d == null { return 0 } - let size = tex_file_len + let size = render3d_st.tex_file_len var i = 2 var w = 0; var h = 0; var mx = 0 var field = 0 @@ -511,8 +494,8 @@ function tex_load_ppm(path: pointer, shrink: int) -> int { } } free(d) - tex_w = ow; tex_h = oh; tex_channels = 3; tex_depth = 8 - let id = tex_upload(px, true, false) + 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) free(px) return id } diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index 126a7bfd..e85b01d0 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -5,108 +5,83 @@ # 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() -> 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) } +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) } # 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 - 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) + 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) } # save the camera - 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] + 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] # 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 * water_level - sy, sz) + let eye = v3_new(sx, 2.0 * render3d_st.water_level - sy, sz) let refl = m4_new() refl[5] = -1.0 - refl[13] = 2.0 * water_level + refl[13] = 2.0 * render3d_st.water_level let mv = floats(16) - m4_mul(mv, water_saved, refl) - m4_copy(cam_view, mv) + m4_mul(mv, render3d_st.water_saved, refl) + m4_copy(render3d_st.cam_view, mv) free(mv); free(refl) let fwd = words(3); let up = words(3); let at = words(3) - 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") } + 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") } # restore - 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) + 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) free(eye); free(fwd); free(up); free(at) - gpu_fb_bind(0) + gpu_fb_bind(render3d_st, 0) } # Is any of the mirrored water in view? The reflection pass is a second copy of the terrain, the @@ -118,39 +93,32 @@ function water_reflection_pass() -> 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. -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 { +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 { # R3D_REFL_DBG: once, what the test is made of and how much of it is in view - wb_dbg = 2 + render3d_st.wb_dbg = 2 var seen = 0 var hidden = 0 - 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`) + 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`) } # 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 .. wb_nblocks { + for b in 0 .. render3d_st.wb_nblocks { let o = b * 6 - 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 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 checked >= 16 { return true } checked += 1 - if not wb_cell_occluded(i) { return true } + if not wb_cell_occluded(render3d_st, i) { return true } } } return false @@ -158,82 +126,82 @@ function water_reflect_visible() -> 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(x0: float, z0: float, x1: float, z1: float) -> bool { - if cam_planes == null { return true } +function wb_rect_visible(render3d_st: Render3dState, x0: float, z0: float, x1: float, z1: float) -> bool { + if render3d_st.cam_planes == null { return true } for p in 0 .. 4 { let q = p * 4 - let a = cam_planes[q]; let c = cam_planes[q + 2] - let by = cam_planes[q + 1] * water_level + cam_planes[q + 3] + 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 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(i: int) -> bool { +function wb_cell_visible(render3d_st: Render3dState, i: int) -> bool { let o = i * 4 - return wb_rect_visible(wb_cells[o], wb_cells[o + 1], wb_cells[o + 2], wb_cells[o + 3]) + 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]) } # 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(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] +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] 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 + (water_level - ey) * t - if terrain_height(cam_pos[0] + dx * t, cam_pos[2] + dz * t) > sy + margin { return true } + 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 } } return false } # every point the wet test sampled (the centre and four near the corners) is out of sight -function wb_cell_occluded(i: int) -> bool { +function wb_cell_occluded(render3d_st: mut Render3dState, i: int) -> bool { let o = i * 4 - let x0 = wb_cells[o]; let z0 = wb_cells[o + 1]; let x1 = wb_cells[o + 2]; let z1 = wb_cells[o + 3] + 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 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(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) + 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) } -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_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_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_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_wet(x: float, z: float, off: float) -> bool { - if terrain_height(x, z) < water_level { return true } +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 } if off == 0.0 { return false } - 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 + 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 } # 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() -> 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) +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) let inside = tx1 > tx0 and tz1 > tz0 var cell = 32.0 var nx = 0 @@ -244,35 +212,35 @@ function water_cells_build() -> void { nx = int((tx1 - tx0) / cell) + 1 nz = int((tz1 - tz0) / cell) + 1 } - if wb_cells != null { free(wb_cells) } - if wb_blocks != null { free(wb_blocks) } - wb_cells = floats((nx * nz + 64 * 64) * 4) + 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) let nbx = (nx + WB_BLOCK - 1) / WB_BLOCK let nbz = (nz + WB_BLOCK - 1) / WB_BLOCK - wb_blocks = words((nbx * nbz + 1) * 6) - wb_ncells = 0 - wb_nblocks = 0 + render3d_st.wb_blocks = words((nbx * nbz + 1) * 6) + render3d_st.wb_ncells = 0 + render3d_st.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 = wb_ncells + let first = render3d_st.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(x0 + half, z0 + half, off) { wb_cell_add(x0, z0, x0 + cell, z0 + cell) } + if wb_wet(render3d_st, x0 + half, z0 + half, off) { wb_cell_add(render3d_st, 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(x0, z0, x0 + float(WB_BLOCK) * cell, z0 + float(WB_BLOCK) * cell, first) + wb_block_add(render3d_st, x0, z0, x0 + float(WB_BLOCK) * cell, z0 + float(WB_BLOCK) * cell, first) } } - let first = wb_ncells + let first = render3d_st.wb_ncells let cw = (bx1 - bx0) / 64.0 let ch = (bz1 - bz0) / 64.0 for iz in 0 .. 64 { @@ -281,64 +249,64 @@ function water_cells_build() -> 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(x0 + cw * 0.5, z0 + ch * 0.5, 0.0) { wb_cell_add(x0, z0, x1, z1) } + if wb_wet(render3d_st, x0 + cw * 0.5, z0 + ch * 0.5, 0.0) { wb_cell_add(render3d_st, x0, z0, x1, z1) } } } - wb_block_add(bx0, bz0, bx1, bz1, first) + wb_block_add(render3d_st, 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() -> 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_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_bodies_clear() -> void { - wb_n = 0 - wb_primary = -1 - wb_ncells = -1 - water_on = false +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 } # 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(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 +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 } return i } # one reflecting plane: what this function always meant, without a new mesh and program # every time it is called -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) +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) } # call after the opaque pass, before the sky: blends over the resolved depth -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) +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) # 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(gpu_uniform(p, "u_screen"), float(post_w), float(post_h)) + u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_screen"), float(render3d_st.post_w), float(render3d_st.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. @@ -347,49 +315,49 @@ function water_draw(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 = wt_wade_x - let wz = wt_wade_z - let ws = wt_wade_s - u_f3(gpu_uniform(p, "u_wade"), wx, wz, ws) + 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) var ron = 0.0 - if water_refl != null and wb_primary >= 0 { + if render3d_st.water_refl != null and render3d_st.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(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_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_depth", 0, depth_tex) - r3d_bind_2d(p, "u_scene", 2, post_scene.color) - sky_bind_lighting(p) - shadow_bind(p) - fog_bind(p) + 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) # 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(false) - gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + gpu_blend(render3d_st, false) + gpu_blend_func(render3d_st, 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(true) - gpu_cull(false) + gpu_depth_write(render3d_st, true) + gpu_cull(render3d_st, 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 .. 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]) + 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]) var r = 0.0 - if i == wb_primary { r = ron } - u_f(gpu_uniform(p, "u_refl_on"), r) + if i == render3d_st.wb_primary { r = ron } + u_f(render3d_st, gpu_uniform(render3d_st, 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 == 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) + 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) } - gpu_blend(false) + gpu_blend(render3d_st, false) } diff --git a/packages/ludic.session/near.ludic b/packages/ludic.session/near.ludic index 056cd203..af272e6b 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(x: float, z: float) -> float { - session__near_i = 0 +export function ply_near_d2(session_st: mut SessionState, x: float, z: float) -> float { + session_st.session__near_i = 0 var best = 0.0 var first = true for p in 0 .. PLY_MAX { - if not ply_on(p) { continue } - let dx = x - ply_x(p) - let dz = z - ply_z(p) + if not ply_on(session_st, p) { continue } + let dx = x - ply_x(session_st, p) + let dz = z - ply_z(session_st, p) let d2 = dx * dx + dz * dz if first or d2 < best { best = d2 - session__near_i = p + session_st.session__near_i = p first = false } } @@ -19,12 +19,12 @@ export function ply_near_d2(x: float, z: float) -> float { } # the distance to the nearest player, in metres; ply_near_i() says which -export function ply_near(x: float, z: float) -> float { return Math.sqrt(ply_near_d2(x, z)) } +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_i() -> int { return session__near_i } +export function ply_near_i(session_st: SessionState) -> int { return session_st.session__near_i } -export function ply_dist(p: int, x: float, z: float) -> float { - let dx = x - ply_x(p) - let dz = z - ply_z(p) +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) return Math.sqrt(dx * dx + dz * dz) } diff --git a/packages/ludic.session/night.ludic b/packages/ludic.session/night.ludic index d13862c8..92e2197c 100644 --- a/packages/ludic.session/night.ludic +++ b/packages/ludic.session/night.ludic @@ -1,43 +1,42 @@ # 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() -> bool { return session__me_sleeping } +export function ses_sleeping(session_st: SessionState) -> bool { return session_st.session__me_sleeping } # this player turns in (or gets up): false alone, where a bed simply sleeps -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) +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) f.yes = on - if on { ses_night_check() } + if on { ses_night_check(base_st, session_st) } return true } # the host: a guest said it turned in, or got up -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() +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) } # the host: is everyone in? Then the night is due, and everyone's readiness starts again -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) +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) } # the night went by on this machine: awake again -export function ses_night_passed() -> void { session__me_sleeping = false } +export function ses_night_passed(session_st: mut SessionState) -> void { session_st.session__me_sleeping = false } # how many are still up, this player included -export function ses_sleep_waiting() -> int { +export function ses_sleep_waiting(session_st: SessionState) -> int { var n = 0 - 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 } + 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 } return n } diff --git a/packages/ludic.session/roles.ludic b/packages/ludic.session/roles.ludic index d5b42f19..48268a34 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() -> int { return session__role } +export function ses_role(session_st: SessionState) -> int { return session_st.session__role } -export function ses_set_role(r: int) -> void { - if r == session__role { return } - session__role = r - session__fact(SES_ROLE, -1, 0) +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) } # this machine runs the world: the clock, the weather, the animals, the people -export function ses_owns_world() -> bool { return session__role != SES_GUEST } +export function ses_owns_world(session_st: SessionState) -> bool { return session_st.session__role != SES_GUEST } # somebody else is in this world, so nothing this player opens may stop it -export function ses_live() -> bool { return session__role != SES_SOLO } +export function ses_live(session_st: SessionState) -> bool { return session_st.session__role != SES_SOLO } -export function ses_hosting() -> bool { return session__role == SES_HOST } +export function ses_hosting(session_st: SessionState) -> bool { return session_st.session__role == SES_HOST } -export function ses_guest() -> bool { return session__role == SES_GUEST } +export function ses_guest(session_st: SessionState) -> bool { return session_st.session__role == SES_GUEST } # whether this player lying down ends the night; alone, it always does -export function ses_night_passes() -> bool { return ses_owns_world() and not ses_live() } +export function ses_night_passes(session_st: SessionState) -> bool { return ses_owns_world(session_st) and not ses_live(session_st) } # the party a host allows (2..PLY_MAX) and whether it takes new players -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_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_open(open: bool) -> void { session__open = open } -export function ses_max() -> int { return session__max } -export function ses_open() -> bool { return session__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 } # may one more join: open, and not full -export function ses_room_for_one() -> bool { return session__open and ply_count() < session__max } +export function ses_room_for_one(session_st: SessionState) -> bool { return session_st.session__open and ply_count(session_st) < session_st.session__max } # the session is over: everyone else gone, alone again, nothing asked and nobody asleep -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 +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 } diff --git a/packages/ludic.session/roster.ludic b/packages/ludic.session/roster.ludic index f0865d11..9d56a989 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() -> void { session__init() } +export function ply_init(session_st: mut SessionState) -> void { session__init(session_st) } -export function ply_on(p: int) -> bool { +export function ply_on(session_st: SessionState, p: int) -> bool { if p == 0 { return true } - if session__slot == null or p < 0 or p >= PLY_MAX { return false } - return session__slot[p].on + if session_st.session__slot == null or p < 0 or p >= PLY_MAX { return false } + return session_st.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(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 } } +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 } } return -1 } -export function ply_count() -> int { +export function ply_count(session_st: SessionState) -> int { var n = 1 - for p in 1 .. PLY_MAX { if ply_on(p) { n += 1 } } + for p in 1 .. PLY_MAX { if ply_on(session_st, 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(pid: int) -> int { - session__init() +export function ply_join(base_st: mut BaseState, session_st: mut SessionState, pid: int) -> int { + session__init(session_st) if pid <= 0 { return -1 } # 0 is this machine's player for p in 1 .. PLY_MAX { - if session__slot[p].on { continue } + if session_st.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__slot[p] = r - session__fact(SES_JOINED, p, pid) + session_st.session__slot[p] = r + session__fact(base_st, session_st, SES_JOINED, p, pid) return p } return -1 } -export function ply_leave(p: int) -> void { - if p <= 0 or not ply_on(p) { return } - let r = session__slot[p] +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] r.on = false r.ready = false - session__fact(SES_LEFT, p, r.pid) + session__fact(base_st, session_st, SES_LEFT, p, r.pid) } -export function ply_leave_all() -> void { for p in 1 .. PLY_MAX { ply_leave(p) } } +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) } } # what a remote player's machine reported this tick -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] +export function ply_report(session_st: 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] r.x = x r.y = y r.z = z @@ -64,13 +64,13 @@ export function ply_report(p: int, x: float, y: float, z: float, yaw: float, spe r.speed = speed } -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_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_name(p: int, name: string) -> void { - if p <= 0 or not ply_on(p) { return } - session__slot[p].name = name +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 } diff --git a/packages/ludic.session/state.ludic b/packages/ludic.session/state.ludic index 35445521..982ce6af 100644 --- a/packages/ludic.session/state.ludic +++ b/packages/ludic.session/state.ludic @@ -59,31 +59,43 @@ function session__zero() -> float { return 0.0 } function session__one() -> float { return 1.0 } function session__nameless() -> string { return "" } -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 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 } -function session__fact(what: int, slot: int, pid: int) -> SessionFact { +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 { let f = new SessionFact f.what = what f.slot = slot f.pid = pid - f.role = session__role - q_push(ses_facts(), f) + f.role = session_st.session__role + q_push(base_st, ses_facts(base_st, session_st), f) return f } -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 +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 } diff --git a/packages/ludic.session/tests/session_test.ludic b/packages/ludic.session/tests/session_test.ludic index 954efe43..4ecdf407 100644 --- a/packages/ludic.session/tests/session_test.ludic +++ b/packages/ludic.session/tests/session_test.ludic @@ -4,174 +4,176 @@ import "ludic.session" import "ludic.base" program SessionTest { numbers float - var me_x: float = 0.0 - var me_z: float = 0.0 - function fx() -> float { return me_x } + state SessiontestState { + me_x: float = 0.0 + me_z: float = 0.0 + } + function fx(sessiontest_st: SessiontestState) -> float { return sessiontest_st.me_x } function fy() -> float { return 0.0 } - function fz() -> float { return me_z } + function fz(sessiontest_st: SessiontestState) -> float { return sessiontest_st.me_z } function fname() -> string { return "me" } bind SessionLocal { x: fn fx, y: fn fy, z: fn fz, name: fn fname } - function whats() -> []int { - let fs = q_drain(ses_facts()) + function whats(base_st: mut BaseState, session_st: mut SessionState) -> []int { + let fs = q_drain(base_st, ses_facts(base_st, session_st)) let out = new []int for i in 0 .. len(fs) { push(out, fs[i].what) } return out } - function last_of(what: int) -> SessionFact { - let fs = q_drain(ses_facts()) + 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)) 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(n: int) -> void { - ply_init() - ses_set_role(SES_HOST) - for i in 0 .. n { ply_join(2 + i) } - q_clear(ses_facts()) + 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)) } - 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) + 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) expect_eq(p, 1) - 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_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_eq(len(w), 2) expect_eq(w[0], SES_JOINED) expect_eq(w[1], SES_LEFT) } - 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 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 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 "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 "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) + 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) expect_eq(f.role, SES_GUEST) - ses_set_role(SES_HOST) - expect(ses_owns_world()) - expect(not ses_night_passes()) + ses_set_role(base_st, session_st, SES_HOST) + expect(ses_owns_world(session_st)) + expect(not ses_night_passes(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 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 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 "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 "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) + 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) expect(f.yes) - ses_sleep(false) - expect(not ses_sleeping()) + ses_sleep(base_st, session_st, false) + expect(not ses_sleeping(session_st)) } - 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) + 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) expect(f.yes) expect_eq(f.param, 9) expect(f.text == "set out") - expect(not ses_vote_live()) + expect(not ses_vote_live(session_st)) } - 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) + 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) expect(f != null) expect(not f.yes) } - 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) + 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) expect_eq(f.id, 5) expect(f.yes) - ses_vote_ended(5, true) - expect_eq(ses_vote_passed(), 1) + ses_vote_ended(session_st, 5, true) + expect_eq(ses_vote_passed(session_st), 1) } - 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()) + 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)) } } diff --git a/packages/ludic.session/vote_state.ludic b/packages/ludic.session/vote_state.ludic index 2c9c6b64..e2bbc240 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() -> 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 } +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 } diff --git a/packages/ludic.session/votes.ludic b/packages/ludic.session/votes.ludic index 85c67170..9ed35195 100644 --- a/packages/ludic.session/votes.ludic +++ b/packages/ludic.session/votes.ludic @@ -8,104 +8,95 @@ 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(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 } +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 } return SES_MAY_ASKED } # the host puts it to the party, voting yes itself -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) +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) f.yes = true - session__vote_check() + session__vote_check(base_st, session_st) return true } # this player's answer; a guest's goes to the host as 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) +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) f.yes = yes return } - ses_vote_heard(session__vote_id, yes) + ses_vote_heard(base_st, session_st, session_st.session__vote_id, yes) } # the host: a player's answer to vote `id` -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() +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) } # a guest: the host opened vote `id`, or ended it -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_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_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 } +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 } } # the vote's thirty seconds; on the host, running out decides it -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() } +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) } } -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_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_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) +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) f.yes = passed } -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 +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 return f } diff --git a/packages/ludic.session/where.ludic b/packages/ludic.session/where.ludic index 142782ee..d0d82a4c 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(p: int) -> int { - if p <= 0 or not ply_on(p) { return 0 } - return session__slot[p].pid +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_name(p: int) -> string { +export function ply_name(session_st: SessionState, p: int) -> string { if p == 0 { return SessionLocal.name() } - if not ply_on(p) { return "" } - return session__slot[p].name + if not ply_on(session_st, p) { return "" } + return session_st.session__slot[p].name } -export function ply_x(p: int) -> float { +export function ply_x(session_st: SessionState, p: int) -> float { if p == 0 { return SessionLocal.x() } - return session__slot[p].x + return session_st.session__slot[p].x } -export function ply_y(p: int) -> float { +export function ply_y(session_st: SessionState, p: int) -> float { if p == 0 { return SessionLocal.y() } - return session__slot[p].y + return session_st.session__slot[p].y } -export function ply_z(p: int) -> float { +export function ply_z(session_st: SessionState, p: int) -> float { if p == 0 { return SessionLocal.z() } - return session__slot[p].z + return session_st.session__slot[p].z } -export function ply_yaw(p: int) -> float { +export function ply_yaw(session_st: SessionState, p: int) -> float { if p == 0 { return SessionLocal.yaw() } - return session__slot[p].yaw + return session_st.session__slot[p].yaw } -export function ply_speed(p: int) -> float { +export function ply_speed(session_st: SessionState, p: int) -> float { if p == 0 { return SessionLocal.speed() } - return session__slot[p].speed + return session_st.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(p: int) -> float { +export function ply_rest(session_st: SessionState, p: int) -> float { if p == 0 { return SessionLocal.rest() } - return session__slot[p].rest + return session_st.session__slot[p].rest } -export function ply_sleeping(p: int) -> bool { - if p == 0 { return session__me_sleeping } - return ply_on(p) and session__slot[p].sleeping +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 } diff --git a/packages/ludic.settings/file.ludic b/packages/ludic.settings/file.ludic index 556de859..2fa98e7a 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() -> Val { - sg_init() +export function settings_to_json(settings_st: mut SettingsState) -> Val { + sg_init(settings_st) let v = Value.object() - for i in 0 .. len(sg_defs) { settings_put(v, i) } + for i in 0 .. len(settings_st.sg_defs) { settings_put(settings_st, 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(v: Val, id: int) -> void { - let d = sg_defs[id] +export function settings_put(settings_st: SettingsState, v: Val, id: int) -> void { + let d = settings_st.sg_defs[id] if not d.saved { return } if d.kind == SETTING_TEXT { - sv_put_str(v, d.key, sg_text[id]) + sv_put_str(v, d.key, settings_st.sg_text[id]) } else if d.kind == SETTING_FLOAT { - sv_put_float(v, d.key, sg_num[id]) + sv_put_float(v, d.key, settings_st.sg_num[id]) } else { - sv_put_int(v, d.key, int(sg_num[id])) + sv_put_int(v, d.key, int(settings_st.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(v: Val) -> void { - sg_init() +export function settings_from_json(settings_st: mut SettingsState, v: Val) -> void { + sg_init(settings_st) if v == null { return } - for i in 0 .. len(sg_defs) { sg_read(v, i) } + for i in 0 .. len(settings_st.sg_defs) { sg_read(settings_st, v, i) } } -function sg_read(v: Val, id: int) -> void { - let d = sg_defs[id] +function sg_read(settings_st: mut SettingsState, v: Val, id: int) -> void { + let d = settings_st.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 { - sg_text[id] = sv_str(v, d.key, sg_text[id]) + settings_st.sg_text[id] = sv_str(v, d.key, settings_st.sg_text[id]) } else if d.kind == SETTING_FLOAT { - sg_num[id] = settings_held(id, sv_float(v, d.key, sg_num[id])) + settings_st.sg_num[id] = settings_held(settings_st, id, sv_float(v, d.key, settings_st.sg_num[id])) } else { - sg_num[id] = settings_held(id, float(sv_int(v, d.key, int(sg_num[id])))) + settings_st.sg_num[id] = settings_held(settings_st, id, float(sv_int(v, d.key, int(settings_st.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(id: int, x: Val) -> void { - let d = sg_defs[id] +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] if d.kind == SETTING_TEXT { - if Value.kind(x) == 4 { settings_set_text(id, Value.as_str(x)) } + if Value.kind(x) == 4 { settings_set_text(base_st, settings_st, id, Value.as_str(x)) } } else if d.kind == SETTING_FLOAT { - settings_set_float(id, float(Value.as_int(x)) / 1000.0) + settings_set_float(base_st, settings_st, id, float(Value.as_int(x)) / 1000.0) } else { - settings_set_int(id, Value.as_int(x)) + settings_set_int(base_st, settings_st, id, Value.as_int(x)) } } diff --git a/packages/ludic.settings/rules.ludic b/packages/ludic.settings/rules.ludic index e5c42278..48102475 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() -> void { - sg_init() - for i in 0 .. len(sg_defs) { - sg_num[i] = sg_defs[i].num - sg_text[i] = sg_defs[i].text +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 } } # 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() -> void { - sg_init() - for i in 0 .. len(sg_defs) { sg_num[i] = settings_held(i, sg_num[i]) } +export function settings_clamp_all(settings_st: mut SettingsState) -> void { + sg_init(settings_st) + for i in 0 .. len(settings_st.sg_defs) { settings_st.sg_num[i] = settings_held(settings_st, i, settings_st.sg_num[i]) } } # the values every machine can run: each setting marked `rescue` back to its default, and the # rest (keys, language, volume) left the player's -export function settings_rescue() -> 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 +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 } } } # whether a setting is on the rescue list at its safe value -export function settings_is_safe(id: int) -> bool { - let d = sg_defs[id] +export function settings_is_safe(settings_st: SettingsState, id: int) -> bool { + let d = settings_st.sg_defs[id] if not d.rescue { return true } - return sg_num[id] == d.num and sg_text[id] == d.text + return settings_st.sg_num[id] == d.num and settings_st.sg_text[id] == d.text } diff --git a/packages/ludic.settings/store.ludic b/packages/ludic.settings/store.ludic index 7b2ef165..34c930bf 100644 --- a/packages/ludic.settings/store.ludic +++ b/packages/ludic.settings/store.ludic @@ -1,88 +1,90 @@ # 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 -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 +export state SettingsState { + sg_defs: []SettingDef = null + sg_num: []float = null + sg_text: []string = null + sg_changes: Queue = null } -export function settings_changes() -> Queue { - if sg_changes == null { sg_changes = queue_new("settings.changes") } - return sg_changes +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 } # forget every setting (a test, or a game that declares its table again) -export function settings_clear() -> void { - sg_defs = new []SettingDef - sg_num = new []float - sg_text = new []string - q_clear(settings_changes()) +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)) } # a setting, at its default; its id is its place in the order added -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_add(settings_st: mut SettingsState, d: SettingDef) -> int { + sg_init(settings_st) + push(settings_st.sg_defs, d) + push(settings_st.sg_num, d.num) + push(settings_st.sg_text, d.text) + return len(settings_st.sg_defs) - 1 } -export function settings_count() -> int { - sg_init() - return len(sg_defs) +export function settings_count(settings_st: mut SettingsState) -> int { + sg_init(settings_st) + return len(settings_st.sg_defs) } -export function settings_def(id: int) -> SettingDef { return sg_defs[id] } +export function settings_def(settings_st: SettingsState, id: int) -> SettingDef { return settings_st.sg_defs[id] } # the id of `key`, or -1 -export function settings_id_of(key: string) -> int { - sg_init() - for i in 0 .. len(sg_defs) { if sg_defs[i].key == key { return i } } +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 } } return -1 } -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] } +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] } -function sg_changed(id: int) -> void { +function sg_changed(base_st: mut BaseState, settings_st: mut SettingsState, id: int) -> void { let c = new SettingChanged c.id = id - c.key = sg_defs[id].key - q_push(settings_changes(), c) + c.key = settings_st.sg_defs[id].key + q_push(base_st, settings_changes(base_st, settings_st), c) } # a value outside the setting's range is its default: a damaged file never reaches the game -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_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_int(id: int, v: int) -> void { settings_set_float(id, float(v)) } +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_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_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_text(id: int, v: string) -> void { - if v == sg_text[id] { return } - sg_text[id] = v - sg_changed(id) +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) } # `v` held to the setting's range: outside it, the default -export function settings_held(id: int, v: float) -> float { - let d = sg_defs[id] +export function settings_held(settings_st: SettingsState, id: int, v: float) -> float { + let d = settings_st.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 c8df2d3c..20a47e67 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() -> void { - settings_clear() - for i in 0 .. TOY_COUNT { settings_add(Toy[i]) } + 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 facts() -> []SettingChanged { return q_drain(settings_changes()) } + function facts(base_st: mut BaseState, settings_st: mut SettingsState) -> []SettingChanged { return q_drain(base_st, settings_changes(base_st, settings_st)) } - 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 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 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() + 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) 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" { - 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 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 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 "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 "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) + 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) let v = Json.parse(text) Value.put(v, "shadow_q", Value.int(7)) Value.put(v, "stranger", Value.int(1)) - 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) + 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) } } diff --git a/packages/ludic.shop/demand.ludic b/packages/ludic.shop/demand.ludic index 7cf99bda..c3a98bd7 100644 --- a/packages/ludic.shop/demand.ludic +++ b/packages/ludic.shop/demand.ludic @@ -1,31 +1,22 @@ # 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(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_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_week() -> int { return (ShopWorld.day() - 1) / shop_week_days } +export function shop_week(shop_st: ShopState) -> int { return (ShopWorld.day() - 1) / shop_st.shop_week_days } -function shop_pick_one(v: int, taken: []int) -> int { +function shop_pick_one(shop_st: ShopState, v: int, taken: []int) -> int { for t in 0 .. 200 { - let it = rng_between(shop_dice, 0, shop_items() - 1) - if not shop_buys(v, it) { continue } + let it = rng_between(shop_st.shop_dice, 0, shop_items(shop_st) - 1) + if not shop_buys(shop_st, v, it) { continue } var dup = false for k in 0 .. len(taken) { if taken[k] == it { dup = true } } if not dup { return it } @@ -33,72 +24,72 @@ function shop_pick_one(v: int, taken: []int) -> int { return -1 } -function shop_roll_vendor(v: int) -> void { +function shop_roll_vendor(shop_st: mut ShopState, v: int) -> void { let taken = new []int - for k in 0 .. shop_wants_n { - let it = shop_pick_one(v, taken) - shop_want_list[v * shop_wants_n + k] = it + 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 if it >= 0 { push(taken, it) } } - shop_full_list[v] = shop_pick_one(v, taken) + shop_st.shop_full_list[v] = shop_pick_one(shop_st, v, taken) } # the week's lists, rolled again only when the week turns -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) } +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) } } -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 } } +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 } } return false } -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 +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 } # what a vendor wants this week, in its order -export function shop_wanted(v: int) -> []int { +export function shop_wanted(shop_st: mut ShopState, v: int) -> []int { let out = new []int - 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 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 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(v: int, it: int) -> float { - if shop_wants(v, it) { return shop_want_mult } - if shop_glutted(v, it) { return shop_glut_mult } +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 } return 1.0 } # this week, `v` wants `it` (a test, staging, a board that says so) -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 } +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 } } -function shop_demand_reset() -> void { - shop_week_rolled = -1 - shop_want_list = null - shop_full_list = null +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 } diff --git a/packages/ludic.shop/prices.ludic b/packages/ludic.shop/prices.ludic index cabac49d..10a6c5a6 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() -> 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_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_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) +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) 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(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) } +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) } return p } # what the vendor at the counter pays -export function shop_price_sell(it: int) -> int { return shop_price_sell_at(shop_at, it) } +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_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 +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 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(v: int, it: int) -> bool { - if not shop_vendor_ok(v) or it == shop_vendor_list[v].refuses { return false } +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 } if ShopWorld.also_buys(v, it) { return true } - let b = shop_vendor_list[v].buys + let b = shop_st.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 31122e1c..eb3f4441 100644 --- a/packages/ludic.shop/state.ludic +++ b/packages/ludic.shop/state.ludic @@ -31,55 +31,67 @@ export property ShopVendor { refuses: int = -1 } -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 +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 +} # 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(prices: []int, sell: []float) -> void { - shop_base = prices - shop_sell_frac = sell +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_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_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_clear_vendors() -> void { shop_vendor_list = new []ShopVendor } +export function shop_clear_vendors(shop_st: mut ShopState) -> void { shop_st.shop_vendor_list = new []ShopVendor } # a vendor; its number is the order added -export function shop_add_vendor(stock: []int, buys: []int, refuses: int) -> int { - if shop_vendor_list == null { shop_clear_vendors() } +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) } let v = new ShopVendor v.stock = stock v.buys = buys v.refuses = refuses - push(shop_vendor_list, v) - return len(shop_vendor_list) - 1 + push(shop_st.shop_vendor_list, v) + return len(shop_st.shop_vendor_list) - 1 } -export function shop_vendors() -> int { - if shop_vendor_list == null { return 0 } - return len(shop_vendor_list) +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_items() -> int { - if shop_base == null { return 0 } - return len(shop_base) +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_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 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 const SHOP_BOUGHT: int = 0 export const SHOP_SOLD: int = 1 @@ -93,9 +105,8 @@ export property ShopTrade { price: int = 0 } -var shop_trade_q: Queue = null -export function shop_trades() -> Queue { - if shop_trade_q == null { shop_trade_q = queue_new("shop.trades") } - return shop_trade_q +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 } diff --git a/packages/ludic.shop/system.ludic b/packages/ludic.shop/system.ludic index 04468ae4..b5e5103d 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() -> void { - shop_at = -1 - shop_demand_reset() - q_clear(shop_trades()) +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_system() -> System { diff --git a/packages/ludic.shop/tests/shop_test.ludic b/packages/ludic.shop/tests/shop_test.ludic index e5ebc600..8afe4679 100644 --- a/packages/ludic.shop/tests/shop_test.ludic +++ b/packages/ludic.shop/tests/shop_test.ludic @@ -19,34 +19,36 @@ program ShopTest { const MADE: float = 1.15 const BOUGHT: float = 0.55 - var rep: int = 0 - var day: int = 1 - var money: int = 0 - var pack: []int = null - var food_too: bool = false + state ShoptestState { + rep: int = 0 + day: int = 1 + money: int = 0 + pack: []int = null + food_too: bool = false + } - 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 + 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 return true } - 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 + 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 return true } - function fake_add(it: int, n: int) -> int { - pack[it] = pack[it] + n + function fake_add(shoptest_st: mut ShoptestState, it: int, n: int) -> int { + shoptest_st.pack[it] = shoptest_st.pack[it] + n return n } - function fake_room(it: int) -> int { - if it == JACKET { return 1 - pack[it] } + function fake_room(shoptest_st: ShoptestState, it: int) -> int { + if it == JACKET { return 1 - shoptest_st.pack[it] } return 99 } @@ -64,7 +66,7 @@ program ShopTest { } # the valley's own numbers: base prices and the sell classes - function fresh() -> void { + function fresh(base_st: mut BaseState, shop_st: mut ShopState, shoptest_st: mut ShoptestState) -> void { let prices = new []int let sell = new []float let p = ints(3, 6, 8, 9) @@ -83,104 +85,104 @@ program ShopTest { push(sell, BOUGHT) push(sell, RAW) push(sell, RAW) - 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() + 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) } - function trades() -> []ShopTrade { return q_drain(shop_trades()) } + function trades(base_st: mut BaseState, shop_st: mut ShopState) -> []ShopTrade { return q_drain(base_st, shop_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() + 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) 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" { - fresh() - money = 3 - expect(not shop_can_buy(JACKET)) - expect(not shop_buy(JACKET)) - expect_eq(money, 3) - expect_eq(len(trades()), 0) + 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 "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 "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 "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) + 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) expect_eq(len(s), 1) expect_eq(s[0], TROUT) - shop_visit(1) - expect_eq(shop_sell(BURNT, 1), 0) + shop_visit(shop_st, 1) + expect_eq(shop_sell(base_st, shop_st, BURNT, 1), 0) } - test "the week's demand is the shop's own dice, and turns with the week" { - fresh() - let a = shop_wanted(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) expect(len(a) > 0) - for k in 0 .. len(a) { expect(shop_buys(0, a[k])) } - let again = shop_wanted(0) + for k in 0 .. len(a) { expect(shop_buys(shop_st, 0, a[k])) } + let again = shop_wanted(shop_st, 0) expect_eq(again[0], a[0]) - shop_visit(0) + shop_visit(shop_st, 0) let it = a[0] - let plain = shop_price_sell_at(-1, it) - expect(shop_demand_mult(0, it) == 1.5) - expect(shop_price_sell(it) >= plain) + 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) var moved = false for w in 1 .. 12 { - day = 1 + w * 7 - let b = shop_wanted(0) + shoptest_st.day = 1 + w * 7 + let b = shop_wanted(shop_st, 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" { - fresh() - for v in 0 .. shop_vendors() { - let st = shop_stock(v) + 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) for k in 0 .. len(st) { for r in 0 .. 11 { - 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]) + 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]) expect(sell < buy) expect(sell >= 0) } @@ -188,49 +190,49 @@ program ShopTest { } } - 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 "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 "friction falls with standing and never inverts" { - fresh() + 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) var last_buy = 1000000 var last_sell = -1 for r in 0 .. 11 { - rep = r * 10 - expect(shop_buy_factor() > shop_sell_factor()) - let b = shop_price_buy(JACKET) - let s = shop_price_sell_at(-1, COOKED) + 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) expect(b <= last_buy) expect(s >= last_sell) last_buy = b last_sell = s } - rep = 0 - let b0 = shop_price_buy(JACKET) - rep = 100 - expect(shop_price_buy(JACKET) < b0) - rep = 0 + 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 expect(float(b0) / 60.0 <= 1.25) } - test "raw material keeps its order, and is not an income" { - fresh() + 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) for r in 0 .. 11 { - 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) + 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) expect(w <= ch and ch <= pf and pf <= tr) expect(pf <= 8 / 2) } - rep = 0 - expect(shop_price_sell_at(-1, WOOD) < shop_price_sell_at(-1, TROUT)) + shoptest_st.rep = 0 + expect(shop_price_sell_at(shop_st, -1, WOOD) < shop_price_sell_at(shop_st, -1, TROUT)) } } diff --git a/packages/ludic.shop/trade.ludic b/packages/ludic.shop/trade.ludic index a7d49626..8d50bb1d 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(v: int) -> void { - if shop_vendor_ok(v) { shop_at = v } else { shop_at = -1 } +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_away() -> void { shop_at = -1 } +export function shop_away(shop_st: mut ShopState) -> void { shop_st.shop_at = -1 } # the vendor at the counter, -1 none -export function shop_vendor() -> int { return shop_at } +export function shop_vendor(shop_st: ShopState) -> int { return shop_st.shop_at } -export function shop_at_counter() -> bool { return shop_at >= 0 } +export function shop_at_counter(shop_st: ShopState) -> bool { return shop_st.shop_at >= 0 } # what `v` has on its shelves, in shelf order -export function shop_stock(v: int) -> []int { +export function shop_stock(shop_st: ShopState, v: int) -> []int { let out = new []int - if not shop_vendor_ok(v) or shop_vendor_list[v].stock == null { return out } - let st = shop_vendor_list[v].stock + 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 for k in 0 .. len(st) { push(out, st[k]) } return out } # what in the pack `v` would buy -export function shop_sellable(v: int) -> []int { +export function shop_sellable(shop_st: ShopState, v: int) -> []int { let out = new []int - for it in 0 .. shop_items() { if ShopPack.count(it) > 0 and shop_buys(v, it) { push(out, it) } } + for it in 0 .. shop_items(shop_st) { if ShopPack.count(it) > 0 and shop_buys(shop_st, v, it) { push(out, it) } } return out } -function shop_trade_fact(what: int, it: int, n: int, price: int) -> void { +function shop_trade_fact(base_st: mut BaseState, shop_st: mut ShopState, what: int, it: int, n: int, price: int) -> void { let t = new ShopTrade t.what = what - t.vendor = shop_at + t.vendor = shop_st.shop_at t.item = it t.n = n t.price = price - q_push(shop_trades(), t) + q_push(base_st, shop_trades(base_st, shop_st), t) } -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 +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 } # one, paid for and in the pack; false and nothing spent when it cannot be -export function shop_buy(it: int) -> bool { - if not shop_can_buy(it) { return false } - let pr = shop_price_buy(it) +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) if not ShopPurse.spend(pr) { return false } ShopPack.add(it, 1) - shop_trade_fact(SHOP_BOUGHT, it, 1, pr) + shop_trade_fact(base_st, shop_st, 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(it: int, n: int) -> int { - if not shop_buys(shop_at, it) { return 0 } +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 } let k = Math.min(n, ShopPack.count(it)) if k <= 0 { return 0 } - let pr = shop_price_sell(it) + let pr = shop_price_sell(shop_st, it) if not ShopPack.take(it, k) { return 0 } ShopPurse.earn(pr * k) - shop_trade_fact(SHOP_SOLD, it, k, pr) + shop_trade_fact(base_st, shop_st, SHOP_SOLD, it, k, pr) return k } diff --git a/packages/ludic.steps/arc.ludic b/packages/ludic.steps/arc.ludic index df4517c7..a5789e2f 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() -> void { - steps__n = new []int - steps__kind = new []int - steps__param = new []int - steps__need = new []int - steps__cur = 0 - steps__done = 0 +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 } -function steps__ready() -> void { if steps__n == null { steps_arc_clear() } } +function steps__ready(steps_st: mut StepsState) -> void { if steps_st.steps__n == null { steps_arc_clear(steps_st) } } # open the next chapter; its number -export function steps_chapter_add() -> int { - steps__ready() - push(steps__n, 0) +export function steps_chapter_add(steps_st: mut StepsState) -> int { + steps__ready(steps_st) + push(steps_st.steps__n, 0) for i in 0 .. STEPS_PER_CHAPTER { - push(steps__kind, -1) - push(steps__param, 0) - push(steps__need, 0) + push(steps_st.steps__kind, -1) + push(steps_st.steps__param, 0) + push(steps_st.steps__need, 0) } - return len(steps__n) - 1 + return len(steps_st.steps__n) - 1 } # a step in the chapter just opened; false when there is no chapter or it is full -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 +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 return true } -export function steps_count() -> int { - if steps__n == null { return 0 } - return len(steps__n) +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_in(ch: int) -> int { - if ch < 0 or ch >= steps_count() { return 0 } - return steps__n[ch] +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] } # a step by its flat index, ch * STEPS_PER_CHAPTER + i (-1, 0, 0 past the table) -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_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_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_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_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_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_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) } +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) } # the chapter being played, and which of its steps are done (sticky) -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() } +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) } # the progress outright: a load, the host's word, a test -export function steps_set_current(ch: int) -> void { steps__cur = ch } -export function steps_set_done(mask: int) -> void { steps__done = mask } +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 } diff --git a/packages/ludic.steps/check.ludic b/packages/ludic.steps/check.ludic index bff9e432..831d82cc 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() -> void { - if steps_finished() { return } - let ch = steps__cur - let n = steps_in(ch) +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) var all = true for i in 0 .. n { - 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) 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) { all = false } + if not steps_done(steps_st, i) { all = false } } if not all { return } - steps__cur = ch + 1 - steps__done = 0 - steps__fact(STEPS_CHAPTER, ch, n) + steps_st.steps__cur = ch + 1 + steps_st.steps__done = 0 + steps__fact(base_st, steps_st, 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(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)) +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)) } # is an open step of the current chapter asking for this kind (and this param, unless < 0)? -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 } +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 } } return false } # back to no arc and no party (tests) -export function steps_reset() -> void { - steps_arc_clear() - steps__party_clear() - q_clear(steps_facts()) +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)) } diff --git a/packages/ludic.steps/each.ludic b/packages/ludic.steps/each.ludic index 6f1d00c5..98c58bc1 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() -> int { +export function steps_players(steps_st: StepsState) -> int { var n = 0 - for p in 0 .. steps__players { if StepsWorld.present(p) { n += 1 } } + for p in 0 .. steps_st.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(i: int) -> int { - steps__cols() +export function steps_each(steps_st: mut StepsState, i: int) -> int { + steps__cols(steps_st) var n = 0 - for p in 0 .. steps__players { - if StepsWorld.present(p) and steps__col[p * STEPS_PER_CHAPTER + i] != 0 { n += 1 } + 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 } } return n } # the seats that do not have it yet -export function steps_lacking(i: int) -> []int { - steps__cols() +export function steps_lacking(steps_st: mut StepsState, i: int) -> []int { + steps__cols(steps_st) let out = new []int - for p in 0 .. steps__players { - if StepsWorld.present(p) and steps__col[p * STEPS_PER_CHAPTER + i] == 0 { push(out, p) } + 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) } } return out } diff --git a/packages/ludic.steps/kinds.ludic b/packages/ludic.steps/kinds.ludic index f2db6590..a92f2a68 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(ks: []StepsKind) -> void { steps__kinds = ks } +export function steps_kinds(steps_st: mut StepsState, ks: []StepsKind) -> void { steps_st.steps__kinds = ks } -function steps__known(kind: int) -> bool { return steps__kinds != null and kind >= 0 and kind < len(steps__kinds) } +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) } -export function steps_shape(kind: int) -> int { - if not steps__known(kind) { return STEPS_SHAPE_YES } - return steps__kinds[kind].shape +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_pool(kind: int) -> int { - if not steps__known(kind) { return STEPS_POOL_WORLD } - return steps__kinds[kind].pool +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_shows(kind: int) -> bool { - if not steps__known(kind) { return false } - return steps__kinds[kind].shows +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 } 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(kind: int, param: int, need: int) -> bool { +export function steps_eval(steps_st: StepsState, kind: int, param: int, need: int) -> bool { if kind < 0 { return false } - return steps__met_by(kind, param, need, steps__value(kind, param)) + return steps__met_by(steps_st, kind, param, need, steps__value(kind, param)) } -function steps__met_by(kind: int, param: int, need: int, v: int) -> bool { - let sh = steps_shape(kind) +function steps__met_by(steps_st: StepsState, kind: int, param: int, need: int, v: int) -> bool { + let sh = steps_shape(steps_st, 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(kind: int, param: int, need: int, v: int) -> bool { } # how far a counted step has got, -1 for a kind a panel shows no count for -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)) +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)) } -function steps__shaped(kind: int, param: int, v: int) -> int { - let sh = steps_shape(kind) +function steps__shaped(steps_st: StepsState, kind: int, param: int, v: int) -> int { + let sh = steps_shape(steps_st, 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(kind: int, param: int, need: int) -> int { - if steps_shape(kind) == STEPS_SHAPE_MASK_ALL { return steps_popcount(param) } +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) } 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(kind: int, param: int, need: int) -> int { - let how = steps_pool(kind) +export function steps_mine(steps_st: StepsState, kind: int, param: int, need: int) -> int { + let how = steps_pool(steps_st, kind) if how == STEPS_POOL_WORLD { return 0 } if how == STEPS_POOL_ANY or how == STEPS_POOL_EACH { - if steps_eval(kind, param, need) { return 1 } + if steps_eval(steps_st, kind, param, need) { return 1 } return 0 } return steps__value(kind, param) @@ -67,22 +67,22 @@ export function steps_mine(kind: int, param: int, need: int) -> int { # 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(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) } +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) } 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(kind) == STEPS_SHAPE_MASK_ALL { return (v & param) == param } + if steps_shape(steps_st, 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(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) +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) } diff --git a/packages/ludic.steps/party.ludic b/packages/ludic.steps/party.ludic index 9c688577..d5bb5371 100644 --- a/packages/ludic.steps/party.ludic +++ b/packages/ludic.steps/party.ludic @@ -1,66 +1,63 @@ # 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(players: int) -> void { - steps__players = players - steps__col = null +export function steps_config(steps_st: mut StepsState, players: int) -> void { + steps_st.steps__players = players + steps_st.steps__col = null } -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 +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 } # a party over (or a new one): every column forgotten -export function steps_party_reset() -> void { steps__col = null } +export function steps_party_reset(steps_st: mut StepsState) -> void { steps_st.steps__col = null } # the columns, emptied when the chapter they were about is not this one; seat 0 refilled -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)) +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)) } } # this machine's shares for the current chapter, to send to whoever owns the story -export function steps_my_shares() -> []int { - steps__cols() +export function steps_my_shares(steps_st: mut StepsState) -> []int { + steps__cols(steps_st) let out = new []int - for i in 0 .. STEPS_PER_CHAPTER { push(out, steps__col[i]) } + for i in 0 .. STEPS_PER_CHAPTER { push(out, steps_st.steps__col[i]) } return out } # a teammate's shares, as they sent them; ignored when they were about another chapter -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(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_of(p: int, i: int) -> int { - steps__cols() - return steps__col[p * STEPS_PER_CHAPTER + 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] } # a seat emptied: what it had goes with it -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 } +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 } } # the shares of every seat taken, combined as the pool says (seat 0 is the start) -function steps__combined(i: int, how: int) -> int { - var v = steps__col[i] - for p in 1 .. steps__players { +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 { if not StepsWorld.present(p) { continue } - let c = steps__col[p * STEPS_PER_CHAPTER + i] + let c = steps_st.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 } } @@ -69,24 +66,24 @@ function steps__combined(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(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)) +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)) } # the party's count for step i of the current chapter, -1 for one that is not a tally -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)) +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)) } diff --git a/packages/ludic.steps/state.ludic b/packages/ludic.steps/state.ludic index 1d56b9cf..56eb76b4 100644 --- a/packages/ludic.steps/state.ludic +++ b/packages/ludic.steps/state.ludic @@ -45,26 +45,31 @@ export property StepsFact { step: int = 0 } -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 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 } -function steps__fact(what: int, ch: int, i: int) -> void { +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 { let f = new StepsFact f.what = what f.chapter = ch f.step = i - q_push(steps_facts(), f) + q_push(base_st, steps_facts(base_st, steps_st), 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 bd4655bc..5d4c9f32 100644 --- a/packages/ludic.steps/tests/steps_test.ludic +++ b/packages/ludic.steps/tests/steps_test.ludic @@ -4,16 +4,18 @@ import "ludic.steps" import "ludic.base" program StepsTest { numbers float - 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(kind: int, param: int, player: int) -> int { - if player != 0 or kind < 0 or kind >= len(vals) { return 0 } - return vals[kind] + state StepstestState { + vals: []int = null # value of kind k for this machine + party: bool = false + seats: int = 1 # seats 0 .. seats-1 are taken } - function fake_party() -> bool { return party } - function fake_present(p: int) -> bool { return p < seats } + + 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_party(stepstest_st: StepstestState) -> bool { return stepstest_st.party } + function fake_present(stepstest_st: StepstestState, p: int) -> bool { return p < stepstest_st.seats } bind StepsWorld { value: fn fake_value, party: fn fake_party, present: fn fake_present } @@ -33,13 +35,13 @@ program StepsTest { return k } - function fresh() -> void { - steps_reset() - steps_config(4) - party = false - seats = 1 - vals = new []int - for i in 0 .. 7 { push(vals, 0) } + 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) } let ks = new []StepsKind push(ks, kind(STEPS_SHAPE_COUNT, STEPS_POOL_SUM, true)) push(ks, kind(STEPS_SHAPE_MASK, STEPS_POOL_OR, true)) @@ -48,21 +50,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(ks) + steps_kinds(steps_st, ks) } # chapter 0: 5 wood, a fire; chapter 1: two signs, the compass - 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 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 facts_of(what: int) -> int { - let fs = q_drain(steps_facts()) + 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)) var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n @@ -75,150 +77,150 @@ program StepsTest { return s } - 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 "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 "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()) + 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)) 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" { - 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 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 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 "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 "the steps asked for right now" { - fresh() - two_chapters() - expect(steps_wants(K_FIRE, -1)) - expect(not steps_wants(K_SIGNS, -1)) + 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 "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 "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 "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 "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 "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) + 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) expect_eq(len(lack), 1) expect_eq(lack[0], 2) - steps_share(2, 1, shares(0, 1)) - expect(steps_met(1)) + steps_share(steps_st, 2, 1, shares(0, 1)) + expect(steps_met(steps_st, 1)) } - 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 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 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 "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 "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) + 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) } } diff --git a/packages/ludic.telemetry/ident.ludic b/packages/ludic.telemetry/ident.ludic index d5ae9164..ed936a49 100644 --- a/packages/ludic.telemetry/ident.ludic +++ b/packages/ludic.telemetry/ident.ludic @@ -1,63 +1,59 @@ # 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(n: int) -> string { - telemetry__rand_n += 1 - let seed = Crypto.random_hex(n) + "|" + `{Time.now()}|{Crypto.random_u32()}|{Os.pid()}|{telemetry__rand_n}` +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}` 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(id: string) -> void { - telemetry__id = id - let path = telemetry__conf().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 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(TELEMETRY_ID, 0, 0, 0) + telemetry__fact(base_st, telemetry_st, TELEMETRY_ID, 0, 0, 0) } -function telemetry__random_id() -> void { telemetry_set_id("r-" + telemetry_random_hex(16)) } +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__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__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__id_begin() -> void { - if len(telemetry__conf().id_salt) == 0 { - telemetry__random_id() +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) 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(Text.trim(m)) } else { telemetry__random_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) } return } - 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() } + 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) } } -function telemetry__spawn_machine(plat: string) -> int { +function telemetry__spawn_machine(telemetry_st: TelemetryState, 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__proc_out, "'", "''") + "'" + let out = "'" + Text.replace(telemetry_st.telemetry__proc_out, "'", "''") + "'" push(args, "-NoProfile") push(args, "-NonInteractive") push(args, "-Command") @@ -65,28 +61,28 @@ function telemetry__spawn_machine(plat: string) -> int { 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__proc_out + "'") + push(args, "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}' > '" + telemetry_st.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() -> void { - if telemetry__proc < 0 { return } - let code = Process.poll(telemetry__proc) +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) if code < 0 { - if Time.now() - telemetry__proc_t0 > 10 { - Process.kill(telemetry__proc) - Process.free(telemetry__proc) - telemetry__proc = -1 - telemetry__random_id() + 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) } return } - Process.free(telemetry__proc) - telemetry__proc = -1 + Process.free(telemetry_st.telemetry__proc) + telemetry_st.telemetry__proc = -1 var raw = "" - let s = Fs.read_text(telemetry__proc_out) + let s = Fs.read_text(telemetry_st.telemetry__proc_out) if s != null { raw = Text.trim(s) } - 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() } + 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) } } diff --git a/packages/ludic.telemetry/ports.ludic b/packages/ludic.telemetry/ports.ludic index 1f1f20d1..7e74c73c 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(c: TelemetryConfig) -> void { telemetry__cfg = c } +export function telemetry_config(telemetry_st: mut TelemetryState, c: TelemetryConfig) -> void { telemetry_st.telemetry__cfg = c } -function telemetry__conf() -> TelemetryConfig { - if telemetry__cfg == null { telemetry__cfg = new TelemetryConfig } - return telemetry__cfg +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__yes() -> bool { return true } -function telemetry__wall_ms() -> int { return (Time.now() - telemetry__t0) * 1000 } +function telemetry__wall_ms(telemetry_st: TelemetryState) -> int { return (Time.now() - telemetry_st.telemetry__t0) * 1000 } function telemetry__wall_stamp() -> string { return DateTime.format(Time.now(), "YYYY-MM-DDTHH:mm:ssZ") } -function telemetry__can_send() -> bool { - let c = telemetry__conf() +function telemetry__can_send(telemetry_st: mut TelemetryState) -> bool { + let c = telemetry__conf(telemetry_st) 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(body: string) -> int { return TelemetryTransport.send(telemetry__conf().host + telemetry__conf().path, body) } +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__poll(h: int) -> int { return TelemetryTransport.poll(h) } # a request abandoned (the player said no) -function telemetry__drop_request() -> void { - if telemetry__h < 0 { return } - TelemetryTransport.drop(telemetry__h) - telemetry__h = -1 +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 } diff --git a/packages/ludic.telemetry/queries.ludic b/packages/ludic.telemetry/queries.ludic index 55667da1..f3ba14c8 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() -> 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_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_queued() -> int { - if telemetry__q == null { return 0 } - return len(telemetry__q) +export function telemetry_queued(telemetry_st: TelemetryState) -> int { + if telemetry_st.telemetry__q == null { return 0 } + return len(telemetry_st.telemetry__q) } # a queued line as it will be sent, player mark and all -export function telemetry_line(i: int) -> string { return telemetry__q[i] } +export function telemetry_line(telemetry_st: TelemetryState, i: int) -> string { return telemetry_st.telemetry__q[i] } # back to before telemetry_start, ports and config kept (tests; a start after a failed boot) -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()) +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)) } diff --git a/packages/ludic.telemetry/queue.ludic b/packages/ludic.telemetry/queue.ludic index c92408c6..55191f74 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() -> 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() } +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) } } # may an event be kept right now? -export function telemetry_on() -> bool { return telemetry__ready and telemetry__enabled() and telemetry__can_send() } +export function telemetry_on(telemetry_st: mut TelemetryState) -> bool { return telemetry_st.telemetry__ready and telemetry__enabled() and telemetry__can_send(telemetry_st) } # 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(name: string, props: Val) -> void { - if not telemetry_on() { return } +export function telemetry_event(telemetry_st: mut TelemetryState, name: string, props: Val) -> void { + if not telemetry_on(telemetry_st) { return } var p = props if p == null { p = Value.object() } - Value.put(p, "$session_id", Value.str(telemetry__session)) - if len(telemetry__conf().lib) > 0 { Value.put(p, "$lib", Value.str(telemetry__conf().lib)) } + 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)) } 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__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) } + 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) } } # the queue on disk now, if it changed (a quit, a crash about to happen, a child starting) -export function telemetry_save() -> void { - if not telemetry__dirty or telemetry__q == null { return } - telemetry__dirty = false - let f = telemetry__conf().queue_file +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 if len(f) == 0 { return } - if len(telemetry__q) == 0 { + if len(telemetry_st.telemetry__q) == 0 { if Fs.exists(f) { Fs.remove(f) } return } - Fs.write_text(f, Text.join(telemetry__q, "\n") + "\n") + Fs.write_text(f, Text.join(telemetry_st.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() -> void { - telemetry__drop_request() - telemetry__q = new []string - telemetry__dirty = false - let f = telemetry__conf().queue_file +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 if len(f) > 0 and Fs.exists(f) { Fs.remove(f) } } -function telemetry__queue_load() -> void { - telemetry__q = new []string - let f = telemetry__conf().queue_file +function telemetry__queue_load(telemetry_st: mut TelemetryState) -> void { + telemetry_st.telemetry__q = new []string + let f = telemetry__conf(telemetry_st).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__q, parts[i]) } } - let max = telemetry__conf().queue_max - if len(telemetry__q) > max { telemetry__queue_drop(len(telemetry__q) - max) } + 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) } } # drop the first n lines (sent, or the oldest past the cap) -function telemetry__queue_drop(n: int) -> void { +function telemetry__queue_drop(telemetry_st: mut TelemetryState, n: int) -> void { let rest = new []string - for i in n .. len(telemetry__q) { push(rest, telemetry__q[i]) } - telemetry__q = rest - telemetry__dirty = true + 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 } # the batch: the first n lines joined, with the player id put in - no parsing -export function telemetry_batch_body(n: int) -> string { +export function telemetry_batch_body(telemetry_st: mut TelemetryState, n: int) -> string { let part = new []string - for i in 0 .. n { push(part, telemetry__q[i]) } + for i in 0 .. n { push(part, telemetry_st.telemetry__q[i]) } let head = Value.object() - Value.put(head, "api_key", Value.str(telemetry__conf().key)) + Value.put(head, "api_key", Value.str(telemetry__conf(telemetry_st).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__id) + return Text.replace(body, TELEMETRY_MARK, telemetry_st.telemetry__id) } function telemetry__read_obj(path: string) -> Val { diff --git a/packages/ludic.telemetry/send.ludic b/packages/ludic.telemetry/send.ludic index 777f20f0..18e4717f 100644 --- a/packages/ludic.telemetry/send.ludic +++ b/packages/ludic.telemetry/send.ludic @@ -1,70 +1,69 @@ # 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() -> void { - if not telemetry__ready { return } - telemetry__id_tick() +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) if not telemetry__enabled() { - if len(telemetry__q) > 0 or telemetry__h >= 0 { telemetry_forget() } + if len(telemetry_st.telemetry__q) > 0 or telemetry_st.telemetry__h >= 0 { telemetry_forget(telemetry_st) } return } - if not telemetry__can_send() { return } + if not telemetry__can_send(telemetry_st) { return } let now = telemetry__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 + 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 } - if now - telemetry__saved_ms >= c.save_ms { - telemetry_save() - 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 telemetry__h >= 0 { - telemetry__answer(now) + if telemetry_st.telemetry__h >= 0 { + telemetry__answer(base_st, telemetry_st, now) return } - 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 } + 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 } } } -function telemetry__answer(now: int) -> void { - let st = telemetry__poll(telemetry__h) +function telemetry__answer(base_st: mut BaseState, telemetry_st: mut TelemetryState, now: int) -> void { + let st = telemetry__poll(telemetry_st.telemetry__h) if st < 0 { return } - telemetry__h = -1 + telemetry_st.telemetry__h = -1 if 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) + 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) } else { - telemetry__backoff(now) - telemetry__fact(TELEMETRY_FAILED, telemetry__sent_n, st, telemetry__next_ms - now) + 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_save() + telemetry_save(telemetry_st) } -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)) +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)) if h < 0 { - telemetry__backoff(now) - telemetry__fact(TELEMETRY_FAILED, n, 0, telemetry__next_ms - now) + telemetry__backoff(telemetry_st, now) + telemetry__fact(base_st, telemetry_st, TELEMETRY_FAILED, n, 0, telemetry_st.telemetry__next_ms - now) return } - telemetry__h = h - telemetry__sent_n = n + telemetry_st.telemetry__h = h + telemetry_st.telemetry__sent_n = n } # a failed send: wait twice as long next time, up to backoff_max doublings -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) +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) } diff --git a/packages/ludic.telemetry/state.ludic b/packages/ludic.telemetry/state.ludic index 5310bce2..5aafa9cc 100644 --- a/packages/ludic.telemetry/state.ludic +++ b/packages/ludic.telemetry/state.ludic @@ -46,30 +46,37 @@ export property TelemetryFact { const TELEMETRY_MARK: string = "@@TELEMETRY_PLAYER@@" -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 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 } -function telemetry__fact(what: int, count: int, status: int, retry: int) -> void { +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 { let f = new TelemetryFact f.what = what f.count = count f.status = status f.retry_ms = retry - q_push(telemetry_facts(), f) + q_push(base_st, telemetry_facts(base_st, telemetry_st), f) } diff --git a/packages/ludic.telemetry/tests/telemetry_test.ludic b/packages/ludic.telemetry/tests/telemetry_test.ludic index 20084f62..071d28f7 100644 --- a/packages/ludic.telemetry/tests/telemetry_test.ludic +++ b/packages/ludic.telemetry/tests/telemetry_test.ludic @@ -4,27 +4,29 @@ import "ludic.telemetry" import "ludic.base" program TelemetryTest { numbers float - 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 = "" + 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 = "" + } - function fake_now() -> int { return clock } - function fake_enabled() -> bool { return on } - function fake_can() -> bool { return allowed } + 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_stamp() -> string { return "2026-09-25T12:00:00Z" } - function fake_send(url: string, body: string) -> int { - if refuse { return -1 } - sends += 1 - last_url = url - last_body = body + 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 return 7 } - function fake_poll(h: int) -> int { return answer } + function fake_poll(telemetrytest_st: TelemetrytestState, h: int) -> int { return telemetrytest_st.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 } @@ -33,16 +35,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() -> void { - clock = 0 - on = true - allowed = true - answer = -1 - refuse = false - sends = 0 - last_url = "" - last_body = "" - telemetry_reset() + 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) if Fs.exists(qfile()) { Fs.remove(qfile()) } Fs.write_text(idfile(), "{\"id\":\"p-1\",\"notices\":2}") let c = new TelemetryConfig @@ -57,144 +59,144 @@ program TelemetryTest { c.queue_max = 8 c.backoff_max = 2 c.save_ms = 100000 - telemetry_config(c) - telemetry_start() - telemetry_tick() + telemetry_config(telemetry_st, c) + telemetry_start(base_st, telemetry_st) + telemetry_tick(base_st, telemetry_st) } - function events(n: int) -> void { - for i in 0 .. n { telemetry_event(`e{i}`, null) } + function events(telemetry_st: mut TelemetryState, n: int) -> void { + for i in 0 .. n { telemetry_event(telemetry_st, `e{i}`, null) } } - function at(ms: int) -> void { - clock = ms - telemetry_tick() + 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 facts_of(what: int) -> int { - let fs = q_drain(telemetry_facts()) + 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)) 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" { - fresh() - expect(telemetry_id() == "p-1") - telemetry_event("hello", null) - expect_eq(telemetry_queued(), 1) - let line = telemetry_line(0) + 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) expect(Text.contains(line, "\"event\":\"hello\"")) expect(Text.contains(line, "test-game")) expect(not Text.contains(line, "p-1")) - let body = telemetry_batch_body(1) + let body = telemetry_batch_body(telemetry_st, 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" { - 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 "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 "enough events go early" { - fresh() - events(5) - at(10) - expect_eq(sends, 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 "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 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 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 "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 "off keeps nothing and deletes the file" { - fresh() - events(3) - telemetry_save() + 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) expect(Fs.exists(qfile())) - on = false - at(10) - expect_eq(telemetry_queued(), 0) + telemetrytest_st.on = false + at(base_st, telemetry_st, telemetrytest_st, 10) + expect_eq(telemetry_queued(telemetry_st), 0) expect(not Fs.exists(qfile())) - events(3) - expect_eq(telemetry_queued(), 0) + events(telemetry_st, 3) + expect_eq(telemetry_queued(telemetry_st), 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 "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 "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 "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 "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 "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 "an id set outright is written beside the file's other keys" { - fresh() - telemetry_set_id("p-2") + 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") 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" { - fresh() - telemetry_reset() + 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) Fs.remove(idfile()) - telemetry_start() - expect(Text.starts_with(telemetry_id(), "r-")) - expect_eq(len(telemetry_session()), 32) + telemetry_start(base_st, telemetry_st) + expect(Text.starts_with(telemetry_id(telemetry_st), "r-")) + expect_eq(len(telemetry_session(telemetry_st)), 32) } } diff --git a/packages/ludic.things/dispatch.ludic b/packages/ludic.things/dispatch.ludic index ebc9bd74..5086144c 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(t: Thing) -> void { +export function thing_use(base_st: mut BaseState, things_st: mut ThingsState, t: Thing) -> void { if t == null or not thing_kind_ok(t.kind) { return } let k = ThingKinds[t.kind] - th_fact(THING_USED, t) + th_fact(base_st, things_st, 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(hours: float) -> void { - let all = things_all() +export function things_tick(things_st: mut ThingsState, hours: float) -> void { + let all = things_all(things_st) 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(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() -> void { - let all = things_all() +export function things_restock(things_st: mut ThingsState) -> void { + let all = things_all(things_st) for i in 0 .. len(all) { let t = all[i] if thing_is_put_down(t) { continue } diff --git a/packages/ludic.things/queries.ludic b/packages/ludic.things/queries.ludic index 22d17e0a..a38a6d86 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(kind: int, id: int) -> Thing { - let all = things_all() +export function thing_find(things_st: mut ThingsState, kind: int, id: int) -> Thing { + let all = things_all(things_st) 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(kind: int, id: int) -> Thing { return null } -export function thing_by_uid(uid: int) -> Thing { - let all = things_all() +export function thing_by_uid(things_st: mut ThingsState, uid: int) -> Thing { + let all = things_all(things_st) 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(kind: int, x: float, z: float) -> Thing { +export function thing_nearest(things_st: mut ThingsState, kind: int, x: float, z: float) -> Thing { var best: Thing = null var bd = 0.0 - let all = things_all() + let all = things_all(things_st) 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(x: float, z: float, r: float) -> []Thing { +export function things_within(things_st: mut ThingsState, x: float, z: float, r: float) -> []Thing { let out = new []Thing - let all = things_all() + let all = things_all(things_st) 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(kind: int) -> []Thing { +export function things_each(things_st: mut ThingsState, kind: int) -> []Thing { let out = new []Thing - let all = things_all() + let all = things_all(things_st) for i in 0 .. len(all) { if all[i].active and th_of(all[i], kind) { push(out, all[i]) } } return out } # every one of a kind, active or not -export function things_each_all(kind: int) -> []Thing { +export function things_each_all(things_st: mut ThingsState, kind: int) -> []Thing { let out = new []Thing - let all = things_all() + let all = things_all(things_st) 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 4c5351ad..171d9a58 100644 --- a/packages/ludic.things/store.ludic +++ b/packages/ludic.things/store.ludic @@ -1,15 +1,18 @@ # 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 -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 state ThingsState { + th_list: []Thing = null + th_uid: int = 0 + th_facts: Queue = null + th_marked: int = -1 } -function th_fact(what: int, t: Thing) -> void { +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 { let f = new ThingFact f.what = what f.uid = t.uid @@ -18,28 +21,28 @@ function th_fact(what: int, t: Thing) -> void { f.owner = t.owner f.x = t.x f.z = t.z - q_push(things_facts(), f) + q_push(base_st, things_facts(base_st, things_st), f) } # whether a world has been set out (things_clear) since the program began -export function things_ready() -> bool { return th_list != null } +export function things_ready(things_st: ThingsState) -> bool { return things_st.th_list != null } # every Thing, in the order placed; the list is the package's, never to be pushed to -export function things_all() -> []Thing { - if th_list == null { th_list = new []Thing } - return th_list +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_count() -> int { return len(things_all()) } +export function things_count(things_st: mut ThingsState) -> int { return len(things_all(things_st)) } # a new world: every Thing forgotten, quietly (the game has already dropped its drawings) -export function things_clear() -> void { th_list = new []Thing } +export function things_clear(things_st: mut ThingsState) -> void { things_st.th_list = new []Thing } -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) +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) } -function th_place(t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing { +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 { t.kind = kind t.look = look t.x = x @@ -47,46 +50,45 @@ function th_place(t: Thing, kind: int, look: string, x: float, y: float, z: floa t.z = z t.yaw = yaw t.id = id - th_uid += 1 - t.uid = th_uid - push(things_all(), t) + things_st.th_uid += 1 + t.uid = things_st.th_uid + push(things_all(things_st), t) ThingsWorld.placed(t) - th_fact(THING_PLACED, t) + th_fact(base_st, things_st, 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(kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing { +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 { let t = new Thing t.reach = reach - return th_place(t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id) + return th_place(base_st, things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id) } # gone for good -export function thing_remove(t: Thing) -> void { +export function thing_remove(base_st: mut BaseState, things_st: mut ThingsState, t: Thing) -> void { if t == null { return } - th_marked = t.uid - things_drop(fn th_is_marked) - th_marked = -1 + things_st.th_marked = t.uid + things_drop(base_st, things_st, fn th_is_marked) + things_st.th_marked = -1 } -var th_marked: int = -1 -function th_is_marked(t: Thing) -> bool { return t.uid == th_marked } +function th_is_marked(things_st: ThingsState, t: Thing) -> bool { return t.uid == things_st.th_marked } # every Thing `gone` says so is removed for good -export function things_drop(gone: fn(Thing) -> bool) -> void { +export function things_drop(base_st: mut BaseState, things_st: mut ThingsState, gone: fn(Thing) -> bool) -> void { let keep = new []Thing - let all = things_all() + let all = things_all(things_st) for i in 0 .. len(all) { let t = all[i] if gone(t) { ThingsWorld.removed(t) - th_fact(THING_REMOVED, t) + th_fact(base_st, things_st, THING_REMOVED, t) } else { push(keep, t) } } - th_list = keep + things_st.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 290ab3ca..742c4447 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() -> Val { +export function things_save(things_st: mut ThingsState) -> Val { let l = Value.list() - let all = things_all() + let all = things_all(things_st) 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() -> void { things_drop(fn th_kept) } +export function things_reset(base_st: mut BaseState, things_st: mut ThingsState) -> void { things_drop(base_st, things_st, 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(v: Val, version: int) -> void { - things_reset() +export function things_load(base_st: mut BaseState, things_st: mut ThingsState, v: Val, version: int) -> void { + things_reset(base_st, things_st) 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(Value.at(l, i)) } + for i in 0 .. Value.count(l) { th_from_json(base_st, things_st, Value.at(l, i)) } } -function th_from_json(o: Val) -> void { +function th_from_json(base_st: mut BaseState, things_st: mut ThingsState, 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(o: Val) -> void { 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(t, k, sv_str(o, "look", ""), x, y, z, sv_float(o, "yaw", 0.0), sv_int(o, "id", 0)) + 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)) ThingsWorld.restored(t) } diff --git a/packages/ludic.things/tests/things_test.ludic b/packages/ludic.things/tests/things_test.ludic index 65616462..a051057b 100644 --- a/packages/ludic.things/tests/things_test.ludic +++ b/packages/ludic.things/tests/things_test.ludic @@ -4,11 +4,13 @@ import "ludic.things" import "ludic.base" program ThingsTest { numbers float - var drawn: int = 0 - var gone: int = 0 - var shows: int = 0 - var back: int = 0 - var wood: int = 0 + state ThingstestState { + drawn: int = 0 + gone: int = 0 + shows: int = 0 + back: int = 0 + wood: int = 0 + } function lit_prompt(t: Thing) -> string { if t.used != 0 { return "Add wood" } @@ -19,8 +21,8 @@ program ThingsTest { if t.owner == 0 { return "Your tent" } return "" } - function branch_use(t: Thing) -> void { - wood += 1 + function branch_use(thingstest_st: mut ThingstestState, t: Thing) -> void { + thingstest_st.wood += 1 thing_hide(t) } function lure_tick(t: Thing, hours: float) -> void { @@ -36,121 +38,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(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 } + 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 } bind ThingsWorld { ground: fn hill, placed: fn on_placed, removed: fn on_removed, shown: fn on_shown, restored: fn on_restored } - 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()) } + 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)) } - 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) + 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) expect(a.uid != b.uid) - expect_eq(things_count(), 2) - let fs = facts() + expect_eq(things_count(things_st), 2) + let fs = facts(base_st, things_st) expect_eq(len(fs), 2) expect_eq(fs[1].what, THING_PLACED) expect_eq(fs[1].kind, TH_FIRE) - expect(thing_by_uid(b.uid) == b) + expect(thing_by_uid(things_st, b.uid) == b) } - 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) + 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) thing_hide(near) - 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) + 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) } - 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) + 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) thing_move(b, 4.0, 0.0, 1.0) expect_near(b.y, 2.0, 0.001) - let c = thing_put(TH_BRANCH, "stick", 2.0, 0.0, 0.0, 0.25, 1.9, 7) + let c = thing_put(base_st, things_st, 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" { - things_clear() - let t = put(TH_TENT, 0.0, 0.0) + 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) expect(thing_name(t) == "A tent") t.owner = 0 expect(thing_name(t) == "Your tent") - let f = put(TH_FIRE, 0.0, 0.0) + let f = put(base_st, things_st, TH_FIRE, 0.0, 0.0) expect(thing_prompt(f) == "Light it") - thing_use(f) + thing_use(base_st, things_st, 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(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_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_eq(fs[len(fs) - 3].what, THING_USED) } - test "ticks and a restock" { - things_clear() - let l = put(TH_LURE, 0.0, 0.0) + 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) l.timer = 1.0 - 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) + 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) expect(l.active) - things_tick(0.6) + things_tick(things_st, 0.6) expect(not l.active) - things_restock() + things_restock(things_st) 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" { - things_clear() - let l = put(TH_LURE, 3.0, 4.0) + 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) l.owner = 0 l.timer = 2.5 - put(TH_TENT, 0.0, 0.0) + put(base_st, things_st, TH_TENT, 0.0, 0.0) let root = save_tree() - save_section(root, "things", 1, things_save()) + save_section(root, "things", 1, things_save(things_st)) let text = save_encode(root) - things_clear() - put(TH_LURE, 9.0, 9.0) + things_clear(things_st) + put(base_st, things_st, TH_LURE, 9.0, 9.0) let n = load_section(save_decode(text), "things") - things_load(n.data, n.version) - expect_eq(things_count(), 1) - let t = thing_find(TH_LURE, -1) + 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) expect_near(t.x, 3.0, 0.001) expect_near(t.timer, 2.5, 0.001) expect_eq(t.owner, 0) - expect_eq(back, 1) + expect_eq(thingstest_st.back, 1) } } diff --git a/packages/ludic.tracks/config.ludic b/packages/ludic.tracks/config.ludic index 7b4c3df4..4c850ccf 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(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) } +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) } for k in 0 .. kinds { - push(tk_count, 0) - push(tk_next, 0) + push(tracks_st.tk_count, 0) + push(tracks_st.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(name_at: int, fresh: float, recent: float) -> void { - tk_name_at = name_at - tk_fresh_h = fresh - tk_recent_h = recent +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 } function tk_now() -> float { return TracksWorld.now() } -function tk_named() -> bool { - return TracksWorld.skill() >= tk_name_at +function tk_named(tracks_st: TracksState) -> bool { + return TracksWorld.skill() >= tracks_st.tk_name_at } function tk_bearing(yaw: float) -> string { diff --git a/packages/ludic.tracks/leave.ludic b/packages/ludic.tracks/leave.ludic index f24fe7a4..d7b95414 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(kind: int) -> bool { return tk_ring != null and kind >= 0 and kind < tk_kinds } +function tk_ok(tracks_st: TracksState, kind: int) -> bool { return tracks_st.tk_ring != null and kind >= 0 and kind < tracks_st.tk_kinds } # a print made at `made` game hours (another machine's, arriving here); the ring index, or -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 +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 } - tk_next[kind] = (i + 1) % tk_cap - let t = tk_ring[kind * tk_cap + i] + tracks_st.tk_next[kind] = (i + 1) % tracks_st.tk_cap + let t = tracks_st.tk_ring[kind * tracks_st.tk_cap + i] t.x = x t.z = z t.yaw = yaw @@ -23,15 +23,15 @@ export function tracks_leave_at(kind: int, x: float, z: float, yaw: float, sp: i return i } -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()) +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()) } # the ground forgets every print (a new world) -export function tracks_clear() -> void { - if tk_ring == null { return } - for k in 0 .. tk_kinds { - tk_count[k] = 0 - tk_next[k] = 0 +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 } } diff --git a/packages/ludic.tracks/queries.ludic b/packages/ludic.tracks/queries.ludic index 8749e468..cbe91287 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() -> int { return tk_kinds } -export function tracks_cap() -> int { return tk_cap } +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_count(kind: int) -> int { - if not tk_ok(kind) { return 0 } - return tk_count[kind] +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_slot(kind: int, i: int) -> int { return kind * tk_cap + i } +export function tracks_slot(tracks_st: TracksState, kind: int, i: int) -> int { return kind * tracks_st.tk_cap + i } -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 } +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 } # game hours since it was made; never below 0 -export function tracks_age(slot: int) -> float { return Math.max(0.0, tk_now() - tk_ring[slot].made) } +export function tracks_age(tracks_st: TracksState, slot: int) -> float { return Math.max(0.0, tk_now() - tracks_st.tk_ring[slot].made) } # the nearest print within `r` of (x, z), as a slot; -1 when there is none -export function tracks_nearest(x: float, z: float, r: float) -> int { +export function tracks_nearest(tracks_st: TracksState, x: float, z: float, r: float) -> int { var best = r * r var at = -1 - for k in 0 .. tk_kinds { - for i in 0 .. tk_count[k] { - let t = tk_ring[k * tk_cap + i] + 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] let dx = t.x - x let dz = t.z - z let d2 = dx * dx + dz * dz if d2 < best { best = d2 - at = k * tk_cap + i + at = k * tracks_st.tk_cap + i } } } @@ -40,8 +40,8 @@ export function tracks_nearest(x: float, z: float, r: float) -> int { } # signs read for the first time, of every source -export function tracks_read_count() -> int { return tk_read_n } +export function tracks_read_count(tracks_st: TracksState) -> int { return tracks_st.tk_read_n } # the heading of the last sign read, and whether there has been one -export function tracks_last_yaw() -> float { return tk_yaw } -export function tracks_has_bearing() -> bool { return tk_has_yaw } +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 } diff --git a/packages/ludic.tracks/read.ludic b/packages/ludic.tracks/read.ludic index 645f413d..6fa4326e 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(age: float) -> int { - if age > tk_recent_h { return TRACK_OLD } - if age > tk_fresh_h { return TRACK_RECENT } +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 } return TRACK_FRESH } -function tk_push(f: TrackRead) -> void { - if f.first { tk_read_n += 1 } - q_push(tracks_reads(), f) +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) } # read the print at a slot (tracks_nearest's answer): a fact every time, a count the first time -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] +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] let f = new TrackRead f.sp = t.sp f.source = TRACK_PRINT - f.kind = slot / tk_cap + f.kind = slot / tracks_st.tk_cap f.legend = t.legend - f.age = tracks_age(slot) - f.fresh = tracks_fresh_of(f.age) + f.age = tracks_age(tracks_st, slot) + f.fresh = tracks_fresh_of(tracks_st, f.age) f.yaw = t.yaw - f.named = tk_named() + f.named = tk_named(tracks_st) f.bearing = tk_bearing(t.yaw) f.first = not t.read t.read = true - tracks_note_bearing(t.yaw) - tk_push(f) + tracks_note_bearing(tracks_st, t.yaw) + tk_push(base_st, tracks_st, 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(sp: int, source: int, legend: bool, yaw: float, first: bool) -> void { +export function tracks_note(base_st: mut BaseState, tracks_st: mut TracksState, 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() + f.named = tk_named(tracks_st) f.bearing = tk_bearing(yaw) f.first = first - tk_push(f) + tk_push(base_st, tracks_st, f) } # the way the last thing read went (a bed, droppings, a print) -export function tracks_note_bearing(yaw: float) -> void { - tk_yaw = yaw - tk_has_yaw = true +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_set_read_count(n: int) -> void { tk_read_n = n } +export function tracks_set_read_count(tracks_st: mut TracksState, n: int) -> void { tracks_st.tk_read_n = n } diff --git a/packages/ludic.tracks/state.ludic b/packages/ludic.tracks/state.ludic index 01c71eda..3a000d32 100644 --- a/packages/ludic.tracks/state.ludic +++ b/packages/ludic.tracks/state.ludic @@ -47,22 +47,24 @@ export property TrackRead { first: bool = false } -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 +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 +} # the facts: every sign read, oldest first -export function tracks_reads() -> Queue { - if tk_reads == null { tk_reads = queue_new("tracks.reads") } - return tk_reads +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 } diff --git a/packages/ludic.tracks/system.ludic b/packages/ludic.tracks/system.ludic index 2ce6f91f..e672824f 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() -> void { - tk_read_n = 0 - tk_yaw = 0.0 - tk_has_yaw = false - q_clear(tracks_reads()) +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_save() -> Val { +export function tracks_save(tracks_st: TracksState) -> Val { let v = Value.object() - sv_put_int(v, "read", tk_read_n) - sv_put_bool(v, "has_yaw", tk_has_yaw) - sv_put_float(v, "yaw", tk_yaw) + 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) return v } -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_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_system() -> System { diff --git a/packages/ludic.tracks/tests/tracks_test.ludic b/packages/ludic.tracks/tests/tracks_test.ludic index 1521b519..b027ef78 100644 --- a/packages/ludic.tracks/tests/tracks_test.ludic +++ b/packages/ludic.tracks/tests/tracks_test.ludic @@ -5,138 +5,140 @@ import "ludic.tracks" import "ludic.base" program TracksTest { numbers float - var now: float = 0.0 - var skill: int = 1 - var placed_n: int = 0 - var placed_last: int = -1 + state TrackstestState { + now: float = 0.0 + skill: int = 1 + placed_n: int = 0 + placed_last: int = -1 + } - function fake_now() -> float { return now } - function fake_skill() -> int { return skill } + function fake_now(trackstest_st: TrackstestState) -> float { return trackstest_st.now } + function fake_skill(trackstest_st: TrackstestState) -> int { return trackstest_st.skill } function fake_bearing(yaw: float) -> string { if yaw > 0.0 { return "east" } return "west" } - function fake_placed(kind: int, i: int) -> void { - placed_n += 1 - placed_last = i + function fake_placed(trackstest_st: mut TrackstestState, kind: int, i: int) -> void { + trackstest_st.placed_n += 1 + trackstest_st.placed_last = i } bind TracksWorld { now: fn fake_now, skill: fn fake_skill, bearing: fn fake_bearing, placed: fn fake_placed } - 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 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 reads() -> []TrackRead { return q_drain(tracks_reads()) } + function reads(base_st: mut BaseState, tracks_st: mut TracksState) -> []TrackRead { return q_drain(base_st, tracks_reads(base_st, tracks_st)) } - 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) + 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) expect_eq(i, 0) - 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) + 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) } - 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 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 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 "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 "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 "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 "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() + 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) 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(), 1) - expect(tracks_has_bearing() and tracks_last_yaw() == 1.0) + expect_eq(tracks_read_count(tracks_st), 1) + expect(tracks_has_bearing(tracks_st) and tracks_last_yaw(tracks_st) == 1.0) } - 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 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 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() + 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) expect_eq(len(rs), 2) expect_eq(rs[0].source, TRACK_SCAT) - expect_eq(tracks_read_count(), 1) + expect_eq(tracks_read_count(tracks_st), 1) } - 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 "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 "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 "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 "a save section reads back the same reader" { - fresh() - tracks_note(1, TRACK_SIGN, true, 0.0, true) - tracks_note_bearing(2.5) + 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) let root = save_tree() - save_section(root, "tracks", 1, tracks_save()) + save_section(root, "tracks", 1, tracks_save(tracks_st)) let text = save_encode(root) - tracks_reset() + tracks_reset(base_st, tracks_st) let n = load_section(save_decode(text), "tracks") - tracks_load(n.data, n.version) - expect_eq(tracks_read_count(), 1) - expect(tracks_has_bearing() and tracks_last_yaw() == 2.5) + 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) } } diff --git a/packages/ludic.update/chain.ludic b/packages/ludic.update/chain.ludic index fb3c863f..34454b27 100644 --- a/packages/ludic.update/chain.ludic +++ b/packages/ludic.update/chain.ludic @@ -1,49 +1,49 @@ # chain.ludic - the plan made: the files to fetch, in order, and the base the first delta patches -function update__plan_clear() -> void { - update__files = new []string - update__sha = new []string - update__size = new []int - update__delta = new []bool - update__ver = new []string - update__full = new []string - update__total_bytes = 0 - update__done_bytes = 0 - update__i = 0 +function update__plan_clear(update_st: mut UpdateState) -> void { + update_st.update__files = new []string + update_st.update__sha = new []string + update_st.update__size = new []int + update_st.update__delta = new []bool + update_st.update__ver = new []string + update_st.update__full = new []string + update_st.update__total_bytes = 0 + update_st.update__done_bytes = 0 + update_st.update__i = 0 } # the plan from a parsed feed; false when there is nothing newer than this version -export function update_plan(feed: Val) -> bool { - update__plan_clear() - let as = update__assets(feed) +export function update_plan(update_st: mut UpdateState, feed: Val) -> bool { + update__plan_clear(update_st) + let as = update__assets(update_st, feed) let n = update__newest_full(as) - if n < 0 or not update_newer(as[n].ver, update__conf().version) { return false } + if n < 0 or not update_newer(as[n].ver, update__conf(update_st).version) { return false } let newest = as[n] - update__target = newest.ver - let base = update_packages() + update__sep() + update__full_name(as, update__conf().version) - let chain = update__chain(as, newest.ver) + update_st.update__target = newest.ver + let base = update_packages(update_st) + update__sep() + update__full_name(update_st, as, update__conf(update_st).version) + let chain = update__chain(update_st, as, newest.ver) var chain_bytes = 0 for i in 0 .. len(chain) { chain_bytes += chain[i].size } - let whole = len(chain) > 0 and len(chain) == update__fulls_after(as) + let whole = len(chain) > 0 and len(chain) == update__fulls_after(update_st, as) if Fs.exists(base) and whole and chain_bytes < newest.size { - for i in 0 .. len(chain) { update__plan_add(chain[i], update__full_name(as, chain[i].ver)) } - update__old = base + for i in 0 .. len(chain) { update__plan_add(update_st, chain[i], update__full_name(update_st, as, chain[i].ver)) } + update_st.update__old = base } else { - update__plan_add(newest, newest.file) - update__old = "" + update__plan_add(update_st, newest, newest.file) + update_st.update__old = "" } return true } # the plan, for a panel or a test -export function update_package_count() -> int { - if update__files == null { return 0 } - return len(update__files) +export function update_package_count(update_st: UpdateState) -> int { + if update_st.update__files == null { return 0 } + return len(update_st.update__files) } -export function update_plan_file(i: int) -> string { return update__files[i] } -export function update_plan_is_delta(i: int) -> bool { return update__delta[i] } -export function update_plan_version(i: int) -> string { return update__ver[i] } -export function update_target() -> string { return update__target } -export function update_total_bytes() -> int { return update__total_bytes } +export function update_plan_file(update_st: UpdateState, i: int) -> string { return update_st.update__files[i] } +export function update_plan_is_delta(update_st: UpdateState, i: int) -> bool { return update_st.update__delta[i] } +export function update_plan_version(update_st: UpdateState, i: int) -> string { return update_st.update__ver[i] } +export function update_target(update_st: UpdateState) -> string { return update_st.update__target } +export function update_total_bytes(update_st: UpdateState) -> int { return update_st.update__total_bytes } -function update__path(i: int) -> string { return update_packages() + update__sep() + update__files[i] } +function update__path(update_st: mut UpdateState, i: int) -> string { return update_packages(update_st) + update__sep() + update_st.update__files[i] } diff --git a/packages/ludic.update/install.ludic b/packages/ludic.update/install.ludic index 9c52ba07..c295a02e 100644 --- a/packages/ludic.update/install.ludic +++ b/packages/ludic.update/install.ludic @@ -44,19 +44,19 @@ function update__up(p: string) -> string { # the executable's directory export function update_dir() -> string { return update__up(UpdateWorld.exe()) } -export function update_root() -> string { - let c = update__conf() +export function update_root(update_st: mut UpdateState) -> string { + let c = update__conf(update_st) if len(c.root) > 0 { return c.root } if update__mac() { return update__up(update__up(update_dir())) } return update_dir() + "\\.." } # where the packages live: never inside a macOS bundle, which is read-only and which apply replaces -export function update_packages() -> string { - let c = update__conf() +export function update_packages(update_st: mut UpdateState) -> string { + let c = update__conf(update_st) if len(c.packages) > 0 { return c.packages } if update__mac() and len(c.mac_packages) > 0 { return c.mac_packages } - return update_root() + update__sep() + "packages" + return update_root(update_st) + update__sep() + "packages" } function update__feed_file() -> string { @@ -64,9 +64,9 @@ function update__feed_file() -> string { return "releases.win.json" } -function update__updater() -> string { - if update__mac() { return update_root() + "/Contents/MacOS/UpdateMac" } - return update_root() + update__sep() + "Update.exe" +function update__updater(update_st: mut UpdateState) -> string { + if update__mac() { return update_root(update_st) + "/Contents/MacOS/UpdateMac" } + return update_root(update_st) + update__sep() + "Update.exe" } # A quarantined Mac app still where it was downloaded runs from a read-only /AppTranslocation/ @@ -76,8 +76,8 @@ export function update_translocated() -> bool { } # can this copy update itself? A copy Velopack did not install downloads the game again instead. -export function update_installed() -> bool { - if len(update__conf().root) > 0 { return true } +export function update_installed(update_st: mut UpdateState) -> bool { + if len(update__conf(update_st).root) > 0 { return true } if update_translocated() { return false } if update__mac() { return Fs.exists(update_dir() + "/UpdateMac") } return Fs.exists(update_dir() + "/../Update.exe") diff --git a/packages/ludic.update/latest.ludic b/packages/ludic.update/latest.ludic index 999369bc..56c61bf4 100644 --- a/packages/ludic.update/latest.ludic +++ b/packages/ludic.update/latest.ludic @@ -1,73 +1,69 @@ # latest.ludic - "a new version is out": latest.json asked once, polled a frame at a time, so a # slow or absent network costs the frame nothing - it simply never says there is one. # latest.json is {"version": "0.5.1", "notes": {"en": ["...", ...], "tr": [...]}} -var update__ask: int = 0 -var update__ask_h: int = -1 -var update__latest: string = "" -var update__notes: Val = null # once a frame on the screen that shows it; with show_current, the notes of the version being # played are kept too (a launcher shows them) -export function update_check_tick(show_current: bool) -> void { - if update__ask == UPDATE_ASK_IDLE { - update__ask_h = UpdateNet.get(update__conf().feed + "/" + update__conf().latest) - update__ask = UPDATE_ASKING - if update__ask_h < 0 { update__ask = UPDATE_NONE } +export function update_check_tick(update_st: mut UpdateState, show_current: bool) -> void { + if update_st.update__ask == UPDATE_ASK_IDLE { + update_st.update__ask_h = UpdateNet.get(update__conf(update_st).feed + "/" + update__conf(update_st).latest) + update_st.update__ask = UPDATE_ASKING + if update_st.update__ask_h < 0 { update_st.update__ask = UPDATE_NONE } return } - if update__ask != UPDATE_ASKING { return } - let st = UpdateNet.poll(update__ask_h) + if update_st.update__ask != UPDATE_ASKING { return } + let st = UpdateNet.poll(update_st.update__ask_h) if st < 0 { return } - update__ask = UPDATE_NONE - if st > 0 { update__latest_read(Json.parse(UpdateNet.text(update__ask_h)), show_current) } - UpdateNet.free(update__ask_h) - update__ask_h = -1 + update_st.update__ask = UPDATE_NONE + if st > 0 { update__latest_read(update_st, Json.parse(UpdateNet.text(update_st.update__ask_h)), show_current) } + UpdateNet.free(update_st.update__ask_h) + update_st.update__ask_h = -1 } -function update__latest_read(v: Val, show_current: bool) -> void { +function update__latest_read(update_st: mut UpdateState, v: Val, show_current: bool) -> void { if v == null or Value.kind(v) != 6 or Value.has(v, "version") == 0 { return } let ver = Value.as_str(Value.get(v, "version")) - let mine = update__conf().version - if update_newer(ver, mine) { update__ask = UPDATE_NEWER } - else if ver == mine and show_current { update__ask = UPDATE_CURRENT } + let mine = update__conf(update_st).version + if update_newer(ver, mine) { update_st.update__ask = UPDATE_NEWER } + else if ver == mine and show_current { update_st.update__ask = UPDATE_CURRENT } else { return } - update__latest = ver - if Value.has(v, "notes") != 0 { update__notes = Value.get(v, "notes") } + update_st.update__latest = ver + if Value.has(v, "notes") != 0 { update_st.update__notes = Value.get(v, "notes") } } -export function update_ask_state() -> int { return update__ask } -export function update_latest() -> string { return update__latest } -export function update_shown() -> bool { return update__ask == UPDATE_NEWER or update__ask == UPDATE_CURRENT } -export function update_fresh() -> bool { return update__ask == UPDATE_NEWER } +export function update_ask_state(update_st: UpdateState) -> int { return update_st.update__ask } +export function update_latest(update_st: UpdateState) -> string { return update_st.update__latest } +export function update_shown(update_st: UpdateState) -> bool { return update_st.update__ask == UPDATE_NEWER or update_st.update__ask == UPDATE_CURRENT } +export function update_fresh(update_st: UpdateState) -> bool { return update_st.update__ask == UPDATE_NEWER } # the notes in this language, else English, else none -export function update_notes(lang: string) -> []string { +export function update_notes(update_st: UpdateState, lang: string) -> []string { let out = new []string - if update__notes == null or Value.kind(update__notes) != 6 { return out } + if update_st.update__notes == null or Value.kind(update_st.update__notes) != 6 { return out } var key = lang - if Value.has(update__notes, key) == 0 { key = "en" } - if Value.has(update__notes, key) == 0 { return out } - let l = Value.get(update__notes, key) + if Value.has(update_st.update__notes, key) == 0 { key = "en" } + if Value.has(update_st.update__notes, key) == 0 { return out } + let l = Value.get(update_st.update__notes, key) for i in 0 .. Value.count(l) { push(out, Value.as_str(Value.at(l, i))) } return out } # back to before anything was asked or installed (tests) -export function update_reset() -> void { - if update__ask_h >= 0 { UpdateNet.free(update__ask_h) } - update__ask_h = -1 - update__ask = UPDATE_ASK_IDLE - update__latest = "" - update__notes = null - update__release() - update__state = UPDATE_IDLE - update__err = UPDATE_ERR_NONE - update__target = "" - update__files = null - update__total_bytes = 0 - update__done_bytes = 0 - update__i = 0 - update__rate = 0 - update__rate_t = -1 - q_clear(update_facts()) +export function update_reset(base_st: mut BaseState, update_st: mut UpdateState) -> void { + if update_st.update__ask_h >= 0 { UpdateNet.free(update_st.update__ask_h) } + update_st.update__ask_h = -1 + update_st.update__ask = UPDATE_ASK_IDLE + update_st.update__latest = "" + update_st.update__notes = null + update__release(update_st) + update_st.update__state = UPDATE_IDLE + update_st.update__err = UPDATE_ERR_NONE + update_st.update__target = "" + update_st.update__files = null + update_st.update__total_bytes = 0 + update_st.update__done_bytes = 0 + update_st.update__i = 0 + update_st.update__rate = 0 + update_st.update__rate_t = -1 + q_clear(base_st, update_facts(base_st, update_st)) } diff --git a/packages/ludic.update/plan.ludic b/packages/ludic.update/plan.ludic index a81f45ab..f2384b9a 100644 --- a/packages/ludic.update/plan.ludic +++ b/packages/ludic.update/plan.ludic @@ -10,13 +10,13 @@ property UpdateAsset { } # this app's packages in the feed ({"Assets": [{PackageId, Version, Type, FileName, SHA256, Size}]}) -function update__assets(feed: Val) -> []UpdateAsset { +function update__assets(update_st: mut UpdateState, feed: Val) -> []UpdateAsset { let out = new []UpdateAsset if feed == null or Value.kind(feed) != 6 or Value.has(feed, "Assets") == 0 { return out } let all = Value.get(feed, "Assets") for i in 0 .. Value.count(all) { let a = Value.at(all, i) - if sv_str(a, "PackageId", "") != update__conf().app_id { continue } + if sv_str(a, "PackageId", "") != update__conf(update_st).app_id { continue } let x = new UpdateAsset x.ver = sv_str(a, "Version", "") x.file = sv_str(a, "FileName", "") @@ -31,9 +31,9 @@ function update__assets(feed: Val) -> []UpdateAsset { # What the feed calls the FULL package of version v. Never built here: Velopack puts the channel in # a macOS name (App-0.3.4-osx-full.nupkg) and not a Windows one, and it is both the name a patch # writes and the name the next update looks for as its base. -function update__full_name(as: []UpdateAsset, v: string) -> string { +function update__full_name(update_st: mut UpdateState, as: []UpdateAsset, v: string) -> string { for i in 0 .. len(as) { if not as[i].delta and as[i].ver == v { return as[i].file } } - return update__conf().app_id + "-" + v + "-full.nupkg" + return update__conf(update_st).app_id + "-" + v + "-full.nupkg" } function update__newest_full(as: []UpdateAsset) -> int { @@ -46,9 +46,9 @@ function update__newest_full(as: []UpdateAsset) -> int { } # the deltas above this version up to the newest, oldest first -function update__chain(as: []UpdateAsset, newest: string) -> []UpdateAsset { +function update__chain(update_st: mut UpdateState, as: []UpdateAsset, newest: string) -> []UpdateAsset { let out = new []UpdateAsset - let mine = update__conf().version + let mine = update__conf(update_st).version for i in 0 .. len(as) { let a = as[i] if not a.delta or not update_newer(a.ver, mine) or update_newer(a.ver, newest) { continue } @@ -65,18 +65,18 @@ function update__chain(as: []UpdateAsset, newest: string) -> []UpdateAsset { return out } -function update__fulls_after(as: []UpdateAsset) -> int { +function update__fulls_after(update_st: mut UpdateState, as: []UpdateAsset) -> int { var n = 0 - for i in 0 .. len(as) { if not as[i].delta and update_newer(as[i].ver, update__conf().version) { n += 1 } } + for i in 0 .. len(as) { if not as[i].delta and update_newer(as[i].ver, update__conf(update_st).version) { n += 1 } } return n } -function update__plan_add(a: UpdateAsset, full: string) -> void { - push(update__files, a.file) - push(update__sha, a.sha) - push(update__size, a.size) - push(update__delta, a.delta) - push(update__ver, a.ver) - push(update__full, full) - update__total_bytes += a.size +function update__plan_add(update_st: mut UpdateState, a: UpdateAsset, full: string) -> void { + push(update_st.update__files, a.file) + push(update_st.update__sha, a.sha) + push(update_st.update__size, a.size) + push(update_st.update__delta, a.delta) + push(update_st.update__ver, a.ver) + push(update_st.update__full, full) + update_st.update__total_bytes += a.size } diff --git a/packages/ludic.update/queries.ludic b/packages/ludic.update/queries.ludic index c1f2895c..2f8dfbda 100644 --- a/packages/ludic.update/queries.ludic +++ b/packages/ludic.update/queries.ludic @@ -1,70 +1,70 @@ # queries.ludic - what a panel draws: the step, the bar, the pace, and why it stopped -export function update_state() -> int { return update__state } -export function update_error() -> int { return update__err } -export function update_busy() -> bool { return update__state != UPDATE_IDLE and update__state != UPDATE_FAILED } -export function update_failed() -> bool { return update__state == UPDATE_FAILED } +export function update_state(update_st: UpdateState) -> int { return update_st.update__state } +export function update_error(update_st: UpdateState) -> int { return update_st.update__err } +export function update_busy(update_st: UpdateState) -> bool { return update_st.update__state != UPDATE_IDLE and update_st.update__state != UPDATE_FAILED } +export function update_failed(update_st: UpdateState) -> bool { return update_st.update__state == UPDATE_FAILED } # checking and patching have no fraction to give: a panel's bar sweeps instead of filling -export function update_working() -> bool { return update__state == UPDATE_CHECK or update__state == UPDATE_PATCH } +export function update_working(update_st: UpdateState) -> bool { return update_st.update__state == UPDATE_CHECK or update_st.update__state == UPDATE_PATCH } # the package being fetched, checked or patched (0-based), its name, size and version -export function update_index() -> int { return update__i } +export function update_index(update_st: UpdateState) -> int { return update_st.update__i } -export function update_file() -> string { - if update__files == null or update__i >= len(update__files) { return "" } - return update__files[update__i] +export function update_file(update_st: UpdateState) -> string { + if update_st.update__files == null or update_st.update__i >= len(update_st.update__files) { return "" } + return update_st.update__files[update_st.update__i] } -export function update_file_size() -> int { - if update__size == null or update__i >= len(update__size) { return 0 } - return update__size[update__i] +export function update_file_size(update_st: UpdateState) -> int { + if update_st.update__size == null or update_st.update__i >= len(update_st.update__size) { return 0 } + return update_st.update__size[update_st.update__i] } -export function update_version_at() -> string { - if update__ver == null or update__i >= len(update__ver) { return "" } - return update__ver[update__i] +export function update_version_at(update_st: UpdateState) -> string { + if update_st.update__ver == null or update_st.update__i >= len(update_st.update__ver) { return "" } + return update_st.update__ver[update_st.update__i] } # bytes of the package in flight, and of the whole plan -export function update_file_received() -> int { - if update__state != UPDATE_FETCH or update__h < 0 { return 0 } - return UpdateNet.received(update__h) +export function update_file_received(update_st: UpdateState) -> int { + if update_st.update__state != UPDATE_FETCH or update_st.update__h < 0 { return 0 } + return UpdateNet.received(update_st.update__h) } -export function update_received() -> int { return update__done_bytes + update_file_received() } +export function update_received(update_st: UpdateState) -> int { return update_st.update__done_bytes + update_file_received(update_st) } # how far, 0..1000, over every byte of the plan (the file being checked counts as fetched) -export function update_progress() -> int { - if update__total_bytes <= 0 { return 0 } - var got = update__done_bytes - if update__state == UPDATE_FETCH { got = update_received() } - if update__state == UPDATE_CHECK { got = update__done_bytes + update__size[update__i] } - return min(1000, got / max(1, update__total_bytes / 1000)) +export function update_progress(update_st: UpdateState) -> int { + if update_st.update__total_bytes <= 0 { return 0 } + var got = update_st.update__done_bytes + if update_st.update__state == UPDATE_FETCH { got = update_received(update_st) } + if update_st.update__state == UPDATE_CHECK { got = update_st.update__done_bytes + update_st.update__size[update_st.update__i] } + return min(1000, got / max(1, update_st.update__total_bytes / 1000)) } # seconds a check or a patch has been running -export function update_step_seconds() -> int { return (UpdateWorld.now_ms() - update__step_t0) / 1000 } +export function update_step_seconds(update_st: UpdateState) -> int { return (UpdateWorld.now_ms() - update_st.update__step_t0) / 1000 } # seconds left at the eased pace; -1 until the pace is known -export function update_eta_seconds() -> int { - if update__state != UPDATE_FETCH or update__rate <= 0 { return -1 } - return (update__total_bytes - update_received()) / update__rate + 1 +export function update_eta_seconds(update_st: UpdateState) -> int { + if update_st.update__state != UPDATE_FETCH or update_st.update__rate <= 0 { return -1 } + return (update_st.update__total_bytes - update_received(update_st)) / update_st.update__rate + 1 } # every half second: the pace, eased so the estimate does not jump -function update__rate_tick() -> void { - if update__state != UPDATE_FETCH { return } +function update__rate_tick(update_st: mut UpdateState) -> void { + if update_st.update__state != UPDATE_FETCH { return } let now = UpdateWorld.now_ms() - let got = update_received() - if update__rate_t < 0 { - update__rate_t = now - update__rate_bytes = got + let got = update_received(update_st) + if update_st.update__rate_t < 0 { + update_st.update__rate_t = now + update_st.update__rate_bytes = got return } - let ms = now - update__rate_t + let ms = now - update_st.update__rate_t if ms < 500 { return } - let per_s = (got - update__rate_bytes) / ms * 1000 - if update__rate == 0 { update__rate = per_s } else { update__rate = (update__rate * 3 + per_s) / 4 } - update__rate_t = now - update__rate_bytes = got + let per_s = (got - update_st.update__rate_bytes) / ms * 1000 + if update_st.update__rate == 0 { update_st.update__rate = per_s } else { update_st.update__rate = (update_st.update__rate * 3 + per_s) / 4 } + update_st.update__rate_t = now + update_st.update__rate_bytes = got } diff --git a/packages/ludic.update/state.ludic b/packages/ludic.update/state.ludic index 844f098e..4c0d447a 100644 --- a/packages/ludic.update/state.ludic +++ b/packages/ludic.update/state.ludic @@ -51,47 +51,53 @@ export property UpdateFact { bytes: int = 0 } -var update__cfg: UpdateConfig = null -var update__state: int = 0 -var update__err: int = 0 -var update__h: int = -1 # the request -var update__proc: int = -1 # the child process -var update__target: string = "" -var update__files: []string = null # the plan: file names, hashes, sizes, delta or not, versions -var update__sha: []string = null -var update__size: []int = null -var update__delta: []bool = null -var update__ver: []string = null -var update__full: []string = null # what the feed calls the FULL package of that version -var update__i: int = 0 -var update__done_bytes: int = 0 -var update__total_bytes: int = 0 -var update__old: string = "" # the full package the next delta patches -var update__package: string = "" # what apply installs -var update__step_t0: int = 0 # when a check or a patch began, ms -var update__rate: int = 0 # bytes a second, eased -var update__rate_t: int = -1 -var update__rate_bytes: int = 0 -var update__facts: Queue = null - -export function update_config(c: UpdateConfig) -> void { update__cfg = c } - -function update__conf() -> UpdateConfig { - if update__cfg == null { update__cfg = new UpdateConfig } - return update__cfg +export state UpdateState { + update__ask: int = 0 + update__ask_h: int = -1 + update__latest: string = "" + update__notes: Val = null + update__cfg: UpdateConfig = null + update__state: int = 0 + update__err: int = 0 + update__h: int = -1 # the request + update__proc: int = -1 # the child process + update__target: string = "" + update__files: []string = null # the plan: file names, hashes, sizes, delta or not, versions + update__sha: []string = null + update__size: []int = null + update__delta: []bool = null + update__ver: []string = null + update__full: []string = null # what the feed calls the FULL package of that version + update__i: int = 0 + update__done_bytes: int = 0 + update__total_bytes: int = 0 + update__old: string = "" # the full package the next delta patches + update__package: string = "" # what apply installs + update__step_t0: int = 0 # when a check or a patch began, ms + update__rate: int = 0 # bytes a second, eased + update__rate_t: int = -1 + update__rate_bytes: int = 0 + update__facts: Queue = null } -export function update_facts() -> Queue { - if update__facts == null { update__facts = queue_new("update.facts") } - return update__facts +export function update_config(update_st: mut UpdateState, c: UpdateConfig) -> void { update_st.update__cfg = c } + +function update__conf(update_st: mut UpdateState) -> UpdateConfig { + if update_st.update__cfg == null { update_st.update__cfg = new UpdateConfig } + return update_st.update__cfg } -function update__fact(what: int) -> void { +export function update_facts(base_st: mut BaseState, update_st: mut UpdateState) -> Queue { + if update_st.update__facts == null { update_st.update__facts = queue_new(base_st, "update.facts") } + return update_st.update__facts +} + +function update__fact(base_st: mut BaseState, update_st: mut UpdateState, what: int) -> void { let f = new UpdateFact f.what = what - f.error = update__err - f.target = update__target - f.packages = update_package_count() - f.bytes = update__total_bytes - q_push(update_facts(), f) + f.error = update_st.update__err + f.target = update_st.update__target + f.packages = update_package_count(update_st) + f.bytes = update_st.update__total_bytes + q_push(base_st, update_facts(base_st, update_st), f) } diff --git a/packages/ludic.update/steps.ludic b/packages/ludic.update/steps.ludic index f2d7a060..da11689f 100644 --- a/packages/ludic.update/steps.ludic +++ b/packages/ludic.update/steps.ludic @@ -1,87 +1,87 @@ # steps.ludic - each step started: the feed asked, a package streamed into the packages folder, its # SHA-256 by a child (Get-FileHash, shasum) so the frame keeps drawing, a patch, the apply # the Update button: false (and nothing started) when Velopack did not install this copy -export function update_start() -> bool { - if not update_installed() { - update__err = UPDATE_ERR_NOT_INSTALLED +export function update_start(base_st: mut BaseState, update_st: mut UpdateState) -> bool { + if not update_installed(update_st) { + update_st.update__err = UPDATE_ERR_NOT_INSTALLED return false } - update__release() - Fs.mkdir(update_packages()) - update__err = UPDATE_ERR_NONE - update__h = UpdateNet.get(update__conf().feed + "/" + update__feed_file()) - if update__h < 0 { - update__fail(UPDATE_ERR_UNREACHABLE) + update__release(update_st) + Fs.mkdir(update_packages(update_st)) + update_st.update__err = UPDATE_ERR_NONE + update_st.update__h = UpdateNet.get(update__conf(update_st).feed + "/" + update__feed_file()) + if update_st.update__h < 0 { + update__fail(base_st, update_st, UPDATE_ERR_UNREACHABLE) return true } - update__state = UPDATE_FEED + update_st.update__state = UPDATE_FEED return true } -function update__release() -> void { - if update__h >= 0 { UpdateNet.free(update__h) } - update__h = -1 - if update__proc >= 0 { UpdateProc.free(update__proc) } - update__proc = -1 +function update__release(update_st: mut UpdateState) -> void { + if update_st.update__h >= 0 { UpdateNet.free(update_st.update__h) } + update_st.update__h = -1 + if update_st.update__proc >= 0 { UpdateProc.free(update_st.update__proc) } + update_st.update__proc = -1 } -function update__fail(err: int) -> void { - update__release() - update__err = err - update__state = UPDATE_FAILED - update__fact(UPDATE_STOPPED) +function update__fail(base_st: mut BaseState, update_st: mut UpdateState, err: int) -> void { + update__release(update_st) + update_st.update__err = err + update_st.update__state = UPDATE_FAILED + update__fact(base_st, update_st, UPDATE_STOPPED) } # fetch file i, unless a copy of the right size is there already - then straight to its check -function update__fetch(i: int) -> void { - update__i = i - let p = update__path(i) - if Fs.exists(p) and Fs.size(p) == update__size[i] { - update__check(i) +function update__fetch(base_st: mut BaseState, update_st: mut UpdateState, i: int) -> void { + update_st.update__i = i + let p = update__path(update_st, i) + if Fs.exists(p) and Fs.size(p) == update_st.update__size[i] { + update__check(base_st, update_st, i) return } if Fs.exists(p) { Fs.remove(p) } - update__h = UpdateNet.download(update__conf().feed + "/" + update__files[i], p) - if update__h < 0 { - update__fail(UPDATE_ERR_UNREACHABLE) + update_st.update__h = UpdateNet.download(update__conf(update_st).feed + "/" + update_st.update__files[i], p) + if update_st.update__h < 0 { + update__fail(base_st, update_st, UPDATE_ERR_UNREACHABLE) return } - update__rate_t = -1 - update__state = UPDATE_FETCH + update_st.update__rate_t = -1 + update_st.update__state = UPDATE_FETCH } -function update__hash_file() -> string { - if len(update__conf().scratch) > 0 { return update__conf().scratch } +function update__hash_file(update_st: mut UpdateState) -> string { + if len(update__conf(update_st).scratch) > 0 { return update__conf(update_st).scratch } return Os.temp_dir() + "/ludic-update-hash.txt" } # no cmd.exe: it parses its own command line and would not survive Process.spawn's quoting -function update__check(i: int) -> void { - let out = update__hash_file() +function update__check(base_st: mut BaseState, update_st: mut UpdateState, i: int) -> void { + let out = update__hash_file(update_st) if Fs.exists(out) { Fs.remove(out) } let args = new []string if UpdateWorld.platform() == "windows" { push(args, "-NoProfile") push(args, "-NonInteractive") push(args, "-Command") - push(args, "(Get-FileHash -Algorithm SHA256 -LiteralPath " + update__psq(update__path(i)) + ").Hash | Out-File -Encoding ascii -LiteralPath " + update__psq(out)) - update__proc = UpdateProc.spawn(update__powershell(), args) + push(args, "(Get-FileHash -Algorithm SHA256 -LiteralPath " + update__psq(update__path(update_st, i)) + ").Hash | Out-File -Encoding ascii -LiteralPath " + update__psq(out)) + update_st.update__proc = UpdateProc.spawn(update__powershell(), args) } else { push(args, "-c") - push(args, "shasum -a 256 '" + update__path(i) + "' | cut -d' ' -f1 > '" + out + "'") - update__proc = UpdateProc.spawn("/bin/sh", args) + push(args, "shasum -a 256 '" + update__path(update_st, i) + "' | cut -d' ' -f1 > '" + out + "'") + update_st.update__proc = UpdateProc.spawn("/bin/sh", args) } - if update__proc < 0 { - update__fail(UPDATE_ERR_CHECK) + if update_st.update__proc < 0 { + update__fail(base_st, update_st, UPDATE_ERR_CHECK) return } - update__state = UPDATE_CHECK - update__step_t0 = UpdateWorld.now_ms() + update_st.update__state = UPDATE_CHECK + update_st.update__step_t0 = UpdateWorld.now_ms() } # the hash the child wrote: the first line with anything on it -function update__hash_read() -> string { - let s = Fs.read_text(update__hash_file()) +function update__hash_read(update_st: mut UpdateState) -> string { + let s = Fs.read_text(update__hash_file(update_st)) if s == null { return "" } let lines = Text.split(s, "\n") for i in 0 .. len(lines) { diff --git a/packages/ludic.update/tests/update_test.ludic b/packages/ludic.update/tests/update_test.ludic index 101ab20e..c3499121 100644 --- a/packages/ludic.update/tests/update_test.ludic +++ b/packages/ludic.update/tests/update_test.ludic @@ -4,40 +4,42 @@ import "ludic.update" import "ludic.base" program UpdateTest { numbers float - var net_answer: int = -1 - var net_text: string = "" - var net_urls: []string = null - var net_paths: []string = null - var net_got: int = 0 - var proc_code: int = -1 - var proc_paths: []string = null - var proc_args: []string = null - var exe: string = "" + state UpdatetestState { + net_answer: int = -1 + net_text: string = "" + net_urls: []string = null + net_paths: []string = null + net_got: int = 0 + proc_code: int = -1 + proc_paths: []string = null + proc_args: []string = null + exe: string = "" + } function fake_platform() -> string { return "macos" } - function fake_exe() -> string { return exe } + function fake_exe(updatetest_st: UpdatetestState) -> string { return updatetest_st.exe } function fake_pid() -> int { return 4242 } function fake_ms() -> int { return 0 } - function fake_get(url: string) -> int { - push(net_urls, url) - push(net_paths, "") - return len(net_urls) - 1 + function fake_get(updatetest_st: mut UpdatetestState, url: string) -> int { + push(updatetest_st.net_urls, url) + push(updatetest_st.net_paths, "") + return len(updatetest_st.net_urls) - 1 } - function fake_download(url: string, path: string) -> int { - push(net_urls, url) - push(net_paths, path) - return len(net_urls) - 1 + function fake_download(updatetest_st: mut UpdatetestState, url: string, path: string) -> int { + push(updatetest_st.net_urls, url) + push(updatetest_st.net_paths, path) + return len(updatetest_st.net_urls) - 1 } - function fake_poll(h: int) -> int { return net_answer } - function fake_text(h: int) -> string { return net_text } - function fake_received(h: int) -> int { return net_got } + function fake_poll(updatetest_st: UpdatetestState, h: int) -> int { return updatetest_st.net_answer } + function fake_text(updatetest_st: UpdatetestState, h: int) -> string { return updatetest_st.net_text } + function fake_received(updatetest_st: UpdatetestState, h: int) -> int { return updatetest_st.net_got } function fake_free(h: int) -> void { } - function fake_spawn(path: string, args: []string) -> int { - push(proc_paths, path) - push(proc_args, Text.join(args, " ")) - return len(proc_paths) - 1 + function fake_spawn(updatetest_st: mut UpdatetestState, path: string, args: []string) -> int { + push(updatetest_st.proc_paths, path) + push(updatetest_st.proc_args, Text.join(args, " ")) + return len(updatetest_st.proc_paths) - 1 } - function fake_proc_poll(h: int) -> int { return proc_code } + function fake_proc_poll(updatetest_st: UpdatetestState, h: int) -> int { return updatetest_st.proc_code } function fake_proc_free(h: int) -> void { } bind UpdateWorld { platform: fn fake_platform, exe: fn fake_exe, pid: fn fake_pid, now_ms: fn fake_ms } @@ -48,16 +50,16 @@ program UpdateTest { function pkgs() -> string { return dir() + "/packages" } function hashf() -> string { return dir() + "/hash.txt" } - function fresh(root: bool) -> void { - net_answer = -1 - net_text = "" - net_urls = new []string - net_paths = new []string - net_got = 0 - proc_code = -1 - proc_paths = new []string - proc_args = new []string - exe = dir() + "/Game.app/Contents/MacOS/Game" + function fresh(base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState, root: bool) -> void { + updatetest_st.net_answer = -1 + updatetest_st.net_text = "" + updatetest_st.net_urls = new []string + updatetest_st.net_paths = new []string + updatetest_st.net_got = 0 + updatetest_st.proc_code = -1 + updatetest_st.proc_paths = new []string + updatetest_st.proc_args = new []string + updatetest_st.exe = dir() + "/Game.app/Contents/MacOS/Game" System.run("rm -rf '" + dir() + "'") Fs.mkdir(dir()) Fs.mkdir(pkgs()) @@ -68,8 +70,8 @@ program UpdateTest { if root { c.root = dir() + "/Game.app" } c.packages = pkgs() c.scratch = hashf() - update_config(c) - update_reset() + update_config(update_st, c) + update_reset(base_st, update_st) } function asset(ty: string, ver: string, size: int) -> string { @@ -85,18 +87,18 @@ program UpdateTest { } function seed_base() -> void { Fs.write_text(pkgs() + "/Game-1.0.0-osx-full.nupkg", "base") } - function plan_of(text: string) -> bool { return update_plan(Json.parse(text)) } + function plan_of(update_st: mut UpdateState, text: string) -> bool { return update_plan(update_st, Json.parse(text)) } - function facts_of(what: int) -> int { - let fs = q_drain(update_facts()) + function facts_of(base_st: mut BaseState, update_st: mut UpdateState, what: int) -> int { + let fs = q_drain(base_st, update_facts(base_st, update_st)) var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } function hash_is(h: string) -> void { Fs.write_text(hashf(), h + "\n") } - function last_args() -> string { return proc_args[len(proc_args) - 1] } - function last_url() -> string { return net_urls[len(net_urls) - 1] } + function last_args(updatetest_st: UpdatetestState) -> string { return updatetest_st.proc_args[len(updatetest_st.proc_args) - 1] } + function last_url(updatetest_st: UpdatetestState) -> string { return updatetest_st.net_urls[len(updatetest_st.net_urls) - 1] } test "versions compare a number at a time" { expect(update_newer("0.10.2", "0.9.9")) @@ -105,166 +107,166 @@ program UpdateTest { expect(not update_newer("0.4.9", "0.5.0")) } - test "a newer version is shown with its notes, in the language asked for or English" { - fresh(true) - update_check_tick(false) - expect(last_url() == "http://feed/latest.json") - net_text = "{\"version\":\"1.2.0\",\"notes\":{\"en\":[\"Fish.\",\"Boats.\"],\"tr\":[\"Balik.\"]}}" - net_answer = 1 - update_check_tick(false) - expect(update_fresh()) - expect(update_latest() == "1.2.0") - expect_eq(len(update_notes("tr")), 1) - expect_eq(len(update_notes("de")), 2) + test "a newer version is shown with its notes, in the language asked for or English" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + update_check_tick(update_st, false) + expect(last_url(updatetest_st) == "http://feed/latest.json") + updatetest_st.net_text = "{\"version\":\"1.2.0\",\"notes\":{\"en\":[\"Fish.\",\"Boats.\"],\"tr\":[\"Balik.\"]}}" + updatetest_st.net_answer = 1 + update_check_tick(update_st, false) + expect(update_fresh(update_st)) + expect(update_latest(update_st) == "1.2.0") + expect_eq(len(update_notes(update_st, "tr")), 1) + expect_eq(len(update_notes(update_st, "de")), 2) } - test "the version being played is shown only when asked" { - fresh(true) - net_text = "{\"version\":\"1.0.0\",\"notes\":{\"en\":[\"x\"]}}" - net_answer = 1 - update_check_tick(false) - update_check_tick(false) - expect(not update_shown()) - update_reset() - update_check_tick(true) - update_check_tick(true) - expect_eq(update_ask_state(), UPDATE_CURRENT) - update_reset() - net_answer = 0 - update_check_tick(true) - update_check_tick(true) - expect_eq(update_ask_state(), UPDATE_NONE) + test "the version being played is shown only when asked" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + updatetest_st.net_text = "{\"version\":\"1.0.0\",\"notes\":{\"en\":[\"x\"]}}" + updatetest_st.net_answer = 1 + update_check_tick(update_st, false) + update_check_tick(update_st, false) + expect(not update_shown(update_st)) + update_reset(base_st, update_st) + update_check_tick(update_st, true) + update_check_tick(update_st, true) + expect_eq(update_ask_state(update_st), UPDATE_CURRENT) + update_reset(base_st, update_st) + updatetest_st.net_answer = 0 + update_check_tick(update_st, true) + update_check_tick(update_st, true) + expect_eq(update_ask_state(update_st), UPDATE_NONE) } - test "with the base on disk and every delta there, the plan is the chain, oldest first" { - fresh(true) + test "with the base on disk and every delta there, the plan is the chain, oldest first" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) seed_base() - expect(plan_of(feed(true))) - expect_eq(update_package_count(), 2) - expect(update_plan_is_delta(0)) - expect(update_plan_version(0) == "1.1.0") - expect(update_plan_version(1) == "1.2.0") - expect(update_target() == "1.2.0") + expect(plan_of(update_st, feed(true))) + expect_eq(update_package_count(update_st), 2) + expect(update_plan_is_delta(update_st, 0)) + expect(update_plan_version(update_st, 0) == "1.1.0") + expect(update_plan_version(update_st, 1) == "1.2.0") + expect(update_target(update_st) == "1.2.0") } - test "no base, or a missing delta, is the whole newest package" { - fresh(true) - expect(plan_of(feed(true))) - expect_eq(update_package_count(), 1) - expect(not update_plan_is_delta(0)) - expect(update_plan_file(0) == "Game-1.2.0-osx-full.nupkg") + test "no base, or a missing delta, is the whole newest package" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + expect(plan_of(update_st, feed(true))) + expect_eq(update_package_count(update_st), 1) + expect(not update_plan_is_delta(update_st, 0)) + expect(update_plan_file(update_st, 0) == "Game-1.2.0-osx-full.nupkg") seed_base() - expect(plan_of(feed(false))) - expect_eq(update_package_count(), 1) + expect(plan_of(update_st, feed(false))) + expect_eq(update_package_count(update_st), 1) } - test "nothing newer is no plan" { - fresh(true) - expect(not plan_of("{\"Assets\":[" + asset("Full", "1.0.0", 900) + "]}")) - expect(not plan_of("{}")) + test "nothing newer is no plan" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + expect(not plan_of(update_st, "{\"Assets\":[" + asset("Full", "1.0.0", 900) + "]}")) + expect(not plan_of(update_st, "{}")) } - test "a copy Velopack did not install does not start" { - fresh(false) - expect(not update_installed()) - expect(not update_start()) - expect_eq(update_error(), UPDATE_ERR_NOT_INSTALLED) + test "a copy Velopack did not install does not start" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, false) + expect(not update_installed(update_st)) + expect(not update_start(base_st, update_st)) + expect_eq(update_error(update_st), UPDATE_ERR_NOT_INSTALLED) Fs.mkdir(dir() + "/Game.app") Fs.mkdir(dir() + "/Game.app/Contents") Fs.mkdir(dir() + "/Game.app/Contents/MacOS") Fs.write_text(dir() + "/Game.app/Contents/MacOS/UpdateMac", "x") - expect(update_installed()) - exe = "/private/var/folders/x/AppTranslocation/y/Game.app/Contents/MacOS/Game" - expect(not update_installed()) + expect(update_installed(update_st)) + updatetest_st.exe = "/private/var/folders/x/AppTranslocation/y/Game.app/Contents/MacOS/Game" + expect(not update_installed(update_st)) } - test "the whole package: fetched, checked, applied, and the game told to go" { - fresh(true) - expect(update_start()) - expect(last_url() == "http://feed/releases.osx.json") - net_text = feed(true) - net_answer = 1 - update_tick() - expect_eq(update_state(), UPDATE_FETCH) - expect_eq(facts_of(UPDATE_STARTED), 1) - expect(last_url() == "http://feed/Game-1.2.0-osx-full.nupkg") - net_got = 500 - expect(update_progress() >= 490 and update_progress() <= 510) - update_tick() - expect_eq(update_state(), UPDATE_CHECK) - expect(Text.contains(last_args(), "shasum -a 256")) + test "the whole package: fetched, checked, applied, and the game told to go" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + expect(update_start(base_st, update_st)) + expect(last_url(updatetest_st) == "http://feed/releases.osx.json") + updatetest_st.net_text = feed(true) + updatetest_st.net_answer = 1 + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_FETCH) + expect_eq(facts_of(base_st, update_st, UPDATE_STARTED), 1) + expect(last_url(updatetest_st) == "http://feed/Game-1.2.0-osx-full.nupkg") + updatetest_st.net_got = 500 + expect(update_progress(update_st) >= 490 and update_progress(update_st) <= 510) + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_CHECK) + expect(Text.contains(last_args(updatetest_st), "shasum -a 256")) hash_is("AB1.2.0") - proc_code = 0 - update_tick() - expect_eq(update_state(), UPDATE_APPLY) - expect(Text.contains(last_args(), "apply --silent --waitPid 4242 --package")) - expect(Text.contains(last_args(), "--rootDir")) - expect(Text.ends_with(proc_paths[len(proc_paths) - 1], "Game.app/Contents/MacOS/UpdateMac")) - expect_eq(facts_of(UPDATE_APPLYING), 1) + updatetest_st.proc_code = 0 + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_APPLY) + expect(Text.contains(last_args(updatetest_st), "apply --silent --waitPid 4242 --package")) + expect(Text.contains(last_args(updatetest_st), "--rootDir")) + expect(Text.ends_with(updatetest_st.proc_paths[len(updatetest_st.proc_paths) - 1], "Game.app/Contents/MacOS/UpdateMac")) + expect_eq(facts_of(base_st, update_st, UPDATE_APPLYING), 1) } - test "a wrong hash stops it and deletes the file" { - fresh(true) - update_start() - net_text = feed(true) - net_answer = 1 - update_tick() + test "a wrong hash stops it and deletes the file" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + update_start(base_st, update_st) + updatetest_st.net_text = feed(true) + updatetest_st.net_answer = 1 + update_tick(base_st, update_st) Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", "junk") - update_tick() + update_tick(base_st, update_st) hash_is("FFFF") - proc_code = 0 - update_tick() - expect(update_failed()) - expect_eq(update_error(), UPDATE_ERR_CORRUPT) + updatetest_st.proc_code = 0 + update_tick(base_st, update_st) + expect(update_failed(update_st)) + expect_eq(update_error(update_st), UPDATE_ERR_CORRUPT) expect(not Fs.exists(pkgs() + "/Game-1.2.0-osx-full.nupkg")) - expect_eq(facts_of(UPDATE_STOPPED), 1) + expect_eq(facts_of(base_st, update_st, UPDATE_STOPPED), 1) } - test "a failed download stops it" { - fresh(true) - update_start() - net_text = feed(true) - net_answer = 1 - update_tick() - net_answer = 0 - update_tick() - expect_eq(update_error(), UPDATE_ERR_DOWNLOAD) + test "a failed download stops it" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + update_start(base_st, update_st) + updatetest_st.net_text = feed(true) + updatetest_st.net_answer = 1 + update_tick(base_st, update_st) + updatetest_st.net_answer = 0 + update_tick(base_st, update_st) + expect_eq(update_error(update_st), UPDATE_ERR_DOWNLOAD) } - test "the delta chain: each fetched, checked and patched onto the last, then applied" { - fresh(true) + test "the delta chain: each fetched, checked and patched onto the last, then applied" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) seed_base() - update_start() - net_text = feed(true) - net_answer = 1 - update_tick() - update_tick() + update_start(base_st, update_st) + updatetest_st.net_text = feed(true) + updatetest_st.net_answer = 1 + update_tick(base_st, update_st) + update_tick(base_st, update_st) hash_is("AB1.1.0") - proc_code = 0 - update_tick() - expect_eq(update_state(), UPDATE_PATCH) - expect(Text.contains(last_args(), "--old " + pkgs() + "/Game-1.0.0-osx-full.nupkg")) - expect(Text.contains(last_args(), "--output " + pkgs() + "/Game-1.1.0-osx-full.nupkg")) + updatetest_st.proc_code = 0 + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_PATCH) + expect(Text.contains(last_args(updatetest_st), "--old " + pkgs() + "/Game-1.0.0-osx-full.nupkg")) + expect(Text.contains(last_args(updatetest_st), "--output " + pkgs() + "/Game-1.1.0-osx-full.nupkg")) Fs.write_text(pkgs() + "/Game-1.1.0-osx-full.nupkg", "patched") - update_tick() - expect_eq(update_state(), UPDATE_FETCH) - expect_eq(update_index(), 1) - update_tick() + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_FETCH) + expect_eq(update_index(update_st), 1) + update_tick(base_st, update_st) hash_is("AB1.2.0") - update_tick() + update_tick(base_st, update_st) Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", "patched") - update_tick() - expect_eq(update_state(), UPDATE_APPLY) - expect(Text.contains(last_args(), "--package " + pkgs() + "/Game-1.2.0-osx-full.nupkg")) + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_APPLY) + expect(Text.contains(last_args(updatetest_st), "--package " + pkgs() + "/Game-1.2.0-osx-full.nupkg")) } - test "a package already on disk at its size is checked, not fetched again" { - fresh(true) - update_start() - net_text = feed(true) - net_answer = 1 + test "a package already on disk at its size is checked, not fetched again" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) { + fresh(base_st, update_st, updatetest_st, true) + update_start(base_st, update_st) + updatetest_st.net_text = feed(true) + updatetest_st.net_answer = 1 Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", Text.repeat("x", 1000)) - update_tick() - expect_eq(update_state(), UPDATE_CHECK) + update_tick(base_st, update_st) + expect_eq(update_state(update_st), UPDATE_CHECK) } } diff --git a/packages/ludic.update/tick.ludic b/packages/ludic.update/tick.ludic index a1c9d788..bb87b8a7 100644 --- a/packages/ludic.update/tick.ludic +++ b/packages/ludic.update/tick.ludic @@ -1,99 +1,99 @@ # tick.ludic - once a frame while an update runs: whatever the step waits on, and the next step -export function update_tick() -> void { - update__rate_tick() - if update__state == UPDATE_FEED { update__feed_tick() } - else if update__state == UPDATE_FETCH { update__fetch_tick() } - else if update__state == UPDATE_CHECK { update__check_tick() } - else if update__state == UPDATE_PATCH { update__patch_tick() } +export function update_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void { + update__rate_tick(update_st) + if update_st.update__state == UPDATE_FEED { update__feed_tick(base_st, update_st) } + else if update_st.update__state == UPDATE_FETCH { update__fetch_tick(base_st, update_st) } + else if update_st.update__state == UPDATE_CHECK { update__check_tick(base_st, update_st) } + else if update_st.update__state == UPDATE_PATCH { update__patch_tick(base_st, update_st) } } -function update__feed_tick() -> void { - let st = UpdateNet.poll(update__h) +function update__feed_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void { + let st = UpdateNet.poll(update_st.update__h) if st < 0 { return } var feed: Val = null - if st > 0 { feed = Json.parse(UpdateNet.text(update__h)) } - UpdateNet.free(update__h) - update__h = -1 + if st > 0 { feed = Json.parse(UpdateNet.text(update_st.update__h)) } + UpdateNet.free(update_st.update__h) + update_st.update__h = -1 if feed == null { - update__fail(UPDATE_ERR_UNREACHABLE) + update__fail(base_st, update_st, UPDATE_ERR_UNREACHABLE) return } - if not update_plan(feed) { - update__fail(UPDATE_ERR_NOTHING) + if not update_plan(update_st, feed) { + update__fail(base_st, update_st, UPDATE_ERR_NOTHING) return } - update__fact(UPDATE_STARTED) - update__fetch(0) + update__fact(base_st, update_st, UPDATE_STARTED) + update__fetch(base_st, update_st, 0) } -function update__fetch_tick() -> void { - let st = UpdateNet.poll(update__h) +function update__fetch_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void { + let st = UpdateNet.poll(update_st.update__h) if st < 0 { return } - UpdateNet.free(update__h) - update__h = -1 + UpdateNet.free(update_st.update__h) + update_st.update__h = -1 if st == 0 { - Fs.remove(update__path(update__i)) - update__fail(UPDATE_ERR_DOWNLOAD) + Fs.remove(update__path(update_st, update_st.update__i)) + update__fail(base_st, update_st, UPDATE_ERR_DOWNLOAD) return } - update__check(update__i) + update__check(base_st, update_st, update_st.update__i) } # a package rebuilt from deltas is not compared with the full one's hash: patch re-assembles the # archive, so the same files come out as different bytes -function update__check_tick() -> void { - let code = UpdateProc.poll(update__proc) +function update__check_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void { + let code = UpdateProc.poll(update_st.update__proc) if code < 0 { return } - UpdateProc.free(update__proc) - update__proc = -1 - if code != 0 or update__hash_read() != Text.upper(update__sha[update__i]) { - Fs.remove(update__path(update__i)) - update__fail(UPDATE_ERR_CORRUPT) + UpdateProc.free(update_st.update__proc) + update_st.update__proc = -1 + if code != 0 or update__hash_read(update_st) != Text.upper(update_st.update__sha[update_st.update__i]) { + Fs.remove(update__path(update_st, update_st.update__i)) + update__fail(base_st, update_st, UPDATE_ERR_CORRUPT) return } - update__done_bytes += update__size[update__i] - if update__delta[update__i] { update__patch(update__i) } else { update__apply(update__path(update__i)) } + update_st.update__done_bytes += update_st.update__size[update_st.update__i] + if update_st.update__delta[update_st.update__i] { update__patch(base_st, update_st, update_st.update__i) } else { update__apply(base_st, update_st, update__path(update_st, update_st.update__i)) } } -function update__patch(i: int) -> void { - let out = update_packages() + update__sep() + update__full[i] +function update__patch(base_st: mut BaseState, update_st: mut UpdateState, i: int) -> void { + let out = update_packages(update_st) + update__sep() + update_st.update__full[i] if Fs.exists(out) { Fs.remove(out) } let args = new []string push(args, "patch") push(args, "--silent") push(args, "--old") - push(args, update__old) + push(args, update_st.update__old) push(args, "--delta") - push(args, update__path(i)) + push(args, update__path(update_st, i)) push(args, "--output") push(args, out) - update__proc = UpdateProc.spawn(update__updater(), args) - if update__proc < 0 { - update__fail(UPDATE_ERR_PATCH_START) + update_st.update__proc = UpdateProc.spawn(update__updater(update_st), args) + if update_st.update__proc < 0 { + update__fail(base_st, update_st, UPDATE_ERR_PATCH_START) return } - update__old = out - update__state = UPDATE_PATCH - update__step_t0 = UpdateWorld.now_ms() + update_st.update__old = out + update_st.update__state = UPDATE_PATCH + update_st.update__step_t0 = UpdateWorld.now_ms() } -function update__patch_tick() -> void { - let code = UpdateProc.poll(update__proc) +function update__patch_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void { + let code = UpdateProc.poll(update_st.update__proc) if code < 0 { return } - UpdateProc.free(update__proc) - update__proc = -1 - if code != 0 or not Fs.exists(update__old) { - update__fail(UPDATE_ERR_PATCH) + UpdateProc.free(update_st.update__proc) + update_st.update__proc = -1 + if code != 0 or not Fs.exists(update_st.update__old) { + update__fail(base_st, update_st, UPDATE_ERR_PATCH) return } - if update__i + 1 < len(update__files) { update__fetch(update__i + 1) } else { update__apply(update__old) } + if update_st.update__i + 1 < len(update_st.update__files) { update__fetch(base_st, update_st, update_st.update__i + 1) } else { update__apply(base_st, update_st, update_st.update__old) } } # Velopack waits for this process to be gone, swaps the install and starts the new version; the # game hears UPDATE_APPLYING and exits. On macOS neither the root nor the packages are where # UpdateMac would look, so both are named. -function update__apply(package: string) -> void { - update__package = package +function update__apply(base_st: mut BaseState, update_st: mut UpdateState, package: string) -> void { + update_st.update__package = package let args = new []string push(args, "apply") push(args, "--silent") @@ -103,15 +103,15 @@ function update__apply(package: string) -> void { push(args, package) if update__mac() { push(args, "--rootDir") - push(args, update_root()) + push(args, update_root(update_st)) push(args, "--packageDir") - push(args, update_packages()) + push(args, update_packages(update_st)) } - update__proc = UpdateProc.spawn(update__updater(), args) - if update__proc < 0 { - update__fail(UPDATE_ERR_APPLY) + update_st.update__proc = UpdateProc.spawn(update__updater(update_st), args) + if update_st.update__proc < 0 { + update__fail(base_st, update_st, UPDATE_ERR_APPLY) return } - update__state = UPDATE_APPLY - update__fact(UPDATE_APPLYING) + update_st.update__state = UPDATE_APPLY + update__fact(base_st, update_st, UPDATE_APPLYING) } diff --git a/packages/ludic.vehicles/call.ludic b/packages/ludic.vehicles/call.ludic index ff789133..290fc606 100644 --- a/packages/ludic.vehicles/call.ludic +++ b/packages/ludic.vehicles/call.ludic @@ -1,6 +1,6 @@ # call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there # after; gone when its player leaves -function ve_add(kind: int, x: float, z: float, yaw: float) -> Vehicle { +function ve_add(vehicles_st: mut VehiclesState, kind: int, x: float, z: float, yaw: float) -> Vehicle { if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null } let v = new Vehicle v.nid = VehicleWorld.next_id() @@ -14,7 +14,7 @@ function ve_add(kind: int, x: float, z: float, yaw: float) -> Vehicle { v.seat_h = 0.30 } ve_put(v, x, z) - push(ve_all(), v) + push(ve_all(vehicles_st), v) VehicleWorld.made(v) return v } @@ -31,46 +31,46 @@ function ve_put(v: Vehicle, x: float, z: float) -> void { # Bring player pid's boat or horse to place k. False when the place is somebody else's, or it is # being ridden. A horse faces the trough: the bay lies on its near side. -export function vehicle_call_for(pid: int, kind: int, k: int) -> bool { - if not ve_have_dock or k < 0 or k >= VEHICLE_PLACES { return false } - let here = vehicle_in_place(kind, k) - let v = vehicle_of(pid, kind) +export function vehicle_call_for(vehicles_st: mut VehiclesState, pid: int, kind: int, k: int) -> bool { + if not vehicles_st.ve_have_dock or k < 0 or k >= VEHICLE_PLACES { return false } + let here = vehicle_in_place(vehicles_st, kind, k) + let v = vehicle_of(vehicles_st, pid, kind) if here != null { return v != null and here.nid == v.nid } - var yaw = ve_dock_yaw - if kind == VEHICLE_HORSE { yaw = ve_rail_yaw } + var yaw = vehicles_st.ve_dock_yaw + if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw } if v == null { - let made = ve_add(kind, vehicle_place_x(kind, k), vehicle_place_z(kind, k), yaw) + let made = ve_add(vehicles_st, kind, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw) if made == null { return false } made.owner = pid - if pid == 0 { ve_apply_kept(made) } + if pid == 0 { ve_apply_kept(vehicles_st, made) } return true } if v.rider >= 0 { return false } - ve_put(v, vehicle_place_x(kind, k), vehicle_place_z(kind, k)) + ve_put(v, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k)) v.yaw = yaw VehicleWorld.placed(v) return true } # a player left: their boat and horse go with them, and their places are free -export function vehicles_release(pid: int) -> void { +export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void { if pid == 0 { return } let keep = new []Vehicle - let l = ve_all() + let l = ve_all(vehicles_st) for i in 0 .. len(l) { if l[i].owner != pid { push(keep, l[i]) } else { VehicleWorld.gone(l[i]) } } - ve_list = keep + vehicles_st.ve_list = keep } # hired or rented for the trip export function vehicle_own(v: Vehicle) -> void { if v != null { v.owned = true } } # hay for a horse, up to a full belly -export function vehicle_feed(v: Vehicle, amount: float) -> void { +export function vehicle_feed(base_st: mut BaseState, vehicles_st: mut VehiclesState, v: Vehicle, amount: float) -> void { if v == null { return } v.food = Math.min(v.food + amount, 100.0) - ve_say(VEHICLE_FED, v, amount) + ve_say(base_st, vehicles_st, VEHICLE_FED, v, amount) } # a horse nobody rides goes where its animal goes, round the home it is tied at (home_x, home_z) @@ -83,22 +83,19 @@ export function vehicle_follow(v: Vehicle, x: float, y: float, z: float, yaw: fl } # a saved trip's word on this player's own: hired, and how fed -export function vehicle_restore(kind: int, owned: bool, food: float) -> void { +export function vehicle_restore(vehicles_st: mut VehiclesState, kind: int, owned: bool, food: float) -> void { if kind == VEHICLE_HORSE { - ve_kept_horse = owned - ve_kept_food = food - } else { ve_kept_boat = owned } - let v = vehicle_find(kind) - if v != null { ve_apply_kept(v) } + vehicles_st.ve_kept_horse = owned + vehicles_st.ve_kept_food = food + } else { vehicles_st.ve_kept_boat = owned } + let v = vehicle_find(vehicles_st, kind) + if v != null { ve_apply_kept(vehicles_st, v) } } -var ve_kept_horse: bool = false -var ve_kept_boat: bool = false -var ve_kept_food: float = 40.0 -function ve_apply_kept(v: Vehicle) -> void { +function ve_apply_kept(vehicles_st: VehiclesState, v: Vehicle) -> void { if v.kind == VEHICLE_HORSE { - v.owned = ve_kept_horse - v.food = ve_kept_food - } else { v.owned = ve_kept_boat } + v.owned = vehicles_st.ve_kept_horse + v.food = vehicles_st.ve_kept_food + } else { v.owned = vehicles_st.ve_kept_boat } } diff --git a/packages/ludic.vehicles/mount.ludic b/packages/ludic.vehicles/mount.ludic index 9577cd33..7ca607f2 100644 --- a/packages/ludic.vehicles/mount.ludic +++ b/packages/ludic.vehicles/mount.ludic @@ -4,27 +4,27 @@ export const VEHICLE_HAY_MIN: float = 10.0 # a horse with less hay than this will not go # this machine's player onto v; false when it is somebody else's, taken, or a hungry horse -export function vehicle_mount(v: Vehicle) -> bool { - if v == null or v.owner != 0 or v.rider >= 0 or ve_cur != null { return false } +export function vehicle_mount(base_st: mut BaseState, vehicles_st: mut VehiclesState, v: Vehicle) -> bool { + if v == null or v.owner != 0 or v.rider >= 0 or vehicles_st.ve_cur != null { return false } if v.kind == VEHICLE_HORSE and v.food < VEHICLE_HAY_MIN { - ve_say(VEHICLE_HUNGRY, v, v.food) + ve_say(base_st, vehicles_st, VEHICLE_HUNGRY, v, v.food) return false } - ve_cur = v + vehicles_st.ve_cur = v v.rider = 0 VehicleRider.board(v.x, v.z, v.yaw) - ve_say(VEHICLE_MOUNTED, v, 0.0) + ve_say(base_st, vehicles_st, VEHICLE_MOUNTED, v, 0.0) return true } # Off, onto the nearest spot a body could stand and walk off: from a boat a wade of up to 1.25 m # within ten metres, from a horse beside its right flank. False (and said) when a boat is too far out. -export function vehicle_dismount() -> bool { - let v = ve_cur +export function vehicle_dismount(base_st: mut BaseState, vehicles_st: mut VehiclesState) -> bool { + let v = vehicles_st.ve_cur if v == null { return false } if v.kind == VEHICLE_BOAT { if not VehicleRider.safe_spot(v.x, v.z, VehicleWorld.water(v.x, v.z), 1.25) { - ve_say(VEHICLE_NO_LANDING, v, 0.0) + ve_say(base_st, vehicles_st, VEHICLE_NO_LANDING, v, 0.0) return false } VehicleRider.step_off(VehicleRider.safe_x(), VehicleRider.safe_z()) @@ -37,18 +37,18 @@ export function vehicle_dismount() -> bool { v.home_x = v.x v.home_z = v.z } - ve_cur = null + vehicles_st.ve_cur = null v.rider = -1 v.speed = 0.0 - ve_say(VEHICLE_DISMOUNTED, v, 0.0) + ve_say(base_st, vehicles_st, VEHICLE_DISMOUNTED, v, 0.0) return true } # off whatever this player is on, wherever it is (a load, a collapse, a map change) -export function vehicle_drop() -> void { - if ve_cur != null { - ve_cur.rider = -1 - ve_cur.speed = 0.0 +export function vehicle_drop(vehicles_st: mut VehiclesState) -> void { + if vehicles_st.ve_cur != null { + vehicles_st.ve_cur.rider = -1 + vehicles_st.ve_cur.speed = 0.0 } - ve_cur = null + vehicles_st.ve_cur = null } diff --git a/packages/ludic.vehicles/places.ludic b/packages/ludic.vehicles/places.ludic index 3a0e3a2d..632c3f21 100644 --- a/packages/ludic.vehicles/places.ludic +++ b/packages/ludic.vehicles/places.ludic @@ -1,51 +1,51 @@ # places.ludic - four berths off the pier head and four bays at the trough. Nobody's boat waits in # one: a player calls theirs into a place that is empty. A place is taken while a boat lies in it or # a horse is tied at it, and frees itself the moment it is rowed or ridden away. -export function vehicles_dock_at(x: float, z: float, yaw: float) -> void { - ve_dock_x = x - ve_dock_z = z - ve_dock_yaw = yaw - ve_have_dock = true +export function vehicles_dock_at(vehicles_st: mut VehiclesState, x: float, z: float, yaw: float) -> void { + vehicles_st.ve_dock_x = x + vehicles_st.ve_dock_z = z + vehicles_st.ve_dock_yaw = yaw + vehicles_st.ve_have_dock = true } -export function vehicles_rail_at(x: float, z: float, yaw: float) -> void { - ve_rail_x = x - ve_rail_z = z - ve_rail_yaw = yaw +export function vehicles_rail_at(vehicles_st: mut VehiclesState, x: float, z: float, yaw: float) -> void { + vehicles_st.ve_rail_x = x + vehicles_st.ve_rail_z = z + vehicles_st.ve_rail_yaw = yaw } # which way the pier head faces, for the posts a game stands on it -export function vehicles_dock_yaw() -> float { return ve_dock_yaw } +export function vehicles_dock_yaw(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_dock_yaw } function ve_across(k: int) -> float { return -3.3 + float(k) * 2.2 } # a berth: 90 cm off the pier head's outer edge, bow out; its post on the deck above it -export function vehicle_berth_x(k: int) -> float { return ve_dock_x - Math.sin(ve_dock_yaw) * 7.25 + Math.cos(ve_dock_yaw) * ve_across(k) } -export function vehicle_berth_z(k: int) -> float { return ve_dock_z - Math.cos(ve_dock_yaw) * 7.25 - Math.sin(ve_dock_yaw) * ve_across(k) } -export function vehicle_post_x(k: int) -> float { return ve_dock_x - Math.sin(ve_dock_yaw) * 6.15 + Math.cos(ve_dock_yaw) * ve_across(k) } -export function vehicle_post_z(k: int) -> float { return ve_dock_z - Math.cos(ve_dock_yaw) * 6.15 - Math.sin(ve_dock_yaw) * ve_across(k) } +export function vehicle_berth_x(vehicles_st: VehiclesState, k: int) -> float { return vehicles_st.ve_dock_x - Math.sin(vehicles_st.ve_dock_yaw) * 7.25 + Math.cos(vehicles_st.ve_dock_yaw) * ve_across(k) } +export function vehicle_berth_z(vehicles_st: VehiclesState, k: int) -> float { return vehicles_st.ve_dock_z - Math.cos(vehicles_st.ve_dock_yaw) * 7.25 - Math.sin(vehicles_st.ve_dock_yaw) * ve_across(k) } +export function vehicle_post_x(vehicles_st: VehiclesState, k: int) -> float { return vehicles_st.ve_dock_x - Math.sin(vehicles_st.ve_dock_yaw) * 6.15 + Math.cos(vehicles_st.ve_dock_yaw) * ve_across(k) } +export function vehicle_post_z(vehicles_st: VehiclesState, k: int) -> float { return vehicles_st.ve_dock_z - Math.cos(vehicles_st.ve_dock_yaw) * 6.15 - Math.sin(vehicles_st.ve_dock_yaw) * ve_across(k) } # a bay: off the trough's long side, a trough's length apart - four horses at a feeding trough # stand shoulder to shoulder along it function ve_bay_across(k: int) -> float { return -1.44 + float(k) * 0.96 } -export function vehicle_bay_x(k: int) -> float { return ve_rail_x - Math.sin(ve_rail_yaw) * 1.4 + Math.cos(ve_rail_yaw) * ve_bay_across(k) } -export function vehicle_bay_z(k: int) -> float { return ve_rail_z - Math.cos(ve_rail_yaw) * 1.4 - Math.sin(ve_rail_yaw) * ve_bay_across(k) } +export function vehicle_bay_x(vehicles_st: VehiclesState, k: int) -> float { return vehicles_st.ve_rail_x - Math.sin(vehicles_st.ve_rail_yaw) * 1.4 + Math.cos(vehicles_st.ve_rail_yaw) * ve_bay_across(k) } +export function vehicle_bay_z(vehicles_st: VehiclesState, k: int) -> float { return vehicles_st.ve_rail_z - Math.cos(vehicles_st.ve_rail_yaw) * 1.4 - Math.sin(vehicles_st.ve_rail_yaw) * ve_bay_across(k) } -export function vehicle_place_x(kind: int, k: int) -> float { - if kind == VEHICLE_HORSE { return vehicle_bay_x(k) } - return vehicle_berth_x(k) +export function vehicle_place_x(vehicles_st: VehiclesState, kind: int, k: int) -> float { + if kind == VEHICLE_HORSE { return vehicle_bay_x(vehicles_st, k) } + return vehicle_berth_x(vehicles_st, k) } -export function vehicle_place_z(kind: int, k: int) -> float { - if kind == VEHICLE_HORSE { return vehicle_bay_z(k) } - return vehicle_berth_z(k) +export function vehicle_place_z(vehicles_st: VehiclesState, kind: int, k: int) -> float { + if kind == VEHICLE_HORSE { return vehicle_bay_z(vehicles_st, k) } + return vehicle_berth_z(vehicles_st, k) } # who is in place k: the boat lying in the berth or the horse tied in the bay, or null -export function vehicle_in_place(kind: int, k: int) -> Vehicle { - let px = vehicle_place_x(kind, k) - let pz = vehicle_place_z(kind, k) - let l = ve_all() +export function vehicle_in_place(vehicles_st: mut VehiclesState, kind: int, k: int) -> Vehicle { + let px = vehicle_place_x(vehicles_st, kind, k) + let pz = vehicle_place_z(vehicles_st, kind, k) + let l = ve_all(vehicles_st) for i in 0 .. len(l) { let v = l[i] if v.kind != kind or v.rider >= 0 { continue } @@ -64,17 +64,17 @@ export function vehicle_in_place(kind: int, k: int) -> Vehicle { } # the first free place, or -1 -export function vehicle_free_place(kind: int) -> int { - for k in 0 .. VEHICLE_PLACES { if vehicle_in_place(kind, k) == null { return k } } +export function vehicle_free_place(vehicles_st: mut VehiclesState, kind: int) -> int { + for k in 0 .. VEHICLE_PLACES { if vehicle_in_place(vehicles_st, kind, k) == null { return k } } return -1 } # this player's own is lying in one of the places -export function vehicle_mine_in_place(kind: int) -> bool { - let mine = vehicle_find(kind) +export function vehicle_mine_in_place(vehicles_st: mut VehiclesState, kind: int) -> bool { + let mine = vehicle_find(vehicles_st, kind) if mine == null { return false } for k in 0 .. VEHICLE_PLACES { - let h = vehicle_in_place(kind, k) + let h = vehicle_in_place(vehicles_st, kind, k) if h != null and h.nid == mine.nid { return true } } return false diff --git a/packages/ludic.vehicles/ports.ludic b/packages/ludic.vehicles/ports.ludic index ab64bab7..9ab2c2c2 100644 --- a/packages/ludic.vehicles/ports.ludic +++ b/packages/ludic.vehicles/ports.ludic @@ -23,25 +23,41 @@ export port VehicleRider { safe_z: fn() -> float = fn ve__safe_z } -var ve_ids: int = 0 -var ve_sx: float = 0.0 -var ve_sz: float = 0.0 +export state VehiclesState { + ve_kept_horse: bool = false + ve_kept_boat: bool = false + ve_kept_food: float = 40.0 + ve_ids: int = 0 + ve_sx: float = 0.0 + ve_sz: float = 0.0 + ve_list: []Vehicle = null + ve_cur: Vehicle = null + ve_dock_x: float = 0.0 + ve_dock_z: float = 0.0 + ve_dock_yaw: float = 0.0 + ve_rail_x: float = 0.0 + ve_rail_z: float = 0.0 + ve_rail_yaw: float = 0.0 + ve_have_dock: bool = false + ve_swell_said: float = 0.0 + ve_fact_q: Queue = null +} function ve__dry(x: float, z: float) -> float { return -10000.0 } function ve__open(x: float, z: float, r: float) -> bool { return false } function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 } function ve__zero() -> float { return 0.0 } -function ve__next() -> int { - ve_ids += 1 - return ve_ids +function ve__next(vehicles_st: mut VehiclesState) -> int { + vehicles_st.ve_ids += 1 + return vehicles_st.ve_ids } function ve__nothing(v: Vehicle) -> void { } function ve__board(x: float, z: float, yaw: float) -> void { } function ve__ride(x: float, y: float, z: float, yaw: float, speed: float) -> void { } function ve__off(x: float, z: float) -> void { } -function ve__safe(x: float, z: float, from_y: float, max_depth: float) -> bool { - ve_sx = x - ve_sz = z +function ve__safe(vehicles_st: mut VehiclesState, x: float, z: float, from_y: float, max_depth: float) -> bool { + vehicles_st.ve_sx = x + vehicles_st.ve_sz = z return true } -function ve__safe_x() -> float { return ve_sx } -function ve__safe_z() -> float { return ve_sz } +function ve__safe_x(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_sx } +function ve__safe_z(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_sz } diff --git a/packages/ludic.vehicles/ride.ludic b/packages/ludic.vehicles/ride.ludic index 554cdae5..e181b789 100644 --- a/packages/ludic.vehicles/ride.ludic +++ b/packages/ludic.vehicles/ride.ludic @@ -8,8 +8,8 @@ const VE_BOAT_TOP: float = 3.2 const VE_BOAT_ACC: float = 1.2 const VE_BOAT_TURN: float = 1.1 -export function vehicle_update(ix: float, iz: float, run: bool, dt: float) -> void { - let v = ve_cur +export function vehicle_update(base_st: mut BaseState, vehicles_st: mut VehiclesState, ix: float, iz: float, run: bool, dt: float) -> void { + let v = vehicles_st.ve_cur if v == null { return } var top = VE_BOAT_TOP var acc = VE_BOAT_ACC @@ -21,7 +21,7 @@ export function vehicle_update(ix: float, iz: float, run: bool, dt: float) -> vo if run and v.fuel > 5.0 { top = VE_HORSE_GALLOP } } v.speed = v.speed + Math.clamp(iz * top - v.speed, -acc * dt, acc * dt) - if v.kind == VEHICLE_BOAT { ve_swell(v, dt) } + if v.kind == VEHICLE_BOAT { ve_swell(base_st, vehicles_st, v, dt) } # Steering follows SIGNED speed: backing up with the stick left swings the nose right if Math.abs(v.speed) > 0.3 { var rate = Math.clamp(Math.abs(v.speed) / 3.0, 0.4, 1.0) @@ -29,14 +29,14 @@ export function vehicle_update(ix: float, iz: float, run: bool, dt: float) -> vo v.yaw = v.yaw - ix * turn * (dt * rate) } let d = v.speed * dt - ve_travel(v, v.x - Math.sin(v.yaw) * d, v.z - Math.cos(v.yaw) * d, d) + ve_travel(base_st, vehicles_st, v, v.x - Math.sin(v.yaw) * d, v.z - Math.cos(v.yaw) * d, d) if v.kind == VEHICLE_HORSE { ve_stamina(v, d, dt) } else { ve_float(v, d, dt) } VehicleWorld.placed(v) VehicleRider.ride_at(v.x, v.y + v.seat_h, v.z, v.yaw, Math.abs(v.speed)) } # a boat only where there is water under it; a horse only on dry ground, up a walkable rise, clear -function ve_travel(v: Vehicle, nx: float, nz: float, d: float) -> void { +function ve_travel(base_st: mut BaseState, vehicles_st: mut VehiclesState, v: Vehicle, nx: float, nz: float, d: float) -> void { let nh = VehicleWorld.ground(nx, nz) var ok = true if v.kind == VEHICLE_BOAT { ok = nh < VehicleWorld.water(nx, nz) - 0.35 } else { @@ -51,12 +51,12 @@ function ve_travel(v: Vehicle, nx: float, nz: float, d: float) -> void { v.x = nx v.z = nz if v.kind != VEHICLE_BOAT { v.y = nh } - if Math.abs(d) > 0.0 { ve_say(VEHICLE_MOVED, v, Math.abs(d)) } + if Math.abs(d) > 0.0 { ve_say(base_st, vehicles_st, VEHICLE_MOVED, v, Math.abs(d)) } } # a boat gets a longer leash than a swimmer and the same current at the end of it: the outward # part of the stroke only, so a boat at the limit can always be put about -function ve_swell(v: Vehicle, dt: float) -> void { +function ve_swell(base_st: mut BaseState, vehicles_st: mut VehiclesState, v: Vehicle, dt: float) -> void { var fx = -Math.sin(v.yaw) var fz = -Math.cos(v.yaw) if v.speed < 0.0 { @@ -65,11 +65,11 @@ function ve_swell(v: Vehicle, dt: float) -> void { } let take = Math.clamp(VehicleWorld.swell(v.x, v.z, fx, fz), 0.0, 1.0) v.speed = v.speed * (1.0 - take) - if take > 0.55 and ve_swell_said < 0.0 { - ve_swell_said = 14.0 - ve_say(VEHICLE_SWELL, v, take) + if take > 0.55 and vehicles_st.ve_swell_said < 0.0 { + vehicles_st.ve_swell_said = 14.0 + ve_say(base_st, vehicles_st, VEHICLE_SWELL, v, take) } - ve_swell_said = ve_swell_said - dt + vehicles_st.ve_swell_said = vehicles_st.ve_swell_said - dt } # a gallop spends stamina and a walk gives it back; every metre costs hay @@ -93,8 +93,8 @@ function ve_float(v: Vehicle, d: float, dt: float) -> void { } # the boats nobody is in ride the swell where they lie -export function vehicles_tick(dt: float) -> void { - let l = ve_all() +export function vehicles_tick(vehicles_st: mut VehiclesState, dt: float) -> void { + let l = ve_all(vehicles_st) for i in 0 .. len(l) { let v = l[i] if v.kind == VEHICLE_BOAT and v.rider < 0 { diff --git a/packages/ludic.vehicles/state.ludic b/packages/ludic.vehicles/state.ludic index e01418ab..dabb3dc3 100644 --- a/packages/ludic.vehicles/state.ludic +++ b/packages/ludic.vehicles/state.ludic @@ -1,24 +1,13 @@ # state.ludic - every vehicle, the one this machine's player is on, where the places are measured # from, and the facts -var ve_list: []Vehicle = null -var ve_cur: Vehicle = null -var ve_dock_x: float = 0.0 -var ve_dock_z: float = 0.0 -var ve_dock_yaw: float = 0.0 -var ve_rail_x: float = 0.0 -var ve_rail_z: float = 0.0 -var ve_rail_yaw: float = 0.0 -var ve_have_dock: bool = false -var ve_swell_said: float = 0.0 -var ve_fact_q: Queue = null const ve_pi: float = 3.14159265 -export function vehicle_facts() -> Queue { - if ve_fact_q == null { ve_fact_q = queue_new("vehicles.facts") } - return ve_fact_q +export function vehicle_facts(base_st: mut BaseState, vehicles_st: mut VehiclesState) -> Queue { + if vehicles_st.ve_fact_q == null { vehicles_st.ve_fact_q = queue_new(base_st, "vehicles.facts") } + return vehicles_st.ve_fact_q } -function ve_say(what: int, v: Vehicle, n: float) -> void { +function ve_say(base_st: mut BaseState, vehicles_st: mut VehiclesState, what: int, v: Vehicle, n: float) -> void { let f = new VehicleFact f.what = what f.nid = v.nid @@ -27,40 +16,40 @@ function ve_say(what: int, v: Vehicle, n: float) -> void { f.x = v.x f.z = v.z f.v = n - q_push(vehicle_facts(), f) + q_push(base_st, vehicle_facts(base_st, vehicles_st), f) } -function ve_all() -> []Vehicle { - if ve_list == null { ve_list = new []Vehicle } - return ve_list +function ve_all(vehicles_st: mut VehiclesState) -> []Vehicle { + if vehicles_st.ve_list == null { vehicles_st.ve_list = new []Vehicle } + return vehicles_st.ve_list } # every vehicle in the valley, to draw and to aim at (change one only through the verbs) -export function vehicles_all() -> []Vehicle { return ve_all() } +export function vehicles_all(vehicles_st: mut VehiclesState) -> []Vehicle { return ve_all(vehicles_st) } # the kind this machine's player is riding, 0 none -export function vehicle_ride_kind() -> int { - if ve_cur == null { return 0 } - return ve_cur.kind +export function vehicle_ride_kind(vehicles_st: VehiclesState) -> int { + if vehicles_st.ve_cur == null { return 0 } + return vehicles_st.ve_cur.kind } -export function vehicle_current() -> Vehicle { return ve_cur } +export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur } -export function vehicle_of(pid: int, kind: int) -> Vehicle { - let l = ve_all() +export function vehicle_of(vehicles_st: mut VehiclesState, pid: int, kind: int) -> Vehicle { + let l = ve_all(vehicles_st) for i in 0 .. len(l) { if l[i].kind == kind and l[i].owner == pid { return l[i] } } return null } # this player's own boat or horse -export function vehicle_find(kind: int) -> Vehicle { return vehicle_of(0, kind) } +export function vehicle_find(vehicles_st: mut VehiclesState, kind: int) -> Vehicle { return vehicle_of(vehicles_st, 0, kind) } # the nearest vehicle nobody is on within `reach` of (x, z); nothing while riding -export function vehicle_nearest(x: float, z: float, reach: float) -> Vehicle { - if ve_cur != null { return null } +export function vehicle_nearest(vehicles_st: mut VehiclesState, x: float, z: float, reach: float) -> Vehicle { + if vehicles_st.ve_cur != null { return null } var best: Vehicle = null var bd = reach - let l = ve_all() + let l = ve_all(vehicles_st) for i in 0 .. len(l) { let v = l[i] if v.rider >= 0 { continue } diff --git a/packages/ludic.vehicles/system.ludic b/packages/ludic.vehicles/system.ludic index c09bed89..98dcefc8 100644 --- a/packages/ludic.vehicles/system.ludic +++ b/packages/ludic.vehicles/system.ludic @@ -1,21 +1,21 @@ # system.ludic - vehicles as a system: a reset, and this player's own saved by kind (hired or not, # and the horse's hay); where they lie is not saved - they are called again. The idle boats' bob is # vehicles_tick, which the game calls where the world is run -export function vehicles_reset() -> void { - ve_list = new []Vehicle - ve_cur = null - ve_have_dock = false - ve_swell_said = 0.0 - ve_kept_horse = false - ve_kept_boat = false - ve_kept_food = 40.0 - q_clear(vehicle_facts()) +export function vehicles_reset(base_st: mut BaseState, vehicles_st: mut VehiclesState) -> void { + vehicles_st.ve_list = new []Vehicle + vehicles_st.ve_cur = null + vehicles_st.ve_have_dock = false + vehicles_st.ve_swell_said = 0.0 + vehicles_st.ve_kept_horse = false + vehicles_st.ve_kept_boat = false + vehicles_st.ve_kept_food = 40.0 + q_clear(base_st, vehicle_facts(base_st, vehicles_st)) } -export function vehicles_save() -> Val { +export function vehicles_save(vehicles_st: mut VehiclesState) -> Val { let v = Value.object() - let h = vehicle_find(VEHICLE_HORSE) - let b = vehicle_find(VEHICLE_BOAT) + let h = vehicle_find(vehicles_st, VEHICLE_HORSE) + let b = vehicle_find(vehicles_st, VEHICLE_BOAT) if h != null { sv_put_bool(v, "horse_own", h.owned) sv_put_float(v, "horse_food", h.food) @@ -24,10 +24,10 @@ export function vehicles_save() -> Val { return v } -export function vehicles_load(v: Val, version: int) -> void { - vehicle_drop() - vehicle_restore(VEHICLE_HORSE, sv_bool(v, "horse_own", false), sv_float(v, "horse_food", 40.0)) - vehicle_restore(VEHICLE_BOAT, sv_bool(v, "boat_own", false), 0.0) +export function vehicles_load(vehicles_st: mut VehiclesState, v: Val, version: int) -> void { + vehicle_drop(vehicles_st) + vehicle_restore(vehicles_st, VEHICLE_HORSE, sv_bool(v, "horse_own", false), sv_float(v, "horse_food", 40.0)) + vehicle_restore(vehicles_st, VEHICLE_BOAT, sv_bool(v, "boat_own", false), 0.0) } export function vehicles_system() -> System { diff --git a/packages/ludic.vehicles/tests/vehicles_test.ludic b/packages/ludic.vehicles/tests/vehicles_test.ludic index 3a9f7927..2a6ba21a 100644 --- a/packages/ludic.vehicles/tests/vehicles_test.ludic +++ b/packages/ludic.vehicles/tests/vehicles_test.ludic @@ -16,222 +16,224 @@ program VehiclesTest { return -10000.0 } function fk_blocked(x: float, z: float, r: float) -> bool { return Math.abs(x) < 1.0 + r and Math.abs(z + 60.0) < 1.0 + r } - var swell: float = 0.0 - function fk_swell(x: float, z: float, fx: float, fz: float) -> float { - if fx > 0.0 { return swell } + state VehiclestestState { + swell: float = 0.0 + wind: float = 0.0 + made: int = 0 + gone: int = 0 + placed: int = 0 + rx: float = 0.0 + ry: float = 0.0 + rz: float = 0.0 + boarded: int = 0 + sx: float = 0.0 + sz: float = 0.0 + } + function fk_swell(vehiclestest_st: VehiclestestState, x: float, z: float, fx: float, fz: float) -> float { + if fx > 0.0 { return vehiclestest_st.swell } return 0.0 } - var wind: float = 0.0 - function fk_wind() -> float { return wind } - var made: int = 0 - var gone: int = 0 - var placed: int = 0 - function fk_made(v: Vehicle) -> void { made += 1 } - function fk_gone(v: Vehicle) -> void { gone += 1 } - function fk_placed(v: Vehicle) -> void { placed += 1 } + function fk_wind(vehiclestest_st: VehiclestestState) -> float { return vehiclestest_st.wind } + function fk_made(vehiclestest_st: mut VehiclestestState, v: Vehicle) -> void { vehiclestest_st.made += 1 } + function fk_gone(vehiclestest_st: mut VehiclestestState, v: Vehicle) -> void { vehiclestest_st.gone += 1 } + function fk_placed(vehiclestest_st: mut VehiclestestState, v: Vehicle) -> void { vehiclestest_st.placed += 1 } # the rider: where it was put, and a safe spot that walks back toward the shore - var rx: float = 0.0 - var ry: float = 0.0 - var rz: float = 0.0 - var boarded: int = 0 - var sx: float = 0.0 - var sz: float = 0.0 - function fk_board(x: float, z: float, yaw: float) -> void { boarded += 1 } - function fk_ride(x: float, y: float, z: float, yaw: float, speed: float) -> void { - rx = x - ry = y - rz = z + function fk_board(vehiclestest_st: mut VehiclestestState, x: float, z: float, yaw: float) -> void { vehiclestest_st.boarded += 1 } + function fk_ride(vehiclestest_st: mut VehiclestestState, x: float, y: float, z: float, yaw: float, speed: float) -> void { + vehiclestest_st.rx = x + vehiclestest_st.ry = y + vehiclestest_st.rz = z } - function fk_off(x: float, z: float) -> void { - rx = x - rz = z - ry = fk_ground(x, z) + function fk_off(vehiclestest_st: mut VehiclestestState, x: float, z: float) -> void { + vehiclestest_st.rx = x + vehiclestest_st.rz = z + vehiclestest_st.ry = fk_ground(x, z) } - function fk_safe(x: float, z: float, from_y: float, max_depth: float) -> bool { + function fk_safe(vehiclestest_st: mut VehiclestestState, x: float, z: float, from_y: float, max_depth: float) -> bool { for s in 0 .. 11 { let px = x - float(s) let depth = fk_water(px, z) - fk_ground(px, z) if not (depth > max_depth) { - sx = px - sz = z + vehiclestest_st.sx = px + vehiclestest_st.sz = z return true } } return false } - function fk_sx() -> float { return sx } - function fk_sz() -> float { return sz } + function fk_sx(vehiclestest_st: VehiclestestState) -> float { return vehiclestest_st.sx } + function fk_sz(vehiclestest_st: VehiclestestState) -> float { return vehiclestest_st.sz } bind VehicleWorld { ground: fn fk_ground, water: fn fk_water, blocked: fn fk_blocked, swell: fn fk_swell, wind: fn fk_wind, made: fn fk_made, gone: fn fk_gone, placed: fn fk_placed } bind VehicleRider { board: fn fk_board, ride_at: fn fk_ride, step_off: fn fk_off, safe_spot: fn fk_safe, safe_x: fn fk_sx, safe_z: fn fk_sz } const EAST: float = -1.5707963 - function fresh() -> void { - vehicles_reset() - vehicles_dock_at(10.0, 0.0, EAST) - vehicles_rail_at(0.0, 5.0, 0.0) - made = 0 - gone = 0 - swell = 0.0 - wind = 0.0 + function fresh(base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) -> void { + vehicles_reset(base_st, vehicles_st) + vehicles_dock_at(vehicles_st, 10.0, 0.0, EAST) + vehicles_rail_at(vehicles_st, 0.0, 5.0, 0.0) + vehiclestest_st.made = 0 + vehiclestest_st.gone = 0 + vehiclestest_st.swell = 0.0 + vehiclestest_st.wind = 0.0 } - function facts() -> []VehicleFact { return q_drain(vehicle_facts()) } + function facts(base_st: mut BaseState, vehicles_st: mut VehiclesState) -> []VehicleFact { return q_drain(base_st, vehicle_facts(base_st, vehicles_st)) } function count(fs: []VehicleFact, what: int) -> int { var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } - function ride(ix: float, iz: float, run: bool, secs: float) -> void { - for i in 0 .. int(secs * 60.0) { vehicle_update(ix, iz, run, 1.0 / 60.0) } + function ride(base_st: mut BaseState, vehicles_st: mut VehiclesState, ix: float, iz: float, run: bool, secs: float) -> void { + for i in 0 .. int(secs * 60.0) { vehicle_update(base_st, vehicles_st, ix, iz, run, 1.0 / 60.0) } } - test "a berth is off the pier head in the water, a bay on the meadow" { - fresh() - expect_near(vehicle_berth_x(0), 17.25, 0.01) - expect_near(vehicle_berth_z(0), -3.3, 0.01) - expect_near(vehicle_post_x(3), 16.15, 0.01) - expect_near(vehicle_bay_z(2), 3.6, 0.01) + test "a berth is off the pier head in the water, a bay on the meadow" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + expect_near(vehicle_berth_x(vehicles_st, 0), 17.25, 0.01) + expect_near(vehicle_berth_z(vehicles_st, 0), -3.3, 0.01) + expect_near(vehicle_post_x(vehicles_st, 3), 16.15, 0.01) + expect_near(vehicle_bay_z(vehicles_st, 2), 3.6, 0.01) } - test "the first call makes a vehicle, a later one fetches it, and a taken place refuses" { - fresh() - expect(vehicle_call_for(0, VEHICLE_BOAT, 0)) - expect_eq(made, 1) - let b = vehicle_find(VEHICLE_BOAT) + test "the first call makes a vehicle, a later one fetches it, and a taken place refuses" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + expect(vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0)) + expect_eq(vehiclestest_st.made, 1) + let b = vehicle_find(vehicles_st, VEHICLE_BOAT) expect(b != null) expect_near(b.y, 0.02, 0.001) - expect(vehicle_call_for(0, VEHICLE_BOAT, 0)) - expect_eq(made, 1) - expect(not vehicle_call_for(2, VEHICLE_BOAT, 0)) - expect(vehicle_call_for(2, VEHICLE_BOAT, 1)) - expect_eq(made, 2) - expect(vehicle_call_for(0, VEHICLE_BOAT, 2)) - expect(vehicle_in_place(VEHICLE_BOAT, 0) == null) - expect_eq(vehicle_free_place(VEHICLE_BOAT), 0) - expect(vehicle_mine_in_place(VEHICLE_BOAT)) + expect(vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0)) + expect_eq(vehiclestest_st.made, 1) + expect(not vehicle_call_for(vehicles_st, 2, VEHICLE_BOAT, 0)) + expect(vehicle_call_for(vehicles_st, 2, VEHICLE_BOAT, 1)) + expect_eq(vehiclestest_st.made, 2) + expect(vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 2)) + expect(vehicle_in_place(vehicles_st, VEHICLE_BOAT, 0) == null) + expect_eq(vehicle_free_place(vehicles_st, VEHICLE_BOAT), 0) + expect(vehicle_mine_in_place(vehicles_st, VEHICLE_BOAT)) } - test "a player leaving takes theirs away; this machine's player never leaves" { - fresh() - vehicle_call_for(0, VEHICLE_HORSE, 0) - vehicle_call_for(3, VEHICLE_HORSE, 1) - vehicles_release(0) - expect_eq(len(vehicles_all()), 2) - vehicles_release(3) - expect_eq(len(vehicles_all()), 1) - expect_eq(gone, 1) - expect(vehicle_in_place(VEHICLE_HORSE, 1) == null) + test "a player leaving takes theirs away; this machine's player never leaves" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + vehicle_call_for(vehicles_st, 3, VEHICLE_HORSE, 1) + vehicles_release(vehicles_st, 0) + expect_eq(len(vehicles_all(vehicles_st)), 2) + vehicles_release(vehicles_st, 3) + expect_eq(len(vehicles_all(vehicles_st)), 1) + expect_eq(vehiclestest_st.gone, 1) + expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 1) == null) } - test "a hungry horse will not go; a fed one is mounted, and only by its owner" { - fresh() - vehicle_call_for(0, VEHICLE_HORSE, 0) - vehicle_call_for(4, VEHICLE_HORSE, 1) - let h = vehicle_find(VEHICLE_HORSE) + test "a hungry horse will not go; a fed one is mounted, and only by its owner" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + vehicle_call_for(vehicles_st, 4, VEHICLE_HORSE, 1) + let h = vehicle_find(vehicles_st, VEHICLE_HORSE) h.food = 5.0 - expect(not vehicle_mount(h)) - expect_eq(count(facts(), VEHICLE_HUNGRY), 1) - vehicle_feed(h, 50.0) + expect(not vehicle_mount(base_st, vehicles_st, h)) + expect_eq(count(facts(base_st, vehicles_st), VEHICLE_HUNGRY), 1) + vehicle_feed(base_st, vehicles_st, h, 50.0) expect_near(h.food, 55.0, 0.001) - expect(not vehicle_mount(vehicle_of(4, VEHICLE_HORSE))) - boarded = 0 - expect(vehicle_mount(h)) - expect_eq(boarded, 1) - expect_eq(vehicle_ride_kind(), VEHICLE_HORSE) + expect(not vehicle_mount(base_st, vehicles_st, vehicle_of(vehicles_st, 4, VEHICLE_HORSE))) + vehiclestest_st.boarded = 0 + expect(vehicle_mount(base_st, vehicles_st, h)) + expect_eq(vehiclestest_st.boarded, 1) + expect_eq(vehicle_ride_kind(vehicles_st), VEHICLE_HORSE) expect_eq(h.rider, 0) - expect(vehicle_nearest(h.x, h.z, 5.0) == null) - expect(vehicle_in_place(VEHICLE_HORSE, 0) == null) + expect(vehicle_nearest(vehicles_st, h.x, h.z, 5.0) == null) + expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 0) == null) } - test "a horse walks, gallops on stamina, stops at the water and at a trunk" { - fresh() - vehicle_call_for(0, VEHICLE_HORSE, 0) - let h = vehicle_find(VEHICLE_HORSE) - vehicle_mount(h) - ride(0.0, 1.0, false, 3.0) + test "a horse walks, gallops on stamina, stops at the water and at a trunk" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + let h = vehicle_find(vehicles_st, VEHICLE_HORSE) + vehicle_mount(base_st, vehicles_st, h) + ride(base_st, vehicles_st, 0.0, 1.0, false, 3.0) expect_near(h.speed, 4.5, 0.05) - expect(count(facts(), VEHICLE_MOVED) > 100) - ride(0.0, 1.0, true, 3.0) + expect(count(facts(base_st, vehicles_st), VEHICLE_MOVED) > 100) + ride(base_st, vehicles_st, 0.0, 1.0, true, 3.0) expect_near(h.speed, 11.0, 0.1) expect(h.fuel < 100.0) expect(h.z < -15.0) - expect_near(rz, h.z, 0.001) - expect_near(ry, 1.35, 0.001) + expect_near(vehiclestest_st.rz, h.z, 0.001) + expect_near(vehiclestest_st.ry, 1.35, 0.001) # the trunk at z = -60 stops it - ride(0.0, 1.0, true, 6.0) + ride(base_st, vehicles_st, 0.0, 1.0, true, 6.0) expect(h.z > -60.0 + 1.7) expect_near(h.speed, 0.0, 0.001) # and the lake to the east does too - vehicle_drop() + vehicle_drop(vehicles_st) h.x = 7.0 h.z = 0.0 h.yaw = EAST - vehicle_mount(h) - ride(0.0, 1.0, false, 4.0) + vehicle_mount(base_st, vehicles_st, h) + ride(base_st, vehicles_st, 0.0, 1.0, false, 4.0) expect(h.x < 9.6) } - test "a boat rows on water, stops at the shore, loses its stroke to the swell, drifts on the wind" { - fresh() - vehicle_call_for(0, VEHICLE_BOAT, 0) - let b = vehicle_find(VEHICLE_BOAT) - vehicle_mount(b) - ride(0.0, 1.0, false, 2.0) + test "a boat rows on water, stops at the shore, loses its stroke to the swell, drifts on the wind" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0) + let b = vehicle_find(vehicles_st, VEHICLE_BOAT) + vehicle_mount(base_st, vehicles_st, b) + ride(base_st, vehicles_st, 0.0, 1.0, false, 2.0) let x2 = b.x expect(x2 > 18.0) - swell = 1.0 - ride(0.0, 1.0, false, 1.0) + vehiclestest_st.swell = 1.0 + ride(base_st, vehicles_st, 0.0, 1.0, false, 1.0) expect_near(b.x, x2, 0.2) - expect_eq(count(facts(), VEHICLE_SWELL), 1) - swell = 0.0 - ride(0.0, -1.0, false, 12.0) + expect_eq(count(facts(base_st, vehicles_st), VEHICLE_SWELL), 1) + vehiclestest_st.swell = 0.0 + ride(base_st, vehicles_st, 0.0, -1.0, false, 12.0) expect(b.x > 10.3) - wind = 1.0 + vehiclestest_st.wind = 1.0 let z0 = b.z - ride(0.0, 0.0, false, 1.0) + ride(base_st, vehicles_st, 0.0, 0.0, false, 1.0) expect(b.z < z0) } - test "getting off a boat needs a shore near enough; a horse is left tied where it stands" { - fresh() - vehicle_call_for(0, VEHICLE_BOAT, 0) - let b = vehicle_find(VEHICLE_BOAT) - vehicle_mount(b) + test "getting off a boat needs a shore near enough; a horse is left tied where it stands" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0) + let b = vehicle_find(vehicles_st, VEHICLE_BOAT) + vehicle_mount(base_st, vehicles_st, b) b.x = 40.0 - expect(not vehicle_dismount()) - expect_eq(count(facts(), VEHICLE_NO_LANDING), 1) - expect_eq(vehicle_ride_kind(), VEHICLE_BOAT) + expect(not vehicle_dismount(base_st, vehicles_st)) + expect_eq(count(facts(base_st, vehicles_st), VEHICLE_NO_LANDING), 1) + expect_eq(vehicle_ride_kind(vehicles_st), VEHICLE_BOAT) b.x = 12.0 - expect(vehicle_dismount()) - expect_near(rx, 11.0, 0.001) - expect_eq(vehicle_ride_kind(), 0) + expect(vehicle_dismount(base_st, vehicles_st)) + expect_near(vehiclestest_st.rx, 11.0, 0.001) + expect_eq(vehicle_ride_kind(vehicles_st), 0) expect_eq(b.rider, -1) - vehicle_call_for(0, VEHICLE_HORSE, 0) - let h = vehicle_find(VEHICLE_HORSE) - vehicle_mount(h) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + let h = vehicle_find(vehicles_st, VEHICLE_HORSE) + vehicle_mount(base_st, vehicles_st, h) h.x = -5.0 h.z = -5.0 - expect(vehicle_dismount()) + expect(vehicle_dismount(base_st, vehicles_st)) expect_near(h.home_x, -5.0, 0.001) - expect(vehicle_in_place(VEHICLE_HORSE, 0) == null) - expect_near(rx, -5.0 + 1.2, 0.001) + expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 0) == null) + expect_near(vehiclestest_st.rx, -5.0 + 1.2, 0.001) } - test "this player's own are saved by kind, and a save read before they are made still applies" { - fresh() - vehicle_call_for(0, VEHICLE_HORSE, 0) - vehicle_call_for(0, VEHICLE_BOAT, 0) - vehicle_own(vehicle_find(VEHICLE_HORSE)) - vehicle_find(VEHICLE_HORSE).food = 77.0 - let saved = vehicles_save() - fresh() - vehicles_load(saved, 1) - vehicle_call_for(0, VEHICLE_HORSE, 0) - vehicle_call_for(0, VEHICLE_BOAT, 0) - expect(vehicle_find(VEHICLE_HORSE).owned) - expect_near(vehicle_find(VEHICLE_HORSE).food, 77.0, 0.01) - expect(not vehicle_find(VEHICLE_BOAT).owned) + test "this player's own are saved by kind, and a save read before they are made still applies" (base_st: mut BaseState, vehicles_st: mut VehiclesState, vehiclestest_st: mut VehiclestestState) { + fresh(base_st, vehicles_st, vehiclestest_st) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0) + vehicle_own(vehicle_find(vehicles_st, VEHICLE_HORSE)) + vehicle_find(vehicles_st, VEHICLE_HORSE).food = 77.0 + let saved = vehicles_save(vehicles_st) + fresh(base_st, vehicles_st, vehiclestest_st) + vehicles_load(vehicles_st, saved, 1) + vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0) + vehicle_call_for(vehicles_st, 0, VEHICLE_BOAT, 0) + expect(vehicle_find(vehicles_st, VEHICLE_HORSE).owned) + expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01) + expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned) } } diff --git a/packages/ludic.wallet/tests/wallet_test.ludic b/packages/ludic.wallet/tests/wallet_test.ludic index abb880ad..6c10b285 100644 --- a/packages/ludic.wallet/tests/wallet_test.ludic +++ b/packages/ludic.wallet/tests/wallet_test.ludic @@ -5,56 +5,56 @@ import "ludic.base" program WalletTest { numbers float - function changes() -> []MoneyChanged { return q_drain(wallet_changes()) } - function waiting() -> int { return q_len(wallet_changes()) } + function changes(base_st: mut BaseState, wallet_st: mut WalletState) -> []MoneyChanged { return q_drain(base_st, wallet_changes(base_st, wallet_st)) } + function waiting(base_st: mut BaseState, wallet_st: mut WalletState) -> int { return q_len(wallet_changes(base_st, wallet_st)) } - test "earning and spending move the purse, and each says so" { - wallet_reset() - wallet_earn(40) - expect(wallet_spend(15)) - expect_eq(wallet_money(), 25) - let cs = changes() + test "earning and spending move the purse, and each says so" (base_st: mut BaseState, wallet_st: mut WalletState) { + wallet_reset(base_st, wallet_st) + wallet_earn(base_st, wallet_st, 40) + expect(wallet_spend(base_st, wallet_st, 15)) + expect_eq(wallet_money(wallet_st), 25) + let cs = changes(base_st, wallet_st) expect_eq(len(cs), 2) expect_eq(cs[0].after, 40) expect_eq(cs[1].before, 40) expect_eq(cs[1].after, 25) } - test "a spend past what is there is refused and changes nothing" { - wallet_reset() - wallet_set(10) - changes() - expect(not wallet_spend(11)) - expect_eq(wallet_money(), 10) - expect_eq(waiting(), 0) - expect(wallet_can(10)) - expect(not wallet_can(11)) + test "a spend past what is there is refused and changes nothing" (base_st: mut BaseState, wallet_st: mut WalletState) { + wallet_reset(base_st, wallet_st) + wallet_set(base_st, wallet_st, 10) + changes(base_st, wallet_st) + expect(not wallet_spend(base_st, wallet_st, 11)) + expect_eq(wallet_money(wallet_st), 10) + expect_eq(waiting(base_st, wallet_st), 0) + expect(wallet_can(wallet_st, 10)) + expect(not wallet_can(wallet_st, 11)) } - test "a loss happens anyway, but never below zero" { - wallet_reset() - wallet_set(7) - wallet_lose(20) - expect_eq(wallet_money(), 0) + test "a loss happens anyway, but never below zero" (base_st: mut BaseState, wallet_st: mut WalletState) { + wallet_reset(base_st, wallet_st) + wallet_set(base_st, wallet_st, 7) + wallet_lose(base_st, wallet_st, 20) + expect_eq(wallet_money(wallet_st), 0) } - test "setting what is already there is no change" { - wallet_reset() - wallet_set(5) - changes() - wallet_set(5) - expect_eq(waiting(), 0) + test "setting what is already there is no change" (base_st: mut BaseState, wallet_st: mut WalletState) { + wallet_reset(base_st, wallet_st) + wallet_set(base_st, wallet_st, 5) + changes(base_st, wallet_st) + wallet_set(base_st, wallet_st, 5) + expect_eq(waiting(base_st, wallet_st), 0) } - test "a save section reads back the same purse" { - wallet_reset() - wallet_set(123) + test "a save section reads back the same purse" (base_st: mut BaseState, wallet_st: mut WalletState) { + wallet_reset(base_st, wallet_st) + wallet_set(base_st, wallet_st, 123) let root = save_tree() - save_section(root, "wallet", 1, wallet_save()) + save_section(root, "wallet", 1, wallet_save(wallet_st)) let text = save_encode(root) - wallet_reset() + wallet_reset(base_st, wallet_st) let n = load_section(save_decode(text), "wallet") - wallet_load(n.data, n.version) - expect_eq(wallet_money(), 123) + wallet_load(base_st, wallet_st, n.data, n.version) + expect_eq(wallet_money(wallet_st), 123) } } diff --git a/packages/ludic.wallet/wallet.ludic b/packages/ludic.wallet/wallet.ludic index 9a8162f2..18aaa578 100644 --- a/packages/ludic.wallet/wallet.ludic +++ b/packages/ludic.wallet/wallet.ludic @@ -4,53 +4,55 @@ export property MoneyChanged { after: int = 0 } -var wl_money: int = 0 -var wl_changes: Queue = null - -export function wallet_changes() -> Queue { - if wl_changes == null { wl_changes = queue_new("wallet.changes") } - return wl_changes +export state WalletState { + wl_money: int = 0 + wl_changes: Queue = null } -export function wallet_money() -> int { return wl_money } -export function wallet_can(n: int) -> bool { return wl_money >= n } +export function wallet_changes(base_st: mut BaseState, wallet_st: mut WalletState) -> Queue { + if wallet_st.wl_changes == null { wallet_st.wl_changes = queue_new(base_st, "wallet.changes") } + return wallet_st.wl_changes +} + +export function wallet_money(wallet_st: WalletState) -> int { return wallet_st.wl_money } +export function wallet_can(wallet_st: WalletState, n: int) -> bool { return wallet_st.wl_money >= n } # the purse holds exactly n (a starting purse, a correction, the network's word) -export function wallet_set(n: int) -> void { - if n == wl_money { return } +export function wallet_set(base_st: mut BaseState, wallet_st: mut WalletState, n: int) -> void { + if n == wallet_st.wl_money { return } let c = new MoneyChanged - c.before = wl_money + c.before = wallet_st.wl_money c.after = n - wl_money = n - q_push(wallet_changes(), c) + wallet_st.wl_money = n + q_push(base_st, wallet_changes(base_st, wallet_st), c) } -export function wallet_earn(n: int) -> void { wallet_set(wl_money + n) } +export function wallet_earn(base_st: mut BaseState, wallet_st: mut WalletState, n: int) -> void { wallet_set(base_st, wallet_st, wallet_st.wl_money + n) } # only what is there: false, and nothing spent, when it is not -export function wallet_spend(n: int) -> bool { - if n > wl_money { return false } - wallet_set(wl_money - n) +export function wallet_spend(base_st: mut BaseState, wallet_st: mut WalletState, n: int) -> bool { + if n > wallet_st.wl_money { return false } + wallet_set(base_st, wallet_st, wallet_st.wl_money - n) return true } # a cost that happens whether or not it can be paid (a rescue, a fine); never below zero -export function wallet_lose(n: int) -> void { wallet_set(Math.max(wl_money - n, 0)) } +export function wallet_lose(base_st: mut BaseState, wallet_st: mut WalletState, n: int) -> void { wallet_set(base_st, wallet_st, Math.max(wallet_st.wl_money - n, 0)) } -export function wallet_reset() -> void { - wl_money = 0 - q_clear(wallet_changes()) +export function wallet_reset(base_st: mut BaseState, wallet_st: mut WalletState) -> void { + wallet_st.wl_money = 0 + q_clear(base_st, wallet_changes(base_st, wallet_st)) } -export function wallet_save() -> Val { +export function wallet_save(wallet_st: WalletState) -> Val { let v = Value.object() - sv_put_int(v, "money", wl_money) + sv_put_int(v, "money", wallet_st.wl_money) return v } -export function wallet_load(v: Val, version: int) -> void { - wl_money = sv_int(v, "money", 0) - q_clear(wallet_changes()) +export function wallet_load(base_st: mut BaseState, wallet_st: mut WalletState, v: Val, version: int) -> void { + wallet_st.wl_money = sv_int(v, "money", 0) + q_clear(base_st, wallet_changes(base_st, wallet_st)) } export function wallet_system() -> System { diff --git a/packages/ludic.weather/bias.ludic b/packages/ludic.weather/bias.ludic index aa3aa2c6..1295e241 100644 --- a/packages/ludic.weather/bias.ludic +++ b/packages/ludic.weather/bias.ludic @@ -5,38 +5,38 @@ function wx_front_kind() -> int { return WeatherWorld.front() } -function wx_want_wet() -> void { - if wx_kinds[wx_state].wet or wx_forced { return } +function wx_want_wet(base_st: mut BaseState, weather_st: mut WeatherState) -> void { + if weather_st.wx_kinds[weather_st.wx_state].wet or weather_st.wx_forced { return } let front = wx_front_kind() - wx_next = front - wx_forecast = front - wx_forced = true - if wx_left > 1.5 { wx_left = 1.5 } - wx_fact(WEATHER_FRONT, front, wx_state) + weather_st.wx_next = front + weather_st.wx_forecast = front + weather_st.wx_forced = true + if weather_st.wx_left > 1.5 { weather_st.wx_left = 1.5 } + wx_fact(base_st, weather_st, WEATHER_FRONT, front, weather_st.wx_state) } -function wx_hold() -> void { - if wx_left < 1.2 { - wx_left = 1.2 - wx_next = wx_state +function wx_hold(weather_st: mut WeatherState) -> void { + if weather_st.wx_left < 1.2 { + weather_st.wx_left = 1.2 + weather_st.wx_next = weather_st.wx_state } } -function wx_want_clear() -> void { - let clear = wx_clear_kind() - if wx_state != clear and not wx_forced { - if wx_left > 0.5 { wx_left = 0.5 } - wx_next = clear +function wx_want_clear(weather_st: mut WeatherState) -> void { + let clear = wx_clear_kind(weather_st) + if weather_st.wx_state != clear and not weather_st.wx_forced { + if weather_st.wx_left > 0.5 { weather_st.wx_left = 0.5 } + weather_st.wx_next = clear } - if wx_state == clear and wx_left < 1.0 { - wx_left = 1.0 - wx_next = clear + if weather_st.wx_state == clear and weather_st.wx_left < 1.0 { + weather_st.wx_left = 1.0 + weather_st.wx_next = clear } } -function wx_apply_bias() -> void { +function wx_apply_bias(base_st: mut BaseState, weather_st: mut WeatherState) -> void { let b = wx_bias() - if b == WEATHER_WANT_WET { wx_want_wet() } - if b == WEATHER_HOLD { wx_hold() } - if b == WEATHER_WANT_CLEAR { wx_want_clear() } + if b == WEATHER_WANT_WET { wx_want_wet(base_st, weather_st) } + if b == WEATHER_HOLD { wx_hold(weather_st) } + if b == WEATHER_WANT_CLEAR { wx_want_clear(weather_st) } } diff --git a/packages/ludic.weather/config.ludic b/packages/ludic.weather/config.ludic index 76e7de50..6f7a8827 100644 --- a/packages/ludic.weather/config.ludic +++ b/packages/ludic.weather/config.ludic @@ -1,34 +1,30 @@ # config.ludic - the kinds the game hands in, the wind's daily turn, and the port's defaults -var wx_down_dir: float = 0.0 # the wind overnight and into the morning -var wx_up_dir: float = 3.1415927 # the wind while the slopes are warm -var wx_up_from: float = 10.0 -var wx_up_to: float = 19.0 # the kinds, in the game's order; the first `clear` one is where a new trip starts -export function weather_kinds(kinds: []WeatherKind) -> void { wx_kinds = kinds } +export function weather_kinds(weather_st: mut WeatherState, kinds: []WeatherKind) -> void { weather_st.wx_kinds = kinds } -export function weather_kind_count() -> int { - if wx_kinds == null { return 0 } - return len(wx_kinds) +export function weather_kind_count(weather_st: WeatherState) -> int { + if weather_st.wx_kinds == null { return 0 } + return len(weather_st.wx_kinds) } -export function weather_kind(k: int) -> WeatherKind { return wx_kinds[k] } +export function weather_kind(weather_st: WeatherState, k: int) -> WeatherKind { return weather_st.wx_kinds[k] } # a valley's wind: down it by night, up it from `up_from` to `up_to` while the slopes are warm -export function weather_config_wind(down_dir: float, up_dir: float, up_from: float, up_to: float) -> void { - wx_down_dir = down_dir - wx_up_dir = up_dir - wx_up_from = up_from - wx_up_to = up_to +export function weather_config_wind(weather_st: mut WeatherState, down_dir: float, up_dir: float, up_from: float, up_to: float) -> void { + weather_st.wx_down_dir = down_dir + weather_st.wx_up_dir = up_dir + weather_st.wx_up_from = up_from + weather_st.wx_up_to = up_to } -function wx_clear_kind() -> int { - for k in 0 .. weather_kind_count() { if wx_kinds[k].clear { return k } } +function wx_clear_kind(weather_st: WeatherState) -> int { + for k in 0 .. weather_kind_count(weather_st) { if weather_st.wx_kinds[k].clear { return k } } return 0 } -function wx_first_wet() -> int { - for k in 0 .. weather_kind_count() { if wx_kinds[k].wet { return k } } +function wx_first_wet(weather_st: WeatherState) -> int { + for k in 0 .. weather_kind_count(weather_st) { if weather_st.wx_kinds[k].wet { return k } } return 0 } diff --git a/packages/ludic.weather/pick.ludic b/packages/ludic.weather/pick.ludic index d53ea6bd..d28a798a 100644 --- a/packages/ludic.weather/pick.ludic +++ b/packages/ludic.weather/pick.ludic @@ -20,11 +20,11 @@ export function weather_draw_from(w: []float, from: int, roll: float) -> int { return len(w) - 1 } -function wx_pick_next(from: int) -> int { - return weather_draw_from(wx_odds(from), from, rng_float(wx_dice)) +function wx_pick_next(weather_st: WeatherState, from: int) -> int { + return weather_draw_from(wx_odds(from), from, rng_float(weather_st.wx_dice)) } -function wx_spell_hours(k: int) -> float { - let kd = wx_kinds[k] - return rng_span(wx_dice, kd.spell_lo, kd.spell_hi) +function wx_spell_hours(weather_st: WeatherState, k: int) -> float { + let kd = weather_st.wx_kinds[k] + return rng_span(weather_st.wx_dice, kd.spell_lo, kd.spell_hi) } diff --git a/packages/ludic.weather/queries.ludic b/packages/ludic.weather/queries.ludic index e221eb09..c29ec8b1 100644 --- a/packages/ludic.weather/queries.ludic +++ b/packages/ludic.weather/queries.ludic @@ -1,38 +1,38 @@ # queries.ludic - what the world asks the sky -export function weather_state() -> int { return wx_state } -export function weather_next() -> int { return wx_next } -export function weather_left() -> float { return wx_left } -export function weather_amount() -> float { return wx_amount } -export function weather_forecast() -> int { return wx_forecast } -export function weather_forced() -> bool { return wx_forced } -export function weather_wind() -> float { return wx_wind } -export function weather_dir() -> float { return wx_dir } -export function weather_ground_wet() -> float { return wx_ground } -export function weather_flash() -> float { return wx_flash } +export function weather_state(weather_st: WeatherState) -> int { return weather_st.wx_state } +export function weather_next(weather_st: WeatherState) -> int { return weather_st.wx_next } +export function weather_left(weather_st: WeatherState) -> float { return weather_st.wx_left } +export function weather_amount(weather_st: WeatherState) -> float { return weather_st.wx_amount } +export function weather_forecast(weather_st: WeatherState) -> int { return weather_st.wx_forecast } +export function weather_forced(weather_st: WeatherState) -> bool { return weather_st.wx_forced } +export function weather_wind(weather_st: WeatherState) -> float { return weather_st.wx_wind } +export function weather_dir(weather_st: WeatherState) -> float { return weather_st.wx_dir } +export function weather_ground_wet(weather_st: WeatherState) -> float { return weather_st.wx_ground } +export function weather_flash(weather_st: WeatherState) -> float { return weather_st.wx_flash } # rain or snow is falling, and more than a spit of it -export function weather_wet() -> bool { - if weather_kind_count() == 0 { return false } - return wx_kinds[wx_state].wet and wx_amount > 0.3 +export function weather_wet(weather_st: WeatherState) -> bool { + if weather_kind_count(weather_st) == 0 { return false } + return weather_st.wx_kinds[weather_st.wx_state].wet and weather_st.wx_amount > 0.3 } -export function weather_overcast() -> float { - if weather_kind_count() == 0 { return 0.0 } - return wx_kinds[wx_state].overcast * wx_amount +export function weather_overcast(weather_st: WeatherState) -> float { + if weather_kind_count(weather_st) == 0 { return 0.0 } + return weather_st.wx_kinds[weather_st.wx_state].overcast * weather_st.wx_amount } -export function weather_cold() -> float { - if weather_kind_count() == 0 { return 0.0 } - return wx_kinds[wx_state].cold * wx_amount +export function weather_cold(weather_st: WeatherState) -> float { + if weather_kind_count(weather_st) == 0 { return 0.0 } + return weather_st.wx_kinds[weather_st.wx_state].cold * weather_st.wx_amount } -export function weather_name(k: int) -> string { - if k < 0 or k >= weather_kind_count() { return "" } - return wx_kinds[k].name +export function weather_name(weather_st: WeatherState, k: int) -> string { + if k < 0 or k >= weather_kind_count(weather_st) { return "" } + return weather_st.wx_kinds[k].name } # the half hour after a front goes through, when the air is glass-clear -export function weather_after_front() -> bool { return wx_clear_t > 0.0 } +export function weather_after_front(weather_st: WeatherState) -> bool { return weather_st.wx_clear_t > 0.0 } # yesterday had rain or snow in it (the valley's fog pools after a wet day) -export function weather_wet_yesterday() -> bool { return wx_wet_yesterday } +export function weather_wet_yesterday(weather_st: WeatherState) -> bool { return weather_st.wx_wet_yesterday } diff --git a/packages/ludic.weather/run.ludic b/packages/ludic.weather/run.ludic index 6f285c51..ae8b207c 100644 --- a/packages/ludic.weather/run.ludic +++ b/packages/ludic.weather/run.ludic @@ -1,75 +1,75 @@ # run.ludic - the sky moving on. Only the machine that owns the world runs the spells; every # machine eases the ground's wetness and makes its own lightning out of a storm it is told of. -function wx_turn() -> void { - let was = wx_state - wx_state = wx_next - wx_next = wx_pick_next(wx_state) - wx_left = wx_spell_hours(wx_state) - if wx_forced { - wx_forced = false +function wx_turn(base_st: mut BaseState, weather_st: mut WeatherState) -> void { + let was = weather_st.wx_state + weather_st.wx_state = weather_st.wx_next + weather_st.wx_next = wx_pick_next(weather_st, weather_st.wx_state) + weather_st.wx_left = wx_spell_hours(weather_st, weather_st.wx_state) + if weather_st.wx_forced { + weather_st.wx_forced = false let least = Math.max(5.0, WeatherWorld.front_hours()) - if wx_left < least { wx_left = least } + if weather_st.wx_left < least { weather_st.wx_left = least } } - if wx_state == was { return } + if weather_st.wx_state == was { return } # a front going through and leaving clear sky behind it: the light people get up for - if wx_kinds[wx_state].clear and wx_kinds[was].wet and not wx_night() { - wx_clear_t = 0.6 - wx_fact(WEATHER_CLEARED, wx_state, was) + if weather_st.wx_kinds[weather_st.wx_state].clear and weather_st.wx_kinds[was].wet and not wx_night() { + weather_st.wx_clear_t = 0.6 + wx_fact(base_st, weather_st, WEATHER_CLEARED, weather_st.wx_state, was) } - wx_fact(WEATHER_BEGAN, wx_state, was) + wx_fact(base_st, weather_st, WEATHER_BEGAN, weather_st.wx_state, was) } -function wx_wind_tick(dt: float) -> void { - let kd = wx_kinds[wx_state] - wx_wind = wx_wind + (kd.wind - wx_wind) * Math.min(1.0, dt * 0.3) +function wx_wind_tick(weather_st: mut WeatherState, dt: float) -> void { + let kd = weather_st.wx_kinds[weather_st.wx_state] + weather_st.wx_wind = weather_st.wx_wind + (kd.wind - weather_st.wx_wind) * Math.min(1.0, dt * 0.3) var want = kd.wind_dir if want < 0.0 { let h = wx_hours() - want = wx_down_dir - if h > wx_up_from and h < wx_up_to { want = wx_up_dir } + want = weather_st.wx_down_dir + if h > weather_st.wx_up_from and h < weather_st.wx_up_to { want = weather_st.wx_up_dir } } let r = dt * 0.12 - wx_dir = wx_wrap(wx_dir + Math.clamp(wx_wrap(want - wx_dir), -r, r)) + weather_st.wx_dir = wx_wrap(weather_st.wx_dir + Math.clamp(wx_wrap(want - weather_st.wx_dir), -r, r)) } # the spells: the story's lean, the clock running down, the edges eased, the wind -function wx_spells(dt: float, gh: float) -> void { - wx_apply_bias() - wx_left = wx_left - gh - if wx_left < 0.0 { wx_turn() } +function wx_spells(base_st: mut BaseState, weather_st: mut WeatherState, dt: float, gh: float) -> void { + wx_apply_bias(base_st, weather_st) + weather_st.wx_left = weather_st.wx_left - gh + if weather_st.wx_left < 0.0 { wx_turn(base_st, weather_st) } var want = 1.0 - if wx_kinds[wx_state].clear { want = 0.0 } - if wx_left < 0.5 and wx_kinds[wx_next].clear { want = 0.0 } - wx_amount = wx_amount + (want - wx_amount) * Math.min(1.0, dt * 0.5) - wx_wind_tick(dt) - if wx_clear_t > 0.0 { wx_clear_t = wx_clear_t - gh } + if weather_st.wx_kinds[weather_st.wx_state].clear { want = 0.0 } + if weather_st.wx_left < 0.5 and weather_st.wx_kinds[weather_st.wx_next].clear { want = 0.0 } + weather_st.wx_amount = weather_st.wx_amount + (want - weather_st.wx_amount) * Math.min(1.0, dt * 0.5) + wx_wind_tick(weather_st, dt) + if weather_st.wx_clear_t > 0.0 { weather_st.wx_clear_t = weather_st.wx_clear_t - gh } } # soaking is faster than drying: a downpour wets the ground in under a minute, and it takes # several to give it back -function wx_ground_tick(dt: float) -> void { - let want = wx_kinds[wx_state].soak * wx_amount +function wx_ground_tick(weather_st: mut WeatherState, dt: float) -> void { + let want = weather_st.wx_kinds[weather_st.wx_state].soak * weather_st.wx_amount var k = dt * 0.10 - if want < wx_ground { k = dt * 0.020 } - wx_ground = wx_ground + (want - wx_ground) * Math.clamp(k, 0.0, 1.0) + if want < weather_st.wx_ground { k = dt * 0.020 } + weather_st.wx_ground = weather_st.wx_ground + (want - weather_st.wx_ground) * Math.clamp(k, 0.0, 1.0) } -function wx_storm_tick(dt: float) -> void { - wx_flash = wx_flash * Math.max(0.0, 1.0 - dt * 9.0) - if not wx_kinds[wx_state].thunder or wx_amount < 0.5 { return } - wx_thunder_t = wx_thunder_t - dt - if wx_thunder_t < 0.0 { - wx_thunder_t = rng_span(wx_dice, 9.0, 34.0) - wx_flash = 1.0 - wx_fact(WEATHER_THUNDER, wx_state, wx_state) +function wx_storm_tick(base_st: mut BaseState, weather_st: mut WeatherState, dt: float) -> void { + weather_st.wx_flash = weather_st.wx_flash * Math.max(0.0, 1.0 - dt * 9.0) + if not weather_st.wx_kinds[weather_st.wx_state].thunder or weather_st.wx_amount < 0.5 { return } + weather_st.wx_thunder_t = weather_st.wx_thunder_t - dt + if weather_st.wx_thunder_t < 0.0 { + weather_st.wx_thunder_t = rng_span(weather_st.wx_dice, 9.0, 34.0) + weather_st.wx_flash = 1.0 + wx_fact(base_st, weather_st, WEATHER_THUNDER, weather_st.wx_state, weather_st.wx_state) } } # one step: `dt` real seconds and `gh` game hours -export function weather_run(dt: float, gh: float) -> void { - if weather_kind_count() == 0 { return } - if wx_owns() { wx_spells(dt, gh) } - wx_ground_tick(dt) - wx_storm_tick(dt) - if weather_wet() { wx_wet_today = true } +export function weather_run(base_st: mut BaseState, weather_st: mut WeatherState, dt: float, gh: float) -> void { + if weather_kind_count(weather_st) == 0 { return } + if wx_owns() { wx_spells(base_st, weather_st, dt, gh) } + wx_ground_tick(weather_st, dt) + wx_storm_tick(base_st, weather_st, dt) + if weather_wet(weather_st) { weather_st.wx_wet_today = true } } diff --git a/packages/ludic.weather/state.ludic b/packages/ludic.weather/state.ludic index b9cd1748..03f035bc 100644 --- a/packages/ludic.weather/state.ludic +++ b/packages/ludic.weather/state.ludic @@ -15,7 +15,7 @@ export port WeatherWorld { function wx_owns_alone() -> bool { return true } function wx_noon() -> float { return 12.0 } function wx_daylight() -> bool { return false } -function wx_own_odds(from: int) -> []float { return wx_kinds[from].odds } +function wx_own_odds(weather_st: WeatherState, from: int) -> []float { return weather_st.wx_kinds[from].odds } function wx_no_bias() -> int { return WEATHER_NO_BIAS } function wx_no_hours() -> float { return 0.0 } function wx_seed_one() -> int { return 1 } @@ -53,34 +53,40 @@ export property WeatherFact { from: int = 0 } -var wx_kinds: []WeatherKind = null -var wx_state: int = 0 -var wx_next: int = 0 -var wx_left: float = 0.0 # game hours left in this spell -var wx_amount: float = 0.0 # 0..1, how far in (eases at the edges) -var wx_forecast: int = 0 -var wx_forced: bool = false # the next spell is a front the story asked for -var wx_wind: float = 0.2 -var wx_dir: float = 0.0 # radians the wind blows toward -var wx_ground: float = 0.0 # 0 dry .. 1 soaked -var wx_flash: float = 0.0 -var wx_thunder_t: float = 0.0 # real seconds to the next strike -var wx_clear_t: float = 0.0 # game hours of after-the-front light left -var wx_wet_today: bool = false -var wx_wet_yesterday: bool = false -var wx_dice: Rng = null -var wx_facts: Queue = null - -# the facts, oldest first; the game drains them into its notices and sounds -export function weather_facts() -> Queue { - if wx_facts == null { wx_facts = queue_new("weather.facts") } - return wx_facts +export state WeatherState { + wx_down_dir: float = 0.0 # the wind overnight and into the morning + wx_up_dir: float = 3.1415927 # the wind while the slopes are warm + wx_up_from: float = 10.0 + wx_up_to: float = 19.0 + wx_kinds: []WeatherKind = null + wx_state: int = 0 + wx_next: int = 0 + wx_left: float = 0.0 # game hours left in this spell + wx_amount: float = 0.0 # 0..1, how far in (eases at the edges) + wx_forecast: int = 0 + wx_forced: bool = false # the next spell is a front the story asked for + wx_wind: float = 0.2 + wx_dir: float = 0.0 # radians the wind blows toward + wx_ground: float = 0.0 # 0 dry .. 1 soaked + wx_flash: float = 0.0 + wx_thunder_t: float = 0.0 # real seconds to the next strike + wx_clear_t: float = 0.0 # game hours of after-the-front light left + wx_wet_today: bool = false + wx_wet_yesterday: bool = false + wx_dice: Rng = null + wx_facts: Queue = null } -function wx_fact(what: int, kind: int, from: int) -> void { +# the facts, oldest first; the game drains them into its notices and sounds +export function weather_facts(base_st: mut BaseState, weather_st: mut WeatherState) -> Queue { + if weather_st.wx_facts == null { weather_st.wx_facts = queue_new(base_st, "weather.facts") } + return weather_st.wx_facts +} + +function wx_fact(base_st: mut BaseState, weather_st: mut WeatherState, what: int, kind: int, from: int) -> void { let f = new WeatherFact f.what = what f.kind = kind f.from = from - q_push(weather_facts(), f) + q_push(base_st, weather_facts(base_st, weather_st), f) } diff --git a/packages/ludic.weather/system.ludic b/packages/ludic.weather/system.ludic index 1e109362..01c24935 100644 --- a/packages/ludic.weather/system.ludic +++ b/packages/ludic.weather/system.ludic @@ -1,43 +1,43 @@ # system.ludic - the weather as a system: PH_SIMULATE, and its own save section -function weather_tick(t: Tick) -> void { weather_run(t.dt, t.hours) } +function weather_tick(base_st: mut BaseState, weather_st: mut WeatherState, t: Tick) -> void { weather_run(base_st, weather_st, t.dt, t.hours) } -export function weather_save() -> Val { +export function weather_save(weather_st: WeatherState) -> Val { let v = Value.object() - sv_put_int(v, "state", wx_state) - sv_put_int(v, "next", wx_next) - sv_put_float(v, "left", wx_left) - sv_put_int(v, "forecast", wx_forecast) - sv_put_bool(v, "forced", wx_forced) - sv_put_float(v, "dir", wx_dir) - sv_put_float(v, "ground", wx_ground) - sv_put_float(v, "clear_t", wx_clear_t) - sv_put_bool(v, "wet_today", wx_wet_today) - sv_put_bool(v, "wet_yesterday", wx_wet_yesterday) - sv_put_int(v, "dice", wx_dice.s) + sv_put_int(v, "state", weather_st.wx_state) + sv_put_int(v, "next", weather_st.wx_next) + sv_put_float(v, "left", weather_st.wx_left) + sv_put_int(v, "forecast", weather_st.wx_forecast) + sv_put_bool(v, "forced", weather_st.wx_forced) + sv_put_float(v, "dir", weather_st.wx_dir) + sv_put_float(v, "ground", weather_st.wx_ground) + sv_put_float(v, "clear_t", weather_st.wx_clear_t) + sv_put_bool(v, "wet_today", weather_st.wx_wet_today) + sv_put_bool(v, "wet_yesterday", weather_st.wx_wet_yesterday) + sv_put_int(v, "dice", weather_st.wx_dice.s) return v } -function wx_kind_ok(k: int) -> int { - if k < 0 or k >= weather_kind_count() { return wx_clear_kind() } +function wx_kind_ok(weather_st: WeatherState, k: int) -> int { + if k < 0 or k >= weather_kind_count(weather_st) { return wx_clear_kind(weather_st) } return k } -export function weather_load(v: Val, version: int) -> void { - weather_reset() - wx_state = wx_kind_ok(sv_int(v, "state", 0)) - wx_next = wx_kind_ok(sv_int(v, "next", 0)) - wx_left = sv_float(v, "left", 10.0) - wx_forecast = wx_kind_ok(sv_int(v, "forecast", 0)) - wx_forced = sv_bool(v, "forced", false) - wx_dir = sv_float(v, "dir", 0.0) - wx_ground = sv_float(v, "ground", 0.0) - wx_clear_t = sv_float(v, "clear_t", 0.0) - wx_wet_today = sv_bool(v, "wet_today", false) - wx_wet_yesterday = sv_bool(v, "wet_yesterday", false) - wx_dice.s = sv_int(v, "dice", wx_dice.s) - if weather_kind_count() == 0 { return } - if not wx_kinds[wx_state].clear { wx_amount = 1.0 } - wx_wind = wx_kinds[wx_state].wind +export function weather_load(base_st: mut BaseState, weather_st: mut WeatherState, v: Val, version: int) -> void { + weather_reset(base_st, weather_st) + weather_st.wx_state = wx_kind_ok(weather_st, sv_int(v, "state", 0)) + weather_st.wx_next = wx_kind_ok(weather_st, sv_int(v, "next", 0)) + weather_st.wx_left = sv_float(v, "left", 10.0) + weather_st.wx_forecast = wx_kind_ok(weather_st, sv_int(v, "forecast", 0)) + weather_st.wx_forced = sv_bool(v, "forced", false) + weather_st.wx_dir = sv_float(v, "dir", 0.0) + weather_st.wx_ground = sv_float(v, "ground", 0.0) + weather_st.wx_clear_t = sv_float(v, "clear_t", 0.0) + weather_st.wx_wet_today = sv_bool(v, "wet_today", false) + weather_st.wx_wet_yesterday = sv_bool(v, "wet_yesterday", false) + weather_st.wx_dice.s = sv_int(v, "dice", weather_st.wx_dice.s) + if weather_kind_count(weather_st) == 0 { return } + if not weather_st.wx_kinds[weather_st.wx_state].clear { weather_st.wx_amount = 1.0 } + weather_st.wx_wind = weather_st.wx_kinds[weather_st.wx_state].wind } export function weather_system() -> System { diff --git a/packages/ludic.weather/tests/weather_test.ludic b/packages/ludic.weather/tests/weather_test.ludic index 80d7f0e9..cd14e123 100644 --- a/packages/ludic.weather/tests/weather_test.ludic +++ b/packages/ludic.weather/tests/weather_test.ludic @@ -4,20 +4,22 @@ import "ludic.weather" import "ludic.base" program WeatherTest { numbers float - var owns: bool = true - var hours: float = 12.0 - var bias: int = 0 - var lean: []float = null + state WeathertestState { + owns: bool = true + hours: float = 12.0 + bias: int = 0 + lean: []float = null + } - function fake_owns() -> bool { return owns } - function fake_hours() -> float { return hours } - function fake_night() -> bool { return hours < 6.0 or hours > 20.0 } - function fake_bias() -> int { return bias } + function fake_owns(weathertest_st: WeathertestState) -> bool { return weathertest_st.owns } + function fake_hours(weathertest_st: WeathertestState) -> float { return weathertest_st.hours } + function fake_night(weathertest_st: WeathertestState) -> bool { return weathertest_st.hours < 6.0 or weathertest_st.hours > 20.0 } + function fake_bias(weathertest_st: WeathertestState) -> int { return weathertest_st.bias } function fake_seed() -> int { return 7 } function fake_front_hours() -> float { return 6.0 } - function fake_odds(from: int) -> []float { - if lean != null { return lean } - return weather_kind(from).odds + function fake_odds(weather_st: WeatherState, weathertest_st: WeathertestState, from: int) -> []float { + if weathertest_st.lean != null { return weathertest_st.lean } + return weather_kind(weather_st, from).odds } function odds3(a: float, b: float, c: float) -> []float { @@ -57,146 +59,146 @@ program WeatherTest { } # clear, rain and storm: clear always goes to rain, rain to storm, storm to clear - function fresh() -> void { - owns = true - hours = 12.0 - bias = WEATHER_NO_BIAS - lean = null + function fresh(base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) -> void { + weathertest_st.owns = true + weathertest_st.hours = 12.0 + weathertest_st.bias = WEATHER_NO_BIAS + weathertest_st.lean = null let ks = new []WeatherKind push(ks, kind("clear", false, false, odds3(0.0, 1.0, 0.0))) push(ks, kind("rain", true, false, odds3(0.0, 0.0, 1.0))) push(ks, kind("storm", true, true, odds3(1.0, 0.0, 0.0))) - weather_kinds(ks) - weather_config_wind(0.5, 2.5, 10.0, 19.0) - weather_reset() + weather_kinds(weather_st, ks) + weather_config_wind(weather_st, 0.5, 2.5, 10.0, 19.0) + weather_reset(base_st, weather_st) } - function facts() -> []WeatherFact { return q_drain(weather_facts()) } - function step(dt: float, gh: float, n: int) -> void { for i in 0 .. n { weather_run(dt, gh) } } + function facts(base_st: mut BaseState, weather_st: mut WeatherState) -> []WeatherFact { return q_drain(base_st, weather_facts(base_st, weather_st)) } + function step(base_st: mut BaseState, weather_st: mut WeatherState, dt: float, gh: float, n: int) -> void { for i in 0 .. n { weather_run(base_st, weather_st, dt, gh) } } function count(fs: []WeatherFact, what: int) -> int { var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } - test "a new trip is clear, dry and forecast clear" { - fresh() - expect_eq(weather_state(), 0) - expect_eq(weather_forecast(), 0) - expect(not weather_wet()) - expect(weather_cold() == 0.0) + test "a new trip is clear, dry and forecast clear" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + expect_eq(weather_state(weather_st), 0) + expect_eq(weather_forecast(weather_st), 0) + expect(not weather_wet(weather_st)) + expect(weather_cold(weather_st) == 0.0) } - test "a spell that runs out gives way to the next, as a fact" { - fresh() - weather_set(0, 1, 0.1) - step(0.1, 0.2, 1) - expect_eq(weather_state(), 1) - expect_eq(weather_next(), 2) - let fs = facts() + test "a spell that runs out gives way to the next, as a fact" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weather_set(weather_st, 0, 1, 0.1) + step(base_st, weather_st, 0.1, 0.2, 1) + expect_eq(weather_state(weather_st), 1) + expect_eq(weather_next(weather_st), 2) + let fs = facts(base_st, weather_st) expect_eq(count(fs, WEATHER_BEGAN), 1) - expect(weather_left() >= 1.0 and weather_left() < 3.0) + expect(weather_left(weather_st) >= 1.0 and weather_left(weather_st) < 3.0) } - test "the amount eases in, and wet follows it" { - fresh() - weather_set(0, 1, 0.01) - step(0.1, 0.02, 1) - expect(not weather_wet()) - step(0.1, 0.0, 200) - expect(weather_wet()) - expect(weather_cold() > 4.0) - expect(weather_ground_wet() > 0.5) + test "the amount eases in, and wet follows it" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weather_set(weather_st, 0, 1, 0.01) + step(base_st, weather_st, 0.1, 0.02, 1) + expect(not weather_wet(weather_st)) + step(base_st, weather_st, 0.1, 0.0, 200) + expect(weather_wet(weather_st)) + expect(weather_cold(weather_st) > 4.0) + expect(weather_ground_wet(weather_st) > 0.5) } - test "a storm throws lightning, and a clearing in daylight is a fact" { - fresh() - weather_set(2, 0, 2.0) - step(1.0, 0.0, 60) - expect(count(facts(), WEATHER_THUNDER) > 0) - weather_set(2, 0, 0.05) - step(0.1, 0.1, 1) - expect_eq(weather_state(), 0) - expect(weather_after_front()) - expect_eq(count(facts(), WEATHER_CLEARED), 1) + test "a storm throws lightning, and a clearing in daylight is a fact" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weather_set(weather_st, 2, 0, 2.0) + step(base_st, weather_st, 1.0, 0.0, 60) + expect(count(facts(base_st, weather_st), WEATHER_THUNDER) > 0) + weather_set(weather_st, 2, 0, 0.05) + step(base_st, weather_st, 0.1, 0.1, 1) + expect_eq(weather_state(weather_st), 0) + expect(weather_after_front(weather_st)) + expect_eq(count(facts(base_st, weather_st), WEATHER_CLEARED), 1) } - test "the world's odds overrule the kind's" { - fresh() - lean = odds3(0.0, 0.0, 1.0) - weather_set(0, 0, 0.01) - step(0.1, 0.1, 1) - expect_eq(weather_next(), 2) + test "the world's odds overrule the kind's" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weathertest_st.lean = odds3(0.0, 0.0, 1.0) + weather_set(weather_st, 0, 0, 0.01) + step(base_st, weather_st, 0.1, 0.1, 1) + expect_eq(weather_next(weather_st), 2) } - test "wanting rain forecasts a front and holds it long enough" { - fresh() - bias = WEATHER_WANT_WET - step(0.1, 0.01, 1) - expect(weather_forced()) - expect_eq(weather_forecast(), 1) - expect_eq(count(facts(), WEATHER_FRONT), 1) - step(0.1, 2.0, 1) - expect_eq(weather_state(), 1) - expect(not weather_forced()) - expect(weather_left() >= 6.0) + test "wanting rain forecasts a front and holds it long enough" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weathertest_st.bias = WEATHER_WANT_WET + step(base_st, weather_st, 0.1, 0.01, 1) + expect(weather_forced(weather_st)) + expect_eq(weather_forecast(weather_st), 1) + expect_eq(count(facts(base_st, weather_st), WEATHER_FRONT), 1) + step(base_st, weather_st, 0.1, 2.0, 1) + expect_eq(weather_state(weather_st), 1) + expect(not weather_forced(weather_st)) + expect(weather_left(weather_st) >= 6.0) } - test "wanting clear sky clears it and keeps it" { - fresh() - weather_set(1, 1, 5.0) - bias = WEATHER_WANT_CLEAR - step(0.1, 0.6, 1) - expect_eq(weather_state(), 0) - step(0.1, 0.9, 5) - expect_eq(weather_state(), 0) + test "wanting clear sky clears it and keeps it" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weather_set(weather_st, 1, 1, 5.0) + weathertest_st.bias = WEATHER_WANT_CLEAR + step(base_st, weather_st, 0.1, 0.6, 1) + expect_eq(weather_state(weather_st), 0) + step(base_st, weather_st, 0.1, 0.9, 5) + expect_eq(weather_state(weather_st), 0) } - test "a machine that does not own the world runs no spells, but is told them" { - fresh() - owns = false - weather_set(0, 1, 0.1) - step(0.1, 5.0, 3) - expect_eq(weather_state(), 0) - weather_told(2, 0, 3.0, 1.0, 0.9, 1.0, 2) - expect_eq(weather_state(), 2) - expect_eq(count(facts(), WEATHER_BEGAN), 1) - step(1.0, 0.0, 60) - expect(count(facts(), WEATHER_THUNDER) > 0) + test "a machine that does not own the world runs no spells, but is told them" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weathertest_st.owns = false + weather_set(weather_st, 0, 1, 0.1) + step(base_st, weather_st, 0.1, 5.0, 3) + expect_eq(weather_state(weather_st), 0) + weather_told(base_st, weather_st, 2, 0, 3.0, 1.0, 0.9, 1.0, 2) + expect_eq(weather_state(weather_st), 2) + expect_eq(count(facts(base_st, weather_st), WEATHER_BEGAN), 1) + step(base_st, weather_st, 1.0, 0.0, 60) + expect(count(facts(base_st, weather_st), WEATHER_THUNDER) > 0) } - test "the wind turns with the hours" { - fresh() - hours = 3.0 - step(1.0, 0.0, 100) - expect(Math.abs(weather_dir() - 0.5) < 0.01) - hours = 12.0 - step(1.0, 0.0, 100) - expect(Math.abs(weather_dir() - 2.5) < 0.01) + test "the wind turns with the hours" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weathertest_st.hours = 3.0 + step(base_st, weather_st, 1.0, 0.0, 100) + expect(Math.abs(weather_dir(weather_st) - 0.5) < 0.01) + weathertest_st.hours = 12.0 + step(base_st, weather_st, 1.0, 0.0, 100) + expect(Math.abs(weather_dir(weather_st) - 2.5) < 0.01) } - test "a morning after rain remembers it was wet" { - fresh() - weather_set(1, 1, 5.0) - step(0.1, 0.0, 1) - weather_morning() - expect(weather_wet_yesterday()) + test "a morning after rain remembers it was wet" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weather_set(weather_st, 1, 1, 5.0) + step(base_st, weather_st, 0.1, 0.0, 1) + weather_morning(weather_st) + expect(weather_wet_yesterday(weather_st)) } - test "a save section reads back the same sky" { - fresh() - weather_set(1, 2, 4.25) - weather_morning() - let fc = weather_forecast() + test "a save section reads back the same sky" (base_st: mut BaseState, weather_st: mut WeatherState, weathertest_st: mut WeathertestState) { + fresh(base_st, weather_st, weathertest_st) + weather_set(weather_st, 1, 2, 4.25) + weather_morning(weather_st) + let fc = weather_forecast(weather_st) let root = save_tree() - save_section(root, "weather", 1, weather_save()) + save_section(root, "weather", 1, weather_save(weather_st)) let text = save_encode(root) - weather_reset() + weather_reset(base_st, weather_st) let n = load_section(save_decode(text), "weather") - weather_load(n.data, n.version) - expect_eq(weather_state(), 1) - expect(weather_left() == 4.25) - expect_eq(weather_forecast(), fc) - expect(weather_wet_yesterday()) + weather_load(base_st, weather_st, n.data, n.version) + expect_eq(weather_state(weather_st), 1) + expect(weather_left(weather_st) == 4.25) + expect_eq(weather_forecast(weather_st), fc) + expect(weather_wet_yesterday(weather_st)) } } diff --git a/packages/ludic.weather/verbs.ludic b/packages/ludic.weather/verbs.ludic index 58090133..76fcc45e 100644 --- a/packages/ludic.weather/verbs.ludic +++ b/packages/ludic.weather/verbs.ludic @@ -1,66 +1,66 @@ # verbs.ludic - the only way the sky changes from outside: a morning, a reset, the host's word # a new trip: clear sky, a still-ish wind, nothing forecast; the dice from the world's seed -export function weather_reset() -> void { - wx_dice = rng_new(WeatherWorld.seed()) - let clear = wx_clear_kind() - wx_state = clear - wx_next = clear - wx_forecast = clear - wx_left = 30.0 - wx_amount = 0.0 - wx_forced = false - wx_wind = 0.2 - wx_dir = 0.0 - wx_ground = 0.0 - wx_flash = 0.0 - wx_thunder_t = 0.0 - wx_clear_t = 0.0 - wx_wet_today = false - wx_wet_yesterday = false - q_clear(weather_facts()) +export function weather_reset(base_st: mut BaseState, weather_st: mut WeatherState) -> void { + weather_st.wx_dice = rng_new(WeatherWorld.seed()) + let clear = wx_clear_kind(weather_st) + weather_st.wx_state = clear + weather_st.wx_next = clear + weather_st.wx_forecast = clear + weather_st.wx_left = 30.0 + weather_st.wx_amount = 0.0 + weather_st.wx_forced = false + weather_st.wx_wind = 0.2 + weather_st.wx_dir = 0.0 + weather_st.wx_ground = 0.0 + weather_st.wx_flash = 0.0 + weather_st.wx_thunder_t = 0.0 + weather_st.wx_clear_t = 0.0 + weather_st.wx_wet_today = false + weather_st.wx_wet_yesterday = false + q_clear(base_st, weather_facts(base_st, weather_st)) } # a new day's forecast: seven times in ten it is the next spell. A front already on its way # stays on its way. -export function weather_morning() -> void { - if weather_kind_count() == 0 { return } - wx_wet_yesterday = wx_wet_today or wx_kinds[wx_state].wet - wx_wet_today = false - if wx_forced { - wx_forecast = wx_next +export function weather_morning(weather_st: mut WeatherState) -> void { + if weather_kind_count(weather_st) == 0 { return } + weather_st.wx_wet_yesterday = weather_st.wx_wet_today or weather_st.wx_kinds[weather_st.wx_state].wet + weather_st.wx_wet_today = false + if weather_st.wx_forced { + weather_st.wx_forecast = weather_st.wx_next return } - wx_forecast = wx_pick_next(wx_state) - if rng_float(wx_dice) < 0.7 { wx_next = wx_forecast } + weather_st.wx_forecast = wx_pick_next(weather_st, weather_st.wx_state) + if rng_float(weather_st.wx_dice) < 0.7 { weather_st.wx_next = weather_st.wx_forecast } } # the sky outright: a spell of `kind` now, `left` hours of it, at full strength when not clear # (staging, a test, a story beat) -export function weather_set(kind: int, next: int, left: float) -> void { - wx_state = kind - wx_next = next - wx_left = left - wx_amount = 1.0 - if wx_kinds[kind].clear { wx_amount = 0.0 } +export function weather_set(weather_st: mut WeatherState, kind: int, next: int, left: float) -> void { + weather_st.wx_state = kind + weather_st.wx_next = next + weather_st.wx_left = left + weather_st.wx_amount = 1.0 + if weather_st.wx_kinds[kind].clear { weather_st.wx_amount = 0.0 } } # what the machine that owns the world says the sky is; a new spell is a fact here too -export function weather_told(kind: int, next: int, left: float, amount: float, wind: float, dir: float, forecast: int) -> void { - let was = wx_state - wx_state = kind - wx_next = next - wx_left = left - wx_amount = amount - wx_wind = wind - wx_dir = dir - wx_forecast = forecast - if kind != was { wx_fact(WEATHER_BEGAN, kind, was) } +export function weather_told(base_st: mut BaseState, weather_st: mut WeatherState, kind: int, next: int, left: float, amount: float, wind: float, dir: float, forecast: int) -> void { + let was = weather_st.wx_state + weather_st.wx_state = kind + weather_st.wx_next = next + weather_st.wx_left = left + weather_st.wx_amount = amount + weather_st.wx_wind = wind + weather_st.wx_dir = dir + weather_st.wx_forecast = forecast + if kind != was { wx_fact(base_st, weather_st, WEATHER_BEGAN, kind, was) } } -export function weather_set_wind(strength: float, dir: float) -> void { - wx_wind = strength - wx_dir = dir +export function weather_set_wind(weather_st: mut WeatherState, strength: float, dir: float) -> void { + weather_st.wx_wind = strength + weather_st.wx_dir = dir } -export function weather_set_ground(wet: float) -> void { wx_ground = wet } -export function weather_set_flash(f: float) -> void { wx_flash = f } +export function weather_set_ground(weather_st: mut WeatherState, wet: float) -> void { weather_st.wx_ground = wet } +export function weather_set_flash(weather_st: mut WeatherState, f: float) -> void { weather_st.wx_flash = f } diff --git a/packages/ludic.wildlife/birds.ludic b/packages/ludic.wildlife/birds.ludic index 5000b509..e63e19a6 100644 --- a/packages/ludic.wildlife/birds.ludic +++ b/packages/ludic.wildlife/birds.ludic @@ -1,36 +1,36 @@ # birds.ludic - a bird circles its home at its own height, comes down now and then (for seed from # further off than for nothing), glides in rather than dropping, pecks and drinks, and lifts off -export function wildlife_tick_bird(a: WildAnimal, dt: float) -> void { +export function wildlife_tick_bird(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> void { a.timer = a.timer - dt - let dh = wl_near(a.x, a.z) + let dh = wl_near(wildlife_st, a.x, a.z) if a.state == WILD_LAND { - wl_land(a, dh, dt) + wl_land(base_st, wildlife_st, a, dh, dt) return } if a.state == WILD_PERCH { - wl_perch(a, dh, dt) + wl_perch(base_st, wildlife_st, a, dh, dt) return } - wl_circle(a, dt) - let s = wl_sp(a.sp) - if wl_bird_seed(a) and dh > s.flee * 0.7 and wl_bird_spot_ok(wl_best.x, wl_best.z) { - let off = 1.0 + wl_rnd() * 2.0 - let oa = wl_rnd() * WL_TAU - wl_come_down(a, wl_best.x + Math.sin(oa) * off, wl_best.z + Math.cos(oa) * off) + wl_circle(wildlife_st, a, dt) + let s = wl_sp(wildlife_st, a.sp) + if wl_bird_seed(wildlife_st, a) and dh > s.flee * 0.7 and wl_bird_spot_ok(wildlife_st.wl_best.x, wildlife_st.wl_best.z) { + let off = 1.0 + wl_rnd(wildlife_st) * 2.0 + let oa = wl_rnd(wildlife_st) * WL_TAU + wl_come_down(a, wildlife_st.wl_best.x + Math.sin(oa) * off, wildlife_st.wl_best.z + Math.cos(oa) * off) return } if a.timer < 0.0 and dh > s.land_shy { - let sx = a.x + (wl_rnd() - 0.5) * 24.0 - let sz = a.z + (wl_rnd() - 0.5) * 24.0 - if wl_rnd() < s.land_chance and wl_bird_spot_ok(sx, sz) { + let sx = a.x + (wl_rnd(wildlife_st) - 0.5) * 24.0 + let sz = a.z + (wl_rnd(wildlife_st) - 0.5) * 24.0 + if wl_rnd(wildlife_st) < s.land_chance and wl_bird_spot_ok(sx, sz) { wl_come_down(a, sx, sz) return } } if a.timer < 0.0 { a.timer = 30.0 - a.home_x = a.home_x + (wl_rnd() - 0.5) * 120.0 - a.home_z = a.home_z + (wl_rnd() - 0.5) * 120.0 + a.home_x = a.home_x + (wl_rnd(wildlife_st) - 0.5) * 120.0 + a.home_z = a.home_z + (wl_rnd(wildlife_st) - 0.5) * 120.0 } } @@ -42,8 +42,8 @@ function wl_come_down(a: WildAnimal, x: float, z: float) -> void { } # a circle about home, climbing to its height: the eagle wide and high, the jay low -function wl_circle(a: WildAnimal, dt: float) -> void { - let s = wl_sp(a.sp) +function wl_circle(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> void { + let s = wl_sp(wildlife_st, a.sp) a.fly_h = a.fly_h + (8.0 + s.home * 0.12 - a.fly_h) * Math.min(1.0, 0.4 * dt) let r = s.home * 0.5 let ang = Math.atan2(a.z - a.home_z, a.x - a.home_x) + s.walk / r * dt @@ -57,45 +57,45 @@ function wl_circle(a: WildAnimal, dt: float) -> void { a.bank = 0.35 } -function wl_take_off(a: WildAnimal, h: float) -> void { +function wl_take_off(wildlife_st: mut WildlifeState, a: WildAnimal, h: float) -> void { a.state = WILD_FLY - a.timer = 20.0 + wl_rnd() * 40.0 + a.timer = 20.0 + wl_rnd(wildlife_st) * 40.0 a.fly_h = Math.max(a.fly_h, h) } -function wl_land(a: WildAnimal, dh: float, dt: float) -> void { - let s = wl_sp(a.sp) +function wl_land(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dh: float, dt: float) -> void { + let s = wl_sp(wildlife_st, a.sp) a.fly_h = Math.max(0.0, a.fly_h - dt * 3.2) a.speed = s.walk * 0.8 wl_face(a, a.tx, a.tz, 2.4, dt) - wl_step(a, dt) + wl_step(base_st, wildlife_st, a, dt) a.y = WildlifeWorld.ground(a.x, a.z) + a.fly_h a.bank = 0.2 * Math.clamp(a.fly_h / 4.0, 0.0, 1.0) if dh < s.flee or a.timer < -20.0 { - wl_take_off(a, 0.0) + wl_take_off(wildlife_st, a, 0.0) return } if a.fly_h < 0.15 and wl_at_target(a, 2.0) { a.state = WILD_PERCH a.fly_h = 0.0 - a.timer = 8.0 + wl_rnd() * 16.0 + a.timer = 8.0 + wl_rnd(wildlife_st) * 16.0 a.bank = 0.0 } } # on the ground it pecks at seed or takes a drink at the water's edge; it leaves no droppings -function wl_perch(a: WildAnimal, dh: float, dt: float) -> void { +function wl_perch(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dh: float, dt: float) -> void { a.speed = 0.0 a.y = WildlifeWorld.ground(a.x, a.z) a.bank = 0.0 - if wl_bird_seed(a) and Math.sqrt((wl_best.x - a.x) * (wl_best.x - a.x) + (wl_best.z - a.z) * (wl_best.z - a.z)) < 3.0 { + if wl_bird_seed(wildlife_st, a) and Math.sqrt((wildlife_st.wl_best.x - a.x) * (wildlife_st.wl_best.x - a.x) + (wildlife_st.wl_best.z - a.z) * (wildlife_st.wl_best.z - a.z)) < 3.0 { a.hunger = Math.max(a.hunger - 30.0 * dt, 0.0) a.timer = Math.max(a.timer, 1.0) - if wl_rnd() < 0.12 * dt and WildlifeWorld.present(wl_best.uid) { wl_ate(a, wl_best.uid, wl_best.owner, WILD_SEED) } + if wl_rnd(wildlife_st) < 0.12 * dt and WildlifeWorld.present(wildlife_st.wl_best.uid) { wl_ate(base_st, wildlife_st, a, wildlife_st.wl_best.uid, wildlife_st.wl_best.owner, WILD_SEED) } } else if wl_wet(a.x - Math.sin(a.yaw) * 2.0, a.z - Math.cos(a.yaw) * 2.0, 0.05) { a.thirst = Math.max(a.thirst - 30.0 * dt, 0.0) } - if a.timer < 0.0 or dh < wl_sp(a.sp).flee { wl_take_off(a, 0.5) } + if a.timer < 0.0 or dh < wl_sp(wildlife_st, a.sp).flee { wl_take_off(wildlife_st, a, 0.5) } } # somewhere a bird would put down: flat and dry diff --git a/packages/ludic.wildlife/facts.ludic b/packages/ludic.wildlife/facts.ludic index 883b6861..4c01ceac 100644 --- a/packages/ludic.wildlife/facts.ludic +++ b/packages/ludic.wildlife/facts.ludic @@ -27,14 +27,13 @@ export property WildFact { diet: int = 0 } -var wl_facts: Queue = null -export function wildlife_facts() -> Queue { - if wl_facts == null { wl_facts = queue_new("wildlife.facts") } - return wl_facts +export function wildlife_facts(base_st: mut BaseState, wildlife_st: mut WildlifeState) -> Queue { + if wildlife_st.wl_facts == null { wildlife_st.wl_facts = queue_new(base_st, "wildlife.facts") } + return wildlife_st.wl_facts } -function wl_fact(what: int, a: WildAnimal) -> WildFact { +function wl_fact(base_st: mut BaseState, wildlife_st: mut WildlifeState, what: int, a: WildAnimal) -> WildFact { let f = new WildFact f.what = what f.key = a.key @@ -45,6 +44,6 @@ function wl_fact(what: int, a: WildAnimal) -> WildFact { f.z = a.z f.yaw = a.yaw f.scale = a.scale - q_push(wildlife_facts(), f) + q_push(base_st, wildlife_facts(base_st, wildlife_st), f) return f } diff --git a/packages/ludic.wildlife/ground.ludic b/packages/ludic.wildlife/ground.ludic index 10eb9962..80885622 100644 --- a/packages/ludic.wildlife/ground.ludic +++ b/packages/ludic.wildlife/ground.ludic @@ -1,87 +1,87 @@ # ground.ludic - a walker's tick: its needs, the alert, then the first of charging, reacting, going # to its spot, walking to something set out, eating, idling and wandering that has it this tick -export function wildlife_tick_ground(a: WildAnimal, dt: float, gh: float) -> void { - wl_needs(a, gh) - let dh = wl_near(a.x, a.z) - let np = wl_near_i +export function wildlife_tick_ground(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void { + wl_needs(base_st, wildlife_st, a, gh) + let dh = wl_near(wildlife_st, a.x, a.z) + let np = wildlife_st.wl_near_i a.timer = a.timer - dt if a.calm > 0.0 { a.calm = a.calm - gh * 60.0 } if a.spook > 0.0 { a.spook = a.spook - gh * 60.0 } - wl_alert(a, np, dh, dt) + wl_alert(wildlife_st, a, np, dh, dt) if a.state == WILD_TRAPPED { a.speed = 0.0 return } - if wl_charge(a, np, dh, dt) { return } - wl_posture(a, np, dt) - if wl_react(a, np, dt) { return } - if wl_seek(a, dt) { return } - if wl_drink(a, dt, gh) { return } - if wl_approach(a, dt) { return } - if wl_eat(a) { return } - let s = wl_sp(a.sp) + if wl_charge(base_st, wildlife_st, a, np, dh, dt) { return } + wl_posture(base_st, wildlife_st, a, np, dt) + if wl_react(base_st, wildlife_st, a, np, dt) { return } + if wl_seek(base_st, wildlife_st, a, dt) { return } + if wl_drink(wildlife_st, a, dt, gh) { return } + if wl_approach(base_st, wildlife_st, a, dt) { return } + if wl_eat(base_st, wildlife_st, a) { return } + let s = wl_sp(wildlife_st, a.sp) if a.state == WILD_IDLE { a.speed = 0.0 if a.timer < 0.0 { - if a.timer > -6.0 and s.tracks and wl_rnd() < 0.06 { wl_fact(WILD_BED, a) } # a long sit leaves crushed grass + if a.timer > -6.0 and s.tracks and wl_rnd(wildlife_st) < 0.06 { wl_fact(base_st, wildlife_st, WILD_BED, a) } # a long sit leaves crushed grass a.state = WILD_WANDER - a.timer = 6.0 + wl_rnd() * 10.0 - wl_clamp(a) - wl_pick_target(a, s.home) + a.timer = 6.0 + wl_rnd(wildlife_st) * 10.0 + wl_clamp(wildlife_st, a) + wl_pick_target(wildlife_st, a, s.home) } return } wl_face(a, a.tx, a.tz, 2.0, dt) a.speed = s.walk - wl_step(a, dt) + wl_step(base_st, wildlife_st, a, dt) if wl_at_target(a, 1.5) or a.timer < 0.0 { a.state = WILD_IDLE - a.timer = 3.0 + wl_rnd() * 8.0 + a.timer = 3.0 + wl_rnd(wildlife_st) * 8.0 } } # a charger close enough comes at the nearest player; true while it is charging -function wl_charge(a: WildAnimal, np: int, dh: float, dt: float) -> bool { - let s = wl_sp(a.sp) +function wl_charge(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dh: float, dt: float) -> bool { + let s = wl_sp(wildlife_st, a.sp) if s.charges and a.state != WILD_CHARGE and dh < WildlifeWorld.charge_reach() and not (a.calm > 0.0) { a.state = WILD_CHARGE a.timer = 2.5 - wl_fact(WILD_CHARGED, a).player = np + wl_fact(base_st, wildlife_st, WILD_CHARGED, a).player = np } if a.state != WILD_CHARGE { return false } wl_face(a, WildlifeWorld.player_x(np), WildlifeWorld.player_z(np), 4.0, dt) a.speed = s.run - wl_step(a, dt) + wl_step(base_st, wildlife_st, a, dt) if dh < 2.6 { - wl_fact(WILD_SHOVED, a).player = np - wl_wander(a, 8.0) + wl_fact(base_st, wildlife_st, WILD_SHOVED, a).player = np + wl_wander(wildlife_st, a, 8.0) a.calm = 30.0 } - if a.timer < 0.0 { wl_wander(a, 6.0) } + if a.timer < 0.0 { wl_wander(wildlife_st, a, 6.0) } return true } # Four postures read off the alert. Staring is a REACTION, not a condition: each times out and # hands the animal back to its business, and only a fresh rise above `seen` looks up again. -function wl_posture(a: WildAnimal, np: int, dt: float) -> void { - if wl_sp(a.sp).tame or a.state == WILD_EAT { return } +function wl_posture(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dt: float) -> void { + if wl_sp(wildlife_st, a.sp).tame or a.state == WILD_EAT { return } if a.alert > 95.0 { if a.state != WILD_FLEE { a.state = WILD_FLEE - a.timer = 3.0 + wl_rnd() * 3.0 - a.spook = 120.0 + wl_rnd() * 180.0 + a.timer = 3.0 + wl_rnd(wildlife_st) * 3.0 + a.spook = 120.0 + wl_rnd(wildlife_st) * 180.0 a.alert = 70.0 - wl_fact(WILD_FLED, a).player = np + wl_fact(base_st, wildlife_st, WILD_FLED, a).player = np } } else if a.alert > 60.0 { if a.state != WILD_WARY and a.state != WILD_FLEE and a.alert > a.seen { a.state = WILD_WARY - a.timer = 2.0 + wl_rnd() * 2.0 + a.timer = 2.0 + wl_rnd(wildlife_st) * 2.0 } } else if a.alert > 25.0 { if a.state != WILD_ALERT and a.state != WILD_WARY and a.alert > a.seen { a.state = WILD_ALERT - a.timer = 1.5 + wl_rnd() * 2.5 + a.timer = 1.5 + wl_rnd(wildlife_st) * 2.5 } } if a.alert > a.seen { a.seen = a.alert } diff --git a/packages/ludic.wildlife/guest.ludic b/packages/ludic.wildlife/guest.ludic index 5f4e94fc..1fdf2f1a 100644 --- a/packages/ludic.wildlife/guest.ludic +++ b/packages/ludic.wildlife/guest.ludic @@ -1,17 +1,16 @@ # guest.ludic - a machine that draws another's animals puts its own away whole, and they come back # as they were when it stops; in between the list is the copies it is handed (wildlife_make) -var wl_own: []WildAnimal = null -export function wildlife_put_away() -> void { - if wl_own != null { return } - wl_own = wildlife_all() - wl_list = new []WildAnimal +export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void { + if wildlife_st.wl_own != null { return } + wildlife_st.wl_own = wildlife_all(wildlife_st) + wildlife_st.wl_list = new []WildAnimal } -export function wildlife_bring_back() -> void { - if wl_own == null { return } - wl_list = wl_own - wl_own = null +export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void { + if wildlife_st.wl_own == null { return } + wildlife_st.wl_list = wildlife_st.wl_own + wildlife_st.wl_own = null } -export function wildlife_away() -> bool { return wl_own != null } +export function wildlife_away(wildlife_st: WildlifeState) -> bool { return wildlife_st.wl_own != null } diff --git a/packages/ludic.wildlife/habitat.ludic b/packages/ludic.wildlife/habitat.ludic index 0206d969..229444e4 100644 --- a/packages/ludic.wildlife/habitat.ludic +++ b/packages/ludic.wildlife/habitat.ludic @@ -1,7 +1,5 @@ # habitat.ludic - the right kind of ground for a species, asked of the port's ground, water, slope # and forest; and a spot of it at a distance from a point, off the package's dice -var wl_px: float = 0.0 -var wl_pz: float = 0.0 function wl_wet(x: float, z: float, depth: float) -> bool { return WildlifeWorld.ground(x, z) < WildlifeWorld.water(x, z) - depth @@ -19,20 +17,20 @@ export function wildlife_habitat_ok(hab: int, x: float, z: float) -> bool { } # a spot of `hab` d0..d1 metres from (x0, z0); where it is, is wildlife_spot_x / _z -export function wildlife_find_spot(hab: int, x0: float, z0: float, d0: float, d1: float) -> bool { +export function wildlife_find_spot(wildlife_st: mut WildlifeState, hab: int, x0: float, z0: float, d0: float, d1: float) -> bool { for t in 0 .. 60 { - let ang = wl_rnd() * WL_TAU - let d = d0 + wl_rnd() * (d1 - d0) + let ang = wl_rnd(wildlife_st) * WL_TAU + let d = d0 + wl_rnd(wildlife_st) * (d1 - d0) let x = x0 + Math.cos(ang) * d let z = z0 + Math.sin(ang) * d if wildlife_habitat_ok(hab, x, z) { - wl_px = x - wl_pz = z + wildlife_st.wl_px = x + wildlife_st.wl_pz = z return true } } return false } -export function wildlife_spot_x() -> float { return wl_px } -export function wildlife_spot_z() -> float { return wl_pz } +export function wildlife_spot_x(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_px } +export function wildlife_spot_z(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_pz } diff --git a/packages/ludic.wildlife/lures.ludic b/packages/ludic.wildlife/lures.ludic index 89dfaa3a..4aa82fa7 100644 --- a/packages/ludic.wildlife/lures.ludic +++ b/packages/ludic.wildlife/lures.ludic @@ -1,13 +1,9 @@ # lures.ludic - what an animal would walk to: food of its diet, a lure of its diet, a snare it is # small enough for (half the time: a curiosity), within 70 m - 175 m for food while feeding is taught -var wl_ask: WildLure = null -var wl_best: WildLure = null -var wl_seed_any: bool = false -var wl_seed_t: float = 0.0 -function wl_lure_scratch() -> void { - if wl_ask == null { wl_ask = new WildLure } - if wl_best == null { wl_best = new WildLure } +function wl_lure_scratch(wildlife_st: mut WildlifeState) -> void { + if wildlife_st.wl_ask == null { wildlife_st.wl_ask = new WildLure } + if wildlife_st.wl_best == null { wildlife_st.wl_best = new WildLure } } function wl_copy_lure(from: WildLure, to: WildLure) -> void { @@ -20,82 +16,82 @@ function wl_copy_lure(from: WildLure, to: WildLure) -> void { to.owner = from.owner } -function wl_draws(a: WildAnimal, l: WildLure) -> bool { - let s = wl_sp(a.sp) - if l.kind == WILD_SNARE { return s.size <= l.size and wl_rnd() > 0.5 } +function wl_draws(wildlife_st: mut WildlifeState, a: WildAnimal, l: WildLure) -> bool { + let s = wl_sp(wildlife_st, a.sp) + if l.kind == WILD_SNARE { return s.size <= l.size and wl_rnd(wildlife_st) > 0.5 } return l.diet == s.diet and l.diet != WILD_SEED } # the nearest thing set out that draws it, into `wl_best`; false when none does -function wl_attraction(a: WildAnimal) -> bool { - wl_lure_scratch() +function wl_attraction(wildlife_st: mut WildlifeState, a: WildAnimal) -> bool { + wl_lure_scratch(wildlife_st) var bd = 70.0 var got = false let taught = WildlifeWorld.taught() for i in 0 .. WildlifeWorld.lures() { - if not WildlifeWorld.lure(i, wl_ask) or not wl_draws(a, wl_ask) { continue } - let dx = wl_ask.x - a.x - let dz = wl_ask.z - a.z + if not WildlifeWorld.lure(i, wildlife_st.wl_ask) or not wl_draws(wildlife_st, a, wildlife_st.wl_ask) { continue } + let dx = wildlife_st.wl_ask.x - a.x + let dz = wildlife_st.wl_ask.z - a.z var d = Math.sqrt(dx * dx + dz * dz) - if taught and wl_ask.kind == WILD_FEED { d = d * 0.4 } + if taught and wildlife_st.wl_ask.kind == WILD_FEED { d = d * 0.4 } if d < bd { bd = d got = true - wl_copy_lure(wl_ask, wl_best) + wl_copy_lure(wildlife_st.wl_ask, wildlife_st.wl_best) } } return got } -function wl_take_lure(a: WildAnimal) -> void { - a.lure = wl_best.uid - a.lure_kind = wl_best.kind - a.lure_x = wl_best.x - a.lure_z = wl_best.z - a.lure_owner = wl_best.owner +function wl_take_lure(wildlife_st: WildlifeState, a: WildAnimal) -> void { + a.lure = wildlife_st.wl_best.uid + a.lure_kind = wildlife_st.wl_best.kind + a.lure_x = wildlife_st.wl_best.x + a.lure_z = wildlife_st.wl_best.z + a.lure_owner = wildlife_st.wl_best.owner a.state = WILD_APPROACH } # Is there ANY seed down? Asked once a second for the whole flock rather than per bird per tick. -function wl_seed_scan(dt: float) -> void { - wl_seed_t = wl_seed_t - dt - if wl_seed_t > 0.0 { return } - wl_seed_t = 1.0 - wl_seed_any = false - wl_lure_scratch() +function wl_seed_scan(wildlife_st: mut WildlifeState, dt: float) -> void { + wildlife_st.wl_seed_t = wildlife_st.wl_seed_t - dt + if wildlife_st.wl_seed_t > 0.0 { return } + wildlife_st.wl_seed_t = 1.0 + wildlife_st.wl_seed_any = false + wl_lure_scratch(wildlife_st) for i in 0 .. WildlifeWorld.lures() { - if WildlifeWorld.lure(i, wl_ask) and wl_ask.kind == WILD_FEED and wl_ask.diet == WILD_SEED { - wl_seed_any = true + if WildlifeWorld.lure(i, wildlife_st.wl_ask) and wildlife_st.wl_ask.kind == WILD_FEED and wildlife_st.wl_ask.diet == WILD_SEED { + wildlife_st.wl_seed_any = true return } } } # the nearest seed within 120 m, into `wl_best`: the one thing that brings an animal TO you -function wl_bird_seed(a: WildAnimal) -> bool { - if not wl_seed_any { return false } - wl_lure_scratch() +function wl_bird_seed(wildlife_st: mut WildlifeState, a: WildAnimal) -> bool { + if not wildlife_st.wl_seed_any { return false } + wl_lure_scratch(wildlife_st) var bd = 120.0 var got = false for i in 0 .. WildlifeWorld.lures() { - if not WildlifeWorld.lure(i, wl_ask) or wl_ask.kind != WILD_FEED or wl_ask.diet != WILD_SEED { continue } - let dx = wl_ask.x - a.x - let dz = wl_ask.z - a.z + if not WildlifeWorld.lure(i, wildlife_st.wl_ask) or wildlife_st.wl_ask.kind != WILD_FEED or wildlife_st.wl_ask.diet != WILD_SEED { continue } + let dx = wildlife_st.wl_ask.x - a.x + let dz = wildlife_st.wl_ask.z - a.z let d = Math.sqrt(dx * dx + dz * dz) if d < bd { bd = d got = true - wl_copy_lure(wl_ask, wl_best) + wl_copy_lure(wildlife_st.wl_ask, wildlife_st.wl_best) } } return got } # it ate what was set out: calm for three game hours, and the game credits whoever left it -function wl_ate(a: WildAnimal, uid: int, owner: int, diet: int) -> void { +function wl_ate(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, uid: int, owner: int, diet: int) -> void { a.calm = 180.0 a.fed += 1 - let f = wl_fact(WILD_FED, a) + let f = wl_fact(base_st, wildlife_st, WILD_FED, a) f.lure = uid f.owner = owner f.diet = diet diff --git a/packages/ludic.wildlife/migrate.ludic b/packages/ludic.wildlife/migrate.ludic index 90167c00..7715c00a 100644 --- a/packages/ludic.wildlife/migrate.ludic +++ b/packages/ludic.wildlife/migrate.ludic @@ -1,18 +1,18 @@ # migrate.ludic - each morning the ranges slide toward where the season wants them, and the animals # that live in them are dragged along rather than teleported; an animal keeps inside its own -export function wildlife_ranges_migrate() -> void { - let rs = wildlife_ranges() +export function wildlife_ranges_migrate(wildlife_st: mut WildlifeState) -> void { + let rs = wildlife_ranges(wildlife_st) let w = WildlifeWorld.winterness() for i in 0 .. len(rs) { rs[i].x = Math.lerp(rs[i].sx, rs[i].wx, w) rs[i].z = Math.lerp(rs[i].sz, rs[i].wz, w) } - let all = wildlife_all() + let all = wildlife_all(wildlife_st) for i in 0 .. len(all) { let a = all[i] - let r = wildlife_range(a.rg) + let r = wildlife_range(wildlife_st, a.rg) if r == null { continue } - wl_clamp(a) + wl_clamp(wildlife_st, a) if wildlife_range_dist(r, a.home_x, a.home_z) > r.r * 1.2 { a.home_x = r.x a.home_z = r.z @@ -24,8 +24,8 @@ export function wildlife_ranges_migrate() -> void { } # its home kept inside its range -function wl_clamp(a: WildAnimal) -> void { - let r = wildlife_range(a.rg) +function wl_clamp(wildlife_st: mut WildlifeState, a: WildAnimal) -> void { + let r = wildlife_range(wildlife_st, a.rg) if r == null { return } let d = wildlife_range_dist(r, a.home_x, a.home_z) if d < r.r { return } @@ -34,26 +34,26 @@ function wl_clamp(a: WildAnimal) -> void { } # how many live in range `i`, legends apart -export function wildlife_range_alive(i: int) -> int { +export function wildlife_range_alive(wildlife_st: mut WildlifeState, i: int) -> int { var n = 0 - let all = wildlife_all() + let all = wildlife_all(wildlife_st) for k in 0 .. len(all) { if all[k].alive and not all[k].legend and all[k].rg == i { n += 1 } } return n } # how many are alive in the whole valley, legends apart -export function wildlife_total() -> int { +export function wildlife_total(wildlife_st: mut WildlifeState) -> int { var n = 0 - let all = wildlife_all() + let all = wildlife_all(wildlife_st) for k in 0 .. len(all) { if all[k].alive and not all[k].legend { n += 1 } } return n } # the nearest range of `sp` a player has found, to (x, z), or -1 -export function wildlife_range_found_near(sp: int, x: float, z: float) -> int { +export function wildlife_range_found_near(wildlife_st: mut WildlifeState, sp: int, x: float, z: float) -> int { var best = -1 var bd = 99999.0 - let rs = wildlife_ranges() + let rs = wildlife_ranges(wildlife_st) for i in 0 .. len(rs) { if rs[i].sp != sp or not rs[i].found { continue } let d = wildlife_range_dist(rs[i], x, z) diff --git a/packages/ludic.wildlife/needs.ludic b/packages/ludic.wildlife/needs.ludic index 6667e8fc..782bc151 100644 --- a/packages/ludic.wildlife/needs.ludic +++ b/packages/ludic.wildlife/needs.ludic @@ -1,6 +1,6 @@ # needs.ludic - thirst and hunger climb with the game hours (midday is thirsty work, a run costs more # than a graze, dawn and dusk are when they go down to drink), and a walker leaves droppings -function wl_needs(a: WildAnimal, gh: float) -> void { +function wl_needs(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, gh: float) -> void { let h = WildlifeWorld.hour() var thirst = 8.0 var hunger = 5.0 @@ -12,11 +12,11 @@ function wl_needs(a: WildAnimal, gh: float) -> void { if (h > 5.0 and h < 8.0) or (h > 18.0 and h < 21.0) { thirst = thirst * 1.8 } a.thirst = Math.min(a.thirst + thirst * gh, 100.0) a.hunger = Math.min(a.hunger + hunger * gh, 100.0) - if not wl_sp(a.sp).tracks { return } + if not wl_sp(wildlife_st, a.sp).tracks { return } a.scat_t = a.scat_t - gh * 60.0 if a.scat_t > 0.0 { return } - a.scat_t = 120.0 + wl_rnd() * 300.0 - wl_near(a.x, a.z) - if wl_pdist(wl_near_i, a) > 500.0 { return } # further than 500 m from everyone: nobody would find it - wl_fact(WILD_SCAT, a) + a.scat_t = 120.0 + wl_rnd(wildlife_st) * 300.0 + wl_near(wildlife_st, a.x, a.z) + if wl_pdist(wildlife_st.wl_near_i, a) > 500.0 { return } # further than 500 m from everyone: nobody would find it + wl_fact(base_st, wildlife_st, WILD_SCAT, a) } diff --git a/packages/ludic.wildlife/port.ludic b/packages/ludic.wildlife/port.ludic index 954217c1..6f8ac14f 100644 --- a/packages/ludic.wildlife/port.ludic +++ b/packages/ludic.wildlife/port.ludic @@ -66,8 +66,7 @@ function wl_no_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { return fa function wl_no_lure(i: int, l: WildLure) -> bool { return false } function wl_nothing(a: WildAnimal) -> void { } function wl_range_nothing(r: WildRange, i: int) -> void { } -var wl_ids: int = 0 -function wl_next_id() -> int { - wl_ids += 1 - return wl_ids +function wl_next_id(wildlife_st: mut WildlifeState) -> int { + wildlife_st.wl_ids += 1 + return wildlife_st.wl_ids } diff --git a/packages/ludic.wildlife/ranges.ludic b/packages/ludic.wildlife/ranges.ludic index c830523b..fb792c3d 100644 --- a/packages/ludic.wildlife/ranges.ludic +++ b/packages/ludic.wildlife/ranges.ludic @@ -1,16 +1,14 @@ # ranges.ludic - a species keeps to discs placed once, against the ground, 120-700 m out from where # the valley starts; each winters lower, toward the timber, as far down as it summered high -var wl_ranges: []WildRange = null -var wl_found: []int = null # found flags a load brought before the ranges were set out -export function wildlife_ranges() -> []WildRange { - if wl_ranges == null { wl_ranges = new []WildRange } - return wl_ranges +export function wildlife_ranges(wildlife_st: mut WildlifeState) -> []WildRange { + if wildlife_st.wl_ranges == null { wildlife_st.wl_ranges = new []WildRange } + return wildlife_st.wl_ranges } -export function wildlife_range(i: int) -> WildRange { - if i < 0 or i >= len(wildlife_ranges()) { return null } - return wildlife_ranges()[i] +export function wildlife_range(wildlife_st: mut WildlifeState, i: int) -> WildRange { + if i < 0 or i >= len(wildlife_ranges(wildlife_st)) { return null } + return wildlife_ranges(wildlife_st)[i] } export function wildlife_range_dist(r: WildRange, x: float, z: float) -> float { @@ -19,42 +17,42 @@ export function wildlife_range_dist(r: WildRange, x: float, z: float) -> float { return Math.sqrt(dx * dx + dz * dz) } -function wl_range_clashes(sp: int, x: float, z: float) -> bool { - let rs = wildlife_ranges() - for j in 0 .. len(rs) { if rs[j].sp == sp and wildlife_range_dist(rs[j], x, z) < wl_sp(sp).range * 1.6 { return true } } +function wl_range_clashes(wildlife_st: mut WildlifeState, sp: int, x: float, z: float) -> bool { + let rs = wildlife_ranges(wildlife_st) + for j in 0 .. len(rs) { if rs[j].sp == sp and wildlife_range_dist(rs[j], x, z) < wl_sp(wildlife_st, sp).range * 1.6 { return true } } return false } -function wl_range_try(sp: int, x0: float, z0: float) -> bool { - let s = wl_sp(sp) +function wl_range_try(wildlife_st: mut WildlifeState, sp: int, x0: float, z0: float) -> bool { + let s = wl_sp(wildlife_st, sp) for tries in 0 .. 80 { - if not wildlife_find_spot(s.habitat, x0, z0, 120.0, 700.0) { continue } - let dx = wl_px - x0 - let dz = wl_pz - z0 - if Math.sqrt(dx * dx + dz * dz) < s.keep_away or wl_range_clashes(sp, wl_px, wl_pz) { continue } + if not wildlife_find_spot(wildlife_st, s.habitat, x0, z0, 120.0, 700.0) { continue } + let dx = wildlife_st.wl_px - x0 + let dz = wildlife_st.wl_pz - z0 + if Math.sqrt(dx * dx + dz * dz) < s.keep_away or wl_range_clashes(wildlife_st, sp, wildlife_st.wl_px, wildlife_st.wl_pz) { continue } let r = new WildRange r.sp = sp - r.x = wl_px - r.z = wl_pz + r.x = wildlife_st.wl_px + r.z = wildlife_st.wl_pz r.r = s.range - r.sx = wl_px - r.sz = wl_pz + r.sx = wildlife_st.wl_px + r.sz = wildlife_st.wl_pz wl_pick_winter(r) - push(wildlife_ranges(), r) + push(wildlife_ranges(wildlife_st), r) return true } return false } # every species' ranges about (x0, z0), then the game told of each (WildlifeWorld.placed) -export function wildlife_ranges_setup(x0: float, z0: float) -> void { - wildlife_seed(613) - wl_ranges = new []WildRange - for sp in 0 .. wildlife_species_count() { - for k in 0 .. WildlifeWorld.ranges(sp) { if not wl_range_try(sp, x0, z0) { break } } +export function wildlife_ranges_setup(wildlife_st: mut WildlifeState, x0: float, z0: float) -> void { + wildlife_seed(wildlife_st, 613) + wildlife_st.wl_ranges = new []WildRange + for sp in 0 .. wildlife_species_count(wildlife_st) { + for k in 0 .. WildlifeWorld.ranges(sp) { if not wl_range_try(wildlife_st, sp, x0, z0) { break } } } - wl_found_apply() - let rs = wildlife_ranges() + wl_found_apply(wildlife_st) + let rs = wildlife_ranges(wildlife_st) for i in 0 .. len(rs) { WildlifeWorld.placed(rs[i], i) } } @@ -85,13 +83,13 @@ function wl_pick_winter(r: WildRange) -> void { } # a player stood at (x, z): any range they are inside is found -export function wildlife_ranges_visit(x: float, z: float) -> void { - let rs = wildlife_ranges() +export function wildlife_ranges_visit(wildlife_st: mut WildlifeState, x: float, z: float) -> void { + let rs = wildlife_ranges(wildlife_st) for i in 0 .. len(rs) { if not rs[i].found and wildlife_range_dist(rs[i], x, z) < rs[i].r { rs[i].found = true } } } -function wl_found_apply() -> void { - if wl_found == null { return } - let rs = wildlife_ranges() - for i in 0 .. len(rs) { if i < len(wl_found) { rs[i].found = wl_found[i] != 0 } } +function wl_found_apply(wildlife_st: mut WildlifeState) -> void { + if wildlife_st.wl_found == null { return } + let rs = wildlife_ranges(wildlife_st) + for i in 0 .. len(rs) { if i < len(wildlife_st.wl_found) { rs[i].found = wildlife_st.wl_found[i] != 0 } } } diff --git a/packages/ludic.wildlife/react.ludic b/packages/ludic.wildlife/react.ludic index 2cdd3108..d7391865 100644 --- a/packages/ludic.wildlife/react.ludic +++ b/packages/ludic.wildlife/react.ludic @@ -1,27 +1,27 @@ # react.ludic - alert (it stops and looks: the window the camera wants), wary (it puts ground between # you without running) and fleeing; true when it is doing one of them, which is all it does this tick -function wl_react(a: WildAnimal, np: int, dt: float) -> bool { - let s = wl_sp(a.sp) +function wl_react(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dt: float) -> bool { + let s = wl_sp(wildlife_st, a.sp) if a.state == WILD_ALERT { a.speed = 0.0 wl_face(a, a.sus_x, a.sus_z, 2.2, dt) - if a.timer < 0.0 { wl_wander(a, 3.0 + wl_rnd() * 4.0) } + if a.timer < 0.0 { wl_wander(wildlife_st, a, 3.0 + wl_rnd(wildlife_st) * 4.0) } return true } if a.state == WILD_WARY { wl_face(a, a.x + (a.x - a.sus_x), a.z + (a.z - a.sus_z), 2.6, dt) a.speed = s.walk * 0.8 - wl_step(a, dt) - if a.timer < 0.0 { wl_wander(a, 3.0 + wl_rnd() * 4.0) } + wl_step(base_st, wildlife_st, a, dt) + if a.timer < 0.0 { wl_wander(wildlife_st, a, 3.0 + wl_rnd(wildlife_st) * 4.0) } return true } if a.state == WILD_FLEE { wl_face(a, a.x + (a.x - WildlifeWorld.player_x(np)), a.z + (a.z - WildlifeWorld.player_z(np)), 5.0, dt) a.speed = s.run - wl_step(a, dt) + wl_step(base_st, wildlife_st, a, dt) if a.timer < 0.0 { - wl_rehome(a) - wl_wander(a, 5.0) + wl_rehome(wildlife_st, a) + wl_wander(wildlife_st, a, 5.0) } return true } @@ -29,8 +29,8 @@ function wl_react(a: WildAnimal, np: int, dt: float) -> bool { } # walking to (or reached) something set out; true while it is -function wl_approach(a: WildAnimal, dt: float) -> bool { - if (a.state == WILD_IDLE or a.state == WILD_WANDER) and wl_attraction(a) { wl_take_lure(a) } +function wl_approach(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { + if (a.state == WILD_IDLE or a.state == WILD_WANDER) and wl_attraction(wildlife_st, a) { wl_take_lure(wildlife_st, a) } if a.state != WILD_APPROACH { return false } if a.lure == 0 or not WildlifeWorld.present(a.lure) { a.lure = 0 @@ -39,20 +39,20 @@ function wl_approach(a: WildAnimal, dt: float) -> bool { return true } wl_face(a, a.lure_x, a.lure_z, 3.0, dt) - a.speed = wl_sp(a.sp).walk + a.speed = wl_sp(wildlife_st, a.sp).walk let dx = a.lure_x - a.x let dz = a.lure_z - a.z if Math.sqrt(dx * dx + dz * dz) < 1.3 { a.speed = 0.0 - wl_arrive(a) + wl_arrive(base_st, wildlife_st, a) return true } - wl_step(a, dt) + wl_step(base_st, wildlife_st, a, dt) return true } # at a snare it is held; at food it eats for 14 s; at a lure it stands curious for 9 -function wl_arrive(a: WildAnimal) -> void { +function wl_arrive(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal) -> void { a.state = WILD_EAT a.timer = 9.0 if a.lure_kind == WILD_FEED { a.timer = 14.0 } @@ -61,19 +61,19 @@ function wl_arrive(a: WildAnimal) -> void { a.x = a.lure_x a.z = a.lure_z a.y = WildlifeWorld.ground(a.x, a.z) - let f = wl_fact(WILD_CAUGHT, a) + let f = wl_fact(base_st, wildlife_st, WILD_CAUGHT, a) f.lure = a.lure f.owner = a.lure_owner } # eating what it came to; when it is done, food is credited and it wanders off -function wl_eat(a: WildAnimal) -> bool { +function wl_eat(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal) -> bool { if a.state != WILD_EAT { return false } a.speed = 0.0 if a.timer < 0.0 { - if a.lure != 0 and a.lure_kind == WILD_FEED and WildlifeWorld.present(a.lure) { wl_ate(a, a.lure, a.lure_owner, wl_sp(a.sp).diet) } + if a.lure != 0 and a.lure_kind == WILD_FEED and WildlifeWorld.present(a.lure) { wl_ate(base_st, wildlife_st, a, a.lure, a.lure_owner, wl_sp(wildlife_st, a.sp).diet) } a.lure = 0 - wl_wander(a, 4.0) + wl_wander(wildlife_st, a, 4.0) } return true } diff --git a/packages/ludic.wildlife/record.ludic b/packages/ludic.wildlife/record.ludic index b30dfff9..3fc3b50f 100644 --- a/packages/ludic.wildlife/record.ludic +++ b/packages/ludic.wildlife/record.ludic @@ -1,34 +1,31 @@ # record.ludic - what this player has met: the species seen up close and the species fed, as masks # by species number, and whether anything has been fed at all -var wl_seen: int = 0 -var wl_fed: int = 0 -var wl_fed_any: bool = false -export function wildlife_seen(sp: int) -> bool { return (wl_seen & (1 << sp)) != 0 } -export function wildlife_seen_mask() -> int { return wl_seen } -export function wildlife_fed_mask() -> int { return wl_fed } -export function wildlife_fed_any() -> bool { return wl_fed_any } +export function wildlife_seen(wildlife_st: WildlifeState, sp: int) -> bool { return (wildlife_st.wl_seen & (1 << sp)) != 0 } +export function wildlife_seen_mask(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_seen } +export function wildlife_fed_mask(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_fed } +export function wildlife_fed_any(wildlife_st: WildlifeState) -> bool { return wildlife_st.wl_fed_any } # seen up close (a guest's own pass, droppings read, a sighting here); true the first time -export function wildlife_saw(sp: int) -> bool { - if wildlife_seen(sp) { return false } - wl_seen = wl_seen | (1 << sp) +export function wildlife_saw(wildlife_st: mut WildlifeState, sp: int) -> bool { + if wildlife_seen(wildlife_st, sp) { return false } + wildlife_st.wl_seen = wildlife_st.wl_seen | (1 << sp) return true } -export function wildlife_set_seen(mask: int) -> void { wl_seen = mask } +export function wildlife_set_seen(wildlife_st: mut WildlifeState, mask: int) -> void { wildlife_st.wl_seen = mask } # a species fed, credited to this player -export function wildlife_mark_fed(sp: int) -> void { - wl_fed = wl_fed | (1 << sp) - wl_fed_any = true +export function wildlife_mark_fed(wildlife_st: mut WildlifeState, sp: int) -> void { + wildlife_st.wl_fed = wildlife_st.wl_fed | (1 << sp) + wildlife_st.wl_fed_any = true } # nothing fed yet (a test that feeds again), or the flag as given -export function wildlife_set_fed_any(on: bool) -> void { wl_fed_any = on } +export function wildlife_set_fed_any(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_fed_any = on } -function wl_record_reset() -> void { - wl_seen = 0 - wl_fed = 0 - wl_fed_any = false +function wl_record_reset(wildlife_st: mut WildlifeState) -> void { + wildlife_st.wl_seen = 0 + wildlife_st.wl_fed = 0 + wildlife_st.wl_fed_any = false } diff --git a/packages/ludic.wildlife/run.ludic b/packages/ludic.wildlife/run.ludic index df037d85..61f991c0 100644 --- a/packages/ludic.wildlife/run.ludic +++ b/packages/ludic.wildlife/run.ludic @@ -1,12 +1,12 @@ # run.ludic - one step of every animal: the port says whether it is about at this hour; anything # within 900 m of a player thinks; the gait follows the ground covered; player 0 sees what is near -export function wildlife_tick(dt: float, gh: float) -> void { - wl_seed_scan(dt) - let all = wildlife_all() - for i in 0 .. len(all) { wl_tick_one(all[i], dt, gh) } +export function wildlife_tick(base_st: mut BaseState, wildlife_st: mut WildlifeState, dt: float, gh: float) -> void { + wl_seed_scan(wildlife_st, dt) + let all = wildlife_all(wildlife_st) + for i in 0 .. len(all) { wl_tick_one(base_st, wildlife_st, all[i], dt, gh) } } -function wl_tick_one(a: WildAnimal, dt: float, gh: float) -> void { +function wl_tick_one(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void { if not a.alive { return } let about = WildlifeWorld.about(a) if about == WILD_AWAY { @@ -15,26 +15,26 @@ function wl_tick_one(a: WildAnimal, dt: float, gh: float) -> void { } if about == WILD_GONE { wildlife_remove(a) - wl_fact(WILD_WENT, a) + wl_fact(base_st, wildlife_st, WILD_WENT, a) return } a.shown = true - let dh = wl_near(a.x, a.z) + let dh = wl_near(wildlife_st, a.x, a.z) if dh > 900.0 { return } - let s = wl_sp(a.sp) - if wl_hold { } + let s = wl_sp(wildlife_st, a.sp) + if wildlife_st.wl_hold { } else if a.pinned { a.speed = 0.0 } - else if s.flies { wildlife_tick_bird(a, dt) } else { wildlife_tick_ground(a, dt, gh) } + else if s.flies { wildlife_tick_bird(base_st, wildlife_st, a, dt) } else { wildlife_tick_ground(base_st, wildlife_st, a, dt, gh) } var stride = 1.2 * a.scale if s.flies { stride = 0.6 } a.phase = (a.phase + a.speed * dt / stride * WL_TAU) % WL_TAU - if WildlifeWorld.player_on(0) and wl_pdist(0, a) < 60.0 and wildlife_saw(a.sp) { wl_fact(WILD_SPOTTED, a) } + if WildlifeWorld.player_on(0) and wl_pdist(0, a) < 60.0 and wildlife_saw(wildlife_st, a.sp) { wl_fact(base_st, wildlife_st, WILD_SPOTTED, a) } } # the gait advanced by a copy the game moves itself (a guest's copy of the host's animal) -export function wildlife_stride(a: WildAnimal, dt: float) -> void { +export function wildlife_stride(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> void { var stride = 1.2 * a.scale - if wl_sp(a.sp).flies { stride = 0.6 } + if wl_sp(wildlife_st, a.sp).flies { stride = 0.6 } a.phase = (a.phase + a.speed * dt / stride * WL_TAU) % WL_TAU } @@ -48,8 +48,8 @@ export function wildlife_release(a: WildAnimal) -> void { } # the animal a snare holds, by the snare's uid -export function wildlife_in_snare(uid: int) -> WildAnimal { - let all = wildlife_all() +export function wildlife_in_snare(wildlife_st: mut WildlifeState, uid: int) -> WildAnimal { + let all = wildlife_all(wildlife_st) for i in 0 .. len(all) { let a = all[i] if a.alive and a.state == WILD_TRAPPED and a.lure == uid { return a } diff --git a/packages/ludic.wildlife/senses.ludic b/packages/ludic.wildlife/senses.ludic index 554be6af..067c0850 100644 --- a/packages/ludic.wildlife/senses.ludic +++ b/packages/ludic.wildlife/senses.ludic @@ -1,18 +1,17 @@ # senses.ludic - what an animal makes of one player: sight (a clear line, closer is worse, the dark # helps), scent (downwind, further than sight), noise; the rise is NET of how fast it forgets, so # an alert settles where the distance puts it instead of climbing to the top and sticking -var wl_near_i: int = 0 # the nearest player's distance; `wl_near_i` says who -function wl_near(x: float, z: float) -> float { +function wl_near(wildlife_st: mut WildlifeState, x: float, z: float) -> float { var best = 999999999999.0 - wl_near_i = 0 + wildlife_st.wl_near_i = 0 for p in 0 .. WildlifeWorld.players() { if not WildlifeWorld.player_on(p) { continue } let d = wl_pdist2(p, x, z) if d < best { best = d - wl_near_i = p + wildlife_st.wl_near_i = p } } return Math.sqrt(best) @@ -47,8 +46,8 @@ function wl_near_sq(d: float, reach: float) -> float { } # alert points a second from player p at `dh` metres; 0 when nothing of them reaches it -export function wildlife_alert_rate(a: WildAnimal, p: int, dh: float) -> float { - let s = wl_sp(a.sp) +export function wildlife_alert_rate(wildlife_st: mut WildlifeState, a: WildAnimal, p: int, dh: float) -> float { + let s = wl_sp(wildlife_st, a.sp) if s.tame or a.calm > 0.0 { return 0.0 } if (a.state == WILD_APPROACH or a.state == WILD_EAT) and a.lure != 0 and a.lure_kind == WILD_FEED and dh > 5.0 and WildlifeWorld.taught() { return 0.0 } var reach = s.flee * WildlifeWorld.wariness() @@ -66,8 +65,8 @@ export function wildlife_alert_rate(a: WildAnimal, p: int, dh: float) -> float { } # how fast it forgets player p: standing still buys it, sitting down buys more -export function wildlife_alert_decay(a: WildAnimal, p: int) -> float { - var d = wl_sp(a.sp).forget +export function wildlife_alert_decay(wildlife_st: mut WildlifeState, a: WildAnimal, p: int) -> float { + var d = wl_sp(wildlife_st, a.sp).forget let still = WildlifeWorld.stillness(p) if still == WILD_SITTING { d = d * 2.5 } else if still == WILD_STILL { d = d * 1.6 } if a.spook > 0.0 { d = d * 0.7 } @@ -75,14 +74,14 @@ export function wildlife_alert_decay(a: WildAnimal, p: int) -> float { } # the alert rises and falls with the player who nets it the most, and it watches them -function wl_alert(a: WildAnimal, np: int, dh: float, dt: float) -> void { - var rise = wildlife_alert_rate(a, np, dh) - var fall = wildlife_alert_decay(a, np) +function wl_alert(wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dh: float, dt: float) -> void { + var rise = wildlife_alert_rate(wildlife_st, a, np, dh) + var fall = wildlife_alert_decay(wildlife_st, a, np) var who = np for p in 0 .. WildlifeWorld.players() { if p == np or not WildlifeWorld.player_on(p) { continue } - let r2 = wildlife_alert_rate(a, p, wl_pdist(p, a)) - let f2 = wildlife_alert_decay(a, p) + let r2 = wildlife_alert_rate(wildlife_st, a, p, wl_pdist(p, a)) + let f2 = wildlife_alert_decay(wildlife_st, a, p) if r2 - f2 > rise - fall { rise = r2 fall = f2 diff --git a/packages/ludic.wildlife/spawn.ludic b/packages/ludic.wildlife/spawn.ludic index c64f4a94..b3403462 100644 --- a/packages/ludic.wildlife/spawn.ludic +++ b/packages/ludic.wildlife/spawn.ludic @@ -1,21 +1,21 @@ # spawn.ludic - a valley filled: ranges set out, then each range to its target on its own habitat. # Each morning the ranges move with the season and every range under its target gets ONE back, # out of every player's sight: a valley recovers, it does not refill. -export function wildlife_spawn(x0: float, z0: float) -> void { - wildlife_ranges_setup(x0, z0) - wildlife_seed(97) - let rs = wildlife_ranges() - for i in 0 .. len(rs) { wl_fill(i, WildlifeWorld.per_range(rs[i].sp)) } - wildlife_ranges_migrate() +export function wildlife_spawn(wildlife_st: mut WildlifeState, x0: float, z0: float) -> void { + wildlife_ranges_setup(wildlife_st, x0, z0) + wildlife_seed(wildlife_st, 97) + let rs = wildlife_ranges(wildlife_st) + for i in 0 .. len(rs) { wl_fill(wildlife_st, i, WildlifeWorld.per_range(rs[i].sp)) } + wildlife_ranges_migrate(wildlife_st) } # up to `want` more in range i; returns how many went in -function wl_fill(i: int, want: int) -> int { - let r = wildlife_range(i) +function wl_fill(wildlife_st: mut WildlifeState, i: int, want: int) -> int { + let r = wildlife_range(wildlife_st, i) var made = 0 for k in 0 .. want { - if not wildlife_find_spot(wl_sp(r.sp).habitat, r.x, r.z, 0.0, r.r) { continue } - let a = wildlife_new(r.sp, wl_px, wl_pz, false) + if not wildlife_find_spot(wildlife_st, wl_sp(wildlife_st, r.sp).habitat, r.x, r.z, 0.0, r.r) { continue } + let a = wildlife_new(wildlife_st, r.sp, wildlife_st.wl_px, wildlife_st.wl_pz, false) if a != null { a.rg = i made += 1 @@ -25,22 +25,22 @@ function wl_fill(i: int, want: int) -> int { } # each morning: the ranges slide with the season, and the valley gets back what it lost -export function wildlife_morning() -> int { - wildlife_ranges_migrate() - return wildlife_repopulate() +export function wildlife_morning(wildlife_st: mut WildlifeState) -> int { + wildlife_ranges_migrate(wildlife_st) + return wildlife_repopulate(wildlife_st) } # one newcomer for each range under its target, never within 200 m of a player; how many came -export function wildlife_repopulate() -> int { - let rs = wildlife_ranges() +export function wildlife_repopulate(wildlife_st: mut WildlifeState) -> int { + let rs = wildlife_ranges(wildlife_st) var added = 0 for i in 0 .. len(rs) { let want = WildlifeWorld.per_range(rs[i].sp) - if want == 0 or wildlife_range_alive(i) >= want { continue } + if want == 0 or wildlife_range_alive(wildlife_st, i) >= want { continue } for t in 0 .. 12 { - if not wildlife_find_spot(wl_sp(rs[i].sp).habitat, rs[i].x, rs[i].z, 0.0, rs[i].r) { continue } - if wl_near(wl_px, wl_pz) < 200.0 { continue } - let a = wildlife_new(rs[i].sp, wl_px, wl_pz, false) + if not wildlife_find_spot(wildlife_st, wl_sp(wildlife_st, rs[i].sp).habitat, rs[i].x, rs[i].z, 0.0, rs[i].r) { continue } + if wl_near(wildlife_st, wildlife_st.wl_px, wildlife_st.wl_pz) < 200.0 { continue } + let a = wildlife_new(wildlife_st, rs[i].sp, wildlife_st.wl_px, wildlife_st.wl_pz, false) if a != null { a.rg = i added += 1 diff --git a/packages/ludic.wildlife/species.ludic b/packages/ludic.wildlife/species.ludic index 30b82c1f..767d2bc0 100644 --- a/packages/ludic.wildlife/species.ludic +++ b/packages/ludic.wildlife/species.ludic @@ -32,26 +32,48 @@ export property WildSpecies { scale: float = 1.0 # its size, times } -var wl_species: []WildSpecies = null - -function wl_sp_all() -> []WildSpecies { - if wl_species == null { wl_species = new []WildSpecies } - return wl_species +export state WildlifeState { + wl_facts: Queue = null + wl_own: []WildAnimal = null + wl_px: float = 0.0 + wl_pz: float = 0.0 + wl_ask: WildLure = null + wl_best: WildLure = null + wl_seed_any: bool = false + wl_seed_t: float = 0.0 + wl_ids: int = 0 + wl_ranges: []WildRange = null + wl_found: []int = null # found flags a load brought before the ranges were set out + wl_seen: int = 0 + wl_fed: int = 0 + wl_fed_any: bool = false + wl_near_i: int = 0 + wl_species: []WildSpecies = null + wl_list: []WildAnimal = null + wl_keys: int = 0 + wl_dice: Rng = null + wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed + wl_spot_ask: WildSpot = null } -export function wildlife_species_clear() -> void { wl_species = new []WildSpecies } +function wl_sp_all(wildlife_st: mut WildlifeState) -> []WildSpecies { + if wildlife_st.wl_species == null { wildlife_st.wl_species = new []WildSpecies } + return wildlife_st.wl_species +} + +export function wildlife_species_clear(wildlife_st: mut WildlifeState) -> void { wildlife_st.wl_species = new []WildSpecies } # the next species; returns its number -export function wildlife_species_add(s: WildSpecies) -> int { - push(wl_sp_all(), s) - return len(wl_sp_all()) - 1 +export function wildlife_species_add(wildlife_st: mut WildlifeState, s: WildSpecies) -> int { + push(wl_sp_all(wildlife_st), s) + return len(wl_sp_all(wildlife_st)) - 1 } -export function wildlife_species_count() -> int { return len(wl_sp_all()) } +export function wildlife_species_count(wildlife_st: mut WildlifeState) -> int { return len(wl_sp_all(wildlife_st)) } -export function wildlife_species(sp: int) -> WildSpecies { - if sp < 0 or sp >= len(wl_sp_all()) { return null } - return wl_sp_all()[sp] +export function wildlife_species(wildlife_st: mut WildlifeState, sp: int) -> WildSpecies { + if sp < 0 or sp >= len(wl_sp_all(wildlife_st)) { return null } + return wl_sp_all(wildlife_st)[sp] } -function wl_sp(sp: int) -> WildSpecies { return wl_sp_all()[sp] } +function wl_sp(wildlife_st: mut WildlifeState, sp: int) -> WildSpecies { return wl_sp_all(wildlife_st)[sp] } diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index 510b84e7..14230b61 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -14,7 +14,7 @@ function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> } # true when it moved -function wl_step(a: WildAnimal, dt: float) -> bool { +function wl_step(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { let d = a.speed * dt if d < 0.0001 { return false } let nx = a.x - Math.sin(a.yaw) * d @@ -29,16 +29,16 @@ function wl_step(a: WildAnimal, dt: float) -> bool { a.z = nz a.y = nh a.track_d = a.track_d + d - if wl_sp(a.sp).tracks and a.track_d > 2.8 { + if wl_sp(wildlife_st, a.sp).tracks and a.track_d > 2.8 { a.track_d = 0.0 - wl_fact(WILD_PRINT, a) + wl_fact(base_st, wildlife_st, WILD_PRINT, a) } return true } -function wl_pick_target(a: WildAnimal, radius: float) -> void { - let ang = wl_rnd() * WL_TAU - let d = wl_rnd() * radius +function wl_pick_target(wildlife_st: mut WildlifeState, a: WildAnimal, radius: float) -> void { + let ang = wl_rnd(wildlife_st) * WL_TAU + let d = wl_rnd(wildlife_st) * radius a.tx = a.home_x + Math.cos(ang) * d a.tz = a.home_z + Math.sin(ang) * d } @@ -50,15 +50,15 @@ function wl_at_target(a: WildAnimal, r: float) -> bool { } # back to wandering about home after `t` seconds of whatever it was doing -function wl_wander(a: WildAnimal, t: float) -> void { +function wl_wander(wildlife_st: mut WildlifeState, a: WildAnimal, t: float) -> void { a.state = WILD_WANDER a.timer = t - wl_pick_target(a, wl_sp(a.sp).home) + wl_pick_target(wildlife_st, a, wl_sp(wildlife_st, a.sp).home) } # home where it stands now, kept inside its range -function wl_rehome(a: WildAnimal) -> void { +function wl_rehome(wildlife_st: mut WildlifeState, a: WildAnimal) -> void { a.home_x = a.x a.home_z = a.z - wl_clamp(a) + wl_clamp(wildlife_st, a) } diff --git a/packages/ludic.wildlife/store.ludic b/packages/ludic.wildlife/store.ludic index 067b61db..d40979c7 100644 --- a/packages/ludic.wildlife/store.ludic +++ b/packages/ludic.wildlife/store.ludic @@ -1,37 +1,33 @@ # store.ludic - the animals, their own dice, and the verbs that make and take one away -var wl_list: []WildAnimal = null -var wl_keys: int = 0 -var wl_dice: Rng = null -var wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed const WL_TAU: float = 6.2831853 -export function wildlife_all() -> []WildAnimal { - if wl_list == null { wl_list = new []WildAnimal } - return wl_list +export function wildlife_all(wildlife_st: mut WildlifeState) -> []WildAnimal { + if wildlife_st.wl_list == null { wildlife_st.wl_list = new []WildAnimal } + return wildlife_st.wl_list } -export function wildlife_count_all() -> int { return len(wildlife_all()) } +export function wildlife_count_all(wildlife_st: mut WildlifeState) -> int { return len(wildlife_all(wildlife_st)) } # a new valley: every animal forgotten, quietly (the game has already let its drawings go) -export function wildlife_clear() -> void { wl_list = new []WildAnimal } +export function wildlife_clear(wildlife_st: mut WildlifeState) -> void { wildlife_st.wl_list = new []WildAnimal } -function wl_rng() -> Rng { - if wl_dice == null { wl_dice = rng_new(97) } - return wl_dice +function wl_rng(wildlife_st: mut WildlifeState) -> Rng { + if wildlife_st.wl_dice == null { wildlife_st.wl_dice = rng_new(97) } + return wildlife_st.wl_dice } -function wl_rnd() -> float { return rng_float(wl_rng()) } +function wl_rnd(wildlife_st: mut WildlifeState) -> float { return rng_float(wl_rng(wildlife_st)) } # the package's dice from a known place (a spawn, a test) -export function wildlife_seed(seed: int) -> void { rng_seed(wl_rng(), seed) } +export function wildlife_seed(wildlife_st: mut WildlifeState, seed: int) -> void { rng_seed(wl_rng(wildlife_st), seed) } # an individual exactly as given: the host's own, or the host's on a guest -export function wildlife_make(sp: int, x: float, z: float, legend: bool, yaw: float, variant: int, scale: float, nid: int) -> WildAnimal { - let s = wildlife_species(sp) +export function wildlife_make(wildlife_st: mut WildlifeState, sp: int, x: float, z: float, legend: bool, yaw: float, variant: int, scale: float, nid: int) -> WildAnimal { + let s = wildlife_species(wildlife_st, sp) if s == null { return null } let a = new WildAnimal - wl_keys += 1 - a.key = wl_keys + wildlife_st.wl_keys += 1 + a.key = wildlife_st.wl_keys a.nid = nid a.sp = sp a.x = x @@ -46,27 +42,27 @@ export function wildlife_make(sp: int, x: float, z: float, legend: bool, yaw: fl a.scale = scale a.legend = legend a.state = WILD_IDLE - push(wildlife_all(), a) + push(wildlife_all(wildlife_st), a) WildlifeWorld.born(a) return a } # a new individual of a species at (x, z), off the package's dice -export function wildlife_new(sp: int, x: float, z: float, legend: bool) -> WildAnimal { - let s = wildlife_species(sp) +export function wildlife_new(wildlife_st: mut WildlifeState, sp: int, x: float, z: float, legend: bool) -> WildAnimal { + let s = wildlife_species(wildlife_st, sp) if s == null { return null } - let yaw = wl_rnd() * WL_TAU - let variant = rng_between(wl_rng(), 0, 11) - let scale = s.scale * (0.88 + wl_rnd() * 0.24) - let a = wildlife_make(sp, x, z, legend, yaw, variant, scale, WildlifeWorld.next_id()) - a.thirst = wl_rnd() * 50.0 - a.hunger = wl_rnd() * 50.0 - a.scat_t = 40.0 + wl_rnd() * 180.0 + let yaw = wl_rnd(wildlife_st) * WL_TAU + let variant = rng_between(wl_rng(wildlife_st), 0, 11) + let scale = s.scale * (0.88 + wl_rnd(wildlife_st) * 0.24) + let a = wildlife_make(wildlife_st, sp, x, z, legend, yaw, variant, scale, WildlifeWorld.next_id()) + a.thirst = wl_rnd(wildlife_st) * 50.0 + a.hunger = wl_rnd(wildlife_st) * 50.0 + a.scat_t = 40.0 + wl_rnd(wildlife_st) * 180.0 if s.flies { a.state = WILD_FLY - a.fly_h = 10.0 + wl_rnd() * 20.0 + a.fly_h = 10.0 + wl_rnd(wildlife_st) * 20.0 } - a.timer = wl_rnd() * 6.0 + a.timer = wl_rnd(wildlife_st) * 6.0 return a } @@ -78,18 +74,18 @@ export function wildlife_remove(a: WildAnimal) -> void { } # staged for a picture: nobody thinks, and everybody keeps the speed they were given -export function wildlife_hold(on: bool) -> void { wl_hold = on } +export function wildlife_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on } -export function wildlife_by_nid(nid: int) -> WildAnimal { - let all = wildlife_all() +export function wildlife_by_nid(wildlife_st: mut WildlifeState, nid: int) -> WildAnimal { + let all = wildlife_all(wildlife_st) for i in 0 .. len(all) { if all[i].nid == nid { return all[i] } } return null } # how many of a species are alive, legends apart -export function wildlife_count(sp: int) -> int { +export function wildlife_count(wildlife_st: mut WildlifeState, sp: int) -> int { var n = 0 - let all = wildlife_all() + let all = wildlife_all(wildlife_st) for i in 0 .. len(all) { if all[i].alive and all[i].sp == sp and not all[i].legend { n += 1 } } return n } diff --git a/packages/ludic.wildlife/system.ludic b/packages/ludic.wildlife/system.ludic index 0eb32983..3feab5de 100644 --- a/packages/ludic.wildlife/system.ludic +++ b/packages/ludic.wildlife/system.ludic @@ -1,21 +1,21 @@ # system.ludic - the wildlife as a system: its save section is this player's record (the species # seen and fed) and which ranges they have found; the animals themselves are respawned with the # valley, so a reset forgets the record and nothing else -export function wildlife_reset() -> void { - wl_record_reset() - wl_found = null - let rs = wildlife_ranges() +export function wildlife_reset(base_st: mut BaseState, wildlife_st: mut WildlifeState) -> void { + wl_record_reset(wildlife_st) + wildlife_st.wl_found = null + let rs = wildlife_ranges(wildlife_st) for i in 0 .. len(rs) { rs[i].found = false } - q_clear(wildlife_facts()) + q_clear(base_st, wildlife_facts(base_st, wildlife_st)) } -export function wildlife_save() -> Val { +export function wildlife_save(wildlife_st: mut WildlifeState) -> Val { let v = Value.object() - sv_put_int(v, "seen", wl_seen) - sv_put_int(v, "fed", wl_fed) - sv_put_bool(v, "fed_any", wl_fed_any) + sv_put_int(v, "seen", wildlife_st.wl_seen) + sv_put_int(v, "fed", wildlife_st.wl_fed) + sv_put_bool(v, "fed_any", wildlife_st.wl_fed_any) let found = new []int - let rs = wildlife_ranges() + let rs = wildlife_ranges(wildlife_st) for i in 0 .. len(rs) { var f = 0 if rs[i].found { f = 1 } @@ -25,13 +25,13 @@ export function wildlife_save() -> Val { return v } -export function wildlife_load(v: Val, version: int) -> void { - wildlife_reset() - wl_seen = sv_int(v, "seen", 0) - wl_fed = sv_int(v, "fed", 0) - wl_fed_any = sv_bool(v, "fed_any", false) - wl_found = sv_ints(v, "found") - wl_found_apply() +export function wildlife_load(base_st: mut BaseState, wildlife_st: mut WildlifeState, v: Val, version: int) -> void { + wildlife_reset(base_st, wildlife_st) + wildlife_st.wl_seen = sv_int(v, "seen", 0) + wildlife_st.wl_fed = sv_int(v, "fed", 0) + wildlife_st.wl_fed_any = sv_bool(v, "fed_any", false) + wildlife_st.wl_found = sv_ints(v, "found") + wl_found_apply(wildlife_st) } # PH_SIMULATE with no tick of its own: the game steps it (wildlife_tick) where the world is owned diff --git a/packages/ludic.wildlife/tests/fake/index.ludic b/packages/ludic.wildlife/tests/fake/index.ludic index 50d45429..05e036e6 100644 --- a/packages/ludic.wildlife/tests/fake/index.ludic +++ b/packages/ludic.wildlife/tests/fake/index.ludic @@ -1,37 +1,39 @@ # fake.ludic - a toy valley for the tests: flat ground at 10 m with a lake west of x = -300 (its # surface at 10.5), forest north of z = 400, up to two players, and a table of things set out -var fk_px: []float = null -var fk_pz: []float = null -var fk_on: []bool = null -var fk_noise: float = 0.0 -var fk_scent: float = 0.0 -var fk_still: int = 0 -var fk_hidden: bool = false -var fk_blocked: bool = false -var fk_wind: float = 0.0 -var fk_wind_dir: float = 0.0 -var fk_winter: float = 0.0 -var fk_about: int = 0 -var fk_born: int = 0 -var fk_lures: []WildLure = null -var fk_gone: int = 0 # a uid the port says is no longer there +export state WildlifeTestsFakeState { + fk_px: []float = null + fk_pz: []float = null + fk_on: []bool = null + fk_noise: float = 0.0 + fk_scent: float = 0.0 + fk_still: int = 0 + fk_hidden: bool = false + fk_blocked: bool = false + fk_wind: float = 0.0 + fk_wind_dir: float = 0.0 + fk_winter: float = 0.0 + fk_about: int = 0 + fk_born: int = 0 + fk_lures: []WildLure = null + fk_gone: int = 0 # a uid the port says is no longer there +} -function fk_setup() -> void { - fk_px = floats(2) - fk_pz = floats(2) - fk_on = new []bool - push(fk_on, true) - push(fk_on, false) - fk_px[0] = 5000.0 - fk_pz[0] = 5000.0 - fk_lures = new []WildLure - wildlife_clear() - wildlife_species_clear() - fk_species() +function fk_setup(wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) -> void { + wildlife_tests_fake_st.fk_px = floats(2) + wildlife_tests_fake_st.fk_pz = floats(2) + wildlife_tests_fake_st.fk_on = new []bool + push(wildlife_tests_fake_st.fk_on, true) + push(wildlife_tests_fake_st.fk_on, false) + wildlife_tests_fake_st.fk_px[0] = 5000.0 + wildlife_tests_fake_st.fk_pz[0] = 5000.0 + wildlife_tests_fake_st.fk_lures = new []WildLure + wildlife_clear(wildlife_st) + wildlife_species_clear(wildlife_st) + fk_species(wildlife_st) } # 0 deer (meadow, tracks, medium), 1 bear (forest, charges), 2 hare (small), 3 jay (flies) -function fk_species() -> void { +function fk_species(wildlife_st: mut WildlifeState) -> void { let d = new WildSpecies d.key = "deer" d.walk = 1.1 @@ -41,7 +43,7 @@ function fk_species() -> void { d.size = 1 d.range = 150.0 d.keep_away = 120.0 - wildlife_species_add(d) + wildlife_species_add(wildlife_st, d) let b = new WildSpecies b.key = "bear" b.habitat = WILD_FOREST @@ -50,10 +52,10 @@ function fk_species() -> void { b.diet = WILD_FISH b.forget = 1.6 b.run = 6.0 - wildlife_species_add(b) + wildlife_species_add(wildlife_st, b) let h = new WildSpecies h.key = "hare" - wildlife_species_add(h) + wildlife_species_add(wildlife_st, h) let j = new WildSpecies j.key = "jay" j.flies = true @@ -61,7 +63,7 @@ function fk_species() -> void { j.flee = 8.0 j.land_shy = 0.0 j.land_chance = 1.0 - wildlife_species_add(j) + wildlife_species_add(wildlife_st, j) } function fk_ground(x: float, z: float) -> float { return 10.0 } @@ -70,24 +72,24 @@ function fk_water(x: float, z: float) -> float { return 0.0 } function fk_forest(x: float, z: float) -> bool { return z > 400.0 } -function fk_sight(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return not fk_blocked } +function fk_sight(wildlife_tests_fake_st: WildlifeTestsFakeState, ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return not wildlife_tests_fake_st.fk_blocked } function fk_players() -> int { return 2 } -function fk_player_on(p: int) -> bool { return fk_on[p] } -function fk_x(p: int) -> float { return fk_px[p] } +function fk_player_on(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> bool { return wildlife_tests_fake_st.fk_on[p] } +function fk_x(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> float { return wildlife_tests_fake_st.fk_px[p] } function fk_y(p: int) -> float { return 10.0 } -function fk_z(p: int) -> float { return fk_pz[p] } -function fk_is_hidden(p: int) -> bool { return fk_hidden } -function fk_noise_of(p: int) -> float { return fk_noise } -function fk_scent_of(p: int) -> float { return fk_scent } -function fk_still_of(p: int) -> int { return fk_still } -function fk_wind_of() -> float { return fk_wind } -function fk_wind_to() -> float { return fk_wind_dir } -function fk_winter_of() -> float { return fk_winter } -function fk_about_of(a: WildAnimal) -> int { return fk_about } +function fk_z(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> float { return wildlife_tests_fake_st.fk_pz[p] } +function fk_is_hidden(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> bool { return wildlife_tests_fake_st.fk_hidden } +function fk_noise_of(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> float { return wildlife_tests_fake_st.fk_noise } +function fk_scent_of(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> float { return wildlife_tests_fake_st.fk_scent } +function fk_still_of(wildlife_tests_fake_st: WildlifeTestsFakeState, p: int) -> int { return wildlife_tests_fake_st.fk_still } +function fk_wind_of(wildlife_tests_fake_st: WildlifeTestsFakeState) -> float { return wildlife_tests_fake_st.fk_wind } +function fk_wind_to(wildlife_tests_fake_st: WildlifeTestsFakeState) -> float { return wildlife_tests_fake_st.fk_wind_dir } +function fk_winter_of(wildlife_tests_fake_st: WildlifeTestsFakeState) -> float { return wildlife_tests_fake_st.fk_winter } +function fk_about_of(wildlife_tests_fake_st: WildlifeTestsFakeState, a: WildAnimal) -> int { return wildlife_tests_fake_st.fk_about } function fk_one(sp: int) -> int { return 1 } -function fk_count() -> int { return len(fk_lures) } -function fk_lure(i: int, l: WildLure) -> bool { - let f = fk_lures[i] +function fk_count(wildlife_tests_fake_st: WildlifeTestsFakeState) -> int { return len(wildlife_tests_fake_st.fk_lures) } +function fk_lure(wildlife_tests_fake_st: WildlifeTestsFakeState, i: int, l: WildLure) -> bool { + let f = wildlife_tests_fake_st.fk_lures[i] l.uid = f.uid l.kind = f.kind l.diet = f.diet @@ -95,10 +97,10 @@ function fk_lure(i: int, l: WildLure) -> bool { l.x = f.x l.z = f.z l.owner = f.owner - return f.uid != fk_gone + return f.uid != wildlife_tests_fake_st.fk_gone } -function fk_present(uid: int) -> bool { return uid != fk_gone } -function fk_on_born(a: WildAnimal) -> void { fk_born += 1 } +function fk_present(wildlife_tests_fake_st: WildlifeTestsFakeState, uid: int) -> bool { return uid != wildlife_tests_fake_st.fk_gone } +function fk_on_born(wildlife_tests_fake_st: mut WildlifeTestsFakeState, a: WildAnimal) -> void { wildlife_tests_fake_st.fk_born += 1 } function fk_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { s.uid = 900 s.x = a.x + 30.0 diff --git a/packages/ludic.wildlife/tests/lures_test.ludic b/packages/ludic.wildlife/tests/lures_test.ludic index 4f5d7ab2..8c729f7d 100644 --- a/packages/ludic.wildlife/tests/lures_test.ludic +++ b/packages/ludic.wildlife/tests/lures_test.ludic @@ -19,8 +19,8 @@ program LuresTest { spot: fn fk_spot } - function facts() -> []WildFact { return q_drain(wildlife_facts()) } - function set_out(uid: int, kind: int, diet: int, x: float, z: float, owner: int) -> WildLure { + function facts(base_st: mut BaseState, wildlife_st: mut WildlifeState) -> []WildFact { return q_drain(base_st, wildlife_facts(base_st, wildlife_st)) } + function set_out(wildlife_tests_fake_st: mut WildlifeTestsFakeState, uid: int, kind: int, diet: int, x: float, z: float, owner: int) -> WildLure { let l = new WildLure l.uid = uid l.kind = kind @@ -28,24 +28,24 @@ program LuresTest { l.x = x l.z = z l.owner = owner - push(fk_lures, l) + push(wildlife_tests_fake_st.fk_lures, l) return l } - function idle(sp: int, x: float, z: float) -> WildAnimal { - let a = wildlife_make(sp, x, z, false, 0.0, 0, 1.0, 7) + function idle(wildlife_st: mut WildlifeState, sp: int, x: float, z: float) -> WildAnimal { + let a = wildlife_make(wildlife_st, sp, x, z, false, 0.0, 0, 1.0, 7) a.timer = 30.0 return a } - function go(a: WildAnimal, secs: float) -> void { - for k in 0 .. int(secs * 20.0) { wildlife_tick_ground(a, 0.05, 0.05 / 60.0) } + function go(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, secs: float) -> void { + for k in 0 .. int(secs * 20.0) { wildlife_tick_ground(base_st, wildlife_st, a, 0.05, 0.05 / 60.0) } } - test "food of its diet draws it in, it eats, and the fact names what and whose" { - fk_setup() - set_out(41, WILD_FEED, WILD_PLANTS, 20.0, 0.0, 3) - let a = idle(0, 0.0, 0.0) - go(a, 40.0) - let fs = facts() + test "food of its diet draws it in, it eats, and the fact names what and whose" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + set_out(wildlife_tests_fake_st, 41, WILD_FEED, WILD_PLANTS, 20.0, 0.0, 3) + let a = idle(wildlife_st, 0, 0.0, 0.0) + go(base_st, wildlife_st, a, 40.0) + let fs = facts(base_st, wildlife_st) var fed = 0 for i in 0 .. len(fs) { if fs[i].what == WILD_FED { @@ -58,70 +58,70 @@ program LuresTest { expect(a.calm > 0.0) } - test "food of another diet is left alone" { - fk_setup() - set_out(41, WILD_FEED, WILD_FISH, 20.0, 0.0, -1) - let a = idle(0, 0.0, 0.0) - go(a, 5.0) + test "food of another diet is left alone" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + set_out(wildlife_tests_fake_st, 41, WILD_FEED, WILD_FISH, 20.0, 0.0, -1) + let a = idle(wildlife_st, 0, 0.0, 0.0) + go(base_st, wildlife_st, a, 5.0) expect(a.state != WILD_APPROACH) expect_eq(a.lure, 0) } - test "food taken away while it walks is given up" { - fk_setup() - set_out(41, WILD_FEED, WILD_PLANTS, 50.0, 0.0, -1) - let a = idle(0, 0.0, 0.0) - go(a, 1.0) + test "food taken away while it walks is given up" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + set_out(wildlife_tests_fake_st, 41, WILD_FEED, WILD_PLANTS, 50.0, 0.0, -1) + let a = idle(wildlife_st, 0, 0.0, 0.0) + go(base_st, wildlife_st, a, 1.0) expect_eq(a.state, WILD_APPROACH) - fk_gone = 41 - go(a, 0.2) + wildlife_tests_fake_st.fk_gone = 41 + go(base_st, wildlife_st, a, 0.2) expect(a.state != WILD_APPROACH) } - test "a snare holds a small animal, the port can find it, and a release lets it go" { - fk_setup() - let l = set_out(55, WILD_SNARE, 0, 10.0, 0.0, -1) + test "a snare holds a small animal, the port can find it, and a release lets it go" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let l = set_out(wildlife_tests_fake_st, 55, WILD_SNARE, 0, 10.0, 0.0, -1) l.size = 0 - let h = idle(2, 0.0, 0.0) + let h = idle(wildlife_st, 2, 0.0, 0.0) for k in 0 .. 40 { if h.state == WILD_TRAPPED { break } - go(h, 2.0) + go(base_st, wildlife_st, h, 2.0) } expect_eq(h.state, WILD_TRAPPED) - expect(wildlife_in_snare(55) == h) - let fs = facts() + expect(wildlife_in_snare(wildlife_st, 55) == h) + let fs = facts(base_st, wildlife_st) var held = false for i in 0 .. len(fs) { if fs[i].what == WILD_CAUGHT and fs[i].lure == 55 { held = true } } expect(held) wildlife_release(h) expect_eq(h.state, WILD_FLEE) - expect(wildlife_in_snare(55) == null) + expect(wildlife_in_snare(wildlife_st, 55) == null) } - test "a thirsty animal walks to the water the port offers, and drinks it down" { - fk_setup() - let a = idle(0, 0.0, 0.0) + test "a thirsty animal walks to the water the port offers, and drinks it down" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = idle(wildlife_st, 0, 0.0, 0.0) a.thirst = 90.0 a.state = WILD_WANDER - go(a, 0.1) + go(base_st, wildlife_st, a, 0.1) expect_eq(a.state, WILD_GO_DRINK) expect_eq(a.spot, 900) - go(a, 40.0) + go(base_st, wildlife_st, a, 40.0) expect(a.thirst < 60.0) } - test "seed down brings a bird out of the sky and it takes it" { - fk_setup() - set_out(77, WILD_FEED, WILD_SEED, 30.0, 0.0, -1) - let j = wildlife_new(3, 0.0, 0.0, false) - fk_px[0] = 200.0 # within 900 m: an animal nobody is near does not think - fk_pz[0] = 0.0 + test "seed down brings a bird out of the sky and it takes it" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + set_out(wildlife_tests_fake_st, 77, WILD_FEED, WILD_SEED, 30.0, 0.0, -1) + let j = wildlife_new(wildlife_st, 3, 0.0, 0.0, false) + wildlife_tests_fake_st.fk_px[0] = 200.0 # within 900 m: an animal nobody is near does not think + wildlife_tests_fake_st.fk_pz[0] = 0.0 var landed = false var ate = false for k in 0 .. 4000 { - wildlife_tick(0.05, 0.0) + wildlife_tick(base_st, wildlife_st, 0.05, 0.0) if j.state == WILD_PERCH { landed = true } - let fs = facts() + let fs = facts(base_st, wildlife_st) for i in 0 .. len(fs) { if fs[i].what == WILD_FED and fs[i].diet == WILD_SEED { ate = true } } if ate { break } } diff --git a/packages/ludic.wildlife/tests/wildlife_test.ludic b/packages/ludic.wildlife/tests/wildlife_test.ludic index 461c8786..5b7e8b4c 100644 --- a/packages/ludic.wildlife/tests/wildlife_test.ludic +++ b/packages/ludic.wildlife/tests/wildlife_test.ludic @@ -31,180 +31,180 @@ program WildlifeTest { spot: fn fk_spot } - function facts() -> []WildFact { return q_drain(wildlife_facts()) } + function facts(base_st: mut BaseState, wildlife_st: mut WildlifeState) -> []WildFact { return q_drain(base_st, wildlife_facts(base_st, wildlife_st)) } function count(fs: []WildFact, what: int) -> int { var n = 0 for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } - function deer_at(x: float, z: float) -> WildAnimal { - let a = wildlife_make(0, x, z, false, 0.0, 0, 1.0, 1) + function deer_at(wildlife_st: mut WildlifeState, x: float, z: float) -> WildAnimal { + let a = wildlife_make(wildlife_st, 0, x, z, false, 0.0, 0, 1.0, 1) a.state = WILD_IDLE a.timer = 30.0 return a } - function player_at(x: float, z: float) -> void { - fk_px[0] = x - fk_pz[0] = z + function player_at(wildlife_tests_fake_st: mut WildlifeTestsFakeState, x: float, z: float) -> void { + wildlife_tests_fake_st.fk_px[0] = x + wildlife_tests_fake_st.fk_pz[0] = z } - function go(a: WildAnimal, secs: float) -> void { - for k in 0 .. int(secs * 20.0) { wildlife_tick_ground(a, 0.05, 0.05 / 60.0) } + function go(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, secs: float) -> void { + for k in 0 .. int(secs * 20.0) { wildlife_tick_ground(base_st, wildlife_st, a, 0.05, 0.05 / 60.0) } } - test "the table handed in numbers the species, and a new animal is born through the port" { - fk_setup() - expect_eq(wildlife_species_count(), 4) - expect(wildlife_species(3).flies) - let a = wildlife_new(0, 10.0, 20.0, false) - expect_eq(fk_born, 1) + test "the table handed in numbers the species, and a new animal is born through the port" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + expect_eq(wildlife_species_count(wildlife_st), 4) + expect(wildlife_species(wildlife_st, 3).flies) + let a = wildlife_new(wildlife_st, 0, 10.0, 20.0, false) + expect_eq(wildlife_tests_fake_st.fk_born, 1) expect_near(a.y, 10.0, 0.001) expect(a.variant >= 0 and a.variant <= 11) expect(a.scale > 0.87 and a.scale < 1.13) - let j = wildlife_new(3, 0.0, 0.0, false) + let j = wildlife_new(wildlife_st, 3, 0.0, 0.0, false) expect_eq(j.state, WILD_FLY) - expect(wildlife_new(9, 0.0, 0.0, false) == null) + expect(wildlife_new(wildlife_st, 9, 0.0, 0.0, false) == null) } - test "a spawn sets ranges out on their own habitat, clear of the start, and fills them" { - fk_setup() - wildlife_spawn(0.0, 0.0) - let rs = wildlife_ranges() + test "a spawn sets ranges out on their own habitat, clear of the start, and fills them" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + wildlife_spawn(wildlife_st, 0.0, 0.0) + let rs = wildlife_ranges(wildlife_st) expect(len(rs) >= 6) for i in 0 .. len(rs) { - expect(wildlife_range_dist(rs[i], 0.0, 0.0) >= wildlife_species(rs[i].sp).keep_away) + expect(wildlife_range_dist(rs[i], 0.0, 0.0) >= wildlife_species(wildlife_st, rs[i].sp).keep_away) if rs[i].sp == 1 { expect(rs[i].z > 400.0) } expect(rs[i].sx > -300.0) } - expect(wildlife_count(0) >= 1) - expect_eq(wildlife_count_all(), wildlife_count(0) + wildlife_count(1) + wildlife_count(2) + wildlife_count(3)) + expect(wildlife_count(wildlife_st, 0) >= 1) + expect_eq(wildlife_count_all(wildlife_st), wildlife_count(wildlife_st, 0) + wildlife_count(wildlife_st, 1) + wildlife_count(wildlife_st, 2) + wildlife_count(wildlife_st, 3)) } - test "a player far off makes no difference that lasts: the alert settles under the unaware line" { - fk_setup() - let a = deer_at(0.0, 0.0) - player_at(55.0, 0.0) - fk_noise = 0.3 - go(a, 60.0) + test "a player far off makes no difference that lasts: the alert settles under the unaware line" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + player_at(wildlife_tests_fake_st, 55.0, 0.0) + wildlife_tests_fake_st.fk_noise = 0.3 + go(base_st, wildlife_st, a, 60.0) expect(a.alert < 25.0) expect(a.state != WILD_FLEE) } - test "close and in the open it is flushed, runs, and says so" { - fk_setup() - let a = deer_at(0.0, 0.0) - player_at(8.0, 0.0) - fk_noise = 0.8 - go(a, 10.0) + test "close and in the open it is flushed, runs, and says so" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + player_at(wildlife_tests_fake_st, 8.0, 0.0) + wildlife_tests_fake_st.fk_noise = 0.8 + go(base_st, wildlife_st, a, 10.0) expect(a.spook > 0.0) - let fs = facts() + let fs = facts(base_st, wildlife_st) expect(count(fs, WILD_FLED) >= 1) expect(a.x < 0.0) } - test "cover hides a player from sight, a hide from everything but the nose" { - fk_setup() - let a = deer_at(0.0, 0.0) - let open = wildlife_alert_rate(a, 0, 30.0) - player_at(30.0, 0.0) - let open2 = wildlife_alert_rate(a, 0, 30.0) - fk_blocked = true - let covered = wildlife_alert_rate(a, 0, 30.0) - fk_blocked = false - fk_hidden = true - let hidden = wildlife_alert_rate(a, 0, 30.0) + test "cover hides a player from sight, a hide from everything but the nose" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + let open = wildlife_alert_rate(wildlife_st, a, 0, 30.0) + player_at(wildlife_tests_fake_st, 30.0, 0.0) + let open2 = wildlife_alert_rate(wildlife_st, a, 0, 30.0) + wildlife_tests_fake_st.fk_blocked = true + let covered = wildlife_alert_rate(wildlife_st, a, 0, 30.0) + wildlife_tests_fake_st.fk_blocked = false + wildlife_tests_fake_st.fk_hidden = true + let hidden = wildlife_alert_rate(wildlife_st, a, 0, 30.0) expect(open2 > 0.0) expect(open >= open2) expect_near(covered, 0.0, 0.0001) expect_near(hidden, 0.0, 0.0001) } - test "sitting still makes it forget faster, and the wind carries scent only downwind" { - fk_setup() - let a = deer_at(0.0, 0.0) - let moving = wildlife_alert_decay(a, 0) - fk_still = WILD_SITTING - expect(wildlife_alert_decay(a, 0) > moving * 2.0) - player_at(0.0, -60.0) - fk_blocked = true - fk_scent = 1.0 - fk_wind = 0.5 - fk_wind_dir = 3.1415927 # blowing toward +z: from the player to the deer + test "sitting still makes it forget faster, and the wind carries scent only downwind" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + let moving = wildlife_alert_decay(wildlife_st, a, 0) + wildlife_tests_fake_st.fk_still = WILD_SITTING + expect(wildlife_alert_decay(wildlife_st, a, 0) > moving * 2.0) + player_at(wildlife_tests_fake_st, 0.0, -60.0) + wildlife_tests_fake_st.fk_blocked = true + wildlife_tests_fake_st.fk_scent = 1.0 + wildlife_tests_fake_st.fk_wind = 0.5 + wildlife_tests_fake_st.fk_wind_dir = 3.1415927 # blowing toward +z: from the player to the deer expect(wildlife_downwind(a, 0) > 0.9) - expect(wildlife_alert_rate(a, 0, 60.0) > 0.0) - fk_wind_dir = 0.0 - expect_near(wildlife_alert_rate(a, 0, 60.0), 0.0, 0.0001) + expect(wildlife_alert_rate(wildlife_st, a, 0, 60.0) > 0.0) + wildlife_tests_fake_st.fk_wind_dir = 0.0 + expect_near(wildlife_alert_rate(wildlife_st, a, 0, 60.0), 0.0, 0.0001) } - test "a charger comes at a player inside its reach, and a shove is a fact for the game" { - fk_setup() - let b = wildlife_make(1, 0.0, 0.0, false, 0.0, 0, 1.0, 2) - player_at(6.0, 0.0) - wildlife_tick_ground(b, 0.05, 0.001) + test "a charger comes at a player inside its reach, and a shove is a fact for the game" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let b = wildlife_make(wildlife_st, 1, 0.0, 0.0, false, 0.0, 0, 1.0, 2) + player_at(wildlife_tests_fake_st, 6.0, 0.0) + wildlife_tick_ground(base_st, wildlife_st, b, 0.05, 0.001) expect_eq(b.state, WILD_CHARGE) - go(b, 2.0) - let fs = facts() + go(base_st, wildlife_st, b, 2.0) + let fs = facts(base_st, wildlife_st) expect_eq(count(fs, WILD_CHARGED), 1) expect_eq(count(fs, WILD_SHOVED), 1) expect(b.calm > 0.0) } - test "away is hidden and skipped, gone is removed and said" { - fk_setup() - let a = deer_at(0.0, 0.0) - player_at(10.0, 0.0) - fk_about = WILD_AWAY - wildlife_tick(0.05, 0.001) + test "away is hidden and skipped, gone is removed and said" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + player_at(wildlife_tests_fake_st, 10.0, 0.0) + wildlife_tests_fake_st.fk_about = WILD_AWAY + wildlife_tick(base_st, wildlife_st, 0.05, 0.001) expect(not a.shown and a.alive) - fk_about = WILD_GONE - wildlife_tick(0.05, 0.001) + wildlife_tests_fake_st.fk_about = WILD_GONE + wildlife_tick(base_st, wildlife_st, 0.05, 0.001) expect(not a.alive) - expect_eq(count(facts(), WILD_WENT), 1) + expect_eq(count(facts(base_st, wildlife_st), WILD_WENT), 1) } - test "a walker leaves a print every 2.8 m, and player 0 spots a species once" { - fk_setup() - let a = deer_at(0.0, 0.0) - player_at(40.0, 0.0) - fk_blocked = true + test "a walker leaves a print every 2.8 m, and player 0 spots a species once" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + player_at(wildlife_tests_fake_st, 40.0, 0.0) + wildlife_tests_fake_st.fk_blocked = true a.state = WILD_WANDER a.tx = 0.0 a.tz = -100.0 a.timer = 100.0 - for k in 0 .. 200 { wildlife_tick(0.05, 0.0) } - let fs = facts() + for k in 0 .. 200 { wildlife_tick(base_st, wildlife_st, 0.05, 0.0) } + let fs = facts(base_st, wildlife_st) expect(count(fs, WILD_PRINT) >= 3) expect_eq(count(fs, WILD_SPOTTED), 1) - expect(wildlife_seen(0)) + expect(wildlife_seen(wildlife_st, 0)) } - test "the record and the found ranges come back from the save section" { - fk_setup() - wildlife_spawn(0.0, 0.0) - wildlife_mark_fed(2) - wildlife_saw(0) - let r = wildlife_ranges()[0] - wildlife_ranges_visit(r.x, r.z) - let v = wildlife_save() - wildlife_reset() - expect(not wildlife_fed_any() and not wildlife_ranges()[0].found) - wildlife_load(v, 1) - expect(wildlife_fed_any()) - expect_eq(wildlife_fed_mask(), 4) - expect(wildlife_seen(0)) - expect(wildlife_ranges()[0].found) + test "the record and the found ranges come back from the save section" (base_st: mut BaseState, wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + wildlife_spawn(wildlife_st, 0.0, 0.0) + wildlife_mark_fed(wildlife_st, 2) + wildlife_saw(wildlife_st, 0) + let r = wildlife_ranges(wildlife_st)[0] + wildlife_ranges_visit(wildlife_st, r.x, r.z) + let v = wildlife_save(wildlife_st) + wildlife_reset(base_st, wildlife_st) + expect(not wildlife_fed_any(wildlife_st) and not wildlife_ranges(wildlife_st)[0].found) + wildlife_load(base_st, wildlife_st, v, 1) + expect(wildlife_fed_any(wildlife_st)) + expect_eq(wildlife_fed_mask(wildlife_st), 4) + expect(wildlife_seen(wildlife_st, 0)) + expect(wildlife_ranges(wildlife_st)[0].found) } - test "a morning moves the ranges toward winter and brings one back to a range that lost its own" { - fk_setup() - wildlife_spawn(0.0, 0.0) - let r = wildlife_ranges()[0] + test "a morning moves the ranges toward winter and brings one back to a range that lost its own" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + wildlife_spawn(wildlife_st, 0.0, 0.0) + let r = wildlife_ranges(wildlife_st)[0] let x0 = r.x - let all = wildlife_all() + let all = wildlife_all(wildlife_st) for i in 0 .. len(all) { if all[i].rg == 0 { wildlife_remove(all[i]) } } - fk_winter = 1.0 - let added = wildlife_morning() + wildlife_tests_fake_st.fk_winter = 1.0 + let added = wildlife_morning(wildlife_st) expect(added >= 1) - expect_eq(wildlife_range_alive(0), 1) + expect_eq(wildlife_range_alive(wildlife_st, 0), 1) expect_near(r.x, r.wx, 0.001) expect(r.x != x0 or r.wx == r.sx) } diff --git a/packages/ludic.wildlife/want.ludic b/packages/ludic.wildlife/want.ludic index 94ecc5b3..aadc807a 100644 --- a/packages/ludic.wildlife/want.ludic +++ b/packages/ludic.wildlife/want.ludic @@ -1,9 +1,8 @@ # want.ludic - thirst and hunger over sixty send it to the water or the forage the game offers for # it (a hungry one on its way turns to food put down nearer); it drinks or grazes there -var wl_spot_ask: WildSpot = null -function wl_seek(a: WildAnimal, dt: float) -> bool { - if a.state == WILD_GO_FEED and wl_attraction(a) and wl_best.kind == WILD_FEED { wl_take_lure(a) } +function wl_seek(base_st: mut BaseState, wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { + if a.state == WILD_GO_FEED and wl_attraction(wildlife_st, a) and wildlife_st.wl_best.kind == WILD_FEED { wl_take_lure(wildlife_st, a) } if a.state != WILD_GO_DRINK and a.state != WILD_GO_FEED { return false } if a.spot == 0 or not WildlifeWorld.present(a.spot) { a.spot = 0 @@ -12,13 +11,13 @@ function wl_seek(a: WildAnimal, dt: float) -> bool { return true } wl_face(a, a.spot_x, a.spot_z, 2.0, dt) - a.speed = wl_sp(a.sp).walk + a.speed = wl_sp(wildlife_st, a.sp).walk a.tx = a.spot_x a.tz = a.spot_z - wl_step(a, dt) + wl_step(base_st, wildlife_st, a, dt) if wl_at_target(a, 2.0) { a.state = WILD_DRINK - a.timer = 6.0 + wl_rnd() * 10.0 + a.timer = 6.0 + wl_rnd(wildlife_st) * 10.0 } a.timer = a.timer - dt if a.timer < -600.0 { @@ -29,26 +28,26 @@ function wl_seek(a: WildAnimal, dt: float) -> bool { } # true while it drinks or grazes, or when it has just set off -function wl_drink(a: WildAnimal, dt: float, gh: float) -> bool { +function wl_drink(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> bool { if a.state == WILD_DRINK { a.speed = 0.0 a.timer = a.timer - dt if a.spot != 0 and a.spot_water { a.thirst = Math.max(a.thirst - 600.0 * gh, 0.0) } else { a.hunger = Math.max(a.hunger - 600.0 * gh, 0.0) } if a.timer < 0.0 or Math.min(a.thirst, a.hunger) < 1.0 { - wl_rehome(a) - wl_wander(a, 6.0) + wl_rehome(wildlife_st, a) + wl_wander(wildlife_st, a, 6.0) } return true } if a.state != WILD_IDLE and a.state != WILD_WANDER { return false } if not (a.thirst > 60.0 or a.hunger > 60.0) { return false } - if wl_spot_ask == null { wl_spot_ask = new WildSpot } + if wildlife_st.wl_spot_ask == null { wildlife_st.wl_spot_ask = new WildSpot } let water = a.thirst > a.hunger - if not WildlifeWorld.spot(a, water, wl_spot_ask) { return false } - a.spot = wl_spot_ask.uid - a.spot_x = wl_spot_ask.x - a.spot_z = wl_spot_ask.z - a.spot_water = wl_spot_ask.water + if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false } + a.spot = wildlife_st.wl_spot_ask.uid + a.spot_x = wildlife_st.wl_spot_ask.x + a.spot_z = wildlife_st.wl_spot_ask.z + a.spot_water = wildlife_st.wl_spot_ask.water a.state = WILD_GO_DRINK if not water { a.state = WILD_GO_FEED } a.timer = 120.0 diff --git a/selfhost/check/check_call.ludic b/selfhost/check/check_call.ludic index bbfced84..8be88fe1 100644 --- a/selfhost/check/check_call.ludic +++ b/selfhost/check/check_call.ludic @@ -242,7 +242,10 @@ function ck_call_named(e: Node, name: pointer) -> pointer { return xt } let g = ck_global(name) - if g != null and is_fn_type(g.ty) { return ck_call_sig(e, name, g.ty) } + if g != null and is_fn_type(g.ty) { + mg_ref(e.a, g) # 0.S2: a call through a var of a function type reads it + return ck_call_sig(e, name, g.ty) + } ck_walk_args(e) return "?" } diff --git a/selfhost/frontend/migrate.ludic b/selfhost/frontend/migrate.ludic index 9325c2af..73c6516a 100644 --- a/selfhost/frontend/migrate.ludic +++ b/selfhost/frontend/migrate.ludic @@ -51,18 +51,73 @@ function mg_camel(s: pointer) -> pointer { } return out } +function mg_param_for(name: pointer) -> pointer { + var base = name + let n = len(name) + if n > 5 and (name[n - 5 .. n] == "State") { base = name[0 .. n - 5] } + var out = "" + var i = 0 + while i < len(base) { + let c = base[i] + if c >= 'A' and c <= 'Z' { + if i > 0 { out = out + "_" } + out = out + reg_lower(base[i .. i + 1]) + } else { out = out + base[i .. i + 1] } + i += 1 + } + return out + "_st" +} function mg_short(m: pointer) -> pointer { if len(m) > 6 and (m[0 .. 6] == "ludic_") { return m[6 .. len(m)] } return m } -function mg_state_of_mod(m: pointer) -> int { +var g_mg_entry: pointer = "" # the program's own file: its vars are the program's state +# the state a var's file moves into: its module's, or - a file in no module - the program's for the +# program's own file, else one named for the file's directory (a test's shared fakes) +function mg_state_of_file(f: pointer) -> int { + let m = module_for_uses(f) + if not (m == "") { return mg_state_of_mod(m, mg_short(m)) } + if (f == g_mg_entry) { return mg_state_of_mod("", reg_lower(g_game_name)) } + var d = dir_of(f) + while len(d) > 1 and d[len(d) - 1] == '/' { d = d[0 .. len(d) - 1] } + return mg_state_of_mod(`@{d}`, mg_dir_name(d)) +} +# a name for a directory's state that two directories called `fake` do not share: the path under +# packages/ (ludic.wildlife/tests/fake: wildlife_tests_fake), else its last two parts +function mg_dir_name(d: pointer) -> pointer { + var at = -1 + var i = 0 + while i + 9 <= len(d) { + if (d[i .. i + 9] == "packages/") { at = i + 9 } + i += 1 + } + var rest: pointer = "" + if at >= 0 { rest = d[at .. len(d)] } else { + var k = len(d) + var slashes = 0 + while k > 0 and slashes < 2 { + k -= 1 + if d[k] == '/' { slashes += 1 } + } + if d[k] == '/' { k += 1 } + rest = d[k .. len(d)] + } + if len(rest) > 6 and (rest[0 .. 6] == "ludic.") { rest = rest[6 .. len(rest)] } + var out = "" + i = 0 + while i < len(rest) { + let c = rest[i] + if c == '/' or c == '.' or c == '-' { out = out + "_" } else { out = out + rest[i .. i + 1] } + i += 1 + } + return out +} +function mg_state_of_mod(m: pointer, base: pointer) -> int { var i = 0 while i < len(g_ms_mod) { if (g_ms_mod[i] == m) { return i } i += 1 } - var base = mg_short(m) - if (m == "") { base = reg_lower(g_game_name) } push(g_ms_mod, m) push(g_ms_name, `{mg_camel(base)}State`) push(g_ms_param, `{base}_st`) @@ -81,7 +136,7 @@ function mg_collect() -> void { while i < g_prog_user_end and i < len(prog) { let d = prog[i] if d.kind == N_VAR and mg_is_target(d) { - let k = mg_state_of_mod(module_for_uses(d.file)) + let k = mg_state_of_file(d.file) push(g_mg_vars, d) push(g_mg_vstate, k) d.mg = len(g_mg_vars) - 1 @@ -116,10 +171,11 @@ function mg_state_named(name: pointer) -> int { if (g_ms_name[i] == name) { return i } i += 1 } - # a state declared by hand: known by its name, its param the name in snake case + # a state from a run before (or by hand): known by its name, its param `_st` as the tool + # names them (BaseState: base_st) push(g_ms_mod, `={name}`) push(g_ms_name, name) - push(g_ms_param, `{reg_lower(name)}`) + push(g_ms_param, mg_param_for(name)) push(g_ms_export, 0) return len(g_ms_mod) - 1 } @@ -154,11 +210,18 @@ function mg_write(t: Node, g: Node) -> void { if not g_migrate or g == null or g.kind != N_VAR or g.mg < 0 { return } if g_mg_cur >= 0 { mg_need(g_mg_cur, g_mg_vstate[g.mg], true) } } +var g_mc_passed: []int = new []int # 1: the call already hands the callee a state (a run before) function mg_call(e: Node, f: Node) -> void { if not g_migrate or f == null or g_mg_cur < 0 { return } push(g_mc_call, e) push(g_mc_unit, g_mg_cur) push(g_mc_callee, mg_unit(f, 0)) + var passed = 0 + if len(e.kids) > 0 and e.kids[0].kind == E_ID { + let li = ck_local(e.kids[0].s) + if li >= 0 and is_state_ty(ck_tys[li]) { passed = 1 } + } + push(g_mc_passed, passed) } function mg_say(n: Node, what: pointer) -> void { var at = "?" @@ -232,13 +295,8 @@ function mg_param_text(u: int, s: int) -> pointer { } var g_mg_lines: []pointer = new []pointer var g_mg_keys: pointer = "," -function mg_emit(line: pointer, key: pointer) -> void { - if not (key == "") { - if has_sub(g_mg_keys, `,{key},`) { return } - g_mg_keys = g_mg_keys + key + "," - } - push(g_mg_lines, line) -} +# the same edit twice (a generic's instances share its source) is dropped by ludic migrate +function mg_emit(line: pointer, key: pointer) -> void { push(g_mg_lines, line) } # text for a plan line: spaces and newlines escaped function mg_esc(s: pointer) -> pointer { var out = "" @@ -328,7 +386,7 @@ function mg_calls() -> void { let need = mg_list(g_mu_need[to]) if len(need) > 0 { var text = "" - let passed = mg_first_positional_is_state(e) + let passed = g_mc_passed[c] == 1 var k = 0 while k < len(need) { if not (mg_declared(callee, need[k]) and passed) { @@ -360,8 +418,27 @@ function mg_refs() -> void { r += 1 } } +# a state needed outside its module is exported: whoever needs it declares it +function mg_exports() -> void { + var u = 0 + while u < len(g_mu_node) { + let n = g_mu_node[u] + if n.file != null { + var here = module_for_uses(n.file) + if (here == "") and not (n.file == g_mg_entry) { here = `@{dir_of(n.file)}` } + let need = mg_list(g_mu_need[u]) + var k = 0 + while k < len(need) { + if not (g_ms_mod[need[k]] == here) { g_ms_export[need[k]] = 1 } + k += 1 + } + } + u += 1 + } +} function mg_finish() -> void { mg_fixpoint() + mg_exports() var i = 0 while i < len(g_mg_vars) { let v = g_mg_vars[i] diff --git a/selfhost/frontend/parse.ludic b/selfhost/frontend/parse.ludic index cc162f9a..5f3958af 100644 --- a/selfhost/frontend/parse.ludic +++ b/selfhost/frontend/parse.ludic @@ -1027,7 +1027,7 @@ function parse_one_decl() -> void { if is_id("component") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_ui_component(); return } # L11 if is_id("state") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_state(); return } # 0.S if is_id("var") { - if not g_allow_globals { + if not g_allow_globals and not has_sub(g_parse_file, "runtime/native/") { # the runtime: until it is migrated (0.S3) perr(`a module-level var is refused: a module's changing data is its state (state Name {{ ... }}), passed to the functions that use it - or, if it never changes, a let`) } push(prog, parse_var()) diff --git a/selfhost/main.ludic b/selfhost/main.ludic index 1f08247d..073e13c9 100644 --- a/selfhost/main.ludic +++ b/selfhost/main.ludic @@ -215,6 +215,7 @@ entry { privates_rename() # L3: a module's private names are its own (privates.ludic) check_duplicate_fns() # two declarations of one name: the cause, before its symptoms check_duplicate_decls() + g_mg_entry = path if g_migrate { mg_collect() } # 0.S2: the vars that move (migrate.ludic) check_program() # L4: the types agree before anything is emitted if g_migrate { mg_finish() } diff --git a/tools/ludic-cli/migrate.ludic b/tools/ludic-cli/migrate.ludic index c1ca250d..b2a46b55 100644 --- a/tools/ludic-cli/migrate.ludic +++ b/tools/ludic-cli/migrate.ludic @@ -59,6 +59,21 @@ function mgx_indent(t: pointer, start: int) -> pointer { while t[i] == 32 or t[i] == 9 { i += 1 } return sslice(t, start, i) } +# the text from byte `at` on, without copying it +function data_at(t: pointer, at: int) -> pointer { return offset(t, at) } +# a var's declaration from its name to its end (`count: int = 0`), and the comment after it on its +# line when nothing else is there (two vars on one line keep their own text each) +function mgx_field(t: pointer, a: int, b: int) -> pointer { + var out = sslice(t, a, b) + var i = b + while t[i] == 32 or t[i] == 9 { i += 1 } + if t[i] == 35 { + var e = i + while t[e] != 0 and t[e] != 10 { e += 1 } + out = out + sslice(t, b, e) + } + return mgx_rtrim(out) +} function mgx_rtrim(s: pointer) -> pointer { var n = slen(s) while n > 0 and (s[n - 1] == 10 or s[n - 1] == 13 or s[n - 1] == 32) { n -= 1 } @@ -66,10 +81,10 @@ function mgx_rtrim(s: pointer) -> pointer { } function cmd_migrate() -> int { if arg_count() < 3 or not (arg(2) == "state") { - err("usage: ludic migrate state [file] [--runtime] [--unsafe] [--dry-run]\n") + err("usage: ludic migrate state [file|dir...] [--runtime] [--unsafe] [--dry-run]\n") return 2 } - var src = "" + let srcs = new []pointer var runtime = "" var dry = false var ai = 3 @@ -79,22 +94,67 @@ function cmd_migrate() -> int { else if a == "--unsafe" { g_unsafe_build = true } else if a == "--dry-run" { dry = true } else if a[0] == '-' { err(`ludic migrate: unknown option {a}\n`); return 2 } - else { src = a } + else if shq(`test -d {sh_single(a)}`) { + let found = test_files_in(a) + for k in 0 .. len(found) { push(srcs, found[k]) } + } + else { push(srcs, a) } ai += 1 } - let entry = find_entry(src) - if entry == "" { return no_entry() } - ensure_ludicc() - let plan = tmp_path("migrate.plan") - shell(`rm -f {plan}`) - shq(`{ludicc()} --headless{unsafe_flag()}{runtime} --migrate-state {plan} {entry} > {tmp_path("migrate.err")} 2>&1`) - let text = read_file(plan) - if text == null { - err(`ludic migrate: the compiler wrote no plan for {entry}:\n`) - err(capture(`grep -i error {tmp_path("migrate.err")} | head -5`)) - return 1 + if len(srcs) == 0 { + let entry = find_entry("") + if entry == "" { return no_entry() } + push(srcs, entry) } - return mgx_apply(split_lines(text), dry) + ensure_ludicc() + # every program's plan from the source as it is, then one pass over the files: programs that + # share a file (a package, a test's fakes) agree about it, and none sees another's half-done work + let all = new []pointer + for i in 0 .. len(srcs) { + let plan = tmp_path(`migrate{string(i)}.plan`) + shell(`rm -f {plan}`) + shq(`{ludicc()} --headless{unsafe_flag()}{runtime} --migrate-state {plan} {sh_single(srcs[i])} > {tmp_path("migrate.err")} 2>&1`) + let text = read_file(plan) + if text == null { + err(`ludic migrate: the compiler wrote no plan for {srcs[i]}:\n`) + err(capture(`grep -i error {tmp_path("migrate.err")} | head -5`)) + return 1 + } + let ls = split_lines(text) + for k in 0 .. len(ls) { push(all, ls[k]) } + } + return mgx_apply(mgx_unique(all), dry) +} +# the lines once each, in order of text (a merge sort: a render3d plan is thousands of lines) +function mgx_unique(xs: []pointer) -> []pointer { + let tmp = new []pointer + for i in 0 .. len(xs) { push(tmp, "") } + mgx_sort(xs, 0, len(xs), tmp) + let out = new []pointer + for i in 0 .. len(xs) { + if i == 0 or not (xs[i] == xs[i - 1]) { push(out, xs[i]) } + } + return out +} +function mgx_sort(xs: []pointer, lo: int, hi: int, tmp: []pointer) -> void { + if hi - lo < 2 { return } + let mid = (lo + hi) / 2 + mgx_sort(xs, lo, mid, tmp) + mgx_sort(xs, mid, hi, tmp) + var i = lo + var j = mid + var k = lo + while k < hi { + if j >= hi or (i < mid and not s_less(xs[j], xs[i])) { + tmp[k] = xs[i] + i += 1 + } else { + tmp[k] = xs[j] + j += 1 + } + k += 1 + } + for m in lo .. hi { xs[m] = tmp[m] } } function mgx_apply(lines: []pointer, dry: bool) -> int { mgx_file = new []pointer; mgx_pos = new []int; mgx_len = new []int; mgx_text = new []pointer @@ -117,6 +177,13 @@ function mgx_apply(lines: []pointer, dry: bool) -> int { push(vpos2, s_to_int(w[3])) push(vstate, w[4]) } else if w[0] == "STATE" and len(w) >= 6 { + # one state from several programs' plans: exported if any of them needs it outside + var known = -1 + for k in 0 .. len(sname) { if sname[k] == w[1] { known = k } } + if known >= 0 { + if s_to_int(w[4]) == 1 { sexp[known] = 1 } + continue + } push(sname, w[1]) push(sfile, w[2]) push(spos, s_to_int(w[3])) @@ -139,18 +206,33 @@ function mgx_apply(lines: []pointer, dry: bool) -> int { push(del_a, a) push(del_b, b) } + # the fields in the order they were declared: by file, then by place + let order = new []int + for v in 0 .. len(vfile) { push(order, v) } + var oa = 1 + while oa < len(order) { + let x = order[oa] + var ob = oa - 1 + while ob >= 0 and (s_less(vfile[x], vfile[order[ob]]) or (vfile[x] == vfile[order[ob]] and vpos[x] < vpos[order[ob]])) { + order[ob + 1] = order[ob] + ob -= 1 + } + order[ob + 1] = x + oa += 1 + } for s in 0 .. len(sname) { var block = "" var at = -1 var indent = "" - for v in 0 .. len(vfile) { + for ov in 0 .. len(order) { + let v = order[ov] if vstate[v] == sname[s] { let t = mgx_text_of(vfile[v]) if vfile[v] == sfile[s] and vpos[v] == spos[s] { at = v indent = mgx_indent(t, del_a[v]) } - block = block + `{indent} {mgx_rtrim(sslice(t, vpos[v], del_b[v]))}\n` + block = block + `{indent} {mgx_field(t, vpos[v], vpos2[v])}\n` } } var head = "" @@ -158,6 +240,7 @@ function mgx_apply(lines: []pointer, dry: bool) -> int { if at >= 0 { mgx_edit(vfile[at], del_a[at], 0, `{indent}{head}state {sname[s]} {{\n{block}{indent}}}\n`) } } for d in 0 .. len(del_file) { mgx_edit(del_file[d], del_a[d], del_b[d] - del_a[d], "") } + for e in 0 .. len(mgx_file) { mgx_text_of(mgx_file[e]) } # every file with an edit, read # an edit inside a line that goes is dropped with it var files = 0 var edits = 0 @@ -174,27 +257,46 @@ function mgx_apply(lines: []pointer, dry: bool) -> int { if not inside { push(idx, e) } } } - # last first, so every earlier place stays where it was; at one place, the deletion last + # in the order of their places; at one place, what is put there before what is taken out var a = 1 while a < len(idx) { let x = idx[a] var b = a - 1 - while b >= 0 and (mgx_pos[idx[b]] < mgx_pos[x] or (mgx_pos[idx[b]] == mgx_pos[x] and mgx_len[idx[b]] < mgx_len[x])) { + while b >= 0 and (mgx_pos[idx[b]] > mgx_pos[x] or (mgx_pos[idx[b]] == mgx_pos[x] and mgx_len[idx[b]] > mgx_len[x])) { idx[b + 1] = idx[b] b -= 1 } idx[b + 1] = x a += 1 } + if len(idx) == 0 { continue } + files += 1 + if dry { continue } + let out = file_open(path, "wb") + if out == null { + err(`ludic migrate: cannot write {path}\n`) + continue + } + var cur = 0 for k in 0 .. len(idx) { let e = idx[k] - t = sslice(t, 0, mgx_pos[e]) + mgx_text[e] + sslice(t, mgx_pos[e] + mgx_len[e], slen(t)) - edits += 1 - } - if len(idx) > 0 { - files += 1 - if not dry { write_file(path, t) } + # an edit made twice (a generic's instances share its source) is made once + var dup = false + if k > 0 { + let p = idx[k - 1] + dup = mgx_pos[p] == mgx_pos[e] and mgx_len[p] == mgx_len[e] and mgx_text[p] == mgx_text[e] + } + if not dup and mgx_pos[e] >= cur { + if mgx_pos[e] > cur { file_write(out, data_at(t, cur), mgx_pos[e] - cur) } + let txt = mgx_text[e] + if slen(txt) > 0 { file_write(out, txt, slen(txt)) } + cur = mgx_pos[e] + mgx_len[e] + edits += 1 + } } + let n = slen(t) + if n > cur { file_write(out, data_at(t, cur), n - cur) } + file_close(out) } print(`migrate: {string(len(vfile))} vars into {string(len(sname))} states; {string(edits)} edits in {string(files)} files`) for r in 0 .. len(reports) { print(` by hand: {sslice(reports[r], 2, slen(reports[r]))}`) }