From 5dcfad9bb0953eec0ddcb63d9a9afc323e6bcc71 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 30 Sep 2026 14:29:33 +1000 Subject: [PATCH] [idd] network: add transport session foundation --- idd/LGIdd/LGIdd.vcxproj | 6 + idd/LGIdd/LGIdd.vcxproj.filters | 21 + idd/LGIdd/display/CDeviceContext.cpp | 2 +- idd/LGIdd/transport/CTransportManager.cpp | 38 ++ idd/LGIdd/transport/CTransportManager.h | 3 + idd/LGIdd/transport/TransportFactory.cpp | 14 +- .../transport/network/CNetworkSession.cpp | 415 ++++++++++++++++++ idd/LGIdd/transport/network/CNetworkSession.h | 181 ++++++++ .../transport/network/CNetworkTransport.cpp | 273 ++++++++++++ .../transport/network/CNetworkTransport.h | 100 +++++ .../transport/network/NetworkSettings.cpp | 194 ++++++++ idd/LGIdd/transport/network/NetworkSettings.h | 45 ++ 12 files changed, 1289 insertions(+), 3 deletions(-) create mode 100644 idd/LGIdd/transport/network/CNetworkSession.cpp create mode 100644 idd/LGIdd/transport/network/CNetworkSession.h create mode 100644 idd/LGIdd/transport/network/CNetworkTransport.cpp create mode 100644 idd/LGIdd/transport/network/CNetworkTransport.h create mode 100644 idd/LGIdd/transport/network/NetworkSettings.cpp create mode 100644 idd/LGIdd/transport/network/NetworkSettings.h 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);