@plasius/gpu-renderer 0.2.47 → 0.2.49

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.
@@ -1,3 +1,4 @@
1
+ import { createWavefrontProgressiveResources } from './wavefront-progressive.js';
1
2
  import {
2
3
  createGpuParallelismCounters,
3
4
  createGpuParallelismDiagnostics,
@@ -299,7 +300,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
299
300
  const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
300
301
  const frameConfigSlotCount = Math.max(
301
302
  1,
302
- tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
303
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise || config.progressive ? 1 : 0)
303
304
  );
304
305
  const configBuffer = createBuffer(
305
306
  device,
@@ -369,7 +370,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
369
370
  format: "rgba16float",
370
371
  usage:
371
372
  constants.texture.STORAGE_BINDING |
372
- constants.texture.TEXTURE_BINDING,
373
+ constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
373
374
  });
374
375
  const radianceView = radianceTexture.createView();
375
376
  const denoiseScratchTexture = device.createTexture({
@@ -563,10 +564,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
563
564
  outputView,
564
565
  sampler,
565
566
  });
567
+ const progressive = await createWavefrontProgressiveResources({device, constants, config, configBuffer, radianceTexture, radianceView, outputView});
566
568
  let activeFrameTelemetry = null;
567
569
  let frameTelemetryResources = null;
568
570
  const frameEncoder = createWavefrontFrameEncoder({
569
571
  getConfig: () => config,
572
+ progressive,
570
573
  getBindGroups: () => bindGroups,
571
574
  pipelines,
572
575
  counterBuffer,
@@ -1022,6 +1025,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
1022
1025
  }
1023
1026
 
1024
1027
  function updateSceneObjects(sceneObjects) {
1028
+ progressive?.reset();
1025
1029
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
1026
1030
  packedScene = nextPackedScene;
1027
1031
  const nextConfig = rebuildLiveConfig();
@@ -1034,6 +1038,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
1034
1038
  }
1035
1039
 
1036
1040
  function updateCamera(cameraOptions = {}) {
1041
+ progressive?.reset();
1037
1042
  activeCameraOptions = cameraOptions;
1038
1043
  config = rebuildLiveConfig();
1039
1044
  return config;
@@ -1042,6 +1047,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
1042
1047
  function getSnapshot() {
1043
1048
  return Object.freeze({
1044
1049
  frame,
1050
+ accumulatedSamplesPerPixel: progressive?.samples ?? 0,
1045
1051
  width: config.width,
1046
1052
  height: config.height,
1047
1053
  maxDepth: config.maxDepth,
@@ -1073,6 +1079,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
1073
1079
  }
1074
1080
 
1075
1081
  function destroy() {
1082
+ progressive?.destroy();
1076
1083
  frameTelemetryResources?.destroy?.();
1077
1084
  activeQueue.destroy?.();
1078
1085
  nextQueue.destroy?.();
@@ -1138,6 +1145,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
1138
1145
  readOutputProbe,
1139
1146
  updateSceneObjects,
1140
1147
  updateCamera,
1148
+ resetAccumulation: () => progressive?.reset(),
1141
1149
  getSnapshot,
1142
1150
  destroy,
1143
1151
  });
@@ -346,6 +346,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
346
346
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
347
347
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
348
348
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
349
+ const progressiveBytes = options.progressive === true ? width * height * 16 + 16 : 0;
349
350
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
350
351
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
351
352
  const triangleBytes = allocatedRecordBytes(triangleCapacity, TRIANGLE_RECORD_BYTES);
@@ -372,7 +373,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
372
373
  const outputConfigSlotCount = deferredPathResolve ? 0 : tileCount;
373
374
  const frameConfigSlotCount = Math.max(
374
375
  1,
375
- tileCount * samplesPerPixel + outputConfigSlotCount + (denoise ? 1 : 0)
376
+ tileCount * samplesPerPixel + outputConfigSlotCount + (denoise || options.progressive === true ? 1 : 0)
376
377
  );
377
378
  const configBufferStride = alignByteSize(
378
379
  CONFIG_BUFFER_BYTES,
@@ -384,6 +385,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
384
385
  1 + bvhSortStageCount + bvhBuildLevelCount
385
386
  ) * configBufferStride;
386
387
  const sceneStorageBufferBindingBytes = Math.max(
388
+ progressiveBytes,
387
389
  sceneObjectBytes,
388
390
  triangleBytes,
389
391
  bvhCombinedBytes,
@@ -397,6 +399,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
397
399
  queueBytes * 2 +
398
400
  hitBytes +
399
401
  accumulationBytes +
402
+ progressiveBytes +
400
403
  pathVertexBytes +
401
404
  sceneObjectBytes +
402
405
  triangleBytes +
@@ -416,6 +419,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
416
419
  queuePairBytes: queueBytes * 2,
417
420
  hitBytes,
418
421
  accumulationBytes,
422
+ progressiveBytes,
419
423
  pathVertexBytes,
420
424
  sceneObjectBytes,
421
425
  triangleBytes,
@@ -459,6 +463,7 @@ export function estimateWavefrontPathTracingMemoryForConfig(config, limits = {})
459
463
  bvhSortStageCount: config.bvhSortStages.length,
460
464
  bvhBuildLevelCount: config.bvhBuildLevels.length,
461
465
  denoise: config.denoise,
466
+ progressive: config.progressive,
462
467
  deferredPathResolve: config.deferredPathResolve,
463
468
  minUniformBufferOffsetAlignment:
464
469
  limits.minUniformBufferOffsetAlignment ??
@@ -587,10 +592,15 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
587
592
  );
588
593
  const deferredPathResolve = resolveDeferredPathResolve(options);
589
594
  const strictPhysicalLowSppLighting = resolveStrictPhysicalLowSppLighting(options);
590
- const transportExperiments = resolveTransportExperiments(options, strictPhysicalLowSppLighting);
595
+ // Progressive batches require distinct frame seeds, even when static-frame
596
+ // sample routing was requested by a consumer.
597
+ const transportExperiments = resolveTransportExperiments(options.progressive === true
598
+ ? { ...options, "renderer.transport.stableSampleRouting.enabled": false }
599
+ : options, strictPhysicalLowSppLighting);
591
600
 
592
601
  return Object.freeze({
593
602
  mode: rendererWavefrontComputeMode,
603
+ progressive: options.progressive === true,
594
604
  width,
595
605
  height,
596
606
  maxDepth,
@@ -641,6 +651,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
641
651
  height,
642
652
  tileSize,
643
653
  samplesPerPixel,
654
+ progressive: options.progressive === true,
644
655
  tilePixelCapacity,
645
656
  maxDepth,
646
657
  sceneObjectCapacity,
@@ -46,17 +46,19 @@ export function dispatchWavefrontFrame({
46
46
  frameEncoder.encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
47
47
  }
48
48
  }
49
- if (config.denoise) {
49
+ if (config.denoise || config.progressive) {
50
+ const totalSamples = renderedSamplesPerPixel + frameEncoder.getAccumulatedSamples();
50
51
  const denoiseConfigOffset = writeFrameConfig(
51
52
  { x: 0, y: 0, width: config.width, height: config.height },
52
- { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
53
+ { sampleIndex: 0, sampleWeight: 1 / totalSamples }
53
54
  );
54
- const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
55
+ if (config.progressive) frameEncoder.encodeProgressive(batch.reserve(1), denoiseConfigOffset, parallelism, renderedSamplesPerPixel);
56
+ const denoisePassCount = totalSamples < 4 ? 2 : 1;
55
57
  frameEncoder.encodeDenoise(
56
58
  batch.reserve(denoisePassCount),
57
59
  denoiseConfigOffset,
58
60
  parallelism,
59
- renderedSamplesPerPixel
61
+ totalSamples
60
62
  );
61
63
  }
62
64
  frameEncoder.encodePresent(batch.reserve(1));
@@ -76,7 +78,7 @@ export function dispatchWavefrontFrameAwaitingGpu({
76
78
  }) {
77
79
  const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
78
80
  const denoisePassCount = config.denoise ? (renderedSamplesPerPixel < 4 ? 2 : 1) : 0;
79
- const tailPassCount = denoisePassCount + 1;
81
+ const tailPassCount = denoisePassCount + 1 + (config.progressive ? 1 : 0);
80
82
  const sampleBatchSize = Math.max(
81
83
  1,
82
84
  Math.floor(
@@ -161,19 +163,21 @@ export function dispatchWavefrontFrameAwaitingGpu({
161
163
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
162
164
  startingSubmissionCount: submissionCount,
163
165
  });
164
- if (includeDenoise && config.denoise) {
166
+ if (includeDenoise && (config.denoise || config.progressive)) {
167
+ const totalSamples = renderedSamplesPerPixel + frameEncoder.getAccumulatedSamples();
165
168
  const denoiseConfigOffset = writeFrameConfigSlot(
166
169
  slot,
167
170
  { x: 0, y: 0, width: config.width, height: config.height },
168
171
  frameIndex,
169
- { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
172
+ { sampleIndex: 0, sampleWeight: 1 / totalSamples }
170
173
  );
171
174
  slot += 1;
175
+ if (config.progressive) frameEncoder.encodeProgressive(tail.reserve(1), denoiseConfigOffset, parallelism, renderedSamplesPerPixel);
172
176
  frameEncoder.encodeDenoise(
173
177
  tail.reserve(denoisePassCount),
174
178
  denoiseConfigOffset,
175
179
  parallelism,
176
- renderedSamplesPerPixel
180
+ totalSamples
177
181
  );
178
182
  }
179
183
  if (includePresent) {
@@ -14,6 +14,7 @@ function resolveGetter(valueOrGetter) {
14
14
 
15
15
  export function createWavefrontFrameEncoder({
16
16
  getConfig,
17
+ progressive,
17
18
  getBindGroups,
18
19
  pipelines,
19
20
  counterBuffer,
@@ -30,6 +31,12 @@ export function createWavefrontFrameEncoder({
30
31
  const resolveBindGroups = resolveGetter(getBindGroups);
31
32
 
32
33
  return Object.freeze({
34
+ getAccumulatedSamples() { return progressive?.samples ?? 0; },
35
+ encodeProgressive(encoder, configOffset, parallelism, samples) {
36
+ progressive.encode(encoder, configOffset, samples);
37
+ const config = resolveConfig();
38
+ recordDirectDispatch(parallelism, [Math.ceil(config.width / 8), Math.ceil(config.height / 8)], WORKGROUP_SIZE);
39
+ },
33
40
  encodeTileSample(encoder, tile, configOffset, parallelism) {
34
41
  const config = resolveConfig();
35
42
  const bindGroups = resolveBindGroups();
@@ -0,0 +1,25 @@
1
+ export const PROGRESSIVE_WGSL = `
2
+ @group(0) @binding(45) var<storage, read_write> progressiveHistory: array<vec4<f32>>;
3
+ @group(0) @binding(46) var<uniform> progressiveCounts: vec4<u32>;
4
+
5
+ @compute @workgroup_size(8, 8)
6
+ fn accumulateProgressiveRadiance(@builtin(global_invocation_id) id: vec3<u32>) {
7
+ if (id.x >= config.canvasWidth || id.y >= config.canvasHeight) { return; }
8
+ let pixel = vec2<i32>(id.xy);
9
+ let index = id.y * config.canvasWidth + id.x;
10
+ let batch = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
11
+ let previousCount = f32(progressiveCounts.x);
12
+ let batchCount = f32(progressiveCounts.y);
13
+ // Branch explicitly: discarded history may contain non-finite values after a reset.
14
+ var mean = batch;
15
+ if (progressiveCounts.x > 0u) {
16
+ let previous = progressiveHistory[index].xyz;
17
+ mean = previous + (batch - previous) * (batchCount / (previousCount + batchCount));
18
+ }
19
+ progressiveHistory[index] = vec4<f32>(mean, 1.0);
20
+ textureStore(denoisedRadianceImage, pixel, vec4<f32>(mean, 1.0));
21
+ if (config.denoise == 0u) {
22
+ textureStore(denoisedOutputImage, pixel, vec4<f32>(present_radiance(mean), 1.0));
23
+ }
24
+ }
25
+ `;
@@ -0,0 +1,46 @@
1
+ import { CONFIG_BUFFER_BYTES } from './wavefront-core.js';
2
+ import { createBuffer } from './wavefront-gpu-resources.js';
3
+ import { assertShaderModuleCompiles, createComputePipeline } from './wavefront-runtime-support.js';
4
+ import { WAVEFRONT_COMPUTE_WGSL } from './wavefront-shaders.js';
5
+ import { PROGRESSIVE_WGSL } from './wavefront-progressive-shader.js';
6
+
7
+ // Accumulate linear, unfiltered radiance. Presentation never feeds back into history.
8
+ export async function createWavefrontProgressiveResources({device, constants, config,
9
+ configBuffer, radianceTexture, radianceView, outputView}) {
10
+ if (!config.progressive) return null;
11
+ const layout = device.createBindGroupLayout({entries:[
12
+ {binding:5,visibility:constants.shader.COMPUTE,buffer:{type:'uniform',hasDynamicOffset:true,minBindingSize:CONFIG_BUFFER_BYTES}},
13
+ {binding:14,visibility:constants.shader.COMPUTE,texture:{sampleType:'float'}},
14
+ {binding:15,visibility:constants.shader.COMPUTE,storageTexture:{access:'write-only',format:'rgba16float'}},
15
+ {binding:18,visibility:constants.shader.COMPUTE,storageTexture:{access:'write-only',format:'rgba8unorm'}},
16
+ {binding:45,visibility:constants.shader.COMPUTE,buffer:{type:'storage'}},
17
+ {binding:46,visibility:constants.shader.COMPUTE,buffer:{type:'uniform'}},
18
+ ]});
19
+ const shader=device.createShaderModule({label:'plasius.wavefront.progressiveShader',code:WAVEFRONT_COMPUTE_WGSL+PROGRESSIVE_WGSL});
20
+ await assertShaderModuleCompiles(shader,'plasius.wavefront.progressiveShader');
21
+ const pipeline=await createComputePipeline(device,shader,device.createPipelineLayout({bindGroupLayouts:[layout]}),
22
+ 'accumulateProgressiveRadiance','plasius.wavefront.progressive');
23
+ const history=createBuffer(device,constants.buffer.STORAGE,config.width*config.height*16,'plasius.wavefront.progressiveHistory');
24
+ const counts=createBuffer(device,constants.buffer.UNIFORM|constants.buffer.COPY_DST,16,'plasius.wavefront.progressiveCounts');
25
+ const texture=device.createTexture({label:'plasius.wavefront.progressiveRadiance',size:{width:config.width,height:config.height},
26
+ format:'rgba16float',usage:constants.texture.STORAGE_BINDING|constants.texture.COPY_SRC});
27
+ const bindGroup=device.createBindGroup({layout,entries:[
28
+ {binding:5,resource:{buffer:configBuffer,size:CONFIG_BUFFER_BYTES}},
29
+ {binding:14,resource:radianceView},{binding:15,resource:texture.createView()},
30
+ {binding:18,resource:outputView},{binding:45,resource:{buffer:history}},{binding:46,resource:{buffer:counts}},
31
+ ]});
32
+ let samples=0;
33
+ return Object.freeze({
34
+ get samples(){return samples;},
35
+ reset(){samples=0;},
36
+ encode(encoder,offset,batchSamples){
37
+ device.queue.writeBuffer(counts,0,new Uint32Array([samples,batchSamples,0,0]));
38
+ const pass=encoder.beginComputePass({label:'plasius.wavefront.progressivePass'});
39
+ pass.setPipeline(pipeline);pass.setBindGroup(0,bindGroup,[offset]);
40
+ pass.dispatchWorkgroups(Math.ceil(config.width/8),Math.ceil(config.height/8));pass.end();
41
+ encoder.copyTextureToTexture({texture},{texture:radianceTexture},{width:config.width,height:config.height,depthOrArrayLayers:1});
42
+ samples+=batchSamples;
43
+ },
44
+ destroy(){history.destroy();counts.destroy();texture.destroy();},
45
+ });
46
+ }