@adobe/ecs-gpu 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -2
- package/dist/index.js +688 -0
- package/dist/index.js.map +1 -0
- package/dist/types/cache/cache.d.ts +6 -0
- package/dist/types/cache/public.d.ts +1 -0
- package/dist/types/device/device.d.ts +24 -0
- package/dist/types/device/public.d.ts +1 -0
- package/dist/types/frame/frame.d.ts +14 -0
- package/dist/types/frame/public.d.ts +1 -0
- package/dist/types/guide/01-targets.d.ts +1 -0
- package/dist/types/guide/02-frames.d.ts +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/surface/public.d.ts +1 -0
- package/dist/types/surface/surface.d.ts +81 -0
- package/dist/types/target/public.d.ts +1 -0
- package/dist/types/target/target.d.ts +78 -0
- package/dist/types/view/public.d.ts +1 -0
- package/dist/types/view/view.d.ts +69 -0
- package/package.json +49 -3
- package/src/cache/cache.ts +23 -0
- package/src/cache/public.ts +2 -0
- package/src/device/device.ts +46 -0
- package/src/device/public.ts +2 -0
- package/src/frame/frame.ts +53 -0
- package/src/frame/public.ts +2 -0
- package/src/guide/01-targets.md +13 -0
- package/src/guide/01-targets.out +5 -0
- package/src/guide/01-targets.ts +40 -0
- package/src/guide/02-frames.md +15 -0
- package/src/guide/02-frames.out +2 -0
- package/src/guide/02-frames.ts +33 -0
- package/src/guide/README.md +7 -0
- package/src/index.ts +11 -0
- package/src/surface/public.ts +2 -0
- package/src/surface/surface.ts +140 -0
- package/src/target/public.ts +2 -0
- package/src/target/target.ts +133 -0
- package/src/view/public.ts +2 -0
- package/src/view/view.ts +125 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { Finalizer, Index, System, gpuDevice } from "@adobe/ecs";
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __exportAll = (all, no_symbols) => {
|
|
5
|
+
let target = {};
|
|
6
|
+
for (var name in all) __defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
return target;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/cache/cache.ts
|
|
15
|
+
/**
|
|
16
|
+
* A cache of what `create` makes for each device and key: a pipeline, a layout, a sampler. Its values for a device go
|
|
17
|
+
* with the device, so a device a world replaced, or one that was lost, holds nothing here. Keys are compared by
|
|
18
|
+
* `keyOf`, by default their JSON.
|
|
19
|
+
*/
|
|
20
|
+
const perDevice = (create, keyOf = (key) => JSON.stringify(key)) => {
|
|
21
|
+
const devices = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
return (device, key) => {
|
|
23
|
+
let made = devices.get(device);
|
|
24
|
+
if (made === void 0) {
|
|
25
|
+
made = /* @__PURE__ */ new Map();
|
|
26
|
+
devices.set(device, made);
|
|
27
|
+
}
|
|
28
|
+
const name = keyOf(key);
|
|
29
|
+
const found = made.get(name);
|
|
30
|
+
if (found !== void 0) return found.value;
|
|
31
|
+
const value = create(device, key);
|
|
32
|
+
made.set(name, { value });
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/cache/public.ts
|
|
38
|
+
var public_exports = /* @__PURE__ */ __exportAll({ perDevice: () => perDevice });
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/device/device.ts
|
|
41
|
+
/**
|
|
42
|
+
* A device on this thread, or null where there's no WebGPU, no adapter for `options`, or the adapter refused the
|
|
43
|
+
* device. Call it on the thread the world runs on: a device can't move between threads.
|
|
44
|
+
*/
|
|
45
|
+
const request = async (options = {}) => {
|
|
46
|
+
const { requiredFeatures, requiredLimits, ...adapterOptions } = options;
|
|
47
|
+
const adapter = await globalThis.navigator?.gpu?.requestAdapter(adapterOptions);
|
|
48
|
+
if (adapter === null || adapter === void 0) return null;
|
|
49
|
+
try {
|
|
50
|
+
return await adapter.requestDevice({
|
|
51
|
+
requiredFeatures,
|
|
52
|
+
requiredLimits
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/** Sets the world's `gpuDevice`. */
|
|
59
|
+
const set = System.create({}, ({ resources }, input) => {
|
|
60
|
+
resources[gpuDevice] = input.device;
|
|
61
|
+
}, {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": { "device": { "anyOf": [{ "allOf": [] }, { "type": "null" }] } }
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Keeps the world supplied with a device: requests one now, and another each time the one it gave is lost. Resolves
|
|
67
|
+
* once the first request settled, to whether it gave the world a device, and a function that stops keeping it. Call it
|
|
68
|
+
* on the world's thread.
|
|
69
|
+
*/
|
|
70
|
+
const keep = async (world, options = {}) => {
|
|
71
|
+
let stopped = false;
|
|
72
|
+
const supply = async () => {
|
|
73
|
+
const device = await request(options);
|
|
74
|
+
if (device === null || stopped) return false;
|
|
75
|
+
await world.run(set, { device });
|
|
76
|
+
device.lost.then(() => {
|
|
77
|
+
if (!stopped) supply();
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
supplied: await supply(),
|
|
83
|
+
stop: () => {
|
|
84
|
+
stopped = true;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/device/public.ts
|
|
90
|
+
var public_exports$1 = /* @__PURE__ */ __exportAll({
|
|
91
|
+
keep: () => keep,
|
|
92
|
+
request: () => request,
|
|
93
|
+
set: () => set
|
|
94
|
+
});
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/target/target.ts
|
|
97
|
+
const Options$1 = { default: {
|
|
98
|
+
format: null,
|
|
99
|
+
sampleCount: 1,
|
|
100
|
+
depthFormat: "depth24plus",
|
|
101
|
+
clear: [
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
104
|
+
0,
|
|
105
|
+
1
|
|
106
|
+
]
|
|
107
|
+
} };
|
|
108
|
+
/** An entity with these is a render target: what its textures are, and their size in pixels. */
|
|
109
|
+
const options$1 = Symbol("Target.options");
|
|
110
|
+
declare$$2(options$1, "component", { default: Options$1.default });
|
|
111
|
+
const size = Symbol("Target.size");
|
|
112
|
+
declare$$2(size, "component", { type: {
|
|
113
|
+
"type": "array",
|
|
114
|
+
"items": {
|
|
115
|
+
"type": "integer",
|
|
116
|
+
"minimum": 0,
|
|
117
|
+
"maximum": 4294967295
|
|
118
|
+
},
|
|
119
|
+
"minItems": 2,
|
|
120
|
+
"maxItems": 2
|
|
121
|
+
} });
|
|
122
|
+
/** Each render target's textures on the world's device, by its entity: null until targets are first updated. */
|
|
123
|
+
const states$2 = Symbol("Target.states");
|
|
124
|
+
declare$$2(states$2, "resource", {});
|
|
125
|
+
const sameOptions$1 = (a, b) => a.format === b.format && a.sampleCount === b.sampleCount && a.depthFormat === b.depthFormat && (a.clear === b.clear || a.clear !== null && b.clear !== null && a.clear.every((value, index) => value === b.clear?.[index]));
|
|
126
|
+
const destroyTextures = (state) => {
|
|
127
|
+
state.color.destroy();
|
|
128
|
+
state.multisampled?.destroy();
|
|
129
|
+
state.depth?.destroy();
|
|
130
|
+
};
|
|
131
|
+
const created = (device, entity, declared, [width, height]) => {
|
|
132
|
+
const format = declared.format ?? navigator.gpu.getPreferredCanvasFormat();
|
|
133
|
+
const extent = [width, height];
|
|
134
|
+
const color = device.createTexture({
|
|
135
|
+
label: `target ${entity} color`,
|
|
136
|
+
size: extent,
|
|
137
|
+
format,
|
|
138
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST
|
|
139
|
+
});
|
|
140
|
+
const multisampled = declared.sampleCount === 1 ? null : device.createTexture({
|
|
141
|
+
label: `target ${entity} multisampled`,
|
|
142
|
+
size: extent,
|
|
143
|
+
format,
|
|
144
|
+
sampleCount: declared.sampleCount,
|
|
145
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
|
146
|
+
});
|
|
147
|
+
const depth = declared.depthFormat === null ? null : device.createTexture({
|
|
148
|
+
label: `target ${entity} depth`,
|
|
149
|
+
size: extent,
|
|
150
|
+
format: declared.depthFormat,
|
|
151
|
+
sampleCount: declared.sampleCount,
|
|
152
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
entity,
|
|
156
|
+
device,
|
|
157
|
+
options: declared,
|
|
158
|
+
format,
|
|
159
|
+
size: [width, height],
|
|
160
|
+
color,
|
|
161
|
+
multisampled,
|
|
162
|
+
depth
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Each render target's textures on `device`, made again where its options, its size or the device changed, destroying
|
|
167
|
+
* those they replace; with no device, none. Gives `previous` itself when nothing changed.
|
|
168
|
+
*/
|
|
169
|
+
const updated$2 = (device, previous, targets) => {
|
|
170
|
+
const held = previous ?? /* @__PURE__ */ new Map();
|
|
171
|
+
let next = null;
|
|
172
|
+
for (let i = 0; i < targets.length; i++) {
|
|
173
|
+
const target = targets.at(i);
|
|
174
|
+
const old = held.get(target.entity);
|
|
175
|
+
const [width, height] = target[size];
|
|
176
|
+
if (device !== null && old !== void 0 && old.device === device && old.size[0] === width && old.size[1] === height && sameOptions$1(old.options, target[options$1])) continue;
|
|
177
|
+
if (device === null && old === void 0) continue;
|
|
178
|
+
next ??= new Map(held);
|
|
179
|
+
if (old !== void 0) destroyTextures(old);
|
|
180
|
+
if (device === null) next.delete(target.entity);
|
|
181
|
+
else next.set(target.entity, created(device, target.entity, target[options$1], target[size]));
|
|
182
|
+
}
|
|
183
|
+
return next ?? held;
|
|
184
|
+
};
|
|
185
|
+
/** Records clearing each target that declares a clear color: its color to that color, and its depth to 1. */
|
|
186
|
+
const recordClears = (encoder, targets) => {
|
|
187
|
+
for (const target of targets.values()) {
|
|
188
|
+
const color = target.options.clear;
|
|
189
|
+
if (color === null) continue;
|
|
190
|
+
const [r, g, b, a] = color;
|
|
191
|
+
encoder.beginRenderPass({
|
|
192
|
+
label: `clear target ${target.entity}`,
|
|
193
|
+
colorAttachments: [{
|
|
194
|
+
view: (target.multisampled ?? target.color).createView(),
|
|
195
|
+
resolveTarget: target.multisampled === null ? void 0 : target.color.createView(),
|
|
196
|
+
clearValue: {
|
|
197
|
+
r,
|
|
198
|
+
g,
|
|
199
|
+
b,
|
|
200
|
+
a
|
|
201
|
+
},
|
|
202
|
+
loadOp: "clear",
|
|
203
|
+
storeOp: "store"
|
|
204
|
+
}],
|
|
205
|
+
depthStencilAttachment: target.depth === null ? void 0 : {
|
|
206
|
+
view: target.depth.createView(),
|
|
207
|
+
depthClearValue: 1,
|
|
208
|
+
depthLoadOp: "clear",
|
|
209
|
+
depthStoreOp: "store"
|
|
210
|
+
}
|
|
211
|
+
}).end();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
/** Destroys a render target's textures when it stops being one. */
|
|
215
|
+
const release$2 = Finalizer.create({ components: { with: [options$1, size] } }, (target, { resources }) => {
|
|
216
|
+
const current = resources[states$2];
|
|
217
|
+
const state = current?.get(target.entity);
|
|
218
|
+
if (current === null || state === void 0) return;
|
|
219
|
+
destroyTextures(state);
|
|
220
|
+
const next = new Map(current);
|
|
221
|
+
next.delete(target.entity);
|
|
222
|
+
resources[states$2] = next;
|
|
223
|
+
});
|
|
224
|
+
/** Sets a render target's size in pixels, at least one by one. */
|
|
225
|
+
const resize = System.create({
|
|
226
|
+
components: { with: [size] },
|
|
227
|
+
target: "target"
|
|
228
|
+
}, (row, _context, input) => {
|
|
229
|
+
row[size] = [Math.max(1, Math.floor(input.width)), Math.max(1, Math.floor(input.height))];
|
|
230
|
+
}, {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"properties": {
|
|
233
|
+
"target": {
|
|
234
|
+
"type": "integer",
|
|
235
|
+
"format": "entity"
|
|
236
|
+
},
|
|
237
|
+
"width": { "type": "number" },
|
|
238
|
+
"height": { "type": "number" }
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
function declare$$2(key, kind, declaration) {
|
|
242
|
+
(globalThis[Symbol.for("@adobe/ecs/declarations@1")] ??= /* @__PURE__ */ new Map()).set(key, {
|
|
243
|
+
kind,
|
|
244
|
+
declaration
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/target/public.ts
|
|
249
|
+
var public_exports$4 = /* @__PURE__ */ __exportAll({
|
|
250
|
+
Options: () => Options$1,
|
|
251
|
+
options: () => options$1,
|
|
252
|
+
release: () => release$2,
|
|
253
|
+
resize: () => resize,
|
|
254
|
+
size: () => size,
|
|
255
|
+
states: () => states$2
|
|
256
|
+
});
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/surface/surface.ts
|
|
259
|
+
const Options = { default: {
|
|
260
|
+
alphaMode: "opaque",
|
|
261
|
+
colorSpace: "srgb",
|
|
262
|
+
toneMapping: "standard"
|
|
263
|
+
} };
|
|
264
|
+
/**
|
|
265
|
+
* A render target with these presents to a canvas: the canvas, on the world's thread, and how it presents. The
|
|
266
|
+
* canvas's size follows the target's.
|
|
267
|
+
*/
|
|
268
|
+
const canvas = Symbol("Surface.canvas");
|
|
269
|
+
declare$$1(canvas, "component", { transfer: true });
|
|
270
|
+
const options = Symbol("Surface.options");
|
|
271
|
+
declare$$1(options, "component", { default: Options.default });
|
|
272
|
+
/** Each surface's configured canvas, by its entity: null until surfaces are first updated. */
|
|
273
|
+
const states$1 = Symbol("Surface.states");
|
|
274
|
+
declare$$1(states$1, "resource", {});
|
|
275
|
+
/**
|
|
276
|
+
* A surface presenting to `canvas`, moved to the world's thread, of `width` by `height` pixels: a canvas's
|
|
277
|
+
* `transferControlToOffscreen()`, or an `OffscreenCanvas` of its own. Resolves to its entity.
|
|
278
|
+
*/
|
|
279
|
+
const attach = System.create({}, ({ commands }, input) => commands.spawn([
|
|
280
|
+
options$1,
|
|
281
|
+
size,
|
|
282
|
+
canvas,
|
|
283
|
+
options
|
|
284
|
+
])({
|
|
285
|
+
[options$1]: input.target ?? Options$1.default,
|
|
286
|
+
[size]: [Math.max(1, Math.floor(input.width)), Math.max(1, Math.floor(input.height))],
|
|
287
|
+
[canvas]: input.canvas,
|
|
288
|
+
[options]: input.surface ?? Options.default
|
|
289
|
+
}), {
|
|
290
|
+
"type": "object",
|
|
291
|
+
"properties": {
|
|
292
|
+
"canvas": { "allOf": [] },
|
|
293
|
+
"width": { "type": "number" },
|
|
294
|
+
"height": { "type": "number" },
|
|
295
|
+
"target": {
|
|
296
|
+
"type": "object",
|
|
297
|
+
"properties": {
|
|
298
|
+
"format": { "anyOf": [{ "allOf": [] }, { "type": "null" }] },
|
|
299
|
+
"sampleCount": { "anyOf": [{ "const": 1 }, { "const": 4 }] },
|
|
300
|
+
"depthFormat": { "anyOf": [{ "allOf": [] }, { "type": "null" }] },
|
|
301
|
+
"clear": { "anyOf": [{
|
|
302
|
+
"type": "array",
|
|
303
|
+
"items": {
|
|
304
|
+
"type": "number",
|
|
305
|
+
"precision": 1
|
|
306
|
+
},
|
|
307
|
+
"minItems": 4,
|
|
308
|
+
"maxItems": 4
|
|
309
|
+
}, { "type": "null" }] }
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
"surface": {
|
|
313
|
+
"type": "object",
|
|
314
|
+
"properties": {
|
|
315
|
+
"alphaMode": { "allOf": [] },
|
|
316
|
+
"colorSpace": { "allOf": [] },
|
|
317
|
+
"toneMapping": { "allOf": [] }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
"required": [
|
|
322
|
+
"canvas",
|
|
323
|
+
"width",
|
|
324
|
+
"height"
|
|
325
|
+
]
|
|
326
|
+
});
|
|
327
|
+
const sameOptions = (a, b) => a.alphaMode === b.alphaMode && a.colorSpace === b.colorSpace && a.toneMapping === b.toneMapping;
|
|
328
|
+
/**
|
|
329
|
+
* Each surface's canvas sized to its target and configured for `device` in its target's format, again where either
|
|
330
|
+
* changed; with no device, or no target state, unconfigured. Gives `previous` itself when nothing changed.
|
|
331
|
+
*/
|
|
332
|
+
const updated$1 = (device, targets, previous, surfaces) => {
|
|
333
|
+
const held = previous ?? /* @__PURE__ */ new Map();
|
|
334
|
+
let next = null;
|
|
335
|
+
for (let i = 0; i < surfaces.length; i++) {
|
|
336
|
+
const surface = surfaces.at(i);
|
|
337
|
+
const presented = surface[canvas];
|
|
338
|
+
const [width, height] = surface[size];
|
|
339
|
+
if (presented.width !== width) presented.width = width;
|
|
340
|
+
if (presented.height !== height) presented.height = height;
|
|
341
|
+
const target = targets.get(surface.entity);
|
|
342
|
+
const old = held.get(surface.entity);
|
|
343
|
+
if (device === null || target === void 0) {
|
|
344
|
+
if (old === void 0) continue;
|
|
345
|
+
old.context.unconfigure();
|
|
346
|
+
next ??= new Map(held);
|
|
347
|
+
next.delete(surface.entity);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const declared = surface[options];
|
|
351
|
+
if (old !== void 0 && old.device === device && old.format === target.format && old.canvas === presented && sameOptions(old.options, declared)) continue;
|
|
352
|
+
const context = presented.getContext("webgpu");
|
|
353
|
+
if (context === null) continue;
|
|
354
|
+
context.configure({
|
|
355
|
+
device,
|
|
356
|
+
format: target.format,
|
|
357
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
|
|
358
|
+
alphaMode: declared.alphaMode,
|
|
359
|
+
colorSpace: declared.colorSpace,
|
|
360
|
+
toneMapping: { mode: declared.toneMapping }
|
|
361
|
+
});
|
|
362
|
+
next ??= new Map(held);
|
|
363
|
+
next.set(surface.entity, {
|
|
364
|
+
entity: surface.entity,
|
|
365
|
+
canvas: presented,
|
|
366
|
+
context,
|
|
367
|
+
device,
|
|
368
|
+
format: target.format,
|
|
369
|
+
options: declared
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return next ?? held;
|
|
373
|
+
};
|
|
374
|
+
/** Unconfigures a surface's canvas when it stops being one. */
|
|
375
|
+
const release$1 = Finalizer.create({ components: { with: [canvas] } }, (surface, { resources }) => {
|
|
376
|
+
const current = resources[states$1];
|
|
377
|
+
const state = current?.get(surface.entity);
|
|
378
|
+
if (current === null || state === void 0) return;
|
|
379
|
+
state.context.unconfigure();
|
|
380
|
+
const next = new Map(current);
|
|
381
|
+
next.delete(surface.entity);
|
|
382
|
+
resources[states$1] = next;
|
|
383
|
+
});
|
|
384
|
+
/** A canvas's size in device pixels, from what a `ResizeObserver` saw of it. */
|
|
385
|
+
const pixelsOf = (entry) => {
|
|
386
|
+
const [device] = entry.devicePixelContentBoxSize ?? [];
|
|
387
|
+
if (device !== void 0) return [device.inlineSize, device.blockSize];
|
|
388
|
+
const [box] = entry.contentBoxSize;
|
|
389
|
+
const scale = globalThis.devicePixelRatio ?? 1;
|
|
390
|
+
return box === void 0 ? [entry.contentRect.width * scale, entry.contentRect.height * scale] : [box.inlineSize * scale, box.blockSize * scale];
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Keeps a surface's size the size of `element` on the page, in device pixels, on the thread the page is on: the world
|
|
394
|
+
* may be here or on a worker that joined it. Returns a function that stops.
|
|
395
|
+
*/
|
|
396
|
+
const observe = (element, world, surface) => {
|
|
397
|
+
const observer = new ResizeObserver((entries) => {
|
|
398
|
+
const entry = entries.at(-1);
|
|
399
|
+
if (entry === void 0) return;
|
|
400
|
+
const [width, height] = pixelsOf(entry);
|
|
401
|
+
world.run(resize, {
|
|
402
|
+
target: surface,
|
|
403
|
+
width,
|
|
404
|
+
height
|
|
405
|
+
}).catch(reportError);
|
|
406
|
+
});
|
|
407
|
+
try {
|
|
408
|
+
observer.observe(element, { box: "device-pixel-content-box" });
|
|
409
|
+
} catch {
|
|
410
|
+
observer.observe(element);
|
|
411
|
+
}
|
|
412
|
+
return () => observer.disconnect();
|
|
413
|
+
};
|
|
414
|
+
function declare$$1(key, kind, declaration) {
|
|
415
|
+
(globalThis[Symbol.for("@adobe/ecs/declarations@1")] ??= /* @__PURE__ */ new Map()).set(key, {
|
|
416
|
+
kind,
|
|
417
|
+
declaration
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/view/view.ts
|
|
422
|
+
/**
|
|
423
|
+
* An entity with these is a view: the render target it draws into, which owns it; its rectangle in that target as
|
|
424
|
+
* fractions of its size (`x`, `y`, `width`, `height`); and the matrix taking its scene to clip space.
|
|
425
|
+
*/
|
|
426
|
+
const target = Symbol("View.target");
|
|
427
|
+
declare$(target, "component", {
|
|
428
|
+
type: {
|
|
429
|
+
"type": "integer",
|
|
430
|
+
"format": "entity"
|
|
431
|
+
},
|
|
432
|
+
reference: "owner"
|
|
433
|
+
});
|
|
434
|
+
const viewport = Symbol("View.viewport");
|
|
435
|
+
declare$(viewport, "component", {
|
|
436
|
+
type: {
|
|
437
|
+
"type": "array",
|
|
438
|
+
"items": {
|
|
439
|
+
"type": "number",
|
|
440
|
+
"precision": 1
|
|
441
|
+
},
|
|
442
|
+
"minItems": 4,
|
|
443
|
+
"maxItems": 4
|
|
444
|
+
},
|
|
445
|
+
default: [
|
|
446
|
+
0,
|
|
447
|
+
0,
|
|
448
|
+
1,
|
|
449
|
+
1
|
|
450
|
+
]
|
|
451
|
+
});
|
|
452
|
+
const projection = Symbol("View.projection");
|
|
453
|
+
declare$(projection, "component", { type: {
|
|
454
|
+
"type": "array",
|
|
455
|
+
"items": {
|
|
456
|
+
"type": "number",
|
|
457
|
+
"precision": 1
|
|
458
|
+
},
|
|
459
|
+
"minItems": 16,
|
|
460
|
+
"maxItems": 16
|
|
461
|
+
} });
|
|
462
|
+
/** Each view's state on the world's device, by its entity: null until views are first updated. */
|
|
463
|
+
const states = Symbol("View.states");
|
|
464
|
+
declare$(states, "resource", {});
|
|
465
|
+
/** The uniform each view binds: its projection, then its size in pixels. */
|
|
466
|
+
const uniformBytes = 80;
|
|
467
|
+
const layouts = perDevice((device, _key) => device.createBindGroupLayout({
|
|
468
|
+
label: "view",
|
|
469
|
+
entries: [{
|
|
470
|
+
binding: 0,
|
|
471
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
|
|
472
|
+
buffer: { type: "uniform" }
|
|
473
|
+
}]
|
|
474
|
+
}), () => "");
|
|
475
|
+
/** The bind group layout of a view's uniform, one per device: a pipeline's layout takes it at the group `wgsl` names. */
|
|
476
|
+
const layout = (device) => layouts(device, null);
|
|
477
|
+
/** The WGSL declarations of a view's uniform at bind group `group`: `view.projection` and `view.size`, in pixels. */
|
|
478
|
+
const wgsl = (group) => `struct View { projection: mat4x4f, size: vec2f }\n@group(${group}) @binding(0) var<uniform> view: View;\n`;
|
|
479
|
+
const rectangleOf = ([x, y, width, height], [targetWidth, targetHeight]) => {
|
|
480
|
+
const left = Math.min(targetWidth - 1, Math.max(0, Math.round(x * targetWidth)));
|
|
481
|
+
const top = Math.min(targetHeight - 1, Math.max(0, Math.round(y * targetHeight)));
|
|
482
|
+
return [
|
|
483
|
+
left,
|
|
484
|
+
top,
|
|
485
|
+
Math.max(1, Math.min(targetWidth - left, Math.round(width * targetWidth))),
|
|
486
|
+
Math.max(1, Math.min(targetHeight - top, Math.round(height * targetHeight)))
|
|
487
|
+
];
|
|
488
|
+
};
|
|
489
|
+
const sameRectangle = (a, b) => a.every((value, index) => value === b[index]);
|
|
490
|
+
const sameMatrix = (a, b) => a.every((value, index) => value === b[index]);
|
|
491
|
+
/**
|
|
492
|
+
* Each view placed in its target, its uniform written where its projection or rectangle changed; a view whose target
|
|
493
|
+
* has no textures, as without a device, has no state. Gives `previous` itself when nothing changed.
|
|
494
|
+
*/
|
|
495
|
+
const updated = (device, targets, previous, views) => {
|
|
496
|
+
const held = previous ?? /* @__PURE__ */ new Map();
|
|
497
|
+
let next = null;
|
|
498
|
+
for (let i = 0; i < views.length; i++) {
|
|
499
|
+
const view = views.at(i);
|
|
500
|
+
const old = held.get(view.entity);
|
|
501
|
+
const drawn = device === null ? void 0 : targets.get(view[target]);
|
|
502
|
+
if (device === null || drawn === void 0) {
|
|
503
|
+
if (old === void 0) continue;
|
|
504
|
+
old.buffer.destroy();
|
|
505
|
+
next ??= new Map(held);
|
|
506
|
+
next.delete(view.entity);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const rectangle = rectangleOf(view[viewport], drawn.size);
|
|
510
|
+
const matrix = view[projection];
|
|
511
|
+
if (old !== void 0 && old.target === drawn && sameRectangle(old.rectangle, rectangle) && sameMatrix(old.projection, matrix)) continue;
|
|
512
|
+
const kept = old !== void 0 && old.target.device === device;
|
|
513
|
+
if (old !== void 0 && !kept) old.buffer.destroy();
|
|
514
|
+
const buffer = kept ? old.buffer : device.createBuffer({
|
|
515
|
+
label: `view ${view.entity}`,
|
|
516
|
+
size: uniformBytes,
|
|
517
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
518
|
+
});
|
|
519
|
+
const bindGroup = kept ? old.bindGroup : device.createBindGroup({
|
|
520
|
+
layout: layout(device),
|
|
521
|
+
entries: [{
|
|
522
|
+
binding: 0,
|
|
523
|
+
resource: { buffer }
|
|
524
|
+
}]
|
|
525
|
+
});
|
|
526
|
+
device.queue.writeBuffer(buffer, 0, new Float32Array([
|
|
527
|
+
...matrix,
|
|
528
|
+
rectangle[2],
|
|
529
|
+
rectangle[3],
|
|
530
|
+
0,
|
|
531
|
+
0
|
|
532
|
+
]));
|
|
533
|
+
next ??= new Map(held);
|
|
534
|
+
next.set(view.entity, {
|
|
535
|
+
entity: view.entity,
|
|
536
|
+
target: drawn,
|
|
537
|
+
rectangle,
|
|
538
|
+
projection: matrix,
|
|
539
|
+
buffer,
|
|
540
|
+
bindGroup
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return next ?? held;
|
|
544
|
+
};
|
|
545
|
+
/** Destroys a view's uniform buffer when it stops being one. */
|
|
546
|
+
const release = Finalizer.create({ components: { with: [
|
|
547
|
+
target,
|
|
548
|
+
viewport,
|
|
549
|
+
projection
|
|
550
|
+
] } }, (view, { resources }) => {
|
|
551
|
+
const current = resources[states];
|
|
552
|
+
const state = current?.get(view.entity);
|
|
553
|
+
if (current === null || state === void 0) return;
|
|
554
|
+
state.buffer.destroy();
|
|
555
|
+
const next = new Map(current);
|
|
556
|
+
next.delete(view.entity);
|
|
557
|
+
resources[states] = next;
|
|
558
|
+
});
|
|
559
|
+
/**
|
|
560
|
+
* Begins a render pass drawing into a view: its target's textures, loading what they hold, limited to its rectangle,
|
|
561
|
+
* multisampled passes resolving into the target's color.
|
|
562
|
+
*/
|
|
563
|
+
const begin = (encoder, view, label) => {
|
|
564
|
+
const { color, multisampled, depth } = view.target;
|
|
565
|
+
const pass = encoder.beginRenderPass({
|
|
566
|
+
label,
|
|
567
|
+
colorAttachments: [{
|
|
568
|
+
view: (multisampled ?? color).createView(),
|
|
569
|
+
resolveTarget: multisampled === null ? void 0 : color.createView(),
|
|
570
|
+
loadOp: "load",
|
|
571
|
+
storeOp: "store"
|
|
572
|
+
}],
|
|
573
|
+
depthStencilAttachment: depth === null ? void 0 : {
|
|
574
|
+
view: depth.createView(),
|
|
575
|
+
depthLoadOp: "load",
|
|
576
|
+
depthStoreOp: "store"
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
const [x, y, width, height] = view.rectangle;
|
|
580
|
+
pass.setViewport(x, y, width, height, 0, 1);
|
|
581
|
+
pass.setScissorRect(x, y, width, height);
|
|
582
|
+
return pass;
|
|
583
|
+
};
|
|
584
|
+
function declare$(key, kind, declaration) {
|
|
585
|
+
(globalThis[Symbol.for("@adobe/ecs/declarations@1")] ??= /* @__PURE__ */ new Map()).set(key, {
|
|
586
|
+
kind,
|
|
587
|
+
declaration
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/frame/frame.ts
|
|
592
|
+
const everyTarget = Index.create({
|
|
593
|
+
key: [],
|
|
594
|
+
components: { with: [options$1, size] }
|
|
595
|
+
});
|
|
596
|
+
const everySurface = Index.create({
|
|
597
|
+
key: [],
|
|
598
|
+
components: { with: [
|
|
599
|
+
canvas,
|
|
600
|
+
options,
|
|
601
|
+
size
|
|
602
|
+
] }
|
|
603
|
+
});
|
|
604
|
+
const everyView = Index.create({
|
|
605
|
+
key: [],
|
|
606
|
+
components: { with: [
|
|
607
|
+
target,
|
|
608
|
+
viewport,
|
|
609
|
+
projection
|
|
610
|
+
] }
|
|
611
|
+
});
|
|
612
|
+
/**
|
|
613
|
+
* Brings render targets, surfaces and views up to date with their entities and the world's device, then clears each
|
|
614
|
+
* target that declares a clear color: its color to that color and its depth to 1. Draws go after it and before
|
|
615
|
+
* `present`.
|
|
616
|
+
*/
|
|
617
|
+
const update = System.create({ lookup: {
|
|
618
|
+
targets: { index: everyTarget },
|
|
619
|
+
surfaces: { index: everySurface },
|
|
620
|
+
views: { index: everyView }
|
|
621
|
+
} }, ({ resources, lookup }) => {
|
|
622
|
+
const device = resources[gpuDevice];
|
|
623
|
+
const targets = updated$2(device, resources[states$2], lookup.targets());
|
|
624
|
+
const surfaces = updated$1(device, targets, resources[states$1], lookup.surfaces());
|
|
625
|
+
const views = updated(device, targets, resources[states], lookup.views());
|
|
626
|
+
if (targets !== resources[states$2]) resources[states$2] = targets;
|
|
627
|
+
if (surfaces !== resources[states$1]) resources[states$1] = surfaces;
|
|
628
|
+
if (views !== resources[states]) resources[states] = views;
|
|
629
|
+
if (device === null || targets.size === 0) return;
|
|
630
|
+
const encoder = device.createCommandEncoder({ label: "clear targets" });
|
|
631
|
+
recordClears(encoder, targets);
|
|
632
|
+
device.queue.submit([encoder.finish()]);
|
|
633
|
+
});
|
|
634
|
+
/**
|
|
635
|
+
* Copies each surface's target into its canvas, in one body, so every draw of the run is in the frame the browser
|
|
636
|
+
* presents, even where the run waited between them.
|
|
637
|
+
*/
|
|
638
|
+
const present = System.render({
|
|
639
|
+
components: { with: [canvas] },
|
|
640
|
+
shader: { accessors: {} },
|
|
641
|
+
after: [update]
|
|
642
|
+
}, ({ encoder }, { resources }) => {
|
|
643
|
+
const targets = resources[states$2];
|
|
644
|
+
for (const surface of resources[states$1]?.values() ?? []) {
|
|
645
|
+
const target = targets?.get(surface.entity);
|
|
646
|
+
if (target === void 0 || target.device !== surface.device) continue;
|
|
647
|
+
const current = surface.context.getCurrentTexture();
|
|
648
|
+
const [width, height] = target.size;
|
|
649
|
+
if (current.width !== width || current.height !== height) continue;
|
|
650
|
+
encoder.copyTextureToTexture({ texture: target.color }, { texture: current }, [width, height]);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
/** Every system of a frame, in order: targets, surfaces and views updated and targets cleared, then surfaces presented. */
|
|
654
|
+
const systems = System.create({ systems: [update, present] });
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/frame/public.ts
|
|
657
|
+
var public_exports$2 = /* @__PURE__ */ __exportAll({
|
|
658
|
+
present: () => present,
|
|
659
|
+
systems: () => systems,
|
|
660
|
+
update: () => update
|
|
661
|
+
});
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/surface/public.ts
|
|
664
|
+
var public_exports$3 = /* @__PURE__ */ __exportAll({
|
|
665
|
+
Options: () => Options,
|
|
666
|
+
attach: () => attach,
|
|
667
|
+
canvas: () => canvas,
|
|
668
|
+
observe: () => observe,
|
|
669
|
+
options: () => options,
|
|
670
|
+
release: () => release$1,
|
|
671
|
+
states: () => states$1
|
|
672
|
+
});
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/view/public.ts
|
|
675
|
+
var public_exports$5 = /* @__PURE__ */ __exportAll({
|
|
676
|
+
begin: () => begin,
|
|
677
|
+
layout: () => layout,
|
|
678
|
+
projection: () => projection,
|
|
679
|
+
release: () => release,
|
|
680
|
+
states: () => states,
|
|
681
|
+
target: () => target,
|
|
682
|
+
viewport: () => viewport,
|
|
683
|
+
wgsl: () => wgsl
|
|
684
|
+
});
|
|
685
|
+
//#endregion
|
|
686
|
+
export { public_exports as Cache, public_exports$1 as Device, public_exports$2 as Frame, public_exports$3 as Surface, public_exports$4 as Target, public_exports$5 as View };
|
|
687
|
+
|
|
688
|
+
//# sourceMappingURL=index.js.map
|