vdi-cloud/terminal/moonlight/patches/0003-vdi-input-priority.patch

180 lines
7.0 KiB
Diff

--- a/app/streaming/session.cpp
+++ b/app/streaming/session.cpp
@@ -39,6 +39,7 @@
#include <QImage>
#include <QGuiApplication>
#include <QCursor>
+#include "input/vdi_input_priority.h"
#include <QScreen>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
@@ -1962,8 +1963,14 @@
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
+ VdiInputPriority vdiInputPriority(qEnvironmentVariableIntValue("VDI_INPUT_PRIORITY") == 1,
+ SDL_CODE_FRAME_READY);
+ VdiInputTiming vdiInputTiming(qEnvironmentVariableIntValue("VDI_INPUT_TIMING") == 1);
+ SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "VDI input priority: %s",
+ qEnvironmentVariableIntValue("VDI_INPUT_PRIORITY") == 1 ? "enabled" : "disabled");
SDL_Event event;
for (;;) {
+ if (!vdiInputPriority.takeDeferred(event, SDL_GetWindowID(m_Window))) {
#if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(STEAM_LINK)
// SDL 2.0.18 has a proper wait event implementation that uses platform
// support to block on events rather than polling on Windows, macOS, X11,
@@ -1995,6 +2002,11 @@
continue;
}
#endif
+ if (vdiInputPriority.deferFrame(event)) {
+ continue;
+ }
+ }
+ vdiInputTiming.observe(event, SDL_GetWindowID(m_Window));
switch (event.type) {
case SDL_QUIT:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -2005,7 +2017,9 @@
switch (event.user.code) {
case SDL_CODE_FRAME_READY:
if (m_VideoDecoder != nullptr) {
+ const auto renderStart = vdiInputTiming.startRender();
m_VideoDecoder->renderFrameOnMainThread();
+ vdiInputTiming.endRender(renderStart);
}
break;
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
@@ -2308,6 +2322,7 @@
}
DispatchDeferredCleanup:
+ vdiInputTiming.report(vdiInputPriority.prioritized());
// Switch back to synchronous logging mode
StreamUtils::exitAsyncLoggingMode();
--- /dev/null
+++ b/app/streaming/input/vdi_input_priority.h
@@ -0,0 +1,121 @@
+#pragma once
+#include <SDL.h>
+#include <array>
+#include <algorithm>
+#include <cstdint>
+
+// SDL main thread only. Inputs may pass one frame-ready notification, never a
+// window/focus/quit/other notification or another frame. Input FIFO is preserved.
+class VdiInputPriority {
+public:
+ VdiInputPriority(bool enabled, Sint32 frameCode)
+ : m_Enabled(enabled), m_FrameCode(frameCode) {}
+
+ bool deferFrame(const SDL_Event& event) {
+ if (!m_Enabled || event.type != SDL_USEREVENT || event.user.code != m_FrameCode)
+ return false;
+ SDL_assert(!m_Pending);
+ m_Frame = event;
+ m_Pending = true;
+ m_Count = 0;
+ m_Start = SDL_GetPerformanceCounter();
+ return true;
+ }
+
+ bool takeDeferred(SDL_Event& event, Uint32 windowId) {
+ if (!m_Pending) return false;
+ SDL_Event next;
+ const auto elapsed = SDL_GetPerformanceCounter() - m_Start;
+ if (m_Count < 32 && elapsed < SDL_GetPerformanceFrequency() / 500 &&
+ SDL_PeepEvents(&next, 1, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) == 1 &&
+ isInputForWindow(next, windowId) &&
+ SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) == 1) {
+ ++m_Count;
+ ++m_Prioritized;
+ return true;
+ }
+ event = m_Frame;
+ m_Pending = false;
+ return true;
+ }
+
+ uint64_t prioritized() const { return m_Prioritized; }
+
+ static bool isInputForWindow(const SDL_Event& event, Uint32 windowId) {
+ switch (event.type) {
+ case SDL_MOUSEMOTION:
+ return event.motion.windowID == windowId && event.motion.which != SDL_TOUCH_MOUSEID;
+ case SDL_MOUSEBUTTONDOWN:
+ case SDL_MOUSEBUTTONUP:
+ return event.button.windowID == windowId && event.button.which != SDL_TOUCH_MOUSEID;
+ case SDL_MOUSEWHEEL:
+ return event.wheel.windowID == windowId && event.wheel.which != SDL_TOUCH_MOUSEID;
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ return event.key.windowID == windowId;
+ default:
+ return false;
+ }
+ }
+
+private:
+ bool m_Enabled;
+ Sint32 m_FrameCode;
+ bool m_Pending = false;
+ SDL_Event m_Frame{};
+ unsigned m_Count = 0;
+ Uint64 m_Start = 0;
+ uint64_t m_Prioritized = 0;
+};
+
+// Aggregated only when explicitly requested. Ages are SDL queue/dispatch ages,
+// not network RTT or physical input-to-photon latency. 0.1 ms histogram bins.
+class VdiInputTiming {
+public:
+ explicit VdiInputTiming(bool enabled) : m_Enabled(enabled) {}
+ void observe(const SDL_Event& event, Uint32 windowId) {
+ if (!m_Enabled || !event.common.timestamp ||
+ !VdiInputPriority::isInputForWindow(event, windowId)) return;
+ const Uint32 age = SDL_GetTicks() - event.common.timestamp;
+ // Ignore invalid/future synthetic timestamps; subtraction handles wrap.
+ if (age < 60000) m_Input.add(Uint64(age) * 10);
+ }
+ Uint64 startRender() const { return m_Enabled ? SDL_GetPerformanceCounter() : 0; }
+ void endRender(Uint64 start) {
+ if (m_Enabled) m_Render.add((SDL_GetPerformanceCounter() - start) * 10000 / SDL_GetPerformanceFrequency());
+ }
+ void report(uint64_t prioritized) const {
+ if (!m_Enabled) return;
+ SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
+ "VDI input timing: SDL dispatch age only; prioritized=%llu",
+ (unsigned long long)prioritized);
+ m_Input.report("input dispatch age");
+ m_Render.report("main-thread render call");
+ }
+private:
+ struct Histogram {
+ std::array<uint64_t, 1024> bins{};
+ uint64_t count = 0, total = 0, max = 0;
+ void add(uint64_t tenths) {
+ ++bins[std::min<uint64_t>(tenths, bins.size()-1)];
+ ++count; total += tenths; max = std::max(max, tenths);
+ }
+ double quantile(unsigned percent) const {
+ const uint64_t rank = (count * percent + 99) / 100;
+ uint64_t accumulated = 0;
+ for (size_t i = 0; i < bins.size(); ++i) {
+ accumulated += bins[i];
+ if (accumulated >= rank) return i / 10.0;
+ }
+ return 0;
+ }
+ void report(const char* label) const {
+ SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
+ "VDI %s: samples=%llu avg=%.2f p50=%.1f p95=%.1f max=%.1f ms (percentiles capped at 102.3ms)",
+ label, (unsigned long long)count, count ? total / (10.0*count) : 0,
+ count ? quantile(50) : 0, count ? quantile(95) : 0, max / 10.0);
+ }
+ };
+ bool m_Enabled;
+ Histogram m_Input, m_Render;
+};