Skip to content

drm/apple: 4K at 120 Hz on the M4 MacBook Air external displays - #1007

Merged
RyanTheTide merged 5 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/extdisp-120
Nov 6, 2026
Merged

RyanTheTide merged 5 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/extdisp-120

Conversation

@erikwb

@erikwb erikwb commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Follows #957. It makes high-refresh modes usable on the M4 MacBook Air's external DCPs:

  1. Offer the sink's DSC timings.
    • A timing whose only 8-bpc RGB SDR colour element needs DSC (DSCRequiredColorElementIDs) was dropped along with the unsafe ones; the parser now tells the two lists apart. For a typical 4K monitor that means 3840x2160 at 120 Hz and 2560x1440 at 240 Hz.
    • A DSC element counts only at 8 bpc; an element that needs no DSC wins at equal depth.
    • DSC modes are checked against the link at 8 bpp. They are never preferred and never the fallback, so a monitor still comes up uncompressed and DSC is used only when userspace picks it.
  2. Realtime bandwidth mailbox.
    • The external firmware asks for a bandwidth mailbox at pipe start (D003). macOS answers with the pipe's 8-byte bandwidth request slot in the PMC, with no doorbell. The slot comes from the DT through apple,bw-scratch, and must lie inside the DCP's disp-4 window.
    • The firmware then maps its register windows (D411). Every disp-N window the DT describes is answered with its physical address, as on the desktop 26.x backends; any other index is refused.
    • The internal panel still declines D003.
  3. Two swaps in flight.
    • The firmware completes a direct swap (D590) up to about 10 ms after it's submitted, whatever the refresh rate. With one swap in flight and the flip at D590, a 120 Hz mode ran at about 60 frames a second.
    • Up to two direct swaps can now be outstanding (t8132_ext_swap_depth, default 2; 1 restores flip-at-D590). D590's displayed flag and its head-of-line completions decide what is on screen, and every buffer that is on screen or in flight stays mapped.
    • At depth 2 a flip event can come before the firmware confirms the frame replaced the previous one, so a compositor may draw into a buffer still on screen. With Hyprland at 4K120 nothing was visible.

DT: a binding note for the T8132 dcpext bandwidth slot, and the J713's two slots plus apple,bw-scratch on dcpext0 and dcpext1.

Full-rate 4K120 also needs the PMC interface enables (#1006, pmdomain). Without them the pipe's display clock isn't boosted, and only the first lines of each frame show. With DSC, 4K120 fits an HBR2 x4 link, so it also runs next to a second 4K60 monitor on one dock.

Testing:

  • KUnit (drm/apple suites): 384 tests, 382 passed, 2 skipped.

  • make dt_binding_check (apple,dcp.yaml) and dtbs_check (t8132-j713): no new messages.

  • scripts/checkpatch.pl --strict: clean on all five commits; each commit builds on its own.

  • J713 with a CalDigit TS5 dock and two MSI MPG 322U X24 monitors, with the PMC PR (pmdomain: apple: pmc: enable the M4 external display boost, for 4K at 120 Hz #1006):

    • 3840x2160 at 120 Hz with DSC: the whole picture, 120 swaps a second at depth 2 and no visible tearing under Hyprland. At depth 1 the same mode runs at about 60.
    • 1920x1080 at 120 Hz without DSC, and switching between it and 4K120.
    • 3840x2160 at 120 Hz on one monitor and 3840x2160 at 60 Hz on the other, both on HBR2 x4 links.
    • Hotplug of either monitor during scanout at depth 2, many times, docked s2idle, and the dock on either Type-C port: both monitors come back in their modes, no warnings. The internal panel is unaffected.
    • With no mode set by userspace the monitors come up uncompressed at their preferred 3840x2160 at 60 Hz. The fbdev console, re-probing at hotplug within its framebuffer's size (the internal panel's 2560x1664), picks the largest mode that fits, 2560x1440 at 240 Hz with DSC, until the compositor sets its own; that also works.

This was developed and tested by Claude (Anthropic), working with me on the J713.

An external sink can list timings, such as 3840x2160 at 120 Hz, whose
8-bpc RGB SDR colour elements the timing marks as needing DSC
(DSCRequiredColorElementIDs). The TimingElements walk treated those like
the unsafe ones (UnsafeColorElementIDs), so such a timing was never
offered.

Tell the two lists apart in the parser, and offer a timing whose only
8-bpc RGB SDR colour element needs DSC. An element that needs DSC is
considered only at 8 bpc, and the lowest depth still comes first, then
an element that needs no DSC over one that does. The choice is unchanged
for every timing whose lowest uncompressed depth is 8 bpc or less. A DSC
mode is checked against the link at 8 bpp, the lowest DSC target; the
firmware picks the compressed rate itself.

A DSC mode is never marked preferred and never taken as the fallback:
it is used only when userspace picks it.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…ailbox

The external DCP firmware asks for a realtime bandwidth mailbox at pipe
start (D003, rt_bandwidth_setup_ap) and writes its bandwidth requests
there every frame. The M4 backend declined the service, so the pipe
never requested the bandwidth it needs at high pixel rates such as
3840x2160 at 120 Hz.

macOS answers D003 for a T8132 external pipe with the pipe's 8-byte
bandwidth request slot in the PMC as the mailbox. The mailbox length (8)
selects the scratch-only protocol, with no doorbell, which the firmware
reaches through its display register window 4.

Take the slot from the DT, through apple,bw-scratch as on the other
DCPs: the provider's reg range, the firmware display register index
(which must be 4) and an offset. The slot must lie inside the DCP's
disp-4 window. Answer D003 with it, and decline as before without one,
as on the internal panel, whose node has none.

The firmware then maps its register windows with D411
(mapDeviceMemoryWithIndex). Answer it for every disp-N window the DT
describes, with the physical address as the DVA, as the desktop 26.x
backends do: the firmware reaches those windows on its DART-bypass
stream. An index the DT does not describe is refused.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The T8132 external DCPs take apple,bw-scratch for their 8-byte realtime
bandwidth request slot in the PMC. Unlike the other DCPs, where the
scratch register becomes an extra display register window, the slot lies
inside the window the display index names (disp-4). Say so in the
property's description.

Signed-off-by: Erik Bourget <erik@ebourget.net>
… slots

Describe the external pipes' realtime bandwidth request slots in the PMC,
0x2c000 (dcpext0) and 0x30000 (dcpext1) into the window both DCPs have as
disp-4, and point each dcpext at its slot with apple,bw-scratch (display
register index 4), so that the driver can hand the slot to the firmware
as its bandwidth mailbox.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The J713 external DCP completes a direct swap (D590) several
milliseconds after its A407 reply, up to about 10 ms whatever the
refresh rate. With one swap in flight and the flip event sent at D590, a
frame takes that time plus the compositor's own, which holds a 120 Hz
mode to about 60 frames a second.

Let up to two direct swaps be outstanding, set by the module parameter
t8132_ext_swap_depth (2 by default, 1 to send every flip at its D590 as
before):

- A replied direct swap moves from the session's swap state to an
  ordered queue (oldest first) with its window, link generation and 5 s
  deadline, so that the next A406/A407 can go out. A third frame while
  two are in flight gets -EBUSY; the copy path stays refused while any
  direct swap is in flight.
- Flip events: a frame submitted with nothing older in flight gets its
  flip event at once (iomfb_m4_ext_present_direct() returns 1); one
  submitted behind another gets it when the older swap completes or is
  given up. At depth 2 a flip event can therefore come before the
  firmware confirms that the frame replaced the previous one, so a
  compositor may draw into a buffer that is still on screen.
- Completions: a D590 completes the swap it names, with its displayed
  flag, and the earlier swaps in its head-of-line list, which macOS
  completes as displayed. A swap it does not name stays in flight until
  its own D590 or its give-up. The firmware completes swaps in order; a
  newer swap completing first is warned about. A swap completed through
  a later swap's D590 is remembered, so that its own late D590 is
  dropped, and A406 handing out such an ID again orphans the start, as
  for given-up swaps.
- Screen state follows the D590s in arrival order, a D590 that comes
  before its swap's A407 reply included. With displayed = 1 the swap's
  window is the one on screen; with displayed = 0 nothing on screen is
  retired, on the direct, given-up, copy and blank paths alike. Windows
  of both swaps in flight, and of every given-up swap that may still
  show them, stay mapped. Every direct completion, and every give-up,
  leaves the copy surfaces stale.
- The give-up work expires queued swaps one by one at their own
  deadline, or at once when the link goes; settle, sleep, suspend and
  hotplug wait for or drop the whole queue.
- D581 (swap_complete_head_of_line) is logged and left to the D590 or
  the give-up.

With the PMC's display bandwidth agents enabled (a separate change), a
3840x2160 120 Hz DSC mode runs at 120 frames a second at depth 2.

Signed-off-by: Erik Bourget <erik@ebourget.net>
@RyanTheTide

Copy link
Copy Markdown
Contributor

Merge train (MTR, 06/11): merging, with notes. Review: all of it runs only under iomfb_is_m4() (T8132 internal and dcpext). The parser change only splits restricted into unsafe and DSC-required for the M4 caller, and the internal panel's swap path is unchanged. No other DCP generation sees a difference, and the J616s and Neo displays were unchanged on hardware.

Notes for follow-up:

  1. Early flip events. At the default t8132_ext_swap_depth=2, a flip event can go out before D590 confirms the new frame replaced the old one. That breaks the KMS rule that the old buffer is free, as your parameter text says. Buffers stay mapped, so it's safe for memory, but a compositor may draw into a buffer that's still on screen. If you see tearing on other compositors, default 1 is the fallback.
  2. ext_early[16] slots are never evicted. If firmware names swaps it never completes on their own, direct scanout silently falls back to copies until reboot. A debugfs counter would make that visible.

Gates: test merge 2d3579ef = integration/aurora 11eccbe + #1005 #994 #930 #658 #992 #1006 #1007 #1008 #531 (#992 has since landed on its own); base 11eccbe.

  • KUnit: drm/apple 112/113 (1 skip); Rust (asahi, incl. asahi_g17_validation_pages) 200/201 (1 skip); arm64 apple-isp-*, apple-pmc, apple-pmgr-pwrstate and thunderbolt 137/138 (1 skip).
  • W=1 on drm/apple, media/apple/isp, pmdomain/apple, thunderbolt and nvmem/apple-spmi-nvmem: 0 new warnings.
  • dt_binding_check (apple,dcp, apple,dart, apple,t8132-isp, apple,spmi-nvmem, apple,pmc): 0 new.
  • Host tests run-host-tests.py 185/185; apple-dcp selftests 86/86.

Hardware (7.3.0-aurora-mtr1106a, one-shot boots, fleet recipe):

  • J616s (M4 Pro) vs fleet17: 0 kernel errors, 0 AVC, 0 failed units; eDP + DP-2, Wi-Fi, Vulkan G16S. Camera NV12 1080p and 720p, and P010 720p, streamed 30 frames each, with no ISP errors. vkcube OK; glmark2 6617/6738/6683 vs fleet17 6646/6555/6664.
  • Neo (A18 Pro), two boots vs fleet16: 0 kernel errors, 0 AVC, os_uuid pin present; eDP, Wi-Fi, Vulkan G17P. Camera NV12 1080p and 720p, no ISP errors. vkcube OK; glmark2 2718/2729/2726/2725 vs fleet16 2043–2108; 0 GPU faults.
  • Both Macs were restored to their default kernels afterwards.

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