1- import { useLayoutEffect , useRef } from "react" ;
1+ import { useCallback , useLayoutEffect , useRef , useState } from "react" ;
22
33import { useMeasure , useRafLoop } from "react-use" ;
44import { Box , Flex } from "@radix-ui/themes" ;
55import type { FlexProps } from "@radix-ui/themes" ;
66import { useShredsChartScale } from "../useShredsChartScale" ;
77import { useSetAtom } from "jotai" ;
8- import { minDirtySlotByChartAtom } from "../atoms" ;
8+ import { isWebgl2SupportedAtom , minDirtySlotByChartAtom } from "../atoms" ;
99import type { RendererObj , TsRange } from "./chartUtils" ;
1010import { setUpRenderer , draw } from "./chartUtils" ;
1111import ShredsSlotLabels from "../ShredsSlotLabels" ;
1212import { MChartAxes , xAxisHeight } from "./ChartAxes" ;
1313import { createLabelsState , type LabelsState } from "../utils" ;
14+ import { disposeWebglResources } from "../webglUtils" ;
1415
1516const REDRAW_INTERVAL_MS = 15 ;
17+ /**
18+ * How long to wait for the GPU to restore a lost WebGL context before falling back to the canvas chart.
19+ * Context loss is usually transient (tab backgrounded, GPU reset, driver hiccup) and the browser restores it within
20+ * 1 ~ 2 frames.
21+ */
22+ const CONTEXT_RESTORE_TIMEOUT_MS = 10_000 ;
1623
17- type FlexPropsSubset = Pick < FlexProps , "height" | "minHeight" | "flexGrow" > ;
18-
19- interface ShredsChartProps {
24+ interface ShredsChartProps
25+ extends Pick < FlexProps , "height" | "minHeight" | "flexGrow" > {
2026 chartId : string ;
2127}
22- export default function ShredsChart ( {
28+ export default function ShredsChart ( props : ShredsChartProps ) {
29+ const [ key , setKey ] = useState ( 0 ) ;
30+ const triggerRemount = useCallback ( ( ) => {
31+ setKey ( ( prev ) => prev + 1 ) ;
32+ } , [ setKey ] ) ;
33+ return (
34+ < ShredsChartInner key = { key } { ...props } triggerRemount = { triggerRemount } />
35+ ) ;
36+ }
37+
38+ interface ShredsChartInnerProps extends ShredsChartProps {
39+ triggerRemount : ( ) => void ;
40+ }
41+
42+ export function ShredsChartInner ( {
2343 chartId,
44+ triggerRemount,
2445 ...flexProps
25- } : ShredsChartProps & FlexPropsSubset ) {
46+ } : ShredsChartInnerProps ) {
2647 const setMinDirtySlotByChart = useSetAtom ( minDirtySlotByChartAtom ) ;
48+ const setWebgl2Supported = useSetAtom ( isWebgl2SupportedAtom ) ;
2749
2850 const prevTimeDiffsRef = useRef < number [ ] > ( [ ] ) ;
2951 const lastRedrawRef = useRef ( 0 ) ;
@@ -41,9 +63,31 @@ export default function ShredsChart({
4163 prevLabels : createLabelsState ( ) ,
4264 tempNewLabels : createLabelsState ( ) ,
4365 } ) ;
66+ /**
67+ * timeout to fall back to canvas chart if context is lost and not restored
68+ */
69+ const contextRestoreTimeoutRef = useRef < ReturnType < typeof setTimeout > > ( ) ;
4470
4571 const scale = useShredsChartScale ( ) ;
4672
73+ const handleContextLost = useCallback (
74+ ( event : Event ) => {
75+ // preventDefault so that the browser fires webglcontextrestored
76+ event . preventDefault ( ) ;
77+
78+ clearTimeout ( contextRestoreTimeoutRef . current ) ;
79+ contextRestoreTimeoutRef . current = setTimeout ( ( ) => {
80+ setWebgl2Supported ( false ) ;
81+ } , CONTEXT_RESTORE_TIMEOUT_MS ) ;
82+ } ,
83+ [ setWebgl2Supported ] ,
84+ ) ;
85+
86+ const handleContextRestored = useCallback ( ( ) => {
87+ clearTimeout ( contextRestoreTimeoutRef . current ) ;
88+ triggerRemount ( ) ;
89+ } , [ triggerRemount ] ) ;
90+
4791 useLayoutEffect ( ( ) => {
4892 // setup dirty slot tracking
4993 setMinDirtySlotByChart ( ( prev ) => {
@@ -58,8 +102,9 @@ export default function ShredsChart({
58102 return prev ;
59103 } ) ;
60104
105+ clearTimeout ( contextRestoreTimeoutRef . current ) ;
106+
61107 // dispose of WebGL resources
62- // NOTE: keep sharedMaterial and unitQuad to be reused across mounts
63108 const obj = rendererRef . current ;
64109 if ( ! obj ) return ;
65110
@@ -69,13 +114,30 @@ export default function ShredsChart({
69114 for ( const slotMesh of obj . availableMeshes ) {
70115 slotMesh . mesh . geometry . dispose ( ) ;
71116 }
117+ // dispose this chart's own unitQuad / sharedMaterial
118+ disposeWebglResources ( obj . resources ) ;
119+
120+ obj . renderer . domElement . removeEventListener (
121+ "webglcontextlost" ,
122+ handleContextLost ,
123+ ) ;
124+ obj . renderer . domElement . removeEventListener (
125+ "webglcontextrestored" ,
126+ handleContextRestored ,
127+ ) ;
128+
72129 obj . renderer . dispose ( ) ;
73130 // force release of GL context on repeated mount / unmounts
74131 obj . renderer . forceContextLoss ( ) ;
75132 obj . renderer . domElement . remove ( ) ;
76133 rendererRef . current = undefined ;
77134 } ;
78- } , [ chartId , setMinDirtySlotByChart ] ) ;
135+ } , [
136+ chartId ,
137+ handleContextLost ,
138+ handleContextRestored ,
139+ setMinDirtySlotByChart ,
140+ ] ) ;
79141
80142 // handle chart resize
81143 useLayoutEffect ( ( ) => {
@@ -95,6 +157,10 @@ export default function ShredsChart({
95157 } , [ scale , width , height , chartId ] ) ;
96158
97159 useRafLoop ( function drawShredsLoop ( time : number ) {
160+ // Don't draw while waiting for context restore.
161+ // but keep canvas mounted to listen for restore event.
162+ if ( contextRestoreTimeoutRef . current != null ) return ;
163+
98164 if (
99165 lastRedrawRef . current == null ||
100166 time - lastRedrawRef . current >= REDRAW_INTERVAL_MS
@@ -105,7 +171,10 @@ export default function ShredsChart({
105171 if ( ! rendererObj ) return ;
106172
107173 rendererRef . current = rendererObj ;
108- containerRef . current ?. replaceChildren ( rendererObj . renderer . domElement ) ;
174+ const canvas = rendererObj . renderer . domElement ;
175+ canvas . addEventListener ( "webglcontextlost" , handleContextLost ) ;
176+ canvas . addEventListener ( "webglcontextrestored" , handleContextRestored ) ;
177+ containerRef . current ?. replaceChildren ( canvas ) ;
109178 } else {
110179 draw (
111180 chartId ,
0 commit comments