]> git.hungrycats.org Git - linux/commitdiff
drm/amd/display: Tear down dangling pipe on boot to fix s0i3
authorTom Chung <chiahsuan.chung@amd.com>
Tue, 23 Jun 2026 07:35:30 +0000 (15:35 +0800)
committerAlex Deucher <alexander.deucher@amd.com>
Wed, 15 Jul 2026 13:15:38 +0000 (09:15 -0400)
[Why]
If an external monitor is connected at power-on and then unplugged
before the driver loads (e.g. at the GRUB menu), the system can no
longer enter the s0i3 deepest suspend state, even though all connectors
report disconnected.

The pre-OS firmware (GOP/vBIOS) lights up a front-end for the display
that is present at power-on. The driver never fully tears this inherited
pipe down, and the leftover pipe keeps the DCN block from reaching idle,
which blocks s0i3.

[How]
Add dc_disable_dangling_timing_generators() to DC core and call it from
amdgpu_dm right after dc_hardware_init(). It scans every enabled timing
generator and classifies it as in-use or dangling. If a dangling pipe
exists and nothing needs to be preserved, power down the hw blocks so
DCN can reach idle.

Assisted-by: Cursor:claude-opus-4.8
Reviewed-by: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Tom Chung <chiahsuan.chung@amd.com>
Signed-off-by: George Zhang <george.zhang@amd.com>
Signed-off-by: Alex Deucher <alexander.deucher@amd.com>
drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
drivers/gpu/drm/amd/display/dc/core/dc.c
drivers/gpu/drm/amd/display/dc/dc.h

index fb19a0781e592ca2d35c97507790f9e705967b61..65e7d49a5f931a8856c4854bcdef9f0302cee6ff 100644 (file)
@@ -779,6 +779,14 @@ static int amdgpu_dm_init(struct amdgpu_device *adev)
 
        dc_hardware_init(adev->dm.dc);
 
+       /* GOP/vBIOS may leave an OPTC enabled for a display present at power-on
+        * but no longer driven (e.g. an external DP unplugged at boot). Such a
+        * dangling pipe keeps DCN out of idle and blocks s0i3. Power it down
+        * here when nothing needs to be preserved.
+        */
+       if (adev->flags & AMD_IS_APU)
+               dc_disable_dangling_timing_generators(adev->dm.dc);
+
        adev->dm.hpd_rx_offload_wq = amdgpu_dm_hpd_rx_irq_create_workqueue(adev);
        if (!adev->dm.hpd_rx_offload_wq) {
                drm_err(adev_to_drm(adev), "failed to create hpd rx offload workqueue.\n");
index ec5d6ddeefbee401e54fbb11326952df50c59317..7934341b47157965248a9d0428307c483c3b22c7 100644 (file)
@@ -6233,6 +6233,115 @@ void dc_interrupt_ack(struct dc *dc, enum dc_irq_source src)
        dal_irq_service_ack(dc->res_pool->irqs, src);
 }
 
+/* Preserve this tg if a physical link is still lighting a present display */
+static bool should_preserve_tg(struct dc *dc, struct timing_generator *tg)
+{
+       unsigned int i, j;
+
+       /* Check if a physical link is lighting this tg */
+       for (i = 0; i < dc->link_count; i++) {
+               struct dc_link *link = dc->links[i];
+               int fe;
+
+               if (!link || link->ep_type != DISPLAY_ENDPOINT_PHY ||
+                               !link->link_enc ||
+                               !link->link_enc->funcs->is_dig_enabled ||
+                               !link->link_enc->funcs->is_dig_enabled(link->link_enc) ||
+                               !link->link_enc->funcs->get_dig_frontend)
+                       continue;
+
+               /* Get the DIG front-end this link's encoder drives; skip if none */
+               fe = link->link_enc->funcs->get_dig_frontend(link->link_enc);
+               if (fe == ENGINE_ID_UNKNOWN)
+                       continue;
+
+               /* Find the stream encoder bound to this link's front-end */
+               for (j = 0; j < dc->res_pool->stream_enc_count; j++) {
+                       struct stream_encoder *se = dc->res_pool->stream_enc[j];
+
+                       /* Skip unless this stream encoder feeds our front-end and drives this tg */
+                       if (se->id != fe || !se->funcs->dig_source_otg ||
+                                       (int)se->funcs->dig_source_otg(se) != tg->inst)
+                               continue;
+
+                       /* This link drives the OTG: keep a seamless-boot eDP, or
+                        * any external link whose sink is still connected.
+                        */
+                       if (link->connector_signal == SIGNAL_TYPE_EDP)
+                               return true;
+                       if (link->link_enc->funcs->get_hpd_state &&
+                                       dc->link_srv->get_hpd_state(link))
+                               return true;
+               }
+       }
+
+       return false;
+}
+
+/*
+ * GOP/vBIOS may leave an OPTC enabled for a display present at power-on but no
+ * longer driven (e.g. external DP unplugged at boot). Such a dangling pipe keeps
+ * DCN out of idle and blocks s0i3. If nothing needs to survive (no committed
+ * stream or seamless-boot eDP) and no sink is still connected, power down all hw
+ * blocks.
+ */
+void dc_disable_dangling_timing_generators(struct dc *dc)
+{
+       struct dce_hwseq *hws = dc->hwseq;
+       bool any_dangling = false;
+       bool any_preserved = false;
+       bool any_connected = false;
+       unsigned int i;
+
+       /* No real hw to touch on a virtual/emulated environment */
+       if (dc->ctx->dce_environment == DCE_ENV_VIRTUAL_HW)
+               return;
+
+       /* Wake hw out of IPS before reading/touching tg state */
+       dc_exit_ips_for_hw_access(dc);
+
+       /* Classify every enabled tg as either to-preserve or dangling */
+       for (i = 0; i < dc->res_pool->timing_generator_count; i++) {
+               struct timing_generator *tg = dc->res_pool->timing_generators[i];
+
+               if (!tg || !tg->funcs->is_tg_enabled ||
+                               !tg->funcs->is_tg_enabled(tg))
+                       continue;
+
+               if (should_preserve_tg(dc, tg))
+                       any_preserved = true;
+               else
+                       any_dangling = true;
+       }
+
+       /* A physically connected sink (HPD asserted) will be re-lit by a
+        * subsequent atomic commit. For that case we don't call the global
+        * power_down().
+        */
+       for (i = 0; i < dc->link_count; i++) {
+               struct dc_link *link = dc->links[i];
+
+               if (link && link->ep_type == DISPLAY_ENDPOINT_PHY &&
+                               link->link_enc && link->link_enc->funcs &&
+                               link->link_enc->funcs->get_hpd_state &&
+                               dc->link_srv->get_hpd_state(link)) {
+                       any_connected = true;
+                       break;
+               }
+       }
+
+       if (!any_dangling)
+               return;
+
+       if (!any_preserved && !any_connected && hws && hws->funcs.power_down) {
+               /* Truly headless / all sinks unplugged: nothing to preserve */
+               DC_LOG_DC("%s: powering down dangling hw blocks to allow idle\n",
+                               __func__);
+               hws->funcs.power_down(dc);
+               return;
+       }
+}
+
 void dc_power_down_on_boot(struct dc *dc)
 {
        if (dc->ctx->dce_environment != DCE_ENV_VIRTUAL_HW &&
index a10ee711668e22261f240705362c082934e18a07..0987258df40c6d94fd724502116ff57eb5bad456 100644 (file)
@@ -3012,6 +3012,8 @@ void dc_resume(struct dc *dc);
 
 void dc_power_down_on_boot(struct dc *dc);
 
+void dc_disable_dangling_timing_generators(struct dc *dc);
+
 /*
  * HDCP Interfaces
  */