diff --git a/doc/call_video.txt b/doc/call_video.txt index 7c26ade9..8da9df96 100644 --- a/doc/call_video.txt +++ b/doc/call_video.txt @@ -1144,3 +1144,52 @@ decode_deadline_stage=input/output, work_us и margin_us описывают им тем самым влияют на расписание. Для устранения зависимости нужны независимое исполнение decoder/планировщика и синхронизация presentation с обновлением дисплея; обычный таймер и ранний ACK из QPainter не дают такой гарантии. + +Независимый desktop-планировщик, 19.10.2026 — реализовано +TX по-прежнему владеет encoder. RX worker теперь единолично владеет decoder, всеми +submit/receive/EOS/open/close, GPU transfer, RGB и rotation. Отдельный display worker +владеет call_video_playout и не вызывает decoder, port_status/pull, Qt post/invokeMethod, +не ждёт GUI и не форматирует/записывает логи. + +Передача RX -> display: SPSC кольцо из трёх неизменяемых Picture. Producer заполняет слот +до release-публикации; consumer освобождает его только после ACK выбранного кадра. +Голова во время GUI ACK остаётся в кольце и входит в его ёмкость. При полном кольце +останавливается подача decoder; до восьми входов разрешают проверенный hardware pipeline. +Старый QImage в слоте заменяет decoder, освобождение его буфера не попадает в display. +Смена поколения сначала выводит принятые decoder-входы; старые готовые изображения +сохраняют собственные PTS/формат и показываются до новых. + +GUI читает один опубликованный слот по QWinEventNotifier (Windows) или QSocketNotifier +(POSIX pipe). Таймер записывает metadata и serial, затем сигналит событие; аллокации +queued Qt lambda в нём больше нет. Только GUI копирует shared QImage и вызывает imageChanged. +ACK имеет глобально возрастающий serial; дубликаты и ACK предыдущего start не освобождают +слот новой сессии. stop запрещает публикации, будит и присоединяет display/RX/TX независимо +от GUI ACK, затем закрывает порт. Notifier удаляется до закрытия native handles. + +Общий запас: cumulative port_received - completed - decoder_discarded; переносы между +encoded/decoder/ready/GUI не меняют счётчик. port_received читается атомарно, без mutex +порта, который может быть занят копированием нового encoded кадра. Decoder-discard +меняется только при настоящем отказе цепочки/codec; короткая пауза ничего не отбрасывает. +call_audio_try_clock использует try_lock и на call_audio, и на jitter mutex. BUSY вызывает +повтор через 500мкс без сброса назначенной головы; экстраполяции устаревшего audio PTS нет. + +У display/RX отдельные wake: Windows event + high-resolution timer, POSIX nonblocking pipe +и pselect. notify не берёт пользовательский mutex. Runtime ошибки wake передаются RX +для вывода и остановки workers. Логи playout копируются в bounded SPSC очередь; форматирует +их RX. Переполнение диагностической очереди считается и явно логируется, кадры не вытесняются. + +Фаза опирается на запланированную презентацию и корректируется по ACK на error/8, +не более 1мс за кадр. Настоящая пауза более половины периода привязывает фазу к новому ACK; +минимальный плановый интервал ограничивает мгновенный burst темпом 1.6x. Большие разрывы +capture PTS не проигрываются повторно. Это не VSync: ACK текущего окна всё ещё paintEvent, +а фактическая презентация Windows-композитором требует отдельного render backend. + +Проверки: C playout — 480/480 при сетевых/CPU паузах 50/100/200/500мс, возврат темпа, +смена render latency 3->9мс без накопленного сдвига (phase=11625мкс). +Qt clock — три сценария 90/90, смена поколения на 45-м кадре, RX80/GUI60мс, audio BUSY, +decoder output+12мс, stop без ACK, запоздалый ACK после start, 100000 конкурентных публикаций. +В последнем Linux-прогоне p95 показа: обычный 34.7мс, с медленным decoder 35.2мс; +максимумы работы display в секундных окнах 26-143мкс. Это локальный замер, не Windows scanout. +Qt encoder/GPU/clock/jitter: 4/4 PASS, core и vibechat собраны. +Полный check.sh вне sandbox: 163/164; известный test_encrypted_attachment снова phase=5, +heartbeats=12001 (описан в doc/tasks.md). Лог: /tmp/video-independent-check-full.log. diff --git a/doc/tasks.md b/doc/tasks.md index 996e3687..de0a2658 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -9,6 +9,9 @@ heartbeats=12001. Отдельный повтор также остановился в phase=5: после старта автора не получены два pending incoming-запроса. Причина и связь с изменениями не установлены. Логи: /tmp/phone-busy-full-check.log, /tmp/phone-busy-encrypted-recheck.log; артефакты: /tmp/utun_encrypted_attachment_RVU2yS. + Повторилось при проверке независимого видеопланировщика: check.sh вне sandbox 163/164, + тот же phase=5/heartbeats=12001; /tmp/video-independent-check-full.log, + артефакты /tmp/utun_encrypted_attachment_IW0W28. Код вложений в этой работе не менялся. [ ] **Разобрать нестабильную загрузку storage-узлов в test_media_delivery_chat** — 19.10.2026: В том же общем прогоне storage st1/st2 не собрали файл, n2 исчерпал попытки блока 0 (15/19 внутренних проверок). diff --git a/tools/chatgui/src/call_video_engine.cpp b/tools/chatgui/src/call_video_engine.cpp index 70b9a3d5..39c9398a 100644 --- a/tools/chatgui/src/call_video_engine.cpp +++ b/tools/chatgui/src/call_video_engine.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #ifdef _WIN32 #include @@ -151,43 +150,43 @@ void CallVideoEngine::publishImage(const QImage& image, int64_t pts) { m->pendingSerial.store(++m->publicationSerial, std::memory_order_release); m->guiWake.notify(); } void CallVideoEngine::deliverImage() { - const uint64_t serial = m->pendingSerial.load(std::memory_order_acquire); - if (!m->running || !serial || serial == m->deliveredSerial) return; - m->deliveredSerial = serial; - const int64_t queued = m->publicationQueuedAt, pts = m->publicationPts; - QImage image; - if (m->publicationVideo) { - const auto* slot = m->pictures.readSlot(); - if (!slot) { - DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video GUI missing published slot serial=%llu; stopping video", (unsigned long long)serial); - stop(); return; - } - image = slot->image; + const uint64_t serial = m->pendingSerial.load(std::memory_order_acquire); + if (!m->running || !serial || serial == m->deliveredSerial) return; + m->deliveredSerial = serial; + const int64_t queued = m->publicationQueuedAt, pts = m->publicationPts; + QImage image; + if (m->publicationVideo) { + const auto* slot = m->pictures.readSlot(); + if (!slot) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video GUI missing published slot serial=%llu; stopping video", (unsigned long long)serial); + stop(); return; } - const int64_t now = nowUs(), wait = now - queued; - ++m->uiPublished; m->uiWaitMax = std::max(m->uiWaitMax, wait); - if (m->uiLastAt) m->uiGapMax = std::max(m->uiGapMax, now - m->uiLastAt); - m->uiLastAt = now; - emit imageChanged(image, serial); - const int64_t slotUs = nowUs() - now; - if (slotUs > 20000 && m->uiImageSlotMax <= 20000) - DEBUG_WARN(DEBUG_CATEGORY_CALL, "video GUI image slot slow call=%llu serial=%llu work_us=%lld mono_us=%lld", - (unsigned long long)m->id, (unsigned long long)serial, (long long)slotUs, (long long)now); - m->uiImageSlotMax = std::max(m->uiImageSlotMax, slotUs); - if (!m->presentationDriven || image.isNull()) imagePresented(serial); - if (!m->uiReportAt) m->uiReportAt = now; - if (now - m->uiReportAt >= 1000000) { - const int64_t paintWait = m->paintWaitMax; m->paintWaitMax = 0; - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video GUI delivery call=%llu interval_us=%lld published=%u " - "notification_wait_max_us=%lld delivery_gap_max_us=%lld paint_wait_max_us=%lld image_slot_max_us=%lld mono_us=%lld", - (unsigned long long)m->id, (long long)(now - m->uiReportAt), m->uiPublished, - (long long)m->uiWaitMax, (long long)m->uiGapMax, (long long)paintWait, (long long)m->uiImageSlotMax, (long long)now); - m->uiReportAt = now; m->uiPublished = 0; m->uiWaitMax = m->uiGapMax = 0; - m->uiImageSlotMax = 0; - } - if (!image.isNull() && pts >= 0 && pts % 1000000 < 34000) - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video timing stage=publish call=%llu pts=%lld mono_us=%lld ui_wait_us=%lld", - (unsigned long long)m->id, (long long)pts, (long long)now, (long long)wait); + image = slot->image; + } + const int64_t now = nowUs(), wait = now - queued; + ++m->uiPublished; m->uiWaitMax = std::max(m->uiWaitMax, wait); + if (m->uiLastAt) m->uiGapMax = std::max(m->uiGapMax, now - m->uiLastAt); + m->uiLastAt = now; + emit imageChanged(image, serial); + const int64_t slotUs = nowUs() - now; + if (slotUs > 20000 && m->uiImageSlotMax <= 20000) + DEBUG_WARN(DEBUG_CATEGORY_CALL, "video GUI image slot slow call=%llu serial=%llu work_us=%lld mono_us=%lld", + (unsigned long long)m->id, (unsigned long long)serial, (long long)slotUs, (long long)now); + m->uiImageSlotMax = std::max(m->uiImageSlotMax, slotUs); + if (!m->presentationDriven || image.isNull()) imagePresented(serial); + if (!m->uiReportAt) m->uiReportAt = now; + if (now - m->uiReportAt >= 1000000) { + const int64_t paintWait = m->paintWaitMax; m->paintWaitMax = 0; + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video GUI delivery call=%llu interval_us=%lld published=%u " + "notification_wait_max_us=%lld delivery_gap_max_us=%lld paint_wait_max_us=%lld image_slot_max_us=%lld mono_us=%lld", + (unsigned long long)m->id, (long long)(now - m->uiReportAt), m->uiPublished, + (long long)m->uiWaitMax, (long long)m->uiGapMax, (long long)paintWait, (long long)m->uiImageSlotMax, (long long)now); + m->uiReportAt = now; m->uiPublished = 0; m->uiWaitMax = m->uiGapMax = 0; + m->uiImageSlotMax = 0; + } + if (!image.isNull() && pts >= 0 && pts % 1000000 < 34000) + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video timing stage=publish call=%llu pts=%lld mono_us=%lld ui_wait_us=%lld", + (unsigned long long)m->id, (long long)pts, (long long)now, (long long)wait); } void CallVideoEngine::start(quint64 id) { if (m->running && m->id == id) return;