[PATCH v25 16/16] ALSA: virtio: preserve playback progress across pause

From: Joonho Lee

Date: Tue Nov 10 2026 - 22:01:21 EST


TX completions can arrive after PAUSE closes transfer admission.
Preserve that progress and schedule catch-up when the stream resumes or
a rejected pause restores RUNNING. Cache device latency in stream-owned
state so a late completion does not need to dereference the ALSA
runtime. Clear both the cache and runtime delay on prepare and before
RESET observes the hardware pointer.

Use the existing indirect hw_queue_size to keep outstanding TX below one
physical ring: buffer_bytes - period_bytes. Equal modulo cursors then
mean no unobserved completion, and a non-interrupt HWSYNC cannot consume
a hidden full-ring wrap before deferred period work. Continue using the
ordinary ALSA pointer update for silence, drain, stop-threshold and
wakeup policy.

Check for pending progress or a latched transfer error again in the
period worker under the PCM stream lock. A queued notification can
outlive a paused RESET or an earlier pointer observation. Calling the
period helper without new progress after resume can turn equal cursors
into a false wrap when the core has backdated its pointer timestamp.

This reduces queued buffering by one period. With two periods, only one
period can be queued. Worst-case scheduling tolerance needs
qualification.

Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@xxxxxxxxx>

---
sound/virtio/virtio_pcm.c | 3 +++
sound/virtio/virtio_pcm.h | 10 ++++++++++
sound/virtio/virtio_pcm_msg.c | 8 +++-----
sound/virtio/virtio_pcm_ops.c | 19 ++++++++++++++++---
4 files changed, 32 insertions(+), 8 deletions(-)

diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c
--- a/sound/virtio/virtio_pcm.c
+++ b/sound/virtio/virtio_pcm.c
@@ -338,6 +338,9 @@
}

if (vss->direction != SNDRV_PCM_STREAM_CAPTURE) {
+ /* Ignore stale work after RESET rebases the playback cursors. */
+ if (!virtsnd_pcm_playback_progress_pending(vss))
+ return;
snd_pcm_period_elapsed_under_stream_lock(substream);
return;
}
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -53,6 +53,7 @@
* substream operators, including @stopped.
* @buffer_bytes: Current buffer size in bytes.
* @hw_ptr: Substream hardware pointer value in bytes [0 ... buffer_bytes).
+ * @playback_latency_bytes: Device delay cached independently of the ALSA runtime.
* @capture_ready: Managed-mode bytes copied but not yet acknowledged by ALSA;
* always zero in free-running mode.
* @capture_boundary: ALSA boundary captured for this prepared epoch.
@@ -113,6 +114,7 @@
bool resetting;
size_t buffer_bytes;
size_t hw_ptr;
+ u32 playback_latency_bytes;
size_t capture_ready;
snd_pcm_uframes_t capture_boundary;
u64 capture_copied_total;
@@ -179,6 +181,14 @@
return schedule_work(&vss->elapsed_period);
}

+/* Caller holds the PCM stream lock so RESET cannot rebase these cursors. */
+static inline bool
+virtsnd_pcm_playback_progress_pending(struct virtio_pcm_substream *vss)
+{
+ guard(spinlock_irqsave)(&vss->lock);
+ return vss->xfer_xrun || vss->hw_ptr != vss->pcm_indirect.hw_io;
+}
+
/*
* ALSA may observe a subperiod tail before its period callback. Keep actual
* unread occupancy separate from pointer observations and notification state;
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -1002,12 +1002,10 @@
/* Keep latched errors until prepare establishes a fresh epoch. */
vss->msg_count--;

- if (vss->xfer_enabled) {
- struct snd_pcm_runtime *runtime = vss->substream->runtime;
-
- runtime->delay = bytes_to_frames(runtime, latency_bytes);
+ /* A queued request can outlive the ALSA runtime after hw_free. */
+ vss->playback_latency_bytes = latency_bytes;
+ if (vss->prepared && !vss->preparing)
virtsnd_pcm_schedule_period_elapsed(vss);
- }
if (!vss->msg_count)
wake_up_all(&vss->msg_empty);
}
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -770,6 +770,13 @@
memset(&vss->pcm_indirect, 0, sizeof(vss->pcm_indirect));
vss->pcm_indirect.sw_buffer_size = vss->buffer_bytes;
vss->pcm_indirect.hw_buffer_size = vss->buffer_bytes;
+ if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK) {
+ /* Keep unobserved completions below one physical ring. */
+ vss->pcm_indirect.hw_queue_size = vss->buffer_bytes -
+ snd_pcm_lib_period_bytes(substream);
+ vss->playback_latency_bytes = 0;
+ substream->runtime->delay = 0;
+ }
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
vss->capture_boundary = substream->runtime->boundary;
rc = virtsnd_pcm_capture_refresh_mode_locked(vss,
@@ -1197,7 +1204,8 @@
}

if (running) {
- if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE ||
+ virtsnd_pcm_playback_progress_pending(vss))
virtsnd_pcm_schedule_period_elapsed(vss);
return 0;
}
@@ -1219,7 +1227,8 @@
return -ECANCELED;
vss->device_running = true;
}
- if (!rc && (vss->direction == SNDRV_PCM_STREAM_CAPTURE))
+ if (!rc && (vss->direction == SNDRV_PCM_STREAM_CAPTURE ||
+ virtsnd_pcm_playback_progress_pending(vss)))
virtsnd_pcm_schedule_period_elapsed(vss);
return rc;
case SNDRV_PCM_TRIGGER_SUSPEND:
@@ -1260,7 +1269,8 @@
virtsnd_pcm_trigger_failed(vss, epoch);
} else if (virtsnd_pcm_start_queue(vss, epoch)) {
virtsnd_pcm_trigger_failed(vss, epoch);
- } else if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ } else if (vss->direction == SNDRV_PCM_STREAM_CAPTURE ||
+ virtsnd_pcm_playback_progress_pending(vss)) {
virtsnd_pcm_schedule_period_elapsed(vss);
}
}
@@ -1610,6 +1620,7 @@
}
if (!paused_pointer)
hw_ptr = vss->hw_ptr;
+ runtime->delay = bytes_to_frames(runtime, vss->playback_latency_bytes);
}
if (paused_pointer) {
if (defer_work)
@@ -2213,6 +2224,8 @@
guard(spinlock)(&vss->lock);
if (vss->msg_count)
return -EBUSY;
+ vss->playback_latency_bytes = 0;
+ runtime->delay = 0;
}
vss->indirect_reset_observing = true;
rc = snd_pcm_lib_reset(substream);