alsa: Improve resume logic after alsa suspend

Currently, when a system is waking up from suspend, the resume process of the
ALSA sink and source is unstable. Sometimes the device needs to be restarted
multiple times and when the system was suspended between snd_pcm_mmap_begin()
and snd_pcm_mmap_commit(), pulseaudio crashes on resume.
Additionally, variables are not reset after the resume, so that sink/source
report wrong latencies.
This patch fixes the issues by closing and re-opening the PCM if recovery
from an error condition is not possible. Additionally, the variables are
reset, so that latencies are reported correctly.
This commit is contained in:
Georg Chini 2019-03-09 20:19:34 +01:00 committed by Arun Raghavan
parent e794d0a21a
commit f7b3537bbf
3 changed files with 237 additions and 133 deletions

View file

@ -158,6 +158,7 @@ enum {
};
static void userdata_free(struct userdata *u);
static int unsuspend(struct userdata *u, bool recovering);
static pa_hook_result_t reserve_cb(pa_reserve_wrapper *r, void *forced, struct userdata *u) {
pa_assert(r);
@ -384,6 +385,39 @@ restart:
u->watermark_dec_not_before = now + TSCHED_WATERMARK_VERIFY_AFTER_USEC;
}
/* Called from IO Context on unsuspend or from main thread when creating source */
static void reset_watermark(struct userdata *u, size_t tsched_watermark, pa_sample_spec *ss,
bool in_thread) {
u->tsched_watermark = pa_convert_size(tsched_watermark, ss, &u->source->sample_spec);
u->watermark_inc_step = pa_usec_to_bytes(TSCHED_WATERMARK_INC_STEP_USEC, &u->source->sample_spec);
u->watermark_dec_step = pa_usec_to_bytes(TSCHED_WATERMARK_DEC_STEP_USEC, &u->source->sample_spec);
u->watermark_inc_threshold = pa_usec_to_bytes_round_up(TSCHED_WATERMARK_INC_THRESHOLD_USEC, &u->source->sample_spec);
u->watermark_dec_threshold = pa_usec_to_bytes_round_up(TSCHED_WATERMARK_DEC_THRESHOLD_USEC, &u->source->sample_spec);
fix_min_sleep_wakeup(u);
fix_tsched_watermark(u);
if (in_thread)
pa_source_set_latency_range_within_thread(u->source,
u->min_latency_ref,
pa_bytes_to_usec(u->hwbuf_size, ss));
else {
pa_source_set_latency_range(u->source,
0,
pa_bytes_to_usec(u->hwbuf_size, ss));
/* work-around assert in pa_source_set_latency_within_thead,
keep track of min_latency and reuse it when
this routine is called from IO context */
u->min_latency_ref = u->source->thread_info.min_latency;
}
pa_log_info("Time scheduling watermark is %0.2fms",
(double) u->tsched_watermark_usec / PA_USEC_PER_MSEC);
}
static void hw_sleep_time(struct userdata *u, pa_usec_t *sleep_usec, pa_usec_t*process_usec) {
pa_usec_t wm, usec;
@ -414,6 +448,31 @@ static void hw_sleep_time(struct userdata *u, pa_usec_t *sleep_usec, pa_usec_t*p
#endif
}
/* Reset smoother and counters */
static void reset_vars(struct userdata *u) {
pa_smoother_reset(u->smoother, pa_rtclock_now(), true);
u->smoother_interval = SMOOTHER_MIN_INTERVAL;
u->last_smoother_update = 0;
u->read_count = 0;
u->first = true;
}
/* Called from IO context */
static void close_pcm(struct userdata *u) {
pa_smoother_pause(u->smoother, pa_rtclock_now());
/* Let's suspend */
snd_pcm_close(u->pcm_handle);
u->pcm_handle = NULL;
if (u->alsa_rtpoll_item) {
pa_rtpoll_item_free(u->alsa_rtpoll_item);
u->alsa_rtpoll_item = NULL;
}
}
static int try_recover(struct userdata *u, const char *call, int err) {
pa_assert(u);
pa_assert(call);
@ -430,11 +489,17 @@ static int try_recover(struct userdata *u, const char *call, int err) {
pa_log_debug("%s: System suspended!", call);
if ((err = snd_pcm_recover(u->pcm_handle, err, 1)) < 0) {
pa_log("%s: %s", call, pa_alsa_strerror(err));
return -1;
pa_log("%s: %s, trying to restart PCM", call, pa_alsa_strerror(err));
/* As a last measure, restart the PCM and inform the caller about it. */
close_pcm(u);
if (unsuspend(u, true) < 0)
return -1;
return 1;
}
u->first = true;
reset_vars(u);
return 0;
}
@ -493,6 +558,7 @@ static size_t check_left_to_record(struct userdata *u, size_t n_bytes, bool on_t
static int mmap_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, bool on_timeout) {
bool work_done = false;
bool recovery_done = false;
pa_usec_t max_sleep_usec = 0, process_usec = 0;
size_t left_to_record;
unsigned j = 0;
@ -511,7 +577,8 @@ static int mmap_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
if (PA_UNLIKELY((n = pa_alsa_safe_avail(u->pcm_handle, u->hwbuf_size, &u->source->sample_spec)) < 0)) {
if ((r = try_recover(u, "snd_pcm_avail", (int) n)) == 0)
recovery_done = true;
if ((r = try_recover(u, "snd_pcm_avail", (int) n)) >= 0)
continue;
return r;
@ -583,9 +650,13 @@ static int mmap_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
if (!after_avail && err == -EAGAIN)
break;
recovery_done = true;
if ((r = try_recover(u, "snd_pcm_mmap_begin", err)) == 0)
continue;
if (r == 1)
break;
return r;
}
@ -617,9 +688,13 @@ static int mmap_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
if (PA_UNLIKELY((sframes = snd_pcm_mmap_commit(u->pcm_handle, offset, frames)) < 0)) {
recovery_done = true;
if ((r = try_recover(u, "snd_pcm_mmap_commit", (int) sframes)) == 0)
continue;
if (r == 1)
break;
return r;
}
@ -646,6 +721,11 @@ static int mmap_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
*sleep_usec -= process_usec;
else
*sleep_usec = 0;
/* If the PCM was recovered, it may need restarting. Reduce the sleep time
* to 0 to ensure immediate restart. */
if (recovery_done)
*sleep_usec = 0;
}
return work_done ? 1 : 0;
@ -653,6 +733,7 @@ static int mmap_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
static int unix_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, bool on_timeout) {
int work_done = false;
bool recovery_done = false;
pa_usec_t max_sleep_usec = 0, process_usec = 0;
size_t left_to_record;
unsigned j = 0;
@ -671,7 +752,8 @@ static int unix_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
if (PA_UNLIKELY((n = pa_alsa_safe_avail(u->pcm_handle, u->hwbuf_size, &u->source->sample_spec)) < 0)) {
if ((r = try_recover(u, "snd_pcm_avail", (int) n)) == 0)
recovery_done = true;
if ((r = try_recover(u, "snd_pcm_avail", (int) n)) >= 0)
continue;
return r;
@ -735,9 +817,13 @@ static int unix_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
if (!after_avail && (int) frames == -EAGAIN)
break;
recovery_done = true;
if ((r = try_recover(u, "snd_pcm_readi", (int) frames)) == 0)
continue;
if (r == 1)
break;
return r;
}
@ -776,6 +862,11 @@ static int unix_read(struct userdata *u, pa_usec_t *sleep_usec, bool polled, boo
*sleep_usec -= process_usec;
else
*sleep_usec = 0;
/* If the PCM was recovered, it may need restarting. Reduce the sleep time
* to 0 to ensure immediate restart. */
if (recovery_done)
*sleep_usec = 0;
}
return work_done ? 1 : 0;
@ -853,18 +944,14 @@ static int build_pollfd(struct userdata *u) {
/* Called from IO context */
static void suspend(struct userdata *u) {
pa_assert(u);
pa_assert(u->pcm_handle);
pa_smoother_pause(u->smoother, pa_rtclock_now());
/* PCM may have been invalidated due to device failure.
* In that case, there is nothing to do. */
if (!u->pcm_handle)
return;
/* Let's suspend */
snd_pcm_close(u->pcm_handle);
u->pcm_handle = NULL;
if (u->alsa_rtpoll_item) {
pa_rtpoll_item_free(u->alsa_rtpoll_item);
u->alsa_rtpoll_item = NULL;
}
/* Close PCM device */
close_pcm(u);
pa_log_info("Device suspended...");
}
@ -922,39 +1009,6 @@ static int update_sw_params(struct userdata *u) {
return 0;
}
/* Called from IO Context on unsuspend or from main thread when creating source */
static void reset_watermark(struct userdata *u, size_t tsched_watermark, pa_sample_spec *ss,
bool in_thread) {
u->tsched_watermark = pa_convert_size(tsched_watermark, ss, &u->source->sample_spec);
u->watermark_inc_step = pa_usec_to_bytes(TSCHED_WATERMARK_INC_STEP_USEC, &u->source->sample_spec);
u->watermark_dec_step = pa_usec_to_bytes(TSCHED_WATERMARK_DEC_STEP_USEC, &u->source->sample_spec);
u->watermark_inc_threshold = pa_usec_to_bytes_round_up(TSCHED_WATERMARK_INC_THRESHOLD_USEC, &u->source->sample_spec);
u->watermark_dec_threshold = pa_usec_to_bytes_round_up(TSCHED_WATERMARK_DEC_THRESHOLD_USEC, &u->source->sample_spec);
fix_min_sleep_wakeup(u);
fix_tsched_watermark(u);
if (in_thread)
pa_source_set_latency_range_within_thread(u->source,
u->min_latency_ref,
pa_bytes_to_usec(u->hwbuf_size, ss));
else {
pa_source_set_latency_range(u->source,
0,
pa_bytes_to_usec(u->hwbuf_size, ss));
/* work-around assert in pa_source_set_latency_within_thead,
keep track of min_latency and reuse it when
this routine is called from IO context */
u->min_latency_ref = u->source->thread_info.min_latency;
}
pa_log_info("Time scheduling watermark is %0.2fms",
(double) u->tsched_watermark_usec / PA_USEC_PER_MSEC);
}
/* Called from IO Context on unsuspend */
static void update_size(struct userdata *u, pa_sample_spec *ss) {
pa_assert(u);
@ -976,7 +1030,7 @@ static void update_size(struct userdata *u, pa_sample_spec *ss) {
}
/* Called from IO context */
static int unsuspend(struct userdata *u) {
static int unsuspend(struct userdata *u, bool recovering) {
pa_sample_spec ss;
int err;
bool b, d;
@ -1047,15 +1101,10 @@ static int unsuspend(struct userdata *u) {
/* FIXME: We need to reload the volume somehow */
u->read_count = 0;
pa_smoother_reset(u->smoother, pa_rtclock_now(), true);
u->smoother_interval = SMOOTHER_MIN_INTERVAL;
u->last_smoother_update = 0;
u->first = true;
reset_vars(u);
/* reset the watermark to the value defined when source was created */
if (u->use_tsched)
if (u->use_tsched && !recovering)
reset_watermark(u, u->tsched_watermark_ref, &u->source->sample_spec, true);
pa_log_info("Resumed successfully...");
@ -1212,7 +1261,7 @@ static int source_set_state_in_io_thread_cb(pa_source *s, pa_source_state_t new_
}
if (s->thread_info.state == PA_SOURCE_SUSPENDED) {
if ((r = unsuspend(u)) < 0)
if ((r = unsuspend(u, false)) < 0)
return r;
}
@ -1668,10 +1717,17 @@ static void thread_func(void *userdata) {
}
if (revents & ~POLLIN) {
if (pa_alsa_recover_from_poll(u->pcm_handle, revents) < 0)
if ((err = pa_alsa_recover_from_poll(u->pcm_handle, revents)) < 0)
goto fail;
u->first = true;
/* Stream needs to be restarted */
if (err == 1) {
close_pcm(u);
if (unsuspend(u, true) < 0)
goto fail;
} else
reset_vars(u);
revents = 0;
} else if (revents && u->use_tsched && pa_log_ratelimit(PA_LOG_DEBUG))
pa_log_debug("Wakeup from ALSA!");