@simulatte/webgpu 0.2.0
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/API_CONTRACT.md +172 -0
- package/COMPAT_SCOPE.md +32 -0
- package/README.md +138 -0
- package/assets/fawn-icon-main-256.png +0 -0
- package/assets/fawn-icon-main.svg +272 -0
- package/assets/package-surface-cube-snapshot.svg +81 -0
- package/bin/fawn-webgpu-bench.js +182 -0
- package/bin/fawn-webgpu-compare.js +96 -0
- package/binding.gyp +21 -0
- package/doe-build-metadata.schema.json +23 -0
- package/headless-webgpu-comparison.md +43 -0
- package/native/doe_napi.c +1922 -0
- package/package.json +55 -0
- package/prebuild-metadata.schema.json +58 -0
- package/prebuilds/darwin-arm64/doe_napi.node +0 -0
- package/prebuilds/darwin-arm64/libwebgpu_dawn.dylib +0 -0
- package/prebuilds/darwin-arm64/libwebgpu_doe.dylib +0 -0
- package/prebuilds/darwin-arm64/metadata.json +26 -0
- package/scripts/generate-readme-assets.js +270 -0
- package/scripts/install.js +36 -0
- package/scripts/prebuild.js +179 -0
- package/scripts/smoke-test.js +118 -0
- package/src/build_metadata.js +104 -0
- package/src/bun-ffi.js +1057 -0
- package/src/bun.js +2 -0
- package/src/index.js +580 -0
- package/src/node-runtime.js +2 -0
- package/src/node.js +2 -0
- package/src/package-entry.js +1 -0
- package/src/runtime_cli.js +202 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simulatte/webgpu",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Doe WebGPU bridge for browserless AI/ML benchmarking and CI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/node-runtime.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"bun": "./src/bun.js",
|
|
10
|
+
"default": "./src/node-runtime.js"
|
|
11
|
+
},
|
|
12
|
+
"./bun": "./src/bun.js",
|
|
13
|
+
"./node": "./src/node-runtime.js"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"fawn-webgpu-bench": "./bin/fawn-webgpu-bench.js",
|
|
17
|
+
"fawn-webgpu-compare": "./bin/fawn-webgpu-compare.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"assets/",
|
|
21
|
+
"bin/",
|
|
22
|
+
"src/",
|
|
23
|
+
"scripts/",
|
|
24
|
+
"native/",
|
|
25
|
+
"prebuilds/",
|
|
26
|
+
"binding.gyp",
|
|
27
|
+
"README.md",
|
|
28
|
+
"API_CONTRACT.md",
|
|
29
|
+
"COMPAT_SCOPE.md",
|
|
30
|
+
"headless-webgpu-comparison.md",
|
|
31
|
+
"doe-build-metadata.schema.json",
|
|
32
|
+
"prebuild-metadata.schema.json"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"install": "node scripts/install.js",
|
|
36
|
+
"build:addon": "node-gyp rebuild",
|
|
37
|
+
"build:readme-assets": "node scripts/generate-readme-assets.js",
|
|
38
|
+
"prebuild": "node scripts/prebuild.js",
|
|
39
|
+
"test": "node ./test-node.js",
|
|
40
|
+
"test:bun": "bun ./test-bun.js",
|
|
41
|
+
"smoke": "node scripts/smoke-test.js"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"webgpu",
|
|
45
|
+
"doe",
|
|
46
|
+
"headless",
|
|
47
|
+
"benchmarking",
|
|
48
|
+
"ai",
|
|
49
|
+
"ml",
|
|
50
|
+
"ci",
|
|
51
|
+
"bun"
|
|
52
|
+
],
|
|
53
|
+
"author": "Fawn",
|
|
54
|
+
"license": "ISC"
|
|
55
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "WebGPU Package Prebuild Metadata",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"schemaVersion",
|
|
7
|
+
"package",
|
|
8
|
+
"packageVersion",
|
|
9
|
+
"platform",
|
|
10
|
+
"arch",
|
|
11
|
+
"nodeNapiVersion",
|
|
12
|
+
"doeVersion",
|
|
13
|
+
"doeBuild",
|
|
14
|
+
"files",
|
|
15
|
+
"builtAt"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"schemaVersion": { "type": "integer", "const": 1 },
|
|
19
|
+
"package": { "type": "string" },
|
|
20
|
+
"packageVersion": { "type": "string" },
|
|
21
|
+
"platform": { "type": "string" },
|
|
22
|
+
"arch": { "type": "string" },
|
|
23
|
+
"nodeNapiVersion": { "type": "integer" },
|
|
24
|
+
"doeVersion": { "type": "string" },
|
|
25
|
+
"doeBuild": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": [
|
|
28
|
+
"artifact",
|
|
29
|
+
"leanVerifiedBuild",
|
|
30
|
+
"proofArtifactSha256"
|
|
31
|
+
],
|
|
32
|
+
"properties": {
|
|
33
|
+
"artifact": { "type": "string", "const": "libwebgpu_doe" },
|
|
34
|
+
"leanVerifiedBuild": { "type": "boolean" },
|
|
35
|
+
"proofArtifactSha256": {
|
|
36
|
+
"anyOf": [
|
|
37
|
+
{ "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
38
|
+
{ "type": "null" }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"additionalProperties": false
|
|
43
|
+
},
|
|
44
|
+
"files": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["sha256"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"builtAt": { "type": "string" }
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"package": "@simulatte/webgpu",
|
|
4
|
+
"packageVersion": "0.2.0",
|
|
5
|
+
"platform": "darwin",
|
|
6
|
+
"arch": "arm64",
|
|
7
|
+
"nodeNapiVersion": 8,
|
|
8
|
+
"doeVersion": "22613a9b0",
|
|
9
|
+
"doeBuild": {
|
|
10
|
+
"artifact": "libwebgpu_doe",
|
|
11
|
+
"leanVerifiedBuild": false,
|
|
12
|
+
"proofArtifactSha256": null
|
|
13
|
+
},
|
|
14
|
+
"files": {
|
|
15
|
+
"doe_napi.node": {
|
|
16
|
+
"sha256": "ccd350506359a770d286f7f3893dd0c6d81582dbcc04524461c9fa81cae4573e"
|
|
17
|
+
},
|
|
18
|
+
"libwebgpu_doe.dylib": {
|
|
19
|
+
"sha256": "30be9ca300c53c0ba02eb76dfa94b683585c20f7a4caaa1f8eeea2cfb17d1f5f"
|
|
20
|
+
},
|
|
21
|
+
"libwebgpu_dawn.dylib": {
|
|
22
|
+
"sha256": "22751faeb459e7a2ec778c0410ca122e23c23366eb3da145c651d1d43e26707d"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"builtAt": "2026-03-07T03:39:41.504Z"
|
|
26
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
9
|
+
const WORKSPACE_ROOT = resolve(PACKAGE_ROOT, '..', '..');
|
|
10
|
+
const CUBE_SUMMARY_PATH = resolve(WORKSPACE_ROOT, 'bench', 'out', 'cube', 'latest', 'cube.summary.json');
|
|
11
|
+
const OUTPUT_PATH = resolve(PACKAGE_ROOT, 'assets', 'package-surface-cube-snapshot.svg');
|
|
12
|
+
|
|
13
|
+
const UI_FONT = '"Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
14
|
+
const MONO_FONT = 'SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
|
|
15
|
+
const TEXT_STROKE = 'paint-order: stroke fill; stroke: #000000; stroke-width: 2px; stroke-linejoin: round;';
|
|
16
|
+
|
|
17
|
+
const SURFACE_SPECS = [
|
|
18
|
+
{
|
|
19
|
+
surface: 'node_package',
|
|
20
|
+
title: 'Node package lane',
|
|
21
|
+
supportLabel: 'Primary support',
|
|
22
|
+
preferredHostProfile: 'mac_apple_silicon',
|
|
23
|
+
focusSets: ['uploads', 'compute_e2e'],
|
|
24
|
+
tone: 'left',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
surface: 'bun_package',
|
|
28
|
+
title: 'Bun package lane',
|
|
29
|
+
supportLabel: 'Prototype support',
|
|
30
|
+
preferredHostProfile: 'linux_x64',
|
|
31
|
+
focusSets: ['compute_e2e', 'uploads'],
|
|
32
|
+
tone: 'right',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const STATUS_STYLE = {
|
|
37
|
+
claimable: { fill: '#16a34a', stroke: '#86efac', label: 'CLAIMABLE' },
|
|
38
|
+
comparable: { fill: '#d97706', stroke: '#fbbf24', label: 'COMPARABLE' },
|
|
39
|
+
diagnostic: { fill: '#dc2626', stroke: '#fca5a5', label: 'DIAGNOSTIC' },
|
|
40
|
+
unimplemented: { fill: '#3f3f46', stroke: '#71717a', label: 'UNIMPLEMENTED' },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function readCubeSummary(summaryPath) {
|
|
44
|
+
return JSON.parse(readFileSync(summaryPath, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeXml(value) {
|
|
48
|
+
return String(value)
|
|
49
|
+
.replaceAll('&', '&')
|
|
50
|
+
.replaceAll('<', '<')
|
|
51
|
+
.replaceAll('>', '>')
|
|
52
|
+
.replaceAll('"', '"')
|
|
53
|
+
.replaceAll("'", ''');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatPercent(value) {
|
|
57
|
+
if (!Number.isFinite(value)) return 'n/a';
|
|
58
|
+
const sign = value > 0 ? '+' : '';
|
|
59
|
+
return `${sign}${value.toFixed(1)}%`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function compactIso(value) {
|
|
63
|
+
if (!value) return 'n/a';
|
|
64
|
+
const parsed = new Date(value);
|
|
65
|
+
if (Number.isNaN(parsed.getTime())) return value;
|
|
66
|
+
return parsed.toISOString().replace('.000Z', 'Z');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function maxGeneratedAt(cells) {
|
|
70
|
+
const stamps = cells
|
|
71
|
+
.map((cell) => cell?.latestGeneratedAt ?? '')
|
|
72
|
+
.filter((stamp) => stamp !== '');
|
|
73
|
+
if (stamps.length === 0) return 'n/a';
|
|
74
|
+
return compactIso(stamps.sort().at(-1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function implementedScore(cell) {
|
|
78
|
+
if (!cell) return 0;
|
|
79
|
+
if ((cell.rowCount ?? 0) > 0 || (cell.reportCount ?? 0) > 0) return 2;
|
|
80
|
+
if (cell.status && cell.status !== 'unimplemented') return 1;
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function selectHostProfile(cells, preferredHostProfile) {
|
|
85
|
+
const statsByHost = new Map();
|
|
86
|
+
for (const cell of cells) {
|
|
87
|
+
const host = cell.hostProfile ?? 'unknown';
|
|
88
|
+
const current = statsByHost.get(host) ?? {
|
|
89
|
+
hostProfile: host,
|
|
90
|
+
populatedCells: 0,
|
|
91
|
+
implementedCells: 0,
|
|
92
|
+
totalRows: 0,
|
|
93
|
+
claimableCells: 0,
|
|
94
|
+
latestGeneratedAt: '',
|
|
95
|
+
};
|
|
96
|
+
const score = implementedScore(cell);
|
|
97
|
+
if (score === 2) current.populatedCells += 1;
|
|
98
|
+
if (score >= 1) current.implementedCells += 1;
|
|
99
|
+
current.totalRows += cell.rowCount ?? 0;
|
|
100
|
+
if (cell.status === 'claimable') current.claimableCells += 1;
|
|
101
|
+
if ((cell.latestGeneratedAt ?? '') > current.latestGeneratedAt) {
|
|
102
|
+
current.latestGeneratedAt = cell.latestGeneratedAt;
|
|
103
|
+
}
|
|
104
|
+
statsByHost.set(host, current);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const preferred = statsByHost.get(preferredHostProfile);
|
|
108
|
+
if (preferred && preferred.populatedCells > 0) return preferred.hostProfile;
|
|
109
|
+
|
|
110
|
+
const ranked = [...statsByHost.values()].sort((left, right) => {
|
|
111
|
+
if (right.populatedCells !== left.populatedCells) {
|
|
112
|
+
return right.populatedCells - left.populatedCells;
|
|
113
|
+
}
|
|
114
|
+
if (right.claimableCells !== left.claimableCells) {
|
|
115
|
+
return right.claimableCells - left.claimableCells;
|
|
116
|
+
}
|
|
117
|
+
if (right.totalRows !== left.totalRows) {
|
|
118
|
+
return right.totalRows - left.totalRows;
|
|
119
|
+
}
|
|
120
|
+
if (right.latestGeneratedAt !== left.latestGeneratedAt) {
|
|
121
|
+
return right.latestGeneratedAt.localeCompare(left.latestGeneratedAt);
|
|
122
|
+
}
|
|
123
|
+
return left.hostProfile.localeCompare(right.hostProfile);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return ranked[0]?.hostProfile ?? preferredHostProfile;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findCell(cells, hostProfile, workloadSet) {
|
|
130
|
+
return cells.find(
|
|
131
|
+
(cell) => cell.hostProfile === hostProfile && cell.workloadSet === workloadSet,
|
|
132
|
+
) ?? {
|
|
133
|
+
hostProfile,
|
|
134
|
+
workloadSet,
|
|
135
|
+
status: 'unimplemented',
|
|
136
|
+
claimStatus: 'diagnostic',
|
|
137
|
+
rowCount: 0,
|
|
138
|
+
latestGeneratedAt: '',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function focusLabel(workloadSet) {
|
|
143
|
+
switch (workloadSet) {
|
|
144
|
+
case 'compute_e2e':
|
|
145
|
+
return 'Compute E2E';
|
|
146
|
+
case 'uploads':
|
|
147
|
+
return 'Uploads';
|
|
148
|
+
default:
|
|
149
|
+
return workloadSet;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pillForCell(cell) {
|
|
154
|
+
const claimStatus = cell.claimStatus ?? cell.status ?? 'diagnostic';
|
|
155
|
+
if (claimStatus === 'claimable') return STATUS_STYLE.claimable;
|
|
156
|
+
return STATUS_STYLE[cell.status] ?? STATUS_STYLE.diagnostic;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function summarizeFocusCell(cell) {
|
|
160
|
+
const statusLabel = (pillForCell(cell).label ?? 'DIAGNOSTIC').toLowerCase();
|
|
161
|
+
return {
|
|
162
|
+
title: focusLabel(cell.workloadSet),
|
|
163
|
+
pill: pillForCell(cell),
|
|
164
|
+
lines: [
|
|
165
|
+
`${cell.rowCount ?? 0} rows | ${statusLabel}`,
|
|
166
|
+
Number.isFinite(cell.deltaP50MedianPercent)
|
|
167
|
+
? `median p50 delta ${formatPercent(cell.deltaP50MedianPercent)}`
|
|
168
|
+
: 'median p50 delta n/a',
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderMetricRow(summary, x, y, toneClass) {
|
|
174
|
+
return `
|
|
175
|
+
<rect x="${x}" y="${y}" width="452" height="82" rx="16" class="metric ${toneClass}"/>
|
|
176
|
+
<text x="${x + 24}" y="${y + 31}" class="metricTitle">${escapeXml(summary.title)}</text>
|
|
177
|
+
<rect x="${x + 296}" y="${y + 15}" width="132" height="28" rx="14" fill="${summary.pill.fill}" stroke="${summary.pill.stroke}" stroke-width="1.5"/>
|
|
178
|
+
<text x="${x + 362}" y="${y + 34}" text-anchor="middle" class="pillText">${escapeXml(summary.pill.label)}</text>
|
|
179
|
+
<text x="${x + 24}" y="${y + 57}" class="metricBody">${escapeXml(summary.lines[0])}</text>
|
|
180
|
+
<text x="${x + 24}" y="${y + 77}" class="metricBody">${escapeXml(summary.lines[1])}</text>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderSurfaceCard(spec, cells, x) {
|
|
184
|
+
const selectedHostProfile = selectHostProfile(cells, spec.preferredHostProfile);
|
|
185
|
+
const focusCells = spec.focusSets.map((workloadSet) => findCell(cells, selectedHostProfile, workloadSet));
|
|
186
|
+
const generatedAt = maxGeneratedAt(focusCells);
|
|
187
|
+
const toneClass = spec.tone === 'left' ? 'toneLeft' : 'toneRight';
|
|
188
|
+
const focusSummaries = focusCells.map(summarizeFocusCell);
|
|
189
|
+
|
|
190
|
+
return `
|
|
191
|
+
<rect x="${x}" y="176" width="488" height="318" rx="24" class="panel ${toneClass}"/>
|
|
192
|
+
<text x="${x + 28}" y="216" class="cardTitle">${escapeXml(spec.title)}</text>
|
|
193
|
+
<text x="${x + 28}" y="244" class="cardMeta">${escapeXml(`${spec.supportLabel} | ${selectedHostProfile}`)}</text>
|
|
194
|
+
<text x="${x + 28}" y="266" class="cardMeta">${escapeXml(`latest populated cell ${generatedAt}`)}</text>
|
|
195
|
+
${renderMetricRow(focusSummaries[0], x + 18, 300, toneClass)}
|
|
196
|
+
${renderMetricRow(focusSummaries[1], x + 18, 396, toneClass)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderSvg(summary) {
|
|
200
|
+
const cells = summary.cells ?? [];
|
|
201
|
+
const nodeCells = cells.filter((cell) => cell.surface === 'node_package');
|
|
202
|
+
const bunCells = cells.filter((cell) => cell.surface === 'bun_package');
|
|
203
|
+
const nodeHost = selectHostProfile(nodeCells, SURFACE_SPECS[0].preferredHostProfile);
|
|
204
|
+
const bunHost = selectHostProfile(bunCells, SURFACE_SPECS[1].preferredHostProfile);
|
|
205
|
+
const generatedAt = maxGeneratedAt([
|
|
206
|
+
...SURFACE_SPECS[0].focusSets.map((workloadSet) => findCell(nodeCells, nodeHost, workloadSet)),
|
|
207
|
+
findCell(nodeCells, nodeHost, 'full_comparable'),
|
|
208
|
+
...SURFACE_SPECS[1].focusSets.map((workloadSet) => findCell(bunCells, bunHost, workloadSet)),
|
|
209
|
+
findCell(bunCells, bunHost, 'full_comparable'),
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" role="img" aria-labelledby="title desc">
|
|
213
|
+
<title id="title">Package surface benchmark cube snapshot</title>
|
|
214
|
+
<desc id="desc">Two-card package surface snapshot for Node and Bun generated from bench/out/cube/latest/cube.summary.json.</desc>
|
|
215
|
+
<defs>
|
|
216
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
217
|
+
<stop offset="0%" stop-color="#050816"/>
|
|
218
|
+
<stop offset="100%" stop-color="#0f172a"/>
|
|
219
|
+
</linearGradient>
|
|
220
|
+
<linearGradient id="panel-left" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
221
|
+
<stop offset="0%" stop-color="#ef444420"/>
|
|
222
|
+
<stop offset="60%" stop-color="#ef444426"/>
|
|
223
|
+
<stop offset="100%" stop-color="#7c3aed22"/>
|
|
224
|
+
</linearGradient>
|
|
225
|
+
<linearGradient id="panel-right" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
226
|
+
<stop offset="0%" stop-color="#7c3aed20"/>
|
|
227
|
+
<stop offset="60%" stop-color="#7c3aed24"/>
|
|
228
|
+
<stop offset="100%" stop-color="#2563eb22"/>
|
|
229
|
+
</linearGradient>
|
|
230
|
+
<style>
|
|
231
|
+
.title { font: 700 34px ${UI_FONT}; fill: #ffffff; ${TEXT_STROKE} }
|
|
232
|
+
.subtitle { font: 500 18px ${UI_FONT}; fill: #cbd5e1; ${TEXT_STROKE} }
|
|
233
|
+
.cardTitle { font: 700 26px ${UI_FONT}; fill: #ffffff; ${TEXT_STROKE} }
|
|
234
|
+
.cardMeta { font: 500 16px ${UI_FONT}; fill: #cbd5e1; ${TEXT_STROKE} }
|
|
235
|
+
.metricTitle { font: 700 18px ${UI_FONT}; fill: #ffffff; ${TEXT_STROKE} }
|
|
236
|
+
.metricBody { font: 500 15px ${MONO_FONT}; fill: #e2e8f0; ${TEXT_STROKE} }
|
|
237
|
+
.pillText { font: 700 13px ${UI_FONT}; fill: #f8fafc; letter-spacing: 0.5px; ${TEXT_STROKE} }
|
|
238
|
+
.foot { font: 500 14px ${UI_FONT}; fill: #cbd5e1; ${TEXT_STROKE} }
|
|
239
|
+
.panel { stroke-width: 4; }
|
|
240
|
+
.toneLeft { fill: url(#panel-left); stroke: #ef4444; }
|
|
241
|
+
.toneRight { fill: url(#panel-right); stroke: #2563eb; }
|
|
242
|
+
.metric { fill: #020617a8; stroke-width: 1.8; }
|
|
243
|
+
.metric.toneLeft { stroke: #ef4444; }
|
|
244
|
+
.metric.toneRight { stroke: #2563eb; }
|
|
245
|
+
</style>
|
|
246
|
+
</defs>
|
|
247
|
+
<rect width="1200" height="640" fill="url(#bg)"/>
|
|
248
|
+
<text x="72" y="72" class="title">@simulatte/webgpu package snapshot</text>
|
|
249
|
+
<text x="72" y="102" class="subtitle">Derived from bench/out/cube/latest/cube.summary.json | latest populated cell ${escapeXml(generatedAt)}</text>
|
|
250
|
+
<text x="72" y="128" class="subtitle">Package-surface evidence only. Backend-native strict claim lanes remain separate.</text>
|
|
251
|
+
${renderSurfaceCard(SURFACE_SPECS[0], nodeCells, 72)}
|
|
252
|
+
${renderSurfaceCard(SURFACE_SPECS[1], bunCells, 640)}
|
|
253
|
+
<text x="72" y="590" class="foot">Generated by nursery/webgpu/scripts/generate-readme-assets.js.</text>
|
|
254
|
+
<text x="72" y="612" class="foot">Static claim and comparability card from the package-surface cube. It is not a substitute for strict backend reports.</text>
|
|
255
|
+
</svg>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function main() {
|
|
260
|
+
const summary = readCubeSummary(CUBE_SUMMARY_PATH);
|
|
261
|
+
const svg = renderSvg(summary);
|
|
262
|
+
mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
|
263
|
+
writeFileSync(
|
|
264
|
+
OUTPUT_PATH,
|
|
265
|
+
`<!-- Generated by scripts/generate-readme-assets.js. Do not edit by hand. -->\n${svg}`,
|
|
266
|
+
);
|
|
267
|
+
console.log(`Wrote ${OUTPUT_PATH}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
main();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prebuild-aware install script.
|
|
3
|
+
// Uses prebuilt binaries when available; falls back to node-gyp for contributors.
|
|
4
|
+
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { resolve, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
12
|
+
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
const arch = process.arch;
|
|
15
|
+
const prebuildDir = resolve(PACKAGE_ROOT, 'prebuilds', `${platform}-${arch}`);
|
|
16
|
+
const addonPath = resolve(prebuildDir, 'doe_napi.node');
|
|
17
|
+
|
|
18
|
+
if (existsSync(addonPath)) {
|
|
19
|
+
console.log(`@simulatte/webgpu: using prebuilt binary for ${platform}-${arch}`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// No prebuild — compile from source.
|
|
24
|
+
console.log(`@simulatte/webgpu: no prebuild for ${platform}-${arch}, compiling from source...`);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
execFileSync('node-gyp', ['rebuild'], {
|
|
28
|
+
cwd: PACKAGE_ROOT,
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error('@simulatte/webgpu: native addon build failed.');
|
|
33
|
+
console.error('Ensure you have a C compiler and node-gyp prerequisites installed.');
|
|
34
|
+
console.error('See https://github.com/nodejs/node-gyp#installation');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build and package prebuilt native artifacts for the current platform.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/prebuild.js [--zig-out PATH]
|
|
6
|
+
//
|
|
7
|
+
// Produces:
|
|
8
|
+
// prebuilds/<platform>-<arch>/
|
|
9
|
+
// doe_napi.node N-API addon
|
|
10
|
+
// libwebgpu_doe.<ext> Doe drop-in WebGPU library
|
|
11
|
+
// libwebgpu_dawn.<ext> Dawn sidecar (required by Doe for proc resolution)
|
|
12
|
+
// metadata.json Integrity manifest
|
|
13
|
+
//
|
|
14
|
+
// Prerequisites:
|
|
15
|
+
// 1. node-gyp rebuild (or existing build/Release/doe_napi.node)
|
|
16
|
+
// 2. zig build dropin (or existing zig-out/lib artifacts)
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import { parseArgs } from 'node:util';
|
|
24
|
+
import { readDoeBuildMetadataFile } from '../src/build_metadata.js';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
28
|
+
const WORKSPACE_ROOT = resolve(PACKAGE_ROOT, '..', '..');
|
|
29
|
+
|
|
30
|
+
const { values: args } = parseArgs({
|
|
31
|
+
options: {
|
|
32
|
+
'zig-out': { type: 'string', default: '' },
|
|
33
|
+
'skip-addon-build': { type: 'boolean', default: false },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const platform = process.platform;
|
|
38
|
+
const arch = process.arch;
|
|
39
|
+
const ext = platform === 'darwin' ? 'dylib' : platform === 'win32' ? 'dll' : 'so';
|
|
40
|
+
|
|
41
|
+
const zigOutLib = args['zig-out']
|
|
42
|
+
? resolve(args['zig-out'], 'lib')
|
|
43
|
+
: resolve(WORKSPACE_ROOT, 'zig', 'zig-out', 'lib');
|
|
44
|
+
const zigOutShare = args['zig-out']
|
|
45
|
+
? resolve(args['zig-out'], 'share')
|
|
46
|
+
: resolve(WORKSPACE_ROOT, 'zig', 'zig-out', 'share');
|
|
47
|
+
|
|
48
|
+
const prebuildDir = resolve(PACKAGE_ROOT, 'prebuilds', `${platform}-${arch}`);
|
|
49
|
+
|
|
50
|
+
function sha256(filePath) {
|
|
51
|
+
const data = readFileSync(filePath);
|
|
52
|
+
return createHash('sha256').update(data).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function copyArtifact(src, destName) {
|
|
56
|
+
if (!existsSync(src)) {
|
|
57
|
+
console.error(`Missing: ${src}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const dest = resolve(prebuildDir, destName);
|
|
61
|
+
copyFileSync(src, dest);
|
|
62
|
+
console.log(` ${destName} <- ${src}`);
|
|
63
|
+
return { name: destName, sha256: sha256(dest) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 1. Build addon if needed.
|
|
67
|
+
const addonSrc = resolve(PACKAGE_ROOT, 'build', 'Release', 'doe_napi.node');
|
|
68
|
+
if (!args['skip-addon-build'] || !existsSync(addonSrc)) {
|
|
69
|
+
console.log('Building native addon...');
|
|
70
|
+
execFileSync('node-gyp', ['rebuild'], { cwd: PACKAGE_ROOT, stdio: 'inherit' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!existsSync(addonSrc)) {
|
|
74
|
+
console.error(`Addon not found at ${addonSrc}. node-gyp rebuild may have failed.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Locate Doe library.
|
|
79
|
+
const doeLib = resolve(zigOutLib, `libwebgpu_doe.${ext}`);
|
|
80
|
+
if (!existsSync(doeLib)) {
|
|
81
|
+
console.error(`Doe library not found at ${doeLib}.`);
|
|
82
|
+
console.error('Run: cd zig && zig build dropin');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Locate Dawn sidecar.
|
|
87
|
+
const SIDECAR_CANDIDATES = {
|
|
88
|
+
darwin: ['libwebgpu_dawn.dylib', 'libwebgpu.dylib'],
|
|
89
|
+
linux: ['libwebgpu_dawn.so', 'libwebgpu.so'],
|
|
90
|
+
win32: ['webgpu_dawn.dll', 'webgpu.dll'],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const candidates = SIDECAR_CANDIDATES[platform] || SIDECAR_CANDIDATES.linux;
|
|
94
|
+
let sidecarSrc = null;
|
|
95
|
+
let sidecarName = null;
|
|
96
|
+
for (const name of candidates) {
|
|
97
|
+
const candidate = resolve(zigOutLib, name);
|
|
98
|
+
if (existsSync(candidate)) {
|
|
99
|
+
sidecarSrc = candidate;
|
|
100
|
+
sidecarName = name;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!sidecarSrc) {
|
|
106
|
+
console.error(`Dawn sidecar not found in ${zigOutLib}. Expected one of: ${candidates.join(', ')}`);
|
|
107
|
+
console.error('Run: cd zig && zig build dropin (with Dawn sidecar available)');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const zigBuildMetadataPath = resolve(zigOutShare, 'doe-build-metadata.json');
|
|
112
|
+
const doeBuild = readDoeBuildMetadataFile(zigBuildMetadataPath);
|
|
113
|
+
if (!doeBuild) {
|
|
114
|
+
console.error(`Doe build metadata not found or invalid at ${zigBuildMetadataPath}.`);
|
|
115
|
+
console.error('Run: cd zig && zig build dropin [ -Dlean-verified=true ]');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 4. Assemble prebuild directory.
|
|
120
|
+
mkdirSync(prebuildDir, { recursive: true });
|
|
121
|
+
console.log(`\nAssembling prebuilds/${platform}-${arch}/`);
|
|
122
|
+
|
|
123
|
+
const files = {};
|
|
124
|
+
const addonEntry = copyArtifact(addonSrc, 'doe_napi.node');
|
|
125
|
+
if (addonEntry) files[addonEntry.name] = { sha256: addonEntry.sha256 };
|
|
126
|
+
|
|
127
|
+
const doeEntry = copyArtifact(doeLib, `libwebgpu_doe.${ext}`);
|
|
128
|
+
if (doeEntry) files[doeEntry.name] = { sha256: doeEntry.sha256 };
|
|
129
|
+
|
|
130
|
+
const sidecarEntry = copyArtifact(sidecarSrc, sidecarName);
|
|
131
|
+
if (sidecarEntry) files[sidecarEntry.name] = { sha256: sidecarEntry.sha256 };
|
|
132
|
+
|
|
133
|
+
// 5. Write metadata manifest.
|
|
134
|
+
const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
135
|
+
let doeVersion = 'unknown';
|
|
136
|
+
try {
|
|
137
|
+
doeVersion = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
138
|
+
cwd: WORKSPACE_ROOT,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
}).trim();
|
|
141
|
+
} catch { /* ignore */ }
|
|
142
|
+
|
|
143
|
+
const metadata = {
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
package: pkg.name,
|
|
146
|
+
packageVersion: pkg.version,
|
|
147
|
+
platform,
|
|
148
|
+
arch,
|
|
149
|
+
nodeNapiVersion: 8,
|
|
150
|
+
doeVersion,
|
|
151
|
+
doeBuild: {
|
|
152
|
+
artifact: 'libwebgpu_doe',
|
|
153
|
+
leanVerifiedBuild: doeBuild.leanVerifiedBuild,
|
|
154
|
+
proofArtifactSha256: doeBuild.proofArtifactSha256,
|
|
155
|
+
},
|
|
156
|
+
files,
|
|
157
|
+
builtAt: new Date().toISOString(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const metadataPath = resolve(prebuildDir, 'metadata.json');
|
|
161
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
162
|
+
console.log(` metadata.json`);
|
|
163
|
+
|
|
164
|
+
// macOS: ad-hoc sign dylibs for distribution.
|
|
165
|
+
if (platform === 'darwin') {
|
|
166
|
+
console.log('\nSigning dylibs (ad-hoc)...');
|
|
167
|
+
for (const name of Object.keys(files)) {
|
|
168
|
+
if (name.endsWith('.dylib')) {
|
|
169
|
+
try {
|
|
170
|
+
execFileSync('codesign', ['-s', '-', resolve(prebuildDir, name)], { stdio: 'inherit' });
|
|
171
|
+
} catch {
|
|
172
|
+
console.warn(` Warning: codesign failed for ${name} (may already be signed)`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`\nDone. Prebuild artifacts in prebuilds/${platform}-${arch}/`);
|
|
179
|
+
console.log(`Total files: ${Object.keys(files).length}`);
|