perf(capture): trade CameraX MFNR for latency, average our own low-light burst off the shutter path
Measured on the 12S Ultra (2203121C), same scene, 1x, EV 0, 3 shots per arm:
- qualityPrioritization 'quality' -> 'balanced': tap->file 1013 ms vs 736 ms
(-277 ms, -27%), settled 1552 ms vs 1280 ms, in-camera 521 ms vs 286 ms.
Repeat on a second session: -228 ms. Image quality at ISO ~450 is inside the
scatter of one arm (flat sigma +2.4%, edge +0.1%, texture -1.1%), so the MFNR
pass quality would have bought is not measurable here. Also removes the
EXPO_PUBLIC_CAPTURE_MODE seam: Metro inlines EXPO_PUBLIC_* in dev only, the
release HBC build does not, so it could never gate shipped behaviour.
- Dark scenes now go through the app's own burst (ISO >= 100 gate for the probe)
and mergeBurst runs in the export queue instead of on the shutter path, so a
second shot is accepted while the frames are still being averaged: double-tap
at 1.2 s used to drop the second photo, now it lands. Output stays the merged
frame (flat sigma 0.825 vs 0.939 for a single frame).
Burst is skipped when the LED is the light source, when the shot came from the
native 0.5x session, for a held burst, for RAW, and below the devicePerf RAM
floor (deviceFloor.ts) where three raws plus the alignment rasters would be
seconds of shutter lag.
Bundles in the work this sits on: frame averaging (burstAlign/burstMerge),
the per-device floor, and the EXIF/ISO readback the burst gate needs (exifWrite,
photoMeta, exportEngine, RecipescamExportModule).
Self-checks: src/utils/{burstAlign,deviceFloor,exifWrite}.check.ts.
This commit is contained in:
+15
@@ -1,5 +1,6 @@
|
||||
package com.locphamtran.recipescamera.export
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -206,6 +207,20 @@ class RecipescamExportModule : Module() {
|
||||
)
|
||||
}
|
||||
|
||||
// Floor-device gate (src/utils/deviceFloor.ts): total RAM decides whether a
|
||||
// full-resolution surface chain and the 3-frame burst fit. One cheap query
|
||||
// and no permission; a platform that will not answer returns 0, which JS
|
||||
// reads as "unknown" and keeps the full-quality path.
|
||||
Function("devicePerf") { ->
|
||||
val am = appContext.reactContext?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
|
||||
val mem = ActivityManager.MemoryInfo()
|
||||
val known = if (am != null) { am.getMemoryInfo(mem); true } else false
|
||||
mapOf(
|
||||
"ramMb" to (if (known) (mem.totalMem / (1024L * 1024L)).toInt() else 0),
|
||||
"cores" to Runtime.getRuntime().availableProcessors(),
|
||||
)
|
||||
}
|
||||
|
||||
// P0 spike harness: copy a bundled drawable asset (e.g. "wallframe", the
|
||||
// 3117x4000 artwork — representative 12MP decode) into cacheDir and hand the
|
||||
// real file path back to JS, which then runs decodeEncodeAsync on it. This
|
||||
|
||||
@@ -105,6 +105,13 @@ export interface RecipescamExportModule {
|
||||
* codename, marketing name, manufacturer, model. Empty strings when absent.
|
||||
*/
|
||||
deviceInfo(): { device: string; marketName: string; manufacturer: string; model: string };
|
||||
/**
|
||||
* Device facts the capture and render paths budget against (see
|
||||
* src/utils/deviceFloor.ts): total RAM in MB and CPU core count. `ramMb` is 0
|
||||
* when the platform refused the read — callers treat 0 as "unknown", never as
|
||||
* a floor device, so an unmeasurable phone keeps the full-quality path.
|
||||
*/
|
||||
devicePerf(): { ramMb: number; cores: number };
|
||||
|
||||
/**
|
||||
* The image a system share handed this launch (AndroidManifest
|
||||
|
||||
Reference in New Issue
Block a user