@swf2es/context3d 0.1.18 → 0.1.20

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.
@@ -1,4 +1,5 @@
1
- import { SAMPLER_FILTERS, SAMPLER_MIPS, SAMPLER_WRAPS, ScissorBoundsError, TextureBindingError, VERTEX_FORMATS, VertexOffsetError, } from "../backend.js";
1
+ import { INDIRECT_BANK } from "../agal.js";
2
+ import { compressedFormat, FRAGMENT_CONSTANTS, MOST_BACK_BUFFER, 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";
@@ -7,14 +8,29 @@ import { resourceLimits } from "./limits.js";
7
8
  import { createCopyProgram, uploadProgram, } from "./programs.js";
8
9
  import { BLEND_FUNCTIONS, DEPTH_FUNCTIONS, range, STENCIL_FUNCTIONS, StencilState, } from "./state.js";
9
10
  import { allocateMultisample, deleteMultisample, resolveTarget, selectSamples, targetFramebuffer, } from "./targets.js";
10
- import { uploadCompressedLevel, uploadTexture, uploadTextureBytes, } from "./textures.js";
11
+ import { storageFormat, uploadCompressedLevel, uploadTexture, uploadTextureBytes, } from "./textures.js";
11
12
  import { validateIndices, validateVertices } from "./validation.js";
12
13
  /** What AGAL's sampler must name to read each texture format: RGBA, DXT1 or DXT5. */
13
14
  const SAMPLER_FORMATS = {
14
15
  bgra: 0,
15
16
  compressed: 1,
16
17
  compressedAlpha: 2,
18
+ bgrPacked565: 0,
19
+ bgraPacked4444: 0,
20
+ rgbaHalfFloat: 0,
17
21
  };
22
+ /**
23
+ * A clear colour's channel as Direct3D's 8-bit clear colour holds it, which AIR fills by
24
+ * truncation (0.5 clears to 127): within 0 to 1 and cut to its byte, NaN kept.
25
+ */
26
+ function clearChannel(value) {
27
+ if (!(value > 0)) {
28
+ return value < 0 ? 0 : value;
29
+ }
30
+ return value >= 1 ? 1 : Math.floor(value * 255 + 1e-3) / 255;
31
+ }
32
+ /** AGAL's wraps (clamp, repeat, clamp_u_repeat_v, repeat_u_clamp_v) as SAMPLER_WRAPS indices. */
33
+ const WRAPS = [0, 3, 2, 1];
18
34
  function constants(registers) {
19
35
  const data = new Float32Array(registers * 4);
20
36
  return {
@@ -31,8 +47,16 @@ export class WebGLContext3D {
31
47
  drawState;
32
48
  width = 0;
33
49
  height = 0;
50
+ /**
51
+ * The back buffer's storage: its size, or less where the GPU allows less. The SWF still
52
+ * sees the size it asked for, drawn at this one and stretched to it when presented.
53
+ */
54
+ drawnWidth = 0;
55
+ drawnHeight = 0;
56
+ gpuMost = null;
34
57
  disposed = false;
35
58
  samplerFormats = true;
59
+ profile = "baseline";
36
60
  buffers = new Map();
37
61
  vertexBuffers = 0;
38
62
  bufferBytes = 0;
@@ -51,7 +75,14 @@ export class WebGLContext3D {
51
75
  renderTexture = null;
52
76
  renderDepth = false;
53
77
  renderFace = 0;
54
- textureSlots = new Array(8).fill(null);
78
+ /** Colour outputs 1 to 3, drawn into with renderTexture, and their cube faces. */
79
+ outputs = [null, null, null];
80
+ outputFaces = new Uint8Array(3);
81
+ outputsChanged = false;
82
+ /** Whether any of outputs is bound, so that a draw alone into its target looks at none. */
83
+ outputsBound = false;
84
+ floatTargets;
85
+ textureSlots = new Array(SAMPLER_SLOTS).fill(null);
55
86
  textureMask = 0;
56
87
  samplers = new Array(96).fill(null);
57
88
  // Two wrap bits, three filter bits and two mip bits fit each slot in one byte.
@@ -59,10 +90,10 @@ export class WebGLContext3D {
59
90
  anisotropy;
60
91
  s3tc;
61
92
  maxAnisotropy = 1;
62
- vertices = new Array(8).fill(null);
93
+ vertices = new Array(VERTEX_SLOTS).fill(null);
63
94
  enabledAttributes = 0;
64
95
  indexBuffer = null;
65
- texturesApplied = new Uint8Array(8);
96
+ texturesApplied = new Uint8Array(SAMPLER_SLOTS);
66
97
  selected = null;
67
98
  framebuffer = null;
68
99
  texture = null;
@@ -71,6 +102,8 @@ export class WebGLContext3D {
71
102
  resolveDirty = false;
72
103
  depthBuffer = null;
73
104
  antiAlias = 0;
105
+ /** The back buffer's pixels to a CSS pixel and to a scissor rectangle's unit. */
106
+ scale = 1;
74
107
  depthWrite = true;
75
108
  depthCompare = "less";
76
109
  blendEnabled = false;
@@ -90,8 +123,8 @@ export class WebGLContext3D {
90
123
  scissorRight = 0;
91
124
  scissorBottom = 0;
92
125
  scissorOutside = false;
93
- vertexConstants = constants(128);
94
- fragmentConstants = constants(28);
126
+ vertexConstants = constants(VERTEX_CONSTANTS);
127
+ fragmentConstants = constants(FRAGMENT_CONSTANTS);
95
128
  pixels = new Uint8Array(0);
96
129
  topDown = new Uint8Array(0);
97
130
  vao;
@@ -150,17 +183,20 @@ export class WebGLContext3D {
150
183
  }
151
184
  }
152
185
  this.drawState.bindFramebuffer(state ? targetFramebuffer(state) : this.framebuffer);
153
- this.drawState.viewport(0, 0, texture?.width ?? this.width, texture?.height ?? this.height);
186
+ if (this.outputsChanged || state?.outputs) {
187
+ this.attachOutputs(state);
188
+ }
189
+ this.drawState.viewport(0, 0, texture?.width ?? this.drawnWidth, texture?.height ?? this.drawnHeight);
154
190
  }
155
191
  /** Temporary attachments and separate READ/DRAW bindings bypass the hot target cache. */
156
192
  bindFramebuffer(target, framebuffer) {
157
193
  this.drawState.forget();
158
194
  this.gl.bindFramebuffer(target, framebuffer);
159
195
  }
160
- configure(width, height, depth = false, antiAlias = 0) {
196
+ configure(width, height, depth = false, antiAlias = 0, scale = 1) {
161
197
  this.alive();
162
- range(32, width - 32, 2048);
163
- range(32, height - 32, 2048);
198
+ range(32, width - 32, MOST_BACK_BUFFER);
199
+ range(32, height - 32, MOST_BACK_BUFFER);
164
200
  if (this.renderTexture) {
165
201
  throw new Context3DError("operation", "Context3D experiment: resize while rendering to texture");
166
202
  }
@@ -171,10 +207,18 @@ export class WebGLContext3D {
171
207
  height === this.height &&
172
208
  depth === (this.depthBuffer !== null) &&
173
209
  antiAlias === this.antiAlias) {
210
+ if (scale !== this.scale) {
211
+ this.scale = scale;
212
+ this.showAt();
213
+ this.applyScissor();
214
+ }
174
215
  return;
175
216
  }
176
217
  const samples = selectSamples(this.gl, antiAlias, depth);
177
218
  const gl = this.gl;
219
+ const most = this.storageLimit();
220
+ const drawnWidth = Math.min(width, most[0]);
221
+ const drawnHeight = Math.min(height, most[1]);
178
222
  const texture = gl.createTexture();
179
223
  const framebuffer = gl.createFramebuffer();
180
224
  const depthBuffer = depth ? gl.createRenderbuffer() : null;
@@ -192,7 +236,7 @@ export class WebGLContext3D {
192
236
  gl.bindTexture(gl.TEXTURE_2D, texture);
193
237
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
194
238
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
195
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
239
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, drawnWidth, drawnHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
196
240
  this.bindFramebuffer(gl.FRAMEBUFFER, resolveFramebuffer ?? framebuffer);
197
241
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
198
242
  if (resolveFramebuffer) {
@@ -201,16 +245,16 @@ export class WebGLContext3D {
201
245
  }
202
246
  this.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
203
247
  gl.bindRenderbuffer(gl.RENDERBUFFER, colorBuffer);
204
- gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, width, height);
248
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, drawnWidth, drawnHeight);
205
249
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuffer);
206
250
  }
207
251
  if (depthBuffer) {
208
252
  gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
209
253
  if (samples) {
210
- gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.DEPTH24_STENCIL8, width, height);
254
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.DEPTH24_STENCIL8, drawnWidth, drawnHeight);
211
255
  }
212
256
  else {
213
- gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, width, height);
257
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, drawnWidth, drawnHeight);
214
258
  }
215
259
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
216
260
  }
@@ -242,19 +286,75 @@ export class WebGLContext3D {
242
286
  this.antiAlias = antiAlias;
243
287
  this.width = width;
244
288
  this.height = height;
245
- gl.canvas.width = width;
246
- gl.canvas.height = height;
247
- this.pixels = new Uint8Array(width * height * 4);
248
- this.topDown = new Uint8Array(this.pixels.length);
289
+ this.drawnWidth = drawnWidth;
290
+ this.drawnHeight = drawnHeight;
291
+ this.scale = scale;
292
+ gl.canvas.width = drawnWidth;
293
+ gl.canvas.height = drawnHeight;
294
+ this.showAt();
295
+ // Readback's storage waits for a read: a back buffer as large as a 4K screen is 33 MB each.
296
+ this.pixels = new Uint8Array(0);
297
+ this.topDown = new Uint8Array(0);
249
298
  this.applyScissor();
250
299
  this.clear(0, 0, 0, 1);
251
300
  }
301
+ /**
302
+ * Shown at the size asked for, in CSS pixels, as an unstyled canvas of that size would be,
303
+ * divided by its scale: a best-resolution back buffer shows its pixels at the screen's.
304
+ */
305
+ showAt() {
306
+ const style = this.gl.canvas.style;
307
+ if (!style) {
308
+ return;
309
+ }
310
+ const plain = this.scale === 1;
311
+ style.width = plain && this.drawnWidth === this.width ? "" : `${this.width / this.scale}px`;
312
+ style.height = plain && this.drawnHeight === this.height ? "" : `${this.height / this.scale}px`;
313
+ }
314
+ /** The largest back buffer storage the GPU gives: a texture, a renderbuffer and a viewport. */
315
+ storageLimit() {
316
+ if (!this.gpuMost) {
317
+ const gl = this.gl;
318
+ const viewport = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
319
+ const side = Math.min(gl.getParameter(gl.MAX_TEXTURE_SIZE), gl.getParameter(gl.MAX_RENDERBUFFER_SIZE));
320
+ this.gpuMost = Int32Array.of(Math.min(side, viewport[0]), Math.min(side, viewport[1]));
321
+ }
322
+ return this.gpuMost;
323
+ }
324
+ unconfigure() {
325
+ this.alive();
326
+ if (this.renderTexture) {
327
+ throw new Context3DError("operation", "Context3D experiment: resize while rendering to texture");
328
+ }
329
+ const gl = this.gl;
330
+ gl.deleteTexture(this.texture);
331
+ gl.deleteFramebuffer(this.framebuffer);
332
+ gl.deleteFramebuffer(this.resolveFramebuffer);
333
+ gl.deleteRenderbuffer(this.depthBuffer);
334
+ gl.deleteRenderbuffer(this.colorBuffer);
335
+ this.drawState.forget();
336
+ this.texture = null;
337
+ this.framebuffer = null;
338
+ this.resolveFramebuffer = null;
339
+ this.colorBuffer = null;
340
+ this.depthBuffer = null;
341
+ this.resolveDirty = false;
342
+ this.antiAlias = 0;
343
+ this.scale = 1;
344
+ this.width = 0;
345
+ this.height = 0;
346
+ this.drawnWidth = 0;
347
+ this.drawnHeight = 0;
348
+ this.pixels = new Uint8Array(0);
349
+ this.topDown = new Uint8Array(0);
350
+ }
252
351
  clear(red, green, blue, alpha, depth = 1, mask = 0xffffffff, stencil = 0) {
253
352
  this.target(false, (mask & 1) !== 0);
254
353
  const gl = this.gl;
255
354
  let bits = 0;
256
355
  if (mask & 1) {
257
- gl.clearColor(red, green, blue, alpha);
356
+ // A float target too: AIR clears through Direct3D's 8-bit colour.
357
+ gl.clearColor(clearChannel(red), clearChannel(green), clearChannel(blue), clearChannel(alpha));
258
358
  bits |= gl.COLOR_BUFFER_BIT;
259
359
  this.changedTarget();
260
360
  }
@@ -401,13 +501,33 @@ export class WebGLContext3D {
401
501
  }
402
502
  const width = this.renderTexture?.width ?? this.width;
403
503
  const height = this.renderTexture?.height ?? this.height;
404
- const left = Math.max(0, Math.min(width, this.scissorLeft));
405
- const top = Math.max(0, Math.min(height, this.scissorTop));
406
- const right = Math.max(0, Math.min(width, this.scissorRight));
407
- const bottom = Math.max(0, Math.min(height, this.scissorBottom));
504
+ let left = Math.max(0, Math.min(width, this.scissorLeft));
505
+ let top = Math.max(0, Math.min(height, this.scissorTop));
506
+ let right = Math.max(0, Math.min(width, this.scissorRight));
507
+ let bottom = Math.max(0, Math.min(height, this.scissorBottom));
508
+ // AIR tells a rectangle outside from its own unit, then scales it to a back buffer
509
+ // configured for best resolution, whose pixels are the screen's.
408
510
  this.scissorOutside = right <= left || bottom <= top;
511
+ if (!this.renderTexture && this.scale !== 1) {
512
+ const scale = this.scale;
513
+ left = Math.max(0, Math.min(width, screenPixels(this.scissorLeft, scale)));
514
+ top = Math.max(0, Math.min(height, screenPixels(this.scissorTop, scale)));
515
+ right = Math.max(0, Math.min(width, screenPixels(this.scissorRight, scale)));
516
+ bottom = Math.max(0, Math.min(height, screenPixels(this.scissorBottom, scale)));
517
+ }
518
+ let drawnHeight = height;
519
+ if (!this.renderTexture && (this.drawnWidth !== width || this.drawnHeight !== height)) {
520
+ // The back buffer's storage is smaller than its size: the rectangle shrinks with it.
521
+ const x = this.drawnWidth / width;
522
+ const y = this.drawnHeight / height;
523
+ left = Math.round(left * x);
524
+ right = Math.round(right * x);
525
+ top = Math.round(top * y);
526
+ bottom = Math.round(bottom * y);
527
+ drawnHeight = this.drawnHeight;
528
+ }
409
529
  gl.enable(gl.SCISSOR_TEST);
410
- gl.scissor(left, this.renderTexture ? top : height - bottom, Math.max(0, right - left), Math.max(0, bottom - top));
530
+ gl.scissor(left, this.renderTexture ? top : drawnHeight - bottom, Math.max(0, right - left), Math.max(0, bottom - top));
411
531
  }
412
532
  setBlend(source, destination) {
413
533
  this.alive();
@@ -456,7 +576,7 @@ export class WebGLContext3D {
456
576
  break;
457
577
  }
458
578
  }
459
- createBuffer(count, stride) {
579
+ createBuffer(count, stride, dynamic = false) {
460
580
  this.alive();
461
581
  range(1, count - 1, stride === 0 ? 524287 : 65535);
462
582
  range(0, stride, 64);
@@ -482,7 +602,7 @@ export class WebGLContext3D {
482
602
  }
483
603
  gl.bindVertexArray(this.vao);
484
604
  gl.bindBuffer(target, handle);
485
- gl.bufferData(target, bytes, gl.STATIC_DRAW);
605
+ gl.bufferData(target, bytes, dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
486
606
  const state = {
487
607
  handle,
488
608
  indices: stride === 0 ? new Uint16Array(count) : null,
@@ -578,6 +698,7 @@ export class WebGLContext3D {
578
698
  attributeMask: 0,
579
699
  vertex: [],
580
700
  vertexBank: null,
701
+ outputs: 1,
581
702
  fragment: [],
582
703
  samplers: [],
583
704
  samplerMask: 0,
@@ -589,11 +710,17 @@ export class WebGLContext3D {
589
710
  vertexVersion: -1,
590
711
  };
591
712
  const program = {
713
+ get outputs() {
714
+ return state.outputs;
715
+ },
716
+ get attributes() {
717
+ return state.attributeMask;
718
+ },
592
719
  upload: (vertex, fragment) => {
593
720
  this.alive();
594
721
  this.program(program);
595
722
  this.drawState.forget();
596
- uploadProgram(this.gl, state, vertex, fragment);
723
+ uploadProgram(this.gl, state, vertex, fragment, this.profile);
597
724
  if (this.selected === program) {
598
725
  this.setProgram(program);
599
726
  }
@@ -636,18 +763,18 @@ export class WebGLContext3D {
636
763
  }
637
764
  return state;
638
765
  }
639
- createTexture(width, height, cube = false, format = "bgra") {
640
- return this.allocateTexture(width, height, cube, false, format);
766
+ createTexture(width, height, cube = false, format = "bgra", streaming = 0) {
767
+ return this.allocateTexture(width, height, cube, false, format, streaming);
641
768
  }
642
- createRectangleTexture(width, height) {
643
- return this.allocateTexture(width, height, false, true, "bgra");
769
+ createRectangleTexture(width, height, format = "bgra") {
770
+ return this.allocateTexture(width, height, false, true, format);
644
771
  }
645
772
  /** GL's internal format for a compressed texture's S3TC, or 0 where the context takes none. */
646
773
  s3tcFormat(format) {
647
774
  if (this.s3tc === undefined) {
648
775
  this.s3tc = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
649
776
  }
650
- if (!this.s3tc || format === "bgra") {
777
+ if (!this.s3tc || !compressedFormat(format)) {
651
778
  return 0;
652
779
  }
653
780
  // AIR's DXT1 has DXT1's one bit of alpha: a block of three colours has transparent black.
@@ -665,33 +792,38 @@ export class WebGLContext3D {
665
792
  for (let level = 0; level < levels; level++) {
666
793
  const w = Math.max(1, width >> level);
667
794
  const h = Math.max(1, height >> level);
668
- size += format === "bgra" ? w * h * 4 : blockBytes(w, h, format === "compressedAlpha");
795
+ size += compressedFormat(format)
796
+ ? blockBytes(w, h, format === "compressedAlpha")
797
+ : w * h * (TEXEL_BYTES[format] ?? 4);
669
798
  }
670
799
  return size * faces;
671
800
  }
672
- allocateTexture(width, height, cube, rectangle, format) {
801
+ allocateTexture(width, height, cube, rectangle, format, streaming = 0) {
673
802
  this.alive();
674
- range(1, width - 1, 2048);
675
- range(1, height - 1, 2048);
803
+ const limits = profileLimits(this.profile);
804
+ const most = rectangle ? limits.rectangleSize : limits.textureSize;
805
+ range(1, width - 1, most);
806
+ range(1, height - 1, most);
676
807
  if (cube && width !== height) {
677
808
  throw new Context3DError("textureShape", "Context3D cube faces must be square");
678
809
  }
679
- if (cube && width > 1024) {
810
+ if (cube && width > MOST_CUBE) {
680
811
  throw new Context3DError("textureSize", "Context3D cube faces must be at most 1024");
681
812
  }
682
813
  const faces = cube ? 6 : 1;
683
814
  const levels = rectangle ? 1 : 1 + Math.floor(Math.log2(Math.max(width, height)));
815
+ range(streaming, 1, levels);
684
816
  if (!rectangle && ((width & (width - 1)) !== 0 || (height & (height - 1)) !== 0)) {
685
817
  throw new Context3DError("texturePowerOfTwo", "Context3D texture size must be a power of two");
686
818
  }
687
- if (format !== "bgra" && rectangle) {
819
+ if (compressedFormat(format) && rectangle) {
688
820
  throw new Context3DError("unsupported", "Context3D rectangle textures are not compressed");
689
821
  }
690
- if (format !== "bgra" && (width < 4 || height < 4)) {
822
+ if (compressedFormat(format) && (width < 4 || height < 4)) {
691
823
  throw new Context3DError("compressedSize", "Context3D compressed textures are at least 4x4");
692
824
  }
693
825
  const gl = this.gl;
694
- const storage = this.s3tcFormat(format) || gl.RGBA8;
826
+ const storage = this.s3tcFormat(format) || storageFormat(gl, format);
695
827
  const size = this.chainBytes(width, height, levels, faces, format);
696
828
  const kind = cube ? 2 : rectangle ? 1 : 0;
697
829
  const countLimit = cube
@@ -706,7 +838,8 @@ export class WebGLContext3D {
706
838
  : this.limits.max2DTextureBytes;
707
839
  // AIR reserves half the base size for mips, independently of the exact
708
840
  // chain storage, and charges DXT1 an eighth of BGRA and DXT5 a quarter.
709
- const quotaBytes = (width * height * faces * (rectangle ? 4 : 6)) /
841
+ const quotaBytes = (width * height * faces * (rectangle ? 4 : 6) * (TEXEL_BYTES[format] ?? 4)) /
842
+ 4 /
710
843
  (format === "compressed" ? 8 : format === "compressedAlpha" ? 4 : 1);
711
844
  if (this.textures.size >= this.limits.maxTextures ||
712
845
  this.textureCounts[kind] >= countLimit ||
@@ -737,6 +870,8 @@ export class WebGLContext3D {
737
870
  multisample: null,
738
871
  samples: 0,
739
872
  antiAlias: 0,
873
+ outputs: 0,
874
+ base: 0,
740
875
  };
741
876
  const texture = {
742
877
  width,
@@ -745,6 +880,7 @@ export class WebGLContext3D {
745
880
  cube,
746
881
  rectangle,
747
882
  format,
883
+ streaming,
748
884
  upload: (pixels, level = 0, face = 0, sourceWidth, sourceHeight) => {
749
885
  this.alive();
750
886
  this.textureState(texture);
@@ -769,8 +905,8 @@ export class WebGLContext3D {
769
905
  uploadCompressed: (blocks, level = 0, face = 0) => {
770
906
  this.alive();
771
907
  this.textureState(texture);
772
- if (format === "bgra") {
773
- throw new Context3DError("textureUpload", "Context3D experiment: BGRA texture given blocks");
908
+ if (!compressedFormat(format)) {
909
+ throw new Context3DError("textureUpload", "Context3D experiment: uncompressed texture given blocks");
774
910
  }
775
911
  try {
776
912
  uploadCompressedLevel(gl, texture, state, blocks, level, face);
@@ -806,6 +942,13 @@ export class WebGLContext3D {
806
942
  state.framebuffer = null;
807
943
  state.mipFramebuffer = null;
808
944
  state.depthBuffer = null;
945
+ for (let i = 0; i < this.outputs.length; i++) {
946
+ if (this.outputs[i] === texture) {
947
+ this.outputs[i] = null;
948
+ this.outputsChanged = true;
949
+ this.outputsBound = this.outputMask() !== 0;
950
+ }
951
+ }
809
952
  for (let i = 0; i < this.textureSlots.length; i++) {
810
953
  if (this.textureSlots[i] === texture) {
811
954
  this.texturesApplied[i] = 0;
@@ -928,7 +1071,7 @@ export class WebGLContext3D {
928
1071
  this.sampler(0);
929
1072
  this.drawState.forget();
930
1073
  this.texturesApplied[0] = 0;
931
- const target = allocateMultisample(this.gl, texture, old, samples, depth, this.depthWrite, this.stencilWrite);
1074
+ const target = allocateMultisample(this.gl, texture, old, samples, depth, this.depthWrite, this.stencilWrite, state.storage);
932
1075
  if (same) {
933
1076
  if (newDepth) {
934
1077
  this.targetDepthBytes += bytes;
@@ -960,9 +1103,10 @@ export class WebGLContext3D {
960
1103
  const gl = this.gl;
961
1104
  if (texture) {
962
1105
  this.textureState(texture);
963
- if (texture.format !== "bgra") {
1106
+ if (compressedFormat(texture.format)) {
964
1107
  throw new Context3DError("operation", "Context3D experiment: compressed textures are no render targets");
965
1108
  }
1109
+ this.renderFormat(texture.format);
966
1110
  }
967
1111
  const samples = texture ? this.multisamples(antiAlias, depth) : 0;
968
1112
  this.finishTarget();
@@ -1060,6 +1204,12 @@ export class WebGLContext3D {
1060
1204
  this.applyScissor();
1061
1205
  }
1062
1206
  changedTarget() {
1207
+ for (let i = 0; this.outputsBound && i < this.outputs.length; i++) {
1208
+ const output = this.outputs[i];
1209
+ if (output) {
1210
+ this.textureState(output).dirty = true;
1211
+ }
1212
+ }
1063
1213
  if (this.renderTexture) {
1064
1214
  const state = this.textureState(this.renderTexture);
1065
1215
  state.dirty = true;
@@ -1077,22 +1227,33 @@ export class WebGLContext3D {
1077
1227
  }
1078
1228
  }
1079
1229
  finishTarget() {
1080
- if (!this.renderTexture) {
1081
- return;
1230
+ if (this.renderTexture) {
1231
+ this.finishTexture(this.renderTexture, this.renderFace);
1082
1232
  }
1083
- const texture = this.renderTexture;
1233
+ for (let i = 0; i < this.outputs.length; i++) {
1234
+ const output = this.outputs[i];
1235
+ if (output) {
1236
+ this.finishTexture(output, this.outputFaces[i]);
1237
+ }
1238
+ }
1239
+ }
1240
+ /** A texture drawn into made whole again: resolved, and its mips regenerated. */
1241
+ finishTexture(texture, renderFace) {
1084
1242
  const state = this.textureState(texture);
1085
1243
  if (state.dirty) {
1086
1244
  const gl = this.gl;
1087
1245
  this.drawState.forget();
1088
1246
  this.texturesApplied[0] = 0;
1089
- resolveTarget(this.gl, texture, state, this.renderFace, this.scissor);
1247
+ resolveTarget(this.gl, texture, state, renderFace, this.scissor);
1090
1248
  gl.activeTexture(gl.TEXTURE0);
1091
1249
  if (texture.cube) {
1250
+ // An extra colour output may never have been a target of its own.
1251
+ state.framebuffer ??= gl.createFramebuffer();
1252
+ state.mipFramebuffer ??= gl.createFramebuffer();
1092
1253
  // generateMipmap(CUBE_MAP) would overwrite manually uploaded mips on
1093
1254
  // untouched faces. AIR regenerates only the face just rendered.
1094
1255
  gl.disable(gl.SCISSOR_TEST);
1095
- const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.renderFace;
1256
+ const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + renderFace;
1096
1257
  // The base-size depth attachment cannot accompany smaller mip images.
1097
1258
  this.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
1098
1259
  if (state.depthAttached) {
@@ -1116,13 +1277,81 @@ export class WebGLContext3D {
1116
1277
  gl.bindTexture(gl.TEXTURE_2D, state.handle);
1117
1278
  gl.generateMipmap(gl.TEXTURE_2D);
1118
1279
  }
1119
- state.uploaded[this.renderFace] = (1 << texture.levels) - 1;
1280
+ state.uploaded[renderFace] = (1 << texture.levels) - 1;
1120
1281
  state.dirty = false;
1121
1282
  }
1122
1283
  }
1284
+ /** Half floats are drawn into where the GPU's WebGL renders float colour, as every desktop one does. */
1285
+ renderFormat(format) {
1286
+ if (format !== "rgbaHalfFloat") {
1287
+ return;
1288
+ }
1289
+ this.floatTargets ??= this.gl.getExtension("EXT_color_buffer_float") !== null;
1290
+ if (!this.floatTargets) {
1291
+ throw new Context3DError("unsupported", "Context3D: this GPU draws into no half floats");
1292
+ }
1293
+ }
1294
+ setColorOutput(index, texture, face = 0) {
1295
+ this.alive();
1296
+ range(index - 1, 1, this.outputs.length);
1297
+ if (texture) {
1298
+ this.textureState(texture);
1299
+ range(face, 1, texture.cube ? 6 : 1);
1300
+ if (compressedFormat(texture.format)) {
1301
+ throw new Context3DError("operation", "Context3D experiment: compressed textures are no render targets");
1302
+ }
1303
+ this.renderFormat(texture.format);
1304
+ }
1305
+ if (this.outputs[index - 1] === texture && this.outputFaces[index - 1] === face) {
1306
+ return;
1307
+ }
1308
+ const previous = this.outputs[index - 1];
1309
+ if (previous) {
1310
+ this.finishTexture(previous, this.outputFaces[index - 1]);
1311
+ }
1312
+ this.outputs[index - 1] = texture;
1313
+ this.outputFaces[index - 1] = texture ? face : 0;
1314
+ this.outputsChanged = true;
1315
+ this.outputsBound = this.outputMask() !== 0;
1316
+ }
1317
+ /**
1318
+ * The current target's framebuffer with the extra colour outputs attached, or without
1319
+ * them: its draws write every output bound, and a program's unwritten ones are refused
1320
+ * before they draw.
1321
+ */
1322
+ attachOutputs(state) {
1323
+ const want = state ? this.outputMask() : 0;
1324
+ if (!state || (!this.outputsChanged && state.outputs === want)) {
1325
+ return;
1326
+ }
1327
+ if (want && state.samples) {
1328
+ throw new Context3DError("unsupported", "Context3D: antialiased colour outputs");
1329
+ }
1330
+ const gl = this.gl;
1331
+ const buffers = [gl.COLOR_ATTACHMENT0];
1332
+ for (let i = 0; i < this.outputs.length; i++) {
1333
+ const output = this.outputs[i];
1334
+ const handle = output ? this.textureState(output).handle : null;
1335
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1 + i, output?.cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.outputFaces[i] : gl.TEXTURE_2D, handle, 0);
1336
+ buffers.push(output ? gl.COLOR_ATTACHMENT1 + i : gl.NONE);
1337
+ }
1338
+ gl.drawBuffers(buffers);
1339
+ state.outputs = want;
1340
+ this.outputsChanged = false;
1341
+ }
1342
+ /** The extra colour outputs bound, a bit each from output 1. */
1343
+ outputMask() {
1344
+ let mask = 0;
1345
+ for (let i = 0; i < this.outputs.length; i++) {
1346
+ if (this.outputs[i]) {
1347
+ mask |= 1 << i;
1348
+ }
1349
+ }
1350
+ return mask;
1351
+ }
1123
1352
  setTexture(index, texture) {
1124
1353
  this.alive();
1125
- range(index, 1, 8);
1354
+ range(index, 1, SAMPLER_SLOTS);
1126
1355
  if (texture && !this.textureOwners.has(texture)) {
1127
1356
  throw new Context3DError("disposed", "Context3D experiment: texture from another context");
1128
1357
  }
@@ -1207,13 +1436,13 @@ export class WebGLContext3D {
1207
1436
  for (let i = 0; i < state.samplers.length; i++) {
1208
1437
  const source = state.samplers[i].source;
1209
1438
  if (!source.ignore) {
1210
- this.setSamplerState(source.index, (source.repeat ? 3 : 0) | (source.filter << 2) | (source.mip << 5));
1439
+ this.setSamplerState(source.index, WRAPS[source.wrap] | (source.filter << 2) | (source.mip << 5));
1211
1440
  }
1212
1441
  }
1213
1442
  }
1214
1443
  setVertex(index, buffer, offset, format) {
1215
1444
  this.alive();
1216
- range(index, 1, 8);
1445
+ range(index, 1, VERTEX_SLOTS);
1217
1446
  if (buffer === null) {
1218
1447
  this.vertices[index] = null;
1219
1448
  return;
@@ -1319,13 +1548,14 @@ export class WebGLContext3D {
1319
1548
  const maximum = validateIndices(index, first, count);
1320
1549
  const gl = this.gl;
1321
1550
  this.drawState.bindVertexArray(this.vao);
1322
- for (let i = 0; i < 8; i++) {
1323
- const bit = 1 << i;
1324
- if (this.enabledAttributes & bit & ~state.attributeMask) {
1325
- gl.disableVertexAttribArray(i);
1326
- this.enabledAttributes &= ~bit;
1327
- }
1328
- }
1551
+ // The streams enabled that the program does not read, highest first.
1552
+ let stale = this.enabledAttributes & ~state.attributeMask;
1553
+ while (stale) {
1554
+ const i = 31 - Math.clz32(stale);
1555
+ gl.disableVertexAttribArray(i);
1556
+ stale &= ~(1 << i);
1557
+ }
1558
+ this.enabledAttributes &= state.attributeMask;
1329
1559
  for (let i = 0; i < state.attributes.length; i++) {
1330
1560
  const slot = state.attributes[i];
1331
1561
  const binding = this.vertices[slot];
@@ -1358,7 +1588,12 @@ export class WebGLContext3D {
1358
1588
  const binding = state.samplers[i];
1359
1589
  const slot = binding.source.index;
1360
1590
  const texture = this.textureSlots[slot];
1361
- if (texture && texture === this.renderTexture) {
1591
+ if (texture &&
1592
+ (texture === this.renderTexture ||
1593
+ (this.outputsBound &&
1594
+ (texture === this.outputs[0] ||
1595
+ texture === this.outputs[1] ||
1596
+ texture === this.outputs[2])))) {
1362
1597
  throw new TextureBindingError("feedback");
1363
1598
  }
1364
1599
  const resource = texture && this.textures.get(texture);
@@ -1383,7 +1618,10 @@ export class WebGLContext3D {
1383
1618
  throw new TextureBindingError("unuploaded");
1384
1619
  }
1385
1620
  }
1386
- for (let face = 0; face < resource.uploaded.length; face++) {
1621
+ const base = texture.streaming
1622
+ ? this.streamingBase(texture, resource, this.samplerStates[slot] >>> 5 !== 0)
1623
+ : 0;
1624
+ for (let face = 0; !texture.streaming && face < resource.uploaded.length; face++) {
1387
1625
  if (!texture.cube && !(resource.uploaded[face] & 1)) {
1388
1626
  throw new TextureBindingError("unuploaded");
1389
1627
  }
@@ -1403,6 +1641,13 @@ export class WebGLContext3D {
1403
1641
  gl.bindSampler(slot, this.sampler(this.samplerStates[slot]));
1404
1642
  this.texturesApplied[slot] = 1;
1405
1643
  }
1644
+ if (resource.base !== base) {
1645
+ const target = texture.cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D;
1646
+ gl.activeTexture(gl.TEXTURE0 + slot);
1647
+ gl.bindTexture(target, resource.handle);
1648
+ gl.texParameteri(target, gl.TEXTURE_BASE_LEVEL, base);
1649
+ resource.base = base;
1650
+ }
1406
1651
  if (!state.samplersSet) {
1407
1652
  gl.uniform1i(binding.location, slot);
1408
1653
  }
@@ -1410,11 +1655,12 @@ export class WebGLContext3D {
1410
1655
  state.samplersSet = true;
1411
1656
  if (state.vertexBank) {
1412
1657
  if (this.vertexConstants.writing || state.vertexVersion !== this.vertexConstants.version) {
1413
- gl.uniform4fv(state.vertexBank, this.vertexConstants.data);
1658
+ gl.uniform4fv(state.vertexBank, this.vertexConstants.data, 0, INDIRECT_BANK * 4);
1414
1659
  state.vertexVersion = this.vertexConstants.version;
1415
1660
  }
1416
1661
  }
1417
- else {
1662
+ // An indirect program's own uniforms are only the registers past its bank.
1663
+ if (state.vertex.length) {
1418
1664
  this.uniforms(state.vertex, this.vertexConstants, state.vertexVersions, state.vertexValues);
1419
1665
  }
1420
1666
  this.uniforms(state.fragment, this.fragmentConstants, state.fragmentVersions, state.fragmentValues);
@@ -1425,6 +1671,45 @@ export class WebGLContext3D {
1425
1671
  gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, first * 2);
1426
1672
  this.changedTarget();
1427
1673
  }
1674
+ /**
1675
+ * The level a streaming texture is drawn from, as AIR 51 draws one: with mip filtering, the
1676
+ * finest whose levels down to the smallest are uploaded on every face, which must reach its
1677
+ * streaming level (#3700 before); without, its level 0 alone (#3713), as a texture that
1678
+ * does not stream.
1679
+ */
1680
+ streamingBase(texture, resource, mip) {
1681
+ const faces = resource.uploaded.length;
1682
+ let uploaded = 0;
1683
+ for (let face = 0; face < faces; face++) {
1684
+ uploaded |= resource.uploaded[face];
1685
+ }
1686
+ if (!uploaded) {
1687
+ throw new TextureBindingError("unuploaded");
1688
+ }
1689
+ if (!mip) {
1690
+ for (let face = 0; face < faces; face++) {
1691
+ if (!(resource.uploaded[face] & 1)) {
1692
+ throw new TextureBindingError("streaming");
1693
+ }
1694
+ }
1695
+ return 0;
1696
+ }
1697
+ let base = texture.levels;
1698
+ while (base > 0) {
1699
+ let whole = true;
1700
+ for (let face = 0; face < faces; face++) {
1701
+ whole &&= (resource.uploaded[face] & (1 << (base - 1))) !== 0;
1702
+ }
1703
+ if (!whole) {
1704
+ break;
1705
+ }
1706
+ base--;
1707
+ }
1708
+ if (base > texture.streaming) {
1709
+ throw new TextureBindingError("mipmaps");
1710
+ }
1711
+ return base;
1712
+ }
1428
1713
  uniforms(locations, bank, previous, values) {
1429
1714
  for (let i = 0; i < locations.length; i++) {
1430
1715
  if (locations[i] && (bank.writing || previous[i] !== bank.versions[i])) {
@@ -1460,7 +1745,7 @@ export class WebGLContext3D {
1460
1745
  if (this.scissor) {
1461
1746
  gl.disable(gl.SCISSOR_TEST);
1462
1747
  }
1463
- gl.blitFramebuffer(0, 0, this.width, this.height, 0, 0, this.width, this.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1748
+ gl.blitFramebuffer(0, 0, this.drawnWidth, this.drawnHeight, 0, 0, this.drawnWidth, this.drawnHeight, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1464
1749
  if (this.scissor) {
1465
1750
  gl.enable(gl.SCISSOR_TEST);
1466
1751
  }
@@ -1474,12 +1759,34 @@ export class WebGLContext3D {
1474
1759
  this.target(true);
1475
1760
  this.resolve();
1476
1761
  const gl = this.gl;
1762
+ const scaleX = this.drawnWidth / this.width;
1763
+ const scaleY = this.drawnHeight / this.height;
1764
+ const drawnWidth = Math.min(this.drawnWidth, Math.ceil(width * scaleX));
1765
+ const drawnHeight = Math.min(this.drawnHeight, Math.ceil(height * scaleY));
1766
+ if (this.pixels.length < drawnWidth * drawnHeight * 4) {
1767
+ this.pixels = new Uint8Array(drawnWidth * drawnHeight * 4);
1768
+ }
1769
+ if (this.topDown.length < width * height * 4) {
1770
+ this.topDown = new Uint8Array(width * height * 4);
1771
+ }
1477
1772
  // GL's rows run bottom-up, so the top-left region starts height rows below the top.
1478
- gl.readPixels(0, this.height - height, width, height, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
1773
+ gl.readPixels(0, this.drawnHeight - drawnHeight, drawnWidth, drawnHeight, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
1479
1774
  const row = width * 4;
1775
+ if (drawnWidth === width && drawnHeight === height) {
1776
+ for (let y = 0; y < height; y++) {
1777
+ const from = (height - y - 1) * row;
1778
+ this.topDown.set(this.pixels.subarray(from, from + row), y * row);
1779
+ }
1780
+ return this.topDown;
1781
+ }
1782
+ // Storage smaller than the back buffer: each pixel is the stored one it falls on.
1783
+ const source = new Uint32Array(this.pixels.buffer, 0, drawnWidth * drawnHeight);
1784
+ const target = new Uint32Array(this.topDown.buffer, 0, width * height);
1480
1785
  for (let y = 0; y < height; y++) {
1481
- const from = (height - y - 1) * row;
1482
- this.topDown.set(this.pixels.subarray(from, from + row), y * row);
1786
+ const from = (drawnHeight - 1 - Math.min(drawnHeight - 1, Math.floor(y * scaleY))) * drawnWidth;
1787
+ for (let x = 0; x < width; x++) {
1788
+ target[y * width + x] = source[from + Math.min(drawnWidth - 1, Math.floor(x * scaleX))];
1789
+ }
1483
1790
  }
1484
1791
  return this.topDown;
1485
1792
  }
@@ -1492,7 +1799,8 @@ export class WebGLContext3D {
1492
1799
  if (this.scissor) {
1493
1800
  gl.disable(gl.SCISSOR_TEST);
1494
1801
  }
1495
- gl.blitFramebuffer(0, 0, this.width, this.height, 0, 0, this.width, this.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1802
+ // To the drawing buffer the browser gave, which it may have made smaller than the canvas.
1803
+ gl.blitFramebuffer(0, 0, this.drawnWidth, this.drawnHeight, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.COLOR_BUFFER_BIT, gl.NEAREST);
1496
1804
  if (this.scissor) {
1497
1805
  gl.enable(gl.SCISSOR_TEST);
1498
1806
  }