mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


  parent reply	other threads:[~2026-10-19 17:46 UTC|newest]

Thread overview: 14+ 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

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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®