@plasius/gpu-renderer 0.2.57 → 0.2.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-renderer",
3
- "version": "0.2.57",
3
+ "version": "0.2.58",
4
4
  "description": "Framework-agnostic WebGPU renderer runtime for Plasius projects.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/index.d.ts CHANGED
@@ -1050,8 +1050,10 @@ export interface WavefrontPathTracingMemoryEstimate {
1050
1050
  readonly pathVertexBytes: number;
1051
1051
  readonly sceneObjectBytes: number;
1052
1052
  readonly triangleBytes: number;
1053
- /** Instance lookup texture bytes; separate from totalHotBufferBytes. */
1053
+ /** Logical 144-byte records, separate from totalHotBufferBytes and before texture row padding. */
1054
1054
  readonly instanceBytes: number;
1055
+ /** Requested instance texture storage including row padding. */
1056
+ readonly instanceTextureBytes: number;
1055
1057
  readonly materialTableBytes: number;
1056
1058
  readonly bvhNodeBytes: number;
1057
1059
  readonly bvhCombinedBytes: number;
@@ -1417,6 +1419,8 @@ export function estimateWavefrontPathTracingMemory(options?: {
1417
1419
  maxDepth?: number;
1418
1420
  sceneObjectCapacity?: number;
1419
1421
  triangleCapacity?: number;
1422
+ instanceBytes?: number;
1423
+ instanceTextureBytes?: number;
1420
1424
  materialCapacity?: number;
1421
1425
  bvhNodeCapacity?: number;
1422
1426
  bvhLeafSortCapacity?: number;
@@ -1,4 +1,5 @@
1
1
  import { uploadWavefrontBuffer } from './wavefront-upload.js';
2
+ import { planInstanceTexture, uploadInstanceTexture } from './wavefront-instance-texture.js';
2
3
  import { createGpuAllocationTracker, openGpuAllocationErrorScopes } from './gpu-allocation-budget.js';
3
4
  import { createWavefrontProgressiveResources } from './wavefront-progressive.js';
4
5
  import {
@@ -242,12 +243,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
242
243
  const memory = config.memory;
243
244
  let instanceTexture = null, instanceView = null;
244
245
  if(config.instancing) {
245
- const count=config.instancing.instanceCount;
246
- instanceTexture=device.createTexture({label:"plasius.wavefront.instances",size:{width:9,height:count},
246
+ const layout=planInstanceTexture(config.instancing.instanceCount,device.limits?.maxTextureDimension2D);
247
+ instanceTexture=device.createTexture({label:"plasius.wavefront.instances",size:{width:layout.width,height:layout.height},
247
248
  format:"rgba32uint",usage:constants.texture.TEXTURE_BINDING|constants.texture.COPY_DST});
248
- const upload=new Uint8Array(count*256),source=new Uint8Array(config.instancing.buffer);
249
- for(let i=0;i<count;i++) upload.set(source.subarray(i*144,(i+1)*144),i*256);
250
- device.queue.writeTexture({texture:instanceTexture},upload,{bytesPerRow:256,rowsPerImage:count},{width:9,height:count,depthOrArrayLayers:1});
249
+ await uploadInstanceTexture(device,instanceTexture,config.instancing.buffer,layout);
251
250
  instanceView=instanceTexture.createView();
252
251
  }
253
252
  const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
@@ -1,4 +1,5 @@
1
- import { prepareWavefrontInstances, buildWavefrontInstances } from './wavefront-instances.js';
1
+ import { planInstanceTexture } from './wavefront-instance-texture.js';
2
+ import { WAVEFRONT_INSTANCE_BYTES, prepareWavefrontInstances, buildWavefrontInstances } from './wavefront-instances.js';
2
3
  import {
3
4
  ACCUMULATION_RECORD_BYTES,
4
5
  BVH_LEAF_REF_RECORD_BYTES,
@@ -359,6 +360,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
359
360
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
360
361
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
361
362
  const instanceBytes = readNonNegativeInteger("instanceBytes", options.instanceBytes, 0);
363
+ const instanceTextureBytes = readNonNegativeInteger("instanceTextureBytes", options.instanceTextureBytes, instanceBytes);
362
364
  const triangleBytes = allocatedRecordBytes(triangleCapacity, TRIANGLE_RECORD_BYTES);
363
365
  const materialTableBytes = 0;
364
366
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -434,6 +436,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
434
436
  sceneObjectBytes,
435
437
  triangleBytes,
436
438
  instanceBytes,
439
+ instanceTextureBytes,
437
440
  materialTableBytes,
438
441
  bvhNodeBytes,
439
442
  bvhCombinedBytes,
@@ -465,6 +468,7 @@ export function estimateWavefrontPathTracingMemoryForConfig(config, limits = {})
465
468
  sceneObjectCapacity: config.sceneObjectCapacity,
466
469
  triangleCapacity: config.triangleCapacity,
467
470
  instanceBytes: config.instancing?.instanceBytes ?? 0,
471
+ instanceTextureBytes: config.instancing ? planInstanceTexture(config.instancing.instanceCount, limits.maxTextureDimension2D).bytes : 0,
468
472
  bvhNodeCapacity: config.bvhNodeCapacity,
469
473
  bvhLeafSortCapacity: config.bvhLeafSortCapacity,
470
474
  emissiveTriangleCapacity: config.emissiveTriangleCapacity,
@@ -519,7 +523,9 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
519
523
  const meshSourceShape = estimateMeshSourceShape(meshes);
520
524
  if(options.memoryBudgetBytes !== undefined) {
521
525
  if(!Number.isSafeInteger(options.memoryBudgetBytes)||options.memoryBudgetBytes<=0) throw new Error("GPU memory budget must be a positive safe integer.");
522
- const geometryMinimumBytes = meshSourceShape.triangleCount * TRIANGLE_RECORD_BYTES + (options.instances?.length ?? 0) * 144;
526
+ const count = options.instances?.length ?? 0;
527
+ const instanceMinimumBytes = count * WAVEFRONT_INSTANCE_BYTES + Math.max(0, count * 2 - 1) * BVH_NODE_RECORD_BYTES;
528
+ const geometryMinimumBytes = meshSourceShape.triangleCount * TRIANGLE_RECORD_BYTES + instanceMinimumBytes;
523
529
  if(!Number.isSafeInteger(geometryMinimumBytes)||geometryMinimumBytes>options.memoryBudgetBytes) throw new Error("Unique geometry exceeds the GPU memory budget before acceleration construction.");
524
530
  }
525
531
  const instancePlan = options.instances === undefined ? null : prepareWavefrontInstances(meshes, options.instances);
@@ -9,17 +9,19 @@ struct MeshInstance {
9
9
  };
10
10
  @group(0) @binding(47) var meshInstances: texture_2d<u32>;
11
11
  fn read_mesh_instance(index: u32) -> MeshInstance {
12
- let y = i32(index);
13
- let metadata = textureLoad(meshInstances, vec2<i32>(8, y), 0);
12
+ let recordsPerRow = textureDimensions(meshInstances).x / 9u;
13
+ let x = i32((index % recordsPerRow) * 9u);
14
+ let y = i32(index / recordsPerRow);
15
+ let metadata = textureLoad(meshInstances, vec2<i32>(x + 8, y), 0);
14
16
  return MeshInstance(
15
- mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(0, y), 0)),
16
- bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(1, y), 0)),
17
- bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(2, y), 0)),
18
- bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(3, y), 0))),
19
- mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(4, y), 0)),
20
- bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(5, y), 0)),
21
- bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(6, y), 0)),
22
- bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(7, y), 0))),
17
+ mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x, y), 0)),
18
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 1, y), 0)),
19
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 2, y), 0)),
20
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 3, y), 0))),
21
+ mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 4, y), 0)),
22
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 5, y), 0)),
23
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 6, y), 0)),
24
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 7, y), 0))),
23
25
  metadata.x, metadata.y, bitcast<f32>(metadata.z), 0u);
24
26
  }
25
27
 
@@ -0,0 +1,43 @@
1
+ import { waitForSubmittedGpuWork } from './wavefront-gpu-synchronization.js';
2
+ import { WAVEFRONT_INSTANCE_BYTES } from './wavefront-instances.js';
3
+
4
+ const TEXELS_PER_RECORD = WAVEFRONT_INSTANCE_BYTES / 16;
5
+
6
+ /** Whole records never cross rows; limits come from the granted GPU device. */
7
+ export function planInstanceTexture(count, maxDimension = 8192) {
8
+ if (!Number.isSafeInteger(count) || count < 1 || count >= 0xffffffff) {
9
+ throw new Error('Instance count must fit unsigned shader addressing.');
10
+ }
11
+ if (!Number.isSafeInteger(maxDimension) || maxDimension < TEXELS_PER_RECORD) {
12
+ throw new Error('GPU instance texture dimension cannot hold one record.');
13
+ }
14
+ const recordsPerRow = Math.min(count, Math.floor(maxDimension / TEXELS_PER_RECORD));
15
+ const width = recordsPerRow * TEXELS_PER_RECORD, height = Math.ceil(count / recordsPerRow);
16
+ if (height > maxDimension) throw new Error('GPU instance texture dimension limit exceeded.');
17
+ const bytes = width * height * 16;
18
+ if (!Number.isSafeInteger(bytes)) throw new Error('Instance texture byte size is out of range.');
19
+ return Object.freeze({ count, recordsPerRow, width, height, bytes });
20
+ }
21
+
22
+ /** Queue writes have no 256-byte row-alignment requirement. Bound staging by rows. */
23
+ export async function uploadInstanceTexture(device, texture, buffer, layout, { chunkBytes = 8 * 1024 * 1024 } = {}) {
24
+ const bytesPerRow = layout.width * 16;
25
+ if (!Number.isSafeInteger(chunkBytes) || chunkBytes < bytesPerRow) {
26
+ throw new Error('Instance upload chunk must hold at least one texture row.');
27
+ }
28
+ if (buffer.byteLength !== layout.count * WAVEFRONT_INSTANCE_BYTES) {
29
+ throw new Error('Instance upload does not match its record count.');
30
+ }
31
+ const source = new Uint8Array(buffer), rowsPerChunk = Math.floor(chunkBytes / bytesPerRow);
32
+ for (let y = 0; y < layout.height; y += rowsPerChunk) {
33
+ const rows = Math.min(rowsPerChunk, layout.height - y), offset = y * bytesPerRow;
34
+ const length = rows * bytesPerRow;
35
+ let chunk = source.subarray(offset, offset + length);
36
+ if (chunk.byteLength < length) {
37
+ const padded = new Uint8Array(length); padded.set(chunk); chunk = padded;
38
+ }
39
+ device.queue.writeTexture({ texture, origin: { x: 0, y, z: 0 } }, chunk,
40
+ { bytesPerRow, rowsPerImage: rows }, { width: layout.width, height: rows, depthOrArrayLayers: 1 });
41
+ await waitForSubmittedGpuWork(device, { timeoutMs: 10000, maxWaitMs: 10000, allowTimeout: false });
42
+ }
43
+ }