@driftengine/native-host 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +58 -0
  4. package/dist/activation.d.ts +26 -0
  5. package/dist/activation.js +38 -0
  6. package/dist/alphaCopy.d.ts +36 -0
  7. package/dist/alphaCopy.js +178 -0
  8. package/dist/audio.d.ts +55 -0
  9. package/dist/audio.js +193 -0
  10. package/dist/audioParams.d.ts +25 -0
  11. package/dist/audioParams.js +128 -0
  12. package/dist/bridge.d.ts +70 -0
  13. package/dist/bridge.js +170 -0
  14. package/dist/canvas.d.ts +97 -0
  15. package/dist/canvas.js +197 -0
  16. package/dist/canvasStyle.d.ts +23 -0
  17. package/dist/canvasStyle.js +79 -0
  18. package/dist/device.d.ts +34 -0
  19. package/dist/device.js +144 -0
  20. package/dist/dialogs.d.ts +23 -0
  21. package/dist/dialogs.js +85 -0
  22. package/dist/domEvents.d.ts +37 -0
  23. package/dist/domEvents.js +145 -0
  24. package/dist/domTree.d.ts +34 -0
  25. package/dist/domTree.js +194 -0
  26. package/dist/dualRumble.d.ts +19 -0
  27. package/dist/dualRumble.js +47 -0
  28. package/dist/eventClasses.d.ts +12 -0
  29. package/dist/eventClasses.js +35 -0
  30. package/dist/files.d.ts +16 -0
  31. package/dist/files.js +59 -0
  32. package/dist/fullscreen.d.ts +33 -0
  33. package/dist/fullscreen.js +65 -0
  34. package/dist/game.d.ts +25 -0
  35. package/dist/game.js +38 -0
  36. package/dist/gamepads.d.ts +65 -0
  37. package/dist/gamepads.js +178 -0
  38. package/dist/globals.d.ts +24 -0
  39. package/dist/globals.js +37 -0
  40. package/dist/gpu.d.ts +26 -0
  41. package/dist/gpu.js +47 -0
  42. package/dist/images.d.ts +50 -0
  43. package/dist/images.js +111 -0
  44. package/dist/index.d.ts +23 -0
  45. package/dist/index.js +21 -0
  46. package/dist/keyCodes.d.ts +30 -0
  47. package/dist/keyCodes.js +205 -0
  48. package/dist/keyEvents.d.ts +54 -0
  49. package/dist/keyEvents.js +117 -0
  50. package/dist/mediaQueries.d.ts +62 -0
  51. package/dist/mediaQueries.js +152 -0
  52. package/dist/mouseEvents.d.ts +96 -0
  53. package/dist/mouseEvents.js +287 -0
  54. package/dist/page.d.ts +118 -0
  55. package/dist/page.js +302 -0
  56. package/dist/pointerCapture.d.ts +32 -0
  57. package/dist/pointerCapture.js +69 -0
  58. package/dist/present.d.ts +27 -0
  59. package/dist/present.js +83 -0
  60. package/dist/rawPad.d.ts +44 -0
  61. package/dist/rawPad.js +65 -0
  62. package/dist/readback.d.ts +18 -0
  63. package/dist/readback.js +46 -0
  64. package/dist/runtime.d.ts +69 -0
  65. package/dist/runtime.js +103 -0
  66. package/dist/standardPad.d.ts +49 -0
  67. package/dist/standardPad.js +80 -0
  68. package/dist/textHost.d.ts +37 -0
  69. package/dist/textHost.js +43 -0
  70. package/dist/threads.d.ts +27 -0
  71. package/dist/threads.js +59 -0
  72. package/dist/tileFiles.d.ts +16 -0
  73. package/dist/tileFiles.js +36 -0
  74. package/dist/touchEvents.d.ts +67 -0
  75. package/dist/touchEvents.js +210 -0
  76. package/dist/uiEvents.d.ts +157 -0
  77. package/dist/uiEvents.js +206 -0
  78. package/dist/uiTouch.d.ts +59 -0
  79. package/dist/uiTouch.js +75 -0
  80. package/dist/window.d.ts +71 -0
  81. package/dist/window.js +164 -0
  82. package/dist/windowMode.d.ts +19 -0
  83. package/dist/windowMode.js +15 -0
  84. package/dist/windowTitle.d.ts +18 -0
  85. package/dist/windowTitle.js +33 -0
  86. package/dist/workerScope.mjs +48 -0
  87. package/licenses/GPL-3.0.txt +674 -0
  88. package/package.json +83 -0
  89. package/src/activation.ts +42 -0
  90. package/src/alphaCopy.ts +201 -0
  91. package/src/audio.ts +246 -0
  92. package/src/audioParams.ts +150 -0
  93. package/src/bridge.ts +230 -0
  94. package/src/canvas.ts +227 -0
  95. package/src/canvasStyle.ts +89 -0
  96. package/src/device.ts +167 -0
  97. package/src/dialogs.ts +98 -0
  98. package/src/domEvents.ts +170 -0
  99. package/src/domTree.ts +215 -0
  100. package/src/drs.d.ts +9 -0
  101. package/src/dualRumble.ts +58 -0
  102. package/src/eventClasses.ts +46 -0
  103. package/src/files.ts +61 -0
  104. package/src/fixtures/grey.jpg +0 -0
  105. package/src/fixtures/rgb.png +0 -0
  106. package/src/fixtures/tone.opus +0 -0
  107. package/src/fullscreen.ts +71 -0
  108. package/src/game.ts +53 -0
  109. package/src/gamepads.ts +195 -0
  110. package/src/globals.ts +39 -0
  111. package/src/gpu.ts +59 -0
  112. package/src/images.ts +148 -0
  113. package/src/index.ts +24 -0
  114. package/src/keyCodes.ts +203 -0
  115. package/src/keyEvents.ts +135 -0
  116. package/src/mediaQueries.ts +173 -0
  117. package/src/mouseEvents.ts +328 -0
  118. package/src/page.ts +337 -0
  119. package/src/pointerCapture.ts +73 -0
  120. package/src/present.ts +92 -0
  121. package/src/rawPad.ts +85 -0
  122. package/src/readback.ts +61 -0
  123. package/src/runtime.ts +162 -0
  124. package/src/standardPad.ts +126 -0
  125. package/src/textHost.ts +66 -0
  126. package/src/threads.ts +66 -0
  127. package/src/tileFiles.ts +39 -0
  128. package/src/touchEvents.ts +238 -0
  129. package/src/uiEvents.ts +268 -0
  130. package/src/uiTouch.ts +97 -0
  131. package/src/window.ts +192 -0
  132. package/src/windowMode.ts +24 -0
  133. package/src/windowTitle.ts +36 -0
  134. package/src/workerScope.mjs +48 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A canvas with no page around it: a drawing buffer, a CSS box, and a WebGPU context.
3
+ *
4
+ * **What the engine asks of a canvas, and no more.** Its WebGPU backend takes a canvas, asks it
5
+ * for a `webgpu` context, configures that against its device, sets the drawing buffer's size, and
6
+ * each frame draws into `getCurrentTexture()`. The renderer's public measurements read the canvas's
7
+ * CSS box. That is the whole surface, so that is what this implements — and anything a scene reaches
8
+ * for beyond it is a gap the host names when it meets one, rather than a document this pretends to
9
+ * be.
10
+ *
11
+ * **The frame lands in a texture the host owns.** A browser presents the canvas itself at the end of
12
+ * a task; a host has to, so the texture carries two usages the engine never asks for — sampled, to be
13
+ * drawn into the window, and copyable, to be read back by the pixel gate.
14
+ */
15
+ import { CanvasStyle } from './canvasStyle.ts';
16
+ import { type Handlers, HostNode } from './domTree.ts';
17
+ import { INPUT_TYPES, type PageDocument } from './page.ts';
18
+ import { PointerCapture } from './pointerCapture.ts';
19
+ export interface NativeCanvas extends Handlers<(typeof INPUT_TYPES)[number]> {
20
+ }
21
+ export declare class NativeCanvas extends HostNode {
22
+ /** The drawing buffer, in device pixels, which the engine sets. */
23
+ width: number;
24
+ height: number;
25
+ /** Device pixels per CSS pixel. */
26
+ readonly pixelRatio: number;
27
+ /** Whether there is a window to present to: false while it is minimised to nothing. */
28
+ visible: boolean;
29
+ private cssWidth;
30
+ private cssHeight;
31
+ private context;
32
+ /** Set by the page that holds this canvas (`page.ts`); a canvas outside one has no pointer to lock. */
33
+ lockPointer: (() => Promise<void>) | null;
34
+ /** Set by the page too, which is what holds the focus and the fullscreen. */
35
+ focuser: ((focus: boolean) => void) | null;
36
+ fullscreener: (() => Promise<void>) | null;
37
+ capture: PointerCapture | null;
38
+ /** Set by what drives the window's mouse, which is where a cursor is shown (`canvasStyle.ts`). */
39
+ cursorer: ((css: string) => void) | null;
40
+ readonly style: CanvasStyle;
41
+ /**
42
+ * A canvas is focusable only once it is given a `tabIndex`, as a browser's is: until then it
43
+ * reports −1 and `focus()` does nothing.
44
+ */
45
+ private focusIndex;
46
+ constructor(width: number, height: number, pixelRatio?: number);
47
+ /** The CSS box, which is what `cssWidth` and `aspect` are asked of. */
48
+ get clientWidth(): number;
49
+ get clientHeight(): number;
50
+ getBoundingClientRect(): DOMRect;
51
+ /**
52
+ * `webgpu`, and nothing else — which is what a browser's canvas answers once it has given one.
53
+ * The engine never asks for another here: the backend is chosen before a context is taken.
54
+ */
55
+ getContext(kind: string): GPUCanvasContext | null;
56
+ get tabIndex(): number;
57
+ set tabIndex(index: number);
58
+ /** Focus, through the page; nothing, as in a browser, for a canvas that is not focusable. */
59
+ focus(_options?: FocusOptions): void;
60
+ blur(): void;
61
+ /** Capture, through the page, which is what knows the pointers (`pointerCapture.ts`). */
62
+ setPointerCapture(pointerId: number): void;
63
+ releasePointerCapture(pointerId: number): void;
64
+ hasPointerCapture(pointerId: number): boolean;
65
+ /** The document this canvas is in, once a page holds it. */
66
+ get ownerDocument(): PageDocument;
67
+ /** Fullscreen, through the page, which is what holds the window. */
68
+ requestFullscreen(_options?: FullscreenOptions): Promise<void>;
69
+ /** A canvas no page holds knows no pointers, so every id is unknown to it. */
70
+ private pointers;
71
+ /** What a browser's canvas answers, through the page, which is what holds the lock. */
72
+ requestPointerLock(_options?: unknown): Promise<void>;
73
+ /**
74
+ * The window's size, in device pixels, as the host is told it.
75
+ *
76
+ * **A window minimised to nothing keeps the last size it had.** It reports zero by zero, and a
77
+ * texture of no size is a validation error that invalidates every command buffer that touches
78
+ * it; the engine sizes its targets from this canvas, so this canvas never goes to zero. `visible`
79
+ * says there is nowhere to present to, and the host skips presenting until there is.
80
+ */
81
+ resizeTo(width: number, height: number): void;
82
+ }
83
+ export declare class NativeCanvasContext {
84
+ readonly canvas: NativeCanvas;
85
+ private configuration;
86
+ private texture;
87
+ constructor(canvas: NativeCanvas);
88
+ configure(configuration: GPUCanvasConfiguration): void;
89
+ unconfigure(): void;
90
+ getConfiguration(): GPUCanvasConfiguration | null;
91
+ /**
92
+ * This frame's texture: the same one until the drawing buffer changes size, and a new one when it
93
+ * does, as a browser's is.
94
+ */
95
+ getCurrentTexture(): GPUTexture;
96
+ private dropTexture;
97
+ }
package/dist/canvas.js ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * A canvas with no page around it: a drawing buffer, a CSS box, and a WebGPU context.
3
+ *
4
+ * **What the engine asks of a canvas, and no more.** Its WebGPU backend takes a canvas, asks it
5
+ * for a `webgpu` context, configures that against its device, sets the drawing buffer's size, and
6
+ * each frame draws into `getCurrentTexture()`. The renderer's public measurements read the canvas's
7
+ * CSS box. That is the whole surface, so that is what this implements — and anything a scene reaches
8
+ * for beyond it is a gap the host names when it meets one, rather than a document this pretends to
9
+ * be.
10
+ *
11
+ * **The frame lands in a texture the host owns.** A browser presents the canvas itself at the end of
12
+ * a task; a host has to, so the texture carries two usages the engine never asks for — sampled, to be
13
+ * drawn into the window, and copyable, to be read back by the pixel gate.
14
+ */
15
+ import { CanvasStyle } from './canvasStyle.js';
16
+ import { HostNode } from './domTree.js';
17
+ import { INPUT_TYPES } from './page.js';
18
+ import { PointerCapture } from './pointerCapture.js';
19
+ /* Texture usage flags, named here rather than read off a global a host may not have installed. */
20
+ const COPY_SRC = 0x01;
21
+ const TEXTURE_BINDING = 0x04;
22
+ const RENDER_ATTACHMENT = 0x10;
23
+ export class NativeCanvas extends HostNode {
24
+ /** The drawing buffer, in device pixels, which the engine sets. */
25
+ width;
26
+ height;
27
+ /** Device pixels per CSS pixel. */
28
+ pixelRatio;
29
+ /** Whether there is a window to present to: false while it is minimised to nothing. */
30
+ visible = true;
31
+ cssWidth;
32
+ cssHeight;
33
+ context = null;
34
+ /** Set by the page that holds this canvas (`page.ts`); a canvas outside one has no pointer to lock. */
35
+ lockPointer = null;
36
+ /** Set by the page too, which is what holds the focus and the fullscreen. */
37
+ focuser = null;
38
+ fullscreener = null;
39
+ capture = null;
40
+ /** Set by what drives the window's mouse, which is where a cursor is shown (`canvasStyle.ts`). */
41
+ cursorer = null;
42
+ style = new CanvasStyle((css) => this.cursorer?.(css));
43
+ /**
44
+ * A canvas is focusable only once it is given a `tabIndex`, as a browser's is: until then it
45
+ * reports −1 and `focus()` does nothing.
46
+ */
47
+ focusIndex = null;
48
+ static {
49
+ HostNode.handles(NativeCanvas.prototype, INPUT_TYPES);
50
+ }
51
+ constructor(width, height, pixelRatio = 1) {
52
+ super();
53
+ this.pixelRatio = pixelRatio;
54
+ this.width = width;
55
+ this.height = height;
56
+ this.cssWidth = width / pixelRatio;
57
+ this.cssHeight = height / pixelRatio;
58
+ }
59
+ /** The CSS box, which is what `cssWidth` and `aspect` are asked of. */
60
+ get clientWidth() {
61
+ return this.cssWidth;
62
+ }
63
+ get clientHeight() {
64
+ return this.cssHeight;
65
+ }
66
+ getBoundingClientRect() {
67
+ const width = this.cssWidth;
68
+ const height = this.cssHeight;
69
+ const rect = { x: 0, y: 0, left: 0, top: 0, right: width, bottom: height, width, height };
70
+ return { ...rect, toJSON: () => rect };
71
+ }
72
+ /**
73
+ * `webgpu`, and nothing else — which is what a browser's canvas answers once it has given one.
74
+ * The engine never asks for another here: the backend is chosen before a context is taken.
75
+ */
76
+ getContext(kind) {
77
+ if (kind !== 'webgpu')
78
+ return null;
79
+ if (this.context === null)
80
+ this.context = new NativeCanvasContext(this);
81
+ return this.context;
82
+ }
83
+ get tabIndex() {
84
+ return this.focusIndex ?? -1;
85
+ }
86
+ set tabIndex(index) {
87
+ this.focusIndex = Math.trunc(index);
88
+ }
89
+ /** Focus, through the page; nothing, as in a browser, for a canvas that is not focusable. */
90
+ focus(_options) {
91
+ if (this.focusIndex !== null)
92
+ this.focuser?.(true);
93
+ }
94
+ blur() {
95
+ this.focuser?.(false);
96
+ }
97
+ /** Capture, through the page, which is what knows the pointers (`pointerCapture.ts`). */
98
+ setPointerCapture(pointerId) {
99
+ this.pointers().set(this, pointerId);
100
+ }
101
+ releasePointerCapture(pointerId) {
102
+ this.pointers().release(this, pointerId);
103
+ }
104
+ hasPointerCapture(pointerId) {
105
+ return this.capture?.has(this, pointerId) ?? false;
106
+ }
107
+ /** The document this canvas is in, once a page holds it. */
108
+ get ownerDocument() {
109
+ if (this.parentNode === null) {
110
+ throw new Error('[driftengine] a canvas no page holds is in no document');
111
+ }
112
+ return this.parentNode;
113
+ }
114
+ /** Fullscreen, through the page, which is what holds the window. */
115
+ requestFullscreen(_options) {
116
+ if (this.fullscreener === null) {
117
+ return Promise.reject(new TypeError('[driftengine] a canvas with no window cannot fill it'));
118
+ }
119
+ return this.fullscreener();
120
+ }
121
+ /** A canvas no page holds knows no pointers, so every id is unknown to it. */
122
+ pointers() {
123
+ return this.capture ?? new PointerCapture();
124
+ }
125
+ /** What a browser's canvas answers, through the page, which is what holds the lock. */
126
+ requestPointerLock(_options) {
127
+ if (this.lockPointer === null) {
128
+ return Promise.reject(new Error('[driftengine] a canvas with no window has no pointer to lock'));
129
+ }
130
+ return this.lockPointer();
131
+ }
132
+ /**
133
+ * The window's size, in device pixels, as the host is told it.
134
+ *
135
+ * **A window minimised to nothing keeps the last size it had.** It reports zero by zero, and a
136
+ * texture of no size is a validation error that invalidates every command buffer that touches
137
+ * it; the engine sizes its targets from this canvas, so this canvas never goes to zero. `visible`
138
+ * says there is nowhere to present to, and the host skips presenting until there is.
139
+ */
140
+ resizeTo(width, height) {
141
+ if (width <= 0 || height <= 0) {
142
+ this.visible = false;
143
+ return;
144
+ }
145
+ this.visible = true;
146
+ this.width = width;
147
+ this.height = height;
148
+ this.cssWidth = width / this.pixelRatio;
149
+ this.cssHeight = height / this.pixelRatio;
150
+ }
151
+ }
152
+ export class NativeCanvasContext {
153
+ canvas;
154
+ configuration = null;
155
+ texture = null;
156
+ constructor(canvas) {
157
+ this.canvas = canvas;
158
+ }
159
+ configure(configuration) {
160
+ this.dropTexture();
161
+ const asked = configuration.usage ?? RENDER_ATTACHMENT;
162
+ this.configuration = { ...configuration, usage: asked | TEXTURE_BINDING | COPY_SRC };
163
+ }
164
+ unconfigure() {
165
+ this.dropTexture();
166
+ this.configuration = null;
167
+ }
168
+ getConfiguration() {
169
+ return this.configuration;
170
+ }
171
+ /**
172
+ * This frame's texture: the same one until the drawing buffer changes size, and a new one when it
173
+ * does, as a browser's is.
174
+ */
175
+ getCurrentTexture() {
176
+ const configuration = this.configuration;
177
+ if (configuration === null) {
178
+ throw new Error('[driftengine] this canvas is not configured, so it has no frame to give');
179
+ }
180
+ const { width, height } = this.canvas;
181
+ const current = this.texture;
182
+ if (current !== null && current.width === width && current.height === height)
183
+ return current;
184
+ this.dropTexture();
185
+ this.texture = configuration.device.createTexture({
186
+ label: 'native canvas',
187
+ size: { width, height },
188
+ format: configuration.format,
189
+ usage: configuration.usage ?? RENDER_ATTACHMENT,
190
+ });
191
+ return this.texture;
192
+ }
193
+ dropTexture() {
194
+ this.texture?.destroy();
195
+ this.texture = null;
196
+ }
197
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A canvas's `style`, which a page writes to as it would a browser's: the cursor is the window's,
3
+ * and anything else is kept and read back, meaning nothing without a layout.
4
+ *
5
+ * **A page sets a cursor on the canvas it draws in** — `grab` over a view it can drag, `pointer`
6
+ * over a button, `none` in a first-person game — and a canvas with no `style` answered each with a
7
+ * `TypeError` inside the page's handler. CSS names more cursors than SDL has, so each is the nearest
8
+ * of SDL's twelve: `grab` and `grabbing` are its hand, as is `pointer`, since SDL has no open palm.
9
+ * A cursor with fallbacks, `url(hand.png) 4 4, crosshair`, is its last keyword; an image cursor is
10
+ * not drawn.
11
+ */
12
+ /** The SDL cursor a CSS `cursor` value asks for: null to hide it, the arrow for one SDL lacks. */
13
+ export declare function sdlCursor(css: string): string | null;
14
+ export declare class CanvasStyle {
15
+ #private;
16
+ [property: string]: unknown;
17
+ constructor(onCursor: (css: string) => void);
18
+ get cursor(): string;
19
+ set cursor(css: string);
20
+ setProperty(name: string, value: string): void;
21
+ getPropertyValue(name: string): string;
22
+ removeProperty(name: string): string;
23
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * A canvas's `style`, which a page writes to as it would a browser's: the cursor is the window's,
3
+ * and anything else is kept and read back, meaning nothing without a layout.
4
+ *
5
+ * **A page sets a cursor on the canvas it draws in** — `grab` over a view it can drag, `pointer`
6
+ * over a button, `none` in a first-person game — and a canvas with no `style` answered each with a
7
+ * `TypeError` inside the page's handler. CSS names more cursors than SDL has, so each is the nearest
8
+ * of SDL's twelve: `grab` and `grabbing` are its hand, as is `pointer`, since SDL has no open palm.
9
+ * A cursor with fallbacks, `url(hand.png) 4 4, crosshair`, is its last keyword; an image cursor is
10
+ * not drawn.
11
+ */
12
+ /** SDL's system cursor for a CSS keyword, or null for `none`, which hides it. */
13
+ const SDL_CURSOR = {
14
+ auto: 'arrow',
15
+ default: 'arrow',
16
+ pointer: 'hand',
17
+ grab: 'hand',
18
+ grabbing: 'hand',
19
+ text: 'ibeam',
20
+ 'vertical-text': 'ibeam',
21
+ wait: 'wait',
22
+ progress: 'waitarrow',
23
+ crosshair: 'crosshair',
24
+ move: 'sizeall',
25
+ 'all-scroll': 'sizeall',
26
+ 'not-allowed': 'no',
27
+ 'no-drop': 'no',
28
+ 'ew-resize': 'sizewe',
29
+ 'col-resize': 'sizewe',
30
+ 'e-resize': 'sizewe',
31
+ 'w-resize': 'sizewe',
32
+ 'ns-resize': 'sizens',
33
+ 'row-resize': 'sizens',
34
+ 'n-resize': 'sizens',
35
+ 's-resize': 'sizens',
36
+ 'nwse-resize': 'sizenwse',
37
+ 'nw-resize': 'sizenwse',
38
+ 'se-resize': 'sizenwse',
39
+ 'nesw-resize': 'sizenesw',
40
+ 'ne-resize': 'sizenesw',
41
+ 'sw-resize': 'sizenesw',
42
+ none: null,
43
+ };
44
+ /** The SDL cursor a CSS `cursor` value asks for: null to hide it, the arrow for one SDL lacks. */
45
+ export function sdlCursor(css) {
46
+ const keyword = css.split(',').at(-1)?.trim().toLowerCase() ?? '';
47
+ const cursor = SDL_CURSOR[keyword];
48
+ return cursor === undefined ? 'arrow' : cursor;
49
+ }
50
+ const camel = (name) => name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
51
+ export class CanvasStyle {
52
+ #cursor = '';
53
+ #onCursor;
54
+ constructor(onCursor) {
55
+ this.#onCursor = onCursor;
56
+ }
57
+ get cursor() {
58
+ return this.#cursor;
59
+ }
60
+ set cursor(css) {
61
+ this.#cursor = css;
62
+ this.#onCursor(css);
63
+ }
64
+ setProperty(name, value) {
65
+ this[camel(name)] = value;
66
+ }
67
+ getPropertyValue(name) {
68
+ const value = this[camel(name)];
69
+ return typeof value === 'string' ? value : '';
70
+ }
71
+ removeProperty(name) {
72
+ const was = this.getPropertyValue(name);
73
+ if (camel(name) === 'cursor')
74
+ this.cursor = '';
75
+ else
76
+ delete this[camel(name)];
77
+ return was;
78
+ }
79
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The device, made to say what it rejected, when its binding will not.
3
+ *
4
+ * **The engine hears one thing from its device**: `uncapturederror`, which its surface turns into a
5
+ * line naming what the GPU refused — the only warning a WebGPU frame gets that it drew nothing.
6
+ * `@kmamal/gpu` 0.2.0 refuses that listener ("no overload matched for addEventListener") and will not
7
+ * take an `onuncapturederror` either (measured 2026-09-18). Error scopes work, so the host opens one
8
+ * around each frame and hands whatever it caught to the listeners the engine registered: the same
9
+ * message, at the end of the frame that caused it, which is when a browser raises it too.
10
+ *
11
+ * **Detected, not assumed.** A device whose binding takes the listener is left exactly as it is, so a
12
+ * later binding that delivers its own events costs nothing here.
13
+ *
14
+ * What it gives up: an error from work outside a frame — a pipeline built at mount — is only caught
15
+ * if the host has a scope open at the time. Dawn still prints every validation error to the
16
+ * terminal itself, so none is silent.
17
+ */
18
+ import type { AlphaStep } from './alphaCopy.ts';
19
+ export interface FrameErrors {
20
+ /** Before the frame: catch what it does. */
21
+ open(): void;
22
+ /** After it: hand anything caught to the engine's listeners. */
23
+ close(): void;
24
+ }
25
+ /** The frame's error scopes for a device this host adapted, or `null` for one it left alone. */
26
+ export declare function errorsOf(device: GPUDevice): FrameErrors | null;
27
+ /** Skia's premultiply, the integer rounding Chrome's image decoders use. */
28
+ export declare function premultiplied(straight: Uint8Array): Uint8Array;
29
+ /**
30
+ * The step a copy takes between the alpha an image is held in and the alpha its texture wants, or
31
+ * null where the two agree and a browser copies the bytes as they are.
32
+ */
33
+ export declare function alphaStepFor(held: boolean, wanted: boolean): AlphaStep | null;
34
+ export declare function adaptDevice(device: GPUDevice): GPUDevice;
package/dist/device.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The device, made to say what it rejected, when its binding will not.
3
+ *
4
+ * **The engine hears one thing from its device**: `uncapturederror`, which its surface turns into a
5
+ * line naming what the GPU refused — the only warning a WebGPU frame gets that it drew nothing.
6
+ * `@kmamal/gpu` 0.2.0 refuses that listener ("no overload matched for addEventListener") and will not
7
+ * take an `onuncapturederror` either (measured 2026-09-18). Error scopes work, so the host opens one
8
+ * around each frame and hands whatever it caught to the listeners the engine registered: the same
9
+ * message, at the end of the frame that caused it, which is when a browser raises it too.
10
+ *
11
+ * **Detected, not assumed.** A device whose binding takes the listener is left exactly as it is, so a
12
+ * later binding that delivers its own events costs nothing here.
13
+ *
14
+ * What it gives up: an error from work outside a frame — a pipeline built at mount — is only caught
15
+ * if the host has a scope open at the time. Dawn still prints every validation error to the
16
+ * terminal itself, so none is silent.
17
+ */
18
+ import { AlphaCopier } from './alphaCopy.js';
19
+ import { isHostBitmap } from './images.js';
20
+ const adapted = new WeakMap();
21
+ /** The frame's error scopes for a device this host adapted, or `null` for one it left alone. */
22
+ export function errorsOf(device) {
23
+ return adapted.get(device) ?? null;
24
+ }
25
+ /** Skia's premultiply, the integer rounding Chrome's image decoders use. */
26
+ export function premultiplied(straight) {
27
+ const out = new Uint8Array(straight.length);
28
+ for (let at = 0; at < straight.length; at += 4) {
29
+ const alpha = straight[at + 3];
30
+ out[at + 3] = alpha;
31
+ for (let channel = 0; channel < 3; channel += 1) {
32
+ out[at + channel] = ((straight[at + channel] * alpha + 128) * 257) >> 16;
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+ /**
38
+ * The step a copy takes between the alpha an image is held in and the alpha its texture wants, or
39
+ * null where the two agree and a browser copies the bytes as they are.
40
+ */
41
+ export function alphaStepFor(held, wanted) {
42
+ if (held === wanted)
43
+ return null;
44
+ return held ? 'unpremultiply' : 'premultiply';
45
+ }
46
+ /**
47
+ * The queue, taking an image the host decoded where a browser would take one it decoded.
48
+ *
49
+ * **The engine puts a decoded image on the device with `copyExternalImageToTexture`**, and a browser
50
+ * copies from its own image there. A host's image is a `HostBitmap` from `images.ts`, four bytes a
51
+ * pixel that Dawn's binding cannot read as an image, so it is copied the way Chrome copies one:
52
+ *
53
+ * - **held as the texture wants it**, the bytes go in with `writeTexture`. Straight or
54
+ * premultiplied, into a plain or an sRGB texture, Chrome writes exactly these (measured);
55
+ * - **held one way and wanted the other**, `alphaCopy.ts` runs the step on the device.
56
+ *
57
+ * A browser holds a decoded image premultiplied unless told `premultiplyAlpha: 'none'`, with Skia's
58
+ * integer rounding, so that is how the bytes are held here. A browser flips rows only when asked,
59
+ * and so does this. Any other source goes to the binding.
60
+ */
61
+ function adaptQueue(device) {
62
+ const queue = device.queue;
63
+ if (queue === undefined)
64
+ return;
65
+ const original = queue.copyExternalImageToTexture.bind(queue);
66
+ const copier = new AlphaCopier(device);
67
+ queue.copyExternalImageToTexture = (source, destination, copySize) => {
68
+ /* A size may be any iterable of numbers or a dictionary; one shape, for both uses below. */
69
+ const size = Symbol.iterator in Object(copySize)
70
+ ? Array.from(copySize)
71
+ : copySize;
72
+ const image = source.source;
73
+ if (!isHostBitmap(image)) {
74
+ original(source, destination, size);
75
+ return;
76
+ }
77
+ const from = source.origin;
78
+ if ((from?.x ?? 0) !== 0 || (from?.y ?? 0) !== 0) {
79
+ throw new Error('[driftengine] the native host copies a whole image, from its top-left corner');
80
+ }
81
+ const row = image.width * 4;
82
+ const straight = new Uint8Array(image.data.buffer, image.data.byteOffset, image.data.byteLength);
83
+ let bytes = image.premultiplied ? premultiplied(straight) : straight;
84
+ if (source.flipY === true) {
85
+ const flipped = new Uint8Array(bytes.length);
86
+ for (let y = 0; y < image.height; y += 1) {
87
+ flipped.set(bytes.subarray(y * row, (y + 1) * row), (image.height - 1 - y) * row);
88
+ }
89
+ bytes = flipped;
90
+ }
91
+ const width = Array.isArray(size) ? size[0] : size.width;
92
+ const height = Array.isArray(size) ? (size[1] ?? 1) : (size.height ?? 1);
93
+ const step = alphaStepFor(image.premultiplied, destination.premultipliedAlpha === true);
94
+ if (step !== null) {
95
+ copier.copy(bytes, image.width, image.height, step, destination, width, height);
96
+ return;
97
+ }
98
+ queue.writeTexture({
99
+ texture: destination.texture,
100
+ mipLevel: destination.mipLevel ?? 0,
101
+ origin: destination.origin ?? { x: 0, y: 0, z: 0 },
102
+ }, bytes, { offset: 0, bytesPerRow: row, rowsPerImage: image.height }, { width, height, depthOrArrayLayers: 1 });
103
+ };
104
+ }
105
+ export function adaptDevice(device) {
106
+ adaptQueue(device);
107
+ const original = device.addEventListener.bind(device);
108
+ try {
109
+ original('uncapturederror', () => undefined);
110
+ return device;
111
+ }
112
+ catch {
113
+ /* The binding refused it: this device needs the adapter. */
114
+ }
115
+ const listeners = new Set();
116
+ const deliver = (error) => {
117
+ const event = { type: 'uncapturederror', error };
118
+ for (const listener of listeners) {
119
+ if (typeof listener === 'function')
120
+ listener(event);
121
+ else
122
+ listener.handleEvent(event);
123
+ }
124
+ };
125
+ const scoped = device;
126
+ scoped.addEventListener = (type, listener) => {
127
+ if (type === 'uncapturederror')
128
+ listeners.add(listener);
129
+ };
130
+ scoped.removeEventListener = (type, listener) => {
131
+ if (type === 'uncapturederror')
132
+ listeners.delete(listener);
133
+ };
134
+ adapted.set(device, {
135
+ open: () => device.pushErrorScope('validation'),
136
+ close: () => {
137
+ void device.popErrorScope().then((error) => {
138
+ if (error !== null)
139
+ deliver(error);
140
+ });
141
+ },
142
+ });
143
+ return device;
144
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Open and save dialogs from the desktop, since SDL 2 gives a window none.
3
+ *
4
+ * **The desktop's own dialog program, asked and waited for**: zenity, which GNOME and the GTK
5
+ * desktops carry, and kdialog where zenity is not installed. What comes back is what the desktop
6
+ * shell hands a game (`main/ipc.ts`): a picked file's name and bytes, `null` for a cancelled pick,
7
+ * and `false` for a cancelled save — a cancellation being exit code 1 for both programs.
8
+ *
9
+ * **A filter is the extensions the game named**, `.json` becoming `*.json`; a MIME type has no
10
+ * pattern either program takes, so it is left out rather than guessed at.
11
+ *
12
+ * What it gives up: Windows and macOS, which carry neither program and are Task 7's; the desktop
13
+ * portal, which is what a sandboxed Linux build would need and answers over D-Bus signals rather
14
+ * than an exit code; and where neither program is installed, `desktopFiles` answers null and the
15
+ * bridge reports every pick cancelled.
16
+ */
17
+ import type { NativeFiles } from './bridge.ts';
18
+ /** Run a dialog program and hand back its exit code and what it printed. */
19
+ export type DialogRunner = (command: string, args: readonly string[]) => Promise<{
20
+ code: number;
21
+ stdout: string;
22
+ }>;
23
+ export declare function desktopFiles(available?: (name: string) => boolean, run?: DialogRunner): NativeFiles | null;