diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index f9539230..57d4c792 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -162,6 +162,7 @@ set(SOURCES src/gl_dynprocs.c src/egl_dynprocs.c src/eglutil.c + src/frame_scheduler.c src/overlay_utils.c src/render_queue.c src/evdev.c diff --git a/client/displayservers/Wayland/presentation.c b/client/displayservers/Wayland/presentation.c index 22cd658e..44bd56a3 100644 --- a/client/displayservers/Wayland/presentation.c +++ b/client/displayservers/Wayland/presentation.c @@ -56,6 +56,8 @@ static void presentationFeedbackPresented(void * opaque, uint32_t tvNsec, uint32_t refresh, uint32_t seqHi, uint32_t seqLo, uint32_t flags) { struct FrameData * data = opaque; + if (refresh) + atomic_store(&wlWm.presentationPeriod, refresh); struct timespec present = { .tv_sec = (uint64_t) tvSecHi << 32 | tvSecLo, .tv_nsec = tvNsec, @@ -69,6 +71,16 @@ static void presentationFeedbackPresented(void * opaque, wp_presentation_feedback_destroy(feedback); } +bool waylandGetFramePeriod(uint64_t * period) +{ + const uint64_t value = atomic_load(&wlWm.presentationPeriod); + if (!value) + return false; + + *period = value; + return true; +} + static void presentationFeedbackDiscarded(void * data, struct wp_presentation_feedback * feedback) { diff --git a/client/displayservers/Wayland/wayland.c b/client/displayservers/Wayland/wayland.c index 0a7d5201..500b4ffd 100644 --- a/client/displayservers/Wayland/wayland.c +++ b/client/displayservers/Wayland/wayland.c @@ -314,6 +314,7 @@ struct LG_DisplayServerOps LGDS_Wayland = .waitFrame = waylandWaitFrame, .skipFrame = waylandSkipFrame, .stopWaitFrame = waylandStopWaitFrame, + .getFramePeriod = waylandGetFramePeriod, .guestPointerUpdated = waylandGuestPointerUpdated, .setPointer = waylandSetPointer, .grabPointer = waylandGrabPointer, diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index 2be2eba3..88ae6906 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -158,6 +158,7 @@ struct WaylandDSState struct wp_presentation * presentation; clockid_t clkId; + _Atomic(uint64_t) presentationPeriod; RingBuffer photonTimings; GraphHandle photonGraph; @@ -399,6 +400,7 @@ bool waylandPollUnregister(int fd); bool waylandPresentationInit(void); void waylandPresentationFrame(void); void waylandPresentationFree(void); +bool waylandGetFramePeriod(uint64_t * period); // registry module bool waylandRegistryInit(void); diff --git a/client/displayservers/X11/x11.c b/client/displayservers/X11/x11.c index 1bdff6c3..3b1dfd14 100644 --- a/client/displayservers/X11/x11.c +++ b/client/displayservers/X11/x11.c @@ -1487,6 +1487,15 @@ static void x11XPresentEvent(XGenericEventCookie *cookie) case PresentCompleteNotify: { XPresentCompleteNotifyEvent * e = cookie->data; + const uint64_t previousMsc = atomic_load(&x11.presentMsc); + const uint64_t previousUst = atomic_load(&x11.presentUst); + if (previousMsc && e->msc > previousMsc && e->ust > previousUst) + { + const uint64_t period = + (e->ust - previousUst) * 1000 / (e->msc - previousMsc); + if (period) + atomic_store(&x11.presentPeriod, period); + } x11DoPresent(e->msc); atomic_store(&x11.presentMsc, e->msc); atomic_store(&x11.presentUst, e->ust); @@ -1496,6 +1505,16 @@ static void x11XPresentEvent(XGenericEventCookie *cookie) } } +static bool x11GetFramePeriod(uint64_t * period) +{ + const uint64_t value = atomic_load(&x11.presentPeriod); + if (!value) + return false; + + *period = value; + return true; +} + #ifdef ENABLE_EGL static EGLDisplay x11GetEGLDisplay(void) { @@ -2025,6 +2044,7 @@ struct LG_DisplayServerOps LGDS_X11 = #endif .waitFrame = x11WaitFrame, .stopWaitFrame = x11StopWaitFrame, + .getFramePeriod = x11GetFramePeriod, .guestPointerUpdated = x11GuestPointerUpdated, .setPointer = x11SetPointer, .grabPointer = x11GrabPointer, diff --git a/client/displayservers/X11/x11.h b/client/displayservers/X11/x11.h index 5e64211b..8a73672c 100644 --- a/client/displayservers/X11/x11.h +++ b/client/displayservers/X11/x11.h @@ -75,6 +75,7 @@ struct X11DSState int xpresentOp; bool jitRender; _Atomic(uint64_t) presentMsc, presentUst; + _Atomic(uint64_t) presentPeriod; uint32_t presentSerial; Pixmap presentPixmap; XserverRegion presentRegion; diff --git a/client/include/interface/displayserver.h b/client/include/interface/displayserver.h index 650c0c18..83a026c7 100644 --- a/client/include/interface/displayserver.h +++ b/client/include/interface/displayserver.h @@ -196,6 +196,9 @@ struct LG_DisplayServerOps /* This is used to interrupt waitFrame. */ void (*stopWaitFrame)(void); + /* Returns the current presentation period in nanoseconds when known. */ + bool (*getFramePeriod)(uint64_t * period); + /* dm specific cursor implementations */ void (*guestPointerUpdated)(double x, double y, double localX, double localY); void (*setPointer)(LG_DSPointer pointer); diff --git a/client/src/frame_scheduler.c b/client/src/frame_scheduler.c new file mode 100644 index 00000000..0725e890 --- /dev/null +++ b/client/src/frame_scheduler.c @@ -0,0 +1,251 @@ +/** + * 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 "main.h" + +#include "common/debug.h" +#include "common/locking.h" +#include "common/time.h" + +#include +#include +#include + +#define FRAME_SCHEDULER_LEASE_MS 1000U +#define FRAME_SCHEDULER_RENEW_NS 250000000ULL +#define FRAME_SCHEDULER_TARGET_SLACK_NS 500000ULL +#define FRAME_SCHEDULER_MIN_PERIOD_NS 2000000ULL +#define FRAME_SCHEDULER_MAX_PERIOD_NS 1000000000ULL +#define FRAME_SCHEDULER_SAMPLE_COUNT 9U + +static struct +{ + LG_Lock lock; + + bool supported; + bool active; + bool controlPending; + uint32_t generation; + uint64_t period; + uint64_t lastSend; + uint64_t lastCadence; + + LG_TransportControlToken controlToken; + + uint64_t renderSamples[FRAME_SCHEDULER_SAMPLE_COUNT]; + unsigned renderSampleIndex; + unsigned renderSampleCount; + uint64_t lastRender; +} +l_frameScheduler; + +static int compareU64(const void * a, const void * b) +{ + const uint64_t lhs = *(const uint64_t *)a; + const uint64_t rhs = *(const uint64_t *)b; + return (lhs > rhs) - (lhs < rhs); +} + +static uint64_t fallbackPeriod(void) +{ + uint64_t samples[FRAME_SCHEDULER_SAMPLE_COUNT]; + unsigned count; + + LG_LOCK(l_frameScheduler.lock); + count = l_frameScheduler.renderSampleCount; + memcpy(samples, l_frameScheduler.renderSamples, + count * sizeof(*samples)); + LG_UNLOCK(l_frameScheduler.lock); + + if (count < 5) + return 0; + + qsort(samples, count, sizeof(*samples), compareU64); + return samples[count / 2]; +} + +static uint64_t presentationPeriod(void) +{ + uint64_t period = 0; + if (g_state.ds->getFramePeriod && + g_state.ds->getFramePeriod(&period)) + return period; + + return g_state.jitRender ? fallbackPeriod() : 0; +} + +static bool controlReady(void) +{ + if (!l_frameScheduler.controlPending) + return true; + + const LG_TransportStatus status = g_state.transportOps->controlStatus( + g_state.transport, l_frameScheduler.controlToken); + if (status == LG_TRANSPORT_UNAVAILABLE) + return false; + + l_frameScheduler.controlPending = false; + return status == LG_TRANSPORT_OK; +} + +static bool sendSchedule(LG_TransportFrameScheduleFlags flags, + uint64_t period) +{ + if (!controlReady()) + return false; + + const LG_TransportControl control = { + .type = LG_TRANSPORT_CONTROL_FRAME_SCHEDULE, + .frameSchedule = { + .generation = l_frameScheduler.generation, + .flags = flags, + .period = period, + .targetSlack = FRAME_SCHEDULER_TARGET_SLACK_NS, + .lease = FRAME_SCHEDULER_LEASE_MS, + }, + }; + + const LG_TransportStatus status = g_state.transportOps->sendControl( + g_state.transport, &control, &l_frameScheduler.controlToken); + if (status == LG_TRANSPORT_UNAVAILABLE) + return false; + if (status != LG_TRANSPORT_OK) + { + DEBUG_WARN("Frame-schedule control failed with status %d", status); + return false; + } + + l_frameScheduler.controlPending = true; + return true; +} + +void frameScheduler_init(void) +{ + memset(&l_frameScheduler, 0, sizeof(l_frameScheduler)); + LG_LOCK_INIT(l_frameScheduler.lock); +} + +void frameScheduler_free(void) +{ + LG_LOCK_FREE(l_frameScheduler.lock); +} + +void frameScheduler_start(LG_TransportFeatureFlags features) +{ + l_frameScheduler.supported = + features & LG_TRANSPORT_FEATURE_FRAME_SCHEDULE; + l_frameScheduler.active = false; + l_frameScheduler.controlPending = false; + l_frameScheduler.period = 0; + l_frameScheduler.lastSend = 0; + l_frameScheduler.lastCadence = 0; + ++l_frameScheduler.generation; + + LG_LOCK(l_frameScheduler.lock); + l_frameScheduler.renderSampleIndex = 0; + l_frameScheduler.renderSampleCount = 0; + l_frameScheduler.lastRender = 0; + LG_UNLOCK(l_frameScheduler.lock); +} + +void frameScheduler_stop(void) +{ + if (l_frameScheduler.supported && l_frameScheduler.active) + sendSchedule(LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0); + + l_frameScheduler.supported = false; + l_frameScheduler.active = false; + l_frameScheduler.controlPending = false; +} + +void frameScheduler_update(void) +{ + if (!l_frameScheduler.supported) + return; + + const uint64_t now = nanotime(); + const uint64_t period = presentationPeriod(); + if (period < FRAME_SCHEDULER_MIN_PERIOD_NS || + period > FRAME_SCHEDULER_MAX_PERIOD_NS) + { + if (l_frameScheduler.active && l_frameScheduler.lastCadence && + now - l_frameScheduler.lastCadence > FRAME_SCHEDULER_RENEW_NS * 2) + { + if (sendSchedule(LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0)) + l_frameScheduler.active = false; + } + return; + } + + l_frameScheduler.lastCadence = now; + bool reset = !l_frameScheduler.period; + if (!reset) + { + const uint64_t delta = l_frameScheduler.period > period ? + l_frameScheduler.period - period : period - l_frameScheduler.period; + reset = delta > l_frameScheduler.period / 20; + } + + if (reset) + { + l_frameScheduler.period = period; + ++l_frameScheduler.generation; + } + else + l_frameScheduler.period = + (l_frameScheduler.period * 7 + period) / 8; + + if (l_frameScheduler.active && !reset && + now - l_frameScheduler.lastSend < FRAME_SCHEDULER_RENEW_NS) + return; + + LG_TransportFrameScheduleFlags flags = + LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE; + if (reset) + flags |= LG_TRANSPORT_FRAME_SCHEDULE_RESET; + + if (sendSchedule(flags, l_frameScheduler.period)) + { + l_frameScheduler.active = true; + l_frameScheduler.lastSend = now; + } +} + +void frameScheduler_observeRender(uint64_t timestamp) +{ + if (!g_state.jitRender) + return; + + LG_LOCK(l_frameScheduler.lock); + if (l_frameScheduler.lastRender && + timestamp > l_frameScheduler.lastRender) + { + l_frameScheduler.renderSamples[l_frameScheduler.renderSampleIndex] = + timestamp - l_frameScheduler.lastRender; + l_frameScheduler.renderSampleIndex = + (l_frameScheduler.renderSampleIndex + 1) % + FRAME_SCHEDULER_SAMPLE_COUNT; + if (l_frameScheduler.renderSampleCount < FRAME_SCHEDULER_SAMPLE_COUNT) + ++l_frameScheduler.renderSampleCount; + } + l_frameScheduler.lastRender = timestamp; + LG_UNLOCK(l_frameScheduler.lock); +} diff --git a/client/src/frame_scheduler.h b/client/src/frame_scheduler.h new file mode 100644 index 00000000..800a0264 --- /dev/null +++ b/client/src/frame_scheduler.h @@ -0,0 +1,35 @@ +/** + * 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 _H_LG_FRAME_SCHEDULER_ +#define _H_LG_FRAME_SCHEDULER_ + +#include + +#include "interface/transport.h" + +void frameScheduler_init(void); +void frameScheduler_free(void); +void frameScheduler_start(LG_TransportFeatureFlags features); +void frameScheduler_stop(void); +void frameScheduler_update(void); +void frameScheduler_observeRender(uint64_t timestamp); + +#endif diff --git a/client/src/main.c b/client/src/main.c index 2f56f7ea..cf2152af 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -68,6 +68,7 @@ #include "util.h" #include "render_queue.h" #include "evdev.h" +#include "frame_scheduler.h" #ifdef ENABLE_TESTS #include "interface/test_capture.h" @@ -698,6 +699,7 @@ static int renderThread(void * unused) const uint64_t delta = t - g_state.lastRenderTime; g_state.lastRenderTime = t; + frameScheduler_observeRender(t); atomic_fetch_add_explicit(&g_state.renderCount, 1, memory_order_relaxed); if (!g_state.jitRender && g_params.fpsMin != 0) @@ -1689,6 +1691,7 @@ static int lg_run(void) //setup the render command queue renderQueue_init(); + frameScheduler_init(); const PSInit psInit = { @@ -1996,6 +1999,7 @@ restart: if (session.uuidValid) memcpy(g_state.guestUUID, session.uuid, sizeof(g_state.guestUUID)); g_state.transportFeatures = session.features; + frameScheduler_start(session.features); if (g_state.spiceReady && g_params.useSpiceInput) keybind_spiceRegister(); @@ -2020,9 +2024,12 @@ restart: break; } lgMessage_process(); + frameScheduler_update(); g_state.ds->wait(100); } + frameScheduler_stop(); + if (app_getState() == APP_STATE_RESTART) { lgSignalEvent(e_startup); @@ -2043,6 +2050,7 @@ restart: static void lg_shutdown(void) { app_setState(APP_STATE_SHUTDOWN); + frameScheduler_stop(); if (t_spice) lgJoinThread(t_spice, NULL); @@ -2093,6 +2101,7 @@ static void lg_shutdown(void) g_state.ds->free(); renderQueue_free(); + frameScheduler_free(); // free metrics ringbuffers ringbuffer_free(&g_state.renderTimings);