From 0a73d140e6297735b33054ec5439dae5174defcc Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 20 Oct 2026 00:12:26 +0200 Subject: [PATCH] Account for full refresh-cycle GUI acknowledgement latency --- src/call/call_video_playout.c | 4 ++- tests/test_call_video_playout.c | 59 +++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/call/call_video_playout.c b/src/call/call_video_playout.c index 4e86b28d..0803b810 100644 --- a/src/call/call_video_playout.c +++ b/src/call/call_video_playout.c @@ -170,7 +170,9 @@ void call_video_playout_presented(struct call_video_playout *p, int64_t pts, int const struct call_video_playout_frame *f = &p->pending; int64_t av = p->pending_av, lead = now - p->selected_us; // GUI freeze не становится постоянным опережением dispatch для последующих кадров. - if (lead <= f->interval_us / 2) p->lead_us = (p->lead_us * 7 + lead) / 8; + // ACK TextureView/композитора может занимать целый период экрана. Его надо вычесть + // из dispatch следующего кадра; иначе один ACK на 60 Hz искусственно снижает FPS. + if (lead <= f->interval_us + f->interval_us / 4 + 1000) p->lead_us = (p->lead_us * 7 + lead) / 8; int64_t error = now - p->head_show_us; if (!p->anchored || error > f->interval_us / 2 || error < -(int64_t)f->interval_us / 2) p->last_deadline = now; else { diff --git a/tests/test_call_video_playout.c b/tests/test_call_video_playout.c index 350b7a57..dc38de7d 100644 --- a/tests/test_call_video_playout.c +++ b/tests/test_call_video_playout.c @@ -16,33 +16,35 @@ static void simulate(unsigned pause_ms, int cpu, int64_t quantum, int accelerate arrival = origin + 1000000 + pause_ms * 1000ll; frames[i] = (struct call_video_playout_frame){origin + i * 33333ll, arrival, 33333}; } - unsigned selected = 0, shown = 0, available = 0, accelerated = 0; + unsigned selected = 0, shown = 0, skipped = 0, available = 0, accelerated = 0; int64_t last_show = 0, ack_at = 0, recovered_lag = INT64_MAX, audio_pts = origin; - for (int64_t now = origin; now < origin + 22000000 && shown < COUNT; now += quantum) { + for (int64_t now = origin; now < origin + 22000000 && shown + skipped < COUNT; now += quantum) { uint32_t rate = accelerated_audio && now >= origin + 1500000 && now < origin + 2500000 ? 1100000 : 1000000; if (now > origin) audio_pts += quantum * rate / 1000000; if (cpu && now >= origin + 1000000 && now < origin + 1000000 + pause_ms * 1000ll) continue; while (available < COUNT && frames[available].received_us <= now) ++available; if (ack_at && now >= ack_at) { - call_video_playout_presented(&p, frames[shown].pts_us, now); + call_video_playout_presented(&p, frames[selected - 1].pts_us, now); CHECK(!last_show || now > last_show); last_show = now; ++shown; ack_at = 0; } struct call_clock_snapshot audio = {.pts_us = audio_pts, .rate_ppm = rate, .valid = !(now >= origin + 4000000 && now < origin + 4100000)}; const struct call_video_playout_frame *f = selected < available ? frames + selected : NULL; - int action = call_video_playout_poll(&p, f, now, &audio, available - shown, 1); - if (now >= origin + 6000000 && shown) recovered_lag = audio.pts_us - frames[shown - 1].pts_us; + int action = call_video_playout_poll(&p, f, now, &audio, available - shown - skipped, 1); + if (now >= origin + 6000000 && shown) recovered_lag = audio.pts_us - p.last_show_pts; if (action == CALL_VIDEO_PLAYOUT_WAIT) continue; + if (action == CALL_VIDEO_PLAYOUT_DROP_LATE) { ++selected; ++skipped; continue; } CHECK(action == CALL_VIDEO_PLAYOUT_SHOW && audio.valid && !ack_at); CHECK(f->pts_us - audio.pts_us <= 40000); if (p.speed_ppm > 1050000) ++accelerated; ++selected; ack_at = now + 3000; // Не мгновенный GUI: кадр остаётся в общем запасе до ACK. CHECK(call_video_playout_poll(&p, selected < available ? frames + selected : NULL, now, &audio, - available - shown, 1) == CALL_VIDEO_PLAYOUT_WAIT); + available - shown - skipped, 1) == CALL_VIDEO_PLAYOUT_WAIT); } - DEBUG_INFO(DEBUG_CATEGORY_CALL, "presentation recovery pause_ms=%u cpu=%d quantum_us=%lld audio_fast=%d shown=%u/%u lag_us=%lld", - pause_ms, cpu, (long long)quantum, accelerated_audio, shown, COUNT, (long long)recovered_lag); - CHECK(shown == COUNT && accelerated); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "presentation recovery pause_ms=%u cpu=%d quantum_us=%lld audio_fast=%d shown=%u skipped=%u total=%u lag_us=%lld", + pause_ms, cpu, (long long)quantum, accelerated_audio, shown, skipped, COUNT, (long long)recovered_lag); + CHECK(shown + skipped == COUNT && accelerated); + if (quantum < 2000 || pause_ms <= 100) CHECK(skipped == 0); CHECK(recovered_lag < (quantum < 2000 ? 100000 : 220000)); } @@ -129,6 +131,42 @@ static void render_latency_change(void) { DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS render latency 3->9ms: phase_us=%lld; no accumulated per-frame GUI delay", (long long)phase); } +/* Вход 60 fps, показ ограничен 60 Hz: после паузы ускориться физически нельзя. + * Каждый вход считается decoded; теряется лишь показ, ACK остаётся единственным владельцем часов. */ +static void limited_display(unsigned pause_ms) { + enum { COUNT = 600 }; + struct call_video_playout p; call_video_playout_init(&p, 14); + unsigned consumed = 0, shown = 0, skipped = 0; + int64_t ack_at = 0, pending_pts = 0, recovered = INT64_MAX; + for (int64_t now = origin; now < origin + 12000000 && consumed < COUNT; now += 1000) { + unsigned available = (unsigned)((now - origin) * 60 / 1000000) + 1; + if (available > COUNT) available = COUNT; + if (ack_at && now >= ack_at) { + call_video_playout_presented(&p, pending_pts, now); ++shown; ack_at = 0; + } + if (now >= origin + 1000000 && now < origin + 1000000 + pause_ms * 1000ll) continue; + if (consumed >= available) continue; + struct call_video_playout_frame f = {origin + consumed * 1000000ll / 60, origin, 16666}; + struct call_clock_snapshot audio = {.pts_us = now, .rate_ppm = 1000000, .valid = 1}; + const int64_t before = p.last_show_us, deadline = p.last_deadline; + int action = call_video_playout_poll(&p, &f, now, &audio, available - consumed + (ack_at != 0), 1); + if (action == CALL_VIDEO_PLAYOUT_SHOW) { + ++consumed; pending_pts = f.pts_us; + ack_at = origin + ((now - origin) * 60 / 1000000 + 1) * 1000000 / 60; + } else if (action == CALL_VIDEO_PLAYOUT_DROP_LATE) { + ++consumed; ++skipped; + CHECK(p.last_show_us == before && p.last_deadline == deadline && !p.awaiting); + } else CHECK(action == CALL_VIDEO_PLAYOUT_WAIT); + if (now > origin + 6000000) recovered = now - f.pts_us; + } + if (ack_at) { call_video_playout_presented(&p, pending_pts, ack_at); ++shown; } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "60Hz recovery pause_ms=%u decoded=%u shown=%u skipped=%u lag_us=%lld", + pause_ms, consumed, shown, skipped, (long long)recovered); + CHECK(consumed == COUNT && shown + skipped == COUNT); + if (pause_ms <= 100) CHECK(skipped == 0); + else CHECK(skipped > 0 && recovered < 180000); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); const unsigned pauses[] = {50, 100, 200, 500}; @@ -137,5 +175,6 @@ int main(void) { simulate(pauses[i], cpu, 1000, 0); simulate(pauses[i], cpu, 15625, 1); } - presentation_deadline(); queued_capture_gap(); slowing_with_queue(); render_latency_change(); return 0; + presentation_deadline(); queued_capture_gap(); slowing_with_queue(); render_latency_change(); + limited_display(50); limited_display(100); limited_display(500); return 0; }