@driftengine/core 4.1.3 → 4.1.4
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 +328 -297
- package/README.md +1 -1
- package/dist/index.js +12 -2
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +46 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +105 -0
- package/dist/render/backend/webgpu/renderer.js +10 -2
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +91 -5
- package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
- package/dist/render/shaders/flat/pointShadow.js +42 -16
- package/dist/render/shaders/flat/preamble.js +44 -7
- package/dist/render/shaders/generated/flat.wgsl.js +16 -16
- package/dist/render/textLayout.d.ts +58 -0
- package/dist/render/textLayout.js +83 -0
- package/dist/render/textRenderer.js +7 -3
- package/package.json +6 -6
- package/src/index.ts +12 -2
- package/src/render/backend/webgpu/gpuDrivenPass.ts +105 -0
- package/src/render/backend/webgpu/renderer.ts +14 -2
- package/src/render/shaders/flat/main.ts +91 -5
- package/src/render/shaders/flat/pointShadow.ts +42 -16
- package/src/render/shaders/flat/preamble.ts +44 -7
- package/src/render/shaders/generated/flat.wgsl.ts +16 -16
- package/src/render/textLayout.ts +88 -0
- package/src/render/textRenderer.ts +22 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The runtime everything needs: renderer, loop, camera, input, geometry. Collision and dynamics live in [`@driftengine/physics`](../physics/README.md), which this package depends on and re-exports whole, so reaching for them through this barrel works as it always did.
|
|
4
4
|
|
|
5
|
-
**Cost:
|
|
5
|
+
**Cost: 721.2 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
|
|
6
6
|
3% — the number is a fact about the build rather than a claim in a document.
|
|
7
7
|
|
|
8
8
|
Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
|
package/dist/index.js
CHANGED
|
@@ -102,8 +102,18 @@ export { createRenderer } from './render/backend/createRenderer.js';
|
|
|
102
102
|
* A consumer who names neither pays nothing: `core-only` is measured with these exported and does
|
|
103
103
|
* not move, because nothing in the forward path reaches them.
|
|
104
104
|
*
|
|
105
|
-
* It draws vertex colour, one directional term, a hemispheric ambient and an emissive add — not
|
|
106
|
-
* the standard material
|
|
105
|
+
* **It draws vertex colour, one directional term, a hemispheric ambient and an emissive add — not
|
|
106
|
+
* the standard material**, and that is the sentence to read before reaching for it. There is no
|
|
107
|
+
* flag that routes `drawMesh` through this: a consumer packs a scene and contributes the pass, and
|
|
108
|
+
* `setMaterial`, `setSurfaceGrain`, `setSurfaceRelief` and a skin palette reach none of it. What
|
|
109
|
+
* would change that is a second copy of the standard material, which `gpuDrivenPass.ts` refuses in
|
|
110
|
+
* its own header.
|
|
111
|
+
*
|
|
112
|
+
* **The line that used to be here said `DRAFT_SCENES`, never `SCENES`, and it went stale the day
|
|
113
|
+
* the city was published.** `city` is in `SCENES` and says `pipelines: ['gpu-driven']`, so a host
|
|
114
|
+
* that cannot run the second pipeline reads that from the list rather than from a mount that
|
|
115
|
+
* refuses. The rule it was standing in for is the one above: what gates a scene is whether this
|
|
116
|
+
* pass can draw it, not which array it is in.
|
|
107
117
|
*/
|
|
108
118
|
export { GPU_DRIVEN_LISTS, GpuDrivenPass, MATERIAL_FLOATS,
|
|
109
119
|
/*
|
|
@@ -470,6 +470,30 @@ export declare class GpuDrivenPass implements PassDefinition {
|
|
|
470
470
|
* before the device answers, which nobody sees; the alternative is a frame that draws nothing.
|
|
471
471
|
*/
|
|
472
472
|
private blendUsable;
|
|
473
|
+
/**
|
|
474
|
+
* The same question about *every other* pipeline in this pass, and the reason it exists is that
|
|
475
|
+
* 4.1.3 asked it about one.
|
|
476
|
+
*
|
|
477
|
+
* **The guard above was written from a log that named `gpu-driven blend`, and the four render
|
|
478
|
+
* pipelines and eight compute pipelines beside it were left with the shape that caused the
|
|
479
|
+
* report.** Reported again from an Android handset once that fix shipped: the transparent pass
|
|
480
|
+
* now skips itself correctly, the overlay still draws and the frame counter still counts — and
|
|
481
|
+
* the world is black, which is what an invalid *opaque* pipeline looks like once the blended one
|
|
482
|
+
* has stopped taking the command buffer with it.
|
|
483
|
+
*
|
|
484
|
+
* So the whole of `buildPipelines` is built inside one pair of scopes and nothing this pass
|
|
485
|
+
* encodes runs until they come back clean. The blend pipeline pushes its own pair *inside* that
|
|
486
|
+
* region, so the two verdicts stay separate: a device that refuses only the transparent pipeline
|
|
487
|
+
* still draws everything opaque, and a device that refuses anything else turns this pass off
|
|
488
|
+
* rather than encoding against a handle the driver has already rejected.
|
|
489
|
+
*
|
|
490
|
+
* **What it gives up** is the first frames, which draw nothing while the device is still
|
|
491
|
+
* answering. **What would make it wrong** is a device that never resolves an error scope; the
|
|
492
|
+
* `popErrorScope` check below is the same concession the blend guard makes, for the same reason.
|
|
493
|
+
*/
|
|
494
|
+
private usable;
|
|
495
|
+
/** Why this pass refused itself, in the words the device used, or null while it has not. */
|
|
496
|
+
private refusal;
|
|
473
497
|
/** Builds the transparent raster's bindings, held until there is a pipeline worth binding to. */
|
|
474
498
|
private makeBlendGroup;
|
|
475
499
|
private blendResolvePipeline;
|
|
@@ -641,6 +665,28 @@ export declare class GpuDrivenPass implements PassDefinition {
|
|
|
641
665
|
* this came from was five errors that were all the same one.
|
|
642
666
|
*/
|
|
643
667
|
private watchBlendPipeline;
|
|
668
|
+
/**
|
|
669
|
+
* Close the scopes every other pipeline was built in, and refuse the pass where one came back.
|
|
670
|
+
*
|
|
671
|
+
* **This is `watchBlendPipeline` widened to the rest of the pass**, and the reason it is separate
|
|
672
|
+
* is that the two refusals mean different things: a blend pipeline nobody can compile costs the
|
|
673
|
+
* glass, and a raster, shadow, blit or compute pipeline nobody can compile costs the world. The
|
|
674
|
+
* first degrades; the second has nothing to degrade to, so it says so and draws nothing rather
|
|
675
|
+
* than encoding a command buffer the driver will reject whole.
|
|
676
|
+
*
|
|
677
|
+
* A device with no scopes is taken at its word, exactly as the blend guard takes it: refusing to
|
|
678
|
+
* draw on hardware that never said no is the worse of the two failures.
|
|
679
|
+
*/
|
|
680
|
+
private watchPipelines;
|
|
681
|
+
/**
|
|
682
|
+
* Why this pass is drawing nothing, in the words the device used, or null where it is not.
|
|
683
|
+
*
|
|
684
|
+
* **Exposed because a black frame that says nothing is the bug this was reported as, twice.** A
|
|
685
|
+
* pipeline refusal arrives asynchronously, long after `createRenderer` has decided the backend
|
|
686
|
+
* can run this at all, so there is no earlier place to throw — and a scene that can read this can
|
|
687
|
+
* tell somebody what happened instead of presenting an empty picture.
|
|
688
|
+
*/
|
|
689
|
+
refusedBecause(): string | null;
|
|
644
690
|
private buildPipelines;
|
|
645
691
|
/**
|
|
646
692
|
* The groups that name `history` and `drawn`, rebuilt when the two swap.
|
|
@@ -445,6 +445,30 @@ export class GpuDrivenPass {
|
|
|
445
445
|
* before the device answers, which nobody sees; the alternative is a frame that draws nothing.
|
|
446
446
|
*/
|
|
447
447
|
blendUsable = false;
|
|
448
|
+
/**
|
|
449
|
+
* The same question about *every other* pipeline in this pass, and the reason it exists is that
|
|
450
|
+
* 4.1.3 asked it about one.
|
|
451
|
+
*
|
|
452
|
+
* **The guard above was written from a log that named `gpu-driven blend`, and the four render
|
|
453
|
+
* pipelines and eight compute pipelines beside it were left with the shape that caused the
|
|
454
|
+
* report.** Reported again from an Android handset once that fix shipped: the transparent pass
|
|
455
|
+
* now skips itself correctly, the overlay still draws and the frame counter still counts — and
|
|
456
|
+
* the world is black, which is what an invalid *opaque* pipeline looks like once the blended one
|
|
457
|
+
* has stopped taking the command buffer with it.
|
|
458
|
+
*
|
|
459
|
+
* So the whole of `buildPipelines` is built inside one pair of scopes and nothing this pass
|
|
460
|
+
* encodes runs until they come back clean. The blend pipeline pushes its own pair *inside* that
|
|
461
|
+
* region, so the two verdicts stay separate: a device that refuses only the transparent pipeline
|
|
462
|
+
* still draws everything opaque, and a device that refuses anything else turns this pass off
|
|
463
|
+
* rather than encoding against a handle the driver has already rejected.
|
|
464
|
+
*
|
|
465
|
+
* **What it gives up** is the first frames, which draw nothing while the device is still
|
|
466
|
+
* answering. **What would make it wrong** is a device that never resolves an error scope; the
|
|
467
|
+
* `popErrorScope` check below is the same concession the blend guard makes, for the same reason.
|
|
468
|
+
*/
|
|
469
|
+
usable = false;
|
|
470
|
+
/** Why this pass refused itself, in the words the device used, or null while it has not. */
|
|
471
|
+
refusal = null;
|
|
448
472
|
/** Builds the transparent raster's bindings, held until there is a pipeline worth binding to. */
|
|
449
473
|
makeBlendGroup = null;
|
|
450
474
|
blendResolvePipeline = null;
|
|
@@ -687,7 +711,18 @@ export class GpuDrivenPass {
|
|
|
687
711
|
});
|
|
688
712
|
}
|
|
689
713
|
this.buildBuffers(device.device);
|
|
714
|
+
/*
|
|
715
|
+
* **Every pipeline this pass owns, inside one pair of scopes.** See `usable`: the blend
|
|
716
|
+
* pipeline pushes its own pair within this region, so its refusal stays its own and any other
|
|
717
|
+
* refusal is caught here and turns the pass off instead of reaching an encoder.
|
|
718
|
+
*/
|
|
719
|
+
const scoped = typeof device.device.pushErrorScope === 'function';
|
|
720
|
+
if (scoped) {
|
|
721
|
+
device.device.pushErrorScope('validation');
|
|
722
|
+
device.device.pushErrorScope('internal');
|
|
723
|
+
}
|
|
690
724
|
this.buildPipelines(device.device, device.format, device.samples);
|
|
725
|
+
this.watchPipelines(device.device, scoped);
|
|
691
726
|
if (this.width > 0)
|
|
692
727
|
this.buildTargets(device.device);
|
|
693
728
|
}
|
|
@@ -1000,6 +1035,50 @@ export class GpuDrivenPass {
|
|
|
1000
1035
|
void device.popErrorScope().then((error) => settle(error, 'internal'));
|
|
1001
1036
|
void device.popErrorScope().then((error) => settle(error, 'validation'));
|
|
1002
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Close the scopes every other pipeline was built in, and refuse the pass where one came back.
|
|
1040
|
+
*
|
|
1041
|
+
* **This is `watchBlendPipeline` widened to the rest of the pass**, and the reason it is separate
|
|
1042
|
+
* is that the two refusals mean different things: a blend pipeline nobody can compile costs the
|
|
1043
|
+
* glass, and a raster, shadow, blit or compute pipeline nobody can compile costs the world. The
|
|
1044
|
+
* first degrades; the second has nothing to degrade to, so it says so and draws nothing rather
|
|
1045
|
+
* than encoding a command buffer the driver will reject whole.
|
|
1046
|
+
*
|
|
1047
|
+
* A device with no scopes is taken at its word, exactly as the blend guard takes it: refusing to
|
|
1048
|
+
* draw on hardware that never said no is the worse of the two failures.
|
|
1049
|
+
*/
|
|
1050
|
+
watchPipelines(device, scoped) {
|
|
1051
|
+
if (!scoped || typeof device.popErrorScope !== 'function') {
|
|
1052
|
+
this.usable = true;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
let outstanding = 2;
|
|
1056
|
+
const settle = (error, scope) => {
|
|
1057
|
+
outstanding -= 1;
|
|
1058
|
+
if (error !== null && this.refusal === null) {
|
|
1059
|
+
this.refusal = error.message;
|
|
1060
|
+
console.warn(`[driftengine] this device refused a gpu-driven pipeline (${scope}), so this pass draws ` +
|
|
1061
|
+
`nothing and the rest of the frame is unaffected: ${error.message}`);
|
|
1062
|
+
}
|
|
1063
|
+
/* Usable only once both have come back clean, which is what keeps an invalid handle out. */
|
|
1064
|
+
if (outstanding > 0 || this.refusal !== null)
|
|
1065
|
+
return;
|
|
1066
|
+
this.usable = true;
|
|
1067
|
+
};
|
|
1068
|
+
void device.popErrorScope().then((error) => settle(error, 'internal'));
|
|
1069
|
+
void device.popErrorScope().then((error) => settle(error, 'validation'));
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Why this pass is drawing nothing, in the words the device used, or null where it is not.
|
|
1073
|
+
*
|
|
1074
|
+
* **Exposed because a black frame that says nothing is the bug this was reported as, twice.** A
|
|
1075
|
+
* pipeline refusal arrives asynchronously, long after `createRenderer` has decided the backend
|
|
1076
|
+
* can run this at all, so there is no earlier place to throw — and a scene that can read this can
|
|
1077
|
+
* tell somebody what happened instead of presenting an empty picture.
|
|
1078
|
+
*/
|
|
1079
|
+
refusedBecause() {
|
|
1080
|
+
return this.refusal;
|
|
1081
|
+
}
|
|
1003
1082
|
buildPipelines(device, format, samples) {
|
|
1004
1083
|
const compute = (label, code) => device.createComputePipeline({
|
|
1005
1084
|
label: `gpu-driven ${label}`,
|
|
@@ -1935,6 +2014,9 @@ export class GpuDrivenPass {
|
|
|
1935
2014
|
prepare(ctx) {
|
|
1936
2015
|
if (ctx.backend !== 'webgpu')
|
|
1937
2016
|
return;
|
|
2017
|
+
/* Nothing is encoded until the device has answered for the pipelines. See `usable`. */
|
|
2018
|
+
if (!this.usable)
|
|
2019
|
+
return;
|
|
1938
2020
|
const device = this.device;
|
|
1939
2021
|
if (device === null || this.levels.length === 0)
|
|
1940
2022
|
return;
|
|
@@ -2165,6 +2247,26 @@ export class GpuDrivenPass {
|
|
|
2165
2247
|
return;
|
|
2166
2248
|
}
|
|
2167
2249
|
case 'blendResolve': {
|
|
2250
|
+
/*
|
|
2251
|
+
* **Skipped on the same verdict as the draw, because the draw is what clears what this
|
|
2252
|
+
* reads.** This is the whole of the reported black frame, and the line above it predicted
|
|
2253
|
+
* it: the blend pass clears reveal to one, meaning nothing has covered this pixel yet, and
|
|
2254
|
+
* says in as many words that cleared to zero "the resolve would show no scene anywhere the
|
|
2255
|
+
* pass ran, which is a black frame that looks like the blend working".
|
|
2256
|
+
*
|
|
2257
|
+
* 4.1.2 skipped the draw and took its clear with it, and left this running against textures
|
|
2258
|
+
* nothing had written — a reveal of zero over every pixel, which is exactly "fully covered".
|
|
2259
|
+
* Reported from a Galaxy S23 Ultra and then an Adreno 740, both of which refuse only
|
|
2260
|
+
* `gpu-driven blend` and both of which drew the city into a buffer this then wiped: the
|
|
2261
|
+
* page's own readout said 43 fps and 1,087 clusters drawn while the screen was black.
|
|
2262
|
+
*
|
|
2263
|
+
* There is nothing to composite when nothing was rasterised, so the honest answer is not to
|
|
2264
|
+
* composite. **What would make it wrong** is a caller that expects this pass to have run for
|
|
2265
|
+
* some other reason; nothing does, and the opaque image is already in `colourView` before
|
|
2266
|
+
* this would load it.
|
|
2267
|
+
*/
|
|
2268
|
+
if (!this.blendUsable)
|
|
2269
|
+
return;
|
|
2168
2270
|
const pass = encoder.beginRenderPass({
|
|
2169
2271
|
label: 'gpu-driven blend resolve',
|
|
2170
2272
|
colorAttachments: [
|
|
@@ -2468,6 +2570,9 @@ export class GpuDrivenPass {
|
|
|
2468
2570
|
draw(ctx) {
|
|
2469
2571
|
if (ctx.backend !== 'webgpu')
|
|
2470
2572
|
return;
|
|
2573
|
+
/* The same gate as `prepare`: an unanswered or refused pipeline set encodes nothing at all. */
|
|
2574
|
+
if (!this.usable)
|
|
2575
|
+
return;
|
|
2471
2576
|
if (this.blitPipeline === null || this.blitGroup === null)
|
|
2472
2577
|
return;
|
|
2473
2578
|
if (ctx.outputTransform !== 0 && !this.gradeWarned) {
|
|
@@ -38,6 +38,7 @@ import { POINT_SHADOW_SAMPLER, PointShadowSystem, } from '../../pointShadowSyste
|
|
|
38
38
|
import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.js';
|
|
39
39
|
import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, panelPipeline, PANEL_FRAG_FIELDS, PANEL_FRAG_SIZE, PANEL_VERTEX_COUNT, PANEL_VERT_FIELDS, PANEL_VERT_SIZE, } from './panelPass.js';
|
|
40
40
|
import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
|
|
41
|
+
import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
|
|
41
42
|
import { GpuSurfaceTexture } from './surfaceTexturePass.js';
|
|
42
43
|
import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
|
|
43
44
|
import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
|
|
@@ -2485,8 +2486,15 @@ export class WebGPURenderer {
|
|
|
2485
2486
|
const at = (name) => TEXT_VERT_FIELDS[name]?.offset ?? 0;
|
|
2486
2487
|
const v = this.textVerts;
|
|
2487
2488
|
v.writeFloats(vertexSlot, at('uViewport'), [viewportWidth, viewportHeight]);
|
|
2488
|
-
|
|
2489
|
-
|
|
2489
|
+
/* Snapped with the cell below: a whole pitch on a fractional origin still splits a stroke.
|
|
2490
|
+
See `deviceSnappedOrigin`. */
|
|
2491
|
+
v.writeFloats(vertexSlot, at('uOrigin'), [
|
|
2492
|
+
deviceSnappedOrigin(originX, viewportWidth, this.surface.canvas.width),
|
|
2493
|
+
deviceSnappedOrigin(originY, viewportHeight, this.surface.canvas.height),
|
|
2494
|
+
]);
|
|
2495
|
+
/* Whole device pixels per cell, or a 5x7 face draws strokes of two different widths. The
|
|
2496
|
+
decision is shared with WebGL2; only this binding is per-backend. See `textLayout.ts`. */
|
|
2497
|
+
v.writeFloat(vertexSlot, at('uCellSize'), deviceSnappedCellSize(style.cellSize, viewportWidth, this.surface.canvas.width));
|
|
2490
2498
|
/* Far enough that a whole line barely converges, near enough that a turn reads as a turn. */
|
|
2491
2499
|
v.writeFloat(vertexSlot, at('uDepth'), Math.max(viewportWidth, 600) * 1.4);
|
|
2492
2500
|
v.writeFloat(vertexSlot, at('uReveal'), style.reveal);
|