@plasius/gpu-renderer 0.1.6 → 0.1.7

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/src/index.js CHANGED
@@ -1,5 +1,468 @@
1
1
  const DEFAULT_CLEAR_COLOR = Object.freeze([0.07, 0.11, 0.18, 1.0]);
2
2
  const DEFAULT_CANVAS_SELECTOR = "canvas[data-plasius-gpu-renderer]";
3
+ export const rendererDebugOwner = "renderer";
4
+ export const rendererWorkerQueueClass = "render";
5
+ export const defaultRendererWorkerProfile = "realtime";
6
+
7
+ function buildRendererWorkerBudgetLevels(jobType, queueClass, levels) {
8
+ return Object.freeze(
9
+ levels.map((level) =>
10
+ Object.freeze({
11
+ id: level.id,
12
+ estimatedCostMs: level.estimatedCostMs,
13
+ config: Object.freeze({
14
+ maxDispatchesPerFrame: level.config.maxDispatchesPerFrame,
15
+ maxJobsPerDispatch: level.config.maxJobsPerDispatch,
16
+ cadenceDivisor: level.config.cadenceDivisor,
17
+ workgroupScale: level.config.workgroupScale,
18
+ maxQueueDepth: level.config.maxQueueDepth,
19
+ metadata: Object.freeze({
20
+ owner: rendererDebugOwner,
21
+ queueClass,
22
+ jobType,
23
+ quality: level.id,
24
+ }),
25
+ }),
26
+ })
27
+ )
28
+ );
29
+ }
30
+
31
+ const rendererWorkerProfileSpecs = {
32
+ realtime: {
33
+ description:
34
+ "Frame-stage DAG for flat rendering with visibility, main encode, post-processing, and submit.",
35
+ suggestedAllocationIds: [
36
+ "renderer.surface.current",
37
+ "renderer.visibility.worklist",
38
+ "renderer.post-process.history",
39
+ ],
40
+ jobs: {
41
+ acquire: {
42
+ priority: 5,
43
+ dependencies: [],
44
+ domain: "resolution",
45
+ importance: "critical",
46
+ levels: [
47
+ {
48
+ id: "fixed",
49
+ estimatedCostMs: 0.2,
50
+ config: {
51
+ maxDispatchesPerFrame: 1,
52
+ maxJobsPerDispatch: 1,
53
+ cadenceDivisor: 1,
54
+ workgroupScale: 1,
55
+ maxQueueDepth: 1,
56
+ },
57
+ },
58
+ ],
59
+ suggestedAllocationIds: ["renderer.surface.current"],
60
+ },
61
+ visibility: {
62
+ priority: 4,
63
+ dependencies: [],
64
+ domain: "geometry",
65
+ importance: "high",
66
+ levels: [
67
+ {
68
+ id: "low",
69
+ estimatedCostMs: 0.4,
70
+ config: {
71
+ maxDispatchesPerFrame: 1,
72
+ maxJobsPerDispatch: 128,
73
+ cadenceDivisor: 2,
74
+ workgroupScale: 0.5,
75
+ maxQueueDepth: 256,
76
+ },
77
+ },
78
+ {
79
+ id: "medium",
80
+ estimatedCostMs: 0.8,
81
+ config: {
82
+ maxDispatchesPerFrame: 1,
83
+ maxJobsPerDispatch: 256,
84
+ cadenceDivisor: 1,
85
+ workgroupScale: 0.75,
86
+ maxQueueDepth: 384,
87
+ },
88
+ },
89
+ {
90
+ id: "high",
91
+ estimatedCostMs: 1.2,
92
+ config: {
93
+ maxDispatchesPerFrame: 2,
94
+ maxJobsPerDispatch: 512,
95
+ cadenceDivisor: 1,
96
+ workgroupScale: 1,
97
+ maxQueueDepth: 512,
98
+ },
99
+ },
100
+ ],
101
+ suggestedAllocationIds: ["renderer.visibility.worklist"],
102
+ },
103
+ mainEncode: {
104
+ priority: 4,
105
+ dependencies: ["acquire", "visibility"],
106
+ domain: "geometry",
107
+ importance: "critical",
108
+ levels: [
109
+ {
110
+ id: "low",
111
+ estimatedCostMs: 1.2,
112
+ config: {
113
+ maxDispatchesPerFrame: 1,
114
+ maxJobsPerDispatch: 128,
115
+ cadenceDivisor: 1,
116
+ workgroupScale: 0.6,
117
+ maxQueueDepth: 192,
118
+ },
119
+ },
120
+ {
121
+ id: "medium",
122
+ estimatedCostMs: 2.1,
123
+ config: {
124
+ maxDispatchesPerFrame: 1,
125
+ maxJobsPerDispatch: 256,
126
+ cadenceDivisor: 1,
127
+ workgroupScale: 0.8,
128
+ maxQueueDepth: 256,
129
+ },
130
+ },
131
+ {
132
+ id: "high",
133
+ estimatedCostMs: 3,
134
+ config: {
135
+ maxDispatchesPerFrame: 1,
136
+ maxJobsPerDispatch: 384,
137
+ cadenceDivisor: 1,
138
+ workgroupScale: 1,
139
+ maxQueueDepth: 384,
140
+ },
141
+ },
142
+ ],
143
+ suggestedAllocationIds: ["renderer.surface.current"],
144
+ },
145
+ postProcess: {
146
+ priority: 3,
147
+ dependencies: ["mainEncode"],
148
+ domain: "post-processing",
149
+ importance: "high",
150
+ levels: [
151
+ {
152
+ id: "low",
153
+ estimatedCostMs: 0.5,
154
+ config: {
155
+ maxDispatchesPerFrame: 1,
156
+ maxJobsPerDispatch: 64,
157
+ cadenceDivisor: 2,
158
+ workgroupScale: 0.5,
159
+ maxQueueDepth: 96,
160
+ },
161
+ },
162
+ {
163
+ id: "medium",
164
+ estimatedCostMs: 0.9,
165
+ config: {
166
+ maxDispatchesPerFrame: 1,
167
+ maxJobsPerDispatch: 128,
168
+ cadenceDivisor: 1,
169
+ workgroupScale: 0.75,
170
+ maxQueueDepth: 128,
171
+ },
172
+ },
173
+ {
174
+ id: "high",
175
+ estimatedCostMs: 1.4,
176
+ config: {
177
+ maxDispatchesPerFrame: 2,
178
+ maxJobsPerDispatch: 192,
179
+ cadenceDivisor: 1,
180
+ workgroupScale: 1,
181
+ maxQueueDepth: 192,
182
+ },
183
+ },
184
+ ],
185
+ suggestedAllocationIds: ["renderer.post-process.history"],
186
+ },
187
+ submit: {
188
+ priority: 2,
189
+ dependencies: ["postProcess"],
190
+ domain: "resolution",
191
+ importance: "critical",
192
+ levels: [
193
+ {
194
+ id: "fixed",
195
+ estimatedCostMs: 0.2,
196
+ config: {
197
+ maxDispatchesPerFrame: 1,
198
+ maxJobsPerDispatch: 1,
199
+ cadenceDivisor: 1,
200
+ workgroupScale: 1,
201
+ maxQueueDepth: 1,
202
+ },
203
+ },
204
+ ],
205
+ suggestedAllocationIds: ["renderer.surface.current"],
206
+ },
207
+ },
208
+ },
209
+ xr: {
210
+ description:
211
+ "Frame-stage DAG for XR rendering with late-latch coordination before main encode and submit.",
212
+ suggestedAllocationIds: [
213
+ "renderer.xr.surface.current",
214
+ "renderer.xr.visibility.worklist",
215
+ ],
216
+ jobs: {
217
+ acquire: {
218
+ priority: 5,
219
+ dependencies: [],
220
+ domain: "xr",
221
+ importance: "critical",
222
+ levels: [
223
+ {
224
+ id: "fixed",
225
+ estimatedCostMs: 0.2,
226
+ config: {
227
+ maxDispatchesPerFrame: 1,
228
+ maxJobsPerDispatch: 1,
229
+ cadenceDivisor: 1,
230
+ workgroupScale: 1,
231
+ maxQueueDepth: 1,
232
+ },
233
+ },
234
+ ],
235
+ suggestedAllocationIds: ["renderer.xr.surface.current"],
236
+ },
237
+ visibility: {
238
+ priority: 4,
239
+ dependencies: [],
240
+ domain: "geometry",
241
+ importance: "high",
242
+ levels: [
243
+ {
244
+ id: "low",
245
+ estimatedCostMs: 0.5,
246
+ config: {
247
+ maxDispatchesPerFrame: 1,
248
+ maxJobsPerDispatch: 96,
249
+ cadenceDivisor: 2,
250
+ workgroupScale: 0.5,
251
+ maxQueueDepth: 192,
252
+ },
253
+ },
254
+ {
255
+ id: "medium",
256
+ estimatedCostMs: 0.9,
257
+ config: {
258
+ maxDispatchesPerFrame: 1,
259
+ maxJobsPerDispatch: 192,
260
+ cadenceDivisor: 1,
261
+ workgroupScale: 0.75,
262
+ maxQueueDepth: 256,
263
+ },
264
+ },
265
+ {
266
+ id: "high",
267
+ estimatedCostMs: 1.3,
268
+ config: {
269
+ maxDispatchesPerFrame: 2,
270
+ maxJobsPerDispatch: 320,
271
+ cadenceDivisor: 1,
272
+ workgroupScale: 1,
273
+ maxQueueDepth: 320,
274
+ },
275
+ },
276
+ ],
277
+ suggestedAllocationIds: ["renderer.xr.visibility.worklist"],
278
+ },
279
+ lateLatch: {
280
+ priority: 5,
281
+ dependencies: ["acquire"],
282
+ domain: "xr",
283
+ importance: "critical",
284
+ levels: [
285
+ {
286
+ id: "fixed",
287
+ estimatedCostMs: 0.15,
288
+ config: {
289
+ maxDispatchesPerFrame: 1,
290
+ maxJobsPerDispatch: 1,
291
+ cadenceDivisor: 1,
292
+ workgroupScale: 1,
293
+ maxQueueDepth: 1,
294
+ },
295
+ },
296
+ ],
297
+ suggestedAllocationIds: ["renderer.xr.surface.current"],
298
+ },
299
+ mainEncode: {
300
+ priority: 4,
301
+ dependencies: ["visibility", "lateLatch"],
302
+ domain: "xr",
303
+ importance: "critical",
304
+ levels: [
305
+ {
306
+ id: "low",
307
+ estimatedCostMs: 1.1,
308
+ config: {
309
+ maxDispatchesPerFrame: 1,
310
+ maxJobsPerDispatch: 96,
311
+ cadenceDivisor: 1,
312
+ workgroupScale: 0.6,
313
+ maxQueueDepth: 128,
314
+ },
315
+ },
316
+ {
317
+ id: "medium",
318
+ estimatedCostMs: 1.8,
319
+ config: {
320
+ maxDispatchesPerFrame: 1,
321
+ maxJobsPerDispatch: 192,
322
+ cadenceDivisor: 1,
323
+ workgroupScale: 0.8,
324
+ maxQueueDepth: 192,
325
+ },
326
+ },
327
+ {
328
+ id: "high",
329
+ estimatedCostMs: 2.6,
330
+ config: {
331
+ maxDispatchesPerFrame: 1,
332
+ maxJobsPerDispatch: 256,
333
+ cadenceDivisor: 1,
334
+ workgroupScale: 1,
335
+ maxQueueDepth: 256,
336
+ },
337
+ },
338
+ ],
339
+ suggestedAllocationIds: ["renderer.xr.surface.current"],
340
+ },
341
+ submit: {
342
+ priority: 2,
343
+ dependencies: ["mainEncode"],
344
+ domain: "xr",
345
+ importance: "critical",
346
+ levels: [
347
+ {
348
+ id: "fixed",
349
+ estimatedCostMs: 0.2,
350
+ config: {
351
+ maxDispatchesPerFrame: 1,
352
+ maxJobsPerDispatch: 1,
353
+ cadenceDivisor: 1,
354
+ workgroupScale: 1,
355
+ maxQueueDepth: 1,
356
+ },
357
+ },
358
+ ],
359
+ suggestedAllocationIds: ["renderer.xr.surface.current"],
360
+ },
361
+ },
362
+ },
363
+ };
364
+
365
+ function buildRendererWorkerProfile(name, spec) {
366
+ return Object.freeze({
367
+ name,
368
+ description: spec.description,
369
+ jobs: Object.freeze(Object.keys(spec.jobs)),
370
+ });
371
+ }
372
+
373
+ function buildRendererWorkerManifestJob(profileName, jobName, spec) {
374
+ const label = `renderer.${profileName}.${jobName}`;
375
+ return Object.freeze({
376
+ key: jobName,
377
+ label,
378
+ worker: Object.freeze({
379
+ jobType: label,
380
+ queueClass: rendererWorkerQueueClass,
381
+ priority: spec.priority,
382
+ dependencies: Object.freeze(
383
+ spec.dependencies.map((dependency) => `renderer.${profileName}.${dependency}`)
384
+ ),
385
+ schedulerMode: "dag",
386
+ }),
387
+ performance: Object.freeze({
388
+ id: label,
389
+ jobType: label,
390
+ queueClass: rendererWorkerQueueClass,
391
+ domain: spec.domain,
392
+ authority: "visual",
393
+ importance: spec.importance,
394
+ levels: buildRendererWorkerBudgetLevels(
395
+ label,
396
+ rendererWorkerQueueClass,
397
+ spec.levels
398
+ ),
399
+ }),
400
+ debug: Object.freeze({
401
+ owner: rendererDebugOwner,
402
+ queueClass: rendererWorkerQueueClass,
403
+ jobType: label,
404
+ tags: Object.freeze(["renderer", profileName, jobName, spec.domain]),
405
+ suggestedAllocationIds: Object.freeze([...spec.suggestedAllocationIds]),
406
+ }),
407
+ });
408
+ }
409
+
410
+ function buildRendererWorkerManifest(name, spec) {
411
+ return Object.freeze({
412
+ schemaVersion: 1,
413
+ owner: rendererDebugOwner,
414
+ profile: name,
415
+ description: spec.description,
416
+ queueClass: rendererWorkerQueueClass,
417
+ schedulerMode: "dag",
418
+ suggestedAllocationIds: Object.freeze([...spec.suggestedAllocationIds]),
419
+ jobs: Object.freeze(
420
+ Object.entries(spec.jobs).map(([jobName, jobSpec]) =>
421
+ buildRendererWorkerManifestJob(name, jobName, jobSpec)
422
+ )
423
+ ),
424
+ });
425
+ }
426
+
427
+ export const rendererWorkerProfiles = Object.freeze(
428
+ Object.fromEntries(
429
+ Object.entries(rendererWorkerProfileSpecs).map(([name, spec]) => [
430
+ name,
431
+ buildRendererWorkerProfile(name, spec),
432
+ ])
433
+ )
434
+ );
435
+
436
+ export const rendererWorkerProfileNames = Object.freeze(
437
+ Object.keys(rendererWorkerProfiles)
438
+ );
439
+
440
+ export const rendererWorkerManifests = Object.freeze(
441
+ Object.fromEntries(
442
+ Object.entries(rendererWorkerProfileSpecs).map(([name, spec]) => [
443
+ name,
444
+ buildRendererWorkerManifest(name, spec),
445
+ ])
446
+ )
447
+ );
448
+
449
+ export function getRendererWorkerProfile(name = defaultRendererWorkerProfile) {
450
+ const profile = rendererWorkerProfiles[name];
451
+ if (!profile) {
452
+ const available = rendererWorkerProfileNames.join(", ");
453
+ throw new Error(`Unknown renderer worker profile "${name}". Available: ${available}.`);
454
+ }
455
+ return profile;
456
+ }
457
+
458
+ export function getRendererWorkerManifest(name = defaultRendererWorkerProfile) {
459
+ const manifest = rendererWorkerManifests[name];
460
+ if (!manifest) {
461
+ const available = rendererWorkerProfileNames.join(", ");
462
+ throw new Error(`Unknown renderer worker profile "${name}". Available: ${available}.`);
463
+ }
464
+ return manifest;
465
+ }
3
466
 
4
467
  function clamp01(value) {
5
468
  return Math.min(1, Math.max(0, value));
@@ -41,6 +504,16 @@ function normalizeColor(value) {
41
504
  return [...DEFAULT_CLEAR_COLOR];
42
505
  }
43
506
 
507
+ function readPositiveNumber(name, value) {
508
+ if (value === undefined) {
509
+ return undefined;
510
+ }
511
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
512
+ throw new Error(`${name} must be a finite number greater than zero.`);
513
+ }
514
+ return value;
515
+ }
516
+
44
517
  function now() {
45
518
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
46
519
  return performance.now();
@@ -48,6 +521,121 @@ function now() {
48
521
  return Date.now();
49
522
  }
50
523
 
524
+ function normalizeFrameId(value) {
525
+ if (typeof value !== "string" || value.trim().length === 0) {
526
+ throw new Error("frameIdFactory must return a non-empty string.");
527
+ }
528
+ return value.trim();
529
+ }
530
+
531
+ function resolveTargetFrameTimeMs(options, event) {
532
+ const {
533
+ targetFrameTimeMs: fixedTargetFrameTimeMs,
534
+ targetFrameRate,
535
+ getTargetFrameTimeMs,
536
+ } = options;
537
+
538
+ if (typeof getTargetFrameTimeMs === "function") {
539
+ const resolved = getTargetFrameTimeMs(event);
540
+ return readPositiveNumber("getTargetFrameTimeMs()", resolved);
541
+ }
542
+
543
+ if (fixedTargetFrameTimeMs !== undefined) {
544
+ return fixedTargetFrameTimeMs;
545
+ }
546
+
547
+ if (targetFrameRate !== undefined) {
548
+ return 1000 / targetFrameRate;
549
+ }
550
+
551
+ return undefined;
552
+ }
553
+
554
+ export function createRendererDebugHooks(options = {}) {
555
+ const {
556
+ debugSession,
557
+ targetFrameTimeMs,
558
+ targetFrameRate,
559
+ getTargetFrameTimeMs,
560
+ onFrameStart,
561
+ onFrameComplete,
562
+ } = options;
563
+
564
+ if (!debugSession || typeof debugSession.recordFrame !== "function") {
565
+ throw new Error(
566
+ "debugSession must expose recordFrame(sample). Use @plasius/gpu-debug createGpuDebugSession()."
567
+ );
568
+ }
569
+
570
+ const fixedTargetFrameTimeMs = readPositiveNumber(
571
+ "targetFrameTimeMs",
572
+ targetFrameTimeMs
573
+ );
574
+ const fixedTargetFrameRate = readPositiveNumber(
575
+ "targetFrameRate",
576
+ targetFrameRate
577
+ );
578
+
579
+ if (
580
+ fixedTargetFrameTimeMs !== undefined &&
581
+ fixedTargetFrameRate !== undefined
582
+ ) {
583
+ throw new Error(
584
+ "Provide either targetFrameTimeMs or targetFrameRate, not both."
585
+ );
586
+ }
587
+
588
+ if (
589
+ getTargetFrameTimeMs !== undefined &&
590
+ typeof getTargetFrameTimeMs !== "function"
591
+ ) {
592
+ throw new Error("getTargetFrameTimeMs must be a function when provided.");
593
+ }
594
+
595
+ const resolvedOptions = {
596
+ targetFrameTimeMs: fixedTargetFrameTimeMs,
597
+ targetFrameRate: fixedTargetFrameRate,
598
+ getTargetFrameTimeMs,
599
+ };
600
+
601
+ return {
602
+ onFrameStart(event) {
603
+ if (typeof onFrameStart === "function") {
604
+ onFrameStart({
605
+ ...event,
606
+ owner: rendererDebugOwner,
607
+ });
608
+ }
609
+ },
610
+ onFrameComplete(event) {
611
+ const resolvedTargetFrameTimeMs = resolveTargetFrameTimeMs(
612
+ resolvedOptions,
613
+ event
614
+ );
615
+
616
+ if (
617
+ typeof event.frameTimeMs === "number" &&
618
+ Number.isFinite(event.frameTimeMs) &&
619
+ event.frameTimeMs > 0
620
+ ) {
621
+ debugSession.recordFrame({
622
+ frameId: event.frameId,
623
+ frameTimeMs: event.frameTimeMs,
624
+ targetFrameTimeMs: resolvedTargetFrameTimeMs,
625
+ });
626
+ }
627
+
628
+ if (typeof onFrameComplete === "function") {
629
+ onFrameComplete({
630
+ ...event,
631
+ owner: rendererDebugOwner,
632
+ targetFrameTimeMs: resolvedTargetFrameTimeMs,
633
+ });
634
+ }
635
+ },
636
+ };
637
+ }
638
+
51
639
  function readNavigator(navigatorOverride) {
52
640
  const currentNavigator = navigatorOverride ?? globalThis.navigator;
53
641
  if (!currentNavigator || typeof currentNavigator !== "object") {
@@ -139,8 +727,11 @@ export async function createGpuRenderer(options = {}) {
139
727
  clearColor = DEFAULT_CLEAR_COLOR,
140
728
  requestAnimationFrame = globalThis.requestAnimationFrame?.bind(globalThis),
141
729
  cancelAnimationFrame = globalThis.cancelAnimationFrame?.bind(globalThis),
730
+ frameIdFactory,
731
+ onFrameStart,
142
732
  onBeforeEncode,
143
733
  onAfterSubmit,
734
+ onFrameComplete,
144
735
  } = options;
145
736
 
146
737
  const gpu = readGpu(navigatorOverride);
@@ -178,6 +769,33 @@ export async function createGpuRenderer(options = {}) {
178
769
  throw new Error("Renderer was destroyed.");
179
770
  }
180
771
 
772
+ const frameNumber = frame + 1;
773
+ const frameId = normalizeFrameId(
774
+ typeof frameIdFactory === "function"
775
+ ? frameIdFactory({
776
+ frame: frameNumber,
777
+ timestamp,
778
+ canvas: targetCanvas,
779
+ xrActive,
780
+ })
781
+ : `renderer.frame.${frameNumber}`
782
+ );
783
+ const frameTimeMs =
784
+ lastTimestamp > 0 ? Math.max(0, timestamp - lastTimestamp) : undefined;
785
+
786
+ if (typeof onFrameStart === "function") {
787
+ onFrameStart({
788
+ frame: frameNumber,
789
+ frameId,
790
+ frameTimeMs,
791
+ timestamp,
792
+ device,
793
+ context,
794
+ canvas: targetCanvas,
795
+ xrActive,
796
+ });
797
+ }
798
+
181
799
  const texture = context.getCurrentTexture?.();
182
800
  if (!texture || typeof texture.createView !== "function") {
183
801
  throw new Error("WebGPU context returned an invalid current texture.");
@@ -193,12 +811,16 @@ export async function createGpuRenderer(options = {}) {
193
811
  if (typeof onBeforeEncode === "function") {
194
812
  onBeforeEncode({
195
813
  frame,
814
+ frameNumber,
815
+ frameId,
816
+ frameTimeMs,
196
817
  timestamp,
197
818
  device,
198
819
  context,
199
820
  encoder,
200
821
  pass,
201
822
  canvas: targetCanvas,
823
+ xrActive,
202
824
  });
203
825
  }
204
826
 
@@ -209,21 +831,40 @@ export async function createGpuRenderer(options = {}) {
209
831
  const commandBuffer = encoder.finish();
210
832
  device.queue.submit([commandBuffer]);
211
833
 
212
- frame += 1;
834
+ frame = frameNumber;
213
835
  lastTimestamp = timestamp;
214
836
 
215
837
  if (typeof onAfterSubmit === "function") {
216
838
  onAfterSubmit({
217
- frame,
839
+ frame: frameNumber,
840
+ frameNumber,
841
+ frameId,
842
+ frameTimeMs,
843
+ timestamp,
844
+ device,
845
+ context,
846
+ canvas: targetCanvas,
847
+ xrActive,
848
+ });
849
+ }
850
+
851
+ if (typeof onFrameComplete === "function") {
852
+ onFrameComplete({
853
+ frame: frameNumber,
854
+ frameId,
855
+ frameTimeMs,
218
856
  timestamp,
219
857
  device,
220
858
  context,
221
859
  canvas: targetCanvas,
860
+ xrActive,
222
861
  });
223
862
  }
224
863
 
225
864
  return {
226
- frame,
865
+ frame: frameNumber,
866
+ frameId,
867
+ frameTimeMs,
227
868
  timestamp,
228
869
  };
229
870
  };