You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
14.x M3 panels: implement real backlight control on J613 and J516S #32
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.
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.
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.
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__ */
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.
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.
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:
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.
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.
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).
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.
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.
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
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:
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.
Activity
aronvandepol commentedon Oct 11, 2026
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
0x354and the target as a little-endian double at0x35c(26.6:0x35e). Tested on 2026.10.11.14 (7cae5a7). The patch below is against7cae5a7and ports the 26.6 backlight code (dcp_v26_brightness_*) to the 14.x path, behind at6030_backlightparameter 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 at0x35e:brightness value: 0, and the panel went black and stayed black until a reboot. Nothing later requested a new level.0x354= 1 with0x355..0x3a0filled with0x40:2130048= 32.50 nits, which is0x4040404040404040as a double. Filling0x355..0x508crashed 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 from0x358:0. The same ladder from0x35c: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, andD208(backlight factor) never arrived during any of this.Results with the patch (
t6030_backlight=1, the module alone, without the #31 parameters):apple-panel-blregisters at the first frame with max 500, from the panel'sapple,max-brightness. systemd-backlight restored 250 nits across a reboot (dcp-t6030: requested 250, sent 250).off 1, sent 0), and DPMS on sent 250 again.dcp-t6030failedstayed 0, with noflip failed, GPU fault orDEVICE 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)
tonibergholm commentedon Oct 16, 2026
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, source5ec1028c81526e1ed31db2b97e47edf8663e83cb, kernel7.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 isv1.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 same0x354update byte and0x35cdouble-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-blregisters with max 500. Requests 100 → 300 → 150 changed the panel visibly and read back exactly; the starting 265 was restored.failed 0, requested/sent 265, no pending request.brightness value: 17367040(= 265 in 16.16).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.
GehrmannSolutions commentedon Oct 18, 2026
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 at0x35c,appledrm.j613_backlight=1(default off),apple-panel-blwith the DT maximum 525 capped at 500.Different in our port:
Checks on a J613 (MacBook Air 13" M3), internal panel:
brightnessctl60 → 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.sent 0with the black frame. DPMS on: the level comes back. No flip failure.failed 0.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.)
GehrmannSolutions commentedon Nov 2, 2026
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 (andappledrm.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.)
M4Marvin commentedon Nov 3, 2026
j615 (MacBook Air 15" M3): the
panel_nitsadmit 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(commit4fdd0fc5c931ef9f14474942b37c3d451e97fc04), in response to the J613 coverage just landed via #312.On stock aurora14,
/sys/class/backlight/is empty, and every boot (including many pre-dating any testing below) logs:I rebuilt
appledrm.koout-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: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:
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 iniomfb_v14_7.c?/sys/kernel/debug/dcp-j615exists but is empty; thedcp_v14_statusbacklight 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.