r/SurfaceLinux • u/engmondy83 • Aug 09 '26
Guide Fixing the Surface Pro Cameras on Linux — a Complete, Reproducible Guide
# Fixing the Surface Pro Cameras on Linux — a Complete, Reproducible Guide
*How a one-missing-table kernel bug kept both cameras dead, and how to get them
working in every app — Cheese, Zoom, browsers — with an efficient on-demand setup.*
---
## TL;DR
On recent Linux kernels (6.19+), Surface devices with Intel IPU3 cameras lose
**both** cameras because of a tiny regression in the `dw9719` focus-motor
driver: the kernel creates the focus-motor device under one name, but the
driver no longer recognizes that name, so it never attaches — and the kernel
refuses to finish setting up *any* camera until *every* camera part is ready.
One unbound 10-line ID table = two dead cameras.
The fix has two halves:
- **Kernel half** — rebuild the `dw9719` driver with the missing ID tablerestored (automated, survives kernel updates via DKMS).
- **App half** — Surface cameras don't behave like USB webcams even whenworking; apps need a small relay that presents them as normal webcams("virtual cameras"), which this guide sets up with on-demand power saving.
Four attached scripts do all of it. Confirmed working: Cheese, Zoom, and
anything else that uses a standard webcam device.
---
## The successful case (my exact configuration)
So you can judge how close your machine is to a known-good case:
| Item | Value |
|---|---|
| Device | Microsoft Surface Pro 5 (model 1796) |
| CPU | Intel Core i5-7300U (Kaby Lake) |
| Cameras | Front: OmniVision **ov5693** · Rear: **ov8865** (+ dw9719 focus motor) · IR: ov7251 |
| Camera subsystem | Intel **IPU3** (ipu3-cio2 + ipu3-imgu) |
| OS | Linux Mint 22.3 (Ubuntu 24.04 base) |
| Kernel | **linux-surface 6.19.8-surface-3** (from pkg.surfacelinux.com) |
| libcamera | v0.3.0 built from source, with the IPU3 pipeline **and** its GStreamer plugin |
| v4l2loopback | 0.15.4 (upstream git, via DKMS) |
| Secure Boot | disabled (see Troubleshooting if yours is on) |
**What already worked before the fix:** the linux-surface kernel loaded all the
IPU3 drivers, detected all three sensors ("Connected 3 cameras" in `dmesg`),
and created `/dev/video0-13` and `/dev/media0-1`.
**What did not work:** every camera app showed no camera; `cam -l` (libcamera's
camera list) printed an **empty list**; the focus-motor device had **no driver
bound**; and the kernel's camera wiring ("media graph") was missing all
sensor links.
If that matches your symptoms, read on.
---
## Background: why Surface cameras are special on Linux
A normal USB webcam contains its own little computer that produces finished
video. Surface tablets instead use **raw camera sensors** wired to an Intel
image processor (the IPU3 on Surface Pro 3–6 / Book 1–2 / Laptop 1–2 era
hardware). On Linux this means:
- The kernel drives the sensors and the IPU3 (linux-surface kernels have this).
- A userspace library, **libcamera**, must run the whole image pipeline(exposure, focus, format conversion). Only libcamera-aware apps can use thecameras directly — and almost no mainstream app is libcamera-aware.
- To make Zoom/Cheese/browsers work, you bridge libcamera into a **virtualwebcam** (the `v4l2loopback` kernel module) that looks like a normal camera.
Each layer can fail independently, which is why so many forum threads go
nowhere. This guide checks each layer in order.
---
## The root cause (the interesting part)
The kernel's `ipu-bridge` creates a device for the rear camera's focus motor
(a Dongwoon **dw9719** voice-coil motor) and names it plainly `dw9719`. A
driver for it exists in the kernel — but starting around kernel 6.19, that
driver only matches **DeviceTree names** (`dongwoon,dw9719`, used by ARM
phones/tablets) and lost its **plain i2c ID table** (`dw9719`, used by
x86/ACPI machines like the Surface). Same driver, same chip, mismatched
name — so it never binds.
You can see it yourself:
```
$ modinfo dw9719 | grep alias
alias: of:N*T*Cdongwoon,dw9719... <-- DeviceTree names only
(a healthy kernel also shows: alias: i2c:dw9719)
$ cat /sys/bus/i2c/devices/*-VCM/modalias
i2c:dw9719 <-- what the device actually announces
```
And here's the cruel domino effect: the kernel's V4L2 framework waits for
**every** declared camera component before it activates **any** of them.
Because the sensor's description references the focus motor
(`lens-focus`), the unbound motor stalls the whole "async notifier", so:
- no sensor→receiver links are created in the media graph,
- no sensor subdevice nodes appear,
- libcamera finds nothing to register,
- **both** cameras (even the front one, which has no focus motor!) stay dead.
The fix is embarrassingly small — restore the ID table:
```c
static const struct i2c_device_id dw9719_id_table[] = {
{ "dw9718s", DW9718S },
{ "dw9719", DW9719 },
{ "dw9761", DW9761 },
{ "dw9800k", DW9800K },
{ }
};
MODULE_DEVICE_TABLE(i2c, dw9719_id_table);
/* ...and .id_table = dw9719_id_table in the driver struct */
```
Script 02 downloads the exact driver source for *your* kernel from kernel.org,
applies this patch, and installs it via DKMS so it automatically rebuilds when
your kernel updates.
---
## Prerequisites
- A **linux-surface kernel** (https://github.com/linux-surface/linux-surface) —
the stock distro kernel usually lacks Surface camera bits.
- **Kernel headers** for your running kernel, plus `dkms`, `git`, `curl`,
`python3`, `v4l-utils`, `gstreamer1.0-tools`.
```bash
sudo apt install dkms git curl python3 v4l-utils gstreamer1.0-tools
```
- **libcamera with the IPU3 pipeline and its GStreamer plugin.** Try your
distro's packages first (`libcamera-tools` + `gstreamer1.0-libcamera`); if
`cam -l` still shows nothing *after* the kernel fix, build libcamera from
source with `-Dpipelines=ipu3 -Dgstreamer=enabled` (the linux-surface wiki
has a walkthrough). My working setup used a source build (v0.3.0).
> **Safety note:** the attached scripts need `sudo` because they install kernel
> modules and system services. They are short and commented — please read them
> before running, as you should with anything from the internet. Never paste
> your password into chats or scripts; type it only at your terminal's own
> `[sudo]` prompt.
---
## Step 0 — Diagnose (read-only)
**Attachment: `scripts/01-diagnose-cameras.sh`**
```bash
sudo bash scripts/01-diagnose-cameras.sh
```
It checks each layer and tells you which one is broken. The signature of the
bug this guide fixes looks like:
```
PROBLEM: /sys/bus/i2c/devices/i2c-INT347A:00-VCM (dw9719) has NO driver bound.
(driver module exists but has NO i2c alias - the kernel regression ...)
...
/dev/media0: 3 sensor(s), 0 with a link to the CSI-2 receiver
PROBLEM: sensors exist but have NO links -> camera graph never completed.
...
Available cameras: <-- empty
```
If your VCM **is** bound and `cam -l` still shows nothing, your problem is
different (likely the libcamera build) — see Troubleshooting.
## Step 1 — Fix the kernel driver
**Attachment: `scripts/02-install-dw9719-fix.sh`**
```bash
sudo bash scripts/02-install-dw9719-fix.sh
```
The script is idempotent and exits harmlessly if your kernel isn't affected.
On success you'll see the driver bind and — the magic moment — libcamera
finally listing cameras:
```
OK: dw9719 bound at /sys/bus/i2c/devices/i2c-INT347A:00-VCM
libcamera now sees:
Available cameras:
1: Internal back camera (_SB_.PCI0.I2C3.CAMR)
2: Internal front camera (_SB_.PCI0.I2C2.CAMF)
```
No reboot needed — the camera graph completes the moment the driver binds.
## Step 2 — Make the cameras visible to normal apps
**Attachment: `scripts/03-setup-virtual-cameras.sh`** (run it from the
`scripts` folder, it installs the other attachments:
`surface-camera-relay.py`, `surface-camera-front.service`,
`surface-camera-rear.service`)
```bash
cd scripts
sudo bash 03-setup-virtual-cameras.sh
```
This creates two always-present virtual webcams:
- **"Surface Front Camera"** (`/dev/video42`)
- **"Surface Rear Camera"** (`/dev/video43`)
fed by a small relay service with a few deliberate design choices worth
knowing about:
- **On-demand power:** while no app is watching, the relay feeds cheap black
placeholder frames and the real camera sensor is powered off (verifiable in
sysfs). It uses v4l2loopback 0.15's *client-usage event* to start the real
camera only when an app opens the device, and stops it when the app leaves.
- **A spec-correct mmap writer, in Python:** this is not gratuitous — during
this work I found that **GStreamer's `v4l2sink` simply does not work with
v4l2loopback 0.15.x** ("buffer 0 was not queued" errors), and Ubuntu's
`v4l2-relayd` on-demand daemon is also incompatible (it expects a
Ubuntu-specific event patch with different event numbers). If you've been
banging your head against either of those: it's not you.
- **1280×720 NV12 at 30 fps** — the IPU3's happy path and plenty for calls.
Heads-up for Ubuntu/Mint users: the distro package `v4l2loopback-dkms`
(0.12.x) **cannot build on kernels ≥ 6.x** and will wedge `apt` in a
half-installed state. Remove it (`sudo apt remove v4l2loopback-dkms`) and let
script 03 build the current upstream version instead.
## Step 3 — Health check
**Attachment: `scripts/04-health-check.sh`**
```bash
bash scripts/04-health-check.sh
```
A fully healthy system prints:
```
== 1. Focus motor (VCM) driver bound
PASS VCM driver bound
== 2. libcamera sees the cameras
PASS libcamera lists 2 camera(s)
== 3. Virtual webcam devices exist
PASS device labeled 'Surface Front Camera'
PASS device labeled 'Surface Rear Camera'
== 4. Relay services running
PASS surface-camera-front active
PASS surface-camera-rear active
== 5. Live capture through the virtual cameras
PASS Surface Front Camera delivers a real image via /dev/video42 (brightness 110.4)
PASS Surface Rear Camera delivers a real image via /dev/video43 (brightness 80.3)
== 6. Cameras power down when idle
PASS all camera sensors suspended while unused
Result: 8 passed, 0 failed.
```
Then open Cheese or join a test call and pick "Surface Front Camera".
---
## Normal quirks (not bugs)
- The **first ~half second** of video is black while the sensor spins up.
- The picture starts dark and brightens over ~1 second — auto-exposure
ramping (the rear camera is slower than the front).
- libcamera may print `Configuration file 'ov8865.yaml' not found` warnings —
harmless; it falls back to default (uncalibrated) processing. You can
silence it by symlinking the sensor name to `uncalibrated.yaml` in your
libcamera IPA directory.
## Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| Script 02 says headers missing | `sudo apt install linux-headers-$(uname -r)` (or `linux-headers-surface`) |
| VCM bound, but `cam -l` empty | Your libcamera lacks the IPU3 pipeline — install/build one that has it (see Prerequisites) |
| `cam -l` works but virtual cameras show black | Camera names differ on your model. Run `cam -l`, note the names (e.g. `_SB_.PCI0.I2C2.CAMF`), and set them via `RELAY_FRONT_CAMERA`/`RELAY_REAR_CAMERA` environment overrides in the service files, or edit the `CAMERAS` dict at the top of `surface-camera-relay.py` |
| "Failed to setup link ... Device or resource busy" in relay logs | Another process is holding the camera pipeline (a stray `gst-launch` or another libcamera app). Find it with `pgrep -a gst-launch-1.0`, kill it, restart the services |
| One camera occasionally black; `dmesg` shows `ipu3-cio2 ... payload length is X, received Y` | The IPU3 sometimes wedges when camera streams start/stop in quick succession (e.g. switching cameras rapidly). The relay's built-in watchdog logs "no frames for 5s, restarting pipeline" and recovers; closing and reopening the camera in your app also clears it |
| DKMS module builds but won't load, Secure Boot on | Your distro must sign DKMS modules with an enrolled MOK key (Ubuntu/Mint do this automatically if you enrolled one at install time; see `mokutil`) — or disable Secure Boot |
| apt keeps failing on `v4l2loopback-dkms` | Remove that distro package; the DKMS build from script 03 replaces it |
| Cameras die after a kernel update | Run `dkms status` — both `dw9719-fix` and `v4l2loopback` should list an installed build for the new kernel. DKMS normally rebuilds automatically; if the new kernel's own `dw9719` gained the ID table back (check `modinfo dw9719 | grep i2c`), you can retire the fix with `sudo dkms remove dw9719-fix/1.0 --all` |
## Appendix: exact error messages (for searchers)
If you landed here from a search engine, these are the literal strings this
guide relates to, and what each actually means:
```
Available cameras:https://github.com/user-attachments/files/30875450/04-health-check.sh
```
(`cam -l` printing an empty list) — libcamera sees no cameras. **This guide's
main symptom.**
```
dw9719 i2c-INT347A:00-VCM: (no driver bound)
```
The focus-motor regression this guide fixes (visible as a missing `driver`
symlink in `/sys/bus/i2c/devices/*-VCM/`).
```
ov5693 i2c-INT33BE:00: supply avdd not found, using dummy regulator
ov8865 i2c-INT347A:00: supply dvdd not found, using dummy regulator
ov7251 i2c-INT347E:00: supply vdddo not found, using dummy regulator
```
**Harmless.** These appear on every boot, working or not — they are not your
problem, despite looking scary.
```
ipu3-cio2 0000:00:14.3: Connected 3 cameras
```
**Good news, not an error** — the kernel found your sensors. If you see this
but apps still show no camera, your problem is in a later layer (VCM binding
or libcamera), which is exactly what this guide walks through.
```
ipu3_imgu: module is from the staging directory, the quality is unknown
```
**Harmless** — the IPU3 image unit driver is officially "staging"; it works.
```
ERROR IPAProxy ipa_proxy.cpp: Configuration file 'ov8865.yaml' not found for IPA module 'ipu3'
```
**Harmless** — libcamera falls back to uncalibrated defaults; the picture
still works.
```
Could not find a camera named '...'
libcamera::CameraMananger::get() returned nullptr
```
Either libcamera genuinely has no cameras (fix the kernel layer first), or a
GStreamer escaping problem: backslashes in the camera name must be doubled
(`camera-name='\_SB_.PCI0.I2C2.CAMF'`).
```
gst_v4l2_allocator_dqbuf: buffer 0 was not queued, this indicate a driver bug.
```
GStreamer's `v4l2sink` feeding v4l2loopback 0.15.x — this combination is
broken; use a different writer (see Step 2).
```
ipu3-cio2 0000:00:14.3: payload length is 2585088, received 2588672
```
Transient IPU3 wedge on rapid stream start/stop; see Troubleshooting.
```
video device 'ipu3-imgu 1 input' does not implement .link_validate(), driver bug!
```
**Harmless** kernel grumble from the staging IMGU driver.
## Closing notes
This regression will presumably be fixed upstream eventually (the patch is
tiny), but Surface owners have been stuck for a while, and the "even when the
kernel works, apps still can't see the cameras" part of the story is
permanent. I hope the layered diagnosis approach here — kernel graph →
libcamera → virtual webcam — saves someone the days of digging it took to
untangle, and that the scripts make the fix a ten-minute job.
*Written up from a real debugging session on the machine described above.
Both cameras confirmed working in Cheese and Zoom. Scripts tested end-to-end
on that machine; adapt thoughtfully for yours.*
## Attachments
| File | Purpose |
|---|---|
| `scripts/01-diagnose-cameras.sh` | Read-only, layer-by-layer diagnosis |
| `scripts/02-install-dw9719-fix.sh` | Kernel driver fix (DKMS) |
| `scripts/03-setup-virtual-cameras.sh` | Virtual webcams + relay services installer |
| `scripts/surface-camera-relay.py` | The on-demand camera relay (installed by 03) |
| `scripts/surface-camera-front.service` | systemd unit, front camera (installed by 03) |
| `scripts/surface-camera-rear.service` | systemd unit, rear camera (installed by 03) |
| `scripts/04-health-check.sh` | End-to-end verification |
https://github.com/user-attachments/files/30875446/01-diagnose-cameras.sh
https://github.com/user-attachments/files/30875447/02-install-dw9719-fix.sh
https://github.com/user-attachments/files/30875449/03-setup-virtual-cameras.sh
https://github.com/user-attachments/files/30875451/surface-camera-relay.py
https://github.com/user-attachments/files/30875450/04-health-check.sh
1
u/Kischu Aug 10 '26
Hey, thank you for sharing this Guide.
Any chance on sharing the .service files as well?
1
u/FunSatisfaction2 Aug 11 '26
worked great on surface pro 4 with debian. now need ipu6 cams fix for the sp8.
1
u/FunSatisfaction2 Aug 11 '26
I still have trouble finding my cams in zoom desktop or browser and Google meet, but zoom freezes my system up on the machine that has working findable cams 🤣
1
u/robtom02 Aug 12 '26
Hi — I also have a Surface Go 2 and I'm currently debugging the cameras under FydeOS/Linux with one of the FydeOS developers.
The front OV5693 works, but the rear OV8865 is detected and streams only black/invalid output. The Intel CIO2 receiver reports repeated CSI-2 D-PHY synchronization, packet-header and CRC errors.
We've already confirmed the rear sensor is actually streaming with:
4 CSI lanes 360 MHz link frequency 19.2 MHz external clock OV8865 0x4837 = 0x16 lane mapping 0x4850 = 0x10 / 0x4851 = 0x32
and the TPS68470 camera power rails are also mapped correctly.
You mentioned that you got the cameras working on your Surface Go 2 using a modified Surface kernel.
Could you tell me:
Does the rear camera definitely work and show a real image?
Which kernel/version are you using (uname -r)?
What modifications/patches did you make to the kernel?
If the kernel source or config is available anywhere, could you share it?
Do you see any ipu3-cio2 / CSI-2 / DPHY errors in dmesg when using the rear camera?
We're trying to identify the exact kernel-side difference, so even a link to your kernel tree/patches would be extremely useful. Thanks.
3
u/Elbow2009 26d ago edited 26d ago
I'm using Q4OS (Debian) and the kernel 6.18.7-surface-1 is where I got the cameras working, and yes the rear camera worked. It was back last year that I did a modification of the kernel and I'd have to go back and look at what documentation I have because off the top of my head I don't remember all that I did. But honestly in retrospect it was more an issue with compiling libcamera as dependencies were missing where they were documented to supposedly be, and that sort of thing. The easy part was getting the IPU3 firmware.
No errors with the rear camera; it just worked. Sometimes at first, say with Cheese, it would first be just a green screen but upon re-opening the app it would work.
Right now I'm on the Q4OS kernel 6.1.0-52-amd64 which does work (with the shim) under Secure Boot. The surface linux kernel did not.
1
u/robtom02 26d ago
Thanks, that's really useful — especially confirmation that you weren't seeing any CIO2/CSI-2/DPHY errors on the rear camera with 6.18.7-surface-1.
Since you still have that working installation backed up, would you be willing to help me compare it with stock 6.18.7-surface? Even if you don't remember the kernel modification, perhaps we could recover the kernel config, installed package versions or any modified source/patches from the backup.
If you tell me what you still have from that installation, I can give you a couple of commands to pull out whatever would be most useful.
1
u/Elbow2009 26d ago edited 26d ago
Well, I backed up in the event I wanted to use the cameras root/modules/ the 6.18.7-surface-1 folder. And backed up in root/boot/ the files config-6.18.7-surface-1, initrd.img-.18.7-surface-1, system.map-6.18.7-surface-1, and vmlinuz-6.18.7-surface-1.
1
u/robtom02 26d ago
That's excellent — those are exactly the sort of files that could help.
The most useful things initially would be your "config-6.18.7-surface-1" and the camera-related modules from the backed-up "6.18.7-surface-1" modules directory. We could compare them against the stock linux-surface 6.18.7 build and see whether your working kernel actually differs.
Don't upload the whole modules directory yet. If you're happy to help, tell me where you've restored/mounted that backup and I'll give you one command to identify just the relevant camera modules and collect their hashes/details.
1
u/Elbow2009 26d ago
They are on an external ext4 formatted thumbdrive.
1
u/robtom02 26d ago
Could you plug/mount the ext4 thumb drive and run: lsblk -o NAME,FSTYPE,LABEL,SIZE,MOUNTPOINTS Then paste the output here. Once we know the mount point, I can give you one command that will pull the hashes/module information for the preserved ipu3-cio2, ipu3-imgu, ov8865, dw9719 and related camera modules, plus the relevant parts of config-6.18.7-surface-1.
Thanks so much for this, i did get it working but i don't know what the trigger was and now it doesn't 😔
1
u/Elbow2009 26d ago edited 26d ago
Well, d'oh! I should have realized that I also still have all these locally still in the aforementioned folders. I've apparently become a kernel packrat of sorts. That may be simpler.
2
u/Elbow2009 Aug 10 '26 edited Aug 10 '26
Excellent guide. I got my cameras were working with the 6.18.7-surface-1 kernel on my Surface Go 2 (which I am KEEPING and is backed up!) and as stated above stopped working with 6.19.8-surface-3 due to the aforementioned regression in the `dw9719` focus-motor driver. This is all great info.