@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
|
-
|
|
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
|
-
{
|
|
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.
|
|
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.
|
|
33
|
-
"@driftengine/physics": "4.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.
|
|
67
|
-
"@driftengine/network": "4.1.
|
|
68
|
-
"@driftengine/texture": "4.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
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
binding:
|
|
2032
|
-
resource:
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
|
2550
|
-
pass.setBindGroup(0, this.blendRasterGroup
|
|
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();
|