[plasma/krdp] src: VideoStream: size the in-flight window from the measured producer frame rate
Nate Graham <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit b1353b8899760b1e329017568b96be37a922aa3d by Nate Graham, on behalf of Shouvik Kar.
Committed on 06/08/2026 at 13:37.
Pushed by ngraham into branch 'master'.
VideoStream: size the in-flight window from the measured producer frame rate
The window was sized from requestedFrameRate. When the source delivers fewer
frames than requested, that over-sizes the window and buffers more latency than
needed. Size maxInFlight from the measured rate of frames entering krdp instead,
smoothed with an EWMA and clamped to a bootstrap floor and the requested rate.
The rate is sampled from the source/encoder receive callbacks, upstream of the
send window, so it cannot feed back into the window it controls; it holds its
last value through idle so a burst after idle is not initially throttled.
M +54 -5 src/VideoStream.cpp
M +1 -0 src/VideoStream.h
https://invent.kde.org/plasma/krdp/-/commit/b1353b8899760b1e329017568b96be37a922aa3d
diff --git a/src/VideoStream.cpp b/src/VideoStream.cpp
index b22fc3f..ae95164 100644
--- a/src/VideoStream.cpp
+++ b/src/VideoStream.cpp
@@ -13,6 +13,7 @@
#include <atomic>
#include <cmath>
#include <condition_variable>
+#include <cstdint>
#include <QDateTime>
#include <QQueue>
@@ -40,6 +41,8 @@ namespace clk = std::chrono;
constexpr qsizetype MaximumInFlightFrames = 2; // in-flight window floor
constexpr double InFlightGain = 1.0; // window spans this many round trips of frames
constexpr double LatencyBudgetSec = 1.0; // never buffer more than this many seconds of video
+constexpr double MinimumWindowFrameRate = 5.0; // floor for producer-rate window sizing (avoids stop-and-wait)
+constexpr double ProducerFpsEwmaAlpha = 0.25; // smoothing for the producer-rate estimate
constexpr uint32_t ProgressiveCodecContextId = 1;
struct RdpCapsInformation {
uint32_t version;
@@ -150,6 +153,12 @@ public:
std::atomic_int requestedFrameRate = 60;
std::atomic<qsizetype> maxInFlight{MaximumInFlightFrames}; // recomputed from RTT on rttChanged
+ // Producer-rate estimate for window sizing (see VideoStream::effectiveProducerFps()).
+ std::atomic<uint64_t> producedFrames = 0; // frames entering krdp; written from frame callbacks
+ uint64_t lastProducedFrames = 0; // touched only by updateInFlightWindow()
+ clk::steady_clock::time_point lastProducerRateUpdate{}; // touched only by updateInFlightWindow()
+ double smoothedProducerFps = MinimumWindowFrameRate; // touched only by updateInFlightWindow()
+
bool initialized = false;
quint8 quality = 100;
@@ -427,6 +436,7 @@ void VideoStream::queueFrame(const KRdp::VideoFrame &frame)
if (d->session->state() != RdpConnection::State::Streaming || !d->enabled) {
return;
}
+ d->producedFrames.fetch_add(1, std::memory_order_relaxed); // count only accepted frames
std::lock_guard lock(d->frameQueueMutex);
if (d->activeEncodingMode == EncodingMode::H264) {
@@ -828,17 +838,56 @@ void VideoStream::performReset(QSize size)
}
}
+double VideoStream::effectiveProducerFps()
+{
+ // Producer rate = frames entering krdp from the source/encoder callbacks. It is measured
+ // upstream of the send window, so sizing the window from it cannot feed back into itself
+ // (unlike client-decoded FPS). Invariant: producedFrames is written from frame callbacks;
+ // the smoothing state is touched only here (updateInFlightWindow() is the sole caller, one
+ // rttChanged connection, so no lock is needed). All returns are clamped to a bootstrap/
+ // stop-and-wait floor (MinimumWindowFrameRate), and to the requested rate when it is above
+ // that floor (so a very low requested rate yields the floor, not less).
+ const double requested = d->requestedFrameRate.load();
+ const double ceiling = std::max(MinimumWindowFrameRate, requested);
+ const auto clampFps = [&](double f) {
+ return std::clamp(f, MinimumWindowFrameRate, ceiling);
+ };
+
+ const auto now = clk::steady_clock::now();
+ const uint64_t total = d->producedFrames.load(std::memory_order_relaxed);
+ if (d->lastProducerRateUpdate == clk::steady_clock::time_point{}) {
+ d->lastProducerRateUpdate = now;
+ d->lastProducedFrames = total;
+ // Optimistic bootstrap: seed from the requested rate so the initial full-screen
+ // burst gets a usable window immediately; the EWMA converges down to the measured
+ // producer rate as frames arrive.
+ d->smoothedProducerFps = clampFps(requested);
+ return d->smoothedProducerFps;
+ }
+ const double elapsedSec = clk::duration<double>(now - d->lastProducerRateUpdate).count();
+ const uint64_t delta = total - d->lastProducedFrames;
+ d->lastProducerRateUpdate = now;
+ d->lastProducedFrames = total;
+ if (elapsedSec <= 0.0 || delta == 0) {
+ return clampFps(d->smoothedProducerFps); // idle: hold last active rate, do not shrink
+ }
+ const double instantFps = double(delta) / elapsedSec;
+ d->smoothedProducerFps = d->smoothedProducerFps * (1.0 - ProducerFpsEwmaAlpha) + instantFps * ProducerFpsEwmaAlpha;
+ return clampFps(d->smoothedProducerFps);
+}
+
void VideoStream::updateInFlightWindow()
{
- // Size the in-flight window from the bandwidth-delay product: at the current source
- // rate, how many frames fit within one base round trip. NetworkDetection publishes the
- // RTT via atomics, so averageRTT() is safe to read here; hasInFlightCapacity() then
- // just reads the cached maxInFlight. Falls back to the fixed floor until an RTT is known.
+ // Size the in-flight window from the bandwidth-delay product: how many produced frames
+ // fit within the current average round-trip time. Producer FPS is measured from frames
+ // entering krdp (not client-decoded frames); averageRTT() is published via atomics; both
+ // are safe to read here. hasInFlightCapacity() then reads the cached maxInFlight. Falls
+ // back to the fixed floor until an RTT is known.
+ const double fps = effectiveProducerFps();
const auto rttMs = clk::duration_cast<clk::milliseconds>(d->session->networkDetection()->averageRTT()).count();
qsizetype window = MaximumInFlightFrames;
if (rttMs > 0 && rttMs < 60000) {
const double rttSec = rttMs / 1000.0;
- const int fps = d->requestedFrameRate.load();
const qsizetype bdp = qsizetype(std::ceil(fps * rttSec * InFlightGain));
const qsizetype cap = std::max<qsizetype>(MaximumInFlightFrames, qsizetype(std::ceil(fps * LatencyBudgetSec)));
window = std::clamp(bdp, qsizetype(MaximumInFlightFrames), cap);
diff --git a/src/VideoStream.h b/src/VideoStream.h
index 285029b..afc13d3 100644
--- a/src/VideoStream.h
+++ b/src/VideoStream.h
@@ -109,6 +109,7 @@ private:
void sendFrameProgressive(const VideoFrame &frame);
void updateInFlightWindow();
+ double effectiveProducerFps();
class Private;
const std::unique_ptr<Private> d;