--- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -39,6 +39,7 @@ #include #include #include +#include "input/vdi_input_priority.h" #include #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 +#include +#include +#include + +// 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 bins{}; + uint64_t count = 0, total = 0, max = 0; + void add(uint64_t tenths) { + ++bins[std::min(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; +};