@plasius/gpu-renderer 0.2.40 → 0.2.41

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.40",
3
+ "version": "0.2.41",
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
@@ -1011,6 +1011,8 @@ export interface CreateWavefrontPathTracingComputeRendererOptions {
1011
1011
  readonly navigator?: Navigator | { gpu?: GPU };
1012
1012
  readonly document?: Document;
1013
1013
  readonly deviceDescriptor?: GPUDeviceDescriptor;
1014
+ /** Opportunistically request WebGPU timestamp-query support when exposed. Defaults to true. */
1015
+ readonly gpuTimestamps?: boolean;
1014
1016
  readonly requiredLimits?: Partial<Record<string, number>>;
1015
1017
  readonly powerPreference?: GPUPowerPreference;
1016
1018
  readonly alpha?: boolean;
@@ -1122,6 +1124,31 @@ export interface WavefrontPathTracingComputeRenderer {
1122
1124
  destroy(): void;
1123
1125
  }
1124
1126
 
1127
+ export interface WavefrontRayCountTelemetry {
1128
+ readonly status: "not-requested" | "available" | "unavailable" | "failed";
1129
+ readonly source: "gpu-active-queue-readback" | null;
1130
+ readonly expectedPrimaryRays: number | null;
1131
+ readonly observedPrimaryRays: number | null;
1132
+ readonly secondaryRays: number | null;
1133
+ readonly totalPathSegments: number | null;
1134
+ readonly bounceHistogram: readonly number[];
1135
+ readonly capturedRayCounts: number;
1136
+ readonly expectedRayCounts: number;
1137
+ readonly reason: string | null;
1138
+ }
1139
+
1140
+ export interface WavefrontFrameTimingTelemetry {
1141
+ readonly status: "not-requested" | "available" | "fallback" | "unavailable" | "failed";
1142
+ readonly source: "timestamp-query" | "queue-completion" | "cpu-submit" | null;
1143
+ readonly timestampQueryStatus: "not-recorded" | "available" | "unsupported" | "failed";
1144
+ readonly totalGpuTimeMs: number | null;
1145
+ readonly totalRenderJobTimeMs: number;
1146
+ readonly classificationTimeMs: number | null;
1147
+ readonly compactionTimeMs: number | null;
1148
+ readonly samplingTimeMs: number | null;
1149
+ readonly reason: string | null;
1150
+ }
1151
+
1125
1152
  export interface WavefrontPathTracingComputeFrameStats {
1126
1153
  readonly frame: number;
1127
1154
  readonly width: number;
@@ -1136,6 +1163,11 @@ export interface WavefrontPathTracingComputeFrameStats {
1136
1163
  readonly maxFramePassesPerSubmission: number;
1137
1164
  readonly screenRays: number;
1138
1165
  readonly primaryRays: number;
1166
+ readonly secondaryRays: number | null;
1167
+ readonly totalPathSegments: number | null;
1168
+ readonly rayCounts: WavefrontRayCountTelemetry;
1169
+ readonly timings: WavefrontFrameTimingTelemetry;
1170
+ readonly telemetryMemoryBytes: number;
1139
1171
  readonly sceneObjectCount: number;
1140
1172
  readonly triangleCount: number;
1141
1173
  readonly emissiveTriangleCount: number;
@@ -1184,6 +1216,7 @@ export interface WavefrontPathTracingComputeFrameStats {
1184
1216
  invalidSamples: number;
1185
1217
  legacyClampEquivalentSamples: number;
1186
1218
  }>;
1219
+ rayCountStatus: WavefrontRayCountTelemetry["status"];
1187
1220
  memory: Readonly<{
1188
1221
  totalBytes: number;
1189
1222
  breakdown: WavefrontPathTracingMemoryEstimate | null;
@@ -1418,6 +1451,7 @@ export const rendererWavefrontComputeStatsStride: 8;
1418
1451
  export const wavefrontPathTracingComputeLimits: Readonly<{
1419
1452
  workgroupSize: 64;
1420
1453
  traceStorageBufferBindings: number;
1454
+ traceSampledTextureBindings: number;
1421
1455
  rayRecordBytes: 96;
1422
1456
  hitRecordBytes: 256;
1423
1457
  sceneObjectRecordBytes: 160;
@@ -50,6 +50,11 @@ import {
50
50
  createWavefrontFrameStats,
51
51
  resolveWavefrontRenderedSamplesPerPixel,
52
52
  } from "./wavefront-frame-stats.js";
53
+ import {
54
+ createWavefrontFrameTelemetryResources,
55
+ createWavefrontFrameTimingTelemetry,
56
+ createWavefrontRayCountTelemetry,
57
+ } from "./wavefront-frame-telemetry.js";
53
58
  import { createWavefrontFrameEncoder } from "./wavefront-frame-encoder.js";
54
59
  import {
55
60
  dispatchWavefrontFrame,
@@ -573,6 +578,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
573
578
  outputView,
574
579
  sampler,
575
580
  });
581
+ let activeFrameTelemetry = null;
582
+ let frameTelemetryResources = null;
576
583
  const frameEncoder = createWavefrontFrameEncoder({
577
584
  getConfig: () => config,
578
585
  getBindGroups: () => bindGroups,
@@ -585,6 +592,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
585
592
  presentPipeline,
586
593
  presentBindGroup,
587
594
  context,
595
+ getFrameTelemetry: () => activeFrameTelemetry,
588
596
  });
589
597
 
590
598
  let frame = 0;
@@ -600,6 +608,18 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
600
608
  const waitForSubmittedGpuWork = (waitOptions = {}) =>
601
609
  waitForSubmittedGpuWorkForDevice(device, waitOptions);
602
610
 
611
+ function ensureFrameTelemetryResources() {
612
+ if (!frameTelemetryResources) {
613
+ frameTelemetryResources = createWavefrontFrameTelemetryResources({
614
+ device,
615
+ constants,
616
+ maxRayCountRecords:
617
+ tiles.length * config.samplesPerPixel * config.maxDepth,
618
+ });
619
+ }
620
+ return frameTelemetryResources;
621
+ }
622
+
603
623
  function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
604
624
  return resolveWavefrontRenderedSamplesPerPixel({
605
625
  config,
@@ -735,6 +755,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
735
755
  frameTimeMs,
736
756
  false
737
757
  ),
758
+ timings: createWavefrontFrameTimingTelemetry({
759
+ status: "not-requested",
760
+ source: "cpu-submit",
761
+ timestampQueryStatus: "not-recorded",
762
+ totalRenderJobTimeMs: frameTimeMs,
763
+ }),
738
764
  });
739
765
  }
740
766
 
@@ -773,96 +799,160 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
773
799
  const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
774
800
  const useThrottledHighSamplePath =
775
801
  awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
802
+ const telemetryRequested = renderOptions.readStats === true;
803
+ const telemetryCanRecord = telemetryRequested && awaitGPUCompletion;
804
+ const telemetryResourcesForFrame = telemetryCanRecord
805
+ ? ensureFrameTelemetryResources()
806
+ : null;
807
+ if (telemetryResourcesForFrame?.available) {
808
+ telemetryResourcesForFrame.beginFrame();
809
+ activeFrameTelemetry = telemetryResourcesForFrame;
810
+ }
776
811
  const frameStartTimeMs = nowMs();
777
812
  let frameStats;
778
- if (useThrottledHighSamplePath) {
779
- frame += 1;
780
- const frameIndex = frame + config.frameIndex;
781
- const parallelismCounters = createGpuParallelismCounters();
782
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
783
- let frameSubmissionCount = 0;
784
- let frameConfigSlot = 0;
785
- if (accelerationBuildSubmitted) {
786
- const accelerationWaitOptions = {
787
- ...estimateSubmittedGpuWorkTiming(
788
- { ...config, renderedSamplesPerPixel: 1 },
789
- 1,
790
- renderOptions.submittedWorkTimeoutMs,
791
- { includeAccelerationBuild: true }
792
- ),
793
- allowTimeout: false,
794
- };
795
- await waitForSubmittedGpuWork(accelerationWaitOptions);
796
- }
797
- for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
798
- const tileRangeDispatch = dispatchFrameAwaitingGpu(
799
- frameIndex,
800
- parallelismCounters,
801
- samplingPlan.renderedSamplesPerPixel,
802
- {
803
- sampleRangeStart: 0,
804
- sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
805
- tileStartIndex: tileIndex,
806
- tileEndIndex: tileIndex + 1,
807
- startingSubmissionCount: frameSubmissionCount,
808
- startingSlot: frameConfigSlot,
809
- includeDenoise: tileIndex + 1 >= tiles.length,
810
- includePresent: tileIndex + 1 >= tiles.length,
811
- }
812
- );
813
- frameSubmissionCount = tileRangeDispatch.submissionCount;
814
- frameConfigSlot = tileRangeDispatch.slot;
815
- const tileWaitOptions = {
816
- ...estimateSubmittedGpuWorkTiming(
817
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
818
- 1,
819
- renderOptions.submittedWorkTimeoutMs,
813
+ try {
814
+ if (useThrottledHighSamplePath) {
815
+ frame += 1;
816
+ const frameIndex = frame + config.frameIndex;
817
+ const parallelismCounters = createGpuParallelismCounters();
818
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
819
+ let frameSubmissionCount = 0;
820
+ let frameConfigSlot = 0;
821
+ if (accelerationBuildSubmitted) {
822
+ const accelerationWaitOptions = {
823
+ ...estimateSubmittedGpuWorkTiming(
824
+ { ...config, renderedSamplesPerPixel: 1 },
825
+ 1,
826
+ renderOptions.submittedWorkTimeoutMs,
827
+ { includeAccelerationBuild: true }
828
+ ),
829
+ allowTimeout: false,
830
+ };
831
+ await waitForSubmittedGpuWork(accelerationWaitOptions);
832
+ }
833
+ for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
834
+ const tileRangeDispatch = dispatchFrameAwaitingGpu(
835
+ frameIndex,
836
+ parallelismCounters,
837
+ samplingPlan.renderedSamplesPerPixel,
820
838
  {
821
- includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
839
+ sampleRangeStart: 0,
840
+ sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
841
+ tileStartIndex: tileIndex,
842
+ tileEndIndex: tileIndex + 1,
843
+ startingSubmissionCount: frameSubmissionCount,
844
+ startingSlot: frameConfigSlot,
845
+ includeDenoise: tileIndex + 1 >= tiles.length,
822
846
  includePresent: tileIndex + 1 >= tiles.length,
823
847
  }
824
- ),
825
- allowTimeout: false,
826
- };
827
- await waitForSubmittedGpuWork(tileWaitOptions);
828
- }
829
- frameStats = createFrameStats({
830
- frameIndex,
831
- accelerationBuildSubmitted,
832
- frameSubmissionCount,
833
- parallelismCounters,
834
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
835
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
836
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
837
- budgetConstrained: samplingPlan.budgetConstrained,
838
- });
839
- } else {
840
- const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
841
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
842
- tiles.length,
843
- renderOptions.submittedWorkTimeoutMs,
844
- { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
845
- );
846
- const submissionWaitOptions = awaitGPUCompletion
847
- ? {
848
- timeoutMs: submittedWorkTiming.timeoutMs,
849
- maxWaitMs: submittedWorkTiming.maxWaitMs,
848
+ );
849
+ frameSubmissionCount = tileRangeDispatch.submissionCount;
850
+ frameConfigSlot = tileRangeDispatch.slot;
851
+ const tileWaitOptions = {
852
+ ...estimateSubmittedGpuWorkTiming(
853
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
854
+ 1,
855
+ renderOptions.submittedWorkTimeoutMs,
856
+ {
857
+ includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
858
+ includePresent: tileIndex + 1 >= tiles.length,
859
+ }
860
+ ),
850
861
  allowTimeout: false,
851
- }
852
- : {
853
- timeoutMs: submittedWorkTiming.timeoutMs,
854
- maxWaitMs: submittedWorkTiming.maxWaitMs,
855
862
  };
856
- frameStats = renderOnce(renderOptions, samplingPlan);
857
- if (awaitGPUCompletion) {
858
- await waitForSubmittedGpuWork(submissionWaitOptions);
863
+ await waitForSubmittedGpuWork(tileWaitOptions);
864
+ }
865
+ frameStats = createFrameStats({
866
+ frameIndex,
867
+ accelerationBuildSubmitted,
868
+ frameSubmissionCount,
869
+ parallelismCounters,
870
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
871
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
872
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
873
+ budgetConstrained: samplingPlan.budgetConstrained,
874
+ });
875
+ } else {
876
+ const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
877
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
878
+ tiles.length,
879
+ renderOptions.submittedWorkTimeoutMs,
880
+ { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
881
+ );
882
+ const submissionWaitOptions = awaitGPUCompletion
883
+ ? {
884
+ timeoutMs: submittedWorkTiming.timeoutMs,
885
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
886
+ allowTimeout: false,
887
+ }
888
+ : {
889
+ timeoutMs: submittedWorkTiming.timeoutMs,
890
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
891
+ };
892
+ frameStats = renderOnce(renderOptions, samplingPlan);
893
+ if (awaitGPUCompletion) {
894
+ await waitForSubmittedGpuWork(submissionWaitOptions);
895
+ }
859
896
  }
897
+ } finally {
898
+ activeFrameTelemetry = null;
860
899
  }
861
900
  const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
862
901
  if (awaitGPUCompletion) {
863
902
  lastCompletedFrameTimeMs = frameTimeMs;
864
903
  lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
865
904
  }
905
+ const expectedPrimaryRays =
906
+ config.width * config.height * samplingPlan.renderedSamplesPerPixel;
907
+ const expectedRayCounts =
908
+ tiles.length * samplingPlan.renderedSamplesPerPixel * config.maxDepth;
909
+ const telemetryReadback = telemetryCanRecord
910
+ ? await telemetryResourcesForFrame.readFrame({
911
+ expectedPrimaryRays,
912
+ expectedRayCounts,
913
+ waitForSubmittedGpuWork,
914
+ })
915
+ : null;
916
+ const rayCounts = !telemetryRequested
917
+ ? createWavefrontRayCountTelemetry()
918
+ : !awaitGPUCompletion
919
+ ? createWavefrontRayCountTelemetry({
920
+ status: "unavailable",
921
+ expectedPrimaryRays,
922
+ expectedRayCounts,
923
+ reason: "gpu-work-not-awaited",
924
+ })
925
+ : telemetryReadback.rayCounts;
926
+ const timings = !telemetryRequested
927
+ ? createWavefrontFrameTimingTelemetry({
928
+ status: "not-requested",
929
+ source: awaitGPUCompletion ? "queue-completion" : "cpu-submit",
930
+ timestampQueryStatus: "not-recorded",
931
+ totalRenderJobTimeMs: frameTimeMs,
932
+ })
933
+ : !awaitGPUCompletion
934
+ ? createWavefrontFrameTimingTelemetry({
935
+ status: "unavailable",
936
+ source: "cpu-submit",
937
+ timestampQueryStatus: "not-recorded",
938
+ totalRenderJobTimeMs: frameTimeMs,
939
+ reason: "gpu-work-not-awaited",
940
+ })
941
+ : telemetryReadback.timestampQueryStatus === "available"
942
+ ? createWavefrontFrameTimingTelemetry({
943
+ status: "available",
944
+ source: "timestamp-query",
945
+ timestampQueryStatus: "available",
946
+ totalGpuTimeMs: telemetryReadback.totalGpuTimeMs,
947
+ totalRenderJobTimeMs: frameTimeMs,
948
+ })
949
+ : createWavefrontFrameTimingTelemetry({
950
+ status: "fallback",
951
+ source: "queue-completion",
952
+ timestampQueryStatus: telemetryReadback.timestampQueryStatus,
953
+ totalRenderJobTimeMs: frameTimeMs,
954
+ reason: telemetryReadback.reason,
955
+ });
866
956
  const deviceLossStatus =
867
957
  awaitGPUCompletion
868
958
  ? "not-detected"
@@ -872,6 +962,13 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
872
962
  frameStats = Object.freeze({
873
963
  ...frameStats,
874
964
  deviceLossStatus,
965
+ secondaryRays:
966
+ rayCounts.status === "available" ? rayCounts.secondaryRays : null,
967
+ totalPathSegments:
968
+ rayCounts.status === "available" ? rayCounts.totalPathSegments : null,
969
+ rayCounts,
970
+ timings,
971
+ telemetryMemoryBytes: telemetryResourcesForFrame?.memoryBytes ?? 0,
875
972
  gpuWorkerJobs: createGpuWorkerJobDiagnostics(
876
973
  frameStats.gpuParallelism,
877
974
  frameStats.commandSubmissions,
@@ -991,6 +1088,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
991
1088
  }
992
1089
 
993
1090
  function destroy() {
1091
+ frameTelemetryResources?.destroy?.();
994
1092
  activeQueue.destroy?.();
995
1093
  nextQueue.destroy?.();
996
1094
  hitBuffer.destroy?.();
@@ -58,6 +58,7 @@ export const COUNTER_TRANSPORT_RESIDUAL_LUMINANCE_OFFSET = 22;
58
58
  export const COUNTER_TRANSPORT_ZERO_TERMINATION_OFFSET = 23;
59
59
  export const COUNTER_TRANSPORT_CHECKSUM_OFFSET = 24;
60
60
  export const TRACE_STORAGE_BUFFER_BINDINGS = 10;
61
+ export const TRACE_SAMPLED_TEXTURE_BINDINGS = 21;
61
62
  export const BRDF_LUT_UPLOAD_CACHE = new Map();
62
63
  export const HIT_TYPE_SURFACE = 0;
63
64
  export const HIT_TYPE_EMISSIVE = 1;
@@ -130,6 +131,7 @@ export const EMPTY_TERMINATION_METRICS = Object.freeze({
130
131
  export const wavefrontPathTracingComputeLimits = Object.freeze({
131
132
  workgroupSize: WORKGROUP_SIZE,
132
133
  traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
134
+ traceSampledTextureBindings: TRACE_SAMPLED_TEXTURE_BINDINGS,
133
135
  rayRecordBytes: RAY_RECORD_BYTES,
134
136
  hitRecordBytes: HIT_RECORD_BYTES,
135
137
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -24,6 +24,7 @@ export function createWavefrontFrameEncoder({
24
24
  presentPipeline,
25
25
  presentBindGroup,
26
26
  context,
27
+ getFrameTelemetry = () => null,
27
28
  }) {
28
29
  const resolveConfig = resolveGetter(getConfig);
29
30
  const resolveBindGroups = resolveGetter(getBindGroups);
@@ -32,9 +33,12 @@ export function createWavefrontFrameEncoder({
32
33
  encodeTileSample(encoder, tile, configOffset, parallelism) {
33
34
  const config = resolveConfig();
34
35
  const bindGroups = resolveBindGroups();
35
- const generatePass = encoder.beginComputePass({
36
- label: "plasius.wavefront.generatePrimaryRaysPass",
37
- });
36
+ const frameTelemetry = getFrameTelemetry();
37
+ const generatePass = encoder.beginComputePass(
38
+ frameTelemetry?.decorateFirstPass({
39
+ label: "plasius.wavefront.generatePrimaryRaysPass",
40
+ }) ?? { label: "plasius.wavefront.generatePrimaryRaysPass" }
41
+ );
38
42
  const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
39
43
 
40
44
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
@@ -44,6 +48,7 @@ export function createWavefrontFrameEncoder({
44
48
  generatePass.end();
45
49
 
46
50
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
51
+ frameTelemetry?.recordActiveRayCount(encoder, counterBuffer, bounceIndex);
47
52
  encoder.copyBufferToBuffer(
48
53
  counterBuffer,
49
54
  COUNTER_DISPATCH_ARGS_OFFSET,
@@ -125,7 +130,7 @@ export function createWavefrontFrameEncoder({
125
130
 
126
131
  encodePresent(encoder) {
127
132
  const texture = context.getCurrentTexture();
128
- const passEncoder = encoder.beginRenderPass({
133
+ const descriptor = {
129
134
  label: "plasius.wavefront.presentPass",
130
135
  colorAttachments: [
131
136
  {
@@ -135,7 +140,10 @@ export function createWavefrontFrameEncoder({
135
140
  storeOp: "store",
136
141
  },
137
142
  ],
138
- });
143
+ };
144
+ const passEncoder = encoder.beginRenderPass(
145
+ getFrameTelemetry()?.decorateFinalPass(descriptor) ?? descriptor
146
+ );
139
147
  passEncoder.setPipeline(presentPipeline);
140
148
  passEncoder.setBindGroup(0, presentBindGroup);
141
149
  passEncoder.draw(3);
@@ -184,6 +184,14 @@ export function createWavefrontTransportGuardrailSummary(frameStats, options = {
184
184
  0,
185
185
  Number(frameStats?.radianceDiagnostics?.legacyClampEquivalentSamples ?? 0)
186
186
  )
187
+ const rayCountStatus =
188
+ typeof frameStats?.rayCounts?.status === "string"
189
+ ? frameStats.rayCounts.status
190
+ : "not-requested"
191
+ const rayCountReason =
192
+ typeof frameStats?.rayCounts?.reason === "string"
193
+ ? frameStats.rayCounts.reason
194
+ : null
187
195
  const memory = summarizeWavefrontMemory(frameStats?.memory)
188
196
  const checks = Object.freeze([
189
197
  createTransportGuardrailCheck(
@@ -226,6 +234,21 @@ export function createWavefrontTransportGuardrailSummary(frameStats, options = {
226
234
  ? `${legacyClampEquivalentSamples} weighted sample(s) exceeded the legacy 4.0 preview clamp threshold; convergence stayed unbiased, but review firefly diagnostics before rollout.`
227
235
  : "No invalid radiance samples or legacy-clamp-equivalent fireflies were recorded."
228
236
  ),
237
+ createTransportGuardrailCheck(
238
+ "ray-count-telemetry",
239
+ rayCountStatus === "failed"
240
+ ? "fail"
241
+ : rayCountStatus === "unavailable"
242
+ ? "warn"
243
+ : "pass",
244
+ rayCountStatus === "failed"
245
+ ? `Ray-count telemetry failed closed: ${rayCountReason ?? "unknown failure"}.`
246
+ : rayCountStatus === "unavailable"
247
+ ? `Ray-count telemetry was requested but unavailable: ${rayCountReason ?? "unknown reason"}.`
248
+ : rayCountStatus === "available"
249
+ ? "Primary, secondary, and total path-segment counts were captured."
250
+ : "Ray-count telemetry was not requested for this frame."
251
+ ),
229
252
  ])
230
253
  const status = checks.some((check) => check.status === "fail")
231
254
  ? "fail"
@@ -249,6 +272,7 @@ export function createWavefrontTransportGuardrailSummary(frameStats, options = {
249
272
  invalidSamples,
250
273
  legacyClampEquivalentSamples,
251
274
  }),
275
+ rayCountStatus,
252
276
  memory,
253
277
  }),
254
278
  checks,
@@ -4,6 +4,10 @@ import {
4
4
  clamp,
5
5
  readPositiveInteger,
6
6
  } from "./wavefront-core.js";
7
+ import {
8
+ createWavefrontFrameTimingTelemetry,
9
+ createWavefrontRayCountTelemetry,
10
+ } from "./wavefront-frame-telemetry.js";
7
11
 
8
12
  export function resolveWavefrontRenderedSamplesPerPixel({
9
13
  config,
@@ -107,6 +111,11 @@ export function createWavefrontFrameStats({
107
111
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
108
112
  screenRays: config.width * config.height,
109
113
  primaryRays: config.width * config.height * renderedSamplesPerPixel,
114
+ secondaryRays: null,
115
+ totalPathSegments: null,
116
+ rayCounts: createWavefrontRayCountTelemetry(),
117
+ timings: createWavefrontFrameTimingTelemetry(),
118
+ telemetryMemoryBytes: 0,
110
119
  sceneObjectCount: config.sceneObjectCount,
111
120
  triangleCount: config.triangleCount,
112
121
  emissiveTriangleCount: config.emissiveTriangleCount,