Investigation notes for cronos (Echo Show 5 2nd gen, 2021). Everything below
was measured against a running device, build
18.1-20260418-UNOFFICIAL-cronos, kernel 4.9.337.
The received summary of this bug is that the camera is broken because Android 7
blobs are incompatible with the newer kernel. On cronos that is not what is
happening, because there is no camera userspace on the device at all:
| Component | State |
|---|---|
| Kernel sensor driver | present (CONFIG_CUSTOM_KERNEL_IMGSENSOR="ov9734_mipi_raw") |
| Kernel ISP / imgsensor | present (CONFIG_MTK_CAMERA_ISP=y, CONFIG_MTK_IMGSENSOR=y) |
| Camera device nodes | present (/dev/camera-isp, camera-pipemgr, camera-sysram, kd_camera_hw, kd_camera_hw_bus2) |
cameraserver |
running |
| AOSP device shims | present (camera.device@1.0-impl.so, camera.device@3.2-impl.so) |
| Camera provider HAL | absent |
| MediaTek camera blobs | absent (zero libcam* in /vendor/lib) |
Opening the camera app produces no crash and no denial, just:
CAM_Camera2OneCamMgr: No back-facing camera found.
CAM_Camera2OneCamMgr: No front-facing camera found.
The vendor manifest declares android.hardware.camera.provider@2.4 at
internal/0 over hwbinder. That is MediaTek's own provider, served by a
camerahalserver binary that Amazon does not ship on Echo Show devices and that
this ROM does not build. So the camera service resolves the provider to nothing
and enumerates zero devices. Nothing has failed yet — the stack was simply never
wired up on this device.
This matters for expectations: the crash chain documented for crown
(CameraDevice::sGetMemory, BsMapper::importBuffer, and so on) is what you get
after the blobs are installed. On cronos those crashes are downstream of work
that has not been done yet.
The OV9734 static metadata — sensor, lens, flashlight and 3A tuning — is
compiled into the stock libcam.halsensor.so. Strings recovered from it name the
original MediaTek source paths:
vendor/mediatek/proprietary_mt8163/custom/mt8163/hal/imgsensor_metadata/ov9734_mipi_raw/
config_static_metadata.sensor.ov9734mipiraw.h
config_static_metadata.lens.ov9734mipiraw.h
config_static_metadata.tuning_3a.ov9734mipiraw.h
config_static_metadata.project.camera.ov9734mipiraw.h
config_static_metadata.project.flashlight.ov9734mipiraw.h
This rules out the otherwise attractive shortcut of transplanting karnak's
camera stack. karnak (Fire HD 8 2018) is the same SoC, its blob set is a
generation newer (API 28, Treble, camerahalserver + mtkcam3), and it is proven
on LineageOS 18.1 through 23 — but its tuning only covers GC2375 sensors:
$ strings libcameracustom.so | grep -oiE '\bov[0-9]{4}|gc[0-9]{4}'
gc2375mipi_raw_blx_front
gc2375mipi_raw_chxt_rear
No OV9734, and the stack is closed source, so the metadata cannot be added.
MediaTek's MT8163 camera HAL sources are not public either — the one promising
repository (vendor-mtk-sources/Amazon-Fire_HD8_8th_Gen) is Amazon's GPL kernel
release, kernel only.
So the stock cronos blob set is the only viable source, and it is API 25
(Android 7.1) even though the surrounding platform is Amazon's amz-p
(Android 9) branch. All three Echo Show devices share this: checkers,
crown and cronos fingerprints all end amz-p, and all three carry
MediaTek's legacy camera1 HAL rather than the mtkcam3 stack of the same era.
Stock images are on GitHub as firmware dumps
(el-vertedero/amazon_cronos_dump, branch
cronos-user-6.0-NS6573-6567-amz-p,release-keys), which is where the measurements
below come from.
This is the part that changes the outlook. Taking the closure of the stock camera stack and resolving every imported symbol against what this device actually provides:
- 45 MediaTek libraries make up the closure (34
libcam*/libmtkcam*, plus 11 transitive dependencies). 17 MB total. - Every other dependency —
libgui,libui,libbinder,libutils,libcamera_client,libcamera_metadata,libhardware,libcutils,libmedia,libdpframework,libsensor, libc, libc++ — is already on the device. - 11 symbols do not resolve. That is the entire link-level gap.
They fall into three groups, and all eleven are the same kind of drift: the current implementation still has the method, it just gained trailing parameters.
_ZN7android9CallStackC1Ev
_ZN7android9CallStackD1Ev
_ZN7android9CallStack6updateEii
_ZNK7android9CallStack3logEPKc19android_LogPriorityS2_
No longer exported. Used only for diagnostic backtraces, so stubs are fine.
| Blob wants | Device has |
|---|---|
GraphicBuffer(w,h,fmt,usage,stride,handle,keep) |
same plus layerCount |
GraphicBuffer::lock(usage, vaddr) |
same plus outBytesPerPixel, outBytesPerStride |
GraphicBufferMapper::lock(handle, usage, bounds, vaddr) |
same plus the two out-params |
Forward with layerCount = 1 and null out-params.
The ROM's libdpframework.so comes from the Android 9 blob set, where these
gained a trailing DpSecure argument and a timeval deadline:
| Blob wants | Device has |
|---|---|
DpIspStream::setSrcConfig(...,bool) |
...,bool,DpSecure |
DpIspStream::setDstConfig(...,bool) |
...,bool,DpSecure |
DpIspStream::startStream() |
startStream(timeval*) |
DpBlitStream::invalidate() |
invalidate(timeval*) |
Forward with DP_SECURE_NONE and a null deadline.
shims/libcamera_shim/ implements all eleven.
The static analysis above held up, but linking was only the first of several layers. Recorded here in the order they were hit.
The shim was built, installed, and completely unused. Nothing in the blobs'
DT_NEEDED referenced it. This tree's extract_utils has no shim mechanism,
and the blobs arrive as plain PRODUCT_COPY_FILES with no Soong module, so
there is no shared_libs to declare either. The entries have to be patched in
directly with the bundled patchelf across the 28 blobs that import shimmed
symbols - see scripts/patch-shim-needed.sh.
libdpframework cannot be linked against. Soong fails with depends on undefined module "libdpframework" for the same reason: it is copied, not built.
The four DpIspStream/DpBlitStream forwarders resolve their targets with
dlsym instead.
One blob needed a library, not a shim. libcam.utils.sensorlistener.so
imports android::SensorManager and android::SensorEventQueue and lists
libgui.so, which is where they lived on Android 7. They moved to libsensor.so
in Android 8 with unchanged signatures, so naming that library is enough.
Everything else that failed to load (libcam.client, libcam.camadapter) did
so transitively through this one blob.
With those three done, all 45 libraries load on the device with zero failures, confirmed by having the device's own linker load each one.
The passthrough -impl module links its whole backend family, so -legacy,
-external and the camera.device@3.x-impl wrappers (external variants
included) must all be installed or the dlopen fails with "library not found",
silently, with the camera service simply reporting no providers.
With everything loading, cameraserver crash-loops. The call chain is clean and
goes all the way down:
CameraService::enumerateProviders
-> PassthroughServiceManager -> HIDL_FETCH_ICameraProvider
-> LegacyCameraProviderImpl_2_4::initialize -> CameraModule::init
-> camera.mt8163.so CamDeviceManagerImp::enumDeviceLocked
-> HalSensorList::searchSensors -> ImgSensorDrv::impSearchSensor
-> SIGSEGV in ImgSensorDrv::getCurrentSensorType, fault addr 0x24
strace gives the real cause:
openat("/dev/kd_camera_hw", O_RDWR) = 9
ioctl(9, _IOC(_IOC_READ|_IOC_WRITE, 0x69, 0x3c, 0x8), ...) = -1 ENOTTY
0x3c is 60, KDIMGSENSORIOC_X_SET_MCLK_PLL, requested with an 8-byte struct.
The kernel implements that ioctl, but _IOWR encodes sizeof() into the ioctl
number and this kernel has two layouts behind a config switch:
| Header | ACDK_SENSOR_MCLK_STRUCT |
Size |
|---|---|---|
kd_imgsensor_define_checkers.h |
MUINT8 on; enum freq |
8 |
kd_imgsensor_define_default.h |
MUINT8 on; MUINT32 freq; MUINT8 TG |
12 |
Only checkers_defconfig sets CONFIG_CHECKERS, so cronos builds the 12-byte
layout while its own stock blobs ask for 8. Different size, different ioctl
number, ENOTTY. The HAL then proceeds without the sensor type it needed and
dereferences null.
So the cmdq/ION risk flagged below was real in category but not in specifics:
the kernel interface did diverge, but as the same trivial parameter drift seen
everywhere else in this port, just expressed through an ioctl number. Fix in
patches/0001-imgsensor-use-the-Amazon-MCLK-struct-layout-on-cronos.patch.
It should apply to crown and rook as well, which have the same mismatch.
Testing it requires a kernel build and a boot partition flash, which is where this currently stands.
With the kernel header fix in place the camera service reports:
Number of camera devices: 1
== Camera Provider HAL legacy/0 (v2.5, passthrough) static info: 1 devices ==
== Camera HAL device device@1.0/legacy/0 (v1.0) static information ==
Facing: Front
Orientation: 270
cameraserver is stable rather than crash-looping, the MediaTek HAL creates a
Cam1Device, and 3A / Sensor / Resource HALs all initialize. The kernel
correctly identifies the sensor:
$ cat /proc/driver/camera_info
CAM[1]:ov9734_mipi_raw;
An application can open the camera - a third-party CameraX app acquired camera 0
successfully, which is why the stock camera app then reported
CAMERA_DISCONNECTED (the legacy HAL1 path allows a single client).
Two candidate fixes were tried. The narrow one - aliasing the ioctl number so
the 8-byte SET_MCLK_PLL is accepted (patches/0002-*) - works at the ioctl
level: after it, _IOC(READ|WRITE, 0x69, 0x3c, 0x8) returns 0 and the trace
contains zero ENOTTY. But cameraserver still crashed in exactly the same
place, because fixing one command's size encoding leaves every other struct
layout mismatched.
The decisive one is the header selection (patches/0001-*).
KDIMGSENSORIOC_X_GETINFO passes the kernel a pointer, and the kernel writes
ACDK_SENSOR_INFO_STRUCT through it. That struct differs between the two header
variants by 20 SensorGrabStartX/Y_* fields plus a reordering, so the HAL reads
every subsequent field at the wrong offset. No ioctl number can guard against a
mismatch inside a buffer the kernel fills in. Selecting the Amazon layout fixes
the whole family at once and makes ACDK_SENSOR_MCLK_STRUCT 8 bytes natively,
which subsumes the alias.
The two patches are mutually exclusive: applied together they produce duplicate
case labels and fail to compile.
Do not dd a plain boot.img over the boot partition, and do not write it at an
offset either. On these amonet-unlocked devices the partition is:
| Range | Content |
|---|---|
0x000-0x3FF |
amonet exploit header, byte-identical to recovery[0x000:0x400] |
0x400-0x7FF |
copy of the real boot image's first 1 KB (its header) |
0x800- |
boot image payload, kernel at its normal page offset |
amonet does not shift the image - it steals blocks 0-1 and relocates the real
header to blocks 2-3, exactly as TWRP's /sbin/fix-bootpatch.sh assumes. Two
devices were bricked here by assuming the payload moved with the header, which
put the kernel at 0xC00; it hangs at the vendor logo with no other symptom, and
looks identical to a bad kernel. The tell is the gzip magic: a good image has
1f8b at 0x800.
scripts/flash-boot.sh implements this and has a --self-test that rebuilds a
known-good partition from its parts and requires a byte-for-byte match.
Also worth knowing while debugging kernels here: the running kernel embeds its
source commit in its version string (4.9.337-gc374d6392fa0), and
/proc/config.gz lets you diff your build config against the shipped one - both
were needed to rule out toolchain and config as causes. The shipped kernel was
built with the same Linaro GCC 6.3-2017.02 that prebuilts/linaro provides.
With the correct sensor driver the stack goes much further. In order, each fix exposing the next layer:
-
Kernel panic in the ported driver's
feature_control. Fixed. It took the output pointer from the parameter buffer, butadopt_CAMERA_HW_FeatureControl()copies that buffer into a kernel allocation andcopy_to_user()s it back - this feature id is in its writeback list - so word 1 is an output slot, not a userspace pointer. Dereferencing it panicked atfeature_control+0x6c0,str w0,[x1]with w0 = 300 (30fps x 10). Write into the buffer and let the wrapper return it. -
Unsupport SensorOutputDataFormat. Fixed. The reference driver reportedSENSOR_OUTPUT_FORMAT_RAW_MONO, which this platform's HAL rejects and which is wrong for a colour Bayer sensor anyway. Set toSENSOR_OUTPUT_FORMAT_RAW_B(matching the in-tree ov9734); if colours come out swapped, rotate this value. -
The sensor is now correctly identified end to end:
$ cat /proc/driver/camera_info CAM[1]:ov02b10_mipi_raw; ImgSensorDrv: [impSearchSensor]found <0x2b/ov02b10_mipi_raw/SENSOR_DRVNAME_OV02B_MIPI_RAW> ImgSensorDrv: [getInfo2]prv w=0x640,h=0x4b0 # 1600x1200, the 2MP geometryCore OV02B static metadata resolves; only optional SCALER/FEATURE/REQUEST entries are reported missing.
-
CameraDevice::sGetMemory+18, fault addr0x8c. Fixed in source. This is the same crash the crown investigation hit as its "Bug 1" and solved by binary-patching the library with LIEF; because we buildcamera.device@1.0-implourselves, it is a normal patch instead.The cause is simpler than that investigation concluded: MediaTek's
libcam.client.socalls theget_memorycallback without forwarding the cookie passed toset_callbacks(), souserarrives NULL andobject->mDeviceCallbackfaults at offset0x8c. Only one CameraDevice is ever open in the process, so recording it atopen()and falling back to it when the cookie is missing fixes it cleanly. Seepatches/0005-camera-device-1.0-tolerate-HALs-that-drop-the-callback-cookie.patch.
The private-copy approach works. Giving the camera blobs their own API 25
libdpframework under a private soname eliminates the crash entirely, and the
display and media stacks keep the Android 9 library untouched. See
scripts/install-private-dpframework.sh; the DpIspStream/DpBlitStream
forwarders were removed from libcamera_shim because the private library
exports those signatures natively.
This matters beyond this one crash: the coupling described upstream - that the
camera needs an older cmdq/display stack while display and media need the
newer one - is what made this port look unfixable. It is only a conflict if both
have to use the same library. Per-consumer versioning dissolves it, and all
ten camera blobs that link the library still resolve cleanly afterwards
(verified with the device's own linker).
Fixed in the shim. sDequeueBuffer used to hand back &mCirculatingBuffers[id]
- the address of a standalone
buffer_handle_tin a map. A realANativeWindowreturns&anwBuffer->handle, a pointer into anANativeWindowBuffer, and MediaTek's blob recovers that object by offsetting back from it. Each imported handle is now wrapped in aGraphicBuffer(WRAP_HANDLE, geometry recorded byset_buffers_geometry) and&gb->handleis returned instead. All crashes stop; the camera app reaches its normal UI rather than "Can't connect to the camera".
The reference driver deferred stream-on to streaming_control(), which is only
reached from SENSOR_FEATURE_SET_STREAMING_SUSPEND/RESUME - features belonging
to MediaTek's newer imgsensor framework that this platform's HAL never
issues. So the sensor was fully configured and never told to start. The
contrast with the in-tree ov9734 driver is stark: its entire preview_setting()
is one line, write_cmos_sensor(0x0100, 0x01) /* Stream on */.
Fixed by calling streaming_control(KAL_TRUE) at the end of all five mode
setters (preview/capture/normal_video/hs_video/slim_video). Result:
irq_TG1_DONE timeouts: 0 (was: every frame, in a reset loop)
Capture failed: 0
Camera error: 0
The camera app now shows a live preview surface with a working shutter button and
no error dialogs, and takePicture runs through to JPEG encoding.
An earlier version of this document said frames were being delivered but arrived empty. That was wrong, and the correction matters for what to do next:
dequeueBuffer calls: 0
enqueueBuffer calls: 0
Nothing is ever handed to the display client. The uniform green is simply the preview surface's initial state - zero-filled YUV420 converts to RGB(0,135,0) in BT.601, which is exactly that shade. It looked like "delivered but blank"; it is "never delivered". Bayer order is therefore not a candidate: a wrong Bayer pattern corrupts content, it cannot remove it.
What actually happens is a crash in the private display framework:
#05 DpIspStream::dequeueDstBuffer(int, void**, bool)+188 libdpframework_cam.so
#04 DpStream::stopStream()+18
#03 DpPathControl::onStopStream()+110
#00 DpPortAdapt::abortPoll(CLIENT_TYPE_ENUM)+2 null deref, fault 0x0
called from MdpMgrImp::dequeueBuf libimageio_plat_drv.so
dequeueDstBuffer cannot obtain a buffer, tears the stream down, and the
teardown path itself dereferences null. The null deref is a symptom; the primary
failure is the dequeue.
This is likely the kernel-ABI half of the coupling. The private-copy trick
fixed the userspace C++ object-size mismatch, but the API 25 libdpframework
also talks to the kernel's MDP/cmdq driver, and this kernel has the newer
CONFIG_MTK_CMDQ_TAB. Two caveats before treating that as established: dmesg
shows no cmdq errors at all, and the failure could equally be a buffer the old
library cannot map. Unproven either way.
Separately, and unexplained: during the capture path the kernel logs
[Camera-ISP][ISP_ioctl] ERROR: Fail, Cmd(1074817798)
CORRECTED - this was a bad lead, do not chase it. An earlier revision
decoded 1074817798 as 0x40144806, magic 0x48 ('H'), and concluded some
foreign driver's ioctl was being issued on the ISP fd. That arithmetic was
wrong. It is 0x40106b06: _IOW, size 16, magic 0x6b = 'k' = ISP_MAGIC,
nr 6 = ISP_CMD_WAIT_IRQ.
So it is not a foreign ioctl at all - it is the ISP's own wait-for-interrupt
call reporting the timeout that is already visible in logcat as
ISP_WAIT_IRQ fail. It is one symptom, not a second bug. See "Open: takePicture
never delivers a JPEG" and docs/handoff-takepicture.md.
The abortPoll crash itself is now fixed by binary patch (see "the private
library, binary patched" below), and the dequeue failure has been traced further
down, to the cmdq submit/wait handshake.
shims/libdpframework_cam/ replaces the private old library with an adapter
over this ROM's Android 9 libdpframework. Of the 28 symbols the camera blobs
import, 24 have byte-identical signatures in both versions; the caller's
undersized object is used to hold nothing but a pointer to a real, heap-allocated
Android 9 object (over-allocated to 8 KB, since its true size is unknowable from
outside), and every method loads that pointer and forwards. The four drifted
methods are adapted here rather than in libcamera_shim.
Result: dequeueDstBuffer no longer fails, no DpPortAdapt::abortPoll null
deref, no TG1 timeouts. This keeps userspace aligned with the kernel, which
removes the whole kernel-ABI question the private-copy approach ran into.
One trap worth knowing: the blobs import more from libdpframework than the
Dp classes - plain functions like
tpipe_main_query_platform_working_buffer_size(int) too. Defining only the 28
broke camera.mt8163.so loading outright and enumeration dropped to zero.
The fix is to patchelf --add-needed libdpframework.so onto the adapter, so
anything it does not define resolves transitively while its own 28 still win by
search order.
Why it was shelved: the adapter ran, the crashes stopped, but no frames
could move. The Android 9 libdpframework rejects the camera blobs' buffers
with not support alloc by va: the old stack hands the display framework plain
malloc'd virtual addresses and the API 25 library translated them internally,
while the Android 9 library only accepts ION buffers. Bridging that inside the
adapter would mean allocating shadow ION buffers and copying every frame both
ways. Rather than build that, the work returned to the private API 25 library
and fixed its kernel interface directly, which turned out to be far closer to
working than assumed (see the cmdq ABI section below).
The current on-device approach: the stock API 25 libdpframework.so installed
under the private soname libdpframework_cam.so (as in the private-copy section
above), plus exactly three binary patches. For patching, file offset = vaddr
minus 0x1000 in this library, and all code is Thumb.
- Device node path. The old library opens
/proc/mtk_cmdq; this kernel creates/dev/mtk_cmdqinstead, so every open failed withcan't open display driver. String patch, one occurrence:/proc/mtk_cmdqbecomes/dev/mtk_cmdq(NUL padded). DpPortAdapt::abortPollmade a no-op. The thunk at vaddr0x14b2c(ldr r0,[r0,#0x20]; ldr r2,[r0]; ldr r2,[r2,#0x3c]; bx r2) is replaced withmovs r0,#0; bx lr; nop; nop. It dereferences a buffer pool pointer atthis+0x20that is already freed on the teardown path, and aborting an in-flight poll early is an optimization, not a correctness requirement.DpDriver::queryEngUsagesreports all engines idle. The stock code at vaddr0x1d696issuesCMDQ_IOCTL_QUERY_USAGE(_IOW('x', 4, 144)), which this kernel's MDP driver does not service compatibly, leaving the 36-word usage array garbage. The call is replaced with an inline loop that zero-fills the array.
scripts/install-private-dpframework.sh now reproduces the patched library
byte for byte from the public firmware dump (fetch, soname rename, all three
patches, with the original bytes verified before each write).
An earlier iteration of the patched library instead NOPed the
blx DpStream::stopStream at 0x18358 in dequeueDstBuffer's failure path,
reasoning that teardown-on-transient-failure was what crashed in abortPoll.
That produced an endless loop of
DpChannel: source port is in locked state then startStream fail(-7).
Disassembly of DpStream::stopStream (0x153b8) shows why: it is the unlock.
It takes the stream mutex, calls DpPathControl::onStopStream, walks the
channel list at this+0x1f8 calling DpChannel::unlockChannel on every node,
and marks the stream stopped. (lockChannel stores 0 to DpChannel+4,
unlockChannel stores 1.) Skipping stopStream therefore leaves the source port
locked forever after the first transient dequeue failure, and every subsequent
start fails with -7.
Two lessons recorded: patch the crashing leaf, never the teardown path above
it; and distrust "first error" readings from logcat, since logd chatty
suppression (expire N lines) had swallowed the earlier cycles and made the
already-locked state look like the initial failure.
The standing fear (the "kernel-ABI half of the coupling" above) was that the API 25 library speaks the old cmdq ioctl dialect to a kernel that only accepts the new MDP one. Measured, that theory is dead:
DpDriver::submitCommand(0x1d6e0) issues ioctl0x40887814on the cmdq fd, which decodes as_IOW('x', 20, 136 bytes), and on success reads a 64-bit job handle from struct offset0x40.- The kernel's
CMDQ_IOCTL_ASYNC_EXECis_IOW('x', 20, struct mdp_submit)(mdp/mdp_def_ex.h). WithCONFIG_MTK_CMDQ_TABset, as on this kernel,cmdqSecDataStructis 64 bytes andsizeof(struct mdp_submit)is exactly 136. The request codes are bit-for-bit identical. - The library's field placement matches
struct mdp_submit, not the oldercmdqCommandStruct: priority at0x0c,engine_flagat0x10(with a special case mapping engine mask0x8967to0x800),job_idat0x40.cmdqCommandStructwould have engineFlag at0x08.
So Amazon's Android 7 library already speaks this kernel's mdp_submit ABI;
Amazon evidently backported the newer MDP ioctl interface into their userspace.
With the /dev/mtk_cmdq path patch the device opens, the submit request code
is recognized, and dmesg shows no unrecognized ioctl complaints.
The camera now produces a live preview with real image content. All of
the following are zero in a clean run: startStream fail(-26),
waitSubmit is failed, dequeueDstBuffer for dispo fail, MDP deque DISPO fail. Consecutive screenshots differ, so it is a live stream and not a stuck
frame.
The cause was one more instance of this port's recurring theme - the old userspace and the new kernel agreeing on the request code and the struct but not on the encoding of a field.
op_meta.event carries a raw GCE hardware event value in the API 25
library, because that is what its vintage of the MDP interface took. This
kernel's translate_meta() instead passes that field straight to
cmdq_op_wait(), which treats it as a CMDQ_EVENT_ENUM index and
translates it to a hardware value through the DTS-populated event table. The
two numbering schemes are unrelated, and the mismatch is fatal rather than
merely wrong, because cmdq_core_init_DTS_data() defaults every hardware-event
slot to CMDQ_SYNC_TOKEN_INVALID - 1 - i, which is always negative. So a
hardware value read as an enum index almost always lands on an event the DTS
never named, cmdq_op_wait() returns -EINVAL, translate_user_job() aborts,
and the entire submit is discarded - one bad meta out of 167 kills the job.
The four events the camera waits on decode perfectly once read as hardware values, which is what confirms the diagnosis rather than merely fitting it:
| Library sends | mt8163.dtsi |
This kernel read it as |
|---|---|---|
| 28 | mdp_rdma0_frame_done |
DISP_CCORR0_SOF (no DTS entry -> -EINVAL) |
| 32 | mdp_wdma_frame_done |
DISP_AAL1_SOF (no DTS entry -> -EINVAL) |
| 33 | mdp_wrot_write_frame_done |
DISP_GAMMA_SOF (DTS has it -> accepted, wrong event) |
| 131 | isp_frame_done_a |
DISP_RDMA2_UNDERRUN (no DTS entry -> -EINVAL) |
Those are exactly the MDP and ISP frame-done events an MDP pipeline must wait on. The library was never wrong; only the field encoding drifted.
Note the third row: event 33 is accepted under the wrong interpretation. That rules out any "reject what looks invalid" heuristic as a fix, because a hardware value can collide with a legitimate enum index.
The fix is applied in userspace, not the kernel. tools/cmdq-trace/
rewrites op_meta.event from hardware value to enum index in the
CMDQ_IOCTL_ASYNC_EXEC path, using a table generated by composing the
kernel's own two sources (gen_event_map.py: cmdq_event_common.h gives
enum -> dts name, mt8163.dtsi gives dts name -> hardware value). Set
CMDQ_FIX_EVENTS=0 to trace without rewriting.
Userspace is the right layer here because /dev/mtk_cmdq has two clients:
/system/bin/cameraserver -> /dev/mtk_cmdq
/vendor/bin/hw/android.hardware.graphics.composer@2.1-service -> /dev/mtk_cmdq
The composer is Android 9 vintage and already agrees with this kernel. A
kernel-side remap would have to apply to both and would break display - the
same coupling that made downgrading libdpframework impossible. Interposing
only on cameraserver sidesteps it entirely.
Before and after, same build, same library:
before: EXEC ... count=167 ... ret=0 errno=0 job_id(after)=0 (rejected)
after: EXEC ... count=167 events(hw): 28 33 ... remapped=6
EXEC ret=0 errno=0 job_id(after)=ad (accepted)
The fix now ships as shims/libcmdqevent/libcmdqevent_shim.so; see below.
checkers: same symptoms, different cause - its libdpframework speaks an ioctl interface this kernel removed
The first checkers bring-up (issue #2) reproduced the exact pre-fix symptom
set - submit command block failed(-1), start the stream failed(-26),
endless dequeueDstBuffer for dispo fail(-1), green preview, EPIPE after
6.5s - with the shim loaded and working. The kernel side was clean: sensor
identified, powered and streaming, zero ISP errors, zero TG1 timeouts.
Scanning both dumps' libdpframework binaries for their 'x'-magic ioctl
constants (Thumb movw/movt pairs) explains it:
| build | submits via | this kernel |
|---|---|---|
| cronos / crown (byte identical) | MDP_IOCTL_ASYNC_EXEC (nr 20) |
served, and the event shim remaps it |
| checkers | CMDQ_IOCTL_EXEC_COMMAND (nr 3) + nr 7 |
removed; cmdq_driver.c returns -ENOIOCTLCMD |
So on checkers the submit ioctl itself fails with -1 (where cronos's
failure was a swallowed status and a zero job id), the library logs
submit command block failed(-1), JobID: 0, and no MDP work is ever
scheduled. The camera blobs were scanned too: libdpframework is the only
library in the checkers set that talks to /dev/mtk_cmdq.
The fix is to pin libdpframework_cam.so to the cronos/crown build on
every device - the library faces the SoC's MDP block and the kernel, not
the sensor, and all three devices share both. install-private-dpframework.sh
now fetches it from the cronos dump regardless of CAMERA_DEVICE and
replaces an already-installed legacy-interface copy, detected by probing
for the nr-20 constant.
scripts/install-cmdq-event-shim.sh installs the shim and
shims/libcmdqevent/camera-bringup.rc, which replaces the whole "runtime
workarounds needed every boot" incantation. Verified from an actual cold boot:
cameraserver running, /dev/MTK_SMI, /dev/mtk_cmdq, /dev/mdp_sync and
/proc/m4u all 0666, /persist mounted, debug.lsc_mgr.type=0 set, and the
camera app opens straight to a working preview with zero MDP errors.
The shim is loaded with setenv LD_PRELOAD in cameraserver's own init rc,
not by patching a blob. That is what scopes it to the one client that needs it
and leaves the graphics composer alone. cameraserver.rc also loses its
disabled line (kept during bring-up so a crashing HAL could not stop the
boot); the original is preserved as cameraserver.rc.orig.
An LD_PRELOAD interposer is a legitimate long-term answer here rather than a
stopgap. ioctl cannot be interposed from libcamera_shim instead, because
that library is reached through the blobs' DT_NEEDED and loses symbol
resolution order to libc; only a preload (or binary-patching the import) wins.
An earlier revision of this document claimed all four Bayer orders had been measured and none was neutral, concluding the Bayer order was not the cause of the magenta cast. That conclusion was wrong and the measurement was void.
The runtime override it relied on never actually ran. Tracing later showed the
shim's KDIMGSENSORIOC_X_GETINFO2 branch logging nothing at all, so all four
"measurements" were taken with the driver's compiled-in RAW_B, and the small
differences between them (R/G 2.30 to 2.71) were nothing but scene and AE drift
between captures.
Recorded as a caution rather than deleted, because the failure mode is easy to repeat: an override that silently never fires looks exactly like an override that fires and changes nothing. Both produce "I tried all the values and it made no difference". Always make the interposer log the value it actually saw and the value it wrote, and confirm those lines appear, before reading any result as a negative.
Why it did not fire is still open. The HAL demonstrably takes the format from
the kernel - it rejected the reference driver's RAW_MONO with
Unsupport SensorOutputDataFormat - so it must arrive through some ioctl. The
likely candidate is KDIMGSENSORIOC_X_GETINFO (_IOWR('i', 5, ...)) rather
than GETINFO2; its ACDK_SENSOR_GETINFO_STRUCT carries pInfo[2] at
offsets 8 and 12 on 32-bit, and ACDK_SENSOR_INFO_STRUCT shares the same
prefix layout, so SensorOutputDataFormat is at offset 36 there too. Handle
both, and log every 'i'-magic ioctl to find out which the HAL really uses.
Fixed by scripts/patch-awb-d65.sh. Measured on the preview, same scene:
| R/G | B/G | |
|---|---|---|
| before | 2.44 | 1.46 |
| AWB reference set to the calibration-implied D65 (731,512,743) | 1.87 | 1.73 |
| AWB reference set to unity (512,512,512) | 1.11 | 1.11 |
R and B now sit equal and barely above G; what warmth remains is the room's actual lighting.
The cast was never a missing correction - it was one correction too many.
This unit's factory calibration.bin (now that /persist is mounted) is read
successfully and applied as a raw pre-gain:
CamCal: ver8900~ [rCalGain.u4R] = 756 (G = 512, B = 742)
awb_algo: [1][updateAWBParam] rUnitGain: R=756, G=512, B=742
So the data reaching AWB is already white balanced. AWB then applied its own D65 reference on top, which is why the output tracked that reference almost exactly. The proof is the unity row above: with AWB's reference neutralised the image is neutral, which can only be true if something upstream had already done the balancing.
The original reference was also simply wrong for this module -
rD65Gain: R=891, G=512, B=610, a strongly red-biased correction, while both
the unit's own calibration (756/512/742) and the LSC path's view of the same
NVRAM block (AwbNvramInfo: D65Gain(731, 512, 743)) are balanced. Two readers
disagreeing about one NVRAM block by exactly one 12-byte AWB_GAIN_T is the
same struct-offset drift seen everywhere else in this port, and it explains why
the bias was red rather than neutral.
Note AWB never adapted anyway: CCT sat at 6500 and the gains were identical
frame after frame for a minute, so nothing is lost by neutralising the
reference. If adaptive AWB is wanted later, the statistics path is the thing to
investigate, not the reference gain.
CORRECTED - AWB does adapt; the freeze was an artifact of a static scene.
Measured by driving the room's smart lights through home automation while logging
awb_algo: small illuminant shifts (a desk lamp, a dim cove) sit inside the
algorithm's stability deadband and the gains hold - which is what the
"identical for a minute" observation was actually seeing. Flooding the
camera's field of view (ceiling cove at full power) moved LV 25 -> 41, made
AE write new sensor exposures, and swung the AWB gains hard
(933/512/639 -> 791/512/723 on stock tuning; 536/512/536 -> 707/512/807 on a
731/512/743 reference). The state machine transitions Searching/Converged
throughout. The adaptation machinery - statistics, classification, smoothing -
is fully alive.
The remaining defect is magnitude, not adaptivity: when AWB genuinely adapts, it overshoots into magenta (ceiling measured R/G 1.9 on stock tuning, 1.6 on the 731 reference, under warm LED flood). The statistics it reads are already balanced by the factory CalGain pre-gain, but the OV9734 tuning tables compute corrections sized for raw-domain data, so every real correction is roughly double-applied. That is the same double-WB mechanism as the original magenta cast, now expressed dynamically. Fixing it properly needs OV02B10 tuning tables, which do not exist in any available blob set. For this device's use (fixed mounting, fixed lighting) the unity reference is the right anchor; if lighting-tracking color is ever needed, do it in the consumer (gray-world on the snapshot) rather than in these blobs.
Debug tooling added for this investigation, all reusable: the preload shim
redirects the 3A blob's hardcoded /sdcard/awb/ AWB statistics dumps
(awb.debug.dump.enable=1) to /data/awbdump/, which camera-bringup.rc
creates; the dump is a per-frame list of 2300 tagged records of algorithm
state plus per-window data. debug.awb.enable=1 / debug.ae.enable=1 turn
on the 3A logs. cameraserver also carries sdcard_rw media_rw groups on
the device (added by hand to /system/etc/init/cameraserver.rc, not in the
repo installer).
Trap, recorded after losing an hour to it: patch-awb-d65.sh originally
defaulted its replacement triple to the calibration-implied 731/512/743, while
the deployed configuration was unity 512/512/512 passed explicitly.
Re-running the script without arguments therefore silently regressed the
device to the magenta reference (ceiling R/G 2.0). The default is now unity.
Daylight measurements, and where the adaptive gains actually come from. Under natural daylight (LV 78, LEDs off) the white ceiling measures:
| AWB gains applied | Ceiling R/G | B/G |
|---|---|---|
| unity 512/512/512 (locked) | 0.71 | 0.70 |
| adaptive output 851/512/638 | 1.39 | 0.96 |
| implied correct ~722/512/732 | ~1.0 | ~1.0 |
The unity row is the key fact: with no WB gain the image is strongly green, i.e. the pixel path carries raw sensor response - the factory CalGain pre-gain balances only the AWB statistics, not the image. So per-illuminant WB gains are genuinely required, no static triple can cover both daylight (needs ~722/732) and warm LED (where near-unity happens to look right only because the warm spectrum offsets the sensor's green dominance), and the in-stack adaptation overshoots red under daylight by a fixed spectral bias (R +18%, B -13%) - the OV9734-golden versus OV02B10 module difference.
Two attempts to retarget the adaptation failed identically: patching the
six-entry per-illuminant gain table at 0xef910 (three copies, entries
(512,557)(549,948)(657,847)(742,870)(758,735)(891,610)) and the D65
reference both left the adapted output byte-for-byte unchanged (851/512/638).
The applied gains are therefore computed from the measured white point plus
the calibration constants (UnitGain 756/512/742, GoldenGain 778/551/763),
not read from those tables. Correcting the adaptation in-stack means
disassembling the gain computation in lib3a.so to find what to bias -
doable (the constants to search for are known) but unfinished.
The spectral bias is now corrected generically, inside the camera stack, with
no environment- or app-specific pieces. The shim interposes lib3a's exported
per-frame entry AwbAlgo::handleAWB(AWB_INPUT_T&, AWB_OUTPUT_T&) (the gains
themselves are applied through mmap'd register stores and cannot be
intercepted at the ISP layer) and rescales the gain triples in the output
struct. Triples are located by pattern - (R, 512, B), both channels in
gain range - because the struct layout is not public; three sit at words
0, 6 and 15 of AWB_OUTPUT_T.
The bias turned out to be illuminant-dependent (the two modules' spectral responses diverge differently under different spectra), so the trim is interpolated between two calibrated anchors. The interpolation key is the algorithm's own untrimmed B gain - it tracks illuminant warmth monotonically (~640 under daylight, ~835 under 2600K LED) and is available inside the very triple being corrected, so no CCT field needs locating. Four 512-based multipliers control it, re-read every 64 frames so tuning needs no restart:
persist.camera.awbtrim.r 507 cool anchor (daylight)
persist.camera.awbtrim.b 692
persist.camera.awbtrim.r.warm 564 warm anchor (2600K LED)
persist.camera.awbtrim.b.warm 536
Recalibrated after the black-level fix. The anchors above replace an earlier set (390/532 cool, 431/412 warm) measured while the OBC pedestal was still lifting every frame, which biased the statistics AWB reads and left a visible green cast. Raising all four by 30% removed it: on a grey wall the render went from R/G 0.91, B/G 0.82 - both channels below green, which is what a green cast is - to R/G 1.02, B/G 1.00. Confirmed by eye as well as by measurement, and cross-checked against the same scene corrected by hand in Photoshop (Color Balance, green/magenta toward magenta).
Both anchors are now verified on device. The warm pair under uniform 2600K LED; the cool pair under natural midday daylight the following morning, where the grey ceiling renders R/G 1.05, B/G 1.06-1.08 and the window-side wall runs slightly cool (B/G 1.23) as it physically should. The cool pair was originally scaled from the warm one rather than measured, and the measurement confirmed it rather than correcting it.
camera-bringup.rc sets the defaults, calibrated against a grey ceiling
(a grey surface is a valid neutral reference) and verified to render it
neutral under natural daylight (R/G 0.97 vs 1.39 uncorrected) and under
uniform 2600K LED (R/G 0.96-1.09 vs 0.85/1.37 blue-shifted uncorrected),
both from a cold boot. Two accuracy limits, measured and worth knowing:
the blob's AWB output wobbles a few percent between sessions on an
identical scene, and the CCM amplifies gain error roughly 3x in the
rendered image, so scene-to-scene rendering varies about +-10% around
neutral. That is inherent to the closed algorithm, not to the trim.
The magenta-vs-cyan spatial gradient investigated along the way (same-paint surfaces rendering 30% apart across one frame) is a low-sun artifact, not a general daylight one: it was measured at golden hour with direct low sunlight entering the room, and is not visible in midday diffuse daylight or under uniform artificial light, where the same surfaces agree within ~5%. It is IR and low-sun light through the aged front glass plus veiling glare from the internal window, which cannot be cleaned from outside. It is a per-unit hardware trait, not correctable by any global gain, and belongs in a known-limitations note.
Fixed by scripts/patch-obc-pedestal.sh. debug.obc_apply.log=1 showed
the ISP subtracting OBC offset 8128 = 0x1FC0 = -64 in the 13-bit register
field - the OV9734 value. The OV02B10's standard 10-bit pedestal of 64 is
256 in the ISP's 12-bit domain, so 192 counts survived into every frame,
which gamma turned into the ~30% floor. Rewriting all 140 per-ISO
ISP_NVRAM_OBC_T blocks in libcameracustom.so from -64 to -256 takes the
dark-room frame from R=43 G=79 B=84 to R=8 G=7 B=8. The pedestal also
contaminated the AWB statistics and desaturated all colors, so the trim
anchors must be recalibrated after this fix.
The original characterisation, kept because the measurement method (a lightless room as a free black-frame rig) is the thing to reuse:
Measured with the room fully dark (blackouts closed, all lights off): the frame averages R=43 G=79 B=84 with a minimum pixel of 25, where a black frame should be near zero. A ~30% grey floor with zero light entering is electronic, not optical: the sensor's black-level pedestal is not fully subtracted by the ISP's OBC stage, the residual is multiplied by AE's maxed analog gain, and the gamma curve lifts it further. This pedestal, not just the glare, is a large part of the perceived haze, and dark image regions carry it in lit scenes too. The cyan tint of the pedestal is the AWB trim's cool-anchor R-cut applied to the fallback gains on top of a roughly equal-channel grey.
Suspected mismatch: the OBC per-ISO offsets come from the OV9734 tuning tables while the OV02B10's pedestal differs. Attack surface, in order: find the OBC per-ISO values (per-module isp_mgr debug enables, or locate the per-ISO ISP tuning arrays in libcameracustom.so and patch the OBC entries); alternatively lower the sensor-side BLC target in the driver we own (register not documented in any public OV02B10 driver - would need the module-parameter sweep technique). Verification is trivial: a dark room frame must go to near-black.
Remaining IQ backlog, all characterised: this OBC fix; recalibrate the cool trim anchors in real daylight (they predate the anti-compounding fix); a fallback-state handler in the trim (when AWB cannot classify it outputs exactly the reference triple - detectable - and the cool-anchor trim is then wrong); confirm standard EV compensation works.
One resolver gotcha inside the wrapper: lib3a.so is dlopen'd by the
passthrough HAL into a local namespace group, so dlsym(RTLD_NEXT, ...)
from the preload cannot see it - and returning failure from the wrapper
silently pins AWB in Searching forever. The wrapper resolves the real
function via dlopen("lib3a.so", RTLD_NOLOAD) instead.
debug.awb_mgr.lock=1 is confirmed working as an instrument: it stops
handleAWB entirely and freezes the gains at whatever the session start
computed - note it inherits the previous session's gains unless cameraserver
itself is restarted.
This one broke every CameraX app on the device, and it was a one-line device
tree bug. pm list features reported:
feature:android.hardware.camera <- a BACK camera, which does not exist
feature:android.hardware.camera.any
<- camera.front missing entirely
mt8163-common/mt8163.mk copied android.hardware.camera.xml, which declares
android.hardware.camera (the back-camera feature). These devices have a
single front camera and no back camera.
It is not cosmetic. CameraX's CameraValidator only looks for a back camera
when FEATURE_CAMERA is advertised, so the bogus feature made validation
fail device-wide:
CameraValidator: Verifying camera lens facing on cronos, lensFacingInteger: null
CameraValidator: Camera LensFacing verification failed, existing cameras: [Camera@...[id=0]]
CameraX: Expected camera missing from device.
Caused by: java.lang.IllegalArgumentException: No available camera can be found
CameraX then retries initialization forever, so any app using it hangs with no
error rather than failing - which is exactly how it presented (a snapshot that
silently never completed). Fixed by copying
android.hardware.camera.front.xml instead, which declares camera.any +
camera.front. Verified: features correct after reboot, CameraX initializes,
zero validator failures, and the app's snapshot returns a real JPEG.
Note the fix belongs in the common makefile because all four Echo devices here are single-front-camera.
The sensor is landscape-mounted, matching the landscape-natural 960x480 panel,
and the driver now flips it 180 degrees so frames arrive upright - but the HAL
still reported 270, so every client that correctly rotates by
SENSOR_ORIENTATION produced a sideways image. Confirmed on a real capture:
the JPEG came back rotated 90 degrees while the preview was upright.
The value comes from DevMetaInfo::queryDeviceWantedOrientation() inside
libcam.utils.so and cannot be corrected in the blobs - see the retracted
getSensorOrientation() attempt below. It is now overridden in
camera.device@1.0-impl, which this tree builds, from the
ro.camera.sensor_orientation property (set in device/amazon/cronos/device.mk).
Both HAL1 clients and the camera2 legacy shim pick it up there.
Verified: dumpsys media.camera reports Orientation: 0 and captures come out
upright.
Fixed in the sensor driver - patches/0010-imgsensor-ov02b10-restore-capture-and-video-mode-timing.patch.
Verified on device: takePicture returns a full-resolution 1600x1200 JPEG in
about one second, repeatably, with zero ISP_WAIT_IRQ errors, and the preview
stays live afterwards.
The root cause was neither the ISP nor the IMGO path. The reference OV02B10
driver ships with the .cap1, .normal_video, .hs_video and .slim_video
entries of imgsensor_info inside one comment block, so all four are zeros.
capture() takes the cap1 branch whenever current_fps != 300, and AE
lowers the preview frame rate indoors, so that branch is effectively always
taken: pclk, line_length, frame_length and min_frame_length all become
0. The first AE exposure write then underflows in write_shutter():
shutter > min_frame_length - margin is unsigned (0u - 7 is huge, so
false), frame_length stays 0, and the dummy-line registers get
0 - 0x4c4 = 0xfb3c. 64316 dummy lines on the 1220-line base is a 65536-line
frame: one frame every 1.78 s. Every 500 ms IRQ wait misses, and because
nothing is actually broken, no error interrupt ever fires. The preview restart
reprograms the mode registers, which is why preview always recovered.
What pinned it: an out-of-band ISP_WAIT_IRQ probe (tools/cmdq-trace/ispwait.c)
showed VS1 interrupts arriving during the stall at exactly 1.78 s intervals -
the sensor was streaming, just 54x too slowly. The LD_PRELOAD shim's ISP
ioctl tracer (debug.isptrace) then showed GET_PIXEL_CLOCK_FREQ and
GET_PERIOD returning zero right after the scenario switch, which named the
zeroed struct.
The fix uncomments the four mode structs (all five modes run the same
1600x1200@30 register table) and floors frame_length at the mode base in
write_shutter()/set_shutter_frame_length() so no future zeroed state can
underflow the dummy-line computation again. The struct fix also repairs video
recording, which reads the same zeroed entries.
The original characterisation, kept for the record:
MtkCam/Cam1Device: [Cam1DeviceBase::takePicture] +
IspDrv: ERROR: ISP_WAIT_IRQ fail(-1). Clear(1), Type(0), Status(0x00000400), Timeout(500).
IspDrv: ERROR: ISP_WAIT_IRQ fail(-1). Clear(1), Type(0), Status(0x00000400), Timeout(500).
IspDrv: ERROR: ISP_WAIT_IRQ fail(-1). Clear(1), Type(0), Status(0x00000001), Timeout(500).
RequestThread-0: Hit timeout for jpeg callback!
The interesting part is which interrupts these are. Decoding against
cameraisp/src/mt8163/inc/camera_isp.h:
| Status | Bit | Meaning |
|---|---|---|
0x00000400 |
10 | ISP_IRQ_INT_STATUS_PASS1_TG1_DON_ST |
0x00000001 |
0 | ISP_IRQ_INT_STATUS_VS1_ST |
Both are pass 1 (sensor into ISP) interrupts, not pass 2. So this is not the capture/JPEG stage failing - the sensor stops feeding the ISP the moment the HAL switches to the capture scenario, and everything downstream waits forever. No capture-error callback is generated, which is why callers see silence.
Facts established while investigating, worth not re-deriving:
- The capture register table is byte-identical to preview. Both mode setters emit the same 91 writes with the same values, so the capture scenario does not reprogram the sensor differently and a bad capture table is not the cause.
- The LSC/TSF capture-table errors are gone. An earlier report cited
shadingTblAlign Err,min_gain is 0andMTKTSF_FEATURE_GEN_CAP_TBLfailures as likely related. They no longer appear - mounting/persistso the factory calibration loads fixed them - andtakePicturestill fails identically, so they were a separate issue. - No M4U, SMI or EMI errors appear during the failing capture, so the DMA remapping is not obviously starved.
Disproved hypothesis (do not retry). The mode setters rewrite PLL and timing
registers, and on a preview -> capture switch they are reached mid-stream, so
standby-before-reprogram looked like the answer. Implemented (patches/0008-*),
built, flashed: the capture still times out on exactly the same interrupts. The
change is kept because the sequence is correct regardless, but it is not a fix.
Where to look next: what the HAL reconfigures on pass 1 for the capture scenario. The preview path drives DISPO at 640x480 out of a 1600x1200 input; capture switches pass 1 to full-resolution IMGO. That the sensor keeps streaming in preview but stops the instant this switch happens points at the IMGO/FBC path or its tpipe configuration rather than at the sensor driver.
Kernel side adds two facts: IrqStatus(0x00000000) (nothing is latched at all,
rather than the wrong bit being set), and the failing thread is Capture@CmdQue.
The register dump is [0x15008008 00003210] and [0x15008100 00008001].
A full handoff for this bug lives in docs/handoff-takepicture.md - repro,
ruled-out leads, tooling, and the device safety rules.
Fixed in the driver (patches/0007-*), so the readout itself is rotated and
captures are correct too, not just the preview. Verified from a cold boot:
upright, R/G 1.08, B/G 1.02, zero errors.
set_mirror_flip() is now implemented as page 1, register 0x12, bit[0]
horizontal / bit[1] vertical, with .mirror = IMAGE_HV_MIRROR.
Finding that register was the whole problem: it is in no source available.
The reference driver this was ported from has set_mirror_flip() #if 0'd out
with all four cases empty, so no vendor ever filled it in. OmniVision's OV02A10
- a sibling part with the same
0xfdpaged architecture - uses0x3f, and that was tried first and does nothing on this sensor.
The technique that solved it is the reusable part. Rather than guess a
register and pay a kernel build plus a boot flash per guess, the driver exposes
the page and register as module parameters and re-applies them on every mode
set. tools/cmdq-trace/flip-sweep.sh then sweeps candidates by reopening the
camera app - no rebuild, no flash, and no cameraserver restart (which
livelocks the device). It scores each capture against the 180-degree rotation
of an unflipped baseline, so a hit is unambiguous:
candidate vs base vs base180 verdict
p1_r12 25.5 13.9 FLIPPED
p1_r3f 2.1 29.5 no change <- the OV02A10 guess
...
The parameters remain in the shipped driver so a future correction is free.
Two changes that must move together. IMAGE_HV_MIRROR shifts the Bayer
phase by one pixel in each axis, so the native BGGR reads out as RGGB. The
intermediate build that flipped without updating
sensor_output_dataformat produced a strong green cast - red and blue swapped.
Setting SENSOR_OUTPUT_FORMAT_RAW_R alongside the flip restores correct colour.
This is also, incidentally, the first evidence that the declared data format
does reach the ISP: the earlier runtime sweep over all four values changed
nothing, because with no phase shift to correct there was nothing to see.
The sensor is mounted rotated 180 degrees and nothing compensates. The ported
driver cannot: it declares .mirror = IMAGE_NORMAL, and its set_mirror_flip()
is #if 0'd out with all four cases empty, so the sensor is never told to flip.
The HAL reports the rotation but with the wrong value:
MtkCam/devicemgr: [enumDeviceLocked] iFacing:1, iWantedOrientation:270,
iSetupOrientation:270
A dead end worth recording so it is not repeated (the working fix is the
sensor-level flip above).
NSCamCustomSensor::getSensorOrientation() in libcameracustom.so looks like
the source - it returns a PC-relative pointer to a three-entry table
{main=90, sub=270, main2=90}, and cronos enumerates as Facing: Front, the
sub slot, whose 270 matches exactly. Patching that entry to 90
(scripts/patch-camera-orientation.sh, which resolves the pointer rather than
hunting for the constant) changes nothing: verified on-device that the bytes
became 90 90 90, yet the HAL still reported 270 and the preview was unchanged.
The value actually comes from DevMetaInfo::queryDeviceWantedOrientation() in
libcam.utils.so, which reads a per-device registry populated during setup, not
that table. There is no property override in either blob. Options, none cheap:
- Implement the OV02B10 mirror/flip registers in the sensor driver. The correct fix - it rotates the readout itself, so captures are fixed too, not just the preview - but it needs the register set for this paged sensor and a boot flash, and a wrong guess can wedge the sensor.
- Override
SENSOR_ORIENTATIONin the AOSP layer, which this tree builds (LegacyMetadataMappermaps HAL1CameraInfo.orientationtoSENSOR_ORIENTATIONfor camera2 clients). Fixes the app-visible rotation and EXIF without touching the kernel, at the cost of a framework build.
Option 1 was taken and worked; see the section above.
With debug.awb.enable 1 the picture is clear. AWB is running - the absence
of any awb tag in a default log is only because its logging is off, which
briefly looked like AWB never starting:
awb_algo: [1][PV AWB Gain] LV = 28, Rgain = 933, Ggain = 512, Bgain = 639
awb_algo: [1] CCT = 6500
awb_algo: [1] CurrentFullAWBGain: R = 933, G = 512, B = 639
512 is unity, so AWB asks for R x1.82 and B x1.25 and holds those values
steadily for as long as the preview runs. Applied, and then pushed further by
the CCM's strong red term (refine_CCM returns 526,-289,19 / -73,411,-82 / -4,-219,479), they land almost exactly on the measured output of R/G 2.41 and
B/G 1.46. So the cast is not a missing correction - it is AWB converging on the
wrong illuminant and the pipeline faithfully applying it.
That points at tuning data rather than code, and matches what this port already
knows: the stock blob set's compiled-in tuning is OV9734 (the gen-1
checkers sensor), while the hardware is OV02B10. Worth checking next which
of libcam.halsensor.so and cronos's own libcameracustom.so actually supplies
the AWB/CCM tables at runtime, since only the latter carries OV02B10 entries.
debug.awb_mgr.lock, debug.isp_apply.disable and debug.isp_debug.enable
are the useful levers.
This is image quality, not function: preview, streaming and capture all work.
State with the current patched library (run15): zero crashes, zero locked ports, clean teardown and retry every cycle, but no frame ever arrives. Each cycle logs:
DpIspStream: wait submit stream failed(-26)
DpIspStream: waitSubmit is failed in asynchronize mode (-26)
MdpMgr startMdp ERROR: startStream fail(-26)
DpIspStream: Both JobID and Framedone List are empty!!!
dequeueDstBuffer for dispo fail(-1)
MtkCam/VSSScen [dequePass2] MDP deque DISPO fail
-26 is the library's own failure code (mvn r0,#0x19), used both after a
failed ioctl and by DpDriver::checkHandle; it is not a kernel errno.
The key kernel-side fact: cmdq_ioctl in cmdq/v2/cmdq_driver.c assigns each
handler's result to status and then unconditionally return 0. Every
recognized ioctl reports success to userspace regardless of what
mdp_ioctl_async_exec / mdp_ioctl_async_wait actually returned. So the
userspace ioctl cannot be what returns the failure; the working hypothesis is
that the submit fails inside the kernel, the error is swallowed, the returned
job_id stays zero or garbage, and the library's own handle check then fails
with -26 on the wait side. That also fits the otherwise puzzling dmesg silence:
the CMDQ_MSG entry lines are log-level gated and the dropped status is never
printed.
Next steps, in order:
- Read
mdp_ioctl_async_execandmdp_ioctl_async_waitinmdp/mdp_ioctl_ex.cend to end: enumerate the failure paths, what each logs, and whatstruct mdp_submit.metasis expected to contain (struct op_metadescriptors, which the kernel validates and translates, versus the raw cmdq instruction words an old userspace would build). - Disassemble
DpDriver::waitFramedone(0x1d7d8) to confirm which call generates the -26 and what it passes to_IOR('x', 21, struct mdp_wait)(job_idat offset 0, thenread_v1_result, thenread_result). - If still ambiguous, build a kernel that logs or stops swallowing the handler status, or strace cameraserver's cmdq fd to capture the exact submitted struct.
Step 3 turned out not to need a kernel build. tools/cmdq-trace/ is an
LD_PRELOAD interposer on ioctl() that decodes the cmdq traffic in place.
It recovers the swallowed status without touching the kernel, using the fact
that mdp_ioctl_async_exec writes user_job.job_id only on its success
path, at the very end, after cmdq_mdp_handle_flush has returned:
EXEC fd=95 cmd=40887814 metas=ead21100 count=167 prio=20 eng=150 ... job=0
EXEC ret=0 errno=0 job_id(after)=0
WAIT job=0 ret=0 errno=0
ret=0 with job_id still zero is only reachable through an early return.
So the hypothesis above is now established fact rather than a working theory:
the ioctl is recognized and dispatched, the handler fails, cmdq_ioctl
discards the status and reports success, and the library's checkHandle then
turns the zero job handle into its own -26. The -26 is downstream noise; the
real error is whatever mdp_ioctl_async_exec returned and threw away.
Note also that the wait ioctl is issued with job=0, which cannot match any
entry in job_mapping_list, so mdp_ioctl_async_wait takes its
job not exists path. That error is swallowed identically.
Two platform facts narrow which early return it is, and both are worth knowing before reading further:
mdp_engine_port[]is all zeros for mt8163 (cmdq/v2/mt8163/mdp_base.h).translate_fd()starts withport = cmdq_mdp_get_hw_port(meta->engine)and returns 0 when that is 0, so anyCMDQ_MOP_WRITE_FDmeta fails unconditionally on this SoC. A userspace that passes buffers as dma-buf fds cannot work here at all; it has to pass MVAs as plain register writes.CMDQ_MOP_READrequires a preallocated readback slot.translate_read_idresolvesmeta->readback_idagainst therb_slot[]table thatCMDQ_IOCTL_ALLOC_READBACK_SLOTS(nr 22) fills in, and returns 0 for an unknown id, whichtranslate_metaturns into-EINVAL. The trace contains no nr-22 ioctl at all, so if the library emits a single READ meta, the whole submit dies.
The good news from the same trace is that the parts that could have been wrong are right. The library passes buffer addresses as ordinary masked register writes to real mt8163 MDP offsets, not as fds:
meta[15] op=0 eng=1 off=0f00 val=1d900000 mask=ffffffff # RDMA0 src base
meta[18] op=0 eng=1 off=0100 val=1d94b000 mask=ffffffff
meta[21] op=0 eng=1 off=0060 val=00000280 mask=001fffff # 640 px
eng 1-5 are all < ENGBASE_COUNT, every off is <= 0xFFC and 4-byte
aligned, and the ops observed are only WRITE (0) and POLL (2). So the first 24
metas of each submit would all translate cleanly. The failure is in the
remaining 143 (or 207) metas, or in handle_flush.
The instrumented run that answers this did not complete. The tracer was
extended to validate all meta_count metas against translate_meta's exact
rules and report the first eight violations with a per-op histogram, but the
device livelocked before its log could be read - see the warning below. Re-run
tools/cmdq-trace/run.sh after a power cycle; it is one cycle from naming the
failing meta.
If the metas all validate, the remaining suspect is cmdq_mdp_handle_flush
and the engine_flag the library sends (0x150 and 0x369 here). Those bits
are enum CMDQ_ENG_ENUM positions, and a flag naming an engine this kernel
does not schedule would fail thread acquisition inside flush.
Do not SIGKILL cameraserver while the ISP is streaming. Doing so between
trace runs is what took the device down: it leaves M4U ports pointed at freed
buffers and the EMI MPU violation storm livelocks the whole system, exactly as
the permissions item below describes. The device stops answering ping and
needs a physical power cycle - there is no ADB, no watchdog reboot, and no
smart plug on it. Use stop cameraserver, or reboot
between runs.
Two non-obvious requirements, both encoded in tools/cmdq-trace/:
RTLD_NEXT is (void *)-1 in the glibc headers but 0xfffffffe on 32-bit
bionic. Getting this wrong makes dlsym return NULL, the interposer
tail-calls through it, and the process spins at 100% CPU in a way that looks
exactly like a hang in the camera stack. The first attempt here lost time to
precisely that.
cameraserver also cannot just be started as root. CameraService checks the
caller's uid and rejects the app with
SecurityException: validateClientPermissionsLocked:1151: Untrusted caller
(calling PID 5656, UID 0) trying to forward camera access to camera 0
for client cameraserver (PID 0, UID -1)
so the camera app crashes in getCameraCharacteristics and the stack is never
exercised. camwrap.c drops to uid/gid 1047 with the rc file's groups plus
media (1013), then re-execs cameraserver with LD_PRELOAD preserved.
Since SELinux is permissive on this build, no policy work is needed.
These are now durable - see "the camera now works from a cold boot" above.
shims/libcmdqevent/camera-bringup.rc performs all of it at post-fs-data.
Kept for the explanation of why each step matters.
Bring-up currently depends on this incantation as root after every boot, before starting the camera:
chmod 666 /dev/mtk_cmdq /dev/mdp_sync /dev/MTK_SMI /proc/m4u
setprop debug.lsc_mgr.type 0
mount -o ro -t ext4 /dev/block/by-name/persist /persist
start cameraserver # disabled in its rc during bring-up
am start -a android.media.action.STILL_IMAGE_CAMERAMissing the m4u/SMI permissions is not benign: the ISP then DMAs through
unconfigured M4U ports and the resulting EMI MPU violation storm (cam_aao)
livelocks the whole device, which presents as a total freeze needing a power
cycle. Durable fixes wanted: ueventd entries or cameraserver group membership
for the device nodes, an init chmod for /proc/m4u, the lsc property as a
build property, the persist fstab entry, and re-enabling cameraserver's rc once
the stack stops taking the device down.
The partition exists and contains this unit's factory data:
$ mount -o ro -t ext4 /dev/block/by-name/persist /persist
$ ls -l /persist
-rw-rw-rw- root root 1895 2021-11-10 12:56 calibration.bin
-rw-rw---- keystore 5490 2022-11-10 20:15 dha_certificate.pem
The ROM never mounts it, which is why the HAL logged
can't open calibration data /persist/calibration.bin and
lsc_mgr2: [importEEPromData] Error(ERR_NO_SHADING) from the very first run.
Mounting it makes both go away. This wants an fstab entry in the device tree, and
is likely worth reporting upstream independently of the camera - a factory
calibration partition going unmounted may affect other subsystems too.
debug.lsc_mgr.type=0 avoids the LscRatioImp::genHwTbl crash entirely - the
default selects the "Rto" (ratio) implementation, which is the one that null
derefs. With type 0 the 3A thread survives. debug.lsc_mgr.enable exists too but
does not prevent that path.
MediaTek's tuning libraries expose a useful set of these:
debug.lsc_mgr.enable debug.lsc_mgr.type debug.lsc_mgr.manual_tsf
debug.lsc_mgr.ratio debug.lsc_mgr.ct debug.isp_apply.disable
debug.isp_tuning_mgr.enable debug.ae_mgr.disableISP
Worth exploring further during bring-up. Note these are runtime-only; a real fix either sets them as build properties or resolves why the ratio path has no table.
signal 6 (SIGABRT)
#01 android::String8::append(String8 const&)+210 libutils.so
#02 android::CameraParameters::flatten() const+152 libcamera_client.so
#03 android::ParamsManager::flatten() const+52 libcam.paramsmgr.so
The HAL builds its parameter string and String8::append aborted on a
zero-size String8 the legacy HAL produces, a case API 25 tolerated and
Android 11 made fatal. Fixed in source in CameraParameters::flatten, since
libcamera_client is built from this tree: see
patches/0006-camera-flatten-tolerate-zero-size-String8-from-legacy-HALs.patch,
built and installed on the device. getParameters now succeeds and the camera
app reaches its normal preview UI.
This assessment has been overtaken by events: the preview now works. It is kept because its reasoning held up exactly.
It predicted a long tail of API 25 versus Android 11 differences, each needing its own diagnosis, with no wall but no reason to think any given one was the last. That is what happened, and the cmdq event encoding was the final entry in the tail. Every blocker in this port turned out to be the same shape - the two sides agreeing on the call and disagreeing on an encoding - and none of them required the architectural surgery the original pessimism assumed.
What is left is bring-up polish rather than architecture: image quality (Bayer
order, lens shading, AWB), making the runtime workarounds durable, and moving
the cmdq event fix out of the bring-up LD_PRELOAD into libcamera_shim.
Even with the calibration file loading, the 3A thread still crashes:
#00 NSIspTuning::LscRatioImp::genHwTbl(uint, const uint*, uint*)+162 libfeatureio.so
#01 NSIspTuning::LscMgr2RtoCycle::tsfBatch()+66 null deref, fault 0x10
#02 NSIspTuning::LscMgr2Rto::tsfPostCmd(...)+228
#03 NSIspTuning::LscMgr2::updateTsf(...)+216
#04 NS3A::StateCameraPreview::sendIntent(intent2type<10>)+1254
So the calibration blob is read but the hardware shading table is still not
built. Candidates: the calibration format may be keyed to a sensor id the HAL
resolves differently, the TSF (shading transfer function) path may want tuning
data that lives in a libcameracustom entry for OV02B that this blob set does
not carry, or LSC may simply need disabling for bring-up. Worth checking whether
the 3A path can be told to skip TSF - a shading-less image is still an image.
The pattern so far has been "fix the next crash", and that worked through six layers. It has now reached blob internals where each step is smaller and the information per attempt is lower. Two strategic options rather than another increment:
- Go back to the Android 9
libdpframeworkand mitigate the object size differently. It matches the kernel, which removes the whole class of kernel-ABI doubt. The problem waslibcam.campipe.sounder-allocatingDpIspStream. That could potentially be addressed by interposing an allocator, or by shimming the constructor to initialise only the fields the old layout has. - Add old-MDP compat to the kernel, the same technique that fixed
SET_MCLK_PLL: find what the old library's ioctls encode versus what this kernel accepts, and alias them. This keeps the private library.
Option 1 is probably the better bet: it aligns userspace with the kernel rather than adding more compatibility surface, and the object-size problem is bounded and inspectable.
The preview surface renders uniform green, which is what zero-filled YUV
looks like - frames are being delivered on schedule but carry no image data. The
still path gets as far as JpegCodec::encode and then crashes inside
libcam.camshot.so, consistent with it being handed the same empty/unmapped
buffer.
Ruled out already:
- M4U.
m4u_config_port failedis gone once/proc/m4uand/dev/MTK_SMIare accessible tocameraserver(see the permissions item below). The DMA remapper is configured. - Sensor streaming.
irq_TG1_DONEfires, so the ISP is receiving frame-end interrupts from a streaming sensor. - Buffer plumbing at the HIDL layer. No import failures, no linker errors, and the display client accepts and cycles the buffers.
Remaining leads, in order:
- Bayer order.
sensor_output_dataformatwas set toSENSOR_OUTPUT_FORMAT_RAW_Bas a starting guess when the reference driver'sRAW_MONOwas rejected. The fourRAW_B/RAW_Gb/RAW_Gr/RAW_Rvalues are cheap to rotate through, and a wrong one plausibly produces a degenerate image. Try this first - it is a one-line change. - Exposure and gain. Check
set_shutter()/ the AE path actually programs non-zero values for this module; the paged register layout means an address from another module's table writes into the wrong bank silently. - ISP output buffer address. Confirm the ISP is writing to the buffer the
display client later reads, rather than one the wrapping in
sDequeueBufferreplaced. TheGraphicBufferwrapper passesGRALLOC_USAGE_HW_CAMERA_WRITE | GRALLOC_USAGE_HW_TEXTUREand the stride reported bydequeueBuffer; a mismatch with what the ISP assumes would give exactly this symptom. BWC: fail to open mem_bw_ctrl driver fileis still logged. That is EMI bandwidth profiling, expected to be cosmetic, but worth excluding if the above do not explain it.
Every software layer now works. The failure is in the sensor itself:
IspDrv: ISP_WAIT_IRQ fail(-1). Clear(1), Type(0), Status(0x00000400), Timeout(500)
iio/ifunc: waitIrq( irq_TG1_DONE ) fail
iio/pathp1: waitBufReady fail
iio/camio: ERROR:dequeueBuf
VSSScen: dequePass1 fail
HalSensor: [reset] pSensorDrv->open succeed, retry = 0 ... setScenario ... End reset
(loops)
irq_TG1_DONE is the timing-generator interrupt raised when a MIPI frame
arrives. It never comes, so the HAL resets the sensor and retries forever.
The sensor is confirmed present in hardware. pSensorDrv->open succeed can
only happen if return_sensor_id() read back 0x2b over I2C at write address
0x78, because open() returns ERROR_SENSOR_CONNECT_FAIL otherwise. So
OV02B10 is real, powered, clocked enough for I2C, and answering. What is not
happening is pixel streaming.
Likely candidates, roughly in order:
- Register init tables. The ported driver's
sensor_init()/preview_setting()came from an mt6765 device's OV02B10 module. OV02B10 is paged (register0xfdselects the bank) and vendors ship substantially different init sequences per module. This is the most likely culprit. - MIPI configuration. The driver declares
SENSOR_MIPI_1_LANEandmclk = 24. If cronos wires two lanes, or a different MCLK, the ISP will wait forever for a frame that never arrives in the expected format. - Streaming enable. Check that
streaming_control(KAL_TRUE)is reached and that its register write matches this module. - Power sequence.
camera_hw/camera_project/cronos/kd_camera_hw.chas a dedicated OV02B branch with its own ordering andNEED_MANUAL_VCAMI2C_POWERhandling; confirm the driver's expectations match it.
Debugging this needs the driver's own logs, so build with
patches/0003-DEBUG-unsuppress-imgsensor-driver-logging.patch and switch the
driver's LOG_INF from no_printk to pr_err - by default it is compiled out
entirely. Note that with cameraserver set to disabled in
/system/etc/init/cameraserver.rc (see below) a bad driver no longer stops the
device booting, so this is iterable.
This is ordinary sensor bring-up: I2C works, streaming does not. It wants a datasheet or a known-good register dump for this module, not more architectural work.
#00 pc c714a0c2 <unknown> ← call through a garbage pointer
#01 sp<ANativeWindowBuffer>::operator=(ANativeWindowBuffer*)+14 libcam.client.so
#02 NSDisplayClient::StreamImgBuf::StreamImgBuf(...)+104 libcam.client.so
MediaTek's NSDisplayClient assigns a preview buffer into an
sp<ANativeWindowBuffer>, which calls incRef through the buffer's
android_native_base_t function pointer - and that pointer is garbage.
The likely cause is a contract mismatch rather than a signature one. AOSP's HAL1
shim implements preview_stream_ops with HIDL callbacks
(CameraPreviewWindow), and its sDequeueBuffer hands back a plain
buffer_handle_t*. MediaTek's blob appears to expect the preview window to be
backed by a real ANativeWindow, whose buffers are full ANativeWindowBuffer
objects with working refcount pointers - which is how this worked on stock,
where the HAL ran against a Surface directly.
This is a deeper class of problem than everything above it: not a symbol or a
struct size, but what kind of object the two sides think they are exchanging.
Fixing it likely means making the shim's preview window present buffers as
proper ANativeWindowBuffers (for example by wrapping each imported handle in a
GraphicBuffer and handing over its ANativeWindowBuffer view) rather than
passing raw handles. The shim is built from source, so this is editable - but it
is a design change, not a patch.
#00 DpIspStream::DpIspStream(DpIspStream::ISPStreamType)+264 libdpframework.so
#01 NSCamPipe::XdpPipe::init()+16 libcam.campipe.so
#02 NSCamPipe::ICamIOPipeBridge::init()+30
This is the blob-mixing problem the port maintainer described, and it is real.
libcam.campipe.so is API 25 and allocates a DpIspStream sized to the Android
7 definition; the libdpframework.so on this ROM is from the Android 9 blob set
and its constructor initialises a larger object, writing past the allocation.
Unlike the signature drift handled by libcamera_shim, a shim cannot fix this -
the caller's allocation is already too small before any of our code runs.
Proposed approach, not yet tried: give the camera stack its own
matching-vintage copy of the library rather than downgrading the system one.
Take the API 25 libdpframework.so from the stock dump, set its soname to
something private (libdpframework_cam.so), install it alongside, and
patchelf --replace-needed it into the camera blobs that link it. The display
and media stacks keep the Android 9 library, so the coupling that made
downgrading impossible no longer applies. Note that if the camera blobs get the
old library, the DpIspStream/DpBlitStream forwarders in libcamera_shim
become unnecessary and must be removed to avoid providing those symbols twice.
Risk: two copies of the display framework in one process may contend for the same kernel resources (cmdq, m4u). Untested.
Enumeration works; frames do not yet flow. Known blockers, in the order they appear:
-
Device node permissions. The HAL runs in
cameraserver(uid 1047, groups audio camera input drmrpc), but on Android 7 it ran inmediaserver, so the nodes it needs are owned bymedia:crw-rw---- media media /dev/MTK_SMI -rw-r----- system media /proc/m4uThis produces
M4U_L: m4u_config_port failedandBWC: Open SMI(/dev/MTK_SMI) driver file failed: Permission denied.chmod 666on both clears those errors, confirming the cause. The durable fix is to addmediato cameraserver's groups, or relabel the nodes tocameravia ueventd;/proc/m4uneeds an initchmodsince ueventd does not manage procfs. -
cronos has the wrong sensor driver, and the right one does not exist in this kernel. This is the blocker, and it is not fixable by configuration.
The stock cronos kernel's symbol table, from the firmware dump's
boot/boot.img-kallsyms, settles it:$ grep -iE 'ov02b|ov9734' boot.img-kallsyms c051a1e0 T OV02B_MIPI_RAW_SensorInit # present # zero ov9734 matchesThe Echo Show 5 2nd gen uses an OV02B10 (2 MP). OV9734 is a 1 MP part - the 1st gen
checkerssensor. Corroborating evidence:- both public cronos firmware dumps ship a
libcameracustom.sowhose sensor table contains onlyov02b10_mipi_rawandov02b10_mipi_raw_vc, no OV9734 at all; - the HAL's own search resolves index 0 to
<0x2b/ov02b10_mipi_raw/SENSOR_DRVNAME_OV02B_MIPI_RAW>; - Echo Show 5 gen 2 is specced with a 2 MP camera, gen 1 with 1 MP.
But every defconfig in R0rt1z2's kernel sets the gen-1 sensor:
cronos CONFIG_CUSTOM_KERNEL_IMGSENSOR="ov9734_mipi_raw" checkers CONFIG_CUSTOM_KERNEL_IMGSENSOR="ov9734_mipi_raw" crown CONFIG_CUSTOM_KERNEL_IMGSENSOR="ov9734_mipi_raw" rook CONFIG_CUSTOM_KERNEL_IMGSENSOR="gc0312_mipi_raw"and no ov02b10 driver exists anywhere in the tree - not on
lineage-18.1, not on the cronos-specificcronos/lineage-18.1branch. The kernel header declaresOV02B_MIPI_RAW_SensorInitbehind#if defined(OV02B10_MIPI_RAW), so the slot is there and the driver was simply never included.This is probably why the
crownwork got further than cronos ever could: crown is a gen-1 device, so itsov9734setting is correct.The driver has been ported and is saved as
patches/ov02b10_mipi_raw-driver-WIP.tar.gzpluspatches/0004-imgsensor-add-ov02b10-support-WIP-does-not-boot.patch.It went in easily. A reference
ov02bmipiraw_Sensor.cfrom an mt6765 tree (danya2271/kuroneko_r_mt6765) already uses the sameSENSOR_FUNCTION_STRUCTand the exactOV02B_MIPI_RAW_SensorInitname ourkd_sensorlist.hdeclares, so it compiled with a single fix - adding the missing forward declaration. What was needed:imgsensor/src/mt8163/ov02b10_mipi_raw/with the sensor.c/.hand a one-line Makefile (obj-y += ov02b10mipiraw_Sensor.o);OV02B_SENSOR_ID 0x2b,OV02BMIPI_SENSOR_IDandOV02B_III_SENSOR_ID 0x2cinkd_imgsensor.h(none were defined);- the
OV02B_MIPI_RAW_SensorInitprototype inkd_sensorlist.h; CONFIG_CUSTOM_KERNEL_IMGSENSOR="ov02b10_mipi_raw"incronos_defconfig.
No Makefile plumbing beyond that:
imgsensor/src/MakefileuppercasesCONFIG_CUSTOM_KERNEL_IMGSENSORinto a-Dflag, soov02b10_mipi_rawautomatically definesOV02B10_MIPI_RAWand activates the existingkd_sensorlist.hentry. Selecting only this sensor also puts it at kernel table index 0, which is where the HAL expects it - so the index-alignment problem resolves itself.The resulting kernel exports
OV02B_MIPI_RAW_SensorInitand no ov9734 symbols, matching the stock kernel's table exactly. But it does not boot - it hangs at the vendor logo and reboots. Verified as an isolated change: same base as a kernel that booted four times, no logging changes bundled in, only the sensor selection and new driver.Bisected, and the cause is narrow. Three flashes, one variable each:
kdSensorList[]index 0Boots? ov02b10 (only sensor compiled) no ov9734 (both compiled, canonical order) yes ov02b10 (both compiled, reordered) no The determining factor is ov02b10 sitting at index 0, not whether ov9734 is compiled in. So something on the boot path touches
kdSensorList[0]- consistent with the ADC-based proto/HVT board identification inkd_sensorlist.c- and the ported driver hangs when it is the one exercised.That localises the bug to the ported driver's power-up path, most likely
get_imgsensor_id()oropen(): the mt6765 reference spins on I2C reads while polling for the chip ID, and on mt8163 the regulators, MCLK and reset GPIO are sequenced bycamera_hw/camera_project/cronos/kd_camera_hw.c, whose OV02B branch has its own ordering andNEED_MANUAL_VCAMI2C_POWERhandling. If the driver reads before that sequence has completed, an I2C read with no timeout will wedge.Next step, no flash required: read the reference driver's
get_imgsensor_id()/open()retry loops against the in-treeov9734mipiraw_Sensor.cequivalents and against the cronos OV02B power branch, and bound every I2C wait. Only flash once that is done, and keep ov9734 at index 0 while iterating so a failed driver cannot stop the device booting - the HAL will bind the wrong sensor, but the device stays usable and the driver can be exercised deliberately.Also worth checking:
kd_sensorlist.cdoes have a boot-time platform/i2c probe, and there is ADC-based board identification in there (g_adc_id_iio_channel is null) - the cronos board distinguishes proto from HVT, so something may key off the compiled sensor;camera_hw/camera_project/cronos/kd_camera_hw.cimplements both sensors' power sequences, so check whether its OV02B branch touches a regulator or GPIO that is unsafe early;- bisect by compiling both sensors
(
CONFIG_CUSTOM_KERNEL_IMGSENSOR="ov9734_mipi_raw ov02b10_mipi_raw") to separate "the new driver breaks boot" from "removing ov9734 breaks boot". That last one is a single flash with a clear prediction either way.
What finishing this requires: porting an ov02b10 sensor driver into
drivers/misc/mediatek/imgsensor/src/mt8163/ov02b10_mipi_raw/, then settingCONFIG_CUSTOM_KERNEL_IMGSENSOR="ov02b10_mipi_raw"incronos_defconfig. Existing ov02b10 drivers are available (for exampledanya2271/kuroneko_r_mt6765andramabondanp/kernel_xiaomi_angelica, both atimgsensor/src/common/v1/ov02b_mipi_raw/ov02bmipiraw_Sensor.c), but they target MediaTek's newercommon/v1framework while mt8163 uses the legacy per-platform layout. The register tables, ID check and mode setup port over; the surroundingSENSOR_FUNCTION_STRUCTplumbing has to be rewritten to match the in-treeov9734_mipi_rawdriver's shape.patches/0003-DEBUG-unsuppress-imgsensor-driver-logging.patchmakes this tractable to debug: the driver routes every macro includingPK_ERRthroughpr_debug, so by default none of its diagnostics reach dmesg at all.Do not bother with sensor-table index alignment. Adding a placeholder entry so the HAL's and kernel's table indices agree does change which sensor the HAL resolves (it moved from
ov02b10_mipi_rawtoov02b10_mipi_raw_vc), but with no OV02B driver behind it there is nothing to talk to, and capture still fails. Tried and reverted. - both public cronos firmware dumps ship a
Kept because the mechanism is worth knowing. The kernel reports the sensor on
CAM[1] (MediaTek's sub/front slot) and names it after whatever driver is
compiled in:
$ cat /proc/driver/camera_info
CAM[1]:ov9734_mipi_raw;
But the HAL only ever tries to build metadata for OV02B, a sensor this unit
does not have, and never attempts OV9734 at all:
constructCustStaticMetadata_DEVICE_SENSOR_SENSOR_DRVNAME_OV02B_MIPI_RAW
returns status[Unknown error -1(-1)]
With no valid static metadata there are no camera parameters, so
getParameters failed (empty parameters) and every capture request fails.
This is why the camera still does not produce frames.
How the identification works: kdSetDriver() takes an index into
kdSensorList[], binds pSensorList[idx].SensorInit, and returns -EIO if
that slot is NULL. kdSensorList[] is a compile-time array whose entries are
each #if defined(<SENSOR>_MIPI_RAW), so it collapses to just the sensors
built in - with only ov9734_mipi_raw enabled it has one entry at index 0.
The HAL carries its own copy of the same table (the drvname strings live in
libcameracustom.so, which has exactly ov02b10_mipi_raw and
ov9734_mipi_raw) and the header comment says the two "should be the same".
Attempted and reverted: adding a placeholder {0, SENSOR_DRVNAME_OV02B_MIPI_RAW, NULL} entry ahead of OV9734, on the theory
that the HAL's index 0 is ov02b10 and a NULL init would make the kernel
reject it so the HAL would move on. The result changed Facing from Front to
Back but the HAL still resolved OV02B and still built no OV9734 metadata, so
the theory is wrong or incomplete. Reverted.
Next step, and do it with evidence rather than guessing: kdSetDriver()
already has PK_INFO("pDrvIndex:0x%08x/0x%08x") at its head, but that log
level is not reaching dmesg. Build a kernel with that promoted to pr_err
(and one in the NULL-init rejection path), then read exactly which indices
and socket bits the HAL requests and in what order. That pins the mapping in
a single flash cycle instead of guessing at orderings. Note the HAL also logs
GetCameraCalData(MainSensorIdx=2b), so 0x2b is worth understanding - it
may be an ID rather than an index.
-
getParameters failed (empty parameters)from the HAL1 shim, which is likely downstream of (2) - with no valid static metadata there are no parameters to return. -
ION, still logging
ioctl c0044901 failed with code -1: Invalid argument, as thecrowninvestigation also saw. Not yet known to be fatal.
It proves the stack will link, and that the mixing problem is far smaller than "the ABI does not line up" suggests — 11 shimmable symbols, all clean parameter drift, no struct-layout surgery and no binary patching required to get loaded.
It does not prove the stack will work. Symbol resolution says nothing about semantics, and the known hazards are all semantic:
- cmdq. The kernel's command-queue driver is the newer
CONFIG_MTK_CMDQ_TABvariant from the Android 9 karnak kernel, while these blobs were built against the older one. Whether the ioctl surface they use still behaves identically is untested. This is the risk that motivated the original pessimism and it is not addressed by anything above. - ION. The
crowninvestigation found the gralloc mapper's first passthrough fetch failing on cold boot withion: ioctl c0044901 failed, which suggests allocator readiness is genuinely marginal at that point in boot. - Struct layouts shared between the camera blobs and
libdpframework/libgralloc_extra(for exampleISP_TPIPE_CONFIG_STRUCT) are not verifiable statically. A silently changed field offset would surface as a crash or corrupted frames rather than a link error.
The honest read is that this converts an open-ended research problem into a bounded engineering one, with a real chance of failing somewhere in the cmdq or ION layer that no amount of static analysis will predict. The next step is empirical: build it, install it, and read the tombstones.
- Transplant karnak's Treble camera stack. Best-looking option on paper — newer blobs, matching kernel vintage, proven on modern LineageOS — but no OV9734 tuning and no way to add it.
- Build the HAL from source. MediaTek MT8163 camera HAL sources are not available.
- Downgrade the kernel's cmdq driver. The display and media stacks depend on it; this is the tradeoff the port maintainer described.
scripts/fetch-camera-blobs.sh # download stock camera stack from the dump
scripts/pull-device-libs.sh # pull this device's platform libraries
python3 scripts/deps.py # dependency closure
python3 scripts/syms.py # unresolved symbol reportNote when reading syms.py output that libc exports memcpy, memset,
strlen and friends as ifuncs, which naive readelf column parsing reports as
missing. They are present.