@myned-ai/gsplat-flame-avatar-renderer 1.0.5 → 1.0.6
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/README.md +130 -32
- package/dist/gsplat-flame-avatar-renderer.cjs.js +3478 -722
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js.map +1 -0
- package/dist/gsplat-flame-avatar-renderer.esm.js +3439 -724
- package/dist/gsplat-flame-avatar-renderer.esm.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.esm.min.js.map +1 -0
- package/package.json +10 -6
- package/src/core/SplatMesh.js +53 -46
- package/src/core/Viewer.js +47 -196
- package/src/errors/ApplicationError.js +185 -0
- package/src/errors/index.js +17 -0
- package/src/flame/FlameAnimator.js +282 -57
- package/src/loaders/PlyLoader.js +302 -44
- package/src/materials/SplatMaterial.js +13 -10
- package/src/materials/SplatMaterial3D.js +72 -27
- package/src/renderer/AnimationManager.js +8 -5
- package/src/renderer/GaussianSplatRenderer.js +668 -217
- package/src/utils/BlobUrlManager.js +294 -0
- package/src/utils/EventEmitter.js +349 -0
- package/src/utils/LoaderUtils.js +2 -1
- package/src/utils/Logger.js +171 -0
- package/src/utils/ObjectPool.js +248 -0
- package/src/utils/RenderLoop.js +306 -0
- package/src/utils/Util.js +59 -18
- package/src/utils/ValidationUtils.js +331 -0
- package/src/utils/index.js +10 -1
- package/dist/gsplat-flame-avatar-renderer.cjs.js.map +0 -1
- package/dist/gsplat-flame-avatar-renderer.esm.js.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger - Structured logging system
|
|
3
|
+
*
|
|
4
|
+
* Provides leveled logging with optional output suppression for production.
|
|
5
|
+
* Replaces direct console.log calls for better control and debugging.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Logger levels in order of severity
|
|
10
|
+
* Note: Named LoggerLevel to avoid conflict with existing LogLevel enum
|
|
11
|
+
*/
|
|
12
|
+
export const LoggerLevel = Object.freeze({
|
|
13
|
+
DEBUG: 0,
|
|
14
|
+
INFO: 1,
|
|
15
|
+
WARN: 2,
|
|
16
|
+
ERROR: 3,
|
|
17
|
+
NONE: 4
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Logger class for structured application logging
|
|
22
|
+
*/
|
|
23
|
+
export class Logger {
|
|
24
|
+
/**
|
|
25
|
+
* Create a Logger instance
|
|
26
|
+
* @param {string} namespace - Logger namespace (e.g., 'Renderer', 'Loader')
|
|
27
|
+
* @param {number} [minLevel=LoggerLevel.INFO] - Minimum log level to output
|
|
28
|
+
*/
|
|
29
|
+
constructor(namespace, minLevel = LoggerLevel.INFO) {
|
|
30
|
+
this.namespace = namespace;
|
|
31
|
+
this.minLevel = minLevel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set minimum log level
|
|
36
|
+
* @param {number} level - Minimum level from LoggerLevel enum
|
|
37
|
+
*/
|
|
38
|
+
setLevel(level) {
|
|
39
|
+
this.minLevel = level;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format log message with namespace and timestamp
|
|
44
|
+
* @private
|
|
45
|
+
* @param {string} level - Log level name
|
|
46
|
+
* @param {Array} args - Log arguments
|
|
47
|
+
* @returns {Array} Formatted arguments
|
|
48
|
+
*/
|
|
49
|
+
_format(level, args) {
|
|
50
|
+
const timestamp = new Date().toISOString();
|
|
51
|
+
const prefix = `[${timestamp}] [${level}] [${this.namespace}]`;
|
|
52
|
+
return [prefix, ...args];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Log debug message (most verbose)
|
|
57
|
+
* @param {...*} args - Arguments to log
|
|
58
|
+
*/
|
|
59
|
+
debug(...args) {
|
|
60
|
+
if (this.minLevel <= LoggerLevel.DEBUG) {
|
|
61
|
+
console.debug(...this._format('DEBUG', args));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Log info message
|
|
67
|
+
* @param {...*} args - Arguments to log
|
|
68
|
+
*/
|
|
69
|
+
info(...args) {
|
|
70
|
+
if (this.minLevel <= LoggerLevel.INFO) {
|
|
71
|
+
console.info(...this._format('INFO', args));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log warning message
|
|
77
|
+
* @param {...*} args - Arguments to log
|
|
78
|
+
*/
|
|
79
|
+
warn(...args) {
|
|
80
|
+
if (this.minLevel <= LoggerLevel.WARN) {
|
|
81
|
+
console.warn(...this._format('WARN', args));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Log error message
|
|
87
|
+
* @param {...*} args - Arguments to log
|
|
88
|
+
*/
|
|
89
|
+
error(...args) {
|
|
90
|
+
if (this.minLevel <= LoggerLevel.ERROR) {
|
|
91
|
+
console.error(...this._format('ERROR', args));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Log error with stack trace
|
|
97
|
+
* @param {Error} error - Error object
|
|
98
|
+
* @param {string} [context] - Additional context
|
|
99
|
+
*/
|
|
100
|
+
errorWithTrace(error, context = '') {
|
|
101
|
+
if (this.minLevel <= LoggerLevel.ERROR) {
|
|
102
|
+
const contextStr = context ? ` Context: ${context}` : '';
|
|
103
|
+
console.error(...this._format('ERROR', [
|
|
104
|
+
`${error.message}${contextStr}`,
|
|
105
|
+
'\nStack:', error.stack
|
|
106
|
+
]));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a child logger with extended namespace
|
|
112
|
+
* @param {string} childNamespace - Child namespace to append
|
|
113
|
+
* @returns {Logger} New logger with combined namespace
|
|
114
|
+
*/
|
|
115
|
+
child(childNamespace) {
|
|
116
|
+
return new Logger(`${this.namespace}:${childNamespace}`, this.minLevel);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Global logger registry
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
const loggers = new Map();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Global log level (affects all new loggers)
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
let globalLogLevel = LoggerLevel.INFO;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set global log level for all loggers
|
|
134
|
+
* @param {number} level - Log level from LoggerLevel enum
|
|
135
|
+
*/
|
|
136
|
+
export function setGlobalLogLevel(level) {
|
|
137
|
+
globalLogLevel = level;
|
|
138
|
+
// Update existing loggers
|
|
139
|
+
for (const logger of loggers.values()) {
|
|
140
|
+
logger.setLevel(level);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get or create a logger for a namespace
|
|
146
|
+
* @param {string} namespace - Logger namespace
|
|
147
|
+
* @returns {Logger} Logger instance
|
|
148
|
+
*/
|
|
149
|
+
export function getLogger(namespace) {
|
|
150
|
+
if (!loggers.has(namespace)) {
|
|
151
|
+
loggers.set(namespace, new Logger(namespace, globalLogLevel));
|
|
152
|
+
}
|
|
153
|
+
return loggers.get(namespace);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Configure logging for production (suppress all logs)
|
|
158
|
+
*/
|
|
159
|
+
export function configureForProduction() {
|
|
160
|
+
setGlobalLogLevel(LoggerLevel.NONE);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Configure logging for development (show all logs)
|
|
165
|
+
*/
|
|
166
|
+
export function configureForDevelopment() {
|
|
167
|
+
setGlobalLogLevel(LoggerLevel.DEBUG);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Export default logger for convenience
|
|
171
|
+
export default getLogger('App');
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectPool - Memory-efficient object pooling
|
|
3
|
+
*
|
|
4
|
+
* Reduces garbage collection pressure by reusing objects instead of
|
|
5
|
+
* repeatedly allocating and deallocating them. Critical for real-time rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Vector3, Matrix4, Quaternion, Euler } from 'three';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generic object pool
|
|
12
|
+
*/
|
|
13
|
+
export class ObjectPool {
|
|
14
|
+
/**
|
|
15
|
+
* Create an ObjectPool
|
|
16
|
+
* @param {Function} factory - Function that creates new objects
|
|
17
|
+
* @param {Function} reset - Function that resets an object to initial state
|
|
18
|
+
* @param {number} [initialSize=10] - Number of objects to pre-allocate
|
|
19
|
+
*/
|
|
20
|
+
constructor(factory, reset, initialSize = 10) {
|
|
21
|
+
this._factory = factory;
|
|
22
|
+
this._reset = reset;
|
|
23
|
+
this._pool = [];
|
|
24
|
+
this._allocated = 0;
|
|
25
|
+
this._maxSize = initialSize * 10; // Prevent unbounded growth
|
|
26
|
+
|
|
27
|
+
// Pre-allocate initial objects
|
|
28
|
+
for (let i = 0; i < initialSize; i++) {
|
|
29
|
+
this._pool.push(factory());
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Acquire an object from the pool
|
|
35
|
+
* @returns {*} Pooled object
|
|
36
|
+
*/
|
|
37
|
+
acquire() {
|
|
38
|
+
this._allocated++;
|
|
39
|
+
if (this._pool.length > 0) {
|
|
40
|
+
return this._pool.pop();
|
|
41
|
+
}
|
|
42
|
+
// Pool exhausted, create new object
|
|
43
|
+
return this._factory();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Release an object back to the pool
|
|
48
|
+
* @param {*} obj - Object to return to pool
|
|
49
|
+
*/
|
|
50
|
+
release(obj) {
|
|
51
|
+
this._allocated--;
|
|
52
|
+
if (this._pool.length < this._maxSize) {
|
|
53
|
+
this._reset(obj);
|
|
54
|
+
this._pool.push(obj);
|
|
55
|
+
}
|
|
56
|
+
// If pool is at max size, let object be garbage collected
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Release multiple objects
|
|
61
|
+
* @param {Array} objects - Objects to return to pool
|
|
62
|
+
*/
|
|
63
|
+
releaseAll(objects) {
|
|
64
|
+
for (const obj of objects) {
|
|
65
|
+
this.release(obj);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get pool statistics
|
|
71
|
+
* @returns {object} Pool stats
|
|
72
|
+
*/
|
|
73
|
+
getStats() {
|
|
74
|
+
return {
|
|
75
|
+
available: this._pool.length,
|
|
76
|
+
allocated: this._allocated,
|
|
77
|
+
maxSize: this._maxSize
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear the pool and release all objects
|
|
83
|
+
*/
|
|
84
|
+
dispose() {
|
|
85
|
+
this._pool.length = 0;
|
|
86
|
+
this._allocated = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pre-configured pools for common Three.js objects
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Vector3 pool
|
|
96
|
+
* @type {ObjectPool}
|
|
97
|
+
*/
|
|
98
|
+
export const vector3Pool = new ObjectPool(
|
|
99
|
+
() => new Vector3(),
|
|
100
|
+
(v) => v.set(0, 0, 0),
|
|
101
|
+
50 // Pre-allocate 50 vectors
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Matrix4 pool
|
|
106
|
+
* @type {ObjectPool}
|
|
107
|
+
*/
|
|
108
|
+
export const matrix4Pool = new ObjectPool(
|
|
109
|
+
() => new Matrix4(),
|
|
110
|
+
(m) => m.identity(),
|
|
111
|
+
20 // Pre-allocate 20 matrices
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Quaternion pool
|
|
116
|
+
* @type {ObjectPool}
|
|
117
|
+
*/
|
|
118
|
+
export const quaternionPool = new ObjectPool(
|
|
119
|
+
() => new Quaternion(),
|
|
120
|
+
(q) => q.set(0, 0, 0, 1),
|
|
121
|
+
30 // Pre-allocate 30 quaternions
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Euler pool
|
|
126
|
+
* @type {ObjectPool}
|
|
127
|
+
*/
|
|
128
|
+
export const eulerPool = new ObjectPool(
|
|
129
|
+
() => new Euler(),
|
|
130
|
+
(e) => e.set(0, 0, 0),
|
|
131
|
+
30 // Pre-allocate 30 eulers
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Scoped pool allocation helper
|
|
136
|
+
*
|
|
137
|
+
* Automatically releases pooled objects when scope exits.
|
|
138
|
+
* Use with try/finally to ensure cleanup.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const scope = new PoolScope();
|
|
142
|
+
* try {
|
|
143
|
+
* const v1 = scope.vector3();
|
|
144
|
+
* const v2 = scope.vector3();
|
|
145
|
+
* // Use vectors...
|
|
146
|
+
* } finally {
|
|
147
|
+
* scope.releaseAll();
|
|
148
|
+
* }
|
|
149
|
+
*/
|
|
150
|
+
export class PoolScope {
|
|
151
|
+
constructor() {
|
|
152
|
+
this._allocated = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Acquire a Vector3 from pool
|
|
157
|
+
* @returns {Vector3} Pooled vector
|
|
158
|
+
*/
|
|
159
|
+
vector3() {
|
|
160
|
+
const obj = vector3Pool.acquire();
|
|
161
|
+
this._allocated.push({ pool: vector3Pool, obj });
|
|
162
|
+
return obj;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Acquire a Matrix4 from pool
|
|
167
|
+
* @returns {Matrix4} Pooled matrix
|
|
168
|
+
*/
|
|
169
|
+
matrix4() {
|
|
170
|
+
const obj = matrix4Pool.acquire();
|
|
171
|
+
this._allocated.push({ pool: matrix4Pool, obj });
|
|
172
|
+
return obj;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Acquire a Quaternion from pool
|
|
177
|
+
* @returns {Quaternion} Pooled quaternion
|
|
178
|
+
*/
|
|
179
|
+
quaternion() {
|
|
180
|
+
const obj = quaternionPool.acquire();
|
|
181
|
+
this._allocated.push({ pool: quaternionPool, obj });
|
|
182
|
+
return obj;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Acquire an Euler from pool
|
|
187
|
+
* @returns {Euler} Pooled euler
|
|
188
|
+
*/
|
|
189
|
+
euler() {
|
|
190
|
+
const obj = eulerPool.acquire();
|
|
191
|
+
this._allocated.push({ pool: eulerPool, obj });
|
|
192
|
+
return obj;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Release all objects allocated in this scope
|
|
197
|
+
*/
|
|
198
|
+
releaseAll() {
|
|
199
|
+
for (const { pool, obj } of this._allocated) {
|
|
200
|
+
pool.release(obj);
|
|
201
|
+
}
|
|
202
|
+
this._allocated.length = 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get count of allocated objects in this scope
|
|
207
|
+
* @returns {number} Number of allocated objects
|
|
208
|
+
*/
|
|
209
|
+
getAllocatedCount() {
|
|
210
|
+
return this._allocated.length;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Module-level temporary objects for hot-path reuse
|
|
216
|
+
* IMPORTANT: These are NOT thread-safe. Only use in single-threaded contexts.
|
|
217
|
+
* Reset these before use to avoid stale data.
|
|
218
|
+
*/
|
|
219
|
+
export const tempVector3A = new Vector3();
|
|
220
|
+
export const tempVector3B = new Vector3();
|
|
221
|
+
export const tempVector3C = new Vector3();
|
|
222
|
+
export const tempMatrix4A = new Matrix4();
|
|
223
|
+
export const tempMatrix4B = new Matrix4();
|
|
224
|
+
export const tempQuaternionA = new Quaternion();
|
|
225
|
+
export const tempQuaternionB = new Quaternion();
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get pool statistics for all pre-configured pools
|
|
229
|
+
* @returns {object} Statistics for all pools
|
|
230
|
+
*/
|
|
231
|
+
export function getPoolStats() {
|
|
232
|
+
return {
|
|
233
|
+
vector3: vector3Pool.getStats(),
|
|
234
|
+
matrix4: matrix4Pool.getStats(),
|
|
235
|
+
quaternion: quaternionPool.getStats(),
|
|
236
|
+
euler: eulerPool.getStats()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Dispose all pre-configured pools
|
|
242
|
+
*/
|
|
243
|
+
export function disposeAllPools() {
|
|
244
|
+
vector3Pool.dispose();
|
|
245
|
+
matrix4Pool.dispose();
|
|
246
|
+
quaternionPool.dispose();
|
|
247
|
+
eulerPool.dispose();
|
|
248
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderLoop - Frame-independent animation loop with budget management
|
|
3
|
+
*
|
|
4
|
+
* Provides a robust requestAnimationFrame loop with:
|
|
5
|
+
* - Delta time calculation for frame-independent updates
|
|
6
|
+
* - Frame budget management to prevent frame drops
|
|
7
|
+
* - Deferred task execution
|
|
8
|
+
* - Performance monitoring
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getLogger } from './Logger.js';
|
|
12
|
+
|
|
13
|
+
const logger = getLogger('RenderLoop');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* RenderLoop - Manages animation frame loop
|
|
17
|
+
*/
|
|
18
|
+
export class RenderLoop {
|
|
19
|
+
/**
|
|
20
|
+
* Create a RenderLoop
|
|
21
|
+
* @param {Function} updateFn - Update function called each frame with deltaTime
|
|
22
|
+
* @param {Function} renderFn - Render function called each frame
|
|
23
|
+
* @param {object} [options] - Configuration options
|
|
24
|
+
* @param {number} [options.targetFps=60] - Target frames per second
|
|
25
|
+
* @param {number} [options.maxDeltaTime=0.1] - Maximum delta time in seconds (prevents spiral of death)
|
|
26
|
+
*/
|
|
27
|
+
constructor(updateFn, renderFn, options = {}) {
|
|
28
|
+
this._update = updateFn;
|
|
29
|
+
this._render = renderFn;
|
|
30
|
+
|
|
31
|
+
this._targetFps = options.targetFps || 60;
|
|
32
|
+
this._maxDeltaTime = options.maxDeltaTime || 0.1; // 100ms max
|
|
33
|
+
this._frameBudget = 1000 / this._targetFps; // ms per frame
|
|
34
|
+
|
|
35
|
+
this._running = false;
|
|
36
|
+
this._rafId = null;
|
|
37
|
+
this._lastTime = 0;
|
|
38
|
+
this._frameCount = 0;
|
|
39
|
+
this._deferredTasks = [];
|
|
40
|
+
|
|
41
|
+
// Performance tracking
|
|
42
|
+
this._fpsHistory = [];
|
|
43
|
+
this._fpsHistorySize = 60; // Track last 60 frames
|
|
44
|
+
this._lastFpsUpdate = 0;
|
|
45
|
+
this._currentFps = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start the render loop
|
|
50
|
+
*/
|
|
51
|
+
start() {
|
|
52
|
+
if (this._running) {
|
|
53
|
+
logger.warn('RenderLoop already running');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._running = true;
|
|
58
|
+
this._lastTime = performance.now();
|
|
59
|
+
this._frameCount = 0;
|
|
60
|
+
logger.info('RenderLoop started');
|
|
61
|
+
|
|
62
|
+
this._tick();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stop the render loop
|
|
67
|
+
*/
|
|
68
|
+
stop() {
|
|
69
|
+
if (!this._running) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._running = false;
|
|
74
|
+
|
|
75
|
+
if (this._rafId !== null) {
|
|
76
|
+
cancelAnimationFrame(this._rafId);
|
|
77
|
+
this._rafId = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.info(`RenderLoop stopped after ${this._frameCount} frames`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Main loop tick
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
_tick = () => {
|
|
88
|
+
if (!this._running) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const frameStart = performance.now();
|
|
93
|
+
const rawDeltaTime = (frameStart - this._lastTime) / 1000; // Convert to seconds
|
|
94
|
+
|
|
95
|
+
// Clamp delta time to prevent spiral of death
|
|
96
|
+
const deltaTime = Math.min(rawDeltaTime, this._maxDeltaTime);
|
|
97
|
+
|
|
98
|
+
this._lastTime = frameStart;
|
|
99
|
+
this._frameCount++;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Update logic
|
|
103
|
+
this._update(deltaTime);
|
|
104
|
+
|
|
105
|
+
// Render
|
|
106
|
+
this._render();
|
|
107
|
+
|
|
108
|
+
// Process deferred tasks if time permits
|
|
109
|
+
const frameElapsed = performance.now() - frameStart;
|
|
110
|
+
const remainingTime = this._frameBudget - frameElapsed;
|
|
111
|
+
|
|
112
|
+
if (remainingTime > 1 && this._deferredTasks.length > 0) {
|
|
113
|
+
this._processDeferredTasks(remainingTime - 1); // Leave 1ms margin
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Update FPS tracking
|
|
117
|
+
this._updateFpsTracking(performance.now() - frameStart);
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error('Error in render loop:', error);
|
|
121
|
+
// Continue loop despite error
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Schedule next frame
|
|
125
|
+
this._rafId = requestAnimationFrame(this._tick);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update FPS tracking
|
|
130
|
+
* @private
|
|
131
|
+
* @param {number} frameTime - Time taken for this frame in ms
|
|
132
|
+
*/
|
|
133
|
+
_updateFpsTracking(frameTime) {
|
|
134
|
+
this._fpsHistory.push(1000 / frameTime);
|
|
135
|
+
|
|
136
|
+
if (this._fpsHistory.length > this._fpsHistorySize) {
|
|
137
|
+
this._fpsHistory.shift();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Update FPS every second
|
|
141
|
+
const now = performance.now();
|
|
142
|
+
if (now - this._lastFpsUpdate > 1000) {
|
|
143
|
+
this._currentFps = this._fpsHistory.reduce((a, b) => a + b, 0) / this._fpsHistory.length;
|
|
144
|
+
this._lastFpsUpdate = now;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Process deferred tasks within time budget
|
|
150
|
+
* @private
|
|
151
|
+
* @param {number} maxTime - Maximum time in ms to spend on tasks
|
|
152
|
+
*/
|
|
153
|
+
_processDeferredTasks(maxTime) {
|
|
154
|
+
const startTime = performance.now();
|
|
155
|
+
|
|
156
|
+
while (this._deferredTasks.length > 0) {
|
|
157
|
+
if (performance.now() - startTime >= maxTime) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const task = this._deferredTasks.shift();
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
task.fn();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.error(`Error in deferred task: ${task.label}`, error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Execute task if within frame budget, otherwise defer
|
|
173
|
+
*
|
|
174
|
+
* @param {Function} task - Task function to execute
|
|
175
|
+
* @param {number} [priority=0] - Task priority (higher = more important)
|
|
176
|
+
* @param {string} [label=''] - Task label for debugging
|
|
177
|
+
*/
|
|
178
|
+
executeOrDefer(task, priority = 0, label = '') {
|
|
179
|
+
const frameElapsed = performance.now() - this._lastTime;
|
|
180
|
+
|
|
181
|
+
if (frameElapsed < this._frameBudget * 0.8) {
|
|
182
|
+
// Within budget, execute now
|
|
183
|
+
task();
|
|
184
|
+
} else {
|
|
185
|
+
// Over budget, defer
|
|
186
|
+
this._deferredTasks.push({ fn: task, priority, label });
|
|
187
|
+
|
|
188
|
+
// Sort by priority (higher first)
|
|
189
|
+
this._deferredTasks.sort((a, b) => b.priority - a.priority);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get current FPS
|
|
195
|
+
* @returns {number} Average FPS over recent frames
|
|
196
|
+
*/
|
|
197
|
+
getFps() {
|
|
198
|
+
return Math.round(this._currentFps);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get performance stats
|
|
203
|
+
* @returns {object} Performance statistics
|
|
204
|
+
*/
|
|
205
|
+
getStats() {
|
|
206
|
+
return {
|
|
207
|
+
fps: this.getFps(),
|
|
208
|
+
frameCount: this._frameCount,
|
|
209
|
+
deferredTaskCount: this._deferredTasks.length,
|
|
210
|
+
running: this._running
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if loop is running
|
|
216
|
+
* @returns {boolean} True if running
|
|
217
|
+
*/
|
|
218
|
+
isRunning() {
|
|
219
|
+
return this._running;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get frame count
|
|
224
|
+
* @returns {number} Total frames rendered
|
|
225
|
+
*/
|
|
226
|
+
getFrameCount() {
|
|
227
|
+
return this._frameCount;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Clear all deferred tasks
|
|
232
|
+
*/
|
|
233
|
+
clearDeferredTasks() {
|
|
234
|
+
this._deferredTasks.length = 0;
|
|
235
|
+
logger.debug('Cleared all deferred tasks');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* FrameBudgetMonitor - Monitors and alerts on frame budget violations
|
|
241
|
+
*/
|
|
242
|
+
export class FrameBudgetMonitor {
|
|
243
|
+
/**
|
|
244
|
+
* Create a FrameBudgetMonitor
|
|
245
|
+
* @param {number} [targetFps=60] - Target FPS
|
|
246
|
+
* @param {Function} [onViolation] - Callback when budget is violated
|
|
247
|
+
*/
|
|
248
|
+
constructor(targetFps = 60, onViolation = null) {
|
|
249
|
+
this._targetFps = targetFps;
|
|
250
|
+
this._frameBudget = 1000 / targetFps;
|
|
251
|
+
this._onViolation = onViolation;
|
|
252
|
+
this._violations = 0;
|
|
253
|
+
this._frameStart = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Mark start of frame
|
|
258
|
+
*/
|
|
259
|
+
startFrame() {
|
|
260
|
+
this._frameStart = performance.now();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if frame is within budget
|
|
265
|
+
* @param {string} [location] - Location identifier for debugging
|
|
266
|
+
* @returns {boolean} True if within budget
|
|
267
|
+
*/
|
|
268
|
+
checkBudget(location = '') {
|
|
269
|
+
const elapsed = performance.now() - this._frameStart;
|
|
270
|
+
const withinBudget = elapsed < this._frameBudget;
|
|
271
|
+
|
|
272
|
+
if (!withinBudget) {
|
|
273
|
+
this._violations++;
|
|
274
|
+
|
|
275
|
+
if (this._onViolation) {
|
|
276
|
+
this._onViolation({
|
|
277
|
+
location,
|
|
278
|
+
elapsed,
|
|
279
|
+
budget: this._frameBudget,
|
|
280
|
+
overrun: elapsed - this._frameBudget
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
logger.warn(`Frame budget violation at ${location}: ${elapsed.toFixed(2)}ms / ${this._frameBudget.toFixed(2)}ms`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return withinBudget;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get violation count
|
|
292
|
+
* @returns {number} Total violations
|
|
293
|
+
*/
|
|
294
|
+
getViolationCount() {
|
|
295
|
+
return this._violations;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Reset violation count
|
|
300
|
+
*/
|
|
301
|
+
resetViolations() {
|
|
302
|
+
this._violations = 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export default RenderLoop;
|