diff --git a/changes/model-release.md b/changes/model-release.md new file mode 100644 index 00000000..705979c7 --- /dev/null +++ b/changes/model-release.md @@ -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. diff --git a/examples/rendering/release.ludic b/examples/rendering/release.ludic new file mode 100644 index 00000000..9a1b787c --- /dev/null +++ b/examples/rendering/release.ludic @@ -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) + } +} diff --git a/packages/ludic.character/tests/character_test.ludic b/packages/ludic.character/tests/character_test.ludic index c44ad399..6641d175 100644 --- a/packages/ludic.character/tests/character_test.ludic +++ b/packages/ludic.character/tests/character_test.ludic @@ -7,6 +7,11 @@ import "ludic.base" import "ludic.physics" program CharacterTest { 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 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 { 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 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.wy = p.y character_test_st.wz = p.z diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md index 0f6eba1f..70d99108 100644 --- a/packages/ludic.nav/README.md +++ b/packages/ludic.nav/README.md @@ -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_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_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 | ## Tests diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib index 3941b8a7..7d84075a 100755 --- a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10555b7baa5958700f0ce4599e85fc4fc18b9c92afbbacfa64f04b630f42f583 -size 176816 +oid sha256:969c1ee0010c0fe88abcdfceb1a166031a8f6519b9e77a27c27331cbe2036e0b +size 176912 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.dll b/packages/ludic.nav/lib/windows-x64/ludicnav.dll index 7a9c6695..0bdc9341 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.dll +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99671e7df79802010fbf132f736d1b5e2681f0cd72ab71e3aa6e7026b837c97c -size 337408 +oid sha256:0ef49349d93dbc3b94c3743f80fa56f2acf4f5e02f16faba6ec13823b5705aaa +size 337920 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.lib b/packages/ludic.nav/lib/windows-x64/ludicnav.lib index 9e1bdfe3..6bcfe136 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.lib +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3e4f7ae19a6113829feffe2ed99e4a8f6b8fb2831c90115f489d1ecf646216a -size 3866 +oid sha256:0ea766bfe59613b41a1566f41de772d49602191ad7e188bef3bcf453c55d6114 +size 4022 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic index 71d02ce1..190af731 100644 --- a/packages/ludic.nav/native.ludic +++ b/packages/ludic.nav/native.ludic @@ -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_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_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_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" diff --git a/packages/ludic.nav/native/shim/nav_query.inl b/packages/ludic.nav/native/shim/nav_query.inl index c3a8fdf3..9eb476a6 100644 --- a/packages/ludic.nav/native/shim/nav_query.inl +++ b/packages/ludic.nav/native/shim/nav_query.inl @@ -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 // -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 -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