Show a patch.

GET /api/patches/27505/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 27505,
    "url": "https://patchwork.libcamera.org/api/patches/27505/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/27505/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20260724174705.9411-2-magdum.foss@gmail.com>",
    "date": "2026-07-24T17:47:01",
    "name": "[RFC,1/1] pipeline: Add NVIDIA Tegra VI pipeline handler",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "3c8e8d70f39a1610f42eeb38034f6ea3e172a60b",
    "submitter": {
        "id": 453,
        "url": "https://patchwork.libcamera.org/api/people/453/?format=api",
        "name": "Magdum",
        "email": "magdum.foss@gmail.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/27505/mbox/",
    "series": [
        {
            "id": 6080,
            "url": "https://patchwork.libcamera.org/api/series/6080/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=6080",
            "date": "2026-07-24T17:47:00",
            "name": "Add NVIDIA Tegra VI pipeline handler",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/6080/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/27505/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/27505/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 3BC13BDE17\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 24 Jul 2026 17:47:16 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id DE63267F45;\n\tFri, 24 Jul 2026 19:47:15 +0200 (CEST)",
            "from mail-wm1-x333.google.com (mail-wm1-x333.google.com\n\t[IPv6:2a00:1450:4864:20::333])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 8E10367F22\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 24 Jul 2026 19:47:14 +0200 (CEST)",
            "by mail-wm1-x333.google.com with SMTP id\n\t5b1f17b1804b1-493b966dd74so4912805e9.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 24 Jul 2026 10:47:14 -0700 (PDT)",
            "from magdum-System-Product-Name.vodafone.ultrahub\n\t([2a02:810d:4b14:4600:38a1:bd9f:46a9:a191])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-496b4eecb4fsm814055e9.7.2026.07.24.10.47.12\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tFri, 24 Jul 2026 10:47:13 -0700 (PDT)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=gmail.com header.i=@gmail.com\n\theader.b=\"DyIqGBSJ\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=gmail.com; s=20251104; t=1784915234; x=1785520034;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:content-type:mime-version:references\n\t:in-reply-to:message-id:date:subject:cc:to:from:from:to:cc:subject\n\t:date:message-id:reply-to:content-type;\n\tbh=Xyy6YStiCp2ys/KPl2GYevY7G9eg3t/kOOXAn7cyJk0=;\n\tb=DyIqGBSJRQBeiFQ672bzh1NPr0Uf647jIseQlpLCWxc798Qe5ramhK9ZzNt/fdGBwU\n\thwwNfQocUyKFrhw32FuEJo2X94V9/sNmY5nifGosMUDglVy17WAytEeR1apzhHOvGBmQ\n\timIt7mpmMT4HvuOQ1Xy5PAwjLzpe3jNdklDt7EMHlXEPaBaeBKcKi3PKryf2DfoCSBvu\n\tgi3iLjhQC+nN0TUAn1s7MUboiss8CUaIa+7WN0jAmJZyUz05vGnF3mM3QqYNvyi06C87\n\tSbMueSochjDrLBOvBgUvrVhMuy1nwH/enxwUMi/swFt0x5C+ZqIzaTchDnhoeR7Wxf2V\n\t4R5w==",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20251104; t=1784915234; x=1785520034;\n\th=content-transfer-encoding:content-type:mime-version:references\n\t:in-reply-to:message-id:date:subject:cc:to:from:x-gm-gg\n\t:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to\n\t:content-type;\n\tbh=Xyy6YStiCp2ys/KPl2GYevY7G9eg3t/kOOXAn7cyJk0=;\n\tb=SQhla+I/TY6uWaPTH1RPv15dtNTq+iWd+ibEuPm/HFfha3kmUC/N/so3WKytGHPo4x\n\t3rbSN+Q0iLNZV3H9tMASfuBB5OPjXU0PWFk/TRfybPbkeeuMrL2FnaQyfw6aTxE6YPcA\n\tB4J1Y7+mUR++X8kft0aCDYDl2zbffb5TyTlLHPlog1WqVc4oCMlixPo0ZmMf2hMe7MHJ\n\tukKiLBgr5x3a3yIpUrivkz4XPql50qWOcYCQb1YQsTsGotEgHXU1K2ELDhwAtoEDnBJz\n\tY99xoC3gzGUE58jxyamcus5vTTfwKHqErGLy7TTtVbejKXagMxLUQ+0vTBQtcGBKnQMt\n\tZrWA==",
        "X-Gm-Message-State": "AOJu0YxtnPnOKjHqHwEmluP4KwsLgVwhKqqJRTXAXqSl0poxqEEubznk\n\t/Qtk7Ugo42muOXO9BJBJ6l/kcV+jWAaEgUvqNXx5irWMyN1z239FuzJEl0JXX/vHS+k=",
        "X-Gm-Gg": "AR+sD11tPaebhK4yNAbwpJ3cMWhpR8fmciIPnNfKYmXGazkzXYSX8M2rb5dJj3y1V3S\n\tvU6Td3kmRck8XFa7IXkm3C5h/tZToX8C21gLWBz0tZK2MIp6nSuZN9sTK8RsiXTDN0QXEhedsjO\n\tldw4myzNAQtIf4rSZj7Jr0wzuMz64ezF86CHdK90etLXOzWlk8rNtEkqIuvdgT0BtOl+nFhhA+T\n\tJrM1hkiqf9u/TtMxo+zpgTPp6DpUE2+8j6JVc/xCYAfBs81JgKXKFbJmESXEHZ0a/+O7SX/C4PY\n\tXxme5V8fhZE4LUuwW0fv+ULbe/H6FXJ4p1KjqyBxayXQFpMVQ7n3jBeRlsV5WAsDWLbwp2cRk6b\n\tvyTKpDcB6xXDfytcsv2HW1J5zI7yYal1dFBV94H/zuM1zVDywIaYS/80q8YSJsYwAxfBRMQ2Kp/\n\taWImlf08R/UgTEWMYywk0GSHYyJjsYTA/TqgVal7E+VhwgVN1LNUZlFgGiXFCZZi4=",
        "X-Received": "by 2002:a05:600c:1394:b0:493:b6ee:fcb7 with SMTP id\n\t5b1f17b1804b1-49573cd41f4mr99506475e9.14.1784915233521; \n\tFri, 24 Jul 2026 10:47:13 -0700 (PDT)",
        "From": "Magdum <magdum.foss@gmail.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Magdum <magdum.foss@gmail.com>",
        "Subject": "[RFC PATCH 1/1] pipeline: Add NVIDIA Tegra VI pipeline handler",
        "Date": "Fri, 24 Jul 2026 19:47:01 +0200",
        "Message-ID": "<20260724174705.9411-2-magdum.foss@gmail.com>",
        "X-Mailer": "git-send-email 2.43.0",
        "In-Reply-To": "<20260724174705.9411-1-magdum.foss@gmail.com>",
        "References": "<20260724174705.9411-1-magdum.foss@gmail.com>",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=UTF-8",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "Introduce a new pipeline handler targeting upstream tegra-video\nV4L2/Media Controller drivers for Jetson platforms (Nano, TX2,\nXavier, Orin).\n\n- Route raw Bayer frames from the VI engine through SoftwareISP\n- Wire up automatic SoftISP dependencies inside meson.build\n- Handle multi-frame synchronization using DelayedControls\n- Add platform matching support for \"tegra-vi\" and \"tegra234-vi\"\n\nSigned-off-by: Magdum <magdum.foss@gmail.com>\n---\n include/libcamera/ipa/meson.build        |    1 +\n meson_options.txt                        |    1 +\n src/libcamera/pipeline/tegra/meson.build |    5 +\n src/libcamera/pipeline/tegra/tegra.cpp   | 1102 ++++++++++++++++++++++\n src/libcamera/software_isp/meson.build   |    2 +-\n 5 files changed, 1110 insertions(+), 1 deletion(-)\n create mode 100644 src/libcamera/pipeline/tegra/meson.build\n create mode 100644 src/libcamera/pipeline/tegra/tegra.cpp",
    "diff": "diff --git a/include/libcamera/ipa/meson.build b/include/libcamera/ipa/meson.build\nindex 3ee3ada30..ed0acd133 100644\n--- a/include/libcamera/ipa/meson.build\n+++ b/include/libcamera/ipa/meson.build\n@@ -69,6 +69,7 @@ pipeline_ipa_mojom_mapping = {\n     'rpi/pisp': 'raspberrypi.mojom',\n     'rpi/vc4': 'raspberrypi.mojom',\n     'simple': 'soft.mojom',\n+    'tegra': 'soft.mojom',\n     'vimc': 'vimc.mojom',\n }\n \ndiff --git a/meson_options.txt b/meson_options.txt\nindex 20baacc4f..8f7ac1dbf 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -85,6 +85,7 @@ option('pipelines',\n             'rpi/pisp',\n             'rpi/vc4',\n             'simple',\n+            'tegra',\n             'uvcvideo',\n             'vimc',\n             'virtual'\ndiff --git a/src/libcamera/pipeline/tegra/meson.build b/src/libcamera/pipeline/tegra/meson.build\nnew file mode 100644\nindex 000000000..c8cb0ff97\n--- /dev/null\n+++ b/src/libcamera/pipeline/tegra/meson.build\n@@ -0,0 +1,5 @@\n+# SPDX-License-Identifier: CC0-1.0\n+\n+libcamera_internal_sources += files([\n+    'tegra.cpp',\n+])\ndiff --git a/src/libcamera/pipeline/tegra/tegra.cpp b/src/libcamera/pipeline/tegra/tegra.cpp\nnew file mode 100644\nindex 000000000..6d84dd718\n--- /dev/null\n+++ b/src/libcamera/pipeline/tegra/tegra.cpp\n@@ -0,0 +1,1102 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2026, Magdum <magdum.foss@gmail.com>\n+ *\n+ * Pipeline handler for NVIDIA Tegra VI (Video Input)\n+ *\n+ * Supports Jetson platforms (Nano / TX2 / Xavier / Orin) using the\n+ * upstream tegra-video V4L2 / Media Controller driver.\n+ *\n+ * Pipeline topology\n+ * -----------------\n+ *\n+ *   [Camera Sensor (I2C)]\n+ *          |  (MIPI CSI-2)\n+ *   [tegra-csi  V4L2 subdev]    <-- sets Bayer bus format and lane count\n+ *          |\n+ *   [tegra-vi   V4L2 capture]   <-- DMA of raw Bayer frames into memory\n+ *          |\n+ *   [SoftwareIsp]               <-- debayer, AGC, AWB, CCM, gamma\n+ *          |\n+ *   [User FrameBuffer]\n+ *\n+ * The capture video device captures raw Bayer frames into a small ring of\n+ * internal buffers.  Each completed raw frame is handed to SoftwareIsp,\n+ * which debayers it into the user-supplied output buffer and then\n+ * completes the associated Request.\n+ *\n+ * Hardware ISP support (ISP7 on Tegra234) can be added in a follow-up\n+ * once the in-kernel tegra-isp driver is upstreamed.\n+ */\n+\n+#include <algorithm>\n+#include <map>\n+#include <memory>\n+#include <queue>\n+#include <set>\n+#include <string>\n+#include <unordered_map>\n+#include <vector>\n+\n+#include <libcamera/base/log.h>\n+\n+#include <libcamera/camera.h>\n+#include <libcamera/color_space.h>\n+#include <libcamera/control_ids.h>\n+#include <libcamera/formats.h>\n+#include <libcamera/geometry.h>\n+#include <libcamera/pixel_format.h>\n+#include <libcamera/stream.h>\n+\n+#include \"libcamera/internal/bayer_format.h\"\n+#include \"libcamera/internal/camera.h\"\n+#include \"libcamera/internal/camera_sensor.h\"\n+#include \"libcamera/internal/camera_sensor_properties.h\"\n+#include \"libcamera/internal/delayed_controls.h\"\n+#include \"libcamera/internal/device_enumerator.h\"\n+#include \"libcamera/internal/formats.h\"\n+#include \"libcamera/internal/media_device.h\"\n+#include \"libcamera/internal/pipeline_handler.h\"\n+#include \"libcamera/internal/request.h\"\n+#include \"libcamera/internal/software_isp/software_isp.h\"\n+#include \"libcamera/internal/v4l2_subdevice.h\"\n+#include \"libcamera/internal/v4l2_videodevice.h\"\n+\n+namespace libcamera {\n+\n+LOG_DEFINE_CATEGORY(TegraPipeline)\n+\n+/* Number of internal raw capture buffers in the VI ring. */\n+static constexpr unsigned int kNumRawBuffers = 4;\n+\n+/*\n+ * Select the smallest sensor size that covers the requested output size.\n+ * Falls back to the largest available size if none is large enough.\n+ */\n+static bool selectSensorSize(const std::vector<Size> &sensorSizes,\n+\t\t\t     const Size &requested, Size *sensorSize)\n+{\n+\tif (sensorSizes.empty())\n+\t\treturn false;\n+\n+\tfor (const Size &size : sensorSizes) {\n+\t\tif (size.width >= requested.width &&\n+\t\t    size.height >= requested.height) {\n+\t\t\t*sensorSize = size;\n+\t\t\treturn true;\n+\t\t}\n+\t}\n+\n+\t*sensorSize = sensorSizes.back();\n+\treturn true;\n+}\n+\n+/* -------------------------------------------------------------------------\n+ * Per-frame bookkeeping\n+ * -------------------------------------------------------------------------\n+ *\n+ * We need to know, for every in-flight frame sequence number:\n+ *   - which Request it belongs to\n+ *   - whether the SoftwareIsp will emit metadata (so we wait for it)\n+ *   - whether that metadata has already arrived\n+ */\n+struct TegraFrameInfo {\n+\tTegraFrameInfo(Request *r, bool m)\n+\t\t: request(r), metadataRequired(m),\n+\t\t  metadataProcessed(false)\n+\t{\n+\t}\n+\n+\tRequest *request;\n+\tbool metadataRequired;\n+\tbool metadataProcessed;\n+};\n+\n+/* -------------------------------------------------------------------------\n+ * Forward declarations\n+ * -------------------------------------------------------------------------*/\n+class PipelineHandlerTegra;\n+\n+/* -------------------------------------------------------------------------\n+ * TegraCameraData\n+ * -------------------------------------------------------------------------\n+ *\n+ * Per-camera state: owns the sensor, CSI subdev, VI video device and the\n+ * software ISP instance.\n+ */\n+class TegraCameraData : public Camera::Private\n+{\n+public:\n+\tTegraCameraData(PipelineHandlerTegra *pipe,\n+\t\t\tstd::shared_ptr<MediaDevice> media);\n+\n+\tint init();\n+\n+\t/* Callbacks */\n+\tvoid imageBufferReady(FrameBuffer *buffer);\n+\tvoid ispOutputBufferReady(FrameBuffer *buffer);\n+\tvoid clearIncompleteRequests();\n+\tvoid ispStatsReady(uint32_t frame, uint32_t bufferId);\n+\tvoid metadataReady(uint32_t frame, const ControlList &metadata);\n+\tvoid setSensorControls(const ControlList &sensorControls);\n+\n+\tPipelineHandlerTegra *pipe();\n+\n+\t/* Hardware resources */\n+\tstd::shared_ptr<MediaDevice> media_;\n+\tstd::unique_ptr<CameraSensor> sensor_;\n+\tstd::unique_ptr<V4L2Subdevice> csi_;\n+\tstd::unique_ptr<V4L2VideoDevice> video_;\n+\tconst MediaEntity *videoEntity_;\n+\n+\t/* Software ISP (debayer + IPA) */\n+\tstd::unique_ptr<SoftwareIsp> swIsp_;\n+\n+\t/* Delayed sensor controls (exposure / gain) */\n+\tstd::unique_ptr<DelayedControls> delayedCtrls_;\n+\n+\t/* The single output stream exposed to the application */\n+\tStream stream_;\n+\n+\t/*\n+\t * Ring of internal raw capture buffers.\n+\t * These are allocated once at start() and pre-queued to the VI node.\n+\t */\n+\tstd::vector<std::unique_ptr<FrameBuffer>> rawBuffers_;\n+\n+\t/*\n+\t * Queue of pending requests: each entry pairs a Request with the\n+\t * user-supplied output FrameBuffer.  The head entry is consumed when\n+\t * the next raw frame arrives from the VI device.\n+\t */\n+\tstruct PendingRequest {\n+\t\tRequest *request;\n+\t\tFrameBuffer *outputBuffer;\n+\t};\n+\tstd::queue<PendingRequest> pendingRequests_;\n+\n+\t/* Per-frame metadata tracking */\n+\tstd::map<uint32_t, TegraFrameInfo> frameInfos_;\n+\n+private:\n+\tvoid tryCompleteRequest(Request *request);\n+};\n+\n+/* -------------------------------------------------------------------------\n+ * TegraCameraConfiguration\n+ * -------------------------------------------------------------------------\n+ *\n+ * Validates and adjusts the stream configuration requested by the\n+ * application.  Only a single output stream is supported.\n+ */\n+class TegraCameraConfiguration : public CameraConfiguration\n+{\n+public:\n+\tTegraCameraConfiguration(Camera *camera, TegraCameraData *data);\n+\n+\tStatus validate() override;\n+\n+\tstatic constexpr unsigned int kNumBuffersDefault = 4;\n+\tstatic constexpr unsigned int kNumBuffersMax = 32;\n+\n+private:\n+\tstd::shared_ptr<Camera> camera_;\n+\tTegraCameraData *data_;\n+};\n+\n+/* -------------------------------------------------------------------------\n+ * PipelineHandlerTegra\n+ * -------------------------------------------------------------------------*/\n+class PipelineHandlerTegra : public PipelineHandler\n+{\n+public:\n+\texplicit PipelineHandlerTegra(CameraManager *manager);\n+\n+\tstd::unique_ptr<CameraConfiguration>\n+\tgenerateConfiguration(Camera *camera,\n+\t\t\t      Span<const StreamRole> roles) override;\n+\n+\tint configure(Camera *camera, CameraConfiguration *config) override;\n+\n+\tint exportFrameBuffers(Camera *camera, Stream *stream,\n+\t\t\t       std::vector<std::unique_ptr<FrameBuffer>> *buffers) override;\n+\n+\tint start(Camera *camera, const ControlList *controls) override;\n+\tvoid stopDevice(Camera *camera) override;\n+\n+\tbool match(DeviceEnumerator *enumerator) override;\n+\n+protected:\n+\tint queueRequestDevice(Camera *camera, Request *request) override;\n+\n+private:\n+\tTegraCameraData *cameraData(Camera *camera)\n+\t{\n+\t\treturn static_cast<TegraCameraData *>(camera->_d());\n+\t}\n+\n+\t/*\n+\t * Traverse the media graph starting from a camera sensor entity and\n+\t * return the first CSI bridge and VI capture node found on the path.\n+\t * Returns true on success.\n+\t */\n+\tstatic bool findPipelineEntities(MediaEntity *sensorEntity,\n+\t\t\t\t\t MediaEntity **csiEntity,\n+\t\t\t\t\t MediaEntity **viEntity);\n+\n+\tbool createCamera(std::shared_ptr<MediaDevice> media,\n+\t\t\t  MediaEntity *sensorEntity);\n+};\n+\n+/* =========================================================================\n+ * TegraCameraData implementation\n+ * =========================================================================*/\n+\n+TegraCameraData::TegraCameraData(PipelineHandlerTegra *pipe,\n+\t\t\t\t std::shared_ptr<MediaDevice> media)\n+\t: Camera::Private(pipe), media_(media), videoEntity_(nullptr)\n+{\n+}\n+\n+PipelineHandlerTegra *TegraCameraData::pipe()\n+{\n+\treturn static_cast<PipelineHandlerTegra *>(Camera::Private::pipe());\n+}\n+\n+/*\n+ * Open all V4L2 devices, create the CameraSensor and SoftwareIsp instances,\n+ * and set up the DelayedControls helper.\n+ */\n+int TegraCameraData::init()\n+{\n+\tint ret;\n+\n+\t/* Open the CSI bridge subdev. */\n+\tret = csi_->open();\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to open CSI subdev: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\t/* Open the VI video device. */\n+\tret = video_->open();\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to open VI video device: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\t/* Build the DelayedControls helper for sensor exposure / gain. */\n+\tconst CameraSensorProperties::SensorDelays &delays =\n+\t\tsensor_->sensorDelays();\n+\n+\tstd::unordered_map<uint32_t, DelayedControls::ControlParams> params = {\n+\t\t{ V4L2_CID_ANALOGUE_GAIN,\n+\t\t  { delays.gainDelay, false } },\n+\t\t{ V4L2_CID_EXPOSURE,\n+\t\t  { delays.exposureDelay, false } },\n+\t};\n+\tdelayedCtrls_ =\n+\t\tstd::make_unique<DelayedControls>(sensor_->device(), params);\n+\n+\t/* Create the Software ISP (provides debayering + AGC/AWB via IPA). */\n+\tswIsp_ = std::make_unique<SoftwareIsp>(pipe(), sensor_.get(),\n+\t\t\t\t\t       &controlInfo_);\n+\tif (!swIsp_->isValid()) {\n+\t\tLOG(TegraPipeline, Error) << \"Failed to create software ISP\";\n+\t\tswIsp_.reset();\n+\t\treturn -ENODEV;\n+\t}\n+\n+\t/* Wire software ISP signals. */\n+\tswIsp_->outputBufferReady.connect(this,\n+\t\t\t\t\t  &TegraCameraData::ispOutputBufferReady);\n+\tswIsp_->ispStatsReady.connect(this, &TegraCameraData::ispStatsReady);\n+\tswIsp_->metadataReady.connect(this, &TegraCameraData::metadataReady);\n+\tswIsp_->setSensorControls.connect(this,\n+\t\t\t\t\t  &TegraCameraData::setSensorControls);\n+\n+\treturn 0;\n+}\n+\n+/* Called by the VI video device when a raw frame DMA is complete. */\n+void TegraCameraData::imageBufferReady(FrameBuffer *buffer)\n+{\n+\tPipelineHandlerTegra *pipe = TegraCameraData::pipe();\n+\n+\t/* On error or cancellation just complete the request immediately. */\n+\tif (buffer->metadata().status != FrameMetadata::FrameSuccess) {\n+\t\tif (pendingRequests_.empty()) {\n+\t\t\t/* Stale buffer with no associated request – requeue. */\n+\t\t\tvideo_->queueBuffer(buffer);\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tPendingRequest &pending = pendingRequests_.front();\n+\t\tRequest *request = pending.request;\n+\n+\t\tauto it = frameInfos_.find(request->sequence());\n+\t\tif (it != frameInfos_.end())\n+\t\t\tit->second.metadataRequired = false;\n+\n+\t\tif (pipe->completeBuffer(request, pending.outputBuffer))\n+\t\t\ttryCompleteRequest(request);\n+\t\tpendingRequests_.pop();\n+\n+\t\t/*\n+\t\t * If the buffer was cancelled we stop; otherwise it is an\n+\t\t * error frame so requeue the internal buffer for the next\n+\t\t * capture.\n+\t\t */\n+\t\tif (buffer->metadata().status != FrameMetadata::FrameCancelled)\n+\t\t\tvideo_->queueBuffer(buffer);\n+\n+\t\treturn;\n+\t}\n+\n+\t/* Stamp the sensor timestamp into request metadata via the correct sequence lookup. */\n+\tif (!pendingRequests_.empty()) {\n+\t\tRequest *request = pendingRequests_.front().request;\n+\t\tauto it = frameInfos_.find(request->sequence());\n+\t\tif (it != frameInfos_.end()) {\n+\t\t\trequest->_d()->metadata().set(controls::SensorTimestamp,\n+\t\t\t\t\t\t      buffer->metadata().timestamp);\n+\t\t}\n+\t}\n+\n+\t/* No pending request: requeue the internal buffer and wait. */\n+\tif (pendingRequests_.empty()) {\n+\t\tvideo_->queueBuffer(buffer);\n+\t\treturn;\n+\t}\n+\n+\t/* Hand the raw frame and the output buffer to the software ISP. */\n+\tPendingRequest &pending = pendingRequests_.front();\n+\tstd::map<const Stream *, FrameBuffer *> outputs{\n+\t\t{ &stream_, pending.outputBuffer }\n+\t};\n+\n+\tconst int ret = swIsp_->queueBuffers(pending.request->sequence(),\n+\t\t\t\t\t     buffer, outputs);\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to queue buffers to software ISP: \" << ret;\n+\t\tif (auto it = frameInfos_.find(pending.request->sequence());\n+\t\t    it != frameInfos_.end())\n+\t\t\tit->second.metadataRequired = false;\n+\t\tif (pipe->completeBuffer(pending.request, pending.outputBuffer))\n+\t\t\ttryCompleteRequest(pending.request);\n+\t\tpendingRequests_.pop();\n+\t\tvideo_->queueBuffer(buffer);\n+\t\treturn;\n+\t}\n+\n+\tpendingRequests_.pop();\n+}\n+\n+void TegraCameraData::ispStatsReady(uint32_t frame, uint32_t bufferId)\n+{\n+\tswIsp_->processStats(frame, bufferId, delayedCtrls_->get(frame));\n+}\n+\n+void TegraCameraData::metadataReady(uint32_t frame,\n+\t\t\t\t    const ControlList &metadata)\n+{\n+\tauto it = frameInfos_.find(frame);\n+\tif (it == frameInfos_.end())\n+\t\treturn;\n+\n+\tTegraFrameInfo &info = it->second;\n+\tinfo.request->_d()->metadata().merge(metadata);\n+\tinfo.metadataProcessed = true;\n+\ttryCompleteRequest(info.request);\n+}\n+\n+void TegraCameraData::setSensorControls(\n+\tconst ControlList &sensorControls)\n+{\n+\tdelayedCtrls_->push(sensorControls);\n+}\n+\n+void TegraCameraData::ispOutputBufferReady(FrameBuffer *buffer)\n+{\n+\tRequest *request = buffer->request();\n+\tif (!request)\n+\t\treturn;\n+\n+\tif (pipe()->completeBuffer(request, buffer))\n+\t\ttryCompleteRequest(request);\n+}\n+\n+void TegraCameraData::tryCompleteRequest(Request *request)\n+{\n+\tif (request->hasPendingBuffers())\n+\t\treturn;\n+\n+\tauto it = frameInfos_.find(request->sequence());\n+\tif (it == frameInfos_.end())\n+\t\treturn;\n+\n+\tconst TegraFrameInfo &info = it->second;\n+\tif (info.metadataRequired && !info.metadataProcessed)\n+\t\treturn;\n+\n+\tframeInfos_.erase(it);\n+\tpipe()->completeRequest(request);\n+}\n+\n+void TegraCameraData::clearIncompleteRequests()\n+{\n+\twhile (!pendingRequests_.empty()) {\n+\t\tpipe()->cancelRequest(pendingRequests_.front().request);\n+\t\tpendingRequests_.pop();\n+\t}\n+}\n+\n+/* =========================================================================\n+ * TegraCameraConfiguration implementation\n+ * =========================================================================*/\n+\n+TegraCameraConfiguration::TegraCameraConfiguration(Camera *camera,\n+\t\t\t\t\t\t   TegraCameraData *data)\n+\t: CameraConfiguration(),\n+\t  camera_(camera->shared_from_this()),\n+\t  data_(data)\n+{\n+}\n+\n+CameraConfiguration::Status TegraCameraConfiguration::validate()\n+{\n+\tStatus status = Valid;\n+\n+\tif (config_.empty())\n+\t\treturn Invalid;\n+\n+\t/* Only a single output stream is supported. */\n+\tif (config_.size() > 1) {\n+\t\tconfig_.resize(1);\n+\t\tstatus = Adjusted;\n+\t}\n+\n+\t/*\n+\t * Fix the orientation: the Tegra VI pipeline does not support\n+\t * hardware transforms, so only Rotate0 is valid.\n+\t */\n+\tif (orientation != Orientation::Rotate0) {\n+\t\torientation = Orientation::Rotate0;\n+\t\tstatus = Adjusted;\n+\t}\n+\n+\tStreamConfiguration &cfg = config_[0];\n+\n+\t/* Validate that the requested pixel format is supported by swIsp. */\n+\tif (!data_->swIsp_)\n+\t\treturn Invalid;\n+\n+\t/*\n+\t * Use the first sensor format to probe what swIsp can produce, then\n+\t * check whether the requested format is in that list.\n+\t */\n+\tconst std::vector<uint32_t> mbusCodes = data_->sensor_->mbusCodes();\n+\tif (mbusCodes.empty())\n+\t\treturn Invalid;\n+\n+\tconst uint32_t primaryCode = mbusCodes.front();\n+\tconst std::vector<Size> &sensorSizes = data_->sensor_->sizes(primaryCode);\n+\tif (sensorSizes.empty())\n+\t\treturn Invalid;\n+\n+\tconst Size &maxSensorSize = sensorSizes.back();\n+\tconst BayerFormat bayerFmt = BayerFormat::fromMbusCode(primaryCode);\n+\tconst std::vector<PixelFormat> supportedFormats =\n+\t\tdata_->swIsp_->formats(\n+\t\t\tbayerFmt.isValid() ? bayerFmt.toPixelFormat()\n+\t\t\t\t\t   : formats::SRGGB10);\n+\n+\tif (!supportedFormats.empty()) {\n+\t\tauto it = std::find(supportedFormats.begin(),\n+\t\t\t\t    supportedFormats.end(),\n+\t\t\t\t    cfg.pixelFormat);\n+\t\tif (it == supportedFormats.end()) {\n+\t\t\tcfg.pixelFormat = supportedFormats.front();\n+\t\t\tLOG(TegraPipeline, Debug)\n+\t\t\t\t<< \"Adjusting pixel format to \"\n+\t\t\t\t<< cfg.pixelFormat;\n+\t\t\tstatus = Adjusted;\n+\t\t}\n+\t}\n+\n+\t/* Clamp size to sensor output range. */\n+\tSizeRange outputSizes =\n+\t\tdata_->swIsp_->sizes(\n+\t\t\tbayerFmt.isValid() ? bayerFmt.toPixelFormat()\n+\t\t\t\t\t   : formats::SRGGB10,\n+\t\t\tmaxSensorSize);\n+\n+\tif (!outputSizes.contains(cfg.size)) {\n+\t\tcfg.size = cfg.size.boundedTo(outputSizes.max)\n+\t\t\t\t   .expandedTo(outputSizes.min);\n+\t\tLOG(TegraPipeline, Debug)\n+\t\t\t<< \"Adjusting size to \" << cfg.size;\n+\t\tstatus = Adjusted;\n+\t}\n+\n+\t/* Fix up color space. */\n+\tif (!cfg.colorSpace) {\n+\t\tconst PixelFormatInfo &info =\n+\t\t\tPixelFormatInfo::info(cfg.pixelFormat);\n+\t\tswitch (info.colourEncoding) {\n+\t\tcase PixelFormatInfo::ColourEncodingRGB:\n+\t\t\tcfg.colorSpace = ColorSpace::Srgb;\n+\t\t\tbreak;\n+\t\tcase PixelFormatInfo::ColourEncodingYUV:\n+\t\t\tcfg.colorSpace = ColorSpace::Sycc;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tcfg.colorSpace = ColorSpace::Raw;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tcfg.colorSpace->adjust(cfg.pixelFormat);\n+\n+\t/* Determine stride and frame size via swIsp. */\n+\tauto [stride, frameSize] =\n+\t\tdata_->swIsp_->strideAndFrameSize(cfg.pixelFormat, cfg.size);\n+\tif (stride == 0)\n+\t\treturn Invalid;\n+\n+\tcfg.stride = stride;\n+\tcfg.frameSize = frameSize;\n+\n+\tif (!cfg.bufferCount)\n+\t\tcfg.bufferCount = TegraCameraConfiguration::kNumBuffersDefault;\n+\telse if (cfg.bufferCount > TegraCameraConfiguration::kNumBuffersMax)\n+\t\tcfg.bufferCount = TegraCameraConfiguration::kNumBuffersMax;\n+\n+\treturn status;\n+}\n+\n+/* =========================================================================\n+ * PipelineHandlerTegra implementation\n+ * =========================================================================*/\n+\n+PipelineHandlerTegra::PipelineHandlerTegra(CameraManager *manager)\n+\t: PipelineHandler(manager)\n+{\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  generateConfiguration                                                  */\n+/* ---------------------------------------------------------------------- */\n+\n+std::unique_ptr<CameraConfiguration>\n+PipelineHandlerTegra::generateConfiguration(Camera *camera,\n+\t\t\t\t\t    Span<const StreamRole> roles)\n+{\n+\tTegraCameraData *data = cameraData(camera);\n+\n+\tauto config = std::make_unique<TegraCameraConfiguration>(camera, data);\n+\n+\tif (roles.empty())\n+\t\treturn config;\n+\n+\t/*\n+\t * Build the set of output formats / sizes that swIsp can produce\n+\t * for the primary sensor media bus code.\n+\t */\n+\tif (!data->swIsp_ || data->sensor_->mbusCodes().empty())\n+\t\treturn nullptr;\n+\n+\tconst uint32_t primaryCode = data->sensor_->mbusCodes().front();\n+\tconst BayerFormat bayer = BayerFormat::fromMbusCode(primaryCode);\n+\tif (!bayer.isValid())\n+\t\treturn nullptr;\n+\tconst PixelFormat bayerFmt = bayer.toPixelFormat();\n+\n+\tconst std::vector<PixelFormat> outFormats =\n+\t\tdata->swIsp_->formats(bayerFmt);\n+\tif (outFormats.empty())\n+\t\treturn nullptr;\n+\n+\t/* Use the largest available sensor size for the default. */\n+\tconst std::vector<Size> &sensorSizes =\n+\t\tdata->sensor_->sizes(primaryCode);\n+\tif (sensorSizes.empty())\n+\t\treturn nullptr;\n+\n+\tconst Size &maxSize = sensorSizes.back();\n+\tSizeRange outSizes = data->swIsp_->sizes(bayerFmt, maxSize);\n+\n+\t/* Build StreamFormats map for the processed stream. */\n+\tstd::map<PixelFormat, std::vector<SizeRange>> fmtMap;\n+\tfor (const PixelFormat &fmt : outFormats)\n+\t\tfmtMap[fmt] = { outSizes };\n+\n+\t/*\n+\t * Only a single output stream is supported; silently accept any role\n+\t * and return one stream configuration.\n+\t */\n+\tStreamConfiguration cfg{ StreamFormats{ fmtMap } };\n+\tcfg.pixelFormat = outFormats.front();\n+\tcfg.size = outSizes.max;\n+\tcfg.bufferCount = TegraCameraConfiguration::kNumBuffersDefault;\n+\n+\tconfig->addConfiguration(cfg);\n+\tconfig->validate();\n+\n+\treturn config;\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  configure                                                              */\n+/* ---------------------------------------------------------------------- */\n+\n+int PipelineHandlerTegra::configure(Camera *camera,\n+\t\t\t\t    CameraConfiguration *c)\n+{\n+\tTegraCameraData *data = cameraData(camera);\n+\tStreamConfiguration &cfg = c->at(0);\n+\tint ret;\n+\n+\t/*\n+\t * Disable all media links on the board to start from a clean state,\n+\t * then re-enable the sensor → CSI → VI path.\n+\t */\n+\tret = data->media_->disableLinks();\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to disable media links: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\t/* Enable sensor → CSI link. */\n+\tconst MediaEntity *sensorEntity = data->sensor_->entity();\n+\tfor (MediaPad *pad : sensorEntity->pads()) {\n+\t\tif (!(pad->flags() & MEDIA_PAD_FL_SOURCE))\n+\t\t\tcontinue;\n+\t\tfor (MediaLink *link : pad->links()) {\n+\t\t\tif (link->sink()->entity() ==\n+\t\t\t    data->csi_->entity()) {\n+\t\t\t\tret = link->setEnabled(true);\n+\t\t\t\tif (ret)\n+\t\t\t\t\treturn ret;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t/* Enable CSI → VI link. */\n+\tfor (MediaPad *pad : data->csi_->entity()->pads()) {\n+\t\tif (!(pad->flags() & MEDIA_PAD_FL_SOURCE))\n+\t\t\tcontinue;\n+\t\tfor (MediaLink *link : pad->links()) {\n+\t\t\tif (link->sink()->entity() ==\n+\t\t\t    data->videoEntity_) {\n+\t\t\t\tret = link->setEnabled(true);\n+\t\t\t\tif (ret)\n+\t\t\t\t\treturn ret;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Select the best sensor format: smallest resolution that covers\n+\t * the requested output size.\n+\t */\n+\tif (data->sensor_->mbusCodes().empty()) {\n+\t\tLOG(TegraPipeline, Error) << \"Sensor has no media bus codes\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tconst uint32_t primaryCode = data->sensor_->mbusCodes().front();\n+\tconst std::vector<Size> &sensorSizes = data->sensor_->sizes(primaryCode);\n+\tSize sensorSize;\n+\tif (!selectSensorSize(sensorSizes, cfg.size, &sensorSize)) {\n+\t\tLOG(TegraPipeline, Error) << \"Sensor has no supported sizes\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tV4L2SubdeviceFormat sensorFmt{};\n+\tsensorFmt.code = primaryCode;\n+\tsensorFmt.size = sensorSize;\n+\n+\tret = data->sensor_->setFormat(&sensorFmt);\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to set sensor format: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\t/*\n+\t * Propagate the format through the CSI bridge.\n+\t * Set it on sink pad 0, then read back the source pad 1 format.\n+\t */\n+\tret = data->csi_->setFormat(0, &sensorFmt);\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to set CSI sink format: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\tV4L2SubdeviceFormat csiFmt{};\n+\tret = data->csi_->getFormat(1, &csiFmt);\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to get CSI source format: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\t/*\n+\t * Set the raw Bayer format on the VI capture node.\n+\t * The stride hint from swIsp allows it to use its preferred layout.\n+\t */\n+\tconst BayerFormat captureBayer = BayerFormat::fromMbusCode(csiFmt.code);\n+\tif (!captureBayer.isValid()) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"CSI source format is not a Bayer format: \"\n+\t\t\t<< csiFmt.code;\n+\t\treturn -EINVAL;\n+\t}\n+\tconst PixelFormat captureFmt = captureBayer.toPixelFormat();\n+\n+\tV4L2DeviceFormat videoFmt{};\n+\tvideoFmt.fourcc = data->video_->toV4L2PixelFormat(captureFmt);\n+\tvideoFmt.size = csiFmt.size;\n+\tvideoFmt.planes[0].bpl =\n+\t\tdata->swIsp_->preferredInputStride(captureFmt, csiFmt.size);\n+\n+\tret = data->video_->setFormat(&videoFmt);\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to set VI format: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\t/* Configure the software ISP using the dynamically negotiated buffer count. */\n+\tStreamConfiguration inputCfg{};\n+\tinputCfg.pixelFormat = captureFmt;\n+\tinputCfg.size = csiFmt.size;\n+\tinputCfg.stride = videoFmt.planes[0].bpl;\n+\tinputCfg.bufferCount = cfg.bufferCount;\n+\n+\tipa::soft::IPAConfigInfo ipaConfig{};\n+\tipaConfig.sensorControls = data->sensor_->controls();\n+\n+\tstd::vector<std::reference_wrapper<const StreamConfiguration>>\n+\t\toutputCfgs{ cfg };\n+\n+\tret = data->swIsp_->configure(inputCfg, outputCfgs, ipaConfig);\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to configure software ISP: \" << ret;\n+\t\treturn ret;\n+\t}\n+\n+\tcfg.setStream(&data->stream_);\n+\n+\tLOG(TegraPipeline, Debug)\n+\t\t<< \"Configured: sensor=\" << sensorFmt\n+\t\t<< \" CSI=\" << csiFmt\n+\t\t<< \" capture=\" << captureFmt\n+\t\t<< \"@\" << csiFmt.size\n+\t\t<< \" -> output=\" << cfg.pixelFormat\n+\t\t<< \"@\" << cfg.size;\n+\n+\treturn 0;\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  exportFrameBuffers                                                     */\n+/* ---------------------------------------------------------------------- */\n+\n+int PipelineHandlerTegra::exportFrameBuffers(\n+\tCamera *camera, Stream *stream,\n+\tstd::vector<std::unique_ptr<FrameBuffer>> *buffers)\n+{\n+\tTegraCameraData *data = cameraData(camera);\n+\tunsigned int count = stream->configuration().bufferCount;\n+\treturn data->swIsp_->exportBuffers(stream, count, buffers);\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  start                                                                  */\n+/* ---------------------------------------------------------------------- */\n+\n+int PipelineHandlerTegra::start(Camera *camera,\n+\t\t\t\t[[maybe_unused]] const ControlList *controls)\n+{\n+\tTegraCameraData *data = cameraData(camera);\n+\tint ret;\n+\n+\t/* Allocate internal raw ring-buffer. */\n+\tret = data->video_->allocateBuffers(kNumRawBuffers,\n+\t\t\t\t\t    &data->rawBuffers_);\n+\tif (ret < static_cast<int>(kNumRawBuffers)) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to allocate raw buffers: \" << ret;\n+\t\treturn ret < 0 ? ret : -ENOMEM;\n+\t}\n+\n+\t/* Connect the VI buffer-ready signal. */\n+\tdata->video_->bufferReady.connect(data,\n+\t\t\t\t\t  &TegraCameraData::imageBufferReady);\n+\n+\t/* Connect the swIsp input-buffer-ready signal to requeue raw buffers. */\n+\tdata->swIsp_->inputBufferReady.connect(\n+\t\tdata->video_.get(),\n+\t\t[data](FrameBuffer *buf) {\n+\t\t\tdata->video_->queueBuffer(buf);\n+\t\t});\n+\n+\t/* Start the software ISP worker. */\n+\tret = data->swIsp_->start();\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to start software ISP: \" << ret;\n+\t\tdata->video_->bufferReady.disconnect();\n+\t\tdata->swIsp_->inputBufferReady.disconnect();\n+\t\tdata->video_->releaseBuffers();\n+\t\treturn ret;\n+\t}\n+\n+\tdata->delayedCtrls_->reset();\n+\n+\t/* Start the VI video device. */\n+\tret = data->video_->streamOn();\n+\tif (ret) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to start VI streaming: \" << ret;\n+\t\tdata->swIsp_->stop();\n+\t\tdata->video_->bufferReady.disconnect();\n+\t\tdata->swIsp_->inputBufferReady.disconnect();\n+\t\tdata->video_->releaseBuffers();\n+\t\treturn ret;\n+\t}\n+\n+\t/* Pre-queue all internal raw buffers to keep the DMA pipeline full. */\n+\tfor (auto &buf : data->rawBuffers_)\n+\t\tdata->video_->queueBuffer(buf.get());\n+\n+\treturn 0;\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  stopDevice                                                             */\n+/* ---------------------------------------------------------------------- */\n+\n+void PipelineHandlerTegra::stopDevice(Camera *camera)\n+{\n+\tTegraCameraData *data = cameraData(camera);\n+\n+\t/* Disconnect signals immediately so no late asynchronous callbacks fire. */\n+\tdata->video_->bufferReady.disconnect(data, &TegraCameraData::imageBufferReady);\n+\tdata->swIsp_->inputBufferReady.disconnect();\n+\tdata->swIsp_->outputBufferReady.disconnect();\n+\n+\t/* Gracefully stop worker processing threads and clear active pipelines. */\n+\tdata->swIsp_->stop();\n+\tdata->video_->streamOff();\n+\n+\t/* Safely release buffers and clear state tracking structures. */\n+\tdata->video_->releaseBuffers();\n+\tdata->rawBuffers_.clear();\n+\tdata->clearIncompleteRequests();\n+\tdata->frameInfos_.clear();\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  queueRequestDevice                                                     */\n+/* ---------------------------------------------------------------------- */\n+\n+int PipelineHandlerTegra::queueRequestDevice(Camera *camera,\n+\t\t\t\t\t     Request *request)\n+{\n+\tTegraCameraData *data = cameraData(camera);\n+\n+\tFrameBuffer *outBuf = request->findBuffer(&data->stream_);\n+\tif (!outBuf) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Request has no buffer for the output stream\";\n+\t\treturn -ENOENT;\n+\t}\n+\n+\t/* Record metadata tracking for this frame. */\n+\tauto [it, inserted] = data->frameInfos_.try_emplace(\n+\t\trequest->sequence(),\n+\t\trequest, /*metadataRequired=*/true);\n+\tif (!inserted) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Duplicate sequence number \" << request->sequence();\n+\t\treturn -EINVAL;\n+\t}\n+\n+\t/* Forward any per-request controls to the IPA. */\n+\tdata->swIsp_->queueRequest(request->sequence(),\n+\t\t\t\t   request->controls());\n+\n+\t/* Enqueue the request output for the next raw-frame arrival. */\n+\tdata->pendingRequests_.push({ request, outBuf });\n+\n+\treturn 0;\n+}\n+\n+/* ---------------------------------------------------------------------- */\n+/*  match                                                                  */\n+/* ---------------------------------------------------------------------- */\n+\n+/*\n+ * Given a camera sensor entity, walk downstream through the media graph to\n+ * find the first CSI bridge (MEDIA_ENT_F_VID_IF_BRIDGE) and then the VI\n+ * capture node (MEDIA_ENT_F_IO_V4L).\n+ *\n+ * Returns true when both entities are found.\n+ */\n+bool PipelineHandlerTegra::findPipelineEntities(\n+\tMediaEntity *sensorEntity,\n+\tMediaEntity **csiEntity,\n+\tMediaEntity **viEntity)\n+{\n+\t*csiEntity = nullptr;\n+\t*viEntity = nullptr;\n+\n+\tfor (MediaPad *pad : sensorEntity->pads()) {\n+\t\tif (!(pad->flags() & MEDIA_PAD_FL_SOURCE))\n+\t\t\tcontinue;\n+\n+\t\tfor (MediaLink *link : pad->links()) {\n+\t\t\tMediaEntity *downstream = link->sink()->entity();\n+\n+\t\t\tif (downstream->function() ==\n+\t\t\t    MEDIA_ENT_F_VID_IF_BRIDGE) {\n+\t\t\t\t*csiEntity = downstream;\n+\n+\t\t\t\t/* Follow one more hop to find the VI node. */\n+\t\t\t\tfor (MediaPad *csiPad : downstream->pads()) {\n+\t\t\t\t\tif (!(csiPad->flags() &\n+\t\t\t\t\t      MEDIA_PAD_FL_SOURCE))\n+\t\t\t\t\t\tcontinue;\n+\n+\t\t\t\t\tfor (MediaLink *csiLink :\n+\t\t\t\t\t     csiPad->links()) {\n+\t\t\t\t\t\tMediaEntity *vi =\n+\t\t\t\t\t\t\tcsiLink->sink()\n+\t\t\t\t\t\t\t\t->entity();\n+\t\t\t\t\t\tif (vi->function() ==\n+\t\t\t\t\t\t    MEDIA_ENT_F_IO_V4L) {\n+\t\t\t\t\t\t\t*viEntity = vi;\n+\t\t\t\t\t\t\treturn true;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treturn false;\n+}\n+\n+bool PipelineHandlerTegra::createCamera(\n+\tstd::shared_ptr<MediaDevice> media,\n+\tMediaEntity *sensorEntity)\n+{\n+\tMediaEntity *csiEntity = nullptr;\n+\tMediaEntity *viEntity = nullptr;\n+\n+\tif (!findPipelineEntities(sensorEntity, &csiEntity, &viEntity)) {\n+\t\tLOG(TegraPipeline, Debug)\n+\t\t\t<< \"No complete sensor→CSI→VI path from \"\n+\t\t\t<< sensorEntity->name();\n+\t\treturn false;\n+\t}\n+\n+\tauto data =\n+\t\tstd::make_unique<TegraCameraData>(this, media);\n+\n+\t/* Create the camera sensor. */\n+\tdata->sensor_ =\n+\t\tCameraSensorFactoryBase::create(sensorEntity);\n+\tif (!data->sensor_) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to create sensor for \"\n+\t\t\t<< sensorEntity->name();\n+\t\treturn false;\n+\t}\n+\n+\t/* Open the CSI bridge subdev. */\n+\tdata->csi_ = std::make_unique<V4L2Subdevice>(csiEntity);\n+\n+\t/* Open the VI video device. */\n+\tdata->video_ =\n+\t\tstd::make_unique<V4L2VideoDevice>(viEntity);\n+\tdata->videoEntity_ = viEntity;\n+\n+\tif (data->init()) {\n+\t\tLOG(TegraPipeline, Error)\n+\t\t\t<< \"Failed to initialise camera data for \"\n+\t\t\t<< sensorEntity->name();\n+\t\treturn false;\n+\t}\n+\n+\t/* Assemble the camera and register it. */\n+\tstd::set<Stream *> streams{ &data->stream_ };\n+\tconst std::string id = data->sensor_->id();\n+\n+\tstd::shared_ptr<Camera> camera =\n+\t\tCamera::create(std::move(data), id, streams);\n+\tregisterCamera(std::move(camera));\n+\n+\tLOG(TegraPipeline, Info)\n+\t\t<< \"Registered Tegra camera: \" << id\n+\t\t<< \" (\" << sensorEntity->name() << \" → \"\n+\t\t<< csiEntity->name() << \" → \"\n+\t\t<< viEntity->name() << \")\";\n+\n+\treturn true;\n+}\n+\n+bool PipelineHandlerTegra::match(DeviceEnumerator *enumerator)\n+{\n+\t/*\n+\t * Try both known Tegra VI driver names.\n+\t *   \"tegra-vi\"    – Tegra 210 / 186 / 194  (Nano / TX2 / Xavier)\n+\t *   \"tegra234-vi\" – Tegra 234              (Orin)\n+\t */\n+\tstatic const char *const kDriverNames[] = {\n+\t\t\"tegra-vi\",\n+\t\t\"tegra234-vi\",\n+\t};\n+\n+\tbool matched = false;\n+\n+\tfor (const char *driverName : kDriverNames) {\n+\t\tDeviceMatch dm(driverName);\n+\t\tstd::shared_ptr<MediaDevice> media =\n+\t\t\tacquireMediaDevice(enumerator, dm);\n+\t\tif (!media)\n+\t\t\tcontinue;\n+\n+\t\tLOG(TegraPipeline, Debug)\n+\t\t\t<< \"Found Tegra VI media device (\"\n+\t\t\t<< driverName << \")\";\n+\n+\t\t/*\n+\t\t * Enumerate all camera sensor entities and attempt to build a\n+\t\t * camera for each one.\n+\t\t */\n+\t\tfor (MediaEntity *entity : media->entities()) {\n+\t\t\tif (entity->function() != MEDIA_ENT_F_CAM_SENSOR)\n+\t\t\t\tcontinue;\n+\n+\t\t\tif (createCamera(media, entity))\n+\t\t\t\tmatched = true;\n+\t\t}\n+\t}\n+\n+\treturn matched;\n+}\n+\n+REGISTER_PIPELINE_HANDLER(PipelineHandlerTegra, \"tegra\")\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/software_isp/meson.build b/src/libcamera/software_isp/meson.build\nindex 15f9e4032..08522fc30 100644\n--- a/src/libcamera/software_isp/meson.build\n+++ b/src/libcamera/software_isp/meson.build\n@@ -1,6 +1,6 @@\n # SPDX-License-Identifier: CC0-1.0\n \n-softisp_enabled = pipelines.contains('simple')\n+softisp_enabled = pipelines.contains('simple') or pipelines.contains('tegra')\n summary({'SoftISP support' : softisp_enabled},\n         bool_yn : true,\n         section : 'Configuration')\n",
    "prefixes": [
        "RFC",
        "1/1"
    ]
}