d3-force-webgpu
Version:
GPU-accelerated force-directed graph layout with adaptive CPU/GPU selection. Drop-in replacement for d3-force with WebGPU support.
1,921 lines (1,567 loc) • 52 kB
JavaScript
// https://github.com/jamescarruthers/d3-force-webgpu v1.0.2 Copyright 2010-2021 Mike Bostock
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-dispatch'), require('d3-timer'), require('d3-quadtree')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-dispatch', 'd3-timer', 'd3-quadtree'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3));
}(this, (function (exports, d3Dispatch, d3Timer, d3Quadtree) { 'use strict';
// https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
const a = 1664525;
const c = 1013904223;
const m = 4294967296; // 2^32
function lcg() {
let s = 1;
return () => (s = (a * s + c) % m) / m;
}
function x$2(d) {
return d.x;
}
function y$2(d) {
return d.y;
}
var initialRadius$1 = 10,
initialAngle$1 = Math.PI * (3 - Math.sqrt(5));
function cpuSimulation(nodes) {
var simulation,
alpha = 1,
alphaMin = 0.001,
alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
alphaTarget = 0,
velocityDecay = 0.6,
forces = new Map(),
stepper = d3Timer.timer(step),
event = d3Dispatch.dispatch("tick", "end"),
random = lcg();
if (nodes == null) nodes = [];
function step() {
tick();
event.call("tick", simulation);
if (alpha < alphaMin) {
stepper.stop();
event.call("end", simulation);
}
}
function tick(iterations) {
var i, n = nodes.length, node;
if (iterations === undefined) iterations = 1;
for (var k = 0; k < iterations; ++k) {
alpha += (alphaTarget - alpha) * alphaDecay;
forces.forEach(function(force) {
force(alpha);
});
for (i = 0; i < n; ++i) {
node = nodes[i];
if (node.fx == null) node.x += node.vx *= velocityDecay;
else node.x = node.fx, node.vx = 0;
if (node.fy == null) node.y += node.vy *= velocityDecay;
else node.y = node.fy, node.vy = 0;
}
}
return simulation;
}
function initializeNodes() {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.index = i;
if (node.fx != null) node.x = node.fx;
if (node.fy != null) node.y = node.fy;
if (isNaN(node.x) || isNaN(node.y)) {
var radius = initialRadius$1 * Math.sqrt(0.5 + i), angle = i * initialAngle$1;
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
}
if (isNaN(node.vx) || isNaN(node.vy)) {
node.vx = node.vy = 0;
}
}
}
function initializeForce(force) {
if (force.initialize) force.initialize(nodes, random);
return force;
}
initializeNodes();
return simulation = {
tick: tick,
restart: function() {
return stepper.restart(step), simulation;
},
stop: function() {
return stepper.stop(), simulation;
},
nodes: function(_) {
return arguments.length ? (nodes = _, initializeNodes(), forces.forEach(initializeForce), simulation) : nodes;
},
alpha: function(_) {
return arguments.length ? (alpha = +_, simulation) : alpha;
},
alphaMin: function(_) {
return arguments.length ? (alphaMin = +_, simulation) : alphaMin;
},
alphaDecay: function(_) {
return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay;
},
alphaTarget: function(_) {
return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget;
},
velocityDecay: function(_) {
return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
},
randomSource: function(_) {
return arguments.length ? (random = _, forces.forEach(initializeForce), simulation) : random;
},
force: function(name, _) {
return arguments.length > 1 ? ((_ == null ? forces.delete(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
},
find: function(x, y, radius) {
var i = 0,
n = nodes.length,
dx,
dy,
d2,
node,
closest;
if (radius == null) radius = Infinity;
else radius *= radius;
for (i = 0; i < n; ++i) {
node = nodes[i];
dx = x - node.x;
dy = y - node.y;
d2 = dx * dx + dy * dy;
if (d2 < radius) closest = node, radius = d2;
}
return closest;
},
on: function(name, _) {
return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
}
};
}
class WebGPUContext {
constructor() {
this.device = null;
this.adapter = null;
}
async initialize() {
if (!navigator.gpu) {
throw new Error('WebGPU is not supported in this browser');
}
this.adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
});
if (!this.adapter) {
throw new Error('Failed to get WebGPU adapter');
}
this.device = await this.adapter.requestDevice({
requiredFeatures: [],
requiredLimits: {
maxStorageBufferBindingSize: this.adapter.limits.maxStorageBufferBindingSize,
maxComputeWorkgroupStorageSize: this.adapter.limits.maxComputeWorkgroupStorageSize,
maxComputeInvocationsPerWorkgroup: this.adapter.limits.maxComputeInvocationsPerWorkgroup,
}
});
this.device.lost.then((info) => {
console.error('WebGPU device was lost:', info.message);
if (info.reason !== 'destroyed') {
this.initialize();
}
});
return this.device;
}
destroy() {
if (this.device) {
this.device.destroy();
this.device = null;
}
this.adapter = null;
}
}
let sharedContext = null;
async function getWebGPUContext() {
if (!sharedContext) {
sharedContext = new WebGPUContext();
await sharedContext.initialize();
}
return sharedContext;
}
class BufferManager {
constructor(device) {
this.device = device;
this.buffers = new Map();
}
createNodeBuffer(nodes) {
const nodeCount = nodes.length;
const floatsPerNode = 8; // x, y, vx, vy, fx, fy, index, _padding
const byteSize = nodeCount * floatsPerNode * 4;
const nodeData = new Float32Array(nodeCount * floatsPerNode);
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
const offset = i * floatsPerNode;
nodeData[offset + 0] = node.x || 0;
nodeData[offset + 1] = node.y || 0;
nodeData[offset + 2] = node.vx || 0;
nodeData[offset + 3] = node.vy || 0;
nodeData[offset + 4] = node.fx !== null && node.fx !== undefined ? node.fx : NaN;
nodeData[offset + 5] = node.fy !== null && node.fy !== undefined ? node.fy : NaN;
nodeData[offset + 6] = i; // index
nodeData[offset + 7] = 0; // padding for alignment
}
const buffer = this.device.createBuffer({
label: 'Node Buffer',
size: byteSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(buffer.getMappedRange()).set(nodeData);
buffer.unmap();
this.buffers.set('nodes', {
buffer,
count: nodeCount,
floatsPerNode
});
return buffer;
}
createSimulationParamsBuffer(params) {
const paramData = new Float32Array([
params.alpha || 1,
params.alphaDecay || 0.0228,
params.alphaTarget || 0,
params.velocityDecay || 0.6,
params.nodeCount || 0,
0, // padding
0, // padding
0 // padding
]);
const buffer = this.device.createBuffer({
label: 'Simulation Parameters',
size: paramData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(buffer.getMappedRange()).set(paramData);
buffer.unmap();
this.buffers.set('simulationParams', { buffer });
return buffer;
}
createReadbackBuffer(size) {
const buffer = this.device.createBuffer({
label: 'Readback Buffer',
size,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});
this.buffers.set('readback', { buffer, size });
return buffer;
}
async readNodeData(nodes) {
const nodeBuffer = this.buffers.get('nodes');
if (!nodeBuffer) return;
const readbackBuffer = this.buffers.get('readback');
if (!readbackBuffer || readbackBuffer.size < nodeBuffer.buffer.size) {
this.createReadbackBuffer(nodeBuffer.buffer.size);
}
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(
nodeBuffer.buffer, 0,
this.buffers.get('readback').buffer, 0,
nodeBuffer.buffer.size
);
this.device.queue.submit([commandEncoder.finish()]);
await this.buffers.get('readback').buffer.mapAsync(GPUMapMode.READ);
const data = new Float32Array(this.buffers.get('readback').buffer.getMappedRange());
for (let i = 0; i < nodeBuffer.count; i++) {
const offset = i * nodeBuffer.floatsPerNode;
const node = nodes[i];
node.x = data[offset + 0];
node.y = data[offset + 1];
node.vx = data[offset + 2];
node.vy = data[offset + 3];
}
this.buffers.get('readback').buffer.unmap();
}
updateSimulationParams(params) {
const paramData = new Float32Array([
params.alpha,
params.alphaDecay,
params.alphaTarget,
params.velocityDecay,
params.nodeCount,
0, 0, 0
]);
this.device.queue.writeBuffer(
this.buffers.get('simulationParams').buffer,
0,
paramData
);
}
destroy() {
for (const [, bufferInfo] of this.buffers) {
bufferInfo.buffer.destroy();
}
this.buffers.clear();
}
}
class ShaderLoader {
constructor(device) {
this.device = device;
this.shaderModules = new Map();
this.pipelines = new Map();
this.bindGroupLayouts = new Map();
}
createSimulationTickPipeline() {
const shaderCode = `
struct Node {
position: vec2<f32>,
velocity: vec2<f32>,
fixedPosition: vec2<f32>,
index: f32,
_padding: f32,
}
struct SimulationParams {
alpha: f32,
alphaDecay: f32,
alphaTarget: f32,
velocityDecay: f32,
nodeCount: f32,
_padding: vec3<f32>,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: SimulationParams;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let idx = global_id.x;
let nodeCount = u32(params.nodeCount);
if (idx >= nodeCount) {
return;
}
var node = nodes[idx];
// Check if position is fixed
let isFixedX = !isnan(node.fixedPosition.x);
let isFixedY = !isnan(node.fixedPosition.y);
// Update positions based on velocity
if (!isFixedX) {
node.position.x += node.velocity.x;
node.velocity.x *= params.velocityDecay;
} else {
node.position.x = node.fixedPosition.x;
node.velocity.x = 0.0;
}
if (!isFixedY) {
node.position.y += node.velocity.y;
node.velocity.y *= params.velocityDecay;
} else {
node.position.y = node.fixedPosition.y;
node.velocity.y = 0.0;
}
nodes[idx] = node;
}`;
const shaderModule = this.device.createShaderModule({
label: 'Simulation Tick Shader',
code: shaderCode
});
const bindGroupLayout = this.device.createBindGroupLayout({
label: 'Simulation Tick Bind Group Layout',
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: 'storage'
}
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: 'uniform'
}
}
]
});
const pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout]
});
const pipeline = this.device.createComputePipeline({
label: 'Simulation Tick Pipeline',
layout: pipelineLayout,
compute: {
module: shaderModule,
entryPoint: 'main'
}
});
this.pipelines.set('simulationTick', pipeline);
this.bindGroupLayouts.set('simulationTick', bindGroupLayout);
return { pipeline, bindGroupLayout };
}
createBindGroup(name, layout, buffers) {
const entries = buffers.map((buffer, index) => ({
binding: index,
resource: { buffer }
}));
return this.device.createBindGroup({
label: `${name} Bind Group`,
layout,
entries
});
}
getPipeline(name) {
return this.pipelines.get(name);
}
getBindGroupLayout(name) {
return this.bindGroupLayouts.get(name);
}
destroy() {
this.shaderModules.clear();
this.pipelines.clear();
this.bindGroupLayouts.clear();
}
}
var initialRadius = 10,
initialAngle = Math.PI * (3 - Math.sqrt(5));
function forceSimulationGPU(nodes) {
var simulation,
alpha = 1,
alphaMin = 0.001,
alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
alphaTarget = 0,
velocityDecay = 0.6,
forces = new Map(),
stepper = d3Timer.timer(step),
event = d3Dispatch.dispatch("tick", "end"),
random = lcg(),
device = null,
bufferManager = null,
shaderLoader = null,
simulationPipeline = null,
bindGroup = null,
isGPUInitialized = false;
if (nodes == null) nodes = [];
async function initializeGPU() {
if (isGPUInitialized) return;
try {
const context = await getWebGPUContext();
device = context.device;
bufferManager = new BufferManager(device);
shaderLoader = new ShaderLoader(device);
const {pipeline, bindGroupLayout} = shaderLoader.createSimulationTickPipeline();
simulationPipeline = pipeline;
const nodeBuffer = bufferManager.createNodeBuffer(nodes);
const paramsBuffer = bufferManager.createSimulationParamsBuffer({
alpha,
alphaDecay,
alphaTarget,
velocityDecay,
nodeCount: nodes.length
});
bindGroup = shaderLoader.createBindGroup('simulationTick', bindGroupLayout, [
nodeBuffer,
paramsBuffer
]);
isGPUInitialized = true;
} catch (error) {
console.error('Failed to initialize WebGPU:', error);
throw error;
}
}
async function step() {
await tick();
event.call("tick", simulation);
if (alpha < alphaMin) {
stepper.stop();
event.call("end", simulation);
}
}
async function tick(iterations) {
if (iterations === undefined) iterations = 1;
if (!isGPUInitialized) {
await initializeGPU();
}
for (var k = 0; k < iterations; ++k) {
alpha += (alphaTarget - alpha) * alphaDecay;
bufferManager.updateSimulationParams({
alpha,
alphaDecay,
alphaTarget,
velocityDecay,
nodeCount: nodes.length
});
// Apply forces first (they should update velocities on GPU)
for (const force of forces.values()) {
if (force && typeof force === 'function') {
await force(alpha);
}
}
// Then apply velocity integration
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(simulationPipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(Math.ceil(nodes.length / 64));
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
// Wait for GPU to finish
await device.queue.onSubmittedWorkDone();
// Read back the updated node positions
await bufferManager.readNodeData(nodes);
}
return simulation;
}
function initializeNodes() {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.index = i;
if (node.fx != null) node.x = node.fx;
if (node.fy != null) node.y = node.fy;
if (isNaN(node.x) || isNaN(node.y)) {
var radius = initialRadius * Math.sqrt(0.5 + i), angle = i * initialAngle;
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
}
if (isNaN(node.vx) || isNaN(node.vy)) {
node.vx = node.vy = 0;
}
}
}
function initializeForce(force) {
if (force.initialize) {
const nodeBuffer = bufferManager ? bufferManager.buffers.get('nodes')?.buffer : null;
force.initialize(nodes, random, device, nodeBuffer);
}
return force;
}
initializeNodes();
return simulation = {
tick: tick,
restart: function() {
return stepper.restart(step), simulation;
},
stop: function() {
return stepper.stop(), simulation;
},
nodes: function(_) {
if (!arguments.length) return nodes;
nodes = _;
initializeNodes();
forces.forEach(initializeForce);
if (isGPUInitialized) {
bufferManager.destroy();
isGPUInitialized = false;
}
return simulation;
},
alpha: function(_) {
return arguments.length ? (alpha = +_, simulation) : alpha;
},
alphaMin: function(_) {
return arguments.length ? (alphaMin = +_, simulation) : alphaMin;
},
alphaDecay: function(_) {
return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay;
},
alphaTarget: function(_) {
return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget;
},
velocityDecay: function(_) {
return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
},
randomSource: function(_) {
return arguments.length ? (random = _, forces.forEach(initializeForce), simulation) : random;
},
force: function(name, _) {
return arguments.length > 1 ? ((_ == null ? forces.delete(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
},
find: function(x, y, radius) {
var i = 0,
n = nodes.length,
dx,
dy,
d2,
node,
closest;
if (radius == null) radius = Infinity;
else radius *= radius;
for (i = 0; i < n; ++i) {
node = nodes[i];
dx = x - node.x;
dy = y - node.y;
d2 = dx * dx + dy * dy;
if (d2 < radius) closest = node, radius = d2;
}
return closest;
},
on: function(name, _) {
return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
},
destroy: function() {
stepper.stop();
if (bufferManager) bufferManager.destroy();
if (shaderLoader) shaderLoader.destroy();
isGPUInitialized = false;
}
};
}
// Performance thresholds based on typical benchmarks
const PERFORMANCE_THRESHOLDS = {
// Below this node count, CPU is usually faster due to GPU overhead
MIN_GPU_NODES: 750,
// If CPU is significantly slower than this FPS, consider GPU
MIN_CPU_FPS: 30,
// If link count exceeds this, GPU becomes more beneficial
MIN_GPU_LINKS: 1000,
// Complexity score threshold (nodes * links)
COMPLEXITY_THRESHOLD: 500000
};
// Auto-detection logic
function shouldUseGPU(nodes, estimatedLinkCount = nodes.length * 2) {
const nodeCount = nodes.length;
const complexity = nodeCount * estimatedLinkCount;
// WebGPU not available
if (!navigator.gpu) {
return false;
}
// Too few nodes - GPU overhead not worth it
if (nodeCount < PERFORMANCE_THRESHOLDS.MIN_GPU_NODES) {
return false;
}
// High complexity - GPU likely beneficial
if (complexity > PERFORMANCE_THRESHOLDS.COMPLEXITY_THRESHOLD) {
return true;
}
// Medium complexity - GPU might be beneficial
if (nodeCount >= PERFORMANCE_THRESHOLDS.MIN_GPU_NODES &&
estimatedLinkCount >= PERFORMANCE_THRESHOLDS.MIN_GPU_LINKS) {
return true;
}
return false;
}
// Performance monitor to switch implementations dynamically
class PerformanceMonitor {
constructor() {
this.fpsHistory = [];
this.frameTimeHistory = [];
this.lastTime = performance.now();
this.frameCount = 0;
this.avgFPS = 0;
this.avgFrameTime = 0;
}
recordFrame(frameTime) {
this.frameTimeHistory.push(frameTime);
this.frameCount++;
const now = performance.now();
if (now - this.lastTime >= 1000) {
const fps = Math.round(this.frameCount * 1000 / (now - this.lastTime));
this.fpsHistory.push(fps);
this.frameCount = 0;
this.lastTime = now;
// Calculate rolling averages
const recentFPS = this.fpsHistory.slice(-5); // Last 5 seconds
const recentFrameTimes = this.frameTimeHistory.slice(-300); // Last ~5 seconds at 60fps
this.avgFPS = recentFPS.reduce((a, b) => a + b, 0) / recentFPS.length;
this.avgFrameTime = recentFrameTimes.reduce((a, b) => a + b, 0) / recentFrameTimes.length;
}
}
shouldSwitchToGPU() {
return this.avgFPS < PERFORMANCE_THRESHOLDS.MIN_CPU_FPS && this.fpsHistory.length >= 3;
}
reset() {
this.fpsHistory = [];
this.frameTimeHistory = [];
this.frameCount = 0;
this.lastTime = performance.now();
}
}
// Adaptive force simulation that chooses the best implementation
async function forceSimulationAdaptive(nodes, options = {}) {
const {
mode = 'auto', // 'auto', 'cpu', 'gpu', 'hybrid'
enableSwitching = true, // Allow dynamic switching based on performance
onModeChange = null, // Callback when mode changes
estimatedLinkCount = nodes?.length * 2
} = options;
let currentMode = mode;
let currentSimulation = null;
let monitor = new PerformanceMonitor();
let switchCheckInterval = null;
// Determine initial mode
if (mode === 'auto') {
currentMode = shouldUseGPU(nodes, estimatedLinkCount) ? 'gpu' : 'cpu';
}
async function createSimulation(nodes, useGPU = false) {
if (useGPU) {
try {
const sim = forceSimulationGPU(nodes);
await sim.initialize?.();
return { simulation: sim, type: 'gpu', initialized: true };
} catch (error) {
console.warn('GPU simulation failed, falling back to CPU:', error.message);
return { simulation: cpuSimulation(nodes), type: 'cpu', initialized: true };
}
} else {
return { simulation: cpuSimulation(nodes), type: 'cpu', initialized: true };
}
}
async function switchToGPU() {
if (currentMode === 'gpu' || !enableSwitching) return;
console.log('🚀 Switching to GPU acceleration for better performance');
const oldSim = currentSimulation.simulation;
const forces = new Map();
// Preserve force configuration
if (oldSim.forces) {
oldSim.forces.forEach((force, name) => {
forces.set(name, force);
});
}
// Stop old simulation
oldSim.stop();
// Create new GPU simulation
const newSim = await createSimulation(nodes, true);
// Transfer forces
forces.forEach((force, name) => {
newSim.simulation.force(name, force);
});
// Update references
currentSimulation = newSim;
currentMode = 'gpu';
monitor.reset();
if (onModeChange) {
onModeChange('gpu', 'Switched to GPU for better performance');
}
return newSim.simulation;
}
// Create initial simulation
const initialSim = await createSimulation(nodes, currentMode === 'gpu');
currentSimulation = initialSim;
// Wrap simulation to add performance monitoring
const originalTick = currentSimulation.simulation.tick;
currentSimulation.simulation.tick = function(...args) {
const startTime = performance.now();
const result = originalTick.apply(this, args);
const frameTime = performance.now() - startTime;
monitor.recordFrame(frameTime);
return result;
};
// Set up performance monitoring for dynamic switching
if (enableSwitching && currentMode === 'cpu') {
switchCheckInterval = setInterval(async () => {
if (monitor.shouldSwitchToGPU()) {
clearInterval(switchCheckInterval);
await switchToGPU();
}
}, 2000); // Check every 2 seconds
}
// Extend simulation with adaptive methods
const adaptiveSimulation = currentSimulation.simulation;
// Add adaptive-specific methods
adaptiveSimulation.getCurrentMode = () => currentMode;
adaptiveSimulation.getPerformanceStats = () => ({
avgFPS: monitor.avgFPS,
avgFrameTime: monitor.avgFrameTime,
mode: currentMode
});
adaptiveSimulation.forceMode = async (newMode) => {
if (newMode === currentMode) return adaptiveSimulation;
const oldSim = currentSimulation.simulation;
const forces = new Map();
// Preserve configuration
if (oldSim.forces) {
oldSim.forces.forEach((force, name) => {
forces.set(name, force);
});
}
oldSim.stop();
// Create new simulation
const newSim = await createSimulation(nodes, newMode === 'gpu');
// Transfer forces
forces.forEach((force, name) => {
newSim.simulation.force(name, force);
});
currentSimulation = newSim;
currentMode = newMode;
monitor.reset();
if (onModeChange) {
onModeChange(newMode, `Manually switched to ${newMode.toUpperCase()}`);
}
return newSim.simulation;
};
// Override stop to clean up monitoring
const originalStop = adaptiveSimulation.stop;
adaptiveSimulation.stop = function() {
if (switchCheckInterval) {
clearInterval(switchCheckInterval);
switchCheckInterval = null;
}
return originalStop.call(this);
};
// Add mode information to simulation
adaptiveSimulation._adaptiveMode = currentMode;
adaptiveSimulation._isAdaptive = true;
if (onModeChange) {
onModeChange(currentMode, `Started with ${currentMode.toUpperCase()} mode`);
}
return adaptiveSimulation;
}
// Export configuration utilities
const AdaptiveConfig = {
setThresholds: (newThresholds) => {
Object.assign(PERFORMANCE_THRESHOLDS, newThresholds);
},
getThresholds: () => ({ ...PERFORMANCE_THRESHOLDS }),
shouldUseGPU,
estimateComplexity: (nodeCount, linkCount) => nodeCount * linkCount,
recommendMode: (nodes, links = []) => {
const complexity = nodes.length * links.length;
if (!navigator.gpu) return 'cpu';
if (nodes.length < 200) return 'cpu';
if (complexity > 1000000) return 'gpu';
if (nodes.length > 2000) return 'gpu';
return 'auto';
}
};
// Convenience exports for specific modes
const forceCPU = cpuSimulation;
const forceGPU = forceSimulationGPU;
const forceAuto = forceSimulationAdaptive;
function center(x, y) {
var nodes, strength = 1;
if (x == null) x = 0;
if (y == null) y = 0;
function force() {
var i,
n = nodes.length,
node,
sx = 0,
sy = 0;
for (i = 0; i < n; ++i) {
node = nodes[i], sx += node.x, sy += node.y;
}
for (sx = (sx / n - x) * strength, sy = (sy / n - y) * strength, i = 0; i < n; ++i) {
node = nodes[i], node.x -= sx, node.y -= sy;
}
}
force.initialize = function(_) {
nodes = _;
};
force.x = function(_) {
return arguments.length ? (x = +_, force) : x;
};
force.y = function(_) {
return arguments.length ? (y = +_, force) : y;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
return force;
}
function constant(x) {
return function() {
return x;
};
}
function jiggle(random) {
return (random() - 0.5) * 1e-6;
}
function x$1(d) {
return d.x + d.vx;
}
function y$1(d) {
return d.y + d.vy;
}
function collide(radius) {
var nodes,
radii,
random,
strength = 1,
iterations = 1;
if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);
function force() {
var i, n = nodes.length,
tree,
node,
xi,
yi,
ri,
ri2;
for (var k = 0; k < iterations; ++k) {
tree = d3Quadtree.quadtree(nodes, x$1, y$1).visitAfter(prepare);
for (i = 0; i < n; ++i) {
node = nodes[i];
ri = radii[node.index], ri2 = ri * ri;
xi = node.x + node.vx;
yi = node.y + node.vy;
tree.visit(apply);
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data, rj = quad.r, r = ri + rj;
if (data) {
if (data.index > node.index) {
var x = xi - data.x - data.vx,
y = yi - data.y - data.vy,
l = x * x + y * y;
if (l < r * r) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
l = (r - (l = Math.sqrt(l))) / l * strength;
node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
node.vy += (y *= l) * r;
data.vx -= x * (r = 1 - r);
data.vy -= y * r;
}
}
return;
}
return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
}
}
function prepare(quad) {
if (quad.data) return quad.r = radii[quad.data.index];
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r;
}
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
radii = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};
return force;
}
function index(d) {
return d.index;
}
function find(nodeById, nodeId) {
var node = nodeById.get(nodeId);
if (!node) throw new Error("node not found: " + nodeId);
return node;
}
function link(links) {
var id = index,
strength = defaultStrength,
strengths,
distance = constant(30),
distances,
nodes,
count,
bias,
random,
iterations = 1;
if (links == null) links = [];
function defaultStrength(link) {
return 1 / Math.min(count[link.source.index], count[link.target.index]);
}
function force(alpha) {
for (var k = 0, n = links.length; k < iterations; ++k) {
for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
link = links[i], source = link.source, target = link.target;
x = target.x + target.vx - source.x - source.vx || jiggle(random);
y = target.y + target.vy - source.y - source.vy || jiggle(random);
l = Math.sqrt(x * x + y * y);
l = (l - distances[i]) / l * alpha * strengths[i];
x *= l, y *= l;
target.vx -= x * (b = bias[i]);
target.vy -= y * b;
source.vx += x * (b = 1 - b);
source.vy += y * b;
}
}
}
function initialize() {
if (!nodes) return;
var i,
n = nodes.length,
m = links.length,
nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d])),
link;
for (i = 0, count = new Array(n); i < m; ++i) {
link = links[i], link.index = i;
if (typeof link.source !== "object") link.source = find(nodeById, link.source);
if (typeof link.target !== "object") link.target = find(nodeById, link.target);
count[link.source.index] = (count[link.source.index] || 0) + 1;
count[link.target.index] = (count[link.target.index] || 0) + 1;
}
for (i = 0, bias = new Array(m); i < m; ++i) {
link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
}
strengths = new Array(m), initializeStrength();
distances = new Array(m), initializeDistance();
}
function initializeStrength() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
strengths[i] = +strength(links[i], i, links);
}
}
function initializeDistance() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
distances[i] = +distance(links[i], i, links);
}
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.links = function(_) {
return arguments.length ? (links = _, initialize(), force) : links;
};
force.id = function(_) {
return arguments.length ? (id = _, force) : id;
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
};
force.distance = function(_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
};
return force;
}
function manyBody() {
var nodes,
node,
random,
alpha,
strength = constant(-30),
strengths,
distanceMin2 = 1,
distanceMax2 = Infinity,
theta2 = 0.81;
function force(_) {
var i, n = nodes.length, tree = d3Quadtree.quadtree(nodes, x$2, y$2).visitAfter(accumulate);
for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply);
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
strengths = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], strengths[node.index] = +strength(node, i, nodes);
}
function accumulate(quad) {
var strength = 0, q, c, weight = 0, x, y, i;
// For internal nodes, accumulate forces from child quadrants.
if (quad.length) {
for (x = y = i = 0; i < 4; ++i) {
if ((q = quad[i]) && (c = Math.abs(q.value))) {
strength += q.value, weight += c, x += c * q.x, y += c * q.y;
}
}
quad.x = x / weight;
quad.y = y / weight;
}
// For leaf nodes, accumulate forces from coincident quadrants.
else {
q = quad;
q.x = q.data.x;
q.y = q.data.y;
do strength += strengths[q.data.index];
while (q = q.next);
}
quad.value = strength;
}
function apply(quad, x1, _, x2) {
if (!quad.value) return true;
var x = quad.x - node.x,
y = quad.y - node.y,
w = x2 - x1,
l = x * x + y * y;
// Apply the Barnes-Hut approximation if possible.
// Limit forces for very close nodes; randomize direction if coincident.
if (w * w / theta2 < l) {
if (l < distanceMax2) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
node.vx += x * quad.value * alpha / l;
node.vy += y * quad.value * alpha / l;
}
return true;
}
// Otherwise, process points directly.
else if (quad.length || l >= distanceMax2) return;
// Limit forces for very close nodes; randomize direction if coincident.
if (quad.data !== node || quad.next) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
}
do if (quad.data !== node) {
w = strengths[quad.data.index] * alpha / l;
node.vx += x * w;
node.vy += y * w;
} while (quad = quad.next);
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.distanceMin = function(_) {
return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
};
force.distanceMax = function(_) {
return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
};
force.theta = function(_) {
return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
};
return force;
}
function radial(radius, x, y) {
var nodes,
strength = constant(0.1),
strengths,
radiuses;
if (typeof radius !== "function") radius = constant(+radius);
if (x == null) x = 0;
if (y == null) y = 0;
function force(alpha) {
for (var i = 0, n = nodes.length; i < n; ++i) {
var node = nodes[i],
dx = node.x - x || 1e-6,
dy = node.y - y || 1e-6,
r = Math.sqrt(dx * dx + dy * dy),
k = (radiuses[i] - r) * strengths[i] * alpha / r;
node.vx += dx * k;
node.vy += dy * k;
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
radiuses = new Array(n);
for (i = 0; i < n; ++i) {
radiuses[i] = +radius(nodes[i], i, nodes);
strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes);
}
}
force.initialize = function(_) {
nodes = _, initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};
force.x = function(_) {
return arguments.length ? (x = +_, force) : x;
};
force.y = function(_) {
return arguments.length ? (y = +_, force) : y;
};
return force;
}
function x(x) {
var strength = constant(0.1),
nodes,
strengths,
xz;
if (typeof x !== "function") x = constant(x == null ? 0 : +x);
function force(alpha) {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
xz = new Array(n);
for (i = 0; i < n; ++i) {
strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
}
}
force.initialize = function(_) {
nodes = _;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.x = function(_) {
return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x;
};
return force;
}
function y(y) {
var strength = constant(0.1),
nodes,
strengths,
yz;
if (typeof y !== "function") y = constant(y == null ? 0 : +y);
function force(alpha) {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
yz = new Array(n);
for (i = 0; i < n; ++i) {
strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
}
}
force.initialize = function(_) {
nodes = _;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.y = function(_) {
return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y;
};
return force;
}
function centerGpu(x, y) {
var nodes,
device,
pipeline,
bindGroup,
centerParamsBuffer,
strength = 1;
if (x == null) x = 0;
if (y == null) y = 0;
async function force() {
if (!device || !pipeline || !bindGroup) return;
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(1); // Single workgroup for reduction
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
// Wait for completion
await device.queue.onSubmittedWorkDone();
}
force.initialize = function(_nodes, _random, _device, nodeBuffer) {
nodes = _nodes;
device = _device;
const shaderCode = `
struct Node {
position: vec2<f32>,
velocity: vec2<f32>,
fixedPosition: vec2<f32>,
index: f32,
_padding: f32,
}
struct CenterParams {
center: vec2<f32>,
strength: f32,
nodeCount: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: CenterParams;
@group(0) @binding(2) var<storage, read_write> reduction: array<vec2<f32>>;
var<workgroup> shared_sum: array<vec2<f32>, 64>;
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(workgroup_id) workgroup_id: vec3<u32>
) {
let tid = local_id.x;
let gid = global_id.x;
let nodeCount = u32(params.nodeCount);
// First pass: compute center of mass
if (workgroup_id.x == 0u) {
var sum = vec2<f32>(0.0, 0.0);
// Each thread sums multiple nodes
for (var i = gid; i < nodeCount; i += 64u) {
sum += nodes[i].position;
}
shared_sum[tid] = sum;
workgroupBarrier();
// Reduction in shared memory
for (var s = 32u; s > 0u; s >>= 1u) {
if (tid < s) {
shared_sum[tid] += shared_sum[tid + s];
}
workgroupBarrier();
}
// Write result
if (tid == 0u) {
reduction[0] = shared_sum[0] / f32(nodeCount);
}
}
workgroupBarrier();
storageBarrier();
// Second pass: apply centering force
if (gid < nodeCount) {
let center_of_mass = reduction[0];
let delta = (params.center - center_of_mass) * params.strength;
nodes[gid].position += delta;
}
}`;
const shaderModule = device.createShaderModule({
label: 'Center Force Shader',
code: shaderCode
});
centerParamsBuffer = device.createBuffer({
label: 'Center Parameters',
size: 4 * 4, // vec2 + 2 floats
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(centerParamsBuffer.getMappedRange()).set([
x, y, strength, nodes.length
]);
centerParamsBuffer.unmap();
const reductionBuffer = device.createBuffer({
label: 'Reduction Buffer',
size: 8, // vec2
usage: GPUBufferUsage.STORAGE
});
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' }
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'uniform' }
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' }
}
]
});
pipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout]
}),
compute: {
module: shaderModule,
entryPoint: 'main'
}
});
bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: nodeBuffer } },
{ binding: 1, resource: { buffer: centerParamsBuffer } },
{ binding: 2, resource: { buffer: reductionBuffer } }
]
});
};
force.x = function(_) {
return arguments.length ? (x = +_, updateCenterParams(), force) : x;
};
force.y = function(_) {
return arguments.length ? (y = +_, updateCenterParams(), force) : y;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, updateCenterParams(), force) : strength;
};
function updateCenterParams() {
if (device && centerParamsBuffer && nodes) {
const params = new Float32Array([x, y, strength, nodes.length]);
device.queue.writeBuffer(centerParamsBuffer, 0, params);
}
}
return force;
}
function manyBodyGpu() {
var nodes,
device,
pipeline,
bindGroup,
strengthBuffer,
forceParamsBuffer,
strength = constant(-30),
strengths,
distanceMin2 = 1,
distanceMax2 = Infinity,
theta2 = 0.81;
async function force(alpha) {
if (!device || !pipeline || !bindGroup) return;
const strengthValue = typeof strength === 'function' ? strength() : strength;
const forceParams = new Float32Array([
strengthValue,
distanceMin2,
distanceMax2,
theta2,
alpha,
nodes.length,
0, 0
]);
device.queue.writeBuffer(forceParamsBuffer, 0, forceParams);
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(Math.ceil(nodes.length / 64));
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
// Wait for completion
await device.queue.onSubmittedWorkDone();
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
strengths = new Float32Array(n);
for (i = 0; i < n; ++i) {
node = nodes[i];
strengths[node.index] = +strength(node, i, nodes);
}
if (device && strengthBuffer) {
device.queue.writeBuffer(strengthBuffer, 0, strengths);
}
}
force.initialize = function(_nodes, _random, _device, nodeBuffer) {
nodes = _nodes;
device = _device;
initialize();
const shaderCode = `
struct Node {
position: vec2<f32>,
velocity: vec2<f32>,
fixedPosition: vec2<f32>,
index: f32,
_padding: f32,
}
struct ForceParams {
strength: f32,
distanceMin2: f32,
distanceMax2: f32,
theta2: f32,
alpha: f32,
nodeCount: f32,
_padding: vec2<f32>,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<storage, read> strengths: array<f32>;
@group(0) @binding(2) var<uniform> params: ForceParams;
const EPSILON: f32 = 1e-6;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let idx = global_id.x;
let nodeCount = u32(params.nodeCount);
if (idx >= nodeCount) {
return;
}
var node = nodes[idx];
var force = vec2<f32>(0.0, 0.0);
for (var j = 0u; j < nodeCount; j++) {
if (j == idx) {
continue;
}
let other = nodes[j];
var delta = node.position - other.position;
var l = dot(delta, delta);
if (l < EPSILON) {
delta = vec2<f32>(
(f32(idx) * 0.618033988749895 - floor(f32(idx) * 0.618033988749895)) * 2.0 - 1.0,
(f32(j) * 0.618033988749895 - floor(f32(j) * 0.618033988749895)) * 2.0 - 1.0
) * EPSILON;
l = dot(delta, delta);
}
if (l >= params.distanceMax2) {
continue;
}
if (l < params.distanceMin2) {
l = sqrt(params.distanceMin2 * l);
} else {
l = sqrt(l);
}
let strength = strengths[j] * params.alpha / l;
force += delta / l * strength;
}
node.velocity += force;
nodes[idx] = node;
}`;
const shaderModule = device.createShaderModule({
label: 'Many Body Force Shader',
code: shaderCode
});
strengthBuffer = device.createBuffer({
label: 'Strength Buffer',
size: strengths.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(strengthBuffer.getMappedRange()).set(strengths);
strengthBuffer.unmap();
forceParamsBuffer = device.createBuffer({
label: 'Force Parameters',
size: 8 * 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' }
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'read-only-storage' }
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'uniform' }
}
]
});
pipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout]
}),
compute: {
module: shaderModule,
entryPoint: 'main'
}
});
bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: nodeBuffer } },
{ binding: 1, resource: { buffer: strengthBuffer } },
{ binding: 2, resource: { buffer: forceParamsBuffer } }
]
});
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.distanceMin = function(_) {
return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
};
force.distanceMax = function(_) {
return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
};
force.theta = function(_) {
return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
};
return force;
}
// Main adaptive d3-force packag