@plasius/gpu-renderer 0.2.43 → 0.2.44

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.43",
3
+ "version": "0.2.44",
4
4
  "description": "Framework-agnostic WebGPU renderer runtime for Plasius projects.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  "scripts": {
28
28
  "build": "tsup",
29
29
  "demo": "python3 -m http.server --directory ..",
30
- "typecheck": "node scripts/check-source-syntax.cjs",
30
+ "typecheck": "node scripts/check-source-syntax.cjs && tsc -p tsconfig.types.json && node scripts/verify-clean-type-consumer.cjs",
31
31
  "audit:eslint": "eslint . --max-warnings=0",
32
32
  "audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
33
33
  "audit:npm": "npm audit --audit-level=high --omit=dev",
@@ -53,14 +53,15 @@
53
53
  "license": "Apache-2.0",
54
54
  "dependencies": {
55
55
  "@plasius/gpu-camera": "^0.1.18",
56
- "@plasius/gpu-shared": "^1.0.14",
57
- "@plasius/gpu-xr": "^0.1.17"
56
+ "@plasius/gpu-shared": "^1.1.1",
57
+ "@plasius/gpu-xr": "^0.1.17",
58
+ "@types/webxr": "^0.5.24"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@eslint/js": "^10.0.1",
61
62
  "c8": "^11.0.0",
62
- "eslint": "^10.6.0",
63
- "globals": "^17.7.0",
63
+ "eslint": "^10.9.1",
64
+ "globals": "^17.11.0",
64
65
  "tsup": "^8.5.1",
65
66
  "typescript": "^7.0.2"
66
67
  },
@@ -0,0 +1,190 @@
1
+ import {
2
+ FEEDBACK_GAME_DIAGNOSTICS_CONTRACT_VERSION,
3
+ feedbackGameDiagnosticSurfaceRegistrations,
4
+ parseFeedbackGameDiagnostics,
5
+ } from "@plasius/gpu-shared/feedback-diagnostics";
6
+
7
+ const inputKeys = Object.freeze([
8
+ "featureEnabled",
9
+ "capabilityGranted",
10
+ "consentConfirmed",
11
+ "surfaceId",
12
+ "renderer",
13
+ "backend",
14
+ "viewportWidth",
15
+ "viewportHeight",
16
+ "frameRate",
17
+ "frameTimeMs",
18
+ "featureIds",
19
+ "counters",
20
+ "errorCodes",
21
+ ]);
22
+
23
+ export function bucketFeedbackGameViewport(width, height) {
24
+ if (
25
+ typeof width !== "number" ||
26
+ typeof height !== "number" ||
27
+ !Number.isFinite(width) ||
28
+ !Number.isFinite(height) ||
29
+ width <= 0 ||
30
+ height <= 0
31
+ ) {
32
+ return "unknown";
33
+ }
34
+
35
+ const shorterEdge = Math.min(width, height);
36
+ const longerEdge = Math.max(width, height);
37
+ const size =
38
+ shorterEdge < 600
39
+ ? "small"
40
+ : shorterEdge < 900 || longerEdge < 1_440
41
+ ? "medium"
42
+ : "large";
43
+ const orientation = width > height ? "landscape" : "portrait";
44
+ return `${size}-${orientation}`;
45
+ }
46
+
47
+ export function bucketFeedbackGameFrameRate(frameRate) {
48
+ if (
49
+ typeof frameRate !== "number" ||
50
+ !Number.isFinite(frameRate) ||
51
+ frameRate < 0
52
+ ) {
53
+ return "unknown";
54
+ }
55
+ if (frameRate < 15) {
56
+ return "under-15";
57
+ }
58
+ if (frameRate < 30) {
59
+ return "15-29";
60
+ }
61
+ if (frameRate < 60) {
62
+ return "30-59";
63
+ }
64
+ return "60-plus";
65
+ }
66
+
67
+ export function bucketFeedbackGameFrameTime(frameTimeMs) {
68
+ if (
69
+ typeof frameTimeMs !== "number" ||
70
+ !Number.isFinite(frameTimeMs) ||
71
+ frameTimeMs < 0
72
+ ) {
73
+ return "unknown";
74
+ }
75
+ if (frameTimeMs < 17) {
76
+ return "under-17ms";
77
+ }
78
+ if (frameTimeMs < 34) {
79
+ return "17-33ms";
80
+ }
81
+ if (frameTimeMs < 67) {
82
+ return "34-66ms";
83
+ }
84
+ return "over-66ms";
85
+ }
86
+
87
+ export function createFeedbackGameDiagnosticSnapshot(input) {
88
+ try {
89
+ return createFeedbackGameDiagnosticSnapshotInternal(input);
90
+ } catch {
91
+ throw new TypeError("Invalid privacy-safe renderer diagnostics input.");
92
+ }
93
+ }
94
+
95
+ function createFeedbackGameDiagnosticSnapshotInternal(input) {
96
+ const snapshot = snapshotInputIfEnabled(input);
97
+ if (snapshot === null) {
98
+ return null;
99
+ }
100
+
101
+ const registration =
102
+ typeof snapshot.surfaceId === "string" &&
103
+ Object.hasOwn(
104
+ feedbackGameDiagnosticSurfaceRegistrations,
105
+ snapshot.surfaceId
106
+ )
107
+ ? feedbackGameDiagnosticSurfaceRegistrations[snapshot.surfaceId]
108
+ : undefined;
109
+
110
+ return parseFeedbackGameDiagnostics({
111
+ type: "feedback-game-diagnostics",
112
+ version: FEEDBACK_GAME_DIAGNOSTICS_CONTRACT_VERSION,
113
+ surfaceId: snapshot.surfaceId,
114
+ consentConfirmed: true,
115
+ provenanceContractId: registration?.provenanceContractId,
116
+ renderer: snapshot.renderer,
117
+ backend: snapshot.backend,
118
+ viewportBucket: bucketFeedbackGameViewport(
119
+ snapshot.viewportWidth,
120
+ snapshot.viewportHeight
121
+ ),
122
+ frameRateBucket: bucketFeedbackGameFrameRate(snapshot.frameRate),
123
+ frameTimeBucket: bucketFeedbackGameFrameTime(snapshot.frameTimeMs),
124
+ featureIds: snapshot.featureIds,
125
+ counters: snapshot.counters,
126
+ errorCodes: snapshot.errorCodes,
127
+ });
128
+ }
129
+
130
+ function snapshotInputIfEnabled(input) {
131
+ assertValid(
132
+ input !== null &&
133
+ typeof input === "object" &&
134
+ !Array.isArray(input) &&
135
+ (Object.getPrototypeOf(input) === Object.prototype ||
136
+ Object.getPrototypeOf(input) === null)
137
+ );
138
+
139
+ const gateKeys = [
140
+ "featureEnabled",
141
+ "capabilityGranted",
142
+ "consentConfirmed",
143
+ ];
144
+ const snapshot = Object.create(null);
145
+ for (const key of gateKeys) {
146
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
147
+ assertValid(
148
+ descriptor !== undefined &&
149
+ descriptor.enumerable === true &&
150
+ Object.hasOwn(descriptor, "value") &&
151
+ typeof descriptor.value === "boolean"
152
+ );
153
+ snapshot[key] = descriptor.value;
154
+ }
155
+
156
+ const { featureEnabled, capabilityGranted, consentConfirmed } = snapshot;
157
+ if (
158
+ !featureEnabled ||
159
+ !capabilityGranted ||
160
+ !consentConfirmed
161
+ ) {
162
+ return null;
163
+ }
164
+
165
+ const actualKeys = Reflect.ownKeys(input);
166
+ assertValid(
167
+ actualKeys.length === inputKeys.length &&
168
+ inputKeys.every((key) => actualKeys.includes(key)) &&
169
+ actualKeys.every(
170
+ (key) => typeof key === "string" && inputKeys.includes(key)
171
+ )
172
+ );
173
+
174
+ for (const key of inputKeys.filter((key) => !gateKeys.includes(key))) {
175
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
176
+ assertValid(
177
+ descriptor !== undefined &&
178
+ descriptor.enumerable === true &&
179
+ Object.hasOwn(descriptor, "value")
180
+ );
181
+ snapshot[key] = descriptor.value;
182
+ }
183
+ return Object.freeze(snapshot);
184
+ }
185
+
186
+ function assertValid(condition) {
187
+ if (!condition) {
188
+ throw new TypeError("Invalid privacy-safe renderer diagnostics input.");
189
+ }
190
+ }
package/src/index.d.ts CHANGED
@@ -1,3 +1,60 @@
1
+ /// <reference types="webxr" />
2
+
3
+ import type {
4
+ FeedbackGameDiagnosticBackend,
5
+ FeedbackGameDiagnosticCounter,
6
+ FeedbackGameDiagnosticErrorCode,
7
+ FeedbackGameDiagnosticFeatureId,
8
+ FeedbackGameDiagnosticFrameRateBucket,
9
+ FeedbackGameDiagnosticFrameTimeBucket,
10
+ FeedbackGameDiagnostics,
11
+ FeedbackGameDiagnosticRenderer,
12
+ FeedbackGameDiagnosticSurfaceId,
13
+ FeedbackGameDiagnosticViewportBucket,
14
+ } from "@plasius/gpu-shared/feedback-diagnostics";
15
+ import type { XrStoreState } from "@plasius/gpu-xr";
16
+
17
+ export interface CreateFeedbackGameDiagnosticSnapshotInput {
18
+ readonly featureEnabled: boolean;
19
+ readonly capabilityGranted: boolean;
20
+ readonly consentConfirmed: boolean;
21
+ readonly surfaceId: FeedbackGameDiagnosticSurfaceId;
22
+ readonly renderer: FeedbackGameDiagnosticRenderer;
23
+ readonly backend: FeedbackGameDiagnosticBackend;
24
+ /** CSS-pixel viewport width; it is bucketed synchronously and never returned. */
25
+ readonly viewportWidth: number | null;
26
+ /** CSS-pixel viewport height; it is bucketed synchronously and never returned. */
27
+ readonly viewportHeight: number | null;
28
+ /** Recent coarse FPS observation; it is bucketed synchronously and never returned. */
29
+ readonly frameRate: number | null;
30
+ /** Recent coarse frame-time observation; it is bucketed synchronously and never returned. */
31
+ readonly frameTimeMs: number | null;
32
+ readonly featureIds: readonly FeedbackGameDiagnosticFeatureId[];
33
+ readonly counters: readonly FeedbackGameDiagnosticCounter[];
34
+ readonly errorCodes: readonly FeedbackGameDiagnosticErrorCode[];
35
+ }
36
+
37
+ export function bucketFeedbackGameViewport(
38
+ width: unknown,
39
+ height: unknown,
40
+ ): FeedbackGameDiagnosticViewportBucket;
41
+
42
+ export function bucketFeedbackGameFrameRate(
43
+ frameRate: unknown,
44
+ ): FeedbackGameDiagnosticFrameRateBucket;
45
+
46
+ export function bucketFeedbackGameFrameTime(
47
+ frameTimeMs: unknown,
48
+ ): FeedbackGameDiagnosticFrameTimeBucket;
49
+
50
+ export function createFeedbackGameDiagnosticSnapshot<
51
+ SurfaceId extends FeedbackGameDiagnosticSurfaceId,
52
+ >(
53
+ input: CreateFeedbackGameDiagnosticSnapshotInput & {
54
+ readonly surfaceId: SurfaceId;
55
+ },
56
+ ): Extract<FeedbackGameDiagnostics, { readonly surfaceId: SurfaceId }> | null;
57
+
1
58
  export type RendererColor = string | [number, number, number, number?];
2
59
  export type AnimatedSceneVector3 = readonly [number, number, number] | readonly number[];
3
60
  export type AnimatedScenePropKind =
@@ -509,7 +566,6 @@ export interface WavefrontMeshInput {
509
566
  readonly occlusionTexture?: WavefrontTextureSampleInput | null;
510
567
  readonly emissiveTexture?: WavefrontTextureSampleInput | null;
511
568
  readonly medium?: WavefrontMediumInput | null;
512
- readonly extensions?: Record<string, Record<string, unknown>>;
513
569
  }
514
570
 
515
571
  export interface WavefrontTriangleRecord {
@@ -1594,13 +1650,12 @@ export interface GpuRenderer {
1594
1650
  setXrActive(active: boolean): void;
1595
1651
  getSnapshot(): RendererSnapshot;
1596
1652
  bindXrManager(
1597
- xrManager: {
1598
- subscribe: (listener: (state: { activeSession: XRSession | null }) => void) => () => void;
1599
- getState?: () => { activeSession: XRSession | null };
1600
- store?: { getSnapshot: () => { activeSession: XRSession | null } };
1601
- },
1653
+ xrManager: RendererXrManager,
1602
1654
  bindOptions?: {
1603
- onSessionStart?: (session: XRSession, renderer: GpuRenderer) => void;
1655
+ onSessionStart?: (
1656
+ session: NonNullable<XrStoreState["activeSession"]>,
1657
+ renderer: GpuRenderer
1658
+ ) => void;
1604
1659
  onSessionEnd?: (renderer: GpuRenderer) => void;
1605
1660
  }
1606
1661
  ): () => void;
@@ -1636,15 +1691,22 @@ export function createRendererDebugHooks(
1636
1691
  options: RendererDebugHooksOptions
1637
1692
  ): Pick<RendererHooks, "onFrameStart" | "onFrameComplete">;
1638
1693
 
1694
+ export type RendererXrState = Pick<XrStoreState, "activeSession">;
1695
+
1696
+ export interface RendererXrManager {
1697
+ subscribe(listener: (state: RendererXrState) => void): () => void;
1698
+ getState?(): RendererXrState;
1699
+ store?: { getSnapshot(): RendererXrState };
1700
+ }
1701
+
1639
1702
  export function bindRendererToXrManager(
1640
1703
  renderer: Pick<GpuRenderer, "setXrActive">,
1641
- xrManager: {
1642
- subscribe: (listener: (state: { activeSession: XRSession | null }) => void) => () => void;
1643
- getState?: () => { activeSession: XRSession | null };
1644
- store?: { getSnapshot: () => { activeSession: XRSession | null } };
1645
- },
1704
+ xrManager: RendererXrManager,
1646
1705
  options?: {
1647
- onSessionStart?: (session: XRSession, renderer: Pick<GpuRenderer, "setXrActive">) => void;
1706
+ onSessionStart?: (
1707
+ session: NonNullable<XrStoreState["activeSession"]>,
1708
+ renderer: Pick<GpuRenderer, "setXrActive">
1709
+ ) => void;
1648
1710
  onSessionEnd?: (renderer: Pick<GpuRenderer, "setXrActive">) => void;
1649
1711
  }
1650
1712
  ): () => void;
package/src/index.js CHANGED
@@ -4,6 +4,12 @@ export {
4
4
  export {
5
5
  createProfessionalAnimatedSceneRenderer,
6
6
  } from "./professional-animated-scene-renderer.js";
7
+ export {
8
+ bucketFeedbackGameFrameRate,
9
+ bucketFeedbackGameFrameTime,
10
+ bucketFeedbackGameViewport,
11
+ createFeedbackGameDiagnosticSnapshot,
12
+ } from "./feedback-diagnostics.js";
7
13
  export {
8
14
  createDefaultWavefrontSceneObjects,
9
15
  createWavefrontBvhBuildLevels,
@@ -211,6 +211,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
211
211
  width: config.width,
212
212
  height: config.height,
213
213
  tileSize: safeTileSize,
214
+ tilePixelCapacity: Math.min(
215
+ config.tilePixelCapacity,
216
+ safeTileSize * safeTileSize
217
+ ),
214
218
  });
215
219
  }
216
220
  config = Object.freeze({
@@ -4,6 +4,7 @@ import {
4
4
  DEFAULT_BRDF_LUT_SIZE,
5
5
  HIT_RECORD_BYTES,
6
6
  MEDIUM_TABLE_ROWS,
7
+ PATH_VERTEX_RECORD_BYTES,
7
8
  RAY_RECORD_BYTES,
8
9
  } from "./wavefront-core.js";
9
10
  import {
@@ -28,7 +29,13 @@ export function clampTileSizeForDevice(config, device) {
28
29
 
29
30
  const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
30
31
  const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
31
- const maxPixels = Math.max(256, Math.min(maxPixelsByRay, maxPixelsByHit));
32
+ const maxPixelsByPathVertex = Math.floor(
33
+ limit / ((config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES)
34
+ );
35
+ const maxPixels = Math.max(
36
+ 256,
37
+ Math.min(maxPixelsByRay, maxPixelsByHit, maxPixelsByPathVertex)
38
+ );
32
39
  if (config.tilePixelCapacity <= maxPixels) {
33
40
  return config.tileSize;
34
41
  }