@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/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 { packGeometry } from "./geometry.js";
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, textureEntry, writeTextures } from "./texturePack.js";
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 ??= requestRenderDevice({
440
- onLost: reason => {
441
- if (!this.disposed)
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
- this.pathTrace.setPrograms(assets.programs, this.current.subsurface.enabled && materials.some(material => (material.type === "MeshStandardMaterial" || material.type === "MeshPhysicalMaterial")
633
- && subsurfaceOf(material) !== null),
634
- // Even turned off, a volume's boundary has to stay invisible, which takes the same code.
635
- materials.some(material => volumeOf(material) !== null), this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0),
636
- // Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
637
- this.current.globalIllumination.portals !== false && materials.some(material => material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
638
- && !(Number(material.values.thickness) > 0)), materials.some(material => grassOf(material) !== null));
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
- const packed = await this.packScene(gpu, revision, planar?.scene ?? assets.scene, assets, matrices, width, height, environment);
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 plan = planTextures(scene, environment ? [environment.texture] : []);
684
- const extra = [];
685
- const append = (words) => {
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 geometry = packGeometry(scene, {
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: plan.extraOffset + extra.length,
865
+ extraBase: extra.next,
703
866
  fog: scene.fog,
704
- });
705
- append(geometry.extraWords);
706
- const lights = packLights(geometry, scene.lights, { power: environmentPower(environment) }, { textures: plan.indexOf, extraBase: plan.extraOffset + extra.length });
707
- append(lights.extraWords);
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, plan.extraOffset + extra.length);
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 textures = writeTextures(plan, new Uint32Array(extra));
718
- const bvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
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, bvh, textures, lights, environment, cdf);
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
- target = await this.packScene(gpu, revision, capture.scene === "scene" ? planar.captureScene : empty, assets, cameraMatrices(capture.camera, width, height), width, height, environment);
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],