@driftengine/core 4.8.0 → 4.8.2

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 (62) hide show
  1. package/CHANGELOG.json +102 -0
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +2 -0
  4. package/dist/input/actionMap.d.ts +6 -1
  5. package/dist/input/actionMap.js +20 -7
  6. package/dist/input/input.d.ts +28 -0
  7. package/dist/input/input.js +71 -9
  8. package/dist/input/inputHarness.d.ts +9 -0
  9. package/dist/input/inputHarness.js +34 -2
  10. package/dist/render/backend/webgl2/renderer.d.ts +52 -3
  11. package/dist/render/backend/webgl2/renderer.js +151 -5
  12. package/dist/render/backend/webgpu/buffers.d.ts +2 -0
  13. package/dist/render/backend/webgpu/buffers.js +2 -0
  14. package/dist/render/backend/webgpu/flatPass.js +16 -7
  15. package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
  16. package/dist/render/backend/webgpu/pipelineCache.js +76 -9
  17. package/dist/render/backend/webgpu/renderer.d.ts +56 -1
  18. package/dist/render/backend/webgpu/renderer.js +151 -26
  19. package/dist/render/mapsWithoutUvs.d.ts +24 -0
  20. package/dist/render/mapsWithoutUvs.js +24 -0
  21. package/dist/render/mesh.d.ts +2 -0
  22. package/dist/render/mesh.js +3 -0
  23. package/dist/render/particlePool.d.ts +15 -0
  24. package/dist/render/particlePool.js +72 -37
  25. package/dist/render/pass.d.ts +12 -0
  26. package/dist/render/renderQuality.d.ts +7 -6
  27. package/dist/render/shaders/flat/glassTint.d.ts +28 -0
  28. package/dist/render/shaders/flat/glassTint.js +31 -0
  29. package/dist/render/shaders/flat/index.d.ts +12 -0
  30. package/dist/render/shaders/flat/index.js +7 -1
  31. package/dist/render/shaders/flat/main.d.ts +1 -1
  32. package/dist/render/shaders/flat/main.js +60 -25
  33. package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
  34. package/dist/render/shaders/flat/surfaceEffects.js +6 -2
  35. package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
  36. package/dist/render/shaders/generated/flat.wgsl.js +102 -38
  37. package/dist/render/surfaceTexture.d.ts +2 -0
  38. package/dist/render/surfaceTexture.js +4 -0
  39. package/dist/render/viewModel.d.ts +29 -0
  40. package/dist/render/viewModel.js +35 -0
  41. package/package.json +6 -6
  42. package/src/index.ts +9 -1
  43. package/src/input/actionMap.ts +23 -8
  44. package/src/input/input.ts +79 -9
  45. package/src/input/inputHarness.ts +27 -2
  46. package/src/render/backend/webgl2/renderer.ts +166 -13
  47. package/src/render/backend/webgpu/buffers.ts +5 -0
  48. package/src/render/backend/webgpu/flatPass.ts +41 -25
  49. package/src/render/backend/webgpu/pipelineCache.ts +82 -8
  50. package/src/render/backend/webgpu/renderer.ts +151 -26
  51. package/src/render/mapsWithoutUvs.ts +43 -0
  52. package/src/render/mesh.ts +3 -0
  53. package/src/render/particlePool.ts +101 -54
  54. package/src/render/pass.ts +12 -0
  55. package/src/render/renderQuality.ts +7 -6
  56. package/src/render/shaders/flat/glassTint.ts +48 -0
  57. package/src/render/shaders/flat/index.ts +19 -1
  58. package/src/render/shaders/flat/main.ts +60 -25
  59. package/src/render/shaders/flat/surfaceEffects.ts +6 -2
  60. package/src/render/shaders/generated/flat.wgsl.ts +102 -38
  61. package/src/render/surfaceTexture.ts +5 -0
  62. package/src/render/viewModel.ts +41 -0
@@ -93,6 +93,9 @@ import { deviceSnappedCellSize } from '../../textLayout.js';
93
93
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
94
94
  import { InstancedBatch } from './instanced.js';
95
95
  import { cutoutOf } from '../../cutoutCaster.js';
96
+ import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
97
+ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
98
+ import { noLitSwitches } from '../../shaders/flat/glassTint.js';
96
99
  /**
97
100
  * How many frames `firstFrameSettled` will ask before it stops asking.
98
101
  *
@@ -961,6 +964,34 @@ export class WebGL2Renderer {
961
964
  * each such draw and handed back, where the other backend bakes it into a pipeline.
962
965
  */
963
966
  materialDoubleSided = false;
967
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
968
+ materialHasMaps = false;
969
+ /** The material last set, so a lit program rebuilt for glass can be given it. */
970
+ currentMaterial = null;
971
+ /**
972
+ * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
973
+ * profile and the content have asked for since (`litWanted`).
974
+ *
975
+ * **Each is off until it is used** — glass until a pane is offered, a fixture until one is loaded,
976
+ * the effects until a material carries a table, DriftLight until a volume is set — and then on for
977
+ * good, rebuilt at the next `beginFrame`. Clustering is the profile's. The code costs the lit pass
978
+ * registers on every surface whether it finds anything or not, and a world without the feature
979
+ * draws the same picture without it. `PipelineCache.litSwitches` makes the same decision on the
980
+ * other backend. The frame a feature is first used in draws without it; the rebuild is a
981
+ * synchronous compile, and the next frame waits for it.
982
+ */
983
+ litOn = noLitSwitches();
984
+ litWanted = noLitSwitches();
985
+ litAsked = false;
986
+ /** Ask for a switch, which the next `beginFrame` compiles in. Never glass with it turned off. */
987
+ askLit(feature) {
988
+ if (this.litWanted[feature])
989
+ return;
990
+ if (feature === 'GLASS_SHADOWS' && this.quality.glassShadows === 'off')
991
+ return;
992
+ this.litWanted[feature] = true;
993
+ this.litAsked = true;
994
+ }
964
995
  /**
965
996
  * The one sink handed to every caster enumeration.
966
997
  *
@@ -992,6 +1023,7 @@ export class WebGL2Renderer {
992
1023
  /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
993
1024
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
994
1025
  this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
1026
+ this.askLit('GLASS_SHADOWS');
995
1027
  }
996
1028
  return;
997
1029
  }
@@ -1037,6 +1069,7 @@ export class WebGL2Renderer {
1037
1069
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1038
1070
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1039
1071
  this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
1072
+ this.askLit('GLASS_SHADOWS');
1040
1073
  }
1041
1074
  return;
1042
1075
  }
@@ -1075,6 +1108,7 @@ export class WebGL2Renderer {
1075
1108
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1076
1109
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1077
1110
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
1111
+ this.askLit('GLASS_SHADOWS');
1078
1112
  }
1079
1113
  return;
1080
1114
  }
@@ -1538,6 +1572,9 @@ export class WebGL2Renderer {
1538
1572
  this.iesRows = this.iesAtlas.height;
1539
1573
  this.iesPlanes = this.iesAtlas.planes;
1540
1574
  this.cookieTiles = this.cookieImages.length;
1575
+ /* The first profile or cookie is when the lit programs start reading them. See `litOn`. */
1576
+ if (this.iesRows > 1 || this.cookieTiles > 0)
1577
+ this.askLit('LIGHT_FIXTURES');
1541
1578
  gl.bindTexture(gl.TEXTURE_2D, null);
1542
1579
  gl.activeTexture(gl.TEXTURE0);
1543
1580
  }
@@ -1896,6 +1933,9 @@ export class WebGL2Renderer {
1896
1933
  * Built for the profile this renderer resolved, not for every profile at once — so a
1897
1934
  * world with shadows off compiles none of the shadow path. See `shaders/flat.ts`.
1898
1935
  */
1936
+ /* The one switch the profile decides; the rest wait for their content. See `litOn`. */
1937
+ this.litOn.CLUSTERED_LIGHTS = this.quality.clusteredLights;
1938
+ this.litWanted.CLUSTERED_LIGHTS = this.quality.clusteredLights;
1899
1939
  const fitted = this.fitFlatProgram(gl);
1900
1940
  this.flatProgram = fitted.program;
1901
1941
  this.flatFragSource = fitted.source;
@@ -2410,6 +2450,8 @@ export class WebGL2Renderer {
2410
2450
  this.materials.dirty();
2411
2451
  this.currentSurfaceTexture = material?.albedo ?? null;
2412
2452
  this.materialDoubleSided = material?.doubleSided === true;
2453
+ this.materialHasMaps = materialHasMaps(material);
2454
+ this.currentMaterial = material;
2413
2455
  /*
2414
2456
  * Written to every flat program, because material state persists across draws and a skinned
2415
2457
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -2508,6 +2550,9 @@ export class WebGL2Renderer {
2508
2550
  }
2509
2551
  else {
2510
2552
  albedo.bindEffects(gl, SURFACE_EFFECTS_TEXTURE_UNIT, this.emptyTexture2D);
2553
+ /* The first table bound is when the lit programs start reading one. See `litOn`. */
2554
+ if (albedo.hasEffects)
2555
+ this.askLit('SURFACE_EFFECTS');
2511
2556
  }
2512
2557
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
2513
2558
  if (albedo === null) {
@@ -4132,16 +4177,76 @@ export class WebGL2Renderer {
4132
4177
  * rather than propagating. Only a shader that will not link at *any* budget throws, and it
4133
4178
  * throws a sentence with the device's numbers in it rather than a bare driver string.
4134
4179
  */
4135
- fitFlatProgram(gl) {
4136
- const variant = {
4180
+ /** The lit fragment source this renderer's profile compiles, at a light budget. */
4181
+ flatFragAt(budget) {
4182
+ return flatFrag({
4137
4183
  pointShadows: this.quality.pointShadows,
4138
4184
  directionalShadows: this.quality.directionalShadows,
4139
4185
  environmentProbe: this.probeCapture !== null,
4140
4186
  nightEmissive: this.quality.nightEmissive,
4141
- /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
4142
- glassShadows: this.quality.glassShadows !== 'off',
4187
+ /* Each off until it is used: see `litOn`. */
4188
+ glassShadows: this.litOn.GLASS_SHADOWS,
4189
+ clusteredLights: this.litOn.CLUSTERED_LIGHTS,
4190
+ lightFixtures: this.litOn.LIGHT_FIXTURES,
4191
+ surfaceEffects: this.litOn.SURFACE_EFFECTS,
4192
+ driftLight: this.litOn.DRIFT_LIGHT,
4193
+ maxLights: budget.maxLights,
4194
+ maxAreaLights: budget.maxAreaLights,
4195
+ });
4196
+ }
4197
+ /**
4198
+ * Every lit program that exists, compiled again with the switches asked for since, at the budget
4199
+ * the renderer already uploads for, and given the pass and the material the old one held.
4200
+ *
4201
+ * At `beginFrame`, not where a feature was first used: a pane is offered inside a shadow pass
4202
+ * with the depth program bound, and a program swapped under it would draw the next caster with
4203
+ * the lit one.
4204
+ */
4205
+ rebuildLit() {
4206
+ const { gl } = this;
4207
+ Object.assign(this.litOn, this.litWanted);
4208
+ this.flatFragSource = this.flatFragAt(this.lightBudget);
4209
+ const recompile = (old, skinned, morphed, instanced, label) => {
4210
+ if (old === null)
4211
+ return null;
4212
+ const program = compileProgram(gl, flatVert({ skinned, morphed, instanced }), this.flatFragSource, label);
4213
+ gl.deleteProgram(old);
4214
+ return { program, uniforms: uniformLocations(gl, program, label) };
4143
4215
  };
4144
- const build = (budget) => flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
4216
+ const plain = recompile(this.flatProgram, false, false, false, 'flat');
4217
+ if (plain !== null) {
4218
+ this.flatProgram = plain.program;
4219
+ this.flatUniforms = plain.uniforms;
4220
+ }
4221
+ const skinned = recompile(this.flatSkinnedProgram, true, false, false, 'flat.skinned');
4222
+ this.flatSkinnedProgram = skinned?.program ?? null;
4223
+ this.flatSkinnedUniforms = skinned?.uniforms ?? null;
4224
+ const morphed = recompile(this.flatMorphedProgram, false, true, false, 'flat.morphed');
4225
+ this.flatMorphedProgram = morphed?.program ?? null;
4226
+ this.flatMorphedUniforms = morphed?.uniforms ?? null;
4227
+ const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
4228
+ this.flatBothProgram = both?.program ?? null;
4229
+ this.flatBothUniforms = both?.uniforms ?? null;
4230
+ const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
4231
+ this.flatInstancedProgram = instanced?.program ?? null;
4232
+ this.flatInstancedUniforms = instanced?.uniforms ?? null;
4233
+ this.flatTargets.length = 0;
4234
+ for (const target of [plain, skinned, morphed, both, instanced]) {
4235
+ if (target !== null)
4236
+ this.flatTargets.push(target);
4237
+ }
4238
+ const camera = this.lastPassCamera;
4239
+ const env = this.lastPassEnv;
4240
+ for (const target of this.flatTargets) {
4241
+ gl.useProgram(target.program);
4242
+ if (camera !== null && env !== null)
4243
+ this.writeMeshPassState(target.uniforms, camera, env);
4244
+ this.writeMaterialState(target.uniforms, this.currentMaterial);
4245
+ }
4246
+ this.useFlatProgram();
4247
+ }
4248
+ fitFlatProgram(gl) {
4249
+ const build = (budget) => this.flatFragAt(budget);
4145
4250
  const vert = flatVert({ skinned: false, morphed: false, instanced: false });
4146
4251
  const limit = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
4147
4252
  const ceiling = {
@@ -4673,6 +4778,34 @@ export class WebGL2Renderer {
4673
4778
  gl.disable(gl.SCISSOR_TEST);
4674
4779
  this.restoreViewport();
4675
4780
  }
4781
+ /**
4782
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
4783
+ *
4784
+ * A first-person view model — arms and a held weapon — drawn with the frame's camera, or with one
4785
+ * of its own through `bindMeshPass`, lands in front of anything past the near plane without the
4786
+ * world's depth being cleared, so every pass that reads depth afterwards still sees the world.
4787
+ * `share` is how much of the range it takes, 1% by default. See `viewModel.ts` for what it gives
4788
+ * up, which includes temporal reconstruction: draw a view model with multisampling.
4789
+ *
4790
+ * Not inside an inset: an inset has its own depth, cleared for it, and needs no squeeze.
4791
+ */
4792
+ beginViewModel(share = VIEW_MODEL_DEPTH_SHARE) {
4793
+ if (this.contextLost)
4794
+ return;
4795
+ viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
4796
+ this.gl.depthRange(this.viewModelRange[0], this.viewModelRange[1]);
4797
+ this.viewModelActive = true;
4798
+ }
4799
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
4800
+ endViewModel() {
4801
+ this.viewModelActive = false;
4802
+ if (this.contextLost)
4803
+ return;
4804
+ this.gl.depthRange(0, 1);
4805
+ }
4806
+ /** Whether a view model is being drawn, so `endFrame` can close one the caller left open. */
4807
+ viewModelActive = false;
4808
+ viewModelRange = [0, 1];
4676
4809
  /**
4677
4810
  * How much speed blur the frame about to be drawn should resolve with, 0 to 1.
4678
4811
  *
@@ -5162,6 +5295,10 @@ export class WebGL2Renderer {
5162
5295
  beginFrame(clearColor) {
5163
5296
  if (this.contextLost)
5164
5297
  return;
5298
+ if (this.litAsked) {
5299
+ this.litAsked = false;
5300
+ this.rebuildLit();
5301
+ }
5165
5302
  this.budget.reset();
5166
5303
  this.materials.dirty();
5167
5304
  /*
@@ -5235,6 +5372,9 @@ export class WebGL2Renderer {
5235
5372
  endFrame() {
5236
5373
  if (this.contextLost)
5237
5374
  return;
5375
+ /* A view model left open would squeeze every pass after the world, the composite included. */
5376
+ if (this.viewModelActive)
5377
+ this.endViewModel();
5238
5378
  /*
5239
5379
  * Camera motion blur, built from the camera the frame was *rendered* with.
5240
5380
  *
@@ -5797,6 +5937,8 @@ export class WebGL2Renderer {
5797
5937
  gl.bindTexture(gl.TEXTURE_3D, null);
5798
5938
  this.driftIndex = index;
5799
5939
  this.driftAtlas = atlas;
5940
+ /* The first volume is when the lit programs start reading one. See `litOn`. */
5941
+ this.askLit('DRIFT_LIGHT');
5800
5942
  }
5801
5943
  /** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
5802
5944
  bindDriftLight(u, eye) {
@@ -6094,6 +6236,8 @@ export class WebGL2Renderer {
6094
6236
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
6095
6237
  gl.uniform1i(u['uDoubleSided'] ?? null, 0);
6096
6238
  this.materialDoubleSided = false;
6239
+ this.materialHasMaps = false;
6240
+ this.currentMaterial = null;
6097
6241
  this.currentSurfaceTexture = null;
6098
6242
  /*
6099
6243
  * **And the normal map, which this did not reset and had to.**
@@ -6213,6 +6357,7 @@ export class WebGL2Renderer {
6213
6357
  and `Mesh.indexCount`. */
6214
6358
  if (!mesh.complete || mesh.indexCount === 0)
6215
6359
  return;
6360
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6216
6361
  /*
6217
6362
  * Skipped where the flag asks and the bounds say so.
6218
6363
  *
@@ -6405,6 +6550,7 @@ export class WebGL2Renderer {
6405
6550
  and `Mesh.indexCount`. */
6406
6551
  if (!mesh.complete || mesh.indexCount === 0)
6407
6552
  return;
6553
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6408
6554
  if (opacity <= 0)
6409
6555
  return;
6410
6556
  /*
@@ -114,6 +114,8 @@ export interface GpuMesh {
114
114
  * the key is built before the present map is consulted.
115
115
  */
116
116
  readonly isSkinned: boolean;
117
+ /** Whether it has texture coordinates, without which a material's maps read one texel. */
118
+ readonly hasUvs: boolean;
117
119
  /**
118
120
  * Whether this mesh carries the per-vertex channel, which an instanced draw cannot.
119
121
  *
@@ -257,6 +257,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
257
257
  progress,
258
258
  hasTangents: data.tangents !== undefined,
259
259
  isSkinned: data.joints !== undefined,
260
+ hasUvs: data.uvs !== undefined,
260
261
  skinOffsets: skinOffsetsOf(supplied),
261
262
  hasChannel: data.channel !== undefined,
262
263
  morph: data.morphTargets === undefined || data.morphTargetCount === undefined
@@ -392,6 +393,7 @@ function gpuMeshOf(parts) {
392
393
  },
393
394
  hasTangents: parts.hasTangents,
394
395
  isSkinned: parts.isSkinned,
396
+ hasUvs: parts.hasUvs,
395
397
  hasChannel: parts.hasChannel,
396
398
  morph: parts.morph,
397
399
  update: parts.update,
@@ -115,18 +115,27 @@ export function flatFragmentBindings(variant) {
115
115
  return found;
116
116
  }
117
117
  /**
118
- * The lit fragment stage's constants, set from the cache: the glass switch, keyed by its **id**.
118
+ * The lit fragment stage's constants, set from the cache's switches, keyed by each one's **id**.
119
119
  *
120
120
  * Not by its name, because an override declared with `@id` answers to the id alone — WebGPU's
121
121
  * "pipeline-overridable constant identifier string" — and naga writes one on every specialisation
122
122
  * constant. Keyed by name, Chrome refused every lit pipeline: *Pipeline overridable constant
123
- * "GLASS_SHADOWS" not found*, and the frame drew nothing.
123
+ * "GLASS_SHADOWS" not found*, and the frame drew nothing. Every override the variant declares is
124
+ * set, so a switch added to the shader and not to the cache is refused here rather than defaulted.
124
125
  */
125
126
  function flatFragmentConstants(variant, cache) {
126
- const id = flatFragmentBindings(variant).overrides?.['GLASS_SHADOWS'];
127
- if (id === undefined)
127
+ const overrides = flatFragmentBindings(variant).overrides;
128
+ if (overrides?.['GLASS_SHADOWS'] === undefined) {
128
129
  throw new Error(`flatPass: variant "${variant}" has no glass switch`);
129
- return { [String(id)]: cache.glassShadows ? 1 : 0 };
130
+ }
131
+ const constants = {};
132
+ for (const [name, id] of Object.entries(overrides)) {
133
+ if (!(name in cache.litSwitches)) {
134
+ throw new Error(`flatPass: variant "${variant}" declares a switch "${name}" no cache sets`);
135
+ }
136
+ constants[String(id)] = cache.litSwitches[name] ? 1 : 0;
137
+ }
138
+ return constants;
130
139
  }
131
140
  /** The depth format both backends compare with. */
132
141
  /* Re-exported so the twenty passes that already import it from here do not each learn a new
@@ -391,7 +400,7 @@ instanced = false,
391
400
  oit = 'none',
392
401
  /** Culls nothing, for a surface seen from both faces. The key must carry it: `|2s`. */
393
402
  doubleSided = false) {
394
- return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided));
403
+ return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided), true);
395
404
  }
396
405
  /**
397
406
  * The same pipeline, compiled off the main thread.
@@ -407,7 +416,7 @@ export function flatPipelineAsync(cache, device, layout, variant, key, present,
407
416
  skinned = false,
408
417
  /** Whether this pipeline reads morph deltas. See `FlatVertexOptions`. */
409
418
  morphed = false) {
410
- return cache.getAsync(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed));
419
+ return cache.getAsync(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed), true);
411
420
  }
412
421
  /**
413
422
  * The colour target for an order-independent pass, or null where this is an ordinary draw.
@@ -1,3 +1,5 @@
1
+ import { type LitSwitch } from '../../shaders/flat/glassTint.ts';
2
+ export type { LitSwitch };
1
3
  /**
2
4
  * Render pipelines, built once and looked up by name.
3
5
  *
@@ -33,14 +35,27 @@ export declare class PipelineCache {
33
35
  */
34
36
  readonly sampleCount: number;
35
37
  /**
36
- * Whether the lit pipelines here read glass in their shadows: `glassShadows` other than `'off'`.
38
+ * Which of the lit stage's switches the pipelines built now have on: the pipeline-overridable
39
+ * constants the generated lit shader branches on (`GLASS_SHADOWS` and `litSwitchesGlsl`), which
40
+ * the device compiles away where they are off. Read by `flatPass` when it describes a pipeline.
37
41
  *
38
- * **Carried by the cache for the reason the format is**: one value every lit pipeline is built
39
- * with, chosen once by the renderer. It is the `GLASS_SHADOWS` override the generated lit shader
40
- * branches on (see `FlatShaderOptions.glassShadows`), so off specialises the lookups away rather
41
- * than leaving code that returns at its first line and still costs the pass its registers.
42
+ * **Each is code a scene pays for in every lit pixel's registers whether it uses it or not**, so
43
+ * each is off until it is used: glass until the renderer is offered a pane, a fixture until one is
44
+ * loaded, the effects until a material carries a table, DriftLight until a volume is set — and
45
+ * then on for good, through `enable`. Clustering is the profile's, fixed at construction. Every
46
+ * scene paid for all of it from 4.5.0 to 4.8.1, and on a phone that was the lit pass: about 30%
47
+ * of the lit shader's instructions for glass alone, measured on RADV. **What it costs** is the
48
+ * frames between a feature's first use and its recompiled pipelines, which draw without it.
42
49
  */
50
+ readonly litSwitches: Record<LitSwitch, boolean>;
51
+ /** Whether glass may ever be switched on here: `glassShadows` other than `'off'`. */
43
52
  readonly glassShadows: boolean;
53
+ /** How to describe each lit pipeline again, so `enable` can rebuild every one. */
54
+ private readonly litDescribers;
55
+ /** Bumped by every switch, so a lit build started before one is not kept after it. */
56
+ private litGeneration;
57
+ /** The last rebuild, which the next one waits for so they land in the order they were asked. */
58
+ private litRebuild;
44
59
  private readonly device;
45
60
  private readonly pipelines;
46
61
  /**
@@ -58,7 +73,9 @@ export declare class PipelineCache {
58
73
  * wait for readiness rather than discovering it mid-frame.
59
74
  */
60
75
  private readonly compiling;
61
- constructor(device: GPUDevice, format: GPUTextureFormat, sampleCount?: number, glassShadows?: boolean);
76
+ constructor(device: GPUDevice, format: GPUTextureFormat, sampleCount?: number, glassShadows?: boolean,
77
+ /** The profile's `clusteredLights`, which compiles the clustered arm in or leaves it out. */
78
+ clusteredLights?: boolean);
62
79
  /** How many distinct pipelines have been built. Read by tests and diagnostics. */
63
80
  get size(): number;
64
81
  /**
@@ -69,7 +86,9 @@ export declare class PipelineCache {
69
86
  * difference is only *when* the driver does the work, and that difference is the whole bug
70
87
  * this exists for.
71
88
  */
72
- getAsync(key: string, describe: () => GPURenderPipelineDescriptor): Promise<GPURenderPipeline>;
89
+ getAsync(key: string, describe: () => GPURenderPipelineDescriptor,
90
+ /** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
91
+ lit?: boolean): Promise<GPURenderPipeline>;
73
92
  /**
74
93
  * Resolve once every pipeline asked for so far has finished compiling.
75
94
  *
@@ -90,7 +109,20 @@ export declare class PipelineCache {
90
109
  * too**: a nested object with vertex buffer layouts and blend state, constructed to be
91
110
  * thrown away on every hit. Passing a function means a hit costs one `Map` lookup.
92
111
  */
93
- get(key: string, describe: () => GPURenderPipelineDescriptor): GPURenderPipeline;
112
+ get(key: string, describe: () => GPURenderPipelineDescriptor,
113
+ /** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
114
+ lit?: boolean): GPURenderPipeline;
115
+ /**
116
+ * Turn one of the lit stage's switches on, for every lit pipeline built from now on and every one
117
+ * already built. See `litSwitches`.
118
+ *
119
+ * **The rebuilt set lands at once**, when every one of them has compiled, so no frame draws some
120
+ * surfaces with a feature and others without. Until then the pipelines already built go on drawing
121
+ * as they were. Once only: a switch is not turned off again when the last user of it leaves,
122
+ * because a second switch would cost a second compile for a saving the first use showed was not
123
+ * needed. Glass is never turned on where the profile asked for no glass shadows.
124
+ */
125
+ enable(feature: LitSwitch): Promise<void>;
94
126
  /**
95
127
  * Drop every pipeline.
96
128
  *
@@ -1,3 +1,4 @@
1
+ import { noLitSwitches } from '../../shaders/flat/glassTint.js';
1
2
  /**
2
3
  * Render pipelines, built once and looked up by name.
3
4
  *
@@ -33,14 +34,27 @@ export class PipelineCache {
33
34
  */
34
35
  sampleCount;
35
36
  /**
36
- * Whether the lit pipelines here read glass in their shadows: `glassShadows` other than `'off'`.
37
+ * Which of the lit stage's switches the pipelines built now have on: the pipeline-overridable
38
+ * constants the generated lit shader branches on (`GLASS_SHADOWS` and `litSwitchesGlsl`), which
39
+ * the device compiles away where they are off. Read by `flatPass` when it describes a pipeline.
37
40
  *
38
- * **Carried by the cache for the reason the format is**: one value every lit pipeline is built
39
- * with, chosen once by the renderer. It is the `GLASS_SHADOWS` override the generated lit shader
40
- * branches on (see `FlatShaderOptions.glassShadows`), so off specialises the lookups away rather
41
- * than leaving code that returns at its first line and still costs the pass its registers.
41
+ * **Each is code a scene pays for in every lit pixel's registers whether it uses it or not**, so
42
+ * each is off until it is used: glass until the renderer is offered a pane, a fixture until one is
43
+ * loaded, the effects until a material carries a table, DriftLight until a volume is set — and
44
+ * then on for good, through `enable`. Clustering is the profile's, fixed at construction. Every
45
+ * scene paid for all of it from 4.5.0 to 4.8.1, and on a phone that was the lit pass: about 30%
46
+ * of the lit shader's instructions for glass alone, measured on RADV. **What it costs** is the
47
+ * frames between a feature's first use and its recompiled pipelines, which draw without it.
42
48
  */
49
+ litSwitches = noLitSwitches();
50
+ /** Whether glass may ever be switched on here: `glassShadows` other than `'off'`. */
43
51
  glassShadows;
52
+ /** How to describe each lit pipeline again, so `enable` can rebuild every one. */
53
+ litDescribers = new Map();
54
+ /** Bumped by every switch, so a lit build started before one is not kept after it. */
55
+ litGeneration = 0;
56
+ /** The last rebuild, which the next one waits for so they land in the order they were asked. */
57
+ litRebuild = Promise.resolve();
44
58
  device;
45
59
  pipelines = new Map();
46
60
  /**
@@ -58,11 +72,14 @@ export class PipelineCache {
58
72
  * wait for readiness rather than discovering it mid-frame.
59
73
  */
60
74
  compiling = new Map();
61
- constructor(device, format, sampleCount = 1, glassShadows = true) {
75
+ constructor(device, format, sampleCount = 1, glassShadows = true,
76
+ /** The profile's `clusteredLights`, which compiles the clustered arm in or leaves it out. */
77
+ clusteredLights = false) {
62
78
  this.device = device;
63
79
  this.format = format;
64
80
  this.sampleCount = sampleCount;
65
81
  this.glassShadows = glassShadows;
82
+ this.litSwitches.CLUSTERED_LIGHTS = clusteredLights;
66
83
  }
67
84
  /** How many distinct pipelines have been built. Read by tests and diagnostics. */
68
85
  get size() {
@@ -76,13 +93,18 @@ export class PipelineCache {
76
93
  * difference is only *when* the driver does the work, and that difference is the whole bug
77
94
  * this exists for.
78
95
  */
79
- async getAsync(key, describe) {
96
+ async getAsync(key, describe,
97
+ /** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
98
+ lit = false) {
99
+ if (lit)
100
+ this.litDescribers.set(key, describe);
80
101
  const existing = this.pipelines.get(key);
81
102
  if (existing !== undefined)
82
103
  return existing;
83
104
  const inFlight = this.compiling.get(key);
84
105
  if (inFlight !== undefined)
85
106
  return inFlight;
107
+ const generation = this.litGeneration;
86
108
  /*
87
109
  * **Falls back where the async form is missing.** It is part of the standard, but a device
88
110
  * can be a test double, a polyfill or an older implementation, and a renderer that throws
@@ -94,7 +116,9 @@ export class PipelineCache {
94
116
  ? this.device.createRenderPipelineAsync(describe())
95
117
  : Promise.resolve(this.device.createRenderPipeline(describe()));
96
118
  const promise = build.then((built) => {
97
- this.pipelines.set(key, built);
119
+ /* A lit pipeline started before a switch is not kept: the switch rebuilds it. */
120
+ if (!lit || generation === this.litGeneration)
121
+ this.pipelines.set(key, built);
98
122
  this.compiling.delete(key);
99
123
  return built;
100
124
  });
@@ -129,7 +153,11 @@ export class PipelineCache {
129
153
  * too**: a nested object with vertex buffer layouts and blend state, constructed to be
130
154
  * thrown away on every hit. Passing a function means a hit costs one `Map` lookup.
131
155
  */
132
- get(key, describe) {
156
+ get(key, describe,
157
+ /** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
158
+ lit = false) {
159
+ if (lit)
160
+ this.litDescribers.set(key, describe);
133
161
  const existing = this.pipelines.get(key);
134
162
  if (existing !== undefined)
135
163
  return existing;
@@ -137,6 +165,45 @@ export class PipelineCache {
137
165
  this.pipelines.set(key, built);
138
166
  return built;
139
167
  }
168
+ /**
169
+ * Turn one of the lit stage's switches on, for every lit pipeline built from now on and every one
170
+ * already built. See `litSwitches`.
171
+ *
172
+ * **The rebuilt set lands at once**, when every one of them has compiled, so no frame draws some
173
+ * surfaces with a feature and others without. Until then the pipelines already built go on drawing
174
+ * as they were. Once only: a switch is not turned off again when the last user of it leaves,
175
+ * because a second switch would cost a second compile for a saving the first use showed was not
176
+ * needed. Glass is never turned on where the profile asked for no glass shadows.
177
+ */
178
+ enable(feature) {
179
+ if (this.litSwitches[feature])
180
+ return this.litRebuild;
181
+ if (feature === 'GLASS_SHADOWS' && !this.glassShadows)
182
+ return this.litRebuild;
183
+ this.litSwitches[feature] = true;
184
+ this.litGeneration += 1;
185
+ const generation = this.litGeneration;
186
+ this.litRebuild = this.litRebuild.then(async () => {
187
+ /* A later switch has its own rebuild, which describes every pipeline with this one on too. */
188
+ if (generation !== this.litGeneration)
189
+ return;
190
+ const rebuilt = [...this.litDescribers].map(([key, describe]) => {
191
+ const descriptor = describe();
192
+ const build = typeof this.device.createRenderPipelineAsync === 'function'
193
+ ? this.device.createRenderPipelineAsync(descriptor)
194
+ : Promise.resolve(this.device.createRenderPipeline(descriptor));
195
+ return build.then((pipeline) => [key, pipeline]);
196
+ });
197
+ const results = await Promise.allSettled(rebuilt);
198
+ if (generation !== this.litGeneration)
199
+ return;
200
+ for (const result of results) {
201
+ if (result.status === 'fulfilled')
202
+ this.pipelines.set(result.value[0], result.value[1]);
203
+ }
204
+ });
205
+ return this.litRebuild;
206
+ }
140
207
  /**
141
208
  * Drop every pipeline.
142
209
  *