UNPKG

d3-force-webgpu

Version:

Force-directed graph layout using velocity Verlet integration with WebGPU acceleration.

1,944 lines (1,639 loc) 66 kB
// https://github.com/jamescarruthers/d3-force-webgpu v3.1.2 Copyright 2010-2021 Mike Bostock (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree'), require('d3-dispatch'), require('d3-timer')) : typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree', 'd3-dispatch', 'd3-timer'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3)); }(this, (function (exports, d3Quadtree, d3Dispatch, d3Timer) { 'use strict'; 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$2(d) { return d.x + d.vx; } function y$2(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$2, y$2).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; } // 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$1(d) { return d.x; } function y$1(d) { return d.y; } var initialRadius$1 = 10, initialAngle$1 = Math.PI * (3 - Math.sqrt(5)); function simulation(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); } }; } 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$1, y$1).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; } // WebGPU Device Manager // Handles device initialization, capability detection, and resource management let gpuDevice = null; let gpuAdapter = null; let initPromise = null; async function initWebGPU() { if (gpuDevice) return gpuDevice; if (initPromise) return initPromise; initPromise = (async () => { if (!navigator.gpu) { throw new Error("WebGPU not supported in this browser"); } gpuAdapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); if (!gpuAdapter) { throw new Error("No WebGPU adapter found"); } const requiredLimits = { maxStorageBufferBindingSize: gpuAdapter.limits.maxStorageBufferBindingSize, maxBufferSize: gpuAdapter.limits.maxBufferSize, maxComputeWorkgroupsPerDimension: gpuAdapter.limits.maxComputeWorkgroupsPerDimension }; gpuDevice = await gpuAdapter.requestDevice({ requiredLimits }); gpuDevice.lost.then((info) => { console.error("WebGPU device lost:", info.message); gpuDevice = null; gpuAdapter = null; initPromise = null; }); // Handle uncaptured errors gpuDevice.onuncapturederror = (event) => { console.error("WebGPU uncaptured error:", event.error); }; return gpuDevice; })(); return initPromise; } function isWebGPUAvailable() { return typeof navigator !== "undefined" && !!navigator.gpu; } async function checkWebGPUSupport() { if (!isWebGPUAvailable()) return false; try { const adapter = await navigator.gpu.requestAdapter(); return !!adapter; } catch (e) { return false; } } // Utility to create a compute pipeline with error handling async function createComputePipeline(device, shaderCode, entryPoint, bindGroupLayout) { const shaderModule = device.createShaderModule({ code: shaderCode }); const compilationInfo = await shaderModule.getCompilationInfo(); if (compilationInfo.messages.some(m => m.type === "error")) { const errors = compilationInfo.messages.filter(m => m.type === "error"); throw new Error("Shader compilation failed: " + errors.map(e => e.message).join("\n")); } return device.createComputePipeline({ layout: bindGroupLayout ? device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }) : "auto", compute: { module: shaderModule, entryPoint } }); } // GPU Buffer Manager // Handles creation and synchronization of GPU buffers for node/link data class NodeBuffers { constructor(device, nodeCount) { this.device = device; this.nodeCount = nodeCount; this.isMapped = false; // Each node has: x, y, vx, vy (4 floats = 16 bytes) // We also need fx, fy for fixed positions (use NaN to indicate not fixed) // And strength for many-body force // Layout: [x, y, vx, vy, fx, fy, strength, radius] = 8 floats per node = 32 bytes this.floatsPerNode = 8; this.bytesPerNode = this.floatsPerNode * 4; this.bufferSize = this.nodeCount * this.bytesPerNode; // Main storage buffer (GPU read/write) this.storageBuffer = device.createBuffer({ size: this.bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, label: "Node Storage Buffer" }); // Staging buffer for reading back to CPU this.stagingBuffer = device.createBuffer({ size: this.bufferSize, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, label: "Node Staging Buffer" }); // CPU-side typed array for uploads this.cpuData = new Float32Array(this.nodeCount * this.floatsPerNode); } // Upload node data from JS objects to GPU uploadNodes(nodes) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const offset = i * this.floatsPerNode; this.cpuData[offset + 0] = node.x || 0; this.cpuData[offset + 1] = node.y || 0; this.cpuData[offset + 2] = node.vx || 0; this.cpuData[offset + 3] = node.vy || 0; this.cpuData[offset + 4] = node.fx != null ? node.fx : NaN; this.cpuData[offset + 5] = node.fy != null ? node.fy : NaN; this.cpuData[offset + 6] = node._strength != null ? node._strength : -30; this.cpuData[offset + 7] = node.radius != null ? node.radius : 5; } this.device.queue.writeBuffer(this.storageBuffer, 0, this.cpuData); } // Read node positions back from GPU to JS objects async downloadNodes(nodes) { // Safety check - don't map if already mapped if (this.isMapped) { console.warn("Buffer already mapped, skipping download"); return; } const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyBufferToBuffer(this.storageBuffer, 0, this.stagingBuffer, 0, this.bufferSize); this.device.queue.submit([commandEncoder.finish()]); this.isMapped = true; try { await this.stagingBuffer.mapAsync(GPUMapMode.READ); const data = new Float32Array(this.stagingBuffer.getMappedRange().slice(0)); this.stagingBuffer.unmap(); for (let i = 0; i < nodes.length; i++) { const offset = i * this.floatsPerNode; nodes[i].x = data[offset + 0]; nodes[i].y = data[offset + 1]; nodes[i].vx = data[offset + 2]; nodes[i].vy = data[offset + 3]; } } finally { this.isMapped = false; } } // Update velocities only (after force computation) async downloadVelocities(nodes) { const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyBufferToBuffer(this.storageBuffer, 0, this.stagingBuffer, 0, this.bufferSize); this.device.queue.submit([commandEncoder.finish()]); await this.stagingBuffer.mapAsync(GPUMapMode.READ); const data = new Float32Array(this.stagingBuffer.getMappedRange().slice(0)); this.stagingBuffer.unmap(); for (let i = 0; i < nodes.length; i++) { const offset = i * this.floatsPerNode; nodes[i].vx = data[offset + 2]; nodes[i].vy = data[offset + 3]; } } destroy() { this.storageBuffer.destroy(); this.stagingBuffer.destroy(); } } class LinkBuffers { constructor(device, linkCount) { this.device = device; this.linkCount = linkCount; // Each link has: sourceIndex, targetIndex, distance, strength, bias (5 values) // Use 8 floats for alignment: [sourceIdx, targetIdx, distance, strength, bias, pad, pad, pad] this.floatsPerLink = 8; this.bytesPerLink = this.floatsPerLink * 4; this.bufferSize = Math.max(32, this.linkCount * this.bytesPerLink); this.storageBuffer = device.createBuffer({ size: this.bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, label: "Link Storage Buffer" }); this.cpuData = new Float32Array(this.linkCount * this.floatsPerLink); } uploadLinks(links, distances, strengths, bias) { for (let i = 0; i < links.length; i++) { const link = links[i]; const offset = i * this.floatsPerLink; this.cpuData[offset + 0] = link.source.index; this.cpuData[offset + 1] = link.target.index; this.cpuData[offset + 2] = distances[i]; this.cpuData[offset + 3] = strengths[i]; this.cpuData[offset + 4] = bias[i]; // padding this.cpuData[offset + 5] = 0; this.cpuData[offset + 6] = 0; this.cpuData[offset + 7] = 0; } this.device.queue.writeBuffer(this.storageBuffer, 0, this.cpuData); } destroy() { this.storageBuffer.destroy(); } } class SimulationParamsBuffer { constructor(device) { this.device = device; // Simulation parameters matching shader struct (24 floats = 96 bytes): // 0: alpha: f32, 1: velocityDecay: f32, 2: nodeCount: u32, 3: linkCount: u32, // 4: centerX: f32, 5: centerY: f32, 6: centerStrength: f32, 7: theta2: f32, // 8: distanceMin2: f32, 9: distanceMax2: f32, 10: iterations: u32, 11: collisionRadius: f32, // 12: collisionStrength: f32, 13: collisionIterations: u32, 14: forceXTarget: f32, 15: forceXStrength: f32, // 16: forceYTarget: f32, 17: forceYStrength: f32, 18: radialX: f32, 19: radialY: f32, // 20: radialRadius: f32, 21: radialStrength: f32, 22: _pad1: f32, 23: _pad2: f32 // 24 x 4 bytes = 96 bytes (aligned to 16 bytes) this.bufferSize = 96; this.storageBuffer = device.createBuffer({ size: this.bufferSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, label: "Simulation Params Buffer" }); // Use ArrayBuffer with views to properly handle mixed f32/u32 types this.arrayBuffer = new ArrayBuffer(96); this.floatView = new Float32Array(this.arrayBuffer); this.uintView = new Uint32Array(this.arrayBuffer); } update(params) { // f32 values this.floatView[0] = params.alpha || 1; this.floatView[1] = params.velocityDecay || 0.6; // u32 values (indices 2, 3) this.uintView[2] = params.nodeCount || 0; this.uintView[3] = params.linkCount || 0; // f32 values this.floatView[4] = params.centerX || 0; this.floatView[5] = params.centerY || 0; this.floatView[6] = params.centerStrength || 1; this.floatView[7] = params.theta2 || 0.81; this.floatView[8] = params.distanceMin2 || 1; // Use a very large finite number instead of Infinity for GPU compatibility const maxDist2 = params.distanceMax2; this.floatView[9] = (maxDist2 === Infinity || maxDist2 > 1e30) ? 1e30 : maxDist2; // u32 value this.uintView[10] = params.iterations || 1; // f32 value this.floatView[11] = params.collisionRadius || 5; this.floatView[12] = params.collisionStrength || 1; // u32 value this.uintView[13] = params.collisionIterations || 1; // forceX params this.floatView[14] = params.forceXTarget || 0; this.floatView[15] = params.forceXStrength || 0.1; // forceY params this.floatView[16] = params.forceYTarget || 0; this.floatView[17] = params.forceYStrength || 0.1; // radial params this.floatView[18] = params.radialX || 0; this.floatView[19] = params.radialY || 0; this.floatView[20] = params.radialRadius || 100; this.floatView[21] = params.radialStrength || 0.1; // padding this.floatView[22] = 0; this.floatView[23] = 0; this.device.queue.writeBuffer(this.storageBuffer, 0, this.arrayBuffer); } destroy() { this.storageBuffer.destroy(); } } // Shader source code as strings // These are embedded directly to avoid async loading issues const manyBodyShader = ` // Many-Body Force Compute Shader // Implements N-body gravitational/repulsive force using tile-based algorithm struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<uniform> params: Params; const TILE_SIZE: u32 = 256u; var<workgroup> tile: array<vec4<f32>, 256>; fn jiggle(seed: u32) -> f32 { let s = (seed * 1103515245u + 12345u) & 0x7fffffffu; return (f32(s) / f32(0x7fffffff) - 0.5) * 1e-6; } @compute @workgroup_size(256) 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 i = global_id.x; let nodeCount = params.nodeCount; let isValid = i < nodeCount; // Load node data (use defaults for out-of-bounds threads) var myPos = vec2<f32>(0.0, 0.0); var myVx: f32 = 0.0; var myVy: f32 = 0.0; if (isValid) { let node = nodes[i]; myPos = vec2<f32>(node.x, node.y); myVx = node.vx; myVy = node.vy; } var forceX: f32 = 0.0; var forceY: f32 = 0.0; let alpha = params.alpha; let distanceMin2 = params.distanceMin2; let distanceMax2 = params.distanceMax2; let numTiles = (nodeCount + TILE_SIZE - 1u) / TILE_SIZE; for (var t: u32 = 0u; t < numTiles; t++) { // All threads participate in loading the tile let tileIdx = t * TILE_SIZE + local_id.x; if (tileIdx < nodeCount) { let other = nodes[tileIdx]; tile[local_id.x] = vec4<f32>(other.x, other.y, other.strength, 0.0); } else { tile[local_id.x] = vec4<f32>(0.0, 0.0, 0.0, 0.0); } // All threads must hit this barrier workgroupBarrier(); // Only valid threads compute forces if (isValid) { let tileEnd = min(TILE_SIZE, nodeCount - t * TILE_SIZE); for (var j: u32 = 0u; j < tileEnd; j++) { let otherIdx = t * TILE_SIZE + j; if (otherIdx != i) { let other = tile[j]; var dx = other.x - myPos.x; var dy = other.y - myPos.y; var l2 = dx * dx + dy * dy; if (l2 < distanceMax2) { if (dx == 0.0) { dx = jiggle(i * nodeCount + otherIdx); l2 += dx * dx; } if (dy == 0.0) { dy = jiggle(i * nodeCount + otherIdx + 1u); l2 += dy * dy; } if (l2 < distanceMin2) { l2 = sqrt(distanceMin2 * l2); } let strength = other.z; let force = strength * alpha / l2; forceX += dx * force; forceY += dy * force; } } } } // All threads must hit this barrier workgroupBarrier(); } // Only valid threads write results if (isValid) { nodes[i].vx = myVx + forceX; nodes[i].vy = myVy + forceY; } } `; const linkShader = ` // Link Force Compute Shader struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Link { sourceIdx: f32, targetIdx: f32, distance: f32, strength: f32, bias: f32, _pad1: f32, _pad2: f32, _pad3: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<storage, read> links: array<Link>; @group(0) @binding(2) var<uniform> params: Params; fn jiggle(seed: u32) -> f32 { let s = (seed * 1103515245u + 12345u) & 0x7fffffffu; return (f32(s) / f32(0x7fffffff) - 0.5) * 1e-6; } @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3<u32> ) { let nodeIdx = global_id.x; let nodeCount = params.nodeCount; let linkCount = params.linkCount; let alpha = params.alpha; if (nodeIdx >= nodeCount) { return; } let node = nodes[nodeIdx]; var dvx: f32 = 0.0; var dvy: f32 = 0.0; for (var i: u32 = 0u; i < linkCount; i++) { let link = links[i]; let sourceIdx = u32(link.sourceIdx); let targetIdx = u32(link.targetIdx); if (sourceIdx != nodeIdx && targetIdx != nodeIdx) { continue; } let srcNode = nodes[sourceIdx]; let dstNode = nodes[targetIdx]; var dx = dstNode.x + dstNode.vx - srcNode.x - srcNode.vx; var dy = dstNode.y + dstNode.vy - srcNode.y - srcNode.vy; if (dx == 0.0 && dy == 0.0) { dx = jiggle(i * 2u); dy = jiggle(i * 2u + 1u); } let l = sqrt(dx * dx + dy * dy); let force = (l - link.distance) / l * alpha * link.strength; let fx = dx * force; let fy = dy * force; if (nodeIdx == targetIdx) { dvx -= fx * link.bias; dvy -= fy * link.bias; } else { dvx += fx * (1.0 - link.bias); dvy += fy * (1.0 - link.bias); } } nodes[nodeIdx].vx = node.vx + dvx; nodes[nodeIdx].vy = node.vy + dvy; } `; const integrateShader = ` // Position Integration Compute Shader struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<uniform> params: Params; fn isNaN(v: f32) -> bool { return !(v == v); } @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3<u32> ) { let i = global_id.x; let nodeCount = params.nodeCount; if (i >= nodeCount) { return; } var node = nodes[i]; let velocityDecay = params.velocityDecay; if (isNaN(node.fx)) { node.vx = node.vx * velocityDecay; node.x = node.x + node.vx; } else { node.x = node.fx; node.vx = 0.0; } if (isNaN(node.fy)) { node.vy = node.vy * velocityDecay; node.y = node.y + node.vy; } else { node.y = node.fy; node.vy = 0.0; } nodes[i] = node; } `; const collisionShader = ` // Collision Force Compute Shader struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<uniform> params: Params; const TILE_SIZE: u32 = 256u; var<workgroup> tile: array<vec4<f32>, 256>; fn jiggle(seed: u32) -> f32 { let s = (seed * 1103515245u + 12345u) & 0x7fffffffu; return (f32(s) / f32(0x7fffffff) - 0.5) * 1e-6; } @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32> ) { let i = global_id.x; let nodeCount = params.nodeCount; let isValid = i < nodeCount; // Load node data (use defaults for out-of-bounds threads) var myX: f32 = 0.0; var myY: f32 = 0.0; var myRadius: f32 = 0.0; var myVx: f32 = 0.0; var myVy: f32 = 0.0; if (isValid) { let node = nodes[i]; myX = node.x; myY = node.y; myRadius = node.radius; myVx = node.vx; myVy = node.vy; } var dvx: f32 = 0.0; var dvy: f32 = 0.0; let strength = params.collisionStrength; let numTiles = (nodeCount + TILE_SIZE - 1u) / TILE_SIZE; for (var t: u32 = 0u; t < numTiles; t++) { // All threads participate in loading the tile let tileIdx = t * TILE_SIZE + local_id.x; if (tileIdx < nodeCount) { let other = nodes[tileIdx]; tile[local_id.x] = vec4<f32>(other.x, other.y, other.radius, 0.0); } else { tile[local_id.x] = vec4<f32>(0.0, 0.0, 0.0, 0.0); } // All threads must hit this barrier workgroupBarrier(); // Only valid threads compute collisions if (isValid) { let tileEnd = min(TILE_SIZE, nodeCount - t * TILE_SIZE); for (var j: u32 = 0u; j < tileEnd; j++) { let otherIdx = t * TILE_SIZE + j; if (otherIdx != i) { let other = tile[j]; let otherRadius = other.z; let combinedRadius = myRadius + otherRadius; var dx = myX - other.x; var dy = myY - other.y; var l2 = dx * dx + dy * dy; let minDist2 = combinedRadius * combinedRadius; if (l2 < minDist2) { if (dx == 0.0) { dx = jiggle(min(i, otherIdx) * nodeCount + max(i, otherIdx)); l2 += dx * dx; } if (dy == 0.0) { dy = jiggle(min(i, otherIdx) * nodeCount + max(i, otherIdx) + 1u); l2 += dy * dy; } let l = sqrt(l2); let overlap = combinedRadius - l; let totalRadius = myRadius + otherRadius; let myWeight = otherRadius / totalRadius; let impulse = overlap * strength * 0.5; let nx = dx / l; let ny = dy / l; dvx += nx * impulse * myWeight; dvy += ny * impulse * myWeight; } } } } // All threads must hit this barrier workgroupBarrier(); } // Only valid threads write results if (isValid) { nodes[i].vx = myVx + dvx; nodes[i].vy = myVy + dvy; } } `; const forceXShader = ` // X-Positioning Force Compute Shader // Pushes nodes toward a target x position struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<uniform> params: Params; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) global_id: vec3<u32>) { let i = global_id.x; if (i >= params.nodeCount) { return; } let node = nodes[i]; let alpha = params.alpha; let targetX = params.forceXTarget; let strength = params.forceXStrength; let dx = targetX - node.x; nodes[i].vx = node.vx + dx * strength * alpha; } `; const forceYShader = ` // Y-Positioning Force Compute Shader // Pushes nodes toward a target y position struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<uniform> params: Params; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) global_id: vec3<u32>) { let i = global_id.x; if (i >= params.nodeCount) { return; } let node = nodes[i]; let alpha = params.alpha; let targetY = params.forceYTarget; let strength = params.forceYStrength; let dy = targetY - node.y; nodes[i].vy = node.vy + dy * strength * alpha; } `; const forceRadialShader = ` // Radial Force Compute Shader // Pushes nodes toward a target radius from a center point struct Node { x: f32, y: f32, vx: f32, vy: f32, fx: f32, fy: f32, strength: f32, radius: f32, } struct Params { alpha: f32, velocityDecay: f32, nodeCount: u32, linkCount: u32, centerX: f32, centerY: f32, centerStrength: f32, theta2: f32, distanceMin2: f32, distanceMax2: f32, iterations: u32, collisionRadius: f32, collisionStrength: f32, collisionIterations: u32, forceXTarget: f32, forceXStrength: f32, forceYTarget: f32, forceYStrength: f32, radialX: f32, radialY: f32, radialRadius: f32, radialStrength: f32, _pad1: f32, _pad2: f32, } @group(0) @binding(0) var<storage, read_write> nodes: array<Node>; @group(0) @binding(1) var<uniform> params: Params; @compute @workgroup_size(256) fn main(@builtin(global_invocation_id) global_id: vec3<u32>) { let i = global_id.x; if (i >= params.nodeCount) { return; } let node = nodes[i]; let alpha = params.alpha; let cx = params.radialX; let cy = params.radialY; let targetRadius = params.radialRadius; let strength = params.radialStrength; let dx = node.x - cx; let dy = node.y - cy; let r = sqrt(dx * dx + dy * dy); if (r > 0.0) { let k = (targetRadius - r) * strength * alpha / r; nodes[i].vx = node.vx + dx * k; nodes[i].vy = node.vy + dy * k; } } `; // GPU-Accelerated Force Simulation const WORKGROUP_SIZE = 256; var initialRadius = 10, initialAngle = Math.PI * (3 - Math.sqrt(5)); function simulationGpu (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(); // GPU resources var gpuInitialized = false, gpuDevice = null, nodeBuffers = null, paramsBuffer = null, pipelines = {}, bindGroups = {}, gpuReadyPromise = null, gpuReadyResolve = null, gpuLoopRunning = false; // GPU compute loop running flag // Force-specific GPU resources var linkBuffers = null, linkData = { links: [], distances: [], strengths: [], bias: [] }; // Force configurations (mirroring CPU forces) var manyBodyConfig = { enabled: false, strength: -30, theta: 0.9, distanceMin: 1, distanceMax: Infinity, }; var centerConfig = { enabled: false, x: 0, y: 0, strength: 1, }; var collisionConfig = { enabled: false, radius: 5, strength: 1, iterations: 1, }; var forceXConfig = { enabled: false, x: 0, strength: 0.1, }; var forceYConfig = { enabled: false, y: 0, strength: 0.1, }; var radialConfig = { enabled: false, x: 0, y: 0, radius: 100, strength: 0.1, }; if (nodes == null) nodes = []; // Create promise for gpuReady() gpuReadyPromise = new Promise((resolve) => { gpuReadyResolve = resolve; }); async function initGPU() { if (gpuInitialized) return true; try { gpuDevice = await initWebGPU(); await createPipelines(); gpuInitialized = true; return true; } catch (e) { console.warn("WebGPU initialization failed, falling back to CPU:", e); return false; } } async function createPipelines() { // Many-body force pipeline pipelines.manyBody = await createComputePipeline( gpuDevice, manyBodyShader, "main" ); // Link force pipeline pipelines.linkForce = await createComputePipeline( gpuDevice, linkShader, "main" ); // Integration pipeline pipelines.integrate = await createComputePipeline( gpuDevice, integrateShader, "main" ); // Collision pipeline pipelines.collision = await createComputePipeline( gpuDevice, collisionShader, "main" ); // ForceX pipeline pipelines.forceX = await createComputePipeline( gpuDevice, forceXShader, "main" ); // ForceY pipeline pipelines.forceY = await createComputePipeline( gpuDevice, forceYShader, "main" ); // Radial force pipeline pipelines.radial = await createComputePipeline( gpuDevice, forceRadialShader, "main" ); } function createBuffers() { if (!gpuDevice || nodes.length === 0) return; // Destroy old buffers if (nodeBuffers) nodeBuffers.destroy(); if (paramsBuffer) paramsBuffer.destroy(); if (linkBuffers) linkBuffers.destroy(); nodeBuffers = new NodeBuffers(gpuDevice, nodes.length); paramsBuffer = new SimulationParamsBuffer(gpuDevice); // Create link buffers if we have links if (linkData.links.length > 0) { linkBuffers = new LinkBuffers(gpuDevice, linkData.links.length); } createBindGroups(); } function createBindGroups() { if (!gpuDevice || !nodeBuffers) return; // Many-body bind group bindGroups.manyBody = gpuDevice.createBindGroup({ layout: pipelines.manyBody.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); // Integration bind group bindGroups.integrate = gpuDevice.createBindGroup({ layout: pipelines.integrate.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); // Collision bind group bindGroups.collision = gpuDevice.createBindGroup({ layout: pipelines.collision.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); // Link bind group (if links exist) if (linkBuffers) { bindGroups.link = gpuDevice.createBindGroup({ layout: pipelines.linkForce.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: linkBuffers.storageBuffer } }, { binding: 2, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); } // ForceX bind group bindGroups.forceX = gpuDevice.createBindGroup({ layout: pipelines.forceX.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); // ForceY bind group bindGroups.forceY = gpuDevice.createBindGroup({ layout: pipelines.forceY.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); // Radial force bind group bindGroups.radial = gpuDevice.createBindGroup({ layout: pipelines.radial.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: nodeBuffers.storageBuffer } }, { binding: 1, resource: { buffer: paramsBuffer.storageBuffer } }, ], }); } function uploadToGPU() { if (!nodeBuffers) return; // Set per-node strengths for many-body force for (let i = 0; i < nodes.length; i++) { nodes[i]._strength = manyBodyConfig.strength; nodes[i].radius = nodes[i].radius || collisionConfig.radius; } nodeBuffers.uploadNodes(nodes); paramsBuffer.update({ alpha: alpha, velocityDecay: velocityDecay, nodeCount: nodes.length, linkCount: linkData.links.length, centerX: centerConfig.x, centerY: centerConfig.y, centerStrength: centerConfig.strength, theta2: manyBodyConfig.theta * manyBodyConfig.theta, distanceMin2: manyBodyConfig.distanceMin * manyBodyConfig.distanceMin, distanceMax2: manyBodyConfig.distanceMax * manyBodyConfig.distanceMax, iterations: 1, collisionRadius: collisionConfig.radius, collisionStrength: collisionConfig.strength, collisionIterations: collisionConfig.iterations, forceXTarget: forceXConfig.x, forceXStrength: forceXConfig.enabled