Skip to content
Merged
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
27 changes: 23 additions & 4 deletions src/components/ShaderView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PixelRatio, StyleSheet } from 'react-native';
import { AppState, PixelRatio, StyleSheet } from 'react-native';
import { Canvas, installWebGPU } from 'react-native-webgpu';
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { createSynchronizable, scheduleOnRuntime } from 'react-native-worklets';
import { colorToVec4 } from '../../utils/colors';
import { useWGPUSetup } from '../../hooks/useWGPUSetup';
Expand Down Expand Up @@ -31,6 +31,21 @@ export default function ShaderView({
}: ShaderViewProps) {
const { canvasRef, runtime, resources, onCanvasLayout } = useWGPUSetup();

// Pause the render loop while the app is backgrounded. The rAF loop otherwise
// keeps churning frames against a surface that's offscreen (and often
// transiently invalid), wasting battery/GPU. We only pause on a true
// 'background' transition — iOS 'inactive' (app switcher peek, notification
// pulldown) is left running to avoid flicker on brief, foreground interruptions.
const [appActive, setAppActive] = useState(
() => (AppState.currentState ?? 'active') !== 'background'
);
useEffect(() => {
const subscription = AppState.addEventListener('change', (state) => {
setAppActive(state !== 'background');
});
return () => subscription.remove();
}, []);

const propsSync = useRef(
createSynchronizable<Float64Array>(new Float64Array(SYNC_SIZE))
).current;
Expand Down Expand Up @@ -81,9 +96,12 @@ export default function ShaderView({
};
}, [propsSync]);

// Start render loop when GPU resources are ready
// Start render loop when GPU resources are ready and the app is foregrounded.
// When the app backgrounds, `appActive` flips false and this effect's cleanup
// tears the loop down (via the `cancelled` token below); on return to the
// foreground it re-runs and starts a fresh loop.
useEffect(() => {
if (!resources) {
if (!resources || !appActive) {
return;
}

Expand Down Expand Up @@ -285,6 +303,7 @@ export default function ShaderView({
};
}, [
resources,
appActive,
runtime,
propsSync,
paramsSynchronizable,
Expand Down
Loading