perf(capture): render exports off the shutter path via FIFO background queue

This commit is contained in:
2026-09-09 12:22:07 +07:00
parent bd7efce600
commit f588849ac5
+38 -9
View File
@@ -95,6 +95,22 @@ export default function App() {
const [shutterSound, setShutterSound] = useState<ShutterSound>('on'); const [shutterSound, setShutterSound] = useState<ShutterSound>('on');
const photoSeq = useRef(0); const photoSeq = useRef(0);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
// Short shutter-burst lock: guards the capturePhotoToFile calls from
// overlapping. Released as soon as the frames are written — NOT after the
// render, so a new shot can start while the previous photo is still being
// processed in the background.
const captureBusyRef = useRef(false);
// FIFO background render queue: processAndExportPhoto (Skia render + gallery
// save, ~1s) runs OFF the shutter path, one at a time, so the next photo can
// be taken immediately. FIFO keeps history order == shutter order. Renders
// are heavy on the JS thread but yield between awaits — UI keeps responding.
const exportQueueRef = useRef<Promise<void>>(Promise.resolve());
const enqueueExport = (task: () => Promise<void>) => {
exportQueueRef.current = exportQueueRef.current
.catch(() => {}) // a failed render must not stall the queue
.then(task)
.catch((e) => console.error('Background export failed:', e));
};
// Hardware Permissions (vision-camera) // Hardware Permissions (vision-camera)
const { hasPermission: cameraPermissionGranted, requestPermission: requestCameraPermission } = useCameraPermission(); const { hasPermission: cameraPermissionGranted, requestPermission: requestCameraPermission } = useCameraPermission();
@@ -435,7 +451,7 @@ export default function App() {
Alert.alert('Camera Not Ready', 'Please wait for the camera to start, then try again.'); Alert.alert('Camera Not Ready', 'Please wait for the camera to start, then try again.');
return; return;
} }
setIsProcessing(true); captureBusyRef.current = true;
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
try { try {
// A photo taken now is at the device's current spot — resolve device GPS // A photo taken now is at the device's current spot — resolve device GPS
@@ -449,7 +465,6 @@ export default function App() {
} }
} }
const tShot = Date.now();
// Hardware AE bias for this shot (best-effort, bounded retries inside). // Hardware AE bias for this shot (best-effort, bounded retries inside).
// Applied here — right before the capture — instead of live during EV // Applied here — right before the capture — instead of live during EV
// slider drags, which re-locked AE on the Xiaomi and flashed preview. // slider drags, which re-locked AE on the Xiaomi and flashed preview.
@@ -478,8 +493,15 @@ export default function App() {
} }
} }
const tExp = Date.now(); // Snapshot the look at shutter time, then render OFF the shutter path:
const result = await processAndExportPhoto( // processAndExportPhoto (~1s Skia render + gallery save) is enqueued on
// the FIFO background queue — the shutter returns in tens of ms and the
// next shot can be taken immediately; no full-screen RENDERING overlay.
// Finished photos land in history (and the gallery) when their turn
// completes; the thumbnail button shows each as it arrives.
const recipeName = activeRecipe.name;
const render = () =>
processAndExportPhoto(
sourceUri, sourceUri,
{ ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag }, { ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag },
selectedFrame, selectedFrame,
@@ -495,13 +517,15 @@ export default function App() {
aspect: aspectRatio === 'full' ? undefined : aspectRatio, aspect: aspectRatio === 'full' ? undefined : aspectRatio,
} }
); );
enqueueExport(async () => {
try {
const result = await render();
if (result) { if (result) {
// No auto preview after a capture — the thumbnail button opens the // No auto preview after a capture — the thumbnail button opens
// swipe viewer. Saved to session history either way. // the swipe viewer. Saved to session history either way.
pushPhoto({ pushPhoto({
uri: result.uri, uri: result.uri,
recipeName: activeRecipe.name, recipeName,
savedToLibrary: result.savedToLibrary, savedToLibrary: result.savedToLibrary,
assetId: result.assetId, assetId: result.assetId,
}); });
@@ -509,11 +533,16 @@ export default function App() {
} else { } else {
Alert.alert('Failed', 'Error occurred while processing the captured photo.'); Alert.alert('Failed', 'Error occurred while processing the captured photo.');
} }
} catch (error) {
console.error('Camera export failed:', error);
Alert.alert('Failed', 'Error occurred while processing the captured photo.');
}
});
} catch (error) { } catch (error) {
console.error('Camera capture failed:', error); console.error('Camera capture failed:', error);
Alert.alert('Capture Failed', 'Could not take the photo. See logs for details.'); Alert.alert('Capture Failed', 'Could not take the photo. See logs for details.');
} finally { } finally {
setIsProcessing(false); captureBusyRef.current = false;
} }
return; return;
} }