From 950d3134f24f78047ec280df4dfb18c9910c8cc3 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 21:23:56 +0300 Subject: [PATCH] fix(nav): nothing allocated per build - the configuration and the empty lists live in NavState, because Ludic never gives an allocation back and a map is thousands of tile builds; nav_drop lets one kind's mesh go; the detail mesh's spacing is a NavConfig setting held to Recast's own floor of 0.9 m (0 took a 40 m test meadow to 3.5 GB) Co-Authored-By: Claude Opus 5.5 --- packages/ludic.nav/build.ludic | 20 +++++++++++-------- .../lib/macos-arm64/libludicnav.dylib | 2 +- packages/ludic.nav/native/shim/nav_build.inl | 5 +++-- packages/ludic.nav/state.ludic | 6 ++++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/ludic.nav/build.ludic b/packages/ludic.nav/build.ludic index a8365d5e..15fc5233 100644 --- a/packages/ludic.nav/build.ludic +++ b/packages/ludic.nav/build.ludic @@ -5,25 +5,28 @@ function nv_ground_ok(g: NavGround) -> bool { if g.nc > 0 and (g.cyl == null or len(g.cyl) < g.nc * 5) { return false } return g.nf == 0 or (g.foot != null and len(g.foot) >= g.nf) } -function nv_cfg(c: NavConfig) -> []float { - let cfg = floats(6) +# the configuration in the state's own buffer: a map is thousands of tile builds, and nothing +# allocated per build is ever given back +function nv_cfg(nav_st: mut NavState, c: NavConfig) -> []float { + let cfg = nav_st.nv_cfg cfg[0] = c.cell cfg[1] = c.cell_h cfg[2] = c.height cfg[3] = c.radius cfg[4] = c.climb cfg[5] = c.slope + cfg[6] = Math.max(c.detail, 0.9) return cfg } -function nv_or_none(xs: []float) -> []float { - if xs == null { return floats(1) } +function nv_or_none(nav_st: NavState, xs: []float, n: int) -> []float { + if xs == null or n == 0 { return nav_st.nv_empty } return xs } # one mesh over the whole of g export function nav_build(nav_st: mut NavState, kind: int, g: NavGround, c: NavConfig) -> bool { if not nv_ground_ok(g) { return false } - return nv_set(nav_st, kind, nvc_build(g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c))) + return nv_set(nav_st, kind, nvc_build(g.v, g.nv, g.t, g.nt, g.area, nv_or_none(nav_st, g.cyl, g.nc), g.nc, nv_or_none(nav_st, g.foot, g.nf), g.nf, nv_cfg(nav_st, c))) } # an empty mesh of square tiles `tile` metres across from (ox, oz), for a map built a tile at a time @@ -31,10 +34,10 @@ export function nav_tiled(nav_st: mut NavState, kind: int, ox: float, oz: float, return nv_set(nav_st, kind, nvc_tiled(ox, oz, tile, max_tiles, max_polys)) } # tile (tx, tz) from ground g covering it and a few metres round it: its polygons, 0 none, -1 failed -export function nav_tile_build(nav_st: NavState, kind: int, tx: int, tz: int, g: NavGround, c: NavConfig) -> int { +export function nav_tile_build(nav_st: mut NavState, kind: int, tx: int, tz: int, g: NavGround, c: NavConfig) -> int { let h = nv_mesh(nav_st, kind) if h == null or not nv_ground_ok(g) { return -1 } - return nvc_tile_build(h, tx, tz, g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c)) + return nvc_tile_build(h, tx, tz, g.v, g.nv, g.t, g.nt, g.area, nv_or_none(nav_st, g.cyl, g.nc), g.nc, nv_or_none(nav_st, g.foot, g.nf), g.nf, nv_cfg(nav_st, c)) } # the polygons a kind's mesh has (0: none) @@ -44,7 +47,8 @@ export function nav_polygons(nav_st: NavState, kind: int) -> int { return nvc_polys(h) } -# a kind's mesh written to a file, and read back +# a kind's mesh written to a file, and read back. Reading holds the file's bytes for good (a slice +# is never given back), so a mesh is loaded once per map, never per frame export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool { let h = nv_mesh(nav_st, kind) if h == null { return false } diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib index 2ad8f0a1..f88c1f04 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:24f64f1f9c6c4d4f4d97ecb92ea9b55cbfb6d0646a053c5e8210e76e7857aab2 +oid sha256:43832102310c1fa30e7efbca67b2665c018997d12efe784b71ebe98f171fde64 size 176736 diff --git a/packages/ludic.nav/native/shim/nav_build.inl b/packages/ludic.nav/native/shim/nav_build.inl index 85ad4b8b..39f22cfa 100644 --- a/packages/ludic.nav/native/shim/nav_build.inl +++ b/packages/ludic.nav/native/shim/nav_build.inl @@ -1,6 +1,7 @@ // nav_build.inl - Recast's pipeline, shared by a whole mesh and a tile: rasterize the triangles, // filter, mark what nothing stands in, erode, regions, contours, polygons, detail, Detour's data. -// c[] is the configuration: cell size, cell height, agent height, radius, max climb, max slope. +// c[] is the configuration: cell size, cell height, agent height, radius, max climb, max slope, and +// the detail mesh's sample spacing (at least 0.9 m, Recast's own limit; 0 grew without bound) // cylinders (x, y, z, r, h each) no agent may stand in: trunks, posts, tents static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const float *cyl, int nc) { @@ -37,7 +38,7 @@ static void nav_config(rcConfig &cfg, const float *c) { cfg.minRegionArea = 8 * 8; cfg.mergeRegionArea = 20 * 20; cfg.maxVertsPerPoly = 6; - cfg.detailSampleDist = cfg.cs * 6.0f; + cfg.detailSampleDist = c[6] > 0.9f ? c[6] : 0.9f; cfg.detailSampleMaxError = cfg.ch; } diff --git a/packages/ludic.nav/state.ludic b/packages/ludic.nav/state.ludic index 302c0687..484bdd87 100644 --- a/packages/ludic.nav/state.ludic +++ b/packages/ludic.nav/state.ludic @@ -16,6 +16,7 @@ export property NavConfig { radius: float = 0.35 climb: float = 0.55 slope: float = 45.0 + detail: float = 1.5 # the detail mesh's samples, metres apart (at least 0.9: 0 took one small test to 3.5 GB) } # what a mesh is built from: v holds nv points (x, y, z) and t nt triangles (three indices each); @@ -38,6 +39,8 @@ export state NavState { nv_out: []float = floats(NAV_CORNERS * 3) nv_count: int = 0 nv_near: []float = floats(3) + nv_cfg: []float = floats(7) # the configuration handed to the shim, filled in place per build + nv_empty: []float = floats(1) # what the shim is handed for no cylinders or no footprints } function nv_none() -> []pointer { let m = new []pointer @@ -55,6 +58,9 @@ function nv_set(nav_st: mut NavState, kind: int, h: pointer) -> bool { return h != null } +# one kind's mesh let go (a bake writes each kind and drops it before the next) +export function nav_drop(nav_st: mut NavState, kind: int) -> void { nv_set(nav_st, kind, null) } + # every mesh let go: a new map, or the world closed export function nav_reset(nav_st: mut NavState) -> void { for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }