Skip to content

Commit 3f51d9e

Browse files
grewebclaude
andcommitted
Read GL pixels inside onDraw callback (before buffer swap)
expo-gl clears the framebuffer after endFrameEXP(), so readPixels after the draw cycle returns zeros. Use Node's onDraw callback which fires right after gl.drawArrays() but before the buffer swap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dff40b3 commit 3f51d9e

1 file changed

Lines changed: 43 additions & 37 deletions

File tree

packages/cookbook-expo/App.tsx

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -76,53 +76,59 @@ void main () {
7676
},
7777
});
7878

79-
// -- GL render check: reads pixels from the Surface to verify non-black output --
79+
// -- GL render check: reads pixels inside the Node's onDraw callback
80+
// (after gl.drawArrays but before endFrameEXP buffer swap) --
8081

8182
function useGLRenderCheck(surfaceRef: React.RefObject<any>) {
8283
const [status, setStatus] = useState("pending");
83-
useEffect(() => {
84-
const timer = setTimeout(() => {
85-
try {
86-
const surface = surfaceRef.current;
87-
if (!surface || !surface.gl) {
88-
setStatus("no-gl");
89-
return;
90-
}
91-
const gl: WebGLRenderingContext = surface.gl;
92-
const pixels = new Uint8Array(4 * 4 * 4); // sample 4x4 center
93-
const w = gl.drawingBufferWidth;
94-
const h = gl.drawingBufferHeight;
95-
const x = Math.floor(w / 2) - 2;
96-
const y = Math.floor(h / 2) - 2;
97-
gl.readPixels(x, y, 4, 4, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
98-
let nonBlack = false;
99-
for (let i = 0; i < pixels.length; i += 4) {
100-
if (pixels[i] > 5 || pixels[i + 1] > 5 || pixels[i + 2] > 5) {
101-
nonBlack = true;
102-
break;
103-
}
104-
}
105-
setStatus(nonBlack ? "rendered" : "black");
106-
} catch {
107-
setStatus("error");
84+
const checked = useRef(false);
85+
const onDraw = useCallback(() => {
86+
if (checked.current) return;
87+
const surface = surfaceRef.current;
88+
if (!surface?.gl) return;
89+
checked.current = true;
90+
const gl: WebGLRenderingContext = surface.gl;
91+
const pixels = new Uint8Array(4 * 4 * 4);
92+
const w = gl.drawingBufferWidth;
93+
const h = gl.drawingBufferHeight;
94+
gl.readPixels(
95+
Math.floor(w / 2) - 2,
96+
Math.floor(h / 2) - 2,
97+
4,
98+
4,
99+
gl.RGBA,
100+
gl.UNSIGNED_BYTE,
101+
pixels,
102+
);
103+
let nonBlack = false;
104+
for (let i = 0; i < pixels.length; i += 4) {
105+
if (pixels[i] > 5 || pixels[i + 1] > 5 || pixels[i + 2] > 5) {
106+
nonBlack = true;
107+
break;
108108
}
109-
}, 500);
110-
return () => clearTimeout(timer);
109+
}
110+
setStatus(nonBlack ? "rendered" : "black");
111111
}, [surfaceRef]);
112-
return status;
112+
return { status, onDraw };
113113
}
114114

115115
// -- Components --
116116

117-
function HelloGL({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
117+
function HelloGL({
118+
surfaceRef,
119+
onDraw,
120+
}: {
121+
surfaceRef?: React.RefObject<any>;
122+
onDraw?: () => void;
123+
}) {
118124
return (
119125
<Surface ref={surfaceRef} style={styles.surface}>
120-
<Node shader={shaders.helloGL} />
126+
<Node shader={shaders.helloGL} onDraw={onDraw} />
121127
</Surface>
122128
);
123129
}
124130

125-
function HelloBlue({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
131+
function HelloBlue({ surfaceRef, onDraw }: { surfaceRef?: React.RefObject<any>; onDraw?: () => void }) {
126132
const [blue, setBlue] = useState(0);
127133
useEffect(() => {
128134
const start = Date.now();
@@ -138,7 +144,7 @@ function HelloBlue({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
138144
);
139145
}
140146

141-
function ColorDisc({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
147+
function ColorDisc({ surfaceRef, onDraw }: { surfaceRef?: React.RefObject<any>; onDraw?: () => void }) {
142148
return (
143149
<Surface style={styles.surface}>
144150
<Node
@@ -152,7 +158,7 @@ function ColorDisc({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
152158
);
153159
}
154160

155-
function RotatingHello({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
161+
function RotatingHello({ surfaceRef, onDraw }: { surfaceRef?: React.RefObject<any>; onDraw?: () => void }) {
156162
const [angle, setAngle] = useState(0);
157163
useEffect(() => {
158164
const start = Date.now();
@@ -176,7 +182,7 @@ function RotatingHello({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
176182
);
177183
}
178184

179-
function MotionBlurDemo({ surfaceRef }: { surfaceRef?: React.RefObject<any> }) {
185+
function MotionBlurDemo({ surfaceRef, onDraw }: { surfaceRef?: React.RefObject<any>; onDraw?: () => void }) {
180186
const [t, setT] = useState(0);
181187
useEffect(() => {
182188
const start = Date.now();
@@ -249,7 +255,7 @@ export default function App() {
249255
const [selected, setSelected] = useState(0);
250256
const Example = examples[selected].component;
251257
const surfaceRef = useRef<any>(null);
252-
const glStatus = useGLRenderCheck(surfaceRef);
258+
const { status: glStatus, onDraw } = useGLRenderCheck(surfaceRef);
253259

254260
return (
255261
<SafeAreaView style={styles.container} testID="app-root">
@@ -260,7 +266,7 @@ export default function App() {
260266
</View>
261267

262268
<View style={styles.canvasContainer} testID="canvas-container">
263-
<Example surfaceRef={surfaceRef} />
269+
<Example surfaceRef={surfaceRef} onDraw={onDraw} />
264270
</View>
265271
<Text testID="gl-status" style={styles.glStatus}>
266272
gl:{glStatus}

0 commit comments

Comments
 (0)