From 38aa4f890d6e49c44395d82104323d6724011ad1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 12 Nov 2026 11:21:06 +0100 Subject: [PATCH 1/5] arm64: dts: apple: t8142-j813: Describe the GPU firmware TEXT The J813 GPU driver is about to identify the firmware build iBoot loaded before it starts it, as the M5 Pro and Max do: it measures both roles' __TEXT, which iBoot loads back to back (ADT gfx-asc and gfx1-asc segment-ranges, 0x50000 + 0x4c000 bytes). Add the region as fw-text. Like fw-data, it is fixed at the address iBoot uses under macOS 27.0 (26A428), 0x100_00e4_4000, until the boot loader fills it in. Under 27.2 (26B123) iBoot loads the TEXT 64 KiB higher, at 0x100_00e5_4000, and the DATA at 0x100_0245_8000, so a 27.2 boot needs a loader that fills both. Signed-off-by: Thomas Glanzmann --- arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi b/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi index 763125bfe4ec7..abc977fab1898 100644 --- a/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi +++ b/arch/arm64/boot/dts/apple/t8142-j813-gpu.dtsi @@ -33,6 +33,18 @@ reg = <0x100 0x02444000 0x0 0x184000>; }; + /* + * Both roles' __TEXT, contiguous (ADT gfx-asc and gfx1-asc segment-ranges), + * which the driver identifies before it starts the firmware. Fixed at the + * macOS 27.0 address like fw-data, until the bootloader fills it in; under + * 27.2 (26B123) iBoot loads it at 0x100_00e5_4000 and the DATA at + * 0x100_0245_8000. + */ + gpu_fw_text: fw-text { + reg = <0x100 0x00e44000 0x0 0x9c000>; + no-map; + }; + uat_handoff: uat-handoff { reg = <0 0 0 0>; }; @@ -105,9 +117,10 @@ power-domains = <&ps_gfx>; memory-region = <&uat_ttbs>, <&uat_pagetables>, <&uat_handoff>, <&uat_l2_pagetables>, <&gfx_data>, <&gfx1_data>, - <&gpu_fw_data>; + <&gpu_fw_data>, <&gpu_fw_text>; memory-region-names = "ttbs", "pagetables", "handoff", - "shared-l2", "gfx-data", "gfx1-data", "fw-data"; + "shared-l2", "gfx-data", "gfx1-data", "fw-data", + "fw-text"; operating-points-v2 = <&gpu_opp>; /* Holds the DCS and fabric floor while GPU work is in flight. */ apple,clpc = <&clpc>; From b37e0b217defe1a87d20d98ade9d7798fc47cf83 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 12 Nov 2026 11:21:06 +0100 Subject: [PATCH 2/5] drm/asahi: g17: M5: InitData for the macOS 27.2 (26B123) common firmware The J813 runs the common 26A428 InitData graph, the J873's interface. macOS 27.2 (26B123) ships a new G17G build (RTKit 3514.41.1), which never acknowledges that graph: the secondary's InitData times out. The 26B123 interface differs from 26A428 in three places, the same three as the J873's G18G build of this release: - both roles expect the InitData root version word with bit 45 set, 0x0c8b_6339_83a0_04c0; - the policy grows by 0x60 and the status config by 8; the other 22 allocations keep their sizes. Everything after the policy in the shared cluster moves by 0x80; the page total and the private cluster (0x6d8 still packs to 0x700) stay as they are; - policy fields +0x2f0c..+0x2f17 move by 0x10 and +0x2fb4..+0x6d9f by 0x18, with a new word at +0x2f28; status-config fields +0x59c..+0x5b3 move by 4 and +0x5b4 to the end by 8, with a new word at +0x5b8 and +0x59c left zero. The new words carry a new accelerator power-controller field, which the generic power and performance setup sets to 5000 and the G17G one leaves alone. The HW data, the roots, the main configs, status B and region C keep their field offsets. Add the 26B123 profile and graph. The 26A428 encoders run unchanged and their policy and status config are moved in place by bands; a nonzero byte no band carries is refused. Compile-time checks keep the bands ascending and inside the new sizes. The board for it is J813_26B123; no configuration selects it yet, and the 26A428 encoding is unchanged. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs | 314 ++++++++++++++++++-- drivers/gpu/drm/asahi/g17/initdata/m5.rs | 2 +- 2 files changed, 284 insertions(+), 32 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs index e4de37f639579..dda570401f0a7 100644 --- a/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs +++ b/drivers/gpu/drm/asahi/g17/fw/m5/initdata.rs @@ -36,6 +36,11 @@ pub(crate) enum RootProfile { J873G18G26A428, J714G17S26A428, + + /// The common interface of macOS 27.2 (26B123, RTKit 3514.41.1): the + /// [`Self::J873G18G26A428`] graph with a longer policy and status config + /// ([`COMMON26B123_LAYOUTS`]). The J813's G17G build runs it as the J873's G18G one does. + J873G18G26B123, } impl RootProfile { @@ -44,13 +49,28 @@ impl RootProfile { Self::J700G17P => INITDATA_VERSION_WORD, Self::J813G17G25G76 => 0x0c8a_a322_039e_04c0, Self::J873G18G26A428 => COMMON26_INITDATA_VERSION_WORD, + Self::J873G18G26B123 => COMMON26B123_INITDATA_VERSION_WORD, Self::J714G17S26A428 => M5_INITDATA_VERSION_WORD, } } + /// Either common-interface build, 26A428 or 26B123. + pub(crate) const fn is_common26(self) -> bool { + matches!(self, Self::J873G18G26A428 | Self::J873G18G26B123) + } + + /// The shared, private and per-object layouts of a common-interface build. + pub(crate) const fn common26_layouts(self) -> Option<&'static Common26Layouts> { + match self { + Self::J873G18G26A428 => Some(&COMMON26_LAYOUTS), + Self::J873G18G26B123 => Some(&COMMON26B123_LAYOUTS), + Self::J700G17P | Self::J813G17G25G76 | Self::J714G17S26A428 => None, + } + } + pub(crate) const fn root_size(self, role: InstanceRole) -> usize { match self { - Self::J873G18G26A428 => COMMON26_ROOT_SIZE, + Self::J873G18G26A428 | Self::J873G18G26B123 => COMMON26_ROOT_SIZE, Self::J714G17S26A428 => M5_ROOT_SIZE, Self::J700G17P | Self::J813G17G25G76 => root_size(role), } @@ -58,10 +78,10 @@ impl RootProfile { pub(crate) const fn control_opening_opcode(self, role: InstanceRole) -> u32 { match (self, role) { - (Self::J873G18G26A428, InstanceRole::Primary) => { + (Self::J873G18G26A428 | Self::J873G18G26B123, InstanceRole::Primary) => { COMMON26_CONTROL_OPENING_OPCODE_PRIMARY } - (Self::J873G18G26A428, InstanceRole::Secondary) => { + (Self::J873G18G26A428 | Self::J873G18G26B123, InstanceRole::Secondary) => { COMMON26_CONTROL_OPENING_OPCODE_SECONDARY } (Self::J714G17S26A428, InstanceRole::Primary) => M5_CONTROL_OPENING_OPCODE_PRIMARY, @@ -75,7 +95,7 @@ impl RootProfile { pub(crate) const fn control_register_opcode(self) -> Option { match self { - Self::J873G18G26A428 | Self::J714G17S26A428 => None, + Self::J873G18G26A428 | Self::J873G18G26B123 | Self::J714G17S26A428 => None, Self::J700G17P | Self::J813G17G25G76 => Some(CONTROL_OPENING_PRIMARY_REGISTER_OPCODE), } } @@ -83,6 +103,7 @@ impl RootProfile { pub(crate) const fn bundle_view_offsets(self) -> [usize; 5] { match self { Self::J873G18G26A428 => COMMON26_BUNDLE_VIEW_OFFSETS, + Self::J873G18G26B123 => COMMON26B123_BUNDLE_VIEW_OFFSETS, Self::J714G17S26A428 => M5_BUNDLE_VIEW_OFFSETS, Self::J700G17P | Self::J813G17G25G76 => BUNDLE_VIEW_OFFSETS, } @@ -90,7 +111,8 @@ impl RootProfile { pub(crate) const fn bundle_view_extents(self) -> [usize; 5] { match self { - Self::J873G18G26A428 => COMMON26_BUNDLE_VIEW_EXTENTS, + // 26B123 grows only the policy and the status config; the five views keep their sizes. + Self::J873G18G26A428 | Self::J873G18G26B123 => COMMON26_BUNDLE_VIEW_EXTENTS, Self::J714G17S26A428 => M5_BUNDLE_VIEW_EXTENTS, Self::J700G17P | Self::J813G17G25G76 => BUNDLE_VIEW_EXTENTS, } @@ -99,7 +121,7 @@ impl RootProfile { /// The GPU-visible aliases of the firmware's own tables the main config names. pub(crate) const fn region_view_low_addrs(self) -> [u64; 2] { match self { - Self::J873G18G26A428 => COMMON26_REGION_VIEW_LOW_ADDRS, + Self::J873G18G26A428 | Self::J873G18G26B123 => COMMON26_REGION_VIEW_LOW_ADDRS, Self::J714G17S26A428 | Self::J700G17P | Self::J813G17G25G76 => REGION_VIEW_LOW_ADDRS, } } @@ -107,6 +129,7 @@ impl RootProfile { pub(crate) const fn repeated_region_bundle_offset(self) -> usize { match self { Self::J873G18G26A428 => COMMON26_SHARED_LAYOUT.block_88, + Self::J873G18G26B123 => COMMON26B123_SHARED_LAYOUT.block_88, Self::J714G17S26A428 => M5_SHARED_LAYOUT.block_88, Self::J700G17P | Self::J813G17G25G76 => MAIN_REPEATED_REGION_BUNDLE_OFFSET, } @@ -115,6 +138,7 @@ impl RootProfile { pub(crate) const fn work_ring_bundle_offsets(self) -> [usize; WORK_CHANNEL_COUNT] { match self { Self::J873G18G26A428 => COMMON26_WORK_RING_BUNDLE_OFFSETS, + Self::J873G18G26B123 => COMMON26B123_WORK_RING_BUNDLE_OFFSETS, Self::J714G17S26A428 => M5_WORK_RING_BUNDLE_OFFSETS, Self::J700G17P | Self::J813G17G25G76 => WORK_RING_BUNDLE_OFFSETS, } @@ -876,18 +900,17 @@ pub(crate) fn encode_main_config_for_profile( RootProfile::J700G17P => MAIN_SECONDARY_KIND_VALUE, RootProfile::J813G17G25G76 => 0xc, - RootProfile::J873G18G26A428 | RootProfile::J714G17S26A428 => { - COMMON26_MAIN_SECONDARY_KIND - } + RootProfile::J873G18G26A428 | RootProfile::J873G18G26B123 + | RootProfile::J714G17S26A428 => COMMON26_MAIN_SECONDARY_KIND, }); if profile == RootProfile::J714G17S26A428 { put_u64(out, COMMON26_MAIN_SECONDARY_POLICY_PTR, bundle + M5_SHARED_LAYOUT.policy as u64); - } else if profile == RootProfile::J873G18G26A428 { + } else if let Some(layouts) = profile.common26_layouts() { put_u64(out, COMMON26_MAIN_SECONDARY_POLICY_PTR, - bundle + COMMON26_SHARED_LAYOUT.policy as u64); + bundle + layouts.shared.policy as u64); } else { let extra = bundle @@ -909,7 +932,7 @@ pub(crate) fn encode_region_c_for_profile( if out.len() != REGION_C_SIZE { return Err(InitdataError::BufferSize); } - if profile == RootProfile::J873G18G26A428 { + if profile.is_common26() { return encode_common26_region_c(out); } @@ -976,7 +999,7 @@ pub(crate) fn encode_secondary_root_extra_1_for_profile( out.fill(0); write_runs(out, SECONDARY_ROOT_EXTRA_1_RUNS); - if matches!(profile, RootProfile::J813G17G25G76 | RootProfile::J873G18G26A428) { + if profile == RootProfile::J813G17G25G76 || profile.is_common26() { put_u32(out, 0x10, 2); } Ok(()) @@ -1904,16 +1927,213 @@ const _: () = { <= COMMON26_STATUS_B_SPAN); }; +// --- The common interface of macOS 27.2 (26B123, RTKit 3514.41.1) ------------------------------- +// +// Against 26A428, the same changes as the J873's G18G build of this release: +// +// - Both roles expect the InitData root version word with bit 45 set. +// - The policy grows by 0x60 (0x7168 -> 0x71c8 for G17G; 0x7800 -> 0x7860 in the common table) +// and the status config by 8 (0x6d0 -> 0x6d8); the other 22 allocations keep their sizes. +// - Policy and status-config fields move as the bands below describe. The new words carry a new +// accelerator power-controller field (accel +0x1122c), which the generic power and performance +// setup sets to 5000 and the G17G one leaves alone. +// - Every other InitData object keeps its field offsets. + +/// FW_PROVEN 26B123: u16 {0x04c0, 0x83a0, 0x6339, 0x0c8b}. +pub(crate) const COMMON26B123_INITDATA_VERSION_WORD: u64 = 0x0c8b_6339_83a0_04c0; + +/// The common 26A428 table with the 26B123 policy and status config. +pub(crate) const ABI_ALLOCATION_SIZES_26B123: [usize; 24] = [ + 0x2848, 0x0c18, 0x1048, 0x0e10, 0x7860, 0x0068, 0x0800, 0x0088, 0x1800, 0x04c0, 0x4000, 0x0030, + 0xe730, 0x06d8, 0x0020, 0x01b0, 0x4800, 0x28800, 0x79800, 0x9000, 0x0060, 0x0008, 0x0e80, 0x00e4, +]; + +/// Everything after the policy in the shared cluster moves by 0x80 (64-byte packing); the page +/// total is unchanged. +pub(crate) const COMMON26B123_SHARED_LAYOUT: SharedClusterLayout = + shared_cluster_layout(&ABI_ALLOCATION_SIZES_26B123); +/// 0x6d8 packs to the 0x700 that 0x6d0 took: the private cluster is the 26A428 one. +pub(crate) const COMMON26B123_PRIVATE_LAYOUT: PrivateClusterLayout = + private_cluster_layout(&ABI_ALLOCATION_SIZES_26B123); + +pub(crate) const COMMON26B123_BUNDLE_VIEW_OFFSETS: [usize; 5] = [ + COMMON26B123_SHARED_LAYOUT.block_c18, + COMMON26B123_SHARED_LAYOUT.block_1048, + COMMON26B123_SHARED_LAYOUT.block_e10, + COMMON26B123_SHARED_LAYOUT.block_68, + COMMON26B123_SHARED_LAYOUT.block_800, +]; + +pub(crate) const COMMON26B123_WORK_RING_BUNDLE_OFFSETS: [usize; WORK_CHANNEL_COUNT] = + work_ring_offsets(&COMMON26B123_SHARED_LAYOUT); + +/// Policy 26A428 -> 26B123. +/// +/// - +0x2f08 stays; the accelerator's power-controller words at +0x2f0c..+0x2f17 move by 0x10 +/// and the new field is the u32 at +0x2f28. +/// - +0x2f18..+0x2fb3 holds nothing the host writes in either build. +/// - From +0x2fb4 to the last 26A428 store (+0x6d98, u64) everything moves by 0x18, among them +/// the policy state pointer, the AON PTD pointer and the boot timestamp. The host writes +/// nothing past +0x6da0. +const COMMON26B123_POLICY_BANDS: &[LayoutBand] = &[ + band(0x0000, 0x2f0c, 0, Evidence::ProducerChecked), + band(0x2f0c, 0x2f18, 0x10, Evidence::ProducerChecked), + band(0x2fb4, 0x6da0, 0x18, Evidence::ProducerChecked), +]; +const COMMON26B123_POLICY_NEW_WORDS: &[(usize, u32)] = &[(0x2f28, 5000)]; + +/// Status config 26A428 -> 26B123: +0x59c..+0x5b3 move by 4 and +0x5b4 to the end by 8 (the +/// link word and pointer among them). The new +0x59c stays zero (only the zeroing of +/// +0x590..+0x59f covers it); +0x5b8 is the new accelerator field. +const COMMON26B123_STATUS_CONFIG_BANDS: &[LayoutBand] = &[ + band(0x000, 0x59c, 0, Evidence::ProducerChecked), + band(0x59c, 0x5b4, 4, Evidence::ProducerChecked), + band(0x5b4, 0x6d0, 8, Evidence::ProducerChecked), +]; +const COMMON26B123_STATUS_CONFIG_NEW_WORDS: &[(usize, u32)] = &[(0x5b8, 5000)]; + +/// How a later build moves the blocks the 26A428 encoders produce. +pub(crate) struct Common26Relayout { + policy_bands: &'static [LayoutBand], + policy_new_words: &'static [(usize, u32)], + status_config_bands: &'static [LayoutBand], + status_config_new_words: &'static [(usize, u32)], +} + +/// The layouts of one common-interface build. +pub(crate) struct Common26Layouts { + pub(crate) shared: SharedClusterLayout, + pub(crate) private: PrivateClusterLayout, + pub(crate) policy_size: usize, + pub(crate) status_config_size: usize, + /// None for 26A428, which the encoders produce directly. + relayout: Option, +} + +pub(crate) const COMMON26_LAYOUTS: Common26Layouts = Common26Layouts { + shared: COMMON26_SHARED_LAYOUT, + private: COMMON26_PRIVATE_LAYOUT, + policy_size: COMMON26_POLICY_SIZE, + status_config_size: COMMON26_STATUS_CONFIG_SIZE, + relayout: None, +}; + +pub(crate) const COMMON26B123_LAYOUTS: Common26Layouts = Common26Layouts { + shared: COMMON26B123_SHARED_LAYOUT, + private: COMMON26B123_PRIVATE_LAYOUT, + policy_size: ABI_ALLOCATION_SIZES_26B123[ALLOC_POLICY], + status_config_size: ABI_ALLOCATION_SIZES_26B123[ALLOC_STATUS_CONFIG], + relayout: Some(Common26Relayout { + policy_bands: COMMON26B123_POLICY_BANDS, + policy_new_words: COMMON26B123_POLICY_NEW_WORDS, + status_config_bands: COMMON26B123_STATUS_CONFIG_BANDS, + status_config_new_words: COMMON26B123_STATUS_CONFIG_NEW_WORDS, + }), +}; + +/// The common 26B123 build (macOS 27.2; the J813). +pub(crate) const COMMON26B123_GRAPH: Graph = Graph { + profile: RootProfile::J873G18G26B123, + private: COMMON26B123_PRIVATE_LAYOUT, + shared: COMMON26B123_SHARED_LAYOUT, + policy_size: COMMON26B123_LAYOUTS.policy_size, + bundle_view_offsets: COMMON26B123_BUNDLE_VIEW_OFFSETS, + bundle_view_extents: COMMON26_BUNDLE_VIEW_EXTENTS, + work_ring_offsets: COMMON26B123_WORK_RING_BUNDLE_OFFSETS, + state_grids: state_grids(&COMMON26B123_PRIVATE_LAYOUT), + power_assert_tally: COMMON26_GRAPH.power_assert_tally, +}; + +const _: () = { + let (old, new) = (COMMON26_SHARED_LAYOUT, COMMON26B123_SHARED_LAYOUT); + assert!(new.total == old.total && new.policy == old.policy); + assert!(new.block_68 == old.block_68 + 0x80 && new.main_config[0] == old.main_config[0] + 0x80); + assert!(COMMON26B123_PRIVATE_LAYOUT.total == COMMON26_PRIVATE_LAYOUT.total); + assert!(COMMON26B123_PRIVATE_LAYOUT.status_config == COMMON26_PRIVATE_LAYOUT.status_config); + assert!(COMMON26B123_PRIVATE_LAYOUT.roles[0].status_a == COMMON26_PRIVATE_LAYOUT.roles[0].status_a); + assert!(COMMON26B123_PRIVATE_LAYOUT.policy_state == COMMON26_PRIVATE_LAYOUT.policy_state); + assert!(COMMON26_STATUS_CONFIG_IN_STATUS_B + COMMON26B123_LAYOUTS.status_config_size + <= COMMON26_STATUS_B_SPAN); + assert!(bands_fit(COMMON26B123_POLICY_BANDS, COMMON26_POLICY_SIZE, + COMMON26B123_LAYOUTS.policy_size)); + assert!(bands_fit(COMMON26B123_STATUS_CONFIG_BANDS, COMMON26_STATUS_CONFIG_SIZE, + COMMON26B123_LAYOUTS.status_config_size)); +}; + +/// Whether `bands` are ascending, move no byte down, keep their order once moved and land +/// inside `new_size`, so [`relayout_by_bands`] can move them in place. +const fn bands_fit(bands: &[LayoutBand], old_size: usize, new_size: usize) -> bool { + let mut index = 0; + while index < bands.len() { + let band = bands[index]; + if band.delta < 0 || band.start >= band.end || band.end > old_size + || band.end + band.delta as usize > new_size { + return false; + } + if index > 0 { + let previous = bands[index - 1]; + if previous.end > band.start + || previous.end + previous.delta as usize > band.start + band.delta as usize { + return false; + } + } + index += 1; + } + true +} + +/// Move a block encoded in the 26A428 layout (the first `old_size` bytes of `out`) to the layout +/// `bands` describe, in place, and zero what the bands leave behind. A nonzero byte no band +/// carries has no place in the new layout and is refused. +fn relayout_by_bands(bands: &[LayoutBand], old_size: usize, out: &mut [u8]) + -> Result<(), InitdataError> { + let old = out.get(..old_size).ok_or(InitdataError::BufferSize)?; + if old.iter().enumerate().any(|(offset, byte)| { + *byte != 0 && relocate_by_bands(bands, offset).is_none() + }) { + return Err(InitdataError::InterfaceMismatch); + } + for band in bands.iter().rev() { + let delta = band.delta as usize; + if delta == 0 { + continue; + } + let target = band.start + delta; + if target + (band.end - band.start) > out.len() { + return Err(InitdataError::BufferSize); + } + out.copy_within(band.start..band.end, target); + out[band.start..band.end.min(target)].fill(0); + } + Ok(()) +} + +/// Bring a block the 26A428 encoder produced to the build's layout and set the words the build +/// added. +fn relayout_common26( + bands: &[LayoutBand], + new_words: &[(usize, u32)], + old_size: usize, + out: &mut [u8], +) -> Result<(), InitdataError> { + relayout_by_bands(bands, old_size, out)?; + for &(offset, value) in new_words { + put_u32(out, offset, value); + } + Ok(()) +} + impl Graph { pub(crate) fn is_common26(&self) -> bool { - self.profile == RootProfile::J873G18G26A428 + self.profile.is_common26() } } -/// Encode the common 26A428 primary status-B span: status B, status config and the gap to -/// status A. The status config carries the board's 25G76 words relocated, the fields 26A428 -/// added and its new cross-link to status B. +/// Encode the common primary status-B span: status B, status config and the gap to status A. The +/// status config carries the board's 25G76 words relocated, the fields 26A428 added and its new +/// cross-link to status B, moved to the layout of the build `layouts` describes. fn encode_common26_primary_status_b( + layouts: &Common26Layouts, fwctl_state: u64, fwctl_ring: u64, status_b: u64, @@ -1926,15 +2146,24 @@ fn encode_common26_primary_status_b( required_address(fwctl_ring)?; required_address(status_b)?; let config = COMMON26_STATUS_CONFIG_IN_STATUS_B; + // Where the build keeps the status config's half of the cross-link. + let link_word = match &layouts.relayout { + Some(relayout) => relocate_by_bands(relayout.status_config_bands, + COMMON26_STATUS_CONFIG_LINK_WORD) + .ok_or(InitdataError::InterfaceMismatch)?, + None => COMMON26_STATUS_CONFIG_LINK_WORD, + }; out.fill(0); encode_status_block(&mut out[..STATUS_BLOCK_SIZE])?; put_u64(out, COMMON26_STATUS_B_FWCTL_STATE, fwctl_state); put_u64(out, COMMON26_STATUS_B_FWCTL_RING, fwctl_ring); put_u32(out, COMMON26_STATUS_B_CONFIG_HEADER, 1); put_u32(out, COMMON26_STATUS_B_LINK_WORD, 0); - put_u64(out, COMMON26_STATUS_B_LINK_PTR, - status_b + (config + COMMON26_STATUS_CONFIG_LINK_WORD) as u64); - let status_config = &mut out[config..config + COMMON26_STATUS_CONFIG_SIZE]; + put_u64(out, COMMON26_STATUS_B_LINK_PTR, status_b + (config + link_word) as u64); + // The 26A428 encoding fills the first COMMON26_STATUS_CONFIG_SIZE bytes; a later build's + // layout is made from it below. + let status_config = out.get_mut(config..config + layouts.status_config_size) + .ok_or(InitdataError::BufferSize)?; write_relocated_words(M5_25G76_STATUS_CONFIG_WORDS, 0, COMMON26_STATUS_CONFIG_BANDS, status_config)?; for (offset, value) in COMMON26_STATUS_CONFIG_NEW_WORDS { @@ -1964,18 +2193,24 @@ fn encode_common26_primary_status_b( put_u32(status_config, COMMON26_STATUS_CONFIG_LINK_WORD, 1); put_u64(status_config, COMMON26_STATUS_CONFIG_LINK_PTR, status_b + COMMON26_STATUS_B_LINK_WORD as u64); + if let Some(relayout) = &layouts.relayout { + relayout_common26(relayout.status_config_bands, relayout.status_config_new_words, + COMMON26_STATUS_CONFIG_SIZE, status_config)?; + } Ok(()) } -/// Encode the common 26A428 private cluster mapped at `private_base`: the status-B span, both -/// status A blocks, the secondary root's 0x60 object and one opening-producer entry per control -/// channel. +/// Encode the private cluster of the common build `profile` mapped at `private_base`: the +/// status-B span, both status A blocks, the secondary root's 0x60 object and one opening-producer +/// entry per control channel. pub(crate) fn encode_common26_private_cluster( + profile: RootProfile, private_base: u64, fwctl_state: u64, out: &mut [u8], ) -> Result<(), InitdataError> { - let layout = COMMON26_PRIVATE_LAYOUT; + let layouts = profile.common26_layouts().ok_or(InitdataError::InterfaceMismatch)?; + let layout = layouts.private; if out.len() != layout.total { return Err(InitdataError::BufferSize); } @@ -1995,13 +2230,14 @@ pub(crate) fn encode_common26_private_cluster( out.fill(0); let status_b = layout.status_b; let status_a = layout.roles[0].status_a; - encode_common26_primary_status_b(fwctl_state, fwctl_ring, private_base + status_b as u64, + encode_common26_primary_status_b(layouts, fwctl_state, fwctl_ring, + private_base + status_b as u64, &mut out[status_b..status_a])?; for role in layout.roles { encode_status_block(&mut out[role.status_a..role.status_a + STATUS_BLOCK_SIZE])?; } let extra = layout.secondary_extra_1; - encode_secondary_root_extra_1_for_profile(RootProfile::J873G18G26A428, + encode_secondary_root_extra_1_for_profile(profile, &mut out[extra..extra + STATUS_BLOCK_SIZE])?; put_u32(out, layout.control_state + CHANNEL_STATE_PRODUCER * CHANNEL_STATE_SPACING, CONTROL_OPENING_PRIMARY_PRODUCER); @@ -2010,15 +2246,18 @@ pub(crate) fn encode_common26_private_cluster( Ok(()) } -/// Encode the six common 26A428 blocks after the HW data (0xc18, 0x1048, 0xe10, perf/policy, -/// 0x68, 0x800): the board's 25G76 policy words relocated, the fields 26A428 added and the owned -/// pointers. Preserves the HW data and everything from the BRN table on. +/// Encode the six common blocks after the HW data (0xc18, 0x1048, 0xe10, perf/policy, 0x68, +/// 0x800): the board's 25G76 policy words relocated, the fields 26A428 added and the owned +/// pointers, in the layout of the board's build. Preserves the HW data and everything from the +/// BRN table on. pub(crate) fn encode_common26_bundle_policy( board: &Board, bindings: M5PolicyBindings, out: &mut [u8], ) -> Result<(), InitdataError> { - let layout = COMMON26_SHARED_LAYOUT; + let layouts = board.graph.profile.common26_layouts() + .ok_or(InitdataError::InterfaceMismatch)?; + let layout = layouts.shared; if out.len() < layout.brn_table { return Err(InitdataError::BufferSize); } @@ -2043,7 +2282,10 @@ pub(crate) fn encode_common26_bundle_policy( return Err(InitdataError::BufferSize); } } - let policy = &mut out[layout.policy..layout.policy + COMMON26_POLICY_SIZE]; + // The 26A428 encoding fills the first COMMON26_POLICY_SIZE bytes; a later build's layout is + // made from it below. + let policy = out.get_mut(layout.policy..layout.policy + layouts.policy_size) + .ok_or(InitdataError::BufferSize)?; for &(offset, value) in M5_25G76_POLICY_WORDS { if (m5.policy..m5_policy_end).contains(&offset) { write_relocated_words(&[(offset, value)], m5.policy, COMMON26_POLICY_BANDS, policy)?; @@ -2067,6 +2309,10 @@ pub(crate) fn encode_common26_bundle_policy( put_u64(policy, COMMON26_POLICY_STATE_PTR, bindings.state); put_u64(policy, COMMON26_POLICY_AON_PTD_PTR, bindings.aon_ptd); put_u64(policy, COMMON26_POLICY_BOOT_TIMESTAMP, bindings.boot_timestamp); + if let Some(relayout) = &layouts.relayout { + relayout_common26(relayout.policy_bands, relayout.policy_new_words, COMMON26_POLICY_SIZE, + policy)?; + } Ok(()) } @@ -2311,6 +2557,12 @@ pub(crate) const J813: Board = Board { completion_inputs: false, }; +/// The J813 on its macOS 27.2 (26B123) firmware: the same board on the common 26B123 graph. +pub(crate) const J813_26B123: Board = Board { + graph: &COMMON26B123_GRAPH, + ..J813 +}; + const _: () = { use super::target::min_parameter_buffer_pages; assert!(J714.min_parameter_buffer as u64 == min_parameter_buffer_pages(J714.clusters) << 15); diff --git a/drivers/gpu/drm/asahi/g17/initdata/m5.rs b/drivers/gpu/drm/asahi/g17/initdata/m5.rs index 5c3200fefce19..9352e5ec32f3e 100644 --- a/drivers/gpu/drm/asahi/g17/initdata/m5.rs +++ b/drivers/gpu/drm/asahi/g17/initdata/m5.rs @@ -205,7 +205,7 @@ impl InitData { m::encode_common26_board_region_c( &mut self.object_mut(self.region_c)?.bytes_mut()[..m::REGION_C_SIZE]) .map_err(|_| EINVAL)?; - m::encode_common26_private_cluster(PRIVATE, FWCTL, + m::encode_common26_private_cluster(board.graph.profile, PRIVATE, FWCTL, &mut self.object_mut(self.cluster)?.bytes_mut()[..private.total]) .map_err(|_| EINVAL)?; } else { From 83acdc76da6871c41b1e446abf3cee6f4a61bd69 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 12 Nov 2026 11:21:13 +0100 Subject: [PATCH 3/5] drm/asahi: t8142: Identify the J813 GPU firmware and admit macOS 27.2 The J813 ran a fixed variant: it started whatever firmware iBoot had loaded on the 26A428 graph. Under the macOS 27.2 stub that is the 26B123 build, which needs its own graph. Identify the loaded build as the M5 Pro and Max do, from the fw-text region: both roles' TEXT (G17G c000, then c010), with the entropy records and the ranges iBoot fills zeroed (the image header's load fields, the boot arguments before each role's GKTS record and the records from it). The 26A428 build keeps the J813 configuration; the 26B123 build runs the board on the 26B123 graph. Both digests reproduce from the stock images in the IPSWs with the same ranges zeroed and were measured on 27.0 (two boots) and 27.2 boots. An unknown build is refused before anything is written to the GPU. Tested on a J813 under the macOS 27.2 stub, with fw-text and fw-data at the 27.2 addresses: both roles acknowledge the 26B123 InitData, the render node registers, and Mesa runs GLES2 16955/0, GLES3 44379/1 (an order-dependent texture-units case that passes alone) and GLES31 37339/0, matching the 27.0 results. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/asahi/fwid.rs | 28 +++++++++++ drivers/gpu/drm/asahi/hw/t8142.rs | 77 ++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/asahi/fwid.rs b/drivers/gpu/drm/asahi/fwid.rs index d646bff94cf1f..258ebafa04f09 100644 --- a/drivers/gpu/drm/asahi/fwid.rs +++ b/drivers/gpu/drm/asahi/fwid.rs @@ -585,4 +585,32 @@ mod tests { assert!(admit(t, d, &e[..1], h).is_none()); } } + + #[test] + fn t8142_identifies_each_build() { + use crate::g17::fw::m5::initdata::RootProfile; + let crate::g17::fw::variant::Firmware::Identified(builds) = crate::hw::t8142::CONFIG.firmware + else { + panic!("the J813 must identify its loaded firmware"); + }; + assert_eq!(builds.len(), 2); + // Same TEXT and DATA sizes for both builds, so each is told apart by its digest at its + // own record offsets. + t6050_admits_only(builds); + let [a428, b123] = [&builds[0].images[0], &builds[1].images[0]]; + for (image, other) in [(a428, builds[1].images), (b123, builds[0].images)] { + assert!(lookup(other, image.text_size, image.data_size, |_| { + Some(image.text_sha256) + }) + .is_none()); + } + // 26A428 runs the board as probed; 26B123 runs it on the 26B123 graph. + assert!(builds[0].config.is_none()); + let config = builds[1].config.unwrap(); + assert_eq!(config.m5.unwrap().graph.profile, RootProfile::J873G18G26B123); + assert_eq!( + crate::hw::t8142::CONFIG.m5.unwrap().graph.profile, + RootProfile::J873G18G26A428 + ); + } } diff --git a/drivers/gpu/drm/asahi/hw/t8142.rs b/drivers/gpu/drm/asahi/hw/t8142.rs index 26fe2e49abcdb..eae2960d74fab 100644 --- a/drivers/gpu/drm/asahi/hw/t8142.rs +++ b/drivers/gpu/drm/asahi/hw/t8142.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT -//! M5 (T8142, MacBook Air J813) on the macOS 27.0 (26A428) GPU firmware. +//! M5 (T8142, MacBook Air J813) on the macOS 27.0 (26A428) and 27.2 (26B123) GPU firmware. //! //! Bring-up only: the M5 graph ([`crate::g17::fw::m5::initdata`]) has no J813 board yet, so //! [`PROBE`] stops at the identity check, after `get_g17_id` has logged the ID registers and @@ -57,8 +57,15 @@ pub(crate) const PROBE: Config = Config { }, }; -/// The J813 on the M5 graph: [`PROBE`] with the GPU's identity and the J813 board. +/// The J813 on the M5 graph: [`PROBE`] with the GPU's identity and the J813 board. It runs +/// either firmware build of [`BUILDS`]; 26B123 runs [`CONFIG_26B123`]. pub(crate) const CONFIG: Config = Config { + firmware: crate::g17::fw::variant::Firmware::Identified(&BUILDS), + ..BASE +}; + +/// The J813 board and identity, before a firmware build is chosen. +const BASE: Config = Config { identity: Identity { family: 0xb, variant: 2, @@ -79,6 +86,72 @@ pub(crate) const CONFIG: Config = Config { ..PROBE }; +/// The J813 on the 27.2 firmware: the board on the common 26B123 graph. +pub(crate) const CONFIG_26B123: Config = Config { + m5: Some(&crate::g17::fw::m5::initdata::J813_26B123), + firmware: crate::g17::fw::variant::Firmware::Identified(&[BUILD_26B123]), + ..BASE +}; + +/// The firmware in `fw-text` must be one of these builds. iBoot loads the G17G c000 image of the +/// primary (TEXT 0x50000) and the c010 image of the secondary (0x4c000) back to back. The digest +/// is that of both TEXTs with the entropy records and the ranges iBoot fills zeroed (the image +/// header's load fields at +0x224 and +0x23c, the 0x740 bytes of boot arguments before each +/// role's GKTS record and the 0x231 bytes of records from it); it is that of the stock images +/// in the IPSW with the same ranges zeroed, and was measured on two 27.0 boots and one 27.2 boot. +const BUILDS: [crate::g17::fw::variant::Build; 2] = [ + BUILD_26A428, + crate::g17::fw::variant::Build { + config: Some(&CONFIG_26B123), + ..BUILD_26B123 + }, +]; + +const BUILD_26A428: crate::g17::fw::variant::Build = crate::g17::fw::variant::Build { + os_version: [27, 0, 0], + name: "27.0 (26A428), G17G", + images: &[crate::fwid::FirmwareBuild { + name: "26A428 for T8142 G17G, primary then secondary", + text_size: 0x9c000, + data_size: 0x184000, + entropy: &[[0x49d10, 0x49df3], [0x997d0, 0x998b3]], + boot_filled: &[ + (0x224, 4), (0x23c, 8), (0x495d0, 0x740), (0x49d10, 0x231), + (0x50224, 4), (0x5023c, 8), (0x99090, 0x740), (0x997d0, 0x231), + ], + text_sha256: [ + 0xec, 0xff, 0x40, 0xe3, 0xad, 0xb8, 0x9c, 0x90, 0x62, 0xe9, 0x1d, 0xb6, 0x3f, + 0x31, 0xba, 0xaf, 0x75, 0xeb, 0x5d, 0x3f, 0x97, 0x22, 0xe3, 0x06, 0x95, 0x45, + 0x3f, 0xad, 0x02, 0xfc, 0xf5, 0x0c, + ], + }], + variant: &crate::g17::fw::variant::V26A428_M5, + config: None, +}; + +/// RTKit 3514.41.1: the 26A428 protocol on the 26B123 InitData graph. +const BUILD_26B123: crate::g17::fw::variant::Build = crate::g17::fw::variant::Build { + os_version: [27, 2, 0], + name: "27.2 (26B123), G17G", + images: &[crate::fwid::FirmwareBuild { + name: "26B123 for T8142 G17G, primary then secondary", + text_size: 0x9c000, + data_size: 0x184000, + entropy: &[[0x49d54, 0x49e37], [0x997e4, 0x998c7]], + boot_filled: &[ + (0x224, 4), (0x23c, 8), (0x49614, 0x740), (0x49d54, 0x231), + (0x50224, 4), (0x5023c, 8), (0x990a4, 0x740), (0x997e4, 0x231), + ], + text_sha256: [ + 0xe3, 0xab, 0x32, 0x7b, 0x73, 0x1b, 0xf2, 0xf1, 0x74, 0x9b, 0x04, 0x44, 0x1f, + 0x5c, 0xb5, 0xab, 0xfc, 0x23, 0xf3, 0x41, 0xc3, 0x99, 0x72, 0x8e, 0x2c, 0xb4, + 0x6f, 0x68, 0xb1, 0x7a, 0x39, 0xb9, + ], + }], + variant: &crate::g17::fw::variant::V26A428_M5, + config: None, +}; + // Each compute operand bank holds 6 KiB per core slot of the board. const _: () = assert!( CONFIG.core_layout.operand_state.bank_bytes() From dc1a72952f98dabe94d8632f4d6790a7c321e121 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 12 Nov 2026 12:57:21 +0100 Subject: [PATCH 4/5] drm/apple: dcp: T8142: Drive the macOS 27.2 firmware On a macOS 27.2 (26B123) stub, the J813 refuses its DCP's apple,firmware-compat of 27.2.0. It then drives the DCP with the H17P interface, and the 27.2 DCP firmware crashes on the first call ("ap_link: rpc_callee_gated: link_rpc_lookup callback not found: 'A427'"). The firmware 27.2 method table (iomfb_v27_methods("27.2.0")) drives the J813's 27.2 DCP: with it the DCP boots, the panel comes up at 2560x1600 and sway presents on it. Admit 27.2.0 on T8142 as firmware 27, so that probe picks that table. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/dcp.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 61bef7cdc5782..c23dea8ed47fe 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -5260,11 +5260,12 @@ static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) * T8142 (J813) on macOS 27.0: the H17P interface written for its 26.x * firmware makes the 27.0 DCP firmware panic right after the first * hotplug ("ap_link: callback failed with: 0x5"). It runs the T6050's - * firmware-27 generation. + * firmware-27 generation, and so does 27.2, with the 27.2 method table. */ if (of_device_is_compatible(dev->of_node, "apple,t8142-dcp")) { ret = dcp_read_fw_version(dev, "apple,firmware-compat", compat_str); - if (ret >= 0 && !strcmp(compat_str, "27.0.0")) + if (ret >= 0 && (!strcmp(compat_str, "27.0.0") || + !strcmp(compat_str, "27.2.0"))) return DCP_FIRMWARE_V_27_0; } From 17229b971200d307915843e06dffeea6359a1435 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 12 Nov 2026 13:35:53 +0100 Subject: [PATCH 5/5] drm/apple: dcp: Give firmware 27.2 its dfb_ready_query The firmware 27.2 table has no ID for dfb_ready_query, so boot_5() skips the query on 27.2. The 27.3 trace sends it at A385, one above its 27.0 ID, as 27.2 moved set_create_DFB and vi_set_temperature_hint up by one. Give the 27.2 table A385, so 27.2 boots with the same sequence as 27.0. The J813 ran with this table on 27.2: boot_done() acknowledges the DCP's boot callback only after the query's reply, and the DCP booted and the panel came up. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/iomfb_v27.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v27.c b/drivers/gpu/drm/apple/iomfb_v27.c index 2b527f2f2bf2f..b8b9406ccb6a8 100644 --- a/drivers/gpu/drm/apple/iomfb_v27.c +++ b/drivers/gpu/drm/apple/iomfb_v27.c @@ -119,9 +119,9 @@ static const struct dcp_method_entry dcp_methods_27_0[dcpep_num_methods] = { * A400/A401, so IOMobileFramebufferAP moved up by 50 (start_signal A451, * get_color_remap_mode A477), and set_create_DFB and vi_set_temperature_hint * up by one. The UPPipe A0xx and A1xx IDs are unchanged. The firmware rejects - * an unknown tag with a panic ("link_rpc_lookup callback not found"), so - * dfb_ready_query, whose 27.2 tag has not been seen in a call, has no entry - * here and is not sent to this build (boot_5()). + * an unknown tag with a panic ("link_rpc_lookup callback not found"). + * dfb_ready_query is A385, one above its 27.0 ID: the 27.3 trace sends it + * there, and the J813's DCP answers it at A385 on 27.2. */ static const struct dcp_method_entry dcp_methods_27_2[dcpep_num_methods] = { IOMFB_METHOD("A000", dcpep_late_init_signal), @@ -147,6 +147,7 @@ static const struct dcp_method_entry dcp_methods_27_2[dcpep_num_methods] = { IOMFB_METHOD("A104", dcpep_set_dfb_dimensions), IOMFB_METHOD("A105", dcpep_commit_dfb_info), IOMFB_METHOD("A035", dcpep_dfb_query), + IOMFB_METHOD("A385", dcpep_dfb_ready_query), IOMFB_METHOD("A353", dcpep_pipe_query_353), IOMFB_METHOD("A352", dcpep_pipe_set_352), IOMFB_METHOD("A465", dcpep_pipe_cfg_415),