From ecdba8fb6363fa5187a9ad932d9c3a94c744e454 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 19:16:17 +0200 Subject: [PATCH] Measure presentation cadence and stop latency while preserving unaccepted GPU KEY on fallback --- tools/chatgui/src/call_video_engine.cpp | 6 ++- tools/chatgui/tests/test_call_video_clock.cpp | 38 +++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/tools/chatgui/src/call_video_engine.cpp b/tools/chatgui/src/call_video_engine.cpp index bf36594e..377b6b46 100644 --- a/tools/chatgui/src/call_video_engine.cpp +++ b/tools/chatgui/src/call_video_engine.cpp @@ -443,7 +443,11 @@ void CallVideoEngine::receive() { if (result > 0) { ++submitted; waitingKey = false; } else { m->report(decoder.error()); - if (decoder.hardware()) decoder.useSoftware(); + if (decoder.hardware()) { + decoder.useSoftware(); waitingKey = true; + // Не принятый KEY остаётся у worker: CPU повторяет тот же кадр без потери. + if (input.flags & CALL_VIDEO_KEY) { requestedWake = 0; continue; } + } else { rejectedGeneration = input.format.generation; call_video_port_decoded(m->id, rejectedGeneration, 0); decoder.close(); diff --git a/tools/chatgui/tests/test_call_video_clock.cpp b/tools/chatgui/tests/test_call_video_clock.cpp index 014f06f4..c4eca25d 100644 --- a/tools/chatgui/tests/test_call_video_clock.cpp +++ b/tools/chatgui/tests/test_call_video_clock.cpp @@ -17,7 +17,7 @@ extern "C" { static std::vector frames; static call_video_format format{1, 640, 360, 30, 0, 1, 31, 4000000, 2}; static std::atomic requests{0}, rejects{0}; -static int cursor = 0; +static std::atomic cursor{0}; static int64_t start; static bool disturbed = false; static int64_t nowUs() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } @@ -27,7 +27,16 @@ extern "C" int __wrap_call_audio_get_clock(uint64_t, int64_t now, call_clock_sna } extern "C" int __wrap_call_video_port_open(uint64_t, const call_video_caps*) { return 0; } extern "C" void __wrap_call_video_port_close(uint64_t) {} -extern "C" int __wrap_call_video_port_status(uint64_t, struct call_video_port_status* out) { *out = {}; return 0; } +extern "C" int __wrap_call_video_port_status(uint64_t, struct call_video_port_status* out) { + *out = {}; + unsigned available = 0; + for (unsigned i = cursor.load(); i < frames.size(); ++i) { + const int64_t received = start + i * 33333ll + (disturbed && i == 30 ? 20000 : 0); + if (received > nowUs()) break; + ++available; + } + out->rx_frames = available; return 0; +} extern "C" void __wrap_call_video_port_request_key(uint64_t) { ++requests; } extern "C" int __wrap_call_video_port_decoded(uint64_t, uint64_t, int accepted) { if (!accepted) ++rejects; return 0; } extern "C" int __wrap_call_video_port_pull(uint64_t id, call_video_frame* out) { @@ -35,7 +44,7 @@ extern "C" int __wrap_call_video_port_pull(uint64_t id, call_video_frame* out) { if (cursor >= (int)frames.size() || nowUs() < received) return 0; // Кадр уже пришёл: остановка RX worker не должна подменить время доступности данных. if (disturbed && cursor == 60) QThread::msleep(80); - const auto& f = frames[cursor]; *out = {}; out->call_id = id; out->format = format; + const auto& f = frames[cursor.load()]; *out = {}; out->call_id = id; out->format = format; out->pts_us = f.pts; out->frame_id = ++cursor; out->flags = f.key ? CALL_VIDEO_KEY : 0; out->received_us = received; out->size = out->length = f.data.size(); auto* data = static_cast(u_malloc(out->size)); @@ -51,29 +60,36 @@ int main(int argc, char** argv) { if (!encoder.encode(image, 1000000 + i * 33333ll, i == 0, output, completion) || output.size() != 1) return 3; frames.push_back(output.front()); } - CallVideoEngine engine; engine.setPresentationDriven(true); std::vector published; + CallVideoEngine engine; engine.setPresentationDriven(true); std::vector published, presented; QObject::connect(&engine, &CallVideoEngine::imageChanged, [&](const QImage& frame, quint64 serial) { if (!frame.isNull()) published.push_back(nowUs()); // Подтверждение потребления отдельно от доставки signal, как paintEvent окна. - QTimer::singleShot(5, &engine, [&engine, serial] { engine.imagePresented(serial); }); + QTimer::singleShot(5, &engine, [&, serial, video = !frame.isNull()] { + if (video) presented.push_back(nowUs()); + engine.imagePresented(serial); + }); }); for (int scenario = 0; scenario < 2; ++scenario) { - disturbed = scenario != 0; cursor = 0; published.clear(); + disturbed = scenario != 0; cursor = 0; published.clear(); presented.clear(); start = nowUs(); engine.start(7); QElapsedTimer timer; timer.start(); bool uiPaused = false; while (timer.elapsed() < 4000) { if (disturbed && !uiPaused && timer.elapsed() >= 1500) { QThread::msleep(60); uiPaused = true; } app.processEvents(); QThread::msleep(2); } - engine.stop(); const auto stoppedCount = published.size(); app.processEvents(); + const int64_t stopAt = nowUs(); engine.stop(); const int64_t stopUs = nowUs() - stopAt; + const auto stoppedCount = published.size(); app.processEvents(); if (published.size() != stoppedCount) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "stale video publication after stop"); return 1; } std::vector gaps; - for (size_t i = 1; i < published.size(); ++i) gaps.push_back(published[i] - published[i - 1]); + for (size_t i = 1; i < presented.size(); ++i) gaps.push_back(presented[i] - presented[i - 1]); std::sort(gaps.begin(), gaps.end()); DEBUG_INFO(DEBUG_CATEGORY_CALL, "CLOCK PROBE disturbed=%d shown=%zu/90 key_requests=%d decoder_rejections=%d " - "gap_p50_us=%lld gap_p95_us=%lld gap_max_us=%lld audio_epochs=32", disturbed, published.size(), requests.load(), rejects.load(), + "gap_p50_us=%lld gap_p95_us=%lld gap_max_us=%lld audio_epochs=32 stop_us=%lld", disturbed, presented.size(), requests.load(), rejects.load(), (long long)(gaps.empty() ? 0 : gaps[gaps.size() / 2]), (long long)(gaps.empty() ? 0 : gaps[gaps.size() * 95 / 100]), - (long long)(gaps.empty() ? 0 : gaps.back())); - if (published.size() != frames.size() || requests || rejects) return 1; + (long long)(gaps.empty() ? 0 : gaps.back()), (long long)stopUs); + if (published.size() != frames.size() || presented.size() != frames.size() || requests || rejects || stopUs > 250000) return 1; + if (!disturbed && (gaps.empty() || gaps[gaps.size() * 95 / 100] > 45000)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "steady video presentation jitter exceeds 45ms at 30fps"); return 1; + } } return 0; }