chartai 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/chart-library.d.ts +170 -0
- package/dist/chart-library.d.ts.map +1 -0
- package/dist/chart-library.js +544 -0
- package/dist/chart-library.min.js +1 -0
- package/dist/gpu-worker.d.ts +2 -0
- package/dist/gpu-worker.d.ts.map +1 -0
- package/dist/gpu-worker.js +853 -0
- package/dist/gpu-worker.min.js +1 -0
- package/dist/hover.d.ts +3 -0
- package/dist/hover.d.ts.map +1 -0
- package/dist/hover.js +181 -0
- package/dist/hover.min.js +1 -0
- package/dist/labels.d.ts +3 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +101 -0
- package/dist/labels.min.js +1 -0
- package/dist/zoom.d.ts +6 -0
- package/dist/zoom.d.ts.map +1 -0
- package/dist/zoom.js +205 -0
- package/dist/zoom.min.js +1 -0
- package/package.json +78 -0
- package/readme.md +121 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
// src/shaders/shared.ts
|
|
2
|
+
var COMPUTE_WG = 256;
|
|
3
|
+
var UNIFORM_STRUCT = `struct Uniforms{width: f32,height: f32,viewMinX: f32,viewMaxX: f32,viewMinY: f32,viewMaxY: f32,pointCount: u32,isDark: f32,bgR: f32,bgG: f32,bgB: f32,pointRadius: f32,dataMinY: f32,dataMaxY: f32,dataMinX: f32,dataMaxX: f32,visibleStart: u32,visibleCount: u32,dispatchXCount: u32,maxSamplesPerPixel: u32,seriesCount: u32,_pad2: u32,_pad3: u32,_pad4: u32,};struct SeriesInfo{color: vec4f,visibleRange: vec2u,pointSize: f32,_pad: f32,};struct SeriesIndex{index: u32,_pad0: u32,_pad1: u32,_pad2: u32,};`;
|
|
4
|
+
var BINARY_SEARCH = `fn lowerBound(val: f32,count: u32)-> u32{var lo = 0u;var hi = count;while(lo < hi){let mid =(lo + hi)/ 2u;if(dataX[mid] < val){lo = mid + 1u;}else{hi = mid;}}return lo;}`;
|
|
5
|
+
var FXAA_SHADER = `fn l(c:vec4f)->f32{return dot(c.rgb,vec3f(.299,.587,.114))+c.a*.25;}fn fxaa(u:vec2f,t:texture_2d<f32>,s:sampler)->vec4f{let r=1./vec2f(textureDimensions(t));let rM=textureSampleLevel(t,s,u,0.);let rN=textureSampleLevel(t,s,u+vec2f(0.,-r.y),0.);let rS=textureSampleLevel(t,s,u+vec2f(0.,r.y),0.);let rE=textureSampleLevel(t,s,u+vec2f(r.x,0.),0.);let rW=textureSampleLevel(t,s,u+vec2f(-r.x,0.),0.);let lM=l(rM);let lN=l(rN);let lS=l(rS);let lE=l(rE);let lW=l(rW);let mi=min(lM,min(min(lN,lS),min(lE,lW)));let ma=max(lM,max(max(lN,lS),max(lE,lW)));let ra=ma-mi;if(ra<max(.0833,ma*.166)){return rM;}let lNW=l(textureSampleLevel(t,s,u+vec2f(-r.x,-r.y),0.));let lNE=l(textureSampleLevel(t,s,u+vec2f(r.x,-r.y),0.));let lSW=l(textureSampleLevel(t,s,u+vec2f(-r.x,r.y),0.));let lSE=l(textureSampleLevel(t,s,u+vec2f(r.x,r.y),0.));let sB=min(.35,max(0.,abs((lN+lS+lE+lW)*.25-lM)/ra-.25)*1.33);let iH=abs(lNW+lNE-2.*lN)+abs(lW+lE-2.*lM)*2.+abs(lSW+lSE-2.*lS)>=abs(lNW+lSW-2.*lW)+abs(lN+lS-2.*lM)*2.+abs(lNE+lSE-2.*lE);let l1=select(lW,lN,iH);let l2=select(lE,lS,iH);let pD=abs(l2-lM)>abs(l1-lM);let sL=select(r.x,r.y,iH);let lA=.5*(select(l1,l2,pD)+lM);let gS=max(abs(l1-lM),abs(l2-lM))*.25;var eU=u;if(iH){eU.y+=select(-.5,.5,pD)*sL;}else{eU.x+=select(-.5,.5,pD)*sL;}let eS=select(vec2f(r.x,0.),vec2f(0.,r.y),iH);var uN=eU-eS;var uP=eU+eS;var eN=l(textureSampleLevel(t,s,uN,0.))-lA;var eP=l(textureSampleLevel(t,s,uP,0.))-lA;var dN=abs(eN)>=gS;var dP=abs(eP)>=gS;for(var i=1;i<8;i++){if(!dN){uN-=eS*1.5;eN=l(textureSampleLevel(t,s,uN,0.))-lA;dN=abs(eN)>=gS;}if(!dP){uP+=eS*1.5;eP=l(textureSampleLevel(t,s,uP,0.))-lA;dP=abs(eP)>=gS;}if(dN&&dP){break;}}let dtN=select(u.x-uN.x,u.y-uN.y,iH);let dtP=select(uP.x-u.x,uP.y-u.y,iH);let eB=select(0.,.5-min(dtN,dtP)/(dtN+dtP),(lM-lA<0.)!=(select(eP<0.,eN<0.,dtN<dtP)));var fU=u;let fL=max(eB,sB);if(iH){fU.y+=select(-1.,1.,pD)*fL*sL;}else{fU.x+=select(-1.,1.,pD)*fL*sL;}return textureSampleLevel(t,s,fU,0.);}`;
|
|
6
|
+
var FXAA_RENDER_SHADER = `${UNIFORM_STRUCT}${FXAA_SHADER}@group(0)@binding(0)var<uniform> u: Uniforms;@group(0)@binding(1)var inputTex: texture_2d<f32>;@group(0)@binding(2)var samp: sampler;struct VertexOutput{@builtin(position)pos: vec4f,@location(0)uv: vec2f,};@vertex fn vs(@builtin(vertex_index)vi: u32)-> VertexOutput{var positions = array<vec2f,4>(vec2f(-1.0,-1.0),vec2f(1.0,-1.0),vec2f(-1.0,1.0),vec2f(1.0,1.0));var uvs = array<vec2f,4>(vec2f(0.0,1.0),vec2f(1.0,1.0),vec2f(0.0,0.0),vec2f(1.0,0.0));var out: VertexOutput;out.pos = vec4f(positions[vi],0.0,1.0);out.uv = uvs[vi];return out;}@fragment fn fs(in: VertexOutput)-> @location(0)vec4f{return fxaa(in.uv,inputTex,samp);}`;
|
|
7
|
+
|
|
8
|
+
// src/shaders/line.ts
|
|
9
|
+
var LINE_COMPUTE_SHADER = `${UNIFORM_STRUCT}struct LineData{screenX: f32,minScreenY: f32,maxScreenY: f32,valid: f32,};@group(0)@binding(0)var<uniform> u: Uniforms;@group(0)@binding(1)var<storage,read> dataX: array<f32>;@group(0)@binding(2)var<storage,read> dataY: array<f32>;@group(0)@binding(3)var<storage,read_write> lineData: array<LineData>;@group(0)@binding(4)var<storage,read> allSeries: array<SeriesInfo>;${BINARY_SEARCH}@compute @workgroup_size(${COMPUTE_WG})fn main(@builtin(global_invocation_id)id: vec3u){let outputIdx = id.x;let maxCols = u32(u.width);let count = u.pointCount;if(outputIdx >= maxCols || count == 0u){if(outputIdx < maxCols){lineData[outputIdx] = LineData(-1.0,-1.0,-1.0,0.0);}return;}let viewRangeX = u.viewMaxX - u.viewMinX;let viewRangeY = u.viewMaxY - u.viewMinY;if(viewRangeX < 0.0001 || viewRangeY < 0.0001){lineData[outputIdx] = LineData(-1.0,-1.0,-1.0,0.0);return;}let relPx = f32(outputIdx);let pixelMinX = u.viewMinX +(relPx / u.width)* viewRangeX;let pixelMaxX = u.viewMinX +((relPx + 1.0)/ u.width)* viewRangeX;let startIdx = lowerBound(pixelMinX,count);var endIdx = lowerBound(pixelMaxX,count);endIdx = min(endIdx,count);let centerX =(pixelMinX + pixelMaxX)* 0.5;if(startIdx >= endIdx){var bestIdx = startIdx;if(startIdx > 0u && startIdx < count){let distPrev = abs(dataX[startIdx - 1u] - centerX);let distCurr = abs(dataX[startIdx] - centerX);if(distPrev < distCurr){bestIdx = startIdx - 1u;}}else if(startIdx >= count && count > 0u){bestIdx = count - 1u;}if(bestIdx >= count){lineData[outputIdx] = LineData(-1.0,-1.0,-1.0,0.0);return;}let y = dataY[bestIdx];let normY =(y - u.viewMinY)/ viewRangeY;let screenY = 1.0 - normY;let normX =(dataX[bestIdx] - u.viewMinX)/ viewRangeX;let screenX = normX;lineData[outputIdx] = LineData(screenX,screenY,screenY,1.0);return;}var dataMinY = dataY[startIdx];var dataMaxY = dataY[startIdx];let rangeCount = endIdx - startIdx;let maxSamples = u.maxSamplesPerPixel;if(maxSamples > 1u && rangeCount > maxSamples){let stride = f32(rangeCount - 1u)/ f32(maxSamples - 1u);for(var s = 0u;s < maxSamples;s++){let idx = startIdx + u32(f32(s)* stride);if(idx < endIdx){let y = dataY[idx];dataMinY = min(dataMinY,y);dataMaxY = max(dataMaxY,y);}}let lastY = dataY[endIdx - 1u];dataMinY = min(dataMinY,lastY);dataMaxY = max(dataMaxY,lastY);}else{for(var i = startIdx + 1u;i < endIdx;i++){let y = dataY[i];dataMinY = min(dataMinY,y);dataMaxY = max(dataMaxY,y);}}let normX =(centerX - u.viewMinX)/ viewRangeX;let screenX = normX;let normMaxY =(dataMaxY - u.viewMinY)/ viewRangeY;let normMinY =(dataMinY - u.viewMinY)/ viewRangeY;let minScreenY = 1.0 - normMaxY;let maxScreenY = 1.0 - normMinY;lineData[outputIdx] = LineData(screenX,minScreenY,maxScreenY,1.0);}`;
|
|
10
|
+
var LINE_RENDER_SHADER = `${UNIFORM_STRUCT}struct LineData{screenX: f32,minScreenY: f32,maxScreenY: f32,valid: f32,};@group(0)@binding(0)var<uniform> u: Uniforms;@group(0)@binding(1)var<storage,read> lineData: array<LineData>;@group(0)@binding(2)var<storage,read> allSeries: array<SeriesInfo>;struct VertexOutput{@builtin(position)pos: vec4f,@location(0)alpha: f32,@location(1)@interpolate(flat)seriesIdx: u32,};@vertex fn vs(@builtin(vertex_index)vi: u32,@builtin(instance_index)series_idx: u32)-> VertexOutput{var out: VertexOutput;out.seriesIdx = series_idx;let maxCols = u32(u.width);let segIdx = vi / 2u;let endpoint = vi % 2u;if(segIdx < maxCols){let d = lineData[segIdx];let y = select(d.maxScreenY,d.minScreenY,endpoint == 0u);out.pos = vec4f(d.screenX * 2.0 - 1.0,1.0 - y * 2.0,0.0,d.valid);out.alpha = d.valid;}else{let connIdx = segIdx - maxCols;if(connIdx + 1u >= maxCols){out.pos = vec4f(0.0,0.0,0.0,0.0);out.alpha = 0.0;return out;}let d0 = lineData[connIdx];let d1 = lineData[connIdx + 1u];let segValid = min(d0.valid,d1.valid);if(endpoint == 0u){let midY =(d0.minScreenY + d0.maxScreenY)* 0.5;out.pos = vec4f(d0.screenX * 2.0 - 1.0,1.0 - midY * 2.0,0.0,segValid);}else{let midY =(d1.minScreenY + d1.maxScreenY)* 0.5;out.pos = vec4f(d1.screenX * 2.0 - 1.0,1.0 - midY * 2.0,0.0,segValid);}out.alpha = segValid;}return out;}@fragment fn fs(in: VertexOutput)-> @location(0)vec4f{if(in.alpha < 0.1){discard;}let series = allSeries[in.seriesIdx];return vec4f(series.color.rgb,1.0);}`;
|
|
11
|
+
|
|
12
|
+
// src/shaders/scatter.ts
|
|
13
|
+
var SCATTER_COMPUTE_SHADER = `${UNIFORM_STRUCT}@group(0)@binding(0)var<uniform> u: Uniforms;@group(0)@binding(1)var<storage,read> dataX: array<f32>;@group(0)@binding(2)var<storage,read> dataY: array<f32>;@group(0)@binding(3)var outputTex: texture_storage_2d<rgba8unorm,write>;@group(0)@binding(4)var<storage,read> allSeries: array<SeriesInfo>;@group(0)@binding(5)var<uniform> seriesIdx: SeriesIndex;@compute @workgroup_size(${COMPUTE_WG})fn main(@builtin(global_invocation_id)id: vec3u){let series = allSeries[seriesIdx.index];let visStart = series.visibleRange.x;let visCount = series.visibleRange.y;let localIdx = id.y * u.dispatchXCount + id.x;if(localIdx >= visCount){return;}let idx = visStart + localIdx;let count = u.pointCount;if(idx >= count){return;}let x = dataX[idx];let y = dataY[idx];if(y < u.viewMinY || y > u.viewMaxY){return;}let width = u32(u.width);let height = u32(u.height);let rangeX = u.viewMaxX - u.viewMinX;let rangeY = u.viewMaxY - u.viewMinY;if(rangeX < 0.0001 || rangeY < 0.0001){return;}let normX =(x - u.viewMinX)/ rangeX;let normY =(y - u.viewMinY)/ rangeY;let screenX = normX;let screenY = 1.0 - normY;let pixelX = i32(screenX * f32(width));let pixelY = i32(screenY * f32(height));if(idx > visStart){let prevX = dataX[idx - 1u];let prevY = dataY[idx - 1u];let prevNormX =(prevX - u.viewMinX)/ rangeX;let prevNormY =(prevY - u.viewMinY)/ rangeY;let prevPx = i32(prevNormX * f32(width));let prevPy = i32((1.0 - prevNormY)* f32(height));if(pixelX == prevPx && pixelY == prevPy){return;}}let iWidth = i32(width);let iHeight = i32(height);if(pixelX < 0 || pixelX >= iWidth){return;}if(pixelY < 0 || pixelY >= iHeight){return;}let color = series.color;let radius = i32(series.pointSize);for(var dy = -radius;dy <= radius;dy++){for(var dx = -radius;dx <= radius;dx++){if(dx * dx + dy * dy > radius * radius){continue;}let px = pixelX + dx;let py = pixelY + dy;if(px >= 0 && px < iWidth && py >= 0 && py < iHeight){textureStore(outputTex,vec2i(px,py),color);}}}}`;
|
|
14
|
+
|
|
15
|
+
// src/shaders/box.ts
|
|
16
|
+
var BOX_COMPUTE_SHADER = `${UNIFORM_STRUCT}struct BarData{screenX: f32,minY: f32,maxY: f32,barWidth: f32,};@group(0)@binding(0)var<uniform> u: Uniforms;@group(0)@binding(1)var<storage,read> dataX: array<f32>;@group(0)@binding(2)var<storage,read> dataY: array<f32>;@group(0)@binding(3)var<storage,read_write> barData: array<BarData>;@group(0)@binding(4)var<storage,read> allSeries: array<SeriesInfo>;@group(0)@binding(5)var<uniform> seriesIdx: SeriesIndex;${BINARY_SEARCH}fn barHalfWidth(idx: u32,count: u32)-> f32{if(count <= 1u){return(u.viewMaxX - u.viewMinX)* 0.4;}var spacing: f32;if(idx == 0u){spacing = dataX[1u] - dataX[0u];}else if(idx >= count - 1u){spacing = dataX[count - 1u] - dataX[count - 2u];}else{spacing = min(dataX[idx + 1u] - dataX[idx],dataX[idx] - dataX[idx - 1u]);}let seriesCount = max(1u,u.seriesCount);return(spacing * 0.4)/ f32(seriesCount);}@compute @workgroup_size(${COMPUTE_WG})fn main(@builtin(global_invocation_id)id: vec3u){let outputIdx = id.x;let maxCols = u32(u.width);let count = u.pointCount;if(outputIdx >= maxCols || count == 0u){if(outputIdx < maxCols){barData[outputIdx] = BarData(0.0,0.0,0.0,0.0);}return;}let viewRangeX = u.viewMaxX - u.viewMinX;let viewRangeY = u.viewMaxY - u.viewMinY;if(viewRangeX < 0.0001 || viewRangeY < 0.0001){barData[outputIdx] = BarData(0.0,0.0,0.0,0.0);return;}let relPx = f32(outputIdx);let pixelMinX = u.viewMinX +(relPx / u.width)* viewRangeX;let pixelMaxX = u.viewMinX +((relPx + 1.0)/ u.width)* viewRangeX;let startIdx = lowerBound(pixelMinX,count);var endIdx = lowerBound(pixelMaxX,count);endIdx = min(endIdx,count);let centerX =(pixelMinX + pixelMaxX)* 0.5;let onePixel = 1.0 / u.width;if(startIdx >= endIdx){var hit = false;var bestX: f32 = 0.0;var bestY: f32 = 0.0;var bestHW: f32 = 0.0;var bestDist: f32 = 1e10;if(startIdx < count){let bx = dataX[startIdx];let hw = barHalfWidth(startIdx,count);if(pixelMinX < bx + hw && pixelMaxX > bx - hw){let d = abs(bx - centerX);bestX = bx;bestY = dataY[startIdx];bestHW = hw;bestDist = d;hit = true;}}if(startIdx > 0u){let prev = startIdx - 1u;let bx = dataX[prev];let hw = barHalfWidth(prev,count);if(pixelMinX < bx + hw && pixelMaxX > bx - hw){let d = abs(bx - centerX);if(d < bestDist){bestX = bx;bestY = dataY[prev];bestHW = hw;bestDist = d;}hit = true;}}if(!hit){barData[outputIdx] = BarData(0.0,0.0,0.0,0.0);return;}let seriesCount = max(1u,u.seriesCount);let barOffset =(f32(seriesIdx.index)- f32(seriesCount - 1u)* 0.5)*(bestHW * 2.0);let offsetX = bestX + barOffset;let normX =(offsetX - u.viewMinX)/ viewRangeX;let fullWidth = bestHW * 2.0 / viewRangeX;let gapSize = max(onePixel,fullWidth * 0.05);let bw = max(fullWidth - gapSize,onePixel);barData[outputIdx] = BarData(normX,bestY,bestY,bw);return;}var dataMinY = dataY[startIdx];var dataMaxY = dataY[startIdx];let rangeCount = endIdx - startIdx;let maxSamples = u.maxSamplesPerPixel;if(maxSamples > 0u && rangeCount > maxSamples){let stride = f32(rangeCount - 1u)/ f32(maxSamples - 1u);for(var s = 0u;s < maxSamples;s++){let idx = startIdx + u32(f32(s)* stride);if(idx < endIdx){let y = dataY[idx];dataMinY = min(dataMinY,y);dataMaxY = max(dataMaxY,y);}}let lastY = dataY[endIdx - 1u];dataMinY = min(dataMinY,lastY);dataMaxY = max(dataMaxY,lastY);}else{for(var i = startIdx + 1u;i < endIdx;i++){let y = dataY[i];dataMinY = min(dataMinY,y);dataMaxY = max(dataMaxY,y);}}let hw = barHalfWidth(startIdx,count);let fullWidth = hw * 2.0 / viewRangeX;let gapSize = max(onePixel,fullWidth * 0.05);let bw = max(fullWidth - gapSize,onePixel);let seriesCount = max(1u,u.seriesCount);let barOffset =(f32(seriesIdx.index)- f32(seriesCount - 1u)* 0.5)*(hw * 2.0);let dataX_centered = dataX[startIdx] + barOffset;let normX =(dataX_centered - u.viewMinX)/ viewRangeX;barData[outputIdx] = BarData(normX,dataMinY,dataMaxY,bw);}`;
|
|
17
|
+
var BOX_RENDER_SHADER = `${UNIFORM_STRUCT}struct BarData{screenX: f32,minY: f32,maxY: f32,barWidth: f32,};@group(0)@binding(0)var<uniform> u: Uniforms;@group(0)@binding(1)var<storage,read> barData: array<BarData>;@group(0)@binding(2)var<storage,read> allSeries: array<SeriesInfo>;struct VertexOutput{@builtin(position)pos: vec4f,@location(0)normY: f32,@location(1)@interpolate(flat)seriesIdx: u32,};@vertex fn vs(@builtin(vertex_index)vi: u32,@builtin(instance_index)series_idx: u32)-> VertexOutput{var out: VertexOutput;out.seriesIdx = series_idx;let maxCols = u32(u.width);let colIdx = vi / 6u;let vertexType = vi % 6u;if(colIdx >= maxCols){out.pos = vec4f(0.0,0.0,0.0,0.0);out.normY = 0.0;return out;}let bd = barData[colIdx];if(bd.barWidth <= 0.0){out.pos = vec4f(0.0,0.0,0.0,0.0);out.normY = 0.0;return out;}let viewRangeY = u.viewMaxY - u.viewMinY;let safeRangeY = select(viewRangeY,1.0,viewRangeY < 0.0001);let normMinY =(min(bd.minY,0.0)- u.viewMinY)/ safeRangeY;let normMaxY =(max(bd.maxY,0.0)- u.viewMinY)/ safeRangeY;let top = 1.0 - normMaxY;let bottom = 1.0 - normMinY;let halfW = bd.barWidth * 0.5;let left = bd.screenX - halfW;let right = bd.screenX + halfW;var positions = array<vec2f,6>(vec2f(left,bottom),vec2f(right,bottom),vec2f(left,top),vec2f(left,top),vec2f(right,bottom),vec2f(right,top));let screenPos = positions[vertexType];let clipX = screenPos.x * 2.0 - 1.0;let clipY = 1.0 - screenPos.y * 2.0;out.pos = vec4f(clipX,clipY,0.0,1.0);out.normY = normMaxY;return out;}@fragment fn fs(in: VertexOutput)-> @location(0)vec4f{let series = allSeries[in.seriesIdx];return vec4f(series.color.rgb,0.85);}`;
|
|
18
|
+
|
|
19
|
+
// src/gpu-worker.ts
|
|
20
|
+
var device;
|
|
21
|
+
var canvasFormat;
|
|
22
|
+
var charts = new Map;
|
|
23
|
+
var isDark = false;
|
|
24
|
+
var layouts = {};
|
|
25
|
+
var pipelines = {};
|
|
26
|
+
var frameCount = 0;
|
|
27
|
+
var lastRenderMs = 0;
|
|
28
|
+
var renderScheduled = false;
|
|
29
|
+
var statsInterval = null;
|
|
30
|
+
var uniformStagingBuffer = new ArrayBuffer(112);
|
|
31
|
+
var uniformStagingF32 = new Float32Array(uniformStagingBuffer);
|
|
32
|
+
var uniformStagingU32 = new Uint32Array(uniformStagingBuffer);
|
|
33
|
+
var fxaaSampler;
|
|
34
|
+
var writeAllSeriesData = (chart, perSeriesPointSize) => {
|
|
35
|
+
if (!chart.seriesStorageBuffer || chart.series.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const data = new Float32Array(chart.series.length * 8);
|
|
38
|
+
const dataU32 = new Uint32Array(data.buffer);
|
|
39
|
+
for (let i = 0;i < chart.series.length; i++) {
|
|
40
|
+
const s = chart.series[i];
|
|
41
|
+
const offset = i * 8;
|
|
42
|
+
data[offset + 0] = s.colorR;
|
|
43
|
+
data[offset + 1] = s.colorG;
|
|
44
|
+
data[offset + 2] = s.colorB;
|
|
45
|
+
data[offset + 3] = 1;
|
|
46
|
+
dataU32[offset + 4] = s.visibleStart;
|
|
47
|
+
dataU32[offset + 5] = s.visibleCount;
|
|
48
|
+
data[offset + 6] = perSeriesPointSize?.[i] ?? chart.pointSize;
|
|
49
|
+
data[offset + 7] = 0;
|
|
50
|
+
}
|
|
51
|
+
device.queue.writeBuffer(chart.seriesStorageBuffer, 0, data);
|
|
52
|
+
};
|
|
53
|
+
var writeUniforms = (chart, series, seriesIndex, pointSize) => {
|
|
54
|
+
const f32 = uniformStagingF32;
|
|
55
|
+
const u32 = uniformStagingU32;
|
|
56
|
+
const rx = chart.maxX - chart.minX, ry = chart.maxY - chart.minY;
|
|
57
|
+
const bg = chart.bgColor ?? (isDark ? [0.11, 0.11, 0.12] : [0.98, 0.98, 0.98]);
|
|
58
|
+
f32.set([
|
|
59
|
+
chart.width,
|
|
60
|
+
chart.height,
|
|
61
|
+
chart.minX + chart.panX * rx,
|
|
62
|
+
chart.minX + chart.panX * rx + rx / chart.zoomX,
|
|
63
|
+
chart.minY + chart.panY * ry,
|
|
64
|
+
chart.minY + chart.panY * ry + ry / chart.zoomY
|
|
65
|
+
], 0);
|
|
66
|
+
u32[6] = series.pointCount;
|
|
67
|
+
f32.set([
|
|
68
|
+
isDark ? 1 : 0,
|
|
69
|
+
...bg,
|
|
70
|
+
pointSize ?? chart.pointSize,
|
|
71
|
+
chart.minY,
|
|
72
|
+
chart.maxY,
|
|
73
|
+
chart.minX,
|
|
74
|
+
chart.maxX
|
|
75
|
+
], 7);
|
|
76
|
+
u32[16] = series.visibleStart;
|
|
77
|
+
u32[17] = series.visibleCount;
|
|
78
|
+
u32[18] = 0;
|
|
79
|
+
u32[19] = chart.maxSamplesPerPixel;
|
|
80
|
+
u32[20] = chart.series.length;
|
|
81
|
+
device.queue.writeBuffer(chart.uniformBuffer, 0, uniformStagingBuffer);
|
|
82
|
+
};
|
|
83
|
+
var createChartPipeline = (name, cShader, rShader, topology, blend) => {
|
|
84
|
+
const cMod = device.createShaderModule({ code: cShader }), rMod = device.createShaderModule({ code: rShader });
|
|
85
|
+
pipelines[`${name}Compute`] = device.createComputePipeline({
|
|
86
|
+
layout: device.createPipelineLayout({
|
|
87
|
+
bindGroupLayouts: [layouts[`${name}Compute`]]
|
|
88
|
+
}),
|
|
89
|
+
compute: { module: cMod, entryPoint: "main" }
|
|
90
|
+
});
|
|
91
|
+
pipelines[`${name}Render`] = device.createRenderPipeline({
|
|
92
|
+
layout: device.createPipelineLayout({
|
|
93
|
+
bindGroupLayouts: [layouts[`${name}Render`]]
|
|
94
|
+
}),
|
|
95
|
+
vertex: { module: rMod, entryPoint: "vs" },
|
|
96
|
+
fragment: {
|
|
97
|
+
module: rMod,
|
|
98
|
+
entryPoint: "fs",
|
|
99
|
+
targets: [{ format: "rgba8unorm", blend }]
|
|
100
|
+
},
|
|
101
|
+
primitive: { topology }
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
async function init() {
|
|
105
|
+
if (device)
|
|
106
|
+
return true;
|
|
107
|
+
if (!navigator.gpu) {
|
|
108
|
+
postMessage({ type: "error", message: "WebGPU not supported" });
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
112
|
+
if (!adapter) {
|
|
113
|
+
postMessage({ type: "error", message: "No GPU adapter found" });
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
device = await adapter.requestDevice({
|
|
117
|
+
requiredLimits: {
|
|
118
|
+
maxBufferSize: adapter.limits.maxBufferSize,
|
|
119
|
+
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
canvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
|
123
|
+
device.lost.then((info) => {
|
|
124
|
+
postMessage({
|
|
125
|
+
type: "error",
|
|
126
|
+
message: `GPU device lost: ${info.reason} - ${info.message}`
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
createPipelines();
|
|
130
|
+
postMessage({ type: "gpu-ready" });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function createPipelines() {
|
|
134
|
+
layouts.lineCompute = device.createBindGroupLayout({
|
|
135
|
+
entries: [
|
|
136
|
+
{
|
|
137
|
+
binding: 0,
|
|
138
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
139
|
+
buffer: { type: "uniform" }
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
binding: 1,
|
|
143
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
144
|
+
buffer: { type: "read-only-storage" }
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
binding: 2,
|
|
148
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
149
|
+
buffer: { type: "read-only-storage" }
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
binding: 3,
|
|
153
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
154
|
+
buffer: { type: "storage" }
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
binding: 4,
|
|
158
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
159
|
+
buffer: { type: "read-only-storage" }
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
layouts.lineRender = device.createBindGroupLayout({
|
|
164
|
+
entries: [
|
|
165
|
+
{
|
|
166
|
+
binding: 0,
|
|
167
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
168
|
+
buffer: { type: "uniform" }
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
binding: 1,
|
|
172
|
+
visibility: GPUShaderStage.VERTEX,
|
|
173
|
+
buffer: { type: "read-only-storage" }
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
binding: 2,
|
|
177
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
178
|
+
buffer: { type: "read-only-storage" }
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
layouts.scatterCompute = device.createBindGroupLayout({
|
|
183
|
+
entries: [
|
|
184
|
+
{
|
|
185
|
+
binding: 0,
|
|
186
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
187
|
+
buffer: { type: "uniform" }
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
binding: 1,
|
|
191
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
192
|
+
buffer: { type: "read-only-storage" }
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
binding: 2,
|
|
196
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
197
|
+
buffer: { type: "read-only-storage" }
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
binding: 3,
|
|
201
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
202
|
+
storageTexture: { access: "write-only", format: "rgba8unorm" }
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
binding: 4,
|
|
206
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
207
|
+
buffer: { type: "read-only-storage" }
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
binding: 5,
|
|
211
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
212
|
+
buffer: { type: "uniform" }
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
});
|
|
216
|
+
layouts.fxaaRender = device.createBindGroupLayout({
|
|
217
|
+
entries: [
|
|
218
|
+
{
|
|
219
|
+
binding: 0,
|
|
220
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
221
|
+
buffer: { type: "uniform" }
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
binding: 1,
|
|
225
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
226
|
+
texture: { sampleType: "float" }
|
|
227
|
+
},
|
|
228
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
|
|
229
|
+
]
|
|
230
|
+
});
|
|
231
|
+
layouts.boxCompute = device.createBindGroupLayout({
|
|
232
|
+
entries: [
|
|
233
|
+
{
|
|
234
|
+
binding: 0,
|
|
235
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
236
|
+
buffer: { type: "uniform" }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
binding: 1,
|
|
240
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
241
|
+
buffer: { type: "read-only-storage" }
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
binding: 2,
|
|
245
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
246
|
+
buffer: { type: "read-only-storage" }
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
binding: 3,
|
|
250
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
251
|
+
buffer: { type: "storage" }
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
binding: 4,
|
|
255
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
256
|
+
buffer: { type: "read-only-storage" }
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
binding: 5,
|
|
260
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
261
|
+
buffer: { type: "uniform" }
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
});
|
|
265
|
+
layouts.boxRender = layouts.lineRender;
|
|
266
|
+
createChartPipeline("line", LINE_COMPUTE_SHADER, LINE_RENDER_SHADER, "line-list", {
|
|
267
|
+
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
|
|
268
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
|
|
269
|
+
});
|
|
270
|
+
createChartPipeline("box", BOX_COMPUTE_SHADER, BOX_RENDER_SHADER, "triangle-list", {
|
|
271
|
+
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
|
|
272
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
|
|
273
|
+
});
|
|
274
|
+
const scatterComputeModule = device.createShaderModule({
|
|
275
|
+
code: SCATTER_COMPUTE_SHADER
|
|
276
|
+
});
|
|
277
|
+
pipelines.scatterCompute = device.createComputePipeline({
|
|
278
|
+
layout: device.createPipelineLayout({
|
|
279
|
+
bindGroupLayouts: [layouts.scatterCompute]
|
|
280
|
+
}),
|
|
281
|
+
compute: { module: scatterComputeModule, entryPoint: "main" }
|
|
282
|
+
});
|
|
283
|
+
const fxaaRenderModule = device.createShaderModule({
|
|
284
|
+
code: FXAA_RENDER_SHADER
|
|
285
|
+
});
|
|
286
|
+
pipelines.fxaaRender = device.createRenderPipeline({
|
|
287
|
+
layout: device.createPipelineLayout({
|
|
288
|
+
bindGroupLayouts: [layouts.fxaaRender]
|
|
289
|
+
}),
|
|
290
|
+
vertex: { module: fxaaRenderModule, entryPoint: "vs" },
|
|
291
|
+
fragment: {
|
|
292
|
+
module: fxaaRenderModule,
|
|
293
|
+
entryPoint: "fs",
|
|
294
|
+
targets: [{ format: canvasFormat }]
|
|
295
|
+
},
|
|
296
|
+
primitive: { topology: "triangle-strip" }
|
|
297
|
+
});
|
|
298
|
+
fxaaSampler = device.createSampler({
|
|
299
|
+
magFilter: "linear",
|
|
300
|
+
minFilter: "linear"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function createChart(id, canvas, type, pointSize = 3, maxSamplesPerPixel = 0, bgColor = null) {
|
|
304
|
+
if (!device) {
|
|
305
|
+
postMessage({
|
|
306
|
+
type: "error",
|
|
307
|
+
message: `Cannot create chart ${id}: GPU device not available`
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const ctx = canvas.getContext("webgpu");
|
|
312
|
+
if (!ctx) {
|
|
313
|
+
postMessage({
|
|
314
|
+
type: "error",
|
|
315
|
+
message: `Cannot create chart ${id}: Failed to get WebGPU context`
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
ctx.configure({ device, format: canvasFormat, alphaMode: "premultiplied" });
|
|
321
|
+
} catch (e) {
|
|
322
|
+
postMessage({
|
|
323
|
+
type: "error",
|
|
324
|
+
message: `Cannot create chart ${id}: configure failed - ${e}`
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const width = canvas.width || 800;
|
|
329
|
+
const height = canvas.height || 400;
|
|
330
|
+
const uniformBuffer = device.createBuffer({
|
|
331
|
+
size: 112,
|
|
332
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
333
|
+
});
|
|
334
|
+
const chart = {
|
|
335
|
+
id,
|
|
336
|
+
canvas,
|
|
337
|
+
ctx,
|
|
338
|
+
type,
|
|
339
|
+
visible: true,
|
|
340
|
+
pointSize,
|
|
341
|
+
maxSamplesPerPixel,
|
|
342
|
+
series: [],
|
|
343
|
+
uniformBuffer,
|
|
344
|
+
seriesStorageBuffer: null,
|
|
345
|
+
outputTexture: null,
|
|
346
|
+
outputTextureView: null,
|
|
347
|
+
fxaaBindGroup: null,
|
|
348
|
+
width,
|
|
349
|
+
height,
|
|
350
|
+
panX: 0,
|
|
351
|
+
panY: 0,
|
|
352
|
+
zoomX: 1,
|
|
353
|
+
zoomY: 1,
|
|
354
|
+
minX: 0,
|
|
355
|
+
maxX: 1,
|
|
356
|
+
minY: 0,
|
|
357
|
+
maxY: 1,
|
|
358
|
+
bgColor,
|
|
359
|
+
dirty: true
|
|
360
|
+
};
|
|
361
|
+
try {
|
|
362
|
+
createChartResources(chart);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
postMessage({
|
|
365
|
+
type: "error",
|
|
366
|
+
message: `Cannot create chart ${id}: resource creation failed - ${e}`
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
charts.set(id, chart);
|
|
371
|
+
postMessage({ type: "chart-registered", id });
|
|
372
|
+
}
|
|
373
|
+
function createChartResources(chart) {
|
|
374
|
+
if (chart.outputTexture)
|
|
375
|
+
chart.outputTexture.destroy();
|
|
376
|
+
const w = Math.max(1, chart.width);
|
|
377
|
+
const h = Math.max(1, chart.height);
|
|
378
|
+
const usage = chart.type === "scatter" ? GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT : GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT;
|
|
379
|
+
chart.outputTexture = device.createTexture({
|
|
380
|
+
size: [w, h],
|
|
381
|
+
format: "rgba8unorm",
|
|
382
|
+
usage
|
|
383
|
+
});
|
|
384
|
+
chart.outputTextureView = chart.outputTexture.createView();
|
|
385
|
+
chart.fxaaBindGroup = device.createBindGroup({
|
|
386
|
+
layout: layouts.fxaaRender,
|
|
387
|
+
entries: [
|
|
388
|
+
{ binding: 0, resource: { buffer: chart.uniformBuffer } },
|
|
389
|
+
{ binding: 1, resource: chart.outputTextureView },
|
|
390
|
+
{ binding: 2, resource: fxaaSampler }
|
|
391
|
+
]
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function buildSeriesBindGroups(chart, series, seriesIndex) {
|
|
395
|
+
if (!chart.seriesStorageBuffer)
|
|
396
|
+
return;
|
|
397
|
+
if (chart.type === "scatter") {
|
|
398
|
+
if (series.seriesIndexBuffer)
|
|
399
|
+
series.seriesIndexBuffer.destroy();
|
|
400
|
+
series.seriesIndexBuffer = device.createBuffer({
|
|
401
|
+
size: 16,
|
|
402
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
403
|
+
});
|
|
404
|
+
const indexData = new Uint32Array([seriesIndex, 0, 0, 0]);
|
|
405
|
+
device.queue.writeBuffer(series.seriesIndexBuffer, 0, indexData);
|
|
406
|
+
series.computeBindGroup = device.createBindGroup({
|
|
407
|
+
layout: layouts.scatterCompute,
|
|
408
|
+
entries: [
|
|
409
|
+
{ binding: 0, resource: { buffer: chart.uniformBuffer } },
|
|
410
|
+
{ binding: 1, resource: { buffer: series.dataX } },
|
|
411
|
+
{ binding: 2, resource: { buffer: series.dataY } },
|
|
412
|
+
{ binding: 3, resource: chart.outputTextureView },
|
|
413
|
+
{ binding: 4, resource: { buffer: chart.seriesStorageBuffer } },
|
|
414
|
+
{ binding: 5, resource: { buffer: series.seriesIndexBuffer } }
|
|
415
|
+
]
|
|
416
|
+
});
|
|
417
|
+
} else {
|
|
418
|
+
const computeLayout = chart.type === "line" ? layouts.lineCompute : layouts.boxCompute;
|
|
419
|
+
const renderLayout = chart.type === "line" ? layouts.lineRender : layouts.boxRender;
|
|
420
|
+
if (series.lineBuffer)
|
|
421
|
+
series.lineBuffer.destroy();
|
|
422
|
+
series.lineBuffer = device.createBuffer({
|
|
423
|
+
size: chart.width * 16,
|
|
424
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX
|
|
425
|
+
});
|
|
426
|
+
if (chart.type === "box") {
|
|
427
|
+
if (series.seriesIndexBuffer)
|
|
428
|
+
series.seriesIndexBuffer.destroy();
|
|
429
|
+
series.seriesIndexBuffer = device.createBuffer({
|
|
430
|
+
size: 16,
|
|
431
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
432
|
+
});
|
|
433
|
+
const indexData = new Uint32Array([seriesIndex, 0, 0, 0]);
|
|
434
|
+
device.queue.writeBuffer(series.seriesIndexBuffer, 0, indexData);
|
|
435
|
+
}
|
|
436
|
+
const computeEntries = [
|
|
437
|
+
{ binding: 0, resource: { buffer: chart.uniformBuffer } },
|
|
438
|
+
{ binding: 1, resource: { buffer: series.dataX } },
|
|
439
|
+
{ binding: 2, resource: { buffer: series.dataY } },
|
|
440
|
+
{ binding: 3, resource: { buffer: series.lineBuffer } },
|
|
441
|
+
{ binding: 4, resource: { buffer: chart.seriesStorageBuffer } }
|
|
442
|
+
];
|
|
443
|
+
if (chart.type === "box" && series.seriesIndexBuffer) {
|
|
444
|
+
computeEntries.push({
|
|
445
|
+
binding: 5,
|
|
446
|
+
resource: { buffer: series.seriesIndexBuffer }
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
series.computeBindGroup = device.createBindGroup({
|
|
450
|
+
layout: computeLayout,
|
|
451
|
+
entries: computeEntries
|
|
452
|
+
});
|
|
453
|
+
series.renderBindGroup = device.createBindGroup({
|
|
454
|
+
layout: renderLayout,
|
|
455
|
+
entries: [
|
|
456
|
+
{ binding: 0, resource: { buffer: chart.uniformBuffer } },
|
|
457
|
+
{ binding: 1, resource: { buffer: series.lineBuffer } },
|
|
458
|
+
{ binding: 2, resource: { buffer: chart.seriesStorageBuffer } }
|
|
459
|
+
]
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function resizeChart(chart, width, height) {
|
|
464
|
+
if (width === chart.width && height === chart.height)
|
|
465
|
+
return;
|
|
466
|
+
if (width <= 0 || height <= 0)
|
|
467
|
+
return;
|
|
468
|
+
chart.width = width;
|
|
469
|
+
chart.height = height;
|
|
470
|
+
chart.canvas.width = width;
|
|
471
|
+
chart.canvas.height = height;
|
|
472
|
+
try {
|
|
473
|
+
createChartResources(chart);
|
|
474
|
+
for (let i = 0;i < chart.series.length; i++) {
|
|
475
|
+
buildSeriesBindGroups(chart, chart.series[i], i);
|
|
476
|
+
}
|
|
477
|
+
} catch (e) {
|
|
478
|
+
postMessage({
|
|
479
|
+
type: "error",
|
|
480
|
+
message: `resize failed for chart ${chart.id}: ${e}`
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function updateSeries(id, seriesData, bounds) {
|
|
485
|
+
const chart = charts.get(id);
|
|
486
|
+
if (!chart || !device) {
|
|
487
|
+
if (!chart)
|
|
488
|
+
postMessage({
|
|
489
|
+
type: "error",
|
|
490
|
+
message: `update-series failed: chart ${id} not found`
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
chart.minX = bounds.minX;
|
|
496
|
+
chart.maxX = bounds.maxX;
|
|
497
|
+
chart.minY = bounds.minY;
|
|
498
|
+
chart.maxY = bounds.maxY;
|
|
499
|
+
for (const s of chart.series) {
|
|
500
|
+
s.dataX.destroy();
|
|
501
|
+
s.dataY.destroy();
|
|
502
|
+
if (s.lineBuffer)
|
|
503
|
+
s.lineBuffer.destroy();
|
|
504
|
+
if (s.seriesIndexBuffer)
|
|
505
|
+
s.seriesIndexBuffer.destroy();
|
|
506
|
+
}
|
|
507
|
+
chart.series = [];
|
|
508
|
+
if (chart.seriesStorageBuffer)
|
|
509
|
+
chart.seriesStorageBuffer.destroy();
|
|
510
|
+
if (seriesData.length > 0) {
|
|
511
|
+
chart.seriesStorageBuffer = device.createBuffer({
|
|
512
|
+
size: Math.max(32, seriesData.length * 32),
|
|
513
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
for (const sd of seriesData) {
|
|
517
|
+
const dataX = device.createBuffer({
|
|
518
|
+
size: Math.max(16, sd.dataX.byteLength),
|
|
519
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
520
|
+
});
|
|
521
|
+
const dataY = device.createBuffer({
|
|
522
|
+
size: Math.max(16, sd.dataY.byteLength),
|
|
523
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
524
|
+
});
|
|
525
|
+
device.queue.writeBuffer(dataX, 0, sd.dataX);
|
|
526
|
+
device.queue.writeBuffer(dataY, 0, sd.dataY);
|
|
527
|
+
const series = {
|
|
528
|
+
label: sd.label,
|
|
529
|
+
colorR: sd.colorR,
|
|
530
|
+
colorG: sd.colorG,
|
|
531
|
+
colorB: sd.colorB,
|
|
532
|
+
dataX,
|
|
533
|
+
dataY,
|
|
534
|
+
lineBuffer: null,
|
|
535
|
+
seriesIndexBuffer: null,
|
|
536
|
+
pointCount: sd.dataX.length,
|
|
537
|
+
visibleStart: 0,
|
|
538
|
+
visibleCount: sd.dataX.length,
|
|
539
|
+
computeBindGroup: null,
|
|
540
|
+
renderBindGroup: null
|
|
541
|
+
};
|
|
542
|
+
chart.series.push(series);
|
|
543
|
+
}
|
|
544
|
+
for (let i = 0;i < chart.series.length; i++) {
|
|
545
|
+
buildSeriesBindGroups(chart, chart.series[i], i);
|
|
546
|
+
}
|
|
547
|
+
postMessage({ type: "bounds-update", id, ...bounds });
|
|
548
|
+
} catch (e) {
|
|
549
|
+
postMessage({
|
|
550
|
+
type: "error",
|
|
551
|
+
message: `update-series failed for chart ${id}: ${e}`
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function render(chart) {
|
|
556
|
+
if (!chart.ctx)
|
|
557
|
+
return;
|
|
558
|
+
if (chart.width === 0 || chart.height === 0)
|
|
559
|
+
return;
|
|
560
|
+
if (chart.series.length === 0)
|
|
561
|
+
return;
|
|
562
|
+
let textureView;
|
|
563
|
+
try {
|
|
564
|
+
textureView = chart.ctx.getCurrentTexture().createView();
|
|
565
|
+
} catch {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
renderChartWithFXAA(chart, textureView);
|
|
569
|
+
}
|
|
570
|
+
function renderChartWithFXAA(chart, textureView) {
|
|
571
|
+
const encoder = device.createCommandEncoder();
|
|
572
|
+
let perSeriesPointSizes;
|
|
573
|
+
if (chart.type === "scatter") {
|
|
574
|
+
const canvasArea = chart.width * chart.height;
|
|
575
|
+
const budget = canvasArea * 4;
|
|
576
|
+
perSeriesPointSizes = chart.series.map((s) => {
|
|
577
|
+
const visCount = Math.max(1, s.visibleCount);
|
|
578
|
+
const pixelsPerPoint = Math.PI * chart.pointSize * chart.pointSize;
|
|
579
|
+
if (visCount * pixelsPerPoint > budget) {
|
|
580
|
+
return Math.max(1, Math.sqrt(budget / (visCount * Math.PI)));
|
|
581
|
+
}
|
|
582
|
+
return chart.pointSize;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
writeAllSeriesData(chart, perSeriesPointSizes);
|
|
586
|
+
if (chart.series.length > 0) {
|
|
587
|
+
writeUniforms(chart, chart.series[0], 0);
|
|
588
|
+
}
|
|
589
|
+
const clearPass = encoder.beginRenderPass({
|
|
590
|
+
colorAttachments: [
|
|
591
|
+
{
|
|
592
|
+
view: chart.outputTextureView,
|
|
593
|
+
loadOp: "clear",
|
|
594
|
+
storeOp: "store",
|
|
595
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
596
|
+
}
|
|
597
|
+
]
|
|
598
|
+
});
|
|
599
|
+
clearPass.end();
|
|
600
|
+
for (let seriesIndex = 0;seriesIndex < chart.series.length; seriesIndex++) {
|
|
601
|
+
const series = chart.series[seriesIndex];
|
|
602
|
+
if (series.pointCount === 0)
|
|
603
|
+
continue;
|
|
604
|
+
if (chart.type === "scatter") {
|
|
605
|
+
const MAX_WG_DIM = 65535;
|
|
606
|
+
const totalWG = Math.ceil(series.visibleCount / COMPUTE_WG);
|
|
607
|
+
const wgX = Math.min(totalWG, MAX_WG_DIM);
|
|
608
|
+
const wgY = Math.ceil(totalWG / MAX_WG_DIM);
|
|
609
|
+
const dispatchBuf = new Uint32Array([wgX * COMPUTE_WG]);
|
|
610
|
+
device.queue.writeBuffer(chart.uniformBuffer, 72, dispatchBuf);
|
|
611
|
+
const computePass = encoder.beginComputePass();
|
|
612
|
+
computePass.setPipeline(pipelines.scatterCompute);
|
|
613
|
+
computePass.setBindGroup(0, series.computeBindGroup);
|
|
614
|
+
computePass.dispatchWorkgroups(wgX, wgY);
|
|
615
|
+
computePass.end();
|
|
616
|
+
} else {
|
|
617
|
+
writeUniforms(chart, series, seriesIndex);
|
|
618
|
+
const computePipeline = chart.type === "line" ? pipelines.lineCompute : pipelines.boxCompute;
|
|
619
|
+
const computePass = encoder.beginComputePass();
|
|
620
|
+
computePass.setPipeline(computePipeline);
|
|
621
|
+
computePass.setBindGroup(0, series.computeBindGroup);
|
|
622
|
+
computePass.dispatchWorkgroups(Math.ceil(chart.width / COMPUTE_WG));
|
|
623
|
+
computePass.end();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (chart.type !== "scatter") {
|
|
627
|
+
const renderPipeline = chart.type === "line" ? pipelines.lineRender : pipelines.boxRender;
|
|
628
|
+
const drawCount = chart.type === "line" ? Math.max(0, chart.width * 4 - 2) : chart.width * 6;
|
|
629
|
+
const renderPass = encoder.beginRenderPass({
|
|
630
|
+
colorAttachments: [
|
|
631
|
+
{
|
|
632
|
+
view: chart.outputTextureView,
|
|
633
|
+
loadOp: "load",
|
|
634
|
+
storeOp: "store"
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
});
|
|
638
|
+
renderPass.setPipeline(renderPipeline);
|
|
639
|
+
for (let seriesIndex = 0;seriesIndex < chart.series.length; seriesIndex++) {
|
|
640
|
+
const series = chart.series[seriesIndex];
|
|
641
|
+
if (series.pointCount === 0)
|
|
642
|
+
continue;
|
|
643
|
+
renderPass.setBindGroup(0, series.renderBindGroup);
|
|
644
|
+
renderPass.draw(drawCount, 1, 0, seriesIndex);
|
|
645
|
+
}
|
|
646
|
+
renderPass.end();
|
|
647
|
+
}
|
|
648
|
+
const fxaaPass = encoder.beginRenderPass({
|
|
649
|
+
colorAttachments: [
|
|
650
|
+
{
|
|
651
|
+
view: textureView,
|
|
652
|
+
loadOp: "clear",
|
|
653
|
+
storeOp: "store",
|
|
654
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
});
|
|
658
|
+
fxaaPass.setPipeline(pipelines.fxaaRender);
|
|
659
|
+
if (chart.fxaaBindGroup) {
|
|
660
|
+
fxaaPass.setBindGroup(0, chart.fxaaBindGroup);
|
|
661
|
+
}
|
|
662
|
+
fxaaPass.draw(4);
|
|
663
|
+
fxaaPass.end();
|
|
664
|
+
device.queue.submit([encoder.finish()]);
|
|
665
|
+
}
|
|
666
|
+
function scheduleRender() {
|
|
667
|
+
if (!renderScheduled) {
|
|
668
|
+
renderScheduled = true;
|
|
669
|
+
requestAnimationFrame(renderFrame);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function markDirty(chart) {
|
|
673
|
+
chart.dirty = true;
|
|
674
|
+
if (chart.visible)
|
|
675
|
+
scheduleRender();
|
|
676
|
+
}
|
|
677
|
+
function markAllDirty() {
|
|
678
|
+
let anyVisible = false;
|
|
679
|
+
for (const chart of charts.values()) {
|
|
680
|
+
chart.dirty = true;
|
|
681
|
+
if (chart.visible)
|
|
682
|
+
anyVisible = true;
|
|
683
|
+
}
|
|
684
|
+
if (anyVisible)
|
|
685
|
+
scheduleRender();
|
|
686
|
+
}
|
|
687
|
+
function countActiveCharts() {
|
|
688
|
+
let n = 0;
|
|
689
|
+
for (const chart of charts.values()) {
|
|
690
|
+
if (chart.visible && chart.width > 0)
|
|
691
|
+
n++;
|
|
692
|
+
}
|
|
693
|
+
return n;
|
|
694
|
+
}
|
|
695
|
+
function startStats() {
|
|
696
|
+
if (statsInterval !== null)
|
|
697
|
+
return;
|
|
698
|
+
statsInterval = setInterval(() => {
|
|
699
|
+
postMessage({
|
|
700
|
+
type: "stats",
|
|
701
|
+
fps: frameCount,
|
|
702
|
+
renderMs: lastRenderMs,
|
|
703
|
+
totalCharts: charts.size,
|
|
704
|
+
activeCharts: countActiveCharts()
|
|
705
|
+
});
|
|
706
|
+
frameCount = 0;
|
|
707
|
+
}, 1000);
|
|
708
|
+
}
|
|
709
|
+
function renderFrame() {
|
|
710
|
+
renderScheduled = false;
|
|
711
|
+
const t0 = performance.now();
|
|
712
|
+
for (const chart of charts.values()) {
|
|
713
|
+
if (chart.visible && chart.dirty && chart.width > 0) {
|
|
714
|
+
render(chart);
|
|
715
|
+
chart.dirty = false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
lastRenderMs = performance.now() - t0;
|
|
719
|
+
frameCount++;
|
|
720
|
+
}
|
|
721
|
+
self.onmessage = async (e) => {
|
|
722
|
+
const { type, ...data } = e.data;
|
|
723
|
+
switch (type) {
|
|
724
|
+
case "init":
|
|
725
|
+
isDark = data.isDark || false;
|
|
726
|
+
if (await init()) {
|
|
727
|
+
startStats();
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
case "theme":
|
|
731
|
+
isDark = data.isDark;
|
|
732
|
+
markAllDirty();
|
|
733
|
+
break;
|
|
734
|
+
case "register-chart":
|
|
735
|
+
createChart(data.id, data.canvas, data.chartType || "scatter", data.pointSize ?? 3, data.maxSamplesPerPixel ?? 100, data.bgColor ?? null);
|
|
736
|
+
{
|
|
737
|
+
const chart = charts.get(data.id);
|
|
738
|
+
if (chart)
|
|
739
|
+
markDirty(chart);
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case "unregister-chart": {
|
|
743
|
+
const chart = charts.get(data.id);
|
|
744
|
+
if (chart) {
|
|
745
|
+
try {
|
|
746
|
+
chart.ctx.unconfigure();
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
chart.uniformBuffer.destroy();
|
|
750
|
+
if (chart.seriesStorageBuffer)
|
|
751
|
+
chart.seriesStorageBuffer.destroy();
|
|
752
|
+
if (chart.outputTexture)
|
|
753
|
+
chart.outputTexture.destroy();
|
|
754
|
+
for (const s of chart.series) {
|
|
755
|
+
s.dataX.destroy();
|
|
756
|
+
s.dataY.destroy();
|
|
757
|
+
if (s.lineBuffer)
|
|
758
|
+
s.lineBuffer.destroy();
|
|
759
|
+
if (s.seriesIndexBuffer)
|
|
760
|
+
s.seriesIndexBuffer.destroy();
|
|
761
|
+
}
|
|
762
|
+
charts.delete(data.id);
|
|
763
|
+
}
|
|
764
|
+
postMessage({ type: "chart-unregistered", id: data.id });
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "set-point-size": {
|
|
768
|
+
const chart = charts.get(data.id);
|
|
769
|
+
if (chart) {
|
|
770
|
+
chart.pointSize = Math.max(1, Math.min(8, data.pointSize));
|
|
771
|
+
markDirty(chart);
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case "set-max-samples": {
|
|
776
|
+
const chart = charts.get(data.id);
|
|
777
|
+
if (chart) {
|
|
778
|
+
chart.maxSamplesPerPixel = Math.max(0, data.maxSamplesPerPixel | 0);
|
|
779
|
+
markDirty(chart);
|
|
780
|
+
}
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case "set-style": {
|
|
784
|
+
const chart = charts.get(data.id);
|
|
785
|
+
if (chart) {
|
|
786
|
+
if (data.bgColor !== undefined)
|
|
787
|
+
chart.bgColor = data.bgColor;
|
|
788
|
+
markDirty(chart);
|
|
789
|
+
}
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
case "update-series": {
|
|
793
|
+
updateSeries(data.id, data.series, data.bounds);
|
|
794
|
+
const chart = charts.get(data.id);
|
|
795
|
+
if (chart)
|
|
796
|
+
markDirty(chart);
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
case "set-visibility": {
|
|
800
|
+
const chart = charts.get(data.id);
|
|
801
|
+
if (chart) {
|
|
802
|
+
chart.visible = data.visible;
|
|
803
|
+
if (data.visible && chart.dirty)
|
|
804
|
+
scheduleRender();
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
case "view-transform": {
|
|
809
|
+
const chart = charts.get(data.id);
|
|
810
|
+
if (chart) {
|
|
811
|
+
chart.panX = data.panX;
|
|
812
|
+
chart.panY = data.panY;
|
|
813
|
+
chart.zoomX = Math.max(0.1, Math.min(1e6, data.zoomX));
|
|
814
|
+
chart.zoomY = Math.max(0.1, Math.min(1e6, data.zoomY));
|
|
815
|
+
markDirty(chart);
|
|
816
|
+
}
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case "resize": {
|
|
820
|
+
const chart = charts.get(data.id);
|
|
821
|
+
if (chart && data.width > 0 && data.height > 0) {
|
|
822
|
+
resizeChart(chart, data.width, data.height);
|
|
823
|
+
markDirty(chart);
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case "batch-view-transform": {
|
|
828
|
+
const zX = Math.max(0.1, Math.min(1e6, data.zoomX));
|
|
829
|
+
const zY = Math.max(0.1, Math.min(1e6, data.zoomY));
|
|
830
|
+
for (const t of data.transforms) {
|
|
831
|
+
const chart = charts.get(t.id);
|
|
832
|
+
if (chart) {
|
|
833
|
+
chart.panX = data.panX;
|
|
834
|
+
chart.panY = data.panY;
|
|
835
|
+
chart.zoomX = zX;
|
|
836
|
+
chart.zoomY = zY;
|
|
837
|
+
chart.dirty = true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
scheduleRender();
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case "sync-view":
|
|
844
|
+
for (const chart of charts.values()) {
|
|
845
|
+
chart.panX = data.panX;
|
|
846
|
+
chart.panY = data.panY;
|
|
847
|
+
chart.zoomX = data.zoomX;
|
|
848
|
+
chart.zoomY = data.zoomY;
|
|
849
|
+
}
|
|
850
|
+
markAllDirty();
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
};
|