@octane-xplat/shaders 0.0.0-nightly.g14544fe

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,108 @@
1
+ # `@octane-xplat/shaders`
2
+
3
+ Runs the published [`shaders`](https://www.npmjs.com/package/shaders) WebGPU
4
+ engine (pinned `4.0.0`, used unchanged — no vendoring, no shader port) on an
5
+ Octane Xplat `Canvas` surface from `@octane-xplat/canvas`. Use it for the
6
+ upstream engine's procedural presets — noise, gradients, and other generated
7
+ effects. The adapter deliberately does not create a new shader API; it only
8
+ adds the surface/lifecycle glue Xplat needs. Preset shapes, effect names,
9
+ parameters, options, and the returned instance's methods — `update(id, props)`,
10
+ `resize()`, `pause()`, `resume()`, `destroy()`, `getFailureReason()` — are
11
+ upstream's.
12
+
13
+ ```sh
14
+ pnpm add @octane-xplat/shaders
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```tsx
20
+ import { Canvas } from '@octane-xplat/canvas'
21
+ import { createShader } from '@octane-xplat/shaders'
22
+
23
+ ;<Canvas
24
+ width={320}
25
+ height={180}
26
+ style={{ width: 320, height: 180 }}
27
+ onReady={({ canvas }) => {
28
+ void createShader(canvas, {
29
+ components: [
30
+ {
31
+ id: 'background',
32
+ type: 'SimplexNoise',
33
+ props: { colorA: '#0f172a', colorB: '#7c3aed', scale: 2, seed: 7, speed: 0.5 },
34
+ },
35
+ ],
36
+ })
37
+ }}
38
+ />
39
+ ```
40
+
41
+ Working demo: `ShaderDemo` in `packages/demos` (parameter updates, pause/resume,
42
+ static fallback, reduced-motion still frame, disposal on unmount).
43
+
44
+ ## What the adapter adds
45
+
46
+ - `createShader(canvas, preset, options?)` — same signature as upstream
47
+ `shaders/js`'s `createShader`, except `canvas` is the Xplat Canvas surface
48
+ (`CanvasReady.canvas`) and options add `signal` (abort deferred/in-flight init;
49
+ a late completion is destroyed, never revived).
50
+ - `disableTelemetry` defaults to `true`. Upstream telemetry stays off unless you
51
+ pass `disableTelemetry: false`. No hosted account, preset download, or network
52
+ access is required for local presets.
53
+ - Deferred init: until the surface reports a positive size, nothing touches the
54
+ GPU. The static content behind your canvas stays put.
55
+ - Host suspension: app backgrounding (NativeScript `suspend`/`resume`), surface
56
+ detach (`loaded`/`unloaded`), and `document` visibility (web) pause rendering —
57
+ without overriding an explicit caller `pause()` when the host returns.
58
+ - `createSharedDevice()` — acquire `{ device, adapter }` through the platform
59
+ entry point (`navigator.gpu` on web, the `@nativescript/canvas` GPU shim on
60
+ native) for callers that want to own/share the device via `options.gpu`.
61
+ Instances never destroy a caller-provided or adapter-shared device.
62
+ - `isWebGPUSupported()` / `getWebGPUSupport()` — upstream capability helpers,
63
+ native-aware.
64
+ - `prefersReducedMotion()` — OS reduced-motion preference; pause after the first
65
+ frame to hold a still, or keep the static fallback.
66
+
67
+ ## Target status
68
+
69
+ | Target | Status |
70
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
+ | 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. |
72
+ | NativeScript iOS | Implemented, experimental. `@nativescript/canvas` WebGPU (wgpu over Metal). Build/bundle/type verified; device execution pending. |
73
+ | NativeScript Android | Implemented, experimental. Same native route over Vulkan. Build/type verified; device/driver execution pending. |
74
+ | 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. |
75
+ | Web code inside a desktop webview | Follows the browser's own WebGPU support; no separate integration. |
76
+
77
+ Adapter-owned native defaults: `navigator.gpu` is bound lazily to the
78
+ `@nativescript/canvas` GPU shim on first use (upstream reads it unconditionally);
79
+ `observeElement` defaults off on native (no DOM observers — the view's
80
+ `layoutChanged` drives `resize()`); frame pacing uses `requestAnimationFrame`
81
+ where the runtime provides it, else upstream's `setTimeout(~16ms)` fallback.
82
+
83
+ ## Limits to know
84
+
85
+ - **Payload:** the engine is heavy — about **709 KB gzip** (minified browser
86
+ bundle for the JS host + SimplexNoise; ~705 KB via `shaders/core`). Per-effect
87
+ imports do not shrink it because the core renderer pulls the full registry.
88
+ This cost lands only in apps that install this optional package.
89
+ - **Upstream packaging defects:** `shaders@4.0.0` declares 34 export subpaths
90
+ that point to absent files (e.g. `./core/RadialLines`, `./core/GradientNoise`).
91
+ `shaders/js`, `shaders/core`, and `shaders/core/SimplexNoise` are intact. Treat
92
+ other subpaths as unverified until you check the file exists. Upstream `.d.ts`
93
+ files also use extensionless relative imports, so they do not resolve under
94
+ `moduleResolution: 'nodenext'` — consumers need bundler-style resolution (all
95
+ Xplat toolchain paths use it).
96
+ - **Catalog coverage:** only intact procedural effect definitions without
97
+ browser-only inputs are in the initial native compatibility claim. DOM capture,
98
+ HTML/text-specific facilities, cursor/mouse-driven effects, media sources,
99
+ hosted presets, and simulations may fail on native — upstream reports terminal
100
+ failures via `onError`/`getFailureReason()`; keep the static fallback mounted.
101
+ - **Recovery:** device loss uses upstream's own rebuild path (≤3 attempts); there
102
+ is no adapter-level retry loop. Terminal reasons stop rendering permanently.
103
+
104
+ ## Publishing
105
+
106
+ The package joins the automated lockstep release (non-private `packages/*`). The
107
+ one-time npm stub + trusted-publisher setup is still required before the first
108
+ release can ship it — see `.agents/docs/releases.md`.
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@octane-xplat/shaders",
3
+ "version": "0.0.0-nightly.g14544fe",
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
+ "types",
26
+ "src",
27
+ "!src/**/*.test.*",
28
+ "!src/**/*.spec.*"
29
+ ],
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "macos": {
34
+ "types": "./types/index.macos.d.ts",
35
+ "default": "./src/index.macos.ts"
36
+ },
37
+ "types": "./types/index.d.ts",
38
+ "web": "./src/index.web.ts",
39
+ "native": "./src/index.ts",
40
+ "default": "./src/index.ts"
41
+ }
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@octane-xplat/canvas": "0.0.0-nightly.g14544fe",
48
+ "shaders": "4.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@nativescript-community/octane": "0.2.5",
52
+ "@nativescript/core": "9.1.3",
53
+ "@octanejs/vite-plugin": "0.1.61",
54
+ "octane": "0.11.0",
55
+ "tsrx-typegen": "0.3.0",
56
+ "typescript": "5.9.3",
57
+ "vite": "8.3.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@nativescript/core": ">=9.1.0 <10"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@nativescript/core": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "nativescript": {
68
+ "platforms": {
69
+ "ios": "7.0.0",
70
+ "android": "7.0.0"
71
+ }
72
+ },
73
+ "scripts": {
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,231 @@
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 {
30
+ CreateShaderOptions,
31
+ PresetConfig,
32
+ SharedGpu,
33
+ ShaderInstance,
34
+ WebGPUSupportInfo,
35
+ } from './types'
36
+
37
+ import { resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance } from './shared'
38
+ import type { Size } from './shared'
39
+
40
+ let gpuShim: any
41
+ let gpuResolved = false
42
+
43
+ /** The plugin's GPU shim is also installed as `navigator.gpu` — upstream
44
+ * (and TypeGPU's `configureContext`) reads it unconditionally. */
45
+ function ensureGpu(): any {
46
+ if (!gpuResolved) {
47
+ gpuResolved = true
48
+ try {
49
+ gpuShim = getGPU()
50
+ } catch {
51
+ gpuShim = null
52
+ }
53
+ }
54
+
55
+ const gpu = gpuShim
56
+ if (!gpu) {
57
+ return null
58
+ }
59
+
60
+ const g = globalThis as any
61
+ try {
62
+ if (!g.navigator) {
63
+ g.navigator = {}
64
+ }
65
+
66
+ if (!g.navigator.gpu) {
67
+ try {
68
+ g.navigator.gpu = gpu
69
+ } catch {
70
+ Object.defineProperty(g.navigator, 'gpu', { configurable: true, value: gpu })
71
+ }
72
+ }
73
+ } catch {
74
+ // `navigator` may be a read-only global — injected `gpu` still applies.
75
+ }
76
+
77
+ return gpu
78
+ }
79
+
80
+ function measure(view: any): Size {
81
+ const rect = view?.getBoundingClientRect?.()
82
+ const actual = view?.getActualSize?.()
83
+ const width = Number(rect?.width) || Number(actual?.width) || Number(view?.width) || 0
84
+ const height = Number(rect?.height) || Number(actual?.height) || Number(view?.height) || 0
85
+ return { width, height }
86
+ }
87
+
88
+ /** DOM-canvas-shaped facade over the plugin view. `width`/`height` writes
89
+ * (upstream's drawing-buffer bookkeeping) are stored locally so the
90
+ * plugin view's layout size is never mutated by buffer pixel counts. */
91
+ function hostCanvas(view: any): any {
92
+ const bag = { style: {} as Record<string, string>, width: 0, height: 0 }
93
+ return {
94
+ getContext: (kind: string, options?: any) => view.getContext(kind, options),
95
+ getBoundingClientRect: () => {
96
+ const { width, height } = measure(view)
97
+ return { x: 0, y: 0, top: 0, left: 0, right: width, bottom: height, width, height }
98
+ },
99
+ get clientWidth() {
100
+ return measure(view).width
101
+ },
102
+ get clientHeight() {
103
+ return measure(view).height
104
+ },
105
+ get width() {
106
+ return bag.width || measure(view).width
107
+ },
108
+ set width(value: number) {
109
+ bag.width = value
110
+ },
111
+ get height() {
112
+ return bag.height || measure(view).height
113
+ },
114
+ set height(value: number) {
115
+ bag.height = value
116
+ },
117
+ get style() {
118
+ return bag.style
119
+ },
120
+ parentElement: null,
121
+ }
122
+ }
123
+
124
+ async function acquireInjectedGpu(): Promise<SharedGpu | null> {
125
+ const gpu = ensureGpu()
126
+ if (!gpu?.requestAdapter) {
127
+ return null
128
+ }
129
+
130
+ try {
131
+ const adapter = await gpu.requestAdapter()
132
+ if (!adapter) {
133
+ return null
134
+ }
135
+
136
+ const device = await adapter.requestDevice()
137
+ return device ? { device, adapter } : null
138
+ } catch {
139
+ return null
140
+ }
141
+ }
142
+
143
+ export async function createShader(
144
+ canvas: unknown,
145
+ preset: PresetConfig,
146
+ options?: CreateShaderOptions,
147
+ ): Promise<ShaderInstance> {
148
+ const { upstream, signal } = resolveOptions(options, { observeElement: false })
149
+ throwIfAborted(signal)
150
+ const host = hostCanvas(canvas)
151
+ await waitForPositiveSize(() => measure(canvas), signal)
152
+ // Adapter-acquired device (shared default when the caller supplies no
153
+ // `gpu`) — never destroyed by the adapter, so one instance's destroy
154
+ // cannot kill another's shared device.
155
+ if (!upstream.gpu) {
156
+ const gpu = await acquireInjectedGpu()
157
+ if (gpu) {
158
+ upstream.gpu = gpu
159
+ }
160
+ }
161
+
162
+ throwIfAborted(signal)
163
+ const inner = await upstreamCreateShader(host, preset, upstream as any)
164
+ if (signal?.aborted) {
165
+ inner.destroy()
166
+ throwIfAborted(signal)
167
+ }
168
+
169
+ const view = canvas as any
170
+ const attach = (hide: () => void, show: () => void) => {
171
+ const onLayout = () => inner.resize()
172
+ Application.on(Application.suspendEvent, hide)
173
+ Application.on(Application.resumeEvent, show)
174
+ view?.on?.(View.unloadedEvent, hide)
175
+ view?.on?.(View.loadedEvent, show)
176
+ view?.on?.(View.layoutChangedEvent, onLayout)
177
+ return () => {
178
+ Application.off(Application.suspendEvent, hide)
179
+ Application.off(Application.resumeEvent, show)
180
+ view?.off?.(View.unloadedEvent, hide)
181
+ view?.off?.(View.loadedEvent, show)
182
+ view?.off?.(View.layoutChangedEvent, onLayout)
183
+ }
184
+ }
185
+
186
+ return wrapInstance(inner, attach)
187
+ }
188
+
189
+ export async function createSharedDevice(options?: {
190
+ powerPreference?: string
191
+ }): Promise<SharedGpu | null> {
192
+ ensureGpu()
193
+ return upstreamCreateSharedDevice(options as any)
194
+ }
195
+
196
+ export function isWebGPUSupported(): boolean {
197
+ ensureGpu()
198
+ return upstreamIsWebGPUSupported()
199
+ }
200
+
201
+ export function getWebGPUSupport(): Promise<WebGPUSupportInfo> {
202
+ ensureGpu()
203
+ return upstreamGetWebGPUSupport()
204
+ }
205
+
206
+ export function prefersReducedMotion(): boolean {
207
+ try {
208
+ if (isIOS) {
209
+ const check = (globalThis as any).UIAccessibilityIsReduceMotionEnabled
210
+ return typeof check === 'function' ? !!check() : false
211
+ }
212
+
213
+ if (isAndroid) {
214
+ const context = Application.android?.context
215
+ const settings = (globalThis as any).android?.provider?.Settings
216
+ if (!context || !settings?.Global) {
217
+ return false
218
+ }
219
+
220
+ const scale = settings.Global.getFloat(
221
+ context.getContentResolver(),
222
+ settings.Global.ANIMATOR_DURATION_SCALE,
223
+ 1,
224
+ )
225
+
226
+ return scale === 0
227
+ }
228
+ } catch {}
229
+
230
+ return false
231
+ }
@@ -0,0 +1,82 @@
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 {
16
+ CreateShaderOptions,
17
+ PresetConfig,
18
+ SharedGpu,
19
+ ShaderInstance,
20
+ WebGPUSupportInfo,
21
+ } from './types'
22
+
23
+ import { resolveOptions, throwIfAborted, waitForPositiveSize, wrapInstance } from './shared'
24
+ import type { Size } from './shared'
25
+
26
+ function measure(canvas: any): Size {
27
+ const rect = canvas?.getBoundingClientRect?.()
28
+ const width = Number(rect?.width) || Number(canvas?.width) || 0
29
+ const height = Number(rect?.height) || Number(canvas?.height) || 0
30
+ return { width, height }
31
+ }
32
+
33
+ function attachVisibility(hide: () => void, show: () => void): () => void {
34
+ const doc = (globalThis as any).document
35
+ if (!doc?.addEventListener) {
36
+ return () => {}
37
+ }
38
+
39
+ const onChange = () => (doc.visibilityState === 'hidden' ? hide() : show())
40
+ doc.addEventListener('visibilitychange', onChange)
41
+ return () => doc.removeEventListener('visibilitychange', onChange)
42
+ }
43
+
44
+ export async function createShader(
45
+ canvas: unknown,
46
+ preset: PresetConfig,
47
+ options?: CreateShaderOptions,
48
+ ): Promise<ShaderInstance> {
49
+ const { upstream, signal } = resolveOptions(options, {})
50
+ throwIfAborted(signal)
51
+ await waitForPositiveSize(() => measure(canvas), signal)
52
+ const inner = await upstreamCreateShader(canvas as any, preset, upstream)
53
+ if (signal?.aborted) {
54
+ // Unmounted while the async engine was starting — release the late
55
+ // completion instead of reviving a disposed surface.
56
+ inner.destroy()
57
+ throwIfAborted(signal)
58
+ }
59
+
60
+ return wrapInstance(inner, attachVisibility)
61
+ }
62
+
63
+ export function createSharedDevice(options?: {
64
+ powerPreference?: string
65
+ }): Promise<SharedGpu | null> {
66
+ return upstreamCreateSharedDevice(options as any)
67
+ }
68
+
69
+ export function isWebGPUSupported(): boolean {
70
+ return upstreamIsWebGPUSupported()
71
+ }
72
+
73
+ export function getWebGPUSupport(): Promise<WebGPUSupportInfo> {
74
+ return upstreamGetWebGPUSupport()
75
+ }
76
+
77
+ export function prefersReducedMotion(): boolean {
78
+ const matchMedia = (globalThis as any).matchMedia
79
+ return typeof matchMedia === 'function'
80
+ ? !!matchMedia('(prefers-reduced-motion: reduce)').matches
81
+ : false
82
+ }
@@ -0,0 +1,49 @@
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 {
8
+ CreateShaderOptions,
9
+ PresetConfig,
10
+ SharedGpu,
11
+ ShaderInstance,
12
+ WebGPUSupportInfo,
13
+ } from './types'
14
+
15
+ const UNSUPPORTED: ShaderInstance = {
16
+ getFailureReason: () => 'unsupported',
17
+ update: () => {},
18
+ resize: () => {},
19
+ pause: () => {},
20
+ resume: () => {},
21
+ destroy: () => {},
22
+ }
23
+
24
+ export async function createShader(
25
+ _canvas: unknown,
26
+ _preset: PresetConfig,
27
+ options?: CreateShaderOptions,
28
+ ): Promise<ShaderInstance> {
29
+ options?.onError?.('unsupported')
30
+ return UNSUPPORTED
31
+ }
32
+
33
+ export async function createSharedDevice(_options?: {
34
+ powerPreference?: string
35
+ }): Promise<SharedGpu | null> {
36
+ return null
37
+ }
38
+
39
+ export function isWebGPUSupported(): boolean {
40
+ return false
41
+ }
42
+
43
+ export async function getWebGPUSupport(): Promise<WebGPUSupportInfo> {
44
+ return { supported: false, reason: 'unsupported' }
45
+ }
46
+
47
+ export function prefersReducedMotion(): boolean {
48
+ return false
49
+ }
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,138 @@
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) {
15
+ throw abortError()
16
+ }
17
+ }
18
+
19
+ export interface Size {
20
+ width: number
21
+ height: number
22
+ }
23
+
24
+ /** Defer until the surface reports a positive size (surface not attached /
25
+ * zero-size layout → wait, don't init). Aborts via `signal`. */
26
+ export function waitForPositiveSize(measure: () => Size, signal?: AbortSignalLike): Promise<void> {
27
+ const first = measure()
28
+ if (first.width > 0 && first.height > 0) {
29
+ return Promise.resolve()
30
+ }
31
+
32
+ return new Promise<void>((resolve, reject) => {
33
+ let timer: ReturnType<typeof setTimeout> | undefined
34
+ const stop = () => {
35
+ if (timer !== undefined) {
36
+ clearTimeout(timer)
37
+ }
38
+
39
+ signal?.removeEventListener?.('abort', onAbort)
40
+ }
41
+
42
+ const onAbort = () => {
43
+ stop()
44
+ reject(abortError())
45
+ }
46
+
47
+ const poll = () => {
48
+ if (signal?.aborted) {
49
+ return onAbort()
50
+ }
51
+
52
+ const size = measure()
53
+ if (size.width > 0 && size.height > 0) {
54
+ stop()
55
+ resolve()
56
+ return
57
+ }
58
+
59
+ timer = setTimeout(poll, 50)
60
+ }
61
+
62
+ signal?.addEventListener?.('abort', onAbort)
63
+ timer = setTimeout(poll, 50)
64
+ })
65
+ }
66
+
67
+ /** Merge caller options with adapter defaults. `disableTelemetry` defaults
68
+ * on for this package's local/preset-driven use; callers may re-enable it.
69
+ * The adapter-only `signal` key is stripped before reaching upstream. */
70
+ export function resolveOptions(
71
+ options: CreateShaderOptions | undefined,
72
+ platformDefaults: Partial<CreateShaderOptions>,
73
+ ): { upstream: Omit<CreateShaderOptions, 'signal'>; signal?: AbortSignalLike } {
74
+ const { signal, ...rest } = options ?? {}
75
+ return {
76
+ signal,
77
+ upstream: {
78
+ ...platformDefaults,
79
+ ...rest,
80
+ disableTelemetry: options?.disableTelemetry ?? true,
81
+ },
82
+ }
83
+ }
84
+
85
+ /** Subscribe/unsubscribe pair for host hide/show (backgrounding, detach,
86
+ * document visibility). */
87
+ export type AttachLifecycle = (hide: () => void, show: () => void) => () => void
88
+
89
+ /** Wrap the upstream instance so host suspension composes with caller
90
+ * pause instead of overriding it: host hide pauses, host show resumes
91
+ * only when the caller did not explicitly pause. `destroy` detaches all
92
+ * adapter-owned listeners before delegating. */
93
+ export function wrapInstance(inner: ShaderInstance, attach: AttachLifecycle): ShaderInstance {
94
+ let callerPaused = false
95
+ let hostSuspended = false
96
+ let destroyed = false
97
+ const detach = attach(
98
+ () => {
99
+ if (destroyed) {
100
+ return
101
+ }
102
+
103
+ hostSuspended = true
104
+ inner.pause()
105
+ },
106
+ () => {
107
+ hostSuspended = false
108
+ if (!destroyed && !callerPaused) {
109
+ inner.resume()
110
+ }
111
+ },
112
+ )
113
+
114
+ return {
115
+ getFailureReason: () => inner.getFailureReason(),
116
+ update: (id, props) => inner.update(id, props),
117
+ resize: (width?, height?) => inner.resize(width, height),
118
+ pause: () => {
119
+ callerPaused = true
120
+ inner.pause()
121
+ },
122
+ resume: () => {
123
+ callerPaused = false
124
+ if (!hostSuspended) {
125
+ inner.resume()
126
+ }
127
+ },
128
+ destroy: () => {
129
+ if (destroyed) {
130
+ return
131
+ }
132
+
133
+ destroyed = true
134
+ detach()
135
+ inner.destroy()
136
+ },
137
+ }
138
+ }
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;