Skip to content

feat(camera): Support media-controller IR cameras (IPU6/IPU7, Qualcomm CAMSS) - #396

Draft
turbineBMW wants to merge 3 commits into
tyvsmith:mainfrom
turbineBMW:feat/media-controller-ir
Draft

turbineBMW wants to merge 3 commits into
tyvsmith:mainfrom
turbineBMW:feat/media-controller-ir

Conversation

@turbineBMW

Copy link
Copy Markdown

Summary

  • add [device] keep_format to use a media-controller node's configured format instead of negotiating one
  • accept GREY frames whose rows are padded past the width, cutting each row before decoding; every other stride mismatch still fails at open
  • add [device] ir_led to light an LED class IR illuminator while the camera is open and switch it off first on release
  • discover and capture from nodes that offer only V4L2_CAP_VIDEO_CAPTURE_MPLANE (single-plane formats only), via a small mplane module beside the v4l crate
  • document the keys in docs/configuration.md, docs/contracts.md and the config template, multi-planar nodes in docs/compatibility.md, and each change in the changelog

Why

IR cameras behind a media-controller pipeline (Intel IPU6/IPU7 ISYS, Qualcomm CAMSS on Snapdragon X laptops) only stream the size the pipeline was set up for, pad GREY rows, and on CAMSS expose multi-planar capture nodes only, so facelock either renegotiated a format the graph rejects, refused the stride at open, or reported "not a video capture device". Their illuminators can also be separate LED class devices that nothing lights during streaming. This was developed on a Surface Pro 11 (VD55G0 IR sensor on CAMSS, ir:flash PMIC LED, 644x604 GREY in a 656-byte stride) and a Surface Pro 12 (IPU7), where enrollment, facelock test and lock-screen unlock now work.

Reviewer notes

  • Classification is not part of this PR. A CAMSS or IPU7 node advertises a static table of every format, so it never has the mono-only evidence a name-only force_ir quirk needs, and it has no USB identity. On my machines I corroborate a name-only quirk when the node is backed by real hardware (sysfs device outside /sys/devices/virtual) and advertises the quirk's IR-typical format_preference. That makes every node on the same card [IR], which conflicts with the per-node reasoning in docs/security.md, so I would rather ask how you want platform IR nodes identified before sending code.
  • Device coupling stores NULL for these cameras. DeviceFingerprint has no vid/pid for a platform device, so templates fall under bind_legacy_templates.
  • ir_led writes only /sys/class/leds/<name>/brightness. The daemon runs as root, so validation refuses anything else, including .. and nested paths.
  • The multi-planar path refuses num_planes != 1. That covers GREY, Y16, YUYV and NV12 as CAMSS delivers them; true multi-plane formats stay out of scope.

Validation

  • cargo test --workspace
  • cargo clippy --workspace --all-targets -- -D warnings, with and without --features tpm
  • cargo fmt --all -- --check
  • just check-docs, just check-agent-docs, just check-pam-standalone
  • each commit builds and passes the camera and core tests on its own
  • enrollment, facelock test and lock-screen unlock on a Surface Pro 11 (/dev/video4, CAMSS, aarch64)

Media-controller capture nodes (Intel IPU6/IPU7 ISYS, Qualcomm CAMSS) only
stream the size the pipeline was configured for, usually by media-ctl, and
their GREY rows are padded past the image width (CAMSS delivers 644-pixel
rows with a 656-byte stride).

- [device] keep_format = true uses the node's current format instead of
  negotiating one; it must still be decodable.
- A GREY stride larger than the width is accepted, and each row is cut to
  the width before decoding. Every other stride mismatch is still rejected
  at open.
Some IR cameras behind a media-controller pipeline have no UVC extension
unit; their illuminator is a separate LED class device, such as the
Surface Pro 11's PMIC flash LED in torch mode (/sys/class/leds/ir:flash),
that nothing turns on while streaming. The new device.ir_led key names
that LED. Camera::open lights it at full brightness, and Camera's Drop
switches it off before anything else, so it goes out on every release
path, including the daemon's hold timer. The daemon writes the LED as
root, so validation accepts only /sys/class/leds/<name>.

Lists the key in docs/contracts.md.
Some SoC camera subsystems, such as Qualcomm CAMSS on Snapdragon X
laptops, expose capture nodes only through the multi-planar API, which
the v4l crate implements for neither formats nor streaming, so facelock
rejected them as "not a video capture device". A new mplane module
enumerates and gets or sets single-plane formats on such nodes and runs
an MMAP stream with one plane per buffer; capture and device discovery
take that path when a node offers only multi-planar capture. Formats
with more than one plane are refused, as facelock decodes none. A failed
dequeue does not re-queue a buffer the kernel still holds.
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment on lines +430 to +441
let current = if mplane_only {
mplane::get_format(&dev)
} else {
dev.format().map(|f| mplane::Format {
width: f.width,
height: f.height,
fourcc: f.fourcc,
stride: f.stride,
})
};
let mut fmt =
current.map_err(|e| FacelockError::Camera(format!("failed to get format: {e}")))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Mplane open fails on a multi-plane current format before set_format runs

current is fetched and ?-propagated on every path. On a multi-planar node, mplane::get_format goes through from_v4l2, which returns an error when num_planes != 1. Then, with keep_format = false, open fails with "failed to get format" before mplane::set_format gets a chance to request a single-plane format. That happens when the node's current format is multi-plane, for example NV12M left behind by libcamera or another app. In that branch the fetched value is never used. Only call mplane::get_format when keep_format is set.

Fetch the current format only where it is used:

let fmt = if config.keep_format {
    let fmt = if mplane_only { mplane::get_format(&dev) } else {
        dev.format().map(|f| mplane::Format { width: f.width, height: f.height, fourcc: f.fourcc, stride: f.stride })
    }.map_err(|e| FacelockError::Camera(format!("failed to get format: {e}")))?;
    // decodable check as before
    fmt
} else if mplane_only {
    mplane::set_format(&dev, selected_fourcc, max_w, max_h)
        .map_err(|e| FacelockError::Camera(format!("failed to set format: {e}")))?
} else { /* single-planar negotiation, building mplane::Format from `set` */ };
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 closed / 1 findings

🟡 Medium risk · Adding multi-planar capture and full-brightness sysfs IR control changes camera operation and could activate an unintended illuminator during capture.

Adds support for media-controller IR cameras (IPU6/IPU7, Qualcomm CAMSS) with multi-planar V4L2 nodes, configurable format preservation, row padding tolerance, and LED illuminator control. Consider calling mplane::get_format only when keep_format is set to avoid failing on multi-plane current formats before set_format runs.

💡 Edge Case: Mplane open fails on a multi-plane current format before set_format runs

📄 crates/facelock-camera/src/capture.rs:430-441 📄 crates/facelock-camera/src/capture.rs:452-454 📄 crates/facelock-camera/src/mplane.rs:119-130

current is fetched and ?-propagated on every path. On a multi-planar node, mplane::get_format goes through from_v4l2, which returns an error when num_planes != 1. Then, with keep_format = false, open fails with "failed to get format" before mplane::set_format gets a chance to request a single-plane format. That happens when the node's current format is multi-plane, for example NV12M left behind by libcamera or another app. In that branch the fetched value is never used. Only call mplane::get_format when keep_format is set.

Fetch the current format only where it is used
let fmt = if config.keep_format {
    let fmt = if mplane_only { mplane::get_format(&dev) } else {
        dev.format().map(|f| mplane::Format { width: f.width, height: f.height, fourcc: f.fourcc, stride: f.stride })
    }.map_err(|e| FacelockError::Camera(format!("failed to get format: {e}")))?;
    // decodable check as before
    fmt
} else if mplane_only {
    mplane::set_format(&dev, selected_fourcc, max_w, max_h)
        .map_err(|e| FacelockError::Camera(format!("failed to set format: {e}")))?
} else { /* single-planar negotiation, building mplane::Format from `set` */ };
🤖 Prompt for agents
Code Review: Adds support for media-controller IR cameras (IPU6/IPU7, Qualcomm CAMSS) with multi-planar V4L2 nodes, configurable format preservation, row padding tolerance, and LED illuminator control. Consider calling `mplane::get_format` only when `keep_format` is set to avoid failing on multi-plane current formats before `set_format` runs.

1. 💡 Edge Case: Mplane open fails on a multi-plane current format before set_format runs
   Files: crates/facelock-camera/src/capture.rs:430-441, crates/facelock-camera/src/capture.rs:452-454, crates/facelock-camera/src/mplane.rs:119-130

   `current` is fetched and `?`-propagated on every path. On a multi-planar node, `mplane::get_format` goes through `from_v4l2`, which returns an error when `num_planes != 1`. Then, with `keep_format = false`, open fails with "failed to get format" before `mplane::set_format` gets a chance to request a single-plane format. That happens when the node's current format is multi-plane, for example NV12M left behind by libcamera or another app. In that branch the fetched value is never used. Only call `mplane::get_format` when `keep_format` is set.

   Fix (Fetch the current format only where it is used):
   let fmt = if config.keep_format {
       let fmt = if mplane_only { mplane::get_format(&dev) } else {
           dev.format().map(|f| mplane::Format { width: f.width, height: f.height, fourcc: f.fourcc, stride: f.stride })
       }.map_err(|e| FacelockError::Camera(format!("failed to get format: {e}")))?;
       // decodable check as before
       fmt
   } else if mplane_only {
       mplane::set_format(&dev, selected_fourcc, max_w, max_h)
           .map_err(|e| FacelockError::Camera(format!("failed to set format: {e}")))?
   } else { /* single-planar negotiation, building mplane::Format from `set` */ };

Review coverage

📋 Rules No rules evaluated

🧪 Functional validation Not enabled · Set up

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant