perf(capture): render exports off the shutter path via FIFO background queue
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user