git: c5daa5a4c32c - main - acpi_spmc: Add system power management controller driver

From: Aymeric Wibo <obiwac_at_FreeBSD.org>
Date: Mon, 26 Jan 2026 13:42:54 UTC
The branch main has been updated by obiwac:

URL: https://cgit.FreeBSD.org/src/commit/?id=c5daa5a4c32c9b1ecb506ddf1a80579c93c3ea6d

commit c5daa5a4c32c9b1ecb506ddf1a80579c93c3ea6d
Author:     Aymeric Wibo <obiwac@FreeBSD.org>
AuthorDate: 2025-06-14 15:30:44 +0000
Commit:     Aymeric Wibo <obiwac@FreeBSD.org>
CommitDate: 2026-01-26 13:42:11 +0000

    acpi_spmc: Add system power management controller driver
    
    Add SPMC (system power management controller) driver as acpi_spmc. This
    is the device which provides the LPI device D-state constraints and
    allows for OSPM to send S0ix/modern standby entry/exit notifications.
    This supports the original Intel DSM
    (https://uefi.org/sites/default/files/resources/Intel_ACPI_Low_Power_S0_Idle.pdf,
    untested), the AMD DSM (tested), and the Microsoft DSM (tested).
    
    Before entry, acpi_spmc_check_constraints is called to notify of any
    violated power constraints. This will use acpi_pwr_get_state to get
    current device D-states when that gets added back.
    
    Reviewed by:    olce
    Tested by:      jkim, Oleksandr Kryvulia, Matthias Lanter
    Approved by:    olce
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D48387
---
 share/man/man4/acpi.4      |   4 +-
 sys/conf/files             |   1 +
 sys/dev/acpica/acpi.c      |   1 +
 sys/dev/acpica/acpi_spmc.c | 618 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 623 insertions(+), 1 deletion(-)

diff --git a/share/man/man4/acpi.4 b/share/man/man4/acpi.4
index cdad3ceeedfc..9c24635c561d 100644
--- a/share/man/man4/acpi.4
+++ b/share/man/man4/acpi.4
@@ -486,10 +486,12 @@ Embedded controller driver
 Fan driver
 .It Li ACPI_OEM
 Platform-specific driver for hotkeys, LED, etc.
-.It Li ACPI_POWER
+.It Li ACPI_POWERRES
 Power resource driver
 .It Li ACPI_PROCESSOR
 CPU driver
+.It Li ACPI_SPMC
+System power management controller driver
 .It Li ACPI_THERMAL
 Thermal zone driver
 .It Li ACPI_TIMER
diff --git a/sys/conf/files b/sys/conf/files
index 66b84fea1bc0..97834f05431d 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -776,6 +776,7 @@ dev/acpica/acpi_thermal.c	optional acpi
 dev/acpica/acpi_throttle.c	optional acpi
 dev/acpica/acpi_video.c		optional acpi_video acpi
 dev/acpica/acpi_dock.c		optional acpi_dock acpi
+dev/acpica/acpi_spmc.c		optional acpi
 dev/adlink/adlink.c		optional adlink
 dev/ae/if_ae.c			optional ae pci
 dev/age/if_age.c		optional age pci
diff --git a/sys/dev/acpica/acpi.c b/sys/dev/acpica/acpi.c
index c76bc8477537..e43ef72ca9d2 100644
--- a/sys/dev/acpica/acpi.c
+++ b/sys/dev/acpica/acpi.c
@@ -4618,6 +4618,7 @@ static struct debugtag	dbg_layer[] = {
     {"ACPI_FAN",		ACPI_FAN},
     {"ACPI_POWERRES",		ACPI_POWERRES},
     {"ACPI_PROCESSOR",		ACPI_PROCESSOR},
+    {"ACPI_SPMC",		ACPI_SPMC},
     {"ACPI_THERMAL",		ACPI_THERMAL},
     {"ACPI_TIMER",		ACPI_TIMER},
     {"ACPI_ALL_DRIVERS",	ACPI_ALL_DRIVERS},
diff --git a/sys/dev/acpica/acpi_spmc.c b/sys/dev/acpica/acpi_spmc.c
new file mode 100644
index 000000000000..57593d9ccae1
--- /dev/null
+++ b/sys/dev/acpica/acpi_spmc.c
@@ -0,0 +1,618 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024-2025 The FreeBSD Foundation
+ *
+ * This software was developed by Aymeric Wibo <obiwac@freebsd.org>
+ * under sponsorship from the FreeBSD Foundation.
+ */
+
+#include <sys/param.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/bus.h>
+#include <sys/malloc.h>
+#include <sys/uuid.h>
+#include <sys/kdb.h>
+
+#include <contrib/dev/acpica/include/acpi.h>
+#include <contrib/dev/acpica/include/accommon.h>
+
+#include <dev/acpica/acpivar.h>
+
+/* Hooks for the ACPI CA debugging infrastructure */
+#define _COMPONENT	ACPI_SPMC
+ACPI_MODULE_NAME("SPMC")
+
+static SYSCTL_NODE(_debug_acpi, OID_AUTO, spmc, CTLFLAG_RD | CTLFLAG_MPSAFE,
+    NULL, "SPMC debugging");
+
+static char *spmc_ids[] = {
+	"PNP0D80",
+	NULL
+};
+
+enum intel_dsm_index {
+	DSM_ENUM_FUNCTIONS		= 0,
+	DSM_GET_DEVICE_CONSTRAINTS	= 1,
+	DSM_GET_CRASH_DUMP_DEVICE	= 2,
+	DSM_DISPLAY_OFF_NOTIF		= 3,
+	DSM_DISPLAY_ON_NOTIF		= 4,
+	DSM_ENTRY_NOTIF			= 5,
+	DSM_EXIT_NOTIF			= 6,
+	/* Only for Microsoft DSM set. */
+	DSM_MODERN_ENTRY_NOTIF		= 7,
+	DSM_MODERN_EXIT_NOTIF		= 8,
+};
+
+enum amd_dsm_index {
+	AMD_DSM_ENUM_FUNCTIONS		= 0,
+	AMD_DSM_GET_DEVICE_CONSTRAINTS	= 1,
+	AMD_DSM_ENTRY_NOTIF		= 2,
+	AMD_DSM_EXIT_NOTIF		= 3,
+	AMD_DSM_DISPLAY_OFF_NOTIF	= 4,
+	AMD_DSM_DISPLAY_ON_NOTIF	= 5,
+};
+
+enum dsm_set_flags {
+	DSM_SET_INTEL	= 1 << 0,
+	DSM_SET_MS	= 1 << 1,
+	DSM_SET_AMD	= 1 << 2,
+};
+
+struct dsm_set {
+	enum dsm_set_flags	flag;
+	const char		*name;
+	int			revision;
+	struct uuid		uuid;
+	uint64_t		dsms_expected;
+};
+
+static struct dsm_set intel_dsm_set = {
+	.flag = DSM_SET_INTEL,
+	.name = "Intel",
+	/*
+	 * XXX Linux uses 1 for the revision on Intel DSMs, but doesn't explain
+	 * why.  The commit that introduces this links to a document mentioning
+	 * revision 0, so default this to 0.
+	 *
+	 * The debug.acpi.spmc.intel_dsm_revision sysctl may be used to configure
+	 * this just in case.
+	 */
+	.revision = 0,
+	.uuid = { /* c4eb40a0-6cd2-11e2-bcfd-0800200c9a66 */
+		0xc4eb40a0, 0x6cd2, 0x11e2, 0xbc, 0xfd,
+		{0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66},
+	},
+	.dsms_expected = DSM_GET_DEVICE_CONSTRAINTS | DSM_DISPLAY_OFF_NOTIF |
+	    DSM_DISPLAY_ON_NOTIF | DSM_ENTRY_NOTIF | DSM_EXIT_NOTIF,
+};
+
+SYSCTL_INT(_debug_acpi_spmc, OID_AUTO, intel_dsm_revision, CTLFLAG_RW,
+    &intel_dsm_set.revision, 0,
+    "Revision to use when evaluating Intel SPMC DSMs");
+
+static struct dsm_set ms_dsm_set = {
+	.flag = DSM_SET_MS,
+	.name = "Microsoft",
+	.revision = 0,
+	.uuid = { /* 11e00d56-ce64-47ce-837b-1f898f9aa461 */
+		0x11e00d56, 0xce64, 0x47ce, 0x83, 0x7b,
+		{0x1f, 0x89, 0x8f, 0x9a, 0xa4, 0x61},
+	},
+	.dsms_expected = DSM_DISPLAY_OFF_NOTIF | DSM_DISPLAY_ON_NOTIF |
+	    DSM_ENTRY_NOTIF | DSM_EXIT_NOTIF | DSM_MODERN_ENTRY_NOTIF |
+	    DSM_MODERN_EXIT_NOTIF,
+};
+
+static struct dsm_set amd_dsm_set = {
+	.flag = DSM_SET_AMD,
+	.name = "AMD",
+	/*
+	 * XXX Linux uses 0 for the revision on AMD DSMs, but at least on the
+	 * Framework 13 AMD 7040 series, the enum functions DSM only returns a
+	 * function mask that covers all the DSMs we need to call when called
+	 * with revision 2.
+	 *
+	 * The debug.acpi.spmc.amd_dsm_revision sysctl may be used to configure
+	 * this just in case.
+	 */
+	.revision = 2,
+	.uuid = { /* e3f32452-febc-43ce-9039-932122d37721 */
+		0xe3f32452, 0xfebc, 0x43ce, 0x90, 0x39,
+		{0x93, 0x21, 0x22, 0xd3, 0x77, 0x21},
+	},
+	.dsms_expected = AMD_DSM_GET_DEVICE_CONSTRAINTS | AMD_DSM_ENTRY_NOTIF |
+	    AMD_DSM_EXIT_NOTIF | AMD_DSM_DISPLAY_OFF_NOTIF |
+	    AMD_DSM_DISPLAY_ON_NOTIF,
+};
+
+SYSCTL_INT(_debug_acpi_spmc, OID_AUTO, amd_dsm_revision, CTLFLAG_RW,
+    &amd_dsm_set.revision, 0, "Revision to use when evaluating AMD SPMC DSMs");
+
+union dsm_index {
+	int			i;
+	enum intel_dsm_index	regular;
+	enum amd_dsm_index	amd;
+};
+
+struct acpi_spmc_constraint {
+	bool		enabled;
+	char		*name;
+	int		min_d_state;
+	ACPI_HANDLE	handle;
+
+	/* Unused, spec only. */
+	uint64_t	lpi_uid;
+	uint64_t	min_dev_specific_state;
+
+	/* Unused, AMD only. */
+	uint64_t	function_states;
+};
+
+struct acpi_spmc_softc {
+	device_t		dev;
+	ACPI_HANDLE		handle;
+	ACPI_OBJECT		*obj;
+	enum dsm_set_flags	dsm_sets;
+
+	bool				constraints_populated;
+	size_t				constraint_count;
+	struct acpi_spmc_constraint	*constraints;
+};
+
+static void	acpi_spmc_check_dsm_set(struct acpi_spmc_softc *sc,
+		    ACPI_HANDLE handle, struct dsm_set *dsm_set);
+static int	acpi_spmc_get_constraints(device_t dev);
+static void	acpi_spmc_free_constraints(struct acpi_spmc_softc *sc);
+
+static int
+acpi_spmc_probe(device_t dev)
+{
+	char			*name;
+	ACPI_HANDLE		handle;
+	struct acpi_spmc_softc	*sc;
+
+	/* Check that this is an enabled device. */
+	if (acpi_get_type(dev) != ACPI_TYPE_DEVICE || acpi_disabled("spmc"))
+		return (ENXIO);
+
+	if (ACPI_ID_PROBE(device_get_parent(dev), dev, spmc_ids, &name) > 0)
+		return (ENXIO);
+
+	handle = acpi_get_handle(dev);
+	if (handle == NULL)
+		return (ENXIO);
+
+	sc = device_get_softc(dev);
+
+	/* Check which sets of DSM's are supported. */
+	sc->dsm_sets = 0;
+
+	acpi_spmc_check_dsm_set(sc, handle, &intel_dsm_set);
+	acpi_spmc_check_dsm_set(sc, handle, &ms_dsm_set);
+	acpi_spmc_check_dsm_set(sc, handle, &amd_dsm_set);
+
+	if (sc->dsm_sets == 0)
+		return (ENXIO);
+
+	device_set_descf(dev, "Low Power S0 Idle (DSM sets 0x%x)",
+	    sc->dsm_sets);
+
+	return (0);
+}
+
+static int
+acpi_spmc_attach(device_t dev)
+{
+	struct acpi_spmc_softc *sc;
+
+	sc = device_get_softc(dev);
+	sc->dev = dev;
+
+	sc->handle = acpi_get_handle(dev);
+	if (sc->handle == NULL)
+		return (ENXIO);
+
+	sc->constraints_populated = false;
+	sc->constraint_count = 0;
+	sc->constraints = NULL;
+
+	/* Get device constraints. We can only call this once so do this now. */
+	acpi_spmc_get_constraints(sc->dev);
+
+	return (0);
+}
+
+static int
+acpi_spmc_detach(device_t dev)
+{
+	acpi_spmc_free_constraints(device_get_softc(dev));
+	return (0);
+}
+
+static void
+acpi_spmc_check_dsm_set(struct acpi_spmc_softc *sc, ACPI_HANDLE handle,
+    struct dsm_set *dsm_set)
+{
+	const uint64_t dsms_supported = acpi_DSMQuery(handle,
+	    (uint8_t *)&dsm_set->uuid, dsm_set->revision);
+
+	/*
+	 * Check if DSM set supported at all.  We do this by checking the
+	 * existence of "enum functions".
+	 */
+	if ((dsms_supported & 1) == 0)
+		return;
+	if ((dsms_supported & dsm_set->dsms_expected)
+	    != dsm_set->dsms_expected) {
+		device_printf(sc->dev, "DSM set %s does not support expected "
+		    "DSMs (0x%lx vs 0x%lx). Some methods may fail.\n",
+		    dsm_set->name, dsms_supported, dsm_set->dsms_expected);
+	}
+	sc->dsm_sets |= dsm_set->flag;
+}
+
+static void
+acpi_spmc_free_constraints(struct acpi_spmc_softc *sc)
+{
+	if (sc->constraints == NULL)
+		return;
+
+	for (size_t i = 0; i < sc->constraint_count; i++) {
+		if (sc->constraints[i].name != NULL)
+			free(sc->constraints[i].name, M_TEMP);
+	}
+
+	free(sc->constraints, M_TEMP);
+	sc->constraints = NULL;
+}
+
+static int
+acpi_spmc_get_constraints_spec(struct acpi_spmc_softc *sc, ACPI_OBJECT *object)
+{
+	struct acpi_spmc_constraint *constraint;
+	int		revision;
+	ACPI_OBJECT	*constraint_obj;
+	ACPI_OBJECT	*name_obj;
+	ACPI_OBJECT	*detail;
+	ACPI_OBJECT	*constraint_package;
+
+	KASSERT(sc->constraints_populated == false,
+	    ("constraints already populated"));
+
+	sc->constraint_count = object->Package.Count;
+	sc->constraints = malloc(sc->constraint_count * sizeof *sc->constraints,
+	    M_TEMP, M_WAITOK | M_ZERO);
+
+	/*
+	 * The value of sc->constraint_count can change during the loop, so
+	 * iterate until object->Package.Count so we actually go over all
+	 * elements in the package.
+	 */
+	for (size_t i = 0; i < object->Package.Count; i++) {
+		constraint_obj = &object->Package.Elements[i];
+		constraint = &sc->constraints[i];
+
+		constraint->enabled =
+		    constraint_obj->Package.Elements[1].Integer.Value;
+
+		name_obj = &constraint_obj->Package.Elements[0];
+		constraint->name = strdup(name_obj->String.Pointer, M_TEMP);
+		if (constraint->name == NULL) {
+			acpi_spmc_free_constraints(sc);
+			return (ENOMEM);
+		}
+
+		/*
+		 * The first element in the device constraint detail package is
+		 * the revision, and should always be zero.
+		 */
+		revision = constraint_obj->Package.Elements[0].Integer.Value;
+		if (revision != 0) {
+			device_printf(sc->dev, "Unknown revision %d for "
+			    "device constraint detail package\n", revision);
+			sc->constraint_count--;
+			continue;
+		}
+
+		detail = &constraint_obj->Package.Elements[2];
+		constraint_package = &detail->Package.Elements[1];
+
+		constraint->lpi_uid =
+		    constraint_package->Package.Elements[0].Integer.Value;
+		constraint->min_d_state =
+		    constraint_package->Package.Elements[1].Integer.Value;
+		constraint->min_dev_specific_state =
+		    constraint_package->Package.Elements[2].Integer.Value;
+	}
+
+	sc->constraints_populated = true;
+	return (0);
+}
+
+static int
+acpi_spmc_get_constraints_amd(struct acpi_spmc_softc *sc, ACPI_OBJECT *object)
+{
+	size_t		constraint_count;
+	ACPI_OBJECT	*constraint_obj;
+	ACPI_OBJECT	*constraints;
+	struct acpi_spmc_constraint *constraint;
+	ACPI_OBJECT	*name_obj;
+
+	KASSERT(sc->constraints_populated == false,
+	    ("constraints already populated"));
+
+	/*
+	 * First element in the package is unknown.
+	 * Second element is the number of device constraints.
+	 * Third element is the list of device constraints itself.
+	 */
+	constraint_count = object->Package.Elements[1].Integer.Value;
+	constraints = &object->Package.Elements[2];
+
+	if (constraints->Package.Count != constraint_count) {
+		device_printf(sc->dev, "constraint count mismatch (%d to %zu)\n",
+		    constraints->Package.Count, constraint_count);
+		return (ENXIO);
+	}
+
+	sc->constraint_count = constraint_count;
+	sc->constraints = malloc(constraint_count * sizeof *sc->constraints,
+	    M_TEMP, M_WAITOK | M_ZERO);
+
+	for (size_t i = 0; i < constraint_count; i++) {
+		/* Parse the constraint package. */
+		constraint_obj = &constraints->Package.Elements[i];
+		if (constraint_obj->Package.Count != 4) {
+			device_printf(sc->dev, "constraint %zu has %d elements\n",
+			    i, constraint_obj->Package.Count);
+			acpi_spmc_free_constraints(sc);
+			return (ENXIO);
+		}
+
+		constraint = &sc->constraints[i];
+		constraint->enabled =
+		    constraint_obj->Package.Elements[0].Integer.Value;
+
+		name_obj = &constraint_obj->Package.Elements[1];
+		constraint->name = strdup(name_obj->String.Pointer, M_TEMP);
+		if (constraint->name == NULL) {
+			acpi_spmc_free_constraints(sc);
+			return (ENOMEM);
+		}
+
+		constraint->function_states =
+		    constraint_obj->Package.Elements[2].Integer.Value;
+		constraint->min_d_state =
+		    constraint_obj->Package.Elements[3].Integer.Value;
+	}
+
+	sc->constraints_populated = true;
+	return (0);
+}
+
+static int
+acpi_spmc_get_constraints(device_t dev)
+{
+	struct acpi_spmc_softc	*sc;
+	union dsm_index		dsm_index;
+	struct dsm_set		*dsm_set;
+	ACPI_STATUS		status;
+	ACPI_BUFFER		result;
+	ACPI_OBJECT		*object;
+	bool			is_amd;
+	int			rv;
+	struct acpi_spmc_constraint *constraint;
+
+	sc = device_get_softc(dev);
+	if (sc->constraints_populated)
+		return (0);
+
+	/* The Microsoft DSM set doesn't have this DSM. */
+	is_amd = (sc->dsm_sets & DSM_SET_AMD) != 0;
+	if (is_amd) {
+		dsm_set = &amd_dsm_set;
+		dsm_index.amd = AMD_DSM_GET_DEVICE_CONSTRAINTS;
+	} else {
+		dsm_set = &intel_dsm_set;
+		dsm_index.regular = DSM_GET_DEVICE_CONSTRAINTS;
+	}
+
+	/* XXX It seems like this DSM fails if called more than once. */
+	status = acpi_EvaluateDSMTyped(sc->handle, (uint8_t *)&dsm_set->uuid,
+	    dsm_set->revision, dsm_index.i, NULL, &result,
+	    ACPI_TYPE_PACKAGE);
+	if (ACPI_FAILURE(status)) {
+		device_printf(dev, "%s failed to call %s DSM %d (rev %d)\n",
+		    __func__, dsm_set->name, dsm_index.i, dsm_set->revision);
+		return (ENXIO);
+	}
+
+	object = (ACPI_OBJECT *)result.Pointer;
+	if (is_amd)
+		rv = acpi_spmc_get_constraints_amd(sc, object);
+	else
+		rv = acpi_spmc_get_constraints_spec(sc, object);
+	AcpiOsFree(object);
+	if (rv != 0)
+		return (rv);
+
+	/* Get handles for each constraint device. */
+	for (size_t i = 0; i < sc->constraint_count; i++) {
+		constraint = &sc->constraints[i];
+
+		status = acpi_GetHandleInScope(sc->handle,
+		    __DECONST(char *, constraint->name), &constraint->handle);
+		if (ACPI_FAILURE(status)) {
+			device_printf(dev, "failed to get handle for %s\n",
+			    constraint->name);
+			constraint->handle = NULL;
+		}
+	}
+	return (0);
+}
+
+static void
+acpi_spmc_check_constraints(struct acpi_spmc_softc *sc)
+{
+	bool violation = false;
+
+	KASSERT(sc->constraints_populated, ("constraints not populated"));
+	for (size_t i = 0; i < sc->constraint_count; i++) {
+		struct acpi_spmc_constraint *constraint = &sc->constraints[i];
+
+		if (!constraint->enabled)
+			continue;
+		if (constraint->handle == NULL)
+			continue;
+
+		ACPI_STATUS status = acpi_GetHandleInScope(sc->handle,
+		    __DECONST(char *, constraint->name), &constraint->handle);
+		if (ACPI_FAILURE(status)) {
+			device_printf(sc->dev, "failed to get handle for %s\n",
+			    constraint->name);
+			constraint->handle = NULL;
+		}
+		if (constraint->handle == NULL)
+			continue;
+
+#ifdef notyet
+		int d_state;
+		if (ACPI_FAILURE(acpi_pwr_get_state(constraint->handle, &d_state)))
+			continue;
+		if (d_state < constraint->min_d_state) {
+			device_printf(sc->dev, "constraint for device %s"
+			    " violated (minimum D-state required was %s, actual"
+			    " D-state is %s), might fail to enter LPI state\n",
+			    constraint->name,
+			    acpi_d_state_to_str(constraint->min_d_state),
+			    acpi_d_state_to_str(d_state));
+			violation = true;
+		}
+#endif
+	}
+	if (!violation)
+		device_printf(sc->dev,
+		    "all device power constraints respected!\n");
+}
+
+static void
+acpi_spmc_run_dsm(device_t dev, struct dsm_set *dsm_set, int index)
+{
+	struct acpi_spmc_softc	*sc;
+	ACPI_STATUS		status;
+	ACPI_BUFFER		result;
+
+	sc = device_get_softc(dev);
+
+	status = acpi_EvaluateDSMTyped(sc->handle, (uint8_t *)&dsm_set->uuid,
+	    dsm_set->revision, index, NULL, &result, ACPI_TYPE_ANY);
+
+	if (ACPI_FAILURE(status)) {
+		device_printf(dev, "%s failed to call %s DSM %d (rev %d)\n",
+		    __func__, dsm_set->name, index, dsm_set->revision);
+		return;
+	}
+
+	AcpiOsFree(result.Pointer);
+}
+
+/*
+ * Try running the DSMs from all the DSM sets we have, as them failing costs us
+ * nothing, and it seems like on AMD platforms, both the AMD entry and Microsoft
+ * "modern" DSM's are required for it to enter modern standby.
+ *
+ * This is what Linux does too.
+ */
+static void
+acpi_spmc_display_off_notif(device_t dev)
+{
+	struct acpi_spmc_softc *sc = device_get_softc(dev);
+
+	if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
+		acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_DISPLAY_OFF_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_MS) != 0)
+		acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_DISPLAY_OFF_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_AMD) != 0)
+		acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_DISPLAY_OFF_NOTIF);
+}
+
+static void
+acpi_spmc_display_on_notif(device_t dev)
+{
+	struct acpi_spmc_softc *sc = device_get_softc(dev);
+
+	if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
+		acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_DISPLAY_ON_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_MS) != 0)
+		acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_DISPLAY_ON_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_AMD) != 0)
+		acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_DISPLAY_ON_NOTIF);
+}
+
+static void
+acpi_spmc_entry_notif(device_t dev)
+{
+	struct acpi_spmc_softc *sc = device_get_softc(dev);
+
+	acpi_spmc_check_constraints(sc);
+
+	if ((sc->dsm_sets & DSM_SET_AMD) != 0)
+		acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_ENTRY_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_MS) != 0) {
+		acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_MODERN_ENTRY_NOTIF);
+		acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_ENTRY_NOTIF);
+	}
+	if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
+		acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_ENTRY_NOTIF);
+}
+
+static void
+acpi_spmc_exit_notif(device_t dev)
+{
+	struct acpi_spmc_softc *sc = device_get_softc(dev);
+
+	if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
+		acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_EXIT_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_AMD) != 0)
+		acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_EXIT_NOTIF);
+	if ((sc->dsm_sets & DSM_SET_MS) != 0) {
+		acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_EXIT_NOTIF);
+		acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_MODERN_EXIT_NOTIF);
+	}
+}
+
+static int
+acpi_spmc_suspend(device_t dev)
+{
+	acpi_spmc_display_off_notif(dev);
+	acpi_spmc_entry_notif(dev);
+
+	return (0);
+}
+
+static int
+acpi_spmc_resume(device_t dev)
+{
+	acpi_spmc_exit_notif(dev);
+	acpi_spmc_display_on_notif(dev);
+
+	return (0);
+}
+
+static device_method_t acpi_spmc_methods[] = {
+	DEVMETHOD(device_probe,		acpi_spmc_probe),
+	DEVMETHOD(device_attach,	acpi_spmc_attach),
+	DEVMETHOD(device_detach,	acpi_spmc_detach),
+	DEVMETHOD_END
+};
+
+static driver_t acpi_spmc_driver = {
+	"acpi_spmc",
+	acpi_spmc_methods,
+	sizeof(struct acpi_spmc_softc),
+};
+
+DRIVER_MODULE_ORDERED(acpi_spmc, acpi, acpi_spmc_driver, NULL, NULL, SI_ORDER_ANY);
+MODULE_DEPEND(acpi_spmc, acpi, 1, 1, 1);