From: Alex Hung <[email protected]>

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 <[email protected]>
Signed-off-by: Alex Hung <[email protected]>
Signed-off-by: George Zhang <[email protected]>
---
 .../drm/amd/display/amdgpu_dm/amdgpu_dm_ism.c |   4 +
 .../amdgpu_dm/tests/amdgpu_dm_ism_test.c      | 353 ++++++++++++++++++
 2 files changed, 357 insertions(+)

diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_ism.c 
b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_ism.c
index 32391b56097e..a9575bf25fc2 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_ism.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_ism.c
@@ -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)
diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_ism_test.c 
b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_ism_test.c
index 7dfb3b351d20..6394b967ac3a 100644
--- a/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_ism_test.c
+++ b/drivers/gpu/drm/amd/display/amdgpu_dm/tests/amdgpu_dm_ism_test.c
@@ -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),
        {}
 };

--
2.55.0

Reply via email to