From dbf52cb405c86085d8ceb5f90d42894232c5647b Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 13:39:37 +1000 Subject: [PATCH] [common] clipboard: define remote file datasets --- common/include/common/KVMFR.h | 2 +- common/include/common/KVMFRClipboard.h | 253 ++++++++++++++++++++++--- common/include/common/LGMPConfig.h | 2 +- 3 files changed, 227 insertions(+), 30 deletions(-) diff --git a/common/include/common/KVMFR.h b/common/include/common/KVMFR.h index a9ece7c6..78c6844c 100644 --- a/common/include/common/KVMFR.h +++ b/common/include/common/KVMFR.h @@ -32,7 +32,7 @@ #include "KVMFRInput.h" #define KVMFR_MAGIC "KVMFR---" -#define KVMFR_VERSION 30 +#define KVMFR_VERSION 31 // Fallback used by producers that cannot report the source display's SDR // white level. IDD frames override this with IDDCX_METADATA2::SdrWhiteLevel. diff --git a/common/include/common/KVMFRClipboard.h b/common/include/common/KVMFRClipboard.h index e5ed2b7c..aa87ae52 100644 --- a/common/include/common/KVMFRClipboard.h +++ b/common/include/common/KVMFRClipboard.h @@ -26,10 +26,14 @@ #include #include -#define KVMFR_CLIPBOARD_VERSION 1U -#define KVMFR_CLIPBOARD_SLOT_COUNT 2U -#define KVMFR_CLIPBOARD_DATA_BYTES (64U * 1024U) -#define KVMFR_CLIPBOARD_SIZE_UNKNOWN UINT64_MAX +#define KVMFR_CLIPBOARD_VERSION 2U +#define KVMFR_CLIPBOARD_SLOT_COUNT 8U +#define KVMFR_CLIPBOARD_DATA_BYTES (64U * 1024U) +#define KVMFR_CLIPBOARD_SIZE_UNKNOWN UINT64_MAX +#define KVMFR_CLIPBOARD_FILE_READ_BYTES (1024U * 1024U) +#define KVMFR_CLIPBOARD_FILE_ROOT_NODE UINT64_C(0) +#define KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS 8U +#define KVMFR_CLIPBOARD_FILE_MAX_REQUESTS 32U /* Transfer IDs are selected by the side which sends REQUEST. Keeping the * namespaces disjoint makes a simultaneous transfer in each direction @@ -48,34 +52,44 @@ static inline int kvmfrClipboardTransferFromClient(uint64_t transfer) enum { - KVMFR_CLIPBOARD_FORMAT_NONE = 0, - KVMFR_CLIPBOARD_FORMAT_TEXT = 1, - KVMFR_CLIPBOARD_FORMAT_PNG = 2, - KVMFR_CLIPBOARD_FORMAT_BMP = 3, - KVMFR_CLIPBOARD_FORMAT_TIFF = 4, - KVMFR_CLIPBOARD_FORMAT_JPEG = 5 + KVMFR_CLIPBOARD_FORMAT_NONE = 0, + KVMFR_CLIPBOARD_FORMAT_TEXT = 1, + KVMFR_CLIPBOARD_FORMAT_PNG = 2, + KVMFR_CLIPBOARD_FORMAT_BMP = 3, + KVMFR_CLIPBOARD_FORMAT_TIFF = 4, + KVMFR_CLIPBOARD_FORMAT_JPEG = 5, + KVMFR_CLIPBOARD_FORMAT_FILES = 6 }; typedef uint32_t KVMFRClipboardFormat; enum { - KVMFR_CLIPBOARD_FORMAT_MASK_TEXT = 1U << 0, - KVMFR_CLIPBOARD_FORMAT_MASK_PNG = 1U << 1, - KVMFR_CLIPBOARD_FORMAT_MASK_BMP = 1U << 2, - KVMFR_CLIPBOARD_FORMAT_MASK_TIFF = 1U << 3, - KVMFR_CLIPBOARD_FORMAT_MASK_JPEG = 1U << 4, - KVMFR_CLIPBOARD_FORMAT_MASK_ALL = + KVMFR_CLIPBOARD_FORMAT_MASK_TEXT = 1U << 0, + KVMFR_CLIPBOARD_FORMAT_MASK_PNG = 1U << 1, + KVMFR_CLIPBOARD_FORMAT_MASK_BMP = 1U << 2, + KVMFR_CLIPBOARD_FORMAT_MASK_TIFF = 1U << 3, + KVMFR_CLIPBOARD_FORMAT_MASK_JPEG = 1U << 4, + KVMFR_CLIPBOARD_FORMAT_MASK_FILES = 1U << 5, + KVMFR_CLIPBOARD_FORMAT_MASK_ALL = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT | KVMFR_CLIPBOARD_FORMAT_MASK_PNG | KVMFR_CLIPBOARD_FORMAT_MASK_BMP | KVMFR_CLIPBOARD_FORMAT_MASK_TIFF | - KVMFR_CLIPBOARD_FORMAT_MASK_JPEG, + KVMFR_CLIPBOARD_FORMAT_MASK_JPEG | + KVMFR_CLIPBOARD_FORMAT_MASK_FILES, }; typedef uint32_t KVMFRClipboardFormatFlags; static inline int kvmfrClipboardFormatValid(KVMFRClipboardFormat format) +{ + return format >= KVMFR_CLIPBOARD_FORMAT_TEXT && + format <= KVMFR_CLIPBOARD_FORMAT_FILES; +} + +static inline int kvmfrClipboardRepresentationFormatValid( + KVMFRClipboardFormat format) { return format >= KVMFR_CLIPBOARD_FORMAT_TEXT && format <= KVMFR_CLIPBOARD_FORMAT_JPEG; @@ -90,17 +104,23 @@ static inline KVMFRClipboardFormatFlags kvmfrClipboardFormatFlag( enum { - KVMFR_CLIPBOARD_MESSAGE_CLAIM = 1, - KVMFR_CLIPBOARD_MESSAGE_RELEASE = 2, - KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE = 3, - KVMFR_CLIPBOARD_MESSAGE_OFFER = 4, - KVMFR_CLIPBOARD_MESSAGE_CLEAR = 5, - KVMFR_CLIPBOARD_MESSAGE_REQUEST = 6, - KVMFR_CLIPBOARD_MESSAGE_DATA = 7, - KVMFR_CLIPBOARD_MESSAGE_COMMIT = 8, - KVMFR_CLIPBOARD_MESSAGE_CANCEL = 9, - KVMFR_CLIPBOARD_MESSAGE_ACK = 10, - KVMFR_CLIPBOARD_MESSAGE_GRANT = 11 + KVMFR_CLIPBOARD_MESSAGE_CLAIM = 1, + KVMFR_CLIPBOARD_MESSAGE_RELEASE = 2, + KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE = 3, + KVMFR_CLIPBOARD_MESSAGE_OFFER = 4, + KVMFR_CLIPBOARD_MESSAGE_CLEAR = 5, + KVMFR_CLIPBOARD_MESSAGE_REQUEST = 6, + KVMFR_CLIPBOARD_MESSAGE_DATA = 7, + KVMFR_CLIPBOARD_MESSAGE_COMMIT = 8, + KVMFR_CLIPBOARD_MESSAGE_CANCEL = 9, + KVMFR_CLIPBOARD_MESSAGE_ACK = 10, + KVMFR_CLIPBOARD_MESSAGE_GRANT = 11, + KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE = 12, + KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED = 13, + KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE = 14, + KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST = 15, + KVMFR_CLIPBOARD_MESSAGE_FILE_DATA = 16, + KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL = 17 }; typedef uint32_t KVMFRClipboardMessageType; @@ -135,6 +155,179 @@ typedef struct KVMFRClipboardMessage } KVMFRClipboardMessage; +enum +{ + KVMFR_CLIPBOARD_FILE_OP_LIST = 1, + KVMFR_CLIPBOARD_FILE_OP_READ = 2 +}; + +typedef uint32_t KVMFRClipboardFileOperation; + +enum +{ + KVMFR_CLIPBOARD_FILE_TYPE_REGULAR = 1, + KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY = 2 +}; + +typedef uint32_t KVMFRClipboardFileType; + +enum +{ + KVMFR_CLIPBOARD_FILE_ERROR_NONE = 0, + KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND = 1, + KVMFR_CLIPBOARD_FILE_ERROR_ACCESS = 2, + KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY = 3, + KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY = 4, + KVMFR_CLIPBOARD_FILE_ERROR_IO = 5, + KVMFR_CLIPBOARD_FILE_ERROR_INVALID = 6, + KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY = 7, + KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE = 8, + KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED = 9, + KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED = 10, + KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED = 11, + KVMFR_CLIPBOARD_FILE_ERROR_STALE = 12 +}; + +typedef uint32_t KVMFRClipboardFileError; + +static inline int kvmfrClipboardFileErrorValid( + KVMFRClipboardFileError error) +{ + return error <= KVMFR_CLIPBOARD_FILE_ERROR_STALE; +} + +/* + * A LIST response is a stream of these headers, each immediately followed by + * nameLength bytes of an unescaped UTF-8 path component and then zero padding + * to the next eight-byte boundary. Node zero is the dataset's synthetic root + * and is never returned as an entry. Times are nanoseconds since the Unix + * epoch. A zero creation time means the source cannot report it. + */ +typedef struct KVMFRClipboardFileEntry +{ + uint64_t node; + uint64_t size; + uint64_t createdNs; + uint64_t modifiedNs; + KVMFRClipboardFileType type; + uint32_t nameLength; +} +KVMFRClipboardFileEntry; + +#define KVMFR_CLIPBOARD_FILE_ENTRY_ALIGN 8U +#define KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameLength) \ + ((uint64_t)(sizeof(KVMFRClipboardFileEntry) + (uint64_t)(nameLength) + \ + (KVMFR_CLIPBOARD_FILE_ENTRY_ALIGN - 1U)) & \ + ~(uint64_t)(KVMFR_CLIPBOARD_FILE_ENTRY_ALIGN - 1U)) + +/* File message field use: + * + * FILE_ACQUIRE / FILE_ACQUIRED / FILE_RELEASE: + * clipboardGeneration identifies the immutable dataset and transfer is the + * acquisition ID selected by the requester. format is FILES. ACQUIRED token + * is a KVMFRClipboardFileError; all other fields are zero. + * + * FILE_REQUEST: + * clipboardGeneration identifies the acquired dataset, transfer is a unique + * request ID, size is the opaque node ID, token is LIST or READ, and format + * is FILES. LIST requires offset and flags to be zero. READ uses offset as + * the file offset and flags as the requested byte count (at most + * KVMFR_CLIPBOARD_FILE_READ_BYTES). length is always zero because REQUEST + * has no attached payload. + * + * FILE_DATA: + * clipboardGeneration, transfer, format and token echo FILE_REQUEST. offset + * is relative to this response (not the source file), sequence starts at + * zero, and size follows DATA's BEGIN/END total-size rules. Chunks for + * different request IDs may be interleaved. + * + * FILE_CANCEL: + * clipboardGeneration and transfer identify an acquisition or request, + * format is FILES, and token is a non-zero KVMFRClipboardFileError. + */ + +static inline int kvmfrClipboardFileOperationValid( + KVMFRClipboardFileOperation operation) +{ + return operation == KVMFR_CLIPBOARD_FILE_OP_LIST || + operation == KVMFR_CLIPBOARD_FILE_OP_READ; +} + +static inline int kvmfrClipboardFileTransferValid(uint64_t transfer) +{ + return transfer != 0; +} + +static inline int kvmfrClipboardFileMessageType( + KVMFRClipboardMessageType type) +{ + return type >= KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE && + type <= KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; +} + +static inline int kvmfrClipboardFileMessageValid( + const KVMFRClipboardMessage * message) +{ + if (!message || message->version != KVMFR_CLIPBOARD_VERSION || + !message->clipboardGeneration || + !kvmfrClipboardFileTransferValid(message->transfer) || + message->format != KVMFR_CLIPBOARD_FORMAT_FILES || + message->length > KVMFR_CLIPBOARD_DATA_BYTES) + return 0; + + switch (message->type) + { + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + return !message->sequence && !message->offset && !message->size && + !message->flags && !message->token && !message->length; + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + return !message->sequence && !message->offset && !message->size && + !message->flags && + kvmfrClipboardFileErrorValid(message->token) && !message->length; + + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + if (message->sequence || + !kvmfrClipboardFileOperationValid(message->token) || + message->length || + (message->token == KVMFR_CLIPBOARD_FILE_OP_READ && + !message->size)) + return 0; + if (message->token == KVMFR_CLIPBOARD_FILE_OP_LIST) + return !message->offset && !message->flags; + return message->flags && + message->flags <= KVMFR_CLIPBOARD_FILE_READ_BYTES && + message->offset <= UINT64_MAX - message->flags; + + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + { + if (!kvmfrClipboardFileOperationValid(message->token) || + (message->flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END)) || + (!message->length && + !(message->flags & KVMFR_CLIPBOARD_FLAG_END)) || + message->offset > UINT64_MAX - message->length) + return 0; + const uint64_t end = message->offset + message->length; + if (message->flags & KVMFR_CLIPBOARD_FLAG_END) + return message->size == end; + if (!(message->flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) + return message->size == KVMFR_CLIPBOARD_SIZE_UNKNOWN; + return message->size == KVMFR_CLIPBOARD_SIZE_UNKNOWN || + message->size >= end; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + return !message->sequence && !message->offset && !message->size && + !message->flags && message->token && + kvmfrClipboardFileErrorValid(message->token) && !message->length; + + default: + return 0; + } +} + /* Type-specific fields: * OFFER: token is KVMFRClipboardFormatFlags. * REQUEST: format and transfer identify the requested representation. @@ -186,6 +379,8 @@ typedef uint32_t KVMFRClipboardQueueType; #if defined(__cplusplus) static_assert(sizeof(KVMFRClipboardMessage) == 64, "KVMFR clipboard control message layout changed"); +static_assert(sizeof(KVMFRClipboardFileEntry) == 40, + "KVMFR clipboard file entry layout changed"); static_assert(sizeof(KVMFRClipboardStatus) == 32, "KVMFR clipboard status layout changed"); static_assert(sizeof(KVMFRClipboardSlotHeader) == 64, @@ -195,6 +390,8 @@ static_assert(sizeof(KVMFRClipboardMessage) <= 64, #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L _Static_assert(sizeof(KVMFRClipboardMessage) == 64, "KVMFR clipboard control message layout changed"); +_Static_assert(sizeof(KVMFRClipboardFileEntry) == 40, + "KVMFR clipboard file entry layout changed"); _Static_assert(sizeof(KVMFRClipboardStatus) == 32, "KVMFR clipboard status layout changed"); _Static_assert(sizeof(KVMFRClipboardSlotHeader) == 64, diff --git a/common/include/common/LGMPConfig.h b/common/include/common/LGMPConfig.h index f88ac621..6a74a11e 100644 --- a/common/include/common/LGMPConfig.h +++ b/common/include/common/LGMPConfig.h @@ -34,6 +34,6 @@ #define LGMP_Q_FRAME_BUFFER_LEN 3 #define LGMP_Q_POINTER_LEN 32 #define LGMP_Q_INPUT_LEN 4 -#define LGMP_Q_CLIPBOARD_LEN 8 +#define LGMP_Q_CLIPBOARD_LEN 32 #endif