Operator, 2026-09-26: our graph has 100k+ nodes and 300k+ edges. Clio's drafts show the direction: far away merge nodes and bundle edges, near by resolve them, in our own renderer on OffscreenCanvas + Neo with no external library.
D#19151 H3-a, adopted as the renderer shape (#10034's promotion record, issuecomment-5844273421): foveated LOD.
far: cluster centroids + inter-cluster bundles;
mid: every node + bundles;
near: the nearest clusters' own edges, drawn as per-cluster index ranges.
The measured lap (probe on 100k nodes / 1M edges / 64 clusters, canvas-worker arm): full draw 28.3 fps; far 117.6, mid 119.9, near 115.9 at 120 Hz. "Edges are the budget and LOD returns it, without ever rebuilding a buffer." The probe is archived outside the repo; the tiers and numbers are in D#19151.
The promotion record names this "the engine leaf (filed by Ada)".#19261 extracted Neo.canvas.GraphScene without the LOD, so this is the owed remainder.
The Problem
GraphScene draws every edge at every camera distance. At 100k nodes and 1M edges the frame is fill-bound, the probe's 26–28 fps for the full draw, and a zoomed-out dense graph is unreadable anyway. A scene cannot say which nodes belong together, so there is nothing to aggregate and no per-cluster edge range to draw.
The CPU path is not the constraint. Measured in Node at dev 108b6ccb1c:
scene
setScene
pick per move
structuredClone, typed / plain
100k / 300k
5.0 ms
1.5 ms
0.47 / 14 ms
100k / 1M
10.9 ms
1.3 ms
0.66 / 28 ms
The Architectural Reality
src/canvas/GraphScene.mjs: normalizeScene settles positions / colors / sizes / edges / paths into typed arrays. upload builds one node VAO, one edge index buffer, and the path buffers with their beads. render draws edges (LINES), paths (LINE_STRIP), beads and nodes (POINTS), all of them every frame. fit frames the bounding sphere, and camera.dist against the fitted distance is the natural LOD input.
Scenes carry no cluster ids. D#19151 H3-e ("zoom into stable-id clusters") places cluster identity in the scene, so the producer or the client layout assigns it and the renderer computes no communities.
normalizeScene is the subclass seam (reachable since #19290), and a subclass that derives clusters itself can fill the field there.
The Fix
Scene input: optional clusters, a Uint32Array with one cluster index per node. Without it, GraphScene draws as today.
Derived once per setScene:
cluster centroids (mean position), sizes (by member count) and colours (mean member colour);
the edges re-ordered so each cluster's intra-cluster edges form one contiguous index range, [start, count] per cluster;
inter-cluster bundles: one line between the centroids of each connected cluster pair.
The level follows camera.dist against the fitted distance, with configurable thresholds:
far: centroids + bundles;
mid: every node + bundles;
near: every node + bundles + the intra-cluster ranges of the nearClusters clusters nearest the camera target.
A level change changes draw ranges, never buffers.
Paths (the Golden Path ribbon) draw at every level.
getStats reports lod: {level, edgesDrawn}. pick answers -1 at far, where no node is drawn.
A scale harness in examples/component/graphScene: a clustered synthetic scene (100k nodes, 1M edges, 64 clusters) that measures fps per level in the canvas worker.
Contract Ledger
Surface
Authority
Behaviour
Evidence
setScene without clusters
the current contract
unchanged
existing arms
setScene({…, clusters})
D#19151 H3-e (stable-id clusters)
derives centroids, per-cluster ranges, bundles; refuses a clusters array whose length is not the node count
new arms, red-first
LOD level
D#19151 H3-a
far / mid / near by camera distance; draw ranges only
new arms + harness fps
getStats().lod
this ticket
{level, edgesDrawn}
new arm
pick at far
this ticket
-1: no node is drawn there
new arm
Acceptance Criteria
AC-1: a scene without clusters draws as today (the existing GraphScene arms stay green).
AC-2: with clusters, setScene derives the centroids, a contiguous intra-cluster range per cluster, and one bundle per connected cluster pair. A clusters array of the wrong length is refused, naming both lengths. Red on dev first.
AC-3: the level follows the camera distance against the fitted distance. A level change uploads nothing.
AC-4: near draws the intra-cluster ranges of the nearClusters clusters nearest the camera target, and no others.
AC-5: getStats reports lod: {level, edgesDrawn}, and pick answers -1 at far.
AC-6: the example's scale harness (100k / 1M / 64 clusters) records fps for the full draw and for far, mid and near in the canvas worker, on a visible, unthrottled page: headless branded Chrome on hardware GL, in both GPU-intent profiles. The PR carries the numbers.
Post-Merge Validation
On the operator's display, in a visible window: "100k clustered", "LOD lap", then "Read stats" after about 20 s. Every level holds the display's frame rate. AC-6 ran headless because the agent's Browser pane was hidden (visibilityState: hidden, rAF at 1/s).
Out of Scope
Computing clusters or communities; that is the producer's or the layout's job.
A layout for 100k nodes (OQ5 is deferred; client-side over bounded neighbourhoods).
Selecting or expanding a cluster (the H3-e Institution leaf).
Curved or edge-bundled routing: bundles are straight centroid lines.
Avoided Traps
An external graph library (Cosmograph, three.js). D#19151 H3-b is rejected, and the operator's position is "our own".
Rebuilding buffers on a level change. The probe holds 116–120 fps precisely because a level only changes ranges.
Clustering in the renderer. It would compete with H3-e's stable ids, and a selection could not survive a refresh.
tobiu referenced in commit 6c0681d - "feat(canvas): GraphScene draws clustered scenes through a foveated LOD (#19295) (#19302) on Sep 27, 2026, 10:45 AM
Context
#10034's promotion record, issuecomment-5844273421): foveated LOD.#19261extractedNeo.canvas.GraphScenewithout the LOD, so this is the owed remainder.The Problem
GraphScene draws every edge at every camera distance. At 100k nodes and 1M edges the frame is fill-bound, the probe's 26–28 fps for the full draw, and a zoomed-out dense graph is unreadable anyway. A scene cannot say which nodes belong together, so there is nothing to aggregate and no per-cluster edge range to draw.
The CPU path is not the constraint. Measured in Node at dev
108b6ccb1c:setScenestructuredClone, typed / plainThe Architectural Reality
src/canvas/GraphScene.mjs:normalizeScenesettlespositions/colors/sizes/edges/pathsinto typed arrays.uploadbuilds one node VAO, one edge index buffer, and the path buffers with their beads.renderdraws edges (LINES), paths (LINE_STRIP), beads and nodes (POINTS), all of them every frame.fitframes the bounding sphere, andcamera.distagainst the fitted distance is the natural LOD input.normalizeSceneis the subclass seam (reachable since#19290), and a subclass that derives clusters itself can fill the field there.The Fix
clusters, aUint32Arraywith one cluster index per node. Without it, GraphScene draws as today.setScene:[start, count]per cluster;camera.distagainst the fitted distance, with configurable thresholds:nearClustersclusters nearest the camera target. A level change changes draw ranges, never buffers.getStatsreportslod: {level, edgesDrawn}.pickanswers-1at far, where no node is drawn.examples/component/graphScene: a clustered synthetic scene (100k nodes, 1M edges, 64 clusters) that measures fps per level in the canvas worker.Contract Ledger
setScenewithoutclusterssetScene({…, clusters})clustersarray whose length is not the node countgetStats().lod{level, edgesDrawn}pickat far-1: no node is drawn thereAcceptance Criteria
clustersdraws as today (the existing GraphScene arms stay green).clusters,setScenederives the centroids, a contiguous intra-cluster range per cluster, and one bundle per connected cluster pair. Aclustersarray of the wrong length is refused, naming both lengths. Red on dev first.nearClustersclusters nearest the camera target, and no others.getStatsreportslod: {level, edgesDrawn}, andpickanswers-1at far.Post-Merge Validation
visibilityState: hidden, rAF at 1/s).Out of Scope
Avoided Traps
Related
#10034(epic, promotion record row H3-a), D#19151,#19261/#19274(GraphScene),#19290(the reachable seam), Institution#252/#256, Institution#234, Brain#533.Decision Record impact: aligned-with D#19151 (H3-a, H3-e;
Decision Record: Optional).Discussion Criteria Mapping:
Epic review: cited, not re-run. Grace's review (same model identity), https://github.com/neomjs/neo/issues/10034#issuecomment-5844343545.
Sweeps:
Origin Session ID: e6630386-876d-490e-a3c8-d22c02c3ad21 Retrieval Hint: "GraphScene foveated LOD cluster centroids bundles per-cluster index ranges 100k nodes"