/** * 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 "interface/transport.h" #include "common/KVMFR.h" #include "common/LGMPConfig.h" #include "common/debug.h" #include "common/ivshmem.h" #include "common/locking.h" #include "common/option.h" #include "common/stringutils.h" #include #include #include #include #include #include struct DMAFrameInfo { const KVMFRFrame * frame; size_t dataSize; int fd; }; struct LG_Transport { struct IVSHMEM shm; PLGMPClient client; PLGMPClientQueue frameQueue; PLGMPClientQueue pointerQueue; LG_Lock pointerLock; unsigned cursorPollInterval; unsigned framePollInterval; bool allowDMA; bool connected; bool framePending; uint32_t frameSerial; bool formatValid; LG_TransportFrameFormat format; struct DMAFrameInfo dma[LGMP_Q_FRAME_LEN]; uint8_t * pointerData; size_t pointerDataSize; }; static bool lgmp_deviceValidator(struct Option * opt, const char ** error) { if (strlen(opt->value.x_string) > 3 && memcmp(opt->value.x_string, "kvmfr", 5) != 0) { struct stat st; if (stat(opt->value.x_string, &st) != 0) { *error = "Invalid path to the shared memory file"; return false; } } return true; } static StringList lgmp_deviceValues(struct Option * option) { StringList values = stringlist_new(true); DIR * dir = opendir("/sys/class/kvmfr"); if (!dir) return values; struct dirent * entry; while ((entry = readdir(dir))) { if (entry->d_name[0] == '.') continue; char * name; alloc_sprintf(&name, "/dev/%s", entry->d_name); stringlist_push(values, name); } closedir(dir); return values; } static void lgmp_setup(void) { struct stat st; const char * defaultDevice = stat("/dev/kvmfr0", &st) == 0 ? "/dev/kvmfr0" : "/dev/shm/looking-glass"; static struct Option options[] = { { .module = "lgmp", .name = "shmDevice", .shortopt = 'f', .description = "Shared memory file or KVMFR device path", .type = OPTION_TYPE_STRING, .value.x_string = NULL, .validator = lgmp_deviceValidator, .getValues = lgmp_deviceValues, }, { .module = "lgmp", .name = "allowDMA", .description = "Allow direct DMA transfers when supported", .type = OPTION_TYPE_BOOL, .value.x_bool = true, }, { .module = "lgmp", .name = "framePollInterval", .description = "Frame queue polling interval in microseconds", .type = OPTION_TYPE_INT, .value.x_int = 1000, }, { .module = "lgmp", .name = "cursorPollInterval", .description = "Pointer queue polling interval in microseconds", .type = OPTION_TYPE_INT, .value.x_int = 1000, }, {0} }; options[0].value.x_string = (char *)defaultDevice; option_register(options); } static bool lgmp_create(LG_Transport ** result) { struct LG_Transport * this = calloc(1, sizeof(*this)); if (!this) return false; this->allowDMA = option_get_bool("lgmp", "allowDMA"); const int framePoll = option_get_int("lgmp", "framePollInterval"); const int cursorPoll = option_get_int("lgmp", "cursorPollInterval"); if (framePoll < 0 || cursorPoll < 0) { DEBUG_ERROR("LGMP polling intervals cannot be negative"); free(this); return false; } this->framePollInterval = framePoll; this->cursorPollInterval = cursorPoll; for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) this->dma[i].fd = -1; LG_LOCK_INIT(this->pointerLock); if (!ivshmemOpenDev(&this->shm, option_get_string("lgmp", "shmDevice"))) { LG_LOCK_FREE(this->pointerLock); free(this); return false; } LGMP_STATUS status = lgmpClientInit(this->shm.mem, this->shm.size, &this->client); if (status != LGMP_OK) { DEBUG_ERROR("lgmpClientInit failed: %s", lgmpStatusString(status)); ivshmemClose(&this->shm); LG_LOCK_FREE(this->pointerLock); free(this); return false; } *result = this; return true; } static void lgmp_closeQueues(struct LG_Transport * this) { if (this->framePending && this->frameQueue) { lgmpClientMessageDone(this->frameQueue); this->framePending = false; } lgmpClientUnsubscribe(&this->frameQueue); LG_LOCK(this->pointerLock); lgmpClientUnsubscribe(&this->pointerQueue); LG_UNLOCK(this->pointerLock); } static void lgmp_closeDMA(struct LG_Transport * this) { for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) { if (this->dma[i].fd >= 0) close(this->dma[i].fd); this->dma[i].fd = -1; this->dma[i].frame = NULL; this->dma[i].dataSize = 0; } } static void lgmp_destroy(LG_Transport ** transport) { if (!transport || !*transport) return; struct LG_Transport * this = *transport; lgmp_closeQueues(this); lgmp_closeDMA(this); free(this->pointerData); lgmpClientFree(&this->client); ivshmemClose(&this->shm); LG_LOCK_FREE(this->pointerLock); free(this); *transport = NULL; } static bool lgmp_parseSession(const uint8_t * data, uint32_t size, LG_TransportSession * session) { if (size < sizeof(KVMFR)) return false; const KVMFR * header = (const KVMFR *)data; if (memcmp(header->magic, KVMFR_MAGIC, sizeof(header->magic)) != 0 || header->version != KVMFR_VERSION) { session->remoteVersion = header->version; return false; } str_copy(session->version, sizeof(session->version), header->hostver, sizeof(header->hostver)); if (header->features & KVMFR_FEATURE_SETCURSORPOS) session->features |= LG_TRANSPORT_FEATURE_SET_CURSOR_POS; if (header->features & KVMFR_FEATURE_WINDOWSIZE) session->features |= LG_TRANSPORT_FEATURE_WINDOW_SIZE; data += sizeof(*header); size -= sizeof(*header); while (size >= sizeof(KVMFRRecord)) { const KVMFRRecord * record = (const KVMFRRecord *)data; data += sizeof(*record); size -= sizeof(*record); if (record->size > size) break; switch (record->type) { case KVMFR_RECORD_VMINFO: if (record->size >= sizeof(KVMFRRecord_VMInfo)) { const KVMFRRecord_VMInfo * info = (const KVMFRRecord_VMInfo *)data; memcpy(session->uuid, info->uuid, sizeof(session->uuid)); for (unsigned i = 0; i < sizeof(session->uuid); ++i) session->uuidValid |= session->uuid[i] != 0; str_copy(session->capture, sizeof(session->capture), info->capture, sizeof(info->capture)); session->cpus = info->cpus; session->cores = info->cores; session->sockets = info->sockets; const size_t fixed = offsetof(KVMFRRecord_VMInfo, model); str_copy(session->cpuModel, sizeof(session->cpuModel), info->model, record->size - fixed); } break; case KVMFR_RECORD_OSINFO: if (record->size >= sizeof(KVMFRRecord_OSInfo)) { const KVMFRRecord_OSInfo * info = (const KVMFRRecord_OSInfo *)data; session->os = info->os <= KVMFR_OS_OTHER ? (LG_TransportGuestOS)info->os : LG_TRANSPORT_OS_OTHER; const size_t fixed = offsetof(KVMFRRecord_OSInfo, name); str_copy(session->osName, sizeof(session->osName), info->name, record->size - fixed); } break; } data += record->size; size -= record->size; } return true; } static LG_TransportStatus lgmp_connect(LG_Transport * this, LG_TransportSession * session) { memset(session, 0, sizeof(*session)); session->os = LG_TRANSPORT_OS_OTHER; uint32_t size; uint8_t * data; uint32_t remoteVersion; LGMP_STATUS status = lgmpClientSessionInit(this->client, &size, &data, NULL, &remoteVersion); session->remoteVersion = remoteVersion; switch (status) { case LGMP_OK: if (!lgmp_parseSession(data, size, session)) return LG_TRANSPORT_INVALID_VERSION; this->connected = true; this->frameSerial = 0; this->formatValid = false; return LG_TRANSPORT_OK; case LGMP_ERR_INVALID_VERSION: return LG_TRANSPORT_INVALID_VERSION; case LGMP_ERR_INVALID_SESSION: case LGMP_ERR_INVALID_MAGIC: return LG_TRANSPORT_UNAVAILABLE; default: DEBUG_ERROR("lgmpClientSessionInit failed: %s", lgmpStatusString(status)); return LG_TRANSPORT_ERROR; } } static void lgmp_disconnect(LG_Transport * this) { lgmp_closeQueues(this); lgmp_closeDMA(this); this->connected = false; this->frameSerial = 0; this->formatValid = false; } static bool lgmp_sessionValid(LG_Transport * this) { return this->connected && lgmpClientSessionValid(this->client); } static bool lgmp_supportsDMA(LG_Transport * this) { return this->allowDMA && ivshmemHasDMA(&this->shm); } static bool lgmp_attachRenderer(LG_Transport * this, const LG_RendererInterop * interop) { return true; } static void lgmp_detachRenderer(LG_Transport * this) { } static LG_TransportStatus lgmp_subscribe(PLGMPClient client, uint32_t id, PLGMPClientQueue * queue) { if (*queue) return LG_TRANSPORT_OK; LGMP_STATUS status = lgmpClientSubscribe(client, id, queue); switch (status) { case LGMP_OK: return LG_TRANSPORT_OK; case LGMP_ERR_NO_SUCH_QUEUE: return LG_TRANSPORT_TIMEOUT; case LGMP_ERR_INVALID_SESSION: return LG_TRANSPORT_DISCONNECTED; default: DEBUG_ERROR("lgmpClientSubscribe failed: %s", lgmpStatusString(status)); return LG_TRANSPORT_ERROR; } } static LG_TransportStatus lgmp_process(PLGMPClientQueue queue, unsigned interval, LGMPMessage * message) { LGMP_STATUS status = lgmpClientProcess(queue, message); switch (status) { case LGMP_OK: return LG_TRANSPORT_OK; case LGMP_ERR_QUEUE_EMPTY: if (interval) usleep(interval); return LG_TRANSPORT_TIMEOUT; case LGMP_ERR_INVALID_SESSION: return LG_TRANSPORT_DISCONNECTED; default: DEBUG_ERROR("lgmpClientProcess failed: %s", lgmpStatusString(status)); return LG_TRANSPORT_ERROR; } } static int lgmp_getDMA(struct LG_Transport * this, const KVMFRFrame * frame, size_t dataSize) { struct DMAFrameInfo * dma = NULL; for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) if (this->dma[i].frame == frame) { dma = &this->dma[i]; if (dma->dataSize < dataSize && dma->fd >= 0) { close(dma->fd); dma->fd = -1; } break; } if (!dma) for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) if (!this->dma[i].frame) { dma = &this->dma[i]; dma->frame = frame; break; } if (!dma) return -1; if (dma->fd >= 0) return dma->fd; const uintptr_t position = (uintptr_t)frame - (uintptr_t)this->shm.mem; const uintptr_t offset = frame->offset + sizeof(FrameBuffer); dma->dataSize = dataSize; dma->fd = ivshmemGetDMABuf(&this->shm, position + offset, dataSize); return dma->fd; } static LG_TransportStatus lgmp_nextFrame(LG_Transport * this, bool useDMA, LG_TransportFrame * result) { if (!this->connected) return LG_TRANSPORT_DISCONNECTED; if (this->framePending) return LG_TRANSPORT_ERROR; LG_TransportStatus status = lgmp_subscribe(this->client, LGMP_Q_FRAME, &this->frameQueue); if (status != LG_TRANSPORT_OK) { if (status == LG_TRANSPORT_TIMEOUT) usleep(1000); return status; } LGMPMessage message; status = lgmp_process(this->frameQueue, this->framePollInterval, &message); if (status != LG_TRANSPORT_OK) return status; if (message.size < sizeof(KVMFRFrame)) { lgmpClientMessageDone(this->frameQueue); DEBUG_ERROR("LGMP frame payload is too small"); return LG_TRANSPORT_ERROR; } const KVMFRFrame * frame = (const KVMFRFrame *)message.mem; const size_t frameDataSize = (size_t)frame->dataHeight * frame->pitch; if (frame->type <= FRAME_TYPE_INVALID || frame->type >= FRAME_TYPE_MAX || frame->offset > message.size - sizeof(FrameBuffer) || frameDataSize > message.size - frame->offset - sizeof(FrameBuffer)) { lgmpClientMessageDone(this->frameQueue); DEBUG_ERROR("LGMP frame payload contains invalid dimensions or offsets"); return LG_TRANSPORT_ERROR; } if (frame->frameSerial == this->frameSerial && this->frameSerial) { lgmpClientMessageDone(this->frameQueue); return LG_TRANSPORT_TIMEOUT; } this->frameSerial = frame->frameSerial; memset(result, 0, sizeof(*result)); result->serial = frame->frameSerial; if (frame->flags & FRAME_FLAG_BLOCK_SCREENSAVER) result->flags |= LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER; if (frame->flags & FRAME_FLAG_REQUEST_ACTIVATION) result->flags |= LG_TRANSPORT_FRAME_REQUEST_ACTIVATION; if (frame->flags & FRAME_FLAG_TRUNCATED) result->flags |= LG_TRANSPORT_FRAME_TRUNCATED; LG_TransportFrameFormat * format = &this->format; if (!this->formatValid || format->version != frame->formatVer) { memset(format, 0, sizeof(*format)); format->version = frame->formatVer; format->type = frame->type; format->screenWidth = frame->screenWidth; format->screenHeight = frame->screenHeight; format->dataWidth = frame->dataWidth; format->dataHeight = frame->dataHeight; format->frameWidth = frame->frameWidth; format->frameHeight = frame->frameHeight; format->rotation = frame->rotation; format->stride = frame->stride; format->pitch = frame->pitch; format->hdr = frame->flags & FRAME_FLAG_HDR; format->hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ; format->hdrMetadata = frame->flags & FRAME_FLAG_HDR_METADATA; format->sdrWhiteLevel = frame->sdrWhiteLevel ? frame->sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT; if (format->hdrMetadata) { memcpy(format->hdrDisplayPrimary, frame->hdrDisplayPrimary, sizeof(format->hdrDisplayPrimary)); memcpy(format->hdrWhitePoint, frame->hdrWhitePoint, sizeof(format->hdrWhitePoint)); format->hdrMaxDisplayLuminance = frame->hdrMaxDisplayLuminance; format->hdrMinDisplayLuminance = frame->hdrMinDisplayLuminance; format->hdrMaxContentLightLevel = frame->hdrMaxContentLightLevel; format->hdrMaxFrameAverageLightLevel = frame->hdrMaxFrameAverageLightLevel; } this->formatValid = true; } result->format = format; result->framebuffer = (const FrameBuffer *)((const uint8_t *)frame + frame->offset); result->dmaFD = -1; if (useDMA) { const size_t dataSize = (size_t)format->dataHeight * format->pitch; result->dmaFD = lgmp_getDMA(this, frame, dataSize); if (result->dmaFD < 0) { lgmpClientMessageDone(this->frameQueue); DEBUG_ERROR("Failed to obtain a DMA buffer for the frame"); return LG_TRANSPORT_ERROR; } } if (frame->damageRectsCount <= KVMFR_MAX_DAMAGE_RECTS) { result->damageRects = frame->damageRects; result->damageRectsCount = frame->damageRectsCount; } else DEBUG_WARN("Invalid damage rectangles, forcing a full update"); this->framePending = true; return LG_TRANSPORT_OK; } static void lgmp_releaseFrame(LG_Transport * this, LG_TransportFrame * frame) { if (this->framePending && this->frameQueue) lgmpClientMessageDone(this->frameQueue); this->framePending = false; memset(frame, 0, sizeof(*frame)); } static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, LG_TransportPointer * result) { if (!this->connected) return LG_TRANSPORT_DISCONNECTED; uint32_t pointerFlags = 0; size_t shapeSize = 0; size_t transformSize = 0; LG_LOCK(this->pointerLock); LG_TransportStatus status = lgmp_subscribe(this->client, LGMP_Q_POINTER, &this->pointerQueue); LG_UNLOCK(this->pointerLock); if (status == LG_TRANSPORT_OK) { LGMPMessage message; status = lgmp_process(this->pointerQueue, this->cursorPollInterval, &message); if (status == LG_TRANSPORT_OK) { if (message.size < sizeof(KVMFRCursor)) { lgmpClientMessageDone(this->pointerQueue); status = LG_TRANSPORT_ERROR; } else { const KVMFRCursor * cursor = (const KVMFRCursor *)message.mem; shapeSize = message.udata & CURSOR_FLAG_SHAPE ? (size_t)cursor->height * cursor->pitch : 0; transformSize = message.udata & CURSOR_FLAG_COLOR_TRANSFORM ? sizeof(KVMFRColorTransform) : 0; if (shapeSize > message.size - sizeof(*cursor) || transformSize > message.size - sizeof(*cursor) - shapeSize) { lgmpClientMessageDone(this->pointerQueue); status = LG_TRANSPORT_ERROR; } else { const size_t needed = sizeof(*cursor) + shapeSize + transformSize; if (needed > this->pointerDataSize) { void * data = realloc(this->pointerData, needed); if (!data) status = LG_TRANSPORT_ERROR; else { this->pointerData = data; this->pointerDataSize = needed; } } if (status == LG_TRANSPORT_OK) { memcpy(this->pointerData, message.mem, needed); pointerFlags = message.udata; } lgmpClientMessageDone(this->pointerQueue); } } } } if (status != LG_TRANSPORT_OK) { if (status == LG_TRANSPORT_TIMEOUT) usleep(1000); return status; } const KVMFRCursor * cursor = (const KVMFRCursor *)this->pointerData; memset(result, 0, sizeof(*result)); if (pointerFlags & CURSOR_FLAG_POSITION) result->flags |= LG_TRANSPORT_POINTER_POSITION; if (pointerFlags & CURSOR_FLAG_VISIBLE) result->flags |= LG_TRANSPORT_POINTER_VISIBLE; if (pointerFlags & CURSOR_FLAG_SHAPE) result->flags |= LG_TRANSPORT_POINTER_SHAPE; if (pointerFlags & CURSOR_FLAG_COLOR_TRANSFORM) result->flags |= LG_TRANSPORT_POINTER_COLOR_TRANSFORM; if (pointerFlags & CURSOR_FLAG_VISIBLE_VALID) result->flags |= LG_TRANSPORT_POINTER_VISIBLE_VALID; result->x = cursor->x; result->y = cursor->y; result->type = cursor->type; result->hx = cursor->hx; result->hy = cursor->hy; result->width = cursor->width; result->height = cursor->height; result->pitch = cursor->pitch; result->sdrWhiteLevel = cursor->sdrWhiteLevel; result->shape = (const uint8_t *)(cursor + 1); if (transformSize) result->colorTransform = (const LGColorTransform *)(result->shape + shapeSize); return LG_TRANSPORT_OK; } static void lgmp_releasePointer(LG_Transport * this, LG_TransportPointer * pointer) { memset(pointer, 0, sizeof(*pointer)); } static LG_TransportStatus lgmp_sendControl(LG_Transport * this, const LG_TransportControl * control, LG_TransportControlToken * token) { uint8_t buffer[sizeof(KVMFRWindowSize)]; uint32_t size; switch (control->type) { case LG_TRANSPORT_CONTROL_SET_CURSOR_POS: { const KVMFRSetCursorPos message = { .msg.type = KVMFR_MESSAGE_SETCURSORPOS, .x = control->cursorPos.x, .y = control->cursorPos.y, }; memcpy(buffer, &message, sizeof(message)); size = sizeof(message); break; } case LG_TRANSPORT_CONTROL_WINDOW_SIZE: { const KVMFRWindowSize message = { .msg.type = KVMFR_MESSAGE_WINDOWSIZE, .w = control->windowSize.width, .h = control->windowSize.height, }; memcpy(buffer, &message, sizeof(message)); size = sizeof(message); break; } default: return LG_TRANSPORT_ERROR; } LG_LOCK(this->pointerLock); if (!this->pointerQueue) { LG_UNLOCK(this->pointerLock); return LG_TRANSPORT_UNAVAILABLE; } uint32_t serial; LGMP_STATUS status = lgmpClientSendData(this->pointerQueue, buffer, size, &serial); LG_UNLOCK(this->pointerLock); if (status != LGMP_OK) return status == LGMP_ERR_INVALID_SESSION ? LG_TRANSPORT_DISCONNECTED : LG_TRANSPORT_ERROR; *token = serial; return LG_TRANSPORT_OK; } static LG_TransportStatus lgmp_controlStatus(LG_Transport * this, LG_TransportControlToken token) { LG_LOCK(this->pointerLock); if (!this->pointerQueue) { LG_UNLOCK(this->pointerLock); return LG_TRANSPORT_DISCONNECTED; } uint32_t serial; LGMP_STATUS status = lgmpClientGetSerial(this->pointerQueue, &serial); LG_UNLOCK(this->pointerLock); if (status != LGMP_OK) return status == LGMP_ERR_INVALID_SESSION ? LG_TRANSPORT_DISCONNECTED : LG_TRANSPORT_ERROR; return serial >= token ? LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE; } const LG_TransportOps LGT_LGMP = { .name = "lgmp", .setup = lgmp_setup, .create = lgmp_create, .destroy = lgmp_destroy, .connect = lgmp_connect, .disconnect = lgmp_disconnect, .sessionValid = lgmp_sessionValid, .supportsDMA = lgmp_supportsDMA, .attachRenderer = lgmp_attachRenderer, .detachRenderer = lgmp_detachRenderer, .nextFrame = lgmp_nextFrame, .releaseFrame = lgmp_releaseFrame, .nextPointer = lgmp_nextPointer, .releasePointer = lgmp_releasePointer, .sendControl = lgmp_sendControl, .controlStatus = lgmp_controlStatus, };