Repository navigation
drm/apple: 5K@120/180 on J514S by splitting the display across both external pipes (opt-in) - #445
JeppeBylov wants to merge 4 commits into
Conversation
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: omacom#434
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical framebuffer-lifetime and split-pipe coordination bugs can unmap memory still being scanned out, allow conflicting HELPER reconfiguration, and leave displays unusable.
8 open issues
Propagate terminal HELPER failure to MAIN's mode and connector state · New Treat MAIN completion timeout as failure and retain scanout buffers · New Pin new buffers before submission and retain them on uncertain failures · New Order HELPER teardown after MAIN actually submits A472 · New Keep HELPER's framebuffer pinned while scanout may still be active · New Reject split scanout unless MAIN uses the supported HDMI/hybrid pipeline · New Reserve HELPER atomically in fabric ownership during split scanout · New Compare mode refresh rates without whole-Hz rounding · New
What changed in this PR
Adds experimental, opt-in dual-pipe scanout to the Apple DRM driver for J514S external modes above a single pipe’s pixel-rate limit, targeting 5120×2160 at 120/180 Hz and addressing #434.
Changes:
- Advertises peer-pipe allocation through DPTX display hints.
- Coordinates split timings, DSC setup, frame submission and power-down across both external pipes.
- Adds optional Thunderbolt placement on the HDMI/hybrid pipeline and longer RPC waits for split operations.
| File | Description |
|---|---|
drivers/gpu/drm/apple/iomfb_v14_7.c |
Implements helper discovery, split modesetting, paired frames and teardown. |
drivers/gpu/drm/apple/iomfb_v14_7_link.h |
Declares extended-call state and the swap-completion wait API. |
drivers/gpu/drm/apple/iomfb_v14_7_link.c |
Adds swap-completion waiting and optional 20-second RPC deadlines. |
drivers/gpu/drm/apple/dptxep.c |
Adds opt-in peer-pipe allocation to display-hint replies. |
drivers/gpu/drm/apple/dcp-fabric.c |
Adds optional Thunderbolt placement on the free HDMI/hybrid pipeline. |
Review effort: Balanced
| if (READ_ONCE(mv14->failed) || READ_ONCE(hv14->failed)) | ||
| return false; |
There was a problem hiding this comment.
Fixed in 6752dd9: when a split swap fails and either pipe has failed, the flush switches split_modeset off, clears MAIN's mode_live, invalidates its mode state and queues a hotplug on MAIN's connector, so the compositor re-probes and the same-mode fast path cannot be taken.
| if (wret == -ETIMEDOUT) { | ||
| dev_warn_ratelimited(mv14->dev, "split: main swap %u not complete in 500 ms\n", id); |
There was a problem hiding this comment.
Fixed: MAIN's previous framebuffer is returned for release only after MAIN's completion was seen. On a timeout it is kept in a small per-MAIN stash and released after a later completed swap or once MAIN is powered off, and the flip is reported as incomplete.
| if (ret) { | ||
| dev_warn_ratelimited(mv14->dev, "split: swap refused: %d\n", ret); | ||
| return false; |
There was a problem hiding this comment.
Fixed: each pipe that accepted A407 gets its own reference to the new framebuffer and publishes it, even if the other pipe or the completion wait failed afterwards. The flip is then reported as incomplete, so DRM keeps its retained buffers as well.
| /* As macOS: MAIN's request goes first. */ | ||
| msleep(30); |
There was a problem hiding this comment.
Fixed: the helper's power-down worker no longer sleeps. It waits (up to 1 s) until MAIN's A472(0) is the outer call on MAIN's link (new dcp_v14_link_outer_pending()), with a bypass when MAIN is already off or failed.
| mutex_lock_nested(&hv14->lock, SINGLE_DEPTH_NESTING); | ||
| hv14->mode_live = false; | ||
| /* A helper that may still scan out keeps its framebuffer pinned. */ | ||
| if (!hv14->powered || READ_ONCE(hv14->failed)) { |
There was a problem hiding this comment.
Fixed: the teardown releases HELPER's active framebuffer only when HELPER is known to be powered off and has not failed; otherwise the reference stays pinned.
| unsigned long flags; | ||
| int ret; | ||
|
|
||
| lockdep_assert_held(&v14->lock); |
There was a problem hiding this comment.
Fixed: dcp_v14_split_modeset_locked() now refuses the split (-EOPNOTSUPP) before touching either pipe unless MAIN is the hybrid pipeline (dcp->fixed_phy, ADT dcpext0).
| WRITE_ONCE(hv14->split_peer, v14); | ||
| WRITE_ONCE(hv14->split_role, DCP_V14_SPLIT_HELPER); |
There was a problem hiding this comment.
Fixed in the fabric commit: dcp_typec_route_busy() also reports a pipe that is lent to a split as HELPER (new iomfb_v14_7_split_helper()), so route selection and the fabric snapshot treat it as busy for as long as the split is live.
| u64 rate = (u64)mode->hdisplay * mode->vdisplay * drm_mode_vrefresh(mode); | ||
|
|
||
| return rate > DCP_V14_SPLIT_PIPE_RATE && !(mode->hdisplay & 1); |
There was a problem hiding this comment.
Good catch, fixed: the predicate now uses the firmware's own formula, active pixels times the whole-Hz refresh computed from the pixel clock and the totals (it logs 5120x2160@119.88 as 'bw 1316044800' = 51202160119), instead of drm_mode_vrefresh()'s rounding. Interlaced modes are excluded, and the u64 product cannot overflow for valid modes.
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: omacom#434
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w
…ipeline 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: omacom#434 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w
0397059 to
6752dd9
Compare
|
Pushed 6752dd9 with all eight review points addressed (replies inline). Re-tested on the J514S with the same driver built for aurora15: boot through the dock at 5120x2160@179.985, 180 -> 120 -> 180 Hz while running, DPMS off/on. All clean, no incomplete swaps or pinned-buffer warnings. Compile-tested against bed544b with clang 18, no new warnings. |
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w
|
Pushed 1aa7692, a fix for booting with the dock already connected (hotplug after boot already worked). Problem: HDMI presence stays Fix: while presence is settling, read the HDMI HPD GPIO directly and treat a low line as an empty port. Tested on aurora16 (7.2.11-16) on a J514S with a G75F behind a TB dock connected at boot:
I pushed this as a separate commit to avoid a force-push. Happy to squash it into the fabric commit if you prefer. (Written with LLM assistance, Claude Code.) |


Summary
Drive external modes above one pipe's pixel rate (5120x2160@120 and @180 on a Samsung Odyssey G75F) on the M3 Pro (J514S) by splitting the display across both T6030 external pipes, the way macOS 14 does it. Tracked in #434, which has the trace analysis.
Everything is behind three module parameters, all off by default. With them off, behaviour is unchanged.
appledrm.split_allocSET_TILED_DISPLAY_HINTS) with the pipe allocation macOS sends (+0x64..+0x77:1, 1, 0x1800, 0x55b62380, 0x4bf00000). The firmware then keeps the 5K@120/180 timings instead of "IOMFB removing mode ... max 1274019840".appledrm.split_modesetappledrm.split_dock_mainHow the split works (from a macOS 14.8.9 hypervisor trace on a J514S)
Mode set (the full call sequence is in the commit message):
Tiling state: D115/D116 become a getter/setter backed by a table per pipe. Event 1 is the pipe's own entry, event 2 the peer's: dual pipe on, pipe index, slice origin, VFTG merge, primary.
Frames: A406 MAIN, then A406 HELPER with MAIN's swap ID, then A407 MAIN, then A407 HELPER. Both records describe the whole framebuffer at the same IOVA;
display-subsystemspans both display DARTs. HELPER's source rect starts at x = hdisplay/2.Leaving the split: both A472(0)s overlap, MAIN first, as in the trace. Sequential power-down kills MAIN (
power_down_M3: auto_mode_change failed with 0x8000000f).Refusal: with
split_modesetoff, a mode that needs two pipes is refused instead of being set on one pipe.The display crossbar needs no change. In split mode macOS routes only MAIN through it (dpin0 → state 0,
MUX_SELECT0, the helper's bits gated), which we checked with an MMIO trace of 0x70304c000.Testing (J514S, linux-aurora 7.2.11.aurora15 + these patches, Samsung G75F)
Works:
split_dock_main/etc/modprobe.d; the helper pipe is started on demandNot tested:
Open questions for reviewers
split_dock_mainbe a placement policy (prefer the hybrid when a split mode is requested), rather than a switch?long_callsbudget for the waiting A411/A472(0) is a guess. It held in every test.Investigation, patches and this description were done with Claude Code (LLM-assisted) and tested on my own J514S.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QgYUjxeetyPfXvgBwpsu4w