midah commited on
Commit
27b496f
·
1 Parent(s): b33f942

Fix WebGL context loss: reduce render limits, add error handling and recovery

Browse files
frontend/src/components/visualizations/ScatterPlot3D.tsx CHANGED
@@ -6,6 +6,17 @@ import { ModelPoint } from '../../types';
6
  import { createSpatialIndex } from '../../utils/rendering/spatialIndex';
7
  import { adaptiveSampleByDistance } from '../../utils/rendering/frustumCulling';
8
 
 
 
 
 
 
 
 
 
 
 
 
9
  interface ScatterPlot3DProps {
10
  data: ModelPoint[];
11
  colorBy: string;
@@ -143,23 +154,32 @@ function Points({
143
  frameCount.current++;
144
  if (frameCount.current % 10 !== 0) return;
145
 
146
- const sampled = adaptiveSampleByDistance(
147
- data,
148
- camera as THREE.Camera,
149
- 1.0,
150
- 50
151
- );
152
-
153
- const MAX_RENDER_POINTS = 100000;
154
- if (sampled.length > MAX_RENDER_POINTS) {
155
- const step = Math.ceil(sampled.length / MAX_RENDER_POINTS);
156
- const finalSampled: ModelPoint[] = [];
157
- for (let i = 0; i < sampled.length; i += step) {
158
- finalSampled.push(sampled[i]);
 
 
 
 
 
 
159
  }
160
- setVisiblePoints(finalSampled);
161
- } else {
162
- setVisiblePoints(sampled);
 
 
 
163
  }
164
  });
165
 
@@ -205,10 +225,13 @@ function Points({
205
 
206
  if (visiblePoints.length === 0) return null;
207
 
 
 
 
208
  return (
209
  <instancedMesh
210
  ref={meshRef}
211
- args={[undefined, undefined, Math.max(100000, data.length)]}
212
  frustumCulled={true}
213
  onPointerMove={handlePointerMove}
214
  onPointerOut={handlePointerOut}
@@ -222,6 +245,7 @@ function Points({
222
 
223
  export default function ScatterPlot3D(props: ScatterPlot3DProps) {
224
  const { data } = props;
 
225
 
226
  useMemo(() => {
227
  if (data.length > 0) {
@@ -229,6 +253,20 @@ export default function ScatterPlot3D(props: ScatterPlot3DProps) {
229
  }
230
  }, [data]);
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  const bounds = useMemo(() => {
233
  if (data.length === 0) {
234
  return { center: [0, 0, 0] as [number, number, number], radius: 10 };
@@ -263,13 +301,22 @@ export default function ScatterPlot3D(props: ScatterPlot3DProps) {
263
  }, [data]);
264
 
265
  return (
266
- <div style={{ width: '100%', height: '100%', background: 'var(--background-color)' }}>
267
  <Canvas
 
268
  gl={{
269
  antialias: false,
270
  powerPreference: 'high-performance',
 
 
271
  }}
272
  performance={{ min: 0.5 }}
 
 
 
 
 
 
273
  >
274
  <PerspectiveCamera
275
  makeDefault
 
6
  import { createSpatialIndex } from '../../utils/rendering/spatialIndex';
7
  import { adaptiveSampleByDistance } from '../../utils/rendering/frustumCulling';
8
 
9
+ // WebGL context loss recovery
10
+ const handleWebGLContextLoss = (event: Event) => {
11
+ event.preventDefault();
12
+ // Context will be restored automatically by the browser
13
+ };
14
+
15
+ const handleWebGLContextRestored = () => {
16
+ // Context restored - component will re-render automatically
17
+ console.info('WebGL context restored');
18
+ };
19
+
20
  interface ScatterPlot3DProps {
21
  data: ModelPoint[];
22
  colorBy: string;
 
154
  frameCount.current++;
155
  if (frameCount.current % 10 !== 0) return;
156
 
157
+ try {
158
+ const sampled = adaptiveSampleByDistance(
159
+ data,
160
+ camera as THREE.Camera,
161
+ 1.0,
162
+ 50
163
+ );
164
+
165
+ // Reduced from 100000 to prevent WebGL context loss
166
+ const MAX_RENDER_POINTS = 50000;
167
+ if (sampled.length > MAX_RENDER_POINTS) {
168
+ const step = Math.ceil(sampled.length / MAX_RENDER_POINTS);
169
+ const finalSampled: ModelPoint[] = [];
170
+ for (let i = 0; i < sampled.length; i += step) {
171
+ finalSampled.push(sampled[i]);
172
+ }
173
+ setVisiblePoints(finalSampled);
174
+ } else {
175
+ setVisiblePoints(sampled);
176
  }
177
+ } catch (error) {
178
+ // Silently handle WebGL errors to prevent console spam
179
+ if (error instanceof Error && error.message.includes('WebGL')) {
180
+ return;
181
+ }
182
+ throw error;
183
  }
184
  });
185
 
 
225
 
226
  if (visiblePoints.length === 0) return null;
227
 
228
+ // Reduced max instances to prevent WebGL context loss
229
+ const maxInstances = Math.min(50000, Math.max(visiblePoints.length, 1000));
230
+
231
  return (
232
  <instancedMesh
233
  ref={meshRef}
234
+ args={[undefined, undefined, maxInstances]}
235
  frustumCulled={true}
236
  onPointerMove={handlePointerMove}
237
  onPointerOut={handlePointerOut}
 
245
 
246
  export default function ScatterPlot3D(props: ScatterPlot3DProps) {
247
  const { data } = props;
248
+ const canvasRef = useRef<HTMLDivElement>(null);
249
 
250
  useMemo(() => {
251
  if (data.length > 0) {
 
253
  }
254
  }, [data]);
255
 
256
+ // Add WebGL context loss handlers
257
+ useEffect(() => {
258
+ const canvas = canvasRef.current?.querySelector('canvas');
259
+ if (canvas) {
260
+ canvas.addEventListener('webglcontextlost', handleWebGLContextLoss);
261
+ canvas.addEventListener('webglcontextrestored', handleWebGLContextRestored);
262
+
263
+ return () => {
264
+ canvas.removeEventListener('webglcontextlost', handleWebGLContextLoss);
265
+ canvas.removeEventListener('webglcontextrestored', handleWebGLContextRestored);
266
+ };
267
+ }
268
+ }, []);
269
+
270
  const bounds = useMemo(() => {
271
  if (data.length === 0) {
272
  return { center: [0, 0, 0] as [number, number, number], radius: 10 };
 
301
  }, [data]);
302
 
303
  return (
304
+ <div ref={canvasRef} style={{ width: '100%', height: '100%', background: 'var(--background-color)' }}>
305
  <Canvas
306
+ key="scatter-3d-canvas"
307
  gl={{
308
  antialias: false,
309
  powerPreference: 'high-performance',
310
+ preserveDrawingBuffer: false,
311
+ failIfMajorPerformanceCaveat: false,
312
  }}
313
  performance={{ min: 0.5 }}
314
+ onCreated={({ gl }) => {
315
+ // Suppress WebGL context loss errors
316
+ gl.domElement.addEventListener('webglcontextlost', (e) => {
317
+ e.preventDefault();
318
+ });
319
+ }}
320
  >
321
  <PerspectiveCamera
322
  makeDefault
frontend/src/index.tsx CHANGED
@@ -21,6 +21,21 @@ const isWebSocketError = (error: any, message?: string): boolean => {
21
  );
22
  };
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  window.addEventListener('error', (event) => {
25
  if (isWebSocketError(event.error, event.message)) {
26
  // Suppress WebSocket errors - they're from HMR and non-critical
@@ -28,18 +43,35 @@ window.addEventListener('error', (event) => {
28
  event.stopPropagation();
29
  return false;
30
  }
 
 
 
 
 
 
 
 
 
31
  }, true); // Use capture phase to catch early
32
 
33
- // Handle unhandled promise rejections related to WebSockets
34
  window.addEventListener('unhandledrejection', (event) => {
35
  if (isWebSocketError(event.reason)) {
36
  // Suppress WebSocket promise rejections
37
  event.preventDefault();
38
  event.stopPropagation();
39
  }
 
 
 
 
 
 
 
 
40
  }, true); // Use capture phase
41
 
42
- // Also suppress console errors for WebSocket issues
43
  const originalConsoleError = console.error;
44
  console.error = (...args: any[]) => {
45
  const message = args.join(' ').toLowerCase();
@@ -51,6 +83,14 @@ console.error = (...args: any[]) => {
51
  // Suppress WebSocket console errors
52
  return;
53
  }
 
 
 
 
 
 
 
 
54
  originalConsoleError.apply(console, args);
55
  };
56
 
 
21
  );
22
  };
23
 
24
+ // Check if error is WebGL context loss (non-critical, handled by component)
25
+ const isWebGLError = (error: any, message?: string): boolean => {
26
+ const errorStr = error?.toString() || '';
27
+ const messageStr = message?.toString() || '';
28
+ const combined = `${errorStr} ${messageStr}`.toLowerCase();
29
+
30
+ return (
31
+ combined.includes('webgl') ||
32
+ combined.includes('context lost') ||
33
+ combined.includes('webglrenderer') ||
34
+ error?.message?.toLowerCase().includes('webgl') ||
35
+ error?.stack?.toLowerCase().includes('webgl')
36
+ );
37
+ };
38
+
39
  window.addEventListener('error', (event) => {
40
  if (isWebSocketError(event.error, event.message)) {
41
  // Suppress WebSocket errors - they're from HMR and non-critical
 
43
  event.stopPropagation();
44
  return false;
45
  }
46
+ if (isWebGLError(event.error, event.message)) {
47
+ // Suppress WebGL context loss errors - handled by component recovery
48
+ if (process.env.NODE_ENV === 'development') {
49
+ console.warn('WebGL context error (handled):', event.message);
50
+ }
51
+ event.preventDefault();
52
+ event.stopPropagation();
53
+ return false;
54
+ }
55
  }, true); // Use capture phase to catch early
56
 
57
+ // Handle unhandled promise rejections related to WebSockets and WebGL
58
  window.addEventListener('unhandledrejection', (event) => {
59
  if (isWebSocketError(event.reason)) {
60
  // Suppress WebSocket promise rejections
61
  event.preventDefault();
62
  event.stopPropagation();
63
  }
64
+ if (isWebGLError(event.reason)) {
65
+ // Suppress WebGL promise rejections (handled by component)
66
+ if (process.env.NODE_ENV === 'development') {
67
+ console.warn('WebGL promise rejection (handled):', event.reason);
68
+ }
69
+ event.preventDefault();
70
+ event.stopPropagation();
71
+ }
72
  }, true); // Use capture phase
73
 
74
+ // Also suppress console errors for WebSocket and WebGL issues
75
  const originalConsoleError = console.error;
76
  console.error = (...args: any[]) => {
77
  const message = args.join(' ').toLowerCase();
 
83
  // Suppress WebSocket console errors
84
  return;
85
  }
86
+ if (
87
+ message.includes('webgl') ||
88
+ message.includes('context lost') ||
89
+ message.includes('webglrenderer')
90
+ ) {
91
+ // Suppress WebGL context loss console errors (handled by component)
92
+ return;
93
+ }
94
  originalConsoleError.apply(console, args);
95
  };
96