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
+62 -33
View File
@@ -95,6 +95,22 @@ export default function App() {
const [shutterSound, setShutterSound] = useState<ShutterSound>('on');
const photoSeq = useRef(0);
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)
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.');
return;
}
setIsProcessing(true);
captureBusyRef.current = true;
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
try {
// 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).
// Applied here — right before the capture — instead of live during EV
// slider drags, which re-locked AE on the Xiaomi and flashed preview.
@@ -478,42 +493,56 @@ export default function App() {
}
}
const tExp = Date.now();
const result = await processAndExportPhoto(
sourceUri,
{ ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag },
selectedFrame,
useGeotag,
captureGps,
// 300dpi header + full-res screen sharpen. evFromCamera: the AE bias
// already applied exposure compensation at capture — don't add 2^EV.
// aspect: crop to the on-screen composition ratio (full = keep 4:3 native).
{
dpi: 300,
sharpen: true,
evFromCamera: true,
aspect: aspectRatio === 'full' ? undefined : aspectRatio,
// Snapshot the look at shutter time, then render OFF the shutter path:
// 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,
{ ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag },
selectedFrame,
useGeotag,
captureGps,
// 300dpi header + full-res screen sharpen. evFromCamera: the AE bias
// already applied exposure compensation at capture — don't add 2^EV.
// aspect: crop to the on-screen composition ratio (full = keep 4:3 native).
{
dpi: 300,
sharpen: true,
evFromCamera: true,
aspect: aspectRatio === 'full' ? undefined : aspectRatio,
}
);
enqueueExport(async () => {
try {
const result = await render();
if (result) {
// No auto preview after a capture — the thumbnail button opens
// the swipe viewer. Saved to session history either way.
pushPhoto({
uri: result.uri,
recipeName,
savedToLibrary: result.savedToLibrary,
assetId: result.assetId,
});
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else {
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.');
}
);
if (result) {
// No auto preview after a capture — the thumbnail button opens the
// swipe viewer. Saved to session history either way.
pushPhoto({
uri: result.uri,
recipeName: activeRecipe.name,
savedToLibrary: result.savedToLibrary,
assetId: result.assetId,
});
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else {
Alert.alert('Failed', 'Error occurred while processing the captured photo.');
}
});
} catch (error) {
console.error('Camera capture failed:', error);
Alert.alert('Capture Failed', 'Could not take the photo. See logs for details.');
} finally {
setIsProcessing(false);
captureBusyRef.current = false;
}
return;
}