The Intel IPU6 (Imaging Processing Unit, generation 6) is the camera subsystem in Tiger Lake, Alder Lake, Raptor Lake, and Meteor Lake laptops. If you have a recent ThinkPad, XPS, or Surface, your webcam likely runs through it. For years, getting it to work on Linux meant an out-of-tree driver stack from Intel’s GitHub - four separate repositories, a DKMS module, proprietary firmware blobs, and a GStreamer-based relay daemon. It was fragile, broke on kernel updates, and couldn’t survive suspend/resume on recent kernels.

The mainline kernel has had IPU6 ISYS support since 6.10. libcamera has had IPU6 support via its Simple pipeline handler since 0.3.2. But making the transition from the old stack to the new one isn’t obvious - the pieces are spread across kernel modules, firmware, PipeWire configuration, and browser flags. This post documents the full migration on a ThinkPad X1 Carbon (Alder Lake) with an OmniVision OV2740 sensor, running CachyOS kernel 6.19.3.

The Old Stack

The out-of-tree setup, typically installed via AUR packages like archlinux-ipu6-webcam, looks like this:

Sensor (OV2740)
  -> IPU6 ISYS (raw capture)
  -> IPU6 PSYS (hardware ISP)        [out-of-tree, not in mainline]
  -> intel-ipu6-camera-hal            [proprietary HAL]
  -> icamerasrc (GStreamer plugin)
  -> v4l2-relayd
  -> v4l2loopback (/dev/video0)
  -> Applications

This required six packages: intel-ipu6-dkms-git, intel-ipu6ep-camera-bin, intel-ipu6ep-camera-hal-git, icamerasrc-git, v4l2-relayd, and v4l2loopback-dkms. The DKMS modules had to be rebuilt for every kernel update, and Intel’s out-of-tree repository stopped keeping up with kernel API changes around 6.16. Suspend/resume broke entirely - intel/ipu6-drivers#381 tracks the regression, which remains unfixed.

The PSYS module (Processing System - the hardware ISP) was never merged into mainline. Intel considers the hardware interface and ISP algorithms proprietary. Without PSYS, the entire icamerasrc pipeline can’t function. On kernel 6.19, the DKMS modules simply won’t build.

The New Stack

The mainline path replaces the hardware ISP with libcamera’s software ISP (SoftISP):

Sensor (OV2740)
  -> IPU6 ISYS (raw Bayer capture)    [in-tree since 6.10]
  -> libcamera Simple pipeline
  -> SoftISP (CPU/GPU debayering)
  -> PipeWire (Video/Source node)
  -> Applications

No DKMS, no proprietary blobs, no GStreamer relay chain. The trade-off is image quality - the SoftISP does CPU-based (or GPU-accelerated, since libcamera 0.7) Bayer demosaicing instead of the dedicated hardware ISP. Colors need tuning. But it works with mainline kernels, survives updates, and integrates natively with PipeWire.

Step 1: Remove the Old Packages

Remove every component of the out-of-tree stack:

sudo pacman -Rns icamerasrc-git-fix \
  intel-ipu6ep-camera-hal-git-fix \
  intel-ipu6ep-camera-bin-fix \
  intel-ipu6-dkms-git-fix \
  intel-ivsc-firmware \
  v4l2-relayd \
  v4l2loopback-dkms-git-fix

The package names may differ depending on which AUR packages you installed. The -Rns flag removes dependencies and config files.

Keep libcamera, libcamera-ipa, pipewire-libcamera, and gst-plugin-libcamera - these are the new stack.

Step 2: Verify Kernel Modules

After a reboot on kernel 6.10+, the in-tree modules should load automatically:

$ lsmod | grep -E "ipu6|ov2740|ivsc"
intel_ipu6_isys       151552  6
intel_ipu6             90112  7 intel_ipu6_isys
ipu_bridge             24576  2 intel_ipu6,intel_ipu6_isys
ivsc_csi               ...
ivsc_ace               ...
ov2740                 28672  0

The critical chain is: mei_vsc_hw -> mei_vsc -> ivsc-ace (powers the sensor) -> ivsc-csi (bridges CSI-2) -> ov2740 (sensor driver) -> intel_ipu6_isys (capture). If ivsc-ace or ivsc-csi are missing, the sensor won’t power on and libcamera will report “No sensor found.”

The required kernel configs (check with zgrep on /proc/config.gz):

CONFIG_VIDEO_INTEL_IPU6=m
CONFIG_INTEL_VSC=m
CONFIG_INTEL_SKL_INT3472=m

CachyOS and recent Arch kernels have all three enabled as modules.

Step 3: Verify libcamera Sees the Camera

$ cam --list
Available cameras:
1: Internal front camera (\_SB_.PC00.LNK1)

The warnings about “Unable to get rectangle” and “sensor kernel driver needs to be fixed” are cosmetic - the OV2740 kernel driver doesn’t implement the full V4L2 selection API that libcamera prefers, but capture works regardless.

If cam --list shows nothing, check permissions. The user needs to be in the video group:

sudo usermod -aG video $USER

Log out and back in for the group change to take effect.

Step 4: Fix the Color Pipeline

Out of the box, the SoftISP uses a generic uncalibrated profile. The image will have a heavy green tint and unstable brightness. libcamera logs confirm the fallback:

WARN: Configuration file 'ov2740.yaml' not found for IPA module 'simple',
      falling back to '/usr/share/libcamera/ipa/simple/uncalibrated.yaml'

The uncalibrated profile enables AGC (auto gain control) and AWB (auto white balance) but has no color correction matrix. The Bayer demosaicing stage inherently produces a green-biased output because the Bayer pattern has twice as many green pixels as red or blue. Without a CCM to compensate, everything looks green.

The AGC flicker problem

The SoftISP’s AGC algorithm (src/ipa/simple/algorithms/agc.cpp) uses histogram-based exposure and gain control. It computes a Mean Sample Value (MSV) from the luminance histogram and adjusts exposure/gain to converge on a target brightness. The problem is the control law: it’s a bang-bang controller with a fixed ~10% step per frame.

// libcamera 0.7.0 - agc.cpp:50-52
static constexpr uint8_t kExpDenominator = 10;
static constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1;    // always +10%
static constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1;  // always -10%

The hysteresis dead band is only +/-0.2 on a 0-5 MSV scale (+/-4%). When the correct exposure falls within one step of the current value - say, 5% above - the controller jumps +10%, overshoots, then -10%, undershoots, and oscillates forever. This is visible as brightness flicker on every sensor, but it’s especially bad on the OV2740 behind IPU6 where the IVSC bridge adds latency to control application.

The fix is to replace the bang-bang controller with a proportional one, where the step size scales with the error magnitude:

// Proportional: small error -> small step, large error -> large step
float step = std::clamp(static_cast<float>(error) * kExpProportionalGain,
                        -kExpMaxStep, kExpMaxStep);
float factor = 1.0f + step;

With kExpProportionalGain = 0.04 and kExpMaxStep = 0.15:

Error (MSV off target)Step size
2.5 (very dark)+10%
1.0+4%
0.3+1.2%
0.1 (nearly correct)+0.4%

The controller converges smoothly without overshooting. There’s a ~3 second ramp-up from cold start (sensor defaults to 1x gain), but no flicker. This is a generic improvement that benefits all sensors on the Simple pipeline, not just the OV2740.

Creating a tuning file

The OV2740 tuning file goes in /usr/share/libcamera/ipa/simple/ov2740.yaml. libcamera discovers it automatically by matching the sensor name. With the proportional AGC fix, AGC can remain enabled:

# /usr/share/libcamera/ipa/simple/ov2740.yaml
# SPDX-License-Identifier: CC0-1.0
%YAML 1.1
---
version: 1
algorithms:
  - BlackLevel:
  - Awb:
  - Ccm:
      ccms:
        - ct: 6500
          ccm: [ 2.49, -0.91, -0.26,
                -0.30,  1.20,  0.10,
                 0.07, -0.80,  2.19 ]
  - Adjust:
  - Agc:

The CCM is tuned to counter the green bias from the debayer stage. The matrix was computed iteratively - capturing frames, measuring RGB ratios in white regions, and adjusting until neutral grey actually read as neutral.

The measured progression across tuning iterations:

IterationR/G ratioB/G ratioCeiling lumaNotes
uncalibrated.yaml0.8190.77388Heavy green, AGC flicker
CCM v1 (balanced rows)0.9310.886108Better, still green, dark
CCM v2 (measured correction)0.9870.979108Near-neutral, dark (no AGC)
CCM v2 + fixed gain1.0231.086169White reads as white

A slight blue tint remains (B/G=1.086) and the left side of the frame is darker than the right due to lens shading - a per-pixel correction that the SoftISP doesn’t implement. For a webcam, this is acceptable.

Setting digital gain

The libcamera Simple IPA’s AGC controls exposure and analogue_gain but not digital_gain. The sensor’s V4L2 controls:

ControlRangeDefaultManaged by
exposure4-11021102libcamera AGC
analogue_gain128-1983128libcamera AGC (128 = 1x, 1983 = 15.5x)
digital_gain1024-40951024Not managed (1024 = 1x)

With the proportional AGC, exposure and analogue_gain are handled automatically. But the OV2740’s analogue gain range (1x-15.5x) isn’t always enough for indoor lighting. A 2x digital gain boost provides the missing headroom.

Since libcamera doesn’t touch digital_gain, there’s no race condition - a udev rule can set it at device probe time:

# /etc/udev/rules.d/99-ov2740-digital-gain.rules
SUBSYSTEM=="video4linux", KERNEL=="v4l-subdev*", ATTR{name}=="ov2740*", \
    ACTION=="add", RUN+="/usr/bin/v4l2-ctl -d /dev/$kernel --set-ctrl=digital_gain=2048"

No systemd services, no WirePlumber restart, no timing hacks. The gain is set once when the kernel creates the subdev node.

Step 5: Browser Configuration

Chromium-based browsers (Chrome, Vivaldi, Edge) need the PipeWire camera feature flag to discover cameras through PipeWire instead of V4L2.

Chrome (~/.config/chrome-flags.conf):

--enable-features=WaylandWindowDecorations,WebRTCPipeWireCapturer,WebRtcPipeWireCamera

Vivaldi (~/.config/vivaldi-stable.conf):

--enable-features=UseOzonePlatform,WaylandWindowDecorations,WebRTCPipeWireCapturer,WebRtcPipeWireCamera

Important: multiple --enable-features lines don’t merge - Chromium uses only the last one. Combine all features into a single comma-separated line.

Firefox: set media.webrtc.camera.allow-pipewire to true in about:config.

The feature flag name is WebRtcPipeWireCamera (not PipeWireCamera - check strings /path/to/browser-binary | grep -i PipeWireCamera to confirm your browser’s naming).

Suspend/Resume

The IPU6 firmware re-authentication fails after S3 resume on kernels 6.16+. After waking from suspend:

intel-ipu6: Unexpected magic number 0xffffffeb
intel-ipu6: FW authentication failed(-110)

The CSE (Converged Security Engine) can’t re-authenticate the IPU6 firmware - the DMA buffer containing the firmware image isn’t properly restored. This is tracked at intel/ipu6-drivers#381 and remains unfixed upstream.

The workaround is a systemd sleep hook that unloads and reloads the modules:

# /usr/lib/systemd/system-sleep/ipu6-suspend.sh
#!/bin/sh
case "$1" in
    pre)
        modprobe -r ov2740 intel_ipu6_isys intel_ipu6
        ;;
    post)
        modprobe intel_ipu6
        ;;
esac

This forces a clean re-probe (including fresh CSE authentication) on every resume.

Upstreaming

Two patches have been submitted to the libcamera mailing list:

1. Proportional AGC for the Simple pipeline. The bang-bang controller in src/ipa/simple/algorithms/agc.cpp is replaced with a proportional controller. This is a generic improvement - any sensor on the Simple pipeline benefits from reduced overshoot. The patch is ~30 lines of changed logic.

2. OV2740 tuning file. The ov2740.yaml goes in src/ipa/simple/data/. As of libcamera 0.7, no sensor-specific tuning files exist for the Simple pipeline - only uncalibrated.yaml. This would be the first.

The cover letter describes the series and testing methodology.

The CCM values are eyeball-calibrated from pixel measurements, not from a proper Macbeth ColorChecker calibration under controlled lighting. A proper submission would benefit from at least one calibration run, ideally at multiple color temperatures (2800K, 4000K, 6500K). The libcamera project provides tuning tools in utils/tuning/ that could help.

The digital_gain udev rule is a system-level workaround that doesn’t belong upstream. The proper fix would be extending the Simple IPA’s AGC to also manage V4L2_CID_DIGITAL_GAIN when the sensor’s analogue gain range is exhausted.

Final Setup

After migration, the working configuration on a ThinkPad X1 Carbon (Alder Lake, OV2740, CachyOS 6.19.3, libcamera 0.7.0) is:

ComponentSourcePurpose
intel-ipu6, intel-ipu6-isysIn-tree kernel moduleRaw Bayer capture
ivsc-ace, ivsc-csiIn-tree kernel moduleSensor power/CSI bridge
ov2740In-tree kernel moduleSensor driver
libcamera 0.7.0 (patched)Arch extra repo + AGC patchSimple pipeline + proportional AGC
pipewire-libcameraArch extra repoPipeWire integration
ov2740.yamlPatched into libcameraCCM + AGC
99-ov2740-digital-gain.rulesudev ruledigital_gain=2x at probe

Image quality is noticeably worse than the proprietary Intel stack - the SoftISP doesn’t have per-pixel lens shading correction, the CCM is a single global matrix rather than a per-channel curve, and the debayer algorithm is simpler. But it works with mainline kernels, doesn’t break on updates, integrates natively with PipeWire, and requires zero out-of-tree code.

For video conferencing, which is what most laptop webcams are used for, it’s sufficient.

References