git: d0450cbec7e6 - main - uvideo: add Camera Terminal controls

From: Baptiste Daroussin <bapt_at_FreeBSD.org>
Date: Tue, 16 Jun 2026 11:23:04 UTC
The branch main has been updated by bapt:

URL: https://cgit.FreeBSD.org/src/commit/?id=d0450cbec7e66ee56fd48c6482fd3bd9fb6549dc

commit d0450cbec7e66ee56fd48c6482fd3bd9fb6549dc
Author:     Baptiste Daroussin <bapt@FreeBSD.org>
AuthorDate: 2026-05-06 16:28:27 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
CommitDate: 2026-06-16 11:21:01 +0000

    uvideo: add Camera Terminal controls
    
    Implement UVC Camera Terminal (CT) controls per UVC 1.5 specification
    Table A-12. This adds support for camera-specific controls that are
    separate from the Processing Unit controls already supported.
    
    Reviewed by:    manu
    Differential Revision:  https://reviews.freebsd.org/D56962
---
 sys/dev/usb/video/uvideo.c      | 210 +++++++++++++++++++++++++++++++++++++---
 sys/dev/usb/video/uvideo.h      |   2 +-
 sys/dev/usb/video/uvideo_v4l2.h |  13 +++
 3 files changed, 211 insertions(+), 14 deletions(-)

diff --git a/sys/dev/usb/video/uvideo.c b/sys/dev/usb/video/uvideo.c
index a33b3ac31ea6..52998fbc9b40 100644
--- a/sys/dev/usb/video/uvideo.c
+++ b/sys/dev/usb/video/uvideo.c
@@ -95,6 +95,10 @@ static usb_error_t	uvideo_vc_parse_desc_header(struct uvideo_softc *,
 			    const struct usb_descriptor *);
 static usb_error_t	uvideo_vc_parse_desc_pu(struct uvideo_softc *,
 			    const struct usb_descriptor *);
+static usb_error_t	uvideo_vc_parse_desc_ct(struct uvideo_softc *,
+			    const struct usb_descriptor *);
+static int		uvideo_has_ct_ctrl(
+			    struct usb_video_camera_terminal_desc *, int);
 static usb_error_t	uvideo_vc_get_ctrl(struct uvideo_softc *, uint8_t *,
 			    uint8_t, uint8_t, uint16_t, uint16_t);
 static usb_error_t	uvideo_vc_set_ctrl(struct uvideo_softc *, uint8_t *,
@@ -245,6 +249,11 @@ struct uvideo_softc {
 	struct usb_video_vc_processing_desc *sc_desc_vc_pu_cur;
 	struct usb_video_vc_processing_desc *sc_desc_vc_pu[UVIDEO_MAX_PU];
 
+#define	UVIDEO_MAX_CT		8
+	int			sc_desc_vc_ct_num;
+	struct usb_video_camera_terminal_desc *sc_desc_vc_ct_cur;
+	struct usb_video_camera_terminal_desc *sc_desc_vc_ct[UVIDEO_MAX_CT];
+
 #define	UVIDEO_MAX_FORMAT	8
 	int			sc_fmtgrp_idx;
 	int			sc_fmtgrp_num;
@@ -391,6 +400,88 @@ static struct uvideo_controls uvideo_ctrls[] = {
 	    1,
 	    0
 	},
+	/* Camera Terminal Controls (UVC 1.5 spec Table A-12) */
+	{
+	    V4L2_CID_EXPOSURE_AUTO,
+	    V4L2_CTRL_TYPE_MENU,
+	    "Exposure, Auto",
+	    1,
+	    CT_AE_MODE_CONTROL,
+	    1,
+	    0
+	},
+	{
+	    V4L2_CID_EXPOSURE_AUTO_PRIORITY,
+	    V4L2_CTRL_TYPE_BOOLEAN,
+	    "Exposure, Auto Priority",
+	    2,
+	    CT_AE_PRIORITY_CONTROL,
+	    1,
+	    0
+	},
+	{
+	    V4L2_CID_EXPOSURE_ABSOLUTE,
+	    V4L2_CTRL_TYPE_INTEGER,
+	    "Exposure (Absolute)",
+	    3,
+	    CT_EXPOSURE_TIME_ABSOLUTE_CONTROL,
+	    4,
+	    0
+	},
+	{
+	    V4L2_CID_FOCUS_ABSOLUTE,
+	    V4L2_CTRL_TYPE_INTEGER,
+	    "Focus (Absolute)",
+	    5,
+	    CT_FOCUS_ABSOLUTE_CONTROL,
+	    2,
+	    0
+	},
+	{
+	    V4L2_CID_FOCUS_AUTO,
+	    V4L2_CTRL_TYPE_BOOLEAN,
+	    "Focus, Auto",
+	    17,
+	    CT_FOCUS_AUTO_CONTROL,
+	    1,
+	    0
+	},
+	{
+	    V4L2_CID_ZOOM_ABSOLUTE,
+	    V4L2_CTRL_TYPE_INTEGER,
+	    "Zoom (Absolute)",
+	    9,
+	    CT_ZOOM_ABSOLUTE_CONTROL,
+	    2,
+	    0
+	},
+	{
+	    V4L2_CID_PAN_ABSOLUTE,
+	    V4L2_CTRL_TYPE_INTEGER,
+	    "Pan (Absolute)",
+	    11,
+	    CT_PANTILT_ABSOLUTE_CONTROL,
+	    4,
+	    1
+	},
+	{
+	    V4L2_CID_TILT_ABSOLUTE,
+	    V4L2_CTRL_TYPE_INTEGER,
+	    "Tilt (Absolute)",
+	    11,
+	    CT_PANTILT_ABSOLUTE_CONTROL,
+	    4,
+	    1
+	},
+	{
+	    V4L2_CID_PRIVACY,
+	    V4L2_CTRL_TYPE_BOOLEAN,
+	    "Privacy",
+	    18,
+	    CT_PRIVACY_CONTROL,
+	    1,
+	    0
+	},
 	{ 0, 0, "", 0, 0, 0, 0 }
 };
 
@@ -824,6 +915,14 @@ uvideo_vc_parse_desc(struct uvideo_softc *sc)
 				return (error);
 			vc_header_found = 1;
 			break;
+		case UDESCSUB_VC_INPUT_TERMINAL:
+		    {
+			struct usb_video_input_terminal_desc *itd;
+			itd = (struct usb_video_input_terminal_desc *)desc;
+			if (UGETW(itd->wTerminalType) == ITT_CAMERA)
+				(void)uvideo_vc_parse_desc_ct(sc, desc);
+			break;
+		    }
 		case UDESCSUB_VC_PROCESSING_UNIT:
 			(void)uvideo_vc_parse_desc_pu(sc, desc);
 			break;
@@ -883,6 +982,25 @@ uvideo_vc_parse_desc_pu(struct uvideo_softc *sc,
 	return (USB_ERR_NORMAL_COMPLETION);
 }
 
+static usb_error_t
+uvideo_vc_parse_desc_ct(struct uvideo_softc *sc,
+    const struct usb_descriptor *desc)
+{
+	struct usb_video_camera_terminal_desc *d;
+
+	d = __DECONST(struct usb_video_camera_terminal_desc *, desc);
+
+	if (sc->sc_desc_vc_ct_num == UVIDEO_MAX_CT) {
+		device_printf(sc->sc_dev, "too many CT descriptors\n");
+		return (USB_ERR_INVAL);
+	}
+
+	sc->sc_desc_vc_ct[sc->sc_desc_vc_ct_num] = d;
+	sc->sc_desc_vc_ct_num++;
+
+	return (USB_ERR_NORMAL_COMPLETION);
+}
+
 static usb_error_t
 uvideo_vc_get_ctrl(struct uvideo_softc *sc, uint8_t *ctrl_data,
     uint8_t request, uint8_t unitid, uint16_t ctrl_selector, uint16_t ctrl_len)
@@ -934,8 +1052,8 @@ uvideo_find_ctrl(struct uvideo_softc *sc, int id)
 {
 	int i, j, found;
 
-	if (sc->sc_desc_vc_pu_num == 0) {
-		DPRINTFN(1, "no processing unit descriptors found!\n");
+	if (sc->sc_desc_vc_pu_num == 0 && sc->sc_desc_vc_ct_num == 0) {
+		DPRINTFN(1, "no PU or CT descriptors found!\n");
 		return (EINVAL);
 	}
 
@@ -951,19 +1069,34 @@ uvideo_find_ctrl(struct uvideo_softc *sc, int id)
 		return (EINVAL);
 	}
 
-	/* does the device support this control? */
+	/* does a PU support this control? */
+	sc->sc_desc_vc_pu_cur = NULL;
+	sc->sc_desc_vc_ct_cur = NULL;
 	for (found = 0, j = 0; j < sc->sc_desc_vc_pu_num; j++) {
 		if (uvideo_has_ctrl(sc->sc_desc_vc_pu[j],
 		    uvideo_ctrls[i].ctrl_bit) != 0) {
 			found = 1;
+			sc->sc_desc_vc_pu_cur = sc->sc_desc_vc_pu[j];
 			break;
 		}
 	}
+
+	/* does a CT support this control? */
+	if (found == 0) {
+		for (j = 0; j < sc->sc_desc_vc_ct_num; j++) {
+			if (uvideo_has_ct_ctrl(sc->sc_desc_vc_ct[j],
+			    uvideo_ctrls[i].ctrl_bit) != 0) {
+				found = 1;
+				sc->sc_desc_vc_ct_cur = sc->sc_desc_vc_ct[j];
+				break;
+			}
+		}
+	}
+
 	if (found == 0) {
 		DPRINTFN(1, "control not supported by device!\n");
 		return (EINVAL);
 	}
-	sc->sc_desc_vc_pu_cur = sc->sc_desc_vc_pu[j];
 
 	return (i);
 }
@@ -978,6 +1111,16 @@ uvideo_has_ctrl(struct usb_video_vc_processing_desc *desc, int ctrl_bit)
 	return (desc->bmControls[byteof(ctrl_bit)] & bitof(ctrl_bit));
 }
 
+static int
+uvideo_has_ct_ctrl(struct usb_video_camera_terminal_desc *desc, int ctrl_bit)
+{
+
+	if (desc->bControlSize * 8 <= ctrl_bit)
+		return (0);
+
+	return (desc->bmControls[byteof(ctrl_bit)] & bitof(ctrl_bit));
+}
+
 static usb_error_t
 uvideo_vs_parse_desc(struct uvideo_softc *sc,
     struct usb_config_descriptor *cdesc)
@@ -3353,13 +3496,19 @@ uvideo_queryctrl(struct uvideo_softc *sc, struct v4l2_queryctrl *qctrl)
 	usb_error_t error;
 	uint8_t *ctrl_data;
 	uint16_t ctrl_len;
+	uint8_t unit_id;
 
 	i = uvideo_find_ctrl(sc, qctrl->id);
 	if (i == EINVAL)
 		return (i);
 
+	if (sc->sc_desc_vc_ct_cur != NULL)
+		unit_id = sc->sc_desc_vc_ct_cur->bTerminalID;
+	else
+		unit_id = sc->sc_desc_vc_pu_cur->bUnitID;
+
 	ctrl_len = uvideo_ctrls[i].ctrl_len;
-	if (ctrl_len < 1 || ctrl_len > 2) {
+	if (ctrl_len < 1 || ctrl_len > 4) {
 		device_printf(sc->sc_dev,
 		    "invalid control length: %d\n", ctrl_len);
 		return (EINVAL);
@@ -3374,7 +3523,7 @@ uvideo_queryctrl(struct uvideo_softc *sc, struct v4l2_queryctrl *qctrl)
 
 	/* get minimum */
 	error = uvideo_vc_get_ctrl(sc, ctrl_data, GET_MIN,
-	    sc->sc_desc_vc_pu_cur->bUnitID,
+	    unit_id,
 	    uvideo_ctrls[i].ctrl_selector, uvideo_ctrls[i].ctrl_len);
 	if (error != USB_ERR_NORMAL_COMPLETION) {
 		ret = EINVAL;
@@ -3389,11 +3538,15 @@ uvideo_queryctrl(struct uvideo_softc *sc, struct v4l2_queryctrl *qctrl)
 		qctrl->minimum = uvideo_ctrls[i].sig ?
 		    (int16_t)UGETW(ctrl_data) : UGETW(ctrl_data);
 		break;
+	case 4:
+		qctrl->minimum = uvideo_ctrls[i].sig ?
+		    (int32_t)UGETDW(ctrl_data) : UGETDW(ctrl_data);
+		break;
 	}
 
 	/* get maximum */
 	error = uvideo_vc_get_ctrl(sc, ctrl_data, GET_MAX,
-	    sc->sc_desc_vc_pu_cur->bUnitID,
+	    unit_id,
 	    uvideo_ctrls[i].ctrl_selector, uvideo_ctrls[i].ctrl_len);
 	if (error != USB_ERR_NORMAL_COMPLETION) {
 		ret = EINVAL;
@@ -3408,11 +3561,15 @@ uvideo_queryctrl(struct uvideo_softc *sc, struct v4l2_queryctrl *qctrl)
 		qctrl->maximum = uvideo_ctrls[i].sig ?
 		    (int16_t)UGETW(ctrl_data) : UGETW(ctrl_data);
 		break;
+	case 4:
+		qctrl->maximum = uvideo_ctrls[i].sig ?
+		    (int32_t)UGETDW(ctrl_data) : UGETDW(ctrl_data);
+		break;
 	}
 
 	/* get resolution/step */
 	error = uvideo_vc_get_ctrl(sc, ctrl_data, GET_RES,
-	    sc->sc_desc_vc_pu_cur->bUnitID,
+	    unit_id,
 	    uvideo_ctrls[i].ctrl_selector, uvideo_ctrls[i].ctrl_len);
 	if (error != USB_ERR_NORMAL_COMPLETION) {
 		ret = EINVAL;
@@ -3427,11 +3584,15 @@ uvideo_queryctrl(struct uvideo_softc *sc, struct v4l2_queryctrl *qctrl)
 		qctrl->step = uvideo_ctrls[i].sig ?
 		    (int16_t)UGETW(ctrl_data) : UGETW(ctrl_data);
 		break;
+	case 4:
+		qctrl->step = uvideo_ctrls[i].sig ?
+		    (int32_t)UGETDW(ctrl_data) : UGETDW(ctrl_data);
+		break;
 	}
 
 	/* get default */
 	error = uvideo_vc_get_ctrl(sc, ctrl_data, GET_DEF,
-	    sc->sc_desc_vc_pu_cur->bUnitID,
+	    unit_id,
 	    uvideo_ctrls[i].ctrl_selector, uvideo_ctrls[i].ctrl_len);
 	if (error != USB_ERR_NORMAL_COMPLETION) {
 		ret = EINVAL;
@@ -3446,6 +3607,10 @@ uvideo_queryctrl(struct uvideo_softc *sc, struct v4l2_queryctrl *qctrl)
 		qctrl->default_value = uvideo_ctrls[i].sig ?
 		    (int16_t)UGETW(ctrl_data) : UGETW(ctrl_data);
 		break;
+	case 4:
+		qctrl->default_value = uvideo_ctrls[i].sig ?
+		    (int32_t)UGETDW(ctrl_data) : UGETDW(ctrl_data);
+		break;
 	}
 
 	qctrl->flags = 0;
@@ -3462,13 +3627,19 @@ uvideo_g_ctrl(struct uvideo_softc *sc, struct v4l2_control *gctrl)
 	usb_error_t error;
 	uint8_t *ctrl_data;
 	uint16_t ctrl_len;
+	uint8_t unit_id;
 
 	i = uvideo_find_ctrl(sc, gctrl->id);
 	if (i == EINVAL)
 		return (i);
 
+	if (sc->sc_desc_vc_ct_cur != NULL)
+		unit_id = sc->sc_desc_vc_ct_cur->bTerminalID;
+	else
+		unit_id = sc->sc_desc_vc_pu_cur->bUnitID;
+
 	ctrl_len = uvideo_ctrls[i].ctrl_len;
-	if (ctrl_len < 1 || ctrl_len > 2)
+	if (ctrl_len < 1 || ctrl_len > 4)
 		return (EINVAL);
 
 	ctrl_data = malloc(ctrl_len, M_USBDEV, M_WAITOK | M_ZERO);
@@ -3476,7 +3647,7 @@ uvideo_g_ctrl(struct uvideo_softc *sc, struct v4l2_control *gctrl)
 		return (ENOMEM);
 
 	error = uvideo_vc_get_ctrl(sc, ctrl_data, GET_CUR,
-	    sc->sc_desc_vc_pu_cur->bUnitID,
+	    unit_id,
 	    uvideo_ctrls[i].ctrl_selector, uvideo_ctrls[i].ctrl_len);
 	if (error != USB_ERR_NORMAL_COMPLETION) {
 		ret = EINVAL;
@@ -3491,6 +3662,10 @@ uvideo_g_ctrl(struct uvideo_softc *sc, struct v4l2_control *gctrl)
 		gctrl->value = uvideo_ctrls[i].sig ?
 		    (int16_t)UGETW(ctrl_data) : UGETW(ctrl_data);
 		break;
+	case 4:
+		gctrl->value = uvideo_ctrls[i].sig ?
+		    (int32_t)UGETDW(ctrl_data) : UGETDW(ctrl_data);
+		break;
 	}
 
 out:
@@ -3505,13 +3680,19 @@ uvideo_s_ctrl(struct uvideo_softc *sc, struct v4l2_control *sctrl)
 	usb_error_t error;
 	uint8_t *ctrl_data;
 	uint16_t ctrl_len;
+	uint8_t unit_id;
 
 	i = uvideo_find_ctrl(sc, sctrl->id);
 	if (i == EINVAL)
 		return (i);
 
+	if (sc->sc_desc_vc_ct_cur != NULL)
+		unit_id = sc->sc_desc_vc_ct_cur->bTerminalID;
+	else
+		unit_id = sc->sc_desc_vc_pu_cur->bUnitID;
+
 	ctrl_len = uvideo_ctrls[i].ctrl_len;
-	if (ctrl_len < 1 || ctrl_len > 2)
+	if (ctrl_len < 1 || ctrl_len > 4)
 		return (EINVAL);
 
 	ctrl_data = malloc(ctrl_len, M_USBDEV, M_WAITOK | M_ZERO);
@@ -3528,10 +3709,13 @@ uvideo_s_ctrl(struct uvideo_softc *sc, struct v4l2_control *sctrl)
 	case 2:
 		USETW(ctrl_data, sctrl->value);
 		break;
+	case 4:
+		USETDW(ctrl_data, sctrl->value);
+		break;
 	}
 
 	error = uvideo_vc_set_ctrl(sc, ctrl_data, SET_CUR,
-	    sc->sc_desc_vc_pu_cur->bUnitID,
+	    unit_id,
 	    uvideo_ctrls[i].ctrl_selector, uvideo_ctrls[i].ctrl_len);
 	if (error != USB_ERR_NORMAL_COMPLETION)
 		ret = EINVAL;
diff --git a/sys/dev/usb/video/uvideo.h b/sys/dev/usb/video/uvideo.h
index bccaf4cf0216..00e5118a65ae 100644
--- a/sys/dev/usb/video/uvideo.h
+++ b/sys/dev/usb/video/uvideo.h
@@ -211,7 +211,7 @@ struct usb_video_camera_terminal_desc {
 	uWord	wObjectiveFocalLengthMax;
 	uWord	wOcularFocalLength;
 	uByte	bControlSize;
-	uByte	*bmControls;
+	uByte	bmControls[255]; /* [bControlSize] */
 } __packed;
 
 /* Table 3-8: VC Processing Unit Descriptor */
diff --git a/sys/dev/usb/video/uvideo_v4l2.h b/sys/dev/usb/video/uvideo_v4l2.h
index 05736bff3e9c..6192ada9fbb6 100644
--- a/sys/dev/usb/video/uvideo_v4l2.h
+++ b/sys/dev/usb/video/uvideo_v4l2.h
@@ -453,6 +453,19 @@ struct v4l2_frmivalenum {
 #define V4L2_CID_SHARPNESS		(V4L2_CID_BASE + 27)
 #define V4L2_CID_BACKLIGHT_COMPENSATION	(V4L2_CID_BASE + 28)
 
+#define V4L2_CID_CAMERA_CLASS_BASE	0x009a0000
+#define V4L2_CID_EXPOSURE_AUTO		(V4L2_CID_CAMERA_CLASS_BASE + 1)
+#define V4L2_CID_EXPOSURE_ABSOLUTE	(V4L2_CID_CAMERA_CLASS_BASE + 2)
+#define V4L2_CID_EXPOSURE_AUTO_PRIORITY	(V4L2_CID_CAMERA_CLASS_BASE + 3)
+#define V4L2_CID_FOCUS_ABSOLUTE		(V4L2_CID_CAMERA_CLASS_BASE + 4)
+#define V4L2_CID_FOCUS_RELATIVE		(V4L2_CID_CAMERA_CLASS_BASE + 5)
+#define V4L2_CID_PAN_ABSOLUTE		(V4L2_CID_CAMERA_CLASS_BASE + 8)
+#define V4L2_CID_TILT_ABSOLUTE		(V4L2_CID_CAMERA_CLASS_BASE + 9)
+#define V4L2_CID_FOCUS_AUTO		(V4L2_CID_CAMERA_CLASS_BASE + 12)
+#define V4L2_CID_ZOOM_ABSOLUTE		(V4L2_CID_CAMERA_CLASS_BASE + 13)
+#define V4L2_CID_ZOOM_CONTINUOUS	(V4L2_CID_CAMERA_CLASS_BASE + 15)
+#define V4L2_CID_PRIVACY		(V4L2_CID_CAMERA_CLASS_BASE + 16)
+
 /*
  *  V4L2 ioctl definitions
  */