@driftengine/core 4.1.0 → 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.
- package/CHANGELOG.json +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +12 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +84 -24
- package/package.json +6 -6
- package/src/index.ts +8 -0
- package/src/render/backend/webgpu/gpuDrivenPass.ts +87 -26
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.1.2",
|
|
4
|
+
"date": "2026-09-20",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "fixed",
|
|
8
|
+
"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.",
|
|
9
|
+
"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."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "4.1.1",
|
|
15
|
+
"date": "2026-09-20",
|
|
16
|
+
"entries": [
|
|
17
|
+
{
|
|
18
|
+
"kind": "added",
|
|
19
|
+
"short": "Two adapters, so the inspector and the network panel have something to read. An entity world becomes inspectable and a lockstep session becomes a readout, both typed by shape rather than by import.",
|
|
20
|
+
"text": "**A panel needs its world in the shape the panel reads, and two of those shapes are the same in every game that has the thing behind them.** `createToolsOverlay` put the panels on screen in 4.1.0 and left every consumer to write the same two bridges, which is the drift this repository opens by describing, one layer in.\n\n**`entitiesInspectable(world, types)` turns an entity world into an `InspectableWorld`.** The inspector addresses a component by name, because a name is what a row carries; a world addresses it by the type object, because that is what indexes its stores. Bridging them is seven forwarding methods and a lookup. **A name with no type behind it does nothing rather than guessing**, so a row built against a stale schema cannot write a field into a component the world does not have and report success, which is a silent loss of an edit somebody just made and watched apply.\n\n**`createSessionRecorder`, `observeSession` and `sessionReadout` turn a lockstep session into a `NetworkReadout`.** The watching is the half worth writing once: `session.desync` is **latched rather than an event**, so it keeps answering with the same disagreement on every frame until the next one arrives, and a recorder that appended what it read would turn one divergence into sixty a second and bury the tick somebody is looking for under copies of itself. `snapshotBytes` is the caller's, because `RewindLoop` is generic over the state it snapshots and cannot know the size of one; a caller passing zero is saying it has not measured.\n\n**Both are typed structurally rather than imported**, the arrangement `@driftengine/drft` already has with the DTEX types `@driftengine/texture` satisfies. So this package gains no dependency, the release is the same ninety five places it was, and a consumer whose state is not an entity world at all can still satisfy the shape.\n\n**5,391 bytes gzipped, from 5,105, and 286 of that is these two.** Named in the size fixture in the same commit, which is the whole point of the paragraph the last release added to that file."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"kind": "fixed",
|
|
24
|
+
"short": "The city scene fits the device it is on. A phone binds 128 MiB and the published reach asked for 145, so every handheld got a blank frame and a refusal in a console nobody had open.",
|
|
25
|
+
"text": "**Reported from a Galaxy S23 Ultra, on the published site: \"A city at dusk\" was a blank frame.** The scene asked for **152,314,560 bytes** as one storage binding and the device binds **134,217,728**. That is 128 MiB, the WebGPU default, and it is what most handhelds offer against the several gigabytes a desktop adapter does.\n\n**Nothing was misconfigured.** `select.ts` already asks the adapter for its ceiling on `maxStorageBufferBindingSize` rather than taking the default, so there was nothing left to raise: the city was simply larger than the part could hold. The refusal was correct, it named the number, and it reached a console rather than a person.\n\n**The scene sizes itself to the device now.** `reachForCeiling` steps down a ladder of reaches until the vertex buffer fits inside four fifths of what the adapter reports, leaving room for the pipeline's other storage bindings. An S23 Ultra gets a reach of 450 metres at 71% of its limit; a desktop adapter keeps the published 900. A smaller reach asked for with `?reach=` is a decision and is never raised to fill the room available.\n\n**The budget could not have fixed this.** The scene ignored the one it was handed, and honouring it would not have helped: a demos page passes `full` to every device and only a `?budget=` in the address says otherwise, so a phone never asked for less. Sizing against the limit the device reports needs no coordination with whoever mounts the scene.\n\n**`VERTEX_FLOATS` is exported for it.** A `StreamCapacity` is counts and what a device refuses is bytes, so whether a capacity fits is `vertices * VERTEX_FLOATS * 4` against the binding ceiling. Without it a consumer either hard-codes the stride or finds out by being refused, which is what this scene did on every phone."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
2
29
|
{
|
|
3
30
|
"version": "4.1.0",
|
|
4
31
|
"date": "2026-09-20",
|
package/dist/index.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export { OcclusionBuffer } from './render/occlusion.ts';
|
|
|
73
73
|
export type { ComputeContext, ComputeDefinition, ComputeDevice, ComputeHandle, } from './render/compute.ts';
|
|
74
74
|
export { createRenderer } from './render/backend/createRenderer.ts';
|
|
75
75
|
export type { CreatedRenderer, CreateRendererOptions, RenderPipeline, } from './render/backend/createRenderer.ts';
|
|
76
|
-
export { GPU_DRIVEN_LISTS, GpuDrivenPass, MATERIAL_FLOATS, } from './render/backend/webgpu/gpuDrivenPass.ts';
|
|
76
|
+
export { GPU_DRIVEN_LISTS, GpuDrivenPass, MATERIAL_FLOATS, VERTEX_FLOATS, } from './render/backend/webgpu/gpuDrivenPass.ts';
|
|
77
77
|
export type { GpuDrivenList, GpuDrivenMaterial, GpuDrivenShadowOptions, GpuDrivenView, } from './render/backend/webgpu/gpuDrivenPass.ts';
|
|
78
78
|
export { atmosphereFog, createFogTarget } from './render/fog.ts';
|
|
79
79
|
export type { FogOptions, FogTarget } from './render/fog.ts';
|
package/dist/index.js
CHANGED
|
@@ -105,7 +105,15 @@ export { createRenderer } from './render/backend/createRenderer.js';
|
|
|
105
105
|
* It draws vertex colour, one directional term, a hemispheric ambient and an emissive add — not
|
|
106
106
|
* the standard material. `DRAFT_SCENES`, never `SCENES`, until it draws the six published ones.
|
|
107
107
|
*/
|
|
108
|
-
export { GPU_DRIVEN_LISTS, GpuDrivenPass, MATERIAL_FLOATS,
|
|
108
|
+
export { GPU_DRIVEN_LISTS, GpuDrivenPass, MATERIAL_FLOATS,
|
|
109
|
+
/*
|
|
110
|
+
* **The vertex stride, because a consumer sizing a streaming scene has to know it.** A
|
|
111
|
+
* `StreamCapacity` is counts, and what a device refuses is bytes: the pipeline binds the whole
|
|
112
|
+
* vertex buffer as one storage binding, so whether a capacity fits is `vertices * VERTEX_FLOATS
|
|
113
|
+
* * 4` against `maxStorageBufferBindingSize`. Without this a consumer either hard-codes 48 or
|
|
114
|
+
* finds out by being refused, and the published city scene did the second on every phone.
|
|
115
|
+
*/
|
|
116
|
+
VERTEX_FLOATS, } from './render/backend/webgpu/gpuDrivenPass.js';
|
|
109
117
|
/*
|
|
110
118
|
* **One environment, one haze, on both pipelines.** `GpuDrivenView.fog` is the medium the forward
|
|
111
119
|
* path binds, and a consumer drawing both in one frame fills it from `atmosphereFog` rather than
|
|
@@ -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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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.
|
|
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.
|
|
33
|
-
"@driftengine/physics": "4.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.
|
|
67
|
-
"@driftengine/network": "4.1.
|
|
68
|
-
"@driftengine/texture": "4.1.
|
|
66
|
+
"@driftengine/nav": "4.1.2",
|
|
67
|
+
"@driftengine/network": "4.1.2",
|
|
68
|
+
"@driftengine/texture": "4.1.2"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -238,6 +238,14 @@ export {
|
|
|
238
238
|
GPU_DRIVEN_LISTS,
|
|
239
239
|
GpuDrivenPass,
|
|
240
240
|
MATERIAL_FLOATS,
|
|
241
|
+
/*
|
|
242
|
+
* **The vertex stride, because a consumer sizing a streaming scene has to know it.** A
|
|
243
|
+
* `StreamCapacity` is counts, and what a device refuses is bytes: the pipeline binds the whole
|
|
244
|
+
* vertex buffer as one storage binding, so whether a capacity fits is `vertices * VERTEX_FLOATS
|
|
245
|
+
* * 4` against `maxStorageBufferBindingSize`. Without this a consumer either hard-codes 48 or
|
|
246
|
+
* finds out by being refused, and the published city scene did the second on every phone.
|
|
247
|
+
*/
|
|
248
|
+
VERTEX_FLOATS,
|
|
241
249
|
} from './render/backend/webgpu/gpuDrivenPass.ts';
|
|
242
250
|
export type {
|
|
243
251
|
GpuDrivenList,
|
|
@@ -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
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
|
2550
|
-
pass.setBindGroup(0, this.blendRasterGroup
|
|
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();
|