@tinypoly/gpu-renderer 0.2.4 → 0.3.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/ARCHITECTURE.md +48 -10
- package/README.md +2 -0
- package/dist/accumulator.d.ts.map +1 -1
- package/dist/accumulator.js +1 -0
- package/dist/accumulator.js.map +1 -1
- package/dist/denoise.js +2 -2
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +9 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +5 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts +48 -8
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +409 -134
- package/dist/geometry.js.map +1 -1
- package/dist/grass.d.ts.map +1 -1
- package/dist/grass.js +2 -1
- package/dist/grass.js.map +1 -1
- package/dist/imageResolve.js +2 -2
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instancing.d.ts +100 -0
- package/dist/instancing.d.ts.map +1 -0
- package/dist/instancing.js +286 -0
- package/dist/instancing.js.map +1 -0
- package/dist/lensPass.d.ts +2 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +7 -4
- package/dist/lensPass.js.map +1 -1
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +8 -5
- package/dist/lights.js.map +1 -1
- package/dist/memoryProbe.d.ts +76 -0
- package/dist/memoryProbe.d.ts.map +1 -0
- package/dist/memoryProbe.js +197 -0
- package/dist/memoryProbe.js.map +1 -0
- package/dist/oidnPass.d.ts +6 -0
- package/dist/oidnPass.d.ts.map +1 -1
- package/dist/oidnPass.js +18 -1
- package/dist/oidnPass.js.map +1 -1
- package/dist/pathTracePass.d.ts +15 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +38 -16
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +248 -41
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +26 -8
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +97 -27
- package/dist/sceneUpload.js.map +1 -1
- package/dist/shaderModule.d.ts.map +1 -1
- package/dist/shaderModule.js +2 -1
- package/dist/shaderModule.js.map +1 -1
- package/dist/skyBake.d.ts.map +1 -1
- package/dist/skyBake.js +5 -3
- package/dist/skyBake.js.map +1 -1
- package/dist/texturePack.d.ts +43 -3
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +209 -37
- package/dist/texturePack.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +5 -1
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +105 -15
- package/dist/wgsl/bvh.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +92 -11
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +1 -1
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lens.wgsl.js +14 -2
- package/dist/wgsl/lens.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +24 -14
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/dist/wgsl/texture.wgsl.d.ts +1 -1
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
- package/dist/wgsl/texture.wgsl.js +11 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/accumulator.ts +1 -0
- package/src/denoise.ts +2 -2
- package/src/device.ts +12 -0
- package/src/frameLayout.ts +5 -0
- package/src/geometry.ts +527 -133
- package/src/grass.ts +2 -1
- package/src/imageResolve.ts +2 -2
- package/src/index.ts +1 -0
- package/src/instancing.ts +415 -0
- package/src/lensPass.ts +9 -4
- package/src/lights.ts +9 -5
- package/src/memoryProbe.ts +275 -0
- package/src/oidnPass.ts +21 -1
- package/src/pathTracePass.ts +42 -18
- package/src/renderer.ts +308 -47
- package/src/sceneUpload.ts +139 -36
- package/src/shaderModule.ts +2 -1
- package/src/skyBake.ts +5 -3
- package/src/texturePack.ts +274 -43
- package/src/wgsl/bvh.wgsl.ts +105 -15
- package/src/wgsl/common.wgsl.ts +92 -11
- package/src/wgsl/lens.wgsl.ts +14 -2
- package/src/wgsl/surface.wgsl.ts +24 -14
- package/src/wgsl/texture.wgsl.ts +11 -0
package/dist/renderer.js
CHANGED
|
@@ -3,12 +3,14 @@ import { Accumulator } from "./accumulator.js";
|
|
|
3
3
|
import { BvhBuilder, createDefaultBvhWorker } from "./bvhBuilder.js";
|
|
4
4
|
import { cameraMatrices } from "./camera.js";
|
|
5
5
|
import { requestRenderDevice } from "./device.js";
|
|
6
|
+
import { logMemory, memoryLogging, retainedBytes, sceneBreakdown, textureUsage, toMB } from "./memoryProbe.js";
|
|
6
7
|
import { loadEnvironment } from "./environment.js";
|
|
7
8
|
import { DispatchScheduler } from "./dispatchScheduler.js";
|
|
8
|
-
import {
|
|
9
|
+
import { packGeometrySliced } from "./geometry.js";
|
|
10
|
+
import { instanceWords, mergeHierarchies, transformBox, trianglesBox } from "./instancing.js";
|
|
9
11
|
import { packLights } from "./lights.js";
|
|
10
12
|
import { NO_PROGRAM } from "./materialLayout.js";
|
|
11
|
-
import { NO_TEXTURE, planTextures,
|
|
13
|
+
import { NO_TEXTURE, planTextures, singleChannelTextures, textureChannels, textureUpload } from "./texturePack.js";
|
|
12
14
|
import { PathTracePass } from "./pathTracePass.js";
|
|
13
15
|
import { ImageResolvePass } from "./imageResolve.js";
|
|
14
16
|
import { PresentPass } from "./presentPass.js";
|
|
@@ -45,6 +47,18 @@ function linearColor(color, fallback) {
|
|
|
45
47
|
}
|
|
46
48
|
/** Hands the thread back to the page for a moment, so input and painting go on between submits. */
|
|
47
49
|
const yieldToPage = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
50
|
+
/**
|
|
51
|
+
* The same, through a message rather than a timer: a page in the background has its timers spaced out, up to a second
|
|
52
|
+
* each, which would stretch packing a large scene by minutes, while messages still arrive at once.
|
|
53
|
+
*/
|
|
54
|
+
const yieldForWork = typeof MessageChannel === "undefined" ? yieldToPage : () => new Promise(resolve => {
|
|
55
|
+
const channel = new MessageChannel();
|
|
56
|
+
channel.port1.onmessage = () => {
|
|
57
|
+
channel.port1.close();
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
channel.port2.postMessage(null);
|
|
61
|
+
});
|
|
48
62
|
/**
|
|
49
63
|
* How often the canvas is drawn and the page gets the thread back while a frame traces: about once a display frame.
|
|
50
64
|
* Waiting for an animation frame after every band would leave the device idle for most of each one.
|
|
@@ -112,6 +126,37 @@ function rowRanges(rows) {
|
|
|
112
126
|
}
|
|
113
127
|
/** How many paths per pixel a planar capture gets. It is a texture a shader distorts, not the image itself. */
|
|
114
128
|
const CAPTURE_SAMPLES = 32;
|
|
129
|
+
/** Words appended after the texels, kept as typed runs: a plain array of numbers costs twice as much while it grows. */
|
|
130
|
+
class ExtraWords {
|
|
131
|
+
base;
|
|
132
|
+
parts = [];
|
|
133
|
+
length = 0;
|
|
134
|
+
constructor(base) {
|
|
135
|
+
this.base = base;
|
|
136
|
+
}
|
|
137
|
+
/** Where the next words will land, absolutely. */
|
|
138
|
+
get next() {
|
|
139
|
+
return this.base + this.length;
|
|
140
|
+
}
|
|
141
|
+
/** Appends words and says where they landed. */
|
|
142
|
+
append(words) {
|
|
143
|
+
const at = this.next;
|
|
144
|
+
if (words.length) {
|
|
145
|
+
this.parts.push(words instanceof Uint32Array ? words : Uint32Array.from(words));
|
|
146
|
+
this.length += words.length;
|
|
147
|
+
}
|
|
148
|
+
return at;
|
|
149
|
+
}
|
|
150
|
+
words() {
|
|
151
|
+
const out = new Uint32Array(this.length);
|
|
152
|
+
let at = 0;
|
|
153
|
+
for (const part of this.parts) {
|
|
154
|
+
out.set(part, at);
|
|
155
|
+
at += part.length;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
115
160
|
/**
|
|
116
161
|
* Path traces a Three.js scene on the GPU, behind the same API as `CpuRenderer`. The setters record the inputs;
|
|
117
162
|
* `render()` opens a frame with whatever is current and accumulates samples until it reaches `settings.samples`.
|
|
@@ -393,6 +438,21 @@ export class GpuRenderer extends EventEmitter {
|
|
|
393
438
|
this.emitIdle();
|
|
394
439
|
this.abort("The frame was cancelled.");
|
|
395
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Lets go of what the network denoiser holds from the last frame before a new one starts, so its buffers and the
|
|
443
|
+
* network's activations are not held on the device while the next scene is uploaded beside them. It waits for a run
|
|
444
|
+
* still in flight, which a replaced frame may have left.
|
|
445
|
+
*/
|
|
446
|
+
async releaseDenoiser() {
|
|
447
|
+
if (!this.oidnPass)
|
|
448
|
+
return;
|
|
449
|
+
await this.oidnRun;
|
|
450
|
+
if (this.denoiseOutput === this.oidnPass.output) {
|
|
451
|
+
this.denoiseOutput = null;
|
|
452
|
+
this.denoised = false;
|
|
453
|
+
}
|
|
454
|
+
await this.oidnPass.release();
|
|
455
|
+
}
|
|
396
456
|
/** Releases the device. A frame in flight rejects with an `AbortError`. */
|
|
397
457
|
dispose() {
|
|
398
458
|
if (this.disposed)
|
|
@@ -415,6 +475,40 @@ export class GpuRenderer extends EventEmitter {
|
|
|
415
475
|
this.accumulator = null;
|
|
416
476
|
this.gpu = null;
|
|
417
477
|
this.ready = null;
|
|
478
|
+
logMemory("disposed");
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* The device was lost: everything that lived on it is gone with it. The next frame asks for a new device and uploads
|
|
482
|
+
* the scene again; the scene's own assets (serialized meshes, translated shaders, the baked sky) are kept.
|
|
483
|
+
*/
|
|
484
|
+
dropDevice() {
|
|
485
|
+
// Whatever the old frame still awaits belongs to the dead device; a newer revision makes it give up quietly.
|
|
486
|
+
this.revision++;
|
|
487
|
+
this.resume();
|
|
488
|
+
this.context?.unconfigure();
|
|
489
|
+
this.context = null;
|
|
490
|
+
this.ready = null;
|
|
491
|
+
this.gpu = null;
|
|
492
|
+
this.accumulator = null;
|
|
493
|
+
this.pathTrace = null;
|
|
494
|
+
this.present = null;
|
|
495
|
+
this.buffers = null;
|
|
496
|
+
this.packed = null;
|
|
497
|
+
this.target = null;
|
|
498
|
+
this.packedView = "";
|
|
499
|
+
this.imageResolve = null;
|
|
500
|
+
this.resolve = null;
|
|
501
|
+
this.denoisePass = null;
|
|
502
|
+
this.oidnPass = null;
|
|
503
|
+
this.denoiseOutput = null;
|
|
504
|
+
this.oidnRun = Promise.resolve();
|
|
505
|
+
this.oidnBusy = false;
|
|
506
|
+
this.lensPass = null;
|
|
507
|
+
this.lensKey = "";
|
|
508
|
+
this.finished = null;
|
|
509
|
+
this.denoised = false;
|
|
510
|
+
this.sceneDirty = true;
|
|
511
|
+
this.scheduler.reset();
|
|
418
512
|
}
|
|
419
513
|
/** Takes the frame in flight, if any, so it can be settled after the events go out. */
|
|
420
514
|
settle() {
|
|
@@ -436,29 +530,64 @@ export class GpuRenderer extends EventEmitter {
|
|
|
436
530
|
this.emit("bucket", { worker: 0, bucket: null, width: this.output?.width ?? 0, height: this.output?.height ?? 0 });
|
|
437
531
|
}
|
|
438
532
|
async device() {
|
|
439
|
-
this.ready
|
|
440
|
-
|
|
441
|
-
|
|
533
|
+
if (!this.ready) {
|
|
534
|
+
const ready = requestRenderDevice({
|
|
535
|
+
onLost: reason => {
|
|
536
|
+
// A device lost after it was replaced, or by dispose(), has nothing left to stop.
|
|
537
|
+
if (this.disposed || this.ready !== ready)
|
|
538
|
+
return;
|
|
539
|
+
logMemory("device lost", { reason });
|
|
540
|
+
this.dropDevice();
|
|
442
541
|
this.fail(new Error(`GPU renderer: the device was lost (${reason}).`));
|
|
443
|
-
|
|
444
|
-
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
this.ready = ready;
|
|
545
|
+
// A device that could not be acquired is asked for again by the next frame.
|
|
546
|
+
ready.catch(() => { if (this.ready === ready)
|
|
547
|
+
this.ready = null; });
|
|
548
|
+
}
|
|
445
549
|
this.gpu = await this.ready;
|
|
446
550
|
return this.gpu;
|
|
447
551
|
}
|
|
448
552
|
async accumulate(revision, camera) {
|
|
449
553
|
this.emit("progress", { completed: 0, total: 1, phase: "prepare" });
|
|
554
|
+
const startedAt = performance.now();
|
|
555
|
+
await this.releaseDenoiser();
|
|
450
556
|
const gpu = await this.device();
|
|
451
557
|
const scene = await this.scene;
|
|
452
558
|
const environment = await this.environmentImage;
|
|
453
559
|
const width = this.renderWidth, height = this.renderHeight;
|
|
454
560
|
this.check(revision);
|
|
561
|
+
const fresh = !this.assets;
|
|
455
562
|
this.assets ??= prepareSceneAssets(scene, { device: gpu.device, sky: this.skyBake });
|
|
456
563
|
const assets = await this.assets;
|
|
457
564
|
this.skyBake = assets.skyBake;
|
|
565
|
+
// Measuring the snapshot walks all of it, which only the memory log is worth.
|
|
566
|
+
if (fresh && memoryLogging()) {
|
|
567
|
+
const usage = textureUsage(scene.textures, scene.meshes.flatMap(mesh => mesh.materials));
|
|
568
|
+
const stored = textureChannels(assets.scene, assets.programs);
|
|
569
|
+
logMemory("texture usage", {
|
|
570
|
+
totalMB: usage.totalMB,
|
|
571
|
+
ifOnlyReadChannelsMB: usage.packedMB,
|
|
572
|
+
...Object.fromEntries(usage.textures.map((row, k) => [`${k + 1}. ${row.id}`,
|
|
573
|
+
`${row.size} ${row.MB} MB, ${row.slots} [${row.channels}] -> ${row.packedMB} MB; `
|
|
574
|
+
+ (stored.single.has(row.fullId) ? "stored one byte a texel" : `whole: ${stored.whole.get(row.fullId)}`)])),
|
|
575
|
+
});
|
|
576
|
+
logMemory("scene assets", {
|
|
577
|
+
snapshotMB: toMB(retainedBytes(scene)),
|
|
578
|
+
meshes: scene.meshes.length,
|
|
579
|
+
textures: scene.textures.length,
|
|
580
|
+
texturesMB: toMB(retainedBytes(scene.textures)),
|
|
581
|
+
environmentMB: environment ? toMB(retainedBytes(environment)) : 0,
|
|
582
|
+
...sceneBreakdown(scene.meshes),
|
|
583
|
+
ms: Math.round(performance.now() - startedAt),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
458
586
|
this.check(revision);
|
|
459
587
|
await this.prepare(gpu, revision, assets, camera, width, height, environment);
|
|
460
588
|
this.output = { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
|
461
589
|
this.emit("start", { width, height });
|
|
590
|
+
logMemory("frame start", { width, height, prepareMs: Math.round(performance.now() - startedAt) });
|
|
462
591
|
const target = Math.max(1, Math.round(this.current.samples));
|
|
463
592
|
// Adaptive sampling: the frame has the work of `target` paths over the whole image to spend. Pixels that have
|
|
464
593
|
// converged stop taking samples, and a row with none left stops being dispatched, so what they would have used goes
|
|
@@ -598,6 +727,7 @@ export class GpuRenderer extends EventEmitter {
|
|
|
598
727
|
this.check(revision);
|
|
599
728
|
this.output = image;
|
|
600
729
|
this.finished = { width, height };
|
|
730
|
+
logMemory("frame done", { width, height, samples: taken, ms: Math.round(performance.now() - startedAt) });
|
|
601
731
|
this.emitIdle();
|
|
602
732
|
this.emit("pixels", { x: 0, y: 0, ...image });
|
|
603
733
|
return image;
|
|
@@ -628,14 +758,19 @@ export class GpuRenderer extends EventEmitter {
|
|
|
628
758
|
/** Uploads what changed and makes sure the passes point at the current buffers. */
|
|
629
759
|
async prepare(gpu, revision, assets, camera, width, height, image) {
|
|
630
760
|
this.pathTrace ??= new PathTracePass(gpu.device);
|
|
761
|
+
const pathTrace = this.pathTrace;
|
|
631
762
|
const materials = assets.scene.meshes.flatMap(mesh => mesh.materials);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
763
|
+
// The kernel is compiled once the scene is packed: whether it enters instances depends on what repeats.
|
|
764
|
+
const compile = async (instances) => {
|
|
765
|
+
await pathTrace.setPrograms(assets.programs, this.current.subsurface.enabled && materials.some(material => (material.type === "MeshStandardMaterial" || material.type === "MeshPhysicalMaterial")
|
|
766
|
+
&& subsurfaceOf(material) !== null),
|
|
767
|
+
// Even turned off, a volume's boundary has to stay invisible, which takes the same code.
|
|
768
|
+
materials.some(material => volumeOf(material) !== null), this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0),
|
|
769
|
+
// Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
|
|
770
|
+
this.current.globalIllumination.portals !== false && materials.some(material => material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
|
|
771
|
+
&& !(Number(material.values.thickness) > 0)), materials.some(material => grassOf(material) !== null), instances);
|
|
772
|
+
this.check(revision);
|
|
773
|
+
};
|
|
639
774
|
// A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
|
|
640
775
|
const environment = assets.sky?.environment ?? image;
|
|
641
776
|
this.environmentAverage = environment ? averageRadiance(environment) : null;
|
|
@@ -650,17 +785,23 @@ export class GpuRenderer extends EventEmitter {
|
|
|
650
785
|
this.buffers = null;
|
|
651
786
|
this.target = null;
|
|
652
787
|
this.packed = null;
|
|
653
|
-
|
|
788
|
+
// The kernel starts compiling as soon as packing knows whether the scene repeats geometry, and compiles while
|
|
789
|
+
// the hierarchy is built and uploaded: for a large kernel that takes seconds.
|
|
790
|
+
const packed = await this.packScene(gpu, revision, planar?.scene ?? assets.scene, assets, matrices, width, height, environment, true, instanced => { compile(instanced).catch(() => undefined); });
|
|
654
791
|
this.packed = packed.packed;
|
|
655
792
|
this.buffers = packed.buffers;
|
|
656
793
|
this.target = packed;
|
|
657
794
|
this.sceneDirty = false;
|
|
658
795
|
this.planar = !!planar;
|
|
796
|
+
// The captures trace with the same kernel, so they are taken once it is compiled for this scene.
|
|
797
|
+
await compile(packed.instanced);
|
|
659
798
|
if (planar)
|
|
660
799
|
await this.renderCaptures(gpu, revision, planar, assets, environment);
|
|
661
800
|
// Only once the captures are in: a frame replaced while they were taken has to take them again.
|
|
662
801
|
this.packedView = view;
|
|
663
802
|
}
|
|
803
|
+
// The settings may have changed what the kernel compiles in since the scene was packed.
|
|
804
|
+
await compile(this.target?.instanced ?? false);
|
|
664
805
|
const resized = !this.accumulator || this.accumulator.resize(width, height);
|
|
665
806
|
this.accumulator ??= new Accumulator(gpu.device, width, height);
|
|
666
807
|
if (resized)
|
|
@@ -676,52 +817,110 @@ export class GpuRenderer extends EventEmitter {
|
|
|
676
817
|
this.encodePresent(encoder, width, height);
|
|
677
818
|
gpu.device.queue.submit([encoder.finish()]);
|
|
678
819
|
}
|
|
820
|
+
/** The memory log's account of a packed scene: what repeats, what it takes, and which meshes took longest. */
|
|
821
|
+
logPacking(geometry, meshTimes, sizes) {
|
|
822
|
+
const repeated = geometry.instancing;
|
|
823
|
+
const slowest = [...meshTimes].sort((a, b) => b.ms - a.ms).slice(0, 5)
|
|
824
|
+
.map(info => ({ ...info, ms: Math.round(info.ms) }));
|
|
825
|
+
const byPath = {};
|
|
826
|
+
for (const info of meshTimes) {
|
|
827
|
+
byPath[info.path] ??= { meshes: 0, triangles: 0, ms: 0 };
|
|
828
|
+
byPath[info.path].meshes++;
|
|
829
|
+
byPath[info.path].triangles += info.triangles;
|
|
830
|
+
byPath[info.path].ms += Math.round(info.ms);
|
|
831
|
+
}
|
|
832
|
+
logMemory("scene packed", {
|
|
833
|
+
sceneTriangles: geometry.triangleCount,
|
|
834
|
+
instances: repeated?.instances.length ?? 0,
|
|
835
|
+
prototypes: repeated?.prototypes.length ?? 0,
|
|
836
|
+
prototypeTriangles: repeated?.triangleCount ?? 0,
|
|
837
|
+
instancedTriangles: repeated?.materials.length ?? 0,
|
|
838
|
+
vertices: geometry.vertexCount,
|
|
839
|
+
geometryMB: toMB(retainedBytes(geometry)),
|
|
840
|
+
...sizes,
|
|
841
|
+
byPath,
|
|
842
|
+
slowest,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
679
845
|
/** Packs and uploads one scene: its textures, geometry, hierarchy, lights and every extra word they point at. */
|
|
680
|
-
async packScene(gpu, revision, scene, assets, matrices, width, height, environment) {
|
|
846
|
+
async packScene(gpu, revision, scene, assets, matrices, width, height, environment, instancing = true, onPacked) {
|
|
681
847
|
// The textures are planned first: a material refers to one by the index it lands on in that table, and to its
|
|
682
848
|
// extra words by where they land after the texels. The environment rides along in the same buffer.
|
|
683
|
-
const
|
|
684
|
-
const
|
|
685
|
-
const
|
|
686
|
-
const at = plan.extraOffset + extra.length;
|
|
687
|
-
for (let i = 0; i < words.length; i++)
|
|
688
|
-
extra.push(words[i]);
|
|
689
|
-
return at;
|
|
690
|
-
};
|
|
849
|
+
const packBegan = performance.now();
|
|
850
|
+
const plan = planTextures(scene, environment ? [environment.texture] : [], singleChannelTextures(scene, assets.programs));
|
|
851
|
+
const extra = new ExtraWords(plan.extraOffset);
|
|
691
852
|
const cdf = environment
|
|
692
|
-
? append(new Uint32Array(environment.distribution.buffer, environment.distribution.byteOffset, environment.distribution.length))
|
|
853
|
+
? extra.append(new Uint32Array(environment.distribution.buffer, environment.distribution.byteOffset, environment.distribution.length))
|
|
693
854
|
: plan.extraOffset;
|
|
694
855
|
const textureData = new Map(scene.textures.map(texture => [texture.id, texture]));
|
|
695
|
-
const
|
|
856
|
+
const meshTimes = [];
|
|
857
|
+
// Sliced, so a large scene does not hold the page while it packs.
|
|
858
|
+
const geometry = await packGeometrySliced(scene, {
|
|
696
859
|
textures: plan.indexOf,
|
|
697
860
|
textureData,
|
|
698
861
|
camera: matrices,
|
|
699
862
|
width,
|
|
700
863
|
height,
|
|
701
864
|
programs: assets.programs,
|
|
702
|
-
extraBase:
|
|
865
|
+
extraBase: extra.next,
|
|
703
866
|
fog: scene.fog,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
867
|
+
instancing,
|
|
868
|
+
onMesh: memoryLogging() ? info => meshTimes.push(info) : undefined,
|
|
869
|
+
}, yieldForWork);
|
|
870
|
+
this.check(revision);
|
|
871
|
+
onPacked?.(geometry.instancing !== null);
|
|
872
|
+
const geometryMs = Math.round(performance.now() - packBegan);
|
|
873
|
+
extra.append(geometry.extraWords);
|
|
874
|
+
const lights = packLights(geometry, scene.lights, { power: environmentPower(environment) }, { textures: plan.indexOf, extraBase: extra.next });
|
|
875
|
+
extra.append(lights.extraWords);
|
|
708
876
|
const foreground = assets.programs.foreground;
|
|
709
877
|
const skyData = foreground && assets.sky
|
|
710
|
-
? append(foreground.packUniforms({ ...assets.sky.uniforms, uCpuSky: { texture: "procedural-sky" } }, plan.indexOf))
|
|
878
|
+
? extra.append(foreground.packUniforms({ ...assets.sky.uniforms, uCpuSky: { texture: "procedural-sky" } }, plan.indexOf))
|
|
711
879
|
: 0;
|
|
712
880
|
const volumetric = packVolumetricLights(scene.lights);
|
|
713
|
-
const volumetricData = append(new Uint32Array(volumetric.buffer));
|
|
714
|
-
const grass = packGrass(geometry,
|
|
881
|
+
const volumetricData = extra.append(new Uint32Array(volumetric.buffer));
|
|
882
|
+
const grass = packGrass(geometry, extra.next);
|
|
715
883
|
if (grass)
|
|
716
|
-
append(grass.words);
|
|
717
|
-
const
|
|
718
|
-
const
|
|
884
|
+
extra.append(grass.words);
|
|
885
|
+
const bvhStarted = performance.now();
|
|
886
|
+
const sceneBvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
|
|
887
|
+
let hierarchy = sceneBvh;
|
|
888
|
+
let instanceData = 0;
|
|
889
|
+
const repeated = geometry.instancing;
|
|
890
|
+
if (repeated) {
|
|
891
|
+
// One hierarchy per prototype, in its own space, joined under the tree over the instances.
|
|
892
|
+
const prototypeBvhs = [];
|
|
893
|
+
for (const prototype of repeated.prototypes) {
|
|
894
|
+
prototypeBvhs.push(await this.bvh.build(repeated.positions.subarray(prototype.first * 9, (prototype.first + prototype.count) * 9), prototype.count));
|
|
895
|
+
this.check(revision);
|
|
896
|
+
}
|
|
897
|
+
const boxes = repeated.prototypes.map(prototype => trianglesBox(repeated.positions, prototype.first, prototype.count));
|
|
898
|
+
const merged = mergeHierarchies(sceneBvh, geometry.triangleCount, prototypeBvhs, repeated.prototypes.map(prototype => prototype.count), repeated.instances.map(instance => transformBox(instance.matrix, boxes[instance.prototype].low, boxes[instance.prototype].high)));
|
|
899
|
+
instanceData = extra.append(instanceWords(repeated.instances, merged.roots, merged.storageFirst));
|
|
900
|
+
hierarchy = merged;
|
|
901
|
+
}
|
|
902
|
+
const bvhMs = Math.round(performance.now() - bvhStarted);
|
|
903
|
+
const extraWords = extra.words();
|
|
904
|
+
const textures = textureUpload(plan, extraWords);
|
|
905
|
+
if (memoryLogging())
|
|
906
|
+
this.logPacking(geometry, meshTimes, {
|
|
907
|
+
extraWords: extraWords.length,
|
|
908
|
+
texturesMB: toMB(textures.byteLength),
|
|
909
|
+
grassMB: grass ? toMB(grass.words.byteLength) : 0,
|
|
910
|
+
bvhNodesMB: toMB(hierarchy.nodes.byteLength),
|
|
911
|
+
geometryMs,
|
|
912
|
+
bvhMs,
|
|
913
|
+
});
|
|
914
|
+
const uploadStarted = performance.now();
|
|
719
915
|
// Nothing is on the device yet, so a frame replaced while the hierarchy was built has nothing to give back.
|
|
720
916
|
this.check(revision);
|
|
721
|
-
const buffers = uploadScene(gpu.device, geometry,
|
|
722
|
-
const textureOffsets = new Map([...textures.indexOf]
|
|
723
|
-
.map(([id, index]) => [id, textures.data[textureEntry(index) + 2]]));
|
|
917
|
+
const buffers = uploadScene(gpu.device, geometry, hierarchy, textures, lights, environment, cdf);
|
|
918
|
+
const textureOffsets = new Map([...textures.indexOf].map(([id, index]) => [id, textures.offsets[index]]));
|
|
724
919
|
const packed = { cameraDependent: geometry.cameraDependent, textureOffsets };
|
|
920
|
+
logMemory("scene uploaded", {
|
|
921
|
+
ms: Math.round(performance.now() - uploadStarted),
|
|
922
|
+
upload: Object.fromEntries(Object.entries(buffers.bytes).map(([kind, bytes]) => [kind, toMB(bytes)])),
|
|
923
|
+
});
|
|
725
924
|
if (buffers.bytes.largest > gpu.maxStorageBufferBindingSize) {
|
|
726
925
|
buffers.destroy();
|
|
727
926
|
throw new Error("GPU renderer: this scene needs a larger storage buffer than the device allows.");
|
|
@@ -735,6 +934,8 @@ export class GpuRenderer extends EventEmitter {
|
|
|
735
934
|
volumes: volumeTriangles(geometry),
|
|
736
935
|
grassShells: grass?.shells ?? GRASS_NONE,
|
|
737
936
|
grassEntries: grass?.entries ?? 0,
|
|
937
|
+
instanced: repeated !== null,
|
|
938
|
+
instanceData,
|
|
738
939
|
};
|
|
739
940
|
}
|
|
740
941
|
/**
|
|
@@ -754,7 +955,8 @@ export class GpuRenderer extends EventEmitter {
|
|
|
754
955
|
for (const capture of planar.captures) {
|
|
755
956
|
let target = scenes.get(capture.scene);
|
|
756
957
|
if (!target) {
|
|
757
|
-
|
|
958
|
+
// Instances only where the kernel was compiled to enter them, which the main scene decided.
|
|
959
|
+
target = await this.packScene(gpu, revision, capture.scene === "scene" ? planar.captureScene : empty, assets, cameraMatrices(capture.camera, width, height), width, height, environment, main.instanced);
|
|
758
960
|
scenes.set(capture.scene, target);
|
|
759
961
|
}
|
|
760
962
|
const clear = gpu.device.createCommandEncoder();
|
|
@@ -984,6 +1186,11 @@ export class GpuRenderer extends EventEmitter {
|
|
|
984
1186
|
colorOffset: buffers?.colorOffset ?? 0,
|
|
985
1187
|
hasColors: buffers?.hasColors ?? false,
|
|
986
1188
|
varyingOffset: buffers?.varyingOffset ?? 0,
|
|
1189
|
+
sceneTriangles: buffers?.sceneTriangles ?? 0,
|
|
1190
|
+
instanceOffset: buffers?.instanceOffset ?? 0,
|
|
1191
|
+
cornerOffset: buffers?.cornerOffset ?? 0,
|
|
1192
|
+
normalOffset: buffers?.normalOffset ?? 0,
|
|
1193
|
+
instanceData: target?.instanceData ?? 0,
|
|
987
1194
|
environmentTexture: surrounding?.texture ?? NO_TEXTURE,
|
|
988
1195
|
environmentCdf: surrounding?.cdf ?? 0,
|
|
989
1196
|
environmentCdfSize: [surrounding?.width ?? 0, surrounding?.height ?? 0],
|