From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=209.85.217.41 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1792431999; b=hXaR2kUpz1tbrbIc9a+tlokyrJzDSXpYzskDFiALIx8NRIEdD4T3IiJJVUZ6yV+UeTaH 3ccWsfTkpzstIxPIGKz4dCcrXv/RjcRjZ2DrTJr596OjtuhDBY6ReNvsRot6W5Ps4cwlU vOJRAyX8uzKQzVPCSh9Ky6+a2wljV4v74jpj1VMNlMY6aa1VSuVIecYdpSVix8Vw9EEWW OOWPNmWhDMAsdCW8H5zrqR5J8o+xEL0CC5ONnDHhSbewOADjPt69Ci5AukFZINUOQTBgR QQ6lvw0KkYOtpQzwLsaE9Bx3qbU3DmZ3J+RgnBc+DJzM/PUOFb3ER/9yRiLvXrPCvzw== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1792431999; h=DMARC-Filter:Received:DKIM-Signature:X-Google-DKIM-Signature: X-Forwarded-Encrypted:X-Gm-Message-State:X-Gm-Gg:X-Received:Received: From:To:Cc:Subject:Date:Message-ID:X-Mailer:In-Reply-To:References: MIME-Version:Content-Transfer-Encoding; bh=/j+L9c85Zmk9gdLPRB45/y1VrP4yGCXm6RLIZSp5kzw=; b=4C85LCojfRqlNQQNEaofy2lYVFk54DpYFM+o7AqYP4GkMOIN+/WiPw/3k2bMR9296pNY ql57HnhLrjnsbQwEHC24xB2ukhX5v2Cz0z0QgQUBlhioHPD/fmBnzpn1kP0AGoDHuRF0I 7zBfOvMM3nK1Xnnv6aKJBj3/7o8DvWrEXqEzsU25bnq2QL1hN2M+a7f4j6C03O9mrdjMR JY/T7dudgEvxRY3QGbhDX+6odJkbvg/quXoARqaKzml3HK8RhU9M77D5O9cJhD5RxeP8b ur4lRjW9EfNoy+Ttm1Gmv8FyF1wCTllgIclW6ejbNRjFCQ6EiYhpJb166oE0dV0fsFg== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=AmpSduC3; arc=none smtp.remote-ip=209.85.217.41 Received: from mail-vs1-f41.google.com (mail-vs1-f41.google.com [209.85.217.41]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1BDF860A4AD9 for ; Mon, 19 Oct 2026 17:46:39 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org 1BDF860A4AD9 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=AmpSduC3 Received: by mail-vs1-f41.google.com with SMTP id ada2fe7eead31-7a6e560f580so1453441137.1 for ; Mon, 19 Oct 2026 10:46:39 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792431998; x=1793036798; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=/j+L9c85Zmk9gdLPRB45/y1VrP4yGCXm6RLIZSp5kzw=; b=AmpSduC3UfBn+AzvxVn+pVam6OOmCc3dzILgnDfMxVUlGJ+/lBEFLhnrp5Gb58wcJJ nq55YP+IMz9QMPLf5JdqzaY3OxJJOSSrF6imo/mXQL+maDYsDv7D4nVHns2S5IaGMKuI 55ckGaCDQafa3OCOAKzAd+YdoCPXNC41YwMKryBlmhev7nPu5gMOh+4wj0vWxDp2sxLe s1lqtb/bm9z8RytRurd/0GftCprHndxQT00p5w0cjj4nwYR4udgc7QIByDIQam3A8y8D Er5Xi1M3d1L5/++/rMu5PkcV9Q4a8ipMIYBuFm4omY2ZgZcHXmxIaT9r5dvZrGTso5Er 9T6g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792431998; x=1793036798; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=/j+L9c85Zmk9gdLPRB45/y1VrP4yGCXm6RLIZSp5kzw=; b=zp0f1p52X+TfmzUWXLrJYTxi/xhdm+Wvf2RDzBXzgb61d/UTyn4GvBJLdJCKcnEDdq AWtPmC5UWec5GWoGNCgGSM3bgoV5JPMNFMdLybuSr1PxhfFkg6hO+jXJ1BrhtNFxR0Cn s4LOE6wCh8VYt8zauj6j2NKhLNgx+RgO1uVwVj+hhK13MExFJd0T692T6lnfqE98VSn/ rja2IgFN4hyDqrpzf+y1OtZqseN2BkeAxgz+FDLzBCXl0YIy7XOYR+86bLzFdlePzFxi sCczx79Gnmd9wwxLhfiRjYuVlwaYwqEryaWUaWh7pekdRVwJue6eof0wTjje/JHdkErM SEOA== X-Forwarded-Encrypted: i=1; AKwUvBw3rEuWfI/C7X/dXZM00wp9WBggA4JI9p4rNuHNFXrTKaEQ3iLID4S7VKJGKIm/7p3aQjrNhhNl+8a8JQ==@vger.kernel.org X-Gm-Message-State: AFq9FYLD7DUzORDam+r8DlcRFFNDnQ3U/4CXginNArsL0yr33Kqktffi AHbYbBd8RTQ5SYugJZwC4CKJysaWIT5V4PMZErs5RcfcRrNy72iWwkgw7fBSBpOC X-Gm-Gg: AYBFou0DXwA/JlOB+OGsUH91PrO5krBUqlJyz3g02fz2DquTn3jVPaTU62+DE6jCwEg YkcLAl1O2yCNqAubeVqDBwopYBqCIWIsBrH9PQ6DddtJLiql5Qo4x94pqDJ0D5tXY+lvUeWQ93n aYdSKTeC5N29UhUheH5rHL2eXlB3UPjB/EMXhcUp+nvbdQIzbaFGnAuq5Svwaxd0g5XYhxn+Jbp 1AkOcvY8/qPo5zqY/9lSf2E8kp8WprSZ2O4Jd0VK75EPXTQlfKrlhu+ppULdGSJz/QyDBsLoHx0 +vDReqeZ6MG5VOyGOLYu2LO+z/vU6IrRFiqSfeAd+ALG8w93xInoBG1UAnAAd5F5Sy57C98FJJi JaVAsiWg1R5qQU6kRpRI5TDhlFO+vnF4UB+qgeF886PuuioMPt/PfboxqLcpEq/ST060Z7/tkKd W/lUYiPXMSvKNkeWzdoeLQZPF5jimDAJY3uDP8moU8cr5laBsggoLXYgtU9wHL0hD0OkRN17MSI QBL36XQGi87EzJ6qU98BMCtaCZQd4vZOubDANmRVNhU8/xadU65qgVO+RlOwWhEzwOb57XGOM/I fSyzuqZcc6h+u/okYDepse8gkfCDOZA= X-Received: by 2002:a05:6102:849b:20b0:7cc:3eb0:6ef5 with SMTP id ada2fe7eead31-7cc3eb09b24mr1525700137.11.1792431997928; Mon, 19 Oct 2026 10:46:37 -0700 (PDT) Received: from alsa-dev.int.suenodeluz.com.do ([74.244.193.177]) by smtp.gmail.com with ESMTPSA id a1e0cc1a2514c-990bb3adfd0sm790707241.12.2026.10.19.10.46.36 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 19 Oct 2026 10:46:36 -0700 (PDT) From: Frank van de Pol 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 Message-ID: <20261019174445.1931263-13-fvdpol@gmail.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261019174445.1931263-1-fvdpol@gmail.com> References: <20261019174445.1931263-1-fvdpol@gmail.com> X-Mailing-List: linux-sound@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 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 --- 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