/** * Looking Glass * Copyright © 2017-2023 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 */ #define _GNU_SOURCE //needed for pthread_setname_np #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "rgb24.effect.h" /** * the following comes from drm_fourcc.h and is included here to avoid the * external dependency for the few simple defines we need */ #define fourcc_code(a, b, c, d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \ ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24)) #define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4') #define DRM_FORMAT_ABGR8888 fourcc_code('A', 'B', '2', '4') #define DRM_FORMAT_BGRA1010102 fourcc_code('B', 'A', '3', '0') #define DRM_FORMAT_ABGR16161616F fourcc_code('A', 'B', '4', 'H') typedef enum { STATE_STOPPED, STATE_OPEN, STATE_STARTING, STATE_RUNNING, STATE_STOPPING, STATE_RESTARTING } LGState; typedef struct { KVMFRFrame * frame; size_t dataSize; int fd; } DMAFrameInfo; typedef struct { obs_source_t * context; LGState state; char * shmFile; uint32_t formatVer; uint32_t screenWidth, screenHeight; uint32_t dataWidth, dataHeight; uint32_t frameWidth, frameHeight; struct vec2 screenScale; FrameType type; int bpp; struct IVSHMEM shmDev; PLGMPClient lgmp; PLGMPClientQueue frameQueue, pointerQueue; gs_texture_t * texture; gs_texture_t * dstTexture; uint8_t * texData; uint32_t linesize; bool hideMouse; #if LIBOBS_API_MAJOR_VER >= 27 bool dmabuf; DMAFrameInfo dmaInfo[LGMP_Q_FRAME_LEN]; #endif pthread_t frameThread, pointerThread; os_sem_t * frameSem; bool cursorMono; gs_texture_t * cursorTex; struct gs_rect cursorRect; bool cursorVisible; KVMFRCursor cursor; os_sem_t * cursorSem; atomic_uint cursorVer; unsigned int cursorCurVer; uint32_t cursorSize; uint32_t * cursorData; gs_effect_t * unpackEffect; gs_eparam_t * image; gs_eparam_t * outputSize; gs_eparam_t * swap; } LGPlugin; static void * frameThread(void * data); static void * pointerThread(void * data); static void lgUpdate(void * data, obs_data_t * settings); static const char * lgGetName(void * unused) { return obs_module_text("Looking Glass Client"); } static void * lgCreate(obs_data_t * settings, obs_source_t * context) { LGPlugin * this = bzalloc(sizeof(LGPlugin)); this->context = context; obs_enter_graphics(); char * error = NULL; this->unpackEffect = gs_effect_create(b_effect_rgb24_effect, "rbg24", &error); if (!this->unpackEffect) { blog(LOG_ERROR, "%s", error); bfree(error); bfree(this); obs_leave_graphics(); return NULL; } this->image = gs_effect_get_param_by_name(this->unpackEffect, "image" ); this->outputSize = gs_effect_get_param_by_name(this->unpackEffect, "outputSize"); this->swap = gs_effect_get_param_by_name(this->unpackEffect, "swap" ); obs_leave_graphics(); os_sem_init (&this->frameSem , 0); os_sem_init (&this->cursorSem, 1); atomic_store(&this->cursorVer, 0); lgUpdate(this, settings); return this; } static void createThreads(LGPlugin * this) { pthread_create(&this->frameThread, NULL, frameThread, this); pthread_setname_np(this->frameThread, "LGFrameThread"); pthread_create(&this->pointerThread, NULL, pointerThread, this); pthread_setname_np(this->pointerThread, "LGPointerThread"); } static void waitThreads(LGPlugin * this) { pthread_join(this->frameThread, NULL); pthread_join(this->pointerThread, NULL); } static void deinit(LGPlugin * this) { switch(this->state) { case STATE_STARTING: /* wait for startup to finish */ while(this->state == STATE_STARTING) usleep(1); /* fallthrough */ case STATE_RUNNING: case STATE_STOPPING: case STATE_RESTARTING: this->state = STATE_STOPPING; waitThreads(this); this->state = STATE_STOPPED; /* fallthrough */ case STATE_OPEN: #if LIBOBS_API_MAJOR_VER >= 27 for (int i = 0 ; i < ARRAY_LENGTH(this->dmaInfo); ++i) if (this->dmaInfo[i].fd >= 0) { close(this->dmaInfo[i].fd); this->dmaInfo[i].fd = -1; } #endif lgmpClientFree(&this->lgmp); ivshmemClose(&this->shmDev); break; case STATE_STOPPED: break; } if (this->shmFile) { bfree(this->shmFile); this->shmFile = NULL; } obs_enter_graphics(); if (this->dstTexture) { if (this->dstTexture == this->texture) this->dstTexture = NULL; else { gs_texture_destroy(this->dstTexture); this->dstTexture = NULL; } } if (this->texture) { if (!this->dmabuf) gs_texture_unmap(this->texture); gs_texture_destroy(this->texture); this->texture = NULL; } if (this->cursorTex) { gs_texture_destroy(this->cursorTex); this->cursorTex = NULL; } obs_leave_graphics(); this->state = STATE_STOPPED; } static void lgDestroy(void * data) { LGPlugin * this = (LGPlugin *)data; deinit(this); os_sem_destroy(this->frameSem ); os_sem_destroy(this->cursorSem); obs_enter_graphics(); gs_effect_destroy(this->unpackEffect); obs_leave_graphics(); bfree(this); } static void lgGetDefaults(obs_data_t * defaults) { obs_data_set_default_string(defaults, "shmFile", "/dev/shm/looking-glass"); } static obs_properties_t * lgGetProperties(void * data) { obs_properties_t * props = obs_properties_create(); obs_properties_add_text(props, "shmFile", obs_module_text("SHM File"), OBS_TEXT_DEFAULT); obs_properties_add_bool(props, "hideMouse", obs_module_text("Hide mouse cursor")); #if LIBOBS_API_MAJOR_VER >= 27 obs_properties_add_bool(props, "dmabuf", obs_module_text("Use DMABUF import (requires kvmfr device)")); #else obs_property_t * dmabuf = obs_properties_add_bool(props, "dmabuf", obs_module_text("Use DMABUF import (requires OBS 27+ and kvmfr device)")); obs_property_set_enabled(dmabuf, false); #endif return props; } static void * frameThread(void * data) { LGPlugin * this = (LGPlugin *)data; if (lgmpClientSubscribe(this->lgmp, LGMP_Q_FRAME, &this->frameQueue) != LGMP_OK) { this->state = STATE_STOPPING; return NULL; } this->state = STATE_RUNNING; os_sem_post(this->frameSem); while(this->state == STATE_RUNNING) { LGMP_STATUS status; os_sem_wait(this->frameSem); if ((status = lgmpClientAdvanceToLast(this->frameQueue)) != LGMP_OK) { if (status != LGMP_ERR_QUEUE_EMPTY) { os_sem_post(this->frameSem); printf("lgmpClientAdvanceToLast: %s\n", lgmpStatusString(status)); break; } } os_sem_post(this->frameSem); usleep(1000); } lgmpClientUnsubscribe(&this->frameQueue); this->state = STATE_RESTARTING; return NULL; } inline static void allocCursorData(LGPlugin * this, const unsigned int size) { if (this->cursorSize >= size) return; bfree(this->cursorData); this->cursorSize = size; this->cursorData = bmalloc(size); } static void * pointerThread(void * data) { LGPlugin * this = (LGPlugin *)data; if (lgmpClientSubscribe(this->lgmp, LGMP_Q_POINTER, &this->pointerQueue) != LGMP_OK) { this->state = STATE_STOPPING; return NULL; } while(this->state == STATE_RUNNING) { LGMP_STATUS status; LGMPMessage msg; if ((status = lgmpClientProcess(this->pointerQueue, &msg)) != LGMP_OK) { if (status != LGMP_ERR_QUEUE_EMPTY) { printf("lgmpClientProcess: %s\n", lgmpStatusString(status)); break; } usleep(1000); continue; } const KVMFRCursor * const cursor = (const KVMFRCursor * const)msg.mem; this->cursorVisible = this->hideMouse ? 0 : msg.udata & CURSOR_FLAG_VISIBLE; if (msg.udata & CURSOR_FLAG_SHAPE) { os_sem_wait(this->cursorSem); const uint8_t * const data = (const uint8_t * const)(cursor + 1); unsigned int dataSize = 0; switch(cursor->type) { case CURSOR_TYPE_MASKED_COLOR: { dataSize = cursor->height * cursor->pitch; allocCursorData(this, dataSize); const uint32_t * s = (const uint32_t *)data; uint32_t * d = this->cursorData; for(int i = 0; i < dataSize / sizeof(uint32_t); ++i, ++s, ++d) *d = (*s & ~0xFF000000) | (*s & 0xFF000000 ? 0x0 : 0xFF000000); break; } case CURSOR_TYPE_COLOR: { dataSize = cursor->height * cursor->pitch; allocCursorData(this, dataSize); memcpy(this->cursorData, data, dataSize); break; } case CURSOR_TYPE_MONOCHROME: { dataSize = cursor->height * cursor->width * sizeof(uint32_t); allocCursorData(this, dataSize); const int hheight = cursor->height / 2; uint32_t * d = this->cursorData; for(int y = 0; y < hheight; ++y) for(int x = 0; x < cursor->width; ++x) { const uint8_t * srcAnd = data + (cursor->pitch * y) + (x / 8); const uint8_t * srcXor = srcAnd + cursor->pitch * hheight; const uint8_t mask = 0x80 >> (x % 8); const uint32_t andMask = (*srcAnd & mask) ? 0xFFFFFFFF : 0xFF000000; const uint32_t xorMask = (*srcXor & mask) ? 0x00FFFFFF : 0x00000000; d[y * cursor->width + x ] = andMask; d[y * cursor->width + x + cursor->width * hheight] = xorMask; } break; } default: printf("Invalid cursor type\n"); break; } this->cursor.type = cursor->type; this->cursor.width = cursor->width; this->cursor.height = cursor->height; atomic_fetch_add_explicit(&this->cursorVer, 1, memory_order_relaxed); os_sem_post(this->cursorSem); } if (msg.udata & CURSOR_FLAG_POSITION) { this->cursor.x = cursor->x; this->cursor.y = cursor->y; } lgmpClientMessageDone(this->pointerQueue); } lgmpClientUnsubscribe(&this->pointerQueue); bfree(this->cursorData); this->cursorData = NULL; this->cursorSize = 0; this->state = STATE_RESTARTING; return NULL; } static void lgUpdate(void * data, obs_data_t * settings) { LGPlugin * this = (LGPlugin *)data; deinit(this); this->shmFile = bstrdup(obs_data_get_string(settings, "shmFile")); if (!ivshmemOpenDev(&this->shmDev, this->shmFile)) return; this->hideMouse = obs_data_get_bool(settings, "hideMouse") ? 1 : 0; #if LIBOBS_API_MAJOR_VER >= 27 this->dmabuf = obs_data_get_bool(settings, "dmabuf") && ivshmemHasDMA(&this->shmDev); #endif this->state = STATE_OPEN; uint32_t udataSize; KVMFR * udata; if (lgmpClientInit(this->shmDev.mem, this->shmDev.size, &this->lgmp) != LGMP_OK) return; usleep(200000); if (lgmpClientSessionInit(this->lgmp, &udataSize, (uint8_t **)&udata, NULL) != LGMP_OK) return; if (udataSize < sizeof(KVMFR) || memcmp(udata->magic, KVMFR_MAGIC, sizeof(udata->magic)) != 0 || udata->version != KVMFR_VERSION) { printf("The host application is not compatible with this client\n"); printf("Expected KVMFR version %d\n", KVMFR_VERSION); printf("This is not a Looking Glass error, do not report this\n"); return; } this->state = STATE_STARTING; createThreads(this); } #if LIBOBS_API_MAJOR_VER >= 27 static int dmabufGetFd(LGPlugin * this, LGMPMessage * msg, KVMFRFrame * frame, size_t dataSize) { DMAFrameInfo * dma = NULL; /* find the existing dma buffer if it exists */ for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i) if (this->dmaInfo[i].frame == frame) { dma = this->dmaInfo + i; /* if it's too small close it */ if (dma->dataSize < dataSize) { close(dma->fd); dma->fd = -1; } break; } /* otherwise find a free buffer for use */ if (!dma) for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i) if (!this->dmaInfo[i].frame) { dma = this->dmaInfo + i; dma->frame = frame; dma->fd = -1; break; } assert(dma); /* open the buffer */ if (dma->fd == -1) { const uintptr_t pos = (uintptr_t) msg->mem - (uintptr_t) this->shmDev.mem; const uintptr_t offset = (uintptr_t) frame->offset + sizeof(FrameBuffer); dma->dataSize = dataSize; dma->fd = ivshmemGetDMABuf(&this->shmDev, pos + offset, dataSize); if (dma->fd < 0) { puts("Failed to get the DMA buffer for the frame"); return -1; } } return dma->fd; } #endif static void lgVideoTick(void * data, float seconds) { LGPlugin * this = (LGPlugin *)data; if (this->state == STATE_RESTARTING) { waitThreads(this); this->state = STATE_STARTING; createThreads(this); } if (this->state != STATE_RUNNING) return; LGMP_STATUS status; LGMPMessage msg; os_sem_wait(this->frameSem); if (this->state != STATE_RUNNING) { os_sem_post(this->frameSem); return; } this->cursorRect.x = this->cursor.x; this->cursorRect.y = this->cursor.y; /* update the cursor texture */ unsigned int cursorVer = atomic_load(&this->cursorVer); if (cursorVer != this->cursorCurVer) { os_sem_wait(this->cursorSem); obs_enter_graphics(); if (this->cursorTex) { gs_texture_destroy(this->cursorTex); this->cursorTex = NULL; } switch(this->cursor.type) { case CURSOR_TYPE_MASKED_COLOR: /* fallthrough */ case CURSOR_TYPE_COLOR: this->cursorMono = false; this->cursorTex = gs_texture_create( this->cursor.width, this->cursor.height, GS_BGRA, 1, (const uint8_t **)&this->cursorData, GS_DYNAMIC); break; case CURSOR_TYPE_MONOCHROME: this->cursorMono = true; this->cursorTex = gs_texture_create( this->cursor.width, this->cursor.height, GS_BGRA, 1, (const uint8_t **)&this->cursorData, GS_DYNAMIC); break; default: break; } obs_leave_graphics(); this->cursorCurVer = cursorVer; this->cursorRect.cx = this->cursor.width; this->cursorRect.cy = this->cursor.height; os_sem_post(this->cursorSem); } if ((status = lgmpClientAdvanceToLast(this->frameQueue)) != LGMP_OK) { if (status != LGMP_ERR_QUEUE_EMPTY) { os_sem_post(this->frameSem); printf("lgmpClientAdvanceToLast: %s\n", lgmpStatusString(status)); return; } } if ((status = lgmpClientProcess(this->frameQueue, &msg)) != LGMP_OK) { if (status == LGMP_ERR_QUEUE_EMPTY) { os_sem_post(this->frameSem); return; } printf("lgmpClientProcess: %s\n", lgmpStatusString(status)); this->state = STATE_STOPPING; os_sem_post(this->frameSem); return; } KVMFRFrame * frame = (KVMFRFrame *)msg.mem; if (!this->texture || this->formatVer != frame->formatVer) { this->formatVer = frame->formatVer; this->screenWidth = frame->screenWidth; this->screenHeight = frame->screenHeight; this->dataWidth = frame->dataWidth; this->dataHeight = frame->dataHeight; this->frameWidth = frame->frameWidth; this->frameHeight = frame->frameHeight; this->type = frame->type; this->screenScale.x = this->screenWidth / this->frameWidth ; this->screenScale.y = this->screenHeight / this->frameHeight; obs_enter_graphics(); if (this->texture) { if (this->dstTexture && this->dstTexture != this->texture) { gs_texture_destroy(this->dstTexture); this->dstTexture = NULL; } if (!this->dmabuf) gs_texture_unmap(this->texture); gs_texture_destroy(this->texture); this->texture = NULL; } enum gs_color_format format; uint32_t drm_format; unsigned width = frame->dataWidth; bool unpack = false; this->bpp = 4; switch(this->type) { case FRAME_TYPE_BGRA: format = GS_BGRA; drm_format = DRM_FORMAT_ARGB8888; break; case FRAME_TYPE_RGBA: format = GS_RGBA; drm_format = DRM_FORMAT_ARGB8888; break; case FRAME_TYPE_RGBA10: format = GS_R10G10B10A2; drm_format = DRM_FORMAT_BGRA1010102; break; case FRAME_TYPE_RGB_24: this->bpp = 3; width = frame->pitch / 4; /* fallthrough */ case FRAME_TYPE_BGR_32: format = GS_BGRA; drm_format = DRM_FORMAT_ARGB8888; unpack = true; break; case FRAME_TYPE_RGBA16F: this->bpp = 8; format = GS_RGBA16F; drm_format = DRM_FORMAT_ABGR16161616F; break; default: printf("invalid type %d\n", this->type); lgmpClientMessageDone(this->frameQueue); os_sem_post(this->frameSem); obs_leave_graphics(); return; } #if LIBOBS_API_MAJOR_VER >= 27 if (this->dmabuf) { int fd = dmabufGetFd(this, &msg, frame, frame->frameHeight * frame->pitch); if (fd >= 0) { this->texture = gs_texture_create_from_dmabuf( width, this->dataHeight, drm_format, format, 1, &fd, &(uint32_t) { frame->pitch }, &(uint32_t) { 0 }, &(uint64_t) { 0 }); if (!this->texture) { puts("Failed to create dmabuf texture"); this->dmabuf = false; } } } #else (void)drm_format; #endif if (!this->dmabuf) { this->texture = gs_texture_create( width, this->dataHeight, format, 1, NULL, GS_DYNAMIC); if (!this->texture) { printf("create texture failed\n"); lgmpClientMessageDone(this->frameQueue); os_sem_post(this->frameSem); obs_leave_graphics(); return; } gs_texture_map(this->texture, &this->texData, &this->linesize); } if (unpack) { // create the render target for format unpacking this->dstTexture = gs_texture_create( this->frameWidth, this->frameHeight, GS_BGRA, 1, NULL, GS_RENDER_TARGET); } else this->dstTexture = this->texture; obs_leave_graphics(); } // if using dmabuf there is nothing more here to do if (!this->texture || this->dmabuf) { lgmpClientMessageDone(this->frameQueue); os_sem_post(this->frameSem); return; } FrameBuffer * fb = (FrameBuffer *)(((uint8_t*)frame) + frame->offset); framebuffer_read( fb, this->texData , // dst this->linesize , // dstpitch this->dataHeight, // height this->dataWidth , // width this->bpp , // bpp frame->pitch ); lgmpClientMessageDone(this->frameQueue); os_sem_post(this->frameSem); obs_enter_graphics(); gs_texture_unmap(this->texture); gs_texture_map(this->texture, &this->texData, &this->linesize); obs_leave_graphics(); } static void lgVideoRender(void * data, gs_effect_t * effect) { LGPlugin * this = (LGPlugin *)data; if (!this->texture) return; if (this->type == FRAME_TYPE_RGB_24 || this->type == FRAME_TYPE_BGR_32) { effect = this->unpackEffect; gs_effect_set_texture(this->image, this->texture); struct vec2 outputSize; vec2_set(&outputSize, this->frameWidth, this->frameHeight); gs_effect_set_vec2(this->outputSize, &outputSize); gs_effect_set_int(this->swap, this->type == FRAME_TYPE_RGB_24 ? 1 : 0); } else { effect = obs_get_base_effect(OBS_EFFECT_OPAQUE); gs_eparam_t * image = gs_effect_get_param_by_name(effect, "image"); gs_effect_set_texture(image, this->texture); } while (gs_effect_loop(effect, "Draw")) gs_draw_sprite(this->dstTexture, 0, 0, 0); if (this->cursorVisible && this->cursorTex) { struct matrix4 m4; gs_matrix_get(&m4); struct gs_rect r = { .x = m4.t.x, .y = m4.t.y, .cx = (double)this->frameWidth * m4.x.x, .cy = (double)this->frameHeight * m4.y.y }; gs_set_scissor_rect(&r); effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); gs_eparam_t * image = gs_effect_get_param_by_name(effect, "image"); gs_effect_set_texture(image, this->cursorTex); gs_matrix_push(); gs_matrix_translate3f( this->cursorRect.x / this->screenScale.x, this->cursorRect.y / this->screenScale.y, 0.0f); if (!this->cursorMono) { gs_blend_function(GS_BLEND_SRCALPHA, GS_BLEND_INVSRCALPHA); while (gs_effect_loop(effect, "Draw")) gs_draw_sprite(this->cursorTex, 0, 0, 0); gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); } else { while (gs_effect_loop(effect, "Draw")) { glEnable(GL_COLOR_LOGIC_OP); glLogicOp(GL_AND); gs_draw_sprite_subregion( this->cursorTex , 0, 0 , 0, this->cursorRect.cx, this->cursorRect.cy / 2); glLogicOp(GL_XOR); gs_draw_sprite_subregion( this->cursorTex , 0, 0 , this->cursorRect.cy / 2, this->cursorRect.cx, this->cursorRect.cy / 2); glDisable(GL_COLOR_LOGIC_OP); } } gs_matrix_pop(); gs_set_scissor_rect(NULL); } } static uint32_t lgGetWidth(void * data) { LGPlugin * this = (LGPlugin *)data; return this->frameWidth; } static uint32_t lgGetHeight(void * data) { LGPlugin * this = (LGPlugin *)data; return this->frameHeight; } struct obs_source_info lg_source = { .id = "looking-glass-obs", .type = OBS_SOURCE_TYPE_INPUT, .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_DO_NOT_DUPLICATE, .get_name = lgGetName, .create = lgCreate, .destroy = lgDestroy, .update = lgUpdate, .get_defaults = lgGetDefaults, .get_properties = lgGetProperties, .video_tick = lgVideoTick, .video_render = lgVideoRender, .get_width = lgGetWidth, .get_height = lgGetHeight, .icon_type = OBS_ICON_TYPE_DESKTOP_CAPTURE };