@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.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +48 -10
  2. package/README.md +2 -0
  3. package/dist/accumulator.d.ts.map +1 -1
  4. package/dist/accumulator.js +1 -0
  5. package/dist/accumulator.js.map +1 -1
  6. package/dist/denoise.js +2 -2
  7. package/dist/denoise.js.map +1 -1
  8. package/dist/device.d.ts.map +1 -1
  9. package/dist/device.js +9 -0
  10. package/dist/device.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +5 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts +48 -8
  15. package/dist/geometry.d.ts.map +1 -1
  16. package/dist/geometry.js +409 -134
  17. package/dist/geometry.js.map +1 -1
  18. package/dist/grass.d.ts.map +1 -1
  19. package/dist/grass.js +2 -1
  20. package/dist/grass.js.map +1 -1
  21. package/dist/imageResolve.js +2 -2
  22. package/dist/imageResolve.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/instancing.d.ts +100 -0
  28. package/dist/instancing.d.ts.map +1 -0
  29. package/dist/instancing.js +286 -0
  30. package/dist/instancing.js.map +1 -0
  31. package/dist/lensPass.d.ts +2 -0
  32. package/dist/lensPass.d.ts.map +1 -1
  33. package/dist/lensPass.js +7 -4
  34. package/dist/lensPass.js.map +1 -1
  35. package/dist/lights.d.ts.map +1 -1
  36. package/dist/lights.js +8 -5
  37. package/dist/lights.js.map +1 -1
  38. package/dist/memoryProbe.d.ts +76 -0
  39. package/dist/memoryProbe.d.ts.map +1 -0
  40. package/dist/memoryProbe.js +197 -0
  41. package/dist/memoryProbe.js.map +1 -0
  42. package/dist/oidnPass.d.ts +6 -0
  43. package/dist/oidnPass.d.ts.map +1 -1
  44. package/dist/oidnPass.js +18 -1
  45. package/dist/oidnPass.js.map +1 -1
  46. package/dist/pathTracePass.d.ts +15 -2
  47. package/dist/pathTracePass.d.ts.map +1 -1
  48. package/dist/pathTracePass.js +38 -16
  49. package/dist/pathTracePass.js.map +1 -1
  50. package/dist/renderer.d.ts +13 -0
  51. package/dist/renderer.d.ts.map +1 -1
  52. package/dist/renderer.js +248 -41
  53. package/dist/renderer.js.map +1 -1
  54. package/dist/sceneUpload.d.ts +26 -8
  55. package/dist/sceneUpload.d.ts.map +1 -1
  56. package/dist/sceneUpload.js +97 -27
  57. package/dist/sceneUpload.js.map +1 -1
  58. package/dist/shaderModule.d.ts.map +1 -1
  59. package/dist/shaderModule.js +2 -1
  60. package/dist/shaderModule.js.map +1 -1
  61. package/dist/skyBake.d.ts.map +1 -1
  62. package/dist/skyBake.js +5 -3
  63. package/dist/skyBake.js.map +1 -1
  64. package/dist/texturePack.d.ts +43 -3
  65. package/dist/texturePack.d.ts.map +1 -1
  66. package/dist/texturePack.js +209 -37
  67. package/dist/texturePack.js.map +1 -1
  68. package/dist/wgsl/bvh.wgsl.d.ts +5 -1
  69. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  70. package/dist/wgsl/bvh.wgsl.js +105 -15
  71. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  72. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  73. package/dist/wgsl/common.wgsl.js +92 -11
  74. package/dist/wgsl/common.wgsl.js.map +1 -1
  75. package/dist/wgsl/lens.wgsl.d.ts +1 -1
  76. package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
  77. package/dist/wgsl/lens.wgsl.js +14 -2
  78. package/dist/wgsl/lens.wgsl.js.map +1 -1
  79. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  80. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  81. package/dist/wgsl/surface.wgsl.js +24 -14
  82. package/dist/wgsl/surface.wgsl.js.map +1 -1
  83. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  84. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  85. package/dist/wgsl/texture.wgsl.js +11 -0
  86. package/dist/wgsl/texture.wgsl.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/accumulator.ts +1 -0
  89. package/src/denoise.ts +2 -2
  90. package/src/device.ts +12 -0
  91. package/src/frameLayout.ts +5 -0
  92. package/src/geometry.ts +527 -133
  93. package/src/grass.ts +2 -1
  94. package/src/imageResolve.ts +2 -2
  95. package/src/index.ts +1 -0
  96. package/src/instancing.ts +415 -0
  97. package/src/lensPass.ts +9 -4
  98. package/src/lights.ts +9 -5
  99. package/src/memoryProbe.ts +275 -0
  100. package/src/oidnPass.ts +21 -1
  101. package/src/pathTracePass.ts +42 -18
  102. package/src/renderer.ts +308 -47
  103. package/src/sceneUpload.ts +139 -36
  104. package/src/shaderModule.ts +2 -1
  105. package/src/skyBake.ts +5 -3
  106. package/src/texturePack.ts +274 -43
  107. package/src/wgsl/bvh.wgsl.ts +105 -15
  108. package/src/wgsl/common.wgsl.ts +92 -11
  109. package/src/wgsl/lens.wgsl.ts +14 -2
  110. package/src/wgsl/surface.wgsl.ts +24 -14
  111. 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 { packGeometry } from "./geometry.js";
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, textureEntry, writeTextures } from "./texturePack.js";
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 ??= requestRenderDevice({
638
- onLost: reason => {
639
- if (!this.disposed) this.fail(new Error(`GPU renderer: the device was lost (${reason}).`));
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
- this.pathTrace.setPrograms(assets.programs,
885
- this.current.subsurface.enabled && materials.some(material =>
886
- (material.type === "MeshStandardMaterial" || material.type === "MeshPhysicalMaterial")
887
- && subsurfaceOf(material) !== null),
888
- // Even turned off, a volume's boundary has to stay invisible, which takes the same code.
889
- materials.some(material => volumeOf(material) !== null),
890
- this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0),
891
- // Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
892
- this.current.globalIllumination.portals !== false && materials.some(material =>
893
- material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
894
- && !(Number(material.values.thickness) > 0)),
895
- materials.some(material => grassOf(material) !== null));
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): Promise<PackedTarget> {
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 plan = planTextures(scene, environment ? [environment.texture] : []);
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
- for (let i = 0; i < words.length; i++) extra.push(words[i]);
1167
+ const plan = planTextures(scene, environment ? [environment.texture] : [],
1168
+ singleChannelTextures(scene, assets.programs));
969
1169
 
970
- return at;
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
- const geometry = packGeometry(scene, {
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: plan.extraOffset + extra.length,
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
- append(geometry.extraWords);
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: plan.extraOffset + extra.length });
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" } }, plan.indexOf))
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, plan.extraOffset + extra.length);
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
- if (grass) append(grass.words);
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
- const textures = writeTextures(plan, new Uint32Array(extra));
1011
- const bvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
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, bvh, textures, lights, environment, cdf);
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],