* [PATCH v2 00/10] nouveau: assorted display fixes (GB20x, r570 DP_CONFIG_STREAM, HF-EEODB EDIDs)
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
Several fixes for GSP-driven displays consisting of four independent
groups. These were created as part of ongoing HDMI 2.1 bring up and
display handling clean-up work, so some of them are not issues now
(e.g., the 2.147GHz pixel clock cap) but fixing them is required to get
advanced features such as FRL, DSC, VRR, etc working properly.
The v1 review asked for the RM vs register programming split to be made
explicit instead of growing more per-chip code inside the RM layer, so
patches 1-3 do that first. No functional change, and then each GB20x fix
(patches 4-8) supplies its hook from a new engine/disp/gb202.c.
Changes in v2:
- Rebased onto drm-misc-next.
- New patches 1-3: move the GSP path's direct MMIO helpers to
engine/disp/ and route them through a per-chip nvkm_disp_func.gsp hook
table. GB20x now has its own gb202_disp_new() instead of display-class
checks in rm/r535/disp.c.
- Patch 4 (was 1+2): merged. Dropped a stray .frl_train reference that
broke the build, no longer drops .scdc, and fixed an undefined shift
when packing byte 3 of each infoframe data word.
- Patch 5 (was 3): the GCP subpack write is now a masked write so
SB1_CTRL keeps its hardware-controlled default.
- Patch 6 (was 4): the vcpi hook returns its control status like the
other RM API entries instead of WARN_ONs under the HAL.
- New patch 7: the head-state readback fix is split out. The core channel
state mirror uses a 0x800 per-head stride on GB20x, fixing scanout pos
and timings readback for heads >= 1.
- Patch 8 (was 5): reworked onto the new hook table. The low-latency
vector is exposed as a second DISP interrupt instance by the r570
engine index translation and re-armed after servicing.
- Patch 9 (was 6): the two HI methods are pushed as one method pair, and
the class-header defines stay offset-sorted.
- Patch 10 (was 7): OF EDIDs now go through drm_edid_connector_update(),
and all firmware provided EDIDs (OF, ACPI, VBIOS embedded) are
validated with drm_edid_valid(). ACPI reads are bounded by the length
_DDC actually returned. Fixed a runtime-PM reference leak when the RM
EDID read fails. No raw struct edid is kept anymore and all the old
raw struct edid consumers are converted to the new API (the SPWG
dual-link byte is cached at EDID-set time as there is no new drm API
alternative for it). get_modes() re-syncs the EDID property only when
the probe helper cleared it.
- Commit messages reworded throughout.
Link to v1:
https://lore.kernel.org/all/20260814235705.59132-1-mohamedahmedegypt2001@gmail.com/
Mohamed Ahmed (10):
drm/nouveau/disp: move GSP head-timing ISR and vblank helpers to
tu102.c
drm/nouveau/disp: move the GSP HDMI GCP AVMute write to engine/disp
drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func
hooks
drm/nouveau/disp: fix HDMI vendor infoframes on GB20x
drm/nouveau/disp: fix HDMI GCP AVMute register offsets on GB20x
drm/nouveau/gsp: use per-version DP_CONFIG_STREAM params on r570
firmware
drm/nouveau/disp: fix head state readback on GB20x
drm/nouveau/gsp: fix vblank interrupts on GB20x
drm/nouveau/dispnv50: program pixel clocks above 2.147GHz on GB20x
drm/nouveau: honor HF-EEODB EDIDs by converting to struct drm_edid
drivers/gpu/drm/nouveau/dispnv04/dfp.c | 5 +-
drivers/gpu/drm/nouveau/dispnv50/disp.c | 4 +-
drivers/gpu/drm/nouveau/dispnv50/headca7d.c | 21 +-
.../drm/nouveau/include/nvhw/class/clca7d.h | 4 +
.../drm/nouveau/include/nvkm/engine/disp.h | 1 +
drivers/gpu/drm/nouveau/nouveau_acpi.c | 21 +-
drivers/gpu/drm/nouveau/nouveau_acpi.h | 10 +-
drivers/gpu/drm/nouveau/nouveau_connector.c | 147 ++++++++++----
drivers/gpu/drm/nouveau/nouveau_connector.h | 12 +-
.../gpu/drm/nouveau/nvkm/engine/device/base.c | 10 +-
.../gpu/drm/nouveau/nvkm/engine/disp/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/ga102.c | 16 +-
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 192 ++++++++++++++++++
.../gpu/drm/nouveau/nvkm/engine/disp/head.h | 3 +
.../gpu/drm/nouveau/nvkm/engine/disp/ior.h | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/priv.h | 20 ++
.../gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 81 +++++++-
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 121 ++++++-----
.../nouveau/nvkm/subdev/gsp/rm/r570/disp.c | 64 ++++++
.../drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c | 9 +
.../nvkm/subdev/gsp/rm/r570/nvrm/disp.h | 2 +
.../gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h | 5 +
22 files changed, 637 insertions(+), 113 deletions(-)
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
--
2.55.0
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v2 00/10] nouveau: assorted display fixes (GB20x, r570 DP_CONFIG_STREAM, HF-EEODB EDIDs)
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
Several fixes for GSP-driven displays consisting of four independent
groups. These were created as part of ongoing HDMI 2.1 bring up and
display handling clean-up work, so some of them are not issues now
(e.g., the 2.147GHz pixel clock cap) but fixing them is required to get
advanced features such as FRL, DSC, VRR, etc working properly.
The v1 review asked for the RM vs register programming split to be made
explicit instead of growing more per-chip code inside the RM layer, so
patches 1-3 do that first. No functional change, and then each GB20x fix
(patches 4-8) supplies its hook from a new engine/disp/gb202.c.
Changes in v2:
- Rebased onto drm-misc-next.
- New patches 1-3: move the GSP path's direct MMIO helpers to
engine/disp/ and route them through a per-chip nvkm_disp_func.gsp hook
table. GB20x now has its own gb202_disp_new() instead of display-class
checks in rm/r535/disp.c.
- Patch 4 (was 1+2): merged. Dropped a stray .frl_train reference that
broke the build, no longer drops .scdc, and fixed an undefined shift
when packing byte 3 of each infoframe data word.
- Patch 5 (was 3): the GCP subpack write is now a masked write so
SB1_CTRL keeps its hardware-controlled default.
- Patch 6 (was 4): the vcpi hook returns its control status like the
other RM API entries instead of WARN_ONs under the HAL.
- New patch 7: the head-state readback fix is split out. The core channel
state mirror uses a 0x800 per-head stride on GB20x, fixing scanout pos
and timings readback for heads >= 1.
- Patch 8 (was 5): reworked onto the new hook table. The low-latency
vector is exposed as a second DISP interrupt instance by the r570
engine index translation and re-armed after servicing.
- Patch 9 (was 6): the two HI methods are pushed as one method pair, and
the class-header defines stay offset-sorted.
- Patch 10 (was 7): OF EDIDs now go through drm_edid_connector_update(),
and all firmware provided EDIDs (OF, ACPI, VBIOS embedded) are
validated with drm_edid_valid(). ACPI reads are bounded by the length
_DDC actually returned. Fixed a runtime-PM reference leak when the RM
EDID read fails. No raw struct edid is kept anymore and all the old
raw struct edid consumers are converted to the new API (the SPWG
dual-link byte is cached at EDID-set time as there is no new drm API
alternative for it). get_modes() re-syncs the EDID property only when
the probe helper cleared it.
- Commit messages reworded throughout.
Link to v1:
https://lore.kernel.org/all/20260814235705.59132-1-mohamedahmedegypt2001@gmail.com/
Mohamed Ahmed (10):
drm/nouveau/disp: move GSP head-timing ISR and vblank helpers to
tu102.c
drm/nouveau/disp: move the GSP HDMI GCP AVMute write to engine/disp
drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func
hooks
drm/nouveau/disp: fix HDMI vendor infoframes on GB20x
drm/nouveau/disp: fix HDMI GCP AVMute register offsets on GB20x
drm/nouveau/gsp: use per-version DP_CONFIG_STREAM params on r570
firmware
drm/nouveau/disp: fix head state readback on GB20x
drm/nouveau/gsp: fix vblank interrupts on GB20x
drm/nouveau/dispnv50: program pixel clocks above 2.147GHz on GB20x
drm/nouveau: honor HF-EEODB EDIDs by converting to struct drm_edid
drivers/gpu/drm/nouveau/dispnv04/dfp.c | 5 +-
drivers/gpu/drm/nouveau/dispnv50/disp.c | 4 +-
drivers/gpu/drm/nouveau/dispnv50/headca7d.c | 21 +-
.../drm/nouveau/include/nvhw/class/clca7d.h | 4 +
.../drm/nouveau/include/nvkm/engine/disp.h | 1 +
drivers/gpu/drm/nouveau/nouveau_acpi.c | 21 +-
drivers/gpu/drm/nouveau/nouveau_acpi.h | 10 +-
drivers/gpu/drm/nouveau/nouveau_connector.c | 147 ++++++++++----
drivers/gpu/drm/nouveau/nouveau_connector.h | 12 +-
.../gpu/drm/nouveau/nvkm/engine/device/base.c | 10 +-
.../gpu/drm/nouveau/nvkm/engine/disp/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/ga102.c | 16 +-
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 192 ++++++++++++++++++
.../gpu/drm/nouveau/nvkm/engine/disp/head.h | 3 +
.../gpu/drm/nouveau/nvkm/engine/disp/ior.h | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/priv.h | 20 ++
.../gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 81 +++++++-
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 121 ++++++-----
.../nouveau/nvkm/subdev/gsp/rm/r570/disp.c | 64 ++++++
.../drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c | 9 +
.../nvkm/subdev/gsp/rm/r570/nvrm/disp.h | 2 +
.../gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h | 5 +
22 files changed, 637 insertions(+), 113 deletions(-)
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
--
2.55.0
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v2 01/10] drm/nouveau/disp: move GSP head-timing ISR and vblank helpers to tu102.c
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The GSP-RM display code in rm/r535/disp.c owns a handful of direct MMIO
routines: the head-timing (vblank) interrupt handler and the per-head
vblank enable/disable. They program display registers, not RM, so they
belong with the rest of the per-chip register code in engine/disp/.
Move them to tu102.c (Turing is the first GSP-capable generation) as
tu102_disp_intr() and tu102_head_vblank_get()/put(), exported for
rm/r535/disp.c, which keeps calling them by name for now. No functional
change.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/head.h | 3 ++
.../gpu/drm/nouveau/nvkm/engine/disp/priv.h | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 50 ++++++++++++++++++
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 52 ++-----------------
4 files changed, 57 insertions(+), 49 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h
index b642729c254f..986043e87554 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h
@@ -56,6 +56,9 @@ int gv100_head_new(struct nvkm_disp *, int id);
void gv100_head_state(struct nvkm_head *head, struct nvkm_head_state *state);
void gv100_head_rgpos(struct nvkm_head *head, u16 *hline, u16 *vline);
+void tu102_head_vblank_get(struct nvkm_head *);
+void tu102_head_vblank_put(struct nvkm_head *);
+
#define HEAD_MSG(h,l,f,a...) do { \
struct nvkm_head *_h = (h); \
nvkm_##l(&_h->disp->engine.subdev, "head-%d: "f"\n", _h->id, ##a); \
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
index a3fd7cb7c488..722ec340e12a 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
@@ -72,6 +72,7 @@ int gv100_disp_wndw_cnt(struct nvkm_disp *, unsigned long *);
int gv100_disp_caps_new(const struct nvkm_oclass *, void *, u32, struct nvkm_object **);
int tu102_disp_init(struct nvkm_disp *);
+irqreturn_t tu102_disp_intr(struct nvkm_inth *);
void nv50_disp_dptmds_war_2(struct nvkm_disp *, struct dcb_output *);
void nv50_disp_dptmds_war_3(struct nvkm_disp *, struct dcb_output *);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
index dcb9f8ba374c..7b70b466fa36 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
@@ -104,6 +104,56 @@ tu102_sor_new(struct nvkm_disp *disp, int id)
return nvkm_ior_new_(&tu102_sor, disp, SOR, id, hda & BIT(id));
}
+/* The GSP-RM display path leaves head-timing (vblank) interrupts and their
+ * enables to us. These program the RM head-timing line (bit 1 of the
+ * per-head enable, not the bit nvkm's own gv100 path uses).
+ */
+void
+tu102_head_vblank_put(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000000);
+}
+
+void
+tu102_head_vblank_get(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_wr32(device, 0x611800 + (head->id * 4), 0x00000002);
+ nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000002);
+}
+
+static void
+tu102_disp_intr_head_timing(struct nvkm_disp *disp, int head)
+{
+ struct nvkm_subdev *subdev = &disp->engine.subdev;
+ struct nvkm_device *device = subdev->device;
+ u32 stat = nvkm_rd32(device, 0x611c00 + (head * 0x04));
+
+ if (stat & 0x00000002) {
+ nvkm_disp_vblank(disp, head);
+
+ nvkm_wr32(device, 0x611800 + (head * 0x04), 0x00000002);
+ }
+}
+
+irqreturn_t
+tu102_disp_intr(struct nvkm_inth *inth)
+{
+ struct nvkm_disp *disp = container_of(inth, typeof(*disp), engine.subdev.inth);
+ struct nvkm_subdev *subdev = &disp->engine.subdev;
+ struct nvkm_device *device = subdev->device;
+ unsigned long mask = nvkm_rd32(device, 0x611ec0) & 0x000000ff;
+ int head;
+
+ for_each_set_bit(head, &mask, 8)
+ tu102_disp_intr_head_timing(disp, head);
+
+ return IRQ_HANDLED;
+}
+
int
tu102_disp_init(struct nvkm_disp *disp)
{
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index e77733a5d9c3..8e57bb6519e5 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -608,29 +608,12 @@ r535_sor_cnt(struct nvkm_disp *disp, unsigned long *pmask)
return 4;
}
-static void
-r535_head_vblank_put(struct nvkm_head *head)
-{
- struct nvkm_device *device = head->disp->engine.subdev.device;
-
- nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000000);
-}
-
-static void
-r535_head_vblank_get(struct nvkm_head *head)
-{
- struct nvkm_device *device = head->disp->engine.subdev.device;
-
- nvkm_wr32(device, 0x611800 + (head->id * 4), 0x00000002);
- nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000002);
-}
-
static const struct nvkm_head_func
r535_head = {
.state = gv100_head_state,
.rgpos = gv100_head_rgpos,
- .vblank_get = r535_head_vblank_get,
- .vblank_put = r535_head_vblank_put,
+ .vblank_get = tu102_head_vblank_get,
+ .vblank_put = tu102_head_vblank_put,
};
static struct nvkm_conn *
@@ -1404,35 +1387,6 @@ static const struct nvkm_event_func
r535_disp_event = {
};
-static void
-r535_disp_intr_head_timing(struct nvkm_disp *disp, int head)
-{
- struct nvkm_subdev *subdev = &disp->engine.subdev;
- struct nvkm_device *device = subdev->device;
- u32 stat = nvkm_rd32(device, 0x611c00 + (head * 0x04));
-
- if (stat & 0x00000002) {
- nvkm_disp_vblank(disp, head);
-
- nvkm_wr32(device, 0x611800 + (head * 0x04), 0x00000002);
- }
-}
-
-static irqreturn_t
-r535_disp_intr(struct nvkm_inth *inth)
-{
- struct nvkm_disp *disp = container_of(inth, typeof(*disp), engine.subdev.inth);
- struct nvkm_subdev *subdev = &disp->engine.subdev;
- struct nvkm_device *device = subdev->device;
- unsigned long mask = nvkm_rd32(device, 0x611ec0) & 0x000000ff;
- int head;
-
- for_each_set_bit(head, &mask, 8)
- r535_disp_intr_head_timing(disp, head);
-
- return IRQ_HANDLED;
-}
-
static void
r535_disp_fini(struct nvkm_disp *disp, bool suspend)
{
@@ -1708,7 +1662,7 @@ r535_disp_oneinit(struct nvkm_disp *disp)
return ret;
ret = nvkm_inth_add(&device->vfn->intr, ret, NVKM_INTR_PRIO_NORMAL, &disp->engine.subdev,
- r535_disp_intr, &disp->engine.subdev.inth);
+ tu102_disp_intr, &disp->engine.subdev.inth);
if (ret)
return ret;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 01/10] drm/nouveau/disp: move GSP head-timing ISR and vblank helpers to tu102.c
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The GSP-RM display code in rm/r535/disp.c owns a handful of direct MMIO
routines: the head-timing (vblank) interrupt handler and the per-head
vblank enable/disable. They program display registers, not RM, so they
belong with the rest of the per-chip register code in engine/disp/.
Move them to tu102.c (Turing is the first GSP-capable generation) as
tu102_disp_intr() and tu102_head_vblank_get()/put(), exported for
rm/r535/disp.c, which keeps calling them by name for now. No functional
change.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/head.h | 3 ++
.../gpu/drm/nouveau/nvkm/engine/disp/priv.h | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 50 ++++++++++++++++++
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 52 ++-----------------
4 files changed, 57 insertions(+), 49 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h
index b642729c254f..986043e87554 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/head.h
@@ -56,6 +56,9 @@ int gv100_head_new(struct nvkm_disp *, int id);
void gv100_head_state(struct nvkm_head *head, struct nvkm_head_state *state);
void gv100_head_rgpos(struct nvkm_head *head, u16 *hline, u16 *vline);
+void tu102_head_vblank_get(struct nvkm_head *);
+void tu102_head_vblank_put(struct nvkm_head *);
+
#define HEAD_MSG(h,l,f,a...) do { \
struct nvkm_head *_h = (h); \
nvkm_##l(&_h->disp->engine.subdev, "head-%d: "f"\n", _h->id, ##a); \
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
index a3fd7cb7c488..722ec340e12a 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
@@ -72,6 +72,7 @@ int gv100_disp_wndw_cnt(struct nvkm_disp *, unsigned long *);
int gv100_disp_caps_new(const struct nvkm_oclass *, void *, u32, struct nvkm_object **);
int tu102_disp_init(struct nvkm_disp *);
+irqreturn_t tu102_disp_intr(struct nvkm_inth *);
void nv50_disp_dptmds_war_2(struct nvkm_disp *, struct dcb_output *);
void nv50_disp_dptmds_war_3(struct nvkm_disp *, struct dcb_output *);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
index dcb9f8ba374c..7b70b466fa36 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
@@ -104,6 +104,56 @@ tu102_sor_new(struct nvkm_disp *disp, int id)
return nvkm_ior_new_(&tu102_sor, disp, SOR, id, hda & BIT(id));
}
+/* The GSP-RM display path leaves head-timing (vblank) interrupts and their
+ * enables to us. These program the RM head-timing line (bit 1 of the
+ * per-head enable, not the bit nvkm's own gv100 path uses).
+ */
+void
+tu102_head_vblank_put(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000000);
+}
+
+void
+tu102_head_vblank_get(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_wr32(device, 0x611800 + (head->id * 4), 0x00000002);
+ nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000002);
+}
+
+static void
+tu102_disp_intr_head_timing(struct nvkm_disp *disp, int head)
+{
+ struct nvkm_subdev *subdev = &disp->engine.subdev;
+ struct nvkm_device *device = subdev->device;
+ u32 stat = nvkm_rd32(device, 0x611c00 + (head * 0x04));
+
+ if (stat & 0x00000002) {
+ nvkm_disp_vblank(disp, head);
+
+ nvkm_wr32(device, 0x611800 + (head * 0x04), 0x00000002);
+ }
+}
+
+irqreturn_t
+tu102_disp_intr(struct nvkm_inth *inth)
+{
+ struct nvkm_disp *disp = container_of(inth, typeof(*disp), engine.subdev.inth);
+ struct nvkm_subdev *subdev = &disp->engine.subdev;
+ struct nvkm_device *device = subdev->device;
+ unsigned long mask = nvkm_rd32(device, 0x611ec0) & 0x000000ff;
+ int head;
+
+ for_each_set_bit(head, &mask, 8)
+ tu102_disp_intr_head_timing(disp, head);
+
+ return IRQ_HANDLED;
+}
+
int
tu102_disp_init(struct nvkm_disp *disp)
{
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index e77733a5d9c3..8e57bb6519e5 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -608,29 +608,12 @@ r535_sor_cnt(struct nvkm_disp *disp, unsigned long *pmask)
return 4;
}
-static void
-r535_head_vblank_put(struct nvkm_head *head)
-{
- struct nvkm_device *device = head->disp->engine.subdev.device;
-
- nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000000);
-}
-
-static void
-r535_head_vblank_get(struct nvkm_head *head)
-{
- struct nvkm_device *device = head->disp->engine.subdev.device;
-
- nvkm_wr32(device, 0x611800 + (head->id * 4), 0x00000002);
- nvkm_mask(device, 0x611d80 + (head->id * 4), 0x00000002, 0x00000002);
-}
-
static const struct nvkm_head_func
r535_head = {
.state = gv100_head_state,
.rgpos = gv100_head_rgpos,
- .vblank_get = r535_head_vblank_get,
- .vblank_put = r535_head_vblank_put,
+ .vblank_get = tu102_head_vblank_get,
+ .vblank_put = tu102_head_vblank_put,
};
static struct nvkm_conn *
@@ -1404,35 +1387,6 @@ static const struct nvkm_event_func
r535_disp_event = {
};
-static void
-r535_disp_intr_head_timing(struct nvkm_disp *disp, int head)
-{
- struct nvkm_subdev *subdev = &disp->engine.subdev;
- struct nvkm_device *device = subdev->device;
- u32 stat = nvkm_rd32(device, 0x611c00 + (head * 0x04));
-
- if (stat & 0x00000002) {
- nvkm_disp_vblank(disp, head);
-
- nvkm_wr32(device, 0x611800 + (head * 0x04), 0x00000002);
- }
-}
-
-static irqreturn_t
-r535_disp_intr(struct nvkm_inth *inth)
-{
- struct nvkm_disp *disp = container_of(inth, typeof(*disp), engine.subdev.inth);
- struct nvkm_subdev *subdev = &disp->engine.subdev;
- struct nvkm_device *device = subdev->device;
- unsigned long mask = nvkm_rd32(device, 0x611ec0) & 0x000000ff;
- int head;
-
- for_each_set_bit(head, &mask, 8)
- r535_disp_intr_head_timing(disp, head);
-
- return IRQ_HANDLED;
-}
-
static void
r535_disp_fini(struct nvkm_disp *disp, bool suspend)
{
@@ -1708,7 +1662,7 @@ r535_disp_oneinit(struct nvkm_disp *disp)
return ret;
ret = nvkm_inth_add(&device->vfn->intr, ret, NVKM_INTR_PRIO_NORMAL, &disp->engine.subdev,
- r535_disp_intr, &disp->engine.subdev.inth);
+ tu102_disp_intr, &disp->engine.subdev.inth);
if (ret)
return ret;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 02/10] drm/nouveau/disp: move the GSP HDMI GCP AVMute write to engine/disp
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
r535_sor_hdmi_audio() pairs two RM controls (a SET_OD_PACKET carrying
the same General Control Packet, and the audio mute-stream toggle)
with a direct write of the GCP AVMute bit through the SF GCP unit.
The controls are RM and stay, but the direct write is register
programming and moves next to the other per-chip display code as
tu102_sor_hdmi_gcp(). No functional change.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h | 1 +
drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 15 +++++++++++++++
.../drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 9 +--------
3 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h
index 3ba04bead2f9..5d682a774f2d 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h
@@ -194,6 +194,7 @@ void gv100_sor_dp_audio_sym(struct nvkm_ior *, int, u16, u32);
void gv100_sor_dp_watermark(struct nvkm_ior *, int, u8);
extern const struct nvkm_ior_func_hda gv100_sor_hda;
+void tu102_sor_hdmi_gcp(struct nvkm_ior *, int, bool);
void tu102_sor_dp_vcpi(struct nvkm_ior *, int, u8, u8, u16, u16);
int nv50_pior_cnt(struct nvkm_disp *, unsigned long *);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
index 7b70b466fa36..6cfd52c9056f 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
@@ -30,6 +30,21 @@
#include <nvif/class.h>
+/* General Control Packet: bracket an audio enable/disable with AVMute
+ * through the legacy GCP SF unit. Used by the GSP-RM path, which sends the
+ * equivalent packet via RM as well but keeps the direct write in sync.
+ */
+void
+tu102_sor_hdmi_gcp(struct nvkm_ior *sor, int head, bool enable)
+{
+ struct nvkm_device *device = sor->disp->engine.subdev.device;
+ const u32 hdmi = head * 0x400;
+
+ nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000000);
+ nvkm_wr32(device, 0x6f00cc + hdmi, !enable ? 0x00000001 : 0x00000010);
+ nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000001);
+}
+
void
tu102_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned)
{
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index 8e57bb6519e5..cd4451e62512 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -545,16 +545,9 @@ r535_sor_hdmi_ctrl_audio(struct nvkm_outp *outp, bool enable)
static void
r535_sor_hdmi_audio(struct nvkm_ior *sor, int head, bool enable)
{
- struct nvkm_device *device = sor->disp->engine.subdev.device;
- const u32 hdmi = head * 0x400;
-
r535_sor_hdmi_ctrl_audio(sor->asy.outp, enable);
r535_sor_hdmi_ctrl_audio_mute(sor->asy.outp, !enable);
-
- /* General Control (GCP). */
- nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000000);
- nvkm_wr32(device, 0x6f00cc + hdmi, !enable ? 0x00000001 : 0x00000010);
- nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000001);
+ tu102_sor_hdmi_gcp(sor, head, enable);
}
static void
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 02/10] drm/nouveau/disp: move the GSP HDMI GCP AVMute write to engine/disp
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
r535_sor_hdmi_audio() pairs two RM controls (a SET_OD_PACKET carrying
the same General Control Packet, and the audio mute-stream toggle)
with a direct write of the GCP AVMute bit through the SF GCP unit.
The controls are RM and stay, but the direct write is register
programming and moves next to the other per-chip display code as
tu102_sor_hdmi_gcp(). No functional change.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h | 1 +
drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 15 +++++++++++++++
.../drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 9 +--------
3 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h
index 3ba04bead2f9..5d682a774f2d 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ior.h
@@ -194,6 +194,7 @@ void gv100_sor_dp_audio_sym(struct nvkm_ior *, int, u16, u32);
void gv100_sor_dp_watermark(struct nvkm_ior *, int, u8);
extern const struct nvkm_ior_func_hda gv100_sor_hda;
+void tu102_sor_hdmi_gcp(struct nvkm_ior *, int, bool);
void tu102_sor_dp_vcpi(struct nvkm_ior *, int, u8, u8, u16, u16);
int nv50_pior_cnt(struct nvkm_disp *, unsigned long *);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
index 7b70b466fa36..6cfd52c9056f 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
@@ -30,6 +30,21 @@
#include <nvif/class.h>
+/* General Control Packet: bracket an audio enable/disable with AVMute
+ * through the legacy GCP SF unit. Used by the GSP-RM path, which sends the
+ * equivalent packet via RM as well but keeps the direct write in sync.
+ */
+void
+tu102_sor_hdmi_gcp(struct nvkm_ior *sor, int head, bool enable)
+{
+ struct nvkm_device *device = sor->disp->engine.subdev.device;
+ const u32 hdmi = head * 0x400;
+
+ nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000000);
+ nvkm_wr32(device, 0x6f00cc + hdmi, !enable ? 0x00000001 : 0x00000010);
+ nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000001);
+}
+
void
tu102_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned)
{
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index 8e57bb6519e5..cd4451e62512 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -545,16 +545,9 @@ r535_sor_hdmi_ctrl_audio(struct nvkm_outp *outp, bool enable)
static void
r535_sor_hdmi_audio(struct nvkm_ior *sor, int head, bool enable)
{
- struct nvkm_device *device = sor->disp->engine.subdev.device;
- const u32 hdmi = head * 0x400;
-
r535_sor_hdmi_ctrl_audio(sor->asy.outp, enable);
r535_sor_hdmi_ctrl_audio_mute(sor->asy.outp, !enable);
-
- /* General Control (GCP). */
- nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000000);
- nvkm_wr32(device, 0x6f00cc + hdmi, !enable ? 0x00000001 : 0x00000010);
- nvkm_mask(device, 0x6f00c0 + hdmi, 0x00000001, 0x00000001);
+ tu102_sor_hdmi_gcp(sor, head, enable);
}
static void
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The GSP-RM display code in rm/r535/disp.c borrows a few
register-programming routines from engine/disp (the head-timing
interrupt handler, vblank enables, armed head state and scanout position
readback, the AVI/VSI infoframe writers and the GCP AVMute write) and so
far picked them by name, which means it has to know which chip it runs
on the moment a generation changes any of them.
Give nvkm_disp_func a .gsp table that each chip fills with exactly those
hooks, add tu102_gsp_disp (TU1xx) and ga102_gsp_disp (GA10x onwards)
carrying the current functions, hand them to r535_disp_new() instead of
the full hardware tables, and make rm/r535/disp.c call through the
hooks. r535_head becomes four forwarders, r535_sor_hdmi gets infoframe
forwarders, r535_sor_hdmi_audio() calls the GCP hook, and the interrupt
handler and its vector come from the table (intr_low_latency selects the
second DISP interrupt instance for chips that raise head timing on a
separate vector). The tables are per chip even though the two currently
coincide, so a generation that changes a hook only touches its own file.
rm/r535/disp.c no longer contains chip-specific register code, and a new
display generation only has to provide its own table. No functional
change.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/ga102.c | 16 ++++-
.../gpu/drm/nouveau/nvkm/engine/disp/priv.h | 19 ++++++
.../gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 16 ++++-
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 60 ++++++++++++++++---
4 files changed, 100 insertions(+), 11 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c
index ab0a85c92430..b48ed7146396 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c
@@ -144,12 +144,26 @@ ga102_disp = {
},
};
+static const struct nvkm_disp_func
+ga102_gsp_disp = {
+ .uevent = &gv100_disp_chan_uevent,
+ .ramht_size = 0x2000,
+ .gsp.intr = tu102_disp_intr,
+ .gsp.head_state = gv100_head_state,
+ .gsp.head_rgpos = gv100_head_rgpos,
+ .gsp.vblank_get = tu102_head_vblank_get,
+ .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ .gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
+ .gsp.hdmi_infoframe_vsi = gv100_sor_hdmi_infoframe_vsi,
+};
+
int
ga102_disp_new(struct nvkm_device *device, enum nvkm_subdev_type type, int inst,
struct nvkm_disp **pdisp)
{
if (nvkm_gsp_rm(device->gsp))
- return r535_disp_new(&ga102_disp, device, type, inst, pdisp);
+ return r535_disp_new(&ga102_gsp_disp, device, type, inst, pdisp);
return nvkm_disp_new_(&ga102_disp, device, type, inst, pdisp);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
index 722ec340e12a..3cb903741fb8 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
@@ -5,6 +5,8 @@
#include <engine/disp.h>
#include <core/enum.h>
struct nvkm_head;
+struct nvkm_head_state;
+struct nvkm_ior;
struct nvkm_outp;
struct dcb_output;
@@ -34,6 +36,23 @@ struct nvkm_disp_func {
int (*new)(struct nvkm_disp *, int id);
} wndw, head, dac, sor, pior;
+ /* Register programming that the GSP-RM display path (rm/r535) needs from
+ * the chip, everything else on that path goes through RM. Every hook
+ * is called unconditionally.
+ */
+ struct {
+ irqreturn_t (*intr)(struct nvkm_inth *);
+ /* Head-timing interrupts arrive on a second DISP vector. */
+ bool intr_low_latency;
+ void (*head_state)(struct nvkm_head *, struct nvkm_head_state *);
+ void (*head_rgpos)(struct nvkm_head *, u16 *hline, u16 *vline);
+ void (*vblank_get)(struct nvkm_head *);
+ void (*vblank_put)(struct nvkm_head *);
+ void (*hdmi_gcp)(struct nvkm_ior *, int head, bool enable);
+ void (*hdmi_infoframe_avi)(struct nvkm_ior *, int head, void *data, u32 size);
+ void (*hdmi_infoframe_vsi)(struct nvkm_ior *, int head, void *data, u32 size);
+ } gsp;
+
u16 ramht_size;
struct nvkm_sclass root;
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
index 6cfd52c9056f..9db3cac487e3 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
@@ -295,12 +295,26 @@ tu102_disp = {
},
};
+static const struct nvkm_disp_func
+tu102_gsp_disp = {
+ .uevent = &gv100_disp_chan_uevent,
+ .ramht_size = 0x2000,
+ .gsp.intr = tu102_disp_intr,
+ .gsp.head_state = gv100_head_state,
+ .gsp.head_rgpos = gv100_head_rgpos,
+ .gsp.vblank_get = tu102_head_vblank_get,
+ .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ .gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
+ .gsp.hdmi_infoframe_vsi = gv100_sor_hdmi_infoframe_vsi,
+};
+
int
tu102_disp_new(struct nvkm_device *device, enum nvkm_subdev_type type, int inst,
struct nvkm_disp **pdisp)
{
if (nvkm_gsp_rm(device->gsp))
- return r535_disp_new(&tu102_disp, device, type, inst, pdisp);
+ return r535_disp_new(&tu102_gsp_disp, device, type, inst, pdisp);
return nvkm_disp_new_(&tu102_disp, device, type, inst, pdisp);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index cd4451e62512..f3e55253bcbc 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -547,7 +547,19 @@ r535_sor_hdmi_audio(struct nvkm_ior *sor, int head, bool enable)
{
r535_sor_hdmi_ctrl_audio(sor->asy.outp, enable);
r535_sor_hdmi_ctrl_audio_mute(sor->asy.outp, !enable);
- tu102_sor_hdmi_gcp(sor, head, enable);
+ sor->disp->func->gsp.hdmi_gcp(sor, head, enable);
+}
+
+static void
+r535_sor_hdmi_infoframe_avi(struct nvkm_ior *sor, int head, void *data, u32 size)
+{
+ sor->disp->func->gsp.hdmi_infoframe_avi(sor, head, data, size);
+}
+
+static void
+r535_sor_hdmi_infoframe_vsi(struct nvkm_ior *sor, int head, void *data, u32 size)
+{
+ sor->disp->func->gsp.hdmi_infoframe_vsi(sor, head, data, size);
}
static void
@@ -575,8 +587,8 @@ r535_sor_hdmi = {
.ctrl = r535_sor_hdmi_ctrl,
.scdc = r535_sor_hdmi_scdc,
/*TODO: SF_USER -> KMS. */
- .infoframe_avi = gv100_sor_hdmi_infoframe_avi,
- .infoframe_vsi = gv100_sor_hdmi_infoframe_vsi,
+ .infoframe_avi = r535_sor_hdmi_infoframe_avi,
+ .infoframe_vsi = r535_sor_hdmi_infoframe_vsi,
.audio = r535_sor_hdmi_audio,
};
@@ -601,12 +613,36 @@ r535_sor_cnt(struct nvkm_disp *disp, unsigned long *pmask)
return 4;
}
+static void
+r535_head_state(struct nvkm_head *head, struct nvkm_head_state *state)
+{
+ head->disp->func->gsp.head_state(head, state);
+}
+
+static void
+r535_head_rgpos(struct nvkm_head *head, u16 *hline, u16 *vline)
+{
+ head->disp->func->gsp.head_rgpos(head, hline, vline);
+}
+
+static void
+r535_head_vblank_get(struct nvkm_head *head)
+{
+ head->disp->func->gsp.vblank_get(head);
+}
+
+static void
+r535_head_vblank_put(struct nvkm_head *head)
+{
+ head->disp->func->gsp.vblank_put(head);
+}
+
static const struct nvkm_head_func
r535_head = {
- .state = gv100_head_state,
- .rgpos = gv100_head_rgpos,
- .vblank_get = tu102_head_vblank_get,
- .vblank_put = tu102_head_vblank_put,
+ .state = r535_head_state,
+ .rgpos = r535_head_rgpos,
+ .vblank_get = r535_head_vblank_get,
+ .vblank_put = r535_head_vblank_put,
};
static struct nvkm_conn *
@@ -1650,12 +1686,17 @@ r535_disp_oneinit(struct nvkm_disp *disp)
if (ret)
return ret;
- ret = nvkm_gsp_intr_stall(gsp, disp->engine.subdev.type, disp->engine.subdev.inst);
+ /* Chips that raise head-timing interrupts on a separate low-latency
+ * vector report it as a second DISP interrupt table entry, exposed
+ * as instance 1 by the RM engine-index translation.
+ */
+ ret = nvkm_gsp_intr_stall(gsp, disp->engine.subdev.type,
+ disp->func->gsp.intr_low_latency ? 1 : disp->engine.subdev.inst);
if (ret < 0)
return ret;
ret = nvkm_inth_add(&device->vfn->intr, ret, NVKM_INTR_PRIO_NORMAL, &disp->engine.subdev,
- tu102_disp_intr, &disp->engine.subdev.inth);
+ disp->func->gsp.intr, &disp->engine.subdev.inth);
if (ret)
return ret;
@@ -1688,6 +1729,7 @@ r535_disp_new(const struct nvkm_disp_func *hw, struct nvkm_device *device,
rm->uevent = hw->uevent;
rm->sor.cnt = r535_sor_cnt;
rm->sor.new = r535_sor_new;
+ rm->gsp = hw->gsp;
rm->ramht_size = hw->ramht_size;
rm->root.oclass = gpu->disp.class.root;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The GSP-RM display code in rm/r535/disp.c borrows a few
register-programming routines from engine/disp (the head-timing
interrupt handler, vblank enables, armed head state and scanout position
readback, the AVI/VSI infoframe writers and the GCP AVMute write) and so
far picked them by name, which means it has to know which chip it runs
on the moment a generation changes any of them.
Give nvkm_disp_func a .gsp table that each chip fills with exactly those
hooks, add tu102_gsp_disp (TU1xx) and ga102_gsp_disp (GA10x onwards)
carrying the current functions, hand them to r535_disp_new() instead of
the full hardware tables, and make rm/r535/disp.c call through the
hooks. r535_head becomes four forwarders, r535_sor_hdmi gets infoframe
forwarders, r535_sor_hdmi_audio() calls the GCP hook, and the interrupt
handler and its vector come from the table (intr_low_latency selects the
second DISP interrupt instance for chips that raise head timing on a
separate vector). The tables are per chip even though the two currently
coincide, so a generation that changes a hook only touches its own file.
rm/r535/disp.c no longer contains chip-specific register code, and a new
display generation only has to provide its own table. No functional
change.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/ga102.c | 16 ++++-
.../gpu/drm/nouveau/nvkm/engine/disp/priv.h | 19 ++++++
.../gpu/drm/nouveau/nvkm/engine/disp/tu102.c | 16 ++++-
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 60 ++++++++++++++++---
4 files changed, 100 insertions(+), 11 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c
index ab0a85c92430..b48ed7146396 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/ga102.c
@@ -144,12 +144,26 @@ ga102_disp = {
},
};
+static const struct nvkm_disp_func
+ga102_gsp_disp = {
+ .uevent = &gv100_disp_chan_uevent,
+ .ramht_size = 0x2000,
+ .gsp.intr = tu102_disp_intr,
+ .gsp.head_state = gv100_head_state,
+ .gsp.head_rgpos = gv100_head_rgpos,
+ .gsp.vblank_get = tu102_head_vblank_get,
+ .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ .gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
+ .gsp.hdmi_infoframe_vsi = gv100_sor_hdmi_infoframe_vsi,
+};
+
int
ga102_disp_new(struct nvkm_device *device, enum nvkm_subdev_type type, int inst,
struct nvkm_disp **pdisp)
{
if (nvkm_gsp_rm(device->gsp))
- return r535_disp_new(&ga102_disp, device, type, inst, pdisp);
+ return r535_disp_new(&ga102_gsp_disp, device, type, inst, pdisp);
return nvkm_disp_new_(&ga102_disp, device, type, inst, pdisp);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
index 722ec340e12a..3cb903741fb8 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/priv.h
@@ -5,6 +5,8 @@
#include <engine/disp.h>
#include <core/enum.h>
struct nvkm_head;
+struct nvkm_head_state;
+struct nvkm_ior;
struct nvkm_outp;
struct dcb_output;
@@ -34,6 +36,23 @@ struct nvkm_disp_func {
int (*new)(struct nvkm_disp *, int id);
} wndw, head, dac, sor, pior;
+ /* Register programming that the GSP-RM display path (rm/r535) needs from
+ * the chip, everything else on that path goes through RM. Every hook
+ * is called unconditionally.
+ */
+ struct {
+ irqreturn_t (*intr)(struct nvkm_inth *);
+ /* Head-timing interrupts arrive on a second DISP vector. */
+ bool intr_low_latency;
+ void (*head_state)(struct nvkm_head *, struct nvkm_head_state *);
+ void (*head_rgpos)(struct nvkm_head *, u16 *hline, u16 *vline);
+ void (*vblank_get)(struct nvkm_head *);
+ void (*vblank_put)(struct nvkm_head *);
+ void (*hdmi_gcp)(struct nvkm_ior *, int head, bool enable);
+ void (*hdmi_infoframe_avi)(struct nvkm_ior *, int head, void *data, u32 size);
+ void (*hdmi_infoframe_vsi)(struct nvkm_ior *, int head, void *data, u32 size);
+ } gsp;
+
u16 ramht_size;
struct nvkm_sclass root;
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
index 6cfd52c9056f..9db3cac487e3 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/tu102.c
@@ -295,12 +295,26 @@ tu102_disp = {
},
};
+static const struct nvkm_disp_func
+tu102_gsp_disp = {
+ .uevent = &gv100_disp_chan_uevent,
+ .ramht_size = 0x2000,
+ .gsp.intr = tu102_disp_intr,
+ .gsp.head_state = gv100_head_state,
+ .gsp.head_rgpos = gv100_head_rgpos,
+ .gsp.vblank_get = tu102_head_vblank_get,
+ .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ .gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
+ .gsp.hdmi_infoframe_vsi = gv100_sor_hdmi_infoframe_vsi,
+};
+
int
tu102_disp_new(struct nvkm_device *device, enum nvkm_subdev_type type, int inst,
struct nvkm_disp **pdisp)
{
if (nvkm_gsp_rm(device->gsp))
- return r535_disp_new(&tu102_disp, device, type, inst, pdisp);
+ return r535_disp_new(&tu102_gsp_disp, device, type, inst, pdisp);
return nvkm_disp_new_(&tu102_disp, device, type, inst, pdisp);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index cd4451e62512..f3e55253bcbc 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -547,7 +547,19 @@ r535_sor_hdmi_audio(struct nvkm_ior *sor, int head, bool enable)
{
r535_sor_hdmi_ctrl_audio(sor->asy.outp, enable);
r535_sor_hdmi_ctrl_audio_mute(sor->asy.outp, !enable);
- tu102_sor_hdmi_gcp(sor, head, enable);
+ sor->disp->func->gsp.hdmi_gcp(sor, head, enable);
+}
+
+static void
+r535_sor_hdmi_infoframe_avi(struct nvkm_ior *sor, int head, void *data, u32 size)
+{
+ sor->disp->func->gsp.hdmi_infoframe_avi(sor, head, data, size);
+}
+
+static void
+r535_sor_hdmi_infoframe_vsi(struct nvkm_ior *sor, int head, void *data, u32 size)
+{
+ sor->disp->func->gsp.hdmi_infoframe_vsi(sor, head, data, size);
}
static void
@@ -575,8 +587,8 @@ r535_sor_hdmi = {
.ctrl = r535_sor_hdmi_ctrl,
.scdc = r535_sor_hdmi_scdc,
/*TODO: SF_USER -> KMS. */
- .infoframe_avi = gv100_sor_hdmi_infoframe_avi,
- .infoframe_vsi = gv100_sor_hdmi_infoframe_vsi,
+ .infoframe_avi = r535_sor_hdmi_infoframe_avi,
+ .infoframe_vsi = r535_sor_hdmi_infoframe_vsi,
.audio = r535_sor_hdmi_audio,
};
@@ -601,12 +613,36 @@ r535_sor_cnt(struct nvkm_disp *disp, unsigned long *pmask)
return 4;
}
+static void
+r535_head_state(struct nvkm_head *head, struct nvkm_head_state *state)
+{
+ head->disp->func->gsp.head_state(head, state);
+}
+
+static void
+r535_head_rgpos(struct nvkm_head *head, u16 *hline, u16 *vline)
+{
+ head->disp->func->gsp.head_rgpos(head, hline, vline);
+}
+
+static void
+r535_head_vblank_get(struct nvkm_head *head)
+{
+ head->disp->func->gsp.vblank_get(head);
+}
+
+static void
+r535_head_vblank_put(struct nvkm_head *head)
+{
+ head->disp->func->gsp.vblank_put(head);
+}
+
static const struct nvkm_head_func
r535_head = {
- .state = gv100_head_state,
- .rgpos = gv100_head_rgpos,
- .vblank_get = tu102_head_vblank_get,
- .vblank_put = tu102_head_vblank_put,
+ .state = r535_head_state,
+ .rgpos = r535_head_rgpos,
+ .vblank_get = r535_head_vblank_get,
+ .vblank_put = r535_head_vblank_put,
};
static struct nvkm_conn *
@@ -1650,12 +1686,17 @@ r535_disp_oneinit(struct nvkm_disp *disp)
if (ret)
return ret;
- ret = nvkm_gsp_intr_stall(gsp, disp->engine.subdev.type, disp->engine.subdev.inst);
+ /* Chips that raise head-timing interrupts on a separate low-latency
+ * vector report it as a second DISP interrupt table entry, exposed
+ * as instance 1 by the RM engine-index translation.
+ */
+ ret = nvkm_gsp_intr_stall(gsp, disp->engine.subdev.type,
+ disp->func->gsp.intr_low_latency ? 1 : disp->engine.subdev.inst);
if (ret < 0)
return ret;
ret = nvkm_inth_add(&device->vfn->intr, ret, NVKM_INTR_PRIO_NORMAL, &disp->engine.subdev,
- tu102_disp_intr, &disp->engine.subdev.inth);
+ disp->func->gsp.intr, &disp->engine.subdev.inth);
if (ret)
return ret;
@@ -1688,6 +1729,7 @@ r535_disp_new(const struct nvkm_disp_func *hw, struct nvkm_device *device,
rm->uevent = hw->uevent;
rm->sor.cnt = r535_sor_cnt;
rm->sor.new = r535_sor_new;
+ rm->gsp = hw->gsp;
rm->ramht_size = hw->ramht_size;
rm->root.oclass = gpu->disp.class.root;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 04/10] drm/nouveau/disp: fix HDMI vendor infoframes on GB20x
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path reuses the GV100 direct-MMIO infoframe writers on every
chip. On GB20x that is only half right as while the legacy SF AVI unit is
unchanged, the legacy VSI unit at 0x6f0100 was removed, so
gv100_sor_hdmi_infoframe_vsi() writes into a reserved area and no vendor
infoframe ever reaches the HW. This affects HDMI-VIC signalling which
can impact some 4K modes for legacy HDMI 1.4 sinks.
GB20x (NVDisplay 5.0+) reorganised the SF HDMI packet units. Per NVIDIA's
published C971/CA71 DISP_SF_USER class headers, only three legacy units
remain (AVI at +0x000, GCP at +0x040, ACR at +0x080), and vendor
infoframes must instead be sent through the shared generic infoframe
units at +0x130, whose 9-dword packet slots are loaded through the
shared data port at +0x3f0/+0x3f4.
Add a VSI writer using the same programming sequence OpenRM uses on
these chips (nvhdmipkt_C971.c, programAdvancedInfoframeC971()): disable
the unit and wait for it to idle, clear the SENT status, write the packet
through the data port with a zero inserted in HB3 after the three header
bytes, then enable the unit for every-frame transmission during vblank.
Generic unit 1 is used for the VSI, matching the slot assignment in
NVIDIA's nvkms (NVHDMIPKT_TYPE_SHARED_GENERIC2, unit 0 is reserved
for extended metadata packets and unit 2 for the HDR DRM infoframe,
if those are wired up later).
GB20x so far shared GA10x's display entry point. Give it its own,
gb202_disp_new(), with a gb202_gsp_disp table that supplies the VSI
writer to the GSP path and otherwise carries the same hooks as GA10x.
The following fixes fill in the rest of the GB20x differences there.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../drm/nouveau/include/nvkm/engine/disp.h | 1 +
.../gpu/drm/nouveau/nvkm/engine/device/base.c | 10 +-
.../gpu/drm/nouveau/nvkm/engine/disp/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 91 +++++++++++++++++++
4 files changed, 98 insertions(+), 5 deletions(-)
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
diff --git a/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h b/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h
index 7903d7470d19..01145db32c53 100644
--- a/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h
+++ b/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h
@@ -87,4 +87,5 @@ int gp102_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct
int gv100_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
int tu102_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
int ga102_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
+int gb202_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
#endif
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c b/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c
index ea62dc97f118..96c8a5b29999 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c
@@ -2846,7 +2846,7 @@ nv1b2_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2862,7 +2862,7 @@ nv1b3_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2878,7 +2878,7 @@ nv1b5_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2894,7 +2894,7 @@ nv1b6_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2910,7 +2910,7 @@ nv1b7_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild b/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild
index e1aecd3fe96c..98d6ca5ac311 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild
@@ -27,6 +27,7 @@ nvkm-y += nvkm/engine/disp/gp102.o
nvkm-y += nvkm/engine/disp/gv100.o
nvkm-y += nvkm/engine/disp/tu102.o
nvkm-y += nvkm/engine/disp/ga102.o
+nvkm-y += nvkm/engine/disp/gb202.o
nvkm-y += nvkm/engine/disp/udisp.o
nvkm-y += nvkm/engine/disp/uconn.o
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
new file mode 100644
index 000000000000..fa83aee35ae7
--- /dev/null
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright 2026 Valve Corp.
+ */
+#include "priv.h"
+#include "head.h"
+#include "ior.h"
+
+#include <subdev/timer.h>
+
+/* GB20x (NVD5.0) reorganised the SF HDMI packet units. The AVI unit is
+ * unchanged from GV100, but the legacy VSI unit is gone. Vendor infoframes
+ * are sent through the shared generic infoframe units instead. Register
+ * layout per NVIDIA's clc971.h/clca71.h, programming sequence per
+ * nvhdmipkt_C971.c:programAdvancedInfoframeC971().
+ */
+static void
+gb202_sor_hdmi_infoframe_vsi(struct nvkm_ior *ior, int head, void *data, u32 size)
+{
+ struct nvkm_device *device = ior->disp->engine.subdev.device;
+ const u32 hoff = head * 0x400;
+ /* Generic infoframe unit 1, the slot NVIDIA's driver uses for the VSI. */
+ const u32 ctrl = 0x6f0138 + hoff;
+ u8 buf[36] = {};
+ int i;
+
+ /* Disable the unit and wait for it to go idle. */
+ nvkm_mask(device, ctrl, 0x00000001, 0x00000000);
+ if (nvkm_msec(device, 2000,
+ if (!(nvkm_rd32(device, ctrl) & 0x00400000))
+ break;
+ ) < 0)
+ return;
+
+ if (!size)
+ return;
+
+ /* Clear SENT status, and point the data port at unit 1's slot. */
+ nvkm_mask(device, ctrl, 0x00800000, 0x00800000);
+ nvkm_wr32(device, 0x6f03f0 + hoff, 0x00000001);
+
+ /* The data port takes the raw packet, except that a zero is inserted
+ * in HB3 after the three header bytes. A slot is 9 dwords (HB0-3 plus
+ * up to 32 payload bytes). An HDMI infoframe carries at most PB0-27,
+ * so the tail stays zero, and we always write the whole slot.
+ */
+ size = min_t(u32, size, 31);
+ memcpy(buf, data, min_t(u32, size, 3));
+ if (size > 3)
+ memcpy(&buf[4], (u8 *)data + 3, size - 3);
+
+ for (i = 0; i < 36; i += 4) {
+ nvkm_wr32(device, 0x6f03f4 + hoff, buf[i + 0] | buf[i + 1] << 8 |
+ buf[i + 2] << 16 |
+ (u32)buf[i + 3] << 24);
+ }
+
+ /* No flip ID or scanline matching. */
+ nvkm_wr32(device, 0x6f013c + hoff, 0x00000000);
+
+ /* ENABLE | RUN_MODE=ALWAYS | LOC=VBLANK | OFFSET=1 | SIZE=0. */
+ nvkm_wr32(device, ctrl, 0x00000041);
+
+ /* Audio priority low (the init value). */
+ nvkm_wr32(device, 0x6f03f8 + hoff, 0x00000002);
+}
+
+/* GB20x is GSP-only. This table supplies the register programming the
+ * GSP-RM display path needs from the chip.
+ */
+static const struct nvkm_disp_func
+gb202_gsp_disp = {
+ .uevent = &gv100_disp_chan_uevent,
+ .ramht_size = 0x2000,
+ .gsp.intr = tu102_disp_intr,
+ .gsp.head_state = gv100_head_state,
+ .gsp.head_rgpos = gv100_head_rgpos,
+ .gsp.vblank_get = tu102_head_vblank_get,
+ .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ /* The legacy AVI unit is unchanged on GB20x. */
+ .gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
+ .gsp.hdmi_infoframe_vsi = gb202_sor_hdmi_infoframe_vsi,
+};
+
+int
+gb202_disp_new(struct nvkm_device *device, enum nvkm_subdev_type type, int inst,
+ struct nvkm_disp **pdisp)
+{
+ return r535_disp_new(&gb202_gsp_disp, device, type, inst, pdisp);
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 04/10] drm/nouveau/disp: fix HDMI vendor infoframes on GB20x
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path reuses the GV100 direct-MMIO infoframe writers on every
chip. On GB20x that is only half right as while the legacy SF AVI unit is
unchanged, the legacy VSI unit at 0x6f0100 was removed, so
gv100_sor_hdmi_infoframe_vsi() writes into a reserved area and no vendor
infoframe ever reaches the HW. This affects HDMI-VIC signalling which
can impact some 4K modes for legacy HDMI 1.4 sinks.
GB20x (NVDisplay 5.0+) reorganised the SF HDMI packet units. Per NVIDIA's
published C971/CA71 DISP_SF_USER class headers, only three legacy units
remain (AVI at +0x000, GCP at +0x040, ACR at +0x080), and vendor
infoframes must instead be sent through the shared generic infoframe
units at +0x130, whose 9-dword packet slots are loaded through the
shared data port at +0x3f0/+0x3f4.
Add a VSI writer using the same programming sequence OpenRM uses on
these chips (nvhdmipkt_C971.c, programAdvancedInfoframeC971()): disable
the unit and wait for it to idle, clear the SENT status, write the packet
through the data port with a zero inserted in HB3 after the three header
bytes, then enable the unit for every-frame transmission during vblank.
Generic unit 1 is used for the VSI, matching the slot assignment in
NVIDIA's nvkms (NVHDMIPKT_TYPE_SHARED_GENERIC2, unit 0 is reserved
for extended metadata packets and unit 2 for the HDR DRM infoframe,
if those are wired up later).
GB20x so far shared GA10x's display entry point. Give it its own,
gb202_disp_new(), with a gb202_gsp_disp table that supplies the VSI
writer to the GSP path and otherwise carries the same hooks as GA10x.
The following fixes fill in the rest of the GB20x differences there.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../drm/nouveau/include/nvkm/engine/disp.h | 1 +
.../gpu/drm/nouveau/nvkm/engine/device/base.c | 10 +-
.../gpu/drm/nouveau/nvkm/engine/disp/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 91 +++++++++++++++++++
4 files changed, 98 insertions(+), 5 deletions(-)
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
diff --git a/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h b/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h
index 7903d7470d19..01145db32c53 100644
--- a/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h
+++ b/drivers/gpu/drm/nouveau/include/nvkm/engine/disp.h
@@ -87,4 +87,5 @@ int gp102_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct
int gv100_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
int tu102_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
int ga102_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
+int gb202_disp_new(struct nvkm_device *, enum nvkm_subdev_type, int inst, struct nvkm_disp **);
#endif
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c b/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c
index ea62dc97f118..96c8a5b29999 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/device/base.c
@@ -2846,7 +2846,7 @@ nv1b2_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2862,7 +2862,7 @@ nv1b3_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2878,7 +2878,7 @@ nv1b5_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2894,7 +2894,7 @@ nv1b6_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
@@ -2910,7 +2910,7 @@ nv1b7_chipset = {
.pci = { 0x00000001, gh100_pci_new },
.timer = { 0x00000001, gk20a_timer_new },
.vfn = { 0x00000001, ga100_vfn_new },
- .disp = { 0x00000001, ga102_disp_new },
+ .disp = { 0x00000001, gb202_disp_new },
.fifo = { 0x00000001, ga102_fifo_new },
};
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild b/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild
index e1aecd3fe96c..98d6ca5ac311 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/Kbuild
@@ -27,6 +27,7 @@ nvkm-y += nvkm/engine/disp/gp102.o
nvkm-y += nvkm/engine/disp/gv100.o
nvkm-y += nvkm/engine/disp/tu102.o
nvkm-y += nvkm/engine/disp/ga102.o
+nvkm-y += nvkm/engine/disp/gb202.o
nvkm-y += nvkm/engine/disp/udisp.o
nvkm-y += nvkm/engine/disp/uconn.o
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
new file mode 100644
index 000000000000..fa83aee35ae7
--- /dev/null
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright 2026 Valve Corp.
+ */
+#include "priv.h"
+#include "head.h"
+#include "ior.h"
+
+#include <subdev/timer.h>
+
+/* GB20x (NVD5.0) reorganised the SF HDMI packet units. The AVI unit is
+ * unchanged from GV100, but the legacy VSI unit is gone. Vendor infoframes
+ * are sent through the shared generic infoframe units instead. Register
+ * layout per NVIDIA's clc971.h/clca71.h, programming sequence per
+ * nvhdmipkt_C971.c:programAdvancedInfoframeC971().
+ */
+static void
+gb202_sor_hdmi_infoframe_vsi(struct nvkm_ior *ior, int head, void *data, u32 size)
+{
+ struct nvkm_device *device = ior->disp->engine.subdev.device;
+ const u32 hoff = head * 0x400;
+ /* Generic infoframe unit 1, the slot NVIDIA's driver uses for the VSI. */
+ const u32 ctrl = 0x6f0138 + hoff;
+ u8 buf[36] = {};
+ int i;
+
+ /* Disable the unit and wait for it to go idle. */
+ nvkm_mask(device, ctrl, 0x00000001, 0x00000000);
+ if (nvkm_msec(device, 2000,
+ if (!(nvkm_rd32(device, ctrl) & 0x00400000))
+ break;
+ ) < 0)
+ return;
+
+ if (!size)
+ return;
+
+ /* Clear SENT status, and point the data port at unit 1's slot. */
+ nvkm_mask(device, ctrl, 0x00800000, 0x00800000);
+ nvkm_wr32(device, 0x6f03f0 + hoff, 0x00000001);
+
+ /* The data port takes the raw packet, except that a zero is inserted
+ * in HB3 after the three header bytes. A slot is 9 dwords (HB0-3 plus
+ * up to 32 payload bytes). An HDMI infoframe carries at most PB0-27,
+ * so the tail stays zero, and we always write the whole slot.
+ */
+ size = min_t(u32, size, 31);
+ memcpy(buf, data, min_t(u32, size, 3));
+ if (size > 3)
+ memcpy(&buf[4], (u8 *)data + 3, size - 3);
+
+ for (i = 0; i < 36; i += 4) {
+ nvkm_wr32(device, 0x6f03f4 + hoff, buf[i + 0] | buf[i + 1] << 8 |
+ buf[i + 2] << 16 |
+ (u32)buf[i + 3] << 24);
+ }
+
+ /* No flip ID or scanline matching. */
+ nvkm_wr32(device, 0x6f013c + hoff, 0x00000000);
+
+ /* ENABLE | RUN_MODE=ALWAYS | LOC=VBLANK | OFFSET=1 | SIZE=0. */
+ nvkm_wr32(device, ctrl, 0x00000041);
+
+ /* Audio priority low (the init value). */
+ nvkm_wr32(device, 0x6f03f8 + hoff, 0x00000002);
+}
+
+/* GB20x is GSP-only. This table supplies the register programming the
+ * GSP-RM display path needs from the chip.
+ */
+static const struct nvkm_disp_func
+gb202_gsp_disp = {
+ .uevent = &gv100_disp_chan_uevent,
+ .ramht_size = 0x2000,
+ .gsp.intr = tu102_disp_intr,
+ .gsp.head_state = gv100_head_state,
+ .gsp.head_rgpos = gv100_head_rgpos,
+ .gsp.vblank_get = tu102_head_vblank_get,
+ .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ /* The legacy AVI unit is unchanged on GB20x. */
+ .gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
+ .gsp.hdmi_infoframe_vsi = gb202_sor_hdmi_infoframe_vsi,
+};
+
+int
+gb202_disp_new(struct nvkm_device *device, enum nvkm_subdev_type type, int inst,
+ struct nvkm_disp **pdisp)
+{
+ return r535_disp_new(&gb202_gsp_disp, device, type, inst, pdisp);
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 05/10] drm/nouveau/disp: fix HDMI GCP AVMute register offsets on GB20x
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path brackets audio enablement with a General Control Packet
AVMute toggle. r535_sor_hdmi_audio() calls the gsp.hdmi_gcp hook, which
every chip so far serves with tu102_sor_hdmi_gcp() and the legacy GCP
unit at 0x6f00c0/0x6f00cc. On GB20x the SF packet units were compacted
and the old generic and VSI units are gone (ACR keeps slot 2) and the
GCP unit moved from slot 3 to slot 1 (control 0x6f0040 and subpack
0x6f004c from NVIDIA's published clc971.h. The same offsets are also
used by OpenRM's hdmiWriteGeneralCtrlPacketC871() on these chips). The
old addresses are reserved on GB20x, so the AVMute writes were silent
no-ops and mitigated only by the equivalent GCP r535_sor_hdmi_audio()
already sends through the SET_OD_PACKET RM control.
Add a GB20x GCP writer using the new offsets and hook it into
gb202_gsp_disp, keeping the direct MMIO path in sync with the hardware
as on earlier chips.
Only SB0 (the AVMute bit) is written. On NVD5.0 the subpack register also
carries SB1_CTRL (bit 24), which selects where the deep-color CD/PP
fields are generated (hardware or from the driver, with the default being
HW). hdmiWriteGeneralCtrlPacketC871() likewise writes only SB0-SB2.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
index fa83aee35ae7..4863b2b36db0 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -65,6 +65,24 @@ gb202_sor_hdmi_infoframe_vsi(struct nvkm_ior *ior, int head, void *data, u32 siz
nvkm_wr32(device, 0x6f03f8 + hoff, 0x00000002);
}
+/* General Control Packet AVMute bracket. The GCP unit moved to slot 1 on
+ * NVD5.0. Only SB0 (the AVMute bit) is ours to write so we must not do a
+ * full write here: SB1 carries the deep-color CD/PP fields, and SB1_CTRL
+ * (bit 24, new with clc871.h) controls where their generation happens (HW
+ * or driver) on these chips, with the default being HW.
+ */
+static void
+gb202_sor_hdmi_gcp(struct nvkm_ior *sor, int head, bool enable)
+{
+ struct nvkm_device *device = sor->disp->engine.subdev.device;
+ const u32 hdmi = head * 0x400;
+
+ nvkm_mask(device, 0x6f0040 + hdmi, 0x00000001, 0x00000000);
+ nvkm_mask(device, 0x6f004c + hdmi, 0x000000ff, !enable ? 0x00000001 :
+ 0x00000010);
+ nvkm_mask(device, 0x6f0040 + hdmi, 0x00000001, 0x00000001);
+}
+
/* GB20x is GSP-only. This table supplies the register programming the
* GSP-RM display path needs from the chip.
*/
@@ -77,7 +95,7 @@ gb202_gsp_disp = {
.gsp.head_rgpos = gv100_head_rgpos,
.gsp.vblank_get = tu102_head_vblank_get,
.gsp.vblank_put = tu102_head_vblank_put,
- .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ .gsp.hdmi_gcp = gb202_sor_hdmi_gcp,
/* The legacy AVI unit is unchanged on GB20x. */
.gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
.gsp.hdmi_infoframe_vsi = gb202_sor_hdmi_infoframe_vsi,
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 05/10] drm/nouveau/disp: fix HDMI GCP AVMute register offsets on GB20x
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path brackets audio enablement with a General Control Packet
AVMute toggle. r535_sor_hdmi_audio() calls the gsp.hdmi_gcp hook, which
every chip so far serves with tu102_sor_hdmi_gcp() and the legacy GCP
unit at 0x6f00c0/0x6f00cc. On GB20x the SF packet units were compacted
and the old generic and VSI units are gone (ACR keeps slot 2) and the
GCP unit moved from slot 3 to slot 1 (control 0x6f0040 and subpack
0x6f004c from NVIDIA's published clc971.h. The same offsets are also
used by OpenRM's hdmiWriteGeneralCtrlPacketC871() on these chips). The
old addresses are reserved on GB20x, so the AVMute writes were silent
no-ops and mitigated only by the equivalent GCP r535_sor_hdmi_audio()
already sends through the SET_OD_PACKET RM control.
Add a GB20x GCP writer using the new offsets and hook it into
gb202_gsp_disp, keeping the direct MMIO path in sync with the hardware
as on earlier chips.
Only SB0 (the AVMute bit) is written. On NVD5.0 the subpack register also
carries SB1_CTRL (bit 24), which selects where the deep-color CD/PP
fields are generated (hardware or from the driver, with the default being
HW). hdmiWriteGeneralCtrlPacketC871() likewise writes only SB0-SB2.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
index fa83aee35ae7..4863b2b36db0 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -65,6 +65,24 @@ gb202_sor_hdmi_infoframe_vsi(struct nvkm_ior *ior, int head, void *data, u32 siz
nvkm_wr32(device, 0x6f03f8 + hoff, 0x00000002);
}
+/* General Control Packet AVMute bracket. The GCP unit moved to slot 1 on
+ * NVD5.0. Only SB0 (the AVMute bit) is ours to write so we must not do a
+ * full write here: SB1 carries the deep-color CD/PP fields, and SB1_CTRL
+ * (bit 24, new with clc871.h) controls where their generation happens (HW
+ * or driver) on these chips, with the default being HW.
+ */
+static void
+gb202_sor_hdmi_gcp(struct nvkm_ior *sor, int head, bool enable)
+{
+ struct nvkm_device *device = sor->disp->engine.subdev.device;
+ const u32 hdmi = head * 0x400;
+
+ nvkm_mask(device, 0x6f0040 + hdmi, 0x00000001, 0x00000000);
+ nvkm_mask(device, 0x6f004c + hdmi, 0x000000ff, !enable ? 0x00000001 :
+ 0x00000010);
+ nvkm_mask(device, 0x6f0040 + hdmi, 0x00000001, 0x00000001);
+}
+
/* GB20x is GSP-only. This table supplies the register programming the
* GSP-RM display path needs from the chip.
*/
@@ -77,7 +95,7 @@ gb202_gsp_disp = {
.gsp.head_rgpos = gv100_head_rgpos,
.gsp.vblank_get = tu102_head_vblank_get,
.gsp.vblank_put = tu102_head_vblank_put,
- .gsp.hdmi_gcp = tu102_sor_hdmi_gcp,
+ .gsp.hdmi_gcp = gb202_sor_hdmi_gcp,
/* The legacy AVI unit is unchanged on GB20x. */
.gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
.gsp.hdmi_infoframe_vsi = gb202_sor_hdmi_infoframe_vsi,
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 06/10] drm/nouveau/gsp: use per-version DP_CONFIG_STREAM params on r570 firmware
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
NVIDIA removed the deprecated actualPclkHz/linkClkFreqHz fields and the
whole Legacy{activeCnt, activeFrac, activePolarity, mvidWarEnabled,
MvidWarParams} block from the SST sub-struct of
NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS between the 535 and 570 releases
(compared in OpenRM tags 535.113.01 vs 570.144), shrinking the struct.
Everything nouveau writes sits at identical offsets in both layouts
except the trailing SST.bEnableAudioOverRightPanel (written as zero), but
the size is wrong on r570, which means r535_sor_dp_sst() and
r535_sor_dp_vcpi() are sent with an incorrect size.
Route the .sst/.vcpi IOR functions through nvkm_rm_api_disp the same way
bl_ctrl and dp.get_caps/set_indexed_link_rates already are. Keep the
existing implementation for r535 and add an r570 implementation built
against the 570.144 layout, which already exists in r570/nvrm/disp.h but
was unused until now. Also add the NV0073_CTRL_CMD_DP_CONFIG_STREAM
define that was missing from the layout.
Other DP controls sent through shared r535 code did not change layout
between the tags.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 33 ++++++++--
.../nouveau/nvkm/subdev/gsp/rm/r570/disp.c | 64 +++++++++++++++++++
.../nvkm/subdev/gsp/rm/r570/nvrm/disp.h | 2 +
.../gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h | 5 ++
4 files changed, 97 insertions(+), 7 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index f3e55253bcbc..3a8ff621ed62 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -400,16 +400,16 @@ r535_sor_dp_audio(struct nvkm_ior *sor, int head, bool enable)
r535_sor_dp_audio_mute(sor, false);
}
-static void
-r535_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
+static int
+r535_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
{
struct nvkm_disp *disp = sor->disp;
struct NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
ctrl = nvkm_gsp_rm_ctrl_get(&disp->rm.objcom,
NV0073_CTRL_CMD_DP_CONFIG_STREAM, sizeof(*ctrl));
- if (WARN_ON(IS_ERR(ctrl)))
- return;
+ if (IS_ERR(ctrl))
+ return PTR_ERR(ctrl);
ctrl->subDeviceInstance = 0;
ctrl->head = head;
@@ -429,12 +429,20 @@ r535_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u
ctrl->MST.sendACT = 0;
ctrl->MST.singleHeadMSTPipeline = 0;
ctrl->MST.bEnableAudioOverRightPanel = 0;
- WARN_ON(nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl));
+ return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
+}
+
+static void
+r535_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
+{
+ const struct nvkm_rm_api *rmapi = sor->disp->engine.subdev.device->gsp->rm->api;
+
+ WARN_ON(rmapi->disp->dp.vcpi(sor, head, slot, slot_nr, pbn, aligned_pbn));
}
static int
-r535_sor_dp_sst(struct nvkm_ior *sor, int head, bool ef,
- u32 watermark, u32 hblanksym, u32 vblanksym)
+r535_dp_sst(struct nvkm_ior *sor, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym)
{
struct nvkm_disp *disp = sor->disp;
struct NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
@@ -461,6 +469,15 @@ r535_sor_dp_sst(struct nvkm_ior *sor, int head, bool ef,
return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
}
+static int
+r535_sor_dp_sst(struct nvkm_ior *sor, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym)
+{
+ const struct nvkm_rm_api *rmapi = sor->disp->engine.subdev.device->gsp->rm->api;
+
+ return rmapi->disp->dp.sst(sor, head, ef, watermark, hblanksym, vblanksym);
+}
+
static const struct nvkm_ior_func_dp
r535_sor_dp = {
.sst = r535_sor_dp_sst,
@@ -1771,6 +1788,8 @@ r535_disp = {
.dp = {
.get_caps = r535_dp_get_caps,
.set_indexed_link_rates = r535_dp_set_indexed_link_rates,
+ .sst = r535_dp_sst,
+ .vcpi = r535_dp_vcpi,
},
.chan = {
.set_pushbuf = r535_disp_chan_set_pushbuf,
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c
index a96e31c2d80b..8a23837f356e 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c
@@ -5,6 +5,7 @@
#include <rm/rm.h>
#include <engine/disp.h>
+#include <engine/disp/ior.h>
#include <engine/disp/outp.h>
#include "nvhw/drf.h"
@@ -74,6 +75,67 @@ r570_disp_chan_set_pushbuf(struct nvkm_disp *disp, s32 oclass, int inst, struct
return nvkm_gsp_rm_ctrl_wr(&gsp->internal.device.subdevice, ctrl);
}
+static int
+r570_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
+{
+ struct nvkm_disp *disp = sor->disp;
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
+
+ ctrl = nvkm_gsp_rm_ctrl_get(&disp->rm.objcom,
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM, sizeof(*ctrl));
+ if (IS_ERR(ctrl))
+ return PTR_ERR(ctrl);
+
+ ctrl->subDeviceInstance = 0;
+ ctrl->head = head;
+ ctrl->sorIndex = sor->id;
+ ctrl->dpLink = sor->asy.link == 2;
+ ctrl->bEnableOverride = 1;
+ ctrl->bMST = 1;
+ ctrl->hBlankSym = 0;
+ ctrl->vBlankSym = 0;
+ ctrl->colorFormat = 0;
+ ctrl->bEnableTwoHeadOneOr = 0;
+ ctrl->singleHeadMultistreamMode = 0;
+ ctrl->MST.slotStart = slot;
+ ctrl->MST.slotEnd = slot + slot_nr - 1;
+ ctrl->MST.PBN = pbn;
+ ctrl->MST.Timeslice = aligned_pbn;
+ ctrl->MST.sendACT = 0;
+ ctrl->MST.singleHeadMSTPipeline = 0;
+ ctrl->MST.bEnableAudioOverRightPanel = 0;
+ return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
+}
+
+static int
+r570_dp_sst(struct nvkm_ior *sor, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym)
+{
+ struct nvkm_disp *disp = sor->disp;
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
+
+ ctrl = nvkm_gsp_rm_ctrl_get(&disp->rm.objcom,
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM, sizeof(*ctrl));
+ if (IS_ERR(ctrl))
+ return PTR_ERR(ctrl);
+
+ ctrl->subDeviceInstance = 0;
+ ctrl->head = head;
+ ctrl->sorIndex = sor->id;
+ ctrl->dpLink = sor->asy.link == 2;
+ ctrl->bEnableOverride = 1;
+ ctrl->bMST = 0;
+ ctrl->hBlankSym = hblanksym;
+ ctrl->vBlankSym = vblanksym;
+ ctrl->colorFormat = 0;
+ ctrl->bEnableTwoHeadOneOr = 0;
+ ctrl->SST.bEnhancedFraming = ef;
+ ctrl->SST.tuSize = 64;
+ ctrl->SST.waterMark = watermark;
+ ctrl->SST.bEnableAudioOverRightPanel = 0;
+ return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
+}
+
static int
r570_dp_set_indexed_link_rates(struct nvkm_outp *outp)
{
@@ -255,6 +317,8 @@ r570_disp = {
.dp = {
.get_caps = r570_dp_get_caps,
.set_indexed_link_rates = r570_dp_set_indexed_link_rates,
+ .sst = r570_dp_sst,
+ .vcpi = r570_dp_vcpi,
},
.chan = {
.set_pushbuf = r570_disp_chan_set_pushbuf,
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h
index 06e972835d77..742b25a2a12d 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h
@@ -256,6 +256,8 @@ typedef struct NV0073_CTRL_DP_CTRL_PARAMS {
NvU32 eightLaneDpcdBaseAddr;
} NV0073_CTRL_DP_CTRL_PARAMS;
+#define NV0073_CTRL_CMD_DP_CONFIG_STREAM (0x731362U) /* finn: Evaluated from "(FINN_NV04_DISPLAY_COMMON_DP_INTERFACE_ID << 8) | NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS_MESSAGE_ID" */
+
typedef struct NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS {
NvU32 subDeviceInstance;
NvU32 head;
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h
index 0fb0e67406c6..e9ac47d86b69 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h
@@ -6,6 +6,7 @@
#ifndef __NVKM_RM_H__
#define __NVKM_RM_H__
#include "handles.h"
+struct nvkm_ior;
struct nvkm_outp;
struct r535_gr;
@@ -93,6 +94,10 @@ struct nvkm_rm_api {
struct {
int (*get_caps)(struct nvkm_disp *, int *link_bw, bool *mst, bool *wm);
int (*set_indexed_link_rates)(struct nvkm_outp *);
+ int (*sst)(struct nvkm_ior *, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym);
+ int (*vcpi)(struct nvkm_ior *, int head,
+ u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn);
} dp;
struct {
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 06/10] drm/nouveau/gsp: use per-version DP_CONFIG_STREAM params on r570 firmware
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
NVIDIA removed the deprecated actualPclkHz/linkClkFreqHz fields and the
whole Legacy{activeCnt, activeFrac, activePolarity, mvidWarEnabled,
MvidWarParams} block from the SST sub-struct of
NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS between the 535 and 570 releases
(compared in OpenRM tags 535.113.01 vs 570.144), shrinking the struct.
Everything nouveau writes sits at identical offsets in both layouts
except the trailing SST.bEnableAudioOverRightPanel (written as zero), but
the size is wrong on r570, which means r535_sor_dp_sst() and
r535_sor_dp_vcpi() are sent with an incorrect size.
Route the .sst/.vcpi IOR functions through nvkm_rm_api_disp the same way
bl_ctrl and dp.get_caps/set_indexed_link_rates already are. Keep the
existing implementation for r535 and add an r570 implementation built
against the 570.144 layout, which already exists in r570/nvrm/disp.h but
was unused until now. Also add the NV0073_CTRL_CMD_DP_CONFIG_STREAM
define that was missing from the layout.
Other DP controls sent through shared r535 code did not change layout
between the tags.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 33 ++++++++--
.../nouveau/nvkm/subdev/gsp/rm/r570/disp.c | 64 +++++++++++++++++++
.../nvkm/subdev/gsp/rm/r570/nvrm/disp.h | 2 +
.../gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h | 5 ++
4 files changed, 97 insertions(+), 7 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index f3e55253bcbc..3a8ff621ed62 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -400,16 +400,16 @@ r535_sor_dp_audio(struct nvkm_ior *sor, int head, bool enable)
r535_sor_dp_audio_mute(sor, false);
}
-static void
-r535_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
+static int
+r535_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
{
struct nvkm_disp *disp = sor->disp;
struct NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
ctrl = nvkm_gsp_rm_ctrl_get(&disp->rm.objcom,
NV0073_CTRL_CMD_DP_CONFIG_STREAM, sizeof(*ctrl));
- if (WARN_ON(IS_ERR(ctrl)))
- return;
+ if (IS_ERR(ctrl))
+ return PTR_ERR(ctrl);
ctrl->subDeviceInstance = 0;
ctrl->head = head;
@@ -429,12 +429,20 @@ r535_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u
ctrl->MST.sendACT = 0;
ctrl->MST.singleHeadMSTPipeline = 0;
ctrl->MST.bEnableAudioOverRightPanel = 0;
- WARN_ON(nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl));
+ return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
+}
+
+static void
+r535_sor_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
+{
+ const struct nvkm_rm_api *rmapi = sor->disp->engine.subdev.device->gsp->rm->api;
+
+ WARN_ON(rmapi->disp->dp.vcpi(sor, head, slot, slot_nr, pbn, aligned_pbn));
}
static int
-r535_sor_dp_sst(struct nvkm_ior *sor, int head, bool ef,
- u32 watermark, u32 hblanksym, u32 vblanksym)
+r535_dp_sst(struct nvkm_ior *sor, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym)
{
struct nvkm_disp *disp = sor->disp;
struct NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
@@ -461,6 +469,15 @@ r535_sor_dp_sst(struct nvkm_ior *sor, int head, bool ef,
return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
}
+static int
+r535_sor_dp_sst(struct nvkm_ior *sor, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym)
+{
+ const struct nvkm_rm_api *rmapi = sor->disp->engine.subdev.device->gsp->rm->api;
+
+ return rmapi->disp->dp.sst(sor, head, ef, watermark, hblanksym, vblanksym);
+}
+
static const struct nvkm_ior_func_dp
r535_sor_dp = {
.sst = r535_sor_dp_sst,
@@ -1771,6 +1788,8 @@ r535_disp = {
.dp = {
.get_caps = r535_dp_get_caps,
.set_indexed_link_rates = r535_dp_set_indexed_link_rates,
+ .sst = r535_dp_sst,
+ .vcpi = r535_dp_vcpi,
},
.chan = {
.set_pushbuf = r535_disp_chan_set_pushbuf,
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c
index a96e31c2d80b..8a23837f356e 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/disp.c
@@ -5,6 +5,7 @@
#include <rm/rm.h>
#include <engine/disp.h>
+#include <engine/disp/ior.h>
#include <engine/disp/outp.h>
#include "nvhw/drf.h"
@@ -74,6 +75,67 @@ r570_disp_chan_set_pushbuf(struct nvkm_disp *disp, s32 oclass, int inst, struct
return nvkm_gsp_rm_ctrl_wr(&gsp->internal.device.subdevice, ctrl);
}
+static int
+r570_dp_vcpi(struct nvkm_ior *sor, int head, u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn)
+{
+ struct nvkm_disp *disp = sor->disp;
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
+
+ ctrl = nvkm_gsp_rm_ctrl_get(&disp->rm.objcom,
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM, sizeof(*ctrl));
+ if (IS_ERR(ctrl))
+ return PTR_ERR(ctrl);
+
+ ctrl->subDeviceInstance = 0;
+ ctrl->head = head;
+ ctrl->sorIndex = sor->id;
+ ctrl->dpLink = sor->asy.link == 2;
+ ctrl->bEnableOverride = 1;
+ ctrl->bMST = 1;
+ ctrl->hBlankSym = 0;
+ ctrl->vBlankSym = 0;
+ ctrl->colorFormat = 0;
+ ctrl->bEnableTwoHeadOneOr = 0;
+ ctrl->singleHeadMultistreamMode = 0;
+ ctrl->MST.slotStart = slot;
+ ctrl->MST.slotEnd = slot + slot_nr - 1;
+ ctrl->MST.PBN = pbn;
+ ctrl->MST.Timeslice = aligned_pbn;
+ ctrl->MST.sendACT = 0;
+ ctrl->MST.singleHeadMSTPipeline = 0;
+ ctrl->MST.bEnableAudioOverRightPanel = 0;
+ return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
+}
+
+static int
+r570_dp_sst(struct nvkm_ior *sor, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym)
+{
+ struct nvkm_disp *disp = sor->disp;
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS *ctrl;
+
+ ctrl = nvkm_gsp_rm_ctrl_get(&disp->rm.objcom,
+ NV0073_CTRL_CMD_DP_CONFIG_STREAM, sizeof(*ctrl));
+ if (IS_ERR(ctrl))
+ return PTR_ERR(ctrl);
+
+ ctrl->subDeviceInstance = 0;
+ ctrl->head = head;
+ ctrl->sorIndex = sor->id;
+ ctrl->dpLink = sor->asy.link == 2;
+ ctrl->bEnableOverride = 1;
+ ctrl->bMST = 0;
+ ctrl->hBlankSym = hblanksym;
+ ctrl->vBlankSym = vblanksym;
+ ctrl->colorFormat = 0;
+ ctrl->bEnableTwoHeadOneOr = 0;
+ ctrl->SST.bEnhancedFraming = ef;
+ ctrl->SST.tuSize = 64;
+ ctrl->SST.waterMark = watermark;
+ ctrl->SST.bEnableAudioOverRightPanel = 0;
+ return nvkm_gsp_rm_ctrl_wr(&disp->rm.objcom, ctrl);
+}
+
static int
r570_dp_set_indexed_link_rates(struct nvkm_outp *outp)
{
@@ -255,6 +317,8 @@ r570_disp = {
.dp = {
.get_caps = r570_dp_get_caps,
.set_indexed_link_rates = r570_dp_set_indexed_link_rates,
+ .sst = r570_dp_sst,
+ .vcpi = r570_dp_vcpi,
},
.chan = {
.set_pushbuf = r570_disp_chan_set_pushbuf,
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h
index 06e972835d77..742b25a2a12d 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/nvrm/disp.h
@@ -256,6 +256,8 @@ typedef struct NV0073_CTRL_DP_CTRL_PARAMS {
NvU32 eightLaneDpcdBaseAddr;
} NV0073_CTRL_DP_CTRL_PARAMS;
+#define NV0073_CTRL_CMD_DP_CONFIG_STREAM (0x731362U) /* finn: Evaluated from "(FINN_NV04_DISPLAY_COMMON_DP_INTERFACE_ID << 8) | NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS_MESSAGE_ID" */
+
typedef struct NV0073_CTRL_CMD_DP_CONFIG_STREAM_PARAMS {
NvU32 subDeviceInstance;
NvU32 head;
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h
index 0fb0e67406c6..e9ac47d86b69 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/rm.h
@@ -6,6 +6,7 @@
#ifndef __NVKM_RM_H__
#define __NVKM_RM_H__
#include "handles.h"
+struct nvkm_ior;
struct nvkm_outp;
struct r535_gr;
@@ -93,6 +94,10 @@ struct nvkm_rm_api {
struct {
int (*get_caps)(struct nvkm_disp *, int *link_bw, bool *mst, bool *wm);
int (*set_indexed_link_rates)(struct nvkm_outp *);
+ int (*sst)(struct nvkm_ior *, int head, bool ef,
+ u32 watermark, u32 hblanksym, u32 vblanksym);
+ int (*vcpi)(struct nvkm_ior *, int head,
+ u8 slot, u8 slot_nr, u16 pbn, u16 aligned_pbn);
} dp;
struct {
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 07/10] drm/nouveau/disp: fix head state readback on GB20x
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path reads armed head state and the RG scanout position through
gv100_head_state() and gv100_head_rgpos() on every generation.
gv100_head_state() reads the core channel's state mirror at a 0x400
per-head stride, which NVD5.0 (GB20x) doubled. Per NVIDIA's published
CA7D class header every HEAD_SET method sits at 0x2000 + head * 0x800,
while the mirror bases are unchanged (assembly at 0x680000, armed at
+0x8000, per OpenRM's v03_00 channel-user-base HAL which is still used on
DISPv0502).
Add gb202_head_state(), the same readback at the 0x800 stride, and
supply it through gb202_gsp_disp.
gv100_head_rgpos() is kept. The RG registers keep their per-head 0x800
stride on NVD5.0, and OpenRM's kdispReadRgLineCountAndFrameCount_v03_00
still reads NV_PDISP_RG_DPCA on DISPv0502.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 49 ++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
index 4863b2b36db0..a66c820be9fe 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -83,6 +83,53 @@ gb202_sor_hdmi_gcp(struct nvkm_ior *sor, int head, bool enable)
nvkm_mask(device, 0x6f0040 + hdmi, 0x00000001, 0x00000001);
}
+/* Same core-channel state mirror as gv100_head_state() (assembly at 0x680000,
+ * armed at +0x8000, per-head method offsets unchanged), but NVD5.0 spaces
+ * heads 0x800 apart (see NVCA7D_HEAD_SET_*(a) in clca7d.h).
+ */
+static void
+gb202_head_state(struct nvkm_head *head, struct nvkm_head_state *state)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+ const u32 hoff = (state == &head->arm) * 0x8000 + head->id * 0x800;
+ u32 data;
+
+ data = nvkm_rd32(device, 0x682064 + hoff);
+ state->vtotal = (data & 0xffff0000) >> 16;
+ state->htotal = (data & 0x0000ffff);
+ data = nvkm_rd32(device, 0x682068 + hoff);
+ state->vsynce = (data & 0xffff0000) >> 16;
+ state->hsynce = (data & 0x0000ffff);
+ data = nvkm_rd32(device, 0x68206c + hoff);
+ state->vblanke = (data & 0xffff0000) >> 16;
+ state->hblanke = (data & 0x0000ffff);
+ data = nvkm_rd32(device, 0x682070 + hoff);
+ state->vblanks = (data & 0xffff0000) >> 16;
+ state->hblanks = (data & 0x0000ffff);
+ /* Bit 31 is ADJ1000DIV1001, not a HERTZ bit. We don't have enough bits
+ * to add the full clock in hz on Blackwell (35 bits), but state->hz
+ * is unused and obsolete under GSP so this is fine.
+ */
+ state->hz = nvkm_rd32(device, 0x68200c + hoff) & 0x7fffffff;
+
+ data = nvkm_rd32(device, 0x682004 + hoff);
+ switch ((data & 0x000000f0) >> 4) {
+ case 5:
+ state->or.depth = 30;
+ break;
+ case 4:
+ state->or.depth = 24;
+ break;
+ case 1:
+ state->or.depth = 18;
+ break;
+ default:
+ state->or.depth = 18;
+ WARN_ON(1);
+ break;
+ }
+}
+
/* GB20x is GSP-only. This table supplies the register programming the
* GSP-RM display path needs from the chip.
*/
@@ -91,7 +138,7 @@ gb202_gsp_disp = {
.uevent = &gv100_disp_chan_uevent,
.ramht_size = 0x2000,
.gsp.intr = tu102_disp_intr,
- .gsp.head_state = gv100_head_state,
+ .gsp.head_state = gb202_head_state,
.gsp.head_rgpos = gv100_head_rgpos,
.gsp.vblank_get = tu102_head_vblank_get,
.gsp.vblank_put = tu102_head_vblank_put,
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 07/10] drm/nouveau/disp: fix head state readback on GB20x
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path reads armed head state and the RG scanout position through
gv100_head_state() and gv100_head_rgpos() on every generation.
gv100_head_state() reads the core channel's state mirror at a 0x400
per-head stride, which NVD5.0 (GB20x) doubled. Per NVIDIA's published
CA7D class header every HEAD_SET method sits at 0x2000 + head * 0x800,
while the mirror bases are unchanged (assembly at 0x680000, armed at
+0x8000, per OpenRM's v03_00 channel-user-base HAL which is still used on
DISPv0502).
Add gb202_head_state(), the same readback at the 0x800 stride, and
supply it through gb202_gsp_disp.
gv100_head_rgpos() is kept. The RG registers keep their per-head 0x800
stride on NVD5.0, and OpenRM's kdispReadRgLineCountAndFrameCount_v03_00
still reads NV_PDISP_RG_DPCA on DISPv0502.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 49 ++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
index 4863b2b36db0..a66c820be9fe 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -83,6 +83,53 @@ gb202_sor_hdmi_gcp(struct nvkm_ior *sor, int head, bool enable)
nvkm_mask(device, 0x6f0040 + hdmi, 0x00000001, 0x00000001);
}
+/* Same core-channel state mirror as gv100_head_state() (assembly at 0x680000,
+ * armed at +0x8000, per-head method offsets unchanged), but NVD5.0 spaces
+ * heads 0x800 apart (see NVCA7D_HEAD_SET_*(a) in clca7d.h).
+ */
+static void
+gb202_head_state(struct nvkm_head *head, struct nvkm_head_state *state)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+ const u32 hoff = (state == &head->arm) * 0x8000 + head->id * 0x800;
+ u32 data;
+
+ data = nvkm_rd32(device, 0x682064 + hoff);
+ state->vtotal = (data & 0xffff0000) >> 16;
+ state->htotal = (data & 0x0000ffff);
+ data = nvkm_rd32(device, 0x682068 + hoff);
+ state->vsynce = (data & 0xffff0000) >> 16;
+ state->hsynce = (data & 0x0000ffff);
+ data = nvkm_rd32(device, 0x68206c + hoff);
+ state->vblanke = (data & 0xffff0000) >> 16;
+ state->hblanke = (data & 0x0000ffff);
+ data = nvkm_rd32(device, 0x682070 + hoff);
+ state->vblanks = (data & 0xffff0000) >> 16;
+ state->hblanks = (data & 0x0000ffff);
+ /* Bit 31 is ADJ1000DIV1001, not a HERTZ bit. We don't have enough bits
+ * to add the full clock in hz on Blackwell (35 bits), but state->hz
+ * is unused and obsolete under GSP so this is fine.
+ */
+ state->hz = nvkm_rd32(device, 0x68200c + hoff) & 0x7fffffff;
+
+ data = nvkm_rd32(device, 0x682004 + hoff);
+ switch ((data & 0x000000f0) >> 4) {
+ case 5:
+ state->or.depth = 30;
+ break;
+ case 4:
+ state->or.depth = 24;
+ break;
+ case 1:
+ state->or.depth = 18;
+ break;
+ default:
+ state->or.depth = 18;
+ WARN_ON(1);
+ break;
+ }
+}
+
/* GB20x is GSP-only. This table supplies the register programming the
* GSP-RM display path needs from the chip.
*/
@@ -91,7 +138,7 @@ gb202_gsp_disp = {
.uevent = &gv100_disp_chan_uevent,
.ramht_size = 0x2000,
.gsp.intr = tu102_disp_intr,
- .gsp.head_state = gv100_head_state,
+ .gsp.head_state = gb202_head_state,
.gsp.head_rgpos = gv100_head_rgpos,
.gsp.vblank_get = tu102_head_vblank_get,
.gsp.vblank_put = tu102_head_vblank_put,
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 08/10] drm/nouveau/gsp: fix vblank interrupts on GB20x
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path programs per-head timing (vblank) interrupts the same way on
every generation. NVD5.0 (GB20x) reworked the FE interrupt frontend
around four message-based kernel vectors (high latency, low latency, PMU,
and GSP) and moved RM head-timing interrupts to the dedicated low-latency
vector:
- The enable is NV_PDISP_FE_RM_INTR_EN1_HEAD_TIMING, 0x611ef0 +
head*4 (570.144 kernel_head_0501.c, renamed kernel_head_0502.c from
575.51.02 on, and v05_01 dev_disp.h).
- The vector is reported as a separate interrupt table entry,
MC_ENGINE_IDX_DISP_LOW (intr_gb202.c, intrCacheDispIntrVectors).
- The vector must be re-armed through NV_PDISP_FE_INTR_RETRIGGER(1)
at 0x611f34 after servicing (kdispServiceInterrupt ->
kdispIntrRetrigger_v05_01).
The event latch (0x611800), per-head status (0x611c00), and dispatch
summary (0x611ec0) the interrupt handler uses are unchanged on GB20x
(kheadReadPendingVblank_v03_00 and kheadResetPendingLastData_v03_00
remain for DISPv0502+).
On GB20x the old code enables head timing onto the legacy vector, leaves
its handler there, and never re-arms the message-based vectors. Page
flips still complete (nv50 sends those events from the commit path), so
the desktop looks fine while DRM vblank waits and vblank sequence queries
are affected.
Supply GB20x vblank enables and an interrupt handler that re-arms the
vector after servicing through gb202_gsp_disp, translate the low-latency
interrupt table entry as a second NVKM_ENGINE_DISP instance, and flag
the table so r535_disp_oneinit() attaches the handler to that instance.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 42 +++++++++++++++++--
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 5 ++-
.../drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c | 9 ++++
3 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
index a66c820be9fe..f78669bafd64 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -130,6 +130,40 @@ gb202_head_state(struct nvkm_head *head, struct nvkm_head_state *state)
}
}
+/* NVD5.0 (GB20x and later) moved the RM head-timing interrupt enable to
+ * the low-latency vector's EN1 block. The event latch is unchanged.
+ */
+static void
+gb202_head_vblank_put(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_mask(device, 0x611ef0 + (head->id * 4), 0x00000002, 0x00000000);
+}
+
+static void
+gb202_head_vblank_get(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_wr32(device, 0x611800 + (head->id * 4), 0x00000002);
+ nvkm_mask(device, 0x611ef0 + (head->id * 4), 0x00000002, 0x00000002);
+}
+
+static irqreturn_t
+gb202_disp_intr(struct nvkm_inth *inth)
+{
+ struct nvkm_disp *disp = container_of(inth, typeof(*disp), engine.subdev.inth);
+ irqreturn_t ret = tu102_disp_intr(inth);
+
+ /* The FE interrupt vectors are message-based on NVD5.0. Re-arm the
+ * low-latency vector so it fires again for any event that latched
+ * while we were servicing.
+ */
+ nvkm_wr32(disp->engine.subdev.device, 0x611f34, 0x00000001);
+ return ret;
+}
+
/* GB20x is GSP-only. This table supplies the register programming the
* GSP-RM display path needs from the chip.
*/
@@ -137,11 +171,13 @@ static const struct nvkm_disp_func
gb202_gsp_disp = {
.uevent = &gv100_disp_chan_uevent,
.ramht_size = 0x2000,
- .gsp.intr = tu102_disp_intr,
+ /* Head timing arrives on the dedicated low-latency vector. */
+ .gsp.intr = gb202_disp_intr,
+ .gsp.intr_low_latency = true,
.gsp.head_state = gb202_head_state,
.gsp.head_rgpos = gv100_head_rgpos,
- .gsp.vblank_get = tu102_head_vblank_get,
- .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.vblank_get = gb202_head_vblank_get,
+ .gsp.vblank_put = gb202_head_vblank_put,
.gsp.hdmi_gcp = gb202_sor_hdmi_gcp,
/* The legacy AVI unit is unchanged on GB20x. */
.gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index 3a8ff621ed62..a95f78c4502f 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -1705,7 +1705,10 @@ r535_disp_oneinit(struct nvkm_disp *disp)
/* Chips that raise head-timing interrupts on a separate low-latency
* vector report it as a second DISP interrupt table entry, exposed
- * as instance 1 by the RM engine-index translation.
+ * as instance 1 by the RM engine-index translation (see
+ * r570_gsp_xlat_mc_engine_idx()). Their high-latency vector
+ * (instance 0) is left unhandled as no event nouveau enables is
+ * routed to it, and without a handler it stays masked.
*/
ret = nvkm_gsp_intr_stall(gsp, disp->engine.subdev.type,
disp->func->gsp.intr_low_latency ? 1 : disp->engine.subdev.inst);
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c
index 3e391646d8f7..b45781cd0dfd 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c
@@ -44,6 +44,15 @@ r570_gsp_xlat_mc_engine_idx(u32 mc_engine_idx, enum nvkm_subdev_type *ptype, int
*ptype = NVKM_ENGINE_DISP;
*pinst = 0;
return true;
+ case MC_ENGINE_IDX_DISP_LOW:
+ /* GB20x+ report a separate low-latency display vector, used
+ * for head-timing interrupts. Expose it as a second DISP
+ * interrupt instance. r535_disp_oneinit() attaches the
+ * handler to it when the chip's gsp.intr_low_latency is set.
+ */
+ *ptype = NVKM_ENGINE_DISP;
+ *pinst = 1;
+ return true;
case MC_ENGINE_IDX_CE0 ... MC_ENGINE_IDX_CE19:
*ptype = NVKM_ENGINE_CE;
*pinst = mc_engine_idx - MC_ENGINE_IDX_CE0;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 08/10] drm/nouveau/gsp: fix vblank interrupts on GB20x
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The GSP path programs per-head timing (vblank) interrupts the same way on
every generation. NVD5.0 (GB20x) reworked the FE interrupt frontend
around four message-based kernel vectors (high latency, low latency, PMU,
and GSP) and moved RM head-timing interrupts to the dedicated low-latency
vector:
- The enable is NV_PDISP_FE_RM_INTR_EN1_HEAD_TIMING, 0x611ef0 +
head*4 (570.144 kernel_head_0501.c, renamed kernel_head_0502.c from
575.51.02 on, and v05_01 dev_disp.h).
- The vector is reported as a separate interrupt table entry,
MC_ENGINE_IDX_DISP_LOW (intr_gb202.c, intrCacheDispIntrVectors).
- The vector must be re-armed through NV_PDISP_FE_INTR_RETRIGGER(1)
at 0x611f34 after servicing (kdispServiceInterrupt ->
kdispIntrRetrigger_v05_01).
The event latch (0x611800), per-head status (0x611c00), and dispatch
summary (0x611ec0) the interrupt handler uses are unchanged on GB20x
(kheadReadPendingVblank_v03_00 and kheadResetPendingLastData_v03_00
remain for DISPv0502+).
On GB20x the old code enables head timing onto the legacy vector, leaves
its handler there, and never re-arms the message-based vectors. Page
flips still complete (nv50 sends those events from the commit path), so
the desktop looks fine while DRM vblank waits and vblank sequence queries
are affected.
Supply GB20x vblank enables and an interrupt handler that re-arms the
vector after servicing through gb202_gsp_disp, translate the low-latency
interrupt table entry as a second NVKM_ENGINE_DISP instance, and flag
the table so r535_disp_oneinit() attaches the handler to that instance.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
.../gpu/drm/nouveau/nvkm/engine/disp/gb202.c | 42 +++++++++++++++++--
.../nouveau/nvkm/subdev/gsp/rm/r535/disp.c | 5 ++-
.../drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c | 9 ++++
3 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
index a66c820be9fe..f78669bafd64 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/disp/gb202.c
@@ -130,6 +130,40 @@ gb202_head_state(struct nvkm_head *head, struct nvkm_head_state *state)
}
}
+/* NVD5.0 (GB20x and later) moved the RM head-timing interrupt enable to
+ * the low-latency vector's EN1 block. The event latch is unchanged.
+ */
+static void
+gb202_head_vblank_put(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_mask(device, 0x611ef0 + (head->id * 4), 0x00000002, 0x00000000);
+}
+
+static void
+gb202_head_vblank_get(struct nvkm_head *head)
+{
+ struct nvkm_device *device = head->disp->engine.subdev.device;
+
+ nvkm_wr32(device, 0x611800 + (head->id * 4), 0x00000002);
+ nvkm_mask(device, 0x611ef0 + (head->id * 4), 0x00000002, 0x00000002);
+}
+
+static irqreturn_t
+gb202_disp_intr(struct nvkm_inth *inth)
+{
+ struct nvkm_disp *disp = container_of(inth, typeof(*disp), engine.subdev.inth);
+ irqreturn_t ret = tu102_disp_intr(inth);
+
+ /* The FE interrupt vectors are message-based on NVD5.0. Re-arm the
+ * low-latency vector so it fires again for any event that latched
+ * while we were servicing.
+ */
+ nvkm_wr32(disp->engine.subdev.device, 0x611f34, 0x00000001);
+ return ret;
+}
+
/* GB20x is GSP-only. This table supplies the register programming the
* GSP-RM display path needs from the chip.
*/
@@ -137,11 +171,13 @@ static const struct nvkm_disp_func
gb202_gsp_disp = {
.uevent = &gv100_disp_chan_uevent,
.ramht_size = 0x2000,
- .gsp.intr = tu102_disp_intr,
+ /* Head timing arrives on the dedicated low-latency vector. */
+ .gsp.intr = gb202_disp_intr,
+ .gsp.intr_low_latency = true,
.gsp.head_state = gb202_head_state,
.gsp.head_rgpos = gv100_head_rgpos,
- .gsp.vblank_get = tu102_head_vblank_get,
- .gsp.vblank_put = tu102_head_vblank_put,
+ .gsp.vblank_get = gb202_head_vblank_get,
+ .gsp.vblank_put = gb202_head_vblank_put,
.gsp.hdmi_gcp = gb202_sor_hdmi_gcp,
/* The legacy AVI unit is unchanged on GB20x. */
.gsp.hdmi_infoframe_avi = gv100_sor_hdmi_infoframe_avi,
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
index 3a8ff621ed62..a95f78c4502f 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
@@ -1705,7 +1705,10 @@ r535_disp_oneinit(struct nvkm_disp *disp)
/* Chips that raise head-timing interrupts on a separate low-latency
* vector report it as a second DISP interrupt table entry, exposed
- * as instance 1 by the RM engine-index translation.
+ * as instance 1 by the RM engine-index translation (see
+ * r570_gsp_xlat_mc_engine_idx()). Their high-latency vector
+ * (instance 0) is left unhandled as no event nouveau enables is
+ * routed to it, and without a handler it stays masked.
*/
ret = nvkm_gsp_intr_stall(gsp, disp->engine.subdev.type,
disp->func->gsp.intr_low_latency ? 1 : disp->engine.subdev.inst);
diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c
index 3e391646d8f7..b45781cd0dfd 100644
--- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c
+++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r570/gsp.c
@@ -44,6 +44,15 @@ r570_gsp_xlat_mc_engine_idx(u32 mc_engine_idx, enum nvkm_subdev_type *ptype, int
*ptype = NVKM_ENGINE_DISP;
*pinst = 0;
return true;
+ case MC_ENGINE_IDX_DISP_LOW:
+ /* GB20x+ report a separate low-latency display vector, used
+ * for head-timing interrupts. Expose it as a second DISP
+ * interrupt instance. r535_disp_oneinit() attaches the
+ * handler to it when the chip's gsp.intr_low_latency is set.
+ */
+ *ptype = NVKM_ENGINE_DISP;
+ *pinst = 1;
+ return true;
case MC_ENGINE_IDX_CE0 ... MC_ENGINE_IDX_CE19:
*ptype = NVKM_ENGINE_CE;
*pinst = mc_engine_idx - MC_ENGINE_IDX_CE0;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 09/10] drm/nouveau/dispnv50: program pixel clocks above 2.147GHz on GB20x
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
The HEAD_SET_PIXEL_CLOCK_FREQUENCY(_MAX) methods carry only 31 HERTZ
bits. Starting with C97D the upper bits live in separate
HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI(_MAX) methods, which nouveau never
programmed and headca7d_mode() computed m->clock * 1000 into the 31-bit
field. NVVAL's mask then silently truncates anything past 2^31 Hz, which
means that every mode scanned out at pclk modulo 2^31.
No mode nouveau can currently commit crosses the boundary (an
uncompressed HDMI FRL mode tops out around 1.78GHz at 8bpc), but this is
a prerequisite for the upcoming DSC work, which makes 2.147GHz+ modes
reachable.
Program the full value split across the low and HI methods, exactly
as OpenRM's EvoSetRasterParams9() does (nvkms-evo4.c, 31-bit low word
plus the 4 HI HERTZ bits, giving 35 bits of range).
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
drivers/gpu/drm/nouveau/dispnv50/headca7d.c | 21 ++++++++++++++++---
.../drm/nouveau/include/nvhw/class/clca7d.h | 4 ++++
2 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/dispnv50/headca7d.c b/drivers/gpu/drm/nouveau/dispnv50/headca7d.c
index eeaeb15aa664..678cbc78ca9f 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/headca7d.c
+++ b/drivers/gpu/drm/nouveau/dispnv50/headca7d.c
@@ -219,10 +219,11 @@ headca7d_mode(struct nv50_head *head, struct nv50_head_atom *asyh)
{
struct nvif_push *push = &head->disp->core->chan.push;
struct nv50_head_mode *m = &asyh->mode;
+ const u64 hz = (u64)m->clock * 1000;
const int i = head->base.index;
int ret;
- ret = PUSH_WAIT(push, 11);
+ ret = PUSH_WAIT(push, 14);
if (ret)
return ret;
@@ -245,11 +246,25 @@ headca7d_mode(struct nv50_head *head, struct nv50_head_atom *asyh)
PUSH_MTHD(push, NVCA7D, HEAD_SET_CONTROL(i),
NVDEF(NVCA7D, HEAD_SET_CONTROL, STRUCTURE, PROGRESSIVE));
+ /* The FREQUENCY methods carry only 31 HERTZ bits; the upper bits
+ * of anything past 2.147GHz live in the HI methods
+ * (EvoSetRasterParams9()). Truncating would scan out at pclk modulo 2^31.
+ */
PUSH_MTHD(push, NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY(i),
- NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY, HERTZ, m->clock * 1000));
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY, HERTZ,
+ (u32)(hz & 0x7fffffff)));
PUSH_MTHD(push, NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_MAX(i),
- NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_MAX, HERTZ, m->clock * 1000));
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_MAX, HERTZ,
+ (u32)(hz & 0x7fffffff)));
+
+ PUSH_MTHD(push, NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI(i),
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI, HERTZ,
+ (u32)(hz >> 31)),
+
+ HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX(i),
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX, HERTZ,
+ (u32)(hz >> 31)));
return 0;
}
diff --git a/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h b/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h
index 0fec6fc21d44..5754878ddc69 100644
--- a/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h
+++ b/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h
@@ -734,6 +734,10 @@
#define NVCA7D_HEAD_SET_CONTROL_CURSOR_COMPOSITION_BYPASS 20:20
#define NVCA7D_HEAD_SET_CONTROL_CURSOR_COMPOSITION_BYPASS_DISABLE (0x00000000)
#define NVCA7D_HEAD_SET_CONTROL_CURSOR_COMPOSITION_BYPASS_ENABLE (0x00000001)
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI(a) (0x000020C0 + (a)*0x00000800)
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_HERTZ 3:0
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX(a) (0x000020C4 + (a)*0x00000800)
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX_HERTZ 3:0
#define NVCA7D_HEAD_SET_SURFACE_ADDRESS_HI_CRC(a) (0x00002150 + (a)*0x00000800)
#define NVCA7D_HEAD_SET_SURFACE_ADDRESS_HI_CRC_ADDRESS_HI 31:0
#define NVCA7D_HEAD_SET_SURFACE_ADDRESS_LO_CRC(a) (0x00002154 + (a)*0x00000800)
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 09/10] drm/nouveau/dispnv50: program pixel clocks above 2.147GHz on GB20x
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
The HEAD_SET_PIXEL_CLOCK_FREQUENCY(_MAX) methods carry only 31 HERTZ
bits. Starting with C97D the upper bits live in separate
HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI(_MAX) methods, which nouveau never
programmed and headca7d_mode() computed m->clock * 1000 into the 31-bit
field. NVVAL's mask then silently truncates anything past 2^31 Hz, which
means that every mode scanned out at pclk modulo 2^31.
No mode nouveau can currently commit crosses the boundary (an
uncompressed HDMI FRL mode tops out around 1.78GHz at 8bpc), but this is
a prerequisite for the upcoming DSC work, which makes 2.147GHz+ modes
reachable.
Program the full value split across the low and HI methods, exactly
as OpenRM's EvoSetRasterParams9() does (nvkms-evo4.c, 31-bit low word
plus the 4 HI HERTZ bits, giving 35 bits of range).
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
drivers/gpu/drm/nouveau/dispnv50/headca7d.c | 21 ++++++++++++++++---
.../drm/nouveau/include/nvhw/class/clca7d.h | 4 ++++
2 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/dispnv50/headca7d.c b/drivers/gpu/drm/nouveau/dispnv50/headca7d.c
index eeaeb15aa664..678cbc78ca9f 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/headca7d.c
+++ b/drivers/gpu/drm/nouveau/dispnv50/headca7d.c
@@ -219,10 +219,11 @@ headca7d_mode(struct nv50_head *head, struct nv50_head_atom *asyh)
{
struct nvif_push *push = &head->disp->core->chan.push;
struct nv50_head_mode *m = &asyh->mode;
+ const u64 hz = (u64)m->clock * 1000;
const int i = head->base.index;
int ret;
- ret = PUSH_WAIT(push, 11);
+ ret = PUSH_WAIT(push, 14);
if (ret)
return ret;
@@ -245,11 +246,25 @@ headca7d_mode(struct nv50_head *head, struct nv50_head_atom *asyh)
PUSH_MTHD(push, NVCA7D, HEAD_SET_CONTROL(i),
NVDEF(NVCA7D, HEAD_SET_CONTROL, STRUCTURE, PROGRESSIVE));
+ /* The FREQUENCY methods carry only 31 HERTZ bits; the upper bits
+ * of anything past 2.147GHz live in the HI methods
+ * (EvoSetRasterParams9()). Truncating would scan out at pclk modulo 2^31.
+ */
PUSH_MTHD(push, NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY(i),
- NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY, HERTZ, m->clock * 1000));
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY, HERTZ,
+ (u32)(hz & 0x7fffffff)));
PUSH_MTHD(push, NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_MAX(i),
- NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_MAX, HERTZ, m->clock * 1000));
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_MAX, HERTZ,
+ (u32)(hz & 0x7fffffff)));
+
+ PUSH_MTHD(push, NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI(i),
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI, HERTZ,
+ (u32)(hz >> 31)),
+
+ HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX(i),
+ NVVAL(NVCA7D, HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX, HERTZ,
+ (u32)(hz >> 31)));
return 0;
}
diff --git a/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h b/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h
index 0fec6fc21d44..5754878ddc69 100644
--- a/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h
+++ b/drivers/gpu/drm/nouveau/include/nvhw/class/clca7d.h
@@ -734,6 +734,10 @@
#define NVCA7D_HEAD_SET_CONTROL_CURSOR_COMPOSITION_BYPASS 20:20
#define NVCA7D_HEAD_SET_CONTROL_CURSOR_COMPOSITION_BYPASS_DISABLE (0x00000000)
#define NVCA7D_HEAD_SET_CONTROL_CURSOR_COMPOSITION_BYPASS_ENABLE (0x00000001)
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI(a) (0x000020C0 + (a)*0x00000800)
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_HERTZ 3:0
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX(a) (0x000020C4 + (a)*0x00000800)
+#define NVCA7D_HEAD_SET_PIXEL_CLOCK_FREQUENCY_HI_MAX_HERTZ 3:0
#define NVCA7D_HEAD_SET_SURFACE_ADDRESS_HI_CRC(a) (0x00002150 + (a)*0x00000800)
#define NVCA7D_HEAD_SET_SURFACE_ADDRESS_HI_CRC_ADDRESS_HI 31:0
#define NVCA7D_HEAD_SET_SURFACE_ADDRESS_LO_CRC(a) (0x00002154 + (a)*0x00000800)
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 10/10] drm/nouveau: honor HF-EEODB EDIDs by converting to struct drm_edid
2026-08-20 16:49 ` Mohamed Ahmed
@ 2026-08-20 16:49 ` Mohamed Ahmed
-1 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Danilo Krummrich, Maarten Lankhorst, Maxime Ripard,
Simona Vetter, Mary Guillemard, nouveau, Mohamed Ahmed
HDMI 2.1 sinks ship HF-EEODB EDIDs. Byte 126 deliberately claims one
extension block for legacy sources, and the true count lives in the CTA
block's first data block. Their high-refresh timings sit in DisplayID
extension blocks 2+.
The kernel already reads these EDIDs whole, and on GSP boards RM returns
the full EDID with its true size. However, nouveau then hands the buffer
to the legacy API which sizes the EDID from byte 126 so the DisplayID
blocks are not parsed/exposed and the sysfs blob truncates to 256 bytes.
The mode list then tops out at the CTA VICs.
Convert to the struct drm_edid API, which carries the real size.
Read via drm_edid_read_ddc()/drm_edid_read_switcheroo() where an
adapter exists, and serve RM's buffer through drm_edid_read_custom()
with a block-reading callback where not, which also routes the GSP
path through drm's block validation and the debugfs EDID override.
A failed RM read now takes the same "no EDID" path as an empty DDC read
which stops it from leaking the runtime-PM reference detect() holds. The
property, display_info, and mode list go through
drm_edid_connector_update()/_add_modes() and every EDID source is
funneled through nouveau_connector_set_edid() so all of them reach the
property that drm_edid_connector_add_modes() reads from.
nouveau_acpi_edid() now returns a drm_edid sized from the EDID's
extension count but never past the length _DDC actually returned, so
padded buffers pass and truncated ones are rejected, and every
firmware-provided EDID (OF, ACPI, VBIOS-embedded) is validated with
drm_edid_valid(). An invalid one is treated as no EDID. get_modes()
re-syncs the property from the connector's copy when the probe helper
has cleared it for a forced-off connector, before adding modes.
The few raw EDID readers are converted along with it, so no raw struct
edid pointer is kept. Ownership and freeing move to the drm_edid. MST
connectors keep their separate legacy path.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
drivers/gpu/drm/nouveau/dispnv04/dfp.c | 5 +-
drivers/gpu/drm/nouveau/dispnv50/disp.c | 4 +-
drivers/gpu/drm/nouveau/nouveau_acpi.c | 21 ++-
drivers/gpu/drm/nouveau/nouveau_acpi.h | 10 +-
drivers/gpu/drm/nouveau/nouveau_connector.c | 147 +++++++++++++++-----
drivers/gpu/drm/nouveau/nouveau_connector.h | 12 +-
6 files changed, 151 insertions(+), 48 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/dispnv04/dfp.c b/drivers/gpu/drm/nouveau/dispnv04/dfp.c
index c9f96ec8455d..00eb2aacbe93 100644
--- a/drivers/gpu/drm/nouveau/dispnv04/dfp.c
+++ b/drivers/gpu/drm/nouveau/dispnv04/dfp.c
@@ -344,9 +344,8 @@ static void nv04_dfp_mode_set(struct drm_encoder *encoder,
regp->fp_control |= (2 << 24);
if (nv_encoder->dcb->type == DCB_OUTPUT_LVDS) {
bool duallink = false, dummy;
- if (nv_connector->edid &&
- nv_connector->type == DCB_CONNECTOR_LVDS_SPWG) {
- duallink = (((u8 *)nv_connector->edid)[121] == 2);
+ if (nv_connector->spwg_links) {
+ duallink = nv_connector->spwg_links == 2;
} else {
nouveau_bios_parse_lvds_table(dev, output_mode->clock,
&duallink, &dummy);
diff --git a/drivers/gpu/drm/nouveau/dispnv50/disp.c b/drivers/gpu/drm/nouveau/dispnv50/disp.c
index 2c66e480b511..add19d479ebe 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/disp.c
+++ b/drivers/gpu/drm/nouveau/dispnv50/disp.c
@@ -1803,8 +1803,8 @@ nv50_sor_atomic_enable(struct drm_encoder *encoder, struct drm_atomic_commit *st
lvds_dual = bios->fp.dual_link;
lvds_8bpc = bios->fp.if_is_24bit;
} else {
- if (nv_connector->type == DCB_CONNECTOR_LVDS_SPWG) {
- if (((u8 *)nv_connector->edid)[121] == 2)
+ if (nv_connector->spwg_links) {
+ if (nv_connector->spwg_links == 2)
lvds_dual = true;
} else
if (mode->clock >= bios->fp.duallink_transition_clk) {
diff --git a/drivers/gpu/drm/nouveau/nouveau_acpi.c b/drivers/gpu/drm/nouveau/nouveau_acpi.c
index 21b56cc7605c..ab2868edd42b 100644
--- a/drivers/gpu/drm/nouveau/nouveau_acpi.c
+++ b/drivers/gpu/drm/nouveau/nouveau_acpi.c
@@ -360,9 +360,10 @@ void nouveau_unregister_dsm_handler(void) {}
void nouveau_switcheroo_optimus_dsm(void) {}
#endif
-void *
+const struct drm_edid *
nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector)
{
+ const struct drm_edid *drm_edid;
struct acpi_device *acpidev;
int type, ret;
void *edid;
@@ -384,7 +385,23 @@ nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector)
if (ret < 0)
return NULL;
- return edid;
+ /* Never let the EDID's own extension count reach past what _DDC
+ * actually returned. Drop the padding some firmware appends so the
+ * container is exactly the EDID, then validate it like the DDC
+ * readers would.
+ */
+ if (ret >= EDID_LENGTH)
+ ret = min_t(int, ret, EDID_LENGTH *
+ (1 + ((const struct edid *)edid)->extensions));
+ drm_edid = drm_edid_alloc(edid, ret);
+ kfree(edid);
+
+ if (drm_edid && !drm_edid_valid(drm_edid)) {
+ drm_dbg_kms(dev, "Invalid EDID from ACPI _DDC\n");
+ drm_edid_free(drm_edid);
+ drm_edid = NULL;
+ }
+ return drm_edid;
}
bool nouveau_acpi_video_backlight_use_native(void)
diff --git a/drivers/gpu/drm/nouveau/nouveau_acpi.h b/drivers/gpu/drm/nouveau/nouveau_acpi.h
index e39dd8b94b8b..bc3ccca7b338 100644
--- a/drivers/gpu/drm/nouveau/nouveau_acpi.h
+++ b/drivers/gpu/drm/nouveau/nouveau_acpi.h
@@ -4,13 +4,15 @@
#define ROM_BIOS_PAGE 4096
+struct drm_edid;
+
#if defined(CONFIG_ACPI) && defined(CONFIG_X86)
bool nouveau_is_optimus(void);
bool nouveau_is_v1_dsm(void);
void nouveau_register_dsm_handler(void);
void nouveau_unregister_dsm_handler(void);
void nouveau_switcheroo_optimus_dsm(void);
-void *nouveau_acpi_edid(struct drm_device *, struct drm_connector *);
+const struct drm_edid *nouveau_acpi_edid(struct drm_device *, struct drm_connector *);
bool nouveau_acpi_video_backlight_use_native(void);
void nouveau_acpi_video_register_backlight(void);
#else
@@ -19,7 +21,11 @@ static inline bool nouveau_is_v1_dsm(void) { return false; };
static inline void nouveau_register_dsm_handler(void) {}
static inline void nouveau_unregister_dsm_handler(void) {}
static inline void nouveau_switcheroo_optimus_dsm(void) {}
-static inline void *nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector) { return NULL; }
+static inline const struct drm_edid *
+nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector)
+{
+ return NULL;
+}
static inline bool nouveau_acpi_video_backlight_use_native(void) { return true; }
static inline void nouveau_acpi_video_register_backlight(void) {}
#endif
diff --git a/drivers/gpu/drm/nouveau/nouveau_connector.c b/drivers/gpu/drm/nouveau/nouveau_connector.c
index b0b0ad9a0c24..55912da83cd0 100644
--- a/drivers/gpu/drm/nouveau/nouveau_connector.c
+++ b/drivers/gpu/drm/nouveau/nouveau_connector.c
@@ -397,7 +397,7 @@ nouveau_connector_destroy(struct drm_connector *connector)
struct nouveau_connector *nv_connector = nouveau_connector(connector);
nvif_event_dtor(&nv_connector->irq);
nvif_event_dtor(&nv_connector->hpd);
- kfree(nv_connector->edid);
+ drm_edid_free(nv_connector->drm_edid);
drm_connector_unregister(connector);
drm_connector_cleanup(connector);
if (nv_connector->aux.transfer)
@@ -469,6 +469,36 @@ nouveau_connector_ddc_detect(struct drm_connector *connector)
return found;
}
+static void
+nouveau_connector_set_edid(struct nouveau_connector *nv_connector,
+ const struct drm_edid *drm_edid)
+{
+ if (nv_connector->drm_edid == drm_edid)
+ return;
+
+ /* Updates the EDID property and display_info with HF-EEODB-aware
+ * sizing. The legacy helpers truncate both to what EDID byte 126
+ * admits, hiding the DisplayID extension blocks that carry the
+ * high-refresh timings.
+ */
+ drm_edid_connector_update(&nv_connector->base, drm_edid);
+
+ drm_edid_free(nv_connector->drm_edid);
+ nv_connector->drm_edid = drm_edid;
+
+ /* The SPWG link-count byte lives in a vendor descriptor drm has no
+ * accessor for. Peek at it once here so nothing else needs the raw
+ * EDID.
+ */
+ nv_connector->spwg_links = 0;
+ if (nv_connector->type == DCB_CONNECTOR_LVDS_SPWG) {
+ const u8 *raw = (const u8 *)drm_edid_raw(drm_edid);
+
+ if (raw)
+ nv_connector->spwg_links = raw[121] == 2 ? 2 : 1;
+ }
+}
+
static struct nouveau_encoder *
nouveau_connector_of_detect(struct drm_connector *connector)
{
@@ -490,8 +520,17 @@ nouveau_connector_of_detect(struct drm_connector *connector)
int idx = name ? name[strlen(name) - 1] - 'A' : 0;
if (nv_encoder->dcb->i2c_index == idx && edid) {
- nv_connector->edid =
- kmemdup(edid, EDID_LENGTH, GFP_KERNEL);
+ const struct drm_edid *drm_edid =
+ drm_edid_alloc(edid, EDID_LENGTH);
+
+ /* Firmware-provided, so validate it like the DDC
+ * readers would.
+ */
+ if (drm_edid && !drm_edid_valid(drm_edid)) {
+ drm_edid_free(drm_edid);
+ drm_edid = NULL;
+ }
+ nouveau_connector_set_edid(nv_connector, drm_edid);
return nv_encoder;
}
}
@@ -546,17 +585,23 @@ nouveau_connector_set_encoder(struct drm_connector *connector,
}
}
-static void
-nouveau_connector_set_edid(struct nouveau_connector *nv_connector,
- struct edid *edid)
+struct nouveau_rm_edid {
+ u8 *data;
+ size_t size;
+};
+
+static int
+nouveau_connector_rm_edid_block(void *context, u8 *buf, unsigned int block,
+ size_t len)
{
- if (nv_connector->edid != edid) {
- struct edid *old_edid = nv_connector->edid;
+ struct nouveau_rm_edid *rm = context;
+ size_t offset = (size_t)block * EDID_LENGTH;
- drm_connector_update_edid_property(&nv_connector->base, edid);
- kfree(old_edid);
- nv_connector->edid = edid;
- }
+ if (offset + len > rm->size)
+ return -EINVAL;
+
+ memcpy(buf, rm->data + offset, len);
+ return 0;
}
static enum drm_connector_status
@@ -590,22 +635,37 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
nv_encoder = nouveau_connector_ddc_detect(connector);
if (nv_encoder) {
- struct edid *new_edid = NULL;
+ const struct drm_edid *new_edid = NULL;
if (nv_encoder->i2c) {
if ((vga_switcheroo_handler_flags() & VGA_SWITCHEROO_CAN_SWITCH_DDC) &&
nv_connector->type == DCB_CONNECTOR_LVDS)
- new_edid = drm_get_edid_switcheroo(connector, nv_encoder->i2c);
+ new_edid = drm_edid_read_switcheroo(connector, nv_encoder->i2c);
else
- new_edid = drm_get_edid(connector, nv_encoder->i2c);
+ new_edid = drm_edid_read_ddc(connector, nv_encoder->i2c);
} else {
- ret = nvif_outp_edid_get(&nv_encoder->outp, (u8 **)&new_edid);
- if (ret < 0)
- return connector_status_disconnected;
+ struct nouveau_rm_edid rm = {};
+
+ /* RM (which owns the DDC pads on GSP boards) reads the
+ * EDID whole and returns its true size, which for an
+ * HF-EEODB EDID exceeds what byte 126 admits. Serve it
+ * through drm's block reader so EEODB sizing, block
+ * validation, and the debugfs EDID override all apply.
+ * A failed read is treated like an empty DDC read,
+ * which releases the runtime-PM reference.
+ */
+ ret = nvif_outp_edid_get(&nv_encoder->outp, &rm.data);
+ if (ret >= 0) {
+ rm.size = ret;
+ new_edid = drm_edid_read_custom(connector,
+ nouveau_connector_rm_edid_block,
+ &rm);
+ kfree(rm.data);
+ }
}
nouveau_connector_set_edid(nv_connector, new_edid);
- if (!nv_connector->edid) {
+ if (!nv_connector->drm_edid) {
NV_ERROR(drm, "DDC responded, but no EDID for %s\n",
connector->name);
goto detect_analog;
@@ -626,7 +686,7 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
nv_partner->dcb->type == DCB_OUTPUT_TMDS) ||
(nv_encoder->dcb->type == DCB_OUTPUT_TMDS &&
nv_partner->dcb->type == DCB_OUTPUT_ANALOG))) {
- if (nv_connector->edid->input & DRM_EDID_INPUT_DIGITAL)
+ if (drm_edid_is_digital(nv_connector->drm_edid))
type = DCB_OUTPUT_TMDS;
else
type = DCB_OUTPUT_ANALOG;
@@ -638,7 +698,8 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
conn_status = connector_status_connected;
if (nv_encoder->dcb->type == DCB_OUTPUT_DP)
- drm_dp_cec_set_edid(&nv_connector->aux, nv_connector->edid);
+ drm_dp_cec_attach(&nv_connector->aux,
+ connector->display_info.source_physical_address);
goto out;
} else {
@@ -670,7 +731,7 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
}
out:
- if (!nv_connector->edid)
+ if (!nv_connector->drm_edid)
drm_dp_cec_unset_edid(&nv_connector->aux);
pm_runtime_mark_last_busy(dev->dev);
@@ -686,7 +747,7 @@ nouveau_connector_detect_lvds(struct drm_connector *connector, bool force)
struct nouveau_drm *drm = nouveau_drm(dev);
struct nouveau_connector *nv_connector = nouveau_connector(connector);
struct nouveau_encoder *nv_encoder = NULL;
- struct edid *edid = NULL;
+ const struct drm_edid *edid = NULL;
enum drm_connector_status status = connector_status_disconnected;
nv_encoder = find_encoder(connector, DCB_OUTPUT_LVDS);
@@ -697,7 +758,7 @@ nouveau_connector_detect_lvds(struct drm_connector *connector, bool force)
if (!drm->vbios.fp_no_ddc) {
status = nouveau_connector_detect(connector, force);
if (status == connector_status_connected) {
- edid = nv_connector->edid;
+ edid = nv_connector->drm_edid;
goto out;
}
}
@@ -733,9 +794,17 @@ nouveau_connector_detect_lvds(struct drm_connector *connector, bool force)
* stored for the panel stored in them.
*/
if (!drm->vbios.fp_no_ddc) {
- edid = (struct edid *)nouveau_bios_embedded_edid(dev);
- if (edid) {
- edid = kmemdup(edid, EDID_LENGTH, GFP_KERNEL);
+ const void *embedded = nouveau_bios_embedded_edid(dev);
+
+ if (embedded) {
+ edid = drm_edid_alloc(embedded, EDID_LENGTH);
+ /* Firmware-provided, so validate it like the DDC
+ * readers would.
+ */
+ if (edid && !drm_edid_valid(edid)) {
+ drm_edid_free(edid);
+ edid = NULL;
+ }
if (edid)
status = connector_status_connected;
}
@@ -886,7 +955,7 @@ nouveau_connector_detect_depth(struct drm_connector *connector)
bool duallink;
/* if the edid is feeling nice enough to provide this info, use it */
- if (nv_connector->edid && connector->display_info.bpc)
+ if (nv_connector->drm_edid && connector->display_info.bpc)
return;
/* EDID 1.4 is *supposed* to be supported on eDP, but, Apple... */
@@ -913,9 +982,8 @@ nouveau_connector_detect_depth(struct drm_connector *connector)
/* LVDS: DDC panel, need to first determine the number of links to
* know which if_is_24bit flag to check...
*/
- if (nv_connector->edid &&
- nv_connector->type == DCB_CONNECTOR_LVDS_SPWG)
- duallink = ((u8 *)nv_connector->edid)[121] == 2;
+ if (nv_connector->spwg_links)
+ duallink = nv_connector->spwg_links == 2;
else
duallink = mode->clock >= bios->fp.duallink_transition_clk;
@@ -973,12 +1041,17 @@ nouveau_connector_get_modes(struct drm_connector *connector)
nv_connector->native_mode = NULL;
}
- if (nv_connector->edid)
- ret = drm_add_edid_modes(connector, nv_connector->edid);
- else
- if (nv_encoder->dcb->type == DCB_OUTPUT_LVDS &&
- (nv_encoder->dcb->lvdsconf.use_straps_for_mode ||
- drm->vbios.fp_no_ddc) && nouveau_bios_fp_mode(dev, NULL)) {
+ if (nv_connector->drm_edid) {
+ /* The probe helper clears the property and display_info for
+ * a forced-off connector without calling detect(). Re-sync
+ * from our copy then, since add_modes() reads the property.
+ */
+ if (!connector->edid_blob_ptr)
+ drm_edid_connector_update(connector, nv_connector->drm_edid);
+ ret = drm_edid_connector_add_modes(connector);
+ } else if (nv_encoder->dcb->type == DCB_OUTPUT_LVDS &&
+ (nv_encoder->dcb->lvdsconf.use_straps_for_mode ||
+ drm->vbios.fp_no_ddc) && nouveau_bios_fp_mode(dev, NULL)) {
struct drm_display_mode mode;
nouveau_bios_fp_mode(dev, &mode);
diff --git a/drivers/gpu/drm/nouveau/nouveau_connector.h b/drivers/gpu/drm/nouveau/nouveau_connector.h
index 0608cabed058..eb292d2ba4bc 100644
--- a/drivers/gpu/drm/nouveau/nouveau_connector.h
+++ b/drivers/gpu/drm/nouveau/nouveau_connector.h
@@ -43,7 +43,7 @@
struct nvkm_i2c_port;
struct dcb_output;
-struct edid;
+struct drm_edid;
#ifdef CONFIG_DRM_NOUVEAU_BACKLIGHT
struct nouveau_backlight {
@@ -121,6 +121,13 @@ struct nouveau_connector {
struct drm_connector base;
enum dcb_connector_type type;
u8 index;
+ /* LVDS_SPWG panels state their link count in EDID descriptor 4 (SPWG
+ * byte 0x79), cached by nouveau_connector_set_edid() so nothing else
+ * needs the raw EDID. 0 = unknown (not an SPWG panel, or no EDID) and
+ * callers fall back to their transition-clock/VBIOS heuristics;
+ * 1 = single link; 2 = dual link.
+ */
+ u8 spwg_links;
struct nvif_conn conn;
u64 hpd_pending;
@@ -137,7 +144,8 @@ struct nouveau_connector {
int scaling_mode;
struct nouveau_encoder *detected_encoder;
- struct edid *edid;
+ /* Owner of the sink's EDID, HF-EEODB-complete. */
+ const struct drm_edid *drm_edid;
struct drm_display_mode *native_mode;
#ifdef CONFIG_DRM_NOUVEAU_BACKLIGHT
struct nouveau_backlight *backlight;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 10/10] drm/nouveau: honor HF-EEODB EDIDs by converting to struct drm_edid
@ 2026-08-20 16:49 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 16:49 UTC (permalink / raw)
To: linux-kernel
Cc: dri-devel, Lyude Paul, Danilo Krummrich, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Mary Guillemard, nouveau, Mohamed Ahmed
HDMI 2.1 sinks ship HF-EEODB EDIDs. Byte 126 deliberately claims one
extension block for legacy sources, and the true count lives in the CTA
block's first data block. Their high-refresh timings sit in DisplayID
extension blocks 2+.
The kernel already reads these EDIDs whole, and on GSP boards RM returns
the full EDID with its true size. However, nouveau then hands the buffer
to the legacy API which sizes the EDID from byte 126 so the DisplayID
blocks are not parsed/exposed and the sysfs blob truncates to 256 bytes.
The mode list then tops out at the CTA VICs.
Convert to the struct drm_edid API, which carries the real size.
Read via drm_edid_read_ddc()/drm_edid_read_switcheroo() where an
adapter exists, and serve RM's buffer through drm_edid_read_custom()
with a block-reading callback where not, which also routes the GSP
path through drm's block validation and the debugfs EDID override.
A failed RM read now takes the same "no EDID" path as an empty DDC read
which stops it from leaking the runtime-PM reference detect() holds. The
property, display_info, and mode list go through
drm_edid_connector_update()/_add_modes() and every EDID source is
funneled through nouveau_connector_set_edid() so all of them reach the
property that drm_edid_connector_add_modes() reads from.
nouveau_acpi_edid() now returns a drm_edid sized from the EDID's
extension count but never past the length _DDC actually returned, so
padded buffers pass and truncated ones are rejected, and every
firmware-provided EDID (OF, ACPI, VBIOS-embedded) is validated with
drm_edid_valid(). An invalid one is treated as no EDID. get_modes()
re-syncs the property from the connector's copy when the probe helper
has cleared it for a forced-off connector, before adding modes.
The few raw EDID readers are converted along with it, so no raw struct
edid pointer is kept. Ownership and freeing move to the drm_edid. MST
connectors keep their separate legacy path.
Signed-off-by: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
---
drivers/gpu/drm/nouveau/dispnv04/dfp.c | 5 +-
drivers/gpu/drm/nouveau/dispnv50/disp.c | 4 +-
drivers/gpu/drm/nouveau/nouveau_acpi.c | 21 ++-
drivers/gpu/drm/nouveau/nouveau_acpi.h | 10 +-
drivers/gpu/drm/nouveau/nouveau_connector.c | 147 +++++++++++++++-----
drivers/gpu/drm/nouveau/nouveau_connector.h | 12 +-
6 files changed, 151 insertions(+), 48 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/dispnv04/dfp.c b/drivers/gpu/drm/nouveau/dispnv04/dfp.c
index c9f96ec8455d..00eb2aacbe93 100644
--- a/drivers/gpu/drm/nouveau/dispnv04/dfp.c
+++ b/drivers/gpu/drm/nouveau/dispnv04/dfp.c
@@ -344,9 +344,8 @@ static void nv04_dfp_mode_set(struct drm_encoder *encoder,
regp->fp_control |= (2 << 24);
if (nv_encoder->dcb->type == DCB_OUTPUT_LVDS) {
bool duallink = false, dummy;
- if (nv_connector->edid &&
- nv_connector->type == DCB_CONNECTOR_LVDS_SPWG) {
- duallink = (((u8 *)nv_connector->edid)[121] == 2);
+ if (nv_connector->spwg_links) {
+ duallink = nv_connector->spwg_links == 2;
} else {
nouveau_bios_parse_lvds_table(dev, output_mode->clock,
&duallink, &dummy);
diff --git a/drivers/gpu/drm/nouveau/dispnv50/disp.c b/drivers/gpu/drm/nouveau/dispnv50/disp.c
index 2c66e480b511..add19d479ebe 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/disp.c
+++ b/drivers/gpu/drm/nouveau/dispnv50/disp.c
@@ -1803,8 +1803,8 @@ nv50_sor_atomic_enable(struct drm_encoder *encoder, struct drm_atomic_commit *st
lvds_dual = bios->fp.dual_link;
lvds_8bpc = bios->fp.if_is_24bit;
} else {
- if (nv_connector->type == DCB_CONNECTOR_LVDS_SPWG) {
- if (((u8 *)nv_connector->edid)[121] == 2)
+ if (nv_connector->spwg_links) {
+ if (nv_connector->spwg_links == 2)
lvds_dual = true;
} else
if (mode->clock >= bios->fp.duallink_transition_clk) {
diff --git a/drivers/gpu/drm/nouveau/nouveau_acpi.c b/drivers/gpu/drm/nouveau/nouveau_acpi.c
index 21b56cc7605c..ab2868edd42b 100644
--- a/drivers/gpu/drm/nouveau/nouveau_acpi.c
+++ b/drivers/gpu/drm/nouveau/nouveau_acpi.c
@@ -360,9 +360,10 @@ void nouveau_unregister_dsm_handler(void) {}
void nouveau_switcheroo_optimus_dsm(void) {}
#endif
-void *
+const struct drm_edid *
nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector)
{
+ const struct drm_edid *drm_edid;
struct acpi_device *acpidev;
int type, ret;
void *edid;
@@ -384,7 +385,23 @@ nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector)
if (ret < 0)
return NULL;
- return edid;
+ /* Never let the EDID's own extension count reach past what _DDC
+ * actually returned. Drop the padding some firmware appends so the
+ * container is exactly the EDID, then validate it like the DDC
+ * readers would.
+ */
+ if (ret >= EDID_LENGTH)
+ ret = min_t(int, ret, EDID_LENGTH *
+ (1 + ((const struct edid *)edid)->extensions));
+ drm_edid = drm_edid_alloc(edid, ret);
+ kfree(edid);
+
+ if (drm_edid && !drm_edid_valid(drm_edid)) {
+ drm_dbg_kms(dev, "Invalid EDID from ACPI _DDC\n");
+ drm_edid_free(drm_edid);
+ drm_edid = NULL;
+ }
+ return drm_edid;
}
bool nouveau_acpi_video_backlight_use_native(void)
diff --git a/drivers/gpu/drm/nouveau/nouveau_acpi.h b/drivers/gpu/drm/nouveau/nouveau_acpi.h
index e39dd8b94b8b..bc3ccca7b338 100644
--- a/drivers/gpu/drm/nouveau/nouveau_acpi.h
+++ b/drivers/gpu/drm/nouveau/nouveau_acpi.h
@@ -4,13 +4,15 @@
#define ROM_BIOS_PAGE 4096
+struct drm_edid;
+
#if defined(CONFIG_ACPI) && defined(CONFIG_X86)
bool nouveau_is_optimus(void);
bool nouveau_is_v1_dsm(void);
void nouveau_register_dsm_handler(void);
void nouveau_unregister_dsm_handler(void);
void nouveau_switcheroo_optimus_dsm(void);
-void *nouveau_acpi_edid(struct drm_device *, struct drm_connector *);
+const struct drm_edid *nouveau_acpi_edid(struct drm_device *, struct drm_connector *);
bool nouveau_acpi_video_backlight_use_native(void);
void nouveau_acpi_video_register_backlight(void);
#else
@@ -19,7 +21,11 @@ static inline bool nouveau_is_v1_dsm(void) { return false; };
static inline void nouveau_register_dsm_handler(void) {}
static inline void nouveau_unregister_dsm_handler(void) {}
static inline void nouveau_switcheroo_optimus_dsm(void) {}
-static inline void *nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector) { return NULL; }
+static inline const struct drm_edid *
+nouveau_acpi_edid(struct drm_device *dev, struct drm_connector *connector)
+{
+ return NULL;
+}
static inline bool nouveau_acpi_video_backlight_use_native(void) { return true; }
static inline void nouveau_acpi_video_register_backlight(void) {}
#endif
diff --git a/drivers/gpu/drm/nouveau/nouveau_connector.c b/drivers/gpu/drm/nouveau/nouveau_connector.c
index b0b0ad9a0c24..55912da83cd0 100644
--- a/drivers/gpu/drm/nouveau/nouveau_connector.c
+++ b/drivers/gpu/drm/nouveau/nouveau_connector.c
@@ -397,7 +397,7 @@ nouveau_connector_destroy(struct drm_connector *connector)
struct nouveau_connector *nv_connector = nouveau_connector(connector);
nvif_event_dtor(&nv_connector->irq);
nvif_event_dtor(&nv_connector->hpd);
- kfree(nv_connector->edid);
+ drm_edid_free(nv_connector->drm_edid);
drm_connector_unregister(connector);
drm_connector_cleanup(connector);
if (nv_connector->aux.transfer)
@@ -469,6 +469,36 @@ nouveau_connector_ddc_detect(struct drm_connector *connector)
return found;
}
+static void
+nouveau_connector_set_edid(struct nouveau_connector *nv_connector,
+ const struct drm_edid *drm_edid)
+{
+ if (nv_connector->drm_edid == drm_edid)
+ return;
+
+ /* Updates the EDID property and display_info with HF-EEODB-aware
+ * sizing. The legacy helpers truncate both to what EDID byte 126
+ * admits, hiding the DisplayID extension blocks that carry the
+ * high-refresh timings.
+ */
+ drm_edid_connector_update(&nv_connector->base, drm_edid);
+
+ drm_edid_free(nv_connector->drm_edid);
+ nv_connector->drm_edid = drm_edid;
+
+ /* The SPWG link-count byte lives in a vendor descriptor drm has no
+ * accessor for. Peek at it once here so nothing else needs the raw
+ * EDID.
+ */
+ nv_connector->spwg_links = 0;
+ if (nv_connector->type == DCB_CONNECTOR_LVDS_SPWG) {
+ const u8 *raw = (const u8 *)drm_edid_raw(drm_edid);
+
+ if (raw)
+ nv_connector->spwg_links = raw[121] == 2 ? 2 : 1;
+ }
+}
+
static struct nouveau_encoder *
nouveau_connector_of_detect(struct drm_connector *connector)
{
@@ -490,8 +520,17 @@ nouveau_connector_of_detect(struct drm_connector *connector)
int idx = name ? name[strlen(name) - 1] - 'A' : 0;
if (nv_encoder->dcb->i2c_index == idx && edid) {
- nv_connector->edid =
- kmemdup(edid, EDID_LENGTH, GFP_KERNEL);
+ const struct drm_edid *drm_edid =
+ drm_edid_alloc(edid, EDID_LENGTH);
+
+ /* Firmware-provided, so validate it like the DDC
+ * readers would.
+ */
+ if (drm_edid && !drm_edid_valid(drm_edid)) {
+ drm_edid_free(drm_edid);
+ drm_edid = NULL;
+ }
+ nouveau_connector_set_edid(nv_connector, drm_edid);
return nv_encoder;
}
}
@@ -546,17 +585,23 @@ nouveau_connector_set_encoder(struct drm_connector *connector,
}
}
-static void
-nouveau_connector_set_edid(struct nouveau_connector *nv_connector,
- struct edid *edid)
+struct nouveau_rm_edid {
+ u8 *data;
+ size_t size;
+};
+
+static int
+nouveau_connector_rm_edid_block(void *context, u8 *buf, unsigned int block,
+ size_t len)
{
- if (nv_connector->edid != edid) {
- struct edid *old_edid = nv_connector->edid;
+ struct nouveau_rm_edid *rm = context;
+ size_t offset = (size_t)block * EDID_LENGTH;
- drm_connector_update_edid_property(&nv_connector->base, edid);
- kfree(old_edid);
- nv_connector->edid = edid;
- }
+ if (offset + len > rm->size)
+ return -EINVAL;
+
+ memcpy(buf, rm->data + offset, len);
+ return 0;
}
static enum drm_connector_status
@@ -590,22 +635,37 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
nv_encoder = nouveau_connector_ddc_detect(connector);
if (nv_encoder) {
- struct edid *new_edid = NULL;
+ const struct drm_edid *new_edid = NULL;
if (nv_encoder->i2c) {
if ((vga_switcheroo_handler_flags() & VGA_SWITCHEROO_CAN_SWITCH_DDC) &&
nv_connector->type == DCB_CONNECTOR_LVDS)
- new_edid = drm_get_edid_switcheroo(connector, nv_encoder->i2c);
+ new_edid = drm_edid_read_switcheroo(connector, nv_encoder->i2c);
else
- new_edid = drm_get_edid(connector, nv_encoder->i2c);
+ new_edid = drm_edid_read_ddc(connector, nv_encoder->i2c);
} else {
- ret = nvif_outp_edid_get(&nv_encoder->outp, (u8 **)&new_edid);
- if (ret < 0)
- return connector_status_disconnected;
+ struct nouveau_rm_edid rm = {};
+
+ /* RM (which owns the DDC pads on GSP boards) reads the
+ * EDID whole and returns its true size, which for an
+ * HF-EEODB EDID exceeds what byte 126 admits. Serve it
+ * through drm's block reader so EEODB sizing, block
+ * validation, and the debugfs EDID override all apply.
+ * A failed read is treated like an empty DDC read,
+ * which releases the runtime-PM reference.
+ */
+ ret = nvif_outp_edid_get(&nv_encoder->outp, &rm.data);
+ if (ret >= 0) {
+ rm.size = ret;
+ new_edid = drm_edid_read_custom(connector,
+ nouveau_connector_rm_edid_block,
+ &rm);
+ kfree(rm.data);
+ }
}
nouveau_connector_set_edid(nv_connector, new_edid);
- if (!nv_connector->edid) {
+ if (!nv_connector->drm_edid) {
NV_ERROR(drm, "DDC responded, but no EDID for %s\n",
connector->name);
goto detect_analog;
@@ -626,7 +686,7 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
nv_partner->dcb->type == DCB_OUTPUT_TMDS) ||
(nv_encoder->dcb->type == DCB_OUTPUT_TMDS &&
nv_partner->dcb->type == DCB_OUTPUT_ANALOG))) {
- if (nv_connector->edid->input & DRM_EDID_INPUT_DIGITAL)
+ if (drm_edid_is_digital(nv_connector->drm_edid))
type = DCB_OUTPUT_TMDS;
else
type = DCB_OUTPUT_ANALOG;
@@ -638,7 +698,8 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
conn_status = connector_status_connected;
if (nv_encoder->dcb->type == DCB_OUTPUT_DP)
- drm_dp_cec_set_edid(&nv_connector->aux, nv_connector->edid);
+ drm_dp_cec_attach(&nv_connector->aux,
+ connector->display_info.source_physical_address);
goto out;
} else {
@@ -670,7 +731,7 @@ nouveau_connector_detect(struct drm_connector *connector, bool force)
}
out:
- if (!nv_connector->edid)
+ if (!nv_connector->drm_edid)
drm_dp_cec_unset_edid(&nv_connector->aux);
pm_runtime_mark_last_busy(dev->dev);
@@ -686,7 +747,7 @@ nouveau_connector_detect_lvds(struct drm_connector *connector, bool force)
struct nouveau_drm *drm = nouveau_drm(dev);
struct nouveau_connector *nv_connector = nouveau_connector(connector);
struct nouveau_encoder *nv_encoder = NULL;
- struct edid *edid = NULL;
+ const struct drm_edid *edid = NULL;
enum drm_connector_status status = connector_status_disconnected;
nv_encoder = find_encoder(connector, DCB_OUTPUT_LVDS);
@@ -697,7 +758,7 @@ nouveau_connector_detect_lvds(struct drm_connector *connector, bool force)
if (!drm->vbios.fp_no_ddc) {
status = nouveau_connector_detect(connector, force);
if (status == connector_status_connected) {
- edid = nv_connector->edid;
+ edid = nv_connector->drm_edid;
goto out;
}
}
@@ -733,9 +794,17 @@ nouveau_connector_detect_lvds(struct drm_connector *connector, bool force)
* stored for the panel stored in them.
*/
if (!drm->vbios.fp_no_ddc) {
- edid = (struct edid *)nouveau_bios_embedded_edid(dev);
- if (edid) {
- edid = kmemdup(edid, EDID_LENGTH, GFP_KERNEL);
+ const void *embedded = nouveau_bios_embedded_edid(dev);
+
+ if (embedded) {
+ edid = drm_edid_alloc(embedded, EDID_LENGTH);
+ /* Firmware-provided, so validate it like the DDC
+ * readers would.
+ */
+ if (edid && !drm_edid_valid(edid)) {
+ drm_edid_free(edid);
+ edid = NULL;
+ }
if (edid)
status = connector_status_connected;
}
@@ -886,7 +955,7 @@ nouveau_connector_detect_depth(struct drm_connector *connector)
bool duallink;
/* if the edid is feeling nice enough to provide this info, use it */
- if (nv_connector->edid && connector->display_info.bpc)
+ if (nv_connector->drm_edid && connector->display_info.bpc)
return;
/* EDID 1.4 is *supposed* to be supported on eDP, but, Apple... */
@@ -913,9 +982,8 @@ nouveau_connector_detect_depth(struct drm_connector *connector)
/* LVDS: DDC panel, need to first determine the number of links to
* know which if_is_24bit flag to check...
*/
- if (nv_connector->edid &&
- nv_connector->type == DCB_CONNECTOR_LVDS_SPWG)
- duallink = ((u8 *)nv_connector->edid)[121] == 2;
+ if (nv_connector->spwg_links)
+ duallink = nv_connector->spwg_links == 2;
else
duallink = mode->clock >= bios->fp.duallink_transition_clk;
@@ -973,12 +1041,17 @@ nouveau_connector_get_modes(struct drm_connector *connector)
nv_connector->native_mode = NULL;
}
- if (nv_connector->edid)
- ret = drm_add_edid_modes(connector, nv_connector->edid);
- else
- if (nv_encoder->dcb->type == DCB_OUTPUT_LVDS &&
- (nv_encoder->dcb->lvdsconf.use_straps_for_mode ||
- drm->vbios.fp_no_ddc) && nouveau_bios_fp_mode(dev, NULL)) {
+ if (nv_connector->drm_edid) {
+ /* The probe helper clears the property and display_info for
+ * a forced-off connector without calling detect(). Re-sync
+ * from our copy then, since add_modes() reads the property.
+ */
+ if (!connector->edid_blob_ptr)
+ drm_edid_connector_update(connector, nv_connector->drm_edid);
+ ret = drm_edid_connector_add_modes(connector);
+ } else if (nv_encoder->dcb->type == DCB_OUTPUT_LVDS &&
+ (nv_encoder->dcb->lvdsconf.use_straps_for_mode ||
+ drm->vbios.fp_no_ddc) && nouveau_bios_fp_mode(dev, NULL)) {
struct drm_display_mode mode;
nouveau_bios_fp_mode(dev, &mode);
diff --git a/drivers/gpu/drm/nouveau/nouveau_connector.h b/drivers/gpu/drm/nouveau/nouveau_connector.h
index 0608cabed058..eb292d2ba4bc 100644
--- a/drivers/gpu/drm/nouveau/nouveau_connector.h
+++ b/drivers/gpu/drm/nouveau/nouveau_connector.h
@@ -43,7 +43,7 @@
struct nvkm_i2c_port;
struct dcb_output;
-struct edid;
+struct drm_edid;
#ifdef CONFIG_DRM_NOUVEAU_BACKLIGHT
struct nouveau_backlight {
@@ -121,6 +121,13 @@ struct nouveau_connector {
struct drm_connector base;
enum dcb_connector_type type;
u8 index;
+ /* LVDS_SPWG panels state their link count in EDID descriptor 4 (SPWG
+ * byte 0x79), cached by nouveau_connector_set_edid() so nothing else
+ * needs the raw EDID. 0 = unknown (not an SPWG panel, or no EDID) and
+ * callers fall back to their transition-clock/VBIOS heuristics;
+ * 1 = single link; 2 = dual link.
+ */
+ u8 spwg_links;
struct nvif_conn conn;
u64 hpd_pending;
@@ -137,7 +144,8 @@ struct nouveau_connector {
int scaling_mode;
struct nouveau_encoder *detected_encoder;
- struct edid *edid;
+ /* Owner of the sink's EDID, HF-EEODB-complete. */
+ const struct drm_edid *drm_edid;
struct drm_display_mode *native_mode;
#ifdef CONFIG_DRM_NOUVEAU_BACKLIGHT
struct nouveau_backlight *backlight;
--
2.55.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* Re: [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks
2026-08-20 16:49 ` Mohamed Ahmed
(?)
@ 2026-08-20 17:01 ` sashiko-bot
2026-08-20 17:03 ` Mohamed Ahmed
-1 siblings, 1 reply; 24+ messages in thread
From: sashiko-bot @ 2026-08-20 17:01 UTC (permalink / raw)
To: Mohamed Ahmed; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [Critical] Pre-existing Use-After-Free and Double Free in `r535_disp_new` error path when `nvkm_disp_new_` fails after subdev registration.
- [High] Pre-existing NULL pointer dereference in the error path of `r535_disp_new` due to unconditional lock initialization on a potentially NULL pointer.
--
--- Patch [3]: [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks ---
commit eaafa1eb740ad36427480951cde140701ddc6d3a
Author: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks
The GSP-RM display code in rm/r535/disp.c borrows a few
register-programming routines from engine/disp (the head-timing
interrupt handler, vblank enables, armed head state and scanout position
readback, the AVI/VSI infoframe writers and the GCP AVMute write) and so
far picked them by name, which means it has to know which chip it runs
on the moment a generation changes any of them.
> diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
> index cd4451e625122..f3e55253bcbca 100644
> --- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
> +++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
[ ... ]
> @@ -1688,6 +1729,7 @@ r535_disp_new(const struct nvkm_disp_func *hw, struct nvkm_device *device,
> rm->uevent = hw->uevent;
> rm->sor.cnt = r535_sor_cnt;
> rm->sor.new = r535_sor_new;
> + rm->gsp = hw->gsp;
> rm->ramht_size = hw->ramht_size;
>
> rm->root.oclass = gpu->disp.class.root;
[Severity: Critical]
This is a pre-existing issue, but can the error path in r535_disp_new() lead
to a use-after-free and double-free?
If nvkm_disp_new_() fails after subdev registration, the subdevice is already
attached to the device's subdev list. r535_disp_new() handles the error by
freeing the dynamically allocated rm struct:
ret = nvkm_disp_new_(rm, device, type, inst, pdisp);
if (ret)
kfree(rm);
However, disp->func still points to this freed memory. Later, during device
teardown on the error path, nvkm_subdev_del() invokes nvkm_disp_dtor() which
evaluates the pointer:
if (disp->func && disp->func->dtor)
disp->func->dtor(disp);
This dereferences the freed rm struct and calls r535_disp_dtor(), which
subsequently executes kfree(disp->func), causing a double-free.
[Severity: High]
This is another pre-existing issue, but can the same error path cause a NULL
pointer dereference?
If nvkm_disp_new_() fails early before allocating the struct, *pdisp is set
to NULL. r535_disp_new() then unconditionally attempts to initialize a mutex
on it:
ret = nvkm_disp_new_(rm, device, type, inst, pdisp);
if (ret)
kfree(rm);
mutex_init(&(*pdisp)->super.mutex);
return ret;
Passing an offset from NULL to mutex_init() will cause a panic when it tries
to write to the mutex fields.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260820164929.17117-1-mohamedahmedegypt2001@gmail.com?part=3
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks
2026-08-20 17:01 ` sashiko-bot
@ 2026-08-20 17:03 ` Mohamed Ahmed
0 siblings, 0 replies; 24+ messages in thread
From: Mohamed Ahmed @ 2026-08-20 17:03 UTC (permalink / raw)
To: sashiko-reviews; +Cc: dri-devel
On Thu, Aug 20, 2026 at 9:01 PM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
>
> Pre-existing issues:
> - [Critical] Pre-existing Use-After-Free and Double Free in `r535_disp_new` error path when `nvkm_disp_new_` fails after subdev registration.
> - [High] Pre-existing NULL pointer dereference in the error path of `r535_disp_new` due to unconditional lock initialization on a potentially NULL pointer.
> --
I saw those and can confirm they are very much real. However, fixing
them is out of scope here (I was actually going to roll them in at
first), because there are 5 (that I have seen, might be more)
r535_disp_ functions that have the exact same issues.
>
> --- Patch [3]: [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks ---
> commit eaafa1eb740ad36427480951cde140701ddc6d3a
> Author: Mohamed Ahmed <mohamedahmedegypt2001@gmail.com>
>
> drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks
>
> The GSP-RM display code in rm/r535/disp.c borrows a few
> register-programming routines from engine/disp (the head-timing
> interrupt handler, vblank enables, armed head state and scanout position
> readback, the AVI/VSI infoframe writers and the GCP AVMute write) and so
> far picked them by name, which means it has to know which chip it runs
> on the moment a generation changes any of them.
>
> > diff --git a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
> > index cd4451e625122..f3e55253bcbca 100644
> > --- a/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
> > +++ b/drivers/gpu/drm/nouveau/nvkm/subdev/gsp/rm/r535/disp.c
> [ ... ]
> > @@ -1688,6 +1729,7 @@ r535_disp_new(const struct nvkm_disp_func *hw, struct nvkm_device *device,
> > rm->uevent = hw->uevent;
> > rm->sor.cnt = r535_sor_cnt;
> > rm->sor.new = r535_sor_new;
> > + rm->gsp = hw->gsp;
> > rm->ramht_size = hw->ramht_size;
> >
> > rm->root.oclass = gpu->disp.class.root;
>
> [Severity: Critical]
> This is a pre-existing issue, but can the error path in r535_disp_new() lead
> to a use-after-free and double-free?
>
> If nvkm_disp_new_() fails after subdev registration, the subdevice is already
> attached to the device's subdev list. r535_disp_new() handles the error by
> freeing the dynamically allocated rm struct:
>
> ret = nvkm_disp_new_(rm, device, type, inst, pdisp);
> if (ret)
> kfree(rm);
>
> However, disp->func still points to this freed memory. Later, during device
> teardown on the error path, nvkm_subdev_del() invokes nvkm_disp_dtor() which
> evaluates the pointer:
>
> if (disp->func && disp->func->dtor)
> disp->func->dtor(disp);
>
> This dereferences the freed rm struct and calls r535_disp_dtor(), which
> subsequently executes kfree(disp->func), causing a double-free.
>
> [Severity: High]
> This is another pre-existing issue, but can the same error path cause a NULL
> pointer dereference?
>
> If nvkm_disp_new_() fails early before allocating the struct, *pdisp is set
> to NULL. r535_disp_new() then unconditionally attempts to initialize a mutex
> on it:
>
> ret = nvkm_disp_new_(rm, device, type, inst, pdisp);
> if (ret)
> kfree(rm);
>
> mutex_init(&(*pdisp)->super.mutex);
> return ret;
>
> Passing an offset from NULL to mutex_init() will cause a panic when it tries
> to write to the mutex fields.
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260820164929.17117-1-mohamedahmedegypt2001@gmail.com?part=3
^ permalink raw reply [flat|nested] 24+ messages in thread
end of thread, other threads:[~2026-08-20 17:04 UTC | newest]
Thread overview: 24+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-20 16:49 [PATCH v2 00/10] nouveau: assorted display fixes (GB20x, r570 DP_CONFIG_STREAM, HF-EEODB EDIDs) Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 01/10] drm/nouveau/disp: move GSP head-timing ISR and vblank helpers to tu102.c Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 02/10] drm/nouveau/disp: move the GSP HDMI GCP AVMute write to engine/disp Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 03/10] drm/nouveau/disp: route GSP-RM display MMIO through nvkm_disp_func hooks Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 17:01 ` sashiko-bot
2026-08-20 17:03 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 04/10] drm/nouveau/disp: fix HDMI vendor infoframes on GB20x Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 05/10] drm/nouveau/disp: fix HDMI GCP AVMute register offsets " Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 06/10] drm/nouveau/gsp: use per-version DP_CONFIG_STREAM params on r570 firmware Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 07/10] drm/nouveau/disp: fix head state readback on GB20x Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 08/10] drm/nouveau/gsp: fix vblank interrupts " Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 09/10] drm/nouveau/dispnv50: program pixel clocks above 2.147GHz " Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
2026-08-20 16:49 ` [PATCH v2 10/10] drm/nouveau: honor HF-EEODB EDIDs by converting to struct drm_edid Mohamed Ahmed
2026-08-20 16:49 ` Mohamed Ahmed
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.