@driftengine/core 4.1.2 → 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.
@@ -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;
@@ -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;
@@ -953,16 +969,36 @@ export class GpuDrivenPass {
953
969
  * this came from was five errors that were all the same one.
954
970
  */
955
971
  watchBlendPipeline(device) {
956
- const refused = (error, scope) => {
957
- if (error === null || this.blendPipeline === null)
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)
958
995
  return;
959
- this.blendPipeline = null;
960
- this.blendRasterGroup = null;
961
- console.warn(`[driftengine] this device refused the gpu-driven blend pipeline (${scope}), so the ` +
962
- `transparent pass is off and everything opaque still draws: ${error.message}`);
996
+ this.blendUsable = true;
997
+ if (this.makeBlendGroup !== null)
998
+ this.blendRasterGroup = this.makeBlendGroup();
963
999
  };
964
- void device.popErrorScope().then((error) => refused(error, 'internal'));
965
- void device.popErrorScope().then((error) => refused(error, 'validation'));
1000
+ void device.popErrorScope().then((error) => settle(error, 'internal'));
1001
+ void device.popErrorScope().then((error) => settle(error, 'validation'));
966
1002
  }
967
1003
  buildPipelines(device, format, samples) {
968
1004
  const compute = (label, code) => device.createComputePipeline({
@@ -1083,8 +1119,11 @@ export class GpuDrivenPass {
1083
1119
  * backends and as validation on others, and this is exactly the case where guessing which
1084
1120
  * costs the whole frame.
1085
1121
  */
1086
- device.pushErrorScope('validation');
1087
- device.pushErrorScope('internal');
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
+ }
1088
1127
  this.blendPipeline = device.createRenderPipeline({
1089
1128
  label: 'gpu-driven blend',
1090
1129
  layout: 'auto',
@@ -1627,41 +1666,44 @@ export class GpuDrivenPass {
1627
1666
  });
1628
1667
  /* Everything the transparent raster reads: the same scene, the same frame, its own list. */
1629
1668
  /*
1630
- * Nothing to bind against where the device refused the pipeline, and the resolve below still
1631
- * wants building — it is a different pipeline and this one's absence is not its problem. An
1632
- * early return here would have taken it with us, which is the same shape of mistake as the
1633
- * cascade this guard exists to stop.
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.
1634
1672
  */
1635
- this.blendRasterGroup =
1636
- this.blendPipeline === null
1637
- ? null
1638
- : device.createBindGroup({
1639
- layout: this.blendPipeline.getBindGroupLayout(0),
1640
- entries: [
1641
- { binding: 0, resource: { buffer: this.viewProjBuffer } },
1642
- { binding: 1, resource: { buffer: this.blendList } },
1643
- { binding: 2, resource: { buffer: this.clusterMeta } },
1644
- { binding: 3, resource: { buffer: this.indices } },
1645
- { binding: 4, resource: { buffer: this.vertices } },
1646
- { binding: 5, resource: { buffer: this.transformBuffer } },
1647
- { binding: 6, resource: { buffer: this.materialOf } },
1648
- { binding: 7, resource: { buffer: this.materialTable } },
1649
- { binding: 8, resource: { buffer: this.frameBuffer } },
1650
- {
1651
- binding: 9,
1652
- resource: this.environmentView ?? this.emptyEnvironmentView,
1653
- },
1654
- {
1655
- binding: 10,
1656
- resource: this.environmentSampler ?? this.emptySampler,
1657
- },
1658
- { binding: 11, resource: this.latentsView },
1659
- { binding: 12, resource: this.latentClamp },
1660
- { binding: 13, resource: this.latentRepeat },
1661
- { binding: 14, resource: { buffer: this.decodeNodes } },
1662
- { binding: 15, resource: { buffer: this.decodeWeights } },
1663
- ],
1664
- });
1673
+ this.makeBlendGroup = () => device.createBindGroup({
1674
+ layout: this.blendPipeline.getBindGroupLayout(0),
1675
+ entries: [
1676
+ { binding: 0, resource: { buffer: this.viewProjBuffer } },
1677
+ { binding: 1, resource: { buffer: this.blendList } },
1678
+ { binding: 2, resource: { buffer: this.clusterMeta } },
1679
+ { binding: 3, resource: { buffer: this.indices } },
1680
+ { binding: 4, resource: { buffer: this.vertices } },
1681
+ { binding: 5, resource: { buffer: this.transformBuffer } },
1682
+ { binding: 6, resource: { buffer: this.materialOf } },
1683
+ { binding: 7, resource: { buffer: this.materialTable } },
1684
+ { binding: 8, resource: { buffer: this.frameBuffer } },
1685
+ {
1686
+ binding: 9,
1687
+ resource: this.environmentView ?? this.emptyEnvironmentView,
1688
+ },
1689
+ {
1690
+ binding: 10,
1691
+ resource: this.environmentSampler ?? this.emptySampler,
1692
+ },
1693
+ { binding: 11, resource: this.latentsView },
1694
+ { binding: 12, resource: this.latentClamp },
1695
+ { binding: 13, resource: this.latentRepeat },
1696
+ { binding: 14, resource: { buffer: this.decodeNodes } },
1697
+ { binding: 15, resource: { buffer: this.decodeWeights } },
1698
+ ],
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();
1665
1707
  this.blendResolveGroup = device.createBindGroup({
1666
1708
  layout: this.blendResolvePipeline.getBindGroupLayout(0),
1667
1709
  entries: [
@@ -2086,7 +2128,7 @@ export class GpuDrivenPass {
2086
2128
  * Everything opaque has already been drawn by the time this runs, so what is lost is the
2087
2129
  * glass and not the city. See `watchBlendPipeline`.
2088
2130
  */
2089
- if (this.blendPipeline === null || this.blendRasterGroup === null)
2131
+ if (!this.blendUsable || this.blendPipeline === null || this.blendRasterGroup === null)
2090
2132
  return;
2091
2133
  const pass = encoder.beginRenderPass({
2092
2134
  label: 'gpu-driven blend draw',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.1.2",
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.2",
33
- "@driftengine/physics": "4.1.2",
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.2",
67
- "@driftengine/network": "4.1.2",
68
- "@driftengine/texture": "4.1.2"
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;
@@ -1311,17 +1327,36 @@ export class GpuDrivenPass implements PassDefinition {
1311
1327
  * this came from was five errors that were all the same one.
1312
1328
  */
1313
1329
  private watchBlendPipeline(device: GPUDevice): void {
1314
- const refused = (error: GPUError | null, scope: string): void => {
1315
- if (error === null || this.blendPipeline === null) return;
1316
- this.blendPipeline = null;
1317
- this.blendRasterGroup = null;
1318
- console.warn(
1319
- `[driftengine] this device refused the gpu-driven blend pipeline (${scope}), so the ` +
1320
- `transparent pass is off and everything opaque still draws: ${error.message}`,
1321
- );
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();
1322
1357
  };
1323
- void device.popErrorScope().then((error) => refused(error, 'internal'));
1324
- void device.popErrorScope().then((error) => refused(error, 'validation'));
1358
+ void device.popErrorScope().then((error) => settle(error, 'internal'));
1359
+ void device.popErrorScope().then((error) => settle(error, 'validation'));
1325
1360
  }
1326
1361
 
1327
1362
  private buildPipelines(device: GPUDevice, format: GPUTextureFormat, samples: number): void {
@@ -1449,8 +1484,11 @@ export class GpuDrivenPass implements PassDefinition {
1449
1484
  * backends and as validation on others, and this is exactly the case where guessing which
1450
1485
  * costs the whole frame.
1451
1486
  */
1452
- device.pushErrorScope('validation');
1453
- device.pushErrorScope('internal');
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
+ }
1454
1492
  this.blendPipeline = device.createRenderPipeline({
1455
1493
  label: 'gpu-driven blend',
1456
1494
  layout: 'auto',
@@ -2059,41 +2097,45 @@ export class GpuDrivenPass implements PassDefinition {
2059
2097
 
2060
2098
  /* Everything the transparent raster reads: the same scene, the same frame, its own list. */
2061
2099
  /*
2062
- * Nothing to bind against where the device refused the pipeline, and the resolve below still
2063
- * wants building — it is a different pipeline and this one's absence is not its problem. An
2064
- * early return here would have taken it with us, which is the same shape of mistake as the
2065
- * cascade this guard exists to stop.
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.
2066
2103
  */
2067
- this.blendRasterGroup =
2068
- this.blendPipeline === null
2069
- ? null
2070
- : device.createBindGroup({
2071
- layout: this.blendPipeline.getBindGroupLayout(0),
2072
- entries: [
2073
- { binding: 0, resource: { buffer: this.viewProjBuffer as GPUBuffer } },
2074
- { binding: 1, resource: { buffer: this.blendList as GPUBuffer } },
2075
- { binding: 2, resource: { buffer: this.clusterMeta as GPUBuffer } },
2076
- { binding: 3, resource: { buffer: this.indices as GPUBuffer } },
2077
- { binding: 4, resource: { buffer: this.vertices as GPUBuffer } },
2078
- { binding: 5, resource: { buffer: this.transformBuffer as GPUBuffer } },
2079
- { binding: 6, resource: { buffer: this.materialOf as GPUBuffer } },
2080
- { binding: 7, resource: { buffer: this.materialTable as GPUBuffer } },
2081
- { binding: 8, resource: { buffer: this.frameBuffer as GPUBuffer } },
2082
- {
2083
- binding: 9,
2084
- resource: this.environmentView ?? (this.emptyEnvironmentView as GPUTextureView),
2085
- },
2086
- {
2087
- binding: 10,
2088
- resource: this.environmentSampler ?? (this.emptySampler as GPUSampler),
2089
- },
2090
- { binding: 11, resource: this.latentsView as GPUTextureView },
2091
- { binding: 12, resource: this.latentClamp as GPUSampler },
2092
- { binding: 13, resource: this.latentRepeat as GPUSampler },
2093
- { binding: 14, resource: { buffer: this.decodeNodes as GPUBuffer } },
2094
- { binding: 15, resource: { buffer: this.decodeWeights as GPUBuffer } },
2095
- ],
2096
- });
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();
2097
2139
  this.blendResolveGroup = device.createBindGroup({
2098
2140
  layout: (this.blendResolvePipeline as GPURenderPipeline).getBindGroupLayout(0),
2099
2141
  entries: [
@@ -2579,7 +2621,8 @@ export class GpuDrivenPass implements PassDefinition {
2579
2621
  * Everything opaque has already been drawn by the time this runs, so what is lost is the
2580
2622
  * glass and not the city. See `watchBlendPipeline`.
2581
2623
  */
2582
- if (this.blendPipeline === null || this.blendRasterGroup === null) return;
2624
+ if (!this.blendUsable || this.blendPipeline === null || this.blendRasterGroup === null)
2625
+ return;
2583
2626
  const pass = encoder.beginRenderPass({
2584
2627
  label: 'gpu-driven blend draw',
2585
2628
  colorAttachments: [