Disclaimer

While every effort is made to make the plugin safe and robust to use, by the very nature of the tuning RyzenAdj provides, it is possible to cause system instability or even hardware damage that may not be covered by Valve's warranty.

By using this plugin, you accept responsibility for all consequences of using this plugin. The developers of this plugin accept no liability or responsibility for damages.

See the LICENSE for the full terms.

Glossary

  • QAM: Quick Access Menu (the right-hand menu accessed from the ... button)

Decisions

Any important decisions -- big or small; technical, design or otherwise -- have been captured here in this decision repository.

The are a few intentions:

  • To provide the plugin developers with a reference as to why particular decisions were made and the thought process that lead to them.
  • To help potential contributors get in to the mindset of the plugin developers -- by reading the decisions already made, you can get a feel for the direction and vision of the project.
  • To be revisited in the future when looking for potential enhancements -- often decisions made won't be perfect first time due to limitations of time, effort, knowledge or understanding of the domain.

Format

Each decision has been written in a format akin to the MADR (Markdown Any Decision Records) template.

Making a new decision

Copy TEMPLATE.md and give it an appropriate name. Each decision must have a unique ID number.

Reset settings to defaults

Context and Problem Statement

This RyzenAdj plugin allows someone to undervolt various components of their Steam Deck.

Due to the nature of undervolting, experimentation to find the right values is necessary. Along the way, instability can be introduced.

There are a few occasions where a user might want to reset their settings back to defaults:

  • The user is uncomfortable with their current settings;
  • The user has noticed an issue and wants to go back to default settings;
  • The user wants to compare the difference in power usage and performance between their tuned settings and stock settings.

Considered Options

  • Allow resetting individual settings using SliderField's resetValue prop.
  • One button that resets all settings at once.
  • A global button shortcut to reset settings back to their defaults, which can be used even when not interacting with the plugin.

Decision Outcome

Both resetting individual settings and all settings within the plugin make sense -- both of those will be implemented.

The global shortcut might be an interesting option at some point, but it may not actually be very useful -- by the time you know something's wrong, the Steam Deck is probably going to need to be restarted (assuming it hasn't hard-locked and the watchdog timer restarts it for you!). This can perhaps be revisited in the future.

In addition, for comparison purposes, it might be nice to have the ability to quickly jump between two different sets of settings (e.g. tuned and defaults). This can be revisited later down the line once the plugin is more complete.

Restoring frontend settings

Context and Problem Statement

There are a few occasions where settings may need to be restored:

  • When the quick access bar is closed, the frontend forgets what settings are applied and needs to be reminded.
  • When the Deck has gone to sleep, the tuned settings likely need to be reapplied.
  • When the Deck has been shutdown or restarted, the tuned settings will need to be reapplied.

This ADR specifically discusses restoring frontend settings.

Restoring settings can potentially be hazardous -- restoring unstable settings may lead to boot loops. However, allowing the frontend to rehydrate its state should be a safe operation so long as due care is taken to ensure that unnecessary settings changes are avoided.

Considered Options

  • Don't restore the current state
  • Have the frontend store state and restore it when needed
  • Have the backend keep track of state and allow the frontend to request current state

Decision

"Have the backend keep track of state and allow the frontend to request current state", since it is going to provide the most correct and consistent behaviour (even if it has potential to be slower).

Pros and Cons of the Options

Don't restore the current state

  • Good, because it's already implemented
  • Bad, because it is confusing for the user to see the values are reset
  • Bad, because it actually applies the reset settings whenever the plugin is accessed

Have the frontend store state and restore it when needed

  • Good, as state will always be ready immediately, minimising any chance of users seeing loading UI
  • Bad, as there are now two sources of truth -- frontend and backend -- since the backend will need to track the current state for its own purposes anyway
    • Sources of truth could become out of sync, resulting in the frontend suggesting that settings are synced when they aren't

Have the backend keep track of state and allow the frontend to request current state

  • Good, as frontend can always reflect the backend's state
  • Good, as there's only one source of truth -- less opportunity for the frontend and backend to go out of sync
  • Bad, as users may see incomplete/loading UI if the backend takes a while to respond

Restoring tuned settings after sleep or charge events

Context and Problem Statement

These are the power events where settings may need to be restored:

  • When the Deck has gone to sleep, the tuned settings likely need to be reapplied.
  • When the Deck has been shutdown or restarted, the tuned settings will need to be reapplied.

This ADR discusses what should happen after the Deck has awoken from sleep.

These assumptions are being made:

  • That it is necessary to restore tuned settings after a sleep/charge event -- no code is unnecessary if this is untrue, but it's good to consider the options anyway.
  • That sleep/charge events can be detected -- this will be revisited if that turns out not to be true.

Considered Options

  • Don't reapply settings
  • Always reapply settings

Decision Outcome

"Always reapply settings" as it'll provide a less manual user experience, assuming that it is both necessary and possible to do so.

Update #1 2023-07-30: Cursory examination suggests that tuned settings do not need to be reapplied when the machine is plugged in to charge or unplugged, at least in Game Mode (where this plugin applies). No action is needed to detect charge events.

Update #2 2023-07-30: Cursory examination suggests that tuned settings do need to be reapplied after the machine is put to sleep. Action is needed to detect sleep events.

Pros and Cons of the Options

Don't reapply settings

  • Good, as it requires no code and is simple
  • Good, as it is potentially more stable if the user had chosen unstable settings
  • Bad, as it means needing to reapply the settings after every sleep/charge event

Always reapply settings

  • Good, as user's desired settings will stick across events, which is likely user expectation
  • Bad, as sleep/charge events needs to be detected and responded to

More Information

Restoring tuned settings after reboot

Context and Problem Statement

When the machine has rebooted, any previously applied tuned settings will be lost. This means that they need to be reapplied.

However, it should not be assumed that the settings should always be reapplied -- it is possible that the machine rebooted because the applied settings were unstable and caused it to crash. If these unstable settings were automatically reapplied, we might cause the system to get stuck in a boot loop, requiring the operating system to be reset.

Decision Drivers

  • Never lock a user out of their machine
  • Be as automatic as possible

Considered Options

  • Always reapply settings
  • Always reapply settings, but allow holding a button/combination on startup to reset settings
  • Attempt to automatically detect a system crash and only apply settings when crash has not occurred
  • Never reapply settings

Decision Outcome

"Attempt to automatically detect a system crash and only apply settings when crash has not occurred", as it is the most automatic. However, as of the time of writing, its reliability is yet unproven.

Since software should always empower the user, "Always reapply settings, but allow holding a button/combination on startup to reset settings" will also be implemented if technically feasible.

In either case, when the settings aren't applied a notification should be shown to the user explaining the reason.

Pro and Cons of the Options

Always reapply settings

  • Good, as the user does not need to reapply settings every boot
  • Bad, as the settings may be unstable, meaning restoring the setting may cause a crash

Always reapply settings, but allow holding a button/combination on startup to reset settings

  • Good, as it allows the user to override any decision made by the plugin, providing an escape hatch
  • Neutral, as the button detection needs to be rock-solid to be an effective measure
  • Bad, as the user needs to know that this action can be taken. If they don't, and there's no other safety measures, they may think they're locked out of the machine.

Attempt to automatically detect a system crash and only apply settings when crash has not occurred

  • Good, as the machine should continue working without manual intervention, even if unstable settings are applied.
  • Neutral, as the crash detection needs to be rock-solid for this to be an effective measure. Likewise, if the detection logic is too sensitive, stable settings may get reverted for no good reason.
  • Bad, as if the crash detection logic does not detect crashes correctly and there are no other safety measures, the user could get locked out of the machine.

Never reapply settings

  • Good, as the user will never get locked out of their machine.
  • Bad, as manual action is needed to reapply the tuning settings, which is laborious.

More Information: Detecting a crash

This article provides a few strategies: https://access.redhat.com/articles/2642741

Inspect wtmp with last -x

After reboot:

(deck@a-deck bin)$ last -Fxn2 shutdown reboot
reboot   system boot  5.13.0-valve36-1 Sun Jul 30 13:00:24 2023   still running
shutdown system down  5.13.0-valve36-1 Sun Jul 30 13:00:03 2023 - Sun Jul 30 13:00:24 2023  (00:00)

wtmp begins Mon Dec 19 14:06:55 2022

Note that there is one "system down" and one "system boot" event. The most recent event is at the top.

After crash:

(deck@a-deck bin)$ last -Fxn2 shutdown reboot
reboot   system boot  5.13.0-valve36-1 Sun Jul 30 11:57:16 2023   still running
reboot   system boot  5.13.0-valve36-1 Sun Jul 30 11:50:24 2023   still running

wtmp begins Mon Dec 19 14:06:55 2022

Note that there are two "system boot" lines mentioning it is "still running". This indicates there was no graceful shutdown.

This method is viable on Steam Deck.

Manage lockfile when plugin loaded and unloaded

Since Decky Loader calls plugin methods when the plugin is loaded and unloaded, this could be used as a measure to detect a system crash.

Specifically:

  1. The plugin creates a lockfile when the plugin loads. The plugin deletes the lockfile when the plugin unloads.
  2. When loading, the plugin can check for the presence of the lockfile:
    1. If the lockfile is absent, apply tuned settings.
    2. If the lockfile is still present, this indicates a system crash. Do not load the tuned settings and notify the user.

I haven't found clear documentation on exactly when the plugin's load and unload methods are called. Hence, this assumes that the plugin's unload method is always called when the system is rebooted or Game Mode is exited (e.g. to switch to Desktop Mode).

If the load and unload methods are additionally called on every sleep and wake, this wouldn't appear to cause any additional issues, so long as the load and unload methods are called a balanced number of times (e.g. one load call followed by one unload call).

Combining detection methods

It's possible to combine multiple of the above methods, and to not load settings if any of them detect a system crash.

Quickly comparing settings

Context and Problem Statement

In the course of tuning, a user will want to see the difference between their current tuned settings and another set of settings, usually the defaults, to see the difference that has been made. This would be particularly useful for benchmarks to verify the effct of the tuned offset.

Decision Drivers

  • Easy to compare settings
  • Doesn't significantly complicate plugin usability
  • Doesn't significantly add development effort unless the benefit is worth it

Considered Options

  • Use the existing reset functionality
  • Allow toggling a particular setting on and off
  • Add profile switching

Decision Outcome

"Allow toggling a particular setting on and off" is reasonably simple conceptually for users to understand.

"Add profile switching" seems overkill at this time as there's only a small number of settings to manage. The overhead in profile management and persistence doesn't seem worth it at this time. However, if more RyzenAdj settings become available, this may become more desirable. However, this does not preclude per-game profile management (to be discussed separately), which may have a place even with the small number of settings.

Pro and Cons of the Options

Use the existing reset functionality

The existing reset functionality provides something like this. However, you can only reset back to defaults.

  • Good, as the functionality already exists and requires no further development
  • Bad, as, after a reset, the user needs to manually set their tuned setting again

Allow toggling a particular setting on and off

Add a toggle for each setting, e.g. "Apply/Tune/Customise CPU Offset". When a setting is toggled off, the default value is applied. When a setting is toggled on, the previous tuned value is applied.

  • Good, as it allows for quick comparison
  • Good, as if someone wants to disable a tuning, it is clear it is disabled
  • Bad, as the UI may become cluttered with a lot of toggles
    • Neutral, as there are only 1-2 settings at this time
    • Neutral, if this could be done with a controller button press instead of a separate ToggleField (although that might be difficult to discover)

Add profile switching

Add the ability to create, modify and delete profiles of settings.

  • Good, as it allows the user to not just compare between tuned settings and defaults but also different tuned settings
  • Bad, adds significant complexity to the UI and interactions with the plugin as profiles need to be managed
  • Bad, as it is perhaps overkill for the 1-2 tunable settings at present

Shape of internal state

Context and Problem Statement

The plugin has a number of different types of state to store and keep track of, each with their own lifetime:

  • UI state (e.g. first load, waiting for response, details of previous RyzenAdj execution)
    • Lifetime: Ephemeral, derived from other parts of state
    • Persistence: Not persisted
  • RyzenAdj State (e.g. cpu_offset, apply_cpu_offset)
    • Lifetime: Ephemeral, until the next confdig is applied
    • Persistence: Not persisted, planned to be persisted
  • UI options not directly related to RyzenAdj state (e.g. show debug information, show GPU offset tuning)
    • Lifetime: As long as the UI is visible
    • Persistence: Persisted
  • Plugin options (e.g. restore settings on reboot)
    • Lifetime: As long as the plugin is loaded
    • Persistence: Persisted

This decision will decide an initial high-level policy how to categorise and store the different types of state. This will include the shape of the state. The goal is to make it clear how to manage these states.

This decision is not deciding by what mechanism to persist or serialise state, although pseudo-configuration may be used to help visualise the state. Additionally, how easy it is to do these things will be a decision driver.

Decision Drivers

  • Ease of maintenance, including:
    • Ease of persisting settings in backend
    • Ease of transmission from backend to frontend
    • Ease of storage, usage and updates in frontend
  • Ease of understanding
  • Easy to serialise

Considered Options

  • Keep completely flat state
  • Separate RyzenAdj state and options state
  • Many separate types of configuration state

Decision Outcome

"Many separate types of configuration state" seems like the option that will allow for the most encapsulated and extensible code.

Pro and Cons of the Options

Keep completely flat state

This conceptually means keeping everything at the same level. No nesting. No separation of the different types of state.

For example:

apply_cpu_offset: true
cpu_offset: -10
show_debug_information: true
seen_launch_warning: false
  • Good, straightforward to understand
  • Bad, difficult to handle as there's no opportunity for encapsulation or classification of different types of options
    • For example, handling multiple RyzenAdj profiles across different games sounds arduous
  • Bad, as a lot of code will need to be written to handle every option -- and it'll all be combined together
    • During development, parts of the code would naturally lend itself to being separated for maintainability... Suggesting this config should be the same.

Separate RyzenAdj state and options state

Since tune profiles and plugin options are often used in different contexts, they could be treated and stored separately:

[tune]
apply_cpu_offset = true
cpu_offset = -10

[options]
show_debug_information = true
seen_launch_warning = false
  • Good, this allows for the storage of multiple tune profiles reasonably easily
  • Bad, for plugin options, this is perhaps still too rigid -- some configuration options can be grouped together

Many separate types of configuration state

As discussed in Types of Options below, there can be multiple different classifications of options within the plugin. These can be kept within their own domains.

[tune]
apply_cpu_offset = true
cpu_offset = -10

[ui]
show_debug_information = true

[notification]
show_notifications = true
show_system_crash = true
show_app_settings_change = false

[modal]
never_show_again = [ "first_start", "gpu_warning" ]

For example, handling modal and notification configuration separately could allow for abstracting them into separate components, e.g. ModalManager and NotificationManager on the frontend and backend, allowing for encapsulated and simplified code.

There might still be room for a generic "options" section if no more-specific section fits.

  • Good, as it allows for encapsulation
  • Good, as it allows for storing multiple tune profiles easily
  • Good, as the configuration is reasonably easy to understand and edit by hand
  • Neutral, the most complex configuration to parse. However, the most complex part will likely be handled by a library.

More Information

Categorising types of state

UI and plugin options

Above, UI and plugin options are represented as being neatly sepoarate from each other. However, I think it makes more sense to consider that any option could potentially affect both the frontend and the backend.

For example, the proposed "Show GPU offset tuning" option would show or hide UI elements, therefore it is a UI option. However, if we say that the GPU offset was tuned to a non-default value and then the "Show GPU offset tuning" setting was turned off, it would perhaps be surprising to the user if the tune persisted. Therefore, to disable the tune would both affect the backend and, potentially, the RyzenAdj state. (Note: In this specific case, ignoring the GPU tuning options instead of resetting them in the RyzenAdj state would be my preferred the more expected way to go as the setting conceptually changes the view of the tuning options, not the tuning options themselves.)

Types of options

It's worth considering that there may be whole classes or types of options.

RyzenAdj settings, for example, are a specific class of state that can be treated as an isolated unit. Each game/app might have its own RyzenAdj settings. Pseudo config:

[tune.default]
apply_cpu_offset = true
cpu_offset = -5

[tune.app."Ratchet & Clank: Rift Apart"]
apply_cpu_offset = true
cpu_offset = -10

Notifications and pop-ups/modal could also be their own type of options. For example:

[notification]
show_notifications = true
show_system_crash = true
show_app_settings_change = false

[modal]
never_show_again_first_start = true
never_show_again_gpu_warning = false

Side note: Alternative modal design which allows for easy resets:

[modal]
never_show_again = [ "first_start", "gpu_warning" ]