@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Clean-machine smoke test for @simulatte/webgpu.
|
|
3
|
+
// Verifies the package loads, finds native artifacts, and can request a GPU device.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node scripts/smoke-test.js
|
|
7
|
+
//
|
|
8
|
+
// Exit codes:
|
|
9
|
+
// 0 All checks passed
|
|
10
|
+
// 1 A check failed (actionable error printed)
|
|
11
|
+
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
|
|
20
|
+
function check(label, condition, detail) {
|
|
21
|
+
if (condition) {
|
|
22
|
+
passed++;
|
|
23
|
+
console.log(` ok: ${label}`);
|
|
24
|
+
} else {
|
|
25
|
+
failed++;
|
|
26
|
+
console.error(` FAIL: ${label}${detail ? ' — ' + detail : ''}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('=== @simulatte/webgpu smoke test ===\n');
|
|
31
|
+
|
|
32
|
+
// 1. Import the package.
|
|
33
|
+
let mod;
|
|
34
|
+
try {
|
|
35
|
+
mod = await import('../src/index.js');
|
|
36
|
+
check('import succeeds', true);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
check('import succeeds', false, err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. providerInfo shape.
|
|
43
|
+
const info = mod.providerInfo();
|
|
44
|
+
console.log('\nproviderInfo:', JSON.stringify(info, null, 2), '\n');
|
|
45
|
+
check('providerInfo.module', info.module === '@simulatte/webgpu');
|
|
46
|
+
check('providerInfo.loaded', info.loaded === true, `got ${info.loaded}`);
|
|
47
|
+
check('providerInfo.loadError empty', info.loadError === '', `got "${info.loadError}"`);
|
|
48
|
+
check('providerInfo.libraryFlavor', info.libraryFlavor === 'doe-dropin', `got "${info.libraryFlavor}"`);
|
|
49
|
+
check('providerInfo.doeNative', info.doeNative === true, `got ${info.doeNative}`);
|
|
50
|
+
check('providerInfo.buildMetadataSource string', typeof info.buildMetadataSource === 'string', `got ${typeof info.buildMetadataSource}`);
|
|
51
|
+
check('providerInfo.buildMetadataPath string', typeof info.buildMetadataPath === 'string', `got ${typeof info.buildMetadataPath}`);
|
|
52
|
+
check('providerInfo.leanVerifiedBuild boolean|null', info.leanVerifiedBuild === null || typeof info.leanVerifiedBuild === 'boolean', `got ${typeof info.leanVerifiedBuild}`);
|
|
53
|
+
check('providerInfo.proofArtifactSha256 string|null', info.proofArtifactSha256 === null || typeof info.proofArtifactSha256 === 'string', `got ${typeof info.proofArtifactSha256}`);
|
|
54
|
+
|
|
55
|
+
// 3. globals present.
|
|
56
|
+
check('globals.GPUBufferUsage', mod.globals.GPUBufferUsage != null);
|
|
57
|
+
check('globals.GPUShaderStage', mod.globals.GPUShaderStage != null);
|
|
58
|
+
|
|
59
|
+
// 4. create() returns GPU object.
|
|
60
|
+
let gpu;
|
|
61
|
+
try {
|
|
62
|
+
gpu = mod.create();
|
|
63
|
+
check('create() succeeds', gpu != null);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
check('create() succeeds', false, err.message);
|
|
66
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
67
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
68
|
+
process.exit(process.exitCode);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. requestAdapter.
|
|
72
|
+
let adapter;
|
|
73
|
+
try {
|
|
74
|
+
adapter = await gpu.requestAdapter();
|
|
75
|
+
check('requestAdapter()', adapter != null);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
check('requestAdapter()', false, err.message);
|
|
78
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
79
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
80
|
+
process.exit(process.exitCode);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 6. requestDevice.
|
|
84
|
+
let device;
|
|
85
|
+
try {
|
|
86
|
+
device = await adapter.requestDevice();
|
|
87
|
+
check('requestDevice()', device != null);
|
|
88
|
+
check('device.queue exists', device.queue != null);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
check('requestDevice()', false, err.message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 7. Basic buffer round-trip.
|
|
94
|
+
if (device) {
|
|
95
|
+
try {
|
|
96
|
+
const buf = device.createBuffer({
|
|
97
|
+
size: 64,
|
|
98
|
+
usage: mod.globals.GPUBufferUsage.COPY_DST | mod.globals.GPUBufferUsage.MAP_READ,
|
|
99
|
+
});
|
|
100
|
+
const data = new Float32Array([1.0, 2.0, 3.0, 4.0]);
|
|
101
|
+
device.queue.writeBuffer(buf, 0, data);
|
|
102
|
+
await buf.mapAsync(mod.globals.GPUMapMode.READ, 0, 16);
|
|
103
|
+
const result = new Float32Array(buf.getMappedRange(0, 16));
|
|
104
|
+
check('buffer round-trip', result[0] === 1.0 && result[3] === 4.0,
|
|
105
|
+
`got [${result[0]}, ${result[1]}, ${result[2]}, ${result[3]}]`);
|
|
106
|
+
buf.unmap();
|
|
107
|
+
buf.destroy();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
check('buffer round-trip', false, err.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (device) {
|
|
114
|
+
device.destroy();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
118
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const UNKNOWN_DOE_BUILD_METADATA = Object.freeze({
|
|
5
|
+
source: 'none',
|
|
6
|
+
path: '',
|
|
7
|
+
leanVerifiedBuild: null,
|
|
8
|
+
proofArtifactSha256: null,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function isObject(value) {
|
|
12
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeProofArtifactSha256(value) {
|
|
16
|
+
return value == null ? null : typeof value === 'string' ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseDoeBuildSidecar(json, metadataPath, source) {
|
|
20
|
+
if (!isObject(json)) return null;
|
|
21
|
+
if (json.schemaVersion !== 1) return null;
|
|
22
|
+
if (json.artifact !== 'libwebgpu_doe') return null;
|
|
23
|
+
if (typeof json.leanVerifiedBuild !== 'boolean') return null;
|
|
24
|
+
const proofArtifactSha256 = normalizeProofArtifactSha256(json.proofArtifactSha256);
|
|
25
|
+
if (proofArtifactSha256 === undefined) return null;
|
|
26
|
+
return {
|
|
27
|
+
source,
|
|
28
|
+
path: metadataPath,
|
|
29
|
+
leanVerifiedBuild: json.leanVerifiedBuild,
|
|
30
|
+
proofArtifactSha256,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parsePrebuildMetadata(json, metadataPath, source) {
|
|
35
|
+
if (!isObject(json)) return null;
|
|
36
|
+
if (json.schemaVersion !== 1) return null;
|
|
37
|
+
const doeBuild = json.doeBuild;
|
|
38
|
+
if (!isObject(doeBuild)) return null;
|
|
39
|
+
if (doeBuild.artifact !== 'libwebgpu_doe') return null;
|
|
40
|
+
if (typeof doeBuild.leanVerifiedBuild !== 'boolean') return null;
|
|
41
|
+
const proofArtifactSha256 = normalizeProofArtifactSha256(doeBuild.proofArtifactSha256);
|
|
42
|
+
if (proofArtifactSha256 === undefined) return null;
|
|
43
|
+
return {
|
|
44
|
+
source,
|
|
45
|
+
path: metadataPath,
|
|
46
|
+
leanVerifiedBuild: doeBuild.leanVerifiedBuild,
|
|
47
|
+
proofArtifactSha256,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readJsonFile(metadataPath) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(metadataPath, 'utf8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readDoeBuildMetadataFile(metadataPath) {
|
|
60
|
+
if (!metadataPath || !existsSync(metadataPath)) return null;
|
|
61
|
+
const parsed = readJsonFile(metadataPath);
|
|
62
|
+
return parseDoeBuildSidecar(parsed, metadataPath, 'zig-out');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadMetadataCandidate(metadataPath, parser, source) {
|
|
66
|
+
if (!metadataPath || !existsSync(metadataPath)) return null;
|
|
67
|
+
const parsed = readJsonFile(metadataPath);
|
|
68
|
+
return parser(parsed, metadataPath, source);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadDoeBuildMetadata({ packageRoot = '', libraryPath = '' } = {}) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const candidates = [];
|
|
74
|
+
|
|
75
|
+
const pushCandidate = (metadataPath, parser, source) => {
|
|
76
|
+
if (!metadataPath || seen.has(metadataPath)) return;
|
|
77
|
+
seen.add(metadataPath);
|
|
78
|
+
candidates.push({ metadataPath, parser, source });
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
pushCandidate(process.env.FAWN_DOE_BUILD_METADATA ?? '', parseDoeBuildSidecar, 'env');
|
|
82
|
+
|
|
83
|
+
if (libraryPath) {
|
|
84
|
+
const libraryDir = dirname(libraryPath);
|
|
85
|
+
pushCandidate(resolve(libraryDir, 'metadata.json'), parsePrebuildMetadata, 'prebuild');
|
|
86
|
+
pushCandidate(resolve(libraryDir, 'doe-build-metadata.json'), parseDoeBuildSidecar, 'adjacent');
|
|
87
|
+
pushCandidate(resolve(libraryDir, '..', 'share', 'doe-build-metadata.json'), parseDoeBuildSidecar, 'workspace');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (packageRoot) {
|
|
91
|
+
pushCandidate(
|
|
92
|
+
resolve(packageRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'metadata.json'),
|
|
93
|
+
parsePrebuildMetadata,
|
|
94
|
+
'package-prebuild',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
const parsed = loadMetadataCandidate(candidate.metadataPath, candidate.parser, candidate.source);
|
|
100
|
+
if (parsed) return parsed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return UNKNOWN_DOE_BUILD_METADATA;
|
|
104
|
+
}
|