@@ -32,6 +32,7 @@ export default class Engine {
3232 audio = null ,
3333 logger = null ,
3434 camera3D = null ,
35+ runtimeHooks = null ,
3536 } = { } ) {
3637 if ( ! canvas ) {
3738 throw new Error ( 'Engine requires a canvas.' ) ;
@@ -70,6 +71,10 @@ export default class Engine {
7071 } ) ;
7172 this . audio = audio || new AudioService ( ) ;
7273 this . logger = logger || new Logger ( { channel : 'engine' } ) ;
74+ this . runtimeHooks = {
75+ onError : typeof runtimeHooks ?. onError === 'function' ? runtimeHooks . onError : null ,
76+ onPerformance : typeof runtimeHooks ?. onPerformance === 'function' ? runtimeHooks . onPerformance : null ,
77+ } ;
7378 this . camera3D = camera3D || new Camera3D ( ) ;
7479 this . settings = new SettingsSystem ( {
7580 namespace : 'toolboxaid:engine-settings' ,
@@ -116,8 +121,10 @@ export default class Engine {
116121 try {
117122 scene . setCamera3D ( this . camera3D , this ) ;
118123 } catch ( error ) {
119- this . logger ?. warn ?. ( 'Engine scene setCamera3D hook failed.' , {
120- error : error ?. message || String ( error ) ,
124+ this . trackRuntimeError ( 'scene.setCamera3D' , error , {
125+ severity : 'warn' ,
126+ isolated : true ,
127+ message : 'Engine scene setCamera3D hook failed.' ,
121128 } ) ;
122129 }
123130 return ;
@@ -130,12 +137,50 @@ export default class Engine {
130137 try {
131138 scene . camera3D = this . camera3D ;
132139 } catch ( error ) {
133- this . logger ?. warn ?. ( 'Engine scene camera3D assignment failed.' , {
134- error : error ?. message || String ( error ) ,
140+ this . trackRuntimeError ( 'scene.camera3D.assign' , error , {
141+ severity : 'warn' ,
142+ isolated : true ,
143+ message : 'Engine scene camera3D assignment failed.' ,
135144 } ) ;
136145 }
137146 }
138147
148+ trackRuntimeError ( stage , error , { severity = 'error' , isolated = false , context = { } , message = null } = { } ) {
149+ const payload = {
150+ stage,
151+ isolated : isolated === true ,
152+ severity,
153+ error : error ?. message || String ( error ) ,
154+ timestamp : new Date ( ) . toISOString ( ) ,
155+ context : { ...context } ,
156+ } ;
157+
158+ const loggerMethod = severity === 'warn' ? 'warn' : 'error' ;
159+ this . logger ?. [ loggerMethod ] ?. ( message || 'Engine runtime issue tracked.' , {
160+ event : 'engine.runtime.error' ,
161+ stage : payload . stage ,
162+ isolated : payload . isolated ,
163+ error : payload . error ,
164+ ...payload . context ,
165+ } ) ;
166+
167+ this . events ?. emit ?. ( 'engine:runtime-error' , payload ) ;
168+ this . runtimeHooks . onError ?. ( payload ) ;
169+ return payload ;
170+ }
171+
172+ publishPerformanceFrame ( frameData ) {
173+ const snapshot = typeof this . metrics ?. getSnapshot === 'function' ? this . metrics . getSnapshot ( ) : null ;
174+ const payload = {
175+ ...frameData ,
176+ snapshot : snapshot ? { ...snapshot } : null ,
177+ timestamp : new Date ( ) . toISOString ( ) ,
178+ } ;
179+ this . events ?. emit ?. ( 'engine:performance-frame' , payload ) ;
180+ this . runtimeHooks . onPerformance ?. ( payload ) ;
181+ return payload ;
182+ }
183+
139184 start ( ) {
140185 if ( this . input && typeof this . input . attach === 'function' ) {
141186 this . input . attach ( ) ;
@@ -188,10 +233,20 @@ export default class Engine {
188233 let fixedUpdates = 0 ;
189234
190235 if ( this . input && typeof this . input . update === 'function' ) {
191- this . input . update ( deltaSeconds ) ;
236+ try {
237+ this . input . update ( deltaSeconds ) ;
238+ } catch ( error ) {
239+ this . trackRuntimeError ( 'input.update' , error , { severity : 'error' } ) ;
240+ throw error ;
241+ }
192242 }
193243 if ( this . audio && typeof this . audio . update === 'function' ) {
194- this . audio . update ( deltaSeconds ) ;
244+ try {
245+ this . audio . update ( deltaSeconds ) ;
246+ } catch ( error ) {
247+ this . trackRuntimeError ( 'audio.update' , error , { severity : 'error' } ) ;
248+ throw error ;
249+ }
195250 }
196251
197252 const updateStart = performance . now ( ) ;
@@ -200,14 +255,21 @@ export default class Engine {
200255 try {
201256 this . scene . step3DPhysics ( stepSeconds , this ) ;
202257 } catch ( error ) {
203- this . logger ?. warn ?. ( 'Engine scene step3DPhysics hook failed.' , {
204- error : error ?. message || String ( error ) ,
258+ this . trackRuntimeError ( 'scene.step3DPhysics' , error , {
259+ severity : 'warn' ,
260+ isolated : true ,
261+ message : 'Engine scene step3DPhysics hook failed.' ,
205262 } ) ;
206263 }
207264 }
208265
209266 if ( this . scene && typeof this . scene . update === 'function' ) {
210- this . scene . update ( stepSeconds , this ) ;
267+ try {
268+ this . scene . update ( stepSeconds , this ) ;
269+ } catch ( error ) {
270+ this . trackRuntimeError ( 'scene.update' , error , { severity : 'error' } ) ;
271+ throw error ;
272+ }
211273 }
212274 } ) ;
213275 fixedUpdates = tickerResult . steps ;
@@ -217,7 +279,12 @@ export default class Engine {
217279 this . renderer . clear ( ) ;
218280 this . backgroundImageLayer ?. render ?. ( this . renderer , { scene : this . scene , engine : this } ) ;
219281 if ( this . scene && typeof this . scene . render === 'function' ) {
220- this . scene . render ( this . renderer , this ) ;
282+ try {
283+ this . scene . render ( this . renderer , this ) ;
284+ } catch ( error ) {
285+ this . trackRuntimeError ( 'scene.render' , error , { severity : 'error' } ) ;
286+ throw error ;
287+ }
221288 }
222289 const fullscreenActive = this . fullscreen ?. getState ?. ( ) . active === true ;
223290 const fullscreenElement = this . fullscreen ?. documentRef ?. fullscreenElement
@@ -226,13 +293,15 @@ export default class Engine {
226293 this . fullscreenBezelLayer ?. sync ?. ( { fullscreenActive, fullscreenElement } ) ;
227294 renderDurationMs = performance . now ( ) - renderStart ;
228295
229- this . metrics . recordFrame ( {
296+ const frameData = {
230297 dtSeconds : deltaSeconds ,
231298 frameMs : performance . now ( ) - frameStart ,
232299 updateMs : updateDurationMs ,
233300 renderMs : renderDurationMs ,
234301 fixedUpdates,
235- } ) ;
302+ } ;
303+ this . metrics . recordFrame ( frameData ) ;
304+ this . publishPerformanceFrame ( frameData ) ;
236305
237306 this . rafId = requestAnimationFrame ( this . tick ) ;
238307 }
0 commit comments