drm/nouveau/disp/dp: maintain link in response to hpd signal

This previously worked for the most part due to userspace doing a
modeset in response to HPD interrupts.  This will allow us to
properly handle cases where sync is lost for other reasons, or if
userspace isn't caring.

Signed-off-by: Ben Skeggs <bskeggs@redhat.com>
diff --git a/drivers/gpu/drm/nouveau/core/engine/disp/dport.c b/drivers/gpu/drm/nouveau/core/engine/disp/dport.c
index 5d4b034..c2bf611 100644
--- a/drivers/gpu/drm/nouveau/core/engine/disp/dport.c
+++ b/drivers/gpu/drm/nouveau/core/engine/disp/dport.c
@@ -102,7 +102,7 @@
 	if (outp->dpcd[DPCD_RC02] & DPCD_RC02_ENHANCED_FRAME_CAP)
 		sink[1] |= DPCD_LC01_ENHANCED_FRAME_EN;
 
-	return nv_wraux(outp->base.edid, DPCD_LC00, sink, 2);
+	return nv_wraux(outp->base.edid, DPCD_LC00_LINK_BW_SET, sink, 2);
 }
 
 static void
@@ -313,14 +313,16 @@
 	{}
 };
 
-int
-nouveau_dp_train(struct nvkm_output_dp *outp, u32 datarate)
+void
+nouveau_dp_train(struct work_struct *w)
 {
+	struct nvkm_output_dp *outp = container_of(w, typeof(*outp), lt.work);
 	struct nouveau_disp *disp = nouveau_disp(outp);
 	const struct dp_rates *cfg = nouveau_dp_rates;
 	struct dp_state _dp = {
 		.outp = outp,
 	}, *dp = &_dp;
+	u32 datarate = 0;
 	int ret;
 
 	/* bring capabilities within encoder limits */
@@ -342,6 +344,9 @@
 	}
 	cfg--;
 
+	/* disable link interrupt handling during link training */
+	nouveau_event_put(outp->irq);
+
 	/* enable down-spreading and execute pre-train script from vbios */
 	dp_link_train_init(dp, outp->dpcd[3] & 0x01);
 
@@ -370,12 +375,16 @@
 		}
 	}
 
-	/* finish link training */
+	/* finish link training and execute post-train script from vbios */
 	dp_set_training_pattern(dp, 0);
 	if (ret < 0)
 		ERR("link training failed\n");
 
-	/* execute post-train script from vbios */
 	dp_link_train_fini(dp);
-	return (ret < 0) ? false : true;
+
+	/* signal completion and enable link interrupt handling */
+	DBG("training complete\n");
+	atomic_set(&outp->lt.done, 1);
+	wake_up(&outp->lt.wait);
+	nouveau_event_get(outp->irq);
 }
diff --git a/drivers/gpu/drm/nouveau/core/engine/disp/dport.h b/drivers/gpu/drm/nouveau/core/engine/disp/dport.h
index 2712e31..5628d2d5 100644
--- a/drivers/gpu/drm/nouveau/core/engine/disp/dport.h
+++ b/drivers/gpu/drm/nouveau/core/engine/disp/dport.h
@@ -13,8 +13,7 @@
 #define DPCD_RC0E_AUX_RD_INTERVAL                                       0x0000e
 
 /* DPCD Link Configuration */
-#define DPCD_LC00                                                       0x00100
-#define DPCD_LC00_LINK_BW_SET                                              0xff
+#define DPCD_LC00_LINK_BW_SET                                           0x00100
 #define DPCD_LC01                                                       0x00101
 #define DPCD_LC01_ENHANCED_FRAME_EN                                        0x80
 #define DPCD_LC01_LANE_COUNT_SET                                           0x1f
@@ -71,4 +70,6 @@
 #define DPCD_LS0C_LANE1_POST_CURSOR2                                       0x0c
 #define DPCD_LS0C_LANE0_POST_CURSOR2                                       0x03
 
+void nouveau_dp_train(struct work_struct *);
+
 #endif
diff --git a/drivers/gpu/drm/nouveau/core/engine/disp/nv50.c b/drivers/gpu/drm/nouveau/core/engine/disp/nv50.c
index a73bc15..90974cd 100644
--- a/drivers/gpu/drm/nouveau/core/engine/disp/nv50.c
+++ b/drivers/gpu/drm/nouveau/core/engine/disp/nv50.c
@@ -1457,6 +1457,24 @@
 	if (!outp)
 		return;
 
+	/* we allow both encoder attach and detach operations to occur
+	 * within a single supervisor (ie. modeset) sequence.  the
+	 * encoder detach scripts quite often switch off power to the
+	 * lanes, which requires the link to be re-trained.
+	 *
+	 * this is not generally an issue as the sink "must" (heh)
+	 * signal an irq when it's lost sync so the driver can
+	 * re-train.
+	 *
+	 * however, on some boards, if one does not configure at least
+	 * the gpu side of the link *before* attaching, then various
+	 * things can go horribly wrong (PDISP disappearing from mmio,
+	 * third supervisor never happens, etc).
+	 *
+	 * the solution is simply to retrain here, if necessary.  last
+	 * i checked, the binary driver userspace does not appear to
+	 * trigger this situation (it forces an UPDATE between steps).
+	 */
 	if (outp->info.type == DCB_OUTPUT_DP) {
 		u32 soff = (ffs(outp->info.or) - 1) * 0x08;
 		u32 ctrl, datarate;
@@ -1478,7 +1496,8 @@
 			break;
 		}
 
-		nouveau_dp_train((void *)outp, datarate / soff);
+		if (nvkm_output_dp_train(outp, datarate / soff, true))
+			ERR("link not trained before attach\n");
 	}
 
 	exec_clkcmp(priv, head, 0, pclk, &conf);
diff --git a/drivers/gpu/drm/nouveau/core/engine/disp/nvd0.c b/drivers/gpu/drm/nouveau/core/engine/disp/nvd0.c
index 77e7ade..4eb16e9 100644
--- a/drivers/gpu/drm/nouveau/core/engine/disp/nvd0.c
+++ b/drivers/gpu/drm/nouveau/core/engine/disp/nvd0.c
@@ -1138,6 +1138,7 @@
 	if (!outp)
 		return;
 
+	/* see note in nv50_disp_intr_unk20_2() */
 	if (outp->info.type == DCB_OUTPUT_DP) {
 		u32 sync = nv_rd32(priv, 0x660404 + (head * 0x300));
 		switch ((sync & 0x000003c0) >> 6) {
@@ -1149,7 +1150,8 @@
 			break;
 		}
 
-		nouveau_dp_train((void *)outp, pclk);
+		if (nvkm_output_dp_train(outp, pclk, true))
+			ERR("link not trained before attach\n");
 	}
 
 	exec_clkcmp(priv, head, 0, pclk, &conf);
diff --git a/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.c b/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.c
index 7f55887..52c299c 100644
--- a/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.c
+++ b/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.c
@@ -28,6 +28,75 @@
 #include "conn.h"
 #include "dport.h"
 
+int
+nvkm_output_dp_train(struct nvkm_output *base, u32 datarate, bool wait)
+{
+	struct nvkm_output_dp *outp = (void *)base;
+	bool retrain = true;
+	u8 link[2], stat[3];
+	u32 rate;
+	int ret, i;
+
+	/* check that the link is trained at a high enough rate */
+	ret = nv_rdaux(outp->base.edid, DPCD_LC00_LINK_BW_SET, link, 2);
+	if (ret) {
+		DBG("failed to read link config, assuming no sink\n");
+		goto done;
+	}
+
+	rate = link[0] * 27000 * (link[1] & DPCD_LC01_LANE_COUNT_SET);
+	if (rate < ((datarate / 8) * 10)) {
+		DBG("link not trained at sufficient rate\n");
+		goto done;
+	}
+
+	/* check that link is still trained */
+	ret = nv_rdaux(outp->base.edid, DPCD_LS02, stat, 3);
+	if (ret) {
+		DBG("failed to read link status, assuming no sink\n");
+		goto done;
+	}
+
+	if (stat[2] & DPCD_LS04_INTERLANE_ALIGN_DONE) {
+		for (i = 0; i < (link[1] & DPCD_LC01_LANE_COUNT_SET); i++) {
+			u8 lane = (stat[i >> 1] >> ((i & 1) * 4)) & 0x0f;
+			if (!(lane & DPCD_LS02_LANE0_CR_DONE) ||
+			    !(lane & DPCD_LS02_LANE0_CHANNEL_EQ_DONE) ||
+			    !(lane & DPCD_LS02_LANE0_SYMBOL_LOCKED)) {
+				DBG("lane %d not equalised\n", lane);
+				goto done;
+			}
+		}
+		retrain = false;
+	} else {
+		DBG("no inter-lane alignment\n");
+	}
+
+done:
+	if (retrain || !atomic_read(&outp->lt.done)) {
+		/* no sink, but still need to configure source */
+		if (outp->dpcd[DPCD_RC00_DPCD_REV] == 0x00) {
+			outp->dpcd[DPCD_RC01_MAX_LINK_RATE] =
+				outp->base.info.dpconf.link_bw;
+			outp->dpcd[DPCD_RC02] =
+				outp->base.info.dpconf.link_nr;
+		}
+		atomic_set(&outp->lt.done, 0);
+		schedule_work(&outp->lt.work);
+	} else {
+		nouveau_event_get(outp->irq);
+	}
+
+	if (wait) {
+		if (!wait_event_timeout(outp->lt.wait,
+					atomic_read(&outp->lt.done),
+					msecs_to_jiffies(2000)))
+			ret = -ETIMEDOUT;
+	}
+
+	return ret;
+}
+
 static void
 nvkm_output_dp_enable(struct nvkm_output_dp *outp, bool present)
 {
@@ -38,12 +107,14 @@
 			DBG("aux power -> always\n");
 			outp->present = true;
 		}
+		nvkm_output_dp_train(&outp->base, 0, true);
 	} else {
 		if (outp->present) {
 			nouveau_i2c(port)->release_pad(port);
 			DBG("aux power -> demand\n");
 			outp->present = false;
 		}
+		atomic_set(&outp->lt.done, 0);
 	}
 }
 
@@ -78,7 +149,7 @@
 	}
 
 	if (type & NVKM_I2C_IRQ) {
-		nouveau_event_get(outp->irq);
+		nvkm_output_dp_train(&outp->base, 0, true);
 		send |= NVKM_HPD_IRQ;
 	}
 
@@ -159,6 +230,11 @@
 
 	DBG("bios dp %02x %02x %02x %02x\n", outp->version, hdr, cnt, len);
 
+	/* link training */
+	INIT_WORK(&outp->lt.work, nouveau_dp_train);
+	init_waitqueue_head(&outp->lt.wait);
+	atomic_set(&outp->lt.done, 0);
+
 	/* link maintenance */
 	ret = nouveau_event_new(i2c->ntfy, NVKM_I2C_IRQ, outp->base.edid->index,
 				nvkm_output_dp_service, outp, &outp->irq);
diff --git a/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.h b/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.h
index 4c32cf5..ff33ba1 100644
--- a/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.h
+++ b/drivers/gpu/drm/nouveau/core/engine/disp/outpdp.h
@@ -18,6 +18,12 @@
 	atomic_t pending;
 	bool present;
 	u8 dpcd[16];
+
+	struct {
+		struct work_struct work;
+		wait_queue_head_t wait;
+		atomic_t done;
+	} lt;
 };
 
 #define nvkm_output_dp_create(p,e,c,b,i,d)                                     \
@@ -54,6 +60,6 @@
 	int (*drv_ctl)(struct nvkm_output_dp *, int ln, int vs, int pe, int pc);
 };
 
-int nouveau_dp_train(struct nvkm_output_dp *, u32 rate);
+int nvkm_output_dp_train(struct nvkm_output *, u32 rate, bool wait);
 
 #endif