@driftengine/core 4.1.1 → 4.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.json CHANGED
@@ -1,4 +1,36 @@
1
1
  [
2
+ {
3
+ "version": "4.1.3",
4
+ "date": "2026-09-20",
5
+ "entries": [
6
+ {
7
+ "kind": "fixed",
8
+ "short": "An imported model draws as it did before 4.0.0. Decoding images with straight alpha let the bytes under a transparent texel reach the frame, and on an opaque draw nothing discards them.",
9
+ "text": "**Reported as a car whose interior, grille, mirrors and lamps came through as hard black and white shards, where the published build of the same game was correct.** 4.0.0 changed how a `.drft` decodes its images, from premultiplied to straight alpha.\n\n**The argument for straight alpha was measured and is still true.** `createImageBitmap` premultiplies unless told not to, the upload into a straight-alpha texture divides back out, and on a PNG holding every colour at every alpha, 98,463 of 196,608 channel values came back changed. None did with `premultiplyAlpha: none`. Every cutout and emblem was losing the colour its author padded past its edge.\n\n**What it did not measure is content.** An imported material routinely carries arbitrary bytes under its fully transparent texels, and premultiplying is what kept them out of the frame. Straight alpha let them through, and an opaque draw discards nothing, so the shader takes the colour whatever the alpha beside it says.\n\n**Isolated in the showroom on held frames**, which is the part worth keeping. A model with 41 images drew clean and two with 64 and 65 drew shards, on both backends, with the old container and the new alike, fully streamed. The count only decides how likely a model is to carry a masked texture at all. Restoring the one option drew the car correctly.\n\nSo the faithful choice per texel was the wrong choice per model. `colorSpaceConversion` is unchanged and stays off."
10
+ },
11
+ {
12
+ "kind": "fixed",
13
+ "short": "A tested material stating a zero alpha reference takes the shader default rather than discarding nothing. A grille's holes and a seat's stitching were coming through filled in.",
14
+ "text": "**`ksAlphaRef` of zero means the shader's own threshold, not discard nothing.** The `.kn5` reader read it literally, and the first car to set the flag set it on eleven materials with a reference of zero on every one: `int_net`, `int_stitching`, `grille_a`, `hood_labels`. A threshold of zero discards nothing, so a grille's holes and a seat's stitching were drawn solid.\n\nA material that says it is alpha tested has already said something is meant to go, so a reference of zero cannot be read literally without contradicting the flag beside it. Half, because that is the convention those shaders were authored against, and because these masks are two valued.\n\nThe comment this replaces said the path was carried on the format's word rather than on evidence. This is the evidence arriving."
15
+ },
16
+ {
17
+ "kind": "fixed",
18
+ "short": "A device that refuses the transparent pipeline now really does lose only the glass. The guard added in 4.1.2 warned correctly and did not prevent a single one of the errors it was written for.",
19
+ "text": "**4.1.2 had this the wrong way round, and the reporter proved it on the next run.** That version built the pipeline, bound a group against its layout, encoded the pass, and disowned the handle when the error scope came back. A scope comes back on a microtask, after all three. The log carried the new warning and then every error it was meant to prevent: `GetBindGroupLayout` on an invalid pipeline, a bind group against an invalid layout, a `SetPipeline` with it, and a refused submit.\n\n**Nothing touches the handle until the device has answered now.** The bindings are held as a closure rather than built, and run once, either where the verdict has already landed or when it does. What that costs is no transparency for the frames before the answer, which nobody sees.\n\n**A device offering no error scopes is taken at its word**, because refusing transparency forever on hardware that never said no would be the worse failure of the two."
20
+ }
21
+ ]
22
+ },
23
+ {
24
+ "version": "4.1.2",
25
+ "date": "2026-09-20",
26
+ "entries": [
27
+ {
28
+ "kind": "fixed",
29
+ "short": "A device that refuses the transparent pipeline loses the glass, not the frame. It was taking the whole command buffer with it, so a phone drew nothing where it could have drawn everything else.",
30
+ "text": "**Reported from a Galaxy S23 Ultra, on the published site.** `CreateGraphicsPipelines failed with VK_ERROR_UNKNOWN` for `gpu-driven blend`, and then four more errors that were all the same one: an invalid pipeline makes an invalid bind group, which makes an invalid encoder, which makes an invalid command buffer. **The city drew nothing, on a device that could have drawn all of it but the glass.**\n\n**`createRenderPipeline` does not throw where a driver refuses it.** It hands back an *invalid* handle and reports asynchronously, so there is nothing to catch at the call and the code after it carried on: a bind group built from the invalid layout, a pass set to the invalid pipeline, a submit of the invalid buffer. Two `as GPURenderPipeline` casts are where that assumption lived.\n\n**So the pipeline is built inside error scopes and disowned where one comes back.** Both scopes, validation and internal, because a shader a driver cannot compile is reported as one on some backends and the other elsewhere, and this is exactly the case where guessing costs the whole frame. The transparent pass then skips itself and everything opaque still draws. The warning is said once, in the words the device used.\n\n**The check that went with it had to be rewritten before it meant anything.** The first version asserted that no transparent draw was encoded, and it could not have failed: the fixture carries nothing transparent, so that draw is absent either way and it passed over an unfixed pass. What is observable is the warning and the frame, so that is what it asserts, and perturbing the disown turns it red.\n\n**What is not fixed is why that driver refuses the pipeline.** Its targets are `rgba16float` and `r8unorm` under `layout: auto`, and diagnosing `VK_ERROR_UNKNOWN` needs the part that produces it. This is the containment: a refusal costs what it is about."
31
+ }
32
+ ]
33
+ },
2
34
  {
3
35
  "version": "4.1.1",
4
36
  "date": "2026-09-20",
@@ -456,6 +456,22 @@ export declare class GpuDrivenPass implements PassDefinition {
456
456
  private oitAccumView;
457
457
  private oitRevealView;
458
458
  private blendPipeline;
459
+ /**
460
+ * Whether the device has actually accepted the blend pipeline.
461
+ *
462
+ * **False until it says so, and 4.1.2 had this the wrong way round.** That version built the
463
+ * pipeline, bound a group against its layout, encoded the pass, and disowned the handle when the
464
+ * error scope came back — but a scope comes back on a microtask, after all three. The reporter's
465
+ * next run carried the new warning *and* every error it was meant to prevent: `GetBindGroupLayout`
466
+ * on an invalid pipeline, a bind group against an invalid layout, a `SetPipeline` with it, and a
467
+ * refused submit.
468
+ *
469
+ * Nothing touches the handle until this is true. The cost is no transparency for the frames
470
+ * before the device answers, which nobody sees; the alternative is a frame that draws nothing.
471
+ */
472
+ private blendUsable;
473
+ /** Builds the transparent raster's bindings, held until there is a pipeline worth binding to. */
474
+ private makeBlendGroup;
459
475
  private blendResolvePipeline;
460
476
  private blendLodGroup;
461
477
  private blendCompactGroup;
@@ -613,6 +629,18 @@ export declare class GpuDrivenPass implements PassDefinition {
613
629
  * origin so nothing ever said so. `StreamingScene` computes them when a mesh is placed.
614
630
  */
615
631
  private buildBuffers;
632
+ /**
633
+ * Close the two scopes the blend pipeline was built in, and disown it where the device refused.
634
+ *
635
+ * **Asynchronous because a pipeline failure is.** Nothing can be known at the call that creates
636
+ * it, so the first frames draw no transparency and then either it arrives or it never does. That
637
+ * is the honest order: a pane missing for two frames is invisible, and a frame missing entirely
638
+ * is what this exists to prevent.
639
+ *
640
+ * The warning is once, in the words the device used, because the whole difficulty of the report
641
+ * this came from was five errors that were all the same one.
642
+ */
643
+ private watchBlendPipeline;
616
644
  private buildPipelines;
617
645
  /**
618
646
  * The groups that name `history` and `drawn`, rebuilt when the two swap.
@@ -431,6 +431,22 @@ export class GpuDrivenPass {
431
431
  oitAccumView = null;
432
432
  oitRevealView = null;
433
433
  blendPipeline = null;
434
+ /**
435
+ * Whether the device has actually accepted the blend pipeline.
436
+ *
437
+ * **False until it says so, and 4.1.2 had this the wrong way round.** That version built the
438
+ * pipeline, bound a group against its layout, encoded the pass, and disowned the handle when the
439
+ * error scope came back — but a scope comes back on a microtask, after all three. The reporter's
440
+ * next run carried the new warning *and* every error it was meant to prevent: `GetBindGroupLayout`
441
+ * on an invalid pipeline, a bind group against an invalid layout, a `SetPipeline` with it, and a
442
+ * refused submit.
443
+ *
444
+ * Nothing touches the handle until this is true. The cost is no transparency for the frames
445
+ * before the device answers, which nobody sees; the alternative is a frame that draws nothing.
446
+ */
447
+ blendUsable = false;
448
+ /** Builds the transparent raster's bindings, held until there is a pipeline worth binding to. */
449
+ makeBlendGroup = null;
434
450
  blendResolvePipeline = null;
435
451
  blendLodGroup = null;
436
452
  blendCompactGroup = null;
@@ -941,6 +957,49 @@ export class GpuDrivenPass {
941
957
  this.zeroHistory = this.empty(device, 'history zero', count * 4);
942
958
  this.historySink = this.empty(device, 'history sink', count * 4);
943
959
  }
960
+ /**
961
+ * Close the two scopes the blend pipeline was built in, and disown it where the device refused.
962
+ *
963
+ * **Asynchronous because a pipeline failure is.** Nothing can be known at the call that creates
964
+ * it, so the first frames draw no transparency and then either it arrives or it never does. That
965
+ * is the honest order: a pane missing for two frames is invisible, and a frame missing entirely
966
+ * is what this exists to prevent.
967
+ *
968
+ * The warning is once, in the words the device used, because the whole difficulty of the report
969
+ * this came from was five errors that were all the same one.
970
+ */
971
+ watchBlendPipeline(device) {
972
+ /*
973
+ * **A device with no error scopes is taken at its word.** Nothing can be asked of it, and
974
+ * refusing transparency forever on a device that never said no would be a worse failure than
975
+ * the one this guards: a frame that quietly loses its glass on hardware that could draw it.
976
+ */
977
+ if (typeof device.popErrorScope !== 'function') {
978
+ this.blendUsable = true;
979
+ if (this.makeBlendGroup !== null)
980
+ this.blendRasterGroup = this.makeBlendGroup();
981
+ return;
982
+ }
983
+ let outstanding = 2;
984
+ const settle = (error, scope) => {
985
+ outstanding -= 1;
986
+ if (error !== null && this.blendPipeline !== null) {
987
+ this.blendPipeline = null;
988
+ this.blendRasterGroup = null;
989
+ this.makeBlendGroup = null;
990
+ console.warn(`[driftengine] this device refused the gpu-driven blend pipeline (${scope}), so the ` +
991
+ `transparent pass is off and everything opaque still draws: ${error.message}`);
992
+ }
993
+ /* Usable only once both scopes have come back clean, and only then is anything bound. */
994
+ if (outstanding > 0 || this.blendPipeline === null)
995
+ return;
996
+ this.blendUsable = true;
997
+ if (this.makeBlendGroup !== null)
998
+ this.blendRasterGroup = this.makeBlendGroup();
999
+ };
1000
+ void device.popErrorScope().then((error) => settle(error, 'internal'));
1001
+ void device.popErrorScope().then((error) => settle(error, 'validation'));
1002
+ }
944
1003
  buildPipelines(device, format, samples) {
945
1004
  const compute = (label, code) => device.createComputePipeline({
946
1005
  label: `gpu-driven ${label}`,
@@ -1045,6 +1104,26 @@ export class GpuDrivenPass {
1045
1104
  * hide. The opaque raster culls back faces, where a closed hull's far side is a wasted draw.
1046
1105
  */
1047
1106
  const blend = device.createShaderModule({ label: 'blend raster', code: BLEND_RASTER_WGSL });
1107
+ /*
1108
+ * **Built inside error scopes, because a driver that refuses this must not take the frame.**
1109
+ *
1110
+ * Reported from a Galaxy S23 Ultra: `CreateGraphicsPipelines failed with VK_ERROR_UNKNOWN`
1111
+ * for this pipeline, and then four more errors that were all the same one. `createRenderPipeline`
1112
+ * does not throw where a driver refuses — it hands back an *invalid* pipeline and reports
1113
+ * asynchronously — so the code that followed built a bind group from its layout, set it on a
1114
+ * pass, and submitted a command buffer, each of which is invalid because the one before it was.
1115
+ * The city drew **nothing**, on a device that could have drawn all of it but the glass.
1116
+ *
1117
+ * So the handle is disowned where the device refused it and the transparent pass skips itself.
1118
+ * Both scopes, because a shader a driver cannot compile is reported as internal on some
1119
+ * backends and as validation on others, and this is exactly the case where guessing which
1120
+ * costs the whole frame.
1121
+ */
1122
+ /* Only where the device offers them: see `watchBlendPipeline` for the case where it does not. */
1123
+ if (typeof device.pushErrorScope === 'function') {
1124
+ device.pushErrorScope('validation');
1125
+ device.pushErrorScope('internal');
1126
+ }
1048
1127
  this.blendPipeline = device.createRenderPipeline({
1049
1128
  label: 'gpu-driven blend',
1050
1129
  layout: 'auto',
@@ -1077,6 +1156,7 @@ export class GpuDrivenPass {
1077
1156
  depthCompare: DEPTH_COMPARE,
1078
1157
  },
1079
1158
  });
1159
+ this.watchBlendPipeline(device);
1080
1160
  /*
1081
1161
  * **Premultiplied over, where `renderer.ts`'s composite is a lerp**, and `blendRaster.wgsl.ts`
1082
1162
  * carries why: this pipeline's colour target has a coverage mask in its alpha, which the blit
@@ -1585,7 +1665,12 @@ export class GpuDrivenPass {
1585
1665
  ],
1586
1666
  });
1587
1667
  /* Everything the transparent raster reads: the same scene, the same frame, its own list. */
1588
- this.blendRasterGroup = device.createBindGroup({
1668
+ /*
1669
+ * Held rather than built, because whether the pipeline is usable is not known yet: the error
1670
+ * scopes settle on a microtask and this runs synchronously. `watchBlendPipeline` runs it once
1671
+ * the device has answered, and never if the answer was no.
1672
+ */
1673
+ this.makeBlendGroup = () => device.createBindGroup({
1589
1674
  layout: this.blendPipeline.getBindGroupLayout(0),
1590
1675
  entries: [
1591
1676
  { binding: 0, resource: { buffer: this.viewProjBuffer } },
@@ -1601,7 +1686,10 @@ export class GpuDrivenPass {
1601
1686
  binding: 9,
1602
1687
  resource: this.environmentView ?? this.emptyEnvironmentView,
1603
1688
  },
1604
- { binding: 10, resource: this.environmentSampler ?? this.emptySampler },
1689
+ {
1690
+ binding: 10,
1691
+ resource: this.environmentSampler ?? this.emptySampler,
1692
+ },
1605
1693
  { binding: 11, resource: this.latentsView },
1606
1694
  { binding: 12, resource: this.latentClamp },
1607
1695
  { binding: 13, resource: this.latentRepeat },
@@ -1609,6 +1697,13 @@ export class GpuDrivenPass {
1609
1697
  { binding: 15, resource: { buffer: this.decodeWeights } },
1610
1698
  ],
1611
1699
  });
1700
+ /*
1701
+ * And build it now where the device has already answered, which is the ordinary case: the
1702
+ * verdict lands before this runs on a device with no error scopes, and after it on one that
1703
+ * has them. Whichever order, exactly one of the two places builds the group.
1704
+ */
1705
+ if (this.blendUsable)
1706
+ this.blendRasterGroup = this.makeBlendGroup();
1612
1707
  this.blendResolveGroup = device.createBindGroup({
1613
1708
  layout: this.blendResolvePipeline.getBindGroupLayout(0),
1614
1709
  entries: [
@@ -2028,6 +2123,13 @@ export class GpuDrivenPass {
2028
2123
  return;
2029
2124
  }
2030
2125
  case 'blendDraw': {
2126
+ /*
2127
+ * **Skipped where the device refused the pipeline, rather than drawn with an invalid one.**
2128
+ * Everything opaque has already been drawn by the time this runs, so what is lost is the
2129
+ * glass and not the city. See `watchBlendPipeline`.
2130
+ */
2131
+ if (!this.blendUsable || this.blendPipeline === null || this.blendRasterGroup === null)
2132
+ return;
2031
2133
  const pass = encoder.beginRenderPass({
2032
2134
  label: 'gpu-driven blend draw',
2033
2135
  colorAttachments: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "DriftEngine reusable browser 3D runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.1.1",
33
- "@driftengine/physics": "4.1.1",
32
+ "@driftengine/drft": "4.1.3",
33
+ "@driftengine/physics": "4.1.3",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -63,8 +63,8 @@
63
63
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
64
64
  },
65
65
  "devDependencies": {
66
- "@driftengine/nav": "4.1.1",
67
- "@driftengine/network": "4.1.1",
68
- "@driftengine/texture": "4.1.1"
66
+ "@driftengine/nav": "4.1.3",
67
+ "@driftengine/network": "4.1.3",
68
+ "@driftengine/texture": "4.1.3"
69
69
  }
70
70
  }
@@ -723,6 +723,22 @@ export class GpuDrivenPass implements PassDefinition {
723
723
  private oitAccumView: GPUTextureView | null = null;
724
724
  private oitRevealView: GPUTextureView | null = null;
725
725
  private blendPipeline: GPURenderPipeline | null = null;
726
+ /**
727
+ * Whether the device has actually accepted the blend pipeline.
728
+ *
729
+ * **False until it says so, and 4.1.2 had this the wrong way round.** That version built the
730
+ * pipeline, bound a group against its layout, encoded the pass, and disowned the handle when the
731
+ * error scope came back — but a scope comes back on a microtask, after all three. The reporter's
732
+ * next run carried the new warning *and* every error it was meant to prevent: `GetBindGroupLayout`
733
+ * on an invalid pipeline, a bind group against an invalid layout, a `SetPipeline` with it, and a
734
+ * refused submit.
735
+ *
736
+ * Nothing touches the handle until this is true. The cost is no transparency for the frames
737
+ * before the device answers, which nobody sees; the alternative is a frame that draws nothing.
738
+ */
739
+ private blendUsable = false;
740
+ /** Builds the transparent raster's bindings, held until there is a pipeline worth binding to. */
741
+ private makeBlendGroup: (() => GPUBindGroup) | null = null;
726
742
  private blendResolvePipeline: GPURenderPipeline | null = null;
727
743
  private blendLodGroup: GPUBindGroup | null = null;
728
744
  private blendCompactGroup: GPUBindGroup | null = null;
@@ -1299,6 +1315,50 @@ export class GpuDrivenPass implements PassDefinition {
1299
1315
  this.historySink = this.empty(device, 'history sink', count * 4);
1300
1316
  }
1301
1317
 
1318
+ /**
1319
+ * Close the two scopes the blend pipeline was built in, and disown it where the device refused.
1320
+ *
1321
+ * **Asynchronous because a pipeline failure is.** Nothing can be known at the call that creates
1322
+ * it, so the first frames draw no transparency and then either it arrives or it never does. That
1323
+ * is the honest order: a pane missing for two frames is invisible, and a frame missing entirely
1324
+ * is what this exists to prevent.
1325
+ *
1326
+ * The warning is once, in the words the device used, because the whole difficulty of the report
1327
+ * this came from was five errors that were all the same one.
1328
+ */
1329
+ private watchBlendPipeline(device: GPUDevice): void {
1330
+ /*
1331
+ * **A device with no error scopes is taken at its word.** Nothing can be asked of it, and
1332
+ * refusing transparency forever on a device that never said no would be a worse failure than
1333
+ * the one this guards: a frame that quietly loses its glass on hardware that could draw it.
1334
+ */
1335
+ if (typeof device.popErrorScope !== 'function') {
1336
+ this.blendUsable = true;
1337
+ if (this.makeBlendGroup !== null) this.blendRasterGroup = this.makeBlendGroup();
1338
+ return;
1339
+ }
1340
+
1341
+ let outstanding = 2;
1342
+ const settle = (error: GPUError | null, scope: string): void => {
1343
+ outstanding -= 1;
1344
+ if (error !== null && this.blendPipeline !== null) {
1345
+ this.blendPipeline = null;
1346
+ this.blendRasterGroup = null;
1347
+ this.makeBlendGroup = null;
1348
+ console.warn(
1349
+ `[driftengine] this device refused the gpu-driven blend pipeline (${scope}), so the ` +
1350
+ `transparent pass is off and everything opaque still draws: ${error.message}`,
1351
+ );
1352
+ }
1353
+ /* Usable only once both scopes have come back clean, and only then is anything bound. */
1354
+ if (outstanding > 0 || this.blendPipeline === null) return;
1355
+ this.blendUsable = true;
1356
+ if (this.makeBlendGroup !== null) this.blendRasterGroup = this.makeBlendGroup();
1357
+ };
1358
+ void device.popErrorScope().then((error) => settle(error, 'internal'));
1359
+ void device.popErrorScope().then((error) => settle(error, 'validation'));
1360
+ }
1361
+
1302
1362
  private buildPipelines(device: GPUDevice, format: GPUTextureFormat, samples: number): void {
1303
1363
  const compute = (label: string, code: string): GPUComputePipeline =>
1304
1364
  device.createComputePipeline({
@@ -1409,6 +1469,26 @@ export class GpuDrivenPass implements PassDefinition {
1409
1469
  * hide. The opaque raster culls back faces, where a closed hull's far side is a wasted draw.
1410
1470
  */
1411
1471
  const blend = device.createShaderModule({ label: 'blend raster', code: BLEND_RASTER_WGSL });
1472
+ /*
1473
+ * **Built inside error scopes, because a driver that refuses this must not take the frame.**
1474
+ *
1475
+ * Reported from a Galaxy S23 Ultra: `CreateGraphicsPipelines failed with VK_ERROR_UNKNOWN`
1476
+ * for this pipeline, and then four more errors that were all the same one. `createRenderPipeline`
1477
+ * does not throw where a driver refuses — it hands back an *invalid* pipeline and reports
1478
+ * asynchronously — so the code that followed built a bind group from its layout, set it on a
1479
+ * pass, and submitted a command buffer, each of which is invalid because the one before it was.
1480
+ * The city drew **nothing**, on a device that could have drawn all of it but the glass.
1481
+ *
1482
+ * So the handle is disowned where the device refused it and the transparent pass skips itself.
1483
+ * Both scopes, because a shader a driver cannot compile is reported as internal on some
1484
+ * backends and as validation on others, and this is exactly the case where guessing which
1485
+ * costs the whole frame.
1486
+ */
1487
+ /* Only where the device offers them: see `watchBlendPipeline` for the case where it does not. */
1488
+ if (typeof device.pushErrorScope === 'function') {
1489
+ device.pushErrorScope('validation');
1490
+ device.pushErrorScope('internal');
1491
+ }
1412
1492
  this.blendPipeline = device.createRenderPipeline({
1413
1493
  label: 'gpu-driven blend',
1414
1494
  layout: 'auto',
@@ -1441,6 +1521,7 @@ export class GpuDrivenPass implements PassDefinition {
1441
1521
  depthCompare: DEPTH_COMPARE,
1442
1522
  },
1443
1523
  });
1524
+ this.watchBlendPipeline(device);
1444
1525
 
1445
1526
  /*
1446
1527
  * **Premultiplied over, where `renderer.ts`'s composite is a lerp**, and `blendRaster.wgsl.ts`
@@ -2015,30 +2096,46 @@ export class GpuDrivenPass implements PassDefinition {
2015
2096
  });
2016
2097
 
2017
2098
  /* Everything the transparent raster reads: the same scene, the same frame, its own list. */
2018
- this.blendRasterGroup = device.createBindGroup({
2019
- layout: (this.blendPipeline as GPURenderPipeline).getBindGroupLayout(0),
2020
- entries: [
2021
- { binding: 0, resource: { buffer: this.viewProjBuffer as GPUBuffer } },
2022
- { binding: 1, resource: { buffer: this.blendList as GPUBuffer } },
2023
- { binding: 2, resource: { buffer: this.clusterMeta as GPUBuffer } },
2024
- { binding: 3, resource: { buffer: this.indices as GPUBuffer } },
2025
- { binding: 4, resource: { buffer: this.vertices as GPUBuffer } },
2026
- { binding: 5, resource: { buffer: this.transformBuffer as GPUBuffer } },
2027
- { binding: 6, resource: { buffer: this.materialOf as GPUBuffer } },
2028
- { binding: 7, resource: { buffer: this.materialTable as GPUBuffer } },
2029
- { binding: 8, resource: { buffer: this.frameBuffer as GPUBuffer } },
2030
- {
2031
- binding: 9,
2032
- resource: this.environmentView ?? (this.emptyEnvironmentView as GPUTextureView),
2033
- },
2034
- { binding: 10, resource: this.environmentSampler ?? (this.emptySampler as GPUSampler) },
2035
- { binding: 11, resource: this.latentsView as GPUTextureView },
2036
- { binding: 12, resource: this.latentClamp as GPUSampler },
2037
- { binding: 13, resource: this.latentRepeat as GPUSampler },
2038
- { binding: 14, resource: { buffer: this.decodeNodes as GPUBuffer } },
2039
- { binding: 15, resource: { buffer: this.decodeWeights as GPUBuffer } },
2040
- ],
2041
- });
2099
+ /*
2100
+ * Held rather than built, because whether the pipeline is usable is not known yet: the error
2101
+ * scopes settle on a microtask and this runs synchronously. `watchBlendPipeline` runs it once
2102
+ * the device has answered, and never if the answer was no.
2103
+ */
2104
+ this.makeBlendGroup = (): GPUBindGroup =>
2105
+ device.createBindGroup({
2106
+ layout: (this.blendPipeline as GPURenderPipeline).getBindGroupLayout(0),
2107
+ entries: [
2108
+ { binding: 0, resource: { buffer: this.viewProjBuffer as GPUBuffer } },
2109
+ { binding: 1, resource: { buffer: this.blendList as GPUBuffer } },
2110
+ { binding: 2, resource: { buffer: this.clusterMeta as GPUBuffer } },
2111
+ { binding: 3, resource: { buffer: this.indices as GPUBuffer } },
2112
+ { binding: 4, resource: { buffer: this.vertices as GPUBuffer } },
2113
+ { binding: 5, resource: { buffer: this.transformBuffer as GPUBuffer } },
2114
+ { binding: 6, resource: { buffer: this.materialOf as GPUBuffer } },
2115
+ { binding: 7, resource: { buffer: this.materialTable as GPUBuffer } },
2116
+ { binding: 8, resource: { buffer: this.frameBuffer as GPUBuffer } },
2117
+ {
2118
+ binding: 9,
2119
+ resource: this.environmentView ?? (this.emptyEnvironmentView as GPUTextureView),
2120
+ },
2121
+ {
2122
+ binding: 10,
2123
+ resource: this.environmentSampler ?? (this.emptySampler as GPUSampler),
2124
+ },
2125
+ { binding: 11, resource: this.latentsView as GPUTextureView },
2126
+ { binding: 12, resource: this.latentClamp as GPUSampler },
2127
+ { binding: 13, resource: this.latentRepeat as GPUSampler },
2128
+ { binding: 14, resource: { buffer: this.decodeNodes as GPUBuffer } },
2129
+ { binding: 15, resource: { buffer: this.decodeWeights as GPUBuffer } },
2130
+ ],
2131
+ });
2132
+
2133
+ /*
2134
+ * And build it now where the device has already answered, which is the ordinary case: the
2135
+ * verdict lands before this runs on a device with no error scopes, and after it on one that
2136
+ * has them. Whichever order, exactly one of the two places builds the group.
2137
+ */
2138
+ if (this.blendUsable) this.blendRasterGroup = this.makeBlendGroup();
2042
2139
  this.blendResolveGroup = device.createBindGroup({
2043
2140
  layout: (this.blendResolvePipeline as GPURenderPipeline).getBindGroupLayout(0),
2044
2141
  entries: [
@@ -2519,6 +2616,13 @@ export class GpuDrivenPass implements PassDefinition {
2519
2616
  return;
2520
2617
  }
2521
2618
  case 'blendDraw': {
2619
+ /*
2620
+ * **Skipped where the device refused the pipeline, rather than drawn with an invalid one.**
2621
+ * Everything opaque has already been drawn by the time this runs, so what is lost is the
2622
+ * glass and not the city. See `watchBlendPipeline`.
2623
+ */
2624
+ if (!this.blendUsable || this.blendPipeline === null || this.blendRasterGroup === null)
2625
+ return;
2522
2626
  const pass = encoder.beginRenderPass({
2523
2627
  label: 'gpu-driven blend draw',
2524
2628
  colorAttachments: [
@@ -2546,8 +2650,8 @@ export class GpuDrivenPass implements PassDefinition {
2546
2650
  },
2547
2651
  timestampWrites: this.writesFor('blendDraw'),
2548
2652
  });
2549
- pass.setPipeline(this.blendPipeline as GPURenderPipeline);
2550
- pass.setBindGroup(0, this.blendRasterGroup as GPUBindGroup);
2653
+ pass.setPipeline(this.blendPipeline);
2654
+ pass.setBindGroup(0, this.blendRasterGroup);
2551
2655
  pass.setViewport(0, 0, this.width, this.height, 0, 1);
2552
2656
  pass.drawIndirect(this.blendArgs as GPUBuffer, 0);
2553
2657
  pass.end();