Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { AsyncStarvation } from "./Diagnostics/AsyncStarvation";
import { DeviceLostHang } from "./Diagnostics/DeviceLostHang";
import { DiagnosticsList } from "./Diagnostics/DiagnosticsList";
import { WorkletRequestAdapter } from "./Diagnostics/WorkletRequestAdapter";
import { ReloadLifecycle } from "./Diagnostics/ReloadLifecycle";
import { ContextEdgeCases } from "./Diagnostics/ContextEdgeCases";
import { ViewFormatsUseAfterFree } from "./Diagnostics/ViewFormatsUseAfterFree";
import { RenderAfterUnmount } from "./Diagnostics/RenderAfterUnmount";
Expand Down Expand Up @@ -113,6 +114,7 @@ function App() {
name="WorkletRequestAdapter"
component={WorkletRequestAdapter}
/>
<Stack.Screen name="ReloadLifecycle" component={ReloadLifecycle} />
<Stack.Screen name="ContextEdgeCases" component={ContextEdgeCases} />
<Stack.Screen
name="ViewFormatsUseAfterFree"
Expand Down
4 changes: 4 additions & 0 deletions apps/example/src/Diagnostics/DiagnosticsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const tests = [
screen: "WorkletRequestAdapter",
title: "⚠️ Worklet requestAdapter",
},
{
screen: "ReloadLifecycle",
title: "⚠️ Runtime Reload Lifecycle",
},
{
screen: "ContextEdgeCases",
title: "⚠️ Context Edge Cases",
Expand Down
139 changes: 139 additions & 0 deletions apps/example/src/Diagnostics/ReloadLifecycle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React, { useEffect, useRef, useState } from "react";
import { DevSettings, Pressable, StyleSheet, Text, View } from "react-native";

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
justifyContent: "center",
backgroundColor: "#111",
},
card: {
padding: 20,
gap: 12,
borderRadius: 12,
borderCurve: "continuous",
backgroundColor: "#1e1e1e",
},
title: {
color: "#f5f5f5",
fontWeight: "600",
},
paragraph: {
color: "#f5f5f5",
lineHeight: 20,
},
error: {
color: "#ffb347",
lineHeight: 20,
},
button: {
alignItems: "center",
padding: 12,
borderRadius: 8,
borderCurve: "continuous",
backgroundColor: "#2563eb",
},
buttonDisabled: {
backgroundColor: "#475569",
},
buttonText: {
color: "white",
fontWeight: "600",
},
note: {
color: "#aaa",
lineHeight: 18,
},
});

const createPendingPipeline = (device: GPUDevice) =>
device.createComputePipelineAsync({
label: "reload-lifecycle-pending-pipeline",
layout: "auto",
compute: {
module: device.createShaderModule({
code: "@compute @workgroup_size(1) fn main() {}",
}),
entryPoint: "main",
},
});

export const ReloadLifecycle = () => {
const deviceRef = useRef<GPUDevice | null>(null);
const [status, setStatus] = useState("Requesting adapter…");
const [error, setError] = useState<string | null>(null);

useEffect(() => {
let cancelled = false;

const prepare = async () => {
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("Failed to acquire a GPU adapter.");
}

const device = await adapter.requestDevice({
label: "reload-lifecycle-probe",
});
if (cancelled) {
return;
}

deviceRef.current = device;
setStatus("WebGPU is ready. Reload can now be triggered.");

// Keep a native callback registered so runtime teardown has to cancel it.
void device.lost.catch(() => undefined);
} catch (cause) {
if (!cancelled) {
setError(cause instanceof Error ? cause.message : String(cause));
}
}
};

void prepare();
return () => {
cancelled = true;
deviceRef.current = null;
};
}, []);

const reload = () => {
const device = deviceRef.current;
if (!device) {
return;
}

// Do not await these operations: reload must invalidate their callbacks while
// they are still associated with the current JavaScript runtime.
void createPendingPipeline(device).catch(() => undefined);
void device.queue.onSubmittedWorkDone().catch(() => undefined);
DevSettings.reload("react-native-webgpu lifecycle regression");
};

const ready = deviceRef.current !== null;

return (
<View style={styles.container}>
<View style={styles.card}>
<Text style={styles.title}>Runtime Reload Lifecycle</Text>
<Text style={styles.paragraph}>{status}</Text>
{error ? <Text style={styles.error}>{error}</Text> : null}
<Pressable
accessibilityRole="button"
disabled={!ready}
onPress={reload}
style={[styles.button, !ready ? styles.buttonDisabled : null]}
>
<Text style={styles.buttonText}>Start async work and reload</Text>
</Pressable>
<Text style={styles.note}>
After reload, open this screen again. A healthy installation acquires
a fresh adapter and device and reaches the ready state.
</Text>
</View>
</View>
);
};
1 change: 1 addition & 0 deletions apps/example/src/Route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type Routes = {
AsyncStarvation: undefined;
DeviceLostHang: undefined;
WorkletRequestAdapter: undefined;
ReloadLifecycle: undefined;
ContextEdgeCases: undefined;
ViewFormatsUseAfterFree: undefined;
RenderAfterUnmount: undefined;
Expand Down
Loading