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 [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,42 +493,56 @@ 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
|
||||||
sourceUri,
|
// the FIFO background queue — the shutter returns in tens of ms and the
|
||||||
{ ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag },
|
// next shot can be taken immediately; no full-screen RENDERING overlay.
|
||||||
selectedFrame,
|
// Finished photos land in history (and the gallery) when their turn
|
||||||
useGeotag,
|
// completes; the thumbnail button shows each as it arrives.
|
||||||
captureGps,
|
const recipeName = activeRecipe.name;
|
||||||
// 300dpi header + full-res screen sharpen. evFromCamera: the AE bias
|
const render = () =>
|
||||||
// already applied exposure compensation at capture — don't add 2^EV.
|
processAndExportPhoto(
|
||||||
// aspect: crop to the on-screen composition ratio (full = keep 4:3 native).
|
sourceUri,
|
||||||
{
|
{ ...activeRecipe, adjustments: renderAdj, frameId: selectedFrame, useGeotag },
|
||||||
dpi: 300,
|
selectedFrame,
|
||||||
sharpen: true,
|
useGeotag,
|
||||||
evFromCamera: true,
|
captureGps,
|
||||||
aspect: aspectRatio === 'full' ? undefined : aspectRatio,
|
// 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) {
|
} 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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user