Document independent video scheduling and verified recovery behavior

This commit is contained in:
evgeny committed 2026-10-19 21:22:39 +02:00
1 parent 55e87d20bf
commit c3c88457a8
3 files changed
+88 -37

No files matched your search

+49
View File
@@ -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.
+3
View File
@@ -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 внутренних проверок).
+36 -37
View File
@@ -13,7 +13,6 @@
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <deque>
#include <chrono>
#ifdef _WIN32
#include <QWinEventNotifier>
@@ -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;