diff --git a/.omm/data-flow/description.md b/.omm/data-flow/description.md index 67904594..e7a517fe 100644 --- a/.omm/data-flow/description.md +++ b/.omm/data-flow/description.md @@ -6,6 +6,8 @@ JPEG에서 YUV를 고르면 앱이 현재 YUV 크기의 프레임을 JPEG로 변 RecentMediaThumbnail은 MediaStore 변경·화면 복귀 시 저장 완료 항목을 조회합니다. 별도 스레드에서 디코딩하고 활성 화면의 최신 결과만 RecentMediaButton에 반영합니다. +LIVE Depth는 ImageReader의 DEPTH16을 DepthFrame에서 표시 표본으로 바꾸고 DepthMapView로 전달합니다. Telemetry에는 크기·시각·형식과 capture 콜백만 남깁니다. 표시 픽셀은 저장하지 않으며 벤치마크 조립기로 전달하지 않습니다. + CLI는 직접 인자 또는 base64url JSON을 받고 shell UID·허용 설정·프로토콜을 검사합니다. CommandStore는 실행 전 요청을 저장합니다. 저장 완료 뒤 CliArtifactRegistry가 계산한 artifact 크기·SHA-256과 원본 URI를 CommandCoordinator가 기록하며, PC에는 URI 없이 결과와 등록된 artifact ID의 읽기 전용 파일을 제공합니다. ADB 스크립트는 manifest의 이름·크기·SHA-256을 검증하고 Download/HALCamera-cli/요청ID에 파일을 준비해 adb pull을 안내합니다. Python CLI도 검증한 임시 파일만 공개합니다. 벤치마크 JSON은 기기 안에서만 생성하며, files/benchmarks와 baseline 목록으로 비교합니다. 외부 JSON 가져오기는 0.13.0에서 제거했습니다. diff --git a/.omm/overall-architecture/camera-engines/depth-engine/description.md b/.omm/overall-architecture/camera-engines/depth-engine/description.md new file mode 100644 index 00000000..fac2b26d --- /dev/null +++ b/.omm/overall-architecture/camera-engines/depth-engine/description.md @@ -0,0 +1,7 @@ +DepthCameraEngine은 LIVE의 DEPTH16 전용 세션을 소유합니다. 공개 ID의 DEPTH_OUTPUT과 선택한 해상도를 재검사하고 ImageReader 하나만 구성합니다. 이전 엔진이 놓은 카메라는 AvailabilityCallback 또는 최대 2초의 대기 뒤 엽니다. 컬러 출력이 없으므로 DEPTH_DEPTH_IS_EXCLUSIVE인 카메라도 대상으로 삼을 수 있습니다. MediaCapture는 구현하지 않으며 촬영과 녹화를 제공하지 않습니다. + +ImageReader는 acquireLatestImage로 버퍼를 읽고 use로 즉시 돌려줍니다. DepthFrame은 crop·rowStride·pixelStride와 little-endian 16비트 표본을 해석합니다. 하위 13비트는 mm 거리이며 상위 3비트는 신뢰도입니다. 신뢰도 코드 0은 100%, 1은 0%입니다. 거리가 0이거나 신뢰도가 50% 미만이면 검게 표시하고, 나머지는 고정된 0–8191 mm 범위에서 가까울수록 밝게 표시합니다. 긴 변은 최대 640픽셀로 표본을 추출하고 UI 전달 대기는 한 프레임으로 제한하며 최대 초당 10회 갱신합니다. + +DepthMapView는 센서·디스플레이 회전과 전면 반전을 적용해 전체 맵을 fit으로 표시합니다. 중앙 거리와 표시 표본의 유효 비율을 함께 보여줍니다. 표시용 픽셀은 저장하지 않으며 Telemetry에는 크기·시각·형식과 Depth 콜백만 기록합니다. + +세션 거절·버퍼 오류·카메라 오류는 상태 메시지로 보고하고 장치를 닫습니다. repeating request 제출 후 8초 이내 첫 표시용 프레임이 없으면 실패로 처리합니다. close(done)는 열기 대기 중인 장치도 onOpened에서 닫고 onClosed 이후 자원을 회수한 뒤 UI 스레드에서 완료를 전달합니다. 이미 해제된 엔진의 close도 완료를 전달합니다. diff --git a/.omm/overall-architecture/camera-engines/depth-engine/meta.yaml b/.omm/overall-architecture/camera-engines/depth-engine/meta.yaml new file mode 100644 index 00000000..fd4b1cdf --- /dev/null +++ b/.omm/overall-architecture/camera-engines/depth-engine/meta.yaml @@ -0,0 +1,5 @@ +kind: nested-class +title: depth-engine +children: [] +parentPath: + - camera-engines diff --git a/.omm/overall-architecture/camera-engines/description.md b/.omm/overall-architecture/camera-engines/description.md index b6ef2165..fdd8a30e 100644 --- a/.omm/overall-architecture/camera-engines/description.md +++ b/.omm/overall-architecture/camera-engines/description.md @@ -1,2 +1,2 @@ -camera/는 CameraEngine 인터페이스와 Camera2Engine·CameraXEngine을 제공합니다. Camera2Engine은 기본 크기 선택 경로와 벤치마크용 명시적 StreamSpec 경로를 구분합니다. 세션별 콜백과 close(done) 완료 통지가 러너에 연결됩니다. +camera/는 CameraEngine 인터페이스와 Camera2Engine·CameraXEngine·DepthCameraEngine을 제공합니다. Camera2Engine은 기본 크기 선택 경로와 벤치마크용 명시적 StreamSpec 경로를 구분합니다. 세션별 콜백과 close(done) 완료 통지가 러너에 연결됩니다. StreamConfiguration은 실제 카메라 출력으로 Callback 목록을 만듭니다. 앱의 YUV JPEG 변환·PIP 합성·GPU 인코더 출력은 Callback에 추가하지 않습니다. diff --git a/.omm/overall-architecture/camera-engines/meta.yaml b/.omm/overall-architecture/camera-engines/meta.yaml index 35fe5d45..f6744b90 100644 --- a/.omm/overall-architecture/camera-engines/meta.yaml +++ b/.omm/overall-architecture/camera-engines/meta.yaml @@ -6,6 +6,7 @@ kind: nested-class title: camera-engines children: - camera2-engine + - depth-engine - camerax-engine - endpoint-model - endpoint-resolver diff --git a/.omm/overall-architecture/screens/expert-screen/description.md b/.omm/overall-architecture/screens/expert-screen/description.md index 3b561c9e..531c69f0 100644 --- a/.omm/overall-architecture/screens/expert-screen/description.md +++ b/.omm/overall-architecture/screens/expert-screen/description.md @@ -1,4 +1,4 @@ -MainActivity는 Live 런처입니다. CameraEngine을 선택하고 카메라 수명주기, 권한, 촬영 모드와 버튼 상태를 관리합니다. LiveReadings가 실시간 측정값을 갱신하고 IncidentActions가 이벤트 ZIP 저장과 공유를 담당합니다. Benchmark는 Lab에서 엽니다. +MainActivity는 Live 런처로 CameraEngine 선택·카메라 수명주기·권한·촬영 모드·버튼 상태를 관리합니다. LiveReadings는 측정값을 갱신하고 IncidentActions는 이벤트 ZIP 저장·공유를 담당합니다. Benchmark는 Lab에서 엽니다. 프리뷰의 제어·측정값·촬영 버튼 배치는 docs/design/APP-UI.md를 따릅니다. 아이콘 터치 영역은 48dp 이상이며 색·서체는 Look을 사용합니다. diff --git a/.omm/overall-architecture/screens/probe-screen/probe-model/description.md b/.omm/overall-architecture/screens/probe-screen/probe-model/description.md index a8d64bc7..64fdb9f7 100644 --- a/.omm/overall-architecture/screens/probe-screen/probe-model/description.md +++ b/.omm/overall-architecture/screens/probe-screen/probe-model/description.md @@ -1,3 +1,5 @@ camera/CameraProbe.kt는 Probe의 순수 Kotlin 모델과 렌더러입니다. ProbeRow·ProbeSection·CameraProbeEntry·CameraProbeSnapshot이 카메라별 섹션 표를 담고, 물리 카메라는 "논리ID.물리ID" 키로 구분합니다. ProbeFormat이 값의 표기(크기·범위·enum 이름)를 통일하고, CameraProbeText가 TXT를, toJsonMap이 JSON을 만듭니다. 이 파일은 Android를 모르므로 JVM 테스트로 검증합니다. camera/CameraProbeReader.kt만 CameraManager를 알고, 공개 카메라 ID와 논리 카메라의 physicalCameraIds를 순회하며 CameraCharacteristics를 섹션별로 읽어 스냅샷을 채웁니다. 섹션 하나가 예외로 실패해도 나머지를 계속 읽고 실패 목록에 남깁니다. SESSION KEYS와 MANDATORY STREAM COMBINATIONS는 API 28·29 이상에서만 읽고, STREAMS는 SCALER_STREAM_CONFIGURATION_MAP의 출력 형식마다 별도 섹션으로 만듭니다. + +DepthCapabilities는 DEPTH_OUTPUT, DEPTH16 해상도와 DEPTH_DEPTH_IS_EXCLUSIVE를 독립적으로 읽습니다. DepthSupport는 DEPTH 섹션의 지원 여부·해상도·컬러 동시 요청 가능 여부를 만듭니다. null이나 읽기 실패는 Unknown 또는 Read error로 보존하고, HAL이 동시 요청을 허용해도 출력 조합을 별도로 확인해야 한다고 표시합니다. 단독으로 열 수 없는 물리 ID는 LIVE에서 직접 선택할 수 없다고 안내합니다. 같은 행을 화면·TXT·JSON에 사용합니다. diff --git a/.omm/state-transitions/engine-lifecycle/description.md b/.omm/state-transitions/engine-lifecycle/description.md index 497a1c49..b2e01ef7 100644 --- a/.omm/state-transitions/engine-lifecycle/description.md +++ b/.omm/state-transitions/engine-lifecycle/description.md @@ -3,3 +3,5 @@ Camera2Engine은 HandlerThread에서 카메라를 열고 세션과 반복 요청 Live 녹화는 선택한 설정, Benchmark 녹화는 profile을 사용합니다. 녹화 상태는 overall-architecture/camera-engines/camera2-engine/live-recorder, 측정 순서는 runner-sequence에 있습니다. PIP 종료 전에 메인 사진 대기를 취소하여 순환 대기를 막습니다. 단계 전이의 시간 제한과 close 완료 처리는 runner-sequence에 있습니다. + +DepthCameraEngine은 DEPTH16만 구성합니다. 열기 중 취소도 onOpened→onClosed 후 완료하며 active로 늦은 신호를 차단합니다. 첫 맵은 요청 후 8초까지 기다리고 해상도 대체·일반 Camera2 재시도는 하지 않습니다. diff --git a/.omm/ui-tool-handoff/description.md b/.omm/ui-tool-handoff/description.md index a8a7c70c..f3d1ff3a 100644 --- a/.omm/ui-tool-handoff/description.md +++ b/.omm/ui-tool-handoff/description.md @@ -7,3 +7,5 @@ Lab의 시스템 뒤로 가기와 상단 Live 링크은 기존 Live로 복귀합 Live Streams는 Lab 또는 Live의 크기 표시에서 열 수 있으며 카메라를 열지 않습니다. 진입 전에 Live 엔진의 close(done)를 기다립니다. 상단·시스템 뒤로 가기는 초안을 버리고 진입한 화면으로 돌아갑니다. 저장과 직전 정상 구성 복원도 같은 화면으로 돌아갑니다. Lab은 결과를 보관했다가 Live 복귀 시 전달하며, 직접 진입은 결과를 Live에 바로 전달합니다. 설정과 직전 정상 구성은 카메라·엔진별로 분리하고 Activity 재생성 시 복원합니다. Multi도 Live의 close(done) 뒤 ConcurrentCameraActivity를 엽니다. Live에서 선택한 엔진을 전달하며 Multi 화면에서도 Camera2와 CameraX를 바꿀 수 있습니다. 이 화면은 모든 카메라의 onClosed를 기다린 뒤 Live로 돌아가며, Live는 기존 엔진 선택으로 프리뷰를 다시 엽니다. + +Depth 보기에서도 Lab 진입 전에 close(done)를 기다립니다. 촬영 모드를 바꾸지 않고 Lab에서 돌아오면 선택한 Depth 해상도를 다시 엽니다. Depth 보기 중에는 크기 표시로 Live Streams를 여는 동작을 비활성화합니다. diff --git a/.omm/ui-zoom/description.md b/.omm/ui-zoom/description.md index 42a09d04..b936e19a 100644 --- a/.omm/ui-zoom/description.md +++ b/.omm/ui-zoom/description.md @@ -2,7 +2,7 @@ Live 프리뷰에서 두 손가락을 벌리거나 모으면 지원 범위 안에서 연속으로 줌을 바꿉니다. 가장 가까운 프리셋의 원에 현재 배율을 소수 첫째 자리까지 표시하며, 펼친 상태에서 그 원을 누르면 해당 프리셋 배율로 돌아갑니다. 화면의 카메라 선택 정보를 갱신해도 중간 배율은 유지합니다. -핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다. +핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. Depth 보기, 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다. | 조건 | 동작 | | --- | --- | diff --git a/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt b/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt index ad6bb36f..1330e9d5 100644 --- a/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt +++ b/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt @@ -18,6 +18,7 @@ class CliStoreInstrumentation : Instrumentation() { private var multiSplit = false private var lintChecks = false private var splitDrag = false + private var depthChecks = false override fun onCreate(arguments: Bundle?) { super.onCreate(arguments) exportReports = arguments?.getString("export_reports") == "true" @@ -29,10 +30,20 @@ class CliStoreInstrumentation : Instrumentation() { multiSplit = arguments?.getString("multi_split") == "true" lintChecks = arguments?.getString("lint_checks") == "true" splitDrag = arguments?.getString("split_drag") == "true" + depthChecks = arguments?.getString("depth_checks") == "true" start() } override fun onStart() { + if (depthChecks) { + try { + val result = dev.halcamera.ui.DepthMapChecks.run(this) + finish(Activity.RESULT_OK, Bundle().apply { putString("stream", "$result\n") }) + } catch (error: Throwable) { + finish(Activity.RESULT_CANCELED, Bundle().apply { putString("stream", "DEPTH_MAP_CHECKS_FAILED: ${error.stackTraceToString()}\n") }) + } + return + } if (splitDrag) { try { val result = dev.halcamera.ui.SplitDragChecks.run(this) diff --git a/app/src/androidTest/java/dev/halcamera/ui/DepthMapChecks.kt b/app/src/androidTest/java/dev/halcamera/ui/DepthMapChecks.kt new file mode 100644 index 00000000..b2873c19 --- /dev/null +++ b/app/src/androidTest/java/dev/halcamera/ui/DepthMapChecks.kt @@ -0,0 +1,58 @@ +package dev.halcamera.ui + +import android.app.Instrumentation +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.widget.FrameLayout +import dev.halcamera.WorkbenchActivity +import dev.halcamera.camera.DepthFrame +import dev.halcamera.camera.LiveSize +import java.io.File +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** Synthetic DEPTH16 display validation; deliberately makes no claim about real depth camera support. */ +object DepthMapChecks { + fun run(instrumentation: Instrumentation): String { + val context = instrumentation.targetContext + val activity = instrumentation.startActivitySync(Intent(context, WorkbenchActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + val output = File(context.getExternalFilesDir(null), "depth-map-synthetic.png") + var failure: Throwable? = null + try { + instrumentation.runOnMainSync { + try { + val width = 160; val height = 120 + val buffer = ByteBuffer.allocate(width * height * 2).order(ByteOrder.LITTLE_ENDIAN) + for (y in 0 until height) for (x in 0 until width) { + val person = x in 55..105 && y in 20..100 + val mm = if (person) 1000 else 4000 + x * 20 + val code = if (x < 12) 1 else 0 + buffer.putShort(((code shl 13) or mm).toShort()) + } + buffer.flip() + val frame = DepthFrame.render(buffer, width, height, width * 2, 2) + check(frame.centerMm == 1000) + val view = DepthMapView(activity, LiveSize(width, height), 90, false) + val stage = FrameLayout(activity).apply { setBackgroundColor(android.graphics.Color.BLACK); addView(view, FrameLayout.LayoutParams(-1, -1)) } + activity.setContentView(stage) + view.setChromeInsets(Look.dp(activity, 160), Look.dp(activity, 180)) + view.bind(frame) + val metrics = context.resources.displayMetrics + stage.measure(android.view.View.MeasureSpec.makeMeasureSpec(metrics.widthPixels, android.view.View.MeasureSpec.EXACTLY), + android.view.View.MeasureSpec.makeMeasureSpec(metrics.heightPixels, android.view.View.MeasureSpec.EXACTLY)) + stage.layout(0, 0, metrics.widthPixels, metrics.heightPixels) + check(view.contentDescription.toString().contains("Center 1.000 m")) + val image = Bitmap.createBitmap(stage.width, stage.height, Bitmap.Config.ARGB_8888) + try { + stage.draw(Canvas(image)) + check((image.getPixel(stage.width / 2, stage.height / 2) and 255) > 0) + output.outputStream().use { check(image.compress(Bitmap.CompressFormat.PNG, 100, it)) } + } finally { image.recycle() } + } catch (error: Throwable) { failure = error } + } + failure?.let { throw it } + } finally { instrumentation.runOnMainSync { activity.finish() } } + return "DEPTH_MAP_CHECKS_OK · synthetic stride decode, center distance, display and accessibility · ${output.absolutePath}" + } +} diff --git a/app/src/main/java/dev/halcamera/MainActivity.kt b/app/src/main/java/dev/halcamera/MainActivity.kt index 6dea5501..0fa42b67 100644 --- a/app/src/main/java/dev/halcamera/MainActivity.kt +++ b/app/src/main/java/dev/halcamera/MainActivity.kt @@ -91,18 +91,18 @@ class MainActivity : ComponentActivity() { private lateinit var bottomBar: LinearLayout private lateinit var graphButton: Button private lateinit var callbackGraph: ResultCallbackGraph - private lateinit var zoomControl: ExpandingZoomControl + internal lateinit var zoomControl: ExpandingZoomControl internal lateinit var controlBar: LiveControlBar - private lateinit var manualPanel: ManualControlPanel + internal lateinit var manualPanel: ManualControlPanel internal var manualCapabilities = ManualSupport() private lateinit var cameraNotice: TextView private var savedNoticeShown = false private val clearNotice = Runnable { savedNoticeShown = false; if (ready || recordingVideo) cameraNotice.visibility = View.GONE } private lateinit var recentMedia: RecentMediaThumbnail - private lateinit var liveIndicator: LiveIndicator + internal lateinit var liveIndicator: LiveIndicator private var lastPreviewFrameNs = 0L private var cameraXStreaming = false - private lateinit var metrics: LiveMeasurementView + internal lateinit var metrics: LiveMeasurementView private lateinit var readings: LiveReadings private val incidents by lazy { IncidentActions(this, io, main, object : IncidentActions.Host { @@ -114,7 +114,7 @@ class MainActivity : ComponentActivity() { } private lateinit var reportButton: Button - private lateinit var mediaButton: ShutterButton + internal lateinit var mediaButton: ShutterButton private lateinit var photoModeButton: Button private lateinit var videoModeButton: Button private lateinit var multiButton: Button @@ -130,6 +130,8 @@ class MainActivity : ComponentActivity() { private var cameraEndpoints = emptyMap() private lateinit var labButton: Button internal var videoMode = false + internal var depthSize: LiveSize? = null + internal lateinit var depthModeButton: Button internal var recordingVideo = false internal var stoppingRecording = false private var recordingStartedAt = 0L @@ -145,7 +147,7 @@ class MainActivity : ComponentActivity() { if (grants.values.all { it }) action?.invoke() else toast("Allow the requested permissions to save media.") } private lateinit var engineButton: Button - private lateinit var physicalPipButton: Button + internal lateinit var physicalPipButton: Button internal var pendingPip: PipSource? = null internal val pipUi by lazy { LivePipController(this,{ previewHost },{ engine as? PipCamera },::updateMediaControls,{ lastPreviewFrameNs = nowNs() },::showNotice,{ cameraId }) } private val physicalPip by lazy { dev.halcamera.ui.PhysicalPipPicker(this,manager) } @@ -224,7 +226,7 @@ class MainActivity : ComponentActivity() { liveIndicator.bindSizes(telemetry.sessions[sessionId]?.get("negotiatedStreams") as? Map<*, *>) } readings.update(events, frames, time, sessionId, controlBar.controls) - manualPanel.bind(controlBar.controls.manual, (ready || recordingVideo) && !stoppingRecording && cli.active == null && !bursts.controlsLocked, + manualPanel.bind(controlBar.controls.manual, depthSize == null && (ready || recordingVideo) && !stoppingRecording && cli.active == null && !bursts.controlsLocked, manualCapabilities, frames.lastOrNull(), time) manualBack.isEnabled = manualPanel.isExpanded if (callbackGraph.isVisible) callbackGraph.updateLive(events,sessionId,time,telemetry.sessions,(engine as? PipCamera)?.pipCallbackSession) @@ -247,6 +249,8 @@ class MainActivity : ComponentActivity() { reconnectFromLab = savedInstanceState?.getBoolean("reconnectFromLab") ?: false zoomRatio = savedInstanceState?.getFloat("zoom") ?: 1f videoMode = savedInstanceState?.getBoolean("videoMode") ?: false + @Suppress("DEPRECATION") + depthSize = savedInstanceState?.getSerializable("depthSize") as? LiveSize @Suppress("DEPRECATION", "UNCHECKED_CAST") (savedInstanceState?.getSerializable("liveStreams") as? HashMap)?.let(streamSettings::putAll) @Suppress("DEPRECATION", "UNCHECKED_CAST") @@ -261,6 +265,7 @@ class MainActivity : ComponentActivity() { override fun onSaveInstanceState(outState: Bundle) { outState.putString("engine", engineName); outState.putString("camera", cameraId); outState.putBoolean("paused", paused); outState.putFloat("zoom", zoomRatio) outState.putBoolean("videoMode", videoMode) + outState.putSerializable("depthSize", depthSize) outState.putSerializable("liveStreams", HashMap(streamSettings)) outState.putSerializable("goodStreams", HashMap(goodStreams)) outState.putBoolean("returningFromLab", returningFromLab) @@ -343,7 +348,7 @@ class MainActivity : ComponentActivity() { ready=false; mediaButton.isEnabled=false; reportButton.isEnabled=false val old = engine; engine = null val released = engineCameraId.takeIf { old != null } - (old as? CameraXEngine)?.releaseOnClose = engineName != "CameraX" + (old as? CameraXEngine)?.releaseOnClose = engineName != "CameraX" || depthSize != null if (old != null) { closing = true; setStatus("Closing camera…", false) old.close { @@ -360,7 +365,7 @@ class MainActivity : ComponentActivity() { } if (cameraId.isEmpty()) { setStatus("No cameras available.", false); return } sessionId = UUID.randomUUID().toString() - if (pendingPip == null && engineCameraId == cameraId) pendingPip = pipUi.selected + if (depthSize == null && pendingPip == null && engineCameraId == cameraId) pendingPip = pipUi.selected previewStartup.reset() engineCameraId = cameraId val thisSession = sessionId @@ -392,7 +397,9 @@ class MainActivity : ComponentActivity() { } val notice = { text: String -> if (thisSession == sessionId && resumed && !closing) showNotice(text) } val status = { text: String, ok: Boolean -> if (thisSession == sessionId && resumed && !closing) setStatus(text,ok) } - engine = if (engineName == "CameraX") { + engine = if (depthSize != null) { + createDepthEngine(released, status) + } else if (engineName == "CameraX") { val view = PreviewView(this).apply { implementationMode = PreviewView.ImplementationMode.COMPATIBLE; scaleType = PreviewView.ScaleType.FILL_CENTER } previewHost.addView(view, FrameLayout.LayoutParams(-1,-1)) view.previewStreamState.observe(this) { state -> @@ -442,7 +449,7 @@ class MainActivity : ComponentActivity() { } }, recordingState = recordingState, notice = notice, status = status) } - previewHost.addView(FocusRing(this, { engine as? TouchMetering }) { controlBar.setAeLock(it) }.apply { + if (depthSize == null) previewHost.addView(FocusRing(this, { engine as? TouchMetering }) { controlBar.setAeLock(it) }.apply { canInteract = { !bursts.controlsLocked && cli.active == null } onZoomScale = { factor -> if (factor.isFinite() && factor > 0f) applyZoom(zoomRatio * factor) @@ -456,6 +463,10 @@ class MainActivity : ComponentActivity() { try { engine?.start() } catch (e: Exception) { setStatus("Start failed: ${e.message}",false) } updateCameraChoices() } + internal fun depthFrameArrived() { + lastPreviewFrameNs = nowNs() + initialPreviewArrived() + } private fun initialPreviewArrived() { previewStartup.frameArrived()?.let { setStatus(it, true) } } @@ -646,9 +657,10 @@ class MainActivity : ComponentActivity() { modeControls=row().apply { gravity=Gravity.CENTER; isBaselineAligned=false } photoModeButton=button("Photo") { selectMode(false) } videoModeButton=button("Video") { selectMode(true) } + depthModeButton=button("Depth") { chooseDepth() } multiButton=button("Multi · P") { openConcurrent(false) } multiVideoButton=button("Multi · V") { openConcurrent(true) } - listOf(photoModeButton,videoModeButton,multiButton,multiVideoButton).forEach { + listOf(photoModeButton,videoModeButton,depthModeButton,multiButton,multiVideoButton).forEach { it.background=cameraChrome(Color.TRANSPARENT) it.textSize=12f; it.gravity=Gravity.CENTER; it.includeFontPadding=false; it.setSingleLine(true) it.setPadding(dp(4),0,dp(4),0) @@ -706,27 +718,18 @@ class MainActivity : ComponentActivity() { pendingPermissionAction=null recorder.finish("camera_changed")?.let(incidents::export) cameraId=chosen; zoomRatio=1f + depthSize=null resetControls(); updateCameraChoices(); restartCamera() } } } - private fun selectMode(video: Boolean) { - if (cli.active != null) return - if (recordingVideo || stoppingRecording || closing || pendingPermissionAction != null || - mediaBusy() || !ready || videoMode==video) return - pendingPermissionAction=null - videoMode=video - resetModeSettings() - restartCamera() - updateMediaControls() - } /** Share lens labels with Benchmark, Probe and History; unknown IDs retain the short label. */ private fun cameraLabel(id:String):String { if(id.isEmpty()) return "No camera" return cameraEndpoints[id]?.let(CameraLabel::full) ?: CameraLabel.short(id) } private fun applyZoom(ratio: Float) { - if (!resumed || paused || closing || cli.active != null || bursts.controlsLocked || + if (depthSize != null || !resumed || paused || closing || cli.active != null || bursts.controlsLocked || !(ready || recordingVideo) || stoppingRecording || cameraId.isEmpty() || !ratio.isFinite()) return val range = zoomRange(manager, cameraId) zoomRatio = ratio.coerceIn(range.first, range.second) @@ -750,9 +753,9 @@ class MainActivity : ComponentActivity() { val idle=!recordingVideo && bursts.run==null cli.setUiBusy(liveCli, recordingVideo || stoppingRecording || pendingPermissionAction != null || mediaBusy()) listOf(photoModeButton,videoModeButton).forEachIndexed { index, button -> - val selected=(index==1)==videoMode + val selected=depthSize == null && (index==1)==videoMode button.isSelected=selected - button.isEnabled=ready && idle && !mediaBusy() && pendingPermissionAction == null + button.isEnabled=(ready || depthSize != null) && idle && !closing && !mediaBusy() && pendingPermissionAction == null button.setTextColor(if(selected) Look.onDark else Look.onDarkMuted) button.setTypeface(null,if(selected) Typeface.BOLD else Typeface.NORMAL) button.contentDescription=if(index==0) "Photo mode: save selected JPEG and RAW outputs" else "Video mode with audio" @@ -792,7 +795,7 @@ class MainActivity : ComponentActivity() { // Zoom stays live while recording (#174): the engine changes the recording request in place. // The engine reports "REC" as not-ready, so a running recording counts as ready here, as for the shutter. zoomControl.isEnabled=(ready || recordingVideo) && !stoppingRecording && !bursts.controlsLocked - controlBar.bind(videoMode,(ready || recordingVideo) && !stoppingRecording && cli.active==null && !bursts.controlsLocked, + controlBar.bind(videoMode,depthSize == null && (ready || recordingVideo) && !stoppingRecording && cli.active==null && !bursts.controlsLocked, pipUi.selected != null || pipUi.busy) galleryButton.isEnabled=idle labButton.isEnabled=!recordingVideo && !stoppingRecording && !closing && !mediaBusy() @@ -813,6 +816,7 @@ class MainActivity : ComponentActivity() { if (cli.active != null) { listOf(mediaButton, engineButton, cameraShortcut, photoModeButton, videoModeButton, zoomControl, galleryButton, labButton, reportButton).forEach { it.isEnabled = false } } + updateDepthControls(callbackGraph.isVisible, topBar.height + dp(8), bottomBar.height + dp(64)) listOf(engineButton,cameraShortcut,photoModeButton,videoModeButton,mediaButton,galleryButton,labButton).forEach { it.alpha=if(it.isEnabled) 1f else 0.4f } @@ -823,6 +827,7 @@ class MainActivity : ComponentActivity() { engineName=name; resetModeSettings(); updateCameraChoices(); restartCamera() } internal fun resetModeSettings() { + depthSize = null if (::callbackGraph.isInitialized) showCallbacks(false) pendingPip = null pipUi.resetMode() @@ -919,7 +924,7 @@ class MainActivity : ComponentActivity() { if (show && ::manualPanel.isInitialized) manualPanel.close() callbackGraph.reset() callbackGraph.visibility = if (show) View.VISIBLE else View.GONE - metrics.visibility = if (show) View.GONE else View.VISIBLE + metrics.visibility = if (show || depthSize != null) View.GONE else View.VISIBLE graphBack.isEnabled = show graphButton.contentDescription = if (show) "Hide callback timing" else "Show callback timing" widgets.highlight(graphButton,show) diff --git a/app/src/main/java/dev/halcamera/MainCliBridge.kt b/app/src/main/java/dev/halcamera/MainCliBridge.kt index 51714cbb..fde7bf3a 100644 --- a/app/src/main/java/dev/halcamera/MainCliBridge.kt +++ b/app/src/main/java/dev/halcamera/MainCliBridge.kt @@ -32,6 +32,7 @@ internal fun MainActivity.createLiveCli(): LiveController = LiveController(cli, return@fold } captureFeedback.clearResult() + depthSize = null showCallbacks(false) val modeChanged = engineName != (command.engine ?: "Camera2") || videoMode != command.videoMode cameraId = requireNotNull(command.camera); engineName = command.engine ?: "Camera2" @@ -71,6 +72,7 @@ internal fun MainActivity.createLiveCli(): LiveController = LiveController(cli, updateMediaControls() } override fun tune(options: dev.halcamera.cli.CliOptions?, reset: Boolean): Map { + if (depthSize != null) throw dev.halcamera.cli.CliFailure("PREFLIGHT_FAILED", "Select Photo or Video before changing camera controls.") if ((!ready && !recordingVideo) || closing || stoppingRecording) throw dev.halcamera.cli.CliFailure("PREFLIGHT_FAILED", "Start preview or recording first") val requested = if (reset) LiveControls() else options!!.controls(controlBar.support, manualCapabilities, videoMode, controlBar.controls, pipUi.selected != null) val zoom = if (reset) 1f else options!!.values["zoom"]?.toFloat() ?: zoomRatio diff --git a/app/src/main/java/dev/halcamera/MainDepthBridge.kt b/app/src/main/java/dev/halcamera/MainDepthBridge.kt new file mode 100644 index 00000000..1d8d7f6f --- /dev/null +++ b/app/src/main/java/dev/halcamera/MainDepthBridge.kt @@ -0,0 +1,91 @@ +package dev.halcamera + +import android.graphics.Typeface +import android.hardware.camera2.CameraCharacteristics +import android.view.View +import android.widget.FrameLayout +import android.widget.Toast +import androidx.core.view.ViewCompat +import dev.halcamera.camera.* +import dev.halcamera.ui.* + +/** LIVE depth UI; MainActivity owns the camera. */ +internal fun MainActivity.chooseDepth() { + if (!resumed || closing || cli.active != null || recordingVideo || stoppingRecording || mediaBusy() || + pendingPermissionAction != null || cameraId.isEmpty()) return + val support = try { depthSupport(manager.getCameraCharacteristics(cameraId)) } catch (e: Exception) { + Toast.makeText(this, "Cannot read depth capabilities: ${e.message}", Toast.LENGTH_LONG).show(); return + } + val sizes = support.sizes.takeIf { support.available }.orEmpty() + val choices = listOf(LiveChoiceSheet.Choice("Off", "Return to Photo")) + + sizes.map { LiveChoiceSheet.Choice("DEPTH16 · $it", "Camera2 · Depth-only view") } + + if (support.available) emptyList() else listOf(LiveChoiceSheet.Choice("Depth unavailable", support.unavailableReason.orEmpty(), false)) + LiveChoiceSheet.show(this, "Depth map", choices, if (videoMode) -1 else sizes.indexOf(depthSize) + 1, + content = Look.text(this, "${support.colorSummary}\nNear: bright · Far: dark · Black: invalid\nView only; photos and video are disabled.", 12, Look.onDarkMuted)) { index -> + if (!resumed || closing || cli.active != null || recordingVideo || mediaBusy()) return@show + val chosen = if (index == 0) null else sizes.getOrNull(index - 1) ?: return@show + if (chosen == depthSize && !videoMode) { return@show } + recorder.finish("depth_view_changed")?.let(::exportCliIncident) + captureFeedback.clearResult() + engineName = "Camera2" + videoMode = false + resetModeSettings() + depthSize = chosen + paused = false + updateCameraChoices() + restartCamera() + } +} + +internal fun MainActivity.createDepthEngine(released: String?, status: (String, Boolean) -> Unit): CameraEngine? { + val size = requireNotNull(depthSize) + val thisSession = sessionId + val chars = try { manager.getCameraCharacteristics(cameraId) } catch (e: Exception) { + setStatus("Cannot read depth camera: ${e.message}", false) + return null + } + val view = DepthMapView(this, size, chars[CameraCharacteristics.SENSOR_ORIENTATION] ?: 0, + chars[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) + previewHost.addView(view, FrameLayout.LayoutParams(-1, -1)) + return DepthCameraEngine(manager, cameraId, size, thisSession, telemetry, released, { frame -> + if (thisSession == sessionId && resumed && !closing && !paused) { + view.bind(frame) + depthFrameArrived() + } + }, status) +} + +internal fun MainActivity.updateDepthControls(callbacks: Boolean, topInset: Int, bottomInset: Int) { + depthModeButton.isEnabled = resumed && !closing && !recordingVideo && bursts.run == null && !stoppingRecording && !mediaBusy() && + cli.active == null && pendingPermissionAction == null && cameraId.isNotEmpty() + depthModeButton.isSelected = depthSize != null + depthModeButton.setTextColor(if (depthSize != null) Look.onDark else Look.onDarkMuted) + depthModeButton.setTypeface(null, if (depthSize != null) Typeface.BOLD else Typeface.NORMAL) + depthModeButton.alpha = if (depthModeButton.isEnabled) 1f else .4f + depthModeButton.contentDescription = "Depth map: choose a DEPTH16 resolution or view availability" + ViewCompat.setStateDescription(depthModeButton, if (depthSize != null) "Selected" else "Not selected") + if (depthSize != null) { + mediaButton.isEnabled = false + mediaButton.contentDescription = "Depth view only: select Photo or Video to capture media" + physicalPipButton.isEnabled = false + physicalPipButton.alpha = .4f + zoomControl.isEnabled = false + controlBar.bind(false, false, false) + liveIndicator.setSizesEnabled(false) + metrics.visibility = View.GONE + manualPanel.close() + (previewHost.getChildAt(0) as? DepthMapView)?.setChromeInsets(topInset, bottomInset) + } else if (!callbacks) metrics.visibility = View.VISIBLE +} + +/** Photo and Video also leave the depth-only session, including after a depth failure. */ +internal fun MainActivity.selectMode(video: Boolean) { + if (cli.active != null) return + if (recordingVideo || stoppingRecording || closing || pendingPermissionAction != null || + mediaBusy() || (depthSize == null && (!ready || videoMode==video))) return + pendingPermissionAction=null + videoMode=video + resetModeSettings() + restartCamera() + updateMediaControls() +} diff --git a/app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt b/app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt index 34992e84..5da08af7 100644 --- a/app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt +++ b/app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt @@ -87,6 +87,7 @@ class CameraProbeReader(private val manager: CameraManager, private val extraDev val sections = ArrayList() sections += guarded("Identity") { identity(id, physicalOf, c) } sections += guarded("Capabilities") { capabilities(c) } + sections += guarded("Depth") { depthSupport(c).probeRows(physicalOf == null) } sections += guarded("Sensor") { sensor(c) } sections += guarded("Lens") { lens(c) } sections += guarded("Control") { control(c) } diff --git a/app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt b/app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt new file mode 100644 index 00000000..be3c0e9b --- /dev/null +++ b/app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt @@ -0,0 +1,179 @@ +package dev.halcamera.camera + +import android.annotation.SuppressLint +import android.graphics.ImageFormat +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager +import android.media.ImageReader +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.SystemClock +import dev.halcamera.telemetry.Telemetry +import java.util.concurrent.atomic.AtomicBoolean + +/** LIVE-only depth session. No color targets, recording, photo saving or benchmark inputs. */ +class DepthCameraEngine( + private val manager: CameraManager, + private val cameraId: String, + private val size: LiveSize, + private val sessionId: String, + private val telemetry: Telemetry, + private val releasedCameraId: String?, + private val frame: (DepthFrame) -> Unit, + private val status: (String, Boolean) -> Unit, +) : CameraEngine { + private val thread = HandlerThread("HAL-depth").apply { start() } + private val handler = Handler(thread.looper) + private val main = Handler(Looper.getMainLooper()) + @Volatile private var active = true + private var opening = false + private var finished = false + private var device: CameraDevice? = null + private var session: CameraCaptureSession? = null + private var reader: ImageReader? = null + private var availability: CameraManager.AvailabilityCallback? = null + private var lastFrameMs: Long? = null + private val pendingFrame = AtomicBoolean(false) + private val closers = mutableListOf<() -> Unit>() + private val openAfterRelease = Runnable { stopWaiting(); open() } + private val firstFrameTimeout = Runnable { if (active && lastFrameMs == null) fail("No DEPTH16 frames received within 8 seconds.") } + + override fun start() { + handler.post { + if (!active) return@post + try { + val support = depthSupport(manager.getCameraCharacteristics(cameraId)) + require(support.available && size in support.sizes) { support.unavailableReason ?: "Unsupported DEPTH16 resolution." } + telemetry.registerSession(sessionId, "Camera2 · Depth", manager, cameraId) + if (releasedCameraId != null && releasedCameraId in manager.cameraIdList) { + availability = object : CameraManager.AvailabilityCallback() { + override fun onCameraAvailable(id: String) { + if (id == releasedCameraId && availability != null) openAfterRelease.run() + } + }.also { manager.registerAvailabilityCallback(it, handler) } + handler.postDelayed(openAfterRelease, CameraReleaseWait.LIMIT_MS) + } else open() + } catch (e: Exception) { fail(e.message ?: "Depth unavailable") } + } + } + + @SuppressLint("MissingPermission") + private fun open() { + if (!active || opening || device != null) return + try { + opening = true + telemetry.event(sessionId, "open_call") + manager.openCamera(cameraId, object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + opening = false + device = camera + if (!active) camera.close() else configure(camera) + } + override fun onDisconnected(camera: CameraDevice) = lost(camera, "Depth camera disconnected") + override fun onError(camera: CameraDevice, error: Int) = lost(camera, "Depth camera error $error") + private fun lost(camera: CameraDevice, reason: String) { + opening = false + device = camera + if (active) fail(reason) else camera.close() + } + override fun onClosed(camera: CameraDevice) { + if (device === camera) device = null + finishClose() + } + }, handler) + } catch (e: Exception) { opening = false; fail(e.message ?: "Cannot open depth camera") } + } + + @Suppress("DEPRECATION") + private fun configure(camera: CameraDevice) { + try { + val output = ImageReader.newInstance(size.width, size.height, ImageFormat.DEPTH16, 3) + reader = output + output.setOnImageAvailableListener({ source -> + try { + source.acquireLatestImage()?.use { image -> + if (!active) return@use + telemetry.image(sessionId, image.timestamp, image.width, image.height, image.format, "depth") + val now = SystemClock.elapsedRealtime() + if (lastFrameMs?.let { now - it < 100 } == true || !pendingFrame.compareAndSet(false, true)) return@use + try { + val plane = image.planes[0] + val crop = image.cropRect + val copy = DepthFrame.render(plane.buffer, crop.width(), crop.height(), plane.rowStride, plane.pixelStride, crop.left, crop.top) + lastFrameMs = now + main.post { + try { if (active) frame(copy) } finally { pendingFrame.set(false) } + } + } catch (e: Exception) { pendingFrame.set(false); throw e } + } + } catch (e: Exception) { if (active) fail("DEPTH16 read failed: ${e.message}") } + }, handler) + telemetry.configureCallbackStreams(sessionId, listOf(mapOf("id" to "depth", "kind" to "DEPTH16", "label" to "Depth", + "eventKind" to "image_available", "repeating" to true, "observable" to true, "width" to size.width, "height" to size.height))) + camera.createCaptureSession(listOf(output.surface), object : CameraCaptureSession.StateCallback() { + override fun onConfigured(value: CameraCaptureSession) { + if (!active || device !== camera) { value.close(); return } + session = value + try { + val request = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { + addTarget(output.surface) + setTag("depth") + }.build() + value.setRepeatingRequest(request, telemetry.callback(sessionId, { listOf("depth") }) { active }, handler) + telemetry.sessions.computeIfPresent(sessionId) { _, old -> old + mapOf("negotiatedStreams" to mapOf("depth" to size.toString())) } + main.post { if (active) status("DEPTH16 · $size · LIVE", true) } + handler.postDelayed(firstFrameTimeout, 8000) + } catch (e: Exception) { fail("Depth request failed: ${e.message}") } + } + override fun onConfigureFailed(value: CameraCaptureSession) { + value.close() + if (active) fail("The HAL rejected the DEPTH16 session at $size.") + } + }, handler) + } catch (e: Exception) { fail("Depth session failed: ${e.message}") } + } + + override fun capture() = Unit + override fun setZoom(ratio: Float) = Unit + + override fun close(done: () -> Unit) { + active = false + if (!handler.post { + if (finished) main.post(done) else { + closers += done + release() + } + }) main.post(done) + } + + private fun fail(reason: String) { + active = false + telemetry.event(sessionId, "depth_failed", mapOf("reason" to reason)) + main.post { status(reason, false) } + release() + } + + private fun stopWaiting() { + handler.removeCallbacks(openAfterRelease) + availability?.let { manager.unregisterAvailabilityCallback(it) } + availability = null + } + + private fun release() { + stopWaiting() + handler.removeCallbacks(firstFrameTimeout) + session?.close(); session = null + if (device != null) device?.close() else if (!opening) finishClose() + } + + private fun finishClose() { + if (device != null || opening || finished) return + reader?.close(); reader = null + finished = true + val callbacks = closers.toList().also { closers.clear() } + main.post { callbacks.forEach { it() } } + thread.quitSafely() + } +} diff --git a/app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt b/app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt new file mode 100644 index 00000000..1c8b5242 --- /dev/null +++ b/app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt @@ -0,0 +1,24 @@ +package dev.halcamera.camera + +import android.graphics.ImageFormat +import android.hardware.camera2.CameraCharacteristics + +/** Read each declaration independently so a missing vendor key never becomes a false support claim. */ +fun depthSupport(c: CameraCharacteristics): DepthSupport { + val errors = mutableListOf() + fun read(name: String, block: () -> T): T? = try { block() } catch (e: Exception) { + errors += "$name: ${e.javaClass.simpleName}: ${e.message}" + null + } + val capabilities = read("REQUEST_AVAILABLE_CAPABILITIES") { c[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES] } + val map = read("SCALER_STREAM_CONFIGURATION_MAP") { c[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP] } + val sizes = read("DEPTH16 sizes") { + map?.getOutputSizes(ImageFormat.DEPTH16) + .orEmpty().map { LiveSize(it.width, it.height) }.distinct().sortedBy { it.width.toLong() * it.height } + }.orEmpty() + return DepthSupport( + capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT), sizes, + read("DEPTH_DEPTH_IS_EXCLUSIVE") { c[CameraCharacteristics.DEPTH_DEPTH_IS_EXCLUSIVE] }, + read("Color output formats") { map?.outputFormats?.any { it != ImageFormat.DEPTH16 && it != ImageFormat.DEPTH_POINT_CLOUD } }, errors, + ) +} diff --git a/app/src/main/java/dev/halcamera/camera/DepthFrame.kt b/app/src/main/java/dev/halcamera/camera/DepthFrame.kt new file mode 100644 index 00000000..afa6b06b --- /dev/null +++ b/app/src/main/java/dev/halcamera/camera/DepthFrame.kt @@ -0,0 +1,45 @@ +package dev.halcamera.camera + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** A display copy only; never serialized into benchmark JSON or incident ZIPs. */ +data class DepthFrame(val width: Int, val height: Int, val pixels: IntArray, val centerMm: Int?, val validPercent: Int) { + companion object { + const val MAX_MM = 8191 + private const val BLACK = -0x1000000 + + fun confidence(sample: Int): Float { + val code = (sample ushr 13) and 7 + return if (code == 0) 1f else (code - 1) / 7f + } + + /** Absolute reads honor row/pixel strides, crop offsets and buffer position, including the final row's padding. */ + fun render(source: ByteBuffer, width: Int, height: Int, rowStride: Int, pixelStride: Int, + left: Int = 0, top: Int = 0, maxEdge: Int = 640): DepthFrame { + require(width > 0 && height > 0 && left >= 0 && top >= 0 && maxEdge > 0) + require(pixelStride == 2 && rowStride.toLong() >= (left.toLong() + width) * pixelStride) + val end = (top.toLong() + height - 1) * rowStride + (left.toLong() + width - 1) * pixelStride + 2 + require(end <= source.remaining().toLong()) { "DEPTH16 plane is truncated" } + val buffer = source.slice().order(ByteOrder.LITTLE_ENDIAN) + val step = ((maxOf(width, height) - 1) / maxEdge) + 1 + val outWidth = (width - 1) / step + 1 + val outHeight = (height - 1) / step + 1 + val pixels = IntArray(outWidth * outHeight) + fun sample(x: Int, y: Int) = buffer.getShort((top + y) * rowStride + (left + x) * pixelStride).toInt() and 0xffff + fun valid(value: Int) = (value and 0x1fff) > 0 && confidence(value) >= 0.5f + var count = 0 + for (y in 0 until outHeight) for (x in 0 until outWidth) { + val value = sample(x * step, y * step) + val mm = value and 0x1fff + pixels[y * outWidth + x] = if (!valid(value)) BLACK else { + count++ + val gray = 32 + (MAX_MM - mm) * 223 / MAX_MM + BLACK or (gray shl 16) or (gray shl 8) or gray + } + } + val center = sample(width / 2, height / 2).takeIf(::valid)?.and(0x1fff) + return DepthFrame(outWidth, outHeight, pixels, center, (count.toLong() * 100 / pixels.size).toInt()) + } + } +} diff --git a/app/src/main/java/dev/halcamera/camera/DepthSupport.kt b/app/src/main/java/dev/halcamera/camera/DepthSupport.kt new file mode 100644 index 00000000..5206985f --- /dev/null +++ b/app/src/main/java/dev/halcamera/camera/DepthSupport.kt @@ -0,0 +1,34 @@ +package dev.halcamera.camera + +/** Static HAL declarations, not a guarantee that a particular output combination will configure. */ +data class DepthSupport( + val advertised: Boolean?, + val sizes: List, + val exclusive: Boolean?, + val color: Boolean?, + val errors: List = emptyList(), +) { + val available get() = advertised == true && sizes.isNotEmpty() + val unavailableReason get() = when { + advertised == null -> "Depth capability could not be read." + !advertised -> "This camera does not advertise DEPTH_OUTPUT." + sizes.isEmpty() -> "No readable DEPTH16 resolutions." + else -> null + } + val colorSummary get() = when { + advertised == false -> "Not applicable: no DEPTH_OUTPUT" + advertised == null -> "Unknown: capability unavailable" + color == false -> "Not applicable: no color output formats reported" + color == null -> "Unknown: color output formats unavailable" + exclusive == true -> "No: interleave color and depth requests" + exclusive == false -> "Allowed by HAL; output combination must still be checked" + else -> "Unknown: DEPTH_DEPTH_IS_EXCLUSIVE not reported" + } + fun probeRows(independentlyOpenable: Boolean = true): List = listOf( + ProbeRow("DEPTH_OUTPUT", when (advertised) { true -> "Supported"; false -> "Not supported"; null -> "Unknown" }), + ProbeRow("DEPTH16 resolutions", sizes.joinToString().ifEmpty { "None reported" }), + ProbeRow("Color + depth (same request)", colorSummary), + ProbeRow("LIVE depth map", if (!independentlyOpenable) "Physical camera: select a publicly listed camera ID in LIVE" + else if (available) "Available in Camera2 · Depth-only view" else unavailableReason.orEmpty()), + ) + errors.map { ProbeRow("Read error", it) } +} diff --git a/app/src/main/java/dev/halcamera/ui/DepthMapView.kt b/app/src/main/java/dev/halcamera/ui/DepthMapView.kt new file mode 100644 index 00000000..e4cdc1ce --- /dev/null +++ b/app/src/main/java/dev/halcamera/ui/DepthMapView.kt @@ -0,0 +1,73 @@ +package dev.halcamera.ui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.view.View +import androidx.core.graphics.createBitmap +import androidx.core.graphics.withTranslation +import dev.halcamera.camera.DepthFrame +import dev.halcamera.camera.LiveSize +import java.util.Locale + +/** Raw sensor depth, fitted without cropping; rotation/mirror are applied only for display. */ +@android.annotation.SuppressLint("ViewConstructor") +internal class DepthMapView(context: Context, private val sourceSize: LiveSize, + private val sensorOrientation: Int, private val front: Boolean) : View(context) { + private var bitmap: Bitmap? = null + private var summary = "Waiting for DEPTH16 frames…" + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Look.onDark; textSize = Look.dp(context, 12).toFloat(); textAlign = Paint.Align.CENTER } + private val imagePaint = Paint().apply { isFilterBitmap = false } + + init { setBackgroundColor(Color.BLACK); importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES } + + fun setChromeInsets(top: Int, bottom: Int) { + if (paddingTop != top || paddingBottom != bottom) setPadding(Look.dp(context, 16), top, Look.dp(context, 16), bottom) + } + + fun bind(frame: DepthFrame) { + val image = bitmap?.takeIf { it.width == frame.width && it.height == frame.height } + ?: createBitmap(frame.width, frame.height).also { bitmap = it } + image.setPixels(frame.pixels, 0, frame.width, 0, 0, frame.width, frame.height) + val center = frame.centerMm?.let { String.format(Locale.US, "Center %.3f m", it / 1000.0) } ?: "Center unavailable" + summary = "$center · Valid ${frame.validPercent}%" + contentDescription = "Depth map $sourceSize. $summary. Near is bright, far is dark. Black means invalid or confidence below 50 percent." + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val top = paddingTop.toFloat() + val availableHeight = (height - paddingTop - paddingBottom).coerceAtLeast(1).toFloat() + val centerY = top + availableHeight / 2f + val textSpace = Look.dp(context, 80).toFloat() + bitmap?.let { image -> + @Suppress("DEPRECATION") + val degrees = (display?.rotation ?: 0) * 90 + val rotation = (sensorOrientation + (if (front) degrees else -degrees) + 360) % 360 + val swapped = rotation % 180 != 0 + val imageWidth = if (swapped) image.height else image.width + val imageHeight = if (swapped) image.width else image.height + val scale = minOf((width - paddingLeft - paddingRight).coerceAtLeast(1).toFloat() / imageWidth, + (availableHeight - textSpace).coerceAtLeast(1f) / imageHeight) + canvas.withTranslation(width / 2f, centerY - textSpace / 2) { + if (front) scale(-1f, 1f) + rotate(rotation.toFloat()) + scale(scale, scale) + drawBitmap(image, -image.width / 2f, -image.height / 2f, imagePaint) + } + } + val baseline = top + availableHeight - Look.dp(context, 56) + canvas.drawText("DEPTH16 · $sourceSize · Depth only", width / 2f, baseline, paint) + canvas.drawText(summary, width / 2f, baseline + Look.dp(context, 20), paint) + canvas.drawText("Near: bright · Far: dark · Black: invalid", width / 2f, baseline + Look.dp(context, 40), paint) + canvas.drawText("0–8.191 m · Confidence ≥ 50%", width / 2f, baseline + Look.dp(context, 60), paint) + } + + override fun onDetachedFromWindow() { + bitmap?.recycle(); bitmap = null + super.onDetachedFromWindow() + } +} diff --git a/app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt b/app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt index 1a2fa3a5..976ee52a 100644 --- a/app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt +++ b/app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt @@ -5,7 +5,7 @@ internal data class LiveStreamReadout(val text: String, val description: String) companion object { private data class Label(val key: String, val short: String, val name: String, val pip: Boolean = false) private val labels = listOf(Label("preview", "P", "Preview"), Label("analysis", "Y", "YUV"), - Label("jpeg", "J", "JPEG"), Label("raw", "RAW", "RAW"), Label("recording", "R", "Recording"), + Label("jpeg", "J", "JPEG"), Label("raw", "RAW", "RAW"), Label("depth", "Depth", "Depth"), Label("recording", "R", "Recording"), Label("pip", "P", "PIP Preview", true), Label("pipYuv", "Y", "PIP YUV", true), Label("pipJpeg", "J", "PIP JPEG", true)) fun from(streams: Map<*, *>?): LiveStreamReadout { diff --git a/app/src/test/java/dev/halcamera/camera/DepthFrameTest.kt b/app/src/test/java/dev/halcamera/camera/DepthFrameTest.kt new file mode 100644 index 00000000..ef98cb0a --- /dev/null +++ b/app/src/test/java/dev/halcamera/camera/DepthFrameTest.kt @@ -0,0 +1,60 @@ +package dev.halcamera.camera + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class DepthFrameTest { + private fun plane(vararg samples: Int) = ByteBuffer.allocate(samples.size * 2).order(ByteOrder.LITTLE_ENDIAN).apply { + samples.forEach { putShort(it.toShort()) }; flip() + } + + @Test fun `confidence zero code means full confidence and code one means invalid`() { + assertEquals(1f, DepthFrame.confidence(1200), 0f) + assertEquals(0f, DepthFrame.confidence((1 shl 13) or 1200), 0f) + assertEquals(1f / 7, DepthFrame.confidence((2 shl 13) or 1200), 0.00001f) + assertEquals(6f / 7, DepthFrame.confidence((7 shl 13) or 1200), 0.00001f) + } + + @Test fun `zero range and low confidence are black but far valid depth is visible`() { + val frame = DepthFrame.render(plane(0, 1, 8191, (1 shl 13) or 1000, (4 shl 13) or 1000, (5 shl 13) or 1000), 6, 1, 12, 2) + assertEquals(-0x1000000, frame.pixels[0]) + assertTrue((frame.pixels[1] and 255) > (frame.pixels[2] and 255)) + assertEquals(32, frame.pixels[2] and 255) + assertEquals(-0x1000000, frame.pixels[3]) + assertEquals(-0x1000000, frame.pixels[4]) + assertNotEquals(-0x1000000, frame.pixels[5]) + assertEquals(50, frame.validPercent) + assertNull(frame.centerMm) + } + + @Test fun `crop and padded rows honor buffer position without requiring last row padding`() { + val buffer = plane(9999, 0, 0, 0, 0, 0, 10, 20, 0, 0, 30, 40) + buffer.position(2) + val frame = DepthFrame.render(buffer, 2, 2, 8, 2, left = 1, top = 1) + assertEquals(40, frame.centerMm) + assertEquals(100, frame.validPercent) + assertEquals(2, buffer.position()) + assertTrue((frame.pixels[0] and 255) >= (frame.pixels[3] and 255)) + } + + @Test fun `large planes are sampled to a bounded display frame`() { + val buffer = ByteBuffer.allocate(1280 * 720 * 2).order(ByteOrder.LITTLE_ENDIAN) + repeat(1280 * 720) { buffer.putShort(2000) }; buffer.flip() + val frame = DepthFrame.render(buffer, 1280, 720, 2560, 2) + assertEquals(640, frame.width) + assertEquals(360, frame.height) + assertEquals(2000, frame.centerMm) + assertEquals(100, frame.validPercent) + } + + @Test(expected = IllegalArgumentException::class) + fun `truncated planes are rejected`() { DepthFrame.render(plane(1000, 2000, 3000), 2, 2, 4, 2) } + + @Test(expected = IllegalArgumentException::class) + fun `unexpected pixel stride is rejected`() { DepthFrame.render(plane(1000, 2000), 2, 1, 4, 1) } +} diff --git a/app/src/test/java/dev/halcamera/camera/DepthSupportTest.kt b/app/src/test/java/dev/halcamera/camera/DepthSupportTest.kt new file mode 100644 index 00000000..9896ce75 --- /dev/null +++ b/app/src/test/java/dev/halcamera/camera/DepthSupportTest.kt @@ -0,0 +1,38 @@ +package dev.halcamera.camera + +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class DepthSupportTest { + private val size = LiveSize(320, 240) + + @Test fun `capability and at least one size are both required for LIVE`() { + assertFalse(DepthSupport(false, listOf(size), false, true).available) + assertFalse(DepthSupport(null, listOf(size), false, true).available) + assertFalse(DepthSupport(true, emptyList(), false, true).available) + assertTrue(DepthSupport(true, listOf(size), null, true).available) + assertTrue(DepthSupport(true, listOf(size), true, false).available) + } + + @Test fun `exclusive and unknown never claim simultaneous color support`() { + assertTrue(DepthSupport(true, listOf(size), true, true).colorSummary.startsWith("No:")) + assertTrue(DepthSupport(true, listOf(size), null, true).colorSummary.startsWith("Unknown:")) + assertTrue(DepthSupport(true, listOf(size), false, true).colorSummary.contains("combination must still be checked")) + assertTrue(DepthSupport(true, listOf(size), false, false).colorSummary.contains("no color output")) + assertTrue(DepthSupport(true, listOf(size), false, null).colorSummary.startsWith("Unknown:")) + assertTrue(DepthSupport(false, emptyList(), null, true).colorSummary.startsWith("Not applicable:")) + } + + @Test fun `depth rows survive text and JSON export with read errors`() { + val support = DepthSupport(true, listOf(size), true, true, listOf("vendor key unavailable")) + val snapshot = CameraProbeSnapshot("now", emptyList(), listOf(CameraProbeEntry("0", null, "Camera 0", + listOf(ProbeSection("Depth", support.probeRows())))), emptyList()) + assertTrue(CameraProbeText.render(snapshot).contains("320x240")) + assertTrue(CameraProbeText.render(snapshot).contains("interleave")) + assertTrue(snapshot.toJsonMap().toString().contains("vendor key unavailable")) + assertTrue(support.probeRows(false).last { it.key == "LIVE depth map" }.value.contains("Physical camera")) + } +} diff --git a/app/src/test/java/dev/halcamera/ui/LiveStreamReadoutTest.kt b/app/src/test/java/dev/halcamera/ui/LiveStreamReadoutTest.kt index 1c53325e..643e1cdf 100644 --- a/app/src/test/java/dev/halcamera/ui/LiveStreamReadoutTest.kt +++ b/app/src/test/java/dev/halcamera/ui/LiveStreamReadoutTest.kt @@ -4,6 +4,10 @@ import org.junit.Assert.* import org.junit.Test class LiveStreamReadoutTest { + @Test fun depthOnlyDoesNotInventColorOutputs() { + assertEquals(LiveStreamReadout("Depth 320×240", "Depth 320x240"), + LiveStreamReadout.from(mapOf("depth" to "320x240"))) + } @Test fun everyInactiveStreamIsHiddenFromBothReadouts() { val values = listOf("analysis", "jpeg", "raw", "recording", "pip", "pipYuv", "pipJpeg").associateWith { null } + mapOf("preview" to "1280x720", "composite" to "590x1280") diff --git a/docs/.site-manifest.json b/docs/.site-manifest.json index a981fc93..97ff5d45 100644 --- a/docs/.site-manifest.json +++ b/docs/.site-manifest.json @@ -4,7 +4,7 @@ ".nojekyll": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "_inputs/decisions.html": "aadc3cad5fb3b8e37af148aa62ae79f9be0ba19a076fd2837f85c471b1629ded", "_inputs/device-verification.yaml": "1852f48d52f75733b4db973418fc7fad1f0f9d340a8b4b555cf9d49aa7016215", - "architecture.html": "53f25c59d4bd7addeecea62ce6713c0ae70e16f3aa2d5e2c9903c07b551414e7", + "architecture.html": "c25e2ce64ddb1b47c6c15aabc52c7742a2aed3fb830f72bbbf353d0450624361", "assets/docflow-design.css": "1cc4f34d8df94abdcf70ab2e9c51e456d8ef7f52283a2ef3adb986a6bd38d44c", "assets/media/automation/bridge.png": "bdbd7cbdc28010caa9b8e320e2d311efd35bb2f66cbb9b5cb322af638b7ccbff", "assets/media/automation/opening-sensor.mp4": "8e26f5eeeb0b92cd241b2235aab7ffd122cbf2ac94b66c37fafb440da4ae1178", @@ -72,8 +72,8 @@ "getting-started.html": "75b356487a9be6930c095686fcdc47229ecdd53b73b77f971a770f1e0f387e0f", "glossary.html": "a26775e3f8a0d6af19b7531b2dc86ea7630a9c0511bb34a308e3a630522462f1", "index.html": "34466a88605267322b92082ec1592c72a48a9fd2e3cdee59e66cea770d79372a", - "live.html": "15a39112070431467c9a8e04c04747df2d4866bad5fcf8d09cf96d51300308d2", - "probe.html": "926734ca1971e5c93cf19474edd8f1931536770100ec81d94944cf801af0aaed", + "live.html": "5a5c7a1bb6ba91857d68ccbbce07e28894c6c6194191c227a91241460686f119", + "probe.html": "95c89f7937f412abe452c3a8a3134b1f129908d8bccebb78671d1c49fd0302a1", "troubleshooting.html": "819baee906880fbaab14d0a649623326227868e748966ddbfe2d0272a50e53cb" } } diff --git a/docs/architecture.html b/docs/architecture.html index 569ce82b..22138c8e 100644 --- a/docs/architecture.html +++ b/docs/architecture.html @@ -79,7 +79,7 @@

One system.
Clear responsibilities.

측정·판 Live · MainActivity -앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영합니다. +앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영하며 지원 카메라의 Depth 맵을 봅니다. Multi · ConcurrentCameraActivity @@ -113,7 +113,7 @@

코드를 처음 읽는 순서

근거와 검토 정보
  • 근거 파일: app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, tools/halcam/halcam/cli.py, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt, app/src/main/java/dev/halcamera/WorkbenchActivity.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/telemetry/Telemetry.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt
  • 근거 수준: 코드 확인
  • -
  • 검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only
  • +
  • 검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review

전체 구조도

화살표는 코드를 사용하는 방향입니다. 이벤트와 파일이 이동하는 순서는 데이터 흐름에서 확인하세요.

@@ -208,7 +208,7 @@

코드를 처음 읽는 순서

camera/
    -
  • 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다.

    +
  • 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. DepthCameraEngine은 컬러 출력 없이 DEPTH16 맵만 받으며 DepthFrame이 순수 Kotlin으로 표시용 표본을 만듭니다.

  • close(done) 완료 전에 다음 카메라를 열지 않습니다.

  • @@ -292,7 +292,7 @@

    코드를 처음 읽는 순서

    근거와 검토 정보
    • 근거 파일: app/src/main/java/dev/halcamera/camera/MultiSession.kt, app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt, app/src/main/java/dev/halcamera/camera/MultiRecording.kt, app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, app/src/main/java/dev/halcamera/cli/LiveController.kt, app/src/main/java/dev/halcamera/cli/BenchmarkController.kt, app/src/main/java/dev/halcamera/cli/CliLibrary.kt, app/src/main/java/dev/halcamera/MainCliBridge.kt, app/src/main/java/dev/halcamera/DualCliBridge.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt, app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt, app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt, app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt, app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt, app/src/main/java/dev/halcamera/ui/Look.kt, app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt, app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt, app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt, app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt, app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt, app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt, app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt, app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt, app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt, app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt, app/src/main/java/dev/halcamera/cts/CtsCatalog.kt, app/src/main/java/dev/halcamera/cts/CtsRunner.kt, app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt, app/src/main/java/dev/halcamera/cts/Camera2Ops.kt, app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt, app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt, app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt, app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt, app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt, app/src/main/java/dev/halcamera/CameraProbeActivity.kt, app/src/main/java/dev/halcamera/camera/CameraProbe.kt, app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt
    • 근거 수준: 코드 확인
    • -
    • 검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only
    • +
    • 검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review

CLI 책임을 따라가세요

CliCommands는 명령별 필수 인자·허용 옵션·기본 제한·실행 대상과 Live에서 실행되는 방식(flow)·녹화 여부를 한곳에서 정의하고 hello.command_specs로 제공합니다.

@@ -309,6 +309,7 @@

주요 실행 흐름

결과 화면과 실행 기록은 Benchmark, 사양 표는 Probe, 케이스 실행과 판정은 CTS, 프레임별 그래프는 Callback, ADB 명령의 사용법은 CLI에서 확인하세요. Live 조작은 Live에 있습니다.

배치·간격·애니메이션·접근성 문구 같은 앱의 조작 규칙은 APP-UI.md가 관리합니다.

Live 셔터 조작은 MainActivity에서 선택한 엔진의 촬영·녹화 동작으로 이어집니다.

+

Live의 Depth 보기는 DepthCameraEngine → ImageReader → DepthFrame → DepthMapView로 연결합니다. 컬러 촬영과 분리된 보기이며, Telemetry에는 버퍼의 시각·크기·형식과 capture 콜백만 기록합니다. 맵 픽셀을 벤치마크나 incident ZIP의 입력으로 전달하지 않습니다. 사용 순서는 Depth 맵 보기에 있습니다.

벤치마크는 별도 화면인 BenchmarkActivity가 준비를 마친 뒤 BenchmarkRunner.start()를 호출하여 시작합니다.

Telemetry.callback(...)이 반환한 Camera2 콜백의 onCaptureStarted는 프레임워크 신호를 기록하며, Live 셔터와 벤치마크 시작을 연결하는 메서드가 아닙니다.

실행에서 저장까지

@@ -437,7 +438,7 @@

CLI 요청과 결과 수집

근거와 검토 정보
  • 근거 파일: app/src/main/java/dev/halcamera/cli/CliProvider.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, tools/halcam/halcam/cli.py, tools/halcam/halcam/download.py, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/GalleryActivity.kt, app/src/main/java/dev/halcamera/camera/MediaLibrary.kt, app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt, app/src/main/java/dev/halcamera/telemetry/Telemetry.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt
  • 근거 수준: 코드 확인
  • -
  • 검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only
  • +
  • 검토 2026-10-19 @ 14ca1ae2 · codex-user-authorized-depth-review

이벤트와 결과가 전달되는 경로

graph TB
@@ -575,7 +576,7 @@ 

카메라 이름을 적는 방법

줌 컨트롤의 상태

현재 배율을 누르면 지원 배율이 펼쳐집니다. 처음에는 현재 배율 하나만 보입니다.

Live 프리뷰에서 두 손가락을 벌리거나 모으면 지원 범위 안에서 연속으로 줌을 바꿉니다. 가장 가까운 프리셋의 원에 현재 배율을 소수 첫째 자리까지 표시하며, 펼친 상태에서 그 원을 누르면 해당 프리셋 배율로 돌아갑니다. 화면의 카메라 선택 정보를 갱신해도 중간 배율은 유지합니다.

-

핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다.

+

핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. Depth 보기, 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다.

@@ -630,6 +631,7 @@

실행과 측정의 제약

  • 벤치마크 JSON과 incident ZIP에는 관측 이벤트·메타데이터를 저장하며 이미지 픽셀을 저장하지 않습니다.

  • +

    Depth 보기는 컬러와 Depth를 같은 요청에 포함하지 않는 별도 Camera2 세션입니다. HAL의 동시 요청 허용 선언은 특정 출력 조합의 성공을 보장하지 않습니다. DepthCameraEngine은 촬영·녹화·수동 제어·줌·터치 측광을 제공하지 않으며 CLI 제어 변경도 거절합니다. Photo·Video 또는 CLI 촬영 준비로 전환하면 Depth를 해제합니다.

    Multi 모드의 MultiSession은 CameraEngine과 별도입니다. Camera2는 선택한 독립 장치를 연 뒤 세션을 구성하며, CameraX는 광고된 동시 조합을 바인딩합니다.

    모든 장치의 종료 통지를 확인해야 출력을 해제하고 다음 동시 세션을 열 수 있습니다. 한 명령의 여러 사진을 센서 동기 촬영으로 해석하지 않습니다.

    Multi V의 MicrophoneCapture는 AudioRecord의 MONOTONIC 하드웨어 시각과 PCM 프레임 위치로 오디오 원점을 구합니다.

    @@ -670,7 +672,7 @@

    CLI의 접근과 완료 경계

    근거와 검토 정보
    • 근거 파일: app/src/main/java/dev/halcamera/camera/MultiSession.kt, app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt, app/src/main/java/dev/halcamera/camera/MultiRecording.kt, app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt, app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt, app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt, app/src/main/java/dev/halcamera/cli/CliProvider.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, app/src/main/java/dev/halcamera/cli/CommandStore.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt, app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt, app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt, app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt
    • 근거 수준: 코드 확인
    • -
    • 검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only
    • +
    • 검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review

    변경 후 검증

    Android 의존성이 없는 러너와 평가 로직은 JVM 단위 테스트로 확인할 수 있습니다. 저장소 루트에서 실행하세요.

    @@ -710,17 +712,17 @@

    미완성 기능과 추가 검증

    - + - + - + @@ -730,32 +732,32 @@

    미완성 기능과 추가 검증

    - + - + - + - + - + - +
    구조 원본 data-flow 최신검토 2026-10-18 @ e9d3233c · codex-multi-cli-code-review검토 2026-10-19 @ 14ca1ae2 · codex-user-authorized-depth-review
    구조 원본 overall-architecture 최신검토 2026-10-18 @ e9d3233c · codex-multi-cli-code-review검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review
    구조 원본 state-transitions 최신검토 2026-10-18 @ e9d3233c · codex-multi-cli-code-review검토 2026-10-19 @ 14ca1ae2 · codex-user-authorized-depth-review
    구조 원본 ui-camera-label 구조 원본 ui-tool-handoff 최신검토 2026-10-18 @ e4bc487a · codex-multi-cli-code-review검토 2026-10-19 @ 14ca1ae2 · codex-user-authorized-depth-review
    구조 원본 ui-zoom 최신검토 2026-10-18 @ 3e7627e0 · claude-cli-specs-refactor검토 2026-10-19 @ 14ca1ae2 · codex-user-authorized-depth-review
    원고 overview 최신검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review
    원고 module-roles 최신검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review
    원고 runtime-flow 최신검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only검토 2026-10-19 @ 14ca1ae2 · codex-user-authorized-depth-review
    원고 constraints 최신검토 2026-10-19 @ cc58ea27 · codex-editorial-formatting-only검토 2026-10-19 @ b0648a05 · codex-user-authorized-depth-review

    표의 최신은 기록된 근거와 원고가 검토 이후 바뀌지 않았다는 뜻입니다. 모든 기기에서 동작을 검증했다는 뜻은 아닙니다.

    diff --git a/docs/design/APP-UI.md b/docs/design/APP-UI.md index a03267a3..13508f48 100644 --- a/docs/design/APP-UI.md +++ b/docs/design/APP-UI.md @@ -22,6 +22,12 @@ AEB가 꺼져 있을 때 셔터를 길게 누르면 연사하고 손을 떼면 셔터 위에는 `AEB 0/3`, `Burst · 2`, `HDR…`처럼 현재 상태만 표시합니다. 결과는 같은 자리에 기본 5초 동안 표시하고 접근성 시간 연장 설정을 따릅니다. 상태 변화로 셔터 위치가 움직이지 않도록 한 줄 높이를 확보하며 긴 실패 이유는 줄바꿈합니다. TalkBack은 `Start burst` 동작과 `Stop burst` 셔터로 연사를 조작합니다. 합성 중 다음 AEB는 시작하지 않습니다. +## Live Depth + +Live 하단의 Depth는 Camera2의 DEPTH16 해상도 선택창을 엽니다. 미지원 카메라는 이유를 표시합니다. 선택한 맵은 잘라내지 않고 표시하며 가까운 곳은 밝게, 먼 곳은 어둡게, 거리 0 또는 신뢰도 50% 미만은 검게 표시합니다. 중앙 거리와 표시 표본의 유효 비율도 읽을 수 있어야 합니다. + +Depth에서는 촬영·녹화·PIP·줌·수동 제어·터치 측광을 비활성화합니다. Photo·Video로 돌아가거나 Depth에서 Off를 선택해 컬러 보기로 복귀합니다. 세션 실패 뒤에도 복귀 동작을 사용할 수 있어야 합니다. + ## Lab과 기기 식별 Live의 `Lab`에서 기기 모델과 Android 정보를 확인하고, 기기 행을 눌러 SoC와 OS 빌드 등 상세 정보를 엽니다. Lab 자체는 카메라를 열거나 촬영 권한을 요구하지 않습니다. SoC는 기기가 보고한 값을 사용하며 Exynos로 가정하지 않습니다. API 31 미만에서는 `Build.HARDWARE`를 표시합니다. @@ -163,7 +169,7 @@ Live의 카메라 선택창은 아래쪽 조작부를 숨기고 위쪽을 흐리 Live 프리뷰에서 두 손가락을 벌리거나 모으면 지원 범위 안에서 연속으로 줌을 바꿉니다. 가장 가까운 프리셋의 원에 현재 배율을 소수 첫째 자리까지 표시하며, 펼친 상태에서 그 원을 누르면 해당 프리셋 배율로 돌아갑니다. 화면의 카메라 선택 정보를 갱신해도 중간 배율은 유지합니다. -핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다. +핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. Depth 보기, 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다. | 조건 | 동작 | | --- | --- | diff --git a/docs/live.html b/docs/live.html index d5a27000..3cb4d5f2 100644 --- a/docs/live.html +++ b/docs/live.html @@ -48,6 +48,16 @@

    Live에서 촬영하세요

    기본 사진은 카메라가 만든 JPEG 한 장입니다. DCIM/HALCamera에 저장하며, 촬영 메타데이터 JSON도 남깁니다.

    JPEG 목록에서 YUV를 고르면 앱이 변환한 JPEG를 저장합니다. Camera2에서 RAW를 켜면 DNG도 저장합니다.

    모드나 엔진을 바꾸면 새 촬영으로 시작합니다. PIP·스트림·줌·촬영 제어는 초기화됩니다. Callback의 고정 시간은 사용자 설정으로 유지합니다.

    +

    Depth 맵을 보세요

    +
      +
    1. Lab → Probe에서 카메라의 Depth 섹션을 확인합니다.
    2. +
    3. Live로 돌아와 카메라를 선택합니다.
    4. +
    5. 하단의 Depth를 누르고 DEPTH16 해상도를 선택합니다. 지원하지 않거나 정보를 읽지 못한 카메라는 이유를 표시합니다.
    6. +
    7. 컬러 화면으로 돌아가려면 Photo 또는 Video를 누릅니다. Depth → Off로도 Photo로 돌아갈 수 있습니다.
    8. +
    +

    Depth는 Camera2의 깊이 맵만 표시하는 보기 모드입니다. 선택하면 기존 카메라를 닫은 뒤 Depth 전용 세션을 엽니다. 컬러와 Depth를 같은 요청에서 받을 수 없는 기기에서도 사용할 수 있으며, 컬러와 정렬된 합성 화면은 제공하지 않습니다. 사진·동영상 저장, 줌·터치 측광·수동 제어와 PIP는 사용할 수 없습니다.

    +

    가까운 곳은 밝고 먼 곳은 어둡습니다. 표시 범위는 0–8.191 m이며, 거리가 0이거나 신뢰도가 50% 미만인 픽셀은 검게 표시합니다. 중앙 거리와 유효 픽셀 비율도 표시합니다. 비율은 화면에 표시할 표본을 기준으로 계산합니다. 표시용 맵은 긴 변이 최대 640픽셀이 되도록 표본을 추출하며, 갱신은 최대 초당 10회입니다.

    +

    선택한 해상도로 세션을 열지 못하거나 8초 동안 첫 Depth 프레임을 받지 못하면 이유를 표시합니다. 이때 다른 해상도를 선택하거나 Photo로 돌아가세요. 맵은 센서 방향과 전면 카메라의 좌우 반전을 반영하며 전체 영역을 잘라내지 않고 보여줍니다. Depth 픽셀을 Benchmark JSON이나 진단 ZIP에 저장하지 않습니다.

    동영상을 찍으세요

    1. 하단에서 Video를 선택합니다.
    2. diff --git a/docs/probe.html b/docs/probe.html index 71a3a7de..cb34f37c 100644 --- a/docs/probe.html +++ b/docs/probe.html @@ -47,11 +47,12 @@

      앱에서 Probe를 여세요

      • 카메라 목록: 논리 카메라와 그 안의 물리 카메라를 구분합니다. 사양이 보여도 단독으로 열 수 없는 물리 카메라가 있습니다.
      • 섹션: 제목을 눌러 접거나 펼칩니다. 출력 형식별 크기는 STREAMS에서 확인합니다.
      • +
      • DEPTH: DEPTH_OUTPUT 지원 여부와 DEPTH16 해상도, 컬러와 Depth를 같은 요청에 포함할 수 있는지를 확인합니다. Allowed by HAL은 기기의 선언이며 특정 출력 조합의 동작을 보장하지 않습니다. Unknown은 해당 정보를 읽지 못한 상태입니다. 단독으로 열 수 없는 물리 카메라는 LIVE에서 직접 선택할 수 없습니다.
      • 필터: 단어가 들어간 줄을 찾습니다. 결과를 누르면 해당 항목으로 이동합니다.
      • REQUEST · RESULT KEYS / SESSION KEYS: 요청·결과에서 지원하는 key 이름입니다. 실제 촬영값은 아닙니다.
      -섹션 이름과 API별 표시 조건

      IDENTITY, CAPABILITIES, SENSOR, LENS, CONTROL, PROCESSING, REQUEST 뒤에 REQUEST · RESULT KEYS가 나옵니다.

      +섹션 이름과 API별 표시 조건

      IDENTITY, CAPABILITIES, DEPTH, SENSOR, LENS, CONTROL, PROCESSING, REQUEST 뒤에 REQUEST · RESULT KEYS가 나옵니다.

      SESSION KEYS는 API 28 이상, MANDATORY STREAM COMBINATIONS는 API 29 이상에서 표시합니다.

      STREAMS는 PRIVATE·JPEG 같은 출력 형식별로 나뉩니다. 이어서 HIGH SPEED VIDEO, REPROCESSING INPUTS, ALL CHARACTERISTICS가 나옵니다.

      enum 값은 가능한 경우 숫자 대신 상수 이름을 표시합니다.

      diff --git a/docs/validation/depth-20261020/README.md b/docs/validation/depth-20261020/README.md new file mode 100644 index 00000000..e900f7a1 --- /dev/null +++ b/docs/validation/depth-20261020/README.md @@ -0,0 +1,66 @@ +# PROBE와 LIVE Depth 검증 + +2026-10-20에 PROBE의 Depth 사양 표시와 LIVE의 DEPTH16 보기 코드를 검증했습니다. 실제 Depth 센서에서 프레임을 받은 결과는 포함하지 않습니다. + +## 자동 검증 + +- `testDebugUnitTest`: 앱 JVM 테스트 907개가 통과했습니다. Depth 신뢰도 코드, 거리 0, stride·crop·버퍼 위치, 잘린 plane, 표시 표본 제한, 지원 여부와 Unknown 구분, TXT·JSON 내보내기와 Depth 스트림 표시를 포함합니다. +- `lintDebug`: 오류와 경고가 없습니다. +- `assembleDebug`, `assembleDebugAndroidTest`: APK를 생성했습니다. +- 문서 회귀 검사 37개가 모두 통과했습니다. 새 요소 수 112개, 입력 크기 한도, dry-run, 앱 UI 규칙 사본 일치와 배포 링크를 포함합니다. +- 사용자 요청에 따라 코드를 대조하고 원본 최신성 15개 항목의 검토 기록을 `codex-user-authorized-depth-review`로 갱신했습니다. 기존 최신 항목의 검토자는 보존했습니다. 전체 `docflow check --build` 6단계와 coverage·생성 일치·사이트 산출물 일치 검사를 통과했습니다. + +## 에뮬레이터 확인 + +기기는 `sdk_gphone64_x86_64`이며 Android 16 / API 36입니다. 기존 앱 데이터를 삭제하지 않고 서명이 일치하는 로컬 빌드로 갱신했습니다. + +1. LIVE에서 `Depth`를 눌러 미지원 카메라의 이유와 비활성화된 해상도 항목을 확인했습니다. +2. `Lab → Probe`에서 `Depth`를 검색해 지원 여부·해상도·컬러 동시 요청 가능 여부가 검색 결과에 포함되는지 확인했습니다. +3. `depth_checks=true` instrumentation으로 합성 DEPTH16을 해석하고, 중앙 거리 1.000 m·유효 표본 비율·맵 표시와 접근성 설명을 확인했습니다. 검증용 testApplicationId는 `dev.halcamera.depthchecks`로 지정했습니다. 결과는 `DEPTH_MAP_CHECKS_OK`입니다. + +| 화면 | 확인 자료 | +| --- | --- | +| PROBE 검색 | [Depth 사양](probe-depth.png) | +| LIVE 미지원 안내 | [Depth unavailable](depth-unavailable.png) | +| 합성 데이터 렌더링 | [Depth map synthetic](depth-map-synthetic.png) | + +## 변경사항 리뷰 + +Depth 지원 선언과 Unknown 처리, confidence 비트·stride·crop 해석, UI 전달 대기 제한, 세션 실패와 close(done) 순서, 늦은 프레임 차단, Photo·Video·CLI 전환 및 픽셀 저장 경계를 대조했습니다. 코드 리뷰에서 남은 기능 수정 사항은 없습니다. 문서에는 Depth의 별도 엔진·촬영 비활성화·줌 제한이 일부 빠져 있어 엔진 비교·데이터 흐름·수명주기·줌 원본에 반영했습니다. + +이 검토는 사용자 요청에 따른 Codex의 코드 대조 기록입니다. 실제 Depth 센서 실측이나 별도 사람의 독립 코드 리뷰를 뜻하지 않습니다. + +## SM-S936N 실기기 확인 + +2026-10-20에 Samsung SM-S936N, Android 16 / API 36에서 앱 0.30.0(691)을 기존 데이터 삭제 없이 갱신했습니다. 아래 결과는 에뮬레이터 결과와 별개입니다. + +PROBE 요청 `0be1d193-a8ac-4ee0-8c88-f9bec825c01b`는 `succeeded`로 끝났으며 메타데이터 읽기 오류가 없었습니다. JSON 367,246바이트와 TXT 261,858바이트를 PC로 회수하고 manifest의 SHA-256과 일치하는지 확인했습니다. [Depth 항목 발췌와 원본 해시](sm-s936n-depth-summary.json)를 보존했습니다. + +| 카메라 ID | 구분 | DEPTH_OUTPUT | DEPTH16 해상도 | 컬러 동시 요청 | +| --- | --- | --- | --- | --- | +| 0 | 후면 Wide, 공개 ID | 미지원 | 없음 | 해당 없음 | +| 1 | 전면 25 mm, 공개 ID | 미지원 | 없음 | 해당 없음 | +| 2 | 후면 UWide, 공개 ID | 미지원 | 없음 | 해당 없음 | +| 3 | 전면 30 mm, 공개 ID | 미지원 | 없음 | 해당 없음 | +| 5 | 카메라 0의 물리 Wide | 미지원 | 없음 | 해당 없음 | +| 6 | 카메라 0의 물리 Tele | 미지원 | 없음 | 해당 없음 | + +이 기기에서 앱에 공개된 Camera2 Depth 스트림은 없습니다. 실제 Depth 센서의 프레임 수신이나 컬러·Depth 조합 검증 결과로 해석하면 안 됩니다. + +실기기에서 `depth_checks=true` instrumentation도 `DEPTH_MAP_CHECKS_OK`로 통과했습니다. 합성 DEPTH16의 stride 해석, 중앙 거리 1.000 m, Android View 렌더링 및 접근성 설명을 확인한 결과이며 센서 출력 검증은 아닙니다. + +잠금 해제 후 실기기 UI에서 다음을 확인했습니다. + +1. 공개 카메라 0·1·2·3 모두 컬러 프리뷰가 준비됐고, `Depth unavailable`과 `This camera does not advertise DEPTH_OUTPUT.` 안내가 표시됐습니다. 미지원 항목을 눌러도 Depth 세션을 시작하지 않았습니다. +2. PROBE에서 `Depth` 검색 시 지원 여부·해상도·컬러 동시 요청·LIVE 안내가 검색됐으며, 검색 결과를 눌러 실제 Depth 사양 섹션으로 이동했습니다. +3. Video에서 `Depth → Off · Return to Photo`를 선택해도 Video에 남는 문제를 발견했습니다. Video에서 Off를 현재 선택으로 표시하지 않도록 고치고, 같은 Depth 크기라는 이유로 Photo 복귀를 건너뛰지 않도록 수정했습니다. +4. 수정한 APK를 재설치한 뒤 `Video → Depth → Off → Photo`를 반복해 Photo 선택과 `Live, preview running`을 확인했습니다. 홈 이동 후 복귀에서도 프리뷰가 다시 준비됐습니다. +5. 회전 설정을 가로로 바꿔도 LIVE는 manifest의 기존 `portrait` 설정대로 세로 화면을 유지했습니다. 검증 후 기기의 원래 `lock 0` 설정을 복원했습니다. 실제 Depth 맵의 센서 방향 보정 검증은 아닙니다. + +수정 후 앱 JVM 테스트 907개와 `lintDebug`(오류·경고 없음), `assembleDebug`가 통과했습니다. 최종 앱 프로세스 로그에서 앱 충돌이나 카메라 세션 실패는 관측하지 않았습니다. `ashmem`의 pinning 폐기 안내와 Adreno/QSPM·`libpenguin.so` 관련 플랫폼 로그는 있었습니다. + +## 실제 기기에서 남은 확인 + +DEPTH_OUTPUT 지원 기기에서 해상도별 세션 구성과 지속 수신, 컬러 보기와의 반복 전환, 백그라운드 복귀와 회전, 카메라 해제 완료를 확인해야 합니다. Depth 전용 세션이므로 컬러와 Depth를 같은 요청으로 받는 조합이나 Bokeh 합성은 이번 검증 범위에 포함하지 않습니다. + +사용 방법은 [Live 가이드](../../../guide/live.md#depth-맵을-보세요)를 참고하세요. diff --git a/docs/validation/depth-20261020/depth-map-synthetic.png b/docs/validation/depth-20261020/depth-map-synthetic.png new file mode 100644 index 00000000..da43f6dd Binary files /dev/null and b/docs/validation/depth-20261020/depth-map-synthetic.png differ diff --git a/docs/validation/depth-20261020/depth-unavailable.png b/docs/validation/depth-20261020/depth-unavailable.png new file mode 100644 index 00000000..32607c7f Binary files /dev/null and b/docs/validation/depth-20261020/depth-unavailable.png differ diff --git a/docs/validation/depth-20261020/probe-depth.png b/docs/validation/depth-20261020/probe-depth.png new file mode 100644 index 00000000..d3bc58aa Binary files /dev/null and b/docs/validation/depth-20261020/probe-depth.png differ diff --git a/docs/validation/depth-20261020/sm-s936n-depth-summary.json b/docs/validation/depth-20261020/sm-s936n-depth-summary.json new file mode 100644 index 00000000..03989cab --- /dev/null +++ b/docs/validation/depth-20261020/sm-s936n-depth-summary.json @@ -0,0 +1,168 @@ +{ + "captured_at": "2026-10-20T01:05:38.0000000+09:00", + "device": { + "Manufacturer": "samsung", + "Model": "SM-S936N", + "Device": "pa2q", + "Android": "16 (API 36)", + "Build": "BP4A.251205.006.S936NKSSCCZH2", + "Security patch": "2026-08-05", + "SoC": "QTI SM8750", + "ABIs": "arm64-v8a" + }, + "request_id": "0be1d193-a8ac-4ee0-8c88-f9bec825c01b", + "state": "succeeded", + "errors": [], + "original_artifacts": [ + { + "name": "camera-probe-SM-S936N-20261020_010538.json", + "size_bytes": 367246, + "sha256": "8675661879a0fb6f6b5e2a90ae25bfa6ceb1cb39b60bd55e41d74ba17acdea9c" + }, + { + "name": "camera-probe-SM-S936N-20261020_010538.txt", + "size_bytes": 261858, + "sha256": "1dc6a056f2ed73328d390bebb655e41ef254bd96f5ba900f087b7c5926753044" + } + ], + "cameras": [ + { + "camera_id": "0", + "physical_of": null, + "title": "Camera · 0 (Wide · Rear) · LEVEL_3", + "depth": [ + { + "key": "DEPTH_OUTPUT", + "value": "Not supported" + }, + { + "key": "DEPTH16 resolutions", + "value": "None reported" + }, + { + "key": "Color + depth (same request)", + "value": "Not applicable: no DEPTH_OUTPUT" + }, + { + "key": "LIVE depth map", + "value": "This camera does not advertise DEPTH_OUTPUT." + } + ] + }, + { + "camera_id": "5", + "physical_of": "0", + "title": "Camera · 0.5 (Wide · Rear) · physical · LEVEL_3", + "depth": [ + { + "key": "DEPTH_OUTPUT", + "value": "Not supported" + }, + { + "key": "DEPTH16 resolutions", + "value": "None reported" + }, + { + "key": "Color + depth (same request)", + "value": "Not applicable: no DEPTH_OUTPUT" + }, + { + "key": "LIVE depth map", + "value": "Physical camera: select a publicly listed camera ID in LIVE" + } + ] + }, + { + "camera_id": "6", + "physical_of": "0", + "title": "Camera · 0.6 (Tele · Rear) · physical · LIMITED", + "depth": [ + { + "key": "DEPTH_OUTPUT", + "value": "Not supported" + }, + { + "key": "DEPTH16 resolutions", + "value": "None reported" + }, + { + "key": "Color + depth (same request)", + "value": "Not applicable: no DEPTH_OUTPUT" + }, + { + "key": "LIVE depth map", + "value": "Physical camera: select a publicly listed camera ID in LIVE" + } + ] + }, + { + "camera_id": "1", + "physical_of": null, + "title": "Camera · 1 (Front · 25 mm) · FULL", + "depth": [ + { + "key": "DEPTH_OUTPUT", + "value": "Not supported" + }, + { + "key": "DEPTH16 resolutions", + "value": "None reported" + }, + { + "key": "Color + depth (same request)", + "value": "Not applicable: no DEPTH_OUTPUT" + }, + { + "key": "LIVE depth map", + "value": "This camera does not advertise DEPTH_OUTPUT." + } + ] + }, + { + "camera_id": "2", + "physical_of": null, + "title": "Camera · 2 (UWide · Rear) · LIMITED", + "depth": [ + { + "key": "DEPTH_OUTPUT", + "value": "Not supported" + }, + { + "key": "DEPTH16 resolutions", + "value": "None reported" + }, + { + "key": "Color + depth (same request)", + "value": "Not applicable: no DEPTH_OUTPUT" + }, + { + "key": "LIVE depth map", + "value": "This camera does not advertise DEPTH_OUTPUT." + } + ] + }, + { + "camera_id": "3", + "physical_of": null, + "title": "Camera · 3 (Front · 30 mm) · FULL", + "depth": [ + { + "key": "DEPTH_OUTPUT", + "value": "Not supported" + }, + { + "key": "DEPTH16 resolutions", + "value": "None reported" + }, + { + "key": "Color + depth (same request)", + "value": "Not applicable: no DEPTH_OUTPUT" + }, + { + "key": "LIVE depth map", + "value": "This camera does not advertise DEPTH_OUTPUT." + } + ] + } + ] +} diff --git a/guide/_bindings.yaml b/guide/_bindings.yaml index 3a1811e8..e879479b 100644 --- a/guide/_bindings.yaml +++ b/guide/_bindings.yaml @@ -117,6 +117,14 @@ sources: - app/src/main/java/dev/halcamera/camera/CameraEngine.kt - app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt - app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt + camera-engines/depth-engine: + evidence: + - app/src/main/java/dev/halcamera/MainDepthBridge.kt + - app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt + - app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt + - app/src/main/java/dev/halcamera/camera/DepthSupport.kt + - app/src/main/java/dev/halcamera/camera/DepthFrame.kt + - app/src/main/java/dev/halcamera/ui/DepthMapView.kt camera-engines/camera2-engine: evidence: - app/src/main/java/dev/halcamera/camera/Camera2Engine.kt @@ -359,6 +367,8 @@ sources: evidence: - app/src/main/java/dev/halcamera/camera/CameraProbe.kt - app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt + - app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt + - app/src/main/java/dev/halcamera/camera/DepthSupport.kt screens/cts-screen: evidence: - app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt diff --git a/guide/_content/_reference/engine/camera2.md b/guide/_content/_reference/engine/camera2.md index 07a8dafa..ecf4309c 100644 --- a/guide/_content/_reference/engine/camera2.md +++ b/guide/_content/_reference/engine/camera2.md @@ -37,6 +37,8 @@ verifications: [] **Camera2Engine은 앱에서 CaptureRequest를 직접 구성합니다.** `request_observed`에 기록한 키를 HAL 로그와 대조할 수 있습니다. 엔진 자체는 세션, 요청 구성, 줌, Live 제어를 담당하고, 사진은 `Camera2StillCapture`, Live 녹화는 `Camera2LiveRecorder`, 터치 측광은 `Camera2TouchFocus`, 벤치마크 녹화는 `BenchmarkRecorder`가 맡습니다. 카메라 요청과 capture 콜백은 엔진이 만든 카메라 스레드 하나에서 처리하고, 버퍼 relay와 파일 저장은 각자의 스레드에서 처리합니다. +Depth 보기는 일반 `Camera2Engine`과 별도인 `DepthCameraEngine`이 담당합니다. 컬러·사진·녹화 출력 대신 DEPTH16 ImageReader 하나만 사용하며, 기존 엔진을 닫은 뒤 선택한 해상도로 엽니다. 사용 방법과 표시 범위는 [Depth 맵 보기](live.md#depth-맵을-보세요)를 확인하세요. + ### PIP 지원을 미리 확인하세요 **현재 해상도와 유지되는 YUV·JPEG·RAW 출력, 보조 카메라의 Preview·사진 출력으로 PIP 지원 여부를 조회합니다.** 선택창을 열 때와 적용 직전에 검사하며 제조사·칩셋·ID별 예외는 두지 않습니다. Physical은 열린 카메라의 `isSessionConfigurationSupported`, Service는 `CameraManager.isConcurrentSessionConfigurationSupported`를 사용합니다. 합성 녹화의 인코더는 GL 출력을 받으므로 카메라 세션 조회에 넣지 않습니다. diff --git a/guide/_content/_reference/engine/camerax.md b/guide/_content/_reference/engine/camerax.md index a3cb83b0..c97446cd 100644 --- a/guide/_content/_reference/engine/camerax.md +++ b/guide/_content/_reference/engine/camerax.md @@ -23,6 +23,8 @@ verifications: [] **CameraXEngine은 CameraX use case로 Live 촬영과 제어를 제공하지만, 요청 키 일부는 CameraX가 정합니다.** Live 스트림 설정에서 Preview·YUV·JPEG 크기와 출력 활성화를 선택할 수 있습니다. 앱이 CameraX 1.6.2에 직접 넣는 Camera2 키에는 AE 잠금(`CONTROL_AE_LOCK`), AF 잠금 해제용 cancel trigger, 광학 보정(`LENS_OPTICAL_STABILIZATION_MODE`)이 있습니다. 엔진 자체는 use case bind, 줌, 수명 주기를 담당하고, 사진은 `CameraXStillCapture`, 녹화는 `CameraXLiveRecorder`, Live 제어와 터치 측광은 `CameraXControls`가 맡습니다. +Depth 해상도를 선택하면 CameraX 세션을 해제하고 Camera2의 `DepthCameraEngine`으로 전환합니다. CameraX use case에 Depth 출력을 추가하지 않습니다. 표시와 촬영 제한은 [Depth 맵 보기](live.md#depth-맵을-보세요)를 확인하세요. + ### CameraX PIP **PIP 목록에는 모든 후보를 표시하고 지원 불가가 확인된 항목만 비활성화합니다.** Physical 후보는 `Camera2 only`로 표시합니다. Service 후보는 CameraX가 제공하는 동시 조합에 포함되어야 합니다. 제조사·칩셋·카메라 ID에 따른 예외는 두지 않습니다. diff --git a/guide/_content/_reference/engine/comparison.md b/guide/_content/_reference/engine/comparison.md index 9f94aac9..c6eb5ba1 100644 --- a/guide/_content/_reference/engine/comparison.md +++ b/guide/_content/_reference/engine/comparison.md @@ -26,6 +26,7 @@ verifications: [] | 항목 | Camera2 | CameraX | | --- | --- | --- | +| Depth 보기 | DEPTH_OUTPUT과 DEPTH16 해상도를 공개한 카메라에서 별도 Depth 전용 세션을 엽니다. 맵 표시만 제공합니다. | Depth 해상도를 선택하면 Camera2로 전환합니다. | | 수동 촬영 | ISO·노출 시간·초점과 WB를 프리뷰·사진·녹화에 적용합니다. 지원 여부와 실제 적용값을 표시합니다. | Manual 버튼을 미지원 상태로 비활성화합니다. | | YUV를 JPEG로 선택 | 요청한 YUV 이미지와 최종 CaptureResult의 센서 시각을 맞춥니다. | 촬영 뒤 받은 analysis 프레임 하나를 변환합니다. | | JPEG 출처 | YUV 앱 변환 또는 HAL JPEG 중 하나와 촬영 JSON을 저장합니다. | YUV 앱 변환 또는 ImageCapture JPEG 중 하나와 촬영 JSON을 저장합니다. | diff --git a/guide/_content/_reference/engine/contract.md b/guide/_content/_reference/engine/contract.md index 65065c0a..3fb63931 100644 --- a/guide/_content/_reference/engine/contract.md +++ b/guide/_content/_reference/engine/contract.md @@ -5,6 +5,7 @@ sources: - app/src/main/java/dev/halcamera/camera/CameraEngine.kt - app/src/main/java/dev/halcamera/camera/Camera2Engine.kt - app/src/main/java/dev/halcamera/camera/CameraXEngine.kt + - app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt - app/src/main/java/dev/halcamera/MainActivity.kt - app/src/main/java/dev/halcamera/ui/LiveControlBar.kt - app/src/main/java/dev/halcamera/ui/FocusRing.kt @@ -28,6 +29,7 @@ classDiagram class TouchMetering { <> } CameraEngine <|.. Camera2Engine CameraEngine <|.. CameraXEngine + CameraEngine <|.. DepthCameraEngine MediaCapture <|.. Camera2Engine MediaCapture <|.. CameraXEngine LiveTuning <|.. Camera2Engine @@ -38,6 +40,8 @@ classDiagram 점선 화살표는 인터페이스 구현을 뜻합니다. 실제로 사용할 수 있는 기능은 엔진의 실행 모드와 아래 조건에 따라 달라집니다. +`DepthCameraEngine`은 LIVE의 깊이 맵 보기에서만 사용하는 Camera2 엔진입니다. `CameraEngine`만 구현하고 컬러·촬영·녹화·수동 제어·터치 측광은 제공하지 않습니다. `Photo` 또는 `Video`로 전환하면 기존 컬러 엔진을 다시 엽니다. + | 인터페이스 | 담당하는 동작 | 구현 | | --- | --- | --- | | `CameraEngine` | 카메라 열기(`start`), 촬영(`capture`), 줌(`setZoom`), 닫기(`close(done)`) | 두 엔진 | @@ -56,7 +60,7 @@ classDiagram 종료 통지와 다음 엔진의 대기 순서는 [엔진 전환 그림](architecture.md#카메라-열기와-닫기)을 확인하세요. -Photo·Video·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지합니다. 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. +Photo·Video·Depth·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지하며 Depth 버튼은 해상도 선택창을 엽니다. 일반 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. Depth는 첫 맵 수신 후에도 촬영 버튼을 비활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. ### Benchmark와 CLI가 쓰는 엔진 @@ -66,7 +70,7 @@ CLI의 `preview`·`capture`·`record.start`는 `LiveController`를 통해 Camera ### 두 엔진이 함께 남기는 기록 -두 엔진은 세션을 열 때 `Telemetry.registerSession`에 엔진 이름(`Camera2` 또는 `CameraX`)을 남기고, 같은 `Telemetry.callback`으로 capture 콜백을 기록합니다. 그래서 `capture_started`, `capture_result` 같은 이벤트 종류가 엔진과 관계없이 같습니다. 엔진별로 요청을 만드는 방법이 다르므로, 같은 이벤트라도 요청에 들어간 키는 엔진마다 다를 수 있습니다. +일반 컬러 엔진은 세션을 열 때 `Telemetry.registerSession`에 엔진 이름(`Camera2` 또는 `CameraX`)을 남기고, 같은 `Telemetry.callback`으로 capture 콜백을 기록합니다. Depth 전용 엔진은 이름을 `Camera2 · Depth`로 남기며 같은 콜백 경로를 사용합니다. 그래서 `capture_started`, `capture_result` 같은 이벤트 종류가 엔진과 관계없이 같습니다. 엔진별로 요청을 만드는 방법이 다르므로, 같은 이벤트라도 요청에 들어간 키는 엔진마다 다를 수 있습니다. Live 제어와 터치 측광은 다음 이벤트를 추가로 남깁니다. diff --git a/guide/_content/architecture/constraints.md b/guide/_content/architecture/constraints.md index bd9b1abb..68b24cbe 100644 --- a/guide/_content/architecture/constraints.md +++ b/guide/_content/architecture/constraints.md @@ -40,6 +40,8 @@ verifications: [] 3. 카메라 열거에는 공개 Camera2 API만 사용합니다. 논리 카메라의 물리 endpoint를 독립적으로 열 수 있다고 가정하지 않습니다. 4. 벤치마크 JSON과 incident ZIP에는 관측 이벤트·메타데이터를 저장하며 이미지 픽셀을 저장하지 않습니다. +Depth 보기는 컬러와 Depth를 같은 요청에 포함하지 않는 별도 Camera2 세션입니다. HAL의 동시 요청 허용 선언은 특정 출력 조합의 성공을 보장하지 않습니다. `DepthCameraEngine`은 촬영·녹화·수동 제어·줌·터치 측광을 제공하지 않으며 CLI 제어 변경도 거절합니다. Photo·Video 또는 CLI 촬영 준비로 전환하면 Depth를 해제합니다. + Multi 모드의 `MultiSession`은 `CameraEngine`과 별도입니다. Camera2는 선택한 독립 장치를 연 뒤 세션을 구성하며, CameraX는 광고된 동시 조합을 바인딩합니다. 모든 장치의 종료 통지를 확인해야 출력을 해제하고 다음 동시 세션을 열 수 있습니다. 한 명령의 여러 사진을 센서 동기 촬영으로 해석하지 않습니다. diff --git a/guide/_content/architecture/module-roles.md b/guide/_content/architecture/module-roles.md index d006625f..4a224f72 100644 --- a/guide/_content/architecture/module-roles.md +++ b/guide/_content/architecture/module-roles.md @@ -94,7 +94,7 @@ verifications: []
      camera/ -- 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. +- 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. `DepthCameraEngine`은 컬러 출력 없이 DEPTH16 맵만 받으며 `DepthFrame`이 순수 Kotlin으로 표시용 표본을 만듭니다. - `close(done)` 완료 전에 다음 카메라를 열지 않습니다. - 엔진 계약(`MediaCapture`, `LiveTuning`, `TouchMetering`)과 두 엔진의 구현, 두 엔진의 차이는 [Engine Comparison](engine.md)에 있습니다. - 벤치마크 RECORD 단계의 recorder 상태 기계는 `BenchmarkRecorder`가 담당하고 `Camera2Engine`은 그 호출을 위임합니다. diff --git a/guide/_content/architecture/overview.md b/guide/_content/architecture/overview.md index adbc19cd..a2372915 100644 --- a/guide/_content/architecture/overview.md +++ b/guide/_content/architecture/overview.md @@ -32,7 +32,7 @@ verifications: [] | 화면 | 할 수 있는 일 | | --- | --- | -| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영합니다. | +| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영하며 지원 카메라의 Depth 맵을 봅니다. | | Multi · `ConcurrentCameraActivity` | 독립 카메라 장치별 사진·영상을 제공합니다. Live의 Multi · P와 Multi · V에서 엽니다. | | Lab · `WorkbenchActivity` | 기기 정보, 검사 도구, 저장된 결과와 설정을 엽니다. | | Benchmark · `BenchmarkActivity` | 정해진 조건(profile)으로 측정하고 결과를 저장합니다. | diff --git a/guide/_content/architecture/runtime-flow.md b/guide/_content/architecture/runtime-flow.md index 751d4095..b06d5103 100644 --- a/guide/_content/architecture/runtime-flow.md +++ b/guide/_content/architecture/runtime-flow.md @@ -39,6 +39,8 @@ verifications: [] Live 셔터 조작은 `MainActivity`에서 선택한 엔진의 촬영·녹화 동작으로 이어집니다. +Live의 Depth 보기는 `DepthCameraEngine → ImageReader → DepthFrame → DepthMapView`로 연결합니다. 컬러 촬영과 분리된 보기이며, Telemetry에는 버퍼의 시각·크기·형식과 capture 콜백만 기록합니다. 맵 픽셀을 벤치마크나 incident ZIP의 입력으로 전달하지 않습니다. 사용 순서는 [Depth 맵 보기](live.md#depth-맵을-보세요)에 있습니다. + 벤치마크는 별도 화면인 `BenchmarkActivity`가 준비를 마친 뒤 `BenchmarkRunner.start()`를 호출하여 시작합니다. `Telemetry.callback(...)`이 반환한 Camera2 콜백의 `onCaptureStarted`는 프레임워크 신호를 기록하며, Live 셔터와 벤치마크 시작을 연결하는 메서드가 아닙니다. diff --git a/guide/_reference/engine.md b/guide/_reference/engine.md index a547fad8..ea334c84 100644 --- a/guide/_reference/engine.md +++ b/guide/_reference/engine.md @@ -23,6 +23,7 @@ nav_order: 5 | 항목 | Camera2 | CameraX | | --- | --- | --- | +| Depth 보기 | DEPTH_OUTPUT과 DEPTH16 해상도를 공개한 카메라에서 별도 Depth 전용 세션을 엽니다. 맵 표시만 제공합니다. | Depth 해상도를 선택하면 Camera2로 전환합니다. | | 수동 촬영 | ISO·노출 시간·초점과 WB를 프리뷰·사진·녹화에 적용합니다. 지원 여부와 실제 적용값을 표시합니다. | Manual 버튼을 미지원 상태로 비활성화합니다. | | YUV를 JPEG로 선택 | 요청한 YUV 이미지와 최종 CaptureResult의 센서 시각을 맞춥니다. | 촬영 뒤 받은 analysis 프레임 하나를 변환합니다. | | JPEG 출처 | YUV 앱 변환 또는 HAL JPEG 중 하나와 촬영 JSON을 저장합니다. | YUV 앱 변환 또는 ImageCapture JPEG 중 하나와 촬영 JSON을 저장합니다. | @@ -59,7 +60,7 @@ PIP와 Multi는 Android 11 이상에서 제공합니다. 저장물과 설정 방 - 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review
      @@ -146,6 +147,7 @@ classDiagram class TouchMetering { <> } CameraEngine <|.. Camera2Engine CameraEngine <|.. CameraXEngine + CameraEngine <|.. DepthCameraEngine MediaCapture <|.. Camera2Engine MediaCapture <|.. CameraXEngine LiveTuning <|.. Camera2Engine @@ -156,6 +158,8 @@ classDiagram 점선 화살표는 인터페이스 구현을 뜻합니다. 실제로 사용할 수 있는 기능은 엔진의 실행 모드와 아래 조건에 따라 달라집니다. +`DepthCameraEngine`은 LIVE의 깊이 맵 보기에서만 사용하는 Camera2 엔진입니다. `CameraEngine`만 구현하고 컬러·촬영·녹화·수동 제어·터치 측광은 제공하지 않습니다. `Photo` 또는 `Video`로 전환하면 기존 컬러 엔진을 다시 엽니다. + | 인터페이스 | 담당하는 동작 | 구현 | | --- | --- | --- | | `CameraEngine` | 카메라 열기(`start`), 촬영(`capture`), 줌(`setZoom`), 닫기(`close(done)`) | 두 엔진 | @@ -174,7 +178,7 @@ classDiagram 종료 통지와 다음 엔진의 대기 순서는 [엔진 전환 그림](architecture.md#카메라-열기와-닫기)을 확인하세요. -Photo·Video·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지합니다. 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. +Photo·Video·Depth·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지하며 Depth 버튼은 해상도 선택창을 엽니다. 일반 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. Depth는 첫 맵 수신 후에도 촬영 버튼을 비활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. ### Benchmark와 CLI가 쓰는 엔진 @@ -184,7 +188,7 @@ CLI의 `preview`·`capture`·`record.start`는 `LiveController`를 통해 Camera ### 두 엔진이 함께 남기는 기록 -두 엔진은 세션을 열 때 `Telemetry.registerSession`에 엔진 이름(`Camera2` 또는 `CameraX`)을 남기고, 같은 `Telemetry.callback`으로 capture 콜백을 기록합니다. 그래서 `capture_started`, `capture_result` 같은 이벤트 종류가 엔진과 관계없이 같습니다. 엔진별로 요청을 만드는 방법이 다르므로, 같은 이벤트라도 요청에 들어간 키는 엔진마다 다를 수 있습니다. +일반 컬러 엔진은 세션을 열 때 `Telemetry.registerSession`에 엔진 이름(`Camera2` 또는 `CameraX`)을 남기고, 같은 `Telemetry.callback`으로 capture 콜백을 기록합니다. Depth 전용 엔진은 이름을 `Camera2 · Depth`로 남기며 같은 콜백 경로를 사용합니다. 그래서 `capture_started`, `capture_result` 같은 이벤트 종류가 엔진과 관계없이 같습니다. 엔진별로 요청을 만드는 방법이 다르므로, 같은 이벤트라도 요청에 들어간 키는 엔진마다 다를 수 있습니다. Live 제어와 터치 측광은 다음 이벤트를 추가로 남깁니다. @@ -213,9 +217,9 @@ Live 제어와 터치 측광은 다음 이벤트를 추가로 남깁니다.
      근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/ui/LiveControlBar.kt`, `app/src/main/java/dev/halcamera/ui/FocusRing.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/StartCardPresenter.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/ui/LiveControlBar.kt`, `app/src/main/java/dev/halcamera/ui/FocusRing.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/StartCardPresenter.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review
      @@ -232,6 +236,8 @@ Live 제어와 터치 측광은 다음 이벤트를 추가로 남깁니다. **Camera2Engine은 앱에서 CaptureRequest를 직접 구성합니다.** `request_observed`에 기록한 키를 HAL 로그와 대조할 수 있습니다. 엔진 자체는 세션, 요청 구성, 줌, Live 제어를 담당하고, 사진은 `Camera2StillCapture`, Live 녹화는 `Camera2LiveRecorder`, 터치 측광은 `Camera2TouchFocus`, 벤치마크 녹화는 `BenchmarkRecorder`가 맡습니다. 카메라 요청과 capture 콜백은 엔진이 만든 카메라 스레드 하나에서 처리하고, 버퍼 relay와 파일 저장은 각자의 스레드에서 처리합니다. +Depth 보기는 일반 `Camera2Engine`과 별도인 `DepthCameraEngine`이 담당합니다. 컬러·사진·녹화 출력 대신 DEPTH16 ImageReader 하나만 사용하며, 기존 엔진을 닫은 뒤 선택한 해상도로 엽니다. 사용 방법과 표시 범위는 [Depth 맵 보기](live.md#depth-맵을-보세요)를 확인하세요. + ### PIP 지원을 미리 확인하세요 **현재 해상도와 유지되는 YUV·JPEG·RAW 출력, 보조 카메라의 Preview·사진 출력으로 PIP 지원 여부를 조회합니다.** 선택창을 열 때와 적용 직전에 검사하며 제조사·칩셋·ID별 예외는 두지 않습니다. Physical은 열린 카메라의 `isSessionConfigurationSupported`, Service는 `CameraManager.isConcurrentSessionConfigurationSupported`를 사용합니다. 합성 녹화의 인코더는 GL 출력을 받으므로 카메라 세션 조회에 넣지 않습니다. @@ -597,7 +603,7 @@ Camera2에서 짧게 터치한 경우입니다. 긴 누르기는 별도의 AE - 근거 파일: `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt`, `app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamSettings.kt`, `app/src/main/java/dev/halcamera/camera/LiveStabilization.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamCapabilities.kt`, `app/src/main/java/dev/halcamera/camera/LiveSessionCheck.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/Camera2VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/camera/PreviewBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/StillPair.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/RawFrame.kt`, `app/src/main/java/dev/halcamera/camera/DngOutput.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/LiveControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/ManualControls.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review
      @@ -614,6 +620,8 @@ Camera2에서 짧게 터치한 경우입니다. 긴 누르기는 별도의 AE **CameraXEngine은 CameraX use case로 Live 촬영과 제어를 제공하지만, 요청 키 일부는 CameraX가 정합니다.** Live 스트림 설정에서 Preview·YUV·JPEG 크기와 출력 활성화를 선택할 수 있습니다. 앱이 CameraX 1.6.2에 직접 넣는 Camera2 키에는 AE 잠금(`CONTROL_AE_LOCK`), AF 잠금 해제용 cancel trigger, 광학 보정(`LENS_OPTICAL_STABILIZATION_MODE`)이 있습니다. 엔진 자체는 use case bind, 줌, 수명 주기를 담당하고, 사진은 `CameraXStillCapture`, 녹화는 `CameraXLiveRecorder`, Live 제어와 터치 측광은 `CameraXControls`가 맡습니다. +Depth 해상도를 선택하면 CameraX 세션을 해제하고 Camera2의 `DepthCameraEngine`으로 전환합니다. CameraX use case에 Depth 출력을 추가하지 않습니다. 표시와 촬영 제한은 [Depth 맵 보기](live.md#depth-맵을-보세요)를 확인하세요. + ### CameraX PIP **PIP 목록에는 모든 후보를 표시하고 지원 불가가 확인된 항목만 비활성화합니다.** Physical 후보는 `Camera2 only`로 표시합니다. Service 후보는 CameraX가 제공하는 동시 조합에 포함되어야 합니다. 제조사·칩셋·카메라 ID에 따른 예외는 두지 않습니다. @@ -814,7 +822,7 @@ AE 재잠금은 Camera2와 같은 [AeRelock 상태도](#노출은-언제-다시- - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXVideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/build.gradle.kts` - 근거 수준: 코드 확인 -- 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review @@ -834,11 +842,11 @@ AE 재잠금은 Camera2와 같은 [AeRelock 상태도](#노출은-언제-다시- | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 원고 `contract` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 원고 `camera2` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 원고 `camerax` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 원고 `comparison` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `contract` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `camera2` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `camerax` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `comparison` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | diff --git a/guide/_reference/evidence.md b/guide/_reference/evidence.md index 64d595d8..8c5f7f9b 100644 --- a/guide/_reference/evidence.md +++ b/guide/_reference/evidence.md @@ -80,7 +80,7 @@ Galaxy S25+·Android 16에서 2026년 9월 17~19일, versionCode 106~108로 수 - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt` - 기기 검증: `V-002` - 근거 수준: 기기 검증 -- 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review @@ -98,8 +98,8 @@ Galaxy S25+·Android 16에서 2026년 9월 17~19일, versionCode 106~108로 수 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 원고 `device-notes` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `device-notes` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | diff --git a/guide/_reference/troubleshooting.md b/guide/_reference/troubleshooting.md index f8cada1c..0cb500ca 100644 --- a/guide/_reference/troubleshooting.md +++ b/guide/_reference/troubleshooting.md @@ -140,7 +140,7 @@ PC의 대기 시간이 끝나도 앱 작업은 계속될 수 있습니다. 요 - 근거 파일: `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `tools/halcam/halcam/cli.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review +- 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review @@ -172,9 +172,9 @@ PC의 대기 시간이 끝나도 앱 작업은 계속될 수 있습니다. 요 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `data-flow` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 원고 `layer-isolation` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | +| 구조 원본 `data-flow` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | +| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | +| 원고 `layer-isolation` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | diff --git a/guide/architecture.md b/guide/architecture.md index 0b519a69..2154b888 100644 --- a/guide/architecture.md +++ b/guide/architecture.md @@ -32,7 +32,7 @@ nav_order: 4 | 화면 | 할 수 있는 일 | | --- | --- | -| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영합니다. | +| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영하며 지원 카메라의 Depth 맵을 봅니다. | | Multi · `ConcurrentCameraActivity` | 독립 카메라 장치별 사진·영상을 제공합니다. Live의 Multi · P와 Multi · V에서 엽니다. | | Lab · `WorkbenchActivity` | 기기 정보, 검사 도구, 저장된 결과와 설정을 엽니다. | | Benchmark · `BenchmarkActivity` | 정해진 조건(profile)으로 측정하고 결과를 저장합니다. | @@ -57,7 +57,7 @@ PC에서 같은 기능을 실행하려면 [CLI](cli.md)를 사용합니다. `Cli - 근거 파일: `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/WorkbenchActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review @@ -135,7 +135,7 @@ flowchart TB
      camera/ -- 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. +- 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. `DepthCameraEngine`은 컬러 출력 없이 DEPTH16 맵만 받으며 `DepthFrame`이 순수 Kotlin으로 표시용 표본을 만듭니다. - `close(done)` 완료 전에 다음 카메라를 열지 않습니다. - 엔진 계약(`MediaCapture`, `LiveTuning`, `TouchMetering`)과 두 엔진의 구현, 두 엔진의 차이는 [Engine Comparison](engine.md)에 있습니다. - 벤치마크 RECORD 단계의 recorder 상태 기계는 `BenchmarkRecorder`가 담당하고 `Camera2Engine`은 그 호출을 위임합니다. @@ -252,7 +252,7 @@ Camera2의 RAW (DNG)는 `RawFrame`이 RAW 샘플을 복사하고 `DngOutput`이 - 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/cli/BenchmarkController.kt`, `app/src/main/java/dev/halcamera/cli/CliLibrary.kt`, `app/src/main/java/dev/halcamera/MainCliBridge.kt`, `app/src/main/java/dev/halcamera/DualCliBridge.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt`, `app/src/main/java/dev/halcamera/ui/Look.kt`, `app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt`, `app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt`, `app/src/main/java/dev/halcamera/cts/CtsCatalog.kt`, `app/src/main/java/dev/halcamera/cts/CtsRunner.kt`, `app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt`, `app/src/main/java/dev/halcamera/cts/Camera2Ops.kt`, `app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt`, `app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt`, `app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt`, `app/src/main/java/dev/halcamera/CameraProbeActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbe.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review
      @@ -290,6 +290,8 @@ Live·CTS·Benchmark 컨트롤러는 `CliCompletion` 또는 `CliExecution`을 Live 셔터 조작은 `MainActivity`에서 선택한 엔진의 촬영·녹화 동작으로 이어집니다. +Live의 Depth 보기는 `DepthCameraEngine → ImageReader → DepthFrame → DepthMapView`로 연결합니다. 컬러 촬영과 분리된 보기이며, Telemetry에는 버퍼의 시각·크기·형식과 capture 콜백만 기록합니다. 맵 픽셀을 벤치마크나 incident ZIP의 입력으로 전달하지 않습니다. 사용 순서는 [Depth 맵 보기](live.md#depth-맵을-보세요)에 있습니다. + 벤치마크는 별도 화면인 `BenchmarkActivity`가 준비를 마친 뒤 `BenchmarkRunner.start()`를 호출하여 시작합니다. `Telemetry.callback(...)`이 반환한 Camera2 콜백의 `onCaptureStarted`는 프레임워크 신호를 기록하며, Live 셔터와 벤치마크 시작을 연결하는 메서드가 아닙니다. @@ -471,7 +473,7 @@ CliLaunchActivity는 Dual·Multi에서 Live로 돌아갈 때 CliNavigationHost - 근거 파일: `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `tools/halcam/halcam/download.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/GalleryActivity.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only +- 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review @@ -641,7 +643,7 @@ sequenceDiagram Live 프리뷰에서 두 손가락을 벌리거나 모으면 지원 범위 안에서 연속으로 줌을 바꿉니다. 가장 가까운 프리셋의 원에 현재 배율을 소수 첫째 자리까지 표시하며, 펼친 상태에서 그 원을 누르면 해당 프리셋 배율로 돌아갑니다. 화면의 카메라 선택 정보를 갱신해도 중간 배율은 유지합니다. -핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다. +핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. Depth 보기, 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다. | 조건 | 동작 | | --- | --- | @@ -683,6 +685,8 @@ Photo·Video 프리뷰와 녹화 중에 두 손가락을 벌리거나 모아 연 3. 카메라 열거에는 공개 Camera2 API만 사용합니다. 논리 카메라의 물리 endpoint를 독립적으로 열 수 있다고 가정하지 않습니다. 4. 벤치마크 JSON과 incident ZIP에는 관측 이벤트·메타데이터를 저장하며 이미지 픽셀을 저장하지 않습니다. +Depth 보기는 컬러와 Depth를 같은 요청에 포함하지 않는 별도 Camera2 세션입니다. HAL의 동시 요청 허용 선언은 특정 출력 조합의 성공을 보장하지 않습니다. `DepthCameraEngine`은 촬영·녹화·수동 제어·줌·터치 측광을 제공하지 않으며 CLI 제어 변경도 거절합니다. Photo·Video 또는 CLI 촬영 준비로 전환하면 Depth를 해제합니다. + Multi 모드의 `MultiSession`은 `CameraEngine`과 별도입니다. Camera2는 선택한 독립 장치를 연 뒤 세션을 구성하며, CameraX는 광고된 동시 조합을 바인딩합니다. 모든 장치의 종료 통지를 확인해야 출력을 해제하고 다음 동시 세션을 열 수 있습니다. 한 명령의 여러 사진을 센서 동기 촬영으로 해석하지 않습니다. @@ -742,7 +746,7 @@ Live 카메라 종료 후 벤치마크 화면으로 이동하고 기존 측정· - 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt`, `app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt`, `app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt`, `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt`, `app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only +- 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review @@ -794,16 +798,16 @@ Android 의존성이 없는 러너와 평가 로직은 JVM 단위 테스트로 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `data-flow` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | -| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-18 @ `e9d3233c` · codex-multi-cli-code-review | +| 구조 원본 `data-flow` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | | 구조 원본 `ui-camera-label` | 최신 | 검토 2026-10-10 @ `93385443` · Codex (mode reset, labels and numeric ID selection review) | -| 구조 원본 `ui-tool-handoff` | 최신 | 검토 2026-10-18 @ `e4bc487a` · codex-multi-cli-code-review | -| 구조 원본 `ui-zoom` | 최신 | 검토 2026-10-18 @ `3e7627e0` · claude-cli-specs-refactor | -| 원고 `overview` | 최신 | 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only | -| 원고 `module-roles` | 최신 | 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only | -| 원고 `runtime-flow` | 최신 | 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only | -| 원고 `constraints` | 최신 | 검토 2026-10-19 @ `cc58ea27` · codex-editorial-formatting-only | +| 구조 원본 `ui-tool-handoff` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | +| 구조 원본 `ui-zoom` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | +| 원고 `overview` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `module-roles` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | +| 원고 `runtime-flow` | 최신 | 검토 2026-10-19 @ `14ca1ae2` · codex-user-authorized-depth-review | +| 원고 `constraints` | 최신 | 검토 2026-10-19 @ `b0648a05` · codex-user-authorized-depth-review | diff --git a/guide/live.md b/guide/live.md index 9c4d3d56..d6b3a5a7 100644 --- a/guide/live.md +++ b/guide/live.md @@ -25,6 +25,19 @@ Photo로 한 장을 찍고, Video로 움직임을 담아 보세요. 처음에는 **모드나 엔진을 바꾸면 새 촬영으로 시작합니다.** PIP·스트림·줌·촬영 제어는 초기화됩니다. Callback의 고정 시간은 사용자 설정으로 유지합니다. +### Depth 맵을 보세요 + +1. `Lab → Probe`에서 카메라의 `Depth` 섹션을 확인합니다. +2. Live로 돌아와 카메라를 선택합니다. +3. 하단의 `Depth`를 누르고 `DEPTH16` 해상도를 선택합니다. 지원하지 않거나 정보를 읽지 못한 카메라는 이유를 표시합니다. +4. 컬러 화면으로 돌아가려면 `Photo` 또는 `Video`를 누릅니다. `Depth → Off`로도 Photo로 돌아갈 수 있습니다. + +**Depth는 Camera2의 깊이 맵만 표시하는 보기 모드입니다.** 선택하면 기존 카메라를 닫은 뒤 Depth 전용 세션을 엽니다. 컬러와 Depth를 같은 요청에서 받을 수 없는 기기에서도 사용할 수 있으며, 컬러와 정렬된 합성 화면은 제공하지 않습니다. 사진·동영상 저장, 줌·터치 측광·수동 제어와 PIP는 사용할 수 없습니다. + +가까운 곳은 밝고 먼 곳은 어둡습니다. 표시 범위는 0–8.191 m이며, 거리가 0이거나 신뢰도가 50% 미만인 픽셀은 검게 표시합니다. 중앙 거리와 유효 픽셀 비율도 표시합니다. 비율은 화면에 표시할 표본을 기준으로 계산합니다. 표시용 맵은 긴 변이 최대 640픽셀이 되도록 표본을 추출하며, 갱신은 최대 초당 10회입니다. + +선택한 해상도로 세션을 열지 못하거나 8초 동안 첫 Depth 프레임을 받지 못하면 이유를 표시합니다. 이때 다른 해상도를 선택하거나 `Photo`로 돌아가세요. 맵은 센서 방향과 전면 카메라의 좌우 반전을 반영하며 전체 영역을 잘라내지 않고 보여줍니다. Depth 픽셀을 Benchmark JSON이나 진단 ZIP에 저장하지 않습니다. + ### 동영상을 찍으세요 1. 하단에서 `Video`를 선택합니다. diff --git a/guide/probe.md b/guide/probe.md index 2870ec24..bb9c4321 100644 --- a/guide/probe.md +++ b/guide/probe.md @@ -28,13 +28,14 @@ title: Probe - **카메라 목록:** 논리 카메라와 그 안의 물리 카메라를 구분합니다. 사양이 보여도 단독으로 열 수 없는 물리 카메라가 있습니다. - **섹션:** 제목을 눌러 접거나 펼칩니다. 출력 형식별 크기는 STREAMS에서 확인합니다. +- **DEPTH:** `DEPTH_OUTPUT` 지원 여부와 `DEPTH16` 해상도, 컬러와 Depth를 같은 요청에 포함할 수 있는지를 확인합니다. `Allowed by HAL`은 기기의 선언이며 특정 출력 조합의 동작을 보장하지 않습니다. `Unknown`은 해당 정보를 읽지 못한 상태입니다. 단독으로 열 수 없는 물리 카메라는 LIVE에서 직접 선택할 수 없습니다. - **필터:** 단어가 들어간 줄을 찾습니다. 결과를 누르면 해당 항목으로 이동합니다. - **REQUEST · RESULT KEYS / SESSION KEYS:** 요청·결과에서 지원하는 key 이름입니다. 실제 촬영값은 아닙니다.
      섹션 이름과 API별 표시 조건 -IDENTITY, CAPABILITIES, SENSOR, LENS, CONTROL, PROCESSING, REQUEST 뒤에 REQUEST · RESULT KEYS가 나옵니다. +IDENTITY, CAPABILITIES, DEPTH, SENSOR, LENS, CONTROL, PROCESSING, REQUEST 뒤에 REQUEST · RESULT KEYS가 나옵니다. SESSION KEYS는 API 28 이상, MANDATORY STREAM COMBINATIONS는 API 29 이상에서 표시합니다. diff --git a/tools/docgen/regression.test.mjs b/tools/docgen/regression.test.mjs index 0cec2db1..e92f96ab 100644 --- a/tools/docgen/regression.test.mjs +++ b/tools/docgen/regression.test.mjs @@ -114,7 +114,7 @@ test('all configured elements fit the prompt budget and exclude descendant field assert.ok(input.files.length > 0); count++; } } - assert.equal(count, 111); // Includes Multi's separate CLI adapter, session and split-layout evidence. + assert.equal(count, 112); // Includes the LIVE depth engine's own evidence. `)); }); diff --git a/tools/docgen/state/evidence.json b/tools/docgen/state/evidence.json index 986baf01..35173d0e 100644 --- a/tools/docgen/state/evidence.json +++ b/tools/docgen/state/evidence.json @@ -6,11 +6,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "418ed3d6b45cba29", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "c896f57e566251f5", + "modelHash": "0f722523cc241bb1", + "at": "2026-10-19", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -20,8 +20,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -75,7 +76,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -93,6 +94,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -238,6 +243,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -255,7 +261,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -285,14 +291,14 @@ "state": "fresh" }, "accepted": { - "codeHash": "22eb6a5c54fd7836", - "modelHash": "9c9db2cb22824dcb", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "97b991ec49bfd746", + "modelHash": "9dbb60c0c18065a2", + "at": "2026-10-19", + "commit": "14ca1ae2", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", "app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt": "3ce60c16472f2ed3e100034c62feee32f13236d3", @@ -336,13 +342,13 @@ "state": "fresh" }, "accepted": { - "codeHash": "98c3b8dd8a671c51", - "modelHash": "32a6fe2cd2a86e7a", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "47bddf1bbf31016c", + "modelHash": "258630c74fc0a4f4", + "at": "2026-10-19", + "commit": "14ca1ae2", + "reviewer": "codex-user-authorized-depth-review", "files": { - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", "app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt": "3ce60c16472f2ed3e100034c62feee32f13236d3", @@ -380,11 +386,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "17867e1678c12800", - "modelHash": "52012a5183697d02", + "codeHash": "19ab0340fcab873e", + "modelHash": "66bd16d22ce9ca3f", "at": "2026-10-19", - "commit": "cc58ea27", - "reviewer": "codex-editorial-formatting-only", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -394,8 +400,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -449,7 +456,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -467,6 +474,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -612,6 +623,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -629,7 +641,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -662,14 +674,14 @@ "state": "fresh" }, "accepted": { - "codeHash": "d8bf9992cd2c94cb", - "modelHash": "8baed94c0132e532", + "codeHash": "1bbf79a32baf27f3", + "modelHash": "2f360beb8b83bf25", "at": "2026-10-19", - "commit": "cc58ea27", - "reviewer": "codex-editorial-formatting-only", + "commit": "14ca1ae2", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", "app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt": "3ce60c16472f2ed3e100034c62feee32f13236d3", @@ -711,11 +723,11 @@ }, "content:architecture.md/module-roles": { "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "f0177f51617171de", + "codeHash": "c896f57e566251f5", + "modelHash": "8ffefe5ee952f837", "at": "2026-10-19", - "commit": "cc58ea27", - "reviewer": "codex-editorial-formatting-only", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -725,8 +737,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -780,7 +793,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -798,6 +811,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -943,6 +960,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -960,7 +978,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -990,11 +1008,11 @@ }, "content:architecture.md/constraints": { "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "09630393d3c7be02", + "codeHash": "c896f57e566251f5", + "modelHash": "90c7c730cb5e8337", "at": "2026-10-19", - "commit": "cc58ea27", - "reviewer": "codex-editorial-formatting-only", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1004,8 +1022,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -1059,7 +1078,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -1077,6 +1096,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -1222,6 +1245,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -1239,7 +1263,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -1288,14 +1312,14 @@ "state": "fresh" }, "accepted": { - "codeHash": "9723c8ef764a7680", - "modelHash": "735607d172653acf", - "at": "2026-10-18", - "commit": "e4bc487a", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "5733479109e046e8", + "modelHash": "9d479f2b55b0b1b7", + "at": "2026-10-19", + "commit": "14ca1ae2", + "reviewer": "codex-user-authorized-depth-review", "files": { - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127" + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da" } } }, @@ -1304,13 +1328,13 @@ "state": "fresh" }, "accepted": { - "codeHash": "3d7b55295e557886", - "modelHash": "c4c3b1236181dcfb", - "at": "2026-10-18", - "commit": "3e7627e0", - "reviewer": "claude-cli-specs-refactor", + "codeHash": "0c74536c0bae608f", + "modelHash": "231d9c4ddb3622f9", + "at": "2026-10-19", + "commit": "14ca1ae2", + "reviewer": "codex-user-authorized-depth-review", "files": { - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", "app/src/main/java/dev/halcamera/ui/FocusRing.kt": "e129ce223edde62c329a15b5662fa34f9f6a6918" } @@ -1321,11 +1345,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "f461c36a13caa3ff", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "c896f57e566251f5", + "modelHash": "a882f52b4155cd27", + "at": "2026-10-19", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1335,8 +1359,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -1390,7 +1415,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -1408,6 +1433,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -1553,6 +1582,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -1570,7 +1600,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -1600,11 +1630,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "89c890e64681f1fd", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "c896f57e566251f5", + "modelHash": "a1c4cf8671b1c8c4", + "at": "2026-10-19", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1614,8 +1644,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -1669,7 +1700,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -1687,6 +1718,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -1832,6 +1867,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -1849,7 +1885,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -1879,11 +1915,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "bc131fb4883c6822", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "c896f57e566251f5", + "modelHash": "ab1f7e43500f5869", + "at": "2026-10-19", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1893,8 +1929,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -1948,7 +1985,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -1966,6 +2003,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -2111,6 +2152,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -2128,7 +2170,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -2158,11 +2200,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "d7255688da5f38c4", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "c896f57e566251f5", + "modelHash": "1ac54c16fae61b66", + "at": "2026-10-19", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -2172,8 +2214,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -2227,7 +2270,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -2245,6 +2288,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -2390,6 +2437,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -2407,7 +2455,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b", @@ -2437,14 +2485,14 @@ "state": "fresh" }, "accepted": { - "codeHash": "127e509c8189a9ce", - "modelHash": "2d4f51d2d054e8c9", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "9edb9935832a8bbc", + "modelHash": "20708f2ee142c5a7", + "at": "2026-10-19", + "commit": "14ca1ae2", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", "app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt": "3ce60c16472f2ed3e100034c62feee32f13236d3", @@ -2499,11 +2547,11 @@ "state": "fresh" }, "accepted": { - "codeHash": "4ee54ca1bee6a0f5", - "modelHash": "cb4d87333b72f2fe", - "at": "2026-10-18", - "commit": "e9d3233c", - "reviewer": "codex-multi-cli-code-review", + "codeHash": "c896f57e566251f5", + "modelHash": "ab3da8834574c341", + "at": "2026-10-19", + "commit": "b0648a05", + "reviewer": "codex-user-authorized-depth-review", "files": { "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -2513,8 +2561,9 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "6dea5501360372fa3ae1ce5f176be9191544d152", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "51714cbb146d31b940b6cf81e3f54011cda31127", + "app/src/main/java/dev/halcamera/MainActivity.kt": "0fa42b6713f8b20632ec77a6b4afd57cf3f80ebb", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "fde7bf3ae0a6f1a451fa84718a37d90052efd6da", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1d8d7f6f32f0b2feaa59c279bb0d0a31481acf4b", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", @@ -2568,7 +2617,7 @@ "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt": "65d85b90a24ff613324cceb838f963028e1d5daa", "app/src/main/java/dev/halcamera/camera/CameraProbe.kt": "c7bc7ce608c811830e4a88e913e0593ad15b0528", - "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "34992e8456e43c174ed65b8964281b7b3b8ef8dc", + "app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt": "5da08af7e2f1b94dc6bf09fd3f94f01fed86c7f9", "app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt": "cd2fb3f441229ac0e4a4d4f09ce7b27de5e64d4e", "app/src/main/java/dev/halcamera/camera/CameraXControls.kt": "2bdcc32c326a7685cb34ac5a59f2665cce940fc8", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -2586,6 +2635,10 @@ "app/src/main/java/dev/halcamera/camera/ConcurrentPhotoStore.kt": "62d925a86f82fc14c62f8fc5114b2d237579321e", "app/src/main/java/dev/halcamera/camera/ConcurrentPlan.kt": "1c9c43c4519dcc619ec711e2dcfc5cd288014fa9", "app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt": "7d4f69e446d9c4a4cc8b756dc2cca0e483841aae", + "app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt": "be3c0e9b1c9081ca426f2b48619b9f04e9888290", + "app/src/main/java/dev/halcamera/camera/DepthCapabilities.kt": "1c8b524293c58b7de5e1d98e2f874c9a2591df95", + "app/src/main/java/dev/halcamera/camera/DepthFrame.kt": "afa6b06b888773e845baf86d52503a6d47b26b98", + "app/src/main/java/dev/halcamera/camera/DepthSupport.kt": "5206985fdeb532bfe5ecf91d7d1624ce59c00fff", "app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt": "266223bc206069ecc0c3d0282c36b006426ec1b2", "app/src/main/java/dev/halcamera/camera/DisplayRotation.kt": "c4d0f95d9566afe62dbcf943ab5c163ccaef6e99", "app/src/main/java/dev/halcamera/camera/DngOutput.kt": "fe89b853141b0fc0dd898570c50ff6718dc6d234", @@ -2731,6 +2784,7 @@ "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", + "app/src/main/java/dev/halcamera/ui/DepthMapView.kt": "e4cdc1cec83c492dee00d3f028816d235c9f1258", "app/src/main/java/dev/halcamera/ui/DeviceIdentity.kt": "df26ad999c1f19768883838d61ac8a6eb78aa34d", "app/src/main/java/dev/halcamera/ui/EvRuler.kt": "365ee3af8ab4bae41e9cbf39a107afeb31c0d539", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", @@ -2748,7 +2802,7 @@ "app/src/main/java/dev/halcamera/ui/LivePipController.kt": "7f706fae51c274c351cee16032a7d1b300563828", "app/src/main/java/dev/halcamera/ui/LiveReadings.kt": "5fb091d0da47c97dee10c62d79492b83c382bf3e", "app/src/main/java/dev/halcamera/ui/LiveReadout.kt": "3f599e31c9eecd3f48c7467632c1c948f02bbdfc", - "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "1a2fa3a58175bf2e28c579f53bfb6a62b7db6dd3", + "app/src/main/java/dev/halcamera/ui/LiveStreamReadout.kt": "976ee52adcfec7b08dc0571ff9bdde7d36e1f72a", "app/src/main/java/dev/halcamera/ui/LiveStreamSettingsView.kt": "d47f8595645c0af61d3bc088c0b6f6773de6dd8d", "app/src/main/java/dev/halcamera/ui/Look.kt": "51326eb313529537407c10dae5e721eb153a9f53", "app/src/main/java/dev/halcamera/ui/ManualControlPanel.kt": "baa1572bd2a5db79b016d066a6d5b5d1105a326b",