Skip to content

drm/apple: 5K@120/180 on J514S by splitting the display across both external pipes (opt-in) - #445

Open
JeppeBylov wants to merge 4 commits into
omacom:integration/aurorafrom
JeppeBylov:split-5k-dual-pipe
Open

JeppeBylov wants to merge 4 commits into
omacom:integration/aurorafrom
JeppeBylov:split-5k-dual-pipe

Conversation

@JeppeBylov

Copy link
Copy Markdown

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.

parameter file what it does
appledrm.split_alloc dptxep.c Answers DPTX apcall 21 (SET_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_modeset iomfb_v14_7*.c Runs the macOS two-pipe mode set and frames for such a mode: MAIN = the pipe owning the link, HELPER = the other dcpext (started on demand).
appledrm.split_dock_main dcp-fabric.c Lets a Thunderbolt display take the HDMI pipeline (dcpext0) while it is free, because the merge only works with dcpext0 as MAIN.

How 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):

  1. A361 reads the full timing.
  2. A362 registers the half timing on both pipes: the eight horizontal u32 and the pixel clock halved, IDs + 10000.
  3. MAIN's A411 blocks until HELPER's A411 has finished. The host relays MAIN's DSC setup (A366/A364/A367/A369 → A365/A368/A370) from MAIN's tiling poll and runs HELPER's half on a worker.

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-subsystem spans 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_modeset off, 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_SELECT 0, 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:

  • 5120x2160@120 and @179.985 over the Mac's HDMI port, and over an Anker TB5 dock (DisplayPort) with split_dock_main
  • enabled from boot via /etc/modprobe.d; the helper pipe is started on demand
  • 120 ↔ 180 Hz switches while running
  • DPMS off/on, including several cycles in a row
  • the dock's link retrain ~3 s after plug-in (the split is rebuilt by itself)
  • compositor overlay: 200+ fps, ~5 ms frame time, no seam at x=2560
  • 30+ minutes without a refused or late swap

Not tested:

  • suspend: the kernel refuses sleep while an external link is up, before and after this change
  • other boards (J516S, T6031/T6034)
  • audio over the dock
  • a monitor whose 5K mode needs no DSC
  • the reversed roles (helper = dcpext0) are known not to work: the firmware accepts everything, but the screen stays black

Open questions for reviewers

  • Should split_dock_main be a placement policy (prefer the hybrid when a split mode is requested), rather than a switch?
  • Should colour mode 94 (what macOS uses) be preferred over 93 for the split IDs?
  • The 20 s long_calls budget 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

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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
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

Comment on lines +3180 to +3181
if (READ_ONCE(mv14->failed) || READ_ONCE(hv14->failed))
return false;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread drivers/gpu/drm/apple/iomfb_v14_7.c Outdated
Comment on lines +3246 to +3247
if (wret == -ETIMEDOUT) {
dev_warn_ratelimited(mv14->dev, "split: main swap %u not complete in 500 ms\n", id);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread drivers/gpu/drm/apple/iomfb_v14_7.c Outdated
Comment on lines +3258 to +3260
if (ret) {
dev_warn_ratelimited(mv14->dev, "split: swap refused: %d\n", ret);
return false;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread drivers/gpu/drm/apple/iomfb_v14_7.c Outdated
Comment on lines +3655 to +3656
/* As macOS: MAIN's request goes first. */
msleep(30);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread drivers/gpu/drm/apple/iomfb_v14_7.c Outdated
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)) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment on lines +3851 to +3852
WRITE_ONCE(hv14->split_peer, v14);
WRITE_ONCE(hv14->split_role, DCP_V14_SPLIT_HELPER);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread drivers/gpu/drm/apple/iomfb_v14_7.c Outdated
Comment on lines +3951 to +3953
u64 rate = (u64)mode->hdisplay * mode->vdisplay * drm_mode_vrefresh(mode);

return rate > DCP_V14_SPLIT_PIPE_RATE && !(mode->hdisplay & 1);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

JeppeBylov and others added 2 commits November 11, 2026 17:49
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
@JeppeBylov

Copy link
Copy Markdown
Author

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
@JeppeBylov

Copy link
Copy Markdown
Author

Pushed 1aa7692, a fix for booting with the dock already connected (hotplug after boot already worked).

Problem: HDMI presence stays DCP_FABRIC_SETTLING for DCP_HDMI_HOLD_MS (10 s) after the first sample at boot, which is exactly when the dock's tunnel comes up. split_dock_main only accepted DCP_FABRIC_ABSENT, so it never fired at boot. The dock stayed on dcpext1 (2d6c00000), where the split can't merge.

Fix: while presence is settling, read the HDMI HPD GPIO directly and treat a low line as an empty port. dcp_typec_route_available() also refuses a settling pipe, so the override does the same idle checks itself: no active route, no dcp_typec_safe_held(), and !dcp_typec_route_busy().

Tested on aurora16 (7.2.11-16) on a J514S with a G75F behind a TB dock connected at boot:

  • the dock takes 2d2c00000.dcp ("split_dock_main: tunnel dpin0 takes this pipeline instead of 2d6c00000.dcp") at ~17 s
  • the split comes up at 5120x2160@120, then @180
  • it rebuilds correctly after the dock's link retrains

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants