From 8ea9a7235304bb9ad612a22db461c798e0f8d6ec Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:08:12 -0700 Subject: [PATCH 1/4] drm/asahi: g15: start T6031 after its native display, then autoload it T6031 was kept out of the G15 autoload table until it had an admission gate ordering its first firmware start after the native display, as T6030 has through its display and PMP links. Give drm/apple one ready/defer helper with a publication slot per SoC display subsystem, published after DRM registration and client setup and withdrawn before unbind, and export apple_drm_t6031_link_device() beside the T6030 one. T6031 admission calls it after the J516C machine check and before anything else: it defers until that display is published, so a boot that leaves the display disabled keeps the GPU waiting (logged once) rather than starting it. T6030 is unchanged and T6031 gains no PMP vote. Then add apple,agx-t6031 to the alias table. A manual load on J516C now waits for the native display as well. The GPU still needs the m1n1 GPU handoff, the opt-in native display (apple_t6031_display.enable=1) and the pinned board file; this change provides none of them. tools/asahi/g15-display-order-tests.py runs the actual bodies on host models with mutants and is registered in tools/aurora-ci/host-tests.json. Generated-by: LLM Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- drivers/gpu/drm/apple/apple_drv.c | 71 +++-- drivers/gpu/drm/asahi/g15/boot.rs | 12 + drivers/gpu/drm/asahi/g15/glue.c | 23 ++ drivers/gpu/drm/asahi/g15/mod.rs | 16 +- include/linux/soc/apple/display.h | 1 + tools/asahi/g15-display-order-tests.py | 409 +++++++++++++++++++++++++ tools/aurora-ci/host-tests.json | 10 + 7 files changed, 514 insertions(+), 28 deletions(-) create mode 100644 tools/asahi/g15-display-order-tests.py diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 8bf6d56f708e04..26c28cc768fa0d 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -57,28 +57,44 @@ struct apple_drm_private { }; /* Component readiness is later than platform probe/bind. */ -static DEFINE_MUTEX(apple_t6030_display_lock); +static DEFINE_MUTEX(apple_g15_display_lock); static struct device *apple_t6030_display; +static struct device *apple_t6031_display; -static int apple_t6030_display_publish(struct device *dev) +/* The publication slot of a G15 SoC's internal display subsystem, or NULL. */ +static struct device **apple_g15_display_slot(struct device *dev) { - if (!of_device_is_compatible(dev->of_node, "apple,t6030-display-subsystem")) + if (of_device_is_compatible(dev->of_node, "apple,t6030-display-subsystem")) + return &apple_t6030_display; + if (of_device_is_compatible(dev->of_node, "apple,t6031-display-subsystem")) + return &apple_t6031_display; + return NULL; +} + +static int apple_g15_display_publish(struct device *dev) +{ + struct device **slot = apple_g15_display_slot(dev); + + if (!slot) return 0; - guard(mutex)(&apple_t6030_display_lock); - if (apple_t6030_display) - return apple_t6030_display == dev ? 0 : -EBUSY; - apple_t6030_display = get_device(dev); + guard(mutex)(&apple_g15_display_lock); + if (*slot) + return *slot == dev ? 0 : -EBUSY; + *slot = get_device(dev); return 0; } -static void apple_t6030_display_withdraw(struct device *dev) +static void apple_g15_display_withdraw(struct device *dev) { + struct device **slot = apple_g15_display_slot(dev); struct device *old = NULL; - scoped_guard(mutex, &apple_t6030_display_lock) { - if (apple_t6030_display == dev) { - old = apple_t6030_display; - apple_t6030_display = NULL; + if (!slot) + return; + scoped_guard(mutex, &apple_g15_display_lock) { + if (*slot == dev) { + old = *slot; + *slot = NULL; } } put_device(old); @@ -89,22 +105,35 @@ static void apple_t6030_display_withdraw(struct device *dev) * publish separately. This lock protects only publication and a device link; * no display calls or firmware RPC run while it is held. */ -int apple_drm_t6030_link_device(struct device *consumer) +static int apple_g15_link_device(struct device *consumer, const char *machine, + const char *gpu, struct device **display) { struct device_link *link; - if (!of_machine_is_compatible("apple,t6030") || !consumer->of_node || - !of_device_is_compatible(consumer->of_node, "apple,agx-t6030")) + if (!of_machine_is_compatible(machine) || !consumer->of_node || + !of_device_is_compatible(consumer->of_node, gpu)) return -ENODEV; - guard(mutex)(&apple_t6030_display_lock); - if (!apple_t6030_display) + guard(mutex)(&apple_g15_display_lock); + if (!*display) return -EPROBE_DEFER; - link = device_link_add(consumer, apple_t6030_display, - DL_FLAG_AUTOREMOVE_CONSUMER); + link = device_link_add(consumer, *display, DL_FLAG_AUTOREMOVE_CONSUMER); return link ? 0 : -EINVAL; } + +int apple_drm_t6030_link_device(struct device *consumer) +{ + return apple_g15_link_device(consumer, "apple,t6030", "apple,agx-t6030", + &apple_t6030_display); +} EXPORT_SYMBOL_GPL(apple_drm_t6030_link_device); +int apple_drm_t6031_link_device(struct device *consumer) +{ + return apple_g15_link_device(consumer, "apple,t6031", "apple,agx-t6031", + &apple_t6031_display); +} +EXPORT_SYMBOL_GPL(apple_drm_t6031_link_device); + DEFINE_DRM_GEM_DMA_FOPS(apple_fops); #define DART_PAGE_SIZE 16384 @@ -1089,7 +1118,7 @@ static int apple_drm_init(struct device *dev) drm_client_setup_with_fourcc(&apple->drm, DRM_FORMAT_XRGB8888); /* A subsequent successful platform probe retries deferred GPU probes. */ - ret = apple_t6030_display_publish(dev); + ret = apple_g15_display_publish(dev); if (ret) { drm_dev_unregister(&apple->drm); goto err_unbind; @@ -1111,7 +1140,7 @@ static void apple_drm_uninit(struct device *dev) { struct apple_drm_private *apple = dev_get_drvdata(dev); - apple_t6030_display_withdraw(dev); + apple_g15_display_withdraw(dev); apple_drm_quiesce_connectors(&apple->drm); drm_dev_unplug(&apple->drm); diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index 38f2444494805c..4db1a7cb097044 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -72,6 +72,7 @@ pub(crate) mod glue { fn asahi_g15_ioremap_wc(start: u64, size: usize) -> *mut core::ffi::c_void; fn asahi_g15_dma_coherent(dev: *mut core::ffi::c_void) -> bool; pub(crate) fn asahi_g15_pmp_prepare(dev: *mut core::ffi::c_void) -> core::ffi::c_int; + pub(crate) fn asahi_g15_display_prepare(dev: *mut core::ffi::c_void) -> core::ffi::c_int; pub(crate) fn asahi_g15_pmp_get(dev: *mut core::ffi::c_void) -> core::ffi::c_int; pub(crate) fn asahi_g15_pmp_put(dev: *mut core::ffi::c_void); fn asahi_g15_reserved_uat(index: u32, start: u64, size: u64) -> bool; @@ -482,6 +483,17 @@ pub(crate) fn admit( } else if !glue::machine(cfg.machine) { dev_err!(dev, "G15: not a {} machine\n", cfg.name); return Err(ENODEV); + } else { + // No PMP vote here, but the first start follows the native display all the same. + // SAFETY: `dev` is the bound GPU device. + let ret = unsafe { glue::asahi_g15_display_prepare(dev.as_raw().cast()) }; + if ret != 0 { + let err = Error::from_errno(ret); + if err != EPROBE_DEFER { + dev_err!(dev, "native display not admitted: {:?}\n", err); + } + return Err(err); + } } let adt = adt::read(dev, cfg) .inspect_err(|e| dev_err!(dev, "GPU regions not admitted: {:?}\n", e))?; diff --git a/drivers/gpu/drm/asahi/g15/glue.c b/drivers/gpu/drm/asahi/g15/glue.c index b087bb9ca954ee..09c5966b313a4f 100644 --- a/drivers/gpu/drm/asahi/g15/glue.c +++ b/drivers/gpu/drm/asahi/g15/glue.c @@ -34,6 +34,7 @@ int asahi_g15_adt_phram(u64 *start, u64 *size); void __iomem *asahi_g15_ioremap_wc(u64 start, size_t size); bool asahi_g15_dma_coherent(struct device *dev); int asahi_g15_pmp_prepare(struct device *dev); +int asahi_g15_display_prepare(struct device *dev); int asahi_g15_pmp_get(struct device *dev); void asahi_g15_pmp_put(struct device *dev); bool asahi_g15_reserved_uat(u32 index, u64 start, u64 size); @@ -141,6 +142,28 @@ int asahi_g15_pmp_prepare(struct device *dev) return apple_pmp_link_device(dev); } +/* + * T6031 has no PMP vote, but its first firmware start follows the native + * display as T6030's does: defer until that display is up. Its display is + * opt-in (apple_t6031_display.enable=1), so say once why the GPU waits. + */ +int asahi_g15_display_prepare(struct device *dev) +{ + struct device_node *np; + int ret; + + if (!dev->of_node || !of_device_is_compatible(dev->of_node, "apple,agx-t6031")) + return -ENODEV; + ret = apple_drm_t6031_link_device(dev); + if (ret != -EPROBE_DEFER) + return ret; + np = of_find_compatible_node(NULL, NULL, "apple,t6031-display-subsystem"); + if (!np || !of_device_is_available(np)) + dev_info_once(dev, "G15: waiting for the T6031 native display, which this boot did not enable (apple_t6031_display.enable=1 and the boot loader's display handoff)\n"); + of_node_put(np); + return ret; +} + int asahi_g15_pmp_get(struct device *dev) { if (!asahi_g15_board() || !dev->of_node || diff --git a/drivers/gpu/drm/asahi/g15/mod.rs b/drivers/gpu/drm/asahi/g15/mod.rs index af77a54f9c189a..4e20d3ecc1507c 100644 --- a/drivers/gpu/drm/asahi/g15/mod.rs +++ b/drivers/gpu/drm/asahi/g15/mod.rs @@ -14,9 +14,9 @@ //! //! # Plugging into the shared driver //! -//! - [`Driver`] is a separate platform driver. T6030 has a module alias; its probe must wait -//! for the native display and PMP supplier before booting firmware. T6031 retains explicit -//! module loading. It calls [`crate::driver::AsahiPlatformDriver::probe_g15`]. +//! - [`Driver`] is a separate platform driver with a module alias. Its probe must wait for +//! the native display (T6030 and T6031) and the PMP supplier (T6030) before booting +//! firmware. It calls [`crate::driver::AsahiPlatformDriver::probe_g15`]. //! - [`Gpu`] implements [`crate::gpu::Gpu`]: GET_PARAMS identity, VM creation (`new_vm`), //! queues (`new_queue`, see [`queue`]) and timestamp buffers. //! - User VMs are shared-layer [`mmu::Vm`]s on a [`mmu::Uat::new_adopted`] UAT with tracked @@ -152,14 +152,16 @@ const OF_TABLE: kernel::device_id::IdArray = ), ]); -// T6030 probe admission requires the native display and PMP supplier before -// firmware starts. T6031 retains explicit loading until it has an equivalent -// display-order admission gate. +// Probe admission requires the native display before firmware starts (T6030 +// also its PMP supplier), so both may be loaded at device creation. kernel::of_device_table!( AUTOLOAD_OF_TABLE, MODULE_OF_TABLE_G15, (), - [(of::DeviceId::new(c_str!("apple,agx-t6030")), ())] + [ + (of::DeviceId::new(c_str!("apple,agx-t6030")), ()), + (of::DeviceId::new(c_str!("apple,agx-t6031")), ()), + ] ); /// The G15 platform adapter: the shared DRM registration probed through diff --git a/include/linux/soc/apple/display.h b/include/linux/soc/apple/display.h index 8978d95d40484f..eb71ce91e09193 100644 --- a/include/linux/soc/apple/display.h +++ b/include/linux/soc/apple/display.h @@ -6,5 +6,6 @@ struct device; /* Initial native display admission, before starting retained G15 firmware. */ int apple_drm_t6030_link_device(struct device *consumer); +int apple_drm_t6031_link_device(struct device *consumer); #endif diff --git a/tools/asahi/g15-display-order-tests.py b/tools/asahi/g15-display-order-tests.py new file mode 100644 index 00000000000000..2fd7aa99a2bbe3 --- /dev/null +++ b/tools/asahi/g15-display-order-tests.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Run the actual G15 native-display start-order bodies on host models. + +The publication slots, the shared link helper and both SoC wrappers are taken +from drivers/gpu/drm/apple/apple_drv.c, the T6031 glue from g15/glue.c and +admit() from g15/boot.rs, unchanged. OF lookups, device references, links and +the admission steps are host models; no device or firmware is touched. Each +mutant must build and fail an intended assertion. +""" +import argparse +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +APPLE = "drivers/gpu/drm/apple/apple_drv.c" +GLUE = "drivers/gpu/drm/asahi/g15/glue.c" +BOOT = "drivers/gpu/drm/asahi/g15/boot.rs" +MODULE = "drivers/gpu/drm/asahi/g15/mod.rs" + + +def item(text, marker): + start = text.index(marker) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +def c_source(root): + apple = (root / APPLE).read_text() + glue = (root / GLUE).read_text() + state = [line for line in apple.splitlines() + if line in ("static DEFINE_MUTEX(apple_g15_display_lock);", + "static struct device *apple_t6030_display;", + "static struct device *apple_t6031_display;")] + assert len(state) == 3, f"publication state not found: {state}" + bodies = [item(apple, marker) for marker in ( + "\nstatic struct device **apple_g15_display_slot(struct device *dev)\n{", + "\nstatic int apple_g15_display_publish(struct device *dev)\n{", + "\nstatic void apple_g15_display_withdraw(struct device *dev)\n{", + "\nstatic int apple_g15_link_device(", + "\nint apple_drm_t6030_link_device(struct device *consumer)\n{", + "\nint apple_drm_t6031_link_device(struct device *consumer)\n{", + )] + bodies.append(item(glue, "\nint asahi_g15_display_prepare(struct device *dev)\n{")) + return "\n".join(state) + "\n\n" + "\n\n".join(bodies) + "\n" + + +C_PREFIX = r''' +#include +#include +#include +#include +#include + +#define ENODEV 19 +#define EBUSY 16 +#define EINVAL 22 +#define EPROBE_DEFER 517 +#define DL_FLAG_AUTOREMOVE_CONSUMER 1u +#define EXPORT_SYMBOL_GPL(sym) extern int sym##_exported + +struct device_node { const char *compatible; bool available; int refs; }; +struct device { struct device_node *of_node; int refs; }; +struct device_link { struct device *consumer, *supplier; unsigned int flags; }; +struct mutex { bool held; }; + +static const char *machine_compat[2]; +static struct device_node *nodes[4]; +static int links_made, link_fail, infos; +static struct device_link last_link; + +static int of_machine_is_compatible(const char *compat) +{ + for (int i = 0; i < 2; i++) + if (machine_compat[i] && !strcmp(machine_compat[i], compat)) + return 1; + return 0; +} +static int of_device_is_compatible(const struct device_node *np, const char *compat) +{ + return np && !strcmp(np->compatible, compat); +} +static struct device_node *of_find_compatible_node(struct device_node *from, const char *type, + const char *compat) +{ + (void)from; (void)type; + for (int i = 0; i < 4; i++) + if (nodes[i] && !strcmp(nodes[i]->compatible, compat)) { + nodes[i]->refs++; + return nodes[i]; + } + return NULL; +} +static bool of_device_is_available(const struct device_node *np) { return np && np->available; } +static void of_node_put(struct device_node *np) { if (np) np->refs--; } +static struct device *get_device(struct device *dev) { dev->refs++; return dev; } +static void put_device(struct device *dev) { if (dev) dev->refs--; } +static struct device_link *device_link_add(struct device *consumer, struct device *supplier, + unsigned int flags) +{ + if (link_fail) + return NULL; + links_made++; + last_link = (struct device_link){ consumer, supplier, flags }; + return &last_link; +} +#define dev_info_once(dev, ...) do { static bool once_; (void)(dev); if (!once_) { once_ = true; infos++; } } while (0) + +static void unlock_(struct mutex **m) { if (*m) { assert((*m)->held); (*m)->held = false; } } +static struct mutex *lock_(struct mutex *m) { assert(!m->held); m->held = true; return m; } +#define DEFINE_MUTEX(name) struct mutex name = { false } +#define guard(type) struct mutex *guard_ __attribute__((cleanup(unlock_))) = lock_ +#define scoped_guard(type, m) for (struct mutex *sg_ __attribute__((cleanup(unlock_))) = lock_(m), *done_ = NULL; !done_; done_ = (struct mutex *)1) +''' + +C_MAIN = r''' +static const char *scenario; +#define CHECK(c) do { if (!(c)) { fprintf(stderr, "%s:%d: %s\n", scenario, __LINE__, #c); exit(1); } } while (0) + +static struct device_node gpu31 = { "apple,agx-t6031", true }, gpu30 = { "apple,agx-t6030", true }; +static struct device_node disp31 = { "apple,t6031-display-subsystem", false }; +static struct device_node disp30 = { "apple,t6030-display-subsystem", true }; +static struct device_node other = { "apple,display-subsystem", true }; +static struct device g31 = { &gpu31 }, g30 = { &gpu30 }, none = { NULL }; +static struct device d31 = { &disp31 }, d31b = { &disp31 }, d30 = { &disp30 }, dother = { &other }; + +static void machine(const char *soc, const char *board) +{ + machine_compat[0] = board; + machine_compat[1] = soc; +} +static void clean(void) +{ + CHECK(!apple_g15_display_lock.held); + for (int i = 0; i < 4; i++) + CHECK(!nodes[i] || nodes[i]->refs == 0); +} +static int prepare(struct device *dev) { int ret = asahi_g15_display_prepare(dev); clean(); return ret; } +static int link30(struct device *dev) { int ret = apple_drm_t6030_link_device(dev); clean(); return ret; } +static int link31(struct device *dev) { int ret = apple_drm_t6031_link_device(dev); clean(); return ret; } +static int publish(struct device *dev) { int ret = apple_g15_display_publish(dev); clean(); return ret; } +static void withdraw(struct device *dev) { apple_g15_display_withdraw(dev); clean(); } + +int main(int argc, char **argv) +{ + scenario = argv[1]; + machine("apple,t6031", "apple,j516c"); + nodes[0] = &gpu31; + nodes[1] = &disp31; + nodes[2] = &disp30; + nodes[3] = &other; + if (!strcmp(scenario, "disabled")) { + /* The gate left the display disabled: no start, no success, said once. */ + CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 1); + CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 1 && links_made == 0); + } else if (!strcmp(scenario, "absent")) { + nodes[1] = NULL; + CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 1 && links_made == 0); + } else if (!strcmp(scenario, "pending")) { + disp31.available = true; + CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 0 && links_made == 0); + } else if (!strcmp(scenario, "ready")) { + disp31.available = true; + CHECK(publish(&d31) == 0 && d31.refs == 1); + CHECK(prepare(&g31) == 0 && links_made == 1 && infos == 0); + CHECK(last_link.consumer == &g31 && last_link.supplier == &d31 && + last_link.flags == DL_FLAG_AUTOREMOVE_CONSUMER); + link_fail = 1; + CHECK(prepare(&g31) == -EINVAL); + link_fail = 0; + CHECK(publish(&d31) == 0 && d31.refs == 1); + CHECK(publish(&d31b) == -EBUSY && d31b.refs == 0); + withdraw(&d31b); + CHECK(prepare(&g31) == 0); + withdraw(&d31); + CHECK(d31.refs == 0 && prepare(&g31) == -EPROBE_DEFER); + } else if (!strcmp(scenario, "isolation")) { + disp31.available = true; + CHECK(publish(&d30) == 0 && d30.refs == 1); + CHECK(publish(&dother) == 0 && dother.refs == 0); + withdraw(&dother); + CHECK(prepare(&g31) == -EPROBE_DEFER && links_made == 0); + } else if (!strcmp(scenario, "foreign")) { + disp31.available = true; + CHECK(publish(&d31) == 0); + CHECK(prepare(&g30) == -ENODEV && link31(&g30) == -ENODEV); + CHECK(prepare(&none) == -ENODEV && link31(&none) == -ENODEV); + machine("apple,t6030", "apple,j516s"); + CHECK(link31(&g31) == -ENODEV && links_made == 0); + } else if (!strcmp(scenario, "t6030")) { + machine("apple,t6030", "apple,j516s"); + CHECK(link30(&g30) == -EPROBE_DEFER); + disp31.available = true; + CHECK(publish(&d31) == 0 && link30(&g30) == -EPROBE_DEFER); + CHECK(publish(&d30) == 0 && link30(&g30) == 0 && last_link.supplier == &d30); + withdraw(&d30); + CHECK(d30.refs == 0 && link30(&g30) == -EPROBE_DEFER); + CHECK(link30(&g31) == -ENODEV); + } else { + CHECK(!"unknown scenario"); + } + return 0; +} +''' + +C_SCENARIOS = ["disabled", "absent", "pending", "ready", "isolation", "foreign", "t6030"] + +C_MUTANTS = { + "unpublished-succeeds": ("if (!*display)\n\t\treturn -EPROBE_DEFER;", "if (!*display)\n\t\treturn 0;"), + "disabled-display-skipped": (re.compile(r"\t\tdev_info_once\(dev,[^;]*;\n"), "\t\t{ of_node_put(np); return 0; }\n"), + "t6031-reads-t6030-slot": ("&apple_t6031_display);", "&apple_t6030_display);"), + "t6031-published-as-t6030": ("return &apple_t6031_display;", "return &apple_t6030_display;"), + "no-machine-check": ("!of_machine_is_compatible(machine) ||", ""), + "withdraw-keeps-display": ("*slot = NULL;", ";"), + "publish-without-reference": ("*slot = get_device(dev);", "*slot = dev;"), +} + +RS_PREFIX = r''' +#![allow(dead_code, unused_variables, unused_imports, non_snake_case)] +use std::cell::RefCell; +use std::marker::PhantomData; +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +struct Error(i32); +impl Error { fn from_errno(errno: i32) -> Self { Self(errno) } } +type Result = std::result::Result; +const ENODEV: Error = Error(-19); +const EINVAL: Error = Error(-22); +const EPROBE_DEFER: Error = Error(-517); +macro_rules! dev_err { ($($args:tt)*) => {{}}; } +macro_rules! dev_info { ($($args:tt)*) => {{}}; } +struct State { board: bool, machine: bool, pmp: i32, display: i32 } +thread_local! { + static CALLS: RefCell> = const { RefCell::new(Vec::new()) }; + static STATE: RefCell = const { RefCell::new(State { board: true, machine: true, pmp: 0, display: 0 }) }; +} +fn call(name: &'static str) { CALLS.with(|c| c.borrow_mut().push(name)); } +mod device { + pub struct Device; + impl Device { pub fn as_raw(&self) -> *mut u8 { std::ptr::null_mut() } } +} +mod platform { + pub struct Device(pub super::device::Device, pub C); + impl Device { pub fn as_ref(&self) -> &super::device::Device { &self.0 } } +} +struct Core<'a>(PhantomData<&'a ()>); +struct AsahiDevice; +mod hw { + #[derive(Copy, Clone, PartialEq, Eq)] pub enum Platform { AdtWithPmp, DeviceTree } + #[derive(Copy, Clone, PartialEq, Eq)] pub enum Board { None, Templates, Blob } +} +struct Config { platform: hw::Platform, machine: &'static str, name: &'static str, firmware: (), board: hw::Board } +mod adt { + pub struct GpuAdt; + pub fn read(_: &super::device::Device, _: &super::Config) -> super::Result { super::call("adt"); Ok(GpuAdt) } +} +mod identity { + pub fn firmware(_: &super::device::Device, _: &super::adt::GpuAdt, _: ()) -> super::Result { super::call("identity"); Ok(()) } +} +mod initdata { + pub fn prepare_board(_: &super::device::Device, _: &super::Config, _: &super::adt::GpuAdt) -> super::Result { + super::call("board_images"); Ok(1) + } +} +mod regs { + pub struct Regs; + impl Regs { + pub fn new(_: &super::platform::Device) -> super::Result { super::call("regs"); Ok(Self) } + pub fn asc_control(&self) -> super::Result { Ok(0) } + } +} +mod glue { + use super::{call, STATE}; + pub fn board() -> bool { call("board"); STATE.with(|s| s.borrow().board) } + pub fn machine(_: &str) -> bool { call("machine"); STATE.with(|s| s.borrow().machine) } + pub unsafe fn asahi_g15_pmp_prepare(_: *mut core::ffi::c_void) -> core::ffi::c_int { call("pmp"); STATE.with(|s| s.borrow().pmp) } + pub unsafe fn asahi_g15_display_prepare(_: *mut core::ffi::c_void) -> core::ffi::c_int { call("display"); STATE.with(|s| s.borrow().display) } + pub fn dma_coherent(_: &super::device::Device) -> bool { call("coherent"); false } + pub fn hold_power(_: &super::device::Device) { call("hold"); } +} +struct Admitted { regs: regs::Regs, adt: adt::GpuAdt, board_images: u32 } +''' + +RS_MAIN = r''' +static T6031: Config = Config { platform: hw::Platform::DeviceTree, machine: "apple,j516c", name: "T6031 G15C", firmware: (), board: hw::Board::Blob }; +static T6030: Config = Config { platform: hw::Platform::AdtWithPmp, machine: "apple,t6030", name: "T6030 G15S", firmware: (), board: hw::Board::Templates }; +const TAIL: [&str; 6] = ["adt", "identity", "coherent", "board_images", "regs", "hold"]; +fn run(cfg: &'static Config, machine: bool, pmp: i32, display: i32) -> (Result<()>, Vec<&'static str>) { + CALLS.with(|c| c.borrow_mut().clear()); + STATE.with(|s| *s.borrow_mut() = State { board: true, machine, pmp, display }); + let pdev = platform::Device(device::Device, Core(PhantomData)); + let ret = admit(&pdev, &AsahiDevice, cfg).map(|_| ()); + (ret, CALLS.with(|c| c.borrow().clone())) +} +fn with_tail(head: &[&'static str]) -> Vec<&'static str> { head.iter().chain(TAIL.iter()).copied().collect() } +fn main() { + assert_eq!(run(&T6031, true, 0, -517), (Err(EPROBE_DEFER), vec!["machine", "display"]), "T6031 defers before firmware"); + assert_eq!(run(&T6031, true, 0, -19), (Err(ENODEV), vec!["machine", "display"]), "T6031 refusal stops admission"); + assert_eq!(run(&T6031, true, 0, 0), (Ok(()), with_tail(&["machine", "display"])), "T6031 admitted after its display"); + assert_eq!(run(&T6031, false, 0, 0), (Err(ENODEV), vec!["machine"]), "the machine check comes first"); + assert_eq!(run(&T6030, true, 0, -517), (Ok(()), with_tail(&["board", "pmp"])), "T6030 path unchanged"); + assert_eq!(run(&T6030, true, -517, 0), (Err(EPROBE_DEFER), vec!["board", "pmp"]), "T6030 still defers on its PMP link"); +} +''' + +RS_MUTANTS = { + "display-refusal-ignored": ("dev_err!(dev, \"native display not admitted: {:?}\\n\", err);\n }\n return Err(err);", + "dev_err!(dev, \"native display not admitted: {:?}\\n\", err);\n }"), + "display-order-skipped": ("let ret = unsafe { glue::asahi_g15_display_prepare(dev.as_raw().cast()) };", "let ret = 0;"), +} + + +def mutate(source, change): + old, new = change + if isinstance(old, re.Pattern): + mutated, count = old.subn(new, source, count=1) + else: + count = source.count(old) + mutated = source.replace(old, new, 1) + assert count >= 1, f"mutation anchor not found: {old!r}" + return mutated + + +def compile_c(work, source, name): + src = work / f"{name}.c" + exe = work / name + src.write_text(C_PREFIX + source + C_MAIN) + result = subprocess.run(["gcc", "-std=gnu11", "-Wall", "-Werror", "-Wno-unused-function", + "-o", str(exe), str(src)], capture_output=True, text=True) + if result.returncode: + raise SystemExit(f"{name}: does not build:\n{result.stderr}") + return exe + + +def c_failures(exe): + return [s for s in C_SCENARIOS if subprocess.run([str(exe), s], capture_output=True).returncode] + + +def compile_rs(work, rustc, source, name): + src = work / f"{name}.rs" + exe = work / name + src.write_text(RS_PREFIX + source + RS_MAIN) + result = subprocess.run([rustc, "--edition=2021", "-o", str(exe), str(src)], capture_output=True, text=True) + if result.returncode: + raise SystemExit(f"{name}: does not build with {rustc}:\n{result.stderr}") + return exe + + +def static_checks(root): + apple = (root / APPLE).read_text() + module = (root / MODULE).read_text() + glue = (root / GLUE).read_text() + compat = re.compile(r'of::DeviceId::new\(c_str!\("([^"]+)"\)\)') + start = module.index("kernel::of_device_table!(\n AUTOLOAD_OF_TABLE,") + autoload = module[start:module.index("\n);", start)] + start = module.index("const OF_TABLE:") + matched = module[start:module.index("]);", start)] + assert compat.findall(autoload) == ["apple,agx-t6030", "apple,agx-t6031"], "autoload table" + assert compat.findall(matched) == ["apple,agx-t6030", "apple,agx-t6031"], "match table" + init = item(apple, "\nstatic int apple_drm_init(struct device *dev)\n{") + order = [init.index(s) for s in ("drm_dev_register(", "drm_client_setup_with_fourcc(", "apple_g15_display_publish(dev)")] + assert order == sorted(order), "publication must follow DRM registration and client setup" + uninit = item(apple, "\nstatic void apple_drm_uninit(struct device *dev)\n{") + assert uninit.index("apple_g15_display_withdraw(dev)") < uninit.index("component_unbind_all("), "withdraw before unbind" + assert "apple_t6030_display_publish" not in apple and "apple_t6030_display_withdraw" not in apple + pmp = item(glue, "\nint asahi_g15_pmp_prepare(struct device *dev)\n{") + assert pmp.index("apple_drm_t6030_link_device(dev)") < pmp.index("apple_pmp_link_device(dev)"), "T6030 order" + return 5 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument("--rustc", action="append", default=[], help="rustc to build the admit() model with") + args = parser.parse_args() + rustcs = args.rustc or ["rustc"] + checks = static_checks(args.root) + source = c_source(args.root) + admit = item((args.root / BOOT).read_text(), "pub(crate) fn admit(") + with tempfile.TemporaryDirectory(prefix="g15-display-order-", dir=os.environ.get("TMPDIR")) as tmp: + work = Path(tmp) + failed = c_failures(compile_c(work, source, "control")) + assert not failed, f"control scenarios failed: {failed}" + for name, change in C_MUTANTS.items(): + failed = c_failures(compile_c(work, mutate(source, change), f"mutant-{name}")) + assert failed, f"C mutant {name} survived" + print(f"mutant {name}: caught by {', '.join(failed)}") + for rustc in rustcs: + exe = compile_rs(work, rustc, admit, "admit") + run = subprocess.run([str(exe)], capture_output=True, text=True) + assert run.returncode == 0, f"admit() control failed with {rustc}:\n{run.stderr}" + for name, change in RS_MUTANTS.items(): + exe = compile_rs(work, rustc, mutate(admit, change), f"admit-{name}") + run = subprocess.run([str(exe)], capture_output=True, text=True) + assert run.returncode, f"admit() mutant {name} survived with {rustc}" + print(f"mutant {name} ({rustc}): caught") + print(f"PASS: {checks} static checks, {len(C_SCENARIOS)} C scenarios, " + f"{len(C_MUTANTS)} C mutants, admit() on {len(rustcs)} rustc, {len(RS_MUTANTS)} admit() mutants") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index 1310ec2bb60146..6fcf5f811cd7c9 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -2188,6 +2188,16 @@ "rust": "1.93.1", "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." }, + { + "name": "tools/asahi/g15-display-order-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-display-order-tests.py" + ], + "rust": "1.93.1", + "note": "Actual display publication, T6030/T6031 link and admit() bodies on OF/device-link and admission-order models; C and admit() mutants must build and fail. No hardware or private fixtures." + }, { "name": "tools/testing/selftests/drivers/gpu/asahi-g15/run.py", "group": "tools", From c69f2b118865d319f61ed453b23de6cd12a1ada6 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:47:43 -0700 Subject: [PATCH 2/4] tools/asahi: t6030 display admission tests: follow the shared display slots The T6030 display publisher and link now go through the shared G15 display slots and link helper (8ea9a723). This suite's anchors and C fixture still named the removed apple_t6030_display_publish/withdraw and their lock, so it failed at its first source-order check. Extract the slot, publish and withdraw functions, the shared link helper and the T6030 wrapper, and give the fixture the shared lock and the T6031 slot. The unrelated-display control now uses a non-G15 display subsystem. A new case checks that a published T6031 display takes its own slot and never satisfies T6030 admission. The seven mutants are carried over to the shared code; wrong-gpu keeps the helper's parameter used so that it still builds. One more mutant maps the T6031 slot onto the T6030 one. Every mutant builds and is killed. Production code is unchanged. Generated-by: LLM Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- tools/asahi/t6030-display-admission-tests.c | 38 +++++++++++++------- tools/asahi/t6030-display-admission-tests.py | 29 ++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/tools/asahi/t6030-display-admission-tests.c b/tools/asahi/t6030-display-admission-tests.c index e2c75eddb5ea53..ebf3e2e868324f 100644 --- a/tools/asahi/t6030-display-admission-tests.c +++ b/tools/asahi/t6030-display-admission-tests.c @@ -22,12 +22,14 @@ struct device { struct device_link { int unused; }; -static pthread_mutex_t apple_t6030_display_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t apple_g15_display_lock = PTHREAD_MUTEX_INITIALIZER; static struct device *apple_t6030_display; +static struct device *apple_t6031_display; static struct device_node display_node = { "apple,t6030-display-subsystem" }, gpu_node = { "apple,agx-t6030" }, - other_node = { "apple,t6031-display-subsystem" }; -static struct device display, duplicate, gpu; + other_node = { "apple,display-subsystem" }, + t6031_display_node = { "apple,t6031-display-subsystem" }; +static struct device display, duplicate, gpu, t6031_display; static const char *machine = "apple,t6030"; static int pmp_calls, link_calls, pmp_result; static bool link_ok = true; @@ -106,7 +108,7 @@ static int apple_pmp_link_device(struct device *dev) #include "production.h" static void reset(void) { - assert(!apple_t6030_display && !locked); + assert(!apple_t6030_display && !apple_t6031_display && !locked); display = (struct device){ .of_node = &display_node, .refs = 1 }; duplicate = (struct device){ .of_node = &display_node, .refs = 1 }; gpu = (struct device){ .of_node = &gpu_node, .refs = 1 }; @@ -127,7 +129,7 @@ static void *admit_thread(void *unused) static void *withdraw_thread(void *unused) { (void)unused; - apple_t6030_display_withdraw(&display); + apple_g15_display_withdraw(&display); atomic_store(&withdrew, true); return NULL; } @@ -140,18 +142,18 @@ int main(void) assert(asahi_g15_pmp_prepare(&gpu) == -EPROBE_DEFER && !pmp_calls); cases++; /* Component platform bind alone never calls this publication. Retry succeeds only afterward. */ - assert(apple_t6030_display_publish(&display) == 0 && + assert(apple_g15_display_publish(&display) == 0 && atomic_load(&display.refs) == 2); cases++; assert(asahi_g15_pmp_prepare(&gpu) == 0 && pmp_calls == 1); cases++; - assert(apple_t6030_display_publish(&display) == 0 && + assert(apple_g15_display_publish(&display) == 0 && atomic_load(&display.refs) == 2); cases++; - assert(apple_t6030_display_publish(&duplicate) == -EBUSY && + assert(apple_g15_display_publish(&duplicate) == -EBUSY && apple_t6030_display == &display); cases++; - apple_t6030_display_withdraw(&duplicate); + apple_g15_display_withdraw(&duplicate); assert(apple_t6030_display == &display && atomic_load(&display.refs) == 2); cases++; @@ -170,17 +172,27 @@ int main(void) assert(apple_drm_t6030_link_device(&gpu) == -ENODEV); cases++; gpu.of_node = &gpu_node; - apple_t6030_display_withdraw(&display); + apple_g15_display_withdraw(&display); assert(atomic_load(&display.refs) == 1 && !apple_t6030_display); cases++; assert(asahi_g15_pmp_prepare(&gpu) == -EPROBE_DEFER && pmp_calls == 2); cases++; display.of_node = &other_node; - assert(apple_t6030_display_publish(&display) == 0 && - !apple_t6030_display && atomic_load(&display.refs) == 1); + assert(apple_g15_display_publish(&display) == 0 && + !apple_t6030_display && !apple_t6031_display && + atomic_load(&display.refs) == 1); + cases++; + /* The T6031 display has its own slot: it never satisfies T6030 admission. */ + t6031_display = (struct device){ .of_node = &t6031_display_node, .refs = 1 }; + assert(apple_g15_display_publish(&t6031_display) == 0 && + apple_t6031_display == &t6031_display && !apple_t6030_display && + atomic_load(&t6031_display.refs) == 2); + assert(asahi_g15_pmp_prepare(&gpu) == -EPROBE_DEFER && pmp_calls == 2); + apple_g15_display_withdraw(&t6031_display); + assert(!apple_t6031_display && atomic_load(&t6031_display.refs) == 1); cases++; reset(); - assert(!apple_t6030_display_publish(&display)); + assert(!apple_g15_display_publish(&display)); atomic_store(&pause_link, true); pthread_t admit, withdraw; assert(!pthread_create(&admit, NULL, admit_thread, NULL)); diff --git a/tools/asahi/t6030-display-admission-tests.py b/tools/asahi/t6030-display-admission-tests.py index a1da987377f182..479c11b00fed01 100644 --- a/tools/asahi/t6030-display-admission-tests.py +++ b/tools/asahi/t6030-display-admission-tests.py @@ -2,9 +2,11 @@ # SPDX-License-Identifier: GPL-2.0-only """Actual T6030 display publisher and G15 admission, with pthread boundaries. -Tests initial admission and pointer lifetime, not safe shutdown of retained G15 -firmware after forced display unbind. Full DRM publication placement is checked -as source ordering separately from executable host boundary controls. +The publisher is the shared G15 display slot code; T6030 admission goes through +the shared link helper. Tests initial admission and pointer lifetime, not safe +shutdown of retained G15 firmware after forced display unbind. Full DRM +publication placement is checked as source ordering separately from executable +host boundary controls. """ import argparse import hashlib @@ -26,23 +28,26 @@ def main(): glue = (ROOT / 'drivers/gpu/drm/asahi/g15/glue.c').read_text() init = extract.function(drv, 'static int apple_drm_init(') uninit = extract.function(drv, 'static void apple_drm_uninit(') - assert init.index('ret = drm_dev_register(') < init.index('drm_client_setup_with_fourcc(') < init.index('ret = apple_t6030_display_publish(') - assert uninit.index('apple_t6030_display_withdraw(') < uninit.index('apple_drm_quiesce_connectors(') < uninit.index('drm_dev_unplug(') + assert init.index('ret = drm_dev_register(') < init.index('drm_client_setup_with_fourcc(') < init.index('ret = apple_g15_display_publish(') + assert uninit.index('apple_g15_display_withdraw(') < uninit.index('apple_drm_quiesce_connectors(') < uninit.index('drm_dev_unplug(') assert 'drm_dev_unregister(&apple->drm);\n\t\tgoto err_unbind;' in init - code = '\n'.join([extract.function(drv, 'static int apple_t6030_display_publish('), - extract.function(drv, 'static void apple_t6030_display_withdraw('), + code = '\n'.join([extract.function(drv, 'static struct device **apple_g15_display_slot('), + extract.function(drv, 'static int apple_g15_display_publish('), + extract.function(drv, 'static void apple_g15_display_withdraw('), + extract.function(drv, 'static int apple_g15_link_device('), extract.function(drv, 'int apple_drm_t6030_link_device('), extract.function(glue, 'int asahi_g15_pmp_prepare(struct device *dev)\n')]) variants = [('control', code)] if args.mutations: for name, old, new in [ - ('no-ready-gate', 'if (!apple_t6030_display)', 'if (false)'), - ('no-display-get', 'apple_t6030_display = get_device(dev);', 'apple_t6030_display = dev;'), + ('no-ready-gate', 'if (!*display)', 'if (false)'), + ('no-display-get', '*slot = get_device(dev);', '*slot = dev;'), ('no-display-put', 'put_device(old);', '(void)old;'), - ('replace-live-provider', 'return apple_t6030_display == dev ? 0 : -EBUSY;', 'apple_t6030_display = dev;'), + ('replace-live-provider', 'return *slot == dev ? 0 : -EBUSY;', '*slot = dev;'), ('ignore-display-error', 'if (ret)\n\t\treturn ret;', 'if (false)\n\t\treturn ret;'), - ('wrong-gpu', '!of_device_is_compatible(consumer->of_node, "apple,agx-t6030")', 'false'), - ('withdraw-wrong-provider', 'if (apple_t6030_display == dev)', 'if (((void)dev, true))'), + ('wrong-gpu', '!of_device_is_compatible(consumer->of_node, gpu)', '((void)gpu, false)'), + ('withdraw-wrong-provider', 'if (*slot == dev)', 'if (((void)dev, true))'), + ('shared-display-slot', 'return &apple_t6031_display;', 'return &apple_t6030_display;'), ]: assert code.count(old) == 1, (name, code.count(old)) variants.append((name, code.replace(old, new))) From 41aa66da56ba28868732848bc88544bf0c445df4 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:32:42 -0700 Subject: [PATCH 3/4] soc: apple: t6030-display-gate: default the T6031 native display on J516C The T6031 native display gate ran only with apple_t6031_display.enable=1 on the kernel command line, so a J516C whose boot loader hands the display over still stayed on the boot framebuffer, and with it the G15 GPU, which waits for that display before its first firmware start. Make the T6031 switch tri-state: unset, the native display is requested on the J516C (root compatible apple,j516c) and not on the other T6031 boards. An explicit apple_t6031_display.enable=0 or =1 wins on every T6031 board, and a value kstrtobool() rejects preserves the previous explicit choice, or the board default when unset. The request is resolved once in gate_run(), before the existing checks. The handoff marker, the boot loader's PMP values, the PMP image, the power layout and every refusal after them are unchanged, so a J516C without a complete handoff keeps the framebuffer and the GPU keeps waiting. T6030, T8122, J514C and T6034 behaviour is unchanged. Update the help, the device tree comment and the G15 wait message, which said that =1 is always needed. The KUnit suite saves and restores the switch and the default board, checks the switch values and their resolution, and runs the default-board path through gate_run() with the test tree's own root: on by default, off with =0, and untouched without the handoff marker. Generated-by: LLM Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- arch/arm64/boot/dts/apple/t6031-dcp.dtsi | 7 +- drivers/gpu/drm/asahi/g15/glue.c | 6 +- drivers/soc/apple/Kconfig | 16 ++-- drivers/soc/apple/t6030-display-gate-test.c | 86 ++++++++++++++++++--- drivers/soc/apple/t6030-display-gate.c | 49 +++++++++--- 5 files changed, 132 insertions(+), 32 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6031-dcp.dtsi b/arch/arm64/boot/dts/apple/t6031-dcp.dtsi index 6d3b53cd95e576..f679c57b97709c 100644 --- a/arch/arm64/boot/dts/apple/t6031-dcp.dtsi +++ b/arch/arm64/boot/dts/apple/t6031-dcp.dtsi @@ -9,9 +9,10 @@ * interrupts and DART windows are those of the J516C ADT. * * Every node stays status = "disabled". The display gate enables the DCP, - * its mailbox, the two DARTs and the display subsystem only when - * apple_t6031_display.enable=1 is on the kernel command line and the boot - * loader has set apple,t6031-handoff = <1>. Compatibles are t6031-specific, + * its mailbox, the two DARTs and the display subsystem only when the boot + * loader has set apple,t6031-handoff = <1> and the native display is + * requested: by default on the J516C, with apple_t6031_display.enable=1 on + * the other boards, never with apple_t6031_display.enable=0. Compatibles are t6031-specific, * so a kernel that does not know them leaves the nodes unbound. * * Only the T6031 board files include this file: t6034.dtsi includes diff --git a/drivers/gpu/drm/asahi/g15/glue.c b/drivers/gpu/drm/asahi/g15/glue.c index 09c5966b313a4f..439238e888d7c4 100644 --- a/drivers/gpu/drm/asahi/g15/glue.c +++ b/drivers/gpu/drm/asahi/g15/glue.c @@ -144,8 +144,8 @@ int asahi_g15_pmp_prepare(struct device *dev) /* * T6031 has no PMP vote, but its first firmware start follows the native - * display as T6030's does: defer until that display is up. Its display is - * opt-in (apple_t6031_display.enable=1), so say once why the GPU waits. + * display as T6030's does: defer until that display is up. The display can + * be off for this boot (no handoff, or not requested), so say once why. */ int asahi_g15_display_prepare(struct device *dev) { @@ -159,7 +159,7 @@ int asahi_g15_display_prepare(struct device *dev) return ret; np = of_find_compatible_node(NULL, NULL, "apple,t6031-display-subsystem"); if (!np || !of_device_is_available(np)) - dev_info_once(dev, "G15: waiting for the T6031 native display, which this boot did not enable (apple_t6031_display.enable=1 and the boot loader's display handoff)\n"); + dev_info_once(dev, "G15: waiting for the T6031 native display, which this boot did not enable (it needs the boot loader's display handoff, and apple_t6031_display.enable=1 except on the J516C)\n"); of_node_put(np); return ret; } diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index 61a46c92ee3f17..e4b9ed2bb77cdf 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -228,9 +228,10 @@ config APPLE_T6030_DISPLAY_GATE nomodeset. It also serves T6031 (M3 Max) device trees with apple,t6031-handoff, - in the same way, but only for a boot with - apple_t6031_display.enable=1 on the kernel command line, and only - with the PMP values the boot loader passes. + in the same way and only with the PMP values the boot loader passes. + There the native display is on by default on the J516C, unless + apple_t6031_display.enable=0; the other T6031 boards need + apple_t6031_display.enable=1 on the kernel command line. Say 'y' if you want the native display on M3 Pro MacBook Pros. @@ -258,10 +259,11 @@ config APPLE_DISPLAY_GATE_KUNIT_TEST default KUNIT_ALL_TESTS help Runs the display gate on test device trees with the T6030, T6031 - and T8122 display nodes, without Apple hardware. On T6031: off by - default, enabled only by apple_t6031_display.enable=1, only with the - boot loader's handoff marker and this Mac's PMP values, and with - nothing changed when a check fails. On T6030: on by default, as + and T8122 display nodes, without Apple hardware. On T6031: on by + default only on the default board (the J516C), otherwise only with + apple_t6031_display.enable=1, off with apple_t6031_display.enable=0, + only with the boot loader's handoff marker and this Mac's PMP values, + and with nothing changed when a check fails. On T6030: on by default, as before, whatever the T6031 option says. On T8122: on by default, and nothing changed when the handoff or the device tree's own PMP topology does not check out. diff --git a/drivers/soc/apple/t6030-display-gate-test.c b/drivers/soc/apple/t6030-display-gate-test.c index a970405c6205e6..15725a38ab5c7d 100644 --- a/drivers/soc/apple/t6030-display-gate-test.c +++ b/drivers/soc/apple/t6030-display-gate-test.c @@ -7,9 +7,11 @@ * - T6030 (t6030-gate-test.dtso): on by default, as before, and * the T6031 switch does not change it. Run first: the T6031 and T8122 * trees describe a PMP, and the T6030 gate refuses to add a second one. - * - T6031 (t6031-gate-test.dtso): off by default, enabled only by - * apple_t6031_display.enable=1 with the full handoff, and nothing changed - * when a check fails. + * - T6031 (t6031-gate-test.dtso): on by default only on the default board + * (the J516C; the cases point it at the test tree's own root to run that + * path), otherwise only with apple_t6031_display.enable=1, never with + * apple_t6031_display.enable=0; always with the full handoff, and nothing + * changed when a check fails. * - T8122 (t8122-gate-test.dtso): on by default, with the full handoff, * and nothing changed when a check of the handoff or of the reused PMP * topology fails. @@ -143,6 +145,8 @@ struct gate_test { const struct gt_soc *s; struct of_changeset cs; /* this case's own edits to the test tree */ bool requested, t6031_requested; + int t6031_switch; + const char *t6031_board; }; static struct device_node *__init gt_node(struct kunit *test, const char *path) @@ -392,6 +396,8 @@ static int __init gt_init(struct kunit *test, const struct gt_soc *s) /* As the boot left them: the test kernel's command line sets neither. */ t->requested = gate_requested; t->t6031_requested = gate_t6031_requested; + t->t6031_switch = gate_t6031_switch; + t->t6031_board = gate_t6031_board; of_changeset_init(&t->cs); gate_soc = s->soc; gate_pmp_values = NULL; @@ -399,6 +405,7 @@ static int __init gt_init(struct kunit *test, const struct gt_soc *s) gate_pmp_reused = false; gate_requested = true; gate_t6031_requested = false; + gate_t6031_switch = -1; if (gt_live_soc) kunit_skip(test, "the booted device tree has its own /soc"); return 0; @@ -444,6 +451,8 @@ static void __init gt_exit(struct kunit *test) gate_pmp_values = NULL; gate_requested = t->requested; gate_t6031_requested = t->t6031_requested; + gate_t6031_switch = t->t6031_switch; + gate_t6031_board = t->t6031_board; gate_soc = NULL; } @@ -455,29 +464,82 @@ static void __init gate_t6031_param(struct kunit *test) struct gate_test *t = test->priv; KUNIT_EXPECT_FALSE(test, t->t6031_requested); + KUNIT_EXPECT_EQ(test, t->t6031_switch, -1); + KUNIT_EXPECT_STREQ(test, t->t6031_board, "apple,j516c"); KUNIT_EXPECT_TRUE(test, t->requested); KUNIT_EXPECT_EQ(test, gate_setup_t6031("1"), 0); - KUNIT_EXPECT_TRUE(test, gate_t6031_requested); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, 1); KUNIT_EXPECT_EQ(test, gate_setup_t6031("0"), 0); - KUNIT_EXPECT_FALSE(test, gate_t6031_requested); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, 0); + /* A rejected value sets nothing: neither an explicit 1 nor a reset to unset. */ KUNIT_EXPECT_NE(test, gate_setup_t6031("maybe"), 0); - KUNIT_EXPECT_FALSE(test, gate_t6031_requested); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, 0); KUNIT_EXPECT_NE(test, gate_setup_t6031(NULL), 0); - KUNIT_EXPECT_FALSE(test, gate_t6031_requested); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, 0); + gate_t6031_switch = -1; + KUNIT_EXPECT_NE(test, gate_setup_t6031("maybe"), 0); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, -1); /* The T6030 and T8122 escapes do not touch it, nor it them. */ KUNIT_EXPECT_EQ(test, gate_setup("0"), 0); KUNIT_EXPECT_FALSE(test, gate_requested); - KUNIT_EXPECT_FALSE(test, gate_t6031_requested); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, -1); KUNIT_EXPECT_EQ(test, gate_setup_t6031("1"), 0); KUNIT_EXPECT_FALSE(test, gate_requested); KUNIT_EXPECT_EQ(test, gate_setup_t8122("1"), 0); KUNIT_EXPECT_TRUE(test, gate_requested); - KUNIT_EXPECT_TRUE(test, gate_t6031_requested); + KUNIT_EXPECT_EQ(test, gate_t6031_switch, 1); +} + +/* An explicit 0 or 1 wins; unset, only the default board asks for it. */ +static void __init gate_t6031_resolution(struct kunit *test) +{ + KUNIT_EXPECT_TRUE(test, gate_t6031_wanted(-1, true)); + KUNIT_EXPECT_FALSE(test, gate_t6031_wanted(-1, false)); + KUNIT_EXPECT_FALSE(test, gate_t6031_wanted(0, true)); + KUNIT_EXPECT_FALSE(test, gate_t6031_wanted(0, false)); + KUNIT_EXPECT_TRUE(test, gate_t6031_wanted(1, true)); + KUNIT_EXPECT_TRUE(test, gate_t6031_wanted(1, false)); +} + +/* Points the default board at the test tree's root, so gate_run() takes that path. */ +static void __init gt_default_board(struct kunit *test) +{ + const char *root = NULL; + + if (of_property_read_string(of_root, "compatible", &root) || !root) + kunit_skip(test, "the test tree's root has no compatible"); + gate_t6031_board = root; +} + +/* On the default board, the full handoff and no switch: enabled. */ +static void __init gate_t6031_default_board_on(struct kunit *test) +{ + gt_default_board(test); + gt_run(test); + gt_expect_enabled(test); +} + +/* apple_t6031_display.enable=0 still wins on the default board. */ +static void __init gate_t6031_default_board_switched_off(struct kunit *test) +{ + gt_default_board(test); + KUNIT_ASSERT_EQ(test, gate_setup_t6031("0"), 0); + gt_run(test); + gt_expect_untouched(test); +} + +/* The default asks for the display; it does not stand in for the handoff. */ +static void __init gate_t6031_default_board_no_marker(struct kunit *test) +{ + gt_default_board(test); + gt_remove(test, gt_t6031.dcp, "apple,t6031-handoff"); + gt_run(test); + gt_expect_untouched(test); } -/* With a complete handoff and no command line switch, nothing changes. */ +/* Off the default board, with a complete handoff and no switch, nothing changes. */ static void __init gate_t6031_off_by_default(struct kunit *test) { gt_run(test); @@ -826,7 +888,11 @@ static void __init gate_t8122_enabled_again(struct kunit *test) /* The case and suite structs outlive init: only the functions are init code. */ static struct kunit_case gate_t6031_cases[] __refdata = { KUNIT_CASE(gate_t6031_param), + KUNIT_CASE(gate_t6031_resolution), KUNIT_CASE(gate_t6031_off_by_default), + KUNIT_CASE(gate_t6031_default_board_on), + KUNIT_CASE(gate_t6031_default_board_switched_off), + KUNIT_CASE(gate_t6031_default_board_no_marker), KUNIT_CASE(gate_t6031_switched_off), KUNIT_CASE(gate_t6031_enabled), KUNIT_CASE(gate_t6031_no_dcp_marker), diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index 2f73104b05c8cc..5b9aa91878e6b5 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -53,9 +53,10 @@ * option. * * On T6031 (M3 Max) the gate serves the same handoff, marked - * apple,t6031-handoff, but only when apple_t6031_display.enable=1 is on the - * kernel command line: the native display is off by default there until it - * has been seen to work. The PMP values must come from the boot loader, in + * apple,t6031-handoff. Its default is scoped to the J516C: there the native + * display is on unless apple_t6031_display.enable=0; the other T6031 boards + * need apple_t6031_display.enable=1. The default changes no check of the + * handoff or the PMP below. The PMP values must come from the boot loader, in * /chosen/asahi,t6031-pmp; there is no fallback. Its PMP topology is * reused from the device tree, as on T8122. The T6031 display power * states nest the other way round (the DCP's DISP_FE under DISP_CPU under @@ -141,9 +142,21 @@ static const char gate_t6031_pmp_uuid[] __initconst = "07FF4C5B-CB2E-3DE6-95FF-3 */ static bool gate_requested __initdata = true; -/* apple_t6031_display.enable: the T6031 native display is off unless it is 1. */ +/* + * apple_t6031_display.enable: -1 while it is not on the command line, else 0 + * or 1. Unset, the T6031 native display is on only on gate_t6031_board. + */ +static int gate_t6031_switch __initdata = -1; +static const char *gate_t6031_board __initdata = "apple,j516c"; +/* The resolved request, set by gate_run() on a T6031. */ static bool gate_t6031_requested __initdata; +/* Whether the T6031 native display is requested: an explicit 0 or 1 wins. */ +static bool __init gate_t6031_wanted(int sw, bool default_board) +{ + return sw < 0 ? default_board : sw; +} + /* One SoC's internal display handoff, as the gate checks and completes it */ struct gate_soc { const char *machine; /* root compatible */ @@ -235,8 +248,9 @@ static const struct gate_soc gate_t8122 __initconst = { }; /* - * T6031 (M3 Max): the T6030 handoff at the T6031 addresses, off unless - * apple_t6031_display.enable=1. The PMP values must come from the boot + * T6031 (M3 Max): the T6030 handoff at the T6031 addresses, on by default on + * the J516C and off unless apple_t6031_display.enable=1 on the other boards; + * apple_t6031_display.enable=0 keeps it off everywhere. The PMP values must come from the boot * loader: there is no built-in fallback. The PMP image is T6031's own; * any other, the T6030 image included, is refused. The report driver checks * the image's SRAM layout before it starts the PMP. The internal display's @@ -327,10 +341,18 @@ static int __init gate_setup_t8122(char *arg) } early_param("apple_t8122_display.enable", gate_setup_t8122); -/* Its own switch: the T6030 and T8122 options do not change it, nor it them. */ +/* + * Its own switch: the T6030 and T8122 options do not change it, nor it them. + * A value kstrtobool() rejects sets nothing, so the board default stays. + */ static int __init gate_setup_t6031(char *arg) { - return kstrtobool(arg, &gate_t6031_requested); + bool on; + int ret = kstrtobool(arg, &on); + + if (!ret) + gate_t6031_switch = on; + return ret; } early_param("apple_t6031_display.enable", gate_setup_t6031); @@ -1941,8 +1963,17 @@ static void __init gate_run(void) if (gate_soc->describe) gate_describe(); + if (gate_soc == &gate_t6031) { + bool board = of_machine_is_compatible(gate_t6031_board); + + gate_t6031_requested = gate_t6031_wanted(gate_t6031_switch, board); + if (gate_t6031_switch < 0 && board) + gate_info("native display on by default on %s; apple_t6031_display.enable=0 keeps the boot framebuffer\n", + gate_t6031_board); + } + if (!*gate_soc->requested) { - if (gate_soc->opt_in) + if (gate_soc->opt_in && gate_t6031_switch < 0) gate_info("native display not requested (%s is not on the command line), display stays on the boot framebuffer\n", gate_soc->opt_in); else From ca6c77a09ff0cd5a0b29bab93c736ada827dc865 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:13:23 +0000 Subject: [PATCH 4/4] drm/asahi: g15: say when the T6031 GPU waits on an enabled display A T6031 GPU starts its firmware only after the native display is published, so apple_t6031_display.enable=0, or a display that never comes up, also keeps the GPU off for that boot. The Kconfig help and the display gate's comments described the switch as display-only. Say that it also keeps the GPU off. The only G15 line while the GPU waited was the one for a display node this boot did not enable. An enabled display that is not published yet, because its DCP refused or failed to bind or it has not registered, left the GPU deferred with nothing from G15. Say once that the GPU is waiting for the display to come up. Return values and the wait itself are unchanged. The display-order host controls now expect that line, once, for an enabled but unpublished display, and a new mutant that drops it is caught. Generated-by: LLM Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- drivers/gpu/drm/asahi/g15/glue.c | 7 ++++++- drivers/soc/apple/Kconfig | 6 +++++- drivers/soc/apple/t6030-display-gate.c | 8 ++++++-- tools/asahi/g15-display-order-tests.py | 5 ++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/asahi/g15/glue.c b/drivers/gpu/drm/asahi/g15/glue.c index 439238e888d7c4..f166d07e02ea75 100644 --- a/drivers/gpu/drm/asahi/g15/glue.c +++ b/drivers/gpu/drm/asahi/g15/glue.c @@ -145,7 +145,10 @@ int asahi_g15_pmp_prepare(struct device *dev) /* * T6031 has no PMP vote, but its first firmware start follows the native * display as T6030's does: defer until that display is up. The display can - * be off for this boot (no handoff, or not requested), so say once why. + * be off for this boot (no handoff, or not requested), so say once why. An + * enabled display that is not published yet is said once too: if it never + * comes up (its DCP refused or failed to bind), that line is why the GPU + * stays off. */ int asahi_g15_display_prepare(struct device *dev) { @@ -160,6 +163,8 @@ int asahi_g15_display_prepare(struct device *dev) np = of_find_compatible_node(NULL, NULL, "apple,t6031-display-subsystem"); if (!np || !of_device_is_available(np)) dev_info_once(dev, "G15: waiting for the T6031 native display, which this boot did not enable (it needs the boot loader's display handoff, and apple_t6031_display.enable=1 except on the J516C)\n"); + else + dev_info_once(dev, "G15: waiting for the T6031 native display to come up: the GPU firmware starts only after the display is published\n"); of_node_put(np); return ret; } diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index e4b9ed2bb77cdf..ba57db91f8860b 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -231,7 +231,11 @@ config APPLE_T6030_DISPLAY_GATE in the same way and only with the PMP values the boot loader passes. There the native display is on by default on the J516C, unless apple_t6031_display.enable=0; the other T6031 boards need - apple_t6031_display.enable=1 on the kernel command line. + apple_t6031_display.enable=1 on the kernel command line. The T6031 + GPU driver starts its firmware only after this native display is + up, so apple_t6031_display.enable=0, or a native display that does + not come up, also keeps the GPU off for that boot. It is not a + display-only switch. Say 'y' if you want the native display on M3 Pro MacBook Pros. diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index 5b9aa91878e6b5..c153a4d7cae0fd 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -55,7 +55,10 @@ * On T6031 (M3 Max) the gate serves the same handoff, marked * apple,t6031-handoff. Its default is scoped to the J516C: there the native * display is on unless apple_t6031_display.enable=0; the other T6031 boards - * need apple_t6031_display.enable=1. The default changes no check of the + * need apple_t6031_display.enable=1. The T6031 GPU starts its firmware only + * after this native display is up, so enable=0 (or a display that never + * comes up) keeps the GPU off for that boot too: it is not a display-only + * switch. The default changes no check of the * handoff or the PMP below. The PMP values must come from the boot loader, in * /chosen/asahi,t6031-pmp; there is no fallback. Its PMP topology is * reused from the device tree, as on T8122. The T6031 display power @@ -250,7 +253,8 @@ static const struct gate_soc gate_t8122 __initconst = { /* * T6031 (M3 Max): the T6030 handoff at the T6031 addresses, on by default on * the J516C and off unless apple_t6031_display.enable=1 on the other boards; - * apple_t6031_display.enable=0 keeps it off everywhere. The PMP values must come from the boot + * apple_t6031_display.enable=0 keeps it, and with it the GPU, off everywhere. + * The PMP values must come from the boot * loader: there is no built-in fallback. The PMP image is T6031's own; * any other, the T6030 image included, is refused. The report driver checks * the image's SRAM layout before it starts the PMP. The internal display's diff --git a/tools/asahi/g15-display-order-tests.py b/tools/asahi/g15-display-order-tests.py index 2fd7aa99a2bbe3..50e0aa48093961 100644 --- a/tools/asahi/g15-display-order-tests.py +++ b/tools/asahi/g15-display-order-tests.py @@ -165,8 +165,10 @@ def c_source(root): nodes[1] = NULL; CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 1 && links_made == 0); } else if (!strcmp(scenario, "pending")) { + /* Enabled but not published: no start, no success, said once. */ disp31.available = true; - CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 0 && links_made == 0); + CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 1 && links_made == 0); + CHECK(prepare(&g31) == -EPROBE_DEFER && infos == 1 && links_made == 0); } else if (!strcmp(scenario, "ready")) { disp31.available = true; CHECK(publish(&d31) == 0 && d31.refs == 1); @@ -216,6 +218,7 @@ def c_source(root): C_MUTANTS = { "unpublished-succeeds": ("if (!*display)\n\t\treturn -EPROBE_DEFER;", "if (!*display)\n\t\treturn 0;"), "disabled-display-skipped": (re.compile(r"\t\tdev_info_once\(dev,[^;]*;\n"), "\t\t{ of_node_put(np); return 0; }\n"), + "pending-display-silent": (re.compile(r"\telse\n\t\tdev_info_once\(dev,[^;]*;\n"), ""), "t6031-reads-t6030-slot": ("&apple_t6031_display);", "&apple_t6030_display);"), "t6031-published-as-t6030": ("return &apple_t6031_display;", "return &apple_t6030_display;"), "no-machine-check": ("!of_machine_is_compatible(machine) ||", ""),