drm/amd/display: Add KUnit tests for ism

Add KUnit coverage for the following ISM functions:

- dm_ism_next_state(): state transitions for running, busy,
  hysteresis-wait, optimized-idle, and aborted states
- dm_ism_get_sso_delay(): null stream, zero frames, 1080p60 3-frame,
  and 4k60 1-frame calculations
- dm_ism_get_idle_allow_delay(): null stream, zero filter/entry/delay
  frames, short-idle filtering, wrap-around, old history cutoff,
  mixed durations, and entry count exceeding history size
- amdgpu_dm_ism_init(): initial state setup
- amdgpu_dm_ism_fini(): cleanup after init
- dm_ism_set_last_idle_ts(): timestamp update
- dm_ism_insert_record(): basic insert and wrap-around
- dm_ism_trigger_event(): valid and invalid transitions

Assisted-by: Copilot:Claude-Opus-4.8
Reviewed-by: Bhawanpreet Lakha <bhawanpreet.lakha@amd.com>
Signed-off-by: Alex Hung <alex.hung@amd.com>
Signed-off-by: George Zhang <george.zhang@amd.com>
Signed-off-by: Alex Deucher <alexander.deucher@amd.com>
This commit is contained in:
Alex Hung
2026-06-18 21:12:21 -06:00
committed by Alex Deucher
parent 5a22cc7c72
commit 3b1fdd6413
2 changed files with 357 additions and 0 deletions

View File

@@ -502,6 +502,7 @@ void amdgpu_dm_ism_commit_event(struct amdgpu_dm_ism *ism,
} while (next_event < DM_ISM_NUM_EVENTS);
}
EXPORT_IF_KUNIT(amdgpu_dm_ism_commit_event);
static void dm_ism_delayed_work_func(struct work_struct *work)
@@ -568,6 +569,7 @@ void amdgpu_dm_ism_disable(struct amdgpu_display_manager *dm)
disable_delayed_work_sync(&ism->sso_delayed_work);
}
}
EXPORT_IF_KUNIT(amdgpu_dm_ism_disable);
/**
* amdgpu_dm_ism_force_full_power - Force every CRTC's ISM FSM to FULL_POWER
@@ -603,6 +605,7 @@ void amdgpu_dm_ism_force_full_power(struct amdgpu_display_manager *dm)
DM_ISM_EVENT_EXIT_IDLE_REQUESTED);
}
}
EXPORT_IF_KUNIT(amdgpu_dm_ism_force_full_power);
/**
* amdgpu_dm_ism_enable - enable the ISM
@@ -626,6 +629,7 @@ void amdgpu_dm_ism_enable(struct amdgpu_display_manager *dm)
enable_delayed_work(&ism->sso_delayed_work);
}
}
EXPORT_IF_KUNIT(amdgpu_dm_ism_enable);
void amdgpu_dm_ism_init(struct amdgpu_dm_ism *ism,
struct amdgpu_dm_ism_config *config)

View File

@@ -8,6 +8,9 @@
#include <kunit/test.h>
#include "dc.h"
#include "amdgpu.h"
#include "amdgpu_mode.h"
#include "amdgpu_dm.h"
#include "amdgpu_dm_ism.h"
#include "amdgpu_dm_kunit_test_helpers.h"
@@ -848,6 +851,345 @@ static void dm_test_dispatch_next_event_no_action_state(struct kunit *test)
KUNIT_EXPECT_EQ(test, (int)result, (int)DM_ISM_NUM_EVENTS);
}
/*
* Helper: allocate an amdgpu_crtc (which embeds the ISM) wired up to a
* minimally-populated amdgpu_device so that the container_of()/drm_to_adev()
* lookups inside the ISM event machinery resolve correctly.
*
* The amdgpu_device is large, so it must be heap-allocated via kunit_kzalloc.
* acrtc->base.dev points at the embedded &adev->ddev, which is what
* drm_to_adev() expects (it is a container_of of ddev).
*/
static struct amdgpu_crtc *alloc_test_acrtc(struct kunit *test,
struct amdgpu_device **adev_out)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc;
struct dc *dc;
adev = kunit_kzalloc(test, sizeof(*adev), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, adev);
dc = kunit_kzalloc(test, sizeof(*dc), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dc);
acrtc = kunit_kzalloc(test, sizeof(*acrtc), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, acrtc);
adev->dm.dc = dc;
adev->dm.ddev = &adev->ddev;
mutex_init(&adev->dm.dc_lock);
acrtc->base.dev = &adev->ddev;
if (adev_out)
*adev_out = adev;
return acrtc;
}
/*
* Helper: register an already-allocated amdgpu_crtc into the DRM device's
* crtc_list so that drm_for_each_crtc() iterates it. Only the list linkage is
* required for the ISM enable/disable/force-full-power helpers.
*/
static void register_test_acrtc(struct amdgpu_device *adev,
struct amdgpu_crtc *acrtc)
{
INIT_LIST_HEAD(&adev->ddev.mode_config.crtc_list);
INIT_LIST_HEAD(&acrtc->base.head);
list_add_tail(&acrtc->base.head, &adev->ddev.mode_config.crtc_list);
}
/* ===== Tests for amdgpu_dm_ism_commit_event ===== */
/**
* dm_test_ism_commit_event_no_state - commit_event returns early without a crtc state
* @test: KUnit test context
*
* When the CRTC has no atomic state (base.state == NULL) the function takes
* the NO_STATE early-return path and the FSM is left untouched.
*/
static void dm_test_ism_commit_event_no_state(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct amdgpu_dm_ism_config config = { 0 };
amdgpu_dm_ism_init(&acrtc->ism, &config);
acrtc->base.state = NULL;
guard(mutex)(&adev->dm.dc_lock);
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_FULL_POWER_RUNNING);
amdgpu_dm_ism_fini(&acrtc->ism);
}
/**
* dm_test_ism_commit_event_cursor_transition - cursor begin/end drive FSM without DC work
* @test: KUnit test context
*
* BEGIN_CURSOR_UPDATE then END_CURSOR_UPDATE from FULL_POWER_RUNNING traverse
* the FULL_POWER_BUSY state. Neither transition reaches the idle-optimization
* commit path, so no DC hardware access occurs and the FSM returns to
* FULL_POWER_RUNNING.
*/
static void dm_test_ism_commit_event_cursor_transition(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct dm_crtc_state *dm_state;
struct amdgpu_dm_ism_config config = { 0 };
dm_state = kunit_kzalloc(test, sizeof(*dm_state), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dm_state);
amdgpu_dm_ism_init(&acrtc->ism, &config);
acrtc->base.state = &dm_state->base;
guard(mutex)(&adev->dm.dc_lock);
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_FULL_POWER_BUSY);
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_END_CURSOR_UPDATE);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_FULL_POWER_RUNNING);
amdgpu_dm_ism_fini(&acrtc->ism);
}
/**
* dm_test_ism_commit_event_invalid_event - invalid event leaves FSM unchanged
* @test: KUnit test context
*
* EXIT_IDLE_REQUESTED is not a valid event from FULL_POWER_RUNNING, so the
* FSM does not transition and no power-state dispatch occurs.
*/
static void dm_test_ism_commit_event_invalid_event(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct dm_crtc_state *dm_state;
struct amdgpu_dm_ism_config config = { 0 };
dm_state = kunit_kzalloc(test, sizeof(*dm_state), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dm_state);
amdgpu_dm_ism_init(&acrtc->ism, &config);
acrtc->base.state = &dm_state->base;
guard(mutex)(&adev->dm.dc_lock);
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_EXIT_IDLE_REQUESTED);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_FULL_POWER_RUNNING);
amdgpu_dm_ism_fini(&acrtc->ism);
}
/* ===== Tests for amdgpu_dm_ism_force_full_power ===== */
/**
* dm_test_ism_force_full_power - force-full-power sends EXIT_IDLE to every CRTC
* @test: KUnit test context
*
* From the initial FULL_POWER_RUNNING state EXIT_IDLE_REQUESTED is a no-op, so
* the FSM remains in FULL_POWER_RUNNING and no DC work is scheduled.
*/
static void dm_test_ism_force_full_power(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct dm_crtc_state *dm_state;
struct amdgpu_dm_ism_config config = { 0 };
dm_state = kunit_kzalloc(test, sizeof(*dm_state), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dm_state);
amdgpu_dm_ism_init(&acrtc->ism, &config);
acrtc->base.state = &dm_state->base;
register_test_acrtc(adev, acrtc);
guard(mutex)(&adev->dm.dc_lock);
amdgpu_dm_ism_force_full_power(&adev->dm);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_FULL_POWER_RUNNING);
amdgpu_dm_ism_fini(&acrtc->ism);
}
/* ===== Tests for amdgpu_dm_ism_disable / amdgpu_dm_ism_enable ===== */
/**
* dm_test_ism_disable_enable_cycle - disable then enable quiesces and re-arms work
* @test: KUnit test context
*
* Walks every CRTC's ISM, disabling its delayed work (synchronously) and then
* re-enabling it. With no work ever scheduled both calls complete without
* touching the FSM state. disable must be called without dc_lock held.
*/
static void dm_test_ism_disable_enable_cycle(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct amdgpu_dm_ism_config config = { 0 };
amdgpu_dm_ism_init(&acrtc->ism, &config);
register_test_acrtc(adev, acrtc);
amdgpu_dm_ism_disable(&adev->dm);
amdgpu_dm_ism_enable(&adev->dm);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_FULL_POWER_RUNNING);
amdgpu_dm_ism_fini(&acrtc->ism);
}
/* ===== Tests for dm_ism_dispatch_power_state (via commit_event) ===== */
/*
* Build a config + history that makes dm_ism_get_idle_allow_delay() return a
* non-zero hysteresis delay. A non-zero delay keeps the FSM parked in
* HYSTERESIS_WAITING (the dispatcher returns DM_ISM_NUM_EVENTS instead of an
* immediate follow-up event), preventing the cascade into the DC-dependent
* OPTIMIZED_IDLE / *_SSO states.
*/
static void setup_idle_delay_history(struct amdgpu_dm_ism *ism,
struct dc_stream_state *stream)
{
uint64_t one_frame_ns;
stream->timing.v_total = 1125;
stream->timing.h_total = 2200;
stream->timing.pix_clk_100hz = 1485000;
one_frame_ns = div64_u64((uint64_t)1125 * 2200 * 10000000ULL, 1485000);
for (int i = 0; i < 8; i++) {
ism->records[i].duration_ns = one_frame_ns;
ism->records[i].timestamp_ns = 0;
}
ism->next_record_idx = 8;
}
/**
* dm_test_ism_dispatch_hysteresis_schedule_and_cancel - cover the HYSTERESIS_WAITING dispatch arms
* @test: KUnit test context
*
* ENTER_IDLE_REQUESTED moves FULL_POWER_RUNNING -> HYSTERESIS_WAITING. The
* current-state arm of dm_ism_dispatch_power_state() then records the idle
* timestamp, computes a (non-zero) idle-allow delay and schedules the delayed
* worker. A subsequent BEGIN_CURSOR_UPDATE (-> HYSTERESIS_BUSY) exercises the
* previous-state arm that cancels that pending worker. Neither arm reaches the
* DC-dependent idle-optimization commit path.
*/
static void dm_test_ism_dispatch_hysteresis_schedule_and_cancel(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct dm_crtc_state *dm_state;
struct dc_stream_state *stream;
struct amdgpu_dm_ism_config config = {
.filter_num_frames = 5,
.filter_entry_count = 3,
.activation_num_delay_frames = 10,
.filter_history_size = 8,
.filter_old_history_threshold = 0,
.sso_num_frames = 0,
};
dm_state = kunit_kzalloc(test, sizeof(*dm_state), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dm_state);
stream = dm_kunit_alloc_stream(test, NULL);
amdgpu_dm_ism_init(&acrtc->ism, &config);
setup_idle_delay_history(&acrtc->ism, stream);
dm_state->stream = stream;
acrtc->base.state = &dm_state->base;
scoped_guard(mutex, &adev->dm.dc_lock) {
/* Enter HYSTERESIS_WAITING: schedules the idle-allow worker. */
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_ENTER_IDLE_REQUESTED);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_HYSTERESIS_WAITING);
/* Cursor update cancels the pending worker (prev-state arm). */
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_BEGIN_CURSOR_UPDATE);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_HYSTERESIS_BUSY);
}
amdgpu_dm_ism_fini(&acrtc->ism);
}
/**
* dm_test_ism_dispatch_optimized_idle_defers_sso - cover OPTIMIZED_IDLE dispatch without DC
* @test: KUnit test context
*
* With sso_num_frames < filter_num_frames the OPTIMIZED_IDLE current-state arm
* skips the idle-optimization commit and only schedules the SSO worker. Driving
* HYSTERESIS_WAITING -> OPTIMIZED_IDLE via TIMER_ELAPSED exercises that arm
* (get_sso_delay + the skip branch + mod_delayed_work) without any DC access.
*/
static void dm_test_ism_dispatch_optimized_idle_defers_sso(struct kunit *test)
{
struct amdgpu_device *adev;
struct amdgpu_crtc *acrtc = alloc_test_acrtc(test, &adev);
struct dm_crtc_state *dm_state;
struct dc_stream_state *stream;
struct amdgpu_dm_ism_config config = {
.filter_num_frames = 5,
.filter_entry_count = 3,
.activation_num_delay_frames = 10,
.filter_history_size = 8,
.filter_old_history_threshold = 0,
.sso_num_frames = 2,
};
dm_state = kunit_kzalloc(test, sizeof(*dm_state), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dm_state);
stream = dm_kunit_alloc_stream(test, NULL);
amdgpu_dm_ism_init(&acrtc->ism, &config);
setup_idle_delay_history(&acrtc->ism, stream);
dm_state->stream = stream;
acrtc->base.state = &dm_state->base;
scoped_guard(mutex, &adev->dm.dc_lock) {
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_ENTER_IDLE_REQUESTED);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_HYSTERESIS_WAITING);
/*
* Timer fires: HYSTERESIS_WAITING -> OPTIMIZED_IDLE. sso_delay
* is non-zero and sso_num_frames < filter_num_frames, so the
* commit is deferred to the SSO worker and the FSM parks here.
*/
amdgpu_dm_ism_commit_event(&acrtc->ism,
DM_ISM_EVENT_TIMER_ELAPSED);
KUNIT_EXPECT_EQ(test, (int)acrtc->ism.current_state,
(int)DM_ISM_STATE_OPTIMIZED_IDLE);
/* Cancel the scheduled SSO worker while still holding dc_lock. */
cancel_delayed_work(&acrtc->ism.sso_delayed_work);
}
amdgpu_dm_ism_fini(&acrtc->ism);
}
static struct kunit_case dm_ism_test_cases[] = {
/* dm_ism_next_state — FULL_POWER_RUNNING */
KUNIT_CASE(dm_test_ism_next_state_running_enter_idle),
@@ -910,6 +1252,17 @@ static struct kunit_case dm_ism_test_cases[] = {
KUNIT_CASE(dm_test_dispatch_next_event_opt_idle_with_sso_delay),
KUNIT_CASE(dm_test_dispatch_next_event_timer_aborted),
KUNIT_CASE(dm_test_dispatch_next_event_no_action_state),
/* amdgpu_dm_ism_commit_event */
KUNIT_CASE(dm_test_ism_commit_event_no_state),
KUNIT_CASE(dm_test_ism_commit_event_cursor_transition),
KUNIT_CASE(dm_test_ism_commit_event_invalid_event),
/* amdgpu_dm_ism_force_full_power */
KUNIT_CASE(dm_test_ism_force_full_power),
/* amdgpu_dm_ism_disable / amdgpu_dm_ism_enable */
KUNIT_CASE(dm_test_ism_disable_enable_cycle),
/* dm_ism_dispatch_power_state (via commit_event) */
KUNIT_CASE(dm_test_ism_dispatch_hysteresis_schedule_and_cancel),
KUNIT_CASE(dm_test_ism_dispatch_optimized_idle_defers_sso),
{}
};