mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-09 08:41:31 +00:00
Bump KVMFR to version 28 and attach generation, epoch, and deadline identities to cadence deliveries and their timing records. Accept phase corrections only for the exact completed publication. Keep VM and client clocks independent by exchanging relative phase errors. Make submission, completion, and work-estimate accounting best effort so scheduler state cannot delay GPU submission or frame publication.
1928 lines
54 KiB
C
1928 lines
54 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/util/platform.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/LGMPConfig.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"
|
|
#include "frame_scheduler.h"
|
|
|
|
#define LG_FRAME_TIMING_SPIN_COUNT 4096
|
|
|
|
/* 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;
|
|
PLGMPClientQueue frameOwnerQueue[LGMP_Q_FRAME_LEN];
|
|
PLGMPClientQueue pointerQueue;
|
|
gs_texture_t * texture;
|
|
gs_texture_t * dstTexture;
|
|
bool textureMapped;
|
|
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_BUFFER_LEN];
|
|
gs_texrender_t * dmaCopyRender;
|
|
gs_samplerstate_t * dmaCopySampler;
|
|
gs_timer_t * dmaCopyTimer;
|
|
#endif
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 28
|
|
enum gs_color_space colorSpace;
|
|
#endif
|
|
|
|
pthread_t frameThread, pointerThread;
|
|
os_sem_t * frameSem;
|
|
uint32_t frameSerial;
|
|
bool frameSerialValid;
|
|
pthread_mutex_t pointerLock;
|
|
LGFrameScheduler frameScheduler;
|
|
|
|
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;
|
|
|
|
typedef struct
|
|
{
|
|
LGMPMessage msg;
|
|
PLGMPClientQueue queue;
|
|
uint32_t scheduleEpoch;
|
|
uint32_t scheduleDeadlineSerial;
|
|
bool owner;
|
|
}
|
|
LGFrameMessage;
|
|
|
|
static void * frameThread(void * data);
|
|
static void * pointerThread(void * data);
|
|
static void lgUpdate(void * data, obs_data_t * settings);
|
|
|
|
static uint64_t lgFramePeriod(void)
|
|
{
|
|
struct obs_video_info videoInfo;
|
|
if (!obs_get_video_info(&videoInfo) ||
|
|
!videoInfo.fps_num || !videoInfo.fps_den)
|
|
return 0;
|
|
|
|
return 1000000000ULL * videoInfo.fps_den / videoInfo.fps_num;
|
|
}
|
|
|
|
static const char * lgGetName(void * unused)
|
|
{
|
|
return obs_module_text("Looking Glass Client");
|
|
}
|
|
|
|
static bool lgFrameSerialNewer(uint32_t lhs, uint32_t rhs)
|
|
{
|
|
return (int32_t)(lhs - rhs) > 0;
|
|
}
|
|
|
|
static bool lgFrameMessageHasMatchingDeadline(
|
|
bool owner, const LGMPMessage * message, const KVMFRFrame * frame)
|
|
{
|
|
if (!owner)
|
|
return false;
|
|
|
|
const uint32_t epoch = (uint32_t)(message->udata >> 32);
|
|
const uint32_t deadlineSerial = (uint32_t)message->udata;
|
|
return deadlineSerial && epoch == frame->scheduleEpoch &&
|
|
deadlineSerial == frame->scheduleDeadlineSerial;
|
|
}
|
|
|
|
static LGMP_STATUS lgFrameProcessNewest(LGPlugin * this,
|
|
LGFrameMessage * result)
|
|
{
|
|
struct
|
|
{
|
|
PLGMPClientQueue queue;
|
|
bool owner;
|
|
}
|
|
queues[LGMP_Q_FRAME_LEN + 1] =
|
|
{
|
|
{ this->frameQueue, false },
|
|
};
|
|
for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
|
|
{
|
|
queues[i + 1].queue = this->frameOwnerQueue[i];
|
|
queues[i + 1].owner = true;
|
|
}
|
|
|
|
memset(result, 0, sizeof(*result));
|
|
for (unsigned int i = 0; i < ARRAY_LENGTH(queues); ++i)
|
|
{
|
|
if (!queues[i].queue)
|
|
continue;
|
|
|
|
LGMP_STATUS status = lgmpClientAdvanceToLast(queues[i].queue);
|
|
if (status != LGMP_OK && status != LGMP_ERR_QUEUE_EMPTY)
|
|
return status;
|
|
|
|
LGMPMessage msg;
|
|
status = lgmpClientProcess(queues[i].queue, &msg);
|
|
if (status == LGMP_ERR_QUEUE_EMPTY)
|
|
continue;
|
|
if (status != LGMP_OK)
|
|
return status;
|
|
|
|
const KVMFRFrame * frame = (const KVMFRFrame *)msg.mem;
|
|
const bool owner = queues[i].owner || msg.udata != 0;
|
|
|
|
if (!result->queue)
|
|
{
|
|
result->msg = msg;
|
|
result->queue = queues[i].queue;
|
|
result->owner = owner;
|
|
continue;
|
|
}
|
|
|
|
const KVMFRFrame * selected = (const KVMFRFrame *)result->msg.mem;
|
|
const bool replace = lgFrameSerialNewer(
|
|
frame->frameSerial, selected->frameSerial) ||
|
|
(frame->frameSerial == selected->frameSerial &&
|
|
((owner && !result->owner) ||
|
|
(lgFrameMessageHasMatchingDeadline(owner, &msg, frame) &&
|
|
!lgFrameMessageHasMatchingDeadline(
|
|
result->owner, &result->msg, selected))));
|
|
PLGMPClientQueue discard = queues[i].queue;
|
|
if (replace)
|
|
{
|
|
discard = result->queue;
|
|
result->msg = msg;
|
|
result->queue = queues[i].queue;
|
|
result->owner = owner;
|
|
}
|
|
|
|
status = lgmpClientMessageDone(discard);
|
|
if (status != LGMP_OK)
|
|
return status;
|
|
}
|
|
|
|
if (result->owner)
|
|
{
|
|
result->scheduleEpoch = (uint32_t)(result->msg.udata >> 32);
|
|
result->scheduleDeadlineSerial = (uint32_t)result->msg.udata;
|
|
}
|
|
|
|
return result->queue ? LGMP_OK : LGMP_ERR_QUEUE_EMPTY;
|
|
}
|
|
|
|
static bool lgFrameTimingReady(const KVMFRFrame * frame)
|
|
{
|
|
return __atomic_load_n(&frame->timingValid, __ATOMIC_ACQUIRE) &&
|
|
frame->timingSerial == frame->frameSerial;
|
|
}
|
|
|
|
static bool lgFramePhaseIdentity(const LGFrameMessage * message,
|
|
const KVMFRFrame * frame, uint32_t * generation)
|
|
{
|
|
if (!message->owner || !message->scheduleEpoch ||
|
|
!message->scheduleDeadlineSerial)
|
|
return false;
|
|
|
|
for (unsigned i = 0;
|
|
!lgFrameTimingReady(frame) && i < LG_FRAME_TIMING_SPIN_COUNT;
|
|
++i)
|
|
{
|
|
}
|
|
|
|
if (!lgFrameTimingReady(frame) ||
|
|
!(frame->timingFlags & KVMFR_FRAME_TIMING_PHASE_VALID) ||
|
|
!frame->scheduleGeneration ||
|
|
frame->scheduleEpoch != message->scheduleEpoch ||
|
|
frame->scheduleDeadlineSerial != message->scheduleDeadlineSerial)
|
|
return false;
|
|
|
|
*generation = frame->scheduleGeneration;
|
|
return true;
|
|
}
|
|
|
|
static void lgFrameUnsubscribeOwnerQueues(LGPlugin * this)
|
|
{
|
|
for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
|
|
{
|
|
if (this->frameOwnerQueue[i])
|
|
lgmpClientUnsubscribe(&this->frameOwnerQueue[i]);
|
|
this->frameOwnerQueue[i] = NULL;
|
|
}
|
|
}
|
|
|
|
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);
|
|
pthread_mutex_init(&this->pointerLock, NULL);
|
|
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->dmabufTested = false;
|
|
}
|
|
|
|
/* The caller must hold the OBS graphics lock. */
|
|
static void dmabufReset(LGPlugin * this)
|
|
{
|
|
dmabufInvalidateTextures(this);
|
|
|
|
if (this->dmaCopyRender)
|
|
{
|
|
gs_texrender_destroy(this->dmaCopyRender);
|
|
this->dmaCopyRender = NULL;
|
|
}
|
|
|
|
if (this->dmaCopySampler)
|
|
{
|
|
gs_samplerstate_destroy(this->dmaCopySampler);
|
|
this->dmaCopySampler = NULL;
|
|
}
|
|
|
|
if (this->dmaCopyTimer)
|
|
{
|
|
gs_timer_destroy(this->dmaCopyTimer);
|
|
this->dmaCopyTimer = NULL;
|
|
}
|
|
|
|
/* 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->textureMapped)
|
|
gs_texture_unmap(this->texture);
|
|
gs_texture_destroy(this->texture);
|
|
this->texture = NULL;
|
|
this->textureMapped = false;
|
|
}
|
|
this->texData = NULL;
|
|
this->linesize = 0;
|
|
|
|
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);
|
|
pthread_mutex_destroy(&this->pointerLock);
|
|
|
|
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;
|
|
for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
|
|
this->frameOwnerQueue[i] = NULL;
|
|
|
|
this->frameQueue = NULL;
|
|
this->frameSerial = 0;
|
|
this->frameSerialValid = false;
|
|
|
|
if (lgmpClientSubscribe(
|
|
this->lgmp, LGMP_Q_FRAME, &this->frameQueue) != LGMP_OK)
|
|
{
|
|
this->state = STATE_STOPPING;
|
|
return NULL;
|
|
}
|
|
|
|
if (this->frameScheduler.supported)
|
|
{
|
|
for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
|
|
{
|
|
if (lgmpClientSubscribe(
|
|
this->lgmp, LGMP_Q_FRAME_OWNER + i,
|
|
&this->frameOwnerQueue[i]) == LGMP_OK)
|
|
continue;
|
|
|
|
lgFrameUnsubscribeOwnerQueues(this);
|
|
lgmpClientUnsubscribe(&this->frameQueue);
|
|
this->frameQueue = NULL;
|
|
this->state = STATE_STOPPING;
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
lgFrameSchedulerRequestImmediate(&this->frameScheduler);
|
|
|
|
this->state = STATE_RUNNING;
|
|
os_sem_post(this->frameSem);
|
|
|
|
while(this->state == STATE_RUNNING)
|
|
{
|
|
os_sem_wait(this->frameSem);
|
|
LGFrameMessage frameMessage;
|
|
const LGMP_STATUS status = lgFrameProcessNewest(this, &frameMessage);
|
|
if (status != LGMP_OK && status != LGMP_ERR_QUEUE_EMPTY)
|
|
{
|
|
os_sem_post(this->frameSem);
|
|
printf("lgFrameProcessNewest: %s\n", lgmpStatusString(status));
|
|
break;
|
|
}
|
|
|
|
uint64_t now = os_gettime_ns();
|
|
if (status == LGMP_OK)
|
|
{
|
|
const KVMFRFrame * frame =
|
|
(const KVMFRFrame *)frameMessage.msg.mem;
|
|
if (frameMessage.owner)
|
|
{
|
|
const FrameBuffer * fb =
|
|
(const FrameBuffer *)((const uint8_t *)frame + frame->offset);
|
|
if (framebuffer_wait(
|
|
fb, (size_t)frame->dataHeight * frame->pitch))
|
|
{
|
|
now = os_gettime_ns();
|
|
uint32_t generation;
|
|
if (lgFramePhaseIdentity(&frameMessage, frame, &generation))
|
|
lgFrameSchedulerObserveFrame(&this->frameScheduler,
|
|
frame->frameSerial, generation, frameMessage.scheduleEpoch,
|
|
frameMessage.scheduleDeadlineSerial, now);
|
|
}
|
|
}
|
|
}
|
|
|
|
pthread_mutex_lock(&this->pointerLock);
|
|
lgFrameSchedulerUpdate(&this->frameScheduler,
|
|
this->pointerQueue, now);
|
|
pthread_mutex_unlock(&this->pointerLock);
|
|
os_sem_post(this->frameSem);
|
|
usleep(1000);
|
|
}
|
|
|
|
lgFrameUnsubscribeOwnerQueues(this);
|
|
lgmpClientUnsubscribe(&this->frameQueue);
|
|
this->frameQueue = NULL;
|
|
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;
|
|
|
|
pthread_mutex_lock(&this->pointerLock);
|
|
const LGMP_STATUS subscribeStatus = lgmpClientSubscribe(
|
|
this->lgmp, LGMP_Q_POINTER, &this->pointerQueue);
|
|
pthread_mutex_unlock(&this->pointerLock);
|
|
if (subscribeStatus != LGMP_OK)
|
|
{
|
|
this->state = STATE_STOPPING;
|
|
return NULL;
|
|
}
|
|
|
|
while(this->state == STATE_RUNNING)
|
|
{
|
|
LGMP_STATUS status;
|
|
LGMPMessage msg;
|
|
|
|
pthread_mutex_lock(&this->pointerLock);
|
|
if ((status = lgmpClientProcess(this->pointerQueue, &msg)) != LGMP_OK)
|
|
{
|
|
pthread_mutex_unlock(&this->pointerLock);
|
|
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 ?
|
|
false : (msg.udata & CURSOR_FLAG_VISIBLE) != 0;
|
|
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);
|
|
pthread_mutex_unlock(&this->pointerLock);
|
|
}
|
|
|
|
pthread_mutex_lock(&this->pointerLock);
|
|
lgmpClientUnsubscribe(&this->pointerQueue);
|
|
pthread_mutex_unlock(&this->pointerLock);
|
|
|
|
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 clientID;
|
|
uint32_t remoteVersion;
|
|
if ((status = lgmpClientSessionInit(this->lgmp, &udataSize,
|
|
(uint8_t **)&udata, &clientID, &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;
|
|
}
|
|
|
|
lgFrameSchedulerInit(&this->frameScheduler,
|
|
udata->features & KVMFR_FEATURE_FRAME_SCHEDULE, clientID);
|
|
lgFrameSchedulerSetPeriod(&this->frameScheduler, lgFramePeriod());
|
|
|
|
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;
|
|
}
|
|
|
|
static bool dmabufCopyTexture(LGPlugin * this, gs_texture_t * texture)
|
|
{
|
|
gs_texrender_reset(this->dmaCopyRender);
|
|
if (!gs_texrender_begin(
|
|
this->dmaCopyRender, this->dataWidth, this->dataHeight))
|
|
return false;
|
|
|
|
gs_ortho(0.0f, (float)this->dataWidth,
|
|
0.0f, (float)this->dataHeight, -100.0f, 100.0f);
|
|
|
|
gs_effect_t * effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
|
|
gs_eparam_t * image = effect ?
|
|
gs_effect_get_param_by_name(effect, "image") : NULL;
|
|
if (!image)
|
|
{
|
|
gs_texrender_end(this->dmaCopyRender);
|
|
return false;
|
|
}
|
|
|
|
/* EGLImages can use XRGB storage that is incompatible with GL copy-image.
|
|
* A render pass samples across that storage boundary while preserving the
|
|
* RGBA channels needed by the packed frame formats. */
|
|
const bool framebufferSRGB = gs_framebuffer_srgb_enabled();
|
|
gs_enable_framebuffer_srgb(false);
|
|
gs_effect_set_texture(image, texture);
|
|
gs_effect_set_next_sampler(image, this->dmaCopySampler);
|
|
gs_blend_state_push();
|
|
gs_enable_blending(false);
|
|
while (gs_effect_loop(effect, "Draw"))
|
|
gs_draw_sprite(texture, 0, this->dataWidth, this->dataHeight);
|
|
gs_effect_set_texture(image, NULL);
|
|
gs_blend_state_pop();
|
|
gs_enable_framebuffer_srgb(framebufferSRGB);
|
|
|
|
gs_texrender_end(this->dmaCopyRender);
|
|
return gs_texrender_get_texture(this->dmaCopyRender) != 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, PLGMPClientQueue frameQueue)
|
|
{
|
|
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);
|
|
if (this->dmaCopyRender)
|
|
{
|
|
gs_texrender_destroy(this->dmaCopyRender);
|
|
this->dmaCopyRender = NULL;
|
|
}
|
|
#endif
|
|
if (this->unpack && this->dstTexture)
|
|
{
|
|
gs_texture_destroy(this->dstTexture);
|
|
this->dstTexture = NULL;
|
|
}
|
|
|
|
if (this->texture)
|
|
{
|
|
if (this->textureMapped)
|
|
gs_texture_unmap(this->texture);
|
|
|
|
gs_texture_destroy(this->texture);
|
|
this->texture = NULL;
|
|
this->textureMapped = false;
|
|
}
|
|
this->texData = NULL;
|
|
this->linesize = 0;
|
|
|
|
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(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;
|
|
}
|
|
|
|
if (!this->dmaCopyTimer)
|
|
this->dmaCopyTimer = gs_timer_create();
|
|
if (!this->dmaCopyTimer)
|
|
{
|
|
puts("Failed to create DMA copy timer");
|
|
this->dmabuf = false;
|
|
}
|
|
|
|
if (this->dmabuf && !this->dmaCopySampler)
|
|
{
|
|
const struct gs_sampler_info samplerInfo =
|
|
{
|
|
.filter = GS_FILTER_POINT,
|
|
.address_u = GS_ADDRESS_CLAMP,
|
|
.address_v = GS_ADDRESS_CLAMP,
|
|
.address_w = GS_ADDRESS_CLAMP,
|
|
};
|
|
this->dmaCopySampler = gs_samplerstate_create(&samplerInfo);
|
|
if (!this->dmaCopySampler)
|
|
{
|
|
puts("Failed to create DMA copy sampler");
|
|
this->dmabuf = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this->dmabuf)
|
|
{
|
|
this->dmaCopyRender =
|
|
gs_texrender_create(this->format, GS_ZS_NONE);
|
|
if (!this->dmaCopyRender)
|
|
{
|
|
puts("Failed to create DMA copy render target");
|
|
this->dmabuf = false;
|
|
}
|
|
}
|
|
#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(frameQueue);
|
|
os_sem_post(this->frameSem);
|
|
obs_leave_graphics();
|
|
return;
|
|
}
|
|
|
|
this->textureMapped =
|
|
gs_texture_map(this->texture, &this->texData, &this->linesize);
|
|
if (!this->textureMapped)
|
|
{
|
|
puts("Failed to map upload texture");
|
|
gs_texture_destroy(this->texture);
|
|
this->texture = NULL;
|
|
this->texData = NULL;
|
|
this->linesize = 0;
|
|
}
|
|
}
|
|
|
|
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;
|
|
(void)seconds;
|
|
|
|
if (this->state == STATE_RESTARTING)
|
|
{
|
|
waitThreads(this);
|
|
|
|
this->state = STATE_STARTING;
|
|
createThreads(this);
|
|
}
|
|
if (this->state != STATE_RUNNING)
|
|
return;
|
|
|
|
const uint64_t tickTime = os_gettime_ns();
|
|
const uint64_t framePeriod = lgFramePeriod();
|
|
LGMP_STATUS status;
|
|
LGFrameMessage frameMessage;
|
|
|
|
os_sem_wait(this->frameSem);
|
|
if (this->state != STATE_RUNNING)
|
|
{
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
lgFrameSchedulerSetPeriod(&this->frameScheduler, framePeriod);
|
|
|
|
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 = lgFrameProcessNewest(this, &frameMessage)) != LGMP_OK)
|
|
{
|
|
if (status == LGMP_ERR_QUEUE_EMPTY)
|
|
{
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
printf("lgFrameProcessNewest: %s\n", lgmpStatusString(status));
|
|
this->state = STATE_STOPPING;
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
const KVMFRFrame * frame = (const KVMFRFrame *)frameMessage.msg.mem;
|
|
const bool equalSerial = this->frameSerialValid &&
|
|
frame->frameSerial == this->frameSerial;
|
|
if (this->frameSerialValid && !equalSerial &&
|
|
!lgFrameSerialNewer(frame->frameSerial, this->frameSerial))
|
|
{
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
if (equalSerial && !frameMessage.owner)
|
|
{
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
const FrameBuffer * fb =
|
|
(const FrameBuffer *)((const uint8_t *)frame + frame->offset);
|
|
bool frameComplete = false;
|
|
if (frameMessage.owner)
|
|
{
|
|
frameComplete = framebuffer_wait(
|
|
fb, (size_t)frame->dataHeight * frame->pitch);
|
|
if (!frameComplete)
|
|
{
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
const uint64_t readyTime = os_gettime_ns();
|
|
uint32_t generation;
|
|
if (lgFramePhaseIdentity(&frameMessage, frame, &generation))
|
|
{
|
|
lgFrameSchedulerObserveFrame(&this->frameScheduler,
|
|
frame->frameSerial, generation, frameMessage.scheduleEpoch,
|
|
frameMessage.scheduleDeadlineSerial, readyTime);
|
|
lgFrameSchedulerFeedback(&this->frameScheduler,
|
|
frame->frameSerial, generation, frameMessage.scheduleEpoch,
|
|
frameMessage.scheduleDeadlineSerial, tickTime);
|
|
}
|
|
}
|
|
|
|
this->frameSerial = frame->frameSerial;
|
|
this->frameSerialValid = true;
|
|
|
|
const bool textureValid = this->dmabuf ?
|
|
this->dmabufTested && this->dmaCopyRender && this->dmaCopySampler &&
|
|
this->dmaCopyTimer &&
|
|
gs_texrender_get_texture(this->dmaCopyRender) :
|
|
this->texture && this->textureMapped;
|
|
if (!textureValid || this->formatVer != frame->formatVer)
|
|
lgFormatInit(this, frame, &frameMessage.msg, frameMessage.queue);
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
if (this->dmabuf)
|
|
{
|
|
obs_enter_graphics();
|
|
DMAFrameInfo * fi = dmabufOpenDMAFrameInfo(this, &frameMessage.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 copying it into OBS-owned
|
|
// storage. The imported texture remains backed by reusable IVSHMEM.
|
|
if (!frameComplete)
|
|
frameComplete = framebuffer_wait(
|
|
fb, (size_t)frame->dataHeight * frame->pitch);
|
|
|
|
if (!frameComplete)
|
|
{
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
obs_enter_graphics();
|
|
uint64_t copyTime;
|
|
gs_timer_begin(this->dmaCopyTimer);
|
|
const bool copied = dmabufCopyTexture(this, fi->texture);
|
|
gs_timer_end(this->dmaCopyTimer);
|
|
if (!gs_timer_get_data(this->dmaCopyTimer, ©Time))
|
|
{
|
|
blog(LOG_ERROR, "Failed to wait for DMA frame copy");
|
|
obs_leave_graphics();
|
|
this->state = STATE_STOPPING;
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
obs_leave_graphics();
|
|
|
|
if (copied)
|
|
{
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
importFailed = true;
|
|
}
|
|
|
|
puts("Failed to import or snapshot dmabuf texture, "
|
|
"falling back to CPU upload");
|
|
this->dmabuf = false;
|
|
lgFormatInit(this, frame, &frameMessage.msg, frameMessage.queue);
|
|
}
|
|
#endif
|
|
|
|
if (!this->texture || !this->textureMapped || !this->texData)
|
|
{
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
return;
|
|
}
|
|
|
|
framebuffer_read(
|
|
fb,
|
|
this->texData , // dst
|
|
this->linesize , // dstpitch
|
|
this->dataHeight, // height
|
|
this->dataWidth , // width
|
|
this->bpp , // bpp
|
|
frame->pitch
|
|
);
|
|
|
|
lgmpClientMessageDone(frameMessage.queue);
|
|
os_sem_post(this->frameSem);
|
|
|
|
obs_enter_graphics();
|
|
if (this->textureMapped)
|
|
gs_texture_unmap(this->texture);
|
|
this->textureMapped = false;
|
|
this->texData = NULL;
|
|
this->linesize = 0;
|
|
this->textureMapped =
|
|
gs_texture_map(this->texture, &this->texData, &this->linesize);
|
|
if (!this->textureMapped)
|
|
{
|
|
blog(LOG_ERROR, "Failed to remap upload texture");
|
|
gs_texture_destroy(this->texture);
|
|
this->texture = NULL;
|
|
}
|
|
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 = this->texture;
|
|
|
|
#if LIBOBS_API_MAJOR_VER >= 27
|
|
if (this->dmabuf)
|
|
texture = this->dmaCopyRender ?
|
|
gs_texrender_get_texture(this->dmaCopyRender) : NULL;
|
|
#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
|
|
};
|