Merge branch 'lang/foundations' into lang/uifree

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 23:46:28 +03:00
commit 5d9cbddb72
43 changed files with 495 additions and 118 deletions

10
changes/model-release.md Normal file
View file

@ -0,0 +1,10 @@
bump: minor
type: feature
**A loaded model can be let go of: `model_release(model)`.** Its meshes are freed at once, and
each texture once no other model uses it - the glTF loader shares textures between models (one
path loaded once, a LOD chain borrowing its LOD0's material), so every cached texture now counts
the primitives using it, and the last one frees it and forgets it, with any remembered material
that named it. A game can unload a species' kit or a map's models when nothing of them is left,
and loading one again afterwards is a fresh load. A scatter layer takes room for what it holds
instead of its whole capacity up front (it doubles as it fills, to its cap), which in Maroon Lake
was 0.4 GB of never-filled instance arrays. `examples/rendering/release.ludic` is the check.

View file

@ -0,0 +1,69 @@
# release.ludic - a loaded model let go of (model_release): its meshes go at once, a texture it
# shares with another model stays until that one goes too, and a model loaded again afterwards is
# whole. Prints RELEASE OK.
#
# bin/ludic build examples/rendering/release.ludic --headless && ./build/release_headless
program Release {
numbers float
import "ludic.render3d/r3d.ludic"
property Marker { on: int = 1 }
model Anchor { Marker }
function scene_draw(render3d_st: mut Render3dState) -> void { }
function scene_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void { }
function stream_fill(s: Stream, cx: int, cz: int, band: int) -> void { }
function check(what: string, cond: bool) -> bool {
if not cond { print(`release: FAILED - {what}`) }
return cond
}
# whether handle `tex` is still a texture the renderer knows (its recorded kind is set)
function tex_live(render3d_st: mut Render3dState, tex: int) -> bool {
let o = gpu_tx_at(render3d_st, tex)
return tex != 0 and o >= 0 and render3d_st.gpu_tx[o] != 0
}
handler Boot(render3d_st: mut Render3dState) phase Start {
spawn Anchor {}
r3d_on_draw(render3d_st, fn scene_draw)
r3d_on_casters(render3d_st, fn scene_draw_casters)
r3d_on_stream_fill(render3d_st, fn stream_fill)
r3d_plate_mode(render3d_st, true)
let dir = "packages/ludic.lab/plate"
render3d_st.r3d_sky_path = `{dir}/sky.hdr`
if not r3d_init(render3d_st, 320, 180, "Release") {
quit()
return
}
var ok = true
# two models of the same file share its two textures
let a = gltf_load(render3d_st, dir, "plate.gltf", "plate")
let b = gltf_load(render3d_st, dir, "plate.gltf", "plate")
ok = check("both plates load", a != null and b != null and len(a.prims) > 0) and ok
let diff = a.prims[0].diff
ok = check("the plates share their textures", diff != 0 and b.prims[0].diff == diff and render3d_st.gltf_tex_n == 2) and ok
model_release(render3d_st, a)
ok = check("the first plate is emptied", len(a.prims) == 0) and ok
ok = check("what the second still uses stays", tex_live(render3d_st, diff) and render3d_st.gltf_tex_n == 2) and ok
model_release(render3d_st, b)
ok = check("the last user frees the textures", not tex_live(render3d_st, diff) and render3d_st.gltf_tex_n == 0) and ok
ok = check("and its material is forgotten", render3d_st.gltf_mat_n == 0) and ok
# one model whose two materials share a texture
let p = gltf_load(render3d_st, dir, "probe.gltf", "probe")
ok = check("the probe loads its three textures", p != null and render3d_st.gltf_tex_n == 3) and ok
model_release(render3d_st, p)
ok = check("the probe gives all three back", render3d_st.gltf_tex_n == 0 and render3d_st.gltf_mat_n == 0) and ok
# and loading again after a release is a fresh load, whole
let c = gltf_load(render3d_st, dir, "plate.gltf", "plate")
ok = check("a plate loaded again is whole", c != null and len(c.prims) > 0 and tex_live(render3d_st, c.prims[0].diff) and render3d_st.gltf_tex_n == 2) and ok
model_release(render3d_st, c)
ok = check("and goes again", render3d_st.gltf_tex_n == 0) and ok
if ok { print("RELEASE OK") } else { print("RELEASE FAILED") }
quit()
}
handler Present(render3d_st: mut Render3dState) phase Render {
r3d_present(render3d_st)
}
}

View file

@ -7,6 +7,11 @@ import "ludic.base"
import "ludic.physics" import "ludic.physics"
program CharacterTest { program CharacterTest {
numbers float numbers float
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
let p = new PhysWalk
if not phys_walker_pose(physics_st, id, p) { return null }
return p
}
# the world: x in 4..8 is a deck 45 cm up; z < -10 a cliff; z > 10 a gentle rise; x > 20 the lake # the world: x in 4..8 is a deck 45 cm up; z < -10 a cliff; z > 10 a gentle rise; x > 20 the lake
function fk_height(x: float, z: float) -> float { function fk_height(x: float, z: float) -> float {
@ -87,7 +92,7 @@ program CharacterTest {
function fk_walk(physics_st: mut PhysicsState, character_test_st: mut CharacterTestState, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, slope: float, dt: float) -> int { function fk_walk(physics_st: mut PhysicsState, character_test_st: mut CharacterTestState, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, slope: float, dt: float) -> int {
if character_test_st.walker < 0 { character_test_st.walker = fk_jolt(physics_st) } if character_test_st.walker < 0 { character_test_st.walker = fk_jolt(physics_st) }
let g = phys_walker_move(physics_st, character_test_st.walker, x, y, z, vx, vz, jump, step, slope, dt) let g = phys_walker_move(physics_st, character_test_st.walker, x, y, z, vx, vz, jump, step, slope, dt)
let p = phys_walker_pose(physics_st, character_test_st.walker) let p = wpose(physics_st, character_test_st.walker)
character_test_st.wx = p.x character_test_st.wx = p.x
character_test_st.wy = p.y character_test_st.wy = p.y
character_test_st.wz = p.z character_test_st.wz = p.z

View file

@ -51,7 +51,7 @@ import "ludic.nav"
| `nav_next_corner(kind, filter, sx, sy, sz, ex, ey, ez) -> bool` | the next corner of that way, read back as the nearest point is (a walker's one question a frame) | | `nav_next_corner(kind, filter, sx, sy, sz, ex, ey, ez) -> bool` | the next corner of that way, read back as the nearest point is (a walker's one question a frame) |
| `nav_straight(kind, filter, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable | | `nav_straight(kind, filter, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
| `nav_random_near(kind, filter, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed | | `nav_random_near(kind, filter, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
| `nav_asked()`, `nav_found()`, `nav_short()` | paths asked for, found and cut short since the meshes loaded (a game's measure of how often its walkers had a way) | | `nav_asked()`, `nav_found()`, `nav_short()`, `nav_us()` | paths asked for, found and cut short since the meshes loaded, and the microseconds they took (timed in the shim by the OS clock: a game's measure of how often its walkers had a way, and what it cost) |
| `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter | | `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter |
## Tests ## Tests

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -11,6 +11,7 @@ extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: point
extern function nvc_area_cost(h: pointer, f: int, area: int, cost: float) -> void = "nav_area_cost" extern function nvc_area_cost(h: pointer, f: int, area: int, cost: float) -> void = "nav_area_cost"
extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path" extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
extern function nvc_partial(h: pointer) -> int = "nav_partial" extern function nvc_partial(h: pointer) -> int = "nav_partial"
extern function nvc_us(h: pointer) -> int = "nav_us"
extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast" extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near" extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
extern function nvc_stage(size: int) -> pointer = "nav_stage" extern function nvc_stage(size: int) -> pointer = "nav_stage"

View file

@ -22,8 +22,7 @@ NAV_SHIM void nav_area_cost(void *h, int f, int area, float cost) {
// the corners of a path from one point to another, at most max of them, into out: the count, or // the corners of a path from one point to another, at most max of them, into out: the count, or
// -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh // -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh
// allows, and nav_partial says so // allows, and nav_partial says so
NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) { static int nav_path_in(Nav *n, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
Nav *n = static_cast<Nav *>(h);
float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3]; float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3];
dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo); dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo);
n->partial = 0; n->partial = 0;
@ -39,6 +38,15 @@ NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, fl
if (dtStatusFailed(n->query->findStraightPath(so, end, n->polys, np, out, nullptr, nullptr, &count, max, 0))) return -1; if (dtStatusFailed(n->query->findStraightPath(so, end, n->polys, np, out, nullptr, nullptr, &count, max, 0))) return -1;
return count; return count;
} }
NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
Nav *n = static_cast<Nav *>(h);
long long t0 = nav_now_ns();
int r = nav_path_in(n, f, sx, sy, sz, ex, ey, ez, out, max);
n->ns += nav_now_ns() - t0;
return r;
}
// microseconds this mesh has spent finding paths
NAV_SHIM int nav_us(void *h) { return static_cast<int>(static_cast<Nav *>(h)->ns / 1000); }
NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; } NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; }
// does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it // does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it

View file

@ -17,7 +17,27 @@
#define NAV_SHIM extern "C" __attribute__((visibility("default"))) #define NAV_SHIM extern "C" __attribute__((visibility("default")))
#endif #endif
#ifdef _WIN32
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(long long *);
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(long long *);
#else
#include <time.h>
#endif
namespace { namespace {
// a monotonic clock in nanoseconds, from the OS alone (KERNEL32 on Windows: no C++ runtime to ship)
long long nav_now_ns() {
#ifdef _WIN32
long long c = 0, f = 1;
QueryPerformanceCounter(&c);
QueryPerformanceFrequency(&f);
return (long long)((double)c * 1e9 / (double)f);
#else
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (long long)t.tv_sec * 1000000000LL + t.tv_nsec;
#endif
}
const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there
const int NAV_MAX_NODES = 4096; // the search's open list const int NAV_MAX_NODES = 4096; // the search's open list
const int NAV_FILTERS = 8; // costs per kind of ground: one set per kind of walker's taste const int NAV_FILTERS = 8; // costs per kind of ground: one set per kind of walker's taste
@ -30,6 +50,7 @@ struct Nav {
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
dtPolyRef polys[NAV_MAX_POLYS]; dtPolyRef polys[NAV_MAX_POLYS];
int partial = 0; // the last path stopped short of its end int partial = 0; // the last path stopped short of its end
long long ns = 0; // time spent finding paths, for the measure (17.10)
}; };
// a navmesh with its query, from params (a set of tiles) or from one tile's data // a navmesh with its query, from params (a set of tiles) or from one tile's data

View file

@ -47,6 +47,15 @@ export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float
export function nav_asked(nav_st: NavState) -> int { return nav_st.nv_asked } export function nav_asked(nav_st: NavState) -> int { return nav_st.nv_asked }
export function nav_found(nav_st: NavState) -> int { return nav_st.nv_found } export function nav_found(nav_st: NavState) -> int { return nav_st.nv_found }
export function nav_short(nav_st: NavState) -> int { return nav_st.nv_short } export function nav_short(nav_st: NavState) -> int { return nav_st.nv_short }
# microseconds spent finding those paths, over every loaded mesh
export function nav_us(nav_st: NavState) -> int {
var us = 0
for k in 0 .. NAV_KINDS {
let h = nv_mesh(nav_st, k)
if h != null { us += nvc_us(h) }
}
return us
}
export function nav_partial(nav_st: NavState, kind: int) -> bool { export function nav_partial(nav_st: NavState, kind: int) -> bool {
let h = nv_mesh(nav_st, kind) let h = nv_mesh(nav_st, kind)
return h != null and nvc_partial(h) == 1 return h != null and nvc_partial(h) == 1

View file

@ -124,4 +124,14 @@ program NavTest {
nav_reset(nav_st) nav_reset(nav_st)
expect_eq(nav_polygons(nav_st, NAV_SMALL), 0) expect_eq(nav_polygons(nav_st, NAV_SMALL), 0)
} }
test "the time paths take is counted, and only while a mesh is loaded" (nav_st: mut NavState) {
expect_eq(nav_us(nav_st), 0)
expect(meadow(nav_st, NAV_PERSON, 0, true))
for i in 0 .. 2000 { nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) }
expect(nav_us(nav_st) > 0)
expect(nav_us(nav_st) < 2000000)
nav_reset(nav_st)
expect_eq(nav_us(nav_st), 0)
}
} }

View file

@ -114,7 +114,7 @@ by a person's taste.
| `npc_pick_name(body)`, `npc_name_used(n)`, `npc_name_clashes()` | names | | `npc_pick_name(body)`, `npc_name_used(n)`, `npc_name_clashes()` | names |
| `npc_talk(p, player)`, `npc_line_for(p)`, `npc_words(p, line)`, `npc_said()`, `npc_choose(p, line, c, player)`, `npc_talked_today(p)`, `npc_greeting(p)` | talk | | `npc_talk(p, player)`, `npc_line_for(p)`, `npc_words(p, line)`, `npc_said()`, `npc_choose(p, line, c, player)`, `npc_talked_today(p)`, `npc_greeting(p)` | talk |
| `npc_mirror(on)`, `npc_mirroring()`, `npc_put(slot, id, kind, look, seed, name, hx, hz)`, `npc_heard(slot, x, y, z, yaw, speed, act, stalk)`, `npc_heard_lately(p)`, `npc_mirror_tick(dt)` | a guest's copy of the host's people | | `npc_mirror(on)`, `npc_mirroring()`, `npc_put(slot, id, kind, look, seed, name, hx, hz)`, `npc_heard(slot, x, y, z, yaw, speed, act, stalk)`, `npc_heard_lately(p)`, `npc_mirror_tick(dt)` | a guest's copy of the host's people |
| `npc_facts() -> Queue<NpcFact>` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`) | | `npc_facts() -> Queue<NpcFact>` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`), `NPC_F_STUCK` (`how` - `NPC_STUCK_DETOUR`, `_RETRY`, `_GAVE_UP` - at `x`, `z`: the way failed it and it fell back) |
| `npc_system() -> System` | `"npc"`, `PH_SIMULATE`, a reset and no tick; nothing saved | | `npc_system() -> System` | `"npc"`, `PH_SIMULATE`, a reset and no tick; nothing saved |
## Tests ## Tests

View file

@ -131,6 +131,7 @@ export open registry NpcNames of NpcName as NPCN
export const NPC_F_GREETED: int = 0 # a player came within notice export const NPC_F_GREETED: int = 0 # a player came within notice
export const NPC_F_TALKED: int = 1 # a player talked (line, choice -1 or the choice taken) export const NPC_F_TALKED: int = 1 # a player talked (line, choice -1 or the choice taken)
export const NPC_F_ARRIVED: int = 2 # got where it was walking, and began the act export const NPC_F_ARRIVED: int = 2 # got where it was walking, and began the act
export const NPC_F_STUCK: int = 3 # the way failed it and it fell back (`how`, at `x`, `z`)
export property NpcFact { export property NpcFact {
what: int = 0 what: int = 0
@ -141,4 +142,7 @@ export property NpcFact {
line: int = -1 line: int = -1
choice: int = -1 choice: int = -1
act: int = -1 act: int = -1
how: int = 0 # NPC_STUCK_*
x: float = 0.0
z: float = 0.0
} }

View file

@ -114,7 +114,7 @@ program NpcTest {
expect_eq(p.act, NPCA_SIT) expect_eq(p.act, NPCA_SIT)
} }
test "nobody walks into the lake" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) { test "nobody walks into the lake, and turning back from it is told as a fallback" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(npc_st, npc_tests_fake_st, NPCK_HIKER) let p = one(npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_go(npc_st, p, -80.0, 0.0, NPCA_SIT, 30.0) npc_go(npc_st, p, -80.0, 0.0, NPCA_SIT, 30.0)
var west = 0.0 var west = 0.0
@ -123,6 +123,12 @@ program NpcTest {
west = Math.min(west, p.x) west = Math.min(west, p.x)
} }
expect(west > -50.5) expect(west > -50.5)
var fell = 0
let fs = q_drain(npc_facts(npc_st))
for i in 0 .. len(fs) {
if fs[i].what == NPC_F_STUCK and fs[i].x > -52.0 { fell += 1 }
}
expect(fell >= 1)
} }
test "a step's spot is where its hours say: the shore by day, home by night" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) { test "a step's spot is where its hours say: the shore by day, home by night" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {

View file

@ -78,5 +78,7 @@ program NpcWayTest {
expect_eq(p.act, NPCA_SIT) expect_eq(p.act, NPCA_SIT)
expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3) expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3)
expect(way_test_st.asked > 5) expect(way_test_st.asked > 5)
let fs = q_drain(npc_facts(npc_st))
for i in 0 .. len(fs) { expect(fs[i].what != NPC_F_STUCK) }
} }
} }

View file

@ -46,6 +46,7 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void
p.stall = p.stall + dt p.stall = p.stall + dt
if p.stall > p.stall_max { p.stall_max = p.stall } if p.stall > p.stall_max { p.stall_max = p.stall }
if p.detours < 10 and np_detour(p, want) { if p.detours < 10 and np_detour(p, want) {
np_stuck_fact(npc_st, p, NPC_STUCK_DETOUR)
p.detours += 1 p.detours += 1
return return
} }

View file

@ -1,6 +1,15 @@
# way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way, # way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way,
# a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way, # a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way,
# and giving the target up after three tries # and giving the target up after three tries - each of those a NPC_F_STUCK fact, so the fallback is seen
export const NPC_STUCK_DETOUR: int = 0 # a step round what was in front of it
export const NPC_STUCK_RETRY: int = 1 # no closer for twenty seconds, or no step round: planned again
export const NPC_STUCK_GAVE_UP: int = 2 # the third of those: the target given up
function np_stuck_fact(npc_st: NpcState, p: NpcPerson, how: int) -> void {
let f = np_fact(npc_st, NPC_F_STUCK, p, -1)
f.how = how
f.x = p.x
f.z = p.z
}
# the next corner toward the target, asked again every half second, on reaching it, or for a new target # the next corner toward the target, asked again every half second, on reaching it, or for a new target
const NP_WAY_EVERY: float = 0.5 const NP_WAY_EVERY: float = 0.5
@ -39,7 +48,11 @@ function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
p.fails += 1 p.fails += 1
p.via_on = false p.via_on = false
if p.fails >= 3 { if p.fails >= 3 {
np_stuck_fact(npc_st, p, NPC_STUCK_GAVE_UP)
p.fails = 0 p.fails = 0
npc_wander(npc_st, p) npc_wander(npc_st, p)
} else { npc_plan(npc_st, p) } } else {
np_stuck_fact(npc_st, p, NPC_STUCK_RETRY)
npc_plan(npc_st, p)
}
} }

View file

@ -26,6 +26,9 @@ import "ludic.physics"
## The API ## The API
A query fills a record the caller keeps (`p`, `h`) and allocates nothing: it is asked every frame, and
Ludic gives no allocation back.
| | | | | |
| --- | --- | | --- | --- |
| `phys_open(st, max_bodies, threads) -> bool`, `phys_close(st)`, `phys_is_open(st)` | a world, and everything in it let go | | `phys_open(st, max_bodies, threads) -> bool`, `phys_close(st)`, `phys_is_open(st)` | a world, and everything in it let go |
@ -34,15 +37,15 @@ import "ludic.physics"
| `phys_ground_add`, `phys_static_add`, `phys_kinematic_add`, `phys_body_add(..., mass)` | bodies: the ground, still things, what the game moves (the player), what falls and floats | | `phys_ground_add`, `phys_static_add`, `phys_kinematic_add`, `phys_body_add(..., mass)` | bodies: the ground, still things, what the game moves (the player), what falls and floats |
| `phys_settle(st)` | after adding many still things, before the first query | | `phys_settle(st)` | after adding many still things, before the first query |
| `phys_remove(st, id)`, `phys_count(st)` | gone for good | | `phys_remove(st, id)`, `phys_count(st)` | gone for good |
| `phys_pose(st, id) -> PhysPose`, `phys_yaw_of(p)` | position, rotation, velocity | | `phys_pose(st, id, p) -> bool`, `phys_yaw_of(p)` | position, rotation, velocity |
| `phys_place`, `phys_impulse`, `phys_set_velocity`, `phys_set_kinematic_target`, `phys_awake` | moving a body | | `phys_place`, `phys_impulse`, `phys_set_velocity`, `phys_set_kinematic_target`, `phys_awake` | moving a body |
| `phys_ray(st, o, d, mask) -> PhysHit` | the first thing along a ray | | `phys_ray(st, o, d, mask, h) -> bool` | the first thing along a ray |
| `phys_top_at(st, x, z, r, from_y, depth, mask) -> float` | what a foot would land on (`CharacterGround.top_at`), `PHYS_NONE` if nothing | | `phys_top_at(st, x, z, r, from_y, depth, mask) -> float` | what a foot would land on (`CharacterGround.top_at`), `PHYS_NONE` if nothing |
| `phys_push(st, x, z, r, y0, y1, mask) -> PhysPush` | the move that stands an upright body clear (`CharacterGround.push`) | | `phys_push(st, x, z, r, y0, y1, mask, p) -> int` | the move that stands an upright body clear (`CharacterGround.push`) |
| `phys_resolve(st, x, z, r, y0, y1, mask) -> bool`, `phys_resolved_x` / `_z` | pushed clear in two passes, the place kept (`CharacterGround.push`, `pushed_x` / `_z`); a thing wholly under the feet or over the head is not in the way | | `phys_resolve(st, x, z, r, y0, y1, mask) -> bool`, `phys_resolved_x` / `_z` | pushed clear in two passes, the place kept (`CharacterGround.push`, `pushed_x` / `_z`); a thing wholly under the feet or over the head is not in the way |
| `phys_step_top(st, x, z, r, feet, reach, mask) -> float` | the highest top a foot could step up to, anything taller than `feet + reach` left out as a wall | | `phys_step_top(st, x, z, r, feet, reach, mask) -> float` | the highest top a foot could step up to, anything taller than `feet + reach` left out as a wall |
| `phys_overlap(st, x, y, z, r, mask) -> []int` | the bodies a ball touches | | `phys_overlap(st, x, y, z, r, mask) -> int`, `phys_overlap_id(st, i)` | how many bodies a ball touches, and each one |
| `phys_walker_add(st, r, h, x, y, z, max_slope, mass)`, `phys_walker_step(st, id, dt, vx, vz, jump) -> ground`, `phys_walker_pose -> PhysWalk`, `phys_walker_place`, `phys_walker_limits(step_up, stick)`, `phys_walker_remove` | a walking body (Jolt's CharacterVirtual): gravity, landing, steps, slopes, following the ground down; it shoves dynamic things and rays find its `body` | | `phys_walker_add(st, r, h, x, y, z, max_slope, mass)`, `phys_walker_step(st, id, dt, vx, vz, jump) -> ground`, `phys_walker_pose(st, id, p) -> bool`, `phys_walker_place`, `phys_walker_limits(step_up, stick)`, `phys_walker_remove` | a walking body (Jolt's CharacterVirtual): gravity, landing, steps, slopes, following the ground down; it shoves dynamic things and rays find its `body` |
| `phys_force`, `phys_torque`, `phys_angular_impulse`, `phys_spin_y`, `phys_set_pose(st, id, pose)` | an oar's stroke and a turn; a guest's copy put where the host said | | `phys_force`, `phys_torque`, `phys_angular_impulse`, `phys_spin_y`, `phys_set_pose(st, id, pose)` | an oar's stroke and a turn; a guest's copy put where the host said |
| `phys_float(st, id, buoyancy, linear_drag, angular_drag)`, `phys_sink`, `phys_floating` | a body buoyed each step against `PhysWater` and carried by its current | | `phys_float(st, id, buoyancy, linear_drag, angular_drag)`, `phys_sink`, `phys_floating` | a body buoyed each step against `PhysWater` and carried by its current |
| `phys_facts(st)` | `PhysFact`: `PHYS_HIT` (two bodies met at `ph_hard` m/s or more), `PHYS_SPLASH` (a floating body reached water) | | `phys_facts(st)` | `PhysFact`: `PHYS_HIT` (two bodies met at `ph_hard` m/s or more), `PHYS_SPLASH` (a floating body reached water) |

View file

@ -43,17 +43,16 @@ export function phys_remove(physics_st: mut PhysicsState, id: int) -> void {
jph_body_remove(physics_st.ph_world, id) jph_body_remove(physics_st.ph_world, id)
} }
# where a body is, how it is turned and how fast it goes; null when there is no such body # where a body is, how it is turned and how fast it goes, into the caller's own record (a query asked
export function phys_pose(physics_st: mut PhysicsState, id: int) -> PhysPose { # every frame allocates nothing: there is no GC); false, and p untouched, when there is no such body
if physics_st.ph_world == null { return null } export function phys_pose(physics_st: mut PhysicsState, id: int, p: PhysPose) -> bool {
ph_buffers() if physics_st.ph_world == null { return false }
if jph_body_read(physics_st.ph_world, id, physics_st.ph_vals) != 0 { return null } if jph_body_read(physics_st.ph_world, id, physics_st.ph_vals) != 0 { return false }
let v = physics_st.ph_vals let v = physics_st.ph_vals
let p = new PhysPose
p.x = v[0]; p.y = v[1]; p.z = v[2] p.x = v[0]; p.y = v[1]; p.z = v[2]
p.qx = v[3]; p.qy = v[4]; p.qz = v[5]; p.qw = v[6] p.qx = v[3]; p.qy = v[4]; p.qz = v[5]; p.qw = v[6]
p.vx = v[7]; p.vy = v[8]; p.vz = v[9] p.vx = v[7]; p.vy = v[8]; p.vz = v[9]
return p return true
} }
# the heading a pose's rotation gives, in radians about y # the heading a pose's rotation gives, in radians about y

View file

@ -12,9 +12,9 @@ export function phys_hull_add(physics_st: mut PhysicsState, x: float, y: float,
# a stroke of `forward` newtons along the bow and a turn of `turn` newton-metres about y, over the # a stroke of `forward` newtons along the bow and a turn of `turn` newton-metres about y, over the
# next step # next step
export function phys_row(physics_st: mut PhysicsState, id: int, forward: float, turn: float) -> void { export function phys_row(physics_st: mut PhysicsState, id: int, forward: float, turn: float) -> void {
let p = phys_pose(physics_st, id) if physics_st.ph_world == null or jph_body_read(physics_st.ph_world, id, physics_st.ph_vals) != 0 { return }
if p == null { return } let v = physics_st.ph_vals
let yaw = phys_yaw_of(p) let yaw = Math.atan2(2.0 * (v[6] * v[4] + v[3] * v[5]), 1.0 - 2.0 * (v[4] * v[4] + v[3] * v[3]))
phys_force(physics_st, id, -Math.sin(yaw) * forward, 0.0, -Math.cos(yaw) * forward) phys_force(physics_st, id, -Math.sin(yaw) * forward, 0.0, -Math.cos(yaw) * forward)
phys_torque(physics_st, id, 0.0, turn, 0.0) phys_torque(physics_st, id, 0.0, turn, 0.0)
} }

View file

@ -12,18 +12,19 @@ export property PhysHit {
nz: float = 0.0 nz: float = 0.0
} }
# a ray from (ox, oy, oz) along (dx, dy, dz), whose length is the reach # a ray from (ox, oy, oz) along (dx, dy, dz), whose length is the reach, into the caller's record;
export function phys_ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int) -> PhysHit { # true when it hit something (h.body -1 and h.fraction 1 when not)
let h = new PhysHit export function phys_ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int, h: PhysHit) -> bool {
if physics_st.ph_world == null { return h } h.body = -1
ph_buffers() h.fraction = 1.0
if physics_st.ph_world == null { return false }
h.body = jph_ray(physics_st.ph_world, ox, oy, oz, dx, dy, dz, mask, physics_st.ph_vals) h.body = jph_ray(physics_st.ph_world, ox, oy, oz, dx, dy, dz, mask, physics_st.ph_vals)
if h.body == -1 { return h } if h.body == -1 { return false }
let v = physics_st.ph_vals let v = physics_st.ph_vals
h.fraction = v[0] h.fraction = v[0]
h.x = v[1]; h.y = v[2]; h.z = v[3] h.x = v[1]; h.y = v[2]; h.z = v[3]
h.nx = v[4]; h.ny = v[5]; h.nz = v[6] h.nx = v[4]; h.ny = v[5]; h.nz = v[6]
return h return true
} }
# a ball r across let down from from_y at most depth metres: the height of what it lands on, or # a ball r across let down from from_y at most depth metres: the height of what it lands on, or
@ -45,15 +46,13 @@ export function phys_step_top(physics_st: PhysicsState, x: float, z: float, r: f
return y return y
} }
# the bodies a ball at (x, y, z) touches, in the order Jolt sorts them # how many bodies a ball at (x, y, z) touches, in the order Jolt sorts them; phys_overlap_id(i)
export function phys_overlap(physics_st: mut PhysicsState, x: float, y: float, z: float, r: float, mask: int) -> []int { # reads them back until the next query
let out = new []int export function phys_overlap(physics_st: mut PhysicsState, x: float, y: float, z: float, r: float, mask: int) -> int {
if physics_st.ph_world == null { return out } if physics_st.ph_world == null { return 0 }
ph_buffers() return jph_overlap(physics_st.ph_world, x, y, z, r, mask, physics_st.ph_ids, PH_CAP)
let n = jph_overlap(physics_st.ph_world, x, y, z, r, mask, physics_st.ph_ids, PH_CAP)
for i in 0 .. n { push(out, physics_st.ph_ids[i]) }
return out
} }
export function phys_overlap_id(physics_st: PhysicsState, i: int) -> int { return physics_st.ph_ids[i] }
export property PhysPush { export property PhysPush {
x: float = 0.0 # how far to move in x and z to stand clear x: float = 0.0 # how far to move in x and z to stand clear
@ -63,14 +62,18 @@ export property PhysPush {
# an upright body r wide from y0 to y1 at (x, z): the move that stands it clear - what # an upright body r wide from y0 to y1 at (x, z): the move that stands it clear - what
# CharacterGround.push asks # CharacterGround.push asks
export function phys_push(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> PhysPush { export function phys_push(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int, p: PhysPush) -> int {
let p = new PhysPush p.n = ph_push(physics_st, x, z, r, y0, y1, mask)
if physics_st.ph_world == null { return p }
ph_buffers()
p.n = jph_push(physics_st.ph_world, x, z, r, y0, y1, mask, physics_st.ph_vals)
p.x = physics_st.ph_vals[0] p.x = physics_st.ph_vals[0]
p.z = physics_st.ph_vals[1] p.z = physics_st.ph_vals[1]
return p return p.n
}
# how many things are in the way, the move clear in ph_vals[0], [1]
function ph_push(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> int {
physics_st.ph_vals[0] = 0.0
physics_st.ph_vals[1] = 0.0
if physics_st.ph_world == null { return 0 }
return jph_push(physics_st.ph_world, x, z, r, y0, y1, mask, physics_st.ph_vals)
} }
# a body r wide from y0 to y1 at (x, z) moved clear of what it stands in, two passes over (a push out # a body r wide from y0 to y1 at (x, z) moved clear of what it stands in, two passes over (a push out
@ -82,10 +85,9 @@ export function phys_resolve(physics_st: mut PhysicsState, x: float, z: float, r
var pz = z var pz = z
var moved = false var moved = false
for pass in 0 .. 2 { for pass in 0 .. 2 {
let p = phys_push(physics_st, px, pz, r, y0, y1, mask) if ph_push(physics_st, px, pz, r, y0, y1, mask) > 0 {
if p.n > 0 { px += physics_st.ph_vals[0]
px += p.x pz += physics_st.ph_vals[1]
pz += p.z
moved = true moved = true
} }
} }

View file

@ -4,6 +4,11 @@ import "ludic.physics"
import "ludic.base" import "ludic.base"
program HullShapeTest { program HullShapeTest {
numbers float numbers float
function ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int) -> PhysHit {
let h = new PhysHit
phys_ray(physics_st, ox, oy, oz, dx, dy, dz, mask, h)
return h
}
# a cube two metres across, its centre two metres along x from its origin # a cube two metres across, its centre two metres along x from its origin
function cube(physics_st: mut PhysicsState) -> int { function cube(physics_st: mut PhysicsState) -> int {
@ -15,7 +20,7 @@ program HullShapeTest {
} }
return phys_convex(physics_st, v, 8) return phys_convex(physics_st, v, 8)
} }
function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit { return phys_ray(physics_st, x, 10.0, z, 0.0, -20.0, 0.0, PHYS_M_ALL) } function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit { return ray(physics_st, x, 10.0, z, 0.0, -20.0, 0.0, PHYS_M_ALL) }
test "a hull stands where its points are, and too few points are no shape" (physics_st: mut PhysicsState) { test "a hull stands where its points are, and too few points are no shape" (physics_st: mut PhysicsState) {
expect(phys_open(physics_st, 16, 1)) expect(phys_open(physics_st, 16, 1))

View file

@ -5,6 +5,11 @@ import "ludic.physics"
import "ludic.base" import "ludic.base"
program MotionTest { program MotionTest {
numbers float numbers float
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
let p = new PhysPose
if not phys_pose(physics_st, id, p) { return null }
return p
}
function lake_h(x: float, z: float) -> float { function lake_h(x: float, z: float) -> float {
if x > 20.0 { return 0.0 } if x > 20.0 { return 0.0 }
@ -45,7 +50,7 @@ program MotionTest {
let crate = phys_body_add(physics_st, phys_box(physics_st, 0.5, 0.25, 0.5), 30.0, 2.0, 0.0, 0.0, 60.0) let crate = phys_body_add(physics_st, phys_box(physics_st, 0.5, 0.25, 0.5), 30.0, 2.0, 0.0, 0.0, 60.0)
phys_float(physics_st, crate, 1.2, 2.0, 2.0) phys_float(physics_st, crate, 1.2, 2.0, 2.0)
sim(physics_st, 8.0) sim(physics_st, 8.0)
let p = phys_pose(physics_st, crate) let p = pose(physics_st, crate)
expect(p.y > -0.4) expect(p.y > -0.4)
expect(p.y < 0.4) expect(p.y < 0.4)
expect(Math.abs(p.vy) < 0.2) expect(Math.abs(p.vy) < 0.2)
@ -55,7 +60,7 @@ program MotionTest {
phys_sink(physics_st, crate) phys_sink(physics_st, crate)
expect(not phys_floating(physics_st, crate)) expect(not phys_floating(physics_st, crate))
sim(physics_st, 4.0) sim(physics_st, 4.0)
expect(phys_pose(physics_st, crate).y < -2.5) expect(pose(physics_st, crate).y < -2.5)
phys_close(physics_st) phys_close(physics_st)
} }
@ -87,7 +92,7 @@ program MotionTest {
phys_set_kinematic_target(physics_st, me, 2.0 + float(i + 1) * 0.03, 0.95, 0.0, 0.0) phys_set_kinematic_target(physics_st, me, 2.0 + float(i + 1) * 0.03, 0.95, 0.0, 0.0)
phys_step(physics_st) phys_step(physics_st)
} }
expect(phys_pose(physics_st, crate).x > 5.0) expect(pose(physics_st, crate).x > 5.0)
phys_close(physics_st) phys_close(physics_st)
} }
@ -114,7 +119,7 @@ program MotionTest {
for i in 0 .. 12 { last = phys_body_add(physics_st, b, 3.0 + float(i % 3) * 0.3, 2.0 + float(i) * 0.7, float(i % 2) * 0.2, float(i) * 0.4, 8.0) } for i in 0 .. 12 { last = phys_body_add(physics_st, b, 3.0 + float(i % 3) * 0.3, 2.0 + float(i) * 0.7, float(i % 2) * 0.2, float(i) * 0.4, 8.0) }
phys_impulse(physics_st, last, 20.0, 0.0, 5.0) phys_impulse(physics_st, last, 20.0, 0.0, 5.0)
sim(physics_st, 4.0) sim(physics_st, 4.0)
let p = phys_pose(physics_st, last) let p = pose(physics_st, last)
phys_close(physics_st) phys_close(physics_st)
return p return p
} }
@ -132,21 +137,21 @@ program MotionTest {
world(physics_st) world(physics_st)
let b = phys_hull_add(physics_st, 40.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0) let b = phys_hull_add(physics_st, 40.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0)
sim(physics_st, 4.0) sim(physics_st, 4.0)
let p0 = phys_pose(physics_st, b) let p0 = pose(physics_st, b)
expect(p0.y > -0.2) expect(p0.y > -0.2)
expect(p0.y < 0.2) expect(p0.y < 0.2)
for i in 0 .. 240 { for i in 0 .. 240 {
phys_row(physics_st, b, 420.0, 0.0) phys_row(physics_st, b, 420.0, 0.0)
phys_step(physics_st) phys_step(physics_st)
} }
let p1 = phys_pose(physics_st, b) let p1 = pose(physics_st, b)
expect(p1.x < 34.0) expect(p1.x < 34.0)
expect(phys_bow_speed(p1) > 1.0) expect(phys_bow_speed(p1) > 1.0)
for i in 0 .. 120 { for i in 0 .. 120 {
phys_row(physics_st, b, 0.0, 520.0) phys_row(physics_st, b, 0.0, 520.0)
phys_step(physics_st) phys_step(physics_st)
} }
expect(Math.abs(phys_yaw_of(phys_pose(physics_st, b)) - phys_yaw_of(p1)) > 0.5) expect(Math.abs(phys_yaw_of(pose(physics_st, b)) - phys_yaw_of(p1)) > 0.5)
phys_close(physics_st) phys_close(physics_st)
world(physics_st) world(physics_st)
let c = phys_hull_add(physics_st, 24.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0) let c = phys_hull_add(physics_st, 24.0, 0.5, 0.0, 1.5707963, 0.6, 0.25, 1.9, 180.0, 4.0, 1.0)
@ -154,7 +159,7 @@ program MotionTest {
phys_row(physics_st, c, 420.0, 0.0) phys_row(physics_st, c, 420.0, 0.0)
phys_step(physics_st) phys_step(physics_st)
} }
expect(phys_pose(physics_st, c).x > 19.5) expect(pose(physics_st, c).x > 19.5)
phys_close(physics_st) phys_close(physics_st)
} }
} }

View file

@ -5,6 +5,21 @@ import "ludic.physics"
import "ludic.base" import "ludic.base"
program PhysicsTest { program PhysicsTest {
numbers float numbers float
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
let p = new PhysPose
if not phys_pose(physics_st, id, p) { return null }
return p
}
function ray(physics_st: mut PhysicsState, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, mask: int) -> PhysHit {
let h = new PhysHit
phys_ray(physics_st, ox, oy, oz, dx, dy, dz, mask, h)
return h
}
function push_of(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> PhysPush {
let p = new PhysPush
phys_push(physics_st, x, z, r, y0, y1, mask, p)
return p
}
# the lake: everything east of x = 20, its surface at y = 0, drifting north at 0.5 m/s # the lake: everything east of x = 20, its surface at y = 0, drifting north at 0.5 m/s
function lake_h(x: float, z: float) -> float { function lake_h(x: float, z: float) -> float {
@ -38,15 +53,15 @@ program PhysicsTest {
let id = phys_body_add(physics_st, b, 0.0, 4.0, 0.0, 0.0, 20.0) let id = phys_body_add(physics_st, b, 0.0, 4.0, 0.0, 0.0, 20.0)
expect(id != -1) expect(id != -1)
sim(physics_st, 3.0) sim(physics_st, 3.0)
let p = phys_pose(physics_st, id) let p = pose(physics_st, id)
expect(p != null) expect(p != null)
expect(Math.abs(p.y - 0.5) < 0.05) expect(Math.abs(p.y - 0.5) < 0.05)
expect(Math.abs(p.vy) < 0.05) expect(Math.abs(p.vy) < 0.05)
let h = phys_ray(physics_st, 0.0, 10.0, 0.0, 0.0, -20.0, 0.0, PHYS_M_ALL) let h = ray(physics_st, 0.0, 10.0, 0.0, 0.0, -20.0, 0.0, PHYS_M_ALL)
expect_eq(h.body, id) expect_eq(h.body, id)
expect(Math.abs(h.y - 1.0) < 0.05) expect(Math.abs(h.y - 1.0) < 0.05)
expect(h.ny > 0.9) expect(h.ny > 0.9)
let g = phys_ray(physics_st, 5.0, 10.0, 5.0, 0.0, -20.0, 0.0, PHYS_M_ALL) let g = ray(physics_st, 5.0, 10.0, 5.0, 0.0, -20.0, 0.0, PHYS_M_ALL)
expect(g.body != -1) expect(g.body != -1)
expect(g.body != id) expect(g.body != id)
expect(Math.abs(g.y) < 0.01) expect(Math.abs(g.y) < 0.01)
@ -59,14 +74,13 @@ program PhysicsTest {
let id = phys_static_add(physics_st, post, 2.0, 0.0, 2.0, 0.0) let id = phys_static_add(physics_st, post, 2.0, 0.0, 2.0, 0.0)
phys_settle(physics_st) phys_settle(physics_st)
let n = phys_count(physics_st) let n = phys_count(physics_st)
let near = phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC) expect_eq(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC), 1)
expect_eq(len(near), 1) expect_eq(phys_overlap_id(physics_st, 0), id)
expect_eq(near[0], id) expect_eq(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_GROUND), 0)
expect_eq(len(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_GROUND)), 0)
phys_remove(physics_st, id) phys_remove(physics_st, id)
expect_eq(phys_count(physics_st), n - 1) expect_eq(phys_count(physics_st), n - 1)
expect_eq(len(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC)), 0) expect_eq(phys_overlap(physics_st, 2.0, 1.0, 2.0, 0.5, PHYS_M_STATIC), 0)
expect(phys_pose(physics_st, id) == null) expect(pose(physics_st, id) == null)
phys_close(physics_st) phys_close(physics_st)
} }
@ -86,11 +100,11 @@ program PhysicsTest {
let post = phys_offset(physics_st, phys_cylinder(physics_st, 1.0, 0.3), 0.0, 1.0, 0.0, 0.0) let post = phys_offset(physics_st, phys_cylinder(physics_st, 1.0, 0.3), 0.0, 1.0, 0.0, 0.0)
phys_static_add(physics_st, post, 0.0, 0.0, 0.0, 0.0) phys_static_add(physics_st, post, 0.0, 0.0, 0.0, 0.0)
phys_settle(physics_st) phys_settle(physics_st)
let p = phys_push(physics_st, 0.5, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC) let p = push_of(physics_st, 0.5, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC)
expect_eq(p.n, 1) expect_eq(p.n, 1)
expect(p.x > 0.1) expect(p.x > 0.1)
expect(Math.abs(p.z) < 0.05) expect(Math.abs(p.z) < 0.05)
let clear = phys_push(physics_st, 3.0, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC) let clear = push_of(physics_st, 3.0, 0.0, 0.4, 0.1, 1.8, PHYS_M_STATIC)
expect_eq(clear.n, 0) expect_eq(clear.n, 0)
phys_close(physics_st) phys_close(physics_st)
} }
@ -141,8 +155,8 @@ program PhysicsTest {
expect(g >= 0) expect(g >= 0)
phys_ground_add(physics_st, g) phys_ground_add(physics_st, g)
phys_settle(physics_st) phys_settle(physics_st)
let odd = phys_ray(physics_st, 5.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND) let odd = ray(physics_st, 5.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND)
let even = phys_ray(physics_st, 6.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND) let even = ray(physics_st, 6.5, 50.0, 10.5, 0.0, -100.0, 0.0, PHYS_M_GROUND)
expect(Math.abs(odd.y - 2.0) < 0.01) expect(Math.abs(odd.y - 2.0) < 0.01)
expect(Math.abs(even.y - 1.0) < 0.01) expect(Math.abs(even.y - 1.0) < 0.01)
phys_close(physics_st) phys_close(physics_st)

View file

@ -4,6 +4,16 @@ import "ludic.physics"
import "ludic.base" import "ludic.base"
program WalkerTest { program WalkerTest {
numbers float numbers float
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
let p = new PhysPose
if not phys_pose(physics_st, id, p) { return null }
return p
}
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
let p = new PhysWalk
if not phys_walker_pose(physics_st, id, p) { return null }
return p
}
function flat(physics_st: mut PhysicsState) -> void { function flat(physics_st: mut PhysicsState) -> void {
expect(phys_open(physics_st, 1024, 1)) expect(phys_open(physics_st, 1024, 1))
@ -20,11 +30,11 @@ program WalkerTest {
flat(physics_st) flat(physics_st)
let me = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 1.0, 0.0, 50.0, 70.0) let me = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 1.0, 0.0, 50.0, 70.0)
walk(physics_st, me, 1.0, 0.0, 0.0) walk(physics_st, me, 1.0, 0.0, 0.0)
let p = phys_walker_pose(physics_st, me) let p = wpose(physics_st, me)
expect_eq(p.ground, PHYS_ON_GROUND) expect_eq(p.ground, PHYS_ON_GROUND)
expect(Math.abs(p.y) < 0.05) expect(Math.abs(p.y) < 0.05)
walk(physics_st, me, 2.0, 3.0, 0.0) walk(physics_st, me, 2.0, 3.0, 0.0)
let q = phys_walker_pose(physics_st, me) let q = wpose(physics_st, me)
expect(Math.abs(q.x - 6.0) < 0.2) expect(Math.abs(q.x - 6.0) < 0.2)
expect(q.body >= 0) expect(q.body >= 0)
phys_close(physics_st) phys_close(physics_st)
@ -40,12 +50,12 @@ program WalkerTest {
let a = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 0.0, 50.0, 70.0) let a = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 0.0, 50.0, 70.0)
phys_walker_limits(physics_st, a, 0.55, 0.5, 50.0) phys_walker_limits(physics_st, a, 0.55, 0.5, 50.0)
walk(physics_st, a, 1.5, 2.0, 0.0) walk(physics_st, a, 1.5, 2.0, 0.0)
let pa = phys_walker_pose(physics_st, a) let pa = wpose(physics_st, a)
expect(pa.x > 2.5) expect(pa.x > 2.5)
expect(Math.abs(pa.y - 0.3) < 0.05) expect(Math.abs(pa.y - 0.3) < 0.05)
let b = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 10.0, 50.0, 70.0) let b = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 10.0, 50.0, 70.0)
walk(physics_st, b, 2.0, 2.0, 0.0) walk(physics_st, b, 2.0, 2.0, 0.0)
let pb = phys_walker_pose(physics_st, b) let pb = wpose(physics_st, b)
expect(pb.x < 1.7) expect(pb.x < 1.7)
expect(Math.abs(pb.y) < 0.05) expect(Math.abs(pb.y) < 0.05)
phys_close(physics_st) phys_close(physics_st)
@ -57,11 +67,11 @@ program WalkerTest {
walk(physics_st, me, 0.5, 0.0, 0.0) walk(physics_st, me, 0.5, 0.0, 0.0)
phys_walker_step(physics_st, me, 1.0 / 60.0, 0.0, 0.0, 5.0) phys_walker_step(physics_st, me, 1.0 / 60.0, 0.0, 0.0, 5.0)
walk(physics_st, me, 0.25, 0.0, 0.0) walk(physics_st, me, 0.25, 0.0, 0.0)
let up = phys_walker_pose(physics_st, me) let up = wpose(physics_st, me)
expect_eq(up.ground, PHYS_IN_AIR) expect_eq(up.ground, PHYS_IN_AIR)
expect(up.y > 0.5) expect(up.y > 0.5)
walk(physics_st, me, 1.5, 0.0, 0.0) walk(physics_st, me, 1.5, 0.0, 0.0)
let down = phys_walker_pose(physics_st, me) let down = wpose(physics_st, me)
expect_eq(down.ground, PHYS_ON_GROUND) expect_eq(down.ground, PHYS_ON_GROUND)
expect(Math.abs(down.y) < 0.05) expect(Math.abs(down.y) < 0.05)
phys_close(physics_st) phys_close(physics_st)
@ -75,12 +85,12 @@ program WalkerTest {
phys_walker_step(physics_st, me, 1.0 / 60.0, 2.0, 0.0, 0.0) phys_walker_step(physics_st, me, 1.0 / 60.0, 2.0, 0.0, 0.0)
phys_step(physics_st) phys_step(physics_st)
} }
expect(phys_pose(physics_st, crate).x > 2.5) expect(pose(physics_st, crate).x > 2.5)
phys_walker_place(physics_st, me, -10.0, 0.0, -10.0) phys_walker_place(physics_st, me, -10.0, 0.0, -10.0)
let p = phys_walker_pose(physics_st, me) let p = wpose(physics_st, me)
expect(Math.abs(p.x + 10.0) < 0.01) expect(Math.abs(p.x + 10.0) < 0.01)
phys_walker_remove(physics_st, me) phys_walker_remove(physics_st, me)
expect(phys_walker_pose(physics_st, me) == null) expect(wpose(physics_st, me) == null)
phys_close(physics_st) phys_close(physics_st)
} }
@ -94,11 +104,11 @@ program WalkerTest {
phys_step(physics_st) phys_step(physics_st)
} }
expect(phys_spin_y(physics_st, hull) > 0.1) expect(phys_spin_y(physics_st, hull) > 0.1)
expect(phys_pose(physics_st, hull).z > 0.5) expect(pose(physics_st, hull).z > 0.5)
let p = phys_pose(physics_st, hull) let p = pose(physics_st, hull)
p.x = 5.0 p.x = 5.0
phys_set_pose(physics_st, hull, p) phys_set_pose(physics_st, hull, p)
expect(Math.abs(phys_pose(physics_st, hull).x - 5.0) < 0.01) expect(Math.abs(pose(physics_st, hull).x - 5.0) < 0.01)
phys_close(physics_st) phys_close(physics_st)
} }
} }

View file

@ -53,19 +53,18 @@ export function phys_walker_step(physics_st: mut PhysicsState, id: int, dt: floa
return jph_char_step(physics_st.ph_world, k, dt, vx, vz, jump) return jph_char_step(physics_st.ph_world, k, dt, vx, vz, jump)
} }
export function phys_walker_pose(physics_st: mut PhysicsState, id: int) -> PhysWalk { # where a walker is, into the caller's record; false when there is no such walker
export function phys_walker_pose(physics_st: mut PhysicsState, id: int, p: PhysWalk) -> bool {
let k = ph_walker(physics_st, id) let k = ph_walker(physics_st, id)
if k == null { return null } if k == null { return false }
ph_buffers()
jph_char_read(k, physics_st.ph_vals) jph_char_read(k, physics_st.ph_vals)
let v = physics_st.ph_vals let v = physics_st.ph_vals
let p = new PhysWalk
p.x = v[0]; p.y = v[1]; p.z = v[2] p.x = v[0]; p.y = v[1]; p.z = v[2]
p.vx = v[3]; p.vy = v[4]; p.vz = v[5] p.vx = v[3]; p.vy = v[4]; p.vz = v[5]
p.ground = int(v[6]) p.ground = int(v[6])
p.ny = v[7] p.ny = v[7]
p.body = int(v[8]) p.body = int(v[8])
return p return true
} }
# put it somewhere, standing still: a teleport, a swap, getting off a boat # put it somewhere, standing still: a teleport, a swap, getting off a boat
@ -77,11 +76,12 @@ export function phys_walker_place(physics_st: mut PhysicsState, id: int, x: floa
# a body the game keeps elsewhere, moved by its walker: put where the body is if something else # a body the game keeps elsewhere, moved by its walker: put where the body is if something else
# moved it (a teleport, a ride, a swim), then one step under the given limits; the ground after it # moved it (a teleport, a ride, a swim), then one step under the given limits; the ground after it
export function phys_walker_move(physics_st: mut PhysicsState, id: int, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, max_slope: float, dt: float) -> int { export function phys_walker_move(physics_st: mut PhysicsState, id: int, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, max_slope: float, dt: float) -> int {
let p = phys_walker_pose(physics_st, id) let k = ph_walker(physics_st, id)
if p == null { return PHYS_IN_AIR } if k == null { return PHYS_IN_AIR }
let dx = p.x - x jph_char_read(k, physics_st.ph_vals)
let dy = p.y - y let dx = physics_st.ph_vals[0] - x
let dz = p.z - z let dy = physics_st.ph_vals[1] - y
let dz = physics_st.ph_vals[2] - z
if dx * dx + dy * dy + dz * dz > 0.0025 { phys_walker_place(physics_st, id, x, y, z) } if dx * dx + dy * dy + dz * dz > 0.0025 { phys_walker_place(physics_st, id, x, y, z) }
phys_walker_limits(physics_st, id, step, 0.5, max_slope) phys_walker_limits(physics_st, id, step, 0.5, max_slope)
return phys_walker_step(physics_st, id, dt, vx, vz, jump) return phys_walker_step(physics_st, id, dt, vx, vz, jump)

View file

@ -99,6 +99,7 @@ export state Render3dState {
gltf_comps: int = 0 gltf_comps: int = 0
gltf_tex_paths: []pointer = null gltf_tex_paths: []pointer = null
gltf_tex_ids: words = null gltf_tex_ids: words = null
gltf_tex_refs: words = null # how many primitives use each cached texture (model_release)
gltf_tex_n: int = 0 gltf_tex_n: int = 0
gltf_white: int = 0 gltf_white: int = 0
gltf_flat: int = 0 gltf_flat: int = 0

View file

@ -50,17 +50,17 @@ function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: i
} }
function gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -> int { 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) } if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256); render3d_st.gltf_tex_refs = words(256) }
var png: string = uri var png: string = uri
let n = len(uri) let n = len(uri)
if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png" } if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png" }
let path = render3d_st.gltf_dir + "/" + png let path = render3d_st.gltf_dir + "/" + png
var i = 0 var i = 0
while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { return render3d_st.gltf_tex_ids[i] }; i += 1 } while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { render3d_st.gltf_tex_refs[i] += 1; return render3d_st.gltf_tex_ids[i] }; i += 1 }
var dil = 0 var dil = 0
if render3d_st.gltf_cutout { dil = 24 } if render3d_st.gltf_cutout { dil = 24 }
let id = tex_load_ex(render3d_st, path, srgb, dil) let id = tex_load_ex(render3d_st, path, srgb, dil)
if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_n += 1 } if 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_refs[render3d_st.gltf_tex_n] = 1; render3d_st.gltf_tex_n += 1 }
return id return id
} }
@ -150,7 +150,10 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) } if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) }
else { else {
let k = gltf_mat_find(render3d_st, mname) 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] } 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]
gltf_tex_ref(render3d_st, pr.diff); gltf_tex_ref(render3d_st, pr.nrm); gltf_tex_ref(render3d_st, pr.arm)
}
else { print(`gltf: material {mname} has no textures and none were loaded before it`) } else { print(`gltf: material {mname} has no textures and none were loaded before it`) }
} }
} }

View file

@ -0,0 +1,70 @@
# gltf_release.ludic - a loaded model let go of: its meshes, and each texture once nothing else uses
# it. The glTF loader shares textures between models (a path loaded once, a LOD chain borrowing its
# LOD0's material), so each cached texture carries a count of the primitives using it.
# one more primitive uses the cached texture `id` (0, or a texture the cache did not load: nothing)
function gltf_tex_ref(render3d_st: mut Render3dState, id: int) -> void {
if id == 0 or render3d_st.gltf_tex_refs == null { return }
for i in 0 .. render3d_st.gltf_tex_n { if render3d_st.gltf_tex_ids[i] == id { render3d_st.gltf_tex_refs[i] += 1; return } }
}
# one primitive fewer: the last one frees the texture and forgets it, with every material naming it
function gltf_tex_unref(render3d_st: mut Render3dState, id: int) -> void {
if id == 0 or render3d_st.gltf_tex_refs == null { return }
for i in 0 .. render3d_st.gltf_tex_n {
if render3d_st.gltf_tex_ids[i] == id {
render3d_st.gltf_tex_refs[i] -= 1
if render3d_st.gltf_tex_refs[i] > 0 { return }
gpu_tex_free(render3d_st, id)
gltf_tex_forget(render3d_st, i)
gltf_mats_forget(render3d_st, id)
return
}
}
}
# the cache entry i goes; the last entry takes its place
function gltf_tex_forget(render3d_st: mut Render3dState, i: int) -> void {
let last = render3d_st.gltf_tex_n - 1
let paths = render3d_st.gltf_tex_paths
paths[i] = paths[last]
render3d_st.gltf_tex_ids[i] = render3d_st.gltf_tex_ids[last]
render3d_st.gltf_tex_refs[i] = render3d_st.gltf_tex_refs[last]
List.pop(paths)
render3d_st.gltf_tex_n = last
}
# a remembered material whose texture was freed is forgotten, so no later load borrows it
function gltf_mats_forget(render3d_st: mut Render3dState, id: int) -> void {
if render3d_st.gltf_mat_names == null { return }
var k = 0
while k < render3d_st.gltf_mat_n {
if render3d_st.gltf_mat_diff[k] == id or render3d_st.gltf_mat_nrm[k] == id or render3d_st.gltf_mat_arm[k] == id {
let last = render3d_st.gltf_mat_n - 1
let names = render3d_st.gltf_mat_names
names[k] = names[last]
render3d_st.gltf_mat_diff[k] = render3d_st.gltf_mat_diff[last]
render3d_st.gltf_mat_nrm[k] = render3d_st.gltf_mat_nrm[last]
render3d_st.gltf_mat_arm[k] = render3d_st.gltf_mat_arm[last]
List.pop(names)
render3d_st.gltf_mat_n = last
} else {
k += 1
}
}
}
# Let a model go: every primitive's mesh, and its textures once no other model uses them. The
# caller says when - no actor may draw the model afterwards (a released model's prims are emptied,
# so a stale draw of it draws nothing).
function model_release(render3d_st: mut Render3dState, m: Model) -> void {
if m == null or m.prims == null { return }
for i in 0 .. len(m.prims) {
let pr = m.prims[i]
if pr.mesh != null { gpu_mesh_free(render3d_st, pr.mesh); pr.mesh = null }
gltf_tex_unref(render3d_st, pr.diff); gltf_tex_unref(render3d_st, pr.nrm); gltf_tex_unref(render3d_st, pr.arm)
pr.diff = 0; pr.nrm = 0; pr.arm = 0
}
let prims = m.prims
while len(prims) > 0 { List.pop(prims) }
}

View file

@ -1903,8 +1903,9 @@ function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st:
return render3d_st.gvk_pc_pipe[k] return render3d_st.gvk_pc_pipe[k]
} }
} }
# a pipeline the driver refused is remembered as 0 too: retried on every draw, each try built its
# key and its create structs again and gave none of them back
let pipe = gvk_pipeline(render3d_st, 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(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_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) 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 } if p < 4096 { render3d_st.gvk_pc_last[p] = len(render3d_st.gvk_pc_prog) - 1 }

View file

@ -409,7 +409,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
let depth = gvk_is_depth(ifmt) let depth = gvk_is_depth(ifmt)
let cb = gvk_once_begin(render3d_st) let cb = gvk_once_begin(render3d_st)
gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
let bic = bytes(VkBufferImageCopy_sizeof) let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
Vk.zero(bic, VkBufferImageCopy_sizeof) Vk.zero(bic, VkBufferImageCopy_sizeof)
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) } else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }

View file

@ -26,6 +26,7 @@ import "shadow.ludic"
import "post.ludic" import "post.ludic"
import "quat.ludic" import "quat.ludic"
import "gltf.ludic" import "gltf.ludic"
import "gltf_release.ludic"
import "skin.ludic" import "skin.ludic"
import "scatter.ludic" import "scatter.ludic"
import "actor.ludic" import "actor.ludic"

View file

@ -24,6 +24,7 @@ property Layer {
inst: floats, # INST_FLOATS per instance inst: floats, # INST_FLOATS per instance
count: int = 0, count: int = 0,
cap: int = 0, cap: int = 0,
have: int = 0, # instances inst and scratch have room for now: grown toward cap as filled
near: float = 0.0, # float bits; instances beyond it draw as impostors (or not at all) near: float = 0.0, # float bits; instances beyond it draw as impostors (or not at all)
cull: float = 0.0, # float bits; instances beyond it are skipped (0 = never) cull: float = 0.0, # float bits; instances beyond it are skipped (0 = never)
buf: int = 0, buf: int = 0,
@ -386,8 +387,11 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
l.near = near l.near = near
l.cull = cull l.cull = cull
l.tint = v3_new(1.0, 1.0, 1.0) l.tint = v3_new(1.0, 1.0, 1.0)
l.inst = floats(cap * INST_FLOATS) # room for what the layer holds, not for what it might: 45 layers at full capacity up front were
l.scratch = floats(cap * INST_FLOATS) # 0.4 GB, most of it never filled (plan 23 of maroon-lake)
l.have = min(cap, 256)
l.inst = floats(l.have * INST_FLOATS)
l.scratch = floats(l.have * INST_FLOATS)
l.last_cam = v3_new(100000.0, 0.0, 0.0) l.last_cam = v3_new(100000.0, 0.0, 0.0)
l.buf = gpu_buffer_new(render3d_st) l.buf = gpu_buffer_new(render3d_st)
l.imp_buf = gpu_buffer_new(render3d_st) l.imp_buf = gpu_buffer_new(render3d_st)
@ -398,8 +402,23 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
return l return l
} }
# room for n instances (at most the layer's cap), doubling what there is so a fill costs a few copies
function layer_room(l: Layer, n: int) -> void {
if n <= l.have { return }
var want = max(l.have * 2, n)
if want > l.cap { want = l.cap }
let inst = floats(want * INST_FLOATS)
if l.count > 0 { mem_copy(data_of(inst), data_of(l.inst), l.count * INST_FLOATS * 4) }
free(l.inst)
free(l.scratch)
l.inst = inst
l.scratch = floats(want * INST_FLOATS)
l.have = want
}
function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void { function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
if l.count >= l.cap { return } if l.count >= l.cap { return }
layer_room(l, l.count + 1)
let o = l.count * INST_FLOATS let o = l.count * INST_FLOATS
l.inst[o] = x; l.inst[o + 1] = y; l.inst[o + 2] = z; l.inst[o + 3] = scale l.inst[o] = x; l.inst[o + 1] = y; l.inst[o + 2] = z; l.inst[o + 3] = scale
l.inst[o + 4] = Math.sin(yaw); l.inst[o + 5] = Math.cos(yaw); l.inst[o + 6] = seed; l.inst[o + 7] = wind l.inst[o + 4] = Math.sin(yaw); l.inst[o + 5] = Math.cos(yaw); l.inst[o + 6] = seed; l.inst[o + 7] = wind

View file

@ -162,16 +162,24 @@ function stream_evict(render3d_st: mut Render3dState, s: Stream) -> void {
} }
t = lo t = lo
# everything wanted by the walk in progress stays whatever the threshold says # everything wanted by the walk in progress stays whatever the threshold says
let kept = new []Chunk # compacted in place, and an evicted chunk goes whole: a new list per eviction and the chunks'
# own records were never given back
var w = 0
var i = 0 var i = 0
while i < s.n { while i < s.n {
let c = s.chunks[i] let c = s.chunks[i]
if c.used >= t or c.used == render3d_st.stream_walk_no { push(kept, c) } if c.used >= t or c.used == render3d_st.stream_walk_no {
else { if c.data != null { free(c.data) } } s.chunks[w] = c
w += 1
} else {
if c.data != null { free(c.data) }
free(c)
}
i += 1 i += 1
} }
s.chunks = kept let ch = s.chunks
s.n = len(kept) while len(ch) > w { List.pop(ch) }
s.n = w
for h in 0 .. STREAM_HASH { s.htab[h] = 0 } for h in 0 .. STREAM_HASH { s.htab[h] = 0 }
i = 0 i = 0
while i < s.n { s.keys[i] = s.chunks[i].key; stream_remember(s, s.chunks[i].key, i); i += 1 } while i < s.n { s.keys[i] = s.chunks[i].key; stream_remember(s, s.chunks[i].key, i); i += 1 }
@ -246,6 +254,7 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
} }
if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(render3d_st, s, cx, cz, c) { if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(render3d_st, s, cx, cz, c) {
let tg = gl_now_us() let tg = gl_now_us()
layer_room(l, l.count + c.count)
mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), data_of(c.data), c.count * INST_FLOATS * 4) mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), data_of(c.data), c.count * INST_FLOATS * 4)
l.count += c.count l.count += c.count
render3d_st.stream_us_gather = render3d_st.stream_us_gather + (gl_now_us() - tg) render3d_st.stream_us_gather = render3d_st.stream_us_gather + (gl_now_us() - tg)

View file

@ -145,6 +145,9 @@ function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float
function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void { function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void {
let px = png_decode(render3d_st, path) let px = png_decode(render3d_st, path)
if px == null { return } if px == null { return }
# a map set up over another lets go of the last one's photograph first
if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px) }
if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex) }
render3d_st.ter_ortho_px = px render3d_st.ter_ortho_px = px
render3d_st.ter_ortho_w = render3d_st.tex_w render3d_st.ter_ortho_w = render3d_st.tex_w
render3d_st.ter_ortho_c = render3d_st.tex_channels render3d_st.ter_ortho_c = render3d_st.tex_channels
@ -154,6 +157,7 @@ function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) gpu_tex_param(render3d_st, 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 { function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex) }
render3d_st.ter_dem_tex = tex_load(render3d_st, path, false) 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_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 render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
@ -167,6 +171,8 @@ function terrain_generate(render3d_st: mut Render3dState) -> void {
if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" } if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" }
if render3d_st.ter_smooth { defs = "#define SMOOTH\n" } if render3d_st.ter_smooth { defs = "#define SMOOTH\n" }
let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", defs) let p = r3d_program(render3d_st, "fullscreen.vert", "heightgen.frag", defs)
# a map generated over another: its 256 MB height texture goes first
if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex); render3d_st.ter_height_tex = 0 }
render3d_st.ter_height_tex = tex_target(render3d_st, TERRAIN_RES, TERRAIN_RES, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR) 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) let fbo = gpu_fb_new(render3d_st)
gpu_fb_bind(render3d_st, fbo) gpu_fb_bind(render3d_st, fbo)
@ -203,8 +209,9 @@ function terrain_generate(render3d_st: mut Render3dState) -> void {
mesh_draw(render3d_st, render3d_st.sky_fullscreen) mesh_draw(render3d_st, render3d_st.sky_fullscreen)
gpu_program_free(render3d_st, pn) gpu_program_free(render3d_st, pn)
gpu_tex_free(render3d_st, raw) gpu_tex_free(render3d_st, raw)
# read the heights back for placement # read the heights back for placement, into the array a previous map had: always the same size,
render3d_st.ter_heights = floats(TERRAIN_RES * TERRAIN_RES) # and made anew on every build it was 64 MB lost per world swap
if render3d_st.ter_heights == null { render3d_st.ter_heights = floats(TERRAIN_RES * TERRAIN_RES) }
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_height_tex) gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_height_tex)
gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 4) 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_tex_read(render3d_st, GPU_TEX2D, GL_RED, GL_FLOAT, data_of(render3d_st.ter_heights))

View file

@ -8,6 +8,16 @@ import "ludic.base"
import "ludic.physics" import "ludic.physics"
program VehiclesTest { program VehiclesTest {
numbers float numbers float
function pose(physics_st: mut PhysicsState, id: int) -> PhysPose {
let p = new PhysPose
if not phys_pose(physics_st, id, p) { return null }
return p
}
function wpose(physics_st: mut PhysicsState, id: int) -> PhysWalk {
let p = new PhysWalk
if not phys_walker_pose(physics_st, id, p) { return null }
return p
}
function fk_ground(x: float, z: float) -> float { function fk_ground(x: float, z: float) -> float {
if x > 10.0 { return -(x - 10.0) } if x > 10.0 { return -(x - 10.0) }
@ -95,7 +105,7 @@ program VehiclesTest {
fk_lake(physics_st) fk_lake(physics_st)
if vehicles_test_st.legs < 0 { vehicles_test_st.legs = phys_walker_add(physics_st, 0.8, 1.7, x, y, z, 40.0, 450.0) } if vehicles_test_st.legs < 0 { vehicles_test_st.legs = phys_walker_add(physics_st, 0.8, 1.7, x, y, z, 40.0, 450.0) }
phys_walker_move(physics_st, vehicles_test_st.legs, x, y, z, vx, vz, 0.0, 0.45, 40.0, dt) phys_walker_move(physics_st, vehicles_test_st.legs, x, y, z, vx, vz, 0.0, 0.45, 40.0, dt)
let p = phys_walker_pose(physics_st, vehicles_test_st.legs) let p = wpose(physics_st, vehicles_test_st.legs)
vehicles_test_st.lx = p.x vehicles_test_st.lx = p.x
vehicles_test_st.ly = p.y vehicles_test_st.ly = p.y
vehicles_test_st.lz = p.z vehicles_test_st.lz = p.z
@ -120,7 +130,7 @@ program VehiclesTest {
function fk_drift(physics_st: mut PhysicsState, vehicles_test_st: VehiclesTestState, nid: int, fx: float, fz: float) -> void { phys_force(physics_st, vehicles_test_st.hull, fx, 0.0, fz) } function fk_drift(physics_st: mut PhysicsState, vehicles_test_st: VehiclesTestState, nid: int, fx: float, fz: float) -> void { phys_force(physics_st, vehicles_test_st.hull, fx, 0.0, fz) }
function fk_read(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int) -> bool { function fk_read(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int) -> bool {
if vehicles_test_st.hull < 0 or vehicles_test_st.hull_nid != nid { return false } if vehicles_test_st.hull < 0 or vehicles_test_st.hull_nid != nid { return false }
let p = phys_pose(physics_st, vehicles_test_st.hull) let p = pose(physics_st, vehicles_test_st.hull)
if p == null { return false } if p == null { return false }
vehicles_test_st.hx = p.x vehicles_test_st.hx = p.x
vehicles_test_st.hy = p.y vehicles_test_st.hy = p.y

View file

@ -9,14 +9,16 @@ function wl_react(wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dt: fl
return true return true
} }
if a.state == WILD_WARY { 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) wl_away(a, a.sus_x, a.sus_z, 20.0)
wl_head(a, a.tx, a.tz, 2.6, dt)
a.speed = s.walk * 0.8 a.speed = s.walk * 0.8
wl_step(wildlife_st, a, dt) wl_step(wildlife_st, a, dt)
if a.timer < 0.0 { wl_wander(wildlife_st, a, 3.0 + wl_rnd(wildlife_st) * 4.0) } if a.timer < 0.0 { wl_wander(wildlife_st, a, 3.0 + wl_rnd(wildlife_st) * 4.0) }
return true return true
} }
if a.state == WILD_FLEE { 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) wl_away(a, WildlifeWorld.player_x(np), WildlifeWorld.player_z(np), 40.0)
wl_head(a, a.tx, a.tz, 5.0, dt)
a.speed = s.run a.speed = s.run
wl_step(wildlife_st, a, dt) wl_step(wildlife_st, a, dt)
if a.timer < 0.0 { if a.timer < 0.0 {

View file

@ -58,6 +58,9 @@ export state WildlifeState {
wl_dice: Rng = rng_new(97) wl_dice: Rng = rng_new(97)
wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed
wl_spot_ask: WildSpot = new WildSpot wl_spot_ask: WildSpot = new WildSpot
wl_turned: int = 0 # steps refused and turned 75 degrees instead: the fallback, counted
wl_turned_wet: int = 0 # of those, for water ahead
wl_turned_steep: int = 0 # for a climb too steep
} }
function wl_sp_all(wildlife_st: WildlifeState) -> []WildSpecies { function wl_sp_all(wildlife_st: WildlifeState) -> []WildSpecies {

View file

@ -14,6 +14,12 @@ function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) ->
} }
# true when it moved; a walker is slid round a still thing (a trunk, a tent), a bird is not # true when it moved; a walker is slid round a still thing (a trunk, a tent), a bird is not
# how many steps were refused and turned from since the start: how often the way was not enough
export function wildlife_turned(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned }
# of those, how many for water ahead and how many for a climb (the rest: pushed back by a still thing)
export function wildlife_turned_wet(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_wet }
export function wildlife_turned_steep(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_steep }
function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool {
let d = a.speed * dt let d = a.speed * dt
if d < 0.0001 { return false } if d < 0.0001 { return false }
@ -26,9 +32,13 @@ function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo
} }
let nh = WildlifeWorld.ground(nx, nz) let nh = WildlifeWorld.ground(nx, nz)
let moved = Math.abs(nx - a.x) + Math.abs(nz - a.z) let moved = Math.abs(nx - a.x) + Math.abs(nz - a.z)
if nh < WildlifeWorld.water(nx, nz) + 0.6 or nh - a.y > d * 1.2 or moved < d * 0.2 { let wet = nh < WildlifeWorld.water(nx, nz) + 0.6
let steep = nh - a.y > d * 1.2
if wet or steep or moved < d * 0.2 {
if wet { wildlife_st.wl_turned_wet += 1 } else if steep { wildlife_st.wl_turned_steep += 1 }
a.yaw = a.yaw + Math.deg_to_rad(75.0) a.yaw = a.yaw + Math.deg_to_rad(75.0)
a.timer = 0.0 a.timer = 0.0
wildlife_st.wl_turned += 1
return false return false
} }
a.x = nx a.x = nx

View file

@ -103,4 +103,23 @@ program WayTest {
expect_near(a.tx, a.home_x + 7.0, 0.001) expect_near(a.tx, a.home_x + 7.0, 0.001)
expect_near(a.tz, a.home_z + 3.0, 0.001) expect_near(a.tz, a.home_z + 3.0, 0.001)
} }
test "it runs from the player by the way too: round the wall's end, not into it" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) {
fk_setup(wildlife_st, wildlife_tests_fake_st)
let a = wildlife_make(wildlife_st, 0, 0.0, 0.0, false, 0.0, 0, 1.0, 1)
wildlife_tests_fake_st.fk_px[0] = 0.0
wildlife_tests_fake_st.fk_pz[0] = 20.0
wildlife_tests_fake_st.fk_hidden = true
a.state = WILD_FLEE
a.timer = 300.0
var east = -100.0
for k in 0 .. 1200 {
wildlife_tick(wildlife_st, 0.05, 0.0)
if a.z > -42.0 and a.z < -38.0 { east = Math.max(east, a.x) }
}
expect_eq(a.state, WILD_FLEE)
expect(east > 29.0)
expect(a.z < -45.0)
expect(way_test_st.asked > 10)
}
} }

View file

@ -34,3 +34,20 @@ function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float
a.tz = WildlifeWorld.way_z() a.tz = WildlifeWorld.way_z()
} }
} }
# a target d metres straight away from (fx, fz): where it runs or backs off to, by the world's way -
# round water and a cliff where there is one, and straight away as before where there is not
function wl_away(a: WildAnimal, fx: float, fz: float, d: float) -> void {
var dx = a.x - fx
var dz = a.z - fz
let l = Math.sqrt(dx * dx + dz * dz)
if l < 0.01 {
dx = -Math.sin(a.yaw)
dz = -Math.cos(a.yaw)
} else {
dx = dx / l
dz = dz / l
}
a.tx = a.x + dx * d
a.tz = a.z + dz * d
}

View file

@ -608,6 +608,13 @@ function headless_case(path: pointer, exp: pointer, label: pointer) -> void {
let got = capture_line(`{out} < /dev/null`) let got = capture_line(`{out} < /dev/null`)
if got == exp { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) } if got == exp { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
} }
# a program built headless whose verdict is one of its lines (the renderer logs before it)
function headless_line_case(path: pointer, exp: pointer, label: pointer) -> void {
let out = `{tmp_dir()}/h_{flat(path)}`
if not shq(`bin/ludicc --headless examples/{path}.ludic -o {out} > {out}.log 2>&1`) { bad2(label, capture_line(`grep -i error {out}.log | head -1`)); return }
let got = capture_line(`{out} < /dev/null 2>&1 | grep -c '^{exp}$'`)
if got == "1" { ok(label) } else { bad2(label, capture_line(`{out} < /dev/null 2>&1 | grep -i fail | head -1`)) }
}
# ludic migrate state and components: the header names what every member needs, a field read in a # ludic migrate state and components: the header names what every member needs, a field read in a
# member is not edited, a module named like a package keeps a state of its own, and the program # member is not edited, a module named like a package keeps a state of its own, and the program
# runs as it did # runs as it did
@ -1045,6 +1052,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/state_component_ro", "look is read-only here (look: Look)", "a component's state is read-only unless its header says mut") reject_case("rejected/state_component_ro", "look is read-only here (look: Look)", "a component's state is read-only unless its header says mut")
controller_case("state/component", "", " #10 big 10 1", "component.ludic (0.S: a component's header names its states; its getters, functions and events take them and the template never sees them)") controller_case("state/component", "", " #10 big 10 1", "component.ludic (0.S: a component's header names its states; its getters, functions and events take them and the template never sees them)")
headless_case("rendering/ui_render3d", "ok", "ui_render3d.ludic (ludic.ui's render3d backend builds against the renderer)") headless_case("rendering/ui_render3d", "ok", "ui_render3d.ludic (ludic.ui's render3d backend builds against the renderer)")
headless_line_case("rendering/release", "RELEASE OK", "release.ludic (model_release: meshes go, a shared texture stays until its last user goes)")
migrate_component_case() migrate_component_case()
migrate_foreign_case() migrate_foreign_case()
reject_case("rejected/runtime_type_clash", "PadButton is the runtime's enum", "a program's type named like one of the runtime's is refused") reject_case("rejected/runtime_type_clash", "PadButton is the runtime's enum", "a program's type named like one of the runtime's is refused")