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