diff --git a/graph-ui/src/hooks/useGraphData.test.ts b/graph-ui/src/hooks/useGraphData.test.ts index 7edb76248..ab60201f7 100644 --- a/graph-ui/src/hooks/useGraphData.test.ts +++ b/graph-ui/src/hooks/useGraphData.test.ts @@ -15,7 +15,7 @@ describe("fetchLayout", () => { await fetchLayout("large-project"); - expect(GRAPH_RENDER_NODE_LIMIT).toBe(2000); + expect(GRAPH_RENDER_NODE_LIMIT).toBe(60000); expect(fetchMock).toHaveBeenCalledTimes(1); const calls = fetchMock.mock.calls as unknown as Array<[string]>; const [url] = calls[0]; diff --git a/graph-ui/src/hooks/useGraphData.ts b/graph-ui/src/hooks/useGraphData.ts index 5705555fa..b4711d5e0 100644 --- a/graph-ui/src/hooks/useGraphData.ts +++ b/graph-ui/src/hooks/useGraphData.ts @@ -9,7 +9,7 @@ interface UseGraphDataResult { fetchDetail: (project: string, centerNode: string) => void; } -export const GRAPH_RENDER_NODE_LIMIT = 2000; +export const GRAPH_RENDER_NODE_LIMIT = 60000; export async function fetchLayout( project: string, diff --git a/src/ui/layout3d.c b/src/ui/layout3d.c index 8e54f48a7..5e4bf1e9c 100644 --- a/src/ui/layout3d.c +++ b/src/ui/layout3d.c @@ -24,9 +24,11 @@ /* ── Constants ────────────────────────────────────────────────── */ -#define DEFAULT_MAX_NODES 2000 -#define HARD_MAX_NODES 10000 +#define DEFAULT_MAX_NODES 60000 +#define HARD_MAX_NODES 200000 #define BH_THETA 1.2f +#define OCTREE_MAX_DEPTH 26 /* stop subdividing coincident points (OOM guard) */ +#define OCTREE_MIN_HALF 1e-4f /* minimum octree cell half-size */ /* Local optimization: gentle, preserves structure */ #define LOCAL_REPULSION 8.0f @@ -175,7 +177,8 @@ static void child_center(octree_node_t *n, int o, float *cx, float *cy, float *c *cy = n->oy + ((o & 2) ? q : -q); *cz = n->oz + ((o & 4) ? q : -q); } -static void octree_insert(octree_node_t *n, int idx, float x, float y, float z, float mass) { +static void octree_insert(octree_node_t *n, int idx, float x, float y, float z, float mass, + int depth) { if (n->total_mass == 0.0f && n->body_index == -1) { n->body_index = idx; n->body_mass = mass; @@ -185,6 +188,20 @@ static void octree_insert(octree_node_t *n, int idx, float x, float y, float z, n->total_mass = mass; return; } + /* OOM guard: when bodies share (or nearly share) a position, subdivision + * never separates them, so half_size shrinks toward zero and we allocate + * octree cells without bound — the runaway that exhausted memory on large + * graphs. Once we hit the depth/size floor, stop splitting and fold the body + * into this cell as an aggregate (mass-weighted centroid). */ + if (depth >= OCTREE_MAX_DEPTH || n->half_size < OCTREE_MIN_HALF) { + float nm = n->total_mass + mass; + n->cx = (n->cx * n->total_mass + x * mass) / nm; + n->cy = (n->cy * n->total_mass + y * mass) / nm; + n->cz = (n->cz * n->total_mass + z * mass) / nm; + n->total_mass = nm; + n->body_index = -1; + return; + } if (n->body_index >= 0) { int oi = n->body_index; float ox = n->cx, oy = n->cy, oz = n->cz, om = n->body_mass; @@ -196,7 +213,7 @@ static void octree_insert(octree_node_t *n, int idx, float x, float y, float z, n->children[o] = octree_new(a, b, c, n->half_size * 0.5f); } if (n->children[o]) - octree_insert(n->children[o], oi, ox, oy, oz, om); + octree_insert(n->children[o], oi, ox, oy, oz, om, depth + 1); } float nm = n->total_mass + mass; n->cx = (n->cx * n->total_mass + x * mass) / nm; @@ -210,7 +227,7 @@ static void octree_insert(octree_node_t *n, int idx, float x, float y, float z, n->children[o] = octree_new(a, b, c, n->half_size * 0.5f); } if (n->children[o]) - octree_insert(n->children[o], idx, x, y, z, mass); + octree_insert(n->children[o], idx, x, y, z, mass, depth + 1); } static void octree_repulse(octree_node_t *n, float px, float py, float pz, float mm, int si, float kr, float *fx, float *fy, float *fz) { @@ -274,7 +291,7 @@ static void local_optimize(body_t *b, int n, const int *es, const int *ed, int n if (!root) break; for (int i = 0; i < n; i++) - octree_insert(root, i, b[i].x, b[i].y, b[i].z, b[i].mass); + octree_insert(root, i, b[i].x, b[i].y, b[i].z, b[i].mass, 0); for (int i = 0; i < n; i++) octree_repulse(root, b[i].x, b[i].y, b[i].z, b[i].mass, i, LOCAL_REPULSION, &b[i].fx, &b[i].fy, &b[i].fz);