Skip to content

14.x M3 panels: implement real backlight control on J613 and J516S #32

Open

Description

@chriskearney

J613 and J516S on14.x have a native panel but no Linux backlight device; brightness keys do nothing and firmware reports missing Backlight service. Handling the D208 backlight-factor notification prevents a separate night-light failure; that notification is not a brightness target. Native25 Air and Neo backlight behavior must be preserved.

Priority: P2 · Readiness: investigation · Implementation owner: kernel

Baseline: Aurora 2026.10.11.13, development commit 5dcdeb8cda8dafe203d4d2f03952ade392c3fe23. Earlier reproductions retain the exact versions above.

Code: drivers/gpu/drm/apple/iomfb_v14_7.c · drivers/gpu/drm/apple/dcp_backlight.c

Completion criteria: Identify the14.x panel brightness service and units. Register a backlight only when actual firmware control is admitted, then verify brightness changes, bounds, error handling and restore after suspend/reboot. Check both named boards and preserve external-display and26.6.2 profile behavior.

Original reports/candidate: original #80 · original #87

This ticket carries forward the remaining work; source inclusion, release delivery and hardware acceptance are recorded separately.

Activity

  1. added
    priority:P2Recovery, performance or hardware qualification
    status:investigationFailure is open; mechanism needs confirmation
    owner:kernelImplementation belongs to kernel
    area:displayAffected display subsystem
    launch:follow-upRetained improvements or broader retests outside the initial launch
    on Oct 11, 2026
  2. aronvandepol commented on Oct 11, 2026

    @aronvandepol

    On the j516s (14.8.3 stub), the 14.x firmware takes panel brightness from the swap record, in nits, with the same update byte as 26.6 and the double two bytes earlier: an update byte at 0x354 and the target as a little-endian double at 0x35c (26.6: 0x35e). Tested on 2026.10.11.14 (7cae5a7). The patch below is against 7cae5a7 and ports the 26.6 backlight code (dcp_v26_brightness_*) to the 14.x path, behind a t6030_backlight parameter that defaults off.

    How I found it. The firmware logs UPPipeDCP_H13P.cpp:2564: brightness_changed: brightness value: N (N in 16.16 nits) the first time a swap carries the update byte. It logs one burst per boot, so I read one result per boot:

    • 0x354 = 1 with the 26.6 double at 0x35e: brightness value: 0, and the panel went black and stayed black until a reboot. Nothing later requested a new level.
    • 0x354 = 1 with 0x355..0x3a0 filled with 0x40: 2130048 = 32.50 nits, which is 0x4040404040404040 as a double. Filling 0x355..0x508 crashed the DCP (EL1 exception), so something in that larger range is not data.
    • 0x354 = 1 with a ladder of doubles, 50 + 10j nits every 8 bytes from 0x358: 0. The same ladder from 0x35c: 3276800 = 50.0 nits, slot 0.

    Sending 60, 450, 150 and 300 nits at 0x35c, five seconds apart, then visibly changed the panel at each step. "IOMFB: Could not find Backlight service" still appears at start, and D208 (backlight factor) never arrived during any of this.

    Results with the patch (t6030_backlight=1, the module alone, without the #31 parameters):

    • apple-panel-bl registers at the first frame with max 500, from the panel's apple,max-brightness. systemd-backlight restored 250 nits across a reboot (dcp-t6030: requested 250, sent 250).
    • From sysfs with the desktop idle, 10 and 250 were each sent by the CRTC-only commit, and 501 was refused.
    • Hyprland DPMS off sent 0 nits (off 1, sent 0), and DPMS on sent 250 again.
    • With the same code built together with the J516S: 120 Hz mode produces approximately60 Hz page-flip completions #31 parameters: 40, 400 and 200 from sysfs were each sent at once, and Omarchy's brightness keys and OSD work through brightnessctl/logind.
    • dcp-t6030 failed stayed 0, with no flip failed, GPU fault or DEVICE LOST.

    Not tested: J613 on 14.x (the patch only enables T6030), suspend/resume (#59 makes lid resume unreliable here anyway), and values above 500 nits.

    Patch (against 7cae5a7)
    diff --git i/drivers/gpu/drm/apple/iomfb_v14_7.c w/drivers/gpu/drm/apple/iomfb_v14_7.c
    index 9593cfbc863c..01e0f578d798 100644
    --- i/drivers/gpu/drm/apple/iomfb_v14_7.c
    +++ w/drivers/gpu/drm/apple/iomfb_v14_7.c
    @@ -1,5 +1,6 @@
     // SPDX-License-Identifier: GPL-2.0-only OR MIT
     
    +#include <linux/backlight.h>
     #include <linux/clk.h>
     #include <linux/debugfs.h>
     #include <linux/device.h>
    @@ -94,6 +95,18 @@ struct dcp_v14_board {
     	u32 ctm_set, ctm_get;
     };
     
    +/* A backlight device driving the T6030 panel through swaps (see dcp_v14_brightness_*). */
    +static bool t6030_backlight;
    +module_param(t6030_backlight, bool, 0444);
    +MODULE_PARM_DESC(t6030_backlight,
    +		 "T6030 built-in panel: register a backlight device, in nits (default off)");
    +/* Reported until the first request; the firmware keeps the iBoot level until then. */
    +#define DCP_V14_DEFAULT_NITS	140
    +/* SDR maximum of the J516S panel; the DT gives the panel's own. */
    +#define DCP_V14_T6030_MAX_NITS	500
    +/* How long a brightness request waits for a flip to carry it. */
    +#define DCP_V14_BL_FLIP_WAIT	msecs_to_jiffies(25)
    +
     static const struct dcp_v14_board dcp_v14_board_t6030 = {
     	.name = "T6030",
     	.debugfs = "dcp-t6030",
    @@ -183,6 +196,22 @@ struct apple_dcp_v14 {
     	/* The last firmware backlight correction notification; not a nits target. */
     	u32 backlight_factor;
     	u64 backlight_factor_updates;
    +	/* Panel brightness, carried by swaps. Under the lock. */
    +	u32 bl_nits;		/* requested */
    +	u32 bl_sent;		/* carried by the last brightness swap */
    +	u64 bl_seq;		/* bumped by every change of the target */
    +	u64 bl_swaps;
    +	bool bl_owned;		/* requested at least once: the driver sets it */
    +	bool bl_off;		/* the CRTC is off: the target is 0 */
    +	bool bl_pending;	/* the target has not been carried yet */
    +	bool bl_queued;		/* the backlight device is being registered */
    +	wait_queue_head_t bl_wait;
    +	/* Guards bl_dev against registration racing removal. */
    +	struct mutex bl_lock;
    +	struct work_struct bl_register_work;
    +	struct backlight_device *bl_dev;
    +	bool bl_detached;
    +	u32 bl_max;		/* 0: no backlight device */
     	u32 ctm_status, ctm_get_status;
     	u64 ctm_readback[9];
     	struct work_struct idle_work;
    @@ -244,6 +273,9 @@ struct apple_dcp_v14 {
     	u64 swap_ns_max;
     };
     
    +static void dcp_v14_backlight_register(struct work_struct *work);
    +static void dcp_v14_backlight_detach(struct apple_dcp_v14 *v14);
    +
     /*
      * The panel's DCP session; one per boot, RTKit is never started twice. The
      * manual external start waits for it, as the PMP runs once the panel's
    @@ -1485,6 +1517,9 @@ int iomfb_v14_7_probe(struct apple_dcp *dcp)
     	v14->board = board;
     	v14->dcp = dcp;
     	mutex_init(&v14->lock);
    +	init_waitqueue_head(&v14->bl_wait);
    +	mutex_init(&v14->bl_lock);
    +	INIT_WORK(&v14->bl_register_work, dcp_v14_backlight_register);
     	init_waitqueue_head(&v14->described_wait);
     	INIT_WORK(&v14->idle_work, dcp_v14_idle);
     	ret = devm_add_action_or_reset(dev, dcp_v14_release, v14);
    @@ -2155,6 +2190,7 @@ void iomfb_v14_7_unbind(struct apple_dcp *dcp)
     		flush_work(&v14->idle_work);
     		cancel_work_sync(&v14->stopped_work);
     	} else {
    +		dcp_v14_backlight_detach(v14);
     		WRITE_ONCE(v14->dcp, NULL);
     	}
     	if (v14->rtk)
    @@ -2224,6 +2260,9 @@ static int dcp_v14_status_show(struct seq_file *m, void *unused)
     		   v14->stride, v14->clock_rate);
     	seq_printf(m, "backlight factor %#x updates %llu\n",
     		   v14->backlight_factor, v14->backlight_factor_updates);
    +	seq_printf(m, "backlight %d max %u\nbrightness requested %u sent %u owned %d off %d pending %d swaps %llu\n",
    +		   !!READ_ONCE(v14->bl_dev), v14->bl_max, v14->bl_nits, v14->bl_sent,
    +		   v14->bl_owned, v14->bl_off, v14->bl_pending, v14->bl_swaps);
     	seq_printf(m, "ctm valid %d disabled %d calls %llu setter %#x getter %#x\n",
     		   v14->ctm_valid, v14->ctm_disabled, v14->ctm_calls, v14->ctm_status,
     		   v14->ctm_get_status);
    @@ -2333,6 +2372,11 @@ int iomfb_v14_7_start(struct apple_dcp *dcp)
     	if (ret)
     		goto fail;
     
    +	/* Nits; the backlight device appears with the first frame. */
    +	if (v14->board == &dcp_v14_board_t6030 && t6030_backlight && dcp->brightness.maximum) {
    +		v14->bl_max = min_t(u32, dcp->brightness.maximum, DCP_V14_T6030_MAX_NITS);
    +		v14->bl_nits = min_t(u32, DCP_V14_DEFAULT_NITS, v14->bl_max);
    +	}
     	v14->started = true;
     	/* Never removed, like the session it describes. */
     	debugfs_create_file(v14->board->debugfs, 0400, NULL, v14, &dcp_v14_status_fops);
    @@ -2450,12 +2494,190 @@ static bool dcp_v14_swap_admit(void *cookie)
      * Swap start, then the swap; returns once the firmware completed that swap.
      * @ctm_state, if set, carries the colour matrix to send first.
      */
    +/*
    + * Panel brightness travels in the swap record, in nits, as on 26.6.
    + *
    + * Until the first request the firmware keeps the brightness iBoot set, and
    + * no swap touches it. From then on the driver sets it: a request, the CRTC
    + * going off (0 nits) and the CRTC coming back (the requested nits) each make
    + * the target pending, and the next swap carries it. A request waits a frame
    + * for a flip to carry it, then makes a swap of its own (see
    + * dcp_v14_brightness_commit()).
    + */
    +
    +/* Called with the lock held. */
    +static void dcp_v14_brightness_changed(struct apple_dcp_v14 *v14)
    +{
    +	lockdep_assert_held(&v14->lock);
    +	if (!v14->bl_owned)
    +		return;
    +	WRITE_ONCE(v14->bl_pending, true);
    +	v14->bl_seq++;
    +}
    +
    +/* Adds a pending target to @swap; returns the token dcp_v14_brightness_done() takes. */
    +static u64 dcp_v14_brightness_encode(struct apple_dcp_v14 *v14, u8 *swap)
    +{
    +	lockdep_assert_held(&v14->lock);
    +	if (!v14->bl_max || !v14->bl_pending)
    +		return 0;
    +	dcp_v14_encode_brightness(swap, v14->bl_off ? 0 : v14->bl_nits);
    +	return v14->bl_seq;
    +}
    +
    +/* A swap completed; @token is what it carried. Called with the lock held. */
    +static void dcp_v14_brightness_done(struct apple_dcp_v14 *v14, u64 token)
    +{
    +	lockdep_assert_held(&v14->lock);
    +	if (!v14->bl_max)
    +		return;
    +	/* The first frame is on the panel: KMS is up, so is the backlight now. */
    +	if (!v14->bl_queued) {
    +		v14->bl_queued = true;
    +		schedule_work(&v14->bl_register_work);
    +	}
    +	if (!token)
    +		return;
    +	v14->bl_swaps++;
    +	/* A change made since then is still pending. */
    +	if (token != v14->bl_seq)
    +		return;
    +	v14->bl_sent = v14->bl_off ? 0 : v14->bl_nits;
    +	WRITE_ONCE(v14->bl_pending, false);
    +	wake_up_all(&v14->bl_wait);
    +}
    +
    +/*
    + * Makes a swap for a pending brightness: a commit of the CRTC alone, which
    + * shows the plane as it is. Nothing is done while the CRTC is off (enabling
    + * it sends the brightness) or before its first modeset (the first frame
    + * sends it), nor when a flip carried it in the meantime.
    + */
    +static int dcp_v14_brightness_commit(struct apple_dcp_v14 *v14)
    +{
    +	struct apple_dcp *dcp = READ_ONCE(v14->dcp);
    +	struct drm_modeset_acquire_ctx ctx;
    +	struct drm_crtc_state *crtc_state;
    +	struct drm_atomic_state *state;
    +	struct drm_crtc *crtc;
    +	int ret;
    +
    +	if (!dcp || !dcp->crtc)
    +		return 0;
    +	crtc = &dcp->crtc->base;
    +	state = drm_atomic_state_alloc(crtc->dev);
    +	if (!state)
    +		return -ENOMEM;
    +	drm_modeset_acquire_init(&ctx, DRM_MODESET_ACQUIRE_INTERRUPTIBLE);
    +	state->acquire_ctx = &ctx;
    +retry:
    +	ret = drm_modeset_lock(&crtc->mutex, &ctx);
    +	if (ret)
    +		goto out;
    +	if (!crtc->state || !crtc->state->active || !READ_ONCE(v14->bl_pending))
    +		goto out;
    +	crtc_state = drm_atomic_get_crtc_state(state, crtc);
    +	if (IS_ERR(crtc_state)) {
    +		ret = PTR_ERR(crtc_state);
    +		goto out;
    +	}
    +	ret = drm_atomic_commit(state);
    +out:
    +	if (ret == -EDEADLK) {
    +		drm_atomic_state_clear(state);
    +		ret = drm_modeset_backoff(&ctx);
    +		if (!ret)
    +			goto retry;
    +	}
    +	drm_atomic_state_put(state);
    +	drm_modeset_drop_locks(&ctx);
    +	drm_modeset_acquire_fini(&ctx);
    +	return ret;
    +}
    +
    +static int dcp_v14_backlight_update(struct backlight_device *bd)
    +{
    +	struct apple_dcp_v14 *v14 = bl_get_data(bd);
    +	int ret = 0;
    +
    +	mutex_lock(&v14->lock);
    +	if (v14->failed) {
    +		ret = -EIO;
    +	} else {
    +		/* Sent even when unchanged: it also repairs the firmware's value. */
    +		v14->bl_nits = backlight_get_brightness(bd);
    +		v14->bl_owned = true;
    +		dcp_v14_brightness_changed(v14);
    +	}
    +	mutex_unlock(&v14->lock);
    +	if (ret)
    +		return ret;
    +
    +	if (wait_event_timeout(v14->bl_wait, !READ_ONCE(v14->bl_pending), DCP_V14_BL_FLIP_WAIT))
    +		return 0;
    +	return dcp_v14_brightness_commit(v14);
    +}
    +
    +static const struct backlight_ops dcp_v14_backlight_ops = {
    +	.update_status = dcp_v14_backlight_update,
    +};
    +
    +/* Nits, like the M1/M2 panels: 0 to the DT maximum, linear in luminance. */
    +static void dcp_v14_backlight_register(struct work_struct *work)
    +{
    +	struct apple_dcp_v14 *v14 = container_of(work, struct apple_dcp_v14, bl_register_work);
    +	struct backlight_properties props = {
    +		.type = BACKLIGHT_PLATFORM,
    +		.scale = BACKLIGHT_SCALE_LINEAR,
    +		.max_brightness = v14->bl_max,
    +		.brightness = min_t(u32, DCP_V14_DEFAULT_NITS, v14->bl_max),
    +	};
    +	struct backlight_device *bd;
    +
    +	mutex_lock(&v14->bl_lock);
    +	if (!v14->bl_detached && !v14->bl_dev) {
    +		bd = backlight_device_register("apple-panel-bl", v14->dev, v14,
    +					       &dcp_v14_backlight_ops, &props);
    +		if (IS_ERR(bd))
    +			dev_err(v14->dev, "no panel backlight device: %pe\n", bd);
    +		else
    +			WRITE_ONCE(v14->bl_dev, bd);
    +	}
    +	mutex_unlock(&v14->bl_lock);
    +}
    +
    +/* KMS goes away: so does the backlight device, before the CRTC it commits to. */
    +static void dcp_v14_backlight_detach(struct apple_dcp_v14 *v14)
    +{
    +	struct backlight_device *bd;
    +
    +	mutex_lock(&v14->bl_lock);
    +	v14->bl_detached = true;
    +	bd = v14->bl_dev;
    +	WRITE_ONCE(v14->bl_dev, NULL);
    +	mutex_unlock(&v14->bl_lock);
    +	cancel_work_sync(&v14->bl_register_work);
    +	backlight_device_unregister(bd);
    +}
    +
    +/* 0 nits while the CRTC is off, the requested nits once it is on again. */
    +static void dcp_v14_brightness_power(struct apple_dcp_v14 *v14, bool on)
    +{
    +	if (!v14->bl_max)
    +		return;
    +	mutex_lock(&v14->lock);
    +	v14->bl_off = !on;
    +	dcp_v14_brightness_changed(v14);
    +	mutex_unlock(&v14->lock);
    +}
    +
     static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova,
     			u32 width, u32 height, u32 dst_y,
     			const struct drm_crtc_state *ctm_state)
     {
     	struct dcp_v14_swap_admission admission = { .v14 = v14 };
     	__le32 start[4] = {}, started[2] = {}, result[3];
    +	u64 brightness = 0;
     	u8 *swap;
     	u32 id;
     	int ret;
    @@ -2484,6 +2706,8 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova,
     	if (!ret) {
     		dcp_v14_encode_swap(swap, id, DCP_V14_BLACK, surface, iova, width, height,
     				    dst_y, DCP_V14_LAYER);
    +		if (!v14->external)
    +			brightness = dcp_v14_brightness_encode(v14, swap);
     		ret = dcp_v14_call_guarded(v14, A(407), swap, DCP_V14_SWAP_SIZE, result, sizeof(result),
     				   id, dcp_v14_swap_admit, &admission);
     		/* The swap status sits at byte 5 of the reply. */
    @@ -2497,6 +2721,8 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova,
     	 */
     	if (ret && !v14->external)
     		v14->failed = true;
    +	else if (!ret && !v14->external)
    +		dcp_v14_brightness_done(v14, brightness);
     	mutex_unlock(&v14->lock);
     
     	kfree(swap);
    @@ -2859,6 +3085,8 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc,
     void iomfb_v14_7_poweron(struct apple_dcp *dcp)
     {
     	/* The panel stays on; the next swap shows the plane again. */
    +	if (dcp->v14 && dcp->v14->started && !dcp->external)
    +		dcp_v14_brightness_power(dcp->v14, true);
     }
     
     void iomfb_v14_7_poweroff(struct apple_dcp *dcp)
    @@ -2877,6 +3105,8 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp)
     		return;
     	}
     	/* Blank to black; the panel and the DCP stay powered. */
    -	if (v14 && v14->started)
    +	if (v14 && v14->started) {
    +		dcp_v14_brightness_power(v14, false);
     		dcp_v14_present(v14, NULL, 0, 0, 0, NULL);
    +	}
     }
    diff --git i/drivers/gpu/drm/apple/iomfb_v14_7_swap.h w/drivers/gpu/drm/apple/iomfb_v14_7_swap.h
    index aab3351dd9ee..e1f9d011c4a9 100644
    --- i/drivers/gpu/drm/apple/iomfb_v14_7_swap.h
    +++ w/drivers/gpu/drm/apple/iomfb_v14_7_swap.h
    @@ -18,6 +18,14 @@
     #define DCP_V14_SWAP_ENABLED		0x14c
     #define DCP_V14_SWAP_COMPLETED		0x150
     #define DCP_V14_SWAP_BACKGROUND		0x154
    +/*
    + * Panel brightness (J516S, 14.8.3 stub): an update byte at 0x354 and the
    + * target as a double in nits at 0x35c. Found on the hardware: the firmware
    + * logs brightness_changed with the value it took, and the panel follows.
    + * 26.6 keeps the same update byte but the double at 0x35e.
    + */
    +#define DCP_V14_SWAP_BRIGHTNESS_UPDATE	0x354
    +#define DCP_V14_SWAP_BRIGHTNESS_NITS	0x35c
     #define DCP_V14_SWAP_SURFACE(p)		(0x508 + (p) * DCP_V14_SURFACE_SIZE)
     #define DCP_V14_SWAP_IOVA(p)		(0xdb8 + (p) * 8)
     #define DCP_V14_SWAP_SURF_NULL(p)	(0x1b4b + (p))
    @@ -115,4 +123,23 @@ static inline void dcp_v14_encode_swap(u8 *swap, u32 id, u32 background, const u
     	}
     }
     
    +/* Whole nits as an IEEE 754 double, without floating point in the kernel. */
    +static inline u64 dcp_v14_nits_to_double(u32 nits)
    +{
    +	u32 exponent;
    +
    +	if (!nits)
    +		return 0;
    +	exponent = fls(nits) - 1;
    +	return ((u64)(1023 + exponent) << 52) |
    +	       (((u64)nits << (52 - exponent)) & GENMASK_ULL(51, 0));
    +}
    +
    +/* Asks for @nits in a swap record built by dcp_v14_encode_swap(). */
    +static inline void dcp_v14_encode_brightness(u8 *swap, u32 nits)
    +{
    +	swap[DCP_V14_SWAP_BRIGHTNESS_UPDATE] = 1;
    +	put_unaligned_le64(dcp_v14_nits_to_double(nits), swap + DCP_V14_SWAP_BRIGHTNESS_NITS);
    +}
    +
     #endif /* __APPLE_IOMFB_V14_7_SWAP_H__ */
  3. tonibergholm commented on Oct 16, 2026

    @tonibergholm

    J613 result: real brightness control works here with a J613-only adaptation of @aronvandepol's patch above. Thanks for finding the swap fields.

    MacBook Air 13-inch M3 (2024), J613/T8122, internal panel only. Base is aurora-2026.10.15.19, source 5ec1028c81526e1ed31db2b97e47edf8663e83cb, kernel 7.1.12-2-20261015.19-sep-ARCH. OS firmware label is 14.7, system firmware 27.0; the GPU separately reports 14.8.3 compatibility. Existing stage1 was retained; stage2 is v1.6.1-omarchy.aurora17.

    The adaptation adds appledrm.j613_backlight=1, default off, restricted to the existing J613 board record and its firmware-image admission. It uses the same 0x354 update byte and 0x35c double-in-nits field. The DT maximum is 525; the candidate remains capped at 500. The original J516S work is credited in the patch.

    Checks on this Air:

    • apple-panel-bl registers with max 500. Requests 100 → 300 → 150 changed the panel visibly and read back exactly; the starting 265 was restored.
    • Brightness keys and Omarchy OSD work. Writing 501 returns EINVAL and leaves the level unchanged.
    • DPMS off/on restores 265; driver status shows failed 0, requested/sent 265, no pending request.
    • A normal reboot restores screen brightness 265 and keyboard backlight 77. Firmware logs brightness value: 17367040 (= 265 in 16.16).
    • A short s2idle/power-button wake and a separate ~35-second lid-close/lid-open cycle preserve brightness. Opening the lid alone wakes the display. Touch ID unlock and audible protected speaker playback work afterwards.
    • OpenGL pixel and Vulkan compute readback pass after reboot and both sleep tests. No flip failure or GPU fault was observed in these checks.

    Source patch, setup and display evidence, sleep results.

    Built as an appledrm module against the exact installed headers/symbols, then embedded and hash-checked in a separate test UKI. Packaged module and normal UKI retained. Local clang is 23.1.1 versus the release's 22.1.8; modpost reported two init-to-exit references in unchanged unregister/error-cleanup code. No full kernel build or KUnit run here.

    Limits: requested nits, no photometer; no repeated/overnight sleep, external display or other-board qualification. The missing-Backlight-service firmware message still appears, but this swap path works. No HDR change or GPU-clock increase was made.

    Prepared with Codex from local tests; visible brightness, keys/OSD, lid wake, Touch ID and sound were confirmed by me on the Mac.

  4. GehrmannSolutions commented on Oct 18, 2026

    @GehrmannSolutions

    J613 confirmation for @tonibergholm's adaptation of @aronvandepol's swap-record patch. We ported it to our tree with the J613 lid-closed set (#127) and changed three things there. This is our daily kernel since 2026-10-16, and since today we also run it on release 20 (5d570a78 + #127 + #135).

    Same as the adaptation: update byte 0x354, nits as a double at 0x35c, appledrm.j613_backlight=1 (default off), apple-panel-bl with the DT maximum 525 capped at 500.

    Different in our port:

    1. DPMS off turns the backlight off. A CRTC that goes off sends 0 nits with its black frame, so the panel is not left black but lit. The next frame after the CRTC or the panel comes back on resends the requested level. This includes panel power-on after the lid opens.
    2. A request goes out without a page flip. It rides in the next swap, or in a CRTC-only commit when no flip comes within a frame. The keys then also work on a static desktop.
    3. No brightness in swaps for a Type-C display on the J613 internal pipe. With the lid closed, j613: second external display with the lid closed (internal pipe on the left-front port) #127 lets the internal DCP drive the left-front port. Its swaps never carry a brightness.

    Checks on a J613 (MacBook Air 13" M3), internal panel:

    • brightnessctl 60 → 300 → 140 → 500: requested = sent each time, and the firmware logs each value (brightness value: 3932160 = 60 nits in 16.16, and so on). Omarchy keys and OSD work, and the panel visibly follows.
    • DPMS off: sent 0 with the black frame. DPMS on: the level comes back. No flip failure.
    • Lid closed for about 30 s (s2idle), then open: panel on, level resent, failed 0.
    • The level survives a reboot through systemd-backlight. It also works on release 20 today, with keys and OSD.

    Not tested: external displays together with a brightness change while the lid is open, and a photometer (requested nits only).

    Commit: GehrmannSolutions/linux-aurora@bbbda9f on branch tars/aurora-j613-r8-rel20 (release 20 + #127 + #135 + our J613 thermal/brightness patches).

    (Port, tests and this text by TARS, my Claude Code agent, with me watching the panel.)

  5. GehrmannSolutions commented on Nov 2, 2026

    @GehrmannSolutions

    For the J613 this is covered now: #312 (merged 2026-10-31) admits the J613 to the nits path with no module parameter. Integration also turns the backlight off with DPMS (dcp_v14_bl_dpms_applies / dcp_backlight_dpms), which was the one thing our port did differently. So we'll drop our J613 port (and appledrm.j613_backlight=1) when we move to 7.2 this week, and report here only if something behaves differently. J516S isn't affected by this.

    (Written with TARS, my Claude Code agent.)

  6. M4Marvin commented on Nov 3, 2026

    @M4Marvin

    j615 (MacBook Air 15" M3): the panel_nits admit flag alone does not yield a backlight — firmware reports "Could not find Backlight service"

    Status from a real j615 on linux-aurora 7.2.11.aurora14-1 (commit 4fdd0fc5c931ef9f14474942b37c3d451e97fc04), in response to the J613 coverage just landed via #312.

    1. On stock aurora14, /sys/class/backlight/ is empty, and every boot (including many pre-dating any testing below) logs:

      apple-dcp 28ec00000.dcp: RTKit: syslog message: nifiedPipeline.cpp:5404: IOMFB: Could not find Backlight service
      
    2. I rebuilt appledrm.ko out-of-tree against the exact installed build tree (/usr/src/linux-aurora, matching .config, MODVERSIONS and MODULE_SIG both off), changing only the j615 gate — exactly PR drm/apple: enable J613 panel brightness control #312's pattern for j613:

     	.panel_width = 2880,
     	.panel_height = 1864,
     	.promotion = false,
    +	.panel_nits = true,
     	.ctm_set = A(421),
     	.ctm_get = A(420),
    ...
     	return board && board->panel_nits &&
     		(machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s") ||
    -		 machine_is(ctx, "apple,j516c") || machine_is(ctx, "apple,j613"));
    +		 machine_is(ctx, "apple,j516c") || machine_is(ctx, "apple,j613") ||
    +		 machine_is(ctx, "apple,j615"));
    1. The module loads cleanly (vermagic 7.2.11-14-1-ARCH SMP preempt mod_unload aarch64, BTF built), the driver binds as usual — but the result is no backlight device: /sys/class/backlight/ stays empty, and the same "Could not find Backlight service" line is logged. The display pipeline itself is unaffected (fail-safe), and the module has been removed again (stock driver restored).

    So on j615 the nits path is blocked at least one layer below the admit flag: the 14.7 backend never finds the backlight service it needs. Questions:

    • Is the "Backlight service" lookup (iomfbep_cb_match_backlight_service / trampoline_create_backlight_service) expected to be firmware-profile dependent on the j615/T8122 image — cf. the J613/J615 per-profile qualification in M3 Air: qualify each shipped J613/J615 firmware profile separately #26 — or does j615 need a different service name/RPC in iomfb_v14_7.c?
    • Is there any additional state I can read out for you? /sys/kernel/debug/dcp-j615 exists but is empty; the dcp_v14_status backlight counters (nits_enabled/sent/presents/refused) are compiled into the module but I could not find an exposed debugfs file for them.

    For completeness on the alternatives: userspace software dimming doesn't provide a usable fallback on this platform either — hyprsunset's CTM-based dimming had no visible effect (we could not confirm the CTM actually reaches the j615 pipeline) and gammastep reports no gamma-adjustment support from the compositor — so hardware nits is the only brightness route.

    Machine is available for testing; patches, full logs and any experiment you want run on a real j615 can be arranged.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:displayAffected display subsystemlaunch:follow-upRetained improvements or broader retests outside the initial launchowner:kernelImplementation belongs to kernelpriority:P2Recovery, performance or hardware qualificationstatus:investigationFailure is open; mechanism needs confirmation

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions