@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,23 +1,34 @@
1
- import type { ProgramType } from "../agal.js";
1
+ import { INDIRECT_BANK, type ProgramType } from "../agal.js";
2
2
  import {
3
3
  type BlendFactor,
4
4
  type Buffer3D,
5
5
  type CompareMode,
6
6
  type Context3D,
7
7
  type CullFace,
8
+ compressedFormat,
9
+ FRAGMENT_CONSTANTS,
10
+ MOST_BACK_BUFFER,
11
+ MOST_CUBE,
12
+ type Profile,
8
13
  type Program3D,
14
+ profileLimits,
9
15
  SAMPLER_FILTERS,
10
16
  SAMPLER_MIPS,
17
+ SAMPLER_SLOTS,
11
18
  SAMPLER_WRAPS,
12
19
  type SamplerFilter,
13
20
  type SamplerMip,
14
21
  type SamplerWrap,
15
22
  ScissorBoundsError,
16
23
  type StencilAction,
24
+ screenPixels,
25
+ TEXEL_BYTES,
17
26
  type Texture3D,
18
27
  TextureBindingError,
19
28
  type TextureFormat,
29
+ VERTEX_CONSTANTS,
20
30
  VERTEX_FORMATS,
31
+ VERTEX_SLOTS,
21
32
  type VertexFormat,
22
33
  VertexOffsetError,
23
34
  } from "../backend.js";
@@ -54,6 +65,7 @@ import {
54
65
  targetFramebuffer,
55
66
  } from "./targets.js";
56
67
  import {
68
+ storageFormat,
57
69
  type TextureState,
58
70
  uploadCompressedLevel,
59
71
  uploadTexture,
@@ -66,8 +78,26 @@ const SAMPLER_FORMATS: Record<TextureFormat, number> = {
66
78
  bgra: 0,
67
79
  compressed: 1,
68
80
  compressedAlpha: 2,
81
+ bgrPacked565: 0,
82
+ bgraPacked4444: 0,
83
+ rgbaHalfFloat: 0,
69
84
  };
70
85
 
86
+ /**
87
+ * A clear colour's channel as Direct3D's 8-bit clear colour holds it, which AIR fills by
88
+ * truncation (0.5 clears to 127): within 0 to 1 and cut to its byte, NaN kept.
89
+ */
90
+ function clearChannel(value: number): number {
91
+ if (!(value > 0)) {
92
+ return value < 0 ? 0 : value;
93
+ }
94
+
95
+ return value >= 1 ? 1 : Math.floor(value * 255 + 1e-3) / 255;
96
+ }
97
+
98
+ /** AGAL's wraps (clamp, repeat, clamp_u_repeat_v, repeat_u_clamp_v) as SAMPLER_WRAPS indices. */
99
+ const WRAPS = [0, 3, 2, 1];
100
+
71
101
  interface Constants {
72
102
  data: Float32Array;
73
103
  bits: Uint32Array;
@@ -94,8 +124,16 @@ export class WebGLContext3D implements Context3D {
94
124
  private readonly drawState: GlState;
95
125
  width = 0;
96
126
  height = 0;
127
+ /**
128
+ * The back buffer's storage: its size, or less where the GPU allows less. The SWF still
129
+ * sees the size it asked for, drawn at this one and stretched to it when presented.
130
+ */
131
+ private drawnWidth = 0;
132
+ private drawnHeight = 0;
133
+ private gpuMost: Int32Array | null = null;
97
134
  disposed = false;
98
135
  samplerFormats = true;
136
+ profile: Profile = "baseline";
99
137
  private readonly buffers = new Map<Buffer3D, BufferState>();
100
138
  private vertexBuffers = 0;
101
139
  private bufferBytes = 0;
@@ -114,7 +152,14 @@ export class WebGLContext3D implements Context3D {
114
152
  private renderTexture: Texture3D | null = null;
115
153
  private renderDepth = false;
116
154
  private renderFace = 0;
117
- private readonly textureSlots: (Texture3D | null)[] = new Array(8).fill(null);
155
+ /** Colour outputs 1 to 3, drawn into with renderTexture, and their cube faces. */
156
+ private readonly outputs: (Texture3D | null)[] = [null, null, null];
157
+ private readonly outputFaces = new Uint8Array(3);
158
+ private outputsChanged = false;
159
+ /** Whether any of outputs is bound, so that a draw alone into its target looks at none. */
160
+ private outputsBound = false;
161
+ private floatTargets: boolean | undefined;
162
+ private readonly textureSlots: (Texture3D | null)[] = new Array(SAMPLER_SLOTS).fill(null);
118
163
  private textureMask = 0;
119
164
  private readonly samplers: (WebGLSampler | null)[] = new Array(96).fill(null);
120
165
  // Two wrap bits, three filter bits and two mip bits fit each slot in one byte.
@@ -122,10 +167,10 @@ export class WebGLContext3D implements Context3D {
122
167
  private anisotropy: EXT_texture_filter_anisotropic | null | undefined;
123
168
  private s3tc: WEBGL_compressed_texture_s3tc | null | undefined;
124
169
  private maxAnisotropy = 1;
125
- private readonly vertices: (VertexBinding | null)[] = new Array(8).fill(null);
170
+ private readonly vertices: (VertexBinding | null)[] = new Array(VERTEX_SLOTS).fill(null);
126
171
  private enabledAttributes = 0;
127
172
  private indexBuffer: WebGLBuffer | null = null;
128
- private readonly texturesApplied = new Uint8Array(8);
173
+ private readonly texturesApplied = new Uint8Array(SAMPLER_SLOTS);
129
174
  private selected: Program3D | null = null;
130
175
  private framebuffer: WebGLFramebuffer | null = null;
131
176
  private texture: WebGLTexture | null = null;
@@ -134,6 +179,8 @@ export class WebGLContext3D implements Context3D {
134
179
  private resolveDirty = false;
135
180
  private depthBuffer: WebGLRenderbuffer | null = null;
136
181
  private antiAlias = 0;
182
+ /** The back buffer's pixels to a CSS pixel and to a scissor rectangle's unit. */
183
+ private scale = 1;
137
184
  private depthWrite = true;
138
185
  private depthCompare: CompareMode = "less";
139
186
  private blendEnabled = false;
@@ -153,8 +200,8 @@ export class WebGLContext3D implements Context3D {
153
200
  private scissorRight = 0;
154
201
  private scissorBottom = 0;
155
202
  private scissorOutside = false;
156
- private readonly vertexConstants = constants(128);
157
- private readonly fragmentConstants = constants(28);
203
+ private readonly vertexConstants = constants(VERTEX_CONSTANTS);
204
+ private readonly fragmentConstants = constants(FRAGMENT_CONSTANTS);
158
205
  private pixels = new Uint8Array(0);
159
206
  private topDown = new Uint8Array(0);
160
207
  private readonly vao: WebGLVertexArrayObject;
@@ -225,7 +272,15 @@ export class WebGLContext3D implements Context3D {
225
272
  }
226
273
  }
227
274
  this.drawState.bindFramebuffer(state ? targetFramebuffer(state) : this.framebuffer);
228
- this.drawState.viewport(0, 0, texture?.width ?? this.width, texture?.height ?? this.height);
275
+ if (this.outputsChanged || state?.outputs) {
276
+ this.attachOutputs(state);
277
+ }
278
+ this.drawState.viewport(
279
+ 0,
280
+ 0,
281
+ texture?.width ?? this.drawnWidth,
282
+ texture?.height ?? this.drawnHeight,
283
+ );
229
284
  }
230
285
 
231
286
  /** Temporary attachments and separate READ/DRAW bindings bypass the hot target cache. */
@@ -234,10 +289,10 @@ export class WebGLContext3D implements Context3D {
234
289
  this.gl.bindFramebuffer(target, framebuffer);
235
290
  }
236
291
 
237
- configure(width: number, height: number, depth = false, antiAlias = 0): void {
292
+ configure(width: number, height: number, depth = false, antiAlias = 0, scale = 1): void {
238
293
  this.alive();
239
- range(32, width - 32, 2048);
240
- range(32, height - 32, 2048);
294
+ range(32, width - 32, MOST_BACK_BUFFER);
295
+ range(32, height - 32, MOST_BACK_BUFFER);
241
296
  if (this.renderTexture) {
242
297
  throw new Context3DError(
243
298
  "operation",
@@ -254,11 +309,20 @@ export class WebGLContext3D implements Context3D {
254
309
  depth === (this.depthBuffer !== null) &&
255
310
  antiAlias === this.antiAlias
256
311
  ) {
312
+ if (scale !== this.scale) {
313
+ this.scale = scale;
314
+ this.showAt();
315
+ this.applyScissor();
316
+ }
317
+
257
318
  return;
258
319
  }
259
320
 
260
321
  const samples = selectSamples(this.gl, antiAlias, depth);
261
322
  const gl = this.gl;
323
+ const most = this.storageLimit();
324
+ const drawnWidth = Math.min(width, most[0]);
325
+ const drawnHeight = Math.min(height, most[1]);
262
326
  const texture = gl.createTexture();
263
327
  const framebuffer = gl.createFramebuffer();
264
328
  const depthBuffer = depth ? gl.createRenderbuffer() : null;
@@ -279,7 +343,17 @@ export class WebGLContext3D implements Context3D {
279
343
  gl.bindTexture(gl.TEXTURE_2D, texture);
280
344
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
281
345
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
282
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
346
+ gl.texImage2D(
347
+ gl.TEXTURE_2D,
348
+ 0,
349
+ gl.RGBA8,
350
+ drawnWidth,
351
+ drawnHeight,
352
+ 0,
353
+ gl.RGBA,
354
+ gl.UNSIGNED_BYTE,
355
+ null,
356
+ );
283
357
  this.bindFramebuffer(gl.FRAMEBUFFER, resolveFramebuffer ?? framebuffer);
284
358
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
285
359
  if (resolveFramebuffer) {
@@ -289,7 +363,13 @@ export class WebGLContext3D implements Context3D {
289
363
 
290
364
  this.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
291
365
  gl.bindRenderbuffer(gl.RENDERBUFFER, colorBuffer);
292
- gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, width, height);
366
+ gl.renderbufferStorageMultisample(
367
+ gl.RENDERBUFFER,
368
+ samples,
369
+ gl.RGBA8,
370
+ drawnWidth,
371
+ drawnHeight,
372
+ );
293
373
  gl.framebufferRenderbuffer(
294
374
  gl.FRAMEBUFFER,
295
375
  gl.COLOR_ATTACHMENT0,
@@ -304,11 +384,11 @@ export class WebGLContext3D implements Context3D {
304
384
  gl.RENDERBUFFER,
305
385
  samples,
306
386
  gl.DEPTH24_STENCIL8,
307
- width,
308
- height,
387
+ drawnWidth,
388
+ drawnHeight,
309
389
  );
310
390
  } else {
311
- gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, width, height);
391
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, drawnWidth, drawnHeight);
312
392
  }
313
393
  gl.framebufferRenderbuffer(
314
394
  gl.FRAMEBUFFER,
@@ -345,14 +425,81 @@ export class WebGLContext3D implements Context3D {
345
425
  this.antiAlias = antiAlias;
346
426
  this.width = width;
347
427
  this.height = height;
348
- gl.canvas.width = width;
349
- gl.canvas.height = height;
350
- this.pixels = new Uint8Array(width * height * 4);
351
- this.topDown = new Uint8Array(this.pixels.length);
428
+ this.drawnWidth = drawnWidth;
429
+ this.drawnHeight = drawnHeight;
430
+ this.scale = scale;
431
+ gl.canvas.width = drawnWidth;
432
+ gl.canvas.height = drawnHeight;
433
+ this.showAt();
434
+ // Readback's storage waits for a read: a back buffer as large as a 4K screen is 33 MB each.
435
+ this.pixels = new Uint8Array(0);
436
+ this.topDown = new Uint8Array(0);
352
437
  this.applyScissor();
353
438
  this.clear(0, 0, 0, 1);
354
439
  }
355
440
 
441
+ /**
442
+ * Shown at the size asked for, in CSS pixels, as an unstyled canvas of that size would be,
443
+ * divided by its scale: a best-resolution back buffer shows its pixels at the screen's.
444
+ */
445
+ private showAt(): void {
446
+ const style = (this.gl.canvas as Partial<HTMLCanvasElement>).style;
447
+ if (!style) {
448
+ return;
449
+ }
450
+
451
+ const plain = this.scale === 1;
452
+ style.width = plain && this.drawnWidth === this.width ? "" : `${this.width / this.scale}px`;
453
+ style.height = plain && this.drawnHeight === this.height ? "" : `${this.height / this.scale}px`;
454
+ }
455
+
456
+ /** The largest back buffer storage the GPU gives: a texture, a renderbuffer and a viewport. */
457
+ private storageLimit(): Int32Array {
458
+ if (!this.gpuMost) {
459
+ const gl = this.gl;
460
+ const viewport = gl.getParameter(gl.MAX_VIEWPORT_DIMS) as Int32Array;
461
+ const side = Math.min(
462
+ gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
463
+ gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
464
+ );
465
+ this.gpuMost = Int32Array.of(Math.min(side, viewport[0]), Math.min(side, viewport[1]));
466
+ }
467
+
468
+ return this.gpuMost;
469
+ }
470
+
471
+ unconfigure(): void {
472
+ this.alive();
473
+ if (this.renderTexture) {
474
+ throw new Context3DError(
475
+ "operation",
476
+ "Context3D experiment: resize while rendering to texture",
477
+ );
478
+ }
479
+
480
+ const gl = this.gl;
481
+ gl.deleteTexture(this.texture);
482
+ gl.deleteFramebuffer(this.framebuffer);
483
+ gl.deleteFramebuffer(this.resolveFramebuffer);
484
+ gl.deleteRenderbuffer(this.depthBuffer);
485
+ gl.deleteRenderbuffer(this.colorBuffer);
486
+ this.drawState.forget();
487
+ this.texture = null;
488
+ this.framebuffer = null;
489
+ this.resolveFramebuffer = null;
490
+ this.colorBuffer = null;
491
+ this.depthBuffer = null;
492
+ this.resolveDirty = false;
493
+ this.antiAlias = 0;
494
+ this.scale = 1;
495
+ this.width = 0;
496
+ this.height = 0;
497
+ this.drawnWidth = 0;
498
+ this.drawnHeight = 0;
499
+ this.pixels = new Uint8Array(0);
500
+ this.topDown = new Uint8Array(0);
501
+ }
502
+
356
503
  clear(
357
504
  red: number,
358
505
  green: number,
@@ -366,7 +513,13 @@ export class WebGLContext3D implements Context3D {
366
513
  const gl = this.gl;
367
514
  let bits = 0;
368
515
  if (mask & 1) {
369
- gl.clearColor(red, green, blue, alpha);
516
+ // A float target too: AIR clears through Direct3D's 8-bit colour.
517
+ gl.clearColor(
518
+ clearChannel(red),
519
+ clearChannel(green),
520
+ clearChannel(blue),
521
+ clearChannel(alpha),
522
+ );
370
523
  bits |= gl.COLOR_BUFFER_BIT;
371
524
  this.changedTarget();
372
525
  }
@@ -565,15 +718,36 @@ export class WebGLContext3D implements Context3D {
565
718
 
566
719
  const width = this.renderTexture?.width ?? this.width;
567
720
  const height = this.renderTexture?.height ?? this.height;
568
- const left = Math.max(0, Math.min(width, this.scissorLeft));
569
- const top = Math.max(0, Math.min(height, this.scissorTop));
570
- const right = Math.max(0, Math.min(width, this.scissorRight));
571
- const bottom = Math.max(0, Math.min(height, this.scissorBottom));
721
+ let left = Math.max(0, Math.min(width, this.scissorLeft));
722
+ let top = Math.max(0, Math.min(height, this.scissorTop));
723
+ let right = Math.max(0, Math.min(width, this.scissorRight));
724
+ let bottom = Math.max(0, Math.min(height, this.scissorBottom));
725
+ // AIR tells a rectangle outside from its own unit, then scales it to a back buffer
726
+ // configured for best resolution, whose pixels are the screen's.
572
727
  this.scissorOutside = right <= left || bottom <= top;
728
+ if (!this.renderTexture && this.scale !== 1) {
729
+ const scale = this.scale;
730
+ left = Math.max(0, Math.min(width, screenPixels(this.scissorLeft, scale)));
731
+ top = Math.max(0, Math.min(height, screenPixels(this.scissorTop, scale)));
732
+ right = Math.max(0, Math.min(width, screenPixels(this.scissorRight, scale)));
733
+ bottom = Math.max(0, Math.min(height, screenPixels(this.scissorBottom, scale)));
734
+ }
735
+ let drawnHeight = height;
736
+ if (!this.renderTexture && (this.drawnWidth !== width || this.drawnHeight !== height)) {
737
+ // The back buffer's storage is smaller than its size: the rectangle shrinks with it.
738
+ const x = this.drawnWidth / width;
739
+ const y = this.drawnHeight / height;
740
+ left = Math.round(left * x);
741
+ right = Math.round(right * x);
742
+ top = Math.round(top * y);
743
+ bottom = Math.round(bottom * y);
744
+ drawnHeight = this.drawnHeight;
745
+ }
746
+
573
747
  gl.enable(gl.SCISSOR_TEST);
574
748
  gl.scissor(
575
749
  left,
576
- this.renderTexture ? top : height - bottom,
750
+ this.renderTexture ? top : drawnHeight - bottom,
577
751
  Math.max(0, right - left),
578
752
  Math.max(0, bottom - top),
579
753
  );
@@ -630,7 +804,7 @@ export class WebGLContext3D implements Context3D {
630
804
  }
631
805
  }
632
806
 
633
- createBuffer(count: number, stride: number): Buffer3D {
807
+ createBuffer(count: number, stride: number, dynamic = false): Buffer3D {
634
808
  this.alive();
635
809
  range(1, count - 1, stride === 0 ? 524287 : 65535);
636
810
  range(0, stride, 64);
@@ -661,7 +835,7 @@ export class WebGLContext3D implements Context3D {
661
835
  }
662
836
  gl.bindVertexArray(this.vao);
663
837
  gl.bindBuffer(target, handle);
664
- gl.bufferData(target, bytes, gl.STATIC_DRAW);
838
+ gl.bufferData(target, bytes, dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
665
839
  const state: BufferState = {
666
840
  handle,
667
841
  indices: stride === 0 ? new Uint16Array(count) : null,
@@ -762,6 +936,7 @@ export class WebGLContext3D implements Context3D {
762
936
  attributeMask: 0,
763
937
  vertex: [],
764
938
  vertexBank: null,
939
+ outputs: 1,
765
940
  fragment: [],
766
941
  samplers: [],
767
942
  samplerMask: 0,
@@ -773,11 +948,17 @@ export class WebGLContext3D implements Context3D {
773
948
  vertexVersion: -1,
774
949
  };
775
950
  const program: Program3D = {
951
+ get outputs() {
952
+ return state.outputs;
953
+ },
954
+ get attributes() {
955
+ return state.attributeMask;
956
+ },
776
957
  upload: (vertex, fragment) => {
777
958
  this.alive();
778
959
  this.program(program);
779
960
  this.drawState.forget();
780
- uploadProgram(this.gl, state, vertex, fragment);
961
+ uploadProgram(this.gl, state, vertex, fragment, this.profile);
781
962
 
782
963
  if (this.selected === program) {
783
964
  this.setProgram(program);
@@ -834,12 +1015,13 @@ export class WebGLContext3D implements Context3D {
834
1015
  height: number,
835
1016
  cube = false,
836
1017
  format: TextureFormat = "bgra",
1018
+ streaming = 0,
837
1019
  ): Texture3D {
838
- return this.allocateTexture(width, height, cube, false, format);
1020
+ return this.allocateTexture(width, height, cube, false, format, streaming);
839
1021
  }
840
1022
 
841
- createRectangleTexture(width: number, height: number): Texture3D {
842
- return this.allocateTexture(width, height, false, true, "bgra");
1023
+ createRectangleTexture(width: number, height: number, format: TextureFormat = "bgra"): Texture3D {
1024
+ return this.allocateTexture(width, height, false, true, format);
843
1025
  }
844
1026
 
845
1027
  /** GL's internal format for a compressed texture's S3TC, or 0 where the context takes none. */
@@ -847,7 +1029,7 @@ export class WebGLContext3D implements Context3D {
847
1029
  if (this.s3tc === undefined) {
848
1030
  this.s3tc = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
849
1031
  }
850
- if (!this.s3tc || format === "bgra") {
1032
+ if (!this.s3tc || !compressedFormat(format)) {
851
1033
  return 0;
852
1034
  }
853
1035
 
@@ -873,7 +1055,9 @@ export class WebGLContext3D implements Context3D {
873
1055
  for (let level = 0; level < levels; level++) {
874
1056
  const w = Math.max(1, width >> level);
875
1057
  const h = Math.max(1, height >> level);
876
- size += format === "bgra" ? w * h * 4 : blockBytes(w, h, format === "compressedAlpha");
1058
+ size += compressedFormat(format)
1059
+ ? blockBytes(w, h, format === "compressedAlpha")
1060
+ : w * h * (TEXEL_BYTES[format] ?? 4);
877
1061
  }
878
1062
 
879
1063
  return size * faces;
@@ -885,34 +1069,38 @@ export class WebGLContext3D implements Context3D {
885
1069
  cube: boolean,
886
1070
  rectangle: boolean,
887
1071
  format: TextureFormat,
1072
+ streaming = 0,
888
1073
  ): Texture3D {
889
1074
  this.alive();
890
- range(1, width - 1, 2048);
891
- range(1, height - 1, 2048);
1075
+ const limits = profileLimits(this.profile);
1076
+ const most = rectangle ? limits.rectangleSize : limits.textureSize;
1077
+ range(1, width - 1, most);
1078
+ range(1, height - 1, most);
892
1079
  if (cube && width !== height) {
893
1080
  throw new Context3DError("textureShape", "Context3D cube faces must be square");
894
1081
  }
895
- if (cube && width > 1024) {
1082
+ if (cube && width > MOST_CUBE) {
896
1083
  throw new Context3DError("textureSize", "Context3D cube faces must be at most 1024");
897
1084
  }
898
1085
 
899
1086
  const faces = cube ? 6 : 1;
900
1087
  const levels = rectangle ? 1 : 1 + Math.floor(Math.log2(Math.max(width, height)));
1088
+ range(streaming, 1, levels);
901
1089
  if (!rectangle && ((width & (width - 1)) !== 0 || (height & (height - 1)) !== 0)) {
902
1090
  throw new Context3DError(
903
1091
  "texturePowerOfTwo",
904
1092
  "Context3D texture size must be a power of two",
905
1093
  );
906
1094
  }
907
- if (format !== "bgra" && rectangle) {
1095
+ if (compressedFormat(format) && rectangle) {
908
1096
  throw new Context3DError("unsupported", "Context3D rectangle textures are not compressed");
909
1097
  }
910
- if (format !== "bgra" && (width < 4 || height < 4)) {
1098
+ if (compressedFormat(format) && (width < 4 || height < 4)) {
911
1099
  throw new Context3DError("compressedSize", "Context3D compressed textures are at least 4x4");
912
1100
  }
913
1101
 
914
1102
  const gl = this.gl;
915
- const storage = this.s3tcFormat(format) || gl.RGBA8;
1103
+ const storage = this.s3tcFormat(format) || storageFormat(gl, format);
916
1104
  const size = this.chainBytes(width, height, levels, faces, format);
917
1105
  const kind = cube ? 2 : rectangle ? 1 : 0;
918
1106
  const countLimit = cube
@@ -928,7 +1116,8 @@ export class WebGLContext3D implements Context3D {
928
1116
  // AIR reserves half the base size for mips, independently of the exact
929
1117
  // chain storage, and charges DXT1 an eighth of BGRA and DXT5 a quarter.
930
1118
  const quotaBytes =
931
- (width * height * faces * (rectangle ? 4 : 6)) /
1119
+ (width * height * faces * (rectangle ? 4 : 6) * (TEXEL_BYTES[format] ?? 4)) /
1120
+ 4 /
932
1121
  (format === "compressed" ? 8 : format === "compressedAlpha" ? 4 : 1);
933
1122
  if (
934
1123
  this.textures.size >= this.limits.maxTextures ||
@@ -963,6 +1152,8 @@ export class WebGLContext3D implements Context3D {
963
1152
  multisample: null,
964
1153
  samples: 0,
965
1154
  antiAlias: 0,
1155
+ outputs: 0,
1156
+ base: 0,
966
1157
  };
967
1158
  const texture: Texture3D = {
968
1159
  width,
@@ -971,6 +1162,7 @@ export class WebGLContext3D implements Context3D {
971
1162
  cube,
972
1163
  rectangle,
973
1164
  format,
1165
+ streaming,
974
1166
  upload: (pixels, level = 0, face = 0, sourceWidth, sourceHeight) => {
975
1167
  this.alive();
976
1168
  this.textureState(texture);
@@ -1003,10 +1195,10 @@ export class WebGLContext3D implements Context3D {
1003
1195
  uploadCompressed: (blocks, level = 0, face = 0) => {
1004
1196
  this.alive();
1005
1197
  this.textureState(texture);
1006
- if (format === "bgra") {
1198
+ if (!compressedFormat(format)) {
1007
1199
  throw new Context3DError(
1008
1200
  "textureUpload",
1009
- "Context3D experiment: BGRA texture given blocks",
1201
+ "Context3D experiment: uncompressed texture given blocks",
1010
1202
  );
1011
1203
  }
1012
1204
 
@@ -1046,6 +1238,13 @@ export class WebGLContext3D implements Context3D {
1046
1238
  state.mipFramebuffer = null;
1047
1239
  state.depthBuffer = null;
1048
1240
 
1241
+ for (let i = 0; i < this.outputs.length; i++) {
1242
+ if (this.outputs[i] === texture) {
1243
+ this.outputs[i] = null;
1244
+ this.outputsChanged = true;
1245
+ this.outputsBound = this.outputMask() !== 0;
1246
+ }
1247
+ }
1049
1248
  for (let i = 0; i < this.textureSlots.length; i++) {
1050
1249
  if (this.textureSlots[i] === texture) {
1051
1250
  this.texturesApplied[i] = 0;
@@ -1201,6 +1400,7 @@ export class WebGLContext3D implements Context3D {
1201
1400
  depth,
1202
1401
  this.depthWrite,
1203
1402
  this.stencilWrite,
1403
+ state.storage,
1204
1404
  );
1205
1405
  if (same) {
1206
1406
  if (newDepth) {
@@ -1236,12 +1436,13 @@ export class WebGLContext3D implements Context3D {
1236
1436
  const gl = this.gl;
1237
1437
  if (texture) {
1238
1438
  this.textureState(texture);
1239
- if (texture.format !== "bgra") {
1439
+ if (compressedFormat(texture.format)) {
1240
1440
  throw new Context3DError(
1241
1441
  "operation",
1242
1442
  "Context3D experiment: compressed textures are no render targets",
1243
1443
  );
1244
1444
  }
1445
+ this.renderFormat(texture.format);
1245
1446
  }
1246
1447
  const samples = texture ? this.multisamples(antiAlias, depth) : 0;
1247
1448
  this.finishTarget();
@@ -1387,6 +1588,12 @@ export class WebGLContext3D implements Context3D {
1387
1588
  }
1388
1589
 
1389
1590
  private changedTarget(): void {
1591
+ for (let i = 0; this.outputsBound && i < this.outputs.length; i++) {
1592
+ const output = this.outputs[i];
1593
+ if (output) {
1594
+ this.textureState(output).dirty = true;
1595
+ }
1596
+ }
1390
1597
  if (this.renderTexture) {
1391
1598
  const state = this.textureState(this.renderTexture);
1392
1599
  state.dirty = true;
@@ -1403,22 +1610,34 @@ export class WebGLContext3D implements Context3D {
1403
1610
  }
1404
1611
 
1405
1612
  private finishTarget(): void {
1406
- if (!this.renderTexture) {
1407
- return;
1613
+ if (this.renderTexture) {
1614
+ this.finishTexture(this.renderTexture, this.renderFace);
1408
1615
  }
1409
- const texture = this.renderTexture;
1616
+ for (let i = 0; i < this.outputs.length; i++) {
1617
+ const output = this.outputs[i];
1618
+ if (output) {
1619
+ this.finishTexture(output, this.outputFaces[i]);
1620
+ }
1621
+ }
1622
+ }
1623
+
1624
+ /** A texture drawn into made whole again: resolved, and its mips regenerated. */
1625
+ private finishTexture(texture: Texture3D, renderFace: number): void {
1410
1626
  const state = this.textureState(texture);
1411
1627
  if (state.dirty) {
1412
1628
  const gl = this.gl;
1413
1629
  this.drawState.forget();
1414
1630
  this.texturesApplied[0] = 0;
1415
- resolveTarget(this.gl, texture, state, this.renderFace, this.scissor);
1631
+ resolveTarget(this.gl, texture, state, renderFace, this.scissor);
1416
1632
  gl.activeTexture(gl.TEXTURE0);
1417
1633
  if (texture.cube) {
1634
+ // An extra colour output may never have been a target of its own.
1635
+ state.framebuffer ??= gl.createFramebuffer();
1636
+ state.mipFramebuffer ??= gl.createFramebuffer();
1418
1637
  // generateMipmap(CUBE_MAP) would overwrite manually uploaded mips on
1419
1638
  // untouched faces. AIR regenerates only the face just rendered.
1420
1639
  gl.disable(gl.SCISSOR_TEST);
1421
- const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.renderFace;
1640
+ const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + renderFace;
1422
1641
  // The base-size depth attachment cannot accompany smaller mip images.
1423
1642
  this.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
1424
1643
  if (state.depthAttached) {
@@ -1480,14 +1699,99 @@ export class WebGLContext3D implements Context3D {
1480
1699
  gl.bindTexture(gl.TEXTURE_2D, state.handle);
1481
1700
  gl.generateMipmap(gl.TEXTURE_2D);
1482
1701
  }
1483
- state.uploaded[this.renderFace] = (1 << texture.levels) - 1;
1702
+ state.uploaded[renderFace] = (1 << texture.levels) - 1;
1484
1703
  state.dirty = false;
1485
1704
  }
1486
1705
  }
1487
1706
 
1707
+ /** Half floats are drawn into where the GPU's WebGL renders float colour, as every desktop one does. */
1708
+ private renderFormat(format: TextureFormat): void {
1709
+ if (format !== "rgbaHalfFloat") {
1710
+ return;
1711
+ }
1712
+
1713
+ this.floatTargets ??= this.gl.getExtension("EXT_color_buffer_float") !== null;
1714
+ if (!this.floatTargets) {
1715
+ throw new Context3DError("unsupported", "Context3D: this GPU draws into no half floats");
1716
+ }
1717
+ }
1718
+
1719
+ setColorOutput(index: number, texture: Texture3D | null, face = 0): void {
1720
+ this.alive();
1721
+ range(index - 1, 1, this.outputs.length);
1722
+ if (texture) {
1723
+ this.textureState(texture);
1724
+ range(face, 1, texture.cube ? 6 : 1);
1725
+ if (compressedFormat(texture.format)) {
1726
+ throw new Context3DError(
1727
+ "operation",
1728
+ "Context3D experiment: compressed textures are no render targets",
1729
+ );
1730
+ }
1731
+ this.renderFormat(texture.format);
1732
+ }
1733
+ if (this.outputs[index - 1] === texture && this.outputFaces[index - 1] === face) {
1734
+ return;
1735
+ }
1736
+
1737
+ const previous = this.outputs[index - 1];
1738
+ if (previous) {
1739
+ this.finishTexture(previous, this.outputFaces[index - 1]);
1740
+ }
1741
+ this.outputs[index - 1] = texture;
1742
+ this.outputFaces[index - 1] = texture ? face : 0;
1743
+ this.outputsChanged = true;
1744
+ this.outputsBound = this.outputMask() !== 0;
1745
+ }
1746
+
1747
+ /**
1748
+ * The current target's framebuffer with the extra colour outputs attached, or without
1749
+ * them: its draws write every output bound, and a program's unwritten ones are refused
1750
+ * before they draw.
1751
+ */
1752
+ private attachOutputs(state: TextureState | null): void {
1753
+ const want = state ? this.outputMask() : 0;
1754
+ if (!state || (!this.outputsChanged && state.outputs === want)) {
1755
+ return;
1756
+ }
1757
+ if (want && state.samples) {
1758
+ throw new Context3DError("unsupported", "Context3D: antialiased colour outputs");
1759
+ }
1760
+
1761
+ const gl = this.gl;
1762
+ const buffers: number[] = [gl.COLOR_ATTACHMENT0];
1763
+ for (let i = 0; i < this.outputs.length; i++) {
1764
+ const output = this.outputs[i];
1765
+ const handle = output ? this.textureState(output).handle : null;
1766
+ gl.framebufferTexture2D(
1767
+ gl.FRAMEBUFFER,
1768
+ gl.COLOR_ATTACHMENT1 + i,
1769
+ output?.cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.outputFaces[i] : gl.TEXTURE_2D,
1770
+ handle,
1771
+ 0,
1772
+ );
1773
+ buffers.push(output ? gl.COLOR_ATTACHMENT1 + i : gl.NONE);
1774
+ }
1775
+ gl.drawBuffers(buffers);
1776
+ state.outputs = want;
1777
+ this.outputsChanged = false;
1778
+ }
1779
+
1780
+ /** The extra colour outputs bound, a bit each from output 1. */
1781
+ private outputMask(): number {
1782
+ let mask = 0;
1783
+ for (let i = 0; i < this.outputs.length; i++) {
1784
+ if (this.outputs[i]) {
1785
+ mask |= 1 << i;
1786
+ }
1787
+ }
1788
+
1789
+ return mask;
1790
+ }
1791
+
1488
1792
  setTexture(index: number, texture: Texture3D | null): void {
1489
1793
  this.alive();
1490
- range(index, 1, 8);
1794
+ range(index, 1, SAMPLER_SLOTS);
1491
1795
  if (texture && !this.textureOwners.has(texture)) {
1492
1796
  throw new Context3DError("disposed", "Context3D experiment: texture from another context");
1493
1797
  }
@@ -1592,7 +1896,7 @@ export class WebGLContext3D implements Context3D {
1592
1896
  if (!source.ignore) {
1593
1897
  this.setSamplerState(
1594
1898
  source.index,
1595
- (source.repeat ? 3 : 0) | (source.filter << 2) | (source.mip << 5),
1899
+ WRAPS[source.wrap] | (source.filter << 2) | (source.mip << 5),
1596
1900
  );
1597
1901
  }
1598
1902
  }
@@ -1600,7 +1904,7 @@ export class WebGLContext3D implements Context3D {
1600
1904
 
1601
1905
  setVertex(index: number, buffer: Buffer3D | null, offset: number, format: VertexFormat): void {
1602
1906
  this.alive();
1603
- range(index, 1, 8);
1907
+ range(index, 1, VERTEX_SLOTS);
1604
1908
  if (buffer === null) {
1605
1909
  this.vertices[index] = null;
1606
1910
  return;
@@ -1723,13 +2027,14 @@ export class WebGLContext3D implements Context3D {
1723
2027
 
1724
2028
  const gl = this.gl;
1725
2029
  this.drawState.bindVertexArray(this.vao);
1726
- for (let i = 0; i < 8; i++) {
1727
- const bit = 1 << i;
1728
- if (this.enabledAttributes & bit & ~state.attributeMask) {
1729
- gl.disableVertexAttribArray(i);
1730
- this.enabledAttributes &= ~bit;
1731
- }
2030
+ // The streams enabled that the program does not read, highest first.
2031
+ let stale = this.enabledAttributes & ~state.attributeMask;
2032
+ while (stale) {
2033
+ const i = 31 - Math.clz32(stale);
2034
+ gl.disableVertexAttribArray(i);
2035
+ stale &= ~(1 << i);
1732
2036
  }
2037
+ this.enabledAttributes &= state.attributeMask;
1733
2038
 
1734
2039
  for (let i = 0; i < state.attributes.length; i++) {
1735
2040
  const slot = state.attributes[i];
@@ -1775,7 +2080,14 @@ export class WebGLContext3D implements Context3D {
1775
2080
  const binding = state.samplers[i];
1776
2081
  const slot = binding.source.index;
1777
2082
  const texture = this.textureSlots[slot];
1778
- if (texture && texture === this.renderTexture) {
2083
+ if (
2084
+ texture &&
2085
+ (texture === this.renderTexture ||
2086
+ (this.outputsBound &&
2087
+ (texture === this.outputs[0] ||
2088
+ texture === this.outputs[1] ||
2089
+ texture === this.outputs[2])))
2090
+ ) {
1779
2091
  throw new TextureBindingError("feedback");
1780
2092
  }
1781
2093
  const resource = texture && this.textures.get(texture);
@@ -1800,7 +2112,10 @@ export class WebGLContext3D implements Context3D {
1800
2112
  throw new TextureBindingError("unuploaded");
1801
2113
  }
1802
2114
  }
1803
- for (let face = 0; face < resource.uploaded.length; face++) {
2115
+ const base = texture.streaming
2116
+ ? this.streamingBase(texture, resource, this.samplerStates[slot] >>> 5 !== 0)
2117
+ : 0;
2118
+ for (let face = 0; !texture.streaming && face < resource.uploaded.length; face++) {
1804
2119
  if (!texture.cube && !(resource.uploaded[face] & 1)) {
1805
2120
  throw new TextureBindingError("unuploaded");
1806
2121
  }
@@ -1823,6 +2138,13 @@ export class WebGLContext3D implements Context3D {
1823
2138
  gl.bindSampler(slot, this.sampler(this.samplerStates[slot]));
1824
2139
  this.texturesApplied[slot] = 1;
1825
2140
  }
2141
+ if (resource.base !== base) {
2142
+ const target = texture.cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D;
2143
+ gl.activeTexture(gl.TEXTURE0 + slot);
2144
+ gl.bindTexture(target, resource.handle);
2145
+ gl.texParameteri(target, gl.TEXTURE_BASE_LEVEL, base);
2146
+ resource.base = base;
2147
+ }
1826
2148
  if (!state.samplersSet) {
1827
2149
  gl.uniform1i(binding.location, slot);
1828
2150
  }
@@ -1832,10 +2154,12 @@ export class WebGLContext3D implements Context3D {
1832
2154
 
1833
2155
  if (state.vertexBank) {
1834
2156
  if (this.vertexConstants.writing || state.vertexVersion !== this.vertexConstants.version) {
1835
- gl.uniform4fv(state.vertexBank, this.vertexConstants.data);
2157
+ gl.uniform4fv(state.vertexBank, this.vertexConstants.data, 0, INDIRECT_BANK * 4);
1836
2158
  state.vertexVersion = this.vertexConstants.version;
1837
2159
  }
1838
- } else {
2160
+ }
2161
+ // An indirect program's own uniforms are only the registers past its bank.
2162
+ if (state.vertex.length) {
1839
2163
  this.uniforms(state.vertex, this.vertexConstants, state.vertexVersions, state.vertexValues);
1840
2164
  }
1841
2165
  this.uniforms(
@@ -1852,6 +2176,50 @@ export class WebGLContext3D implements Context3D {
1852
2176
  this.changedTarget();
1853
2177
  }
1854
2178
 
2179
+ /**
2180
+ * The level a streaming texture is drawn from, as AIR 51 draws one: with mip filtering, the
2181
+ * finest whose levels down to the smallest are uploaded on every face, which must reach its
2182
+ * streaming level (#3700 before); without, its level 0 alone (#3713), as a texture that
2183
+ * does not stream.
2184
+ */
2185
+ private streamingBase(texture: Texture3D, resource: TextureState, mip: boolean): number {
2186
+ const faces = resource.uploaded.length;
2187
+ let uploaded = 0;
2188
+ for (let face = 0; face < faces; face++) {
2189
+ uploaded |= resource.uploaded[face];
2190
+ }
2191
+ if (!uploaded) {
2192
+ throw new TextureBindingError("unuploaded");
2193
+ }
2194
+ if (!mip) {
2195
+ for (let face = 0; face < faces; face++) {
2196
+ if (!(resource.uploaded[face] & 1)) {
2197
+ throw new TextureBindingError("streaming");
2198
+ }
2199
+ }
2200
+
2201
+ return 0;
2202
+ }
2203
+
2204
+ let base = texture.levels;
2205
+ while (base > 0) {
2206
+ let whole = true;
2207
+ for (let face = 0; face < faces; face++) {
2208
+ whole &&= (resource.uploaded[face] & (1 << (base - 1))) !== 0;
2209
+ }
2210
+ if (!whole) {
2211
+ break;
2212
+ }
2213
+
2214
+ base--;
2215
+ }
2216
+ if (base > texture.streaming) {
2217
+ throw new TextureBindingError("mipmaps");
2218
+ }
2219
+
2220
+ return base;
2221
+ }
2222
+
1855
2223
  private uniforms(
1856
2224
  locations: (WebGLUniformLocation | null)[],
1857
2225
  bank: Constants,
@@ -1899,12 +2267,12 @@ export class WebGLContext3D implements Context3D {
1899
2267
  gl.blitFramebuffer(
1900
2268
  0,
1901
2269
  0,
1902
- this.width,
1903
- this.height,
2270
+ this.drawnWidth,
2271
+ this.drawnHeight,
1904
2272
  0,
1905
2273
  0,
1906
- this.width,
1907
- this.height,
2274
+ this.drawnWidth,
2275
+ this.drawnHeight,
1908
2276
  gl.COLOR_BUFFER_BIT,
1909
2277
  gl.NEAREST,
1910
2278
  );
@@ -1922,12 +2290,46 @@ export class WebGLContext3D implements Context3D {
1922
2290
  this.target(true);
1923
2291
  this.resolve();
1924
2292
  const gl = this.gl;
2293
+ const scaleX = this.drawnWidth / this.width;
2294
+ const scaleY = this.drawnHeight / this.height;
2295
+ const drawnWidth = Math.min(this.drawnWidth, Math.ceil(width * scaleX));
2296
+ const drawnHeight = Math.min(this.drawnHeight, Math.ceil(height * scaleY));
2297
+ if (this.pixels.length < drawnWidth * drawnHeight * 4) {
2298
+ this.pixels = new Uint8Array(drawnWidth * drawnHeight * 4);
2299
+ }
2300
+ if (this.topDown.length < width * height * 4) {
2301
+ this.topDown = new Uint8Array(width * height * 4);
2302
+ }
2303
+
1925
2304
  // GL's rows run bottom-up, so the top-left region starts height rows below the top.
1926
- gl.readPixels(0, this.height - height, width, height, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
2305
+ gl.readPixels(
2306
+ 0,
2307
+ this.drawnHeight - drawnHeight,
2308
+ drawnWidth,
2309
+ drawnHeight,
2310
+ gl.RGBA,
2311
+ gl.UNSIGNED_BYTE,
2312
+ this.pixels,
2313
+ );
1927
2314
  const row = width * 4;
2315
+ if (drawnWidth === width && drawnHeight === height) {
2316
+ for (let y = 0; y < height; y++) {
2317
+ const from = (height - y - 1) * row;
2318
+ this.topDown.set(this.pixels.subarray(from, from + row), y * row);
2319
+ }
2320
+
2321
+ return this.topDown;
2322
+ }
2323
+
2324
+ // Storage smaller than the back buffer: each pixel is the stored one it falls on.
2325
+ const source = new Uint32Array(this.pixels.buffer, 0, drawnWidth * drawnHeight);
2326
+ const target = new Uint32Array(this.topDown.buffer, 0, width * height);
1928
2327
  for (let y = 0; y < height; y++) {
1929
- const from = (height - y - 1) * row;
1930
- this.topDown.set(this.pixels.subarray(from, from + row), y * row);
2328
+ const from =
2329
+ (drawnHeight - 1 - Math.min(drawnHeight - 1, Math.floor(y * scaleY))) * drawnWidth;
2330
+ for (let x = 0; x < width; x++) {
2331
+ target[y * width + x] = source[from + Math.min(drawnWidth - 1, Math.floor(x * scaleX))];
2332
+ }
1931
2333
  }
1932
2334
 
1933
2335
  return this.topDown;
@@ -1942,15 +2344,16 @@ export class WebGLContext3D implements Context3D {
1942
2344
  if (this.scissor) {
1943
2345
  gl.disable(gl.SCISSOR_TEST);
1944
2346
  }
2347
+ // To the drawing buffer the browser gave, which it may have made smaller than the canvas.
1945
2348
  gl.blitFramebuffer(
1946
2349
  0,
1947
2350
  0,
1948
- this.width,
1949
- this.height,
2351
+ this.drawnWidth,
2352
+ this.drawnHeight,
1950
2353
  0,
1951
2354
  0,
1952
- this.width,
1953
- this.height,
2355
+ gl.drawingBufferWidth,
2356
+ gl.drawingBufferHeight,
1954
2357
  gl.COLOR_BUFFER_BIT,
1955
2358
  gl.NEAREST,
1956
2359
  );