From: Frank van de Pol <fvdpol@gmail.com>
To: tiwai@suse.de, perex@perex.cz
Cc: corbet@lwn.net, khan@linuxfoundation.org, rdunlap@infradead.org,
fvdpol@gmail.com, linux-sound@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [PATCH v5 12/13] ALSA: jockey3: Add power management
Date: Mon, 19 Oct 2026 13:44:44 -0400 [thread overview]
Message-ID: <20261019174445.1931263-13-fvdpol@gmail.com> (raw)
In-Reply-To: <20261019174445.1931263-1-fvdpol@gmail.com>
Add suspend, resume and reset_resume callbacks.
Suspend stops the URBs. Resume programs the rate again and restarts
them; reset_resume also repeats the initialization handshake first.
SNDRV_PCM_INFO_RESUME is not set. The device loses stream
synchronization across a suspend, so userspace has to prepare the
stream again.
A watchdog tick or a recovery that is already running must not restart
or reset a device that is being suspended. JOCKEY3_FLAG_SUSPENDED is
set before the URBs are stopped, and the watchdog and the recovery
ladder test it at each point where they would otherwise act.
Assisted-by: LLM
Signed-off-by: Frank van de Pol <fvdpol@gmail.com>
---
sound/usb/jockey3/jockey3.c | 183 +++++++++++++++++++++++++++++++-----
1 file changed, 161 insertions(+), 22 deletions(-)
diff --git a/sound/usb/jockey3/jockey3.c b/sound/usb/jockey3/jockey3.c
index 17c014f2e69d0..db351c42d85c3 100644
--- a/sound/usb/jockey3/jockey3.c
+++ b/sound/usb/jockey3/jockey3.c
@@ -170,17 +170,18 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
*
* The watchdog work item adds one rule: rate_mutex must never be held across
* cancel_delayed_work_sync(&chip->watchdog_work). jockey3_stop_urbs() is called
- * from inside rate_mutex at three sites, so it disarms with the non-sync
+ * from inside rate_mutex at four sites, so it disarms with the non-sync
* cancel_delayed_work(), which is safe under any lock; a tick that is already
* running when the cancel lands re-reads 'stopping' and does nothing. The sync
* form appears only in jockey3_disconnect(), in probe's error path and in
* jockey3_free_resources(), none of which holds a mutex.
*
* Because that disarm does not wait, a tick can be parked on rate_mutex while
- * jockey3_pre_reset() takes the device down, then wake to the timestamps it
- * zeroed and read the stream as stalled. So jockey3_recover_urb_stream()
- * re-tests DISCONNECTED and RESETTING under the mutex and leaves the restart
- * to jockey3_post_reset().
+ * jockey3_suspend() or jockey3_pre_reset() takes the device down, then wake to
+ * the timestamps they zeroed and read the stream as stalled. So
+ * jockey3_recover_urb_stream() re-tests DISCONNECTED, SUSPENDED and RESETTING
+ * under the mutex and leaves the restart to whichever path is bringing the
+ * device back.
*
* jockey3_watchdog_work() itself may call jockey3_recover_urb_stream(),
* which takes rate_mutex and calls jockey3_stop_urbs() -- i.e. the watchdog's
@@ -450,7 +451,10 @@ struct jockey3_pcm_urb_stream {
* registered or the watchdog armed, so it is the only user in existence.
* @rate_mutex: serializes sample-rate changes and the URB stop/start that goes
* with them; process context only, outermost lock
- * @flags: JOCKEY3_FLAG_* bits, atomic bitops
+ * @flags: JOCKEY3_FLAG_* bits, atomic bitops. SUSPENDED is set before
+ * jockey3_suspend() takes @rate_mutex and cleared by
+ * jockey3_restore_device() under it, so a tick blocked on that mutex sees
+ * it on the way out.
* @current_rate: sample rate the hardware is programmed to; @rate_mutex
* @dev_idx: card slot held in jockey3_devices_used
* @reset_done: completed by jockey3_post_reset(), and by jockey3_disconnect()
@@ -483,7 +487,7 @@ struct jockey3_pcm_urb_stream {
* callers elsewhere poll rather than block on recovery finishing.
* @warm_start: true if the current URB ring was last started by the stall
* watchdog's own lightweight restart, false for every other (re)start
- * (first open, rate change, USB reset, probe). Both kinds share
+ * (first open, rate change, USB reset, resume, probe). Both kinds share
* one start_grace_ms; this only labels which one is in effect, for
* dev_dbg(). Chip-wide, not per-direction: jockey3_start_urbs() restarts
* the shared ring for both directions at once. Plain bool, written by
@@ -585,6 +589,7 @@ static void jockey3_release_slot(unsigned int dev_idx)
/* Chip flags */
#define JOCKEY3_FLAG_DISCONNECTED 0
#define JOCKEY3_FLAG_RESETTING 1
+#define JOCKEY3_FLAG_SUSPENDED 2
static inline bool jockey3_is_disconnected(const struct jockey3_chip *chip)
{
@@ -596,6 +601,11 @@ static inline bool jockey3_is_resetting(const struct jockey3_chip *chip)
return test_bit(JOCKEY3_FLAG_RESETTING, &chip->flags);
}
+static inline bool jockey3_is_suspended(const struct jockey3_chip *chip)
+{
+ return test_bit(JOCKEY3_FLAG_SUSPENDED, &chip->flags);
+}
+
/*
* Publish a new sample rate.
*
@@ -1711,7 +1721,7 @@ static void jockey3_submit_failed(struct jockey3_chip *chip, int err,
* @chip: driver state
* @warm: true only for the stall watchdog's own lightweight restart of a ring
* that was streaming a moment earlier; false for a cold start (first open,
- * rate change, USB reset, probe). Both share start_grace_ms; this
+ * rate change, USB reset, resume, probe). Both share start_grace_ms; this
* only labels the restart for dev_dbg() and is recorded chip-wide in
* @chip->warm_start.
*
@@ -1736,6 +1746,13 @@ static int jockey3_start_urbs(struct jockey3_chip *chip, bool warm)
if (jockey3_is_disconnected(chip))
return -ENODEV;
+ /*
+ * Belt and braces; jockey3_recover_urb_stream() is what actually bails.
+ * jockey3_restore_device() clears the flag before its own restart.
+ */
+ if (jockey3_is_suspended(chip))
+ return -ESHUTDOWN;
+
dev_dbg(&chip->intf0->dev, "Starting all URBs (%s start, grace %u ms)\n",
warm ? "warm" : "cold", jockey3_start_grace_ms());
@@ -1891,6 +1908,13 @@ static void jockey3_start_urbs_failed(struct jockey3_chip *chip, int err, const
return;
}
+ if (err == -ESHUTDOWN || jockey3_is_suspended(chip)) {
+ dev_dbg(&chip->intf0->dev,
+ "Could not start URBs after %s: device is suspended; the resume path will start them\n",
+ context);
+ return;
+ }
+
if (err == -ENOENT) {
dev_err(&chip->intf0->dev,
"Endpoints are disabled after %s; the device needs a reset to restore them\n",
@@ -2139,7 +2163,7 @@ static void jockey3_watchdog_check(struct jockey3_chip *chip, const int directio
scoped_guard(spinlock_irqsave, &urb_stream->lock) {
/*
- * One flag covers every deliberate stop -- rate change,
+ * One flag covers every deliberate stop -- rate change, suspend,
* pre_reset and teardown all reach jockey3_stop_urbs(). Checked
* before now/last/age_ns are sampled, not after: this tick can
* have been sitting on system_long_wq for a while before it got
@@ -2272,7 +2296,15 @@ static void jockey3_watchdog_work(struct work_struct *work)
struct jockey3_chip *chip = container_of(to_delayed_work(work),
struct jockey3_chip, watchdog_work);
- if (jockey3_is_disconnected(chip))
+ /*
+ * Suspend as well as teardown: jockey3_stop_urbs() disarms with the
+ * non-sync cancel_delayed_work(), which leaves a tick that is already
+ * running to finish. Without this it would requeue itself and keep
+ * ticking for the whole suspend, for no purpose -- every check below
+ * bails out on the suspended flag anyway. Resume re-arms via
+ * jockey3_start_urbs().
+ */
+ if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip))
return;
/*
@@ -2327,11 +2359,11 @@ static bool jockey3_wait_urb_stream_started(struct jockey3_chip *chip, const int
while (time_before(jiffies, deadline)) {
/*
- * A device that went away mid-wait completes nothing more;
- * spinning out the grace only delays the caller's next decision,
- * which in the recovery path is whether to reset.
+ * A device that went down mid-wait completes nothing until it is
+ * restored; spinning out the grace only delays the caller's next
+ * decision, which in the recovery path is whether to reset.
*/
- if (jockey3_is_disconnected(chip))
+ if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip))
return false;
if (require_healthy ? jockey3_stream_streaming_healthy(chip, urb_stream)
@@ -2434,14 +2466,16 @@ static int jockey3_run_recovery_ladder(struct jockey3_chip *chip, const int dire
/*
* Stale for the same reason the alive check above is repeated: a
* tick that blocked here may have been waiting on
- * jockey3_pre_reset(), and wakes to a stream that looks dead
- * only because that zeroed the timestamps. Restarting on that
- * would submit URBs to a device that is in the middle of a reset.
+ * jockey3_suspend() or jockey3_pre_reset(), and wakes to a
+ * stream that looks dead only because they zeroed the
+ * timestamps. Restarting on that would submit URBs to a
+ * suspended device, then escalate to resetting one.
*
- * Bailing loses nothing: jockey3_post_reset() restarts the ring
- * itself on the way back up.
+ * Bailing loses nothing: both paths restart the ring themselves
+ * on the way back up.
*/
- if (jockey3_is_disconnected(chip) || jockey3_is_resetting(chip)) {
+ if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip) ||
+ jockey3_is_resetting(chip)) {
dev_dbg(&chip->intf0->dev,
"%s stream stalled (%s), but the device is being taken down; leaving the restart to the resume path\n",
type, context);
@@ -2474,12 +2508,21 @@ static int jockey3_run_recovery_ladder(struct jockey3_chip *chip, const int dire
* timeout on a reset that will never come. Ahead of the budget, so this
* spends no attempt.
*
- * Racy by construction -- a disconnect arriving just after the test still
+ * Suspended as well as disconnected, and this is not the same check as
+ * the one under rate_mutex above. That one covers a tick that arrives
+ * after jockey3_suspend() has run. A tick which passed it earlier, while
+ * the device was still live, is legitimately mid-recovery and reaches
+ * here only after its grace has elapsed -- by which time suspend may have
+ * happened. Resetting from there was observed to queue a reset the
+ * suspended device could not complete, leaving the resume path waiting
+ * out jockey3_wait_for_reset_completion() twice over.
+ *
+ * Racy by construction -- either state arriving just after the test still
* hits it. The timeout bounds that; closing it properly would mean
* serializing against jockey3_disconnect(), which must not block on
* driver locks.
*/
- if (jockey3_is_disconnected(chip)) {
+ if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip)) {
dev_dbg(&chip->intf0->dev,
"%s stream still stalled after URB restart, but the device is down; not resetting (%s)\n",
type, context);
@@ -2584,6 +2627,10 @@ static int jockey3_recover_urb_stream(struct jockey3_chip *chip, const int direc
* SNDRV_PCM_INFO_BATCH: the hardware pointer only advances once per completed
* bulk URB (a whole Ploytec frame group), never per sample, so userspace must
* not assume a sample-accurate position.
+ *
+ * SNDRV_PCM_INFO_RESUME is deliberately absent: the device loses stream
+ * synchronization across a suspend, so the ALSA core should return -ESTRPIPE
+ * and have userspace re-prepare rather than issue TRIGGER_RESUME.
*/
#define JOCKEY3_PCM_INFO (SNDRV_PCM_INFO_MMAP | \
SNDRV_PCM_INFO_INTERLEAVED | \
@@ -2895,6 +2942,7 @@ static int jockey3_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
urb_stream->running = true;
break;
case SNDRV_PCM_TRIGGER_STOP:
+ case SNDRV_PCM_TRIGGER_SUSPEND:
urb_stream->running = false;
break;
default:
@@ -3953,6 +4001,94 @@ static int jockey3_post_reset(struct usb_interface *intf)
return 0;
}
+static int jockey3_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+ if (chip && intf == chip->intf0) {
+ dev_dbg(&intf->dev, "USB suspend, stopping URBs\n");
+
+ /*
+ * Before rate_mutex below: a tick blocked on that mutex resumes
+ * the moment this drops it, and the flag is what stops it
+ * restarting the ring. See jockey3_recover_urb_stream().
+ */
+ set_bit(JOCKEY3_FLAG_SUSPENDED, &chip->flags);
+
+ /* Notify ALSA core to transition state and unblock userspace */
+ if (chip->pcm)
+ snd_pcm_suspend_all(chip->pcm);
+
+ /*
+ * Stop the physical URBs under rate_mutex, matching
+ * jockey3_restore_device() on the resume side -- otherwise a
+ * suspend could land in the middle of a rate change.
+ */
+ scoped_guard(mutex, &chip->rate_mutex)
+ jockey3_stop_urbs(chip);
+ }
+ return 0;
+}
+
+static int jockey3_restore_device(struct jockey3_chip *chip, bool reset)
+{
+ int ret;
+
+ guard(mutex)(&chip->rate_mutex);
+
+ /*
+ * Cleared under the mutex, before the restart below that
+ * jockey3_recover_urb_stream() defers to. A tick cannot slip in between:
+ * it needs this same mutex to restart anything.
+ */
+ clear_bit(JOCKEY3_FLAG_SUSPENDED, &chip->flags);
+
+ if (reset) {
+ ret = jockey3_initialize_ploytec(chip, NULL);
+ if (ret < 0)
+ return ret;
+ }
+
+ ret = jockey3_set_rate(chip, chip->current_rate, true);
+ if (ret < 0)
+ return ret;
+
+ /*
+ * Report the failure up: the PM core logs a failed resume, and unlike the
+ * reset path there is no queued recovery on the way that would pick this
+ * up on its own.
+ */
+ ret = jockey3_start_urbs(chip, false);
+ if (ret < 0) {
+ dev_err(&chip->intf0->dev, "Failed to start URBs while restoring device: %d\n",
+ ret);
+ return ret;
+ }
+ return 0;
+}
+
+static int jockey3_resume(struct usb_interface *intf)
+{
+ struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+ if (chip && intf == chip->intf0) {
+ dev_dbg(&intf->dev, "USB resume, restoring device\n");
+ return jockey3_restore_device(chip, false);
+ }
+ return 0;
+}
+
+static int jockey3_reset_resume(struct usb_interface *intf)
+{
+ struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+ if (chip && intf == chip->intf0) {
+ dev_dbg(&intf->dev, "USB reset resume, restoring device\n");
+ return jockey3_restore_device(chip, true);
+ }
+ return 0;
+}
+
static const struct usb_device_id jockey3_ids[] = {
{ USB_DEVICE(RELOOP_VENDOR_ID, RELOOP_JOCKEY3_ME_PID), .driver_info = JOCKEY3_ME },
{ USB_DEVICE(RELOOP_VENDOR_ID, RELOOP_JOCKEY3_REMIX_PID), .driver_info = JOCKEY3_REMIX },
@@ -3981,6 +4117,9 @@ static struct usb_driver jockey3_driver = {
.disconnect = jockey3_disconnect,
.pre_reset = jockey3_pre_reset,
.post_reset = jockey3_post_reset,
+ .suspend = jockey3_suspend,
+ .resume = jockey3_resume,
+ .reset_resume = jockey3_reset_resume,
.id_table = jockey3_ids
};
--
2.47.3
next prev parent reply other threads:[~2026-10-19 17:46 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-19 17:44 [PATCH v5 00/13] ALSA: jockey3: Add support for Reloop Jockey 3 DJ controllers Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 01/13] ALSA: jockey3: Add probe skeleton " Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 02/13] ALSA: jockey3: Start the bulk streams Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 03/13] ALSA: jockey3: Add PCM playback Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 04/13] ALSA: jockey3: Add PCM capture Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 05/13] ALSA: jockey3: Add KUnit tests for the Ploytec codec Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 06/13] ALSA: jockey3: Add optimized codec variants Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 07/13] ALSA: jockey3: Coalesce several packets into one URB Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 08/13] ALSA: jockey3: Recover a stalled stream Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 09/13] ALSA: jockey3: Add a watchdog for mid-stream stalls Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 10/13] ALSA: jockey3: Add sample rate switching Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 11/13] ALSA: jockey3: Add MIDI support Frank van de Pol
2026-10-19 17:44 ` Frank van de Pol [this message]
2026-10-19 17:44 ` [PATCH v5 13/13] ALSA: jockey3: Add driver documentation Frank van de Pol
2026-10-20 13:14 ` [PATCH v5 00/13] ALSA: jockey3: Add support for Reloop Jockey 3 DJ controllers Takashi Iwai
2026-10-20 16:25 ` Frank van de Pol
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261019174445.1931263-13-fvdpol@gmail.com \
--to=fvdpol@gmail.com \
--cc=corbet@lwn.net \
--cc=khan@linuxfoundation.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-sound@vger.kernel.org \
--cc=perex@perex.cz \
--cc=rdunlap@infradead.org \
--cc=tiwai@suse.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.