From c297d7766616a8331fd0060ff012d0470d63fd20 Mon Sep 17 00:00:00 2001 From: Jeppe Bylov Date: Wed, 11 Nov 2026 16:10:59 +0100 Subject: [PATCH 1/4] drm/apple: dptxep: optionally offer a peer pipe in SetTiledDisplayHints One T6030 external pipe is limited to 1274019840 active px/s, so the firmware drops modes such as 5120x2160@120/180 ("IOMFB removing mode ... max 1274019840"). macOS 14 drives them on two pipes. A hypervisor trace of macOS 14.8.9 on a J514S shows it answering DPTX apcall 21 (SET_TILED_DISPLAY_HINTS) with a pipe allocation at payload +0x64..+0x77: 1, 1, 0x1800, 0x55b62380, 0x4bf00000. Linux echoes zeros there, so the firmware never counts a peer pipe. Add split_alloc (default off, 0644, takes effect on the next hotplug). When set and the hint carries a display size for a single-stream sink, fill the block like macOS. The firmware then takes its VFTGMerge path and keeps the 5K timings (IDs 78/79 on a Samsung Odyssey G75F). Mode setting such a timing needs split_modeset (next commit). Link: https://github.com/omacom/linux-aurora/issues/434 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w --- drivers/gpu/drm/apple/dptxep.c | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 741f40bc8c9ed5..b3fb777576fb2e 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -99,6 +99,21 @@ struct dptxport_apcall_set_tiled { #define DPTX_TILE_HINT_HEIGHT 0x3c #define DPTX_TILE_HINT_MIN_SIZE 0x40 +/* + * Experimental (split_alloc): macOS 14 on J514S answers this call + * with a pipe allocation at +0x64..+0x77 (seen in a hypervisor trace: + * 1, 1, 6144, 1438000000, 1274019840). The DCP then offers 5K@120/180 as + * split timings (ID + 10000). Off by default; can be flipped at runtime and + * takes effect on the next hotplug. + */ +static bool split_alloc; +module_param(split_alloc, bool, 0644); +MODULE_PARM_DESC(split_alloc, + "EXPERIMENT: offer a dcpext peer pipe in SetTiledDisplayHints replies like macOS 14"); + +#define DPTX_SPLIT_ALLOC_OFF 0x64 +#define DPTX_SPLIT_ALLOC_END 0x78 + /* * A Thunderbolt DP tunnel uses the same CORE|ATC|DIE|CONNECTED target as a * direct alt-mode PHY: CORE is the DFP port (0 = dpphy, 1/2 = dpin0/dpin1) @@ -661,6 +676,30 @@ static int dptxport_call_set_tiled_display_hint(struct apple_epic_service *servi } reply->retcode = cpu_to_le32(1); + + /* Only for a single-stream sink that reports its size (not a 2-tile LG). */ + if (READ_ONCE(split_alloc) && data_size >= DPTX_SPLIT_ALLOC_END && + reply_size >= DPTX_SPLIT_ALLOC_END && + get_unaligned_le32(hint + DPTX_TILE_HINT_TILES_H) <= 1 && + get_unaligned_le32(hint + DPTX_TILE_HINT_WIDTH) && + get_unaligned_le32(hint + DPTX_TILE_HINT_HEIGHT)) { + u8 *r = reply_; + + put_unaligned_le32(1, r + DPTX_SPLIT_ALLOC_OFF + 0x00); + put_unaligned_le32(1, r + DPTX_SPLIT_ALLOC_OFF + 0x04); + put_unaligned_le32(0x1800, r + DPTX_SPLIT_ALLOC_OFF + 0x08); + put_unaligned_le32(0x55b62380, r + DPTX_SPLIT_ALLOC_OFF + 0x0c); + put_unaligned_le32(0x4bf00000, r + DPTX_SPLIT_ALLOC_OFF + 0x10); + dev_info(service->ep->dcp->dev, + "DPTXPort: EXPERIMENT split_alloc: offered a peer pipe for %ux%u (data %zu, reply %zu)\n", + get_unaligned_le32(hint + DPTX_TILE_HINT_WIDTH), + get_unaligned_le32(hint + DPTX_TILE_HINT_HEIGHT), + data_size, reply_size); + } else if (READ_ONCE(split_alloc)) { + dev_info(service->ep->dcp->dev, + "DPTXPort: EXPERIMENT split_alloc: hint not used (data %zu, reply %zu)\n", + data_size, reply_size); + } return 0; } From cfa08413c586074dbcf8c258be8cc69cda8f67a4 Mon Sep 17 00:00:00 2001 From: Jeppe Bylov Date: Wed, 11 Nov 2026 16:10:59 +0100 Subject: [PATCH 2/4] drm/apple: v14: drive modes above one pipe's rate on two external pipes Add split_modeset (default off, 0644). When a selected mode's active pixel rate exceeds one pipe's (1274019840 px/s), the external mode set runs the host side macOS 14 uses, from a hypervisor trace on a J514S: - MAIN is the pipe that owns the link, HELPER the other T6030 dcpext with no display of its own (started on demand if it is still idle). - A409(2), A361 (full timing), A472(0) both, A359(0) both, A363(1) HELPER, then A362 on both with the half timing (the eight horizontal u32 and the pixel clock halved, IDs + 10000), A472(1) both, A357 both (+A358 on HELPER when the swap counters differ). - MAIN's A411(colour + 10000, timing + 10000) does not return until HELPER's has finished: after MAIN stores tiling para 10 = 2, the next D115 poll relays MAIN's DSC setup (A366, A364, A367, A369) to HELPER (A365, A368, A370) and a worker runs HELPER's A411. - D115/D116 become a tiling-state getter/setter backed by a table per pipe (event 1 own entry, event 2 the peer's): dual pipe on, pipe index, slice origin, VFTG merge, primary. - Frames: A406 MAIN, A406 HELPER with MAIN's swap ID, A407 MAIN, A407 HELPER, both records for the whole framebuffer at the same IOVA (the display subsystem's domain spans both display DARTs), HELPER's source rect starting at x = hdisplay / 2. MAIN's completion is consumed too. - Leaving the split powers both pipes down together, MAIN first and HELPER on a worker once MAIN's A472(0) is on the link, as macOS does; powering them down one after the other kills MAIN ("power_down_M3: auto_mode_change failed with 0x8000000f"). - A mode that needs two pipes is refused while split_modeset is off, instead of being set on one pipe and leaving the display dark. The check uses active pixels x whole-Hz refresh from the pixel clock, as the firmware's filter does, so no mode the firmware offers for one pipe is refused. - Only the hybrid (HDMI) pipeline may be MAIN; anything else is refused before either pipe is touched. - Framebuffers stay pinned on every pipe that took a swap; MAIN's previous one is released only after MAIN's completion was seen. HELPER's is released only when HELPER is known to be off. - If a pipe dies while the split is live, the split is switched off, the mode is marked not live and the connector is re-probed. The link gets long_calls (20 s) for the A411s and A472(0)s that wait for the peer, and dcp_v14_link_wait_swap() for MAIN's completion. The merge only works with ADT dcpext0 as MAIN, as on macOS. On a J514S the Mac's HDMI port uses dcpext0; a Thunderbolt display needs split_dock_main (next commit). Tested on a J514S with a Samsung Odyssey G75F: 5120x2160@120 and @179.985 over HDMI and over an Anker TB5 dock (DisplayPort), from boot, 120 <-> 180 Hz switches while running, DPMS off/on, link retrains; 200+ fps with ~5 ms frame time on the compositor overlay. Link: https://github.com/omacom/linux-aurora/issues/434 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w --- drivers/gpu/drm/apple/iomfb_v14_7.c | 826 ++++++++++++++++++++++- drivers/gpu/drm/apple/iomfb_v14_7.h | 1 + drivers/gpu/drm/apple/iomfb_v14_7_link.c | 41 +- drivers/gpu/drm/apple/iomfb_v14_7_link.h | 5 + 4 files changed, 870 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 7a6c96dfa1f74f..5fe3b1a5e7594f 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT #include +#include #include #include #include @@ -373,8 +374,57 @@ struct apple_dcp_v14 { enum dcp_v14_bl_gate bl_gate; u32 nits_sent; u64 nits_presents, brightness_presents, nits_refused; + + /* + * Experimental (split_modeset): one display driven by two external + * pipes, as macOS 14 does for modes above one pipe's pixel rate. + * MAIN owns the link and the connector; HELPER drives the right half. + * split_peer is set on both while the split is live (under MAIN's lock, + * then HELPER's). + */ + enum { DCP_V14_SPLIT_NONE, DCP_V14_SPLIT_MAIN, DCP_V14_SPLIT_HELPER } split_role; + struct apple_dcp_v14 *split_peer; + struct device *split_peer_dev; /* reference held by MAIN */ + u32 split_x; /* this pipe's slice origin in the framebuffer */ + /* D116 tiling state as stored by this pipe's firmware (para 0..15). */ + u32 tiling_state[16]; + /* MAIN, during its split A411: the relay to HELPER and HELPER's A411. */ + struct work_struct split_work; + struct completion split_done; + int split_helper_ret; + bool split_relay_queued; + bool split_relay_pending; /* MAIN stored para 10 = 2 */ + u32 split_color, split_timing; /* the +10000 IDs */ + u8 split_dsc_caps[0x20]; /* A364 reply -> A365 */ + u8 split_dsc_bpp[8]; /* A367 reply -> A368 (first 4 bytes) */ + u8 split_dsc_slice[12]; /* A369 reply -> A370 */ + u32 split_w; /* slice width (hdisplay / 2) */ + /* MAIN: framebuffers MAIN may still read (completion not seen yet). */ + struct drm_framebuffer *split_stale[4]; + unsigned int split_nstale; + /* MAIN, while leaving the split: HELPER's power-down runs alongside MAIN's. */ + struct work_struct split_off_work; + struct completion split_off_done; + int split_off_ret; + bool split_off_after_main; /* HELPER waits until MAIN's A472(0) is pending */ }; +/* split_modeset: the tiling tables of both pipes are read by either callback path. */ +static DEFINE_SPINLOCK(dcp_v14_split_table_lock); + +static bool split_modeset; +module_param(split_modeset, bool, 0644); +MODULE_PARM_DESC(split_modeset, + "EXPERIMENT: drive modes above one pipe's pixel rate on two T6030 dcpext pipes (needs dptxep split_alloc)"); + +static bool split_flip_index; +module_param(split_flip_index, bool, 0644); +MODULE_PARM_DESC(split_flip_index, + "EXPERIMENT: answer D115 para 4 (pipe index) the other way round (roles are reversed vs macOS on J514S)"); + +/* One pipe's active pixel rate (the 0x4bf00000 per-pipe default in the firmware). */ +#define DCP_V14_SPLIT_PIPE_RATE 1274019840ULL + /* * The panel's DCP session; one per boot, RTKit is never started twice. The * manual external start waits for it, as the PMP runs once the panel's @@ -1155,6 +1205,99 @@ static int dcp_v14_external_callback(struct apple_dcp_v14 *v14, u32 tag, const u } /* Runs on the thread that owns the RPC stream; the lock is held. */ +/* + * split_modeset: MAIN's firmware stored "mode set done, waiting for the + * peer" (D116 para 10 = 2) inside MAIN's A411. Read the DSC setup MAIN + * negotiated (nested calls: MAIN's outer slot holds the A411), then let a + * worker give it to HELPER and set HELPER's mode while this thread keeps + * answering MAIN's callbacks. Values are copied as macOS 14 does. + */ +static void dcp_v14_split_relay(struct apple_dcp_v14 *v14) +{ + u8 z36[0x24] = {}, z4[4] = {}, z12[12] = {}; + __le32 dsc_on = 0; + int ret; + + ret = dcp_v14_call(v14, A(366), NULL, 0, &dsc_on, sizeof(dsc_on), 0); + if (!ret) + ret = dcp_v14_call(v14, A(364), z36, sizeof(z36), v14->split_dsc_caps, + sizeof(v14->split_dsc_caps), 0); + if (!ret) + ret = dcp_v14_call(v14, A(367), z4, sizeof(z4), v14->split_dsc_bpp, + sizeof(v14->split_dsc_bpp), 0); + if (!ret) + ret = dcp_v14_call(v14, A(369), z12, sizeof(z12), v14->split_dsc_slice, + sizeof(v14->split_dsc_slice), 0); + dev_info(v14->dev, "split: DSC relay from main: %d (dsc %u, bpp %*phN, slice %*phN)\n", + ret, le32_to_cpu(dsc_on), 4, v14->split_dsc_bpp, 12, v14->split_dsc_slice); + if (ret) { + v14->split_helper_ret = ret; + complete(&v14->split_done); + return; + } + queue_work(system_unbound_wq, &v14->split_work); +} + +/* + * split_modeset: D115 reads a tiling entry (event 1: this pipe's own, + * event 2: the peer's), D116 stores this pipe's entry. Answers as macOS 14: + * para 1 dual pipe on, 4 pipe index, 5 slice x origin, 8 VFTG merge on, + * 9 primary (owns the link), 11 zero; firmware-set paras are shared. + */ +static int dcp_v14_split_tiling(struct apple_dcp_v14 *v14, u32 tag, const u8 *in, u8 *out) +{ + bool is_main = READ_ONCE(v14->split_role) == DCP_V14_SPLIT_MAIN; + struct apple_dcp_v14 *peer = READ_ONCE(v14->split_peer); + u32 ev = get_unaligned_le32(in), para = get_unaligned_le32(in + 4); + u32 val = get_unaligned_le32(in + 8), value = 0; + unsigned long flags; + + if (tag == D(116)) { + if (para < ARRAY_SIZE(v14->tiling_state)) { + spin_lock_irqsave(&dcp_v14_split_table_lock, flags); + v14->tiling_state[para] = val; + spin_unlock_irqrestore(&dcp_v14_split_table_lock, flags); + } + out[0] = 1; + /* As macOS: the relay follows the acknowledged D116, at MAIN's next poll. */ + if (is_main && para == 10 && val == 2) + v14->split_relay_pending = true; + return 0; + } + if (is_main && ev == 2 && para == 10 && v14->split_relay_pending && + !v14->split_relay_queued) { + v14->split_relay_queued = true; + v14->split_relay_pending = false; + dcp_v14_split_relay(v14); + } + if (ev == 1) { + switch (para) { + case 1: value = 1; break; + case 4: value = (is_main ^ READ_ONCE(split_flip_index)) ? 0 : 1; break; + case 5: value = is_main ? 0 : v14->split_w; break; + case 8: value = 1; break; + case 9: value = is_main ? 1 : 0; break; + case 11: value = 0; break; + default: + if (para < ARRAY_SIZE(v14->tiling_state)) { + spin_lock_irqsave(&dcp_v14_split_table_lock, flags); + value = v14->tiling_state[para]; + spin_unlock_irqrestore(&dcp_v14_split_table_lock, flags); + } + break; + } + } else if (para == 4) { + value = (is_main ^ READ_ONCE(split_flip_index)) ? 1 : 0; + } else if (peer && para < ARRAY_SIZE(peer->tiling_state)) { + spin_lock_irqsave(&dcp_v14_split_table_lock, flags); + value = peer->tiling_state[para]; + spin_unlock_irqrestore(&dcp_v14_split_table_lock, flags); + } + put_unaligned_le32(value, out); + out[4] = 1; + return 0; +} + static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_size, void *output, u32 out_size) { @@ -1195,6 +1338,8 @@ static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_siz * byte. Like the M1/M2 hosts: no tiled display, no setter. */ if ((tag == D(115) && SHAPE(16, 8)) || (tag == D(116) && SHAPE(16, 4))) { + if (READ_ONCE(v14->split_role) != DCP_V14_SPLIT_NONE) + return dcp_v14_split_tiling(v14, tag, in, out); if (in[12] > 1) return -EINVAL; if (tag == D(115)) @@ -2176,6 +2321,14 @@ const char *iomfb_v14_7_external_busy(struct apple_dcp *dcp) } } +/* Experimental (split_modeset): this pipe drives half of another display. */ +bool iomfb_v14_7_split_helper(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = READ_ONCE(dcp->v14); + + return v14 && READ_ONCE(v14->split_role) == DCP_V14_SPLIT_HELPER; +} + bool iomfb_v14_7_external_failed(struct apple_dcp *dcp) { return smp_load_acquire(&dcp->external_phase) == DCPEXT_FAILED || @@ -2814,6 +2967,17 @@ static bool dcp_v14_swap_admit(void *cookie) if (!v14->external) return true; + /* split_modeset HELPER: no display of its own; follows MAIN's. */ + if (READ_ONCE(v14->split_role) == DCP_V14_SPLIT_HELPER) { + struct apple_dcp_v14 *mv14 = READ_ONCE(v14->split_peer); + struct apple_dcp *mdcp = mv14 ? READ_ONCE(mv14->dcp) : NULL; + + if (READ_ONCE(v14->failed) || !READ_ONCE(v14->powered) || + !READ_ONCE(v14->mode_live) || !mdcp) + return false; + connector = READ_ONCE(mdcp->connector); + return connector && READ_ONCE(connector->connected); + } if (!dcp || READ_ONCE(v14->failed) || !READ_ONCE(v14->powered) || !READ_ONCE(v14->mode_live) || admission->generation != v14->description_generation || @@ -3002,6 +3166,172 @@ static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebu return true; } +/* split_modeset: keep @fb pinned while MAIN may still read it (MAIN's lock held). */ +static void dcp_v14_split_stash(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb) +{ + if (!fb) + return; + if (v14->split_nstale < ARRAY_SIZE(v14->split_stale)) { + v14->split_stale[v14->split_nstale++] = fb; + return; + } + /* Never unmap what may be scanned out: keep the reference. */ + dev_warn_ratelimited(v14->dev, "split: framebuffer kept pinned until reboot\n"); +} + +/* split_modeset: MAIN completed a later swap or is off: release what was kept. */ +static void dcp_v14_split_unstash(struct apple_dcp_v14 *v14) +{ + while (v14->split_nstale) + drm_framebuffer_put(v14->split_stale[--v14->split_nstale]); +} + +/* + * split_modeset: one frame on both pipes, as macOS 14 does it: A406 on MAIN, + * A406 on HELPER with MAIN's swap ID, A407 on MAIN, A407 on HELPER. Both + * records describe the whole framebuffer at the same IOVA (the display + * subsystem's DMA domain spans both display DARTs); only the source rect + * differs: MAIN (0, 0, w, h), HELPER (w, 0, w, h), dst (0, 0, w, h) on both. + * Both locks held (MAIN outer). Returns true when both swaps were taken. + */ +static bool dcp_v14_split_present_locked(struct apple_dcp_v14 *mv14, struct apple_dcp_v14 *hv14, + struct drm_framebuffer *fb, u32 w, u32 h, + const struct drm_crtc_state *ctm_state, + struct drm_framebuffer **r0, struct drm_framebuffer **r1) +{ + struct dcp_v14_swap_admission ma = { .v14 = mv14 }, ha = { .v14 = hv14 }; + bool m_taken = false, h_taken = false, m_done = false; + __le32 start[4] = {}, started[2] = {}, result[3]; + u8 surface[DCP_V14_SURFACE_SIZE] = {}; + u8 *mswap = NULL, *hswap = NULL; + u64 iova = 0; + u32 id, hid; + int ret; + + lockdep_assert_held(&mv14->lock); + lockdep_assert_held(&hv14->lock); + if (READ_ONCE(mv14->failed) || READ_ONCE(hv14->failed)) + return false; + if (fb && (!w || !h || 2 * w > fb->width || h > fb->height || + w > mv14->panel_width || w > hv14->panel_width || + h > mv14->panel_height || h > hv14->panel_height)) + return false; + mswap = kmalloc(DCP_V14_SWAP_SIZE, GFP_KERNEL); + hswap = kmalloc(DCP_V14_SWAP_SIZE, GFP_KERNEL); + if (!mswap || !hswap) { + ret = -ENOMEM; + goto out; + } + if (fb) { + dcp_v14_encode_surface(surface, fb->pitches[0], fb->width, fb->height, + fb->format->format == DRM_FORMAT_XRGB8888); + iova = drm_fb_dma_get_gem_obj(fb, 0)->dma_addr; + } + ma.generation = mv14->description_generation; + ha.generation = hv14->description_generation; + dcp_v14_ctm_locked(mv14, ctm_state); + dcp_v14_ctm_locked(hv14, ctm_state); + + ret = dcp_v14_call_guarded(mv14, A(406), start, sizeof(start), started, sizeof(started), + 0, dcp_v14_swap_admit, &ma); + if (!ret && le32_to_cpu(started[1])) + ret = -EIO; + id = le32_to_cpu(started[0]); + if (!ret && !id) + ret = -EPROTO; + if (ret) + goto out; + start[0] = cpu_to_le32(id); + started[0] = started[1] = 0; + ret = dcp_v14_call_guarded(hv14, A(406), start, sizeof(start), started, sizeof(started), + 0, dcp_v14_swap_admit, &ha); + if (!ret && le32_to_cpu(started[1])) + ret = -EIO; + hid = le32_to_cpu(started[0]); + if (!ret && hid != id) { + dev_warn_ratelimited(hv14->dev, "split: helper swap id %u, main %u\n", hid, id); + ret = -EPROTO; + } + if (ret) + goto out; + + dcp_v14_encode_swap(mswap, id, DCP_V14_BLACK, fb ? surface : NULL, iova, w, h, 0, + DCP_V14_LAYER); + dcp_v14_encode_swap(hswap, id, DCP_V14_BLACK, fb ? surface : NULL, iova, w, h, 0, + DCP_V14_LAYER); + if (fb) + dcp_v14_put_rect(hswap + DCP_V14_SWAP_SRC(DCP_V14_LAYER), w, 0, w, h); + /* MAIN's submit is answered at once; its completion may wait for HELPER's. */ + ret = dcp_v14_call_guarded(mv14, A(407), mswap, DCP_V14_SWAP_SIZE, result, sizeof(result), + 0, dcp_v14_swap_admit, &ma); + if (!ret && get_unaligned_le32((u8 *)result + 5)) + ret = -EIO; + m_taken = !ret; + if (m_taken) { + ret = dcp_v14_call_guarded(hv14, A(407), hswap, DCP_V14_SWAP_SIZE, result, + sizeof(result), id, dcp_v14_swap_admit, &ha); + if (!ret && get_unaligned_le32((u8 *)result + 5)) + ret = -EIO; + h_taken = !ret; + } + /* MAIN's completion follows HELPER's submit: take it before reporting the flip. */ + if (m_taken) { + int wret = dcp_v14_link_wait_swap(&mv14->link, id, HZ / 2, dcp_v14_callback, mv14); + + if (!wret) { + m_done = true; + } else if (wret == -ETIMEDOUT) { + dev_warn_ratelimited(mv14->dev, "split: main swap %u not complete in 500 ms\n", id); + } else { + mv14->failed = true; + dev_err(mv14->dev, "split: main link failed waiting for swap %u: %d\n", id, wret); + dcp_v14_external_stopped(mv14); + if (!ret) + ret = wret; + } + } +out: + kfree(mswap); + kfree(hswap); + /* + * A pipe that took the swap may scan the new framebuffer out: pin it + * there. MAIN's previous one is released only once MAIN's completion + * was seen; otherwise it stays pinned until a later completed swap. + * HELPER's A407 waited for its own completion. + */ + if (m_taken) { + struct drm_framebuffer *old; + + if (fb) + drm_framebuffer_get(fb); + old = dcp_v14_publish_surface(mv14, fb, surface, iova, w, h, 0); + if (m_done) + *r0 = old; + else + dcp_v14_split_stash(mv14, old); + } + if (h_taken) { + if (fb) + drm_framebuffer_get(fb); + *r1 = dcp_v14_publish_surface(hv14, fb, surface, iova, w, h, 0); + } + if (!m_taken || !h_taken || !m_done) { + dev_warn_ratelimited(mv14->dev, + "split: swap incomplete (main %d/%d, helper %d): %d\n", + m_taken, m_done, h_taken, ret); + return false; + } + dcp_v14_split_unstash(mv14); + mv14->swaps++; + if (++hv14->swaps <= 3) + dev_info(hv14->dev, "split: swap %u shown on both pipes\n", id); + if (fb) { + mv14->mode_swaps++; + hv14->mode_swaps++; + } + return true; +} + static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, u32 width, u32 height, u32 dst_y, const struct drm_crtc_state *ctm_state) @@ -3288,6 +3618,426 @@ static void dcp_v14_audio_on(struct apple_dcp *dcp) } /* An external display: power down, set the new mode, power up. */ +/* + * split_modeset: the other T6030 external processor, running, opened by + * nobody else and with no display of its own. Returns it with a reference + * on its device in *devp, or NULL. + */ +static struct apple_dcp *dcp_v14_split_find_helper(struct apple_dcp *dcp, struct device **devp) +{ + struct device_node *np; + + for_each_compatible_node(np, NULL, "apple,t6030-dcpext") { + struct platform_device *pdev; + struct apple_dcp *h; + + if (np == dcp->dev->of_node) + continue; + pdev = of_find_device_by_node(np); + if (!pdev) + continue; + h = platform_get_drvdata(pdev); + if (h && h != dcp && h->external_native && h->v14 && + smp_load_acquire(&h->external_phase) == DCPEXT_RUNNING && + !(h->connector && READ_ONCE(h->connector->connected)) && + h->v14->split_role == DCP_V14_SPLIT_NONE && !READ_ONCE(h->v14->failed)) { + of_node_put(np); + *devp = &pdev->dev; + return h; + } + put_device(&pdev->dev); + } + + /* + * No running one: start an idle processor's firmware, as a display + * attach would (only an attach starts it otherwise), and wait for it. + */ + for_each_compatible_node(np, NULL, "apple,t6030-dcpext") { + struct platform_device *pdev; + struct apple_dcp *h; + int i; + + if (np == dcp->dev->of_node) + continue; + pdev = of_find_device_by_node(np); + if (!pdev) + continue; + h = platform_get_drvdata(pdev); + /* An idle processor has no v14 session yet: dcpext_boot() makes it. */ + if (!h || h == dcp || !h->external_native || + smp_load_acquire(&h->external_phase) != DCPEXT_IDLE || + (h->connector && READ_ONCE(h->connector->connected))) { + put_device(&pdev->dev); + continue; + } + dev_info(dcp->dev, "split: starting %s to help\n", dev_name(&pdev->dev)); + iomfb_v14_7_external_ready(h); + for (i = 0; i < 200; i++) { + unsigned int phase = smp_load_acquire(&h->external_phase); + + if (phase == DCPEXT_RUNNING || phase == DCPEXT_FAILED) + break; + msleep(50); + } + if (smp_load_acquire(&h->external_phase) == DCPEXT_RUNNING && h->v14 && + h->v14->split_role == DCP_V14_SPLIT_NONE && !READ_ONCE(h->v14->failed)) { + dev_info(dcp->dev, "split: %s running after %d ms\n", + dev_name(&pdev->dev), i * 50); + of_node_put(np); + *devp = &pdev->dev; + return h; + } + dev_warn(dcp->dev, "split: %s did not start\n", dev_name(&pdev->dev)); + put_device(&pdev->dev); + } + return NULL; +} + +/* + * split_modeset: MAIN's lock held. Leaves the split as macOS 14 does: + * MAIN powered down first, then HELPER, both still answering tiling state + * as a split; only then are the roles cleared. Safe on a failed MAIN. + */ +/* + * split_modeset: HELPER's A472(0), sent while MAIN's is pending. In dual + * pipe mode MAIN's power-down waits for HELPER's (trace 396149-396660): + * the two run-mode changes complete together. + */ +static void dcp_v14_split_off_work(struct work_struct *work) +{ + struct apple_dcp_v14 *v14 = container_of(work, struct apple_dcp_v14, split_off_work); + struct apple_dcp_v14 *hv14 = READ_ONCE(v14->split_peer); + int ret = 0; + + /* As macOS: MAIN's request goes first. Wait until it is on the link. */ + if (READ_ONCE(v14->split_off_after_main)) { + unsigned long until = jiffies + HZ; + + while (!dcp_v14_link_outer_pending(&v14->link) && + !READ_ONCE(v14->failed) && time_before(jiffies, until)) + usleep_range(1000, 2000); + } + if (hv14) { + mutex_lock(&hv14->lock); + if (hv14->powered && !READ_ONCE(hv14->failed)) { + WRITE_ONCE(hv14->link.long_calls, true); + ret = dcp_v14_set_power(hv14, false); + WRITE_ONCE(hv14->link.long_calls, false); + } + mutex_unlock(&hv14->lock); + } + v14->split_off_ret = ret; + complete(&v14->split_off_done); +} + +static void dcp_v14_split_teardown_locked(struct apple_dcp_v14 *v14) +{ + struct apple_dcp_v14 *hv14 = v14->split_peer; + struct drm_framebuffer *old = NULL; + bool helper_off = false; + int ret = 0; + + lockdep_assert_held(&v14->lock); + if (v14->split_role != DCP_V14_SPLIT_MAIN || !hv14) + return; + /* Both pipes power down together, still answering as a split. */ + if (READ_ONCE(hv14->powered) && !READ_ONCE(hv14->failed)) { + v14->split_off_after_main = v14->powered && !READ_ONCE(v14->failed); + init_completion(&v14->split_off_done); + INIT_WORK(&v14->split_off_work, dcp_v14_split_off_work); + queue_work(system_unbound_wq, &v14->split_off_work); + helper_off = true; + } + if (v14->powered && !READ_ONCE(v14->failed)) { + WRITE_ONCE(v14->link.long_calls, true); + ret = dcp_v14_set_power(v14, false); + WRITE_ONCE(v14->link.long_calls, false); + if (ret) + dev_warn(v14->dev, "split: main did not power down: %d\n", ret); + } + if (helper_off) { + if (!wait_for_completion_timeout(&v14->split_off_done, 25 * HZ)) + dev_err(v14->dev, "split: helper power-down did not finish\n"); + flush_work(&v14->split_off_work); + if (v14->split_off_ret) + dev_warn(hv14->dev, "split: helper did not power down: %d\n", + v14->split_off_ret); + } + dev_info(v14->dev, "split: both pipes powered down (main %d, helper %d)\n", + ret, helper_off ? v14->split_off_ret : 0); + /* MAIN no longer scans anything out once it is off. */ + if (!v14->powered && !READ_ONCE(v14->failed)) + dcp_v14_split_unstash(v14); + mutex_lock_nested(&hv14->lock, SINGLE_DEPTH_NESTING); + hv14->mode_live = false; + /* Only a helper known to be off releases its framebuffer. */ + if (!hv14->powered && !READ_ONCE(hv14->failed)) { + old = hv14->active_fb; + hv14->active_fb = NULL; + } + WRITE_ONCE(hv14->split_peer, NULL); + WRITE_ONCE(hv14->split_role, DCP_V14_SPLIT_NONE); + mutex_unlock(&hv14->lock); + WRITE_ONCE(v14->split_peer, NULL); + WRITE_ONCE(v14->split_role, DCP_V14_SPLIT_NONE); + v14->mode_live = false; + put_device(v14->split_peer_dev); + v14->split_peer_dev = NULL; + dev_info(v14->dev, "split: helper released\n"); + if (old) + drm_framebuffer_put(old); +} + +/* + * split_modeset: HELPER's half of the mode set, on a worker while MAIN's + * A411 is still pending on the modeset thread: the DSC setup MAIN + * negotiated (A365, A368, A370), then HELPER's own A411. + */ +static void dcp_v14_split_work(struct work_struct *work) +{ + struct apple_dcp_v14 *v14 = container_of(work, struct apple_dcp_v14, split_work); + struct apple_dcp_v14 *hv14 = READ_ONCE(v14->split_peer); + u8 in365[0x24] = {}, out365[0x20], out368[8], out370[12]; + int ret = -ENODEV; + + if (!hv14) + goto done; + memcpy(in365, v14->split_dsc_caps, sizeof(v14->split_dsc_caps)); + mutex_lock(&hv14->lock); + ret = dcp_v14_call(hv14, A(365), in365, sizeof(in365), out365, sizeof(out365), 0); + if (!ret) + ret = dcp_v14_call(hv14, A(368), v14->split_dsc_bpp, 4, out368, sizeof(out368), 0); + if (!ret) + ret = dcp_v14_call(hv14, A(370), v14->split_dsc_slice, sizeof(v14->split_dsc_slice), + out370, sizeof(out370), 0); + if (!ret) { + WRITE_ONCE(hv14->link.long_calls, true); + ret = dcp_v14_set_mode(hv14, v14->split_color, v14->split_timing); + WRITE_ONCE(hv14->link.long_calls, false); + } + mutex_unlock(&hv14->lock); + dev_info(hv14->dev, "split: helper mode (color %u, timing %u): %d\n", + v14->split_color, v14->split_timing, ret); +done: + v14->split_helper_ret = ret; + complete(&v14->split_done); +} + +/* + * split_modeset: the macOS 14 mode set of one display over two pipes, as + * traced on a J514S (omacom/linux-aurora#434). MAIN's lock held. + * On success both pipes run the split timing (@mode IDs + 10000) at half + * width; on failure the split is torn down and split_modeset switched off. + */ +static int dcp_v14_split_modeset_locked(struct apple_dcp *dcp, struct apple_dcp_v14 *v14, + struct dcp_display_mode *mode) +{ + u8 in361[0x7c] = {}, out361[0x74] = {}, in362[0x7c], out362[0x74]; + u32 w = mode->mode.hdisplay / 2, i, color, timing; + __le32 two = cpu_to_le32(2), zero = 0, one = cpu_to_le32(1), mctr = 0, hctr = 0; + __le32 r409 = 0; + struct apple_dcp_v14 *hv14 = NULL; + struct device *hdev = NULL; + struct apple_dcp *h; + unsigned long flags; + int ret; + + lockdep_assert_held(&v14->lock); + /* + * The merge works only with the hybrid (HDMI) pipeline, ADT dcpext0, as + * the pipe that owns the link: with the roles reversed every call + * succeeds and the display stays dark. Touch nothing then. + */ + if (!dcp->fixed_phy) { + dev_warn(dcp->dev, "split: only the HDMI pipeline can own a split display (a dock needs split_dock_main)\n"); + return -EOPNOTSUPP; + } + color = mode->color_mode_id; + timing = mode->timing_mode_id; + + /* The full timing, read while MAIN still runs its current mode (as macOS). */ + ret = dcp_v14_call(v14, A(409), &two, sizeof(two), &r409, sizeof(r409), 0); + put_unaligned_le32(timing, in361 + 0x70); + put_unaligned_le32(color, in361 + 0x74); + if (!ret) + ret = dcp_v14_call(v14, A(361), in361, sizeof(in361), out361, sizeof(out361), 0); + if (!ret && (out361[0x70] != 1 || + get_unaligned_le32(out361 + 0x08) != mode->mode.hdisplay || + get_unaligned_le32(out361 + 0x28) != mode->mode.vdisplay || + !get_unaligned_le32(out361 + 0x44))) + ret = -ENODATA; + dev_info(dcp->dev, "split: A361 (timing %u, color %u): %d, timing %*phN %*phN\n", + timing, color, ret, 0x40, out361, 0x34, out361 + 0x40); + if (ret) + goto fail_off; + + dcp_v14_split_teardown_locked(v14); + h = dcp_v14_split_find_helper(dcp, &hdev); + if (!h) { + dev_warn(dcp->dev, "split: no free external pipe to help\n"); + ret = -ENODEV; + goto fail_off; + } + hv14 = h->v14; + ret = iomfb_v14_7_external_open(h); + /* A harmless getter: a dead helper fails here, before MAIN goes dark. */ + if (!ret) { + mutex_lock_nested(&hv14->lock, SINGLE_DEPTH_NESTING); + ret = dcp_v14_call(hv14, A(357), NULL, 0, &hctr, sizeof(hctr), 0); + mutex_unlock(&hv14->lock); + } + if (ret) { + dev_warn(dcp->dev, "split: helper %s not usable: %d\n", dev_name(hdev), ret); + goto fail_unlinked; + } + dev_info(dcp->dev, "split: %ux%u on this pipe and %s, %u px each\n", + mode->mode.hdisplay, mode->mode.vdisplay, dev_name(hdev), w); + + mutex_lock_nested(&hv14->lock, SINGLE_DEPTH_NESTING); + if (v14->powered && dcp_v14_set_power(v14, false)) + dev_warn(dcp->dev, "split: main did not power down\n"); + if (hv14->powered && dcp_v14_set_power(hv14, false)) + dev_warn(hv14->dev, "split: helper did not power down\n"); + ret = dcp_v14_call(v14, A(359), &zero, sizeof(zero), NULL, 0, 0); + if (!ret) + ret = dcp_v14_call(hv14, A(359), &zero, sizeof(zero), NULL, 0, 0); + if (!ret) + ret = dcp_v14_call(hv14, A(363), &one, sizeof(one), NULL, 0, 0); + if (ret) + goto fail_locked_unlinked; + + /* The relay's work item exists before any callback could ask for it. */ + v14->split_color = color + 10000; + v14->split_timing = timing + 10000; + v14->split_helper_ret = -EINPROGRESS; + v14->split_relay_queued = true; /* not before MAIN's A411 */ + v14->split_relay_pending = false; + init_completion(&v14->split_done); + INIT_WORK(&v14->split_work, dcp_v14_split_work); + + /* Arm the split: the firmware asks tiling state from A362 on. */ + spin_lock_irqsave(&dcp_v14_split_table_lock, flags); + memset(v14->tiling_state, 0, sizeof(v14->tiling_state)); + memset(hv14->tiling_state, 0, sizeof(hv14->tiling_state)); + v14->tiling_state[10] = 1; + hv14->tiling_state[10] = 1; + spin_unlock_irqrestore(&dcp_v14_split_table_lock, flags); + v14->split_w = hv14->split_w = w; + v14->split_peer_dev = hdev; + WRITE_ONCE(hv14->split_peer, v14); + WRITE_ONCE(hv14->split_role, DCP_V14_SPLIT_HELPER); + WRITE_ONCE(v14->split_peer, hv14); + WRITE_ONCE(v14->split_role, DCP_V14_SPLIT_MAIN); + hdev = NULL; /* owned by the split now; teardown puts it */ + + /* Half timing: the eight horizontal u32 and the pixel clock halved. */ + memcpy(in362, out361, 0x70); + for (i = 0; i < 8; i++) + put_unaligned_le32(get_unaligned_le32(out361 + 4 * i) / 2, in362 + 4 * i); + put_unaligned_le32(get_unaligned_le32(out361 + 0x44) / 2, in362 + 0x44); + put_unaligned_le32(timing + 10000, in362 + 0x70); + put_unaligned_le32(color + 10000, in362 + 0x74); + put_unaligned_le32(0, in362 + 0x78); + dev_info(dcp->dev, "split: half timing %*phN %*phN\n", 0x40, in362, 0x3c, in362 + 0x40); + ret = dcp_v14_call(v14, A(362), in362, sizeof(in362), out362, sizeof(out362), 0); + dev_info(dcp->dev, "split: A362 main: %d (%*phN)\n", ret, 4, out362 + 0x70); + if (!ret) { + ret = dcp_v14_call(hv14, A(362), in362, sizeof(in362), out362, sizeof(out362), 0); + dev_info(hv14->dev, "split: A362 helper: %d (%*phN)\n", ret, 4, out362 + 0x70); + } + if (!ret) + ret = dcp_v14_set_power(v14, true); + if (!ret) + ret = dcp_v14_set_power(hv14, true); + if (!ret) + ret = dcp_v14_call(v14, A(357), NULL, 0, &mctr, sizeof(mctr), 0); + if (!ret) + ret = dcp_v14_call(hv14, A(357), NULL, 0, &hctr, sizeof(hctr), 0); + if (!ret && mctr != hctr) + ret = dcp_v14_call(hv14, A(358), &mctr, sizeof(mctr), NULL, 0, 0); + dev_info(dcp->dev, "split: prepared (swap counters %u/%u): %d\n", + le32_to_cpu(mctr), le32_to_cpu(hctr), ret); + if (ret) + goto fail_locked; + /* HELPER's calls run on the worker, which takes HELPER's lock. */ + mutex_unlock(&hv14->lock); + + v14->split_relay_queued = false; + WRITE_ONCE(v14->link.long_calls, true); + ret = dcp_v14_set_mode(v14, v14->split_color, v14->split_timing); + WRITE_ONCE(v14->link.long_calls, false); + if (v14->split_relay_queued) { + if (!wait_for_completion_timeout(&v14->split_done, 25 * HZ)) + dev_err(dcp->dev, "split: helper mode set did not finish\n"); + flush_work(&v14->split_work); + if (!ret) + ret = v14->split_helper_ret; + } else { + v14->split_relay_queued = true; /* no late relay from now on */ + if (!ret) { + dev_err(dcp->dev, "split: main mode set ended without asking for its peer\n"); + ret = -EPROTO; + } + } + dev_info(dcp->dev, "split: mode (color %u, timing %u) on both pipes: %d\n", + v14->split_color, v14->split_timing, ret); + if (ret) + goto fail; + + mutex_lock_nested(&hv14->lock, SINGLE_DEPTH_NESTING); + hv14->panel_width = w; + hv14->panel_height = mode->mode.vdisplay; + hv14->mode_timing = v14->split_timing; + hv14->mode_color = v14->split_color; + hv14->mode_swaps = 0; + hv14->mode_live = true; + mutex_unlock(&hv14->lock); + v14->panel_width = w; + v14->panel_height = mode->mode.vdisplay; + return 0; + +fail_locked_unlinked: + mutex_unlock(&hv14->lock); +fail_unlinked: + put_device(hdev); + goto fail_off; +fail_locked: + mutex_unlock(&hv14->lock); +fail: + dcp_v14_split_teardown_locked(v14); +fail_off: + /* + * A pipe that died stays dead until reboot: switch the split off so + * retries do not touch it again. Anything else (a link not up yet after + * a wake, a missing helper) is left to the normal retries. + */ + if (READ_ONCE(v14->failed) || (hv14 && READ_ONCE(hv14->failed))) { + WRITE_ONCE(split_modeset, false); + dev_warn(dcp->dev, "split: mode set failed: %d; a pipe died, split_modeset switched off\n", + ret); + } else { + dev_warn(dcp->dev, "split: mode set failed: %d; will retry\n", ret); + } + return ret; +} + +/* Does @mode need two pipes? (active pixel rate above one pipe's) */ +static bool dcp_v14_split_needed(const struct drm_display_mode *mode) +{ + u64 total = (u64)mode->htotal * mode->vtotal, hz, rate; + + /* + * As the firmware's filter: active pixels times the whole-Hz refresh + * ("5120 x 2160 @ 119, bw 1316044800" for 119.88 Hz), from the pixel + * clock, not drm_mode_vrefresh(), which rounds to the nearest Hz. + */ + if (!total || mode->clock <= 0 || (mode->flags & DRM_MODE_FLAG_INTERLACE)) + return false; + hz = div64_u64((u64)mode->clock * 1000, total); + rate = (u64)mode->hdisplay * mode->vdisplay * hz; + return rate > DCP_V14_SPLIT_PIPE_RATE && !(mode->hdisplay & 1); +} + static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state *crtc_state) { struct apple_dcp_v14 *v14 = dcp->v14; @@ -3297,6 +4047,11 @@ static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state if (!v14 || READ_ONCE(v14->failed) || !v14->opened) return -EIO; + /* split_modeset: a pipe lent to another display takes no mode of its own. */ + if (READ_ONCE(v14->split_role) == DCP_V14_SPLIT_HELPER) { + dev_warn(dcp->dev, "split: this pipe helps another display; mode set refused\n"); + return -EBUSY; + } /* * The sink's audio stops before the display is powered down for the * mode change (as dcp_poweroff() has it), outside the RPC lock; it is @@ -3330,6 +4085,27 @@ static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state dcp_v14_audio_on(dcp); return 0; } + /* Experimental (split_modeset): a mode above one pipe's rate on two pipes. */ + if (READ_ONCE(split_modeset) && dcp_v14_split_needed(&mode.mode)) { + ret = dcp_v14_split_modeset_locked(dcp, v14, &mode); + if (!ret) { + v14->mode_timing = mode.timing_mode_id; + v14->mode_color = mode.color_mode_id; + v14->mode_swaps = 0; + v14->mode_live = true; + } + mutex_unlock(&v14->lock); + goto out; + } + /* One pipe cannot drive it: refuse rather than leave the display dark. */ + if (dcp_v14_split_needed(&mode.mode)) { + mutex_unlock(&v14->lock); + dev_warn(dcp->dev, "split: " DRM_MODE_FMT " needs two pipes; split_modeset is off\n", + DRM_MODE_ARG(&mode.mode)); + ret = -EINVAL; + goto out; + } + dcp_v14_split_teardown_locked(v14); if (v14->powered && dcp_v14_set_power(v14, false)) dev_warn(dcp->dev, "display did not power down before the mode change\n"); if (READ_ONCE(v14->failed)) @@ -3423,6 +4199,47 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, if (!dcp->external) dcp_v14_configure_backlight(dcp); dcp->swap_start = ktime_get(); + /* split_modeset: MAIN shows the left half, then HELPER the right half. */ + if (dcp->v14 && READ_ONCE(dcp->v14->split_role) == DCP_V14_SPLIT_MAIN && + READ_ONCE(dcp->v14->split_peer)) { + struct apple_dcp_v14 *v14 = dcp->v14, *hv14; + struct drm_framebuffer *r0 = NULL, *r1 = NULL; + u32 w = fb ? width / 2 : 0, h = fb ? height : 0; + bool shown = false, stopped = false; + + mutex_lock(&v14->lock); + /* Teardown clears the link under this lock: read it again here. */ + hv14 = v14->split_role == DCP_V14_SPLIT_MAIN ? v14->split_peer : NULL; + if (hv14) { + mutex_lock_nested(&hv14->lock, SINGLE_DEPTH_NESTING); + shown = dcp_v14_split_present_locked(v14, hv14, fb, w, h, cs, &r0, &r1); + mutex_unlock(&hv14->lock); + } + /* A pipe that died ends the split: the mode is not live any more. */ + if (!shown && hv14 && (READ_ONCE(hv14->failed) || READ_ONCE(v14->failed)) && + READ_ONCE(v14->mode_live)) { + WRITE_ONCE(split_modeset, false); + WRITE_ONCE(v14->mode_live, false); + dcp_mode_invalidate(&dcp->mode_state); + dev_err(dcp->dev, "split: a pipe stopped; split_modeset switched off\n"); + stopped = true; + } + mutex_unlock(&v14->lock); + if (r0) + drm_framebuffer_put(r0); + if (r1) + drm_framebuffer_put(r1); + /* Let the compositor pick a mode one pipe can drive. */ + if (stopped && dcp->connector) + dcp_queue_hotplug(dcp->connector); + if (shown) { + dcp_drm_crtc_page_flip(dcp, ktime_get()); + dcp_release_all_retained_framebuffers(dcp); + } else { + dcp_v14_cancel_event(dcp); + } + return; + } if (dcp->v14 && dcp_v14_present(dcp->v14, fb, width, height, dcp->notch_height, cs)) { dcp_drm_crtc_page_flip(dcp, ktime_get()); /* @@ -3452,9 +4269,14 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) /* Audio stops with the display, as dcp_poweroff() has it. */ if (dcp->avep) av_service_disconnect(dcp); - if (v14 && v14->opened && !READ_ONCE(v14->failed)) { + /* split_modeset: the helper stays on while it shows MAIN's right half. */ + if (v14 && READ_ONCE(v14->split_role) == DCP_V14_SPLIT_HELPER) + return; + if (v14 && v14->opened) { mutex_lock(&v14->lock); - if (v14->powered && dcp_v14_set_power(v14, false)) + dcp_v14_split_teardown_locked(v14); + if (!READ_ONCE(v14->failed) && v14->powered && + dcp_v14_set_power(v14, false)) dev_warn(dcp->dev, "display did not power down\n"); mutex_unlock(&v14->lock); } diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.h b/drivers/gpu/drm/apple/iomfb_v14_7.h index 7eb6ada2b1c26c..9f77e3e46f00f7 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7.h @@ -60,6 +60,7 @@ int iomfb_v14_7_external_open(struct apple_dcp *dcp); const char *iomfb_v14_7_external_busy(struct apple_dcp *dcp); /* The external session can no longer be used until reboot. */ bool iomfb_v14_7_external_failed(struct apple_dcp *dcp); +bool iomfb_v14_7_split_helper(struct apple_dcp *dcp); /* The attached display is powered (A472 on) in the mode last set. */ bool iomfb_v14_7_external_powered(struct apple_dcp *dcp); /* The firmware runs this mode, set last, and shows swaps in it. */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_link.c b/drivers/gpu/drm/apple/iomfb_v14_7_link.c index eb54b2821f008a..3f30cf79b6f845 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_link.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_link.c @@ -402,6 +402,44 @@ int dcp_v14_link_pump(struct dcp_v14_link *link, unsigned long timeout, return ret; } +/* Experimental (split_modeset): is an outer call on the link right now? */ +bool dcp_v14_link_outer_pending(struct dcp_v14_link *link) +{ + bool pending; + + mutex_lock(&link->lock); + pending = link->calls[0].active; + mutex_unlock(&link->lock); + return pending; +} + +/* + * Experimental (split_modeset): handle callbacks until the swap-complete + * callback for swap @id, with no call outstanding. Same rules as + * dcp_v14_link_pump(). -ETIMEDOUT leaves the link usable. + */ +int dcp_v14_link_wait_swap(struct dcp_v14_link *link, u32 id, unsigned long timeout, + dcp_v14_callback_fn callback, void *cookie) +{ + unsigned long deadline = jiffies + timeout; + u32 completed = 0; + int ret; + + while (time_before(jiffies, deadline)) { + struct dcp_v14_event *e = link_next_event(link, max_t(long, deadline - jiffies, 0)); + + if (IS_ERR(e)) + return PTR_ERR(e); + ret = link_callback(link, e, callback, cookie, &completed); + kfree(e); + if (ret) + return ret; + if (completed == id) + return 0; + } + return -ETIMEDOUT; +} + /* * Calls @tag and handles the callbacks that arrive until its reply, and, for * a non-zero @completion_id, until the swap-complete callback for that swap. @@ -415,7 +453,8 @@ int dcp_v14_link_call_guarded(struct dcp_v14_link *link, u32 tag, const void *in dcp_v14_admit_fn admit, void *admit_cookie, bool *vetoed) { struct apple_dcp_link_rpc_header *packet; - unsigned long deadline = jiffies + DCP_V14_CALL_TIMEOUT; + unsigned long deadline = jiffies + + (READ_ONCE(link->long_calls) ? 20 * HZ : DCP_V14_CALL_TIMEOUT); u64 expected = APPLE_DCP_LINK_MSG_RPC_REPLY, parent = 0; u32 size, completed = 0; bool nested, replied = false; diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_link.h b/drivers/gpu/drm/apple/iomfb_v14_7_link.h index 9d98217a44d151..a7aaa04b08d6ab 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_link.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_link.h @@ -46,6 +46,8 @@ struct dcp_v14_link { unsigned int callback_count; /* Callback handlers running on the caller's stack. */ unsigned int callback_depth; + /* Experimental (split_modeset): outer calls may take 20 s instead of 5 s. */ + bool long_calls; bool failed; bool enabled; struct work_struct *idle_work; @@ -92,6 +94,9 @@ int dcp_v14_link_call_guarded(struct dcp_v14_link *link, u32 tag, const void *in u32 input_size, void *output, u32 output_size, u32 completion_id, dcp_v14_callback_fn callback, void *cookie, dcp_v14_admit_fn admit, void *admit_cookie, bool *vetoed); +bool dcp_v14_link_outer_pending(struct dcp_v14_link *link); +int dcp_v14_link_wait_swap(struct dcp_v14_link *link, u32 id, unsigned long timeout, + dcp_v14_callback_fn callback, void *cookie); int dcp_v14_link_pump(struct dcp_v14_link *link, unsigned long timeout, dcp_v14_callback_fn callback, void *cookie); From 6752dd9afaeaf9edd424a814c7f7874a23168eb4 Mon Sep 17 00:00:00 2001 From: Jeppe Bylov Date: Wed, 11 Nov 2026 16:10:59 +0100 Subject: [PATCH 3/4] drm/apple: fabric: optionally put a Thunderbolt display on the HDMI pipeline The two-pipe merge of split_modeset works only with ADT dcpext0 as the pipe that owns the link, as on macOS. dcpext0 is the hybrid (HDMI) pipeline, which a Thunderbolt tunnel never takes while dcpext1 is free, so a display behind a dock lands on dcpext1 and stays dark in split mode while every firmware call succeeds. Add split_dock_main (default off, 0644). When set, a tunnel that would take a Type-C-only pipeline takes the hybrid instead, if it is free and no HDMI display is present. A pipe lent to a split as HELPER counts as busy for route selection, so no other display can take it while the split is live. With it, the dock's dpin0 routes to state 0 in the display crossbar, exactly the register state macOS 14 leaves in 5120x2160@180 split mode. Link: https://github.com/omacom/linux-aurora/issues/434 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w --- drivers/gpu/drm/apple/dcp-fabric.c | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 29321f8032a490..e3ae12d49f896f 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -34,6 +34,11 @@ module_param(tiled_split, bool, 0644); MODULE_PARM_DESC(tiled_split, "Drive both halves of a tiled Thunderbolt display (LG UltraFine 5K) on one pipeline, through its two DPTX ports (default: on)"); +static bool split_dock_main; +module_param(split_dock_main, bool, 0644); +MODULE_PARM_DESC(split_dock_main, + "EXPERIMENT: put a Thunderbolt display on the free HDMI pipeline (dcpext0), so split_modeset can drive it over two pipes as macOS does"); + static bool typec_follow_crtc = true; module_param(typec_follow_crtc, bool, 0444); MODULE_PARM_DESC(typec_follow_crtc, @@ -180,7 +185,9 @@ static bool dcp_typec_route_busy(struct apple_dcp_typec_route *route) struct apple_dcp *dcp = route->dcp; return dcp_typec_route_fixed_output_busy(route) || - (dcp->external_native && iomfb_v14_7_external_failed(dcp)); + (dcp->external_native && iomfb_v14_7_external_failed(dcp)) || + /* Experimental (split_modeset): lent to another display's split. */ + (dcp->external_native && iomfb_v14_7_split_helper(dcp)); } static bool dcp_typec_route_available(struct apple_dcp_typec_route *route) @@ -1954,6 +1961,28 @@ static int dcp_tb_candidate(void *data) return ret; } + /* + * Experimental (split_dock_main): a display driven over two pipes + * needs the HDMI pipeline (the hybrid, dcpext0 on T6030) as the one + * that owns the link, as macOS does. Take it for the tunnel while it is + * free and no HDMI display is present. + */ + if (READ_ONCE(split_dock_main) && !best->dcp->fixed_phy) { + struct apple_dcp_typec_route *r; + + list_for_each_entry(r, &port->routes, port_link) { + if (r == best || !r->dcp->fixed_phy || + !dcp_typec_route_available(r) || + dcp_hdmi_presence(r->dcp) != DCP_FABRIC_ABSENT || + !dcp_typec_tunnel_ctl(r, dpin)) + continue; + dev_info(r->dcp->dev, "split_dock_main: tunnel dpin%u takes this pipeline instead of %s\n", + dpin, dev_name(best->dcp->dev)); + best = r; + break; + } + } + ctl = dcp_typec_tunnel_ctl(best, dpin); if (!ctl) return -EOPNOTSUPP; From 1aa769271c97b742d0c8b1b8e67228f7bf488d57 Mon Sep 17 00:00:00 2001 From: Jeppe Bylov Date: Thu, 12 Nov 2026 06:00:08 +0100 Subject: [PATCH 4/4] drm/apple: fabric: let split_dock_main act while HDMI presence settles HDMI presence stays DCP_FABRIC_SETTLING for DCP_HDMI_HOLD_MS after any edge, including the first sample at boot, which is exactly when a dock that was connected at boot brings up its tunnel. The override then never fired and the dock stayed on dcpext1, where the split cannot merge. While presence is settling, read the HDMI HPD line directly and treat a low line as an empty port. Because dcp_typec_route_available() would also refuse a settling pipe, check the pipe is idle with the same conditions it uses (no active route, no safe-mode hold, route not busy). Tested on J514S (M3 Pro) with a G75F behind a TB dock connected at boot: the dock takes 2d2c00000.dcp at ~17 s and comes up split at 5120x2160@180. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w --- drivers/gpu/drm/apple/dcp-fabric.c | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index e3ae12d49f896f..6afb94fc67d521 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -1971,10 +1971,29 @@ static int dcp_tb_candidate(void *data) struct apple_dcp_typec_route *r; list_for_each_entry(r, &port->routes, port_link) { - if (r == best || !r->dcp->fixed_phy || - !dcp_typec_route_available(r) || - dcp_hdmi_presence(r->dcp) != DCP_FABRIC_ABSENT || - !dcp_typec_tunnel_ctl(r, dpin)) + struct apple_dcp *h = r->dcp; + bool hdmi_empty; + + if (r == best || !h->fixed_phy || !dcp_typec_tunnel_ctl(r, dpin)) + continue; + /* + * HDMI presence stays SETTLING for DCP_HDMI_HOLD_MS after + * any edge, including the first sample at boot, which is + * when a dock's tunnel comes up. Read the HPD line then. + */ + switch (dcp_hdmi_presence(h)) { + case DCP_FABRIC_ABSENT: + hdmi_empty = true; + break; + case DCP_FABRIC_SETTLING: + hdmi_empty = h->hdmi_hpd && !gpiod_get_value_cansleep(h->hdmi_hpd); + break; + default: + hdmi_empty = false; + break; + } + if (!hdmi_empty || h->active_typec_route || dcp_typec_safe_held(h) || + dcp_typec_route_busy(r)) continue; dev_info(r->dcp->dev, "split_dock_main: tunnel dpin%u takes this pipeline instead of %s\n", dpin, dev_name(best->dcp->dev));