[{"id":33090,"web_url":"https://patchwork.libcamera.org/comment/33090/","msgid":"<CAEmqJPr2oH_Fdm0rk6j0ND1=rk_wF8U2ogNPq_XO=VFvLEYL3w@mail.gmail.com>","date":"2025-01-17T09:26:17","subject":"Re: [PATCH v3 6/7] ipa: rpi: sync: Add an implementation of the\n\tcamera sync algorithm","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/people/34/","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"content":"Hi David,\n\nOn Thu, 9 Jan 2025 at 14:32, David Plowman\n<david.plowman@raspberrypi.com> wrote:\n>\n> In this implementation, the server sends data packets out onto the\n> network every 30 frames or so.\n>\n> Clients listening for this packet will send frame length deltas back\n> to the pipeline handler to match the synchronisation of the server.\n>\n> We use wallclock timestamps, passed to us from the pipeline handler,\n> that have been de-jittered appropriately, meaning that the\n> synchronisation will actually work across networked devices.\n>\n> When the server's advertised \"ready time\" is reached, both client and\n> server will signal this through metadata back to their respective\n> controlling applications.\n>\n> Signed-off-by: David Plowman <david.plowman@raspberrypi.com>\n> Signed-off-by: Arsen Mikovic <arsen.mikovic@raspberrypi.com>\n> Signed-off-by: Naushir Patuck <naush@raspberrypi.com>\n> ---\n>  src/ipa/rpi/controller/meson.build  |   1 +\n>  src/ipa/rpi/controller/rpi/sync.cpp | 330 ++++++++++++++++++++++++++++\n>  src/ipa/rpi/controller/rpi/sync.h   |  68 ++++++\n>  3 files changed, 399 insertions(+)\n>  create mode 100644 src/ipa/rpi/controller/rpi/sync.cpp\n>  create mode 100644 src/ipa/rpi/controller/rpi/sync.h\n>\n> diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build\n> index 74b74888..dde4ac12 100644\n> --- a/src/ipa/rpi/controller/meson.build\n> +++ b/src/ipa/rpi/controller/meson.build\n> @@ -23,6 +23,7 @@ rpi_ipa_controller_sources = files([\n>      'rpi/saturation.cpp',\n>      'rpi/sdn.cpp',\n>      'rpi/sharpen.cpp',\n> +    'rpi/sync.cpp',\n>      'rpi/tonemap.cpp',\n>  ])\n>\n> diff --git a/src/ipa/rpi/controller/rpi/sync.cpp b/src/ipa/rpi/controller/rpi/sync.cpp\n> new file mode 100644\n> index 00000000..43a8cbe6\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/sync.cpp\n> @@ -0,0 +1,330 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2024, Raspberry Pi Ltd\n> + *\n> + * sync.cpp - sync algorithm\n> + */\n> +#include \"sync.h\"\n> +\n> +#include <chrono>\n> +#include <ctype.h>\n> +#include <fcntl.h>\n> +#include <strings.h>\n> +#include <unistd.h>\n> +\n> +#include <libcamera/base/log.h>\n> +\n> +#include <arpa/inet.h>\n> +\n> +#include \"sync_status.h\"\n> +\n> +using namespace std;\n> +using namespace std::chrono_literals;\n> +using namespace RPiController;\n> +using namespace libcamera;\n> +\n> +LOG_DEFINE_CATEGORY(RPiSync)\n> +\n> +#define NAME \"rpi.sync\"\n> +\n> +const char *kDefaultGroup = \"239.255.255.250\";\n> +constexpr unsigned int kDefaultPort = 10000;\n> +constexpr unsigned int kDefaultSyncPeriod = 30;\n> +constexpr unsigned int kDefaultReadyFrame = 100;\n> +constexpr unsigned int kDefaultMinAdjustment = 50;\n\nI wonder if we can embed these constants into the read() code below,\nbut I'm not fussed either way.\n\n> +\n> +Sync::Sync(Controller *controller)\n> +       : SyncAlgorithm(controller), mode_(Mode::Off), socket_(-1), frameDuration_(0s), frameCount_(0)\n> +{\n> +}\n> +\n> +Sync::~Sync()\n> +{\n> +       if (socket_ >= 0)\n> +               close(socket_);\n> +}\n> +\n> +char const *Sync::name() const\n> +{\n> +       return NAME;\n> +}\n> +\n> +/* This reads from json file and intitiaises server and client */\n> +int Sync::read(const libcamera::YamlObject &params)\n> +{\n> +       /* Socket on which to communicate. */\n> +       group_ = params[\"group\"].get<std::string>(kDefaultGroup);\n> +       port_ = params[\"port\"].get<uint16_t>(kDefaultPort);\n> +       /* Send a sync message every this many frames. */\n> +       syncPeriod_ = params[\"sync_period\"].get<uint32_t>(kDefaultSyncPeriod);\n> +       /* Application will be told we're ready after this many frames. */\n> +       readyFrame_ = params[\"ready_frame\"].get<uint32_t>(kDefaultReadyFrame);\n> +       /* Don't change client frame length unless the change exceeds this amount (microseconds). */\n> +       minAdjustment_ = params[\"min_adjustment\"].get<uint32_t>(kDefaultMinAdjustment);\n> +\n> +       return 0;\n> +}\n> +\n> +void Sync::initialiseSocket()\n> +{\n> +       socket_ = socket(AF_INET, SOCK_DGRAM, 0);\n> +       if (socket_ < 0) {\n> +               LOG(RPiSync, Error) << \"Unable to create socket\";\n> +               return;\n> +       }\n> +\n> +       memset(&addr_, 0, sizeof(addr_));\n> +       addr_.sin_family = AF_INET;\n> +       addr_.sin_addr.s_addr = mode_ == Mode::Client ? htonl(INADDR_ANY) : inet_addr(group_.c_str());\n> +       addr_.sin_port = htons(port_);\n> +\n> +       if (mode_ == Mode::Client) {\n> +               /* Set to non-blocking. */\n> +               int flags = fcntl(socket_, F_GETFL, 0);\n> +               fcntl(socket_, F_SETFL, flags | O_NONBLOCK);\n> +\n> +               unsigned int en = 1;\n> +               if (setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, &en, sizeof(en)) < 0) {\n> +                       LOG(RPiSync, Error) << \"Unable to set socket options\";\n> +                       goto err;\n> +               }\n> +\n> +               struct ip_mreq mreq {\n> +               };\n\nExtra newline added accidentaly?\n\n> +               mreq.imr_multiaddr.s_addr = inet_addr(group_.c_str());\n> +               mreq.imr_interface.s_addr = htonl(INADDR_ANY);\n> +               if (setsockopt(socket_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {\n> +                       LOG(RPiSync, Error) << \"Unable to set socket options\";\n> +                       goto err;\n> +               }\n> +\n> +               if (bind(socket_, (struct sockaddr *)&addr_, sizeof(addr_)) < 0) {\n> +                       LOG(RPiSync, Error) << \"Unable to bind client socket\";\n> +                       goto err;\n> +               }\n> +       }\n> +\n> +       return;\n> +\n> +err:\n> +       close(socket_);\n> +       socket_ = -1;\n> +}\n> +\n> +void Sync::switchMode([[maybe_unused]] CameraMode const &cameraMode, [[maybe_unused]] Metadata *metadata)\n> +{\n> +       /*\n> +        * A mode switch means the camera has stopped, so synchronisation will be lost.\n> +        * Reset all the internal state so that we start over.\n> +        */\n> +       reset();\n> +}\n> +\n> +/*\n> + * Camera sync algorithm.\n> + *     Server - there is a single server that sends framerate timing information over the network to any\n> + *         clients that are listening. It also signals when it will send a \"everything is synchronised, now go\"\n> + *         message back to the algorithm.\n> + *     Client - there may be many clients, either on the same Pi or different ones. They match their\n> + *         framerates to the server, and indicate when to \"go\" at the same instant as the server.\n> + */\n> +void Sync::process([[maybe_unused]] StatisticsPtr &stats, Metadata *imageMetadata)\n> +{\n> +       SyncPayload payload;\n> +       SyncParams local{};\n> +       SyncStatus status{};\n> +       bool timerKnown = true;\n> +\n> +       if (mode_ == Mode::Off)\n> +               return;\n> +\n> +       if (!frameDuration_) {\n> +               LOG(RPiSync, Error) << \"Sync frame duration not set!\";\n> +               return;\n> +       }\n> +\n> +       if (socket_ < 0) {\n> +               initialiseSocket();\n> +\n> +               if (socket_ < 0)\n> +                       return;\n\nMaybe we should add a warning log message here?\n\n> +\n> +               /*\n> +                * For the client, flush anything in the socket. It might be stale from a previous sync run,\n> +                * or we might get another packet in a frame to two before the adjustment caused by this (old)\n> +                * packet, although correct, had taken effect. So this keeps things simpler.\n> +                */\n> +               if (mode_ == Mode::Client) {\n> +                       socklen_t addrlen = sizeof(addr_);\n> +                       int ret = 0;\n> +                       while (ret >= 0)\n> +                               ret = recvfrom(socket_, &payload, sizeof(payload), 0, (struct sockaddr *)&addr_, &addrlen);\n> +               }\n> +       }\n> +\n> +       imageMetadata->get(\"sync.params\", local);\n> +\n> +       /* The wallclock has already been de-jittered for us. */\n> +       uint64_t wallClockFrameTimestamp = local.wallClock;\n> +\n> +       /*\n> +        * This is the headline frame duration in microseconds as programmed into the sensor. Strictly,\n> +        * the sensor might not quite match the system clock, but this shouldn't matter for the calculations\n> +        * we'll do with it, unless it's a very very long way out!\n> +        */\n> +       uint32_t frameDuration = frameDuration_.get<std::micro>();\n> +\n> +       /* Timestamps tell us if we've dropped any frames, but we still want to count them. */\n> +       int droppedFrames = 0;\n> +       if (frameCount_) {\n> +               /*\n> +                * Round down here, because frameCount_ gets incremented at the end of the function. Also\n> +                * ensure droppedFrames can't go negative. It shouldn't, but things would go badly wrong\n> +                * if it did.\n> +                */\n> +               wallClockFrameTimestamp = std::max<uint64_t>(wallClockFrameTimestamp, lastWallClockFrameTimestamp_ + frameDuration / 2);\n> +               droppedFrames = (wallClockFrameTimestamp - lastWallClockFrameTimestamp_ - frameDuration / 2) / frameDuration;\n> +               frameCount_ += droppedFrames;\n> +       }\n> +\n> +       if (mode_ == Mode::Server) {\n> +               /*\n> +                * Server sends a packet every syncPeriod_ frames, or as soon after as possible (if any\n> +                * frames were dropped).\n> +                */\n> +               serverFrameCountPeriod_ += droppedFrames;\n> +\n> +               /*\n> +                * The client may want a better idea of the true frame duration. Any error would feed straight\n> +                * into the correction term because of how it uses it to get the \"nearest\" frame.\n> +                */\n> +               if (frameCount_ == 0)\n> +                       frameDurationEstimated_ = frameDuration;\n> +               else {\n> +                       double diff = (wallClockFrameTimestamp - lastWallClockFrameTimestamp_) / (1 + droppedFrames);\n> +                       int N = std::min(frameCount_, 99U);\n> +                       frameDurationEstimated_ = frameCount_ == 1 ? diff : (N * frameDurationEstimated_ + diff) / (N + 1);\n> +               }\n> +\n> +               /* Calculate frames remaining, and therefore \"time left until ready\". */\n> +               int framesRemaining = readyFrame_ - frameCount_;\n> +               uint64_t wallClockReadyTime = wallClockFrameTimestamp + (int64_t)framesRemaining * frameDurationEstimated_;\n> +\n> +               if (serverFrameCountPeriod_ >= syncPeriod_) {\n> +                       serverFrameCountPeriod_ = 0;\n> +\n> +                       payload.frameDuration = frameDurationEstimated_ + .5; /* round to nearest */\n> +                       payload.wallClockFrameTimestamp = wallClockFrameTimestamp;\n> +                       payload.wallClockReadyTime = wallClockReadyTime;\n> +\n> +                       LOG(RPiSync, Debug) << \"Send packet (frameNumber \" << frameCount_ << \"):\";\n> +                       LOG(RPiSync, Debug) << \"            frameDuration \" << payload.frameDuration;\n> +                       LOG(RPiSync, Debug) << \"            wallClockFrameTimestamp \" << wallClockFrameTimestamp\n> +                                           << \" (\" << wallClockFrameTimestamp - lastWallClockFrameTimestamp_ << \")\";\n> +                       LOG(RPiSync, Debug) << \"            wallClockReadyTime \" << wallClockReadyTime;\n> +\n> +                       if (sendto(socket_, &payload, sizeof(payload), 0, (const sockaddr *)&addr_, sizeof(addr_)) < 0)\n> +                               LOG(RPiSync, Error) << \"Send error! \" << strerror(errno);\n> +               }\n> +\n> +               timerValue_ = static_cast<int64_t>(wallClockReadyTime - wallClockFrameTimestamp);\n> +               if (!syncReady_ && wallClockFrameTimestamp + frameDurationEstimated_ / 2 > wallClockReadyTime) {\n> +                       syncReady_ = true;\n> +                       LOG(RPiSync, Info) << \"*** Sync achieved! Difference \" << timerValue_ << \"us\";\n\nCould we remove the *** from the message?\n\n> +               }\n> +\n> +               serverFrameCountPeriod_ += 1;\n> +\n> +       } else if (mode_ == Mode::Client) {\n> +               uint64_t serverFrameTimestamp = 0;\n> +\n> +               bool packetReceived = false;\n> +               while (true) {\n> +                       socklen_t addrlen = sizeof(addr_);\n> +                       int ret = recvfrom(socket_, &payload, sizeof(payload), 0, (struct sockaddr *)&addr_, &addrlen);\n> +\n> +                       if (ret < 0)\n> +                               break;\n> +                       packetReceived = (ret > 0);\n> +                       clientSeenPacket_ = true;\n> +\n> +                       frameDurationEstimated_ = payload.frameDuration;\n> +                       serverFrameTimestamp = payload.wallClockFrameTimestamp;\n> +                       serverReadyTime_ = payload.wallClockReadyTime;\n> +               }\n> +\n> +               if (packetReceived) {\n> +                       uint64_t clientFrameTimestamp = wallClockFrameTimestamp;\n> +                       int64_t clientServerDelta = clientFrameTimestamp - serverFrameTimestamp;\n> +                       /* \"A few frames ago\" may have better matched the server's frame. Calculate when it was. */\n> +                       int framePeriodErrors = (clientServerDelta + frameDurationEstimated_ / 2) / frameDurationEstimated_;\n> +                       int64_t clientFrameTimestampNearest = clientFrameTimestamp - framePeriodErrors * frameDurationEstimated_;\n> +                       /* We must shorten a single client frame by this amount if it exceeds the minimum: */\n> +                       int32_t correction = clientFrameTimestampNearest - serverFrameTimestamp;\n> +                       if (std::abs(correction) < minAdjustment_)\n> +                               correction = 0;\n> +\n> +                       LOG(RPiSync, Debug) << \"Received packet (frameNumber \" << frameCount_ << \"):\";\n> +                       LOG(RPiSync, Debug) << \"                serverFrameTimestamp \" << serverFrameTimestamp;\n> +                       LOG(RPiSync, Debug) << \"                serverReadyTime \" << serverReadyTime_;\n> +                       LOG(RPiSync, Debug) << \"                clientFrameTimestamp \" << clientFrameTimestamp;\n> +                       LOG(RPiSync, Debug) << \"                clientFrameTimestampNearest \" << clientFrameTimestampNearest\n> +                                           << \" (\" << framePeriodErrors << \")\";\n> +                       LOG(RPiSync, Debug) << \"                correction \" << correction;\n> +\n> +                       status.frameDurationOffset = correction * 1us;\n> +               }\n> +\n> +               timerValue_ = static_cast<int64_t>(serverReadyTime_ - wallClockFrameTimestamp);\n> +               timerKnown = clientSeenPacket_; /* client must receive a packet before the timer value is correct */\n> +               if (clientSeenPacket_ && !syncReady_ && wallClockFrameTimestamp + frameDurationEstimated_ / 2 > serverReadyTime_) {\n> +                       syncReady_ = true;\n> +                       LOG(RPiSync, Info) << \"*** Sync achieved! Difference \" << timerValue_ << \"us\";\n> +               }\n> +       }\n> +\n> +       lastWallClockFrameTimestamp_ = wallClockFrameTimestamp;\n> +\n> +       status.ready = syncReady_;\n> +       status.timerValue = timerValue_;\n> +       status.timerKnown = timerKnown;\n> +       imageMetadata->set(\"sync.status\", status);\n> +       frameCount_++;\n> +}\n> +\n> +void Sync::reset()\n> +{\n> +       /* This resets the state so that the synchronisation procedure will start over. */\n> +       syncReady_ = false;\n> +       frameCount_ = 0;\n> +       timerValue_ = 0;\n> +       serverFrameCountPeriod_ = 0;\n> +       serverReadyTime_ = 0;\n> +       clientSeenPacket_ = false;\n> +}\n> +\n> +void Sync::setMode(Mode mode)\n> +{\n> +       mode_ = mode;\n> +\n> +       /* Another \"sync session\" can be started by turning it off and on again. */\n> +       if (mode == Mode::Off)\n> +               reset();\n\nShould we have a top-level Sync::Reset() API call for this?\n\nMinors aside:\n\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n\n> +}\n> +\n> +void Sync::setFrameDuration(libcamera::utils::Duration frameDuration)\n> +{\n> +       frameDuration_ = frameDuration;\n> +};\n> +\n> +void Sync::setReadyFrame(unsigned int frame)\n> +{\n> +       readyFrame_ = frame;\n> +};\n> +\n> +/* Register algorithm with the system. */\n> +static Algorithm *create(Controller *controller)\n> +{\n> +       return (Algorithm *)new Sync(controller);\n> +}\n> +static RegisterAlgorithm reg(NAME, &create);\n> diff --git a/src/ipa/rpi/controller/rpi/sync.h b/src/ipa/rpi/controller/rpi/sync.h\n> new file mode 100644\n> index 00000000..d3c79b7a\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/sync.h\n> @@ -0,0 +1,68 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2024, Raspberry Pi Ltd\n> + *\n> + * sync.h - sync algorithm\n> + */\n> +#pragma once\n> +\n> +#include <netinet/ip.h>\n> +\n> +#include \"../sync_algorithm.h\"\n> +\n> +namespace RPiController {\n> +\n> +struct SyncPayload {\n> +       /* Frame duration in microseconds. */\n> +       uint32_t frameDuration;\n> +       /* Server system (kernel) frame timestamp. */\n> +       uint64_t systemFrameTimestamp;\n> +       /* Server wall clock version of the frame timestamp. */\n> +       uint64_t wallClockFrameTimestamp;\n> +       /* Server system (kernel) sync time (the time at which frames are marked ready). */\n> +       uint64_t systemReadyTime;\n> +       /* Server wall clock version of the sync time. */\n> +       uint64_t wallClockReadyTime;\n> +};\n> +\n> +class Sync : public SyncAlgorithm\n> +{\n> +public:\n> +       Sync(Controller *controller);\n> +       ~Sync();\n> +       char const *name() const override;\n> +       int read(const libcamera::YamlObject &params) override;\n> +       void setMode(Mode mode) override;\n> +       void initialiseSocket();\n> +       void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;\n> +       void process(StatisticsPtr &stats, Metadata *imageMetadata) override;\n> +       void setFrameDuration(libcamera::utils::Duration frameDuration) override;\n> +       void setReadyFrame(unsigned int frame) override;\n> +\n> +private:\n> +       void reset(); /* reset internal state and start over */\n> +\n> +       Mode mode_; /* server or client */\n> +       std::string group_; /* IP group address for sync messages */\n> +       uint16_t port_; /* port number for messages */\n> +       uint32_t syncPeriod_; /* send a sync message every this many frames */\n> +       uint32_t readyFrame_; /* tell the application we're ready after this many frames */\n> +       uint32_t minAdjustment_; /* don't adjust the client frame length by less than this */\n> +\n> +       struct sockaddr_in addr_;\n> +       int socket_ = -1;\n> +       libcamera::utils::Duration frameDuration_;\n> +       unsigned int frameCount_;\n> +       bool syncReady_;\n> +       int64_t timerValue_ = 0; /* time until \"ready time\" */\n> +\n> +       double frameDurationEstimated_ = 0; /* estimate the true frame duration of the sensor */\n> +       uint64_t lastWallClockFrameTimestamp_; /* wall clock timestamp of previous frame */\n> +\n> +       uint32_t serverFrameCountPeriod_ = 0; /* send the next packet when this reaches syncPeriod_ */\n> +\n> +       bool clientSeenPacket_ = false; /* whether the client has received a packet yet */\n> +       uint64_t serverReadyTime_ = 0; /* the client's latest value for when the server will be \"ready\" */\n> +};\n> +\n> +} /* namespace RPiController */\n> --\n> 2.39.5\n>","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 CF021BD7D8\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 17 Jan 2025 09:26:56 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 0E8BB6854A;\n\tFri, 17 Jan 2025 10:26:56 +0100 (CET)","from mail-yb1-xb31.google.com (mail-yb1-xb31.google.com\n\t[IPv6:2607:f8b0:4864:20::b31])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 2FD496851D\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 17 Jan 2025 10:26:54 +0100 (CET)","by mail-yb1-xb31.google.com with SMTP id\n\t3f1490d57ef6-e3a0d9aab47so298533276.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 17 Jan 2025 01:26:54 -0800 (PST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=raspberrypi.com header.i=@raspberrypi.com\n\theader.b=\"poXvrfiE\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1737106013; x=1737710813;\n\tdarn=lists.libcamera.org; \n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:from:to:cc:subject:date:message-id:reply-to;\n\tbh=RSejWEI/lySb+2imR18CnWJBmzjmQIUlNcnxrjQmNZE=;\n\tb=poXvrfiEChx4h8m9SgMrncFqT9us3Vh/Fq5zH+qt0etj2fGJLyDw0joK1NrwtqQQSl\n\t4a2m9pksUZBIGWpNcEmhwe+L4SwOZw5WhSIB6Ek7ji2pI0hZMfAe2D1XuJSlkvASvrFd\n\tR+D8U1qMiFDPXyoLzqBI7ryZpVUDGd1GkA/Qecl6elZZkFj4rJvowGs6kOcbiXryRwb7\n\tapcKfrG2MNf4yhXPLxo1w1ltkGQ8xK+RMOS1qDcrZkpuInZ//79uGG50Prs7g1pQh9JA\n\t9j6ibQsU12CyC2n/xrBv+yB8Eu11HKzYvKMJ3pvUgxInACOElSNh5myfaDWjAMMlWKS7\n\tRAhg==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1737106013; x=1737710813;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-message-state:from:to:cc:subject:date:message-id\n\t:reply-to;\n\tbh=RSejWEI/lySb+2imR18CnWJBmzjmQIUlNcnxrjQmNZE=;\n\tb=kys58huin6i2U5sD8lxl6SXWmhQuG+RYYtHRxOflP16bPSp36srcwF9w84Tw8g8AJO\n\tq0DaZiGALarKbIytnU99bcYkWVZOrq/vejZwua20B5pguQEXiuINmh5FjnnUap4fYVEl\n\trocezVXTA3h92iWmcOTiJ1rAwH3lvwsb94GBoiAm4Bl1fv3bOwKj2fABvZl/yBATTYOT\n\toLNqoNq7ZSbHVXelmdfI+G3uDiuZ778HOyYPOUwZn7gZeEficSCabVC6gHSoN6wySSFi\n\tVCVauSr8+y4zHETMNfRlFfyHJWvM1H2hiDtGo+rDwejn19AUa/YY5oUyNYkI2cOq6MMg\n\tw8mw==","X-Gm-Message-State":"AOJu0Yy/xicnKHThkHs27bJMTBCKMUVV0zcZqzOF2ekVqy5PwWdYQ+yO\n\t9eGGRcmbgv3AIsgvmWTaZJHQEOynIwSr4KPsCaj3UvchVVhsGfmjCO9+7NxYw3T0xTidI2ACZdM\n\tpyfezgM4XMKep2PttvuCpZMq+BK4Jfe+Rh+Hwew==","X-Gm-Gg":"ASbGncuhwM2VP7CkwFWnt7aGeIZqoF38jOOrV3rKqKhUMAQ8+uQtuhA9161GO4i9cBf\n\t2qc6BnNTR3OSg6IO/Wz8PwAgJWx0FF1hWjwJ2tBfYANnqxPEt5b1cV3FWeuD8/xwEV272uA==","X-Google-Smtp-Source":"AGHT+IHOzlLCPYJZmASXtsZEWSLeHjt2dXwhwsJuc8du/vwJMnEFY5k7cqJtdbGEtRT75RQM6QSLptrp4sJTQ747CfE=","X-Received":"by 2002:a05:6902:1185:b0:e57:902a:66e5 with SMTP id\n\t3f1490d57ef6-e57b138232dmr536962276.10.1737106012881; Fri, 17 Jan 2025\n\t01:26:52 -0800 (PST)","MIME-Version":"1.0","References":"<20250109143211.11939-1-david.plowman@raspberrypi.com>\n\t<20250109143211.11939-7-david.plowman@raspberrypi.com>","In-Reply-To":"<20250109143211.11939-7-david.plowman@raspberrypi.com>","From":"Naushir Patuck <naush@raspberrypi.com>","Date":"Fri, 17 Jan 2025 09:26:17 +0000","X-Gm-Features":"AbW1kvaFJp67HCVgpIqt9o3HQuAVhRvRF9IwJlHG6f2onXR_ykkjkjXQZomJkEY","Message-ID":"<CAEmqJPr2oH_Fdm0rk6j0ND1=rk_wF8U2ogNPq_XO=VFvLEYL3w@mail.gmail.com>","Subject":"Re: [PATCH v3 6/7] ipa: rpi: sync: Add an implementation of the\n\tcamera sync algorithm","To":"David Plowman <david.plowman@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org, \n\tArsen Mikovic <arsen.mikovic@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","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>"}}]