@swf2es/context3d 0.1.19 → 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,14 +1,20 @@
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,
8
10
  MOST_BACK_BUFFER,
11
+ MOST_CUBE,
12
+ type Profile,
9
13
  type Program3D,
14
+ profileLimits,
10
15
  SAMPLER_FILTERS,
11
16
  SAMPLER_MIPS,
17
+ SAMPLER_SLOTS,
12
18
  SAMPLER_WRAPS,
13
19
  type SamplerFilter,
14
20
  type SamplerMip,
@@ -16,10 +22,13 @@ import {
16
22
  ScissorBoundsError,
17
23
  type StencilAction,
18
24
  screenPixels,
25
+ TEXEL_BYTES,
19
26
  type Texture3D,
20
27
  TextureBindingError,
21
28
  type TextureFormat,
29
+ VERTEX_CONSTANTS,
22
30
  VERTEX_FORMATS,
31
+ VERTEX_SLOTS,
23
32
  type VertexFormat,
24
33
  VertexOffsetError,
25
34
  } from "../backend.js";
@@ -56,6 +65,7 @@ import {
56
65
  targetFramebuffer,
57
66
  } from "./targets.js";
58
67
  import {
68
+ storageFormat,
59
69
  type TextureState,
60
70
  uploadCompressedLevel,
61
71
  uploadTexture,
@@ -68,8 +78,26 @@ const SAMPLER_FORMATS: Record<TextureFormat, number> = {
68
78
  bgra: 0,
69
79
  compressed: 1,
70
80
  compressedAlpha: 2,
81
+ bgrPacked565: 0,
82
+ bgraPacked4444: 0,
83
+ rgbaHalfFloat: 0,
71
84
  };
72
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
+
73
101
  interface Constants {
74
102
  data: Float32Array;
75
103
  bits: Uint32Array;
@@ -105,6 +133,7 @@ export class WebGLContext3D implements Context3D {
105
133
  private gpuMost: Int32Array | null = null;
106
134
  disposed = false;
107
135
  samplerFormats = true;
136
+ profile: Profile = "baseline";
108
137
  private readonly buffers = new Map<Buffer3D, BufferState>();
109
138
  private vertexBuffers = 0;
110
139
  private bufferBytes = 0;
@@ -123,7 +152,14 @@ export class WebGLContext3D implements Context3D {
123
152
  private renderTexture: Texture3D | null = null;
124
153
  private renderDepth = false;
125
154
  private renderFace = 0;
126
- 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);
127
163
  private textureMask = 0;
128
164
  private readonly samplers: (WebGLSampler | null)[] = new Array(96).fill(null);
129
165
  // Two wrap bits, three filter bits and two mip bits fit each slot in one byte.
@@ -131,10 +167,10 @@ export class WebGLContext3D implements Context3D {
131
167
  private anisotropy: EXT_texture_filter_anisotropic | null | undefined;
132
168
  private s3tc: WEBGL_compressed_texture_s3tc | null | undefined;
133
169
  private maxAnisotropy = 1;
134
- private readonly vertices: (VertexBinding | null)[] = new Array(8).fill(null);
170
+ private readonly vertices: (VertexBinding | null)[] = new Array(VERTEX_SLOTS).fill(null);
135
171
  private enabledAttributes = 0;
136
172
  private indexBuffer: WebGLBuffer | null = null;
137
- private readonly texturesApplied = new Uint8Array(8);
173
+ private readonly texturesApplied = new Uint8Array(SAMPLER_SLOTS);
138
174
  private selected: Program3D | null = null;
139
175
  private framebuffer: WebGLFramebuffer | null = null;
140
176
  private texture: WebGLTexture | null = null;
@@ -164,8 +200,8 @@ export class WebGLContext3D implements Context3D {
164
200
  private scissorRight = 0;
165
201
  private scissorBottom = 0;
166
202
  private scissorOutside = false;
167
- private readonly vertexConstants = constants(128);
168
- private readonly fragmentConstants = constants(28);
203
+ private readonly vertexConstants = constants(VERTEX_CONSTANTS);
204
+ private readonly fragmentConstants = constants(FRAGMENT_CONSTANTS);
169
205
  private pixels = new Uint8Array(0);
170
206
  private topDown = new Uint8Array(0);
171
207
  private readonly vao: WebGLVertexArrayObject;
@@ -236,6 +272,9 @@ export class WebGLContext3D implements Context3D {
236
272
  }
237
273
  }
238
274
  this.drawState.bindFramebuffer(state ? targetFramebuffer(state) : this.framebuffer);
275
+ if (this.outputsChanged || state?.outputs) {
276
+ this.attachOutputs(state);
277
+ }
239
278
  this.drawState.viewport(
240
279
  0,
241
280
  0,
@@ -474,7 +513,13 @@ export class WebGLContext3D implements Context3D {
474
513
  const gl = this.gl;
475
514
  let bits = 0;
476
515
  if (mask & 1) {
477
- 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
+ );
478
523
  bits |= gl.COLOR_BUFFER_BIT;
479
524
  this.changedTarget();
480
525
  }
@@ -891,6 +936,7 @@ export class WebGLContext3D implements Context3D {
891
936
  attributeMask: 0,
892
937
  vertex: [],
893
938
  vertexBank: null,
939
+ outputs: 1,
894
940
  fragment: [],
895
941
  samplers: [],
896
942
  samplerMask: 0,
@@ -902,11 +948,17 @@ export class WebGLContext3D implements Context3D {
902
948
  vertexVersion: -1,
903
949
  };
904
950
  const program: Program3D = {
951
+ get outputs() {
952
+ return state.outputs;
953
+ },
954
+ get attributes() {
955
+ return state.attributeMask;
956
+ },
905
957
  upload: (vertex, fragment) => {
906
958
  this.alive();
907
959
  this.program(program);
908
960
  this.drawState.forget();
909
- uploadProgram(this.gl, state, vertex, fragment);
961
+ uploadProgram(this.gl, state, vertex, fragment, this.profile);
910
962
 
911
963
  if (this.selected === program) {
912
964
  this.setProgram(program);
@@ -968,8 +1020,8 @@ export class WebGLContext3D implements Context3D {
968
1020
  return this.allocateTexture(width, height, cube, false, format, streaming);
969
1021
  }
970
1022
 
971
- createRectangleTexture(width: number, height: number): Texture3D {
972
- 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);
973
1025
  }
974
1026
 
975
1027
  /** GL's internal format for a compressed texture's S3TC, or 0 where the context takes none. */
@@ -977,7 +1029,7 @@ export class WebGLContext3D implements Context3D {
977
1029
  if (this.s3tc === undefined) {
978
1030
  this.s3tc = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
979
1031
  }
980
- if (!this.s3tc || format === "bgra") {
1032
+ if (!this.s3tc || !compressedFormat(format)) {
981
1033
  return 0;
982
1034
  }
983
1035
 
@@ -1003,7 +1055,9 @@ export class WebGLContext3D implements Context3D {
1003
1055
  for (let level = 0; level < levels; level++) {
1004
1056
  const w = Math.max(1, width >> level);
1005
1057
  const h = Math.max(1, height >> level);
1006
- 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);
1007
1061
  }
1008
1062
 
1009
1063
  return size * faces;
@@ -1018,12 +1072,14 @@ export class WebGLContext3D implements Context3D {
1018
1072
  streaming = 0,
1019
1073
  ): Texture3D {
1020
1074
  this.alive();
1021
- range(1, width - 1, 2048);
1022
- 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);
1023
1079
  if (cube && width !== height) {
1024
1080
  throw new Context3DError("textureShape", "Context3D cube faces must be square");
1025
1081
  }
1026
- if (cube && width > 1024) {
1082
+ if (cube && width > MOST_CUBE) {
1027
1083
  throw new Context3DError("textureSize", "Context3D cube faces must be at most 1024");
1028
1084
  }
1029
1085
 
@@ -1036,15 +1092,15 @@ export class WebGLContext3D implements Context3D {
1036
1092
  "Context3D texture size must be a power of two",
1037
1093
  );
1038
1094
  }
1039
- if (format !== "bgra" && rectangle) {
1095
+ if (compressedFormat(format) && rectangle) {
1040
1096
  throw new Context3DError("unsupported", "Context3D rectangle textures are not compressed");
1041
1097
  }
1042
- if (format !== "bgra" && (width < 4 || height < 4)) {
1098
+ if (compressedFormat(format) && (width < 4 || height < 4)) {
1043
1099
  throw new Context3DError("compressedSize", "Context3D compressed textures are at least 4x4");
1044
1100
  }
1045
1101
 
1046
1102
  const gl = this.gl;
1047
- const storage = this.s3tcFormat(format) || gl.RGBA8;
1103
+ const storage = this.s3tcFormat(format) || storageFormat(gl, format);
1048
1104
  const size = this.chainBytes(width, height, levels, faces, format);
1049
1105
  const kind = cube ? 2 : rectangle ? 1 : 0;
1050
1106
  const countLimit = cube
@@ -1060,7 +1116,8 @@ export class WebGLContext3D implements Context3D {
1060
1116
  // AIR reserves half the base size for mips, independently of the exact
1061
1117
  // chain storage, and charges DXT1 an eighth of BGRA and DXT5 a quarter.
1062
1118
  const quotaBytes =
1063
- (width * height * faces * (rectangle ? 4 : 6)) /
1119
+ (width * height * faces * (rectangle ? 4 : 6) * (TEXEL_BYTES[format] ?? 4)) /
1120
+ 4 /
1064
1121
  (format === "compressed" ? 8 : format === "compressedAlpha" ? 4 : 1);
1065
1122
  if (
1066
1123
  this.textures.size >= this.limits.maxTextures ||
@@ -1095,6 +1152,7 @@ export class WebGLContext3D implements Context3D {
1095
1152
  multisample: null,
1096
1153
  samples: 0,
1097
1154
  antiAlias: 0,
1155
+ outputs: 0,
1098
1156
  base: 0,
1099
1157
  };
1100
1158
  const texture: Texture3D = {
@@ -1137,10 +1195,10 @@ export class WebGLContext3D implements Context3D {
1137
1195
  uploadCompressed: (blocks, level = 0, face = 0) => {
1138
1196
  this.alive();
1139
1197
  this.textureState(texture);
1140
- if (format === "bgra") {
1198
+ if (!compressedFormat(format)) {
1141
1199
  throw new Context3DError(
1142
1200
  "textureUpload",
1143
- "Context3D experiment: BGRA texture given blocks",
1201
+ "Context3D experiment: uncompressed texture given blocks",
1144
1202
  );
1145
1203
  }
1146
1204
 
@@ -1180,6 +1238,13 @@ export class WebGLContext3D implements Context3D {
1180
1238
  state.mipFramebuffer = null;
1181
1239
  state.depthBuffer = null;
1182
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
+ }
1183
1248
  for (let i = 0; i < this.textureSlots.length; i++) {
1184
1249
  if (this.textureSlots[i] === texture) {
1185
1250
  this.texturesApplied[i] = 0;
@@ -1335,6 +1400,7 @@ export class WebGLContext3D implements Context3D {
1335
1400
  depth,
1336
1401
  this.depthWrite,
1337
1402
  this.stencilWrite,
1403
+ state.storage,
1338
1404
  );
1339
1405
  if (same) {
1340
1406
  if (newDepth) {
@@ -1370,12 +1436,13 @@ export class WebGLContext3D implements Context3D {
1370
1436
  const gl = this.gl;
1371
1437
  if (texture) {
1372
1438
  this.textureState(texture);
1373
- if (texture.format !== "bgra") {
1439
+ if (compressedFormat(texture.format)) {
1374
1440
  throw new Context3DError(
1375
1441
  "operation",
1376
1442
  "Context3D experiment: compressed textures are no render targets",
1377
1443
  );
1378
1444
  }
1445
+ this.renderFormat(texture.format);
1379
1446
  }
1380
1447
  const samples = texture ? this.multisamples(antiAlias, depth) : 0;
1381
1448
  this.finishTarget();
@@ -1521,6 +1588,12 @@ export class WebGLContext3D implements Context3D {
1521
1588
  }
1522
1589
 
1523
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
+ }
1524
1597
  if (this.renderTexture) {
1525
1598
  const state = this.textureState(this.renderTexture);
1526
1599
  state.dirty = true;
@@ -1537,22 +1610,34 @@ export class WebGLContext3D implements Context3D {
1537
1610
  }
1538
1611
 
1539
1612
  private finishTarget(): void {
1540
- if (!this.renderTexture) {
1541
- return;
1613
+ if (this.renderTexture) {
1614
+ this.finishTexture(this.renderTexture, this.renderFace);
1615
+ }
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
+ }
1542
1621
  }
1543
- const texture = this.renderTexture;
1622
+ }
1623
+
1624
+ /** A texture drawn into made whole again: resolved, and its mips regenerated. */
1625
+ private finishTexture(texture: Texture3D, renderFace: number): void {
1544
1626
  const state = this.textureState(texture);
1545
1627
  if (state.dirty) {
1546
1628
  const gl = this.gl;
1547
1629
  this.drawState.forget();
1548
1630
  this.texturesApplied[0] = 0;
1549
- resolveTarget(this.gl, texture, state, this.renderFace, this.scissor);
1631
+ resolveTarget(this.gl, texture, state, renderFace, this.scissor);
1550
1632
  gl.activeTexture(gl.TEXTURE0);
1551
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();
1552
1637
  // generateMipmap(CUBE_MAP) would overwrite manually uploaded mips on
1553
1638
  // untouched faces. AIR regenerates only the face just rendered.
1554
1639
  gl.disable(gl.SCISSOR_TEST);
1555
- const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.renderFace;
1640
+ const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + renderFace;
1556
1641
  // The base-size depth attachment cannot accompany smaller mip images.
1557
1642
  this.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
1558
1643
  if (state.depthAttached) {
@@ -1614,14 +1699,99 @@ export class WebGLContext3D implements Context3D {
1614
1699
  gl.bindTexture(gl.TEXTURE_2D, state.handle);
1615
1700
  gl.generateMipmap(gl.TEXTURE_2D);
1616
1701
  }
1617
- state.uploaded[this.renderFace] = (1 << texture.levels) - 1;
1702
+ state.uploaded[renderFace] = (1 << texture.levels) - 1;
1618
1703
  state.dirty = false;
1619
1704
  }
1620
1705
  }
1621
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
+
1622
1792
  setTexture(index: number, texture: Texture3D | null): void {
1623
1793
  this.alive();
1624
- range(index, 1, 8);
1794
+ range(index, 1, SAMPLER_SLOTS);
1625
1795
  if (texture && !this.textureOwners.has(texture)) {
1626
1796
  throw new Context3DError("disposed", "Context3D experiment: texture from another context");
1627
1797
  }
@@ -1726,7 +1896,7 @@ export class WebGLContext3D implements Context3D {
1726
1896
  if (!source.ignore) {
1727
1897
  this.setSamplerState(
1728
1898
  source.index,
1729
- (source.repeat ? 3 : 0) | (source.filter << 2) | (source.mip << 5),
1899
+ WRAPS[source.wrap] | (source.filter << 2) | (source.mip << 5),
1730
1900
  );
1731
1901
  }
1732
1902
  }
@@ -1734,7 +1904,7 @@ export class WebGLContext3D implements Context3D {
1734
1904
 
1735
1905
  setVertex(index: number, buffer: Buffer3D | null, offset: number, format: VertexFormat): void {
1736
1906
  this.alive();
1737
- range(index, 1, 8);
1907
+ range(index, 1, VERTEX_SLOTS);
1738
1908
  if (buffer === null) {
1739
1909
  this.vertices[index] = null;
1740
1910
  return;
@@ -1857,13 +2027,14 @@ export class WebGLContext3D implements Context3D {
1857
2027
 
1858
2028
  const gl = this.gl;
1859
2029
  this.drawState.bindVertexArray(this.vao);
1860
- for (let i = 0; i < 8; i++) {
1861
- const bit = 1 << i;
1862
- if (this.enabledAttributes & bit & ~state.attributeMask) {
1863
- gl.disableVertexAttribArray(i);
1864
- this.enabledAttributes &= ~bit;
1865
- }
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);
1866
2036
  }
2037
+ this.enabledAttributes &= state.attributeMask;
1867
2038
 
1868
2039
  for (let i = 0; i < state.attributes.length; i++) {
1869
2040
  const slot = state.attributes[i];
@@ -1909,7 +2080,14 @@ export class WebGLContext3D implements Context3D {
1909
2080
  const binding = state.samplers[i];
1910
2081
  const slot = binding.source.index;
1911
2082
  const texture = this.textureSlots[slot];
1912
- 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
+ ) {
1913
2091
  throw new TextureBindingError("feedback");
1914
2092
  }
1915
2093
  const resource = texture && this.textures.get(texture);
@@ -1976,10 +2154,12 @@ export class WebGLContext3D implements Context3D {
1976
2154
 
1977
2155
  if (state.vertexBank) {
1978
2156
  if (this.vertexConstants.writing || state.vertexVersion !== this.vertexConstants.version) {
1979
- gl.uniform4fv(state.vertexBank, this.vertexConstants.data);
2157
+ gl.uniform4fv(state.vertexBank, this.vertexConstants.data, 0, INDIRECT_BANK * 4);
1980
2158
  state.vertexVersion = this.vertexConstants.version;
1981
2159
  }
1982
- } else {
2160
+ }
2161
+ // An indirect program's own uniforms are only the registers past its bank.
2162
+ if (state.vertex.length) {
1983
2163
  this.uniforms(state.vertex, this.vertexConstants, state.vertexVersions, state.vertexValues);
1984
2164
  }
1985
2165
  this.uniforms(
@@ -1,4 +1,5 @@
1
1
  import { type SamplerSource, translate } from "../agal.js";
2
+ import type { Profile } from "../backend.js";
2
3
  import { linkProgram as link, ShaderError } from "../device/programs.js";
3
4
  import { Context3DError } from "../errors.js";
4
5
 
@@ -10,6 +11,8 @@ export interface ProgramState {
10
11
  attributeMask: number;
11
12
  vertex: (WebGLUniformLocation | null)[];
12
13
  vertexBank: WebGLUniformLocation | null;
14
+ /** The colour outputs its fragment program writes. */
15
+ outputs: number;
13
16
  fragment: (WebGLUniformLocation | null)[];
14
17
  samplers: { source: SamplerSource; location: WebGLUniformLocation | null }[];
15
18
  samplerMask: number;
@@ -32,19 +35,20 @@ export function uploadProgram(
32
35
  state: ProgramState,
33
36
  vertex: Uint8Array,
34
37
  fragment: Uint8Array,
38
+ profile: Profile,
35
39
  ): void {
36
40
  // Translate both before allocating anything on the GPU.
37
- const vs = translate(vertex, "vertex");
38
- const fs = translate(fragment, "fragment");
39
- for (let i = 0; i < 8; i++) {
40
- if (fs.varyings[i] & ~vs.varyings[i]) {
41
+ const vs = translate(vertex, "vertex", profile);
42
+ const fs = translate(fragment, "fragment", profile);
43
+ for (let i = 0; i < fs.varyings.length; i++) {
44
+ if (fs.varyings[i] & ~(vs.varyings[i] ?? 0)) {
41
45
  throw new Context3DError(
42
46
  "agalVaryingMissing",
43
47
  "Fragment program reads an unwritten varying component",
44
48
  );
45
49
  }
46
50
  }
47
- for (let i = 0; i < 8; i++) {
51
+ for (let i = 0; i < vs.varyings.length; i++) {
48
52
  if (vs.varyings[i] && vs.varyings[i] !== 15) {
49
53
  throw new Context3DError(
50
54
  "agalVaryingPartial",
@@ -70,6 +74,7 @@ export function uploadProgram(
70
74
 
71
75
  state.vertex = [];
72
76
  state.vertexBank = vs.indirect ? gl.getUniformLocation(handle, "vc[0]") : null;
77
+ state.outputs = fs.outputs;
73
78
  state.fragment = [];
74
79
  state.samplers = [];
75
80
  state.samplerMask = 0;
@@ -107,6 +107,8 @@ export function allocateMultisample(
107
107
  depth: boolean,
108
108
  depthWrite: boolean,
109
109
  stencilWrite: number,
110
+ /** The texture's internal format, which its samples resolve into. */
111
+ storage: number = gl.RGBA8,
110
112
  ): MultisampleTarget {
111
113
  const same = old?.samples === samples;
112
114
  const newDepth = depth && (!same || !old.depth);
@@ -125,7 +127,7 @@ export function allocateMultisample(
125
127
  if (texture.cube && !same) {
126
128
  gl.activeTexture(gl.TEXTURE0);
127
129
  gl.bindTexture(gl.TEXTURE_2D, resolved);
128
- gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, texture.width, texture.height);
130
+ gl.texStorage2D(gl.TEXTURE_2D, 1, storage, texture.width, texture.height);
129
131
  gl.bindFramebuffer(gl.FRAMEBUFFER, resolve);
130
132
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resolved, 0);
131
133
  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
@@ -148,7 +150,7 @@ export function allocateMultisample(
148
150
  gl.renderbufferStorageMultisample(
149
151
  gl.RENDERBUFFER,
150
152
  samples,
151
- gl.RGBA8,
153
+ storage,
152
154
  texture.width,
153
155
  texture.height,
154
156
  );