diff --git a/obs/CMakeLists.txt b/obs/CMakeLists.txt index bbb60936..02c0e9d8 100644 --- a/obs/CMakeLists.txt +++ b/obs/CMakeLists.txt @@ -104,6 +104,7 @@ link_libraries( set(SOURCES ${CMAKE_BINARY_DIR}/version.c + frame_scheduler.c main.c lg.c ) diff --git a/obs/frame_scheduler.c b/obs/frame_scheduler.c new file mode 100644 index 00000000..0cd57426 --- /dev/null +++ b/obs/frame_scheduler.c @@ -0,0 +1,156 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "frame_scheduler.h" + +#include + +#include + +#define FRAME_SCHEDULER_LEASE_MS 1000U +#define FRAME_SCHEDULER_RENEW_NS 250000000ULL +#define FRAME_SCHEDULER_FEEDBACK_NS 50000000ULL +#define FRAME_SCHEDULER_TARGET_SLACK_NS 1000000ULL +#define FRAME_SCHEDULER_MIN_PERIOD_NS 2000000ULL +#define FRAME_SCHEDULER_MAX_PERIOD_NS 1000000000ULL + +void lgFrameSchedulerInit(LGFrameScheduler * scheduler, bool supported, + uint32_t clientID) +{ + memset(scheduler, 0, sizeof(*scheduler)); + scheduler->supported = supported; + scheduler->clientID = clientID; +} + +void lgFrameSchedulerSetPeriod(LGFrameScheduler * scheduler, + uint64_t period) +{ + if (!scheduler->supported || + period < FRAME_SCHEDULER_MIN_PERIOD_NS || + period > FRAME_SCHEDULER_MAX_PERIOD_NS) + return; + + bool reset = !scheduler->period; + if (!reset) + { + const uint64_t delta = scheduler->period > period ? + scheduler->period - period : period - scheduler->period; + reset = delta > scheduler->period / 20; + } + + scheduler->period = period; + if (!reset) + return; + + ++scheduler->generation; + scheduler->resetPending = true; + scheduler->phaseError = 0; + scheduler->feedbackFrameSerial = 0; + scheduler->feedbackSamples = 0; + scheduler->feedbackDirty = false; +} + +void lgFrameSchedulerObserveFrame(LGFrameScheduler * scheduler, + uint32_t frameSerial, uint32_t generation, uint64_t readyTime) +{ + if (!generation || + (scheduler->readyFrameSerial == frameSerial && + scheduler->readyGeneration == generation)) + return; + + scheduler->readyFrameSerial = frameSerial; + scheduler->readyGeneration = generation; + scheduler->readyTime = readyTime; +} + +void lgFrameSchedulerFeedback(LGFrameScheduler * scheduler, + uint32_t frameSerial, uint32_t generation, uint64_t neededTime) +{ + if (!scheduler->active || generation != scheduler->generation || + frameSerial == scheduler->feedbackFrameSerial) + return; + + uint64_t readyTime = neededTime; + if (scheduler->readyFrameSerial == frameSerial && + scheduler->readyGeneration == generation) + readyTime = scheduler->readyTime; + + const uint64_t measuredPhase = neededTime > readyTime ? + neededTime - readyTime : 0; + int64_t error = measuredPhase > FRAME_SCHEDULER_TARGET_SLACK_NS ? + (int64_t)(measuredPhase - FRAME_SCHEDULER_TARGET_SLACK_NS) : + -(int64_t)(FRAME_SCHEDULER_TARGET_SLACK_NS - measuredPhase); + const int64_t period = (int64_t)scheduler->period; + error %= period; + if (error > period / 2) + error -= period; + else if (error < -period / 2) + error += period; + + if (!scheduler->feedbackSamples) + scheduler->phaseError = error; + else + scheduler->phaseError = (scheduler->phaseError * 7 + error) / 8; + if (scheduler->feedbackSamples < 32) + ++scheduler->feedbackSamples; + + scheduler->feedbackFrameSerial = frameSerial; + scheduler->feedbackDirty = true; +} + +void lgFrameSchedulerUpdate(LGFrameScheduler * scheduler, + PLGMPClientQueue queue, uint64_t now) +{ + if (!scheduler->supported || !scheduler->period || !queue) + return; + + const uint64_t interval = scheduler->feedbackDirty ? + FRAME_SCHEDULER_FEEDBACK_NS : FRAME_SCHEDULER_RENEW_NS; + if (scheduler->active && !scheduler->resetPending && + now - scheduler->lastSend < interval) + return; + + KVMFRFrameScheduleFlags flags = KVMFR_FRAME_SCHEDULE_ACTIVE; + if (scheduler->resetPending) + flags |= KVMFR_FRAME_SCHEDULE_RESET; + + const uint32_t feedbackFrameSerial = + scheduler->feedbackFrameSerial; + const KVMFRFrameSchedule message = { + .msg.type = KVMFR_MESSAGE_FRAME_SCHEDULE, + .clientID = scheduler->clientID, + .generation = scheduler->generation, + .flags = flags, + .period = scheduler->period, + .targetSlack = FRAME_SCHEDULER_TARGET_SLACK_NS, + .phaseError = scheduler->phaseError, + .feedbackFrameSerial = feedbackFrameSerial, + .lease = FRAME_SCHEDULER_LEASE_MS, + }; + + if (lgmpClientSendData(queue, &message, sizeof(message), NULL) != LGMP_OK) + return; + + scheduler->active = true; + scheduler->resetPending = false; + scheduler->lastSend = now; + if (scheduler->feedbackFrameSerial == feedbackFrameSerial) + scheduler->feedbackDirty = false; +} diff --git a/obs/frame_scheduler.h b/obs/frame_scheduler.h new file mode 100644 index 00000000..091b71d4 --- /dev/null +++ b/obs/frame_scheduler.h @@ -0,0 +1,61 @@ +/** + * 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 + */ + +#ifndef LG_OBS_FRAME_SCHEDULER_H +#define LG_OBS_FRAME_SCHEDULER_H + +#include + +#include +#include + +typedef struct LGFrameScheduler +{ + bool supported; + bool active; + bool resetPending; + bool feedbackDirty; + uint32_t clientID; + uint32_t generation; + uint64_t period; + uint64_t lastSend; + + int64_t phaseError; + uint32_t feedbackFrameSerial; + unsigned feedbackSamples; + + uint32_t readyFrameSerial; + uint32_t readyGeneration; + uint64_t readyTime; +} +LGFrameScheduler; + +void lgFrameSchedulerInit(LGFrameScheduler * scheduler, bool supported, + uint32_t clientID); +void lgFrameSchedulerSetPeriod(LGFrameScheduler * scheduler, + uint64_t period); +void lgFrameSchedulerObserveFrame(LGFrameScheduler * scheduler, + uint32_t frameSerial, uint32_t generation, uint64_t readyTime); +void lgFrameSchedulerFeedback(LGFrameScheduler * scheduler, + uint32_t frameSerial, uint32_t generation, uint64_t neededTime); +void lgFrameSchedulerUpdate(LGFrameScheduler * scheduler, + PLGMPClientQueue queue, uint64_t now); + +#endif diff --git a/obs/lg.c b/obs/lg.c index 0950999d..496f79cf 100644 --- a/obs/lg.c +++ b/obs/lg.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -40,6 +41,7 @@ #include "rgb24.effect.h" #include "hdrpq.effect.h" #include "cursor.effect.h" +#include "frame_scheduler.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 @@ -122,6 +124,8 @@ typedef struct pthread_t frameThread, pointerThread; os_sem_t * frameSem; + pthread_mutex_t pointerLock; + LGFrameScheduler frameScheduler; bool cursorMono; gs_texture_t * cursorTex; @@ -169,6 +173,16 @@ 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"); @@ -266,6 +280,7 @@ static void * lgCreate(obs_data_t * settings, obs_source_t * context) 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); @@ -422,6 +437,7 @@ static void lgDestroy(void * 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); @@ -488,6 +504,23 @@ static void * frameThread(void * data) break; } } + + const uint64_t now = os_gettime_ns(); + if (status == LGMP_OK) + { + LGMPMessage msg; + if (lgmpClientProcess(this->frameQueue, &msg) == LGMP_OK) + { + const KVMFRFrame * frame = (const KVMFRFrame *)msg.mem; + lgFrameSchedulerObserveFrame(&this->frameScheduler, + frame->frameSerial, msg.udata, now); + } + } + + pthread_mutex_lock(&this->pointerLock); + lgFrameSchedulerUpdate(&this->frameScheduler, + this->pointerQueue, now); + pthread_mutex_unlock(&this->pointerLock); os_sem_post(this->frameSem); usleep(1000); } @@ -511,8 +544,11 @@ static void * pointerThread(void * data) { LGPlugin * this = (LGPlugin *)data; - if (lgmpClientSubscribe( - this->lgmp, LGMP_Q_POINTER, &this->pointerQueue) != LGMP_OK) + 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; @@ -523,8 +559,10 @@ static void * pointerThread(void * data) 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)); @@ -651,9 +689,12 @@ static void * pointerThread(void * data) } 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; @@ -693,9 +734,10 @@ static void lgUpdate(void * data, obs_data_t * settings) usleep(200000); + uint32_t clientID; uint32_t remoteVersion; if ((status = lgmpClientSessionInit(this->lgmp, &udataSize, - (uint8_t **)&udata, NULL, &remoteVersion)) != LGMP_OK) + (uint8_t **)&udata, &clientID, &remoteVersion)) != LGMP_OK) { printf("lgmpClientSessionInit: %s", lgmpStatusString(status)); if (status == LGMP_ERR_INVALID_VERSION) @@ -719,6 +761,10 @@ static void lgUpdate(void * data, obs_data_t * settings) return; } + lgFrameSchedulerInit(&this->frameScheduler, + udata->features & KVMFR_FEATURE_FRAME_SCHEDULE, clientID); + lgFrameSchedulerSetPeriod(&this->frameScheduler, lgFramePeriod()); + this->state = STATE_STARTING; createThreads(this); } @@ -1075,6 +1121,7 @@ static void lgFormatInit(LGPlugin * this, const KVMFRFrame * frame, static void lgVideoTick(void * data, float seconds) { LGPlugin * this = (LGPlugin *)data; + (void)seconds; if (this->state == STATE_RESTARTING) { @@ -1086,6 +1133,8 @@ static void lgVideoTick(void * data, float seconds) if (this->state != STATE_RUNNING) return; + const uint64_t tickTime = os_gettime_ns(); + const uint64_t framePeriod = lgFramePeriod(); LGMP_STATUS status; LGMPMessage msg; @@ -1096,6 +1145,8 @@ static void lgVideoTick(void * data, float seconds) return; } + lgFrameSchedulerSetPeriod(&this->frameScheduler, framePeriod); + this->cursorRect.x = this->cursor.x; this->cursorRect.y = this->cursor.y; @@ -1236,6 +1287,10 @@ static void lgVideoTick(void * data, float seconds) } const KVMFRFrame * frame = (KVMFRFrame *)msg.mem; + lgFrameSchedulerObserveFrame(&this->frameScheduler, + frame->frameSerial, msg.udata, tickTime); + lgFrameSchedulerFeedback(&this->frameScheduler, + frame->frameSerial, msg.udata, tickTime); bool textureValid = (this->dmabufTested && this->dmabuf) || this->texture; if (!textureValid || this->formatVer != frame->formatVer)