Skip to content

j613: second external display with the lid closed (internal pipe on the left-front port) - #127

Open
GehrmannSolutions wants to merge 6 commits into
omacom:integration/aurorafrom
GehrmannSolutions:fay/aurora-j613-clamshell-on-integration
Open

GehrmannSolutions wants to merge 6 commits into
omacom:integration/aurorafrom
GehrmannSolutions:fay/aurora-j613-clamshell-on-integration

Conversation

@GehrmannSolutions

@GehrmannSolutions GehrmannSolutions commented Oct 15, 2026 •

Copy link
Copy Markdown

Follow-up to #114 / #103, as suggested in #103: the lid-closed dual-display set on top of the merged J613 dcpext series.

What

On the J613 (MacBook Air 13" M3) with the lid open, one external display runs on dcpext and a second one waits (the log line and takeover from #114). macOS does more with the lid closed: the panel is off, so the internal DCP's pipe drives a second external display on the left-front USB-C port (IOMFBIntDcpUsedForExtWhenLidClose). This series does the same:

  • Patch 1 (drm/apple): a typec-routes container with no enabled route no longer takes the internal panel down, for every DCP except the external ones. A route that fails to register on the panel's DCP is disabled, and the panel goes on without it; so is the route of a panel DCP whose probe fails for good, so the left-front port never waits for it. Needed because patch 2 adds a route that stays disabled when the gate does not enable dcpext.
  • Patch 2 (DT): the internal DCP gets a disabled Type-C route to the left-front port: ATC1's DP PHY and crossbar, crossbar state 0 (dispext0,0, from the J613 ADT), DPTX target 1, and a second endpoint on the front connector.
  • Patch 3 (display gate): enables that route in the same changeset as dcpext, under the same checks and the same boot-loader marker, with one more log line. No dcpext, no route.
  • Patch 4 (drm/apple): follows the lid like macOS: tells the panel's IOMFB the lid state (A475), powers the panel off on close (A472), and reports eDP-1 disconnected while the lid is closed (also when booting closed, so the panel is never lit). Lid changes run from a freezable work item, so after a wake by opening the lid they apply only once DRM has restored its state. Only a lid switch with a parent device counts (not a uinput clone). Without this, the firmware takes the pipe for an external display at every panel power-on and the next swap times out.
  • Patch 5 (drm/apple): with the lid closed and the panel and its CRTC off, the internal pipe counts as a free pipeline for the left-front port. The DPTX/DPAV endpoints are started as on the external processors, the link is connected as on dcpext, and the 14.7 session handles the description, mode set and swaps of a Type-C display. When the lid opens, the display gives the pipe back first, then takes dcpext if it is free, or waits with the usual log line.
  • Patch 6 (drm/apple): moves the left-front display from dcpext to the internal pipe only when a left-back display needs dcpext: a display plugged into the left-back port with the lid closed, the lid closing while a left-back display waits, and boot with the lid closed. A display that takes over a pipeline it waited for logs the display on USB-C Left-front takes over this pipeline.

Other machines and a J613 without the new route (no marker, apple_t8122_display.dcpext=0, older DT) are unchanged. Like #114, the series needs the boot loader to set apple,t8122-dcpext-firmware-ready (the m1n1 half in #103).

Tested

Kernel: this branch on integration/aurora 54f78d60c. W=1 build of every commit with no new warnings in drm/apple and soc/apple (the review fixes: compile-checked per commit, full W=1 build of the head); checkpatch clean apart from the trailers. DTB built from this branch, m1n1 with the marker. Test setup: a 3440x1440 monitor (through its USB-C hub) on the left-back port, a 1080p portable on the left-front port, and Hyprland.

  • Lid open: each port alone; second display waits with the log line and desktop hint; takeover both ways when the active display is unplugged (about 3 s). ✅
  • Lid close with both displays: panel off at once, left-front display moves to the internal pipe (about 2.5 s), left-back display stays on dcpext. Lid open: panel back, the left-front display waits again. ✅
  • Lid closed: unplug and replug either display, each comes back on its pipe without waiting. ✅
  • Boot with the lid closed: panel never powered, both external displays lit (internal pipe after about 12 s). ✅
  • Suspend with the lid closed, wake by opening the lid: panel back, both external displays restored, then the left-front display gives the pipe back and waits as with the lid open, the left-back display keeps dcpext. 2 of 2 runs with the review fixes, with no Failed to resume any more. Before the fixes (same branch): 2 runs, see the notes below.
  • Five lid-open s2idle cycles in a day of normal use on this branch (before the review fixes), all resumed cleanly.

Review

I ran a Claude Code review over the series (GehrmannSolutions#2). Fixed in the patches they belong to: the lid work could run during resume, before DRM restored its state, which made drm_mode_config_helper_resume fail with -22 for every CRTC (that was the error we had taken as harmless); the lid-close black frame could race a commit's swap over which framebuffer is on screen; a Type-C display on the internal pipe had the panel's framebuffer-size rule instead of the external one; a failing panel-DCP probe left the route enabled; any SW_LID device (also uinput) set the lid state; a refused panel power-on was only logged at info level. Left for a follow-up: phys/mux-controls sit on the static dcp node even while the route is disabled (fw_devlink then makes the panel wait for phy-apple-atc, which is loaded on J613 anyway for USB), and some duplicated route helpers in dcp-fabric.c.

Known, not changed here

  • After a resume, whichever display is ready first gets dcpext (seen once before the fixes). A monitor whose own hub is still asleep can lose dcpext to the other display, which then runs while the first one waits. Unplugging the running display hands dcpext over as usual.
  • Seen once on this branch, after a lid-closed resume: the firmware described the left-back monitor through DCPDPService instead of DCPDP13Service, without link training, with a short TimingElements (48 kB instead of 281 kB) and no usable mode. The driver rejected it (failed to parse modes) and the firmware dropped the display. After that dcpext could not light it again, even after a replug (the link went down in the middle of the mode set), until a reboot. The same run on the series before the rebase passed right after. That resume also ran into the -22 restore failure fixed above, so the dcpext display was not restored by DRM and was set up again from scratch; that may well have been the trigger. Not seen since the fix (2 runs), and I collect the rejected description if it happens again.
  • No USB wake from s2idle on this machine (xhci wakeup disabled), so a closed laptop wakes only by opening the lid.
  • Userspace: Omarchy suspends on lid close as soon as the external display count is zero for a moment, which can happen during the move to the internal pipe (Lid closed with external displays: logind suspends as soon as the display count drops to zero for a moment omarchy#15368). We run a small user service that suspends a closed laptop only after ten seconds without an external display.

(Code, tests and this text by TARS, my Claude Code agent, with me plugging displays and reviewing; Signed-off-by is mine.)

🤖 Generated with Claude Code

https://claude.ai/code/session_017YMqWu7A3VwEYtHaA1TusK

GehrmannSolutions and others added 6 commits November 12, 2026 12:12
…anel

On J613 the internal DCP gets a Type-C display route to the left-front
USB-C port in the device tree: with the lid closed and the panel off,
its pipe can drive a second external display. The route
stays disabled there; the display gate enables it at boot together with
dcpext. When the gate does not (no boot-loader marker, a refused check,
apple_t8122_display.dcpext=0, the 26.6 profile that includes the J613
device tree, or a kernel that does not know the route), the processor
has a typec-routes container with no enabled route, which made route
registration fail with "Type-C route container is empty" and took the
panel down with it.

- A typec-routes container without an enabled route means no route, for
  every processor but the external ones (T6030 and T8122 dcpext, whose
  routes are enabled together with them and must register).
- On the internal panel's processor (14.7 DCPLink session), a route that
  fails to register is disabled in the device tree, as for a T6030
  external processor, and the panel goes on without it.
- On J613, a probe of the panel's processor that fails for any other
  reason (a deferral is not a failure) disables the gate-enabled route
  too, so that the left-front port does not wait for it forever.

Device trees whose routes are enabled are unchanged.

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marius Gehrmann <marius@gehrmann.solutions>
[Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved a
 conflict with the H17P probe path in dcp_platform_probe(); the T8140
 dcpext (H17P), an external processor added since, is excluded from the
 empty-container exemption too, so it still fails without an enabled
 route; message reworded; Claude-Session trailer dropped]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
[TARS: ported to integration/aurora 7.2.11 (4fdd0fc): dcp.c reorganised, so
 the dcp.c hunks are applied by hand; uses integration's dcp_is_v14_7_panel()
 instead of adding dcp_is_v14_panel()]
(cherry picked from commit 93258ff)
…ront port

With the lid closed and the panel off, the internal DCP's pipe can
drive a second external display next to dcpext. In the J613 ADT
the internal DCP is display-crossbar ufp0, port 0: crossbar state 0
(dispext0,0), here on ATC1's crossbar, with DPTX target 1 as dcpext's
route to the same port.

Give the internal DCP that Type-C display route to the left-front port
(typec1): ATC1's DP PHY and display crossbar as typec0, crossbar state 0,
DPTX target 1, and a second endpoint on the front connector's port@3,
one per pipeline as on the M1 Pro/Max laptops.

The route stays disabled, like dcpext and its routes; the display gate
enables it in the same changeset, behind the boot loader's marker. The
display driver uses it only while the lid is closed and the panel and
its CRTC are off.

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marius Gehrmann <marius@gehrmann.solutions>
[Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; message
 and device tree comment reworded; Claude-Session trailer dropped]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
(cherry picked from commit 4c643f7)
…cpext

The J613 device tree gives the internal DCP a disabled Type-C route to
the left-front port, the second pipeline for a display there with the
lid closed. It leads to a port that dcpext drives too, and the display
driver only uses it while dcpext runs alongside, so it is usable exactly
when dcpext is.

Enable the internal DCP's routes in the changeset that enables dcpext,
its routes, DARTs, mailbox and crossbars, with one more log line:

  enabled the Type-C routes of /soc/dcp@28ec00000 with it, for a
  second external display with the lid closed

As for the rest of that changeset, the routes must be described and
still disabled, and every display crossbar the internal DCP switches
must be one of dcpext's, which the changeset enables; otherwise dcpext
is refused and nothing is enabled. Without dcpext (no marker, other
firmware, refused checks, apple_t8122_display.dcpext=0) the route stays
disabled and the internal DCP has no route. Device trees without such a
route are unchanged.

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marius Gehrmann <marius@gehrmann.solutions>
[Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff;
 Claude-Session trailer dropped]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
(cherry picked from commit 1991adf)
…l off

On J613 the internal panel's pipe can also drive a display on the
left-front USB-C port, but only once its panel is off: the pipe's first
DPTX core drives the panel otherwise. Once told the lid is closed, the
firmware takes the pipe for a second external display at every panel
power-on and swallows each panel frame; a swallowed frame never
completes, the swap times out and the DCP is gone until reboot.

For the internal pipe that has a Type-C route (dcp->clamshell), tell
the panel's IOMFB the lid state, power the panel off while the lid is
closed, and treat a closed lid as a panel that is not there:

- Follow the lid switch (SW_LID, the SMC's on Apple laptops) with an
  input handler, registered once the panel has started. Only a switch
  with a parent device counts, not one made from user space (uinput).
  Lid changes are applied from a freezable work item: opening the lid
  wakes the system, and the change must wait until the DRM device has
  restored its state.
- Tell IOMFB the state with A475 (one u32: 1 open, 2 closed): the real
  state from the start, then every change. IOMFB hands it to its DPTX
  power controller at each panel power-on, which refuses anything else.
- Closing the lid powers the panel off at once (A472(0)), its CRTC on or
  not, after a black frame while IOMFB still has the lid open. Presents
  are serialized, so that black frame and a commit's swap never mix up
  which framebuffer is on screen. From
  A475(2) on no frame goes to the panel; one that raced with the lid is
  dropped under the session lock, not sent.
- While the lid is closed the panel's connector reads disconnected, from
  boot on if the lid is closed then, so compositors switch its CRTC off
  and never light it. A CRTC switched on anyway leaves the panel off and
  gets no frames (one warning).
- When the lid opens IOMFB is told first (A475(1)), then a panel whose
  CRTC is on is powered on again (A472(1), colour matrix resent), and
  its connector reads connected again.
- Answer what the firmware sends around those power changes: D2, D591
  and D574 as on an external processor, D593 (four bytes once after
  power-on, meaning not known) with nothing.

Other machines, and J613 without the Type-C route, are unchanged. The
next patch lets the free pipe drive the display on its route.

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marius Gehrmann <marius@gehrmann.solutions>
[Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved a
 conflict with the H17P power-on path in dcp_poweron(); message and
 comments reworded; Claude-Session trailer dropped]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
[TARS: ported to integration/aurora 7.2.11 (4fdd0fc): presents already run
 under v14->lock there (dcp_v14_present_locked()), so present_lock is gone and
 the lid's black frame is sent under v14->lock together with A475; a frame
 not sent with the lid closed is handed back as *retired; poweron/poweroff
 keep integration's backlight DPMS; dcp.c hunks applied by hand; the
 clamshell unbind releases the cable (integration disables the internal
 processor's Type-C work without releasing it)]
(cherry picked from commit cce117a)
…osed

With the lid closed the panel is off, and the internal DCP's pipe,
whose first DPTX core is free then, can drive a second external
display on the left-front USB-C port. When the lid opens, the pipe
goes back to the panel. Do this on J613, where the internal pipe has a
Type-C route to that port (dcp->clamshell).

Fabric. The internal pipe counts as busy unless the lid is closed and
the panel and its CRTC are off (dcp->clamshell_free). While it is free
it ranks as a plain pipeline, before dcpext: with the panel's connector
disconnected, aquamarine (Hyprland) gives a display coming up on the
left-front port the lowest free CRTC it can use, the internal pipe's,
and the route follows that pairing (routed to dcpext instead, every mode
failed the atomic check, and the panel found its only CRTC taken on lid
open). When the pipe becomes free it takes a display left waiting, as
any pipeline that is let go of (dcp_typec_pipeline_changed()). When the
lid opens, that display lets go of the pipe first (powered off, HPD
dropped, route taken down), then IOMFB is told; the display goes to
dcpext if dcpext is free, or waits again with the usual log line.

Lid open, order. The panel's connector comes back once the compositor
switched the pipe's CRTC off, or two seconds later, and at once if the
CRTC was off already. A left-front display that leaves the pipe for a
free dcpext reads disconnected until then (at most 3 s) and is reported
200 ms after the panel, so that the compositor gives the panel its CRTC
back and the display dcpext's.

Session. Once the panel has started, the DPTX endpoint is started for
the route, and the DPAV controller endpoint with it, as for the T6030
external processors. The DPTX RemotePort unit 0 (core 0, crossbar
dispext0,0) is DPTX port 0; the panel's own DPTX port is never bound.
The link is connected as on the T8122 dcpext (dcp_ext_dptx_607()), the
same firmware image. Call 25, which the firmware sends to each
RemotePort when the lid state changes, is acknowledged quietly. While
the route is active (v14->typec) the session handles the display
description as on an external processor; the mode is set with A472(1)
before A411 (with A411 alone the mode is set, the pipe not enabled),
its CRTC off powers it off (A472(0)), swaps are sized to its mode (a
larger framebuffer is shown from its top-left, as on an external
processor) without notch rows or the panel's colour matrix, and a
refused swap does not end the session. When the route is taken down, the
panel's one mode is the pipe's catalog again.

Other machines are unchanged, and so is J613 without the route.

Link: AsahiLinux#607
Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marius Gehrmann <marius@gehrmann.solutions>
[Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved a
 conflict with DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE in dptxep.h; call 25
 still gets -EOPNOTSUPP on the H17P external processor, as any call it
 does not handle; message and comments reworded; Claude-Session trailer
 dropped]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
[TARS: ported to integration/aurora 7.2.11 (4fdd0fc): the panel's own port
 is skipped in dptxport_prepare(), where 7.2 binds DPTX services; call 25 as
 before; dcp_ext_dptx_607() keeps integration's fw_14_7_endpoints check;
 detect keeps dcp_connector_connected(); swaps keep lockdep and backlight
 present state; the lid's black frame (under v14->lock) skips a Type-C
 display; poweron does not restore the panel backlight while the pipe drives
 a Type-C display; fabric and dcp.c hunks applied by hand]
(cherry picked from commit cdd32c0)
…n needed

On J613 with the lid closed, dcpext can drive either USB-C port and the
internal pipe only the left-front one. A display on the left-front port
can hold dcpext while a display on the left-back port, which only dcpext
can drive, waits, although the internal pipe is free.

Move the left-front display to the internal pipe when that is needed,
and only then: as any move, an unplug and replug (its link goes down
first, which also tells the old CRTC's late power-off to leave the next
display alone), then the other display gets the dcpext it let go of. If
the internal pipe cannot take the moved display after all, that display
waits and the other one is driven. Three cases need it:

- A display arrives on the left-back port with the lid closed while
  dcpext drives the left-front one (dcp_typec_clamshell_make_room()).
- The lid closes while a left-back display waits for dcpext, which
  drives the left-front one (dcp_typec_clamshell_serve_waiting()).
- Boot with the lid closed, before a compositor has paired the
  connectors: a left-front display that took dcpext first moves to the
  internal pipe, where aquamarine will pair it, and a waiting display
  then takes dcpext (dcp_typec_clamshell_settle()). Whether a
  compositor runs is looked up from any pipeline's CRTC: the dual-stream
  helper finds no DRM device on J613.

Otherwise a lid close moves nothing, so the left-front display stays
lit. (Moving it at every lid close frees dcpext early too, but blanks it
for about four seconds each time, for nothing when no second display
comes.)

A display that takes a pipeline it waited for says so ("the display on
<connector> takes over this pipeline").

Tested on J613 (MacBook Air 13" M3, display firmware 14.7) with a
1080p display on the left-front port and a 3440x1440 one on the
left-back port, Hyprland, and a user service that suspends a closed
laptop only after ten seconds without an external display: lid closed
and opened five times with both displays, both lit each time with the
lid closed and the panel back 33-173 ms after opening; a left-back
display waiting with the lid open gets dcpext on lid close; a display
arriving with the lid closed; boot with the lid closed (panel never
powered); suspend with the lid closed and wake by opening it.

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marius Gehrmann <marius@gehrmann.solutions>
[Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved
 conflicts with the H17P SAFE-phase mux callbacks and the H17P -ENOSPC
 check in dcp_typec_route_set() (dcp-fabric.c); message and comments
 reworded; Claude-Session trailer dropped]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
[TARS: ported to integration/aurora 7.2.11 (4fdd0fc): integration reports a
 port without a pipeline as -ENOSPC on every DCP, so the H17P-only -ENOSPC
 before dcp_typec_port_busy() is gone; the new helpers sit after
 integration's dcp_typec_port_busy()]
(cherry picked from commit d39a981)
@GehrmannSolutions
GehrmannSolutions force-pushed the fay/aurora-j613-clamshell-on-integration branch from 5bafd20 to 2688004 Compare November 12, 2026 13:13
@GehrmannSolutions

Copy link
Copy Markdown
Author

Rebased onto current integration/aurora (bed544b, the tree packaged as linux-aurora 7.2.11.aurora16) and retested on a J613. New head: 268800455b3b; checkpatch is clean apart from the Co-Authored-By trailer warnings.

Result: a J613 with the lid closed drives two external displays.

  • T34w-30 at 3440×1440 runs on dcpext, Left-back.
  • M14 at 1920×1080 runs on the internal pipe, Left-front.
lid closed, panel off: the internal pipe drives the display on USB-C Left-front
the display on USB-C Left-front takes over this pipeline
internal pipe: Type-C display mode "1920x1080": 60 148500 …

With the lid open, aurora16's "waits for a free external display pipeline" behaviour is unchanged: the second display waits, and it lights up when the first is unplugged.

Rebase notes:

Ordering matters for the DT patch (2/6). I booted the packaged aurora16 kernel once with this series' t8122-j613.dtb. Without 1/6, apple-dcp rejects the internal DCP (Type-C route container is empty, probe -22), and the panel falls back to simple-framebuffer. So 2/6 must not land or ship ahead of 1/6.

The J613 test used a localmodconfig build of the series as an extra Limine entry. The boot.bin was rebuilt once with the series' DTB and restored to the packaged one afterwards.

Disclosure: the rebase, the analysis and this text were made with an AI coding agent (Claude Code). The test ran on my J613, and I checked both displays with the lid closed.

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.

1 participant