@plasius/gpu-renderer 0.1.6 → 0.1.8

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/dist/index.js CHANGED
@@ -1,6 +1,641 @@
1
1
  // src/index.js
2
2
  var DEFAULT_CLEAR_COLOR = Object.freeze([0.07, 0.11, 0.18, 1]);
3
3
  var DEFAULT_CANVAS_SELECTOR = "canvas[data-plasius-gpu-renderer]";
4
+ var rendererDebugOwner = "renderer";
5
+ var rendererWorkerQueueClass = "render";
6
+ var defaultRendererWorkerProfile = "realtime";
7
+ var rendererRepresentationBands = Object.freeze([
8
+ "near",
9
+ "mid",
10
+ "far",
11
+ "horizon"
12
+ ]);
13
+ var rendererAccelerationStructureUpdateClasses = Object.freeze([
14
+ "static",
15
+ "rigid-dynamic",
16
+ "deforming",
17
+ "proxy"
18
+ ]);
19
+ var rendererRayTracingStageOrder = Object.freeze([
20
+ "primaryVisibility",
21
+ "shadowAssist",
22
+ "opaqueFoundation",
23
+ "rtDirectLighting",
24
+ "rtReflections",
25
+ "rtGlobalIllumination",
26
+ "denoiseTemporal",
27
+ "transparents",
28
+ "composition",
29
+ "present"
30
+ ]);
31
+ var rendererRayTracingStageDefinitions = Object.freeze(
32
+ rendererRayTracingStageOrder.map(
33
+ (key, index) => Object.freeze({
34
+ key,
35
+ order: index + 1,
36
+ required: true,
37
+ description: {
38
+ primaryVisibility: "Primary visibility and depth preparation.",
39
+ shadowAssist: "Shadow assist passes and regional shadow preparation.",
40
+ opaqueFoundation: "Main opaque foundation for shading and tracing inputs.",
41
+ rtDirectLighting: "Ray-traced direct lighting and premium shadows.",
42
+ rtReflections: "Ray-traced reflections for important surfaces.",
43
+ rtGlobalIllumination: "Selective ray-traced indirect lighting and GI.",
44
+ denoiseTemporal: "Required denoise and temporal accumulation stage.",
45
+ transparents: "Transparents, particles, and volumetrics composition.",
46
+ composition: "Final world composition and color resolve.",
47
+ present: "Presentation to the active surface."
48
+ }[key]
49
+ })
50
+ )
51
+ );
52
+ var rendererRepresentationBandPolicies = Object.freeze({
53
+ near: Object.freeze({
54
+ band: "near",
55
+ rasterMode: "full-live",
56
+ rtParticipation: "premium",
57
+ shadowSource: "ray-traced-primary",
58
+ temporalReuse: "balanced",
59
+ updateCadenceDivisor: 1
60
+ }),
61
+ mid: Object.freeze({
62
+ band: "mid",
63
+ rasterMode: "simplified-live",
64
+ rtParticipation: "selective",
65
+ shadowSource: "regional-raster-and-proxy",
66
+ temporalReuse: "aggressive",
67
+ updateCadenceDivisor: 2
68
+ }),
69
+ far: Object.freeze({
70
+ band: "far",
71
+ rasterMode: "proxy-or-cached",
72
+ rtParticipation: "proxy",
73
+ shadowSource: "merged-proxy-casters",
74
+ temporalReuse: "high",
75
+ updateCadenceDivisor: 8
76
+ }),
77
+ horizon: Object.freeze({
78
+ band: "horizon",
79
+ rasterMode: "horizon-shell",
80
+ rtParticipation: "disabled",
81
+ shadowSource: "baked-impression",
82
+ temporalReuse: "cached",
83
+ updateCadenceDivisor: 60
84
+ })
85
+ });
86
+ var rendererAccelerationStructurePolicies = Object.freeze(
87
+ rendererAccelerationStructureUpdateClasses.map(
88
+ (updateClass) => Object.freeze({
89
+ updateClass,
90
+ description: {
91
+ static: "Stable static world geometry with infrequent rebuilds.",
92
+ "rigid-dynamic": "Rigid transforms that can be refit or relinked without full deformation updates.",
93
+ deforming: "Skinned or vertex-deforming content treated as a managed RT cost center.",
94
+ proxy: "Low-cost RT proxy or distant representation updates."
95
+ }[updateClass]
96
+ })
97
+ )
98
+ );
99
+ function buildRendererWorkerBudgetLevels(jobType, queueClass, levels) {
100
+ return Object.freeze(
101
+ levels.map(
102
+ (level) => Object.freeze({
103
+ id: level.id,
104
+ estimatedCostMs: level.estimatedCostMs,
105
+ config: Object.freeze({
106
+ maxDispatchesPerFrame: level.config.maxDispatchesPerFrame,
107
+ maxJobsPerDispatch: level.config.maxJobsPerDispatch,
108
+ cadenceDivisor: level.config.cadenceDivisor,
109
+ workgroupScale: level.config.workgroupScale,
110
+ maxQueueDepth: level.config.maxQueueDepth,
111
+ metadata: Object.freeze({
112
+ owner: rendererDebugOwner,
113
+ queueClass,
114
+ jobType,
115
+ quality: level.id
116
+ })
117
+ })
118
+ })
119
+ )
120
+ );
121
+ }
122
+ var rendererWorkerProfileSpecs = {
123
+ realtime: {
124
+ description: "Frame-stage DAG for flat rendering with visibility, main encode, post-processing, and submit.",
125
+ suggestedAllocationIds: [
126
+ "renderer.surface.current",
127
+ "renderer.visibility.worklist",
128
+ "renderer.post-process.history"
129
+ ],
130
+ jobs: {
131
+ acquire: {
132
+ priority: 5,
133
+ dependencies: [],
134
+ domain: "resolution",
135
+ importance: "critical",
136
+ levels: [
137
+ {
138
+ id: "fixed",
139
+ estimatedCostMs: 0.2,
140
+ config: {
141
+ maxDispatchesPerFrame: 1,
142
+ maxJobsPerDispatch: 1,
143
+ cadenceDivisor: 1,
144
+ workgroupScale: 1,
145
+ maxQueueDepth: 1
146
+ }
147
+ }
148
+ ],
149
+ suggestedAllocationIds: ["renderer.surface.current"]
150
+ },
151
+ visibility: {
152
+ priority: 4,
153
+ dependencies: [],
154
+ domain: "geometry",
155
+ importance: "high",
156
+ levels: [
157
+ {
158
+ id: "low",
159
+ estimatedCostMs: 0.4,
160
+ config: {
161
+ maxDispatchesPerFrame: 1,
162
+ maxJobsPerDispatch: 128,
163
+ cadenceDivisor: 2,
164
+ workgroupScale: 0.5,
165
+ maxQueueDepth: 256
166
+ }
167
+ },
168
+ {
169
+ id: "medium",
170
+ estimatedCostMs: 0.8,
171
+ config: {
172
+ maxDispatchesPerFrame: 1,
173
+ maxJobsPerDispatch: 256,
174
+ cadenceDivisor: 1,
175
+ workgroupScale: 0.75,
176
+ maxQueueDepth: 384
177
+ }
178
+ },
179
+ {
180
+ id: "high",
181
+ estimatedCostMs: 1.2,
182
+ config: {
183
+ maxDispatchesPerFrame: 2,
184
+ maxJobsPerDispatch: 512,
185
+ cadenceDivisor: 1,
186
+ workgroupScale: 1,
187
+ maxQueueDepth: 512
188
+ }
189
+ }
190
+ ],
191
+ suggestedAllocationIds: ["renderer.visibility.worklist"]
192
+ },
193
+ mainEncode: {
194
+ priority: 4,
195
+ dependencies: ["acquire", "visibility"],
196
+ domain: "geometry",
197
+ importance: "critical",
198
+ levels: [
199
+ {
200
+ id: "low",
201
+ estimatedCostMs: 1.2,
202
+ config: {
203
+ maxDispatchesPerFrame: 1,
204
+ maxJobsPerDispatch: 128,
205
+ cadenceDivisor: 1,
206
+ workgroupScale: 0.6,
207
+ maxQueueDepth: 192
208
+ }
209
+ },
210
+ {
211
+ id: "medium",
212
+ estimatedCostMs: 2.1,
213
+ config: {
214
+ maxDispatchesPerFrame: 1,
215
+ maxJobsPerDispatch: 256,
216
+ cadenceDivisor: 1,
217
+ workgroupScale: 0.8,
218
+ maxQueueDepth: 256
219
+ }
220
+ },
221
+ {
222
+ id: "high",
223
+ estimatedCostMs: 3,
224
+ config: {
225
+ maxDispatchesPerFrame: 1,
226
+ maxJobsPerDispatch: 384,
227
+ cadenceDivisor: 1,
228
+ workgroupScale: 1,
229
+ maxQueueDepth: 384
230
+ }
231
+ }
232
+ ],
233
+ suggestedAllocationIds: ["renderer.surface.current"]
234
+ },
235
+ postProcess: {
236
+ priority: 3,
237
+ dependencies: ["mainEncode"],
238
+ domain: "post-processing",
239
+ importance: "high",
240
+ levels: [
241
+ {
242
+ id: "low",
243
+ estimatedCostMs: 0.5,
244
+ config: {
245
+ maxDispatchesPerFrame: 1,
246
+ maxJobsPerDispatch: 64,
247
+ cadenceDivisor: 2,
248
+ workgroupScale: 0.5,
249
+ maxQueueDepth: 96
250
+ }
251
+ },
252
+ {
253
+ id: "medium",
254
+ estimatedCostMs: 0.9,
255
+ config: {
256
+ maxDispatchesPerFrame: 1,
257
+ maxJobsPerDispatch: 128,
258
+ cadenceDivisor: 1,
259
+ workgroupScale: 0.75,
260
+ maxQueueDepth: 128
261
+ }
262
+ },
263
+ {
264
+ id: "high",
265
+ estimatedCostMs: 1.4,
266
+ config: {
267
+ maxDispatchesPerFrame: 2,
268
+ maxJobsPerDispatch: 192,
269
+ cadenceDivisor: 1,
270
+ workgroupScale: 1,
271
+ maxQueueDepth: 192
272
+ }
273
+ }
274
+ ],
275
+ suggestedAllocationIds: ["renderer.post-process.history"]
276
+ },
277
+ submit: {
278
+ priority: 2,
279
+ dependencies: ["postProcess"],
280
+ domain: "resolution",
281
+ importance: "critical",
282
+ levels: [
283
+ {
284
+ id: "fixed",
285
+ estimatedCostMs: 0.2,
286
+ config: {
287
+ maxDispatchesPerFrame: 1,
288
+ maxJobsPerDispatch: 1,
289
+ cadenceDivisor: 1,
290
+ workgroupScale: 1,
291
+ maxQueueDepth: 1
292
+ }
293
+ }
294
+ ],
295
+ suggestedAllocationIds: ["renderer.surface.current"]
296
+ }
297
+ }
298
+ },
299
+ xr: {
300
+ description: "Frame-stage DAG for XR rendering with late-latch coordination before main encode and submit.",
301
+ suggestedAllocationIds: [
302
+ "renderer.xr.surface.current",
303
+ "renderer.xr.visibility.worklist"
304
+ ],
305
+ jobs: {
306
+ acquire: {
307
+ priority: 5,
308
+ dependencies: [],
309
+ domain: "xr",
310
+ importance: "critical",
311
+ levels: [
312
+ {
313
+ id: "fixed",
314
+ estimatedCostMs: 0.2,
315
+ config: {
316
+ maxDispatchesPerFrame: 1,
317
+ maxJobsPerDispatch: 1,
318
+ cadenceDivisor: 1,
319
+ workgroupScale: 1,
320
+ maxQueueDepth: 1
321
+ }
322
+ }
323
+ ],
324
+ suggestedAllocationIds: ["renderer.xr.surface.current"]
325
+ },
326
+ visibility: {
327
+ priority: 4,
328
+ dependencies: [],
329
+ domain: "geometry",
330
+ importance: "high",
331
+ levels: [
332
+ {
333
+ id: "low",
334
+ estimatedCostMs: 0.5,
335
+ config: {
336
+ maxDispatchesPerFrame: 1,
337
+ maxJobsPerDispatch: 96,
338
+ cadenceDivisor: 2,
339
+ workgroupScale: 0.5,
340
+ maxQueueDepth: 192
341
+ }
342
+ },
343
+ {
344
+ id: "medium",
345
+ estimatedCostMs: 0.9,
346
+ config: {
347
+ maxDispatchesPerFrame: 1,
348
+ maxJobsPerDispatch: 192,
349
+ cadenceDivisor: 1,
350
+ workgroupScale: 0.75,
351
+ maxQueueDepth: 256
352
+ }
353
+ },
354
+ {
355
+ id: "high",
356
+ estimatedCostMs: 1.3,
357
+ config: {
358
+ maxDispatchesPerFrame: 2,
359
+ maxJobsPerDispatch: 320,
360
+ cadenceDivisor: 1,
361
+ workgroupScale: 1,
362
+ maxQueueDepth: 320
363
+ }
364
+ }
365
+ ],
366
+ suggestedAllocationIds: ["renderer.xr.visibility.worklist"]
367
+ },
368
+ lateLatch: {
369
+ priority: 5,
370
+ dependencies: ["acquire"],
371
+ domain: "xr",
372
+ importance: "critical",
373
+ levels: [
374
+ {
375
+ id: "fixed",
376
+ estimatedCostMs: 0.15,
377
+ config: {
378
+ maxDispatchesPerFrame: 1,
379
+ maxJobsPerDispatch: 1,
380
+ cadenceDivisor: 1,
381
+ workgroupScale: 1,
382
+ maxQueueDepth: 1
383
+ }
384
+ }
385
+ ],
386
+ suggestedAllocationIds: ["renderer.xr.surface.current"]
387
+ },
388
+ mainEncode: {
389
+ priority: 4,
390
+ dependencies: ["visibility", "lateLatch"],
391
+ domain: "xr",
392
+ importance: "critical",
393
+ levels: [
394
+ {
395
+ id: "low",
396
+ estimatedCostMs: 1.1,
397
+ config: {
398
+ maxDispatchesPerFrame: 1,
399
+ maxJobsPerDispatch: 96,
400
+ cadenceDivisor: 1,
401
+ workgroupScale: 0.6,
402
+ maxQueueDepth: 128
403
+ }
404
+ },
405
+ {
406
+ id: "medium",
407
+ estimatedCostMs: 1.8,
408
+ config: {
409
+ maxDispatchesPerFrame: 1,
410
+ maxJobsPerDispatch: 192,
411
+ cadenceDivisor: 1,
412
+ workgroupScale: 0.8,
413
+ maxQueueDepth: 192
414
+ }
415
+ },
416
+ {
417
+ id: "high",
418
+ estimatedCostMs: 2.6,
419
+ config: {
420
+ maxDispatchesPerFrame: 1,
421
+ maxJobsPerDispatch: 256,
422
+ cadenceDivisor: 1,
423
+ workgroupScale: 1,
424
+ maxQueueDepth: 256
425
+ }
426
+ }
427
+ ],
428
+ suggestedAllocationIds: ["renderer.xr.surface.current"]
429
+ },
430
+ submit: {
431
+ priority: 2,
432
+ dependencies: ["mainEncode"],
433
+ domain: "xr",
434
+ importance: "critical",
435
+ levels: [
436
+ {
437
+ id: "fixed",
438
+ estimatedCostMs: 0.2,
439
+ config: {
440
+ maxDispatchesPerFrame: 1,
441
+ maxJobsPerDispatch: 1,
442
+ cadenceDivisor: 1,
443
+ workgroupScale: 1,
444
+ maxQueueDepth: 1
445
+ }
446
+ }
447
+ ],
448
+ suggestedAllocationIds: ["renderer.xr.surface.current"]
449
+ }
450
+ }
451
+ }
452
+ };
453
+ function buildRendererInputBoundary(profile) {
454
+ return Object.freeze({
455
+ type: "stable-visual-snapshot",
456
+ owner: rendererDebugOwner,
457
+ profile,
458
+ authority: "visual",
459
+ source: "scene-preparation",
460
+ stable: true
461
+ });
462
+ }
463
+ function buildRendererRenderStages(profile) {
464
+ return Object.freeze(
465
+ rendererRayTracingStageDefinitions.map(
466
+ (stage) => Object.freeze({
467
+ ...stage,
468
+ profile,
469
+ workerJobKeys: profile === "xr" && stage.key === "primaryVisibility" ? Object.freeze(["lateLatch", "visibility"]) : stage.key === "present" ? Object.freeze(["submit"]) : stage.key === "denoiseTemporal" || stage.key === "transparents" || stage.key === "composition" ? Object.freeze(["postProcess"]) : stage.key === "primaryVisibility" ? Object.freeze(["visibility"]) : stage.key === "shadowAssist" || stage.key === "opaqueFoundation" || stage.key === "rtDirectLighting" || stage.key === "rtReflections" || stage.key === "rtGlobalIllumination" ? Object.freeze(["mainEncode"]) : Object.freeze(["mainEncode"])
470
+ })
471
+ )
472
+ );
473
+ }
474
+ function buildRendererRepresentationBands(profile) {
475
+ return Object.freeze(
476
+ rendererRepresentationBands.map(
477
+ (band) => Object.freeze({
478
+ ...rendererRepresentationBandPolicies[band],
479
+ profile
480
+ })
481
+ )
482
+ );
483
+ }
484
+ function buildRendererAccelerationStructureUpdates(profile) {
485
+ return Object.freeze(
486
+ rendererAccelerationStructurePolicies.map(
487
+ (policy) => Object.freeze({
488
+ ...policy,
489
+ profile
490
+ })
491
+ )
492
+ );
493
+ }
494
+ function assertRendererIdentifier(name, value) {
495
+ if (typeof value !== "string" || value.trim().length === 0) {
496
+ throw new Error(`${name} must be a non-empty string.`);
497
+ }
498
+ return value.trim();
499
+ }
500
+ function buildRendererWorkerProfile(name, spec) {
501
+ return Object.freeze({
502
+ name,
503
+ description: spec.description,
504
+ jobs: Object.freeze(Object.keys(spec.jobs))
505
+ });
506
+ }
507
+ function buildRendererWorkerManifestJob(profileName, jobName, spec) {
508
+ const label = `renderer.${profileName}.${jobName}`;
509
+ return Object.freeze({
510
+ key: jobName,
511
+ label,
512
+ worker: Object.freeze({
513
+ jobType: label,
514
+ queueClass: rendererWorkerQueueClass,
515
+ priority: spec.priority,
516
+ dependencies: Object.freeze(
517
+ spec.dependencies.map((dependency) => `renderer.${profileName}.${dependency}`)
518
+ ),
519
+ schedulerMode: "dag"
520
+ }),
521
+ performance: Object.freeze({
522
+ id: label,
523
+ jobType: label,
524
+ queueClass: rendererWorkerQueueClass,
525
+ domain: spec.domain,
526
+ authority: "visual",
527
+ importance: spec.importance,
528
+ levels: buildRendererWorkerBudgetLevels(
529
+ label,
530
+ rendererWorkerQueueClass,
531
+ spec.levels
532
+ )
533
+ }),
534
+ debug: Object.freeze({
535
+ owner: rendererDebugOwner,
536
+ queueClass: rendererWorkerQueueClass,
537
+ jobType: label,
538
+ tags: Object.freeze(["renderer", profileName, jobName, spec.domain]),
539
+ suggestedAllocationIds: Object.freeze([...spec.suggestedAllocationIds])
540
+ })
541
+ });
542
+ }
543
+ function buildRendererWorkerManifest(name, spec) {
544
+ return Object.freeze({
545
+ schemaVersion: 1,
546
+ owner: rendererDebugOwner,
547
+ profile: name,
548
+ description: spec.description,
549
+ queueClass: rendererWorkerQueueClass,
550
+ schedulerMode: "dag",
551
+ inputBoundary: buildRendererInputBoundary(name),
552
+ renderStages: buildRendererRenderStages(name),
553
+ representationBands: buildRendererRepresentationBands(name),
554
+ accelerationStructureUpdates: buildRendererAccelerationStructureUpdates(name),
555
+ suggestedAllocationIds: Object.freeze([...spec.suggestedAllocationIds]),
556
+ jobs: Object.freeze(
557
+ Object.entries(spec.jobs).map(
558
+ ([jobName, jobSpec]) => buildRendererWorkerManifestJob(name, jobName, jobSpec)
559
+ )
560
+ )
561
+ });
562
+ }
563
+ var rendererWorkerProfiles = Object.freeze(
564
+ Object.fromEntries(
565
+ Object.entries(rendererWorkerProfileSpecs).map(([name, spec]) => [
566
+ name,
567
+ buildRendererWorkerProfile(name, spec)
568
+ ])
569
+ )
570
+ );
571
+ var rendererWorkerProfileNames = Object.freeze(
572
+ Object.keys(rendererWorkerProfiles)
573
+ );
574
+ var rendererWorkerManifests = Object.freeze(
575
+ Object.fromEntries(
576
+ Object.entries(rendererWorkerProfileSpecs).map(([name, spec]) => [
577
+ name,
578
+ buildRendererWorkerManifest(name, spec)
579
+ ])
580
+ )
581
+ );
582
+ function getRendererWorkerProfile(name = defaultRendererWorkerProfile) {
583
+ const profile = rendererWorkerProfiles[name];
584
+ if (!profile) {
585
+ const available = rendererWorkerProfileNames.join(", ");
586
+ throw new Error(`Unknown renderer worker profile "${name}". Available: ${available}.`);
587
+ }
588
+ return profile;
589
+ }
590
+ function getRendererWorkerManifest(name = defaultRendererWorkerProfile) {
591
+ const manifest = rendererWorkerManifests[name];
592
+ if (!manifest) {
593
+ const available = rendererWorkerProfileNames.join(", ");
594
+ throw new Error(`Unknown renderer worker profile "${name}". Available: ${available}.`);
595
+ }
596
+ return manifest;
597
+ }
598
+ function createRayTracingRenderPlan(options = {}) {
599
+ const profile = options.profile ?? defaultRendererWorkerProfile;
600
+ const snapshotId = assertRendererIdentifier(
601
+ "snapshotId",
602
+ options.snapshotId
603
+ );
604
+ const workerManifest = getRendererWorkerManifest(profile);
605
+ const representations = Array.isArray(options.representations) ? Object.freeze(
606
+ options.representations.map((representation, index) => {
607
+ if (!representation || typeof representation !== "object") {
608
+ throw new Error(`representations[${index}] must be an object.`);
609
+ }
610
+ const band = assertRendererIdentifier(
611
+ `representations[${index}].band`,
612
+ representation.band
613
+ );
614
+ if (!rendererRepresentationBands.includes(band)) {
615
+ throw new Error(
616
+ `representations[${index}].band must be one of: ${rendererRepresentationBands.join(", ")}.`
617
+ );
618
+ }
619
+ return Object.freeze({
620
+ ...representation,
621
+ band
622
+ });
623
+ })
624
+ ) : workerManifest.representationBands;
625
+ return Object.freeze({
626
+ schemaVersion: 1,
627
+ owner: rendererDebugOwner,
628
+ profile,
629
+ inputBoundary: Object.freeze({
630
+ ...workerManifest.inputBoundary,
631
+ snapshotId
632
+ }),
633
+ renderStages: workerManifest.renderStages,
634
+ representationBands: representations,
635
+ accelerationStructureUpdates: workerManifest.accelerationStructureUpdates,
636
+ workerManifest
637
+ });
638
+ }
4
639
  function clamp01(value) {
5
640
  return Math.min(1, Math.max(0, value));
6
641
  }
@@ -36,12 +671,111 @@ function normalizeColor(value) {
36
671
  }
37
672
  return [...DEFAULT_CLEAR_COLOR];
38
673
  }
674
+ function readPositiveNumber(name, value) {
675
+ if (value === void 0) {
676
+ return void 0;
677
+ }
678
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
679
+ throw new Error(`${name} must be a finite number greater than zero.`);
680
+ }
681
+ return value;
682
+ }
39
683
  function now() {
40
684
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
41
685
  return performance.now();
42
686
  }
43
687
  return Date.now();
44
688
  }
689
+ function normalizeFrameId(value) {
690
+ if (typeof value !== "string" || value.trim().length === 0) {
691
+ throw new Error("frameIdFactory must return a non-empty string.");
692
+ }
693
+ return value.trim();
694
+ }
695
+ function resolveTargetFrameTimeMs(options, event) {
696
+ const {
697
+ targetFrameTimeMs: fixedTargetFrameTimeMs,
698
+ targetFrameRate,
699
+ getTargetFrameTimeMs
700
+ } = options;
701
+ if (typeof getTargetFrameTimeMs === "function") {
702
+ const resolved = getTargetFrameTimeMs(event);
703
+ return readPositiveNumber("getTargetFrameTimeMs()", resolved);
704
+ }
705
+ if (fixedTargetFrameTimeMs !== void 0) {
706
+ return fixedTargetFrameTimeMs;
707
+ }
708
+ if (targetFrameRate !== void 0) {
709
+ return 1e3 / targetFrameRate;
710
+ }
711
+ return void 0;
712
+ }
713
+ function createRendererDebugHooks(options = {}) {
714
+ const {
715
+ debugSession,
716
+ targetFrameTimeMs,
717
+ targetFrameRate,
718
+ getTargetFrameTimeMs,
719
+ onFrameStart,
720
+ onFrameComplete
721
+ } = options;
722
+ if (!debugSession || typeof debugSession.recordFrame !== "function") {
723
+ throw new Error(
724
+ "debugSession must expose recordFrame(sample). Use @plasius/gpu-debug createGpuDebugSession()."
725
+ );
726
+ }
727
+ const fixedTargetFrameTimeMs = readPositiveNumber(
728
+ "targetFrameTimeMs",
729
+ targetFrameTimeMs
730
+ );
731
+ const fixedTargetFrameRate = readPositiveNumber(
732
+ "targetFrameRate",
733
+ targetFrameRate
734
+ );
735
+ if (fixedTargetFrameTimeMs !== void 0 && fixedTargetFrameRate !== void 0) {
736
+ throw new Error(
737
+ "Provide either targetFrameTimeMs or targetFrameRate, not both."
738
+ );
739
+ }
740
+ if (getTargetFrameTimeMs !== void 0 && typeof getTargetFrameTimeMs !== "function") {
741
+ throw new Error("getTargetFrameTimeMs must be a function when provided.");
742
+ }
743
+ const resolvedOptions = {
744
+ targetFrameTimeMs: fixedTargetFrameTimeMs,
745
+ targetFrameRate: fixedTargetFrameRate,
746
+ getTargetFrameTimeMs
747
+ };
748
+ return {
749
+ onFrameStart(event) {
750
+ if (typeof onFrameStart === "function") {
751
+ onFrameStart({
752
+ ...event,
753
+ owner: rendererDebugOwner
754
+ });
755
+ }
756
+ },
757
+ onFrameComplete(event) {
758
+ const resolvedTargetFrameTimeMs = resolveTargetFrameTimeMs(
759
+ resolvedOptions,
760
+ event
761
+ );
762
+ if (typeof event.frameTimeMs === "number" && Number.isFinite(event.frameTimeMs) && event.frameTimeMs > 0) {
763
+ debugSession.recordFrame({
764
+ frameId: event.frameId,
765
+ frameTimeMs: event.frameTimeMs,
766
+ targetFrameTimeMs: resolvedTargetFrameTimeMs
767
+ });
768
+ }
769
+ if (typeof onFrameComplete === "function") {
770
+ onFrameComplete({
771
+ ...event,
772
+ owner: rendererDebugOwner,
773
+ targetFrameTimeMs: resolvedTargetFrameTimeMs
774
+ });
775
+ }
776
+ }
777
+ };
778
+ }
45
779
  function readNavigator(navigatorOverride) {
46
780
  const currentNavigator = navigatorOverride ?? globalThis.navigator;
47
781
  if (!currentNavigator || typeof currentNavigator !== "object") {
@@ -122,8 +856,11 @@ async function createGpuRenderer(options = {}) {
122
856
  clearColor = DEFAULT_CLEAR_COLOR,
123
857
  requestAnimationFrame = globalThis.requestAnimationFrame?.bind(globalThis),
124
858
  cancelAnimationFrame = globalThis.cancelAnimationFrame?.bind(globalThis),
859
+ frameIdFactory,
860
+ onFrameStart,
125
861
  onBeforeEncode,
126
- onAfterSubmit
862
+ onAfterSubmit,
863
+ onFrameComplete
127
864
  } = options;
128
865
  const gpu = readGpu(navigatorOverride);
129
866
  const adapter = await gpu.requestAdapter({ powerPreference });
@@ -150,6 +887,28 @@ async function createGpuRenderer(options = {}) {
150
887
  if (destroyed) {
151
888
  throw new Error("Renderer was destroyed.");
152
889
  }
890
+ const frameNumber = frame + 1;
891
+ const frameId = normalizeFrameId(
892
+ typeof frameIdFactory === "function" ? frameIdFactory({
893
+ frame: frameNumber,
894
+ timestamp,
895
+ canvas: targetCanvas,
896
+ xrActive
897
+ }) : `renderer.frame.${frameNumber}`
898
+ );
899
+ const frameTimeMs = lastTimestamp > 0 ? Math.max(0, timestamp - lastTimestamp) : void 0;
900
+ if (typeof onFrameStart === "function") {
901
+ onFrameStart({
902
+ frame: frameNumber,
903
+ frameId,
904
+ frameTimeMs,
905
+ timestamp,
906
+ device,
907
+ context,
908
+ canvas: targetCanvas,
909
+ xrActive
910
+ });
911
+ }
153
912
  const texture = context.getCurrentTexture?.();
154
913
  if (!texture || typeof texture.createView !== "function") {
155
914
  throw new Error("WebGPU context returned an invalid current texture.");
@@ -162,12 +921,16 @@ async function createGpuRenderer(options = {}) {
162
921
  if (typeof onBeforeEncode === "function") {
163
922
  onBeforeEncode({
164
923
  frame,
924
+ frameNumber,
925
+ frameId,
926
+ frameTimeMs,
165
927
  timestamp,
166
928
  device,
167
929
  context,
168
930
  encoder,
169
931
  pass,
170
- canvas: targetCanvas
932
+ canvas: targetCanvas,
933
+ xrActive
171
934
  });
172
935
  }
173
936
  if (typeof pass.end === "function") {
@@ -175,19 +938,37 @@ async function createGpuRenderer(options = {}) {
175
938
  }
176
939
  const commandBuffer = encoder.finish();
177
940
  device.queue.submit([commandBuffer]);
178
- frame += 1;
941
+ frame = frameNumber;
179
942
  lastTimestamp = timestamp;
180
943
  if (typeof onAfterSubmit === "function") {
181
944
  onAfterSubmit({
182
- frame,
945
+ frame: frameNumber,
946
+ frameNumber,
947
+ frameId,
948
+ frameTimeMs,
949
+ timestamp,
950
+ device,
951
+ context,
952
+ canvas: targetCanvas,
953
+ xrActive
954
+ });
955
+ }
956
+ if (typeof onFrameComplete === "function") {
957
+ onFrameComplete({
958
+ frame: frameNumber,
959
+ frameId,
960
+ frameTimeMs,
183
961
  timestamp,
184
962
  device,
185
963
  context,
186
- canvas: targetCanvas
964
+ canvas: targetCanvas,
965
+ xrActive
187
966
  });
188
967
  }
189
968
  return {
190
- frame,
969
+ frame: frameNumber,
970
+ frameId,
971
+ frameTimeMs,
191
972
  timestamp
192
973
  };
193
974
  };
@@ -328,7 +1109,20 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
328
1109
  export {
329
1110
  bindRendererToXrManager,
330
1111
  createGpuRenderer,
1112
+ createRayTracingRenderPlan,
1113
+ createRendererDebugHooks,
331
1114
  defaultRendererClearColor,
1115
+ defaultRendererWorkerProfile,
1116
+ getRendererWorkerManifest,
1117
+ getRendererWorkerProfile,
1118
+ rendererAccelerationStructureUpdateClasses,
1119
+ rendererDebugOwner,
1120
+ rendererRayTracingStageOrder,
1121
+ rendererRepresentationBands,
1122
+ rendererWorkerManifests,
1123
+ rendererWorkerProfileNames,
1124
+ rendererWorkerProfiles,
1125
+ rendererWorkerQueueClass,
332
1126
  supportsWebGpu
333
1127
  };
334
1128
  //# sourceMappingURL=index.js.map