drm/amd/display: Add Idle state manager(ISM)

[Why]

Rapid allow/disallow of idle optimization calls, whether it be IPS or
self-refresh features, can end up using more power if actual
time-in-idle is low. It can also spam DMUB command submission in a way
that prevents it from servicing other requestors.

[How]

Introduce the Idle State Manager (ISM) to amdgpu. It maintains a finite
state machine that uses a hysteresis to determine if a delay should be
inserted between a caller allowing idle, and when the actual idle
optimizations are programmed.

A second timer is also introduced to enable static screen optimizations
(SSO) such as PSR1 and Replay low HZ idle mode. Rapid SSO enable/disable
can have a negative power impact on some low hz video playback, and can
introduce user lag for PSR1 (due to up to 3 frames of sync latency).

This effectively rate-limits idle optimizations, based on hysteresis.

This also replaces the existing delay logic used for PSR1, allowing
drm_vblank_crtc_config.disable_immediate = true, and thus allowing
drm_crtc_vblank_restore().

v2:
* Loosen criteria for ISM to exit idle optimizations; it failed to exit
  idle correctly on cursor updates when there are no drm_vblank
  requestors,
* Document default_ism_config
* Convert pr_debug to trace events to reduce overhead on frequent
  codepaths
* checkpatch.pl fixes

Link: https://gitlab.freedesktop.org/drm/amd/-/issues/4527
Link: https://gitlab.freedesktop.org/drm/amd/-/issues/3709
Fixes: 58a261bfc9 ("drm/amd/display: use a more lax vblank enable policy for older ASICs")
Signed-off-by: Ray Wu <ray.wu@amd.com>
Signed-off-by: Leo Li <sunpeng.li@amd.com>
Reviewed-by: Mario Limonciello (AMD) <superm1@kernel.org>
Signed-off-by: Alex Deucher <alexander.deucher@amd.com>
This commit is contained in:
Ray Wu
2025-12-17 16:36:02 +08:00
committed by Alex Deucher
parent e4b1715a87
commit 754003486c
9 changed files with 903 additions and 63 deletions

View File

@@ -44,6 +44,7 @@
#include <drm/display/drm_dp_mst_helper.h>
#include "modules/inc/mod_freesync.h"
#include "amdgpu_dm_irq_params.h"
#include "amdgpu_dm_ism.h"
struct amdgpu_bo;
struct amdgpu_device;
@@ -486,6 +487,10 @@ struct amdgpu_crtc {
int deferred_flip_completion;
/* parameters access from DM IRQ handler */
struct dm_irq_params dm_irq_params;
/* DM idle state manager */
struct amdgpu_dm_ism ism;
/* pll sharing */
struct amdgpu_atom_ss ss;
bool ss_enabled;

View File

@@ -40,7 +40,8 @@ AMDGPUDM = \
amdgpu_dm_replay.o \
amdgpu_dm_quirks.o \
amdgpu_dm_wb.o \
amdgpu_dm_colorop.o
amdgpu_dm_colorop.o \
amdgpu_dm_ism.o
ifdef CONFIG_DRM_AMD_DC_FP
AMDGPUDM += dc_fpu.o

View File

@@ -3283,6 +3283,7 @@ static int dm_suspend(struct amdgpu_ip_block *ip_block)
mutex_lock(&dm->dc_lock);
amdgpu_dm_ism_disable(dm);
dc_allow_idle_optimizations(adev->dm.dc, false);
dm->cached_dc_state = dc_state_create_copy(dm->dc->current_state);
@@ -3316,6 +3317,9 @@ static int dm_suspend(struct amdgpu_ip_block *ip_block)
amdgpu_dm_irq_suspend(adev);
scoped_guard(mutex, &dm->dc_lock)
amdgpu_dm_ism_disable(dm);
hpd_rx_irq_work_suspend(dm);
dc_set_power_state(dm->dc, DC_ACPI_CM_POWER_STATE_D3);
@@ -3606,6 +3610,7 @@ static int dm_resume(struct amdgpu_ip_block *ip_block)
dc_resume(dm->dc);
amdgpu_dm_ism_enable(dm);
amdgpu_dm_irq_resume_early(adev);
for (i = 0; i < dc_state->stream_count; i++) {
@@ -3666,6 +3671,9 @@ static int dm_resume(struct amdgpu_ip_block *ip_block)
/* program HPD filter */
dc_resume(dm->dc);
scoped_guard(mutex, &dm->dc_lock)
amdgpu_dm_ism_enable(dm);
/*
* early enable HPD Rx IRQ, should be done before set mode as short
* pulse interrupts are used for MST
@@ -9334,31 +9342,7 @@ static void manage_dm_interrupts(struct amdgpu_device *adev,
if (acrtc_state) {
timing = &acrtc_state->stream->timing;
/*
* Depending on when the HW latching event of double-buffered
* registers happen relative to the PSR SDP deadline, and how
* bad the Panel clock has drifted since the last ALPM off
* event, there can be up to 3 frames of delay between sending
* the PSR exit cmd to DMUB fw, and when the panel starts
* displaying live frames.
*
* We can set:
*
* 20/100 * offdelay_ms = 3_frames_ms
* => offdelay_ms = 5 * 3_frames_ms
*
* This ensures that `3_frames_ms` will only be experienced as a
* 20% delay on top how long the display has been static, and
* thus make the delay less perceivable.
*/
if (acrtc_state->stream->link->psr_settings.psr_version <
DC_PSR_VERSION_UNSUPPORTED) {
offdelay = DIV64_U64_ROUND_UP((u64)5 * 3 * 10 *
timing->v_total *
timing->h_total,
timing->pix_clk_100hz);
config.offdelay_ms = offdelay ?: 30;
} else if (amdgpu_ip_version(adev, DCE_HWIP, 0) <
if (amdgpu_ip_version(adev, DCE_HWIP, 0) <
IP_VERSION(3, 5, 0) ||
!(adev->flags & AMD_IS_APU)) {
/*

View File

@@ -124,37 +124,37 @@ bool amdgpu_dm_crtc_vrr_active(const struct dm_crtc_state *dm_state)
* - Enable condition same as above
* - Disable when vblank counter is enabled
*/
static void amdgpu_dm_crtc_set_panel_sr_feature(
struct vblank_control_work *vblank_work,
void amdgpu_dm_crtc_set_panel_sr_feature(
struct amdgpu_display_manager *dm,
struct amdgpu_crtc *acrtc,
struct dc_stream_state *stream,
bool vblank_enabled, bool allow_sr_entry)
{
struct dc_link *link = vblank_work->stream->link;
struct dc_link *link = stream->link;
bool is_sr_active = (link->replay_settings.replay_allow_active ||
link->psr_settings.psr_allow_active);
bool is_crc_window_active = false;
bool vrr_active = amdgpu_dm_crtc_vrr_active_irq(vblank_work->acrtc);
bool vrr_active = amdgpu_dm_crtc_vrr_active_irq(acrtc);
#ifdef CONFIG_DRM_AMD_SECURE_DISPLAY
is_crc_window_active =
amdgpu_dm_crc_window_is_activated(&vblank_work->acrtc->base);
amdgpu_dm_crc_window_is_activated(&acrtc->base);
#endif
if (link->replay_settings.replay_feature_enabled && !vrr_active &&
allow_sr_entry && !is_sr_active && !is_crc_window_active) {
amdgpu_dm_replay_enable(vblank_work->stream, true);
amdgpu_dm_replay_enable(stream, true);
} else if (vblank_enabled) {
if (link->psr_settings.psr_version < DC_PSR_VERSION_SU_1 && is_sr_active)
amdgpu_dm_psr_disable(vblank_work->stream, false);
amdgpu_dm_psr_disable(stream, false);
} else if (link->psr_settings.psr_feature_enabled && !vrr_active &&
allow_sr_entry && !is_sr_active && !is_crc_window_active) {
struct amdgpu_dm_connector *aconn =
(struct amdgpu_dm_connector *) vblank_work->stream->dm_stream_context;
(struct amdgpu_dm_connector *) stream->dm_stream_context;
if (!aconn->disallow_edp_enter_psr) {
struct amdgpu_display_manager *dm = vblank_work->dm;
amdgpu_dm_psr_enable(vblank_work->stream);
amdgpu_dm_psr_enable(stream);
if (dm->idle_workqueue &&
(dm->dc->config.disable_ips == DMUB_IPS_ENABLE) &&
dm->dc->idle_optimizations_allowed &&
@@ -251,33 +251,15 @@ static void amdgpu_dm_crtc_vblank_control_worker(struct work_struct *work)
mutex_lock(&dm->dc_lock);
if (vblank_work->enable)
if (vblank_work->enable) {
dm->active_vblank_irq_count++;
else if (dm->active_vblank_irq_count)
dm->active_vblank_irq_count--;
if (dm->active_vblank_irq_count > 0)
dc_allow_idle_optimizations(dm->dc, false);
/*
* Control PSR based on vblank requirements from OS
*
* If panel supports PSR SU, there's no need to disable PSR when OS is
* submitting fast atomic commits (we infer this by whether the OS
* requests vblank events). Fast atomic commits will simply trigger a
* full-frame-update (FFU); a specific case of selective-update (SU)
* where the SU region is the full hactive*vactive region. See
* fill_dc_dirty_rects().
*/
if (vblank_work->stream && vblank_work->stream->link && vblank_work->acrtc) {
amdgpu_dm_crtc_set_panel_sr_feature(
vblank_work, vblank_work->enable,
vblank_work->acrtc->dm_irq_params.allow_sr_entry);
}
if (dm->active_vblank_irq_count == 0) {
dc_post_update_surfaces_to_stream(dm->dc);
dc_allow_idle_optimizations(dm->dc, true);
amdgpu_dm_ism_commit_event(&vblank_work->acrtc->ism,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED);
} else {
if (dm->active_vblank_irq_count > 0)
dm->active_vblank_irq_count--;
amdgpu_dm_ism_commit_event(&vblank_work->acrtc->ism,
DM_ISM_EVENT_ENTER_IDLE_REQUESTED);
}
mutex_unlock(&dm->dc_lock);
@@ -476,6 +458,9 @@ static struct drm_crtc_state *amdgpu_dm_crtc_duplicate_state(struct drm_crtc *cr
static void amdgpu_dm_crtc_destroy(struct drm_crtc *crtc)
{
struct amdgpu_crtc *acrtc = to_amdgpu_crtc(crtc);
amdgpu_dm_ism_fini(&acrtc->ism);
drm_crtc_cleanup(crtc);
kfree(crtc);
}
@@ -719,6 +704,35 @@ static const struct drm_crtc_helper_funcs amdgpu_dm_crtc_helper_funcs = {
.get_scanout_position = amdgpu_crtc_get_scanout_position,
};
/*
* This hysteresis filter as configured will:
*
* * Search through the latest 8[filter_history_size] entries in history,
* skipping entries that are older than [filter_old_history_threshold] frames
* (0 means ignore age)
* * Searches for short-idle-periods that lasted shorter than
* 4[filter_num_frames] frames-times
* * If there is at least 1[filter_entry_count] short-idle-period, then a delay
* of 4[activation_num_delay_frames] will applied before allowing idle
* optimizations again.
* * An additional delay of 11[sso_num_frames] is applied before enabling
* panel-specific optimizations.
*
* The values were determined empirically on another OS, optimizing for Z8
* residency on APUs when running a productivity + web browsing test.
*
* TODO: Run similar tests to determine if these values are also optimal for
* Linux, and if each APU generation benefits differently.
*/
static struct amdgpu_dm_ism_config default_ism_config = {
.filter_num_frames = 4,
.filter_history_size = 8,
.filter_entry_count = 1,
.activation_num_delay_frames = 4,
.filter_old_history_threshold = 0,
.sso_num_frames = 11,
};
int amdgpu_dm_crtc_init(struct amdgpu_display_manager *dm,
struct drm_plane *plane,
uint32_t crtc_index)
@@ -749,6 +763,8 @@ int amdgpu_dm_crtc_init(struct amdgpu_display_manager *dm,
if (res)
goto fail;
amdgpu_dm_ism_init(&acrtc->ism, &default_ism_config);
drm_crtc_helper_add(&acrtc->base, &amdgpu_dm_crtc_helper_funcs);
/* Create (reset) the plane state */

View File

@@ -27,6 +27,12 @@
#ifndef __AMDGPU_DM_CRTC_H__
#define __AMDGPU_DM_CRTC_H__
void amdgpu_dm_crtc_set_panel_sr_feature(
struct amdgpu_display_manager *dm,
struct amdgpu_crtc *acrtc,
struct dc_stream_state *stream,
bool vblank_enabled, bool allow_sr_entry);
void amdgpu_dm_crtc_handle_vblank(struct amdgpu_crtc *acrtc);
bool amdgpu_dm_crtc_modeset_required(struct drm_crtc_state *crtc_state,

View File

@@ -0,0 +1,598 @@
// SPDX-License-Identifier: MIT
/*
* Copyright 2026 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Authors: AMD
*
*/
#include <linux/types.h>
#include <drm/drm_vblank.h>
#include "dc.h"
#include "amdgpu.h"
#include "amdgpu_dm_ism.h"
#include "amdgpu_dm_crtc.h"
#include "amdgpu_dm_trace.h"
/**
* dm_ism_next_state - Get next state based on current state and event
*
* This function defines the idle state management FSM. Invalid transitions
* are ignored and will not progress the FSM.
*/
static bool dm_ism_next_state(enum amdgpu_dm_ism_state current_state,
enum amdgpu_dm_ism_event event,
enum amdgpu_dm_ism_state *next_state)
{
switch (STATE_EVENT(current_state, event)) {
case STATE_EVENT(DM_ISM_STATE_FULL_POWER_RUNNING,
DM_ISM_EVENT_ENTER_IDLE_REQUESTED):
*next_state = DM_ISM_STATE_HYSTERESIS_WAITING;
break;
case STATE_EVENT(DM_ISM_STATE_FULL_POWER_RUNNING,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE):
*next_state = DM_ISM_STATE_FULL_POWER_BUSY;
break;
case STATE_EVENT(DM_ISM_STATE_FULL_POWER_BUSY,
DM_ISM_EVENT_ENTER_IDLE_REQUESTED):
*next_state = DM_ISM_STATE_HYSTERESIS_BUSY;
break;
case STATE_EVENT(DM_ISM_STATE_FULL_POWER_BUSY,
DM_ISM_EVENT_END_CURSOR_UPDATE):
*next_state = DM_ISM_STATE_FULL_POWER_RUNNING;
break;
case STATE_EVENT(DM_ISM_STATE_HYSTERESIS_WAITING,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED):
*next_state = DM_ISM_STATE_TIMER_ABORTED;
break;
case STATE_EVENT(DM_ISM_STATE_HYSTERESIS_WAITING,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE):
*next_state = DM_ISM_STATE_HYSTERESIS_BUSY;
break;
case STATE_EVENT(DM_ISM_STATE_HYSTERESIS_WAITING,
DM_ISM_EVENT_TIMER_ELAPSED):
*next_state = DM_ISM_STATE_OPTIMIZED_IDLE;
break;
case STATE_EVENT(DM_ISM_STATE_HYSTERESIS_WAITING,
DM_ISM_EVENT_IMMEDIATE):
*next_state = DM_ISM_STATE_OPTIMIZED_IDLE;
break;
case STATE_EVENT(DM_ISM_STATE_HYSTERESIS_BUSY,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED):
*next_state = DM_ISM_STATE_FULL_POWER_BUSY;
break;
case STATE_EVENT(DM_ISM_STATE_HYSTERESIS_BUSY,
DM_ISM_EVENT_END_CURSOR_UPDATE):
*next_state = DM_ISM_STATE_HYSTERESIS_WAITING;
break;
case STATE_EVENT(DM_ISM_STATE_OPTIMIZED_IDLE,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED):
*next_state = DM_ISM_STATE_FULL_POWER_RUNNING;
break;
case STATE_EVENT(DM_ISM_STATE_OPTIMIZED_IDLE,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE):
*next_state = DM_ISM_STATE_HYSTERESIS_BUSY;
break;
case STATE_EVENT(DM_ISM_STATE_OPTIMIZED_IDLE,
DM_ISM_EVENT_SSO_TIMER_ELAPSED):
case STATE_EVENT(DM_ISM_STATE_OPTIMIZED_IDLE,
DM_ISM_EVENT_IMMEDIATE):
*next_state = DM_ISM_STATE_OPTIMIZED_IDLE_SSO;
break;
case STATE_EVENT(DM_ISM_STATE_OPTIMIZED_IDLE_SSO,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED):
*next_state = DM_ISM_STATE_FULL_POWER_RUNNING;
break;
case STATE_EVENT(DM_ISM_STATE_OPTIMIZED_IDLE_SSO,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE):
*next_state = DM_ISM_STATE_HYSTERESIS_BUSY;
break;
case STATE_EVENT(DM_ISM_STATE_TIMER_ABORTED,
DM_ISM_EVENT_IMMEDIATE):
*next_state = DM_ISM_STATE_FULL_POWER_RUNNING;
break;
default:
return false;
}
return true;
}
static uint64_t dm_ism_get_sso_delay(const struct amdgpu_dm_ism *ism,
const struct dc_stream_state *stream)
{
const struct amdgpu_dm_ism_config *config = &ism->config;
uint32_t v_total, h_total;
uint64_t one_frame_ns, sso_delay_ns;
if (!stream)
return 0;
if (!config->sso_num_frames)
return 0;
v_total = stream->timing.v_total;
h_total = stream->timing.h_total;
one_frame_ns = div64_u64(v_total * h_total * 10000000ull,
stream->timing.pix_clk_100hz);
sso_delay_ns = config->sso_num_frames * one_frame_ns;
return sso_delay_ns;
}
/**
* dm_ism_get_idle_allow_delay - Calculate hysteresis-based idle allow delay
*/
static uint64_t dm_ism_get_idle_allow_delay(const struct amdgpu_dm_ism *ism,
const struct dc_stream_state *stream)
{
const struct amdgpu_dm_ism_config *config = &ism->config;
uint32_t v_total, h_total;
uint64_t one_frame_ns, short_idle_ns, old_hist_ns;
uint32_t history_size;
int pos;
uint32_t short_idle_count = 0;
uint64_t ret_ns = 0;
if (!stream)
return 0;
if (!config->filter_num_frames)
return 0;
if (!config->filter_entry_count)
return 0;
if (!config->activation_num_delay_frames)
return 0;
v_total = stream->timing.v_total;
h_total = stream->timing.h_total;
one_frame_ns = div64_u64(v_total * h_total * 10000000ull,
stream->timing.pix_clk_100hz);
short_idle_ns = config->filter_num_frames * one_frame_ns;
old_hist_ns = config->filter_old_history_threshold * one_frame_ns;
/*
* Look back into the recent history and count how many times we entered
* idle power state for a short duration of time
*/
history_size = min(
max(config->filter_history_size, config->filter_entry_count),
AMDGPU_DM_IDLE_HIST_LEN);
pos = ism->next_record_idx;
for (int k = 0; k < history_size; k++) {
if (pos <= 0 || pos > AMDGPU_DM_IDLE_HIST_LEN)
pos = AMDGPU_DM_IDLE_HIST_LEN;
pos -= 1;
if (ism->records[pos].duration_ns <= short_idle_ns)
short_idle_count += 1;
if (short_idle_count >= config->filter_entry_count)
break;
if (old_hist_ns > 0 &&
ism->last_idle_timestamp_ns - ism->records[pos].timestamp_ns > old_hist_ns)
break;
}
if (short_idle_count >= config->filter_entry_count)
ret_ns = config->activation_num_delay_frames * one_frame_ns;
return ret_ns;
}
/**
* dm_ism_insert_record - Insert a record into the circular history buffer
*/
static void dm_ism_insert_record(struct amdgpu_dm_ism *ism)
{
struct amdgpu_dm_ism_record *record;
if (ism->next_record_idx < 0 ||
ism->next_record_idx >= AMDGPU_DM_IDLE_HIST_LEN)
ism->next_record_idx = 0;
record = &ism->records[ism->next_record_idx];
ism->next_record_idx += 1;
record->timestamp_ns = ktime_get_ns();
record->duration_ns =
record->timestamp_ns - ism->last_idle_timestamp_ns;
}
static void dm_ism_set_last_idle_ts(struct amdgpu_dm_ism *ism)
{
ism->last_idle_timestamp_ns = ktime_get_ns();
}
static bool dm_ism_trigger_event(struct amdgpu_dm_ism *ism,
enum amdgpu_dm_ism_event event)
{
enum amdgpu_dm_ism_state next_state;
bool gotNextState = dm_ism_next_state(ism->current_state, event,
&next_state);
if (gotNextState) {
ism->previous_state = ism->current_state;
ism->current_state = next_state;
}
return gotNextState;
}
static void dm_ism_commit_idle_optimization_state(struct amdgpu_dm_ism *ism,
struct dc_stream_state *stream,
bool vblank_enabled,
bool allow_panel_sso)
{
struct amdgpu_crtc *acrtc = ism_to_amdgpu_crtc(ism);
struct amdgpu_device *adev = drm_to_adev(acrtc->base.dev);
struct amdgpu_display_manager *dm = &adev->dm;
int r;
trace_amdgpu_dm_ism_commit(dm->active_vblank_irq_count,
vblank_enabled,
allow_panel_sso);
/*
* If there is an active vblank requestor, or if SSO is being engaged,
* then disallow idle optimizations.
*/
if (vblank_enabled || allow_panel_sso)
dc_allow_idle_optimizations(dm->dc, false);
/*
* Control PSR based on vblank requirements from OS
*
* If panel supports PSR SU/Replay, there's no need to exit self-refresh
* when OS is submitting fast atomic commits, as they can allow
* self-refresh during vblank periods.
*/
if (stream && stream->link) {
/*
* If allow_panel_sso is true when disabling vblank, allow
* deeper panel sleep states such as PSR1 and Replay static
* screen optimization.
*/
if (!vblank_enabled && allow_panel_sso) {
amdgpu_dm_crtc_set_panel_sr_feature(
dm, acrtc, stream, false,
acrtc->dm_irq_params.allow_sr_entry);
} else if (vblank_enabled) {
/* Make sure to exit SSO on vblank enable */
amdgpu_dm_crtc_set_panel_sr_feature(
dm, acrtc, stream, true,
acrtc->dm_irq_params.allow_sr_entry);
}
/*
* Else, vblank_enabled == false and allow_panel_sso == false;
* do nothing here.
*/
}
/*
* Check for any active drm vblank requestors on other CRTCs
* (dm->active_vblank_irq_count) before allowing HW-wide idle
* optimizations.
*
* There's no need to have a "balanced" check when disallowing idle
* optimizations at the start of this func -- we should disallow
* whenever there's *an* active CRTC.
*/
if (!vblank_enabled && dm->active_vblank_irq_count == 0) {
dc_post_update_surfaces_to_stream(dm->dc);
r = amdgpu_dpm_pause_power_profile(adev, true);
if (r)
dev_warn(adev->dev, "failed to set default power profile mode\n");
dc_allow_idle_optimizations(dm->dc, true);
r = amdgpu_dpm_pause_power_profile(adev, false);
if (r)
dev_warn(adev->dev, "failed to restore the power profile mode\n");
}
}
static enum amdgpu_dm_ism_event dm_ism_dispatch_power_state(
struct amdgpu_dm_ism *ism,
struct dm_crtc_state *acrtc_state,
enum amdgpu_dm_ism_event event)
{
enum amdgpu_dm_ism_event ret = event;
const struct amdgpu_dm_ism_config *config = &ism->config;
uint64_t delay_ns, sso_delay_ns;
switch (ism->previous_state) {
case DM_ISM_STATE_HYSTERESIS_WAITING:
/*
* Stop the timer if it was set, and we're not running from the
* idle allow worker.
*/
if (ism->current_state != DM_ISM_STATE_OPTIMIZED_IDLE &&
ism->current_state != DM_ISM_STATE_OPTIMIZED_IDLE_SSO)
cancel_delayed_work(&ism->delayed_work);
break;
case DM_ISM_STATE_OPTIMIZED_IDLE:
if (ism->current_state == DM_ISM_STATE_OPTIMIZED_IDLE_SSO)
break;
/* If idle disallow, cancel SSO work and insert record */
cancel_delayed_work(&ism->sso_delayed_work);
dm_ism_insert_record(ism);
dm_ism_commit_idle_optimization_state(ism, acrtc_state->stream,
true, false);
break;
case DM_ISM_STATE_OPTIMIZED_IDLE_SSO:
/* Disable idle optimization */
dm_ism_insert_record(ism);
dm_ism_commit_idle_optimization_state(ism, acrtc_state->stream,
true, false);
break;
default:
break;
}
switch (ism->current_state) {
case DM_ISM_STATE_HYSTERESIS_WAITING:
dm_ism_set_last_idle_ts(ism);
/* CRTC can be disabled; allow immediate idle */
if (!acrtc_state->stream) {
ret = DM_ISM_EVENT_IMMEDIATE;
break;
}
delay_ns = dm_ism_get_idle_allow_delay(ism,
acrtc_state->stream);
if (delay_ns == 0) {
ret = DM_ISM_EVENT_IMMEDIATE;
break;
}
/* Schedule worker */
mod_delayed_work(system_unbound_wq, &ism->delayed_work,
nsecs_to_jiffies(delay_ns));
break;
case DM_ISM_STATE_OPTIMIZED_IDLE:
sso_delay_ns = dm_ism_get_sso_delay(ism, acrtc_state->stream);
if (sso_delay_ns == 0)
ret = DM_ISM_EVENT_IMMEDIATE;
else if (config->sso_num_frames < config->filter_num_frames) {
/*
* If sso_num_frames is less than hysteresis frames, it
* indicates that allowing idle here, then disallowing
* idle after sso_num_frames has expired, will likely
* have a negative power impact. Skip idle allow here,
* and let the sso_delayed_work handle it.
*/
mod_delayed_work(system_unbound_wq,
&ism->sso_delayed_work,
nsecs_to_jiffies(sso_delay_ns));
} else {
/* Enable idle optimization without SSO */
dm_ism_commit_idle_optimization_state(
ism, acrtc_state->stream, false, false);
mod_delayed_work(system_unbound_wq,
&ism->sso_delayed_work,
nsecs_to_jiffies(sso_delay_ns));
}
break;
case DM_ISM_STATE_OPTIMIZED_IDLE_SSO:
/* Enable static screen optimizations. */
dm_ism_commit_idle_optimization_state(ism, acrtc_state->stream,
false, true);
break;
case DM_ISM_STATE_TIMER_ABORTED:
dm_ism_insert_record(ism);
dm_ism_commit_idle_optimization_state(ism, acrtc_state->stream,
true, false);
ret = DM_ISM_EVENT_IMMEDIATE;
break;
default:
break;
}
return ret;
}
static char *dm_ism_events_str[DM_ISM_NUM_EVENTS] = {
[DM_ISM_EVENT_IMMEDIATE] = "IMMEDIATE",
[DM_ISM_EVENT_ENTER_IDLE_REQUESTED] = "ENTER_IDLE_REQUESTED",
[DM_ISM_EVENT_EXIT_IDLE_REQUESTED] = "EXIT_IDLE_REQUESTED",
[DM_ISM_EVENT_BEGIN_CURSOR_UPDATE] = "BEGIN_CURSOR_UPDATE",
[DM_ISM_EVENT_END_CURSOR_UPDATE] = "END_CURSOR_UPDATE",
[DM_ISM_EVENT_TIMER_ELAPSED] = "TIMER_ELAPSED",
[DM_ISM_EVENT_SSO_TIMER_ELAPSED] = "SSO_TIMER_ELAPSED",
};
static char *dm_ism_states_str[DM_ISM_NUM_STATES] = {
[DM_ISM_STATE_FULL_POWER_RUNNING] = "FULL_POWER_RUNNING",
[DM_ISM_STATE_FULL_POWER_BUSY] = "FULL_POWER_BUSY",
[DM_ISM_STATE_HYSTERESIS_WAITING] = "HYSTERESIS_WAITING",
[DM_ISM_STATE_HYSTERESIS_BUSY] = "HYSTERESIS_BUSY",
[DM_ISM_STATE_OPTIMIZED_IDLE] = "OPTIMIZED_IDLE",
[DM_ISM_STATE_OPTIMIZED_IDLE_SSO] = "OPTIMIZED_IDLE_SSO",
[DM_ISM_STATE_TIMER_ABORTED] = "TIMER_ABORTED",
};
void amdgpu_dm_ism_commit_event(struct amdgpu_dm_ism *ism,
enum amdgpu_dm_ism_event event)
{
enum amdgpu_dm_ism_event next_event = event;
struct amdgpu_crtc *acrtc = ism_to_amdgpu_crtc(ism);
struct amdgpu_device *adev = drm_to_adev(acrtc->base.dev);
struct amdgpu_display_manager *dm = &adev->dm;
struct dm_crtc_state *acrtc_state = to_dm_crtc_state(acrtc->base.state);
/* ISM transitions must be called with mutex acquired */
ASSERT(mutex_is_locked(&dm->dc_lock));
if (!acrtc_state) {
trace_amdgpu_dm_ism_event(acrtc->crtc_id, "NO_STATE",
"NO_STATE", "N/A");
return;
}
do {
bool transition = dm_ism_trigger_event(ism, event);
next_event = DM_ISM_NUM_EVENTS;
if (transition) {
trace_amdgpu_dm_ism_event(
acrtc->crtc_id,
dm_ism_states_str[ism->previous_state],
dm_ism_states_str[ism->current_state],
dm_ism_events_str[event]);
next_event = dm_ism_dispatch_power_state(
ism, acrtc_state, next_event);
} else {
trace_amdgpu_dm_ism_event(
acrtc->crtc_id,
dm_ism_states_str[ism->current_state],
dm_ism_states_str[ism->current_state],
dm_ism_events_str[event]);
}
event = next_event;
} while (next_event < DM_ISM_NUM_EVENTS);
}
static void dm_ism_delayed_work_func(struct work_struct *work)
{
struct amdgpu_dm_ism *ism =
container_of(work, struct amdgpu_dm_ism, delayed_work.work);
struct amdgpu_crtc *acrtc = ism_to_amdgpu_crtc(ism);
struct amdgpu_device *adev = drm_to_adev(acrtc->base.dev);
struct amdgpu_display_manager *dm = &adev->dm;
guard(mutex)(&dm->dc_lock);
amdgpu_dm_ism_commit_event(ism, DM_ISM_EVENT_TIMER_ELAPSED);
}
static void dm_ism_sso_delayed_work_func(struct work_struct *work)
{
struct amdgpu_dm_ism *ism =
container_of(work, struct amdgpu_dm_ism, sso_delayed_work.work);
struct amdgpu_crtc *acrtc = ism_to_amdgpu_crtc(ism);
struct amdgpu_device *adev = drm_to_adev(acrtc->base.dev);
struct amdgpu_display_manager *dm = &adev->dm;
guard(mutex)(&dm->dc_lock);
amdgpu_dm_ism_commit_event(ism, DM_ISM_EVENT_SSO_TIMER_ELAPSED);
}
/**
* amdgpu_dm_ism_disable - Disable the ISM
*
* @dm: The amdgpu display manager
*
* Disable the idle state manager by disabling any ISM work, canceling pending
* work, and waiting for in-progress work to finish. After disabling, the system
* is left in DM_ISM_STATE_FULL_POWER_RUNNING state.
*/
void amdgpu_dm_ism_disable(struct amdgpu_display_manager *dm)
{
struct drm_crtc *crtc;
struct amdgpu_crtc *acrtc;
struct amdgpu_dm_ism *ism;
drm_for_each_crtc(crtc, dm->ddev) {
acrtc = to_amdgpu_crtc(crtc);
ism = &acrtc->ism;
/* Cancel and disable any pending work */
disable_delayed_work_sync(&ism->delayed_work);
disable_delayed_work_sync(&ism->sso_delayed_work);
/*
* When disabled, leave in FULL_POWER_RUNNING state.
* EXIT_IDLE will not queue any work
*/
amdgpu_dm_ism_commit_event(ism,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED);
}
}
/**
* amdgpu_dm_ism_enable - enable the ISM
*
* @dm: The amdgpu display manager
*
* Re-enable the idle state manager by enabling work that was disabled by
* amdgpu_dm_ism_disable.
*/
void amdgpu_dm_ism_enable(struct amdgpu_display_manager *dm)
{
struct drm_crtc *crtc;
struct amdgpu_crtc *acrtc;
struct amdgpu_dm_ism *ism;
drm_for_each_crtc(crtc, dm->ddev) {
acrtc = to_amdgpu_crtc(crtc);
ism = &acrtc->ism;
enable_delayed_work(&ism->delayed_work);
enable_delayed_work(&ism->sso_delayed_work);
}
}
void amdgpu_dm_ism_init(struct amdgpu_dm_ism *ism,
struct amdgpu_dm_ism_config *config)
{
ism->config = *config;
ism->current_state = DM_ISM_STATE_FULL_POWER_RUNNING;
ism->previous_state = DM_ISM_STATE_FULL_POWER_RUNNING;
ism->next_record_idx = 0;
ism->last_idle_timestamp_ns = 0;
INIT_DELAYED_WORK(&ism->delayed_work, dm_ism_delayed_work_func);
INIT_DELAYED_WORK(&ism->sso_delayed_work, dm_ism_sso_delayed_work_func);
}
void amdgpu_dm_ism_fini(struct amdgpu_dm_ism *ism)
{
cancel_delayed_work_sync(&ism->sso_delayed_work);
cancel_delayed_work_sync(&ism->delayed_work);
}

View File

@@ -0,0 +1,151 @@
/* SPDX-License-Identifier: MIT */
/*
* Copyright 2026 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Authors: AMD
*
*/
#ifndef __AMDGPU_DM_ISM_H__
#define __AMDGPU_DM_ISM_H__
#include <linux/workqueue.h>
struct amdgpu_crtc;
struct amdgpu_display_manager;
#define AMDGPU_DM_IDLE_HIST_LEN 16
enum amdgpu_dm_ism_state {
DM_ISM_STATE_FULL_POWER_RUNNING,
DM_ISM_STATE_FULL_POWER_BUSY,
DM_ISM_STATE_HYSTERESIS_WAITING,
DM_ISM_STATE_HYSTERESIS_BUSY,
DM_ISM_STATE_OPTIMIZED_IDLE,
DM_ISM_STATE_OPTIMIZED_IDLE_SSO,
DM_ISM_STATE_TIMER_ABORTED,
DM_ISM_NUM_STATES,
};
enum amdgpu_dm_ism_event {
DM_ISM_EVENT_IMMEDIATE,
DM_ISM_EVENT_ENTER_IDLE_REQUESTED,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE,
DM_ISM_EVENT_END_CURSOR_UPDATE,
DM_ISM_EVENT_TIMER_ELAPSED,
DM_ISM_EVENT_SSO_TIMER_ELAPSED,
DM_ISM_NUM_EVENTS,
};
#define STATE_EVENT(state, event) (((state) << 8) | (event))
struct amdgpu_dm_ism_config {
/**
* @filter_num_frames: Idle periods shorter than this number of frames
* will be considered a "short idle period" for filtering.
*
* 0 indicates no filtering (i.e. no idle allow delay will be applied)
*/
unsigned int filter_num_frames;
/**
* @filter_history_size: Number of recent idle periods to consider when
* counting the number of short idle periods.
*/
unsigned int filter_history_size;
/**
* @filter_entry_count: When the number of short idle periods within
* recent &filter_history_size reaches this count, the idle allow delay
* will be applied.
*
* 0 indicates no filtering (i.e. no idle allow delay will be applied)
*/
unsigned int filter_entry_count;
/**
* @activation_num_delay_frames: Defines the number of frames to wait
* for the idle allow delay.
*
* 0 indicates no filtering (i.e. no idle allow delay will be applied)
*/
unsigned int activation_num_delay_frames;
/**
* @filter_old_history_threshold: A time-based restriction on top of
* &filter_history_size. Idle periods older than this threshold (in
* number of frames) will be ignored when counting the number of short
* idle periods.
*
* 0 indicates no time-based restriction, i.e. history is limited only
* by &filter_history_size.
*/
unsigned int filter_old_history_threshold;
/**
* @sso_num_frames: Number of frames to delay before enabling static
* screen optimizations, such as PSR1 and Replay low HZ idle mode.
*
* 0 indicates immediate SSO enable upon allowing idle.
*/
unsigned int sso_num_frames;
};
struct amdgpu_dm_ism_record {
/**
* @timestamp_ns: When idle was allowed
*/
unsigned long long timestamp_ns;
/**
* @duration_ns: How long idle was allowed
*/
unsigned long long duration_ns;
};
struct amdgpu_dm_ism {
struct amdgpu_dm_ism_config config;
unsigned long long last_idle_timestamp_ns;
enum amdgpu_dm_ism_state current_state;
enum amdgpu_dm_ism_state previous_state;
struct amdgpu_dm_ism_record records[AMDGPU_DM_IDLE_HIST_LEN];
int next_record_idx;
struct delayed_work delayed_work;
struct delayed_work sso_delayed_work;
};
#define ism_to_amdgpu_crtc(ism_ptr) \
container_of(ism_ptr, struct amdgpu_crtc, ism)
void amdgpu_dm_ism_init(struct amdgpu_dm_ism *ism,
struct amdgpu_dm_ism_config *config);
void amdgpu_dm_ism_fini(struct amdgpu_dm_ism *ism);
void amdgpu_dm_ism_commit_event(struct amdgpu_dm_ism *ism,
enum amdgpu_dm_ism_event event);
void amdgpu_dm_ism_disable(struct amdgpu_display_manager *dm);
void amdgpu_dm_ism_enable(struct amdgpu_display_manager *dm);
#endif

View File

@@ -1374,8 +1374,16 @@ void amdgpu_dm_plane_handle_cursor_update(struct drm_plane *plane,
/* turn off cursor */
if (crtc_state && crtc_state->stream) {
mutex_lock(&adev->dm.dc_lock);
amdgpu_dm_ism_commit_event(
&amdgpu_crtc->ism,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE);
dc_stream_program_cursor_position(crtc_state->stream,
&position);
amdgpu_dm_ism_commit_event(
&amdgpu_crtc->ism,
DM_ISM_EVENT_END_CURSOR_UPDATE);
mutex_unlock(&adev->dm.dc_lock);
}
return;
@@ -1405,6 +1413,10 @@ void amdgpu_dm_plane_handle_cursor_update(struct drm_plane *plane,
if (crtc_state->stream) {
mutex_lock(&adev->dm.dc_lock);
amdgpu_dm_ism_commit_event(
&amdgpu_crtc->ism,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE);
if (!dc_stream_program_cursor_attributes(crtc_state->stream,
&attributes))
DRM_ERROR("DC failed to set cursor attributes\n");
@@ -1412,6 +1424,10 @@ void amdgpu_dm_plane_handle_cursor_update(struct drm_plane *plane,
if (!dc_stream_program_cursor_position(crtc_state->stream,
&position))
DRM_ERROR("DC failed to set cursor position\n");
amdgpu_dm_ism_commit_event(
&amdgpu_crtc->ism,
DM_ISM_EVENT_END_CURSOR_UPDATE);
mutex_unlock(&adev->dm.dc_lock);
}
}

View File

@@ -753,6 +753,69 @@ TRACE_EVENT(amdgpu_dm_brightness,
)
);
TRACE_EVENT(amdgpu_dm_ism_commit,
TP_PROTO(
int active_vblank_irq_count,
bool vblank_enabled,
bool allow_panel_sso
),
TP_ARGS(
active_vblank_irq_count,
vblank_enabled,
allow_panel_sso
),
TP_STRUCT__entry(
__field(int, active_vblank_irq_count)
__field(bool, vblank_enabled)
__field(bool, allow_panel_sso)
),
TP_fast_assign(
__entry->active_vblank_irq_count = active_vblank_irq_count;
__entry->vblank_enabled = vblank_enabled;
__entry->allow_panel_sso = allow_panel_sso;
),
TP_printk(
"active_vblank_irq_count=%d vblank_enabled=%d allow_panel_sso=%d",
__entry->active_vblank_irq_count,
__entry->vblank_enabled,
__entry->allow_panel_sso
)
);
TRACE_EVENT(amdgpu_dm_ism_event,
TP_PROTO(
int crtc_id,
const char *prev_state,
const char *curr_state,
const char *event
),
TP_ARGS(
crtc_id,
prev_state,
curr_state,
event
),
TP_STRUCT__entry(
__field(int, crtc_id)
__string(prev_state, prev_state)
__string(curr_state, curr_state)
__string(event, event)
),
TP_fast_assign(
__entry->crtc_id = crtc_id;
__assign_str(prev_state);
__assign_str(curr_state);
__assign_str(event);
),
TP_printk(
"[CRTC %d] %s -> %s on event %s",
__entry->crtc_id,
__get_str(prev_state),
__get_str(curr_state),
__get_str(event))
);
#endif /* _AMDGPU_DM_TRACE_H_ */
#undef TRACE_INCLUDE_PATH