@aethervtt/shard-gpu-webgl2 0.0.0-stage → 0.2.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/binding.d.ts +32 -0
  4. package/dist/binding.js +81 -0
  5. package/dist/cache.d.ts +73 -0
  6. package/dist/cache.js +184 -0
  7. package/dist/commands.d.ts +101 -0
  8. package/dist/commands.js +391 -0
  9. package/dist/constants.d.ts +41 -0
  10. package/dist/constants.js +38 -0
  11. package/dist/copies.d.ts +61 -0
  12. package/dist/copies.js +629 -0
  13. package/dist/device.d.ts +168 -0
  14. package/dist/device.js +401 -0
  15. package/dist/errors.d.ts +8 -0
  16. package/dist/errors.js +18 -0
  17. package/dist/fake-gl.d.ts +338 -0
  18. package/dist/fake-gl.js +980 -0
  19. package/dist/fbo.d.ts +30 -0
  20. package/dist/fbo.js +142 -0
  21. package/dist/formats.d.ts +25 -0
  22. package/dist/formats.js +106 -0
  23. package/dist/gl.d.ts +216 -0
  24. package/dist/gl.js +232 -0
  25. package/dist/gpu.d.ts +58 -0
  26. package/dist/gpu.js +215 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +42 -0
  29. package/dist/naga.d.ts +37 -0
  30. package/dist/naga.js +99 -0
  31. package/dist/pipeline.d.ts +113 -0
  32. package/dist/pipeline.js +294 -0
  33. package/dist/present.d.ts +18 -0
  34. package/dist/present.js +108 -0
  35. package/dist/queries.d.ts +41 -0
  36. package/dist/queries.js +110 -0
  37. package/dist/queue.d.ts +13 -0
  38. package/dist/queue.js +50 -0
  39. package/dist/readback.d.ts +17 -0
  40. package/dist/readback.js +104 -0
  41. package/dist/replay.d.ts +78 -0
  42. package/dist/replay.js +678 -0
  43. package/dist/resources.d.ts +96 -0
  44. package/dist/resources.js +355 -0
  45. package/dist/state.d.ts +43 -0
  46. package/dist/state.js +169 -0
  47. package/package.json +36 -4
  48. package/wasm/shard_naga.wasm +0 -0
@@ -0,0 +1,168 @@
1
+ import { Webgl2BindGroup, Webgl2BindGroupLayout, Webgl2PipelineLayout } from './binding.js';
2
+ import type { BakedTranslations, ShaderCacheStats, Translator } from './cache.js';
3
+ import { type Webgl2CommandBuffer, Webgl2CommandEncoder } from './commands.js';
4
+ import { Copier } from './copies.js';
5
+ import { ShimError } from './errors.js';
6
+ import { FramebufferCache } from './fbo.js';
7
+ import { Webgl2RenderPipeline, Webgl2ShaderModule } from './pipeline.js';
8
+ import { QueryPool, Webgl2QuerySet } from './queries.js';
9
+ import { Webgl2Queue } from './queue.js';
10
+ import { Replayer } from './replay.js';
11
+ import { Webgl2Buffer, Webgl2Sampler, Webgl2Texture } from './resources.js';
12
+ import { GlState } from './state.js';
13
+ /** What the context can do, read once from GL (or WebGL2's floor, for the 'minimum' profile). */
14
+ export interface Webgl2Caps {
15
+ /** The most samples any renderbuffer takes. */
16
+ maxSamples: number;
17
+ /** EXT_texture_filter_anisotropic's maximum; 1 without it. */
18
+ anisotropy: number;
19
+ /** KHR_parallel_shader_compile: links finish in the background. */
20
+ parallelCompile: boolean;
21
+ /** Texture units a fragment shader samples (MAX_TEXTURE_IMAGE_UNITS). */
22
+ textureUnits: number;
23
+ vertexTextureUnits: number;
24
+ combinedUnits: number;
25
+ /** EXT_clip_control: depth in [0, 1] as WebGPU has it. */
26
+ clipControl: boolean;
27
+ /** EXT_float_blend: blending into 32-bit float targets. */
28
+ floatBlend: boolean;
29
+ /** OES_texture_float_linear: filtering 32-bit float textures. */
30
+ floatLinear: boolean;
31
+ maxTextureSize: number;
32
+ max3dTextureSize: number;
33
+ maxArrayLayers: number;
34
+ maxCubeSize: number;
35
+ maxDrawBuffers: number;
36
+ maxAttributes: number;
37
+ /** Varying vectors between the stages. */
38
+ maxVaryings: number;
39
+ uniformBlockSize: number;
40
+ /** Uniform blocks a stage reads (the smaller of vertex and fragment). */
41
+ uniformBlocks: number;
42
+ uniformBindings: number;
43
+ uniformAlignment: number;
44
+ /** Compressed families whose every format the context has. */
45
+ compression: {
46
+ bc: boolean;
47
+ astc: boolean;
48
+ etc2: boolean;
49
+ };
50
+ }
51
+ export interface Webgl2AdapterInfo {
52
+ vendor: string;
53
+ architecture: string;
54
+ device: string;
55
+ description: string;
56
+ }
57
+ export interface Webgl2DeviceOptions {
58
+ label?: string;
59
+ /** The context's canvas: where loss is heard, and the surface it presents straight into. */
60
+ canvas?: HTMLCanvasElement | OffscreenCanvas;
61
+ /** The shim made the context: `destroy()` loses it (browsers cap live contexts). */
62
+ ownsContext: boolean;
63
+ /** Checks GL errors after every submit: a round trip to the GPU process, so dev only. */
64
+ checkErrors: boolean;
65
+ features: ReadonlySet<string>;
66
+ limits: Readonly<Record<string, number>>;
67
+ info: Webgl2AdapterInfo;
68
+ /** WGSL to GLSL: memory, a baked set, IndexedDB, then naga. */
69
+ translator: Translator;
70
+ extensions: Webgl2Extensions;
71
+ }
72
+ /** Extensions the device calls into, enabled by the adapter. */
73
+ export interface Webgl2Extensions {
74
+ clipControl: unknown;
75
+ /** EXT_disjoint_timer_query_webgl2: TIME_ELAPSED queries time passes (0074). */
76
+ timer: unknown;
77
+ indexed: OES_draw_buffers_indexed | null;
78
+ lose: WEBGL_lose_context | null;
79
+ }
80
+ export interface Webgl2DeviceLostInfo {
81
+ reason: 'destroyed' | 'unknown';
82
+ message: string;
83
+ }
84
+ export declare class Webgl2Device extends EventTarget {
85
+ label: string;
86
+ readonly gl: WebGL2RenderingContext;
87
+ readonly caps: Webgl2Caps;
88
+ readonly state: GlState;
89
+ readonly fbos: FramebufferCache;
90
+ readonly copier: Copier;
91
+ /** Query objects for timed passes (`timestamp-query`, 0074). */
92
+ readonly queries: QueryPool;
93
+ /** Where the next timed pass's stamps start: one timeline across frames, as GPU clocks are. */
94
+ timerAt: bigint;
95
+ readonly replayer: Replayer;
96
+ readonly queue: Webgl2Queue;
97
+ readonly features: ReadonlySet<string>;
98
+ readonly limits: Readonly<Record<string, number>>;
99
+ readonly adapterInfo: Webgl2AdapterInfo;
100
+ readonly canvas: HTMLCanvasElement | OffscreenCanvas | undefined;
101
+ readonly lost: Promise<Webgl2DeviceLostInfo>;
102
+ onuncapturederror: ((event: Event) => void) | null;
103
+ /** True once lost or destroyed: calls do nothing. */
104
+ isLost: boolean;
105
+ private resolveLost;
106
+ private readonly options;
107
+ private readonly scopes;
108
+ private readonly streams;
109
+ private readonly encoders;
110
+ private readonly fences;
111
+ private polling;
112
+ private readonly touched;
113
+ private readonly samples;
114
+ private readonly onContextLost;
115
+ constructor(gl: WebGL2RenderingContext, caps: Webgl2Caps, options: Webgl2DeviceOptions);
116
+ createBuffer(descriptor: GPUBufferDescriptor): Webgl2Buffer;
117
+ createTexture(descriptor: GPUTextureDescriptor): Webgl2Texture;
118
+ createSampler(descriptor?: GPUSamplerDescriptor): Webgl2Sampler;
119
+ createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): Webgl2BindGroupLayout;
120
+ createPipelineLayout(descriptor: GPUPipelineLayoutDescriptor): Webgl2PipelineLayout;
121
+ createBindGroup(descriptor: GPUBindGroupDescriptor): Webgl2BindGroup;
122
+ createShaderModule(descriptor: GPUShaderModuleDescriptor): Webgl2ShaderModule;
123
+ /**
124
+ * Translates and links now, from translations already in memory (or the baked set). Otherwise
125
+ * the pipeline is invalid and raises a validation error: the engine makes its pipelines with
126
+ * createRenderPipelineAsync, which can load what it needs.
127
+ */
128
+ createRenderPipeline(descriptor: GPURenderPipelineDescriptor): Webgl2RenderPipeline;
129
+ createRenderPipelineAsync(descriptor: GPURenderPipelineDescriptor): Promise<Webgl2RenderPipeline>;
130
+ createComputePipeline(descriptor: GPUComputePipelineDescriptor): never;
131
+ createComputePipelineAsync(descriptor: GPUComputePipelineDescriptor): Promise<never>;
132
+ /** Timestamp query sets where the timer extension exists (see queries.ts). */
133
+ createQuerySet(descriptor: GPUQuerySetDescriptor): Webgl2QuerySet;
134
+ createRenderBundleEncoder(): never;
135
+ importExternalTexture(): never;
136
+ createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): Webgl2CommandEncoder;
137
+ pushErrorScope(filter: GPUErrorFilter): void;
138
+ popErrorScope(): Promise<ShimError | null>;
139
+ /** Reports an error as WebGPU would: to the innermost scope that filters it, else uncaptured. */
140
+ raise(kind: ShimError['kind'], message: string): void;
141
+ destroy(): void;
142
+ private lose;
143
+ /** @internal Replays command buffers, then shows the canvases they drew. */
144
+ submit(buffers: Iterable<Webgl2CommandBuffer>): void;
145
+ private replay;
146
+ /** @internal Notes a canvas texture drawn this submit: it's shown when the submit ends. */
147
+ touch(texture: Webgl2Texture): void;
148
+ /** Resolves once the GPU has run everything submitted so far. */
149
+ afterGpu(): Promise<void>;
150
+ private schedulePoll;
151
+ private readonly poll;
152
+ /** @internal `buffer.mapAsync`: reads its bytes once the GPU is done writing them. */
153
+ mapBuffer(buffer: Webgl2Buffer, mode: number, offset: number, size: number): Promise<undefined>;
154
+ /** Sets a texture's base and max level, the mip range a view reads (the texture is bound and active). */
155
+ levels(texture: Webgl2Texture, base: number, max: number): void;
156
+ /** The most samples a renderbuffer of `internal` format takes. */
157
+ samplesFor(internal: number): number;
158
+ /** @internal Forgets a destroyed texture's framebuffers, bindings and copies. */
159
+ forgetTexture(texture: Webgl2Texture): void;
160
+ /** @internal Forgets a destroyed buffer's bindings and pending reads. */
161
+ forgetBuffer(buffer: Webgl2Buffer): void;
162
+ /** Raises GL errors since the last check. */
163
+ checkErrors(where: string): void;
164
+ /** Where translations came from, and what naga translated this session (0064). */
165
+ get shaderCache(): ShaderCacheStats;
166
+ /** The translations this device used, as a baked set (see `Translator.export`). */
167
+ exportShaders(): BakedTranslations;
168
+ }
package/dist/device.js ADDED
@@ -0,0 +1,401 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { Webgl2BindGroup, Webgl2BindGroupLayout, Webgl2PipelineLayout } from "./binding.js";
3
+ import { CommandStream, Webgl2CommandEncoder } from "./commands.js";
4
+ import { BufferUsage, MapMode } from "./constants.js";
5
+ import { Copier } from "./copies.js";
6
+ import { ShimError, unsupported } from "./errors.js";
7
+ import { FramebufferCache } from "./fbo.js";
8
+ import { CLIP, GL } from "./gl.js";
9
+ import { Webgl2RenderPipeline, Webgl2ShaderModule } from "./pipeline.js";
10
+ import { finishQueryReads, QueryPool, Webgl2QuerySet } from "./queries.js";
11
+ import { Webgl2Queue } from "./queue.js";
12
+ import { Replayer } from "./replay.js";
13
+ import { Webgl2Buffer, Webgl2Sampler, Webgl2Texture } from "./resources.js";
14
+ import { GlState } from "./state.js";
15
+ const nextTask = () => new Promise((resolve) => setTimeout(resolve, 0));
16
+ class Webgl2Device extends EventTarget {
17
+ label;
18
+ gl;
19
+ caps;
20
+ state;
21
+ fbos;
22
+ copier;
23
+ /** Query objects for timed passes (`timestamp-query`, 0074). */
24
+ queries;
25
+ /** Where the next timed pass's stamps start: one timeline across frames, as GPU clocks are. */
26
+ timerAt = 1n;
27
+ replayer;
28
+ queue;
29
+ features;
30
+ limits;
31
+ adapterInfo;
32
+ canvas;
33
+ lost;
34
+ onuncapturederror = null;
35
+ /** True once lost or destroyed: calls do nothing. */
36
+ isLost = false;
37
+ resolveLost;
38
+ options;
39
+ scopes = [];
40
+ streams = [];
41
+ encoders = [];
42
+ fences = [];
43
+ polling = false;
44
+ touched = [];
45
+ samples = /* @__PURE__ */ new Map();
46
+ onContextLost = (event) => {
47
+ event.preventDefault();
48
+ this.lose("unknown", "The WebGL2 context was lost");
49
+ };
50
+ constructor(gl, caps, options) {
51
+ super();
52
+ this.gl = gl;
53
+ this.caps = caps;
54
+ this.options = options;
55
+ this.label = options.label ?? "";
56
+ this.canvas = options.canvas;
57
+ this.features = options.features;
58
+ this.limits = options.limits;
59
+ this.adapterInfo = options.info;
60
+ this.lost = new Promise((resolve) => {
61
+ this.resolveLost = resolve;
62
+ });
63
+ this.state = new GlState(gl, options.extensions.indexed);
64
+ this.state.scratchUnit = caps.combinedUnits - 1;
65
+ this.fbos = new FramebufferCache(gl);
66
+ if (options.checkErrors) {
67
+ this.fbos.incomplete = (status) => this.raise("validation", `A framebuffer is incomplete (0x${status.toString(16)})`);
68
+ }
69
+ this.copier = new Copier(this);
70
+ this.queries = new QueryPool(gl);
71
+ this.replayer = new Replayer(this);
72
+ this.queue = new Webgl2Queue(this);
73
+ gl.pixelStorei(GL.UNPACK_ALIGNMENT, 1);
74
+ gl.pixelStorei(GL.PACK_ALIGNMENT, 1);
75
+ gl.pixelStorei(GL.UNPACK_ROW_LENGTH, 0);
76
+ gl.pixelStorei(GL.UNPACK_IMAGE_HEIGHT, 0);
77
+ gl.pixelStorei(GL.PACK_ROW_LENGTH, 0);
78
+ gl.disable(GL.DITHER);
79
+ const clip = options.extensions.clipControl;
80
+ if (caps.clipControl && clip) clip.clipControlEXT(CLIP.LOWER_LEFT_EXT, CLIP.ZERO_TO_ONE_EXT);
81
+ this.canvas?.addEventListener("webglcontextlost", this.onContextLost);
82
+ }
83
+ // --- objects ----------------------------------------------------------------------
84
+ createBuffer(descriptor) {
85
+ return new Webgl2Buffer(this, descriptor);
86
+ }
87
+ createTexture(descriptor) {
88
+ return new Webgl2Texture(this, descriptor);
89
+ }
90
+ createSampler(descriptor) {
91
+ return new Webgl2Sampler(this, descriptor);
92
+ }
93
+ createBindGroupLayout(descriptor) {
94
+ return new Webgl2BindGroupLayout(descriptor);
95
+ }
96
+ createPipelineLayout(descriptor) {
97
+ return new Webgl2PipelineLayout(descriptor);
98
+ }
99
+ createBindGroup(descriptor) {
100
+ return new Webgl2BindGroup(descriptor);
101
+ }
102
+ createShaderModule(descriptor) {
103
+ return new Webgl2ShaderModule(descriptor);
104
+ }
105
+ /**
106
+ * Translates and links now, from translations already in memory (or the baked set). Otherwise
107
+ * the pipeline is invalid and raises a validation error: the engine makes its pipelines with
108
+ * createRenderPipelineAsync, which can load what it needs.
109
+ */
110
+ createRenderPipeline(descriptor) {
111
+ const pipeline = new Webgl2RenderPipeline(this, descriptor);
112
+ const t = this.options.translator;
113
+ const { vertex, fragment } = pipeline.stages();
114
+ const v = t.cached(vertex.code, vertex.entry, "vertex");
115
+ const f = fragment && t.cached(fragment.code, fragment.entry, "fragment");
116
+ if (!v || fragment && !f) {
117
+ this.raise(
118
+ "validation",
119
+ `"${pipeline.label}" has no translation yet: make it with createRenderPipelineAsync`
120
+ );
121
+ return pipeline;
122
+ }
123
+ try {
124
+ pipeline.build(v, f);
125
+ pipeline.finish();
126
+ } catch (err) {
127
+ this.raise("validation", err.message);
128
+ }
129
+ return pipeline;
130
+ }
131
+ async createRenderPipelineAsync(descriptor) {
132
+ const pipeline = new Webgl2RenderPipeline(this, descriptor);
133
+ const t = this.options.translator;
134
+ const { vertex, fragment } = pipeline.stages();
135
+ const [v, f] = await Promise.all([
136
+ t.translate(vertex.code, vertex.entry, "vertex"),
137
+ fragment ? t.translate(fragment.code, fragment.entry, "fragment") : void 0
138
+ ]);
139
+ if (this.isLost) throw new ShardError("gpu-webgl2/lost", "The device was lost");
140
+ pipeline.build(v, f);
141
+ while (!pipeline.linked()) await nextTask();
142
+ pipeline.finish();
143
+ return pipeline;
144
+ }
145
+ createComputePipeline(descriptor) {
146
+ throw unsupported(
147
+ `make the compute pipeline "${descriptor.label ?? ""}"`,
148
+ 'Compute is the full tier: a feature that needs it registers baseline "unsupported" (0064).'
149
+ );
150
+ }
151
+ createComputePipelineAsync(descriptor) {
152
+ return Promise.reject(
153
+ unsupported(
154
+ `make the compute pipeline "${descriptor.label ?? ""}"`,
155
+ 'Compute is the full tier: a feature that needs it registers baseline "unsupported" (0064).'
156
+ )
157
+ );
158
+ }
159
+ /** Timestamp query sets where the timer extension exists (see queries.ts). */
160
+ createQuerySet(descriptor) {
161
+ if (descriptor.type !== "timestamp" || !this.features.has("timestamp-query")) {
162
+ throw unsupported(`make a ${descriptor.type} query set`);
163
+ }
164
+ return new Webgl2QuerySet(this, descriptor);
165
+ }
166
+ createRenderBundleEncoder() {
167
+ throw unsupported("record render bundles");
168
+ }
169
+ importExternalTexture() {
170
+ throw unsupported("import external textures", "Copy the frame with copyExternalImageToTexture.");
171
+ }
172
+ createCommandEncoder(descriptor) {
173
+ const stream = this.streams.pop() ?? new CommandStream();
174
+ const encoder = this.encoders.pop() ?? new Webgl2CommandEncoder();
175
+ return encoder.begin(stream, descriptor?.label ?? "");
176
+ }
177
+ // --- errors and loss --------------------------------------------------------------
178
+ pushErrorScope(filter) {
179
+ this.scopes.push({ filter, error: null });
180
+ }
181
+ popErrorScope() {
182
+ const scope = this.scopes.pop();
183
+ if (!scope) return Promise.reject(new Error("popErrorScope: no error scope to pop"));
184
+ return Promise.resolve(scope.error);
185
+ }
186
+ /** Reports an error as WebGPU would: to the innermost scope that filters it, else uncaptured. */
187
+ raise(kind, message) {
188
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
189
+ const scope = this.scopes[i];
190
+ if (scope.filter !== kind) continue;
191
+ scope.error ??= new ShimError(kind, message);
192
+ return;
193
+ }
194
+ const event = Object.assign(new Event("uncapturederror"), {
195
+ error: new ShimError(kind, message)
196
+ });
197
+ queueMicrotask(() => {
198
+ this.dispatchEvent(event);
199
+ this.onuncapturederror?.(event);
200
+ });
201
+ }
202
+ destroy() {
203
+ if (this.isLost) return;
204
+ this.fbos.clear();
205
+ this.copier.clear();
206
+ this.lose("destroyed", "The device was destroyed");
207
+ if (this.options.ownsContext) this.options.extensions.lose?.loseContext();
208
+ }
209
+ lose(reason, message) {
210
+ if (this.isLost) return;
211
+ this.isLost = true;
212
+ this.queries.clear();
213
+ this.canvas?.removeEventListener("webglcontextlost", this.onContextLost);
214
+ for (const f of this.fences.splice(0)) f.resolve();
215
+ this.resolveLost({ reason, message });
216
+ }
217
+ // --- submission ---------------------------------------------------------------------
218
+ /** @internal Replays command buffers, then shows the canvases they drew. */
219
+ submit(buffers) {
220
+ if (this.isLost) return;
221
+ if (Array.isArray(buffers)) {
222
+ const list = buffers;
223
+ for (let i = 0; i < list.length; i++) this.replay(list[i]);
224
+ } else {
225
+ for (const b of buffers) this.replay(b);
226
+ }
227
+ const touched = this.touched;
228
+ for (let i = 0; i < touched.length; i++) {
229
+ const surface = touched[i].surface;
230
+ surface?.present(touched[i]);
231
+ }
232
+ touched.length = 0;
233
+ if (this.options.checkErrors) this.checkErrors("a submit");
234
+ }
235
+ replay(buffer) {
236
+ const stream = buffer.stream;
237
+ if (!stream) {
238
+ this.raise("validation", `Command buffer "${buffer.label}" was already submitted`);
239
+ return;
240
+ }
241
+ buffer.stream = void 0;
242
+ try {
243
+ this.replayer.run(stream);
244
+ } finally {
245
+ stream.reset();
246
+ this.streams.push(stream);
247
+ this.encoders.push(buffer.encoder);
248
+ }
249
+ }
250
+ /** @internal Notes a canvas texture drawn this submit: it's shown when the submit ends. */
251
+ touch(texture) {
252
+ if (texture.surface && !this.touched.includes(texture)) this.touched.push(texture);
253
+ }
254
+ /** Resolves once the GPU has run everything submitted so far. */
255
+ afterGpu() {
256
+ if (this.isLost) return Promise.resolve();
257
+ const gl = this.gl;
258
+ const sync = gl.fenceSync(GL.SYNC_GPU_COMMANDS_COMPLETE, 0);
259
+ gl.flush();
260
+ return new Promise((resolve) => {
261
+ this.fences.push({ sync, resolve });
262
+ this.schedulePoll();
263
+ });
264
+ }
265
+ schedulePoll() {
266
+ if (this.polling) return;
267
+ this.polling = true;
268
+ setTimeout(this.poll, 0);
269
+ }
270
+ poll = () => {
271
+ this.polling = false;
272
+ const gl = this.gl;
273
+ while (this.fences.length > 0) {
274
+ const f = this.fences[0];
275
+ if (f.sync && gl.getSyncParameter(f.sync, GL.SYNC_STATUS) !== GL.SIGNALED && !gl.isContextLost())
276
+ break;
277
+ this.fences.shift();
278
+ if (f.sync) gl.deleteSync(f.sync);
279
+ f.resolve();
280
+ }
281
+ if (this.fences.length > 0) this.schedulePoll();
282
+ };
283
+ /** @internal `buffer.mapAsync`: reads its bytes once the GPU is done writing them. */
284
+ mapBuffer(buffer, mode, offset, size) {
285
+ if (mode & MapMode.WRITE) {
286
+ return Promise.reject(
287
+ unsupported("map a buffer for writing", "Write it with queue.writeBuffer.")
288
+ );
289
+ }
290
+ if ((buffer.usage & BufferUsage.MAP_READ) === 0 || buffer.mapState !== "unmapped" || buffer.destroyed) {
291
+ const error = new Error(`Buffer "${buffer.label}" can't be mapped for reading now`);
292
+ error.name = "OperationError";
293
+ return Promise.reject(error);
294
+ }
295
+ buffer.mapState = "pending";
296
+ return this.afterGpu().then(async () => {
297
+ if (buffer.destroyed || buffer.mapState !== "pending") {
298
+ const error = new Error(`Mapping "${buffer.label}" was aborted`);
299
+ error.name = "AbortError";
300
+ throw error;
301
+ }
302
+ const bytes = new Uint8Array(size);
303
+ if (!this.isLost) {
304
+ const gl = this.gl;
305
+ gl.bindBuffer(GL.COPY_READ_BUFFER, buffer.gl);
306
+ gl.getBufferSubData(GL.COPY_READ_BUFFER, offset, bytes);
307
+ gl.bindBuffer(GL.COPY_READ_BUFFER, null);
308
+ for (const job of buffer.reads) {
309
+ const last = job.bytesPerRow * (job.rowsPerImage * (job.layers - 1) + job.rows - 1) + job.width * job.info.bytes;
310
+ if (job.offset >= offset && job.offset + last <= offset + size) {
311
+ this.copier.finishRead(job, bytes, -offset);
312
+ } else {
313
+ this.copier.releaseRead(job);
314
+ }
315
+ }
316
+ buffer.reads.length = 0;
317
+ }
318
+ if (buffer.queryReads.length > 0) await finishQueryReads(this, buffer, bytes, offset);
319
+ if (buffer.destroyed || buffer.mapState !== "pending") {
320
+ const error = new Error(`Mapping "${buffer.label}" was aborted`);
321
+ error.name = "AbortError";
322
+ throw error;
323
+ }
324
+ buffer.mapped = bytes.buffer;
325
+ buffer.mappedOffset = offset;
326
+ buffer.mapState = "mapped";
327
+ return void 0;
328
+ });
329
+ }
330
+ // --- helpers for the objects ------------------------------------------------------
331
+ /** Sets a texture's base and max level, the mip range a view reads (the texture is bound and active). */
332
+ levels(texture, base, max) {
333
+ if (!texture.gl) return;
334
+ if (texture.baseLevel !== base) {
335
+ this.gl.texParameteri(texture.target, GL.TEXTURE_BASE_LEVEL, base);
336
+ texture.baseLevel = base;
337
+ }
338
+ if (texture.maxLevel !== max) {
339
+ this.gl.texParameteri(texture.target, GL.TEXTURE_MAX_LEVEL, max);
340
+ texture.maxLevel = max;
341
+ }
342
+ }
343
+ /** The most samples a renderbuffer of `internal` format takes. */
344
+ samplesFor(internal) {
345
+ let n = this.samples.get(internal);
346
+ if (n === void 0) {
347
+ const counts = this.gl.getInternalformatParameter(
348
+ GL.RENDERBUFFER,
349
+ internal,
350
+ GL.SAMPLES
351
+ );
352
+ n = Math.min(counts && counts.length > 0 ? Math.max(...counts) : 0, this.caps.maxSamples);
353
+ this.samples.set(internal, n);
354
+ }
355
+ return n;
356
+ }
357
+ /** @internal Forgets a destroyed texture's framebuffers, bindings and copies. */
358
+ forgetTexture(texture) {
359
+ this.state.forgetTexture(texture.gl);
360
+ this.fbos.forget(texture);
361
+ this.replayer.forget(texture);
362
+ const i = this.touched.indexOf(texture);
363
+ if (i !== -1) this.touched.splice(i, 1);
364
+ }
365
+ /** @internal Forgets a destroyed buffer's bindings and pending reads. */
366
+ forgetBuffer(buffer) {
367
+ this.state.forgetBuffer(buffer.gl);
368
+ for (const job of buffer.reads) this.copier.releaseRead(job);
369
+ buffer.reads.length = 0;
370
+ }
371
+ /** Raises GL errors since the last check. */
372
+ checkErrors(where) {
373
+ const gl = this.gl;
374
+ for (let n = 0; n < 8; n++) {
375
+ const e = gl.getError();
376
+ if (e === GL.NO_ERROR || e === GL.CONTEXT_LOST_WEBGL) return;
377
+ this.raise(
378
+ e === GL.OUT_OF_MEMORY ? "out-of-memory" : "validation",
379
+ `WebGL error ${GL_ERRORS[e] ?? `0x${e.toString(16)}`} in ${where}`
380
+ );
381
+ }
382
+ }
383
+ /** Where translations came from, and what naga translated this session (0064). */
384
+ get shaderCache() {
385
+ return this.options.translator.stats;
386
+ }
387
+ /** The translations this device used, as a baked set (see `Translator.export`). */
388
+ exportShaders() {
389
+ return this.options.translator.export();
390
+ }
391
+ }
392
+ const GL_ERRORS = {
393
+ [GL.INVALID_ENUM]: "INVALID_ENUM",
394
+ [GL.INVALID_VALUE]: "INVALID_VALUE",
395
+ [GL.INVALID_OPERATION]: "INVALID_OPERATION",
396
+ [GL.INVALID_FRAMEBUFFER_OPERATION]: "INVALID_FRAMEBUFFER_OPERATION",
397
+ [GL.OUT_OF_MEMORY]: "OUT_OF_MEMORY"
398
+ };
399
+ export {
400
+ Webgl2Device
401
+ };
@@ -0,0 +1,8 @@
1
+ import { ShardError } from '@aethervtt/shard-core';
2
+ /** A call outside the subset WebGL2 runs (compute, a reinterpreting view): thrown, named. */
3
+ export declare function unsupported(what: string, hint?: string): ShardError;
4
+ /** WebGPU's error classes, as error scopes and `uncapturederror` hand them out. */
5
+ export declare class ShimError extends Error {
6
+ readonly kind: 'validation' | 'out-of-memory' | 'internal';
7
+ constructor(kind: 'validation' | 'out-of-memory' | 'internal', message: string);
8
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,18 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ function unsupported(what, hint) {
3
+ return new ShardError("gpu-webgl2/unsupported", `WebGL2 can't ${what}`, {
4
+ hint: hint ?? "The baseline tier (0064) keeps to what WebGL2 runs; see specs/0064-webgl2-fallback.md."
5
+ });
6
+ }
7
+ class ShimError extends Error {
8
+ kind;
9
+ constructor(kind, message) {
10
+ super(message);
11
+ this.kind = kind;
12
+ this.name = kind === "validation" ? "GPUValidationError" : kind === "out-of-memory" ? "GPUOutOfMemoryError" : "GPUInternalError";
13
+ }
14
+ }
15
+ export {
16
+ ShimError,
17
+ unsupported
18
+ };