@swf2es/context3d 0.1.19 → 0.1.21

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 (56) hide show
  1. package/README.md +120 -27
  2. package/dist/agal.d.ts +11 -2
  3. package/dist/agal.d.ts.map +1 -1
  4. package/dist/agal.js +318 -47
  5. package/dist/agal.js.map +1 -1
  6. package/dist/backend.d.ts +50 -3
  7. package/dist/backend.d.ts.map +1 -1
  8. package/dist/backend.js +74 -2
  9. package/dist/backend.js.map +1 -1
  10. package/dist/errors.d.ts +1 -1
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/webgl/back-buffer.d.ts +6 -0
  18. package/dist/webgl/back-buffer.d.ts.map +1 -0
  19. package/dist/webgl/back-buffer.js +197 -0
  20. package/dist/webgl/back-buffer.js.map +1 -0
  21. package/dist/webgl/buffers.d.ts +2 -2
  22. package/dist/webgl/buffers.d.ts.map +1 -1
  23. package/dist/webgl/buffers.js +105 -2
  24. package/dist/webgl/buffers.js.map +1 -1
  25. package/dist/webgl/context.d.ts +29 -48
  26. package/dist/webgl/context.d.ts.map +1 -1
  27. package/dist/webgl/context.js +227 -364
  28. package/dist/webgl/context.js.map +1 -1
  29. package/dist/webgl/programs.d.ts +4 -1
  30. package/dist/webgl/programs.d.ts.map +1 -1
  31. package/dist/webgl/programs.js +7 -6
  32. package/dist/webgl/programs.js.map +1 -1
  33. package/dist/webgl/readback.d.ts +3 -0
  34. package/dist/webgl/readback.d.ts.map +1 -0
  35. package/dist/webgl/readback.js +40 -0
  36. package/dist/webgl/readback.js.map +1 -0
  37. package/dist/webgl/targets.d.ts +3 -1
  38. package/dist/webgl/targets.d.ts.map +1 -1
  39. package/dist/webgl/targets.js +5 -3
  40. package/dist/webgl/targets.js.map +1 -1
  41. package/dist/webgl/textures.d.ts +9 -1
  42. package/dist/webgl/textures.d.ts.map +1 -1
  43. package/dist/webgl/textures.js +161 -7
  44. package/dist/webgl/textures.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/agal.ts +375 -51
  47. package/src/backend.ts +113 -3
  48. package/src/errors.ts +27 -1
  49. package/src/index.ts +12 -1
  50. package/src/webgl/back-buffer.ts +287 -0
  51. package/src/webgl/buffers.ts +114 -2
  52. package/src/webgl/context.ts +291 -489
  53. package/src/webgl/programs.ts +10 -5
  54. package/src/webgl/readback.ts +54 -0
  55. package/src/webgl/targets.ts +4 -2
  56. package/src/webgl/textures.ts +183 -9
@@ -1,20 +1,38 @@
1
- import { MOST_BACK_BUFFER, SAMPLER_FILTERS, SAMPLER_MIPS, SAMPLER_WRAPS, ScissorBoundsError, screenPixels, TextureBindingError, VERTEX_FORMATS, VertexOffsetError, } from "../backend.js";
1
+ import { INDIRECT_BANK } from "../agal.js";
2
+ import { compressedFormat, FRAGMENT_CONSTANTS, MOST_CUBE, profileLimits, SAMPLER_FILTERS, SAMPLER_MIPS, SAMPLER_SLOTS, SAMPLER_WRAPS, ScissorBoundsError, screenPixels, TEXEL_BYTES, TextureBindingError, VERTEX_CONSTANTS, VERTEX_FORMATS, VERTEX_SLOTS, VertexOffsetError, } from "../backend.js";
2
3
  import { GlState } from "../device/state.js";
3
4
  import { blockBytes } from "../dxt.js";
4
5
  import { Context3DError } from "../errors.js";
5
- import { uploadBuffer, uploadBufferBytes, } from "./buffers.js";
6
+ import { configureBackBuffer, presentBackBuffer, unconfigureBackBuffer } from "./back-buffer.js";
7
+ import { allocateBuffer } from "./buffers.js";
6
8
  import { resourceLimits } from "./limits.js";
7
9
  import { createCopyProgram, uploadProgram, } from "./programs.js";
10
+ import { readBackBuffer } from "./readback.js";
8
11
  import { BLEND_FUNCTIONS, DEPTH_FUNCTIONS, range, STENCIL_FUNCTIONS, StencilState, } from "./state.js";
9
12
  import { allocateMultisample, deleteMultisample, resolveTarget, selectSamples, targetFramebuffer, } from "./targets.js";
10
- import { uploadCompressedLevel, uploadTexture, uploadTextureBytes, } from "./textures.js";
13
+ import { storageFormat, uploadCompressedLevel, uploadTexture, uploadTextureBytes, } from "./textures.js";
11
14
  import { validateIndices, validateVertices } from "./validation.js";
12
15
  /** What AGAL's sampler must name to read each texture format: RGBA, DXT1 or DXT5. */
13
16
  const SAMPLER_FORMATS = {
14
17
  bgra: 0,
15
18
  compressed: 1,
16
19
  compressedAlpha: 2,
20
+ bgrPacked565: 0,
21
+ bgraPacked4444: 0,
22
+ rgbaHalfFloat: 0,
17
23
  };
24
+ /**
25
+ * A clear colour's channel as Direct3D's 8-bit clear colour holds it, which AIR fills by
26
+ * truncation (0.5 clears to 127): within 0 to 1 and cut to its byte, NaN kept.
27
+ */
28
+ function clearChannel(value) {
29
+ if (!(value > 0)) {
30
+ return value < 0 ? 0 : value;
31
+ }
32
+ return value >= 1 ? 1 : Math.floor(value * 255 + 1e-3) / 255;
33
+ }
34
+ /** AGAL's wraps (clamp, repeat, clamp_u_repeat_v, repeat_u_clamp_v) as SAMPLER_WRAPS indices. */
35
+ const WRAPS = [0, 3, 2, 1];
18
36
  function constants(registers) {
19
37
  const data = new Float32Array(registers * 4);
20
38
  return {
@@ -25,9 +43,14 @@ function constants(registers) {
25
43
  writing: 0,
26
44
  };
27
45
  }
28
- /** An owned WebGL2 context, deliberately independent of the 2D renderer. */
46
+ /**
47
+ * An owned WebGL2 context, deliberately independent of the 2D renderer. Its members that are
48
+ * neither private nor Context3D's are for back-buffer.ts, buffers.ts and readback.ts, which
49
+ * hold parts of it as functions over it.
50
+ */
29
51
  export class WebGLContext3D {
30
52
  gl;
53
+ /** @internal */
31
54
  drawState;
32
55
  width = 0;
33
56
  height = 0;
@@ -35,14 +58,22 @@ export class WebGLContext3D {
35
58
  * The back buffer's storage: its size, or less where the GPU allows less. The SWF still
36
59
  * sees the size it asked for, drawn at this one and stretched to it when presented.
37
60
  */
61
+ /** @internal */
38
62
  drawnWidth = 0;
63
+ /** @internal */
39
64
  drawnHeight = 0;
65
+ /** @internal */
40
66
  gpuMost = null;
41
67
  disposed = false;
42
68
  samplerFormats = true;
69
+ profile = "baseline";
70
+ /** @internal */
43
71
  buffers = new Map();
72
+ /** @internal */
44
73
  vertexBuffers = 0;
74
+ /** @internal */
45
75
  bufferBytes = 0;
76
+ /** @internal */
46
77
  vertexBufferBytes = 0;
47
78
  programs = new Map();
48
79
  textures = new Map();
@@ -55,10 +86,18 @@ export class WebGLContext3D {
55
86
  copy2D = null;
56
87
  copyCube = null;
57
88
  targetSamples = new Int8Array(10).fill(-1);
89
+ /** @internal */
58
90
  renderTexture = null;
59
91
  renderDepth = false;
60
92
  renderFace = 0;
61
- textureSlots = new Array(8).fill(null);
93
+ /** Colour outputs 1 to 3, drawn into with renderTexture, and their cube faces. */
94
+ outputs = [null, null, null];
95
+ outputFaces = new Uint8Array(3);
96
+ outputsChanged = false;
97
+ /** Whether any of outputs is bound, so that a draw alone into its target looks at none. */
98
+ outputsBound = false;
99
+ floatTargets;
100
+ textureSlots = new Array(SAMPLER_SLOTS).fill(null);
62
101
  textureMask = 0;
63
102
  samplers = new Array(96).fill(null);
64
103
  // Two wrap bits, three filter bits and two mip bits fit each slot in one byte.
@@ -66,19 +105,30 @@ export class WebGLContext3D {
66
105
  anisotropy;
67
106
  s3tc;
68
107
  maxAnisotropy = 1;
69
- vertices = new Array(8).fill(null);
108
+ /** @internal */
109
+ vertices = new Array(VERTEX_SLOTS).fill(null);
70
110
  enabledAttributes = 0;
111
+ /** @internal */
71
112
  indexBuffer = null;
72
- texturesApplied = new Uint8Array(8);
113
+ /** @internal */
114
+ texturesApplied = new Uint8Array(SAMPLER_SLOTS);
73
115
  selected = null;
116
+ /** @internal */
74
117
  framebuffer = null;
118
+ /** @internal */
75
119
  texture = null;
120
+ /** @internal */
76
121
  resolveFramebuffer = null;
122
+ /** @internal */
77
123
  colorBuffer = null;
124
+ /** @internal */
78
125
  resolveDirty = false;
126
+ /** @internal */
79
127
  depthBuffer = null;
128
+ /** @internal */
80
129
  antiAlias = 0;
81
130
  /** The back buffer's pixels to a CSS pixel and to a scissor rectangle's unit. */
131
+ /** @internal */
82
132
  scale = 1;
83
133
  depthWrite = true;
84
134
  depthCompare = "less";
@@ -93,17 +143,22 @@ export class WebGLContext3D {
93
143
  stencilReference = 0;
94
144
  stencilRead = 255;
95
145
  stencilWrite = 255;
146
+ /** @internal */
96
147
  scissor = false;
97
148
  scissorLeft = 0;
98
149
  scissorTop = 0;
99
150
  scissorRight = 0;
100
151
  scissorBottom = 0;
101
152
  scissorOutside = false;
102
- vertexConstants = constants(128);
103
- fragmentConstants = constants(28);
153
+ vertexConstants = constants(VERTEX_CONSTANTS);
154
+ fragmentConstants = constants(FRAGMENT_CONSTANTS);
155
+ /** @internal */
104
156
  pixels = new Uint8Array(0);
157
+ /** @internal */
105
158
  topDown = new Uint8Array(0);
159
+ /** @internal */
106
160
  vao;
161
+ /** @internal */
107
162
  limits;
108
163
  dxtEncoder = null;
109
164
  /** The host's whole-level encoder; a refusal retains the TypeScript path. */
@@ -138,11 +193,13 @@ export class WebGLContext3D {
138
193
  gl.frontFace(gl.CW);
139
194
  gl.disable(gl.DITHER);
140
195
  }
196
+ /** @internal */
141
197
  alive() {
142
198
  if (this.disposed || this.gl.isContextLost()) {
143
199
  throw new Context3DError("disposed", "Context3D experiment: context disposed or lost");
144
200
  }
145
201
  }
202
+ /** @internal */
146
203
  target(back = false, discardColor = false) {
147
204
  this.alive();
148
205
  if (!this.framebuffer) {
@@ -159,174 +216,30 @@ export class WebGLContext3D {
159
216
  }
160
217
  }
161
218
  this.drawState.bindFramebuffer(state ? targetFramebuffer(state) : this.framebuffer);
219
+ if (this.outputsChanged || state?.outputs) {
220
+ this.attachOutputs(state);
221
+ }
162
222
  this.drawState.viewport(0, 0, texture?.width ?? this.drawnWidth, texture?.height ?? this.drawnHeight);
163
223
  }
164
224
  /** Temporary attachments and separate READ/DRAW bindings bypass the hot target cache. */
225
+ /** @internal */
165
226
  bindFramebuffer(target, framebuffer) {
166
227
  this.drawState.forget();
167
228
  this.gl.bindFramebuffer(target, framebuffer);
168
229
  }
169
230
  configure(width, height, depth = false, antiAlias = 0, scale = 1) {
170
- this.alive();
171
- range(32, width - 32, MOST_BACK_BUFFER);
172
- range(32, height - 32, MOST_BACK_BUFFER);
173
- if (this.renderTexture) {
174
- throw new Context3DError("operation", "Context3D experiment: resize while rendering to texture");
175
- }
176
- // AIR preserves color, depth and bindings when the request is unchanged.
177
- // Shadow passes can issue this every frame when returning to the back buffer.
178
- if (this.framebuffer &&
179
- width === this.width &&
180
- height === this.height &&
181
- depth === (this.depthBuffer !== null) &&
182
- antiAlias === this.antiAlias) {
183
- if (scale !== this.scale) {
184
- this.scale = scale;
185
- this.showAt();
186
- this.applyScissor();
187
- }
188
- return;
189
- }
190
- const samples = selectSamples(this.gl, antiAlias, depth);
191
- const gl = this.gl;
192
- const most = this.storageLimit();
193
- const drawnWidth = Math.min(width, most[0]);
194
- const drawnHeight = Math.min(height, most[1]);
195
- const texture = gl.createTexture();
196
- const framebuffer = gl.createFramebuffer();
197
- const depthBuffer = depth ? gl.createRenderbuffer() : null;
198
- const resolveFramebuffer = samples ? gl.createFramebuffer() : null;
199
- const colorBuffer = samples ? gl.createRenderbuffer() : null;
200
- try {
201
- if (!texture ||
202
- !framebuffer ||
203
- (depth && !depthBuffer) ||
204
- (samples && (!resolveFramebuffer || !colorBuffer))) {
205
- throw new Context3DError("operation", "Context3D experiment: cannot allocate back buffer");
206
- }
207
- this.texturesApplied[0] = 0;
208
- gl.activeTexture(gl.TEXTURE0);
209
- gl.bindTexture(gl.TEXTURE_2D, texture);
210
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
211
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
212
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, drawnWidth, drawnHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
213
- this.bindFramebuffer(gl.FRAMEBUFFER, resolveFramebuffer ?? framebuffer);
214
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
215
- if (resolveFramebuffer) {
216
- if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
217
- throw new Context3DError("operation", "Context3D experiment: incomplete resolve buffer");
218
- }
219
- this.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
220
- gl.bindRenderbuffer(gl.RENDERBUFFER, colorBuffer);
221
- gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, drawnWidth, drawnHeight);
222
- gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuffer);
223
- }
224
- if (depthBuffer) {
225
- gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
226
- if (samples) {
227
- gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.DEPTH24_STENCIL8, drawnWidth, drawnHeight);
228
- }
229
- else {
230
- gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, drawnWidth, drawnHeight);
231
- }
232
- gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
233
- }
234
- gl.bindRenderbuffer(gl.RENDERBUFFER, null);
235
- if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
236
- throw new Context3DError("operation", "Context3D experiment: incomplete back buffer");
237
- }
238
- }
239
- catch (error) {
240
- gl.deleteTexture(texture);
241
- gl.deleteFramebuffer(framebuffer);
242
- gl.deleteFramebuffer(resolveFramebuffer);
243
- gl.deleteRenderbuffer(depthBuffer);
244
- gl.deleteRenderbuffer(colorBuffer);
245
- gl.bindRenderbuffer(gl.RENDERBUFFER, null);
246
- this.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
247
- throw error;
248
- }
249
- gl.deleteTexture(this.texture);
250
- gl.deleteFramebuffer(this.framebuffer);
251
- gl.deleteFramebuffer(this.resolveFramebuffer);
252
- gl.deleteRenderbuffer(this.depthBuffer);
253
- gl.deleteRenderbuffer(this.colorBuffer);
254
- this.texture = texture;
255
- this.framebuffer = framebuffer;
256
- this.resolveFramebuffer = resolveFramebuffer;
257
- this.colorBuffer = colorBuffer;
258
- this.depthBuffer = depthBuffer;
259
- this.antiAlias = antiAlias;
260
- this.width = width;
261
- this.height = height;
262
- this.drawnWidth = drawnWidth;
263
- this.drawnHeight = drawnHeight;
264
- this.scale = scale;
265
- gl.canvas.width = drawnWidth;
266
- gl.canvas.height = drawnHeight;
267
- this.showAt();
268
- // Readback's storage waits for a read: a back buffer as large as a 4K screen is 33 MB each.
269
- this.pixels = new Uint8Array(0);
270
- this.topDown = new Uint8Array(0);
271
- this.applyScissor();
272
- this.clear(0, 0, 0, 1);
273
- }
274
- /**
275
- * Shown at the size asked for, in CSS pixels, as an unstyled canvas of that size would be,
276
- * divided by its scale: a best-resolution back buffer shows its pixels at the screen's.
277
- */
278
- showAt() {
279
- const style = this.gl.canvas.style;
280
- if (!style) {
281
- return;
282
- }
283
- const plain = this.scale === 1;
284
- style.width = plain && this.drawnWidth === this.width ? "" : `${this.width / this.scale}px`;
285
- style.height = plain && this.drawnHeight === this.height ? "" : `${this.height / this.scale}px`;
286
- }
287
- /** The largest back buffer storage the GPU gives: a texture, a renderbuffer and a viewport. */
288
- storageLimit() {
289
- if (!this.gpuMost) {
290
- const gl = this.gl;
291
- const viewport = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
292
- const side = Math.min(gl.getParameter(gl.MAX_TEXTURE_SIZE), gl.getParameter(gl.MAX_RENDERBUFFER_SIZE));
293
- this.gpuMost = Int32Array.of(Math.min(side, viewport[0]), Math.min(side, viewport[1]));
294
- }
295
- return this.gpuMost;
231
+ configureBackBuffer(this, width, height, depth, antiAlias, scale);
296
232
  }
297
233
  unconfigure() {
298
- this.alive();
299
- if (this.renderTexture) {
300
- throw new Context3DError("operation", "Context3D experiment: resize while rendering to texture");
301
- }
302
- const gl = this.gl;
303
- gl.deleteTexture(this.texture);
304
- gl.deleteFramebuffer(this.framebuffer);
305
- gl.deleteFramebuffer(this.resolveFramebuffer);
306
- gl.deleteRenderbuffer(this.depthBuffer);
307
- gl.deleteRenderbuffer(this.colorBuffer);
308
- this.drawState.forget();
309
- this.texture = null;
310
- this.framebuffer = null;
311
- this.resolveFramebuffer = null;
312
- this.colorBuffer = null;
313
- this.depthBuffer = null;
314
- this.resolveDirty = false;
315
- this.antiAlias = 0;
316
- this.scale = 1;
317
- this.width = 0;
318
- this.height = 0;
319
- this.drawnWidth = 0;
320
- this.drawnHeight = 0;
321
- this.pixels = new Uint8Array(0);
322
- this.topDown = new Uint8Array(0);
234
+ unconfigureBackBuffer(this);
323
235
  }
324
236
  clear(red, green, blue, alpha, depth = 1, mask = 0xffffffff, stencil = 0) {
325
237
  this.target(false, (mask & 1) !== 0);
326
238
  const gl = this.gl;
327
239
  let bits = 0;
328
240
  if (mask & 1) {
329
- gl.clearColor(red, green, blue, alpha);
241
+ // A float target too: AIR clears through Direct3D's 8-bit colour.
242
+ gl.clearColor(clearChannel(red), clearChannel(green), clearChannel(blue), clearChannel(alpha));
330
243
  bits |= gl.COLOR_BUFFER_BIT;
331
244
  this.changedTarget();
332
245
  }
@@ -464,6 +377,7 @@ export class WebGLContext3D {
464
377
  }
465
378
  this.applyScissor();
466
379
  }
380
+ /** @internal */
467
381
  applyScissor() {
468
382
  const gl = this.gl;
469
383
  if (!this.scissor) {
@@ -549,107 +463,9 @@ export class WebGLContext3D {
549
463
  }
550
464
  }
551
465
  createBuffer(count, stride, dynamic = false) {
552
- this.alive();
553
- range(1, count - 1, stride === 0 ? 524287 : 65535);
554
- range(0, stride, 64);
555
- const countLimit = stride === 0 ? this.limits.maxIndexBuffers : this.limits.maxVertexBuffers;
556
- const used = stride === 0 ? this.buffers.size - this.vertexBuffers : this.vertexBuffers;
557
- if (this.buffers.size >= this.limits.maxBuffers || used >= countLimit) {
558
- throw new Context3DError("limit", "Context3D experiment: buffer limit");
559
- }
560
- const bytes = count * (stride === 0 ? 2 : stride * 4);
561
- const usedBytes = stride === 0 ? this.bufferBytes - this.vertexBufferBytes : this.vertexBufferBytes;
562
- const byteLimit = stride === 0 ? this.limits.maxIndexBufferBytes : this.limits.maxVertexBufferBytes;
563
- if (this.bufferBytes + bytes > this.limits.maxBufferBytes || usedBytes + bytes > byteLimit) {
564
- throw new Context3DError("limit", "Context3D experiment: buffer memory limit");
565
- }
566
- const gl = this.gl;
567
- const handle = gl.createBuffer();
568
- if (!handle) {
569
- throw new Context3DError("bufferAllocation", "Context3D experiment: cannot allocate buffer");
570
- }
571
- const target = stride === 0 ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER;
572
- if (stride === 0) {
573
- this.indexBuffer = null;
574
- }
575
- gl.bindVertexArray(this.vao);
576
- gl.bindBuffer(target, handle);
577
- gl.bufferData(target, bytes, dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
578
- const state = {
579
- handle,
580
- indices: stride === 0 ? new Uint16Array(count) : null,
581
- uploaded: new Uint8Array(count),
582
- prefix: 0,
583
- version: 0,
584
- checkedVersion: -1,
585
- checkedFirst: 0,
586
- checkedCount: 0,
587
- maximum: -1,
588
- ranges: null,
589
- };
590
- const buffer = {
591
- count,
592
- stride,
593
- upload: (values, start, amount) => {
594
- this.alive();
595
- this.buffer(buffer);
596
- try {
597
- uploadBuffer(gl, this.vao, buffer, state, values, start, amount);
598
- }
599
- finally {
600
- // Source getters can draw before the upload replaces the VAO's index binding.
601
- if (stride === 0) {
602
- this.indexBuffer = null;
603
- }
604
- }
605
- },
606
- uploadBytes: (data, offset, start, amount) => {
607
- this.alive();
608
- this.buffer(buffer);
609
- try {
610
- uploadBufferBytes(gl, this.vao, buffer, state, data, offset, start, amount);
611
- }
612
- finally {
613
- if (stride === 0) {
614
- this.indexBuffer = null;
615
- }
616
- }
617
- },
618
- dispose: () => {
619
- if (!this.buffers.delete(buffer)) {
620
- return;
621
- }
622
- if (this.indexBuffer === handle) {
623
- this.indexBuffer = null;
624
- }
625
- gl.deleteBuffer(handle);
626
- this.bufferBytes -= bytes;
627
- if (stride !== 0) {
628
- this.vertexBuffers--;
629
- this.vertexBufferBytes -= bytes;
630
- }
631
- state.indices = null;
632
- state.ranges = null;
633
- state.uploaded = new Uint8Array(0);
634
- for (let i = 0; i < this.vertices.length; i++) {
635
- const binding = this.vertices[i];
636
- if (binding?.buffer === buffer) {
637
- this.vertices[i] = null;
638
- }
639
- else if (binding?.checkedIndex === state) {
640
- binding.checkedIndex = null;
641
- }
642
- }
643
- },
644
- };
645
- this.buffers.set(buffer, state);
646
- if (stride !== 0) {
647
- this.vertexBuffers++;
648
- this.vertexBufferBytes += bytes;
649
- }
650
- this.bufferBytes += bytes;
651
- return buffer;
466
+ return allocateBuffer(this, count, stride, dynamic);
652
467
  }
468
+ /** @internal */
653
469
  buffer(buffer) {
654
470
  const state = this.buffers.get(buffer);
655
471
  if (!state) {
@@ -670,6 +486,7 @@ export class WebGLContext3D {
670
486
  attributeMask: 0,
671
487
  vertex: [],
672
488
  vertexBank: null,
489
+ outputs: 1,
673
490
  fragment: [],
674
491
  samplers: [],
675
492
  samplerMask: 0,
@@ -681,11 +498,17 @@ export class WebGLContext3D {
681
498
  vertexVersion: -1,
682
499
  };
683
500
  const program = {
501
+ get outputs() {
502
+ return state.outputs;
503
+ },
504
+ get attributes() {
505
+ return state.attributeMask;
506
+ },
684
507
  upload: (vertex, fragment) => {
685
508
  this.alive();
686
509
  this.program(program);
687
510
  this.drawState.forget();
688
- uploadProgram(this.gl, state, vertex, fragment);
511
+ uploadProgram(this.gl, state, vertex, fragment, this.profile);
689
512
  if (this.selected === program) {
690
513
  this.setProgram(program);
691
514
  }
@@ -731,15 +554,15 @@ export class WebGLContext3D {
731
554
  createTexture(width, height, cube = false, format = "bgra", streaming = 0) {
732
555
  return this.allocateTexture(width, height, cube, false, format, streaming);
733
556
  }
734
- createRectangleTexture(width, height) {
735
- return this.allocateTexture(width, height, false, true, "bgra");
557
+ createRectangleTexture(width, height, format = "bgra") {
558
+ return this.allocateTexture(width, height, false, true, format);
736
559
  }
737
560
  /** GL's internal format for a compressed texture's S3TC, or 0 where the context takes none. */
738
561
  s3tcFormat(format) {
739
562
  if (this.s3tc === undefined) {
740
563
  this.s3tc = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
741
564
  }
742
- if (!this.s3tc || format === "bgra") {
565
+ if (!this.s3tc || !compressedFormat(format)) {
743
566
  return 0;
744
567
  }
745
568
  // AIR's DXT1 has DXT1's one bit of alpha: a block of three colours has transparent black.
@@ -757,18 +580,22 @@ export class WebGLContext3D {
757
580
  for (let level = 0; level < levels; level++) {
758
581
  const w = Math.max(1, width >> level);
759
582
  const h = Math.max(1, height >> level);
760
- size += format === "bgra" ? w * h * 4 : blockBytes(w, h, format === "compressedAlpha");
583
+ size += compressedFormat(format)
584
+ ? blockBytes(w, h, format === "compressedAlpha")
585
+ : w * h * (TEXEL_BYTES[format] ?? 4);
761
586
  }
762
587
  return size * faces;
763
588
  }
764
589
  allocateTexture(width, height, cube, rectangle, format, streaming = 0) {
765
590
  this.alive();
766
- range(1, width - 1, 2048);
767
- range(1, height - 1, 2048);
591
+ const limits = profileLimits(this.profile);
592
+ const most = rectangle ? limits.rectangleSize : limits.textureSize;
593
+ range(1, width - 1, most);
594
+ range(1, height - 1, most);
768
595
  if (cube && width !== height) {
769
596
  throw new Context3DError("textureShape", "Context3D cube faces must be square");
770
597
  }
771
- if (cube && width > 1024) {
598
+ if (cube && width > MOST_CUBE) {
772
599
  throw new Context3DError("textureSize", "Context3D cube faces must be at most 1024");
773
600
  }
774
601
  const faces = cube ? 6 : 1;
@@ -777,14 +604,14 @@ export class WebGLContext3D {
777
604
  if (!rectangle && ((width & (width - 1)) !== 0 || (height & (height - 1)) !== 0)) {
778
605
  throw new Context3DError("texturePowerOfTwo", "Context3D texture size must be a power of two");
779
606
  }
780
- if (format !== "bgra" && rectangle) {
607
+ if (compressedFormat(format) && rectangle) {
781
608
  throw new Context3DError("unsupported", "Context3D rectangle textures are not compressed");
782
609
  }
783
- if (format !== "bgra" && (width < 4 || height < 4)) {
610
+ if (compressedFormat(format) && (width < 4 || height < 4)) {
784
611
  throw new Context3DError("compressedSize", "Context3D compressed textures are at least 4x4");
785
612
  }
786
613
  const gl = this.gl;
787
- const storage = this.s3tcFormat(format) || gl.RGBA8;
614
+ const storage = this.s3tcFormat(format) || storageFormat(gl, format);
788
615
  const size = this.chainBytes(width, height, levels, faces, format);
789
616
  const kind = cube ? 2 : rectangle ? 1 : 0;
790
617
  const countLimit = cube
@@ -799,7 +626,8 @@ export class WebGLContext3D {
799
626
  : this.limits.max2DTextureBytes;
800
627
  // AIR reserves half the base size for mips, independently of the exact
801
628
  // chain storage, and charges DXT1 an eighth of BGRA and DXT5 a quarter.
802
- const quotaBytes = (width * height * faces * (rectangle ? 4 : 6)) /
629
+ const quotaBytes = (width * height * faces * (rectangle ? 4 : 6) * (TEXEL_BYTES[format] ?? 4)) /
630
+ 4 /
803
631
  (format === "compressed" ? 8 : format === "compressedAlpha" ? 4 : 1);
804
632
  if (this.textures.size >= this.limits.maxTextures ||
805
633
  this.textureCounts[kind] >= countLimit ||
@@ -830,6 +658,7 @@ export class WebGLContext3D {
830
658
  multisample: null,
831
659
  samples: 0,
832
660
  antiAlias: 0,
661
+ outputs: 0,
833
662
  base: 0,
834
663
  };
835
664
  const texture = {
@@ -864,8 +693,8 @@ export class WebGLContext3D {
864
693
  uploadCompressed: (blocks, level = 0, face = 0) => {
865
694
  this.alive();
866
695
  this.textureState(texture);
867
- if (format === "bgra") {
868
- throw new Context3DError("textureUpload", "Context3D experiment: BGRA texture given blocks");
696
+ if (!compressedFormat(format)) {
697
+ throw new Context3DError("textureUpload", "Context3D experiment: uncompressed texture given blocks");
869
698
  }
870
699
  try {
871
700
  uploadCompressedLevel(gl, texture, state, blocks, level, face);
@@ -901,6 +730,13 @@ export class WebGLContext3D {
901
730
  state.framebuffer = null;
902
731
  state.mipFramebuffer = null;
903
732
  state.depthBuffer = null;
733
+ for (let i = 0; i < this.outputs.length; i++) {
734
+ if (this.outputs[i] === texture) {
735
+ this.outputs[i] = null;
736
+ this.outputsChanged = true;
737
+ this.outputsBound = this.outputMask() !== 0;
738
+ }
739
+ }
904
740
  for (let i = 0; i < this.textureSlots.length; i++) {
905
741
  if (this.textureSlots[i] === texture) {
906
742
  this.texturesApplied[i] = 0;
@@ -1023,7 +859,7 @@ export class WebGLContext3D {
1023
859
  this.sampler(0);
1024
860
  this.drawState.forget();
1025
861
  this.texturesApplied[0] = 0;
1026
- const target = allocateMultisample(this.gl, texture, old, samples, depth, this.depthWrite, this.stencilWrite);
862
+ const target = allocateMultisample(this.gl, texture, old, samples, depth, this.depthWrite, this.stencilWrite, state.storage);
1027
863
  if (same) {
1028
864
  if (newDepth) {
1029
865
  this.targetDepthBytes += bytes;
@@ -1055,9 +891,10 @@ export class WebGLContext3D {
1055
891
  const gl = this.gl;
1056
892
  if (texture) {
1057
893
  this.textureState(texture);
1058
- if (texture.format !== "bgra") {
894
+ if (compressedFormat(texture.format)) {
1059
895
  throw new Context3DError("operation", "Context3D experiment: compressed textures are no render targets");
1060
896
  }
897
+ this.renderFormat(texture.format);
1061
898
  }
1062
899
  const samples = texture ? this.multisamples(antiAlias, depth) : 0;
1063
900
  this.finishTarget();
@@ -1155,6 +992,12 @@ export class WebGLContext3D {
1155
992
  this.applyScissor();
1156
993
  }
1157
994
  changedTarget() {
995
+ for (let i = 0; this.outputsBound && i < this.outputs.length; i++) {
996
+ const output = this.outputs[i];
997
+ if (output) {
998
+ this.textureState(output).dirty = true;
999
+ }
1000
+ }
1158
1001
  if (this.renderTexture) {
1159
1002
  const state = this.textureState(this.renderTexture);
1160
1003
  state.dirty = true;
@@ -1172,22 +1015,33 @@ export class WebGLContext3D {
1172
1015
  }
1173
1016
  }
1174
1017
  finishTarget() {
1175
- if (!this.renderTexture) {
1176
- return;
1018
+ if (this.renderTexture) {
1019
+ this.finishTexture(this.renderTexture, this.renderFace);
1177
1020
  }
1178
- const texture = this.renderTexture;
1021
+ for (let i = 0; i < this.outputs.length; i++) {
1022
+ const output = this.outputs[i];
1023
+ if (output) {
1024
+ this.finishTexture(output, this.outputFaces[i]);
1025
+ }
1026
+ }
1027
+ }
1028
+ /** A texture drawn into made whole again: resolved, and its mips regenerated. */
1029
+ finishTexture(texture, renderFace) {
1179
1030
  const state = this.textureState(texture);
1180
1031
  if (state.dirty) {
1181
1032
  const gl = this.gl;
1182
1033
  this.drawState.forget();
1183
1034
  this.texturesApplied[0] = 0;
1184
- resolveTarget(this.gl, texture, state, this.renderFace, this.scissor);
1035
+ resolveTarget(this.gl, texture, state, renderFace, this.scissor);
1185
1036
  gl.activeTexture(gl.TEXTURE0);
1186
1037
  if (texture.cube) {
1038
+ // An extra colour output may never have been a target of its own.
1039
+ state.framebuffer ??= gl.createFramebuffer();
1040
+ state.mipFramebuffer ??= gl.createFramebuffer();
1187
1041
  // generateMipmap(CUBE_MAP) would overwrite manually uploaded mips on
1188
1042
  // untouched faces. AIR regenerates only the face just rendered.
1189
1043
  gl.disable(gl.SCISSOR_TEST);
1190
- const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.renderFace;
1044
+ const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + renderFace;
1191
1045
  // The base-size depth attachment cannot accompany smaller mip images.
1192
1046
  this.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
1193
1047
  if (state.depthAttached) {
@@ -1211,13 +1065,81 @@ export class WebGLContext3D {
1211
1065
  gl.bindTexture(gl.TEXTURE_2D, state.handle);
1212
1066
  gl.generateMipmap(gl.TEXTURE_2D);
1213
1067
  }
1214
- state.uploaded[this.renderFace] = (1 << texture.levels) - 1;
1068
+ state.uploaded[renderFace] = (1 << texture.levels) - 1;
1215
1069
  state.dirty = false;
1216
1070
  }
1217
1071
  }
1072
+ /** Half floats are drawn into where the GPU's WebGL renders float colour, as every desktop one does. */
1073
+ renderFormat(format) {
1074
+ if (format !== "rgbaHalfFloat") {
1075
+ return;
1076
+ }
1077
+ this.floatTargets ??= this.gl.getExtension("EXT_color_buffer_float") !== null;
1078
+ if (!this.floatTargets) {
1079
+ throw new Context3DError("unsupported", "Context3D: this GPU draws into no half floats");
1080
+ }
1081
+ }
1082
+ setColorOutput(index, texture, face = 0) {
1083
+ this.alive();
1084
+ range(index - 1, 1, this.outputs.length);
1085
+ if (texture) {
1086
+ this.textureState(texture);
1087
+ range(face, 1, texture.cube ? 6 : 1);
1088
+ if (compressedFormat(texture.format)) {
1089
+ throw new Context3DError("operation", "Context3D experiment: compressed textures are no render targets");
1090
+ }
1091
+ this.renderFormat(texture.format);
1092
+ }
1093
+ if (this.outputs[index - 1] === texture && this.outputFaces[index - 1] === face) {
1094
+ return;
1095
+ }
1096
+ const previous = this.outputs[index - 1];
1097
+ if (previous) {
1098
+ this.finishTexture(previous, this.outputFaces[index - 1]);
1099
+ }
1100
+ this.outputs[index - 1] = texture;
1101
+ this.outputFaces[index - 1] = texture ? face : 0;
1102
+ this.outputsChanged = true;
1103
+ this.outputsBound = this.outputMask() !== 0;
1104
+ }
1105
+ /**
1106
+ * The current target's framebuffer with the extra colour outputs attached, or without
1107
+ * them: its draws write every output bound, and a program's unwritten ones are refused
1108
+ * before they draw.
1109
+ */
1110
+ attachOutputs(state) {
1111
+ const want = state ? this.outputMask() : 0;
1112
+ if (!state || (!this.outputsChanged && state.outputs === want)) {
1113
+ return;
1114
+ }
1115
+ if (want && state.samples) {
1116
+ throw new Context3DError("unsupported", "Context3D: antialiased colour outputs");
1117
+ }
1118
+ const gl = this.gl;
1119
+ const buffers = [gl.COLOR_ATTACHMENT0];
1120
+ for (let i = 0; i < this.outputs.length; i++) {
1121
+ const output = this.outputs[i];
1122
+ const handle = output ? this.textureState(output).handle : null;
1123
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1 + i, output?.cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.outputFaces[i] : gl.TEXTURE_2D, handle, 0);
1124
+ buffers.push(output ? gl.COLOR_ATTACHMENT1 + i : gl.NONE);
1125
+ }
1126
+ gl.drawBuffers(buffers);
1127
+ state.outputs = want;
1128
+ this.outputsChanged = false;
1129
+ }
1130
+ /** The extra colour outputs bound, a bit each from output 1. */
1131
+ outputMask() {
1132
+ let mask = 0;
1133
+ for (let i = 0; i < this.outputs.length; i++) {
1134
+ if (this.outputs[i]) {
1135
+ mask |= 1 << i;
1136
+ }
1137
+ }
1138
+ return mask;
1139
+ }
1218
1140
  setTexture(index, texture) {
1219
1141
  this.alive();
1220
- range(index, 1, 8);
1142
+ range(index, 1, SAMPLER_SLOTS);
1221
1143
  if (texture && !this.textureOwners.has(texture)) {
1222
1144
  throw new Context3DError("disposed", "Context3D experiment: texture from another context");
1223
1145
  }
@@ -1302,13 +1224,13 @@ export class WebGLContext3D {
1302
1224
  for (let i = 0; i < state.samplers.length; i++) {
1303
1225
  const source = state.samplers[i].source;
1304
1226
  if (!source.ignore) {
1305
- this.setSamplerState(source.index, (source.repeat ? 3 : 0) | (source.filter << 2) | (source.mip << 5));
1227
+ this.setSamplerState(source.index, WRAPS[source.wrap] | (source.filter << 2) | (source.mip << 5));
1306
1228
  }
1307
1229
  }
1308
1230
  }
1309
1231
  setVertex(index, buffer, offset, format) {
1310
1232
  this.alive();
1311
- range(index, 1, 8);
1233
+ range(index, 1, VERTEX_SLOTS);
1312
1234
  if (buffer === null) {
1313
1235
  this.vertices[index] = null;
1314
1236
  return;
@@ -1414,13 +1336,14 @@ export class WebGLContext3D {
1414
1336
  const maximum = validateIndices(index, first, count);
1415
1337
  const gl = this.gl;
1416
1338
  this.drawState.bindVertexArray(this.vao);
1417
- for (let i = 0; i < 8; i++) {
1418
- const bit = 1 << i;
1419
- if (this.enabledAttributes & bit & ~state.attributeMask) {
1420
- gl.disableVertexAttribArray(i);
1421
- this.enabledAttributes &= ~bit;
1422
- }
1423
- }
1339
+ // The streams enabled that the program does not read, highest first.
1340
+ let stale = this.enabledAttributes & ~state.attributeMask;
1341
+ while (stale) {
1342
+ const i = 31 - Math.clz32(stale);
1343
+ gl.disableVertexAttribArray(i);
1344
+ stale &= ~(1 << i);
1345
+ }
1346
+ this.enabledAttributes &= state.attributeMask;
1424
1347
  for (let i = 0; i < state.attributes.length; i++) {
1425
1348
  const slot = state.attributes[i];
1426
1349
  const binding = this.vertices[slot];
@@ -1453,7 +1376,12 @@ export class WebGLContext3D {
1453
1376
  const binding = state.samplers[i];
1454
1377
  const slot = binding.source.index;
1455
1378
  const texture = this.textureSlots[slot];
1456
- if (texture && texture === this.renderTexture) {
1379
+ if (texture &&
1380
+ (texture === this.renderTexture ||
1381
+ (this.outputsBound &&
1382
+ (texture === this.outputs[0] ||
1383
+ texture === this.outputs[1] ||
1384
+ texture === this.outputs[2])))) {
1457
1385
  throw new TextureBindingError("feedback");
1458
1386
  }
1459
1387
  const resource = texture && this.textures.get(texture);
@@ -1515,11 +1443,12 @@ export class WebGLContext3D {
1515
1443
  state.samplersSet = true;
1516
1444
  if (state.vertexBank) {
1517
1445
  if (this.vertexConstants.writing || state.vertexVersion !== this.vertexConstants.version) {
1518
- gl.uniform4fv(state.vertexBank, this.vertexConstants.data);
1446
+ gl.uniform4fv(state.vertexBank, this.vertexConstants.data, 0, INDIRECT_BANK * 4);
1519
1447
  state.vertexVersion = this.vertexConstants.version;
1520
1448
  }
1521
1449
  }
1522
- else {
1450
+ // An indirect program's own uniforms are only the registers past its bank.
1451
+ if (state.vertex.length) {
1523
1452
  this.uniforms(state.vertex, this.vertexConstants, state.vertexVersions, state.vertexValues);
1524
1453
  }
1525
1454
  this.uniforms(state.fragment, this.fragmentConstants, state.fragmentVersions, state.fragmentValues);
@@ -1593,77 +1522,11 @@ export class WebGLContext3D {
1593
1522
  }
1594
1523
  }
1595
1524
  }
1596
- resolve() {
1597
- if (!this.resolveFramebuffer) {
1598
- return;
1599
- }
1600
- const gl = this.gl;
1601
- if (this.resolveDirty) {
1602
- this.bindFramebuffer(gl.READ_FRAMEBUFFER, this.framebuffer);
1603
- this.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.resolveFramebuffer);
1604
- if (this.scissor) {
1605
- gl.disable(gl.SCISSOR_TEST);
1606
- }
1607
- gl.blitFramebuffer(0, 0, this.drawnWidth, this.drawnHeight, 0, 0, this.drawnWidth, this.drawnHeight, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1608
- if (this.scissor) {
1609
- gl.enable(gl.SCISSOR_TEST);
1610
- }
1611
- this.resolveDirty = false;
1612
- }
1613
- this.bindFramebuffer(gl.READ_FRAMEBUFFER, this.resolveFramebuffer);
1614
- }
1615
1525
  read(width = this.width, height = this.height) {
1616
- width = Math.max(0, Math.min(this.width, Math.floor(width) || 0));
1617
- height = Math.max(0, Math.min(this.height, Math.floor(height) || 0));
1618
- this.target(true);
1619
- this.resolve();
1620
- const gl = this.gl;
1621
- const scaleX = this.drawnWidth / this.width;
1622
- const scaleY = this.drawnHeight / this.height;
1623
- const drawnWidth = Math.min(this.drawnWidth, Math.ceil(width * scaleX));
1624
- const drawnHeight = Math.min(this.drawnHeight, Math.ceil(height * scaleY));
1625
- if (this.pixels.length < drawnWidth * drawnHeight * 4) {
1626
- this.pixels = new Uint8Array(drawnWidth * drawnHeight * 4);
1627
- }
1628
- if (this.topDown.length < width * height * 4) {
1629
- this.topDown = new Uint8Array(width * height * 4);
1630
- }
1631
- // GL's rows run bottom-up, so the top-left region starts height rows below the top.
1632
- gl.readPixels(0, this.drawnHeight - drawnHeight, drawnWidth, drawnHeight, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
1633
- const row = width * 4;
1634
- if (drawnWidth === width && drawnHeight === height) {
1635
- for (let y = 0; y < height; y++) {
1636
- const from = (height - y - 1) * row;
1637
- this.topDown.set(this.pixels.subarray(from, from + row), y * row);
1638
- }
1639
- return this.topDown;
1640
- }
1641
- // Storage smaller than the back buffer: each pixel is the stored one it falls on.
1642
- const source = new Uint32Array(this.pixels.buffer, 0, drawnWidth * drawnHeight);
1643
- const target = new Uint32Array(this.topDown.buffer, 0, width * height);
1644
- for (let y = 0; y < height; y++) {
1645
- const from = (drawnHeight - 1 - Math.min(drawnHeight - 1, Math.floor(y * scaleY))) * drawnWidth;
1646
- for (let x = 0; x < width; x++) {
1647
- target[y * width + x] = source[from + Math.min(drawnWidth - 1, Math.floor(x * scaleX))];
1648
- }
1649
- }
1650
- return this.topDown;
1526
+ return readBackBuffer(this, width, height);
1651
1527
  }
1652
1528
  present() {
1653
- this.setRenderTarget(null);
1654
- this.target();
1655
- this.resolve();
1656
- const gl = this.gl;
1657
- this.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
1658
- if (this.scissor) {
1659
- gl.disable(gl.SCISSOR_TEST);
1660
- }
1661
- // To the drawing buffer the browser gave, which it may have made smaller than the canvas.
1662
- gl.blitFramebuffer(0, 0, this.drawnWidth, this.drawnHeight, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1663
- if (this.scissor) {
1664
- gl.enable(gl.SCISSOR_TEST);
1665
- }
1666
- gl.flush();
1529
+ presentBackBuffer(this);
1667
1530
  }
1668
1531
  dispose() {
1669
1532
  if (this.disposed) {