[idd] clipboard: add direct synchronization

This commit is contained in:
Geoffrey McRae
2026-08-14 06:26:35 +10:00
parent a8d531b797
commit b786167f72
35 changed files with 5956 additions and 79 deletions

View File

@@ -0,0 +1,380 @@
/**
* 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/CClipboardHub.h"
#include "Seq.h"
namespace
{
bool ValidHelperDirection(const KVMFRClipboardMessage& record)
{
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
return kvmfrClipboardTransferFromHelper(record.transfer);
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
return kvmfrClipboardTransferFromClient(record.transfer);
return true;
}
}
CClipboardHub::CClipboardHub(CClipboardChannel& channel) :
m_channel(channel)
{
m_channel.SetHandler(this);
}
CClipboardHub::~CClipboardHub()
{
Stop();
}
void CClipboardHub::AdvanceGenerationNL()
{
Seq::Inc(m_generation);
}
bool CClipboardHub::MarkFailed(IClipboardSource& source)
{
CSRWExclusiveLock lock(m_lock);
if (m_source != &source || (!m_active && !m_reserved))
return false;
const bool changed = !m_failed;
m_failed = true;
m_failurePending = true;
return changed;
}
ClipboardChannelResult CClipboardHub::HoldOrDiscard(
const KVMFRClipboardMessage& record)
{
// Preserve the pending clipboard publication until a source is rebound.
// Transfer-scoped records belong to the source which admitted them and
// must never cross a transport generation.
if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR)
return ClipboardChannelResult::BUSY;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
{
KVMFRClipboardMessage cancel = {};
cancel.version = KVMFR_CLIPBOARD_VERSION;
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.generation = record.generation;
cancel.clipboardGeneration = record.clipboardGeneration;
cancel.transfer = record.transfer;
cancel.format = record.format;
cancel.token = ERROR_DEVICE_NOT_CONNECTED;
const ClipboardChannelResult result = m_channel.Send(cancel);
if (result != ClipboardChannelResult::ACCEPTED)
return result;
}
return ClipboardChannelResult::ACCEPTED;
}
bool CClipboardHub::Bind(
BackendId backend, uint32_t epoch, IClipboardSource& source)
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
if (!backend || !epoch)
return false;
bool available;
uint32_t generation;
uint64_t channelEpoch;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped || m_source || m_active || m_reserved)
return false;
AdvanceGenerationNL();
m_source = &source;
m_backend = backend;
m_epoch = epoch;
m_active = true;
m_reserved = true;
m_failed = false;
m_failurePending = false;
available = m_available;
generation = m_generation;
channelEpoch = m_channelEpoch;
}
CSRWExclusiveLock callbackLock(m_callbackLock);
const bool started = source.Start(*this);
bool attached = false;
{
CSRWExclusiveLock lock(m_lock);
if (started && m_source == &source && m_backend == backend &&
m_epoch == epoch && m_active && m_reserved && !m_failed &&
!m_stopped)
{
m_running = true;
m_reserved = false;
available = m_available;
generation = m_generation;
channelEpoch = m_channelEpoch;
attached = true;
}
else
{
m_active = false;
m_running = false;
}
}
if (!attached)
{
source.Stop();
CSRWExclusiveLock lock(m_lock);
if (m_source == &source && m_backend == backend && m_epoch == epoch)
{
m_source = nullptr;
m_backend = 0;
m_epoch = 0;
m_active = false;
m_running = false;
m_reserved = false;
m_failed = false;
m_failurePending = false;
}
return false;
}
source.ClipboardState(available, generation);
callbackLock.Unlock();
if (available && channelEpoch)
m_channel.Kick(channelEpoch);
return true;
}
void CClipboardHub::Unbind(BackendId backend, uint32_t epoch)
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
IClipboardSource * source = nullptr;
bool stop = false;
{
CSRWExclusiveLock lock(m_lock);
if ((!m_active && !m_failed && !m_reserved) ||
m_backend != backend || m_epoch != epoch || !m_source)
return;
source = m_source;
stop = m_running || m_reserved;
m_active = false;
m_running = false;
m_reserved = true;
m_failed = false;
m_failurePending = false;
AdvanceGenerationNL();
}
CSRWExclusiveLock callbackLock(m_callbackLock);
if (stop)
source->Stop();
CSRWExclusiveLock lock(m_lock);
if (m_source == source && m_backend == backend && m_epoch == epoch)
{
m_source = nullptr;
m_backend = 0;
m_epoch = 0;
m_active = false;
m_running = false;
m_reserved = false;
m_failed = false;
m_failurePending = false;
}
}
bool CClipboardHub::TakeFailure(SourceKey& source)
{
CSRWSharedLock lifecycleLock(m_lifecycleLock);
CSRWExclusiveLock lock(m_lock);
if (!m_failurePending)
return false;
source = {};
source.backend = m_backend;
source.epoch = m_epoch;
m_failurePending = false;
return true;
}
void CClipboardHub::Stop()
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
IClipboardSource * source = nullptr;
bool stop = false;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped)
return;
m_stopped = true;
source = m_source;
stop = source && (m_running || m_reserved);
m_active = false;
m_running = false;
m_reserved = source != nullptr;
m_failed = false;
m_failurePending = false;
AdvanceGenerationNL();
}
// Clearing the channel handler is a callback-quiescence barrier. It must
// precede taking m_callbackLock because an in-flight channel callback owns
// the channel's handler lock while waiting for m_callbackLock.
m_channel.ClearHandler(this);
CSRWExclusiveLock callbackLock(m_callbackLock);
if (stop)
source->Stop();
CSRWExclusiveLock lock(m_lock);
m_source = nullptr;
m_backend = 0;
m_epoch = 0;
m_active = false;
m_running = false;
m_reserved = false;
m_failed = false;
m_failurePending = false;
}
void CClipboardHub::ClipboardState(bool available, uint64_t epoch)
{
CSRWExclusiveLock callbackLock(m_callbackLock);
IClipboardSource * source = nullptr;
uint32_t generation;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped)
return;
if (m_available != available || m_channelEpoch != epoch)
AdvanceGenerationNL();
m_available = available;
m_channelEpoch = epoch;
generation = m_generation;
if (m_active && m_running && !m_failed)
source = m_source;
}
if (source)
source->ClipboardState(available, generation);
}
ClipboardChannelResult CClipboardHub::ClipboardRecord(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (!ValidHelperDirection(record))
return ClipboardChannelResult::FAILED;
CSRWExclusiveLock callbackLock(m_callbackLock);
IClipboardSource * source = nullptr;
uint32_t generation;
{
CSRWSharedLock lock(m_lock);
if (m_stopped || !m_available)
return ClipboardChannelResult::ACCEPTED;
if (m_failed || !m_active || !m_running || !m_source)
return HoldOrDiscard(record);
source = m_source;
generation = m_generation;
}
KVMFRClipboardMessage stamped = record;
stamped.generation = generation;
const ClipboardChannelResult result =
source->SendClipboard(stamped, data);
if (result != ClipboardChannelResult::FAILED)
return result;
if (MarkFailed(*source))
{
const uint64_t epoch = m_channel.Epoch();
if (epoch)
m_channel.Kick(epoch);
}
return HoldOrDiscard(record);
}
void CClipboardHub::ClipboardReset(uint64_t epoch, uint32_t reason)
{
CSRWExclusiveLock callbackLock(m_callbackLock);
IClipboardSource * source = nullptr;
uint32_t generation;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped || m_channelEpoch != epoch)
return;
AdvanceGenerationNL();
generation = m_generation;
if (m_active && m_running && !m_failed)
source = m_source;
}
if (source)
source->ClipboardReset(generation, reason);
}
ClipboardChannelResult CClipboardHub::SendClipboard(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
// Keep the shared lock through Send so Unbind cannot advance the binding
// generation after validation but before the record enters the channel.
CSRWSharedLock lock(m_lock);
if (m_stopped || !m_available || !m_active || !m_running || m_failed ||
!m_source || record.generation != m_generation)
return ClipboardChannelResult::FAILED;
return m_channel.Send(record, data);
}
void CClipboardHub::ClipboardReceiveReady()
{
{
CSRWSharedLock lock(m_lock);
if (m_stopped || !m_available || !m_active || m_failed ||
!m_running)
return;
}
const uint64_t epoch = m_channel.Epoch();
if (epoch)
m_channel.Kick(epoch);
}
void CClipboardHub::ClipboardFailed()
{
IClipboardSource * source;
{
CSRWSharedLock lock(m_lock);
source = m_source;
}
if (!source || !MarkFailed(*source))
return;
const uint64_t epoch = m_channel.Epoch();
if (epoch)
m_channel.Kick(epoch);
}

View File

@@ -0,0 +1,78 @@
/**
* 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 "CClipboardChannel.h"
#include "CSRWLock.h"
#include "transport/IClipboardSource.h"
#include "transport/ITransport.h"
class CClipboardHub final : public IClipboardTarget,
private IClipboardChannelHandler
{
private:
CClipboardChannel& m_channel;
CSRWLock m_lifecycleLock;
CSRWLock m_callbackLock;
CSRWLock m_lock;
IClipboardSource * m_source = nullptr;
BackendId m_backend = 0;
uint32_t m_epoch = 0;
uint64_t m_channelEpoch = 0;
uint32_t m_generation = 1;
bool m_available = false;
bool m_active = false;
bool m_running = false;
bool m_reserved = false;
bool m_failed = false;
bool m_failurePending = false;
bool m_stopped = false;
void AdvanceGenerationNL();
bool MarkFailed(IClipboardSource& source);
ClipboardChannelResult HoldOrDiscard(
const KVMFRClipboardMessage& record);
void ClipboardState(bool available, uint64_t epoch) override;
ClipboardChannelResult ClipboardRecord(
const KVMFRClipboardMessage& record,
const uint8_t * data) override;
void ClipboardReset(uint64_t epoch, uint32_t reason) override;
public:
explicit CClipboardHub(CClipboardChannel& channel);
~CClipboardHub() override;
CClipboardHub(const CClipboardHub&) = delete;
CClipboardHub& operator=(const CClipboardHub&) = delete;
bool Bind(BackendId backend, uint32_t epoch, IClipboardSource& source);
void Unbind(BackendId backend, uint32_t epoch);
bool TakeFailure(SourceKey& source);
void Stop();
ClipboardChannelResult SendClipboard(
const KVMFRClipboardMessage& record,
const uint8_t * data) override;
void ClipboardReceiveReady() override;
void ClipboardFailed() override;
};

View File

@@ -23,7 +23,9 @@
#include "Atomic.h"
#include "capture/CFrameGraph.h"
#include "CDebug.h"
#include "ipc/CPipeServer.h"
#include "Seq.h"
#include "transport/IClipboardSource.h"
#include "transport/ITexStage.h"
#include <Windows.h>
@@ -121,7 +123,8 @@ CTransportManager::Entry::~Entry()
CloseHandle(idleEvent);
}
CTransportManager::CTransportManager() : m_tex(m_frameRev)
CTransportManager::CTransportManager() :
m_tex(m_frameRev), m_clipboard(g_pipe.Clipboard())
{
m_phaseIdle = CreateEvent(nullptr, TRUE, TRUE, nullptr);
m_stoppedEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
@@ -492,48 +495,55 @@ bool CTransportManager::InitializeEntry(Entry& entry)
bool CTransportManager::AddServices(Entry& entry)
{
std::shared_ptr<ITransport> transport;
BackendId id = 0;
uint32_t epoch = 0;
bool primary = false;
bool required = false;
uint32_t services = 0;
bool controlAdded = false;
bool controlFailed = false;
bool controlAbsent = false;
bool inputAdded = false;
bool inputFailed = false;
bool inputAbsent = false;
bool frameAdded = false;
bool frameBound = false;
bool frameAbsent = false;
uint64_t retryAt = 0;
BackendId id = 0;
uint32_t epoch = 0;
bool primary = false;
bool required = false;
uint32_t services = 0;
bool controlAdded = false;
bool controlFailed = false;
bool controlAbsent = false;
bool inputAdded = false;
bool inputFailed = false;
bool inputAbsent = false;
bool clipboardAdded = false;
bool clipboardFailed = false;
bool clipboardAbsent = false;
bool frameAdded = false;
bool frameBound = false;
bool frameAbsent = false;
uint64_t retryAt = 0;
{
CSRWSharedLock entryLock(entry.lock);
transport = entry.transport;
id = entry.id;
epoch = entry.epoch;
primary = entry.primary;
required = entry.required;
services = entry.config.services;
controlAdded = entry.controlAdded;
controlFailed = entry.controlFailed;
controlAbsent = entry.controlAbsent;
inputAdded = entry.inputAdded;
inputFailed = entry.inputFailed;
inputAbsent = entry.inputAbsent;
frameAdded = entry.frameAdded;
frameAbsent = entry.frameAbsent;
retryAt = entry.serviceRetryAt;
transport = entry.transport;
id = entry.id;
epoch = entry.epoch;
primary = entry.primary;
required = entry.required;
services = entry.config.services;
controlAdded = entry.controlAdded;
controlFailed = entry.controlFailed;
controlAbsent = entry.controlAbsent;
inputAdded = entry.inputAdded;
inputFailed = entry.inputFailed;
inputAbsent = entry.inputAbsent;
clipboardAdded = entry.clipboardAdded;
clipboardFailed = entry.clipboardFailed;
clipboardAbsent = entry.clipboardAbsent;
frameAdded = entry.frameAdded;
frameAbsent = entry.frameAbsent;
retryAt = entry.serviceRetryAt;
}
if (!transport)
return !required;
const uint64_t now = GetTickCount64();
const bool attach = now >= retryAt;
bool controlRetry = false;
bool frameRetry = false;
bool inputRetry = false;
const uint64_t now = GetTickCount64();
const bool attach = now >= retryAt;
bool controlRetry = false;
bool frameRetry = false;
bool inputRetry = false;
bool clipboardRetry = false;
if (!(services & TRANSPORT_SERVICE_CONTROL))
controlAbsent = true;
else if (attach && !controlAdded && !controlFailed && !controlAbsent)
@@ -610,7 +620,30 @@ bool CTransportManager::AddServices(Entry& entry)
}
}
if (attach && (controlRetry || inputRetry || frameRetry))
if (!(services & TRANSPORT_SERVICE_CLIPBOARD))
clipboardAbsent = true;
else if (attach && !clipboardAdded && !clipboardFailed &&
!clipboardAbsent)
{
IClipboardSource * clipboard = transport->Clipboard();
if (clipboard && m_clipboard.Bind(id, epoch, *clipboard))
{
CSRWExclusiveLock entryLock(entry.lock);
entry.clipboardAdded = true;
clipboardAdded = true;
}
else if (clipboard)
clipboardRetry = true;
else
{
clipboardAbsent = true;
CSRWExclusiveLock entryLock(entry.lock);
entry.clipboardAbsent = true;
}
}
if (attach && (controlRetry || inputRetry || frameRetry ||
clipboardRetry))
{
CSRWExclusiveLock entryLock(entry.lock);
entry.serviceRetryAt = now + SERVICE_RETRY_DELAY_MS;
@@ -623,9 +656,10 @@ bool CTransportManager::AddServices(Entry& entry)
}
const bool servicesReady =
(!(services & TRANSPORT_SERVICE_FRAME) || frameAdded) &&
(!(services & TRANSPORT_SERVICE_CONTROL) || controlAdded) &&
(!(services & TRANSPORT_SERVICE_INPUT) || inputAdded);
(!(services & TRANSPORT_SERVICE_FRAME) || frameAdded) &&
(!(services & TRANSPORT_SERVICE_CONTROL) || controlAdded) &&
(!(services & TRANSPORT_SERVICE_INPUT) || inputAdded) &&
(!(services & TRANSPORT_SERVICE_CLIPBOARD) || clipboardAdded);
return !required || servicesReady;
}
@@ -720,6 +754,35 @@ void CTransportManager::HandleServiceFailures()
break;
}
}
source = {};
while (m_clipboard.TakeFailure(source))
{
Entry * entries[FRAME_MAX_SINKS] = {};
const unsigned count = Entries(entries);
for (unsigned i = 0; i < count; ++i)
{
Entry& entry = *entries[i];
bool restart = false;
{
CSRWExclusiveLock entryLock(entry.lock);
if (entry.id != source.backend || entry.epoch != source.epoch ||
!entry.clipboardAdded)
continue;
entry.clipboardAdded = false;
entry.clipboardFailed = true;
restart = !entry.exposed;
}
m_clipboard.Unbind(source.backend, source.epoch);
if (restart)
{
RemoveServices(entry);
ScheduleRetry(entry);
}
break;
}
}
}
bool CTransportManager::SetupEntry(Entry& entry, size_t alignment)
@@ -770,12 +833,13 @@ void CTransportManager::ScheduleRetry(Entry& entry)
void CTransportManager::RemoveServices(Entry& entry)
{
BackendId id = 0;
uint32_t epoch = 0;
bool frameAdded = false;
bool frameLegacy = false;
bool controlAdded = false;
bool inputAdded = false;
BackendId id = 0;
uint32_t epoch = 0;
bool frameAdded = false;
bool frameLegacy = false;
bool controlAdded = false;
bool inputAdded = false;
bool clipboardAdded = false;
{
CSRWExclusiveLock entryLock(entry.lock);
id = entry.id;
@@ -784,11 +848,13 @@ void CTransportManager::RemoveServices(Entry& entry)
frameLegacy = entry.frameLegacy;
controlAdded = entry.controlAdded;
inputAdded = entry.inputAdded;
clipboardAdded = entry.clipboardAdded;
entry.frameAdded = false;
entry.frameLegacy = false;
entry.texSink = nullptr;
entry.controlAdded = false;
entry.inputAdded = false;
entry.clipboardAdded = false;
entry.frameRetryAt = 0;
}
@@ -796,6 +862,8 @@ void CTransportManager::RemoveServices(Entry& entry)
m_tex.Drop(id, epoch);
if (inputAdded)
m_input.Unbind(id, epoch);
if (clipboardAdded)
m_clipboard.Unbind(id, epoch);
if (controlAdded)
m_control.Remove(id, epoch);
if (frameLegacy)
@@ -835,6 +903,8 @@ void CTransportManager::RetryEntry(Entry& entry, uint64_t now,
entry.controlAbsent = false;
entry.inputFailed = false;
entry.inputAbsent = false;
entry.clipboardFailed = false;
entry.clipboardAbsent = false;
entry.frameAbsent = false;
entry.serviceRetryAt = 0;
Seq::Inc(entry.epoch);
@@ -1601,6 +1671,7 @@ void CTransportManager::Stop()
}
m_input.Stop();
m_clipboard.Stop();
for (unsigned i = count; i > 0; --i)
{

View File

@@ -22,6 +22,7 @@
#include "Atomic.h"
#include "CSRWLock.h"
#include "transport/CClipboardHub.h"
#include "transport/CControlHub.h"
#include "transport/CFrameHub.h"
#include "transport/CInputHub.h"
@@ -109,6 +110,9 @@ private:
bool inputAdded = false;
bool inputFailed = false;
bool inputAbsent = false;
bool clipboardAdded = false;
bool clipboardFailed = false;
bool clipboardAbsent = false;
bool frameAdded = false;
bool frameLegacy = false;
ITexSink * texSink = nullptr;
@@ -132,6 +136,7 @@ private:
CFrameHub m_frames;
CTexHub m_tex;
CInputHub m_input;
CClipboardHub m_clipboard;
CRecoveryHub m_recovery;
Entry * m_primary = nullptr;
bool m_initialized = false;

View File

@@ -0,0 +1,77 @@
/**
* 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 "CClipboardChannel.h"
#include "common/KVMFRClipboard.h"
#include <stdint.h>
class IClipboardTarget
{
public:
virtual ~IClipboardTarget() = default;
// Sends one client-to-Helper record. record.generation must be the binding
// generation most recently supplied by ClipboardState; transports which
// expose their own endpoint generation validate and replace it at this
// adapter boundary. clipboardGeneration remains the clipboard content
// generation and is preserved end-to-end.
//
// BUSY consumes neither the record nor its borrowed data; the source
// retains and retries the exact record from its bounded worker/Process
// drain.
virtual ClipboardChannelResult SendClipboard(
const KVMFRClipboardMessage& record, const uint8_t * data) = 0;
// The source calls ClipboardReceiveReady after a Helper-to-client record
// returned BUSY and it can accept an exact retry of that record.
virtual void ClipboardReceiveReady() = 0;
virtual void ClipboardFailed() = 0;
};
class IClipboardSource
{
public:
virtual ~IClipboardSource() = default;
// Start attaches the target, but the source must wait for the initial
// ClipboardState callback before calling target.SendClipboard. Stop is a
// callback-quiescence barrier and leaves no work which can access target;
// it also cleans up a partially completed or failed Start.
virtual bool Start(IClipboardTarget& target) = 0;
virtual void Stop() = 0;
// All callbacks into the source, including Start and Stop, are serialized.
// record.generation is stamped with the current binding generation. A
// generation change invalidates records from an earlier generation and
// cancels their transfers; clipboardGeneration is unaffected. data is
// borrowed only for the duration of SendClipboard. BUSY consumes neither
// record nor data, and the channel retries the same record after
// ClipboardReceiveReady (or its bounded polling fallback).
virtual void ClipboardState(bool available, uint32_t generation) = 0;
virtual ClipboardChannelResult SendClipboard(
const KVMFRClipboardMessage& record, const uint8_t * data) = 0;
// Reset advances generation before the callback. The source cancels all
// transfers from the preceding generation.
virtual void ClipboardReset(uint32_t generation, uint32_t reason) = 0;
};

View File

@@ -29,6 +29,7 @@
#include <stddef.h>
#include <stdint.h>
class IClipboardSource;
class IControlSink;
class IFrameSink;
class IInputSource;
@@ -178,4 +179,5 @@ public:
virtual ITexSink * TexSink() { return nullptr; }
virtual IControlSink * Control() { return nullptr; }
virtual IInputSource * Input() { return nullptr; }
virtual IClipboardSource * Clipboard() { return nullptr; }
};

View File

@@ -125,6 +125,8 @@ namespace
service = TRANSPORT_SERVICE_CONTROL;
else if (Equal(name, L"input"))
service = TRANSPORT_SERVICE_INPUT;
else if (Equal(name, L"clipboard"))
service = TRANSPORT_SERVICE_CLIPBOARD;
else
return false;

View File

@@ -28,11 +28,13 @@ using BackendId = uint32_t;
enum TransportService : uint32_t
{
TRANSPORT_SERVICE_FRAME = 1U << 0,
TRANSPORT_SERVICE_CONTROL = 1U << 1,
TRANSPORT_SERVICE_INPUT = 1U << 2,
TRANSPORT_SERVICE_ALL = TRANSPORT_SERVICE_FRAME |
TRANSPORT_SERVICE_CONTROL | TRANSPORT_SERVICE_INPUT,
TRANSPORT_SERVICE_FRAME = 1U << 0,
TRANSPORT_SERVICE_CONTROL = 1U << 1,
TRANSPORT_SERVICE_INPUT = 1U << 2,
TRANSPORT_SERVICE_CLIPBOARD = 1U << 3,
TRANSPORT_SERVICE_ALL = TRANSPORT_SERVICE_FRAME |
TRANSPORT_SERVICE_CONTROL | TRANSPORT_SERVICE_INPUT |
TRANSPORT_SERVICE_CLIPBOARD,
};
struct TransportInstance

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
/**
* 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/IClipboardSource.h"
#include "common/KVMFRClipboard.h"
#include "common/LGMPConfig.h"
#include <Windows.h>
#include <stdint.h>
extern "C" {
#include "lgmp/host.h"
}
class CLGMPHost;
class CLGMPClipboardTransport final : public IClipboardSource
{
private:
static constexpr unsigned MEMORY_COUNT = 2;
static constexpr unsigned INTERNAL_TARGET_COUNT = 3;
static constexpr ULONGLONG OWNER_LEASE_MS = 1000;
static constexpr DWORD ACTIVE_POLL_MS = 5;
static constexpr DWORD IDLE_POLL_MS = 50;
static constexpr uint32_t SLOT_BYTES =
sizeof(KVMFRClipboardSlotHeader) + KVMFR_CLIPBOARD_DATA_BYTES;
enum class PostResult
{
POSTED,
BUSY,
GONE,
FAILED,
};
struct Transfer
{
uint64_t transfer = 0;
uint64_t clipboardGeneration = 0;
uint64_t nextOffset = 0;
uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_NONE;
uint32_t nextSequence = 0;
bool began = false;
void Clear() { *this = {}; }
bool Active() const { return transfer != 0; }
};
struct Grant
{
uint32_t generation = 0;
bool offered = false;
bool committed = false;
};
struct PendingTarget
{
bool valid = false;
bool acknowledge = false;
int grant = -1;
KVMFRClipboardMessage record = {};
uint8_t data[KVMFR_CLIPBOARD_DATA_BYTES] = {};
void Clear()
{
valid = false;
acknowledge = false;
grant = -1;
record = {};
}
};
CLGMPHost& m_host;
PLGMPHostQueue m_queue = nullptr;
PLGMPMemory m_statusMemory [MEMORY_COUNT] = {};
PLGMPMemory m_messageMemory[MEMORY_COUNT] = {};
PLGMPMemory m_grantMemory [MEMORY_COUNT] = {};
PLGMPMemory m_dataMemory [MEMORY_COUNT] = {};
CSRWLock m_lifecycleLock;
CSRWLock m_lock;
IClipboardTarget * m_target = nullptr;
HANDLE m_stopEvent = nullptr;
HANDLE m_wakeEvent = nullptr;
HANDLE m_thread = nullptr;
bool m_available = false;
bool m_statusDirty = true;
bool m_cachedValid = false;
bool m_replayPending = false;
bool m_outboundBlocked = false;
bool m_failed = false;
uint32_t m_endpointGeneration = 0;
uint32_t m_ownerClientID = 0;
uint32_t m_ownerGeneration = 0;
uint32_t m_statusSerial = 0;
uint32_t m_messageSerial = 0;
uint32_t m_dataSerial = 0;
uint32_t m_helperFormats = 0;
uint32_t m_clientFormats = 0;
uint32_t m_blockedOwnerClientID = 0;
uint32_t m_blockedOwnerGeneration = 0;
uint64_t m_ownerDeadline = 0;
uint64_t m_clientClipboardGeneration = 0;
uint64_t m_discardHelperToClient = 0;
KVMFRClipboardMessage m_cachedClipboard = {};
KVMFRClipboardMessage m_blockedOutbound = {};
Transfer m_clientToHelper;
Transfer m_helperToClient;
Grant m_grants[MEMORY_COUNT];
PendingTarget m_pendingTarget;
KVMFRClipboardMessage m_internalTarget[INTERNAL_TARGET_COUNT] = {};
unsigned m_internalTargetCount = 0;
bool Initialize();
void DeInit();
static DWORD CALLBACK ThreadProc(void * context);
void Thread();
void Wake();
bool IsOwner(uint32_t clientID, uint32_t generation) const;
bool OwnerSubscribed() const;
void RenewLease();
void BlockOutbound(const KVMFRClipboardMessage& record,
uint32_t ownerClientID, uint32_t ownerGeneration);
void ClearOutboundBlock();
bool BlockedOwnerLost(const KVMFRClipboardMessage& record) const;
void ReleaseOwner(const char * reason, bool clearHelper);
void ResetProtocol(bool keepClipboard);
void QueueHelperClear();
void QueueTransferCancel(const Transfer& transfer);
void QueueStaleRequestCancel(
const KVMFRClipboardMessage& request);
bool QueueInternalTarget(const KVMFRClipboardMessage& record);
bool PumpInternalTarget();
PLGMPMemory FindAvailable(PLGMPMemory (&memory)[MEMORY_COUNT]) const;
PostResult PostForOwner(uint64_t udata, PLGMPMemory memory);
bool PublishStatus();
bool PostGrants();
bool ReplayClipboard();
bool DrainMessage();
bool ProcessMessage(uint32_t clientID,
const KVMFRClipboardMessage& message);
bool RetryTarget();
void FinishTarget(bool accepted);
void DropPendingTarget();
void Fail(const char * operation, LGMP_STATUS status);
bool ValidateClaim(const KVMFRClipboardMessage& message) const;
bool ValidateOwnedControl(const KVMFRClipboardMessage& message) const;
bool ValidateInboundRecord(const KVMFRClipboardMessage& message) const;
bool ValidateOutboundRecord(const KVMFRClipboardMessage& message) const;
bool ValidateChunk(const Transfer& transfer,
const KVMFRClipboardMessage& message) const;
void AdvanceChunk(Transfer& transfer,
const KVMFRClipboardMessage& message);
void ApplyInbound(const KVMFRClipboardMessage& message);
void ApplyOutbound(const KVMFRClipboardMessage& message);
bool BeginTarget(const KVMFRClipboardMessage& message,
const uint8_t * data, int grant);
ClipboardChannelResult SendControl(
const KVMFRClipboardMessage& record);
ClipboardChannelResult SendData(
const KVMFRClipboardMessage& record, const uint8_t * data);
friend class CLGMPTransport;
public:
explicit CLGMPClipboardTransport(CLGMPHost& host) :
m_host(host) {}
~CLGMPClipboardTransport() override;
CLGMPClipboardTransport(const CLGMPClipboardTransport&) = delete;
CLGMPClipboardTransport& operator=(
const CLGMPClipboardTransport&) = delete;
bool Start(IClipboardTarget& target) override;
void Stop() override;
void ClipboardState(bool available, uint32_t generation) override;
ClipboardChannelResult SendClipboard(
const KVMFRClipboardMessage& record,
const uint8_t * data) override;
void ClipboardReset(uint32_t generation, uint32_t reason) override;
};

View File

@@ -52,7 +52,8 @@ bool CLGMPHost::Initialize(CIVSHMEM& ivshmem)
KVMFR_FEATURE_SETCURSORPOS |
KVMFR_FEATURE_WINDOWSIZE |
KVMFR_FEATURE_FRAME_SCHEDULE |
KVMFR_FEATURE_INPUT;
KVMFR_FEATURE_INPUT |
KVMFR_FEATURE_CLIPBOARD;
strncpy_s(kvmfr.hostver, LG_VERSION_STR, sizeof(kvmfr.hostver) - 1);
ss.write(reinterpret_cast<const char *>(&kvmfr), sizeof(kvmfr));
}

View File

@@ -67,7 +67,8 @@ CLGMPTransport::CLGMPTransport(const TransportInstance& config) :
m_config(config),
m_control(m_host),
m_frames(m_host, m_ivshmem),
m_input(m_host)
m_input(m_host),
m_clipboard(m_host)
{
}
@@ -90,10 +91,10 @@ bool CLGMPTransport::Initialize()
if (!m_host.Initialize(m_ivshmem))
return false;
// Preserve the existing shared-memory layout by appending input after the
// frame and pointer queues and retained pointer state allocations.
// Preserve the existing shared-memory layout by appending input and then
// clipboard after the frame/pointer queues and retained pointer state.
if (!m_frames.Initialize() || !m_control.Initialize() ||
!m_input.Initialize())
!m_input.Initialize() || !m_clipboard.Initialize())
return false;
m_frames.SealMemoryLayout();
@@ -242,6 +243,7 @@ void CLGMPTransport::Stop()
Atomic::Store(m_ready, false, std::memory_order_release);
Abort();
m_hasActive = false;
m_clipboard.Stop();
m_input.Stop();
}

View File

@@ -23,6 +23,7 @@
#include "Atomic.h"
#include "transport/ITransport.h"
#include "transport/lgmp/CIVSHMEM.h"
#include "transport/lgmp/CLGMPClipboardTransport.h"
#include "transport/lgmp/CLGMPControl.h"
#include "transport/lgmp/CLGMPFrameTransport.h"
#include "transport/lgmp/CLGMPHost.h"
@@ -32,20 +33,21 @@
class CLGMPTransport final : public ITransport
{
private:
TransportInstance m_config;
TransportInstance m_config;
// Keep this declaration order. Destruction must release frame and control
// allocations before the LGMP host and its IVSHMEM mapping are destroyed.
CIVSHMEM m_ivshmem;
CLGMPHost m_host;
CLGMPControl m_control;
CLGMPFrameTransport m_frames;
CLGMPInputTransport m_input;
CRecovery m_recovery;
std::atomic<bool> m_ready = false;
FrameCfg m_activeCfg;
FrameCfg m_pendingCfg;
bool m_hasActive = false;
bool m_hasPending = false;
CIVSHMEM m_ivshmem;
CLGMPHost m_host;
CLGMPControl m_control;
CLGMPFrameTransport m_frames;
CLGMPInputTransport m_input;
CLGMPClipboardTransport m_clipboard;
CRecovery m_recovery;
std::atomic<bool> m_ready = false;
FrameCfg m_activeCfg;
FrameCfg m_pendingCfg;
bool m_hasActive = false;
bool m_hasPending = false;
public:
explicit CLGMPTransport(const TransportInstance& config);
@@ -76,4 +78,5 @@ public:
IFrameSink * FrameSink() override { return &m_frames; }
IControlSink * Control() override { return &m_control; }
IInputSource * Input() override { return &m_input; }
IClipboardSource * Clipboard() override { return &m_clipboard; }
};