{"id":26051,"url":"https://patchwork.libcamera.org/api/patches/26051/?format=json","web_url":"https://patchwork.libcamera.org/patch/26051/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260130080935.2569621-3-paul.elder@ideasonboard.com>","date":"2026-01-30T08:09:33","name":"[v2,2/4] layer: Add layer that implements the sync algorithm","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"2ff4f04bb74f6756fde65a1bb8c5f957b00a3958","submitter":{"id":17,"url":"https://patchwork.libcamera.org/api/people/17/?format=json","name":"Paul Elder","email":"paul.elder@ideasonboard.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26051/mbox/","series":[{"id":5759,"url":"https://patchwork.libcamera.org/api/series/5759/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5759","date":"2026-01-30T08:09:31","name":"Add Sync Layer","version":2,"mbox":"https://patchwork.libcamera.org/series/5759/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26051/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26051/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 BBD91C3226\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 30 Jan 2026 08:09:55 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 7400061FD2;\n\tFri, 30 Jan 2026 09:09:55 +0100 (CET)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id D2F6261FA0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 30 Jan 2026 09:09:53 +0100 (CET)","from neptunite.hamster-moth.ts.net (unknown\n\t[IPv6:2404:7a81:160:2100:ec11:5e0c:deb8:1e2d])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 8F1891E23;\n\tFri, 30 Jan 2026 09:09:13 +0100 (CET)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"AbJ8xZB7\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1769760555;\n\tbh=SD6awa9bfRediz1L9MVK8p6/dNUu0WrkLN6isbS636I=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=AbJ8xZB7SFU9HPpEh5vS9XJDw5COonHP/m+EpU79d6lKZaisvxuLMvjWzYy3gLhGN\n\tlKKwJ64vZL6usUCPcJ6SJsslbuSd+eNf9XRBaGgKNX2OA2w1oGLF73BAm0CIzDH63L\n\taUEUL37T6oX3Mzdfxo2YNVHu0ImlXL8OSfVd6b6Y=","From":"Paul Elder <paul.elder@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"Paul Elder <paul.elder@ideasonboard.com>, david.plowman@raspberrypi.com, \n\tnaush@raspberrypi.com, kieran.bingham@ideasonboard.com,\n\tstefan.klug@ideasonboard.com","Subject":"[PATCH v2 2/4] layer: Add layer that implements the sync algorithm","Date":"Fri, 30 Jan 2026 17:09:33 +0900","Message-ID":"<20260130080935.2569621-3-paul.elder@ideasonboard.com>","X-Mailer":"git-send-email 2.47.2","In-Reply-To":"<20260130080935.2569621-1-paul.elder@ideasonboard.com>","References":"<20260130080935.2569621-1-paul.elder@ideasonboard.com>","MIME-Version":"1.0","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":"Implement the sync layer, which implements the sync algorithm. Any\nCamera that supports the SyncAdjustment and FrameDurationLimits\ncontrols, and that reports the SensorTimestamp metadata will\nautomatically be supported by this layer.\n\nThis code is heavily based on Raspberry Pi's sync algorithm\nimplementation, from the following files in commit [1]:\n- src/ipa/rpi/controller/sync_algorithm.h\n- src/ipa/rpi/controller/sync_status.h\n- src/ipa/rpi/controller/rpi/sync.cpp\n- src/ipa/rpi/controller/rpi/sync.h\n\n[1] https://github.com/raspberrypi/libcamera/commit/d1a712060dcb0aab8564e0d1d86efe9ffcfee6b9\n\nSigned-off-by: Paul Elder <paul.elder@ideasonboard.com>\n\n---\nChanges in v2:\n- use UniqueFD for socket\n- remove setting nonblock from fcntl() to socket()\n  - add cloexec\n---\n src/layer/meson.build      |   1 +\n src/layer/sync/meson.build |  15 ++\n src/layer/sync/sync.cpp    | 463 +++++++++++++++++++++++++++++++++++++\n src/layer/sync/sync.h      |  98 ++++++++\n 4 files changed, 577 insertions(+)\n create mode 100644 src/layer/sync/meson.build\n create mode 100644 src/layer/sync/sync.cpp\n create mode 100644 src/layer/sync/sync.h","diff":"diff --git a/src/layer/meson.build b/src/layer/meson.build\nindex 3d8b70ad2cd2..24012b239eb0 100644\n--- a/src/layer/meson.build\n+++ b/src/layer/meson.build\n@@ -14,3 +14,4 @@ layers_env.set('LIBCAMERA_LAYER_PATH', meson.current_build_dir())\n meson.add_devenv(layers_env)\n \n subdir('inject_controls')\n+subdir('sync')\ndiff --git a/src/layer/sync/meson.build b/src/layer/sync/meson.build\nnew file mode 100644\nindex 000000000000..acee5bef7aba\n--- /dev/null\n+++ b/src/layer/sync/meson.build\n@@ -0,0 +1,15 @@\n+# SPDX-License-Identifier: CC0-1.0\n+\n+layer_name = 'sync'\n+\n+sync_sources = files([\n+    'sync.cpp',\n+])\n+\n+mod = shared_module(layer_name, [sync_sources, libcamera_internal_headers],\n+                    name_prefix : '',\n+                    include_directories : layer_includes,\n+                    dependencies : libcamera_private,\n+                    gnu_symbol_visibility: 'hidden',\n+                    install : true,\n+                    install_dir : layer_install_dir)\ndiff --git a/src/layer/sync/sync.cpp b/src/layer/sync/sync.cpp\nnew file mode 100644\nindex 000000000000..7e19ab72c043\n--- /dev/null\n+++ b/src/layer/sync/sync.cpp\n@@ -0,0 +1,463 @@\n+/* SPDX-License-Identifier: BSD-2-Clause */\n+/*\n+ * Copyright (C) 2024, Raspberry Pi Ltd\n+ * Copyright (C) 2025, Ideas On Board Oy\n+ *\n+ * Layer implementation for sync algorithm\n+ */\n+\n+#include \"sync.h\"\n+\n+#include <arpa/inet.h>\n+#include <chrono>\n+#include <fcntl.h>\n+#include <netinet/ip.h>\n+#include <string.h>\n+#include <string_view>\n+#include <sys/socket.h>\n+#include <unistd.h>\n+\n+#include <libcamera/base/log.h>\n+#include <libcamera/base/unique_fd.h>\n+#include <libcamera/base/utils.h>\n+\n+#include <libcamera/control_ids.h>\n+#include <libcamera/layer.h>\n+\n+namespace libcamera {\n+\n+LOG_DEFINE_CATEGORY(SyncLayer)\n+\n+} /* namespace libcamera */\n+\n+using namespace libcamera;\n+using namespace std::chrono_literals;\n+\n+void *init([[maybe_unused]] const std::string &id)\n+{\n+\tSyncLayerData *data = new SyncLayerData;\n+\n+\tLOG(SyncLayer, Info) << \"Initializing sync layer\";\n+\n+\tconst char *kDefaultGroup = \"239.255.255.250\";\n+\tconstexpr unsigned int kDefaultPort = 10000;\n+\tconstexpr unsigned int kDefaultSyncPeriod = 30;\n+\tconstexpr unsigned int kDefaultReadyFrame = 100;\n+\tconstexpr unsigned int kDefaultMinAdjustment = 50;\n+\n+\t/* \\todo load these from configuration file */\n+\tdata->group = kDefaultGroup;\n+\tdata->port = kDefaultPort;\n+\tdata->syncPeriod = kDefaultSyncPeriod;\n+\tdata->readyFrame = kDefaultReadyFrame;\n+\tdata->minAdjustment = kDefaultMinAdjustment;\n+\n+\treturn data;\n+}\n+\n+void terminate(void *closure)\n+{\n+\tSyncLayerData *data = static_cast<SyncLayerData *>(closure);\n+\n+\tdata->socket.reset();\n+\n+\tdelete data;\n+}\n+\n+ControlList requestCompleted(void *closure, Request *request)\n+{\n+\tSyncLayerData *data = static_cast<SyncLayerData *>(closure);\n+\n+\tconst ControlList &metadata = request->metadata();\n+\tControlList ret;\n+\n+\t/* SensorTimestamp is required for sync */\n+\t/* \\todo Document this requirement (along with SyncAdjustment) */\n+\tauto sensorTimestamp = metadata.get<int64_t>(controls::SensorTimestamp);\n+\tif (sensorTimestamp) {\n+\t\tdata->clockRecovery.addSample();\n+\t\tuint64_t frameWallClock = data->clockRecovery.getOutput(*sensorTimestamp);\n+\t\tret.set(controls::FrameWallClock, static_cast<int64_t>(frameWallClock));\n+\t\tif (data->mode != Mode::Off && data->frameDuration)\n+\t\t\tprocessFrame(data, frameWallClock, ret);\n+\t}\n+\n+\treturn ret;\n+}\n+\n+ControlInfoMap::Map updateControls(void *closure, ControlInfoMap &controls)\n+{\n+\tSyncLayerData *data = static_cast<SyncLayerData *>(closure);\n+\n+\t/*\n+\t * If the SyncAdjustment control is unavailable then the Camera does\n+\t * not support Sync adjustment\n+\t */\n+\tauto it = controls.find(&controls::SyncAdjustment);\n+\tdata->syncAvailable = it != controls.end();\n+\tif (!data->syncAvailable) {\n+\t\tLOG(SyncLayer, Warning)\n+\t\t\t<< \"Sync layer is not supported: SyncAdjustment control is not available\";\n+\t\treturn {};\n+\t}\n+\n+\t/*\n+\t * Save the default FrameDurationLimits. If it's not available then we\n+\t * have to wait until one is supplied in a request. We cannot use the\n+\t * FrameDuration returned from the first frame as the\n+\t * FrameDurationLimits has to be explicitly set by the application, as\n+\t * this is the frame rate target that the cameras will sync to.\n+\t * \\todo Document that FrameDurationLimits is a required control\n+\t */\n+\tit = controls.find(&controls::FrameDurationLimits);\n+\tif (it != controls.end())\n+\t\tdata->frameDuration = std::chrono::microseconds(it->second.min().get<int64_t>());\n+\n+\treturn {\n+\t\t{ &controls::rpi::SyncMode,\n+\t\t  ControlInfo(controls::rpi::SyncModeValues) },\n+\t\t{ &controls::rpi::SyncFrames,\n+\t\t  ControlInfo(1, 1000000, 100) },\n+\t\t{ &controls::SyncInterface,\n+\t\t  ControlInfo(1, 40) }\n+\t};\n+}\n+\n+void queueRequest(void *closure, Request *request)\n+{\n+\tSyncLayerData *data = static_cast<SyncLayerData *>(closure);\n+\tif (!data->syncAvailable)\n+\t\treturn;\n+\n+\tprocessControls(data, request->controls());\n+\n+\tif (data->mode == Mode::Client) {\n+\t\trequest->controls().set(controls::SyncAdjustment,\n+\t\t\t\t\tdata->frameDurationOffset.count() / 1000);\n+\t\tdata->frameDurationOffset = utils::Duration(0);\n+\t}\n+}\n+\n+void start(void *closure, ControlList &controls)\n+{\n+\tSyncLayerData *data = static_cast<SyncLayerData *>(closure);\n+\tif (!data->syncAvailable)\n+\t\treturn;\n+\n+\treset(data);\n+\tdata->clockRecovery.addSample();\n+\tprocessControls(data, controls);\n+}\n+\n+void reset(SyncLayerData *data)\n+{\n+\tdata->syncReady = false;\n+\tdata->frameCount = 0;\n+\tdata->serverFrameCountPeriod = 0;\n+\tdata->serverReadyTime = 0;\n+\tdata->clientSeenPacket = false;\n+}\n+\n+void initializeSocket(SyncLayerData *data)\n+{\n+\tssize_t ret;\n+\tstruct ip_mreq mreq{};\n+\tsocklen_t addrlen;\n+\tSyncPayload payload;\n+\tunsigned int en = 1;\n+\n+\tif (data->socket.isValid()) {\n+\t\tLOG(SyncLayer, Debug)\n+\t\t\t<< \"Socket already exists; not recreating\";\n+\t\treturn;\n+\t}\n+\n+\tif (!data->networkInterface.empty())\n+\t\tmreq.imr_interface.s_addr = inet_addr(data->networkInterface.c_str());\n+\telse\n+\t\tmreq.imr_interface.s_addr = htonl(INADDR_ANY);\n+\n+\tint flags = SOCK_CLOEXEC;\n+\tif (data->mode == Mode::Client)\n+\t\tflags |= SOCK_NONBLOCK;\n+\n+\tint fd = socket(AF_INET, SOCK_DGRAM | flags, 0);\n+\tif (fd < 0) {\n+\t\tLOG(SyncLayer, Error) << \"Unable to create socket\";\n+\t\treturn;\n+\t}\n+\tdata->socket = UniqueFD(fd);\n+\n+\tstruct sockaddr_in &addr = data->addr;\n+\tmemset(&addr, 0, sizeof(addr));\n+\taddr.sin_family = AF_INET;\n+\taddr.sin_addr.s_addr = data->mode == Mode::Client ? htonl(INADDR_ANY) : inet_addr(data->group.c_str());\n+\taddr.sin_port = htons(data->port);\n+\n+\tif (data->mode != Mode::Client)\n+\t\treturn;\n+\n+\tif (setsockopt(data->socket.get(), SOL_SOCKET, SO_REUSEADDR, &en, sizeof(en)) < 0) {\n+\t\tLOG(SyncLayer, Error) << \"Unable to set socket options: \" << strerror(errno);\n+\t\tgoto err;\n+\t}\n+\n+\tmreq.imr_multiaddr.s_addr = inet_addr(data->group.c_str());\n+\tif (setsockopt(data->socket.get(), IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {\n+\t\tLOG(SyncLayer, Error) << \"Unable to set socket options: \" << strerror(errno);\n+\t\tgoto err;\n+\t}\n+\n+\tif (bind(data->socket.get(), (struct sockaddr *)&addr, sizeof(addr)) < 0) {\n+\t\tLOG(SyncLayer, Error) << \"Unable to bind client socket: \" << strerror(errno);\n+\t\tgoto err;\n+\t}\n+\n+\t/*\n+\t * For the client, flush anything in the socket. It might be stale from a previous sync run,\n+\t * or we might get another packet in a frame to two before the adjustment caused by this (old)\n+\t * packet, although correct, had taken effect. So this keeps things simpler.\n+\t */\n+\taddrlen = sizeof(addr);\n+\tret = 0;\n+\twhile (ret >= 0) {\n+\t\tret = recvfrom(data->socket.get(),\n+\t\t\t       &payload, sizeof(payload), 0,\n+\t\t\t       (struct sockaddr *)&addr, &addrlen);\n+\t}\n+\n+\treturn;\n+\n+err:\n+\tdata->socket.reset();\n+}\n+\n+void processControls(SyncLayerData *data, ControlList &controls)\n+{\n+\tauto intf = controls.get<std::string_view>(controls::SyncInterface);\n+\tif (intf)\n+\t\tdata->networkInterface = *intf;\n+\n+\tauto mode = controls.get<int32_t>(controls::rpi::SyncMode);\n+\tif (mode) {\n+\t\tdata->mode = static_cast<Mode>(*mode);\n+\t\tif (data->mode == Mode::Off) {\n+\t\t\treset(data);\n+\t\t} else {\n+\t\t\t/*\n+\t\t\t * This goes here instead of init() because we need the control\n+\t\t\t * to tell us whether we're server or client\n+\t\t\t */\n+\t\t\tinitializeSocket(data);\n+\t\t}\n+\t}\n+\n+\tauto syncFrames = controls.get<int32_t>(controls::rpi::SyncFrames);\n+\tif (syncFrames && *syncFrames > 0)\n+\t\tdata->readyFrame = *syncFrames;\n+\n+\tauto frameDurationLimits = controls.get(controls::FrameDurationLimits);\n+\tif (frameDurationLimits)\n+\t\tdata->frameDuration = std::chrono::microseconds((*frameDurationLimits)[0]);\n+\n+\t/*\n+\t * \\todo Should we just let SyncAdjustment through as-is if the\n+\t * application provides it? Maybe it wants to do sync itself without\n+\t * the layer, but the layer has been loaded anyway\n+\t */\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 device 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 processFrame(SyncLayerData *data, uint64_t frameWallClock,\n+\t\t  ControlList &metadata)\n+{\n+\t/* frameWallClock has already been de-jittered for us. Convert from ns into us. */\n+\tuint64_t wallClockFrameTimestamp = frameWallClock / 1000;\n+\n+\t/*\n+\t * This is the headline frame duration in microseconds as programmed into the sensor. Strictly,\n+\t * the sensor might not quite match the system clock, but this shouldn't matter for the calculations\n+\t * we'll do with it, unless it's a very very long way out!\n+\t */\n+\tuint32_t frameDuration = data->frameDuration.get<std::micro>();\n+\n+\t/* Timestamps tell us if we've dropped any frames, but we still want to count them. */\n+\tunsigned int droppedFrames = 0;\n+\t/* Count dropped frames into the frame counter */\n+\tif (data->frameCount) {\n+\t\twallClockFrameTimestamp =\n+\t\t\tstd::max<uint64_t>(wallClockFrameTimestamp,\n+\t\t\t\t\t   data->lastWallClockFrameTimestamp + frameDuration / 2);\n+\t\t/*\n+\t\t * Round down here, because data->frameCount gets incremented\n+\t\t * at the end of the function.\n+\t\t */\n+\t\tdroppedFrames =\n+\t\t\t(wallClockFrameTimestamp - data->lastWallClockFrameTimestamp - frameDuration / 2) / frameDuration;\n+\t\tdata->frameCount += droppedFrames;\n+\t}\n+\n+\tif (data->mode == Mode::Server)\n+\t\tprocessFrameServer(data, wallClockFrameTimestamp,\n+\t\t\t\t   metadata, droppedFrames);\n+\telse if (data->mode == Mode::Client)\n+\t\tprocessFrameClient(data, wallClockFrameTimestamp,\n+\t\t\t\t   metadata);\n+\n+\tdata->lastWallClockFrameTimestamp = wallClockFrameTimestamp;\n+\n+\tmetadata.set(controls::rpi::SyncReady, data->syncReady);\n+\n+\tdata->frameCount++;\n+}\n+\n+void processFrameServer(SyncLayerData *data, int64_t wallClockFrameTimestamp,\n+\t\t\tControlList &metadata, unsigned int droppedFrames)\n+{\n+\tuint32_t frameDuration = data->frameDuration.get<std::micro>();\n+\n+\t/*\n+\t * Server sends a packet every syncPeriod frames, or as soon after as possible (if any\n+\t * frames were dropped).\n+\t */\n+\tdata->serverFrameCountPeriod += droppedFrames;\n+\n+\tif (data->frameCount == 0) {\n+\t\tdata->frameDurationEstimated = frameDuration;\n+\t} else {\n+\t\tdouble diff = (wallClockFrameTimestamp - data->lastWallClockFrameTimestamp) / (1 + droppedFrames);\n+\t\tunsigned int N = std::min(data->frameCount, 99U);\n+\t\t/*\n+\t\t * Smooth out the variance of the frame duration estimation to\n+\t\t * get closer to the true frame duration\n+\t\t */\n+\t\tdata->frameDurationEstimated = data->frameCount == 1 ? diff\n+\t\t\t\t\t     : (N * data->frameDurationEstimated + diff) / (N + 1);\n+\t}\n+\n+\t/* Calculate frames remaining, and therefore \"time left until ready\". */\n+\tint framesRemaining = data->readyFrame - data->frameCount;\n+\tuint64_t wallClockReadyTime = wallClockFrameTimestamp + (int64_t)framesRemaining * data->frameDurationEstimated;\n+\n+\tif (data->serverFrameCountPeriod >= data->syncPeriod) {\n+\t\t/* It's time to sync */\n+\t\tdata->serverFrameCountPeriod = 0;\n+\n+\t\tSyncPayload payload;\n+\t\t/* round to nearest us */\n+\t\tpayload.frameDuration = data->frameDurationEstimated + .5;\n+\t\tpayload.wallClockFrameTimestamp = wallClockFrameTimestamp;\n+\t\tpayload.wallClockReadyTime = wallClockReadyTime;\n+\n+\t\tLOG(SyncLayer, Debug) << \"Send packet (frameNumber \" << data->frameCount << \"):\";\n+\t\tLOG(SyncLayer, Debug) << \"            frameDuration \" << payload.frameDuration;\n+\t\tLOG(SyncLayer, Debug) << \"            wallClockFrameTimestamp \" << wallClockFrameTimestamp\n+\t\t\t\t      << \" (\" << wallClockFrameTimestamp - data->lastWallClockFrameTimestamp << \")\";\n+\t\tLOG(SyncLayer, Debug) << \"            wallClockReadyTime \" << wallClockReadyTime;\n+\n+\t\tif (sendto(data->socket.get(), &payload, sizeof(payload), 0, (const sockaddr *)&data->addr, sizeof(data->addr)) < 0)\n+\t\t\tLOG(SyncLayer, Error) << \"Send error! \" << strerror(errno);\n+\t}\n+\n+\tint64_t timerValue = static_cast<int64_t>(wallClockReadyTime - wallClockFrameTimestamp);\n+\tif (!data->syncReady && wallClockFrameTimestamp + data->frameDurationEstimated / 2 > wallClockReadyTime) {\n+\t\tdata->syncReady = true;\n+\t\tLOG(SyncLayer, Info) << \"*** Sync achieved! Difference \" << timerValue << \"us\";\n+\t}\n+\n+\t/* Server always reports this */\n+\tmetadata.set(controls::rpi::SyncTimer, timerValue);\n+\n+\tdata->serverFrameCountPeriod += 1;\n+}\n+\n+void processFrameClient(SyncLayerData *data, int64_t wallClockFrameTimestamp,\n+\t\t\tControlList &metadata)\n+{\n+\tuint64_t serverFrameTimestamp = 0;\n+\tSyncPayload payload;\n+\n+\tbool packetReceived = false;\n+\twhile (true) {\n+\t\tssize_t ret = recv(data->socket.get(), &payload, sizeof(payload), 0);\n+\n+\t\tif (ret != sizeof(payload))\n+\t\t\tbreak;\n+\t\tpacketReceived = true;\n+\t\tdata->clientSeenPacket = true;\n+\n+\t\tdata->frameDurationEstimated = payload.frameDuration;\n+\t\tserverFrameTimestamp = payload.wallClockFrameTimestamp;\n+\t\tdata->serverReadyTime = payload.wallClockReadyTime;\n+\t}\n+\n+\tif (packetReceived) {\n+\t\tuint64_t clientFrameTimestamp = wallClockFrameTimestamp;\n+\t\tint64_t clientServerDelta = clientFrameTimestamp - serverFrameTimestamp;\n+\t\t/* \"A few frames ago\" may have better matched the server's frame. Calculate when it was. */\n+\t\tint framePeriodErrors = (clientServerDelta + data->frameDurationEstimated / 2) / data->frameDurationEstimated;\n+\t\tint64_t clientFrameTimestampNearest = clientFrameTimestamp - framePeriodErrors * data->frameDurationEstimated;\n+\t\t/* We must shorten a single client frame by this amount if it exceeds the minimum: */\n+\t\tint32_t correction = clientFrameTimestampNearest - serverFrameTimestamp;\n+\t\tif (std::abs(correction) < data->minAdjustment)\n+\t\t\tcorrection = 0;\n+\n+\t\tLOG(SyncLayer, Debug) << \"Received packet (frameNumber \" << data->frameCount << \"):\";\n+\t\tLOG(SyncLayer, Debug) << \"                serverFrameTimestamp \" << serverFrameTimestamp;\n+\t\tLOG(SyncLayer, Debug) << \"                serverReadyTime \" << data->serverReadyTime;\n+\t\tLOG(SyncLayer, Debug) << \"                clientFrameTimestamp \" << clientFrameTimestamp;\n+\t\tLOG(SyncLayer, Debug) << \"                clientFrameTimestampNearest \" << clientFrameTimestampNearest\n+\t\t\t\t      << \" (\" << framePeriodErrors << \")\";\n+\t\tLOG(SyncLayer, Debug) << \"                correction \" << correction;\n+\n+\t\tdata->frameDurationOffset = correction * 1us;\n+\t}\n+\n+\tint64_t timerValue = static_cast<int64_t>(data->serverReadyTime - wallClockFrameTimestamp);\n+\tif (data->clientSeenPacket && !data->syncReady && wallClockFrameTimestamp + data->frameDurationEstimated / 2 > data->serverReadyTime) {\n+\t\tdata->syncReady = true;\n+\t\tLOG(SyncLayer, Info) << \"*** Sync achieved! Difference \" << timerValue << \"us\";\n+\t}\n+\n+\t/* Client reports this once it receives it from the server */\n+\tif (data->clientSeenPacket)\n+\t\tmetadata.set(controls::rpi::SyncTimer, timerValue);\n+}\n+\n+namespace libcamera {\n+\n+extern \"C\" {\n+\n+[[gnu::visibility(\"default\")]]\n+struct LayerInfo layerInfo{\n+\t.name = \"sync\",\n+\t.layerAPIVersion = 1,\n+};\n+\n+[[gnu::visibility(\"default\")]]\n+struct LayerInterface layerInterface{\n+\t.init = init,\n+\t.terminate = terminate,\n+\t.bufferCompleted = nullptr,\n+\t.requestCompleted = requestCompleted,\n+\t.disconnected = nullptr,\n+\t.acquire = nullptr,\n+\t.release = nullptr,\n+\t.controls = updateControls,\n+\t.properties = nullptr,\n+\t.configure = nullptr,\n+\t.createRequest = nullptr,\n+\t.queueRequest = queueRequest,\n+\t.start = start,\n+\t.stop = nullptr,\n+};\n+}\n+\n+} /* namespace libcamera */\ndiff --git a/src/layer/sync/sync.h b/src/layer/sync/sync.h\nnew file mode 100644\nindex 000000000000..e507b0efc566\n--- /dev/null\n+++ b/src/layer/sync/sync.h\n@@ -0,0 +1,98 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2025, Ideas On Board Oy\n+ *\n+ * Layer implementation for sync algorithm\n+ */\n+\n+#pragma once\n+\n+#include <arpa/inet.h>\n+#include <string>\n+\n+#include <libcamera/base/unique_fd.h>\n+#include <libcamera/base/utils.h>\n+\n+#include <libcamera/controls.h>\n+#include <libcamera/request.h>\n+\n+#include \"libcamera/internal/clock_recovery.h\"\n+\n+enum class Mode {\n+\tOff,\n+\tServer,\n+\tClient,\n+};\n+\n+struct SyncPayload {\n+\t/* Frame duration in microseconds. */\n+\tuint32_t frameDuration;\n+\t/* Server wall clock version of the frame timestamp. */\n+\tuint64_t wallClockFrameTimestamp;\n+\t/* Server wall clock version of the sync time. */\n+\tuint64_t wallClockReadyTime;\n+};\n+\n+struct SyncLayerData {\n+\tbool syncAvailable;\n+\n+\t/* Sync algorithm parameters */\n+\t/* IP group address for sync messages */\n+\tstd::string group;\n+\t/* port number for messages */\n+\tuint16_t port;\n+\t/* send a sync message every this many frames */\n+\tuint32_t syncPeriod;\n+\t/* don't adjust the client frame length by less than this (us) */\n+\tuint32_t minAdjustment;\n+\t/* This is the network interface to listen/multicast on */\n+\tstd::string networkInterface;\n+\n+\t/* Sync algorithm controls */\n+\tMode mode;\n+\tlibcamera::utils::Duration frameDuration;\n+\t/* tell the application we're ready after this many frames */\n+\tuint32_t readyFrame;\n+\n+\t/* Sync algorithm state */\n+\tbool syncReady = false;\n+\tunsigned int frameCount = 0;\n+\t/* send the next packet when this reaches syncPeriod */\n+\tuint32_t serverFrameCountPeriod = 0;\n+\t/* the client's latest value for when the server will be \"ready\" */\n+\tuint64_t serverReadyTime = 0;\n+\t/* whether the client has received a packet yet */\n+\tbool clientSeenPacket = false;\n+\n+\t/* estimate the true frame duration of the sensor (in us) */\n+\tdouble frameDurationEstimated = 0;\n+\t/* wall clock timestamp of previous frame (in us) */\n+\tuint64_t lastWallClockFrameTimestamp;\n+\n+\t/* Frame length correction to apply */\n+\tlibcamera::utils::Duration frameDurationOffset;\n+\n+\t/* Infrastructure state */\n+\tlibcamera::ClockRecovery clockRecovery;\n+\tsockaddr_in addr;\n+\tlibcamera::UniqueFD socket;\n+};\n+\n+void *init(const std::string &id);\n+void terminate(void *closure);\n+libcamera::ControlList requestCompleted(void *closure, libcamera::Request *request);\n+libcamera::ControlInfoMap::Map updateControls(void *closure,\n+\t\t\t\t\t      libcamera::ControlInfoMap &controls);\n+void queueRequest(void *closure, libcamera::Request *request);\n+void start(void *closure, libcamera::ControlList &controls);\n+\n+void reset(SyncLayerData *data);\n+void initializeSocket(SyncLayerData *data);\n+void processControls(SyncLayerData *data, libcamera::ControlList &controls);\n+void processFrame(SyncLayerData *data, uint64_t frameWallClock,\n+\t\t  libcamera::ControlList &metadata);\n+void processFrameServer(SyncLayerData *data, int64_t wallClockFrameTimestamp,\n+\t\t\tlibcamera::ControlList &metadata,\n+\t\t\tunsigned int droppedFrames);\n+void processFrameClient(SyncLayerData *data, int64_t wallClockFrameTimestamp,\n+\t\t\tlibcamera::ControlList &metadata);\n","prefixes":["v2","2/4"]}