@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.
package/src/agal.ts CHANGED
@@ -1,6 +1,7 @@
1
- // Deliberately small AGAL v1 slice for the opt-in Context3D probe.
1
+ // AGAL 1, 2 and 3 to GLSL ES 3.00, with AIR's validation.
2
2
  // Format: https://github.com/adobe/glsl2agal/blob/master/agalassembler/agal.h
3
- import { SamplerBiasError } from "./backend.js";
3
+ // Each version's registers and rules are as AIR 51 enforces them (tests/context3d/AGALProfiles.as).
4
+ import { type Profile, profileLimits, SamplerBiasError } from "./backend.js";
4
5
  import { Context3DError } from "./errors.js";
5
6
 
6
7
  export type ProgramType = "vertex" | "fragment";
@@ -10,7 +11,8 @@ export interface SamplerSource {
10
11
  /** What the sampler reads: 0 RGBA, 1 DXT1 ("compressed"), 2 DXT5 ("compressedAlpha"). */
11
12
  format: number;
12
13
  filter: number;
13
- repeat: boolean;
14
+ /** AGAL's wrap: 0 clamp, 1 repeat, 2 clamp_u_repeat_v, 3 repeat_u_clamp_v. */
15
+ wrap: number;
14
16
  mip: number;
15
17
  cube: boolean;
16
18
  ignore: boolean;
@@ -25,27 +27,107 @@ export interface ShaderSource {
25
27
  constants: number[];
26
28
  indirect: boolean;
27
29
  samplers: SamplerSource[];
30
+ /** Colour outputs a fragment program writes, oc0 up: 1 to 4. */
31
+ outputs: number;
32
+ }
33
+
34
+ /** The registers each AGAL version has, and how long its programs may be. */
35
+ interface Registers {
36
+ attributes: number;
37
+ vertexConstants: number;
38
+ fragmentConstants: number;
39
+ temporaries: number;
40
+ varyings: number;
41
+ samplers: number;
42
+ outputs: number;
43
+ depth: boolean;
44
+ tokens: number;
45
+ }
46
+
47
+ function registers(version: number, profile: Profile): Registers {
48
+ if (version === 1) {
49
+ return {
50
+ attributes: 8,
51
+ vertexConstants: 128,
52
+ fragmentConstants: 28,
53
+ temporaries: profile === "baselineConstrained" ? 7 : 8,
54
+ varyings: 8,
55
+ samplers: 8,
56
+ outputs: 1,
57
+ depth: false,
58
+ tokens: 200,
59
+ };
60
+ }
61
+
62
+ const constrained = profile === "standardConstrained";
63
+ return {
64
+ attributes: version === 2 ? 8 : 16,
65
+ vertexConstants: 250,
66
+ fragmentConstants: version === 2 ? 64 : 200,
67
+ temporaries: 26,
68
+ varyings: constrained ? 8 : 10,
69
+ samplers: constrained ? 8 : 16,
70
+ outputs: constrained ? 1 : 4,
71
+ depth: !constrained,
72
+ tokens: version === 2 ? 1024 : 2048,
73
+ };
28
74
  }
29
75
 
30
76
  const SUPPORTED = new Set([
31
77
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
32
- 0x15, 0x16, 0x17, 0x18, 0x19, 0x27, 0x28, 0x29, 0x2a, 0x2c,
78
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x27, 0x28, 0x29, 0x2a, 0x2c, 0x2d,
79
+ ]);
80
+ /** ddx, ddy and the conditionals: ife, ine, ifg, ifl, els and eif. */
81
+ const VERSION2 = new Set([0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21]);
82
+ const UNARY = new Set([
83
+ 0, 5, 8, 9, 0x0a, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x14, 0x15, 0x16, 0x1a, 0x1b, 0x27,
33
84
  ]);
34
- const UNARY = new Set([0, 5, 8, 9, 0x0a, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x14, 0x15, 0x16, 0x27]);
85
+ const COMPARISONS = ["==", "!=", ">", "<"];
86
+ /**
87
+ * The vertex constants an indirect read reaches: AIR's reach the first 128 in AGAL 2 and 3
88
+ * too, and read 0 past them; the rest only direct reads reach.
89
+ */
90
+ export const INDIRECT_BANK = 128;
91
+ /** The instructions a baselineConstrained fragment program may have. */
92
+ const CONSTRAINED_FRAGMENT = 96;
93
+ /** The deepest AIR nests conditionals. */
94
+ const NESTING = 4;
95
+
96
+ /** A conditional being translated: its predicate, and what was written before and in it. */
97
+ interface Block {
98
+ predicate: string;
99
+ condition: string;
100
+ parent: string;
101
+ otherwise: boolean;
102
+ instructions: number;
103
+ temporaries: Uint8Array;
104
+ varyings: Uint8Array;
105
+ }
35
106
 
36
- export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
107
+ export function translate(
108
+ bytes: Uint8Array,
109
+ stage: ProgramType,
110
+ profile: Profile = "baseline",
111
+ ): ShaderSource {
37
112
  const data = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
38
113
  if (bytes.length < 31 || (bytes.length - 7) % 24 !== 0) {
39
114
  throw new Context3DError("agalLength", "Invalid AGAL program length");
40
115
  }
41
- if ((bytes.length - 7) / 24 > 200) {
116
+
117
+ // AIR takes version 0 as 1.
118
+ const version = data.getUint32(1, true) || 1;
119
+ const limits = registers(Math.min(version, 3), profile);
120
+ if ((bytes.length - 7) / 24 > limits.tokens) {
42
121
  throw new Context3DError("agalTokens", "Too many AGAL instructions");
43
122
  }
44
123
  if (bytes[0] !== 0xa0) {
45
124
  throw new Context3DError("agalMagic", "Invalid AGAL magic byte");
46
125
  }
47
- if (data.getUint32(1, true) !== 1) {
48
- throw new Context3DError("agalVersion", "Context3D experiment requires an AGAL v1 program");
126
+ if (version > profileLimits(profile).agal) {
127
+ throw new Context3DError(
128
+ "agalProfileVersion",
129
+ `AGAL version ${version} is not valid in the ${profile} profile`,
130
+ );
49
131
  }
50
132
  if (bytes[5] !== 0xa1) {
51
133
  throw new Context3DError("agalStageMarker", "Invalid AGAL stage marker");
@@ -73,6 +155,7 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
73
155
  }
74
156
  }
75
157
 
158
+ const bank = INDIRECT_BANK;
76
159
  const names = new Map<string, string>();
77
160
  const attributes = new Set<number>();
78
161
  const constants = new Set<number>();
@@ -80,8 +163,11 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
80
163
  const lines: string[] = [];
81
164
  const components = "xyzw";
82
165
  let output = 0;
83
- const temporaries = new Uint8Array(8);
84
- const varyings = new Uint8Array(8);
166
+ let outputs = 0;
167
+ let temporaries: Uint8Array = new Uint8Array(limits.temporaries);
168
+ let varyings: Uint8Array = new Uint8Array(limits.varyings);
169
+ const blocks: Block[] = [];
170
+ let hoisted = 0;
85
171
  const readTemporary = (index: number, mask: number): void => {
86
172
  if (!temporaries[index]) {
87
173
  throw new Context3DError("agalTemporaryRead", "AGAL temporary read before writing");
@@ -93,12 +179,20 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
93
179
  );
94
180
  }
95
181
  };
182
+ const outputInBlock = (): void => {
183
+ if (blocks.length) {
184
+ throw new Context3DError(
185
+ "agalConditionOutput",
186
+ "AGAL outputs cannot be written in conditionals",
187
+ );
188
+ }
189
+ };
96
190
  const register = (type: number, index: number, write: boolean): string => {
97
191
  let name: string;
98
192
  let declaration: string;
99
193
  switch (type) {
100
194
  case 0:
101
- if (write || stage !== "vertex" || index >= 8) {
195
+ if (write || stage !== "vertex" || index >= limits.attributes) {
102
196
  const reason = write
103
197
  ? "agalAttributeWrite"
104
198
  : stage !== "vertex"
@@ -111,15 +205,18 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
111
205
  attributes.add(index);
112
206
  break;
113
207
  case 1:
114
- if (write || index >= (stage === "vertex" ? 128 : 28)) {
208
+ if (
209
+ write ||
210
+ index >= (stage === "vertex" ? limits.vertexConstants : limits.fragmentConstants)
211
+ ) {
115
212
  throw new Context3DError(
116
213
  write ? "agalConstantWrite" : "agalConstantIndex",
117
214
  "Invalid AGAL constant",
118
215
  );
119
216
  }
120
217
 
121
- if (indirect && stage === "vertex") {
122
- names.set("vc", "uniform vec4 vc[128];");
218
+ if (indirect && stage === "vertex" && index < bank) {
219
+ names.set("vc", `uniform vec4 vc[${bank}];`);
123
220
  return `vc[${index}]`;
124
221
  }
125
222
 
@@ -128,30 +225,67 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
128
225
  constants.add(index);
129
226
  break;
130
227
  case 2:
131
- if (index >= 8) {
228
+ if (index >= limits.temporaries) {
132
229
  throw new Context3DError("agalTemporaryIndex", "Invalid AGAL temporary");
133
230
  }
134
231
  name = `t${index}`;
135
232
  declaration = `vec4 ${name};`;
136
233
  break;
137
234
  case 3:
138
- if (!write || index !== 0) {
235
+ if (!write || index >= (stage === "vertex" ? 1 : limits.outputs)) {
139
236
  throw new Context3DError(
140
237
  write ? "agalOutputIndex" : "agalOutputRead",
141
238
  "Invalid AGAL output",
142
239
  );
143
240
  }
144
- return "result";
241
+ outputInBlock();
242
+ if (index === 0) {
243
+ return "result";
244
+ }
245
+
246
+ name = `result${index}`;
247
+ declaration = `vec4 ${name} = vec4(0.0);`;
248
+ break;
145
249
  case 4:
146
- if (index >= 8 || write !== (stage === "vertex")) {
250
+ if (index >= limits.varyings || write !== (stage === "vertex")) {
147
251
  throw new Context3DError(
148
- index >= 8 ? "agalVaryingIndex" : write ? "agalVaryingWrite" : "agalVaryingRead",
252
+ index >= limits.varyings
253
+ ? "agalVaryingIndex"
254
+ : write
255
+ ? "agalVaryingWrite"
256
+ : "agalVaryingRead",
149
257
  "Invalid AGAL varying",
150
258
  );
151
259
  }
152
260
  name = `v${index}`;
153
261
  declaration = `${stage === "vertex" ? "out" : "in"} vec4 ${name};`;
154
262
  break;
263
+ case 6:
264
+ if (stage === "vertex") {
265
+ throw new Context3DError("agalDepthStage", "AGAL depth output in a vertex program");
266
+ }
267
+ if (!write) {
268
+ throw new Context3DError("agalRegister", "Unsupported AGAL register");
269
+ }
270
+ if (!limits.depth || index !== 0) {
271
+ throw new Context3DError("agalDepthIndex", "Invalid AGAL depth output");
272
+ }
273
+ outputInBlock();
274
+ name = "depth";
275
+ declaration = "vec4 depth = vec4(0.0);";
276
+ break;
277
+ case 7:
278
+ if (stage !== "vertex") {
279
+ throw new Context3DError("agalInstanceStage", "AGAL instance id in a fragment program");
280
+ }
281
+ if (write || index !== 0) {
282
+ throw new Context3DError("agalRegister", "Unsupported AGAL register");
283
+ }
284
+ // AIR reads the instance id from vertex stream 15, which a draw must bind (#3611).
285
+ name = "va15";
286
+ declaration = "layout(location = 15) in vec4 va15;";
287
+ attributes.add(15);
288
+ break;
155
289
  default:
156
290
  throw new Context3DError("agalRegister", "Unsupported AGAL register");
157
291
  }
@@ -173,8 +307,7 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
173
307
  high >>> 24 !== 128 ||
174
308
  (high & 255) !== 1 ||
175
309
  type > 2 ||
176
- component > 3 ||
177
- add >= 128
310
+ component > 3
178
311
  ) {
179
312
  throw new Context3DError("agalIndirect", "Unsupported AGAL indirect constant");
180
313
  }
@@ -183,7 +316,7 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
183
316
  if (type === 2) {
184
317
  readTemporary(low & 65535, 1 << component);
185
318
  }
186
- names.set("vc", "uniform vec4 vc[128];");
319
+ names.set("vc", `uniform vec4 vc[${bank}];`);
187
320
  name = `readConstant(${index}.${components[component]}, ${add + row}.0)`;
188
321
  } else {
189
322
  if (high >>> 8 || ((low >>> 16) & 255) !== 0) {
@@ -214,24 +347,119 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
214
347
  return `${name}.${lanes}`;
215
348
  };
216
349
 
350
+ const scalar = (offset: number): boolean => {
351
+ const swizzle = data.getUint32(offset, true) >>> 24;
352
+ return swizzle === (swizzle & 3) * 0x55;
353
+ };
354
+
217
355
  for (let offset = 7; offset < bytes.length; offset += 24) {
218
356
  const opcode = data.getUint32(offset, true);
357
+ if (opcode > 0x2e) {
358
+ throw new Context3DError("agalOpcodeRange", "AGAL opcode out of range");
359
+ }
360
+ // tld, a vertex texture read AIR names but does not do: its sampler is refused first.
361
+ if (opcode === 0x2e && stage === "vertex") {
362
+ throw new Context3DError("agalSamplerStage", "AGAL sampler in a vertex program");
363
+ }
219
364
  // Unsupported instructions may have no destination or a sampler operand;
220
365
  // do not misreport those layouts as malformed arithmetic registers.
221
- if (!SUPPORTED.has(opcode)) {
366
+ if (!SUPPORTED.has(opcode) && !VERSION2.has(opcode)) {
222
367
  throw new Context3DError("agalOpcode", "Unsupported AGAL opcode");
223
368
  }
369
+ if ((opcode === 0x1a || opcode === 0x1b) && stage !== "fragment") {
370
+ throw new Context3DError("agalFragmentOnly", "AGAL opcode only in fragment programs");
371
+ }
372
+ if (VERSION2.has(opcode) && version < 2) {
373
+ throw new Context3DError("agalOpcodeVersion", "AGAL opcode needs version 2");
374
+ }
224
375
 
225
376
  const dest = data.getUint32(offset + 4, true);
377
+ const block = blocks[blocks.length - 1];
378
+ if (opcode === 0x20 || opcode === 0x21) {
379
+ if (!block) {
380
+ throw new Context3DError(
381
+ opcode === 0x20 ? "agalElse" : "agalEndIf",
382
+ "AGAL else or eif without if",
383
+ );
384
+ }
385
+ if (opcode === 0x20 && block.otherwise) {
386
+ throw new Context3DError("agalElse", "AGAL else chain broken");
387
+ }
388
+ if (!block.instructions) {
389
+ throw new Context3DError("agalEmptyBranch", "Empty AGAL conditional branch");
390
+ }
391
+
392
+ if (opcode === 0x20) {
393
+ // The else branch starts from what was written before the if.
394
+ block.otherwise = true;
395
+ temporaries = block.temporaries.slice();
396
+ varyings = block.varyings.slice();
397
+ block.instructions = 0;
398
+ block.predicate = `p${offset}`;
399
+ lines.push(`bool ${block.predicate} = ${block.parent}!${block.condition};`);
400
+ continue;
401
+ }
402
+
403
+ // After the eif AIR counts what the else branch wrote, or, with none, what was written
404
+ // before the if: an if branch's writes alone are not.
405
+ if (!block.otherwise) {
406
+ temporaries = block.temporaries;
407
+ varyings = block.varyings;
408
+ }
409
+ blocks.pop();
410
+ continue;
411
+ }
412
+
413
+ if (opcode >= 0x1c && opcode <= 0x1f) {
414
+ if (dest !== 0) {
415
+ throw new Context3DError("agalConditionDestination", "AGAL conditional with a destination");
416
+ }
417
+ if (!scalar(offset + 8) || !scalar(offset + 16)) {
418
+ throw new Context3DError("agalScalar", "AGAL conditional operands must be scalar");
419
+ }
420
+ if (blocks.length === NESTING) {
421
+ throw new Context3DError("agalNesting", "AGAL conditionals nested too deep");
422
+ }
423
+
424
+ const a = source(offset + 8);
425
+ const b = source(offset + 16);
426
+ if (block) {
427
+ block.instructions++;
428
+ }
429
+ // Both branches run for every pixel and only their writes are predicated, as
430
+ // Direct3D's predication runs them: derivatives stay defined.
431
+ const parent = block ? `${block.predicate} && ` : "";
432
+ const condition = `c${offset}`;
433
+ const name = `p${offset}`;
434
+ lines.push(`bool ${condition} = ${a}.x ${COMPARISONS[opcode - 0x1c]} ${b}.x;`);
435
+ lines.push(`bool ${name} = ${parent}${condition};`);
436
+ blocks.push({
437
+ predicate: name,
438
+ condition,
439
+ parent,
440
+ otherwise: false,
441
+ instructions: 0,
442
+ temporaries: temporaries.slice(),
443
+ varyings: varyings.slice(),
444
+ });
445
+ continue;
446
+ }
447
+
226
448
  const unary = UNARY.has(opcode);
227
449
  if (unary && (data.getUint32(offset + 16, true) || data.getUint32(offset + 20, true))) {
228
450
  throw new Context3DError("agalUnusedSource", "Nonempty unused AGAL source");
229
451
  }
452
+ if (block) {
453
+ block.instructions++;
454
+ }
230
455
 
231
456
  if (opcode === 0x27) {
232
457
  const operand = data.getUint32(offset + 8, true);
233
458
  const type = data.getUint32(offset + 12, true);
234
459
  const swizzle = operand >>> 24;
460
+ if (type === 1 && stage === "fragment" && dest === 0) {
461
+ throw new Context3DError("agalConstants", "AGAL operands are both constants");
462
+ }
235
463
  if (
236
464
  stage !== "fragment" ||
237
465
  dest !== 0 ||
@@ -241,7 +469,8 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
241
469
  throw new Context3DError("agalDiscard", "Invalid AGAL discard");
242
470
  }
243
471
 
244
- lines.push(`if (${source(offset + 8)}.x < 0.0) { discard; }`);
472
+ const p = block ? `${block.predicate} && ` : "";
473
+ lines.push(`if (${p}${source(offset + 8)}.x < 0.0) { discard; }`);
245
474
  continue;
246
475
  }
247
476
 
@@ -253,8 +482,34 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
253
482
  const targetType = dest >>> 24;
254
483
  const targetIndex = dest & 65535;
255
484
  const target = register(targetType, targetIndex, true);
256
- if (stage === "fragment" && targetType === 3 && mask !== 15) {
257
- throw new Context3DError("agalColorMask", "AGAL color output must write all components");
485
+ if (stage === "fragment" && targetType === 3) {
486
+ if (mask !== 15) {
487
+ throw new Context3DError("agalColorMask", "AGAL color output must write all components");
488
+ }
489
+ if (outputs & (1 << targetIndex)) {
490
+ throw new Context3DError("agalOutputTwice", "AGAL color output written twice");
491
+ }
492
+ }
493
+ if (targetType === 6 && mask !== 1) {
494
+ throw new Context3DError("agalDepthMask", "AGAL depth output must write only x");
495
+ }
496
+ if (opcode === 0x28 && block && data.getUint32(offset + 12, true) !== 4) {
497
+ throw new Context3DError(
498
+ "agalConditionTexture",
499
+ "AGAL texture reads in conditionals take only varyings",
500
+ );
501
+ }
502
+ if ((opcode === 0x1a || opcode === 0x1b) && block) {
503
+ throw new Context3DError("agalConditionDerivative", "AGAL ddx and ddy in a conditional");
504
+ }
505
+ // Two constants make a value AIR wants precomputed; a lone one in kil, ddx and ddy too.
506
+ if (
507
+ opcode !== 0 &&
508
+ opcode !== 0x28 &&
509
+ (data.getUint32(offset + 12, true) & 255) === 1 &&
510
+ (unary ? opcode === 0x1a || opcode === 0x1b : (data.getUint32(offset + 20, true) & 255) === 1)
511
+ ) {
512
+ throw new Context3DError("agalConstants", "AGAL operands are both constants");
258
513
  }
259
514
  const a = source(offset + 8);
260
515
  const b = unary || opcode === 0x28 ? "" : source(offset + 16);
@@ -374,29 +629,48 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
374
629
  expression = `vec${rows}(${dots.join(", ")})`;
375
630
  break;
376
631
  }
632
+ case 0x1a:
633
+ expression = `dFdx(${a})`;
634
+ break;
635
+ case 0x1b:
636
+ // AIR's y grows down its target; GL's up the back buffer, and down a texture, which
637
+ // targetY flips.
638
+ names.set("targetY", "uniform float targetY;");
639
+ expression = `(-targetY * dFdy(${a}))`;
640
+ break;
377
641
  case 0x28: {
378
642
  const low = data.getUint32(offset + 16, true);
379
643
  const high = data.getUint32(offset + 20, true);
380
644
  const index = low & 65535;
381
645
  const bias = data.getInt8(offset + 18) / 8;
382
646
  const format = (high >>> 8) & 15;
647
+ const dimension = (high >>> 12) & 15;
648
+ const special = (high >>> 16) & 15;
649
+ const wrap = (high >>> 20) & 15;
383
650
  const mip = (high >>> 24) & 15;
384
651
  const filter = high >>> 28;
385
- if (
386
- stage !== "fragment" ||
387
- index >= 8 ||
388
- low >>> 24 !== 0 ||
389
- (high & ~0x73141f00) !== 5 ||
390
- format > 2 ||
391
- filter > 5 ||
392
- mip > 2
393
- ) {
652
+ if (stage !== "fragment" || low >>> 24 !== 0 || (high & 255) !== 5) {
394
653
  throw new Context3DError("agalSampler", "Unsupported AGAL sampler encoding");
395
654
  }
655
+ if (index >= limits.samplers) {
656
+ throw new Context3DError("agalSamplerIndex", "AGAL sampler index out of bounds");
657
+ }
658
+ if (dimension > 1) {
659
+ throw new Context3DError("agalSamplerDimension", "Unknown AGAL sampler dimension");
660
+ }
661
+ // Of the special flags only ignoresampler: AIR refuses centroid and single.
662
+ if (special & ~4 || wrap > 3) {
663
+ throw new Context3DError("agalSampler", "Unsupported AGAL sampler encoding");
664
+ }
665
+ if (mip > 2) {
666
+ throw new Context3DError("agalSamplerMip", "Unknown AGAL sampler mipmap mode");
667
+ }
668
+ if (filter > 5) {
669
+ throw new Context3DError("agalSamplerFilter", "Unknown AGAL sampler filter");
670
+ }
396
671
 
397
- const cube = (high & 0x1000) !== 0;
398
- const repeat = (high & 0x00100000) !== 0;
399
- const ignore = (high & 0x00040000) !== 0;
672
+ const cube = dimension === 1;
673
+ const ignore = (special & 4) !== 0;
400
674
  const previous = samplers.get(index);
401
675
  if (previous && previous.bias !== bias) {
402
676
  throw new SamplerBiasError("Conflicting AGAL sampler biases");
@@ -406,7 +680,7 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
406
680
  previous &&
407
681
  (previous.format !== format ||
408
682
  previous.filter !== filter ||
409
- previous.repeat !== repeat ||
683
+ previous.wrap !== wrap ||
410
684
  previous.mip !== mip ||
411
685
  previous.cube !== cube ||
412
686
  previous.ignore !== ignore)
@@ -414,7 +688,7 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
414
688
  throw new Context3DError("agalSampler", "Conflicting AGAL sampler states");
415
689
  }
416
690
 
417
- samplers.set(index, { index, format, filter, repeat, mip, cube, ignore, bias });
691
+ samplers.set(index, { index, format, filter, wrap, mip, cube, ignore, bias });
418
692
  names.set(`fs${index}`, `uniform ${cube ? "samplerCube" : "sampler2D"} fs${index};`);
419
693
  // Bias stays in the shader even when ignoresampler preserves runtime state.
420
694
  expression = `texture(fs${index}, ${a}.${cube ? "xyz" : "xy"}${bias === 0 ? "" : `, ${bias.toFixed(3)}`})`;
@@ -429,6 +703,9 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
429
703
  case 0x2c:
430
704
  expression = `vec4(equal(${a}, ${b}))`;
431
705
  break;
706
+ case 0x2d:
707
+ expression = `vec4(notEqual(${a}, ${b}))`;
708
+ break;
432
709
  default:
433
710
  throw new Context3DError("agalOpcode", "Unsupported AGAL opcode");
434
711
  }
@@ -442,13 +719,28 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
442
719
 
443
720
  // One vector assignment evaluates every source before changing any lane,
444
721
  // including overlapping temporary registers in matrix operations.
445
- lines.push(`${target}.${lanes} = ${expression}.${lanes};`);
722
+ if (!block) {
723
+ lines.push(`${target}.${lanes} = ${expression}.${lanes};`);
724
+ } else if (opcode === 0x28) {
725
+ // A texture read's derivatives are taken outside the branch, where every pixel of a
726
+ // quad computes them.
727
+ const value = `h${hoisted++}`;
728
+ lines.push(`vec4 ${value} = ${expression};`);
729
+ lines.push(`if (${block.predicate}) { ${target}.${lanes} = ${value}.${lanes}; }`);
730
+ } else {
731
+ lines.push(`if (${block.predicate}) { ${target}.${lanes} = ${expression}.${lanes}; }`);
732
+ }
446
733
  switch (targetType) {
447
734
  case 2:
448
735
  temporaries[targetIndex] |= mask;
449
736
  break;
450
737
  case 3:
451
- output |= mask;
738
+ if (stage === "fragment") {
739
+ outputs |= 1 << targetIndex;
740
+ }
741
+ if (targetIndex === 0) {
742
+ output |= mask;
743
+ }
452
744
  break;
453
745
  case 4:
454
746
  varyings[targetIndex] |= mask;
@@ -462,26 +754,57 @@ export function translate(bytes: Uint8Array, stage: ProgramType): ShaderSource {
462
754
  "AGAL program does not write its complete output",
463
755
  );
464
756
  }
757
+ if (outputs & (outputs + 1)) {
758
+ throw new Context3DError("agalOutputSlots", "AGAL color outputs are not consecutive");
759
+ }
760
+ // ps_2_0's 64 arithmetic and 32 texture instructions, which AIR counts once the program
761
+ // is valid; an instruction here is taken as one there.
762
+ if (
763
+ profile === "baselineConstrained" &&
764
+ stage === "fragment" &&
765
+ (bytes.length - 7) / 24 > CONSTRAINED_FRAGMENT
766
+ ) {
767
+ throw new Context3DError("agalNativeLength", "Too many instructions for the native shader");
768
+ }
769
+
770
+ const count = stage === "fragment" ? 32 - Math.clz32(outputs) : 1;
771
+ let results: string;
772
+ let finish: string;
773
+ if (stage === "vertex") {
774
+ results = "uniform float targetY;";
775
+ finish =
776
+ "gl_Position = vec4(result.x, result.y * targetY, result.z * 2.0 - result.w, result.w);";
777
+ } else if (count === 1) {
778
+ results = "out vec4 color;";
779
+ finish = "color = result;";
780
+ } else {
781
+ const outs: string[] = [];
782
+ const sets: string[] = [];
783
+ for (let i = 0; i < count; i++) {
784
+ outs.push(`layout(location = ${i}) out vec4 color${i || ""};`);
785
+ sets.push(`color${i || ""} = result${i || ""};`);
786
+ }
787
+ results = outs.join("\n");
788
+ finish = sets.join("\n ");
789
+ }
790
+ if (names.has("depth")) {
791
+ finish += "\n gl_FragDepth = depth.x;";
792
+ }
465
793
 
466
- const declarations = [...names.values()].join("\n");
467
- const finish =
468
- stage === "vertex"
469
- ? "gl_Position = vec4(result.x, result.y * targetY, result.z * 2.0 - result.w, result.w);"
470
- : "color = result;";
471
794
  return {
472
795
  source: `#version 300 es
473
796
  precision highp float;
474
- ${declarations}
797
+ ${[...names.values()].join("\n")}
475
798
  ${
476
799
  indirect
477
800
  ? `vec4 readConstant(float address, float offset) {
478
801
  float index = roundEven(address) + offset;
479
- if (!(index >= 0.0 && index < 128.0)) { return vec4(0.0); }
802
+ if (!(index >= 0.0 && index < ${bank}.0)) { return vec4(0.0); }
480
803
  return vc[int(index)];
481
804
  }`
482
805
  : ""
483
806
  }
484
- ${stage === "fragment" ? "out vec4 color;" : "uniform float targetY;"}
807
+ ${results}
485
808
  void main() {
486
809
  vec4 result = vec4(0.0);
487
810
  ${lines.join("\n ")}
@@ -492,5 +815,6 @@ void main() {
492
815
  constants: [...constants],
493
816
  indirect,
494
817
  samplers: [...samplers.values()],
818
+ outputs: count,
495
819
  };
496
820
  }