From 55e87d20bf6bf7d279101b40bb19a5665d92af88 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 21:18:03 +0200 Subject: [PATCH] Verify generation changes and stale ACK isolation after restart --- tools/chatgui/src/call_video_engine.cpp | 2 +- tools/chatgui/src/call_video_wake.h | 13 +++++++------ tools/chatgui/tests/test_call_video_clock.cpp | 13 +++++++++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tools/chatgui/src/call_video_engine.cpp b/tools/chatgui/src/call_video_engine.cpp index 6d4d58a6..70b9a3d5 100644 --- a/tools/chatgui/src/call_video_engine.cpp +++ b/tools/chatgui/src/call_video_engine.cpp @@ -28,7 +28,7 @@ struct CallVideoEngine::Impl { std::mutex lock; std::condition_variable wake; CallVideoWake rxWake; - CallVideoWake displayWake, guiWake; + CallVideoWake displayWake, guiWake{false}; QObject* guiNotifier = nullptr; struct Picture { call_video_playout_frame timing; QImage image; }; CallVideoQueue pictures; diff --git a/tools/chatgui/src/call_video_wake.h b/tools/chatgui/src/call_video_wake.h index dceef37d..44f43d05 100644 --- a/tools/chatgui/src/call_video_wake.h +++ b/tools/chatgui/src/call_video_wake.h @@ -19,24 +19,25 @@ class CallVideoWake { std::atomic serial{0}; std::atomic error{0}; + bool timerRequired; #ifdef _WIN32 HANDLE event = nullptr, timer = nullptr; #else int pipefd[2] = {-1, -1}; #endif public: - CallVideoWake() { + explicit CallVideoWake(bool withTimer = true) : timerRequired(withTimer) { #ifdef _WIN32 event = CreateEventW(nullptr, FALSE, FALSE, nullptr); constexpr DWORD highResolution = 0x00000002; // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION (Windows 10 1803+). - timer = CreateWaitableTimerExW(nullptr, nullptr, highResolution, TIMER_MODIFY_STATE | SYNCHRONIZE); - if (!timer) { + if (timerRequired) timer = CreateWaitableTimerExW(nullptr, nullptr, highResolution, TIMER_MODIFY_STATE | SYNCHRONIZE); + if (timerRequired && !timer) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "video high resolution timer unavailable error=%lu; using regular timer", GetLastError()); timer = CreateWaitableTimerW(nullptr, FALSE, nullptr); } - if (!event || !timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video wait handles unavailable event=%p timer=%p error=%lu", + if (!event || (timerRequired && !timer)) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video wait handles unavailable event=%p timer=%p error=%lu", event, timer, GetLastError()); - else DEBUG_INFO(DEBUG_CATEGORY_CALL, "video RX waitable timer ready event=%p timer=%p", event, timer); + else DEBUG_INFO(DEBUG_CATEGORY_CALL, "video wake ready event=%p timer=%p timed=%d", event, timer, timerRequired); #else if (pipe(pipefd) || fcntl(pipefd[0], F_SETFL, O_NONBLOCK) < 0 || fcntl(pipefd[1], F_SETFL, O_NONBLOCK) < 0 || fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) < 0 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) < 0) { @@ -55,7 +56,7 @@ public: } bool valid() const { #ifdef _WIN32 - return event && timer; + return event && (!timerRequired || timer); #else return pipefd[0] >= 0 && pipefd[0] < FD_SETSIZE; #endif diff --git a/tools/chatgui/tests/test_call_video_clock.cpp b/tools/chatgui/tests/test_call_video_clock.cpp index 8d8d99d2..50d55443 100644 --- a/tools/chatgui/tests/test_call_video_clock.cpp +++ b/tools/chatgui/tests/test_call_video_clock.cpp @@ -70,6 +70,7 @@ extern "C" int __wrap_call_video_port_pull(uint64_t id, call_video_frame* out) { // Кадр уже пришёл: остановка RX worker не должна подменить время доступности данных. if (disturbed && cursor == 60) QThread::msleep(80); const auto& f = frames[cursor.load()]; *out = {}; out->call_id = id; out->format = format; + if (cursor >= 45) ++out->format.generation; 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)); @@ -112,12 +113,13 @@ int main(int argc, char** argv) { QImage image(640, 360, QImage::Format_RGB32); image.fill(Qt::blue); for (int i = 0; i < 90; ++i) { QList output; CallVideoRateCompletion completion; - if (!encoder.encode(image, 1000000 + i * 33333ll, i == 0, output, completion) || output.size() != 1) return 3; + if (!encoder.encode(image, 1000000 + i * 33333ll, i == 0 || i == 45, output, completion) || output.size() != 1) return 3; frames.push_back(output.front()); } CallVideoEngine engine; engine.setPresentationDriven(true); std::vector published, presented; + quint64 lastSerial = 0; QObject::connect(&engine, &CallVideoEngine::imageChanged, [&](const QImage& frame, quint64 serial) { - if (!frame.isNull()) published.push_back(nowUs()); + if (!frame.isNull()) { published.push_back(nowUs()); lastSerial = serial; } if (holdAck) return; // Подтверждение потребления отдельно от доставки signal, как paintEvent окна. QTimer::singleShot(5, &engine, [&, serial, video = !frame.isNull()] { @@ -156,6 +158,13 @@ int main(int argc, char** argv) { } const int64_t stopAt = nowUs(); engine.stop(); app.processEvents(); if (nowUs() - stopAt > 250000 || unsafeTimer) return 1; + const quint64 oldSerial = lastSerial; + cursor = 0; published.clear(); displayThread = 0; start = nowUs(); engine.start(7); stalled.restart(); + while (stalled.elapsed() < 250) { app.processEvents(); engine.imagePresented(oldSerial); QThread::msleep(1); } + if (published.size() != 1 || cursor != 3 || lastSerial == oldSerial) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video stale ACK consumed restarted slot published=%zu inputs=%d", published.size(), cursor.load()); return 1; + } + engine.stop(); app.processEvents(); DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS stop without GUI ACK: one selected + two ready, decoder and port never run on display thread"); return 0; }