diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index d4930589..8a166e64 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -88,6 +88,9 @@
+
+
+
@@ -168,6 +171,9 @@
+
+
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index 4c43e19a..ca867ed0 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -35,6 +35,9 @@
{530EE339-4AD6-4AE7-9A74-09420E73D04B}
+
+ {9B627995-4F7D-4F7D-AE88-AD0AA785880D}
+
{82810F1C-E51C-4DBD-86B2-1CDE8C16A2A9}
@@ -283,6 +286,15 @@
Transport\LGMP
+
+ Transport\Network
+
+
+ Transport\Network
+
+
+ Transport\Network
+
Configuration
@@ -459,6 +471,15 @@
Transport\LGMP
+
+ Transport\Network
+
+
+ Transport\Network
+
+
+ Transport\Network
+
Configuration
diff --git a/idd/LGIdd/display/CDeviceContext.cpp b/idd/LGIdd/display/CDeviceContext.cpp
index f5dfde38..49b66d08 100644
--- a/idd/LGIdd/display/CDeviceContext.cpp
+++ b/idd/LGIdd/display/CDeviceContext.cpp
@@ -649,7 +649,7 @@ bool CDeviceContext::SetupTransport(size_t alignSize)
{
// Frame buffers cannot be allocated until the GPU-specific alignment is
// known. The swap-chain path may call this again after setup completed.
- if (!m_transport->Frames().GetMaxFrameSize())
+ if (!m_transport->SetupReady())
{
if (!InitializeTransport() || !m_transport->Setup(alignSize))
return false;
diff --git a/idd/LGIdd/transport/CTransportManager.cpp b/idd/LGIdd/transport/CTransportManager.cpp
index ae847ddc..d5ecf779 100644
--- a/idd/LGIdd/transport/CTransportManager.cpp
+++ b/idd/LGIdd/transport/CTransportManager.cpp
@@ -1256,6 +1256,44 @@ bool CTransportManager::Setup(size_t alignment)
return success;
}
+bool CTransportManager::SetupReady() const
+{
+ CTransportManager * manager = const_cast(this);
+ if (!manager->BeginPhase(Phase::ACCESS, true))
+ return false;
+
+ Entry * entries[FRAME_MAX_SINKS] = {};
+ Entry * primary = nullptr;
+ unsigned count = 0;
+ bool setup = false;
+ {
+ CSRWSharedLock managerLock(m_lock);
+ primary = m_primary;
+ setup = m_setup && !m_stopping && !m_stopped;
+ count = m_entryCount;
+ for (unsigned i = 0; i < count; ++i)
+ entries[i] = m_entries[i].get();
+ }
+ bool primaryReady = false;
+ bool ready = setup && primary != nullptr;
+ for (unsigned i = 0; ready && i < count; ++i)
+ {
+ Entry& entry = *entries[i];
+ CSRWSharedLock entryLock(entry.lock);
+ if (entry.required &&
+ (entry.state != State::READY || !entry.setupDone))
+ {
+ ready = false;
+ break;
+ }
+ if (&entry == primary)
+ primaryReady = entry.state == State::READY &&
+ entry.setupDone && entry.frameAdded;
+ }
+ manager->EndPhase();
+ return ready && primaryReady;
+}
+
CfgResult CTransportManager::Cfg(const GraphCfg& cfg,
uint64_t revision, CFrameGraph& graph, ITexStage * activation)
{
diff --git a/idd/LGIdd/transport/CTransportManager.h b/idd/LGIdd/transport/CTransportManager.h
index e38de902..d16e1fb1 100644
--- a/idd/LGIdd/transport/CTransportManager.h
+++ b/idd/LGIdd/transport/CTransportManager.h
@@ -195,6 +195,9 @@ public:
OpenResult Open();
bool Initialize();
bool Setup(size_t alignment);
+ // Unlike the legacy frame-size sentinel, this also recognizes a primary
+ // texture route and verifies every required transport completed setup.
+ bool SetupReady() const;
CfgResult Cfg(const GraphCfg& cfg, uint64_t revision,
CFrameGraph& graph, ITexStage * activation = nullptr);
ProcessResult Process(ITransportActions& actions);
diff --git a/idd/LGIdd/transport/TransportFactory.cpp b/idd/LGIdd/transport/TransportFactory.cpp
index 2d864175..cc320569 100644
--- a/idd/LGIdd/transport/TransportFactory.cpp
+++ b/idd/LGIdd/transport/TransportFactory.cpp
@@ -24,6 +24,7 @@
#include "config/CSettings.h"
#include "transport/CTransportManager.h"
#include "transport/lgmp/CLGMPTransport.h"
+#include "transport/network/CNetworkTransport.h"
#include
@@ -41,14 +42,23 @@ namespace
new (std::nothrow) CLGMPTransport(config));
}
+ std::unique_ptr CreateNetwork(
+ const TransportInstance& config)
+ {
+ return std::unique_ptr(
+ new (std::nothrow) CNetworkTransport(config));
+ }
+
const TransportKind KINDS[] =
{
- { L"LGMP", 1 },
+ { L"LGMP", 1 },
+ { L"NETWORK", 1 },
};
const Provider PROVIDERS[] =
{
- { CreateLGMP },
+ { CreateLGMP },
+ { CreateNetwork },
};
static_assert(ARRAYSIZE(KINDS) == ARRAYSIZE(PROVIDERS),
diff --git a/idd/LGIdd/transport/network/CNetworkSession.cpp b/idd/LGIdd/transport/network/CNetworkSession.cpp
new file mode 100644
index 00000000..62b72bb2
--- /dev/null
+++ b/idd/LGIdd/transport/network/CNetworkSession.cpp
@@ -0,0 +1,415 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "transport/network/CNetworkSession.h"
+
+#include
+
+bool CNetworkEventQueue::Push(const NetworkEvent& event)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (m_count == CAPACITY)
+ return false;
+
+ const unsigned tail = (m_head + m_count) % CAPACITY;
+ m_events[tail] = event;
+ ++m_count;
+ return true;
+}
+
+bool CNetworkEventQueue::Take(NetworkEvent& event)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_count)
+ return false;
+
+ event = m_events[m_head];
+ m_events[m_head] = {};
+ m_head = (m_head + 1) % CAPACITY;
+ --m_count;
+ return true;
+}
+
+void CNetworkEventQueue::Reset()
+{
+ CSRWExclusiveLock lock(m_lock);
+ for (NetworkEvent& event : m_events)
+ event = {};
+ m_head = 0;
+ m_count = 0;
+}
+
+bool CNetworkInputSource::Start(IInputTarget& target)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (m_target && m_target != &target)
+ return false;
+ m_target = ⌖
+ return true;
+}
+
+void CNetworkInputSource::Stop()
+{
+ CSRWExclusiveLock lock(m_lock);
+ m_target = nullptr;
+}
+
+InputTargetState CNetworkInputSource::GetState(
+ const InputSourceId& source) const
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ? m_target->GetState(source) : InputTargetState {};
+}
+
+InputResult CNetworkInputSource::Claim(const InputSourceId& source)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ?
+ m_target->Claim(source) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkInputSource::Touch(const InputSourceId& source)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ?
+ m_target->Touch(source) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkInputSource::Release(
+ const InputSourceId& source, bool reset)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ?
+ m_target->Release(source, reset) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkInputSource::SendMouseRelative(
+ const InputSourceId& source, int32_t deltaX, int32_t deltaY,
+ int32_t wheel, uint32_t buttons)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ? m_target->SendMouseRelative(
+ source, deltaX, deltaY, wheel, buttons) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkInputSource::SendMouseAbsolute(
+ const InputSourceId& source, uint16_t x, uint16_t y,
+ int32_t wheel, uint32_t buttons)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ? m_target->SendMouseAbsolute(
+ source, x, y, wheel, buttons) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkInputSource::SendKeyboard(
+ const InputSourceId& source, uint8_t modifiers, const uint8_t * keys)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ? m_target->SendKeyboard(
+ source, modifiers, keys) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkInputSource::Reset(const InputSourceId& source)
+{
+ CSRWSharedLock lock(m_lock);
+ return m_target ?
+ m_target->Reset(source) : InputResult::UNAVAILABLE;
+}
+
+void CNetworkControlSink::SetControlEvents(
+ IControlEvents * events, const ControlToken& token)
+{
+ CSRWExclusiveLock lock(m_lock);
+ m_events = events;
+ m_token = events ? token : ControlToken {};
+}
+
+ControlResult CNetworkControlSink::SendCursor(
+ const IDARG_OUT_QUERY_HWCURSOR& info, const BYTE * data,
+ size_t size, UINT sdrWhiteLevel)
+{
+ const bool shape = info.CursorShapeInfo.CursorType !=
+ IDDCX_CURSOR_SHAPE_TYPE_UNINITIALIZED;
+ if (shape && info.CursorShapeInfo.Height &&
+ info.CursorShapeInfo.Pitch >
+ SIZE_MAX / info.CursorShapeInfo.Height)
+ return ControlResult::FAILED;
+ const size_t shapeSize = shape ? static_cast(
+ info.CursorShapeInfo.Height) * info.CursorShapeInfo.Pitch : 0;
+ if (size != shapeSize || (size && !data))
+ return ControlResult::FAILED;
+
+ std::vector cursorData;
+ if (shapeSize)
+ cursorData.assign(data, data + size);
+
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_events)
+ return ControlResult::RETRY;
+ m_state.cursor.IsCursorVisible = info.IsCursorVisible;
+ m_state.cursor.X = info.X;
+ m_state.cursor.Y = info.Y;
+ m_state.sdrWhiteLevel = sdrWhiteLevel;
+ m_state.cursorValid = true;
+ if (shape)
+ {
+ m_state.cursor.IsCursorShapeUpdated = info.IsCursorShapeUpdated;
+ m_state.cursor.CursorShapeInfo = info.CursorShapeInfo;
+ m_state.cursorData = std::move(cursorData);
+ m_state.shapeValid = true;
+ }
+ ++m_state.revision;
+ return ControlResult::APPLIED;
+}
+
+ControlResult CNetworkControlSink::SetColorTransform(
+ std::shared_ptr transform)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_events)
+ return ControlResult::RETRY;
+ m_state.transform = std::move(transform);
+ m_state.transformValid = true;
+ ++m_state.revision;
+ return ControlResult::APPLIED;
+}
+
+void CNetworkControlSink::RequestReplay()
+{
+ CSRWSharedLock lock(m_lock);
+ if (m_events)
+ m_events->OnControlReplay(m_token);
+}
+
+bool CNetworkControlSink::Snapshot(NetworkControlSnapshot& state) const
+{
+ CSRWSharedLock lock(m_lock);
+ if (!m_events)
+ return false;
+ state = m_state;
+ return true;
+}
+
+CNetworkSession::CNetworkSession(uint32_t client, uint32_t generation,
+ CNetworkEventQueue& events, CNetworkInputSource& input) :
+ m_events(events), m_input(input)
+{
+ m_source.client = client;
+ m_source.generation = generation;
+}
+
+CNetworkSession::~CNetworkSession()
+{
+ Close();
+}
+
+SourceKey CNetworkSession::EventSource() const
+{
+ SourceKey source;
+ source.client = m_source.client;
+ source.generation = m_source.generation;
+ return source;
+}
+
+bool CNetworkSession::IsOpen() const
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open;
+}
+
+void CNetworkSession::Close()
+{
+ CSRWExclusiveLock lock(m_lifecycleLock);
+ if (!m_open)
+ return;
+ m_open = false;
+ m_input.Release(m_source, true);
+}
+
+bool CNetworkSession::SetCursorPosition(int32_t x, int32_t y)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ if (!m_open)
+ return false;
+
+ NetworkEvent event;
+ event.type = NetworkEventType::SET_CURSOR_POSITION;
+ event.source = EventSource();
+ event.x = x;
+ event.y = y;
+ return m_events.Push(event);
+}
+
+bool CNetworkSession::SetResolution(uint32_t width, uint32_t height)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ if (!m_open || !width || !height)
+ return false;
+
+ NetworkEvent event;
+ event.type = NetworkEventType::SET_RESOLUTION;
+ event.source = EventSource();
+ event.width = width;
+ event.height = height;
+ return m_events.Push(event);
+}
+
+bool CNetworkSession::RequestRecovery(
+ uint64_t session, uint32_t serial, bool active)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ if (!m_open || !session || !serial)
+ return false;
+
+ NetworkEvent event;
+ event.type = NetworkEventType::RECOVERY_REQUEST;
+ event.source = EventSource();
+ event.session = session;
+ event.serial = serial;
+ event.active = active;
+ return m_events.Push(event);
+}
+
+void CNetworkSession::ProcessEvent(
+ const NetworkEvent& event, ITransportEvents& events)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ if (!m_open || event.source.client != m_source.client ||
+ event.source.generation != m_source.generation)
+ return;
+
+ switch (event.type)
+ {
+ case NetworkEventType::SET_CURSOR_POSITION:
+ events.OnSetCursorPos(event.source, event.x, event.y);
+ break;
+
+ case NetworkEventType::SET_RESOLUTION:
+ events.OnSetResolution(event.source, event.width, event.height);
+ break;
+
+ case NetworkEventType::RECOVERY_REQUEST:
+ {
+ const RecoveryAdmission admission = events.OnRecoveryRequest(
+ event.source, event.session, event.serial, event.active);
+ if (!admission.complete)
+ break;
+
+ NetworkRecoveryStatus status;
+ status.session = event.session;
+ status.serial = event.serial;
+ status.active = event.active;
+ status.state = admission.state;
+ status.error = admission.error;
+ SetRecoveryStatus(status);
+ break;
+ }
+ }
+}
+
+void CNetworkSession::SetRecoveryStatus(
+ const NetworkRecoveryStatus& status)
+{
+ CSRWExclusiveLock lock(m_recoveryLock);
+ if (m_recoveryCount == RECOVERY_CAPACITY)
+ {
+ m_recovery[m_recoveryHead] = {};
+ m_recoveryHead =
+ (m_recoveryHead + 1) % RECOVERY_CAPACITY;
+ --m_recoveryCount;
+ }
+
+ const unsigned tail =
+ (m_recoveryHead + m_recoveryCount) % RECOVERY_CAPACITY;
+ m_recovery[tail] = status;
+ ++m_recoveryCount;
+}
+
+bool CNetworkSession::TakeRecoveryStatus(NetworkRecoveryStatus& status)
+{
+ CSRWExclusiveLock lock(m_recoveryLock);
+ if (!m_recoveryCount)
+ return false;
+
+ status = m_recovery[m_recoveryHead];
+ m_recovery[m_recoveryHead] = {};
+ m_recoveryHead =
+ (m_recoveryHead + 1) % RECOVERY_CAPACITY;
+ --m_recoveryCount;
+ return true;
+}
+
+InputTargetState CNetworkSession::InputState() const
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ? m_input.GetState(m_source) : InputTargetState {};
+}
+
+InputResult CNetworkSession::ClaimInput()
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ?
+ m_input.Claim(m_source) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkSession::TouchInput()
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ?
+ m_input.Touch(m_source) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkSession::ReleaseInput(bool reset)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ?
+ m_input.Release(m_source, reset) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkSession::SendMouseRelative(
+ int32_t deltaX, int32_t deltaY, int32_t wheel, uint32_t buttons)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ? m_input.SendMouseRelative(
+ m_source, deltaX, deltaY, wheel, buttons) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkSession::SendMouseAbsolute(
+ uint16_t x, uint16_t y, int32_t wheel, uint32_t buttons)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ? m_input.SendMouseAbsolute(
+ m_source, x, y, wheel, buttons) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkSession::SendKeyboard(
+ uint8_t modifiers, const uint8_t * keys)
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ? m_input.SendKeyboard(
+ m_source, modifiers, keys) : InputResult::UNAVAILABLE;
+}
+
+InputResult CNetworkSession::ResetInput()
+{
+ CSRWSharedLock lock(m_lifecycleLock);
+ return m_open ?
+ m_input.Reset(m_source) : InputResult::UNAVAILABLE;
+}
diff --git a/idd/LGIdd/transport/network/CNetworkSession.h b/idd/LGIdd/transport/network/CNetworkSession.h
new file mode 100644
index 00000000..ac72cc30
--- /dev/null
+++ b/idd/LGIdd/transport/network/CNetworkSession.h
@@ -0,0 +1,181 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include "CSRWLock.h"
+#include "transport/IControlSink.h"
+#include "transport/IInputSource.h"
+#include "transport/ITransport.h"
+
+#include
+#include
+
+enum class NetworkEventType : uint8_t
+{
+ SET_CURSOR_POSITION,
+ SET_RESOLUTION,
+ RECOVERY_REQUEST,
+};
+
+struct NetworkEvent
+{
+ NetworkEventType type = NetworkEventType::SET_CURSOR_POSITION;
+ SourceKey source;
+ int32_t x = 0;
+ int32_t y = 0;
+ uint32_t width = 0;
+ uint32_t height = 0;
+ uint64_t session = 0;
+ uint32_t serial = 0;
+ bool active = false;
+};
+
+struct NetworkRecoveryStatus
+{
+ uint64_t session = 0;
+ uint32_t serial = 0;
+ bool active = false;
+ RecoveryState state = RecoveryState::FAILED;
+ uint32_t error = 0;
+};
+
+class CNetworkEventQueue
+{
+private:
+ static constexpr unsigned CAPACITY = 128;
+
+ CSRWLock m_lock;
+ NetworkEvent m_events[CAPACITY] = {};
+ unsigned m_head = 0;
+ unsigned m_count = 0;
+
+public:
+ bool Push(const NetworkEvent& event);
+ bool Take(NetworkEvent& event);
+ void Reset();
+};
+
+class CNetworkInputSource final : public IInputSource
+{
+private:
+ mutable CSRWLock m_lock;
+ IInputTarget * m_target = nullptr;
+
+public:
+ bool Start(IInputTarget& target) override;
+ void Stop() override;
+
+ InputTargetState GetState(const InputSourceId& source) const;
+ InputResult Claim(const InputSourceId& source);
+ InputResult Touch(const InputSourceId& source);
+ InputResult Release(const InputSourceId& source, bool reset);
+ InputResult SendMouseRelative(const InputSourceId& source,
+ int32_t deltaX, int32_t deltaY, int32_t wheel, uint32_t buttons);
+ InputResult SendMouseAbsolute(const InputSourceId& source,
+ uint16_t x, uint16_t y, int32_t wheel, uint32_t buttons);
+ InputResult SendKeyboard(const InputSourceId& source,
+ uint8_t modifiers, const uint8_t * keys);
+ InputResult Reset(const InputSourceId& source);
+};
+
+struct NetworkControlSnapshot
+{
+ IDARG_OUT_QUERY_HWCURSOR cursor = {};
+ std::vector cursorData;
+ std::shared_ptr transform;
+ UINT sdrWhiteLevel = 0;
+ uint64_t revision = 0;
+ bool cursorValid = false;
+ bool shapeValid = false;
+ bool transformValid = false;
+};
+
+class CNetworkControlSink final : public IControlSink
+{
+private:
+ mutable CSRWLock m_lock;
+ IControlEvents * m_events = nullptr;
+ ControlToken m_token;
+ NetworkControlSnapshot m_state;
+
+public:
+ void SetControlEvents(
+ IControlEvents * events, const ControlToken& token) override;
+ ControlResult SendCursor(const IDARG_OUT_QUERY_HWCURSOR& info,
+ const BYTE * data, size_t size, UINT sdrWhiteLevel) override;
+ ControlResult SetColorTransform(
+ std::shared_ptr transform) override;
+
+ void RequestReplay();
+ bool Snapshot(NetworkControlSnapshot& state) const;
+};
+
+// A session is transport-independent state owned by the eventual QUIC
+// provider. Decoded requests enter the bounded event queue or the existing
+// input hub; encoded replies are drained from the bounded status queue.
+class CNetworkSession final
+{
+private:
+ static constexpr unsigned RECOVERY_CAPACITY = 16;
+
+ CNetworkEventQueue& m_events;
+ CNetworkInputSource& m_input;
+ InputSourceId m_source;
+ mutable CSRWLock m_lifecycleLock;
+ bool m_open = true;
+
+ CSRWLock m_recoveryLock;
+ NetworkRecoveryStatus m_recovery[RECOVERY_CAPACITY] = {};
+ unsigned m_recoveryHead = 0;
+ unsigned m_recoveryCount = 0;
+
+ SourceKey EventSource() const;
+
+public:
+ CNetworkSession(uint32_t client, uint32_t generation,
+ CNetworkEventQueue& events, CNetworkInputSource& input);
+ ~CNetworkSession();
+
+ CNetworkSession(const CNetworkSession&) = delete;
+ CNetworkSession& operator=(const CNetworkSession&) = delete;
+
+ InputSourceId Source() const { return m_source; }
+ bool IsOpen() const;
+ void Close();
+
+ bool SetCursorPosition(int32_t x, int32_t y);
+ bool SetResolution(uint32_t width, uint32_t height);
+ bool RequestRecovery(uint64_t session, uint32_t serial, bool active);
+ void ProcessEvent(const NetworkEvent& event, ITransportEvents& events);
+ void SetRecoveryStatus(const NetworkRecoveryStatus& status);
+ bool TakeRecoveryStatus(NetworkRecoveryStatus& status);
+
+ InputTargetState InputState() const;
+ InputResult ClaimInput();
+ InputResult TouchInput();
+ InputResult ReleaseInput(bool reset);
+ InputResult SendMouseRelative(
+ int32_t deltaX, int32_t deltaY, int32_t wheel, uint32_t buttons);
+ InputResult SendMouseAbsolute(
+ uint16_t x, uint16_t y, int32_t wheel, uint32_t buttons);
+ InputResult SendKeyboard(uint8_t modifiers, const uint8_t * keys);
+ InputResult ResetInput();
+};
diff --git a/idd/LGIdd/transport/network/CNetworkTransport.cpp b/idd/LGIdd/transport/network/CNetworkTransport.cpp
new file mode 100644
index 00000000..6682073c
--- /dev/null
+++ b/idd/LGIdd/transport/network/CNetworkTransport.cpp
@@ -0,0 +1,273 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "transport/network/CNetworkTransport.h"
+
+#include "CDebug.h"
+
+#include
+#include
+
+static constexpr unsigned EVENT_DRAIN_LIMIT = 64;
+
+CNetworkTransport::CNetworkTransport(const TransportInstance& config) :
+ m_config(config)
+{
+}
+
+CNetworkTransport::~CNetworkTransport()
+{
+ Stop();
+}
+
+ITransport::OpenResult CNetworkTransport::Open()
+{
+ CSRWExclusiveLock lock(m_lifecycleLock);
+ if (m_state == State::STOPPED)
+ return OpenResult::FAILURE;
+ if (m_state != State::CONSTRUCTED)
+ return OpenResult::SUCCESS;
+
+ NetworkSettings settings;
+ std::wstring error;
+ if (!ParseNetworkSettings(m_config.settings, settings, &error))
+ {
+ DEBUG_ERROR("Invalid network transport settings: %ls", error.c_str());
+ return OpenResult::FAILURE;
+ }
+
+ m_settings = std::move(settings);
+ m_state = State::OPEN;
+ DEBUG_INFO("Network transport configured on [%ls]:%u for up to %u "
+ "clients (%ls authentication)",
+ m_settings.bindAddress.c_str(),
+ static_cast(m_settings.port),
+ static_cast(m_settings.maxClients),
+ m_settings.auth == LG_NET_AUTH_PASSWORD ? L"password" : L"none");
+ return OpenResult::SUCCESS;
+}
+
+bool CNetworkTransport::Initialize()
+{
+ CSRWExclusiveLock lock(m_lifecycleLock);
+ if (m_state == State::INITIALIZED || m_state == State::READY)
+ return true;
+ if (m_state != State::OPEN)
+ return false;
+
+ m_state = State::INITIALIZED;
+ if (m_config.services & TRANSPORT_SERVICE_FRAME)
+ DEBUG_WARN("Network video is unavailable until the GPU codec and "
+ "texture sink are attached");
+ return true;
+}
+
+bool CNetworkTransport::Setup(size_t alignment)
+{
+ CSRWExclusiveLock lock(m_lifecycleLock);
+ if (m_state == State::READY)
+ return m_alignment == alignment;
+ if (m_state != State::INITIALIZED)
+ return false;
+
+ m_alignment = alignment;
+ m_state = State::READY;
+ return true;
+}
+
+ITransport::ProcessResult CNetworkTransport::Process(
+ ITransportEvents& events)
+{
+ {
+ CSRWSharedLock lock(m_lifecycleLock);
+ if (m_state != State::INITIALIZED && m_state != State::READY)
+ return m_state == State::STOPPED ?
+ ProcessResult::FAILURE : ProcessResult::OK;
+ }
+
+ NetworkEvent event;
+ for (unsigned i = 0; i < EVENT_DRAIN_LIMIT && m_events.Take(event); ++i)
+ {
+ const SessionPtr session = FindSession(event.source);
+ if (!session)
+ continue;
+ session->ProcessEvent(event, events);
+ }
+ return ProcessResult::OK;
+}
+
+void CNetworkTransport::Stop()
+{
+ {
+ CSRWExclusiveLock lock(m_lifecycleLock);
+ if (m_state == State::STOPPED)
+ return;
+ m_state = State::STOPPED;
+ }
+
+ std::vector sessions;
+ {
+ CSRWExclusiveLock lock(m_sessionLock);
+ sessions.swap(m_sessions);
+ }
+ for (const SessionPtr& session : sessions)
+ session->Close();
+
+ m_input.Stop();
+ m_control.SetControlEvents(nullptr, {});
+ m_events.Reset();
+}
+
+void CNetworkTransport::RecoveryStatus(const SourceKey& source,
+ uint64_t session, uint32_t serial, bool active,
+ Recovery state, uint32_t error)
+{
+ const SessionPtr target = FindSession(source);
+ if (!target)
+ return;
+
+ NetworkRecoveryStatus status;
+ status.session = session;
+ status.serial = serial;
+ status.active = active;
+ status.state = state;
+ status.error = error;
+ target->SetRecoveryStatus(status);
+}
+
+const FrameProfile * CNetworkTransport::Profiles(unsigned& count) const
+{
+ count = 0;
+ return nullptr;
+}
+
+CfgResult CNetworkTransport::Probe(const FrameCfg& cfg) const
+{
+ UNREFERENCED_PARAMETER(cfg);
+ return CfgResult::REJECTED;
+}
+
+CfgResult CNetworkTransport::Prepare(const FrameCfg& cfg)
+{
+ UNREFERENCED_PARAMETER(cfg);
+ return CfgResult::REJECTED;
+}
+
+void CNetworkTransport::Commit()
+{
+}
+
+void CNetworkTransport::Abort()
+{
+}
+
+std::shared_ptr CNetworkTransport::GetFrameCaps() const
+{
+ return std::shared_ptr();
+}
+
+DirectFrameBufferMemory CNetworkTransport::GetDirectMemory() const
+{
+ return DirectFrameBufferMemory {};
+}
+
+IControlSink * CNetworkTransport::Control()
+{
+ return (m_config.services & TRANSPORT_SERVICE_CONTROL) ?
+ &m_control : nullptr;
+}
+
+IInputSource * CNetworkTransport::Input()
+{
+ return (m_config.services & TRANSPORT_SERVICE_INPUT) ? &m_input : nullptr;
+}
+
+bool CNetworkTransport::ControlSnapshot(
+ NetworkControlSnapshot& state) const
+{
+ return m_control.Snapshot(state);
+}
+
+std::shared_ptr CNetworkTransport::CreateSession(
+ uint32_t client, uint32_t generation)
+{
+ if (!client || !generation)
+ return std::shared_ptr();
+
+ SessionPtr created;
+ {
+ CSRWSharedLock lifecycleLock(m_lifecycleLock);
+ if (m_state != State::READY)
+ return std::shared_ptr();
+
+ CSRWExclusiveLock lock(m_sessionLock);
+ if (m_sessions.size() >= m_settings.maxClients)
+ return std::shared_ptr();
+ for (const SessionPtr& existing : m_sessions)
+ {
+ const InputSourceId source = existing->Source();
+ if (source.client == client && source.generation == generation)
+ return std::shared_ptr();
+ }
+
+ created.reset(
+ new (std::nothrow) CNetworkSession(
+ client, generation, m_events, m_input));
+ if (!created)
+ return std::shared_ptr();
+ m_sessions.push_back(created);
+ }
+ m_control.RequestReplay();
+ return created;
+}
+
+void CNetworkTransport::RemoveSession(
+ uint32_t client, uint32_t generation)
+{
+ SessionPtr removed;
+ {
+ CSRWExclusiveLock lock(m_sessionLock);
+ for (auto it = m_sessions.begin(); it != m_sessions.end(); ++it)
+ {
+ const InputSourceId source = (*it)->Source();
+ if (source.client != client || source.generation != generation)
+ continue;
+ removed = *it;
+ m_sessions.erase(it);
+ break;
+ }
+ }
+ if (removed)
+ removed->Close();
+}
+
+std::shared_ptr CNetworkTransport::FindSession(
+ const SourceKey& source) const
+{
+ CSRWSharedLock lock(m_sessionLock);
+ for (const SessionPtr& session : m_sessions)
+ {
+ const InputSourceId candidate = session->Source();
+ if (candidate.client == source.client &&
+ candidate.generation == source.generation)
+ return session;
+ }
+ return std::shared_ptr();
+}
diff --git a/idd/LGIdd/transport/network/CNetworkTransport.h b/idd/LGIdd/transport/network/CNetworkTransport.h
new file mode 100644
index 00000000..e52efc20
--- /dev/null
+++ b/idd/LGIdd/transport/network/CNetworkTransport.h
@@ -0,0 +1,100 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include "CSRWLock.h"
+#include "transport/ITransport.h"
+#include "transport/network/CNetworkSession.h"
+#include "transport/network/NetworkSettings.h"
+
+#include
+#include
+
+// This provider owns transport-independent lifecycle and session state. It
+// deliberately advertises no frame capabilities and no texture sink until a
+// GPU codec backend is attached; it must not be mistaken for working media.
+class CNetworkTransport final : public ITransport
+{
+private:
+ using SessionPtr = std::shared_ptr;
+
+ enum class State
+ {
+ CONSTRUCTED,
+ OPEN,
+ INITIALIZED,
+ READY,
+ STOPPED,
+ };
+
+ TransportInstance m_config;
+ NetworkSettings m_settings;
+
+ mutable CSRWLock m_lifecycleLock;
+ State m_state = State::CONSTRUCTED;
+ size_t m_alignment = 0;
+
+ CNetworkEventQueue m_events;
+ CNetworkInputSource m_input;
+ CNetworkControlSink m_control;
+
+ mutable CSRWLock m_sessionLock;
+ std::vector m_sessions;
+
+ std::shared_ptr FindSession(
+ const SourceKey& source) const;
+
+public:
+ explicit CNetworkTransport(const TransportInstance& config);
+ ~CNetworkTransport() override;
+
+ CNetworkTransport(const CNetworkTransport&) = delete;
+ CNetworkTransport& operator=(const CNetworkTransport&) = delete;
+
+ OpenResult Open() override;
+ bool Initialize() override;
+ bool Setup(size_t alignment) override;
+ ProcessResult Process(ITransportEvents& events) override;
+ void Stop() override;
+ void RecoveryStatus(const SourceKey& source,
+ uint64_t session, uint32_t serial, bool active,
+ Recovery state, uint32_t error) override;
+
+ const FrameProfile * Profiles(unsigned& count) const override;
+ CfgResult Probe(const FrameCfg& cfg) const override;
+ CfgResult Prepare(const FrameCfg& cfg) override;
+ void Commit() override;
+ void Abort() override;
+
+ std::shared_ptr GetFrameCaps() const override;
+ DirectFrameBufferMemory GetDirectMemory() const override;
+ IControlSink * Control() override;
+ IInputSource * Input() override;
+
+ const NetworkSettings& Settings() const { return m_settings; }
+ bool ControlSnapshot(NetworkControlSnapshot& state) const;
+
+ // The QUIC provider calls these only after authenticating a peer. Keeping
+ // the registry here makes Stop a barrier for session-originated callbacks.
+ std::shared_ptr CreateSession(
+ uint32_t client, uint32_t generation);
+ void RemoveSession(uint32_t client, uint32_t generation);
+};
diff --git a/idd/LGIdd/transport/network/NetworkSettings.cpp b/idd/LGIdd/transport/network/NetworkSettings.cpp
new file mode 100644
index 00000000..217e6959
--- /dev/null
+++ b/idd/LGIdd/transport/network/NetworkSettings.cpp
@@ -0,0 +1,194 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "transport/network/NetworkSettings.h"
+
+#include
+#include
+#include
+#include
+
+namespace
+{
+ enum Field : unsigned
+ {
+ FIELD_BIND = 1U << 0,
+ FIELD_PORT = 1U << 1,
+ FIELD_MAX_CLIENTS = 1U << 2,
+ FIELD_AUTH = 1U << 3,
+ FIELD_PASSWORD = 1U << 4,
+ };
+
+ std::wstring Trim(const std::wstring& value)
+ {
+ size_t begin = 0;
+ size_t end = value.size();
+ while (begin < end && iswspace(value[begin]))
+ ++begin;
+ while (end > begin && iswspace(value[end - 1]))
+ --end;
+ return value.substr(begin, end - begin);
+ }
+
+ bool Equal(const std::wstring& left, const wchar_t * right)
+ {
+ if (!right)
+ return false;
+
+ size_t index = 0;
+ for (; index < left.size() && right[index]; ++index)
+ if (towlower(left[index]) != towlower(right[index]))
+ return false;
+ return index == left.size() && !right[index];
+ }
+
+ bool Fail(std::wstring * error, const wchar_t * message)
+ {
+ if (error)
+ *error = message;
+ return false;
+ }
+
+ bool ValidText(const std::wstring& value, size_t maximum)
+ {
+ if (value.empty() || value.size() > maximum)
+ return false;
+ for (wchar_t character : value)
+ if (iswcntrl(character))
+ return false;
+ return true;
+ }
+
+ bool ParseUnsigned(const std::wstring& value,
+ unsigned long maximum, unsigned long& result)
+ {
+ if (value.empty() || value[0] == L'-')
+ return false;
+
+ wchar_t * end = nullptr;
+ errno = 0;
+ const unsigned long parsed = wcstoul(value.c_str(), &end, 10);
+ if (errno == ERANGE || !end || *end || !parsed || parsed > maximum)
+ return false;
+ result = parsed;
+ return true;
+ }
+
+ bool SetField(const std::wstring& key, const std::wstring& rawValue,
+ NetworkSettings& settings, unsigned& fields,
+ std::wstring * error)
+ {
+ const std::wstring value = Trim(rawValue);
+ unsigned field = 0;
+ if (Equal(key, L"bind"))
+ {
+ field = FIELD_BIND;
+ if (!ValidText(value, 255))
+ return Fail(error, L"invalid bind address");
+ settings.bindAddress = value;
+ }
+ else if (Equal(key, L"port"))
+ {
+ field = FIELD_PORT;
+ unsigned long parsed = 0;
+ if (!ParseUnsigned(value, UINT16_MAX, parsed))
+ return Fail(error, L"invalid port");
+ settings.port = static_cast(parsed);
+ }
+ else if (Equal(key, L"maxClients"))
+ {
+ field = FIELD_MAX_CLIENTS;
+ unsigned long parsed = 0;
+ if (!ParseUnsigned(value, NETWORK_MAX_CLIENTS, parsed))
+ return Fail(error, L"invalid maximum client count");
+ settings.maxClients = static_cast(parsed);
+ }
+ else if (Equal(key, L"auth"))
+ {
+ field = FIELD_AUTH;
+ if (Equal(value, L"none"))
+ settings.auth = LG_NET_AUTH_NONE;
+ else if (Equal(value, L"password"))
+ settings.auth = LG_NET_AUTH_PASSWORD;
+ else
+ return Fail(error, L"invalid authentication mode");
+ }
+ else if (Equal(key, L"password"))
+ {
+ field = FIELD_PASSWORD;
+ if (!ValidText(value, 255))
+ return Fail(error, L"invalid password");
+ settings.password = value;
+ }
+ else
+ return Fail(error, L"unknown setting");
+
+ if (fields & field)
+ return Fail(error, L"duplicate setting");
+ fields |= field;
+ return true;
+ }
+}
+
+bool ParseNetworkSettings(const std::wstring& source,
+ NetworkSettings& settings, std::wstring * error)
+{
+ NetworkSettings parsed;
+ if (error)
+ error->clear();
+
+ const std::wstring input = Trim(source);
+ if (input.empty())
+ {
+ settings = std::move(parsed);
+ return true;
+ }
+ if (input.size() > 4096)
+ return Fail(error, L"settings are too long");
+
+ unsigned fields = 0;
+ size_t begin = 0;
+ while (begin < input.size())
+ {
+ const size_t separator = input.find(L';', begin);
+ const size_t end = separator == std::wstring::npos ?
+ input.size() : separator;
+ const std::wstring field = input.substr(begin, end - begin);
+ const size_t equals = field.find(L'=');
+ if (equals == std::wstring::npos ||
+ !SetField(Trim(field.substr(0, equals)),
+ field.substr(equals + 1), parsed, fields, error))
+ return false;
+
+ if (separator == std::wstring::npos)
+ break;
+ begin = separator + 1;
+ if (begin == input.size())
+ return Fail(error, L"empty setting");
+ }
+
+ if (parsed.auth == LG_NET_AUTH_PASSWORD && parsed.password.empty())
+ return Fail(error, L"password authentication requires a password");
+ if (parsed.auth == LG_NET_AUTH_NONE && !parsed.password.empty())
+ return Fail(error, L"password requires password authentication");
+
+ settings = std::move(parsed);
+ return true;
+}
diff --git a/idd/LGIdd/transport/network/NetworkSettings.h b/idd/LGIdd/transport/network/NetworkSettings.h
new file mode 100644
index 00000000..7aa37ed7
--- /dev/null
+++ b/idd/LGIdd/transport/network/NetworkSettings.h
@@ -0,0 +1,45 @@
+/**
+ * Looking Glass
+ * Copyright © 2017-2026 The Looking Glass Authors
+ * https://looking-glass.io
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 59
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#pragma once
+
+#include "common/network/protocol.h"
+
+#include
+#include
+
+static constexpr uint16_t NETWORK_DEFAULT_MAX_CLIENTS = 4;
+static constexpr uint16_t NETWORK_MAX_CLIENTS = 32;
+
+struct NetworkSettings
+{
+ std::wstring bindAddress = L"::";
+ uint16_t port = static_cast(LG_NET_DEFAULT_PORT);
+ uint16_t maxClients = NETWORK_DEFAULT_MAX_CLIENTS;
+ LGNetAuthMode auth = LG_NET_AUTH_MODE_DEFAULT;
+ std::wstring password;
+};
+
+// Settings use a semicolon-separated key=value list. Supported keys are
+// bind, port, maxClients, auth, and password. Password authentication requires
+// a non-empty password. The default address requests a dual-stack wildcard
+// listener from the eventual QUIC provider.
+bool ParseNetworkSettings(const std::wstring& source,
+ NetworkSettings& settings, std::wstring * error = nullptr);