mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-07-21 23:02:05 +00:00
Keep masked-color XOR operands on the untransformed logical path rather than applying the color cursor shader. Preserve the white level delivered with cursor updates instead of replacing it with frame SDR white during format changes. This keeps inverse-destination blending intact and ensures HDR cursor brightness follows QueryHardwareCursor3.
1507 lines
41 KiB
C
1507 lines
41 KiB
C
/**
|
|
* 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
|
|
*/
|
|
|
|
#define _GNU_SOURCE //needed for pthread_setname_np
|
|
|
|
#include <obs/obs-config.h>
|
|
#include <obs/obs-module.h>
|
|
#include <obs/util/threading.h>
|
|
#include <obs/graphics/graphics.h>
|
|
#include <obs/graphics/matrix4.h>
|
|
|
|
#include <common/array.h>
|
|
#include <common/ivshmem.h>
|
|
#include <common/KVMFR.h>
|
|
#include <common/framebuffer.h>
|
|
#include <lgmp/client.h>
|
|
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <stdatomic.h>
|
|
|
|
#include "rgb24.effect.h"
|
|
#include "hdrpq.effect.h"
|
|
#include "cursor.effect.h"
|
|
|
|
/* scRGB reference white in cd/m² (OBS GS_CS_709_SCRGB is defined as
|
|
* 1.0 = 80 cd/m²). PQ encodes an absolute 0..10000 cd/m² range, so linear
|
|
* PQ output is scaled by 10000 / 80 to reach the scRGB convention. */
|
|
#define LG_SCRGB_REFERENCE_WHITE 80.0f
|
|
|
|
/**
|
|
* 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_XRGB8888 fourcc_code('X', 'R', '2', '4')
|
|
#define DRM_FORMAT_XBGR8888 fourcc_code('X', 'B', '2', '4')
|
|
#define DRM_FORMAT_XBGR2101010 fourcc_code('X', 'B', '3', '0')
|
|
#define DRM_FORMAT_XBGR16161616F fourcc_code('X', 'B', '4', 'H')
|
|
|
|
typedef enum
|
|
{
|
|
STATE_STOPPED,
|
|
STATE_OPEN,
|
|
STATE_STARTING,
|
|
STATE_RUNNING,
|
|
STATE_STOPPING,
|
|
STATE_RESTARTING
|
|
}
|
|
LGState;
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
typedef struct
|
|
{
|
|
const KVMFRFrame * frame;
|
|
size_t dataSize;
|
|
int fd;
|
|
gs_texture_t * texture;
|
|
}
|
|
DMAFrameInfo;
|
|
#endif
|
|
|
|
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;
|
|
enum gs_color_format format;
|
|
bool unpack;
|
|
uint32_t drmFormat;
|
|
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;
|
|
bool hdr;
|
|
bool hdrPQ;
|
|
_Atomic(float) sdrWhiteLevel; // source cursor white level in nits
|
|
struct vec3 colorMatrix[3]; // source primaries -> BT.709 (linear)
|
|
float hdrScale; // scRGB reference white scaling
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
bool dmabuf;
|
|
bool dmabufTested;
|
|
DMAFrameInfo dmaInfo[LGMP_Q_FRAME_LEN];
|
|
gs_texture_t * dmaTexture;
|
|
#endif
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
enum gs_color_space colorSpace;
|
|
#endif
|
|
|
|
pthread_t frameThread, pointerThread;
|
|
os_sem_t * frameSem;
|
|
|
|
bool cursorMono;
|
|
gs_texture_t * cursorTex;
|
|
gs_texture_t * cursorXorTex;
|
|
struct gs_rect cursorRect;
|
|
|
|
bool cursorVisible;
|
|
KVMFRCursor cursor;
|
|
os_sem_t * cursorSem;
|
|
atomic_uint cursorVer;
|
|
unsigned int cursorCurVer;
|
|
KVMFRColorTransform pendingCursorTransform;
|
|
KVMFRColorTransform activeCursorTransform;
|
|
atomic_uint cursorTransformVer;
|
|
unsigned int cursorTransformCurVer;
|
|
gs_texture_t * cursorLUT;
|
|
uint32_t cursorSize;
|
|
uint32_t * cursorData;
|
|
|
|
gs_effect_t * unpackEffect;
|
|
gs_eparam_t * image;
|
|
gs_eparam_t * outputSize;
|
|
gs_eparam_t * swap;
|
|
|
|
gs_effect_t * hdrEffect;
|
|
gs_eparam_t * hdrImage;
|
|
gs_eparam_t * hdrOutputSize;
|
|
gs_eparam_t * hdrColorMatrix[3];
|
|
gs_eparam_t * hdrScaleParam;
|
|
|
|
gs_effect_t * cursorEffect;
|
|
gs_eparam_t * cursorImage;
|
|
gs_eparam_t * cursorMultiplier;
|
|
gs_eparam_t * cursorSDRWhiteLevel;
|
|
gs_eparam_t * cursorOutputTransfer;
|
|
gs_eparam_t * cursorMatrixEnabled;
|
|
gs_eparam_t * cursorLUTEnabled;
|
|
gs_eparam_t * cursorColorMatrix[3];
|
|
gs_eparam_t * cursorColorScalar;
|
|
gs_eparam_t * cursorColorLUT;
|
|
}
|
|
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;
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
|
|
this->dmaInfo[i].fd = -1;
|
|
#endif
|
|
|
|
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" );
|
|
|
|
this->hdrEffect = gs_effect_create(b_effect_hdrpq_effect, "hdrpq", &error);
|
|
if (!this->hdrEffect)
|
|
{
|
|
blog(LOG_ERROR, "%s", error);
|
|
bfree(error);
|
|
gs_effect_destroy(this->unpackEffect);
|
|
bfree(this);
|
|
obs_leave_graphics();
|
|
return NULL;
|
|
}
|
|
|
|
this->hdrImage = gs_effect_get_param_by_name(
|
|
this->hdrEffect, "image" );
|
|
this->hdrOutputSize = gs_effect_get_param_by_name(
|
|
this->hdrEffect, "outputSize" );
|
|
this->hdrColorMatrix[0] = gs_effect_get_param_by_name(
|
|
this->hdrEffect, "colorMatrix0");
|
|
this->hdrColorMatrix[1] = gs_effect_get_param_by_name(
|
|
this->hdrEffect, "colorMatrix1");
|
|
this->hdrColorMatrix[2] = gs_effect_get_param_by_name(
|
|
this->hdrEffect, "colorMatrix2");
|
|
this->hdrScaleParam = gs_effect_get_param_by_name(
|
|
this->hdrEffect, "scale" );
|
|
|
|
this->cursorEffect = gs_effect_create(
|
|
b_effect_cursor_effect, "cursor", &error);
|
|
if (!this->cursorEffect)
|
|
{
|
|
blog(LOG_ERROR, "%s", error);
|
|
bfree(error);
|
|
gs_effect_destroy(this->hdrEffect);
|
|
gs_effect_destroy(this->unpackEffect);
|
|
bfree(this);
|
|
obs_leave_graphics();
|
|
return NULL;
|
|
}
|
|
|
|
this->cursorImage = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "image" );
|
|
this->cursorMultiplier = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "multiplier");
|
|
this->cursorSDRWhiteLevel = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "sdrWhiteLevel");
|
|
this->cursorOutputTransfer = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "outputTransfer");
|
|
this->cursorMatrixEnabled = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "matrixEnabled");
|
|
this->cursorLUTEnabled = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "lutEnabled");
|
|
this->cursorColorMatrix[0] = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "colorMatrix0");
|
|
this->cursorColorMatrix[1] = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "colorMatrix1");
|
|
this->cursorColorMatrix[2] = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "colorMatrix2");
|
|
this->cursorColorScalar = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "colorScalar");
|
|
this->cursorColorLUT = gs_effect_get_param_by_name(
|
|
this->cursorEffect, "colorLUT");
|
|
obs_leave_graphics();
|
|
|
|
os_sem_init (&this->frameSem , 0);
|
|
os_sem_init (&this->cursorSem, 1);
|
|
atomic_store(&this->cursorVer, 0);
|
|
atomic_store(&this->cursorTransformVer, 0);
|
|
atomic_store(&this->sdrWhiteLevel, KVMFR_SDR_WHITE_LEVEL_DEFAULT);
|
|
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);
|
|
}
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
/* The caller must hold the OBS graphics lock. */
|
|
static void dmabufInvalidateTextures(LGPlugin * this)
|
|
{
|
|
for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
|
|
{
|
|
if (!this->dmaInfo[i].texture)
|
|
continue;
|
|
|
|
gs_texture_destroy(this->dmaInfo[i].texture);
|
|
this->dmaInfo[i].texture = NULL;
|
|
}
|
|
|
|
this->dmaTexture = NULL;
|
|
this->dmabufTested = false;
|
|
}
|
|
|
|
/* The caller must hold the OBS graphics lock. */
|
|
static void dmabufReset(LGPlugin * this)
|
|
{
|
|
dmabufInvalidateTextures(this);
|
|
|
|
/* Ensure the driver releases every DMA-BUF attachment before the exported
|
|
* FDs and their backing IVSHMEM device are closed. */
|
|
gs_flush();
|
|
|
|
for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
|
|
{
|
|
DMAFrameInfo * fi = &this->dmaInfo[i];
|
|
if (fi->fd >= 0)
|
|
close(fi->fd);
|
|
|
|
memset(fi, 0, sizeof(*fi));
|
|
fi->fd = -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void deinit(LGPlugin * this)
|
|
{
|
|
bool closeShm = false;
|
|
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:
|
|
closeShm = true;
|
|
break;
|
|
|
|
case STATE_STOPPED:
|
|
break;
|
|
}
|
|
|
|
if (this->shmFile)
|
|
{
|
|
bfree(this->shmFile);
|
|
this->shmFile = NULL;
|
|
}
|
|
|
|
obs_enter_graphics();
|
|
if (this->unpack && this->dstTexture)
|
|
{
|
|
gs_texture_destroy(this->dstTexture);
|
|
this->dstTexture = NULL;
|
|
this->unpack = false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (this->cursorXorTex)
|
|
{
|
|
gs_texture_destroy(this->cursorXorTex);
|
|
this->cursorXorTex = NULL;
|
|
}
|
|
|
|
if (this->cursorLUT)
|
|
{
|
|
gs_effect_set_texture(this->cursorColorLUT, NULL);
|
|
gs_texture_destroy(this->cursorLUT);
|
|
this->cursorLUT = NULL;
|
|
}
|
|
memset(&this->pendingCursorTransform, 0,
|
|
sizeof(this->pendingCursorTransform));
|
|
memset(&this->activeCursorTransform, 0,
|
|
sizeof(this->activeCursorTransform));
|
|
this->pendingCursorTransform.scalar = 1.0f;
|
|
this->activeCursorTransform.scalar = 1.0f;
|
|
atomic_store(&this->cursorTransformVer, 0);
|
|
this->cursorTransformCurVer = 0;
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
dmabufReset(this);
|
|
#endif
|
|
|
|
obs_leave_graphics();
|
|
|
|
if (closeShm)
|
|
{
|
|
lgmpClientFree(&this->lgmp);
|
|
ivshmemClose(&this->shmDev);
|
|
}
|
|
|
|
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);
|
|
gs_effect_destroy(this->hdrEffect);
|
|
gs_effect_destroy(this->cursorEffect);
|
|
obs_leave_graphics();
|
|
|
|
bfree(this);
|
|
}
|
|
|
|
static void lgGetDefaults(obs_data_t * defaults)
|
|
{
|
|
obs_data_set_default_string(defaults, "shmFile", "/dev/kvmfr0");
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
obs_data_set_default_bool(defaults, "dmabuf", true);
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
if (msg.udata & CURSOR_FLAG_VISIBLE_VALID)
|
|
{
|
|
this->cursorVisible = this->hideMouse ?
|
|
0 : msg.udata & CURSOR_FLAG_VISIBLE;
|
|
if (cursor->sdrWhiteLevel)
|
|
atomic_store(&this->sdrWhiteLevel, cursor->sdrWhiteLevel);
|
|
}
|
|
|
|
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:
|
|
{
|
|
const size_t pixels = (size_t)cursor->width * cursor->height;
|
|
dataSize = pixels * sizeof(uint32_t) * 2;
|
|
allocCursorData(this, dataSize);
|
|
|
|
uint32_t * normal = this->cursorData;
|
|
uint32_t * xorData = normal + pixels;
|
|
for(int y = 0; y < cursor->height; ++y)
|
|
for(int x = 0; x < cursor->width; ++x)
|
|
{
|
|
const uint32_t src = *(const uint32_t *)
|
|
(data + cursor->pitch * y + sizeof(uint32_t) * x);
|
|
|
|
const uint32_t rgb = src & 0x00FFFFFF;
|
|
const bool masked = src & 0xFF000000;
|
|
const size_t pos = y * cursor->width + x;
|
|
|
|
if (masked)
|
|
{
|
|
normal [pos] = 0x00000000;
|
|
xorData[pos] = rgb ? rgb | 0xFF000000 : 0x00000000;
|
|
}
|
|
else
|
|
{
|
|
normal [pos] = rgb | 0xFF000000;
|
|
xorData[pos] = 0x00000000;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CURSOR_TYPE_COLOR:
|
|
{
|
|
dataSize = cursor->height * cursor->pitch;
|
|
allocCursorData(this, dataSize);
|
|
memcpy(this->cursorData, data, dataSize);
|
|
break;
|
|
}
|
|
|
|
case CURSOR_TYPE_MONOCHROME:
|
|
{
|
|
const int height = cursor->height / 2;
|
|
dataSize = height * cursor->width * sizeof(uint32_t);
|
|
allocCursorData(this, dataSize);
|
|
|
|
uint32_t * d = this->cursorData;
|
|
for(int y = 0; y < height; ++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 * height;
|
|
const uint8_t mask = 0x80 >> (x % 8);
|
|
const bool andMask = *srcAnd & mask;
|
|
const bool xorMask = *srcXor & mask;
|
|
d[y * cursor->width + x] = andMask ?
|
|
(xorMask ? 0xFFFFFFFF : 0x00000000) :
|
|
(xorMask ? 0x00FFFFFF : 0xFF000000);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
printf("Invalid cursor type\n");
|
|
break;
|
|
}
|
|
|
|
this->cursor.type = cursor->type;
|
|
this->cursor.width = cursor->width;
|
|
this->cursor.height = cursor->type == CURSOR_TYPE_MONOCHROME ?
|
|
cursor->height / 2 : cursor->height;
|
|
|
|
atomic_fetch_add_explicit(&this->cursorVer, 1, memory_order_relaxed);
|
|
os_sem_post(this->cursorSem);
|
|
}
|
|
|
|
if (msg.udata & CURSOR_FLAG_COLOR_TRANSFORM)
|
|
{
|
|
const size_t shapeSize = msg.udata & CURSOR_FLAG_SHAPE ?
|
|
(size_t)cursor->height * cursor->pitch : 0;
|
|
const KVMFRColorTransform * transform =
|
|
(const KVMFRColorTransform *)((const uint8_t *)(cursor + 1) +
|
|
shapeSize);
|
|
|
|
os_sem_wait(this->cursorSem);
|
|
this->pendingCursorTransform = *transform;
|
|
atomic_fetch_add_explicit(
|
|
&this->cursorTransformVer, 1, memory_order_release);
|
|
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;
|
|
|
|
LGMP_STATUS status;
|
|
if ((status = lgmpClientInit(this->shmDev.mem, this->shmDev.size,
|
|
&this->lgmp)) != LGMP_OK)
|
|
{
|
|
printf("lgmpClientInit: %s\n", lgmpStatusString(status));
|
|
return;
|
|
}
|
|
|
|
usleep(200000);
|
|
|
|
uint32_t remoteVersion;
|
|
if ((status = lgmpClientSessionInit(this->lgmp, &udataSize,
|
|
(uint8_t **)&udata, NULL, &remoteVersion)) != LGMP_OK)
|
|
{
|
|
printf("lgmpClientSessionInit: %s", lgmpStatusString(status));
|
|
if (status == LGMP_ERR_INVALID_VERSION)
|
|
{
|
|
printf("The host application is not compatible with this client\n");
|
|
printf("Expected LGMP version %u but got %u\n",
|
|
LGMP_PROTOCOL_VERSION, remoteVersion);
|
|
printf("This is not a Looking Glass error, do not report this");
|
|
}
|
|
printf("\n");
|
|
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 DMAFrameInfo * dmabufOpenDMAFrameInfo(LGPlugin * this, LGMPMessage * msg,
|
|
const KVMFRFrame * frame, size_t dataSize)
|
|
{
|
|
DMAFrameInfo * fi = 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)
|
|
fi = &this->dmaInfo[i];
|
|
|
|
/* if it's too small close it */
|
|
if (fi && fi->dataSize < dataSize)
|
|
{
|
|
if (fi->texture)
|
|
{
|
|
gs_texture_destroy(fi->texture);
|
|
fi->texture = NULL;
|
|
}
|
|
if (fi->fd >= 0)
|
|
close(fi->fd);
|
|
fi->fd = -1;
|
|
}
|
|
|
|
/* otherwise find a free buffer for use */
|
|
if (!fi)
|
|
for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
|
|
if (!this->dmaInfo[i].frame)
|
|
{
|
|
fi = &this->dmaInfo[i];
|
|
fi->frame = frame;
|
|
fi->fd = -1;
|
|
break;
|
|
}
|
|
|
|
if (!fi)
|
|
{
|
|
puts("Failed to obtain a free DMA frame slot");
|
|
return NULL;
|
|
}
|
|
|
|
/* open the buffer */
|
|
if (fi->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);
|
|
|
|
fi->dataSize = dataSize;
|
|
fi->fd = ivshmemGetDMABuf(&this->shmDev, pos + offset, dataSize);
|
|
fi->texture = NULL;
|
|
if (fi->fd < 0)
|
|
{
|
|
puts("Failed to get the DMA buffer for the frame");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return fi;
|
|
}
|
|
|
|
static bool dmabufCreateTexture(LGPlugin * this, DMAFrameInfo * fi,
|
|
const KVMFRFrame * frame)
|
|
{
|
|
if (!fi)
|
|
return false;
|
|
|
|
fi->texture = gs_texture_create_from_dmabuf(
|
|
this->dataWidth,
|
|
this->dataHeight,
|
|
this->drmFormat,
|
|
this->format,
|
|
1,
|
|
&fi->fd,
|
|
&(uint32_t) { frame->pitch },
|
|
&(uint32_t) { 0 },
|
|
NULL);
|
|
|
|
return fi->texture != NULL;
|
|
}
|
|
#endif
|
|
|
|
static bool mat3Inverse(const double m[9], double out[9])
|
|
{
|
|
const double det =
|
|
m[0] * (m[4] * m[8] - m[5] * m[7]) -
|
|
m[1] * (m[3] * m[8] - m[5] * m[6]) +
|
|
m[2] * (m[3] * m[7] - m[4] * m[6]);
|
|
|
|
if (det == 0.0)
|
|
return false;
|
|
|
|
const double inv = 1.0 / det;
|
|
out[0] = (m[4] * m[8] - m[5] * m[7]) * inv;
|
|
out[1] = (m[2] * m[7] - m[1] * m[8]) * inv;
|
|
out[2] = (m[1] * m[5] - m[2] * m[4]) * inv;
|
|
out[3] = (m[5] * m[6] - m[3] * m[8]) * inv;
|
|
out[4] = (m[0] * m[8] - m[2] * m[6]) * inv;
|
|
out[5] = (m[2] * m[3] - m[0] * m[5]) * inv;
|
|
out[6] = (m[3] * m[7] - m[4] * m[6]) * inv;
|
|
out[7] = (m[1] * m[6] - m[0] * m[7]) * inv;
|
|
out[8] = (m[0] * m[4] - m[1] * m[3]) * inv;
|
|
return true;
|
|
}
|
|
|
|
static void mat3Mul(const double a[9], const double b[9], double out[9])
|
|
{
|
|
for (int r = 0; r < 3; ++r)
|
|
for (int c = 0; c < 3; ++c)
|
|
out[r * 3 + c] =
|
|
a[r * 3 + 0] * b[0 * 3 + c] +
|
|
a[r * 3 + 1] * b[1 * 3 + c] +
|
|
a[r * 3 + 2] * b[2 * 3 + c];
|
|
}
|
|
|
|
/* build the linear RGB -> CIE XYZ matrix for a set of primaries and white
|
|
* point (Bruce Lindbloom's method) */
|
|
static bool primariesToXYZ(double xr, double yr, double xg, double yg,
|
|
double xb, double yb, double xw, double yw, double out[9])
|
|
{
|
|
if (yr == 0.0 || yg == 0.0 || yb == 0.0 || yw == 0.0)
|
|
return false;
|
|
|
|
const double Xr = xr / yr, Yr = 1.0, Zr = (1.0 - xr - yr) / yr;
|
|
const double Xg = xg / yg, Yg = 1.0, Zg = (1.0 - xg - yg) / yg;
|
|
const double Xb = xb / yb, Yb = 1.0, Zb = (1.0 - xb - yb) / yb;
|
|
|
|
const double P[9] =
|
|
{
|
|
Xr, Xg, Xb,
|
|
Yr, Yg, Yb,
|
|
Zr, Zg, Zb
|
|
};
|
|
|
|
double Pinv[9];
|
|
if (!mat3Inverse(P, Pinv))
|
|
return false;
|
|
|
|
const double Xw = xw / yw, Yw = 1.0, Zw = (1.0 - xw - yw) / yw;
|
|
const double Sr = Pinv[0] * Xw + Pinv[1] * Yw + Pinv[2] * Zw;
|
|
const double Sg = Pinv[3] * Xw + Pinv[4] * Yw + Pinv[5] * Zw;
|
|
const double Sb = Pinv[6] * Xw + Pinv[7] * Yw + Pinv[8] * Zw;
|
|
|
|
out[0] = Sr * Xr; out[1] = Sg * Xg; out[2] = Sb * Xb;
|
|
out[3] = Sr * Yr; out[4] = Sg * Yg; out[5] = Sb * Yb;
|
|
out[6] = Sr * Zr; out[7] = Sg * Zg; out[8] = Sb * Zb;
|
|
return true;
|
|
}
|
|
|
|
/* Compute the linear-light matrix that converts BT.2020 to BT.709. HDR10
|
|
* mastering-display primaries describe the display used to master the
|
|
* content; they do not change the BT.2020 encoding gamut of the pixels. */
|
|
static void lgComputeColorMatrix(LGPlugin * this)
|
|
{
|
|
static const double bt709[9] =
|
|
{
|
|
1.0, 0.0, 0.0,
|
|
0.0, 1.0, 0.0,
|
|
0.0, 0.0, 1.0
|
|
};
|
|
const double * result = bt709;
|
|
double src[9], dst709[9], dst709inv[9], conv[9];
|
|
|
|
/* BT.2020 / D65 source */
|
|
const double xr = 0.7080, yr = 0.2920;
|
|
const double xg = 0.1700, yg = 0.7970;
|
|
const double xb = 0.1310, yb = 0.0460;
|
|
const double xw = 0.3127, yw = 0.3290;
|
|
|
|
/* BT.709 / D65 target */
|
|
if (primariesToXYZ(xr, yr, xg, yg, xb, yb, xw, yw, src) &&
|
|
primariesToXYZ(0.640, 0.330, 0.300, 0.600, 0.150, 0.060,
|
|
0.3127, 0.3290, dst709) &&
|
|
mat3Inverse(dst709, dst709inv))
|
|
{
|
|
mat3Mul(dst709inv, src, conv);
|
|
result = conv;
|
|
}
|
|
else
|
|
printf("Failed to compute BT.2020 to BT.709 color matrix\n");
|
|
|
|
for (int r = 0; r < 3; ++r)
|
|
vec3_set(&this->colorMatrix[r],
|
|
(float)result[r * 3 + 0],
|
|
(float)result[r * 3 + 1],
|
|
(float)result[r * 3 + 2]);
|
|
|
|
this->hdrScale = 10000.0f / LG_SCRGB_REFERENCE_WHITE;
|
|
}
|
|
|
|
static void lgFormatInit(LGPlugin * this, const KVMFRFrame * frame,
|
|
LGMPMessage * msg)
|
|
{
|
|
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 LIBOBS_API_MAJOR_VER >= 27
|
|
dmabufInvalidateTextures(this);
|
|
#endif
|
|
if (this->unpack && this->dstTexture)
|
|
{
|
|
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;
|
|
}
|
|
|
|
this->dataWidth = frame->dataWidth;
|
|
this->unpack = false;
|
|
this->hdr = frame->flags & FRAME_FLAG_HDR;
|
|
this->hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ;
|
|
/* Keep the white level supplied with the cursor message. The frame value
|
|
* is only a general SDR-content level and can differ from QueryHWCursor3. */
|
|
|
|
if (this->hdr && this->hdrPQ)
|
|
lgComputeColorMatrix(this);
|
|
|
|
this->bpp = 4;
|
|
switch(this->type)
|
|
{
|
|
case FRAME_TYPE_BGRA:
|
|
this->format = GS_BGRA_UNORM;
|
|
this->drmFormat = DRM_FORMAT_XRGB8888;
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
this->colorSpace = GS_CS_SRGB;
|
|
#endif
|
|
break;
|
|
|
|
case FRAME_TYPE_RGBA:
|
|
this->format = GS_RGBA_UNORM;
|
|
this->drmFormat = DRM_FORMAT_XBGR8888;
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
this->colorSpace = GS_CS_SRGB;
|
|
#endif
|
|
break;
|
|
|
|
case FRAME_TYPE_RGBA10:
|
|
this->format = GS_R10G10B10A2;
|
|
this->drmFormat = DRM_FORMAT_XBGR2101010;
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
this->colorSpace = this->hdr ? GS_CS_709_SCRGB : GS_CS_SRGB;
|
|
#endif
|
|
break;
|
|
|
|
case FRAME_TYPE_RGB_24:
|
|
this->bpp = 3;
|
|
this->dataWidth = frame->pitch / 4;
|
|
/* fallthrough */
|
|
|
|
case FRAME_TYPE_BGR_32:
|
|
this->format = GS_BGRA_UNORM;
|
|
this->drmFormat = DRM_FORMAT_ARGB8888;
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
this->colorSpace = GS_CS_SRGB;
|
|
#endif
|
|
this->unpack = true;
|
|
break;
|
|
|
|
case FRAME_TYPE_RGBA16F:
|
|
this->bpp = 8;
|
|
this->format = GS_RGBA16F;
|
|
this->drmFormat = DRM_FORMAT_XBGR16161616F;
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
this->colorSpace = GS_CS_709_SCRGB;
|
|
#endif
|
|
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)
|
|
{
|
|
DMAFrameInfo * fi = dmabufOpenDMAFrameInfo(this, msg, frame,
|
|
(size_t)frame->dataHeight * frame->pitch);
|
|
if (fi && !fi->texture)
|
|
{
|
|
// create the first texture now so we can test if dmabuf is usable
|
|
if (!dmabufCreateTexture(this, fi, frame))
|
|
{
|
|
puts("Failed to create dmabuf texture");
|
|
this->dmabuf = false;
|
|
}
|
|
|
|
this->dmabufTested = true;
|
|
}
|
|
}
|
|
#else
|
|
(void)drmFormat;
|
|
#endif
|
|
|
|
if (!this->dmabuf)
|
|
{
|
|
this->texture = gs_texture_create(
|
|
this->dataWidth,
|
|
this->dataHeight,
|
|
this->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 (this->unpack)
|
|
{
|
|
// create the render target for format unpacking
|
|
this->dstTexture = gs_texture_create(
|
|
this->frameWidth,
|
|
this->frameHeight,
|
|
GS_BGRA,
|
|
1,
|
|
NULL,
|
|
GS_RENDER_TARGET);
|
|
}
|
|
|
|
obs_leave_graphics();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (this->cursorXorTex)
|
|
{
|
|
gs_texture_destroy(this->cursorXorTex);
|
|
this->cursorXorTex = NULL;
|
|
}
|
|
|
|
switch(this->cursor.type)
|
|
{
|
|
case CURSOR_TYPE_MASKED_COLOR:
|
|
{
|
|
const uint8_t * normal = (const uint8_t *)this->cursorData;
|
|
const uint8_t * xorData = normal +
|
|
this->cursor.width * this->cursor.height * sizeof(uint32_t);
|
|
|
|
this->cursorMono = false;
|
|
this->cursorTex = gs_texture_create(
|
|
this->cursor.width,
|
|
this->cursor.height,
|
|
GS_BGRA,
|
|
1,
|
|
&normal,
|
|
GS_DYNAMIC);
|
|
this->cursorXorTex = gs_texture_create(
|
|
this->cursor.width,
|
|
this->cursor.height,
|
|
GS_BGRA,
|
|
1,
|
|
&xorData,
|
|
GS_DYNAMIC);
|
|
break;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
const unsigned int cursorTransformVer = atomic_load_explicit(
|
|
&this->cursorTransformVer, memory_order_acquire);
|
|
if (cursorTransformVer != this->cursorTransformCurVer)
|
|
{
|
|
os_sem_wait(this->cursorSem);
|
|
this->activeCursorTransform = this->pendingCursorTransform;
|
|
|
|
obs_enter_graphics();
|
|
if (this->cursorLUT)
|
|
{
|
|
gs_effect_set_texture(this->cursorColorLUT, NULL);
|
|
gs_texture_destroy(this->cursorLUT);
|
|
this->cursorLUT = NULL;
|
|
}
|
|
|
|
if (this->activeCursorTransform.flags & KVMFR_COLOR_TRANSFORM_LUT)
|
|
{
|
|
const uint8_t * lut =
|
|
(const uint8_t *)this->activeCursorTransform.lut;
|
|
this->cursorLUT = gs_texture_create(
|
|
4096, 1, GS_RGBA32F, 1, &lut, GS_DYNAMIC);
|
|
if (!this->cursorLUT)
|
|
blog(LOG_ERROR, "Failed to create cursor colour-transform LUT");
|
|
}
|
|
obs_leave_graphics();
|
|
|
|
this->cursorTransformCurVer = cursorTransformVer;
|
|
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;
|
|
}
|
|
|
|
const KVMFRFrame * frame = (KVMFRFrame *)msg.mem;
|
|
|
|
bool textureValid = (this->dmabufTested && this->dmabuf) || this->texture;
|
|
if (!textureValid || this->formatVer != frame->formatVer)
|
|
lgFormatInit(this, frame, &msg);
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
if (this->dmabuf)
|
|
{
|
|
obs_enter_graphics();
|
|
DMAFrameInfo * fi = dmabufOpenDMAFrameInfo(this, &msg, frame,
|
|
(size_t)frame->dataHeight * frame->pitch);
|
|
bool importFailed = !fi;
|
|
if (fi && !fi->texture)
|
|
importFailed = !dmabufCreateTexture(this, fi, frame);
|
|
obs_leave_graphics();
|
|
|
|
if (!importFailed)
|
|
{
|
|
// wait for the frame to be complete before we try to use it
|
|
FrameBuffer * fb = (FrameBuffer *)(((uint8_t*)frame) + frame->offset);
|
|
const bool complete = framebuffer_wait(
|
|
fb, (size_t)frame->dataHeight * frame->pitch);
|
|
lgmpClientMessageDone(this->frameQueue);
|
|
|
|
if (!complete)
|
|
{
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
this->dmaTexture = fi->texture;
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
puts("Failed to create dmabuf texture, falling back to CPU upload");
|
|
this->dmabuf = false;
|
|
lgFormatInit(this, frame, &msg);
|
|
}
|
|
#endif
|
|
|
|
if (!this->texture)
|
|
{
|
|
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 lgSetupCursorEffect(LGPlugin * this, int outputTransfer)
|
|
{
|
|
const KVMFRColorTransform * transform = &this->activeCursorTransform;
|
|
const bool matrixEnabled =
|
|
transform->flags & KVMFR_COLOR_TRANSFORM_MATRIX;
|
|
const bool lutEnabled =
|
|
(transform->flags & KVMFR_COLOR_TRANSFORM_LUT) && this->cursorLUT;
|
|
const float whiteLevel = atomic_load(&this->sdrWhiteLevel);
|
|
|
|
gs_effect_set_float(this->cursorMultiplier,
|
|
whiteLevel / LG_SCRGB_REFERENCE_WHITE);
|
|
gs_effect_set_float(this->cursorSDRWhiteLevel, whiteLevel);
|
|
gs_effect_set_int(this->cursorOutputTransfer, outputTransfer);
|
|
gs_effect_set_bool(this->cursorMatrixEnabled, matrixEnabled);
|
|
gs_effect_set_bool(this->cursorLUTEnabled, lutEnabled);
|
|
for (int i = 0; i < 3; ++i)
|
|
gs_effect_set_val(this->cursorColorMatrix[i],
|
|
transform->matrix[i], sizeof(transform->matrix[i]));
|
|
gs_effect_set_float(this->cursorColorScalar, transform->scalar);
|
|
if (lutEnabled)
|
|
gs_effect_set_texture(this->cursorColorLUT, this->cursorLUT);
|
|
}
|
|
|
|
static void lgVideoRender(void * data, gs_effect_t * effect)
|
|
{
|
|
LGPlugin * this = (LGPlugin *)data;
|
|
gs_texture_t * texture;
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
texture = this->dmaTexture;
|
|
if (!texture)
|
|
texture = this->texture;
|
|
#endif
|
|
|
|
if (!texture)
|
|
return;
|
|
|
|
const bool hdrDecode =
|
|
this->type == FRAME_TYPE_RGBA10 && this->hdr && this->hdrPQ;
|
|
|
|
if (this->type == FRAME_TYPE_RGB_24 || this->type == FRAME_TYPE_BGR_32)
|
|
{
|
|
effect = this->unpackEffect;
|
|
gs_effect_set_texture(this->image, 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 if (hdrDecode)
|
|
{
|
|
/* decode the PQ (ST.2084) encoded frame to linear scRGB and remap the
|
|
* source primaries to BT.709 so OBS receives valid GS_CS_709_SCRGB data */
|
|
effect = this->hdrEffect;
|
|
gs_effect_set_texture(this->hdrImage, texture);
|
|
struct vec2 outputSize;
|
|
vec2_set(&outputSize, this->frameWidth, this->frameHeight);
|
|
gs_effect_set_vec2(this->hdrOutputSize, &outputSize);
|
|
for (int i = 0; i < 3; ++i)
|
|
gs_effect_set_vec3(this->hdrColorMatrix[i], &this->colorMatrix[i]);
|
|
gs_effect_set_float(this->hdrScaleParam, this->hdrScale);
|
|
}
|
|
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, texture);
|
|
}
|
|
|
|
if (this->unpack)
|
|
texture = this->dstTexture;
|
|
|
|
while (gs_effect_loop(effect, "Draw"))
|
|
gs_draw_sprite(texture, 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);
|
|
|
|
/* Color cursor pixels are premultiplied sRGB. When rendering into OBS's
|
|
* linear scRGB space, decode them and reproduce the source display's SDR
|
|
* white level reported by the IDD. */
|
|
bool mapCursorToScRGB = false;
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
mapCursorToScRGB = this->colorSpace == GS_CS_709_SCRGB;
|
|
#endif
|
|
|
|
const bool hasCursorTransform =
|
|
this->activeCursorTransform.flags != 0;
|
|
const bool useCursorEffect = !this->cursorMono &&
|
|
(mapCursorToScRGB || hasCursorTransform);
|
|
const int cursorOutputTransfer = !mapCursorToScRGB ? 0 :
|
|
(this->hdrPQ ? 2 : 1);
|
|
|
|
effect = useCursorEffect ?
|
|
this->cursorEffect : obs_get_base_effect(OBS_EFFECT_DEFAULT);
|
|
gs_eparam_t * image = useCursorEffect ?
|
|
this->cursorImage : gs_effect_get_param_by_name(effect, "image");
|
|
gs_effect_set_texture(image, this->cursorTex);
|
|
if (useCursorEffect)
|
|
lgSetupCursorEffect(this, cursorOutputTransfer);
|
|
|
|
gs_matrix_push();
|
|
gs_matrix_translate3f(
|
|
this->cursorRect.x / this->screenScale.x,
|
|
this->cursorRect.y / this->screenScale.y,
|
|
0.0f);
|
|
|
|
gs_blend_state_push();
|
|
gs_enable_blending(true);
|
|
|
|
if (!this->cursorMono)
|
|
{
|
|
gs_blend_function_separate(
|
|
GS_BLEND_ONE, GS_BLEND_INVSRCALPHA,
|
|
GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
|
|
while (gs_effect_loop(effect, "Draw"))
|
|
gs_draw_sprite(this->cursorTex, 0, 0, 0);
|
|
|
|
if (this->cursor.type == CURSOR_TYPE_MASKED_COLOR &&
|
|
this->cursorXorTex)
|
|
{
|
|
/* The XOR plane is a logical operand, not SDR colour. Applying the
|
|
* cursor colour transform or white-level mapping changes the mask
|
|
* before the inverse-destination blend. */
|
|
effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
|
|
image = gs_effect_get_param_by_name(effect, "image");
|
|
gs_effect_set_texture(image, this->cursorXorTex);
|
|
gs_blend_function_separate(
|
|
GS_BLEND_INVDSTCOLOR, GS_BLEND_INVSRCALPHA,
|
|
GS_BLEND_ZERO , GS_BLEND_ONE);
|
|
while (gs_effect_loop(effect, "Draw"))
|
|
gs_draw_sprite(this->cursorXorTex, 0, 0, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gs_blend_function_separate(
|
|
GS_BLEND_INVDSTCOLOR, GS_BLEND_INVSRCALPHA,
|
|
GS_BLEND_ONE , GS_BLEND_INVSRCALPHA);
|
|
while (gs_effect_loop(effect, "Draw"))
|
|
gs_draw_sprite(this->cursorTex, 0, 0, 0);
|
|
}
|
|
|
|
gs_blend_state_pop();
|
|
gs_matrix_pop();
|
|
gs_set_scissor_rect(NULL);
|
|
}
|
|
}
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
static enum gs_color_space lgVideoGetColorSpace(void *data, size_t count,
|
|
const enum gs_color_space *preferred_spaces)
|
|
{
|
|
LGPlugin * this = (LGPlugin *)data;
|
|
return this->colorSpace;
|
|
}
|
|
#endif
|
|
|
|
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 | OBS_SOURCE_SRGB,
|
|
.get_name = lgGetName,
|
|
.create = lgCreate,
|
|
.destroy = lgDestroy,
|
|
.update = lgUpdate,
|
|
.get_defaults = lgGetDefaults,
|
|
.get_properties = lgGetProperties,
|
|
.video_tick = lgVideoTick,
|
|
.video_render = lgVideoRender,
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
.video_get_color_space = lgVideoGetColorSpace,
|
|
#endif
|
|
.get_width = lgGetWidth,
|
|
.get_height = lgGetHeight,
|
|
.icon_type = OBS_ICON_TYPE_DESKTOP_CAPTURE
|
|
};
|