native-dawn 0.1.0 → 0.1.2

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/CMakeLists.txt CHANGED
@@ -77,6 +77,14 @@ if(NATIVE_DAWN_NODE)
77
77
  set_target_properties(dawn_node PROPERTIES INSTALL_RPATH "@loader_path" BUILD_WITH_INSTALL_RPATH ON)
78
78
  elseif(UNIX)
79
79
  set_target_properties(dawn_node PROPERTIES INSTALL_RPATH "$ORIGIN" BUILD_WITH_INSTALL_RPATH ON)
80
+ elseif(WIN32)
81
+ # Node-API is imported from node.exe. Delay-load it and resolve it to the
82
+ # module providing Node-API in the loading process (src/node/
83
+ # win_delay_load_hook.cc), so programs that embed Node under another name
84
+ # can load the addon too, as node-gyp addons do.
85
+ target_sources(dawn_node PRIVATE src/node/win_delay_load_hook.cc)
86
+ target_link_options(dawn_node PRIVATE "/DELAYLOAD:node.exe")
87
+ target_link_libraries(dawn_node PRIVATE delayimp)
80
88
  endif()
81
89
 
82
90
  # The addon sits next to the shared library: lib/ on Linux and macOS, bin/ on
package/README.md CHANGED
@@ -5,7 +5,7 @@ Prebuilt [Dawn](https://dawn.googlesource.com/dawn), Google's WebGPU implementat
5
5
  - **Native code.** A shared `webgpu_dawn` library, the WebGPU C and C++ headers, and a CMake package, so a C or C++ program can use WebGPU without building Dawn.
6
6
  - **Node.js.** Dawn's own Node-API bindings, built against that same library, plus a swap chain for native windows.
7
7
 
8
- It is the WebGPU counterpart of [native-gles](https://github.com/monteslu/native-gles): the native layer, kept separate from the browser-style JavaScript API that sits on top of it (webgpu-node, as [webgl-node](https://github.com/monteslu/webgl-node) is for native-gles).
8
+ It is the WebGPU counterpart of [native-gles](https://github.com/monteslu/native-gles): the native layer, kept separate from the browser-style JavaScript API that sits on top of it, [webgpu-node](https://github.com/monteslu/webgpu-node), as [webgl-node](https://github.com/monteslu/webgl-node) is for native-gles. If you want `navigator.gpu`, a canvas, or to run Three.js and other browser WebGPU code in Node, install webgpu-node; it depends on this package and installs it for you.
9
9
 
10
10
  Because the Node addon links the shared library instead of carrying its own copy of Dawn, JavaScript and native code in one process use the same Dawn. A `GPUDevice` created in JavaScript can be handed to a C++ addon as a `WGPUDevice`.
11
11
 
@@ -15,19 +15,29 @@ Because the Node addon links the shared library instead of carrying its own copy
15
15
  npm install native-dawn
16
16
  ```
17
17
 
18
- Installation downloads the archive for your platform from this repository's GitHub releases, checks its SHA-256 and version, and loads the addon once to make sure it works.
18
+ Installation downloads the archive for your platform from this repository's GitHub releases, checks its SHA-256 and version, and loads the addon once to make sure it works. If that fails (an unsupported platform, no network, a C library too old for the binary), the install prints a warning and still succeeds, so packages that depend on native-dawn install everywhere; importing native-dawn then throws. A corrupt or mismatched archive is never installed. Set `NATIVE_DAWN_STRICT_INSTALL=1` to make install failures fatal.
19
19
 
20
20
  | Platform | Architectures | Backend |
21
21
  | --- | --- | --- |
22
- | Linux (glibc; release builds run on Ubuntu 22.04 and newer) | x64, arm64 | Vulkan |
22
+ | Linux (glibc 2.34+: Ubuntu 22.04, Debian 12 and newer) | x64, arm64 | Vulkan |
23
23
  | macOS 11+ | x64, arm64 | Metal |
24
24
  | Windows | x64, arm64 | D3D12 |
25
25
  | Android 8.0+ (API 26), C SDK only | arm64 | Vulkan, OpenGL ES |
26
26
 
27
27
  Linux builds support X11 and Wayland windows and also include the OpenGL ES backend (see [OpenGL ES](#opengl-es-compatibility-mode)). The Node addon needs Node.js 22 or newer.
28
28
 
29
+ The addon also loads into programs that embed Node, statically or as `libnode.dll`. On Linux and macOS it finds Node-API in the process that loads it. On Windows it delay-loads Node-API from `node.exe` and resolves that to the loading program, as node-gyp addons do, so a program under another name only has to export the Node-API functions.
30
+
29
31
  ## Using it from Node
30
32
 
33
+ This package is the low-level layer: Dawn's WebGPU objects and a window swap chain. For the browser-style API (`navigator.gpu`, a canvas and `GPUCanvasContext`, `installGlobals()` for libraries such as Three.js, and SDL window presentation), use [webgpu-node](https://github.com/monteslu/webgpu-node) instead:
34
+
35
+ ```sh
36
+ npm install webgpu-node # installs native-dawn too
37
+ ```
38
+
39
+ To use native-dawn directly:
40
+
31
41
  ```js
32
42
  import { create, globals } from 'native-dawn'
33
43
 
@@ -38,7 +48,7 @@ const device = await adapter.requestDevice()
38
48
  const buffer = device.createBuffer({ size: 16, usage: globals.GPUBufferUsage.COPY_DST | globals.GPUBufferUsage.MAP_READ })
39
49
  ```
40
50
 
41
- `gpu`, `adapter` and `device` are standard WebGPU objects. `globals` holds the WebGPU classes and constants (`GPUBufferUsage`, `GPUDevice`, `GPUValidationError`, ...). Nothing is installed on `globalThis` or `navigator`; webgpu-node does that, along with a canvas and `GPUCanvasContext`.
51
+ `gpu`, `adapter` and `device` are standard WebGPU objects. `globals` holds the WebGPU classes and constants (`GPUBufferUsage`, `GPUDevice`, `GPUValidationError`, ...). Nothing is installed on `globalThis` or `navigator`; [webgpu-node](https://github.com/monteslu/webgpu-node) does that, along with a canvas and `GPUCanvasContext`.
42
52
 
43
53
  Adapters and devices stay valid even if the `gpu` object is garbage collected. Call `device.destroy()` when you are done to release GPU memory promptly. Only pending asynchronous work (a `mapAsync`, `onSubmittedWorkDone`, pipeline creation and the like) keeps Node running; an idle device does not, and it costs no CPU. Dawn notices a lost device (a driver reset, for example) on the next WebGPU call or asynchronous operation, and `device.lost` settles then.
44
54
 
@@ -229,6 +239,7 @@ Environment variables:
229
239
  | `NATIVE_DAWN_BUILD_FROM_SOURCE=1` | Make `npm install` build instead of downloading |
230
240
  | `NATIVE_DAWN_BINARY=/path/to/archive.tar.gz` | Install a local archive (with its `.sha256` beside it) |
231
241
  | `NATIVE_DAWN_SKIP_INSTALL=1` | Skip the install step entirely |
242
+ | `NATIVE_DAWN_STRICT_INSTALL=1` | Fail `npm install` when no native build can be installed |
232
243
 
233
244
  ## Tests
234
245
 
@@ -246,7 +257,7 @@ Tests need a working adapter, hardware or software (Mesa's lavapipe on Linux, WA
246
257
 
247
258
  ## CI and releases
248
259
 
249
- [CI](.github/workflows/ci.yml) builds the six desktop targets on native runners and runs every suite above on each one, on Node 22 and 24, against the runner's software adapter. Linux also runs the C tests on OpenGL ES in compatibility mode. The window tests run everywhere except Windows ARM64, which has no @kmamal/sdl build. Android is cross-compiled on Linux and its C tests run on an x86_64 Android emulator, through Android's ARM translation.
260
+ [CI](.github/workflows/ci.yml) builds the six desktop targets on native runners and runs every suite above on each one, on Node 22 and 24, against the runner's software adapter. Linux also runs the C tests on OpenGL ES in compatibility mode. The window tests run everywhere except Windows ARM64, which has no @kmamal/sdl build. On Windows, CI also checks that the addon delay-loads `node.exe` and resolves Node-API from a test program that is not `node.exe` (`npm run test:win-host`). Android is cross-compiled on Linux and its C tests run on an x86_64 Android emulator, through Android's ARM translation.
250
261
 
251
262
  Pushing a `v<version>` tag that matches `package.json` runs the same jobs, and if all seven pass, creates a GitHub release with the seven archives and their checksums. npm publishing is a separate, manual step after that, since `npm install` downloads from the release.
252
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-dawn",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Prebuilt Dawn WebGPU for Node.js and native code: shared library, C headers, and Node-API bindings",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -45,6 +45,7 @@
45
45
  "test:c": "node scripts/test-c.mjs",
46
46
  "test:window": "node test/window.mjs",
47
47
  "test:package": "node scripts/test-package.mjs",
48
+ "test:win-host": "node scripts/test-win-host.mjs",
48
49
  "package:binary": "node scripts/package-binary.mjs",
49
50
  "test:types": "tsc -p test/tsconfig.json",
50
51
  "test:android": "node scripts/test-android.mjs"
@@ -69,9 +69,14 @@ export async function install() {
69
69
  }
70
70
  }
71
71
 
72
+ // A failed install leaves native-dawn without a native build but never fails
73
+ // the surrounding npm install: packages that depend on it (webgpu-node, and
74
+ // through it wasmcart) must still install on platforms native-dawn does not
75
+ // cover, and only error when WebGPU is actually used. A bad archive is never
76
+ // installed either way. NATIVE_DAWN_STRICT_INSTALL=1 makes failures fatal.
72
77
  try {
73
78
  await install()
74
79
  } catch (error) {
75
- console.error(`native-dawn: ${error.message}\nTo build from source instead, install the toolchain in README.md and set NATIVE_DAWN_BUILD_FROM_SOURCE=1.`)
76
- process.exitCode = 1
80
+ console.warn(`native-dawn: no native build installed: ${error.message}\nWebGPU will not be available on this machine; importing native-dawn will throw. To build from source, install the toolchain in README.md and set NATIVE_DAWN_BUILD_FROM_SOURCE=1.`)
81
+ if (process.env.NATIVE_DAWN_STRICT_INSTALL === '1') process.exitCode = 1
77
82
  }
package/scripts/pe.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // Reads the import, delay-load import and export tables of a Windows PE file
2
+ // (PE32 or PE32+), for checks that must not depend on dumpbin's text format.
3
+ import fs from 'node:fs'
4
+
5
+ /**
6
+ * @param {string} file
7
+ * @returns {{ imports: Map<string, string[]>, delayImports: Map<string, string[]>, exports: string[] }}
8
+ * DLL names are as written in the file; functions imported by ordinal are '#<n>'.
9
+ */
10
+ export function readPe(file) {
11
+ const b = fs.readFileSync(file)
12
+ if (b.readUInt16LE(0) !== 0x5a4d) throw new Error(`${file}: not a PE file`)
13
+ const pe = b.readUInt32LE(0x3c)
14
+ if (b.readUInt32LE(pe) !== 0x4550) throw new Error(`${file}: no PE signature`)
15
+ const sectionCount = b.readUInt16LE(pe + 6)
16
+ const optional = pe + 24
17
+ const pe32plus = b.readUInt16LE(optional) === 0x20b
18
+ const dirs = optional + (pe32plus ? 112 : 96)
19
+ const dir = i => ({ rva: b.readUInt32LE(dirs + i * 8), size: b.readUInt32LE(dirs + i * 8 + 4) })
20
+ const sectionTable = optional + b.readUInt16LE(pe + 20)
21
+ const sections = []
22
+ for (let i = 0; i < sectionCount; i++) {
23
+ const s = sectionTable + i * 40
24
+ sections.push({ va: b.readUInt32LE(s + 12), vsize: Math.max(b.readUInt32LE(s + 8), b.readUInt32LE(s + 16)), raw: b.readUInt32LE(s + 20) })
25
+ }
26
+ const off = rva => {
27
+ const s = sections.find(s => rva >= s.va && rva < s.va + s.vsize)
28
+ if (!s) throw new Error(`${file}: RVA 0x${rva.toString(16)} is in no section`)
29
+ return rva - s.va + s.raw
30
+ }
31
+ const str = rva => { const o = off(rva); return b.toString('latin1', o, b.indexOf(0, o)) }
32
+ const thunkSize = pe32plus ? 8 : 4
33
+ const names = tableRva => {
34
+ const out = []
35
+ for (let o = off(tableRva); ; o += thunkSize) {
36
+ const t = pe32plus ? b.readBigUInt64LE(o) : BigInt(b.readUInt32LE(o))
37
+ if (t === 0n) break
38
+ const byOrdinal = pe32plus ? t >> 63n : t >> 31n
39
+ out.push(byOrdinal ? `#${Number(t & 0xffffn)}` : str(Number(t & 0x7fffffffn) + 2))
40
+ }
41
+ return out
42
+ }
43
+
44
+ const imports = new Map()
45
+ const imp = dir(1)
46
+ if (imp.rva) {
47
+ for (let o = off(imp.rva); b.readUInt32LE(o + 12); o += 20) {
48
+ imports.set(str(b.readUInt32LE(o + 12)), names(b.readUInt32LE(o) || b.readUInt32LE(o + 16)))
49
+ }
50
+ }
51
+ const delayImports = new Map()
52
+ const delay = dir(13)
53
+ if (delay.rva) {
54
+ for (let o = off(delay.rva); b.readUInt32LE(o + 4); o += 32) {
55
+ delayImports.set(str(b.readUInt32LE(o + 4)), names(b.readUInt32LE(o + 16)))
56
+ }
57
+ }
58
+ const exports = []
59
+ const exp = dir(0)
60
+ if (exp.rva) {
61
+ const o = off(exp.rva)
62
+ const count = b.readUInt32LE(o + 24)
63
+ const nameTable = off(b.readUInt32LE(o + 32))
64
+ for (let i = 0; i < count; i++) exports.push(str(b.readUInt32LE(nameTable + i * 4)))
65
+ }
66
+ return { imports, delayImports, exports }
67
+ }
68
+
69
+ /** The map key for a DLL name, matched case-insensitively as Windows does. */
70
+ export function findDll(map, name) {
71
+ for (const [key, value] of map) if (key.toLowerCase() === name.toLowerCase()) return value
72
+ return null
73
+ }
@@ -0,0 +1,75 @@
1
+ // Windows: dawn.node loads into node.exe and into a program that only
2
+ // exports Node-API (as one that embeds Node under another name does).
3
+ //
4
+ // 1. Its import table: Node-API comes from node.exe, delay-loaded, and nothing
5
+ // is imported from node.exe the ordinary way.
6
+ // 2. node.exe loads it.
7
+ // 3. A host built here (not node.exe, no Node in it) exports every function
8
+ // dawn.node takes from node.exe, each a stub that reports its name and ends
9
+ // the process. Calling the addon's napi_register_module_v1 makes its first
10
+ // Node-API call, which the delay-load hook must resolve to that host. Without
11
+ // the hook the delay-load helper looks for a node.exe file and the host dies
12
+ // with an exception (or reaches a node.exe on PATH, which is not the host).
13
+ //
14
+ // Run from an MSVC developer environment (cl.exe), after `npm run build`.
15
+ import fs from 'node:fs'
16
+ import path from 'node:path'
17
+ import { spawnSync } from 'node:child_process'
18
+ import { root, target, distDir, run } from './common.mjs'
19
+ import { readPe, findDll } from './pe.mjs'
20
+
21
+ if (!target.startsWith('win32-')) throw new Error(`test-win-host is for win32 targets, not ${target}`)
22
+ const addon = path.join(distDir, 'bin', 'dawn.node')
23
+
24
+ const { imports, delayImports } = readPe(addon)
25
+ if (findDll(imports, 'node.exe')) throw new Error('dawn.node imports node.exe directly; it must be delay-loaded (/DELAYLOAD:node.exe)')
26
+ const napi = findDll(delayImports, 'node.exe')
27
+ if (!napi?.length) throw new Error(`dawn.node does not delay-load node.exe (delay-loaded: ${[...delayImports.keys()].join(', ') || 'nothing'})`)
28
+ const bad = napi.filter(n => !/^(napi|node_api)_[a-z0-9_]+$/.test(n))
29
+ if (bad.length) throw new Error(`dawn.node takes more than Node-API from node.exe: ${bad.join(', ')}`)
30
+ console.log(`dawn.node delay-loads ${napi.length} Node-API functions from node.exe`)
31
+
32
+ const node = spawnSync(process.execPath, ['-e', `
33
+ const dawn = require(${JSON.stringify(addon)});
34
+ if (typeof dawn.create !== 'function') throw new Error('dawn.node has no create()');
35
+ `], { encoding: 'utf8' })
36
+ if (node.status !== 0) throw new Error(`node.exe could not load dawn.node:\n${node.stdout}${node.stderr}`)
37
+ console.log('node.exe loads dawn.node')
38
+
39
+ const dir = path.join(root, 'build', 'win-host-test')
40
+ fs.rmSync(dir, { recursive: true, force: true })
41
+ fs.mkdirSync(dir, { recursive: true })
42
+ fs.writeFileSync(path.join(dir, 'host.c'), `// Generated by scripts/test-win-host.mjs
43
+ #define WIN32_LEAN_AND_MEAN
44
+ #include <windows.h>
45
+ #include <stdio.h>
46
+
47
+ static void reached(const char* name) {
48
+ printf("first Node-API call resolved to the host: %s\\n", name);
49
+ fflush(stdout);
50
+ TerminateProcess(GetCurrentProcess(), 0);
51
+ }
52
+ ${napi.map(n => `__declspec(dllexport) void* ${n}(void) { reached("${n}"); return 0; }`).join('\n')}
53
+
54
+ typedef void* (*register_fn)(void* env, void* exports);
55
+
56
+ int wmain(int argc, wchar_t** argv) {
57
+ if (argc != 2) return 2;
58
+ HMODULE addon = LoadLibraryExW(argv[1], NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
59
+ if (!addon) { printf("LoadLibraryExW failed: %lu\\n", GetLastError()); return 1; }
60
+ register_fn reg = (register_fn)GetProcAddress(addon, "napi_register_module_v1");
61
+ if (!reg) { printf("no napi_register_module_v1\\n"); return 1; }
62
+ reg((void*)1, (void*)2);
63
+ printf("napi_register_module_v1 returned without calling Node-API\\n");
64
+ return 1;
65
+ }
66
+ `)
67
+ run('cl', ['/nologo', '/W3', 'host.c', '/Fe:host.exe'], { cwd: dir })
68
+ const host = spawnSync(path.join(dir, 'host.exe'), [addon], { encoding: 'utf8' })
69
+ const out = `${host.stdout}${host.stderr}`
70
+ if (host.status !== 0 || !out.includes('resolved to the host')) {
71
+ const code = host.status === null ? host.signal : `0x${(host.status >>> 0).toString(16)}`
72
+ throw new Error(`dawn.node did not resolve Node-API from a host that is not node.exe (exit ${code}):\n${out}`)
73
+ }
74
+ console.log(out.trim())
75
+ fs.rmSync(dir, { recursive: true, force: true })
@@ -0,0 +1,36 @@
1
+ // Windows: lets dawn.node load into any program that exports Node-API, not
2
+ // only one named node.exe.
3
+ //
4
+ // The addon's Node-API imports name the module node.exe (Dawn's generated
5
+ // NapiSymbols.lib), and CMakeLists.txt links them with /DELAYLOAD:node.exe.
6
+ // When the first one is resolved, this hook answers the load of node.exe with
7
+ // the module that already provides Node-API in this process: libnode.dll when
8
+ // a program embeds Node through it, otherwise the program itself (node.exe,
9
+ // Electron, or an executable with libnode linked in that exports the
10
+ // napi_* symbols). This is the mechanism node-gyp builds into every addon
11
+ // (its win_delay_load_hook.cc).
12
+
13
+ #ifdef _MSC_VER
14
+
15
+ #ifndef WIN32_LEAN_AND_MEAN
16
+ #define WIN32_LEAN_AND_MEAN
17
+ #endif
18
+ #include <windows.h>
19
+
20
+ #include <delayimp.h>
21
+ #include <string.h>
22
+
23
+ static FARPROC WINAPI native_dawn_load_host(unsigned int event, DelayLoadInfo* info) {
24
+ if (event != dliNotePreLoadLibrary || _stricmp(info->szDll, "node.exe") != 0) {
25
+ return nullptr;
26
+ }
27
+ HMODULE host = GetModuleHandleA("libnode.dll");
28
+ if (host == nullptr) {
29
+ host = GetModuleHandleA(nullptr);
30
+ }
31
+ return reinterpret_cast<FARPROC>(host);
32
+ }
33
+
34
+ decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = native_dawn_load_host;
35
+
36
+ #endif // _MSC_VER