@driftengine/core 4.8.1 → 4.8.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 +86 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/input/actionMap.d.ts +6 -1
- package/dist/input/actionMap.js +20 -7
- package/dist/input/input.d.ts +28 -0
- package/dist/input/input.js +71 -9
- package/dist/input/inputHarness.d.ts +9 -0
- package/dist/input/inputHarness.js +34 -2
- package/dist/render/backend/webgl2/renderer.d.ts +52 -3
- package/dist/render/backend/webgl2/renderer.js +151 -5
- package/dist/render/backend/webgpu/buffers.d.ts +2 -0
- package/dist/render/backend/webgpu/buffers.js +2 -0
- package/dist/render/backend/webgpu/flatPass.js +16 -7
- package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
- package/dist/render/backend/webgpu/pipelineCache.js +76 -9
- package/dist/render/backend/webgpu/renderer.d.ts +56 -1
- package/dist/render/backend/webgpu/renderer.js +151 -26
- package/dist/render/mapsWithoutUvs.d.ts +24 -0
- package/dist/render/mapsWithoutUvs.js +24 -0
- package/dist/render/mesh.d.ts +2 -0
- package/dist/render/mesh.js +3 -0
- package/dist/render/particlePool.d.ts +15 -0
- package/dist/render/particlePool.js +72 -37
- package/dist/render/pass.d.ts +12 -0
- package/dist/render/renderQuality.d.ts +7 -6
- package/dist/render/shaders/flat/glassTint.d.ts +28 -0
- package/dist/render/shaders/flat/glassTint.js +31 -0
- package/dist/render/shaders/flat/index.d.ts +12 -0
- package/dist/render/shaders/flat/index.js +7 -1
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +60 -25
- package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
- package/dist/render/shaders/flat/surfaceEffects.js +6 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
- package/dist/render/shaders/generated/flat.wgsl.js +102 -38
- package/dist/render/surfaceTexture.d.ts +2 -0
- package/dist/render/surfaceTexture.js +4 -0
- package/dist/render/viewModel.d.ts +29 -0
- package/dist/render/viewModel.js +35 -0
- package/package.json +6 -6
- package/src/index.ts +9 -1
- package/src/input/actionMap.ts +23 -8
- package/src/input/input.ts +79 -9
- package/src/input/inputHarness.ts +27 -2
- package/src/render/backend/webgl2/renderer.ts +166 -13
- package/src/render/backend/webgpu/buffers.ts +5 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -25
- package/src/render/backend/webgpu/pipelineCache.ts +82 -8
- package/src/render/backend/webgpu/renderer.ts +151 -26
- package/src/render/mapsWithoutUvs.ts +43 -0
- package/src/render/mesh.ts +3 -0
- package/src/render/particlePool.ts +101 -54
- package/src/render/pass.ts +12 -0
- package/src/render/renderQuality.ts +7 -6
- package/src/render/shaders/flat/glassTint.ts +48 -0
- package/src/render/shaders/flat/index.ts +19 -1
- package/src/render/shaders/flat/main.ts +60 -25
- package/src/render/shaders/flat/surfaceEffects.ts +6 -2
- package/src/render/shaders/generated/flat.wgsl.ts +102 -38
- package/src/render/surfaceTexture.ts +5 -0
- package/src/render/viewModel.ts +41 -0
|
@@ -93,6 +93,9 @@ import { deviceSnappedCellSize } from '../../textLayout.js';
|
|
|
93
93
|
import { WindStreakRenderer } from '../../windStreakRenderer.js';
|
|
94
94
|
import { InstancedBatch } from './instanced.js';
|
|
95
95
|
import { cutoutOf } from '../../cutoutCaster.js';
|
|
96
|
+
import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
|
|
97
|
+
import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
|
|
98
|
+
import { noLitSwitches } from '../../shaders/flat/glassTint.js';
|
|
96
99
|
/**
|
|
97
100
|
* How many frames `firstFrameSettled` will ask before it stops asking.
|
|
98
101
|
*
|
|
@@ -961,6 +964,34 @@ export class WebGL2Renderer {
|
|
|
961
964
|
* each such draw and handed back, where the other backend bakes it into a pipeline.
|
|
962
965
|
*/
|
|
963
966
|
materialDoubleSided = false;
|
|
967
|
+
/** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
|
|
968
|
+
materialHasMaps = false;
|
|
969
|
+
/** The material last set, so a lit program rebuilt for glass can be given it. */
|
|
970
|
+
currentMaterial = null;
|
|
971
|
+
/**
|
|
972
|
+
* The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
|
|
973
|
+
* profile and the content have asked for since (`litWanted`).
|
|
974
|
+
*
|
|
975
|
+
* **Each is off until it is used** — glass until a pane is offered, a fixture until one is loaded,
|
|
976
|
+
* the effects until a material carries a table, DriftLight until a volume is set — and then on for
|
|
977
|
+
* good, rebuilt at the next `beginFrame`. Clustering is the profile's. The code costs the lit pass
|
|
978
|
+
* registers on every surface whether it finds anything or not, and a world without the feature
|
|
979
|
+
* draws the same picture without it. `PipelineCache.litSwitches` makes the same decision on the
|
|
980
|
+
* other backend. The frame a feature is first used in draws without it; the rebuild is a
|
|
981
|
+
* synchronous compile, and the next frame waits for it.
|
|
982
|
+
*/
|
|
983
|
+
litOn = noLitSwitches();
|
|
984
|
+
litWanted = noLitSwitches();
|
|
985
|
+
litAsked = false;
|
|
986
|
+
/** Ask for a switch, which the next `beginFrame` compiles in. Never glass with it turned off. */
|
|
987
|
+
askLit(feature) {
|
|
988
|
+
if (this.litWanted[feature])
|
|
989
|
+
return;
|
|
990
|
+
if (feature === 'GLASS_SHADOWS' && this.quality.glassShadows === 'off')
|
|
991
|
+
return;
|
|
992
|
+
this.litWanted[feature] = true;
|
|
993
|
+
this.litAsked = true;
|
|
994
|
+
}
|
|
964
995
|
/**
|
|
965
996
|
* The one sink handed to every caster enumeration.
|
|
966
997
|
*
|
|
@@ -992,6 +1023,7 @@ export class WebGL2Renderer {
|
|
|
992
1023
|
/* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
|
|
993
1024
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
994
1025
|
this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
|
|
1026
|
+
this.askLit('GLASS_SHADOWS');
|
|
995
1027
|
}
|
|
996
1028
|
return;
|
|
997
1029
|
}
|
|
@@ -1037,6 +1069,7 @@ export class WebGL2Renderer {
|
|
|
1037
1069
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
1038
1070
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
1039
1071
|
this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
|
|
1072
|
+
this.askLit('GLASS_SHADOWS');
|
|
1040
1073
|
}
|
|
1041
1074
|
return;
|
|
1042
1075
|
}
|
|
@@ -1075,6 +1108,7 @@ export class WebGL2Renderer {
|
|
|
1075
1108
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
1076
1109
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
1077
1110
|
this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
|
|
1111
|
+
this.askLit('GLASS_SHADOWS');
|
|
1078
1112
|
}
|
|
1079
1113
|
return;
|
|
1080
1114
|
}
|
|
@@ -1538,6 +1572,9 @@ export class WebGL2Renderer {
|
|
|
1538
1572
|
this.iesRows = this.iesAtlas.height;
|
|
1539
1573
|
this.iesPlanes = this.iesAtlas.planes;
|
|
1540
1574
|
this.cookieTiles = this.cookieImages.length;
|
|
1575
|
+
/* The first profile or cookie is when the lit programs start reading them. See `litOn`. */
|
|
1576
|
+
if (this.iesRows > 1 || this.cookieTiles > 0)
|
|
1577
|
+
this.askLit('LIGHT_FIXTURES');
|
|
1541
1578
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
1542
1579
|
gl.activeTexture(gl.TEXTURE0);
|
|
1543
1580
|
}
|
|
@@ -1896,6 +1933,9 @@ export class WebGL2Renderer {
|
|
|
1896
1933
|
* Built for the profile this renderer resolved, not for every profile at once — so a
|
|
1897
1934
|
* world with shadows off compiles none of the shadow path. See `shaders/flat.ts`.
|
|
1898
1935
|
*/
|
|
1936
|
+
/* The one switch the profile decides; the rest wait for their content. See `litOn`. */
|
|
1937
|
+
this.litOn.CLUSTERED_LIGHTS = this.quality.clusteredLights;
|
|
1938
|
+
this.litWanted.CLUSTERED_LIGHTS = this.quality.clusteredLights;
|
|
1899
1939
|
const fitted = this.fitFlatProgram(gl);
|
|
1900
1940
|
this.flatProgram = fitted.program;
|
|
1901
1941
|
this.flatFragSource = fitted.source;
|
|
@@ -2410,6 +2450,8 @@ export class WebGL2Renderer {
|
|
|
2410
2450
|
this.materials.dirty();
|
|
2411
2451
|
this.currentSurfaceTexture = material?.albedo ?? null;
|
|
2412
2452
|
this.materialDoubleSided = material?.doubleSided === true;
|
|
2453
|
+
this.materialHasMaps = materialHasMaps(material);
|
|
2454
|
+
this.currentMaterial = material;
|
|
2413
2455
|
/*
|
|
2414
2456
|
* Written to every flat program, because material state persists across draws and a skinned
|
|
2415
2457
|
* draw is entitled to the material the caller set before it. A second program holding none of
|
|
@@ -2508,6 +2550,9 @@ export class WebGL2Renderer {
|
|
|
2508
2550
|
}
|
|
2509
2551
|
else {
|
|
2510
2552
|
albedo.bindEffects(gl, SURFACE_EFFECTS_TEXTURE_UNIT, this.emptyTexture2D);
|
|
2553
|
+
/* The first table bound is when the lit programs start reading one. See `litOn`. */
|
|
2554
|
+
if (albedo.hasEffects)
|
|
2555
|
+
this.askLit('SURFACE_EFFECTS');
|
|
2511
2556
|
}
|
|
2512
2557
|
gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
|
|
2513
2558
|
if (albedo === null) {
|
|
@@ -4132,16 +4177,76 @@ export class WebGL2Renderer {
|
|
|
4132
4177
|
* rather than propagating. Only a shader that will not link at *any* budget throws, and it
|
|
4133
4178
|
* throws a sentence with the device's numbers in it rather than a bare driver string.
|
|
4134
4179
|
*/
|
|
4135
|
-
|
|
4136
|
-
|
|
4180
|
+
/** The lit fragment source this renderer's profile compiles, at a light budget. */
|
|
4181
|
+
flatFragAt(budget) {
|
|
4182
|
+
return flatFrag({
|
|
4137
4183
|
pointShadows: this.quality.pointShadows,
|
|
4138
4184
|
directionalShadows: this.quality.directionalShadows,
|
|
4139
4185
|
environmentProbe: this.probeCapture !== null,
|
|
4140
4186
|
nightEmissive: this.quality.nightEmissive,
|
|
4141
|
-
/*
|
|
4142
|
-
glassShadows: this.
|
|
4187
|
+
/* Each off until it is used: see `litOn`. */
|
|
4188
|
+
glassShadows: this.litOn.GLASS_SHADOWS,
|
|
4189
|
+
clusteredLights: this.litOn.CLUSTERED_LIGHTS,
|
|
4190
|
+
lightFixtures: this.litOn.LIGHT_FIXTURES,
|
|
4191
|
+
surfaceEffects: this.litOn.SURFACE_EFFECTS,
|
|
4192
|
+
driftLight: this.litOn.DRIFT_LIGHT,
|
|
4193
|
+
maxLights: budget.maxLights,
|
|
4194
|
+
maxAreaLights: budget.maxAreaLights,
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
/**
|
|
4198
|
+
* Every lit program that exists, compiled again with the switches asked for since, at the budget
|
|
4199
|
+
* the renderer already uploads for, and given the pass and the material the old one held.
|
|
4200
|
+
*
|
|
4201
|
+
* At `beginFrame`, not where a feature was first used: a pane is offered inside a shadow pass
|
|
4202
|
+
* with the depth program bound, and a program swapped under it would draw the next caster with
|
|
4203
|
+
* the lit one.
|
|
4204
|
+
*/
|
|
4205
|
+
rebuildLit() {
|
|
4206
|
+
const { gl } = this;
|
|
4207
|
+
Object.assign(this.litOn, this.litWanted);
|
|
4208
|
+
this.flatFragSource = this.flatFragAt(this.lightBudget);
|
|
4209
|
+
const recompile = (old, skinned, morphed, instanced, label) => {
|
|
4210
|
+
if (old === null)
|
|
4211
|
+
return null;
|
|
4212
|
+
const program = compileProgram(gl, flatVert({ skinned, morphed, instanced }), this.flatFragSource, label);
|
|
4213
|
+
gl.deleteProgram(old);
|
|
4214
|
+
return { program, uniforms: uniformLocations(gl, program, label) };
|
|
4143
4215
|
};
|
|
4144
|
-
const
|
|
4216
|
+
const plain = recompile(this.flatProgram, false, false, false, 'flat');
|
|
4217
|
+
if (plain !== null) {
|
|
4218
|
+
this.flatProgram = plain.program;
|
|
4219
|
+
this.flatUniforms = plain.uniforms;
|
|
4220
|
+
}
|
|
4221
|
+
const skinned = recompile(this.flatSkinnedProgram, true, false, false, 'flat.skinned');
|
|
4222
|
+
this.flatSkinnedProgram = skinned?.program ?? null;
|
|
4223
|
+
this.flatSkinnedUniforms = skinned?.uniforms ?? null;
|
|
4224
|
+
const morphed = recompile(this.flatMorphedProgram, false, true, false, 'flat.morphed');
|
|
4225
|
+
this.flatMorphedProgram = morphed?.program ?? null;
|
|
4226
|
+
this.flatMorphedUniforms = morphed?.uniforms ?? null;
|
|
4227
|
+
const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
|
|
4228
|
+
this.flatBothProgram = both?.program ?? null;
|
|
4229
|
+
this.flatBothUniforms = both?.uniforms ?? null;
|
|
4230
|
+
const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
|
|
4231
|
+
this.flatInstancedProgram = instanced?.program ?? null;
|
|
4232
|
+
this.flatInstancedUniforms = instanced?.uniforms ?? null;
|
|
4233
|
+
this.flatTargets.length = 0;
|
|
4234
|
+
for (const target of [plain, skinned, morphed, both, instanced]) {
|
|
4235
|
+
if (target !== null)
|
|
4236
|
+
this.flatTargets.push(target);
|
|
4237
|
+
}
|
|
4238
|
+
const camera = this.lastPassCamera;
|
|
4239
|
+
const env = this.lastPassEnv;
|
|
4240
|
+
for (const target of this.flatTargets) {
|
|
4241
|
+
gl.useProgram(target.program);
|
|
4242
|
+
if (camera !== null && env !== null)
|
|
4243
|
+
this.writeMeshPassState(target.uniforms, camera, env);
|
|
4244
|
+
this.writeMaterialState(target.uniforms, this.currentMaterial);
|
|
4245
|
+
}
|
|
4246
|
+
this.useFlatProgram();
|
|
4247
|
+
}
|
|
4248
|
+
fitFlatProgram(gl) {
|
|
4249
|
+
const build = (budget) => this.flatFragAt(budget);
|
|
4145
4250
|
const vert = flatVert({ skinned: false, morphed: false, instanced: false });
|
|
4146
4251
|
const limit = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
|
|
4147
4252
|
const ceiling = {
|
|
@@ -4673,6 +4778,34 @@ export class WebGL2Renderer {
|
|
|
4673
4778
|
gl.disable(gl.SCISSOR_TEST);
|
|
4674
4779
|
this.restoreViewport();
|
|
4675
4780
|
}
|
|
4781
|
+
/**
|
|
4782
|
+
* Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
|
|
4783
|
+
*
|
|
4784
|
+
* A first-person view model — arms and a held weapon — drawn with the frame's camera, or with one
|
|
4785
|
+
* of its own through `bindMeshPass`, lands in front of anything past the near plane without the
|
|
4786
|
+
* world's depth being cleared, so every pass that reads depth afterwards still sees the world.
|
|
4787
|
+
* `share` is how much of the range it takes, 1% by default. See `viewModel.ts` for what it gives
|
|
4788
|
+
* up, which includes temporal reconstruction: draw a view model with multisampling.
|
|
4789
|
+
*
|
|
4790
|
+
* Not inside an inset: an inset has its own depth, cleared for it, and needs no squeeze.
|
|
4791
|
+
*/
|
|
4792
|
+
beginViewModel(share = VIEW_MODEL_DEPTH_SHARE) {
|
|
4793
|
+
if (this.contextLost)
|
|
4794
|
+
return;
|
|
4795
|
+
viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
|
|
4796
|
+
this.gl.depthRange(this.viewModelRange[0], this.viewModelRange[1]);
|
|
4797
|
+
this.viewModelActive = true;
|
|
4798
|
+
}
|
|
4799
|
+
/** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
|
|
4800
|
+
endViewModel() {
|
|
4801
|
+
this.viewModelActive = false;
|
|
4802
|
+
if (this.contextLost)
|
|
4803
|
+
return;
|
|
4804
|
+
this.gl.depthRange(0, 1);
|
|
4805
|
+
}
|
|
4806
|
+
/** Whether a view model is being drawn, so `endFrame` can close one the caller left open. */
|
|
4807
|
+
viewModelActive = false;
|
|
4808
|
+
viewModelRange = [0, 1];
|
|
4676
4809
|
/**
|
|
4677
4810
|
* How much speed blur the frame about to be drawn should resolve with, 0 to 1.
|
|
4678
4811
|
*
|
|
@@ -5162,6 +5295,10 @@ export class WebGL2Renderer {
|
|
|
5162
5295
|
beginFrame(clearColor) {
|
|
5163
5296
|
if (this.contextLost)
|
|
5164
5297
|
return;
|
|
5298
|
+
if (this.litAsked) {
|
|
5299
|
+
this.litAsked = false;
|
|
5300
|
+
this.rebuildLit();
|
|
5301
|
+
}
|
|
5165
5302
|
this.budget.reset();
|
|
5166
5303
|
this.materials.dirty();
|
|
5167
5304
|
/*
|
|
@@ -5235,6 +5372,9 @@ export class WebGL2Renderer {
|
|
|
5235
5372
|
endFrame() {
|
|
5236
5373
|
if (this.contextLost)
|
|
5237
5374
|
return;
|
|
5375
|
+
/* A view model left open would squeeze every pass after the world, the composite included. */
|
|
5376
|
+
if (this.viewModelActive)
|
|
5377
|
+
this.endViewModel();
|
|
5238
5378
|
/*
|
|
5239
5379
|
* Camera motion blur, built from the camera the frame was *rendered* with.
|
|
5240
5380
|
*
|
|
@@ -5797,6 +5937,8 @@ export class WebGL2Renderer {
|
|
|
5797
5937
|
gl.bindTexture(gl.TEXTURE_3D, null);
|
|
5798
5938
|
this.driftIndex = index;
|
|
5799
5939
|
this.driftAtlas = atlas;
|
|
5940
|
+
/* The first volume is when the lit programs start reading one. See `litOn`. */
|
|
5941
|
+
this.askLit('DRIFT_LIGHT');
|
|
5800
5942
|
}
|
|
5801
5943
|
/** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
|
|
5802
5944
|
bindDriftLight(u, eye) {
|
|
@@ -6094,6 +6236,8 @@ export class WebGL2Renderer {
|
|
|
6094
6236
|
gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
|
|
6095
6237
|
gl.uniform1i(u['uDoubleSided'] ?? null, 0);
|
|
6096
6238
|
this.materialDoubleSided = false;
|
|
6239
|
+
this.materialHasMaps = false;
|
|
6240
|
+
this.currentMaterial = null;
|
|
6097
6241
|
this.currentSurfaceTexture = null;
|
|
6098
6242
|
/*
|
|
6099
6243
|
* **And the normal map, which this did not reset and had to.**
|
|
@@ -6213,6 +6357,7 @@ export class WebGL2Renderer {
|
|
|
6213
6357
|
and `Mesh.indexCount`. */
|
|
6214
6358
|
if (!mesh.complete || mesh.indexCount === 0)
|
|
6215
6359
|
return;
|
|
6360
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
6216
6361
|
/*
|
|
6217
6362
|
* Skipped where the flag asks and the bounds say so.
|
|
6218
6363
|
*
|
|
@@ -6405,6 +6550,7 @@ export class WebGL2Renderer {
|
|
|
6405
6550
|
and `Mesh.indexCount`. */
|
|
6406
6551
|
if (!mesh.complete || mesh.indexCount === 0)
|
|
6407
6552
|
return;
|
|
6553
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
6408
6554
|
if (opacity <= 0)
|
|
6409
6555
|
return;
|
|
6410
6556
|
/*
|
|
@@ -114,6 +114,8 @@ export interface GpuMesh {
|
|
|
114
114
|
* the key is built before the present map is consulted.
|
|
115
115
|
*/
|
|
116
116
|
readonly isSkinned: boolean;
|
|
117
|
+
/** Whether it has texture coordinates, without which a material's maps read one texel. */
|
|
118
|
+
readonly hasUvs: boolean;
|
|
117
119
|
/**
|
|
118
120
|
* Whether this mesh carries the per-vertex channel, which an instanced draw cannot.
|
|
119
121
|
*
|
|
@@ -257,6 +257,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
257
257
|
progress,
|
|
258
258
|
hasTangents: data.tangents !== undefined,
|
|
259
259
|
isSkinned: data.joints !== undefined,
|
|
260
|
+
hasUvs: data.uvs !== undefined,
|
|
260
261
|
skinOffsets: skinOffsetsOf(supplied),
|
|
261
262
|
hasChannel: data.channel !== undefined,
|
|
262
263
|
morph: data.morphTargets === undefined || data.morphTargetCount === undefined
|
|
@@ -392,6 +393,7 @@ function gpuMeshOf(parts) {
|
|
|
392
393
|
},
|
|
393
394
|
hasTangents: parts.hasTangents,
|
|
394
395
|
isSkinned: parts.isSkinned,
|
|
396
|
+
hasUvs: parts.hasUvs,
|
|
395
397
|
hasChannel: parts.hasChannel,
|
|
396
398
|
morph: parts.morph,
|
|
397
399
|
update: parts.update,
|
|
@@ -115,18 +115,27 @@ export function flatFragmentBindings(variant) {
|
|
|
115
115
|
return found;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
* The lit fragment stage's constants, set from the cache
|
|
118
|
+
* The lit fragment stage's constants, set from the cache's switches, keyed by each one's **id**.
|
|
119
119
|
*
|
|
120
120
|
* Not by its name, because an override declared with `@id` answers to the id alone — WebGPU's
|
|
121
121
|
* "pipeline-overridable constant identifier string" — and naga writes one on every specialisation
|
|
122
122
|
* constant. Keyed by name, Chrome refused every lit pipeline: *Pipeline overridable constant
|
|
123
|
-
* "GLASS_SHADOWS" not found*, and the frame drew nothing.
|
|
123
|
+
* "GLASS_SHADOWS" not found*, and the frame drew nothing. Every override the variant declares is
|
|
124
|
+
* set, so a switch added to the shader and not to the cache is refused here rather than defaulted.
|
|
124
125
|
*/
|
|
125
126
|
function flatFragmentConstants(variant, cache) {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
127
|
+
const overrides = flatFragmentBindings(variant).overrides;
|
|
128
|
+
if (overrides?.['GLASS_SHADOWS'] === undefined) {
|
|
128
129
|
throw new Error(`flatPass: variant "${variant}" has no glass switch`);
|
|
129
|
-
|
|
130
|
+
}
|
|
131
|
+
const constants = {};
|
|
132
|
+
for (const [name, id] of Object.entries(overrides)) {
|
|
133
|
+
if (!(name in cache.litSwitches)) {
|
|
134
|
+
throw new Error(`flatPass: variant "${variant}" declares a switch "${name}" no cache sets`);
|
|
135
|
+
}
|
|
136
|
+
constants[String(id)] = cache.litSwitches[name] ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
return constants;
|
|
130
139
|
}
|
|
131
140
|
/** The depth format both backends compare with. */
|
|
132
141
|
/* Re-exported so the twenty passes that already import it from here do not each learn a new
|
|
@@ -391,7 +400,7 @@ instanced = false,
|
|
|
391
400
|
oit = 'none',
|
|
392
401
|
/** Culls nothing, for a surface seen from both faces. The key must carry it: `|2s`. */
|
|
393
402
|
doubleSided = false) {
|
|
394
|
-
return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided));
|
|
403
|
+
return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided), true);
|
|
395
404
|
}
|
|
396
405
|
/**
|
|
397
406
|
* The same pipeline, compiled off the main thread.
|
|
@@ -407,7 +416,7 @@ export function flatPipelineAsync(cache, device, layout, variant, key, present,
|
|
|
407
416
|
skinned = false,
|
|
408
417
|
/** Whether this pipeline reads morph deltas. See `FlatVertexOptions`. */
|
|
409
418
|
morphed = false) {
|
|
410
|
-
return cache.getAsync(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed));
|
|
419
|
+
return cache.getAsync(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed), true);
|
|
411
420
|
}
|
|
412
421
|
/**
|
|
413
422
|
* The colour target for an order-independent pass, or null where this is an ordinary draw.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type LitSwitch } from '../../shaders/flat/glassTint.ts';
|
|
2
|
+
export type { LitSwitch };
|
|
1
3
|
/**
|
|
2
4
|
* Render pipelines, built once and looked up by name.
|
|
3
5
|
*
|
|
@@ -33,14 +35,27 @@ export declare class PipelineCache {
|
|
|
33
35
|
*/
|
|
34
36
|
readonly sampleCount: number;
|
|
35
37
|
/**
|
|
36
|
-
*
|
|
38
|
+
* Which of the lit stage's switches the pipelines built now have on: the pipeline-overridable
|
|
39
|
+
* constants the generated lit shader branches on (`GLASS_SHADOWS` and `litSwitchesGlsl`), which
|
|
40
|
+
* the device compiles away where they are off. Read by `flatPass` when it describes a pipeline.
|
|
37
41
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
+
* **Each is code a scene pays for in every lit pixel's registers whether it uses it or not**, so
|
|
43
|
+
* each is off until it is used: glass until the renderer is offered a pane, a fixture until one is
|
|
44
|
+
* loaded, the effects until a material carries a table, DriftLight until a volume is set — and
|
|
45
|
+
* then on for good, through `enable`. Clustering is the profile's, fixed at construction. Every
|
|
46
|
+
* scene paid for all of it from 4.5.0 to 4.8.1, and on a phone that was the lit pass: about 30%
|
|
47
|
+
* of the lit shader's instructions for glass alone, measured on RADV. **What it costs** is the
|
|
48
|
+
* frames between a feature's first use and its recompiled pipelines, which draw without it.
|
|
42
49
|
*/
|
|
50
|
+
readonly litSwitches: Record<LitSwitch, boolean>;
|
|
51
|
+
/** Whether glass may ever be switched on here: `glassShadows` other than `'off'`. */
|
|
43
52
|
readonly glassShadows: boolean;
|
|
53
|
+
/** How to describe each lit pipeline again, so `enable` can rebuild every one. */
|
|
54
|
+
private readonly litDescribers;
|
|
55
|
+
/** Bumped by every switch, so a lit build started before one is not kept after it. */
|
|
56
|
+
private litGeneration;
|
|
57
|
+
/** The last rebuild, which the next one waits for so they land in the order they were asked. */
|
|
58
|
+
private litRebuild;
|
|
44
59
|
private readonly device;
|
|
45
60
|
private readonly pipelines;
|
|
46
61
|
/**
|
|
@@ -58,7 +73,9 @@ export declare class PipelineCache {
|
|
|
58
73
|
* wait for readiness rather than discovering it mid-frame.
|
|
59
74
|
*/
|
|
60
75
|
private readonly compiling;
|
|
61
|
-
constructor(device: GPUDevice, format: GPUTextureFormat, sampleCount?: number, glassShadows?: boolean
|
|
76
|
+
constructor(device: GPUDevice, format: GPUTextureFormat, sampleCount?: number, glassShadows?: boolean,
|
|
77
|
+
/** The profile's `clusteredLights`, which compiles the clustered arm in or leaves it out. */
|
|
78
|
+
clusteredLights?: boolean);
|
|
62
79
|
/** How many distinct pipelines have been built. Read by tests and diagnostics. */
|
|
63
80
|
get size(): number;
|
|
64
81
|
/**
|
|
@@ -69,7 +86,9 @@ export declare class PipelineCache {
|
|
|
69
86
|
* difference is only *when* the driver does the work, and that difference is the whole bug
|
|
70
87
|
* this exists for.
|
|
71
88
|
*/
|
|
72
|
-
getAsync(key: string, describe: () => GPURenderPipelineDescriptor
|
|
89
|
+
getAsync(key: string, describe: () => GPURenderPipelineDescriptor,
|
|
90
|
+
/** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
|
|
91
|
+
lit?: boolean): Promise<GPURenderPipeline>;
|
|
73
92
|
/**
|
|
74
93
|
* Resolve once every pipeline asked for so far has finished compiling.
|
|
75
94
|
*
|
|
@@ -90,7 +109,20 @@ export declare class PipelineCache {
|
|
|
90
109
|
* too**: a nested object with vertex buffer layouts and blend state, constructed to be
|
|
91
110
|
* thrown away on every hit. Passing a function means a hit costs one `Map` lookup.
|
|
92
111
|
*/
|
|
93
|
-
get(key: string, describe: () => GPURenderPipelineDescriptor
|
|
112
|
+
get(key: string, describe: () => GPURenderPipelineDescriptor,
|
|
113
|
+
/** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
|
|
114
|
+
lit?: boolean): GPURenderPipeline;
|
|
115
|
+
/**
|
|
116
|
+
* Turn one of the lit stage's switches on, for every lit pipeline built from now on and every one
|
|
117
|
+
* already built. See `litSwitches`.
|
|
118
|
+
*
|
|
119
|
+
* **The rebuilt set lands at once**, when every one of them has compiled, so no frame draws some
|
|
120
|
+
* surfaces with a feature and others without. Until then the pipelines already built go on drawing
|
|
121
|
+
* as they were. Once only: a switch is not turned off again when the last user of it leaves,
|
|
122
|
+
* because a second switch would cost a second compile for a saving the first use showed was not
|
|
123
|
+
* needed. Glass is never turned on where the profile asked for no glass shadows.
|
|
124
|
+
*/
|
|
125
|
+
enable(feature: LitSwitch): Promise<void>;
|
|
94
126
|
/**
|
|
95
127
|
* Drop every pipeline.
|
|
96
128
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { noLitSwitches } from '../../shaders/flat/glassTint.js';
|
|
1
2
|
/**
|
|
2
3
|
* Render pipelines, built once and looked up by name.
|
|
3
4
|
*
|
|
@@ -33,14 +34,27 @@ export class PipelineCache {
|
|
|
33
34
|
*/
|
|
34
35
|
sampleCount;
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
+
* Which of the lit stage's switches the pipelines built now have on: the pipeline-overridable
|
|
38
|
+
* constants the generated lit shader branches on (`GLASS_SHADOWS` and `litSwitchesGlsl`), which
|
|
39
|
+
* the device compiles away where they are off. Read by `flatPass` when it describes a pipeline.
|
|
37
40
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* **Each is code a scene pays for in every lit pixel's registers whether it uses it or not**, so
|
|
42
|
+
* each is off until it is used: glass until the renderer is offered a pane, a fixture until one is
|
|
43
|
+
* loaded, the effects until a material carries a table, DriftLight until a volume is set — and
|
|
44
|
+
* then on for good, through `enable`. Clustering is the profile's, fixed at construction. Every
|
|
45
|
+
* scene paid for all of it from 4.5.0 to 4.8.1, and on a phone that was the lit pass: about 30%
|
|
46
|
+
* of the lit shader's instructions for glass alone, measured on RADV. **What it costs** is the
|
|
47
|
+
* frames between a feature's first use and its recompiled pipelines, which draw without it.
|
|
42
48
|
*/
|
|
49
|
+
litSwitches = noLitSwitches();
|
|
50
|
+
/** Whether glass may ever be switched on here: `glassShadows` other than `'off'`. */
|
|
43
51
|
glassShadows;
|
|
52
|
+
/** How to describe each lit pipeline again, so `enable` can rebuild every one. */
|
|
53
|
+
litDescribers = new Map();
|
|
54
|
+
/** Bumped by every switch, so a lit build started before one is not kept after it. */
|
|
55
|
+
litGeneration = 0;
|
|
56
|
+
/** The last rebuild, which the next one waits for so they land in the order they were asked. */
|
|
57
|
+
litRebuild = Promise.resolve();
|
|
44
58
|
device;
|
|
45
59
|
pipelines = new Map();
|
|
46
60
|
/**
|
|
@@ -58,11 +72,14 @@ export class PipelineCache {
|
|
|
58
72
|
* wait for readiness rather than discovering it mid-frame.
|
|
59
73
|
*/
|
|
60
74
|
compiling = new Map();
|
|
61
|
-
constructor(device, format, sampleCount = 1, glassShadows = true
|
|
75
|
+
constructor(device, format, sampleCount = 1, glassShadows = true,
|
|
76
|
+
/** The profile's `clusteredLights`, which compiles the clustered arm in or leaves it out. */
|
|
77
|
+
clusteredLights = false) {
|
|
62
78
|
this.device = device;
|
|
63
79
|
this.format = format;
|
|
64
80
|
this.sampleCount = sampleCount;
|
|
65
81
|
this.glassShadows = glassShadows;
|
|
82
|
+
this.litSwitches.CLUSTERED_LIGHTS = clusteredLights;
|
|
66
83
|
}
|
|
67
84
|
/** How many distinct pipelines have been built. Read by tests and diagnostics. */
|
|
68
85
|
get size() {
|
|
@@ -76,13 +93,18 @@ export class PipelineCache {
|
|
|
76
93
|
* difference is only *when* the driver does the work, and that difference is the whole bug
|
|
77
94
|
* this exists for.
|
|
78
95
|
*/
|
|
79
|
-
async getAsync(key, describe
|
|
96
|
+
async getAsync(key, describe,
|
|
97
|
+
/** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
|
|
98
|
+
lit = false) {
|
|
99
|
+
if (lit)
|
|
100
|
+
this.litDescribers.set(key, describe);
|
|
80
101
|
const existing = this.pipelines.get(key);
|
|
81
102
|
if (existing !== undefined)
|
|
82
103
|
return existing;
|
|
83
104
|
const inFlight = this.compiling.get(key);
|
|
84
105
|
if (inFlight !== undefined)
|
|
85
106
|
return inFlight;
|
|
107
|
+
const generation = this.litGeneration;
|
|
86
108
|
/*
|
|
87
109
|
* **Falls back where the async form is missing.** It is part of the standard, but a device
|
|
88
110
|
* can be a test double, a polyfill or an older implementation, and a renderer that throws
|
|
@@ -94,7 +116,9 @@ export class PipelineCache {
|
|
|
94
116
|
? this.device.createRenderPipelineAsync(describe())
|
|
95
117
|
: Promise.resolve(this.device.createRenderPipeline(describe()));
|
|
96
118
|
const promise = build.then((built) => {
|
|
97
|
-
|
|
119
|
+
/* A lit pipeline started before a switch is not kept: the switch rebuilds it. */
|
|
120
|
+
if (!lit || generation === this.litGeneration)
|
|
121
|
+
this.pipelines.set(key, built);
|
|
98
122
|
this.compiling.delete(key);
|
|
99
123
|
return built;
|
|
100
124
|
});
|
|
@@ -129,7 +153,11 @@ export class PipelineCache {
|
|
|
129
153
|
* too**: a nested object with vertex buffer layouts and blend state, constructed to be
|
|
130
154
|
* thrown away on every hit. Passing a function means a hit costs one `Map` lookup.
|
|
131
155
|
*/
|
|
132
|
-
get(key, describe
|
|
156
|
+
get(key, describe,
|
|
157
|
+
/** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
|
|
158
|
+
lit = false) {
|
|
159
|
+
if (lit)
|
|
160
|
+
this.litDescribers.set(key, describe);
|
|
133
161
|
const existing = this.pipelines.get(key);
|
|
134
162
|
if (existing !== undefined)
|
|
135
163
|
return existing;
|
|
@@ -137,6 +165,45 @@ export class PipelineCache {
|
|
|
137
165
|
this.pipelines.set(key, built);
|
|
138
166
|
return built;
|
|
139
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Turn one of the lit stage's switches on, for every lit pipeline built from now on and every one
|
|
170
|
+
* already built. See `litSwitches`.
|
|
171
|
+
*
|
|
172
|
+
* **The rebuilt set lands at once**, when every one of them has compiled, so no frame draws some
|
|
173
|
+
* surfaces with a feature and others without. Until then the pipelines already built go on drawing
|
|
174
|
+
* as they were. Once only: a switch is not turned off again when the last user of it leaves,
|
|
175
|
+
* because a second switch would cost a second compile for a saving the first use showed was not
|
|
176
|
+
* needed. Glass is never turned on where the profile asked for no glass shadows.
|
|
177
|
+
*/
|
|
178
|
+
enable(feature) {
|
|
179
|
+
if (this.litSwitches[feature])
|
|
180
|
+
return this.litRebuild;
|
|
181
|
+
if (feature === 'GLASS_SHADOWS' && !this.glassShadows)
|
|
182
|
+
return this.litRebuild;
|
|
183
|
+
this.litSwitches[feature] = true;
|
|
184
|
+
this.litGeneration += 1;
|
|
185
|
+
const generation = this.litGeneration;
|
|
186
|
+
this.litRebuild = this.litRebuild.then(async () => {
|
|
187
|
+
/* A later switch has its own rebuild, which describes every pipeline with this one on too. */
|
|
188
|
+
if (generation !== this.litGeneration)
|
|
189
|
+
return;
|
|
190
|
+
const rebuilt = [...this.litDescribers].map(([key, describe]) => {
|
|
191
|
+
const descriptor = describe();
|
|
192
|
+
const build = typeof this.device.createRenderPipelineAsync === 'function'
|
|
193
|
+
? this.device.createRenderPipelineAsync(descriptor)
|
|
194
|
+
: Promise.resolve(this.device.createRenderPipeline(descriptor));
|
|
195
|
+
return build.then((pipeline) => [key, pipeline]);
|
|
196
|
+
});
|
|
197
|
+
const results = await Promise.allSettled(rebuilt);
|
|
198
|
+
if (generation !== this.litGeneration)
|
|
199
|
+
return;
|
|
200
|
+
for (const result of results) {
|
|
201
|
+
if (result.status === 'fulfilled')
|
|
202
|
+
this.pipelines.set(result.value[0], result.value[1]);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return this.litRebuild;
|
|
206
|
+
}
|
|
140
207
|
/**
|
|
141
208
|
* Drop every pipeline.
|
|
142
209
|
*
|