diff mbox series

[1/3] ACPI: platform_profile: Add support for hidden choices

Message ID 20250228170155.2623386-2-superm1@kernel.org (mailing list archive)
State Handled Elsewhere, archived
Headers show
Series Add support for hidden choices to platform_profile | expand

Commit Message

Mario Limonciello Feb. 28, 2025, 5:01 p.m. UTC
From: Mario Limonciello <mario.limonciello@amd.com>

When two drivers don't support all the same profiles the legacy interface
only exports the common profiles.

This causes problems for cases where one driver uses low-power but another
uses quiet because the result is that neither is exported to sysfs.

To allow two drivers to disagree, add support for "hidden choices".
Hidden choices are platform profiles that a driver supports to be
compatible with the platform profile of another driver.

Fixes: 688834743d67 ("ACPI: platform_profile: Allow multiple handlers")
Reported-by: Antheas Kapenekakis <lkml@antheas.dev>
Closes: https://lore.kernel.org/platform-driver-x86/e64b771e-3255-42ad-9257-5b8fc6c24ac9@gmx.de/T/#mc068042dd29df36c16c8af92664860fc4763974b
Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
---
Cc: "Luke D. Jones" <luke@ljones.dev>
 drivers/acpi/platform_profile.c  | 94 +++++++++++++++++++++++++-------
 include/linux/platform_profile.h |  3 +
 2 files changed, 76 insertions(+), 21 deletions(-)

Comments

Antheas Kapenekakis Feb. 28, 2025, 5:15 p.m. UTC | #1
LGTM. Although patch is a bit more complicated.

I do not have time to test this today. I can try tomorrow.

On Fri, 28 Feb 2025 at 18:02, Mario Limonciello <superm1@kernel.org> wrote:
>
> From: Mario Limonciello <mario.limonciello@amd.com>
>
> When two drivers don't support all the same profiles the legacy interface
> only exports the common profiles.
>
> This causes problems for cases where one driver uses low-power but another
> uses quiet because the result is that neither is exported to sysfs.
>
> To allow two drivers to disagree, add support for "hidden choices".
> Hidden choices are platform profiles that a driver supports to be
> compatible with the platform profile of another driver.
>
> Fixes: 688834743d67 ("ACPI: platform_profile: Allow multiple handlers")
> Reported-by: Antheas Kapenekakis <lkml@antheas.dev>
> Closes: https://lore.kernel.org/platform-driver-x86/e64b771e-3255-42ad-9257-5b8fc6c24ac9@gmx.de/T/#mc068042dd29df36c16c8af92664860fc4763974b
> Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
> ---
> Cc: "Luke D. Jones" <luke@ljones.dev>
>  drivers/acpi/platform_profile.c  | 94 +++++++++++++++++++++++++-------
>  include/linux/platform_profile.h |  3 +
>  2 files changed, 76 insertions(+), 21 deletions(-)
>
> diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c
> index 2ad53cc6aae53..ef9444482db19 100644
> --- a/drivers/acpi/platform_profile.c
> +++ b/drivers/acpi/platform_profile.c
> @@ -21,9 +21,15 @@ struct platform_profile_handler {
>         struct device dev;
>         int minor;
>         unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +       unsigned long hidden_choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>         const struct platform_profile_ops *ops;
>  };
>
> +struct aggregate_choices_data {
> +       unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +       int count;
> +};
> +
>  static const char * const profile_names[] = {
>         [PLATFORM_PROFILE_LOW_POWER] = "low-power",
>         [PLATFORM_PROFILE_COOL] = "cool",
> @@ -73,7 +79,7 @@ static int _store_class_profile(struct device *dev, void *data)
>
>         lockdep_assert_held(&profile_lock);
>         handler = to_pprof_handler(dev);
> -       if (!test_bit(*bit, handler->choices))
> +       if (!test_bit(*bit, handler->choices) && !test_bit(*bit, handler->hidden_choices))
>                 return -EOPNOTSUPP;
>
>         return handler->ops->profile_set(dev, *bit);
> @@ -239,21 +245,44 @@ static const struct class platform_profile_class = {
>  /**
>   * _aggregate_choices - Aggregate the available profile choices
>   * @dev: The device
> - * @data: The available profile choices
> + * @arg: struct aggregate_choices_data
>   *
>   * Return: 0 on success, -errno on failure
>   */
> -static int _aggregate_choices(struct device *dev, void *data)
> +static int _aggregate_choices(struct device *dev, void *arg)
>  {
> +       unsigned long tmp[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +       struct aggregate_choices_data *data = arg;
>         struct platform_profile_handler *handler;
> -       unsigned long *aggregate = data;
>
>         lockdep_assert_held(&profile_lock);
>         handler = to_pprof_handler(dev);
> -       if (test_bit(PLATFORM_PROFILE_LAST, aggregate))
> -               bitmap_copy(aggregate, handler->choices, PLATFORM_PROFILE_LAST);
> +       bitmap_or(tmp, handler->choices, handler->hidden_choices, PLATFORM_PROFILE_LAST);
> +       if (test_bit(PLATFORM_PROFILE_LAST, data->aggregate))
> +               bitmap_copy(data->aggregate, tmp, PLATFORM_PROFILE_LAST);
>         else
> -               bitmap_and(aggregate, handler->choices, aggregate, PLATFORM_PROFILE_LAST);
> +               bitmap_and(data->aggregate, tmp, data->aggregate, PLATFORM_PROFILE_LAST);
> +       data->count++;
> +
> +       return 0;
> +}
> +
> +/**
> + * _remove_hidden_choices - Remove hidden choices from aggregate data
> + * @dev: The device
> + * @arg: struct aggregate_choices_data
> + *
> + * Return: 0 on success, -errno on failure
> + */
> +static int _remove_hidden_choices(struct device *dev, void *arg)
> +{
> +       struct aggregate_choices_data *data = arg;
> +       struct platform_profile_handler *handler;
> +
> +       lockdep_assert_held(&profile_lock);
> +       handler = to_pprof_handler(dev);
> +       bitmap_andnot(data->aggregate, handler->choices,
> +                     handler->hidden_choices, PLATFORM_PROFILE_LAST);
>
>         return 0;
>  }
> @@ -270,22 +299,31 @@ static ssize_t platform_profile_choices_show(struct device *dev,
>                                              struct device_attribute *attr,
>                                              char *buf)
>  {
> -       unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +       struct aggregate_choices_data data = {
> +               .aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> +               .count = 0,
> +       };
>         int err;
>
> -       set_bit(PLATFORM_PROFILE_LAST, aggregate);
> +       set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>         scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>                 err = class_for_each_device(&platform_profile_class, NULL,
> -                                           aggregate, _aggregate_choices);
> +                                           &data, _aggregate_choices);
>                 if (err)
>                         return err;
> +               if (data.count == 1) {
> +                       err = class_for_each_device(&platform_profile_class, NULL,
> +                                                   &data, _remove_hidden_choices);
> +                       if (err)
> +                               return err;
> +               }
>         }
>
>         /* no profile handler registered any more */
> -       if (bitmap_empty(aggregate, PLATFORM_PROFILE_LAST))
> +       if (bitmap_empty(data.aggregate, PLATFORM_PROFILE_LAST))
>                 return -EINVAL;
>
> -       return _commmon_choices_show(aggregate, buf);
> +       return _commmon_choices_show(data.aggregate, buf);
>  }
>
>  /**
> @@ -373,7 +411,10 @@ static ssize_t platform_profile_store(struct device *dev,
>                                       struct device_attribute *attr,
>                                       const char *buf, size_t count)
>  {
> -       unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +       struct aggregate_choices_data data = {
> +               .aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> +               .count = 0,
> +       };
>         int ret;
>         int i;
>
> @@ -381,13 +422,13 @@ static ssize_t platform_profile_store(struct device *dev,
>         i = sysfs_match_string(profile_names, buf);
>         if (i < 0 || i == PLATFORM_PROFILE_CUSTOM)
>                 return -EINVAL;
> -       set_bit(PLATFORM_PROFILE_LAST, choices);
> +       set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>         scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>                 ret = class_for_each_device(&platform_profile_class, NULL,
> -                                           choices, _aggregate_choices);
> +                                           &data, _aggregate_choices);
>                 if (ret)
>                         return ret;
> -               if (!test_bit(i, choices))
> +               if (!test_bit(i, data.aggregate))
>                         return -EOPNOTSUPP;
>
>                 ret = class_for_each_device(&platform_profile_class, NULL, &i,
> @@ -453,12 +494,15 @@ EXPORT_SYMBOL_GPL(platform_profile_notify);
>   */
>  int platform_profile_cycle(void)
>  {
> +       struct aggregate_choices_data data = {
> +               .aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> +               .count = 0,
> +       };
>         enum platform_profile_option next = PLATFORM_PROFILE_LAST;
>         enum platform_profile_option profile = PLATFORM_PROFILE_LAST;
> -       unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>         int err;
>
> -       set_bit(PLATFORM_PROFILE_LAST, choices);
> +       set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>         scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>                 err = class_for_each_device(&platform_profile_class, NULL,
>                                             &profile, _aggregate_profiles);
> @@ -470,14 +514,14 @@ int platform_profile_cycle(void)
>                         return -EINVAL;
>
>                 err = class_for_each_device(&platform_profile_class, NULL,
> -                                           choices, _aggregate_choices);
> +                                           &data, _aggregate_choices);
>                 if (err)
>                         return err;
>
>                 /* never iterate into a custom if all drivers supported it */
> -               clear_bit(PLATFORM_PROFILE_CUSTOM, choices);
> +               clear_bit(PLATFORM_PROFILE_CUSTOM, data.aggregate);
>
> -               next = find_next_bit_wrap(choices,
> +               next = find_next_bit_wrap(data.aggregate,
>                                           PLATFORM_PROFILE_LAST,
>                                           profile + 1);
>
> @@ -532,6 +576,14 @@ struct device *platform_profile_register(struct device *dev, const char *name,
>                 return ERR_PTR(-EINVAL);
>         }
>
> +       if (ops->hidden_choices) {
> +               err = ops->hidden_choices(drvdata, pprof->hidden_choices);
> +               if (err) {
> +                       dev_err(dev, "platform_profile hidden_choices failed\n");
> +                       return ERR_PTR(err);
> +               }
> +       }
> +
>         guard(mutex)(&profile_lock);
>
>         /* create class interface for individual handler */
> diff --git a/include/linux/platform_profile.h b/include/linux/platform_profile.h
> index 8ab5b0e8eb2c1..8c9df7dadd5d3 100644
> --- a/include/linux/platform_profile.h
> +++ b/include/linux/platform_profile.h
> @@ -33,6 +33,8 @@ enum platform_profile_option {
>   * @probe: Callback to setup choices available to the new class device. These
>   *        choices will only be enforced when setting a new profile, not when
>   *        getting the current one.
> + * @hidden_choices: Callback to setup choices that are not visible to the user
> + *                 but can be set by the driver.
>   * @profile_get: Callback that will be called when showing the current platform
>   *              profile in sysfs.
>   * @profile_set: Callback that will be called when storing a new platform
> @@ -40,6 +42,7 @@ enum platform_profile_option {
>   */
>  struct platform_profile_ops {
>         int (*probe)(void *drvdata, unsigned long *choices);
> +       int (*hidden_choices)(void *drvdata, unsigned long *choices);
>         int (*profile_get)(struct device *dev, enum platform_profile_option *profile);
>         int (*profile_set)(struct device *dev, enum platform_profile_option profile);
>  };
> --
> 2.43.0
>
Kurt Borja Feb. 28, 2025, 10:08 p.m. UTC | #2
Hi Mario,

On Fri Feb 28, 2025 at 12:01 PM -05, Mario Limonciello wrote:
> From: Mario Limonciello <mario.limonciello@amd.com>
>
> When two drivers don't support all the same profiles the legacy interface
> only exports the common profiles.
>
> This causes problems for cases where one driver uses low-power but another
> uses quiet because the result is that neither is exported to sysfs.
>
> To allow two drivers to disagree, add support for "hidden choices".
> Hidden choices are platform profiles that a driver supports to be
> compatible with the platform profile of another driver.
>
> Fixes: 688834743d67 ("ACPI: platform_profile: Allow multiple handlers")
> Reported-by: Antheas Kapenekakis <lkml@antheas.dev>
> Closes: https://lore.kernel.org/platform-driver-x86/e64b771e-3255-42ad-9257-5b8fc6c24ac9@gmx.de/T/#mc068042dd29df36c16c8af92664860fc4763974b
> Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
> ---
> Cc: "Luke D. Jones" <luke@ljones.dev>
>  drivers/acpi/platform_profile.c  | 94 +++++++++++++++++++++++++-------
>  include/linux/platform_profile.h |  3 +
>  2 files changed, 76 insertions(+), 21 deletions(-)
>
> diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c
> index 2ad53cc6aae53..ef9444482db19 100644
> --- a/drivers/acpi/platform_profile.c
> +++ b/drivers/acpi/platform_profile.c
> @@ -21,9 +21,15 @@ struct platform_profile_handler {
>  	struct device dev;
>  	int minor;
>  	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +	unsigned long hidden_choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>  	const struct platform_profile_ops *ops;
>  };
>  
> +struct aggregate_choices_data {
> +	unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +	int count;
> +};
> +
>  static const char * const profile_names[] = {
>  	[PLATFORM_PROFILE_LOW_POWER] = "low-power",
>  	[PLATFORM_PROFILE_COOL] = "cool",
> @@ -73,7 +79,7 @@ static int _store_class_profile(struct device *dev, void *data)
>  
>  	lockdep_assert_held(&profile_lock);
>  	handler = to_pprof_handler(dev);
> -	if (!test_bit(*bit, handler->choices))
> +	if (!test_bit(*bit, handler->choices) && !test_bit(*bit, handler->hidden_choices))
>  		return -EOPNOTSUPP;
>  
>  	return handler->ops->profile_set(dev, *bit);
> @@ -239,21 +245,44 @@ static const struct class platform_profile_class = {
>  /**
>   * _aggregate_choices - Aggregate the available profile choices
>   * @dev: The device
> - * @data: The available profile choices
> + * @arg: struct aggregate_choices_data
>   *
>   * Return: 0 on success, -errno on failure
>   */
> -static int _aggregate_choices(struct device *dev, void *data)
> +static int _aggregate_choices(struct device *dev, void *arg)
>  {
> +	unsigned long tmp[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +	struct aggregate_choices_data *data = arg;
>  	struct platform_profile_handler *handler;
> -	unsigned long *aggregate = data;
>  
>  	lockdep_assert_held(&profile_lock);
>  	handler = to_pprof_handler(dev);
> -	if (test_bit(PLATFORM_PROFILE_LAST, aggregate))
> -		bitmap_copy(aggregate, handler->choices, PLATFORM_PROFILE_LAST);
> +	bitmap_or(tmp, handler->choices, handler->hidden_choices, PLATFORM_PROFILE_LAST);
> +	if (test_bit(PLATFORM_PROFILE_LAST, data->aggregate))
> +		bitmap_copy(data->aggregate, tmp, PLATFORM_PROFILE_LAST);
>  	else
> -		bitmap_and(aggregate, handler->choices, aggregate, PLATFORM_PROFILE_LAST);
> +		bitmap_and(data->aggregate, tmp, data->aggregate, PLATFORM_PROFILE_LAST);
> +	data->count++;
> +
> +	return 0;
> +}
> +
> +/**
> + * _remove_hidden_choices - Remove hidden choices from aggregate data
> + * @dev: The device
> + * @arg: struct aggregate_choices_data
> + *
> + * Return: 0 on success, -errno on failure
> + */
> +static int _remove_hidden_choices(struct device *dev, void *arg)
> +{
> +	struct aggregate_choices_data *data = arg;
> +	struct platform_profile_handler *handler;
> +
> +	lockdep_assert_held(&profile_lock);
> +	handler = to_pprof_handler(dev);
> +	bitmap_andnot(data->aggregate, handler->choices,
> +		      handler->hidden_choices, PLATFORM_PROFILE_LAST);
>  
>  	return 0;
>  }
> @@ -270,22 +299,31 @@ static ssize_t platform_profile_choices_show(struct device *dev,
>  					     struct device_attribute *attr,
>  					     char *buf)
>  {
> -	unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +	struct aggregate_choices_data data = {
> +		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> +		.count = 0,
> +	};
>  	int err;
>  
> -	set_bit(PLATFORM_PROFILE_LAST, aggregate);
> +	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>  	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>  		err = class_for_each_device(&platform_profile_class, NULL,
> -					    aggregate, _aggregate_choices);
> +					    &data, _aggregate_choices);
>  		if (err)
>  			return err;
> +		if (data.count == 1) {
> +			err = class_for_each_device(&platform_profile_class, NULL,
> +						    &data, _remove_hidden_choices);
> +			if (err)
> +				return err;
> +		}
>  	}
>  
>  	/* no profile handler registered any more */
> -	if (bitmap_empty(aggregate, PLATFORM_PROFILE_LAST))
> +	if (bitmap_empty(data.aggregate, PLATFORM_PROFILE_LAST))
>  		return -EINVAL;
>  
> -	return _commmon_choices_show(aggregate, buf);
> +	return _commmon_choices_show(data.aggregate, buf);
>  }
>  
>  /**
> @@ -373,7 +411,10 @@ static ssize_t platform_profile_store(struct device *dev,
>  				      struct device_attribute *attr,
>  				      const char *buf, size_t count)
>  {
> -	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> +	struct aggregate_choices_data data = {
> +		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> +		.count = 0,
> +	};
>  	int ret;
>  	int i;
>  
> @@ -381,13 +422,13 @@ static ssize_t platform_profile_store(struct device *dev,
>  	i = sysfs_match_string(profile_names, buf);
>  	if (i < 0 || i == PLATFORM_PROFILE_CUSTOM)
>  		return -EINVAL;
> -	set_bit(PLATFORM_PROFILE_LAST, choices);
> +	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>  	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>  		ret = class_for_each_device(&platform_profile_class, NULL,
> -					    choices, _aggregate_choices);
> +					    &data, _aggregate_choices);
>  		if (ret)
>  			return ret;
> -		if (!test_bit(i, choices))
> +		if (!test_bit(i, data.aggregate))
>  			return -EOPNOTSUPP;
>  
>  		ret = class_for_each_device(&platform_profile_class, NULL, &i,
> @@ -453,12 +494,15 @@ EXPORT_SYMBOL_GPL(platform_profile_notify);
>   */
>  int platform_profile_cycle(void)
>  {
> +	struct aggregate_choices_data data = {
> +		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> +		.count = 0,
> +	};
>  	enum platform_profile_option next = PLATFORM_PROFILE_LAST;
>  	enum platform_profile_option profile = PLATFORM_PROFILE_LAST;
> -	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>  	int err;
>  
> -	set_bit(PLATFORM_PROFILE_LAST, choices);
> +	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>  	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>  		err = class_for_each_device(&platform_profile_class, NULL,
>  					    &profile, _aggregate_profiles);
> @@ -470,14 +514,14 @@ int platform_profile_cycle(void)
>  			return -EINVAL;
>  
>  		err = class_for_each_device(&platform_profile_class, NULL,
> -					    choices, _aggregate_choices);
> +					    &data, _aggregate_choices);
>  		if (err)
>  			return err;
>  
>  		/* never iterate into a custom if all drivers supported it */
> -		clear_bit(PLATFORM_PROFILE_CUSTOM, choices);
> +		clear_bit(PLATFORM_PROFILE_CUSTOM, data.aggregate);
>  
> -		next = find_next_bit_wrap(choices,
> +		next = find_next_bit_wrap(data.aggregate,
>  					  PLATFORM_PROFILE_LAST,
>  					  profile + 1);
>  
> @@ -532,6 +576,14 @@ struct device *platform_profile_register(struct device *dev, const char *name,
>  		return ERR_PTR(-EINVAL);
>  	}
>  
> +	if (ops->hidden_choices) {
> +		err = ops->hidden_choices(drvdata, pprof->hidden_choices);
> +		if (err) {
> +			dev_err(dev, "platform_profile hidden_choices failed\n");
> +			return ERR_PTR(err);
> +		}
> +	}
> +
>  	guard(mutex)(&profile_lock);
>  
>  	/* create class interface for individual handler */
> diff --git a/include/linux/platform_profile.h b/include/linux/platform_profile.h
> index 8ab5b0e8eb2c1..8c9df7dadd5d3 100644
> --- a/include/linux/platform_profile.h
> +++ b/include/linux/platform_profile.h
> @@ -33,6 +33,8 @@ enum platform_profile_option {
>   * @probe: Callback to setup choices available to the new class device. These
>   *	   choices will only be enforced when setting a new profile, not when
>   *	   getting the current one.
> + * @hidden_choices: Callback to setup choices that are not visible to the user
> + *		    but can be set by the driver.
>   * @profile_get: Callback that will be called when showing the current platform
>   *		 profile in sysfs.
>   * @profile_set: Callback that will be called when storing a new platform
> @@ -40,6 +42,7 @@ enum platform_profile_option {
>   */
>  struct platform_profile_ops {
>  	int (*probe)(void *drvdata, unsigned long *choices);
> +	int (*hidden_choices)(void *drvdata, unsigned long *choices);
>  	int (*profile_get)(struct device *dev, enum platform_profile_option *profile);
>  	int (*profile_set)(struct device *dev, enum platform_profile_option profile);
>  };

This approach works really well for the PMF driver because the
profile_get callback retrieves the raw profile that the profile_set
callback cached. However this is not the case for quite a few drivers,
which usually just retrieve the current profile from WMI for example.

This means that writing a profile to the legacy platform_profile
attribute, which a driver has selected as a "hidden choice" may result
in the operation succeeding, but if the user were to immediately read
from platform_profile it would display "custom", because the profiles
for different handlers may be unsynchronized.

This makes me wonder if the added complexity this patch brings, is
really worth it.

IMHO we should do what Armin suggested in the patch proposed by Antheas.
In fact, I would suggest an even simpler version:

  1. The legacy platform_profile_choices should aggregate `choices`
     with bitmap_or instead of bitmap_and. i.e. It should display all
     available choices
  2. When writing a profile to the legacy platform_profile, if a handler
     doesn't support it, we simply ignore it without failing and
     continue to the next

I believe this works well with power-profiles-daemon, but I'm not
entirely sure. Maybe you know more about it.

This of course has the problem that profiles would be unsync and
platform_profile might display "custom" immediately after setting a
profile, but this patch has the same "issue".

For me this "custom" issue, is not really an issue. The legacy interface
should be deprecated in favor of the class interface, and new/old
user-space tools should use/migrate to that instead.

Let me know what you think!
Mario Limonciello March 1, 2025, 3:19 a.m. UTC | #3
On 2/28/2025 16:08, Kurt Borja wrote:
> Hi Mario,
> 
> On Fri Feb 28, 2025 at 12:01 PM -05, Mario Limonciello wrote:
>> From: Mario Limonciello <mario.limonciello@amd.com>
>>
>> When two drivers don't support all the same profiles the legacy interface
>> only exports the common profiles.
>>
>> This causes problems for cases where one driver uses low-power but another
>> uses quiet because the result is that neither is exported to sysfs.
>>
>> To allow two drivers to disagree, add support for "hidden choices".
>> Hidden choices are platform profiles that a driver supports to be
>> compatible with the platform profile of another driver.
>>
>> Fixes: 688834743d67 ("ACPI: platform_profile: Allow multiple handlers")
>> Reported-by: Antheas Kapenekakis <lkml@antheas.dev>
>> Closes: https://lore.kernel.org/platform-driver-x86/e64b771e-3255-42ad-9257-5b8fc6c24ac9@gmx.de/T/#mc068042dd29df36c16c8af92664860fc4763974b
>> Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
>> ---
>> Cc: "Luke D. Jones" <luke@ljones.dev>
>>   drivers/acpi/platform_profile.c  | 94 +++++++++++++++++++++++++-------
>>   include/linux/platform_profile.h |  3 +
>>   2 files changed, 76 insertions(+), 21 deletions(-)
>>
>> diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c
>> index 2ad53cc6aae53..ef9444482db19 100644
>> --- a/drivers/acpi/platform_profile.c
>> +++ b/drivers/acpi/platform_profile.c
>> @@ -21,9 +21,15 @@ struct platform_profile_handler {
>>   	struct device dev;
>>   	int minor;
>>   	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>> +	unsigned long hidden_choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>>   	const struct platform_profile_ops *ops;
>>   };
>>   
>> +struct aggregate_choices_data {
>> +	unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>> +	int count;
>> +};
>> +
>>   static const char * const profile_names[] = {
>>   	[PLATFORM_PROFILE_LOW_POWER] = "low-power",
>>   	[PLATFORM_PROFILE_COOL] = "cool",
>> @@ -73,7 +79,7 @@ static int _store_class_profile(struct device *dev, void *data)
>>   
>>   	lockdep_assert_held(&profile_lock);
>>   	handler = to_pprof_handler(dev);
>> -	if (!test_bit(*bit, handler->choices))
>> +	if (!test_bit(*bit, handler->choices) && !test_bit(*bit, handler->hidden_choices))
>>   		return -EOPNOTSUPP;
>>   
>>   	return handler->ops->profile_set(dev, *bit);
>> @@ -239,21 +245,44 @@ static const struct class platform_profile_class = {
>>   /**
>>    * _aggregate_choices - Aggregate the available profile choices
>>    * @dev: The device
>> - * @data: The available profile choices
>> + * @arg: struct aggregate_choices_data
>>    *
>>    * Return: 0 on success, -errno on failure
>>    */
>> -static int _aggregate_choices(struct device *dev, void *data)
>> +static int _aggregate_choices(struct device *dev, void *arg)
>>   {
>> +	unsigned long tmp[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>> +	struct aggregate_choices_data *data = arg;
>>   	struct platform_profile_handler *handler;
>> -	unsigned long *aggregate = data;
>>   
>>   	lockdep_assert_held(&profile_lock);
>>   	handler = to_pprof_handler(dev);
>> -	if (test_bit(PLATFORM_PROFILE_LAST, aggregate))
>> -		bitmap_copy(aggregate, handler->choices, PLATFORM_PROFILE_LAST);
>> +	bitmap_or(tmp, handler->choices, handler->hidden_choices, PLATFORM_PROFILE_LAST);
>> +	if (test_bit(PLATFORM_PROFILE_LAST, data->aggregate))
>> +		bitmap_copy(data->aggregate, tmp, PLATFORM_PROFILE_LAST);
>>   	else
>> -		bitmap_and(aggregate, handler->choices, aggregate, PLATFORM_PROFILE_LAST);
>> +		bitmap_and(data->aggregate, tmp, data->aggregate, PLATFORM_PROFILE_LAST);
>> +	data->count++;
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * _remove_hidden_choices - Remove hidden choices from aggregate data
>> + * @dev: The device
>> + * @arg: struct aggregate_choices_data
>> + *
>> + * Return: 0 on success, -errno on failure
>> + */
>> +static int _remove_hidden_choices(struct device *dev, void *arg)
>> +{
>> +	struct aggregate_choices_data *data = arg;
>> +	struct platform_profile_handler *handler;
>> +
>> +	lockdep_assert_held(&profile_lock);
>> +	handler = to_pprof_handler(dev);
>> +	bitmap_andnot(data->aggregate, handler->choices,
>> +		      handler->hidden_choices, PLATFORM_PROFILE_LAST);
>>   
>>   	return 0;
>>   }
>> @@ -270,22 +299,31 @@ static ssize_t platform_profile_choices_show(struct device *dev,
>>   					     struct device_attribute *attr,
>>   					     char *buf)
>>   {
>> -	unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>> +	struct aggregate_choices_data data = {
>> +		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
>> +		.count = 0,
>> +	};
>>   	int err;
>>   
>> -	set_bit(PLATFORM_PROFILE_LAST, aggregate);
>> +	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>>   	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>>   		err = class_for_each_device(&platform_profile_class, NULL,
>> -					    aggregate, _aggregate_choices);
>> +					    &data, _aggregate_choices);
>>   		if (err)
>>   			return err;
>> +		if (data.count == 1) {
>> +			err = class_for_each_device(&platform_profile_class, NULL,
>> +						    &data, _remove_hidden_choices);
>> +			if (err)
>> +				return err;
>> +		}
>>   	}
>>   
>>   	/* no profile handler registered any more */
>> -	if (bitmap_empty(aggregate, PLATFORM_PROFILE_LAST))
>> +	if (bitmap_empty(data.aggregate, PLATFORM_PROFILE_LAST))
>>   		return -EINVAL;
>>   
>> -	return _commmon_choices_show(aggregate, buf);
>> +	return _commmon_choices_show(data.aggregate, buf);
>>   }
>>   
>>   /**
>> @@ -373,7 +411,10 @@ static ssize_t platform_profile_store(struct device *dev,
>>   				      struct device_attribute *attr,
>>   				      const char *buf, size_t count)
>>   {
>> -	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>> +	struct aggregate_choices_data data = {
>> +		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
>> +		.count = 0,
>> +	};
>>   	int ret;
>>   	int i;
>>   
>> @@ -381,13 +422,13 @@ static ssize_t platform_profile_store(struct device *dev,
>>   	i = sysfs_match_string(profile_names, buf);
>>   	if (i < 0 || i == PLATFORM_PROFILE_CUSTOM)
>>   		return -EINVAL;
>> -	set_bit(PLATFORM_PROFILE_LAST, choices);
>> +	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>>   	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>>   		ret = class_for_each_device(&platform_profile_class, NULL,
>> -					    choices, _aggregate_choices);
>> +					    &data, _aggregate_choices);
>>   		if (ret)
>>   			return ret;
>> -		if (!test_bit(i, choices))
>> +		if (!test_bit(i, data.aggregate))
>>   			return -EOPNOTSUPP;
>>   
>>   		ret = class_for_each_device(&platform_profile_class, NULL, &i,
>> @@ -453,12 +494,15 @@ EXPORT_SYMBOL_GPL(platform_profile_notify);
>>    */
>>   int platform_profile_cycle(void)
>>   {
>> +	struct aggregate_choices_data data = {
>> +		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
>> +		.count = 0,
>> +	};
>>   	enum platform_profile_option next = PLATFORM_PROFILE_LAST;
>>   	enum platform_profile_option profile = PLATFORM_PROFILE_LAST;
>> -	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
>>   	int err;
>>   
>> -	set_bit(PLATFORM_PROFILE_LAST, choices);
>> +	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
>>   	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
>>   		err = class_for_each_device(&platform_profile_class, NULL,
>>   					    &profile, _aggregate_profiles);
>> @@ -470,14 +514,14 @@ int platform_profile_cycle(void)
>>   			return -EINVAL;
>>   
>>   		err = class_for_each_device(&platform_profile_class, NULL,
>> -					    choices, _aggregate_choices);
>> +					    &data, _aggregate_choices);
>>   		if (err)
>>   			return err;
>>   
>>   		/* never iterate into a custom if all drivers supported it */
>> -		clear_bit(PLATFORM_PROFILE_CUSTOM, choices);
>> +		clear_bit(PLATFORM_PROFILE_CUSTOM, data.aggregate);
>>   
>> -		next = find_next_bit_wrap(choices,
>> +		next = find_next_bit_wrap(data.aggregate,
>>   					  PLATFORM_PROFILE_LAST,
>>   					  profile + 1);
>>   
>> @@ -532,6 +576,14 @@ struct device *platform_profile_register(struct device *dev, const char *name,
>>   		return ERR_PTR(-EINVAL);
>>   	}
>>   
>> +	if (ops->hidden_choices) {
>> +		err = ops->hidden_choices(drvdata, pprof->hidden_choices);
>> +		if (err) {
>> +			dev_err(dev, "platform_profile hidden_choices failed\n");
>> +			return ERR_PTR(err);
>> +		}
>> +	}
>> +
>>   	guard(mutex)(&profile_lock);
>>   
>>   	/* create class interface for individual handler */
>> diff --git a/include/linux/platform_profile.h b/include/linux/platform_profile.h
>> index 8ab5b0e8eb2c1..8c9df7dadd5d3 100644
>> --- a/include/linux/platform_profile.h
>> +++ b/include/linux/platform_profile.h
>> @@ -33,6 +33,8 @@ enum platform_profile_option {
>>    * @probe: Callback to setup choices available to the new class device. These
>>    *	   choices will only be enforced when setting a new profile, not when
>>    *	   getting the current one.
>> + * @hidden_choices: Callback to setup choices that are not visible to the user
>> + *		    but can be set by the driver.
>>    * @profile_get: Callback that will be called when showing the current platform
>>    *		 profile in sysfs.
>>    * @profile_set: Callback that will be called when storing a new platform
>> @@ -40,6 +42,7 @@ enum platform_profile_option {
>>    */
>>   struct platform_profile_ops {
>>   	int (*probe)(void *drvdata, unsigned long *choices);
>> +	int (*hidden_choices)(void *drvdata, unsigned long *choices);
>>   	int (*profile_get)(struct device *dev, enum platform_profile_option *profile);
>>   	int (*profile_set)(struct device *dev, enum platform_profile_option profile);
>>   };
> 
> This approach works really well for the PMF driver because the
> profile_get callback retrieves the raw profile that the profile_set
> callback cached. However this is not the case for quite a few drivers,
> which usually just retrieve the current profile from WMI for example.
> 
> This means that writing a profile to the legacy platform_profile
> attribute, which a driver has selected as a "hidden choice" may result
> in the operation succeeding, but if the user were to immediately read
> from platform_profile it would display "custom", because the profiles
> for different handlers may be unsynchronized.

I guess we need to think about how many other drivers would really need 
hidden choices added.

Is it just PMF?

> 
> This makes me wonder if the added complexity this patch brings, is
> really worth it.
> 
> IMHO we should do what Armin suggested in the patch proposed by Antheas.
> In fact, I would suggest an even simpler version:
> 
>    1. The legacy platform_profile_choices should aggregate `choices`
>       with bitmap_or instead of bitmap_and. i.e. It should display all
>       available choices
>    2. When writing a profile to the legacy platform_profile, if a handler
>       doesn't support it, we simply ignore it without failing and
>       continue to the next
> 
> I believe this works well with power-profiles-daemon, but I'm not
> entirely sure. Maybe you know more about it.
> 
> This of course has the problem that profiles would be unsync and
> platform_profile might display "custom" immediately after setting a
> profile, but this patch has the same "issue".
> 
> For me this "custom" issue, is not really an issue. The legacy interface
> should be deprecated in favor of the class interface, and new/old
> user-space tools should use/migrate to that instead.
> 
> Let me know what you think!

I don't really like that profiles can get out of sync, this is asking 
for a non-deterministic behavior that can be difficult to diagnose 
issues and also difficult for userspace to work with.
Antheas Kapenekakis March 1, 2025, 11:06 a.m. UTC | #4
On Sat, 1 Mar 2025 at 04:19, Mario Limonciello <superm1@kernel.org> wrote:
>
> On 2/28/2025 16:08, Kurt Borja wrote:
> > Hi Mario,
> >
> > On Fri Feb 28, 2025 at 12:01 PM -05, Mario Limonciello wrote:
> >> From: Mario Limonciello <mario.limonciello@amd.com>
> >>
> >> When two drivers don't support all the same profiles the legacy interface
> >> only exports the common profiles.
> >>
> >> This causes problems for cases where one driver uses low-power but another
> >> uses quiet because the result is that neither is exported to sysfs.
> >>
> >> To allow two drivers to disagree, add support for "hidden choices".
> >> Hidden choices are platform profiles that a driver supports to be
> >> compatible with the platform profile of another driver.
> >>
> >> Fixes: 688834743d67 ("ACPI: platform_profile: Allow multiple handlers")
> >> Reported-by: Antheas Kapenekakis <lkml@antheas.dev>
> >> Closes: https://lore.kernel.org/platform-driver-x86/e64b771e-3255-42ad-9257-5b8fc6c24ac9@gmx.de/T/#mc068042dd29df36c16c8af92664860fc4763974b
> >> Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
> >> ---
> >> Cc: "Luke D. Jones" <luke@ljones.dev>
> >>   drivers/acpi/platform_profile.c  | 94 +++++++++++++++++++++++++-------
> >>   include/linux/platform_profile.h |  3 +
> >>   2 files changed, 76 insertions(+), 21 deletions(-)
> >>
> >> diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c
> >> index 2ad53cc6aae53..ef9444482db19 100644
> >> --- a/drivers/acpi/platform_profile.c
> >> +++ b/drivers/acpi/platform_profile.c
> >> @@ -21,9 +21,15 @@ struct platform_profile_handler {
> >>      struct device dev;
> >>      int minor;
> >>      unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >> +    unsigned long hidden_choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >>      const struct platform_profile_ops *ops;
> >>   };
> >>
> >> +struct aggregate_choices_data {
> >> +    unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >> +    int count;
> >> +};
> >> +
> >>   static const char * const profile_names[] = {
> >>      [PLATFORM_PROFILE_LOW_POWER] = "low-power",
> >>      [PLATFORM_PROFILE_COOL] = "cool",
> >> @@ -73,7 +79,7 @@ static int _store_class_profile(struct device *dev, void *data)
> >>
> >>      lockdep_assert_held(&profile_lock);
> >>      handler = to_pprof_handler(dev);
> >> -    if (!test_bit(*bit, handler->choices))
> >> +    if (!test_bit(*bit, handler->choices) && !test_bit(*bit, handler->hidden_choices))
> >>              return -EOPNOTSUPP;
> >>
> >>      return handler->ops->profile_set(dev, *bit);
> >> @@ -239,21 +245,44 @@ static const struct class platform_profile_class = {
> >>   /**
> >>    * _aggregate_choices - Aggregate the available profile choices
> >>    * @dev: The device
> >> - * @data: The available profile choices
> >> + * @arg: struct aggregate_choices_data
> >>    *
> >>    * Return: 0 on success, -errno on failure
> >>    */
> >> -static int _aggregate_choices(struct device *dev, void *data)
> >> +static int _aggregate_choices(struct device *dev, void *arg)
> >>   {
> >> +    unsigned long tmp[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >> +    struct aggregate_choices_data *data = arg;
> >>      struct platform_profile_handler *handler;
> >> -    unsigned long *aggregate = data;
> >>
> >>      lockdep_assert_held(&profile_lock);
> >>      handler = to_pprof_handler(dev);
> >> -    if (test_bit(PLATFORM_PROFILE_LAST, aggregate))
> >> -            bitmap_copy(aggregate, handler->choices, PLATFORM_PROFILE_LAST);
> >> +    bitmap_or(tmp, handler->choices, handler->hidden_choices, PLATFORM_PROFILE_LAST);
> >> +    if (test_bit(PLATFORM_PROFILE_LAST, data->aggregate))
> >> +            bitmap_copy(data->aggregate, tmp, PLATFORM_PROFILE_LAST);
> >>      else
> >> -            bitmap_and(aggregate, handler->choices, aggregate, PLATFORM_PROFILE_LAST);
> >> +            bitmap_and(data->aggregate, tmp, data->aggregate, PLATFORM_PROFILE_LAST);
> >> +    data->count++;
> >> +
> >> +    return 0;
> >> +}
> >> +
> >> +/**
> >> + * _remove_hidden_choices - Remove hidden choices from aggregate data
> >> + * @dev: The device
> >> + * @arg: struct aggregate_choices_data
> >> + *
> >> + * Return: 0 on success, -errno on failure
> >> + */
> >> +static int _remove_hidden_choices(struct device *dev, void *arg)
> >> +{
> >> +    struct aggregate_choices_data *data = arg;
> >> +    struct platform_profile_handler *handler;
> >> +
> >> +    lockdep_assert_held(&profile_lock);
> >> +    handler = to_pprof_handler(dev);
> >> +    bitmap_andnot(data->aggregate, handler->choices,
> >> +                  handler->hidden_choices, PLATFORM_PROFILE_LAST);
> >>
> >>      return 0;
> >>   }
> >> @@ -270,22 +299,31 @@ static ssize_t platform_profile_choices_show(struct device *dev,
> >>                                           struct device_attribute *attr,
> >>                                           char *buf)
> >>   {
> >> -    unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >> +    struct aggregate_choices_data data = {
> >> +            .aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> >> +            .count = 0,
> >> +    };
> >>      int err;
> >>
> >> -    set_bit(PLATFORM_PROFILE_LAST, aggregate);
> >> +    set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
> >>      scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
> >>              err = class_for_each_device(&platform_profile_class, NULL,
> >> -                                        aggregate, _aggregate_choices);
> >> +                                        &data, _aggregate_choices);
> >>              if (err)
> >>                      return err;
> >> +            if (data.count == 1) {
> >> +                    err = class_for_each_device(&platform_profile_class, NULL,
> >> +                                                &data, _remove_hidden_choices);
> >> +                    if (err)
> >> +                            return err;
> >> +            }
> >>      }
> >>
> >>      /* no profile handler registered any more */
> >> -    if (bitmap_empty(aggregate, PLATFORM_PROFILE_LAST))
> >> +    if (bitmap_empty(data.aggregate, PLATFORM_PROFILE_LAST))
> >>              return -EINVAL;
> >>
> >> -    return _commmon_choices_show(aggregate, buf);
> >> +    return _commmon_choices_show(data.aggregate, buf);
> >>   }
> >>
> >>   /**
> >> @@ -373,7 +411,10 @@ static ssize_t platform_profile_store(struct device *dev,
> >>                                    struct device_attribute *attr,
> >>                                    const char *buf, size_t count)
> >>   {
> >> -    unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >> +    struct aggregate_choices_data data = {
> >> +            .aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> >> +            .count = 0,
> >> +    };
> >>      int ret;
> >>      int i;
> >>
> >> @@ -381,13 +422,13 @@ static ssize_t platform_profile_store(struct device *dev,
> >>      i = sysfs_match_string(profile_names, buf);
> >>      if (i < 0 || i == PLATFORM_PROFILE_CUSTOM)
> >>              return -EINVAL;
> >> -    set_bit(PLATFORM_PROFILE_LAST, choices);
> >> +    set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
> >>      scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
> >>              ret = class_for_each_device(&platform_profile_class, NULL,
> >> -                                        choices, _aggregate_choices);
> >> +                                        &data, _aggregate_choices);
> >>              if (ret)
> >>                      return ret;
> >> -            if (!test_bit(i, choices))
> >> +            if (!test_bit(i, data.aggregate))
> >>                      return -EOPNOTSUPP;
> >>
> >>              ret = class_for_each_device(&platform_profile_class, NULL, &i,
> >> @@ -453,12 +494,15 @@ EXPORT_SYMBOL_GPL(platform_profile_notify);
> >>    */
> >>   int platform_profile_cycle(void)
> >>   {
> >> +    struct aggregate_choices_data data = {
> >> +            .aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
> >> +            .count = 0,
> >> +    };
> >>      enum platform_profile_option next = PLATFORM_PROFILE_LAST;
> >>      enum platform_profile_option profile = PLATFORM_PROFILE_LAST;
> >> -    unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
> >>      int err;
> >>
> >> -    set_bit(PLATFORM_PROFILE_LAST, choices);
> >> +    set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
> >>      scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
> >>              err = class_for_each_device(&platform_profile_class, NULL,
> >>                                          &profile, _aggregate_profiles);
> >> @@ -470,14 +514,14 @@ int platform_profile_cycle(void)
> >>                      return -EINVAL;
> >>
> >>              err = class_for_each_device(&platform_profile_class, NULL,
> >> -                                        choices, _aggregate_choices);
> >> +                                        &data, _aggregate_choices);
> >>              if (err)
> >>                      return err;
> >>
> >>              /* never iterate into a custom if all drivers supported it */
> >> -            clear_bit(PLATFORM_PROFILE_CUSTOM, choices);
> >> +            clear_bit(PLATFORM_PROFILE_CUSTOM, data.aggregate);
> >>
> >> -            next = find_next_bit_wrap(choices,
> >> +            next = find_next_bit_wrap(data.aggregate,
> >>                                        PLATFORM_PROFILE_LAST,
> >>                                        profile + 1);
> >>
> >> @@ -532,6 +576,14 @@ struct device *platform_profile_register(struct device *dev, const char *name,
> >>              return ERR_PTR(-EINVAL);
> >>      }
> >>
> >> +    if (ops->hidden_choices) {
> >> +            err = ops->hidden_choices(drvdata, pprof->hidden_choices);
> >> +            if (err) {
> >> +                    dev_err(dev, "platform_profile hidden_choices failed\n");
> >> +                    return ERR_PTR(err);
> >> +            }
> >> +    }
> >> +
> >>      guard(mutex)(&profile_lock);
> >>
> >>      /* create class interface for individual handler */
> >> diff --git a/include/linux/platform_profile.h b/include/linux/platform_profile.h
> >> index 8ab5b0e8eb2c1..8c9df7dadd5d3 100644
> >> --- a/include/linux/platform_profile.h
> >> +++ b/include/linux/platform_profile.h
> >> @@ -33,6 +33,8 @@ enum platform_profile_option {
> >>    * @probe: Callback to setup choices available to the new class device. These
> >>    *    choices will only be enforced when setting a new profile, not when
> >>    *    getting the current one.
> >> + * @hidden_choices: Callback to setup choices that are not visible to the user
> >> + *              but can be set by the driver.
> >>    * @profile_get: Callback that will be called when showing the current platform
> >>    *          profile in sysfs.
> >>    * @profile_set: Callback that will be called when storing a new platform
> >> @@ -40,6 +42,7 @@ enum platform_profile_option {
> >>    */
> >>   struct platform_profile_ops {
> >>      int (*probe)(void *drvdata, unsigned long *choices);
> >> +    int (*hidden_choices)(void *drvdata, unsigned long *choices);
> >>      int (*profile_get)(struct device *dev, enum platform_profile_option *profile);
> >>      int (*profile_set)(struct device *dev, enum platform_profile_option profile);
> >>   };
> >
> > This approach works really well for the PMF driver because the
> > profile_get callback retrieves the raw profile that the profile_set
> > callback cached. However this is not the case for quite a few drivers,
> > which usually just retrieve the current profile from WMI for example.
> >
> > This means that writing a profile to the legacy platform_profile
> > attribute, which a driver has selected as a "hidden choice" may result
> > in the operation succeeding, but if the user were to immediately read
> > from platform_profile it would display "custom", because the profiles
> > for different handlers may be unsynchronized.
>
> I guess we need to think about how many other drivers would really need
> hidden choices added.
>
> Is it just PMF?
>
> >
> > This makes me wonder if the added complexity this patch brings, is
> > really worth it.
> >
> > IMHO we should do what Armin suggested in the patch proposed by Antheas.
> > In fact, I would suggest an even simpler version:
> >
> >    1. The legacy platform_profile_choices should aggregate `choices`
> >       with bitmap_or instead of bitmap_and. i.e. It should display all
> >       available choices
> >    2. When writing a profile to the legacy platform_profile, if a handler
> >       doesn't support it, we simply ignore it without failing and
> >       continue to the next
> >
> > I believe this works well with power-profiles-daemon, but I'm not
> > entirely sure. Maybe you know more about it.
> >
> > This of course has the problem that profiles would be unsync and
> > platform_profile might display "custom" immediately after setting a
> > profile, but this patch has the same "issue".
> >
> > For me this "custom" issue, is not really an issue. The legacy interface
> > should be deprecated in favor of the class interface, and new/old
> > user-space tools should use/migrate to that instead.
> >
> > Let me know what you think!
>
> I don't really like that profiles can get out of sync, this is asking
> for a non-deterministic behavior that can be difficult to diagnose
> issues and also difficult for userspace to work with.

I agree with Mario here. Imagine two drivers, one with low-power and
one with quiet. They both begin at performance.

Then, userspace software gets confused (incl. ppd) and sets firmware
profile to low-power. The latter gets left in performance, causing
excess drain.

I do not believe the legacy interface should be deprecated. Right now,
amd-pmf is a NOOP in most devices so there is actually 0 reason for
generic power handlers to move to the new API. Just extra work. So
lets make sure the legacy endpoint works properly for the foreseeable
future.

Also, when power handlers start moving to the new interface, they will
hardcode choices based on the name. As they should. TDP needs to be
customized per device/manufacturer. So moving handlers between
low-power and quiet will not be possible.

@Mario: I do not have a device with an amd-pmf integration. All of
mine have stub handlers. I would expect that a properly configured pmf
handler for e.g., Asus would do the same as the armoury interface, so
that users do not have to rely to vendor software on WIndows. Then
power profiles would be synced between windows and armoury. In that
case, we have a problem of setting the power mode twice. What would be
the mitigation for something like that?

Antheas
Mario Limonciello March 1, 2025, 1:52 p.m. UTC | #5
>>> Let me know what you think!
>>
>> I don't really like that profiles can get out of sync, this is asking
>> for a non-deterministic behavior that can be difficult to diagnose
>> issues and also difficult for userspace to work with.
> 
> I agree with Mario here. Imagine two drivers, one with low-power and
> one with quiet. They both begin at performance.
> 
> Then, userspace software gets confused (incl. ppd) and sets firmware
> profile to low-power. The latter gets left in performance, causing
> excess drain.
> 
> I do not believe the legacy interface should be deprecated. Right now,
> amd-pmf is a NOOP in most devices 

"Most" devices is not accurate.  There are a lot of devices that it does 
enable.  In the gaming space right now it's often behaving as a no-op.

> so there is actually 0 reason for
> generic power handlers to move to the new API. Just extra work. So
> lets make sure the legacy endpoint works properly for the foreseeable
> future.
> 
> Also, when power handlers start moving to the new interface, they will
> hardcode choices based on the name. As they should. TDP needs to be
> customized per device/manufacturer. So moving handlers between
> low-power and quiet will not be possible.
> 
> @Mario: I do not have a device with an amd-pmf integration. All of
> mine have stub handlers. I would expect that a properly configured pmf
> handler for e.g., Asus would do the same as the armoury interface, so
> that users do not have to rely to vendor software on WIndows. Then
> power profiles would be synced between windows and armoury. In that
> case, we have a problem of setting the power mode twice. What would be
> the mitigation for something like that?
> 
> Antheas

"Power mode" is a concept, it doesn't just apply to configuring sPPT and 
fPPT.  I envisage that a vendor that actively uses PMF and their own 
interface would be changing different things by the different interfaces.

For "example" PMF may reconfigure sPPT, fPPT, STT and STAPM but their 
driver may notify their EC to change a fan curve.

If we really end up with a situation that vendor interface and PMF do 
the same thing we can cross that bridge then.
Antheas Kapenekakis March 1, 2025, 2:06 p.m. UTC | #6
On Sat, 1 Mar 2025 at 14:52, Mario Limonciello <superm1@kernel.org> wrote:
>
> >>> Let me know what you think!
> >>
> >> I don't really like that profiles can get out of sync, this is asking
> >> for a non-deterministic behavior that can be difficult to diagnose
> >> issues and also difficult for userspace to work with.
> >
> > I agree with Mario here. Imagine two drivers, one with low-power and
> > one with quiet. They both begin at performance.
> >
> > Then, userspace software gets confused (incl. ppd) and sets firmware
> > profile to low-power. The latter gets left in performance, causing
> > excess drain.
> >
> > I do not believe the legacy interface should be deprecated. Right now,
> > amd-pmf is a NOOP in most devices
>
> "Most" devices is not accurate.  There are a lot of devices that it does
> enable.  In the gaming space right now it's often behaving as a no-op.

That would be a fair description. Can you give some examples of
devices that use the interface? Devices with and without vendor
software.

> > so there is actually 0 reason for
> > generic power handlers to move to the new API. Just extra work. So
> > lets make sure the legacy endpoint works properly for the foreseeable
> > future.
> >
> > Also, when power handlers start moving to the new interface, they will
> > hardcode choices based on the name. As they should. TDP needs to be
> > customized per device/manufacturer. So moving handlers between
> > low-power and quiet will not be possible.
> >
> > @Mario: I do not have a device with an amd-pmf integration. All of
> > mine have stub handlers. I would expect that a properly configured pmf
> > handler for e.g., Asus would do the same as the armoury interface, so
> > that users do not have to rely to vendor software on WIndows. Then
> > power profiles would be synced between windows and armoury. In that
> > case, we have a problem of setting the power mode twice. What would be
> > the mitigation for something like that?
> >
> > Antheas
>
> "Power mode" is a concept, it doesn't just apply to configuring sPPT and
> fPPT.  I envisage that a vendor that actively uses PMF and their own
> interface would be changing different things by the different interfaces.
>
> For "example" PMF may reconfigure sPPT, fPPT, STT and STAPM but their
> driver may notify their EC to change a fan curve.

No. If PMF changes these values it also needs to change the fan curve
itself via the BIOS notification. Doing otherwise would lead to
situations where users do not install the vendor driver and cook their
device. So I expect that when PMF controls things it controls
everything. I would expect if vendors fallback to the pmf firmware
notifications while also providing vendor software there would be some
synergy between them, such as changing which fan preset is selected by
the PMF interface.

> If we really end up with a situation that vendor interface and PMF do
> the same thing we can cross that bridge then.
Mario Limonciello March 1, 2025, 4:03 p.m. UTC | #7
On 3/1/25 08:06, Antheas Kapenekakis wrote:
> On Sat, 1 Mar 2025 at 14:52, Mario Limonciello <superm1@kernel.org> wrote:
>>
>>>>> Let me know what you think!
>>>>
>>>> I don't really like that profiles can get out of sync, this is asking
>>>> for a non-deterministic behavior that can be difficult to diagnose
>>>> issues and also difficult for userspace to work with.
>>>
>>> I agree with Mario here. Imagine two drivers, one with low-power and
>>> one with quiet. They both begin at performance.
>>>
>>> Then, userspace software gets confused (incl. ppd) and sets firmware
>>> profile to low-power. The latter gets left in performance, causing
>>> excess drain.
>>>
>>> I do not believe the legacy interface should be deprecated. Right now,
>>> amd-pmf is a NOOP in most devices
>>
>> "Most" devices is not accurate.  There are a lot of devices that it does
>> enable.  In the gaming space right now it's often behaving as a no-op.
> 
> That would be a fair description. Can you give some examples of
> devices that use the interface? Devices with and without vendor
> software.

Off hand the Framework 13 and 16 AMD both use PMF exclusively.  So do a 
bunch of HP commercial laptops.

Mark can keep me honest, but I want to say the Strix Thinkpad laptops 
have both PMF and vendor interface (thinkpad-acpi).
  >>
>> "Power mode" is a concept, it doesn't just apply to configuring sPPT and
>> fPPT.  I envisage that a vendor that actively uses PMF and their own
>> interface would be changing different things by the different interfaces.
>>
>> For "example" PMF may reconfigure sPPT, fPPT, STT and STAPM but their
>> driver may notify their EC to change a fan curve.
> 
> No. If PMF changes these values it also needs to change the fan curve
> itself via the BIOS notification. Doing otherwise would lead to
> situations where users do not install the vendor driver and cook their
> device. 

Fan curves are just that; curves.  They just control how quickly fans 
ramp up not whether or not they "work".

But in any case; that's a firmware issue not a platform profile design 
issue.

> So I expect that when PMF controls things it controls
> everything. I would expect if vendors fallback to the pmf firmware
> notifications while also providing vendor software there would be some
> synergy between them, such as changing which fan preset is selected by
> the PMF interface.
> 

I can't control what vendors do; it's their decision how to manage their 
systems.  All I can do is provide infrastructure to help.
Antheas Kapenekakis March 1, 2025, 4:15 p.m. UTC | #8
On Sat, 1 Mar 2025 at 17:04, Mario Limonciello <superm1@kernel.org> wrote:
>
>
>
> On 3/1/25 08:06, Antheas Kapenekakis wrote:
> > On Sat, 1 Mar 2025 at 14:52, Mario Limonciello <superm1@kernel.org> wrote:
> >>
> >>>>> Let me know what you think!
> >>>>
> >>>> I don't really like that profiles can get out of sync, this is asking
> >>>> for a non-deterministic behavior that can be difficult to diagnose
> >>>> issues and also difficult for userspace to work with.
> >>>
> >>> I agree with Mario here. Imagine two drivers, one with low-power and
> >>> one with quiet. They both begin at performance.
> >>>
> >>> Then, userspace software gets confused (incl. ppd) and sets firmware
> >>> profile to low-power. The latter gets left in performance, causing
> >>> excess drain.
> >>>
> >>> I do not believe the legacy interface should be deprecated. Right now,
> >>> amd-pmf is a NOOP in most devices
> >>
> >> "Most" devices is not accurate.  There are a lot of devices that it does
> >> enable.  In the gaming space right now it's often behaving as a no-op.
> >
> > That would be a fair description. Can you give some examples of
> > devices that use the interface? Devices with and without vendor
> > software.
>
> Off hand the Framework 13 and 16 AMD both use PMF exclusively.  So do a
> bunch of HP commercial laptops.

I will ask Kyle to check it out.

> Mark can keep me honest, but I want to say the Strix Thinkpad laptops
> have both PMF and vendor interface (thinkpad-acpi).

Hm, yeah that would be interesting to hear about

>   >>
> >> "Power mode" is a concept, it doesn't just apply to configuring sPPT and
> >> fPPT.  I envisage that a vendor that actively uses PMF and their own
> >> interface would be changing different things by the different interfaces.
> >>
> >> For "example" PMF may reconfigure sPPT, fPPT, STT and STAPM but their
> >> driver may notify their EC to change a fan curve.
> >
> > No. If PMF changes these values it also needs to change the fan curve
> > itself via the BIOS notification. Doing otherwise would lead to
> > situations where users do not install the vendor driver and cook their
> > device.
>
> Fan curves are just that; curves.  They just control how quickly fans
> ramp up not whether or not they "work".

The APU reaches a similar temperature (Tctl) across a wide TDP range,
so temperature cannot be used on its own to determine fan speed.
Manufacturers that provide different fan curves depending on the TDP
mode usually cap the maximum fan speed on low TDPs. So you can get
funny situations where the device is set to 30W, but the fan runs as
if its using 10W leading to thermal soaking. So it is very important
for those to be inline.

> But in any case; that's a firmware issue not a platform profile design
> issue.

It would be a hypothetical scenario. I do not expect such a device to exist.

> > So I expect that when PMF controls things it controls
> > everything. I would expect if vendors fallback to the pmf firmware
> > notifications while also providing vendor software there would be some
> > synergy between them, such as changing which fan preset is selected by
> > the PMF interface.
> >
>
> I can't control what vendors do; it's their decision how to manage their
> systems.  All I can do is provide infrastructure to help.

This was more of my intuition of how I would expect amd-pmf
integration to be done in Windows where one of the drivers might be
missing.

Since only thinkpads are expected to do both, perhaps Mark can check
out how they work. I have a thinkpad that is 11th gen intel.

Antheas
Mark Pearson March 2, 2025, 3:23 a.m. UTC | #9
On Sat, Mar 1, 2025, at 11:15 AM, Antheas Kapenekakis wrote:
> On Sat, 1 Mar 2025 at 17:04, Mario Limonciello <superm1@kernel.org> wrote:
>>
>>
>>
>> On 3/1/25 08:06, Antheas Kapenekakis wrote:
>> > On Sat, 1 Mar 2025 at 14:52, Mario Limonciello <superm1@kernel.org> wrote:
>> >>
>> >>>>> Let me know what you think!
>> >>>>
>> >>>> I don't really like that profiles can get out of sync, this is asking
>> >>>> for a non-deterministic behavior that can be difficult to diagnose
>> >>>> issues and also difficult for userspace to work with.
>> >>>
>> >>> I agree with Mario here. Imagine two drivers, one with low-power and
>> >>> one with quiet. They both begin at performance.
>> >>>
>> >>> Then, userspace software gets confused (incl. ppd) and sets firmware
>> >>> profile to low-power. The latter gets left in performance, causing
>> >>> excess drain.
>> >>>
>> >>> I do not believe the legacy interface should be deprecated. Right now,
>> >>> amd-pmf is a NOOP in most devices
>> >>
>> >> "Most" devices is not accurate.  There are a lot of devices that it does
>> >> enable.  In the gaming space right now it's often behaving as a no-op.
>> >
>> > That would be a fair description. Can you give some examples of
>> > devices that use the interface? Devices with and without vendor
>> > software.
>>
>> Off hand the Framework 13 and 16 AMD both use PMF exclusively.  So do a
>> bunch of HP commercial laptops.
>
> I will ask Kyle to check it out.
>
>> Mark can keep me honest, but I want to say the Strix Thinkpad laptops
>> have both PMF and vendor interface (thinkpad-acpi).
>
> Hm, yeah that would be interesting to hear about
>

Yep, support both.

>>   >>
>> >> "Power mode" is a concept, it doesn't just apply to configuring sPPT and
>> >> fPPT.  I envisage that a vendor that actively uses PMF and their own
>> >> interface would be changing different things by the different interfaces.
>> >>
>> >> For "example" PMF may reconfigure sPPT, fPPT, STT and STAPM but their
>> >> driver may notify their EC to change a fan curve.
>> >
>> > No. If PMF changes these values it also needs to change the fan curve
>> > itself via the BIOS notification. Doing otherwise would lead to
>> > situations where users do not install the vendor driver and cook their
>> > device.
>>
>> Fan curves are just that; curves.  They just control how quickly fans
>> ramp up not whether or not they "work".
>
> The APU reaches a similar temperature (Tctl) across a wide TDP range,
> so temperature cannot be used on its own to determine fan speed.
> Manufacturers that provide different fan curves depending on the TDP
> mode usually cap the maximum fan speed on low TDPs. So you can get
> funny situations where the device is set to 30W, but the fan runs as
> if its using 10W leading to thermal soaking. So it is very important
> for those to be inline.
>
>> But in any case; that's a firmware issue not a platform profile design
>> issue.
>
> It would be a hypothetical scenario. I do not expect such a device to exist.
>
>> > So I expect that when PMF controls things it controls
>> > everything. I would expect if vendors fallback to the pmf firmware
>> > notifications while also providing vendor software there would be some
>> > synergy between them, such as changing which fan preset is selected by
>> > the PMF interface.
>> >
>>
>> I can't control what vendors do; it's their decision how to manage their
>> systems.  All I can do is provide infrastructure to help.
>
> This was more of my intuition of how I would expect amd-pmf
> integration to be done in Windows where one of the drivers might be
> missing.
>
> Since only thinkpads are expected to do both, perhaps Mark can check
> out how they work. I have a thinkpad that is 11th gen intel.
>

I'll do some checking next week (away this weekend). Mario will ping you offline for best testing to do.

Mark
diff mbox series

Patch

diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c
index 2ad53cc6aae53..ef9444482db19 100644
--- a/drivers/acpi/platform_profile.c
+++ b/drivers/acpi/platform_profile.c
@@ -21,9 +21,15 @@  struct platform_profile_handler {
 	struct device dev;
 	int minor;
 	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
+	unsigned long hidden_choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
 	const struct platform_profile_ops *ops;
 };
 
+struct aggregate_choices_data {
+	unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
+	int count;
+};
+
 static const char * const profile_names[] = {
 	[PLATFORM_PROFILE_LOW_POWER] = "low-power",
 	[PLATFORM_PROFILE_COOL] = "cool",
@@ -73,7 +79,7 @@  static int _store_class_profile(struct device *dev, void *data)
 
 	lockdep_assert_held(&profile_lock);
 	handler = to_pprof_handler(dev);
-	if (!test_bit(*bit, handler->choices))
+	if (!test_bit(*bit, handler->choices) && !test_bit(*bit, handler->hidden_choices))
 		return -EOPNOTSUPP;
 
 	return handler->ops->profile_set(dev, *bit);
@@ -239,21 +245,44 @@  static const struct class platform_profile_class = {
 /**
  * _aggregate_choices - Aggregate the available profile choices
  * @dev: The device
- * @data: The available profile choices
+ * @arg: struct aggregate_choices_data
  *
  * Return: 0 on success, -errno on failure
  */
-static int _aggregate_choices(struct device *dev, void *data)
+static int _aggregate_choices(struct device *dev, void *arg)
 {
+	unsigned long tmp[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
+	struct aggregate_choices_data *data = arg;
 	struct platform_profile_handler *handler;
-	unsigned long *aggregate = data;
 
 	lockdep_assert_held(&profile_lock);
 	handler = to_pprof_handler(dev);
-	if (test_bit(PLATFORM_PROFILE_LAST, aggregate))
-		bitmap_copy(aggregate, handler->choices, PLATFORM_PROFILE_LAST);
+	bitmap_or(tmp, handler->choices, handler->hidden_choices, PLATFORM_PROFILE_LAST);
+	if (test_bit(PLATFORM_PROFILE_LAST, data->aggregate))
+		bitmap_copy(data->aggregate, tmp, PLATFORM_PROFILE_LAST);
 	else
-		bitmap_and(aggregate, handler->choices, aggregate, PLATFORM_PROFILE_LAST);
+		bitmap_and(data->aggregate, tmp, data->aggregate, PLATFORM_PROFILE_LAST);
+	data->count++;
+
+	return 0;
+}
+
+/**
+ * _remove_hidden_choices - Remove hidden choices from aggregate data
+ * @dev: The device
+ * @arg: struct aggregate_choices_data
+ *
+ * Return: 0 on success, -errno on failure
+ */
+static int _remove_hidden_choices(struct device *dev, void *arg)
+{
+	struct aggregate_choices_data *data = arg;
+	struct platform_profile_handler *handler;
+
+	lockdep_assert_held(&profile_lock);
+	handler = to_pprof_handler(dev);
+	bitmap_andnot(data->aggregate, handler->choices,
+		      handler->hidden_choices, PLATFORM_PROFILE_LAST);
 
 	return 0;
 }
@@ -270,22 +299,31 @@  static ssize_t platform_profile_choices_show(struct device *dev,
 					     struct device_attribute *attr,
 					     char *buf)
 {
-	unsigned long aggregate[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
+	struct aggregate_choices_data data = {
+		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
+		.count = 0,
+	};
 	int err;
 
-	set_bit(PLATFORM_PROFILE_LAST, aggregate);
+	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
 	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
 		err = class_for_each_device(&platform_profile_class, NULL,
-					    aggregate, _aggregate_choices);
+					    &data, _aggregate_choices);
 		if (err)
 			return err;
+		if (data.count == 1) {
+			err = class_for_each_device(&platform_profile_class, NULL,
+						    &data, _remove_hidden_choices);
+			if (err)
+				return err;
+		}
 	}
 
 	/* no profile handler registered any more */
-	if (bitmap_empty(aggregate, PLATFORM_PROFILE_LAST))
+	if (bitmap_empty(data.aggregate, PLATFORM_PROFILE_LAST))
 		return -EINVAL;
 
-	return _commmon_choices_show(aggregate, buf);
+	return _commmon_choices_show(data.aggregate, buf);
 }
 
 /**
@@ -373,7 +411,10 @@  static ssize_t platform_profile_store(struct device *dev,
 				      struct device_attribute *attr,
 				      const char *buf, size_t count)
 {
-	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
+	struct aggregate_choices_data data = {
+		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
+		.count = 0,
+	};
 	int ret;
 	int i;
 
@@ -381,13 +422,13 @@  static ssize_t platform_profile_store(struct device *dev,
 	i = sysfs_match_string(profile_names, buf);
 	if (i < 0 || i == PLATFORM_PROFILE_CUSTOM)
 		return -EINVAL;
-	set_bit(PLATFORM_PROFILE_LAST, choices);
+	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
 	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
 		ret = class_for_each_device(&platform_profile_class, NULL,
-					    choices, _aggregate_choices);
+					    &data, _aggregate_choices);
 		if (ret)
 			return ret;
-		if (!test_bit(i, choices))
+		if (!test_bit(i, data.aggregate))
 			return -EOPNOTSUPP;
 
 		ret = class_for_each_device(&platform_profile_class, NULL, &i,
@@ -453,12 +494,15 @@  EXPORT_SYMBOL_GPL(platform_profile_notify);
  */
 int platform_profile_cycle(void)
 {
+	struct aggregate_choices_data data = {
+		.aggregate = { [0 ... BITS_TO_LONGS(PLATFORM_PROFILE_LAST) - 1] = ~0UL },
+		.count = 0,
+	};
 	enum platform_profile_option next = PLATFORM_PROFILE_LAST;
 	enum platform_profile_option profile = PLATFORM_PROFILE_LAST;
-	unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
 	int err;
 
-	set_bit(PLATFORM_PROFILE_LAST, choices);
+	set_bit(PLATFORM_PROFILE_LAST, data.aggregate);
 	scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &profile_lock) {
 		err = class_for_each_device(&platform_profile_class, NULL,
 					    &profile, _aggregate_profiles);
@@ -470,14 +514,14 @@  int platform_profile_cycle(void)
 			return -EINVAL;
 
 		err = class_for_each_device(&platform_profile_class, NULL,
-					    choices, _aggregate_choices);
+					    &data, _aggregate_choices);
 		if (err)
 			return err;
 
 		/* never iterate into a custom if all drivers supported it */
-		clear_bit(PLATFORM_PROFILE_CUSTOM, choices);
+		clear_bit(PLATFORM_PROFILE_CUSTOM, data.aggregate);
 
-		next = find_next_bit_wrap(choices,
+		next = find_next_bit_wrap(data.aggregate,
 					  PLATFORM_PROFILE_LAST,
 					  profile + 1);
 
@@ -532,6 +576,14 @@  struct device *platform_profile_register(struct device *dev, const char *name,
 		return ERR_PTR(-EINVAL);
 	}
 
+	if (ops->hidden_choices) {
+		err = ops->hidden_choices(drvdata, pprof->hidden_choices);
+		if (err) {
+			dev_err(dev, "platform_profile hidden_choices failed\n");
+			return ERR_PTR(err);
+		}
+	}
+
 	guard(mutex)(&profile_lock);
 
 	/* create class interface for individual handler */
diff --git a/include/linux/platform_profile.h b/include/linux/platform_profile.h
index 8ab5b0e8eb2c1..8c9df7dadd5d3 100644
--- a/include/linux/platform_profile.h
+++ b/include/linux/platform_profile.h
@@ -33,6 +33,8 @@  enum platform_profile_option {
  * @probe: Callback to setup choices available to the new class device. These
  *	   choices will only be enforced when setting a new profile, not when
  *	   getting the current one.
+ * @hidden_choices: Callback to setup choices that are not visible to the user
+ *		    but can be set by the driver.
  * @profile_get: Callback that will be called when showing the current platform
  *		 profile in sysfs.
  * @profile_set: Callback that will be called when storing a new platform
@@ -40,6 +42,7 @@  enum platform_profile_option {
  */
 struct platform_profile_ops {
 	int (*probe)(void *drvdata, unsigned long *choices);
+	int (*hidden_choices)(void *drvdata, unsigned long *choices);
 	int (*profile_get)(struct device *dev, enum platform_profile_option *profile);
 	int (*profile_set)(struct device *dev, enum platform_profile_option profile);
 };