@driftengine/core 4.9.0 → 4.10.0
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 +62 -11
- package/README.md +43 -38
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -3
- package/dist/render/ambientHarmonics.d.ts +1 -1
- package/dist/render/ambientHarmonics.js +1 -1
- package/dist/render/areaLights.d.ts +4 -4
- package/dist/render/areaLights.js +1 -1
- package/dist/render/backend/api.d.ts +10 -3
- package/dist/render/backend/select.js +11 -5
- package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
- package/dist/render/backend/webgl2/boneAnimations.js +80 -0
- package/dist/render/backend/webgl2/instanced.d.ts +5 -1
- package/dist/render/backend/webgl2/instanced.js +13 -1
- package/dist/render/backend/webgl2/renderer.d.ts +90 -2
- package/dist/render/backend/webgl2/renderer.js +253 -24
- package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
- package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
- package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
- package/dist/render/backend/webgpu/boneAnimations.js +76 -0
- package/dist/render/backend/webgpu/buffers.d.ts +19 -4
- package/dist/render/backend/webgpu/buffers.js +18 -11
- package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
- package/dist/render/backend/webgpu/compressedUpload.js +53 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
- package/dist/render/backend/webgpu/depthPass.js +49 -14
- package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
- package/dist/render/backend/webgpu/drawCommand.js +10 -3
- package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
- package/dist/render/backend/webgpu/flatPass.js +136 -20
- package/dist/render/backend/webgpu/instanced.d.ts +14 -1
- package/dist/render/backend/webgpu/instanced.js +24 -1
- package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
- package/dist/render/backend/webgpu/materialStore.js +265 -0
- package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
- package/dist/render/backend/webgpu/pipelineCache.js +7 -0
- package/dist/render/backend/webgpu/renderer.d.ts +152 -35
- package/dist/render/backend/webgpu/renderer.js +787 -142
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
- package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
- package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
- package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
- package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
- package/dist/render/backend/webgpu/staticBundles.js +241 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
- package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
- package/dist/render/backend/webgpu/uniformRing.js +9 -1
- package/dist/render/bloomChain.d.ts +3 -3
- package/dist/render/boneAnimation.d.ts +92 -0
- package/dist/render/boneAnimation.js +133 -0
- package/dist/render/compressedSource.d.ts +33 -13
- package/dist/render/compressedSource.js +70 -25
- package/dist/render/glCompressed.js +83 -0
- package/dist/render/instances.d.ts +26 -0
- package/dist/render/lightBudget.d.ts +10 -0
- package/dist/render/lightBudget.js +10 -0
- package/dist/render/lightmap.d.ts +1 -1
- package/dist/render/mesh.d.ts +5 -0
- package/dist/render/mesh.js +6 -0
- package/dist/render/pointLightSelection.d.ts +4 -4
- package/dist/render/screenSpaceReflection.d.ts +2 -1
- package/dist/render/shaders/boneAnimation.d.ts +19 -0
- package/dist/render/shaders/boneAnimation.js +63 -0
- package/dist/render/shaders/depth.d.ts +7 -0
- package/dist/render/shaders/depth.js +35 -10
- package/dist/render/shaders/flat/index.d.ts +6 -0
- package/dist/render/shaders/flat/index.js +27 -5
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +15 -5
- package/dist/render/shaders/flat/models.js +1 -1
- package/dist/render/shaders/flat/overlay.js +1 -1
- package/dist/render/shaders/flat/pointShadow.js +2 -2
- package/dist/render/shaders/flat/preamble.js +38 -24
- package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
- package/dist/render/shaders/generated/depth.wgsl.js +93 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +3077 -2903
- package/dist/render/shaders/generated/flat.wgsl.js +3100 -2925
- package/dist/render/shaders/outputTransform.d.ts +7 -0
- package/dist/render/shaders/outputTransform.js +28 -14
- package/dist/render/shaders/vertexChannel.d.ts +7 -0
- package/dist/render/shaders/vertexChannel.js +26 -0
- package/dist/render/staticDraws.d.ts +47 -0
- package/dist/render/staticDraws.js +43 -0
- package/dist/render/surfaceTexture.d.ts +38 -7
- package/dist/render/surfaceTexture.js +56 -17
- package/package.json +6 -6
- package/src/index.ts +8 -2
- package/src/render/ambientHarmonics.ts +1 -1
- package/src/render/areaLights.ts +4 -4
- package/src/render/backend/api.ts +13 -3
- package/src/render/backend/select.ts +10 -5
- package/src/render/backend/webgl2/boneAnimations.ts +105 -0
- package/src/render/backend/webgl2/instanced.ts +20 -1
- package/src/render/backend/webgl2/renderer.ts +315 -24
- package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
- package/src/render/backend/webgpu/boneAnimations.ts +97 -0
- package/src/render/backend/webgpu/buffers.ts +42 -16
- package/src/render/backend/webgpu/compressedUpload.ts +59 -9
- package/src/render/backend/webgpu/depthPass.ts +51 -10
- package/src/render/backend/webgpu/drawCommand.ts +22 -4
- package/src/render/backend/webgpu/flatPass.ts +187 -14
- package/src/render/backend/webgpu/instanced.ts +32 -1
- package/src/render/backend/webgpu/materialStore.ts +276 -0
- package/src/render/backend/webgpu/pipelineCache.ts +7 -0
- package/src/render/backend/webgpu/renderer.ts +928 -155
- package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
- package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
- package/src/render/backend/webgpu/staticBundles.ts +363 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
- package/src/render/backend/webgpu/uniformRing.ts +10 -1
- package/src/render/bloomChain.ts +3 -3
- package/src/render/boneAnimation.ts +223 -0
- package/src/render/compressedSource.ts +121 -29
- package/src/render/glCompressed.ts +83 -0
- package/src/render/instances.ts +27 -0
- package/src/render/lightBudget.ts +11 -0
- package/src/render/lightmap.ts +1 -1
- package/src/render/mesh.ts +6 -0
- package/src/render/pointLightSelection.ts +4 -4
- package/src/render/screenSpaceReflection.ts +2 -1
- package/src/render/shaders/boneAnimation.ts +63 -0
- package/src/render/shaders/depth.ts +45 -10
- package/src/render/shaders/flat/index.ts +33 -5
- package/src/render/shaders/flat/main.ts +15 -5
- package/src/render/shaders/flat/models.ts +1 -1
- package/src/render/shaders/flat/overlay.ts +1 -1
- package/src/render/shaders/flat/pointShadow.ts +2 -2
- package/src/render/shaders/flat/preamble.ts +38 -24
- package/src/render/shaders/generated/depth.wgsl.ts +95 -0
- package/src/render/shaders/generated/flat.wgsl.ts +3100 -2925
- package/src/render/shaders/outputTransform.ts +30 -14
- package/src/render/shaders/vertexChannel.ts +27 -0
- package/src/render/staticDraws.ts +95 -0
- package/src/render/surfaceTexture.ts +94 -27
|
@@ -99,6 +99,20 @@ export interface StageBindings {
|
|
|
99
99
|
readonly textures: Readonly<Record<string, TextureBinding>>;
|
|
100
100
|
/** Each pipeline-overridable constant's id, by the GLSL name it was declared under. */
|
|
101
101
|
readonly overrides?: Readonly<Record<string, number>>;
|
|
102
|
+
/**
|
|
103
|
+
* The second block, of the uniforms marked `// wgsl:material`: its binding, size and fields.
|
|
104
|
+
* Absent from a stage that marks none, which is every stage but the lit fragment's.
|
|
105
|
+
*/
|
|
106
|
+
readonly materialUniforms?: number;
|
|
107
|
+
readonly materialSize?: number;
|
|
108
|
+
readonly materialFields?: StageBindings['fields'];
|
|
109
|
+
/**
|
|
110
|
+
* The vertex stage's view block, of the uniforms marked `// wgsl:view`: the camera, the light's
|
|
111
|
+
* matrix and the wind, which a pass settles. Absent from every stage but the lit vertex one.
|
|
112
|
+
*/
|
|
113
|
+
readonly viewUniforms?: number;
|
|
114
|
+
readonly viewSize?: number;
|
|
115
|
+
readonly viewFields?: StageBindings['fields'];
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/*
|
|
@@ -130,9 +144,16 @@ const VERTEX_BINDINGS = FLAT_BINDINGS.flatVert as unknown as Readonly<
|
|
|
130
144
|
>;
|
|
131
145
|
|
|
132
146
|
/** The key for a set of vertex flags: the same rule `variantKey` uses, flags on, sorted, joined. */
|
|
133
|
-
export function flatVertexKey(
|
|
147
|
+
export function flatVertexKey(
|
|
148
|
+
skinned: boolean,
|
|
149
|
+
morphed: boolean,
|
|
150
|
+
instanced = false,
|
|
151
|
+
/** Whether an instanced variant plays a bone animation. See `shaders/boneAnimation.ts`. */
|
|
152
|
+
animated = false,
|
|
153
|
+
): string {
|
|
134
154
|
const on: string[] = [];
|
|
135
155
|
/* Alphabetical, because the generator's own `variantKey` sorts and the two keys must agree. */
|
|
156
|
+
if (animated) on.push('animated');
|
|
136
157
|
if (instanced) on.push('instanced');
|
|
137
158
|
if (morphed) on.push('morphed');
|
|
138
159
|
if (skinned) on.push('skinned');
|
|
@@ -144,8 +165,9 @@ export function flatVertexBindings(
|
|
|
144
165
|
skinned: boolean,
|
|
145
166
|
morphed = false,
|
|
146
167
|
instanced = false,
|
|
168
|
+
animated = false,
|
|
147
169
|
): StageBindings {
|
|
148
|
-
const key = flatVertexKey(skinned, morphed, instanced);
|
|
170
|
+
const key = flatVertexKey(skinned, morphed, instanced, animated);
|
|
149
171
|
const found = VERTEX_BINDINGS[key];
|
|
150
172
|
if (found === undefined) throw new Error(`flatPass: no vertex bindings for variant "${key}"`);
|
|
151
173
|
return found;
|
|
@@ -221,6 +243,8 @@ for (const [key, bindings] of Object.entries(VERTEX_BINDINGS)) {
|
|
|
221
243
|
}
|
|
222
244
|
|
|
223
245
|
export const FLAT_VERT_FIELDS = FLAT_BINDINGS.flatVert.none.fields;
|
|
246
|
+
/** The animated instanced variant's block: the instanced fields, and the clip's `uBoneClip`. */
|
|
247
|
+
export const FLAT_ANIMATED_VERT_FIELDS = FLAT_BINDINGS.flatVert['animated+instanced'].fields;
|
|
224
248
|
/**
|
|
225
249
|
* The **largest** variant's block, because one ring serves every draw.
|
|
226
250
|
*
|
|
@@ -233,6 +257,35 @@ export const FLAT_VERT_SIZE = Math.max(
|
|
|
233
257
|
);
|
|
234
258
|
const FLAT_VERT_BINDING = PLAIN_VERTEX.uniforms;
|
|
235
259
|
|
|
260
|
+
/*
|
|
261
|
+
* **The view block may be shorter in a variant, never different.** One slot serves every lit draw
|
|
262
|
+
* of a pass, plain and instanced alike, so every field a variant's view block declares must sit
|
|
263
|
+
* where the plain variant has it. The instanced variant declares no wind, and its block is the plain
|
|
264
|
+
* one's first two matrices.
|
|
265
|
+
*/
|
|
266
|
+
const PLAIN_VIEW_FIELDS = PLAIN_VERTEX.viewFields ?? {};
|
|
267
|
+
for (const [key, bindings] of Object.entries(VERTEX_BINDINGS)) {
|
|
268
|
+
if (bindings.viewUniforms !== PLAIN_VERTEX.viewUniforms) {
|
|
269
|
+
throw new Error(`flatPass: vertex variant "${key}" binds its view block elsewhere`);
|
|
270
|
+
}
|
|
271
|
+
for (const [field, here] of Object.entries(bindings.viewFields ?? {})) {
|
|
272
|
+
if (PLAIN_VIEW_FIELDS[field]?.offset !== here.offset) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`flatPass: vertex variant "${key}" puts view field ${field} at ${here.offset} where the ` +
|
|
275
|
+
`plain variant has it at ${PLAIN_VIEW_FIELDS[field]?.offset}.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The view block's fields, binding and size: the largest variant's, as `FLAT_VERT_SIZE` is. */
|
|
282
|
+
export const FLAT_VIEW_FIELDS = PLAIN_VIEW_FIELDS;
|
|
283
|
+
export const FLAT_VIEW_SIZE = Math.max(
|
|
284
|
+
...Object.values(VERTEX_BINDINGS).map((bindings) => bindings.viewSize ?? 0),
|
|
285
|
+
);
|
|
286
|
+
const FLAT_VIEW_BINDING = PLAIN_VERTEX.viewUniforms ?? -1;
|
|
287
|
+
if (FLAT_VIEW_BINDING < 0) throw new Error('flatPass: the lit vertex stage declares no view block');
|
|
288
|
+
|
|
236
289
|
/**
|
|
237
290
|
* The most sampled textures and distinct samplers any lit variant carrying the environment probe
|
|
238
291
|
* binds in its fragment stage, counted from the generated bindings: what a device must allow per
|
|
@@ -260,6 +313,46 @@ export function flatFragmentBindings(variant: FlatVariant): StageBindings {
|
|
|
260
313
|
return found;
|
|
261
314
|
}
|
|
262
315
|
|
|
316
|
+
/**
|
|
317
|
+
* The lit fragment stage's two blocks as the renderer keeps them on the CPU: the pass's, then the
|
|
318
|
+
* material's straight after it, in one scratch, with every field at its offset in that scratch.
|
|
319
|
+
*
|
|
320
|
+
* **One scratch rather than two, so no setter has to know which block its field is in.** A setter
|
|
321
|
+
* writes by name, as it always has; the draw copies the material's bytes — `passSize` onwards — into
|
|
322
|
+
* the store and the pass's bytes into a ring slot, each only when something has changed it.
|
|
323
|
+
*/
|
|
324
|
+
export interface FlatFragmentScratch {
|
|
325
|
+
readonly fields: StageBindings['fields'];
|
|
326
|
+
/** Bytes of the pass's block, which is where the material's starts in the scratch. */
|
|
327
|
+
readonly passSize: number;
|
|
328
|
+
readonly passBinding: number;
|
|
329
|
+
readonly materialSize: number;
|
|
330
|
+
readonly materialBinding: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function flatFragmentScratch(variant: FlatVariant): FlatFragmentScratch {
|
|
334
|
+
const bindings = flatFragmentBindings(variant);
|
|
335
|
+
const { materialUniforms, materialSize, materialFields } = bindings;
|
|
336
|
+
if (
|
|
337
|
+
materialUniforms === undefined ||
|
|
338
|
+
materialSize === undefined ||
|
|
339
|
+
materialFields === undefined
|
|
340
|
+
) {
|
|
341
|
+
throw new Error(`flatPass: variant "${variant}" has no material block`);
|
|
342
|
+
}
|
|
343
|
+
const fields: Record<string, StageBindings['fields'][string]> = { ...bindings.fields };
|
|
344
|
+
for (const [name, field] of Object.entries(materialFields)) {
|
|
345
|
+
fields[name] = { ...field, offset: bindings.uniformSize + field.offset };
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
fields,
|
|
349
|
+
passSize: bindings.uniformSize,
|
|
350
|
+
passBinding: bindings.uniforms,
|
|
351
|
+
materialSize,
|
|
352
|
+
materialBinding: materialUniforms,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
263
356
|
/**
|
|
264
357
|
* The lit fragment stage's constants, set from the cache's switches, keyed by each one's **id**.
|
|
265
358
|
*
|
|
@@ -306,6 +399,14 @@ function flatFragmentConstants(
|
|
|
306
399
|
return constants;
|
|
307
400
|
}
|
|
308
401
|
|
|
402
|
+
/** The material block's binding, which every lit variant declares. */
|
|
403
|
+
function materialBinding(fragment: StageBindings): number {
|
|
404
|
+
if (fragment.materialUniforms === undefined) {
|
|
405
|
+
throw new Error('flatPass: the lit fragment stage declares no material block');
|
|
406
|
+
}
|
|
407
|
+
return fragment.materialUniforms;
|
|
408
|
+
}
|
|
409
|
+
|
|
309
410
|
/** The depth format both backends compare with. */
|
|
310
411
|
/* Re-exported so the twenty passes that already import it from here do not each learn a new
|
|
311
412
|
module; `depthConvention.ts` is where the decision lives and why. */
|
|
@@ -317,15 +418,19 @@ const VISIBILITY_FRAGMENT = 0x2;
|
|
|
317
418
|
/**
|
|
318
419
|
* The layout this variant binds against.
|
|
319
420
|
*
|
|
320
|
-
*
|
|
321
|
-
* from its own slot of one buffer
|
|
322
|
-
*
|
|
421
|
+
* All four blocks are **dynamic**: the vertex block because every draw supplies its own model
|
|
422
|
+
* matrix and tint from its own slot of one buffer; the fragment stage's pass block — the lights, the
|
|
423
|
+
* fog, the camera — and the vertex stage's view block because a frame binds more than one pass; and
|
|
424
|
+
* the material block because a material changes between draws. Their dynamic offsets go in binding
|
|
425
|
+
* order: the draw's, the pass's, the material's, the view's.
|
|
323
426
|
*/
|
|
324
427
|
export function createFlatBindGroupLayout(
|
|
325
428
|
device: GPUDevice,
|
|
326
429
|
variant: FlatVariant,
|
|
327
430
|
skinned = false,
|
|
328
431
|
morphed = false,
|
|
432
|
+
/** The instanced variant that plays a bone animation: its three vertex textures. */
|
|
433
|
+
animated = false,
|
|
329
434
|
): GPUBindGroupLayout {
|
|
330
435
|
const fragment = flatFragmentBindings(variant);
|
|
331
436
|
const entries: GPUBindGroupLayoutEntry[] = [
|
|
@@ -334,15 +439,31 @@ export function createFlatBindGroupLayout(
|
|
|
334
439
|
visibility: VISIBILITY_VERTEX,
|
|
335
440
|
buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: FLAT_VERT_SIZE },
|
|
336
441
|
},
|
|
442
|
+
{
|
|
443
|
+
binding: FLAT_VIEW_BINDING,
|
|
444
|
+
visibility: VISIBILITY_VERTEX,
|
|
445
|
+
/* Dynamic, a slot a pass, for the reason the fragment stage's pass block is. */
|
|
446
|
+
buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: FLAT_VIEW_SIZE },
|
|
447
|
+
},
|
|
337
448
|
{
|
|
338
449
|
binding: fragment.uniforms,
|
|
339
450
|
visibility: VISIBILITY_FRAGMENT,
|
|
340
451
|
/*
|
|
341
|
-
* Dynamic, because
|
|
342
|
-
*
|
|
452
|
+
* Dynamic, because a pass's lights and camera change between passes of one frame — a
|
|
453
|
+
* mirror's, a capture's — and one buffer cannot hold two answers. See `WebGPURenderer.passRing`.
|
|
343
454
|
*/
|
|
344
455
|
buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: fragment.uniformSize },
|
|
345
456
|
},
|
|
457
|
+
{
|
|
458
|
+
binding: materialBinding(fragment),
|
|
459
|
+
visibility: VISIBILITY_FRAGMENT,
|
|
460
|
+
/* Dynamic, because a material changes between draws. See `MaterialStore`. */
|
|
461
|
+
buffer: {
|
|
462
|
+
type: 'uniform',
|
|
463
|
+
hasDynamicOffset: true,
|
|
464
|
+
minBindingSize: fragment.materialSize ?? 0,
|
|
465
|
+
},
|
|
466
|
+
},
|
|
346
467
|
];
|
|
347
468
|
|
|
348
469
|
/** What each sampler binding was declared as, so a shared one is declared once and agrees. */
|
|
@@ -489,12 +610,43 @@ export function createFlatBindGroupLayout(
|
|
|
489
610
|
}
|
|
490
611
|
}
|
|
491
612
|
|
|
613
|
+
/* A bone animation's places, turns and the batch's clocks, read by index as the palette is. */
|
|
614
|
+
if (animated) {
|
|
615
|
+
for (const binding of animationBindings()) {
|
|
616
|
+
entries.push({
|
|
617
|
+
binding,
|
|
618
|
+
visibility: VISIBILITY_VERTEX,
|
|
619
|
+
texture: { sampleType: 'unfilterable-float' },
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
492
624
|
return device.createBindGroupLayout({
|
|
493
|
-
label: `flat.layout:${variant}|${flatVertexKey(skinned, morphed)}`,
|
|
625
|
+
label: `flat.layout:${variant}|${flatVertexKey(skinned, morphed, animated, animated)}`,
|
|
494
626
|
entries,
|
|
495
627
|
});
|
|
496
628
|
}
|
|
497
629
|
|
|
630
|
+
/** The three textures a bone animation binds, in the order `AnimationViews` holds them. */
|
|
631
|
+
function animationBindings(): [number, number, number] {
|
|
632
|
+
const textures = flatVertexBindings(false, false, true, true).textures;
|
|
633
|
+
const at = (name: string): number => {
|
|
634
|
+
const found = textures[name];
|
|
635
|
+
if (found === undefined) {
|
|
636
|
+
throw new Error(`flatPass: the animated vertex shader declares no ${name}`);
|
|
637
|
+
}
|
|
638
|
+
return found.texture;
|
|
639
|
+
};
|
|
640
|
+
return [at('uBonePlaces'), at('uBoneTurns'), at('uInstanceClocks')];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** What an animated draw binds besides its material: the clip's two textures and its batch's clocks. */
|
|
644
|
+
export interface AnimationViews {
|
|
645
|
+
readonly places: GPUTextureView;
|
|
646
|
+
readonly turns: GPUTextureView;
|
|
647
|
+
readonly clocks: GPUTextureView;
|
|
648
|
+
}
|
|
649
|
+
|
|
498
650
|
/**
|
|
499
651
|
* Bind the buffers and one view per declared texture.
|
|
500
652
|
*
|
|
@@ -509,7 +661,9 @@ export function createFlatBindGroup(
|
|
|
509
661
|
layout: GPUBindGroupLayout,
|
|
510
662
|
variant: FlatVariant,
|
|
511
663
|
perDraw: GPUBuffer,
|
|
512
|
-
|
|
664
|
+
passes: GPUBuffer,
|
|
665
|
+
materials: GPUBuffer,
|
|
666
|
+
views: GPUBuffer,
|
|
513
667
|
resolve: (name: string) => { view: GPUTextureView; sampler: GPUSampler },
|
|
514
668
|
/** The palette view for a skinned bind group, or null for an unskinned one. */
|
|
515
669
|
palette: GPUTextureView | null = null,
|
|
@@ -517,6 +671,8 @@ export function createFlatBindGroup(
|
|
|
517
671
|
deltas: GPUTextureView | null = null,
|
|
518
672
|
/** A skinned bind group's cloth views — the draw's, or the stand-in where it sets none. */
|
|
519
673
|
cloth: ClothViews | null = null,
|
|
674
|
+
/** An animated instanced group's views. See `AnimationViews`. */
|
|
675
|
+
animation: AnimationViews | null = null,
|
|
520
676
|
): GPUBindGroup {
|
|
521
677
|
const fragment = flatFragmentBindings(variant);
|
|
522
678
|
const entries: GPUBindGroupEntry[] = [
|
|
@@ -524,7 +680,12 @@ export function createFlatBindGroup(
|
|
|
524
680
|
binding: FLAT_VERT_BINDING,
|
|
525
681
|
resource: { buffer: perDraw, size: FLAT_VERT_SIZE },
|
|
526
682
|
},
|
|
527
|
-
{ binding:
|
|
683
|
+
{ binding: FLAT_VIEW_BINDING, resource: { buffer: views, size: FLAT_VIEW_SIZE } },
|
|
684
|
+
{ binding: fragment.uniforms, resource: { buffer: passes, size: fragment.uniformSize } },
|
|
685
|
+
{
|
|
686
|
+
binding: materialBinding(fragment),
|
|
687
|
+
resource: { buffer: materials, size: fragment.materialSize ?? 0 },
|
|
688
|
+
},
|
|
528
689
|
];
|
|
529
690
|
|
|
530
691
|
/* The same dedupe the layout does, and for the same reason: the shadow bindings share one
|
|
@@ -563,8 +724,15 @@ export function createFlatBindGroup(
|
|
|
563
724
|
entries.push({ binding: restAt, resource: cloth.rest });
|
|
564
725
|
}
|
|
565
726
|
|
|
727
|
+
if (animation !== null) {
|
|
728
|
+
const [places, turns, clocks] = animationBindings();
|
|
729
|
+
entries.push({ binding: places, resource: animation.places });
|
|
730
|
+
entries.push({ binding: turns, resource: animation.turns });
|
|
731
|
+
entries.push({ binding: clocks, resource: animation.clocks });
|
|
732
|
+
}
|
|
733
|
+
|
|
566
734
|
return device.createBindGroup({
|
|
567
|
-
label: `flat.bindGroup:${variant}|${flatVertexKey(palette !== null, deltas !== null)}`,
|
|
735
|
+
label: `flat.bindGroup:${variant}|${flatVertexKey(palette !== null, deltas !== null, animation !== null, animation !== null)}`,
|
|
568
736
|
layout,
|
|
569
737
|
entries,
|
|
570
738
|
});
|
|
@@ -641,6 +809,8 @@ export function flatPipeline(
|
|
|
641
809
|
* targets `reflectionSurfacePass.ts` holds. The key must carry it: `|rs`.
|
|
642
810
|
*/
|
|
643
811
|
reflectionSurface = false,
|
|
812
|
+
/** An instanced pipeline that plays a bone animation. The key must carry it: `|anim`. */
|
|
813
|
+
animated = false,
|
|
644
814
|
): GPURenderPipeline {
|
|
645
815
|
return cache.get(
|
|
646
816
|
key,
|
|
@@ -666,6 +836,7 @@ export function flatPipeline(
|
|
|
666
836
|
model,
|
|
667
837
|
skinPass,
|
|
668
838
|
reflectionSurface,
|
|
839
|
+
animated,
|
|
669
840
|
),
|
|
670
841
|
true,
|
|
671
842
|
);
|
|
@@ -799,6 +970,8 @@ function flatDescriptor(
|
|
|
799
970
|
model: SurfaceModelKind | null = null,
|
|
800
971
|
skinPass: SkinHalf = 'whole',
|
|
801
972
|
reflectionSurface = false,
|
|
973
|
+
/** An instanced pipeline that plays a bone animation. See `shaders/boneAnimation.ts`. */
|
|
974
|
+
animated = false,
|
|
802
975
|
): GPURenderPipelineDescriptor {
|
|
803
976
|
const offset = depthOffsetForLayer(depthLayer);
|
|
804
977
|
/* The diffuse and colour halves are the same surface again, into the blur's targets; and so is
|
|
@@ -814,13 +987,13 @@ function flatDescriptor(
|
|
|
814
987
|
* layout validates, draws, and is wrong, exactly as this file's header says.
|
|
815
988
|
*/
|
|
816
989
|
module: shaderModule(device, {
|
|
817
|
-
label: `flat.vert:${flatVertexKey(skinned, morphed, instanced)}`,
|
|
818
|
-
code: FLAT_VERT_WGSL[flatVertexKey(skinned, morphed, instanced)] ?? '',
|
|
990
|
+
label: `flat.vert:${flatVertexKey(skinned, morphed, instanced, animated)}`,
|
|
991
|
+
code: FLAT_VERT_WGSL[flatVertexKey(skinned, morphed, instanced, animated)] ?? '',
|
|
819
992
|
}),
|
|
820
993
|
entryPoint: 'main',
|
|
821
994
|
buffers: vertexBufferLayouts(present, instanced),
|
|
822
995
|
constants: vertexConstants(
|
|
823
|
-
flatVertexBindings(skinned, morphed, instanced),
|
|
996
|
+
flatVertexBindings(skinned, morphed, instanced, animated),
|
|
824
997
|
skinEight,
|
|
825
998
|
cloth,
|
|
826
999
|
model === 'lightmap',
|
|
@@ -4,6 +4,8 @@ import { destroySlot } from './instanceCullPass.ts';
|
|
|
4
4
|
import type { CullSlot } from './instanceCullPass.ts';
|
|
5
5
|
import type { MeshInstances } from '../../instances.ts';
|
|
6
6
|
import type { GpuMesh } from './buffers.ts';
|
|
7
|
+
import { packInstanceClocks } from '../../boneAnimation.ts';
|
|
8
|
+
import { GpuInstanceClocks, type GpuBoneAnimation } from './boneAnimations.ts';
|
|
7
9
|
|
|
8
10
|
/** VERTEX | COPY_DST, spelled the way every other pass here spells it. */
|
|
9
11
|
const USAGE_VERTEX = 0x0020 | 0x0008;
|
|
@@ -36,10 +38,34 @@ export class GpuInstancedBatch {
|
|
|
36
38
|
slotFrame = -1;
|
|
37
39
|
slotsTaken = 0;
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
/** The bone animation every instance plays, and its clocks; null for a batch that plays none. */
|
|
42
|
+
readonly animation: GpuBoneAnimation | null;
|
|
43
|
+
readonly clocks: GpuInstanceClocks | null;
|
|
44
|
+
/**
|
|
45
|
+
* An animated batch's own groups — its lit twins by the material group they were built beside, its
|
|
46
|
+
* shadow groups by what they cut with — and the renderer's group epoch they were built in, after
|
|
47
|
+
* which every one is dropped. See `WebGPURenderer.animatedGroup`.
|
|
48
|
+
*/
|
|
49
|
+
readonly litGroups = new Map<GPUBindGroup, GPUBindGroup>();
|
|
50
|
+
readonly depthGroups = new Map<unknown, GPUBindGroup>();
|
|
51
|
+
readonly depthPeelGroups = new Map<unknown, GPUBindGroup>();
|
|
52
|
+
groupEpoch = -1;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
device: GPUDevice,
|
|
56
|
+
mesh: GpuMesh,
|
|
57
|
+
capacity: number,
|
|
58
|
+
label: string,
|
|
59
|
+
cull = false,
|
|
60
|
+
animation: GpuBoneAnimation | null = null,
|
|
61
|
+
) {
|
|
40
62
|
this.mesh = mesh;
|
|
41
63
|
this.capacity = capacity;
|
|
64
|
+
/* An animated batch is culled whole and never by instance: see `submitInstanced`. */
|
|
42
65
|
this.cull = cull;
|
|
66
|
+
this.animation = animation;
|
|
67
|
+
this.clocks =
|
|
68
|
+
animation === null ? null : new GpuInstanceClocks(device, capacity, `${label}.clocks`);
|
|
43
69
|
this.staging = new Float32Array(capacity * INSTANCE_FLOATS);
|
|
44
70
|
this.buffer = device.createBuffer({
|
|
45
71
|
/*
|
|
@@ -83,11 +109,16 @@ export class GpuInstancedBatch {
|
|
|
83
109
|
if (count === 0) return;
|
|
84
110
|
packInstances(data, this.staging);
|
|
85
111
|
queue.writeBuffer(this.buffer, 0, this.staging, 0, count * INSTANCE_FLOATS);
|
|
112
|
+
if (this.clocks !== null) {
|
|
113
|
+
packInstanceClocks(data, this.clocks.staging);
|
|
114
|
+
this.clocks.upload(queue, count);
|
|
115
|
+
}
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
dispose(): void {
|
|
89
119
|
this.previousBuffer?.destroy();
|
|
90
120
|
this.buffer.destroy();
|
|
121
|
+
this.clocks?.dispose();
|
|
91
122
|
for (const slot of this.slots) destroySlot(slot);
|
|
92
123
|
}
|
|
93
124
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { BudgetCounter } from '../budget.ts';
|
|
2
|
+
import { DYNAMIC_ALIGNMENT, MAX_RING_BYTES } from './uniformRing.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The lit stage's material blocks, each kept in its slot for as long as later draws ask for the
|
|
6
|
+
* same numbers, so a material that has not changed since the last frame costs no upload at all.
|
|
7
|
+
*
|
|
8
|
+
* **Keyed by content, because nothing else names a material.** A material here is whatever the
|
|
9
|
+
* setters left in the block when a draw took it — a consumer never hands over an object that could
|
|
10
|
+
* carry an identity — so the block's own words are the key: hashed to find a bucket, compared word
|
|
11
|
+
* for word to accept a match, and bound by the slot's offset. What it gives up is a hash and a
|
|
12
|
+
* compare of a few hundred bytes per material change, against copying and uploading the whole
|
|
13
|
+
* block every frame, which a stage of four hundred materials measured at megabytes a frame.
|
|
14
|
+
*
|
|
15
|
+
* **A slot the frame has asked for is never given away within it**, for the reason `UniformRing`
|
|
16
|
+
* exists: a queue write does not interleave with recorded draws, so rewriting a slot a recorded draw
|
|
17
|
+
* reads would hand that draw the new numbers. A frame asking for more distinct blocks than there are
|
|
18
|
+
* slots is refused, counted and skipped, as the ring was. Across frames rewriting is safe, because a
|
|
19
|
+
* write issued after a submit is ordered after the commands in it.
|
|
20
|
+
*
|
|
21
|
+
* **What a new block displaces is a slot nobody asked for this frame or the last**, found by a hand
|
|
22
|
+
* that sweeps the slots in turn; only when every slot was asked for recently does it take one the
|
|
23
|
+
* last frame used. What would make that wrong is a scene whose distinct materials a frame approach
|
|
24
|
+
* the store's size: it then rewrites some of its steady materials every frame, which costs their
|
|
25
|
+
* upload and nothing else.
|
|
26
|
+
*/
|
|
27
|
+
export class MaterialStore {
|
|
28
|
+
/** Replaced by `growTo`, so every bind group over it is rebuilt when that says so. */
|
|
29
|
+
buffer: GPUBuffer;
|
|
30
|
+
/** Bytes per slot, rounded up to the alignment a dynamic offset requires. */
|
|
31
|
+
readonly slotSize: number;
|
|
32
|
+
|
|
33
|
+
private readonly words: number;
|
|
34
|
+
private readonly stride: number;
|
|
35
|
+
private capacity: number;
|
|
36
|
+
private staging: ArrayBuffer;
|
|
37
|
+
private ints: Int32Array;
|
|
38
|
+
/** Per slot: its block's hash, the next slot in its bucket or -1, and whether it holds one. */
|
|
39
|
+
private hashes: Int32Array;
|
|
40
|
+
private next: Int32Array;
|
|
41
|
+
private held: Uint8Array;
|
|
42
|
+
/** Per slot: the frame that last asked for it, and whether it was written since the last flush. */
|
|
43
|
+
private askedIn: Int32Array;
|
|
44
|
+
private pending: Uint8Array;
|
|
45
|
+
/** Per slot: how many holders pinned it. A pinned slot is never displaced. */
|
|
46
|
+
private pins: Int32Array;
|
|
47
|
+
/**
|
|
48
|
+
* How many times the store has forgotten everything it held, which growth does: a holder of an
|
|
49
|
+
* offset compares this, because an offset from before is a slot of a buffer that is gone.
|
|
50
|
+
*/
|
|
51
|
+
epoch = 0;
|
|
52
|
+
/** Per bucket: its first slot, or -1. A power of two at least twice the slots. */
|
|
53
|
+
private heads: Int32Array;
|
|
54
|
+
private mask: number;
|
|
55
|
+
private lowPending: number;
|
|
56
|
+
private highPending = -1;
|
|
57
|
+
private hand = 0;
|
|
58
|
+
/** Starts past zero, so a slot never asked for reads as long unasked. */
|
|
59
|
+
private frame = 2;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly device: GPUDevice,
|
|
63
|
+
bytesPerBlock: number,
|
|
64
|
+
slots: number,
|
|
65
|
+
private readonly usage: number,
|
|
66
|
+
private readonly label = 'materials.store',
|
|
67
|
+
private readonly budget: BudgetCounter | null = null,
|
|
68
|
+
) {
|
|
69
|
+
this.words = Math.ceil(bytesPerBlock / 4);
|
|
70
|
+
this.slotSize = Math.ceil(bytesPerBlock / DYNAMIC_ALIGNMENT) * DYNAMIC_ALIGNMENT;
|
|
71
|
+
this.stride = this.slotSize / 4;
|
|
72
|
+
this.capacity = slots;
|
|
73
|
+
this.lowPending = slots;
|
|
74
|
+
this.staging = new ArrayBuffer(this.slotSize * slots);
|
|
75
|
+
this.ints = new Int32Array(this.staging);
|
|
76
|
+
this.hashes = new Int32Array(slots);
|
|
77
|
+
this.next = new Int32Array(slots);
|
|
78
|
+
this.held = new Uint8Array(slots);
|
|
79
|
+
this.askedIn = new Int32Array(slots);
|
|
80
|
+
this.pending = new Uint8Array(slots);
|
|
81
|
+
this.pins = new Int32Array(slots);
|
|
82
|
+
this.heads = new Int32Array(bucketsFor(slots)).fill(-1);
|
|
83
|
+
this.mask = this.heads.length - 1;
|
|
84
|
+
this.buffer = device.createBuffer({ label, size: this.staging.byteLength, usage });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** How many distinct blocks a frame may hold. */
|
|
88
|
+
get slots(): number {
|
|
89
|
+
return this.capacity;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Start a frame: every slot the last one asked for may be displaced from here on, last. */
|
|
93
|
+
beginFrame(): void {
|
|
94
|
+
this.frame += 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The byte offset of a slot holding exactly `block`, written there if no slot did, or null when
|
|
99
|
+
* every slot is one this frame has asked for. `block` is the whole block, word for word.
|
|
100
|
+
*/
|
|
101
|
+
place(block: Int32Array): number | null {
|
|
102
|
+
this.budget?.ask();
|
|
103
|
+
const hash = hashWords(block, this.words);
|
|
104
|
+
const bucket = hash & this.mask;
|
|
105
|
+
for (let slot = this.heads[bucket] as number; slot >= 0; slot = this.next[slot] as number) {
|
|
106
|
+
if (this.hashes[slot] === hash && this.holds(slot, block)) {
|
|
107
|
+
this.askedIn[slot] = this.frame;
|
|
108
|
+
return slot * this.slotSize;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const slot = this.displace();
|
|
112
|
+
if (slot < 0) {
|
|
113
|
+
this.budget?.drop();
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (this.held[slot] === 1) this.unlink(slot);
|
|
117
|
+
this.ints.set(block, slot * this.stride);
|
|
118
|
+
this.hashes[slot] = hash;
|
|
119
|
+
this.next[slot] = this.heads[bucket] as number;
|
|
120
|
+
this.heads[bucket] = slot;
|
|
121
|
+
this.held[slot] = 1;
|
|
122
|
+
this.askedIn[slot] = this.frame;
|
|
123
|
+
this.pending[slot] = 1;
|
|
124
|
+
if (slot < this.lowPending) this.lowPending = slot;
|
|
125
|
+
if (slot > this.highPending) this.highPending = slot;
|
|
126
|
+
return slot * this.slotSize;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Upload every slot written since the last flush, one write per run of neighbouring slots. */
|
|
130
|
+
flush(): void {
|
|
131
|
+
let slot = this.lowPending;
|
|
132
|
+
while (slot <= this.highPending) {
|
|
133
|
+
if (this.pending[slot] !== 1) {
|
|
134
|
+
slot += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const from = slot;
|
|
138
|
+
while (slot <= this.highPending && this.pending[slot] === 1) {
|
|
139
|
+
this.pending[slot] = 0;
|
|
140
|
+
slot += 1;
|
|
141
|
+
}
|
|
142
|
+
const at = from * this.slotSize;
|
|
143
|
+
this.device.queue.writeBuffer(
|
|
144
|
+
this.buffer,
|
|
145
|
+
at,
|
|
146
|
+
this.staging,
|
|
147
|
+
at,
|
|
148
|
+
(slot - from) * this.slotSize,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
this.lowPending = this.capacity;
|
|
152
|
+
this.highPending = -1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Make room for at least `slots`, forgetting every block held, and say whether the buffer was
|
|
157
|
+
* replaced. Called where `UniformRing.growTo` is, at a frame's start, for the reasons it gives,
|
|
158
|
+
* and refused past the same ceiling.
|
|
159
|
+
*/
|
|
160
|
+
growTo(slots: number): boolean {
|
|
161
|
+
if (slots <= this.capacity) return false;
|
|
162
|
+
if (this.slotSize * slots > MAX_RING_BYTES) return false;
|
|
163
|
+
this.buffer.destroy();
|
|
164
|
+
this.capacity = slots;
|
|
165
|
+
this.staging = new ArrayBuffer(this.slotSize * slots);
|
|
166
|
+
this.ints = new Int32Array(this.staging);
|
|
167
|
+
this.hashes = new Int32Array(slots);
|
|
168
|
+
this.next = new Int32Array(slots);
|
|
169
|
+
this.held = new Uint8Array(slots);
|
|
170
|
+
this.askedIn = new Int32Array(slots);
|
|
171
|
+
this.pending = new Uint8Array(slots);
|
|
172
|
+
this.pins = new Int32Array(slots);
|
|
173
|
+
this.heads = new Int32Array(bucketsFor(slots)).fill(-1);
|
|
174
|
+
this.mask = this.heads.length - 1;
|
|
175
|
+
this.lowPending = slots;
|
|
176
|
+
this.epoch += 1;
|
|
177
|
+
this.highPending = -1;
|
|
178
|
+
this.hand = 0;
|
|
179
|
+
this.buffer = this.device.createBuffer({
|
|
180
|
+
label: this.label,
|
|
181
|
+
size: this.staging.byteLength,
|
|
182
|
+
usage: this.usage,
|
|
183
|
+
});
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Keep the block at `offset` where it is until `unpin`: a holder binding it by offset frame after
|
|
189
|
+
* frame — a list recorded once — whether or not anything asks for it. Counted, so two holders of
|
|
190
|
+
* one block each let go of their own pin.
|
|
191
|
+
*/
|
|
192
|
+
pin(offset: number): void {
|
|
193
|
+
const slot = offset / this.slotSize;
|
|
194
|
+
this.pins[slot] = (this.pins[slot] as number) + 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
unpin(offset: number): void {
|
|
198
|
+
const slot = offset / this.slotSize;
|
|
199
|
+
if ((this.pins[slot] as number) > 0) this.pins[slot] = (this.pins[slot] as number) - 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
dispose(): void {
|
|
203
|
+
this.buffer.destroy();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private holds(slot: number, block: Int32Array): boolean {
|
|
207
|
+
const base = slot * this.stride;
|
|
208
|
+
for (let word = 0; word < this.words; word++) {
|
|
209
|
+
if (this.ints[base + word] !== block[word]) return false;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The slot a new block goes in: the first from the hand nobody asked for this frame or the last,
|
|
216
|
+
* else the first this frame has not asked for, else none. One sweep at most.
|
|
217
|
+
*/
|
|
218
|
+
private displace(): number {
|
|
219
|
+
let fallback = -1;
|
|
220
|
+
for (let step = 0; step < this.capacity; step++) {
|
|
221
|
+
const slot = this.hand;
|
|
222
|
+
this.hand = slot + 1 === this.capacity ? 0 : slot + 1;
|
|
223
|
+
if ((this.pins[slot] as number) > 0) continue;
|
|
224
|
+
const asked = this.askedIn[slot] as number;
|
|
225
|
+
if (asked < this.frame - 1) return slot;
|
|
226
|
+
if (fallback < 0 && asked !== this.frame) fallback = slot;
|
|
227
|
+
}
|
|
228
|
+
return fallback;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private unlink(slot: number): void {
|
|
232
|
+
const bucket = (this.hashes[slot] as number) & this.mask;
|
|
233
|
+
let at = this.heads[bucket] as number;
|
|
234
|
+
if (at === slot) {
|
|
235
|
+
this.heads[bucket] = this.next[slot] as number;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
while (at >= 0) {
|
|
239
|
+
const after = this.next[at] as number;
|
|
240
|
+
if (after === slot) {
|
|
241
|
+
this.next[at] = this.next[slot] as number;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
at = after;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function bucketsFor(slots: number): number {
|
|
250
|
+
let buckets = 16;
|
|
251
|
+
while (buckets < slots * 2) buckets *= 2;
|
|
252
|
+
return buckets;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* MurmurHash3's 32-bit mix over the block's words. Not FNV, whose multiply carries a difference only
|
|
257
|
+
* upward: the bucket is the hash's low bits, and a material's words are mostly floats, whose low
|
|
258
|
+
* mantissa bits are zero for every value a person types — 0.5, 1, 2 — so blocks differing only in
|
|
259
|
+
* those would all have shared one bucket. The compare that follows a match is what decides.
|
|
260
|
+
*/
|
|
261
|
+
export function hashWords(block: Int32Array, words: number): number {
|
|
262
|
+
let hash = 0x9747b28c;
|
|
263
|
+
for (let word = 0; word < words; word++) {
|
|
264
|
+
let k = Math.imul(block[word] as number, 0xcc9e2d51);
|
|
265
|
+
k = (k << 15) | (k >>> 17);
|
|
266
|
+
hash ^= Math.imul(k, 0x1b873593);
|
|
267
|
+
hash = (hash << 13) | (hash >>> 19);
|
|
268
|
+
hash = (Math.imul(hash, 5) + 0xe6546b64) | 0;
|
|
269
|
+
}
|
|
270
|
+
hash ^= words * 4;
|
|
271
|
+
hash ^= hash >>> 16;
|
|
272
|
+
hash = Math.imul(hash, 0x85ebca6b);
|
|
273
|
+
hash ^= hash >>> 13;
|
|
274
|
+
hash = Math.imul(hash, 0xc2b2ae35);
|
|
275
|
+
return hash ^ (hash >>> 16);
|
|
276
|
+
}
|