public inbox for linux-acpi@vger.kernel.org
 help / color / mirror / Atom feed
From: Antheas Kapenekakis <lkml@antheas.dev>
To: dmitry.osipenko@collabora.com
Cc: bob.beckett@collabora.com, bookeldor@gmail.com,
	hadess@hadess.net, jaap@haitsma.org, kernel@collabora.com,
	lennart@poettering.net, linux-acpi@vger.kernel.org,
	linux-kernel@vger.kernel.org, lkml@antheas.dev, mccann@jhu.edu,
	rafael@kernel.org, richard@hughsie.com,
	sebastian.reichel@collabora.com, superm1@kernel.org,
	systemd-devel@lists.freedesktop.org, xaver.hugl@gmail.com
Subject: [RFC v2 09/10] acpi/x86: s2idle: Listen to idle hints to perform MS transitions
Date: Sat, 25 Apr 2026 23:57:33 +0200	[thread overview]
Message-ID: <20260425215734.14116-10-lkml@antheas.dev> (raw)
In-Reply-To: <20260425215734.14116-1-lkml@antheas.dev>

Modern Standby capable devices allow controlling their appearance by
userspace to appear inactive or asleep while the system is still running.
Expose these states to userspace as idle hints, so userspace can
leverage "dark resume" states to perform background tasks while the
system appears asleep.

If userspace is not idle-aware, transition to snooze and back to active
as part of the normal begin() and end() callbacks, so that normal
functionality (e.g., pulsing the power light) is maintained.

In addition, in case we have fired the intent to turn display on
notification and are in the resume idle state, the transition to s2idle
is undefined behavior. Therefore, momentarily transition to active and
back to snooze and emit an error, instead of bailing.

Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
---
 drivers/acpi/Kconfig      |   1 +
 drivers/acpi/x86/s2idle.c | 226 +++++++++++++++++++++++++++++++-------
 2 files changed, 186 insertions(+), 41 deletions(-)

diff --git a/drivers/acpi/Kconfig b/drivers/acpi/Kconfig
index 6f4b545f7377..08622ace9c67 100644
--- a/drivers/acpi/Kconfig
+++ b/drivers/acpi/Kconfig
@@ -14,6 +14,7 @@ menuconfig ACPI
 	select NLS
 	select CRC32
 	select FIRMWARE_TABLE
+	select HINT if X86 && SUSPEND # s2idle idle hint
 	default y if X86
 	help
 	  Advanced Configuration and Power Interface (ACPI) support for 
diff --git a/drivers/acpi/x86/s2idle.c b/drivers/acpi/x86/s2idle.c
index 8b48f999e0e9..357d6f9406dc 100644
--- a/drivers/acpi/x86/s2idle.c
+++ b/drivers/acpi/x86/s2idle.c
@@ -19,6 +19,7 @@
 #include <linux/delay.h>
 #include <linux/device.h>
 #include <linux/dmi.h>
+#include <linux/hint.h>
 #include <linux/suspend.h>
 
 #include "../sleep.h"
@@ -67,6 +68,9 @@ static guid_t lps0_dsm_guid_microsoft;
 static int lps0_dsm_func_mask_microsoft;
 static int lps0_dsm_state;
 
+static enum hint_idle_option current_idle = HINT_IDLE_ACTIVE;
+static enum hint_idle_option presuspend_idle = HINT_IDLE_ACTIVE;
+
 /* Device constraint entry structure */
 struct lpi_device_info {
 	char *name;
@@ -439,9 +443,171 @@ static const struct acpi_device_id amd_hid_ids[] = {
 	{}
 };
 
+static int acpi_s2idle_idle_probe(void *drvdata, unsigned long *choices)
+{
+	if (!lps0_device_handle || sleep_no_lps0)
+		return 0;
+
+	if (lps0_dsm_func_mask_microsoft > 0) {
+		*choices |= BIT(HINT_IDLE_ACTIVE);
+		if (lps0_dsm_func_mask_microsoft &
+		    (1 << ACPI_LPS0_DISPLAY_OFF | 1 << ACPI_LPS0_DISPLAY_ON))
+			*choices |= BIT(HINT_IDLE_INACTIVE);
+		if (lps0_dsm_func_mask_microsoft &
+		    (1 << ACPI_LPS0_SLEEP_ENTRY | 1 << ACPI_LPS0_SLEEP_EXIT))
+			*choices |= BIT(HINT_IDLE_SNOOZE);
+		if (lps0_dsm_func_mask_microsoft &
+		    (1 << ACPI_LPS0_TURN_ON_DISPLAY))
+			*choices |= BIT(HINT_IDLE_RESUME);
+	}
+
+	if (lps0_dsm_func_mask > 0) {
+		*choices |= BIT(HINT_IDLE_ACTIVE);
+		if (acpi_s2idle_vendor_amd()) {
+			if (lps0_dsm_func_mask &
+			    (1 << ACPI_LPS0_DISPLAY_OFF_AMD |
+			     1 << ACPI_LPS0_DISPLAY_ON_AMD))
+				*choices |= BIT(HINT_IDLE_INACTIVE);
+		} else {
+			if (lps0_dsm_func_mask & (1 << ACPI_LPS0_DISPLAY_OFF |
+						  1 << ACPI_LPS0_DISPLAY_ON))
+				*choices |= BIT(HINT_IDLE_INACTIVE);
+		}
+	}
+
+	return 0;
+}
+
+static int acpi_s2idle_idle_get(struct device *dev, enum hint_idle_option *idle)
+{
+	*idle = current_idle;
+	return 0;
+}
+
+static int acpi_s2idle_idle_set(struct device *dev, enum hint_idle_option idle)
+{
+	if (idle >= HINT_IDLE_LAST)
+		return -EINVAL;
+
+	if (idle == current_idle)
+		return 0;
+
+	acpi_handle_debug(lps0_device_handle,
+			  "Idle state transition from %d to %d\n",
+			  current_idle, idle);
+
+	/* Resume can only be entered if we are on the snooze state. */
+	if (idle == HINT_IDLE_RESUME) {
+		if (current_idle != HINT_IDLE_SNOOZE)
+			return -EINVAL;
+
+		if (lps0_dsm_func_mask_microsoft > 0)
+			acpi_sleep_run_lps0_dsm(ACPI_LPS0_TURN_ON_DISPLAY,
+						lps0_dsm_func_mask_microsoft,
+						lps0_dsm_guid_microsoft);
+
+		current_idle = HINT_IDLE_RESUME;
+		return 0;
+	}
+
+	/*
+	 * The system should not be able to re-enter snooze from resume as it
+	 * is undefined behavior. As part of setting the idle to "Resume",
+	 * userspace promised a transition to "Inactive" or "Active".
+	 */
+	if (current_idle == HINT_IDLE_RESUME &&
+	    idle == HINT_IDLE_SNOOZE)
+		return -EINVAL;
+
+	/*
+	 * When leaving snooze, always fire the resume notification first if
+	 * the device supports it. This is to counteract buggy firmware
+	 * (e.g., Lenovo) that expects the resume notification to fire always.
+	 */
+	if (current_idle == HINT_IDLE_SNOOZE && idle < current_idle &&
+	    lps0_dsm_func_mask_microsoft > 0) {
+		acpi_sleep_run_lps0_dsm(ACPI_LPS0_TURN_ON_DISPLAY,
+					lps0_dsm_func_mask_microsoft,
+					lps0_dsm_guid_microsoft);
+	}
+
+	/* Resume is the Snooze state logic-wise. */
+	if (current_idle == HINT_IDLE_RESUME)
+		current_idle = HINT_IDLE_SNOOZE;
+
+	if (current_idle < idle) {
+		for (; current_idle < idle; current_idle++) {
+			switch (current_idle + 1) {
+			case HINT_IDLE_INACTIVE:
+				if (lps0_dsm_func_mask > 0)
+					acpi_sleep_run_lps0_dsm(
+						acpi_s2idle_vendor_amd() ?
+							ACPI_LPS0_DISPLAY_OFF_AMD :
+							ACPI_LPS0_DISPLAY_OFF,
+						lps0_dsm_func_mask,
+						lps0_dsm_guid);
+
+				if (lps0_dsm_func_mask_microsoft > 0)
+					acpi_sleep_run_lps0_dsm(
+						ACPI_LPS0_DISPLAY_OFF,
+						lps0_dsm_func_mask_microsoft,
+						lps0_dsm_guid_microsoft);
+				break;
+			case HINT_IDLE_SNOOZE:
+				if (lps0_dsm_func_mask_microsoft > 0)
+					acpi_sleep_run_lps0_dsm(
+						ACPI_LPS0_SLEEP_ENTRY,
+						lps0_dsm_func_mask_microsoft,
+						lps0_dsm_guid_microsoft);
+				break;
+			default:
+				break;
+			}
+		}
+	} else if (current_idle > idle) {
+		for (; current_idle > idle; current_idle--) {
+			switch (current_idle) {
+			case HINT_IDLE_INACTIVE:
+				if (lps0_dsm_func_mask > 0)
+					acpi_sleep_run_lps0_dsm(
+						acpi_s2idle_vendor_amd() ?
+							ACPI_LPS0_DISPLAY_ON_AMD :
+							ACPI_LPS0_DISPLAY_ON,
+						lps0_dsm_func_mask,
+						lps0_dsm_guid);
+				if (lps0_dsm_func_mask_microsoft > 0)
+					acpi_sleep_run_lps0_dsm(
+						ACPI_LPS0_DISPLAY_ON,
+						lps0_dsm_func_mask_microsoft,
+						lps0_dsm_guid_microsoft);
+				break;
+			case HINT_IDLE_SNOOZE:
+				if (lps0_dsm_func_mask_microsoft > 0)
+					acpi_sleep_run_lps0_dsm(
+						ACPI_LPS0_SLEEP_EXIT,
+						lps0_dsm_func_mask_microsoft,
+						lps0_dsm_guid_microsoft);
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	return 0;
+}
+
+static struct hint_ops acpi_s2idle_hint_ops = {
+	.idle_probe = acpi_s2idle_idle_probe,
+	.idle_get = acpi_s2idle_idle_get,
+	.idle_set = acpi_s2idle_idle_set,
+};
+
 static int lps0_device_attach(struct acpi_device *adev,
 			      const struct acpi_device_id *not_used)
 {
+	struct device *hdev;
+
 	if (lps0_device_handle)
 		return 0;
 
@@ -508,6 +674,15 @@ static int lps0_device_attach(struct acpi_device *adev,
 	 */
 	acpi_ec_mark_gpe_for_wake();
 
+	/*
+	 * Add idle hint handler to lps0_device_handle.
+	 */
+	hdev = devm_hint_register(&adev->dev, "s2idle", NULL,
+				  &acpi_s2idle_hint_ops);
+	if (IS_ERR(hdev))
+		acpi_handle_err(adev->handle,
+				"Failed to register idle hint device\n");
+
 	return 0;
 }
 
@@ -538,23 +713,14 @@ static int acpi_s2idle_begin_lps0(void)
 			lpi_constraints_table = ERR_PTR(-ENODATA);
 	}
 
-	/* Display off */
-	if (lps0_dsm_func_mask > 0)
-		acpi_sleep_run_lps0_dsm(acpi_s2idle_vendor_amd() ?
-					ACPI_LPS0_DISPLAY_OFF_AMD :
-					ACPI_LPS0_DISPLAY_OFF,
-					lps0_dsm_func_mask, lps0_dsm_guid);
-
-	if (lps0_dsm_func_mask_microsoft > 0)
-		acpi_sleep_run_lps0_dsm(ACPI_LPS0_DISPLAY_OFF,
-					lps0_dsm_func_mask_microsoft,
-					lps0_dsm_guid_microsoft);
-
-	/* Modern Standby entry */
-	if (lps0_dsm_func_mask_microsoft > 0)
-		acpi_sleep_run_lps0_dsm(ACPI_LPS0_SLEEP_ENTRY,
-					lps0_dsm_func_mask_microsoft,
-					lps0_dsm_guid_microsoft);
+	presuspend_idle = current_idle;
+	if (current_idle == HINT_IDLE_RESUME) {
+		acpi_handle_err(
+			lps0_device_handle,
+			"Unexpected idle state: Resume. Transitioning to active and back.\n");
+		acpi_s2idle_idle_set(NULL, HINT_IDLE_ACTIVE);
+	}
+	acpi_s2idle_idle_set(NULL, HINT_IDLE_SNOOZE);
 
 	list_for_each_entry(handler, &lps0_s2idle_devops_head, list_node) {
 		if (handler->begin_delay && handler->begin_delay > delay)
@@ -636,30 +802,8 @@ static void acpi_s2idle_end_lps0(void)
 {
 	acpi_s2idle_end();
 
-	if (!lps0_device_handle || sleep_no_lps0)
-		return;
-
-	if (lps0_dsm_func_mask_microsoft > 0) {
-		/* Intent to turn on display */
-		acpi_sleep_run_lps0_dsm(ACPI_LPS0_TURN_ON_DISPLAY,
-					lps0_dsm_func_mask_microsoft,
-					lps0_dsm_guid_microsoft);
-		/* Modern Standby exit */
-		acpi_sleep_run_lps0_dsm(ACPI_LPS0_SLEEP_EXIT,
-					lps0_dsm_func_mask_microsoft,
-					lps0_dsm_guid_microsoft);
-	}
-
-	/* Display on */
-	if (lps0_dsm_func_mask_microsoft > 0)
-		acpi_sleep_run_lps0_dsm(ACPI_LPS0_DISPLAY_ON,
-					lps0_dsm_func_mask_microsoft,
-					lps0_dsm_guid_microsoft);
-	if (lps0_dsm_func_mask > 0)
-		acpi_sleep_run_lps0_dsm(acpi_s2idle_vendor_amd() ?
-					ACPI_LPS0_DISPLAY_ON_AMD :
-					ACPI_LPS0_DISPLAY_ON,
-					lps0_dsm_func_mask, lps0_dsm_guid);
+	if (lps0_device_handle && !sleep_no_lps0)
+		acpi_s2idle_idle_set(NULL, presuspend_idle);
 }
 
 static const struct platform_s2idle_ops acpi_s2idle_ops_lps0 = {
-- 
2.53.0



  parent reply	other threads:[~2026-04-25 21:58 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-25 21:57 [RFC v2 00/10] acpi/x86: s2idle: Introduce and implement hint class ABI and idle hint for s2idle Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 01/10] acpi/x86: s2idle: Rename LPS0 constants so they mirror their function Antheas Kapenekakis
2026-04-26 14:14   ` Rafael J. Wysocki
2026-04-26 16:54     ` Antheas Kapenekakis
2026-04-27 15:07       ` Rafael J. Wysocki
2026-04-25 21:57 ` [RFC v2 02/10] acpi/x86: s2idle: Move Modern Standby calls to s2idle begin/end Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 03/10] acpi/x86: s2idle: Add support for adding a delay after begin MS calls Antheas Kapenekakis
2026-04-28  1:57   ` Mario Limonciello
2026-04-28  7:47     ` Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 04/10] platform/x86: asus-wmi: add s2idle begin delay for Ally devices Antheas Kapenekakis
2026-04-28  1:56   ` Mario Limonciello
2026-04-28  6:34   ` [systemd-devel] " Paul Menzel
2026-04-28  8:18     ` Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 05/10] HID: asus: remove quirk handling " Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 06/10] platform/x86: asus-wmi: Remove Ally s2idle resume fixes Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 07/10] Documentation: Add documentation for the new sysfs hints class Antheas Kapenekakis
2026-04-25 21:57 ` [RFC v2 08/10] hint: Add hint class ABI for devices to receive updates on host activity Antheas Kapenekakis
2026-04-25 21:57 ` Antheas Kapenekakis [this message]
2026-04-25 21:57 ` [RFC v2 10/10] acpi/x86: s2idle: Subtract delay from last DSM fire in begin delay Antheas Kapenekakis

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=20260425215734.14116-10-lkml@antheas.dev \
    --to=lkml@antheas.dev \
    --cc=bob.beckett@collabora.com \
    --cc=bookeldor@gmail.com \
    --cc=dmitry.osipenko@collabora.com \
    --cc=hadess@hadess.net \
    --cc=jaap@haitsma.org \
    --cc=kernel@collabora.com \
    --cc=lennart@poettering.net \
    --cc=linux-acpi@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mccann@jhu.edu \
    --cc=rafael@kernel.org \
    --cc=richard@hughsie.com \
    --cc=sebastian.reichel@collabora.com \
    --cc=superm1@kernel.org \
    --cc=systemd-devel@lists.freedesktop.org \
    --cc=xaver.hugl@gmail.com \
    /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