Skip to content

M3 Max (J516C): external displays over USB-C and HDMI - #292

Open
digital-brew wants to merge 15 commits into
omacom:integration/aurorafrom
digital-brew:t6031-external-displays
Open

digital-brew wants to merge 15 commits into
omacom:integration/aurorafrom
digital-brew:t6031-external-displays

Conversation

@digital-brew

@digital-brew digital-brew commented Oct 30, 2026 •

Copy link
Copy Markdown

Problem. On the 16-inch M3 Max MacBook Pro (J516C, T6031) no external display works: the USB-C ports never get a display pipeline, and the HDMI port has no connector at all. The T6030 (M3 Pro) external display support does not reach T6031: its four dcpext processors and the ATC display crossbars are not described, several T6030 paths check apple,t6030 alone, and the J516C tree describes only ATC0..2, not the ATC3 PHY behind the HDMI port.

Fix. Bring T6031 onto the T6030 external display path, then describe the J516C HDMI port.

Patches

  1. arm64: dts: apple: t6031: describe dcpext0..3 and the ATC display crossbars.
  2. soc: apple: display-gate: hand over T6031 external display processors. Opt-in, behind apple_t6031_display.dcpext=1.
  3. drm: apple: run T6031 external processors on the J516C 14.7 image.
  4. arm64: dts: apple: t6031: link each dcpext to its PMP DISPEXT request.
  5. drm: apple: find the T6031 display subsystem for a native dcpext.
  6. drm: apple: check the T6031 dcpext floor on its own dispext<N>_fe.
  7. apple: put the T6031 dcpext power floor on its CPU domain. A T6031 dispext<N>_fe state does not hold the PMGR minimum-state field.
  8. drm: apple: take a T6031 dcpext timing's colors from ColorElements. The J516C 14.7 firmware publishes every external timing with an empty ColorModes array, so every mode was rejected.
  9. drm: apple: clock the T6031 crossbar's direct DP output on link config. Without it every mode set failed with IOAVVideoInterface prepareLink() failed.
  10. apple: run Thunderbolt DP tunnels on T6031. The T6030 source map is assumed to hold on T6031.
  11. thunderbolt: keep T6031 DP tunnels out of bandwidth allocation mode. With allocation mode on, the tunnel reported 0 Mb/s consumed and the sink dropped the stream after about 1.5 s.
  12. arm64: dts: apple: j516c: describe the HDMI port. From the J516C ADT: ATC3 and its crossbar at the T6030 addresses, the J514S/J516S SMC power pins, and HPD on AOP GPIO 42 (47 on the J514S/J516S). dcpext0 takes the fixed route as a hybrid pipeline. The atcphy3 alias lets m1n1 copy the ATC3 calibration.
  13. arm64: dts: apple: j516c: keep dcpext1..3 from the display gate. Stopgap, see below.
  14. drm: apple: rebuild a T6031 dcpext catalog when its colors change. Review follow-up to patch 8: the catalog is rebuilt on either publication order, replacement and withdrawal, and the shared-colors fallback is T6031-only.
  15. drm: apple: wait for a dcpext display's timings before its first swap. Fixes the first-mode failure described below.

Depends on m1n1. The handoff comes from an m1n1 change that is not in this PR: kboot_display_handoff: hand off the T6031 external display processors, on omacom/m1n1 bc79acc4, armed by chosen.asahi,t6031-dispext=1: omacom/m1n1#21.

Why patch 13. On this J516C only dcpext0 brings up a display. dcpext1..3 time out in the DPTX remote port, on USB-C and, for dcpext3, on the HDMI route too:

AppleDCPDPTXRemotePort (chan:1) command type 0xc0 tag 0x0300 timed out after 1000 ms
dcp_dptx_connect: failed to assert Type-C HPD: -110
DPTXPort: firmware reports link fault 22 on target 0:3

With HDMI on dcpext0, the allocator offers a hybrid pipeline to USB-C last (dcp_fabric_score), so every USB-C display landed on a broken processor. Marking dcpext1..3 "fail" keeps them from the gate, and the DRM device waits only for available processors. This limits the J516C to one external display until dcpext1..3 are understood. I have not found why they fail.

Testing (J516C, iBoot 10151.140.19.700.2, OS firmware 14.7, system firmware reported as unknown; m1n1 bc79acc4 + the handoff change; boot with apple_t6031_display.dcpext=1; Hyprland):

  • Base: tested on integration 3bdf11d3 (aurora11) + this series. Rebased onto 12be0475 without conflicts; not re-run on that base.
  • USB-C, directly and through a USB4 dock: pass. A 4K-class DSC monitor at 60 Hz, and at 120 Hz with DSC on (firmware: DSC=YES). All three ports passed with patches 1–11; with patches 12–13, one port was re-run.
  • HDMI: pass, same monitor and modes.
  • Moving the display from USB-C to HDMI without a reboot: pass. dcpext0 releases the Type-C route and takes the fixed route.
  • Coldplug and hotplug, USB-C and HDMI: pass.
  • No DPTX timeouts, link faults or flip failures on dcpext0 in these runs.
  • Not tested: two external displays (not possible with patch 13), the 14-inch J514C, system sleep with an external display, HDMI audio (not described yet).
  • KUnit (arm64, qemu): 114/114 pass, including the new dcp_v14_shared_colors (patch 14) and dcp_v14_normal_mode (patch 15) suites, under W=1 and PROVE_LOCKING.

First-mode failure, fixed by patch 15. When an external display's first mode on a new connection was a DSC one (3840x2560@60), the first swap was dropped and the processor stopped until reboot:

IOMFB: swap_submit_dcp: swallowed swap ID 1 as timinsg are not enabled for External Display
DCP call 0x41343037 failed: -110; recovery requires a reboot
native DCP flip failed -110; buffers pinned until reboot

Cause: after A472(1) the firmware starts the display's timings in a deferred run mode change, then sets the boolean property NormalModeActive (D104) to 1. With a slow first mode (2560x1600@60) that change ends inside A472; with the DSC mode it ends about 20 ms after A472 replied, and a swap sent in that gap is dropped. Later swaps sent early are held rather than dropped, which is why only a session's first mode failed. With dynamic debug on the callbacks:

8.597  A472(1)
8.599  D104 NormalModeActive = 0
8.613  A472 replied; A406, A407 sent
8.614  swallowed swap ID 1 as timings are not enabled
8.633  switch to normal mode succeeded; D104 NormalModeActive = 1

Patch 15 waits after every power-on, under the RPC lock, until NormalModeActive is 1 (handling queued callbacks as dcp_v14_idle() does, 500 ms limit, no wait if the firmware never reported it). On the J516C through a USB-C dock with the 3840x2560 display, tested on the aurora11 base with patches 1–11 + 15: six boots (reboot, power cycle, hotplug after login) each set 3840x2560@60 first and completed its first swap in about 18 ms; 20 mode sets in all, 8 at 120 Hz with DSC, all succeeded. The wait took 10–21 ms. Without it, 5 of 5 boots lost the session.

Related: #61 (M3 Max bring-up), #289 (G15 firmware pin on the J516C, needed for the GPU on this Mac; separate change).

🤖 Generated with Claude Code

DB and others added 13 commits October 30, 2026 16:37
…ssbars

Add the M3 Max's four external display processors, their mailboxes and
firmware DARTs, all disabled, at the addresses, interrupts and power
states of the J516C ADT (dcpextN, dart-dcpextN), translated through
/arm-io ranges. The internal DCP's ADT values translate to exactly what
t6031-dcp.dtsi already has, which checks the translation.

  dcpext0 0x3_06c00000  mbox IRQ 1068..1071  DART 0x3_0530c000 IRQ 1047
  dcpext1 0x4_4ac00000  mbox IRQ 1104..1107  DART 0x4_4930c000 IRQ 1083
  dcpext2 0x4_4ec00000  mbox IRQ 1140..1143  DART 0x4_4d30c000 IRQ 1119
  dcpext3 0x2_cac00000  mbox IRQ 1176..1179  DART 0x2_c930c000 IRQ 1155

Each processor's ADT power gate is DISPEXTn-CPU-V, whose parents are
DISPEXTn_CPU and DISPEXTn_FE, as DISP-CPU-V is the internal DCP's. So
each sits on ps_dispextN_fe, as the internal DCP sits on ps_disp_fe.

The node shape is T6030's dcpext: Type-C routes to ATC0..2, disabled
until the display gate enables their processor. The compatible keeps
apple,t6030-dcpext as a fallback so the display driver takes its T6030
external path. apple,typec-mux-indices is N for dcpextN because the
ADT's display-crossbar0 lists its upstream ports as dispext0..7 in
order; this is not yet confirmed on hardware.

Add the three ATC display crossbars (ADT atcN-dpxbar, atc-dpxbar,t602x)
at the T6030 addresses. With the t6020 compatible their probe writes no
register.

Give the PMP's pmp-dispext0..3 requests apple,always-on and
apple,defer-startup-ack, as T6030's. They stay disabled, and the report
driver seeds only available entries, so nothing changes until the gate
enables one.

Add the J514C/J516C Type-C port@3 endpoints for all four processors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
Enable the dcpext gate on T6031 behind its own switch,
apple_t6031_display.dcpext=1, which is off by default. Without it, or
without the boot loader's handoff markers, every processor and its
Type-C routes stay disabled.

The gate's dcpext path assumed T6030's power order: the processor on
dispextN_cpu, the child of dispextN_fe. On T6031 the processor is on
dispextN_fe, the child of dispextN_cpu. Resolve the chain in the SoC's
order, and check the mailbox, firmware DART and display DART against the
processor's own power state, which also gets the active floor (its
parents follow it).

The boot loader's markers stay the T6030 ones
(apple,t6030-dcpext-memory-ready, apple,t6030-dispext-handoff), so the
display driver's checks need no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
A T6031 dcpext loads the same DCP image as its internal DCP
(F493F03E-CC40-3D89-9A08-5F259BA2B246, per the J516C ADT's iop nubs).
Accept that image on apple,t6031-dcpext nodes only; every other
external processor still needs the T6030 image.

Also give T6031 the T6030 display-crossbar programming. Both use the
same dispext pipes and the same t602x crossbar, and the T6030 path
handles dispext bits 0..3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
The display gate needs the pmp-dispextN report entry to have a phandle,
and dtc emits one only for referenced nodes. Name the entry in
apple,pmp-report, as T8122 does; the gate sets the same value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
dcp_t6030_ext_native() looked only for apple,t6030-display-subsystem, so
a T6031 dcpext the gate had handed its display DART fell back to the
manual diagnostic path and never joined the DRM device. Look up the
subsystem of the processor's own SoC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
dcpext_check_power() required the processor's power domain to be
dispext<N>_cpu, as on T6030. On T6031 the dcpext sits on dispext<N>_fe,
whose parent is the CPU domain, and the display gate puts the active
floor there, so startup was refused with -EINVAL on attach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
A T6031 dispext<N>_fe power state does not hold the PMGR minimum-state
field: with apple,min-state written it still reads 0xff, as the
internal display's FE state does, while the CPU states keep it. Floor
the CPU domain in the display gate (the same node as before on T6030),
and have the driver check the floor there, reached through the FE
domain the T6031 dcpext sits on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
The J516C 14.7 firmware on a T6031 external processor publishes every
timing with an empty ColorModes array; the display's color modes are
only in ColorElements, and each timing names the ones it excludes in
UnsafeColorElementIDs and DSCRequiredColorElementIDs. Every timing so
had no color mode, parse_mode() rejected all of them, and the
connector stayed disconnected with HPD asserted.

When an external timing has no color mode of its own, select one from
the published ColorElements, less its excluded IDs, with the external
SDR ranking (full-range SDR RGB, 8 or 10 bits).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
The display crossbar runs T6031 in its T6030 mode (t6030_dpin): a route
is selected on activate, and its clocks come up only from the DCP's
DidChangeLinkConfiguration call, through dcp_direct_crossbar_link().
That function returned early on anything but apple,t6030, so on T6031
the direct DP PHY output stayed unclocked ('T602X route 0, clocks
disabled'), and every mode set failed in the firmware with
'IOAVVideoInterface prepareLink() failed'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
T6031 (M3 Max) has the T6030 (M3 Pro) display fabric: T8122-generation
ATC PHYs, a crossbar in t6030_dpin mode and dcpext processors bound as
apple,t6030-dcpext. Its DP IN tunnel path was still disabled, though:

 - the Thunderbolt driver leaves DP IN off on SoCs missing from
   apple_dpin_qualified_soc, and the T6031 ACIO matched only the t8103
   fallback (M1 DP IN policy);
 - the DCP fabric and the ATC PHY pick the T6030 DP IN pixel clock path
   and its source map on of_machine_is_compatible("apple,t6030") alone.

Qualify T6031, give its ACIO the T6030 DP IN policy while it keeps the
t8103 boot sequence (its DT node has no MxWrap region), and take the
T6030 clock path on T6031 too. The T6030 source map (crossbar sources
0 and 2 feed DP IN0/1) is assumed to hold on T6031; untested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
The T6031 (M3 Max) DCP DPTX firmware drives a DP tunnel like a plain DP
link: it never requests bandwidth through the DP IN adapter's allocation
registers. With allocation mode enabled the tunnel reported "consumed
bandwidth 0/12960 Mb/s", the mode set completed and the sink (a DisplayPort
monitor behind a USB4 dock, DP OUT x2 HBR3) dropped the stream
about 1.5 s later (DPTX INACTIVE_SINK_DETECTED). The same setup works
with thunderbolt.bw_alloc_mode=0.

Add a TB_HOST_DP_NO_BW_ALLOC host policy that takes the bw_alloc_mode=0
path for that host's tunnels only, and give T6031 its own policy: the
T6030 flow plus the new flag. T6030 keeps its current behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
The J516C's HDMI port is ATC3 used as a DisplayPort-only PHY behind
display-crossbar0, feeding an on-board DP-to-HDMI converter, as on the
J514S/J516S (t603x-j514s-j516s-hdmi.dtsi). The T6031 tree described
only ATC0..2, so the port had no connector.

From the J516C ADT:
- atc-phy3 and atc3-dpxbar sit where the T6030's do (0x1303000000,
  0x130304c000);
- dp2hdmi-gpio0: hdmi_pwr_en and dp2hdmi_pwr_en are SMC gP0f and gP1a,
  the J514S/J516S pins;
- atc3-hdmi: function-hdmi_hpd is AOP GPIO 42 (47 on the J514S/J516S).

dcpext0 takes the fixed route and stays a hybrid pipeline, lent to a
USB-C port while no HDMI display is attached. The atcphy3 alias lets
m1n1 copy the ADT's atc-phy3 calibration; without it the PHY reports
"Calibration tunables missing, DisplayPort disabled".

HDMI audio (DPA1 over the SIO) is not described yet.

Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the J516C only dcpext0 brings up an external display. dcpext1..3
time out in the DPTX remote port on USB-C (HPD assert, link
configuration) and dcpext3 does the same on the HDMI route:

  AppleDCPDPTXRemotePort (chan:1) command type 0xc0 tag 0x0300 timed out after 1000 ms
  dcp_dptx_connect: failed to assert Type-C HPD: -110
  DPTXPort: firmware reports link fault 22 on target 0:3

With the HDMI route on dcpext0, the allocator offers a hybrid pipeline to
USB-C last, so a USB-C display always landed on a broken processor.
Mark dcpext1..3 "fail": the display gate enables only processors left
"disabled", and the DRM device waits only for available ones, so
dcpext0 serves USB-C and HDMI alike.

This is a stopgap that limits the J516C to one external display until
dcpext1..3 are fixed; drop it then.

Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chriskearney chriskearney 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.

On exact head 26ea9ca83c9cf6050ee9990b08cb662f6b753471, the new shared-color fallback introduces a dependency that the publication callback does not rebuild. dcp_v14_external_published() calls dcp_modes_replace() only for TimingElements. A later ColorElements publication updates the description bits/generation and clears mode_live, but does not reparse the retained timings.

A bounded CPU witness using the exact callback confirms: timings with no local colors first produce zero modes; publishing valid shared colors afterward still produces zero modes; republishing timings then produces the mode. Replacing shared color ID 7 with 11 leaves the catalog selecting 7 until timings are republished. The mode-live guard protects existing swap admission; this is a catalog dependency/order issue, not a demonstrated UAF or hardware reproduction. The fallback also applies to existing native v14.7 external displays, beyond the T6031 opt-in.

Please rebuild or invalidate dependent catalogs for either publication order, replacement and withdrawal, with serialized timing/color parser-publication tests and generation transitions. Alternatively, establish and enforce a protocol ordering invariant that makes these sequences impossible. Waiting longer in external_settle does not rebuild the catalog.

Separately, the documented first-high-bandwidth modeset failure and the unpublished local m1n1 external-handoff dependency remain qualification limits. This source finding can be closed without new hardware access; CI/QEMU would not establish external-display qualification.

@digital-brew

Copy link
Copy Markdown
Author

The m1n1 side is up: omacom/m1n1#21 (kboot_display_handoff: hand off the T6031 external display processors). It is the source this series was tested with, plus a test update for the per-SoC table.

A T6031 external processor's timings take their colors from
ColorElements, but dcp_v14_external_published() built the mode catalog
only on a TimingElements publication. Timings published before their
colors so gave no mode until republished, replaced colors (ID 7 by 11)
left the catalog on the old ID, and withdrawn colors left an admitted
catalog naming color IDs that were gone. The fallback also applied to
every native v14.7 external processor, beyond the T6031 opt-in.

Opt the T6031 dcpext in explicitly (shared_colors), and on it rebuild
the catalog from the published timings and the colors published now
whenever either is published, replaced or withdrawn, in either order.
A color change that leaves the timings no mode withdraws the catalog
(dcp_modes_withdraw(): no longer admitted, kept for snapshot readers)
and the timings' part of the description, and reports the display
withdrawn once: the firmware withdraws ColorElements before
TimingElements on an unplug, and a second report would release a
connector kept connected through the withdrawal. A removal has no
transfer and applies to the current attachment; a transfer begun
before it is still refused as stale.

The dcp_v14_shared_colors KUnit suite runs the production callback
through both publication orders, replacement, unsafe-ID exclusion,
withdrawal in both orders (one report each), republication, stale
generations, and a processor without the opt-in. With the rebuild on
a color change disabled, six of its eight cases fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHu9VBxYcBYR8voXKiSV65
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
@digital-brew

Copy link
Copy Markdown
Author

Thanks for the review. The catalog dependency is addressed in f401690 ("drm: apple: rebuild a T6031 dcpext catalog when its colors change").

Rebuild on either publication. dcp_v14_external_published() now builds the catalog through one helper, dcp_v14_external_catalog(), from the retained TimingElements and the ColorElements published at that moment. On a shared-colors processor it runs on a TimingElements publication and also on every ColorElements publication, replacement and withdrawal, as long as timings are retained. Your witness sequences now behave as follows:

  • timings without local colors first, then shared colors: the colors publication rebuilds the catalog and the mode appears without republishing the timings;
  • shared color 7 replaced by 11: the catalog selects 11 straight away;
  • shared colors withdrawn, or replaced by colors the timings exclude: the old catalog is withdrawn rather than kept. A new dcp_modes_withdraw() clears admission under modes_lock (the array stays allocated for snapshot readers, as with replacement), DESC_TIMING is cleared so external_settle() waits again, and the display is reported withdrawn.

Generations. A ColorElements transfer rebuilds with its own transfer generation, so one begun before the current attachment is still refused with -ESTALE and neither publishes nor withdraws anything. A removal has no transfer and uses the current generation. The description generation bump and the mode_live clear for color changes are unchanged.

Withdrawal reported once. On hardware the firmware withdraws ColorElements before TimingElements on an unplug (kernel log: ColorElements withdrawn, then TimingElements withdrawn). Reporting both would make the second iomfb_v14_7_external_withdrawn() call release a connector that dcp_v14_external_hold() kept connected. A colors_withdrew flag makes the TimingElements removal skip the report when the color withdrawal already made it. The flag clears on the next catalog that parses.

Scope. The fallback is now opt-in per processor: shared_colors is set only for apple,t6031-dcpext. Other native v14.7 external processors never receive ctx.shared_colors, and their publication path is the same as before this series.

Tests. New suite dcp_v14_shared_colors (iomfb_v14_7_shared_colors_test.c, included from iomfb_v14_7.c so the production callback and raw-property store run; only iomfb_v14_7_external_withdrawn() is a static stub, to count reports). It serializes the publications and checks the catalog, the description and the generations after each step:

  • colors then timings, and timings then colors;
  • replacement 7 → 11 with no timing republication (mode_live cleared, description generation advanced);
  • an UnsafeColorElementIDs exclusion in shared colors, and a replacement that leaves only the excluded ID (catalog withdrawn);
  • colors withdrawn before timings and timings withdrawn before colors, each reported exactly once;
  • withdrawal followed by republication;
  • a ColorElements transfer begun before a new attachment (stale, no publish, no withdrawal), then a current one;
  • a processor without the opt-in.

arm64 KUnit under qemu: 106/106 pass (the Apple display and parser suites). With the rebuild on a color publication disabled, 6 of the 8 new cases fail; the two that still pass are colors-first and no-opt-in, which the previous code already handled.

Still open, as you noted: the first high-bandwidth modeset failure, and the m1n1 external handoff dependency (omacom/m1n1#21, awaiting review). Neither is changed by this commit.

@digital-brew

digital-brew commented Nov 6, 2026 •

Copy link
Copy Markdown
Author

The first high-bandwidth modeset limit is addressed in 8f6ea2e ("drm: apple: wait for a dcpext display's timings before its first swap"). As you suggested, the driver no longer swaps before the link timings are enabled; the firmware reports exactly when that is.

Cause. After A472(1) the firmware starts the display's timings in a deferred run mode change and then sets the boolean property NormalModeActive (D104) to 1. A472(1) sets it to 0 first. With 2560x1600@60 the change ends inside A472. With 3840x2560@60 (DSC) it ends about 20 ms after A472 replied, and swap ID 1, sent in that gap, is dropped (swallowed swap ID 1 as timings are not enabled), so its A407 never completes. Swaps sent early later in a session are held instead of dropped, which is why only a session's first mode failed. Logged with dynamic debug on the link callbacks and the property names:

8.597  A472(1)
8.599  D104 NormalModeActive = 0
8.613  A472 replied; A406, A407 sent
8.614  swallowed swap ID 1 as timings are not enabled
8.633  switch to normal mode succeeded; D104 NormalModeActive = 1

Fix. dcp_v14_external_wait_normal() runs after every successful A472(1), still under the RPC lock, and waits for NormalModeActive (service 0, not the IOMF service's copy set through D415) to be 1. While it waits it handles the queued callbacks through dcp_v14_link_pump(), as dcp_v14_idle() does. Edge cases:

  • Timeout: after 500 ms the mode set fails into the existing retry path rather than leaving a swap to hang.
  • A failed callback stops the session, as in the idle worker.
  • Firmware that never reported the property is not waited for.

Hardware. J516C, 3840x2560 display through a USB-C dock, aurora11 base with patches 1–11 plus this one, no bring-up workaround:

  • 6 boots (reboot, power cycle, hotplug after login), each setting 3840x2560@60 first. The first swap completed in 16.8–17.8 ms every time.
  • 20 mode sets in all, 8 of them 3840x2560@120 with DSC. All succeeded.
  • The wait took 10–21 ms.
  • Without the wait, 5 of 5 boots lost the session.

Tests. New dcp_v14_normal_mode suite. It runs the wait and the production D104 handler with a scripted link pump, covering: not reported, already running, deferred, every power-on, other service, timeout, callback failure and failed session. arm64 KUnit: 114/114 pass, under W=1 and PROVE_LOCKING.

The m1n1 dependency remains, in omacom/m1n1#21. The PR description is updated accordingly.

A T6031 external session whose first mode was fast hung on its first
swap: with 3840x2560@60 (DSC) on the J516C 14.7 image, the firmware
dropped swap ID 1 and its A407 never completed, ending the session
until reboot:

  IOMFB: swap_submit_dcp: swallowed swap ID 1 as timinsg are not enabled for External Display
  DCP call 0x41343037 failed: -110; recovery requires a reboot

The firmware starts a display's timings in a deferred run mode change
after A472(1) and then sets the boolean property NormalModeActive (D104)
to 1; A472(1) sets it to 0 first. With a slow mode (2560x1600@60) the
change ends within A472, NormalModeActive already 1 when it replies.
With the DSC mode it ends about 20 ms after A472 replied, and the swap
sent meanwhile is dropped. Later swaps sent early are held instead, so
only a session's first mode failed. Logged on a J516C:

  8.597  A472(1)
  8.599  D104 NormalModeActive = 0
  8.613  A472 replied; A406, A407 sent
  8.614  swallowed swap ID 1 as timings are not enabled
  8.633  switch to normal mode succeeded; D104 NormalModeActive = 1

After A472(1), wait under the RPC lock for NormalModeActive (service 0)
to be 1, handling the queued callbacks as dcp_v14_idle() does, for up
to 500 ms. A timeout fails the mode set into the retry path; a failed
callback stops the session; a firmware that never reported the property
is not waited for. Every power-on is waited for, about 20 ms for the
first DSC mode and 10 ms for later ones.

On the J516C with a 3840x2560 display (DP over USB-C), six boots
(reboot, power cycle, replug) set 3840x2560@60 first and showed its
first swap in 18 ms; 14 later mode sets, 3840x2560@120 included, all
succeeded. Without the wait, 5 of 5 boots lost the session.

The dcp_v14_normal_mode KUnit suite runs the wait and the production
D104 handler with a scripted link pump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHu9VCTrUXYgBxj2gKTGrp
Signed-off-by: DB <rafal+claudeai@digitalbrew.io>
@digital-brew
digital-brew force-pushed the t6031-external-displays branch from b66db34 to 8f6ea2e Compare November 6, 2026 14:17
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