@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/src/renderer.ts
CHANGED
|
@@ -3,12 +3,15 @@ import { Accumulator } from "./accumulator.js";
|
|
|
3
3
|
import { BvhBuilder, createDefaultBvhWorker, type BvhWorkerFactory } from "./bvhBuilder.js";
|
|
4
4
|
import { cameraMatrices, type CameraMatrices } from "./camera.js";
|
|
5
5
|
import { requestRenderDevice, type RenderDevice } from "./device.js";
|
|
6
|
+
import { logMemory, memoryLogging, retainedBytes, sceneBreakdown, textureUsage, toMB } from "./memoryProbe.js";
|
|
6
7
|
import { loadEnvironment, type PackedEnvironment } from "./environment.js";
|
|
7
8
|
import { DispatchScheduler } from "./dispatchScheduler.js";
|
|
8
|
-
import {
|
|
9
|
+
import { packGeometrySliced, type MeshPacked, type PackedGeometry } from "./geometry.js";
|
|
10
|
+
import { instanceWords, mergeHierarchies, transformBox, trianglesBox } from "./instancing.js";
|
|
11
|
+
import type { PackedBvh } from "./bvh.js";
|
|
9
12
|
import { packLights } from "./lights.js";
|
|
10
13
|
import { NO_PROGRAM } from "./materialLayout.js";
|
|
11
|
-
import { NO_TEXTURE, planTextures,
|
|
14
|
+
import { NO_TEXTURE, planTextures, singleChannelTextures, textureChannels, textureUpload } from "./texturePack.js";
|
|
12
15
|
import { PathTracePass, type FrameUniforms } from "./pathTracePass.js";
|
|
13
16
|
import { ImageResolvePass } from "./imageResolve.js";
|
|
14
17
|
import { PresentPass } from "./presentPass.js";
|
|
@@ -45,7 +48,7 @@ import { DEFAULT_OIDN_WEIGHTS, OidnPass } from "./oidnPass.js";
|
|
|
45
48
|
import { CaptureResolvePass, planarCaptures, type PlanarCaptures } from "./planarCapture.js";
|
|
46
49
|
import { prepareSceneAssets, type SceneAssets } from "./sceneAssets.js";
|
|
47
50
|
import { packVolumetricLights, VOLUMETRIC_WORDS } from "./volumetric.js";
|
|
48
|
-
import { environmentPower, uploadScene, type SceneBuffers } from "./sceneUpload.js";
|
|
51
|
+
import { environmentPower, uploadScene, type SceneBuffers, type UploadHierarchy } from "./sceneUpload.js";
|
|
49
52
|
import { atmosphereExtinction, averageRadiance, brightestSun, gradientAverage } from "./atmosphere.js";
|
|
50
53
|
import {
|
|
51
54
|
DEFAULT_RENDER_ENVIRONMENT,
|
|
@@ -153,6 +156,21 @@ function linearColor(color: string | undefined, fallback: readonly number[]): nu
|
|
|
153
156
|
/** Hands the thread back to the page for a moment, so input and painting go on between submits. */
|
|
154
157
|
const yieldToPage = () => new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
155
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The same, through a message rather than a timer: a page in the background has its timers spaced out, up to a second
|
|
161
|
+
* each, which would stretch packing a large scene by minutes, while messages still arrive at once.
|
|
162
|
+
*/
|
|
163
|
+
const yieldForWork = typeof MessageChannel === "undefined" ? yieldToPage : () => new Promise<void>(resolve => {
|
|
164
|
+
const channel = new MessageChannel();
|
|
165
|
+
|
|
166
|
+
channel.port1.onmessage = () => {
|
|
167
|
+
channel.port1.close();
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
channel.port2.postMessage(null);
|
|
172
|
+
});
|
|
173
|
+
|
|
156
174
|
/**
|
|
157
175
|
* How often the canvas is drawn and the page gets the thread back while a frame traces: about once a display frame.
|
|
158
176
|
* Waiting for an animation frame after every band would leave the device idle for most of each one.
|
|
@@ -256,6 +274,46 @@ interface PackedTarget {
|
|
|
256
274
|
/** Where the lawns' hierarchy and entries start in the texture buffer: see grass.ts. */
|
|
257
275
|
grassShells: number;
|
|
258
276
|
grassEntries: number;
|
|
277
|
+
/** Whether the scene keeps repeated geometry as instances, and where their transforms start in the texture buffer. */
|
|
278
|
+
instanced: boolean;
|
|
279
|
+
instanceData: number;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Words appended after the texels, kept as typed runs: a plain array of numbers costs twice as much while it grows. */
|
|
283
|
+
class ExtraWords {
|
|
284
|
+
private readonly parts: Uint32Array[] = [];
|
|
285
|
+
private length = 0;
|
|
286
|
+
|
|
287
|
+
constructor(private readonly base: number) {}
|
|
288
|
+
|
|
289
|
+
/** Where the next words will land, absolutely. */
|
|
290
|
+
get next(): number {
|
|
291
|
+
return this.base + this.length;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Appends words and says where they landed. */
|
|
295
|
+
append(words: ArrayLike<number>): number {
|
|
296
|
+
const at = this.next;
|
|
297
|
+
|
|
298
|
+
if (words.length) {
|
|
299
|
+
this.parts.push(words instanceof Uint32Array ? words : Uint32Array.from(words));
|
|
300
|
+
this.length += words.length;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return at;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
words(): Uint32Array {
|
|
307
|
+
const out = new Uint32Array(this.length);
|
|
308
|
+
let at = 0;
|
|
309
|
+
|
|
310
|
+
for (const part of this.parts) {
|
|
311
|
+
out.set(part, at);
|
|
312
|
+
at += part.length;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
259
317
|
}
|
|
260
318
|
|
|
261
319
|
/**
|
|
@@ -583,6 +641,24 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
583
641
|
this.abort("The frame was cancelled.");
|
|
584
642
|
}
|
|
585
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Lets go of what the network denoiser holds from the last frame before a new one starts, so its buffers and the
|
|
646
|
+
* network's activations are not held on the device while the next scene is uploaded beside them. It waits for a run
|
|
647
|
+
* still in flight, which a replaced frame may have left.
|
|
648
|
+
*/
|
|
649
|
+
private async releaseDenoiser() {
|
|
650
|
+
if (!this.oidnPass) return;
|
|
651
|
+
|
|
652
|
+
await this.oidnRun;
|
|
653
|
+
|
|
654
|
+
if (this.denoiseOutput === this.oidnPass.output) {
|
|
655
|
+
this.denoiseOutput = null;
|
|
656
|
+
this.denoised = false;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
await this.oidnPass.release();
|
|
660
|
+
}
|
|
661
|
+
|
|
586
662
|
/** Releases the device. A frame in flight rejects with an `AbortError`. */
|
|
587
663
|
dispose() {
|
|
588
664
|
if (this.disposed) return;
|
|
@@ -605,6 +681,41 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
605
681
|
this.accumulator = null;
|
|
606
682
|
this.gpu = null;
|
|
607
683
|
this.ready = null;
|
|
684
|
+
logMemory("disposed");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The device was lost: everything that lived on it is gone with it. The next frame asks for a new device and uploads
|
|
689
|
+
* the scene again; the scene's own assets (serialized meshes, translated shaders, the baked sky) are kept.
|
|
690
|
+
*/
|
|
691
|
+
private dropDevice() {
|
|
692
|
+
// Whatever the old frame still awaits belongs to the dead device; a newer revision makes it give up quietly.
|
|
693
|
+
this.revision++;
|
|
694
|
+
this.resume();
|
|
695
|
+
this.context?.unconfigure();
|
|
696
|
+
this.context = null;
|
|
697
|
+
this.ready = null;
|
|
698
|
+
this.gpu = null;
|
|
699
|
+
this.accumulator = null;
|
|
700
|
+
this.pathTrace = null;
|
|
701
|
+
this.present = null;
|
|
702
|
+
this.buffers = null;
|
|
703
|
+
this.packed = null;
|
|
704
|
+
this.target = null;
|
|
705
|
+
this.packedView = "";
|
|
706
|
+
this.imageResolve = null;
|
|
707
|
+
this.resolve = null;
|
|
708
|
+
this.denoisePass = null;
|
|
709
|
+
this.oidnPass = null;
|
|
710
|
+
this.denoiseOutput = null;
|
|
711
|
+
this.oidnRun = Promise.resolve();
|
|
712
|
+
this.oidnBusy = false;
|
|
713
|
+
this.lensPass = null;
|
|
714
|
+
this.lensKey = "";
|
|
715
|
+
this.finished = null;
|
|
716
|
+
this.denoised = false;
|
|
717
|
+
this.sceneDirty = true;
|
|
718
|
+
this.scheduler.reset();
|
|
608
719
|
}
|
|
609
720
|
|
|
610
721
|
/** Takes the frame in flight, if any, so it can be settled after the events go out. */
|
|
@@ -634,11 +745,23 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
634
745
|
}
|
|
635
746
|
|
|
636
747
|
private async device(): Promise<RenderDevice> {
|
|
637
|
-
this.ready
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
748
|
+
if (!this.ready) {
|
|
749
|
+
const ready: Promise<RenderDevice> = requestRenderDevice({
|
|
750
|
+
onLost: reason => {
|
|
751
|
+
// A device lost after it was replaced, or by dispose(), has nothing left to stop.
|
|
752
|
+
if (this.disposed || this.ready !== ready) return;
|
|
753
|
+
|
|
754
|
+
logMemory("device lost", { reason });
|
|
755
|
+
this.dropDevice();
|
|
756
|
+
this.fail(new Error(`GPU renderer: the device was lost (${reason}).`));
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
this.ready = ready;
|
|
761
|
+
// A device that could not be acquired is asked for again by the next frame.
|
|
762
|
+
ready.catch(() => { if (this.ready === ready) this.ready = null; });
|
|
763
|
+
}
|
|
764
|
+
|
|
642
765
|
this.gpu = await this.ready;
|
|
643
766
|
|
|
644
767
|
return this.gpu;
|
|
@@ -647,22 +770,54 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
647
770
|
private async accumulate(revision: number, camera: CameraState): Promise<RenderImage> {
|
|
648
771
|
this.emit("progress", { completed: 0, total: 1, phase: "prepare" });
|
|
649
772
|
|
|
773
|
+
const startedAt = performance.now();
|
|
774
|
+
|
|
775
|
+
await this.releaseDenoiser();
|
|
776
|
+
|
|
650
777
|
const gpu = await this.device();
|
|
651
778
|
const scene = await this.scene;
|
|
652
779
|
const environment = await this.environmentImage;
|
|
653
780
|
const width = this.renderWidth, height = this.renderHeight;
|
|
654
781
|
|
|
655
782
|
this.check(revision);
|
|
783
|
+
|
|
784
|
+
const fresh = !this.assets;
|
|
785
|
+
|
|
656
786
|
this.assets ??= prepareSceneAssets(scene, { device: gpu.device, sky: this.skyBake });
|
|
657
787
|
|
|
658
788
|
const assets = await this.assets;
|
|
659
789
|
|
|
660
790
|
this.skyBake = assets.skyBake;
|
|
661
791
|
|
|
792
|
+
// Measuring the snapshot walks all of it, which only the memory log is worth.
|
|
793
|
+
if (fresh && memoryLogging()) {
|
|
794
|
+
const usage = textureUsage(scene.textures, scene.meshes.flatMap(mesh => mesh.materials));
|
|
795
|
+
|
|
796
|
+
const stored = textureChannels(assets.scene, assets.programs);
|
|
797
|
+
|
|
798
|
+
logMemory("texture usage", {
|
|
799
|
+
totalMB: usage.totalMB,
|
|
800
|
+
ifOnlyReadChannelsMB: usage.packedMB,
|
|
801
|
+
...Object.fromEntries(usage.textures.map((row, k) => [`${k + 1}. ${row.id}`,
|
|
802
|
+
`${row.size} ${row.MB} MB, ${row.slots} [${row.channels}] -> ${row.packedMB} MB; `
|
|
803
|
+
+ (stored.single.has(row.fullId) ? "stored one byte a texel" : `whole: ${stored.whole.get(row.fullId)}`)])),
|
|
804
|
+
});
|
|
805
|
+
logMemory("scene assets", {
|
|
806
|
+
snapshotMB: toMB(retainedBytes(scene)),
|
|
807
|
+
meshes: scene.meshes.length,
|
|
808
|
+
textures: scene.textures.length,
|
|
809
|
+
texturesMB: toMB(retainedBytes(scene.textures)),
|
|
810
|
+
environmentMB: environment ? toMB(retainedBytes(environment)) : 0,
|
|
811
|
+
...sceneBreakdown(scene.meshes),
|
|
812
|
+
ms: Math.round(performance.now() - startedAt),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
662
816
|
this.check(revision);
|
|
663
817
|
await this.prepare(gpu, revision, assets, camera, width, height, environment);
|
|
664
818
|
this.output = { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
|
665
819
|
this.emit("start", { width, height });
|
|
820
|
+
logMemory("frame start", { width, height, prepareMs: Math.round(performance.now() - startedAt) });
|
|
666
821
|
|
|
667
822
|
const target = Math.max(1, Math.round(this.current.samples));
|
|
668
823
|
// Adaptive sampling: the frame has the work of `target` paths over the whole image to spend. Pixels that have
|
|
@@ -834,6 +989,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
834
989
|
this.check(revision);
|
|
835
990
|
this.output = image;
|
|
836
991
|
this.finished = { width, height };
|
|
992
|
+
logMemory("frame done", { width, height, samples: taken, ms: Math.round(performance.now() - startedAt) });
|
|
837
993
|
this.emitIdle();
|
|
838
994
|
this.emit("pixels", { x: 0, y: 0, ...image });
|
|
839
995
|
|
|
@@ -879,20 +1035,26 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
879
1035
|
height: number,
|
|
880
1036
|
image: PackedEnvironment | null) {
|
|
881
1037
|
this.pathTrace ??= new PathTracePass(gpu.device);
|
|
1038
|
+
const pathTrace = this.pathTrace;
|
|
882
1039
|
const materials = assets.scene.meshes.flatMap(mesh => mesh.materials);
|
|
883
1040
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
&&
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
&&
|
|
895
|
-
|
|
1041
|
+
// The kernel is compiled once the scene is packed: whether it enters instances depends on what repeats.
|
|
1042
|
+
const compile = async(instances: boolean) => {
|
|
1043
|
+
await pathTrace.setPrograms(assets.programs,
|
|
1044
|
+
this.current.subsurface.enabled && materials.some(material =>
|
|
1045
|
+
(material.type === "MeshStandardMaterial" || material.type === "MeshPhysicalMaterial")
|
|
1046
|
+
&& subsurfaceOf(material) !== null),
|
|
1047
|
+
// Even turned off, a volume's boundary has to stay invisible, which takes the same code.
|
|
1048
|
+
materials.some(material => volumeOf(material) !== null),
|
|
1049
|
+
this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0),
|
|
1050
|
+
// Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
|
|
1051
|
+
this.current.globalIllumination.portals !== false && materials.some(material =>
|
|
1052
|
+
material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
|
|
1053
|
+
&& !(Number(material.values.thickness) > 0)),
|
|
1054
|
+
materials.some(material => grassOf(material) !== null),
|
|
1055
|
+
instances);
|
|
1056
|
+
this.check(revision);
|
|
1057
|
+
};
|
|
896
1058
|
|
|
897
1059
|
// A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
|
|
898
1060
|
const environment = assets.sky?.environment ?? image;
|
|
@@ -912,8 +1074,11 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
912
1074
|
this.target = null;
|
|
913
1075
|
this.packed = null;
|
|
914
1076
|
|
|
1077
|
+
// The kernel starts compiling as soon as packing knows whether the scene repeats geometry, and compiles while
|
|
1078
|
+
// the hierarchy is built and uploaded: for a large kernel that takes seconds.
|
|
915
1079
|
const packed = await this.packScene(
|
|
916
|
-
gpu, revision, planar?.scene ?? assets.scene, assets, matrices, width, height, environment
|
|
1080
|
+
gpu, revision, planar?.scene ?? assets.scene, assets, matrices, width, height, environment, true,
|
|
1081
|
+
instanced => { compile(instanced).catch(() => undefined); });
|
|
917
1082
|
|
|
918
1083
|
this.packed = packed.packed;
|
|
919
1084
|
this.buffers = packed.buffers;
|
|
@@ -921,12 +1086,18 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
921
1086
|
this.sceneDirty = false;
|
|
922
1087
|
this.planar = !!planar;
|
|
923
1088
|
|
|
1089
|
+
// The captures trace with the same kernel, so they are taken once it is compiled for this scene.
|
|
1090
|
+
await compile(packed.instanced);
|
|
1091
|
+
|
|
924
1092
|
if (planar) await this.renderCaptures(gpu, revision, planar, assets, environment);
|
|
925
1093
|
|
|
926
1094
|
// Only once the captures are in: a frame replaced while they were taken has to take them again.
|
|
927
1095
|
this.packedView = view;
|
|
928
1096
|
}
|
|
929
1097
|
|
|
1098
|
+
// The settings may have changed what the kernel compiles in since the scene was packed.
|
|
1099
|
+
await compile(this.target?.instanced ?? false);
|
|
1100
|
+
|
|
930
1101
|
const resized = !this.accumulator || this.accumulator.resize(width, height);
|
|
931
1102
|
|
|
932
1103
|
this.accumulator ??= new Accumulator(gpu.device, width, height);
|
|
@@ -947,6 +1118,36 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
947
1118
|
gpu.device.queue.submit([encoder.finish()]);
|
|
948
1119
|
}
|
|
949
1120
|
|
|
1121
|
+
/** The memory log's account of a packed scene: what repeats, what it takes, and which meshes took longest. */
|
|
1122
|
+
private logPacking(geometry: PackedGeometry, meshTimes: readonly MeshPacked[], sizes: Record<string, number>) {
|
|
1123
|
+
const repeated = geometry.instancing;
|
|
1124
|
+
|
|
1125
|
+
const slowest = [...meshTimes].sort((a, b) => b.ms - a.ms).slice(0, 5)
|
|
1126
|
+
.map(info => ({ ...info, ms: Math.round(info.ms) }));
|
|
1127
|
+
|
|
1128
|
+
const byPath: Record<string, { meshes: number; triangles: number; ms: number }> = {};
|
|
1129
|
+
|
|
1130
|
+
for (const info of meshTimes) {
|
|
1131
|
+
byPath[info.path] ??= { meshes: 0, triangles: 0, ms: 0 };
|
|
1132
|
+
byPath[info.path].meshes++;
|
|
1133
|
+
byPath[info.path].triangles += info.triangles;
|
|
1134
|
+
byPath[info.path].ms += Math.round(info.ms);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
logMemory("scene packed", {
|
|
1138
|
+
sceneTriangles: geometry.triangleCount,
|
|
1139
|
+
instances: repeated?.instances.length ?? 0,
|
|
1140
|
+
prototypes: repeated?.prototypes.length ?? 0,
|
|
1141
|
+
prototypeTriangles: repeated?.triangleCount ?? 0,
|
|
1142
|
+
instancedTriangles: repeated?.materials.length ?? 0,
|
|
1143
|
+
vertices: geometry.vertexCount,
|
|
1144
|
+
geometryMB: toMB(retainedBytes(geometry)),
|
|
1145
|
+
...sizes,
|
|
1146
|
+
byPath,
|
|
1147
|
+
slowest,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
950
1151
|
/** Packs and uploads one scene: its textures, geometry, hierarchy, lights and every extra word they point at. */
|
|
951
1152
|
private async packScene(
|
|
952
1153
|
gpu: RenderDevice,
|
|
@@ -956,70 +1157,122 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
956
1157
|
matrices: CameraMatrices,
|
|
957
1158
|
width: number,
|
|
958
1159
|
height: number,
|
|
959
|
-
environment: PackedEnvironment | null
|
|
1160
|
+
environment: PackedEnvironment | null,
|
|
1161
|
+
instancing = true,
|
|
1162
|
+
onPacked?: (instanced: boolean) => void): Promise<PackedTarget> {
|
|
960
1163
|
// The textures are planned first: a material refers to one by the index it lands on in that table, and to its
|
|
961
1164
|
// extra words by where they land after the texels. The environment rides along in the same buffer.
|
|
962
|
-
const
|
|
963
|
-
const extra: number[] = [];
|
|
964
|
-
|
|
965
|
-
const append = (words: ArrayLike<number>) => {
|
|
966
|
-
const at = plan.extraOffset + extra.length;
|
|
1165
|
+
const packBegan = performance.now();
|
|
967
1166
|
|
|
968
|
-
|
|
1167
|
+
const plan = planTextures(scene, environment ? [environment.texture] : [],
|
|
1168
|
+
singleChannelTextures(scene, assets.programs));
|
|
969
1169
|
|
|
970
|
-
|
|
971
|
-
};
|
|
1170
|
+
const extra = new ExtraWords(plan.extraOffset);
|
|
972
1171
|
|
|
973
1172
|
const cdf = environment
|
|
974
|
-
? append(new Uint32Array(environment.distribution.buffer, environment.distribution.byteOffset,
|
|
1173
|
+
? extra.append(new Uint32Array(environment.distribution.buffer, environment.distribution.byteOffset,
|
|
975
1174
|
environment.distribution.length))
|
|
976
1175
|
: plan.extraOffset;
|
|
977
1176
|
|
|
978
1177
|
const textureData = new Map(scene.textures.map(texture => [texture.id, texture]));
|
|
1178
|
+
const meshTimes: MeshPacked[] = [];
|
|
979
1179
|
|
|
980
|
-
|
|
1180
|
+
// Sliced, so a large scene does not hold the page while it packs.
|
|
1181
|
+
const geometry = await packGeometrySliced(scene, {
|
|
981
1182
|
textures: plan.indexOf,
|
|
982
1183
|
textureData,
|
|
983
1184
|
camera: matrices,
|
|
984
1185
|
width,
|
|
985
1186
|
height,
|
|
986
1187
|
programs: assets.programs,
|
|
987
|
-
extraBase:
|
|
1188
|
+
extraBase: extra.next,
|
|
988
1189
|
fog: scene.fog,
|
|
989
|
-
|
|
1190
|
+
instancing,
|
|
1191
|
+
onMesh: memoryLogging() ? info => meshTimes.push(info) : undefined,
|
|
1192
|
+
}, yieldForWork);
|
|
1193
|
+
|
|
1194
|
+
this.check(revision);
|
|
1195
|
+
onPacked?.(geometry.instancing !== null);
|
|
990
1196
|
|
|
991
|
-
|
|
1197
|
+
const geometryMs = Math.round(performance.now() - packBegan);
|
|
1198
|
+
|
|
1199
|
+
extra.append(geometry.extraWords);
|
|
992
1200
|
|
|
993
1201
|
const lights = packLights(geometry, scene.lights, { power: environmentPower(environment) },
|
|
994
|
-
{ textures: plan.indexOf, extraBase:
|
|
1202
|
+
{ textures: plan.indexOf, extraBase: extra.next });
|
|
995
1203
|
|
|
996
|
-
append(lights.extraWords);
|
|
1204
|
+
extra.append(lights.extraWords);
|
|
997
1205
|
|
|
998
1206
|
const foreground = assets.programs.foreground;
|
|
999
1207
|
|
|
1000
1208
|
const skyData = foreground && assets.sky
|
|
1001
|
-
? append(foreground.packUniforms({ ...assets.sky.uniforms, uCpuSky: { texture: "procedural-sky" } },
|
|
1209
|
+
? extra.append(foreground.packUniforms({ ...assets.sky.uniforms, uCpuSky: { texture: "procedural-sky" } },
|
|
1210
|
+
plan.indexOf))
|
|
1002
1211
|
: 0;
|
|
1003
1212
|
|
|
1004
1213
|
const volumetric = packVolumetricLights(scene.lights);
|
|
1005
|
-
const volumetricData = append(new Uint32Array(volumetric.buffer));
|
|
1006
|
-
const grass = packGrass(geometry,
|
|
1214
|
+
const volumetricData = extra.append(new Uint32Array(volumetric.buffer));
|
|
1215
|
+
const grass = packGrass(geometry, extra.next);
|
|
1216
|
+
|
|
1217
|
+
if (grass) extra.append(grass.words);
|
|
1007
1218
|
|
|
1008
|
-
|
|
1219
|
+
const bvhStarted = performance.now();
|
|
1220
|
+
const sceneBvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
|
|
1221
|
+
let hierarchy: UploadHierarchy = sceneBvh;
|
|
1222
|
+
let instanceData = 0;
|
|
1223
|
+
const repeated = geometry.instancing;
|
|
1009
1224
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1225
|
+
if (repeated) {
|
|
1226
|
+
// One hierarchy per prototype, in its own space, joined under the tree over the instances.
|
|
1227
|
+
const prototypeBvhs: PackedBvh[] = [];
|
|
1228
|
+
|
|
1229
|
+
for (const prototype of repeated.prototypes) {
|
|
1230
|
+
prototypeBvhs.push(await this.bvh.build(
|
|
1231
|
+
repeated.positions.subarray(prototype.first * 9, (prototype.first + prototype.count) * 9), prototype.count));
|
|
1232
|
+
this.check(revision);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const boxes = repeated.prototypes.map(prototype =>
|
|
1236
|
+
trianglesBox(repeated.positions, prototype.first, prototype.count));
|
|
1237
|
+
|
|
1238
|
+
const merged = mergeHierarchies(sceneBvh, geometry.triangleCount, prototypeBvhs,
|
|
1239
|
+
repeated.prototypes.map(prototype => prototype.count),
|
|
1240
|
+
repeated.instances.map(instance =>
|
|
1241
|
+
transformBox(instance.matrix, boxes[instance.prototype].low, boxes[instance.prototype].high)));
|
|
1242
|
+
|
|
1243
|
+
instanceData = extra.append(instanceWords(repeated.instances, merged.roots, merged.storageFirst));
|
|
1244
|
+
hierarchy = merged;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const bvhMs = Math.round(performance.now() - bvhStarted);
|
|
1248
|
+
const extraWords = extra.words();
|
|
1249
|
+
const textures = textureUpload(plan, extraWords);
|
|
1250
|
+
|
|
1251
|
+
if (memoryLogging()) this.logPacking(geometry, meshTimes, {
|
|
1252
|
+
extraWords: extraWords.length,
|
|
1253
|
+
texturesMB: toMB(textures.byteLength),
|
|
1254
|
+
grassMB: grass ? toMB(grass.words.byteLength) : 0,
|
|
1255
|
+
bvhNodesMB: toMB(hierarchy.nodes.byteLength),
|
|
1256
|
+
geometryMs,
|
|
1257
|
+
bvhMs,
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
const uploadStarted = performance.now();
|
|
1012
1261
|
|
|
1013
1262
|
// Nothing is on the device yet, so a frame replaced while the hierarchy was built has nothing to give back.
|
|
1014
1263
|
this.check(revision);
|
|
1015
1264
|
|
|
1016
|
-
const buffers = uploadScene(gpu.device, geometry,
|
|
1265
|
+
const buffers = uploadScene(gpu.device, geometry, hierarchy, textures, lights, environment, cdf);
|
|
1017
1266
|
|
|
1018
|
-
const textureOffsets = new Map([...textures.indexOf]
|
|
1019
|
-
.map(([id, index]) => [id, textures.data[textureEntry(index) + 2]]));
|
|
1267
|
+
const textureOffsets = new Map([...textures.indexOf].map(([id, index]) => [id, textures.offsets[index]]));
|
|
1020
1268
|
|
|
1021
1269
|
const packed: PackedSummary = { cameraDependent: geometry.cameraDependent, textureOffsets };
|
|
1022
1270
|
|
|
1271
|
+
logMemory("scene uploaded", {
|
|
1272
|
+
ms: Math.round(performance.now() - uploadStarted),
|
|
1273
|
+
upload: Object.fromEntries(Object.entries(buffers.bytes).map(([kind, bytes]) => [kind, toMB(bytes)])),
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1023
1276
|
if (buffers.bytes.largest > gpu.maxStorageBufferBindingSize) {
|
|
1024
1277
|
buffers.destroy();
|
|
1025
1278
|
throw new Error("GPU renderer: this scene needs a larger storage buffer than the device allows.");
|
|
@@ -1034,6 +1287,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1034
1287
|
volumes: volumeTriangles(geometry),
|
|
1035
1288
|
grassShells: grass?.shells ?? GRASS_NONE,
|
|
1036
1289
|
grassEntries: grass?.entries ?? 0,
|
|
1290
|
+
instanced: repeated !== null,
|
|
1291
|
+
instanceData,
|
|
1037
1292
|
};
|
|
1038
1293
|
}
|
|
1039
1294
|
|
|
@@ -1058,8 +1313,9 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1058
1313
|
let target = scenes.get(capture.scene);
|
|
1059
1314
|
|
|
1060
1315
|
if (!target) {
|
|
1316
|
+
// Instances only where the kernel was compiled to enter them, which the main scene decided.
|
|
1061
1317
|
target = await this.packScene(gpu, revision, capture.scene === "scene" ? planar.captureScene : empty, assets,
|
|
1062
|
-
cameraMatrices(capture.camera, width, height), width, height, environment);
|
|
1318
|
+
cameraMatrices(capture.camera, width, height), width, height, environment, main.instanced);
|
|
1063
1319
|
scenes.set(capture.scene, target);
|
|
1064
1320
|
}
|
|
1065
1321
|
|
|
@@ -1339,6 +1595,11 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1339
1595
|
colorOffset: buffers?.colorOffset ?? 0,
|
|
1340
1596
|
hasColors: buffers?.hasColors ?? false,
|
|
1341
1597
|
varyingOffset: buffers?.varyingOffset ?? 0,
|
|
1598
|
+
sceneTriangles: buffers?.sceneTriangles ?? 0,
|
|
1599
|
+
instanceOffset: buffers?.instanceOffset ?? 0,
|
|
1600
|
+
cornerOffset: buffers?.cornerOffset ?? 0,
|
|
1601
|
+
normalOffset: buffers?.normalOffset ?? 0,
|
|
1602
|
+
instanceData: target?.instanceData ?? 0,
|
|
1342
1603
|
environmentTexture: surrounding?.texture ?? NO_TEXTURE,
|
|
1343
1604
|
environmentCdf: surrounding?.cdf ?? 0,
|
|
1344
1605
|
environmentCdfSize: [surrounding?.width ?? 0, surrounding?.height ?? 0],
|