[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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,231 @@
/**
* 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 <Windows.h>
#include <array>
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <type_traits>
#include <vector>
class CClipboardSpool;
class CClipboardManager final : private IClipboardChannelHandler
{
private:
enum class WorkType
{
STATE,
RESET,
RECORD,
SEND,
SEND_DATA,
};
enum class UIType
{
STATE,
OFFER,
CLEAR,
REQUEST,
};
struct Work
{
WorkType type = WorkType::STATE;
bool available = false;
uint64_t epoch = 0;
uint32_t reason = 0;
KVMFRClipboardMessage record = {};
std::vector<uint8_t> data;
std::shared_ptr<CClipboardSpool> spool;
uint64_t deadline = 0;
};
struct UIWork
{
UIType type = UIType::STATE;
bool available = false;
uint64_t epoch = 0;
KVMFRClipboardMessage record = {};
};
struct PendingCancel
{
bool valid = false;
KVMFRClipboardMessage record = {};
uint64_t deadline = 0;
};
struct PendingControl
{
WorkType type = WorkType::STATE;
bool available = false;
uint64_t epoch = 0;
uint32_t reason = 0;
};
static_assert(std::is_nothrow_move_constructible_v<Work>);
static_assert(std::is_nothrow_move_assignable_v<Work>);
struct IncomingTransfer
{
uint64_t transfer = 0;
uint64_t generation = 0;
KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_NONE;
uint64_t nextOffset = 0;
uint32_t nextSequence = 0;
uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
bool began = false;
bool complete = false;
uint32_t error = ERROR_SUCCESS;
HANDLE event = nullptr;
std::shared_ptr<CClipboardSpool> spool;
~IncomingTransfer();
};
static constexpr UINT WM_CLIPBOARD_WORK = WM_APP + 0x4c;
static constexpr size_t MAX_WORK = 16;
static constexpr size_t MAX_UI_WORK = 16;
static constexpr size_t MAX_PENDING_CANCEL = 8;
static constexpr size_t MAX_PENDING_CONTROL = 16;
static constexpr DWORD RENDER_TIMEOUT_MS = 15000;
static constexpr DWORD SEND_TIMEOUT_MS = RENDER_TIMEOUT_MS;
HWND m_hwnd;
CClipboardChannel& m_channel;
HANDLE m_stop = nullptr;
HANDLE m_wake = nullptr;
HANDLE m_thread = nullptr;
std::mutex m_workLock;
std::array<std::optional<Work>, MAX_WORK> m_recordWork;
size_t m_recordWorkCount = 0;
std::array<std::optional<Work>, MAX_WORK> m_sendWork;
size_t m_sendWorkCount = 0;
std::array<PendingCancel, MAX_PENDING_CANCEL> m_pendingCancel;
size_t m_pendingCancelCursor = 0;
std::array<PendingControl, MAX_PENDING_CONTROL> m_pendingControl;
size_t m_pendingControlCount = 0;
std::recursive_mutex m_uiLock;
std::array<UIWork, MAX_UI_WORK> m_uiWork;
size_t m_uiWorkCount = 0;
std::mutex m_transferLock;
std::shared_ptr<IncomingTransfer> m_incoming;
std::mutex m_outgoingLock;
UINT m_formatPNG = 0;
UINT m_formatJPEG = 0;
UINT m_formatOrigin = 0;
bool m_listener = false;
std::atomic<bool> m_shutdown { false };
bool m_applyingRemote = false;
bool m_available = false;
uint64_t m_epoch = 0;
uint64_t m_localGeneration = 0;
DWORD m_localSequence = 0;
uint64_t m_remoteControlGeneration = 0;
uint64_t m_remoteGeneration = 0;
uint32_t m_remoteFormats = 0;
DWORD m_ownedSequence = 0;
std::atomic<uint64_t> m_liveLocalGeneration { 0 };
std::atomic<uint64_t> m_nextTransfer {
KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1) };
std::atomic<uint64_t> m_outgoingTransfer { 0 };
KVMFRClipboardMessage m_pendingRemoteOffer = {};
uint64_t m_remoteRetryDeadline = 0;
static DWORD WINAPI ThreadProc(void * context);
void Thread();
bool QueueWork(Work&& work);
bool QueueCancel(const KVMFRClipboardMessage& record, uint32_t reason,
uint64_t deadline = 0);
void QueueControl(WorkType type, bool available,
uint64_t epoch, uint32_t reason);
bool QueueUI(UIWork&& work);
void DrainUI();
void ProcessWork(Work&& work);
void ProcessRecord(const KVMFRClipboardMessage& record,
const uint8_t * data);
void ProcessData(const KVMFRClipboardMessage& record,
const uint8_t * data);
void ProcessSend(Work&& work);
void ProcessSendData(Work&& work);
void CancelIncoming(uint32_t reason, uint64_t transfer = 0);
void ReleaseOutgoing(uint64_t transfer);
void HandleState(bool available, uint64_t epoch);
void HandleOffer(const KVMFRClipboardMessage& record);
void HandleClear(const KVMFRClipboardMessage& record);
void HandleRequest(const KVMFRClipboardMessage& record);
void HandleClipboardUpdate();
void HandleDestroyClipboard();
void RenderFormat(UINT format, uint64_t deadline = 0);
void RenderAllFormats();
void RetryRemoteOffer();
bool OpenClipboardRetry() const;
bool IsOurClipboard();
uint32_t EnumerateFormats() const;
void PublishLocalClipboard();
void PublishClear(uint64_t generation);
bool ApplyRemoteOffer(uint32_t formats, uint64_t generation);
void ClearOwnedClipboard();
void InvalidateOutgoing(uint32_t reason);
void InvalidateLocalClipboard(uint32_t reason);
void ClearRemoteRetry();
bool SetOriginMarker(uint64_t generation) const;
KVMFRClipboardFormat ToWireFormat(UINT format) const;
UINT ToWindowsFormat(KVMFRClipboardFormat format) const;
std::shared_ptr<CClipboardSpool> CaptureFormat(
KVMFRClipboardFormat format, DWORD sequence);
bool MaterializeFormat(KVMFRClipboardFormat format,
CClipboardSpool& spool);
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:
CClipboardManager(HWND hwnd, CClipboardChannel& channel);
~CClipboardManager();
CClipboardManager(const CClipboardManager&) = delete;
CClipboardManager& operator=(const CClipboardManager&) = delete;
bool Initialize();
void Shutdown();
bool HandleMessage(UINT message, WPARAM wParam, LPARAM lParam,
LRESULT& result);
};

View File

@@ -19,6 +19,7 @@
*/
#include "CNotifyWindow.h"
#include "CClipboardManager.h"
#include "CConfigWindow.h"
#include "Resources.h"
#include <CDebug.h>
@@ -86,6 +87,11 @@ CNotifyWindow::~CNotifyWindow()
LRESULT CNotifyWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT clipboardResult;
if (m_clipboard && m_clipboard->HandleMessage(
uMsg, wParam, lParam, clipboardResult))
return clipboardResult;
switch (uMsg)
{
case WM_NOTIFY_ICON:
@@ -168,6 +174,8 @@ LRESULT CNotifyWindow::onClose()
LRESULT CNotifyWindow::onDestroy()
{
if (m_clipboard)
m_clipboard->Shutdown();
KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER);
Shell_NotifyIcon(NIM_DELETE, &m_iconData);
return 0;
@@ -310,6 +318,20 @@ void CNotifyWindow::setRecoveryMode(bool active)
DEBUG_ERROR_HR(GetLastError(), "Failed to update recovery state");
}
bool CNotifyWindow::initClipboard(CClipboardChannel& channel)
{
if (m_clipboard)
return true;
std::unique_ptr<CClipboardManager> clipboard(
new (std::nothrow) CClipboardManager(m_hwnd, channel));
if (!clipboard)
return false;
if (!clipboard->Initialize())
return false;
m_clipboard = std::move(clipboard);
return true;
}
void CNotifyWindow::handleResolutionRejected(uint32_t width, uint32_t height,
uint32_t requiredSizeMiB)
{

View File

@@ -27,6 +27,8 @@
#include <optional>
class CConfigWindow;
class CClipboardChannel;
class CClipboardManager;
class CNotifyWindow : public CWindow
{
@@ -40,6 +42,7 @@ class CNotifyWindow : public CWindow
std::atomic_bool closeRequested;
bool m_recoveryActive;
std::unique_ptr<CConfigWindow> m_config;
std::unique_ptr<CClipboardManager> m_clipboard;
std::function<void()> m_onSettingChange;
std::function<bool()> m_onEnsureOnlyDisplay;
@@ -80,6 +83,8 @@ public:
uint32_t requiredSizeMiB);
void setRecoveryMode(bool active);
bool initClipboard(CClipboardChannel& channel);
HWND hwndDialog();
void close();

View File

@@ -19,6 +19,7 @@
*/
#include "CPipeClient.h"
#include "CClipboardRing.h"
#include "CDebug.h"
#include "CSRWLock.h"
#include "CNotifyWindow.h"
@@ -399,7 +400,11 @@ bool CPipeClient::Init()
void CPipeClient::DeInit()
{
// Stop first so no endpoint callback can race mapping teardown.
m_endpoint.Stop();
CSRWExclusiveLock lock(m_clipboardSetupLock);
ResetClipboardSetupLocked();
}
bool CPipeClient::IsLGIddDeviceAttached()
@@ -484,6 +489,88 @@ void CPipeClient::OnPipeConnected()
if (hasStatus)
WriteMsg(status);
if (!m_clipboardEnabled)
return;
CSRWExclusiveLock setupLock(m_clipboardSetupLock);
ResetClipboardSetupLocked();
LARGE_INTEGER size = {};
size.QuadPart = sizeof(ClipboardMapping);
m_clipboardMapping = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, size.HighPart, size.LowPart, nullptr);
if (!m_clipboardMapping)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to create the clipboard mapping");
return;
}
ClipboardMapping * view = static_cast<ClipboardMapping *>(MapViewOfFile(
m_clipboardMapping, FILE_MAP_ALL_ACCESS, 0, 0,
sizeof(ClipboardMapping)));
if (!view)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to initialize the clipboard mapping");
ResetClipboardSetupLocked();
return;
}
++m_clipboardEpochCounter;
if (!m_clipboardEpochCounter)
++m_clipboardEpochCounter;
m_clipboardEpoch = m_clipboardEpochCounter;
CClipboardRing::Initialize(*view, m_clipboardEpoch);
UnmapViewOfFile(view);
DWORD serverPid = 0;
if (!GetNamedPipeServerProcessId(m_endpoint.NativeHandle(), &serverPid))
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to identify the clipboard mapping target");
ResetClipboardSetupLocked();
return;
}
HANDLE target = OpenProcess(PROCESS_DUP_HANDLE, FALSE, serverPid);
HANDLE remote = nullptr;
if (!target || !DuplicateHandle(GetCurrentProcess(),
m_clipboardMapping, target, &remote, 0, FALSE,
DUPLICATE_SAME_ACCESS))
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to share the clipboard mapping with the IDD");
if (target)
CloseHandle(target);
ResetClipboardSetupLocked();
return;
}
LGPipeMsg setup = {};
setup.size = sizeof(setup);
setup.type = LGPipeMsg::CLIPBOARD_SETUP;
setup.clipboardSetup.handle =
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(remote));
setup.clipboardSetup.bytes = sizeof(ClipboardMapping);
const bool sent = m_endpoint.Send(&setup, sizeof(setup));
if (!sent)
{
DEBUG_WARN("Failed to send clipboard mapping setup");
HANDLE reclaimed = nullptr;
if (DuplicateHandle(target, remote, GetCurrentProcess(), &reclaimed,
0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
CloseHandle(reclaimed);
ResetClipboardSetupLocked();
}
CloseHandle(target);
}
void CPipeClient::OnPipeDisconnected()
{
CSRWExclusiveLock lock(m_clipboardSetupLock);
ResetClipboardSetupLocked();
}
void CPipeClient::ReloadSettings()
@@ -740,12 +827,85 @@ bool CPipeClient::OnPipeMessage(const void * message, size_t size)
HandleSetRecovery(msg);
return true;
case LGPipeMsg::CLIPBOARD_READY:
{
CSRWExclusiveLock setupLock(m_clipboardSetupLock);
if (msg.clipboardReady.status != ERROR_SUCCESS ||
msg.clipboardReady.epoch != m_clipboardEpoch)
{
DEBUG_WARN("IDD rejected the clipboard mapping (%u)",
msg.clipboardReady.status);
ResetClipboardSetupLocked();
return true;
}
HANDLE mapping = nullptr;
if (!m_clipboardMapping ||
!DuplicateHandle(GetCurrentProcess(), m_clipboardMapping,
GetCurrentProcess(), &mapping, 0, FALSE, DUPLICATE_SAME_ACCESS) ||
!m_clipboard.Attach(mapping, m_clipboardEpoch, true, *this))
{
DEBUG_ERROR("Failed to activate the clipboard mapping");
const uint64_t epoch = m_clipboardEpoch;
ResetClipboardSetupLocked();
setupLock.Unlock();
// Tell the IDD that its successful mapping is unusable here, so it
// does not leave a one-sided channel advertised as available.
LGPipeMsg failure = {};
failure.size = sizeof(failure);
failure.type = LGPipeMsg::CLIPBOARD_READY;
failure.clipboardReady.epoch = epoch;
failure.clipboardReady.status = ERROR_NOT_READY;
m_endpoint.Send(&failure, sizeof(failure));
}
return true;
}
case LGPipeMsg::CLIPBOARD_KICK:
m_clipboard.Kick(msg.clipboardKick.epoch);
return true;
case LGPipeMsg::CLIPBOARD_RESET:
m_clipboard.Reset(
msg.clipboardReset.epoch, msg.clipboardReset.reason);
return true;
default:
DEBUG_ERROR("Unknown message type %d", msg.type);
return true;
}
}
bool CPipeClient::ClipboardKick(uint64_t epoch)
{
LGPipeMsg msg = {};
msg.size = sizeof(msg);
msg.type = LGPipeMsg::CLIPBOARD_KICK;
msg.clipboardKick.epoch = epoch;
return m_endpoint.Send(&msg, sizeof(msg));
}
void CPipeClient::ClipboardResetPeer(uint64_t epoch, uint32_t reason)
{
LGPipeMsg msg = {};
msg.size = sizeof(msg);
msg.type = LGPipeMsg::CLIPBOARD_RESET;
msg.clipboardReset.epoch = epoch;
msg.clipboardReset.reason = reason;
m_endpoint.Send(&msg, sizeof(msg));
}
void CPipeClient::ResetClipboardSetupLocked()
{
m_clipboard.Detach();
if (m_clipboardMapping)
CloseHandle(m_clipboardMapping);
m_clipboardMapping = nullptr;
m_clipboardEpoch = 0;
}
void CPipeClient::HandleSetCursorPos(const LGPipeMsg& msg)
{
SetActiveDesktop();

View File

@@ -24,14 +24,22 @@
#include <stdint.h>
#include "CPipeEndpoint.h"
#include "CClipboardChannel.h"
#include "CSRWLock.h"
#include "PipeMsg.h"
class CPipeClient : private IPipeEndpointHandler
class CPipeClient : private IPipeEndpointHandler,
public IClipboardChannelDoorbell
{
private:
CPipeEndpoint m_endpoint;
CSRWLock m_displayLock;
CPipeEndpoint m_endpoint;
CClipboardChannel m_clipboard;
CSRWLock m_clipboardSetupLock;
HANDLE m_clipboardMapping = nullptr;
uint64_t m_clipboardEpoch = 0;
uint64_t m_clipboardEpochCounter = 0;
bool m_clipboardEnabled = false;
CSRWLock m_displayLock;
bool m_recoveryActive = false;
bool m_hasRecoveryStatus = false;
@@ -50,8 +58,10 @@ private:
void HandleGPUStatus(const LGPipeMsg& msg);
void HandleResolutionRejected(const LGPipeMsg& msg);
void HandleSetRecovery(const LGPipeMsg& msg);
void ResetClipboardSetupLocked();
void OnPipeConnected() override;
void OnPipeDisconnected() override;
bool ShouldReconnect() override;
bool OnPipeMessage(const void * message, size_t size) override;
@@ -64,6 +74,11 @@ public:
void DeInit();
bool IsRunning() { return m_endpoint.IsRunning(); }
CClipboardChannel& Clipboard() { return m_clipboard; }
void EnableClipboard() { m_clipboardEnabled = true; }
bool ClipboardKick(uint64_t epoch) override;
void ClipboardResetPeer(uint64_t epoch, uint32_t reason) override;
void ReloadSettings();
bool EnsureOnlyDisplay();
};

View File

@@ -191,6 +191,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
<ItemGroup>
<ClCompile Include="CButton.cpp" />
<ClCompile Include="CCheckbox.cpp" />
<ClCompile Include="CClipboardManager.cpp" />
<ClCompile Include="CConfigWindow.cpp" />
<ClCompile Include="CEditWidget.cpp" />
<ClCompile Include="CGroupBox.cpp" />
@@ -207,6 +208,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
<ItemGroup>
<ClInclude Include="CButton.h" />
<ClInclude Include="CCheckbox.h" />
<ClInclude Include="CClipboardManager.h" />
<ClInclude Include="CConfigWindow.h" />
<ClInclude Include="CEditWidget.h" />
<ClInclude Include="CGroupBox.h" />

View File

@@ -57,6 +57,9 @@
<ClCompile Include="CCheckbox.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CClipboardManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="CPipeClient.h">
@@ -98,6 +101,9 @@
<ClInclude Include="CCheckbox.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CClipboardManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Resources.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

@@ -136,6 +136,11 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _
CNotifyWindow& window = CNotifyWindow::instance();
if (!window.initClipboard(g_pipe.Clipboard()))
DEBUG_ERROR("Failed to initialize clipboard synchronization");
else
g_pipe.EnableClipboard();
// the pipe must be initialized after the CNotifyWindow
// has been created to avoid a potential race
if (!g_pipe.Init())