Skip to content

Commit 684d671

Browse files
fuzzie360claude
andcommitted
feat: Rebuild on Expo SDK 57
The app had not been touched since its initial commit in March 2020 and targeted Expo SDK 36 against today's 57, so it could not run at all — which is why its 100 dependency alerts were beside the point. Most of those came from expo-cli being listed as a runtime dependency; it is a global tool, since deprecated by Expo, and it dragged in @expo/xdl, axios, webpack-dev-server and the rest. Rebuilt from the current Expo template: SDK 57, React 19.2, React Native 0.86, TypeScript, and the modern index.ts entry point. The app is now a diagnostic rather than a minimal demo. It reports each stage — context creation, GPU creation, feature detection, kernel compilation, kernel run — separately, with the error and the stage that produced it shown on screen. Since Expo's GL cannot run outside a device, a failure report from a user is the only signal available, and this makes that signal specific. CI typechecks and runs expo export, which makes Metro resolve and compile every import: that catches a broken shim, a GPU.js that will not bundle for React Native, and drift against the SDK. Verified locally — 693 modules, 2.2MB bundle. @gpujs/expo-gl is referenced from git until 1.0.0 is published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 7f8d36e commit 684d671

17 files changed

Lines changed: 6948 additions & 25200 deletions

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 20
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 22
19+
cache: npm
20+
21+
- name: Install packages
22+
run: npm install
23+
24+
- name: Typecheck
25+
run: npm run typecheck
26+
27+
- name: Bundle
28+
# Metro resolves and compiles every import, so this catches a broken
29+
# @gpujs/expo-gl, a gpu.js that will not bundle for React Native, and
30+
# version drift against the Expo SDK. It cannot catch anything that
31+
# only shows up against a real GL context — that needs a device, which
32+
# is what this app is for.
33+
run: npm run bundle

.gitignore

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1-
node_modules/**/*
2-
.expo/*
3-
npm-debug.*
1+
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2+
3+
# dependencies
4+
node_modules/
5+
6+
# Expo
7+
.expo/
8+
dist/
9+
web-build/
10+
expo-env.d.ts
11+
12+
# Native
13+
.kotlin/
14+
*.orig.*
415
*.jks
516
*.p8
617
*.p12
718
*.key
819
*.mobileprovision
9-
*.orig.*
10-
web-build/
11-
web-report/
20+
21+
# Metro
22+
.metro-health-check*
23+
24+
# debug
25+
npm-debug.*
26+
yarn-debug.*
27+
yarn-error.*
1228

1329
# macOS
1430
.DS_Store
31+
*.pem
32+
33+
# local env files
34+
.env*.local
35+
36+
# typescript
37+
*.tsbuildinfo
38+
39+
# generated native folders
40+
/ios
41+
/android

App.tsx

Lines changed: 144 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,156 @@
1-
import React, { Component, Fragment } from 'react';
2-
import { Button, StyleSheet, Text, View } from 'react-native';
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import { Button, ScrollView, StyleSheet, Text, View } from 'react-native';
3+
import { StatusBar } from 'expo-status-bar';
34
import { GLView } from 'expo-gl';
4-
import { GPU, IKernelRunShortcut } from '@gpujs/expo-gl';
5-
6-
async function createVanillaKernel() {
7-
const context = await GLView.createContextAsync();
8-
const gpu = new GPU({ context });
9-
return gpu.createKernel(function () {
10-
return Math.random();
11-
}, {
12-
output: [2048, 2048],
13-
});
14-
}
5+
import { GPU } from '@gpujs/expo-gl';
6+
7+
// Deliberately a diagnostic rather than a minimal demo: each stage of bringing
8+
// GPU.js up on Expo is reported separately, so a failure on a device says which
9+
// stage broke rather than just "it didn't work".
10+
type StageState = 'pending' | 'running' | 'done' | 'failed';
1511

16-
interface IAppState {
17-
error: any,
18-
kernel: IKernelRunShortcut,
19-
milliseconds: number,
20-
kernelResult: number[][],
21-
kernelRunning: boolean,
12+
interface Stage {
13+
name: string;
14+
state: StageState;
15+
detail?: string;
2216
}
2317

24-
export default class App extends Component<any, IAppState> {
25-
setError = error => {
26-
this.setState({ error });
27-
};
28-
29-
setKernel = async kernel => {
30-
this.setState({ kernel });
31-
};
32-
33-
runKernel = () => {
34-
this.setState({
35-
kernelRunning: true,
36-
});
37-
const start = Date.now();
38-
const result = this.state.kernel();
39-
const milliseconds = Date.now() - start;
40-
this.setState({
41-
kernelResult: result as number[][],
42-
milliseconds,
43-
kernelRunning: false,
44-
});
45-
};
46-
47-
constructor(props) {
48-
super(props);
49-
this.state = {
50-
error: null,
51-
kernel: null,
52-
kernelResult: null,
53-
milliseconds: null,
54-
kernelRunning: false,
55-
};
56-
}
18+
const STAGES = [
19+
'Create GL context',
20+
'Create GPU',
21+
'Detect features',
22+
'Compile kernel',
23+
'Run kernel',
24+
] as const;
5725

58-
componentDidMount(): void {
59-
createVanillaKernel()
60-
.catch(this.setError)
61-
.then(this.setKernel);
62-
}
26+
const SIZE = 512;
27+
28+
export default function App() {
29+
const [stages, setStages] = useState<Stage[]>(
30+
STAGES.map(name => ({ name, state: 'pending' }))
31+
);
32+
const [kernel, setKernel] = useState<((...args: any[]) => any) | null>(null);
33+
const [result, setResult] = useState<string | null>(null);
34+
const [milliseconds, setMilliseconds] = useState<number | null>(null);
35+
36+
const update = useCallback((index: number, state: StageState, detail?: string) => {
37+
setStages(current => current.map((stage, i) => (
38+
i === index ? { ...stage, state, detail } : stage
39+
)));
40+
}, []);
41+
42+
useEffect(() => {
43+
let cancelled = false;
44+
45+
(async () => {
46+
let stage = 0;
47+
try {
48+
update(stage, 'running');
49+
const context = await GLView.createContextAsync();
50+
if (cancelled) return;
51+
update(stage, 'done', `contextId ${(context as any).contextId}`);
52+
53+
stage = 1;
54+
update(stage, 'running');
55+
const gpu = new GPU({ context });
56+
update(stage, 'done', gpu.Kernel?.name ?? 'kernel selected');
57+
58+
stage = 2;
59+
update(stage, 'running');
60+
// features are detected lazily, and detection itself compiles and runs
61+
// a probe kernel — so this stage exercises real GL work
62+
const features = (gpu.Kernel as any).features;
63+
update(stage, 'done', `float read ${features?.isFloatRead}, max texture ${features?.maxTextureSize}`);
6364

64-
render() {
65-
const {
66-
error,
67-
kernel,
68-
kernelResult,
69-
milliseconds,
70-
kernelRunning,
71-
} = this.state;
72-
73-
let children;
74-
75-
if (error) {
76-
children = <Text>There was an error { error.toString() }</Text>;
77-
} else if (!kernel) {
78-
children = <Text>Loading context</Text>;
79-
} else {
80-
children = <Fragment>
81-
<Button
82-
disabled={kernelRunning}
83-
onPress={this.runKernel}
84-
title="Tap to run Kernel"
85-
/>
86-
{
87-
kernelResult
88-
? <Text>Kernel calculated with length of { kernelResult.length * kernelResult[0].length } and took { milliseconds } milliseconds</Text>
89-
: null
90-
}
91-
</Fragment>
65+
stage = 3;
66+
update(stage, 'running');
67+
const built = gpu.createKernel(function (a: number[][], b: number[][]) {
68+
let sum = 0;
69+
for (let i = 0; i < 512; i++) {
70+
sum += a[this.thread.y][i] * b[i][this.thread.x];
71+
}
72+
return sum;
73+
}).setOutput([SIZE, SIZE]);
74+
update(stage, 'done', `${SIZE}x${SIZE} matrix multiply`);
75+
76+
if (!cancelled) setKernel(() => built);
77+
} catch (error: any) {
78+
if (!cancelled) update(stage, 'failed', String(error?.message ?? error));
79+
}
80+
})();
81+
82+
return () => { cancelled = true; };
83+
}, [update]);
84+
85+
const run = useCallback(() => {
86+
if (!kernel) return;
87+
update(4, 'running');
88+
try {
89+
const a = Array.from({ length: SIZE }, () => Array.from({ length: SIZE }, () => Math.random()));
90+
const b = Array.from({ length: SIZE }, () => Array.from({ length: SIZE }, () => Math.random()));
91+
const start = Date.now();
92+
const output = kernel(a, b) as number[][];
93+
const elapsed = Date.now() - start;
94+
setMilliseconds(elapsed);
95+
setResult(`${output.length}x${output[0].length}, [0][0] = ${output[0][0].toFixed(4)}`);
96+
update(4, 'done', `${elapsed} ms`);
97+
} catch (error: any) {
98+
update(4, 'failed', String(error?.message ?? error));
9299
}
93-
return (<View style={styles.container}>{children}</View>);
100+
}, [kernel, update]);
101+
102+
const failed = stages.some(stage => stage.state === 'failed');
103+
104+
return (
105+
<View style={styles.container}>
106+
<StatusBar style="auto" />
107+
<Text style={styles.title}>GPU.js on Expo</Text>
108+
<ScrollView style={styles.stages} contentContainerStyle={styles.stagesContent}>
109+
{stages.map(stage => (
110+
<View key={stage.name} style={styles.stage}>
111+
<Text style={styles.marker}>{marker(stage.state)}</Text>
112+
<View style={styles.stageText}>
113+
<Text style={stage.state === 'failed' ? styles.failed : styles.name}>{stage.name}</Text>
114+
{stage.detail ? <Text style={styles.detail}>{stage.detail}</Text> : null}
115+
</View>
116+
</View>
117+
))}
118+
</ScrollView>
119+
120+
<Button title={kernel ? 'Run kernel' : 'Preparing…'} onPress={run} disabled={!kernel} />
121+
122+
{result ? (
123+
<Text style={styles.result}>{result}{milliseconds !== null ? ` in ${milliseconds} ms` : ''}</Text>
124+
) : null}
125+
{failed ? (
126+
<Text style={styles.hint}>
127+
Report this at github.com/gpujs/expo-gl/issues, including the failing stage above.
128+
</Text>
129+
) : null}
130+
</View>
131+
);
132+
}
133+
134+
function marker(state: StageState) {
135+
switch (state) {
136+
case 'done': return '✓';
137+
case 'failed': return '✗';
138+
case 'running': return '…';
139+
default: return '·';
94140
}
95141
}
96142

97143
const styles = StyleSheet.create({
98-
container: {
99-
flex: 1,
100-
backgroundColor: '#fff',
101-
alignItems: 'center',
102-
justifyContent: 'center',
103-
},
144+
container: { flex: 1, backgroundColor: '#fff', padding: 24, paddingTop: 72 },
145+
title: { fontSize: 22, fontWeight: '600', marginBottom: 20 },
146+
stages: { flexGrow: 0, marginBottom: 20 },
147+
stagesContent: { gap: 12 },
148+
stage: { flexDirection: 'row', gap: 10 },
149+
marker: { width: 16, fontSize: 15 },
150+
stageText: { flex: 1 },
151+
name: { fontSize: 15 },
152+
failed: { fontSize: 15, color: '#b00020', fontWeight: '600' },
153+
detail: { fontSize: 12, color: '#666', marginTop: 2 },
154+
result: { marginTop: 16, fontSize: 14 },
155+
hint: { marginTop: 16, fontSize: 12, color: '#666' },
104156
});

README.md

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
1-
# expo-gl-example
2-
A GPU.js native example used with https://github.com/gpujs/expo-gl
3-
Created using `expo init` with minimal working example.
1+
# GPU.js on Expo — example
2+
3+
[![CI](https://github.com/gpujs/expo-gl-example/actions/workflows/ci.yml/badge.svg)](https://github.com/gpujs/expo-gl-example/actions/workflows/ci.yml)
4+
5+
A minimal Expo app that runs [GPU.js](https://gpu.rocks) kernels on the device
6+
GPU through [`@gpujs/expo-gl`](https://github.com/gpujs/expo-gl).
7+
8+
## Running it
9+
10+
```sh
11+
npm install
12+
npx expo start
13+
```
14+
15+
Then open it on a device or simulator (`i` for iOS, `a` for Android). Expo Go is
16+
enough — no custom native build is required.
17+
18+
The app is deliberately a **diagnostic** rather than a minimal demo. It reports
19+
each stage separately:
20+
21+
1. **Create GL context**`GLView.createContextAsync()`
22+
2. **Create GPU**`new GPU({ context })`, which selects the Expo kernel
23+
3. **Detect features** — compiles and runs a probe kernel, so this is the first
24+
real GL work
25+
4. **Compile kernel** — a 512×512 matrix multiply
26+
5. **Run kernel** — press the button; reports elapsed milliseconds
27+
28+
If something breaks, the failing stage and its error appear on screen, which is
29+
far more useful in a bug report than "it didn't work".
30+
31+
## Web is not supported
32+
33+
`@gpujs/expo-gl` targets Expo's native GL. On web, use GPU.js directly — it
34+
already runs on WebGL there.
35+
36+
## What CI can and cannot check
37+
38+
CI typechecks and runs `expo export`, which makes Metro resolve and compile
39+
every import. That catches a broken shim, a GPU.js that will not bundle for
40+
React Native, and version drift against the Expo SDK.
41+
42+
It cannot run a kernel: there is no Node implementation of Expo's GL. Anything
43+
past "it bundles" has to be verified on a device — which is the reason this app
44+
exists.

0 commit comments

Comments
 (0)