From 1176e78a7f198f2ab184ce67975b4a476624b116 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 4 Aug 2026 13:05:46 +1000 Subject: [PATCH] [idd] elect the fastest frame timing client --- idd/LGIdd/CFrameScheduler.cpp | 216 +++++++++++++++++++++++++++ idd/LGIdd/CFrameScheduler.h | 71 +++++++++ idd/LGIdd/CIndirectDeviceContext.cpp | 23 +++ idd/LGIdd/CIndirectDeviceContext.h | 3 + idd/LGIdd/LGIdd.vcxproj | 2 + idd/LGIdd/LGIdd.vcxproj.filters | 6 + 6 files changed, 321 insertions(+) create mode 100644 idd/LGIdd/CFrameScheduler.cpp create mode 100644 idd/LGIdd/CFrameScheduler.h diff --git a/idd/LGIdd/CFrameScheduler.cpp b/idd/LGIdd/CFrameScheduler.cpp new file mode 100644 index 00000000..37b88ee2 --- /dev/null +++ b/idd/LGIdd/CFrameScheduler.cpp @@ -0,0 +1,216 @@ +/** + * 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 "CFrameScheduler.h" + +#include "CDebug.h" + +static const uint64_t MIN_PERIOD_NS = 2000000ULL; +static const uint64_t MAX_PERIOD_NS = 1000000000ULL; +static const uint32_t MIN_LEASE_MS = 100; +static const uint32_t MAX_LEASE_MS = 5000; + +uint64_t CFrameScheduler::Nanotime() +{ + static const uint64_t frequency = []() + { + LARGE_INTEGER value; + QueryPerformanceFrequency(&value); + return static_cast(value.QuadPart); + }(); + + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + const uint64_t ticks = static_cast(counter.QuadPart); + return ticks / frequency * 1000000000ULL + + ticks % frequency * 1000000000ULL / frequency; +} + +CFrameScheduler::Client * CFrameScheduler::FindClient(uint32_t clientID) +{ + if (!clientID) + return nullptr; + + for (Client& client : m_clients) + if (client.clientID == clientID) + return &client; + + return nullptr; +} + +void CFrameScheduler::ElectOwner(uint64_t now) +{ + Client * fastest = nullptr; + Client * incumbent = FindClient(m_schedule.clientID); + unsigned subscribers = 0; + + for (Client& client : m_clients) + { + if (!client.subscribed) + continue; + + ++subscribers; + if (!client.active || client.expiry <= now) + { + client.active = false; + fastest = nullptr; + break; + } + + if (!fastest || client.period < fastest->period) + fastest = &client; + } + + if (!subscribers) + fastest = nullptr; + + if (fastest && incumbent && incumbent->subscribed && incumbent->active && + incumbent->expiry > now && + incumbent->period <= fastest->period + fastest->period / 200) + fastest = incumbent; + + const uint32_t oldClientID = m_schedule.clientID; + const uint32_t oldGeneration = m_schedule.generation; + if (!fastest) + { + m_schedule = {}; + m_scheduling = false; + } + else + { + m_schedule.clientID = fastest->clientID; + m_schedule.generation = fastest->generation; + m_schedule.period = fastest->period; + m_schedule.targetSlack = fastest->targetSlack; + m_scheduling = true; + } + + if (oldClientID != m_schedule.clientID || + oldGeneration != m_schedule.generation) + { + if (m_scheduling) + DEBUG_INFO("Frame timing owner %u generation %u at %.3f Hz", + m_schedule.clientID, m_schedule.generation, + 1000000000.0 / m_schedule.period); + else if (oldClientID) + DEBUG_INFO("Frame timing owner released; using push delivery"); + } +} + +void CFrameScheduler::Reset() +{ + AcquireSRWLockExclusive(&m_lock); + for (Client& client : m_clients) + client = {}; + m_schedule = {}; + m_scheduling = false; + ReleaseSRWLockExclusive(&m_lock); +} + +void CFrameScheduler::UpdateSubscribers(const uint32_t * clientIDs, + unsigned count, uint64_t now) +{ + AcquireSRWLockExclusive(&m_lock); + + for (Client& client : m_clients) + client.subscribed = false; + + for (unsigned i = 0; i < count; ++i) + { + Client * client = FindClient(clientIDs[i]); + if (!client) + for (Client& candidate : m_clients) + if (!candidate.clientID) + { + candidate.clientID = clientIDs[i]; + client = &candidate; + break; + } + + if (client) + client->subscribed = true; + } + + for (Client& client : m_clients) + if (client.clientID && !client.subscribed) + client = {}; + + ElectOwner(now); + ReleaseSRWLockExclusive(&m_lock); +} + +bool CFrameScheduler::UpdateSchedule(const KVMFRFrameSchedule& schedule, + uint64_t now) +{ + static const KVMFRFrameScheduleFlags validFlags = + KVMFR_FRAME_SCHEDULE_ACTIVE | + KVMFR_FRAME_SCHEDULE_RELEASE | + KVMFR_FRAME_SCHEDULE_RESET | + KVMFR_FRAME_SCHEDULE_IMMEDIATE; + + if (!schedule.clientID || schedule.flags & ~validFlags) + return false; + + AcquireSRWLockExclusive(&m_lock); + Client * client = FindClient(schedule.clientID); + if (!client || !client->subscribed) + { + ReleaseSRWLockExclusive(&m_lock); + return false; + } + + if (schedule.flags & KVMFR_FRAME_SCHEDULE_RELEASE) + { + client->active = false; + client->expiry = 0; + ElectOwner(now); + ReleaseSRWLockExclusive(&m_lock); + return true; + } + + if (!(schedule.flags & KVMFR_FRAME_SCHEDULE_ACTIVE) || + schedule.period < MIN_PERIOD_NS || + schedule.period > MAX_PERIOD_NS || + schedule.targetSlack >= schedule.period || + schedule.lease < MIN_LEASE_MS || schedule.lease > MAX_LEASE_MS) + { + ReleaseSRWLockExclusive(&m_lock); + return false; + } + + client->generation = schedule.generation; + client->period = schedule.period; + client->targetSlack = schedule.targetSlack; + client->expiry = now + static_cast(schedule.lease) * 1000000; + client->active = true; + ElectOwner(now); + ReleaseSRWLockExclusive(&m_lock); + return true; +} + +bool CFrameScheduler::GetSchedule(Schedule& schedule) const +{ + AcquireSRWLockShared(&m_lock); + const bool result = m_scheduling; + if (result) + schedule = m_schedule; + ReleaseSRWLockShared(&m_lock); + return result; +} diff --git a/idd/LGIdd/CFrameScheduler.h b/idd/LGIdd/CFrameScheduler.h new file mode 100644 index 00000000..bae9a300 --- /dev/null +++ b/idd/LGIdd/CFrameScheduler.h @@ -0,0 +1,71 @@ +/** + * 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 + */ + +#pragma once + +#include +#include + +extern "C" { + #include +} + +#include "common/KVMFR.h" + +class CFrameScheduler +{ +public: + struct Schedule + { + uint32_t clientID; + uint32_t generation; + uint64_t period; + uint64_t targetSlack; + }; + +private: + struct Client + { + uint32_t clientID; + uint32_t generation; + uint64_t period; + uint64_t targetSlack; + uint64_t expiry; + bool subscribed; + bool active; + }; + + mutable SRWLOCK m_lock = SRWLOCK_INIT; + Client m_clients[LGMP_MAX_CLIENTS] = {}; + Schedule m_schedule = {}; + bool m_scheduling = false; + + Client * FindClient(uint32_t clientID); + void ElectOwner(uint64_t now); + +public: + static uint64_t Nanotime(); + + void Reset(); + void UpdateSubscribers(const uint32_t * clientIDs, unsigned count, + uint64_t now); + bool UpdateSchedule(const KVMFRFrameSchedule& schedule, uint64_t now); + bool GetSchedule(Schedule& schedule) const; +}; diff --git a/idd/LGIdd/CIndirectDeviceContext.cpp b/idd/LGIdd/CIndirectDeviceContext.cpp index edbc20eb..006d8b37 100644 --- a/idd/LGIdd/CIndirectDeviceContext.cpp +++ b/idd/LGIdd/CIndirectDeviceContext.cpp @@ -1195,6 +1195,7 @@ void CIndirectDeviceContext::DeInitLGMP() if (m_lgmp == nullptr) { + m_frameScheduler.Reset(); m_publishedFrameIndex.store(-1, std::memory_order_release); m_frameResendPending = false; return; @@ -1206,6 +1207,8 @@ void CIndirectDeviceContext::DeInitLGMP() m_lgmpTimer = nullptr; } + m_frameScheduler.Reset(); + AcquireSRWLockExclusive(&m_framePublishLock); m_publishedFrameIndex.store(-1, std::memory_order_release); m_frameResendPending = false; @@ -1256,6 +1259,16 @@ void CIndirectDeviceContext::LGMPTimer() return; } + const uint64_t now = CFrameScheduler::Nanotime(); + uint32_t clientIDs[LGMP_MAX_CLIENTS] = {}; + unsigned clientCount = 0; + status = lgmpHostGetClientIDs(m_frameQueue, clientIDs, &clientCount); + if (status == LGMP_OK) + m_frameScheduler.UpdateSubscribers(clientIDs, clientCount, now); + else + DEBUG_WARN("Failed to query LGMP frame subscribers: %s", + lgmpStatusString(status)); + uint8_t data[LGMP_MSGS_SIZE]; size_t size; while ((status = lgmpHostReadData(m_pointerQueue, &data, &size)) == LGMP_OK) @@ -1274,6 +1287,16 @@ void CIndirectDeviceContext::LGMPTimer() { KVMFRWindowSize* ws = (KVMFRWindowSize*)msg; SetResolution(ws->w, ws->h); + break; + } + + case KVMFR_MESSAGE_FRAME_SCHEDULE: + { + if (size != sizeof(KVMFRFrameSchedule) || + !m_frameScheduler.UpdateSchedule( + *reinterpret_cast(msg), now)) + DEBUG_WARN("Ignoring invalid KVMFR frame schedule"); + break; } } diff --git a/idd/LGIdd/CIndirectDeviceContext.h b/idd/LGIdd/CIndirectDeviceContext.h index 47a3ac13..e46db882 100644 --- a/idd/LGIdd/CIndirectDeviceContext.h +++ b/idd/LGIdd/CIndirectDeviceContext.h @@ -30,6 +30,7 @@ #include "CSettings.h" #include "CEdid.h" #include "CPostProcessor.h" +#include "CFrameScheduler.h" extern "C" { #include "lgmp/host.h" @@ -89,6 +90,8 @@ private: WDFTIMER m_lgmpTimer = nullptr; PLGMPHostQueue m_frameQueue = nullptr; + CFrameScheduler m_frameScheduler; + PLGMPHostQueue m_pointerQueue = nullptr; PLGMPMemory m_pointerMemory [LGMP_Q_POINTER_LEN ] = {}; PLGMPMemory m_pointerShapeMemory[POINTER_SHAPE_BUFFERS] = {}; diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 9f17e34e..a2c36b3d 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -29,6 +29,7 @@ + @@ -55,6 +56,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index 1bcf7aca..ee0c33c8 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -82,6 +82,9 @@ Header Files + + Header Files + Header Files @@ -122,6 +125,9 @@ Source Files + + Source Files + Source Files