@octane-xplat/shaders 0.0.1 → 0.11.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 ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @octane-xplat/shaders
2
+
3
+ Thin adapter that runs the published [`shaders`](https://www.npmjs.com/package/shaders)
4
+ WebGPU engine (pinned `4.0.0`, used unchanged — no vendoring, no shader port) on an
5
+ Octane Xplat `Canvas` surface from `@octane-xplat/canvas`.
6
+
7
+ The package deliberately does not create a new shader API. Preset shapes, effect
8
+ names, parameters, options, and the returned instance's methods are upstream's:
9
+ `update(id, props)`, `resize()`, `pause()`, `resume()`, `destroy()`,
10
+ `getFailureReason()`. The adapter only adds the surface/lifecycle glue Xplat needs.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Canvas } from '@octane-xplat/canvas'
16
+ import { createShader } from '@octane-xplat/shaders'
17
+
18
+ <Canvas
19
+ width={320}
20
+ height={180}
21
+ style={{ width: 320, height: 180 }}
22
+ onReady={({ canvas }) => {
23
+ void createShader(canvas, {
24
+ components: [
25
+ {
26
+ id: 'background',
27
+ type: 'SimplexNoise',
28
+ props: { colorA: '#0f172a', colorB: '#7c3aed', scale: 2, seed: 7, speed: 0.5 },
29
+ },
30
+ ],
31
+ })
32
+ }}
33
+ />
34
+ ```
35
+
36
+ Working demo: `ShaderDemo` in `packages/demos` (parameter updates, pause/resume,
37
+ static fallback, reduced-motion still frame, disposal on unmount).
38
+
39
+ ## What the adapter adds
40
+
41
+ - `createShader(canvas, preset, options?)` — same signature as upstream
42
+ `shaders/js`'s `createShader`, except `canvas` is the Xplat Canvas surface
43
+ (`CanvasReady.canvas`) and options add `signal` (abort deferred/in-flight init;
44
+ a late completion is destroyed, never revived).
45
+ - `disableTelemetry` defaults to `true`. Upstream telemetry stays off unless you
46
+ pass `disableTelemetry: false`. No hosted account, preset download, or network
47
+ access is required for local presets.
48
+ - Deferred init: until the surface reports a positive size, nothing touches the
49
+ GPU. The static content behind your canvas stays put.
50
+ - Host suspension: app backgrounding (NativeScript `suspend`/`resume`), surface
51
+ detach (`loaded`/`unloaded`), and `document` visibility (web) pause rendering —
52
+ without overriding an explicit caller `pause()` when the host returns.
53
+ - `createSharedDevice()` — acquire `{ device, adapter }` through the platform
54
+ entry point (`navigator.gpu` on web, the `@nativescript/canvas` GPU shim on
55
+ native) for callers that want to own/share the device via `options.gpu`.
56
+ Instances never destroy a caller-provided or adapter-shared device.
57
+ - `isWebGPUSupported()` / `getWebGPUSupport()` — upstream capability helpers,
58
+ native-aware.
59
+ - `prefersReducedMotion()` — OS reduced-motion preference; pause after the first
60
+ frame to hold a still, or keep the static fallback.
61
+
62
+ ## Target status
63
+
64
+ | Target | Status |
65
+ | --- | --- |
66
+ | Web (DOM) | Implemented. Upstream engine on the browser WebGPU canvas; reached ready + submissions in the headless-Chromium spike (SwiftShader). Hardware GPU correctness, sustained lifecycle, and visual output are unverified — a SwiftShader device-loss observed in the spike was reproduced without the library (raw WebGPU loop) and is attributed to the headless backend, not the package. |
67
+ | NativeScript iOS | Implemented, experimental. `@nativescript/canvas` WebGPU (wgpu over Metal). Build/bundle/type verified; device execution pending. |
68
+ | NativeScript Android | Implemented, experimental. Same native route over Vulkan. Build/type verified; device/driver execution pending. |
69
+ | AppKit macOS / desktop native | Explicitly unsupported. Importing the package is inert (no GPU work, no engine load); `createShader` resolves to a no-op instance with `getFailureReason() === 'unsupported'` so fallback handling is identical. |
70
+ | Web code inside a desktop webview | Follows the browser's own WebGPU support; no separate integration. |
71
+
72
+ Adapter-owned native defaults: `navigator.gpu` is bound lazily to the
73
+ `@nativescript/canvas` GPU shim on first use (upstream reads it unconditionally);
74
+ `observeElement` defaults off on native (no DOM observers — the view's
75
+ `layoutChanged` drives `resize()`); frame pacing uses `requestAnimationFrame`
76
+ where the runtime provides it, else upstream's `setTimeout(~16ms)` fallback.
77
+
78
+ ## Limits to know
79
+
80
+ - **Payload:** the engine is heavy — about **709 KB gzip** (minified browser
81
+ bundle for the JS host + SimplexNoise; ~705 KB via `shaders/core`). Per-effect
82
+ imports do not shrink it because the core renderer pulls the full registry.
83
+ This cost lands only in apps that install this optional package.
84
+ - **Upstream packaging defects:** `shaders@4.0.0` declares 34 export subpaths
85
+ that point to absent files (e.g. `./core/RadialLines`, `./core/GradientNoise`).
86
+ `shaders/js`, `shaders/core`, and `shaders/core/SimplexNoise` are intact. Treat
87
+ other subpaths as unverified until you check the file exists. Upstream `.d.ts`
88
+ files also use extensionless relative imports, so they do not resolve under
89
+ `moduleResolution: 'nodenext'` — consumers need bundler-style resolution (all
90
+ Xplat toolchain paths use it).
91
+ - **Catalog coverage:** only intact procedural effect definitions without
92
+ browser-only inputs are in the initial native compatibility claim. DOM capture,
93
+ HTML/text-specific facilities, cursor/mouse-driven effects, media sources,
94
+ hosted presets, and simulations may fail on native — upstream reports terminal
95
+ failures via `onError`/`getFailureReason()`; keep the static fallback mounted.
96
+ - **Recovery:** device loss uses upstream's own rebuild path (≤3 attempts); there
97
+ is no adapter-level retry loop. Terminal reasons stop rendering permanently.
98
+
99
+ ## Publishing
100
+
101
+ The package joins the automated lockstep release (non-private `packages/*`). The
102
+ one-time npm stub + trusted-publisher setup is still required before the first
103
+ release can ship it — see `.agents/docs/releases.md`.
@@ -0,0 +1,2 @@
1
+ import { createShader, createSharedDevice, getWebGPUSupport, isWebGPUSupported, prefersReducedMotion } from "./index3.js";
2
+ export { createShader, createSharedDevice, getWebGPUSupport, isWebGPUSupported, prefersReducedMotion };
@@ -0,0 +1,90 @@
1
+ //#region src/shared.ts
2
+ function abortError() {
3
+ const error = /* @__PURE__ */ new Error("createShader aborted before initialization completed");
4
+ error.name = "AbortError";
5
+ return error;
6
+ }
7
+ function throwIfAborted(signal) {
8
+ if (signal?.aborted) throw abortError();
9
+ }
10
+ /** Defer until the surface reports a positive size (surface not attached /
11
+ * zero-size layout → wait, don't init). Aborts via `signal`. */
12
+ function waitForPositiveSize(measure, signal) {
13
+ const first = measure();
14
+ if (first.width > 0 && first.height > 0) return Promise.resolve();
15
+ return new Promise((resolve, reject) => {
16
+ let timer;
17
+ const stop = () => {
18
+ if (timer !== void 0) clearTimeout(timer);
19
+ signal?.removeEventListener?.("abort", onAbort);
20
+ };
21
+ const onAbort = () => {
22
+ stop();
23
+ reject(abortError());
24
+ };
25
+ const poll = () => {
26
+ if (signal?.aborted) return onAbort();
27
+ const size = measure();
28
+ if (size.width > 0 && size.height > 0) {
29
+ stop();
30
+ resolve();
31
+ return;
32
+ }
33
+ timer = setTimeout(poll, 50);
34
+ };
35
+ signal?.addEventListener?.("abort", onAbort);
36
+ timer = setTimeout(poll, 50);
37
+ });
38
+ }
39
+ /** Merge caller options with adapter defaults. `disableTelemetry` defaults
40
+ * on for this package's local/preset-driven use; callers may re-enable it.
41
+ * The adapter-only `signal` key is stripped before reaching upstream. */
42
+ function resolveOptions(options, platformDefaults) {
43
+ const { signal, ...rest } = options ?? {};
44
+ return {
45
+ signal,
46
+ upstream: {
47
+ ...platformDefaults,
48
+ ...rest,
49
+ disableTelemetry: options?.disableTelemetry ?? true
50
+ }
51
+ };
52
+ }
53
+ /** Wrap the upstream instance so host suspension composes with caller
54
+ * pause instead of overriding it: host hide pauses, host show resumes
55
+ * only when the caller did not explicitly pause. `destroy` detaches all
56
+ * adapter-owned listeners before delegating. */
57
+ function wrapInstance(inner, attach) {
58
+ let callerPaused = false;
59
+ let hostSuspended = false;
60
+ let destroyed = false;
61
+ const detach = attach(() => {
62
+ if (destroyed) return;
63
+ hostSuspended = true;
64
+ inner.pause();
65
+ }, () => {
66
+ hostSuspended = false;
67
+ if (!destroyed && !callerPaused) inner.resume();
68
+ });
69
+ return {
70
+ getFailureReason: () => inner.getFailureReason(),
71
+ update: (id, props) => inner.update(id, props),
72
+ resize: (width, height) => inner.resize(width, height),
73
+ pause: () => {
74
+ callerPaused = true;
75
+ inner.pause();
76
+ },
77
+ resume: () => {
78
+ callerPaused = false;
79
+ if (!hostSuspended) inner.resume();
80
+ },
81
+ destroy: () => {
82
+ if (destroyed) return;
83
+ destroyed = true;
84
+ detach();
85
+ inner.destroy();
86
+ }
87
+ };
88
+ }
89
+ //#endregion
90
+ export { abortError, resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance };
@@ -0,0 +1,167 @@
1
+ import { resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance } from "./index2.js";
2
+ import { Application, View, isAndroid, isIOS } from "@nativescript/core";
3
+ import { getGPU } from "@octane-xplat/canvas";
4
+ import { createShader, createSharedDevice, getWebGPUSupport, isWebGPUSupported } from "shaders/js";
5
+ //#region src/host.ts
6
+ var gpuShim;
7
+ var gpuResolved = false;
8
+ /** The plugin's GPU shim is also installed as `navigator.gpu` — upstream
9
+ * (and TypeGPU's `configureContext`) reads it unconditionally. */
10
+ function ensureGpu() {
11
+ if (!gpuResolved) {
12
+ gpuResolved = true;
13
+ try {
14
+ gpuShim = getGPU();
15
+ } catch {
16
+ gpuShim = null;
17
+ }
18
+ }
19
+ const gpu = gpuShim;
20
+ if (!gpu) return null;
21
+ const g = globalThis;
22
+ try {
23
+ if (!g.navigator) g.navigator = {};
24
+ if (!g.navigator.gpu) try {
25
+ g.navigator.gpu = gpu;
26
+ } catch {
27
+ Object.defineProperty(g.navigator, "gpu", {
28
+ configurable: true,
29
+ value: gpu
30
+ });
31
+ }
32
+ } catch {}
33
+ return gpu;
34
+ }
35
+ function measure(view) {
36
+ const rect = view?.getBoundingClientRect?.();
37
+ const actual = view?.getActualSize?.();
38
+ return {
39
+ width: Number(rect?.width) || Number(actual?.width) || Number(view?.width) || 0,
40
+ height: Number(rect?.height) || Number(actual?.height) || Number(view?.height) || 0
41
+ };
42
+ }
43
+ /** DOM-canvas-shaped facade over the plugin view. `width`/`height` writes
44
+ * (upstream's drawing-buffer bookkeeping) are stored locally so the
45
+ * plugin view's layout size is never mutated by buffer pixel counts. */
46
+ function hostCanvas(view) {
47
+ const bag = {
48
+ style: {},
49
+ width: 0,
50
+ height: 0
51
+ };
52
+ return {
53
+ getContext: (kind, options) => view.getContext(kind, options),
54
+ getBoundingClientRect: () => {
55
+ const { width, height } = measure(view);
56
+ return {
57
+ x: 0,
58
+ y: 0,
59
+ top: 0,
60
+ left: 0,
61
+ right: width,
62
+ bottom: height,
63
+ width,
64
+ height
65
+ };
66
+ },
67
+ get clientWidth() {
68
+ return measure(view).width;
69
+ },
70
+ get clientHeight() {
71
+ return measure(view).height;
72
+ },
73
+ get width() {
74
+ return bag.width || measure(view).width;
75
+ },
76
+ set width(value) {
77
+ bag.width = value;
78
+ },
79
+ get height() {
80
+ return bag.height || measure(view).height;
81
+ },
82
+ set height(value) {
83
+ bag.height = value;
84
+ },
85
+ get style() {
86
+ return bag.style;
87
+ },
88
+ parentElement: null
89
+ };
90
+ }
91
+ async function acquireInjectedGpu() {
92
+ const gpu = ensureGpu();
93
+ if (!gpu?.requestAdapter) return null;
94
+ try {
95
+ const adapter = await gpu.requestAdapter();
96
+ if (!adapter) return null;
97
+ const device = await adapter.requestDevice();
98
+ return device ? {
99
+ device,
100
+ adapter
101
+ } : null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ async function createShader$1(canvas, preset, options) {
107
+ const { upstream, signal } = resolveOptions(options, { observeElement: false });
108
+ throwIfAborted(signal);
109
+ const host = hostCanvas(canvas);
110
+ await waitForPositiveSize(() => measure(canvas), signal);
111
+ if (!upstream.gpu) {
112
+ const gpu = await acquireInjectedGpu();
113
+ if (gpu) upstream.gpu = gpu;
114
+ }
115
+ throwIfAborted(signal);
116
+ const inner = await createShader(host, preset, upstream);
117
+ if (signal?.aborted) {
118
+ inner.destroy();
119
+ throwIfAborted(signal);
120
+ }
121
+ const view = canvas;
122
+ const attach = (hide, show) => {
123
+ const onLayout = () => inner.resize();
124
+ Application.on(Application.suspendEvent, hide);
125
+ Application.on(Application.resumeEvent, show);
126
+ view?.on?.(View.unloadedEvent, hide);
127
+ view?.on?.(View.loadedEvent, show);
128
+ view?.on?.(View.layoutChangedEvent, onLayout);
129
+ return () => {
130
+ Application.off(Application.suspendEvent, hide);
131
+ Application.off(Application.resumeEvent, show);
132
+ view?.off?.(View.unloadedEvent, hide);
133
+ view?.off?.(View.loadedEvent, show);
134
+ view?.off?.(View.layoutChangedEvent, onLayout);
135
+ };
136
+ };
137
+ return wrapInstance(inner, attach);
138
+ }
139
+ async function createSharedDevice$1(options) {
140
+ ensureGpu();
141
+ return createSharedDevice(options);
142
+ }
143
+ function isWebGPUSupported$1() {
144
+ ensureGpu();
145
+ return isWebGPUSupported();
146
+ }
147
+ function getWebGPUSupport$1() {
148
+ ensureGpu();
149
+ return getWebGPUSupport();
150
+ }
151
+ function prefersReducedMotion() {
152
+ try {
153
+ if (isIOS) {
154
+ const check = globalThis.UIAccessibilityIsReduceMotionEnabled;
155
+ return typeof check === "function" ? !!check() : false;
156
+ }
157
+ if (isAndroid) {
158
+ const context = Application.android?.context;
159
+ const settings = globalThis.android?.provider?.Settings;
160
+ if (!context || !settings?.Global) return false;
161
+ return settings.Global.getFloat(context.getContentResolver(), settings.Global.ANIMATOR_DURATION_SCALE, 1) === 0;
162
+ }
163
+ } catch {}
164
+ return false;
165
+ }
166
+ //#endregion
167
+ export { createShader$1 as createShader, createSharedDevice$1 as createSharedDevice, getWebGPUSupport$1 as getWebGPUSupport, isWebGPUSupported$1 as isWebGPUSupported, prefersReducedMotion };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "b84fbf0f-5895-496f-8489-f300adedefa9",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ import { createShader, createSharedDevice, getWebGPUSupport, isWebGPUSupported, prefersReducedMotion } from "./index3.js";
2
+ export { createShader, createSharedDevice, getWebGPUSupport, isWebGPUSupported, prefersReducedMotion };
@@ -0,0 +1,90 @@
1
+ //#region src/shared.ts
2
+ function abortError() {
3
+ const error = /* @__PURE__ */ new Error("createShader aborted before initialization completed");
4
+ error.name = "AbortError";
5
+ return error;
6
+ }
7
+ function throwIfAborted(signal) {
8
+ if (signal?.aborted) throw abortError();
9
+ }
10
+ /** Defer until the surface reports a positive size (surface not attached /
11
+ * zero-size layout → wait, don't init). Aborts via `signal`. */
12
+ function waitForPositiveSize(measure, signal) {
13
+ const first = measure();
14
+ if (first.width > 0 && first.height > 0) return Promise.resolve();
15
+ return new Promise((resolve, reject) => {
16
+ let timer;
17
+ const stop = () => {
18
+ if (timer !== void 0) clearTimeout(timer);
19
+ signal?.removeEventListener?.("abort", onAbort);
20
+ };
21
+ const onAbort = () => {
22
+ stop();
23
+ reject(abortError());
24
+ };
25
+ const poll = () => {
26
+ if (signal?.aborted) return onAbort();
27
+ const size = measure();
28
+ if (size.width > 0 && size.height > 0) {
29
+ stop();
30
+ resolve();
31
+ return;
32
+ }
33
+ timer = setTimeout(poll, 50);
34
+ };
35
+ signal?.addEventListener?.("abort", onAbort);
36
+ timer = setTimeout(poll, 50);
37
+ });
38
+ }
39
+ /** Merge caller options with adapter defaults. `disableTelemetry` defaults
40
+ * on for this package's local/preset-driven use; callers may re-enable it.
41
+ * The adapter-only `signal` key is stripped before reaching upstream. */
42
+ function resolveOptions(options, platformDefaults) {
43
+ const { signal, ...rest } = options ?? {};
44
+ return {
45
+ signal,
46
+ upstream: {
47
+ ...platformDefaults,
48
+ ...rest,
49
+ disableTelemetry: options?.disableTelemetry ?? true
50
+ }
51
+ };
52
+ }
53
+ /** Wrap the upstream instance so host suspension composes with caller
54
+ * pause instead of overriding it: host hide pauses, host show resumes
55
+ * only when the caller did not explicitly pause. `destroy` detaches all
56
+ * adapter-owned listeners before delegating. */
57
+ function wrapInstance(inner, attach) {
58
+ let callerPaused = false;
59
+ let hostSuspended = false;
60
+ let destroyed = false;
61
+ const detach = attach(() => {
62
+ if (destroyed) return;
63
+ hostSuspended = true;
64
+ inner.pause();
65
+ }, () => {
66
+ hostSuspended = false;
67
+ if (!destroyed && !callerPaused) inner.resume();
68
+ });
69
+ return {
70
+ getFailureReason: () => inner.getFailureReason(),
71
+ update: (id, props) => inner.update(id, props),
72
+ resize: (width, height) => inner.resize(width, height),
73
+ pause: () => {
74
+ callerPaused = true;
75
+ inner.pause();
76
+ },
77
+ resume: () => {
78
+ callerPaused = false;
79
+ if (!hostSuspended) inner.resume();
80
+ },
81
+ destroy: () => {
82
+ if (destroyed) return;
83
+ destroyed = true;
84
+ detach();
85
+ inner.destroy();
86
+ }
87
+ };
88
+ }
89
+ //#endregion
90
+ export { abortError, resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance };
@@ -0,0 +1,43 @@
1
+ import { resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance } from "./index2.js";
2
+ import { createShader, createSharedDevice, getWebGPUSupport, isWebGPUSupported } from "shaders/js";
3
+ //#region src/host.web.ts
4
+ function measure(canvas) {
5
+ const rect = canvas?.getBoundingClientRect?.();
6
+ return {
7
+ width: Number(rect?.width) || Number(canvas?.width) || 0,
8
+ height: Number(rect?.height) || Number(canvas?.height) || 0
9
+ };
10
+ }
11
+ function attachVisibility(hide, show) {
12
+ const doc = globalThis.document;
13
+ if (!doc?.addEventListener) return () => {};
14
+ const onChange = () => doc.visibilityState === "hidden" ? hide() : show();
15
+ doc.addEventListener("visibilitychange", onChange);
16
+ return () => doc.removeEventListener("visibilitychange", onChange);
17
+ }
18
+ async function createShader$1(canvas, preset, options) {
19
+ const { upstream, signal } = resolveOptions(options, {});
20
+ throwIfAborted(signal);
21
+ await waitForPositiveSize(() => measure(canvas), signal);
22
+ const inner = await createShader(canvas, preset, upstream);
23
+ if (signal?.aborted) {
24
+ inner.destroy();
25
+ throwIfAborted(signal);
26
+ }
27
+ return wrapInstance(inner, attachVisibility);
28
+ }
29
+ function createSharedDevice$1(options) {
30
+ return createSharedDevice(options);
31
+ }
32
+ function isWebGPUSupported$1() {
33
+ return isWebGPUSupported();
34
+ }
35
+ function getWebGPUSupport$1() {
36
+ return getWebGPUSupport();
37
+ }
38
+ function prefersReducedMotion() {
39
+ const matchMedia = globalThis.matchMedia;
40
+ return typeof matchMedia === "function" ? !!matchMedia("(prefers-reduced-motion: reduce)").matches : false;
41
+ }
42
+ //#endregion
43
+ export { createShader$1 as createShader, createSharedDevice$1 as createSharedDevice, getWebGPUSupport$1 as getWebGPUSupport, isWebGPUSupported$1 as isWebGPUSupported, prefersReducedMotion };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "de39686e-0430-449c-b6e4-68cfe8c2ceb4",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,12 +1,79 @@
1
1
  {
2
- "name": "@octane-xplat/shaders",
3
- "version": "0.0.1",
4
- "description": "Placeholder stub — no supported API yet. First real release ships via octane-xplat's lockstep release workflow.",
5
- "license": "MIT",
6
- "private": false,
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/octane-xplat/octane-xplat",
10
- "directory": "packages/shaders"
11
- }
12
- }
2
+ "name": "@octane-xplat/shaders",
3
+ "version": "0.11.1",
4
+ "description": "Thin adapter that runs the published `shaders` WebGPU library on Octane xplat Canvas surfaces — web (implemented) and NativeScript iOS/Android (experimental, device verification pending)",
5
+ "keywords": [
6
+ "canvas",
7
+ "cross-platform",
8
+ "gpu",
9
+ "graphics",
10
+ "nativescript",
11
+ "octane",
12
+ "shader",
13
+ "webgpu",
14
+ "wgsl",
15
+ "xplat"
16
+ ],
17
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/shaders",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/octane-xplat/octane-xplat",
22
+ "directory": "packages/shaders"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "types",
27
+ "src"
28
+ ],
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "macos": {
33
+ "types": "./types/index.macos.d.ts",
34
+ "default": "./src/index.macos.ts"
35
+ },
36
+ "types": "./types/index.d.ts",
37
+ "web": "./dist/web/index.js",
38
+ "native": "./dist/native/index.js",
39
+ "default": "./dist/native/index.js"
40
+ }
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@octane-xplat/canvas": "0.11.1",
47
+ "shaders": "4.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@nativescript-community/octane": "0.2.4",
51
+ "@nativescript/core": "9.1.2",
52
+ "@octanejs/vite-plugin": "0.1.61",
53
+ "octane": "0.6.3",
54
+ "tsrx-typegen": "0.2.2",
55
+ "typescript": "5.9.3",
56
+ "vite": "8.3.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@nativescript/core": ">=9.1.0 <10"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@nativescript/core": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "nativescript": {
67
+ "platforms": {
68
+ "ios": "7.0.0",
69
+ "android": "7.0.0"
70
+ }
71
+ },
72
+ "scripts": {
73
+ "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
74
+ "typegen": "tsc -p tsconfig.types.json",
75
+ "check-types": "tsc -p tsconfig.check.json",
76
+ "test:packed": "node tests/packed-consumer.mjs",
77
+ "pack:check": "tsrx-typegen --pack-check"
78
+ }
79
+ }
package/src/host.ts ADDED
@@ -0,0 +1,205 @@
1
+ // Native host (iOS/Android via NativeScript) — the Xplat Canvas surface is
2
+ // a `@nativescript/canvas` Canvas view. The engine consumes a DOM-canvas
3
+ // shape (`style`, `getBoundingClientRect`, `width`/`height` buffer
4
+ // writes), so the view is wrapped in a facade that delegates `getContext`
5
+ // and swallows style/buffer writes instead of resizing the view.
6
+ //
7
+ // Adapter-owned native defaults:
8
+ // - `navigator.gpu` is lazily bound to the plugin's GPU shim on first use
9
+ // (never at import) so upstream's own acquisition, capability checks and
10
+ // device-loss recovery work unchanged.
11
+ // - `observeElement` defaults off: there is no DOM
12
+ // ResizeObserver/IntersectionObserver; sizing is driven by the view's
13
+ // `layoutChanged` event and forwarded to `instance.resize()`.
14
+ // - App suspend/resume and view unload/load map to host suspension.
15
+ //
16
+ // Frame pacing falls back to upstream's `setTimeout(16)` loop when the
17
+ // runtime lacks `requestAnimationFrame`. Experimental: iOS/Android device
18
+ // verification is still pending.
19
+
20
+ import { Application, View, isAndroid, isIOS } from '@nativescript/core'
21
+ import { getGPU } from '@octane-xplat/canvas'
22
+ import {
23
+ createShader as upstreamCreateShader,
24
+ createSharedDevice as upstreamCreateSharedDevice,
25
+ getWebGPUSupport as upstreamGetWebGPUSupport,
26
+ isWebGPUSupported as upstreamIsWebGPUSupported,
27
+ } from 'shaders/js'
28
+
29
+ import type { CreateShaderOptions, PresetConfig, SharedGpu, ShaderInstance, WebGPUSupportInfo } from './types'
30
+ import { resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance } from './shared'
31
+ import type { Size } from './shared'
32
+
33
+ let gpuShim: any
34
+ let gpuResolved = false
35
+
36
+ /** The plugin's GPU shim is also installed as `navigator.gpu` — upstream
37
+ * (and TypeGPU's `configureContext`) reads it unconditionally. */
38
+ function ensureGpu(): any {
39
+ if (!gpuResolved) {
40
+ gpuResolved = true
41
+ try {
42
+ gpuShim = getGPU()
43
+ } catch {
44
+ gpuShim = null
45
+ }
46
+ }
47
+
48
+ const gpu = gpuShim
49
+ if (!gpu) {return null}
50
+ const g = globalThis as any
51
+ try {
52
+ if (!g.navigator) {g.navigator = {}}
53
+ if (!g.navigator.gpu) {
54
+ try {
55
+ g.navigator.gpu = gpu
56
+ } catch {
57
+ Object.defineProperty(g.navigator, 'gpu', { configurable: true, value: gpu })
58
+ }
59
+ }
60
+ } catch {
61
+ // `navigator` may be a read-only global — injected `gpu` still applies.
62
+ }
63
+
64
+ return gpu
65
+ }
66
+
67
+ function measure(view: any): Size {
68
+ const rect = view?.getBoundingClientRect?.()
69
+ const actual = view?.getActualSize?.()
70
+ const width = Number(rect?.width) || Number(actual?.width) || Number(view?.width) || 0
71
+ const height = Number(rect?.height) || Number(actual?.height) || Number(view?.height) || 0
72
+ return { width, height }
73
+ }
74
+
75
+ /** DOM-canvas-shaped facade over the plugin view. `width`/`height` writes
76
+ * (upstream's drawing-buffer bookkeeping) are stored locally so the
77
+ * plugin view's layout size is never mutated by buffer pixel counts. */
78
+ function hostCanvas(view: any): any {
79
+ const bag = { style: {} as Record<string, string>, width: 0, height: 0 }
80
+ return {
81
+ getContext: (kind: string, options?: any) => view.getContext(kind, options),
82
+ getBoundingClientRect: () => {
83
+ const { width, height } = measure(view)
84
+ return { x: 0, y: 0, top: 0, left: 0, right: width, bottom: height, width, height }
85
+ },
86
+ get clientWidth() {
87
+ return measure(view).width
88
+ },
89
+ get clientHeight() {
90
+ return measure(view).height
91
+ },
92
+ get width() {
93
+ return bag.width || measure(view).width
94
+ },
95
+ set width(value: number) {
96
+ bag.width = value
97
+ },
98
+ get height() {
99
+ return bag.height || measure(view).height
100
+ },
101
+ set height(value: number) {
102
+ bag.height = value
103
+ },
104
+ get style() {
105
+ return bag.style
106
+ },
107
+ parentElement: null,
108
+ }
109
+ }
110
+
111
+ async function acquireInjectedGpu(): Promise<SharedGpu | null> {
112
+ const gpu = ensureGpu()
113
+ if (!gpu?.requestAdapter) {return null}
114
+ try {
115
+ const adapter = await gpu.requestAdapter()
116
+ if (!adapter) {return null}
117
+ const device = await adapter.requestDevice()
118
+ return device ? { device, adapter } : null
119
+ } catch {
120
+ return null
121
+ }
122
+ }
123
+
124
+ export async function createShader(
125
+ canvas: unknown,
126
+ preset: PresetConfig,
127
+ options?: CreateShaderOptions,
128
+ ): Promise<ShaderInstance> {
129
+ const { upstream, signal } = resolveOptions(options, { observeElement: false })
130
+ throwIfAborted(signal)
131
+ const host = hostCanvas(canvas)
132
+ await waitForPositiveSize(() => measure(canvas), signal)
133
+ // Adapter-acquired device (shared default when the caller supplies no
134
+ // `gpu`) — never destroyed by the adapter, so one instance's destroy
135
+ // cannot kill another's shared device.
136
+ if (!upstream.gpu) {
137
+ const gpu = await acquireInjectedGpu()
138
+ if (gpu) {upstream.gpu = gpu}
139
+ }
140
+
141
+ throwIfAborted(signal)
142
+ const inner = await upstreamCreateShader(host, preset, upstream as any)
143
+ if (signal?.aborted) {
144
+ inner.destroy()
145
+ throwIfAborted(signal)
146
+ }
147
+
148
+ const view = canvas as any
149
+ const attach = (hide: () => void, show: () => void) => {
150
+ const onLayout = () => inner.resize()
151
+ Application.on(Application.suspendEvent, hide)
152
+ Application.on(Application.resumeEvent, show)
153
+ view?.on?.(View.unloadedEvent, hide)
154
+ view?.on?.(View.loadedEvent, show)
155
+ view?.on?.(View.layoutChangedEvent, onLayout)
156
+ return () => {
157
+ Application.off(Application.suspendEvent, hide)
158
+ Application.off(Application.resumeEvent, show)
159
+ view?.off?.(View.unloadedEvent, hide)
160
+ view?.off?.(View.loadedEvent, show)
161
+ view?.off?.(View.layoutChangedEvent, onLayout)
162
+ }
163
+ }
164
+
165
+ return wrapInstance(inner, attach)
166
+ }
167
+
168
+ export async function createSharedDevice(options?: { powerPreference?: string }): Promise<SharedGpu | null> {
169
+ ensureGpu()
170
+ return upstreamCreateSharedDevice(options as any)
171
+ }
172
+
173
+ export function isWebGPUSupported(): boolean {
174
+ ensureGpu()
175
+ return upstreamIsWebGPUSupported()
176
+ }
177
+
178
+ export function getWebGPUSupport(): Promise<WebGPUSupportInfo> {
179
+ ensureGpu()
180
+ return upstreamGetWebGPUSupport()
181
+ }
182
+
183
+ export function prefersReducedMotion(): boolean {
184
+ try {
185
+ if (isIOS) {
186
+ const check = (globalThis as any).UIAccessibilityIsReduceMotionEnabled
187
+ return typeof check === 'function' ? !!check() : false
188
+ }
189
+
190
+ if (isAndroid) {
191
+ const context = Application.android?.context
192
+ const settings = (globalThis as any).android?.provider?.Settings
193
+ if (!context || !settings?.Global) {return false}
194
+ const scale = settings.Global.getFloat(
195
+ context.getContentResolver(),
196
+ settings.Global.ANIMATOR_DURATION_SCALE,
197
+ 1,
198
+ )
199
+
200
+ return scale === 0
201
+ }
202
+ } catch {}
203
+
204
+ return false
205
+ }
@@ -0,0 +1,68 @@
1
+ // Web host — the Xplat Canvas surface is already a real
2
+ // HTMLCanvasElement, so it reaches upstream unchanged. The adapter adds:
3
+ // deferred init until positive size, telemetry off by default, abortable
4
+ // init, and document-visibility suspension that composes with caller
5
+ // pause. Offscreen/detach suspension is upstream's own observers
6
+ // (`observeElement`, default on).
7
+
8
+ import {
9
+ createShader as upstreamCreateShader,
10
+ createSharedDevice as upstreamCreateSharedDevice,
11
+ getWebGPUSupport as upstreamGetWebGPUSupport,
12
+ isWebGPUSupported as upstreamIsWebGPUSupported,
13
+ } from 'shaders/js'
14
+
15
+ import type { CreateShaderOptions, PresetConfig, SharedGpu, ShaderInstance, WebGPUSupportInfo } from './types'
16
+ import { resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance } from './shared'
17
+ import type { Size } from './shared'
18
+
19
+ function measure(canvas: any): Size {
20
+ const rect = canvas?.getBoundingClientRect?.()
21
+ const width = Number(rect?.width) || Number(canvas?.width) || 0
22
+ const height = Number(rect?.height) || Number(canvas?.height) || 0
23
+ return { width, height }
24
+ }
25
+
26
+ function attachVisibility(hide: () => void, show: () => void): () => void {
27
+ const doc = (globalThis as any).document
28
+ if (!doc?.addEventListener) {return () => {}}
29
+ const onChange = () => (doc.visibilityState === 'hidden' ? hide() : show())
30
+ doc.addEventListener('visibilitychange', onChange)
31
+ return () => doc.removeEventListener('visibilitychange', onChange)
32
+ }
33
+
34
+ export async function createShader(
35
+ canvas: unknown,
36
+ preset: PresetConfig,
37
+ options?: CreateShaderOptions,
38
+ ): Promise<ShaderInstance> {
39
+ const { upstream, signal } = resolveOptions(options, {})
40
+ throwIfAborted(signal)
41
+ await waitForPositiveSize(() => measure(canvas), signal)
42
+ const inner = await upstreamCreateShader(canvas as any, preset, upstream)
43
+ if (signal?.aborted) {
44
+ // Unmounted while the async engine was starting — release the late
45
+ // completion instead of reviving a disposed surface.
46
+ inner.destroy()
47
+ throwIfAborted(signal)
48
+ }
49
+
50
+ return wrapInstance(inner, attachVisibility)
51
+ }
52
+
53
+ export function createSharedDevice(options?: { powerPreference?: string }): Promise<SharedGpu | null> {
54
+ return upstreamCreateSharedDevice(options as any)
55
+ }
56
+
57
+ export function isWebGPUSupported(): boolean {
58
+ return upstreamIsWebGPUSupported()
59
+ }
60
+
61
+ export function getWebGPUSupport(): Promise<WebGPUSupportInfo> {
62
+ return upstreamGetWebGPUSupport()
63
+ }
64
+
65
+ export function prefersReducedMotion(): boolean {
66
+ const matchMedia = (globalThis as any).matchMedia
67
+ return typeof matchMedia === 'function' ? !!matchMedia('(prefers-reduced-motion: reduce)').matches : false
68
+ }
@@ -0,0 +1,41 @@
1
+ // Native-desktop entry — explicitly unsupported. No engine import, no GPU
2
+ // work at module load: this leaf never touches `shaders` at runtime, so
3
+ // importing the package on AppKit is inert. `createShader` resolves to a
4
+ // no-op instance reporting the upstream-style 'unsupported' failure
5
+ // reason so callers keep their static fallback path identical to web's.
6
+ export * from './types'
7
+ import type { CreateShaderOptions, PresetConfig, SharedGpu, ShaderInstance, WebGPUSupportInfo } from './types'
8
+
9
+ const UNSUPPORTED: ShaderInstance = {
10
+ getFailureReason: () => 'unsupported',
11
+ update: () => {},
12
+ resize: () => {},
13
+ pause: () => {},
14
+ resume: () => {},
15
+ destroy: () => {},
16
+ }
17
+
18
+ export async function createShader(
19
+ _canvas: unknown,
20
+ _preset: PresetConfig,
21
+ options?: CreateShaderOptions,
22
+ ): Promise<ShaderInstance> {
23
+ options?.onError?.('unsupported')
24
+ return UNSUPPORTED
25
+ }
26
+
27
+ export async function createSharedDevice(_options?: { powerPreference?: string }): Promise<SharedGpu | null> {
28
+ return null
29
+ }
30
+
31
+ export function isWebGPUSupported(): boolean {
32
+ return false
33
+ }
34
+
35
+ export async function getWebGPUSupport(): Promise<WebGPUSupportInfo> {
36
+ return { supported: false, reason: 'unsupported' }
37
+ }
38
+
39
+ export function prefersReducedMotion(): boolean {
40
+ return false
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Native entry (iOS/Android via @nativescript/canvas) — same exports as
2
+ // index.web.ts and index.macos.ts.
3
+ export * from './types'
4
+ export {
5
+ createShader,
6
+ createSharedDevice,
7
+ getWebGPUSupport,
8
+ isWebGPUSupported,
9
+ prefersReducedMotion,
10
+ } from './host'
@@ -0,0 +1,9 @@
1
+ // Web entry — same exports as index.ts and index.macos.ts.
2
+ export * from './types'
3
+ export {
4
+ createShader,
5
+ createSharedDevice,
6
+ getWebGPUSupport,
7
+ isWebGPUSupported,
8
+ prefersReducedMotion,
9
+ } from './host.web'
package/src/shared.ts ADDED
@@ -0,0 +1,120 @@
1
+ // Shared adapter internals — platform-free. No DOM globals, no
2
+ // NativeScript imports; each platform leaf injects its measure/attach
3
+ // hooks.
4
+
5
+ import type { AbortSignalLike, CreateShaderOptions, ShaderInstance } from './types'
6
+
7
+ export function abortError(): Error {
8
+ const error = new Error('createShader aborted before initialization completed')
9
+ error.name = 'AbortError'
10
+ return error
11
+ }
12
+
13
+ export function throwIfAborted(signal?: AbortSignalLike): void {
14
+ if (signal?.aborted) {throw abortError()}
15
+ }
16
+
17
+ export interface Size {
18
+ width: number
19
+ height: number
20
+ }
21
+
22
+ /** Defer until the surface reports a positive size (surface not attached /
23
+ * zero-size layout → wait, don't init). Aborts via `signal`. */
24
+ export function waitForPositiveSize(
25
+ measure: () => Size,
26
+ signal?: AbortSignalLike,
27
+ ): Promise<void> {
28
+ const first = measure()
29
+ if (first.width > 0 && first.height > 0) {return Promise.resolve()}
30
+ return new Promise<void>((resolve, reject) => {
31
+ let timer: ReturnType<typeof setTimeout> | undefined
32
+ const stop = () => {
33
+ if (timer !== undefined) {clearTimeout(timer)}
34
+ signal?.removeEventListener?.('abort', onAbort)
35
+ }
36
+
37
+ const onAbort = () => {
38
+ stop()
39
+ reject(abortError())
40
+ }
41
+
42
+ const poll = () => {
43
+ if (signal?.aborted) {return onAbort()}
44
+ const size = measure()
45
+ if (size.width > 0 && size.height > 0) {
46
+ stop()
47
+ resolve()
48
+ return
49
+ }
50
+
51
+ timer = setTimeout(poll, 50)
52
+ }
53
+
54
+ signal?.addEventListener?.('abort', onAbort)
55
+ timer = setTimeout(poll, 50)
56
+ })
57
+ }
58
+
59
+ /** Merge caller options with adapter defaults. `disableTelemetry` defaults
60
+ * on for this package's local/preset-driven use; callers may re-enable it.
61
+ * The adapter-only `signal` key is stripped before reaching upstream. */
62
+ export function resolveOptions(
63
+ options: CreateShaderOptions | undefined,
64
+ platformDefaults: Partial<CreateShaderOptions>,
65
+ ): { upstream: Omit<CreateShaderOptions, 'signal'>; signal?: AbortSignalLike } {
66
+ const { signal, ...rest } = options ?? {}
67
+ return {
68
+ signal,
69
+ upstream: {
70
+ ...platformDefaults,
71
+ ...rest,
72
+ disableTelemetry: options?.disableTelemetry ?? true,
73
+ },
74
+ }
75
+ }
76
+
77
+ /** Subscribe/unsubscribe pair for host hide/show (backgrounding, detach,
78
+ * document visibility). */
79
+ export type AttachLifecycle = (hide: () => void, show: () => void) => () => void
80
+
81
+ /** Wrap the upstream instance so host suspension composes with caller
82
+ * pause instead of overriding it: host hide pauses, host show resumes
83
+ * only when the caller did not explicitly pause. `destroy` detaches all
84
+ * adapter-owned listeners before delegating. */
85
+ export function wrapInstance(inner: ShaderInstance, attach: AttachLifecycle): ShaderInstance {
86
+ let callerPaused = false
87
+ let hostSuspended = false
88
+ let destroyed = false
89
+ const detach = attach(
90
+ () => {
91
+ if (destroyed) {return}
92
+ hostSuspended = true
93
+ inner.pause()
94
+ },
95
+ () => {
96
+ hostSuspended = false
97
+ if (!destroyed && !callerPaused) {inner.resume()}
98
+ },
99
+ )
100
+
101
+ return {
102
+ getFailureReason: () => inner.getFailureReason(),
103
+ update: (id, props) => inner.update(id, props),
104
+ resize: (width?, height?) => inner.resize(width, height),
105
+ pause: () => {
106
+ callerPaused = true
107
+ inner.pause()
108
+ },
109
+ resume: () => {
110
+ callerPaused = false
111
+ if (!hostSuspended) {inner.resume()}
112
+ },
113
+ destroy: () => {
114
+ if (destroyed) {return}
115
+ destroyed = true
116
+ detach()
117
+ inner.destroy()
118
+ },
119
+ }
120
+ }
package/src/types.ts ADDED
@@ -0,0 +1,86 @@
1
+ // Public contract for @octane-xplat/shaders — re-exports the upstream
2
+ // `shaders` types verbatim and declares the adapter's added surface. Kept
3
+ // platform-free: every platform leaf (web / native / macos stub) exposes
4
+ // exactly these names.
5
+
6
+ // Upstream types are re-exported as aliases rather than `export type {…}
7
+ // from 'shaders/js'` so they stay unambiguously type-only for the pack
8
+ // check even where the specifier can't be resolved.
9
+ export type ComponentConfig = import('shaders/js').ComponentConfig
10
+ export type GpuFailureReason = import('shaders/js').GpuFailureReason
11
+ export type PresetConfig = import('shaders/js').PresetConfig
12
+ export type ShaderInstance = import('shaders/js').ShaderInstance
13
+ export type ShaderOptions = import('shaders/js').ShaderOptions
14
+ export type WebGPUSupportInfo = import('shaders/js').WebGPUSupportInfo
15
+
16
+ /** Structural AbortSignal — the real DOM AbortSignal satisfies this; so
17
+ * does NativeScript's. Kept structural so shared code never names DOM
18
+ * globals. */
19
+ export interface AbortSignalLike {
20
+ readonly aborted: boolean
21
+ addEventListener?(type: 'abort', listener: () => void): void
22
+ removeEventListener?(type: 'abort', listener: () => void): void
23
+ }
24
+
25
+ /** Externally acquired WebGPU device + adapter pair — the caller owns its
26
+ * lifetime. Pass to `createShader` via `options.gpu` to share one device
27
+ * across instances. */
28
+ export interface SharedGpu {
29
+ device: any
30
+ adapter: any
31
+ }
32
+
33
+ /** Upstream `ShaderOptions` plus adapter lifecycle glue. Preset, component
34
+ * names, parameters, and all other fields keep upstream semantics. */
35
+ export interface CreateShaderOptions extends ShaderOptions {
36
+ /** Aborts deferred or in-flight initialization — e.g. the surface
37
+ * unmounted while the engine was still starting. A late completion is
38
+ * destroyed, never revived. */
39
+ signal?: AbortSignalLike
40
+ }
41
+
42
+ /** Create a shader on an Xplat Canvas surface (the `canvas` from
43
+ * `CanvasReady`: HTMLCanvasElement on web, `@nativescript/canvas` view on
44
+ * native). Same signature shape and return contract as upstream
45
+ * `createShader` — the resolved instance keeps upstream
46
+ * `update/resize/pause/resume/destroy/getFailureReason` semantics.
47
+ *
48
+ * Adapter additions, all documented:
49
+ * - `disableTelemetry` defaults to true (upstream default is off → on).
50
+ * - Init defers until the surface reports a positive size.
51
+ * - Host suspension (app background, surface detach, document hidden)
52
+ * pauses rendering without overriding an explicit caller `pause()`.
53
+ * - `options.signal` cancels before init completes.
54
+ * - On native, the canvas is wrapped in a DOM-canvas-shaped facade and
55
+ * `navigator.gpu` is lazily bound to the `@nativescript/canvas` GPU
56
+ * shim so upstream capability checks and device-loss recovery work.
57
+ *
58
+ * On unsupported hosts (native desktop) the returned promise resolves to
59
+ * a no-op instance whose `getFailureReason()` is `'unsupported'`. */
60
+ export declare function createShader(
61
+ canvas: unknown,
62
+ preset: PresetConfig,
63
+ options?: CreateShaderOptions,
64
+ ): Promise<ShaderInstance>
65
+
66
+ /** Acquire a WebGPU `{device, adapter}` through the platform entry point
67
+ * (`navigator.gpu` on web, the `@nativescript/canvas` GPU shim on native).
68
+ * Resolves null where WebGPU is unavailable — keep the static fallback in
69
+ * that case. The caller owns the device; the adapter never destroys it. */
70
+ export declare function createSharedDevice(options?: {
71
+ powerPreference?: string
72
+ }): Promise<SharedGpu | null>
73
+
74
+ /** True when a WebGPU entry point exists. On native this reflects the
75
+ * `@nativescript/canvas` GPU shim, not actual driver capability — request
76
+ * an adapter via `createSharedDevice()`/`getWebGPUSupport()` for a real
77
+ * check. */
78
+ export declare function isWebGPUSupported(): boolean
79
+
80
+ /** Async capability probe — resolves `{supported, reason?}` using upstream
81
+ * failure-reason vocabulary ('unsupported', 'no-adapter', …). */
82
+ export declare function getWebGPUSupport(): Promise<WebGPUSupportInfo>
83
+
84
+ /** OS-level reduced-motion preference. Use it to hold a still frame
85
+ * (pause after the first rendered frame) or keep the static fallback. */
86
+ export declare function prefersReducedMotion(): boolean
@@ -0,0 +1 @@
1
+ export * from './types.js'
@@ -0,0 +1,5 @@
1
+ // Native desktop keeps the same public surface as the other targets —
2
+ // the functions resolve to the unsupported no-op implementations.
3
+ // Re-exports types.d.ts directly: under .macos moduleSuffixes, './index.js'
4
+ // would resolve back to this file.
5
+ export * from './types.js'
@@ -0,0 +1,66 @@
1
+ export type ComponentConfig = import('shaders/js').ComponentConfig;
2
+ export type GpuFailureReason = import('shaders/js').GpuFailureReason;
3
+ export type PresetConfig = import('shaders/js').PresetConfig;
4
+ export type ShaderInstance = import('shaders/js').ShaderInstance;
5
+ export type ShaderOptions = import('shaders/js').ShaderOptions;
6
+ export type WebGPUSupportInfo = import('shaders/js').WebGPUSupportInfo;
7
+ /** Structural AbortSignal — the real DOM AbortSignal satisfies this; so
8
+ * does NativeScript's. Kept structural so shared code never names DOM
9
+ * globals. */
10
+ export interface AbortSignalLike {
11
+ readonly aborted: boolean;
12
+ addEventListener?(type: 'abort', listener: () => void): void;
13
+ removeEventListener?(type: 'abort', listener: () => void): void;
14
+ }
15
+ /** Externally acquired WebGPU device + adapter pair — the caller owns its
16
+ * lifetime. Pass to `createShader` via `options.gpu` to share one device
17
+ * across instances. */
18
+ export interface SharedGpu {
19
+ device: any;
20
+ adapter: any;
21
+ }
22
+ /** Upstream `ShaderOptions` plus adapter lifecycle glue. Preset, component
23
+ * names, parameters, and all other fields keep upstream semantics. */
24
+ export interface CreateShaderOptions extends ShaderOptions {
25
+ /** Aborts deferred or in-flight initialization — e.g. the surface
26
+ * unmounted while the engine was still starting. A late completion is
27
+ * destroyed, never revived. */
28
+ signal?: AbortSignalLike;
29
+ }
30
+ /** Create a shader on an Xplat Canvas surface (the `canvas` from
31
+ * `CanvasReady`: HTMLCanvasElement on web, `@nativescript/canvas` view on
32
+ * native). Same signature shape and return contract as upstream
33
+ * `createShader` — the resolved instance keeps upstream
34
+ * `update/resize/pause/resume/destroy/getFailureReason` semantics.
35
+ *
36
+ * Adapter additions, all documented:
37
+ * - `disableTelemetry` defaults to true (upstream default is off → on).
38
+ * - Init defers until the surface reports a positive size.
39
+ * - Host suspension (app background, surface detach, document hidden)
40
+ * pauses rendering without overriding an explicit caller `pause()`.
41
+ * - `options.signal` cancels before init completes.
42
+ * - On native, the canvas is wrapped in a DOM-canvas-shaped facade and
43
+ * `navigator.gpu` is lazily bound to the `@nativescript/canvas` GPU
44
+ * shim so upstream capability checks and device-loss recovery work.
45
+ *
46
+ * On unsupported hosts (native desktop) the returned promise resolves to
47
+ * a no-op instance whose `getFailureReason()` is `'unsupported'`. */
48
+ export declare function createShader(canvas: unknown, preset: PresetConfig, options?: CreateShaderOptions): Promise<ShaderInstance>;
49
+ /** Acquire a WebGPU `{device, adapter}` through the platform entry point
50
+ * (`navigator.gpu` on web, the `@nativescript/canvas` GPU shim on native).
51
+ * Resolves null where WebGPU is unavailable — keep the static fallback in
52
+ * that case. The caller owns the device; the adapter never destroys it. */
53
+ export declare function createSharedDevice(options?: {
54
+ powerPreference?: string;
55
+ }): Promise<SharedGpu | null>;
56
+ /** True when a WebGPU entry point exists. On native this reflects the
57
+ * `@nativescript/canvas` GPU shim, not actual driver capability — request
58
+ * an adapter via `createSharedDevice()`/`getWebGPUSupport()` for a real
59
+ * check. */
60
+ export declare function isWebGPUSupported(): boolean;
61
+ /** Async capability probe — resolves `{supported, reason?}` using upstream
62
+ * failure-reason vocabulary ('unsupported', 'no-adapter', …). */
63
+ export declare function getWebGPUSupport(): Promise<WebGPUSupportInfo>;
64
+ /** OS-level reduced-motion preference. Use it to hold a still frame
65
+ * (pause after the first rendered frame) or keep the static fallback. */
66
+ export declare function prefersReducedMotion(): boolean;