@driftengine/core 4.1.1 → 4.1.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.
@@ -613,6 +613,18 @@ export declare class GpuDrivenPass implements PassDefinition {
613
613
  * origin so nothing ever said so. `StreamingScene` computes them when a mesh is placed.
614
614
  */
615
615
  private buildBuffers;
616
+ /**
617
+ * Close the two scopes the blend pipeline was built in, and disown it where the device refused.
618
+ *
619
+ * **Asynchronous because a pipeline failure is.** Nothing can be known at the call that creates
620
+ * it, so the first frames draw no transparency and then either it arrives or it never does. That
621
+ * is the honest order: a pane missing for two frames is invisible, and a frame missing entirely
622
+ * is what this exists to prevent.
623
+ *
624
+ * The warning is once, in the words the device used, because the whole difficulty of the report
625
+ * this came from was five errors that were all the same one.
626
+ */
627
+ private watchBlendPipeline;
616
628
  private buildPipelines;
617
629
  /**
618
630
  * The groups that name `history` and `drawn`, rebuilt when the two swap.
@@ -941,6 +941,29 @@ export class GpuDrivenPass {
941
941
  this.zeroHistory = this.empty(device, 'history zero', count * 4);
942
942
  this.historySink = this.empty(device, 'history sink', count * 4);
943
943
  }
944
+ /**
945
+ * Close the two scopes the blend pipeline was built in, and disown it where the device refused.
946
+ *
947
+ * **Asynchronous because a pipeline failure is.** Nothing can be known at the call that creates
948
+ * it, so the first frames draw no transparency and then either it arrives or it never does. That
949
+ * is the honest order: a pane missing for two frames is invisible, and a frame missing entirely
950
+ * is what this exists to prevent.
951
+ *
952
+ * The warning is once, in the words the device used, because the whole difficulty of the report
953
+ * this came from was five errors that were all the same one.
954
+ */
955
+ watchBlendPipeline(device) {
956
+ const refused = (error, scope) => {
957
+ if (error === null || this.blendPipeline === null)
958
+ 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}`);
963
+ };
964
+ void device.popErrorScope().then((error) => refused(error, 'internal'));
965
+ void device.popErrorScope().then((error) => refused(error, 'validation'));
966
+ }
944
967
  buildPipelines(device, format, samples) {
945
968
  const compute = (label, code) => device.createComputePipeline({
946
969
  label: `gpu-driven ${label}`,
@@ -1045,6 +1068,23 @@ export class GpuDrivenPass {
1045
1068
  * hide. The opaque raster culls back faces, where a closed hull's far side is a wasted draw.
1046
1069
  */
1047
1070
  const blend = device.createShaderModule({ label: 'blend raster', code: BLEND_RASTER_WGSL });
1071
+ /*
1072
+ * **Built inside error scopes, because a driver that refuses this must not take the frame.**
1073
+ *
1074
+ * Reported from a Galaxy S23 Ultra: `CreateGraphicsPipelines failed with VK_ERROR_UNKNOWN`
1075
+ * for this pipeline, and then four more errors that were all the same one. `createRenderPipeline`
1076
+ * does not throw where a driver refuses — it hands back an *invalid* pipeline and reports
1077
+ * asynchronously — so the code that followed built a bind group from its layout, set it on a
1078
+ * pass, and submitted a command buffer, each of which is invalid because the one before it was.
1079
+ * The city drew **nothing**, on a device that could have drawn all of it but the glass.
1080
+ *
1081
+ * So the handle is disowned where the device refused it and the transparent pass skips itself.
1082
+ * Both scopes, because a shader a driver cannot compile is reported as internal on some
1083
+ * backends and as validation on others, and this is exactly the case where guessing which
1084
+ * costs the whole frame.
1085
+ */
1086
+ device.pushErrorScope('validation');
1087
+ device.pushErrorScope('internal');
1048
1088
  this.blendPipeline = device.createRenderPipeline({
1049
1089
  label: 'gpu-driven blend',
1050
1090
  layout: 'auto',
@@ -1077,6 +1117,7 @@ export class GpuDrivenPass {
1077
1117
  depthCompare: DEPTH_COMPARE,
1078
1118
  },
1079
1119
  });
1120
+ this.watchBlendPipeline(device);
1080
1121
  /*
1081
1122
  * **Premultiplied over, where `renderer.ts`'s composite is a lerp**, and `blendRaster.wgsl.ts`
1082
1123
  * carries why: this pipeline's colour target has a coverage mask in its alpha, which the blit
@@ -1585,30 +1626,42 @@ export class GpuDrivenPass {
1585
1626
  ],
1586
1627
  });
1587
1628
  /* Everything the transparent raster reads: the same scene, the same frame, its own list. */
1588
- this.blendRasterGroup = device.createBindGroup({
1589
- layout: this.blendPipeline.getBindGroupLayout(0),
1590
- entries: [
1591
- { binding: 0, resource: { buffer: this.viewProjBuffer } },
1592
- { binding: 1, resource: { buffer: this.blendList } },
1593
- { binding: 2, resource: { buffer: this.clusterMeta } },
1594
- { binding: 3, resource: { buffer: this.indices } },
1595
- { binding: 4, resource: { buffer: this.vertices } },
1596
- { binding: 5, resource: { buffer: this.transformBuffer } },
1597
- { binding: 6, resource: { buffer: this.materialOf } },
1598
- { binding: 7, resource: { buffer: this.materialTable } },
1599
- { binding: 8, resource: { buffer: this.frameBuffer } },
1600
- {
1601
- binding: 9,
1602
- resource: this.environmentView ?? this.emptyEnvironmentView,
1603
- },
1604
- { binding: 10, resource: this.environmentSampler ?? this.emptySampler },
1605
- { binding: 11, resource: this.latentsView },
1606
- { binding: 12, resource: this.latentClamp },
1607
- { binding: 13, resource: this.latentRepeat },
1608
- { binding: 14, resource: { buffer: this.decodeNodes } },
1609
- { binding: 15, resource: { buffer: this.decodeWeights } },
1610
- ],
1611
- });
1629
+ /*
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.
1634
+ */
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
+ });
1612
1665
  this.blendResolveGroup = device.createBindGroup({
1613
1666
  layout: this.blendResolvePipeline.getBindGroupLayout(0),
1614
1667
  entries: [
@@ -2028,6 +2081,13 @@ export class GpuDrivenPass {
2028
2081
  return;
2029
2082
  }
2030
2083
  case 'blendDraw': {
2084
+ /*
2085
+ * **Skipped where the device refused the pipeline, rather than drawn with an invalid one.**
2086
+ * Everything opaque has already been drawn by the time this runs, so what is lost is the
2087
+ * glass and not the city. See `watchBlendPipeline`.
2088
+ */
2089
+ if (this.blendPipeline === null || this.blendRasterGroup === null)
2090
+ return;
2031
2091
  const pass = encoder.beginRenderPass({
2032
2092
  label: 'gpu-driven blend draw',
2033
2093
  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.2",
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.2",
33
+ "@driftengine/physics": "4.1.2",
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.2",
67
+ "@driftengine/network": "4.1.2",
68
+ "@driftengine/texture": "4.1.2"
69
69
  }
70
70
  }
@@ -1299,6 +1299,31 @@ export class GpuDrivenPass implements PassDefinition {
1299
1299
  this.historySink = this.empty(device, 'history sink', count * 4);
1300
1300
  }
1301
1301
 
1302
+ /**
1303
+ * Close the two scopes the blend pipeline was built in, and disown it where the device refused.
1304
+ *
1305
+ * **Asynchronous because a pipeline failure is.** Nothing can be known at the call that creates
1306
+ * it, so the first frames draw no transparency and then either it arrives or it never does. That
1307
+ * is the honest order: a pane missing for two frames is invisible, and a frame missing entirely
1308
+ * is what this exists to prevent.
1309
+ *
1310
+ * The warning is once, in the words the device used, because the whole difficulty of the report
1311
+ * this came from was five errors that were all the same one.
1312
+ */
1313
+ 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
+ );
1322
+ };
1323
+ void device.popErrorScope().then((error) => refused(error, 'internal'));
1324
+ void device.popErrorScope().then((error) => refused(error, 'validation'));
1325
+ }
1326
+
1302
1327
  private buildPipelines(device: GPUDevice, format: GPUTextureFormat, samples: number): void {
1303
1328
  const compute = (label: string, code: string): GPUComputePipeline =>
1304
1329
  device.createComputePipeline({
@@ -1409,6 +1434,23 @@ export class GpuDrivenPass implements PassDefinition {
1409
1434
  * hide. The opaque raster culls back faces, where a closed hull's far side is a wasted draw.
1410
1435
  */
1411
1436
  const blend = device.createShaderModule({ label: 'blend raster', code: BLEND_RASTER_WGSL });
1437
+ /*
1438
+ * **Built inside error scopes, because a driver that refuses this must not take the frame.**
1439
+ *
1440
+ * Reported from a Galaxy S23 Ultra: `CreateGraphicsPipelines failed with VK_ERROR_UNKNOWN`
1441
+ * for this pipeline, and then four more errors that were all the same one. `createRenderPipeline`
1442
+ * does not throw where a driver refuses — it hands back an *invalid* pipeline and reports
1443
+ * asynchronously — so the code that followed built a bind group from its layout, set it on a
1444
+ * pass, and submitted a command buffer, each of which is invalid because the one before it was.
1445
+ * The city drew **nothing**, on a device that could have drawn all of it but the glass.
1446
+ *
1447
+ * So the handle is disowned where the device refused it and the transparent pass skips itself.
1448
+ * Both scopes, because a shader a driver cannot compile is reported as internal on some
1449
+ * backends and as validation on others, and this is exactly the case where guessing which
1450
+ * costs the whole frame.
1451
+ */
1452
+ device.pushErrorScope('validation');
1453
+ device.pushErrorScope('internal');
1412
1454
  this.blendPipeline = device.createRenderPipeline({
1413
1455
  label: 'gpu-driven blend',
1414
1456
  layout: 'auto',
@@ -1441,6 +1483,7 @@ export class GpuDrivenPass implements PassDefinition {
1441
1483
  depthCompare: DEPTH_COMPARE,
1442
1484
  },
1443
1485
  });
1486
+ this.watchBlendPipeline(device);
1444
1487
 
1445
1488
  /*
1446
1489
  * **Premultiplied over, where `renderer.ts`'s composite is a lerp**, and `blendRaster.wgsl.ts`
@@ -2015,30 +2058,42 @@ export class GpuDrivenPass implements PassDefinition {
2015
2058
  });
2016
2059
 
2017
2060
  /* 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
- });
2061
+ /*
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.
2066
+ */
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
+ });
2042
2097
  this.blendResolveGroup = device.createBindGroup({
2043
2098
  layout: (this.blendResolvePipeline as GPURenderPipeline).getBindGroupLayout(0),
2044
2099
  entries: [
@@ -2519,6 +2574,12 @@ export class GpuDrivenPass implements PassDefinition {
2519
2574
  return;
2520
2575
  }
2521
2576
  case 'blendDraw': {
2577
+ /*
2578
+ * **Skipped where the device refused the pipeline, rather than drawn with an invalid one.**
2579
+ * Everything opaque has already been drawn by the time this runs, so what is lost is the
2580
+ * glass and not the city. See `watchBlendPipeline`.
2581
+ */
2582
+ if (this.blendPipeline === null || this.blendRasterGroup === null) return;
2522
2583
  const pass = encoder.beginRenderPass({
2523
2584
  label: 'gpu-driven blend draw',
2524
2585
  colorAttachments: [
@@ -2546,8 +2607,8 @@ export class GpuDrivenPass implements PassDefinition {
2546
2607
  },
2547
2608
  timestampWrites: this.writesFor('blendDraw'),
2548
2609
  });
2549
- pass.setPipeline(this.blendPipeline as GPURenderPipeline);
2550
- pass.setBindGroup(0, this.blendRasterGroup as GPUBindGroup);
2610
+ pass.setPipeline(this.blendPipeline);
2611
+ pass.setBindGroup(0, this.blendRasterGroup);
2551
2612
  pass.setViewport(0, 0, this.width, this.height, 0, 1);
2552
2613
  pass.drawIndirect(this.blendArgs as GPUBuffer, 0);
2553
2614
  pass.end();