@carbonenginejs/runtime-resource 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/format/CjsFormat.js +84 -83
  2. package/dist/format/CjsFormat.js.map +1 -1
  3. package/dist/format/CjsResourceProbe.js +87 -86
  4. package/dist/format/CjsResourceProbe.js.map +1 -1
  5. package/dist/format/carbonEffect/backendEngineId.js +100 -0
  6. package/dist/format/carbonEffect/backendEngineId.js.map +1 -0
  7. package/dist/format/carbonEffect/carbonEffectBackendBlock.js +23 -23
  8. package/dist/format/carbonEffect/carbonEffectBackendBlock.js.map +1 -1
  9. package/dist/format/index.js +1 -1
  10. package/dist/formats/png/core/helpers.js +27 -8
  11. package/dist/formats/png/core/helpers.js.map +1 -1
  12. package/dist/formats/webgl/core/effectPackage.js +141 -53
  13. package/dist/formats/webgl/core/effectPackage.js.map +1 -1
  14. package/dist/formats/webgl/core/glsl/DxbcGlslEmitter.js +799 -456
  15. package/dist/formats/webgl/core/glsl/DxbcGlslEmitter.js.map +1 -1
  16. package/dist/formats/webgl/core/glslBackendBlock.js +70 -27
  17. package/dist/formats/webgl/core/glslBackendBlock.js.map +1 -1
  18. package/dist/formats/webgl/core/helpers.js +53 -53
  19. package/dist/formats/webgl/core/helpers.js.map +1 -1
  20. package/dist/formats/webgl/core/readGlslEffectContainer.js +82 -67
  21. package/dist/formats/webgl/core/readGlslEffectContainer.js.map +1 -1
  22. package/dist/formats/webgpu/core/carbonWebgpu/CarbonWebgpuContainer.js +10 -1
  23. package/dist/formats/webgpu/core/carbonWebgpu/CarbonWebgpuContainer.js.map +1 -1
  24. package/dist/formats/wem/CjsWemFormat.js +121 -121
  25. package/dist/formats/wem/CjsWemFormat.js.map +1 -1
  26. package/dist/formats/wem/core/resolve.js +8 -8
  27. package/dist/formats/wem/core/resolve.js.map +1 -1
  28. package/dist/resource/shader/reflection/Tr2EffectStageInput.js +102 -60
  29. package/dist/resource/shader/reflection/Tr2EffectStageInput.js.map +1 -1
  30. package/docs/concepts/format-type-resolution.md +79 -0
  31. package/docs/formats/README.md +11 -0
  32. package/docs/formats/wwise.md +25 -0
  33. package/docs/roadmap.md +19 -0
  34. package/package.json +1 -1
@@ -5,20 +5,25 @@ import { DxbcGlslHelperRegistry } from './DxbcGlslHelpers.js';
5
5
 
6
6
  const COMPONENTS = ["x", "y", "z", "w"];
7
7
 
8
- /**
9
- * Map-style compute lowering caps simultaneous UAV writes to this many
10
- * fragment color outputs. WebGL2 only guarantees `MAX_DRAW_BUFFERS >= 4`
11
- * (real implementations commonly expose up to 8); a compute kernel that
12
- * writes more UAVs than fit in one draw call's attachment set (e.g. a
13
- * hierarchical mip-chain "Pack" pass writing 16 mip levels per dispatch)
14
- * cannot become a single map-style fragment pass at all.
8
+ /**
9
+ * Map-style compute lowering caps simultaneous UAV writes to this many
10
+ * fragment color outputs. WebGL2 only guarantees `MAX_DRAW_BUFFERS >= 4`
11
+ * (real implementations commonly expose up to 8); a compute kernel that
12
+ * writes more UAVs than fit in one draw call's attachment set (e.g. a
13
+ * hierarchical mip-chain "Pack" pass writing 16 mip levels per dispatch)
14
+ * cannot become a single map-style fragment pass at all.
15
15
  */
16
16
  const MAX_MAP_STYLE_UAV_OUTPUTS = 8;
17
17
 
18
- /**
19
- * GLSL sampler type per DXBC resource-dimension id (float return types; the
20
- * stripped-RDEF corpus reflects everything as float).
18
+ /**
19
+ * GLSL sampler type per DXBC resource-dimension id (float return types; the
20
+ * stripped-RDEF corpus reflects everything as float).
21
21
  */
22
+ /**
23
+ * DXBC resource dimension for a cube map. Emulated addressing refuses it: a
24
+ * cube coordinate is a direction, so a [0,1] range test is meaningless there.
25
+ */
26
+ const CUBE_DIMENSION = 6;
22
27
  const SAMPLER_TYPE_BY_DIMENSION = {
23
28
  2: "sampler2D",
24
29
  3: "sampler2D",
@@ -27,11 +32,11 @@ const SAMPLER_TYPE_BY_DIMENSION = {
27
32
  8: "sampler2DArray"
28
33
  };
29
34
 
30
- /**
31
- * Shadow-sampler type per WebGL2-supported DXBC resource dimension.
32
- * Comparison sampling changes both the call signature and the uniform type;
33
- * keeping this separate from the ordinary sampler table guarantees shaders
34
- * without comparison opcodes retain their existing declarations verbatim.
35
+ /**
36
+ * Shadow-sampler type per WebGL2-supported DXBC resource dimension.
37
+ * Comparison sampling changes both the call signature and the uniform type;
38
+ * keeping this separate from the ordinary sampler table guarantees shaders
39
+ * without comparison opcodes retain their existing declarations verbatim.
35
40
  */
36
41
  const SHADOW_SAMPLER_TYPE_BY_DIMENSION = {
37
42
  3: "sampler2DShadow",
@@ -41,9 +46,9 @@ const SHADOW_SAMPLER_TYPE_BY_DIMENSION = {
41
46
  const COMPARISON_SAMPLE_OPCODES = new Set(["sample_c", "sample_c_lz"]);
42
47
  const NON_COMPARISON_TEXTURE_OPCODES = new Set(["sample", "sample_l", "sample_b", "sample_d", "gather4", "gather4_po", "ld", "ld_ms", "lod"]);
43
48
 
44
- /**
45
- * Filtered-sample coordinate mask per resource dimension
46
- * (HLSLcc `TranslateTexCoord`, toGLSLInstruction.cpp:985-1031).
49
+ /**
50
+ * Filtered-sample coordinate mask per resource dimension
51
+ * (HLSLcc `TranslateTexCoord`, toGLSLInstruction.cpp:985-1031).
47
52
  */
48
53
  /** GLSL symbol for the merged detail-map array. */
49
54
  const DETAIL_MAP_ARRAY_SYMBOL = "sDetailArrayMap";
@@ -57,32 +62,32 @@ const COORD_MASK_BY_DIMENSION = {
57
62
  10: "xyzw"
58
63
  };
59
64
 
60
- /**
61
- * DXBC -> GLSL ES 3.00 emitter for vertex and pixel stages.
62
- *
63
- * Implements the lowering rules in `docs/dxbc-lowering/*.md`, which pin every
64
- * template to HLSLcc's `toGLSLInstruction.cpp`/`toGLSLDeclaration.cpp`.
65
- * Registers are stored as float vec4s with bitcasts at use sites, comparison
66
- * results are 0xFFFFFFFF/0 masks (never bools), and register-stable symbols
67
- * (`cb#.data[]`, `t#`, `in_SEMANTIC#`, `vs_SEMANTIC#`) form the ABI surface.
68
- *
69
- * Emitter-wide policy: Adreno-3xx driver hygiene from HLSLcc (per-component
70
- * bitwise splits, `op_not`, `uint(0)` literals, `uvecN()` ctor wraps) is NOT
71
- * ported; the target is ANGLE-backed WebGL2 and the plain spec-legal forms
72
- * are emitted instead.
65
+ /**
66
+ * DXBC -> GLSL ES 3.00 emitter for vertex and pixel stages.
67
+ *
68
+ * Implements the lowering rules in `docs/dxbc-lowering/*.md`, which pin every
69
+ * template to HLSLcc's `toGLSLInstruction.cpp`/`toGLSLDeclaration.cpp`.
70
+ * Registers are stored as float vec4s with bitcasts at use sites, comparison
71
+ * results are 0xFFFFFFFF/0 masks (never bools), and register-stable symbols
72
+ * (`cb#.data[]`, `t#`, `in_SEMANTIC#`, `vs_SEMANTIC#`) form the ABI surface.
73
+ *
74
+ * Emitter-wide policy: Adreno-3xx driver hygiene from HLSLcc (per-component
75
+ * bitwise splits, `op_not`, `uint(0)` literals, `uvecN()` ctor wraps) is NOT
76
+ * ported; the target is ANGLE-backed WebGL2 and the plain spec-legal forms
77
+ * are emitted instead.
73
78
  */
74
79
  class DxbcGlslEmitter {
75
- /**
76
- * @param {object} [options] Emitter options.
77
- * @param {object} [options.profile] Target-runtime profile overrides.
78
- * @param {"array"|"std140"} [options.profile.constantBufferStyle] `uniform vec4 cbN[]`
79
- * arrays for ccpwgl's uniform4fv path, or std140 blocks.
80
- * @param {Object.<number,number>} [options.profile.pixelConstantBufferRemap] Pixel-stage
81
- * cb slot renames (ccpwgl keeps PS effect constants at cb7).
82
- * @param {function(number,string):string} [options.profile.samplerName] Texture uniform
83
- * naming per register and stage.
84
- * @param {number} [options.profile.vertexStructuredCapacity] Element capacity for
85
- * vertex-stage structured-buffer UBOs (bones; Carbon max is 69 joints).
80
+ /**
81
+ * @param {object} [options] Emitter options.
82
+ * @param {object} [options.profile] Target-runtime profile overrides.
83
+ * @param {"array"|"std140"} [options.profile.constantBufferStyle] `uniform vec4 cbN[]`
84
+ * arrays for ccpwgl's uniform4fv path, or std140 blocks.
85
+ * @param {Object.<number,number>} [options.profile.pixelConstantBufferRemap] Pixel-stage
86
+ * cb slot renames (ccpwgl keeps PS effect constants at cb7).
87
+ * @param {function(number,string):string} [options.profile.samplerName] Texture uniform
88
+ * naming per register and stage.
89
+ * @param {number} [options.profile.vertexStructuredCapacity] Element capacity for
90
+ * vertex-stage structured-buffer UBOs (bones; Carbon max is 69 joints).
86
91
  */
87
92
  constructor(options = {}) {
88
93
  this.helpers = new DxbcGlslHelperRegistry();
@@ -124,13 +129,54 @@ class DxbcGlslEmitter {
124
129
  };
125
130
  }
126
131
 
127
- /**
128
- * Registers the GLSL helper functions this emitter's lowerings can require.
129
- * GLSL ES 3.00 has no `bitfieldExtract`/`bitfieldInsert`/`bitCount`/
130
- * `textureGather` (all ES 3.10+), so `ibfe`/`ubfe`/`bfi`/`countbits`/
131
- * `gather4` lower to these hand-written equivalents instead.
132
+ /**
133
+ * Registers the GLSL helper functions this emitter's lowerings can require.
134
+ * GLSL ES 3.00 has no `bitfieldExtract`/`bitfieldInsert`/`bitCount`/
135
+ * `textureGather` (all ES 3.10+), so `ibfe`/`ubfe`/`bfi`/`countbits`/
136
+ * `gather4` lower to these hand-written equivalents instead.
132
137
  */
133
138
  _defineHelpers() {
139
+ // Texture address modes WebGL2 cannot express, emulated in the shader.
140
+ //
141
+ // WebGL2 does REPEAT, MIRRORED_REPEAT and CLAMP_TO_EDGE natively. It has
142
+ // no CLAMP_TO_BORDER (that needs EXT_texture_border_clamp, absent on our
143
+ // desktop contexts) and no MIRROR_ONCE. Only those two are done here.
144
+ //
145
+ // The mode is a RUNTIME value, per axis, read from the address buffer.
146
+ // It cannot be baked: ccpwgl resolves sampler overrides after
147
+ // translation and caches one translated GLSL per resource path across
148
+ // every instance, so a compile-time mode is wrong for any object whose
149
+ // override differs from the container - in both directions.
150
+ //
151
+ // Modes are the Trinity enum as stored: 1 wrap, 2 mirror, 3 clamp-edge,
152
+ // 4 border, 5 mirror-once. **0 means "nothing to emulate"**, which is
153
+ // what every failure produces - a zeroed buffer, an absent upload, a
154
+ // texture the consumer did not know about. Carbon's enum starts at 1 and
155
+ // no shipped sampler carries 0, so 0 cannot swallow a real mode.
156
+ //
157
+ // Two arities rather than one vec3 form: a 2D sample has no third
158
+ // coordinate, and its sampler may still declare a W mode - eight shipped
159
+ // samplers declare border on W - which must not be tested against a
160
+ // component that does not exist.
161
+ this.helpers.define("cjsAddressCoord", {
162
+ source: ["vec2 cjsAddressCoord(vec2 uv, vec2 modes) {", " if (int(modes.x) == 5) uv.x = clamp(abs(uv.x), 0.0, 1.0);", " if (int(modes.y) == 5) uv.y = clamp(abs(uv.y), 0.0, 1.0);", " return uv;", "}", "", "vec3 cjsAddressCoord(vec3 uv, vec3 modes) {", " if (int(modes.x) == 5) uv.x = clamp(abs(uv.x), 0.0, 1.0);", " if (int(modes.y) == 5) uv.y = clamp(abs(uv.y), 0.0, 1.0);", " if (int(modes.z) == 5) uv.z = clamp(abs(uv.z), 0.0, 1.0);", " return uv;", "}"].join("\n")
163
+ });
164
+
165
+ // Applied to the already sampled value, so it composes with every sample
166
+ // form the emitter produces - texture, textureLod, textureGrad and the
167
+ // bias variant - instead of needing an overload for each.
168
+ //
169
+ // The border colour is baked, not read from the buffer: Carbon's
170
+ // AddSamplerOverride takes only U and V, so no override can change it.
171
+ // It is NOT always transparent black - specialfx/cloud, cloudsimple and
172
+ // volumetrichalfsphereglow use opaque white.
173
+ //
174
+ // Testing the post-transform coordinate is safe: an axis is either
175
+ // mirror-once or border, never both, and mirror-once leaves other axes
176
+ // untouched.
177
+ this.helpers.define("cjsAddressBorder", {
178
+ source: ["vec4 cjsAddressBorder(vec4 sampled, vec2 uv, vec2 modes, vec4 borderColor) {", " if (int(modes.x) == 4 && (uv.x < 0.0 || uv.x > 1.0)) return borderColor;", " if (int(modes.y) == 4 && (uv.y < 0.0 || uv.y > 1.0)) return borderColor;", " return sampled;", "}", "", "vec4 cjsAddressBorder(vec4 sampled, vec3 uv, vec3 modes, vec4 borderColor) {", " if (int(modes.x) == 4 && (uv.x < 0.0 || uv.x > 1.0)) return borderColor;", " if (int(modes.y) == 4 && (uv.y < 0.0 || uv.y > 1.0)) return borderColor;", " if (int(modes.z) == 4 && (uv.z < 0.0 || uv.z > 1.0)) return borderColor;", " return sampled;", "}"].join("\n")
179
+ });
134
180
  // D3D11 IBFE pseudocode: width = src0 & 0x1f, offset = src1 & 0x1f;
135
181
  // width == 0 -> 0; width + offset < 32 -> a left/right arithmetic-shift
136
182
  // pair that sign-extends from bit (width-1); else an arithmetic shift by
@@ -173,16 +219,16 @@ class DxbcGlslEmitter {
173
219
  });
174
220
  }
175
221
 
176
- /**
177
- * Translates one DXBC stage into GLSL ES 3.00 source.
178
- *
179
- * @param {ArrayBuffer|ArrayBufferView|Uint8Array} bytes DXBC container bytes.
180
- * @param {object} [options] Emit options.
181
- * @param {string} [options.source] Source name used in error details.
182
- * @returns {{source:string,stageName:string,inputs:object[],outputs:object[],bindings:object[],warnings:string[],computeFragment:(object|undefined)}}
183
- * GLSL text plus the IO contract the packaging layer records; compute
184
- * stages add a `computeFragment` host contract (thread group,
185
- * dispatch-origin uniform, per-output UAV slice routing).
222
+ /**
223
+ * Translates one DXBC stage into GLSL ES 3.00 source.
224
+ *
225
+ * @param {ArrayBuffer|ArrayBufferView|Uint8Array} bytes DXBC container bytes.
226
+ * @param {object} [options] Emit options.
227
+ * @param {string} [options.source] Source name used in error details.
228
+ * @returns {{source:string,stageName:string,inputs:object[],outputs:object[],bindings:object[],warnings:string[],computeFragment:(object|undefined)}}
229
+ * GLSL text plus the IO contract the packaging layer records; compute
230
+ * stages add a `computeFragment` host contract (thread group,
231
+ * dispatch-origin uniform, per-output UAV slice routing).
186
232
  */
187
233
  Emit(bytes, options = {}) {
188
234
  const sourceName = options.source || "memory";
@@ -214,12 +260,20 @@ class DxbcGlslEmitter {
214
260
  outputNames: new Map(),
215
261
  resourceDimensions: new Map(),
216
262
  resourceNames: new Map(),
217
- comparisonResources: this._analyzeComparisonResources(raw.decoder, sourceName),
263
+ ...(() => {
264
+ const analysed = this._analyzeComparisonResources(raw.decoder, sourceName);
265
+ return {
266
+ comparisonResources: analysed.comparison,
267
+ pairedSamplers: analysed.paired
268
+ };
269
+ })(),
218
270
  constantBufferNames: new Map(),
219
271
  structuredBuffers: new Map(),
220
272
  lightConstantBuffer: this._normalizeLightConstantBufferProfile(),
221
273
  lightConstantBufferDeclared: false,
222
274
  lightPackedTexture: this._normalizeLightPackedTextureProfile(),
275
+ emulatedAddressing: this._normalizeEmulatedAddressingProfile(),
276
+ addressBufferDeclared: false,
223
277
  lightPackedTextureDeclared: false,
224
278
  // Resource (t#) registers stubbed to a compile-time zero: their
225
279
  // declarations/bindings are dropped and every read of them lowers
@@ -361,10 +415,10 @@ class DxbcGlslEmitter {
361
415
  return result;
362
416
  }
363
417
 
364
- /**
365
- * Normalizes the optional local-light constant-buffer lowering profile.
366
- *
367
- * @returns {object|null} Normalized profile, or null when disabled.
418
+ /**
419
+ * Normalizes the optional local-light constant-buffer lowering profile.
420
+ *
421
+ * @returns {object|null} Normalized profile, or null when disabled.
368
422
  */
369
423
  _normalizeLightConstantBufferProfile() {
370
424
  const profile = this.profile.lightConstantBuffer;
@@ -383,30 +437,52 @@ class DxbcGlslEmitter {
383
437
  };
384
438
  }
385
439
 
386
- /**
387
- * Finds resources used by comparison-sample instructions before resource
388
- * declarations are emitted, so those declarations can use shadow sampler
389
- * types. A single GLSL uniform cannot be both a shadow and ordinary sampler;
390
- * reject that shape explicitly instead of producing invalid GLSL.
391
- *
392
- * @param {object} decoder Decoded DXBC instruction stream.
393
- * @param {string} sourceName Source name used in error details.
394
- * @returns {Map<number,Set<number>>} Resource register -> sampler registers.
440
+ /**
441
+ * Finds resources used by comparison-sample instructions before resource
442
+ * declarations are emitted, so those declarations can use shadow sampler
443
+ * types. A single GLSL uniform cannot be both a shadow and ordinary sampler;
444
+ * reject that shape explicitly instead of producing invalid GLSL.
445
+ *
446
+ * The same walk records the t#/s# pairing for EVERY sampled resource, not
447
+ * only the comparison ones. D3D carries sampler state on a shared sampler
448
+ * object and pairs it with a texture at the sample site; GLSL merges the two
449
+ * into one uniform, so that pairing is the only place the pairing exists once
450
+ * the shader is translated. Dropping it forces a consumer to guess, and the
451
+ * available guess - "the sampler whose register equals the texture's" - is
452
+ * numeric coincidence: with two pattern samplers declared, `NormalMap` at t3
453
+ * collides with the clamp sampler at s3 and every hull's normal map is read
454
+ * clamped instead of wrapped.
455
+ *
456
+ * @param {object} decoder Decoded DXBC instruction stream.
457
+ * @param {string} sourceName Source name used in error details.
458
+ * @returns {{comparison:Map<number,Set<number>>,paired:Map<number,Set<number>>}}
459
+ * Resource register -> sampler registers, for comparison sampling and for
460
+ * all sampling respectively.
395
461
  */
396
462
  _analyzeComparisonResources(decoder, sourceName) {
397
463
  const comparisonResources = new Map();
464
+ const pairedSamplers = new Map();
398
465
  const ordinaryResources = new Set();
466
+ const recordPair = (map, register, samplerRegister) => {
467
+ const samplers = map.get(register) || new Set();
468
+ if (Number.isInteger(samplerRegister)) samplers.add(samplerRegister);
469
+ map.set(register, samplers);
470
+ };
399
471
  for (const instruction of decoder.instructions) {
400
472
  const resourceOperand = instruction.operands?.[2];
401
473
  if (!resourceOperand || !Number.isInteger(resourceOperand.registerIndex)) continue;
474
+ const register = resourceOperand.registerIndex;
475
+ const samplerRegister = instruction.operands?.[3]?.registerIndex;
402
476
  if (COMPARISON_SAMPLE_OPCODES.has(instruction.opcodeName)) {
403
- const register = resourceOperand.registerIndex;
404
- const samplerRegister = instruction.operands?.[3]?.registerIndex;
405
- const samplers = comparisonResources.get(register) || new Set();
406
- if (Number.isInteger(samplerRegister)) samplers.add(samplerRegister);
407
- comparisonResources.set(register, samplers);
477
+ recordPair(comparisonResources, register, samplerRegister);
478
+ recordPair(pairedSamplers, register, samplerRegister);
408
479
  } else if (NON_COMPARISON_TEXTURE_OPCODES.has(instruction.opcodeName)) {
409
- ordinaryResources.add(resourceOperand.registerIndex);
480
+ ordinaryResources.add(register);
481
+ // `ld`-family fetches carry no sampler operand and legitimately
482
+ // pair with nothing; only record a pair when one is present.
483
+ if (Number.isInteger(samplerRegister)) {
484
+ recordPair(pairedSamplers, register, samplerRegister);
485
+ }
410
486
  }
411
487
  }
412
488
  for (const register of comparisonResources.keys()) {
@@ -417,13 +493,258 @@ class DxbcGlslEmitter {
417
493
  });
418
494
  }
419
495
  }
420
- return comparisonResources;
496
+ return {
497
+ comparison: comparisonResources,
498
+ paired: pairedSamplers
499
+ };
500
+ }
501
+
502
+ /**
503
+ * Normalizes the emulated-addressing profile.
504
+ *
505
+ * Keyed by RESOURCE register, not sampler register, because that is what
506
+ * every layer we touch keys on: GL stores wrap state on the texture object,
507
+ * ccpwgl keys overrides by texture name, and the emitted GLSL declares one
508
+ * sampler uniform per resource. Only D3D treats the mode as a property of a
509
+ * shared sampler - in `decalv5` five textures share one bordered sampler and
510
+ * each can resolve to a different mode, which a sampler-keyed scheme cannot
511
+ * represent.
512
+ *
513
+ * It arrives as a profile option because it CANNOT be discovered here: DXBC
514
+ * carries no sampler state, which lives in the Carbon container wrapping it.
515
+ * The caller also chooses the SUPERSET - listing a texture whose container
516
+ * mode needs no emulation is deliberate and cheap, because the runtime mode
517
+ * decides, and it is what lets an override correct a container that is wrong.
518
+ *
519
+ * Shape: `{ bufferRegister, textures: [{ registerIndex, borderColor }] }`.
520
+ *
521
+ * @returns {{bufferRegister:number, textures:Map<number,{color:number[]}>}|null}
522
+ * Null when disabled.
523
+ */
524
+ _normalizeEmulatedAddressingProfile() {
525
+ const value = this.profile.emulatedAddressing;
526
+
527
+ // Either input alone is a complete profile: `samplerModes` is the
528
+ // ordinary case (container modes, mapped onto resources here), and
529
+ // `textures` is for a caller gating something the container gives it no
530
+ // reason to gate. Requiring both would reject the ordinary case.
531
+ if (!value || typeof value !== "object") return null;
532
+ if (!Array.isArray(value.textures) && !value.samplerModes) return null;
533
+ const bufferRegister = Number(value.bufferRegister ?? 8);
534
+ if (!Number.isInteger(bufferRegister) || bufferRegister < 0 || bufferRegister > 254) {
535
+ throw new WebglReadError("Emulated-addressing buffer register must be an integer in 0..254", {
536
+ bufferRegister: value.bufferRegister
537
+ });
538
+ }
539
+ const colorOf = raw => {
540
+ if (raw === undefined || raw === null) return [0, 0, 0, 0];
541
+ if (!Array.isArray(raw) || raw.length !== 4) {
542
+ throw new WebglReadError("Border colour must be four components", {
543
+ color: raw
544
+ });
545
+ }
546
+ const parsed = raw.map(Number);
547
+
548
+ // A malformed colour must not become a plausible one: transparent
549
+ // black is the common case, so silently defaulting to it would hide
550
+ // the mistake in exactly the effects that differ from it.
551
+ if (parsed.some(c => !Number.isFinite(c))) {
552
+ throw new WebglReadError("Border colour components must be finite numbers", {
553
+ color: raw
554
+ });
555
+ }
556
+ return parsed;
557
+ };
558
+
559
+ // Container sampler modes, keyed by SAMPLER register. Supplied so the
560
+ // emitter can derive which resources need a gate: only it knows, from
561
+ // the DXBC, which resource is read through which sampler. The caller
562
+ // knows the modes but not that mapping; the emitter knows the mapping
563
+ // but not the modes, because DXBC carries no sampler state.
564
+ const samplerModes = new Map();
565
+ for (const [key, modes] of Object.entries(value.samplerModes ?? {})) {
566
+ const registerIndex = Number(key);
567
+ if (!Number.isInteger(registerIndex) || registerIndex < 0) continue;
568
+ const axes = [modes?.u, modes?.v, modes?.w].map(Number);
569
+
570
+ // Only the two modes WebGL2 cannot express need a gate. Everything
571
+ // else is left to GL, so listing it would cost a branch for nothing.
572
+ if (axes.some(m => m === 4 || m === 5)) {
573
+ samplerModes.set(registerIndex, true);
574
+ }
575
+ }
576
+ const textures = new Map();
577
+ for (const entry of value.textures ?? []) {
578
+ const registerIndex = Number.isInteger(entry) ? entry : Number(entry?.registerIndex);
579
+ if (!Number.isInteger(registerIndex) || registerIndex < 0) {
580
+ throw new WebglReadError("Emulated-addressing entries need an integer resource registerIndex", {
581
+ entry
582
+ });
583
+ }
584
+ textures.set(registerIndex, {
585
+ color: colorOf(Number.isInteger(entry) ? null : entry?.borderColor)
586
+ });
587
+ }
588
+ if (!textures.size && !samplerModes.size) return null;
589
+ return {
590
+ bufferRegister,
591
+ textures,
592
+ samplerModes,
593
+ defaultColor: colorOf(value.borderColor)
594
+ };
595
+ }
596
+
597
+ /**
598
+ * Applies emulated address modes to one sample.
599
+ *
600
+ * Emits nothing for a texture the caller did not list, so a shader with no
601
+ * emulated addressing is byte-identical to one emitted without the profile.
602
+ *
603
+ * @param {object} state Emit state.
604
+ * @param {object} instruction Decoded texture instruction.
605
+ * @param {string} call The GLSL sample expression built so far.
606
+ * @param {string} coord The coordinate expression passed to that sample.
607
+ * @param {string} coordMask Component mask of the coordinate.
608
+ * @returns {string} The sample expression, addressed when the caller listed it.
609
+ * @private
610
+ */
611
+ _applyEmulatedAddressing(state, instruction, call, coord, coordMask) {
612
+ const profile = state.emulatedAddressing;
613
+ if (!profile) return call;
614
+ const resourceRegister = instruction.operands?.[2]?.registerIndex;
615
+ if (!Number.isInteger(resourceRegister)) return call;
616
+
617
+ // An explicit entry wins; otherwise derive from the sampler this
618
+ // resource is read through. Deriving is the ordinary case - the caller
619
+ // supplies container modes and the emitter maps them onto resources -
620
+ // and the explicit list is for a caller that wants a texture gated the
621
+ // container gives it no reason to gate. That is not redundant: a
622
+ // sampler override can introduce an emulated mode the container never
623
+ // declared, and overrides exist precisely to correct a wrong container.
624
+ let entry = profile.textures.get(resourceRegister);
625
+ if (!entry) {
626
+ const samplerRegister = instruction.operands?.[3]?.registerIndex;
627
+ if (!Number.isInteger(samplerRegister)) return call;
628
+ if (!profile.samplerModes.has(samplerRegister)) return call;
629
+ entry = {
630
+ color: profile.defaultColor
631
+ };
632
+ }
633
+
634
+ // A cube coordinate is a DIRECTION, not a texture coordinate, so a
635
+ // [0,1] test has no meaning on it. Refuse rather than emit a test that
636
+ // is silently nonsense - the caller must not list cube-target
637
+ // resources. decalholev5's interior cube and cubetextureviewer are the
638
+ // shipped cases that reach here.
639
+ const dimension = state.resourceDimensions.get(resourceRegister);
640
+ if (dimension === CUBE_DIMENSION) {
641
+ // Reported and skipped, not fatal. `decalholev5` reads its interior
642
+ // cube through a bordered sampler, so refusing would make a shipped
643
+ // effect unbuildable over a mode that cannot apply to it anyway -
644
+ // trading a missing border for no shader at all. Skipping silently
645
+ // is the other wrong answer, so it goes in warnings.
646
+ state.warnings.push(`emulated addressing skipped for cube resource t${resourceRegister}: ` + "a cube coordinate is a direction, so a [0,1] range test has no meaning");
647
+ return call;
648
+ }
649
+ if (coordMask.length !== 2 && coordMask.length !== 3) {
650
+ throw new WebglReadError("Emulated addressing supports two- and three-component sampling only", {
651
+ source: state.sourceName,
652
+ registerIndex: resourceRegister,
653
+ coordMask
654
+ });
655
+ }
656
+ const buffer = this._ensureAddressBuffer(state);
657
+ const swizzle = coordMask.length === 2 ? "xy" : "xyz";
658
+ const modes = `${buffer}[${resourceRegister}].${swizzle}`;
659
+ const glslFloat = n => Number.isInteger(n) ? `${n}.0` : String(n);
660
+ const color = entry.color.map(glslFloat).join(", ");
661
+
662
+ // Mirror-once rewrites the coordinate BEFORE the fetch; border tests
663
+ // after it. Both are emitted: an axis is only ever one of them, and the
664
+ // unused one reads mode 0 or a native mode and does nothing.
665
+ const coordHelper = this.helpers.require("cjsAddressCoord");
666
+ const borderHelper = this.helpers.require("cjsAddressBorder");
667
+ const addressedCoord = `${coordHelper}(${coord}, ${modes})`;
668
+ const rewritten = call.split(coord).join(addressedCoord);
669
+ return `${borderHelper}(${rewritten}, ${addressedCoord}, ${modes}, vec4(${color}))`;
670
+ }
671
+
672
+ /**
673
+ * Declares the emulated-addressing constant buffer, once per shader.
674
+ *
675
+ * Sized to the highest listed resource register so the array stays compact,
676
+ * the way Carbon emitters declare compact `cb` arrays. The register is
677
+ * caller-chosen and defaults to 8: shipped effects declare only cb0-4, 6 and
678
+ * 7, and ccpwgl already resolves `cb0`..`cb15` by name, so 8 needs no new
679
+ * binding table.
680
+ *
681
+ * @param {object} state Emit state.
682
+ * @returns {string} The buffer's GLSL name.
683
+ * @private
684
+ */
685
+ _ensureAddressBuffer(state) {
686
+ const {
687
+ bufferRegister,
688
+ textures
689
+ } = state.emulatedAddressing;
690
+ const name = `cb${bufferRegister}`;
691
+ if (state.addressBufferDeclared) return name;
692
+
693
+ // Two declarations of one cb name is a GLSL redefinition error that
694
+ // nothing downstream checks, so catch it here, where the register is
695
+ // still attributable to a caller's choice.
696
+ //
697
+ // Compared by NAME, not by register: `constantBufferNames` is keyed by
698
+ // DXBC slot while the emitted name uses the remapped register, and on a
699
+ // pixel stage slot 0 becomes `cb7`. Comparing register to slot would
700
+ // miss exactly that case and emit two `cb7` declarations.
701
+ if ([...state.constantBufferNames.values()].includes(name)) {
702
+ throw new WebglReadError("Emulated-addressing buffer register collides with a declared constant buffer", {
703
+ source: state.sourceName,
704
+ bufferRegister
705
+ });
706
+ }
707
+
708
+ // Sized from every resource the shader DECLARES, not from the addressed
709
+ // set: with a derived set the addressed registers are not all known yet,
710
+ // and the array is indexed by resource register, so it must be long
711
+ // enough for any of them. DXBC puts declarations before instructions, so
712
+ // resourceDimensions is complete by the first sample.
713
+ const highest = Math.max(-1, ...state.resourceDimensions.keys(), ...textures.keys());
714
+ const rows = highest + 1;
715
+ if (rows < 1) {
716
+ throw new WebglReadError("Emulated addressing found no declared resource to size its buffer", {
717
+ source: state.sourceName
718
+ });
719
+ }
720
+ state.constantBufferNames.set(bufferRegister, name);
721
+ state.addressBufferDeclared = true;
722
+ state.declarationLines.push(`uniform vec4 ${name}[${rows}];`);
723
+ // An ordinary constantBuffer binding, deliberately with no extra fields.
724
+ // `cjsSemantic` is reserved vocabulary for the local-light family and
725
+ // the block writer throws on any other value, and the wire drops fields
726
+ // it does not encode - so an invented one would vanish for every effect
727
+ // loaded from bytes, which is exactly how the packed-light branch came
728
+ // to be silently dead.
729
+ //
730
+ // The consumer identifies it by register instead: Carbon declares only
731
+ // cb0-4, 6 and 7 across all 537 shipped effects, so a constant buffer at
732
+ // 8 or above is ours. That is a convention, and it is written down in
733
+ // the addressing contract rather than left to be inferred.
734
+ state.bindings.push({
735
+ kind: "constantBuffer",
736
+ registerIndex: bufferRegister,
737
+ name,
738
+ sizeInVec4: rows,
739
+ style: "array"
740
+ });
741
+ return name;
421
742
  }
422
743
 
423
- /**
424
- * Normalizes the optional packed local-light texture lowering profile.
425
- *
426
- * @returns {object|null} Normalized profile, or null when disabled.
744
+ /**
745
+ * Normalizes the optional packed local-light texture lowering profile.
746
+ *
747
+ * @returns {object|null} Normalized profile, or null when disabled.
427
748
  */
428
749
  _normalizeLightPackedTextureProfile() {
429
750
  const profile = this.profile.lightPackedTexture;
@@ -441,12 +762,12 @@ class DxbcGlslEmitter {
441
762
  };
442
763
  }
443
764
 
444
- /**
445
- * Declares the packed local-light texture ABI used to replace the
446
- * LightIndexBuffer/LightBuffer/LightProfileArray trio with one RGBA32UI
447
- * data texture.
448
- *
449
- * @param {object} state Emit state.
765
+ /**
766
+ * Declares the packed local-light texture ABI used to replace the
767
+ * LightIndexBuffer/LightBuffer/LightProfileArray trio with one RGBA32UI
768
+ * data texture.
769
+ *
770
+ * @param {object} state Emit state.
450
771
  */
451
772
  _ensureLightPackedTexture(state) {
452
773
  const profile = state.lightPackedTexture;
@@ -468,11 +789,11 @@ class DxbcGlslEmitter {
468
789
  });
469
790
  }
470
791
 
471
- /**
472
- * Declares the local-light constant-buffer ABI used to replace the two
473
- * tiled-light structured buffers without consuming sampler units.
474
- *
475
- * @param {object} state Emit state.
792
+ /**
793
+ * Declares the local-light constant-buffer ABI used to replace the two
794
+ * tiled-light structured buffers without consuming sampler units.
795
+ *
796
+ * @param {object} state Emit state.
476
797
  */
477
798
  _ensureLightConstantBuffer(state) {
478
799
  const profile = state.lightConstantBuffer;
@@ -495,15 +816,15 @@ class DxbcGlslEmitter {
495
816
  });
496
817
  }
497
818
 
498
- /**
499
- * Rejects compute shaders that need real compute-pipeline features (shared
500
- * memory, barriers, atomics, raw/structured/typed UAV reads) instead of the
501
- * "map-style" thread-per-fragment shape this emitter lowers. The message
502
- * intentionally contains "not supported" so packaging's kill-list
503
- * classifier regex (`/not supported|No GLSL lowering|unimplementable/i`)
504
- * routes these to the exclusion list rather than the failure list.
505
- *
506
- * @param {object} state Emit state.
819
+ /**
820
+ * Rejects compute shaders that need real compute-pipeline features (shared
821
+ * memory, barriers, atomics, raw/structured/typed UAV reads) instead of the
822
+ * "map-style" thread-per-fragment shape this emitter lowers. The message
823
+ * intentionally contains "not supported" so packaging's kill-list
824
+ * classifier regex (`/not supported|No GLSL lowering|unimplementable/i`)
825
+ * routes these to the exclusion list rather than the failure list.
826
+ *
827
+ * @param {object} state Emit state.
507
828
  */
508
829
  _assertMapStyleCompute(state) {
509
830
  for (const instruction of state.decoder.instructions) {
@@ -519,26 +840,26 @@ class DxbcGlslEmitter {
519
840
  }
520
841
  }
521
842
 
522
- /**
523
- * Pre-scans a map-style compute shader's `store_uav_typed` instructions and
524
- * builds a per-UAV slice plan before any declaration is emitted.
525
- *
526
- * Straight-line constant propagation only: the address registers ASSAO-style
527
- * Prepare passes build are `mov rX.<c>, l(k)` immediates, so the tracker
528
- * records the last constant written to each temp component and treats every
529
- * other write as dynamic. Each stored-to UAV register resolves to either
530
- * `{ kind: "single" }` (at most one store; the array-slice coordinate, when
531
- * present, is dropped — the host attaches the target layer via
532
- * `framebufferTextureLayer`) or `{ kind: "multiSlice", slices, sliceByOffset }`
533
- * (several stores at statically distinct texture2darray slices, each routed
534
- * to its own fragment output — the host attaches those layers as sequential
535
- * color attachments).
536
- *
537
- * Anything outside those two shapes cannot become one fragment pass, so the
538
- * throw messages deliberately contain "not supported" for packaging's
539
- * kill-list classifier (see `_assertMapStyleCompute`).
540
- *
541
- * @param {object} state Emit state.
843
+ /**
844
+ * Pre-scans a map-style compute shader's `store_uav_typed` instructions and
845
+ * builds a per-UAV slice plan before any declaration is emitted.
846
+ *
847
+ * Straight-line constant propagation only: the address registers ASSAO-style
848
+ * Prepare passes build are `mov rX.<c>, l(k)` immediates, so the tracker
849
+ * records the last constant written to each temp component and treats every
850
+ * other write as dynamic. Each stored-to UAV register resolves to either
851
+ * `{ kind: "single" }` (at most one store; the array-slice coordinate, when
852
+ * present, is dropped — the host attaches the target layer via
853
+ * `framebufferTextureLayer`) or `{ kind: "multiSlice", slices, sliceByOffset }`
854
+ * (several stores at statically distinct texture2darray slices, each routed
855
+ * to its own fragment output — the host attaches those layers as sequential
856
+ * color attachments).
857
+ *
858
+ * Anything outside those two shapes cannot become one fragment pass, so the
859
+ * throw messages deliberately contain "not supported" for packaging's
860
+ * kill-list classifier (see `_assertMapStyleCompute`).
861
+ *
862
+ * @param {object} state Emit state.
542
863
  */
543
864
  _analyzeUavStores(state) {
544
865
  for (const instruction of state.decoder.instructions) {
@@ -611,16 +932,16 @@ class DxbcGlslEmitter {
611
932
  }
612
933
  }
613
934
 
614
- /**
615
- * Updates the per-temp-component constant tracker for one instruction:
616
- * `mov rX.<mask>, l(...)` records each written component's immediate dword,
617
- * and any other write to a temp component invalidates it. Destination
618
- * operands are exactly the mask-selection temp operands (DXBC sources
619
- * always read through swizzle/select1), so this also handles multi-
620
- * destination opcodes like `sincos` and `udiv` conservatively.
621
- *
622
- * @param {object} instruction Decoded non-declaration instruction.
623
- * @param {Map<string, number>} constantTemps Constant tracker keyed `<register>.<component>`.
935
+ /**
936
+ * Updates the per-temp-component constant tracker for one instruction:
937
+ * `mov rX.<mask>, l(...)` records each written component's immediate dword,
938
+ * and any other write to a temp component invalidates it. Destination
939
+ * operands are exactly the mask-selection temp operands (DXBC sources
940
+ * always read through swizzle/select1), so this also handles multi-
941
+ * destination opcodes like `sincos` and `udiv` conservatively.
942
+ *
943
+ * @param {object} instruction Decoded non-declaration instruction.
944
+ * @param {Map<string, number>} constantTemps Constant tracker keyed `<register>.<component>`.
624
945
  */
625
946
  _trackTempConstants(instruction, constantTemps) {
626
947
  const source = instruction.operands[1];
@@ -639,13 +960,13 @@ class DxbcGlslEmitter {
639
960
  }
640
961
  }
641
962
 
642
- /**
643
- * Resolves the array-slice (z) component of a `store_uav_typed` address
644
- * operand to a compile-time constant, or null when it is dynamic.
645
- *
646
- * @param {object} addressOperand Decoded address operand.
647
- * @param {Map<string, number>} constantTemps Constant tracker keyed `<register>.<component>`.
648
- * @returns {number|null} Constant slice index, or null.
963
+ /**
964
+ * Resolves the array-slice (z) component of a `store_uav_typed` address
965
+ * operand to a compile-time constant, or null when it is dynamic.
966
+ *
967
+ * @param {object} addressOperand Decoded address operand.
968
+ * @param {Map<string, number>} constantTemps Constant tracker keyed `<register>.<component>`.
969
+ * @returns {number|null} Constant slice index, or null.
649
970
  */
650
971
  _resolveSliceConstant(addressOperand, constantTemps) {
651
972
  if (addressOperand.type === 4) {
@@ -660,14 +981,14 @@ class DxbcGlslEmitter {
660
981
  return constantTemps.has(key) ? constantTemps.get(key) : null;
661
982
  }
662
983
 
663
- /**
664
- * Allocates the next map-style UAV fragment-output location, enforcing the
665
- * `MAX_MAP_STYLE_UAV_OUTPUTS` attachment budget across single-output UAVs
666
- * and per-slice multi-output UAVs alike.
667
- *
668
- * @param {object} state Emit state.
669
- * @param {number} register UAV register index (error detail only).
670
- * @returns {number} Allocated output location.
984
+ /**
985
+ * Allocates the next map-style UAV fragment-output location, enforcing the
986
+ * `MAX_MAP_STYLE_UAV_OUTPUTS` attachment budget across single-output UAVs
987
+ * and per-slice multi-output UAVs alike.
988
+ *
989
+ * @param {object} state Emit state.
990
+ * @param {number} register UAV register index (error detail only).
991
+ * @returns {number} Allocated output location.
671
992
  */
672
993
  _allocateUavOutputLocation(state, register) {
673
994
  const location = state.uavOutputCount;
@@ -686,12 +1007,12 @@ class DxbcGlslEmitter {
686
1007
  // Declarations
687
1008
  // ------------------------------------------------------------------
688
1009
 
689
- /**
690
- * Emits one decoded DXBC declaration into the GLSL interface and manifest.
691
- *
692
- * @param {object} state Mutable emission state.
693
- * @param {object} instruction Decoded DXBC declaration instruction.
694
- * @private
1010
+ /**
1011
+ * Emits one decoded DXBC declaration into the GLSL interface and manifest.
1012
+ *
1013
+ * @param {object} state Mutable emission state.
1014
+ * @param {object} instruction Decoded DXBC declaration instruction.
1015
+ * @private
695
1016
  */
696
1017
  _emitDeclaration(state, instruction) {
697
1018
  const declaration = instruction.declaration;
@@ -795,12 +1116,21 @@ class DxbcGlslEmitter {
795
1116
  state.resourceDimensions.set(register, declaration.resourceDimension);
796
1117
  state.resourceNames.set(register, name);
797
1118
  state.declarationLines.push(`uniform mediump ${samplerType} ${name};`);
1119
+ // The sampler this texture is actually paired with at its sample
1120
+ // sites. A texture sampled through more than one sampler cannot
1121
+ // be represented by one GLSL uniform's state, so record them all
1122
+ // and let the packaging layer decide; a texture sampled through
1123
+ // none (pure `ld`) simply has no sampler state to carry.
1124
+ const paired = state.pairedSamplers.get(register);
798
1125
  state.bindings.push({
799
1126
  kind: "resource",
800
1127
  registerIndex: register,
801
1128
  name,
802
1129
  samplerType,
803
1130
  dimensionName: declaration.resourceDimensionName,
1131
+ ...(paired?.size ? {
1132
+ pairedSamplerRegisters: [...paired].sort((a, b) => a - b)
1133
+ } : {}),
804
1134
  ...(comparisonSamplers ? {
805
1135
  comparison: true,
806
1136
  samplerRegisterIndices: [...comparisonSamplers].sort((a, b) => a - b)
@@ -978,24 +1308,24 @@ class DxbcGlslEmitter {
978
1308
  }
979
1309
  }
980
1310
 
981
- /**
982
- * Finds the signature element assigned to a register.
983
- *
984
- * @param {object|null} signature Decoded DXBC signature.
985
- * @param {number} registerIndex Register index.
986
- * @returns {object|null} Matching signature element.
987
- * @private
1311
+ /**
1312
+ * Finds the signature element assigned to a register.
1313
+ *
1314
+ * @param {object|null} signature Decoded DXBC signature.
1315
+ * @param {number} registerIndex Register index.
1316
+ * @returns {object|null} Matching signature element.
1317
+ * @private
988
1318
  */
989
1319
  _signatureElement(signature, registerIndex) {
990
1320
  return signature?.elements.find(element => element.registerIndex === registerIndex) || null;
991
1321
  }
992
1322
 
993
- /**
994
- * Declares one vertex input using its Carbon semantic identity.
995
- *
996
- * @param {object} state Mutable emission state.
997
- * @param {object} instruction Decoded input declaration.
998
- * @private
1323
+ /**
1324
+ * Declares one vertex input using its Carbon semantic identity.
1325
+ *
1326
+ * @param {object} state Mutable emission state.
1327
+ * @param {object} instruction Decoded input declaration.
1328
+ * @private
999
1329
  */
1000
1330
  _declareVertexInput(state, instruction) {
1001
1331
  const register = instruction.declaration.registerIndex;
@@ -1037,12 +1367,12 @@ class DxbcGlslEmitter {
1037
1367
  });
1038
1368
  }
1039
1369
 
1040
- /**
1041
- * Declares one pixel-stage varying or system input.
1042
- *
1043
- * @param {object} state Mutable emission state.
1044
- * @param {object} instruction Decoded input declaration.
1045
- * @private
1370
+ /**
1371
+ * Declares one pixel-stage varying or system input.
1372
+ *
1373
+ * @param {object} state Mutable emission state.
1374
+ * @param {object} instruction Decoded input declaration.
1375
+ * @private
1046
1376
  */
1047
1377
  _declarePixelInput(state, instruction) {
1048
1378
  const register = instruction.declaration.registerIndex;
@@ -1082,12 +1412,12 @@ class DxbcGlslEmitter {
1082
1412
  });
1083
1413
  }
1084
1414
 
1085
- /**
1086
- * Maps a supported system-generated input to its GLSL expression.
1087
- *
1088
- * @param {object} state Mutable emission state.
1089
- * @param {object} instruction Decoded system-input declaration.
1090
- * @private
1415
+ /**
1416
+ * Maps a supported system-generated input to its GLSL expression.
1417
+ *
1418
+ * @param {object} state Mutable emission state.
1419
+ * @param {object} instruction Decoded system-input declaration.
1420
+ * @private
1091
1421
  */
1092
1422
  _declareSystemInput(state, instruction) {
1093
1423
  const declaration = instruction.declaration;
@@ -1110,16 +1440,16 @@ class DxbcGlslEmitter {
1110
1440
  }
1111
1441
  }
1112
1442
 
1113
- /**
1114
- * Records which compute pseudo-input(s) a `dcl_input` declaration exposes.
1115
- * The actual GLSL (uniform + derived locals) is emitted once, after every
1116
- * declaration has been seen, by `_emitComputeInputPrelude` — `dcl_input`
1117
- * for a thread-id kind can appear before or after `dcl_thread_group` in the
1118
- * bytecode stream, and the flattened/group-id forms need the workgroup
1119
- * size regardless of declaration order.
1120
- *
1121
- * @param {object} state Emit state.
1122
- * @param {object} instruction `dcl_input` instruction with a thread-id-kind operand.
1443
+ /**
1444
+ * Records which compute pseudo-input(s) a `dcl_input` declaration exposes.
1445
+ * The actual GLSL (uniform + derived locals) is emitted once, after every
1446
+ * declaration has been seen, by `_emitComputeInputPrelude` — `dcl_input`
1447
+ * for a thread-id kind can appear before or after `dcl_thread_group` in the
1448
+ * bytecode stream, and the flattened/group-id forms need the workgroup
1449
+ * size regardless of declaration order.
1450
+ *
1451
+ * @param {object} state Emit state.
1452
+ * @param {object} instruction `dcl_input` instruction with a thread-id-kind operand.
1123
1453
  */
1124
1454
  _declareComputeInput(state, instruction) {
1125
1455
  const kind = instruction.declaration.operandTypeName;
@@ -1144,13 +1474,13 @@ class DxbcGlslEmitter {
1144
1474
  }
1145
1475
  }
1146
1476
 
1147
- /**
1148
- * Emits the shared `gl_FragCoord`-derived thread-id prelude for map-style
1149
- * compute shaders: a `cjsDispatchOrigin` uniform (so a dispatch can be
1150
- * tiled across more than one draw/viewport), then only the intermediate
1151
- * and final locals this shader actually declared inputs for.
1152
- *
1153
- * @param {object} state Emit state.
1477
+ /**
1478
+ * Emits the shared `gl_FragCoord`-derived thread-id prelude for map-style
1479
+ * compute shaders: a `cjsDispatchOrigin` uniform (so a dispatch can be
1480
+ * tiled across more than one draw/viewport), then only the intermediate
1481
+ * and final locals this shader actually declared inputs for.
1482
+ *
1483
+ * @param {object} state Emit state.
1154
1484
  */
1155
1485
  _emitComputeInputPrelude(state) {
1156
1486
  const used = state.computeInputsUsed;
@@ -1185,12 +1515,12 @@ class DxbcGlslEmitter {
1185
1515
  }
1186
1516
  }
1187
1517
 
1188
- /**
1189
- * Reads the workgroup size recorded by `dcl_thread_group`, required to
1190
- * split the flat thread id back into group id / id-in-group components.
1191
- *
1192
- * @param {object} state Emit state.
1193
- * @returns {[number,number,number]} Workgroup size.
1518
+ /**
1519
+ * Reads the workgroup size recorded by `dcl_thread_group`, required to
1520
+ * split the flat thread id back into group id / id-in-group components.
1521
+ *
1522
+ * @param {object} state Emit state.
1523
+ * @returns {[number,number,number]} Workgroup size.
1194
1524
  */
1195
1525
  _threadGroupSizeOrThrow(state) {
1196
1526
  if (!Array.isArray(state.threadGroup)) {
@@ -1201,12 +1531,12 @@ class DxbcGlslEmitter {
1201
1531
  return state.threadGroup;
1202
1532
  }
1203
1533
 
1204
- /**
1205
- * Declares one stage output and records its backend identity.
1206
- *
1207
- * @param {object} state Mutable emission state.
1208
- * @param {object} instruction Decoded output declaration.
1209
- * @private
1534
+ /**
1535
+ * Declares one stage output and records its backend identity.
1536
+ *
1537
+ * @param {object} state Mutable emission state.
1538
+ * @param {object} instruction Decoded output declaration.
1539
+ * @private
1210
1540
  */
1211
1541
  _declareOutput(state, instruction) {
1212
1542
  const declaration = instruction.declaration;
@@ -1264,12 +1594,12 @@ class DxbcGlslEmitter {
1264
1594
  }
1265
1595
  }
1266
1596
 
1267
- /**
1268
- * Materializes an immediate constant-buffer declaration.
1269
- *
1270
- * @param {object} state Mutable emission state.
1271
- * @param {object} instruction Decoded custom-data declaration.
1272
- * @private
1597
+ /**
1598
+ * Materializes an immediate constant-buffer declaration.
1599
+ *
1600
+ * @param {object} state Mutable emission state.
1601
+ * @param {object} instruction Decoded custom-data declaration.
1602
+ * @private
1273
1603
  */
1274
1604
  _declareImmediateConstantBuffer(state, instruction) {
1275
1605
  const rows = instruction.customData?.immediateConstantBuffer;
@@ -1280,11 +1610,11 @@ class DxbcGlslEmitter {
1280
1610
  state.declarationLines.push(`const vec4 ${state.icbName}[${rows.length}] = vec4[${rows.length}](`, ...literals.map((literal, index) => ` ${literal}${index < literals.length - 1 ? "," : ""}`), ");");
1281
1611
  }
1282
1612
 
1283
- /**
1284
- * Initializes paired vertex outputs that the source shader does not write.
1285
- *
1286
- * @param {object} state Mutable emission state.
1287
- * @private
1613
+ /**
1614
+ * Initializes paired vertex outputs that the source shader does not write.
1615
+ *
1616
+ * @param {object} state Mutable emission state.
1617
+ * @private
1288
1618
  */
1289
1619
  _zeroFillUnwrittenOutputs(state) {
1290
1620
  const written = new Set();
@@ -1309,12 +1639,12 @@ class DxbcGlslEmitter {
1309
1639
  // Instructions
1310
1640
  // ------------------------------------------------------------------
1311
1641
 
1312
- /**
1313
- * Dispatches one decoded instruction to its GLSL lowering rule.
1314
- *
1315
- * @param {object} state Mutable emission state.
1316
- * @param {object} instruction Decoded DXBC instruction.
1317
- * @private
1642
+ /**
1643
+ * Dispatches one decoded instruction to its GLSL lowering rule.
1644
+ *
1645
+ * @param {object} state Mutable emission state.
1646
+ * @param {object} instruction Decoded DXBC instruction.
1647
+ * @private
1318
1648
  */
1319
1649
  _emitInstruction(state, instruction) {
1320
1650
  const lower = DxbcGlslEmitter.LOWERINGS[instruction.opcodeName];
@@ -1328,23 +1658,23 @@ class DxbcGlslEmitter {
1328
1658
  lower.call(this, state, instruction);
1329
1659
  }
1330
1660
 
1331
- /**
1332
- * Appends one correctly indented line to the emitted function body.
1333
- *
1334
- * @param {object} state Mutable emission state.
1335
- * @param {string} text GLSL source line.
1336
- * @private
1661
+ /**
1662
+ * Appends one correctly indented line to the emitted function body.
1663
+ *
1664
+ * @param {object} state Mutable emission state.
1665
+ * @param {string} text GLSL source line.
1666
+ * @private
1337
1667
  */
1338
1668
  _line(state, text) {
1339
1669
  state.bodyLines.push(`${" ".repeat(state.indent)}${text}`);
1340
1670
  }
1341
1671
 
1342
- /**
1343
- * Counts the active components in a DXBC mask.
1344
- *
1345
- * @param {number} mask DXBC component bit mask.
1346
- * @returns {number} Active component count.
1347
- * @private
1672
+ /**
1673
+ * Counts the active components in a DXBC mask.
1674
+ *
1675
+ * @param {number} mask DXBC component bit mask.
1676
+ * @returns {number} Active component count.
1677
+ * @private
1348
1678
  */
1349
1679
  _maskWidth(mask) {
1350
1680
  let width = 0;
@@ -1354,51 +1684,51 @@ class DxbcGlslEmitter {
1354
1684
  return width || 4;
1355
1685
  }
1356
1686
 
1357
- /**
1358
- * Converts a DXBC component bit mask to GLSL swizzle characters.
1359
- *
1360
- * @param {number} mask DXBC component bit mask.
1361
- * @returns {string} GLSL component string.
1362
- * @private
1687
+ /**
1688
+ * Converts a DXBC component bit mask to GLSL swizzle characters.
1689
+ *
1690
+ * @param {number} mask DXBC component bit mask.
1691
+ * @returns {string} GLSL component string.
1692
+ * @private
1363
1693
  */
1364
1694
  _maskChars(mask) {
1365
1695
  return COMPONENTS.filter((_, bit) => mask & 1 << bit).join("") || "xyzw";
1366
1696
  }
1367
1697
 
1368
- /**
1369
- * Builds the lvalue text for one register-space destination component,
1370
- * remapped into the target variable's declared component space.
1698
+ /**
1699
+ * Builds the lvalue text for one register-space destination component,
1700
+ * remapped into the target variable's declared component space.
1371
1701
  */
1372
1702
  _destComponentRef(state, destOperand, target, component) {
1373
1703
  const remapped = state.formatter.remapComponents(destOperand, component);
1374
1704
  return `${target.ref}${remapped ? `.${remapped}` : ""}`;
1375
1705
  }
1376
1706
 
1377
- /**
1378
- * Formats a resolved destination reference and optional mask.
1379
- *
1380
- * @param {object} target Resolved destination.
1381
- * @returns {string} GLSL lvalue text.
1382
- * @private
1707
+ /**
1708
+ * Formats a resolved destination reference and optional mask.
1709
+ *
1710
+ * @param {object} target Resolved destination.
1711
+ * @returns {string} GLSL lvalue text.
1712
+ * @private
1383
1713
  */
1384
1714
  _destText(target) {
1385
1715
  return `${target.ref}${target.mask ? `.${target.mask}` : ""}`;
1386
1716
  }
1387
1717
 
1388
- /**
1389
- * Selects the GLSL floating scalar or vector type for a width.
1390
- *
1391
- * @param {number} width Component width.
1392
- * @returns {string} GLSL type name.
1393
- * @private
1718
+ /**
1719
+ * Selects the GLSL floating scalar or vector type for a width.
1720
+ *
1721
+ * @param {number} width Component width.
1722
+ * @returns {string} GLSL type name.
1723
+ * @private
1394
1724
  */
1395
1725
  _vecType(width) {
1396
1726
  return width === 1 ? "float" : `vec${width}`;
1397
1727
  }
1398
1728
 
1399
- /**
1400
- * Reads a source at the destination mask's width, wrapping in a matching
1401
- * constructor so intrinsic arguments always type-check exactly.
1729
+ /**
1730
+ * Reads a source at the destination mask's width, wrapping in a matching
1731
+ * constructor so intrinsic arguments always type-check exactly.
1402
1732
  */
1403
1733
  _vecArg(state, operand, destMask, as = "float") {
1404
1734
  const expression = state.formatter.sourceExpression(operand, {
@@ -1416,14 +1746,14 @@ class DxbcGlslEmitter {
1416
1746
  return `${ctor}(${expression})`;
1417
1747
  }
1418
1748
 
1419
- /**
1420
- * Resolves a destination operand and its register-space component mask.
1421
- *
1422
- * @param {object} state Mutable emission state.
1423
- * @param {object} instruction Decoded DXBC instruction.
1424
- * @param {number} [operandIndex=0] Destination operand index.
1425
- * @returns {{target: object|null, mask: string}} Destination and mask.
1426
- * @private
1749
+ /**
1750
+ * Resolves a destination operand and its register-space component mask.
1751
+ *
1752
+ * @param {object} state Mutable emission state.
1753
+ * @param {object} instruction Decoded DXBC instruction.
1754
+ * @param {number} [operandIndex=0] Destination operand index.
1755
+ * @returns {{target: object|null, mask: string}} Destination and mask.
1756
+ * @private
1427
1757
  */
1428
1758
  _destMask(state, instruction, operandIndex = 0) {
1429
1759
  const target = state.formatter.destination(instruction.operands[operandIndex]);
@@ -1436,25 +1766,25 @@ class DxbcGlslEmitter {
1436
1766
  };
1437
1767
  }
1438
1768
 
1439
- /**
1440
- * Tests whether an operand produces one scalar component.
1441
- *
1442
- * @param {object} operand Decoded DXBC operand.
1443
- * @returns {boolean} True when the source is scalar.
1444
- * @private
1769
+ /**
1770
+ * Tests whether an operand produces one scalar component.
1771
+ *
1772
+ * @param {object} operand Decoded DXBC operand.
1773
+ * @returns {boolean} True when the source is scalar.
1774
+ * @private
1445
1775
  */
1446
1776
  _isScalarSource(operand) {
1447
1777
  return operand.selectionModeName === "select1" || operand.type === 4 && operand.componentCount === 1 || operand.componentCount === 1;
1448
1778
  }
1449
1779
 
1450
- /**
1451
- * Emits an assignment and applies instruction saturation when required.
1452
- *
1453
- * @param {object} state Mutable emission state.
1454
- * @param {object} instruction Decoded DXBC instruction.
1455
- * @param {string} valueExpression GLSL right-hand expression.
1456
- * @param {object} [options={}] Assignment formatting options.
1457
- * @private
1780
+ /**
1781
+ * Emits an assignment and applies instruction saturation when required.
1782
+ *
1783
+ * @param {object} state Mutable emission state.
1784
+ * @param {object} instruction Decoded DXBC instruction.
1785
+ * @param {string} valueExpression GLSL right-hand expression.
1786
+ * @param {object} [options={}] Assignment formatting options.
1787
+ * @private
1458
1788
  */
1459
1789
  _assign(state, instruction, valueExpression, options = {}) {
1460
1790
  const statement = state.formatter.assignment(instruction.operands[0], valueExpression, options);
@@ -1470,13 +1800,13 @@ class DxbcGlslEmitter {
1470
1800
  }
1471
1801
  }
1472
1802
 
1473
- /**
1474
- * Emits a width-correct floating binary infix operation.
1475
- *
1476
- * @param {object} state Mutable emission state.
1477
- * @param {object} instruction Decoded DXBC instruction.
1478
- * @param {string} operator GLSL infix operator.
1479
- * @private
1803
+ /**
1804
+ * Emits a width-correct floating binary infix operation.
1805
+ *
1806
+ * @param {object} state Mutable emission state.
1807
+ * @param {object} instruction Decoded DXBC instruction.
1808
+ * @param {string} operator GLSL infix operator.
1809
+ * @private
1480
1810
  */
1481
1811
  _infixBinary(state, instruction, operator) {
1482
1812
  const {
@@ -1492,15 +1822,15 @@ class DxbcGlslEmitter {
1492
1822
  this._assignWidened(state, instruction, mask, `${a} ${operator} ${b}`, width === 1);
1493
1823
  }
1494
1824
 
1495
- /**
1496
- * Widens a scalar result when assigning it to a vector destination.
1497
- *
1498
- * @param {object} state Mutable emission state.
1499
- * @param {object} instruction Decoded DXBC instruction.
1500
- * @param {string} mask Destination component mask.
1501
- * @param {string} expression GLSL expression.
1502
- * @param {boolean} rhsIsScalar Whether the expression is scalar.
1503
- * @private
1825
+ /**
1826
+ * Widens a scalar result when assigning it to a vector destination.
1827
+ *
1828
+ * @param {object} state Mutable emission state.
1829
+ * @param {object} instruction Decoded DXBC instruction.
1830
+ * @param {string} mask Destination component mask.
1831
+ * @param {string} expression GLSL expression.
1832
+ * @param {boolean} rhsIsScalar Whether the expression is scalar.
1833
+ * @private
1504
1834
  */
1505
1835
  _assignWidened(state, instruction, mask, expression, rhsIsScalar) {
1506
1836
  const value = rhsIsScalar && mask.length > 1 ? `vec${mask.length}(${expression})` : expression;
@@ -1509,13 +1839,13 @@ class DxbcGlslEmitter {
1509
1839
  });
1510
1840
  }
1511
1841
 
1512
- /**
1513
- * Emits a unary GLSL intrinsic with destination-width handling.
1514
- *
1515
- * @param {object} state Mutable emission state.
1516
- * @param {object} instruction Decoded DXBC instruction.
1517
- * @param {string} glslName GLSL intrinsic name.
1518
- * @private
1842
+ /**
1843
+ * Emits a unary GLSL intrinsic with destination-width handling.
1844
+ *
1845
+ * @param {object} state Mutable emission state.
1846
+ * @param {object} instruction Decoded DXBC instruction.
1847
+ * @param {string} glslName GLSL intrinsic name.
1848
+ * @private
1519
1849
  */
1520
1850
  _helperUnary(state, instruction, glslName) {
1521
1851
  const {
@@ -1527,13 +1857,13 @@ class DxbcGlslEmitter {
1527
1857
  this._assignWidened(state, instruction, mask, `${glslName}(${src})`, state.formatter.expressionWidth(instruction.operands[1], mask) === 1);
1528
1858
  }
1529
1859
 
1530
- /**
1531
- * Emits a binary GLSL intrinsic with width-matched arguments.
1532
- *
1533
- * @param {object} state Mutable emission state.
1534
- * @param {object} instruction Decoded DXBC instruction.
1535
- * @param {string} glslName GLSL intrinsic name.
1536
- * @private
1860
+ /**
1861
+ * Emits a binary GLSL intrinsic with width-matched arguments.
1862
+ *
1863
+ * @param {object} state Mutable emission state.
1864
+ * @param {object} instruction Decoded DXBC instruction.
1865
+ * @param {string} glslName GLSL intrinsic name.
1866
+ * @private
1537
1867
  */
1538
1868
  _helperBinary(state, instruction, glslName) {
1539
1869
  const {
@@ -1546,13 +1876,13 @@ class DxbcGlslEmitter {
1546
1876
  });
1547
1877
  }
1548
1878
 
1549
- /**
1550
- * Emits a dot product and replicates its scalar result when necessary.
1551
- *
1552
- * @param {object} state Mutable emission state.
1553
- * @param {object} instruction Decoded DXBC instruction.
1554
- * @param {string} sourceMask Source component mask.
1555
- * @private
1879
+ /**
1880
+ * Emits a dot product and replicates its scalar result when necessary.
1881
+ *
1882
+ * @param {object} state Mutable emission state.
1883
+ * @param {object} instruction Decoded DXBC instruction.
1884
+ * @param {string} sourceMask Source component mask.
1885
+ * @private
1556
1886
  */
1557
1887
  _dotProduct(state, instruction, sourceMask) {
1558
1888
  const {
@@ -1566,16 +1896,16 @@ class DxbcGlslEmitter {
1566
1896
  });
1567
1897
  }
1568
1898
 
1569
- /**
1570
- * Emits scalar or vector comparison bits in the float register model.
1571
- *
1572
- * @param {object} state Mutable emission state.
1573
- * @param {object} instruction Decoded DXBC instruction.
1574
- * @param {object} options Comparison options.
1575
- * @param {string} options.intrinsic GLSL vector comparison intrinsic.
1576
- * @param {string} options.operator GLSL scalar comparison operator.
1577
- * @param {"float"|"int"|"uint"} options.as Operand interpretation.
1578
- * @private
1899
+ /**
1900
+ * Emits scalar or vector comparison bits in the float register model.
1901
+ *
1902
+ * @param {object} state Mutable emission state.
1903
+ * @param {object} instruction Decoded DXBC instruction.
1904
+ * @param {object} options Comparison options.
1905
+ * @param {string} options.intrinsic GLSL vector comparison intrinsic.
1906
+ * @param {string} options.operator GLSL scalar comparison operator.
1907
+ * @param {"float"|"int"|"uint"} options.as Operand interpretation.
1908
+ * @private
1579
1909
  */
1580
1910
  _comparison(state, instruction, {
1581
1911
  intrinsic,
@@ -1602,13 +1932,13 @@ class DxbcGlslEmitter {
1602
1932
  this._assign(state, instruction, `uintBitsToFloat(uvec${mask.length}(${intrinsic}(${a}, ${b})) * 0xFFFFFFFFu)`);
1603
1933
  }
1604
1934
 
1605
- /**
1606
- * Emits an unsigned bitwise operation through register bitcasts.
1607
- *
1608
- * @param {object} state Mutable emission state.
1609
- * @param {object} instruction Decoded DXBC instruction.
1610
- * @param {string} operator GLSL bitwise operator.
1611
- * @private
1935
+ /**
1936
+ * Emits an unsigned bitwise operation through register bitcasts.
1937
+ *
1938
+ * @param {object} state Mutable emission state.
1939
+ * @param {object} instruction Decoded DXBC instruction.
1940
+ * @param {string} operator GLSL bitwise operator.
1941
+ * @private
1612
1942
  */
1613
1943
  _bitwiseBinary(state, instruction, operator) {
1614
1944
  const {
@@ -1619,14 +1949,14 @@ class DxbcGlslEmitter {
1619
1949
  this._assign(state, instruction, `uintBitsToFloat(${a} ${operator} ${b})`);
1620
1950
  }
1621
1951
 
1622
- /**
1623
- * Emits a signed or unsigned integer binary operation through bitcasts.
1624
- *
1625
- * @param {object} state Mutable emission state.
1626
- * @param {object} instruction Decoded DXBC instruction.
1627
- * @param {string} operator GLSL integer operator.
1628
- * @param {"int"|"uint"} [as="int"] Integer interpretation.
1629
- * @private
1952
+ /**
1953
+ * Emits a signed or unsigned integer binary operation through bitcasts.
1954
+ *
1955
+ * @param {object} state Mutable emission state.
1956
+ * @param {object} instruction Decoded DXBC instruction.
1957
+ * @param {string} operator GLSL integer operator.
1958
+ * @param {"int"|"uint"} [as="int"] Integer interpretation.
1959
+ * @private
1630
1960
  */
1631
1961
  _intBinary(state, instruction, operator, as = "int") {
1632
1962
  const {
@@ -1638,13 +1968,13 @@ class DxbcGlslEmitter {
1638
1968
  this._assign(state, instruction, `${wrap}(${a} ${operator} ${b})`);
1639
1969
  }
1640
1970
 
1641
- /**
1642
- * Formats the DXBC zero or nonzero control-flow condition.
1643
- *
1644
- * @param {object} state Mutable emission state.
1645
- * @param {object} instruction Decoded DXBC instruction.
1646
- * @returns {string} GLSL boolean expression.
1647
- * @private
1971
+ /**
1972
+ * Formats the DXBC zero or nonzero control-flow condition.
1973
+ *
1974
+ * @param {object} state Mutable emission state.
1975
+ * @param {object} instruction Decoded DXBC instruction.
1976
+ * @returns {string} GLSL boolean expression.
1977
+ * @private
1648
1978
  */
1649
1979
  _condition(state, instruction) {
1650
1980
  const scalar = state.formatter.sourceExpression(instruction.operands[0], {
@@ -1654,12 +1984,12 @@ class DxbcGlslEmitter {
1654
1984
  return `floatBitsToUint(${scalar}) ${test} 0u`;
1655
1985
  }
1656
1986
 
1657
- /**
1658
- * Emits component-wise conditional moves without aliasing destinations.
1659
- *
1660
- * @param {object} state Mutable emission state.
1661
- * @param {object} instruction Decoded DXBC instruction.
1662
- * @private
1987
+ /**
1988
+ * Emits component-wise conditional moves without aliasing destinations.
1989
+ *
1990
+ * @param {object} state Mutable emission state.
1991
+ * @param {object} instruction Decoded DXBC instruction.
1992
+ * @private
1663
1993
  */
1664
1994
  _movc(state, instruction) {
1665
1995
  const destOperand = instruction.operands[0];
@@ -1699,12 +2029,12 @@ class DxbcGlslEmitter {
1699
2029
  }
1700
2030
  }
1701
2031
 
1702
- /**
1703
- * Emits the paired sine and cosine destinations in alias-safe order.
1704
- *
1705
- * @param {object} state Mutable emission state.
1706
- * @param {object} instruction Decoded DXBC instruction.
1707
- * @private
2032
+ /**
2033
+ * Emits the paired sine and cosine destinations in alias-safe order.
2034
+ *
2035
+ * @param {object} state Mutable emission state.
2036
+ * @param {object} instruction Decoded DXBC instruction.
2037
+ * @private
1708
2038
  */
1709
2039
  _sincos(state, instruction) {
1710
2040
  const [sinDest, cosDest, angle] = instruction.operands;
@@ -1732,20 +2062,20 @@ class DxbcGlslEmitter {
1732
2062
  }
1733
2063
  }
1734
2064
 
1735
- /**
1736
- * Declares the merged detail-map array, once, on the first member seen.
1737
- *
1738
- * Each member keeps its own entry in `resourceNames` pointing at the shared
1739
- * array, so every existing reference site resolves to it without knowing a
1740
- * merge happened. The declared dimension deliberately stays 2D: the layer is
1741
- * appended to the coordinate at each sample site rather than read out of the
1742
- * shader's own operand, which has no third component to give.
1743
- *
1744
- * @param {object} state Mutable emission state.
1745
- * @param {number} register Resource register being declared.
1746
- * @param {object} declaration Decoded resource declaration.
1747
- * @param {Set<number>|null} comparisonSamplers Comparison samplers, when any.
1748
- * @private
2065
+ /**
2066
+ * Declares the merged detail-map array, once, on the first member seen.
2067
+ *
2068
+ * Each member keeps its own entry in `resourceNames` pointing at the shared
2069
+ * array, so every existing reference site resolves to it without knowing a
2070
+ * merge happened. The declared dimension deliberately stays 2D: the layer is
2071
+ * appended to the coordinate at each sample site rather than read out of the
2072
+ * shader's own operand, which has no third component to give.
2073
+ *
2074
+ * @param {object} state Mutable emission state.
2075
+ * @param {number} register Resource register being declared.
2076
+ * @param {object} declaration Decoded resource declaration.
2077
+ * @param {Set<number>|null} comparisonSamplers Comparison samplers, when any.
2078
+ * @private
1749
2079
  */
1750
2080
  _declareDetailArrayMap(state, register, declaration, comparisonSamplers) {
1751
2081
  // A comparison-sampled or non-2D detail map is not the family the
@@ -1761,6 +2091,15 @@ class DxbcGlslEmitter {
1761
2091
  state.resourceNames.set(register, DETAIL_MAP_ARRAY_SYMBOL);
1762
2092
  if (state.detailMapArrayDeclared) return;
1763
2093
  state.detailMapArrayDeclared = true;
2094
+
2095
+ // One GL uniform now stands for every merged layer, so it can carry only
2096
+ // one sampler state. Union the layers' pairings: if they disagree the
2097
+ // merge itself was invalid, and the packaging layer is where that is
2098
+ // detectable, because only it holds the sampler values to compare.
2099
+ const merged = new Set();
2100
+ for (const layerRegister of state.detailMapArrayLayers.keys()) {
2101
+ for (const sampler of state.pairedSamplers.get(layerRegister) ?? []) merged.add(sampler);
2102
+ }
1764
2103
  state.declarationLines.push(`uniform mediump sampler2DArray ${DETAIL_MAP_ARRAY_SYMBOL};`);
1765
2104
  state.bindings.push({
1766
2105
  kind: "resource",
@@ -1769,35 +2108,38 @@ class DxbcGlslEmitter {
1769
2108
  samplerType: "sampler2DArray",
1770
2109
  dimensionName: "texture2darray",
1771
2110
  arrayLayerCount: state.detailMapArrayLayers.size,
1772
- mergedFrom: [...state.detailMapArrayLayers.keys()]
2111
+ mergedFrom: [...state.detailMapArrayLayers.keys()],
2112
+ ...(merged.size ? {
2113
+ pairedSamplerRegisters: [...merged].sort((a, b) => a - b)
2114
+ } : {})
1773
2115
  });
1774
2116
  }
1775
2117
 
1776
- /**
1777
- * Returns the array layer a texture operand maps to, when it was merged.
1778
- *
1779
- * @param {object} state Mutable emission state.
1780
- * @param {object} texOperand Texture resource operand.
1781
- * @returns {number|null} Layer index, or null when the register was not merged.
1782
- * @private
2118
+ /**
2119
+ * Returns the array layer a texture operand maps to, when it was merged.
2120
+ *
2121
+ * @param {object} state Mutable emission state.
2122
+ * @param {object} texOperand Texture resource operand.
2123
+ * @returns {number|null} Layer index, or null when the register was not merged.
2124
+ * @private
1783
2125
  */
1784
2126
  _detailMapArrayLayer(state, texOperand) {
1785
2127
  const layer = state.detailMapArrayLayers.get(texOperand.registerIndex);
1786
2128
  return layer === undefined ? null : layer;
1787
2129
  }
1788
2130
 
1789
- /**
1790
- * Refuses an operation that cannot be redirected at an array layer.
1791
- *
1792
- * Recognising the detail family proves the resources are mergeable; it does
1793
- * not prove every *use* is. A texel fetch or a size query against a merged
1794
- * register would silently mean something different once the register became
1795
- * one layer of an array, so those fail the build instead.
1796
- *
1797
- * @param {object} state Mutable emission state.
1798
- * @param {object} instruction Decoded DXBC instruction.
1799
- * @param {object} texOperand Texture resource operand.
1800
- * @private
2131
+ /**
2132
+ * Refuses an operation that cannot be redirected at an array layer.
2133
+ *
2134
+ * Recognising the detail family proves the resources are mergeable; it does
2135
+ * not prove every *use* is. A texel fetch or a size query against a merged
2136
+ * register would silently mean something different once the register became
2137
+ * one layer of an array, so those fail the build instead.
2138
+ *
2139
+ * @param {object} state Mutable emission state.
2140
+ * @param {object} instruction Decoded DXBC instruction.
2141
+ * @param {object} texOperand Texture resource operand.
2142
+ * @private
1801
2143
  */
1802
2144
  _rejectDetailArrayMapUse(state, instruction, texOperand) {
1803
2145
  if (!state.detailMapArrayLayers.has(texOperand.registerIndex)) return;
@@ -1808,14 +2150,14 @@ class DxbcGlslEmitter {
1808
2150
  });
1809
2151
  }
1810
2152
 
1811
- /**
1812
- * Resolves and validates the declared dimension of a texture operand.
1813
- *
1814
- * @param {object} state Mutable emission state.
1815
- * @param {object} instruction Decoded DXBC instruction.
1816
- * @param {object} texOperand Texture resource operand.
1817
- * @returns {number} DXBC resource-dimension code.
1818
- * @private
2153
+ /**
2154
+ * Resolves and validates the declared dimension of a texture operand.
2155
+ *
2156
+ * @param {object} state Mutable emission state.
2157
+ * @param {object} instruction Decoded DXBC instruction.
2158
+ * @param {object} texOperand Texture resource operand.
2159
+ * @returns {number} DXBC resource-dimension code.
2160
+ * @private
1819
2161
  */
1820
2162
  _resourceDimension(state, instruction, texOperand) {
1821
2163
  const dimension = state.resourceDimensions.get(texOperand.registerIndex);
@@ -1829,18 +2171,18 @@ class DxbcGlslEmitter {
1829
2171
  return dimension;
1830
2172
  }
1831
2173
 
1832
- /**
1833
- * Emits a regular, LOD, bias, gradient, or comparison texture sample.
1834
- *
1835
- * @param {object} state Mutable emission state.
1836
- * @param {object} instruction Decoded DXBC texture instruction.
1837
- * @param {object} [options={}] Sampling-mode operand indexes.
1838
- * @param {number|null} [options.lodOperandIndex=null] Explicit LOD operand.
1839
- * @param {number|null} [options.biasOperandIndex=null] LOD bias operand.
1840
- * @param {number[]|null} [options.gradOperandIndexes=null] Gradient operands.
1841
- * @param {number|null} [options.comparisonRefOperandIndex=null] Comparison reference operand.
1842
- * @param {boolean} [options.forceLodZero=false] Whether to force the LOD-zero adaptation.
1843
- * @private
2174
+ /**
2175
+ * Emits a regular, LOD, bias, gradient, or comparison texture sample.
2176
+ *
2177
+ * @param {object} state Mutable emission state.
2178
+ * @param {object} instruction Decoded DXBC texture instruction.
2179
+ * @param {object} [options={}] Sampling-mode operand indexes.
2180
+ * @param {number|null} [options.lodOperandIndex=null] Explicit LOD operand.
2181
+ * @param {number|null} [options.biasOperandIndex=null] LOD bias operand.
2182
+ * @param {number[]|null} [options.gradOperandIndexes=null] Gradient operands.
2183
+ * @param {number|null} [options.comparisonRefOperandIndex=null] Comparison reference operand.
2184
+ * @param {boolean} [options.forceLodZero=false] Whether to force the LOD-zero adaptation.
2185
+ * @private
1844
2186
  */
1845
2187
  _textureSample(state, instruction, {
1846
2188
  lodOperandIndex = null,
@@ -1948,17 +2290,18 @@ class DxbcGlslEmitter {
1948
2290
  } else {
1949
2291
  call = `texture(${texName}, ${coord})`;
1950
2292
  }
2293
+ call = this._applyEmulatedAddressing(state, instruction, call, coord, coordMask);
1951
2294
  this._assign(state, instruction, `${call}.${returnSwizzle}`, {
1952
2295
  saturate: instruction.saturate
1953
2296
  });
1954
2297
  }
1955
2298
 
1956
- /**
1957
- * Emits an integer-coordinate texel fetch for a declared resource.
1958
- *
1959
- * @param {object} state Mutable emission state.
1960
- * @param {object} instruction Decoded DXBC load instruction.
1961
- * @private
2299
+ /**
2300
+ * Emits an integer-coordinate texel fetch for a declared resource.
2301
+ *
2302
+ * @param {object} state Mutable emission state.
2303
+ * @param {object} instruction Decoded DXBC load instruction.
2304
+ * @private
1962
2305
  */
1963
2306
  _texelFetch(state, instruction) {
1964
2307
  const {
@@ -1994,9 +2337,9 @@ class DxbcGlslEmitter {
1994
2337
  }
1995
2338
  }
1996
2339
 
1997
- /**
1998
- * Per-opcode lowering table. Every rule cites its family spec in
1999
- * `docs/dxbc-lowering/`; templates are HLSLcc-derived, see each doc.
2340
+ /**
2341
+ * Per-opcode lowering table. Every rule cites its family spec in
2342
+ * `docs/dxbc-lowering/`; templates are HLSLcc-derived, see each doc.
2000
2343
  */
2001
2344
  DxbcGlslEmitter.LOWERINGS = {
2002
2345
  // --- float-alu ---
@@ -2571,11 +2914,11 @@ DxbcGlslEmitter.LOWERINGS = {
2571
2914
  }
2572
2915
  };
2573
2916
 
2574
- /**
2575
- * `ld_structured` lowering. Vertex UBO path reads `name.data[row]` vec4 rows;
2576
- * pixel data-texture path reads RGBA32UI texels (4 dwords per texel, fixed
2577
- * 2048-texel row width). 16-byte-aligned reads use one static channel per
2578
- * component; unaligned reads fall back to per-dword dynamic addressing.
2917
+ /**
2918
+ * `ld_structured` lowering. Vertex UBO path reads `name.data[row]` vec4 rows;
2919
+ * pixel data-texture path reads RGBA32UI texels (4 dwords per texel, fixed
2920
+ * 2048-texel row width). 16-byte-aligned reads use one static channel per
2921
+ * component; unaligned reads fall back to per-dword dynamic addressing.
2579
2922
  */
2580
2923
  DxbcGlslEmitter.prototype._ldStructured = function _ldStructured(state, instruction) {
2581
2924
  const target = state.formatter.destination(instruction.operands[0]);
@@ -2654,10 +2997,10 @@ DxbcGlslEmitter.prototype._ldStructured = function _ldStructured(state, instruct
2654
2997
  }
2655
2998
  };
2656
2999
 
2657
- /**
2658
- * `resinfo` lowering: width/height/depth via textureSize per live destination
2659
- * component; the 4th (mip-count) channel has no WebGL2 equivalent and throws
2660
- * only when a shader actually reads it.
3000
+ /**
3001
+ * `resinfo` lowering: width/height/depth via textureSize per live destination
3002
+ * component; the 4th (mip-count) channel has no WebGL2 equivalent and throws
3003
+ * only when a shader actually reads it.
2661
3004
  */
2662
3005
  DxbcGlslEmitter.prototype._resinfo = function _resinfo(state, instruction) {
2663
3006
  const {
@@ -2701,8 +3044,8 @@ DxbcGlslEmitter.prototype._resinfo = function _resinfo(state, instruction) {
2701
3044
  }
2702
3045
  };
2703
3046
 
2704
- /**
2705
- * Shared int/uint min-max lowering used by imax/imin/umax/umin.
3047
+ /**
3048
+ * Shared int/uint min-max lowering used by imax/imin/umax/umin.
2706
3049
  */
2707
3050
  DxbcGlslEmitter.prototype._intMinMax = function _intMinMax(state, instruction, fn, as) {
2708
3051
  const {
@@ -2714,16 +3057,16 @@ DxbcGlslEmitter.prototype._intMinMax = function _intMinMax(state, instruction, f
2714
3057
  this._assign(state, instruction, `${wrap}(${fn}(${a}, ${b}))`);
2715
3058
  };
2716
3059
 
2717
- /**
2718
- * `gather4` lowering: fetches one channel from each of the four texels a
2719
- * bilinear sample would blend, via the `hlslcc_textureGather4Emulated` /
2720
- * `hlslcc_textureGather4ArrayEmulated` helpers (GLSL ES 3.00 has no
2721
- * `textureGather`). Only 2D and 2D-array resources are supported — the
2722
- * dimensions the AO/blur/FSR/CAS map-style corpus this lowering targets is
2723
- * known to use (ASSAO gathers from its deinterleaved texture2darray slices).
2724
- *
2725
- * @param {object} state Emit state.
2726
- * @param {object} instruction `gather4` instruction.
3060
+ /**
3061
+ * `gather4` lowering: fetches one channel from each of the four texels a
3062
+ * bilinear sample would blend, via the `hlslcc_textureGather4Emulated` /
3063
+ * `hlslcc_textureGather4ArrayEmulated` helpers (GLSL ES 3.00 has no
3064
+ * `textureGather`). Only 2D and 2D-array resources are supported — the
3065
+ * dimensions the AO/blur/FSR/CAS map-style corpus this lowering targets is
3066
+ * known to use (ASSAO gathers from its deinterleaved texture2darray slices).
3067
+ *
3068
+ * @param {object} state Emit state.
3069
+ * @param {object} instruction `gather4` instruction.
2727
3070
  */
2728
3071
  DxbcGlslEmitter.prototype._gather4 = function _gather4(state, instruction) {
2729
3072
  const {
@@ -2757,13 +3100,13 @@ DxbcGlslEmitter.prototype._gather4 = function _gather4(state, instruction) {
2757
3100
  });
2758
3101
  };
2759
3102
 
2760
- /**
2761
- * Reads the gathered-channel selector `gather4`/`gather4_po` carry on their
2762
- * sampler operand (a 1-component swizzle; red/`x` is the default when no
2763
- * selection is encoded).
2764
- *
2765
- * @param {object} operand Decoded sampler operand.
2766
- * @returns {string} Channel letter (`x`/`y`/`z`/`w`).
3103
+ /**
3104
+ * Reads the gathered-channel selector `gather4`/`gather4_po` carry on their
3105
+ * sampler operand (a 1-component swizzle; red/`x` is the default when no
3106
+ * selection is encoded).
3107
+ *
3108
+ * @param {object} operand Decoded sampler operand.
3109
+ * @returns {string} Channel letter (`x`/`y`/`z`/`w`).
2767
3110
  */
2768
3111
  DxbcGlslEmitter.prototype._gather4Channel = function _gather4Channel(operand) {
2769
3112
  if (operand.selectionModeName === "select1" && operand.selected) {
@@ -2775,8 +3118,8 @@ DxbcGlslEmitter.prototype._gather4Channel = function _gather4Channel(operand) {
2775
3118
  return "x";
2776
3119
  };
2777
3120
 
2778
- /**
2779
- * Assembles the final GLSL text.
3121
+ /**
3122
+ * Assembles the final GLSL text.
2780
3123
  */
2781
3124
  DxbcGlslEmitter.prototype._assemble = function _assemble(state) {
2782
3125
  const lines = ["#version 300 es"];