UNPKG

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
// 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